Files
Maven/CLAUDE.md
T
kami 0c65387a5f feat(router): array route contract + shorter RU router prompt
Route contract is now a JSON array of action objects (one per ask) so
compound utterances route all their intents, not just the first. Grammar
root emits `[{intent...},...]`; parseActions tolerates a bare object.
Cascade still returns one Decision — full N-action dispatch lands with the
engine turn-on (marked in-code).

Router prompt rewritten shorter + decision-ordered (prompt-guy feedback),
fact redefined as "implicit update" not "trackable state", kept in Russian
to match the CPT base + phraser. "интент" → "намерение".

CLAUDE.md: routing-architecture section + refreshed open items.
docs/plans: route-data generation plan.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017GMrVfuYN3nE4L1vEiFYC9
2026-07-11 23:27:44 +04:00

150 lines
8.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Maven RU LoRA — training guide
Fine-tuning a small LLM so Maven speaks correct, in-character Russian (and English)
without breaking Cyrillic. Training scripts live in
`/home/kami/Programs/esp32-whisper-fine-tune/llm`; the model is consumed by this
daemon.
## Two machines — don't conflate them
| Role | Box | Specs | Constraint |
|---|---|---|---|
| **Train** | this workstation | RX 7900 GRE, gfx1100 (RDNA3), **16GB VRAM**, ROCm 7.2 / torch 2.10 HIP, `cuda.is_available()==True` | fits 1.5B bf16 LoRA with huge headroom |
| **Deploy** | homesrv `kami@192.168.1.104` | Ryzen 5 5600U (Zen3 6c/12t), Vega iGPU (Cezanne), **13GB RAM, already swapping** (~8.8GB avail), Ubuntu 24.04 | **CPU-only** inference; caps model at ~1.5B for voice latency |
**Path duality via SSHFS:** `/mnt/server/home/kami/` is an SSHFS mount from homesrv. So when you're in this repo:
- `/home/kami/Programs/`**local** to workpc
- `/mnt/server/home/kami/`**remote** homesrv filesystem
- `/mnt/server/home/kami/apps/Maven/` = this repo = remote filesystem accessed locally
**Inference router** (`localhost:6446`) isn't running on either machine. Check for a compose file (`docker compose up -d`) on whichever box has it before assuming it's available.
- The Vega iGPU is **not** worth ROCm inference (gfx90c). Inference is CPU llama.cpp;
Vulkan can offload a few layers (`-ngl 4`-ish) for a small speedup.
- homesrv swaps already → every ~200MB of model footprint matters. Keep `--mlock` on.
- A 4B model at any quant is ~58 tok/s here = too slow for voice. **Stay ≤1.5B.**
## Root cause of broken Russian
Cyrillic breakage is a **base-model + quantization** problem, **not** a missing LoRA:
- Shipped models (`LFM2.5-1.2B-Thinking`, `Nemotron-3-Nano-4B`) are Western, thin-RU,
run at **Q4_K** → mangled endings, Latin/Cyrillic homoglyph swaps.
- A LoRA **cannot** add spelling knowledge the base lacks. It only steers
format / persona / mood / tool-schema / language-mirroring.
**Levers, in order of impact:**
1. **RU-native base**`Vikhrmodels/Vikhr-Qwen-2.5-1.5B-Instruct` (continued RU
pretraining on Qwen2.5). Same latency class as the current 1.2B, doesn't shatter
Cyrillic. *(Verify exact HF repo id before download — Vikhr naming shifts.)*
2. **Less aggressive quant** — deploy Q4_K_M first (~1.0GB, matches current
footprint); bump to Q5_K_M only if Cyrillic still breaks. RU-native base tolerates
Q4 far better than the Western models did.
3. **LoRA** — format/persona/mood/tools, on top of a base that already spells.
## Output contract: `{response,mood}` (Decision B, 2026-07-11)
All LLM output paths now produce `{"response":"...","mood":"..."}` — the Go side
(`replier_llm.go`, `llmphraser.go`) parses it in all 4 consumers. Fallback logic
preserves backward compat with plain text and the old `{"body","summary"}` format.
| Consumer (file) | Now parses |
|---|---|
| Replier (`cmd/mavend/replier_llm.go`) | `{"response","mood"}` via `parseResponseMood` |
| Nudges (`internal/phraser/llmphraser.go`) | `{"response","mood"}` via `parseResponseMood``Body`, `Mood` on struct |
| Reminders (`internal/phraser/llmphraser.go`) | same, summary derived from response |
| Chat / query (`internal/phraser/llmphraser.go`) | `{"response","mood"}` → returns plain response text |
Every training sample's assistant turn must produce `{"response":"...","mood":"..."}`.
## Routing architecture: LLM-as-router (REARCH.md, target)
Target arch = **LLM-as-router** (`REARCH.md`, supersedes the classifier-first
model). One resident model — the **CPT'd Qwen3-1.7B** (RU-CPT run; replaces LFM,
"too meh") — fills **both** router and phraser roles, two call-sites / two contracts:
| Prompt | Contract | Source of truth |
|---|---|---|
| route-prompt | `{"intent":<enum>, key?, value?, text?, verb?}` GBNF-constrained | `internal/router/llmrouter.go` (`routeSystem`+`routeGrammar`) |
| phrase-prompt | `{"response","mood"}` | above section |
7 intents: `fact, reminder, note, query, act, chat, system`. Key rule:
«запомни/запиши» = note, «напомни/не забудь» = reminder. Embedder is **demoted**
from router to a tool (RAG hint), not a threshold gate.
**Status: phase 1 NOT done.** Plumbing exists (`LLMRouter`, `Route` cascade calls
it at `router.go:87`) but `voice.go:209` wires it `nil` — engine OFF, classifier
stopgap still active. Flip `nil``NewLLMRouter(qwen)` after CPT finishes. Do NOT
read the current committed code as the intended design — it's the interim stopgap.
Route-training data: `esp32-whisper-fine-tune/llm/gen_route_data.py` relabels real
utterances through the verbatim `routeSystem` into `{intent,...}`. Keep its
`ROUTE_SYSTEM` in sync with the Go const.
## Data defects measured in current corpus
- **Mood collapse:** `neutral 1051, thinking 666, happy 280, confused 206, tired 7`
(of 2210). Graceful failure (`tired`) is essentially untrained — the most important
small-assistant behavior. Rebalance to ~1015% combined `tired`+`confused`.
- **System-prompt drift:** 3 variants in `synthetic_dataset.jsonl` + a 4th in
`function_calling.jsonl`. Pick **one canonical system prompt = the exact string the
daemon sends at inference**, normalize all data to it. Train-prompt ≠ deploy-prompt
is a silent accuracy tax.
- **Dolphin trap:** `dolphin_to_messages` wraps ~3000 free-form paragraph outputs
under a strict-format system prompt → teaches the model the format is optional.
Drop it, or reshape via the distiller. Consistency > volume for format LoRAs.
- **`user-*.jsonl`** are bare message fragments (not wrapped in `{"messages":[...]}`);
the `"messages" in s` filter silently drops them. Wrap/normalize before use.
## Target data recipe (RU-native base — spelling is NOT the job)
Job = output contract + persona + mood mapping + tool schema + language mirroring.
| Bucket | ~Share | Source |
|---|---|---|
| Persona chit-chat, on-contract | 45% | distill (Qwen3-4B / API): topic → canonical prompt → generate → validate (parses, mood∈enum, Cyrillic-clean, length) |
| Graceful failure / clarify | 15% | author unanswerable prompts → `tired`/`confused` (current hole) |
| Tool calls | 20% | expand `function_calling.jsonl` (473) — paraphrase 35× RU+EN, vary params |
| Real utterances | 10% | mine Maven dialogue history / logs, relabel |
| English → English | 10% | same buckets, EN in/out, so language-mirroring is trained |
**Hygiene:** dedup near-dup user turns; hold out a *real* (non-distiller) eval set;
route all data through a validator rejecting mixed-script words + invalid JSON — the
same validator is the regression metric (Cyrillic eval).
## Training (ROCm) — `esp32-whisper-fine-tune/llm/train_rocm.py`
Adapted from `train_llama.py`; the ROCm fix is dropping the two CUDA-only pieces:
- **No bitsandbytes** — bf16 full weights (1.5B fits 16GB easily), no 4-bit BnB.
- `optim="adamw_torch_fused"` replaces `paged_adamw_8bit`.
- Base `Vikhr-Qwen-2.5-1.5B`; Qwen ChatML masking (`<|im_start|>assistant` / `<|im_end|>`).
- `attn_implementation="eager"` (flash-attn ROCm wheels flaky).
- `HSA_OVERRIDE_GFX_VERSION=11.0.0` belt-and-suspenders (gfx1100 is officially supported).
- batch 2 × grad-accum 4 (eff. 8), LoRA r16/α32, 3 epochs, grad checkpointing.
## Deploy path (after training)
```bash
# 1. merge adapter → bf16 weights
python -c "from peft import AutoPeftModelForCausalLM; import torch; \
m=AutoPeftModelForCausalLM.from_pretrained('./Vikhr-Qwen-1.5b-ru-lora',torch_dtype=torch.bfloat16); \
m.merge_and_unload().save_pretrained('./Vikhr-merged'); \
from transformers import AutoTokenizer; AutoTokenizer.from_pretrained('./Vikhr-Qwen-1.5b-ru-lora').save_pretrained('./Vikhr-merged')"
# 2. HF → GGUF (in llama.cpp)
python convert_hf_to_gguf.py ./Vikhr-merged --outfile vikhr-maven-f16.gguf --outtype f16
# 3. quantize (start Q4_K_M, test Cyrillic, bump to Q5_K_M if needed)
./llama-quantize vikhr-maven-f16.gguf Vikhr-Maven-1.5B-Q4_K_M.gguf Q4_K_M
# 4. scp to homesrv; point deploy/mavend.json phraser.model_path at it; --mlock, small -ngl on Vulkan
```
## Open items
- [x] **Decide output contract****B: `{response,mood}`**, Go side patched (2026-07-11).
- [x] **Base model** — Qwen3-1.7B, RU via **continued pretraining** (not Vikhr). CPT run in progress.
- [x] **Write `gen_data.py` distiller** — ran, produced `persona_train.jsonl` (2045) + eval (107), mood collapse fixed.
- [x] **Write `gen_route_data.py`** — route-schema relabeler (run when router up).
- [ ] **Turn router engine ON** — swap `voice.go:209` `nil``NewLLMRouter(qwen)` after CPT (REARCH phase 1).
- [ ] Run `gen_route_data.py`; train route-LoRA (or fold into persona SFT).
- [ ] Improve `routeSystem` prompt for sub-1B disambiguation (awaiting prompt-guy input).
- [ ] Write Cyrillic-validity + JSON eval (extend `llama-eval-test.py`).
- [ ] Normalize `user-*.jsonl` into `{"messages":[...]}`.