perf(cuda): extend sdpa_vector decode kernels to head_dim 256/288 via MLX overlay#681
Merged
Merged
Conversation
Extend the CUDA sdpa_vector decode kernels and the supports_sdpa_vector gate to head_dim 256 and 288 through the mlxcel MLX overlay, so head_dim > 128 models (gemma family 256, qwen3.5/3.6 256, baichuan-m1 256, paligemma2 288) take the fused vector path at decode instead of MLX's unfused materializing SDPA fallback. This is a single full-file overlay of scaled_dot_product_attention.cu; the .cpp is untouched because cudnn rejects head_dim > 128, so widening this gate alone routes 256/288 decode to the vector kernels. dispatch_headdim gains case 256/288 plus a throwing default (a gate/dispatch desync previously launched no kernel and left the output buffer as garbage), the D=256/288 instantiations get __launch_bounds__ so their larger per-thread register footprint still fits the 1024/256/1024 thread launches, and the path sits behind the MLXCEL_SDPA_VECTOR_LARGE_D env kill switch (default on) for A/B and rollback without a rebuild. Measured on GB10 (same bench harness, env toggle): the fused path is a real win on full-attention head_dim-256 families, qwen3.5-2b-4bit at 8192 context decodes 129.06 vs 114.89 tok/s (1.123x) and qwen3.5-9b-4bit 38.55 vs 36.92 tok/s (1.044x), growing with attention share. gemma-4-12b-it-4bit stays flat (0.993/1.001/0.994/1.007x at 512/2048/8192/32768) because only 8 of its 48 layers are full-attention and its decode is dominated by model-specific fixed overhead rather than attention; that overhead is tracked as follow-up issue #680, which carries the original 2x goal. Output is byte-identical to the fallback on a chat-template greedy prompt, and the near-tie divergence seen on degenerate pad prompts is FP-ordering noise present in both paths. Numbers are recorded in benchmarks/cuda_gb10_sdpav_675_2026-07-06.csv, and the overlay is a full-file copy synced to upstream e9463bb that must be re-synced on MLX pin bumps.
Add the missing row to the environment-variables reference page for the CUDA sdpa_vector large-head_dim kill switch introduced in #681, alongside the other attention-path diagnostic toggles.
Member
Author
PR Finalization CompleteSummary
All checks passing. Ready for merge. |
This was referenced Jul 7, 2026
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.
Summary
Extend the CUDA
sdpa_vectordecode kernels and thesupports_sdpa_vectorgate to head_dim 256 and 288 via the mlxcel MLX source overlay, so head_dim > 128 models (gemma family 256, qwen3.5/3.6 256, baichuan-m1 256, paligemma2 288) take the fused vector decode path instead of MLX's unfused materializing SDPA fallback.Root cause and single-file overlay approach
On CUDA, decode (
q_len < 4) for head_dim > 128 always took MLX's materializing SDPA fallback:supports_sdpa_cudnnrequires head_dim <= 128 andsupports_sdpa_vectoronly accepted head_dim in {64, 96, 128}, so 256/288 fell through to the explicit matmul + mask + softmax + matmul composite with per-token launch overhead and no fused softmax.This is a single, full-file overlay of
mlx/backend/cuda/scaled_dot_product_attention.cu, copied over the fetched MLX source at CMake configure time (the same overlay mechanism as PR #652). The.cppis deliberately untouched:ScaledDotProductAttention::use_fallbackreturns!supports_sdpa_cudnn && !supports_sdpa_vectorand cudnn rejects head_dim > 128, so wideningsupports_sdpa_vectoralone is sufficient to route 256/288 decode to the vector kernels. The three kernels (kernel_sdpav_1pass,kernel_sdpav_2pass_1,kernel_sdpav_2pass_2) already parameterize on templateint Dwith fixed-size, D-independent shared tiles and a 32x32 transpose aggregation, so D=256 (v_per_thread=8) and D=288 (v_per_thread=9) are handled by the existing kernel bodies; only the dispatch and the gate needed widening.Changes: (A)
dispatch_headdimgainscase 256andcase 288plus adefault:that throwsstd::runtime_errornaming the head_dim, since upstream had no default and a gate/dispatch desync would silently launch no kernel and leave the output buffer as uninitialized garbage; this turns that failure into a loud error. (B)supports_sdpa_vectoraccepts head_dim 256/288 behind a static env kill switch while keeping theq_len < 4,!has_arr_mask, and!output_logsumexpgates unchanged. (C)__launch_bounds__is added to the three kernels.Launch-bounds rationale
The D=256/288 instantiations hold roughly
3 * v_per_threadfloats in per-thread registers, so without launch bounds nvcc may compile to more than 64 regs/thread and a 1024-thread launch would fail at runtime with "too many resources requested for launch".__launch_bounds__(1024)onkernel_sdpav_1passandkernel_sdpav_2pass_2and__launch_bounds__(256)onkernel_sdpav_2pass_1(their exact launch sizes) cap registers, spilling if needed, and are a no-op for D <= 128 which already fit.Kill switch
static bool large_d_enabled = env::get_var("MLXCEL_SDPA_VECTOR_LARGE_D", 1)(default on) gates only the 256/288 additions, so settingMLXCEL_SDPA_VECTOR_LARGE_D=0restores the exact prior materializing fallback with no rebuild; that is how the A/B below was collected and is also the rollback lever.Correctness
Greedy decode on gemma-4-12b-it-4bit with the chat template produces byte-identical output between the vector path (default) and the fallback (
MLXCEL_SDPA_VECTOR_LARGE_D=0). The vector path is provably engaged: on degenerate pad-dominated prompts the two paths diverge, which is only possible if different code runs, and that divergence is FP-ordering noise (fused online-softmax with exp2/log2e vs the materializing plain softmax) on near-tie logits, present in both directions and not a correctness defect.Measured decode throughput (GB10, release, same bench harness and env toggle)
The fused path is a real +4 to +12% win on full-attention head_dim-256 families (qwen3.5/3.6, baichuan-m1), growing with attention share; full rows (prefill, peak memory, notes) are in
benchmarks/cuda_gb10_sdpav_675_2026-07-06.csv.gemma-4 is not attention-bound (follow-up #680)
gemma-4-12b stays flat across 512/2048/8192/32768 because only 8 of its 48 layers are full-attention (a 5-sliding + 1-full pattern) and its decode is dominated by roughly 44 ms/token of model-specific fixed overhead rather than attention; that overhead, and the original 2x decode target for gemma-4, are re-attributed to follow-up issue #680, and this PR does not attempt it.
Deferred scope
Head_dim 192 and 512 are out of scope: no shipped model uses symmetric qk=v=192, and the MLA families (deepseek-v2/v3 qk 192 / v 128 and deepseek-v4-flash 512) need asymmetric
D_qk != D_vkernel signatures, which is a separate change.Overlay maintenance
The overlay is a full-file copy of upstream
scaled_dot_product_attention.cusynced to MLX commit e9463bb with only the dispatch, gate, and launch-bounds deltas applied, so it must be re-synced (three-way reconciled) on any MLX pin bump;docs/benchmark_results/mlx-pin-upgrade-2026-07-03.mdnow lists it in the overlay table.Closes #675