feat(llm): grosse MoE-Modelle auf kleinen Karten — Experten in den Arbeitsspeicher
Bei MoE-Modellen (z.B. Qwen3-Coder-30B-A3B: 18,6 GB, davon 16,35 GB Experten) rechnet pro Wort nur ein kleiner Teil. llama.cpp kann die Experten in den RAM legen (--cpu-moe / --n-cpu-moe N), der Rest bleibt auf der Karte. Adapter: - Registry-Feld cpuMoe: 0 = Karte, -1 = alle Experten in den RAM, N = die der ersten N Schichten; -2 beim Laden = automatisch aufteilen. - Automatisch: liest nur den Kopf der GGUF bei HuggingFace (HTTP-Range, Tensor-Verzeichnis), rechnet Experten je Schicht + KV-Cache gegen den VRAM der Karte und legt so viele Experten auf die Karte wie passen (12 GB/32k: 16 von 48 Schichten auf der Karte, 32 im RAM). - Kontext-Auswahl: GB-Schaetzung fuer die Karte ohne ausgelagerte Experten; slot meldet moe, cpuMoe, ramMB. KV-Rechnung als _kv_bytes() geteilt. Diagnostic: MoE-Modell zu gross fuer die Karte → Rueckfrage "Experten automatisch aufteilen?"; pro Karte Auswahl Experten: Karte / automatisch / Arbeitsspeicher. Kontextwechsel rechnet die Aufteilung neu. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+44
-4
@@ -9465,6 +9465,14 @@
|
||||
rows.push(`<div style="display:grid;grid-template-columns:minmax(220px,1fr) minmax(260px,2fr) 260px 160px;gap:10px;align-items:center;">
|
||||
<div><b style="color:#E0E0F0;">${escapeHtml(b.node)}</b> · Karte ${escapeHtml(sl.gpu)}
|
||||
<div style="color:#555570;font-size:11px;">${escapeHtml(sl.gpuName || '')}${vramGB ? ' · ' + vramGB.toFixed(0) + ' GB' : ''}${sl.vramUsedMB >= 0 ? ' · belegt ' + (sl.vramUsedMB / 1024).toFixed(1) + ' GB' : ''}${sl.ctx ? ' · Kontext ' + sl.ctx : ''}</div>
|
||||
${sl.moe ? `<div style="margin-top:4px;font-size:11px;color:#8888AA;">Experten (MoE):
|
||||
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-model="${escapeHtml(sl.model)}" data-current="${sl.cpuMoe > 0 ? -2 : (sl.cpuMoe || 0)}"
|
||||
onchange="onLlmCardMoe(this)" ${job ? 'disabled' : ''} title="Bei MoE-Modellen rechnet pro Wort nur ein Teil — die Experten dürfen in den Arbeitsspeicher. Langsamer, aber große Modelle passen so auf kleine Karten."
|
||||
style="background:#1E1E2E;border:1px solid #333;border-radius:4px;padding:2px 4px;color:#E0E0F0;font-size:11px;">
|
||||
<option value="0"${!sl.cpuMoe ? ' selected' : ''}>alle auf der Karte</option>
|
||||
<option value="-2"${sl.cpuMoe > 0 ? ' selected' : ''}>automatisch aufteilen</option>
|
||||
<option value="-1"${sl.cpuMoe < 0 ? ' selected' : ''}>alle im Arbeitsspeicher</option>
|
||||
</select>${sl.ramMB ? ' · ' + (sl.ramMB / 1024).toFixed(1).replace('.', ',') + ' GB im RAM' : ''}${sl.cpuMoe > 0 ? ' (' + sl.cpuMoe + ' Schichten)' : ''}</div>` : ''}
|
||||
${sl.onCpu ? '<div style="color:#FF9F0A;font-size:11px;" title="llama.cpp findet die Karte nicht und rechnet langsam auf der CPU. Ursache meist: Treiber zu alt fuer das CUDA im llama-swap-Image. Abhilfe: auf der Box sudo ./bootstrap.sh --upgrade-driver">⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)</div>' : ''}</div>
|
||||
<select data-box="${escapeHtml(b.instanceId)}" data-gpu="${escapeHtml(sl.gpu)}" data-current="${escapeHtml(sl.model || '')}"
|
||||
onchange="onLlmCardSelect(this)" ${job ? 'disabled' : ''}
|
||||
@@ -9615,7 +9623,9 @@
|
||||
if (!confirm(`${model} auf ${_boxName(target)} · Karte ${gpu} mit ${next} statt ${cur || '?'} Tokens Kontext neu laden?\n\n`
|
||||
+ (more ? 'Mehr Kontext braucht mehr Grafikspeicher. Passt es nicht, setzt die Box die Karte von selbst auf den alten Stand zurück.\n\n' : '')
|
||||
+ 'Die Box startet ihre Modelle dabei kurz neu. Programme bekommen den neuen Wert automatisch.')) { sel.value = cur; return; }
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: next });
|
||||
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
|
||||
const cpuMoe = sl.cpuMoe > 0 ? -2 : (sl.cpuMoe || 0); // teilweise aufgeteilt → fuer den neuen Kontext neu rechnen
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: next, cpuMoe });
|
||||
openLlmDownloads();
|
||||
}
|
||||
|
||||
@@ -9633,11 +9643,25 @@
|
||||
if (!m || !m.hfRepo) { alert(`${next} steht nicht im Modell-Katalog — Herkunft unbekannt.`); sel.value = current; return; }
|
||||
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
|
||||
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
|
||||
const warn = (vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92)
|
||||
const cpuMoe = askCpuMoe(m, sl.vramMB || 0);
|
||||
const warn = (!cpuMoe && vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92)
|
||||
? `\n\n⚠ ${next} ist etwa ${m.sizeGB} GB groß, die Karte hat ${vramGB.toFixed(0)} GB — wahrscheinlich passt es nicht.` : '';
|
||||
const repl = current ? `\n\nErsetzt ${current} — Programme, die es nutzen, verlieren es.` : '';
|
||||
if (!confirm(`${next} auf ${node} · Karte ${gpu} laden?${repl}${warn}\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx });
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx, cpuMoe });
|
||||
openLlmDownloads();
|
||||
}
|
||||
|
||||
// MoE: Experten zwischen Karte und Arbeitsspeicher — gleiches Modell neu laden
|
||||
function onLlmCardMoe(sel) {
|
||||
const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
|
||||
const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
|
||||
const m = _llmCatalog.find(x => x.id === model);
|
||||
if (!m || !m.hfRepo) { alert(`${model} steht nicht im Modell-Katalog.`); sel.value = String(cur); return; }
|
||||
const txt = { '0': 'alle Experten auf die Karte', '-2': 'automatisch aufteilen (so viele wie passen auf die Karte)', '-1': 'alle Experten in den Arbeitsspeicher' };
|
||||
if (!confirm(`${model} neu laden: ${txt[String(next)] || next}?\n\nDie Box startet ihre Modelle dabei kurz neu.`)) { sel.value = String(cur); return; }
|
||||
const sl = (workers.find(w => w.instanceId === target)?.slots || []).find(x => String(x.gpu) === String(gpu)) || {};
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: sl.ctx || m.ctx, cpuMoe: next });
|
||||
openLlmDownloads();
|
||||
}
|
||||
|
||||
@@ -9687,6 +9711,21 @@
|
||||
return out;
|
||||
}
|
||||
|
||||
// MoE-Modelle (Mixture of Experts): pro Wort rechnet nur ein Teil — die
|
||||
// "Experten" duerfen darum in den Arbeitsspeicher, ohne dass es unbrauchbar
|
||||
// langsam wird. Erkennung am Namen (A3B, A4B, MoE …); sicher weiss es die Box
|
||||
// erst nach dem Laden (slot.moe).
|
||||
function looksMoe(m) { return /(\bA\d+(\.\d+)?B\b|moe|mixtral)/i.test((m.id || '') + ' ' + (m.hfRepo || '')); }
|
||||
/** 0 = normal laden, -2 = Experten automatisch aufteilen, null = abbrechen */
|
||||
function askCpuMoe(m, vramMB) {
|
||||
if (!looksMoe(m) || !vramMB || !m.sizeGB || m.sizeGB * 1024 <= vramMB * 0.85) return 0;
|
||||
return confirm(`${m.id} ist ein MoE-Modell (pro Wort rechnet nur ein kleiner Teil) und mit ~${m.sizeGB} GB `
|
||||
+ `größer als die Karte (${(vramMB / 1024).toFixed(0)} GB).\n\n`
|
||||
+ `Die Experten automatisch zwischen Karte und Arbeitsspeicher aufteilen? So viele wie passen bleiben `
|
||||
+ `auf der Karte, der Rest geht in den RAM der Box. Langsamer als ganz auf der Karte, aber es läuft.\n\n`
|
||||
+ `OK = aufteilen · Abbrechen = normal laden (passt dann vermutlich nicht)`) ? -2 : 0;
|
||||
}
|
||||
|
||||
function provisionModel(i) {
|
||||
const m = _llmCatalog[i];
|
||||
if (!m) return;
|
||||
@@ -9701,9 +9740,10 @@
|
||||
!confirm(`Karte ${gpu} auf ${tgt.box.node} hält gerade ${tgt.slot.model}.\n\n`
|
||||
+ `${m.id} ersetzt es. Clients, die ${tgt.slot.model} nutzen, verlieren es dann.\n`
|
||||
+ `Die anderen Karten dieser Box starten dabei kurz neu.\n\nErsetzen?`)) return;
|
||||
const cpuMoe = askCpuMoe(m, (tgt && tgt.slot && tgt.slot.vramMB) || 0);
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
if (st) { st.textContent = `Lade '${m.id}' auf ${target}… (GGUF-Download kann dauern)`; st.style.color = '#FFD60A'; }
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu: gpu || '', key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx });
|
||||
send({ action: 'llm_provision_model', targetInstance: target, gpu: gpu || '', key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: m.ctx, cpuMoe });
|
||||
openLlmDownloads();
|
||||
}
|
||||
function runLlmTest() {
|
||||
|
||||
@@ -3601,6 +3601,7 @@ wss.on("connection", (ws) => {
|
||||
sendToRVS_raw({ type: "llm_provision_model", payload: {
|
||||
targetInstance: msg.targetInstance || "",
|
||||
key: msg.key, hfRepo: msg.hfRepo, quant: msg.quant, ctx: msg.ctx, ngl: msg.ngl,
|
||||
cpuMoe: msg.cpuMoe ?? 0, // MoE: 0 Karte, -1 alle Experten in den RAM, -2 automatisch verteilen
|
||||
gpu: msg.gpu ?? "", // Ziel-Karte auf der Box
|
||||
}, timestamp: Date.now() });
|
||||
log("info", "llm", `provision '${msg.key}' (${msg.hfRepo}) → ${msg.targetInstance || "?"}`);
|
||||
|
||||
+154
-28
@@ -226,6 +226,31 @@ def _base_config() -> dict:
|
||||
return {}
|
||||
|
||||
|
||||
def _cpu_moe_flag(v) -> str:
|
||||
"""MoE-Modelle: Experten in den Arbeitsspeicher. -1 = alle (--cpu-moe),
|
||||
N > 0 = die der ersten N Schichten (--n-cpu-moe N), 0/leer = alles auf die Karte.
|
||||
Der Rest (Attention, gemeinsame Teile, KV-Cache) bleibt auf der Karte — pro
|
||||
Wort rechnet ein MoE nur wenige Experten, darum bleibt es brauchbar schnell."""
|
||||
try:
|
||||
n = int(v or 0)
|
||||
except (TypeError, ValueError):
|
||||
return ""
|
||||
if n < 0:
|
||||
return " --cpu-moe"
|
||||
return f" --n-cpu-moe {n}" if n > 0 else ""
|
||||
|
||||
|
||||
def _model_cpu_moe(key: str) -> int:
|
||||
for e in _load_registry():
|
||||
if (e.get("key") or "").strip() == key:
|
||||
try:
|
||||
return int(e.get("cpuMoe") or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
m = re.search(r"--n-cpu-moe\s+(\d+)", _model_cmd(key) or "")
|
||||
return int(m.group(1)) if m else (-1 if "--cpu-moe" in (_model_cmd(key) or "") else 0)
|
||||
|
||||
|
||||
def _model_cmd(key: str) -> str:
|
||||
"""llama-server-Aufruf fuer ein Modell: aus der Registry (ueber die
|
||||
Diagnostic geladen) oder aus dem Basis-Template (qwen3-8b & Co.)."""
|
||||
@@ -234,7 +259,8 @@ def _model_cmd(key: str) -> str:
|
||||
quant = (e.get("quant") or "Q4_K_M").strip()
|
||||
return (f"llama-server --port ${{PORT}} --host 127.0.0.1\n"
|
||||
f"-hf {e['hfRepo'].strip()}:{quant}\n"
|
||||
f"-ngl {int(e.get('ngl') or 99)} -c {int(e.get('ctx') or 8192)} --jinja")
|
||||
f"-ngl {int(e.get('ngl') or 99)} -c {int(e.get('ctx') or 8192)} --jinja"
|
||||
+ _cpu_moe_flag(e.get("cpuMoe")))
|
||||
spec = (_base_config().get("models") or {}).get(key) or {}
|
||||
return str(spec.get("cmd") or "")
|
||||
|
||||
@@ -534,15 +560,16 @@ VRAM_OVERHEAD_MB = 1400
|
||||
UBATCH = 512
|
||||
|
||||
|
||||
def _gguf_meta(path: str) -> dict:
|
||||
def _gguf_meta(path: str, fsize: int = 0) -> dict:
|
||||
"""Metadaten-Block einer GGUF-Datei (gecacht). Lange String-Listen (der
|
||||
Tokenizer) werden uebersprungen, nicht gespeichert."""
|
||||
Tokenizer) werden uebersprungen, nicht gespeichert. fsize: wahre Groesse,
|
||||
wenn path nur der Anfang der Datei ist (Kopf per HTTP-Range geholt)."""
|
||||
try:
|
||||
mt = os.path.getmtime(path)
|
||||
except OSError:
|
||||
return {}
|
||||
hit = _gguf_cache.get(path)
|
||||
if hit and hit[0] == mt:
|
||||
if hit and hit[0] == mt and not fsize:
|
||||
return hit[1]
|
||||
import struct
|
||||
meta: dict = {}
|
||||
@@ -575,12 +602,35 @@ def _gguf_meta(path: str) -> dict:
|
||||
with open(path, "rb") as f:
|
||||
if f.read(4) != b"GGUF":
|
||||
return {}
|
||||
rd(f, "I"); rd(f, "Q")
|
||||
rd(f, "I")
|
||||
n_tensors = rd(f, "Q")
|
||||
for _ in range(rd(f, "Q")):
|
||||
k = rstr(f)
|
||||
v = rval(f, rd(f, "I"))
|
||||
if v is not None:
|
||||
meta[k] = v
|
||||
# Tensor-Verzeichnis: Groesse der MoE-Experten (…_exps) je Schicht —
|
||||
# fuer die Schaetzung "was bleibt auf der Karte, was geht in den RAM"
|
||||
infos = []
|
||||
for _ in range(n_tensors):
|
||||
name = rstr(f)
|
||||
nd = rd(f, "I")
|
||||
for _ in range(nd):
|
||||
rd(f, "Q")
|
||||
rd(f, "I")
|
||||
infos.append((rd(f, "Q"), name))
|
||||
align = int(meta.get("general.alignment") or 32)
|
||||
data_start = (f.tell() + align - 1) // align * align
|
||||
fsize = fsize or os.path.getsize(path)
|
||||
infos.sort()
|
||||
exp_by_layer: dict = {}
|
||||
for i, (off, name) in enumerate(infos):
|
||||
nxt = infos[i + 1][0] if i + 1 < len(infos) else fsize - data_start
|
||||
if "_exps" in name:
|
||||
mm = re.match(r"blk\.(\d+)\.", name)
|
||||
lay = int(mm.group(1)) if mm else -1
|
||||
exp_by_layer[lay] = exp_by_layer.get(lay, 0) + max(0, nxt - off)
|
||||
meta["_expert_bytes_by_layer"] = exp_by_layer
|
||||
except Exception as e:
|
||||
logger.debug("GGUF %s nicht lesbar: %s", path, e)
|
||||
meta = {}
|
||||
@@ -588,6 +638,74 @@ def _gguf_meta(path: str) -> dict:
|
||||
return meta
|
||||
|
||||
|
||||
def _kv_bytes(meta: dict, ctx: int) -> int:
|
||||
"""KV-Cache (f16) fuer ctx Tokens laut GGUF-Metadaten — Sliding-Window-
|
||||
Schichten (Gemma) halten nur ihr Fenster."""
|
||||
arch = meta.get("general.architecture")
|
||||
if not arch:
|
||||
return 0
|
||||
g = lambda k, d=None: meta.get(f"{arch}.{k}", d)
|
||||
n_layer = int(g("block_count", 0) or 0)
|
||||
if not n_layer:
|
||||
return 0
|
||||
|
||||
def per_layer(v, d=0):
|
||||
return list(v) if isinstance(v, list) else [v if v is not None else d] * n_layer
|
||||
kv_heads = per_layer(g("attention.head_count_kv", g("attention.head_count", 0)))
|
||||
k_len = int(g("attention.key_length", 0) or 0)
|
||||
v_len = int(g("attention.value_length", 0) or 0)
|
||||
if not (k_len and v_len):
|
||||
emb, heads = int(g("embedding_length", 0) or 0), int(g("attention.head_count", 0) or 0)
|
||||
k_len = v_len = emb // heads if heads else 0
|
||||
k_swa = int(g("attention.key_length_swa", k_len) or k_len)
|
||||
v_swa = int(g("attention.value_length_swa", v_len) or v_len)
|
||||
window = int(g("attention.sliding_window", 0) or 0)
|
||||
pattern = g("attention.sliding_window_pattern")
|
||||
swa = per_layer(pattern, False) if isinstance(pattern, list) and window else [False] * n_layer
|
||||
per_tok = sum(int(kv_heads[i]) * (k_len + v_len) * 2 for i in range(n_layer) if not swa[i])
|
||||
swa_tok = sum(int(kv_heads[i]) * (k_swa + v_swa) * 2 for i in range(n_layer) if swa[i])
|
||||
return per_tok * ctx + swa_tok * min(ctx, window + UBATCH)
|
||||
|
||||
|
||||
async def _auto_cpu_moe(repo: str, file: dict, vram_mb: int, ctx: int) -> int:
|
||||
"""Wie viele Schichten mit Experten muessen in den RAM, damit der Rest auf
|
||||
die Karte passt? Holt nur den Kopf der GGUF (Metadaten + Tensor-Verzeichnis,
|
||||
wenige MB per HTTP-Range) — die Datei selbst laedt erst llama.cpp.
|
||||
0 = passt komplett, -1 = alle Experten in den RAM (oder unbekannt)."""
|
||||
import tempfile
|
||||
url = f"https://huggingface.co/{repo}/resolve/main/{file['path']}"
|
||||
headers = {"Range": "bytes=0-41943039"}
|
||||
if HF_TOKEN:
|
||||
headers["Authorization"] = f"Bearer {HF_TOKEN}"
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=60, follow_redirects=True) as c:
|
||||
r = await c.get(url, headers=headers)
|
||||
r.raise_for_status()
|
||||
with tempfile.NamedTemporaryFile(suffix=".gguf", delete=False) as tf:
|
||||
tf.write(r.content)
|
||||
tmp = tf.name
|
||||
try:
|
||||
meta = _gguf_meta(tmp, fsize=int(file.get("size") or 0))
|
||||
finally:
|
||||
_gguf_cache.pop(tmp, None)
|
||||
os.unlink(tmp)
|
||||
except Exception as e:
|
||||
logger.warning("MoE-Aufteilung fuer %s nicht berechenbar (%s) — alle Experten in den RAM", repo, e)
|
||||
return -1
|
||||
exp = meta.get("_expert_bytes_by_layer") or {}
|
||||
if not exp:
|
||||
return 0 # kein MoE
|
||||
total_mb = int(file.get("size") or 0) / 1048576
|
||||
exp_mb = sum(exp.values()) / 1048576
|
||||
budget = vram_mb * 0.92 - VRAM_OVERHEAD_MB - _kv_bytes(meta, ctx) / 1048576 - (total_mb - exp_mb)
|
||||
per_layer = exp_mb / len(exp)
|
||||
on_gpu = max(0, int(budget // per_layer)) if per_layer else 0
|
||||
n = max(0, len(exp) - on_gpu)
|
||||
logger.info("MoE %s: %d von %d Schichten mit Experten in den RAM (Karte %d MB, ctx %d)",
|
||||
repo, n, len(exp), vram_mb, ctx)
|
||||
return -1 if n >= len(exp) else n
|
||||
|
||||
|
||||
def _model_files(key: str) -> tuple:
|
||||
"""(Gewichte.gguf, mmproj.gguf|None) eines Modells im HF-Cache unter /models
|
||||
— aus "-hf org/repo:QUANT" bzw. "-m pfad" im llama-server-Aufruf."""
|
||||
@@ -624,35 +742,29 @@ def _ctx_profile(key: str) -> dict:
|
||||
if not n_layer:
|
||||
return {}
|
||||
|
||||
def per_layer(v, d=0):
|
||||
return list(v) if isinstance(v, list) else [v if v is not None else d] * n_layer
|
||||
|
||||
kv_heads = per_layer(g("attention.head_count_kv", g("attention.head_count", 0)))
|
||||
k_len = int(g("attention.key_length", 0) or 0)
|
||||
v_len = int(g("attention.value_length", 0) or 0)
|
||||
if not (k_len and v_len):
|
||||
emb, heads = int(g("embedding_length", 0) or 0), int(g("attention.head_count", 0) or 0)
|
||||
k_len = v_len = emb // heads if heads else 0
|
||||
k_swa = int(g("attention.key_length_swa", k_len) or k_len)
|
||||
v_swa = int(g("attention.value_length_swa", v_len) or v_len)
|
||||
window = int(g("attention.sliding_window", 0) or 0)
|
||||
pattern = g("attention.sliding_window_pattern")
|
||||
swa = per_layer(pattern, False) if isinstance(pattern, list) else [False] * n_layer
|
||||
if not window:
|
||||
swa = [False] * n_layer
|
||||
|
||||
per_tok = sum(int(kv_heads[i]) * (k_len + v_len) * 2 for i in range(n_layer) if not swa[i])
|
||||
swa_tok = sum(int(kv_heads[i]) * (k_swa + v_swa) * 2 for i in range(n_layer) if swa[i])
|
||||
files_mb = (os.path.getsize(weights) + (os.path.getsize(mmproj) if mmproj else 0)) / 1048576
|
||||
ctx_max = int(g("context_length", 0) or 0)
|
||||
exp_by_layer = meta.get("_expert_bytes_by_layer") or {}
|
||||
moe = bool(exp_by_layer) or int(g("expert_count", 0) or 0) > 0
|
||||
cpu_moe = _model_cpu_moe(key) if moe else 0
|
||||
if cpu_moe:
|
||||
layers = sorted(exp_by_layer) if cpu_moe < 0 else sorted(exp_by_layer)[:cpu_moe]
|
||||
ram_mb = sum(exp_by_layer[l] for l in layers) / 1048576
|
||||
else:
|
||||
ram_mb = 0.0
|
||||
gpu_files_mb = files_mb - ram_mb
|
||||
steps = [c for c in CTX_STEPS if not ctx_max or c <= ctx_max]
|
||||
if ctx_max and ctx_max not in steps:
|
||||
steps.append(ctx_max) # krummes Maximum (z.B. 40960) mit anbieten
|
||||
opts = []
|
||||
for c in steps:
|
||||
kv_mb = (per_tok * c + swa_tok * min(c, window + UBATCH)) / 1048576
|
||||
opts.append({"ctx": c, "estMB": int(files_mb + kv_mb + VRAM_OVERHEAD_MB)})
|
||||
return {"ctxMax": ctx_max, "options": opts}
|
||||
kv_mb = _kv_bytes(meta, c) / 1048576
|
||||
opts.append({"ctx": c, "estMB": int(gpu_files_mb + kv_mb + VRAM_OVERHEAD_MB)})
|
||||
out = {"ctxMax": ctx_max, "options": opts, "moe": moe, "cpuMoe": cpu_moe}
|
||||
if moe:
|
||||
out["expertsMB"] = int(sum(exp_by_layer.values()) / 1048576)
|
||||
out["ramMB"] = int(ram_mb)
|
||||
return out
|
||||
|
||||
|
||||
async def _vram_used() -> dict:
|
||||
@@ -1112,10 +1224,24 @@ async def _handle_provision(ws, payload: dict) -> None:
|
||||
if quant != wanted.upper():
|
||||
quant_note = f"{wanted} gibt es in {repo} nicht — nehme {quant}"
|
||||
logger.info("Karte %s: %s", card, quant_note)
|
||||
ctx_req = int(payload.get("ctx") or 8192)
|
||||
try:
|
||||
cpu_moe = int(payload.get("cpuMoe") or 0)
|
||||
except (TypeError, ValueError):
|
||||
cpu_moe = 0
|
||||
if cpu_moe == -2:
|
||||
# "automatisch verteilen": so viele Experten auf die Karte wie passen
|
||||
qfiles = _files_for_quant(repo_files, quant) if repo_files else []
|
||||
vram = int(((await _gpu_names()).get(card) or {}).get("vramMB") or 0)
|
||||
if len(qfiles) == 1 and vram:
|
||||
cpu_moe = await _auto_cpu_moe(repo, qfiles[0], vram, ctx_req)
|
||||
else:
|
||||
cpu_moe = -1 # gesplittete Datei / Karte unbekannt: alle in den RAM
|
||||
entry = {
|
||||
"key": key, "hfRepo": repo, "quant": quant,
|
||||
"ctx": int(payload.get("ctx") or 8192),
|
||||
"ctx": ctx_req,
|
||||
"ngl": int(payload.get("ngl") or 99),
|
||||
"cpuMoe": cpu_moe,
|
||||
}
|
||||
_save_registry([e for e in _load_registry() if e.get("key") != key] + [entry])
|
||||
elif not _model_cmd(key):
|
||||
|
||||
Reference in New Issue
Block a user