Skip to content

Commit b0520da

Browse files
committed
NNX: fix DiLoCo train loop + checkpoint under pure_nnx
Under enable_diloco the state becomes a DiLoCoTrainState, but the pure_nnx path still merged it against the plain-model graphdef (nnx.merge leaf mismatch + segfault), and several downstream sites assumed a plain TrainStateNNX. Guard the merge and surface the graphdef as model; fix get_first_step, jit_model, params_shardings, setup_params, and the rng args in train_loop; match the diloco sharding's params to_pure_dict; and handle the DiLoCoTrainState in maybe_save_checkpoint by saving the synchronized global model. Train + checkpoint save/restore validated end-to-end on CPU.
1 parent d358e8b commit b0520da

3 files changed

Lines changed: 39 additions & 8 deletions

File tree

src/maxtext/common/checkpointing.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,7 +1040,8 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10401040
actual_step = int(step)
10411041
else:
10421042
if config.pure_nnx:
1043-
actual_step = int(state.optimizer.step) - 1
1043+
# Under DiLoCo the step lives on the DiLoCoTrainState; otherwise on the optimizer.
1044+
actual_step = int(state.step if config.enable_diloco else state.optimizer.step) - 1
10441045
else:
10451046
# Linen TrainState has .step attribute
10461047
actual_step = int(state.step) - 1
@@ -1051,7 +1052,13 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
10511052

10521053
if config.pure_nnx:
10531054
# Save in the Linen on-disk layout so pure_nnx and Linen checkpoints are interchangeable.
1054-
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
1055+
if config.enable_diloco:
1056+
# DiLoCoTrainState: persist the synchronized global model (outer params).
1057+
# The per-replica inner optimizer / outer-momentum state is not checkpointed.
1058+
step_value = state.step.get_value() if hasattr(state.step, "get_value") else state.step
1059+
state = train_state_nnx.to_linen_checkpoint_dict({"model": state.params, "optimizer": {"step": step_value}})
1060+
else:
1061+
state = train_state_nnx.to_linen_checkpoint_dict(state.to_pure_dict())
10551062

10561063
# Determine if a checkpoint save should be forced, overriding the usual `config.checkpoint_period` logic.
10571064
# This occurs if this function was called:

src/maxtext/trainers/pre_train/train.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@
7777
def get_first_step(model, state):
7878
if isinstance(model, nn.Module):
7979
return int(state.step)
80+
if hasattr(state, "inner_state"): # DiLoCoTrainState (NNX DiLoCo): step is the optimizer step var
81+
return int(state.step.get_value())
8082
return int(state.optimizer.step.get_value())
8183

8284

@@ -598,10 +600,19 @@ def train_loop(config, recorder, state=None):
598600

599601
if isinstance(model, nn.Module):
600602
jit_model = model
603+
elif config.enable_diloco:
604+
# state is the DiLoCoTrainState; `model` is already the TrainStateNNX graphdef the inner step needs.
605+
jit_model = model
601606
else:
602607
jit_model, state = nnx.split(state)
603608

604-
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
609+
if config.pure_nnx and config.enable_diloco:
610+
# state_mesh_shardings is a DiLoCoTrainState; its .params field already holds the
611+
# plain param shardings the inner train step needs (the Zero-1 opt update, which
612+
# reads .model/.optimizer, doesn't apply to the wrapped diloco shardings here).
613+
params_shardings = state_mesh_shardings.params
614+
else:
615+
params_shardings, state_mesh_shardings = sharding.maybe_update_params_sharding_with_opt(config, state_mesh_shardings)
605616

606617
p_train_step, p_eval_step = train_utils.jit_train_and_eval_step(
607618
config,
@@ -622,7 +633,8 @@ def train_loop(config, recorder, state=None):
622633
elif config.shard_optimizer_over_data:
623634
# NNX: reshard state so params match the data-sharded in_shardings (Zero-1 layout)
624635
state = jax.device_put(state, state_mesh_shardings)
625-
if isinstance(model, nn.Module):
636+
if isinstance(model, nn.Module) or config.enable_diloco:
637+
# The DiLoCo train step takes (state, batch, rng), like the Linen step.
626638
lower_args = (state, shaped_batch, init_rng)
627639
else:
628640
lower_args = (state, shaped_batch)
@@ -638,6 +650,8 @@ def train_loop(config, recorder, state=None):
638650
# Write train config params, num model params, and XLA flags to tensorboard
639651
if isinstance(model, nn.Module):
640652
setup_params = state.params
653+
elif config.enable_diloco:
654+
setup_params = state.params # DiLoCoTrainState.params: the outer (global) params
641655
else:
642656
_, setup_params, _ = nnx.split(state.model, nnx.Param, ...)
643657
metric_logger_instance.write_setup_info_to_tensorboard(setup_params)
@@ -652,7 +666,7 @@ def train_loop(config, recorder, state=None):
652666

653667
with jax.profiler.StepTraceAnnotation("train", step_num=step):
654668
example_batch = data_loader.load_next_batch(rampup_manager=rampup_manager)
655-
if isinstance(model, nn.Module):
669+
if isinstance(model, nn.Module) or config.enable_diloco:
656670
# pylint: disable=not-callable
657671
step_rng_args = (jax.jit(jax.random.fold_in)(init_rng, step),)
658672
else:

src/maxtext/utils/train_utils.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,9 @@ def create_train_state_fn():
295295
inner_state_shardings = diloco.add_diloco_to_sharding(state_mesh_shardings)
296296
state_mesh_shardings = diloco.DiLoCoTrainState(
297297
inner_state_shardings,
298-
state_mesh_shardings_params,
298+
# Match the outer params' pure-dict structure (build_diloco_state stores
299+
# outer_params via to_pure_dict), so the sharding tree matches the state tree.
300+
state_mesh_shardings_params.to_pure_dict() if config.pure_nnx else state_mesh_shardings_params,
299301
outer_opt_state_sharding,
300302
jax.sharding.NamedSharding(mesh=step_mesh, spec=jax.sharding.PartitionSpec()),
301303
)
@@ -319,8 +321,16 @@ def create_train_state_fn():
319321
maxtext_utils.print_shardings_params(state_params, state_mesh_shardings_params, mesh, logical_annotations_params)
320322

321323
if config.pure_nnx:
322-
train_state = nnx.merge(state_graphdef, state)
323-
model = train_state.model
324+
if config.enable_diloco:
325+
# `state` is a DiLoCoTrainState whose inner_state holds the broadcast
326+
# TrainStateNNX. Don't merge it into the plain-model graphdef. The inner
327+
# train step needs that graphdef, so surface it as `model`: train_loop
328+
# uses it as jit_model and feeds the DiLoCoTrainState through as state.
329+
train_state = state
330+
model = state_graphdef
331+
else:
332+
train_state = nnx.merge(state_graphdef, state)
333+
model = train_state.model
324334
else:
325335
train_state = state
326336

0 commit comments

Comments
 (0)