Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Prevent Voxtral NaNs in CUDA split-K attention - #22133

Merged
digantdesai merged 1 commit into
mainfrom
split_k_nan
Aug 27, 2026
Merged

Prevent Voxtral NaNs in CUDA split-K attention#22133
digantdesai merged 1 commit into
mainfrom
split_k_nan

Conversation

@digantdesai

@digantdesaidigantdesai commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Voxtral attention scores can exceed the fixed-phi exponent range, turning partial softmax values into infinities and decoder logits into NaNs.

Use stable normalization because a fixed offset cannot cover both large positive and negative score ranges. No perf regression.

 ### A100 performance vs fixed phi=5 (quick and dirty benchmarking)
Measured the complete graph-captured split-K SDPA operator, not full-model latency. Results are two-run averages using PyTorch 2.13 and Triton 3.7.1.
Shape phi=5 this PR Change
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━ ━━━━━━━━
Voxtral, D128, Lq1, Lkv4096 16.9 µs 16.6 µs −1.7%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv8192 28.2 µs 26.8 µs −4.9%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq1, Lkv32768 107.5 µs 93.8 µs −12.8%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv4096 18.5 µs 18.9 µs +2.2%
────────────────────────────── ────────── ─────────────────── ────────
Voxtral, D128, Lq4, Lkv32768 116.7 µs 103.5 µs −11.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv4096 15.0 µs 15.7 µs +5.0%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv8192 19.4 µs 17.6 µs −9.4%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv16384 34.2 µs 31.7 µs −7.5%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq1, Lkv32768 70.7 µs 59.4 µs −15.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv16384 45.3 µs 44.9 µs −0.9%
────────────────────────────── ────────── ─────────────────── ────────
Qwen3.5, D256, Lq4, Lkv32768 76.7 µs 69.7 µs −9.0%
At long context, the stable implementation is 5–16% faster than phi=5. Short-context results range from approximately neutral to 5–7% slower, with more run-to-run noise.

@digantdesai
digantdesai requested review from Gasoonjia and a lite review from CopilotAugust 25, 2026 04:06
@pytorch-bot

pytorch-botBot commented Aug 25, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22133

Note: Links to docs will display an error until the docs builds have been completed.

✅ No Failures

As of commit 238a723 with merge base 469debd (image):
💚 Looks good so far! There are no failures yet. 💚

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-clameta-claBot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 25, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes numerical instability in the CUDA Triton split-K decode SDPA path by replacing the prior fixed-offset (“phi”) softmax approximation with a stable online-softmax that tracks per-split maxima and rescales partials during the cross-split reduction. This directly targets Voxtral decode cases where very large positive or negative attention scores previously caused overflow/underflow leading to NaNs or silent zero outputs.

Changes:

  • Implement stable per-split online softmax in _sdpa_decode_splitk_kernel, and stable global rescaling in _sdpa_decode_reduce_kernel via a new M_partial buffer.
  • Remove the fixed _DEFAULT_SPLITK_PHI usage from the split-K decode implementation (while keeping phi in the operator signature for schema compatibility and explicitly documenting it as ignored).
  • Add regression tests covering large positive logits (overflow case), large negative logits (underflow-to-zero case), and correctness with kv_len excluding empty trailing splits.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

FileDescription
backends/cuda/triton/kernels/sdpa.pyReworks split-K decode softmax to be numerically stable using per-split max tracking + global rescaling; introduces M_partial and updates launch plumbing.
backends/cuda/tests/test_triton_sdpa_splitk.pyAdds targeted regression tests to prevent NaNs/Infs and validate correctness under extreme logits and kv_len-bounded decode.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@shoumikhin

Copy link
Copy Markdown
Contributor

The algorithm change looks correct to me, and it matches what tq4_sdpa.py
already does. I reproduced both the bug and the fix on an H100 (torch 2.13,
triton 3.7.1) using the inputs from your new test_voxtral_large_logits_stable:
on the pre-change kernel sdpa() returns 4096 NaNs at L_q=1, on this branch it
returns finite output with max abs error 0.0074. Accuracy against a float64
reference is also slightly better than the fixed-phi path, out to L_kv=32768.

Three things before it lands.

1. The rebase has to cover both split-K kernels.

