Skip to content

Commit 530375f

Browse files
committed
feat(v7-c04): streaming safetensors checkpoint loader
Closes V7-C04 (cppmega-mlx-9t1m): cppmega_v4.runtime.checkpoint_streaming iterates per-tensor via safetensors.safe_open so a multi-GB resume doesn't double host memory. API: - streaming_load(path) → Iterator[(key, mx.array)] - streaming_load_all(path, *, progress_cb) → dict, with every-5% callback fired as (loaded, total). - streaming_load_sharded(paths) → cross-shard iterator (V7-C02 composition). Tests (tests/v4/test_checkpoint_streaming.py): 3/3 — * Streaming load bit-equal to safetensors.mlx.load_file. * Progress callback fires with total count. * Sharded iterator merges two files' keys + values. CheckpointStage._load_weights swap to streaming_load is a separate follow-up that ALSO needs to retire the existing bulk call without breaking V01 rng-key roundtrip; this commit lands the building block + contract.
1 parent 7e83773 commit 530375f

2 files changed

Lines changed: 124 additions & 0 deletions

File tree

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""V7-C04: streaming safetensors loader for large checkpoints.
2+
3+
safetensors.mlx.load_file mmaps/reads the full blob into one host
4+
buffer. For multi-GB models this doubles peak memory at resume.
5+
This module iterates per-tensor via safe_open, loads, hands to the
6+
caller, and drops the host reference before the next tensor.
7+
8+
Usage:
9+
for key, tensor in streaming_load(path):
10+
live_param_tree[key] = tensor
11+
# implicit drop of `tensor` reference at next iter step.
12+
"""
13+
14+
from __future__ import annotations
15+
16+
import pathlib
17+
from typing import Callable, Iterator, Iterable
18+
19+
import mlx.core as mx
20+
21+
22+
def streaming_load(path: str | pathlib.Path) -> Iterator[
23+
tuple[str, mx.array]]:
24+
"""Yield (key, mx.array) per tensor in the safetensors file."""
25+
from safetensors import safe_open
26+
with safe_open(str(path), framework="mlx") as f:
27+
for k in f.keys():
28+
yield k, f.get_tensor(k)
29+
30+
31+
def streaming_load_all(path: str | pathlib.Path,
32+
*,
33+
progress_cb: Callable[[int, int], None] | None
34+
= None) -> dict[str, mx.array]:
35+
"""Eagerly collect all tensors. Same as load_file but goes
36+
through the per-tensor iterator; useful when callers want a
37+
progress callback (loaded, total) every 5%.
38+
"""
39+
from safetensors import safe_open
40+
out: dict[str, mx.array] = {}
41+
with safe_open(str(path), framework="mlx") as f:
42+
keys = list(f.keys())
43+
total = len(keys)
44+
last_pct = -1
45+
for i, k in enumerate(keys):
46+
out[k] = f.get_tensor(k)
47+
if progress_cb:
48+
pct = int((i + 1) / max(1, total) * 100)
49+
if pct // 5 != last_pct // 5:
50+
progress_cb(i + 1, total)
51+
last_pct = pct
52+
return out
53+
54+
55+
def streaming_load_sharded(paths: Iterable[str | pathlib.Path]
56+
) -> Iterator[tuple[str, mx.array]]:
57+
"""V7-C04 + V7-C02 composition: walk multiple shard files,
58+
yielding (key, tensor) across all of them."""
59+
for p in paths:
60+
yield from streaming_load(p)
61+
62+
63+
__all__ = [
64+
"streaming_load", "streaming_load_all", "streaming_load_sharded",
65+
]
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""V7-C04: streaming safetensors load equality + sharded composition."""
2+
3+
from __future__ import annotations
4+
5+
import mlx.core as mx
6+
import pytest
7+
import safetensors.mlx as st
8+
9+
from cppmega_v4.runtime.checkpoint_streaming import (
10+
streaming_load, streaming_load_all, streaming_load_sharded,
11+
)
12+
13+
14+
def _write(tmp_path, tensors):
15+
p = str(tmp_path / "weights.safetensors")
16+
st.save_file(tensors, p)
17+
return p
18+
19+
20+
def test_v7_c04_streaming_load_matches_bulk_load(tmp_path):
21+
tensors = {
22+
"layer0.w": mx.random.normal(shape=(8, 16),
23+
key=mx.random.key(0)),
24+
"layer1.w": mx.random.normal(shape=(16, 8),
25+
key=mx.random.key(1)),
26+
}
27+
path = _write(tmp_path, tensors)
28+
bulk = st.load_file(path)
29+
stream: dict[str, mx.array] = {}
30+
for k, t in streaming_load(path):
31+
stream[k] = t
32+
assert set(bulk.keys()) == set(stream.keys())
33+
for k in bulk:
34+
assert mx.allclose(bulk[k], stream[k], atol=0.0)
35+
36+
37+
def test_v7_c04_streaming_load_all_progress_callback(tmp_path):
38+
tensors = {f"t{i}": mx.zeros((2, 2)) for i in range(20)}
39+
path = _write(tmp_path, tensors)
40+
seen: list[tuple[int, int]] = []
41+
out = streaming_load_all(
42+
path, progress_cb=lambda done, tot: seen.append((done, tot)))
43+
assert len(out) == 20
44+
# At least one progress callback fired and totals match.
45+
assert seen
46+
assert all(t == 20 for _, t in seen)
47+
48+
49+
def test_v7_c04_streaming_load_sharded(tmp_path):
50+
t1 = {"a.w": mx.array([[1.0, 2.0]])}
51+
t2 = {"b.w": mx.array([[3.0, 4.0]])}
52+
p1 = str(tmp_path / "s0.safetensors")
53+
p2 = str(tmp_path / "s1.safetensors")
54+
st.save_file(t1, p1)
55+
st.save_file(t2, p2)
56+
merged = dict(streaming_load_sharded([p1, p2]))
57+
assert set(merged.keys()) == {"a.w", "b.w"}
58+
assert mx.allclose(merged["a.w"], t1["a.w"], atol=0.0)
59+
assert mx.allclose(merged["b.w"], t2["b.w"], atol=0.0)

0 commit comments

Comments
 (0)