Skip to content

Commit bb1a1ff

Browse files
JohnQinAMDclaude
andauthored
[Bugfix][Kernel] Fix fp8 dense attention: softmax normalisation and an int32 dim overflow (#1020)
Bug 1: rows lose softmax mass Fix: scale P up before the cast so it uses e4m3's whole range. l_row scales with it, so nothing changes but the tail that no longer underflows. Free — the scale rides an FMA that already runs, as an optional bias on _scale_sub_score_pair; bf16 never passes one. Bug 2: int32 overflow on the shape Fix: one launch per batch entry when the total would overflow. Entries are independent and a leading slice of a contiguous tensor is still contiguous, so no copy and no kernel change. Co-authored-by: yanyuan.qin@amd.com <yanyuan.qin@amd.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 4a6f955 commit bb1a1ff

4 files changed

Lines changed: 364 additions & 10 deletions

File tree

kernels/attention/flash_attn_fp8_gfx950.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def build_flash_attn_dualwave_swp_fp8_module(
4343
waves_per_eu=2,
4444
daz=True,
4545
dualwave_swp_lazy_rescale=True,
46+
rescale_threshold=6.0,
4647
dualwave_swp_setprio=True,
4748
dualwave_swp_debug_lazy_counts=False,
4849
dualwave_swp_enable_stagger=True,
@@ -90,6 +91,7 @@ def build_flash_attn_dualwave_swp_fp8_module(
9091
waves_per_eu=waves_per_eu,
9192
daz=daz,
9293
dualwave_swp_lazy_rescale=dualwave_swp_lazy_rescale,
94+
rescale_threshold=rescale_threshold,
9395
dualwave_swp_setprio=dualwave_swp_setprio,
9496
dualwave_swp_debug_lazy_counts=dualwave_swp_debug_lazy_counts,
9597
dualwave_swp_enable_stagger=dualwave_swp_enable_stagger,
@@ -219,6 +221,13 @@ def _mask_pair(v_s_a, v_s_b, j):
219221
return softmax_helper.causal_mask_pair_if_needed(v_s_a, v_s_b, j)
220222
return v_s_a, v_s_b
221223

224+
def _correct_o(v_o, m_row, l_row, m_tile):
225+
if const_expr(traits.DUALWAVE_SWP_LAZY_RESCALE):
226+
return softmax_helper.lazy_correct_o(v_o, m_row, l_row, m_tile)
227+
m_new, corr = softmax_helper.rescale_from_tile_max(m_row, m_tile)
228+
softmax_helper.scale_o(v_o, corr)
229+
return v_o, m_new, softmax_helper.apply_l_rescale(l_row, corr)
230+
222231
def _merge_tile_max(v_s_a, v_s_b):
223232
m_tile = softmax_helper.max2(softmax_helper.reduce_max(v_s_a), softmax_helper.reduce_max(v_s_b))
224233
if const_expr(traits.CAUSAL):
@@ -298,7 +307,7 @@ def _ring_wrap(x):
298307
v_s_b = _mask_sub(v_s_b, j + fx.Index(1))
299308
v_s_a, v_s_b = _mask_pair(v_s_a, v_s_b, j)
300309
m_tile = _merge_tile_max(v_s_a, v_s_b)
301-
v_o, m_new, l_row = softmax_helper.lazy_correct_o(v_o, m_row, l_row, m_tile)
310+
v_o, m_new, l_row = _correct_o(v_o, m_row, l_row, m_tile)
302311
v_o = softmax_helper.anchor_v_o(v_o)
303312
v_p_a, l_row = _softmax_part(v_s_a, l_row, m_new)
304313
_phase_bar()
@@ -326,7 +335,7 @@ def _ring_wrap(x):
326335
v_o = _pv_part(v_p_b, v_v_b, v_o)
327336
else:
328337
m_tile = _merge_tile_max(v_s_a, v_s_b)
329-
v_o, m_new, l_row = softmax_helper.lazy_correct_o(v_o, m_row, l_row, m_tile)
338+
v_o, m_new, l_row = _correct_o(v_o, m_row, l_row, m_tile)
330339
v_o = softmax_helper.anchor_v_o(v_o)
331340

332341
v_o, l_row = _subtile_tail(v_s_a, v_v_a, v_o, l_row, m_new)
@@ -624,6 +633,7 @@ def _compile(
624633
waves_per_eu=waves_per_eu,
625634
daz=daz,
626635
dualwave_swp_lazy_rescale=dualwave_swp_lazy_rescale,
636+
rescale_threshold=rescale_threshold,
627637
dualwave_swp_setprio=dualwave_swp_setprio,
628638
dualwave_swp_debug_lazy_counts=dualwave_swp_debug_lazy_counts,
629639
dualwave_swp_enable_stagger=dualwave_swp_enable_stagger,

kernels/attention/flash_attn_interface.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,24 @@
4343

4444
# Short varlen/paged cases use the lightweight generic path.
4545
_VARLEN_LIGHT_MAX_SEQ = 256
46+
# Largest flat element count the fp8 C-ABI can address; see the split below.
47+
_FP8_MAX_FLAT_ELEMS = 2**31
48+
# fp8 lifts P by log2(448) - RESCALE_THRESHOLD. Past this KV length enough tiles
49+
# sit far below the running max that the extra two log2 units matter more than
50+
# the ~0.3% the lower threshold costs there; below it the two are equally
51+
# accurate and 6 is cheaper.
52+
_FP8_LONG_SEQ = 4096
4653
_DENSE_LIGHT_CU_FALLBACK = 256
4754
_DENSE_DUALWAVE_MIN_SEQ = 256
4855
_DENSE_DUALWAVE_LARGE_BATCH = 8
4956
_DENSE_DUALWAVE_MIN_SEQ_LARGE_BATCH = 192
5057
_DENSE_M256_MIN_TOKENS = 4096
5158

5259

60+
def _fp8_rescale_threshold(seqlen_kv: int) -> float:
61+
return 6.0 if seqlen_kv <= _FP8_LONG_SEQ else 4.0
62+
63+
5364
def _dtype_str(t: torch.Tensor) -> str:
5465
s = _DTYPE_MAP.get(t.dtype)
5566
if s is None:
@@ -173,6 +184,7 @@ def _build_dense_fp8(
173184
num_heads: int,
174185
num_kv_heads: int,
175186
causal: bool,
187+
rescale_threshold: float,
176188
waves_per_eu: int,
177189
daz: bool,
178190
lazy_rescale: bool,
@@ -190,6 +202,7 @@ def _build_dense_fp8(
190202
num_kv_heads=num_kv_heads,
191203
waves_per_eu=waves_per_eu,
192204
daz=daz,
205+
rescale_threshold=rescale_threshold,
193206
dualwave_swp_lazy_rescale=lazy_rescale,
194207
dualwave_swp_setprio=setprio,
195208
dualwave_swp_enable_stagger=enable_stagger,
@@ -821,6 +834,58 @@ def flydsl_flash_attn_func(
821834
raise NotImplementedError(f"flydsl_flash_attn_func: {_name} is not supported for fp8")
822835
if not _t.is_cuda or _t.device != q.device:
823836
raise ValueError(f"flydsl_flash_attn_func: {_name} must be a CUDA tensor on {q.device}, got {_t.device}")
837+
838+
# The fp8 path flattens Q/K/V/O to 1-D and the C-ABI packs a dynamic dim as
839+
# int32, so a launch aborts once any of them reaches 2**31 (S >= 131072 at
840+
# D=128, H=64). K/V are checked too: cross-attention can hold a short Q and
841+
# an over-long KV. Batch entries are independent and a leading slice of a
842+
# contiguous tensor is still contiguous, so one launch per entry divides the
843+
# flat dim by B at no copy. bf16 passes the natural 4-D shape and is exempt.
844+
if (
845+
dtype_str == "fp8"
846+
and not paged_kv
847+
and cu_seqlens_q is None
848+
and cu_seqlens_kv is None
849+
and q.dim() == 4
850+
and max(q.numel(), k.numel(), v.numel()) >= _FP8_MAX_FLAT_ELEMS
851+
):
852+
if q.shape[0] == 1:
853+
# Out of batch to divide by. Launching would abort inside the C ABI
854+
# with a struct.error naming neither the tensor nor the limit.
855+
raise NotImplementedError(
856+
"flydsl_flash_attn_func: fp8 flattens Q/K/V/O and packs the dynamic dim as int32, so a "
857+
f"single batch entry cannot exceed {_FP8_MAX_FLAT_ELEMS} elements; got q={q.numel()}, "
858+
f"k={k.numel()}, v={v.numel()}. Shorten the sequence or use bf16."
859+
)
860+
kw = dict(
861+
causal=causal,
862+
num_kv_heads=num_kv_heads,
863+
max_seqlen_q=max_seqlen_q,
864+
max_seqlen_kv=max_seqlen_kv,
865+
cross_seqlen=cross_seqlen,
866+
kv_cache_layout=kv_cache_layout,
867+
num_kv_splits=num_kv_splits,
868+
q_descale=q_descale,
869+
k_descale=k_descale,
870+
v_descale=v_descale,
871+
waves_per_eu=waves_per_eu,
872+
daz=daz,
873+
dualwave_swp_lazy_rescale=dualwave_swp_lazy_rescale,
874+
dualwave_swp_setprio=dualwave_swp_setprio,
875+
dualwave_swp_enable_stagger=dualwave_swp_enable_stagger,
876+
debug_counts=debug_counts,
877+
stream=stream,
878+
)
879+
if out is None:
880+
# Allocate once and hand each launch its own slice. Concatenating
881+
# afterwards would consume the parts on the ambient stream while the
882+
# kernels are still running on `stream`, and would hold two full
883+
# outputs at a size where one is already several GB.
884+
out = torch.empty(q.shape, dtype=torch.bfloat16 if dtype_str == "fp8" else q.dtype, device=q.device)
885+
for i in range(q.shape[0]):
886+
sl = slice(i, i + 1)
887+
flydsl_flash_attn_func(q[sl].contiguous(), k[sl].contiguous(), v[sl].contiguous(), out=out[sl], **kw)
888+
return out
824889
if has_bias:
825890
if bias.dtype != q.dtype:
826891
raise ValueError(f"flydsl_flash_attn_func: bias dtype must match q dtype {q.dtype}, got {bias.dtype}")
@@ -1046,6 +1111,7 @@ def flydsl_flash_attn_func(
10461111
num_heads=H,
10471112
num_kv_heads=num_kv_heads,
10481113
causal=causal,
1114+
rescale_threshold=_fp8_rescale_threshold(int(Skv)),
10491115
waves_per_eu=waves_per_eu,
10501116
daz=daz,
10511117
lazy_rescale=dualwave_swp_lazy_rescale,

kernels/attention/flash_attn_utils.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -411,17 +411,24 @@ def _sub_score_pair(v_s, row_max, fm_fast):
411411
return Vec.from_elements(lo_sub, fx.Float32).ir_value(), Vec.from_elements(hi_sub, fx.Float32).ir_value()
412412

413413

414-
def _scale_sub_score_pair(v_s, row_max_raw, scale, zero_f, fm_fast):
414+
def _scale_sub_score_pair(v_s, row_max_raw, scale, zero_f, fm_fast, bias=None):
415415
"""Fused softmax-scale + row-max subtraction (optimization 1-A).
416416
417-
Returns ``scale * (v_s - row_max_raw)`` per element via a single FMA
418-
(``fma(s, scale, -scale*row_max_raw)``), so the fp8 QK MMA can emit raw
417+
Returns ``scale * (v_s - row_max_raw) + bias`` per element via a single FMA
418+
(``fma(s, scale, bias - scale*row_max_raw)``), so the fp8 QK MMA can emit raw
419419
(un-scaled) logits and reduce_max can run in the raw domain (scale > 0 is
420420
order-preserving). Replaces the separate post-QK scale multiply + subtract.
421421
``-inf`` masked lanes stay ``-inf`` (scale > 0), matching the un-fused path.
422+
423+
``bias`` lands in the FMA's addend, so a caller needing ``exp2`` to produce
424+
``2**bias * P`` pays nothing -- see ``DualwaveFp8SoftmaxHelper.sub_m``.
422425
"""
423426
s_lo, s_hi = v_s
424427
neg_scaled_max = zero_f - scale * row_max_raw
428+
if bias is not None:
429+
# exp2 lands on 2**bias * P instead of P, at no extra instruction: the
430+
# FMA's addend absorbs it.
431+
neg_scaled_max = neg_scaled_max + bias
425432
scale_v = Vec.from_elements([scale], fx.Float32).broadcast_to(16)
426433
nsm_v = Vec.from_elements([neg_scaled_max], fx.Float32).broadcast_to(16)
427434
lo = fx.fma(Vec(s_lo), scale_v, nsm_v, fastmath=fm_fast)
@@ -1857,6 +1864,7 @@ def cache_tag(self):
18571864
self.WAVES_PER_EU,
18581865
self.DAZ,
18591866
self.DUALWAVE_SWP_LAZY_RESCALE,
1867+
self.DUALWAVE_SWP_RESCALE_THRESHOLD,
18601868
self.DUALWAVE_SWP_SETPRIO,
18611869
self.DUALWAVE_SWP_DEBUG_LAZY_COUNTS,
18621870
self.DUALWAVE_SWP_ENABLE_STAGGER,
@@ -1885,6 +1893,7 @@ def _make_dualwave_swp_fp8_traits(
18851893
num_heads,
18861894
num_kv_heads,
18871895
head_dim,
1896+
rescale_threshold,
18881897
causal=True,
18891898
waves_per_eu=2,
18901899
daz=True,
@@ -2035,7 +2044,7 @@ def _make_dualwave_swp_fp8_traits(
20352044
URV_DC_AXIS0_BF=snrpt_bf * vls_bf,
20362045
URV_DC_AXIS1_BF=32,
20372046
URV_I5_BF=d128_bf,
2038-
DUALWAVE_SWP_RESCALE_THRESHOLD=8.0,
2047+
DUALWAVE_SWP_RESCALE_THRESHOLD=rescale_threshold,
20392048
SCHED_MFMA_MASK=0x008,
20402049
SCHED_VALU_MASK=0x002,
20412050
SCHED_EXP_MASK=0x400,
@@ -4406,7 +4415,7 @@ def init_types_and_constants(self):
44064415
self.c_neg_inf = fx.Float32(float("-inf"))
44074416
self.c_neg_floor = fx.Float32(-3.0e38)
44084417
self.c_zero_f = fx.Float32(0.0)
4409-
self.c_eight_f = fx.Float32(traits.DUALWAVE_SWP_RESCALE_THRESHOLD)
4418+
self.c_rescale_thr_f = fx.Float32(traits.DUALWAVE_SWP_RESCALE_THRESHOLD)
44104419
self.c_zero_v16f32 = Vec.filled(16, 0.0, fx.Float32)
44114420

44124421
def init_runtime_indices(self):
@@ -5101,8 +5110,24 @@ def max2(self, a, b):
51015110
def floor_masked_max(self, row_max):
51025111
return fx.maxnumf(row_max, self.c_neg_floor)
51035112

5113+
# log2 of e4m3's largest finite value, 448.
5114+
_P_HEADROOM_LOG2 = 8.807354922057604
5115+
51045116
def sub_m(self, v_s, row_max):
5105-
return _scale_sub_score_pair(v_s, row_max, self.c_logit_scale, self.c_zero_f, self.fm_fast)
5117+
# P is cast to e4m3, whose smallest subnormal is 2**-9, so a softmax
5118+
# over thousands of keys loses its tail to flush-to-zero -- while l_row,
5119+
# summed before the cast, still counts it. Scaling P up first uses the
5120+
# format's whole range; l_row scales with it, so the output is unchanged
5121+
# apart from the tail that survives. Free: it rides the FMA's addend.
5122+
#
5123+
# Available headroom is bounded by how large exp2 gets: the lazy path
5124+
# holds the running max until a tile exceeds it by RESCALE_THRESHOLD, so
5125+
# exp2 <= 2**THRESHOLD there; the eager path rebases every tile.
5126+
headroom = self._P_HEADROOM_LOG2
5127+
if const_expr(self.traits.DUALWAVE_SWP_LAZY_RESCALE):
5128+
headroom -= self.traits.DUALWAVE_SWP_RESCALE_THRESHOLD
5129+
bias = fx.Float32(headroom) if headroom > 0.0 else None
5130+
return _scale_sub_score_pair(v_s, row_max, self.c_logit_scale, self.c_zero_f, self.fm_fast, bias)
51065131

51075132
def exp2(self, v_s, start, length):
51085133
return _exp2_score_slice(v_s, start, length)
@@ -5180,7 +5205,7 @@ def lazy_rescale_o(self, v_o, m_row, l_row, m_tile_max, v_p):
51805205
def _run(v_o, m_row, l_row, m_tile_max, v_p):
51815206
m_diff = m_tile_max - m_row
51825207
m_diff_scaled = m_diff * self.c_logit_scale
5183-
below = fx.Float32(m_diff_scaled) <= self.c_eight_f
5208+
below = fx.Float32(m_diff_scaled) <= self.c_rescale_thr_f
51845209
ballot = rocdl.ballot(T.i64, as_mlir_value(below))
51855210
all_below = arith.cmpi(arith.CmpIPredicate.eq, as_mlir_value(ballot), _read_exec_i64())
51865211
all_below = llvm.intr_expect(all_below, arith.constant(1, type=ir.IntegerType.get_signless(1)))
@@ -5210,7 +5235,7 @@ def lazy_correct_o(self, v_o, m_row, l_row, m_tile_max):
52105235
def _run(v_o, m_row, l_row, m_tile_max):
52115236
m_diff = m_tile_max - m_row
52125237
m_diff_scaled = m_diff * self.c_logit_scale
5213-
below = fx.Float32(m_diff_scaled) <= self.c_eight_f
5238+
below = fx.Float32(m_diff_scaled) <= self.c_rescale_thr_f
52145239
ballot = rocdl.ballot(T.i64, as_mlir_value(below))
52155240
all_below = arith.cmpi(arith.CmpIPredicate.eq, as_mlir_value(ballot), _read_exec_i64())
52165241
all_below = llvm.intr_expect(all_below, arith.constant(1, type=ir.IntegerType.get_signless(1)))

0 commit comments

Comments
 (0)