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:
2026-09-29 10:55:47 +02:00
co-authored by Claude Opus 5
parent fae5a9e5d0
commit e9ba48224e
7 changed files with 441 additions and 0 deletions
+32
View File
@@ -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"]
+62
View File
@@ -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))
+48
View File
@@ -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))))
+37
View File
@@ -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')