Skip to content

Commit 6c32c0c

Browse files
undefinedclaude
andcommitted
[CuTe,Bwd,Sm100] Support head_dim not a multiple of 32 in backward
Backward on SM100 (B200/B300) crashed or silently produced wrong dK/dV when head_dim was not a multiple of 32 (e.g. ViT head_dim=72, 104). Fixes #2492. Three independent root causes: 1. flash_bwd_preprocess.py: the O/dO load predicate (predicate_k) carries a CPY_M mode broadcast over m, but the copy source is sliced per-m (tOgO[None, m, None]) without slicing the predicate. The vectorized 128-bit copy atom's predicate-shape verifier then rejects the extra mode ("expects (1,(k)) but got (1,(cpy_m,k))"). Slice the predicate per-m to match, as flash_fwd.py / flash_bwd.py already do. 2. flash_bwd_sm100.py: tile_hdim padded head_dim to a multiple of 16, but the dQ accumulator reduce requires a multiple of 32 (assert tile_hdim % dQ_reduce_ncol == 0) and both the preprocess kernel and interface already round to 32. head_dim=72 -> 80 tripped the assert. Pad to 32 so all three agree (96); this reuses the already-working head_dim=96 path. 3. interface.py: varlen MHA uses the non-TMA dK/dV epilogue store, which writes tile_hdim (mult of 32) columns per row with no head-dim OOB predication (its 16-wide vectorization cannot mask a 72 boundary). It therefore spilled past the real head_dim and corrupted neighboring dK/dV. Route that store through a head-dim-padded scratch buffer, then slice back. Validated on B300 (SM103) against torch.nn.functional.scaled_dot_product_attention (fp32 reference): dense + varlen, causal + non-causal, head_dim in {64, 72, 80, 104, 128}, MHA and GQA(hd=128). dq/dk/dv max relative error 2e-3..7e-3 (bf16 precision), matching the multiple-of-32 baselines. No regression on aligned head dims. End-to-end Qwen3.5-VL training (ViT head_dim=72, varlen) trains with a decreasing loss and finite grad norm. Note: this is an alternative to #2633. #2633 keeps V padding at a multiple of 16 and predicates the non-TMA store via a reduced tmem Repetition, which is neater for varlen; however in our testing that leaves the dense (TMA-store) dV path numerically wrong for head_dim % 32 != 0 (large relative error), whereas padding V to 32 keeps both the dense and varlen paths correct. Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
1 parent 5835c73 commit 6c32c0c

3 files changed

Lines changed: 43 additions & 7 deletions

File tree

flash_attn/cute/flash_bwd_preprocess.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
# So the main backward kernel is unchanged; we just replace D with D' = D - dLSE here.
1414
import math
1515
import operator
16-
from functools import partial
1716
from typing import Callable, Type, Optional
1817

