Skip to content

Commit 049210a

Browse files
committed
feat(splash-attention): heads_per_tile + configurable vmem_limit_bytes for ulysses ring attention
1 parent f62927f commit 049210a

4 files changed

Lines changed: 311 additions & 25 deletions

File tree

src/maxdiffusion/kernels/splash_attention/ring_attention_kernel_test.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,84 @@ def ring_attn(ring_kernel, q, k, v, segment_ids):
160160
self._assert_allclose(dv, dv_ref, rtol=1e-2, atol=1e-2)
161161

162162

163+
class RingAttentionHeadsPerTileTest(test_utils.SplashAttentionTestCase):
164+
"""`heads_per_tile` (multi-head-per-tile) invariance for ring attention.
165+
166+
heads_per_tile is a pure tiling/scheduling choice for the forward kernel: for a
167+
fixed input, running with heads_per_tile=N must produce the same output as
168+
heads_per_tile=1. This guards against the block-index class of bug (a wrong
169+
head-tile mapping compiles and runs but silently returns garbage).
170+
"""
171+
172+
def setUp(self):
173+
if jax.default_backend() != "tpu":
174+
self.skipTest("Multi-head-per-tile ring attention runs on TPU.")
175+
super().setUp()
176+
177+
@parameterized.product(
178+
heads_per_tile=[2, 4],
179+
head_dim=[128],
180+
dtype=[jnp.bfloat16],
181+
)
182+
def test_heads_per_tile_matches_single_head(self, heads_per_tile, head_dim, dtype):
183+
ring_size = 2
184+
num_heads = 4 # MHA (num_q_heads == num_kv_heads); divisible by heads_per_tile.
185+
if len(jax.devices()) < ring_size:
186+
self.skipTest(f"This test requires {ring_size} devices, but has only {len(jax.devices())}.")
187+
188+
ring_axis = "ring"
189+
devices = np.asarray(jax.devices()[:ring_size]).reshape(1, ring_size)
190+
mesh = jax.sharding.Mesh(devices, ("heads", ring_axis))
191+
seq_len = 1024 * ring_size
192+
193+
k1, k2, k3 = random.split(random.key(0), 3)
194+
scale = head_dim**-0.5
195+
q = random.normal(k1, (num_heads, seq_len, head_dim), dtype=dtype) * scale
196+
k = random.normal(k2, (num_heads, seq_len, head_dim), dtype=dtype) * scale
197+
v = random.normal(k3, (num_heads, seq_len, head_dim), dtype=dtype) * scale
198+
199+
# The mhpt fast path supports full MHA + static FullMask + HEAD_DIM_MINOR only.
200+
mask = mask_lib.FullMask(_shape=(seq_len, seq_len))
201+
q_spec = P(None, ring_axis, None)
202+
kv_spec = q_spec
203+
204+
def run(hpt):
205+
config = splash.SplashConfig.get_default()
206+
config = dataclasses.replace(
207+
config,
208+
use_base2_exp=False,
209+
fuse_reciprocal=True,
210+
heads_per_tile=hpt,
211+
)
212+
ring_kernel = ring_attention_kernel.make_ring_attention(
213+
mask,
214+
is_mqa=False,
215+
ring_axis=ring_axis,
216+
config=config,
217+
save_residuals=False,
218+
q_seq_shards=ring_size,
219+
kv_seq_shards=ring_size,
220+
)
221+
kernel_spec = ring_kernel.manual_sharding_spec()
222+
223+
@partial(
224+
jax.shard_map,
225+
mesh=mesh,
226+
in_specs=(kernel_spec, q_spec, kv_spec, kv_spec, None),
227+
out_specs=q_spec,
228+
check_vma=False,
229+
)
230+
def ring_attn(ring_kernel, q, k, v, segment_ids):
231+
return ring_kernel(q, k, v, segment_ids)
232+
233+
return ring_attn(ring_kernel, q, k, v, None)
234+
235+
out_ref = run(1) # baseline: single head per tile (flash_attention_kernel)
236+
out_mhpt = run(heads_per_tile) # multi-head-per-tile (flash_attention_kernel_mhpt)
237+
238+
# Pure tiling => numerically equivalent to the single-head-per-tile baseline.
239+
self._assert_allclose(out_mhpt, out_ref, rtol=5e-3, atol=5e-3)
240+
241+
163242
if __name__ == "__main__":
164243
absltest.main()

0 commit comments

Comments
 (0)