Skip to content

feat: add opt-in single_host_tpu_init for node-local diagnostics on multi-host slices - #144

Open
simrankaurb wants to merge 1 commit into
AI-Hypercomputer:mainfrom
simrankaurb:fix/gemm-replicated-inputs-no-d2d
Open

simrankaurb wants to merge 1 commit into
AI-Hypercomputer:mainfrom
simrankaurb:fix/gemm-replicated-inputs-no-d2d

Conversation

@simrankaurb

@simrankaurb simrankaurb commented Oct 26, 2026 •

Copy link
Copy Markdown

Summary

Adds an opt‑in single_host_tpu_init (YAML key single_host_tpu_init: true / CLI --single_host_tpu_init, default off) that initializes the current host standalone inside a multi‑host slice, so single‑host benchmarks (gemm, hbm, …) can be used for node‑local diagnostics — e.g. by Cluster Health Scanner — without the other hosts of the slice participating. No existing behaviour changes when the flag is off (the matmul.py diff is pure additions). This brings the chs branch's single‑host init path (#142) to main.

What the flag does

core/single_host.py::configure_single_host_tpu(), called by the runner after op_flags.yaml is applied and before the first JAX device call:

  • env: TPU_HOST_BOUNDS=1,1,1, TPU_WORKER_ID=0, TPU_WORKER_HOSTNAMES=localhost, TPU_SKIP_MDS_QUERY=true
  • appends to LIBTPU_INIT_ARGS: --deepsea_hal_test_skip_slicebuilder=true --deepsea_hal_test_allow_multichip_skip_slicebuilder=true --xla_tpu_use_enhanced_launch_barrier=false (and flips an existing enhanced_launch_barrier=true)

Because the slice's ICI routing is left untouched, device‑to‑device transfers hang in this mode. GeneralizedGemmBenchmark.generate_inputs samples the operands on the default device and then calls jax.device_put(x, replicated_sharding), which broadcasts over ICI — so gemm hung for any matrix size, with and without xprof (Slow PjRt TPU operation detected on all local devices). When the flag is on, the operands (and optional scaling factors / accumulator) are instead generated independently on each local mesh device from a CPU‑pinned key and assembled with jax.make_array_from_single_device_arrays; the only transfers are host→device. Same sharding (P(None, None)) and same contents (same key everywhere). A key left on TPU:0 was verified to still hang — the per‑device device_put of the key is itself a D2D copy.

Plumbing mirrors xprof_timing: BenchmarkKey.SINGLE_HOST_TPU_INIT + bool validation in config.py, --single_host_tpu_init on benchmark run, pop/forward in run-config, runner.run_benchmarks(single_host_tpu_init=…). README updated.

Verification

tpu7x‑standard‑4t host of a 2x2x4 slice, jax 0.12.0 / libtpu 0.0.50, no manual environment set up in the pod:

case result
YAML single_host_tpu_init: true, gemm 16384³ bf16 ×20, xprof_device_timing_aggregation: local_slowest_device ✅ 78 s · xprof p50 8.27 ms · 2127 TFLOP/s per chip (1064 / device) · 20/20 xprof runs
--single_host_tpu_init gemm 1024³ ✅ 20 s
--single_host_tpu_init hbm copy, 1 G elems, device 0 ✅ 2790 GB/s (single‑device path unaffected)
Control: same pod, no flag ❌ init fails: Invalid --deepsea_slice_builder_worker_addresses … Expected 4 worker addresses, got 1

CPU (4‑device host mesh): flagged and default input paths produce identical replicated arrays and bit‑identical GEMM output for the same seed (incl. scaling‑factor + accumulator inputs); single_host_tpu_init: "yes" is rejected with a clear ValueError; the CLI flag parses and is not forwarded into GemmParams.

Note: upstream tests/ import accelerator_microbenchmarks.tests, which is not resolvable in the OSS checkout, so they were not run here.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a helper method _replicated_normal to generate replicated arrays of normal samples locally on each device, preventing device-to-device transfers that can hang when ICI routing is not configured. The generate_inputs method is updated to utilize this helper for input and scaling factor generation. The review feedback suggests several improvements: simplifying the creation of replicated_sharding with an empty PartitionSpec(), using the idiomatic self.mesh.local_devices property instead of manual filtering, and catching both RuntimeError and IndexError when accessing the CPU device to prevent potential crashes.

