Skip to content

Commit 7528f71

Browse files
committed
2D ring (USP) attention with custom splash kernel
1 parent eae38c4 commit 7528f71

5 files changed

Lines changed: 704 additions & 30 deletions

File tree

src/maxdiffusion/configs/base_wan_27b.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,13 @@ jit_initializers: True
8383
# Set true to load weights from pytorch
8484
from_pt: True
8585
split_head_dim: True
86-
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, ulysses, ulysses_custom, ulysses_ring
86+
attention: 'flash' # Supported attention: dot_product, flash, tokamax_flash, cudnn_flash_te, ring, tokamax_ring, tokamax_ring_custom, ulysses, ulysses_custom, ulysses_ring, ulysses_ring_custom, ulysses_ring_custom_bidir
87+
#
88+
# Best 2D-ring / USP (Ulysses x ring) configs for WAN2.2-T2V-A14B (720x1280, 81 frames)
89+
# Set attention=ulysses_ring_custom and ulysses_shards=U (ring degree R=CP/U):
90+
# CP4 (v7x-8): ulysses_shards=2 (R=2), BQ=9472
91+
# CP8 (v7x-8): ulysses_shards=4 (R=2), BQ=9472
92+
# CP16 (v7x-16): ulysses_shards=8 (R=2), BQ=9472
8793
use_base2_exp: True
8894
use_experimental_scheduler: True
8995
# For attention=ulysses_ring, hidden Ulysses shard count; ring shards are context / this.

src/maxdiffusion/kernels/custom_splash_attention.py

