review round: pointer written+read-back before prune, verify retried, latest rolled to final, stale-pointer scan on resume, ddp_timeout 4h, chunked batched commits, manifest self-consistency gate, full content verification after upload
Browse files- train/hubckpt.py +64 -18
train/hubckpt.py
CHANGED
|
@@ -211,8 +211,29 @@ def ensure_repo(repo, api, token=None, repo_type="dataset"):
|
|
| 211 |
"files_before": len(have)}
|
| 212 |
|
| 213 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
def push_and_prune(repo, dirpath, path_in_repo, api, repo_type="dataset", token=None,
|
| 215 |
-
prune=True, min_free_gb=6.0):
|
| 216 |
"""One full §3.13 cycle. Raises if verification fails and pruning was requested -- the local copy is
|
| 217 |
kept in exactly that case, because the Hub copy is not yet proven to exist."""
|
| 218 |
files = sorted(os.listdir(dirpath))
|
|
@@ -224,26 +245,28 @@ def push_and_prune(repo, dirpath, path_in_repo, api, repo_type="dataset", token=
|
|
| 224 |
up = api.upload_folder(repo_id=repo, folder_path=dirpath, path_in_repo=path_in_repo,
|
| 225 |
repo_type=repo_type, commit_message=f"checkpoint {path_in_repo}",
|
| 226 |
token=token)
|
| 227 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
res = {"uploaded_to": getattr(up, "commit_url", str(up)[:120]), "verify": ver,
|
| 229 |
-
"free_before_gb": round(free_before / 1e9, 2)
|
|
|
|
| 230 |
if prune:
|
| 231 |
if not ver["ok"]:
|
| 232 |
-
res["pruned"] = False
|
| 233 |
res["prune_blocked_reason"] = (
|
| 234 |
f"hub verification failed ({len(ver['missing'])} missing, "
|
| 235 |
f"{len(ver['mismatch'])} mismatched) -- keeping the local copy in {dirpath}")
|
| 236 |
raise RuntimeError(res["prune_blocked_reason"])
|
| 237 |
-
|
| 238 |
-
res["pruned"] = True
|
| 239 |
-
res["local_dir_exists_after"] = os.path.exists(dirpath)
|
| 240 |
-
res["free_after_gb"] = round(free_bytes() / 1e9, 2)
|
| 241 |
-
res["disk_ok"] = res["free_after_gb"] >= min_free_gb
|
| 242 |
-
if prune and not res["disk_ok"]:
|
| 243 |
-
# Free space is a training-blocking metric (§3.13), not a footnote: say so loudly here rather
|
| 244 |
-
# than letting the next checkpoint fail with an OSError.
|
| 245 |
-
res["disk_warning"] = (f"only {res['free_after_gb']} GB free after pruning "
|
| 246 |
-
f"(floor {min_free_gb} GB)")
|
| 247 |
return res
|
| 248 |
|
| 249 |
|
|
@@ -311,10 +334,33 @@ def download_checkpoint(repo, path_in_repo, dest, api, repo_type="dataset", toke
|
|
| 311 |
"dir": dirs.pop(), "requested_dest": dest, "path_in_repo": path_in_repo}
|
| 312 |
|
| 313 |
|
| 314 |
-
def write_latest_pointer(repo, cur, api, repo_type="dataset", token=None):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
p = os.path.join("/tmp", "latest.json")
|
| 316 |
with open(p, "w") as f:
|
| 317 |
json.dump(cur, f, indent=1, sort_keys=True)
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
"files_before": len(have)}
|
| 212 |
|
| 213 |
|
| 214 |
+
def prune_local(dirpath, res, min_free_gb=6.0):
|
| 215 |
+
"""Delete a local checkpoint whose Hub copy has been verified, and record the space that freed.
|
| 216 |
+
|
| 217 |
+
Separate from `push_and_prune` because the caller has to roll `latest.json` and read it back *between*
|
| 218 |
+
the verification and the deletion: pruning first would destroy the only copy the pointer should be
|
| 219 |
+
naming, and a pointer commit that failed silently would then be unrecoverable.
|
| 220 |
+
"""
|
| 221 |
+
shutil.rmtree(dirpath, ignore_errors=True)
|
| 222 |
+
res["pruned"] = True
|
| 223 |
+
res["local_dir_exists_after"] = os.path.exists(dirpath)
|
| 224 |
+
res["free_after_gb"] = round(free_bytes() / 1e9, 2)
|
| 225 |
+
res["disk_ok"] = res["free_after_gb"] >= min_free_gb
|
| 226 |
+
if not res["disk_ok"]:
|
| 227 |
+
# Free space is a training-blocking metric (§3.13), not a footnote: say so loudly here rather
|
| 228 |
+
# than letting the next checkpoint fail with an OSError.
|
| 229 |
+
res["disk_warning"] = (f"only {res['free_after_gb']} GB free after pruning "
|
| 230 |
+
f"(floor {min_free_gb} GB)")
|
| 231 |
+
print(f" DISK WARNING: {res['disk_warning']}", flush=True)
|
| 232 |
+
return res
|
| 233 |
+
|
| 234 |
+
|
| 235 |
def push_and_prune(repo, dirpath, path_in_repo, api, repo_type="dataset", token=None,
|
| 236 |
+
prune=True, min_free_gb=6.0, verify_tries=3):
|
| 237 |
"""One full §3.13 cycle. Raises if verification fails and pruning was requested -- the local copy is
|
| 238 |
kept in exactly that case, because the Hub copy is not yet proven to exist."""
|
| 239 |
files = sorted(os.listdir(dirpath))
|
|
|
|
| 245 |
up = api.upload_folder(repo_id=repo, folder_path=dirpath, path_in_repo=path_in_repo,
|
| 246 |
repo_type=repo_type, commit_message=f"checkpoint {path_in_repo}",
|
| 247 |
token=token)
|
| 248 |
+
# Retry the verification, not the upload. `verify_checkpoint` reads the tree immediately after the
|
| 249 |
+
# commit, and E-033 showed this Hub path answers transiently under load; treating a propagation race
|
| 250 |
+
# as a content problem would kill a billed session over a checkpoint that is actually fine.
|
| 251 |
+
ver = None
|
| 252 |
+
for attempt in range(1, verify_tries + 1):
|
| 253 |
+
ver = verify_checkpoint(repo, dirpath, path_in_repo, token=token, repo_type=repo_type)
|
| 254 |
+
if ver["ok"]:
|
| 255 |
+
break
|
| 256 |
+
if attempt < verify_tries:
|
| 257 |
+
print(f" verify attempt {attempt}: {len(ver['missing'])} missing, "
|
| 258 |
+
f"{len(ver['mismatch'])} mismatched -- re-checking in {20 * attempt}s", flush=True)
|
| 259 |
+
__import__("time").sleep(20 * attempt)
|
| 260 |
res = {"uploaded_to": getattr(up, "commit_url", str(up)[:120]), "verify": ver,
|
| 261 |
+
"free_before_gb": round(free_before / 1e9, 2), "pruned": False,
|
| 262 |
+
"free_after_gb": round(free_bytes() / 1e9, 2)}
|
| 263 |
if prune:
|
| 264 |
if not ver["ok"]:
|
|
|
|
| 265 |
res["prune_blocked_reason"] = (
|
| 266 |
f"hub verification failed ({len(ver['missing'])} missing, "
|
| 267 |
f"{len(ver['mismatch'])} mismatched) -- keeping the local copy in {dirpath}")
|
| 268 |
raise RuntimeError(res["prune_blocked_reason"])
|
| 269 |
+
prune_local(dirpath, res, min_free_gb)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 270 |
return res
|
| 271 |
|
| 272 |
|
|
|
|
| 334 |
"dir": dirs.pop(), "requested_dest": dest, "path_in_repo": path_in_repo}
|
| 335 |
|
| 336 |
|
| 337 |
+
def write_latest_pointer(repo, cur, api, repo_type="dataset", token=None, retries=3):
|
| 338 |
+
"""Roll `latest.json` and prove the Hub now serves it.
|
| 339 |
+
|
| 340 |
+
The read-back is not ceremony: the caller prunes its local copy immediately afterwards, so a pointer
|
| 341 |
+
commit that silently failed would leave a Hub-verified checkpoint that nothing points at, and the next
|
| 342 |
+
session would resume from the previous one and re-read ~100 M tokens with every self-consistency guard
|
| 343 |
+
passing (weights, cursor, fingerprint and seed all agree with each other -- only the pointer is wrong).
|
| 344 |
+
"""
|
| 345 |
p = os.path.join("/tmp", "latest.json")
|
| 346 |
with open(p, "w") as f:
|
| 347 |
json.dump(cur, f, indent=1, sort_keys=True)
|
| 348 |
+
want_step = cur.get("step")
|
| 349 |
+
last = None
|
| 350 |
+
for attempt in range(1, retries + 1):
|
| 351 |
+
try:
|
| 352 |
+
api.upload_file(path_or_fileobj=p, path_in_repo="latest.json", repo_id=repo,
|
| 353 |
+
repo_type=repo_type, commit_message=f"latest -> {want_step}", token=token)
|
| 354 |
+
except Exception as e:
|
| 355 |
+
last = f"upload {type(e).__name__}: {str(e)[:120]}"
|
| 356 |
+
else:
|
| 357 |
+
back = latest_pointer(repo, repo_type=repo_type, token=token)
|
| 358 |
+
if back.get("step") == want_step and back.get("path_in_repo") == cur.get("path_in_repo"):
|
| 359 |
+
return p
|
| 360 |
+
last = f"hub reports {back.get('step')!r} at {back.get('path_in_repo')!r}, " \
|
| 361 |
+
f"we wrote {want_step!r} at {cur.get('path_in_repo')!r} ({str(back)[:120]})"
|
| 362 |
+
print(f" latest.json attempt {attempt} failed: {last}; retrying", flush=True)
|
| 363 |
+
__import__("time").sleep(15 * attempt)
|
| 364 |
+
raise RuntimeError(f"latest.json never landed at step {want_step} on {repo} after {retries} "
|
| 365 |
+
f"attempts: {last}. Refusing to prune the local copy that this pointer should "
|
| 366 |
+
"point at.")
|