Skip to content

Commit 2bc572e

Browse files
authored
New --force flag for add. Appease PET (#197)
1 parent 8bf76d2 commit 2bc572e

7 files changed

Lines changed: 156 additions & 3 deletions

File tree

rootstock/cli.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,15 @@ def main():
243243
action="store_true",
244244
help="Skip the verify phase (download only). Login-node escape hatch.",
245245
)
246+
add_parser.add_argument(
247+
"--force",
248+
action="store_true",
249+
help=(
250+
"Re-run the download even if the manifest records the checkpoint "
251+
"as fetched — repairs a cache file that has gone missing. Cheap "
252+
"when the cache is intact (cache hit, no transfer)."
253+
),
254+
)
246255
add_parser.add_argument(
247256
"--root",
248257
default=os.environ.get(ROOTSTOCK_ROOT_ENV),

rootstock/commands/add.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@ def cmd_add(args) -> int:
8282
device=args.device,
8383
verify=not args.no_verify,
8484
push=not args.no_push,
85+
force=args.force,
8586
setup_kwargs=setup_kwargs,
8687
progress=print,
8788
)

rootstock/operations.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,7 @@ def fetch_checkpoint(
10411041
cache_root: Path | None = None,
10421042
refresh: bool = True,
10431043
push: bool = True,
1044+
force: bool = False,
10441045
progress: Progress | None = None,
10451046
) -> FetchResult:
10461047
"""
@@ -1055,6 +1056,12 @@ def fetch_checkpoint(
10551056
skips the trailing full manifest refresh (+ push): a batch driver running
10561057
many fetches refreshes once at the end instead of once per checkpoint.
10571058
1059+
``force=True`` re-runs the download even when the manifest records the
1060+
checkpoint as fetched — the repair path for cache files that have gone
1061+
missing behind the manifest's back (cleaned, or an interrupted download
1062+
that stamped anyway). The underlying download is cache-aware, so a
1063+
forced fetch of an intact checkpoint costs a cache hit, not a transfer.
1064+
10581065
Raises CheckpointNotFoundError when no installed env declares the id,
10591066
and OperationError when the download fails (also recorded in the
10601067
manifest's ``last_error``).
@@ -1072,7 +1079,7 @@ def fetch_checkpoint(
10721079
peek = load_manifest(root)
10731080
peek_env = peek.environments.get(env_name) if peek else None
10741081
peek_ckpt = peek_env.checkpoints.get(checkpoint) if peek_env else None
1075-
already_fetched = peek_ckpt is not None and peek_ckpt.fetched_at is not None
1082+
already_fetched = not force and peek_ckpt is not None and peek_ckpt.fetched_at is not None
10761083
fetched_at = peek_ckpt.fetched_at if peek_ckpt else None
10771084

10781085
# ---- Download (runs outside the lock) -------------------------------
@@ -1183,6 +1190,7 @@ def add_checkpoint(
11831190
device: str = "cuda",
11841191
verify: bool = True,
11851192
push: bool = True,
1193+
force: bool = False,
11861194
setup_kwargs: dict | None = None,
11871195
cache_root: Path | None = None,
11881196
progress: Progress | None = None,
@@ -1196,7 +1204,8 @@ def add_checkpoint(
11961204
refresh at the end. The hosting env is resolved by matching the id
11971205
against each installed env's CHECKPOINTS table. Downloads happen on CPU
11981206
(the cache-aware path); verification runs on ``device`` unless
1199-
``verify`` is False.
1207+
``verify`` is False. ``force`` re-runs the download past the manifest's
1208+
fetched stamp (see :func:`fetch_checkpoint`).
12001209
12011210
Raises CheckpointNotFoundError when no installed env declares the id,
12021211
and OperationError for download/verify failures (the failure is also
@@ -1214,6 +1223,7 @@ def add_checkpoint(
12141223
setup_kwargs=setup_kwargs,
12151224
cache_root=cache_root,
12161225
refresh=False,
1226+
force=force,
12171227
progress=progress,
12181228
)
12191229

sample_model_configurations/nvidia_configs/pet.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,18 @@
2424

2525

2626
def setup(checkpoint: str, device: str = "cuda"):
27+
from huggingface_hub import hf_hub_download
2728
from upet.calculator import UPETCalculator
2829

30+
# Passing model=/version= makes UPETCalculator resolve the name by listing
31+
# the hub repo — an uncached API call that fails on workers, which run
32+
# with HF_HUB_OFFLINE=1 (and on any node without internet). Fetch the
33+
# pinned file ourselves — a cache hit needs no network even offline — and
34+
# hand it over as checkpoint_path, which skips the resolve entirely.
2935
model, version = CHECKPOINTS[checkpoint].split("@", 1)
30-
return UPETCalculator(model=model, version=version, device=device)
36+
path = hf_hub_download(
37+
repo_id="lab-cosmo/upet",
38+
filename=f"{model}-v{version}.ckpt",
39+
subfolder="models",
40+
)
41+
return UPETCalculator(checkpoint_path=path, device=device)

tests/cli/test_add.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ class _Args:
9494
args.kwarg = overrides.get("kwarg")
9595
args.device = overrides.get("device", "cuda")
9696
args.no_verify = overrides.get("no_verify", False)
97+
args.force = overrides.get("force", False)
9798
args.root = str(root)
9899
args.no_push = overrides.get("no_push", True)
99100
return args
@@ -159,6 +160,22 @@ def fake_verify(*a, **kw):
159160
assert ckpt.last_error is None
160161

161162

163+
def test_add_force_redownloads(fake_root, monkeypatch):
164+
"""--force repairs a cache file gone missing behind the manifest's
165+
fetched stamp."""
166+
download_calls = []
167+
monkeypatch.setattr(
168+
operations,
169+
"_run_download",
170+
lambda *a, **kw: (download_calls.append(a), (True, None))[1],
171+
)
172+
173+
assert cmd_add(_make_args(fake_root, no_verify=True)) == 0
174+
assert cmd_add(_make_args(fake_root, no_verify=True, force=True)) == 0
175+
176+
assert len(download_calls) == 2, "--force must re-run the download"
177+
178+
162179
def test_add_records_download_failure_and_returns_1(fake_root, monkeypatch):
163180
monkeypatch.setattr(
164181
operations,

tests/commands/test_add_split.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,22 @@ def test_fetch_is_idempotent(fake_root, refresh_calls, monkeypatch):
101101
assert second.fetched_at == first.fetched_at
102102

103103

104+
def test_fetch_force_redownloads_past_fetched_stamp(fake_root, refresh_calls, monkeypatch):
105+
"""The repair path for cache files gone missing behind the manifest."""
106+
downloads = []
107+
monkeypatch.setattr(
108+
operations, "_run_download", lambda *a, **kw: (downloads.append(a), (True, None))[1]
109+
)
110+
111+
first = fetch_checkpoint(fake_root, "mace-mp-0-medium")
112+
second = fetch_checkpoint(fake_root, "mace-mp-0-medium", force=True)
113+
114+
assert len(downloads) == 2, "force must re-run the download"
115+
assert not second.already_fetched
116+
assert second.fetched_at is not None
117+
assert second.fetched_at >= first.fetched_at
118+
119+
104120
def test_fetch_failure_records_last_error_and_raises(fake_root, refresh_calls, monkeypatch):
105121
monkeypatch.setattr(
106122
operations, "_run_download", lambda *a, **kw: (False, "ConnectionError: hub unreachable")
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""Tests for the pet env's hub-resolve bypass.
2+
3+
UPETCalculator(model=..., version=...) resolves the name by listing the hub
4+
repo — an uncached API call that fails on workers (HF_HUB_OFFLINE=1) and on
5+
nodes without internet. The env fetches the pinned file itself via
6+
hf_hub_download (a cache hit needs no network) and passes checkpoint_path,
7+
which skips the resolve.
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import importlib.util
13+
import sys
14+
import types
15+
from pathlib import Path
16+
17+
import pytest
18+
19+
_CONFIGS_DIR = (
20+
Path(__file__).parent.parent.parent / "sample_model_configurations" / "nvidia_configs"
21+
)
22+
23+
24+
def _load_env_module():
25+
spec = importlib.util.spec_from_file_location("pet_env", _CONFIGS_DIR / "pet.py")
26+
module = importlib.util.module_from_spec(spec)
27+
spec.loader.exec_module(module)
28+
return module
29+
30+
31+
@pytest.fixture
32+
def stubbed_libs(monkeypatch):
33+
"""Stub huggingface_hub and upet; capture what setup() passes to each."""
34+
captured = {}
35+
36+
hf = types.ModuleType("huggingface_hub")
37+
38+
def hf_hub_download(**kwargs):
39+
captured["download"] = kwargs
40+
return "/shared/cache/models/stub.ckpt"
41+
42+
hf.hf_hub_download = hf_hub_download
43+
44+
upet = types.ModuleType("upet")
45+
upet_calculator = types.ModuleType("upet.calculator")
46+
47+
class UPETCalculator:
48+
def __init__(self, **kwargs):
49+
captured["calculator"] = kwargs
50+
51+
upet_calculator.UPETCalculator = UPETCalculator
52+
upet.calculator = upet_calculator
53+
54+
monkeypatch.setitem(sys.modules, "huggingface_hub", hf)
55+
monkeypatch.setitem(sys.modules, "upet", upet)
56+
monkeypatch.setitem(sys.modules, "upet.calculator", upet_calculator)
57+
return captured
58+
59+
60+
def test_setup_downloads_pinned_filename(stubbed_libs):
61+
env = _load_env_module()
62+
env.setup("pet-oam-xl", device="cuda")
63+
assert stubbed_libs["download"] == {
64+
"repo_id": "lab-cosmo/upet",
65+
"filename": "pet-oam-xl-v1.0.0.ckpt",
66+
"subfolder": "models",
67+
}
68+
69+
70+
def test_setup_passes_checkpoint_path_not_model_name(stubbed_libs):
71+
"""model=/version= would trigger the hub-listing resolve — never pass them."""
72+
env = _load_env_module()
73+
env.setup("pet-omatpes-l", device="cpu")
74+
calc_kwargs = stubbed_libs["calculator"]
75+
assert calc_kwargs == {
76+
"checkpoint_path": "/shared/cache/models/stub.ckpt",
77+
"device": "cpu",
78+
}
79+
80+
81+
def test_every_checkpoint_maps_to_parseable_filename():
82+
"""upet parses (model, size, version) out of the filename — every pinned
83+
entry must render to the {model}-{size}-v{version}.ckpt shape."""
84+
env = _load_env_module()
85+
for upstream in env.CHECKPOINTS.values():
86+
model, version = upstream.split("@", 1)
87+
filename = f"{model}-v{version}.ckpt"
88+
assert filename.endswith(f"-v{version}.ckpt")
89+
assert "@" not in filename

0 commit comments

Comments
 (0)