Feat: Vulkan/RADV-Engine + vocab-gepruefte Spec-Drafts + Provisioning/Sync

Engine-Cutover ROCm/HIP -> Vulkan/RADV (gfx1151): +12-22% tg auf MoE (llama-bench
verifiziert, fast 53->65 t/s). ROCm-Build bleibt als Rollback unter /opt/llamacpp.

- Backend: vocab-aware Speculative Decoding. services/gguf_meta.py liest den
  Tokenizer-Fingerprint (model/pre/n_vocab) direkt aus dem GGUF-Header (ohne Modell-Load);
  register_model + migrate_config haengen nur VOCAB-KOMPATIBLE Drafts an (inkl. --spec-type,
  das in dieser llama.cpp-Generation noetig ist). Neue Endpoints /api/models/drafts + /{id}/draft.
- Frontend: idiotensichere Spec-Draft-UI (SpecDraftModal) - nur kompatible Drafts waehlbar,
  inkompatible gesperrt mit Begruendung; SPEC/SPEC?-Badge nach echtem Aktiv-Status; Rolle in AddModel.
- maintenance.py: Engine-Update-Quelle -> ggml-org/llama.cpp (Build-Nummer-Vergleich),
  ENGINE_PATH=/opt/llamacpp-vulkan.
- Startup-Warmup der brains (deploy/warmup.sh, self-detaching ExecStartPost) + deploy/provision-engine.sh.
- Cleanup: tote LiteLLM gateway/config.yaml + alle Referenzen (config.py/backup.py/backup.sh) entfernt;
  README + docs/memory aktualisiert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-27 01:54:29 +02:00
