Skip to content

Commit 0d5d0f8

Browse files
committed
test(splash-ring): make mhpt heads_per_tile test pass under multi-host
1 parent b47433e commit 0d5d0f8

2 files changed

Lines changed: 42 additions & 1 deletion

File tree

src/maxdiffusion/kernels/splash_attention/ring_attention_kernel_test.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,10 @@ def ring_attn(ring_kernel, q, k, v, segment_ids):
236236
out_mhpt = run(heads_per_tile) # multi-head-per-tile (flash_attention_kernel_mhpt)
237237

238238
# 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)
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)
240243

241244

242245
if __name__ == "__main__":

src/maxdiffusion/kernels/splash_attention/splash_attention_test_utils.py

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

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

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+
79117

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

0 commit comments

Comments
 (0)