Skip to content

Commit 9339349

Browse files
mridul-sahuOrbax Authors
authored andcommitted
Internal Change
FUTURE_COPYBARA_INTEGRATE_REVIEW=#3455 from mridul-sahu:feature/safetensors-sharding-driven-load 739128d PiperOrigin-RevId: 947822041
1 parent fbe6113 commit 9339349

14 files changed

Lines changed: 1611 additions & 878 deletions

File tree

checkpoint/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Support explicit devices for Pathways MTC init
1414
- Avoid stale Pathways MTC worker dummy handles
1515

16+
### Changed
17+
18+
- #v1 Drive the safetensors load by the target sharding: each process reads
19+
only the byte ranges its own devices need (coalesced under
20+
`SafetensorsOptions.max_over_read_ratio`, fetched as concurrent ranged
21+
reads of `SafetensorsOptions.read_chunk_bytes` each, bounded by
22+
`MemoryOptions.read_concurrent_bytes`), replacing the transient-array
23+
resharding load. Supports arbitrary shardings, including inner-dimension
24+
partitions.
25+
1626
## [0.12.1] - 2026-06-24
1727

1828
### Added

checkpoint/orbax/checkpoint/_src/arrays/types.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
NdSlice = tuple[slice, ...] | type(Ellipsis)
3232

3333
Index = tuple[slice, ...]
34+
IndexBounds = tuple[tuple[int, int], ...]
3435

3536

3637
@dataclasses.dataclass(frozen=True)

