Skip to content

Commit ecf666f

Browse files
Merge pull request #4322 from AI-Hypercomputer:sujinesh/fix-emergency-nnx-restore
PiperOrigin-RevId: 941369401
2 parents ec8b0e0 + 0da53c3 commit ecf666f

2 files changed

Lines changed: 103 additions & 55 deletions

File tree

src/maxtext/common/checkpointing.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,27 @@ def _load_linen_checkpoint_into_nnx(
245245
return _populate_pure_dict_from_partial(nnx_abstract_pure, partial_nnx)
246246

247247

248+
def _restore_emergency_linen_checkpoint_into_nnx(
249+
checkpoint_manager,
250+
step,
251+
abstract_nnx_state,
252+
map_to_pspec,
253+
):
254+
"""Restores an emergency Linen-layout checkpoint into an NNX state."""
255+
max_logging.log(f"Restoring emergency Linen-layout checkpoint into NNX state at step {step}")
256+
nnx_abstract_pure = abstract_nnx_state.to_pure_dict()
257+
linen_abstract = train_state_nnx.to_linen_checkpoint_dict(nnx_abstract_pure)
258+
restore_args = jax.tree_util.tree_map(map_to_pspec, linen_abstract)
259+
checkpoint_args = ocp.args.PyTreeRestore(
260+
item=linen_abstract,
261+
restore_args=restore_args,
262+
partial_restore=True,
263+
)
264+
restored = checkpoint_manager.restore(step, args=Composite(state=checkpoint_args)).state
265+
partial_nnx = train_state_nnx.from_linen_checkpoint_dict(restored)
266+
return _populate_pure_dict_from_partial(nnx_abstract_pure, partial_nnx)
267+
268+
248269
def _rebuild_nnx_with_values(abstract_nnx_state, concrete_weights):
249270
"""Fills each Variable in `abstract_nnx_state` with the matching restored array."""
250271
leaves, treedef = jax.tree_util.tree_flatten(abstract_nnx_state, is_leaf=lambda x: isinstance(x, nnx.Variable))
@@ -850,6 +871,24 @@ def map_to_pspec(data):
850871
_assert_no_shaped_dtype_struct(restored_nnx)
851872
return ({"items": restored_nnx}, None)
852873

874+
if isinstance(abstract_unboxed_pre_state, nnx.State) and isinstance(
875+
checkpoint_manager,
876+
(EmergencyCheckpointManager, EmergencyReplicatorCheckpointManager),
877+
):
878+
checkpoint_path = str(checkpoint_manager.directory / str(step))
879+
with handle_checkpoint_mismatch("restore emergency NNX checkpoint", checkpoint_path):
880+
restored = _restore_emergency_linen_checkpoint_into_nnx(
881+
checkpoint_manager,
882+
step,
883+
abstract_unboxed_pre_state,
884+
map_to_pspec,
885+
)
886+
_assert_no_shaped_dtype_struct(restored)
887+
return (
888+
restored,
889+
None,
890+
)
891+
853892
# Convert nnx.State to pure dict to match how checkpoints are saved for NNX
854893
restore_target = abstract_unboxed_pre_state
855894
if isinstance(abstract_unboxed_pre_state, nnx.State):

tests/unit/checkpointing_nnx_load_test.py

Lines changed: 64 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,56 @@ def _abstract_nnx_state():
4646
class TestLoadStateIfPossibleNNX(unittest.TestCase):
4747
"""Cover the NNX branches in load_state_if_possible."""
4848

49+
def test_emergency_linen_restore_converts_back_to_nnx(self):
50+
kernel_abstract = jax.ShapeDtypeStruct((2, 1), jnp.float32)
51+
step_abstract = jax.ShapeDtypeStruct((), jnp.uint32)
52+
abstract_nnx_pure = {
53+
"model": {
54+
"linear": {"kernel": kernel_abstract},
55+
"dropout": {"rngs": {"params": {"count": step_abstract}}},
56+
},
57+
"optimizer": {"step": step_abstract},
58+
}
59+
abstract_nnx_state = mock.Mock()
60+
abstract_nnx_state.to_pure_dict.return_value = abstract_nnx_pure
61+
restored_linen = {
62+
"params": {"params": {"linear": {"kernel": jnp.ones((2, 1))}}},
63+
"step": jnp.asarray(7, dtype=jnp.int32),
64+
}
65+
checkpoint_manager = mock.Mock()
66+
checkpoint_manager.restore.return_value = mock.Mock(state=restored_linen)
67+
68+
restored = checkpointing._restore_emergency_linen_checkpoint_into_nnx( # pylint: disable=protected-access
69+
checkpoint_manager,
70+
14,
71+
abstract_nnx_state,
72+
lambda leaf: ocp.type_handlers.ArrayRestoreArgs(
73+
global_shape=leaf.shape,
74+
dtype=leaf.dtype,
75+
),
76+
)
77+
78+
checkpoint_manager.restore.assert_called_once()
79+
restore_args = checkpoint_manager.restore.call_args.kwargs["args"].state
80+
self.assertEqual(
81+
set(restore_args.item.keys()),
82+
{"params", "step"},
83+
)
84+
self.assertNotIn("model", restore_args.item)
85+
self.assertNotIn("optimizer", restore_args.item)
86+
self.assertTrue(restore_args.partial_restore)
87+
self.assertIn("model", restored)
88+
self.assertIn("optimizer", restored)
89+
self.assertNotIn("params", restored)
90+
self.assertNotIn("opt_state", restored)
91+
self.assertTrue(bool(jnp.array_equal(restored["model"]["linear"]["kernel"], jnp.ones((2, 1)))))
92+
self.assertEqual(restored["optimizer"]["step"].dtype, jnp.uint32)
93+
self.assertEqual(int(restored["optimizer"]["step"]), 7)
94+
self.assertEqual(
95+
restored["model"]["dropout"]["rngs"]["params"]["count"].shape,
96+
(),
97+
)
98+
4999
def test_load_parameters_from_path_splits_nnx_state_for_param_view(self):
50100
"""When abstract_unboxed_pre_state is an nnx.State, the function must call
51101
nnx.split(model, nnx.Param, ...) to get the params and forward them to load_params_from_path."""
@@ -126,8 +176,7 @@ def test_load_state_if_possible_wraps_load_params_mismatch_exception(self):
126176
str(ctx.exception),
127177
)
128178
self.assertIn(
129-
"This is often caused by a mismatch in the 'scan_layers'"
130-
" configuration",
179+
"This is often caused by a mismatch in the 'scan_layers'" " configuration",
131180
str(ctx.exception),
132181
)
133182

