Skip to content

Commit 6e60e75

Browse files
Merge pull request AI-Hypercomputer#4566 from AI-Hypercomputer:feat/checkpointing-load-state-dedup
PiperOrigin-RevId: 953304112
2 parents e12d2fd + 82ef7bf commit 6e60e75

5 files changed

Lines changed: 369 additions & 95 deletions

File tree

src/maxtext/checkpoint_conversion/utils/load_dynamic.py

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,9 +251,12 @@ def tensor_getter(key):
251251
return {"params": restored_params}
252252

253253

254-
def load_safetensors_dynamic_state(path, abstract_unboxed_pre_state, maxtext_config):
254+
def load_safetensors_dynamic_state(path, abstract_params, maxtext_config):
255255
"""Main entry point to dynamically build and load safetensors into MaxText format.
256256
257+
`abstract_params` is the weights of the target state -- Linen's `params` collection, or the
258+
NNX params state -- not the full train state; the HF param mappings name weights only.
259+
257260
Splits execution into:
258261
1. Deriving Mappings
259262
2. Loading Sharded arrays directly to TPUs
@@ -349,11 +352,7 @@ def load_safetensors_dynamic_state(path, abstract_unboxed_pre_state, maxtext_con
349352
param_map_mt_to_hf, hook_fn_map_mt = get_hf_config_and_mappings(maxtext_config)
350353
max_logging.log(f"[1/3] Mappings derived in {time.time() - t_total:.2f}s")
351354

352-
target_tree = (
353-
abstract_unboxed_pre_state.to_pure_dict()
354-
if isinstance(abstract_unboxed_pre_state, nnx.State)
355-
else abstract_unboxed_pre_state.params
356-
)
355+
target_tree = abstract_params.to_pure_dict() if isinstance(abstract_params, nnx.State) else abstract_params
357356

358357
t1 = time.time()
359358
hf_state = load_sharded_hf_state(path)

src/maxtext/common/checkpointing.py

Lines changed: 86 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"""Create an Orbax CheckpointManager with specified (Async or not) Checkpointer."""
1717

1818
import datetime
19+
import importlib
1920
import time
2021
from typing import Any
2122

@@ -157,33 +158,58 @@ def _load_linen_checkpoint_into_nnx(
157158
restore_args = ocp.checkpoint_utils.construct_restore_args(linen_abstract)
158159
restored = ocp.args.PyTreeRestore(item=linen_abstract, restore_args=restore_args, partial_restore=True)
159160
restored = ckptr.restore(epath.Path(path), args=restored)
160-
_raise_on_weight_mismatch(*_expected_and_restored_params(abstract_nnx_state, restored))
161-
return _linen_items_to_nnx(restored, abstract_nnx_state)
161+
return _restored_linen_to_nnx(restored, abstract_nnx_state)
162162

163163

164-
def _restore_emergency_linen_checkpoint_into_nnx(
165-
checkpoint_manager,
166-
step,
167-
abstract_nnx_state,
168-
map_to_pspec,
169-
):
170-
"""Restores an emergency Linen-layout checkpoint into an NNX state.
164+
def _restored_linen_to_nnx(restored_linen, abstract_nnx_state):
165+
"""Reshapes a restored Linen-layout tree into the NNX state.
171166
172-
The `nnx_aux` subtree is stored inside `items`, so an emergency checkpoint
173-
carries it too; it's restored when present and otherwise kept at its fresh
174-
init value. A genuinely-missing weight raises.
167+
Raises if the checkpoint is missing a weight. Every NNX restore path ends here: the load
168+
itself is the Linen one, since pure_nnx reads and writes the Linen on-disk layout.
175169
"""
176-
max_logging.log(f"Restoring emergency Linen-layout checkpoint into NNX state at step {step}")
177-
linen_abstract = train_state_nnx.to_checkpoint_dict(abstract_nnx_state)
178-
restore_args = jax.tree_util.tree_map(map_to_pspec, linen_abstract)
179-
checkpoint_args = ocp.args.PyTreeRestore(
180-
item=linen_abstract,
181-
restore_args=restore_args,
182-
partial_restore=True,
183-
)
184-
restored = checkpoint_manager.restore(step, args=Composite(state=checkpoint_args)).state
185-
_raise_on_weight_mismatch(*_expected_and_restored_params(abstract_nnx_state, restored))
186-
return _linen_items_to_nnx(restored, abstract_nnx_state)
170+
_raise_on_weight_mismatch(*_expected_and_restored_params(abstract_nnx_state, restored_linen))
171+
return _linen_items_to_nnx(restored_linen, abstract_nnx_state)
172+
173+
174+
def _abstract_params(abstract_unboxed_pre_state):
175+
"""Returns the state's weights: the NNX Param subtree, or Linen's `params` collection."""
176+
if isinstance(abstract_unboxed_pre_state, nnx.State):
177+
return nnx.split_state(abstract_unboxed_pre_state.model, nnx.Param, ...)[0]
178+
return abstract_unboxed_pre_state.params
179+
180+
181+
def _bare_weights(tree):
182+
"""Strips the Flax `params` collection wrapper so weights compare at the same depth.
183+
184+
A Linen params tree is the collection, an NNX one the bare weights; the dynamic
185+
safetensors loader always returns the collection.
186+
"""
187+
return tree["params"] if isinstance(tree, dict) and len(tree) == 1 and "params" in tree else tree
188+
189+
190+
def _resolve_conversion_fn(checkpoint_conversion_fn):
191+
"""Returns `checkpoint_conversion_fn` as a callable.
192+
193+
Config carries it as a dotted string ("my_pkg.my_module.my_fn"), so it has to be imported
194+
before it can be called. A callable is used as is.
195+
"""
196+
if checkpoint_conversion_fn is None:
197+
raise ValueError(
198+
"source_checkpoint_layout='safetensors' needs `checkpoint_conversion_fn` to map the "
199+
"checkpoint's weights onto the model's, e.g. checkpoint_conversion_fn=my_pkg.my_module.my_fn."
200+
)
201+
if callable(checkpoint_conversion_fn):
202+
return checkpoint_conversion_fn
203+
module_name, _, fn_name = str(checkpoint_conversion_fn).rpartition(".")
204+
if not module_name:
205+
raise ValueError(f"`checkpoint_conversion_fn` must be a dotted path to a function, got {checkpoint_conversion_fn!r}.")
206+
try:
207+
fn = getattr(importlib.import_module(module_name), fn_name, None)
208+
except ImportError as e:
209+
raise ValueError(f"Could not import `checkpoint_conversion_fn` {checkpoint_conversion_fn!r}: {e}") from e
210+
if not callable(fn):
211+
raise ValueError(f"`checkpoint_conversion_fn` {checkpoint_conversion_fn!r} is not a function.")
212+
return fn
187213

