Skip to content

Commit f835ffb

Browse files
Migrate Tunix's SFT Checkpoint Manager usage of Orbax V0 Checkpoint Manager to V1 Checkpointer.
Reverts 19fbc5e PiperOrigin-RevId: 948071113
1 parent 7a3c6ce commit f835ffb

6 files changed

Lines changed: 81 additions & 238 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@44af800726dd5b2c5779a1987a9294f9a3eec9ef"
237+
pip install "git+https://github.com/google/tunix@110932a8395086511228483312131841521695c1"
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/44af800726dd5b2c5779a1987a9294f9a3eec9ef.zip
1+
google-tunix @ https://github.com/google/tunix/archive/683256db1a0919b5cfd46cee52cebc96331494fb.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: 3 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

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

18-
import asyncio
1918
import time
2019
from typing import Any, Optional
2120

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

172171

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-
211172
def _weight_mismatches(want, have, path=()):
212173
"""Returns `(path, problem)` for each weight in `want` that `have` didn't restore faithfully.
213174
@@ -1190,13 +1151,9 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
11901151
if isinstance(data_iterator, RemoteIteratorWrapper):
11911152
# Pass the wrapper directly; GrainCheckpointHandler will call save_state with the step
11921153
save_args_composite["iter"] = GrainCheckpointSave(item=data_iterator) # pyrefly: ignore[bad-assignment]
1193-
elif not isinstance(data_iterator, list) and isinstance(
1194-
data_iterator.local_iterator, ElasticIterator
1195-
): # pyrefly: ignore[missing-attribute]
1154+
elif not isinstance(data_iterator, list) and isinstance(data_iterator.local_iterator, ElasticIterator): # pyrefly: ignore[missing-attribute]
11961155
# ElasticIterator checkpoints a single global scalar shared by all shards.
1197-
save_args_composite["iter"] = GrainCheckpointSave(
1198-
item=data_iterator.local_iterator
1199-
) # pyrefly: ignore[bad-assignment]
1156+
save_args_composite["iter"] = GrainCheckpointSave(item=data_iterator.local_iterator) # pyrefly: ignore[bad-assignment]
12001157
else:
12011158
if not isinstance(data_iterator, list):
12021159
data_iterator = [data_iterator]
@@ -1206,9 +1163,7 @@ def save_checkpoint(checkpoint_manager, step, state, config=None, data_iterator=
12061163
process_count_total = process_count_total // config.expansion_factor_real_data
12071164
for i, data_iter in enumerate(data_iterator):
12081165
process_index = jax.process_index() + i * jax.process_count()
1209-
grain_iters_to_save.append(
1210-
(data_iter.local_iterator, process_index, process_count_total)
1211-
) # pyrefly: ignore[missing-attribute]
1166+
grain_iters_to_save.append((data_iter.local_iterator, process_index, process_count_total)) # pyrefly: ignore[missing-attribute]
12121167
save_args_composite["iter"] = GrainCheckpointSave(item=grain_iters_to_save) # pyrefly: ignore[bad-assignment]
12131168

12141169
custom_metadata = {}

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

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

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

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

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

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

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

566563
total_loss = base_logit_loss + feature_loss
567564

@@ -650,9 +647,8 @@ def create_labels(self, targets, targets_segmentation=None, **kwargs):
650647
class MaxTextCheckpointManager(tunix_checkpoint_manager.CheckpointManager):
651648
"""Custom CheckpointManager that uses MaxText's native handlers.
652649
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``.
650+
This manager extends Tunix to support saving/restoring the MaxText input pipeline
651+
(Grain) alongside the model and optimizer.
656652
"""
657653

658654
def __init__(
@@ -666,6 +662,32 @@ def __init__(
666662
self.student_config = student_config
667663
self._iterator = raw_iterator
668664

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+
669691
def save(
670692
self,
671693
step,
@@ -675,8 +697,10 @@ def save(
675697
force=False,
676698
custom_metadata=None,
677699
):
678-
"""Saves model, optimizer and the Grain input pipeline state."""
679-
if self._checkpointer is None:
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):
680704
return False
681705

682706
# Standard Tunix Logic for Model/Optimizer.
@@ -687,12 +711,21 @@ def save(
687711
else:
688712
params = nnx.state(target_model)
689713

690-
checkpointables: dict[str, Any] = {"model_params": params}
691-
# Exclude optimizer state when learn_to_init_mode is active.
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.
692722
exclude_opt = self.student_config.learn_to_init_mode
693723

694724
if optimizer is not None and not exclude_opt:
695-
checkpointables["optimizer_state"] = nnx.state(optimizer, nnx.optimizer.OptState)
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+
)
696729

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

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

717-
return self._save_checkpointables(step, checkpointables, force, custom_metadata)
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+
)
718755

719756
def maybe_restore( # pyrefly: ignore[bad-override]
720757
self,
@@ -729,12 +766,12 @@ def maybe_restore( # pyrefly: ignore[bad-override]
729766
Returns:
730767
(restored step, custom_metadata dict). Step is 0 if no checkpoint exists.
731768
"""
732-
if self._checkpointer is None:
769+
if self._checkpoint_manager is None:
733770
return 0, {}
734771

735772
target_model = getattr(model, "student_model", model)
736773

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

745782
max_logging.log(f"Restored from checkpoint step {step}.")
746783

747-
return step, dict(custom_metadata or {})
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)
748791

749792
def restore_iterator(self):
750793
"""Restores the iterator using MaxText's logic."""
751-
if self._checkpointer is None or self._iterator is None:
794+
if self._checkpoint_manager is None or self._iterator is None:
752795
return None
753796

754-
step = self.latest_step()
797+
step = self._checkpoint_manager.latest_step()
755798
if step is None:
756799
return None
757800

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

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

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

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

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@44af800726dd5b2c5779a1987a9294f9a3eec9ef
108+
# default: git+https://github.com/google/tunix@110932a8395086511228483312131841521695c1
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@44af800726dd5b2c5779a1987a9294f9a3eec9ef}"
167+
: "${TUNIX_SOURCE:=git+https://github.com/google/tunix@110932a8395086511228483312131841521695c1}"
168168
: "${JAX_PIN:=0.10.0}"
169169
: "${JAXLIB_PIN:=0.10.0}"
170170
: "${LIBTPU_PIN:=0.0.39}"

0 commit comments

Comments
 (0)