|
16 | 16 | import unittest |
17 | 17 | from absl.testing import parameterized |
18 | 18 | import jax |
| 19 | +from jax.experimental import multihost_utils |
19 | 20 | import jax.numpy as jnp |
20 | 21 | import numpy as np |
21 | 22 |
|
@@ -76,6 +77,43 @@ def _assert_allclose(self, x, y, **kwargs): |
76 | 77 | self.assertTupleEqual(x.shape, y.shape) |
77 | 78 | np.testing.assert_allclose(x, y, **kwargs) |
78 | 79 |
|
| 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 | + |
79 | 117 |
|
80 | 118 | def create_segment_ids(seq_len: int, num_breaks: int = 2) -> base.SegmentIds: |
81 | 119 | break_indices = np.random.choice(range(1, seq_len), num_breaks, replace=False) |
|
0 commit comments