1918
import cuda.bindings.driver as cuda
@@ -365,8 +364,8 @@ def kernel(
365364
tOpO = None
366365
if const_expr(self.check_hdim_v_oob):
367366
tOpO = copy_utils.predicate_k(tOcO, limit=headdim_v)
368-
# Each copy will use the same predicate
369-
copy = partial(copy_utils.copy, pred=tOpO)
367+
# Each copy uses the predicate sliced to the current m (see loop below).
368+
copy = copy_utils.copy
370369

371370
tOrO = cute.make_rmem_tensor_like(tOgO)
372371
tOrdO = cute.make_rmem_tensor_like(tOgdO)
@@ -378,8 +377,13 @@ def kernel(
378377
# Instead of using tOcO, we using t0OcO and subtract the offset from the limit.
379378
# This is bc the entries of t0OcO are known at compile time.
380379
if t0OcO[0, m, 0][0] < seqlen_limit - tOcO[0][0]:
381-
copy(tOgO[None, m, None], tOrO[None, m, None])
382-
copy(tOgdO[None, m, None], tOrdO[None, m, None])
380+
# The predicate carries a CPY_M mode (broadcast over m); slice it to the
381+
# current m so its shape matches the m-sliced copy source. Otherwise the
382+
# vectorized copy atom's predicate-shape verification fails when head_dim_v
383+
# is not a multiple of the copy-atom width (e.g. 72, 104).
384+
tOpO_cur = tOpO[None, m, None] if const_expr(tOpO is not None) else None
385+
copy(tOgO[None, m, None], tOrO[None, m, None], pred=tOpO_cur)
386+
copy(tOgdO[None, m, None], tOrdO[None, m, None], pred=tOpO_cur)
383387
# O and dO loads are done; signal that the next kernel can start.
384388
# Correctness is ensured by griddepcontrol_wait() in bwd_sm90 before it reads our outputs.
385389
if const_expr(self.use_pdl):

flash_attn/cute/flash_bwd_sm100.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ def __init__(
6868
has_aux_tensors: cutlass.Constexpr = False,
6969
q_subtile_factor: cutlass.Constexpr[int] = 1,
7070
):
71-
# padding head_dim to a multiple of 16 as k_block_size
72-
hdim_multiple_of = 16
71+
# Pad head_dim to a multiple of 32 (k_block_size). Must stay a multiple of 32 so
72+
# that the dQ accumulator reduce (dQ_reduce_ncol up to 32) tiles evenly and matches
73+
# both the preprocess kernel and the interface's head_dim_rounded (also mult of 32).
74+
hdim_multiple_of = 32
7375
self.tile_hdim = int(math.ceil(head_dim / hdim_multiple_of) * hdim_multiple_of)
7476
head_dim_v = head_dim_v if head_dim_v is not None else head_dim
7577
self.same_hdim_kv = head_dim == head_dim_v

flash_attn/cute/interface.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1611,6 +1611,28 @@ def _flash_attn_bwd(
16111611
dtype = torch2cute_dtype_map[q.dtype]
16121612
current_stream = cute.runtime.make_fake_stream(use_tvm_ffi_env_stream=True)
16131613

1614+
# Varlen MHA uses the non-TMA dK/dV epilogue store, which writes tile_hdim (a
1615+
# multiple of 32) columns per row and has no head-dim OOB predication (unlike the
1616+
# TMA store used elsewhere). When head_dim is not a multiple of 32 the store would
1617+
# spill past the real head_dim into neighboring memory and corrupt dK/dV. Route the
1618+
# store through a head-dim-padded scratch buffer, then slice back to head_dim.
1619+
head_dim_v_rounded = (head_dim_v + 32 - 1) // 32 * 32
1620+
dkv_hdim_pad = (
1621+
not dKV_postprocess
1622+
and cu_seqlens_k is not None
1623+
and (head_dim != head_dim_rounded or head_dim_v != head_dim_v_rounded)
1624+
)
1625+
dk_final, dv_final = dk, dv
1626+
if dkv_hdim_pad:
1627+
if head_dim != head_dim_rounded:
1628+
dk = torch.empty(
1629+
*dk_final.shape[:-1], head_dim_rounded, dtype=out_torch_dtype, device=device
1630+
)
1631+
if head_dim_v != head_dim_v_rounded:
1632+
dv = torch.empty(
1633+
*dv_final.shape[:-1], head_dim_v_rounded, dtype=out_torch_dtype, device=device
1634+
)
1635+
16141636
if deterministic:
16151637
dQ_semaphore = torch.zeros(batch_size, num_head, seqlen_q_rounded // m_block_size, cluster_size, dtype=torch.int32, device=device)
16161638
else:
@@ -1998,6 +2020,14 @@ def _flash_attn_bwd(
19982020
cluster_size=cluster_size,
19992021
)
20002022

2023+
if dkv_hdim_pad:
2024+
if not is_fake_mode():
2025+
if head_dim != head_dim_rounded:
2026+
dk_final.copy_(dk[..., :head_dim])
2027+
if head_dim_v != head_dim_v_rounded:
2028+
dv_final.copy_(dv[..., :head_dim_v])
2029+
dk, dv = dk_final, dv_final
2030+
20012031
return dq, dk, dv
20022032

20032033

0 commit comments

Comments
 (0)