Skip to content

Commit 1d6af66

Browse files
committed
bring back wrap free ring for more tests
1 parent d8638ff commit 1d6af66

1 file changed

Lines changed: 130 additions & 0 deletions

File tree

src/maxdiffusion/kernels/splash_attention/ring_attention_kernel.py

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,109 @@ def make_ring_attention(
726726
# ---------------------------------------------------------------------------
727727

728728

729+
def _custom_bidirectional_ring_forward(
730+
q: jax.Array,
731+
k: jax.Array,
732+
v: jax.Array,
733+
*,
734+
block_sizes: "custom_splash._BlockSizes",
735+
bkv_compute_in: int,
736+
orig_q_seq_len: int,
737+
orig_kv_seq_len: int,
738+
use_base2_exp: bool,
739+
use_experimental_scheduler: bool,
740+
vmem_limit_bytes: int | None,
741+
mask_value: float,
742+
ring_axis: str,
743+
) -> jax.Array:
744+
"""Wrap-free (bidirectional) ring attention for a NON-wrapping ring axis.
745+
746+
On a torus dimension the +1-mod-R ppermute is nearest-neighbor, but on a
747+
cut/non-wrapping axis (e.g. the size-4 z line of a v7x-16 slice) the wrap edge
748+
(R-1 -> 0) spans the whole line diameter. Instead of one rotating stream with
749+
that long, congested wrap, stream K/V BOTH directions one hop at a time:
750+
- rightward stream: device i holds KV_{i-t} after t hops,
751+
- leftward stream: device i holds KV_{i+t} after t hops,
752+
with out-of-range shards (line ends) masked out of the online softmax. Every
753+
step is a single hop and uses both link directions; no edge ever spans the
754+
diameter.
755+
756+
Trade-off: each device computes ~2x attention blocks (the line-end ones are
757+
masked), traded for the removed multi-hop wrap. Net win when the ring is
758+
comms-bound (the case on a non-wrapping axis). Operates on the FULL real ring
759+
axis (no sub-group perm).
760+
"""
761+
axis_size = lax.axis_size(ring_axis)
762+
idx = lax.axis_index(ring_axis)
763+
exp_fn = jnp.exp2 if use_base2_exp else jnp.exp
764+
765+
def _attn(kc, vc):
766+
o, m, l = custom_splash._splash_attention_forward_ring( # pylint: disable=protected-access
767+
q,
768+
kc,
769+
vc,
770+
block_sizes,
771+
bkv_compute_in,
772+
q_seq_len=orig_q_seq_len,
773+
kv_seq_len=orig_kv_seq_len,
774+
use_base2_exp=use_base2_exp,
775+
use_experimental_scheduler=use_experimental_scheduler,
776+
vmem_limit_bytes=vmem_limit_bytes,
777+
)
778+
return o.astype(jnp.float32), m.astype(jnp.float32), l.astype(jnp.float32)
779+
780+
def _merge(m, l, o, mc, lc, oc, valid):
781+
# Nullify invalid (line-end) contributions. Force mc to mask_value (beta -> 0)
782+
# AND zero lc/oc so a non-finite zero-buffer result can't leak via 0*inf=nan.
783+
mc = jnp.where(valid, mc, mask_value)
784+
lc = jnp.where(valid, lc, 0.0)
785+
oc = jnp.where(valid, oc, 0.0)
786+
m_next = jnp.maximum(m, mc)
787+
alpha = exp_fn(m - m_next)
788+
beta = exp_fn(mc - m_next)
789+
return m_next, alpha * l + beta * lc, alpha[..., None] * o + beta[..., None] * oc
790+
791+
# t=0: own shard (always valid). _attn returns (o, m, l).
792+
o, m, l = _attn(k, v)
793+
794+
# Non-wrapping one-hop shifts (line ends send/receive nothing).
795+
shift_r = partial(lax.ppermute, axis_name=ring_axis, perm=[(i, i + 1) for i in range(axis_size - 1)])
796+
shift_l = partial(lax.ppermute, axis_name=ring_axis, perm=[(i, i - 1) for i in range(1, axis_size)])
797+
798+
# Prime buffers for t=1 (one hop each direction): device i -> KV_{i-1}, KV_{i+1}.
799+
kr, vr = shift_r(k), shift_r(v)
800+
kl, vl = shift_l(k), shift_l(v)
801+
802+
def body(carry, t):
803+
m, l, o, kr, vr, kl, vl = carry
804+
valid_r = (idx - t) >= 0
805+
valid_l = (idx + t) <= (axis_size - 1)
806+
# Feed real (own) K/V on invalid steps so _attn never runs on a degenerate
807+
# zero buffer (line ends receive 0 from the partial ppermute); masked below.
808+
kr_s, vr_s = jnp.where(valid_r, kr, k), jnp.where(valid_r, vr, v)
809+
kl_s, vl_s = jnp.where(valid_l, kl, k), jnp.where(valid_l, vl, v)
810+
# Compute against the current shards (KV_{i-t}, KV_{i+t}) ...
811+
o_r, m_r, l_r = _attn(kr_s, vr_s)
812+
m, l, o = _merge(m, l, o, m_r, l_r, o_r, valid_r)
813+
o_l, m_l, l_l = _attn(kl_s, vl_s)
814+
m, l, o = _merge(m, l, o, m_l, l_l, o_l, valid_l)
815+
# ... and prefetch the next hop (independent of the matmuls above -> overlaps).
816+
kr_n, vr_n = shift_r(kr), shift_r(vr)
817+
kl_n, vl_n = shift_l(kl), shift_l(vl)
818+
return (m, l, o, kr_n, vr_n, kl_n, vl_n), None
819+
820+
(_, l_final, o_final, *_), _ = lax.scan(
821+
body,
822+
(m, l, o, kr, vr, kl, vl),
823+
xs=jnp.arange(1, axis_size),
824+
length=axis_size - 1,
825+
unroll=True,
826+
)
827+
828+
l_inv = jnp.where(l_final == 0.0, 0.0, 1.0 / l_final)
829+
return (o_final * l_inv[..., None]).astype(q.dtype)
830+
831+
729832
def _custom_ring_attention_forward(
730833
q: jax.Array,
731834
k: jax.Array,
@@ -742,6 +845,7 @@ def _custom_ring_attention_forward(
742845
ring_axis: str,
743846
ring_size: int | None = None,
744847
perm: list[tuple[int, int]] | None = None,
848+
bidirectional: bool = False,
745849
) -> jax.Array:
746850
"""Forward-only ring attention using the custom dense splash kernel.
747851
@@ -776,6 +880,26 @@ def _custom_ring_attention_forward(
776880
Normalized attention output, shape `(num_q_heads, q_seq_len, head_dim_v)`.
777881
"""
778882
axis_size = lax.axis_size(ring_axis)
883+
if bidirectional:
884+
if perm is not None or (ring_size is not None and ring_size != axis_size):
885+
raise ValueError(
886+
"bidirectional (wrap-free) ring requires perm=None and ring_size==axis_size "
887+
"(it operates on the full real ring axis)."
888+
)
889+
return _custom_bidirectional_ring_forward(
890+
q,
891+
k,
892+
v,
893+
block_sizes=block_sizes,
894+
bkv_compute_in=bkv_compute_in,
895+
orig_q_seq_len=orig_q_seq_len,
896+
orig_kv_seq_len=orig_kv_seq_len,
897+
use_base2_exp=use_base2_exp,
898+
use_experimental_scheduler=use_experimental_scheduler,
899+
vmem_limit_bytes=vmem_limit_bytes,
900+
mask_value=mask_value,
901+
ring_axis=ring_axis,
902+
)
779903
if ring_size is None:
780904
ring_size = axis_size
781905
if perm is None:
@@ -847,6 +971,7 @@ def make_custom_ring_attention(
847971
ring_axis: str = "context",
848972
ring_size: int | None = None,
849973
perm: list[tuple[int, int]] | None = None,
974+
bidirectional: bool = False,
850975
):
851976
"""Builds a forward-only ring-attention callable around the custom kernel.
852977
@@ -858,6 +983,10 @@ def make_custom_ring_attention(
858983
`ring_size` / `perm` let a caller restrict the rotation to a ring sub-group of
859984
the axis (for the hybrid Ulysses+Ring / USP split); when omitted the rotation
860985
covers the whole `ring_axis`.
986+
987+
`bidirectional=True` selects the wrap-free schedule (streams K/V both directions
988+
one hop at a time) for a NON-wrapping ring axis, avoiding the diameter-length
989+
wrap hop. Requires `perm=None` and the full real ring axis (no sub-group).
861990
"""
862991

863992
def _ring(q, k, v):
@@ -876,6 +1005,7 @@ def _ring(q, k, v):
8761005
ring_axis=ring_axis,
8771006
ring_size=ring_size,
8781007
perm=perm,
1008+
bidirectional=bidirectional,
8791009
)
8801010

8811011
return _ring

0 commit comments

Comments
 (0)