- runbook: identity now 4x262144, audio verified, throughput table with the OOM and negative-result rows, deployed flags updated to reality. - AGENTS.md frank-small paragraph matches the deployed shape. - log.md: the sweep entry. - scripts/frank-small.env: my earlier line-range edit had clobbered NGL=999 and FLASH_ATTN=on out of the file; the seams caught it. The live service was unaffected because the script's defaults match the declared values exactly, but the file contract is restored. Suite: ALL TESTS PASSED (321), vault 125 pages 0 unresolved.
9.0 KiB
type: reference title: frank-small on Dankclaw — llama.cpp, Gemma4-12B QAT Q4_K_M description: The active frank-small runbook — mainline llama.cpp on Dankclaw's RTX 3090 serving HauhauCS Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced at Q4_K_M with a separate MTP assistant sidecar chained behind ngram-mod, q8_0 KV, and --fit sizing the full 262k window: identity, payload, KV arithmetic, the spec-decode chain, deployed flags, the client contract, and what to check when it misbehaves. tags:
- llm
- frank-small
- dankclaw
- llama.cpp
- rtx3090
- gemma
- mtp
- vision timestamp: 2026-09-17
frank-small — llama.cpp on the RTX 3090 (Gemma4-12B QAT)
ACTIVE since 2026-09-17. Replaces frank-small-qwen38-gsq-rco (one-day rollback target). Same unit, same port, same client contract — the model, the drafter shape and the KV type changed.
Identity
| what | value |
|---|---|
| runtime | mainline llama.cpp master 37b53fd, CUDA build with -DGGML_CUDA_FA_ALL_QUANTS=ON (rebuilt 2026-09-17 for q8 KV) |
| binary | /home/dank/GitPackages/llama.cpp-mainline/build-cuda/bin/llama-server |
| unit | root systemd unit frank-small.service (repo scripts/frank-small.service, relink_united into /etc/systemd/system/) |
| model | HauhauCS/Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced Q4_K_M |
| model dir | /home/dank/models/Gemma4-12B-QAT/ |
| port | 7771 (alias frank-small; frank-large keeps 7770) |
| window | 4 slots x 262,144 (CTX_SIZE=1048576, PARALLEL=4); q8_0 K and V |
| vision + audio | fleet image path via BF16 mmproj (input: [text, image]); input_audio transcribes verbatim (verified) |
Payload (all sha256-verified against the Hub)
| file | bytes | sha256 (head…) |
|---|---|---|
Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced-Q4_K_M.gguf |
7,381,381,760 | 59656d74… |
mmproj-Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced-BF16.gguf |
175,115,264 | b59e8154… |
mtp-gemma-4-12B-it.gguf |
253,707,328 | c50c91c3… |
sha256.txt beside them on the host carries the full digests.
What the preflight found (GGUF headers, read over HTTP Range)
- Target (
arch gemma4, 48 blocks): ctx 262144, MHA — 16 kv heads x 512 dim (no GQA), embedding 3840, rope base 1e6, logit softcap 30, sliding_window 1024,swa_type = STANDARD, per-layer token embeddings (PLE) viaLLM_KV_EMBEDDING_LENGTH_PER_LAYER. 12 full-attention layers every 4th ([3,7,…,47]implied byfull_attention_interval = 4). 48 blocks maps toLLM_TYPE_UNKNOWNin the loader's size switch — cosmetic only. - mmproj: clip, vision 224/16 patches, also carries an audio
encoder (
has_audio_encoder=True) — vision is the fleet contract, and audio works: a JFK WAV sent as aninput_audiocontent part was transcribed verbatim (2026-09-17). - MTP sidecar (
arch gemma4-assistant): 4 nextn layers, pure assistant (no trunk). Its KV shares the target's last layers — the share lambda atllama-model.cpp:2662referencesllama_model_n_layer(model_other) - 2for SWA layers. Adding the drafter therefore costs no second KV pool.
KV arithmetic — why four full windows fit
Per token per layer at q8_0: 16 heads x 512 dim x 1 B x 2 (K,V) = 16 KiB (32 KiB at f16). With SWA_STANDARD and window 1024, the sliding layers cache only the window; the full-attention layers cache the full context — the pattern that makes the window cheap (the naive "all-global" worst case was 32 GiB at q8/262k, impossible).
Measured on the 3090 (24,576 MiB): one full-context slot idles at
11,996 MiB, four at 22,346 MiB — 3,450 MiB per slot, ~8,546 MiB fixed.
A fifth slot projects past the card, so 4 x 262,144 is the ceiling.
CTX_SIZE is set explicitly to PARALLEL x 262144 rather than left for
--fit: fit maximises the TOTAL pool and would divide a smaller window
per slot at parallel > 1. The slot-grid seam in
tests/frank_small_unit.bats pins both the full-window division and the
budget inequality so an oversized bump fails pre-deploy, not in boot.
Spec-decode chain
--spec-type draft-mtp,ngram-mod with --model-draft $MODEL_DIR/mtp-gemma-4-12B-it.gguf:
draft-mtpis context-backed and reads the assistant sidecar. Two context-backed drafters cannot share one process, so the chain carries exactly one.ngram-modis draftless and attaches unconditionally, catching prompt-echo traffic MTP would miss. It prints no startup banner — verify behaviorally via the per-taskdraft acceptanceline, not the load log.- Measured at deploy:
draft acceptance = 0.38–0.46, mean len ≈ 2.2(lower acceptance than the retired Qwen in-file MTP's 0.66–0.94, but a much smaller model — the point of the swap was the VRAM and the window, not acceptance).
Throughput tuning (measured 2026-09-17)
Salted API probes at the deployed geometry (4 slots, q8 KV, flash attn), prefill ~254k tokens / decode at ~253k context:
| batch (b/ub) | draft n-max / poll | pp t/s | tg@250k t/s | idle MiB |
|---|---|---|---|---|
| 2048/512 (old) | 3 / 50 | 912–924 | 90*–204 | 22,346 |
| 8192/1024 (deployed) | 3 / 50 | 962–965 | 226–302 | 23,300 |
| 8192/1024 | 4 / 100 | 961 | 230–257 | 23,300 |
| 4096/1024 | 4 / 100 | 959 | 205–277 | 23,300 |
| 8192/2048 | — | — | — | OOM |
- ubatch 1024 is the winner: +5% max-length prefill, and decode at 250k clearly above the 512 baseline.
- ubatch 2048 cannot run: the MTP draft context needs ~3.3 GiB of pp
compute buffer at that ubatch against ~2.2 GiB headroom — it dies in
common_speculative_init_resultafter the main model loads. - Deeper draft and busy-wait HURT long-ctx decode: draft n-max 4 with poll 100 verified a longer round per token, and at 250k the extra full-attention re-read outweighs the accepted-token gain (230–257 vs 257–302). Keep 3 / 50.
- All ubatch-1024 variants idle identically (23,300 MiB) — compute buffers are reserved at max graph size regardless of the logical batch, so 8192 costs nothing over 4096.
- Concurrency caveat: a long prefill monopolises the GPU — a decode running beside a 254k-token prefill drops from ~240 to ~100–135 t/s. Four slots share one engine with no per-slot QoS; that is inherent, not a config fault.
- No CUDA graphs in this build —
--helpshows zero graph flags; the decode-launch-overhead lever does not exist here.
Deployed flags
Repo truth: scripts/frank-small.env + scripts/serve-frank-small.sh.
Key values: MODEL_DIR=/home/dank/models/Gemma4-12B-QAT,
MODEL_FILE=Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced-Q4_K_M.gguf,
DRAFT_FILE=mtp-gemma-4-12B-it.gguf, CACHE_TYPE_K=V=q8_0,
SPEC_TYPE=draft-mtp,ngram-mod, SPEC_DRAFT_N_MAX=3, POLL_LEVEL=50,
CTX_SIZE=1048576 with PARALLEL=4 (four full 262,144-token slots),
BATCH_SIZE=8192, UBATCH_SIZE=1024, NGL=999, FLASH_ATTN=on,
--jinja, --metrics, greedy --temp 0.0 server default. The serve
script refuses to start on any missing file.
Client contract
reasoning_contentcarries thinking;preserve_thinking: trueis sent by the harness to keep it across turns.- Every
reasoning_effortlevel is accepted (all seven probed 2026-09-17: 200) — unlike Qwen3.8's template gate. The harness'sreasoningEffortMapin~/.omp/agent/models.ymlis therefore inert but kept as a safety net. - Tools work through the
--jinjatemplate (tool_callsverified). - Images work through the mmproj (verified with a live JPEG).
- Four concurrent conversations (one per slot). No per-slot QoS: a long prefill in one slot slows decodes in the others — see the caveat above.
- Harness:
contextWindow: 262144,maxTokens: 32768.
Troubleshooting
Gemma4Assistant requires ctx_other to be setprinted at boot — the--fitprobe, not a fault (fit cannot build the assistant context during measurement). WithCTX_SIZEset explicitly, as deployed, this line does not appear at all. Do not restart over it either way.- Draft looks unengaged: ngram-mod is silent at load; read the
draft acceptance = …line per task fromjournalctl -u frank-small(the server must not be restarted just to see this). - Quantized KV regression: if someone rebuilds
build-cudawithoutGGML_CUDA_FA_ALL_QUANTS=ON, prefill collapses ~12x with q8 KV (see the CUDA lesson in AGENTS.md). Checkgrep FA_ALL_QUANTS build-cuda/CMakeCache.txtafter any rebuild. - Logs:
journalctl -u frank-small(root unit, not quadlet).
Rollback
Redeploying the prior model means repointing MODEL_DIR/MODEL_FILE/
DRAFT_FILE back to the Qwen trio and SPEC_TYPE=draft-mtp — see
frank-small-qwen38-gsq-rco for the exact deployed values. The Qwen
weights were NOT deleted from the host.
References
scripts/serve-frank-small.sh— the actual exec line- frank-small-qwen38-gsq-rco — RETIRED predecessor (rollback)
- halogen-flash-server — the sibling endpoint on the iGPU
- x4-pair-experiment — why the 3090 serves its own model, not the trunk