Skip to content

Commit 0e7124b

Browse files
committed
[NNX] Emergency checkpoint: build the manager with a Linen-layout abstract
pure_nnx runs save in the Linen on-disk layout (params/opt_state/step), but the emergency checkpoint manager was constructed with the NNX-native abstract state (model/optimizer + rngs/dropout). The manager bakes that abstract in and restores against it, so on resume Orbax compared a Linen on-disk tree against an NNX target and raised 'User-provided restore item and on-disk value metadata tree structures do not match', failing the EMC resume-from-GCS job. The prior restore-side fix was ineffective because the manager ignores the restore-time item and uses its construction-time abstract. Convert the abstract to the Linen layout in create_orbax_emergency_checkpoint_manager so it matches what maybe_save_checkpoint writes; the restore path reshapes it back to NNX. Adds a mock-based guard (asserts the manager gets a Linen abstract) plus an end-to-end save/restore round trip through the real emergency manager.
1 parent b9aee47 commit 0e7124b

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

src/maxtext/common/checkpointing.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,13 @@ def create_orbax_emergency_checkpoint_manager(
498498

499499
persistent_p = gcs_utils.mkdir_and_check_permissions(persistent_checkpoint_dir)
500500

501+
# pure_nnx passes an NNX-native abstract (model/optimizer), but pure_nnx runs save in the
502+
# Linen on-disk layout (params/opt_state/step). The emergency manager bakes this abstract in
503+
# and restores against it, so it must match the on-disk layout -- convert it to Linen here,
504+
# matching what maybe_save_checkpoint writes. The restore path reshapes it back to NNX.
505+
if isinstance(abstract_state, nnx.State):
506+
abstract_state = train_state_nnx.to_linen_checkpoint_dict(abstract_state.to_pure_dict())
507+
501508
manager = EmergencyCheckpointManager(
502509
local_checkpoint_dir,
503510
persistent_p,
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
# Copyright 2025-2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Emergency-checkpoint tests for the pure_nnx layout.
16+
17+
Regression coverage for the EMC restore failure where the emergency manager was
18+
constructed with an NNX-native abstract (model/optimizer) while pure_nnx runs save
19+
in the Linen on-disk layout (params/opt_state/step). The manager bakes its abstract
20+
in and restores against it, so the two must agree -- otherwise Orbax raises
21+
"User-provided restore item and on-disk value metadata tree structures do not match".
22+
"""
23+
24+
import os
25+
import tempfile
26+
import unittest
27+
from types import SimpleNamespace
28+
from unittest import mock
29+
30+
from etils import epath
31+
from flax import nnx
32+
import jax
33+
import jax.numpy as jnp
34+
from maxtext.common import checkpointing
35+
from maxtext.common import train_state_nnx
36+
import optax
37+
38+
39+
class _Model(nnx.Module):
40+
"""Linear + dropout, so the NNX state carries model, optimizer, and rngs."""
41+
42+
def __init__(self, rngs: nnx.Rngs):
43+
self.linear = nnx.Linear(2, 3, rngs=rngs)
44+
self.dropout = nnx.Dropout(rate=0.5, rngs=rngs)
45+
46+
def __call__(self, x, deterministic=False):
47+
return self.dropout(self.linear(x), deterministic=deterministic)
48+
49+
50+
_TX = optax.adam(1e-3)
51+
52+
53+
def _nnx_abstract_state():
54+
"""An abstract NNX state (model/optimizer), the shape pure_nnx hands the manager."""
55+
56+
def make():
57+
model = _Model(nnx.Rngs(0))
58+
return nnx.state(train_state_nnx.TrainStateNNX(model, nnx.Optimizer(model, _TX, wrt=nnx.Param)))
59+
60+
return nnx.eval_shape(make)
61+
62+
63+
class TestEmergencyManagerAbstractLayout(unittest.TestCase):
64+
"""The regression guard: the emergency manager must be built with a Linen abstract.
65+
66+
Fast and environment-independent -- mocks the Orbax manager, so it catches a
67+
reintroduction of the bug in CI without needing a real multi-host EMC run.
68+
"""
69+
70+
def _create(self, abstract_state):
71+
"""Runs create_orbax_emergency_checkpoint_manager with the Orbax manager mocked out."""
72+
mesh = jax.sharding.Mesh(jax.devices(), ("x",))
73+
with (
74+
mock.patch.object(checkpointing, "EmergencyCheckpointManager") as manager_cls,
75+
mock.patch.object(checkpointing.gcs_utils, "mkdir_and_check_permissions", side_effect=lambda p: epath.Path(p)),
76+
):
77+
with tempfile.TemporaryDirectory() as d:
78+
checkpointing.create_orbax_emergency_checkpoint_manager(
79+
os.path.join(d, "local"),
80+
os.path.join(d, "persist"),
81+
mesh,
82+
abstract_state,
83+
local_save_interval_steps=1,
84+
persistent_save_interval_steps=1,
85+
)
86+
return manager_cls.call_args.kwargs["abstract_state"]
87+
88+
def test_nnx_abstract_is_converted_to_linen_layout(self):
89+
"""An NNX-native abstract (model/optimizer) is reshaped to the Linen on-disk layout."""
90+
passed = self._create(_nnx_abstract_state())
91+
self.assertNotIsInstance(passed, nnx.State)
92+
# Linen on-disk keys, not the NNX model/optimizer roots that caused the mismatch.
93+
self.assertEqual(set(passed.keys()), {"params", "opt_state", "step"})
94+
self.assertNotIn("model", passed)
95+
self.assertNotIn("optimizer", passed)
96+
97+
def test_non_nnx_abstract_is_passed_through_unchanged(self):
98+
"""A Linen TrainState (not an nnx.State) is forwarded as-is, not double-converted."""
99+
sentinel = SimpleNamespace(params={"a": 1}, opt_state=(), step=0) # stand-in Linen state
100+
passed = self._create(sentinel)
101+
self.assertIs(passed, sentinel)
102+
103+
104+
class TestEmergencySaveRestoreRoundTrip(unittest.TestCase):
105+
"""End-to-end: save (Linen) then restore through the real emergency manager.
106+
107+
Reproduces the production DAG. Skips where a single-host EMC manager can't be
108+
constructed (some CI backends); runs fully on TPU/GPU/CPU that support it.
109+
"""
110+
111+
def setUp(self):
112+
self._dir = tempfile.mkdtemp()
113+
114+
def _config(self):
115+
return SimpleNamespace(
116+
pure_nnx=True,
117+
enable_diloco=False,
118+
enable_checkpointing=True,
119+
enable_continuous_checkpointing=False,
120+
enable_emergency_checkpoint=True,
121+
enable_autocheckpoint=False,
122+
enable_multi_tier_checkpointing=False,
123+
checkpoint_period=1,
124+
local_checkpoint_period=1,
125+
async_checkpointing=False,
126+
dataset_type="tfds",
127+
lora=None,
128+
checkpoint_storage_target_data_file_size_bytes=checkpointing.DEFAULT_OCDBT_TARGET_DATA_FILE_SIZE,
129+
elastic_enabled=False,
130+
)
131+
132+
def test_emergency_round_trip_reshapes_back_to_nnx(self):
133+
mesh = jax.sharding.Mesh(jax.devices(), ("x",))
134+
sharding = jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
135+
abstract = jax.tree.map(
136+
lambda x: jax.ShapeDtypeStruct(x.shape, x.dtype, sharding=sharding) if hasattr(x, "shape") else x,
137+
_nnx_abstract_state(),
138+
)
139+
140+
try:
141+
manager = checkpointing.create_orbax_emergency_checkpoint_manager(
142+
os.path.join(self._dir, "local"),
143+
os.path.join(self._dir, "persist"),
144+
mesh,
145+
abstract,
146+
local_save_interval_steps=1,
147+
persistent_save_interval_steps=1,
148+
)
149+
except Exception as e: # pylint: disable=broad-except
150+
raise unittest.SkipTest(f"EMC manager unavailable in this environment: {e}")
151+
152+
# Train one step so weights/optimizer/step are non-trivial, then save (Linen layout).
153+
model = _Model(nnx.Rngs(0))
154+
state = train_state_nnx.TrainStateNNX(model, nnx.Optimizer(model, _TX, wrt=nnx.Param))
155+
grads = nnx.grad(lambda m: jnp.mean(m(jnp.ones((4, 2)), deterministic=False) ** 2))(state.model)
156+
state.apply_gradients(grads)
157+
saved_kernel = jnp.asarray(nnx.state(state).to_pure_dict()["model"]["linear"]["kernel"])
158+
159+
checkpointing.maybe_save_checkpoint(manager, nnx.state(state), self._config(), data_iterator=None, step=1)
160+
manager.wait_until_finished()
161+
162+
# Restore: must not raise the structural-mismatch ValueError, and must come back as NNX.
163+
full, _ = checkpointing.load_state_if_possible(
164+
manager,
165+
data_iterator=None,
166+
load_parameters_from_path="",
167+
load_full_state_from_path="",
168+
checkpoint_storage_concurrent_gb=8,
169+
abstract_unboxed_pre_state=abstract,
170+
dataset_type="tfds",
171+
maxtext_config=self._config(),
172+
)
173+
self.assertIn("model", full)
174+
self.assertIn("optimizer", full)
175+
self.assertNotIn("params", full)
176+
self.assertTrue(jnp.allclose(full["model"]["linear"]["kernel"], saved_kernel))
177+
178+
179+
if __name__ == "__main__":
180+
unittest.main()

0 commit comments

Comments
 (0)