engines card: canonical per-model memory info; cap GME vision token budget

The tooltip matched loaded_info entries (raw engine keys) against
canonicalized display names client-side and mostly missed — footprints
now canonicalize server-side with the same mapping as the model list,
keyed by canonical id, so every loaded model shows its VRAM (+RAM when
offloading).

llama-vl: bound mtmd image_max_tokens (config `image_max_tokens`,
default 1024) — large photos otherwise expand to thousands of vision
tokens whose transient compute buffer evicts co-resident models on a
small card.
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014S8VtAvG499SsCbeESRK7V
parent e64c1964
...@@ -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.45" __version__ = "0.1.46"
# 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
......
...@@ -317,15 +317,9 @@ async function loadEngines(){ ...@@ -317,15 +317,9 @@ async function loadEngines(){
const models = loaded.length; const models = loaded.length;
// Tooltip: one line per model with its memory footprint — VRAM, plus the // Tooltip: one line per model with its memory footprint — VRAM, plus the
// host-RAM slice separately when the model is CPU-offloading. // host-RAM slice separately when the model is CPU-offloading.
const info = e.loaded_info||[]; const info = e.loaded_info||{}; // canonical model id -> {vram_gb, ram_gb}
const infoByBase = {};
info.forEach(m => {
const b = (m.model||'').split('/').pop().replace(/\.gguf$/i,'');
infoByBase[b] = m; infoByBase[m.model] = m;
});
const mtip = models ? esc(loaded.map(name => { const mtip = models ? esc(loaded.map(name => {
const b = (name||'').split('/').pop().replace(/\.gguf$/i,''); const m = info[name];
const m = infoByBase[name] || infoByBase[b];
if (!m || (!m.vram_gb && !m.ram_gb)) return name; if (!m || (!m.vram_gb && !m.ram_gb)) return name;
let s = `${name} — VRAM ${m.vram_gb} GB`; let s = `${name} — VRAM ${m.vram_gb} GB`;
if (m.ram_gb) s += ` · RAM ${m.ram_gb} GB (offload)`; if (m.ram_gb) s += ` · RAM ${m.ram_gb} GB (offload)`;
......
...@@ -290,6 +290,16 @@ def _load_embedding_model(model_name: str, device: str, model_config: dict = Non ...@@ -290,6 +290,16 @@ def _load_embedding_model(model_name: str, device: str, model_config: dict = Non
mp.use_gpu = (n_gpu_layers != 0) mp.use_gpu = (n_gpu_layers != 0)
mp.print_timings = False mp.print_timings = False
mp.n_threads = 8 mp.n_threads = 8
# Bound the vision token budget: qwen2-vl dynamic resolution
# turns a large photo into thousands of image tokens, and the
# vision tower's transient compute buffer then spikes VRAM —
# which evicts co-resident models mid-encode on a small card.
try:
mp.image_max_tokens = int(
cfg.get('image_max_tokens')
or raw.get('image_max_tokens') or 1024)
except Exception:
pass
mctx = _M.mtmd_init_from_file( mctx = _M.mtmd_init_from_file(
str(_mmproj).encode(), llm._model.model, mp) str(_mmproj).encode(), llm._model.model, mp)
if not mctx: if not mctx:
......
...@@ -1100,6 +1100,21 @@ class FrontProxy: ...@@ -1100,6 +1100,21 @@ class FrontProxy:
seen.setdefault(canon, None) seen.setdefault(canon, None)
return list(seen.keys()) return list(seen.keys())
def _canonical_info(self, loaded_info) -> dict:
"""loaded_info entries keyed by CANONICAL model id (same mapping as
_canonical_loaded), deduping the several key forms one resident model
reports under; keeps the largest footprint among duplicates."""
out: dict = {}
for m in (loaded_info or []):
if not isinstance(m, dict):
continue
k = str(m.get("model") or "")
canon = (self._model_info(k).get("model_id") or k)
prev = out.get(canon)
if prev is None or (m.get("vram_gb") or 0) > (prev.get("vram_gb") or 0):
out[canon] = {**m, "model": canon}
return out
def engines_list(self) -> list: def engines_list(self) -> list:
out = [] out = []
for e in self.registry.all(): for e in self.registry.all():
...@@ -1116,7 +1131,8 @@ class FrontProxy: ...@@ -1116,7 +1131,8 @@ class FrontProxy:
"thermal_paused": bool(getattr(e, "therm_paused", False)), "thermal_paused": bool(getattr(e, "therm_paused", False)),
"thermal_frozen": bool(getattr(e, "therm_sigstopped", False)), "thermal_frozen": bool(getattr(e, "therm_sigstopped", False)),
"loaded_models": self._canonical_loaded(e.loaded_models), "loaded_models": self._canonical_loaded(e.loaded_models),
"loaded_info": list(getattr(e, "loaded_info", []) or []), "loaded_info": self._canonical_info(
getattr(e, "loaded_info", None)),
"inflight": int(getattr(e, "inflight", 0) or 0), "inflight": int(getattr(e, "inflight", 0) or 0),
"processing": (int(getattr(e, "inflight", 0) or 0) > 0 "processing": (int(getattr(e, "inflight", 0) or 0) > 0
or bool(getattr(e, "loading", None))), or bool(getattr(e, "loading", None))),
......
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