@tuned-tensor/cli 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -7
- package/dist/{chunk-4Q54NHKR.js → chunk-4SUXBHIY.js} +1715 -392
- package/dist/chunk-4SUXBHIY.js.map +1 -0
- package/dist/index.js +1910 -356
- package/dist/index.js.map +1 -1
- package/dist/local-runtime.js +147 -49
- package/dist/local-runtime.js.map +1 -1
- package/docs/local-runtime/architecture.md +44 -1
- package/docs/local-runtime/aws-gpu.md +2 -0
- package/docs/local-runtime/decision.md +142 -0
- package/docs/spec-architecture.html +2 -2
- package/docs/spec-workflow.md +18 -3
- package/examples/single-spec/decision/tunedtensor.json +630 -0
- package/package.json +5 -2
- package/training/adapter/src/evaluate.py +14 -7
- package/training/adapter/src/hub_download.py +384 -0
- package/training/adapter/src/model_contract.py +38 -5
- package/training/adapter/src/prefetch.py +26 -32
- package/training/adapter/src/prefetch_dataset.py +116 -0
- package/training/adapter/src/serve.py +42 -12
- package/training/adapter/src/train.py +8 -0
- package/training/decision/pyproject.toml +25 -0
- package/training/decision/src/common.py +127 -0
- package/training/decision/src/evaluate.py +70 -0
- package/training/decision/src/train.py +181 -0
- package/training/decision/uv.lock +1175 -0
- package/dist/chunk-4Q54NHKR.js.map +0 -1
|
@@ -10,12 +10,12 @@ from tempfile import TemporaryDirectory
|
|
|
10
10
|
from typing import Any
|
|
11
11
|
|
|
12
12
|
from model_contract import (
|
|
13
|
-
CERTIFIED_BASE_MODELS,
|
|
14
13
|
CERTIFIED_BASE_MODEL,
|
|
15
14
|
assert_certified_base_model,
|
|
16
15
|
assert_certified_base_model_revision,
|
|
17
16
|
assert_certified_model_config,
|
|
18
17
|
chat_template_kwargs,
|
|
18
|
+
uncertified_load_kwargs,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
MAX_ARCHIVE_MEMBERS = 20_000
|
|
@@ -145,10 +145,12 @@ def resolve_device(device: str) -> str:
|
|
|
145
145
|
return device
|
|
146
146
|
|
|
147
147
|
|
|
148
|
-
def _assert_certified_model_source(base_model: str) -> None:
|
|
149
|
-
|
|
148
|
+
def _assert_certified_model_source(base_model: str, model_source: str) -> None:
|
|
149
|
+
# A hub source must be the requested model; certified or not, it is
|
|
150
|
+
# checked by assert_certified_base_model.
|
|
151
|
+
if not Path(model_source).exists() and model_source != base_model:
|
|
150
152
|
raise ValueError(
|
|
151
|
-
f"The bundled evaluator does not certify {
|
|
153
|
+
f"The bundled evaluator does not certify {model_source!r}"
|
|
152
154
|
)
|
|
153
155
|
|
|
154
156
|
|
|
@@ -160,9 +162,14 @@ def load_text_model(
|
|
|
160
162
|
base_model = str(payload["base_model"])
|
|
161
163
|
model_source = str(payload.get("model_source", base_model))
|
|
162
164
|
assert_certified_base_model(base_model, "Evaluation base model")
|
|
163
|
-
_assert_certified_model_source(model_source)
|
|
165
|
+
_assert_certified_model_source(base_model, model_source)
|
|
164
166
|
revision = payload.get("base_model_revision")
|
|
165
|
-
assert_certified_base_model_revision(
|
|
167
|
+
assert_certified_base_model_revision(
|
|
168
|
+
base_model,
|
|
169
|
+
revision,
|
|
170
|
+
"Evaluation base model revision",
|
|
171
|
+
local_source=Path(model_source).is_dir(),
|
|
172
|
+
)
|
|
166
173
|
device = resolve_device(str(payload.get("device", "cuda")))
|
|
167
174
|
token = os.getenv("HF_TOKEN")
|
|
168
175
|
source_kwargs: dict[str, Any] = {
|
|
@@ -184,7 +191,7 @@ def load_text_model(
|
|
|
184
191
|
if device == "cuda":
|
|
185
192
|
dtype = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
|
|
186
193
|
|
|
187
|
-
model_kwargs: dict[str, Any] =
|
|
194
|
+
model_kwargs: dict[str, Any] = {**source_kwargs, **uncertified_load_kwargs(base_model)}
|
|
188
195
|
if dtype is not None:
|
|
189
196
|
model_kwargs["dtype"] = dtype
|
|
190
197
|
if device == "cuda":
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
"""Resumable, observable Hugging Face Hub snapshot downloads.
|
|
2
|
+
|
|
3
|
+
Model and dataset prefetch share this module so both get the same behavior:
|
|
4
|
+
|
|
5
|
+
* the requested revision is resolved once to an immutable commit, so retries
|
|
6
|
+
can never mix files from two commits when a branch moves mid-download;
|
|
7
|
+
* transient network failures are retried with exponential backoff. Completed
|
|
8
|
+
files stay in the Hugging Face cache, so each retry only fetches what is
|
|
9
|
+
still missing;
|
|
10
|
+
* progress is emitted as ``@@tt-progress {json}`` lines on stdout, which the
|
|
11
|
+
Node runner renders as a progress bar. huggingface_hub's own tqdm bars are
|
|
12
|
+
disabled because redrawn carriage-return output is unreadable in logs.
|
|
13
|
+
|
|
14
|
+
huggingface_hub is imported lazily so the pure helpers stay testable with the
|
|
15
|
+
system Python.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import errno
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Callable, Iterable
|
|
30
|
+
|
|
31
|
+
PROGRESS_PREFIX = "@@tt-progress "
|
|
32
|
+
COMMIT = re.compile(r"[0-9a-fA-F]{40}")
|
|
33
|
+
DEFAULT_MAX_ATTEMPTS = 10
|
|
34
|
+
MAX_BACKOFF_SECONDS = 60.0
|
|
35
|
+
# A per-request read timeout that tolerates slow or briefly stalled Wi-Fi.
|
|
36
|
+
DEFAULT_DOWNLOAD_TIMEOUT_SECONDS = "60"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class PlannedFile:
|
|
41
|
+
"""One repository file expected in the snapshot, keyed by its cache blob name."""
|
|
42
|
+
|
|
43
|
+
path: str
|
|
44
|
+
size: int
|
|
45
|
+
etag: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def configure_download_environment() -> None:
|
|
49
|
+
"""Must run before huggingface_hub is imported; it reads these at import."""
|
|
50
|
+
os.environ["HF_HUB_DISABLE_PROGRESS_BARS"] = "1"
|
|
51
|
+
os.environ.setdefault("HF_HUB_DOWNLOAD_TIMEOUT", DEFAULT_DOWNLOAD_TIMEOUT_SECONDS)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def repo_folder_name(repo_id: str, repo_type: str) -> str:
|
|
55
|
+
return f"{repo_type}s--{repo_id.replace('/', '--')}"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def max_attempts_from_env(environ: dict[str, str] | None = None) -> int:
|
|
59
|
+
raw = (environ if environ is not None else os.environ).get("TT_HF_DOWNLOAD_MAX_ATTEMPTS")
|
|
60
|
+
if raw is None or not raw.strip():
|
|
61
|
+
return DEFAULT_MAX_ATTEMPTS
|
|
62
|
+
try:
|
|
63
|
+
value = int(raw)
|
|
64
|
+
except ValueError as exc:
|
|
65
|
+
raise ValueError("TT_HF_DOWNLOAD_MAX_ATTEMPTS must be an integer from 1 to 100") from exc
|
|
66
|
+
if not 1 <= value <= 100:
|
|
67
|
+
raise ValueError("TT_HF_DOWNLOAD_MAX_ATTEMPTS must be an integer from 1 to 100")
|
|
68
|
+
return value
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def backoff_seconds(attempt: int) -> float:
|
|
72
|
+
"""Delay before retry ``attempt`` (1-based): 2, 4, 8, ... capped at 60s."""
|
|
73
|
+
return min(MAX_BACKOFF_SECONDS, float(2 ** attempt))
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
class IncompleteDownloadError(RuntimeError):
|
|
77
|
+
"""snapshot_download returned a cached folder that still lacks planned files."""
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_retryable(error: BaseException) -> bool:
|
|
81
|
+
"""Retry network and server failures; fail fast on caller or auth errors.
|
|
82
|
+
|
|
83
|
+
huggingface_hub reports a connection failure on a file that is not cached
|
|
84
|
+
yet as LocalEntryNotFoundError (a FileNotFoundError), so that and
|
|
85
|
+
IncompleteSnapshotError are retried during downloads.
|
|
86
|
+
"""
|
|
87
|
+
if isinstance(error, (KeyboardInterrupt, SystemExit)):
|
|
88
|
+
return False
|
|
89
|
+
if isinstance(error, OSError) and error.errno in (errno.ENOSPC, errno.EACCES, errno.EROFS, errno.EDQUOT):
|
|
90
|
+
return False
|
|
91
|
+
name = type(error).__name__
|
|
92
|
+
if name in {
|
|
93
|
+
"RepositoryNotFoundError",
|
|
94
|
+
"GatedRepoError",
|
|
95
|
+
"RevisionNotFoundError",
|
|
96
|
+
"RemoteEntryNotFoundError",
|
|
97
|
+
"DisabledRepoError",
|
|
98
|
+
"OfflineModeIsEnabled",
|
|
99
|
+
}:
|
|
100
|
+
return False
|
|
101
|
+
if isinstance(error, (ValueError, TypeError)):
|
|
102
|
+
return False
|
|
103
|
+
response = getattr(error, "response", None)
|
|
104
|
+
status = getattr(response, "status_code", None)
|
|
105
|
+
if isinstance(status, int) and 400 <= status < 500 and status not in (408, 429):
|
|
106
|
+
return False
|
|
107
|
+
return True
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def plan_files(siblings: Iterable[Any]) -> list[PlannedFile]:
|
|
111
|
+
"""Build the expected cache inventory from repo_info(files_metadata=True)."""
|
|
112
|
+
planned: list[PlannedFile] = []
|
|
113
|
+
for sibling in siblings:
|
|
114
|
+
lfs = getattr(sibling, "lfs", None)
|
|
115
|
+
etag = getattr(lfs, "sha256", None) if lfs is not None else None
|
|
116
|
+
etag = etag or getattr(sibling, "blob_id", None)
|
|
117
|
+
size = getattr(sibling, "size", None)
|
|
118
|
+
if not etag or not isinstance(size, int):
|
|
119
|
+
# Without a blob name and size the file cannot be tracked; the
|
|
120
|
+
# download still fetches and verifies it, only progress is coarser.
|
|
121
|
+
continue
|
|
122
|
+
planned.append(PlannedFile(path=sibling.rfilename, size=size, etag=str(etag)))
|
|
123
|
+
return planned
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def downloaded_bytes(blobs: Path, planned: PlannedFile) -> tuple[int, bool]:
|
|
127
|
+
"""Bytes present for one planned file and whether its blob is complete."""
|
|
128
|
+
if (blobs / planned.etag).is_file():
|
|
129
|
+
return planned.size, True
|
|
130
|
+
partial = 0
|
|
131
|
+
try:
|
|
132
|
+
for candidate in blobs.glob(f"{planned.etag}*.incomplete"):
|
|
133
|
+
try:
|
|
134
|
+
partial = max(partial, candidate.stat().st_size)
|
|
135
|
+
except OSError:
|
|
136
|
+
continue
|
|
137
|
+
except OSError:
|
|
138
|
+
return 0, False
|
|
139
|
+
return min(partial, planned.size), False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def measure(blobs: Path, planned: list[PlannedFile]) -> dict[str, int]:
|
|
143
|
+
completed_bytes = 0
|
|
144
|
+
completed_files = 0
|
|
145
|
+
for item in planned:
|
|
146
|
+
size, done = downloaded_bytes(blobs, item)
|
|
147
|
+
completed_bytes += size
|
|
148
|
+
completed_files += int(done)
|
|
149
|
+
return {
|
|
150
|
+
"completed_bytes": completed_bytes,
|
|
151
|
+
"total_bytes": sum(item.size for item in planned),
|
|
152
|
+
"files_completed": completed_files,
|
|
153
|
+
"files_total": len(planned),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
_STDOUT_LOCK = threading.Lock()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def write_line(text: str, stream: Any = None) -> None:
|
|
161
|
+
"""Write one whole line; the progress thread and log messages share stdout."""
|
|
162
|
+
target = stream if stream is not None else sys.stdout
|
|
163
|
+
with _STDOUT_LOCK:
|
|
164
|
+
target.write(text + "\n")
|
|
165
|
+
target.flush()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def emit_progress(event: dict[str, Any], stream: Any = None) -> None:
|
|
169
|
+
write_line(PROGRESS_PREFIX + json.dumps(event, separators=(",", ":")), stream)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ProgressMonitor:
|
|
173
|
+
"""Polls the cache blob directory and emits progress while a download runs."""
|
|
174
|
+
|
|
175
|
+
def __init__(
|
|
176
|
+
self,
|
|
177
|
+
blobs: Path,
|
|
178
|
+
planned: list[PlannedFile],
|
|
179
|
+
label: str,
|
|
180
|
+
emit: Callable[[dict[str, Any]], None] = emit_progress,
|
|
181
|
+
interval: float = 0.5,
|
|
182
|
+
heartbeat: float = 5.0,
|
|
183
|
+
) -> None:
|
|
184
|
+
self.blobs = blobs
|
|
185
|
+
self.planned = planned
|
|
186
|
+
self.label = label
|
|
187
|
+
self.emit = emit
|
|
188
|
+
self.interval = interval
|
|
189
|
+
self.heartbeat = heartbeat
|
|
190
|
+
self._stop = threading.Event()
|
|
191
|
+
self._thread: threading.Thread | None = None
|
|
192
|
+
self._last: dict[str, int] | None = None
|
|
193
|
+
self._last_emit = 0.0
|
|
194
|
+
|
|
195
|
+
def snapshot(self, force: bool = False, phase: str = "download") -> None:
|
|
196
|
+
current = measure(self.blobs, self.planned)
|
|
197
|
+
now = time.monotonic()
|
|
198
|
+
if not force and current == self._last and now - self._last_emit < self.heartbeat:
|
|
199
|
+
return
|
|
200
|
+
self._last = current
|
|
201
|
+
self._last_emit = now
|
|
202
|
+
self.emit({"label": self.label, "phase": phase, **current})
|
|
203
|
+
|
|
204
|
+
def _run(self) -> None:
|
|
205
|
+
while not self._stop.wait(self.interval):
|
|
206
|
+
try:
|
|
207
|
+
self.snapshot()
|
|
208
|
+
except Exception: # Progress is best effort; never break the download.
|
|
209
|
+
pass
|
|
210
|
+
|
|
211
|
+
def __enter__(self) -> "ProgressMonitor":
|
|
212
|
+
self.snapshot(force=True)
|
|
213
|
+
self._thread = threading.Thread(target=self._run, name="tt-progress", daemon=True)
|
|
214
|
+
self._thread.start()
|
|
215
|
+
return self
|
|
216
|
+
|
|
217
|
+
def __exit__(self, *_: object) -> None:
|
|
218
|
+
self._stop.set()
|
|
219
|
+
if self._thread:
|
|
220
|
+
self._thread.join(timeout=2)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def with_retries(
|
|
224
|
+
action: Callable[[], Any],
|
|
225
|
+
*,
|
|
226
|
+
description: str,
|
|
227
|
+
max_attempts: int,
|
|
228
|
+
sleep: Callable[[float], None] | None = None,
|
|
229
|
+
log: Callable[[str], None] = write_line,
|
|
230
|
+
) -> Any:
|
|
231
|
+
for attempt in range(1, max_attempts + 1):
|
|
232
|
+
try:
|
|
233
|
+
return action()
|
|
234
|
+
except Exception as error: # noqa: BLE001 - classified below
|
|
235
|
+
if attempt >= max_attempts or not is_retryable(error):
|
|
236
|
+
raise
|
|
237
|
+
delay = backoff_seconds(attempt)
|
|
238
|
+
log(
|
|
239
|
+
f"{description} interrupted ({type(error).__name__}: {error}). "
|
|
240
|
+
f"Retrying in {delay:.0f}s (attempt {attempt + 1}/{max_attempts}); "
|
|
241
|
+
"completed files are kept in the cache."
|
|
242
|
+
)
|
|
243
|
+
(sleep or time.sleep)(delay)
|
|
244
|
+
raise AssertionError("unreachable")
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def verify_blob(path: Path) -> bool:
|
|
248
|
+
"""Check a cached file against its content-addressed blob name.
|
|
249
|
+
|
|
250
|
+
Returns True when the blob name is a recognized digest and it matches;
|
|
251
|
+
False when the name carries no digest (for example a cache without
|
|
252
|
+
symlinks). Raises ValueError on a mismatch.
|
|
253
|
+
"""
|
|
254
|
+
return verify_blob_digest(path)[0]
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def verify_blob_digest(path: Path) -> tuple[bool, str]:
|
|
258
|
+
"""Like verify_blob, also returning the file's SHA-256 from the same read."""
|
|
259
|
+
blob = path.resolve()
|
|
260
|
+
expected = blob.name.lower()
|
|
261
|
+
sha256 = hashlib.sha256()
|
|
262
|
+
git_sha1 = hashlib.sha1(f"blob {blob.stat().st_size}\0".encode())
|
|
263
|
+
with blob.open("rb") as source:
|
|
264
|
+
for chunk in iter(lambda: source.read(8 * 1024 * 1024), b""):
|
|
265
|
+
sha256.update(chunk)
|
|
266
|
+
git_sha1.update(chunk)
|
|
267
|
+
digests = {64: sha256.hexdigest(), 40: git_sha1.hexdigest()}
|
|
268
|
+
if len(expected) in digests and all(character in "0123456789abcdef" for character in expected):
|
|
269
|
+
if digests[len(expected)] != expected:
|
|
270
|
+
raise ValueError(f"Cached Hugging Face blob checksum mismatch: {path}")
|
|
271
|
+
return True, digests[64]
|
|
272
|
+
return False, digests[64]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def download_snapshot(
|
|
276
|
+
*,
|
|
277
|
+
repo_id: str,
|
|
278
|
+
repo_type: str,
|
|
279
|
+
revision: str | None,
|
|
280
|
+
allow_patterns: list[str] | None = None,
|
|
281
|
+
ignore_patterns: list[str] | None = None,
|
|
282
|
+
local_files_only: bool = False,
|
|
283
|
+
label: str,
|
|
284
|
+
) -> tuple[str, str]:
|
|
285
|
+
"""Download (or verify locally) one snapshot. Returns (path, commit sha)."""
|
|
286
|
+
from huggingface_hub import constants, snapshot_download
|
|
287
|
+
|
|
288
|
+
token = os.getenv("HF_TOKEN")
|
|
289
|
+
if local_files_only:
|
|
290
|
+
path = snapshot_download(
|
|
291
|
+
repo_id=repo_id,
|
|
292
|
+
repo_type=repo_type,
|
|
293
|
+
revision=revision,
|
|
294
|
+
token=token,
|
|
295
|
+
allow_patterns=allow_patterns,
|
|
296
|
+
ignore_patterns=ignore_patterns,
|
|
297
|
+
local_files_only=True,
|
|
298
|
+
)
|
|
299
|
+
return path, Path(path).name
|
|
300
|
+
|
|
301
|
+
if revision and COMMIT.fullmatch(revision):
|
|
302
|
+
# A pinned commit that is already fully cached needs no network, so
|
|
303
|
+
# prefetch keeps working offline. huggingface_hub checks completeness
|
|
304
|
+
# against its cached tree listing and raises if files are missing.
|
|
305
|
+
try:
|
|
306
|
+
path = snapshot_download(
|
|
307
|
+
repo_id=repo_id,
|
|
308
|
+
repo_type=repo_type,
|
|
309
|
+
revision=revision,
|
|
310
|
+
token=token,
|
|
311
|
+
allow_patterns=allow_patterns,
|
|
312
|
+
ignore_patterns=ignore_patterns,
|
|
313
|
+
local_files_only=True,
|
|
314
|
+
)
|
|
315
|
+
write_line(f"{label}: {repo_id}@{revision} is already cached; no download needed.")
|
|
316
|
+
return path, revision
|
|
317
|
+
except Exception: # noqa: BLE001 - not cached or incomplete: download below
|
|
318
|
+
pass
|
|
319
|
+
|
|
320
|
+
from huggingface_hub import HfApi
|
|
321
|
+
from huggingface_hub.utils import filter_repo_objects
|
|
322
|
+
|
|
323
|
+
max_attempts = max_attempts_from_env()
|
|
324
|
+
api = HfApi(token=token)
|
|
325
|
+
info = with_retries(
|
|
326
|
+
lambda: api.repo_info(repo_id, repo_type=repo_type, revision=revision, files_metadata=True),
|
|
327
|
+
description=f"Resolving {repo_id}",
|
|
328
|
+
max_attempts=max_attempts,
|
|
329
|
+
)
|
|
330
|
+
commit = str(info.sha)
|
|
331
|
+
siblings = list(filter_repo_objects(
|
|
332
|
+
info.siblings or [],
|
|
333
|
+
allow_patterns=allow_patterns,
|
|
334
|
+
ignore_patterns=ignore_patterns,
|
|
335
|
+
key=lambda sibling: sibling.rfilename,
|
|
336
|
+
))
|
|
337
|
+
planned = plan_files(siblings)
|
|
338
|
+
blobs = Path(constants.HF_HUB_CACHE) / repo_folder_name(repo_id, repo_type) / "blobs"
|
|
339
|
+
blobs.mkdir(parents=True, exist_ok=True)
|
|
340
|
+
total = sum(item.size for item in planned)
|
|
341
|
+
write_line(
|
|
342
|
+
f"{label}: {repo_id}@{commit} — {len(planned)} file(s), {total} bytes. "
|
|
343
|
+
"Interrupted downloads can be restarted; completed files are reused."
|
|
344
|
+
)
|
|
345
|
+
monitor = ProgressMonitor(blobs, planned, label)
|
|
346
|
+
with monitor:
|
|
347
|
+
def attempt() -> str:
|
|
348
|
+
path = snapshot_download(
|
|
349
|
+
repo_id=repo_id,
|
|
350
|
+
repo_type=repo_type,
|
|
351
|
+
revision=commit,
|
|
352
|
+
token=token,
|
|
353
|
+
allow_patterns=allow_patterns,
|
|
354
|
+
ignore_patterns=ignore_patterns,
|
|
355
|
+
)
|
|
356
|
+
# If the Hub becomes unreachable mid-way, huggingface_hub may hand
|
|
357
|
+
# back the partial cached folder; treat that as a retryable failure.
|
|
358
|
+
missing = [sibling.rfilename for sibling in siblings if not (Path(path) / sibling.rfilename).exists()]
|
|
359
|
+
if missing:
|
|
360
|
+
raise IncompleteDownloadError(f"{len(missing)} file(s) still missing, e.g. {missing[0]}")
|
|
361
|
+
return path
|
|
362
|
+
|
|
363
|
+
path = with_retries(attempt, description=f"Downloading {repo_id}", max_attempts=max_attempts)
|
|
364
|
+
monitor.snapshot(force=True, phase="downloaded")
|
|
365
|
+
if revision and revision != commit:
|
|
366
|
+
# Downloading by commit skips huggingface_hub's ref bookkeeping. Record
|
|
367
|
+
# the branch or tag -> commit mapping it would have written, so later
|
|
368
|
+
# offline resolution of an unpinned revision finds this snapshot.
|
|
369
|
+
write_ref(Path(constants.HF_HUB_CACHE) / repo_folder_name(repo_id, repo_type), revision, commit)
|
|
370
|
+
elif not revision:
|
|
371
|
+
write_ref(Path(constants.HF_HUB_CACHE) / repo_folder_name(repo_id, repo_type), "main", commit)
|
|
372
|
+
return path, commit
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def write_ref(repository: Path, revision: str, commit: str) -> None:
|
|
376
|
+
if revision.lower() == commit.lower():
|
|
377
|
+
return
|
|
378
|
+
ref = repository / "refs" / revision
|
|
379
|
+
if repository.resolve() not in ref.resolve().parents:
|
|
380
|
+
raise ValueError(f"Invalid Hugging Face revision name: {revision}")
|
|
381
|
+
ref.parent.mkdir(parents=True, exist_ok=True)
|
|
382
|
+
temporary = ref.with_name(f".{ref.name}.{os.getpid()}.tmp")
|
|
383
|
+
temporary.write_text(commit, encoding="utf-8")
|
|
384
|
+
os.replace(temporary, ref)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from typing import Any
|
|
4
5
|
|
|
5
6
|
|
|
@@ -15,6 +16,10 @@ NEMOTRON_BASE_MODEL_REVISION = "ce38b6ab8b252b4b8ee7165b4605e93191cafd73"
|
|
|
15
16
|
# Immutable Hugging Face revision reviewed and certified for Muse Glimmer.
|
|
16
17
|
MUSE_GLIMMER_BASE_MODEL_REVISION = "a4e59da52a7bc87ae7251dd5545c0dd437c44b68"
|
|
17
18
|
CERTIFIED_BASE_MODELS = (CERTIFIED_BASE_MODEL, NEMOTRON_BASE_MODEL, MUSE_GLIMMER_BASE_MODEL)
|
|
19
|
+
# Any other Hugging Face repo may be trained as an uncertified causal LM when
|
|
20
|
+
# the run pins an immutable commit. Node enforces the same contract.
|
|
21
|
+
HUGGING_FACE_REPO_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,95}/[A-Za-z0-9][A-Za-z0-9._-]{0,95}")
|
|
22
|
+
COMMIT_SHA = re.compile(r"[0-9a-fA-F]{40}")
|
|
18
23
|
CERTIFIED_QWEN_TEXT_CONFIG = {
|
|
19
24
|
"model_type": "qwen3_5_text",
|
|
20
25
|
"hidden_size": 2048,
|
|
@@ -58,6 +63,9 @@ def assert_certified_model_config(
|
|
|
58
63
|
label: str = "base-model config",
|
|
59
64
|
expected_model_id: str | None = None,
|
|
60
65
|
) -> None:
|
|
66
|
+
if expected_model_id and not is_certified_base_model(expected_model_id):
|
|
67
|
+
# No reviewed architecture exists for an uncertified model.
|
|
68
|
+
return
|
|
61
69
|
model_type = _field(value, "model_type")
|
|
62
70
|
architectures = _field(value, "architectures")
|
|
63
71
|
|
|
@@ -126,17 +134,30 @@ def assert_certified_model_config(
|
|
|
126
134
|
)
|
|
127
135
|
|
|
128
136
|
|
|
137
|
+
def is_certified_base_model(model_id: str) -> bool:
|
|
138
|
+
return model_id in CERTIFIED_BASE_MODELS
|
|
139
|
+
|
|
140
|
+
|
|
129
141
|
def assert_certified_base_model(model_id: str, label: str = "base model") -> None:
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
142
|
+
"""Accept a certified model or an uncertified Hugging Face repo id."""
|
|
143
|
+
if is_certified_base_model(model_id) or HUGGING_FACE_REPO_ID.fullmatch(model_id):
|
|
144
|
+
return
|
|
145
|
+
supported = ", ".join(CERTIFIED_BASE_MODELS)
|
|
146
|
+
raise ValueError(f"{label} must be one of {supported} or a Hugging Face repo id; got {model_id!r}")
|
|
133
147
|
|
|
134
148
|
|
|
135
|
-
def assert_certified_base_model_revision(
|
|
149
|
+
def assert_certified_base_model_revision(
|
|
150
|
+
base_model: str,
|
|
151
|
+
revision: str | None,
|
|
152
|
+
label: str = "base model revision",
|
|
153
|
+
*,
|
|
154
|
+
local_source: bool = False,
|
|
155
|
+
) -> None:
|
|
136
156
|
"""Require the certified immutable revision for pinned training models.
|
|
137
157
|
|
|
138
158
|
Qwen, Nemotron, and Muse Glimmer loads are bound here; local snapshot
|
|
139
|
-
contents are additionally verified by the Node runtime.
|
|
159
|
+
contents are additionally verified by the Node runtime. An uncertified
|
|
160
|
+
model must pin a commit SHA unless it loads from a local directory.
|
|
140
161
|
"""
|
|
141
162
|
expected = {
|
|
142
163
|
CERTIFIED_BASE_MODEL: QWEN_BASE_MODEL_REVISION,
|
|
@@ -144,6 +165,13 @@ def assert_certified_base_model_revision(base_model: str, revision: str | None,
|
|
|
144
165
|
MUSE_GLIMMER_BASE_MODEL: MUSE_GLIMMER_BASE_MODEL_REVISION,
|
|
145
166
|
}.get(base_model)
|
|
146
167
|
if expected is None:
|
|
168
|
+
if revision is None and local_source:
|
|
169
|
+
return
|
|
170
|
+
if revision is None or not COMMIT_SHA.fullmatch(str(revision)):
|
|
171
|
+
raise ValueError(
|
|
172
|
+
f"{label} must be a 40-character commit SHA for uncertified base model {base_model}; "
|
|
173
|
+
f"got {revision!r}"
|
|
174
|
+
)
|
|
147
175
|
return
|
|
148
176
|
if revision is None:
|
|
149
177
|
raise ValueError(f"{label} is required for {base_model}")
|
|
@@ -167,3 +195,8 @@ def chat_template_kwargs(model_id: str) -> dict[str, Any]:
|
|
|
167
195
|
if model_id == NEMOTRON_BASE_MODEL:
|
|
168
196
|
return {"enable_thinking": False}
|
|
169
197
|
return {}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def uncertified_load_kwargs(model_id: str) -> dict[str, Any]:
|
|
201
|
+
"""Refuse pickle weights for models TT has not reviewed."""
|
|
202
|
+
return {} if is_certified_base_model(model_id) else {"use_safetensors": True}
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import hashlib
|
|
3
2
|
import json
|
|
4
3
|
import os
|
|
5
4
|
import re
|
|
6
5
|
from pathlib import Path
|
|
7
6
|
from typing import Any
|
|
8
7
|
|
|
8
|
+
from hub_download import configure_download_environment, download_snapshot, verify_blob
|
|
9
9
|
from model_contract import (
|
|
10
10
|
assert_certified_base_model,
|
|
11
11
|
assert_certified_base_model_revision,
|
|
12
12
|
assert_certified_model_config,
|
|
13
|
+
is_certified_base_model,
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
ALLOW_PATTERNS = [
|
|
@@ -76,15 +77,6 @@ def configure_hugging_face_cache(cache_home: str | None) -> None:
|
|
|
76
77
|
os.environ.pop(deprecated, None)
|
|
77
78
|
|
|
78
79
|
|
|
79
|
-
def hash_file(path: Path, algorithm: str, prefix: bytes = b"") -> str:
|
|
80
|
-
digest = hashlib.new(algorithm)
|
|
81
|
-
digest.update(prefix)
|
|
82
|
-
with path.open("rb") as source:
|
|
83
|
-
for chunk in iter(lambda: source.read(8 * 1024 * 1024), b""):
|
|
84
|
-
digest.update(chunk)
|
|
85
|
-
return digest.hexdigest()
|
|
86
|
-
|
|
87
|
-
|
|
88
80
|
def verify_snapshot(snapshot: Path, expected_model_id: str | None = None) -> tuple[list[Path], int]:
|
|
89
81
|
config = snapshot / "config.json"
|
|
90
82
|
if not config.is_file() or config.stat().st_size == 0:
|
|
@@ -99,8 +91,15 @@ def verify_snapshot(snapshot: Path, expected_model_id: str | None = None) -> tup
|
|
|
99
91
|
except (OSError, json.JSONDecodeError) as exc:
|
|
100
92
|
raise ValueError(f"Cached snapshot has an invalid config.json: {snapshot}") from exc
|
|
101
93
|
|
|
94
|
+
if expected_model_id and not is_certified_base_model(expected_model_id):
|
|
95
|
+
if not any(path.stat().st_size > 0 for path in snapshot.glob("*.safetensors")):
|
|
96
|
+
raise ValueError("Uncertified base models require non-empty safetensors weights; pickle-only checkpoints are unsupported")
|
|
97
|
+
|
|
102
98
|
weight_files = sorted(snapshot.glob("*.safetensors")) + sorted(snapshot.glob("pytorch_model*.bin"))
|
|
103
99
|
index_files = sorted(snapshot.glob("*.safetensors.index.json")) + sorted(snapshot.glob("pytorch_model*.bin.index.json"))
|
|
100
|
+
# Keep lexical snapshot names: normal HF file symlinks target sibling blobs.
|
|
101
|
+
files = [path for path in snapshot.rglob("*") if path.is_file()]
|
|
102
|
+
inventory = set(files)
|
|
104
103
|
for index in index_files:
|
|
105
104
|
try:
|
|
106
105
|
weight_map = json.loads(index.read_text(encoding="utf-8")).get("weight_map", {})
|
|
@@ -108,8 +107,13 @@ def verify_snapshot(snapshot: Path, expected_model_id: str | None = None) -> tup
|
|
|
108
107
|
raise ValueError(f"Cached snapshot has an invalid weight index: {index}") from exc
|
|
109
108
|
if not isinstance(weight_map, dict) or not weight_map:
|
|
110
109
|
raise ValueError(f"Cached snapshot has an empty weight index: {index}")
|
|
111
|
-
for name in
|
|
112
|
-
if not isinstance(name, str) or not
|
|
110
|
+
for name in weight_map.values():
|
|
111
|
+
if (not isinstance(name, str) or not name
|
|
112
|
+
or re.match(r"^(?:[A-Za-z]:|[/\\])", name)
|
|
113
|
+
or ".." in re.split(r"[/\\]", name)):
|
|
114
|
+
raise ValueError("Cached snapshot has an invalid indexed weight shard path")
|
|
115
|
+
shard = snapshot / name
|
|
116
|
+
if shard not in inventory or shard.stat().st_size == 0:
|
|
113
117
|
raise ValueError(f"Cached snapshot is missing an indexed weight shard: {name}")
|
|
114
118
|
if not weight_files:
|
|
115
119
|
raise ValueError(f"Cached snapshot contains no Transformers model weights: {snapshot}")
|
|
@@ -129,20 +133,7 @@ def verify_snapshot(snapshot: Path, expected_model_id: str | None = None) -> tup
|
|
|
129
133
|
if not tokenizer_config.is_file() or not any(path.is_file() and path.stat().st_size > 0 for path in vocabulary):
|
|
130
134
|
raise ValueError(f"Cached snapshot is missing tokenizer metadata or vocabulary files: {snapshot}")
|
|
131
135
|
|
|
132
|
-
|
|
133
|
-
verified_blobs = 0
|
|
134
|
-
for path in files:
|
|
135
|
-
blob = path.resolve()
|
|
136
|
-
expected = blob.name.lower()
|
|
137
|
-
if len(expected) == 64 and all(character in "0123456789abcdef" for character in expected):
|
|
138
|
-
actual = hash_file(blob, "sha256")
|
|
139
|
-
elif len(expected) == 40 and all(character in "0123456789abcdef" for character in expected):
|
|
140
|
-
actual = hash_file(blob, "sha1", f"blob {blob.stat().st_size}\0".encode())
|
|
141
|
-
else:
|
|
142
|
-
continue
|
|
143
|
-
if actual != expected:
|
|
144
|
-
raise ValueError(f"Cached Hugging Face blob checksum mismatch: {path}")
|
|
145
|
-
verified_blobs += 1
|
|
136
|
+
verified_blobs = sum(1 for path in files if verify_blob(path))
|
|
146
137
|
return files, verified_blobs
|
|
147
138
|
|
|
148
139
|
|
|
@@ -166,17 +157,20 @@ def main() -> None:
|
|
|
166
157
|
|
|
167
158
|
# Import only after the cache environment is final. huggingface_hub reads
|
|
168
159
|
# these variables while importing its constants module.
|
|
169
|
-
|
|
160
|
+
configure_download_environment()
|
|
161
|
+
from huggingface_hub import constants
|
|
170
162
|
|
|
171
|
-
|
|
163
|
+
local_only = bool(payload.get("local_files_only", False))
|
|
164
|
+
action = "Verifying cached" if local_only else "Prefetching"
|
|
172
165
|
print(f"{action} {base_model} in {constants.HF_HUB_CACHE}...", flush=True)
|
|
173
|
-
snapshot_path =
|
|
166
|
+
snapshot_path, _ = download_snapshot(
|
|
174
167
|
repo_id=base_model,
|
|
168
|
+
repo_type="model",
|
|
175
169
|
revision=payload.get("revision"),
|
|
176
|
-
token=os.getenv("HF_TOKEN"),
|
|
177
170
|
allow_patterns=ALLOW_PATTERNS,
|
|
178
|
-
ignore_patterns=IGNORE_PATTERNS,
|
|
179
|
-
local_files_only=
|
|
171
|
+
ignore_patterns=IGNORE_PATTERNS + ([] if is_certified_base_model(base_model) else ["pytorch_model*.bin"]),
|
|
172
|
+
local_files_only=local_only,
|
|
173
|
+
label="model_prefetch",
|
|
180
174
|
)
|
|
181
175
|
snapshot = Path(snapshot_path)
|
|
182
176
|
snapshot_revision = snapshot.name
|