checkpoint/orbax/checkpoint/_src/testing/benchmarks/run_benchmarks.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,16 @@
2020
using standard environment variables like JAX_COORDINATOR_ADDRESS,
2121
JAX_PROCESS_ID and JAX_NUM_PROCESSES.
2222
"""
23-
# pylint: disable=g-statement-before-imports,g-import-not-at-top
23+
# pylint: disable=g-statement-before-imports,g-import-not-at-top,g-bad-import-order
2424

25-
try: # SimDevice import must occur before JAX.
26-
import simdevice # pylint: disable=unused-import
27-
except (ImportError, FileNotFoundError):
28-
pass
25+
import sys
26+
27+
# SimDevice import must occur before JAX only if simulated run is requested.
28+
if any('simdevice' in arg for arg in sys.argv):
29+
try:
30+
import simdevice # pylint: disable=unused-import
31+
except (ImportError, FileNotFoundError):
32+
pass
2933

3034
import os
3135

checkpoint/orbax/checkpoint/_src/testing/benchmarks/safetensors/configs/llama3-405b.yaml

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
# of byte runs to bin-pack);
99
# * per-host shard sizes are tiny relative to file sizes (4 GB
1010
# shards out of 100+ GB files);
11-
# * concurrent reads cap (`_MAX_CONCURRENT_READS`) is the active
12-
# bottleneck, not bandwidth.
11+
# * the byte budget (`max_in_flight_bytes`, default 2 GiB) governs
12+
# concurrency by total bytes — so on this tier each read is small
13+
# (~4 GB per host, easily within budget) and many reads run
14+
# concurrently. Raise the budget on RAM-rich hosts to absorb the
15+
# larger coalesced blocks cross-tensor coalescing produces.
1316
#
1417
# Llama 3 405B is gated; see safetensor_load_llama3-8b.yaml.
1518
# GCS pre-staging is *strongly* recommended -- 810 GB download per VM

checkpoint/orbax/checkpoint/_src/testing/benchmarks/safetensors/configs/mistral7b.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# bf16 weights at fsdp=8 -> ~1.75 GB per core, comfortable on any v5p/v5e.
1313

1414
suite_name: "safetensors_load_mistral7b"
15-
num_repeats: 1
15+
num_repeats: 2
1616

1717
mesh_config:
1818
mesh_axes: ["fsdp"]
@@ -23,5 +23,7 @@ benchmarks:
2323
options:
2424
checkpoint_path: "gs://orbax-benchmarks/st_fixtures/mistral7b/"
2525
sharding_config_path: "gs://orbax-benchmarks/st_fixtures/sharding/mistral7b_fsdp8.json"
26-
enable_trace: true
27-
# Capture-only.
26+
enable_trace: false
27+
enable_xprof: true
28+
safetensors_max_over_read_ratio: [1.0]
29+
safetensors_read_chunk_mb: [256]

checkpoint/orbax/checkpoint/_src/testing/benchmarks/safetensors/configs/mistral7b_tp.yaml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
# new loader's reads should be bounded at 2x ideal, not K x.
3232

3333
suite_name: "safetensor_load_mistral7b_tp"
34-
num_repeats: 4
34+
num_repeats: 2
3535

3636
mesh_config:
3737
mesh_axes: ["tp"]
@@ -41,5 +41,9 @@ benchmarks:
4141
- generator: "orbax.checkpoint._src.testing.benchmarks.safetensors.load_benchmark.SafetensorLoadBenchmark"
4242
options:
4343
checkpoint_path: "gs://orbax-benchmarks/st_fixtures/mistral7b/"
44-
sharding_config_path: "gs://orbax-benchmarks/st_fixtures/sharding/mistral7b_tp8.json"
45-
enable_trace: true
44+
sharding_config_path: "gs://orbax-benchmarks/st_fixtures/sharding/mistral7b_fsdp8.json"
45+
enable_trace: false
46+
enable_xprof: true
47+
# The sweep. Each value becomes a separate run in the dashboard;
48+
# the parallel-coordinates view colors them so the curve is visible.
49+
safetensors_max_over_read_ratio: [2.0]

checkpoint/orbax/checkpoint/_src/testing/benchmarks/safetensors/load_benchmark.py

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828

2929
from __future__ import annotations
3030

31+
import contextlib
3132
import dataclasses
3233
import json
3334
import pprint
@@ -44,14 +45,16 @@
4445
from orbax.checkpoint._src.testing.benchmarks.core import pytree_utils
4546

4647

48+
4749
@dataclasses.dataclass(frozen=True)
4850
class SafetensorLoadBenchmarkOptions(benchmarks_core.BenchmarkOptions):
4951
"""Configuration for loading a HuggingFace-format safetensors checkpoint.
5052
5153
Self-contained: builds its own `ocp.Context()` with the SAFETENSORS layout
5254
pinned from the load-side tuning knobs, so a single run can sweep e.g.
53-
`use_load_and_broadcast` or `restore_concurrent_gb` for A/B comparison. Each
54-
knob may be a single value or a list to expand into a parameter sweep.
55+
`use_load_and_broadcast`, `restore_concurrent_gb`, or
56+
`safetensors_max_over_read_ratio` for A/B comparison. Each knob may be a
57+
single value or a list to expand into a parameter sweep.
5558
5659
Attributes:
5760
checkpoint_path: Path (local or `gs://`) to a directory containing one or
@@ -74,6 +77,17 @@ class SafetensorLoadBenchmarkOptions(benchmarks_core.BenchmarkOptions):
7477
others instead of every replica reading from storage.
7578
restore_concurrent_gb: Cap on concurrent read bytes (in GiB); `None` leaves
7679
the Orbax default.
80+
safetensors_max_over_read_ratio: Override for
81+
`SafetensorsOptions.max_over_read_ratio`. Bounds per-host cluster egress
82+
on strided shardings (inner-dim TP, HSDP). Sweep as `[2.0, 3.0, 4.0]` on a
83+
TP variant to measure the read-count vs over-read tradeoff. `None`
84+
(default) uses the loader's default (currently 2.0). In-flight read bytes
85+
are bounded by `restore_concurrent_gb` (shared with the rest of restore).
86+
safetensors_read_chunk_mb: Override for
87+
`SafetensorsOptions.read_chunk_bytes`, in MiB. Coalesced blocks are split
88+
at this size into concurrent ranged reads; sweep as `[64, 128, 512]` to
89+
measure the request-count vs read-parallelism tradeoff on a given storage
90+
backend. `None` (default) uses the loader's default (currently 128 MiB).
7791
metric_tracemalloc_enabled: Whether to capture the tracemalloc metric
7892
(opt-in because its per-allocation snapshots are expensive).
7993
"""
@@ -84,7 +98,10 @@ class SafetensorLoadBenchmarkOptions(benchmarks_core.BenchmarkOptions):
8498
reference_digests_path: str | None = None
8599
use_load_and_broadcast: bool | list[bool] = False
86100
restore_concurrent_gb: int | None | list[int | None] = None
101+
safetensors_max_over_read_ratio: float | None | list[float | None] = None
102+
safetensors_read_chunk_mb: int | None | list[int | None] = None
87103
metric_tracemalloc_enabled: bool = False
104+
enable_xprof: bool = False
88105

89106
def is_valid(self) -> bool:
90107
if self.checkpoint_path is None:
@@ -94,7 +111,15 @@ def is_valid(self) -> bool:
94111
@property
95112
def context(self) -> ocp.Context:
96113
ctx = ocp.Context(
97-
checkpoint_layout=ocp.options.CheckpointLayout.SAFETENSORS
114+
checkpoint_layout=ocp.options.CheckpointLayout.SAFETENSORS,
115+
safetensors_options=ocp.options.SafetensorsOptions(
116+
max_over_read_ratio=self.safetensors_max_over_read_ratio,
117+
read_chunk_bytes=(
118+
self.safetensors_read_chunk_mb * 1024**2
119+
if self.safetensors_read_chunk_mb is not None
120+
else None
121+
),
122+
),
98123
)
99124
# TODO(b/519204863): Fix type hint for args like list[T] | T
100125
ctx.array.loading.use_load_and_broadcast = self.use_load_and_broadcast
@@ -214,12 +239,18 @@ def test_fn(
214239
else:
215240
abstract_pytree = _replicated_abstract_state(metadata.metadata)
216241

242+
xprof_session = contextlib.nullcontext()
243+
217244
if load_trace is not None:
218245
jax.profiler.start_trace(str(load_trace))
219-
with metrics.measure("load", metrics_to_measure):
220-
restored_pytree = ocp.load(
221-
checkpoint_path, abstract_state=abstract_pytree
222-
)
246+
with xprof_session as url:
247+
if url:
248+
logging.info("XProf Session URL: %s", url)
249+
print(f"\n[XPROF] Session URL: {url}\n")
250+
with metrics.measure("load", metrics_to_measure):
251+
restored_pytree = ocp.load(
252+
checkpoint_path, abstract_state=abstract_pytree
253+
)
223254
if load_trace is not None:
224255
jax.profiler.stop_trace()
225256

checkpoint/orbax/checkpoint/_src/testing/benchmarks/safetensors/load_benchmark_test.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,16 @@ def test_context_pins_safetensors_layout(self):
5555
ocp.options.CheckpointLayout.SAFETENSORS,
5656
)
5757

58+
def test_context_maps_safetensors_knobs(self):
59+
opts = slb.SafetensorLoadBenchmarkOptions(
60+
checkpoint_path="/tmp/x",
61+
safetensors_max_over_read_ratio=3.0,
62+
safetensors_read_chunk_mb=64,
63+
)
64+
ctx = opts.context
65+
self.assertEqual(ctx.safetensors_options.max_over_read_ratio, 3.0)
66+
self.assertEqual(ctx.safetensors_options.read_chunk_bytes, 64 * 1024**2)
67+
5868

5969
class GenerationTest(absltest.TestCase):
6070

checkpoint/orbax/checkpoint/_src/testing/benchmarks/safetensors/prepare.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ def _mirror_to_gcs(local_dir: epath.Path, gcs_uri: str) -> None:
190190
)
191191

192192

193+
194+
193195
def _write_sharding(
194196
model_dir: epath.Path, axis_size: int, strategy: str, out: str
195197
) -> int:
@@ -232,15 +234,20 @@ def _write_sharding(
232234
flags.mark_flags_as_mutual_exclusive(["repo", "local_dir"], required=True)
233235

234236

235-
def main(argv: list[str]) -> None:
236-
del argv # Unused; inputs come from flags.
237-
if not _GCS.value and not _SHARDING_OUT.value:
237+
def _validate_flags() -> None:
238+
is_mirroring = _GCS.value
239+
if not is_mirroring and not _SHARDING_OUT.value:
238240
raise app.UsageError("nothing to do: pass --gcs and/or --sharding_out.")
239241
if _SHARDING_OUT.value and _AXIS_SIZE.value is None:
240242
raise app.UsageError("--sharding_out requires --axis_size.")
241243

244+
245+
def main(argv: list[str]) -> None:
246+
del argv # Unused; inputs come from flags.
247+
_validate_flags()
248+
242249
def _run(model_dir: epath.Path) -> None:
243-
if _GCS.value:
250+
if _GCS.value and model_dir != epath.Path(_GCS.value):
244251
print(f">>> Mirroring {model_dir} -> {_GCS.value}")
245252
_mirror_to_gcs(model_dir, _GCS.value)
246253
if _SHARDING_OUT.value:
@@ -251,7 +258,7 @@ def _run(model_dir: epath.Path) -> None:
251258

252259
if _REPO.value:
253260
if _DOWNLOAD_DIR.value:
254-
dest = epath.Path(_DOWNLOAD_DIR.value) / "safetensors-prepare-model"
261+
dest = epath.Path(_DOWNLOAD_DIR.value)
255262
else:
256263
model_slug = _REPO.value.replace("/", "--")
257264
dest = (
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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+
from unittest import mock
16+
from absl import flags
17+
from absl.testing import absltest
18+
from etils import epath
19+
from orbax.checkpoint._src.testing.benchmarks.safetensors import prepare
20+
21+
22+
class PrepareTest(absltest.TestCase):
23+
24+
@mock.patch("huggingface_hub.snapshot_download")
25+
@mock.patch("subprocess.run")
26+
def test_main_with_repo_and_gcs(self, mock_run, mock_download):
27+
flags.FLAGS.repo = "dummy-org/dummy-repo"
28+
flags.FLAGS.gcs = "gs://dummy-bucket/model"
29+
flags.FLAGS.local_dir = None
30+
31+
temp_dir = self.create_tempdir()
32+
(epath.Path(temp_dir.full_path) / "file1.safetensors").write_text("dummy")
33+
(epath.Path(temp_dir.full_path) / "file2.safetensors").write_text("dummy")
34+
35+
mock_download.return_value = temp_dir.full_path
36+
37+
prepare.main([])
38+
39+
mock_download.assert_called_once_with(
40+
repo_id="dummy-org/dummy-repo",
41+
local_dir=mock.ANY,
42+
allow_patterns=["*.safetensors", "*.safetensors.index.json"],
43+
)
44+
mock_run.assert_called_with(
45+
[
46+
"gcloud",
47+
"storage",
48+
"rsync",
49+
temp_dir.full_path,
50+
"gs://dummy-bucket/model",
51+
"-r",
52+
],
53+
check=True,
54+
)
55+
56+
57+
58+
if __name__ == "__main__":
59+
# Bypass required flag check during test initialization
60+
flags.FLAGS.set_default("repo", "dummy-org/dummy-repo")
61+
absltest.main()

0 commit comments

Comments
 (0)