Skip to content

Unify the NNX and Linen restore paths in load_state_if_possible#4566

Open
ecnal-cienet wants to merge 1 commit into
mainfrom
feat/checkpointing-load-state-dedup
Open

Unify the NNX and Linen restore paths in load_state_if_possible#4566
ecnal-cienet wants to merge 1 commit into
mainfrom
feat/checkpointing-load-state-dedup

Conversation

@ecnal-cienet

@ecnal-cienet ecnal-cienet commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

Description

The NNX path had two early-return blocks that each duplicated the Linen restore flow, one for the standard manager and one for the emergency managers. Any change to restore logic had to be made in three places, and they had already drifted: the NNX grain branch dropped the iterator element that the Linen branch returns.

pure_nnx saves in the Linen on-disk layout, so both paths restore the same tree. This converts the NNX abstract to that layout going in, runs the existing match statement unchanged, and reshapes what comes back on the way out. Linen behavior is unchanged.

Bugs fixed behind that duplication

  • Dynamic safetensors got the whole abstract state instead of the weights. For NNX that is fatal before any weight is read: opt_state is keyed by integer chain indices, and the loader flattens its target with a "." separator, so it died in flatten_dict with TypeError: sequence item 2: expected str instance, int found.
  • A weight no mapping covered reached the model as an untrained init value. Orbax rejects a shape conflict itself, but leaves a weight the checkpoint does not carry as an unmaterialized placeholder. The weights-only Orbax load already checks for this; the dynamic load now does too.
  • checkpoint_conversion_fn was unusable. It arrives from config as a dotted string but was called directly, raising TypeError for any value that was set. It is now imported, and resolved before the checkpoint is read.
  • Full-state safetensors now handles NNX, previously wired only for the orbax layout.

Testing

92 unit tests pass across the six suites that touch this code:

python -m pytest tests/unit/checkpointing_nnx_load_test.py \
                 tests/unit/checkpointing_test.py \
                 tests/unit/checkpointing_nnx_roundtrip_test.py \
                 tests/unit/checkpointing_emergency_nnx_test.py \
                 tests/unit/setup_initial_state_nnx_test.py \
                 tests/unit/grain_utility_test.py

End-to-end on a v6e-8: 74 assertions across gpt3-52k / gemma2-2b / llama2-7b, 0 failures.

Case Path Model Evidence Log
1a/1b NNX save → resume (standard manager) llama2-7b restoring from this run's directory step 9, resumed 10 → 19 log log
2a/2b Linen save → resume (regression) gemma2-2b Resumed 10 → 19, behavior unchanged log log
3 Linen checkpoint → NNX resume llama2-7b Restoring Linen-layout checkpoint into NNX state, continued from the Linen step 19 → 22 log
4 Weights-only load (load_parameters_path) gemma2-2b Params restored into the NNX state, training continues log
5 Full-state load (load_full_state_path) llama2-7b Reshaped into NNX, continued from the restored step 9 → 12 log
9a–9d Emergency manager save → resume gemma2-2b Local dir wiped between save and resume, so the restore came from the persistent GCS copy; NNX and Linen both 6 → 11 log log log log
6a Shape mismatch is rejected gpt3-52k Requested shape (2048, 16) is not compatible with the stored shape (1024, 16), zero training steps log
6b Missing weight is named gpt3-52k 'decoder/layers/2/self_attention/qkv_proj/kernel': missing (model expects (16, 3, 2, 8) float32), zero training steps log
7 safetensors without a conversion fn gpt3-52k Names the missing config flag; no raw 'NoneType' object is not callable log
8/8b HuggingFace safetensors → NNX qwen3-0.6b Successfully mapped 13 parameters, trained 3 steps. Pre-fix, same command: TypeError, zero parameters mapped, zero steps log log

The resume also matches Linen numerically. Two independently trained gemma2-2b runs, across the save and the restore:

step NNX Linen
9 (checkpoint written) 5.488 5.498
17 (after restore) 2.280 2.288
19 2.036 2.043

Checklist

Before submitting this PR, please make sure (put X in square brackets):

  • I have performed a self-review of my code. For an optional AI review, add the gemini-review label.
  • I have necessary comments in my code, particularly in hard-to-understand areas.
  • I have run end-to-end tests tests and provided workload links above if applicable.
  • I have made or will make corresponding changes to the doc if needed, including adding new documentation pages to the relevant Table of Contents (toctree directive) as explained in our documentation.

@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@ecnal-cienet
ecnal-cienet force-pushed the feat/checkpointing-load-state-dedup branch 3 times, most recently from 1db2586 to e533a50 Compare July 22, 2026 20:21
@codecov

codecov Bot commented Jul 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.00000% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/maxtext/common/checkpointing.py 97.91% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Comment thread src/maxtext/common/checkpointing.py Outdated
Comment thread src/maxtext/common/checkpointing.py Outdated
The NNX path had two early-return blocks that each duplicated the Linen restore flow, one
for the standard manager and one for the emergency managers. Any change to restore logic
had to be made in three places, and they had already drifted: the NNX grain branch dropped
the iterator element that the Linen branch returns.

pure_nnx saves in the Linen on-disk layout, so both paths restore the same tree. Convert
the NNX abstract to that layout going in, run the existing match statement unchanged, and
reshape what comes back on the way out. Linen behavior is unchanged. The weights-only
branch's if/else folds into `_abstract_params`, which uses nnx.split_state rather than
nnx.split -- the State-native API, without the unused GraphDef.

That leaves the safetensors paths, which had the same duplication problem and three bugs
behind it:

The dynamic safetensors loader received the whole abstract state rather than the weights.
For an NNX state that is fatal before any weight is read: the optimizer's opt_state is
keyed by integer chain indices, and the loader flattens its target with a "." separator,
so the load died in flatten_dict with `TypeError: sequence item 2: expected str instance,
int found`. The loader now receives the weights, the way the weights-only Orbax load
already does, and its result is unwrapped back into the NNX params state.

A weight that no mapping covered came back as the unmaterialized leaf it went in as and
reached the model as an untrained init value. Orbax does not flag this -- a shape conflict
it rejects itself, but a weight the checkpoint simply does not carry it leaves as a
placeholder. The weights-only Orbax load already makes that check; the dynamic load now
makes it too, for both state types.

`checkpoint_conversion_fn` arrives from config as a dotted string but was called directly,
so it raised TypeError for any value that was set. It is now imported, and resolved before
the checkpoint is read so a bad config fails immediately.

Full-state safetensors also handles NNX now: the conversion fn produces MaxText's on-disk
layout, which is what pure_nnx reads, so it goes through the same reshape as every other
NNX restore.
@ecnal-cienet
ecnal-cienet force-pushed the feat/checkpointing-load-state-dedup branch from e533a50 to 82ef7bf Compare July 23, 2026 16:04
@ecnal-cienet
ecnal-cienet marked this pull request as ready for review July 23, 2026 17:29
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants