aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/backends/common.py92
-rw-r--r--app/backends/qwen.py69
2 files changed, 107 insertions, 54 deletions
diff --git a/app/backends/common.py b/app/backends/common.py
index a12a366..7974b58 100644
--- a/app/backends/common.py
+++ b/app/backends/common.py
@@ -11,6 +11,7 @@ run.
import os
import re
+import shutil
import sys
import time
import urllib.parse
@@ -743,3 +744,94 @@ def pip_uninstall(packages: List[str], *, emit=None,
"""
from backends import envs
return envs.pip_uninstall(packages, emit=emit, env_dir=env_dir)
+
+
+# ----------------------------------------------------------------------
+# HuggingFace hub cache
+#
+# Every pip-based backend's model weights are fetched by its server into
+# the standard hub cache on first start (and pre-downloaded by the
+# Install action via the venv's hf CLI). These helpers read and delete
+# the same directories ``from_pretrained`` writes, honoring the same
+# environment overrides, so per-model (un)installs land exactly where a
+# server start would look. The cache is shared across backends: a repo
+# two backends host exists once, and its deletion affects both — the
+# same convention every backend here accepts.
+# ----------------------------------------------------------------------
+
+def hf_cache_dir() -> Path:
+ """The HF hub cache dir servers fetch model weights into.
+
+ Resolution mirrors huggingface_hub.constants: HF_HUB_CACHE beats
+ HUGGINGFACE_HUB_CACHE beats HF_HOME/hub beats ~/.cache/huggingface/hub.
+ """
+ override = os.environ.get("HF_HUB_CACHE") or os.environ.get(
+ "HUGGINGFACE_HUB_CACHE")
+ if override:
+ return Path(override)
+ home = os.environ.get("HF_HOME")
+ if home:
+ return Path(home) / "hub"
+ return Path.home() / ".cache" / "huggingface" / "hub"
+
+
+def hf_repo_dir(repo_id: str) -> Path:
+ """The cache directory HF keeps REPO_ID's weights in."""
+ return hf_cache_dir() / ("models--" + repo_id.replace("/", "--"))
+
+
+def hf_tree_has_file(path: Path) -> bool:
+ """True when any file or symlink exists under PATH (recursively)."""
+ try:
+ for item in path.iterdir():
+ # Snapshot files are symlinks into blobs/; count them even
+ # when temporarily broken (presence is what the loader
+ # checks).
+ if item.is_symlink() or item.is_file():
+ return True
+ if item.is_dir() and hf_tree_has_file(item):
+ return True
+ except OSError:
+ return False
+ return False
+
+
+def hf_delete_model_weights(repo_ids: List[str]) -> int:
+ """Delete the cached HF weight dirs of REPO_IDS; returns how many
+ were present and removed.
+
+ Best-effort rmtree of each ``models--<org>--<name>`` directory; only
+ those directories are ever touched — the rest of the HF cache may be
+ shared with unrelated tools. Prints its progress, which streams into
+ the task view under curses too.
+ """
+ removed = 0
+ for repo_id in repo_ids:
+ directory = hf_repo_dir(repo_id)
+ if not directory.is_dir():
+ continue
+ print(f"[INFO] Removing cached {repo_id} weights...")
+ shutil.rmtree(directory, ignore_errors=True)
+ if directory.exists():
+ print(f"[WARNING] Could not fully remove {directory}")
+ continue
+ removed += 1
+ if removed:
+ print(f"[OK] Deleted cached weights for {removed} "
+ f"{'model' if removed == 1 else 'models'}.")
+ return removed
+
+
+def hf_download_prefix(env_dir: Path) -> Optional[List[str]]:
+ """The venv ENV_DIR's hf CLI argv prefix (None when absent).
+
+ The hf CLI (or its older ``huggingface-cli`` name) is what a server
+ start uses to fetch weights, so the install action pre-downloads
+ through it too — resumable, streamed, cancelable.
+ """
+ from backends import envs
+ for name in ("hf", "huggingface-cli"):
+ candidate = envs.env_script(name, env_dir)
+ if candidate.is_file():
+ return [str(candidate)]
+ return None
diff --git a/app/backends/qwen.py b/app/backends/qwen.py
index c099c84..45b691b 100644
--- a/app/backends/qwen.py
+++ b/app/backends/qwen.py
@@ -25,8 +25,6 @@ Usage:
"""
import argparse
-import os
-import shutil
import sys
from pathlib import Path
from typing import List, Optional
@@ -81,25 +79,15 @@ QWEN_SPEAKERS = QWEN3_TTS_SPEAKERS
# -- HuggingFace weight cache ------------------------------------------------
#
# The demo servers pull each model's snapshot into huggingface_hub's default
-# hub cache on first start (nothing in this project redirects it). These
-# helpers read and delete the same directories ``from_pretrained`` writes,
-# honoring the same environment overrides, so per-model (un)installs land
-# exactly where a server start would look.
+# hub cache on first start (nothing in this project redirects it). The
+# cache primitives are shared with the sglang-omni backend (which fetches
+# the same repos into the same cache) in backends.common; the wrappers
+# keep the module-level names internal callers (and the tests) reference.
def _hf_cache_dir() -> Path:
- """The HF hub cache dir servers fetch model weights into.
-
- Resolution mirrors huggingface_hub.constants: HF_HUB_CACHE beats
- HUGGINGFACE_HUB_CACHE beats HF_HOME/hub beats ~/.cache/huggingface/hub.
- """
- override = os.environ.get("HF_HUB_CACHE") or os.environ.get(
- "HUGGINGFACE_HUB_CACHE")
- if override:
- return Path(override)
- home = os.environ.get("HF_HOME")
- if home:
- return Path(home) / "hub"
- return Path.home() / ".cache" / "huggingface" / "hub"
+ """The HF hub cache dir the servers fetch model weights into;
+ common.hf_cache_dir resolves the environment overrides."""
+ return common.hf_cache_dir()
def repo_dir(repo_id: str) -> Path:
@@ -114,17 +102,7 @@ def model_repo_dir(model: str) -> Path:
def _tree_has_file(path: Path) -> bool:
"""True when any file or symlink exists under PATH (recursively)."""
- try:
- for item in path.iterdir():
- # Snapshot files are symlinks into blobs/; count them even when
- # temporarily broken (presence is what the loader checks).
- if item.is_symlink() or item.is_file():
- return True
- if item.is_dir() and _tree_has_file(item):
- return True
- except OSError:
- return False
- return False
+ return common.hf_tree_has_file(path)
def model_installed(model: str) -> bool:
@@ -148,27 +126,14 @@ def installed_models() -> List[str]:
def delete_model_weights(models: Optional[List[str]] = None) -> int:
"""Delete the cached HF weight dirs of MODELS (every model by default).
- Best-effort rmtree of each ``models--Qwen--…`` directory; returns how
- many were present and removed. Only those directories are ever touched —
- the rest of the HF cache may be shared with unrelated tools. Prints its
- progress, which streams into the task view under curses too.
+ Delegates to common.hf_delete_model_weights: best-effort rmtree of
+ each ``models--Qwen--…`` directory; only those directories are ever
+ touched — the rest of the HF cache may be shared with unrelated tools.
+ Returns how many were present and removed.
"""
names = sorted(MODEL_REPOS) if models is None else list(models)
- removed = 0
- for name in names:
- directory = model_repo_dir(name)
- if not directory.is_dir():
- continue
- print(f"[INFO] Removing cached {MODEL_REPOS[name]} weights...")
- shutil.rmtree(directory)
- if directory.exists():
- print(f"[WARNING] Could not fully remove {directory}")
- continue
- removed += 1
- if removed:
- print(f"[OK] Deleted cached weights for {removed} "
- f"{'model' if removed == 1 else 'models'}.")
- return removed
+ return common.hf_delete_model_weights([MODEL_REPOS[name]
+ for name in names])
def _is_installed() -> bool:
@@ -381,11 +346,7 @@ def _detect_remote(managed: bool = False):
def _hf_download_prefix() -> Optional[List[str]]:
"""The qwen env's hf CLI argv prefix (None when neither script is present)."""
- for name in ("hf", "huggingface-cli"):
- candidate = envs.env_script(name, QWEN_ENV)
- if candidate.is_file():
- return [str(candidate)]
- return None
+ return common.hf_download_prefix(QWEN_ENV)
def install_model(model: str, *, emit=None, cancel=None) -> int: