Skip to content

CUDA backend: keep wide attention heads that are not a power of 2 off the Triton SDPA kernel - #24427

Open
shoumikhin wants to merge 3 commits into
pytorch:mainfrom
shoumikhin:cuda-sdpa-wide-head
Open

shoumikhin wants to merge 3 commits into
pytorch:mainfrom
shoumikhin:cuda-sdpa-wide-head

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

The CUDA backend replaces scaled_dot_product_attention with its own Triton kernel when the kernel accepts the call. For head sizes that are not a power of 2, that kernel holds the whole head in one tile of at most 256 values. Wider heads of that kind, such as 288, 320, 384, 640 or 768, are still accepted. The kernel then uses only the first 256 values of each query and key, and it never writes the rest of the output. The program runs and returns wrong numbers, with no error. For example, the mid-block attention of the LingBot-VA video VAE encoder has heads of 640, and its lowered encoder drifts far from eager.

q, k, v = (torch.randn(2, 1, 64, 640, device="cuda", dtype=torch.bfloat16) for _ in range(3))
sdpa(q, k, v)  # max error against PyTorch: about 4

What this change does

The shape check that the kernel and the replacement pass share now rejects head sizes that are not a power of 2 and are wider than that one tile. The pass already leaves calls the kernel rejects to the regular CUDA lowering, so such attention in an exported model now gives the right result. Code that calls the Triton kernel directly, such as the off-graph KV lowering, now gets an error instead of wrong numbers. Heads that are a power of 2, including 512 and 1024, use the other Triton kernel, which covers the whole head, so they still use Triton.

Because this fixes wrong results, it should also go to release/1.6.

What was tested

On an A100 with torch 2.14:

  • test_sdpa_head_sizes lowers attention with heads of 96, 192, 256, 512, 288, 320, 384, 640 and 768, runs it, and compares each result with eager. Without this change the last five fail.
  • test_heads_the_kernel_takes_are_supported checks that the pass still gives heads of 192, 256, 512 and 1024 to the Triton kernel. test_wide_head_that_is_not_a_power_of_2_is_not_replaced checks that a head of 640 exports through the regular lowering and logs why. test_wide_non_pow2_head_rejected checks that a direct call with a head of 640 raises.
  • The other Triton SDPA kernel tests and test_sdpa_splitk_replacement.py still pass.
  • At 64 tokens and head 640, one lowered attention call takes about as long as before: 767 µs now, 784 µs with the wrong result. This was measured at 64 tokens only. The regular lowering builds the full score matrix, so for long sequences it can need much more time and memory than the Triton kernel did.
  • With torch 2.15, the LingBot-VA VAE encoder, lowered with default settings, now matches eager within bfloat16 precision.

Copilot AI balanced review requested due to automatic review settings November 2, 2026 13:34
@pytorch-bot

pytorch-bot Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

⏳ 4 Pending, 1 Unrelated Failure

As of commit 82b00ec with merge base ab68460 (image):

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

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

@meta-cla meta-cla Bot 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 Nov 2, 2026
@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@claude

claude Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 15m 52s —— View job


The new head-size bound is correct and nothing blocks merging. The five issues are minor and mostly about tests: heads of 1024 are said to still use Triton, but nothing compiles the kernel at that size, and no test checks wide heads on ROCm.

Detailed review

PR Review: #24427

Summary

This PR makes _validate_qkv_shapes reject head sizes that are not a power of 2 and are above 256. The replacement pass then leaves those calls to the regular lowering, and direct triton::sdpa callers get an error instead of truncated output. The bound is correct. There are five minor issues below. The CUDA tests were not run for this review because no GPU was available.

