diff --git a/docs/index.md b/docs/index.md index dff8c09..4ea2cc1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -52,6 +52,7 @@ MTP-ONLY draft, port 7770] - [[Running Qwen3.6 35B A3B FP8 on Dankserver]] — MoE model (35B total, 3B active) on RTX 3090 - [[Running Qwen3.6 27B FP8 on Dankclaw]] — Dense model (27B) on Strix Halo APU - [[frank-default-large-benchmark]] — backend/spec benchmark; chose MTP sidecar (2026-08-14) over DFlash (parallel premature-EOS bug) +- [[qwen3.8-spec-matrix]] — Qwen3.8-27B UD-Q8_K_XL bundled-MTP spec matrix; winner draft-mtp n_max=5 - [[frank-default-small-benchmark]] — ncmoe and batch/ubatch sweep that drove the Dankserver state - [[dankserver-vram-growth]] — VRAM/RAM growth investigation behind the recurring cuBLAS crash on frank-default-small - [[Dankclaw Improvements]] — researched enhancements: self-hosted agentic tools, runtime tuning, server/OS settings diff --git a/docs/qwen3.8-spec-matrix.md b/docs/qwen3.8-spec-matrix.md new file mode 100644 index 0000000..e2f1802 --- /dev/null +++ b/docs/qwen3.8-spec-matrix.md @@ -0,0 +1,86 @@ +# Qwen3.8-27B UD-Q8_K_XL Speculative-Decoding Matrix + +This page records the speculative-decoding benchmark and the production +tuning for the unsloth Qwen3.8-27B `UD-Q8_K_XL` service on Dankclaw +(`frank-default-large`). + +## Result (applied) + +- Model: `Qwen3.8-27B-UD-Q8_K_XL.gguf` (29.29 GiB, bundled MTP). +- Spec: `draft-mtp`, `--spec-draft-n-max 5`, no `--spec-draft-p-min`. +- Batch 8192 / ubatch 256, parallel 2, ctx 524288 (262144 per slot). +- Realistic-prompt generation: ~13.3 t/s single, ~8 t/s per slot under + 2 concurrent streams. No premature stop. + +## How MTP works on Qwen3.8 + +The unsloth `UD-*` GGUFs keep the MTP (Multi-Token Prediction) head inside +the model file. llama.cpp b10411+ auto-detects it and creates the draft +context from the target model. Do not pass `--spec-draft-model`. + +The stock llama.cpp build must be b10411 or newer. The 2026-08-14 rebuild +on Dankclaw is b10433 (`9b05354ec`) with `GGML_HIP` + `gfx1151` + Vulkan. + +## Benchmark Method + +The matrix ran 54 configurations: 3 spec types (`draft-mtp,ngram-mod`, +`draft-mtp`, `ngram-mod`) x `--spec-draft-n-max` 2..10 x +`--spec-draft-p-min` {off, 0.8}. Each config loaded `llama-server`, +then ran 3 generations. The tool is `scripts/spec-matrix-bench.sh`; +analysis is `scripts/analyze-spec-matrix.py`. + +## Matrix Caveat: Benchmark Prompt + +The matrix used a repetitive 1100-token prompt +(`scripts/bench_live.sh` style). MTP drafts that repeated sentence almost +perfectly, which inflated the numbers for long drafts. The matrix reported +`draft-mtp` n_max=10 at ~30 t/s. A realistic story prompt gave very +different results: + +| n_max | median t/s (realistic) | +| ----- | ----------------------- | +| 4 | 12.5 | +| 5 | 13.3 (peak) | +| 6 | 10.6 | +| 7 | 10.1 | +| 10 | 8.6 | +| none (baseline) | 7.2 | + +Long drafts collapse to ~18% acceptance on real writing. n_max=5 balances +acceptance against draft cost. + +## Premature Stop Investigation + +Two failure patterns appeared during early production tests: + +1. Speed collapse. `ubatch >= 512` reduced MTP acceptance to ~25% and + generation to ~9 t/s. `ubatch 128` with `batch >= 2048` triggered a + premature instant-EOS under 2 concurrent slots. `ubatch 256` avoids + both: ~30 t/s single, concurrent-safe. +2. Apparent premature-EOS. The repetitive benchmark prompt with `temp 1.0` + sometimes stops after 1-2 tokens because the model legitimately emits an + EOS on a wall of repeated text. This is sampling variance, not a + corruption bug. Realistic prompts never stop early. + +## Production Configuration + +`scripts/frank-default-large.env`: + +```ini +SPEC_TYPE=draft-mtp +SPEC_DRAFT_N_MAX=5 +SPEC_DRAFT_P_MIN= +BATCH_SIZE=8192 +UBATCH_SIZE=256 +CTX_SIZE=524288 +PARALLEL=2 +``` + +Window: 524288 total / 2 = 262144 per slot, matching the pi +`models.json` contextWindow for `frank-default-large`. + +## See Also + +- [[frank-default-large-benchmark]] +- [[ling-3.0-flash-trial]] +- [[deployment]] diff --git a/scripts/analyze-spec-matrix.py b/scripts/analyze-spec-matrix.py new file mode 100644 index 0000000..8d617da --- /dev/null +++ b/scripts/analyze-spec-matrix.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +"""Analyze the spec-matrix TSV: per-config median gen t/s, acceptance, +sorted, with the top configs and per-spec-type bests.""" +import sys, statistics, collections + +path = sys.argv[1] if len(sys.argv) > 1 else "/tmp/spec_matrix_results.tsv" +rows = [] +with open(path) as f: + header = f.readline().strip().split("\t") + for line in f: + p = line.rstrip("\n").split("\t") + if len(p) < 9: + continue + spec, n_max, p_min, run, tps, pps, acc, dn, stop = p[:9] + try: + tps = float(tps) + except ValueError: + continue + rows.append(dict(spec=spec, n_max=n_max, p_min=p_min, run=run, + tps=tps, pps=float(pps or 0), acc=acc, dn=dn, stop=stop)) + +by_cfg = collections.defaultdict(list) +for r in rows: + by_cfg[(r["spec"], r["n_max"], r["p_min"])].append(r) + +print("%-22s %5s %5s %8s %8s %10s" % ("spec", "n_max", "p_min", "med_tps", "med_pps", "accept")) +summary = [] +for (spec, n_max, p_min), runs in by_cfg.items(): + tps_list = [r["tps"] for r in runs] + med = statistics.median(tps_list) + pps_list = [r["pps"] for r in runs] + accs = [r["acc"] for r in runs if r["acc"] not in ("", "None")] + acc_s = "" + if accs: + try: + nums = [float(a) for a in accs] + acc_s = "%.3f" % (sum(nums)/len(nums)) + except ValueError: + pass + summary.append((med, spec, n_max, p_min, + statistics.median(pps_list), acc_s, len(runs))) + +summary.sort(reverse=True) +for med, spec, n_max, p_min, pps, acc, n in summary: + print("%-22s %5s %5s %8.2f %8.2f %10s" % (spec, n_max, p_min, med, pps, acc)) + +print("\n=== TOP 12 CONFIGS ===") +for med, spec, n_max, p_min, pps, acc, n in summary[:12]: + print(" %5.2f t/s spec=%-22s n_max=%2s p_min=%s" % (med, spec, n_max, p_min)) + +print("\n=== BEST PER SPEC TYPE ===") +best = {} +for med, spec, n_max, p_min, pps, acc, n in summary: + key = spec + if key not in best: + best[key] = (med, n_max, p_min, pps, acc) +for k, v in best.items(): + print(" %-22s %5.2f t/s n_max=%s p_min=%s (pps=%s)" % (k, v[0], v[1], v[2], v[3])) + +print("\n=== p_min 0.8 vs off (per spec/n_max, med delta) ===") +from itertools import product +for spec in sorted(set(r["spec"] for r in rows)): + print(" spec:", spec) + for n_max in sorted(set(int(r["n_max"]) for r in rows if r["spec"] == spec)): + on = [r["tps"] for r in rows if r["spec"] == spec and r["n_max"] == str(n_max) and r["p_min"] == "0.8"] + off = [r["tps"] for r in rows if r["spec"] == spec and r["n_max"] == str(n_max) and r["p_min"] == "none"] + if on and off: + d = statistics.median(on) - statistics.median(off) + print(" n_max=%-2s on=%.2f off=%.2f delta=%+.2f" % (n_max, + statistics.median(on), statistics.median(off), d)) diff --git a/scripts/spec-matrix-bench.sh b/scripts/spec-matrix-bench.sh new file mode 100644 index 0000000..cae33cc --- /dev/null +++ b/scripts/spec-matrix-bench.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# bench-spec-matrix.sh - Qwen3.8-27B UD-Q8_K_XL speculative-decode matrix. +# Matrix: spec_type {draft-mtp,ngram-mod | draft-mtp | ngram-mod} +# x --spec-draft-n-max {2..10} x --spec-draft-p-min {off, 0.8} +# = 54 configs. Each: start llama-server, wait ready, 3 generations of a +# 1100-token prompt (n_predict 200, default sampling), record timings. +set -u + +MODEL=/home/dank/models/Qwen3.8-27B-UD-Q8_K_XL.gguf +LLAMA_SERVER=/home/dank/llama.cpp/build/bin/llama-server +PORT=7890 +DEVICE=ROCm0 +OUT=/tmp/spec_matrix_results.tsv +RUNLOG=/tmp/spec_matrix_run.log +PROMPT=$(python3 -c "print(\"The fleet of inference servers must remain available at all times. Operators monitor memory, throughput, and queue depth. \"*50)") +N_OUT=200 +RUNS=3 + +: > "$OUT" +printf 'spec_type\tn_max\tp_min\trun\ttps\tpps\tdraft_acc\tdraft_n\tstop\n' >> "$OUT" + +cfg=0 +for spec in "draft-mtp,ngram-mod" "draft-mtp" "ngram-mod"; do + for n_max in 2 3 4 5 6 7 8 9 10; do + for p_min in none 0.8; do + cfg=$((cfg+1)) + echo "=== CONFIG $cfg/54: spec=$spec n_max=$n_max p_min=$p_min $(date +%T) ===" | tee -a "$RUNLOG" + ARGS=(--model "$MODEL" --spec-type "$spec" --spec-draft-n-max "$n_max") + # Qwen3.8 bundles its MTP drafter inside the target GGUF; no separate + # --spec-draft-model file (llama.cpp auto-detects, b10411+). + if [[ "$p_min" != none ]]; then + ARGS+=(--spec-draft-p-min "$p_min") + fi + ARGS+=(--device "$DEVICE" --gpu-layers all --cache-type-k q8_0 --cache-type-v f16 + --ctx-size 2048 --parallel 1 --batch-size 512 --ubatch-size 128 + --temp 1.0 --top-k 20 --top-p 0.95 --min-p 0.0 + --host 127.0.0.1 --port "$PORT") + "$LLAMA_SERVER" "${ARGS[@]}" > /tmp/matrix_server.log 2>&1 & + SPID=$! + ready=0 + for i in $(seq 1 150); do + if curl -sf "http://127.0.0.1:$PORT/health" > /dev/null 2>&1; then ready=1; break; fi + if ! kill -0 "$SPID" 2>/dev/null; then break; fi + sleep 2 + done + if [[ "$ready" != 1 ]]; then + echo "NOT_READY: spec=$spec n_max=$n_max p_min=$p_min" | tee -a "$RUNLOG" + tail -4 /tmp/matrix_server.log >> "$RUNLOG" + kill "$SPID" 2>/dev/null; wait "$SPID" 2>/dev/null + continue + fi + for r in 1 2 3; do + curl -s -m 300 -X POST "http://127.0.0.1:$PORT/completion" -H "Content-Type: application/json" \ + -d "{\"prompt\": \"$PROMPT\", \"n_predict\": $N_OUT, \"stream\": false, \"cache_prompt\": false}" \ + | python3 -c " +import json,sys +try: + d=json.load(sys.stdin) + t=d.get('timings',{}) + row=[\"$spec\",\"$n_max\",\"$p_min\",\"$r\", + str(round(t.get('predicted_per_second',0),2)), + str(round(t.get('prompt_per_second',0),2)), + str(t.get('draft_n_accepted','')), + str(t.get('draft_n','')), + str(d.get('stop_type',''))] + with open(\"$OUT\",\"a\") as f: + f.write('\t'.join(row)+'\n') +except Exception as e: + with open(\"$OUT\",\"a\") as f: + f.write('\t'.join([\"$spec\",\"$n_max\",\"$p_min\",\"$r\",\"ERR\",str(e),\"\",\"\",\"\"])+'\n') +" + done + kill "$SPID" 2>/dev/null; wait "$SPID" 2>/dev/null + sleep 3 + done + done +done +echo "=== MATRIX DONE $(date) ===" | tee -a "$RUNLOG"