colibri/models: don't false-flag a complete HF model as "incomplete"

hf leaves a stale <sha>.<etag>.incomplete orphan in blobs/ after a resumed or
retried chunk even though the real file finished — so "download complete" and the
model page disagreed (settings said done, the page showed  incomplete).

- model list: an .incomplete file now counts as an unfinished download ONLY when its
  final blob (<sha>) is absent; an orphan whose final blob exists is ignored. Fixes
  the false badge for ALL HF models, not just colibri.
- colibri catalog: _colibri_repo_complete now tests for dangling snapshot symlinks
  (the true "file missing" signal) instead of .incomplete presence, so the settings
  "present" indicator matches reality.
- colibri catalog label → the real size (~429 GB / 400 GiB).
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoSpEthysqmseCc6Geizty
parent 1b7187d4
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
# Canonical product version for CoderAI — single source of truth. Both the API # Canonical product version for CoderAI — single source of truth. Both the API
# metadata and the admin web UI read from here. # metadata and the admin web UI read from here.
__version__ = "0.1.70" __version__ = "0.1.71"
# Configure the CUDA caching allocator BEFORE torch is imported anywhere. # Configure the CUDA caching allocator BEFORE torch is imported anywhere.
# expandable_segments lets the allocator return freed pages to the driver even # expandable_segments lets the allocator return freed pages to the driver even
......
...@@ -863,8 +863,8 @@ async def api_ds4_default_models(username: str = Depends(require_admin)): ...@@ -863,8 +863,8 @@ async def api_ds4_default_models(username: str = Depends(require_admin)):
_COLIBRI_DEFAULT_MODELS = [ _COLIBRI_DEFAULT_MODELS = [
{"key": "glm-5.2-int4-g64-mtp", {"key": "glm-5.2-int4-g64-mtp",
"repo": "mastouri/GLM-5.2-colibri-int4-g64-with-int8-mtp", "repo": "mastouri/GLM-5.2-colibri-int4-g64-with-int8-mtp",
"label": "GLM-5.2 int4 g64 + int8-MTP (~372 GB) — the colibri container", "label": "GLM-5.2 int4 g64 + int8-MTP (~429 GB / 400 GiB) — the colibri container",
"size_gb": 372}, "size_gb": 429},
] ]
...@@ -881,16 +881,32 @@ async def api_colibri_default_models(username: str = Depends(require_admin)): ...@@ -881,16 +881,32 @@ async def api_colibri_default_models(username: str = Depends(require_admin)):
pass pass
out = [] out = []
for m in _COLIBRI_DEFAULT_MODELS: for m in _COLIBRI_DEFAULT_MODELS:
present = False out.append({**m, "present": _colibri_repo_complete(m["repo"])})
try:
from codai.backends.colibri import ColibriBackend
present = bool(ColibriBackend._hf_snapshot_dir(m["repo"]))
except Exception:
present = False
out.append({**m, "present": present})
return {"models": out, "model_id": model_id} return {"models": out, "model_id": model_id}
def _colibri_repo_complete(repo: str) -> bool:
"""True when the HF repo is fully downloaded — the snapshot resolves AND every
file symlink points to an existing blob (no dangling links). This is hf's own
notion of "cached". Stale ``.incomplete`` temp files whose final blob already
exists are IGNORED: hf leaves them behind after a resumed/retried chunk, so their
mere presence does NOT mean the model is unfinished (a missing final blob shows up
as a dangling snapshot symlink, which is what we actually check)."""
try:
from codai.backends.colibri import ColibriBackend
snap = ColibriBackend._hf_snapshot_dir(repo)
if not snap or not os.path.isdir(snap):
return False
for root, _dirs, files in os.walk(snap):
for n in files:
p = os.path.join(root, n)
if os.path.islink(p) and not os.path.exists(p):
return False # dangling symlink = a file not yet downloaded
return True
except Exception:
return False
def _cancel_download_session(session_id: str) -> bool: def _cancel_download_session(session_id: str) -> bool:
"""Cancel an active download by flagging the session and terminating its worker """Cancel an active download by flagging the session and terminating its worker
process. Returns False if there is no such download session. process. Returns False if there is no such download session.
...@@ -1197,16 +1213,31 @@ def _scan_caches() -> dict: ...@@ -1197,16 +1213,31 @@ def _scan_caches() -> dict:
if rid: if rid:
incomplete_repos.add(str(rid)) incomplete_repos.add(str(rid))
# Also scan each repo's blobs directory for .incomplete marker files # Also scan each repo's blobs directory for .incomplete marker files
# (used by some huggingface_hub versions for in-progress downloads). # (used by some huggingface_hub versions for in-progress downloads). A
# `.incomplete` file is only evidence of an UNFINISHED download when its
# final blob is absent — hf leaves a stale `.incomplete` orphan behind
# after a resumed/retried chunk even though the real file completed, so
# counting those would flag a fully-downloaded model as incomplete.
try: try:
for _repo_entry in os.scandir(hf_dir): for _repo_entry in os.scandir(hf_dir):
if not _repo_entry.is_dir() or not _repo_entry.name.startswith('models--'): if not _repo_entry.is_dir() or not _repo_entry.name.startswith('models--'):
continue continue
_blobs = os.path.join(_repo_entry.path, 'blobs') _blobs = os.path.join(_repo_entry.path, 'blobs')
if os.path.isdir(_blobs) and any( if not os.path.isdir(_blobs):
n.endswith('.incomplete') or n.endswith('.lock') continue
for n in os.listdir(_blobs) _names = os.listdir(_blobs)
): _finals = set(_names)
_genuine = False
for n in _names:
if n.endswith('.lock'):
continue # transient/orphaned lock, not a partial
if n.endswith('.incomplete'):
# <sha>.<etag>.incomplete → final blob is <sha>; if that
# blob exists this is a harmless orphan, else a real partial.
if n.split('.', 1)[0] not in _finals:
_genuine = True
break
if _genuine:
_rid = _repo_entry.name[len('models--'):].replace('--', '/', 1) _rid = _repo_entry.name[len('models--'):].replace('--', '/', 1)
incomplete_repos.add(_rid) incomplete_repos.add(_rid)
except Exception: except Exception:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment