2727
2828from absl import logging
2929from flax import nnx
30- from flax .traverse_util import flatten_dict
31- from flax .traverse_util import unflatten_dict
3230import jax
3331import jax .numpy as jnp
3432from 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 ()
0 commit comments