Skip to content

DArray: Infer the result type of reductions through f - #767

Merged
jpsamaroo merged 4 commits into
JuliaParallel:masterfrom
aalquwayfili:mapreduce-result-eltype
Oct 7, 2026
Merged

jpsamaroo merged 4 commits into
JuliaParallel:masterfrom
aalquwayfili:mapreduce-result-eltype

Conversation

@aalquwayfili

Copy link
Copy Markdown
Contributor

Fixes #764.

MapReduce took its eltype from op_outer(T, T) with T the input eltype, although op_outer combines what f returns. Four commits:

  1. Keep var/std in the input precision with dims. OnlineStats' value(::Variance) is inferred as Union{S,Float64}, because it multiplies by a Float64 NaN when it has no finite mean. Without this the next commit would give var of a Float32 array that union as its eltype.
  2. Infer the eltype through f. Fixes the wrong type of sum(x->x>0.5, DA), the InexactError of sum(x->complex(x, 1.0), DA), and the Any eltype of mean/var/std and maximum(abs, DC) with dims. An init takes part in the inference, so sum(f, DA; init=0.0) keeps the wider type. _onlinestats_mapreduce now takes the statistic as ::Type{S}, otherwise Julia 1.11 does not infer the closure.
  3. Do not wrap a BitArray partial reduction. to_array only matched Array, so all(f, DA; dims=1) returned a matrix of BitMatrix, and any/prod/maximum with a Bool valued f threw. Add Reactant optimization support #738 changes the same line the same way, so whichever lands second just drops it.
  4. Give extrema with dims a concrete eltype. It was Tuple{Any,Any}. This one is separate from the bug and easy to drop if you would rather not have the extra _mapped_type method.

The new tests compare types as well as values, since isapprox accepts a Matrix{Any}. With the source of master, about half of them fail or error (50 of 96, counted before I added the two init cases).

array/mapreduce passes on Julia 1.10, 1.11 and 1.12 (421 tests, 3 workers), and each commit passes on its own. Before the init change I also ran broadcast, indexing, copyto, permute, allocation, linalg/core and linalg/arithmetic on 1.12, and all passed.

Nothing here adds work per element. The only new cost is one more Base._return_type call per reduction.

array/core has one error in map! on my machine with Julia 1.12.7. It is the same on master, so I left it alone.

OnlineStats' `value(::Variance)` multiplies by a `Float64` `NaN` literal
when it has no finite mean, so it is inferred as `Union{S,Float64}` for
any other precision `S`. Convert the value back to `S`, so that a
reduction over `dims` can carry a concrete eltype.

Nothing visible changes yet: the result eltype is `Any` until the
reduction infers its eltype through `f`.
`MapReduce` took its eltype from `op_outer(T, T)`, where `T` is the input
eltype. But `op_outer` combines what `f` returns, not the input elements.

When `f` changes the type, the result was labelled with the wrong eltype
and `collect` converted to it. For a `Float64` matrix `A` and a
`ComplexF64` matrix `C`:

    sum(x->x>0.5, A)            # 13.0 instead of 13
    sum(abs2, C)                # ComplexF64 instead of Float64
    sum(x->complex(x, 1.0), A)  # InexactError

When `op_outer` has no method for `T` at all, the eltype fell back to
`Any`: `mean`/`var`/`std` with `dims` (`merge` of two numbers), and
`maximum(abs, C; dims)` (`max` of two complex numbers).

`_onlinestats_mapreduce` now takes the statistic as a `::Type{S}`. Julia
1.11 does not infer what the closure returns when it captures the type
from an untyped argument.

An `init` takes part in the inference, so `sum(f, A; init=0.0)` keeps
the type of the `init` when that is the wider one.

The existing tests compare with `isapprox`, which accepts a `Matrix{Any}`
and a `13.0`, so the new ones also compare the types.
The tree reduction broadcasts `op_outer` over two partial results. When
those hold `Bool`s the broadcast returns a `BitArray`, which `to_array`
did not take for an array, so it wrapped it in a 1-element `Array`.

For a matrix split into several partitions, `all(f, A; dims=1)` returned
a matrix of `BitMatrix`, and `any`, `prod` and `maximum` with a `Bool`
valued `f` threw.
`extrema` reduces `Extrema` values, which `_extrema_inner` builds from
what `f` returns. `_extrema_outer` has no method for the bare values, so
`extrema(A; dims=1)` had the eltype `Tuple{Any,Any}`. Tell `MapReduce`
what the partial results hold.
@aalquwayfili

Copy link
Copy Markdown
Contributor Author

Heads up, one thing I saw while benchmarking this. On Julia 1.12.7 with 8 threads, the first collect(mean(DA; dims=1)) got stuck one time and never came back. The main task was sitting in Base.promote_op in stage(::Map), and the workers were all waiting on the type inference lock.

I tried to make it happen again and couldn't. 40 more runs on this branch and 24 on master all finished fine. So I'm not sure if this PR is the cause. It might make it a bit more likely, because promote_op gets a concrete eltype there now and before it was just Any.

@jpsamaroo

Copy link
Copy Markdown
Member

That sounds like a Julia bug (and I've seen stuff like this as well around calls into the compiler), and with it appearing to be quite rare, I'm not too worried about it right until it becomes a noticeable issue (and stuff like this tends to get fixed upstream anyway, so it may just "go away" without any action by us). Regardless, I much appreciate the report, and I'll keep an eye out for it! If you do manage to reproduce it with frequency, let me know 😄

Anyway, this PR is great, thank you!

@jpsamaroo
jpsamaroo merged commit 0238175 into JuliaParallel:master Oct 7, 2026
42 of 57 checks passed
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.

Reductions on a DArray return the wrong type when f changes the element type

2 participants