Conversation
RohanGautam
left a comment
There was a problem hiding this comment.
Thanks for the PR! Besides the comments below - could you give some end to end prefill numbers using mlx-lm before and after your change too? Once adressed, I'll run it on some more M5 machines to ensure this improves performance more generally.
| return false; | ||
| } | ||
| if (do_causal && (q.dtype() == float16 || q.dtype() == bfloat16) && | ||
| query_sequence_length >= 512 && k.shape(2) <= 1536) { |
There was a problem hiding this comment.
How were 512/1534 chosen? I understand they could come from the qwen, but we could do a sweep over the shapes and find the point where it the fusion starts being useful, so its more generalisable maybe.
| # The shapes cover the causal D=256 window (512 <= qL < 1024, | ||
| # kL <= 1536) and its kL edge. | ||
| @unittest.skipIf(not mx.metal.is_available(), "Metal kernel path only") | ||
| def test_sdpa_head_dim_256_causal_prefill_window(self): |
There was a problem hiding this comment.
From what I understand, this test would pass regardless of this PR or not, right? What is the reason this test is introduced? Seems to be testing numerics which would be correct regardless of the routing changes
Causal fp16/bf16 D256 prefill with 512–1023 query rows currently defaults to unfused attention. This extends the existing NAX split-head-dimension kernel to fp16/bf16 chunks with 512–1023 query rows and at most 1536 keys, where the measurements below favor it. Query/value dimensions must match and query length must not exceed key length.
This window covers the first few 512-token prefill chunks in models such as Qwen3.5-35B-A3B, whose full-attention layers use D256 and 16/2 query/KV heads. It requires an ordinary floating-point KV cache; full-size 2048-token chunks and quantized KV cache do not benefit.
Apple M5 Max,
MLX_ENABLE_TF32=0. Times are arm medians; ratios are paired geometric means of main/PR time, with 95% CIs. These are operator measurements, not whole-model throughput. The six window cells were measured at runtimebb0b8a33e; final head69e88a18achanges only tests and was used for the two control measurements in a separate session.The k1537 and float32 controls both passed calibration and drift checks. Neither showed a significant difference, and both 95% intervals lie within the predeclared main/PR ratio band of 0.95–1.05. This bounds these two controls; other head/batch/sinks combinations remain unmeasured.
Tests cover both dtypes, boundary lengths, sliced KV, batch 2 and sinks. From
python/tests,MLX_ENABLE_TF32=0 python -m unittest -v test_fast_sdpapassed (27 tests, 2 skipped).uvx pre-commit run --all-filespassed.Benchmark reproduction
Measured builds: main
5778a97c0, candidate runtimebb0b8a33e(controls at test-only final head69e88a18a); Apple M5 Max, 128 GiB, macOS 27.0 (26A5425a). Build separate source checkouts with identical Release settings and Python bindings under each checkout'spython/directory. Save the script below assdpa_microbench.py.Set
--dtype,--q,--k,--hq,--hkand--batchfor each row. The script reports seconds per call using a four-call dependent chain. Keep other GPU work idle; thermal-limit telemetry was unavailable during these measurements.Use 15 fixed main/main calibration pairs followed by 15 main/PR pairs, except fp16 q512/k1536 and both controls use 30 of each. Alternate package order. Initial preconditioning/cooldown settings were 10s/10s, except fp16 q512/k512 used 60s/10s and fp16 q512/k1536 and each control used 60s/30s after five minutes of initial cooling.
Reject arm-median drift above 5% between session halves; allow one retry after 120 seconds of cooling with doubled preconditioning. Retain all pairs, including flagged outliers, in the paired log-ratio mean and Student-t 95% interval. Do not pool sessions; treat overlap with the calibration interval as unresolved.