Skip to content

Commit 161e408

Browse files
authored
Merge pull request #2 from lapy/refactor/simplify
Finish refactoring
2 parents 8324326 + 7edaf6f commit 161e408

40 files changed

Lines changed: 4331 additions & 3789 deletions

‎Dockerfile‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,8 @@ ENV DEBIAN_FRONTEND=noninteractive \
8181
CUDA_VISIBLE_DEVICES=all \
8282
NVIDIA_VISIBLE_DEVICES=all \
8383
NVIDIA_DRIVER_CAPABILITIES=compute,utility \
84-
HF_HOME=/app/data/temp/.cache/huggingface \
85-
HUGGINGFACE_HUB_CACHE=/app/data/temp/.cache/huggingface/hub \
84+
HF_HOME=/app/data/hf-cache \
85+
HUGGINGFACE_HUB_CACHE=/app/data/hf-cache/hub \
8686
VENV_PATH=/opt/venv \
8787
PYTHONPATH=/app \
8888
PATH="/app/data/cuda/current/bin:${PATH}" \
@@ -133,7 +133,7 @@ RUN curl -fsSL "https://github.com/Kitware/CMake/releases/download/v${CMAKE_VERS
133133
&& cmake --version
134134

135135
# Install llama-swap binary
136-
ARG LLAMA_SWAP_VERSION=179
136+
ARG LLAMA_SWAP_VERSION=197
137137
RUN curl -fsSL "https://github.com/mostlygeek/llama-swap/releases/download/v${LLAMA_SWAP_VERSION}/llama-swap_${LLAMA_SWAP_VERSION}_linux_amd64.tar.gz" -o /tmp/llama-swap.tar.gz && \
138138
tar -xzf /tmp/llama-swap.tar.gz -C /tmp && \
139139
mv /tmp/llama-swap /usr/local/bin/llama-swap && \
@@ -168,7 +168,7 @@ RUN ln -sf /usr/bin/python3 /usr/bin/python
168168

169169
# Create non-root user and data directory structure
170170
RUN useradd -m -s /bin/bash appuser && \
171-
mkdir -p /app/data/models /app/data/config /app/data/configs /app/data/logs /app/data/llama-cpp /app/data/temp/.cache/huggingface/hub && \
171+
mkdir -p /app/data/models /app/data/config /app/data/configs /app/data/logs /app/data/llama-cpp /app/data/hf-cache/hub && \
172172
chown -R appuser:appuser /app && \
173173
# Ensure entrypoint script is accessible to appuser
174174
chmod 755 /usr/local/bin/docker-entrypoint.sh

‎README.md‎

Lines changed: 376 additions & 402 deletions
Large diffs are not rendered by default.

‎backend/data_store.py‎

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
"""YAML-backed data store replacing SQLite."""
22

3+
import json
34
import os
5+
import re
46
import threading
57
from typing import Any, Dict, List, Optional
68

@@ -31,6 +33,59 @@ def generate_proxy_name(huggingface_id: str, quantization: Optional[str] = None)
3133
return huggingface_slug
3234

3335

36+
def _coerce_config(config_value: Optional[Any]) -> Dict[str, Any]:
37+
if not config_value:
38+
return {}
39+
if isinstance(config_value, dict):
40+
return config_value
41+
if isinstance(config_value, str):
42+
try:
43+
return json.loads(config_value)
44+
except json.JSONDecodeError:
45+
return {}
46+
return {}
47+
48+
49+
def _model_value(model: Any, key: str, default: Any = None) -> Any:
50+
if isinstance(model, dict):
51+
return model.get(key, default)
52+
return getattr(model, key, default)
53+
54+
55+
def normalize_proxy_alias(alias: Optional[str]) -> str:
56+
"""Normalize a user-provided model alias into a safe exposed engine ID."""
57+
if alias is None:
58+
return ""
59+
60+
normalized = str(alias).strip().lower()
61+
if not normalized:
62+
return ""
63+
64+
normalized = normalized.replace("/", "-").replace("\\", "-")
65+
normalized = re.sub(r"\s+", "-", normalized)
66+
normalized = re.sub(r"[^a-z0-9._-]", "-", normalized)
67+
normalized = re.sub(r"-{2,}", "-", normalized)
68+
normalized = normalized.strip("._-")
69+
return normalized
70+
71+
72+
def resolve_proxy_name(model: Any) -> str:
73+
"""Return the exposed runtime model ID for a stored model."""
74+
config = _coerce_config(_model_value(model, "config"))
75+
alias = normalize_proxy_alias(config.get("model_alias"))
76+
if alias:
77+
return alias
78+
79+
existing = normalize_proxy_alias(_model_value(model, "proxy_name"))
80+
if existing:
81+
return existing
82+
83+
return generate_proxy_name(
84+
_model_value(model, "huggingface_id", ""),
85+
_model_value(model, "quantization"),
86+
)
87+
88+
3489
class DataStore:
3590
"""Thread-safe YAML-backed data store replacing SQLite."""
3691