Code Quality

  • backends/cuda/triton/kernels/sdpa.py:273-278: the 256 repeats the clamp in _next_power_of_2 (line 190), and only the comment links the two.
    • The tile the kernel uses comes from _next_power_of_2 (BLOCK_D = _next_power_of_2(D), line 1189), so if that clamp changes, the check and the tile disagree again. For example, if the clamp drops to 128, heads of 129 to 255 are cut short without an error.
    • if not _is_power_of_2(D_q) and _next_power_of_2(D_q) < D_q: tests the tile directly, and the comment is no longer needed.
    • Fix this →

Testing

  • backends/cuda/tests/test_sdpa_splitk_replacement.py:374, backends/cuda/tests/test_cuda_export.py:702: nothing compiles the power-of-2 kernel at a head of 1024.
    • The routing test asserts (1024, True), and the description says heads of 1024 still use Triton.
    • head_sizes has no power of 2 above 512, the routing test does not compile the kernel on purpose, and the kernel tests use 1024 only as a sequence length.
    • At a head of 1024, _sdpa_prefill_prune (sdpa.py:737) keeps only three configs: (BLOCK_M 16, BLOCK_N 32, 2 stages), (16, 64, 3 stages) and (32, 64, 3 stages).
    • Even the smallest of these holds a 64 KB K tile and a 64 KB V tile for each of its 2 stages, about 256 KB of shared memory. An A100 allows about 163 KB per block, so possibly no config fits.
    • If so, the pass gives the kernel a call it can't run, and the export fails instead of falling back.
    • Low confidence, because I could not compile this here. Adding 1024 to head_sizes would settle it.
    • Fix this →
  • backends/cuda/tests/test_sdpa_splitk_replacement.py:367-380: the new test does not follow the file's pattern, and as a result nothing checks wide heads on ROCm.
    • The test sits in TestSDPAKernelSupportCheck, whose docstring (line 323) says the class holds calls that cannot be exported end to end. Heads of 264 and 640 do export end to end; test_sdpa_head_sizes exports 640.
    • The rest of the file tests calls the validator rejects in TestUnsupportedSDPAStaysWithRegularLowering with assertStays (lines 250-252). assertStays also checks the logged reason and that the export succeeds.
    • The ROCm job runs this file (.ci/scripts/test-rocm-aoti.sh:156), but test_sdpa_head_sizes is skipped on ROCm. _require_regular_lowering_on_rocm (line 34) records that AOTInductor fails to compile the regular lowering of some SDPA calls on ROCm. So nothing checks that a head of 640 still exports on ROCm now that it uses the regular lowering.
    • Suggest adding q = _bf16(1, 2, 64, 640) with self.assertStays(self._logs(q, q, q), "not a power of 2") to that class. If the accepted sizes stay in TestSDPAKernelSupportCheck, also reword its docstring at line 323.
    • Fix this →

API Design

  • backends/cuda/triton/kernels/sdpa.py:1281-1284: the docstring of the public triton::sdpa op lists its other input limits but not the new head-size limit.
    • The listed limits are bf16 tensors, a bool mask, and dropout_p equal to 0.0.
    • Direct callers have no fallback and now get a RuntimeError. One example is the off-graph KV lowering (backends/cuda/passes/lower_offgraph_kv.py:181, :227).
    • The description of D should say "a power of 2, or at most 256".
    • Fix this →

Backward Compatibility

  • backends/cuda/triton/replacement_pass.py:157-190: the release/1.6 cherry-pick may need another change with it.
    • Exported models fall back to the regular lowering only because _sdpa_kernel_supports runs _validate_qkv_shapes and catches its RuntimeError.
    • If release/1.6 does not have that method, the pass there still swaps in triton.sdpa for a head of 640. The fake kernel (sdpa.py:1465) then raises during lowering.
    • The cherry-pick would then turn wrong results into a failed export, not a correct result.
    • This checkout is shallow, so I could not look at release/1.6. Please confirm that branch has _sdpa_kernel_supports, or pick it together with this change.

Recommendation

Approve: none of the above blocks the fix. Before the cherry-pick, the most important steps are compiling a head of 1024 and checking release/1.6.

  • Gather context (diff, changed files)
  • Run three independent reviewers against the review checklist
  • Consolidate findings
  • Fact-check findings against the code
  • Post the final review

