feat(llm): Speicherplatz vor dem Download pruefen, klarer Aufteilen-Dialog
llm-adapter: Vor jeder Aenderung an Registry/Belegung rechnet die Box aus,
was vom Modell noch fehlt (fertige Blobs zaehlen nicht), und vergleicht es
mit dem freien Platz minus Puffer (LLM_DISK_RESERVE_GB, Standard 2). Reicht
es nicht, werden halbe Downloads, an denen seit 30 min niemand schreibt,
geloescht und neu gerechnet; reicht es dann immer noch nicht, bricht das
Laden mit klarer Meldung im Download-Fenster ab ("braucht ca. X GB, frei
sind Y GB"). Frueher brach llama.cpp mitten im Download ab, und der Grund
kam ueber llama-swap nicht an.
Diagnostic: Die MoE-Frage war ein confirm(), bei dem "Abbrechen" "ganz auf
die Karte laden" hiess. Jetzt ein Dialog mit drei Knoepfen: Aufteilen
(empfohlen) / Ganz auf die Karte / Abbrechen (laedt nichts). Esc und Klick
daneben brechen ab.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import logging
|
||||
import os
|
||||
import re
|
||||
import shlex
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import httpx
|
||||
@@ -1141,6 +1142,63 @@ def _scan_download(oids: set) -> tuple:
|
||||
return done, active, found
|
||||
|
||||
|
||||
# Freier Platz vor dem Download: llama.cpp bricht bei voller Platte mitten
|
||||
# im Download ab, und der Grund kommt ueber llama-swap nicht an. Vorher
|
||||
# rechnen: was fehlt noch (fertige Blobs zaehlen nicht) gegen den freien
|
||||
# Platz minus Puffer. Halbe Downloads, an denen seit STALE_PARTIAL_S niemand
|
||||
# mehr schreibt, sind Reste abgebrochener Versuche — die duerfen weg.
|
||||
DISK_RESERVE_BYTES = int(float(os.getenv("LLM_DISK_RESERVE_GB", "2")) * 1024**3)
|
||||
STALE_PARTIAL_S = 30 * 60
|
||||
|
||||
|
||||
def _blob_state(oids: set) -> tuple:
|
||||
"""({oid: Bytes fertig}, [(pfad, bytes, mtime)] aller halben Downloads)."""
|
||||
done, partial = {}, []
|
||||
for root, _dirs, fns in os.walk(MODELS_DIR):
|
||||
for fn in fns:
|
||||
path = os.path.join(root, fn)
|
||||
try:
|
||||
st = os.stat(path)
|
||||
except OSError:
|
||||
continue
|
||||
if fn.endswith(_INPROGRESS):
|
||||
partial.append((path, st.st_size, st.st_mtime))
|
||||
elif fn in oids:
|
||||
done[fn] = st.st_size
|
||||
return done, partial
|
||||
|
||||
|
||||
def _check_disk(files: list) -> tuple:
|
||||
"""(ok, Meldung). Ohne HF-Dateiliste (files leer) wird nicht geprueft."""
|
||||
if not files:
|
||||
return True, ""
|
||||
done, partial = _blob_state({f["oid"] for f in files if f.get("oid")})
|
||||
need = sum(f["size"] for f in files if f.get("oid") not in done)
|
||||
if not need:
|
||||
return True, ""
|
||||
free = shutil.disk_usage(MODELS_DIR).free
|
||||
note = ""
|
||||
if need + DISK_RESERVE_BYTES > free:
|
||||
stale = [(p, b) for p, b, mt in partial if time.time() - mt > STALE_PARTIAL_S]
|
||||
for path, _b in stale:
|
||||
try:
|
||||
os.remove(path)
|
||||
except OSError:
|
||||
pass
|
||||
if stale:
|
||||
freed = sum(b for _p, b in stale)
|
||||
note = f"Reste abgebrochener Downloads geloescht ({len(stale)} Datei(en), {freed / 1024**3:.1f} GB)"
|
||||
logger.info("Speicherplatz: %s", note)
|
||||
free = shutil.disk_usage(MODELS_DIR).free
|
||||
if need + DISK_RESERVE_BYTES > free:
|
||||
gb = lambda n: f"{n / 1024**3:.1f}".replace(".", ",")
|
||||
return False, (f"Zu wenig Speicherplatz auf {NODE_NAME}: das Modell braucht ca. {gb(need)} GB, "
|
||||
f"frei sind {gb(free)} GB (dazu {gb(DISK_RESERVE_BYTES)} GB Puffer). "
|
||||
f"Nicht mehr gebrauchte Modelle im Katalog entfernen oder Platz auf der Box schaffen."
|
||||
+ (f" ({note})" if note else ""))
|
||||
return True, note
|
||||
|
||||
|
||||
async def _gpu_info(card: str) -> dict:
|
||||
"""Name + VRAM einer Karte nach dem Laden (fuer 'in Karte N geladen')."""
|
||||
if not card:
|
||||
@@ -1258,6 +1316,13 @@ async def _handle_provision(ws, payload: dict) -> None:
|
||||
if quant != wanted.upper():
|
||||
quant_note = f"{wanted} gibt es in {repo} nicht — nehme {quant}"
|
||||
logger.info("Karte %s: %s", card, quant_note)
|
||||
# Vor jeder Aenderung an Registry/Belegung: passt es auf die Platte?
|
||||
disk_ok, disk_note = await asyncio.to_thread(_check_disk, _files_for_quant(repo_files, quant))
|
||||
if not disk_ok:
|
||||
await _emit_progress(ws, key, "error", gpu=card, error=disk_note)
|
||||
return await _provision_result(ws, key, False, gpu=card, error=disk_note)
|
||||
if disk_note:
|
||||
quant_note = "; ".join(x for x in (quant_note, disk_note) if x)
|
||||
ctx_req = int(payload.get("ctx") or 8192)
|
||||
try:
|
||||
cpu_moe = int(payload.get("cpuMoe") or 0)
|
||||
|
||||
Reference in New Issue
Block a user