Skip to content

Commit 346a621

Browse files
committed
feat(training_engine): support Raiden-FFI weight synchronization and adopt single synchronizer
- Support Raiden-FFI device-to-host synchronization in MaxTextTrainingEngine. - Enforce Raiden-FFI under Pathways and fail fast if _raiden_ffi is unavailable. - Eliminate multi-chunk staging and adopt a single persistent RaidenSynchronizer. - Clean up lifecycle in release_weight_sync and close. - Set scan_layers=False in maxtext_engine_e2e_test setup_config.
1 parent 2586d60 commit 346a621

2 files changed

Lines changed: 69 additions & 99 deletions

File tree

src/maxtext/training_engine/maxtext_engine.py

Lines changed: 67 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,6 @@
2727

2828
from absl import logging
2929
from flax import nnx
30-
from flax.traverse_util import flatten_dict
31-
from flax.traverse_util import unflatten_dict
3230
import jax
3331
import jax.numpy as jnp
3432
from jax.typing import ArrayLike # pylint: disable=g-importing-member
@@ -330,7 +328,6 @@ def __init__(
330328
self._compile_requested = False
331329
self._compiled_signature: Any = None
332330
self._signature_compare_warned: bool = False
333-
self._raiden_syncs: Any = None
334331
if not training_config.model_name:
335332
raise ValueError("training_config.model_name must be specified")
336333
model_or_model_mesh_pair = model_creation_utils.from_pretrained(
@@ -366,7 +363,7 @@ def __init__(
366363
)
367364
self._metrics_recorder = metrics_module.MetricsRecorder()
368365
self._throttler = inflight_throttler.InflightThrottler(config=self._config)
369-
self._raiden_syncs: Any = None
366+
self._raiden_sync: Any = None
370367

371368
@property
372369
def model(self) -> Any:
@@ -1101,38 +1098,6 @@ def _get_trainable_params_state(self) -> Any:
11011098
return nnx.state(model, nnx.Param)
11021099
return self.model
11031100

1104-
def _split_into_chunks(self, nested_state: Any, num_chunks: int) -> list[Any]:
1105-
"""Splits a nested param dict into `num_chunks` nested dicts of near-equal leaf count.
1106-
1107-
Rebinding raiden's native WeightSynchronizer with a full new array list
1108-
only releases its hold on the PREVIOUS bind's buffers atomically with
1109-
acquiring the new ones (BindWeights: "releases the holds on the
1110-
previously bound buffers and acquires holds on the new ones") -- so the
1111-
complete new state must already be host-staged before the old one can be
1112-
dropped, and every rebind after the first needs ~2x one copy's worth of
1113-
host memory, not ~1x. Splitting the state across `num_chunks` independent
1114-
RaidenSynchronizer instances -- each bound, D2H'd, and released one at a
1115-
time -- bounds that overlap to ~(num_chunks+1)/num_chunks of one copy
1116-
instead of ~2x. Confirmed against a live OOM at num_chunks=1: main hit
1117-
the 420G container limit on a Qwen3-30B-A3B (~245GB bf16) trainer's
1118-
SECOND weight-sync cycle (the first has no stale buffer to overlap with).
1119-
The orchestrator and rollout's RaidenSamplerAdapter already support a
1120-
source contributing multiple WorkUnitMetadata entries (pooled by exact
1121-
variable name in manifest preflight, not by unit count), so no changes
1122-
are needed outside this trainer-side split.
1123-
"""
1124-
if hasattr(nested_state, "to_pure_dict"):
1125-
pure_state = nested_state.to_pure_dict()
1126-
elif hasattr(nested_state, "to_dict"):
1127-
pure_state = nested_state.to_dict()
1128-
else:
1129-
pure_state = nested_state
1130-
flat = flatten_dict(pure_state)
1131-
chunk_flats = [{} for _ in range(num_chunks)]
1132-
for i, key in enumerate(flat):
1133-
chunk_flats[i % num_chunks][key] = flat[key]
1134-
return [unflatten_dict(cf) for cf in chunk_flats]
1135-
11361101
def prepare_weight_sync(
11371102
self,
11381103
staging_transport: str = "raiden",
@@ -1149,10 +1114,18 @@ def prepare_weight_sync(
11491114
"""
11501115
if staging_transport == "raiden":
11511116
try:
1152-
from tunix.experimental.worker import raiden_synchronizer # pylint: disable=g-import-not-at-top,import-outside-toplevel
1153-
except ImportError:
1154-
logging.warning("tunix.experimental.worker.raiden_synchronizer not found; returning empty metadata.")
1155-
return []
1117+
from tunix.experimental.weight_sync import raiden_synchronizer # pylint: disable=g-import-not-at-top,import-outside-toplevel
1118+
except ImportError as exc:
1119+
# Fatal, not a warning: Raiden staging was explicitly requested and cannot be
1120+
# provided. Returning empty metadata instead defers the failure to the caller --
1121+
# `WeightSyncCoordinator` eventually raises "metadata collection returned an empty
1122+
# side", which reports a count from another process and never mentions the missing
1123+
# module, leaving the real cause in this worker's log on another host.
1124+
raise RuntimeError(
1125+
"staging_transport='raiden' requires tunix.experimental.weight_sync."
1126+
"raiden_synchronizer, which the installed tunix does not provide. Install a"
1127+
" tunix build that ships it, or select a different staging_transport."
1128+
) from exc
11561129

11571130
# 1. Drain all in-flight TPU computations to ensure weights are fully updated
11581131
self._throttler.wait_for_all()
@@ -1183,80 +1156,75 @@ def prepare_weight_sync(
11831156
scan_axis=self._config.param_scan_axis,
11841157
)
11851158

1186-
# 3. Bind parameters to the Raiden transport, one chunk at a time (see
1187-
# _split_into_chunks) -- construct the per-chunk synchronizers once,
1188-
# matching the persistent-instance-per-cycle pattern the rebind
1189-
# optimization (fewer stale holds) depends on.
1190-
num_chunks = max(1, int(os.environ.get("RAIDEN_WEIGHT_SYNC_CHUNKS", "1")))
1191-
if self._raiden_syncs is None:
1192-
# Under Pathways (JAX_PLATFORMS=proxy + JAX_BACKEND_TARGET set, same
1193-
# detection tunix's K8sJaxContext.initialize() uses), trainer params
1194-
# are proxy-backed and Raiden can't bind them in place -- host_stage
1195-
# pulls them to client host memory first. Direct-TPU trainers skip
1196-
# that extra copy since their params already live on TPU.
1197-
is_pathways = bool("proxy" in os.environ.get("JAX_PLATFORMS", "") and os.environ.get("JAX_BACKEND_TARGET"))
1198-
# worker_index must be unique per chunk (it seeds WorkUnitId's
1199-
# job_replica_id) -- otherwise every chunk's work unit collides under
1200-
# the same id in the handler's registry and only one survives
1201-
# registration.
1202-
self._raiden_syncs = [
1203-
raiden_synchronizer.RaidenSynchronizer(
1204-
job_name="trainer",
1205-
worker_index=jax.process_index() if num_chunks == 1 else (jax.process_index() * num_chunks + i + 1),
1206-
auto_h2d=False,
1207-
host_stage=is_pathways,
1208-
parallelism=4,
1209-
)
1210-
for i in range(num_chunks)
1211-
]
1212-
1213-
chunks = self._split_into_chunks(params_state, num_chunks) if num_chunks > 1 else [params_state]
1214-
del params_state
1215-
1216-
verify_weights = os.environ.get("VERIFY_WEIGHTS", "").lower() == "true"
1217-
all_metadata = []
1218-
total_variables = 0
1219-
for chunk_idx, (sync, chunk_state) in enumerate(zip(self._raiden_syncs, chunks)):
1220-
sync.bind(chunk_state)
1159+
# 3. Bind parameters to the Raiden transport. Construct the synchronizer
1160+
# once, matching the persistent-instance-per-cycle pattern the rebind
1161+
# optimization depends on.
1162+
#
1163+
# Under Pathways (JAX_PLATFORMS=proxy + JAX_BACKEND_TARGET set, same
1164+
# detection tunix's K8sJaxContext.initialize() uses), trainer params
1165+
# are proxy-backed. Raiden must use FFI (weight_synchronizer_ffi) to bind
1166+
# directly to device arrays on Pathways TPU workers without host CPU staging,
1167+
# avoiding client host OOM and multi-minute proxy transfer timeouts.
1168+
is_pathways = bool("proxy" in os.environ.get("JAX_PLATFORMS", "") and os.environ.get("JAX_BACKEND_TARGET"))
1169+
if is_pathways:
1170+
if getattr(raiden_synchronizer, "_raiden_ffi", None) is None:
1171+
raise RuntimeError(
1172+
"Under Pathways (JAX_PLATFORMS=proxy), Raiden weight synchronization "
1173+
"requires weight_synchronizer_ffi (from tpu_raiden_jax) to avoid client host OOM "
1174+
"and proxy staging timeouts. However, _raiden_ffi is not available in "
1175+
"tunix.experimental.weight_sync.raiden_synchronizer. Please ensure a "
1176+
"compatible tpu_raiden_jax wheel with FFI support is installed."
1177+
)
1178+
use_ffi = True
1179+
else:
1180+
use_ffi = os.environ.get("RAIDEN_USE_FFI", "").lower() in ("true", "1")
1181+
1182+
if self._raiden_sync is None:
1183+
self._raiden_sync = raiden_synchronizer.RaidenSynchronizer(
1184+
job_name="trainer",
1185+
worker_index=jax.process_index(),
1186+
auto_h2d=False,
1187+
use_ffi=use_ffi,
1188+
parallelism=4,
1189+
)
12211190

1222-
# 4. Initiate Device-to-Host transfer to stage this chunk for network
1223-
# transfer before moving on to the next chunk.
1224-
if sync.active:
1225-
sync.d2h()
1191+
self._raiden_sync.bind(params_state)
1192+
del params_state
12261193

1227-
if verify_weights:
1228-
logging.info("Source weights checksums (chunk %d): %s", chunk_idx, sync.checksums())
1194+
# 4. Initiate Device-to-Host transfer to stage weights for network transfer.
1195+
if self._raiden_sync.active:
1196+
self._raiden_sync.d2h()
12291197

1230-
metadata = sync.work_unit_metadata()
1231-
total_variables += len(metadata.variables)
1232-
all_metadata.append(metadata)
1198+
verify_weights = os.environ.get("VERIFY_WEIGHTS", "").lower() == "true"
1199+
if verify_weights:
1200+
logging.info("Source weights checksums: %s", self._raiden_sync.checksums())
12331201

1202+
metadata = self._raiden_sync.work_unit_metadata()
12341203
logging.info(
1235-
"Trainer prepared weight sync for step %d: registered %d variables across %d chunk(s) on mesh %s",
1204+
"Trainer prepared weight sync for step %d: registered %d variables on mesh %s",
12361205
self.train_step,
1237-
total_variables,
1238-
num_chunks,
1239-
all_metadata[0].mesh_axes if all_metadata else None,
1206+
len(metadata.variables),
1207+
metadata.mesh_axes,
12401208
)
1241-
return all_metadata
1209+
return [metadata]
12421210

1243-
return []
1211+
# Unknown transport: raise rather than return empty metadata. A typo would otherwise
1212+
# surface only as the coordinator's "empty side" error, with nothing logged anywhere
1213+
# naming the transport that was actually asked for.
1214+
raise ValueError(f"unknown staging_transport {staging_transport!r}; expected 'raiden'.")
12441215

12451216
def release_weight_sync(self, **kwargs: Any) -> Any:
12461217
"""Releases staged weight buffers after transfer completion."""
1247-
if self._raiden_syncs:
1248-
for sync in self._raiden_syncs:
1249-
logging.vlog(1, "Trainer Raiden metrics: %s", sync.metrics())
1250-
sync.release_host_arrays()
1218+
if self._raiden_sync:
1219+
logging.vlog(1, "Trainer Raiden metrics: %s", self._raiden_sync.metrics())
12511220
return True
12521221

12531222
def close(self) -> None:
12541223
"""Closes the trainer and its associated resources."""
1255-
if self._raiden_syncs:
1256-
for sync in self._raiden_syncs:
1257-
if hasattr(sync, "close"):
1258-
sync.close()
1259-
self._raiden_syncs = None
1224+
if self._raiden_sync:
1225+
if hasattr(self._raiden_sync, "close"):
1226+
self._raiden_sync.close()
1227+
self._raiden_sync = None
12601228
self._throttler.cleanup()
12611229
self._metrics_recorder.cleanup()
12621230
self._checkpoint_manager.close()

tests/post_training/unit/maxtext_engine_e2e_test.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ def setup_config(self, enable_checkpointing: bool = False, **kwargs):
121121
"tensorboard_dir": self.create_tempdir().full_path,
122122
"skip_jax_distributed_system": True,
123123
"enable_checkpointing": enable_checkpointing,
124+
# Disable scan_layers to prevent prepare_weight_sync from trying to unscan layers on DummyNNXModel
125+
"scan_layers": False,
124126
}
125127
if enable_checkpointing:
126128
overrides.update(

0 commit comments

Comments
 (0)