diff --git a/diagnostic/index.html b/diagnostic/index.html
index 144afad..453a808 100644
--- a/diagnostic/index.html
+++ b/diagnostic/index.html
@@ -9298,6 +9298,7 @@
// ── Modell-Katalog (Stage D) ────────────────────────────
let _llmCatalog = [];
+ const _llmCardPending = {}; // Karten: gewaehlt, noch nicht geladen — "instanceId|karte" → {model, ctx}
async function loadLlmCatalog() {
if (typeof requestWorkers === 'function') requestWorkers(); // Flotte aktiv nachziehen
try {
@@ -9332,7 +9333,7 @@
box._llmDeferred = false;
if (_llmRenderDeferred(box, fn)) return; // Fokus nur innerhalb gewechselt → weiter warten
fn();
- }, 0), { once: true });
+ }, 300), { once: true });
}
return true;
}
@@ -9349,6 +9350,16 @@
const none = document.getElementById('llm-catalog-none');
if (none) none.style.display = rows.length && !shown ? '' : 'none';
}
+ // Kontext-Stufen fuer Modelle, deren Speicherbedarf die Box noch nicht kennt
+ const _CTX_STEPS = [4096, 8192, 16384, 32768, 65536, 131072];
+ function _ctxSteps(cur) { return cur && !_CTX_STEPS.includes(cur) ? [cur, ..._CTX_STEPS].sort((a, b) => a - b) : _CTX_STEPS; }
+ function _ctxLabel(n) { return n >= 1024 ? (n % 1024 ? (n / 1024).toFixed(1) : n / 1024) + 'k' : String(n); }
+ // Auswahl im Katalog (Ziel-Karte, Kontext) je Modell — ueberlebt das Neuzeichnen
+ const _llmCatSel = {};
+ function llmCatPick(i, field, value) {
+ const m = _llmCatalog[i];
+ if (m) _llmCatSel[m.id] = { ...(_llmCatSel[m.id] || {}), [field]: value };
+ }
function renderLlmCatalog() {
const box = document.getElementById('llm-catalog-list');
if (!box) return;
@@ -9378,7 +9389,10 @@
const vramStyle = m.sizeGB >= 12 ? 'color:#FFB020;' : 'color:#AAB;';
// Ziel = Karte. Vorauswahl: eine leere Karte, sonst die erste.
const targets = llmCardTargets();
- const pre = (targets.find(t => t.slot && !t.slot.model) || targets[0] || {}).value;
+ const pick = _llmCatSel[m.id] || {};
+ const pre = targets.some(t => t.value === pick.target) ? pick.target
+ : (targets.find(t => t.slot && !t.slot.model) || targets[0] || {}).value;
+ const ctxPick = pick.ctx || m.ctx || 8192;
const opts = targets.length
? targets.map(t => ``).join('')
: '';
@@ -9391,7 +9405,9 @@
+ '' +
'
' + vram + ' | ' +
'' + haveTxt + ' | ' +
- ' | ' +
+ ' | ' +
+ ' | ' +
' | ' +
'';
}).join('');
@@ -9400,7 +9416,7 @@
'' +
'' +
'| Modell | Kann gut / Beschreibung | VRAM | ' +
- 'Status | Ziel-Karte | | ' +
+ 'Status | Ziel-Karte | Kontext | | ' +
'
' + rows + '
' +
'Kein Modell passt zur Suche.
';
filterLlmCatalog();
@@ -9542,6 +9558,12 @@
for (const sl of b.slots) {
const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
const ids = new Set(_llmCatalog.map(m => m.id));
+ // Gewaehlt, aber noch nicht geladen? Dann zeigt die Zeile die Auswahl.
+ const pkey = b.instanceId + '|' + sl.gpu;
+ const curCtx = sl.ctx || 0;
+ let pend = _llmCardPending[pkey];
+ if (pend && pend.model === (sl.model || '') && (!pend.model || pend.ctx === curCtx)) { delete _llmCardPending[pkey]; pend = null; }
+ const shown = pend ? pend.model : (sl.model || '');
const opts = [''];
let curLabel = '— leer —';
const list = _llmCatalog.filter(m => !m.unusable);
@@ -9553,8 +9575,8 @@
const tooBig = vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92;
const label = m.id + (m.sizeGB ? ` · ~${m.sizeGB} GB` : '') + (m.quant ? ` · ${m.quant}` : '')
+ (tooBig ? ' · passt nicht' : '');
- if (m.id === sl.model) curLabel = label;
- opts.push(``);
+ if (m.id === shown) curLabel = label;
+ opts.push(``);
}
let [c, st] = sl.model ? (SLOT_STATE[sl.state] || ['#8888AA', escapeHtml(sl.state || '?')]) : ['#555570', 'leer'];
if (sl.onCpu) { c = '#FF9F0A'; st = '⚠ auf der CPU'; }
@@ -9565,21 +9587,24 @@
// nicht, setzt die Box die Karte von selbst zurueck.
// Liste + Speicherbedarf kommen von der Box (aus der Modelldatei:
// trainiertes Maximum, KV-Cache pro Token). Alte Box: feste Liste.
- const curCtx = sl.ctx || 0;
- let ctxList = Array.isArray(sl.options) && sl.options.length
+ // Fuer ein anderes (noch nicht geladenes) Modell kennt die Box den
+ // Speicherbedarf nicht → feste Stufen ohne GB-Angabe.
+ const sameModel = shown && shown === sl.model;
+ const selCtx = pend ? pend.ctx : curCtx;
+ let ctxList = sameModel && Array.isArray(sl.options) && sl.options.length
? sl.options.map(o => ({ ctx: o.ctx, est: o.estMB }))
- : [4096, 8192, 16384, 32768, 65536, 131072].map(n => ({ ctx: n, est: 0 }));
- if (curCtx && !ctxList.some(o => o.ctx === curCtx)) ctxList = [{ ctx: curCtx, est: 0 }, ...ctxList];
+ : _CTX_STEPS.map(n => ({ ctx: n, est: 0 }));
+ if (selCtx && !ctxList.some(o => o.ctx === selCtx)) ctxList = [{ ctx: selCtx, est: 0 }, ...ctxList];
const vramMB = sl.vramMB || 0;
const ctxOpts = ctxList.map(o => {
const label = o.ctx >= 1024 ? (o.ctx % 1024 ? (o.ctx / 1024).toFixed(1) : o.ctx / 1024) + 'k' : o.ctx;
const gb = o.est ? ` · ca. ${(o.est / 1024).toFixed(1).replace('.', ',')} GB` : '';
const tooBig = o.est && vramMB && o.est > vramMB * 0.97 ? ' ⚠ passt nicht' : '';
- return ``;
+ return ``;
}).join('');
- const ctxSel = sl.model
- ? ``
: '';
const ctxCell = `${ctxSel}
@@ -9598,14 +9623,19 @@
${sl.ramMB ? ' · ' + (sl.ramMB / 1024).toFixed(1).replace('.', ',') + ' GB im RAM' : ''}${sl.cpuMoe > 0 ? ' (' + sl.cpuMoe + ' Schichten)' : ''}
` : ''}
${sl.onCpu ? '⚠ Modell läuft auf der CPU, Karte leer — Treiber zu alt? (bootstrap.sh --upgrade-driver)
' : ''}
-
+
${ctxCell}
- ${jobTxt || st}
+ ${pend && !job
+ ? `
+
+ `
+ : `${jobTxt || st}`}
`);
}
}
@@ -9643,7 +9673,7 @@
closeLlmPicker();
if (!o || o.value === sel.value) return;
sel.value = o.value;
- onLlmCardSelect(sel);
+ onLlmCardPick(sel);
};
list.addEventListener('mousedown', e => {
const d = e.target.closest('[data-i]');
@@ -9803,49 +9833,68 @@
function openCtxInfo() { document.getElementById('ctx-info-modal').style.display = 'flex'; }
- // Kontextfenster einer Karte aendern = dasselbe Modell mit anderem -c neu
- // laden. Programme bekommen den neuen Wert automatisch (ctx in
- // llm_cluster_models). Passt es nicht in den Grafikspeicher, setzt die Box
- // die Karte auf den alten Stand zurueck.
- async function onLlmCardCtx(sel) {
- const target = sel.dataset.box, gpu = sel.dataset.gpu, model = sel.dataset.model;
- sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
- const cur = Number(sel.dataset.current || 0), next = Number(sel.value);
- if (next === cur) return;
- const m = _llmCatalog.find(x => x.id === model);
- if (!m || !m.hfRepo) { uiAlert(`${model} steht nicht im Modell-Katalog — Kontext so nicht änderbar.`); sel.value = cur; return; }
- const more = next > cur;
- if (!await uiConfirm(`${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; }
+ // Karten: Modell und Kontext erst AUSWAEHLEN, geladen wird mit "Laden" —
+ // sonst laedt man zweimal (erst das Modell, dann nochmal mit anderem Kontext).
+ function _llmSlotOf(pkey) {
+ const at = pkey.lastIndexOf('|');
+ const target = pkey.slice(0, at), gpu = pkey.slice(at + 1);
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();
+ return { target, gpu, sl };
}
-
- async function onLlmCardSelect(sel) {
- const target = sel.dataset.box, gpu = sel.dataset.gpu, current = sel.dataset.current, next = sel.value;
- sel.blur(); // Fokus weg → aufgeschobenes Neuzeichnen holt den Stand nach
- const node = _boxName(target);
- if (next === current) return;
- if (!next) {
+ function onLlmCardPick(sel) {
+ const pkey = sel.dataset.key, model = sel.value;
+ const { sl } = _llmSlotOf(pkey);
+ const m = _llmCatalog.find(x => x.id === model);
+ const ctx = model && model === sl.model ? (sl.ctx || 0) : ((m && m.ctx) || 8192);
+ _llmCardPending[pkey] = { model, ctx };
+ renderLlmCardSettings();
+ }
+ function onLlmCardCtxPick(sel) {
+ const pkey = sel.dataset.key;
+ sel.blur();
+ _llmCardPending[pkey] = { model: sel.dataset.model, ctx: Number(sel.value) };
+ renderLlmCardSettings();
+ }
+ function discardLlmCard(pkey) {
+ delete _llmCardPending[pkey];
+ renderLlmCardSettings();
+ }
+ // Kontextfenster aendern = dasselbe Modell mit anderem -c neu laden.
+ // Passt es nicht in den Grafikspeicher, setzt die Box die Karte zurueck.
+ async function applyLlmCard(pkey) {
+ const pend = _llmCardPending[pkey];
+ if (!pend) return;
+ const { target, gpu, sl } = _llmSlotOf(pkey);
+ const node = _boxName(target), current = sl.model || '', cur = sl.ctx || 0;
+ if (!pend.model) {
if (!await uiConfirm(`Karte ${gpu} auf ${node} leeren?\n\n${current} wird entladen; ARIA und Programme können es dann nicht mehr wählen. `
- + `Die Datei bleibt auf der Box.\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) { sel.value = current; return; }
+ + `Die Datei bleibt auf der Box.\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) return;
send({ action: 'llm_unload_model', targetInstance: target, gpu });
+ delete _llmCardPending[pkey];
+ renderLlmCardSettings();
return;
}
- const m = _llmCatalog.find(x => x.id === next);
- if (!m || !m.hfRepo) { uiAlert(`${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 cpuMoe = await askCpuMoe(m, sl.vramMB || 0);
- if (cpuMoe === null) { sel.value = current; return; }
- 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 (!await uiConfirm(`${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, cpuMoe });
+ const m = _llmCatalog.find(x => x.id === pend.model);
+ if (!m || !m.hfRepo) { uiAlert(`${pend.model} steht nicht im Modell-Katalog — Herkunft unbekannt.`); return; }
+ let cpuMoe;
+ if (pend.model === current) {
+ const more = pend.ctx > cur;
+ if (!await uiConfirm(`${current} auf ${node} · Karte ${gpu} mit ${_ctxLabel(pend.ctx)} statt ${cur ? _ctxLabel(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.')) return;
+ cpuMoe = sl.cpuMoe > 0 ? -2 : (sl.cpuMoe || 0); // teilweise aufgeteilt → fuer den neuen Kontext neu rechnen
+ } else {
+ const vramGB = sl.vramMB ? sl.vramMB / 1024 : 0;
+ cpuMoe = await askCpuMoe(m, sl.vramMB || 0);
+ if (cpuMoe === null) return;
+ const warn = (!cpuMoe && vramGB && m.sizeGB && m.sizeGB > vramGB * 0.92)
+ ? `\n\n⚠ ${m.id} 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 (!await uiConfirm(`${m.id} mit ${_ctxLabel(pend.ctx)} Tokens Kontext auf ${node} · Karte ${gpu} laden?${repl}${warn}\n\nDie anderen Karten dieser Box starten dabei kurz neu.`)) return;
+ }
+ send({ action: 'llm_provision_model', targetInstance: target, gpu, key: m.id, hfRepo: m.hfRepo, quant: m.quant, ctx: pend.ctx, cpuMoe });
+ delete _llmCardPending[pkey];
+ renderLlmCardSettings();
openLlmDownloads();
}
@@ -9978,8 +10027,10 @@
const cpuMoe = await askCpuMoe(m, (tgt && tgt.slot && tgt.slot.vramMB) || 0);
if (cpuMoe === null) return;
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, cpuMoe });
+ const ctxSel = document.getElementById('llm-cat-ctx-' + i);
+ const ctx = Number((ctxSel && ctxSel.value) || (_llmCatSel[m.id] || {}).ctx || m.ctx || 8192);
+ if (st) { st.textContent = `Lade '${m.id}' mit ${_ctxLabel(ctx)} Kontext 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, cpuMoe });
openLlmDownloads();
}
function runLlmTest() {