parent 530d77ff1b
commit c48e583790
23 changed files with 1128 additions and 508 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -7,8 +7,8 @@
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<title>Mission Control 2.0</title>
<script type="module" crossorigin src="/assets/index-D4mBflTE.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C0dfTDFj.css">
<script type="module" crossorigin src="/assets/index-DPqptZjm.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CjZVMvhL.css">
</head>
<body>
<div id="root"></div>
@@ -0,0 +1,161 @@
import { useState } from "react"
import { X, Check, Zap, AlertTriangle } from "lucide-react"
import { api, type ModelInfo, type DraftInfo } from "@/lib/api"
import { useDrafts } from "@/lib/queries"
import { fmtSize } from "@/lib/format"
import { cn } from "@/lib/utils"
/**
* Idiotensichere Speculative-Decoding-Konfiguration für EIN Modell.
* Zeigt nur VOCAB-KOMPATIBLE Drafts als wählbar; inkompatible werden gesperrt
* und mit Begründung angezeigt. So kann nie ein kaputter Draft gesetzt werden
* (der das Modell beim Laden scheitern ließe).
*/
export function SpecDraftModal({
model, onClose, onChanged,
}: { model: ModelInfo; onClose: () => void; onChanged: () => void }) {
const { data, isLoading } = useDrafts(model.gguf_path)
const [busy, setBusy] = useState<string | null>(null)
const [err, setErr] = useState("")
const tv = data?.target_vocab
const drafts = data?.drafts ?? []
const compatibles = drafts.filter((d) => d.compatible === true)
const currentFile = model.spec_draft_model
async function apply(draftPath: string | null) {
setBusy(draftPath ?? "__clear__")
setErr("")
try {
await api(`/api/models/${encodeURIComponent(model.name)}/draft`, {
method: "POST",
body: JSON.stringify({ draft_path: draftPath }),
})
onChanged()
onClose()
} catch (e: any) {
setErr(String(e?.message || e))
setBusy(null)
}
}
const vocabLabel = (v?: { pre: string | null; n_vocab: number | null } | null) =>
v ? `${v.pre ?? "?"} · ${v.n_vocab?.toLocaleString() ?? "?"} Tokens` : "—"
return (
<div className="fixed inset-0 z-50 bg-black/60 backdrop-blur-sm flex items-center justify-center p-4">
<div className="w-full max-w-lg rounded-2xl border border-border/80 bg-card p-6 shadow-2xl space-y-4 animate-in fade-in zoom-in-95 duration-200">
<div className="flex items-center justify-between border-b border-border/20 pb-2">
<h3 className="text-xs font-bold uppercase tracking-wider text-primary flex items-center gap-2">
<Zap className="h-4 w-4" /> Speculative Draft
</h3>
<button onClick={onClose} className="p-1 rounded hover:bg-accent text-muted-foreground hover:text-foreground cursor-pointer">
<X className="h-4 w-4" />
</button>
</div>
<div className="text-xs text-muted-foreground leading-relaxed">
Beschleunigt die Token-Generierung mit einem kleinen "Draft"-Modell. Voraussetzung:
der Draft muss den <strong className="text-foreground">exakt gleichen Tokenizer (Vocab)</strong> haben
wie das Modell sonst lehnt llama.cpp es ab.
</div>
{/* Ziel-Vocab */}
<div className="rounded-lg border border-border/40 bg-background/30 px-3 py-2 text-[11px] font-mono flex items-center justify-between">
<span className="text-muted-foreground">{model.name.split("/").pop()?.replace(/\.gguf$/i, "")}</span>
<span className="text-foreground">Vocab: {vocabLabel(tv)}</span>
</div>
{/* Aktueller Zustand */}
{model.spec_active && currentFile && (
<div className="rounded-lg border border-emerald-500/30 bg-emerald-500/5 px-3 py-2 flex items-center justify-between gap-2">
<span className="text-[11px] text-emerald-400 font-mono flex items-center gap-1.5 truncate">
<Check className="h-3.5 w-3.5 shrink-0" /> Aktiv: {currentFile}
</span>
<button
onClick={() => apply(null)}
disabled={busy !== null}
className="h-6 px-2.5 rounded border border-amber-500/30 bg-amber-500/5 hover:bg-amber-500/10 text-amber-400 text-[9px] font-bold uppercase shrink-0 cursor-pointer disabled:opacity-50"
>
Deaktivieren
</button>
</div>
)}
{!data?.target_exists && (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-[11px] text-amber-400 flex items-center gap-2">
<AlertTriangle className="h-3.5 w-3.5 shrink-0" />
Modell-GGUF noch nicht vorhanden Spec-Draft ist nach dem Download konfigurierbar.
</div>
)}
{/* Draft-Liste */}
<div className="space-y-1.5 max-h-64 overflow-y-auto pr-1">
{isLoading ? (
<div className="text-xs text-muted-foreground py-6 text-center">Prüfe Vocab-Kompatibilität</div>
) : drafts.length === 0 ? (
<div className="text-[11px] text-muted-foreground border border-dashed border-border/50 rounded-lg p-4 text-center">
Keine Draft-Modelle in <code className="font-mono">/srv/models/drafts</code>.
Lade ein kleines same-family-Modell (z.B. Qwen3-0.6B) und lege es dort ab.
</div>
) : (
drafts.map((d: DraftInfo) => {
const isCurrent = d.filename === currentFile
const ok = d.compatible === true
return (
<div
key={d.path}
className={cn(
"rounded-lg border px-3 py-2 flex items-center justify-between gap-2 text-left",
ok ? "border-border/40 bg-background/20" : "border-border/20 bg-background/10 opacity-60",
isCurrent && "border-primary/40 bg-primary/10",
)}
>
<div className="min-w-0">
<div className="text-[11px] font-mono font-semibold text-foreground truncate">{d.filename}</div>
<div className="text-[9px] text-muted-foreground font-mono">
{fmtSize(d.size_bytes)} · Vocab: {vocabLabel(d.vocab)}
</div>
</div>
{ok ? (
isCurrent ? (
<span className="text-[9px] font-bold uppercase text-primary flex items-center gap-1 shrink-0"><Check className="h-3.5 w-3.5" /> Aktiv</span>
) : (
<button
onClick={() => apply(d.path)}
disabled={busy !== null}
className="h-7 px-3 rounded-lg border border-emerald-500/30 bg-emerald-500/5 hover:bg-emerald-500/15 text-emerald-400 text-[9px] font-bold uppercase shrink-0 cursor-pointer disabled:opacity-50"
>
Aktivieren
</button>
)
) : (
<span
className="text-[9px] font-bold uppercase text-amber-400/80 flex items-center gap-1 shrink-0"
title={d.compatible === false
? `Inkompatibel: Tokenizer/Vocab weicht ab (Draft ${d.vocab?.pre}/${d.vocab?.n_vocab} ≠ Modell ${tv?.pre}/${tv?.n_vocab}).`
: "Kompatibilität nicht prüfbar (Datei fehlt)."}
>
<AlertTriangle className="h-3.5 w-3.5" /> {d.compatible === false ? "Vocab ≠" : "n/a"}
</span>
)}
</div>
)
})
)}
</div>
{/* Hinweis, wenn Drafts da sind aber keiner kompatibel */}
{!isLoading && data?.target_exists && drafts.length > 0 && compatibles.length === 0 && (
<div className="rounded-lg border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-[11px] text-amber-400 leading-relaxed">
Kein vocab-kompatibler Draft verfügbar Speculative Decoding ist für dieses Modell nicht möglich.
Es braucht einen Draft mit identischem Tokenizer (pre=<span className="font-mono">{tv?.pre}</span>,
n_vocab=<span className="font-mono">{tv?.n_vocab?.toLocaleString()}</span>).
</div>
)}
{err && <div className="text-[10px] text-red-400 font-mono">{err}</div>}
</div>
</div>
)
}
+24
View File
@@ -88,10 +88,34 @@ export interface ModelInfo {
incomplete: boolean
prompt_cache: boolean
spec_draft_model: string | null
spec_type: string | null
spec_active: boolean
parallel_slots: number
capabilities: Capabilities
}
export interface VocabFingerprint {
model: string | null
pre: string | null
n_vocab: number | null
arch: string | null
}
export interface DraftInfo {
path: string
filename: string
size_bytes: number | null
vocab: VocabFingerprint | null
compatible: boolean | null // null = nicht prüfbar (Ziel-GGUF fehlt)
}
export interface DraftsResp {
target_path: string
target_exists: boolean
target_vocab: VocabFingerprint | null
drafts: DraftInfo[]
}
export interface DiscoverModel {
name: string
author: string
+10
View File
@@ -8,6 +8,7 @@ import {
type AgentStatus,
type ConnectResp,
type DiscoverResp,
type DraftsResp,
type Health,
type Job,
type Memory,
@@ -31,6 +32,7 @@ export const qk = {
agentStatus: ["agent-status"] as const,
updates: ["updates"] as const,
discover: ["discover"] as const,
drafts: (target?: string) => ["drafts", target ?? ""] as const,
connect: (params?: string) => ["connect", params ?? ""] as const,
memory: (q?: string, category?: string) => ["memory", q ?? "", category ?? ""] as const,
}
@@ -70,6 +72,14 @@ export const useUpdates = (refetchInterval?: number) =>
export const useDiscover = () =>
useQuery({ queryKey: qk.discover, queryFn: () => api<DiscoverResp>("/api/discover") })
// Verfügbare Spec-Draft-Modelle + Vocab-Kompatibilität zum Ziel-Modell (GGUF-Pfad).
export const useDrafts = (target?: string) =>
useQuery({
queryKey: qk.drafts(target),
queryFn: () => api<DraftsResp>(`/api/models/drafts?target=${encodeURIComponent(target ?? "")}`),
enabled: !!target,
})
export const useConnect = (params?: string) =>
useQuery({
queryKey: qk.connect(params),
+23 -6
View File
@@ -7,8 +7,10 @@ export function AddModel() {
const [quants, setQuants] = useState<string[]>([])
const [quant, setQuant] = useState("Q4_K_M")
const [msg, setMsg] = useState("")
const [role, setRole] = useState("")
const [q, setQ] = useState("")
const [results, setResults] = useState<{ repo: string; downloads: number }[]>([])
const ROLE_OPTS = ["fast", "heavy", "coder", "vision", "scout"]
async function loadQuants(r?: string) {
const rr = r ?? repo
@@ -42,10 +44,16 @@ export function AddModel() {
setMsg("Download-Job wird initiiert...")
try {
await api("/api/models/install", {
method: "POST",
body: JSON.stringify({ repo, quant, jinja: true }),
method: "POST",
body: JSON.stringify({ repo, quant, role: role || undefined, jinja: true }),
})
setMsg(`Download gestartet: ${repo} (${quant}). Fortschritt wird oben angezeigt.`)
setMsg(
`Download gestartet: ${repo} (${quant})${role ? `, Rolle: ${role}` : ""}. ` +
`Fortschritt oben.` +
(role === "fast" || role === "coder"
? " Speculative Draft (Vocab-geprüft) kannst du nach dem Download an der Modellkarte (»Spec«) setzen."
: "")
)
} catch (e) {
setMsg(`Download-Fehler: ${e}`)
}
@@ -71,13 +79,22 @@ export function AddModel() {
</button>
{quants.length > 0 && (
<>
<select
value={quant}
onChange={(e) => setQuant(e.target.value)}
<select
value={quant}
onChange={(e) => setQuant(e.target.value)}
className="h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none text-foreground font-semibold"
>
{quants.map((qq) => <option key={qq} value={qq} className="bg-popover text-foreground">{qq}</option>)}
</select>
<select
value={role}
onChange={(e) => setRole(e.target.value)}
title="Rolle (optional) — bestimmt Auto-Konfiguration wie parallele Slots"
className="h-9 rounded-lg border border-border/60 bg-background/40 px-3 text-xs outline-none text-foreground font-semibold"
>
<option value="" className="bg-popover text-foreground">Rolle</option>
{ROLE_OPTS.map((r) => <option key={r} value={r} className="bg-popover text-foreground">{r}</option>)}
</select>
<button
onClick={install}
className="h-9 px-4 rounded-lg bg-primary text-primary-foreground text-xs font-semibold hover:opacity-90 transition-all cursor-pointer flex items-center gap-1.5"
+51 -9
View File
@@ -1,12 +1,13 @@
import { useState, useRef, useCallback } from "react"
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy } from "lucide-react"
import { api } from "@/lib/api"
import { Download, Trash2, Edit3, Activity, HardDrive, X, Check, Copy, Zap } from "lucide-react"
import { api, type ModelInfo } from "@/lib/api"
import { useModels, useRouting, useConnect, useUpdates, useQueryClient, qk } from "@/lib/queries"
import { useDialog } from "@/lib/useDialog"
import { CapsChips } from "@/components/CapsChips"
import { cn } from "@/lib/utils"
import { fmtSize, fmtCtx } from "@/lib/format"
import { getBrandInfo, ROLES } from "@/components/models/ModelBadges"
import { SpecDraftModal } from "@/components/models/SpecDraftModal"
export function Cockpit() {
const qc = useQueryClient()
@@ -26,6 +27,7 @@ export function Cockpit() {
// UI state
const [activeClient, setActiveClient] = useState<"roocode" | "cursor" | "opencode" | "zed" | "continue" | null>(null)
const [activeRoleForAssign, setActiveRoleForAssign] = useState<string | null>(null)
const [specModel, setSpecModel] = useState<ModelInfo | null>(null)
const [copied, setCopied] = useState(false)
const [hoveredNode, setHoveredNode] = useState<string | null>(null)
const [viewMode, setViewMode] = useState<"grid" | "list">("grid")
@@ -742,11 +744,15 @@ export function Cockpit() {
PC
</span>
)}
{m.spec_draft_model && (
{m.spec_active ? (
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[9px] font-mono text-emerald-400 font-bold uppercase" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
SPEC
</span>
)}
) : m.spec_draft_model ? (
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[9px] font-mono text-amber-400 font-bold uppercase" title={`Draft gesetzt (${m.spec_draft_model}), aber INAKTIV — --spec-type fehlt oder Vocab inkompatibel. Über 'Spec' neu konfigurieren.`}>
SPEC?
</span>
) : null}
{m.parallel_slots > 1 && (
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[9px] font-mono text-violet-400 font-bold uppercase" title={`${m.parallel_slots} parallele Slots aktiv`}>
SLOTS: {m.parallel_slots}
@@ -825,6 +831,18 @@ export function Cockpit() {
>
Ctx
</button>
<button
onClick={() => setSpecModel(m)}
className={cn(
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-colors cursor-pointer flex items-center gap-1",
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
: "border-border/40 text-muted-foreground hover:bg-accent"
)}
title="Speculative Draft konfigurieren (Vocab-geprüft)"
>
<Zap className="h-3 w-3" /> Spec
</button>
<button
onClick={() => handleDelete(m.name)}
className="h-7 w-7 rounded-lg border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 flex items-center justify-center transition-colors cursor-pointer"
@@ -885,11 +903,15 @@ export function Cockpit() {
PC
</span>
)}
{m.spec_draft_model && (
{m.spec_active ? (
<span className="px-1.5 py-0.5 rounded bg-emerald-500/10 border border-emerald-500/20 text-[8px] font-mono text-emerald-400 font-bold uppercase shrink-0" title={`Speculative Decoding aktiv (Draft: ${m.spec_draft_model})`}>
SPEC
</span>
)}
) : m.spec_draft_model ? (
<span className="px-1.5 py-0.5 rounded bg-amber-500/10 border border-amber-500/20 text-[8px] font-mono text-amber-400 font-bold uppercase shrink-0" title={`Draft gesetzt (${m.spec_draft_model}), aber INAKTIV — --spec-type fehlt oder Vocab inkompatibel.`}>
SPEC?
</span>
) : null}
{m.parallel_slots > 1 && (
<span className="px-1.5 py-0.5 rounded bg-violet-500/10 border border-violet-500/20 text-[8px] font-mono text-violet-400 font-bold uppercase shrink-0" title={`${m.parallel_slots} parallele Slots aktiv`}>
SLOTS: {m.parallel_slots}
@@ -937,15 +959,27 @@ export function Cockpit() {
>
{isRunning ? "Entladen" : "Laden"}
</button>
<button
<button
onClick={() => handleSetCtx(m.name, m.ctx)}
className="h-7 px-2.5 rounded-lg border border-border/40 text-[9px] font-bold uppercase hover:bg-accent text-muted-foreground transition-all cursor-pointer"
title="Kontextlänge anpassen"
>
Ctx
</button>
<button
onClick={() => handleDelete(m.name)}
<button
onClick={() => setSpecModel(m)}
className={cn(
"h-7 px-2.5 rounded-lg border text-[9px] font-bold uppercase transition-all cursor-pointer flex items-center gap-1",
m.spec_active ? "border-emerald-500/30 text-emerald-400 hover:bg-emerald-500/10"
: m.spec_draft_model ? "border-amber-500/30 text-amber-400 hover:bg-amber-500/10"
: "border-border/40 text-muted-foreground hover:bg-accent"
)}
title="Speculative Draft konfigurieren (Vocab-geprüft)"
>
<Zap className="h-3 w-3" /> Spec
</button>
<button
onClick={() => handleDelete(m.name)}
className="h-7 w-7 rounded-lg flex items-center justify-center border border-border/40 text-muted-foreground hover:text-red-400 hover:bg-red-500/5 transition-all cursor-pointer"
title="Modell löschen"
>
@@ -1016,6 +1050,14 @@ export function Cockpit() {
</div>
)}
{specModel && (
<SpecDraftModal
model={specModel}
onClose={() => setSpecModel(null)}
onChanged={reload}
/>
)}
{dialogElement}
</div>
)