Skip to content

Commit 085962d

Browse files
committed
fix(v7-c02): cache bug + backward-compat single-file + opened-set assertions
Closes AC gaps in V7-C02 audit: - Bug fix: load_sharded_for_rank used 'out.get("__loaded__", set())' where out is dict[str, mx.array] — re-opened the same shard once per parameter (O(num_params) instead of O(num_owned_shards)). Replaced with explicit seen_shards set keyed on shard filename. - AC#3: new load_with_backward_compat accepts legacy single-file safetensors checkpoints AND sharded index.json AND a directory containing model.index.json. - AC#2: both loaders accept a test-only _opened list sink so tests can assert exactly which shard files were opened (and not opened). - _shard_index_from_name helper deduplicates the index-parsing logic. 5 new pytest cover: owned-shards-only opened, no duplicate opens for load_sharded, legacy single-file roundtrip, sharded via index, dir discovery, missing-dir error. bd: cppmega-mlx-9vv (AC#2/AC#3 closure) 11/11 shard tests + 4/4 topology regression all green.
1 parent 43740d0 commit 085962d

2 files changed

Lines changed: 151 additions & 23 deletions

File tree

cppmega_v4/runtime/checkpoint_shard.py

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -63,48 +63,103 @@ def save_sharded(state: dict[str, mx.array],
6363
return index
6464

6565

66-
def load_sharded(index_path: str | pathlib.Path) -> dict[str, mx.array]:
66+
def _shard_index_from_name(shard_name: str) -> int | None:
67+
"""Parse shard index from '<prefix>.shard-NNNNN-of-MMMMM.safetensors'."""
68+
try:
69+
return int(shard_name.split("-")[-3]) - 1
70+
except (ValueError, IndexError):
71+
return None
72+
73+
74+
def load_sharded(
75+
index_path: str | pathlib.Path,
76+
*, _opened: list[str] | None = None,
77+
) -> dict[str, mx.array]:
78+
"""Read every shard listed in the index manifest into a single dict.
79+
80+
``_opened`` is a test-only sink — when supplied, each shard's absolute
81+
path is appended exactly once. Lets tests assert which files were
82+
actually opened on disk (V7-C02 AC#2)."""
6783
index_path = pathlib.Path(index_path)
6884
index = json.loads(index_path.read_text())
6985
base = index_path.parent
7086
out: dict[str, mx.array] = {}
7187
seen_shards: set[str] = set()
72-
for k, shard_name in index["weight_map"].items():
88+
for shard_name in index["weight_map"].values():
7389
if shard_name in seen_shards:
7490
continue
7591
seen_shards.add(shard_name)
76-
out.update(st.load_file(str(base / shard_name)))
92+
path = str(base / shard_name)
93+
if _opened is not None:
94+
_opened.append(path)
95+
out.update(st.load_file(path))
7796
return out
7897

7998

80-
def load_sharded_for_rank(index_path: str | pathlib.Path,
81-
*, rank: int, world_size: int
82-
) -> dict[str, mx.array]:
99+
def load_sharded_for_rank(
100+
index_path: str | pathlib.Path,
101+
*, rank: int, world_size: int,
102+
_opened: list[str] | None = None,
103+
) -> dict[str, mx.array]:
83104
"""Round-robin shard ownership: rank r reads shards i where
84-
i % world_size == r."""
105+
i % world_size == r. Each owned shard is opened **exactly once**.
106+
107+
``_opened`` is the same test-only sink as ``load_sharded``."""
85108
if world_size <= 0 or not (0 <= rank < world_size):
86-
raise ValueError("invalid rank/world_size")
109+
raise ValueError(
110+
f"invalid rank/world_size: rank={rank}, world_size={world_size}")
87111
index_path = pathlib.Path(index_path)
88112
index = json.loads(index_path.read_text())
89113
base = index_path.parent
90114
num_shards = int(index["metadata"]["num_shards"])
91115
owned = {i for i in range(num_shards) if i % world_size == rank}
92116
out: dict[str, mx.array] = {}
93-
for k, shard_name in index["weight_map"].items():
94-
# Reverse-derive shard index from filename "*-NNNNN-of-MMMMM"
95-
try:
96-
idx = int(shard_name.split("-")[-3]) - 1
97-
except (ValueError, IndexError):
117+
# Deduplicate shard filenames before opening — the previous impl
118+
# iterated weight_map keys and re-opened the same file once per
119+
# tensor, which scaled O(num_params) instead of O(num_owned_shards).
120+
seen_shards: set[str] = set()
121+
for shard_name in index["weight_map"].values():
122+
if shard_name in seen_shards:
98123
continue
99-
if idx in owned:
100-
if shard_name not in {n for n in
101-
(index["weight_map"][kk]
102-
for kk in index["weight_map"])}:
103-
continue
104-
# Load each shard once.
105-
if shard_name not in out.get("__loaded__", set()):
106-
out.update(st.load_file(str(base / shard_name)))
124+
idx = _shard_index_from_name(shard_name)
125+
if idx is None or idx not in owned:
126+
continue
127+
seen_shards.add(shard_name)
128+
path = str(base / shard_name)
129+
if _opened is not None:
130+
_opened.append(path)
131+
out.update(st.load_file(path))
107132
return out
108133

109134

110-
__all__ = ["save_sharded", "load_sharded", "load_sharded_for_rank"]
135+
def load_with_backward_compat(
136+
path: str | pathlib.Path,
137+
*, _opened: list[str] | None = None,
138+
) -> dict[str, mx.array]:
139+
"""V7-C02 AC#3: accept either a sharded index.json OR a legacy
140+
single-file safetensors checkpoint.
141+
142+
If ``path`` ends in ``.index.json`` (or names an index.json file
143+
that exists), delegate to :func:`load_sharded`. Otherwise treat the
144+
path as a flat safetensors file and read it directly.
145+
"""
146+
p = pathlib.Path(path)
147+
if p.is_dir():
148+
# Convenience: caller passed a checkpoint directory — pick the
149+
# canonical model.index.json.
150+
cand = p / "model.index.json"
151+
if cand.is_file():
152+
return load_sharded(cand, _opened=_opened)
153+
raise FileNotFoundError(f"no model.index.json under {p}")
154+
if p.name.endswith(".index.json"):
155+
return load_sharded(p, _opened=_opened)
156+
# Legacy single-file path.
157+
if _opened is not None:
158+
_opened.append(str(p))
159+
return st.load_file(str(p))
160+
161+
162+
__all__ = [
163+
"save_sharded", "load_sharded", "load_sharded_for_rank",
164+
"load_with_backward_compat",
165+
]

tests/v4/test_checkpoint_shard.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
import mlx.core as mx
88
import pytest
99

10+
import mlx.core as mx
11+
import safetensors.mlx as st
12+
1013
from cppmega_v4.runtime.checkpoint_shard import (
11-
load_sharded, load_sharded_for_rank, save_sharded,
14+
load_sharded, load_sharded_for_rank,
15+
load_with_backward_compat, save_sharded,
1216
)
1317

1418

@@ -67,3 +71,72 @@ def test_v7_c02_load_sharded_for_rank_round_robin(tmp_path):
6771
def test_v7_c02_num_shards_validation(tmp_path):
6872
with pytest.raises(ValueError):
6973
save_sharded({"k": mx.zeros((2,))}, tmp_path, num_shards=0)
74+
75+
76+
def test_v7_c02_load_sharded_for_rank_opens_owned_only_once(tmp_path):
77+
"""AC#2: rank R must open ONLY its own shards, each exactly once.
78+
79+
Previously the impl re-opened the same shard once per parameter
80+
(buggy `out.get('__loaded__')` check) — opened count scaled with
81+
num_params instead of num_owned_shards."""
82+
save_sharded(_state(), tmp_path, num_shards=4)
83+
opened: list[str] = []
84+
out = load_sharded_for_rank(
85+
tmp_path / "model.index.json",
86+
rank=0, world_size=2, _opened=opened)
87+
# rank 0 / world 2 owns shards 0 and 2 (i % 2 == 0).
88+
assert len(opened) == 2, opened
89+
assert all("shard-00001-of-00004" in p or "shard-00003-of-00004" in p
90+
for p in opened)
91+
# Other shards (1, 3) must NOT have been opened.
92+
assert not any("shard-00002-of-00004" in p for p in opened)
93+
assert not any("shard-00004-of-00004" in p for p in opened)
94+
# Coverage still right for rank's owned subset.
95+
assert len(out) > 0
96+
97+
98+
def test_v7_c02_load_sharded_opens_each_shard_exactly_once(tmp_path):
99+
save_sharded(_state(), tmp_path, num_shards=3)
100+
opened: list[str] = []
101+
load_sharded(tmp_path / "model.index.json", _opened=opened)
102+
assert len(opened) == 3
103+
# No duplicates.
104+
assert len(set(opened)) == 3
105+
106+
107+
def test_v7_c02_backward_compat_loads_legacy_single_file(tmp_path):
108+
"""AC#3: single-file safetensors checkpoints saved before the
109+
sharded format still load via load_with_backward_compat."""
110+
state = _state()
111+
legacy = tmp_path / "legacy.safetensors"
112+
st.save_file(state, str(legacy))
113+
opened: list[str] = []
114+
out = load_with_backward_compat(legacy, _opened=opened)
115+
assert set(out.keys()) == set(state.keys())
116+
for k in state:
117+
assert mx.allclose(out[k], state[k], atol=0.0)
118+
assert opened == [str(legacy)]
119+
120+
121+
def test_v7_c02_backward_compat_loads_sharded_via_index(tmp_path):
122+
save_sharded(_state(), tmp_path, num_shards=2)
123+
opened: list[str] = []
124+
out = load_with_backward_compat(
125+
tmp_path / "model.index.json", _opened=opened)
126+
assert set(out.keys()) == set(_state().keys())
127+
# 2 shards opened, no duplicates.
128+
assert len(opened) == 2
129+
assert len(set(opened)) == 2
130+
131+
132+
def test_v7_c02_backward_compat_directory_picks_index(tmp_path):
133+
save_sharded(_state(), tmp_path, num_shards=2)
134+
out = load_with_backward_compat(tmp_path)
135+
assert set(out.keys()) == set(_state().keys())
136+
137+
138+
def test_v7_c02_backward_compat_missing_directory_raises(tmp_path):
139+
empty = tmp_path / "empty"
140+
empty.mkdir()
141+
with pytest.raises(FileNotFoundError):
142+
load_with_backward_compat(empty)

0 commit comments

Comments
 (0)