A/B-Aufbau fuer Qwen3.5-4B: Messstrecke und Konvertierungs-Kit
lab/ab-llm.mjs vergleicht zwei LLMs ueber reanalyze-llm (ohne Neu-Diarisierung), mit snapshot/restore der Originalanalysen und einem Report aus Tabelle plus Volltext-Vergleich. tools/rkllm-qwen35/ konvertiert Qwen3.5-4B auf dem PC mit rkllm-toolkit 1.3.0 (muss zur Board-Runtime passen), Parameter identisch zum produktiven Modell: w8a8_g128, 3 NPU-Kerne, max_context 16384. Doku: Changelog 2026-09-29 (2) mit Recherchestand, Sicherung und GOTCHAs, plus neuer Abschnitt "Modellwechsel und A/B-Tests" in Kapitel 05 samt Mermaid-Ablauf. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
// A/B-Messstrecke fuer den LLM-Wechsel (Qwen3-4B vs. Qwen3.5-4B).
|
||||
//
|
||||
// Misst NUR das Modell: laeuft ueber /api/recordings/:id/reanalyze-llm, also
|
||||
// ohne Neu-Diarisierung -- gleiches Transkript, gleiche Sprecher, gleicher
|
||||
// Kontextvermerk. Verglichen werden Titel, Zusammenfassung, Todos und die
|
||||
// Analysedauer.
|
||||
//
|
||||
// WICHTIG: reanalyze-llm ueberschreibt die gespeicherte Analyse. Deshalb IMMER
|
||||
// zuerst "snapshot" laufen lassen; "restore" schreibt den Originalstand zurueck.
|
||||
//
|
||||
// node lab/ab-llm.mjs snapshot 101 114 133
|
||||
// node lab/ab-llm.mjs run qwen3 101 114 133 # aktuelles Modell
|
||||
// ... Modell umstellen, callrec neu starten ...
|
||||
// node lab/ab-llm.mjs run qwen35 101 114 133
|
||||
// node lab/ab-llm.mjs report qwen3 qwen35
|
||||
// node lab/ab-llm.mjs restore
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { pool } from '../db.js';
|
||||
|
||||
const OUT = '/opt/callrec/lab/ab-llm';
|
||||
const API = 'http://127.0.0.1';
|
||||
const POLL_MS = 5000;
|
||||
const TIMEOUT_MS = 45 * 60 * 1000; // eine Analyse darf lange dauern (Whole-Pass)
|
||||
|
||||
fs.mkdirSync(OUT, { recursive: true });
|
||||
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
const jread = f => JSON.parse(fs.readFileSync(f, 'utf8'));
|
||||
const jwrite = (f, o) => fs.writeFileSync(f, JSON.stringify(o, null, 1));
|
||||
|
||||
async function row(id) {
|
||||
const [rows] = await pool.query(
|
||||
`SELECT id, title, title_locked, summary, todos, participants, transcript_clean,
|
||||
analysis_status, analysis_ms, char_length(transcript) AS tr_chars
|
||||
FROM recordings WHERE id=?`, [id]);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function activeModel() {
|
||||
const r = await fetch(`${API}/api/llm/status`).then(r => r.json()).catch(() => ({}));
|
||||
return r.model || '(unbekannt)';
|
||||
}
|
||||
|
||||
async function snapshot(ids) {
|
||||
for (const id of ids) {
|
||||
const r = await row(id);
|
||||
if (!r) { console.log(`#${id}: nicht gefunden`); continue; }
|
||||
jwrite(path.join(OUT, `orig-${id}.json`), r);
|
||||
console.log(`#${id}: gesichert (${r.tr_chars} Zeichen Transkript, analysis_ms=${r.analysis_ms})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function restore() {
|
||||
const files = fs.readdirSync(OUT).filter(f => /^orig-\d+\.json$/.test(f));
|
||||
if (!files.length) return console.log('Nichts zu restaurieren.');
|
||||
for (const f of files) {
|
||||
const o = jread(path.join(OUT, f));
|
||||
await pool.execute(
|
||||
`UPDATE recordings SET title=?, summary=?, todos=?, participants=?,
|
||||
transcript_clean=?, analysis_status=?, analysis_ms=? WHERE id=?`,
|
||||
[o.title, o.summary, o.todos, o.participants, o.transcript_clean,
|
||||
o.analysis_status, o.analysis_ms, o.id]);
|
||||
console.log(`#${o.id}: Originalanalyse zurueckgeschrieben`);
|
||||
}
|
||||
}
|
||||
|
||||
async function run(label, ids) {
|
||||
const model = await activeModel();
|
||||
console.log(`== Lauf "${label}" mit Modell ${model}`);
|
||||
const results = [];
|
||||
for (const id of ids) {
|
||||
const before = await row(id);
|
||||
if (!before) { console.log(`#${id}: nicht gefunden`); continue; }
|
||||
if (!fs.existsSync(path.join(OUT, `orig-${id}.json`)))
|
||||
throw new Error(`#${id}: kein Snapshot -- erst "snapshot" laufen lassen!`);
|
||||
|
||||
process.stdout.write(`#${id} (${before.tr_chars} Zeichen): starte ... `);
|
||||
const t0 = Date.now();
|
||||
const res = await fetch(`${API}/api/recordings/${id}/reanalyze-llm`, { method: 'POST' });
|
||||
if (!res.ok) { console.log(`FEHLER HTTP ${res.status}`); continue; }
|
||||
|
||||
let r, waited = 0;
|
||||
do {
|
||||
await sleep(POLL_MS); waited = Date.now() - t0;
|
||||
r = await row(id);
|
||||
process.stdout.write('.');
|
||||
} while (r && r.analysis_status === 'pending' && waited < TIMEOUT_MS);
|
||||
|
||||
const wall = Date.now() - t0;
|
||||
console.log(` ${r.analysis_status} nach ${(wall / 1000).toFixed(0)} s (analysis_ms=${r.analysis_ms})`);
|
||||
results.push({
|
||||
id, model, label,
|
||||
status: r.analysis_status,
|
||||
wall_ms: wall,
|
||||
analysis_ms: r.analysis_ms,
|
||||
tr_chars: r.tr_chars,
|
||||
title: r.title,
|
||||
summary: r.summary,
|
||||
summary_chars: (r.summary || '').length,
|
||||
todos: r.todos,
|
||||
todo_count: (() => { try { return JSON.parse(r.todos || '[]').length; } catch { return null; } })(),
|
||||
participants: r.participants,
|
||||
});
|
||||
jwrite(path.join(OUT, `run-${label}.json`), results);
|
||||
}
|
||||
console.log(`\nGeschrieben: ${OUT}/run-${label}.json`);
|
||||
}
|
||||
|
||||
function report(a, b) {
|
||||
const A = jread(path.join(OUT, `run-${a}.json`));
|
||||
const B = jread(path.join(OUT, `run-${b}.json`));
|
||||
const byId = x => Object.fromEntries(x.map(r => [r.id, r]));
|
||||
const ma = byId(A), mb = byId(B);
|
||||
const ids = [...new Set([...A.map(r => r.id), ...B.map(r => r.id)])].sort((x, y) => x - y);
|
||||
|
||||
console.log(`\nModell A (${a}): ${A[0]?.model}`);
|
||||
console.log(`Modell B (${b}): ${B[0]?.model}\n`);
|
||||
console.log('ID Zeichen | Dauer A | Dauer B | Summary A | Summary B | Todos A/B | Status');
|
||||
console.log('-'.repeat(92));
|
||||
for (const id of ids) {
|
||||
const x = ma[id] || {}, y = mb[id] || {};
|
||||
const s = v => v == null ? ' -' : (v / 1000).toFixed(0).padStart(5) + ' s';
|
||||
console.log(
|
||||
String(id).padStart(4), String(x.tr_chars ?? y.tr_chars ?? '').padStart(8),
|
||||
'|', s(x.analysis_ms), '|', s(y.analysis_ms),
|
||||
'|', String(x.summary_chars ?? '-').padStart(9), '|', String(y.summary_chars ?? '-').padStart(9),
|
||||
'|', `${x.todo_count ?? '-'}/${y.todo_count ?? '-'}`.padStart(8),
|
||||
'|', `${x.status ?? '-'}/${y.status ?? '-'}`);
|
||||
}
|
||||
|
||||
// Volltext zum Danebenlegen -- die inhaltliche Bewertung macht der Mensch.
|
||||
const md = [`# A/B: ${a} vs. ${b}`, '',
|
||||
`- **A (${a})**: \`${A[0]?.model}\``, `- **B (${b})**: \`${B[0]?.model}\``, ''];
|
||||
for (const id of ids) {
|
||||
const x = ma[id] || {}, y = mb[id] || {};
|
||||
md.push(`## Aufnahme #${id} (${x.tr_chars ?? y.tr_chars} Zeichen Transkript)`, '',
|
||||
`### A — ${a} (${x.analysis_ms ? (x.analysis_ms / 1000).toFixed(0) + ' s' : 'n/a'})`, '',
|
||||
`**Titel:** ${x.title ?? '-'}`, '', (x.summary || '_keine_'), '',
|
||||
`**Todos:** ${x.todos ?? '-'}`, '',
|
||||
`### B — ${b} (${y.analysis_ms ? (y.analysis_ms / 1000).toFixed(0) + ' s' : 'n/a'})`, '',
|
||||
`**Titel:** ${y.title ?? '-'}`, '', (y.summary || '_keine_'), '',
|
||||
`**Todos:** ${y.todos ?? '-'}`, '');
|
||||
}
|
||||
const f = path.join(OUT, `vergleich-${a}-vs-${b}.md`);
|
||||
fs.writeFileSync(f, md.join('\n'));
|
||||
console.log(`\nVolltext-Vergleich: ${f}`);
|
||||
}
|
||||
|
||||
const [cmd, ...rest] = process.argv.slice(2);
|
||||
try {
|
||||
if (cmd === 'snapshot') await snapshot(rest.map(Number));
|
||||
else if (cmd === 'run') await run(rest[0], rest.slice(1).map(Number));
|
||||
else if (cmd === 'restore') await restore();
|
||||
else if (cmd === 'report') report(rest[0], rest[1]);
|
||||
else console.log('Befehle: snapshot <ids...> | run <label> <ids...> | report <a> <b> | restore');
|
||||
} finally {
|
||||
await pool.end();
|
||||
}
|
||||
@@ -356,3 +356,37 @@ frei** statt 100 MB.
|
||||
|
||||
**Preis:** die nächste Analyse lädt rkllm neu. Der Kurz-Clip-Pfad verliert damit seinen „rkllm bleibt
|
||||
warm"-Vorteil (Kap. 03) — dafür ist die Analyse verlässlich statt schnell-aber-manchmal-tot.
|
||||
|
||||
## Modellwechsel und A/B-Tests
|
||||
|
||||
Das aktive Modell steht in **einer** systemd-Zeile:
|
||||
|
||||
```bash
|
||||
cat /etc/systemd/system/callrec.service.d/gemma-model.conf
|
||||
# [Service]
|
||||
# Environment=RKLLM_MODEL=/opt/rkllm-models/qwen3-4b-w8a8g128-16k.rkllm
|
||||
systemctl daemon-reload && systemctl restart callrec
|
||||
```
|
||||
|
||||
`llm.js:10` liest `process.env.RKLLM_MODEL` (Fallback: das alte qwen2.5-3b). Ein Wechsel ist damit
|
||||
umkehrbar, solange die alte `.rkllm` liegen bleibt. **Vor jedem Restart prüfen, ob eine Analyse läuft**
|
||||
— ein Restart killt sie ersatzlos.
|
||||
|
||||
Gemessen wird mit `lab/ab-llm.mjs` (seit 2026-09-29). Es ruft `reanalyze-llm` auf, also **ohne**
|
||||
Neu-Diarisierung: gleiches Transkript, gleiche Sprecher, gleiche Kontextnotiz — der einzige Unterschied
|
||||
zwischen zwei Läufen ist das Modell. `snapshot` sichert die Originalanalysen vorher, `restore` schreibt
|
||||
sie zurück, `report` legt Tabelle und Volltext-Vergleich nebeneinander.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
SNAP["snapshot <ids><br/>Originalanalysen sichern"] --> RUNA["run A<br/>aktuelles Modell"]
|
||||
RUNA --> SWAP["gemma-model.conf umstellen<br/>daemon-reload + restart"]
|
||||
SWAP --> RUNB["run B<br/>neues Modell"]
|
||||
RUNB --> REP["report A B<br/>Dauer / Laenge / Todos + Volltext"]
|
||||
REP --> DEC{"besser?"}
|
||||
DEC -->|ja| KEEP["Drop-in bleibt<br/>altes .rkllm liegt als Rueckweg"]
|
||||
DEC -->|nein| BACK["Drop-in zurueck<br/>restore schreibt Analysen zurueck"]
|
||||
```
|
||||
|
||||
Die Konvertierung eines neuen Modells passiert **nicht** auf dem Board (kein CUDA), sondern auf dem PC
|
||||
mit `tools/rkllm-qwen35/` — rkllm-toolkit-Version muss exakt zur Runtime auf dem Board passen.
|
||||
|
||||
@@ -3,6 +3,74 @@
|
||||
Chronik der Entwicklungsschritte. **Regel: jede zukünftige Änderung bekommt hier einen Eintrag** (Datum,
|
||||
Was, Warum, betroffene Dateien/Backups).
|
||||
|
||||
## 2026-09-29 (2) — A/B-Aufbau Qwen3.5-4B (Sicherung, Messstrecke, Konvertierungs-Kit)
|
||||
|
||||
**Anlass:** Recherche zur Frage, ob es inzwischen ein besseres LLM gibt als das produktive
|
||||
Qwen3-4B-Instruct-2507. Ergebnis: **Qwen3.5** (02.03.2026, Apache 2.0, Größen 0,8/2/4/9B, hybride
|
||||
Gated-DeltaNet-Architektur, 262k Kontext, 201 Sprachen) ist der ernsthafte Kandidat — im
|
||||
Artificial-Analysis-Index springt die 4B von 18 auf 27. **rknn-llm v1.3.0** (17.06.2026) hat die
|
||||
Unterstützung dafür gebracht, und genau diese Version läuft bereits auf dem Board
|
||||
(`RKLLM SDK 1.3.0 | build 82bf4aa8e 2026-06-16`, NPU-Treiber v0.9.8) — **kein Runtime-Upgrade nötig.**
|
||||
|
||||
### Sicherung vor dem Test
|
||||
|
||||
Gezielte Sicherung nach `D:\Vibe Coding\_Spielplatz\Aime-Backup-2026-09-29` (nicht im Repo):
|
||||
`/opt/callrec` ohne `recordings/`+`models/` (14,7 MB), `/etc/systemd/system` + `sudoers.d` + Netz/Host
|
||||
(7,4 KB), WirePlumber/PipeWire-Drop-ins (1,5 KB), MariaDB-Dump (8,0 MB), Zustandsbericht mit den md5
|
||||
aller `.rkllm`-Modelle. Alle Prüfsummen nach der Übertragung verifiziert. Aufnahmen (5,4 GB) und Modelle
|
||||
(14 GB) bewusst nicht mitgezogen — die liegen unverändert auf dem Board, die Vollsicherung des rootfs
|
||||
ist die vom 2026-08-03.
|
||||
|
||||
### Warum der Wechsel billig rückgängig zu machen ist
|
||||
|
||||
Das aktive Modell hängt an einer einzigen systemd-Zeile,
|
||||
`/etc/systemd/system/callrec.service.d/gemma-model.conf` → `Environment=RKLLM_MODEL=...`
|
||||
(`llm.js:10` liest `process.env.RKLLM_MODEL`). Zurückschalten = Pfad tauschen, `daemon-reload`,
|
||||
`systemctl restart callrec`. Die alten `.rkllm` bleiben liegen.
|
||||
|
||||
### Neu: Messstrecke `lab/ab-llm.mjs`
|
||||
|
||||
Der Vergleich läuft über `POST /api/recordings/:id/reanalyze-llm` — **ohne** Neu-Diarisierung, also
|
||||
gleiches Transkript, gleiche Sprecher, gleiche Kontextnotiz. Gemessen wird nur das Modell.
|
||||
|
||||
```
|
||||
node lab/ab-llm.mjs snapshot 86 123 114 # Originalanalysen sichern (PFLICHT, reanalyze ueberschreibt)
|
||||
node lab/ab-llm.mjs run qwen3 86 123 114 # Baseline mit dem aktuellen Modell
|
||||
# ... Modell umstellen, callrec neu starten ...
|
||||
node lab/ab-llm.mjs run qwen35 86 123 114
|
||||
node lab/ab-llm.mjs report qwen3 qwen35 # Tabelle + Volltext-Vergleich als Markdown
|
||||
node lab/ab-llm.mjs restore # Originalanalysen zurueckschreiben
|
||||
```
|
||||
|
||||
Testfälle bewusst über die Pfadgrenzen gelegt: **#86** (8,4k Zeichen, kurzer Whole-Pass), **#123**
|
||||
(19,3k — die Größenordnung, an der Gemma im Juli-A/B die Gewichtung verlor), **#114** (34,8k, also
|
||||
über `WHOLE_MAX_CHARS`=32000 → gestückelter Sektions-Pfad).
|
||||
|
||||
### Neu: `tools/rkllm-qwen35/` (Konvertierung auf dem PC)
|
||||
|
||||
`Dockerfile` (CUDA 12.4 + torch 2.6.0-cu124 + rkllm-toolkit **1.3.0**, Version muss zur Board-Runtime
|
||||
passen), `convert_qwen35.py` und `run_convert.ps1`. Parameter identisch zum produktiven Modell, damit der
|
||||
A/B das Modell misst und nicht die Konvertierung: `w8a8_g128`, 3 NPU-Kerne, `max_context=16384`
|
||||
(Toolkit-Obergrenze; die Runtime nutzt davon 12288, `rkllm_server.cpp:52`).
|
||||
|
||||
**GOTCHAs, festgehalten:**
|
||||
- `run_convert.ps1` gehört in die **PowerShell**, nicht in die Git-Bash — die schreibt Docker-Mount-Pfade
|
||||
um (`/data` → `C:/Program Files/Git/data`). Gleiche Falle wie bei der Qwen3-Konvertierung im Juli.
|
||||
- torch wird im Dockerfile **vor** den übrigen Requirements und explizit als cu124-Rad installiert, und
|
||||
`torch`/`torchvision` werden aus `requirements.txt` gefiltert — sonst zieht pip die CPU-Räder nach und
|
||||
die Quantisierung läuft stundenlang auf der CPU.
|
||||
- `auto_gptq` darf fehlschlagen (nur für den GPTQ-Pfad nötig, nicht für w8a8).
|
||||
- Thinking ist kein Problem: Qwen3.5 denkt per Default, aber `rkllm_server.cpp:89` setzt bereits
|
||||
`in.enable_thinking = false`.
|
||||
|
||||
**Kalibrierung:** `make_calib.py` baut aus echten deutschen Transkripten einen Kalibriersatz
|
||||
(`[{"input":..,"target":..}]`, 64 Ausschnitte à ~1400 Zeichen). Der Satz bleibt bewusst **auf dem Board**
|
||||
(`/tmp/calib-de.json`) — Gesprächsinhalte wandern nicht auf den PC. Wer ihn nutzen will, konvertiert auf
|
||||
dem Board oder kopiert ihn bewusst; ohne `--dataset` nimmt das Toolkit seinen eingebauten Datensatz.
|
||||
|
||||
**Stand:** Baseline-Lauf `qwen3` gestartet, Modell noch nicht konvertiert — Ergebnis folgt in einem
|
||||
eigenen Eintrag.
|
||||
|
||||
## 2026-09-29 — Aufnahmezeitpunkt in der Detailansicht
|
||||
|
||||
**Wunsch:** „Wenn man eine Aufzeichnung geöffnet hat, soll neben der Analysezeit auch das Datum und
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# rkllm-toolkit 1.3.0 auf CUDA — konvertiert Qwen3.5-4B nach .rkllm fuer den RK3588.
|
||||
# Version 1.3.0 ist Pflicht: sie muss zur Runtime auf dem Board passen
|
||||
# (RKLLM SDK 1.3.0, build 82bf4aa8e 2026-06-16) und bringt erst die Qwen3.5-Unterstuetzung.
|
||||
FROM nvidia/cuda:12.4.1-cudnn-devel-ubuntu22.04
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
python3.10 python3-pip python3.10-dev git ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /work
|
||||
COPY requirements.txt /work/requirements.txt
|
||||
COPY rkllm_toolkit-1.3.0-cp310-cp310-linux_x86_64.whl /work/
|
||||
|
||||
# torch zuerst und explizit als CUDA-Build; danach die uebrigen Requirements OHNE
|
||||
# torch/torchvision, sonst zieht pip die CPU-Raeder nach und die Quantisierung
|
||||
# laeuft auf der CPU (Stunden statt Minuten).
|
||||
RUN pip3 install --no-cache-dir torch==2.6.0 torchvision==0.21.0 \
|
||||
--index-url https://download.pytorch.org/whl/cu124
|
||||
RUN grep -viE '^(torch|torchvision)==' /work/requirements.txt > /work/req-rest.txt \
|
||||
&& pip3 install --no-cache-dir -r /work/req-rest.txt
|
||||
# auto_gptq kompiliert gegen CUDA und ist fuer unseren w8a8-Pfad nicht noetig:
|
||||
# Fehlschlag darf den Build nicht killen.
|
||||
RUN pip3 install --no-cache-dir auto_gptq==0.7.1 || echo "auto_gptq uebersprungen (fuer w8a8 nicht noetig)"
|
||||
RUN pip3 install --no-cache-dir /work/rkllm_toolkit-1.3.0-cp310-cp310-linux_x86_64.whl
|
||||
|
||||
# HF-Cache und Ausgabe liegen im gemounteten /data, damit ein Neustart des
|
||||
# Containers den 8-GB-Download nicht wiederholt.
|
||||
ENV HF_HOME=/data/hf
|
||||
ENV PYTHONUNBUFFERED=1
|
||||
COPY convert_qwen35.py /work/convert_qwen35.py
|
||||
CMD ["python3", "/work/convert_qwen35.py"]
|
||||
@@ -0,0 +1,62 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Qwen3.5-4B -> .rkllm (RK3588), Parameter identisch zum laufenden Qwen3-4B.
|
||||
|
||||
Damit der A/B-Test das MODELL misst und nicht die Konvertierung, sind alle
|
||||
Stellschrauben so gesetzt wie beim produktiven qwen3-4b-w8a8g128-16k.rkllm:
|
||||
w8a8 mit Gruppengroesse 128, 3 NPU-Kerne, max_context 16384 (das Maximum des
|
||||
Toolkits; die Runtime auf dem Board nutzt davon 12288, siehe rkllm_server.cpp:52).
|
||||
"""
|
||||
import argparse, os, sys, time
|
||||
|
||||
from rkllm.api import RKLLM
|
||||
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument('--model', default=os.environ.get('MODEL_ID', 'Qwen/Qwen3.5-4B'),
|
||||
help='HF-Repo oder lokaler Pfad')
|
||||
ap.add_argument('--out', default='/data/out/qwen35-4b-w8a8g128-16k.rkllm')
|
||||
ap.add_argument('--dtype', default='w8a8_g128',
|
||||
help='w8a8, w8a8_g128, w8a8_g256, w8a8_g512, w4a16, w4a16_g32/64/128')
|
||||
ap.add_argument('--max-context', type=int, default=16384, help='max. 16384, Vielfaches von 32')
|
||||
ap.add_argument('--npu-cores', type=int, default=3)
|
||||
ap.add_argument('--platform', default='rk3588')
|
||||
ap.add_argument('--device', default='cuda', help='cuda oder cpu')
|
||||
ap.add_argument('--dataset', default=None,
|
||||
help='Kalibrier-JSON [{"input":"","target":""}]. Ohne Angabe nimmt das '
|
||||
'Toolkit seinen eingebauten Datensatz.')
|
||||
ap.add_argument('--opt-level', type=int, default=1)
|
||||
ap.add_argument('--hybrid-rate', type=float, default=0)
|
||||
args = ap.parse_args()
|
||||
|
||||
os.makedirs(os.path.dirname(args.out), exist_ok=True)
|
||||
t0 = time.time()
|
||||
|
||||
print('== Lade %s (device=%s)' % (args.model, args.device), flush=True)
|
||||
llm = RKLLM()
|
||||
ret = llm.load_huggingface(model=args.model, device=args.device)
|
||||
if ret != 0:
|
||||
sys.exit('Laden fehlgeschlagen (ret=%d)' % ret)
|
||||
print(' geladen nach %.0f s' % (time.time() - t0), flush=True)
|
||||
|
||||
print('== Quantisiere: %s, max_context=%d, %d NPU-Kerne, Ziel %s'
|
||||
% (args.dtype, args.max_context, args.npu_cores, args.platform), flush=True)
|
||||
ret = llm.build(do_quantization=True,
|
||||
optimization_level=args.opt_level,
|
||||
quantized_dtype=args.dtype,
|
||||
quantized_algorithm='normal',
|
||||
target_platform=args.platform,
|
||||
num_npu_core=args.npu_cores,
|
||||
extra_qparams=None,
|
||||
dataset=args.dataset,
|
||||
hybrid_rate=args.hybrid_rate,
|
||||
max_context=args.max_context)
|
||||
if ret != 0:
|
||||
sys.exit('Build fehlgeschlagen (ret=%d)' % ret)
|
||||
print(' quantisiert nach %.0f s' % (time.time() - t0), flush=True)
|
||||
|
||||
print('== Exportiere nach %s' % args.out, flush=True)
|
||||
ret = llm.export_rkllm(args.out)
|
||||
if ret != 0:
|
||||
sys.exit('Export fehlgeschlagen (ret=%d)' % ret)
|
||||
|
||||
size = os.path.getsize(args.out) / (1024 ** 3)
|
||||
print('FERTIG: %s (%.2f GB) in %.0f s' % (args.out, size, time.time() - t0))
|
||||
@@ -0,0 +1,48 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Baut aus echten deutschen Gespraechstranskripten einen Kalibrier-Datensatz
|
||||
fuer die rkllm-Quantisierung. Laeuft AUF DEM BOARD (python3 + mysql client).
|
||||
|
||||
Format laut rkllm-toolkit 1.3.0: [{"input": "...", "target": "..."}, ...]
|
||||
"""
|
||||
import json, subprocess, random
|
||||
|
||||
SQL = (
|
||||
"select id, transcript from recordings "
|
||||
"where transcript is not null and char_length(transcript) > 2000 "
|
||||
"and deleted_at is null order by id desc limit 24;"
|
||||
)
|
||||
|
||||
out = subprocess.run(
|
||||
['mysql', '-ucallrec', '-pcallrec123', 'callrec', '-N', '-B', '-e', SQL],
|
||||
capture_output=True)
|
||||
rows = out.stdout.decode('utf-8', 'replace').splitlines()
|
||||
|
||||
random.seed(29)
|
||||
samples = []
|
||||
for row in rows:
|
||||
parts = row.split('\t', 1)
|
||||
if len(parts) != 2:
|
||||
continue
|
||||
rid, text = parts
|
||||
# mysql -B escapt Zeilenumbrueche als \n
|
||||
text = text.replace('\\n', '\n').replace('\\t', '\t')
|
||||
text = ' '.join(text.split())
|
||||
if len(text) < 2000:
|
||||
continue
|
||||
# drei Ausschnitte je Aufnahme: Anfang, Mitte, zufaellig
|
||||
starts = [0, max(0, len(text) // 2 - 600)]
|
||||
if len(text) > 6000:
|
||||
starts.append(random.randint(1000, len(text) - 2000))
|
||||
for st in starts:
|
||||
chunk = text[st:st + 1400].strip()
|
||||
if len(chunk) > 800:
|
||||
samples.append({"input": chunk, "target": ""})
|
||||
|
||||
random.shuffle(samples)
|
||||
samples = samples[:64]
|
||||
with open('/tmp/calib-de.json', 'w', encoding='utf-8') as f:
|
||||
json.dump(samples, f, ensure_ascii=False, indent=1)
|
||||
|
||||
print('Samples: %d' % len(samples))
|
||||
print('Zeichen gesamt: %d' % sum(len(s['input']) for s in samples))
|
||||
print('Durchschnitt: %d' % (sum(len(s['input']) for s in samples) // max(1, len(samples))))
|
||||
@@ -0,0 +1,37 @@
|
||||
# In PowerShell ausfuehren, NICHT in der Git-Bash: Git-Bash schreibt Docker-Mount-Pfade
|
||||
# um (/data wird zu C:/Program Files/Git/data) -- das war die Falle bei der
|
||||
# Qwen3-Konvertierung im Juli 2026.
|
||||
#
|
||||
# cd "D:\Vibe Coding\_Spielplatz\Aime\_rkllm-qwen35"
|
||||
# .\run_convert.ps1
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$here = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
$data = Join-Path $here 'data'
|
||||
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $data 'hf') | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path (Join-Path $data 'out') | Out-Null
|
||||
|
||||
Write-Host '== 1/3 GPU sichtbar?' -ForegroundColor Cyan
|
||||
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi --query-gpu=name,memory.total --format=csv
|
||||
if (-not $?) { throw 'Docker sieht die GPU nicht. WSL2-Backend + NVIDIA Container Toolkit pruefen.' }
|
||||
|
||||
Write-Host '== 2/3 Image bauen (einmalig, ~10-15 Min)' -ForegroundColor Cyan
|
||||
docker build -t rkllm-toolkit:1.3.0 $here
|
||||
if (-not $?) { throw 'docker build fehlgeschlagen' }
|
||||
|
||||
Write-Host '== 3/3 Konvertieren (Download ~8 GB beim ersten Lauf, dann ~30-60 Min)' -ForegroundColor Cyan
|
||||
docker run --rm --gpus all --shm-size=8g `
|
||||
-v "${data}:/data" `
|
||||
rkllm-toolkit:1.3.0 `
|
||||
python3 /work/convert_qwen35.py `
|
||||
--model Qwen/Qwen3.5-4B `
|
||||
--out /data/out/qwen35-4b-w8a8g128-16k.rkllm `
|
||||
--dtype w8a8_g128 `
|
||||
--max-context 16384 `
|
||||
--npu-cores 3 `
|
||||
--device cuda
|
||||
|
||||
Write-Host ''
|
||||
Write-Host 'Ergebnis liegt in:' -ForegroundColor Green
|
||||
Get-ChildItem (Join-Path $data 'out')
|
||||
Reference in New Issue
Block a user