[ROCm] Fix THD/ragged dQ backward on the bf16 dq_shuffle path (gfx950)#671
[ROCm] Fix THD/ragged dQ backward on the bf16 dq_shuffle path (gfx950)#671wenchenvincent wants to merge 2 commits into
Conversation
On ROCm/gfx950 the CK fused-attention backward silently corrupts dQ for THD/ragged (packed / document-masked) attention on the default bf16 dq accumulation path (NVTE_CK_IS_V3_ATOMIC_FP32=0). Forward, dK and dV are correct. Root cause: build_bwd_fmha_args allocates the dq_acc scratch as fp32-packed (nsplits, H, total_q, d_qk) with batch_stride_dq_acc=0. That matches the fp32 dq_convert post-kernel, but the bf16 dq_shuffle post-kernel expects a per-segment padded layout, so with batch_stride=0 every ragged segment past cu_seqlens offset 0 is written to the offset-0 slot -- dQ is correct only for the first segment and garbage for the rest. Fix: when the v3 asm path actually runs (probed via ck_attn_bwd_uses_v3, added in #662), switch dq_acc to AITER's per-segment bf16 layout (nsplits, B, H, pad16(s_q), 128) with matching strides. v2/fallback and non-ragged/atomic_fp32 keep the fp32-packed layout. fused_attn_ck.cpp sizes the scratch to hold either layout (the layout choice needs a GPU probe that must not run in the workspace-sizing pass). Tests: add an equal-dim-128 bf16 GQA self-attn config (BSHD + THD/RAGGED) to test_fused_attn.py -- previously there was no symmetric 128/128 THD backward coverage -- and an NVTE_CK_IS_V3_ATOMIC_FP32=0 (atomic16 / dq_shuffle) CI run of the THD backward tests, which the default fp32 path never exercised. Verified on MI355X (gfx950): THD padding_causal backward dQ cos 0.0036 -> 1.0000 with atomic_fp32=0; per-segment all correct across [256], [100,80,76], [128,128], [64x4], [200,56], for both the v3 (dq_shuffle) and v2 paths; atomic_fp32=1 unchanged. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| fmha_args.stride_dq_acc = 128; | ||
| fmha_args.nhead_stride_dq_acc = static_cast<int64_t>(padded_sq * 128); | ||
| fmha_args.batch_stride_dq_acc = static_cast<int64_t>(args.h * padded_sq * 128); | ||
| fmha_args.split_stride_dq_acc = static_cast<int>(args.b * args.h * padded_sq * 128); |
There was a problem hiding this comment.
Nit / maintainability: the 128 here is the fixed head-dim of the v3 asm bwd_hd128_dq_shuffle_group kernel, and it is repeated (uncommented) at fused_attn_ck.cpp:759 in the sizing pass. The whole branch silently assumes that whenever ck_attn_bwd_uses_v3(args) returns true, args.d_qk == 128. That's true today, but if AITER ever adds an hd64 or hd192 v3 variant, the strides here and the buffer size on the other side will both be wrong in the same way — undetectable without a repro.
Two low-cost hardening options:
- Name it (
static constexpr int64_t kV3DqAccHeadDim = 128;) in a shared header so the two sites reference the same constant, or - Add a
NVTE_CHECK(args.d_qk == 128, ...)inside this branch so a future kernel-support expansion fails loudly instead of silently corrupting dQ again.
Not blocking — the fix as written is correct for the current kernel set.
| // case; ck_attn_bwd sets the strides. Non-ragged / atomic_fp32 only ever use the fp32-packed layout. | ||
| const size_t dq_acc_fp32_bytes = nsplits*h*max_tokens_q*d_qk*sizeof(float); | ||
| const size_t dq_acc_padded_sq = ((s_q + 15) / 16) * 16; | ||
| const size_t dq_acc_bf16_bytes = nsplits*b*h*dq_acc_padded_sq*128*nvte_dtype_size(dtype); |
There was a problem hiding this comment.
Two small points on the bf16 layout sizing:
-
The
128here mirrors the hard-coded128inck_fused_attn_bwd.cpp:617-620(v3bwd_hd128_dq_shuffle_grouphead dim). See the sibling comment there — sharing a named constant, or assertingd_qk == 128on the CK side, would make the coupling explicit. -
nvte_dtype_size(dtype)"happens to" give 2 here because the v3 atomic16 path is only reached for 16-bit input dtypes. If some future path admits fp8/fp4 backwards through this branch, the buffer would be under-sized (strides inck_fused_attn_bwd.cppare computed assuming the element size is 2 — element strides are in units of dtype). Usingsizeof(uint16_t)or a2literal with a// atomic16comment would make the layout invariant local and more robust than tying it to the input dtype.
Non-blocking nits; correctness is fine for the current set of supported dtypes.
|
Reviewed the THD/ragged dQ backward fix for the bf16 dq_shuffle path (4 files, +62/-4). Verdict: Looks good. Root cause and fix are well-explained inline; the sizing/stride switch is correctly gated on Nits (non-blocking): Two inline comments on the hard-coded Copyright headers: OK — all four modified files carry AMD headers ending in 2026. |
| // The dq_acc buffer is sized to hold either layout in fused_attn_ck.cpp. | ||
| if (args.is_group_mode() && !args.is_v3_atomic_fp32 && ck_attn_bwd_uses_v3(args)) { | ||
| const int64_t padded_sq = static_cast<int64_t>(((args.s_q + 15) / 16) * 16); | ||
| fmha_args.stride_dq_acc = 128; |
There was a problem hiding this comment.
Is this only for hdim 128 cases?
| // the v3 asm path actually runs (ck_attn_bwd_uses_v3 probe) -- v2/fallback keeps the fp32-packed layout. | ||
| // The dq_acc buffer is sized to hold either layout in fused_attn_ck.cpp. | ||
| if (args.is_group_mode() && !args.is_v3_atomic_fp32 && ck_attn_bwd_uses_v3(args)) { | ||
| const int64_t padded_sq = static_cast<int64_t>(((args.s_q + 15) / 16) * 16); |
There was a problem hiding this comment.
1). I presume this pad16(s_q) is coming from aiter source codes, better put a code pointer here for future reference
2). previously we never test cases with sq not multiple of 16. So if we wanted to put this in, we better add a testcase for non 16 multiple sq and also make sure ck flow does not break
| fmha_args.stride_dq_acc = 128; | ||
| fmha_args.nhead_stride_dq_acc = static_cast<int64_t>(padded_sq * 128); | ||
| fmha_args.batch_stride_dq_acc = static_cast<int64_t>(args.h * padded_sq * 128); | ||
| fmha_args.split_stride_dq_acc = static_cast<int>(args.b * args.h * padded_sq * 128); |
There was a problem hiding this comment.
Actually, aiter do not have deterministic path so split stride should not affect the results. (their nsplits=1)
| // case; ck_attn_bwd sets the strides. Non-ragged / atomic_fp32 only ever use the fp32-packed layout. | ||
| const size_t dq_acc_fp32_bytes = nsplits*h*max_tokens_q*d_qk*sizeof(float); | ||
| const size_t dq_acc_padded_sq = ((s_q + 15) / 16) * 16; | ||
| const size_t dq_acc_bf16_bytes = nsplits*b*h*dq_acc_padded_sq*128*nvte_dtype_size(dtype); |
There was a problem hiding this comment.
This will over allocate for hdim 64 and under allocate for hdim 192 cases
| const size_t dq_acc_padded_sq = ((s_q + 15) / 16) * 16; | ||
| const size_t dq_acc_bf16_bytes = nsplits*b*h*dq_acc_padded_sq*128*nvte_dtype_size(dtype); | ||
| const bool dq_acc_max_of_both = is_ragged && !nvte_ck_is_v3_atomic_fp32; | ||
| const size_t dq_acc_bytes = (dq_acc_max_of_both && dq_acc_bf16_bytes > dq_acc_fp32_bytes) |
There was a problem hiding this comment.
I recall we have a is_v3_api_check which we can use now
… test Review feedback on the THD/ragged dq_shuffle dq_acc layout (wangye805): - Name the fixed v3 dq_shuffle head dim as ck_fused_attn::kV3DqAccHeadDim (128) and the seqlen alignment as kV3DqAccSeqAlign (16) in the shared header, and reference them from both the dq_acc sizing (fused_attn_ck.cpp) and the stride override (ck_fused_attn_bwd.cpp) so the two sites can't drift. - Add a code pointer to AITER's asm_mha_varlen_bwd.cu (the atomic16 dq_acc layout / pad16 origin) at both sites. - Assert d_qk == kV3DqAccHeadDim in the stride override: the only ragged dq_shuffle kernel is bwd_hd128_dq_shuffle_group, so a future hd64/hd192 dq_shuffle would otherwise silently corrupt dQ again -- now it fails loudly. - Gate the bf16 dq_acc sizing on d_qk == kV3DqAccHeadDim (the exact hd128-only condition) instead of just ragged/non-atomic, so it no longer over-sizes for hd64/hd192. Use sizeof(uint16_t) with an // atomic16 comment for the element size rather than tying it to the input dtype. - Note that split_stride_dq_acc is a no-op here (v3 has no deterministic mode, so nsplits==1); set it consistently anyway. - Add a non-16-multiple seqlen THD config (2044) to test_fused_attn.py to exercise the pad16(s_q) rounding and confirm the CK flow does not break. Verified on MI355X (gfx950): dQ cos 1.0 for v3/v2/atomic_fp32=1 and per-segment across [256],[100,80,76],[128,128],[64x4],[200,56]; non-16 seqlen (segments 103/77/61, S=241 -> pad16 256) dQ cos 1.0, finite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Thanks @wangye805 — addressed in 60f2d12:
|
Summary
On ROCm/gfx950 the CK fused-attention backward silently corrupts dQ for THD/ragged (packed / document-masked) attention on the default bf16 dq-accumulation path (
NVTE_CK_IS_V3_ATOMIC_FP32=0). Forward, dK and dV are correct.Root cause
build_bwd_fmha_argsallocates thedq_accscratch as fp32-packed(nsplits, H, total_q, d_qk)withbatch_stride_dq_acc = 0. That matches the fp32dq_convertpost-kernel, but the bf16dq_shufflepost-kernel expects a per-segment padded layout — withbatch_stride=0every ragged segment pastcu_seqlensoffset 0 is written to the offset-0 slot, so dQ is correct only for the first segment and garbage for the rest.Fix
When the v3 asm path actually runs — probed via
ck_attn_bwd_uses_v3(added in #662) — switchdq_accto AITER's per-segment bf16 layout(nsplits, B, H, pad16(s_q), 128)with matchingstride/nhead/batch/splitstrides. The v2/fallback path and non-ragged/atomic_fp32=1keep the fp32-packed layout.fused_attn_ck.cppsizes the scratch to hold either layout (the layout choice needs a GPU probe that must not run in the workspace-sizing pass).Tests
test_fused_attn.py. Previously there was no symmetric 128/128 THD backward coverage (only asymmetric 128/64) — exactly the llama3 shape.NVTE_CK_IS_V3_ATOMIC_FP32=0(atomic16 /dq_shuffle) CI run of the THD backward tests inci/jax.sh. The default fp32 path never exercised the bf16dq_accpath.Verification (MI355X / gfx950)
TE THD
padding_causalbackward vs a pure-JAX block-diagonal reference:atomic_fp32=0; per-segment all correct across[256],[100,80,76],[128,128],[64×4],[200,56], for both the v3 (dq_shuffle) and v2 paths.bwd_hd128_dq_shuffle_groupkernel (fix is on the fast path, not a fallback).atomic_fp32=1unchanged; end-to-end MaxText llama3.1-8B block-diagonal THD packing converges to eval-loss 3.3 (previously stalled ~6.1 and NaN'd).Builds on the V3 API check mechanism from #662. Supersedes #657 (which targeted an earlier base before #662).
🤖 Generated with Claude Code