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 @@ '' + '' + '' + - '' + + '' + '' + rows + '
ModellKann gut / BeschreibungVRAMStatusZiel-KarteStatusZiel-KarteKontext
' + ''; 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() {