Repository navigation
Add a multi-row 2-pass SDPA vector kernel for 8 < qL <= 16 - #3838
katlun-lgtm wants to merge 4 commits into
Conversation
|
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. |
|
Bumping this one — it's been open since July 13 with no CI run against it. The workflow run for On the substance, the main thing that changed since the description was written: @pierre427, who filed #3826, rebuilt both sides from source on an M5 Max with NAX verified active and reproduced the speedup independently — 1.95× / 2.19× at L=12 and 1.56× / 1.72× at L=16 (S=16k/32k, @angeloskath, you picked up #3875 in this area recently — are you the right person for this one, or should I be pointing it at someone else? |
zcbenz
left a comment
There was a problem hiding this comment.
It seems that the code does a lot more than the title "Extend SDPA vector 2-pass kernel to query sequence lengths up to 16"? We can only verify performance changes when it only improves a single thing.
Adds sdpa_vector_2pass_1_multirow: each simdgroup owns up to 4 consecutive query rows (2 for D > 128), reading each K/V row into registers once and reusing it across the rows, so the threadgroup fits the 1024-thread limit where a row-per-simdgroup layout would not. Routed for 8 < q_seq_len <= 16 when the keys are long enough that the block-parallel reduction over the sequence beats the full kernel's single query tile (the same S >= 1024 threshold the decode routing uses for the 2-pass split), or when the full kernel does not support the head dim. The existing kernels, their dispatch, and the q_seq_len <= 8 routing are untouched.
b09124a to
942a6d8
Compare
|
Fair point — the previous revision restructured the existing kernel into a dual body, so the decode path read as touched even where the codegen was meant to be unchanged. Reworked so the diff is one thing, purely additive:
Re-measured on the restructured branch (M3 Max, fp16, D=128, GQA 32/8, latency per call): L=9 goes 2.38 → 1.24 ms at S=16k and 4.51 → 2.10 ms at S=32k, tapering to 1.23–1.33× at L=16; L=8 and L≥17 measure parity, as they keep their existing routes. Full tables in the updated description. pierre427's M5 numbers in the description are from the previous revision — the multi-row body and the routing thresholds are unchanged by the restructure, only the packaging (in-place branch → separate kernel). Also retitled since "extend" no longer describes it. |
|
@jessegross Would you mind doing a quick check at whether this would improve MTP since you worked on qmv_wide which I assume optimize in a very similar way? This is a bit out of my scope. |
|
On the MTP question — I measured it on an M3 Max rather than guess. It helps where the kernel engages and does nothing where it doesn't, and the split turns out to be structural.
The reason n_kv ≤ 2 is flat is Method matched to #3837's so the numbers compare: bf16, D=128, n_q=32, L=32768, mask=None, warm 1 s then median of five 0.4 s wall-clock windows, eval per call, three repeats (spread under 5%, mostly under 1%). qL=1 is the control since that path is byte-identical here, and it lands at 1.00×. Baseline pip 0.32.0 against |
|
@zcbenz In my experience MTP is rarely effective when drafting 8 or more tokens at a time, especially at the longer context lengths where this is targeted (drafting 2-4 tokens is more typical). At long draft lengths the acceptance rate is very low for the last tokens and you spend more time verifying them against the target model than you gain from the cases where you do accept them. If you can optimize things really well so there is a very small incremental cost to verifying additional tokens in a batch then it would make sense to draft longer. So in that sense this could help but I don't think it is enough to make those draft lengths profitable by itself. |
|
Thanks @jessegross for sharing the production experience. Adding a new 2-pass SDPA vector kernel has non-trivial costs on both reviewing and maintenance, since it is not likely going to be used in production, I'm closing this PR, but still thanks a lot for experimenting the optimization. |
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 revision addresses the review: the change is purely additive and does one thing — give
8 < qL <= 16(the speculative-decoding verify window) a 2-pass vector path.sdpa_vector_2pass_1_multirow: each simdgroup owns up to 4 consecutive query rows (2 for head dims > 128, bounding per-lane register state), reading each K/V row into registers once and reusing it across its rows. The block-parallel reduction over the key sequence is retained and pass 2 is unchanged.sdpa_vector_2pass_multirow(same structure as the existing one) and one additiveelse ifineval_gpurouting8 < qL <= 16there whenkL >= 1024(the same threshold the existing decode routing uses for its 2-pass split) or when the full kernel does not support the head dim.sdpa_vector_2pass_1, its dispatch, and theqL <= 8routing are byte-identical to main: the diff is +587/−2, and the only removed line is theuse_fallbackreturn statement picking up the new branch. Decode cannot regress — no code it runs was touched.Benchmarks (M3 Max
g15s, fp16, D=128, GQA 32/8, B=1; latency per call —mx.evaleach step, as a verify loop runs — min of 10 trials × 20 reps; baseline = mlx 0.32.0, whose behavior at these shapes is identical to current main on this hardware: the only SDPA changes since are M5-only #3843 and env-gated #3875):qL ≤ 8 and qL ≥ 17 keep their existing routes and measure parity within noise (L 1–8 scatter is ±3–4% in both directions across the sweep).
Correctness:
test_fast_sdpa.pypasses in full (17 tests, 602 subtests). The new additivetest_sdpa_multirow_querysweeps Lq {9,12,16} × Nkv {1,2} at D=128/kL=4096 through the new route, plus D=256 (the 2-rows-per-simdgroup bound) and a D=96/kL=512 case (the unsupported-full-head-dim route), each against masks {none, scalar-bool, bool-vector, two broadcast bool layouts, additive float, causal}, plus the transposed-query layout and sinks. The pre-existing tests are untouched.Independent validation on M5 Max (nax path) — confirmed by pierre427 on the previous revision of this branch. The multi-row kernel body and the routing thresholds are unchanged by the restructure — only the packaging changed (in-place branch in the existing kernel → separate additive kernel):
Boundary behavior on nax matched the design: qL ≤ 8 decode unchanged, qL ≥ 24 returns to the existing full-attention route with no regression, correctness at fp16 noise (max abs err 1.4e-4).
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_multirow_query, additive)