fix(llm): Modelle mit anderer Quantisierung laden (Gemma 4 von unsloth)
Laden von unsloth/gemma-4-12B-it-qat-GGUF scheiterte mit "500": der Katalog
trug pauschal Q4_K_M ein, das Repo hat aber nur UD-Q4_K_XL. llama-server fand
die Datei nicht und beendete sich sofort; llama-swap reicht den Grund nicht
weiter ("upstream command exited prematurely"), es kam nur "500" an.
llm-adapter: fragt vor dem Laden die Dateiliste des Repos ab. Gibt es die
gewuenschte Quantisierung nicht, nimmt er die naechstbeste aus einer festen
Reihenfolge (Q4_K_M, UD-Q4_K_XL, Q4_K_XL, Q4_K_S, IQ4_XS, …) und sagt das im
Download-Fenster an. Kein ladbares GGUF im Repo → sofortige Absage, die Karte
bleibt unberuehrt. Bild-Projektor (mmproj), MTP-Zusatzdateien und imatrix
zaehlen nicht als Modell. Endet llama-server trotzdem sofort, nennt die
Meldung die haeufigsten Gruende statt nur "500".
Diagnostic: "Von HuggingFace aktualisieren" traegt pro HF-Repo die tatsaechlich
vorhandene Quantisierung und die echte Groesse ein (bisher 0 GB) — auch bei
schon vorhandenen HF-Eintraegen. Repos ohne ladbares Modell kommen nicht mehr
in den Katalog bzw. werden als "nicht ladbar" markiert. Katalog zeigt die
Quantisierung pro Modell.
Gemma 4 selbst: llama.cpp kann es seit Juni (#24077), der Stand auf der Box ist
vom 14.08. — es lag nur an der Datei.
Getestet gegen echte HF-Repos (Gemma → UD-Q4_K_XL 6,7 GB, Qwen/Mistral bleiben
Q4_K_M) in Adapter und Diagnostic identisch, und gegen llama-swap: Ausweichen
angesagt + geladen; Repo ohne GGUF → Absage ohne Download.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+92
-22
@@ -645,8 +645,33 @@ _INPROGRESS = ".downloadInProgress"
|
||||
_SPLIT_RE = re.compile(r"-(\d{5})-of-(\d{5})\.gguf$", re.IGNORECASE)
|
||||
|
||||
|
||||
async def _hf_expected_files(repo: str, quant: str) -> list:
|
||||
"""Welche GGUF-Dateien zieht 'repo:quant'? [{path, oid, size}]. Fail-soft: []."""
|
||||
# Quantisierung aus einem Dateinamen: "gemma-4-12B-it-qat-UD-Q4_K_XL.gguf" → "UD-Q4_K_XL"
|
||||
_QUANT_RE = re.compile(r"(?i)((?:UD-)?(?:IQ[1-4]_(?:XXS|XS|S|M|NL)|Q[2-8]_K(?:_(?:XL|L|M|S))?|Q[4-8]_[01]|BF16|F16|F32))")
|
||||
# Wenn die gewuenschte fehlt: die erste vorhandene aus dieser Liste. 4-Bit zuerst
|
||||
# (beste Groesse/Qualitaet fuer 12-GB-Karten), dann groessere.
|
||||
QUANT_PREFERENCE = ["Q4_K_M", "UD-Q4_K_XL", "Q4_K_XL", "Q4_K_S", "IQ4_XS", "IQ4_NL", "Q4_0",
|
||||
"Q5_K_M", "UD-Q5_K_XL", "Q6_K", "Q8_0"]
|
||||
|
||||
|
||||
def _quant_of(path: str) -> str:
|
||||
base = os.path.basename(path)
|
||||
base = _SPLIT_RE.sub(".gguf", base)
|
||||
m = _QUANT_RE.findall(base[:-5] if base.lower().endswith(".gguf") else base)
|
||||
return m[-1].upper() if m else ""
|
||||
|
||||
|
||||
def _is_main_gguf(path: str) -> bool:
|
||||
"""Das eigentliche Modell — nicht Bild-Projektor (mmproj), nicht die
|
||||
Zusatzdatei fuers spekulative Dekodieren (mtp), keine imatrix."""
|
||||
low = path.lower()
|
||||
base = os.path.basename(low)
|
||||
return (low.endswith(".gguf") and "mmproj" not in base and not base.startswith("mtp")
|
||||
and "/mtp/" not in ("/" + low) and "imatrix" not in base)
|
||||
|
||||
|
||||
async def _hf_repo_files(repo: str):
|
||||
"""Alle Modell-GGUFs eines Repos: [{path, oid, size, quant}] — oder None,
|
||||
wenn Hugging Face nicht erreichbar ist (dann pruefen wir eben nicht)."""
|
||||
url = f"https://huggingface.co/api/models/{repo}/tree/main?recursive=1"
|
||||
headers = {"Authorization": f"Bearer {HF_TOKEN}"} if HF_TOKEN else {}
|
||||
try:
|
||||
@@ -655,28 +680,42 @@ async def _hf_expected_files(repo: str, quant: str) -> list:
|
||||
r.raise_for_status()
|
||||
entries = r.json()
|
||||
except Exception as e:
|
||||
logger.warning("HF-Dateiliste fuer %s nicht abrufbar (%s) — Fortschritt ohne Gesamtgroesse", repo, e)
|
||||
return []
|
||||
q = (quant or "").lower()
|
||||
files = []
|
||||
logger.warning("HF-Dateiliste fuer %s nicht abrufbar (%s)", repo, e)
|
||||
return None
|
||||
out = []
|
||||
for e in entries if isinstance(entries, list) else []:
|
||||
path = e.get("path") or ""
|
||||
low = path.lower()
|
||||
if e.get("type") != "file" or not low.endswith(".gguf") or "mmproj" in low:
|
||||
continue
|
||||
if q and q not in low:
|
||||
if e.get("type") != "file" or not _is_main_gguf(path):
|
||||
continue
|
||||
lfs = e.get("lfs") or {}
|
||||
files.append({"path": path, "oid": lfs.get("oid") or e.get("oid") or "",
|
||||
"size": int(lfs.get("size") or e.get("size") or 0)})
|
||||
if not files:
|
||||
return []
|
||||
# Gesplittetes Modell → alle Teile; sonst die eine passende Datei (bei
|
||||
# mehreren Treffern die mit dem kuerzesten Namen, z.B. ohne "-imat").
|
||||
split = [f for f in files if _SPLIT_RE.search(f["path"])]
|
||||
out.append({"path": path, "oid": lfs.get("oid") or e.get("oid") or "",
|
||||
"size": int(lfs.get("size") or e.get("size") or 0), "quant": _quant_of(path)})
|
||||
return out
|
||||
|
||||
|
||||
def _pick_quant(files: list, wanted: str) -> str:
|
||||
"""Die gewuenschte Quantisierung, wenn es sie gibt — sonst die erste aus
|
||||
QUANT_PREFERENCE, die das Repo hat. "" wenn gar nichts Brauchbares da ist."""
|
||||
have = {f["quant"] for f in files if f["quant"]}
|
||||
if wanted and wanted.upper() in have:
|
||||
return wanted.upper()
|
||||
return next((q for q in QUANT_PREFERENCE if q in have), "")
|
||||
|
||||
|
||||
def _files_for_quant(files: list, quant: str) -> list:
|
||||
"""Die Datei(en), die llama.cpp fuer repo:quant zieht. Gesplittet → alle
|
||||
Teile; sonst die eine (bei mehreren die mit dem kuerzesten Namen)."""
|
||||
match = [f for f in files if f["quant"] == quant.upper()]
|
||||
split = [f for f in match if _SPLIT_RE.search(f["path"])]
|
||||
if split:
|
||||
return split
|
||||
return [min(files, key=lambda f: len(f["path"]))]
|
||||
return [min(match, key=lambda f: len(f["path"]))] if match else []
|
||||
|
||||
|
||||
async def _hf_expected_files(repo: str, quant: str) -> list:
|
||||
"""Welche GGUF-Dateien zieht 'repo:quant'? [{path, oid, size}]. Fail-soft: []."""
|
||||
files = await _hf_repo_files(repo)
|
||||
return _files_for_quant(files, quant) if files else []
|
||||
|
||||
|
||||
def _scan_download(oids: set) -> tuple:
|
||||
@@ -792,10 +831,32 @@ async def _handle_provision(ws, payload: dict) -> None:
|
||||
return await _provision_result(
|
||||
ws, key, False, gpu=card,
|
||||
error=f"{key} liegt schon auf Karte {elsewhere} dieser Box — ein Modell pro Box nur einmal.")
|
||||
quant_note = ""
|
||||
repo_files = None
|
||||
if repo:
|
||||
wanted = (payload.get("quant") or "Q4_K_M").strip()
|
||||
quant = wanted
|
||||
# Gibt es die Datei ueberhaupt? Fehlt sie, beendet sich llama-server
|
||||
# sofort und llama-swap meldet nur "500" — der Grund geht verloren.
|
||||
# Lieber vorher pruefen und ggf. die naechstbeste Quantisierung nehmen
|
||||
# (unsloth z.B. hat statt Q4_K_M ein UD-Q4_K_XL).
|
||||
repo_files = await _hf_repo_files(repo)
|
||||
if repo_files is not None:
|
||||
if not repo_files:
|
||||
return await _provision_result(
|
||||
ws, key, False, gpu=card,
|
||||
error=f"In {repo} gibt es keine GGUF-Modelldatei.")
|
||||
quant = _pick_quant(repo_files, wanted)
|
||||
if not quant:
|
||||
have = sorted({f["quant"] for f in repo_files if f["quant"]}) or ["?"]
|
||||
return await _provision_result(
|
||||
ws, key, False, gpu=card,
|
||||
error=f"{repo}: keine brauchbare Quantisierung. Vorhanden: {', '.join(have)}")
|
||||
if quant != wanted.upper():
|
||||
quant_note = f"{wanted} gibt es in {repo} nicht — nehme {quant}"
|
||||
logger.info("Karte %s: %s", card, quant_note)
|
||||
entry = {
|
||||
"key": key, "hfRepo": repo,
|
||||
"quant": (payload.get("quant") or "Q4_K_M").strip(),
|
||||
"key": key, "hfRepo": repo, "quant": quant,
|
||||
"ctx": int(payload.get("ctx") or 8192),
|
||||
"ngl": int(payload.get("ngl") or 99),
|
||||
}
|
||||
@@ -809,9 +870,10 @@ async def _handle_provision(ws, payload: dict) -> None:
|
||||
_generate_config()
|
||||
|
||||
quant = next((e.get("quant") for e in _load_registry() if e.get("key") == key), "") or "Q4_K_M"
|
||||
files = await _hf_expected_files(repo, quant) if repo else []
|
||||
files = _files_for_quant(repo_files, quant) if repo_files else []
|
||||
await _emit_progress(ws, key, "start", total=sum(f["size"] for f in files),
|
||||
files=len(files), gpu=card, replaces=previous)
|
||||
files=len(files), gpu=card, replaces=previous, quant=quant,
|
||||
note=quant_note)
|
||||
await _reload_llama()
|
||||
await _announce(ws)
|
||||
stop = asyncio.Event()
|
||||
@@ -842,6 +904,14 @@ async def _handle_provision(ws, payload: dict) -> None:
|
||||
# ein kaputtes Modell in der Config, und jedes Neuladen von llama-swap
|
||||
# risse die anderen Karten mit.
|
||||
err = (res.get("error") or "Laden fehlgeschlagen")[:200]
|
||||
if "500" in err and dt < 120:
|
||||
# llama-swap reicht den Grund nicht weiter ("upstream command exited
|
||||
# prematurely"). Kam der Abbruch schnell, lief kein Download — dann ist
|
||||
# es fast immer eins von dreien.
|
||||
err = ("llama.cpp hat sich beim Start sofort beendet. Haeufigste Gruende: "
|
||||
"die Modell-Architektur ist neuer als die llama.cpp-Version der Box, "
|
||||
"zu wenig (V)RAM, oder die Datei passt nicht. (llama-swap gibt den "
|
||||
"genauen Grund nicht weiter.)")
|
||||
slots = _load_slots()
|
||||
if previous:
|
||||
slots[card] = previous
|
||||
|
||||
Reference in New Issue
Block a user