Skip to content

Commit 4e60f00

Browse files
abhishek002002Orbax Authors
authored andcommitted
Adds /checkpoint/experimental/v1/_src/synchronization/multihost_test.py to the presubmit tests.
PiperOrigin-RevId: 930299393
1 parent a91e9a8 commit 4e60f00

5 files changed

Lines changed: 515 additions & 1 deletion

File tree

checkpoint/orbax/checkpoint/_src/futures/synchronization.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,12 @@ def get_current_operation_id(cls) -> str:
6565
"""Returns the current operation id."""
6666
return str(cls._operation_id)
6767

68+
@classmethod
69+
def set_operation_id(cls, operation_id: int) -> None:
70+
"""Synchronizes the operation id counter to allow lagging ranks to catch up."""
71+
cls._operation_id = operation_id
72+
cls._operation_id_counter = itertools.count(operation_id + 1)
73+
6874

6975
class MultihostSynchronizedValue(Generic[_T]):
7076
"""A thread-safe value that is synchronized across all processes."""

checkpoint/orbax/checkpoint/_src/testing/oss/multiprocess_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from absl.testing import absltest
3333
import jax
3434
from jax import config
35+
from orbax.checkpoint._src.futures import synchronization
3536
from orbax.checkpoint._src.multihost import multihost
3637
import portpicker
3738

@@ -318,6 +319,15 @@ def setUp(self):
318319
f"multiprocess_test_ensure_all_processes_arrive_at_test_case_{self._testMethodName}",
319320
10000,
320321
)
322+
sync_key = f"sync_op_id_{self._testMethodName}"
323+
if jax.process_index() == 0:
324+
client.key_value_set(
325+
sync_key,
326+
synchronization.OperationIdGenerator.get_current_operation_id(),
327+
allow_overwrite=True,
328+
)
329+
max_op_id = int(client.blocking_key_value_get(sync_key, 10000))
330+
synchronization.OperationIdGenerator.set_operation_id(max_op_id)
321331

322332
def multiprocess_create_tempdir(self, name: str | None = None) -> str:
323333
"""Creates a temporary directory for the test."""

checkpoint/orbax/checkpoint/_src/testing/oss/run_tests.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@
2222
from absl import app
2323
from absl import flags
2424
from absl import logging
25+
import jax
26+
from orbax.checkpoint._src.futures import synchronization
27+
from orbax.checkpoint._src.multihost import multihost
2528
import pytest
2629
import yaml
2730

@@ -130,7 +133,27 @@ def main(argv: Sequence[str]) -> None:
130133