@@ -156,69 +205,33 @@ class TestCheckpointMismatchHandling(unittest.TestCase):
156205
def test_is_structural_or_shape_mismatch(self):
157206
"""Verifies that is_structural_or_shape_mismatch matches only shape/tree mismatches in ValueError/TypeError."""
158207
# Matches
159-
self.assertTrue(
160-
checkpointing.is_structural_or_shape_mismatch(
161-
ValueError("PyTree structure mismatch")
162-
)
163-
)
164-
self.assertTrue(
165-
checkpointing.is_structural_or_shape_mismatch(
166-
TypeError("shape mismatch in leaf")
167-
)
168-
)
169-
self.assertTrue(
170-
checkpointing.is_structural_or_shape_mismatch(
171-
ValueError("tree paths matched 143/145")
172-
)
173-
)
174-
self.assertTrue(
175-
checkpointing.is_structural_or_shape_mismatch(
176-
ValueError("invalid type shapedtypestruct")
177-
)
178-
)
208+
self.assertTrue(checkpointing.is_structural_or_shape_mismatch(ValueError("PyTree structure mismatch")))
209+
self.assertTrue(checkpointing.is_structural_or_shape_mismatch(TypeError("shape mismatch in leaf")))
210+
self.assertTrue(checkpointing.is_structural_or_shape_mismatch(ValueError("tree paths matched 143/145")))
211+
self.assertTrue(checkpointing.is_structural_or_shape_mismatch(ValueError("invalid type shapedtypestruct")))
179212

180213
# Does not match
181-
self.assertFalse(
182-
checkpointing.is_structural_or_shape_mismatch(
183-
ValueError("checkpoint directory does not exist")
184-
)
185-
)
186-
self.assertFalse(
187-
checkpointing.is_structural_or_shape_mismatch(
188-
FileNotFoundError("file not found: checkpoint")
189-
)
190-
)
191-
self.assertFalse(
192-
checkpointing.is_structural_or_shape_mismatch(
193-
RuntimeError("something went wrong")
194-
)
195-
)
214+
self.assertFalse(checkpointing.is_structural_or_shape_mismatch(ValueError("checkpoint directory does not exist")))
215+
self.assertFalse(checkpointing.is_structural_or_shape_mismatch(FileNotFoundError("file not found: checkpoint")))
216+
self.assertFalse(checkpointing.is_structural_or_shape_mismatch(RuntimeError("something went wrong")))
196217

197218
def test_handle_checkpoint_mismatch_intercepts_matching_exceptions(self):
198219
"""Verifies that handle_checkpoint_mismatch intercepts and wraps structural errors."""
199220
with self.assertRaises(ValueError) as ctx:
200-
with checkpointing.handle_checkpoint_mismatch(
201-
"load parameters", "gs://bucket/params"
202-
):
221+
with checkpointing.handle_checkpoint_mismatch("load parameters", "gs://bucket/params"):
203222
raise ValueError("PyTree structure mismatch")
204223

205-
self.assertIn(
206-
"Failed to load parameters from gs://bucket/params.", str(ctx.exception)
207-
)
224+
self.assertIn("Failed to load parameters from gs://bucket/params.", str(ctx.exception))
208225
self.assertIn(
209226
"This is often caused by a mismatch in the 'scan_layers' configuration",
210227
str(ctx.exception),
211228
)
212-
self.assertIn(
213-
"Original error: PyTree structure mismatch", str(ctx.exception)
214-
)
229+
self.assertIn("Original error: PyTree structure mismatch", str(ctx.exception))
215230

216231
def test_handle_checkpoint_mismatch_re_raises_non_matching_exceptions(self):
217232
"""Verifies that handle_checkpoint_mismatch does not intercept non-structural errors."""
218233
with self.assertRaises(FileNotFoundError):
219-
with checkpointing.handle_checkpoint_mismatch(
220-
"load parameters", "gs://bucket/params"
221-
):
234+
with checkpointing.handle_checkpoint_mismatch("load parameters", "gs://bucket/params"):
222235
raise FileNotFoundError("file not found: checkpoint")
223236

224237

@@ -248,12 +261,8 @@ def test_linen_layout_params_restore_into_nnx_state(self):
248261

249262
self.assertIsInstance(restored, nnx.State)
250263
pure = restored.to_pure_dict()
251-
self.assertTrue(
252-
jnp.array_equal(pure["linear"]["kernel"], weights["linear"]["kernel"])
253-
)
254-
self.assertTrue(
255-
jnp.array_equal(pure["linear"]["bias"], weights["linear"]["bias"])
256-
)
264+
self.assertTrue(jnp.array_equal(pure["linear"]["kernel"], weights["linear"]["kernel"]))
265+
self.assertTrue(jnp.array_equal(pure["linear"]["bias"], weights["linear"]["bias"]))
257266

258267

259268
if __name__ == "__main__":

0 commit comments

Comments
 (0)