@@ -175,6 +230,23 @@ def delete_engine_version(self, engine: str, version: str) -> bool:
175230
self._save_yaml("engines.yaml", data)
176231
return True
177232

233+
def get_engine_build_settings(self, engine: str) -> Dict[str, Any]:
234+
"""Return persisted build settings for the given engine (or empty dict)."""
235+
data = self._read_yaml("engines.yaml")
236+
return data.get(engine, {}).get("build_settings", {}) or {}
237+
238+
def update_engine_build_settings(self, engine: str, settings: Dict[str, Any]) -> Dict[str, Any]:
239+
"""Merge and persist build settings for the given engine. Returns the stored settings."""
240+
if not isinstance(settings, dict):
241+
settings = {}
242+
data = self._read_yaml("engines.yaml")
243+
engine_data = data.setdefault(engine, {})
244+
existing = engine_data.get("build_settings") or {}
245+
merged = {**existing, **settings}
246+
engine_data["build_settings"] = merged
247+
self._save_yaml("engines.yaml", data)
248+
return merged
249+
178250
# --- LMDeploy ---
179251

180252
def get_lmdeploy_status(self) -> dict:
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"global": {
3+
"context_length": {
4+
"preferred_keys": [
5+
"general.context_length",
6+
"general.model_max_length",
7+
"general.max_position_embeddings"
8+
]
9+
}
10+
},
11+
"glm4": {
12+
"match_arch": ["glm4", "glm4moe"],
13+
"context_length": {
14+
"preferred_keys": ["glm4.context_length", "glm4.model_max_length"],
15+
"fallback_terms": ["context", "max_position_embeddings"]
16+
},
17+
"layer_count": {
18+
"preferred_keys": ["glm4.num_hidden_layers"],
19+
"fallback_terms": ["layer", "block"]
20+
}
21+
}
22+
}
23+

