From 7bdabb6c830e7035437eb129120d1c29926a23fa Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Fri, 18 Sep 2026 12:15:52 -0400 Subject: [PATCH 1/3] Use the `DiskArrayEngineBackend` type defined in DiskArrays DiskArrays now owns the backend type (JuliaIO/DiskArrays.jl#313), so that it can be a member of its `DynamicBackend` sum type. We only add methods. - Type `a` in the reduction impls to resolve an ambiguity with the `::ComputeBackend` fallbacks in DiskArrays. - Tests select the backend per array with `withbackend` instead of relying on the backend preference, and import `AccessCountDiskArray` from `DiskArrays.TestTypes`. - Temporary `[sources]` entry pointing to the DiskArrays branch. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh --- Project.toml | 4 ++++ src/enginearrays.jl | 7 ++++--- test/test_compute_backend.jl | 6 ++++-- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/Project.toml b/Project.toml index bbec3e0..cbbdb00 100644 --- a/Project.toml +++ b/Project.toml @@ -26,6 +26,10 @@ Zarr = "0a941bbe-ad1d-11e8-39d9-ab76183a1d99" [weakdeps] Dagger = "d58978e5-989f-55fb-8d15-ea34adc7bf54" +# TODO remove once JuliaIO/DiskArrays.jl#313 is merged and released +[sources] +DiskArrays = {url = "https://github.com/JuliaIO/DiskArrays.jl", rev = "as/static_backends"} + [extensions] DaggerExt = "Dagger" diff --git a/src/enginearrays.jl b/src/enginearrays.jl index 131549b..6e38703 100644 --- a/src/enginearrays.jl +++ b/src/enginearrays.jl @@ -3,8 +3,8 @@ using OnlineStats: OnlineStats using Statistics export engine, compute, compute! -import DiskArrays: ComputeBackend -struct DiskArrayEngineBackend <: ComputeBackend end +# The backend type is owned by DiskArrays, we only add methods for it +import DiskArrays: DiskArrayEngineBackend bcdims(p) = ntuple(identity,ndims(p)) function collect_bcdims(A) @@ -100,7 +100,8 @@ wrap_reduction(a::OnlineStats.OnlineStat) = OnlineStats.value(a) for func in (:maximum, :minimum, :sum, :extrema, :median, :mean) fname = Symbol("diskarrays_$(func)_impl") eval(quote - function DiskArrays.$(fname)(ff, a, ::DiskArrayEngineBackend; dims=:, skipmissing=false, strategy=:auto) + # `a::AbstractDiskArray` avoids an ambiguity with the `::ComputeBackend` fallback in DiskArrays + function DiskArrays.$(fname)(ff, a::A, ::DiskArrayEngineBackend; dims=:, skipmissing=false, strategy=:auto) where {A<:AbstractDiskArray} if dims === Colon() dimspec = ntuple(i->i=>nothing, ndims(a)) res = aggregate_diskarray(a, $func, dimspec; skipmissing, preproc=ff, strategy) diff --git a/test/test_compute_backend.jl b/test/test_compute_backend.jl index 3253806..e233cb1 100644 --- a/test/test_compute_backend.jl +++ b/test/test_compute_backend.jl @@ -1,5 +1,6 @@ using DiskArrayEngine -using DiskArrays: AbstractDiskArray, AccessCountDiskArray +using DiskArrays: AbstractDiskArray, DiskArrayEngineBackend, withbackend +using DiskArrays.TestTypes: AccessCountDiskArray using Statistics using Test @@ -9,7 +10,8 @@ import DiskArrayEngine as DAE function make_arrays(data; chunksize=size(data)) mat = data - da = AccessCountDiskArray(data; chunksize=chunksize) + # Select the backend per array, so the tests do not depend on the backend preference + da = withbackend(AccessCountDiskArray(data; chunksize=chunksize), DiskArrayEngineBackend()) return (materialized=mat, disk=da) end From d18414076c98df9e548fe79cd1fef1370777becf Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 20 Sep 2026 13:07:54 -0400 Subject: [PATCH 2/3] Fix mistakes in the compute backend tests - Qualify `Base.mapreducedim!`, size the output from `ndims` instead of assuming 3 dimensions, and start the reference from `zero(R)` instead of uninitialized memory. - `init=0.0` for float data, an Int `init` with `dims` throws in Base too. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh --- test/test_compute_backend.jl | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/test/test_compute_backend.jl b/test/test_compute_backend.jl index e233cb1..a1ffed3 100644 --- a/test/test_compute_backend.jl +++ b/test/test_compute_backend.jl @@ -103,16 +103,19 @@ function test_mapreduce(data; chunksize=ntuple(i -> max(1, size(data)[i] ÷ 2), @testset "mapreduce (init)" begin @test Array(mapreduce(identity, +, da; init=0)) ≈ mapreduce(identity, +, mat; init=0) @test Array(mapreduce(identity, *, da; init=1)) ≈ mapreduce(identity, *, mat; init=1) - @test Array(mapreduce(identity, +, da; dims=1, init=0)) ≈ mapreduce(identity, +, mat; dims=1, init=0) + # An Int `init` with float data and `dims` is an InexactError in Base as well + @test Array(mapreduce(identity, +, da; dims=1, init=0.0)) ≈ mapreduce(identity, +, mat; dims=1, init=0.0) end @testset "mapreducedim!" begin - R = zeros(size(da, 1), size(da, 2), 1) - mapreducedim!(x -> 2x, +, R, da) - @test R ≈ mapreducedim!(x -> 2x, +, similar(R, size(da, 1), size(da, 2), 1), mat) - R2 = zeros(1, size(da, 2), size(da, 3)) - mapreducedim!(x -> x^2, +, R2, da) - @test R2 ≈ mapreducedim!(x -> x^2, +, similar(R2), mat) + # Reduce over the last and over the first dimension + redsize(d) = ntuple(i -> i == d ? 1 : size(mat, i), ndims(mat)) + R = zeros(redsize(ndims(mat))) + Base.mapreducedim!(x -> 2x, +, R, da) + @test R ≈ Base.mapreducedim!(x -> 2x, +, zero(R), mat) + R2 = zeros(redsize(1)) + Base.mapreducedim!(x -> x^2, +, R2, da) + @test R2 ≈ Base.mapreducedim!(x -> x^2, +, zero(R2), mat) end @testset "mapfoldl (no init)" begin From 1fed2f10fe7f977ab723a09cc4e726839c5006d6 Mon Sep 17 00:00:00 2001 From: Anshul Singhvi Date: Sun, 20 Sep 2026 13:45:38 -0400 Subject: [PATCH 3/3] Fix the DiskArrays backend implementation so the test suite passes - Reductions over all dimensions return a scalar like in Base, instead of the 1x...x1 array from `compute`. Same for `mapreduce` without `dims`. - `mapreduce` applied only `op` and never `f`. - Without `init`, `mapreduce` got DiskArrays' `_InitialValue` sentinel to fill the buffers with. Use a neutral element of `op` instead. - `value(::OnlineStats.Extrema)` is a NamedTuple `(min, max, nmin, nmax)`, so `maximum` with `strategy=:reduce` returned `nmax`, a count, and `extrema` failed to convert. Use the accessors of the stat. This fixes the `@test_broken` "gives all ones for some reason" in the Aggregate tests. - `extrema` defaults to `strategy=:reduce`, the direct aggregator broadcasts its result into the output, which fails for a Tuple. - Tests: scalar results for `mapreduce` without `dims`, an associative `op`, and regression tests for the OnlineStats path. - Point `[sources]` to the DiskArrays branch with the `count` fallback. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh --- Project.toml | 4 ++-- src/enginearrays.jl | 24 +++++++++++++++++++----- src/util/onlinestats_helpers.jl | 7 ++++--- test/test_compute_backend.jl | 10 +++++++--- test/test_interpolation.jl | 3 +-- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/Project.toml b/Project.toml index cbbdb00..a31dbe7 100644 --- a/Project.toml +++ b/Project.toml @@ -26,9 +26,9 @@ Zarr = "0a941bbe-ad1d-11e8-39d9-ab76183a1d99" [weakdeps] Dagger = "d58978e5-989f-55fb-8d15-ea34adc7bf54" -# TODO remove once JuliaIO/DiskArrays.jl#313 is merged and released +# TODO remove once JuliaIO/DiskArrays.jl#315 is merged and released [sources] -DiskArrays = {url = "https://github.com/JuliaIO/DiskArrays.jl", rev = "as/static_backends"} +DiskArrays = {url = "https://github.com/JuliaIO/DiskArrays.jl", rev = "as/backend_fallbacks"} [extensions] DaggerExt = "Dagger" diff --git a/src/enginearrays.jl b/src/enginearrays.jl index 6e38703..506edb8 100644 --- a/src/enginearrays.jl +++ b/src/enginearrays.jl @@ -34,14 +34,26 @@ function collect_bcdims(A) last.(oc), first.(oc) end -DiskArrays.diskarrays_mapreduce_impl(f, op, a, dims, init, ::DiskArrayEngineBackend) = - mapreduce_engine(f, op, a; dims, init) +function DiskArrays.diskarrays_mapreduce_impl(f, op, a, dims, init, ::DiskArrayEngineBackend) + res = mapreduce_engine(f, op, a; dims, init=neutral_element(f, op, a, init)) + # Like in Base, a reduction over all dimensions returns a scalar + dims === Colon() ? only(compute(res)) : res +end + +# The output buffers are initialized with `init`, so without one we need a neutral element of `op` +neutral_element(f, op, a, init) = init +function neutral_element(f, op, a, ::Base._InitialValue) + T = Base.promote_op(f, eltype(a)) + op === max && return typemin(T) + op === min && return typemax(T) + Base.reduce_empty(op, T) +end struct _DAEReducer{F,R} <: Function f::F r::R end -(d::_DAEReducer{F,R})(xout, xin...) where {F,R} = xout[] = d.r(xout[], xin...) +(d::_DAEReducer{F,R})(xout, xin...) where {F,R} = xout[] = d.r(xout[], d.f(xin...)) function mapreduce_engine(f, op, A...; dims=:, init=nothing, fin=identity) @@ -99,14 +111,16 @@ wrap_reduction(a::OnlineStats.OnlineStat) = OnlineStats.value(a) for func in (:maximum, :minimum, :sum, :extrema, :median, :mean) fname = Symbol("diskarrays_$(func)_impl") + # The direct aggregator broadcasts its result into the output, which does not work for the Tuple from `extrema` + default_strategy = QuoteNode(func === :extrema ? :reduce : :auto) eval(quote # `a::AbstractDiskArray` avoids an ambiguity with the `::ComputeBackend` fallback in DiskArrays - function DiskArrays.$(fname)(ff, a::A, ::DiskArrayEngineBackend; dims=:, skipmissing=false, strategy=:auto) where {A<:AbstractDiskArray} + function DiskArrays.$(fname)(ff, a::A, ::DiskArrayEngineBackend; dims=:, skipmissing=false, strategy=$default_strategy) where {A<:AbstractDiskArray} if dims === Colon() dimspec = ntuple(i->i=>nothing, ndims(a)) res = aggregate_diskarray(a, $func, dimspec; skipmissing, preproc=ff, strategy) - compute(res) + only(compute(res)) else dimspec = ntuple(i->i=>nothing, dims) aggregate_diskarray(a, $func, dimspec; skipmissing, preproc=ff, strategy) diff --git a/src/util/onlinestats_helpers.jl b/src/util/onlinestats_helpers.jl index 0172e34..1bc04a6 100644 --- a/src/util/onlinestats_helpers.jl +++ b/src/util/onlinestats_helpers.jl @@ -44,8 +44,9 @@ has_onlineversion(f) = f in keys(func_to_online) const func_to_online = Dict([ mean => (OnlineStats.Mean,Union{Float64,Missing}), sum => (OnlineStats.Sum,Union{Float64,Missing}), - extrema => (OnlineStats.Extrema, Union{Tuple{Float64,Float64},Missing}), - maximum => (DerivedOnlineStat{OnlineStats.Extrema,v -> last(OnlineStats.value(v)),OnlineStats.fit!,()}, Union{Float64,Missing}), - minimum => (DerivedOnlineStat{OnlineStats.Extrema,v -> first(OnlineStats.value(v)),OnlineStats.fit!,()}, Union{Float64,Missing}), + # `value(::Extrema)` is a NamedTuple `(min, max, nmin, nmax)`, so use the accessors instead of `first`/`last` + extrema => (DerivedOnlineStat{OnlineStats.Extrema,extrema,OnlineStats.fit!,()}, Union{Tuple{Float64,Float64},Missing}), + maximum => (DerivedOnlineStat{OnlineStats.Extrema,maximum,OnlineStats.fit!,()}, Union{Float64,Missing}), + minimum => (DerivedOnlineStat{OnlineStats.Extrema,minimum,OnlineStats.fit!,()}, Union{Float64,Missing}), median => (DerivedOnlineStat{OnlineStats.ExpandingHist,OnlineStats.median,OnlineStats.fit!,(200,)}, Union{Float64,Missing}) ]) diff --git a/test/test_compute_backend.jl b/test/test_compute_backend.jl index a1ffed3..4d3b97d 100644 --- a/test/test_compute_backend.jl +++ b/test/test_compute_backend.jl @@ -40,6 +40,9 @@ function test_scalar_reductions(data; chunksize=ntuple(i -> max(1, size(data)[i] @test maximum(da) ≈ maximum(mat) @test maximum(identity, da) ≈ maximum(mat) @test maximum(x -> abs(x), da) ≈ maximum(x -> abs(x), mat) + # The OnlineStats path, `:auto` mostly picks the direct one for these small arrays + @test minimum(da; strategy=:reduce) ≈ minimum(mat) + @test maximum(da; strategy=:reduce) ≈ maximum(mat) end @testset "extrema" begin @@ -92,7 +95,8 @@ function test_mapreduce(data; chunksize=ntuple(i -> max(1, size(data)[i] ÷ 2), @testset "mapreduce (no dims, no init)" begin @test mapreduce(x -> 2x, +, da) ≈ mapreduce(x -> 2x, +, mat) - @test mapreduce(*, -, da) ≈ mapreduce(*, -, mat) + # `op` must be associative, chunks are reduced in an arbitrary order + @test mapreduce(abs, max, da) ≈ mapreduce(abs, max, mat) end @testset "mapreduce (dims=)" begin @@ -101,8 +105,8 @@ function test_mapreduce(data; chunksize=ntuple(i -> max(1, size(data)[i] ÷ 2), end @testset "mapreduce (init)" begin - @test Array(mapreduce(identity, +, da; init=0)) ≈ mapreduce(identity, +, mat; init=0) - @test Array(mapreduce(identity, *, da; init=1)) ≈ mapreduce(identity, *, mat; init=1) + @test mapreduce(identity, +, da; init=0) ≈ mapreduce(identity, +, mat; init=0) + @test mapreduce(identity, *, da; init=1) ≈ mapreduce(identity, *, mat; init=1) # An Int `init` with float data and `dims` is an InexactError in Base as well @test Array(mapreduce(identity, +, da; dims=1, init=0.0)) ≈ mapreduce(identity, +, mat; dims=1, init=0.0) end diff --git a/test/test_interpolation.jl b/test/test_interpolation.jl index e6f60c2..f2521f4 100644 --- a/test/test_interpolation.jl +++ b/test/test_interpolation.jl @@ -26,8 +26,7 @@ end @test size(agg_mean) == (1,5,6) @test agg_mean[:,:,:] == mean(a, dims=1) agg_max = aggregate_diskarray(a, maximum, (2=>nothing,), strategy=:reduce) - # This gives all ones for some reason - @test_broken agg_max[:,:,:] == maximum(a, dims=2) + @test agg_max[:,:,:] == maximum(a, dims=2) agg_sec = aggregate_diskarray(a, mean, (2=>2,)) # This should work but currently throws a bounds error @test_throws BoundsError agg_sec[:,:,:]