131134
logging.info('Running test: %s (found from %s)', test_path, test_file_yaml)
132135
try:
133-
exit_code = pytest.main([test_path])
136+
try:
137+
client = multihost.get_jax_distributed_client()
138+
if client is not None:
139+
normalized_name = (
140+
test_file_yaml.replace('/', '_').replace(':', '_')
141+
)
142+
sync_key = f'sync_op_id_file_{normalized_name}'
143+
if jax.process_index() == 0:
144+
client.key_value_set(
145+
sync_key,
146+
synchronization.OperationIdGenerator.get_current_operation_id(),
147+
allow_overwrite=True,
148+
)
149+
max_op_id = int(client.blocking_key_value_get(sync_key, 10000))
150+
synchronization.OperationIdGenerator.set_operation_id(max_op_id)
151+
except Exception as sync_e: # pylint: disable=broad-exception-caught
152+
logging.warning(
153+
'Could not synchronize OperationIdGenerator for file: %s', sync_e
154+
)
155+
156+
exit_code = pytest.main(['--import-mode=importlib', test_path])
134157
if exit_code == 0:
135158
results[test_file_yaml] = 'PASSED'
136159
logging.info('%s: PASSED', test_path)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright 2026 The Orbax Authors.
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+
# http://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+
import time
16+
17+
from absl import flags
18+
from absl.testing import flagsaver
19+
from absl.testing import parameterized
20+
from etils import epath
21+
import jax
22+
import numpy as np
23+
from orbax.checkpoint import args
24+
from orbax.checkpoint import checkpoint_manager
25+
from orbax.checkpoint import checkpoint_utils
26+
from orbax.checkpoint import test_utils
27+
from orbax.checkpoint import utils
28+
from orbax.checkpoint._src.handlers import handler_registration
29+
from orbax.checkpoint._src.handlers import pytree_checkpoint_handler
30+
from orbax.checkpoint._src.metadata import array_metadata_store as array_metadata_store_lib
31+
from orbax.checkpoint._src.multihost import multihost
32+
from orbax.checkpoint._src.serialization import type_handler_registry
33+
from orbax.checkpoint._src.serialization import type_handlers
34+
from orbax.checkpoint._src.testing import multiprocess_test
35+
36+
37+
FLAGS = flags.FLAGS
38+
PyTreeCheckpointHandler = pytree_checkpoint_handler.PyTreeCheckpointHandler
39+
CheckpointManager = checkpoint_manager.CheckpointManager
40+
CheckpointManagerOptions = checkpoint_manager.CheckpointManagerOptions
41+
42+
43+
@test_utils.barrier_compatible_test
44+
class CheckpointManagerSliceTest(
45+
parameterized.TestCase, multiprocess_test.MultiProcessTest
46+
):
47+
"""Structure allows test to run as subclasses, not base class."""
48+
49+
def setUp(self):
50+
super().setUp()
51+
52+
if not multihost.is_runtime_to_distributed_ids_initialized():
53+
multihost.initialize_runtime_to_distributed_ids()
54+
55+
self.assertEqual(jax.device_count(), 8)
56+
self.assertEqual(jax.process_count(), 4)
57+
self.assertEqual(jax.local_device_count(), 2)
58+
59+
self.directory = epath.Path(
60+
self.multiprocess_create_tempdir(name='checkpoint_manager_slice_test')
61+
)
62+
test_utils.set_tensorstore_driver_for_test()
63+
64+
test_utils.sync_global_processes(
65+
'CheckpointManagerSliceTest:setup_complete'
66+
)
67+
68+
def tearDown(self):
69+
test_utils.sync_global_processes(
70+
'CheckpointManagerSliceTest:tests_complete'
71+
)
72+
super().tearDown()
73+
74+
def wait_if_async(self, manager):
75+
manager.wait_until_finished() # no-op if no async checkpointers.
76+
77+
@parameterized.product(
78+
enable_async_checkpointing=(False, True),
79+
array_metadata_store=(None, array_metadata_store_lib.Store()),
80+
)
81+
def test_slice(
82+
self,
83+
enable_async_checkpointing: bool,
84+
array_metadata_store: array_metadata_store_lib.Store | None,
85+
):
86+
"""Test slice."""
87+
self.enter_context(
88+
flagsaver.flagsaver(experimental_orbax_use_distributed_process_id=True)
89+
)
90+
global_mesh = test_utils.get_fake_global_mesh_for_slices([{0, 1}, {2, 3}])
91+
92+
mesh_axes = jax.sharding.PartitionSpec('data')
93+
arrays = [
94+
test_utils.create_sharded_array(arr, global_mesh, mesh_axes)
95+
for arr in [np.arange(8), np.arange(16)]
96+
]
97+
assert len(global_mesh.devices[0]) == 4
98+
assert jax.process_count() == 4
99+
active_processes = {0, 1}
100+
primary_host = 0
101+
if multihost.process_index() in active_processes:
102+
single_slice_arrays = test_utils.select_single_replica(
103+
arrays, global_mesh
104+
)
105+
options = CheckpointManagerOptions(
106+
create=False,
107+
enable_async_checkpointing=enable_async_checkpointing,
108+
multiprocessing_options=checkpoint_manager.MultiprocessingOptions(
109+
primary_host=primary_host,
110+
active_processes=active_processes,
111+
),
112+
)
113+
registry = type_handler_registry.create_type_handler_registry(
114+
(
115+
jax.Array,
116+
type_handlers.ArrayHandler(
117+
primary_host=None,
118+
replica_id=None,
119+
use_replica_parallel=False,
120+
array_metadata_store=array_metadata_store,
121+
),
122+
),
123+
)
124+
handler = PyTreeCheckpointHandler(
125+
multiprocessing_options=options.multiprocessing_options,
126+
type_handler_registry=registry,
127+
)
128+
registry = handler_registration.DefaultCheckpointHandlerRegistry()
129+
registry.add(None, args.PyTreeSave, handler)
130+
registry.add(None, args.PyTreeRestore, handler)
131+
with CheckpointManager(
132+
self.directory,
133+
options=options,
134+
handler_registry=registry,
135+
) as manager:
136+
self.assertTrue(manager.save(0, args=args.PyTreeSave(arrays)))
137+
time.sleep(10)
138+
self.wait_if_async(manager)
139+
abstract_target = jax.tree.map(
140+
utils.to_shape_dtype_struct, single_slice_arrays
141+
)
142+
restore_args = checkpoint_utils.construct_restore_args(abstract_target)
143+
restored = manager.restore(
144+
0, args=args.PyTreeRestore(restore_args=restore_args)
145+
)
146+
test_utils.assert_tree_equal(self, single_slice_arrays, restored)
147+
148+
149+
if __name__ == '__main__':
150+
multiprocess_test.main()

0 commit comments

Comments
 (0)