Comment on lines +215 to +217
replicated_sharding = jax.sharding.NamedSharding(
self.mesh, jax.sharding.PartitionSpec(*([None] * len(shape)))
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

We can simplify the creation of replicated_sharding by using an empty PartitionSpec(). In JAX, an empty PartitionSpec() represents a fully replicated sharding across all dimensions of the mesh, which is cleaner and matches the usage elsewhere in this file (e.g., line 524).

Suggested change
replicated_sharding = jax.sharding.NamedSharding(
self.mesh, jax.sharding.PartitionSpec(*([None] * len(shape)))
)
replicated_sharding = jax.sharding.NamedSharding(
self.mesh, jax.sharding.PartitionSpec()
)

Comment on lines +225 to +227
local_devices = [
d for d in self.mesh.devices.flat if d.process_index == jax.process_index()
]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Instead of manually filtering self.mesh.devices.flat by process_index, we can use the built-in self.mesh.local_devices property. This is more idiomatic, cleaner, and less error-prone.

    local_devices = self.mesh.local_devices

Comment on lines +221 to +224
try:
key = jax.device_put(key, jax.devices("cpu")[0])
except RuntimeError:
pass # No CPU backend (JAX_PLATFORMS=tpu); fall back to device key.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

If the CPU backend is registered but has no devices, jax.devices("cpu") would return an empty list, and accessing [0] would raise an IndexError. It is safer to catch both RuntimeError and IndexError to ensure robust fallback behavior.

Suggested change
try:
key = jax.device_put(key, jax.devices("cpu")[0])
except RuntimeError:
pass # No CPU backend (JAX_PLATFORMS=tpu); fall back to device key.
try:
key = jax.device_put(key, jax.devices("cpu")[0])
except (RuntimeError, IndexError):
pass # No CPU backend (JAX_PLATFORMS=tpu); fall back to device key.

Comment on lines +257 to +260
try:
key = jax.device_put(key, jax.devices("cpu")[0])
except RuntimeError:
pass # No CPU backend (JAX_PLATFORMS=tpu); fall back to device key.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similarly, we should catch both RuntimeError and IndexError here to handle cases where the CPU backend is registered but has no devices.

Suggested change
try:
key = jax.device_put(key, jax.devices("cpu")[0])
except RuntimeError:
pass # No CPU backend (JAX_PLATFORMS=tpu); fall back to device key.
try:
key = jax.device_put(key, jax.devices("cpu")[0])
except (RuntimeError, IndexError):
pass # No CPU backend (JAX_PLATFORMS=tpu); fall back to device key.

@simrankaurb
simrankaurb force-pushed the fix/gemm-replicated-inputs-no-d2d branch from d6baf11 to b78b61c Compare October 26, 2026 12:05
@simrankaurb simrankaurb changed the title fix(gemm): generate replicated inputs per device instead of broadcasting over ICI feat: add opt-in single_host_tpu_init for node-local diagnostics on multi-host slices Oct 26, 2026
…-host slices

Adds a benchmark-level `single_host_tpu_init: true` config key and a
`--single_host_tpu_init` common execution flag (default off, accepted by
both `benchmark run` and `benchmark run-config`) that initialize the
current host standalone inside a multi-host slice, so single-host
benchmarks such as `gemm` and `hbm` can be used for node-local diagnostics
(e.g. Cluster Health Scanner) without the other hosts of the slice
participating.

When enabled, `core/single_host.py` (applied after op_flags and before the
first JAX device call) forces a 1-host topology (`TPU_HOST_BOUNDS`,
`TPU_WORKER_ID`, `TPU_WORKER_HOSTNAMES`, `TPU_SKIP_MDS_QUERY`), skips
SliceBuilder/tpunetd (`--deepsea_hal_test_skip_slicebuilder=true`,
`--deepsea_hal_test_allow_multichip_skip_slicebuilder=true`) and disables
the cross-host launch barrier. LIBTPU_INIT_ARGS is merged by flag name so
pre-existing values are overridden rather than duplicated, and every
overridden env var / flag is logged with its previous value.

Because the slice's ICI routing is left untouched, device-to-device
transfers hang in this mode. `GeneralizedGemmBenchmark.generate_inputs`
previously sampled the operands on the default device and broadcast them
with `jax.device_put(x, replicated_sharding)`, which hung for any matrix
size. It now generates each operand under `jax.jit(...,
out_shardings=replicated_sharding)` (the pattern already used by `hbm.py`),
so every device produces the same replicated values and no broadcast is
needed. This is a single code path for both modes: with the flag off the
only behavioural change is that GEMM operand generation no longer
broadcasts over ICI (values are bit-identical). Only `gemm`,
`gemm_throttling` (same input path) and `hbm` are supported in this mode;
the runner prints a warning when the flag is used with any other benchmark.

Verification:
- TPU, earlier revision of this change (replicated operands built via
  `jax.make_array_from_single_device_arrays` instead of
  `jax.jit(out_shardings=...)`), on a tpu7x-standard-4t host of a 2x2x4
  slice (jax 0.12.0, libtpu 0.0.50), no manual environment set up:
  `single_host_tpu_init: true`, gemm 16384^3 bf16 x20, xprof
  local_slowest_device: 2127 TFLOP/s per chip, 20/20 xprof runs, 78 s;
  `--single_host_tpu_init` gemm 1024^3: 20 s; hbm copy 1G elems:
  2790 GB/s; control without the flag on the same node fails at init
  ("Expected 4 worker addresses, got 1").
- CPU: the current `jax.jit(out_shardings=...)` path yields arrays
  bit-identical to the previous default path with fully replicated,
  identical shards on a 4-device CPU mesh; it has not yet been re-run on
  TPU. New unit tests cover config/CLI plumbing, LIBTPU_INIT_ARGS merging,
  runner stage ordering and the unsupported-benchmark warning.
@simrankaurb
simrankaurb force-pushed the fix/gemm-replicated-inputs-no-d2d branch from b78b61c to a80aaba Compare October 28, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant