Skip to content

Commit 19fbc5e

Browse files
angel-coreGoogle-ML-Automation
authored andcommitted
Migrate Tunix's SFT Checkpoint Manager usage of Orbax V0 Checkpoint Manager to V1 Checkpointer.
PiperOrigin-RevId: 948007269
1 parent 71c7ea9 commit 19fbc5e

6 files changed

Lines changed: 238 additions & 81 deletions

File tree

docs/tutorials/posttraining/knowledge_distillation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ python3 -m maxtext.checkpoint_conversion.to_maxtext \
234234
The online distillation trainer depends on Tunix. The XPK launcher script ([`scripts/run_distill_xpk.sh`](https://github.com/AI-Hypercomputer/maxtext/blob/main/src/maxtext/trainers/post_train/distillation/scripts/run_distill_xpk.sh)) contains a `prep_image` step that layers Tunix on top of the MaxText base image. For local runs, install the same pin used by the launcher — the default `TUNIX_SOURCE` in `run_distill_xpk.sh` is the source of truth. As of this writing:
235235

236236
```bash
237-
pip install "git+https://github.com/google/tunix@110932a8395086511228483312131841521695c1"
237+
pip install "git+https://github.com/google/tunix@44af800726dd5b2c5779a1987a9294f9a3eec9ef"
238238
```
239239

240240
> **Note:** The commit pin above will drift as the launcher is updated. Before installing, check the `TUNIX_SOURCE` default in [`run_distill_xpk.sh`](https://github.com/AI-Hypercomputer/maxtext/blob/main/src/maxtext/trainers/post_train/distillation/scripts/run_distill_xpk.sh) and use that spec. Once a Tunix PyPI release ships, this will become a versioned `google-tunix==<ver>` install.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
google-tunix @ https://github.com/google/tunix/archive/683256db1a0919b5cfd46cee52cebc96331494fb.zip
1+
google-tunix @ https://github.com/google/tunix/archive/44af800726dd5b2c5779a1987a9294f9a3eec9ef.zip
22
tpu-inference @ https://github.com/vllm-project/tpu-inference/archive/4d08971683a64fd796a1b9fd0bb71128188882d5.zip
33
vllm @ git+https://github.com/vllm-project/vllm@2131b597b18d051dced4c4a605d362fa37f46ed1

src/maxtext/common/checkpointing.py

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
"""Create an Orbax CheckpointManager with specified (Async or not) Checkpointer."""
1717

18+
import asyncio
1819
import time
1920
from typing import Any, Optional
2021

@@ -169,6 +170,44 @@ class GrainCheckpointRestore(ocp.args.CheckpointArgs):
169170
process_count: Optional[int] = None
170171

171172

173+
class GrainCheckpointable(ocp_v1.StatefulCheckpointable):
174+
"""Adapts `GrainCheckpointHandler` to Orbax v1's `StatefulCheckpointable`."""
175+
176+
def __init__(
177+
self,
178+
*,
179+
save_args: GrainCheckpointSave | None = None,
180+
restore_args: GrainCheckpointRestore | None = None,
181+
):
182+
self._handler = GrainCheckpointHandler()
183+
self._save_args = save_args
184+
self._restore_args = restore_args
185+
186+
async def save(self, directory):
187+
"""Saves the Grain iterator state to the given directory."""
188+
# `GrainCheckpointHandler.save` snapshots iterator state (`get_state`) AND
189+
# writes it; both must happen in this (blocking) save phase, NOT in the
190+
# returned background coroutine.
191+
path = await directory.await_creation()
192+
self._handler.save(path, args=self._save_args)
193+
194+
async def _committed(): # nothing left for the background commit phase
195+
return None
196+
197+
return _committed()
198+
199+
async def load(self, directory: epath.Path):
200+
"""Loads the Grain iterator state from the given directory."""
201+
handler, args = self._handler, self._restore_args
202+
203+
# This will be ran to completion so asynchronous portion is just for
204+
# compatibility with Orbax v1 API.
205+
async def _background_load():
206+
await asyncio.to_thread(handler.restore, directory, args=args)
207+
208+
return _background_load()
209+
210+
172211
def _weight_mismatches(want, have, path=()):
173212
"""Returns `(path, problem)` for each weight in `want` that `have` didn't restore faithfully.
174213
@@ -1151,9 +1190,13 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
11511190
if isinstance(data_iterator, RemoteIteratorWrapper):
11521191
# Pass the wrapper directly; GrainCheckpointHandler will call save_state with the step
11531192
save_args_composite["iter"] = GrainCheckpointSave(item=data_iterator) # pyrefly: ignore[bad-assignment]
1154-
elif not isinstance(data_iterator, list) and isinstance(data_iterator.local_iterator, ElasticIterator): # pyrefly: ignore[missing-attribute]
1193+
elif not isinstance(data_iterator, list) and isinstance(
1194+
data_iterator.local_iterator, ElasticIterator
1195+
): # pyrefly: ignore[missing-attribute]
11551196
# ElasticIterator checkpoints a single global scalar shared by all shards.
1156-
save_args_composite["iter"] = GrainCheckpointSave(item=data_iterator.local_iterator) # pyrefly: ignore[bad-assignment]
1197+
save_args_composite["iter"] = GrainCheckpointSave(
1198+
item=data_iterator.local_iterator
1199+
) # pyrefly: ignore[bad-assignment]
11571200
else:
11581201
if not isinstance(data_iterator, list):
11591202
data_iterator = [data_iterator]
@@ -1163,7 +1206,9 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
11631206
process_count_total = process_count_total // config.expansion_factor_real_data
11641207
for i, data_iter in enumerate(data_iterator):
11651208
process_index = jax.process_index() + i * jax.process_count()
1166-
grain_iters_to_save.append((data_iter.local_iterator, process_index, process_count_total)) # pyrefly: ignore[missing-attribute]
1209+
grain_iters_to_save.append(
1210+
(data_iter.local_iterator, process_index, process_count_total)
1211+
) # pyrefly: ignore[missing-attribute]
11671212
save_args_composite["iter"] = GrainCheckpointSave(item=grain_iters_to_save) # pyrefly: ignore[bad-assignment]
11681213

11691214
custom_metadata = {}

src/maxtext/trainers/post_train/distillation/distillation_utils.py

Lines changed: 30 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131

3232
from maxtext.utils import max_logging
3333
from maxtext.utils import maxtext_utils
34-
# Reuse MaxText's native checkpointing logic.
35-
from maxtext.common.checkpointing import GrainCheckpointHandler, GrainCheckpointSave, GrainCheckpointRestore
34+
from maxtext.common import checkpointing
3635
from tunix.sft import checkpoint_manager as tunix_checkpoint_manager
3736
from tunix.sft import peft_trainer
3837

@@ -508,7 +507,9 @@ def compute_loss(
508507
log_t_p_T_sparse = jax.nn.log_softmax(t_logits / temperature, axis=-1)
509508

510509
# 2. Gather Student unnormalized logits at the Teacher's exact Top-K indices
511-
s_logits_sparse = jnp.take_along_axis(s_logits, teacher_output.top_k_indices, axis=-1) # pyrefly: ignore[bad-argument-type]
510+
s_logits_sparse = jnp.take_along_axis(
511+
s_logits, teacher_output.top_k_indices, axis=-1
512+
) # pyrefly: ignore[bad-argument-type]
512513

513514
# 3. Normalize Student probabilities only over the exact same Top-K subset
514515
log_s_T_sparse = jax.nn.log_softmax(s_logits_sparse / temperature, axis=-1)
@@ -558,7 +559,9 @@ def compute_loss(
558559
s_features_sliced = s_features_sliced.astype(jnp.float32)
559560
t_features_sliced = t_features_sliced.astype(jnp.float32)
560561

561-
feature_loss = beta_feature * self.feature_loss_fn(s_features_sliced, t_features_sliced, mask) # pyrefly: ignore[not-callable]
562+
feature_loss = beta_feature * self.feature_loss_fn(
563+
s_features_sliced, t_features_sliced, mask
564+
) # pyrefly: ignore[not-callable]
562565

563566
total_loss = base_logit_loss + feature_loss
564567

@@ -647,8 +650,9 @@ def create_labels(self, targets, targets_segmentation=None, **kwargs):
647650
class MaxTextCheckpointManager(tunix_checkpoint_manager.CheckpointManager):
648651
"""Custom CheckpointManager that uses MaxText's native handlers.
649652
650-
This manager extends Tunix to support saving/restoring the MaxText input pipeline
651-
(Grain) alongside the model and optimizer.
653+
Model and optimizer are delegated to Tunix's v1 ``Checkpointer`` unchanged.
654+
The Grain input pipeline is added as an extra ``"iter"`` checkpointable via
655+
``GrainCheckpointable``, which wraps MaxText's ``GrainCheckpointHandler``.
652656
"""
653657

654658
def __init__(
@@ -662,32 +666,6 @@ def __init__(
662666
self.student_config = student_config
663667
self._iterator = raw_iterator
664668

665-
# Re-initialize internal Orbax manager with MaxText's Grain handler
666-
# pylint: disable=access-member-before-definition
667-
# pytype: disable=attribute-error
668-
if self._checkpoint_manager is not None:
669-
root_directory = self._checkpoint_manager.directory
670-
671-
if options is None:
672-
options = getattr(self._checkpoint_manager, "options", None)
673-
674-
item_handlers = {
675-
"model_params": checkpoint.PyTreeCheckpointHandler(),
676-
"optimizer_state": checkpoint.PyTreeCheckpointHandler(),
677-
"custom_metadata": checkpoint.JsonCheckpointHandler(),
678-
# Use MaxText's handler for the iterator
679-
"iter": GrainCheckpointHandler(),
680-
}
681-
682-
self._checkpoint_manager.close()
683-
self._checkpoint_manager = checkpoint.CheckpointManager(
684-
root_directory,
685-
item_handlers=item_handlers,
686-
options=options,
687-
)
688-
# pytype: enable=attribute-error
689-
# pylint: enable=access-member-before-definition
690-
691669
def save(
692670
self,
693671
step,
@@ -697,10 +675,8 @@ def save(
697675
force=False,
698676
custom_metadata=None,
699677
):
700-
"""Saves the checkpoint including the input pipeline state (if available)."""
701-
if self._checkpoint_manager is None:
702-
return False
703-
if not force and not self._checkpoint_manager.should_save(step):
678+
"""Saves model, optimizer and the Grain input pipeline state."""
679+
if self._checkpointer is None:
704680
return False
705681

706682
# Standard Tunix Logic for Model/Optimizer.
@@ -711,21 +687,12 @@ def save(
711687
else:
712688
params = nnx.state(target_model)
713689

714-
# Define standard SaveArgs once to reuse
715-
default_save_args = checkpoint.SaveArgs()
716-
cp_save_args = {
717-
"model_params": checkpoint.args.PyTreeSave(
718-
item=params, save_args=jax.tree.map(lambda _: default_save_args, params)
719-
),
720-
}
721-
# Exclude optimizer state if the flag is set OR if learn_to_init_mode is active.
690+
checkpointables: dict[str, Any] = {"model_params": params}
691+
# Exclude optimizer state when learn_to_init_mode is active.
722692
exclude_opt = self.student_config.learn_to_init_mode
723693

724694
if optimizer is not None and not exclude_opt:
725-
optimizer_state = nnx.state(optimizer, nnx.optimizer.OptState)
726-
cp_save_args["optimizer_state"] = checkpoint.args.PyTreeSave(
727-
item=optimizer_state, save_args=jax.tree.map(lambda _: default_save_args, optimizer_state)
728-
)
695+
checkpointables["optimizer_state"] = nnx.state(optimizer, nnx.optimizer.OptState)
729696

730697
if self._iterator is not None:
731698
# Follow MaxText's logic to handle multi-process saving
@@ -743,15 +710,11 @@ def save(
743710
local_iter = data_iter.local_iterator if hasattr(data_iter, "local_iterator") else data_iter
744711
grain_iters_to_save.append((local_iter, process_index, process_count_total))
745712

746-
# Use GrainCheckpointSave wrapper
747-
cp_save_args["iter"] = GrainCheckpointSave(item=grain_iters_to_save) # pyrefly: ignore[bad-assignment]
713+
checkpointables["iter"] = checkpointing.GrainCheckpointable(
714+
save_args=checkpointing.GrainCheckpointSave(item=grain_iters_to_save) # pyrefly: ignore[bad-assignment]
715+
)
748716

749-
return self._checkpoint_manager.save(
750-
step,
751-
args=checkpoint.args.Composite(**cp_save_args),
752-
custom_metadata=custom_metadata or {},
753-
force=force,
754-
)
717+
return self._save_checkpointables(step, checkpointables, force, custom_metadata)
755718

756719
def maybe_restore( # pyrefly: ignore[bad-override]
757720
self,
@@ -766,12 +729,12 @@ def maybe_restore( # pyrefly: ignore[bad-override]
766729
Returns:
767730
(restored step, custom_metadata dict). Step is 0 if no checkpoint exists.
768731
"""
769-
if self._checkpoint_manager is None:
732+
if self._checkpointer is None:
770733
return 0, {}
771734

772735
target_model = getattr(model, "student_model", model)
773736

774-
step, _ = super().maybe_restore(
737+
step, custom_metadata = super().maybe_restore(
775738
model=target_model, # pyrefly: ignore[bad-argument-type]
776739
optimizer=optimizer,
777740
restore_only_lora_params=restore_only_lora_params,
@@ -781,20 +744,14 @@ def maybe_restore( # pyrefly: ignore[bad-override]
781744

782745
max_logging.log(f"Restored from checkpoint step {step}.")
783746

784-
metadata = self._checkpoint_manager.metadata(step)
785-
if metadata and hasattr(metadata, "custom_metadata") and metadata.custom_metadata is not None:
786-
custom_metadata = metadata.custom_metadata
787-
else:
788-
custom_metadata = {}
789-
790-
return step, dict(custom_metadata)
747+
return step, dict(custom_metadata or {})
791748

792749
def restore_iterator(self):
793750
"""Restores the iterator using MaxText's logic."""
794-
if self._checkpoint_manager is None or self._iterator is None:
751+
if self._checkpointer is None or self._iterator is None:
795752
return None
796753

797-
step = self._checkpoint_manager.latest_step()
754+
step = self.latest_step()
798755
if step is None:
799756
return None
800757

@@ -804,9 +761,10 @@ def restore_iterator(self):
804761
data_iter = self._iterator
805762
local_iter = data_iter.local_iterator if hasattr(data_iter, "local_iterator") else data_iter
806763

807-
restore_args = GrainCheckpointRestore(item=local_iter)
808-
809-
self._checkpoint_manager.restore(step, args=checkpoint.args.Composite(iter=restore_args))
764+
self._checkpointer.load_checkpointables(
765+
step,
766+
{"iter": checkpointing.GrainCheckpointable(restore_args=checkpointing.GrainCheckpointRestore(item=local_iter))},
767+
)
810768
# Since Grain restores in-place via set_state(), we return the original object
811769
return self._iterator
812770

@@ -816,5 +774,5 @@ def restore_iterator(self):
816774

817775
def wait_until_finished(self):
818776
"""Blocks until all outstanding checkpoint operations are complete."""
819-
if self._checkpoint_manager is not None:
820-
self._checkpoint_manager.wait_until_finished()
777+
if self._checkpointer is not None:
778+
self._checkpointer.wait()

src/maxtext/trainers/post_train/distillation/scripts/run_distill_xpk.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@
105105
#
106106
# Image pinning (used by prep_image):
107107
# TUNIX_SOURCE pip-installable spec for tunix.
108-
# default: git+https://github.com/google/tunix@110932a8395086511228483312131841521695c1
108+
# default: git+https://github.com/google/tunix@44af800726dd5b2c5779a1987a9294f9a3eec9ef
109109
# Use "google-tunix==<ver>" once a pypi release ships with the
110110
# multi-host shard_input fix.
111111
# JAX_PIN default: 0.10.0 — version to pin back after tunix deps resolve.
@@ -164,7 +164,7 @@ require_env() {
164164
: "${DISTILL_LAYER_INDICES:=[0,1,2,3,4,5,6,7]}"
165165

166166
# Image pinning (used by prep_image).
167-
: "${TUNIX_SOURCE:=git+https://github.com/google/tunix@110932a8395086511228483312131841521695c1}"
167+
: "${TUNIX_SOURCE:=git+https://github.com/google/tunix@44af800726dd5b2c5779a1987a9294f9a3eec9ef}"
168168
: "${JAX_PIN:=0.10.0}"
169169
: "${JAXLIB_PIN:=0.10.0}"
170170
: "${LIBTPU_PIN:=0.0.39}"

0 commit comments

Comments
 (0)