Skip to content

perf(nearfield,m2l): fold the self-leaf block into the leaf-pair kernel; contention-free M2L chunk reduction - #339

Merged
TobiBu merged 20 commits into
mainfrom
perf/small-leaves
Sep 10, 2026
Merged

perf(nearfield,m2l): fold the self-leaf block into the leaf-pair kernel; contention-free M2L chunk reduction#339
TobiBu merged 20 commits into
mainfrom
perf/small-leaves

Conversation

@TobiBu

@TobiBu TobiBu commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Two independent single-GPU fused-lane changes from the small-leaves plan (docs/small_leaves_2026-09.md lands with the follow-up CSR M2L PR).

1. Self-leaf block folded into the leaf-pair Pallas kernel (nearfield_fused_leaf.py, include_self; flag JACCPOT_NEARFIELD_LEAFPAIR_FOLD_SELF, default on). One extra unconditional pass per program over its own leaf with the diagonal masked before the rsqrt, gated to chunk 0 on the source-chunk grid; replaces the lax.scan over leaves (_self_contributions) on the forward prepacked lane. The cvjp lane keeps self_acc + cvjp(...), so the gradient path is byte-identical. Kernel name gains _self. Time-neutral (the scan was already batched by #334): leaf-pair kernel 48.5 vs 48.2 ms at leaf 128, forces identical (aggL2 1.2011e-03 at leaf 64 either way).

2. _chunk_segment_scatter_add without duplicate-address atomics. The per-chunk M2L reduction sent every pair of a target to the same segment_sum row and every non-head slot (~3800 of 4096) to node 0 as a zero add; XLA's atomic scatter serialised both. At N=200k Plummer, leaf 64, theta 0.6 that was ONE fusion at 686 us x 247 launches = 169 ms of a 410 ms step. Now a segmented associative_scan reduces within the sorted segments with no scatter, and non-head slots carry an out-of-bounds index dropped by mode="drop", so the one remaining scatter has unique in-bounds indices.

Per step (strict_run_v2, N=200k, p=4, theta 0.6, idle A100):

leaf before after
256 131.8 ms 126.4
128 187.5 145.2
64 409.6 236.5
32 1168 649

Tests: fold == pairs + _self_contributions to fp32 summation order on both grids, subtile lane indexing, softening 0, wide accumulator, default-off byte parity, Triton (sm_80) run; scatter pinned to np.add.at on random chunks (padded tail, all-same-target, nothing valid), node 0 untouched unless targeted, deterministic, run on CPU and A100. Near-field suite 54/54 and strict-lane integration tests 17/17 on A100. The new sm_80 tests are not in _MUST_RUN_SM80: its registry test is bound to one module.

🤖 Generated with Claude Code

TobiBu and others added 17 commits September 7, 2026 09:59
…ernel

One extra unconditional pass per program over the program's own leaf, diagonal
masked before the rsqrt, gated to chunk 0 on the source-chunk grid, replaces
the lax.scan over leaves (_self_contributions) on the forward prepacked lane.
JACCPOT_NEARFIELD_LEAFPAIR_FOLD_SELF=0 restores the scan; the cvjp lane keeps
self_acc + cvjp(...) so the gradient path is byte-identical. Kernel name gains
_self so the two variants never alias in the compilation cache.

Tests: fold == pairs + _self_contributions to fp32 summation order on both grids,
subtile lane indexing, softening 0 (no NaN, potential lane), wide accumulator,
default-off byte parity, and a Triton (sm_80) run registered in the GPU gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_chunk_segment_scatter_add ran once per 4096-pair chunk and its two scatters
were pathological for XLA's atomic-add lowering: segment_sum sent every pair
of a target to the same group row, and the final .at[].add sent every
non-head slot (~3800 of 4096) to node 0 with a zero value. At leaf 64
(992k far pairs, 243 chunks/step) that was 169 ms of a 410 ms step, 686 us
per launch for a 4096 x 25 reduction. Now a segmented associative_scan
reduces within the sorted segments with no scatter, and non-head slots carry
an out-of-bounds sink index dropped by mode='drop', so the one remaining
scatter has unique in-bounds indices and no atomics.

Pinned to np.add.at on random chunks (padded tail, all-same-target, nothing
valid, degenerate width), node-0 untouched unless targeted, deterministic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…R by target)