Lines changed: 125 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ def _flash_attention_kernel(
5959
l_scratch_ref,
6060
o_scratch_ref,
6161
o_ref,
62+
l_ring_ref=None,
63+
m_ring_ref=None,
6264
*,
6365
mask_value: float,
6466
grid_width: int,
@@ -70,6 +72,7 @@ def _flash_attention_kernel(
7072
q_seq_len: int,
7173
kv_seq_len: int,
7274
use_base2_exp: bool = True,
75+
fuse_reciprocal: bool = True,
7376
):
7477
float32 = jnp.float32
7578
head_dim_v_repeats, rem = divmod(head_dim_v, NUM_SUBLANES)
@@ -192,8 +195,18 @@ def last_body():
192195
@pl.when(j == grid_width - 1)
193196
def end():
194197
l = l_scratch_ref[...]
195-
l_inv = jnp.tile(1.0 / l, (head_dim_v_repeats, 1))
196-
o_ref[...] = (o_scratch_ref[...] * l_inv).astype(o_ref.dtype)
198+
if fuse_reciprocal:
199+
l_inv = jnp.tile(1.0 / l, (head_dim_v_repeats, 1))
200+
o_ref[...] = (o_scratch_ref[...] * l_inv).astype(o_ref.dtype)
201+
else:
202+
# Ring path: emit the un-normalized numerator plus the running softmax
203+
# stats (max logit `m` and linear denominator `l`) so the outer ring loop
204+
# can merge shard contributions and normalize only once at the very end.
205+
o_ref[...] = o_scratch_ref[...].astype(o_ref.dtype)
206+
if l_ring_ref is not None:
207+
l_ring_ref[...] = l.astype(l_ring_ref.dtype)
208+
if m_ring_ref is not None:
209+
m_ring_ref[...] = m_scratch_ref[...].astype(m_ring_ref.dtype)
197210

198211

199212
def _flash_attention_kernel_mhpt(
@@ -437,6 +450,116 @@ def v_index_map(h, i, j, *_):
437450
return all_out[-1]
438451

439452

453+
def _splash_attention_forward_ring(
454+
q: jax.Array,
455+
k: jax.Array,
456+
v: jax.Array,
457+
block_sizes: _BlockSizes,
458+
bkv_compute_in: int,
459+
q_seq_len: int | None = None,
460+
kv_seq_len: int | None = None,
461+
use_base2_exp: bool = True,
462+
use_experimental_scheduler: bool = False,
463+
vmem_limit_bytes: int | None = None,
464+
):
465+
"""Ring-specific forward path that returns pre-reciprocal fp32 accumulators.
466+
467+
Mirrors `_splash_attention_forward`, but instead of normalizing the output by
468+
the softmax denominator inside the kernel, it returns the un-normalized
469+
numerator (`out`) together with the per-row max logit (`m`) and linear softmax
470+
denominator (`l`). The outer ring loop merges these shard contributions and
471+
normalizes only once at the very end (see
472+
`ring_attention_kernel._custom_ring_attention_forward`).
473+
474+
Returns:
475+
A tuple `(out, m, l)` where
476+
- `out` has shape `(num_q_heads, q_seq_len, head_dim_v)` (fp32, un-normalized),
477+
- `m` and `l` have shape `(num_q_heads, q_seq_len)` (fp32).
478+
"""
479+
num_q_heads, padded_q_seq_len, head_dim_qk = q.shape
480+
head_dim_v = v.shape[-1]
481+
bq, bkv = block_sizes.block_q, block_sizes.block_kv
482+
bkv_compute = block_sizes.block_kv_compute
483+
num_kv_heads = k.shape[0]
484+
padded_kv_seq_len = k.shape[1]
485+
486+
actual_q_seq_len = q_seq_len if q_seq_len is not None else padded_q_seq_len
487+
actual_kv_seq_len = kv_seq_len if kv_seq_len is not None else padded_kv_seq_len
488+
q_heads_per_kv_head = num_q_heads // num_kv_heads
489+
490+
def q_index_map(h, i, j, *_):
491+
return (h, i, 0)
492+
493+
def out_index_map(h, i, j, *_):
494+
return h, 0, i
495+
496+
def k_index_map(h, i, j, *_):
497+
return (h // q_heads_per_kv_head, j, 0)
498+
499+
def v_index_map(h, i, j, *_):
500+
return (h // q_heads_per_kv_head, j, 0)
501+
502+
in_specs = [
503+
pl.BlockSpec((None, bq, head_dim_qk), q_index_map),
504+
pl.BlockSpec((None, bkv, head_dim_qk), k_index_map),
505+
pl.BlockSpec((None, bkv, head_dim_v), v_index_map),
506+
]
507+
out_shapes = [
508+
jax.ShapeDtypeStruct((NUM_SUBLANES, bq), jnp.float32),
509+
jax.ShapeDtypeStruct((NUM_SUBLANES, bq), jnp.float32),
510+
jax.ShapeDtypeStruct((head_dim_v, bq), jnp.float32),
511+
jax.ShapeDtypeStruct((num_q_heads, head_dim_v, actual_q_seq_len), jnp.float32),
512+
jax.ShapeDtypeStruct((num_q_heads, NUM_SUBLANES, actual_q_seq_len), jnp.float32),
513+
jax.ShapeDtypeStruct((num_q_heads, NUM_SUBLANES, actual_q_seq_len), jnp.float32),
514+
]
515+
out_specs = [
516+
pl.BlockSpec((NUM_SUBLANES, bq), lambda *_: (0, 0)),
517+
pl.BlockSpec((NUM_SUBLANES, bq), lambda *_: (0, 0)),
518+
pl.BlockSpec((head_dim_v, bq), lambda *_: (0, 0)),
519+
pl.BlockSpec((None, head_dim_v, bq), out_index_map),
520+
pl.BlockSpec((None, NUM_SUBLANES, bq), out_index_map),
521+
pl.BlockSpec((None, NUM_SUBLANES, bq), out_index_map),
522+
]
523+
grid_width = (actual_kv_seq_len + bkv - 1) // bkv
524+
grid_height = (actual_q_seq_len + bq - 1) // bq
525+
grid = (num_q_heads, grid_height, grid_width)
526+
527+
all_out = pl.pallas_call(
528+
functools.partial(
529+
_flash_attention_kernel,
530+
mask_value=DEFAULT_MASK_VALUE,
531+
grid_width=grid_width,
532+
bq=bq,
533+
bkv=bkv,
534+
bkv_compute=bkv_compute,
535+
bkv_compute_in=bkv_compute_in,
536+
head_dim_v=head_dim_v,
537+
q_seq_len=actual_q_seq_len,
538+
kv_seq_len=actual_kv_seq_len,
539+
use_base2_exp=use_base2_exp,
540+
fuse_reciprocal=False,
541+
),
542+
grid_spec=pltpu.PrefetchScalarGridSpec(
543+
num_scalar_prefetch=0,
544+
in_specs=in_specs,
545+
out_specs=out_specs,
546+
grid=grid,
547+
),
548+
compiler_params=pltpu.CompilerParams(
549+
dimension_semantics=("parallel", "arbitrary", "arbitrary"),
550+
flags={"XLA_TPU_FORCE_LP_LLO_SCHEDULER": use_experimental_scheduler},
551+
disable_bounds_checks=True,
552+
skip_device_barrier=True,
553+
vmem_limit_bytes=vmem_limit_bytes,
554+
),
555+
out_shape=out_shapes,
556+
)(q, k, v)
557+
out = jnp.swapaxes(all_out[3], 1, 2) # (h, head_dim_v, s) -> (h, s, head_dim_v)
558+
l = all_out[4][:, 0, :] # (h, s)
559+
m = all_out[5][:, 0, :] # (h, s)
560+
return out, m, l
561+
562+
440563
def _splash_attention_forward_mhpt(
441564
q: jax.Array,
442565
k: jax.Array,

0 commit comments

Comments
 (0)