Fix double transpose of K/V in templated attention backward - #14341
Fix double transpose of K/V in templated attention backward#14341guptaishaan wants to merge 2 commits into
Conversation
_cudnn_attention_forward_op saves query/key/value after transposing them to (B, H, S, D), but _cudnn_attention_backward_op transposed key and value a second time before calling aten::_scaled_dot_product_cudnn_attention_backward. The ATen op therefore received Q as BHSD and K/V as BSHD, and returned gradients unrelated to the true ones. Only the context parallel path uses these ops, so the forward pass and plain inference were unaffected. _native_flash_attention_backward_op carried the identical two lines against the identical forward-op contract and is fixed the same way. The grad_out transpose stays: the forward op returns out in BSHD, so autograd hands back grad_out in BSHD. Adds tests/models/test_attention_dispatch.py, which drives each forward/backward op pair through a minimal autograd.Function and checks all three gradients against an eager fp32 reference. num_heads == seq_len so a layout mix-up cannot hide behind a shape check. Fixes huggingface#14338
|
Reporter of #14338 here. You mentioned you only have Ampere, so I ran your patch on Hopper and Blackwell as well. Confirmed on all of them — and the larger shape turned up something worth adding to the PR. Setup: torch 2.13.0+cu130, cuDNN 9.20, bf16, your exact test harness (the minimal Your test's shape, B=2 S=16 H=16 D=64
Reference magnitudes 1.86 / 2.26 / 2.75, so pre-patch the error exceeds the value being approximated. Identical across architectures because at S=16 the residual is dominated by bf16 input quantisation rather than by the kernel, so this shape does not discriminate between backends — which is fine, it is a layout test. A DiT-realistic shape, B=2 S=1024 H=24 D=64This is where it gets more interesting. With on all three architectures, because K/V arrive claiming
(Here the per-architecture spread is visible, as expected once the real kernels engage.) Two things this suggests for the PR:
Fixing Repro script if useful: it just swaps |
The existing shape uses num_heads == seq_len, where the double transpose
of K/V passes every shape check and silently corrupts the gradients. On
any shape where num_heads != seq_len the same mix-up makes the backend
reject the head count instead, so the failure is a RuntimeError rather
than bad numbers:
cudnn: cuDNN Frontend error: For group-query attention, number
of heads for key and query must be a factor of number of
heads for query
native_flash: Number of heads in key/value must divide number of heads
in query
Parametrizes the test over a second, DiT-like shape (B=2, S=1024, H=24,
D=64) so both halves of the failure mode are covered.
|
Thanks for running this on sm_90 and sm_100, and for the larger shape. Both points taken. Added the second case: the test is now parametrized over and all 4 cases pass with the patch. Gradient max abs error on the new shape here is 0.0024 / 0.0035 / 0.0023 for both backends, reference magnitudes 0.74 / 1.13 / 0.51, in line with your A100 column. Your S=16 numbers also came out identical to mine to four decimals, which is a nice confirmation that the harness is the same one. The test asserts on the gradients, not on the error text, since that wording comes from cuDNN and from ATen and will drift. The docstring now records which half of the failure mode each shape covers. On your point 1: agreed, and stating it plainly here since it is the useful part for anyone finding this later. With No need for the repro script. If you do feel like running something else, the |
Fixes #14338
_cudnn_attention_forward_opsaves query/key/value after transposing them to(B, H, S, D),but
_cudnn_attention_backward_optransposedkeyandvaluea second time, soaten::_scaled_dot_product_cudnn_attention_backwardgot Q as BHSD and K/V as BSHD. Thegrad_outtranspose is correct and stays, since the forward op returnsoutin BSHD._native_flash_attention_backward_ophas the identical two lines against the identicalforward-op contract, so it is fixed here too. Easy to drop that hunk if you would rather split it.
Four lines deleted, no behaviour change to the forward pass.
Test
New
tests/models/test_attention_dispatch.py: drives each forward/backward op pair through aminimal
autograd.Function(same contract theTemplated*Attentionwrappers use) and checksall three gradients against an eager fp32 reference.
num_heads == seq_lenso a layout mix-upcannot hide behind a shape check. Fails on both backends before the patch, passes after.
Verified
On 2x A40 (sm_86), torch 2.13.0+cu126, cuDNN 9.10.2, bf16:
B=2 S=16 H=16 D=64reproduces the reporter's numbers. dQ/dK/dV max abserror goes from 7.90 / 7.97 / 3.98 to 0.006 / 0.008 / 0.010, against reference magnitudes of
1.86 / 2.26 / 2.75.
ContextParallelConfig(ulysses_degree=2),attention_backend("_native_flash"), parameter grads all-reduced and compared to asingle-GPU reference. Before the patch the backward raises
Number of heads in key/value must divide number of heads in query. After, the worst relative grad error is 0.0138, which isexactly what the plain
nativebackend gives on the same harness.pytest -m "attention or context_parallel"overtest_attention_dispatch.py,test_attention_processor.pyandtest_models_transformer_flux.py: 14 passed, 42 skipped.The 10 failures in the full Flux file (LoRA hotswap + AOT compile) are pre-existing and
reproduce on a clean tree.
ruff check,ruff format --check,utils/check_copies.pyall clean.Not verified
flash_hub/_flash_3_hub/ varlen / sage backends skipped,kernelsis not installed. Iread their backward ops and none of them have this pattern.
Two things left for follow-ups
_cudnn_attention_forward_oppassescompute_log_sumexp=return_lse. The Ulysses wrapperforwards the caller's
return_lse, which isFalsefor ordinary model attention, so cuDNNnever computes the lse and the backward op gets an empty
logsumexp. On the end-to-endharness above, ulysses +
_native_cudnnstill gives a worst relative grad error of 328 afterthis patch, and drops to 0.0138 if I locally force
compute_log_sumexp=True. So cuDNN CPtraining is still broken, for a different reason than cuDNN attention backward corrupts gradients: K/V transposed twice, Q once #14338. Torch's own SDPA dispatch
derives that flag from
query/key/value.requires_grad. Happy to send it separately.torch.boolattn_maskgoing into the ATenattn_biasslot without conversion to an additive-inf/0bias. Real by inspection, nottouched here, not tested.
Also worth noting:
test_context_parallel_backwardonly asserts that gradients are finite,never that they are correct, which is why this went unnoticed.
Thanks to @YangXu1990uiuc (NVIDIA cuDNN team) for the precise diagnosis and the repro numbers,
which matched what I measured on A40 to three decimal places.