188214

189215
def _load_full_state_from_path(
@@ -226,6 +252,8 @@ def _load_full_state_from_path(
226252
with context:
227253
return ocp_v1.load_pytree(path, abstract_unboxed_pre_state)
228254
elif source_checkpoint_layout == "safetensors":
255+
# Resolved first, so a bad config fails before the weights are read.
256+
conversion_fn = _resolve_conversion_fn(checkpoint_conversion_fn)
229257
context = ocp_v1.Context(checkpoint_layout=ocp_v1.options.CheckpointLayout.SAFETENSORS)
230258
with context:
231259
metadata = ocp_v1.pytree_metadata(path)
@@ -237,7 +265,11 @@ def combine_sharding(sds, shardings):
237265

238266
sharded_abstract_state = jax.tree.map(combine_sharding, simple_abstract_state, shardings)
239267
pre_transformed_state = ocp_v1.load_pytree(path, sharded_abstract_state)
240-
state = checkpoint_conversion_fn(pre_transformed_state)
268+
state = conversion_fn(pre_transformed_state)
269+
# The conversion fn returns MaxText's on-disk (Linen) layout, which is what pure_nnx reads,
270+
# so NNX needs the same reshape as every other restore. An NNX state passes through.
271+
if isinstance(abstract_unboxed_pre_state, nnx.State) and not isinstance(state, nnx.State):
272+
state = _restored_linen_to_nnx(state, abstract_unboxed_pre_state)
241273
return state
242274
else:
243275
raise ocp_v1.errors.InvalidLayoutError(f"Unknown checkpoint layout: {source_checkpoint_layout}")
@@ -406,6 +438,11 @@ def load_state_if_possible(
406438
set.
407439
"""
408440

441+
# pure_nnx saves in the Linen on-disk layout, so every branch below loads the same tree Linen
442+
# does: the NNX abstract is converted to that layout going in, and what comes back is reshaped
443+
# into the NNX state on the way out.
444+
is_nnx = isinstance(abstract_unboxed_pre_state, nnx.State)
445+
409446
if checkpoint_manager is not None:
410447
max_logging.log("checkpoint manager exists so trying to load this run's existing checkpoint")
411448

@@ -437,49 +474,9 @@ def map_to_pspec(data):
437474
)
438475
ocp.type_handlers.register_type_handler(jax.Array, array_handler, override=True)
439476

440-
# pure_nnx saves in the Linen on-disk layout; restore that layout (weights +
441-
# opt_state + step + nnx_aux), restoring the grain iterator in place when
442-
# present, then reshape it back into the NNX state.
443-
# (Emergency managers use their own restore path below.)
444-
if isinstance(abstract_unboxed_pre_state, nnx.State) and not isinstance(
445-
checkpoint_manager,
446-
(EmergencyCheckpointManager, EmergencyReplicatorCheckpointManager),
447-
):
448-
linen_abstract = train_state_nnx.to_checkpoint_dict(abstract_unboxed_pre_state)
449-
restore_args = jax.tree_util.tree_map(map_to_pspec, linen_abstract)
450-
checkpoint_args = ocp.args.PyTreeRestore(item=linen_abstract, restore_args=restore_args, partial_restore=True)
451-
if (
452-
dataset_type == "grain"
453-
and data_iterator
454-
and not isinstance(data_iterator, PlaceHolderDataIterator)
455-
and (checkpoint_manager.directory / str(step) / "iter").exists()
456-
):
457-
restored, _ = grain_utility.restore_grain_iterator(
458-
checkpoint_manager, step, data_iterator, checkpoint_args, expansion_factor_real_data
459-
)
460-
else:
461-
restored = checkpoint_manager.restore(step, args=Composite(items=checkpoint_args))
462-
_raise_on_weight_mismatch(*_expected_and_restored_params(abstract_unboxed_pre_state, restored["items"]))
463-
restored_nnx = _linen_items_to_nnx(restored["items"], abstract_unboxed_pre_state)
464-
return ({"items": restored_nnx}, None)
465-
466-
if isinstance(abstract_unboxed_pre_state, nnx.State) and isinstance(
467-
checkpoint_manager,
468-
(EmergencyCheckpointManager, EmergencyReplicatorCheckpointManager),
469-
):
470-
restored = _restore_emergency_linen_checkpoint_into_nnx(
471-
checkpoint_manager,
472-
step,
473-
abstract_unboxed_pre_state,
474-
map_to_pspec,
475-
)
476-
return (
477-
restored,
478-
None,
479-
)
480-
481-
# Only Linen TrainState reaches here; the NNX cases returned above.
482-
restore_target = abstract_unboxed_pre_state
477+
restore_target = (
478+
train_state_nnx.to_checkpoint_dict(abstract_unboxed_pre_state) if is_nnx else abstract_unboxed_pre_state
479+
)
483480
restore_args = jax.tree_util.tree_map(map_to_pspec, restore_target)
484481
checkpoint_args = ocp.args.PyTreeRestore(
485482
item=restore_target,
@@ -499,6 +496,8 @@ def map_to_pspec(data):
499496
),
500497
):
501498
restored = checkpoint_manager.restore(step, args=Composite(state=checkpoint_args)).state
499+
if is_nnx:
500+
restored = _restored_linen_to_nnx(restored, abstract_unboxed_pre_state)
502501
return (
503502
restored,
504503
None,
@@ -515,30 +514,43 @@ def map_to_pspec(data):
515514
and not isinstance(data_iterator, PlaceHolderDataIterator)
516515
and (checkpoint_manager.directory / str(step) / "iter").exists()
517516
):
518-
return grain_utility.restore_grain_iterator(
517+
restored, iterator = grain_utility.restore_grain_iterator(
519518
checkpoint_manager,
520519
step,
521520
data_iterator,
522521
checkpoint_args,
523522
expansion_factor_real_data,
524523
)
524+
if is_nnx:
525+
restored = {"items": _restored_linen_to_nnx(restored["items"], abstract_unboxed_pre_state)}
526+
return (restored, iterator)
525527
# Case 3: Default/Fallback case.
526528
# This case acts as a wildcard ('_') and matches if none of the preceding cases were met.
527529
case _:
528530
restored = checkpoint_manager.restore(step, args=Composite(items=checkpoint_args))
531+
if is_nnx:
532+
restored = {"items": _restored_linen_to_nnx(restored["items"], abstract_unboxed_pre_state)}
529533
return (restored, None)
530534

531535
if source_checkpoint_layout == "safetensors_dynamic":
532536
path = load_parameters_from_path or load_full_state_from_path
533537
max_logging.log(f"Dynamic On-the-Fly Formatting: Loading SafeTensors from {path}")
534538

535-
return load_safetensors_dynamic_state(path, abstract_unboxed_pre_state, maxtext_config)
539+
# Weights-only for both paths, so the loader gets the weights rather than the whole state:
540+
# the HF param mappings name weights, and an NNX state hides them under `model`.
541+
params = _abstract_params(abstract_unboxed_pre_state)
542+
restored, restored_params = load_safetensors_dynamic_state(path, params, maxtext_config)
543+
# A weight no HF mapping covered comes back unmaterialized and would reach the model as an
544+
# untrained init value. Same check the Orbax weights-only load makes.
545+
_raise_on_weight_mismatch(_bare_weights(params.to_pure_dict() if is_nnx else params), _bare_weights(restored_params))
546+
if is_nnx:
547+
# The loader returns the Linen `params` collection; NNX holds bare weights, so unwrap it
548+
# back into the params state, the shape load_params_from_path returns.
549+
nnx.replace_by_pure_dict(params, restored_params["params"])
550+
return restored, params
551+
return restored, restored_params
536552
elif load_parameters_from_path != "":
537-
if isinstance(abstract_unboxed_pre_state, nnx.State):
538-
_, params, _ = nnx.split(abstract_unboxed_pre_state.model, nnx.Param, ...)
539-
else:
540-
params = abstract_unboxed_pre_state.params
541-
553+
params = _abstract_params(abstract_unboxed_pre_state)
542554
restored_params = load_params_from_path(
543555
load_parameters_from_path,
544556
params,

0 commit comments

Comments
 (0)