Skip to content

Unseed only the chunk, and split unseed! out of seed! - #821

Open
ChrisRackauckas-Claude wants to merge 3 commits into
JuliaDiff:masterfrom
ChrisRackauckas-Claude:narrow-unseed
Open

Unseed only the chunk, and split unseed! out of seed!#821
ChrisRackauckas-Claude wants to merge 3 commits into
JuliaDiff:masterfrom
ChrisRackauckas-Claude:narrow-unseed

Conversation

@ChrisRackauckas-Claude

@ChrisRackauckas-Claude ChrisRackauckas-Claude commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Standalone against master (previously stacked on #816; now independent of it — the two
touch the same function and will need a trivial conflict resolution whichever lands second).

In ForwardDiff 0.10, the chunk-mode "unseed" call seed!(duals, x, index, seed) wrote
exactly the N chunk elements starting at index. The 1.x rewrite changed it to write
from index to the end of the array:

idxs = Iterators.drop(structural_eachindex(duals, x), offset)
for idx in idxs   # no take(N) — runs to the end

Chunk mode calls this after every chunk (seed!(xdual, x, i) in
chunk_mode_gradient_expr / chunk_mode_jacobian_expr) purely to clear the chunk it
just seeded — the rest of the array is already unseeded (everything is zeroed once
up front, and every other chunk clears itself). So writing to the end does
Σ(n − i) ≈ n²/2N redundant dual writes per gradient/jacobian sweep. At n = 100000 with
chunk 12 and Dual{…,Float64,12} (104 bytes each) that is ~40 GB of memory traffic.

This restores the 0.10 behavior: write at most N elements starting at index
(Iterators.take(…, N)). The 3-arg seed! form has no other callers in the package.

Second commit, from review: the two seed! methods whose seed::Partials argument
defaulted to zero(Partials) were only ever called to unseed, so they are now
unseed!(duals, x) and unseed!(duals, x, index) with that argument dropped (it was
never passed outside the allocation test). The NTuple{N,Partials} seeding methods keep
the seed! name and are unchanged.

Benchmarks

Julia 1.12, gradient! of f(x) = sum(abs2, x) with default chunk 12, median of 11 runs:

master this PR
gradient! n=1000 502 μs 279 μs
gradient! n=10000 51.3 ms 30.4 ms
gradient! n=100000 5.39 s 2.99 s
unseed sweep only, n=100000 2.49 s 0.33 s

The remaining unseed cost is the Iterators.drop(structural_eachindex(…), index-1) walk,
which is still O(index) per chunk; #816's dense fast path removes that separately.

Tests

No new tests: chunk-vs-vector-mode consistency (including non-divisible chunk sizes and
UpperTriangular/Diagonal inputs) is covered by the existing suite, which passes
locally (9134/9134 on Julia 1.12). Verified additionally that chunked results match
full-chunk references for n ∈ {5, 12, 13, 24, 25, 100} × chunk ∈ {1, 3, 5, 12}, for
gradient, jacobian, and the mutating jacobian(f!, y, x) form, plus UpperTriangular
/ Diagonal structural inputs and a BigFloat (non-isbits) input.


Note

Opened as a draft by an agent on behalf of @ChrisRackauckas. Please ignore
until reviewed by @ChrisRackauckas.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.66%. Comparing base (37bab0a) to head (f512c61).

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #821      +/-   ##
==========================================
- Coverage   90.74%   90.66%   -0.09%     
==========================================
  Files          11       11              
  Lines        1070     1071       +1     
==========================================
  Hits          971      971              
- Misses         99      100       +1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

CI: all 19 Julia 1 / lts / min-patch jobs green. The 6 "Julia pre" failures are the pre-existing Julia 1.13-rc1 + JET issue that also fails on master — root-cause analysis in #816 (comment). This commit introduces no new failures relative to the #816 branch it stacks on.

@ChrisRackauckas
ChrisRackauckas marked this pull request as ready for review July 8, 2026 12:01
@ChrisRackauckas
ChrisRackauckas requested a review from devmotion July 8, 2026 12:01
@devmotion
devmotion marked this pull request as draft July 27, 2026 14:07
@devmotion

