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:
2026-10-28 17:24:41 +01:00
co-authored by Claude Opus 5.5
parent 26ad872c6c
commit dd5d1fd2cb
3 changed files with 199 additions and 32 deletions
+44 -4
View File
@@ -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() {
+1
View File
@@ -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 || "?"}`);