Repository navigation
Conversation
…9 to 32-row windows (MTPLX_NAX_FLASH_WIDE, off by default) MLX falls back to its unfused SDPA for more than 8 query rows at head dim 256, so context-copy verify blocks of 9 to 32 rows re-read the dense KV cache and materialise scores: 112 ms at 16 rows and 131 ms at 24 rows per verify at an 80K prefix. The head-dim-split TensorOps flash kernel now walks groups of 32 query rows on a grid axis inside one launch (q_len 9..32), shipping contract for q_len <= 5 bit-identical, kernel self-check lane and tests included. M5 Pro, stock MLX 0.32.2, Qwen3.8-27B: attention at 80K 2.70/3.69/5.67/6.63 ms per layer at 9/16/24/32 rows (stock 9.83/6.80/7.88/8.45); verify forward at 80K 286 to 164 ms (9 rows), 241 to 190 (16), 336 to 294 (24).
jvmenen
force-pushed
the
perf/wide-verify-attention
branch
from
October 17, 2026 20:21
97064cc to
b45b9ba
Compare
jvmenen
pushed a commit
to jvmenen/MTPLX
that referenced
this pull request
Oct 17, 2026
The segment-kernel coupling with youssofal#593/youssofal#594 is on prod since cb017d5; youssofal#639 now holds it upstream. This brings the docstrings and the two hd128 tests that pin MTPLX_NAX_FLASH_WIDE off to the youssofal#639 + youssofal#630 text, so prod carries exactly what the PRs hold. No code change (AST-identical).
This branch has not been deployed
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.
Why
When MTPLX checks a block of 9 to 32 proposed tokens at once, attention gets several times slower than for 8 tokens. Such blocks come from context-copy, which proposes a whole span of text found earlier in the prompt. At head size 256, MLX's fast attention kernel handles at most 8 query rows; for more rows MLX falls back to a route that first writes the full table of attention scores to memory and reads the KV cache several times. On Qwen3.8-27B with an 80K-token conversation, attention for a 13-row block takes 11.5 ms per layer, against 2.6 ms for an 8-row block on MTPLX's own kernel, and a whole verify step of 9 rows takes 286 ms against 156 ms for 8 rows. A longer copied span, which should save time, therefore makes each step much more expensive.
How it works, in plain terms
In a verify step the model computes, for every proposed token (a row), how much attention it pays to each earlier token in the conversation. A flash kernel does that in one pass over the cache and keeps the intermediate scores on the chip, instead of writing the whole score table to memory and reading it back. MTPLX already has such a kernel for blocks of up to 8 rows. With
MTPLX_NAX_FLASH_WIDE=1, blocks of 9 to 32 rows go to the same kernel, which now processes the rows of each KV head in groups of 32 within one GPU launch. Blocks of 8 rows or fewer do not change. For 9 or more rows the rounding differs slightly from the stock route; at every tested position the chosen token was the same, except where two tokens were exactly tied in bf16.What changes
MTPLX_NAX_FLASH_WIDE=1(off by default) sends q_len 9 to 32 to the existing head-dim-split flash kernel (sdpa_nax_flash_dsplit). The kernel now tiles the query rows: the 6 × q_len rows of a KV head are processed in groups of 32 rows within one launch, on a new grid axis, and split over the KV length as before.mtplx/kernels/sdpa_nax_flash_dsplit.py: new template parameterTG_M(16-row blocks per threadgroup) and a row-group axis. Tail-causal masking uses the global row index. The shared gate and dispatch servesdpa_nax_flash_dsplit(contract unchanged, up to 32 rows) and the newsdpa_nax_flash_dsplit_wide(up to 192 rows).mtplx/attention_split.py: with the switch on, the packed-GQA route admits q_len up to 32 and routes 9 or more to the wide kernel. Rows 6 to 8 stay onsdpa_nax_flash, which is as fast there. A bail falls back to stock SDPA.kernel_selfcheck.py: new lanenax_flash_dsplit_wide_sdpa, probed only when the switch is on.[Unreleased].Results
M5 Pro 64 GB, stock MLX 0.32.2, v2.12.2 plus this branch, Qwen3.8-27B Optimized-Speed.
Attention op at an 80K prefix, 16 layers chained, ms per layer:
sdpa_nax_flashThe error against an fp32 reference stays within the stock route's error at every shape tested (rel. 2.7e-3 against 4.4e-3).
Whole verify forward (
target_verify, median of 7, ms, switch off → on):Rows 4 to 8 are bit-identical. For 9 to 25 rows the argmax is equal at every position at 20K and 80K. At 50K it differs at 1 or 2 positions per block, and every such position is an exact bf16 tie (top-2 margin of 0 or one bf16 step).
End to end (
generate_mtpk, depth 3, context-copy on, 600 tokens, greedy, median of 3 interleaved, one process per series):Tests
tests/test_sdpa_nax_flash_dsplit_wide.pychecks the kernel against an fp32 tail-causal reference and the stock route for q_len 1 to 32. It covers several context lengths, 1, 2 and 4 blocks per threadgroup, a tensor offset, block counts and the contract bails. Existing dsplit tests pass unchanged (q_len up to 5 is bit-identical with the switch off). Full suite passes.Notes
MTPLX_NAX_FLASH_WIDE_TGMsets the row blocks per threadgroup (default 2).🤖 Generated with Claude Code