Copy link
Copy Markdown
Member

Marked as draft since it depends on #816. Seemingly could be decoupled if supposed to be reviewed more urgently.

The chunk-mode unseed call seed!(xdual, x, i) only needs to clear the
N-wide chunk seeded at i: the rest of the array is zeroed up front and
every other chunk clears itself. ForwardDiff 0.10 wrote exactly N
elements here; the 1.x rewrite made it write from i to the end of the
array, i.e. O(n^2/2N) redundant dual writes per chunked
gradient/jacobian sweep (~40 GB of memory traffic for gradient! of
100000 elements at chunk 12).

Write at most N elements starting at index. The 3-arg seed! form has no
other callers in the package.

gradient! of sum(abs2, x) at chunk 12: n=1000 502 -> 279 us,
n=100000 5.39 -> 2.99 s.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019GcNzbNzaHCqTm4W14eKhN
@ChrisRackauckas-Claude

Copy link
Copy Markdown
Contributor Author

Unstacked: force-pushed narrow-unseed to a single commit rebased directly onto current master (a26b375), dropping #816's commit. The change is now expressed against master's structural-only seed! (Iterators.take(…, N)); the dense-path variant of the same clamp lives in #816 and the two will conflict trivially in src/apiutils.jl depending on merge order.

Re-verified locally on the unstacked branch: full Pkg.test() passes (9134/9134, Julia 1.12) and chunked results match full-chunk references across n ∈ {5, 12, 13, 24, 25, 100} × chunk ∈ {1, 3, 5, 12}. Benchmarks in the description were re-measured against master alone (the old table's numbers were on top of #816).

Comment thread src/apiutils.jl Outdated
Comment on lines +109 to +111
# Writes at most N elements starting at `index`: chunk mode only ever needs to
# clear the N-wide chunk it just seeded, so writing through to the end of the
# array would be O(n) redundant work per chunk (O(n^2) per sweep).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment is confusing - albeit seemingly correct - since the function is not actually used to "seed" anything but to unseed existing seeds (to replace by zero partials). Can we decouple the unseeding and seeding calls? E.g. use unseed!(duals, x, index) instead of seed! with an implicit zero(Partials(...)) argument?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed — done in b8a9b17.

seed!(duals, x) and seed!(duals, x, index) are now unseed!(duals, x) and unseed!(duals, x, index), and I dropped the seed::Partials argument from them rather than keeping it as an implicit zero: nothing in the package ever passed it (the only caller of the explicit-seed form was AllocationsTest, which now exercises unseed! instead). The NTuple{N,Partials} methods keep the seed! name and are otherwise untouched, so the two operations are now separate functions rather than one function distinguished by a default argument.

I renamed the whole-array form too, not just the indexed one flagged here — seed!(xdual, x) before the chunk loop and seed!(ydual, y) for the output buffer are unseeding as well, so leaving that one as seed! would have kept exactly the confusion you're pointing at. Happy to narrow it back to the 3-arg method if you'd rather keep the diff minimal.

Verified locally after the rename: full Pkg.test() passes (9134/9134, Julia 1.12), including the allocation tests, and the benchmark numbers are unchanged (unseed sweep at n=100000: 2.49 s on master, 0.29 s here).

Review feedback: the zero-Partials default of seed! made the unseeding
calls read as seeding. Give them their own name and drop the seed
argument, which was never passed for those two methods (the only caller
of the explicit-seed form was the allocation test).

seed!(duals, x)        -> unseed!(duals, x)
seed!(duals, x, index) -> unseed!(duals, x, index)

The NTuple-of-seeds methods are unchanged.

Co-Authored-By: Chris Rackauckas <accounts@chrisrackauckas.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019GcNzbNzaHCqTm4W14eKhN
@ChrisRackauckas-Claude ChrisRackauckas-Claude changed the title Unseed only the chunk in 3-arg seed! Unseed only the chunk, and split unseed! out of seed! Jul 28, 2026
@devmotion
devmotion marked this pull request as ready for review July 29, 2026 07:52

@devmotion devmotion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Can you update the version number?

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.

3 participants