feat(llm): Downloads parallel, abbrechbar; heruntergeladene Modelle loeschen
llm-adapter: - Paralleler Download: llama.cpp zieht ueber EINE Verbindung (oft 1-50 MB/s, auch bei 1 Gbit). Die Box holt fehlende Dateien jetzt vorher selbst in 64-MB-Stuecken ueber LLM_DOWNLOAD_CONNECTIONS (Standard 8) Verbindungen, prueft die sha256 und legt sie im HF-Cache-Layout ab, das llama.cpp selbst benutzt (blobs/<sha256>, snapshots/<commit>/<datei>, refs/main). Die Karte bleibt waehrenddessen beim alten Modell. Scheitert es, laedt llama.cpp wie bisher selbst. Fortschritt zaehlt bei sparse-Dateien nur geschriebene Bytes. - Abbrechen: llm_remove_model mit cancel=true (kein neuer Typ → RVS-Allow-List unveraendert). Waehrend des Downloads: Stopp + halbe Datei weg, Karte unveraendert. Waehrend llama.cpp laedt: wie ein Fehlschlag → Karte zurueck. Laeuft nichts mehr, schliesst die Antwort den haengenden Eintrag. - Loeschen: llm_remove_model mit deleteFiles=true entfernt das Repo aus dem Cache — nicht, solange es auf einer Karte liegt oder geladen wird. - worker_hello meldet "disk": freier Platz + heruntergeladene Modelle je Repo. Diagnostic: Abbrechen-Knopf je laufendem Download; im Katalog "auf ki-box · 18,6 GB" mit Loeschen-Knopf und eine Zeile mit dem freien Platz je Box. Getestet gegen HuggingFace (ggml-org/tiny-llamas, 4 Verbindungen, 2-MB- Stuecke: Layout + Pruefsumme + Abbruch) und Loeschen inkl. Sperren. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+31
-4
@@ -2731,7 +2731,8 @@
|
||||
const p = msg.payload || {};
|
||||
const st = document.getElementById('llm-catalog-status');
|
||||
if (st) {
|
||||
if (p.removed) { st.textContent = `✓ '${p.key}' entfernt`; st.style.color = '#8888AA'; }
|
||||
if (p.deleted) { st.textContent = `✓ '${p.key}' von der Box gelöscht` + (p.freedBytes ? ` (${_fmtBytes(p.freedBytes)} frei geworden)` : ''); st.style.color = '#8888AA'; }
|
||||
else if (p.removed) { st.textContent = `✓ '${p.key}' entfernt`; st.style.color = '#8888AA'; }
|
||||
else if (p.unloaded) { st.textContent = `✓ Karte ${p.gpu} geleert (${p.key})`; st.style.color = '#8888AA'; }
|
||||
else if (p.ok) { st.textContent = `✓ '${p.key}' geladen/aktiv auf ${p.instanceId || '?'}`; st.style.color = '#3FFF3F'; }
|
||||
else { st.textContent = `✗ '${p.key}': ${p.error || 'Fehler'}`; st.style.color = '#FF6B6B'; }
|
||||
@@ -9301,7 +9302,14 @@
|
||||
const sl = (b.slots || []).find(x => x.model === m.id);
|
||||
return escapeHtml(b.node) + (sl ? ' · Karte ' + escapeHtml(sl.gpu) : '');
|
||||
};
|
||||
const haveTxt = have.length ? `<span style="color:#3FFF3F;">✓ ${have.map(where).join(', ')}</span>` : '<span style="color:#6a6a88;">nicht geladen</span>';
|
||||
// Liegt nur auf der Platte (keine Karte)? → Groesse + Loeschen
|
||||
const stored = boxes.filter(b => !have.includes(b))
|
||||
.map(b => ({ b, d: ((b.disk && b.disk.models) || []).find(x => x.repo === m.hfRepo) })).filter(x => x.d);
|
||||
const storedTxt = stored.map(({ b, d }) => `<div style="color:#8888AA;">💾 auf ${escapeHtml(b.node)} · ${_fmtBytes(d.bytes)}
|
||||
<button class="btn secondary" onclick="deleteLlmFiles('${escapeHtmlAttr(b.instanceId)}', ${i})" title="Heruntergeladene Datei von der Box löschen (gibt Platz frei)"
|
||||
style="padding:1px 6px;font-size:10px;margin-left:4px;">Löschen</button></div>`).join('');
|
||||
const haveTxt = have.length ? `<span style="color:#3FFF3F;">✓ ${have.map(where).join(', ')}</span>` + storedTxt
|
||||
: (storedTxt || '<span style="color:#6a6a88;">nicht geladen</span>');
|
||||
const vram = m.sizeGB ? '~' + m.sizeGB + ' GB' : '—';
|
||||
const vramStyle = m.sizeGB >= 12 ? 'color:#FFB020;' : 'color:#AAB;';
|
||||
// Ziel = Karte. Vorauswahl: eine leere Karte, sonst die erste.
|
||||
@@ -9323,7 +9331,9 @@
|
||||
'<td ' + td + '"><button class="btn secondary" ' + (boxes.length ? '' : 'disabled') + ' onclick="provisionModel(' + i + ')" style="padding:3px 10px;font-size:11px;">Laden</button></td>' +
|
||||
'</tr>';
|
||||
}).join('');
|
||||
box.innerHTML = '<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:760px;">' +
|
||||
const diskLine = boxes.filter(b => b.disk).map(b => `${escapeHtml(b.node)}: ${_fmtBytes(b.disk.freeBytes)} frei`).join(' · ');
|
||||
box.innerHTML = (diskLine ? '<div style="color:#6a6a88;font-size:11px;margin:0 0 6px;">💾 Speicherplatz — ' + diskLine + '</div>' : '') +
|
||||
'<table style="width:100%;border-collapse:collapse;font-size:12px;min-width:760px;">' +
|
||||
'<thead><tr style="color:#8888AA;border-bottom:1px solid #2A2A3E;">' +
|
||||
'<th ' + th + '>Modell</th><th ' + th + '>Kann gut / Beschreibung</th><th ' + th + '>VRAM</th>' +
|
||||
'<th ' + th + '>Status</th><th ' + th + '>Ziel-Karte</th><th ' + th + '></th>' +
|
||||
@@ -9411,7 +9421,9 @@
|
||||
return `<div style="background:#12121E;border:1px solid #2A2A3E;border-radius:8px;padding:10px 12px;">
|
||||
<div style="display:flex;justify-content:space-between;gap:10px;">
|
||||
<strong style="color:#E0E0F0;">${escapeHtml(j.key)}</strong>
|
||||
<span style="color:#8888AA;font-size:11px;">→ ${box_}</span>
|
||||
<span style="color:#8888AA;font-size:11px;">→ ${box_}
|
||||
${['ready', 'error'].includes(j.phase) ? '' : `<button class="btn secondary" onclick="cancelLlmJob('${escapeHtmlAttr(j.instanceId)}','${escapeHtmlAttr(j.key)}')"
|
||||
style="padding:1px 8px;font-size:10px;margin-left:8px;">Abbrechen</button>`}</span>
|
||||
</div>${bar}
|
||||
<div style="color:${color === '#0096FF' ? '#E0E0F0' : color};font-size:12px;">${line}</div>
|
||||
${sub ? `<div style="color:#6a6a88;font-size:11px;margin-top:2px;">${sub}</div>` : ''}
|
||||
@@ -9862,6 +9874,21 @@
|
||||
});
|
||||
}
|
||||
|
||||
function deleteLlmFiles(instanceId, i) {
|
||||
const m = _llmCatalog[i];
|
||||
if (!m) return;
|
||||
const b = onlineLlmBoxes().find(w => w.instanceId === instanceId);
|
||||
const d = ((b && b.disk && b.disk.models) || []).find(x => x.repo === m.hfRepo);
|
||||
if (!confirm(`${m.id} von ${(b && b.node) || instanceId} löschen?\n\n`
|
||||
+ `Die heruntergeladene Datei (${d ? _fmtBytes(d.bytes) : '?'}, Repo ${m.hfRepo}) wird von der Box entfernt. `
|
||||
+ `Wer es wieder will, muss es neu herunterladen.`)) return;
|
||||
send({ action: 'llm_remove_model', targetInstance: instanceId, key: m.id, hfRepo: m.hfRepo, deleteFiles: true });
|
||||
}
|
||||
function cancelLlmJob(instanceId, key) {
|
||||
if (!confirm(`Laden von ${key} abbrechen?\n\nDer halbe Download wird gelöscht, die Karte bleibt bzw. wird wie vorher.`)) return;
|
||||
send({ action: 'llm_remove_model', targetInstance: instanceId, key, cancel: true });
|
||||
}
|
||||
|
||||
async function provisionModel(i) {
|
||||
const m = _llmCatalog[i];
|
||||
if (!m) return;
|
||||
|
||||
Reference in New Issue
Block a user