Since this branched, main added _sdpa_small_query_splitk_kernel (#21628), and
sdpa() routes L_q 2 to 4 into it. It still computes exp(qk - phi) with a
fixed phi and sums partials with no rescaling, so it has the same bug. On current
main, with the same inputs your new test uses:

sdpa() L_q=1 nan=4096
sdpa() L_q=2 nan=8192
sdpa() L_q=4 nan=16384

The merge is also not safe to resolve mechanically. This PR deletes
_DEFAULT_SPLITK_PHI at sdpa.py:54, and that deletion sits outside the conflict
hunk while main's two remaining uses of it sit inside. Taking main's side of the
conflict leaves the constant referenced but not defined, so it fails at runtime.

2. "No perf regression" does not hold at D=256.

Alternating base/head processes on an idle H100, do_bench medians, best of three
rounds:

B, H_q, H_kv, D, L_kvbeforeafterchange
1, 16, 2, 256, 1638436.7 us48.6 us+32%
1, 16, 2, 256, 819225.6 us29.9 us+17%
1, 32, 8, 128, 3276898.0 us91.9 us-6%
1, 32, 8, 128, 819233.6 us33.3 us-1%

Profiling attributes essentially all of it to the reduce kernel, which gets 3.2x
to 3.4x slower (5.1 to 17.1 us at L_kv=16384). grid_reduce is (B * H_q,), so
that shape launches 16 blocks on 132 SMs and the kernel is a latency-bound serial
loop over splits. The change puts two tl.exp on the loop-carried dependency
through m_global, plus a HEAD_DIM-wide multiply-add where there used to be an
add. Voxtral shapes have more query heads and a narrower accumulator, so they do
not see it.

For what it is worth, the torch.zeros to torch.empty change does not offset
this: the memsets it removes measure about 1 us at that shape.

Happy either way on the fix, but the claim in the description should probably be
scoped to the Voxtral shapes, or the reduce restructured (a cheap option is
parallelizing the reduce over D, or a separate max pass so the exponentials come
off the serial chain).

3. test_kv_len_ignores_empty_trailing_splits is not a regression test.

It passes on the pre-change kernel too, so it cannot fail for the reason this PR
exists. The other two do fail there, so those two are doing real work.

It is still a useful test of the new uninitialized-buffer contract, just a weak
one, since dirty allocator memory rarely shows up in a fresh process. Pre-filling
M_partial and L_partial with NaN before the launch would pin the invariant
properly. I NaN-poisoned every float32 CUDA allocation and swept batch sizes,
group counts, and L_kv from 1 to 40000, and every partial slot did get written,
so the invariant does hold today.

Comment nit, sdpa.py:1389:

# The split grid unconditionally writes every partial, including empty# splits, so these buffers do not need initialization kernels.

The stores are not unconditional, they are masked by g_valid, and the kernel
comment three lines above says so. What actually makes it safe is that the grid
partitions the h_q range exactly. Worth rewording, because as written it invites
someone to add an early return for empty splits later, which would silently
reintroduce garbage.

Also, since phi is now ignored, the docstring line reads better as "deprecated,
accepted for schema compatibility and ignored" rather than implying it still does
something.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

Comment threadbackends/cuda/triton/kernels/sdpa.py
A fixed softmax offset is only valid while every attention score remains
within the fp32 exponent range. Voxtral can exceed that range, producing
infinities and NaNs. Sufficiently negative scores can instead underflow every
partial weight and silently return zeros. Both split-K paths have this risk.
Use each split's observed maximum and rescale against the global maximum. Each
split processes only a few KV tiles, so online softmax removes the
model-specific score assumption without materially extending the split-kernel
recurrence. Reduce all partials together so cross-split normalization does not
become a serial bottleneck.
This change was authored with Codex.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

@GasoonjiaGasoonjia left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, it would be great if we can have a perf comparsion between static phi version and online softmax

(filled) positions (O(context) instead of O(max_seq_len)). Read
on-device, CUDA-graph safe. When None, sweeps the full L_kv.

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we no longer need to mention phi here; there's no phi in the op

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do it later, don't want to run CI again for this :p

@digantdesai

Copy link
Copy Markdown
ContributorAuthor

phi is deprecated, accepted for operator-schema compatibility, and ignored.

Updated the PR summary.

@digantdesai
digantdesai merged commit d58fc25 into mainAug 27, 2026
259 checks passed
@digantdesai
digantdesai deleted the split_k_nan branch August 27, 2026 14:47
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA SignedThis label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

@digantdesai@shoumikhin@Gasoonjia