‎backend/gguf_reader.py‎

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
GGUF file metadata reader for extracting model layer information
33
"""
44

5-
import struct
65
import os
6+
import struct
77
import mmap
88
from enum import IntEnum
9-
from typing import Dict, Optional, Any, List, Tuple, BinaryIO
9+
from typing import Any, BinaryIO, Dict, List, Optional, Tuple
1010

1111
from backend.logging_config import get_logger
12+
from backend.model_introspection import GgufIntrospector
1213

1314
logger = get_logger(__name__)
1415

@@ -1247,22 +1248,34 @@ def get_model_layer_info(model_path: str) -> Optional[Dict[str, Any]]:
12471248
logger.error(f"Model file is not GGUF format: {model_path}")
12481249
return None
12491250

1250-
metadata = read_gguf_metadata(model_path)
1251-
if metadata:
1252-
return {
1253-
"layer_count": metadata["layer_count"],
1254-
"architecture": metadata["architecture"],
1255-
"context_length": metadata["context_length"],
1256-
"vocab_size": 0, # Not extracted from metadata
1257-
"embedding_length": metadata["embedding_length"],
1258-
"attention_head_count": metadata["attention_head_count"],
1259-
"attention_head_count_kv": metadata["attention_head_count_kv"],
1260-
"block_count": metadata["block_count"],
1261-
"is_moe": metadata["is_moe"],
1262-
"expert_count": metadata["expert_count"],
1263-
"experts_used_count": metadata["experts_used_count"],
1264-
}
1265-
return None
1251+
with GGUFReader(model_path) as reader:
1252+
metadata = reader.metadata
1253+
tensors = reader.tensors
1254+
1255+
introspector = GgufIntrospector(metadata=metadata, tensors=tensors)
1256+
info = introspector.build_model_info()
1257+
1258+
return {
1259+
"layer_count": int(info.layer_count) if info.layer_count else 0,
1260+
"architecture": metadata.get("general.architecture", ""),
1261+
"context_length": int(info.context_length) if info.context_length else 0,
1262+
"vocab_size": int(info.vocab_size) if info.vocab_size else 0,
1263+
"embedding_length": int(info.embedding_length)
1264+
if info.embedding_length
1265+
else 0,
1266+
"attention_head_count": int(info.attention_head_count)
1267+
if info.attention_head_count
1268+
else 0,
1269+
"attention_head_count_kv": int(info.attention_head_count_kv)
1270+
if info.attention_head_count_kv
1271+
else 0,
1272+
"block_count": int(info.block_count) if info.block_count else 0,
1273+
"is_moe": bool(info.is_moe),
1274+
"expert_count": int(info.expert_count) if info.expert_count else 0,
1275+
"experts_used_count": int(info.experts_used_count)
1276+
if info.experts_used_count
1277+
else 0,
1278+
}
12661279
except Exception as e:
12671280
logger.error(
12681281
f"Failed to get model layer info from {model_path}: {e}", exc_info=True

‎backend/huggingface.py‎

Lines changed: 29 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -863,9 +863,6 @@ async def _fetch_and_merge(repo_id: Optional[str]):
863863
metadata["tokenizer"] = tokenizer_json
864864

865865
await _fetch_and_merge(huggingface_id)
866-
if huggingface_id and huggingface_id.lower().endswith("-gguf"):
867-
base_repo = huggingface_id[:-5]
868-
await _fetch_and_merge(base_repo)
869866

870867
try:
871868
layer_info = get_model_layer_info(file_path) or {}
@@ -1210,6 +1207,20 @@ async def process_model(model):
12101207
if result is not None:
12111208
valid_results.append(result)
12121209

1210+
if model_format == "gguf":
1211+
def _gguf_sort_key(item: Dict[str, Any]):
1212+
quantizations = item.get("quantizations") or {}
1213+
size_candidates = [
1214+
q.get("total_size") or 0
1215+
for q in quantizations.values()
1216+
if isinstance(q, dict)
1217+
]
1218+
positive_sizes = [size for size in size_candidates if size > 0]
1219+
min_size = min(positive_sizes) if positive_sizes else float("inf")
1220+
return (min_size, -(item.get("downloads") or 0), item.get("id") or "")
1221+
1222+
valid_results.sort(key=_gguf_sort_key)
1223+
12131224
return valid_results[:limit]
12141225

12151226

@@ -1219,27 +1230,33 @@ async def _process_single_model(model, model_format: str) -> Optional[Dict]:
12191230
logger.info(f"Processing model: {model.id}")
12201231

12211232
quantizations: Dict[str, Dict] = {}
1233+
mmproj_files: List[Dict[str, Any]] = []
12221234
safetensors_files: List[Dict] = []
12231235
repo_files: List[Dict[str, Any]] = []
12241236

12251237
if hasattr(model, "siblings") and model.siblings:
12261238
if model_format == "gguf":
1227-
# Group GGUF files by logical quantization, handling multi-part shards
1228-
# Accept both plain `.gguf` and multi-part patterns like `.gguf.part1of2`
1229-
# Exclude mmproj (vision/multimodal projection) files – they are extensions, not standalone quants
1239+
# Group GGUF files by logical quantization, handling multi-part shards.
12301240
gguf_siblings = [
12311241
s
12321242
for s in model.siblings
12331243
if isinstance(getattr(s, "rfilename", None), str)
12341244
and re.search(r"\.gguf(\.|$)", s.rfilename)
1235-
and "mmproj" not in s.rfilename.lower()
12361245
]
12371246
logger.info(f"Model {model.id}: {len(gguf_siblings)} GGUF files found")
12381247
if not gguf_siblings:
12391248
return None
12401249

12411250
for sibling in gguf_siblings:
12421251
filename = sibling.rfilename
1252+
if "mmproj" in filename.lower():
1253+
mmproj_files.append(
1254+
{
1255+
"filename": filename,
1256+
"size": getattr(sibling, "size", 0) or 0,
1257+
}
1258+
)
1259+
continue
12431260
# Normalize filename by stripping shard suffix patterns like:
12441261
# -00001-of-00002.gguf (TheBloke-style)
12451262
# .gguf.part1of2 (Hugging Face-style multi-part)
@@ -1298,25 +1315,9 @@ async def _process_single_model(model, model_format: str) -> Optional[Dict]:
12981315
else 0.0
12991316
)
13001317

1301-
# Siblings from list_models often have size=None; fetch accurate sizes from Hub
1302-
try:
1303-
all_filenames = [s.rfilename for s in gguf_siblings]
1304-
accurate_sizes = get_accurate_file_sizes(model.id, all_filenames)
1305-
if accurate_sizes:
1306-
for entry in quantizations.values():
1307-
for f in entry["files"]:
1308-
f["size"] = accurate_sizes.get(f["filename"]) or f["size"] or 0
1309-
entry["total_size"] = sum(f["size"] for f in entry["files"])
1310-
entry["size_mb"] = (
1311-
round(entry["total_size"] / (1024 * 1024), 2)
1312-
if entry["total_size"]
1313-
else 0.0
1314-
)
1315-
except Exception as size_err:
1316-
logger.debug(f"Could not fetch accurate sizes for {model.id}: {size_err}")
1317-
1318-
# If no quantizations were detected after grouping, skip this model
1319-
if not quantizations:
1318+
# Search should stay to a single HF API call. Accurate file sizes are lazy-loaded on expand.
1319+
# If no downloadable GGUF entries were detected after grouping, skip this model.
1320+
if not quantizations and not mmproj_files:
13201321
return None
13211322
else:
13221323
safetensors_files = []
@@ -1338,15 +1339,6 @@ async def _process_single_model(model, model_format: str) -> Optional[Dict]:
13381339
)
13391340
if not safetensors_files:
13401341
return None
1341-
# Fetch accurate sizes; list_models siblings often have size=None
1342-
try:
1343-
st_filenames = [f["filename"] for f in safetensors_files]
1344-
accurate_sizes = get_accurate_file_sizes(model.id, st_filenames)
1345-
if accurate_sizes:
1346-
for f in safetensors_files:
1347-
f["size"] = accurate_sizes.get(f["filename"]) or 0
1348-
except Exception as size_err:
1349-
logger.debug(f"Could not fetch accurate sizes for {model.id}: {size_err}")
13501342
else:
13511343
return None
13521344

@@ -1364,6 +1356,7 @@ async def _process_single_model(model, model_format: str) -> Optional[Dict]:
13641356
"tags": model.tags or [],
13651357
"model_format": model_format,
13661358
"quantizations": quantizations if model_format == "gguf" else {},
1359+
"mmproj_files": mmproj_files if model_format == "gguf" else [],
13671360
"safetensors_files": (
13681361
safetensors_files if model_format == "safetensors" else []
13691362
),
@@ -1668,7 +1661,7 @@ async def get_model_details(model_id: str) -> Dict:
16681661
config_path = hf_hub_download(
16691662
repo_id=model_id,
16701663
filename="config.json",
1671-
local_dir="data/temp",
1664+
local_dir="data/hf-cache",
16721665
local_dir_use_symlinks=False,
16731666
)
16741667

‎backend/llama_manager.py‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,13 +2273,19 @@ def set_flag(flag: str, value: bool):
22732273
logger.error(f"Build failed: {e}")
22742274
if progress_manager and task_id:
22752275
try:
2276-
await progress_manager.send_build_progress(
2277-
task_id=task_id,
2278-
stage="error",
2279-
progress=0,
2280-
message=f"Build failed: {str(e)}",
2281-
log_lines=[f"Error: {str(e)}"],
2276+
existing_task = progress_manager.get_task(task_id)
2277+
existing_logs = (
2278+
(existing_task or {}).get("metadata", {}).get("log_lines") or []
22822279
)
2280+
error_text = str(e)
2281+
if error_text not in existing_logs:
2282+
await progress_manager.send_build_progress(
2283+
task_id=task_id,
2284+
stage="error",
2285+
progress=0,
2286+
message=f"Build failed: {error_text}",
2287+
log_lines=[f"Error: {error_text}"],
2288+
)
22832289
except Exception as ws_error:
22842290
logger.error(f"Failed to send error via SSE: {ws_error}")
22852291
raise Exception(f"Failed to build from source {commit_sha}: {e}")

‎backend/llama_swap_client.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,23 @@ async def get_model_info(self, model_id: str, upstream_path: str = "v1/models"):
149149
except Exception as e:
150150
logger.error(f"Failed to get model info for {model_id}: {e}")
151151
raise
152+
153+
async def load_model(self, model_name: str, retries: int = 20, delay: float = 0.5):
154+
"""Trigger on-demand model loading via llama-swap's upstream route."""
155+
last_error = None
156+
for _ in range(max(1, retries)):
157+
try:
158+
async with httpx.AsyncClient() as client:
159+
response = await client.get(
160+
f"{self.base_url}/upstream/{model_name}/v1/models", timeout=30
161+
)
162+
response.raise_for_status()
163+
self._loading_models.discard(model_name)
164+
return response.json()
165+
except Exception as e:
166+
last_error = e
167+
self._loading_models.add(model_name)
168+
await asyncio.sleep(delay)
169+
self._loading_models.discard(model_name)
170+
logger.error(f"Failed to load model {model_name}: {last_error}")
171+
raise last_error

0 commit comments

Comments
 (0)