Skip to content

Commit 9c9b707

Browse files
committed
Add fixed-m softmax for pure-Ulysses WAN attention (ulysses_custom_fixed_m) with CFG batch-fold fix
1 parent 0984457 commit 9c9b707

4 files changed

Lines changed: 414 additions & 61 deletions

File tree

src/maxdiffusion/kernels/custom_splash_attention.py

Lines changed: 120 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,21 @@ def __init__(self, block_q: int, block_kv: int, block_kv_compute: int | None = N
5151
self.block_kv_compute = block_kv_compute if block_kv_compute is not None else block_kv
5252

5353

54+
# Fixed-m softmax-bound constants. Instead of tracking the online-softmax
55+
# running max per KV block, eligible heads subtract a precomputed per-query
56+
# upper bound on the logits (Cauchy-Schwarz: max_j q_i.k_j <= ||q_i|| *
57+
# max_j||k_j||). _FIXED_M_RECENTER (C) shifts the exp2 exponents up so the
58+
# largest surviving term stays above the f32 subnormal-flush floor 2^-126:
59+
# with k-smoothing the per-row max is >= 0, so the max term has exponent
60+
# >= -ceil(bound) + C, which stays > -126 while ceil(bound) <=
61+
# _FIXED_M_SAFE_BOUND (= C + 126 - 1 of margin). Heads whose worst-case bound
62+
# exceeds the gate fall back to online softmax (the "sink" heads).
63+
_FIXED_M_RECENTER = 88.0
64+
_FIXED_M_SAFE_BOUND = 213.0
65+
66+
5467
def _flash_attention_kernel(
68+
mk_ref,
5569
q_ref,
5670
k_ref,
5771
v_ref,
@@ -73,34 +87,45 @@ def _flash_attention_kernel(
7387
kv_seq_len: int,
7488
use_base2_exp: bool = True,
7589
fuse_reciprocal: bool = True,
90+
use_fixed_m: bool = False,
7691
):
7792
float32 = jnp.float32
7893
head_dim_v_repeats, rem = divmod(head_dim_v, NUM_SUBLANES)
7994
if rem != 0:
8095
raise NotImplementedError(f"{head_dim_v=} should be a multiple of {NUM_SUBLANES}")
8196

82-
_, _, j = pl.program_id(0), pl.program_id(1), pl.program_id(2)
97+
h, _, j = pl.program_id(0), pl.program_id(1), pl.program_id(2)
8398
exp = jnp.exp2 if use_base2_exp else jnp.exp
99+
sv_dims = (((0,), (0,)), ((), ()))
100+
101+
# Per-head dispatch: heads inside the no-flush window run fixed-m, the rest
102+
# keep online softmax. Branch once per head (body level), never per step.
103+
is_fixed = (mk_ref[1, h] > 0.5) if use_fixed_m else False
84104

85105
@pl.when(j == 0)
86106
def init():
87107
o_scratch_ref[...] = jnp.zeros_like(o_scratch_ref)
88-
m_scratch_ref[...] = jnp.full_like(m_scratch_ref, mask_value)
89108
l_scratch_ref[...] = jnp.zeros_like(l_scratch_ref)
109+
if use_fixed_m:
90110

91-
def compute_body(kv_compute_index, _):
92-
m_prev, l_prev = m_scratch_ref[...], l_scratch_ref[...]
93-
q = q_ref[...]
94-
o_prev = o_scratch_ref[:]
111+
@pl.when(is_fixed)
112+
def _init_fixed():
113+
# Per-query Cauchy-Schwarz bound m_i = ceil(||q_i|| * max_j||k_j||) - C.
114+
qf = q_ref[...].astype(float32)
115+
qn = jnp.sqrt((qf * qf).sum(axis=1))[None, :] # (1, bq) per-query norm
116+
bound = qn * mk_ref[0, h]
117+
m_fixed = jnp.ceil(bound) - _FIXED_M_RECENTER
118+
m_scratch_ref[...] = jnp.broadcast_to(m_fixed, m_scratch_ref.shape)
95119

96-
base_offset = kv_compute_index * bkv_compute
97-
slice_k = pl.ds(base_offset, bkv_compute)
98-
k_chunk = k_ref[slice_k, :]
120+
@pl.when(jnp.logical_not(is_fixed))
121+
def _init_online():
122+
m_scratch_ref[...] = jnp.full_like(m_scratch_ref, mask_value)
99123

100-
qk = lax.dot_general(k_chunk, q, NT_DIM_NUMBERS, preferred_element_type=float32)
101-
v_chunk = v_ref[slice_k, :]
124+
else:
125+
m_scratch_ref[...] = jnp.full_like(m_scratch_ref, mask_value)
102126

103-
# --- V1 VPU REGISTER TILING ---
127+
def _online_inner(qk, v_chunk, m_prev, l_prev, o_prev):
128+
# Standard online-softmax tiling over the VPU register block.
104129
step = bkv_compute_in
105130
for i in range(0, qk.shape[0], step):
106131
qk_slice = qk[i : i + step]
@@ -113,84 +138,99 @@ def compute_body(kv_compute_index, _):
113138
alpha = exp(m_prev - m_next)
114139
l_next = l_curr + alpha * l_prev
115140

116-
sv_dims = (((0,), (0,)), ((), ()))
117141
o_curr = lax.dot_general(
118142
v_chunk[i : i + step],
119143
s_curr.astype(q_ref.dtype),
120144
sv_dims,
121145
preferred_element_type=float32,
122146
)
123-
124-
alpha_o = alpha[0:1, ...]
125-
o_prev = alpha_o * o_prev + o_curr
126-
147+
o_prev = alpha[0:1, ...] * o_prev + o_curr
127148
m_prev, l_prev = m_next, l_next
128-
# --- END V1 TILING ---
129-
130-
m_scratch_ref[...], l_scratch_ref[...] = m_prev, l_prev
131-
o_scratch_ref[:] = o_prev
149+
return m_prev, l_prev, o_prev
132150

133-
def last_compute_body(kv_compute_index):
134-
m_prev, l_prev = m_scratch_ref[...], l_scratch_ref[...]
135-
q = q_ref[...]
136-
o_prev = o_scratch_ref[:]
137-
138-
slice_k_len = kv_seq_len % bkv_compute
139-
slice_k = pl.ds(kv_compute_index * bkv_compute, slice_k_len)
140-
k_chunk = k_ref[slice_k, :]
141-
142-
qk = lax.dot_general(k_chunk, q, NT_DIM_NUMBERS, preferred_element_type=float32)
143-
v_chunk = v_ref[slice_k, :]
144-
145-
# --- V1 VPU REGISTER TILING ---
151+
def _fixed_inner(qk, v_chunk, m_fix, l_prev, o_prev):
152+
# Fixed-m fast path: m is constant, so no reduce-max and no alpha rescale.
146153
step = bkv_compute_in
147154
for i in range(0, qk.shape[0], step):
148155
qk_slice = qk[i : i + step]
149156

150-
m_curr = qk_slice.max(axis=0)[None, :]
151-
m_next = jnp.maximum(m_prev, m_curr)
152-
s_curr = exp(qk_slice - m_next[0:1])
157+
s_curr = exp(qk_slice - m_fix[0:1])
153158
l_curr = s_curr.sum(axis=0, keepdims=True)
154159

155-
alpha = exp(m_prev - m_next)
156-
l_next = l_curr + alpha * l_prev
157-
158-
sv_dims = (((0,), (0,)), ((), ()))
159160
o_curr = lax.dot_general(
160161
v_chunk[i : i + step],
161162
s_curr.astype(q_ref.dtype),
162163
sv_dims,
163164
preferred_element_type=float32,
164165
)
166+
o_prev = o_prev + o_curr
167+
l_prev = l_prev + l_curr
168+
return l_prev, o_prev
165169

166-
alpha_o = alpha[0:1, ...]
167-
o_prev = alpha_o * o_prev + o_curr
170+
def compute_body_online(kv_compute_index, _):
171+
q = q_ref[...]
172+
base_offset = kv_compute_index * bkv_compute
173+
slice_k = pl.ds(base_offset, bkv_compute)
174+
qk = lax.dot_general(k_ref[slice_k, :], q, NT_DIM_NUMBERS, preferred_element_type=float32)
175+
v_chunk = v_ref[slice_k, :]
176+
m_prev, l_prev, o_prev = _online_inner(qk, v_chunk, m_scratch_ref[...], l_scratch_ref[...], o_scratch_ref[:])
177+
m_scratch_ref[...], l_scratch_ref[...] = m_prev, l_prev
178+
o_scratch_ref[:] = o_prev
168179

169-
m_prev, l_prev = m_next, l_next
170-
# --- END V1 TILING ---
180+
def compute_body_fixed(kv_compute_index, _):
181+
q = q_ref[...]
182+
base_offset = kv_compute_index * bkv_compute
183+
slice_k = pl.ds(base_offset, bkv_compute)
184+
qk = lax.dot_general(k_ref[slice_k, :], q, NT_DIM_NUMBERS, preferred_element_type=float32)
185+
v_chunk = v_ref[slice_k, :]
186+
l_prev, o_prev = _fixed_inner(qk, v_chunk, m_scratch_ref[...], l_scratch_ref[...], o_scratch_ref[:])
187+
l_scratch_ref[...] = l_prev
188+
o_scratch_ref[:] = o_prev
171189

190+
def last_compute_body_online(kv_compute_index):
191+
q = q_ref[...]
192+
slice_k_len = kv_seq_len % bkv_compute
193+
slice_k = pl.ds(kv_compute_index * bkv_compute, slice_k_len)
194+
qk = lax.dot_general(k_ref[slice_k, :], q, NT_DIM_NUMBERS, preferred_element_type=float32)
195+
v_chunk = v_ref[slice_k, :]
196+
m_prev, l_prev, o_prev = _online_inner(qk, v_chunk, m_scratch_ref[...], l_scratch_ref[...], o_scratch_ref[:])
172197
m_scratch_ref[...], l_scratch_ref[...] = m_prev, l_prev
173198
o_scratch_ref[:] = o_prev
174199

175200
assert bkv % bkv_compute == 0
176201

177-
@pl.when(j != grid_width - 1)
178-
def body():
179-
lax.fori_loop(0, (bkv // bkv_compute), compute_body, None, unroll=True)
202+
if use_fixed_m:
203+
204+
@pl.when((j != grid_width - 1) & is_fixed)
205+
def _body_fixed():
206+
lax.fori_loop(0, (bkv // bkv_compute), compute_body_fixed, None, unroll=True)
207+
208+
@pl.when((j != grid_width - 1) & jnp.logical_not(is_fixed))
209+
def _body_online():
210+
lax.fori_loop(0, (bkv // bkv_compute), compute_body_online, None, unroll=True)
211+
212+
else:
213+
214+
@pl.when(j != grid_width - 1)
215+
def body():
216+
lax.fori_loop(0, (bkv // bkv_compute), compute_body_online, None, unroll=True)
180217

218+
# The final KV block always runs online for every head. Fixed-m heads arrive
219+
# with m_scratch = ceil(bound) - C and o/l at that reference; the online
220+
# alpha = exp2(m_prev - m_next) rescale renormalizes them transparently.
181221
@pl.when(j == grid_width - 1)
182222
def last_body():
183223
if kv_seq_len % bkv == 0:
184224
iter_num = bkv // bkv_compute
185-
lax.fori_loop(0, iter_num, compute_body, None, unroll=True)
225+
lax.fori_loop(0, iter_num, compute_body_online, None, unroll=True)
186226
else:
187227
remain_kv_seq_len = kv_seq_len % bkv
188228
iter_num = (remain_kv_seq_len + bkv_compute - 1) // bkv_compute
189229
if remain_kv_seq_len % bkv_compute == 0:
190-
lax.fori_loop(0, iter_num, compute_body, None, unroll=True)
230+
lax.fori_loop(0, iter_num, compute_body_online, None, unroll=True)
191231
else:
192-
lax.fori_loop(0, iter_num - 1, compute_body, None, unroll=True)
193-
last_compute_body(iter_num - 1)
232+
lax.fori_loop(0, iter_num - 1, compute_body_online, None, unroll=True)
233+
last_compute_body_online(iter_num - 1)
194234

195235
@pl.when(j == grid_width - 1)
196236
def end():
@@ -373,9 +413,16 @@ def _splash_attention_forward(
373413
use_base2_exp: bool = True,
374414
use_experimental_scheduler: bool = False,
375415
vmem_limit_bytes: int | None = None,
416+
use_fixed_m: bool = False,
417+
mk: jax.Array | None = None,
376418
):
377419
num_q_heads, padded_q_seq_len, head_dim_qk = q.shape
378420
head_dim_v = v.shape[-1]
421+
# Scalar-prefetch operand carrying per-head fixed-m data:
422+
# mk[0, h] = max_j||k_j|| (Cauchy-Schwarz factor), mk[1, h] = eligibility.
423+
# A dummy is supplied for online callers; the kernel ignores it.
424+
if mk is None:
425+
mk = jnp.zeros((2, num_q_heads), jnp.float32)
379426
bq, bkv = block_sizes.block_q, block_sizes.block_kv
380427
bkv_compute = block_sizes.block_kv_compute
381428
num_kv_heads = k.shape[0]
@@ -431,9 +478,10 @@ def v_index_map(h, i, j, *_):
431478
q_seq_len=actual_q_seq_len,
432479
kv_seq_len=actual_kv_seq_len,
433480
use_base2_exp=use_base2_exp,
481+
use_fixed_m=use_fixed_m,
434482
),
435483
grid_spec=pltpu.PrefetchScalarGridSpec(
436-
num_scalar_prefetch=0,
484+
num_scalar_prefetch=1,
437485
in_specs=in_specs,
438486
out_specs=out_specs,
439487
grid=grid,
@@ -446,7 +494,7 @@ def v_index_map(h, i, j, *_):
446494
vmem_limit_bytes=vmem_limit_bytes,
447495
),
448496
out_shape=out_shapes,
449-
)(q, k, v)
497+
)(mk, q, k, v)
450498
return all_out[-1]
451499

452500

@@ -461,6 +509,8 @@ def _splash_attention_forward_ring(
461509
use_base2_exp: bool = True,
462510
use_experimental_scheduler: bool = False,
463511
vmem_limit_bytes: int | None = None,
512+
use_fixed_m: bool = False,
513+
mk: jax.Array | None = None,
464514
):
465515
"""Ring-specific forward path that returns pre-reciprocal fp32 accumulators.
466516
@@ -524,6 +574,13 @@ def v_index_map(h, i, j, *_):
524574
grid_height = (actual_q_seq_len + bq - 1) // bq
525575
grid = (num_q_heads, grid_height, grid_width)
526576

577+
# Scalar-prefetch operand carrying per-head fixed-m data (same convention as
578+
# `_splash_attention_forward`): mk[0, h] = max_j||k_j|| over ALL ring shards
579+
# (the caller all-reduces this over the ring axis), mk[1, h] = eligibility.
580+
# A dummy is supplied for online callers; the kernel ignores it.
581+
if mk is None:
582+
mk = jnp.zeros((2, num_q_heads), jnp.float32)
583+
527584
all_out = pl.pallas_call(
528585
functools.partial(
529586
_flash_attention_kernel,
@@ -538,9 +595,10 @@ def v_index_map(h, i, j, *_):
538595
kv_seq_len=actual_kv_seq_len,
539596
use_base2_exp=use_base2_exp,
540597
fuse_reciprocal=False,
598+
use_fixed_m=use_fixed_m,
541599
),
542600
grid_spec=pltpu.PrefetchScalarGridSpec(
543-
num_scalar_prefetch=0,
601+
num_scalar_prefetch=1,
544602
in_specs=in_specs,
545603
out_specs=out_specs,
546604
grid=grid,
@@ -553,7 +611,7 @@ def v_index_map(h, i, j, *_):
553611
vmem_limit_bytes=vmem_limit_bytes,
554612
),
555613
out_shape=out_shapes,
556-
)(q, k, v)
614+
)(mk, q, k, v)
557615
out = jnp.swapaxes(all_out[3], 1, 2) # (h, head_dim_v, s) -> (h, s, head_dim_v)
558616
l = all_out[4][:, 0, :] # (h, s)
559617
m = all_out[5][:, 0, :] # (h, s)
@@ -660,9 +718,12 @@ def make_splash_mha(
660718
use_base2_exp: bool = True,
661719
use_experimental_scheduler: bool = False,
662720
vmem_limit_bytes: int | None = None,
721+
use_fixed_m: bool = False,
663722
):
664-
def _splash_attention(q, k, v):
723+
def _splash_attention(q, k, v, mk=None):
665724
if heads_per_tile > 1:
725+
if use_fixed_m:
726+
raise NotImplementedError("fixed-m is not supported with heads_per_tile > 1")
666727
return _splash_attention_forward_mhpt(
667728
q,
668729
k,
@@ -687,6 +748,8 @@ def _splash_attention(q, k, v):
687748
use_base2_exp=use_base2_exp,
688749
use_experimental_scheduler=use_experimental_scheduler,
689750
vmem_limit_bytes=vmem_limit_bytes,
751+
use_fixed_m=use_fixed_m,
752+
mk=mk,
690753
)
691754

692755
return _splash_attention

src/maxdiffusion/kernels/splash_attention/ring_attention_kernel.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -846,6 +846,8 @@ def _custom_ring_attention_forward(
846846
ring_size: int | None = None,
847847
perm: list[tuple[int, int]] | None = None,
848848
bidirectional: bool = False,
849+
use_fixed_m: bool = False,
850+
mk: jax.Array | None = None,
849851
) -> jax.Array:
850852
"""Forward-only ring attention using the custom dense splash kernel.
851853
@@ -886,6 +888,8 @@ def _custom_ring_attention_forward(
886888
"bidirectional (wrap-free) ring requires perm=None and ring_size==axis_size "
887889
"(it operates on the full real ring axis)."
888890
)
891+
if use_fixed_m:
892+
raise NotImplementedError("fixed-m is not yet supported on the bidirectional ring path.")
889893
return _custom_bidirectional_ring_forward(
890894
q,
891895
k,
@@ -932,6 +936,8 @@ def body(carry, i):
932936
use_base2_exp=use_base2_exp,
933937
use_experimental_scheduler=use_experimental_scheduler,
934938
vmem_limit_bytes=vmem_limit_bytes,
939+
use_fixed_m=use_fixed_m,
940+
mk=mk,
935941
)
936942
m_curr = m_curr.astype(jnp.float32)
937943
l_curr = l_curr.astype(jnp.float32)
@@ -972,6 +978,8 @@ def make_custom_ring_attention(
972978
ring_size: int | None = None,
973979
perm: list[tuple[int, int]] | None = None,
974980
bidirectional: bool = False,
981+
use_fixed_m: bool = False,
982+
mk: jax.Array | None = None,
975983
):
976984
"""Builds a forward-only ring-attention callable around the custom kernel.
977985
@@ -1006,6 +1014,8 @@ def _ring(q, k, v):
10061014
ring_size=ring_size,
10071015
perm=perm,
10081016
bidirectional=bidirectional,
1017+
use_fixed_m=use_fixed_m,
1018+
mk=mk,
10091019
)
10101020

10111021
return _ring

0 commit comments

Comments
 (0)