Extend SDPA vector 2-pass kernel to query sequence lengths up to 16#3838
Extend SDPA vector 2-pass kernel to query sequence lengths up to 16#3838katlun-lgtm wants to merge 1 commit into
Conversation
The 2-pass vector kernel packed every (query head, query position) pair into its own simdgroup, capping the threadgroup at gqa_factor * qL <= 32 and forcing 8 < qL <= 48 onto the single-query-tile full attention kernel, which streams all keys serially per head (issue ml-explore#3826). Each simdgroup now processes up to 4 query rows serially (2 for head dims > 128 to bound register pressure), reusing the K row already in registers, so the block-parallel reduction over the key sequence is retained for the speculative-decoding verify window (qL ~ 4-16). Routing prefers the vector path for 8 < qL <= 16 when kL >= 1024.
|
Confirmed on M5 Max — the win transfers to the nax path, and it's a bit larger than your M3 Max numbers. Method: built the merge-base (
Boundaries behave exactly as intended on the nax path: So the mechanism you describe — replacing "one query tile per head, serial over S" with the 2-pass block-parallel-over-S reduction — carries over to the nax full-attn baseline unchanged, and the window recovers near-linear scaling in L (16k: L=16 patched 1.262 ms ≈ 1.67× the L=8 cost, vs 2.6× before). Your framing that One note for the description: the M5 gains at L=16 (1.56–1.72×) are meaningfully above the M3 L=16 numbers (1.28–1.31×), so the nax path benefits across the whole 9–16 window rather than tapering near 16 the way it does on M3. |
|
Thanks for the rigorous run — building both sides from source with NAX verified active (via the GEMM canary) makes this the comparison I couldn't produce, and I've folded your tables into the PR description in place of the "unverified on nax" caveat. Two of your datapoints are worth highlighting for reviewers:
Agreed that L=24–48 remains the steel split-K's job — on both machines those shapes sit back on the full-attention plateau by design, so that can be tackled independently of this PR. |
Proposed changes
Addresses #3826 (2.3–2.8× latency jump from L=8 to L=12 query rows, flat plateau until L≥48).
The 2-pass vector kernel packs every (query head, query position) pair into its own simdgroup, which caps the threadgroup at
gqa_factor * qL <= 32— for GQA 32/8 the wall sits exactly at qL=8. Anything above leaves the decode-shaped kernel and lands on the full-attention kernel, which launches one query tile per head and streams all S keys serially, so L=12 pays ~L=48 cost (mechanism detailed in the issue thread).This PR:
sdpa_vector_2pass_1process up to 4 consecutive query rows serially (2 for head dims > 128 to bound per-lane register state), reusing the K/V rows already in registers. The block-parallel reduction over the key sequence is retained, K/V is still read once per (block, threadgroup), and pass 2 is unchanged.n_per_simd == 1function constant, so decode (qL ≤ 8) codegen is unchanged.8 < qL <= 16to the vector path whenkL >= 1024(the speculative-decoding verify window from the issue); qL ≤ 8 and qL > 16 keep their existing routes. A first cut also moved qL 17–32, but that was slower than the steel path at L=32 on M3 Max, so v1 stops at 16.Benchmarks (M3 Max, fp16, D=128, mask=None, 40-iter timed, baseline/patched interleaved A/B/A/B — numbers stable across both rounds):
Decode shapes (qL ≤ 8, S 4096/32768, D 128 and 256) measure parity within noise, as expected from the unchanged single-row body.
Correctness, beyond the updated unit tests: swept qL {1,7,8,9,12,16,24,31,32} × gqa {1,4,8} × D {64,128,256} × masks {none, causal, bool, float} × S {512,4096,16384}, plus bf16, the transposed-query layout, and sinks (gqa {4,8} × qL {1,8,9,12,16}) against an unfused fp32 reference — all within fp16 tolerance (2.5e-3).
Independent validation on M5 Max (nax path) — confirmed by pierre427, who built the merge-base and this branch from source with the NAX kernels verified active, so the baseline genuinely takes
sdpa_full_self_attention_nax:Boundary behavior on nax matches the design: qL ≤ 8 decode unchanged (within noise), qL ≥ 24 returns to the existing full-attention route with no regression, correctness at fp16 noise (max abs err 1.4e-4). Notably the L=16 gains on M5 (1.56–1.72×) exceed the M3 Max ones (1.28–1.31×) — the nax path benefits across the whole 9–16 window.
Checklist
Put an
xin the boxes that apply.pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes (clang-format v21.1.8 as pinned)test_sdpa_few_querynow sweeps Lq {8,12,16} × Nkv {1,2} at kL=4096 through the new path)