14 KiB
Plan: Transkription + Bildgenerierung, Anbindung an Laravel (laravel/ai)
Stand der Umsetzung (2026-10-15) – Server fertig, Laravel-Teil dokumentiert:
- Weder A noch B, sondern Variante C: Vulkan schafft nur 54 % der ROCm-Prompt-Verarbeitung (Messwerte unten). Deshalb baut
llama-swap/Dockerfileauf dem ROCm-Image auf und kopiertwhisper-server+sd-serverausunified-vulkan-265dazu. LLMs laufen weiter auf ROCm, Whisper und Bilder über Vulkan, und alles verwaltet ein llama-swap.- Vulkan-Gerät 0 ist die iGPU. Die RX 6950 XT ist Gerät 1 →
VK_VISIBLE_DEVICES=1.- Whisper läuft auf der GPU statt der CPU: 0,46 s statt 3,3 s (CPU, 12 Threads,
-l de) bzw. 9,3 s (CPU, 8 Threads,-l auto) für 6,6 s Sprache. Es braucht ~1,2 GB VRAM und kostet die Chat-Geschwindigkeit nachweislich nichts (852 t/s mit und ohne).- ffmpeg ist im ROCm-Image schon enthalten,
--convertfunktioniert: WAV, WebM/Opus und M4A/AAC werden fehlerfrei transkribiert.- Bilder: Z-Image-Turbo Q8, VAE von
Comfy-Org/z_image_turbo(nicht gated). Die Dauer hängt an der GPU-Rechenleistung (~4 s/Schritt bei 1024²).--offload-to-cpu/--backend te=cpuändern daran nichts,--offload-to-cpubleibt (spart VRAM).- OpenAI-Format getestet: Alias
gpt-image-1,size: 1536x1024,quality(wird ignoriert), ohneresponse_format→b64_json.scripts/start.sh: baut das Image, lädt Whisper vor,--pullaktualisiert die Basis-Images und baut neu.- Phase 5 (Laravel) ist als Anleitung in
docs/laravel.mdumgesetzt. In einer echten Laravel-App ist sie noch nicht getestet (offen: wo die App läuft).- Nebenbefund, älter als dieser Plan: Der Startup-Preload von llama-swap meldet für
qwen2.5-coder-1.5b/nomic-embed„status 404“ (llama-server ohne Web-UI antwortet auf/mit 404). Die Modelle laden trotzdem.
Context
Eine Laravel-Anwendung soll über das Laravel AI SDK (laravel/ai) diesen Server nutzen:
- Agent (Chat, Tools, Streaming, Embeddings). Das geht schon heute ohne Server-Änderung.
- Spracheingabe: Aufnahmetaste im Browser → Transkription (Whisper) → Text als Prompt an den Agenten.
- Bildgenerierung, auch als Tool des Agenten.
Was das SDK laut Doku (https://laravel.com/docs/ai-sdk) kann:
| Funktion | Treiber openai-compatible |
Bemerkung |
|---|---|---|
| Text, Streaming, Tools, Structured Output | ✅ | |
| Embeddings | ✅ | Standardmodell in der Config nötig |
| Transkription (STT) | ✅ | Standardmodell in der Config nötig, keine Sprecher-Trennung (Diarization) |
| Bilder | ❌ | Umweg: Treiber openai mit eigener url oder ein eigener HTTP-Aufruf |
| TTS | ❌ | nicht Teil dieses Plans |
Kernproblem: Image-Wahl (ROCm vs. Vulkan)
Das aktuelle Image llama-swap:vXXX-rocm-bYYYY enthält nur llama-server.
whisper-server und sd-server gibt es nur im unified-Image, und das für AMD nur als
unified-vulkan (versionierte Tags: unified-vulkan-NNN). Ein ROCm-unified-Image gibt es nicht.
A: alles auf unified-vulkan (bevorzugt) |
B: ROCm bleibt, Whisper separat | |
|---|---|---|
| Container | 1 × llama-swap | llama-swap (ROCm) + whisper-Container (CPU) |
| VRAM-Koordination Chat ↔ Bildmodell | llama-swap übernimmt das (Gruppe chat) |
keine. Bildmodell und Chat-Modell würden sich um VRAM streiten, Bilder fallen in B weg |
| Routing | alles über llama-swap | Caddy leitet /v1/audio/* am llama-swap vorbei |
| Risiko | Vulkan ist evtl. langsamer als ROCm, deshalb Benchmark | mehr Teile, keine Bilder |
Entscheidung über Benchmark (Phase 1): Kommt Vulkan bei Token-Generierung und Prompt-Verarbeitung auf ≥ ~85 % von ROCm, dann Variante A. Sonst Variante B, und die Bilder bleiben vorerst weg.
Phase 1: Benchmark Vulkan vs. ROCm (~1 h)
- Den Tag ermitteln: https://github.com/mostlygeek/llama-swap/pkgs/container/llama-swap → neuestes
unified-vulkan-NNN. - Messen, während der laufende Stack aus ist (
docker compose stop), mit denselben Modelldateien aus./models:
Dasselbe mit dem ROCm-Image (docker run --rm --device /dev/dri --group-add 987 --group-add 983 \ -e GGML_VK_VISIBLE_DEVICES=0 -v ./models:/models \ --entrypoint llama-bench ghcr.io/mostlygeek/llama-swap:unified-vulkan-NNN \ -m /models/<Pfad zur Qwen3.5-35B-A3B-Q4_K_M.gguf> -p 2048 -n 128 -fa 1/dev/kfddazu,HIP_VISIBLE_DEVICES=0). Den GGUF-Pfad untermodels/models--unsloth--Qwen3.5-35B-A3B-GGUF/snapshots/*/suchen.- Ob
llama-benchin beiden Images vorhanden ist, prüfen. Falls nicht:llama-serverstarten und die t/s aus dem Log einer Test-Anfrage nehmen. - Wichtig: Für den 35B-MoE muss die Auslagerung in den RAM gleich sein. Notfalls mit
--n-cpu-moevergleichbar festlegen.
- Ob
GGML_VK_VISIBLE_DEVICES=0: Der 7900X3D hat eine iGPU, die Vulkan sonst als zweites Gerät sieht. Mitvulkaninfo --summarybzw. dem Startlog prüfen, dass Gerät 0 die RX 6950 XT ist.- Die Ergebnisse unten unter „Messwerte“ eintragen und A oder B entscheiden.
Phase 2 (A): Stack auf unified-vulkan umstellen
.env/.env.example:LLAMA_SWAP_TAG=unified-vulkan-NNN. Den alten ROCm-Tag als Kommentar behalten (Rollback).docker-compose.yml(llama-swap):devices: nur noch/dev/drinötig (/dev/kfdschadet nicht, kann aber weg).environment:GGML_VK_VISIBLE_DEVICES: "${VK_VISIBLE_DEVICES:-0}"statt bzw. zusätzlich zuHIP_VISIBLE_DEVICES.- Prüfen, ob das Image mit
user: 1000:1000läuft. Es gibt auch-rootless-Varianten. - Prüfen, ob Binärpfade (
/app/llama-servervs.llama-serverim PATH) zumserver-Makro passen.
llama-swap/config.yaml: Bestehende Modelle unverändert lassen. Prüfen, ob--fit/--fit-targetunter Vulkan genauso greift (VRAM mitrocm-smibeobachten).- Gegenprobe: opencode + Continue (Chat, Autocomplete,
@codebase) funktionieren wie vorher. - README: Tabelle „Auf einen anderen PC übertragen“ anpassen (unified-vulkan ist jetzt Standard).
Phase 3: Whisper (Transkription)
- Modell herunterladen (whisper-server kann kein
-hf):mkdir -p models/whisper curl -L -o models/whisper/ggml-large-v3-turbo-q8_0.bin \ https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-large-v3-turbo-q8_0.bin - Eintrag in
llama-swap/config.yaml, auf der CPU und inalways-on, damit er nie das Chat-Modell verdrängt:whisper: name: Whisper large-v3-turbo (CPU) checkEndpoint: / # prüfen: hat whisper-server /health? cmd: >- whisper-server --host 127.0.0.1 --port ${PORT} -m /models/whisper/ggml-large-v3-turbo-q8_0.bin --inference-path /v1/audio/transcriptions --convert --tmp-dir /tmp -ng -t 8 -l auto ttl: 0- Die Flags mit
docker compose exec llama-swap whisper-server --helpprüfen:--inference-path,--convert(braucht ffmpeg im Image, mitwhich ffmpegprüfen),-ng/--no-gpu,--tmp-dir. - Ohne ffmpeg:
--convertweglassen, dann wandelt Laravel vorher in 16-kHz-Mono-WAV um (siehe Phase 5). whisperzurouting…groups.always-on.membersund zuhooks.on_startup.preloadhinzufügen.
- Die Flags mit
scripts/start.sh: Beim Preload einen Whisper-Request schicken. Dafür eine kurze Test-WAV unterscripts/ablegen, z. B. 1 s Stille mitffmpeg -f lavfi -i anullsrc=r=16000:cl=mono -t 1 scripts/silence.wav.- Test:
Mit einer echten deutschen Sprachaufnahme testen und die Dauer bei ~10 s Audio notieren (Ziel < 3 s).curl -H "Authorization: Bearer $KEY" http://127.0.0.1:8800/v1/audio/transcriptions \ -F model=whisper -F file=@test.webm -F response_format=json - Caddy: keine Änderung nötig. Es gibt standardmäßig kein Upload-Limit, und der
read_timeoutvon 30 min reicht.
Phase 4: Bildgenerierung (nur Variante A)
- Modell: Z-Image-Turbo (Vorlage aus
docker/config.example.yamlvon llama-swap, 8 Steps, schnell). Benötigt drei Dateien inmodels/sd/:z_image_turbo-Q8_0.gguf,ae.safetensors(VAE),qwen3-4b-instruct-2507-q8_0.gguf(Text-Encoder). Die Hugging-Face-Quellen beim Umsetzen heraussuchen. Alternative: FLUX.1-schnell (GGUF). - Eintrag in der
chat-Gruppe, damit llama-swap das Chat-Modell entlädt, solange ein Bild generiert wird:z-image: name: Z-Image Turbo (Bilder) checkEndpoint: / cmd: | sd-server --listen-ip 127.0.0.1 --listen-port ${PORT} --diffusion-fa --diffusion-model /models/sd/z_image_turbo-Q8_0.gguf --vae /models/sd/ae.safetensors --llm /models/sd/qwen3-4b-instruct-2507-q8_0.gguf --offload-to-cpu --cfg-scale 1.0 --steps 8 --height 1024 --width 1024 aliases: [gpt-image-1, dall-e-3] ttl: 600- Die Flags mit
sd-server --helpprüfen, besonders den Namen von--listen-ip. - Die
aliasessind wichtig: Der OpenAI-Treiber von Laravel schickt seinen eigenen Modellnamen.
- Die Flags mit
- Test:
curl …/v1/images/generations -d '{"model":"z-image","prompt":"a red bicycle","response_format":"b64_json"}'. Dabei messen: Dauer inkl. Modellwechsel, VRAM-Spitze, ob Autocomplete (Continue) weiterläuft. - Hinweis für den Alltag: Jede Bildgenerierung wirft das Chat-Modell raus (Neuladen dauert einige zehn Sekunden). Für einen Agenten, der Bilder als Tool erzeugt, heißt das: Chat → Bild → Chat bedeutet zwei Modellwechsel.
Phase 5: Laravel-Anwendung
5.1 Config (config/ai.php + .env)
'providers' => [
// Text, Agent, Embeddings, Transkription
'local' => [
'driver' => 'openai-compatible',
'url' => env('LOCAL_AI_URL'), // http://<server>:8800/v1
'key' => env('LOCAL_AI_API_KEY'),
'models' => [
'text' => ['default' => 'qwen3.5-35b-a3b'],
'embeddings' => ['default' => 'nomic-embed', 'dimensions' => 768],
'transcription' => ['default' => 'whisper'],
],
],
// nur für Bilder: OpenAI-Treiber auf den eigenen Server umgebogen
'local-images' => [
'driver' => 'openai',
'key' => env('LOCAL_AI_API_KEY'),
'url' => env('LOCAL_AI_URL'),
],
],
- Den Env-Namen für die URL-Überschreibung je nach SDK-Version prüfen (Doku 13.x:
url/OPENAI_URL, 12.x:OPENAI_BASE_URL). - Den Schlüssel prüfen, mit dem
localzum Standard-Provider für Transkription wird (Transcriptionhat in der Doku keinenprovider:-Parameter). - Netz: Der Laravel-Server muss in
ALLOWED_NETSliegen, im LAN oder als NetBird-Peer. - Timeouts großzügig setzen (
timeout: 300beim Prompt), Agent-Läufe mit Tools als Queue-Job.
5.2 Agent
php artisan make:agent Assistant: Anweisungen, Tools.- Smoke-Test:
(new Assistant)->prompt('Hallo', provider: 'local'), danach ein Tool-Call-Test.
5.3 Spracheingabe (Aufnahmetaste)
- Frontend:
getUserMedia+MediaRecorder(gedrückt halten = aufnehmen). Bevorzugtaudio/webm;codecs=opus, Safari liefertaudio/mp4. Den Blob perFormDataanPOST /voiceschicken, die Antwort als Stream lesen. - HTTPS ist Pflicht. Das Mikrofon geht nur in einem sicheren Kontext. Die Laravel-App braucht deshalb HTTPS, lokal geht auch
localhost. - Backend:
Zwei Schritte, damit der User den erkannten Text vor dem Absenden sieht und korrigieren kann. Das ist wichtig bei Agenten, deren Tools etwas ausführen.Route::post('/voice/transcribe', fn (Request $r) => ['text' => (string) Transcription::fromUpload($r->file('audio'))->generate()]); Route::post('/voice/ask', fn (Request $r) => (new Assistant)->stream($r->string('text'))); - Wenn
whisper-serverkein--convertkann: In Laravel vorher umwandeln mitffmpeg -i in.webm -ar 16000 -ac 1 out.wav(Symfony Process), dannTranscription::fromPath(...). - Validierung: Upload-Limit (z. B. 20 MB), Mindestlänge, leere Transkripte abfangen.
5.4 Bilder
- Direkt:
Image::of('...')->generate(provider: 'local-images')(die genaue API in der Doku nachsehen). Falls der OpenAI-Treiber Parameter schickt, diesd-serverablehnt: Fallback aufHttp::withToken(...)->post(.../images/generations)undb64_jsonselbst in den Storage schreiben. - Als Agent-Tool:
GenerateImage-Tool, das eins von beidem aufruft und eine URL bzw. einen Storage-Pfad zurückgibt.
Phase 6: Doku im Repo
docs/laravel.md: Config, Netz/Key, Spracheingabe, Bilder, Stolperfallen (HTTPS, Formate, Modellwechsel).- README: Modelltabelle um
whisperundz-imageergänzen, Images und Rollback beschreiben. PLAN.md-Stil: Abweichungen oben in diesem Plan unter „Stand der Umsetzung“ festhalten.
Verifikation
- opencode + Continue funktionieren unverändert (nach dem Image-Wechsel!), die t/s liegen im erwarteten Bereich.
GET /v1/modelslistetwhisperundz-image.- Transkription per curl (deutsch, webm und wav) liefert korrekten Text. Im Log wird dabei kein Chat-Modell entladen.
- Bild per curl: PNG kommt zurück, danach lädt das Chat-Modell beim nächsten Request wieder.
- Laravel: Agent-Prompt mit Tool-Call, Spracheingabe Ende-zu-Ende im Browser (Chrome + Firefox, wenn möglich Safari), Bild über das Agent-Tool.
- Nach einem Reboot ist Whisper vorgeladen (
/running).
Rollback
LLAMA_SWAP_TAG in .env zurück auf v265-rocm-b11610, /dev/kfd + HIP_VISIBLE_DEVICES wieder rein,
die Einträge whisper/z-image auskommentieren, docker compose up -d.
Messwerte (in Phase 1 ausfüllen)
| Image | pp2048 t/s | tg128 t/s | Bemerkung |
|---|---|---|---|
ROCm v265-rocm-b11610 |
~1030 | ~49,8 | nur 35B geladen, --fit-target 3072 |
unified-vulkan-265 |
~555 | ~43,2 | gleiche Bedingungen |
| ROCm im Betrieb (mit Autocomplete + Embeddings) | ~850 | ~47,5 | identisch mit und ohne Whisper auf der GPU |
Gemessen mit llama-server (Betriebs-Parameter), 2302 Prompt-Tokens, 256 generierte Tokens, cache_prompt: false.
| Bilder (Z-Image-Turbo Q8, 8 Schritte) | Dauer |
|---|---|
| 512×512 | ~11 s |
| 1024×1024 | ~40 s (Diffusion 32 s, VAE 6 s) |
| 1536×1024 | ~66 s |
| erster Request inkl. Laden | +4 s |
| Chat-Modell danach zurückladen | ~5 s |
| VRAM-Spitze beim Bild | 8,4 GB (mit Autocomplete + Whisper) |
Offene Punkte
- Wo läuft die Laravel-App (gleiches LAN, NetBird, öffentlicher Server)? Davon hängen
ALLOWED_NETSund HTTPS ab. - Bildmodell: Reichen Qualität und Geschwindigkeit von Z-Image-Turbo, oder lieber FLUX.1-schnell?
- Später ggf. TTS (die Antwort vorlesen): audio.cpp ist im unified-Image, aber das Laravel SDK kann TTS nicht über
openai-compatible.