One program per target node loops over its far-pair segment of a target-sorted
source list and builds rotate -> z-translate -> rotate-back on chip from the
two alignment angles: Dz as diag(cos |m| t) + A sin(|m| t) with a constant
antisymmetric pattern, the constant B stack per degree, and a separable z-core
Z = Zsf * outer(rinv^(n+1), rinv^k). Nothing per pair touches HBM except one
(Cp,) multipole row; the program owns its output row, so the chunked lane's
per-chunk argsort + segment_sum scatter disappear. Forward only.

Interpret-mode parity vs m2l_rot_scale_real_batch: < 1e-10 (fp64) for orders
2..6, < 3e-4 (fp32); random CSR with empty targets and a padded -1 tail,
active_pair_count truncation, on-axis deltas, and the wrapper under jit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…as no n-ary stack

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…icrobench

JACCPOT_STATIC_STRICT_FUSED_M2L_CSR=1 (Ampere+, or JACCPOT_M2L_CSR_INTERPRET=1
for CPU parity) routes _solidfmm_downward_accumulate_from_multipoles' flat real
lanes to m2l_real_csr_pallas: one program per target owns its local row, so the
chunked scan and its per-chunk scatter are gone. Wiring test counts the kernel
entry and pins the force to the chunked lane (rel < 2e-5 at N=3000, p=3).
bench/m2l_csr_microbench.py: ns/pair vs the pure-JAX chunked lane (degree-batched
off/on) at 1M and 6M pairs, orders 4 and 6 -- the plan's G2a gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…k, degree-only z-core

The first cut held two 128x32 one-hot pack/unpack matrices and a Cp x Cp z-core
in registers per program and ran at ~93 ns/pair on a shared A100 (non-record),
no faster than the pure-JAX lane. The multipole table is now packed into the
centred (Bp, Wp) layout once in the wrapper and the output unpacked once; the
z-translation preserves m, so its operator is a Bp x Bp degree matrix per
column. Half the per-pair MACs, a third of the constants; 4 warps per program.
Microbench: iterate the well-separation rejection (a coincident pair made the
reference NaN), assert finiteness. CSR sm_80 tests registered in the GPU gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…n the slow list (28 s on CPU)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…(the knob is trace-time)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ixes, per-step table, capacity rules

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… sm_80 tests run in the ordinary suite

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ernel

One extra unconditional pass per program over the program's own leaf, diagonal
masked before the rsqrt, gated to chunk 0 on the source-chunk grid, replaces
the lax.scan over leaves (_self_contributions) on the forward prepacked lane.
JACCPOT_NEARFIELD_LEAFPAIR_FOLD_SELF=0 restores the scan; the cvjp lane keeps
self_acc + cvjp(...) so the gradient path is byte-identical. Kernel name gains
_self so the two variants never alias in the compilation cache.

Tests: fold == pairs + _self_contributions to fp32 summation order on both grids,
subtile lane indexing, softening 0 (no NaN, potential lane), wide accumulator,
default-off byte parity, and a Triton (sm_80) run registered in the GPU gate.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
_chunk_segment_scatter_add ran once per 4096-pair chunk and its two scatters
were pathological for XLA's atomic-add lowering: segment_sum sent every pair
of a target to the same group row, and the final .at[].add sent every
non-head slot (~3800 of 4096) to node 0 with a zero value. At leaf 64
(992k far pairs, 243 chunks/step) that was 169 ms of a 410 ms step, 686 us
per launch for a 4096 x 25 reduction. Now a segmented associative_scan
reduces within the sorted segments with no scatter, and non-head slots carry
an out-of-bounds sink index dropped by mode='drop', so the one remaining
scatter has unique in-bounds indices and no atomics.

Pinned to np.add.at on random chunks (padded tail, all-same-target, nothing
valid, degenerate width), node-0 untouched unless targeted, deterministic.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
pre-commit-ci Bot and others added 3 commits September 9, 2026 22:46
CI on #340: test_type_annotation_guard flagged the inner `one`/`body` closures;
pydoclint wanted Raises sections on m2l_real_csr_tables / m2l_real_csr_pallas
and full docstrings on _bapply/_dz; black reformatted four files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
feat(m2l): target-tiled CSR Pallas M2L kernel with on-chip rotations (opt-in lane)
@TobiBu
TobiBu added this pull request to stack #342 September 10, 2026 19:34
@TobiBu
TobiBu merged commit f970c98 into main Sep 10, 2026
20 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.

1 participant