perf(corrfunc): chunk the near-field reduction to lift the memory ceiling#42
Merged
Merged
Conversation
…ling
soft_pair_counts_from_topology materialised the full per-pair soft-bin
weight tensors at once -- w_cross of shape (P, max_leaf, max_leaf, nbins)
and w_within of shape (L, ...) -- so peak memory scaled as
P*max_leaf^2*nbins and OOM'd above N~1e4-2e4 (value_and_grad ~doubled it).
Reduce the near field in fixed-size chunks over the leaf-pair axis via a
rematerialised jax.lax.scan (new _accumulate_block_pairs helper),
accumulating into the (nbins,) output so the (chunk, ml, ml, nbins)
weight tensor is the only large live intermediate; memory is now
O(chunk_size*max_leaf^2*nbins). jax.checkpoint on the scan body is
essential -- without it the reverse pass would stack every chunk's
residual and restore the O(P) ceiling this removes.
chunk_size is a new keyword-only parameter (default 1024) on
soft_pair_counts_from_topology and soft_pair_counts; a single-chunk fast
path keeps small-N results bit-identical to the previous code.
build_pair_topology and the far-field monopole path are unchanged.
Validated: forward + jax.grad bit-identical at N=1000 (theta in
{0,0.3,0.6}); tests/applications all pass; on an A100-40GB the estimator
now runs at N=1e5 (acc 0.5s / vgrad 0.9s) and N=1e6 (acc 7.7s / vgrad
15.7s) with no OOM, and validation errors match the committed 40GB run
at the overlapping sizes (estimator counts within 6e-16 relative).
Bench harness (no estimator-code change): validate_vs_baseline gains
--brute-force-max-n so the O(N^2) reference is skipped where it would
itself OOM (above it the theta=0 tree run, whose near field is exact, is
used as the reference); scaling right-sizes the oversized traversal
capacity buffers (1<<15 -> 1<<13, which preallocated ~8.6GiB and OOM'd
the *build* at 1e6) and warms the build_s timing.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
soft_pair_counts_from_topology(the differentiable corrfunc estimator) materialised the full per-pair soft-bin weight tensors at once:w_crossof shape(P, max_leaf, max_leaf, nbins),P= near leaf-pairsw_withinof shape(L, max_leaf, max_leaf, nbins)Peak memory scaled as
P·max_leaf²·nbins·8 Bwith no chunking, so it OOM'd above N≈1e4–2e4 (attheta=0,P= all leaf-pairs = O((N/leaf)²)).value_and_gradroughly doubled it.Fix
Reduce the near field in fixed-size chunks over the leaf-pair axis (both the cross-leaf
Paxis and the within-leafLaxis) via a rematerialisedjax.lax.scan— new_accumulate_block_pairshelper — accumulating into the(nbins,)output so the(chunk, max_leaf, max_leaf, nbins)weight tensor is the only large live intermediate. Memory is nowO(chunk_size·max_leaf²·nbins).Two correctness details:
jax.checkpointon the scan body is essential. Without it, reverse-mode would stack every chunk's distance/weight residual over the scan axis and restore the exact O(P) ceiling this removes. With it, backward recomputes each chunk, so peak grad memory ≈ one chunk.q ≤ chunk_size): reproduces the original reduction with no padding/scan, so small-N is bit-identical to the pre-fix code.chunk_sizeis a new keyword-only parameter (default1024) onsoft_pair_counts_from_topologyandsoft_pair_counts— backward-compatible.build_pair_topologyand the far-field monopole path are unchanged.Validation
jax.grad): default chunk = 0.0 abs diff vs the pre-fix implementation; scan path (torture-tested at chunk=7/128) matches to ~1e-15 relative (float64 ULP).pytest -o addopts="" tests/applications/: 14 passed.validate_vs_baseline.py --sizes 1000 10000 100000— no OOM; overlapping-size errors match the committed 40 GB run exactly (estimator counts within 6.08e-16 relative).scaling.py --sizes 10000 100000 1000000— no OOM: N=1e5 acc 0.51 s / vgrad 0.93 s; N=1e6 acc 7.7 s / vgrad 15.7 s.black+basedpyrightclean on the changed files.Bench harness (no estimator-code change)
Two pre-existing, non-estimator OOMs blocked the acceptance commands:
validate_vs_baseline.py— the O(N²) brute-force baseline OOMs at 1e5 (that's why the committed validation stopped at 1e4). Added--brute-force-max-n(default 20000); beyond it the θ=0 estimator (near field exact) is used as the reference, so the estimator is still exercised at 1e5. Also enlarged the undersized pair queue (θ=0 at 1e5 makes the whole field near).scaling.py— the traversal preallocated anum_nodes × (1<<15)far-interaction buffer ≈ 8.6 GiB at 1e6, OOMing the build before the estimator ran. Right-sized the caps to1<<13(still >40× the observed θ=0.5 peaks); list contents and results unchanged. Also warmed thebuild_stiming withtime_callableand recorded full build stats.🤖 Generated with Claude Code