Skip to content

Static backend selection: sumtype DynamicBackend + withbackend - #313

Merged
meggart merged 4 commits into
fg/compute_backendsfrom
as/static_backends
Sep 21, 2026
Merged

meggart merged 4 commits into
fg/compute_backendsfrom
as/static_backends

Conversation

@asinghvi17

@asinghvi17 asinghvi17 commented Sep 18, 2026 •

Copy link
Copy Markdown
Member

Stacked on #308, actions my review comments there. Companion PR for DiskArrayEngine: meggart/DiskArrayEngine.jl#70.

Changes

  • DiskArrayEngineBackend is defined in DiskArrays. A MethodError hint points to using DiskArrayEngine when its methods are missing, or says that DiskArrayEngine does not implement the operation when it is already loaded. This removes the DiskArrayEngine dep and DiskArrayEngineExt, which only imported the type (and made load_backend error for the "DiskArrayEngine" preference, since the extension cannot be loaded yet while DiskArrays precompiles).
  • DynamicBackend is a sum type: current_backend::Union{DefaultBackend,DiskArrayEngineBackend}. Dispatch is union-split into an isa branch.
  • The global backend is always a DynamicBackend, since the branch costs nothing measurable (below). The preference only selects the backend that is active at load time, so the "dynamic" value is gone. set_dynamic_backend!(backend) switches for the session, set_backend(name) switches and saves the preference, no restart needed.
  • withbackend(array, backend) attaches any other backend to an array via a WithBackendDiskArray wrapper. Entry points now call get_backend(array), which walks up parent until it finds the wrapper, stops at parent(a) === a, and falls back to the global backend. BroadcastStyle only sees the type, so it still uses the global backend.
  • Entry points specialize on f. sum(f::Function, a; kwargs...) and friends only pass f through, so Julia did not specialize on it and the inner call was a dynamic dispatch. Unrelated to backends, but it was the only overhead left in this path.

Overhead

2-element AccessCountDiskArray, Julia 1.13, minimum times. The global backend is a DynamicBackend in every row but the last two.

call time allocs
sum(a) 61 ns 7
diskarrays_sum_impl(identity, a, get_backend(a)) 61 ns 7
diskarrays_sum_impl(identity, a, DefaultBackend()) (hard-coded) 62 ns 7
sum(a) before specializing on f 142 ns 11
get_backend(view(withbackend(a, b), 1:2)) 1 ns 0

sum, mapreduce and sum(a .+ 1) all infer Float64, typeof(a .+ 1) infers concretely, and sum(a) contains no dynamic calls.

For comparison, the abstractly typed field from #308, with 4 extra backend methods defined to mimic a loaded ecosystem (with a single method the compiler devirtualizes either design), median times:

backend time allocs inferred dynamic calls
static DefaultBackend 110 ns 7 Float64 0
DynamicBackend (Union, this PR) 126 ns 7 Float64 0
abstract field (#308) 374 ns 12 Any 1

Tests

test_backend_dispatch no longer needs a preference: it uses withbackend with the TestBackend, checks lookup through a view, runtime switching of the global backend, type stability through DynamicBackend, and the error hint.

Note that test/backend_tests.jl already fails on #308 itself, before reaching this testset (flaky @test_broken unique(...) unexpected passes, median(da, dims=2) on a vector, unqualified mapreducedim!, cumsum without dims, exact == on float sums, count on a float array, and mapreduce(...; dims=1, init=0) on float data, which throws the same InexactError for a plain Array in Base on Julia 1.10, 1.12 and 1.13, so that is not a DiskArrays bug). I left those alone. With them bypassed locally, the dispatch testset and the rest of runtests.jl (including Aqua) pass.

🤖 Generated with Claude Code

https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh

- Define `DiskArrayEngineBackend` in DiskArrays itself and add a MethodError
  hint pointing to DiskArrayEngine.jl. Drops the DiskArrayEngine dependency
  and extension, which only existed to import the type.
- `DynamicBackend` now holds a `Union{DefaultBackend,DiskArrayEngineBackend}`
  so dispatch on it is union-split into a branch instead of a dynamic
  dispatch. `set_dynamic_backend!` switches it at runtime.
- Backends unknown to DiskArrays are attached per array with
  `withbackend(array, backend)`. Entry points find the backend with
  `get_backend(array)`, which walks up `parent` until `parent(a) === a`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh
@asinghvi17
asinghvi17 added this pull request to stack #314 September 18, 2026 15:46
@asinghvi17 asinghvi17 changed the title Make backend selection static: sumtype DynamicBackend, withbackend Static backend selection: sumtype DynamicBackend + withbackend Sep 18, 2026
@asinghvi17
asinghvi17 marked this pull request as ready for review September 18, 2026 15:54
asinghvi17 and others added 2 commits September 18, 2026 12:00
The union-split branch costs ~nothing (measured: same time and allocations
as a hard-coded DefaultBackend), so runtime switching no longer needs a
"dynamic" preference. The preference now only selects the backend that is
active at load time, and `set_backend` also switches the running session
instead of asking for a restart.

Also force specialization on `f` in the `sum`/`prod`/.../`extrema`/`mean`
entry points. `f::Function` is only passed through there, so Julia did not
specialize on it and the inner call was a dynamic dispatch: `sum(a)` on a
2-element array goes from 142 ns / 11 allocs to 61 ns / 7 allocs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh
- `unique` visits chunks in a different order than an Array's elements, so
  test with `issetequal` (of the `f` values for `unique(f, x)`, where the
  first representative differs) instead of `@test_broken ... ==`, which
  passed or failed depending on the random data.
- Qualify `Base.mapreducedim!`, size the output from `ndims` instead of
  assuming 3 dimensions, and start the reference from `zero(R)` instead of
  uninitialized memory.
- Only test `median(dims=2)` for ndims >= 2 and `cumsum`/`cumprod` without
  `dims` for vectors.
- `init=0.0` for float data, an Int `init` with `dims` throws in Base too.
- Compare chunked float reductions with `≈`, and do not `count` floats.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh
@meggart

meggart commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Thanks a lot. I agree on all of them, but am still a bit unsure about

withbackend(array, backend) attaches any other backend to an array via a WithBackendDiskArray wrapper. Entry points now call get_backend(array), which walks up parent until it finds the wrapper, stops at parent(a) === a, and falls back to the global backend. BroadcastStyle only sees the type, so it still uses the global backend.

It just feels a bit un-natural to me to tie the way you do processing to an array. IMO it would feel much more natural to tie processing engine to an operation instead of to the data. So what about just adding a keyword argument to all user-callable functions defined here to set a backend optionally?

Regarding naming, when I saw withbackend I was very much reminded of the python with and my first expectation was that it would allow something like:

withbackend(DiskArrayEngineBackend) do
  # Inside this block, global backend is set do DAE
  b = sum(a)
  c = count(a)
end

which would be very neat to have and which I would prefer over wrapping arrays.

Also, I intended to add support for multi-arg mapreduce later to avoid complicated zipping, which would make backend selection a bit harder in the future if we select compute backends based on the data.

@meggart
meggart merged commit 9d2bddc into main Sep 21, 2026
meggart pushed a commit to meggart/DiskArrayEngine.jl that referenced this pull request Sep 21, 2026
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ra7EAJeFE1dtekwf4Tzxh
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants