Skip to content

flash-attn-4 4.0.0b11: DotProductAttention batched backward returns wrong dQ/dK/dV at head_dim=256 on Blackwell (sm_100) #3216

Description

@XiaohanZhangCMU

Summary

On a B200 (sm_100), transformer_engine.pytorch.DotProductAttention with the FlashAttention-4 backend (flash-attn-4==4.0.0b11) computes a correct forward but an incorrect backward for head_dim=256 when the batch contains more than one sequence. The returned dQ is ~5x larger than the true gradient and points in nearly the opposite direction (cosine ≈ -0.11). The UnfusedDotProductAttention backend and a plain fp32 PyTorch SDPA reference agree with each other to <1% on the same inputs. This silently corrupts training gradients (grad_norm explodes ~1000x, loss diverges) while the forward/loss looks normal.

Environment

  • GPU: NVIDIA B200, compute capability sm_100 (10, 0)
  • flash-attn-4==4.0.0b11
  • TransformerEngine 2.16.0
  • torch 2.11.0+cu128, CUDA 12.8
  • nvidia-cudnn-cu12 9.19.0.56 (as pinned by flash-attn-4)

Conditions

  • DotProductAttention, qkv_format="sbhd", attn_mask_type="causal", num_attention_heads=16, num_gqa_groups=2, head_dim=256, dtype bfloat16.
  • Input-dependent: random Gaussian Q/K/V do not trigger it (even with a large incoming gradient). It reproduces with real transformer activations (attached).
  • Requires batch >= 2 sequences: a batch=1 slice of the same data is correct under FA4; batch>=2 is wrong.
  • Forward is correct (matches fp32 to ~1e-3). Only the backward (dQ/dK/dV) is wrong.
  • cuDNN-fused attention is unavailable for head_dim=256 ("No dot product attention backend is available"), so on Blackwell the usable backends for this shape are FA4 (buggy) and unfused (correct, slow).

Reproduce

Attached: fa4_repro_data.pt (real captured q,k,v,d_out + meta; sbhd shape [384, 2, 16, 256], bf16 — the minimal 2-sequence batch) and the script below.

# FA4 (buggy):
NVTE_FLASH_ATTN=1 NVTE_FUSED_ATTN=0 NVTE_UNFUSED_ATTN=0 python fa4_bug_repro.py fa4_repro_data.pt
# Unfused (correct, sanity):
NVTE_FLASH_ATTN=0 NVTE_FUSED_ATTN=0 NVTE_UNFUSED_ATTN=1 python fa4_bug_repro.py fa4_repro_data.pt
fa4_bug_repro.py
import math, sys, torch
import transformer_engine, transformer_engine.pytorch as te

def fp32_reference(q, k, v, causal):  # sbhd q[s,b,hq,d], k/v[s,b,hkv,d] -> [s,b,hq,d]
    s, b, hq, d = q.shape; hkv = k.shape[2]
    qt = q.permute(1,2,0,3).float(); kt = k.permute(1,2,0,3).float(); vt = v.permute(1,2,0,3).float()
    rep = hq // hkv
    if rep > 1: kt = kt.repeat_interleave(rep, 1); vt = vt.repeat_interleave(rep, 1)
    sc = torch.matmul(qt, kt.transpose(-1,-2)) / math.sqrt(d)
    if causal: sc = sc.masked_fill(torch.triu(torch.ones(s,s,device=q.device,dtype=torch.bool),1), float("-inf"))
    return torch.matmul(torch.softmax(sc,-1), vt).permute(2,0,1,3).contiguous()

cap = torch.load(sys.argv[1], map_location="cpu"); m = cap["meta"]; dev = torch.device("cuda")
hq, hkv, hd = m["num_attention_heads"], m["num_gqa_groups"], m["kv_channels"] or cap["q"].shape[-1]
causal = "causal" in str(m["attn_mask_type"]).lower()
def leaf(n, dt): x = cap[n].to(dev).to(dt).detach().clone(); x.requires_grad_(True); return x
rq, rk, rv = leaf("q",torch.float32), leaf("k",torch.float32), leaf("v",torch.float32)
rout = fp32_reference(rq, rk, rv, causal).reshape(cap["d_out"].shape); rout.backward(cap["d_out"].to(dev).float())
q, k, v = leaf("q",torch.bfloat16), leaf("k",torch.bfloat16), leaf("v",torch.bfloat16)
dpa = te.DotProductAttention(num_attention_heads=hq, kv_channels=hd, num_gqa_groups=hkv,
    attention_dropout=0.0, qkv_format="sbhd", attn_mask_type=m["attn_mask_type"] or "causal").to(dev)
out = dpa(q, k, v); out.backward(cap["d_out"].to(dev).to(torch.bfloat16))
print(f"GPU={torch.cuda.get_device_name(0)} sm={torch.cuda.get_device_capability(0)} TE={transformer_engine.__version__}")
for nm, a, b in [("out",rout,out),("dQ",rq.grad,q.grad),("dK",rk.grad,k.grad),("dV",rv.grad,v.grad)]:
    a, b = a.float().flatten(), b.float().flatten()
    rel = (a-b).norm().item()/(a.norm().item() or 1.0); cos = torch.nn.functional.cosine_similarity(a,b,0).item()
    print(f"  {nm}: rel_l2={rel:.3e} cos={cos:+.4f} ref_norm={a.norm():.1f} te_norm={b.norm():.1f}"
          + ("  <-- WRONG" if (rel>5e-2 or cos<0.99) else ""))

Observed (vs fp32 reference, identical input)

fa4_repro_data.pt ([384, 2, 16, 256]), FA4 backend:

tensor rel_l2 vs fp32 ref cos ref norm FA4 norm
out (fwd) 1.7e-3 +1.000 577.9 577.9
dQ 4.56 -0.11 5,441 23,588
dK 3.48 +0.12 2,531 8,747
dV 1.75 +0.07 8,275 12,499

Same input through UnfusedDotProductAttention matches the fp32 reference (dQ rel_l2 6.7e-3, cos +1.000). A batch=1 slice of the same tensors is correct under FA4; an 8-sequence batch behaves the same as batch=2 (FA4 dQ 85,710 vs ref 17,487). Since batch=1 is correct and batch>=2 is not, this looks like a batch-indexing/accumulation bug in the FA4 backward at head_dim=256, not a precision issue.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions