Add force_fused option to scaled_dot_product_attention - #4185
Conversation
Add a force_fused kwarg to mx.fast.scaled_dot_product_attention that ignores the automatic dispatch heuristics and uses the fused Metal kernel, raising when no fused kernel is available for the given shapes. This lets runtimes that know their memory budget choose the fused path's bounded-memory behavior. Also add fused full-attention kernels for head_dim 192/256 (reachable only via force_fused) and the missing head_dim=192 vector kernel. Supersedes ml-explore#3660. Co-Authored-By: Claude <noreply@anthropic.com>
|
Hi @hojin12312 — happy to report the M1 validation is done, and the results turned out more interesting than a simple thumbs-up. Thanks again for folding the safety and diagnostics work into your PR so cleanly; reviewing the capability-reason code while testing it on two machines was a pleasure, and I found nothing to object to — consider that portion reviewed and approved from my side. The setup: your branch at 1. The #3885 register cliff is still real — but it moved with the OS, and it's specialization-dependent.
(2-pass pipelines: 1024 in every variant; M4 Max: 1024 across the board.) Back in July, every d512 variant I probed reported 832. The current Metal compiler squeezes exactly one specialization under the dispatch size; everything else still caps below the kernel's hard-coded 1024-thread launch. I did not expect the ground to shift under this experiment in three weeks, but here we are. 2. The check behaves exactly as intended, end to end (
One reproduction note for anyone retracing this: at kL >= 1024 on 's'-class devices the dispatcher selects the 2-pass kernels (which fit everywhere), so the capped 1-pass is only reachable at shorter kL. My first run sat exactly on the wrong side of that boundary and sailed through — worth knowing before anyone concludes the hazard is gone. 3. Your The takeaway I'd offer for the PR description: the hazard these checks guard is not a static device property. It shifts with the Metal compiler version and with function-constant specialization — the same kernel is safe unmasked and unsafe masked, on the same chip, today. No support-matrix allowlist can track that, which makes the unconditional dispatch-time check exactly the right shape, and your full/NAX additions close the last unchecked dispatches. I'll close #4186 now — glad the two efforts converged, and looking forward to seeing this land. The d512 vector instantiations can then follow as the small opt-in PR discussed in #3658, with their safety story now validated on real hardware. |
Proposed changes
Add a
force_fusedoption tomx.fast.scaled_dot_product_attentionthatignores the automatic dispatch heuristics, uses a fused kernel, and raises
with the unsupported constraint when no fused kernel is available. This lets
inference runtimes — the only party that knows their resident weights, KV
cache size, and memory budget — decide when the fused path's bounded-memory
behavior is worth its throughput cost.
Also restores the fused full-attention support for
head_dim=192/256that wasproposed in #3293/#3660. Those kernels are now reachable only through
force_fused; the default dispatch does not route to them.Follow-up hardening incorporates the full-dispatch threadgroup safety check
and capability-diagnostic concerns raised by @apocryphx in #4186, while
retaining this PR's 192/256 kernels and C++ positional-call compatibility.
Background
#3658 tracks models with genuine
head_dim=192/256full-attention layers. Atlong context, the unfused path materializes a score
transient of
O(n_heads × qL × kL)per full-attention layer. The fusedfull-attention kernel tiles K/V so its transient stays bounded regardless of
kL. However, benchmark measurements on a genuinehead_dim=256workload(the Qwen 3.6 35B checkpoint measured in #3658) show the fused path trades
prompt throughput for that memory floor:
These stock
mlx-lmbenchmark arms were separated by other main-branchcommits, so they are supporting workload evidence rather than a strict
single-commit A/B. The optional 131K fused arm aborted with an
interactivity-impacting command-buffer error and is intentionally omitted.
A fixed dispatch threshold cannot pick correctly for all runtimes, so the
decision is exposed to the caller instead. This PR is the follow-up to the
maintainer request in #3658:
Supersedes the closed #3660.
Addresses #3658.
API
force_fused=False(default): preserves existing dispatch for previouslysupported shapes. This PR separately adds and auto-routes the missing
head_dim=192vector kernel.force_fused=True: bypass the dispatch heuristics and use a fused kernel.Raises
ValueErrorwith the unsupported constraint when no fused kernel isavailable, which covers:
(including unsupported
head_dims, e.g. 512);logsumexpoutput (the fused kernels do notemit one).
is_trainingon its own (when the VJP path already falls back) is allowed touse the fused forward kernel — the output is identical.
Kernel support
steel_attention): addhead_dim=192/256instantiations.Selected only when
force_fused=True. Extend the existing large-head-dimV-tile synchronization barriers from
BD == 128toBD >= 128, covering192/256 as well as 128. This synchronization gap was also independently
identified in giaki3003/mlx@1175b4d.
sdpa_vector): add the missinghead_dim=192(192,192)and aggregation instantiations. Auto-routed like the existing
head_dim=256vector kernel.
head_dim >= 192away from the NAX kernel family, which has no192/256 instantiations.
attention dispatches, avoiding silent zero output on register-limited GPUs.
cuDNN/vector kernels share the same opt-in semantics.
Validation
python/tests/test_fast_sdpa.py:force_fused=Truefull attention forhead_dim192/256 at short keylengths (where the default dispatch stays unfused) matches the reference;
force_fused=Truefull attention forhead_dim192/256 atkL=16385matches the reference, covering the Steel V-tile barrier path;
force_fused=Trueon an already-supportedhead_dim=128matches;force_fused=Trueon an unsupported shape (head_dim=512) raises;qL > kL, vector GQA limits,and CPU execution raise with the specific violated constraint;
head_dim192/256 full attention stays unfused andmatches the reference;
head_dim=192vector attention matches the reference.test_fast_sdpa.pyandtest_fast.pysuites continue to pass.Scope
head_dim=512, which is outside this PR's scope (see Metal SDPA: no fused path for head_dim=512 (vector or full kernel), and the fallback is silent #3885).head_dim=256layers there are bounded sliding-window layers.head_dim=256full attention and demonstrates the intended memory/throughputtradeoff. The optional 131K fused arm did not complete, so this PR does not
claim a valid 131K A/B result.
force_fused; a no-GPU buildraises rather than silently ignoring the flag. CUDA compilation remains a
GitHub CI validation boundary because the local validation host is macOS/Metal.
Checklist
pre-commit run --all-filesto format my code / installed pre-commit prior to committing changes