File size: 5,453 Bytes
ede84b4 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 | """
core/model_lock.py — ล็อก revision ของโมเดลจาก HF Hub (28 ต.ค.)
================================================================
models.yaml (รากรีโป · สร้างด้วย models_lock.py จากแคชบนเครื่องที่ผ่านชุดทดสอบแล้ว)
models:
jinaai/jina-embeddings-v3:
revision: 0123abcd… # commit เต็ม 40 ตัว
ไม่มีไฟล์ หรือไม่มีชื่อโมเดลในไฟล์ → from_pretrained แบบเดิม (ไม่ส่ง revision) พฤติกรรมเท่าเดิมทุกอย่าง
มีไฟล์ → ส่ง revision=<commit> ทุกครั้ง Space จะโหลดตัวเดียวกับที่ทดสอบบนเครื่องเสมอ แม้เจ้าของโมเดลอัปเดต main
LOADED เก็บ commit ที่โหลดจริง (config._commit_hash) ไว้ให้ /health/detail เทียบกับที่ล็อก
ไม่ import transformers — ใช้ได้ในเทสที่ไม่มีโมเดล
"""
from __future__ import annotations
import os
import re
from typing import Any, Optional
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
_SHA = re.compile(r"^[0-9a-f]{40}$")
LOADED: dict[str, Optional[str]] = {} # model_id → commit ที่โหลดจริง (None = โฟลเดอร์ในเครื่อง/ไม่ทราบ)
_cache: Optional[dict] = None
def lock_path() -> str:
return os.environ.get("MODELS_LOCK") or os.path.join(BASE_DIR, "models.yaml")
class ModelLockError(ValueError):
pass
def load(path: Optional[str] = None, refresh: bool = False) -> dict:
"""{model_id: {"revision": sha, ...}} · ไม่มีไฟล์ = {} · ไฟล์ผิดรูป = หยุด (ไม่เดาว่าตั้งใจล็อกอะไร)"""
global _cache
if _cache is not None and not refresh and path is None:
return _cache
p = path or lock_path()
if not os.path.isfile(p):
out: dict = {}
else:
import yaml
with open(p, encoding="utf-8") as f:
raw = yaml.safe_load(f) or {}
models = raw.get("models") if isinstance(raw, dict) else None
if not isinstance(models, dict):
raise ModelLockError(f"{os.path.basename(p)}: ต้องมีหัวข้อ models:")
out = {}
for mid, spec in models.items():
rev = str((spec or {}).get("revision") or "").strip() if isinstance(spec, dict) else ""
if not _SHA.match(rev):
raise ModelLockError(f"{os.path.basename(p)} › {mid}: revision ต้องเป็น commit 40 ตัว (ได้ '{rev}')")
out[str(mid)] = dict(spec, revision=rev)
if path is None:
_cache = out
return out
def pin(model_id: str) -> dict:
"""kwargs สำหรับ from_pretrained — {} เมื่อไม่ได้ล็อก (เท่าเดิม)"""
spec = load().get(model_id)
return {"revision": spec["revision"]} if spec else {}
def commit_from_cache(model_id: str, revision: Optional[str] = None,
cache_dir: Optional[str] = None) -> Optional[str]:
"""commit จากพาธ snapshot ในแคช HF (…/snapshots/<commit>/config.json)
ใช้เมื่อ config._commit_hash ว่าง — เช่น SiglipVisionModel ที่ดึงเฉพาะ vision_config ออกมา (28 ต.ค.)"""
try:
from huggingface_hub import try_to_load_from_cache
p = try_to_load_from_cache(model_id, "config.json", revision=revision, cache_dir=cache_dir)
except Exception: # noqa: BLE001
return None
if not isinstance(p, str):
return None
parts = os.path.normpath(p).split(os.sep)
if "snapshots" in parts:
i = parts.index("snapshots")
if i + 1 < len(parts) and _SHA.match(parts[i + 1]):
return parts[i + 1]
return None
def note(model_id: str, model: Any) -> None:
"""บันทึก commit ที่โหลดจริง · โหลดซ้ำหลายตัว (processor + model) ใช้ค่าล่าสุดที่ไม่ใช่ None"""
commit = getattr(getattr(model, "config", None), "_commit_hash", None)
if not commit and "/" in model_id and not os.path.isdir(model_id):
commit = commit_from_cache(model_id, pin(model_id).get("revision"))
if commit or model_id not in LOADED:
LOADED[model_id] = commit if isinstance(commit, str) else None
def status() -> dict:
"""ต่อโมเดล: ที่ล็อก · ที่โหลดจริง · ตรงไหม (None = เทียบไม่ได้)"""
locked = load()
out = {}
for mid in sorted(set(locked) | set(LOADED)):
want = (locked.get(mid) or {}).get("revision")
got = LOADED.get(mid)
out[mid] = {"locked": want, "loaded": got,
"match": (want == got) if (want and got) else None}
return out
|