Not done: no GPU was available, so no CUDA tests were run, and release/1.6 could not be fetched to check the backport.

…PA kernel

The Triton SDPA kernel for head sizes that are not a power of 2 loads
the whole head in one tile, and that tile is at most 256 wide. For a
wider head, such as 640 in the mid-block attention of a video VAE
encoder, it reads only the first 256 values of each query and key, and
it never writes the rest of the output, which then holds whatever was in
memory. The program runs and returns wrong numbers.

The shape check that the kernel and the replacement pass share now
rejects such heads, so the pass leaves the call to the regular CUDA
lowering. Heads that are a power of 2 use the other kernel, which
covers the whole head, so they still use Triton.

test_sdpa_head_sizes lowers attention over head sizes on both sides of
the limit and compares each result with eager. Two smaller tests check
that the pass still gives heads of 512 and 1024 to Triton, and that a
direct call with a wide head raises.
The check now compares the head size with the tile that
_next_power_of_2 picks, so it stays right if that cap changes. The
docstring of triton::sdpa states the limit for direct callers.

A head of 640 is now exported through the regular lowering in the
tests, which also checks the logged reason. The hand-built support test
keeps only the heads that the kernel still takes, since some CI GPUs
lack the shared memory to compile a head of 1024.
Copilot AI balanced review requested due to automatic review settings November 2, 2026 14:56

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

Thanks. The new commit fixes three of these: the check now follows the tile size from _next_power_of_2, the triton::sdpa docstring states the limit, and a head of 640 is now exported end to end in the tests, with the logged reason checked.

