Skip to content

Commit 758cb1f

Browse files
committed
test(splash-ring): use full mesh and all-gather in mhpt invariance test
1 parent 0cd23dd commit 758cb1f

2 files changed

Lines changed: 15 additions & 47 deletions

File tree

src/maxdiffusion/kernels/splash_attention/ring_attention_kernel_test.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from absl.testing import parameterized
2222
import jax
2323
from jax import random
24+
from jax.experimental import multihost_utils
2425
import jax.numpy as jnp
2526
import numpy as np
2627
from . import base
@@ -180,13 +181,17 @@ def setUp(self):
180181
dtype=[jnp.bfloat16],
181182
)
182183
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())}.")
184+
# Use ALL devices so the sharded outputs are addressable on every process
185+
# (a subset mesh like jax.devices()[:2] lives only on process 0, making the
186+
# result non-addressable on the other hosts). jax.devices() is itself a
187+
# global barrier, so every host must launch this test together regardless.
188+
ring_size = jax.device_count()
189+
num_heads = 8 # MHA (num_q_heads == num_kv_heads); divisible by heads_per_tile.
190+
if ring_size < 2:
191+
self.skipTest(f"This test needs at least 2 devices, but has {ring_size}.")
187192

188193
ring_axis = "ring"
189-
devices = np.asarray(jax.devices()[:ring_size]).reshape(1, ring_size)
194+
devices = np.asarray(jax.devices()).reshape(1, ring_size)
190195
mesh = jax.sharding.Mesh(devices, ("heads", ring_axis))
191196
seq_len = 1024 * ring_size
192197

@@ -236,10 +241,11 @@ def ring_attn(ring_kernel, q, k, v, segment_ids):
236241
out_mhpt = run(heads_per_tile) # multi-head-per-tile (flash_attention_kernel_mhpt)
237242

238243
# Pure tiling => numerically equivalent to the single-head-per-tile baseline.
239-
# The ring mesh is jax.devices()[:ring_size] (all on process 0), so the
240-
# outputs are only addressable there; use the multi-controller-safe compare
241-
# so this passes on every host, not just the owner.
242-
self.assert_allclose_mcjax(out_mhpt, out_ref, rtol=5e-3, atol=5e-3)
244+
# Outputs are sharded across all hosts; all-gather to a fully-replicated host
245+
# array on every process, then compare with the standard helper.
246+
out_mhpt = multihost_utils.process_allgather(out_mhpt, tiled=True)
247+
out_ref = multihost_utils.process_allgather(out_ref, tiled=True)
248+
self._assert_allclose(out_mhpt, out_ref, rtol=5e-3, atol=5e-3)
243249

244250

245251
if __name__ == "__main__":

src/maxdiffusion/kernels/splash_attention/splash_attention_test_utils.py

Lines changed: 0 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
import unittest
1717
from absl.testing import parameterized
1818
import jax
19-
from jax.experimental import multihost_utils
2019
import jax.numpy as jnp
2120
import numpy as np
2221

@@ -77,43 +76,6 @@ def _assert_allclose(self, x, y, **kwargs):
7776
self.assertTupleEqual(x.shape, y.shape)
7877
np.testing.assert_allclose(x, y, **kwargs)
7978

80-
def assert_allclose_mcjax(self, x, y, *, rtol, atol):
81-
"""`allclose` that is safe under multi-controller (multi-host) JAX.
82-
83-
Some tests build their device mesh from a subset of the global devices --
84-
e.g. `jax.devices()[:ring_size]`, which all live on process 0 -- so the
85-
result `jax.Array`s are only addressable on that one process. Pulling them
86-
to host with `np.testing.assert_allclose` (as `_assert_allclose` does)
87-
raises `RuntimeError: ... spans non-addressable devices` on every *other*
88-
process, failing the test on all but one host.
89-
90-
Instead, evaluate the comparison on-device (works on all processes, no host
91-
fetch), read the scalar verdict only on the owning process (process 0, which
92-
holds `jax.devices()[:ring_size]`), and broadcast it to every process with a
93-
single collective. Every process runs the same two `broadcast_one_to_all`
94-
calls in the same order, so there is no collective-participation mismatch /
95-
deadlock. The result is identical on all hosts and genuinely reflects the
96-
owner's computation.
97-
98-
Only use this for tests whose mesh is a subset of one process's devices; for
99-
fully-sharded (every-process-owns-a-shard) arrays use `_assert_allclose`.
100-
"""
101-
if x.dtype == np.dtype(jnp.bfloat16):
102-
x = x.astype(jnp.float32)
103-
if y.dtype == np.dtype(jnp.bfloat16):
104-
y = y.astype(jnp.float32)
105-
self.assertTupleEqual(x.shape, y.shape)
106-
ok = jnp.all(jnp.abs(x - y) <= atol + rtol * jnp.abs(y))
107-
max_err = jnp.max(jnp.abs(x - y))
108-
is_owner = jax.process_index() == 0
109-
local_ok = np.asarray(ok) if is_owner else np.array(True)
110-
local_err = np.asarray(max_err) if is_owner else np.array(0.0, np.float32)
111-
global_err = float(multihost_utils.broadcast_one_to_all(local_err))
112-
self.assertTrue(
113-
bool(multihost_utils.broadcast_one_to_all(local_ok)),
114-
f"arrays differ: max abs err {global_err:.3e} exceeds rtol={rtol} atol={atol}",
115-
)
116-
11779

11880
def create_segment_ids(seq_len: int, num_breaks: int = 2) -> base.SegmentIds:
11981
break_indices = np.random.choice(range(1, seq_len), num_breaks, replace=False)

0 commit comments

Comments
 (0)