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 || "?"}`);
|
||||
|
||||
Reference in New Issue
Block a user