mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-31 02:02:22 +01:00
Merge pull request #6930 from redpersongpt/codex/stale-hf-cache-markers
fix(cookbook): ignore empty markers beside completed blobs
This commit is contained in:
@@ -14,6 +14,7 @@ from fastapi import HTTPException
|
|||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from routes._validators import validate_remote_host, validate_ssh_port
|
from routes._validators import validate_remote_host, validate_ssh_port
|
||||||
|
from routes.cookbook_output import HF_PARTIAL_BLOB_HELPER
|
||||||
from core.platform_compat import _ssh_exec_argv
|
from core.platform_compat import _ssh_exec_argv
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -378,6 +379,7 @@ def _cached_model_scan_script(model_dirs: list[str] | None = None, add_hf_cache:
|
|||||||
"""
|
"""
|
||||||
lines = [
|
lines = [
|
||||||
"import json, os, re, shutil, subprocess, urllib.request",
|
"import json, os, re, shutil, subprocess, urllib.request",
|
||||||
|
*HF_PARTIAL_BLOB_HELPER.splitlines(),
|
||||||
"models = []",
|
"models = []",
|
||||||
"seen = set()",
|
"seen = set()",
|
||||||
"BLOCKED_ROOTS = ('/sys', '/proc', '/dev', '/run', '/var/run')",
|
"BLOCKED_ROOTS = ('/sys', '/proc', '/dev', '/run', '/var/run')",
|
||||||
@@ -436,8 +438,10 @@ def _cached_model_scan_script(model_dirs: list[str] | None = None, add_hf_cache:
|
|||||||
" sz, nf, ic = 0, 0, False",
|
" sz, nf, ic = 0, 0, False",
|
||||||
" if os.path.isdir(blobs):",
|
" if os.path.isdir(blobs):",
|
||||||
" for f in os.scandir(blobs):",
|
" for f in os.scandir(blobs):",
|
||||||
|
" if f.name.endswith('.incomplete'):",
|
||||||
|
" if not is_partial_blob(f.path): continue",
|
||||||
|
" ic = True",
|
||||||
" if f.is_file(): nf += 1; sz += f.stat().st_size",
|
" if f.is_file(): nf += 1; sz += f.stat().st_size",
|
||||||
" if f.name.endswith('.incomplete'): ic = True",
|
|
||||||
" snap = os.path.join(cache, d, 'snapshots')",
|
" snap = os.path.join(cache, d, 'snapshots')",
|
||||||
" def snapshot_size():",
|
" def snapshot_size():",
|
||||||
" total, count, incomplete = 0, 0, False",
|
" total, count, incomplete = 0, 0, False",
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ import re
|
|||||||
|
|
||||||
_FETCHING_ZERO_FILES_RE = re.compile(r"Fetching\s+0\s+files", re.IGNORECASE)
|
_FETCHING_ZERO_FILES_RE = re.compile(r"Fetching\s+0\s+files", re.IGNORECASE)
|
||||||
|
|
||||||
|
# Shared source embedded in local/remote inventory and status probes. A blank
|
||||||
|
# retry marker beside its completed, nonempty content-addressed blob is stale;
|
||||||
|
# a blank marker alone may be a download that has not received its first byte.
|
||||||
|
HF_PARTIAL_BLOB_HELPER = """def is_partial_blob(path):
|
||||||
|
if not path.endswith('.incomplete'):
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
completed = path[:-len('.incomplete')]
|
||||||
|
return (os.path.getsize(path) != 0 or not os.path.isfile(completed)
|
||||||
|
or os.path.getsize(completed) == 0)
|
||||||
|
except OSError:
|
||||||
|
return True
|
||||||
|
"""
|
||||||
|
|
||||||
# Probe scripts for the dead-session download check, run as
|
# Probe scripts for the dead-session download check, run as
|
||||||
# `python3 -c <PROBE> <repo_id> <cache_root>` (locally or over SSH).
|
# `python3 -c <PROBE> <repo_id> <cache_root>` (locally or over SSH).
|
||||||
# cache_root is the task's custom download dir, '' for the default HF cache.
|
# cache_root is the task's custom download dir, '' for the default HF cache.
|
||||||
@@ -15,7 +29,7 @@ _FETCHING_ZERO_FILES_RE = re.compile(r"Fetching\s+0\s+files", re.IGNORECASE)
|
|||||||
# HF_HOME=<local_dir>, so that task's cache lives under <local_dir>/hub, and
|
# HF_HOME=<local_dir>, so that task's cache lives under <local_dir>/hub, and
|
||||||
# the probe process's own environment knows nothing about it.
|
# the probe process's own environment knows nothing about it.
|
||||||
HF_CACHE_COMPLETE_PROBE = (
|
HF_CACHE_COMPLETE_PROBE = (
|
||||||
"import os,sys;"
|
"import os,sys\n" + HF_PARTIAL_BLOB_HELPER +
|
||||||
"repo=sys.argv[1];"
|
"repo=sys.argv[1];"
|
||||||
"root=os.path.expanduser(sys.argv[2]) if len(sys.argv)>2 and sys.argv[2] else '';"
|
"root=os.path.expanduser(sys.argv[2]) if len(sys.argv)>2 and sys.argv[2] else '';"
|
||||||
"base=os.path.join(root,'hub') if root else (os.environ.get('HUGGINGFACE_HUB_CACHE') or os.path.join(os.environ.get('HF_HOME', os.path.expanduser('~/.cache/huggingface')), 'hub'));"
|
"base=os.path.join(root,'hub') if root else (os.environ.get('HUGGINGFACE_HUB_CACHE') or os.path.join(os.environ.get('HF_HOME', os.path.expanduser('~/.cache/huggingface')), 'hub'));"
|
||||||
@@ -24,18 +38,18 @@ HF_CACHE_COMPLETE_PROBE = (
|
|||||||
"ok=os.path.isdir(snap) and any(os.path.isdir(os.path.join(snap,x)) and os.listdir(os.path.join(snap,x)) for x in os.listdir(snap));"
|
"ok=os.path.isdir(snap) and any(os.path.isdir(os.path.join(snap,x)) and os.listdir(os.path.join(snap,x)) for x in os.listdir(snap));"
|
||||||
"inc=False;"
|
"inc=False;"
|
||||||
"blobs=os.path.join(d,'blobs');"
|
"blobs=os.path.join(d,'blobs');"
|
||||||
"inc=os.path.isdir(blobs) and any(x.endswith('.incomplete') for x in os.listdir(blobs));"
|
"inc=os.path.isdir(blobs) and any(is_partial_blob(os.path.join(blobs,x)) for x in os.listdir(blobs));"
|
||||||
"sys.exit(0 if ok and not inc else 1)"
|
"sys.exit(0 if ok and not inc else 1)"
|
||||||
)
|
)
|
||||||
|
|
||||||
HF_CACHE_INCOMPLETE_PROBE = (
|
HF_CACHE_INCOMPLETE_PROBE = (
|
||||||
"import os,sys;"
|
"import os,sys\n" + HF_PARTIAL_BLOB_HELPER +
|
||||||
"repo=sys.argv[1];"
|
"repo=sys.argv[1];"
|
||||||
"root=os.path.expanduser(sys.argv[2]) if len(sys.argv)>2 and sys.argv[2] else '';"
|
"root=os.path.expanduser(sys.argv[2]) if len(sys.argv)>2 and sys.argv[2] else '';"
|
||||||
"base=os.path.join(root,'hub') if root else (os.environ.get('HUGGINGFACE_HUB_CACHE') or os.path.join(os.environ.get('HF_HOME', os.path.expanduser('~/.cache/huggingface')), 'hub'));"
|
"base=os.path.join(root,'hub') if root else (os.environ.get('HUGGINGFACE_HUB_CACHE') or os.path.join(os.environ.get('HF_HOME', os.path.expanduser('~/.cache/huggingface')), 'hub'));"
|
||||||
"d=os.path.join(base,'models--'+repo.replace('/','--'));"
|
"d=os.path.join(base,'models--'+repo.replace('/','--'));"
|
||||||
"blobs=os.path.join(d,'blobs');"
|
"blobs=os.path.join(d,'blobs');"
|
||||||
"inc=os.path.isdir(blobs) and any(x.endswith('.incomplete') for x in os.listdir(blobs));"
|
"inc=os.path.isdir(blobs) and any(is_partial_blob(os.path.join(blobs,x)) for x in os.listdir(blobs));"
|
||||||
"sys.exit(0 if inc else 1)"
|
"sys.exit(0 if inc else 1)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4302,7 +4302,7 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
tmux output can stop at a stale progress line if the pane/session
|
tmux output can stop at a stale progress line if the pane/session
|
||||||
disappears before Cookbook captures the final DOWNLOAD_OK marker.
|
disappears before Cookbook captures the final DOWNLOAD_OK marker.
|
||||||
In that case, trust the cache shape: a snapshot directory with files
|
In that case, trust the cache shape: a snapshot directory with files
|
||||||
and no *.incomplete blobs means HuggingFace finished materializing the
|
and no unfinished blobs means HuggingFace finished materializing the
|
||||||
model. cache_root is the task's custom download dir — the runner
|
model. cache_root is the task's custom download dir — the runner
|
||||||
pointed HF_HOME there, so the cache lives under <cache_root>/hub,
|
pointed HF_HOME there, so the cache lives under <cache_root>/hub,
|
||||||
not wherever this probe's environment says.
|
not wherever this probe's environment says.
|
||||||
@@ -4327,7 +4327,8 @@ def setup_cookbook_routes() -> APIRouter:
|
|||||||
"""Best-effort check for resumable HF partial blobs.
|
"""Best-effort check for resumable HF partial blobs.
|
||||||
|
|
||||||
A lost SSH/tmux session can leave a real download still incomplete.
|
A lost SSH/tmux session can leave a real download still incomplete.
|
||||||
Treat any *.incomplete blob as stronger evidence than stale
|
Treat unfinished blobs (excluding empty markers paired with a
|
||||||
|
completed blob) as stronger evidence than stale
|
||||||
"100%" lines in the captured pane output.
|
"100%" lines in the captured pane output.
|
||||||
"""
|
"""
|
||||||
if not repo_id or "/" not in repo_id:
|
if not repo_id or "/" not in repo_id:
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""A completed blob's empty retry marker must not keep its model stalled."""
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from routes.cookbook_helpers import _cached_model_scan_script
|
||||||
|
from routes.cookbook_output import HF_CACHE_COMPLETE_PROBE, HF_CACHE_INCOMPLETE_PROBE
|
||||||
|
|
||||||
|
_REPO = "fixture/stale-marker"
|
||||||
|
_STATES = ["complete", "stale", "empty-unpaired", "partial", "partial-paired", "empty-paired-empty"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=["custom", "hf-home", "hub-env"])
|
||||||
|
def cache(request, tmp_path):
|
||||||
|
root = tmp_path / "cache with spaces"
|
||||||
|
model = root / "hub" / ("models--" + _REPO.replace("/", "--"))
|
||||||
|
blobs = model / "blobs"
|
||||||
|
blobs.mkdir(parents=True)
|
||||||
|
snapshot = model / "snapshots" / "revision"
|
||||||
|
snapshot.mkdir(parents=True)
|
||||||
|
(snapshot / "config.json").write_text("{}")
|
||||||
|
env = {key: value for key, value in os.environ.items()
|
||||||
|
if key not in {"HF_HOME", "HUGGINGFACE_HUB_CACHE", "HF_HUB_CACHE"}}
|
||||||
|
if request.param == "custom":
|
||||||
|
argument = str(root)
|
||||||
|
scan = _cached_model_scan_script(add_hf_cache=str(root / "hub"))
|
||||||
|
else:
|
||||||
|
argument = ""
|
||||||
|
env["HF_HOME" if request.param == "hf-home" else "HUGGINGFACE_HUB_CACHE"] = str(
|
||||||
|
root if request.param == "hf-home" else root / "hub"
|
||||||
|
)
|
||||||
|
scan = _cached_model_scan_script()
|
||||||
|
return blobs, argument, env, scan
|
||||||
|
|
||||||
|
|
||||||
|
def _seed(blobs, state):
|
||||||
|
if state in {"complete", "stale", "partial-paired"}:
|
||||||
|
(blobs / "sha").write_bytes(b"completed model data")
|
||||||
|
elif state == "empty-paired-empty":
|
||||||
|
(blobs / "sha").touch()
|
||||||
|
if state != "complete":
|
||||||
|
(blobs / "sha.incomplete").write_bytes(b"partial" if state.startswith("partial") else b"")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("state", _STATES)
|
||||||
|
@pytest.mark.parametrize("probe", [HF_CACHE_COMPLETE_PROBE, HF_CACHE_INCOMPLETE_PROBE], ids=["complete", "incomplete"])
|
||||||
|
def test_cache_status_probes_distinguish_stale_and_partial(cache, state, probe):
|
||||||
|
blobs, argument, env, _ = cache
|
||||||
|
_seed(blobs, state)
|
||||||
|
before = {path.name: path.read_bytes() for path in blobs.iterdir()}
|
||||||
|
result = subprocess.run([sys.executable, "-c", probe, _REPO, argument],
|
||||||
|
env=env, capture_output=True, text=True, timeout=15)
|
||||||
|
complete = state in {"complete", "stale"}
|
||||||
|
expected = complete if probe == HF_CACHE_COMPLETE_PROBE else not complete
|
||||||
|
assert result.returncode == (0 if expected else 1), result.stderr
|
||||||
|
assert {path.name: path.read_bytes() for path in blobs.iterdir()} == before
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("state", _STATES)
|
||||||
|
def test_generated_inventory_agrees_without_mutating_cache(cache, state):
|
||||||
|
blobs, _, env, scan = cache
|
||||||
|
_seed(blobs, state)
|
||||||
|
before = {path.name: path.read_bytes() for path in blobs.iterdir()}
|
||||||
|
# Inventory's unrelated Ollama discovery stays off the network. Execute
|
||||||
|
# the actual generated scanner, including its cache-root resolution.
|
||||||
|
harness = (
|
||||||
|
"import urllib.request,shutil\n"
|
||||||
|
"def offline(*args,**kwargs): raise OSError('Ollama outside this fixture')\n"
|
||||||
|
"urllib.request.urlopen=offline\nshutil.which=lambda name: None\n"
|
||||||
|
+ scan
|
||||||
|
)
|
||||||
|
result = subprocess.run([sys.executable, "-c", harness], env=env,
|
||||||
|
capture_output=True, text=True, timeout=15)
|
||||||
|
assert result.returncode == 0, result.stderr
|
||||||
|
row = next(item for item in json.loads(result.stdout) if item["repo_id"] == _REPO)
|
||||||
|
assert row["has_incomplete"] == (state not in {"complete", "stale"})
|
||||||
|
if state in {"complete", "stale"}:
|
||||||
|
assert row["nb_files"] == 1
|
||||||
|
assert row["size_bytes"] == len(b"completed model data")
|
||||||
|
assert {path.name: path.read_bytes() for path in blobs.iterdir()} == before
|
||||||
Reference in New Issue
Block a user