Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
c7dbb64
test(rocke): tiled 2D rows with a fully masked first KV tile
AviralGoelAMD Oct 27, 2026
120a0da
fix(rocke): carry the unclamped running max in tiled 2D attention
AviralGoelAMD Oct 27, 2026
e38908f
fix(rocke): mirror the tiled 2D running-max carry in the C++ twins; r…
AviralGoelAMD Oct 27, 2026
5b2a01d
fix(rocke): drop dead running-max copies and stale comments in tiled …
AviralGoelAMD Oct 28, 2026
d584aed
test(rocke): scalar unified attention rows with fully masked leading …
AviralGoelAMD Oct 28, 2026
a4d3c22
fix(rocke): carry the raw running max in the scalar unified attention…
AviralGoelAMD Oct 28, 2026
5f20fa2
fix(rocke): re-bless scalar attention goldens
AviralGoelAMD Oct 28, 2026
9a8cb2b
fix(rocke): let NaN through the scalar unified attention reduce guards
AviralGoelAMD Oct 28, 2026
7ac7cf6
test(rocke): cover KV page size 64 in the tiled 2D masked-tile test
AviralGoelAMD Oct 28, 2026
1bf4570
test(rocke): pin NaN through the scalar reduce; widen masked-tile cov…
AviralGoelAMD Oct 28, 2026
95640e9
test(rocke): gate the scalar unified attention reduce in both engines
AviralGoelAMD Oct 28, 2026
0feeefa
fix(rocke): describe the scalar reduce guards in the C++ phase comments
AviralGoelAMD Oct 28, 2026
3cd8c2a
test(rocke): state the masked-tile underflow points consistently
AviralGoelAMD Oct 28, 2026
3699af8
fix(rocke): store 0 for keyless rows in scalar 2D attention under eve…
AviralGoelAMD Oct 28, 2026
f9217a6
fix(rocke): keep NaN rows NaN in the tiled 2D attention epilogue
AviralGoelAMD Oct 28, 2026
cdf5221
test(rocke): pin NaN propagation through each tiled 2D epilogue branch
AviralGoelAMD Oct 28, 2026
86fc67b
Merge origin/develop into users/avirgoel/rocke/tiled-2d-running-max-c…
AviralGoelAMD Oct 28, 2026
ff25dfa
fix(rocke): drop the dead empty-segment select from the scalar reduce…
AviralGoelAMD Oct 29, 2026
1d1beb9
fix(rocke): use a safe exponent reference in the default-mask scalar …
AviralGoelAMD Oct 29, 2026
cc6de09
Merge origin/develop into users/avirgoel/rocke/tiled-2d-running-max-c…
AviralGoelAMD Oct 29, 2026
35d9846
test(rocke): run the masked-tile attention numerics without torch; pi…
AviralGoelAMD Oct 29, 2026
a58c926
test(rocke): pass block_table_stride to the scalar 3D segment launch
AviralGoelAMD Oct 29, 2026
7fe29bb
Merge origin/develop into users/avirgoel/rocke/tiled-2d-running-max-c…
AviralGoelAMD Oct 29, 2026
39bbcca
test(rocke): gate the scalar 3D segment kernel in the byte-identity c…
AviralGoelAMD Oct 29, 2026
0f329d8
fix(rocke): use one exponent reference in the scalar reduce combine pass
AviralGoelAMD Oct 29, 2026
44974cb
fix(rocke): honour the sliding window in the default-mask scalar 3D body
AviralGoelAMD Oct 29, 2026
346649a
Merge branch 'develop' into users/avirgoel/rocke/tiled-2d-running-max…
cgmillette Oct 30, 2026
ffecdee
Merge origin/develop into users/avirgoel/rocke/tiled-2d-running-max-c…
AviralGoelAMD Oct 30, 2026
3e13e13
Merge origin/develop into users/avirgoel/rocke/tiled-2d-running-max-c…
AviralGoelAMD Oct 30, 2026
c58ca2f
Merge branch 'develop' into users/avirgoel/rocke/tiled-2d-running-max…
AviralGoelAMD Oct 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6447,21 +6447,18 @@ def _scalar_band_ok(
context_len,
query_pos,
kpos,
*,
window: bool = True,
) -> Optional[Value]:
"""Visibility of key ``kpos`` for the query: the right bound ``k <= d + right``
(``d = context_len + query_pos``) AND the left bound ``d - k < sliding_window``.
``None`` when neither bounds anything (the loop range already limits to kv_len).
``window=False`` skips the left bound.
"""
ok = None
if p.right_bound >= 0:
upper = b.add(context_len, query_pos)
if p.right_bound > 0:
upper = b.add(upper, b.const_i32(p.right_bound))
ok = b.cmp_le(kpos, upper)
if window and p.sliding_window > 0:
if p.sliding_window > 0:
dist = b.sub(b.add(context_len, query_pos), kpos)
sw_ok = b.cmp_lt(dist, b.const_i32(p.sliding_window))
ok = sw_ok if ok is None else b.land(ok, sw_ok)
Expand Down Expand Up @@ -6637,9 +6634,11 @@ def build_unified_attention_2d(
if band_ok is not None:
score = b.select(band_ok, score, neg_inf)
new_m_raw = b.fmax(m_val, score)
# If both running max and current score are -inf, the row is fully
# masked; force m to 0 so the resulting alpha/prob are 0 instead of NaN
# (matches Triton's `m_j = tl.where(m_j > -inf, m_j, 0.0)`).
# If both running max and current score are -inf, the row has seen no key
# yet; use 0 as the exponent reference so alpha/prob are 0 instead of NaN
# (matches Triton's `m_j = tl.where(m_j > -inf, m_j, 0.0)`). Carry the raw
# max: carrying the 0 pins m at 0 past masked leading keys (sliding
# window), and exp2(s - 0) then underflows P for very negative scores.
is_finite = b.fcmp("ogt", new_m_raw, neg_inf)
new_m = b.select(is_finite, new_m_raw, zero_f)
alpha = b.exp2(b.fsub(m_val, new_m))
Expand All @@ -6657,14 +6656,14 @@ def build_unified_attention_2d(
if p.use_fp8:
vv = b.fmul(vv, v_scale)
new_acc = b.fadd(b.fmul(acc_val, alpha), b.fmul(prob, vv))
b.scf_yield(new_m, new_l, new_acc)
b.scf_yield(new_m_raw, new_l, new_acc)

out_val = b.fmul(loop.results[2], b.rcp(loop.results[1]))
if not p.default_mask:
# Rows can see no key at all (e.g. top-left rows past ``kv_len`` with a window
# or a right bound): emit 0 instead of acc * rcp(0) == NaN, like the tiled
# kernels. The default mask never produces such a row, so its IR is unchanged.
out_val = b.select(b.fcmp("ogt", loop.results[1], zero_f), out_val, zero_f)
# Rows can see no key at all under any mask (bottom-right causal with Sq > Skv,
# or a window / right bound): store 0 instead of acc * rcp(0) == NaN. This is
# the tiled 2D epilogue's rule: only an exact-zero sum counts as keyless, so a
# NaN ``l`` still reaches O.
out_val = b.select(b.fcmp("oeq", loop.results[1], zero_f), zero_f, out_val)
out_cast = b.cast_f32_to(out_val, dtype)
out_off, _ = q_desc.offset(b, token=q_tok, head=q_head, dim=dim)
valid = b.land(active, b.cmp_lt(dim, b.const_i32(p.head_size)))
Expand Down Expand Up @@ -6693,9 +6692,7 @@ def kernel_name(self) -> str:
flags={
"tl": p.causal_top_left,
**_right_bound_flag(p),
# The default-mask 3D scalar kernel never applied the window (its name
# and IR are pinned); every other mask does.
"sw": p.sliding_window > 0 and not p.default_mask,
"sw": p.sliding_window > 0,
},
)

Expand Down Expand Up @@ -6801,28 +6798,28 @@ def build_unified_attention_3d(
scale,
rcp_ln2,
)
# The scalar 3D segment kernel has never applied the left bound; keep that
# for the default mask (its IR is pinned) and honour it for every other mask.
band_ok = _scalar_band_ok(
b, p, context_len, query_pos, kpos, window=not p.default_mask
)
# Every mask, the default one included: the right (causal) bound, and
# the left (sliding-window) bound whenever sliding_window > 0.
band_ok = _scalar_band_ok(b, p, context_len, query_pos, kpos)
if band_ok is not None:
score = b.select(band_ok, score, neg_inf)
new_m = b.fmax(m_val, score)
if not p.default_mask:
# Non-default masks can leave whole segments fully masked (top-left
# decode sees only key 0; a window or right bound skips others). Force m
# to 0 there so alpha/prob are 0 rather than NaN, which the reduce would
# propagate as NaN * 0. The default mask never hits this: IR unchanged.
new_m = b.select(b.fcmp("ogt", new_m, neg_inf), new_m, zero_f)
new_m_raw = b.fmax(m_val, score)
# A segment, or a segment's leading keys, can be fully masked for a row:
# a default-mask prefill row whose causal limit ends before a trailing
# segment, top-left decode (only key 0 visible), a window or right bound.
# Use 0 as the exponent reference there so alpha/prob are 0 rather than
# exp2(-inf - -inf) = NaN, but carry and store the raw max: it stays -inf
# until a key is seen, so the reduce ignores an empty segment and a
# masked prefix cannot pin m at 0.
new_m = b.select(b.fcmp("ogt", new_m_raw, neg_inf), new_m_raw, zero_f)
Comment thread
AviralGoelAMD marked this conversation as resolved.
Resolved
alpha = b.exp2(b.fsub(m_val, new_m))
prob = b.exp2(b.fsub(score, new_m))
new_l = b.fadd(b.fmul(l_val, alpha), prob)
vv = _emit_v_load(
b, p, dtype, value, block_tables, bt_stride, seq_idx, kv_head, kpos, dim
)
new_acc = b.fadd(b.fmul(acc_val, alpha), b.fmul(prob, vv))
b.scf_yield(new_m, new_l, new_acc)
b.scf_yield(new_m_raw, new_l, new_acc)

ml_desc, out_desc = _segm_descriptors(p, spec.num_segments)
base, _ = out_desc.offset(b, token=q_tok, head=q_head, seg=segm_idx, dim=dim)
Expand Down Expand Up @@ -6907,11 +6904,20 @@ def build_unified_attention_reduce(
[("mx", neg_inf)],
iv_name="seg",
)
# A segment that saw no key stores max = -inf with sum = 0: the segment kernel
# carries the raw max, so the max pass ignores that segment as is, and the
# combine pass weights it by exp2(-inf - ref) = 0 whenever ref is finite. ref
# is the overall max, or 0 when every segment is empty (overall = -inf), the
# same exponent-reference clamp the segment kernel uses: a row with no key at
# all then has den = 0 and gets O = 0 instead of NaN from exp2(-inf - -inf) or
# acc * rcp(0). Only an exact zero den counts as empty, so a NaN sum or
# denominator still yields NaN.
with max_loop as (seg, (mx,)):
idx, _ = ml_desc.offset(b, token=q_tok, head=q_head, seg=seg)
mv = b.global_load_f32(segm_max, idx)
b.scf_yield(b.fmax(mx, mv))
overall = max_loop.results[0]
ref = b.select(b.fcmp("ogt", overall, neg_inf), overall, zero)
red = b.scf_for_iter(
b.const_i32(0),
b.const_i32(spec.num_segments),
Expand All @@ -6923,7 +6929,7 @@ def build_unified_attention_reduce(
idx, _ = ml_desc.offset(b, token=q_tok, head=q_head, seg=seg)
mv = b.global_load_f32(segm_max, idx)
lv = b.global_load_f32(segm_expsum, idx)
factor = b.exp2(b.fsub(mv, overall))
factor = b.exp2(b.fsub(mv, ref))
den2 = b.fadd(den, b.fmul(lv, factor))
out_idx, _ = seg_out_desc.offset(
b,
Expand All @@ -6936,6 +6942,7 @@ def build_unified_attention_reduce(
acc2 = b.fadd(acc, b.fmul(ov, factor))
b.scf_yield(den2, acc2)
result = b.fmul(red.results[1], b.rcp(red.results[0]))
result = b.select(b.fcmp("oeq", red.results[0], zero), zero, result)
cast = b.cast_f32_to(result, dtype)
out_idx, _ = q_desc.offset(b, token=q_tok, head=q_head, dim=dim)
with b.scf_if(active):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4564,6 +4564,7 @@ def _kslot(group_idx: int) -> int:
# full ``l_new = alpha * l_old + l_sum`` updates are applied
# by that shared downstream block, not here.
m_new = [st_m_new for _ in range(SOFTMAX_STATE_SLOTS)]
m_carry = [st_m_raw for _ in range(SOFTMAX_STATE_SLOTS)]
l_local = [st_l_sum for _ in range(SOFTMAX_STATE_SLOTS)]
# In the transposed orientation we already have masked /
# scaled / softcapped scores in ``st_scores`` and softmax
Expand Down Expand Up @@ -4732,6 +4733,7 @@ def _kslot(group_idx: int) -> int:
masked[(n, reg)] = score

m_new = []
m_carry = []
s_local = {}
for reg in range(REGS_PER_LANE):
local_max = neg_inf
Expand All @@ -4743,6 +4745,7 @@ def _kslot(group_idx: int) -> int:
full_max_raw = b.fmax(m_vals[reg], tile_max)
ok = b.fcmp("ogt", full_max_raw, neg_inf)
m_new.append(b.select(ok, full_max_raw, zero_f))
m_carry.append(full_max_raw)

l_local = []
for reg in range(REGS_PER_LANE):
Expand Down Expand Up @@ -4821,6 +4824,7 @@ def _kslot(group_idx: int) -> int:
# repeated for every N-tile. Local lane-max across N-tiles, then 4-stage
# XOR butterfly across the 16 lanes in the row-group.
m_new = []
m_carry = []
s_local = {} # (reg, n) -> the lane's masked score (still owned per-lane)
for reg in range(REGS_PER_LANE):
local_max = neg_inf
Expand All @@ -4833,6 +4837,7 @@ def _kslot(group_idx: int) -> int:
full_max_raw = b.fmax(m_vals[reg], tile_max)
ok = b.fcmp("ogt", full_max_raw, neg_inf)
m_new.append(b.select(ok, full_max_raw, zero_f))
m_carry.append(full_max_raw)

# ---- compute P = exp2(S - m_new) and l_local = sum(P) per row ----
# P_lds[row, col] = exp2(S[row, col] - m_new[row]). Each warp publishes
Expand Down Expand Up @@ -5431,9 +5436,14 @@ def _strided_v_b_operand(k_iter: int) -> Value:
for atom in range(M_ATOMS_PER_WARP):
new_acc[n * M_ATOMS_PER_WARP + atom] = acc_per_atom[atom]

# Carry the unclamped running max: it stays -inf until the row has seen a
# key. ``m_new`` (-inf replaced by 0) is only the exponent reference for
# P and alpha. Carrying the clamped 0 instead pins m at 0 when a row's
# first tile is fully masked (sliding window), and exp2(s - 0) then
# underflows P for very negative scores so the row's O collapses.
yields = []
for r in range(SOFTMAX_STATE_SLOTS):
yields.append(m_new[r])
yields.append(m_carry[r])
yields.append(new_l_vals[r])
for n in range(ACC_N_TILES):
for atom in range(ACC_M_ATOMS):
Expand Down Expand Up @@ -5505,9 +5515,10 @@ def _strided_v_b_operand(k_iter: int) -> Value:
def _acc_final_get(n: int, atom: int) -> Value:
return acc_final[n * ACC_M_ATOMS + atom]

# Per-row reciprocal of L (computed once, reused across stripes).
# Per-row reciprocal of L (computed once, reused across stripes). Only an
# exact-zero L marks a keyless row (stored as 0); a NaN L stays NaN in O.
rcp_l = [b.rcp(l_final[r]) for r in range(SOFTMAX_STATE_SLOTS)]
l_nonzero = [b.fcmp("ogt", l_final[r], zero_f) for r in range(SOFTMAX_STATE_SLOTS)]
l_zero = [b.fcmp("oeq", l_final[r], zero_f) for r in range(SOFTMAX_STATE_SLOTS)]

if USE_MFMA_32X32:
if TRANSPOSED_QK_32X32:
Expand Down Expand Up @@ -5542,7 +5553,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
dim=b.const_i32(0),
)
inv_l_t = b.rcp(l_final[0])
l_nonzero_t = b.fcmp("ogt", l_final[0], zero_f)
l_zero_t = b.fcmp("oeq", l_final[0], zero_f)
for n in range(ACC_N_TILES):
acc32 = _acc_final_get(n, 0)
for reg in range(REGS_PER_LANE):
Expand All @@ -5552,7 +5563,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
v = b.vec_extract(acc32, reg)
normalized = b.fmul(v, inv_l_t)
final_h = b.cast_f32_to(
b.select(l_nonzero_t, normalized, zero_f), dtype
b.select(l_zero_t, zero_f, normalized), dtype
)
with b.scf_if(op_mask_t):
b.global_store(
Expand Down Expand Up @@ -5602,7 +5613,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
v = b.vec_extract(acc32, reg)
normalized = b.fmul(v, rcp_l[reg])
final_h = b.cast_f32_to(
b.select(l_nonzero[reg], normalized, zero_f), dtype
b.select(l_zero[reg], zero_f, normalized), dtype
)
b.smem_store_vN(Acc_lds, [row, col_in_stripe], final_h, 1)
b.sync()
Expand Down Expand Up @@ -5712,7 +5723,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
v = b.vec_extract(_acc_final_get(n, atom), in_atom)
normalized = b.fmul(v, rcp_l[reg])
final_h = b.cast_f32_to(
b.select(l_nonzero[reg], normalized, zero_f), dtype
b.select(l_zero[reg], zero_f, normalized), dtype
)
b.smem_store_vN(Acc_lds, [row, col_in_stripe], final_h, 1)
b.sync()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3656,6 +3656,7 @@ def _mod3_add(x, c):
# full ``l_new = alpha * l_old + l_sum`` updates are applied
# by that shared downstream block, not here.
m_new = [st_m_new for _ in range(SOFTMAX_STATE_SLOTS)]
m_carry = [st_m_raw for _ in range(SOFTMAX_STATE_SLOTS)]
l_local = [st_l_sum for _ in range(SOFTMAX_STATE_SLOTS)]
# In the transposed orientation we already have masked /
# scaled / softcapped scores in ``st_scores`` and softmax
Expand Down Expand Up @@ -3849,6 +3850,7 @@ def _mod3_add(x, c):
masked[(n, reg)] = score

m_new = []
m_carry = []
s_local = {}
for reg in range(REGS_PER_LANE):
local_max = neg_inf
Expand All @@ -3860,6 +3862,7 @@ def _mod3_add(x, c):
full_max_raw = b.fmax(m_vals[reg], tile_max)
ok = b.fcmp("ogt", full_max_raw, neg_inf)
m_new.append(b.select(ok, full_max_raw, zero_f))
m_carry.append(full_max_raw)

l_local = []
for reg in range(REGS_PER_LANE):
Expand Down Expand Up @@ -3952,6 +3955,7 @@ def _mod3_add(x, c):
# repeated for every N-tile. Local lane-max across N-tiles, then 4-stage
# XOR butterfly across the 16 lanes in the row-group.
m_new = []
m_carry = []
s_local = {} # (reg, n) -> the lane's masked score (still owned per-lane)
for reg in range(REGS_PER_LANE):
local_max = neg_inf
Expand All @@ -3964,6 +3968,7 @@ def _mod3_add(x, c):
full_max_raw = b.fmax(m_vals[reg], tile_max)
ok = b.fcmp("ogt", full_max_raw, neg_inf)
m_new.append(b.select(ok, full_max_raw, zero_f))
m_carry.append(full_max_raw)

# ---- compute P = exp2(S - m_new) and l_local = sum(P) per row ----
# P_lds[row, col] = exp2(S[row, col] - m_new[row]). Each warp publishes
Expand Down Expand Up @@ -4423,9 +4428,14 @@ def _apply_transposed_pv_regs(acc32: Value, n: int, p_regs) -> Value:
for atom in range(M_ATOMS_PER_WARP):
new_acc[n * M_ATOMS_PER_WARP + atom] = acc_per_atom[atom]

# Carry the unclamped running max: it stays -inf until the row has seen a
# key. ``m_new`` (-inf replaced by 0) is only the exponent reference for
# P and alpha. Carrying the clamped 0 instead pins m at 0 when a row's
# first tile is fully masked (sliding window), and exp2(s - 0) then
# underflows P for very negative scores so the row's O collapses.
yields = []
for r in range(SOFTMAX_STATE_SLOTS):
yields.append(m_new[r])
yields.append(m_carry[r])
yields.append(new_l_vals[r])
for n in range(ACC_N_TILES):
for atom in range(ACC_M_ATOMS):
Expand Down Expand Up @@ -4522,9 +4532,10 @@ def _apply_transposed_pv_regs(acc32: Value, n: int, p_regs) -> Value:
def _acc_final_get(n: int, atom: int) -> Value:
return acc_final[n * ACC_M_ATOMS + atom]

# Per-row reciprocal of L (computed once, reused across stripes).
# Per-row reciprocal of L (computed once, reused across stripes). Only an
# exact-zero L marks a keyless row (stored as 0); a NaN L stays NaN in O.
rcp_l = [b.rcp(l_final[r]) for r in range(SOFTMAX_STATE_SLOTS)]
l_nonzero = [b.fcmp("ogt", l_final[r], zero_f) for r in range(SOFTMAX_STATE_SLOTS)]
l_zero = [b.fcmp("oeq", l_final[r], zero_f) for r in range(SOFTMAX_STATE_SLOTS)]

if USE_MFMA_32X32:
if TRANSPOSED_QK_32X32:
Expand Down Expand Up @@ -4559,7 +4570,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
dim=b.const_i32(0),
)
inv_l_t = b.rcp(l_final[0])
l_nonzero_t = b.fcmp("ogt", l_final[0], zero_f)
l_zero_t = b.fcmp("oeq", l_final[0], zero_f)
for n in range(ACC_N_TILES):
acc32 = _acc_final_get(n, 0)
for reg in range(REGS_PER_LANE):
Expand All @@ -4569,7 +4580,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
v = b.vec_extract(acc32, reg)
normalized = b.fmul(v, inv_l_t)
final_h = b.cast_f32_to(
b.select(l_nonzero_t, normalized, zero_f), dtype
b.select(l_zero_t, zero_f, normalized), dtype
)
with b.scf_if(op_mask_t):
b.global_store(
Expand Down Expand Up @@ -4619,7 +4630,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
v = b.vec_extract(acc32, reg)
normalized = b.fmul(v, rcp_l[reg])
final_h = b.cast_f32_to(
b.select(l_nonzero[reg], normalized, zero_f), dtype
b.select(l_zero[reg], zero_f, normalized), dtype
)
b.smem_store_vN(Acc_lds, [row, col_in_stripe], final_h, 1)
b.sync()
Expand Down Expand Up @@ -4729,7 +4740,7 @@ def _acc_final_get(n: int, atom: int) -> Value:
v = b.vec_extract(_acc_final_get(n, atom), in_atom)
normalized = b.fmul(v, rcp_l[reg])
final_h = b.cast_f32_to(
b.select(l_nonzero[reg], normalized, zero_f), dtype
b.select(l_zero[reg], zero_f, normalized), dtype
)
b.smem_store_vN(Acc_lds, [row, col_in_stripe], final_h, 1)
b.sync()
Expand Down
Loading
Loading