Skip to content

Commit de5e210

Browse files
angel-coreGoogle-ML-Automation
authored andcommitted
Add checkpoint_context.py to map flat orbax v0 flags to Orbax v1 Context.
This new module provides functions to build Orbax v1 Context, SaveDecisionPolicy, and PreservationPolicy objects based on MaxText's checkpoint configuration flags. It centralizes the logic for configuring Orbax v1, replacing various scattered v0 Orbax constructs previously used in MaxText. Unit tests are included to verify the correct mapping of flags to Orbax v1 options. PiperOrigin-RevId: 952300152
1 parent 629fb1c commit de5e210

2 files changed

Lines changed: 310 additions & 0 deletions

File tree

Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# Copyright 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+
"""Maps MaxText checkpoint config onto the Orbax v1 Context + training policies.
16+
17+
This module is the single place that translates MaxText's flat checkpoint flags
18+
into those objects. It builds configuration only.
19+
"""
20+
import datetime
21+
22+
from orbax.checkpoint import pathways as ocp_pathways
23+
from orbax.checkpoint import v1 as ocp
24+
25+
26+
# v0 PyTreeCheckpointHandler converts `*_concurrent_gb` with GB = 10**9 bytes.
27+
_BYTES_PER_GB = 10**9
28+
29+
# Matches the v0 SingleReplicaArrayHandler broadcast limit (1000 MB) that
30+
# MaxText used when restoring a single replica and broadcasting to the rest.
31+
_SINGLE_REPLICA_BROADCAST_MEMORY_LIMIT_BYTES = 1024 * 1024 * 1000
32+
33+
34+
def build_save_decision_policy(
35+
*,
36+
save_interval_steps: int | None = None,
37+
enable_continuous_checkpointing: bool = False,
38+
enable_autocheckpoint: bool = False,
39+
) -> ocp.training.save_decision_policies.SaveDecisionPolicy:
40+
"""Builds the v1 SaveDecisionPolicy.
41+
42+
- continuous: save as often as possible (async-friendly).
43+
- autocheckpoint: save on preemption OR at the fixed interval (if provided).
44+
- otherwise: save at the fixed interval.
45+
46+
Args:
47+
save_interval_steps: Save every N steps. Optional.
48+
enable_continuous_checkpointing: If true, save as often as possible.
49+
enable_autocheckpoint: If true, save on preemption OR at the fixed interval.
50+
51+
Returns:
52+
A configured ``ocp.training.save_decision_policies.SaveDecisionPolicy``.
53+
"""
54+
policies = ocp.training.save_decision_policies
55+
if enable_continuous_checkpointing:
56+
return policies.ContinuousCheckpointingPolicy() # pyrefly: ignore[bad-return]
57+
if enable_autocheckpoint:
58+
if save_interval_steps is not None:
59+
return policies.AnySavePolicy( # pyrefly: ignore[bad-return]
60+
[
61+
policies.PreemptionCheckpointingPolicy(),
62+
policies.FixedIntervalPolicy(save_interval_steps),
63+
]
64+
)
65+
return policies.PreemptionCheckpointingPolicy() # pyrefly: ignore[bad-return]
66+
if save_interval_steps is None:
67+
raise ValueError("save_interval_steps must be provided for fixed interval checkpointing.")
68+
return policies.FixedIntervalPolicy(interval=save_interval_steps) # pyrefly: ignore[bad-return]
69+
70+
71+
def build_preservation_policy(*, max_to_keep: int) -> ocp.training.preservation_policies.PreservationPolicy:
72+
"""Builds the v1 PreservationPolicy (keep the latest N checkpoints).
73+
74+
Args:
75+
max_to_keep: The maximum number of checkpoints to keep.
76+
77+
Returns:
78+
A configured ``ocp.training.preservation_policies.PreservationPolicy``.
79+
"""
80+
return ocp.training.preservation_policies.LatestN(max_to_keep) # pyrefly: ignore[bad-return]
81+
82+
83+
def build_context(
84+
*,
85+
use_ocdbt: bool = True,
86+
use_zarr3: bool = True,
87+
ocdbt_target_data_file_size_bytes: int | None = None,
88+
checkpoint_storage_concurrent_gb: int | None = None,
89+
enable_continuous_checkpointing: bool = False,
90+
todelete_full_path: str | None = None,
91+
todelete_subdir: str | None = None,
92+
enable_single_replica_ckpt_restoring: bool = False,
93+
replica_axis_index: int = 0,
94+
colocated_python_checkpointing: bool = False,
95+
partial_load: bool = False,
96+
checkpoint_layout: ocp.options.CheckpointLayout | None = None,
97+
) -> ocp.Context:
98+
"""Builds an Orbax v1 ``Context`` from MaxText checkpoint flags.
99+
100+
The returned Context is unfrozen (its options are mutable until it is entered
101+
via ``with ctx:``); callers pass it to ``ocp_v1.training.Checkpointer``, which
102+
applies it to every save/load.
103+
104+
Args:
105+
use_ocdbt: Use OCDBT storage format.
106+
use_zarr3: Use Zarr3 storage format.
107+
ocdbt_target_data_file_size_bytes: Target OCDBT data-file size; also used as
108+
the per-array ``chunk_byte_size`` (matching the v0 ``SaveArgs`` value).
109+
checkpoint_storage_concurrent_gb: Concurrent IO budget in GB; applied to
110+
both write and read as a byte limit (v0 used one value for both).
111+
enable_continuous_checkpointing: If true, set a 60-minute async timeout.
112+
todelete_full_path: GCS soft-delete path.
113+
todelete_subdir: Subdirectory renaming hook for deletions.
114+
enable_single_replica_ckpt_restoring: Restore on one replica and broadcast
115+
to the rest (replaces the v0 ``SingleReplicaArrayHandler``).
116+
replica_axis_index: Mesh axis separating replicas for load-and-broadcast.
117+
colocated_python_checkpointing: Use Pathways colocated-python checkpointing.
118+
partial_load: Restore only the keys present in the abstract tree (the v1
119+
equivalent of v0 ``partial_restore=True``).
120+
checkpoint_layout: On-disk layout (``ORBAX`` or ``SAFETENSORS``) for
121+
loading.
122+
123+
Returns:
124+
A configured, unfrozen ``ocp_v1.Context``.
125+
"""
126+
ctx = ocp.Context()
127+
128+
# Array storage format + file sizing.
129+
ctx.array.saving.use_ocdbt = use_ocdbt
130+
ctx.array.saving.use_zarr3 = use_zarr3
131+
if ocdbt_target_data_file_size_bytes is not None:
132+
ctx.array.saving.ocdbt_target_data_file_size = ocdbt_target_data_file_size_bytes
133+
ctx.array.saving.storage_options.chunk_byte_size = ocdbt_target_data_file_size_bytes
134+
135+
# Concurrent IO budget: v0 GB -> v1 bytes, applied to both directions.
136+
if checkpoint_storage_concurrent_gb is not None:
137+
concurrent_bytes = checkpoint_storage_concurrent_gb * _BYTES_PER_GB
138+
ctx.memory.write_concurrent_bytes = concurrent_bytes
139+
ctx.memory.read_concurrent_bytes = concurrent_bytes
140+
141+
if enable_continuous_checkpointing:
142+
ctx.asynchronous.timeout_secs = int(datetime.timedelta(minutes=60).total_seconds())
143+
144+
if todelete_full_path is not None:
145+
ctx.deletion.gcs_deletion_options.todelete_full_path = todelete_full_path
146+
147+
if todelete_subdir is not None:
148+
raise ValueError("Renaming to subdirectory before deleting (todelete_subdir) is now unsupported by Orbax v1.")
149+
150+
# Single-replica restore (load on one replica, broadcast to the others).
151+
if enable_single_replica_ckpt_restoring:
152+
ctx.array.loading.use_load_and_broadcast = True
153+
ctx.array.loading.load_and_broadcast_options.replica_axis_index = replica_axis_index
154+
ctx.array.loading.load_and_broadcast_options.broadcast_memory_limit_bytes = (
155+
_SINGLE_REPLICA_BROADCAST_MEMORY_LIMIT_BYTES
156+
)
157+
158+
if colocated_python_checkpointing:
159+
ctx.pathways.checkpointing_impl = ocp_pathways.CheckpointingImpl.from_options(
160+
use_colocated_python=True,
161+
)
162+
else:
163+
# v0 only used Pathways handlers when explicitly registered,
164+
# and the persistence handler rejects non-NamedSharding arrays and the
165+
# OCDBT/zarr3 layout MaxText writes. NO_DISPATCHER restores the standard
166+
# controller-side ArrayHandler.
167+
ctx.pathways.checkpointing_impl = ocp_pathways.CheckpointingImpl.NO_DISPATCHER
168+
169+
if partial_load:
170+
ctx.pytree.loading.partial_load = True
171+
172+
if checkpoint_layout is not None:
173+
ctx.checkpoint_layout = checkpoint_layout
174+
175+
return ctx
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Copyright 2023–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+
"""Unit tests for the Orbax v1 Context / policy builders."""
16+
17+
import datetime
18+
import unittest
19+
20+
from absl.testing import absltest
21+
from maxtext.common import checkpoint_context
22+
from orbax.checkpoint import v1 as ocp_v1
23+
24+
25+
_GB = 10**9 # v0 PyTreeCheckpointHandler used decimal GB for *_concurrent_gb
26+
27+
28+
class TestSaveDecisionPolicy(unittest.TestCase):
29+
"""build_save_decision_policy mirrors the v0 manager's selection logic."""
30+
31+
def test_fixed_interval_by_default(self):
32+
policies = ocp_v1.training.save_decision_policies
33+
policy = checkpoint_context.build_save_decision_policy(save_interval_steps=7)
34+
self.assertIsInstance(policy, policies.FixedIntervalPolicy)
35+
36+
def test_continuous(self):
37+
policies = ocp_v1.training.save_decision_policies
38+
policy = checkpoint_context.build_save_decision_policy(save_interval_steps=7, enable_continuous_checkpointing=True)
39+
self.assertIsInstance(policy, policies.ContinuousCheckpointingPolicy)
40+
41+
def test_continuous_without_interval(self):
42+
policies = ocp_v1.training.save_decision_policies
43+
policy = checkpoint_context.build_save_decision_policy(enable_continuous_checkpointing=True)
44+
self.assertIsInstance(policy, policies.ContinuousCheckpointingPolicy)
45+
46+
def test_autocheckpoint_is_any_of_preemption_or_interval(self):
47+
policies = ocp_v1.training.save_decision_policies
48+
policy = checkpoint_context.build_save_decision_policy(save_interval_steps=7, enable_autocheckpoint=True)
49+
self.assertIsInstance(policy, policies.AnySavePolicy)
50+
51+
def test_autocheckpoint_without_interval(self):
52+
policies = ocp_v1.training.save_decision_policies
53+
policy = checkpoint_context.build_save_decision_policy(enable_autocheckpoint=True)
54+
self.assertIsInstance(policy, policies.PreemptionCheckpointingPolicy)
55+
56+
def test_continuous_takes_precedence_over_autocheckpoint(self):
57+
policies = ocp_v1.training.save_decision_policies
58+
policy = checkpoint_context.build_save_decision_policy(
59+
save_interval_steps=7,
60+
enable_continuous_checkpointing=True,
61+
enable_autocheckpoint=True,
62+
)
63+
self.assertIsInstance(policy, policies.ContinuousCheckpointingPolicy)
64+
65+
def test_missing_interval_raises_value_error(self):
66+
with self.assertRaises(ValueError):
67+
checkpoint_context.build_save_decision_policy()
68+
69+
70+
class TestPreservationPolicy(unittest.TestCase):
71+
72+
def test_latest_n(self):
73+
policy = checkpoint_context.build_preservation_policy(max_to_keep=5)
74+
self.assertIsInstance(policy, ocp_v1.training.preservation_policies.LatestN)
75+
76+
77+
class TestBuildContext(unittest.TestCase):
78+
"""build_context maps flat flags onto the right Context fields."""
79+
80+
def test_storage_format_and_file_size(self):
81+
ctx = checkpoint_context.build_context(use_ocdbt=False, use_zarr3=False, ocdbt_target_data_file_size_bytes=2048)
82+
self.assertFalse(ctx.array.saving.use_ocdbt)
83+
self.assertFalse(ctx.array.saving.use_zarr3)
84+
self.assertEqual(ctx.array.saving.ocdbt_target_data_file_size, 2048)
85+
self.assertEqual(ctx.array.saving.storage_options.chunk_byte_size, 2048)
86+
87+
def test_concurrent_gb_to_bytes_both_directions(self):
88+
ctx = checkpoint_context.build_context(checkpoint_storage_concurrent_gb=96)
89+
self.assertEqual(ctx.memory.write_concurrent_bytes, 96 * _GB)
90+
self.assertEqual(ctx.memory.read_concurrent_bytes, 96 * _GB)
91+
92+
def test_continuous_sets_async_timeout(self):
93+
ctx = checkpoint_context.build_context(enable_continuous_checkpointing=True)
94+
self.assertEqual(
95+
ctx.asynchronous.timeout_secs,
96+
int(datetime.timedelta(minutes=60).total_seconds()),
97+
)
98+
99+
def test_todelete_full_path(self):
100+
ctx = checkpoint_context.build_context(todelete_full_path="trash")
101+
self.assertEqual(ctx.deletion.gcs_deletion_options.todelete_full_path, "trash")
102+
103+
def test_todelete_subdir_unsupported(self):
104+
with self.assertRaisesRegex(ValueError, "todelete_subdir"):
105+
checkpoint_context.build_context(todelete_subdir="old")
106+
107+
def test_single_replica_restore_enables_load_and_broadcast(self):
108+
ctx = checkpoint_context.build_context(enable_single_replica_ckpt_restoring=True, replica_axis_index=1)
109+
self.assertTrue(ctx.array.loading.use_load_and_broadcast)
110+
self.assertEqual(ctx.array.loading.load_and_broadcast_options.replica_axis_index, 1)
111+
self.assertEqual(
112+
ctx.array.loading.load_and_broadcast_options.broadcast_memory_limit_bytes,
113+
1024 * 1024 * 1000,
114+
)
115+
116+
def test_single_replica_off_by_default(self):
117+
ctx = checkpoint_context.build_context()
118+
self.assertFalse(ctx.array.loading.use_load_and_broadcast)
119+
120+
def test_colocated_python_sets_pathways_impl(self):
121+
ctx = checkpoint_context.build_context(colocated_python_checkpointing=True)
122+
self.assertIsNotNone(ctx.pathways.checkpointing_impl)
123+
124+
def test_checkpoint_layout(self):
125+
ctx = checkpoint_context.build_context(checkpoint_layout=ocp_v1.options.CheckpointLayout.SAFETENSORS)
126+
self.assertEqual(ctx.checkpoint_layout, ocp_v1.options.CheckpointLayout.SAFETENSORS)
127+
128+
def test_defaults_leave_ocdbt_zarr3_on(self):
129+
ctx = checkpoint_context.build_context()
130+
self.assertTrue(ctx.array.saving.use_ocdbt)
131+
self.assertTrue(ctx.array.saving.use_zarr3)
132+
133+
134+
if __name__ == "__main__":
135+
absltest.main()

0 commit comments

Comments
 (0)