pläne gelöscht
This commit is contained in:
parent
2e2490c1bc
commit
a465821bb8
@ -1,227 +0,0 @@
|
||||
# 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/Dockerfile`](llama-swap/Dockerfile) auf dem ROCm-Image auf und kopiert `whisper-server` + `sd-server` aus `unified-vulkan-265` dazu. 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, `--convert` funktioniert: 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-cpu` bleibt (spart VRAM).
|
||||
> - OpenAI-Format getestet: Alias `gpt-image-1`, `size: 1536x1024`, `quality` (wird ignoriert), ohne `response_format` → `b64_json`.
|
||||
> - `scripts/start.sh`: baut das Image, lädt Whisper vor, `--pull` aktualisiert die Basis-Images und baut neu.
|
||||
> - Phase 5 (Laravel) ist als Anleitung in [`docs/laravel.md`](docs/laravel.md) umgesetzt. 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:
|
||||
1. **Agent** (Chat, Tools, Streaming, Embeddings). Das geht schon heute ohne Server-Änderung.
|
||||
2. **Spracheingabe**: Aufnahmetaste im Browser → Transkription (Whisper) → Text als Prompt an den Agenten.
|
||||
3. **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)
|
||||
1. Den Tag ermitteln: https://github.com/mostlygeek/llama-swap/pkgs/container/llama-swap → neuestes `unified-vulkan-NNN`.
|
||||
2. Messen, während der laufende Stack aus ist (`docker compose stop`), mit denselben Modelldateien aus `./models`:
|
||||
```bash
|
||||
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
|
||||
```
|
||||
Dasselbe mit dem ROCm-Image (`/dev/kfd` dazu, `HIP_VISIBLE_DEVICES=0`). Den GGUF-Pfad unter
|
||||
`models/models--unsloth--Qwen3.5-35B-A3B-GGUF/snapshots/*/` suchen.
|
||||
- Ob `llama-bench` in beiden Images vorhanden ist, prüfen. Falls nicht: `llama-server` starten 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-moe` vergleichbar festlegen.
|
||||
3. `GGML_VK_VISIBLE_DEVICES=0`: Der 7900X3D hat eine iGPU, die Vulkan sonst als zweites Gerät sieht.
|
||||
Mit `vulkaninfo --summary` bzw. dem Startlog prüfen, dass Gerät 0 die RX 6950 XT ist.
|
||||
4. 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/dri` nötig (`/dev/kfd` schadet nicht, kann aber weg).
|
||||
- `environment`: `GGML_VK_VISIBLE_DEVICES: "${VK_VISIBLE_DEVICES:-0}"` statt bzw. zusätzlich zu `HIP_VISIBLE_DEVICES`.
|
||||
- Prüfen, ob das Image mit `user: 1000:1000` läuft. Es gibt auch `-rootless`-Varianten.
|
||||
- Prüfen, ob Binärpfade (`/app/llama-server` vs. `llama-server` im PATH) zum `server`-Makro passen.
|
||||
- `llama-swap/config.yaml`: Bestehende Modelle unverändert lassen. Prüfen, ob `--fit` / `--fit-target` unter Vulkan genauso greift (VRAM mit `rocm-smi` beobachten).
|
||||
- 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)
|
||||
1. Modell herunterladen (whisper-server kann kein `-hf`):
|
||||
```bash
|
||||
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
|
||||
```
|
||||
2. Eintrag in `llama-swap/config.yaml`, **auf der CPU** und in `always-on`, damit er nie das Chat-Modell verdrängt:
|
||||
```yaml
|
||||
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 --help` prüfen: `--inference-path`, `--convert` (braucht **ffmpeg** im Image, mit `which ffmpeg` prüfen), `-ng`/`--no-gpu`, `--tmp-dir`.
|
||||
- Ohne ffmpeg: `--convert` weglassen, dann wandelt Laravel vorher in 16-kHz-Mono-WAV um (siehe Phase 5).
|
||||
- `whisper` zu `routing…groups.always-on.members` und zu `hooks.on_startup.preload` hinzufügen.
|
||||
3. `scripts/start.sh`: Beim Preload einen Whisper-Request schicken. Dafür eine kurze Test-WAV unter `scripts/` ablegen, z. B. 1 s Stille mit `ffmpeg -f lavfi -i anullsrc=r=16000:cl=mono -t 1 scripts/silence.wav`.
|
||||
4. Test:
|
||||
```bash
|
||||
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
|
||||
```
|
||||
Mit einer echten deutschen Sprachaufnahme testen und die Dauer bei ~10 s Audio notieren (Ziel < 3 s).
|
||||
5. Caddy: keine Änderung nötig. Es gibt standardmäßig kein Upload-Limit, und der `read_timeout` von 30 min reicht.
|
||||
|
||||
## Phase 4: Bildgenerierung (nur Variante A)
|
||||
1. Modell: **Z-Image-Turbo** (Vorlage aus `docker/config.example.yaml` von llama-swap, 8 Steps, schnell).
|
||||
Benötigt drei Dateien in `models/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).
|
||||
2. Eintrag in der **`chat`-Gruppe**, damit llama-swap das Chat-Modell entlädt, solange ein Bild generiert wird:
|
||||
```yaml
|
||||
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 --help` prüfen, besonders den Namen von `--listen-ip`.
|
||||
- Die `aliases` sind wichtig: Der OpenAI-Treiber von Laravel schickt seinen eigenen Modellnamen.
|
||||
3. 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.
|
||||
4. **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`)
|
||||
```php
|
||||
'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 `local` zum Standard-Provider für Transkription wird (`Transcription` hat in der Doku keinen `provider:`-Parameter).
|
||||
- Netz: Der Laravel-Server muss in `ALLOWED_NETS` liegen, im LAN oder als NetBird-Peer.
|
||||
- Timeouts großzügig setzen (`timeout: 300` beim 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). Bevorzugt `audio/webm;codecs=opus`, Safari liefert `audio/mp4`.
|
||||
Den Blob per `FormData` an `POST /voice` schicken, 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:**
|
||||
```php
|
||||
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')));
|
||||
```
|
||||
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.
|
||||
- Wenn `whisper-server` kein `--convert` kann: In Laravel vorher umwandeln mit
|
||||
`ffmpeg -i in.webm -ar 16000 -ac 1 out.wav` (Symfony Process), dann `Transcription::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, die `sd-server` ablehnt: Fallback auf `Http::withToken(...)->post(.../images/generations)`
|
||||
und `b64_json` selbst 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 `whisper` und `z-image` ergänzen, Images und Rollback beschreiben.
|
||||
- `PLAN.md`-Stil: Abweichungen oben in diesem Plan unter „Stand der Umsetzung“ festhalten.
|
||||
|
||||
## Verifikation
|
||||
1. opencode + Continue funktionieren unverändert (nach dem Image-Wechsel!), die t/s liegen im erwarteten Bereich.
|
||||
2. `GET /v1/models` listet `whisper` und `z-image`.
|
||||
3. Transkription per curl (deutsch, webm und wav) liefert korrekten Text. Im Log wird dabei **kein** Chat-Modell entladen.
|
||||
4. Bild per curl: PNG kommt zurück, danach lädt das Chat-Modell beim nächsten Request wieder.
|
||||
5. Laravel: Agent-Prompt mit Tool-Call, Spracheingabe Ende-zu-Ende im Browser (Chrome + Firefox, wenn möglich Safari), Bild über das Agent-Tool.
|
||||
6. 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_NETS` und 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`.
|
||||
209
PLAN.md
209
PLAN.md
@ -1,209 +0,0 @@
|
||||
# Plan: Lokaler LLM-Server für opencode per Docker Compose
|
||||
|
||||
> **Stand der Umsetzung (2026-10-14) – Abweichungen vom Plan:**
|
||||
> - Externer Port ist **8800** statt 8000 (8000 ist lokal von VS Code belegt).
|
||||
> - Kein manuelles `--n-cpu-moe`: llama.cpp `--fit` (Standard an) verteilt automatisch, `--fit-target 3072` reserviert VRAM fürs Autocomplete-Modell. `--no-mmap` existiert in diesem Build nicht mehr.
|
||||
> - Autocomplete-Modell: Base-Variante `ggml-org/Qwen2.5-Coder-1.5B-Q8_0-GGUF` (besser für FIM als Instruct).
|
||||
> - Image: `ghcr.io/mostlygeek/llama-swap:v265-rocm-b11610`, läuft als UID 1000.
|
||||
> - Offen (braucht sudo / deine Entscheidung): ufw-Freigabe, Host-Ollama deaktivieren, alte Ollama-Modelle löschen, lokale opencode-Config ersetzen.
|
||||
|
||||
## Context
|
||||
Ziel: Programmieren mit opencode **und dem VS-Code-Plugin Continue** ohne Cloud-KI. Diese Maschine (RX 6950 XT 16 GB, gfx1030; Ryzen 9 7900X3D; 62 GB RAM) stellt **zentral eine OpenAI-kompatible LLM-API** bereit. opencode läuft auf den Client-PCs (auch auf dieser Maschine) mit deren eigenem Code und nutzt diese API.
|
||||
Anforderungen:
|
||||
- Alles als `docker-compose` in `/home/alegab/workspace/localAI`, damit es portabel ist. Die Software auf dem Host (systemd-Ollama, llama-cpp-Paket) wird nicht genutzt.
|
||||
- Zugriff nur von localhost, aus dem LAN (`br0` 192.168.178.0/24) und über NetBird (`wt0` 100.73.0.0/16), zusätzlich mit API-Key.
|
||||
- Backend: llama.cpp mit ROCm (schnell bei MoE dank `--n-cpu-moe`). Die Modelle liegen als GGUF in `./models`.
|
||||
|
||||
Wichtig: ufw ist aktiv, aber Ports, die Docker veröffentlicht, **umgehen ufw**. Deshalb läuft der Proxy im Host-Netz (dort greift ufw) und filtert zusätzlich selbst nach Quell-IP. Das Backend ist nur auf 127.0.0.1 gebunden.
|
||||
|
||||
## Architektur
|
||||
```
|
||||
Client-PC (opencode) ──LAN / NetBird──┐
|
||||
Lokales opencode ─────────────────────┤
|
||||
▼
|
||||
caddy (network_mode: host, :8000)
|
||||
├─ IP-Filter: 127.0.0.0/8, ::1, 192.168.178.0/24, 100.73.0.0/16
|
||||
├─ Bearer-Token-Prüfung (LLM_API_KEY)
|
||||
└─ reverse_proxy → 127.0.0.1:8080
|
||||
▼
|
||||
llama-swap (ghcr.io/mostlygeek/llama-swap:rocm, nur 127.0.0.1:8080)
|
||||
└─ startet/wechselt llama-server je nach "model"-Feld
|
||||
/dev/kfd + /dev/dri → RX 6950 XT, Modelle in ./models
|
||||
```
|
||||
|
||||
## Dateien (alle neu)
|
||||
```
|
||||
localAI/
|
||||
├── docker-compose.yml
|
||||
├── .env.example # Vorlage. .env wird lokal erzeugt (git-ignored)
|
||||
├── .gitignore # .env, models/
|
||||
├── llama-swap/config.yaml
|
||||
├── caddy/Caddyfile
|
||||
├── client/opencode.json # Vorlage für ~/.config/opencode/opencode.json der Clients
|
||||
├── client/continue/config.yaml # Vorlage für ~/.continue/config.yaml
|
||||
├── client/continue/.env.example # LOCALAI_API_KEY für Continue-Secrets
|
||||
├── docs/opencode.md # Client-Einrichtung opencode (Linux/Windows/macOS)
|
||||
├── docs/continue.md # Client-Einrichtung Continue in VS Code, Schritt für Schritt
|
||||
├── scripts/firewall-ufw.sh # einmalige ufw-Freigaben für Port 8000 (LAN + wt0)
|
||||
├── PLAN.md
|
||||
└── README.md # Überblick, Server-Setup, Portieren, Links auf docs/
|
||||
```
|
||||
|
||||
### docker-compose.yml (Kern)
|
||||
- **llama-swap**
|
||||
- Image: `ghcr.io/mostlygeek/llama-swap:rocm`, beim Umsetzen auf einen festen Tag pinnen. Das Image bringt llama-server mit ROCm mit.
|
||||
- Ports: `127.0.0.1:8080:8080`.
|
||||
- `devices: [/dev/kfd, /dev/dri]`, `group_add: ["${RENDER_GID}", "${VIDEO_GID}"]` (hier 987/983), `security_opt: [seccomp=unconfined]`, `ipc: host`.
|
||||
- Umgebung: `HIP_VISIBLE_DEVICES=0` (iGPU ausblenden), `LLAMA_CACHE=/models`.
|
||||
- Volumes: `./models:/models` und `./llama-swap/config.yaml:/app/config.yaml:ro`.
|
||||
- `restart: unless-stopped`, Healthcheck auf `/health`.
|
||||
- **caddy**
|
||||
- Image: `caddy:2-alpine` (Tag pinnen), `network_mode: host`.
|
||||
- Volume: `./caddy/Caddyfile`, Umgebung `LLM_API_KEY`, `ALLOWED_NETS`, `LISTEN_PORT`.
|
||||
- `depends_on: llama-swap`.
|
||||
|
||||
### caddy/Caddyfile
|
||||
```
|
||||
{ admin off
|
||||
auto_https off }
|
||||
:{$LISTEN_PORT:8000} {
|
||||
@blocked not remote_ip {$ALLOWED_NETS}
|
||||
respond @blocked 403
|
||||
@noauth not header Authorization "Bearer {$LLM_API_KEY}"
|
||||
respond @noauth 401
|
||||
reverse_proxy 127.0.0.1:8080 { flush_interval -1 }
|
||||
}
|
||||
```
|
||||
`ALLOWED_NETS` in `.env`: `127.0.0.0/8 ::1 192.168.178.0/24 100.73.0.0/16`. Auf einem anderen PC passt man nur diese Zeile an.
|
||||
|
||||
### llama-swap/config.yaml
|
||||
Nur ein Modell gleichzeitig (16 GB VRAM). Die Modelle werden beim ersten Aufruf per `-hf` nach `/models` geladen. Repo- und Quant-Namen beim Umsetzen auf Hugging Face verifizieren.
|
||||
```yaml
|
||||
healthCheckTimeout: 900 # erster Start lädt ~20 GB
|
||||
models:
|
||||
qwen3.5-35b-a3b: # Hauptmodell (MoE, ~3B aktiv)
|
||||
cmd: >
|
||||
llama-server --port ${PORT} --host 127.0.0.1
|
||||
-hf unsloth/Qwen3.5-35B-A3B-GGUF:Q4_K_M --jinja
|
||||
-c 65536 -ngl 99 --n-cpu-moe 16 -fa on -ctk q8_0 -ctv q8_0
|
||||
--threads 12 --no-mmap
|
||||
ttl: 1800
|
||||
glm-4.7-flash: # Alternative mit starkem Tool-Calling
|
||||
cmd: > ...analog (unsloth/GLM-4.7-Flash-GGUF:Q4_K_M)
|
||||
ttl: 1800
|
||||
qwen3.5-9b: # klein, komplett im VRAM, schnell
|
||||
cmd: > ...analog, -c 32768, ohne --n-cpu-moe
|
||||
ttl: 1800
|
||||
```
|
||||
`--n-cpu-moe` wird beim Test eingestellt: so niedrig wie möglich, ohne dass der VRAM überläuft. Den Verbrauch dabei mit `rocm-smi` beobachten.
|
||||
|
||||
**Ergänzung für Continue:** Zwei weitere Modelle laufen dauerhaft neben dem jeweils aktiven Chat-Modell.
|
||||
```yaml
|
||||
qwen2.5-coder-1.5b: # Autocomplete (FIM), GPU, ~2 GB VRAM
|
||||
cmd: llama-server ... -hf Qwen/Qwen2.5-Coder-1.5B-Instruct-GGUF:Q8_0 -c 8192 -ngl 99 -fa on
|
||||
nomic-embed: # Embeddings für @codebase, auf der CPU
|
||||
cmd: llama-server ... -hf nomic-ai/nomic-embed-text-v1.5-GGUF:Q8_0 --embedding --pooling mean -ngl 0 -c 8192
|
||||
groups:
|
||||
chat: { swap: true, exclusive: false, members: [qwen3.5-35b-a3b, glm-4.7-flash, qwen3.5-9b] }
|
||||
always-on: { swap: false, exclusive: false, persistent: true, members: [qwen2.5-coder-1.5b, nomic-embed] }
|
||||
```
|
||||
Der VRAM-Bedarf des Autocomplete-Modells (~2 GB) wird beim Einstellen von `--n-cpu-moe` der Chat-Modelle eingeplant, also etwas höher ansetzen. Die Gruppen-Syntax beim Umsetzen gegen die llama-swap-Doku prüfen. Optional kommt ein Reranker hinzu, z. B. bge-reranker-v2-m3 mit `--reranking`.
|
||||
|
||||
### client/opencode.json (Vorlage)
|
||||
```json
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"model": "localai/qwen3.5-35b-a3b",
|
||||
"small_model": "localai/qwen3.5-35b-a3b",
|
||||
"share": "disabled",
|
||||
"autoupdate": false,
|
||||
"disabled_providers": ["opencode", "anthropic", "openai", "google", "ollama"],
|
||||
"provider": {
|
||||
"localai": {
|
||||
"name": "LocalAI (alegab-pc)",
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"options": {
|
||||
"baseURL": "http://{env:LOCALAI_HOST}:8000/v1",
|
||||
"apiKey": "{env:LOCALAI_API_KEY}"
|
||||
},
|
||||
"models": {
|
||||
"qwen3.5-35b-a3b": { "name": "Qwen3.5 35B-A3B", "limit": { "context": 65536, "output": 16384 } },
|
||||
"glm-4.7-flash": { "name": "GLM-4.7 Flash", "limit": { "context": 65536, "output": 16384 } },
|
||||
"qwen3.5-9b": { "name": "Qwen3.5 9B", "limit": { "context": 32768, "output": 8192 } }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
`small_model` zeigt bewusst auf das Hauptmodell. Ein eigenes kleines Modell würde llama-swap bei jedem Titel- oder Zusammenfassungs-Aufruf zum Modellwechsel zwingen.
|
||||
`LOCALAI_HOST` ist hier `127.0.0.1`, im LAN `192.168.178.72` und über NetBird `100.73.82.219` bzw. der NetBird-DNS-Name.
|
||||
|
||||
### client/continue/config.yaml (Vorlage)
|
||||
```yaml
|
||||
name: LocalAI
|
||||
version: 1.0.0
|
||||
schema: v1
|
||||
models:
|
||||
- name: Qwen3.5 35B-A3B (Chat/Agent)
|
||||
provider: openai
|
||||
model: qwen3.5-35b-a3b
|
||||
apiBase: http://192.168.178.72:8000/v1 # bzw. NetBird-IP/-Name
|
||||
apiKey: ${{ secrets.LOCALAI_API_KEY }}
|
||||
roles: [chat, edit, apply]
|
||||
capabilities: [tool_use]
|
||||
defaultCompletionOptions: { contextLength: 65536, maxTokens: 8192 }
|
||||
- name: GLM-4.7 Flash
|
||||
provider: openai
|
||||
model: glm-4.7-flash
|
||||
... (gleiche apiBase/apiKey), roles: [chat, edit, apply], capabilities: [tool_use]
|
||||
- name: Qwen2.5-Coder 1.5B (Autocomplete)
|
||||
provider: openai
|
||||
model: qwen2.5-coder-1.5b
|
||||
...
|
||||
roles: [autocomplete]
|
||||
autocompleteOptions: { debounceDelay: 250, maxPromptTokens: 2048 }
|
||||
- name: Nomic Embed
|
||||
provider: openai
|
||||
model: nomic-embed
|
||||
...
|
||||
roles: [embed]
|
||||
```
|
||||
- Das Secret steht in `~/.continue/.env` als `LOCALAI_API_KEY=...`.
|
||||
- Beim Umsetzen anhand der Continue-Doku verifizieren: FIM über den OpenAI-Provider (ggf. `useLegacyCompletionsEndpoint: true` oder explizites `promptTemplates.autocomplete` im Qwen-FIM-Format), die Schlüssel für Telemetrie und Remote-Config sowie die passende Kontext-Länge.
|
||||
- `docs/continue.md` enthält:
|
||||
1. Extension installieren (Marketplace bzw. Open VSX für VSCodium).
|
||||
2. Bei Continue *nicht* anmelden, Hub-Login überspringen.
|
||||
3. `config.yaml` und `.env` kopieren.
|
||||
4. Host-IP je nach Netz eintragen (LAN oder NetBird).
|
||||
5. Telemetrie aus (VS-Code-Setting `continue.telemetryEnabled: false`).
|
||||
6. Die Rollen erklären (Chat/Agent, Edit, Autocomplete, @codebase-Indexing).
|
||||
7. Troubleshooting: 401 = Key falsch, 403 = falsches Netz, Timeout beim ersten Request = Modell lädt, Autocomplete leer = FIM-Template prüfen.
|
||||
8. Fertige Varianten: nur LAN, nur NetBird, Windows-Pfade (`%USERPROFILE%\.continue`).
|
||||
|
||||
### scripts/firewall-ufw.sh
|
||||
Einmalig mit sudo ausführen, nicht Teil von Compose:
|
||||
`ufw allow from 192.168.178.0/24 to any port 8000 proto tcp` und `ufw allow in on wt0 to any port 8000 proto tcp`.
|
||||
|
||||
## Umsetzungsschritte
|
||||
0. Diesen Plan als `/home/alegab/workspace/localAI/PLAN.md` ablegen.
|
||||
1. Die Dateien oben anlegen. `.env` aus `.env.example` erzeugen, mit `LLM_API_KEY=$(openssl rand -hex 32)` und den GIDs aus `getent group render video`.
|
||||
2. Den Host-Ollama-Dienst stoppen und deaktivieren (`sudo systemctl disable --now ollama`). Er belegt VRAM und lauscht auf 0.0.0.0. **Vorher bei dir nachfragen.**
|
||||
3. `docker compose pull && docker compose up -d`, dann `scripts/firewall-ufw.sh` ausführen (sudo, bei dir nachfragen).
|
||||
4. Den ersten Request schicken, damit der Modell-Download startet. `--n-cpu-moe` einstellen.
|
||||
5. Die lokale `~/.config/opencode/opencode.json` sichern und durch die Client-Vorlage ersetzen.
|
||||
6. Continue-Vorlage, `docs/continue.md`, `docs/opencode.md` und README schreiben. Continue in VS Code auf dieser Maschine mit der Vorlage testen.
|
||||
7. Erst nachdem alles verifiziert ist und du zugestimmt hast: die alten Ollama-Modelle unter `/usr/share/ollama` löschen.
|
||||
|
||||
## Verifikation
|
||||
1. `docker compose ps`: beide Dienste healthy. `docker compose logs llama-swap` zeigt beim Laden ROCm/gfx1030 und die Zahl der offloaded layers.
|
||||
2. Auth: `curl 127.0.0.1:8000/v1/models` → 401. Mit `-H "Authorization: Bearer $KEY"` → Modellliste.
|
||||
3. Bindung: `ss -tlnp` zeigt 8080 nur auf 127.0.0.1 und 8000 auf allen Interfaces.
|
||||
4. Chat-Test: `curl .../v1/chat/completions` mit einem Tool-Definitions-Payload liefert einen `tool_calls`-Response. Dabei die Tokens/s im Log notieren.
|
||||
5. Remote: von einem LAN-PC und über NetBird `curl http://<ip>:8000/v1/models` mit Key → 200. Ein Zugriff aus einem nicht freigegebenen Netz, z. B. aus einem Docker-Bridge-Container (`docker run --rm curlimages/curl http://172.17.0.1:8000/v1/models`), → 403.
|
||||
6. opencode lokal: `/models` zeigt nur `localai/*`. Testauftrag „lege hello.py an und führe sie aus" prüft Read/Write/Bash-Tool-Calls.
|
||||
7. Continue:
|
||||
- Chat-Antwort kommt an.
|
||||
- Agent-Modus führt einen Tool-Call aus.
|
||||
- Ghost-Text-Autocomplete erscheint in unter ~1 s. Im llama-swap-Log stehen Requests an `qwen2.5-coder-1.5b`, ohne dass das Chat-Modell entladen wird.
|
||||
- `@codebase` indexiert, im Log stehen Requests an `nomic-embed`.
|
||||
8. Gegenprobe offline: Das Internet trennen (nach dem Modell-Download), opencode muss weiter funktionieren.
|
||||
Loading…
Reference in New Issue
Block a user