Skip to content

Commit 6762cbb

Browse files
Merge pull request #4759 from AI-Hypercomputer:chris/spmd-streaming-dlco
PiperOrigin-RevId: 966865365
2 parents 6797fbf + c660f58 commit 6762cbb

19 files changed

Lines changed: 2159 additions & 234 deletions

src/maxtext/common/checkpointing.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from etils import epath
2626
from flax import nnx
27+
from flax import struct
2728

2829

2930
from flax.training import train_state
@@ -36,6 +37,7 @@
3637
from maxtext.input_pipeline.multihost_dataloading import MultiHostDataLoadIterator
3738
from maxtext.input_pipeline.multihost_dataloading import RemoteIteratorWrapper
3839
from maxtext.input_pipeline.synthetic_data_processing import PlaceHolderDataIterator
40+
from maxtext.trainers.diloco.utils import spmd_diloco_checkpointing as diloco_checkpoint_utils
3941
from maxtext.utils import elastic_utils
4042
from maxtext.utils import exceptions
4143
from maxtext.utils import gcs_utils
@@ -186,6 +188,16 @@ def _load_linen_checkpoint_into_nnx(
186188
present, else keep their fresh init value. A genuinely-missing weight raises.
187189
"""
188190
max_logging.log(f"Restoring Linen-layout checkpoint into NNX state at {path}")
191+
if config and getattr(config, "enable_diloco", False):
192+
return diloco_checkpoint_utils.restore_diloco_checkpoint(
193+
path,
194+
abstract_nnx_state,
195+
checkpoint_storage_concurrent_gb,
196+
use_ocdbt=use_ocdbt,
197+
use_zarr3=use_zarr3,
198+
config=config,
199+
)
200+
189201
linen_abstract = train_state_nnx.to_checkpoint_dict(abstract_nnx_state)
190202
if config and getattr(getattr(config, "lora", None), "enable_lora", False):
191203
linen_abstract = _filter_lora_trainable_state(linen_abstract)
@@ -536,9 +548,21 @@ def map_to_pspec(data):
536548
)
537549
ocp.type_handlers.register_type_handler(jax.Array, array_handler, override=True)
538550

539-
restore_target = (
540-
train_state_nnx.to_checkpoint_dict(abstract_unboxed_pre_state) if is_nnx else abstract_unboxed_pre_state
541-
)
551+
is_diloco = bool(maxtext_config and getattr(maxtext_config, "enable_diloco", False))
552+
553+
# Map the expected training state to the on-disk checkpoint dictionary layout:
554+
# - DiLoCo: DiLoCoTrainState (wrapping NNX or Linen inner state + outer params + opt state).
555+
# - Standard non-DiLoCo NNX: TrainStateNNX (converted to Linen collection layout for storage).
556+
# - Standard non-DiLoCo Linen: TrainState dataclass (used directly).
557+
if is_diloco:
558+
restore_target = diloco_checkpoint_utils.to_diloco_checkpoint_dict(
559+
abstract_unboxed_pre_state, config=maxtext_config
560+
)
561+
elif is_nnx:
562+
restore_target = train_state_nnx.to_checkpoint_dict(abstract_unboxed_pre_state)
563+
else:
564+
restore_target = abstract_unboxed_pre_state
565+
542566
if maxtext_config and getattr(getattr(maxtext_config, "lora", None), "enable_lora", False):
543567
restore_target = _filter_lora_trainable_state(restore_target)
544568
restore_args = jax.tree_util.tree_map(map_to_pspec, restore_target)
@@ -560,7 +584,11 @@ def map_to_pspec(data):
560584
),
561585
):
562586
restored = checkpoint_manager.restore(step, args=Composite(state=checkpoint_args)).state
563-
if is_nnx:
587+
if is_diloco:
588+
restored = diloco_checkpoint_utils.from_diloco_checkpoint_dict(
589+
restored, abstract_unboxed_pre_state, config=maxtext_config
590+
)
591+
elif is_nnx:
564592
restored = _restored_linen_to_nnx(restored, abstract_unboxed_pre_state, config=maxtext_config)
565593
return (
566594
restored,
@@ -585,15 +613,25 @@ def map_to_pspec(data):
585613
checkpoint_args,
586614
expansion_factor_real_data,
587615
)
588-
if is_nnx:
616+
if is_diloco:
617+
restored_items = diloco_checkpoint_utils.from_diloco_checkpoint_dict(
618+
restored["items"], abstract_unboxed_pre_state, config=maxtext_config
619+
)
620+
restored = {"items": restored_items}
621+
elif is_nnx:
589622
restored_items = _restored_linen_to_nnx(restored["items"], abstract_unboxed_pre_state, config=maxtext_config)
590623
restored = {"items": restored_items}
591624
return (restored, iterator)
592625
# Case 3: Default/Fallback case.
593626
# This case acts as a wildcard ('_') and matches if none of the preceding cases were met.
594627
case _:
595628
restored = checkpoint_manager.restore(step, args=Composite(items=checkpoint_args))
596-
if is_nnx:
629+
if is_diloco:
630+
restored_items = diloco_checkpoint_utils.from_diloco_checkpoint_dict(
631+
restored["items"], abstract_unboxed_pre_state, config=maxtext_config
632+
)
633+
restored = {"items": restored_items}
634+
elif is_nnx:
597635
restored_items = _restored_linen_to_nnx(restored["items"], abstract_unboxed_pre_state, config=maxtext_config)
598636
restored = {"items": restored_items}
599637
return (restored, None)
@@ -848,7 +886,9 @@ def maybe_save_checkpoint(checkpoint_manager, state, config, data_iterator, step
848886
_handle_post_checkpoint_preemption(checkpoint_manager, actual_step, force_ckpt_save)
849887
return
850888

851-
if latest_step(checkpoint_manager) == actual_step:
889+
# Skip if step directory already exists (e.g. step 0 or prior checkpoints in all_steps())
890+
# to prevent Orbax OCDBT UUID collisions during auto-resume / continuation runs for DiLoCo.
891+
if latest_step(checkpoint_manager) == actual_step or actual_step in checkpoint_manager.all_steps():
852892
max_logging.log(f"Checkpoint for step {actual_step} already exists, skipping save.")
853893
return
854894

@@ -903,18 +943,18 @@ def _filter_dict(val, path=()):
903943

904944
def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=None, force=False):
905945
"""Wrapper for saving checkpoint."""
906-
if not isinstance(state, (dict, nnx.State, train_state.TrainState)):
946+
# Allow struct.PyTreeNode so Flax dataclass states (e.g. DiLoCoTrainState) aren't cleared to empty dicts ({})
947+
if not isinstance(state, (dict, nnx.State, train_state.TrainState, struct.PyTreeNode)):
907948
if isinstance(state, train_state_nnx.TrainStateNNX):
908949
state = nnx.state(state)
909950
elif not isinstance(state, (dict, nnx.State)):
910951
state = {}
911952

912-
if config and getattr(config, "pure_nnx", False) and isinstance(state, nnx.State):
953+
if config and getattr(config, "enable_diloco", False):
954+
state = diloco_checkpoint_utils.to_diloco_checkpoint_dict(state, config)
955+
elif config and getattr(config, "pure_nnx", False):
913956
# Save in the Linen on-disk layout so pure_nnx and Linen checkpoints are interchangeable.
914-
if getattr(config, "enable_diloco", False):
915-
step_value = state.step.get_value() if hasattr(state.step, "get_value") else state.step
916-
state = train_state_nnx.to_linen_checkpoint_dict({"model": state.params, "optimizer": {"step": step_value}})
917-
else:
957+
if isinstance(state, nnx.State):
918958
state = train_state_nnx.to_checkpoint_dict(state)
919959

920960
if config and getattr(config, "enable_checkpointing", False):

src/maxtext/common/data_loader.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
GoodputEvent,
2424
maybe_record_goodput,
2525
)
26-
from maxtext.trainers.diloco import diloco
26+
from maxtext.utils import diloco_sharding
2727
from maxtext.utils import elastic_utils
2828
from maxtext.utils import exceptions
2929
from maxtext.utils.sharding import get_input_data_sharding
@@ -89,7 +89,7 @@ def load_next_batch(self, *args, **kwargs):
8989
"""Loads the next batch with sharding hint."""
9090
example_batch = self.load_next_batch_pre_sharding()
9191
if self.config.enable_diloco:
92-
example_batch = diloco.reshape_first_axis_with_diloco(self.config.num_diloco_replicas, example_batch)
92+
example_batch = diloco_sharding.reshape_first_axis_with_diloco(self.config.num_diloco_replicas, example_batch)
9393
return jax.device_put(example_batch, self.input_data_shardings)
9494

9595
def check_example_batch(self):
@@ -171,7 +171,7 @@ def _slice(data):
171171
output = jax.tree.map(_slice, self.batch_buffer)
172172
self.rampup_active = rampup_manager.update()
173173
if self.config.enable_diloco:
174-
output = diloco.reshape_first_axis_with_diloco(self.config.num_diloco_replicas, output)
174+
output = diloco_sharding.reshape_first_axis_with_diloco(self.config.num_diloco_replicas, output)
175175
return jax.device_put(output, self.input_data_shardings)
176176

177177

src/maxtext/configs/base.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -943,6 +943,14 @@ dcn_bandwidth_latency: "50ms"
943943
# The network interface to apply throttling rules to.
944944
dcn_bandwidth_interface: "eth0"
945945

946+
# Streaming DiLoCo params
947+
enable_streaming_diloco: false
948+
num_diloco_fragments: null
949+
use_sequential_layers: false
950+
num_communication_overlapping_steps: 0
951+
communication_overlapping_alpha: 0.0
952+
953+
946954
# You may disable clipping by setting gradient_clipping_threshold to zero.
947955
gradient_clipping_threshold: 1.0
948956

src/maxtext/configs/types.py

Lines changed: 65 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1781,11 +1781,26 @@ class DilocoParams(BaseModel):
17811781

17821782
enable_diloco: bool = Field(False, description="Enable Diloco parallelism")
17831783
diloco_sync_period: int = Field(36, description="Diloco sync period.")
1784+
1785+
@model_validator(mode="after")
1786+
def validate_streaming_diloco_params(self) -> "DilocoParams":
1787+
"""Validates streaming DiLoCo parameters."""
1788+
if self.enable_streaming_diloco:
1789+
if not self.enable_diloco:
1790+
raise ValueError("enable_diloco must be True when enable_streaming_diloco is True.")
1791+
if self.num_diloco_fragments is None:
1792+
raise ValueError("num_diloco_fragments must be specified when enable_streaming_diloco is True.")
1793+
if self.num_diloco_fragments < 2:
1794+
raise ValueError(
1795+
f"num_diloco_fragments ({self.num_diloco_fragments}) must be at least 2 when enable_streaming_diloco "
1796+
"is True (1 for non-scanned parameters, at least 1 for scanned layers)."
1797+
)
1798+
return self
1799+
17841800
diloco_outer_lr: float = Field(0.3, description="learning rate for outer optimizer.")
17851801
diloco_outer_momentum: float = Field(0.9, description="momentum for outer optimizer.")
17861802
dcn_bandwidth_limit: str = Field(
1787-
"",
1788-
description="Programmatic DCN egress bandwidth limit (e.g., '28gbit'). Empty means no limit.",
1803+
"", description="Programmatic DCN egress bandwidth limit per VM (e.g., '28gbit'). Empty means no limit."
17891804
)
17901805
dcn_bandwidth_burst: str = Field("10mb", description="Burst size for Token Bucket Filter (TBF) traffic shaping.")
17911806
dcn_bandwidth_latency: str = Field(
@@ -1794,6 +1809,40 @@ class DilocoParams(BaseModel):
17941809
)
17951810
dcn_bandwidth_interface: str = Field("eth0", description="Network interface to apply bandwidth limits on.")
17961811

1812+
# Streaming DiLoCo parameters
1813+
enable_streaming_diloco: bool = Field(
1814+
False,
1815+
description=(
1816+
"Enable streaming DiLoCo parallelism (https://arxiv.org/abs/2501.18512). Streaming DiLoCo partitions"
1817+
" model parameters into fragments and pipelines cross-island synchronization one fragment per inner step,"
1818+
" overlapping inter-cluster communication with accelerator computation."
1819+
),
1820+
)
1821+
num_diloco_fragments: int | None = Field(
1822+
None,
1823+
description=(
1824+
"Total number of fragments to partition the model layers into (including 1 fragment for non-scanned"
1825+
" parameters). Required when enable_streaming_diloco is True."
1826+
),
1827+
)
1828+
use_sequential_layers: bool = Field(False, description="Whether to sync layers sequentially (or interleaved).")
1829+
num_communication_overlapping_steps: NonNegativeInt = Field(
1830+
0, description="Steps of communication overlap with computation. \\tau from the paper."
1831+
)
1832+
communication_overlapping_alpha: float = Field(
1833+
0.0,
1834+
ge=0.0,
1835+
le=1.0,
1836+
description=(
1837+
"Interpolation factor between local and global parameters. alpha=1"
1838+
" means no communication between islands, alpha=0 means discards any"
1839+
" updates done in the inner optimizer in the first"
1840+
" `num_communication_overlapping_steps` steps. alpha=0.5 does a"
1841+
" uniform average between the local fragment parameters and the"
1842+
" globally shared one."
1843+
),
1844+
)
1845+
17971846

17981847
class Optimizer(BaseModel):
17991848
"""Configuration for the optimizer and learning rate schedule."""
@@ -3829,6 +3878,20 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
38293878
self.validate_ragged_buffer_factor()
38303879
self.validate_num_moe_emb_chunks()
38313880

3881+
if self.enable_diloco and not self.pure_nnx:
3882+
raise ValueError("enable_diloco=True requires pure_nnx=True (Linen support for DiLoCo has been removed).")
3883+
3884+
if self.enable_streaming_diloco:
3885+
if not self.scan_layers:
3886+
raise ValueError("enable_streaming_diloco=True requires scan_layers=True.")
3887+
if self.num_diloco_fragments is not None and self.num_diloco_fragments > 1:
3888+
num_transformer_fragments = self.num_diloco_fragments - 1
3889+
if self.num_decoder_layers % num_transformer_fragments != 0:
3890+
raise ValueError(
3891+
f"The number of decoder layers ({self.num_decoder_layers}) must be divisible by "
3892+
f"(num_diloco_fragments - 1) ({num_transformer_fragments}) when enable_streaming_diloco is True."
3893+
)
3894+
38323895
# Gemma 4 small (E2B / E4B) uses per-layer KV sharing, which is incompatible with nn.scan.
38333896
if self.model_name in ("gemma4-e2b", "gemma4-e4b") and self.scan_layers:
38343897
raise ValueError(

src/maxtext/input_pipeline/synthetic_data_processing.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
from maxtext.input_pipeline import multihost_dataloading
2727
from maxtext.configs import pyconfig
28+
from maxtext.utils.diloco_sharding import reshape_first_axis_with_diloco
2829
from maxtext.utils import sharding
2930

3031

@@ -126,6 +127,8 @@ def raw_generate_synthetic_data(config: pyconfig.HyperParameters, data):
126127
output["targets"] = tokens[:, 1:]
127128
output["targets_position"] = positions[:, 1:]
128129
output["targets_segmentation"] = segmentation
130+
if config.enable_diloco:
131+
output = reshape_first_axis_with_diloco(config.num_diloco_replicas, output)
129132
return output
130133

131134

0 commit comments

Comments
 (0)