Two I left as they are:

  • A head of 1024 compiles and matches PyTorch on an A100. But even its smallest kernel config needs about 161 KB of shared memory, which is more than the A10G CI runner allows, so a compiled test would fail there. This was already the case before this change. It is a separate follow-up.
  • release/1.6 already has _sdpa_kernel_supports (Leave SDPA calls the Triton kernel cannot take to the regular CUDA lowering #23349), and its sdpa.py is the same as on main, so the cherry-pick needs nothing else.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 15m 11s —— View job


The new head-size check is correct and nothing blocks merging; the one minor issue is that the new triton::sdpa docstring and support test treat every power-of-2 head as supported, while heads of 2048 or more cannot compile on any GPU.

Detailed review

PR Review: #24427

Summary

This PR makes _validate_qkv_shapes reject heads that are not a power of 2 and are wider than the tile from _next_power_of_2. The replacement pass now leaves those calls to the regular lowering, and direct triton::sdpa calls raise instead of returning truncated output. The check is correct. The one issue below is minor and predates this change. No tests were run for this review, because this runner allows neither Python nor GPU commands.

API Design

  • backends/cuda/triton/kernels/sdpa.py:1283-1284: the new docstring says D may be any power of 2, but the power-of-2 kernel cannot compile heads of 2048 or more on any GPU.
    • _sdpa_prefill_prune notes (lines 729-730) that its smallest configs meet its register budget only for heads up to 1024. At 2048 the budget rejects every config, so the function keeps only the smallest one (BLOCK_M 16, BLOCK_N 32, 2 stages).
    • That config's tiles grow linearly with the head size. The author measured about 161 KB of shared memory at 1024, so at 2048 it needs about 320 KB. No NVIDIA GPU allows that much per block; the most any allows is 227 KB, on H100 and B200.
    • _validate_qkv_shapes accepts these heads, so _sdpa_kernel_supports (replacement_pass.py:158-163) sends them to Triton and the export fails. That method exists to keep such calls with the regular lowering. Direct callers such as the off-graph KV lowering (lower_offgraph_kv.py:181) rely on this docstring and have no fallback.
    • This predates the PR, but the PR now writes it into the contract. The new test_heads_that_fit_the_kernel_are_supported (test_sdpa_splitk_replacement.py:377) also lists 1024 as a head that fits, while its comment at line 372 says some CI GPUs cannot compile it.
    • If the same check also rejected power-of-2 heads above 1024, the pass would fall back for them, as it now does for wide heads that are not a power of 2. The docstring could then say "at most 256, or a power of 2 up to 1024". A head of 1024 on GPUs with less shared memory stays with the follow-up you already planned.
    • Fix this →

Recommendation

Approve: the issue above is minor and predates this PR, and these heads fail with an error rather than giving wrong numbers.

  • Gather context (diff, changed files)
  • Run three independent reviewers against the review checklist
  • Consolidate findings
  • Fact-check findings against the code
  • Post the final review

Not done: this runner allows neither Python nor GPU commands, so no tests were run. release/1.6 is not in this shallow checkout, so your note that the cherry-pick needs nothing else was not checked.

…string

The docstring said any power-of-2 head works, but the power-of-2 kernel
has its own shared-memory limits for very wide heads. It now states only
the limit for heads that are not a power of 2, and the support test is
named for what it checks: which heads the pass gives to the kernel.
Copilot AI balanced review requested due to automatic review settings November 2, 2026 16:03

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

Thanks. The docstring now states only the limit this change adds: a head that is not a power of 2 must be at most 256. The support test is renamed to say what it checks, which is the heads the pass gives to the kernel.

Rejecting power-of-2 heads above 1024 is a separate change. Those heads cannot give wrong numbers today: when they do not fit, compiling stops with an error. The right limit for heads of 1024 and below depends on how much shared memory each GPU has, so I am keeping it for a follow-up.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Nov 2, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 13m 29s —— View job


The head-size check is correct and nothing blocks merging. One minor note: the heads it rejects now use the MATH fallback, which gets slower with the square of the sequence length, and the timing in the description covers only 64 tokens.

Detailed review

PR Review: #24427

Summary

This PR makes _validate_qkv_shapes reject heads that are not a power of 2 and are wider than the 256-wide tile. The replacement pass then leaves those calls to the regular lowering, and direct triton::sdpa calls raise an error. The check is correct. The one issue below is minor. No tests were run for this review.

Performance

  • backends/cuda/triton/kernels/sdpa.py:275: the heads this check rejects now run through the MATH fallback, which can be slow on long sequences.
    • Calls the pass leaves alone are compiled under sdpa_kernel([SDPBackend.MATH]) (backends/cuda/cuda_backend.py:1182).
    • MATH builds a full L_q × L_kv score matrix for each head, so its time and memory grow with the square of the sequence length. The Triton kernel these heads used before works through K and V in blocks and never builds that matrix.
    • The description times only 64 tokens, where that matrix is small. "About as long as before" may not hold for wide heads over thousands of tokens, such as attention over a high-resolution VAE latent. There, the fallback could be much slower or run out of memory.
    • This does not block, because the old results were wrong. Please say in the description that the timing holds only for short sequences.
    • A follow-up could keep these heads on Triton. For example, padding heads of 288 to 384 with zeros up to 512 would let the power-of-2 kernel take them, as long as scale comes from the real head size.
    • This was not measured. It follows from reading the code.

Recommendation

Approve: the issue above is minor, and the change replaces wrong numbers with correct ones.

  • Gather context (diff, changed files)
  • Run three independent reviewers against the review checklist
  • Consolidate findings
  • Fact-check findings against the code
  • Post the final review

Not done: no tests were run. git fetch was not permitted, so the diff was taken against ab68460, the parent of the PR's first commit. It matches the PR's 4 files and +70/−2.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

Agreed. The timing is for the shape we measured only, which is 64 tokens. For long sequences the regular lowering builds the full score matrix, so it can need much more time and memory. The description now says so. Keeping these heads on Triton, for example by padding them with zeros to a power of 2, is a follow-up.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. release notes: backends

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants