Repository navigation
Fold mapreduce in element order unless op is known to commute - #1063
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1063 +/- ##
==========================================
- Coverage 94.79% 94.78% -0.02%
==========================================
Files 16 16
Lines 10012 10146 +134
==========================================
+ Hits 9491 9617 +126
- Misses 521 529 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
ViralBShah
left a comment
There was a problem hiding this comment.
Reviewed by Codex at 72463c7. One correctness regression found in the new ordered reduction path.
| # value and in type, which makes a run of any length that one value | ||
| function _unstoredvalue(f::F, op::G, ::Type{T}) where {F,G,T} | ||
| z = Base.reduce_first(op, f(zero(T))) | ||
| zz = op(z, z) |
There was a problem hiding this comment.
[P2] Avoid probing op(z, z) when there is only one unstored entry
mapreduce(_ -> zeros(2, 3), *, spzeros(1)) returns the 2×3 mapped value on the base revision and for dense input, but now throws DimensionMismatch here. The reduction contains only one element, so no matrix multiplication is required; the idempotence probe introduces a multiplication that is not part of the reduction. The same problem affects isolated zero runs whose mapped value cannot multiply by itself. Defer the probe until a run actually needs at least two copies, rather than requiring every mapped zero to support op(z, z).
There was a problem hiding this comment.
Fixed in 0c491d1. _unstoredvalue now calls op(z, z) only when the counts guarantee two adjacent unstored entries (2·nstored < nslices·(len − 1)), which is exactly when the reduction calls it anyway; otherwise a run of two or more is folded by repeated squaring without the idempotence shortcut. mapreduce(_ -> zeros(2, 3), *, spzeros(1)) returns the 2×3 value again, and isolated zeros between stored entries match dense.
Written by Claude Code.
72463c7 to
0c491d1
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tries are adjacent `op(z, z)` is not part of a reduction whose unstored entries all stand alone, and may throw there, as for `mapreduce(_ -> zeros(2, 3), *, spzeros(1))`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…each of them Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
11cfefd to
04d1460
Compare
Sparse
mapreducefolds the stored entries first and the unstored ones last, which gives a wrong result for anopthat does not commute. This happens onmainand on Julia 1.13, for a matrix, a column-subset view, a sparse vector,dims=1,dims=2andsparse=true.Before
After
An
opthat is not known to commute is now folded over the positions in element order. A run of unstored entries is folded by repeated squaring, in the typeopgives it, and joined to the stored entry after it, so the cost stays O(nnz) calls offplus O(log) calls ofopper run;f(0)is evaluated once. The fast kernels remain for+,*over real or complex values,min,max,|,&and Base's internal reducers._commutesbecomes one definition, overVarargtypes, shared bymapreduce(f, op, A)andmapreduce(f, op, A, B), so the two-array form now takes its fast kernel for|and&as well. Withsparse=truethe ordered case computes the dense reduction and stores the pattern the sparse kernels would produce.An
opthe package does not recognise, such as an anonymous(a, b) -> a + b, went through Base's vectorized reduction of the stored values onmain, and a strict fold in element order is 6 to 10 times slower than that. So the whole-array reduction first folds the stored values alone, pairwise and in their order, and tests in the same loop whetherf(0)is a two-sided identity ofopfor each of them (op(z, x) === xandop(x, z) === x, in value and in type). Where it is, the unstored entries drop out wherever they lie, by associativity alone, and that fold is the result;fis called once per stored entry. Where it is not, the ordered fold runs: at once forvcator a string product, which fail the test on the first entry, and after a wasted pass when the test fails late, as for a stored-0.0under+(0.0 + -0.0is0.0).Minimum of 30 runs, 10^6 stored entries in a 10^5 × 10^5 matrix
Aand in a length-10^7 vectorx. The recognised reducers are unchanged:mainsum(A)sum(A; dims=1)sum(A; dims=2)maximum(A; dims=2)mapreduce(x -> x + 1, +, A; dims=2)sum(x)With
add = (a, b) -> a + b, an unrecognisedopis 1.0 to 1.7 timesmainwhere the identity test holds, no slower where it fails on the first entry (x -> x + 1), and 10 times where it fails late:mainmapreduce(abs, add, A)mapreduce(identity, add, A)mapreduce(abs, add, x)mapreduce(identity, add, x)mapreduce(identity, (a, b) -> max(a, b), x)mapreduce(x -> x + 1, add, x)mapreduce(identity, add, xm),xmbeingxwith its last stored value-0.0Along a dimension the kernels were not vectorized on
maineither, so they do not use the test.sparse=trueallocates the dense reduction as well:mainmapreduce(abs, add, A; dims=1, init=0.0)mapreduce(abs, add, A; dims=2, init=0.0)mapreduce(abs, add, A; dims=1, init=0.0, sparse=true)mapreduce(abs, add, A; dims=2, init=0.0, sparse=true)A sweep of 36000 random cases (vectors, matrices, column views, twelve
f/oppairs, stored zeros of either sign) agrees withfoldlover the dense array in value, sign and type.Left out: a non-associative
opstill differs from dense by grouping, which is outsidemapreduce's contract.Touches dispatch:
_commutes(internal) goes from two type arguments toVararg{Type,N}; no method of a Base function is added or widened.Split from #1022.
Backport: this fixes a wrong result, but it is a large change to the reduction kernels and slows some unrecognised reducers, so it is a doubtful backport candidate.
Written by Claude Code.
🤖 Generated with Claude Code