Skip to content

Commit e47de14

Browse files
angel-coreOrbax Authors
authored andcommitted
Fork CloudPathwaysArrayHandler Orbax from pathwaysutils.
PiperOrigin-RevId: 930332434
1 parent a91e9a8 commit e47de14

7 files changed

Lines changed: 703 additions & 11 deletions

File tree

checkpoint/orbax/checkpoint/_src/multihost/multihost.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ def coordination_timeout() -> int:
6161

6262

6363

64+
def is_proxy_pathways_backend() -> bool:
65+
return jax.devices()[0].client.runtime_type == 'proxy/pathways'
66+
67+
6468
def is_pathways_backend() -> bool:
6569
# Pathways is single-host.
6670
return (
@@ -411,6 +415,10 @@ def _maybe_log_reached_preemption(
411415

412416
def reached_preemption(step: int) -> bool:
413417
"""Returns True if a preemption sync point has been reached."""
418+
# TODO(b/403305420) : disable checkpoing saving when preemption for MLCR.
419+
# Remove this once the bug is closed.
420+
if is_proxy_pathways_backend():
421+
return False
414422

415423
preemption_sync_point_reached = multihost_utils.reached_preemption_sync_point(
416424
step
Lines changed: 292 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,292 @@
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+
# Copyright 2026 Google LLC
16+
#
17+
# Licensed under the Apache License, Version 2.0 (the "License");
18+
# you may not use this file except in compliance with the License.
19+
# You may obtain a copy of the License at
20+
#
21+
# https://www.apache.org/licenses/LICENSE-2.0
22+
#
23+
# Unless required by applicable law or agreed to in writing, software
24+
# distributed under the License is distributed on an "AS IS" BASIS,
25+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
26+
# See the License for the specific language governing permissions and
27+
# limitations under the License.
28+
"""CloudPathwaysArrayHandler for Pathways on Cloud with Persistence API."""
29+
30+
from __future__ import annotations
31+
32+
import collections
33+
from collections.abc import Coroutine, Sequence
34+
import concurrent.futures
35+
import datetime
36+
import functools
37+
from typing import Any, cast
38+
39+
from absl import logging
40+
import jax
41+
from orbax.checkpoint._src.futures import future
42+
from orbax.checkpoint._src.metadata import array_metadata as array_metadata_lib
43+
from orbax.checkpoint._src.metadata import array_metadata_store as array_metadata_store_lib
44+
from orbax.checkpoint._src.serialization import cloud_pathways_helper
45+
from orbax.checkpoint._src.serialization import jax_array_handlers
46+
from orbax.checkpoint._src.serialization import jax_array_restore_args
47+
from orbax.checkpoint._src.serialization import types
48+
49+
50+
_logger = logging
51+
ArrayMetadata = array_metadata_lib.ArrayMetadata
52+
53+
54+
def extract_parent_dir_and_name(
55+
infos: Sequence[types.ParamInfo],
56+
) -> tuple[Sequence[str], Sequence[str]]:
57+
"""Extracts names and locations from ParamInfos."""
58+
parent_dirs = [str(info.parent_dir) for info in infos]
59+
names = [str(info.name) for info in infos]
60+
return parent_dirs, names
61+
62+
63+
class CloudPathwaysArrayHandler(jax_array_handlers.ArrayHandler):
64+
"""A TypeHandler for array types when using Pathways."""
65+
66+
def __init__(
67+
self,
68+
timeout: datetime.timedelta | None = None,
69+
use_ocdbt: bool = False,
70+
array_metadata_store: array_metadata_store_lib.Store | None = None,
71+
**kwargs,
72+
):
73+
"""Orbax array handler for Pathways on Cloud with Persistence API.
74+
75+
Args:
76+
timeout: Duration indicating the timeout for reading and writing arrays.
77+
Default is 1 hour.
78+
use_ocdbt: allows using Tensorstore OCDBT driver.
79+
array_metadata_store: An optional store for writing and reading array
80+
metadata. Only required for saving new-style jax random keys.
81+
**kwargs: Other keyword arguments.
82+
"""
83+
del kwargs
84+
if timeout is None:
85+
timeout = datetime.timedelta(hours=1)
86+
self.timeout = timeout
87+
88+
if use_ocdbt:
89+
raise ValueError("OCDBT not supported for Pathways.")
90+
super().__init__(array_metadata_store=array_metadata_store)
91+
92+
async def _background_serialize(
93+
self,
94+
futures_results: Sequence[concurrent.futures.Future[None]],
95+
metadata_coroutine: Coroutine[Any, Any, None] | None = None,
96+
) -> None:
97+
if metadata_coroutine:
98+
await metadata_coroutine
99+
100+
for future_result in futures_results:
101+
future_result.result()
102+
103+
def _wait_for_directory_creation_signals(self):
104+
async def _no_op():
105+
pass
106+
107+
# Wait for directory creation signals to be set.
108+
future.CommitFutureAwaitingContractedSignals(_no_op()).result()
109+
110+
async def serialize(
111+
self,
112+
values: Sequence[jax.Array],
113+
infos: Sequence[types.ParamInfo],
114+
args: Sequence[types.SaveArgs] | None = None,
115+
) -> Sequence[future.Future]:
116+
"""Uses Pathways Persistence API to serialize a jax array."""
117+
types.check_input_arguments(values, infos, args)
118+
119+
if any([arg.dtype is not None for arg in args]):
120+
raise ValueError("Casting during save not supported for Pathways.")
121+
122+
array_metadatas = []
123+
any_random_key = False
124+
arrays = []
125+
for v, info, arg in zip(values, infos, args):
126+
ext_metadata = None
127+
if jax.dtypes.issubdtype(v.dtype, jax.dtypes.prng_key):
128+
any_random_key = True
129+
impl = str(jax.random.key_impl(v))
130+
v = jax.random.key_data(v)
131+
ext_metadata = {array_metadata_lib.RANDOM_KEY_IMPL: impl}
132+
133+
array_metadatas.append(
134+
ArrayMetadata(
135+
param_name=info.name,
136+
shape=v.shape,
137+
dtype=(arg.dtype if arg is not None else v.dtype),
138+
write_shape=getattr(v, "local_shape", v.shape),
139+
chunk_shape=getattr(v, "local_shape", v.shape),
140+
use_ocdbt=False,
141+
use_zarr3=False,
142+
ext_metadata=ext_metadata,
143+
)
144+
)
145+
arrays.append(v)
146+
147+
if any_random_key and self._array_metadata_store is None:
148+
raise ValueError(
149+
"Array metadata store is not set with a checkpoint that requires"
150+
f" it. Array metadata: {array_metadatas}"
151+
)
152+
153+
metadata_coroutine = None
154+
if self._array_metadata_store is not None:
155+
metadata_coroutine = self._array_metadata_store.write(
156+
checkpoint_dir=infos[0].parent_dir,
157+
array_metadatas=array_metadatas,
158+
process_index=0,
159+
)
160+
161+
self._wait_for_directory_creation_signals()
162+
locations, names = extract_parent_dir_and_name(infos)
163+
f = functools.partial(
164+
cloud_pathways_helper.write_one_array, timeout=self.timeout
165+
)
166+
futures_results = list(map(f, locations, names, arrays))
167+
168+
return [
169+
future.CommitFutureAwaitingContractedSignals(
170+
self._background_serialize(futures_results, metadata_coroutine),
171+
name="cloud_pathways_array_handler",
172+
)
173+
]
174+
175+
async def deserialize(
176+
self,
177+
infos: Sequence[types.ParamInfo],
178+
args: Sequence[types.RestoreArgs] | None = None,
179+
) -> Sequence[jax.Array]:
180+
"""Uses Pathways Persistence API to deserialize a jax array."""
181+
if args is None:
182+
raise ValueError("Must provide ArrayRestoreArgs to restore as jax.Array.")
183+
types.check_input_arguments(infos, args)
184+
185+
global_meshes = []
186+
mesh_axes = []
187+
global_shapes = []
188+
dtypes = []
189+
shardings = []
190+
191+
should_open_metadata = False
192+
for arg in args:
193+
if not isinstance(arg, jax_array_restore_args.ArrayRestoreArgs):
194+
raise ValueError(
195+
"To restore jax.Array, provide ArrayRestoreArgs; found"
196+
f" {type(arg).__name__}"
197+
)
198+
arg = cast(jax_array_restore_args.ArrayRestoreArgs, arg)
199+
if arg.sharding is None and (arg.mesh is None or arg.mesh_axes is None):
200+
raise ValueError(
201+
"Sharding of jax.Array cannot be None. Provide `mesh`"
202+
" and `mesh_axes` OR `sharding`."
203+
)
204+
if arg.sharding is None:
205+
global_meshes.append(arg.mesh)
206+
mesh_axes.append(arg.mesh_axes)
207+
shardings.append(
208+
jax.sharding.NamedSharding(mesh=arg.mesh, spec=arg.mesh_axes)
209+
)
210+
else:
211+
if not isinstance(arg.sharding, jax.sharding.NamedSharding):
212+
raise ValueError("Pathways only supports jax.sharding.NamedSharding.")
213+
sharding = cast(jax.sharding.NamedSharding, arg.sharding)
214+
global_meshes.append(sharding.mesh)
215+
mesh_axes.append(sharding.spec)
216+
shardings.append(sharding)
217+
if arg.global_shape is None or arg.dtype is None:
218+
_logger.warning(
219+
"Shape or dtype not provided for restoration. Provide these"
220+
" properties for improved performance."
221+
)
222+
should_open_metadata = True
223+
global_shapes.append(arg.global_shape)
224+
dtypes.append(arg.dtype)
225+
226+
if should_open_metadata:
227+
metadatas = await self.metadata(infos)
228+
global_shapes = [
229+
m.shape if s is None else s for m, s in zip(metadatas, global_shapes)
230+
]
231+
dtypes = [m.dtype if d is None else d for m, d in zip(metadatas, dtypes)]
232+
233+
array_metadatas_cache = {}
234+
if self._array_metadata_store is not None:
235+
if array_metadatas := await self._array_metadata_store.read(
236+
checkpoint_dir=infos[0].parent_dir,
237+
process_index=0,
238+
):
239+
if not isinstance(array_metadatas, list):
240+
raise ValueError(
241+
"Array metadata store returned unexpected result:"
242+
f" {array_metadatas}"
243+
)
244+
245+
array_metadatas_cache = {
246+
array_metadata.param_name: array_metadata
247+
for array_metadata in array_metadatas
248+
}
249+
250+
# Group inputs by global_mesh so that we can perform batched Array
251+
# construction for each global_mesh.
252+
inputs_by_global_mesh = collections.defaultdict(list)
253+
for i, global_mesh in enumerate(global_meshes):
254+
inputs_by_global_mesh[global_mesh].append(i)
255+
256+
results = cast(list[jax.Array], [None] * len(infos))
257+
258+
for global_mesh, idxs in inputs_by_global_mesh.items():
259+
grouped_infos = [infos[idx] for idx in idxs]
260+
grouped_global_shapes = [global_shapes[idx] for idx in idxs]
261+
grouped_dtypes = [dtypes[idx] for idx in idxs]
262+
grouped_shardings = [shardings[idx] for idx in idxs]
263+
locations, names = extract_parent_dir_and_name(grouped_infos)
264+
grouped_arrays, read_future = cloud_pathways_helper.read_arrays(
265+
locations[0],
266+
names,
267+
grouped_dtypes,
268+
grouped_global_shapes,
269+
grouped_shardings,
270+
global_mesh.devices,
271+
timeout=self.timeout,
272+
)
273+
# each persistence call is awaited serially.
274+
read_future.result()
275+
for idx, info, arr in zip(idxs, grouped_infos, grouped_arrays):
276+
if meta := array_metadatas_cache.get(info.name):
277+
assert isinstance(
278+
meta, array_metadata_lib.SerializedArrayMetadata
279+
), f"Expecting SerializedArrayMetadata but got {type(meta)}."
280+
if meta.ext_metadata:
281+
assert isinstance(meta.ext_metadata, dict), (
282+
"Expecting ext_metadata to be a dict but got"
283+
f" {type(meta.ext_metadata)}."
284+
)
285+
286+
if impl := meta.ext_metadata.get(
287+
array_metadata_lib.RANDOM_KEY_IMPL
288+
):
289+
arr = jax.random.wrap_key_data(arr, impl=impl)
290+
results[idx] = arr
291+
292+
return results

0 commit comments

Comments
 (0)