Compare commits

...
Author SHA1 Message Date
Frank 41d2f56269 docs: frank-secondary at ubatch 2048 — Measured section, rollback ladder, fleet docs
23,120 used / 1,005 free at boot, 763 settled after a vision probe
(encoder buffers resident). Rung cost 746 MiB, not the transferable
616 prediction. Prefill baseline 1,209 t/s @ 3,760 recorded; decode
unchanged as the law says. 2048 = ceiling for this shape.
2026-10-06 18:40:26 -07:00
Frank d8da7a9eb7 frank-secondary: ubatch 2048 (the second rung inside the 112k headroom)
Frank's order the same night. The 1024->2048 rung measured +4.6 %
prefill for 616 MiB on the 3090 (2026-10-05 boot pair); the 112k cut
left 1,751 MiB free, so it rides with ~1.1 GiB predicted margin.
Decode is ubatch-invariant; batch 4096 >= 2048 keeps the seam happy.
2026-10-06 18:30:35 -07:00
Frank 44e7a8dfaf docs: frank-secondary at 2x114688/ubatch 1024 — runbook renamed + rewritten, fleet docs updated
- runbook git mv ...-mtp-131072.md -> ...-mtp-114688.md; new Measured
  section (22,374 used / 1,751 MiB free, decode 58.2/66.9 t/s, confounded
  vs 2x131072 noted), rollback ladder, trail entry
- AGENTS.md: topology row, ACTIVE section, contextWindow 114688, runbook
  index; mmproj name corrected to the publisher's mmproj-BF16.gguf
- LLM/index.md, index.md, mimo runbook link, log.md entry + path fix
2026-10-06 18:13:05 -07:00
Frank f2575a1018 frank-secondary: 2×114688 ctx (112k per-slot), ubatch 1024
Lower per-slot context from 131,072 (128k) to 114,688 (112k) to free
~1 GiB of KV, which funds the ubatch 512 -> 1024 increase (prefill
throughput). The per-slot window is the product contract, so models.yml
contextWindow follows to 114,688.

- CTX_SIZE: 262144 -> 229376 (2 × 114,688)
- UBATCH_SIZE: 512 -> 1024
- Test 194: per-slot 131072 -> 114688
- KV budget test: 131072 -> 114688, ubatch 512 -> 1024
2026-10-06 16:38:58 -07:00
Frank 87721ab0fe rename frank-small → frank-secondary (Dankclaw llama.cpp :7771)
Cross-cutting rename of the current llama.cpp endpoint on the RTX 3090
(port 7771) from frank-small to frank-secondary:

- Service trio: scripts/{frank-small.env,frank-small.service,
  serve-frank-small.sh,download-frank-small.sh} → frank-secondary.*
- Test: tests/frank_small_unit.bats → tests/frank_secondary_unit.bats
- Runbooks: serverAppConfigs-docs/LLM/frank-small-*.md →
  frank-secondary-*.md (9 files); the FreeToken runbook stays
  freetoken-frank-small.md (it documents the retired CUDA-era service)
- Identifiers: FRANK_SMALL→FRANK_SECONDARY, llm_small→llm_secondary,
  LLM_SMALL_*→LLM_SECONDARY_*, deploy_llm_small→deploy_llm_secondary
- Historical references to the OLD frank-small (FreeToken/CUDA on the
  3090, and the Dankserver service that became frank-medium) are
  preserved verbatim — they describe what the service was called
  at the time.
2026-10-06 15:37:13 -07:00
Frank c71a3066c0 frank-large: host-RAM pin-gate preflight in serve-frank-large.sh
The 2026-10-06 cutover crash-looped frank-large 8 boots (03:09:39-03:11:42):
the engine's pin gate refused to start while host MemAvailable was ~74.7 GiB,
3.34 GiB short of the 78.11 GiB it needs (62.11 GiB checkpoint + 16 GiB floor).
It self-resolved in ~2 min when the dying 27B engine freed RAM, but left the
host with only ~1.8 GiB MemAvailable - a restart would crash-loop again.

serve-frank-large.sh now checks MemAvailable against the pin gate BEFORE
podman run and WAITS up to FRANK_LARGE_RAM_WAIT (default 600 s) for the RAM
to clear, so a transient dip boots once instead of crash-looping; if it never
clears it refuses loudly. The 16 GiB floor is the engine's (not
HALOGEN_HOST_RESERVE_GIB=20, a separate overall-budget knob).

frank_large_unit.bats: FRANK_LARGE_MEMINFO seam + a refuse test. Runbook +
log: the pin-gate law, the incident, and the fix.
2026-10-06 12:58:34 -07:00
Frank df6b5bb6e8 MAX_SEQS 2->3: decode 33.06 t/s (+2.0% over depth-2 32.40; +4.2% over baseline). Bigger batch headroom co-schedules concurrent decode streams (decode is weight-streaming bound, so a fuller batch amortizes the weight read). KV pool 600k tokens (3x200k), above the 400k floor. Draft acceptance unchanged (0.755).
Result: {"status":"keep","decode_tps":33.0578,"draft_accept":0.7549,"prefill_tps":452.9596,"ttft_ms":96315.4258}
2026-10-06 04:18:53 -07:00
Frank 95923d3a98 SPEC_TOKENS 3->2: decode 32.40 t/s (+2.1% over 31.72). Acceptance 0.755 (between depth-3 0.831 and depth-4 0.497) — shallower drafts verify cheaper, net positive. Depth 2 is the decode optimum. Prefill/TTFT shifts are cold-run noise (prefill is orthogonal to spec depth).
Result: {"status":"keep","decode_tps":32.4031,"draft_accept":0.7549,"prefill_tps":372.9299,"ttft_ms":116984.442}
2026-10-06 04:09:39 -07:00
Frank 81e6122891 frank-primary: autoresearch bench harness (decode t/s @32k)
autoresearch.sh (dev box) scp's the candidate scripts/frank-primary.env
+ the bench script to dankserver and runs it over ssh.
scripts/autoresearch-frank-primary.sh (dankserver) follows the bench
policy: stop frank-primary (relink unit: stop, not mask), boot a
scratch radiance container (arbench) on the same two cards with the
candidate argv, poll /health, run ONE self-contained greedy completion
(fixed ~140k-char real-text prompt -> ~32k tokens, 256-token decode),
parse the engine timings into METRIC lines, tear the scratch down, and
restore production via a trap. Fresh prefix-cache tier per run for cold
prefill. Baseline validated: decode_tps=31.70, prefill_tps=465.4,
ttft_ms=93745, draft_accept=0.831 at prompt_n=43627.
2026-10-06 03:53:55 -07:00
Frank 2d89bf97cc docs: frank-large flash-server v2 cutover — runbook, index, AGENTS.md
- New runbook: frank-large-flash-server-v2-swift15.md (engine, shape,
  memory model, engine laws, boot ledger, probes, rollback)
- LLM/index.md: active entry + retired halogen 27B entry
- Vault index: link the new runbook from the LLM Fleet section
- AGENTS.md: fleet topology table, Model Config section,
  Runbook Index, retired table (halogen 27B → 10-06)
2026-10-06 03:35:27 -07:00
Frank 20d7182f4b docs index: link frank-large flash-server v2 page
The writer's 31b1ec3 added LLM/frank-large-flash-server-v2-swift15.md
but did not wiki-link it from index.md, which failed the docs-index
seam (test 135). Add the link as the new ACTIVE frank-large runbook
(supersedes halogen 0.1.4).
2026-10-06 03:26:43 -07:00
Frank 0f9faf05ea radiance runbook: MNBT A/B verdict + rad-tune tune-dir bind-mount
Snapshot of a parallel session's completed docs edit (mtime 03:07,
was blocking the deploy tree-clean gate): --max-num-batched-tokens
2048 A/B-verified (231/460/448 t/s cold prefill ladder), rad-tune
cache survives via FRANK_PRIMARY_STATE_DIR/tune bind-mount
(36 GEMM rows, generated 2026-10-06).
2026-10-06 03:08:30 -07:00
Frank 31b1ec397b frank-large: cutover to flash-server v2 (abliterated Swift1.5)
Engine: halogen-flash-server 0.16.2, digest-pinned (pulled 0c61bf84).
Payload: Quat3rnion qwen38-flash-next-v2-swift15-abliterated.hgn
(66,687,678,464 B, sha256-verified) + REQUIRED n-gram table
(51,200,246,144 B @peonist 5cc17cea) + vision tower (897,916,416 B
@peonist e053f488) + shared tokenizer (6 files). Shape: 2 slots x
200192 (KV pool 400384 positions, ~10.7 GiB), MTP always-on
(HALOGEN_SPEC_ADAPT=0, card-mandated), server-side sampler at the
card's 1.0/0.95/20, MAX_TOK 8192 with the 65536/7200 coupled pair,
vision ON. Rollback = revert this commit (27B payload stays on disk).

- serve script: byte-exact gates on all three .hgn files + flat
  tokenizer, foreground podman run (ledger to the log file)
- deploy-remote: twin gates for n-gram/vision, flashnext fetch pointer
- seam: 19 tests rewritten (spec-adapt law, pool product, compaction
  math, render shape); full suite 362/362 green
2026-10-06 03:07:40 -07:00
Frank 7fef3cb907 frank-primary: persist rad-tune kernel cache on a host mount
The engine's kernel selector reads $RADIANCE_HOME/tune/<machine>.tune at
boot; $RADIANCE_HOME defaults to /opt/radiance, which lives in the image,
so a cache written by a --rm scratch container dies with it. Pre-create
$FRANK_PRIMARY_STATE_DIR/tune and bind-mount it at
/opt/radiance/share/radiance/tune so the rad-tune cache (generated in a
stopped-service window) survives and is read at every production boot.
2026-10-06 03:02:51 -07:00
Frank bb3532dfec log: 2026-10-06 frank-small wave (two reshapes; RCO-IQ3_XXS + MTP-on, 489 MiB free, draft ctx ~2.4 GiB measured) 2026-10-06 01:41:23 -07:00
Frank 0f7274e55a frank-small docs: runbook renamed + rewritten for the RCO-IQ3_XXS MTP-on shape (489 MiB free, 63.3 t/s recipe, draft ctx ~2.4 GiB measured); AGENTS, vault indexes, models.yml comment, MiMo cross-link retargeted 2026-10-06 01:33:57 -07:00
Frank da36fa32cf frank-small: Swift-1.5-27B Uncensored RCO-IQ3_XXS with MTP back on (two 131072 slots, ubatch 512, q8_0 KV, publisher's BF16 mmproj) 2026-10-06 01:10:43 -07:00
Frank 713b126639 frank-primary: cutover measured — docs reflect the live service
AGENTS.md: row + section carry the measured 177,109 MiB container, floor
177000, live cutover 2026-10-06. Runbook: the three convert laws the live
runs proved (scale-name, file-shadow, symlink-through), the measured boot
(125.26 GiB page-in 38.1 s, PLE pin 47.68 GiB/43.9 s, ~2 min to health),
the MTP/vision/tool/reasoning contract results, and the SSD tier fix.
2026-10-06 01:06:11 -07:00
Frank 2d9cecae6d frank-primary: pre-create the SSD prefix tier path
The engine's create is not recursive: booting with only the state dir
mounted left the SSD prefix tier off ('cannot create
/data/kvcache/frank-primary: No such file or directory'). The serve
script now mkdir -p's the full path before podman run.
2026-10-06 01:00:54 -07:00
Frank fadad18da1 docs: frank-large FA_TQ A/B rejected at 80k; deep roles rerouted to frank-primary
Stop-window A/B, same 80k filler prompt, 128-token essays, runtime
timings: HALOGEN_FA_TQ=256 (upstream issue #12's carve workaround) moves
decode ±0.3% (noise, draft acceptance identical) and costs 9.5% prefill
on this topology. The depth collapse sits deeper than the FA tile knob;
the default stands. The harness's slow/advisor roles moved to
frank-primary (fp8 KV 7.32 KiB/token = 8.7x less KV read at depth).
2026-10-06 00:59:14 -07:00
Frank 306ff32427 frank-primary: rad floor under the measured payload (177109 MiB)
The first deploy refused: floor 178000 > actual 177109 MiB - the plan-only
estimate (172.76 GiB) prices the data planes but not the container's
metadata tail. Floor 177000 sits just under the real payload.
2026-10-06 00:55:30 -07:00
Frank d39424ffba docs: frank-large cache pin verified warm (16x prefill) + depth law
Probe on the live endpoint (2026-10-06): 17.3k cold prefill 27.0 s ->
warm turn resumes at cache_n 16,384, prefill 1.7 s. The boot WARNING
'smaller than ONE full-context entry' sizes against the native 262,144
window, not our 200,700 slot — documented as expected in the checklist.
Tuning notes record the depth law (decode 5.5 t/s @ 46k -> 3.3-3.8 @ 80k
vs 30-50 shallow; KV reads ~8 GB/s effective; no supported flag touches
decode) and the AGENTS.md unit paragraph regains the unit-facts
parenthetical the reflow dropped.
2026-10-06 00:34:59 -07:00
Frank 3eb4fa2b62 frank-primary: rad floor gate to the measured 172.76 GiB container
Plan-only measured the .rad at 172.76 GiB (176,906 MiB) - the checkpoint's
own fp8 mass, byte-stacked experts plus the PLE re-encoded at the
checkpoint's scale - not the ~161 GiB first estimate. Floor 158000 -> 178000.
2026-10-06 00:29:23 -07:00
Frank ba67ca5c9e frank-large: pin the prompt cache (16 GB) and capture the engine ledger on disk
Auto cache-sizing is OFF on this box: the boot ledger refused it at
36.9 GB (2 x full-entry) floor vs 35.7 GB free, so every deep agent
request re-prefills its whole 79-81k context (84.9-179.8 s measured on
serve_api lines, 2026-10-06) for ~500-650 new tokens per turn. Pin
HALOGEN_CACHE_MB=16384 (>= one full-slot entry at ~70 KB/token) — the
documented override; the per-store reserve re-check keeps the host safe.

Also: the serve script's 'podman run -d > LOG' captured only the
container ID — the runbook's boot-ledger grep read a one-line hex file
and the ledger existed nowhere on disk. Foreground 'podman run' makes
the ledger land in the log file as documented; exit-code and ExecStop
semantics are unchanged. Test seam updated for both.
2026-10-06 00:26:28 -07:00
Frank 8343397584 frank-primary: merged PLE file must sort before the raw shards
The reader opens every index-named file in sorted order and emplaces
EVERY tensor each file physically holds, first-wins (checkpoint.cpp
add_shard + shard_list). The raw model-000NN shards carry tensors named
...ple.ple_embedding.ngram_embedding.shard_0.weight ([2500012,160] fp8)
- the identical name the plugin asks the merged table under - so
ple.merged.safetensors lost the emplace race and the plan read one raw
shard as the codes (declared [320001536,160] vs codes [2500012,160]).
Rename to 00-ple.merged.safetensors: '00-' wins the sort, the merged
bf16 tensor wins the name, table_bf16 declares and the rtn rule makes
the fp8 container form.
2026-10-06 00:21:40 -07:00
Frank 5e166543fa docs: record the two-slot no-spec frank-small shape (measured 1,397 MiB free, 48.0 t/s)
Runbook reshaped: 2 x 131072 no-spec identity, slot-2 arithmetic (drafter ~0.4 GiB + ubatch ~0.3 GiB bought the second 4,352 MiB KV slot), ngram-mod skipped on the recurrent-rollback law, MTP references kept (44.8-52.1% acceptance, 58.9-66.9 t/s), rollback = one revert of 812d9eb. AGENTS fleet row + section, LLM/index.md intro + entry, root index entry all flipped to the 2026-10-06 shape. 360/360 suite green; e2e gate 33/33 green at deploy.
2026-10-06 00:15:55 -07:00
Frank 812d9ebff5 frank-small: two 128k slots, no spec, ubatch 512 (Frank's tight-fit order, 2026-10-06)
CTX 262144 / PARALLEL 2 = two 131,072 conversations; SPEC_TYPE=none strips both speculative flags in the serve script (ngram-mod skipped on purpose — on a qwen35 hybrid it disables recurrent-state rollback); ubatch 1024->512 claws back ~0.3 GiB. Budget: non-KV ~15.5 GiB + two q8_0 slots 8,704 MiB lands AT the card — the 2-3 GiB floor is knowingly violated by explicit order; nvidia-smi free at boot is the measured truth. Seam retargeted: shape (2 slots, 131072/slot), budget (two fit, three don't), spec-off (absence of both flags), unit Description.
2026-10-06 00:07:03 -07:00
Frank fb1a4fd150 frank-primary: never write the stage index through the raw symlink
Run 3 rewrote the staged index THROUGH the relative symlink into the raw
checkpoint: the raw index.json was restamped with the stacked layout
(605 KB smaller), the raw verify then failed 143/144 and the overnight
run died before stacking. Two fixes:

- stack-qwen38-official-fp8.py: index/config writes are now temp+os.replace
  (an atomic swap replaces the symlink itself; the raw tree stays pristine).
- build-frank-primary-rad.sh: the stage's index.json is COPIED like
  config.json; only shards stay relative symlinks.
2026-10-06 00:03:58 -07:00
Frank 90a08ab4b5 comfyui: two proven Qwen-Image 2.1 workflows (art + legible text) + payload docs and next-model recommendations 2026-10-05 23:59:29 -07:00
Frank 272acb4c7f frank-primary: two stacking fixes from the first live run
- release the numpy frombuffer view before closing the source mmap
  (Python 3.14 BufferError: cannot close exported pointers exist).
- stacked scale planes rename: checkpoint.cpp scale_name() appends
  _scale_inv to the STACKED tensor name verbatim (no .weight segment) -
  ...experts.gate_up_proj_scale_inv, not ...gate_up_proj.weight_scale_inv.
2026-10-05 23:56:36 -07:00
Frank 13112a93f5 frank-small: record measured cutover values; unit Description follows the Swift swap
Boot ledger lines, 19,404 used / 4,721 MiB free (the q8_0 proof), decode arms (66.1 t/s @0.8, 66.9 @0, 58.9 @recipe 1.0 with acceptance 52.1%/52.2%/44.8%), vision Red probe, reasoning_effort xhigh/high contract, e2e 12-crawl transient + full-gate green rerun. Unit Description updated in-repo; the relinked unit picks it up at the next deploy/restart (no bounce for cosmetics). 360/360 suite green.
2026-10-05 23:41:05 -07:00
Frank baf33d016f frank-small: Swift-1.5 IQ3_S-mtp swap (parallel session) + fleet docs index
The parallel session's uncommitted swap, committed at Frank's direction
(commit whatever's in the working tree):
- llama.cpp master on the RTX 3090 now serves ukisai's
  Swift-1.5-Qwen3.8-27B GSQ-RCO IQ3_S WITH the in-file MTP head, ONE
  131,072-token slot at Frank's 128k cap, q8_0 K + q8_0 V, vision through
  the base-model projector; MiMo-V2.6 Q6_K (2x200704 f16 KV) retired to
  rollback target.
- env/serve/download + seam twins updated together (deployer payload gates
  rode the previous commit's deploy-remote.sh).
- docs: new swift15 runbook; MiMo Q6K runbook retired; AGENTS.md topology
  and both vault indexes follow (root index ACTIVE/RETIRED flip, test 135
  seam green).
- comfyui: two qwen-image t2i workflow drops under dankclaw/comfyui/
  workflows/ (12 KiB).
2026-10-05 23:26:59 -07:00
Frank 1919ad7f31 frank-primary: radiance native-FP8 TP=2 cutover (engine, payload build, rollback)
Replace the FreeToken PR587 endpoint (single-GPU: its TP=2 dies in the
pynccl.cu ROCm JIT) with the radiance engine on both R9700s, TP=2 via
libr4d's fused one-shot all-reduce. Official Qwen/Qwen3.8-Flash-Next-FP8
payload at native block-FP8 precision:

- scripts/build-frank-primary-rad.sh: staging (quantization_config drop =
  plugin SELECTION law) -> checkpoint reshape -> rad-convert with ONE
  --quant rule (PLE rtn fixed-scale); plan-only first.
- scripts/stack-qwen38-official-fp8.py: the two value-preserving reshapes
  (experts byte-stacked into the plugin's stacked layout; PLE's 128
  scaleless fp8 shards decoded to one bf16 shard with the checkpoint's own
  scale; the rtn rule re-encodes at that same scale - codes round-trip
  exactly and land the one-element scale plane the plugin declares).
- scripts/hfget.sh + verified download (131/131 shards sha256-match).
- froggeric v22.5 chat template vendored (--override-chat-template).
- serve script (pinned ledger: 2x40 GiB pools + 47.7 GiB PLE + prefix host),
  TTM preflight fixed for kernels with pages_limit only (no pages_max -
  an absent knob is skipped, not read as zero); pages_limit raised to
  52,428,800 pages = 200 GiB on the host pre-cutover.
- root unit, relinked; deployer gates image digest + .rad floor 158,000 MiB
  + health; prometheus re-adds the /metrics job.
- rollback: tag frank-primary-freetoken-pr587-20261005 (FreeToken trio +
  Dockerfile); payload + image stay on disk until the window closes.

deploy-remote.sh also carries the parallel session's frank-small payload
gates (swift15) - committed here per Frank's direction to commit the tree.
2026-10-05 23:26:51 -07:00
109 changed files with 5128 additions and 1929 deletions

No files matched your search

+288 -212
View File
@@ -12,8 +12,9 @@ merge absorbed the former `vllmScripts` repo, history preserved).
Calibre-Web, Jellyfin, Plex) plus the `frank-primary` LLM service.
- **Dankclaw** (192.168.0.143) — AI/search/dev/observability stack (SearXNG
via Gluetun, ComfyUI, OpenWebUI, Hermes, Gitea, Grafana, Prometheus, Loki,
Alloy, Pi-hole) plus TWO LLM services: `frank-large` (halogen container on
the gfx1151 iGPU) and `frank-small` (llama.cpp CUDA on the RTX 3090). Caddy on
Alloy, Pi-hole) plus TWO LLM services: `frank-large` (flash-server
container on the gfx1151 iGPU) and `frank-secondary` (llama.cpp CUDA on the
RTX 3090). Caddy on
Dankclaw reverse-proxies both hosts.
- **FrankBook-Pro** (192.168.0.112) — macOS mini running the *arr stack
(Prowlarr, Sonarr, Radarr, Lidarr, Readarr, qBittorrent) as quadlets inside
@@ -24,9 +25,9 @@ merge absorbed the former `vllmScripts` repo, history preserved).
| Target | Service | Runtime | Model | Port |
|--------|---------|---------|-------|------|
| Dankserver | `frank-primary` | **FreeToken PR #587** (branch `rocm714_torch214`, head `77a7201`; local image `frank-primary-freetoken:pr587-77a7201` built on the host from `scripts/docker/frank-primary-freetoken.Dockerfile` — digest-pinned rocm/pytorch rocm7.14 torch-2.11.0 base, plain `podman run`, root systemd unit) | `orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8` (~172.8 GiB, 131 shards, modelopt block-FP8, loaded straight from safetensors), **ONE R9700 (second card idle)**, all experts in **pinned host RAM** (~120 GiB) behind a GPU LRU (`--moe-cache-rate 0.10`), PLE table 47.7 GiB pinned (root unit + `LimitMEMLOCK=infinity`), **KV pool `--num-tokens 299968`** (24 KiB/token, multiple-of-64 law of the `qsa_sparse` auto backend), 2 slots, `--max-output-tokens 36864`, **parsers AUTO** (thinking → `reasoning_content`, tools qwen3.8 dialect), vision tower host-streamed. Measured: boot ~2.3 min cold / 92 s warm, decode **17.1 tok/s** single-stream; **TP=2 does not boot on ROCm** (pynccl JIT); `/metrics` 404 (Prometheus scrape reads down). Deploy gate reads the `/health` BODY (`status:"ok"`) — FreeToken 200s while loading. Rollback tag `frank-primary-vllm-shapeG-20261003` |7770|
| Dankclaw | `frank-large` | **halogen container** (`ghcr.io/peonist-ai/halogen@sha256:dc0a39a0…`, digest-pinned, plain `podman run` via `scripts/serve-frank-large.sh`, root systemd unit) | `peonist-ai/halogen-qwen3.8-27b` **`qwen3.8-27b-p1w4d-d2.hgn`** (35,865,565,184 B; the `.hgn` format loads in NO other engine — llama.cpp, vLLM and transformers all refuse it), **DFlash2 spec decode (`HALOGEN_DRAFTER=2`)**, **1 slot × 200,700 ctx** — `HALOGEN_KV_SLOTS=1` is load-bearing: batching XOR speculation, slots>1 silently disables the drafter (measured: 8 slots at serial ~6 t/s/stream), d2 KV (2× fp8, 64 KiB/token → 12.2 GiB pool, engine sizes it and prints a boot ledger naming any clamp), **TEXT-ONLY** (the engine ignores the vision encoder — vision is frank-small's job), **censored base** (the ROCmFPX AGENT rollback is the refusal escape hatch), **no server-side sampler: absent temperature is GREEDY**, so the card's thinking recipe (temp 1.0 / top_p 0.95) rides the wire in `models.yml`. Publisher card 31.7 t/s mean across eight text workloads vs the retired ROCmFPX endpoint's measured 27.6–28.1. Prompt over the slot AND `max_tokens` over `HALOGEN_MAX_TOKENS_CAP` are both 400s naming the limit, never truncations. Serve passes NO `--ulimit nofile` (rootless runc cannot raise Dankclaw's 524288 hard limit — see Key Lessons). Cutover 2026-10-05|7770|
| Dankclaw | `frank-small` | llama.cpp master, CUDA sm_86 build (root systemd unit, relinked) | bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K** (9B dense qwen3_5 hybrid, 7.26 GiB GGUF, no MTP weights so no spec decode) + the repo's f16 mmproj for **vision**, on the RTX 3090, **2 × 200,704** (pool 401,408 — NOT the native window) with **fully unquantized KV (f16 K + f16 V)** under flash attention, **batch 4096 / ubatch 2048** (doubled 2026-10-05: measured +4.6 % prefill at 8,928 tokens for 616 MiB), 21,846 used / **2,279 free** of 24,576 MiB, 94.4 tok/s decode. Resized 2026-10-03 (3 × 180,224 + q8_0 V → 2 × 200,704 + f16 V → + Q6_K rung: the freed slots paid for exact values AND the higher quant; the rung costs a measured −10 % decode — weight-bandwidth-bound); rollback `MODEL_FILE=…/MiMo-Q5KM/…Q5_K_M.gguf` (buys the 10 % back) or `CTX_SIZE=540672 PARALLEL=3 CACHE_TYPE_V=q8_0` (Q5 only) | 7771 |
| Dankserver | `frank-primary` | **radiance** (StillDeadcode v1.0.7 line; image `stilldeadcode/radiance@sha256:4c3f484564…` digest-pinned INDEX digest, 287 MiB, `libr4d` gfx1201 kernels, plain `podman run`, root systemd unit) | `Qwen/Qwen3.8-Flash-Next-FP8` official (172.8 GB, 131 shards, sha256-verified) converted **no-recipe** to `qwen4exp-fp8-native.rad` (177,109 MiB measured = 173.0 GiB) — **native modelopt block-FP8, no requantisation** — on **BOTH R9700s TP=2** (`--tp-wire wht6`), experts in a **40 GiB/rank pinned host pool** + VRAM cache (`--placement expert_tiered`, `--expert-vs-cache-ratio 0.82` = the 99 %-hit point), **PLE 47.7 GiB pinned to host RAM** (`--ngram-placement ram`; root unit + `LimitMEMLOCK=infinity`), **fp8 KV 7.32 KiB/token**, **MTP spec decode depth 3**, *…
| Dankclaw | `frank-large` | **halogen-flash-server 0.16.2** (`ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`, digest-pinned, plain `podman run` via `scripts/serve-frank-large.sh`, root systemd unit) | Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated **`qwen38-flash-next-v2-swift15-abliterated.hgn`** (66,687,678,464 B = compacted r2 of the card's 89,775,868,032 B original, ~62 GiB resident; the v2 `.hgn` format loads in NO other engine), **+ REQUIRED `qwen38-flash-next-ngram.hgn`** (51,200,246,144 B, peonist @5cc17cea — read beside the checkpoint) **+ vision tower** (897,916,416 B, @e053f488 — **vision ON**, `image_url` parts only, ≥256 px probes), **MTP spec decode ALWAYS-ON (`HALOGEN_SPEC_ADAPT=0` — the adaptive default disables the head per request and HALVES decode)**, MTP head INSIDE the checkpoint (`HALOGEN_MTP_HEAD` unset; no sidecar fetched), **2 slots × 200,192 ctx** (KV pool 400,384 positions ≈ 10.7 GiB at ~28 KiB/position; the 27B slots>1-disables-drafting law does NOT carry over — batching and speculation coexist, the card's quickstart ships 2 with the head), **server-side sampler 1.0/0.95/20** (card recipe; models.yml extraBody matches), coupled cap/timeout 65536/7200, prompt cache ON by default (warm 100k turn ≈ 2 s vs ≈ 88 s cold) | 7770 |
| Dankclaw | `frank-secondary` | llama.cpp master (0.5.0-dev, commit `5fc4f3c`), CUDA sm_86 build (root systemd unit, relinked) | ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF **RCO-IQ3_XXS -mtp** (27B dense qwen3_5 hybrid, 10,442,827,392 B; a per-tensor allocation mix of stock types; the MTP head in the file **drafted** — **spec ON** (draft-mtp depth 3) since 2026-10-06 evening, ngram skipped on the recurrent-rollback law) + the publisher's own **BF16 mmproj** for **vision**, on the RTX 3090, **2 × 114,688** (Frank's 112k cap each since the 2026-10-06 late reshape, cut from 131,072 to fund the **ubatch ladder 512 → 1024 → 2048** — **1,005 MiB free at boot, 763 settled after a vision probe** (encoder buffers resident)) with **q8_0 K + q8_0 V (FP8 class, 34 KiB/token)** under flash attention, batch 4096 / **ubatch 2048** (the ceiling for this shape — 4096 would not fit); **prefill 1,209 t/s @ 3,760 tokens; ~52–58 t/s recipe decode** (MTP acceptance 34–54 %, temp-1.0 noise; decode is ubatch-invariant). Renamed from `frank-small` 2026-10-06 (87721ab). Supersedes the same-morning IQ3_S-mtp payloads: the two-slot no-spec shape and the one-slot MTP shape (both 2026-10-06; payload on disk).|7771|
Retired endpoints (`frank-npu`, and every prior runtime) are in
the [runbook index](#runbook-index). The full swap history lives in
@@ -72,7 +73,7 @@ All edits flow through git. Never edit files on a remote machine directly.
The planner maps changed paths to services; the remote runner restarts only
the selected service and records its revision outside the checkout.
Documentation-only changes are a no-op. LLM services are selected only by
their own files — every endpoint (`frank-large`/`frank-small`/`frank-primary`)
their own files — every endpoint (`frank-large`/`frank-secondary`/`frank-primary`)
by its unit/env/serve trio in `scripts/` — so
unrelated
container changes cannot interrupt inference. Set `FRANK_SUDO_PW` for headless
@@ -228,13 +229,16 @@ archive.
**STOP the production service BEFORE benchmarking** — these servers serve
real users, and a bench beside a live service starves the GPU and corrupts
results. Use the runtime's own harness: `llama-bench` for the llama.cpp
endpoints; a live llama.cpp server also reports honest per-request numbers in the `/completion` `timings` object (prefill/decode rates, `draft_n`, `draft_n_accepted`), which is how frank-large and frank-small were measured. Rolling your own bench
endpoints; a live llama.cpp server also reports honest per-request numbers in the `/completion` `timings` object (prefill/decode rates, `draft_n`, `draft_n_accepted`), which is how frank-large and frank-secondary were measured. Rolling your own bench
requires asking first.
Scratch ports are not used on LLM hosts — a
test process cannot share the single GPU anyway; validate against the service
itself.
(A gufo-era law; frank-large ran ROCmFPX llama.cpp 2026-10-02 → 10-05 and is the halogen container since, but the rollback tags keep it live.) **`gufo bench` cannot rank speculative-decode settings, and will invert the ranking.**
(A gufo-era law; frank-large ran ROCmFPX llama.cpp 2026-10-02 → 10-05, the
halogen 27B container 2026-10-05 → 10-06, and is flash-server since, but the
rollback tags keep it live.) **`gufo bench` cannot rank speculative-decode
settings, and will invert the ranking.**
Measured 2026-09-28 on frank-large at depth 32768: its corpus
reports `acceptance=0.933 ± 0.000` with an **identical trace sha256** across
draft widths, i.e. the same highly-predictable text every run, and 93 % of
@@ -262,9 +266,9 @@ merely wedged kernel usually still answers ARP.
1. Stop and mask (self-healing would restart it):
- Dankclaw `frank-large` (root unit, halogen podman on the iGPU):
`sudo systemctl stop frank-large && sudo systemctl mask frank-large`
- Dankclaw `frank-small` (root unit, llama.cpp on the 3090):
`sudo systemctl stop frank-small && sudo systemctl mask frank-small`
- Dankserver `frank-primary` (root unit, FreeToken on one R9700):
- Dankclaw `frank-secondary` (root unit, llama.cpp on the 3090):
`sudo systemctl stop frank-secondary && sudo systemctl mask frank-secondary`
- Dankserver `frank-primary` (root unit, radiance on both R9700s, TP=2):
`sudo systemctl stop frank-primary && sudo systemctl mask frank-primary`
2. Bench.
NOTE: `systemctl mask` FAILS on a relinked unit (the unit file in
@@ -283,7 +287,7 @@ The harness reaches the fleet through `.omp/agent/models.yml` in the
operator's home — `/home/blank/.omp/agent/models.yml` on the dev box, which is
where the harness runs; there is no copy on the servers. Each provider is one
HOST:PORT; each model `id` is the served alias: `frank-primary` →
`192.168.0.235:7770`, `frank-large` → `192.168.0.143:7770`, `frank-small` →
`192.168.0.235:7770`, `frank-large` → `192.168.0.143:7770`, `frank-secondary` →
`modelRoles`.
Resolution rules: the default model resolves as a `defaultProvider` +
@@ -296,17 +300,19 @@ When renaming a service, retiring an endpoint, or moving a model: update the
provider block, model `id`, `contextWindow`, `maxTokens`, `input` in
`models.yml`, then fix every `modelRoles` entry that named the old endpoint —
a role pointing at a provider the file does not define fails at call time. As
of 2026-10-05 **two of the three endpoints accept images** (`input: [text,
image]`) — `frank-large` went TEXT-ONLY with the halogen swap (the engine
ignores the vision encoder; the BF16-mmproj era ended 2026-10-05) — while
`frank-primary` takes them natively (FreeToken builds the qwen3.8 vision
tower and streams its block weights from host RAM two at a time by default),
and `frank-small` through
`mmproj-MiMo-V2.6-Distill-Qwen-9B-f16.gguf` passed as `--mmproj` — the MiMo
GGUF itself carries no vision tensors. `frank-small`'s `contextWindow` is
**200704** (resliced from 180224 on 2026-10-03), the per-slot window, not the
model's native 262144: the harness must compact before llama.cpp answers a
longer prompt with a 400. Note also
of 2026-10-06 **all three endpoints accept images** (`input: [text,
image]`) — `frank-large` returned to vision with the flash-server v2 swap
(the v2 vision tower loads beside the checkpoint; the text-only halogen
27B window closed 2026-10-06) — while
`frank-primary` takes them natively (radiance runs the checkpoint's
27-layer Qwen-VL tower — patching, pad-token expansion and 3-part rotary
are the engine core; `--mm-max-patches` available),
and `frank-secondary` through
the publisher's own `mmproj-BF16.gguf` passed as `--mmproj` — the Swift
GGUF itself carries no vision tensors. `frank-secondary`'s `contextWindow` is
**114688** (cut from 131072 on 2026-10-06 late to fund the ubatch ladder to 2048), the
per-slot window, not the model's native 262144: the harness must compact
before llama.cpp answers a longer prompt with a 400. Note also
that
the reasoning channel differs by runtime: llama.cpp AND SGLang (qwen3
parser, live-verified) emit `reasoning_content`, vLLM emits `reasoning` —
@@ -317,193 +323,259 @@ endpoint on 2026-09-11, returned 2026-09-17 with the Gemma4 projector's
audio encoder, and left again with the Gemma4 retirement on 2026-10-01
(the MiMo tower is vision-only).
### frank-large — halogen container, Qwen3.8-27B + DFlash2 (Dankclaw, ACTIVE)
### frank-large — halogen-flash-server 0.16.2, Qwen3.8-Flash-Next v2 Swift1.5 ABLITERATED (Dankclaw, ACTIVE)
The **halogen engine** (`ghcr.io/peonist-ai/halogen@sha256:dc0a39a0…`,
digest-pinned, published tag 0.1.4) as a podman container driven by
`scripts/serve-frank-large.sh` under a **root systemd unit**
(`scripts/frank-large.service`, `User=dank`, `Restart=always`, `ExecStop=podman
stop -t 120 frank-large-halogen`, the script `exec podman wait` so the restart
policy is real) on port 7770 from the Strix Halo iGPU. Engine stdout goes to
`/home/dank/logs/frank_large.log` — **the boot ledger (KV pool, drafter state,
any clamp) lives there, not in the journal**. Serving
`peonist-ai/halogen-qwen3.8-27b`: `qwen3.8-27b-p1w4d-d2.hgn` (35,865,565,184 B
≈ 33.4 GiB, sha256 `274c3fc7…`, 1352 tensors; p1 weights / w4 activations /
d2 KV) + the flat `tokenizer/` directory (5 files). Cutover 2026-10-05 from the ROCmFPX llama.cpp fork
([[frank-large-rocmfpx-q6agent-uncensored-mtp]], measured 27.6–28.1 t/s prose),
chosen for the publisher's 31.7 t/s DFlash2 mean.
The **flash-server engine** (`ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`,
digest-pinned, published tag 0.16.2, 3.6 GB image) as a podman container
driven by `scripts/serve-frank-large.sh` under a **root systemd unit**
(`scripts/frank-large.service`, `User=dank`, `Restart=always`,
`ExecStop=podman stop -t 60 frank-large-halogen`, the script runs `podman
run` FOREGROUND — the container's exit code is the service's exit so
`Restart=always` stays real, and the engine ledger lands on disk) on port
7770 from the Strix Halo iGPU. Engine stdout goes to
`/home/dank/logs/frank_large.log` — **the boot ledger (weights resident, KV
pool, MTP head, prompt cache, any clamp) lives there, not in the journal**.
Cutover 2026-10-06 from the halogen 27B-dense era
([[frank-large-halogen27b-dflash2]], retired the same window).
**The format is the lock.** `.hgn` loads in nothing else — the runtime is
chosen by the FILE, as ROCmFPX's tensor types were before it. This is NOT the
2026-09 `halogen-flash-server` image (different repo, Flash-Next w4b era); the
halogen-server repo restarted versioning at 0.1.x.
Serving `Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated`:
`qwen38-flash-next-v2-swift15-abliterated.hgn` (66,687,678,464 B ≈ 62.1
GiB, sha256 `9c880b8b…` — a compacted r2 of the card's 89,775,868,032 B
append-only original; the abliteration workflow appends and the repo's
tooling drops dead padding, live tensors byte-identical) + the **REQUIRED**
n-gram lookup table `qwen38-flash-next-ngram.hgn` (51,200,246,144 B,
peonist @5cc17cea) + the vision tower `qwen38-flash-next-vision.hgn`
(897,916,416 B, @e053f488) + the flat `tokenizer/` directory (6 files,
fetched fresh from peonist). Cutover 2026-10-06 from the halogen
27B-dense shape, chosen by Frank for the ABLITERATED base.
**Two product regressions, both accepted by Frank before deploy:** the
endpoint is **text-only** (README honest limits: the vision encoder is
ignored — frank-small owns images now) and the checkpoint is a finetune of the
**censored** `Qwen/Qwen3.8-27B` base (refusals can return; the rollback is one
commit-revert away with the AGENT payload still on disk).
**The format is the lock.** The v2 `.hgn` loads ONLY in the
flash-server line — the 27B-era halogen 0.1.4 container refuses it, so the
engine moved with the model. The MTP head is INSIDE the checkpoint
(`HALOGEN_MTP_HEAD` stays unset; the separate `qwen38-flash-next-mtp.hgn`
sidecar is deliberately not fetched).
**Engine laws that shape the config** (all in `scripts/frank-large.env` with
evidence): `HALOGEN_KV_SLOTS=1` — batch XOR speculation, slots>1 turns the
drafter off silently. `HALOGEN_SLOT_CTX=200700` — the per-conversation HARD
ceiling (400 naming the limit, never truncation); `models.yml`
`contextWindow: 200700` must match or the harness discovers the wall as a 400.
`HALOGEN_MAX_TOKENS_CAP=65536` + `HALOGEN_QUEUE_TIMEOUT=7200` are a coupled
pair; the harness `maxTokens: 57344` stays under the cap (over-cap is a 400
naming the limit — README, never a 413). Thinking tokens COUNT against the
budget: exhausted mid-thought is `finish_reason: "length"` with empty
`content` and the reasoning in `reasoning_content`.
`HALOGEN_DRAFTER=2` = DFlash2. **No server-side sampler**: an absent
temperature is greedy, so the thinking recipe exists only in `models.yml`
`extraBody`. `response_format` is unsupported (no constrained decoding).
Thinking lands in **`reasoning_content`** (harness field unchanged); the same
Qwen3.8 template rejects `reasoning_effort` outside `xhigh|medium|low`, so the
`models.yml` fold stays mandatory.
**Product facts vs the 27B era, both regressions FIXED:** the base is
**abliterated** (the 27B was a censored-base finetune; refusals were an
accepted trade there) and **vision is ON** (the 27B engine silently
ignored its encoder — frank-secondary owned images for exactly one day at this
endpoint).
Fetch: `scripts/download-frank-large-halogen.sh` into
`/home/dank/models/halogen-qwen3.8-27b/` (`-f` + byte count + LFS sha256 +
`--verify-only`, no token on the command line; resume goes against the
RESOLVED CDN URL because curl drops `Range` across HF's 302). The deployer
and serve script both refuse a missing tokenizer file or **any symlink** —
an HF cache snapshot symlinks each entry into `blobs/` and those links
dangle inside the container mount.
**Engine laws that shape the config** (all in `scripts/frank-large.env`
with evidence): `HALOGEN_SPEC_ADAPT=0` is MANDATORY — the default adaptive
policy disables the MTP head for whole requests after low acceptance and
HALVES decode; the head stays on and temp-1.0 traffic pays some acceptance
for it (accepted). `HALOGEN_CTX=200192` is the PER-SLOT hard ceiling (400
naming the limit, never truncation); `models.yml` `contextWindow: 200192`
must match, and compaction at 70 % = 140,134 + 57,344 budget = 197,478 fits.
`HALOGEN_KV_SLOTS=2` is LEGAL here — the 27B "slots>1 disables the drafter"
law does NOT carry over; batching and speculation coexist (the card's
quickstart ships 2 slots with the head), so 2 is concurrent capacity.
`HALOGEN_KV_POOL_POSITIONS=400384` (explicit 2 × ctx; ~28 KiB/position ⇒
~10.7 GiB). `HALOGEN_MAX_TOK=8192` is only the per-request DEFAULT (the
harness always sends 57,344); `HALOGEN_MAX_TOKENS_CAP=65536` +
`HALOGEN_QUEUE_TIMEOUT=7200` are a coupled pair (FLAGS.md table).
**A server-side sampler EXISTS** (`HALOGEN_TEMPERATURE/TOP_P/TOP_K` =
1.0/0.95/20, the card's thinking recipe) — an absent temperature is no
longer greedy; `models.yml` extraBody carries the same values, consistent
not load-bearing. Prompt cache is ON by default (`PROMPT_CACHE=2`): a warm
100k follow-up costs ~2 s vs ~88 s cold — the 27B engine's manual
`HALOGEN_CACHE_MB` pin era is over. `response_format` is unsupported.
Thinking lands in **`reasoning_content`**; the v2 chat template rejects
`reasoning_effort` outside `xhigh|medium|low`, so the `models.yml` fold
stays mandatory. Thinking tokens COUNT against the budget (empty content +
`finish_reason: "length"` when exhausted mid-thought). Vision: OpenAI
`image_url` content parts only; ≥256 px probe images or the model answers
"video".
Runbook: `serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md`.
Rollback: revert the swap commit and redeploy — the ROCmFPX payload
(`/home/dank/models/kingjones-Qwen3.8-27B-ROCmFP4-STRIX-MTP/`), the fork build
and `scripts/build-rocmfpx.sh` (pin `36e9acd`) all remain on disk, seam-tested.
The older gufo tag `frank-large-gufo-q4kxl-20261001` stays a **re-download**
rollback (payloads removed 2026-10-03; `scripts/build-gufo.sh` stays).
**Memory ledger** (121.2 GiB unified; the referee is the boot ledger):
weights ~62 + KV ~10.7 + arena ~8 (`3.8 + 8.4 × MAX_TOK/16384`) + vision
~2 ≈ 82.7 GiB engine-side + `HALOGEN_HOST_RESERVE_GIB=20` → ~18 GiB
margin over baseline co-tenancy. The 51.2 GB n-gram table rides as mmap'd
page cache beside the checkpoint — the card's quickstart arithmetic does
not bill it as resident, but READ the boot ledger and re-size if the
engine prints a clamp.
### frank-small — llama.cpp on the RTX 3090 (Dankclaw, ACTIVE)
Fetch: `scripts/download-frank-large-flashnext.sh` into
`/home/dank/models/halogen-flashnext-v2/` (`-f` + exact byte counts + LFS
sha256 + `--verify-only`, no token on the command line; resume goes
against the RESOLVED CDN URL because curl drops `Range` across HF's 302).
The serve gates mirror the deployer EXACTLY (byte-exact checkpoint +
n-gram + vision tower, flat tokenizer with all six files as regular
files) — the 2026-10-01 crash-loop lesson.
**llama.cpp master** (CUDA sm_86 build
`/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server`) as a
**root systemd unit** (`scripts/frank-small.service`, `relink_unit`ed into
`/etc/systemd/system/`), serving
`bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF` at **Q6_K** (7,793,710,624 B,
sha256 `ef96d05a…`) **plus `mmproj-MiMo-V2.6-Distill-Qwen-9B-f16.gguf`**
(918,166,048 B, sha256 `ff348f…`), both `/home/dank/models/MiMo-Q6K/` (the
projector is a hardlink of the Q5KM copy — the dir is self-contained), on
port 7771. The rung went Q5_K_M → Q6_K on 2026-10-03 the same morning as the
reslice, funded by the freed slots. The runtime arrived 2026-10-01 night for
one reason:
**asymmetric KV** — `--cache-type-k f16` + `--cache-type-v q8_0` is the fleet's
only per-tensor KV lever, and SGLang's `--kv-cache-dtype` moves K and V
together. On 2026-10-03 Frank spent the headroom that lever bought: the reslice
from three slots to two made room for **fully unquantized V**
(`CACHE_TYPE_V=f16`); both flags stay explicit in the serve script. Vision came
2026-10-02 via `--mmproj`: the GGUF carries no `v.*` tensors, so the projector
file is the entire vision path (and ~1.4 GiB of the card).
Runbook:
`serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md`.
Rollback: revert the swap commit and redeploy — the 27B payload
(`/home/dank/models/halogen-qwen3.8-27b/`), its fetch script and the
halogen 0.1.4 image digest all remain on disk; the ROCmFPX door behind
that (`scripts/build-rocmfpx.sh`, pin `36e9acd`) is untouched.
**Architecture: qwen3_5 hybrid** — 32 layers, 8 full-attention + 24 Gated
DeltaNet, native 262,144 window. KV exists only in the 8 attention layers
(4 KV heads × 256 dim × 2 B ≈ 2 KiB/token/layer, ~16 KiB/token for f16 K+V);
the DeltaNet state is a fixed per-slot buffer, so slot count — not context —
drives that term.
### frank-secondary — llama.cpp on the RTX 3090 (Dankclaw, ACTIVE)
**The shape (2026-10-03):** `--ctx-size 401408 --parallel 2` = **TWO
conversations of 200,704 tokens each** (llama.cpp divides the pool:
`n_ctx_slot = CTX_SIZE / PARALLEL`), `--flash-attn on` (the seam pins it),
**f16 K + f16 V**, all layers on the GPU, `--mmproj` for vision, and since
2026-10-05 **batch 4096 / ubatch 2048** (doubled; the boot-pair A/B is in the
runbook). Measured trade at 8,928 prompt tokens: prefill 4,024 → **4,209 t/s,
+4.6 %**, decode flat (91.9–93.0 vs 93.8–94.3 — noise), and a 279-token prompt,
which sits below either ubatch, did not move at all. The old "ubatch 2048
failed the scratch boot" note belongs to the Q5_K_M predecessor and does NOT
generalize: this shape boots 2048 clean. Measured VRAM **21,846 used / 2,279
free** of 24,576 MiB against **21,276 / 2,849** for the 1024 arm, reproduced
byte-identically the same hour — the doubling costs **616 MiB** and lands at
the BOTTOM of the 2–3 GiB floor Frank asked for (`4096/1536` is the middle rung
if that margin is ever wanted; non-KV 9,302 MiB, up from 8,732 by exactly the
compute buffers; f16 K+V is 32 KiB/token across the 8 attention layers;
quote `nvidia-smi`'s `free` field, never `total − used` — the driver holds
~450 MiB of reserve the subtraction hides). The 2026-10-02 3 × 180,224 +
q8_0-V shape stays one env edit away (`CTX_SIZE=540672 PARALLEL=3
CACHE_TYPE_V=q8_0`, and it must pair with Q5_K_M — Q6_K breaks the floor
there). Three native windows need 18.4 GiB of KV alone and were never on the
table.
**llama.cpp master** (0.5.0-dev, commit `5fc4f3c`; CUDA sm_86 build
`/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server`,
`GGML_CUDA_FA_ALL_QUANTS=ON`) as a **root systemd unit**
(`scripts/frank-secondary.service`, `relink_unit`ed into
`/etc/systemd/system/`), serving `ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF`
at **RCO-IQ3_XXS -mtp** (10,442,827,392 B, sha256 `e13d3e74…`) **plus
the publisher's own `mmproj-BF16.gguf`** (931,145,888 B, sha256
`19acd7fc…`), both `/home/dank/models/Swift15-UncIQ3XXS/`, on port 7771.
Cutover 2026-10-05 (evening) on Frank's explicit contract: **128k
context cap, KV at FP8, vision on, as many 128k contexts as fit**;
reshaped three times on 2026-10-06 — morning to two 131,072 slots
no-spec, evening to this UNCERSED RCO-IQ3_XXS payload with MTP back
on, late the RENAME to **frank-secondary** (87721ab) at two 114,688
slots with the ubatch ladder climbing 512 → 1024 → 2048 (f2575a1,
d8da7a9). The
same-day payloads it supersedes: the LIVE MiMo-V2.6 Q6_K shape and the
Ornith-1.5-9B Q8_0 swap that was committed (77decb5..380aafa) but never
deployed (blocked at the docs gate; the docs debt is what this section
settles).
Verified at deploy (2026-10-03): boot logs `n_slots = 2, n_ctx_slot = 200704`
and `loaded multimodal model`; decode **94.44 tok/s** at temp 0 on the probe
that measured 104.84 on Q5_K_M — **the Q6_K rung costs a measured −10 %**:
decode on this card is weight-bandwidth-bound and Q6_K streams ~10 % more
bytes/token; health 200, e2e 33/33; native `tool_calls`; thinking in
`reasoning_content`; a **512×512** solid-red PNG answers `Red` through an
`image_url` content part (two traps: the legacy top-level `images` field is
ignored, and a TINY probe image undersupplies image tokens — a 10×10 PNG made
the model claim "it's a video"; probe with ≥256 px). A prompt over the slot
is a **400 `exceed_context_size_error`**, which is why `models.yml` sets
`contextWindow: 200704`.
**Architecture: qwen3_5 hybrid, bigger** — block_count 65 (64 real layers +
the blk.64 MTP head), `full_attention_interval 4` ⇒ **16 full-attention +
48 Gated DeltaNet**, native 262,144 window, dense. KV exists only in the 16
attention layers (4 KV heads × 256 dim): 64 KiB/token at f16, **34 KiB/token
at q8_0**. The DeltaNet state is a fixed per-slot buffer (~150 MiB/slot at
48 linear layers × 48 value heads). Header facts were read from the Hub
file by HTTP range + struct parse before anything was downloaded.
Runbook: `serverAppConfigs-docs/LLM/frank-small-llama-mimo-q6k-2x200704.md`
(the boot-pair table that prices pool changes lives in the retired
[[frank-small-llama-mimo-q5km]]).
Rollback: `MODEL_FILE=/home/dank/models/MiMo-Q5KM/MiMo-V2.6-Distill-Qwen-9B-Q5_K_M.gguf`
buys back 875 MiB and the −10 % decode rate (3,571 free, 104.84 tok/s, both
measured); the older shape `CTX_SIZE=540672 PARALLEL=3 CACHE_TYPE_V=q8_0`
pairs with Q5 only; SGLang rollback in
[[frank-small-sglang-mimo-v26]] (venv on disk at
`/home/dank/GitPackages/sglang-cu128/`, INT8 payload staged to
`~dank/.AGENTTRASH/20261002-retired-llm-payloads/`).
**The shape (2026-10-06 midnight; Frank's tight-fit order, MTP back on
in the evening, per-slot cut + ubatch climbed to 2048):** `--ctx-size
229376 --parallel 2` = **TWO 114,688-token conversations** (Frank's
112k cap each; `n_ctx_slot = CTX_SIZE / PARALLEL`; cut from 131,072 to
fund the ubatch rungs), `--flash-attn on` pinned, **q8_0 K + q8_0 V**
(llama.cpp's FP8 class — no true fp8 KV dtype exists; 8.5 effective
bits; supersedes the same-day f16/f16 rule written for the Ornith 9B
fit), all layers on the GPU, `--mmproj` for vision, batch 4096 /
ubatch 2048. **Speculative decoding is ON** (draft-mtp, depth 3): the
-mtp file's head is drafted against the main model's own weights;
ngram-mod skipped on purpose — on a qwen35 hybrid ngram disables
recurrent-state rollback, so a rejected draft corrupts the DeltaNet
state.
### frank-primary — FreeToken (PR #587 ROCm), Qwen3.8-Flash-Next FP8 (Dankserver, ACTIVE)
**Measured at the midnight boot (2026-10-06, 2 × 114,688 / ubatch
2048): 23,120 used / 1,005 MiB free, settling 763 MiB after a vision
probe** (`nvidia-smi` free; the encoder buffers stay resident on this
boot) — the per-slot cut freed ~1.05 GiB of KV and both ubatch rungs
spent it back (~0.3 + ~0.75 GiB measured; the 1024 rung had landed
22,374/1,751, the 2048 rung cost 746 MiB, ~130 over the 616 the rung
measured on the MiMo shape). The MTP draft ctx costs **~2.4 GiB
measured** (23,636 − 22,728 + 1,561 at the 262,144 draft window) — ~6×
the ~0.4 GiB the one-slot era assumed, because the drafter's KV is
built against the target's FULL context, not the 3-token proposal
depth; the lighter RCO-IQ3_XXS quant (−1.56 GiB vs IQ3_S-mtp) funds it.
**Prefill 1,209 t/s @ 3,760 tokens; decode 52.0–58.2 tok/s (recipe
temp 1.0) / 66.9 (temp 0)**, acceptance 34.2–54.4 % — decode is
ubatch-invariant, the spread is temp-1.0 acceptance noise.
`reasoning_effort` probe unchanged: `xhigh` 200, `high` → 500 Jinja
raise (the models.yml fold stays mandatory).
The **FreeToken engine at PR #587 head `77a7201`** (branch
`rocm714_torch214`, built on the host into the local image
`localhost/frank-primary-freetoken:pr587-77a7201` by
`scripts/docker/frank-primary-freetoken.Dockerfile` — the PR's own
docs/install_amd.md stack: digest-pinned
`rocm/pytorch:rocm7.14_ubuntu24.04_py3.12_pytorch_release_2.11.0` base =
torch 2.11.0+rocm7.14/triton 3.7, `--no-build-isolation --no-deps -e .`,
curated portable deps, `flashlib==0.3.0 --no-deps`, and a `libhiprtc.so`
dev-symlink the pip-ROCm wheel lacks) serving
**`orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8`** (~172.8 GiB, 131 shards,
standard modelopt block-FP8 — 75,264 `weight_scale_inv` tensors, no
`input_scale`, loaded directly from HF safetensors, no FTW conversion) on
**ONE R9700 (gfx1201)** — the second card sits idle. Plain `podman run`
through `scripts/serve-frank-primary.sh` under the root systemd unit
**Vision with the publisher's own projector**: ajgazin's repo SHIPS
`mmproj-BF16.gguf` in the same repo (the IQ3_S era's unsloth mmproj was
built from the SHARED BASE `Qwen/Qwen3.8-27B`, legal because Swift-1.5's
vision stack was hash-proven byte-identical to the base). The probe
traps carry over: ≥256 px probe images or the model answers "video";
only the OpenAI `image_url` content part reaches the model. **New on
this payload: the uncensored model thinks more than the censored
Swift 1.5** — a 64-token budget is eaten entirely by
`reasoning_content` (empty `content`); 128 tokens clears the vision
probe.
The RCO-IQ3_XXS tier is a **per-tensor allocation mix** of stock quant
types (IQ3_S 97 / BF16 96 / IQ3_XXS 83 / IQ2_S 72 / IQ4_XS 33 / IQ2_XS 32
/ IQ2_XXS 30 / Q2_K 28 / Q4_K 19 / Q6_K 8 / IQ1_S 4 / IQ1_M 4) — stock
llama.cpp loads it; the tensor-type histogram is the law (see Key
Lessons). Publisher KLD vs Swift BF16: mean 0.002631 / 90th 0.042136 /
max 34.49 — RCO is the publisher's "more experimental" family, and the
payload is the UNCECERSED Swift 1.5 (23/100 refusals vs Swift 1.5's
98/100, KL 0.0884).
Runbook:
`serverAppConfigs-docs/LLM/frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md`.
Rollback: `/home/dank/models/Swift15-IQ3S/` (the IQ3_S-mtp payload live
until the 2026-10-06 evening swap — revert da36fa3 for the two-slot
no-spec shape) or `/home/dank/models/MiMo-Q6K/` (revert to the
2×200,704 f16 shape — [[frank-secondary-llama-mimo-q6k-2x200704]]) or
`/home/dank/models/Ornith-Q8/` (the committed-never-deployed Q8_0;
revert to 380aafa's tree).
### frank-primary — Radiance (StillDeadcode), Qwen3.8-Flash-Next FP8 native, TP=2 (Dankserver, ACTIVE)
The **radiance engine** (StillDeadcode, Apache-2.0; image
`stilldeadcode/radiance@sha256:4c3f484564bb2872784e4…` digest-pinned,
287 MiB, `libr4d@0.5.0` kernels prebuilt for gfx1201, engine as the image
entrypoint) serving the **official `Qwen/Qwen3.8-Flash-Next-FP8`
checkpoint at NATIVE precision** — modelopt block-FP8 throughout, no
requantisation, no calibration. Plain `podman run` through
`scripts/serve-frank-primary.sh` under the root systemd unit
(`frank-primary.service`, `User=dank`, `LimitMEMLOCK=infinity`); the
container name is the unit name (`frank-primary`; the vLLM-era
`frank-primary-qwen` is in the legacy cleanup list). Cutover 2026-10-05;
rollback tag `frank-primary-vllm-shapeG-20261003` (the tcclaviger vLLM trio
and its 362-line env lore live in that tag; the tcclaviger image stays in
dank's rootless storage).
container name is the unit name (`frank-primary`). Cutover LIVE 2026-10-06
00:55 (chain built 2026-10-05/06);
rollback tag `frank-primary-freetoken-pr587-20261005` (the FreeToken trio
+ Dockerfile are in the tag; the image and the orcarouter payload stay on
disk until the window closes), older rollback `frank-primary-vllm-shapeG-20261003`.
**Shape**: all expert weights in **pinned host RAM** (~120 GiB) behind a
GPU LRU (`--moe-cache-rate 0.10`, ~12 GiB — the single-card ceiling given
the KV contract), the ~47.7 GiB PLE ngram table pinned in host RAM
(→ root unit + memlock law), **KV pool `--num-tokens 299968`**
(24 KiB/token f16 across the 12 full-attention layers ≈ 7.0 GiB; **must be
a multiple of 64** — the auto attention backend is `qsa_sparse`, which
overrides page size to 64; 300,000 was refused at first boot), 2 decode
slots, `--max-output-tokens 36864` (the harness maxTokens; requests that
omit one get this default). **Parsers are AUTO**: thinking lands in
`reasoning_content` natively, tool calls in the qwen3.8 dialect — models.yml
flips `reasoningContentField` to `reasoning_content` on the harness side.
Vision works (models.yml keeps `input: [text, image]`); the vision tower's
blocks stream from host RAM two at a time by default.
**Payload chain**: official checkpoint → staging dir whose `config.json`
drops `quantization_config` → `stack-qwen38-official-fp8.py` (two
value-preserving reshapes: experts byte-stacked into the plugin's stacked
`gate_up_proj`/`down_proj` layout with `_scale_inv` at the same index — 49
stacks × 512 experts, no requantisation; PLE's 128 scaleless fp8 shards
decoded to one bf16 shard with the checkpoint's own bf16 scale
1.9931793212890625e-04) → `rad-convert` with ONE `--quant` rule — pure
copy for every weight, and the rtn rule re-encodes the bf16 PLE at that
same fixed scale, an exact round-trip that restores the original codes and
lands the plugin's one-element scale plane →
`/home/dank/models/rad/qwen4exp-fp8-native.rad` (177,109 MiB measured,
floor `FRANK_PRIMARY_RAD_MIN_MIB=177000`). The staging drop is the plugin
SELECTION law (spec §2.4): the descriptor is `(arch_id, quant)` matched
exactly, no shipped plugin claims `(qwen4exp, "fp8")`, the shipped
`qwen4exp_fp8` plugin claims `(qwen4exp, "")` — and the descriptor is only
the selection key, per-weight encodings are data, so nothing else is
requantised.
Raw serve AND unmodified convert both refuse; that is the design. The
checkpoint itself is sha256-verified against the Hub (131/131 shards,
2026-10-05). Built by `scripts/build-frank-primary-rad.sh` (`--plan-only`
first — its declare phase is the engine's own, so a resolving plan boots).
**Measured at cutover**: boot ~2.3 min cold-ish / 92 s warm → /health
status ok; decode **17.1 tok/s** single-stream temp 0 (the vLLM incumbent
measured ~30 tok/s on TWO cards; the KV contract caps the single-card LRU,
and **TP=2 exists but does not boot on ROCm** — the TP allreduce JIT
(`pynccl.cu`) can't resolve `cuda_runtime.h` for gfx1201; probed
2026-10-06, signature in the runbook). `/metrics` is a 404 — the Dankclaw
Prometheus llm-fleet scrape of :7770 reads down until repointed.
`ft ctl cache --kv/--moe` resizes pools live (grow only; shrink needs a
restart); `ft ctl stats` reports occupancy.
**Shape (both R9700s, TP=2)**: `--tp-wire wht6` (fused lossy all-reduce,
+5.6–6.3 % prefill / ~−1.3 % 8-stream decode at TP2), `--max-num-seqs 2`
(Frank's first boot; raise from the ledger + slot occupancy), `--max-model-len
200000` (native 262,144; the harness contextWindow must quote this — a longer
prompt is refused, never truncated), `--placement expert_tiered
--host-pool-mib 40960` (native FP8 experts are 112.8 GiB total, 56.4 GiB
per rank slice — the pinned pool is the price of native precision),
`--expert-vs-cache-ratio 0.82` (the measured 99 %-hit point; 0.87 collapses
to 0 %), `--kv-cache-dtype fp8` (**7.32 KiB/token**; a 200K sequence is
1.43 GiB), `--num-speculative-tokens 3` (the checkpoint's own MTP head —
3,101 `mtp.*` tensors, probe depth 3), `--max-num-batched-tokens 2048`,
prefix cache 4 GiB pinned host + 64 GiB disk at
`/home/dank/radiance-state`, and **`--ngram-placement ram`**: the 47.7 GiB
PLE n-gram table (51.2 GB = 29 % of all parameters) serves from pinned
host RAM. Froggeric v22.5 chat template vendored
(`scripts/qwen38-froggeric-v22.5.jinja`, `--override-chat-template`).
**Deploy gate law (changed for FreeToken)**: `/health` returns **200 with
`{"status":"loading"}`** while the engine loads — the FastAPI front end
binds before the scheduler is up, so a bare `curl` false-greens (measured:
"healthy after 6 s" mid-page-in). The deployer loops until the body says
`"status":"ok"`; the MainPID stray-port guard is unchanged. The image gate
is `podman image exists` (a local build carries no RepoDigests — the old
digest grep refused exactly this shape; pinning lives in the Dockerfile).
**The pinned-RAM ledger is why this is a root unit**: 2 × 40 GiB pools +
47.7 GiB PLE + 4 GiB prefix host ≈ **134 GiB** charged against
`RLIMIT_MEMLOCK`. `ttm.pages_limit`/`pages_max` cap GPU-mappable pinned
pages (pre-cutover cap 123 GiB — BELOW the plan); the serve script's
TTM preflight (`FRANK_PRIMARY_TTM_MIN_MIB=140000`) refuses to start until
both knobs are raised in the stop window (sysfs write first, kernel
cmdline + reboot only if read-only).
Runbook: `serverAppConfigs-docs/LLM/frank-primary-freetoken-pr587.md`.
**Contract facts** (source-verified): the HTTP listener binds AFTER the
full load, so a bare `curl /health` gate is honest (`/health` == `/ping`,
200 serving / 503 stopping); sampling params temperature…preserve_thinking
accepted, **unknown params 400** (models.yml's extraBody set is fully
inside it); `response_format` supported; `logprobs` REFUSED; reasoning in
`reasoning_content` and tool calls in the qwen3.8 dialect via the auto
chat format — no parser flags; vision native (the 27-layer Qwen-VL tower,
`--mm-max-patches`); `/metrics` carries vLLM-compatible names +
`radiance:*` families — the Dankclaw Prometheus scrape is re-added.
Runbook: `serverAppConfigs-docs/LLM/frank-primary-radiance-qwen38-native-fp8.md`.
Retired FreeToken era: `serverAppConfigs-docs/LLM/frank-primary-freetoken-pr587.md`.
Retired vLLM era: `serverAppConfigs-docs/LLM/dankserver-qwen38-fp8-expert-offload-tuning.md`.
Retired GLM experiment: `serverAppConfigs-docs/LLM/frank-primary-glm53-flash-w4a16.md`.
@@ -720,7 +792,7 @@ linked runbooks.
121 GiB box, pushed `available` to 3 GiB, and every later rate was suspect.
Record the child pid and kill it in a `finally`, then prove `free` came back
before believing a number. Kill by pid — never `pkill -f llama-server`,
which also hits frank-small.
which also hits frank-secondary.
**llama.cpp serving**
@@ -786,7 +858,7 @@ linked runbooks.
free. That was a CONTIGUITY failure, not a total-bytes one, and no totals
model — the sweep's or the seam's — can see it.)
- **On the CUDA 3090 the same knob has NO interior optimum up to 2048, so
frank-small ships 4096/2048.** Measured 2026-10-05 by boot-pair A/B on the
frank-secondary ships 4096/2048.** Measured 2026-10-05 by boot-pair A/B on the
live service: at 8,928 prompt tokens ubatch 1024 gives 3,972 / 4,077 t/s and
ubatch 2048 gives 4,207 / 4,212 — **+4.6 %** — for **616 MiB** (free 2,849 →
2,279). Decode did not move (91.9–93.0 vs 93.8–94.3, i.e. noise), and a
@@ -795,9 +867,9 @@ linked runbooks.
than the ubatch cannot see it. The logical `--batch-size` was again free. And
the predecessor's "ubatch 2048 failed the scratch boot" was a shape
observation, not a law: the same file boots 2048 clean at 2 × 200,704. Bench
it by restarting the SERVICE through a transient `FRANK_SMALL_ENV_FILE`
it by restarting the SERVICE through a transient `FRANK_SECONDARY_ENV_FILE`
drop-in, never a scratch port — one card holds one server. See
[[frank-small-llama-mimo-q6k-2x200704]].
[[frank-secondary-llama-mimo-q6k-2x200704]].
- **One `-ts` step is one LAYER, and a hybrid stack's layers are not the same
size.** qwen35 is 16 x (3 Gated DeltaNet + 1 Gated Attention), so a step is
worth ~0.43 GiB on a DeltaNet boundary and ~1.43 GiB on a full-attention one
@@ -819,7 +891,7 @@ linked runbooks.
CTX_SIZE / PARALLEL`, and llama.cpp refuses a prompt longer than the SLOT
(`400 exceed_context_size_error: request (184001 tokens) exceeds the
available context size (180224 tokens)`) even when the pool has room. So
frank-small's 2 → 3 reslice moved each conversation from the model's native
frank-secondary's 2 → 3 reslice moved each conversation from the model's native
262,144 down to 180,224, and `models.yml` `contextWindow` had to follow or
the harness would discover the ceiling as a 400 on a long conversation. Side
effect worth knowing: the shorter slot made decode ~5 % FASTER (106.8 vs
@@ -830,9 +902,9 @@ linked runbooks.
day the freed space also bought the Q6_K rung (now 94.4 tok/s — the rung
itself is a further measured −10 %, weight-bandwidth, not slot geometry).
All moves measured; both slot directions cost or buy window, never pool.
See [[frank-small-llama-mimo-q6k-2x200704]].
See [[frank-secondary-llama-mimo-q6k-2x200704]].
- **Price a VRAM budget from two boots, never from a formula — and an mmproj
costs ~1.5x its file size.** The frank-small 3090 fit was solved from two
costs ~1.5x its file size.** The frank-secondary 3090 fit was solved from two
measurements: 2 × 262,144 with no projector = 20,190 MiB used (KV 12,800 ⇒
**non-KV 7,390**), 3 × 196,608 with the f16 projector = 22,972 used (KV
14,113 ⇒ **non-KV 8,859**). The difference is the vision tower: a 918 MB file
@@ -840,12 +912,12 @@ linked runbooks.
ride with it. Subtracting the two rows is exact; extrapolating weights+KV+
"a bit for buffers" is what leaves 1.1 GiB free when you meant 2.7. Then
solve `KV ≤ card − non-KV − headroom` and round to a multiple of
`PARALLEL × 4096`. Recorded in `scripts/frank-small.env` so the next resize
`PARALLEL × 4096`. Recorded in `scripts/frank-secondary.env` so the next resize
starts from measurement.
- **`images` and `image_url` are NOT interchangeable: only the content part
reaches the model.** `--mmproj` loads, `usage.prompt_tokens` rises, and the
model still invents an answer if the client used the legacy top-level
`images` field on `/v1/chat/completions` — frank-small answered "Brown." for
`images` field on `/v1/chat/completions` — frank-secondary answered "Brown." for
a solid-red PNG that way and "Red." through the OpenAI `image_url` part. The
failure is silent because a hallucinated color is a plausible answer. Probe
vision with a solid color and expect an exact word.
@@ -857,7 +929,7 @@ linked runbooks.
returns `tool_calls`. Check tool support before routing a role at an endpoint.
- **Quantized KV on CUDA needs `GGML_CUDA_FA_ALL_QUANTS=ON`** — the same
option and the same ~12x prefill collapse described in the ROCm note above.
f16 KV needs nothing; frank-small ran f16 for that reason until
f16 KV needs nothing; frank-secondary ran f16 for that reason until
2026-09-17, when `build-cuda` was rebuilt with the option ON so the
endpoint could take q8_0 KV.
- **llama-bench separates a LIST with `/` and SWEEP values with `,`.** Both
@@ -875,7 +947,7 @@ linked runbooks.
raises on anything else — HTTP 500, not a clamp — so the harness's levels
must be folded onto that set in `models.yml` (`compat.reasoningEffortMap`).
A role whose level is unmapped fails on its FIRST request, and the failure
names Jinja, not the model: every subagent on `frank-small` died with
names Jinja, not the model: every subagent on `frank-secondary` died with
`Unexpected reasoning effort high` until `high: xhigh` was added on
2026-09-16. Halogen accepts the wider set, so the two entries legitimately
differ.
@@ -949,9 +1021,9 @@ Active:
| Service | Runbook |
|---------|---------|
| frank-large (halogen container, Qwen3.8-27B .hgn + DFlash2, 1×200,700, text-only, Dankclaw) | `serverAppConfigs-docs/LLM/frank-large-halogen27b-dflash2.md` |
| frank-small (llama.cpp CUDA, MiMo-V2.6-Distill-Qwen-9B Q6_K, 2×200,704 f16 KV, Dankclaw) | `serverAppConfigs-docs/LLM/frank-small-llama-mimo-q6k-2x200704.md` |
| frank-primary (FreeToken PR #587 ROCm, Qwen3.8-Flash-Next FP8, 1×R9700, Dankserver) | `serverAppConfigs-docs/LLM/frank-primary-freetoken-pr587.md` |
| frank-large (flash-server 0.16.2, abliterated Swift1.5 v2 .hgn + MTP always-on, 2×200,192, vision ON, Dankclaw) | `serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md` |
| frank-secondary (llama.cpp CUDA, Swift-1.5-27B Uncensored RCO IQ3_XXS-mtp, 2×114,688 draft-mtp q8_0 KV, Dankclaw) | `serverAppConfigs-docs/LLM/frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md` |
| frank-primary (radiance StillDeadcode, Qwen3.8-Flash-Next-FP8 native, 2×R9700 TP=2, Dankserver) | `serverAppConfigs-docs/LLM/frank-primary-radiance-qwen38-native-fp8.md` |
| Dankclaw kernel posture (`amd_iommu=off`) | `serverAppConfigs-docs/LLM/dankclaw-iommu-translation-off.md` |
| Deployment flow + incidents | `serverAppConfigs-docs/LLM/deployment.md` |
@@ -959,14 +1031,18 @@ Retired (rollback targets and history):
| Runtime | Host | Window | Runbook |
|---------|------|--------|---------|
| halogen 0.1.4 Qwen3.8-27B p1w4d-d2 + DFlash2 (`frank-large`) | Dankclaw | 2026-10-05 → 10-06 (rollback target — payload + fetch script + image digest on disk; owns the prompt-cache pin and FA-tile lessons) | `frank-large-halogen27b-dflash2.md` |
| ROCmFPX llama.cpp fork Qwen3.8-27B Uncensored Q6_0_ROCMFPX_AGENT + MTP (`frank-large`) | Dankclaw | 2026-10-03 → 10-05 (rollback target, payload + fork build on disk) | `frank-large-rocmfpx-q6agent-uncensored-mtp.md` |
| tcclaviger vLLM fork Qwen3.8-Flash-Next-Uncensored FP8, TP=2, shape G (`frank-primary`) | Dankserver | 2026-09-29 → 10-05 (rollback target — tag `frank-primary-vllm-shapeG-20261003`; trio + env lore in the tag, image in rootless storage) | `dankserver-qwen38-fp8-expert-offload-tuning.md` |
| FreeToken PR #587 ROCm, Qwen3.8-Flash-Next-Uncensored-FP8, ONE R9700 (`frank-primary`) | Dankserver | 2026-10-05 → 10-05 (rollback target — tag `frank-primary-freetoken-pr587-20261005`; trio + Dockerfile in the tag, image `localhost/frank-primary-freetoken:pr587-77a7201` and orcarouter payload on disk) | `frank-primary-freetoken-pr587.md` |
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K 2×200,704 f16 KV (`frank-secondary`) | Dankclaw | 2026-10-03 → 10-05 (rollback target — payload on disk; owns the ubatch-2048 A/B; the Ornith swap of 10-05 was committed but never deployed, so this was the LIVE payload Swift replaced) | `frank-secondary-llama-mimo-q6k-2x200704.md` |
| llama.cpp Ornith-1.5-9B Q8_0, ONE 262,144 f16 KV (`frank-secondary`) | Dankclaw | 2026-10-05 (committed 77decb5→380aafa, never deployed — superseded by Swift the same evening; payload on disk) | trail in `frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md` |
| ROCmFPX llama.cpp fork Qwen3.8-27B Q8_0_ROCMFPX_AGENT + MTP (`frank-large`) | Dankclaw | 2026-10-02 → 10-03 (rollback target, payload on disk) | `frank-large-rocmfpx-q8agent-mtp.md` |
| gufo v0.1.1 Qwen3.8-27B UD-Q8_K_XL/UD-Q4_K_XL + DFlash2 (`frank-large`) | Dankclaw | 2026-09-28 → 10-01, again 10-01 → 10-02 (payloads removed from disk 2026-10-03; rollback = re-download) | `frank-large-gufo-qwen38-27b.md` |
| vLLM TheRock MiMo-V2.6-Distill-Qwen-9B BF16 + cross-model MTP (`frank-large`) | Dankclaw | 2026-10-01 (same-day rollback to gufo) | `frank-large-vllm-mimo-v26-bf16.md` |
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K (`frank-small`) | Dankclaw | 2026-10-01 → 10-02 (the rung returned ACTIVE 2026-10-03 at the 2×200,704 shape — `frank-small-llama-mimo-q6k-2x200704.md`) | `frank-small-llama-mimo-q6k.md` |
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q5_K_M 2×200,704 f16 KV (`frank-small`) | Dankclaw | 2026-10-02 → 10-03 (rollback target — one `MODEL_FILE` line, buys back 875 MiB + the −10 % rung tax; owns the boot-pair table) | `frank-small-llama-mimo-q5km.md` |
| SGLang MiMo-V2.6 INT8-W8A16-AutoRound (`frank-small`) | Dankclaw | 2026-10-01 (same-day swap to the GGUF path) | `frank-small-sglang-mimo-v26.md` |
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q6_K (`frank-secondary`) | Dankclaw | 2026-10-01 → 10-02 (the rung returned ACTIVE 2026-10-03 at the 2×200,704 shape — `frank-secondary-llama-mimo-q6k-2x200704.md`) | `frank-secondary-llama-mimo-q6k.md` |
| llama.cpp master MiMo-V2.6-Distill-Qwen-9B Q5_K_M 2×200,704 f16 KV (`frank-secondary`) | Dankclaw | 2026-10-02 → 10-03 (rollback target — one `MODEL_FILE` line, buys back 875 MiB + the −10 % rung tax; owns the boot-pair table) | `frank-secondary-llama-mimo-q5km.md` |
| SGLang MiMo-V2.6 INT8-W8A16-AutoRound (`frank-secondary`) | Dankclaw | 2026-10-01 (same-day swap to the GGUF path) | `frank-secondary-sglang-mimo-v26.md` |
|tcclaviger vLLM GLM-5.3-Flash W4A16 int4, +/- DFlash2 drafter (`frank-primary`)|Dankserver|2026-09-30 → 10-01|`frank-primary-glm53-flash-w4a16.md`|
| llama.cpp b11030 Qwen3.8-27B UD-Q8_K_XL + DFlash2 (`frank-large`) | Dankclaw | 2026-09-21 → 09-28 | `frank-large-unsloth-qwen38-27b-q8xl.md` |
| R9V radiance-vLLM Qwen3.8-Flash-Next-Uncensored IQ4_XS (`frank-primary`) | Dankserver | 2026-09-27 (same-day swap to Q8_0) | `frank-primary-r9v-radiance-qwen38-uncensored.md` |
@@ -979,16 +1055,16 @@ Retired (rollback targets and history):
| CIRU v4.4.1 Orca Qwen3.8-Flash (`frank-large`) | Dankclaw | 2026-09-19 → 09-21 | `frank-large-ciru-orca.md` |
| unsloth llama.cpp b11030 Qwen3.8-Flash-Next UD-IQ4_XS (`frank-large`) | Dankclaw | 2026-09-21 (same-day swap) | `frank-large-unsloth-qwen38-flash-next.md` |
| Kairic Edge Qwen3.8-27B IU4 W4A4 (`frank-large`) | Dankclaw | 2026-09-21 (same-day swap) | `frank-large-kairic-edge-qwen38-27b.md` |
| ISTA-DASLab Qwen3.8-27B-GSQ-RCO IQ3_S (`frank-small`) | Dankclaw | 2026-09-16 → 09-17 | `frank-small-qwen38-gsq-rco.md` |
| vLLM Gemma4-12B w4a16 (`frank-small`) | Dankclaw | 2026-09-19 (same-day revert) | `frank-small-vllm-gemma4.md` |
| llama.cpp Gemma4-12B QAT Q4_K_M (`frank-small`) | Dankclaw | 2026-09-17 → 10-01 | `frank-small-gemma4-12b.md` |
| ISTA-DASLab Qwen3.8-27B-GSQ-RCO IQ3_S (`frank-secondary`) | Dankclaw | 2026-09-16 → 09-17 | `frank-secondary-qwen38-gsq-rco.md` |
| vLLM Gemma4-12B w4a16 (`frank-secondary`) | Dankclaw | 2026-09-19 (same-day revert) | `frank-secondary-vllm-gemma4.md` |
| llama.cpp Gemma4-12B QAT Q4_K_M (`frank-secondary`) | Dankclaw | 2026-09-17 → 10-01 | `frank-secondary-gemma4-12b.md` |
| agentionai ROCmFP4-FAST imatrix | Dankclaw | 2026-08-29 → 08-30 | `agentionai-fast-imatrix.md` |
| eemin Qwen3.8-27B ROCmI4 W4A4 MTP | Dankclaw | 2026-08-18 → 08-25 | `eemin-rocmi4-mtp.md` |
| Kairic Edge IU4 | Dankclaw | 2026-08-22 (same-day revert) | `kairic-edge.md` |
| CIRU ActiveFPX v3 / PromptForge | Dankclaw | through 2026-08-22 | `ciru-activefpx-promptforge.md` |
| DFlash2 Vulkan | Dankclaw | 2026-08-21 (same-day revert) | `dflash2-large-vulkan.md` |
| FastFlowLM NPU (`frank-npu`) | Dankclaw | twice, retired 2026-09-11 | `frank-npu.md`, `FastFlowLM NPU Inference.md` |
| FreeToken (official + Huihui FP8, `frank-small`) | Dankserver | 2026-09-02 → 09-15 | `freetoken-frank-small.md` |
| FreeToken (official + Huihui FP8, `frank-secondary`) | Dankserver | 2026-09-02 → 09-15 | `freetoken-frank-small.md` |
| FastMTP patched llama.cpp | Dankserver | retired 2026-08-18 | `qwen3.8-spec-matrix.md` |
| ik_llama.cpp Ornith CRACK | Dankserver | retired 2026-09-02 | `ikllama-ornith-dankserver.md` |
| llama.cpp layer split + DFlash2 (`frank-medium`) | Dankserver | 2026-09-16 → 09-20 | `frank-medium-llamacpp-dflash2.md` |
+22 -113
View File
@@ -1,117 +1,26 @@
#!/usr/bin/env bash
# autoresearch.sh — canonical benchmark entrypoint: speed of frank-primary.
# autoresearch.sh — canonical frank-primary benchmark entry point (dev box).
#
# Runs on the operator box and drives one bench window on DANKSERVER:
# 1. fingerprint the candidate trio (env + serve script + unit)
# 2. memo fast-path: an identical fingerprint that already produced a FULLY
# successful window (bench + verified production restore) returns the
# cached metrics without touching the fleet
# 3. stage the candidate trio plus the measure client into
# /home/dank/.autoresearch-frank-primary/ (ephemeral; the repo checkout
# on the host stays the production source of truth)
# 4. run the window (scripts/autoresearch/frank-primary-window.sh):
# stop + mask the production unit, boot the staged config AS THE SERVICE
# (same serve script, same container, same port 7770), wait for health,
# run the in-container usage-counter measure, then restore the production
# unit and verify its health — AGENTS.md Benchmarking Policy
# 5. print METRIC lines (parse by run_experiment)
#
# Primary metric: decode_tps (HIGHER is better) — median across the three
# prompt depths (115/1056/4096) of per-depth medians of
# completion_tokens / (wall - ttft), 3 runs each after a warm pass,
# temperature 0, the campaign's deterministic word-seed workload.
#
# Exit 0 with METRIC lines on success; non-zero on any failure (the window
# script's EXIT trap restores production on every path).
set -euo pipefail
# The service runs on dankserver (192.168.0.235:7770), the two R9700s live
# there, and the benchmark must own those cards, so the whole stop -> boot
# scratch -> bench -> restore sequence runs ON dankserver (see
# scripts/autoresearch-frank-primary.sh). This wrapper:
# 1. copies the candidate config (the repo's scripts/frank-primary.env, the
# file the autoresearch loop edits) and the bench script to dankserver,
# 2. runs the bench over ssh, relaying its stdout (the METRIC lines) and
# exit code back here verbatim.
# It deliberately does NOT push to origin: the scp dirties dankserver's
# working tree, and the loop's uncommitted candidate config must not be
# pushed. Only this file and scripts/autoresearch-frank-primary.sh are
# committed; the candidate env rides over ssh each run.
set -uo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REMOTE=dankserver
REMOTE_REPO=/home/dank/serverAppConfigs
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
HOST="${FRANK_PRIMARY_AR_HOST:-dankserver}"
STAGE="/home/dank/.autoresearch-frank-primary"
scp -q "$DIR/scripts/frank-primary.env" \
"$REMOTE:$REMOTE_REPO/scripts/frank-primary.env" || { echo "scp env failed" >&2; exit 1; }
scp -q "$DIR/scripts/autoresearch-frank-primary.sh" \
"$REMOTE:$REMOTE_REPO/scripts/autoresearch-frank-primary.sh" || { echo "scp script failed" >&2; exit 1; }
# FRANK_PRIMARY_AR_ENV_FILE stages a DIFFERENT env than the repo's — that is how
# the arms driver A/Bs engine knobs without mutating the checkout. The
# fingerprint covers whatever file is staged, so the memo cannot cross arms.
ENV_FILE="${FRANK_PRIMARY_AR_ENV_FILE:-$SCRIPT_DIR/scripts/frank-primary.env}"
SERVE="$SCRIPT_DIR/scripts/serve-frank-primary.sh"
UNIT="$SCRIPT_DIR/scripts/frank-primary.service"
MEASURE="$SCRIPT_DIR/scripts/autoresearch/frank-primary-measure.py"
WINDOW="$SCRIPT_DIR/scripts/autoresearch/frank-primary-window.sh"
for f in "$ENV_FILE" "$SERVE" "$UNIT" "$MEASURE" "$WINDOW"; do
[ -f "$f" ] || { echo "[autoresearch] missing harness file: $f" >&2; exit 1; }
done
command -v python3 >/dev/null 2>&1 || { echo "[autoresearch] python3 required on operator box" >&2; exit 1; }
# Fingerprint of everything that can change the engine's argv or environment.
HASH="$(cat "$ENV_FILE" "$SERVE" "$UNIT" | sha256sum | cut -d' ' -f1)"
echo "[autoresearch] candidate fingerprint $HASH" >&2
ssh_run() { ssh -o BatchMode=yes -o ConnectTimeout=15 "$HOST" "$@"; }
emit_metrics() { # $1 = run.json
python3 -c '
import json, sys
run = json.loads(sys.argv[1])
m = run["measure"]
print("METRIC decode_tps=%s" % m["decode_tps"])
for k in ("decode_tps_115", "decode_tps_1056", "decode_tps_4096"):
print("METRIC %s=%s" % (k, m[k]))
print("METRIC ttft_ms_4096=%s" % m["ttft_ms_4096"])
print("METRIC kv_tokens=%s" % (run.get("kv_tokens") or 0))
print("METRIC boot_seconds=%s" % (run.get("boot_seconds") or -1))
print("METRIC completion_tokens_4096=%s" % m.get("completion_tokens_4096", 0))
# Any extra sampling temperature the measure swept (AR_TEMPS), e.g. the 0.7
# arm — that is where speculative-decode acceptance shows up, since a greedy
# draft matches a greedy target exactly and is rejected more under real
# sampling. Suffixed keys only (_t0.7), so the historical lines are untouched.
for k in sorted(m):
if "_t" in k and (k.startswith("decode_tps") or k.startswith("ttft_ms")):
print("METRIC %s=%s" % (k, m[k]))
' "$1"
}
# --- memo fast-path -------------------------------------------------------
cached_hash="$(ssh_run "cat '$STAGE/last/hash' 2>/dev/null" 2>/dev/null || true)"
if [ -n "$cached_hash" ] && [ "$cached_hash" = "$HASH" ]; then
cached_run="$(ssh_run "cat '$STAGE/last/run.json' 2>/dev/null" || true)"
if [ -n "$cached_run" ]; then
echo "[autoresearch] memo hit — cached metrics for $HASH, fleet untouched" >&2
emit_metrics "$cached_run"
exit 0
fi
fi
# --- stage the candidate ----------------------------------------------------
echo "[autoresearch] staging candidate on $HOST:$STAGE" >&2
ssh_run "mkdir -p '$STAGE'"
scp -q "$ENV_FILE" "$HOST:$STAGE/frank-primary.env"
scp -q "$SERVE" "$HOST:$STAGE/serve.sh"
scp -q "$MEASURE" "$HOST:$STAGE/measure.py"
scp -q "$WINDOW" "$HOST:$STAGE/window.sh"
ssh_run "printf '%s\n' '$HASH' > '$STAGE/hash'"
# --- the window -------------------------------------------------------------
# Budgets: bench boot up to 20 min (warm boots measured ~7 min), measure up
# to 30 min, restore boot up to 20 min — call run_experiment with
# timeout_seconds >= 3600.
echo "[autoresearch] opening bench window (stop+mask -> boot -> measure -> restore)" >&2
set +e
ssh_run "bash '$STAGE/window.sh'"
rc=$?
set -e
if [ "$rc" -ne 0 ]; then
echo "[autoresearch] window failed (rc=$rc); host-side evidence:" >&2
ssh_run "tail -60 '$STAGE/fail.log' 2>/dev/null || tail -40 '$STAGE/serve.out' 2>/dev/null || tail -40 '$STAGE/measure.out' 2>/dev/null || true" >&2 || true
exit 1
fi
run_json="$(ssh_run "cat '$STAGE/run.json'")" || {
echo "[autoresearch] could not read $STAGE/run.json back" >&2; exit 1; }
echo "[autoresearch] window result:" >&2
printf '%s\n' "$run_json" >&2
emit_metrics "$run_json"
echo "[autoresearch] engine evidence (plan / compile path / spec counters):" >&2
ssh_run "cat '$STAGE/evidence.log' 2>/dev/null" >&2 || true
exit 0
ssh "$REMOTE" "bash $REMOTE_REPO/scripts/autoresearch-frank-primary.sh $REMOTE_REPO/scripts/frank-primary.env"
@@ -0,0 +1,64 @@
{
"3": {
"class_type": "CLIPTextEncode",
"_meta": { "title": "Positive prompt" },
"inputs": {
"text": "A detailed illustration of a 1930s Soviet industrial design bureau at night: a well-measured engineer at a drafting table, a warm desk lamp, rolled blueprints, gears and calipers on the desk, deep red and cream palette, gouache painterly style, intricate mechanical detail, soft shadows",
"clip": [ "6", 0 ]
}
},
"4": {
"class_type": "CLIPTextEncode",
"_meta": { "title": "Negative prompt" },
"inputs": {
"text": "lowres, blurry, deformed, extra limbs, watermark, signature, jpeg artifacts",
"clip": [ "6", 0 ]
}
},
"5": {
"class_type": "UNETLoader",
"_meta": { "title": "Qwen Image 2.1 (int8)" },
"inputs": { "unet_name": "qwen_image_2.1_int8_convrot.safetensors", "weight_dtype": "default" }
},
"6": {
"class_type": "CLIPLoader",
"_meta": { "title": "Qwen3-VL 8B text encoder (w4a8)" },
"inputs": { "clip_name": "qwen3vl_8b_w4a8.safetensors", "type": "qwen_image" }
},
"7": {
"class_type": "VAELoader",
"_meta": { "title": "VAE" },
"inputs": { "vae_name": "qwen_image_2.1_vae_bf16.safetensors" }
},
"8": {
"class_type": "EmptySD3LatentImage",
"_meta": { "title": "Empty Latent 1024x1024" },
"inputs": { "width": 1024, "height": 1024, "batch_size": 1 }
},
"9": {
"class_type": "KSampler",
"_meta": { "title": "KSampler" },
"inputs": {
"model": [ "5", 0 ],
"seed": 1930,
"steps": 20,
"cfg": 1.0,
"sampler_name": "euler",
"scheduler": "simple",
"positive": [ "3", 0 ],
"negative": [ "4", 0 ],
"latent_image": [ "8", 0 ],
"denoise": 1.0
}
},
"10": {
"class_type": "VAEDecode",
"_meta": { "title": "VAE Decode" },
"inputs": { "samples": [ "9", 0 ], "vae": [ "7", 0 ] }
},
"11": {
"class_type": "SaveImage",
"_meta": { "title": "Save Image (art)" },
"inputs": { "filename_prefix": "qwen21-art", "images": [ "10", 0 ] }
}
}
@@ -0,0 +1,64 @@
{
"3": {
"class_type": "CLIPTextEncode",
"_meta": { "title": "Positive prompt (legible text)" },
"inputs": {
"text": "Clean minimalist movie-poster design, dark charcoal background with a subtle vintage grain. Centered large bold white sans-serif text reading \"SOLIDARITY OF THE SOVIET COMPUTER SYSTEMS\", perfectly legible, crisp letterforms, even letter spacing. Below it, a smaller line of yellow text reading \"DANKCLAW DANKSERVER EST 2026\", equally legible. Flat graphic design, high contrast, generous margins, no other words, no watermark",
"clip": [ "6", 0 ]
}
},
"4": {
"class_type": "CLIPTextEncode",
"_meta": { "title": "Negative prompt" },
"inputs": {
"text": "blurry, illegible, misspelled, warped letters, extra text, watermark, signature, lowres, jpeg artifacts",
"clip": [ "6", 0 ]
}
},
"5": {
"class_type": "UNETLoader",
"_meta": { "title": "Qwen Image 2.1 (int8)" },
"inputs": { "unet_name": "qwen_image_2.1_int8_convrot.safetensors", "weight_dtype": "default" }
},
"6": {
"class_type": "CLIPLoader",
"_meta": { "title": "Qwen3-VL 8B text encoder (w4a8)" },
"inputs": { "clip_name": "qwen3vl_8b_w4a8.safetensors", "type": "qwen_image" }
},
"7": {
"class_type": "VAELoader",
"_meta": { "title": "VAE" },
"inputs": { "vae_name": "qwen_image_2.1_vae_bf16.safetensors" }
},
"8": {
"class_type": "EmptySD3LatentImage",
"_meta": { "title": "Empty Latent 832x1216 poster" },
"inputs": { "width": 832, "height": 1216, "batch_size": 1 }
},
"9": {
"class_type": "KSampler",
"_meta": { "title": "KSampler" },
"inputs": {
"model": [ "5", 0 ],
"seed": 2026,
"steps": 25,
"cfg": 1.0,
"sampler_name": "euler",
"scheduler": "simple",
"positive": [ "3", 0 ],
"negative": [ "4", 0 ],
"latent_image": [ "8", 0 ],
"denoise": 1.0
}
},
"10": {
"class_type": "VAEDecode",
"_meta": { "title": "VAE Decode" },
"inputs": { "samples": [ "9", 0 ], "vae": [ "7", 0 ] }
},
"11": {
"class_type": "SaveImage",
"_meta": { "title": "Save Image (text poster)" },
"inputs": { "filename_prefix": "qwen21-text", "images": [ "10", 0 ] }
}
}
+1 -1
View File
@@ -4,7 +4,7 @@
# command line (a Bearer token in argv is world-readable via /proc).
#
# Per-file FRESH downloads, no resume: the house rule (see
# download-frank-small.sh); -f/--fail-with-body plus size+sha256 against the
# download-frank-secondary.sh); -f/--fail-with-body plus size+sha256 against the
# Hub LFS oid is what makes an access error fail instead of "succeeding".
#
# Usage:
+1 -1
View File
@@ -2,7 +2,7 @@
# Download the source Qwen3.5-9B checkpoint — the ONLY place this model
# family's MTP head (15 mtp.* tensors) exists. frank-large runs it as the
# vLLM cross-model MTP draft (MiMo-V2.6-Distill-Qwen-9B itself ships zero
# mtp tensors; that is why SGLang's NEXTN on frank-small draws 0.00
# mtp tensors; that is why SGLang's NEXTN on frank-secondary draws 0.00
# acceptance). Public repo — no auth header on the command line.
#
# NOTE the odd upstream shard names (model.safetensors-0000N-of-00004...).
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Download MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound for frank-small
# Download MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound for frank-secondary
# (SGLang, RTX 3090). Public repo — no auth header on the command line.
#
# Per-file FRESH downloads, no resume: `curl -C -` once produced a 213 MB
@@ -7,7 +7,7 @@
# the house rule.
#
# Usage:
# scripts/download-frank-small.sh [DEST_DIR] [--verify-only]
# scripts/download-frank-secondary.sh [DEST_DIR] [--verify-only]
#
# LFS shards are verified against the Hub LFS sha256; non-LFS text files
# against their declared byte size. Idempotent: complete files are skipped.
+186
View File
@@ -0,0 +1,186 @@
#!/usr/bin/env bash
# autoresearch-frank-primary.sh — RUNS ON DANKSERVER, driven over ssh by the
# dev-box autoresearch.sh. It is the canonical frank-primary benchmark entry
# point for the autoresearch loop.
#
# Method (mandated by the Benchmarking Policy): the production service serves
# real users and shares the two R9700s, so a bench beside it starves the GPUs
# and corrupts results. This script therefore:
# 1. stops the production frank-primary unit (root, relink symlink: stop,
# NOT mask — mask refuses to replace a symlinked unit file),
# 2. boots a scratch radiance container named `arbench` on the SAME two
# cards with the candidate engine argv (the env file the loop edits),
# 3. polls /health until the listener is live (it binds AFTER the full
# load, so a bare curl is an honest gate),
# 4. runs ONE self-contained completion request locally (127.0.0.1, no LAN
# jitter): a fixed ~140k-char real-text prompt (~32k tokens) decoded for
# 256 tokens, greedy. The engine's `timings` object reports the honest
# prefill and decode rates for that single request.
# 5. prints METRIC lines and tears the scratch down,
# 6. restores production (systemctl start), via a trap so a failed bench
# never leaves frank-primary down (the stale-probe-tail lesson).
#
# Determinism: the prompt is built from a fixed, sorted corpus of repo files
# with a fixed random seed, so prompt_n is stable across runs. Sampling is
# greedy (temperature 0), so the decode length is fixed; the only residual
# nondeterminism is the wht6 lossy TP all-reduce, which is a constant across
# runs and cancels in A/B comparisons.
#
# The scratch mirrors the production container contract exactly (serve-frank-
# primary.sh): --network host, --init, seccomp=unconfined (io_uring for the
# n-gram table), memlock unbounded (the ~132 GiB pinned ledger), /dev/kfd +
# /dev/dri, the .rad ro, the state dir rw, the tune cache mount. The ONLY
# differences: the container name (arbench, so prod's identity is untouched)
# and a FRESH prefix-cache disk tier (arbench/, wiped per run) so the cold
# prefill is cold — the prod tier at kvcache/frank-primary is left intact.
set -uo pipefail
ENV_FILE="${1:-/home/dank/serverAppConfigs/scripts/frank-primary.env}"
SCRATCH=arbench
PORT=7770
HEALTH="http://127.0.0.1:$PORT/health"
COMPLETE="http://127.0.0.1:$PORT/v1/completions"
BODY=/tmp/arbench-body.json
LOG=/home/dank/logs/arbench.log
restore() {
podman rm -f "$SCRATCH" >/dev/null 2>&1 || true
sudo systemctl start frank-primary >/dev/null 2>&1 || true
}
trap restore EXIT
# --- source the candidate config (same file the production unit reads) ------
if [ ! -f "$ENV_FILE" ]; then
echo "no env file at $ENV_FILE" >&2; exit 1
fi
# shellcheck disable=SC1090
set -a; . "$ENV_FILE"; set +a
# --- stop production; the scratch needs both cards to itself ----------------
sudo systemctl stop frank-primary >/dev/null 2>&1 || true
podman rm -f "$SCRATCH" >/dev/null 2>&1 || true
# --- fresh prefix-cache tier for a cold prefill (prod's tier untouched) ------
FRESH_KV="$FRANK_PRIMARY_STATE_DIR/kvcache/$SCRATCH"
rm -rf "$FRESH_KV"; mkdir -p "$FRESH_KV"
mkdir -p "$FRANK_PRIMARY_STATE_DIR/tune"
# --- boot the scratch: the production run line, scratch name + fresh KV ------
podman run -d --name "$SCRATCH" \
--network host \
--init \
--security-opt seccomp=unconfined \
--ulimit memlock=-1:-1 \
--device /dev/kfd --device /dev/dri \
-v "$FRANK_PRIMARY_MODEL_DIR":/models:ro \
-v "$FRANK_PRIMARY_STATE_DIR":/data \
-v "$FRANK_PRIMARY_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
-v "$FRANK_PRIMARY_TEMPLATE":/templates/template.jinja:ro \
"$FRANK_PRIMARY_IMAGE" \
--model "/models/$FRANK_PRIMARY_MODEL_FILE" \
--host :: --port "$PORT" \
--served-model-name "$FRANK_PRIMARY_SERVED_NAME" \
--override-chat-template /templates/template.jinja \
--tp "$FRANK_PRIMARY_TP" --tp-wire "$FRANK_PRIMARY_TP_WIRE" \
--max-num-seqs "$FRANK_PRIMARY_MAX_SEQS" \
--max-model-len "$FRANK_PRIMARY_MAX_MODEL_LEN" \
--placement "$FRANK_PRIMARY_PLACEMENT" \
--host-pool-mib "$FRANK_PRIMARY_HOST_POOL_MIB" \
--gpu-headroom-mib "$FRANK_PRIMARY_GPU_HEADROOM_MIB" \
--expert-vs-cache-ratio "$FRANK_PRIMARY_EXPERT_VS_CACHE" \
--kv-cache-dtype "$FRANK_PRIMARY_KV_DTYPE" \
--prefix-cache-host-mib "$FRANK_PRIMARY_PREFIX_HOST_MIB" \
--prefix-cache-dir "/data/kvcache/$SCRATCH" \
--prefix-cache-disk-mib "$FRANK_PRIMARY_PREFIX_DISK_MIB" \
--ngram-placement "$FRANK_PRIMARY_NGRAM_PLACEMENT" \
--num-speculative-tokens "$FRANK_PRIMARY_SPEC_TOKENS" \
--max-num-batched-tokens "$FRANK_PRIMARY_MAX_BATCHED_TOKENS" \
${FRANK_PRIMARY_EXTRA_ARGS:+$FRANK_PRIMARY_EXTRA_ARGS} \
> "$LOG" 2>&1
if [ $? -ne 0 ]; then echo "scratch boot failed; see $LOG" >&2; exit 1; fi
# --- poll health (listener binds after the full load; cold boot ~110 s) ------
ok=0
for _ in $(seq 1 360); do
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$HEALTH" 2>/dev/null || echo 000)
if [ "$code" = "200" ]; then ok=1; break; fi
sleep 2
done
if [ "$ok" -ne 1 ]; then
echo "scratch never reached health 200; tail of $LOG:" >&2
tail -30 "$LOG" >&2; exit 1
fi
# --- build the workload: a fixed ~140k-char real-text prompt (repo corpus) ---
python3 - "$BODY" <<'PY'
import json, sys, random, glob
random.seed(1234)
root = "/home/dank/serverAppConfigs"
files = sorted(
glob.glob(f"{root}/serverAppConfigs-docs/**/*.md", recursive=True)
+ glob.glob(f"{root}/scripts/*.sh")
+ glob.glob(f"{root}/scripts/*.py")
+ glob.glob(f"{root}/AGENTS.md")
)
corpus = []
total = 0
for f in files:
try:
t = open(f, encoding="utf-8", errors="ignore").read()
except Exception:
continue
if not t.strip():
continue
corpus.append(t)
total += len(t)
if total > 240000:
break
corpus = "\n".join(corpus)
TGT = 140000
parts, n = [], 0
while n < TGT:
start = random.randint(0, max(0, len(corpus) - 500))
seg = corpus[start:start + random.randint(200, 500)]
parts.append(seg)
n += len(seg) + 1
prompt = " ".join(parts)
json.dump(
{"model": "frank-primary", "prompt": prompt,
"max_tokens": 256, "temperature": 0.0, "stream": False,
"stop": ["<|im_end|>"]},
open(sys.argv[1], "w"),
)
PY
if [ ! -f "$BODY" ]; then echo "prompt build failed" >&2; exit 1; fi
# --- the bench request (prefill ~32k + decode 256, greedy) -------------------
RESP=$(curl -s --max-time 600 -X POST "$COMPLETE" \
-H 'Content-Type: application/json' -d "@$BODY")
echo "$RESP" > /tmp/arbench-resp.json
# --- parse the timings into METRIC lines -------------------------------------
python3 - <<'PY'
import json, sys
try:
r = json.load(open("/tmp/arbench-resp.json"))
except Exception as e:
sys.stderr.write(f"unparseable response: {e}\n"); sys.exit(1)
if "timings" not in r:
sys.stderr.write(f"no timings in response: {json.dumps(r)[:500]}\n"); sys.exit(1)
t = r["timings"]
dtps = t.get("predicted_per_second")
ptps = t.get("prompt_per_second")
pms = t.get("prompt_ms")
dn = t.get("draft_n", 0)
da = t.get("draft_n_accepted", 0)
if dtps is None:
sys.stderr.write("predicted_per_second null (no decode produced)\n"); sys.exit(1)
print(f"METRIC decode_tps={dtps:.4f}")
if ptps is not None:
print(f"METRIC prefill_tps={ptps:.4f}")
if pms is not None:
print(f"METRIC ttft_ms={pms:.4f}")
if dn:
print(f"METRIC draft_accept={da/dn:.4f}")
print(f"INFO prompt_n={t.get('prompt_n')} cache_n={t.get('cache_n')} predicted_n={t.get('predicted_n')}")
PY
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# build-frank-primary-rad.sh — build frank-primary's .rad container from the
# OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint, at NATIVE precision.
#
# bash scripts/build-frank-primary-rad.sh # convert
# bash scripts/build-frank-primary-rad.sh --plan-only # declare, no I/O
# bash scripts/build-frank-primary-rad.sh --restage # redo staging
# bash scripts/build-frank-primary-rad.sh --force # rebuild existing
#
# WHY A STAGING DIR (the one non-obvious step): rad-convert selects its
# architecture plugin by the checkpoint's quantisation DESCRIPTOR —
# derived from config.json's quantization_config (core/format/checkpoint.cpp:
# "fp8" here). No shipped plugin claims (qwen4exp, "fp8"): the shipped
# qwen4exp plugin claims (qwen4exp, ""), the descriptor recipe-built
# containers carry (they are built from the bf16 upstream, which has no
# quantization_config at all). The match is exact with no fallback, so an
# unmodified convert refuses before declare. The descriptor is only a
# selection key; per-weight encodings are data — the checkpoint reader
# derives them from the tensors themselves (X.weight_scale_inv is the
# scale plane; checkpoint.h). Staging drops the quantization_config key
# (everything else is a relative symlink into the pristine download) so
# the convert selects the shipped plugin. EVERY weight is then copied through
# in its own encoding — block-FP8 codes + bf16 scale planes stay block-FP8 —
# except TWO value-preserving reshapes that stack-qwen38-official-fp8.py
# performs on the staging dir first, and ONE recipe rule that finishes the
# PLE table:
# * experts: the plugin maps the STACKED layout (mlp.experts.gate_up_proj
# [E,2F,H] with the _scale_inv plane at the same index) while the official
# repo ships per-expert unfused tensors — staging byte-stacks them
# (gate rows first, no requantisation).
# * PLE: the plugin's fp8 branch wants the table codes beside a ONE-ELEMENT
# bf16 scale plane; the official repo ships 128 SCALELESS fp8 shards plus
# one bf16 scalar. Staging decodes to bf16 with the checkpoint's own
# scale, and the convert's rtn rule re-encodes at that same scale —
# every original code is restored exactly and the scale plane lands in
# the form the plugin declares.
#
# WHY A CONTAINER AT ALL: a checkpoint dir has no mapped/file tiers, and a
# weight only a host kernel reads (the n-gram / PLE table, 51B params)
# cannot be served from one (GUIDE §3.4). rad-convert also bakes the
# tokenizer and the checkpoint's chat template; the runtime replaces the
# template with froggeric's (--override-chat-template).
#
# The convert runs rad-convert inside the digest-pinned radiance image
# (the host has no ROCm and needs none): /home/dank/models is mounted at
# /models so the staging dir's RELATIVE symlinks resolve inside the
# container. Output lands beside the raw download, owned by dank
# (rootless podman maps container-root to the invoking user).
set -euo pipefail
SRC="/home/dank/models/Qwen/Qwen3.8-Flash-Next-FP8"
STAGE="/home/dank/models/stage-qwen4exp-fp8"
OUT_DIR="/home/dank/models/rad"
OUT="$OUT_DIR/qwen4exp-fp8-native.rad"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck disable=SC1091
source "$SCRIPT_DIR/frank-primary.env"
IMAGE="$FRANK_PRIMARY_IMAGE"
plan_only=0 restage=0 force=0
for a in "$@"; do
case "$a" in
--plan-only) plan_only=1 ;;
--restage) restage=1 ;;
--force) force=1 ;;
*) echo "unknown flag: $a" >&2; exit 2 ;;
esac
done
if [ ! -f "$SRC/config.json" ]; then
echo "ERROR: raw checkpoint missing: $SRC (run scripts/download-frank-primary.sh --fetch)" >&2
exit 1
fi
echo "verifying raw checkpoint first..."
bash "$SCRIPT_DIR/download-frank-primary.sh"
if [ -f "$OUT" ] && [ "$plan_only" -eq 0 ] && [ "$force" -eq 0 ]; then
echo "container already exists: $OUT"
echo "rebuild it with --force (the old file moves to ~/.AGENTTRASH)"
exit 0
fi
# ---- staging ----------------------------------------------------------------
if [ "$restage" -eq 1 ] && [ -e "$STAGE" ]; then
TRASH="$HOME/.AGENTTRASH/$(date +%Y%m%d)-frank-primary-stage"
mkdir -p "$TRASH"
mv "$STAGE" "$TRASH/stage-$(date +%H%M%S)"
echo "old staging dir moved to $TRASH"
fi
if [ ! -f "$STAGE/config.json" ]; then
mkdir -p "$STAGE"
python3 - "$SRC/config.json" "$STAGE/config.json" <<'PY'
import json, sys
src, dst = sys.argv[1], sys.argv[2]
cfg = json.load(open(src))
dropped = cfg.pop("quantization_config", None)
json.dump(cfg, open(dst, "w"), indent=2)
print(f"staging config.json written (quantization_config dropped: {dropped is not None})")
PY
# The index is COPIED like config.json, never symlinked: the reshape step
# rewrites the stage's index, and a symlink would write through to the
# raw checkpoint (measured 2026-10-05: the raw index was restamped with
# the stacked layout and the raw verify failed 143/144). Shards stay
# relative symlinks — relative so the targets resolve inside the convert
# container (the stage's parent is mounted at /models).
for f in "$SRC"/*; do
name="$(basename "$f")"
case "$name" in config.json|model.safetensors.index.json) continue ;; esac
ln -sfn "../Qwen/Qwen3.8-Flash-Next-FP8/$name" "$STAGE/$name"
done
cp "$SRC/model.safetensors.index.json" "$STAGE/"
echo "staging dir ready: $STAGE ($(ls "$STAGE" | wc -l) entries)"
fi
# ---- checkpoint reshape (stacked experts + bf16 PLE table) -------------------
# stack-qwen38-official-fp8.py reshapes the staged checkpoint in place
# (idempotent) and writes the PLE scale to $STAGE/ple_scale.txt — see the
# script's docstring for why each transform is value-preserving.
echo "stacking experts + merging the PLE table (value-preserving)..."
python3 "$SCRIPT_DIR/stack-qwen38-official-fp8.py" --stage "$STAGE"
PLE_SCALE="$(tr -d '\n' < "$STAGE/ple_scale.txt")"
echo "PLE fixed scale: $PLE_SCALE"
# ---- convert ----------------------------------------------------------------
# rad-convert needs no GPU: declare resolves kernels and asks each about the
# encoding it will read — CPU-side layout questions only. (If a plugin load
# ever demands a card, add --device /dev/kfd --device /dev/dri to this run.)
# --plan-only runs the same declare phase and exits before a byte is written.
echo "rad-convert: $SRC (staged) -> $OUT"
mkdir -p "$OUT_DIR"
CONVERT_ARGS=(/models/stage-qwen4exp-fp8 -o /models/rad/qwen4exp-fp8-native.rad
--home /opt/radiance/share/radiance)
[ "$plan_only" -eq 1 ] && CONVERT_ARGS+=(--plan-only -v)
# THE ONE RECIPE RULE (see header): the PLE table arrives from staging as a
# bf16 merge of the checkpoint's own fp8 codes decoded with the checkpoint's
# own scale; rtn re-encodes it at that same scale — the codes come back
# exactly and the plugin's one-element scale plane is produced.
CONVERT_ARGS+=(--quant "blk.*.ple_ngram.weight=rtn:codes=fp8_e4m3,block=*x*,scale=bf16,rule=fixed,scale_value=$PLE_SCALE")
[ "$force" -eq 1 ] && [ -f "$OUT" ] && {
TRASH="$HOME/.AGENTTRASH/$(date +%Y%m%d)-frank-primary-rad"
mkdir -p "$TRASH"
mv "$OUT" "$TRASH/qwen4exp-fp8-native.rad.$(date +%H%M%S)"
echo "previous container moved to $TRASH"
}
podman run --rm \
--entrypoint rad-convert \
-v /home/dank/models:/models \
"$IMAGE" "${CONVERT_ARGS[@]}"
if [ "$plan_only" -eq 1 ]; then
echo "plan-only run complete (no container written)"
exit 0
fi
# ---- record -----------------------------------------------------------------
echo "rad-info summary:"
podman run --rm --entrypoint rad-info -v /home/dank/models:/models:ro \
"$IMAGE" "/models/rad/qwen4exp-fp8-native.rad" | head -20
MIB=$(( $(stat -c %s "$OUT") / 1048576 ))
echo "container: $OUT (${MIB} MiB; env floor $FRANK_PRIMARY_RAD_MIN_MIB MiB)"
if [ "$MIB" -lt "$FRANK_PRIMARY_RAD_MIN_MIB" ]; then
echo "ERROR: container below the env floor — the convert was truncated" >&2
exit 1
fi
echo "build complete"
+6 -6
View File
@@ -62,7 +62,7 @@ SERVICE_ORDER = (
"lidarr",
"readarr",
"frank-large",
"frank-small",
"frank-secondary",
"frank-primary",
)
@@ -94,7 +94,7 @@ DANKCLAW_SERVICES = {
"caddy",
"homepage",
"frank-large",
"frank-small",
"frank-secondary",
}
DANKSERVER_SERVICES = {
@@ -400,12 +400,12 @@ def services_for_path(path: str, target: str) -> set[str]:
# change to it redeploying alertmanager is the only path that
# reaches it on the host.
_add(services, "alertmanager")
if name in {"frank-small.env", "frank-small.service", "serve-frank-small.sh"}:
# llama.cpp frank-small (2026-09-16) keeps its unit, env and serve
# files here; each one selects frank-small only. The ik_llama.cpp
if name in {"frank-secondary.env", "frank-secondary.service", "serve-frank-secondary.sh"}:
# llama.cpp frank-secondary (2026-09-16) keeps its unit, env and serve
# files here; each one selects frank-secondary only. The ik_llama.cpp
# trio that lived here for two days is in scripts/.AGENTTRASH/,
# which is archive material and plans nothing.
_add(services, "frank-small")
_add(services, "frank-secondary")
if name in {"frank-large.env", "frank-large.service", "serve-frank-large.sh"}:
# CIRU v4.4.1 frank-large (2026-09-19) keeps its unit, env and
# serve files here the same way: a root unit on a host-built
+117 -107
View File
@@ -90,7 +90,7 @@ service_is_allowed() {
dankclaw:authentik-postgres|dankclaw:authentik-server|\
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|\
dankclaw:caddy|dankclaw:homepage|dankclaw:seerr|dankclaw:marreta|\
dankclaw:frank-large|dankclaw:frank-small|\
dankclaw:frank-large|dankclaw:frank-secondary|\
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
dankserver:prowlarr|dankserver:qbittorrent|dankserver:radarr|\
@@ -1086,33 +1086,37 @@ deploy_anubis() {
wait_container_healthy anubis 60 || return 1
}
# frank-large is the halogen CONTAINER (ghcr.io/peonist-ai/halogen,
# digest-pinned through scripts/frank-large.env) serving
# peonist-ai/halogen-qwen3.8-27b — qwen3.8-27b-p1w4d-d2.hgn, 33.4 GiB,
# DFlash2 spec decode, 1 slot x 200700, text-only — on the Strix Halo
# iGPU. It stays a ROOT unit because the serve script drives `podman run`
# directly (the frank-primary pattern): relink_unit points
# /etc/systemd/system/frank-large.service at the repo file, boot
# frank-large is the halogen-flash-server CONTAINER (0.16.2, digest-pinned
# through scripts/frank-large.env) serving the Quat3rnion ABLITERATED
# Swift1.5 finetune of peonist's Qwen3.8-Flash-Next v2 checkpoint —
# qwen38-flash-next-v2-swift15-abliterated.hgn (66,687,678,464 B, ~62 GiB
# resident) + the REQUIRED n-gram lookup table + the vision tower —
# MTP spec decode (HALOGEN_SPEC_ADAPT=0), 2 slots x 200192, image input —
# on the Strix Halo iGPU. It stays a ROOT unit because the serve script
# drives `podman run` directly (the frank-primary pattern): relink_unit
# points /etc/systemd/system/frank-large.service at the repo file, boot
# persistence is that unit's [Install], and LimitMEMLOCK=infinity is
# fleet parity. 2026-10-05 cutover from the ROCmFPX llama.cpp shape —
# rollback = revert the swap commit's trio; that payload, its draft head
# and the fork build all stay on disk
# (~/models/kingjones-Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP,
# ~/GitPackages/kingjones-ROCmFPX).
# fleet parity. 2026-10-06 cutover from the halogen 27B-dense shape —
# rollback = revert the swap commit's trio; the 27B payload stays on
# disk (~/models/halogen-qwen3.8-27b).
#
# The gates mirror serve-frank-large.sh's predicates EXACTLY (the
# 2026-10-01 crash-loop lesson): image digest in dank's podman storage,
# the .hgn checkpoint at the exact byte count, the tokenizer directory
# holding every named file as a regular (not symlinked) file — a green
# deploy against a missing artifact would
# leave a restart-looping unit and a dark endpoint. The port gate is the
# same as every cutover since 2026-09-16: nothing starts until 7770 is
# released.
LLM_LARGE_IMAGE="${LLM_LARGE_IMAGE:-ghcr.io/peonist-ai/halogen@sha256:dc0a39a0016d6cfc58a197978febaafdf8d28403f724ded6111d98b5fb7ac0ea}"
LLM_LARGE_CKPT="${LLM_LARGE_CKPT:-/home/dank/models/halogen-qwen3.8-27b/qwen3.8-27b-p1w4d-d2.hgn}"
LLM_LARGE_CKPT_BYTES="${LLM_LARGE_CKPT_BYTES:-35865565184}"
LLM_LARGE_TOK_DIR="${LLM_LARGE_TOK_DIR:-/home/dank/models/halogen-qwen3.8-27b/tokenizer}"
LLM_LARGE_TOK_FILES="${LLM_LARGE_TOK_FILES:-tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json}"
# each .hgn payload (checkpoint, n-gram table, vision tower) at the exact
# byte count, the tokenizer directory holding every named file as a
# regular (not symlinked) file — a green deploy against a missing
# artifact would leave a restart-looping unit and a dark endpoint. The
# port gate is the same as every cutover since 2026-09-16: nothing
# starts until 7770 is released.
LLM_LARGE_IMAGE="${LLM_LARGE_IMAGE:-ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a}"
LLM_LARGE_CKPT="${LLM_LARGE_CKPT:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-v2-swift15-abliterated.hgn}"
LLM_LARGE_CKPT_BYTES="${LLM_LARGE_CKPT_BYTES:-66687678464}"
LLM_LARGE_NGRAM="${LLM_LARGE_NGRAM:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-ngram.hgn}"
LLM_LARGE_NGRAM_BYTES="${LLM_LARGE_NGRAM_BYTES:-51200246144}"
LLM_LARGE_VISION="${LLM_LARGE_VISION:-/home/dank/models/halogen-flashnext-v2/qwen38-flash-next-vision.hgn}"
LLM_LARGE_VISION_BYTES="${LLM_LARGE_VISION_BYTES:-897916416}"
LLM_LARGE_TOK_DIR="${LLM_LARGE_TOK_DIR:-/home/dank/models/halogen-flashnext-v2/tokenizer}"
LLM_LARGE_TOK_FILES="${LLM_LARGE_TOK_FILES:-tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json generation_config.json}"
retire_halogen_large_quadlet() {
local trash="$HOME/.AGENTTRASH/20260919-orca-cutover/quadlet"
@@ -1156,23 +1160,37 @@ deploy_llm_large() {
fi
if [[ ! -f "$LLM_LARGE_CKPT" ]]; then
echo "ERROR: halogen checkpoint absent at $LLM_LARGE_CKPT" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-halogen.sh" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
return 1
fi
if [[ "$(stat -c %s "$LLM_LARGE_CKPT")" != "$LLM_LARGE_CKPT_BYTES" ]]; then
echo "ERROR: halogen checkpoint is not $LLM_LARGE_CKPT_BYTES bytes (partial download?)" >&2
echo "frank-large: re-check: bash scripts/download-frank-large-halogen.sh --verify-only" >&2
echo "frank-large: re-check: bash scripts/download-frank-large-flashnext.sh --verify-only" >&2
return 1
fi
for gate in "$LLM_LARGE_NGRAM:$LLM_LARGE_NGRAM_BYTES:n-gram table" \
"$LLM_LARGE_VISION:$LLM_LARGE_VISION_BYTES:vision tower"; do
IFS=: read -r gpath gbytes gname <<<"$gate"
if [[ ! -f "$gpath" ]]; then
echo "ERROR: halogen $gname absent at $gpath" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
return 1
fi
if [[ "$(stat -c %s "$gpath")" != "$gbytes" ]]; then
echo "ERROR: halogen $gname is not $gbytes bytes (partial download?)" >&2
echo "frank-large: re-check: bash scripts/download-frank-large-flashnext.sh --verify-only" >&2
return 1
fi
done
if [[ -L "$LLM_LARGE_TOK_DIR" || ! -d "$LLM_LARGE_TOK_DIR" ]]; then
echo "ERROR: tokenizer dir missing or a symlink at $LLM_LARGE_TOK_DIR" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-halogen.sh" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
return 1
fi
for tf in $LLM_LARGE_TOK_FILES; do
if [[ ! -f "$LLM_LARGE_TOK_DIR/$tf" || -L "$LLM_LARGE_TOK_DIR/$tf" ]]; then
echo "ERROR: tokenizer file missing or a symlink: $LLM_LARGE_TOK_DIR/$tf" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-halogen.sh" >&2
echo "frank-large: fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
return 1
fi
done
@@ -1184,7 +1202,7 @@ deploy_llm_large() {
# `podman run --name frank-large-halogen` / `podman wait` but NOT in
# this deployer's own command line (`bash deploy-remote.sh
# frank-large`) — so it cannot shoot itself in the foot (the fleet
# pkill law), and it can never match frank-small's llama-server.
# pkill law), and it can never match frank-secondary's llama-server.
run_privileged systemctl stop frank-large.service 2>/dev/null || true
pkill -f 'frank-large-halogen' 2>/dev/null || true
podman rm -f frank-large frank-large-halogen >/dev/null 2>&1 || true
@@ -1198,75 +1216,77 @@ deploy_llm_large() {
wait_for_health frank-large http://127.0.0.1:7770/health 1200
}
# deploy_llm_small — frank-small is llama.cpp (master build, CUDA sm_86) on
# deploy_llm_secondary — frank-secondary is llama.cpp (master build, CUDA sm_86) on
# the RTX 3090 as a ROOT unit: the runtime is a native binary tree under
# ~/GitPackages/llama-master/build-cuda, so there is no container image and
# no generated unit to avoid. relink_unit points
# /etc/systemd/system/frank-small.service at the repo file and boot
# /etc/systemd/system/frank-secondary.service at the repo file and boot
# persistence is that unit's [Install] section.
#
# It is a SEPARATE endpoint on port 7771; frank-large keeps 7770. The 3090 is
# not shared: frank-large's engine runs on the gfx1151 iGPU.
LLM_SMALL_MODEL_FILE="${LLM_SMALL_MODEL_FILE:-/home/dank/models/Ornith-Q8/Ornith-1.5-9B-Q8_0.gguf}"
LLM_SMALL_RUNTIME="${LLM_SMALL_RUNTIME:-/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server}"
# The projector IS the vision capability here: the Ornith GGUF ships no
# `v.*` tensors, so a deploy that starts without this file silently ships a
# text-only endpoint. Checked unconditionally, like frank-large's.
LLM_SMALL_MMPROJ="${LLM_SMALL_MMPROJ:-/home/dank/models/Ornith-Q8/mmproj-Ornith-1.5-9B-BF16.gguf}"
LLM_SECONDARY_MODEL_FILE="${LLM_SECONDARY_MODEL_FILE:-/home/dank/models/Swift15-UncIQ3XXS/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf}"
LLM_SECONDARY_RUNTIME="${LLM_SECONDARY_RUNTIME:-/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server}"
# The projector IS the vision capability here: the GGUF carries no vision
# tensors, and the publisher ships mmproj-BF16.gguf in the same repo, so a
# deploy that starts without this file silently ships a text-only
# endpoint. Checked unconditionally, like frank-large's.
LLM_SECONDARY_MMPROJ="${LLM_SECONDARY_MMPROJ:-/home/dank/models/Swift15-UncIQ3XXS/mmproj-BF16.gguf}"
deploy_llm_small() {
deploy_llm_secondary() {
# Refuse rather than restart-loop: a unit whose runtime or payload is
# missing never becomes healthy, and a green deploy would leave the
# endpoint dark with nothing in the log to say why.
if [[ ! -x "$LLM_SMALL_RUNTIME" ]]; then
echo "ERROR: llama.cpp runtime absent at $LLM_SMALL_RUNTIME" >&2
echo "frank-small: build it (cmake -B build-cuda -DGGML_CUDA=ON" >&2
echo "frank-small: -DCMAKE_CUDA_ARCHITECTURES=86 -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13" >&2
echo "frank-small: && cmake --build build-cuda --target llama-server) first." >&2
if [[ ! -x "$LLM_SECONDARY_RUNTIME" ]]; then
echo "ERROR: llama.cpp runtime absent at $LLM_SECONDARY_RUNTIME" >&2
echo "frank-secondary: build it (cmake -B build-cuda -DGGML_CUDA=ON" >&2
echo "frank-secondary: -DCMAKE_CUDA_ARCHITECTURES=86 -DCMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13" >&2
echo "frank-secondary: && cmake --build build-cuda --target llama-server) first." >&2
return 1
fi
for f in "$LLM_SMALL_MODEL_FILE" "$LLM_SMALL_MMPROJ"; do
for f in "$LLM_SECONDARY_MODEL_FILE" "$LLM_SECONDARY_MMPROJ"; do
if [[ ! -f "$f" ]]; then
echo "ERROR: frank-small payload file absent at $f" >&2
echo "frank-small: run scripts/download-frank-small.sh first;" >&2
echo "frank-small: refusing to start a unit that cannot load." >&2
echo "ERROR: frank-secondary payload file absent at $f" >&2
echo "frank-secondary: run scripts/download-frank-secondary.sh first;" >&2
echo "frank-secondary: refusing to start a unit that cannot load." >&2
return 1
fi
done
relink_unit frank-small
relink_unit frank-secondary
run_privileged systemctl daemon-reload
run_privileged systemctl enable frank-small.service
run_privileged systemctl restart frank-small.service
wait_for_health frank-small http://127.0.0.1:7771/health 900 || return 1
run_privileged systemctl enable frank-secondary.service
run_privileged systemctl restart frank-secondary.service
wait_for_health frank-secondary http://127.0.0.1:7771/health 900 || return 1
# False-green hardening (frank-large's lesson): the port answering
# proves nothing about WHO answers — the unit must be active with a
# live MainPID, or a stray SGLang holdover is masquerading as deployed.
local main_pid
main_pid="$(run_privileged systemctl show -p MainPID --value frank-small.service)"
main_pid="$(run_privileged systemctl show -p MainPID --value frank-secondary.service)"
if [[ -z "$main_pid" || "$main_pid" == "0" ]]; then
echo "FAIL: :7771 answered but frank-small.service has no MainPID — a stray process holds the port" >&2
echo "FAIL: :7771 answered but frank-secondary.service has no MainPID — a stray process holds the port" >&2
return 1
fi
echo "frank-small: health answered by unit MainPID $main_pid"
echo "frank-secondary: health answered by unit MainPID $main_pid"
}
# deploy_llm_primary — frank-primary is the FreeToken engine (PR #587 ROCm
# build, gfx1201) serving the orcarouter Qwen3.8-Flash-Next-Uncensored-FP8
# payload (131 safetensors shards) on ONE R9700 as a ROOT unit whose serve
# script drives `podman run` directly (the image carries `ft serve`).
# 2026-10-05 cutover from the tcclaviger vLLM fork (pre-cutover tag
# frank-primary-vllm-shapeG-20261003); before that, 2026-09-29 cutover from
# the R9V radiance fork, whose launcher/shim/setup-state machinery is
# retired. Same port and alias as the endpoints it replaced; the harness
# models.yml id is unchanged.
# deploy_llm_primary — frank-primary is the radiance engine (StillDeadcode,
# digest-pinned stilldeadcode/radiance image) serving the OFFICIAL
# Qwen/Qwen3.8-Flash-Next-FP8 at native block-FP8 precision from the
# rad-convert container qwen4exp-fp8-native.rad, on BOTH R9700s (TP=2) as a
# ROOT unit whose serve script drives `podman run` directly (the engine is
# the image entrypoint; the image lives in dank's rootless podman storage).
# 2026-10-05 cutover from FreeToken (pre-cutover tag
# frank-primary-freetoken-pr587-20261005); before that, the tcclaviger vLLM
# fork (tag frank-primary-vllm-shapeG-20261003). Same port and alias; the
# harness models.yml id is unchanged.
#
# Gates: the runtime image must exist in dank's rootless storage
# (FRANK_PRIMARY_IMAGE — a LOCAL tag: the image is built on the host from
# scripts/docker/frank-primary-freetoken.Dockerfile, which pins the
# FreeToken PR-head SHA and the base image), the payload dir must carry
# config.json and the expected shard count (FRANK_PRIMARY_MODEL_SHARDS),
# and the unit env must be complete. Per-file sha256 verification was the
# fetch script's job at download time, never re-hashed here.
# Gates: the image must exist under its digest ref in dank's rootless
# storage (FRANK_PRIMARY_IMAGE), the .rad container file must exist at the
# env's size floor (FRANK_PRIMARY_MODEL_FILE >= FRANK_PRIMARY_RAD_MIN_MIB —
# a truncated convert is a guaranteed long-timeout boot), and the unit env
# must be complete. Per-file sha256 verification is the fetch script's job
# at download time and rad-convert/rad-info's job at build time, never
# re-hashed here.
LLM_PRIMARY_CONTAINER=frank-primary
LLM_PRIMARY_LEGACY_CONTAINERS="frank-primary-qwen frank-primary-r9v qwen-fp8 glm-probe"
@@ -1275,44 +1295,36 @@ deploy_llm_primary() {
# missing never becomes healthy, and a green deploy would leave the
# endpoint dark with nothing in the log to say why.
local env_file="/home/dank/serverAppConfigs/scripts/frank-primary.env"
local image model_dir shards count
local image model_dir model_file rad_min mib
if [[ ! -f "$env_file" ]]; then
echo "ERROR: frank-primary env file absent: $env_file" >&2
return 1
fi
image="$(sed -n 's/^FRANK_PRIMARY_IMAGE=//p' "$env_file" | tail -1)"
model_dir="$(sed -n 's/^FRANK_PRIMARY_MODEL_DIR=//p' "$env_file" | tail -1)"
shards="$(sed -n 's/^FRANK_PRIMARY_MODEL_SHARDS=//p' "$env_file" | tail -1)"
if [[ -z "$image" || -z "$model_dir" || -z "$shards" ]]; then
echo "ERROR: frank-primary.env missing FRANK_PRIMARY_IMAGE/MODEL_DIR/MODEL_SHARDS" >&2
model_file="$(sed -n 's/^FRANK_PRIMARY_MODEL_FILE=//p' "$env_file" | tail -1)"
rad_min="$(sed -n 's/^FRANK_PRIMARY_RAD_MIN_MIB=//p' "$env_file" | tail -1)"
if [[ -z "$image" || -z "$model_dir" || -z "$model_file" || -z "$rad_min" ]]; then
echo "ERROR: frank-primary.env missing FRANK_PRIMARY_IMAGE/MODEL_DIR/MODEL_FILE/RAD_MIN_MIB" >&2
return 1
fi
# The image gate is EXISTENCE IN STORAGE, not a registry digest: the
# FreeToken image is built ON THE HOST (a local tag carries NO
# RepoDigests entry — the old grep refused exactly this shape), and
# pinning lives in the Dockerfile (pinned PR-head SHA + digest-pinned
# base image). `podman image exists` accepts a local tag and a
# name@digest ref alike, in one command — no pipefail/grep race (the
# 2026-09-30 lesson) for either shape.
# The image gate is the digest ref itself: a name@sha256 ref exists in
# storage only if THAT manifest was pulled. One command — no
# pipefail/grep race (the 2026-09-30 lesson).
if ! podman image exists "$image"; then
echo "ERROR: runtime image absent from rootless storage: $image" >&2
echo "frank-primary: build it (scripts/docker/frank-primary-freetoken.Dockerfile) or pull it" >&2
echo "ERROR: runtime image absent from storage: $image" >&2
echo "frank-primary: podman pull docker://stilldeadcode/radiance:latest" >&2
return 1
fi
if [[ ! -f "$model_dir/config.json" ]]; then
echo "ERROR: payload config missing: $model_dir/config.json" >&2
echo "frank-primary: re-run scripts/download-frank-primary.sh" >&2
if [[ ! -f "$model_dir/$model_file" ]]; then
echo "ERROR: .rad container missing: $model_dir/$model_file" >&2
echo "frank-primary: run scripts/build-frank-primary-rad.sh" >&2
return 1
fi
if [[ ! -f "$model_dir/model.safetensors.index.json" ]]; then
echo "ERROR: weight-map index missing: $model_dir/model.safetensors.index.json" >&2
echo "frank-primary: re-run scripts/download-frank-primary.sh" >&2
return 1
fi
count="$(find "$model_dir" -maxdepth 1 -name 'model-*-of-*.safetensors' 2>/dev/null | wc -l)"
if [[ "$count" != "$shards" ]]; then
echo "ERROR: payload shard count wrong: $model_dir (found $count, want $shards)" >&2
echo "frank-primary: re-run scripts/download-frank-primary.sh" >&2
mib=$(( $(stat -c %s "$model_dir/$model_file") / 1048576 ))
if [[ "$mib" -lt "$rad_min" ]]; then
echo "ERROR: .rad container too small: $model_dir/$model_file is ${mib} MiB, want >= $rad_min" >&2
echo "frank-primary: rebuild it (scripts/build-frank-primary-rad.sh --force)" >&2
return 1
fi
@@ -1337,18 +1349,16 @@ deploy_llm_primary() {
run_privileged systemctl daemon-reload
run_privileged systemctl enable frank-primary.service
run_privileged systemctl restart frank-primary.service
# Cold start: FP8 page-in plus torch compile/graph capture (~15 min cold,
# measured 2026-09-29). Parity with every predecessor on this port.
# FreeToken's /health returns 200 with {"status":"loading"} while the
# engine loads (the FastAPI front end binds before the scheduler is
# up), so a bare 200 false-greens — measured 2026-10-06: "healthy after
# 6 s" while 131 shards were still paging in. vLLM 503'd during load,
# which made the bare curl honest there; FreeToken needs the BODY read.
# Only status:"ok" counts. (The MainPID check below still guards the
# stray-port case.)
# Cold start: .rad load + PLE pin + expert host-pool upload (the
# publisher's compose healthcheck allows a 15-minute start_period).
# Parity with every predecessor on this port. The HTTP listener binds
# AFTER the full load (main.cpp: engine.start() then engine.serve()),
# so during load the port REFUSES connections — a bare curl is honest
# here, unlike FreeToken's 200-while-loading body. (The MainPID check
# below still guards the stray-port case.)
local i ok=0
for ((i=1; i<=900; i++)); do
if curl -fsS http://127.0.0.1:7770/health 2>/dev/null | grep -q '"status":"ok"'; then
if curl -fsS http://127.0.0.1:7770/health 2>/dev/null >/dev/null; then
echo "frank-primary engine healthy after approximately $((i * 2))s"
ok=1
break
@@ -1356,7 +1366,7 @@ deploy_llm_primary() {
sleep 2
done
if [[ "$ok" != 1 ]]; then
echo "FAIL: frank-primary never reached /health status ok in 1800s" >&2
echo "FAIL: frank-primary /health never answered 200 in 1800s" >&2
return 1
fi
# False-green hardening (the 2026-09-26 lesson): a bare curl against the
@@ -1557,7 +1567,7 @@ deploy_service() {
seerr) deploy_seerr ;;
marreta) deploy_marreta ;;
frank-large) deploy_llm_large ;;
frank-small) deploy_llm_small ;;
frank-secondary) deploy_llm_secondary ;;
frank-primary) deploy_llm_primary ;;
hermes|hermes-dashboard|hermes-webui) deploy_hermes ;;
romm|romm-mariadb|romm-redis|mariadb|redis) deploy_romm_stack "$service" ;;
+1 -1
View File
@@ -141,7 +141,7 @@ valid_service() {
dankclaw:alertmanager|\
dankclaw:node-exporter|dankclaw:authentik-postgres|dankclaw:authentik-server|\
dankclaw:authentik-worker|dankclaw:authentik-outpost|dankclaw:authentik-outpost-ldap|dankclaw:anubis|dankclaw:seerr|\
dankclaw:frank-large|dankclaw:frank-small|\
dankclaw:frank-large|dankclaw:frank-secondary|\
dankserver:romm-mariadb|dankserver:romm-redis|dankserver:romm|\
dankserver:copyparty|dankserver:jellyfin|dankserver:calibre-web|dankserver:calibre-server|dankserver:plex|dankserver:tautulli|\
dankserver:qbittorrent|dankserver:radarr|dankserver:sonarr|\
@@ -1,63 +0,0 @@
# frank-primary-freetoken.Dockerfile — FreeToken (PR #587 ROCm) serving image
# for the frank-primary endpoint (dankserver :7770, 1x R9700 gfx1201).
#
# Built on dankserver with rootless podman (dank user), same storage the
# service's `podman run` reads:
# podman build -t frank-primary-freetoken:pr587-77a7201 \
# -f scripts/docker/frank-primary-freetoken.Dockerfile scripts/docker/
#
# Base: the PR's own docs/install_amd.md names this exact image as the
# known-good ROCm 7.14 stack for gfx1201 (torch 2.11.0+rocm7.14, triton-rocm
# 3.7 — the original pin; FreeToken main+PR587 accepts torch>=2.11,<2.16 and
# triton>=3.6,<3.9). There is no rocm7.14+torch2.14 image on Docker Hub (the
# 2.14 images are rocm10.1.0 — an untested pair for this PR), so the PR doc's
# verbatim recommendation is the deployed stack.
#
# FreeToken source: FlashML-org/FreeToken PR #587 at pinned head
# 77a7201f98f00d93f618c6cf8b81ffe8360e0899 (branch rocm714_torch214, base
# main 581fdca3). The PR is ROCm-gated end to end; the NVIDIA/CUDA path is
# byte-identical to main.
#
# Build law from docs/install_amd.md:
# - `pip install --no-build-isolation --no-deps -e .` — the image's ROCm
# torch is preserved and used to compile the C++/HIP extensions.
# - NEVER a plain `pip install -e .` on ROCm: the dependency set is
# authored for NVIDIA (flashlib pulls numba + nvidia-cutlass-dsl; torch
# metadata pulls cuda-bindings) and the resolve can REPLACE the ROCm
# torch with a CUDA build.
# - Runtime deps installed explicitly, curated by the same doc. flashinfer
# / sgl_kernel / nvidia-cutlass-dsl have no ROCm builds and are
# intentionally absent — FreeToken gates them off and falls back to its
# Triton kernels on ROCm.
#
# Base is digest-pinned (2026-10-05): sha256:a223aee1… is the manifest this
# build was qualified against; a re-pushed tag cannot change the stack. The
# readable tag rides along for humans (podman verifies the digest matches).
FROM rocm/pytorch:rocm7.14_ubuntu24.04_py3.12_pytorch_release_2.11.0@sha256:a223aee17aef5d21c3b9f63436dd19d27d1c665ec8b2f40011c9546cabae2a80
# Pin the PR head, not the branch: a future push to rocm714_torch214 would
# otherwise silently change the deployed engine.
# The pip-ROCm torch in this image ships libhiprtc as the VERSIONED
# libhiprtc.so.7 only; the PR's `_cpu_moe` extension links `-lhiprtc`, and
# ld looks for the unversioned dev symlink in the ALREADY-LINKED
# _rocm_sdk_core/lib dir. Create it there (first build failure 2026-10-05:
# `ld: cannot find -lhiprtc`).
RUN ln -s libhiprtc.so.7 /opt/venv/lib/python3.12/site-packages/_rocm_sdk_core/lib/libhiprtc.so
RUN git clone https://github.com/FlashML-org/FreeToken.git /opt/FreeToken \
&& cd /opt/FreeToken \
&& git fetch origin pull/587/head \
&& git checkout 77a7201f98f00d93f618c6cf8b81ffe8360e0899
WORKDIR /opt/FreeToken
RUN python -m pip install --no-build-isolation --no-deps -e .
# Curated runtime dependencies (docs/install_amd.md). transformers is pinned
# to the pyproject window (>=5.16,<5.17); the rest are the doc's portable set.
# torch / torchvision / triton-rocm come from the base image, 1:1 aligned.
RUN python -m pip install \
"transformers>=5.16,<5.17" tokenizers safetensors huggingface_hub \
fastapi uvicorn pydantic openai httpx anyio \
einops gguf msgpack pyzmq typer rich apache-tvm-ffi \
partial-json-parser \
&& python -m pip install --no-deps flashlib==0.3.0 \
&& python -m pip install ninja
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env bash
# download-frank-large-flashnext.sh — fetch frank-large's flash-server v2
# payload: the Quat3rnion abliterated Swift1.5 checkpoint of peonist's v2
# .hgn, the REQUIRED lookup table and the optional vision tower from
# peonist-ai/halogen-qwen3.8-flash-next (both at the revisions the model
# card pins), and the flat tokenizer the two repos share. All public, no
# token — and per the fleet secret law, none would ever touch argv.
#
# The fetch law, same as every sibling script: curl -f (an access error
# must not become a "successful" 153-byte artifact), exact byte count from
# the Hub tree API, LFS sha256 verification, --verify-only to re-check an
# existing payload without downloading.
#
# File manifest (verified against the Hub tree API on 2026-10-06):
# qwen38-flash-next-v2-swift15-abliterated.hgn 66,687,678,464 B sha256
# 9c880b8ba48deabfa06172eff1e7572e4fe465e09e97faec6e50c3f6fdcac037
# (Quat3rnion main; compacted r2 of the card's 89,775,868,032 B
# append-only original — byte-identical live tensors, dropped padding)
# qwen38-flash-next-ngram.hgn 51,200,246,144 B sha256
# 3450acada94e19aabad88bd49b45eb70304820949c2240fc178067b20ae5dffe
# (peonist @5cc17cea; REQUIRED — the engine reads it beside the
# checkpoint)
# qwen38-flash-next-vision.hgn 897,916,416 B sha256
# d62e0ae553fe88afd3833733d4a4c669f34d20fd8dfce4b9610525bed2134b10
# (peonist @e053f488; optional, enables image input)
# tokenizer/* @e053f488 — byte-identical to the 27B-era tokenizer
# already on disk (tokenizer.json LFS
# 0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3);
# fetched fresh rather than symlinked, per the no-symlink law.
set -euo pipefail
DEST="${FRANK_LARGE_MODEL_DIR:-$HOME/models/halogen-flashnext-v2}"
ABLIT="https://huggingface.co/Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated/resolve/main"
PEONIST="https://huggingface.co/peonist-ai/halogen-qwen3.8-flash-next/resolve"
NGRAM_REV="5cc17cea1a10b502b4a5db41d8d2b1a0f22dba26"
TOK_REV="e053f488b120b99ed2525e6ac99f68c51b3b6179"
FILES=(qwen38-flash-next-v2-swift15-abliterated.hgn
qwen38-flash-next-ngram.hgn
qwen38-flash-next-vision.hgn
tokenizer/tokenizer.json
tokenizer/chat_template.jinja
tokenizer/generation_config.json
tokenizer/merges.txt
tokenizer/vocab.json
tokenizer/tokenizer_config.json)
SIZES=(66687678464
51200246144
897916416
12809320
8952
202
3353259
6722759
17928)
SHAS=(9c880b8ba48deabfa06172eff1e7572e4fe465e09e97faec6e50c3f6fdcac037
3450acada94e19aabad88bd49b45eb70304820949c2240fc178067b20ae5dffe
d62e0ae553fe88afd3833733d4a4c669f34d20fd8dfce4b9610525bed2134b10
0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3
- - - - - -)
# Fetch source per file: repo base + revision (empty = ABLIT @main).
SRCS=("$ABLIT"
"$PEONIST/$NGRAM_REV"
"$PEONIST/$TOK_REV"
"$PEONIST/$TOK_REV"
"$PEONIST/$TOK_REV"
"$PEONIST/$TOK_REV"
"$PEONIST/$TOK_REV"
"$PEONIST/$TOK_REV"
"$PEONIST/$TOK_REV")
VERIFY_ONLY=0
[ "${1:-}" = "--verify-only" ] && VERIFY_ONLY=1
mkdir -p "$DEST"
for i in "${!FILES[@]}"; do
f="${FILES[$i]}"; want="${SIZES[$i]}"; sha="${SHAS[$i]}"; base="${SRCS[$i]}"
path="$DEST/$f"
mkdir -p "$(dirname "$path")"
if [ "$VERIFY_ONLY" -eq 0 ]; then
# A file already at the published size skips the fetch entirely:
# resuming it would ask the CDN for a range past EOF (416 -> curl
# error under set -e), and re-pulling 62 GiB to prove a point is
# not what --verify-only is for. sha256 below still decides.
if [ -f "$path" ] && [ "$(stat -c %s "$path")" = "$want" ]; then
echo "HAVE $f ($want bytes) — skipping fetch"
else
echo "FETCH $f -> $path"
# HF answers /resolve/ with a 302 to the Xet CDN and curl DROPS
# the Range header across redirects — a naive -C - then
# "resumes" by pulling the ~1 KB redirect body (measured
# 2026-10-05 on the 27B fetch: file frozen 14 MiB short, curl
# exit 0). Resolve the effective URL first, then resume
# against the CDN, which honors Range.
url=$(curl -sfIL -o /dev/null -w '%{url_effective}' "$base/$f") \
|| { echo "cannot resolve $base/$f" >&2; exit 1; }
curl -fL --retry 5 -C - -o "$path" "$url"
fi
fi
[ -f "$path" ] || { echo "MISSING $path (run without --verify-only)" >&2; exit 1; }
got="$(stat -c %s "$path")"
[ "$got" = "$want" ] || { echo "SIZE $path: $got != $want" >&2; exit 1; }
echo "OK $f ($got bytes)"
done
echo "byte counts match; verifying sha256 where the Hub publishes an LFS oid..."
for i in "${!FILES[@]}"; do
f="${FILES[$i]}"; sha="${SHAS[$i]}"
if [ "$sha" = "-" ]; then
echo "OK $f (plain git blob, size-checked)"
continue
fi
got="$(sha256sum "$DEST/$f" | cut -d' ' -f1)"
[ "$got" = "$sha" ] || { echo "SHA256 $f: $got != $sha" >&2; exit 1; }
echo "OK $f sha256"
done
echo "DOWNLOAD_DONE $DEST"
+84 -61
View File
@@ -1,88 +1,111 @@
#!/usr/bin/env bash
# Payload inventory/verification for frank-primary:
# orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8 (compressed-tensors FP8, 131
# shards + weight-map index, ~172.8 GiB) — the tcclaviger-stack payload that
# became frank-primary on 2026-09-29.
# download-frank-primary.sh — payload fetch/verify for frank-primary:
# the OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint (131 safetensors
# shards + weight-map index + tokenizer + vision preprocessing configs,
# ~172.8 GB). 2026-10-05: replaces the orcarouter uncensored payload of the
# FreeToken era (that script lived here; its repo was gated and needed
# HF_TOKEN — this one is UNGATED and needs no token at all).
#
# bash scripts/download-frank-primary.sh --verify-only (the ONLY mode)
# bash scripts/download-frank-primary.sh --fetch
# bash scripts/download-frank-primary.sh --fetch resume/complete
# bash scripts/download-frank-primary.sh verify only
#
# THIS REPO IS GATED (401 anonymous — measured 2026-09-29): anonymous fetch
# is impossible and the tree API masks LFS hashes, so a byte-exact upstream
# manifest cannot be built without the authed endpoint. The payload on
# /home/dank/models was fetched with the operator's HF token (out of band:
# HF_TOKEN in the environment, never in argv) at bring-up time.
#
# Modes:
# --fetch resume any missing/truncated shard via `hf download` with
# HF_TOKEN from the environment (NEVER passed on the command
# line — a -H auth header is world-readable in
# /proc/<pid>/cmdline, the 2026-09-16 leak).
# --verify-only default: local integrity check — shard count vs the
# weight-map index, every file nonzero and its size equal to
# the index's declared total, plus sha256 of every shard
# (slow, ~10 min for 173 GiB).
# Fetch uses the vendored scripts/hfget.sh (StillDeadcode's Apache-2.0
# listing/resume downloader, curl+jq only): per-file size checks against
# the Hub listing, `-C -` resume, pickle-format duplicates skipped.
# Verify (default) is the AGENTS.md law in three parts: every listed file
# present with the listed size, the shard count matches the weight-map
# index, and every shard's sha256 equals the Hub's LFS oid. Verify needs
# only the public API.
set -euo pipefail
REPO="orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8"
DEST="/home/dank/models/Qwen3.8-Flash-Next-Uncensored-FP8"
REPO="Qwen/Qwen3.8-Flash-Next-FP8"
DEST="/home/dank/models/Qwen/Qwen3.8-Flash-Next-FP8"
EXPECTED_SHARDS=131
mode="verify"
[[ "${1:-}" == "--fetch" ]] && mode="fetch"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
if [[ "$mode" == "fetch" ]]; then
: "${HF_TOKEN:?--fetch needs HF_TOKEN in the environment (never argv)}"
export HF_TOKEN
mkdir -p "$DEST"
hf download "$REPO" --local-dir "$DEST"
# HFGET_JOBS=4 is hfget's default (a few streams saturate the link).
bash "$SCRIPT_DIR/hfget.sh" "$REPO" "$DEST" main
fi
echo "verifying local payload: $DEST"
fail=0
# The weight-map index is the source of truth for the shard set.
INDEX="$DEST/model.safetensors.index.json"
if [[ ! -f "$INDEX" ]]; then
echo " BAD weight-map index missing: $INDEX" >&2
exit 1
fi
# Every index entry must exist, nonzero, and sized equal to the map's total.
python3 - "$INDEX" "$DEST" <<'PY' || fail=1
import json, os, sys
index, dest = sys.argv[1], sys.argv[2]
wm = json.load(open(index))["weight_map"]
files = sorted(set(wm.values()))
print(f" index maps {len(wm)} tensors across {len(files)} files")
python3 - "$REPO" "$DEST" <<'PY' || fail=1
import hashlib, json, os, sys, urllib.request
repo, dest = sys.argv[1], sys.argv[2]
def listing():
"""The tree API, following the Link cursor exactly like hfget.sh."""
out, api, pages = [], (
f"https://huggingface.co/api/models/{repo}/tree/main"
"?recursive=true&expand=true"), 0
while api:
with urllib.request.urlopen(api) as r:
out += json.load(r)
api = None
link = r.headers.get("Link", "")
for part in link.split(","):
if 'rel="next"' in part:
api = part[part.find("<") + 1:part.find(">")]
pages += 1
if pages > 200:
raise SystemExit("listing did not terminate")
return out
files = [e for e in listing() if e.get("type") == "file"]
sizes = {e["path"]: e.get("size", 0) for e in files}
lfs = {e["path"]: e["lfs"]["oid"] for e in files if e.get("lfs")}
bad = 0
total = 0
for f in files:
p = os.path.join(dest, f)
if not os.path.isfile(p) or os.path.getsize(p) == 0:
print(f" BAD {f}: missing or zero bytes" )
for path, want in sorted(sizes.items()):
p = os.path.join(dest, path)
if not os.path.isfile(p):
print(f" BAD {path}: missing")
bad += 1
else:
total += os.path.getsize(p)
print(f" {'OK' if bad == 0 else 'FAIL'}: {len(files) - bad}/{len(files)} files present, {total/2**30:.1f} GiB total")
raise SystemExit(1 if bad else 0)
elif os.path.getsize(p) != want:
print(f" BAD {path}: {os.path.getsize(p)} bytes, listing says {want}")
bad += 1
print(f" {'OK' if bad == 0 else 'FAIL'}: {len(sizes) - bad}/{len(sizes)} files present at listed size")
if bad:
raise SystemExit(1)
# Shard set against the weight-map index.
wm = json.load(open(os.path.join(dest, "model.safetensors.index.json")))["weight_map"]
shards = sorted(set(wm.values()))
print(f" index maps {len(wm)} tensors across {len(shards)} shards")
if len(shards) != 131:
print(f" BAD shard count {len(shards)}, want 131")
raise SystemExit(1)
# Content: every LFS-tracked file hashed against the Hub oid. The safetensors
# shards dominate the bytes; tokenizer/config files ride the same check when
# the Hub tracks them in LFS (they are plain blobs here, already size-checked).
todo = [p for p in shards if p in lfs]
for i, path in enumerate(todo):
h = hashlib.sha256()
with open(os.path.join(dest, path), "rb") as f:
for chunk in iter(lambda: f.read(1 << 24), b""):
h.update(chunk)
if h.hexdigest() != lfs[path]:
print(f" BAD {path}: sha256 mismatch (want {lfs[path]})")
bad += 1
if (i + 1) % 16 == 0:
print(f" ...hashed {i + 1}/{len(todo)}")
print(f" {'OK' if bad == 0 else 'FAIL'}: {len(todo) - bad}/{len(todo)} shards sha256-match the Hub")
if bad:
raise SystemExit(1)
PY
# Shard count gate (what the serve script and deployer check).
n="$(find "$DEST" -maxdepth 1 -name 'model-*-of-*.safetensors' | wc -l)"
if [[ "$n" != "$EXPECTED_SHARDS" ]]; then
echo " BAD shard count: found $n, want $EXPECTED_SHARDS" >&2
fail=1
else
echo " OK shard count $n"
fi
if [[ "${2:-}" == "--hash" || "${VERIFY_HASH:-0}" == "1" ]]; then
echo "hashing every shard (slow)..."
cd "$DEST"
sha256sum -c --quiet model.safetensors.index.json 2>/dev/null || \
sha256sum model-*-of-00131.safetensors > /dev/null # presence check; content hash list printed on demand
fi
[[ "$fail" -eq 0 ]] || exit 1
echo "payload inventory OK: $EXPECTED_SHARDS shards + index present"
echo "payload verified: 131 shards + index + tokenizer + configs, sizes and sha256 checked"
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# Download the frank-secondary payload for llama.cpp on the RTX 3090:
# ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF:RCO-IQ3_XXS
# **with the MTP head in the file** (the -mtp variant), plus the
# PUBLISHER's OWN vision projector (mmproj-BF16.gguf, same repo). Public
# repo — no auth header, and never a token on the command line (it is
# world-readable in /proc/<pid>/cmdline).
#
# Fresh downloads, no resume (`curl -C -` once produced a 213 MB oversized
# file on a 31 GiB payload); -f/--fail-with-body so an HTTP error is not a
# success, then byte count and sha256. This repo is on Xet storage, so
# the Hub tree/blob APIs expose no LFS oid: the sha256 values below were
# computed locally after the first verified fetch (byte counts match the
# Hub's declared sizes). Idempotent: complete files are skipped. The
# projector is stored under a self-documenting dest name; the URL name
# and sha stay pinned in the table below.
#
# 2026-10-06 (Frank, evening): the payload moved to the UNCELERSED
# Swift-1.5-Qwen3.8-27B at RCO-IQ3_XXS (1.56 GiB lighter than the
# IQ3_S-mtp it replaced; the lighter quant funds the MTP draft ctx plus
# the second 131,072 slot). Rollback payload stays on disk:
# /home/dank/models/Swift15-IQ3S/ (the IQ3_S-mtp that was live until this
# swap); earlier payloads Ornith-Q8/ and MiMo-Q6K/ as well. Their
# re-fetch recipes are in git history (predecessors of this script).
#
# Usage:
# scripts/download-frank-secondary.sh [DEST_DIR] [--verify-only]
set -euo pipefail
DEST=""
VERIFY_ONLY=0
for arg in "$@"; do
case "$arg" in
--verify-only) VERIFY_ONLY=1 ;;
*) DEST="$arg" ;;
esac
done
DEST="${DEST:-/home/dank/models/Swift15-UncIQ3XXS}"
# repo | hub-name | dest-name | bytes | sha256 (computed locally; Xet repo)
FILES=(
"ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF|RCO/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf|Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf|10442827392|e13d3e7400b6c7f980ebd30c0edd6be43eb1865b50be8562a9db042706c7d803"
"ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF|mmproj-BF16.gguf|mmproj-BF16.gguf|931145888|19acd7fcf4ee504328566eddf0f8e603ef853a205b4830209d2a4908213abdea"
)
mkdir -p "$DEST"
status=0
for entry in "${FILES[@]}"; do
IFS='|' read -r repo hubname destname size sha <<<"$entry"
url="https://huggingface.co/${repo}/resolve/main/${hubname}"
f="$DEST/$destname"
have=0
if [[ -f "$f" ]] && [[ "$(stat -c%s "$f")" == "$size" ]]; then
if sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
have=1
fi
fi
if (( have )); then
echo "OK $destname"
continue
fi
if (( VERIFY_ONLY )); then
echo "MISSING $destname"
status=1
continue
fi
echo "FETCHING $destname (from $repo)"
tmp="$f.part"
curl -fsSL --fail-with-body -o "$tmp" "$url"
mv "$tmp" "$f"
if [[ "$(stat -c%s "$f")" != "$size" ]]; then
echo "SIZE FAIL $destname ($(stat -c%s "$f") != $size)"
status=1
continue
fi
if ! sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
echo "SHA FAIL $destname"
status=1
continue
fi
echo "VERIFIED $destname"
done
if (( status == 0 )); then
echo "DOWNLOAD_DONE $DEST"
else
echo "DOWNLOAD_FAILED $DEST" >&2
fi
exit "$status"
-87
View File
@@ -1,87 +0,0 @@
#!/usr/bin/env bash
# Download the frank-small payload for llama.cpp on the RTX 3090:
# ornith-ai/Ornith-1.5-9B-GGUF:Q8_0 plus its vision projector
# (the GGUF carries no `v.*` tensors — the tower lives only in the mmproj, so
# without this file the endpoint is text-only by construction). Public repo —
# no auth header, and never a token on the command line (it is world-readable
# in /proc/<pid>/cmdline).
#
# Fresh downloads, no resume (`curl -C -` once produced a 213 MB oversized file
# on a 31 GiB payload); -f/--fail-with-body so an HTTP 401 is not a success,
# then byte count and the Hub LFS sha256. Idempotent: complete files are
# skipped.
#
# 2026-10-05 (Frank): the rung is Ornith-1.5-9B Q8_0. The payload moved off
# MiMo-V2.6 (Q6_K) to Ornith because it is the fleet's own model and it is the
# only one the harness's own prompt recipes were written against; Q8_0 was the
# rung chosen so the quant does not cost the throughput this endpoint already
# runs at. The Q5_K_M file this script served from 2026-10-02 stays on disk at
# /home/dank/models/MiMo-Q5KM/ (6876124704 B, LFS oid
# 04cf46f2b5584ef922697d6c063f94879434fb86c467f1a09e52daf0cddaf35f) as the
# rollback target; re-fetch it from the same repo with the same recipe.
#
# Usage:
# scripts/download-frank-small.sh [DEST_DIR] [--verify-only]
set -euo pipefail
REPO="ornith-ai/Ornith-1.5-9B-GGUF"
BASE="https://huggingface.co/${REPO}/resolve/main"
DEST=""
VERIFY_ONLY=0
for arg in "$@"; do
case "$arg" in
--verify-only) VERIFY_ONLY=1 ;;
*) DEST="$arg" ;;
esac
done
DEST="${DEST:-/home/dank/models/Ornith-Q8}"
# name bytes sha256 (the sha256 is the LFS oid in the repo's pointer file)
FILES=(
"Ornith-1.5-9B-Q8_0.gguf 9786060384 22086870b009dbe9815ee752c48a82de930118a7c5ce5599590892ae03b8b010"
"mmproj-Ornith-1.5-9B-BF16.gguf 921704672 626f9f90627402a6bf4a999111d0fbd69b5fcca7aa8ba089d69e5f10e8858e1d"
)
mkdir -p "$DEST"
status=0
for entry in "${FILES[@]}"; do
read -r name size sha <<<"$entry"
f="$DEST/$name"
have=0
if [[ -f "$f" ]] && [[ "$(stat -c%s "$f")" == "$size" ]]; then
if sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
have=1
fi
fi
if (( have )); then
echo "OK $name"
continue
fi
if (( VERIFY_ONLY )); then
echo "MISSING $name"
status=1
continue
fi
echo "FETCHING $name"
tmp="$f.part"
curl -fsSL --fail-with-body -o "$tmp" "$BASE/$name"
mv "$tmp" "$f"
if [[ "$(stat -c%s "$f")" != "$size" ]]; then
echo "SIZE FAIL $name ($(stat -c%s "$f") != $size)"
status=1
continue
fi
if ! sha256sum -c --quiet <(printf '%s %s\n' "$sha" "$f"); then
echo "SHA FAIL $name"
status=1
continue
fi
echo "VERIFIED $name"
done
if (( status == 0 )); then
echo "DOWNLOAD_DONE $DEST"
else
echo "DOWNLOAD_FAILED $DEST" >&2
fi
exit "$status"
+120 -88
View File
@@ -1,75 +1,88 @@
# frank-large.env — tunables for the frank-large endpoint on Dankclaw's
# Strix Halo iGPU (gfx1151), host port 7770.
#
# RUNTIME (2026-10-05, Frank): halogen — the closed-source C++/HIP engine
# (github.com/peonist-ai/halogen-server, image ghcr.io/peonist-ai/halogen,
# digest-pinned below) serving peonist-ai/halogen-qwen3.8-27b, a ~6.32 bpw
# NVFP4-derivative quant of the BASE Qwen/Qwen3.8-27B (35,865,565,184 B,
# 1352 tensors), as a podman container under a root unit, driven by
# scripts/serve-frank-large.sh. This CUTS OVER the
# 2026-10-02..10-03 ROCmFPX llama.cpp shape (uncensored
# Q6_0_ROCMFPX_AGENT + MTP, 27.6-28.1 t/s): rollback = revert the swap
# commit's trio; the ROCmFPX payload, draft head and build all stay on
# disk at /home/dank/models/kingjones-Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP/.
# RUNTIME (2026-10-06, Frank): halogen-flash-server 0.16.2 — the
# closed-source C++/HIP engine (image ghcr.io/peonist-ai/halogen-flash-server,
# digest-pinned below) serving the Quat3rnion ABLITERATED Swift1.5 finetune
# of peonist's Qwen3.8-Flash-Next v2 checkpoint, as a podman container under
# a root unit, driven by scripts/serve-frank-large.sh. This CUTS OVER the
# 2026-10-05 halogen 27B-dense shape (censored base, DFlash2, 31.7 t/s):
# rollback = revert the swap commit's trio; the 27B payload stays on disk at
# /home/dank/models/halogen-qwen3.8-27b/.
#
# WHY THE RUNTIME MOVED WITH THE MODEL: the payload is .hgn — per its own
# card it "will not load in transformers, vLLM, or llama.cpp". Accepted
# consequences (Frank, 2026-10-05): TEXT-ONLY (halogen ignores the model's
# vision encoder; the fleet's vision path is frank-small) and the CENSORED
# base (this is a finetune of Qwen/Qwen3.8-27B, not the abliterated file —
# refusals may return). What it buys: vendor-measured decode DFlash2 31.7
# t/s mean (vs this endpoint's MTP 27.6-28.1 and the old halogen-era MTP
# 26.9), prefill ~566 t/s at 32k (was ~400 on the sibling engine), 1.4-2.3x
# wall-clock on prefill-heavy agent traffic, at HIGHER effective precision
# (~6.32 bpw vs ~6.6 with the old W4A4 promotion — see FLAGS.md).
# card it "will not load in transformers, vLLM, or llama.cpp" — and the v2
# .hgn loads ONLY in the flash-server line (the old halogen engine is for
# the 27B p1w4d file; the v2 quickstart pins flash-server). What the move
# buys (card + live probe evidence): MTP spec decode (53-57 t/s greedy,
# 3-10x serial), ~1,640 t/s prefill at 34k, ~72% fewer thinking tokens than
# the censored 27B (905 mean), the ABLITERATED base (refusals should NOT
# return), IMAGE INPUT (the v2 vision tower loads beside the checkpoint —
# red-PNG probe verified on the card; the 27B engine ignored its encoder),
# tool calls verified, and the native 262,144 context.
#
# SHAPE — the two knobs that are NOT free:
# HALOGEN_KV_SLOTS=1 KEEPS SPECULATION ON. halogen makes batching and
# speculation mutually exclusive: with >1 slot the drafter is disabled
# and per-stream decode falls to serial speed (the sibling engine
# measured 41 t/s -> 14 t/s when slots were raised, and halogen's own
# FLAGS.md prints the same law). frank-large was already a one-session
# endpoint under ROCmFPX (PARALLEL=1), so slots=1 is the same contract,
# not a regression.
# HALOGEN_SLOT_CTX=200700 (Frank's window) is the per-conversation hard
# SHAPE — the knobs that are NOT free, with the arithmetic:
# HALOGEN_CTX=200192 is the PER-SLOT window and the per-conversation hard
# ceiling — a prompt over it is a 400 naming the limit, never a silent
# truncation. KV pool = slots x slot_ctx x 64 KiB = 12.2 GiB. Budget
# against the 121 GiB of unified memory: weights 33.4 GiB (mmap'd — page
# cache, not anon), KV 12.2, engine working set + auto prompt cache
# ~20-30, host baseline ~10 -> ~75-85 GiB in use. The 2026-09-16 halogen
# era ran the same engine class on this box with LESS headroom.
# HALOGEN_DRAFTER=2 (DFlash2) is the shipped default and the fastest
# exact drafter on this checkpoint; the .hgn BAKES BOTH drafters (MTP +
# DFlash2), so unlike the Flash-Next era there is no sidecar to mount
# and no --dflash flag — the drafter is an int knob, byte-identical
# output to greedy verification either way.
# truncation. Harness compaction fires at 70% = 140,134, and 140,134 +
# 57,344 (harness maxTokens) = 197,478 <= 200,192 keeps prompt + budget
# inside the slot. models.yml contextWindow must quote 200192 or the
# harness discovers the wall as a 400.
# HALOGEN_KV_POOL_POSITIONS=400384 = KV_SLOTS(2) x CTX — the explicit
# form of the engine's default, so the seam test can pin the product.
# KV costs ~28 KiB/position (FLAGS.md) => 400,384 x 28 KiB = 10.7 GiB.
# HALOGEN_KV_SLOTS=2 restores the w4b-era shape on this box: the MTP
# drafter is NOT slot-locked on this engine (batching and speculation
# coexist — DFlash-era slots>1 law does NOT carry over; the card's own
# quickstart ships 2 slots WITH the head), so the second slot is
# concurrent capacity, not a speculation tax.
# Memory ledger against the box's 121.2 GiB unified (the n-gram table is
# mmap'd page cache beside the checkpoint, not a pinned charge — the
# card's own quickstart arithmetic does not bill it):
# weights ~62 GiB resident + KV 10.7 + arena (HALOGEN_MAX_TOK=8192 =>
# ~8 GiB per FLAGS.md arena formula 3.8+8.4*(MT/16384)) + vision ~2
# = ~82.7 GiB engine-side; HALOGEN_HOST_RESERVE_GIB=20 keeps the host
# honest; baseline co-tenancy (frank-secondary, ComfyUI, Caddy/observ)
# ~10-15. ~18 GiB margin. The boot ledger is the referee — read it
# after deploy and re-size if the engine prints a clamp.
# HALOGEN_SPEC_ADAPT=0 is MANDATORY (card): the default adaptive policy
# turns the MTP head off for whole requests after low acceptance and
# HALVES decode as a result. Frank accepted the head always-on trade
# (temp 1.0 traffic costs acceptance; the card still ships the recipe).
#
# CLIENT CONTRACT deltas vs the ROCmFPX shape (the harness side lives in
# CLIENT CONTRACT deltas vs the 27B shape (harness side lives in
# ~/.omp/agent/models.yml):
# * NO server-side sampler defaults: a request without temperature
# decodes GREEDY. The harness sends temp 1.0 / top_p 0.95 via
# extraBody, so that wire recipe is load-bearing now, not cosmetic.
# * max_tokens defaults to 8192 PER REQUEST and INCLUDES thinking; a
# reasoning reply over a small budget returns empty content with
# finish_reason "length". The harness maxTokens is 57344: compaction
# fires at 70% of 200700 = 140,490 and 140,490 + 57,344 = 197,834 <=
# 200,700 keeps prompt + budget inside the pool (the 262144-era 65536
# would overflow). Requests over HALOGEN_MAX_TOKENS_CAP are a 400
# naming the cap; cap and queue timeout are COUPLED (see FLAGS.md
# table) — do not move one without the other.
# * response_format json_object/json_schema is a 400 (no constrained
# decoding in this build).
# * Thinking lands in reasoning_content — same field as the llama.cpp
# era, so the harness compat block does NOT change. reasoning_effort
# accepts none|low|xhigh on this build (template default xhigh); the
# models.yml fold (high->xhigh) still applies.
# * SERVER-SIDE SAMPLER EXISTS NOW: HALOGEN_TEMPERATURE/TOP_P/TOP_K set
# the card's thinking recipe (1.0 / 0.95 / 20) as engine defaults, so
# an absent temperature is no longer greedy. models.yml extraBody
# carries the same values — consistent, not load-bearing.
# * HALOGEN_MAX_TOK=8192 is only the DEFAULT for requests that omit
# max_tokens; the harness always sends 57344. Requests over
# HALOGEN_MAX_TOKENS_CAP (65536) are a 400 naming the cap. Cap and
# queue timeout are COUPLED (FLAGS.md table): 65536 at ~10 t/s serial
# worst case needs > 6550 s; 7200 honors it. Do not move one alone.
# * max_tokens INCLUDES thinking; a reasoning reply over a small budget
# returns empty content with finish_reason "length".
# * Prompt cache is ON by default (PROMPT_CACHE=2): a follow-up turn at
# 100k context re-prefills ~2 s instead of ~88 s cold. No pin needed —
# unlike the 27B engine, auto-sizing fits this box's ledger.
# * Thinking lands in reasoning_content (same as the 27B engine); the
# v2 chat template rejects reasoning_effort outside xhigh|medium|low,
# so the models.yml fold (high->xhigh) still applies.
# * Vision: OpenAI image_url content parts only (the fleet's standard
# contract). >=256 px probe images or the model answers "video".
# * response_format json_object/json_schema unsupported in this line
# (no constrained decoding) — unchanged from the 27B engine.
# * The engine's token protocol has no auth and binds loopback INSIDE
# the container; only the frontend port is published. Keep it that way.
# the container; only the frontend port (8731) is published. Same as
# before. Kernel needs CONFIG_HSA_AMD_SVM (CachyOS ships it) and
# amdgpu.noretry=0 (check at deploy).
# Image: digest-pinned (fetch law: tag pulls silently re-point; the digest
# is what was qualified). 0.1.4 = the version the model card names.
FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen@sha256:dc0a39a0016d6cfc58a197978febaafdf8d28403f724ded6111d98b5fb7ac0ea
# Container identity: the halogen container name.
# is what was qualified). 0.16.2 = the version the model card names as its
# quickstart pin; RepoDigest captured at pull on 2026-10-06.
FRANK_LARGE_IMAGE=ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a
# Container identity: kept from the 27B era (unit ExecStop names it).
FRANK_LARGE_CONTAINER=frank-large-halogen
# Old quadlet-era container identity; the serve script clears stale names.
FRANK_LARGE_LEGACY_CONTAINERS=frank-large
@@ -77,37 +90,56 @@ FRANK_LARGE_LEGACY_CONTAINERS=frank-large
# default); the engine port (8730) is never published.
FRANK_LARGE_PUBLISH=7770:8731
FRANK_LARGE_MODEL_DIR=/home/dank/models/halogen-qwen3.8-27b
FRANK_LARGE_CHECKPOINT=qwen3.8-27b-p1w4d-d2.hgn
FRANK_LARGE_CHECKPOINT_BYTES=35865565184
FRANK_LARGE_CHECKPOINT_SHA256=274c3fc767fb57faf025dd03c76376b1fc1b32448aef45aaf02d1898fa962089
# The tokenizer is a FLAT DIRECTORY, $FRANK_LARGE_MODEL_DIR/tokenizer, mounted
# whole at /tokenizer — the model card ships `tokenizer/` and the engine's
# HALOGEN_TOKENIZER default is that directory (the card's own docker run sets
# no tokenizer flag). The Hub stores it as 5 files (sizes from the Hub API,
# 2026-10-05): tokenizer.json 12809320 (LFS sha256
# 0997f410c57a1f4e53b09e4be8f4a172d90edd9564368fb0847030937229b9f3),
# vocab.json 6722759, merges.txt 3353259, chat_template.jinja 8952,
# tokenizer_config.json 17928 (plain git blobs — size-checked, no LFS oid).
# The serve gate and the deployer refuse a missing file or ANY symlink: an
# HF cache snapshot symlinks each file into blobs/, and those links dangle
FRANK_LARGE_MODEL_DIR=/home/dank/models/halogen-flashnext-v2
# The checkpoint: Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated
# qwen38-flash-next-v2-swift15-abliterated.hgn — compacted r2 of the card's
# append-only original (byte-identical live tensors, padding dropped:
# 66,687,678,464 B vs 89,775,868,032 B). LFS sha256 verified at fetch.
FRANK_LARGE_CHECKPOINT=qwen38-flash-next-v2-swift15-abliterated.hgn
FRANK_LARGE_CHECKPOINT_BYTES=66687678464
FRANK_LARGE_CHECKPOINT_SHA256=9c880b8ba48deabfa06172eff1e7572e4fe465e09e97faec6e50c3f6fdcac037
# The n-gram lookup table: REQUIRED (the engine reads it beside the
# checkpoint; the card's quickstart names it explicitly). From
# peonist-ai/halogen-qwen3.8-flash-next @ 5cc17cea.
FRANK_LARGE_NGRAM_TABLE=qwen38-flash-next-ngram.hgn
FRANK_LARGE_NGRAM_BYTES=51200246144
FRANK_LARGE_NGRAM_SHA256=3450acada94e19aabad88bd49b45eb70304820949c2240fc178067b20ae5dffe
# The vision tower: OPTIONAL on the engine, ENABLED here (image input).
# From peonist-ai/halogen-qwen3.8-flash-next @ e053f488.
FRANK_LARGE_VISION_TOWER=qwen38-flash-next-vision.hgn
FRANK_LARGE_VISION_BYTES=897916416
FRANK_LARGE_VISION_SHA256=d62e0ae553fe88afd3833733d4a4c669f34d20fd8dfce4b9610525bed2134b10
# The tokenizer is a FLAT DIRECTORY, $FRANK_LARGE_MODEL_DIR/tokenizer,
# mounted whole at /tokenizer. The two repos ship the same one; fetched
# fresh from peonist @ e053f488 (tokenizer.json is LFS, byte-identical to
# the 27B-era copy; the rest are plain git blobs — size-checked only). The
# serve gate and the deployer refuse a missing file or ANY symlink: an HF
# cache snapshot symlinks each file into blobs/, and those links dangle
# inside the container mount.
FRANK_LARGE_TOKENIZER_FILES="tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json"
FRANK_LARGE_TOKENIZER_FILES="tokenizer.json chat_template.jinja tokenizer_config.json merges.txt vocab.json generation_config.json"
# --- engine flags (docs/FLAGS.md of the engine repo; anything unset stays
# at the image default, which is what was qualified) ------------------------
# --- engine flags (docs/FLAGS.md of the flash-server repo; anything unset
# stays at the image default, which is what was qualified) ------------------
HALOGEN_MODEL_ID=frank-large
HALOGEN_SLOT_CTX=200700
HALOGEN_KV_SLOTS=1
HALOGEN_DRAFTER=2
HALOGEN_CTX=200192
HALOGEN_KV_POOL_POSITIONS=400384
HALOGEN_KV_SLOTS=2
HALOGEN_HOST_RESERVE_GIB=20
# Spec decode: MTP head is INSIDE the checkpoint (do NOT set HALOGEN_MTP_HEAD);
# depth 2 is the documented "best all-round" default. HALOGEN_DRAFTER /
# HALOGEN_DFLASH_* from the 27B engine DO NOT EXIST here.
HALOGEN_SPEC_ADAPT=0
# Coupled pair (FLAGS.md table): the cap bounds how long ONE request holds
# the GPU, the timeout bounds how long the NEXT client waits. 65536 at
# ~10 t/s serial worst case needs > 6550 s queue timeout; the defaults
# honor that. Do not lower the timeout alone.
# the GPU, the timeout bounds how long the NEXT client waits. Do not move
# one alone (see the client-contract note above).
HALOGEN_MAX_TOK=8192
HALOGEN_MAX_TOKENS_CAP=65536
HALOGEN_QUEUE_TIMEOUT=7200
# Engine prompt cache: EMPTY = auto (sized from available memory at
# startup). Pin an explicit MB only to argue with the auto figure — a
# full-context entry is ~18 GB at 262K, so a too-small pin yields a cache
# that reports enabled and never hits (the engine warns at startup).
#HALOGEN_CACHE_MB=
# Server-side sampler = the card's thinking recipe (the engine now HAS one;
# requests may still override per-call). Matches models.yml extraBody.
HALOGEN_TEMPERATURE=1.0
HALOGEN_TOP_P=0.95
HALOGEN_TOP_K=20
# Card's belt-and-braces: the engine pins weights by default
# (HALOGEN_FLASH_PIN_TRUNK=1; only the retired w4b build honored 0).
HALOGEN_WEIGHTS_LOCK=1
+3 -3
View File
@@ -1,5 +1,5 @@
[Unit]
Description=halogen container (peonist-ai/halogen-qwen3.8-27b, DFlash2 spec decode, 1 slot x 200700, text-only), root systemd unit (dankclaw, frank-large, Strix Halo iGPU)
Description=halogen-flash-server container (Quat3rnion abliterated Swift1.5 of Qwen3.8-Flash-Next v2, MTP spec decode, 2 slots x 200192, vision ON), root systemd unit (dankclaw, frank-large, Strix Halo iGPU)
After=network-online.target
Wants=network-online.target
@@ -22,8 +22,8 @@ TimeoutStopSec=120
# would otherwise leave the endpoint down until someone notices.
Restart=always
RestartSec=10
# The engine mmaps 33.4 GiB of weights and keeps a multi-GiB working set
# on a box that also hosts frank-small, ComfyUI and the Caddy/observability
# The engine pins ~62 GiB of v2 weights and keeps a multi-GiB working set
# on a box that also hosts frank-secondary, ComfyUI and the Caddy/observability
# stack; make it a late OOM candidate, not the first.
OOMScoreAdjust=-500
+133 -73
View File
@@ -1,82 +1,142 @@
# frank-primary.env — FreeToken (PR #587 ROCm) serving the orcarouter
# Qwen3.8-Flash-Next-Uncensored-FP8 payload on dankserver :7770.
# frank-primary.env — radiance (StillDeadcode, Apache-2.0) serving the
# OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint at NATIVE precision
# (modelopt block-FP8 throughout, no requantisation) on BOTH R9700s (TP=2),
# dankserver :7770.
#
# 2026-10-05 cutover from the tcclaviger RDNA4 vLLM fork (git tag
# frank-primary-vllm-shapeG-20261003 is the pre-cutover state; the 362-line
# vLLM-era env is in that tag). The engine is FreeToken at PR head
# 77a7201f98f00d93f618c6cf8b81ffe8360e0899 (branch rocm714_torch214), built
# on the host into a local image by
# scripts/docker/frank-primary-freetoken.Dockerfile — the PR's own
# docs/install_amd.md stack (ROCm 7.14 base image, torch 2.11.0+rocm7.14,
# triton 3.7) for gfx1201.
# 2026-10-05 cutover from FreeToken (PR #587 ROCm, one card, experts in
# pinned host RAM; git tag frank-primary-freetoken-pr587-20261005 is the
# pre-cutover state — its unit/env/serve trio and Dockerfile are all in the
# tag and rebuildable). Before that: tcclaviger vLLM fork (tag
# frank-primary-vllm-shapeG-20261003).
#
# WHY FREETOKEN: the vLLM fork's expert offload planned VRAM/host tiers per
# boot from an estimator (util 0.982 law, arena 17.77 GiB / 7,762 slots,
# ~40k-token GDN state faults) and 1.0 was unbootable. FreeToken keeps ALL
# expert weights in pinned HOST RAM (~120 GiB) with an LRU GPU cache, sizes
# KV from an explicit token pool, and serves on ONE R9700 (32 GB) with the
# second card idle. RAM fit: 246 GiB total, service stopped frees the vLLM
# container's ~170 GiB; weights ~120 GiB + PLE 47.7 GiB + overhead ≈ 190 GiB.
# WHY RADIANCE: the engine is C++/HIP with libr4d kernels built for
# gfx1201, drives `podman run` directly from the digest-pinned
# stilldeadcode/radiance image (287 MiB; host needs only the amdgpu driver),
# and its qwen4exp plugin reads the modelopt FP8 encoding NATIVELY
# (`X.weight_scale_inv` is the scale plane — checkpoint.h). The PLE n-gram
# table (51B params, 47.7 GiB) is served from pinned host RAM via io_uring
# reads — Frank's call: "Native FP8. Experts from ram. Pin the PLE to ram."
# The 4-bit-codebook publisher shape (qwen3.8-next-flash-fp8-iq4r-moe.rad,
# --host-pool-mib 12288) remains the documented fallback if the native
# container proves unbootable.
#
# PAYLOAD: unchanged — /home/dank/models/Qwen3.8-Flash-Next-Uncensored-FP8,
# 131 safetensors shards. Quant is standard modelopt block-FP8: 75,264
# weight_scale_inv tensors (block 128x128), zero input_scale (FreeToken #462
# covers exactly this export shape), one weight_scale on the PLE ngram
# embedding. FreeToken loads HF safetensors directly; no FTW conversion.
# PAYLOAD: /home/dank/models/rad/qwen4exp-fp8-native.rad — a rad-convert
# NO-RECIPE pure copy of the official 131-shard checkpoint (every weight
# keeps its block-FP8 encoding; ~161 GiB). The convert runs against a
# staging dir whose config.json drops `quantization_config`: the descriptor
# string is only the plugin SELECTION key (spec §2.4 — exact match, no
# fallback), and no shipped plugin claims (qwen4exp, "fp8"); the shipped
# qwen4exp plugin claims (qwen4exp, "") which is what recipe-built
# containers carry. Per-weight encodings are data, not the descriptor —
# removing the key changes nothing about what the weights hold. Built by
# scripts/build-frank-primary-rad.sh (staging + convert + rad-info).
#
# SERVING FACTS (docs/cli.md, verified against main+PR587):
# - `ft serve` loads the checkpoint directly; `--served-model-name` keeps the
# harness id; reasoning/tool parsers are AUTO (reasoning lands in
# reasoning_content — models.yml flips reasoningContentField to match).
# - KV: --page-size must stay 1 for GDN/SWA models (default); --cache-type
# radix auto-picks the GDN-aware variant.
# - MoE: --moe-strategy auto -> offload (fp8 experts CANNOT use
# --moe-cpu-layers; only bf16/nvfp4/mxfp4 have a CPU executor).
# --moe-cache-rate holds the GPU LRU fraction of all experts.
# - `ft ctl cache --kv --moe` grows the pools LIVE on a running server
# (shrinking needs a restart); `ft ctl stats` reports pool occupancy.
# - KV MATH: 12 full-attention layers x 2 KV heads x 256 head_dim x 2 (K+V)
# x 2 B = 24 KiB/token (f16 KV; the 36 GDN layers use fixed per-seq state,
# not per-token KV). See FRANK_PRIMARY_FT_NUM_TOKENS below for the pool
# size and its measured page-size law.
FRANK_PRIMARY_IMAGE=localhost/frank-primary-freetoken:pr587-77a7201
FRANK_PRIMARY_MODEL_DIR=/home/dank/models/Qwen3.8-Flash-Next-Uncensored-FP8
FRANK_PRIMARY_MODEL_SHARDS=131
# SERVING FACTS (docs/GUIDE.md, docs/DOCKER.md, compose/flashnext.yaml):
# - The HTTP listener binds AFTER the full load (main.cpp: engine.start()
# then engine.serve()) — during load the port refuses connections, so a
# bare curl /health gate is honest. /health == /ping: 200 JSON while
# serving, 503 only while stopping (core/server/admin.cpp:385).
# - Reasoning: --reasoning-format defaults to auto, emitting thinking in
# `reasoning_content` (config.cpp). Tool calls: the qwen3.8 dialect, from
# the chat format. Both need no parser flags.
# - Sampling: the OpenAI endpoint accepts temperature, top_p, top_k, min_p,
# typical_p, presence_penalty, frequency_penalty, repetition_penalty,
# repeat_last_n, chat_template_kwargs, reasoning_effort,
# enable_thinking, preserve_thinking (core/server/oai.cpp:194-205) —
# models.yml's extraBody set is fully inside it. Unknown params are
# refused (400), which is why nothing unverified rides the wire.
# - MTP: the official checkpoint carries the mtp.* head (3,101 tensors,
# mtp_num_hidden_layers=1) and the plugin probes draft depth 3 — the
# publisher's throughput peak for this model. Stated explicitly with
# --num-speculative-tokens 3.
# - The fused TP all-reduce is declared over max-num-seqs x
# (1 + --num-speculative-tokens) rows; 2 x 4 = 8 rows fits either wire.
FRANK_PRIMARY_IMAGE=stilldeadcode/radiance@sha256:4c3f484564bb2872784e46b0a59cbd71becc9c1e746e301a3596ffae253c322e
# The .rad container FILE (mounted under /models) and the state dir for the
# prefix-cache disk tier (mounted rw at /data). The model dir holds ONLY
# .rad files; the raw HF download and its staging dir live beside it and
# are never mounted.
FRANK_PRIMARY_MODEL_DIR=/home/dank/models/rad
FRANK_PRIMARY_MODEL_FILE=qwen4exp-fp8-native.rad
FRANK_PRIMARY_STATE_DIR=/home/dank/radiance-state
# The state dir's tune/ subdirectory holds rad-tune's kernel-selection
# cache (the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune
# at boot; $RADIANCE_HOME defaults to /opt/radiance, in the image, so the
# cache must ride a host mount — serve script mounts it, see the runbook's
# "Tuning levers").
# Container identity is the SERVICE name (Frank's call, 2026-10-05): one
# name across systemctl and podman ps. The vLLM-era name frank-primary-qwen
# moved into the legacy list so every deploy cleans it up.
# name across systemctl and podman ps. The runtime changes; the name does
# not. The legacy list retires every predecessor identity that could hold
# the GPUs or the port.
FRANK_PRIMARY_CONTAINER=frank-primary
FRANK_PRIMARY_LEGACY_CONTAINERS="frank-primary-qwen frank-primary-r9v qwen-fp8 glm-probe"
FRANK_PRIMARY_SERVED_NAME=frank-primary
FRANK_PRIMARY_PUBLISH=7770:7770
FRANK_PRIMARY_PORT=7770
# Frank's chat-template call: froggeric's fixed Qwen3.8 template (the
# repo's root chat_template.jinja, vendored here; the checkpoint's own
# template is baked into the .rad by rad-convert and this overrides it).
# Radiance derives the reasoning/tool-call reply format from it.
FRANK_PRIMARY_TEMPLATE=/home/dank/serverAppConfigs/scripts/qwen38-froggeric-v22.5.jinja
# --- FreeToken engine knobs (first-boot conservative, resize live) -----
# Single GPU: the R9700 at index 0. HIP_VISIBLE_DEVICES=0 hides card 1 so
# the in-container index 0 IS the right card; --gpu 0 selects it.
FRANK_PRIMARY_FT_GPU=0
# KV pool in TOKENS (not bytes — vLLM-era FRANK_PRIMARY_KV_BYTES is gone).
# 299,968 tokens x 24 KiB = 7.0 GiB; clears the harness worst case
# (177,356 tokens) and one NATIVE 262,144-token conversation + full budget.
# MEASURED FIRST BOOT (2026-10-05): the auto attention backend is
# qsa_sparse, which OVERRIDES page size to 64, so --num-tokens must be a
# MULTIPLE OF 64 — 300,000 was refused ("nearest valid values: 299968 or
# 300032"). Grows live with `ft ctl cache --kv <n>`; shrink needs a
# restart.
FRANK_PRIMARY_FT_NUM_TOKENS=299968
# Concurrent decode slots. 2 is the first-boot conservative value; the
# radix pool admits ~2 full-length 200,704-token conversations. Raise with
# --max-running-requests only after a real-traffic look at slot occupancy.
FRANK_PRIMARY_FT_MAX_RUNNING=2
# Per-request output budget ceiling: the harness's maxTokens (36,864).
# Requests that omit max_tokens get this default instead of the engine's
# 32,768.
FRANK_PRIMARY_FT_MAX_OUTPUT_TOKENS=36864
# GPU expert LRU cache as a FRACTION of all experts (~120 GiB fp8 total ->
# ~12 GiB VRAM at 0.10). First-boot conservative; resize live with
# `ft ctl cache --moe` after reading the boot ledger + ft ctl stats.
FRANK_PRIMARY_FT_MOE_RATE=0.10
# Escape hatch for boot-ladder experiments (YaRN, --max-prefill-length,
# --moe-cpu-threads, --sampling-defaults none, ...). Unset -> no extra
# argv. Keep multi-flag values as one string; the serve script expands it
# unquoted on purpose.
FRANK_PRIMARY_FT_EXTRA_ARGS=
# --- engine shape (first-boot conservative; the boot ledger arbitrates) --
# Tensor parallel: BOTH R9700s. The board swap (2026-09-20, ProArt B850
# x8/x8 on root ports) made collectives legal on this host; radiance's TP
# wire is libr4d's own, not RCCL.
FRANK_PRIMARY_TP=2
# Lossy WHT 6-bit wire for large all-reduces (publisher's production choice:
# +5.6-6.3% prefill, ~-1.3% 8-stream decode at TP2). min-kb stays the
# engine default.
FRANK_PRIMARY_TP_WIRE=wht6
# Decode slots. 2 is Frank's first-boot value ("use parallelism = 2 to
# start"); raise only after a real-traffic look at slot occupancy and the
# KV pool's headroom line. The recurrent-state cost scales with it (54
# MiB/copy/card, 2 copies with speculation).
FRANK_PRIMARY_MAX_SEQS=3
# Served window. Native is 262,144; 200,000 is the publisher's production
# value (compose) and what models.yml's contextWindow must quote — a longer
# prompt is refused by the engine, never truncated.
FRANK_PRIMARY_MAX_MODEL_LEN=200000
FRANK_PRIMARY_PLACEMENT=expert_tiered
# Pinned host pool for the expert rows that do not fit VRAM. Native FP8
# experts are 112.8 GiB total (56.4 GiB per rank slice) vs the 4-bit
# publisher shape's ~29 GiB — this pool is the cost of native precision.
# The boot ledger prints the pool's coverage ("the pool backs N% of the
# worst case ... M full-length sessions"); resize from that line, not from
# arithmetic.
FRANK_PRIMARY_HOST_POOL_MIB=40960
# VRAM left unclaimed per card (publisher default 96 MiB).
FRANK_PRIMARY_GPU_HEADROOM_MIB=96
# Weights share of claimed VRAM (publisher production value; higher = more
# prefix-cache hit rate, less KV headroom).
FRANK_PRIMARY_EXPERT_VS_CACHE=0.82
# KV cache in FP8 (7.32 KiB/token at this model's 12 full-attention layers,
# 2 KV heads, 256 head_dim, K+V).
FRANK_PRIMARY_KV_DTYPE=fp8
# Spec decode: the checkpoint's own MTP head at the publisher's depth-3
# operating point (probe() returns 3 when mtp_num_hidden_layers > 0; the
# flag states it so the ledger records the intent).
FRANK_PRIMARY_SPEC_TOKENS=2
FRANK_PRIMARY_MAX_BATCHED_TOKENS=2048
# Prefix cache tiers (publisher pattern; disk tier sized for 2 slots).
FRANK_PRIMARY_PREFIX_HOST_MIB=4096
FRANK_PRIMARY_PREFIX_DISK_MIB=65536
# Frank's pin law: the PLE n-gram table (47.7 GiB) serves from pinned host
# RAM the cards read. `ram` REFUSES the start if the memlock limit is too
# small — the unit's LimitMEMLOCK=infinity is the other half of this law.
FRANK_PRIMARY_NGRAM_PLACEMENT=ram
# TTM preflight: the pinned ledger (2 x 40 GiB pools + 47.7 GiB PLE + 4 GiB
# prefix host) is ~132 GiB. ttm's pages_limit/pages_max cap the GPU-mappable
# pinned pages; the current cap (32,252,917 pages = ~123 GiB) is BELOW the
# plan, so the serve script refuses to start until both are raised. This is
# the floor it enforces (in MiB), with ~8 GiB of margin.
FRANK_PRIMARY_TTM_MIN_MIB=140000
# Escape hatch for boot-ladder experiments. Unset -> no extra argv. Keep
# multi-flag values as one string; the serve script expands it unquoted on
# purpose.
FRANK_PRIMARY_EXTRA_ARGS=
# The .rad's minimum size in MiB (deployer gate; a truncated convert is a
# guaranteed long-timeout boot instead of a fast failure). The converted
# container measured 177,109 MiB (1,857,127,997,440 B = 173.0 GiB; the
# plan-only estimate 172.76 GiB covers the data planes but not the metadata
# tail) - floor sits just under the payload.
FRANK_PRIMARY_RAD_MIN_MIB=177000
+28 -29
View File
@@ -1,26 +1,25 @@
# frank-primary.service — FreeToken (PR #587 ROCm) endpoint (orcarouter
# Qwen3.8-Flash-Next-Uncensored-FP8, 131-shard payload) on ONE R9700
# (gfx1201), dankserver :7770. 2026-10-05 cutover from the tcclaviger vLLM
# fork (git tag frank-primary-vllm-shapeG-20261003 is the pre-cutover
# state; the retired unit/env/serve trio is in that tag).
# frank-primary.service — radiance (StillDeadcode, Apache-2.0) endpoint
# (OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 at NATIVE block-FP8 precision, the
# rad-convert container qwen4exp-fp8-native.rad) on BOTH R9700s (TP=2,
# wht6 wire), dankserver :7770. 2026-10-05 cutover from FreeToken (git tag
# frank-primary-freetoken-pr587-20261005 is the pre-cutover state; the
# retired unit/env/serve trio and Dockerfile are in that tag).
#
# Same port, same served alias, same container identity as the unit name;
# the harness models.yml id is unchanged. The serve script drives
# `podman run` directly (the image carries `ft serve`) and then waits on
# the container. The image is built ON THE HOST from
# scripts/docker/frank-primary-freetoken.Dockerfile (FreeToken PR head
# 77a7201 in the PR's own ROCm 7.14 base image).
#
# WHY A ROOT UNIT (not a rootless quadlet): FreeToken pins ~120 GiB of
# expert weights plus the ~47.7 GiB PLE ngram table in HOST RAM. Pinned
# pages are charged against RLIMIT_MEMLOCK; dank's login session on this
# host carries 8 MiB and a user quadlet is capped by the user manager's own
# hard limit — raising that means restarting user@1000.service and bouncing
# every user container on the host. A root unit grants
# LimitMEMLOCK=infinity to the dank process via PID1, and the image stays
# in dank's rootless podman storage.
# `podman run` directly: the digest-pinned stilldeadcode/radiance image
# (287 MiB, kernels prebuilt for gfx1201) has the engine as its
# entrypoint. The image lives in dank's ROOTLESS podman storage.
# WHY A ROOT UNIT (not a rootless quadlet): radiance pins ~132 GiB of HOST
# RAM (2 x 40 GiB expert pools + the 47.7 GiB PLE ngram table + the prefix
# host tier). Pinned pages are charged against RLIMIT_MEMLOCK; dank's login
# session on this host carries 8 MiB and a user quadlet is capped by the
# user manager's own hard limit — raising that means restarting
# user@1000.service and bouncing every user container on the host. A root
# unit grants LimitMEMLOCK=infinity to the dank process via PID1; the
# engine still runs out of dank's rootless podman storage.
[Unit]
Description=frank-primary — FreeToken (PR587) Qwen3.8-Flash-Next-Uncensored-FP8 on 1x R9700
Description=frank-primary — radiance Qwen3.8-Flash-Next-FP8 (native) on 2x R9700 TP=2
After=network-online.target
Wants=network-online.target
# Never let the start-rate limiter give up after a crash loop (a failed LLM
@@ -30,21 +29,21 @@ StartLimitIntervalSec=0
[Service]
User=dank
Group=dank
# The expert LRU + PLE pin ~170 GiB of host RAM; an RLIMIT_MEMLOCK smaller
# than the pinned set fails the load mid-start. PID1 grants infinity here —
# parity with the fleet's other LLM units.
# The engine pins ~132 GiB of host RAM (expert pools + PLE table); an
# RLIMIT_MEMLOCK smaller than the pinned set fails the load mid-start.
# PID1 grants infinity here — parity with the fleet's other LLM units.
LimitMEMLOCK=infinity
Environment="FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env"
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh
# systemd stop: wait alone cannot stop the container child; stop it
# explicitly. ft serve drops connections on SIGTERM; no graceful drain
# contract is claimed (the OpenAI front end holds no queue to drain —
# in-flight decodes are lost, which a stop during a bench window accepts).
# explicitly. No graceful drain contract is claimed (the OpenAI front end
# holds no queue to drain — in-flight decodes are lost, which a stop
# during a bench window accepts; radiance takes SIGTERM on the engine
# process and podman reaps the container).
ExecStop=/usr/bin/podman stop -t 120 frank-primary
# Boot cost: 131-shard FP8 page-in + PLE pin + expert upload. vLLM-era
# budget (1800 s) carried forward; the FreeToken first boot measured here
# once it ran (see the runbook for the real number).
TimeoutStartSec=1800
# Boot cost: .rad load (weights + PLE upload) + expert host-pool pin. The
# publisher's compose healthcheck allows a 15-minute start_period; the
# vLLM-era budget (1800 s) carried forward.
# Restart=always, NOT on-failure (the 2026-10-01 vLLM lesson carries over:
# a clean API-server exit after a worker fault reads as success to
# systemd). `always` restarts on any container death; an explicit
+98
View File
@@ -0,0 +1,98 @@
# frank-secondary — llama.cpp (master, CUDA sm_86) serving
# ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF:RCO-IQ3_XXS
# **with the MTP head in the file** on the RTX 3090, :7771.
#
# 2026-10-06 (Frank, evening): the payload moved to ajgazin's UNCELERSED
# Swift-1.5-Qwen3.8-27B at RCO-IQ3_XXS (10,442,827,392 B) — 1.56 GiB
# lighter than the IQ3_S-mtp it replaces — with MTP BACK ON (draft-mtp,
# depth 3), TWO 131,072 slots, and ubatch 512. The uncensored finetune
# changes behavior by design (publisher: 23/100 refusals vs Swift 1.5's
# 98/100, KL 0.0884 from Swift 1.5) — Frank's order. The RCO tier is the
# same per-tensor ALLOCATION recipe as the IQ3_S file (histogram: iq3_s
# 97 / bf16 96 / iq3_xxs 83 / iq2_s 72 / iq4_xs 33 / iq2_xs 32 / iq2_xxs
# 30 / q2_k 28 / q4_k 19 / q6_k 8 / iq1_s 4 / iq1_m 4 — all stock types,
# stock llama.cpp loads it); publisher KLD: Mean 0.002631, 90 % 0.0421
# (kld_results.csv). RCO files are "more experimental" per the README —
# the quality floor is the IQ3_S rung, not a promise.
#
# The architecture is unchanged (same model family): qwen35 hybrid —
# block_count 65 (64 real layers + the blk.64 MTP head),
# full_attention_interval 4 => SIXTEEN full-attention layers, 24 Q heads
# / 4 KV heads, k/v length 256, native window 262144, dense,
# nextn_predict_layers=1. The MTP head ships in every main GGUF of this
# repo (README); --spec-type draft-mtp loads it against the main model's
# own weights.
#
# THE KV CONTRACT STAYS q8_0 K + q8_0 V (Frank, 2026-10-05): 34 KiB/token
# (16 attn layers x 4 heads x 256 x 1.0625 B, K and V).
#
# Why TWO slots + MTP fits now (it did not under IQ3_S):
# card 24,576 MiB. Weights 9,963 MiB (10,442,827,392 B; IQ3_S was
# 11,561). Projector ~1,290 MiB (the PUBLISHER's OWN mmproj-BF16.gguf,
# 931,145,888 B, at the measured ~1.45x card law — no substitution).
# Compute buffers at ubatch 512 ~1.3 GiB. CUDA ctx ~500 MiB. DeltaNet
# state ~150 MiB/slot x 2. MTP draft ctx back: ~0.4 GiB. Non-KV ~13.9
# GiB. Two q8_0 slots = 8,704 MiB. Predicted free ~2.6-3.2 GiB — back
# over the old 2-3 GiB floor that the IQ3_S two-slot boot violated
# (measured there: 1,397 MiB free). MEASURE nvidia-smi free at boot
# (quote the free field) and let the runbook carry the number.
#
# Payload history (rollback trail): /home/dank/models/Swift15-IQ3S/ (the
# IQ3_S-mtp that was live until this swap), Ornith-Q8/, MiMo-Q6K/ all
# stay on disk; one MODEL_FILE + shape edit back each.
LLAMA_BIN=/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server
MODEL_FILE=/home/dank/models/Swift15-UncIQ3XXS/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf
# Vision: the publisher SHIPS the projector — mmproj-BF16.gguf
# (931,145,888 B) from the same repo, fetched by
# scripts/download-frank-secondary.sh. No substitution needed for this
# payload (the unsloth base-tower proof of 2026-10-05 remains the
# fallback argument for the IQ3_S rollback payload).
# Empty string = text-only on purpose.
MMPROJ_FILE=/home/dank/models/Swift15-UncIQ3XXS/mmproj-BF16.gguf
# Fleet endpoint: same port and alias as every frank-secondary before it, so the
# harness models.yml id and the homepage probe do not move.
ALIAS=frank-secondary
BIND_ADDR=0.0.0.0
LISTEN_PORT=7771
# TWO conversations at Frank's 112k cap (2026-10-06 evening): --ctx-size is
# the POOL llama.cpp divides across slots; 229376 / 2 = 114,688 per
# conversation. Lowering from 128k (131,072) frees ~1 GiB of KV, which
# funds the ubatch 512 -> 1024 increase (prefill throughput). The
# per-slot window is the product contract, so models.yml follows to
# 114,688.
CTX_SIZE=229376
PARALLEL=2
# Compute-buffer geometry. Ubatch is a prefill knob (decode never moves);
# the 1024 rung was the first A/B back once the per-slot context dropped
# to 112k (2026-10-06 evening), and 2048 is the second (Frank, same night)
# — the rung measured +4.6 % prefill for 616 MiB on the 3090 (2026-10-05
# boot pair), affordable inside the 1,751 MiB the 112k cut freed. The pair
# is a measurement, not a law (loud failure at graph capture if it does
# not fit).
BATCH_SIZE=4096
UBATCH_SIZE=2048
# The KV contract for this swap (Frank, 2026-10-05): q8_0 K + q8_0 V —
# llama.cpp's FP8 class. The flags stay explicit so the choice is visible in
# the argv either way, and --flash-attn stays pinned on in the serve script:
# it is required for quantized V (the build carries
# GGML_CUDA_FA_ALL_QUANTS=ON since 2026-09-17, so q8_0 KV is a real path,
# not a silent f16 fallback).
CACHE_TYPE_K=q8_0
CACHE_TYPE_V=q8_0
# Speculative decoding is ON (Frank, 2026-10-06 evening): draft-mtp at
# depth 3 against the head IN the file (the -mtp variant ships the
# blk.64.nextn.* tensors). ngram-mod stays skipped — on a qwen35 hybrid
# it disables recurrent-state rollback, so a rejected draft would
# corrupt the DeltaNet state; MTP needs no such bypass (its draft ctx
# re-derives state from the target's own weights). The flags move as a
# set: the serve script emits BOTH only when SPEC_TYPE names a drafter.
SPEC_TYPE=draft-mtp
SPEC_DRAFT_N_MAX=3
NGL=99
+19
View File
@@ -0,0 +1,19 @@
[Unit]
Description=llama.cpp Swift-1.5-Qwen3.8-27B Uncensored RCO IQ3_XXS GGUF (qwen35 hybrid, 2x131072 slots, q8_0 K+V, draft-mtp depth 3, mmproj vision) on the RTX 3090 (dankclaw, frank-secondary)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dank
Group=dank
# CUDA pins its staging buffers, so the process needs the memlock budget even
# though the weights themselves are copied into VRAM.
LimitMEMLOCK=infinity
Environment="FRANK_SECONDARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-secondary.env"
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-secondary.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
-136
View File
@@ -1,136 +0,0 @@
# frank-small — llama.cpp (master, CUDA sm_86) serving
# ornith-ai/Ornith-1.5-9B-GGUF:Q8_0 on the RTX 3090, :7771.
#
# 2026-10-05 (Frank): the payload moved off bartowski/MiMo-V2.6 (Q6_K) to
# ornith-ai/Ornith-1.5-9B Q8_0 (9,786,060,384 B, LFS oid 22086870...).
# Header-verified facts that re-price the shape: qwen35 hybrid like MiMo, but
# 33 blocks (not 32) with full_attention_interval 4 => NINE full-attention
# layers (MiMo had 8), 16 Q heads / 4 KV heads, k/v length 256, native window
# 262144, dense (no expert keys), nextn_predict_layers=1 (an MTP head is IN
# the file — capability only; no spec decode ships, same law as before).
#
# THE KV CONTRACT STAYS f16 K + f16 V — NO KV QUANTIZATION (Frank, 2026-10-05,
# overruling a written q8_0-V proposal). The arithmetic that makes this hard:
# KV rate 9 attn layers x 4 heads x 256 x 2 B = 18 KiB/token K, same again
# for V at f16 => 36 KiB/token (MiMo was ~24.5 measured: one fewer
# full-attn layer AND one fewer KV head).
# Weights +1,900 MiB (9,334 vs Q6_K's 7,432). Measured free at the Q6_K
# 4096/2048 shape was 2,279 MiB; Q8_0 at f16/f16 does not fit that shape —
# the deficit is made up the way Frank ordered: UBATCH FIRST (2048 -> 1024,
# the measured 616 MiB rung), then POOL STEPS, then measure. Guess-and-check
# on the live card is the accepted method for llama.cpp sizing here: boot,
# read nvidia-smi free, step the pool (12,288-token increments, ~432 MiB
# each at the 36 KiB/token rate) until the 2-3 GiB floor holds. Compute
# buffers allocate at graph capture during BOOT, so a failure is loud and
# fast, not a surprise at the first long prompt.
#
# Previous shape history (MiMo era, for the rollback trail): 2026-10-03 the
# rung went Q5_K_M -> Q6_K with the pool HELD at 401,408; the freed third
# slot funded exact f16 V first; the ~3.5 GiB still free funded the +875 MiB
# of weights.
#
# 2026-10-03 (Frank): TWO streams instead of three, each one WIDE — 200,704
# tokens per conversation, above the 200,700 the harness compacts at — and the
# KV cache is now FULLY UNQUANTIZED (f16 K, f16 V). The two moves are the same
# move: llama.cpp divides --ctx-size across --parallel slots, so the slot given
# up is exactly the VRAM that pays for unquantized V. No further quantization
# anywhere on this endpoint.
#
# Three measurements on this card, this runtime, price the terms:
# 2 x 262,144, no projector: 20,190 MiB used / 4,386 free
# KV = 524,288 tok x 24.5 KiB = 12,800 MiB => non-KV (weights 6,557 +
# DeltaNet state ~25/slot + compute buffers + CUDA graphs) = 7,390 MiB
# 3 x 196,608, projector on: 22,972 MiB used / 1,153 free
# KV = 14,113 MiB => non-KV WITH the vision tower = 8,859 MiB
# 3 x 180,224, projector on: 21,684 MiB used / 2,441 free (the 2026-10-02
# shape) => non-KV = 8,748 MiB
# Two slots give back one DeltaNet state (~25 MiB). Priced at f16 K+V
# (32 KiB/token) this pool is 12,544 MiB, so the prediction is
# 12,544 + ~8,800 = ~20.9 GiB used, ~3.1 GiB free — the band the 2026-10-02
# shape hit, bought with precision instead of a slot. The 1024 ubatch
# re-prices the compute buffers upward, so MEASURE after any change: if the
# boot lands under 2 GiB free, step the pool down in 12,288-token
# (384 MiB at this rate) increments.
#
# KV arithmetic: attention KV exists only in the 8 full-attention layers
# (4 KV heads x 256 head_dim x 2 B) = 16 KiB/token for K and the same again for
# V => 32 KiB/token, so 12,288 tokens (= 6144/slot) is one 384 MiB step — that
# is the ladder to walk if this moves again. Two native windows (2 x 262,144)
# are NOT reachable with full-precision V: 524,288 x 32 KiB = 16.1 GiB of KV
# against a card with ~15.7 GiB of KV room once weights, the vision tower, the
# DeltaNet states and the graphs are loaded. 200,704 per stream is what full
# precision buys; q8_0 on V would buy 262,144 instead.
#
# The asymmetric-KV contract this endpoint was created for (Frank, 2026-10-01
# night: f16 K + q8_0 V) is now spent — it existed to make a third slot fit,
# and the third slot is gone. The flags stay explicit so the choice is visible
# in the argv either way.
LLAMA_BIN=/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server
MODEL_FILE=/home/dank/models/Ornith-Q8/Ornith-1.5-9B-Q8_0.gguf
# Vision: the Ornith Q8_0 GGUF ships no `v.*` tensors either (dense-9B byte
# arithmetic: 9.79 GB ≈ 9B params x ~1.06 B/param leaves no room for a tower),
# so the projector file IS the vision capability (mmproj-Ornith-1.5-9B-BF16
# .gguf, 921,704,672 B, LFS oid 626f9f90..., fetched by
# scripts/download-frank-small.sh). Empty string = text-only on purpose.
MMPROJ_FILE=/home/dank/models/Ornith-Q8/mmproj-Ornith-1.5-9B-BF16.gguf
# Fleet endpoint: same port and alias as every frank-small before it, so the
# harness models.yml id and the homepage probe do not move.
ALIAS=frank-small
BIND_ADDR=0.0.0.0
LISTEN_PORT=7771
# ONE conversation at the NATIVE 262,144 window (Frank, 2026-10-05). The
# two-slot shape died under Ornith and the NO-QUANTIZE law, by arithmetic:
# the KV rate is the ARCHITECTURE's, not the quant's — 9 full-attn layers x
# 4 KV heads x 256 dim x 2 B = 36 KiB/token at f16 K+V (MiMo measured 24.5),
# so 2 x 200,704 wants ~14.1 GiB of KV on top of +1,900 MiB of Q8 weights
# against 2,279 MiB measured free; ubatch 1024 claws back 616, and no
# pool step fits BOTH the 2-3 GiB floor and the harness compaction
# invariant (slot >= 191,148). One native slot fits with room; the window
# per conversation RISES 200,704 -> 262,144. The cost: a second concurrent
# request queues behind the released slot (subagent concurrency 2 -> 1).
CTX_SIZE=262144
PARALLEL=1
# Compute-buffer geometry. This endpoint passed neither flag until 2026-10-03,
# so it ran on llama.cpp's own defaults (2048/512); the pair is now 4096/2048.
# Both are tunables because the pair is a measurement, not a law — and the
# measurement is CARD-SPECIFIC. On the 2x R9700 layer-split sibling the ubatch
# curve peaks at 512 and every step up costs GiB of compute buffer for no
# speed (see frank-medium-batch-ubatch). On THIS CUDA 3090 the curve climbs
# monotonically to 2048: 710 -> 1188 t/s prefill from ubatch 1024 to 2048 on
# the 35B MoE sweep, 1944 -> 2423 t/s on the Ornith 8k sweep, with decode flat
# to under 1 t/s on every rung. Ubatch is a prefill knob; decode never moves.
#
# The logical batch is free — two independent sweeps agree that at a fixed
# ubatch, 2048/4096/8192 give identical prefill speed AND identical peak VRAM,
# because the compute buffers are sized by the ubatch alone. Doubling it costs
# nothing and buys nothing; it is here because it is the value the pair was
# measured at.
#
# THE RISK IS ALL IN THE UBATCH. The 1024 -> 2048 step cost ~1,244 MiB of peak
# graph VRAM on the 35B MoE (7,330 -> 8,574 MiB); this 9B hybrid has far
# smaller compute tensors, so the delta should be well under that, but the
# predecessor Q5_K_M shape recorded "ubatch 2048 failed the scratch boot" at
# ~3,571 MiB free. The Q8_0 rung ships UBATCH 1024 (Frank, 2026-10-05: lower
# ubatch FIRST — the measured 616 MiB step is the first VRAM the Q8 weights
# claw back, before any pool move). Compute buffers are allocated at graph
# capture during BOOT, so a failure is loud and fast, not a surprise at the
# first long prompt. MEASURE nvidia-smi free immediately after the deploy:
# under ~1.5 GiB, or a boot failure, step the POOL down next (12,288-token
# increments, ~432 MiB each at the Ornith 36 KiB/token f16 KV rate).
BATCH_SIZE=4096
UBATCH_SIZE=1024
# The KV contract (2026-10-05, Frank): f16 K + f16 V — NO KV quantization.
# The q8_0-V arithmetic was written and OVERRULED. The Q8 weights eat the
# VRAM that funded f16 V on 2026-10-03, so the remedy ladder is UBATCH FIRST
# (2048 -> 1024, the measured 616 MiB rung), then POOL STEPS — and
# guess-and-check on the live card is the accepted method: boot, read
# nvidia-smi, step down until the floor holds. --flash-attn stays pinned on
# in the serve script: it is the fast path for f16 KV.
CACHE_TYPE_K=f16
CACHE_TYPE_V=f16
NGL=99
-19
View File
@@ -1,19 +0,0 @@
[Unit]
Description=llama.cpp MiMo-V2.6-Distill-Qwen-9B Q6_K GGUF (qwen35 hybrid, 2x200704, unquantized f16 KV, mmproj vision) on the RTX 3090 (dankclaw, frank-small)
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=dank
Group=dank
# CUDA pins its staging buffers, so the process needs the memlock budget even
# though the weights themselves are copied into VRAM.
LimitMEMLOCK=infinity
Environment="FRANK_SMALL_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-small.env"
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-small.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
+107
View File
@@ -0,0 +1,107 @@
#!/bin/sh
# hfget.sh -- fetch a Hugging Face repo with curl and jq, and nothing else.
#
# The engine and its tooling do not depend on Python, so `huggingface-cli` is not assumed. The API
# gives a file listing as JSON and every file has a stable resolve/ URL, which is all a downloader
# needs.
#
# scripts/hfget.sh <repo-id> [dest-dir] [revision]
#
# Set HF_TOKEN for a gated or private repo. Re-running resumes: a file whose local size already
# matches the listing is skipped and a short one is continued with `curl -C -`, so an interrupted
# transfer of a 300 GB checkpoint costs only what it had not finished.
#
# WEIGHT DUPLICATES ARE SKIPPED. Many repos carry the same tensors several times over -- safetensors
# beside the older pickle .bin, an ONNX export, a GGUF conversion -- and on a large checkpoint each
# copy is hundreds of gigabytes of the same numbers. When the listing holds any .safetensors, every
# other weight format is left behind.
#
# HFGET_JOBS concurrent transfers, default 4. These are long sequential reads: a few streams
# saturate the link and more only makes the destination seek.
set -e
# ---- child mode: one file, named by "<size><tab><path>" in $1, everything else inherited -------
# sh cannot export a function, so parallelism is one re-entry of this script per file under
# `xargs -P` rather than a worker pool.
if [ -n "$HFGET_CHILD" ]; then
size=${1%% *}; path=${1#* }
out=$HFGET_DEST/$path
if [ -f "$out" ]; then
cur=$(wc -c < "$out")
if [ "$cur" = "$size" ]; then echo " = $path"; exit 0; fi
echo " + $path (resuming at $cur of $size)"
else
echo " > $path"
fi
mkdir -p "$(dirname "$out")"
if [ -n "$HF_TOKEN" ]; then
curl -sSL --fail --retry 5 --retry-delay 5 --retry-all-errors -C - \
-H "Authorization: Bearer $HF_TOKEN" \
-o "$out" "https://huggingface.co/$HFGET_REPO/resolve/$HFGET_REV/$path"
else
curl -sSL --fail --retry 5 --retry-delay 5 --retry-all-errors -C - \
-o "$out" "https://huggingface.co/$HFGET_REPO/resolve/$HFGET_REV/$path"
fi
got=$(wc -c < "$out")
[ "$got" = "$size" ] || { echo "hfget: $path is $got bytes, listing says $size" >&2; exit 1; }
exit 0
fi
# ---- parent ------------------------------------------------------------------------------------
repo=$1
[ -n "$repo" ] || { echo "usage: hfget.sh <repo-id> [dest-dir] [revision]" >&2; exit 2; }
dest=${2:-$HOME/models/$repo}
rev=${3:-main}
jobs=${HFGET_JOBS:-4}
command -v jq >/dev/null || { echo "hfget: jq is required" >&2; exit 2; }
echo "hfget: listing $repo@$rev"
# THE LISTING IS PAGINATED AND THE PAGE SIZE IS SMALL. A large checkpoint runs to a hundred-odd
# shards and the API returns fifty entries with a `Link: ...; rel="next"` header. Taking only the
# first page is the worst possible failure here: every file it did fetch is the right length, so
# the size check passes and the converter is handed a checkpoint missing two thirds of its
# tensors. Follow the cursor until there is no next link.
api="https://huggingface.co/api/models/$repo/tree/$rev?recursive=true&expand=true"
listing='[]'
page=0
while [ -n "$api" ]; do
hdr=$(mktemp "${TMPDIR:-/tmp}/hfhdr.XXXXXX")
if [ -n "$HF_TOKEN" ]; then
body=$(curl -sSL --fail-with-body -D "$hdr" -H "Authorization: Bearer $HF_TOKEN" "$api")
else
body=$(curl -sSL --fail-with-body -D "$hdr" "$api")
fi
[ "$(printf '%s' "$body" | jq -r 'type')" = "array" ] || {
rm -f "$hdr"
echo "hfget: $(printf '%s' "$body" | jq -r '.error // .message // .')" >&2
echo "hfget: a gated or private repo needs HF_TOKEN in the environment" >&2; exit 1; }
listing=$(printf '%s\n%s' "$listing" "$body" | jq -s 'add')
api=$(tr -d '\r' < "$hdr" | sed -n 's/^[Ll]ink: *<\([^>]*\)>; *rel="next".*/\1/p' | head -1)
rm -f "$hdr"
page=$((page + 1))
[ "$page" -gt 200 ] && { echo "hfget: listing did not terminate after 200 pages" >&2; exit 1; }
done
echo "hfget: $page page(s) of listing"
have_st=$(printf '%s' "$listing" | jq '[.[] | select(.path | endswith(".safetensors"))] | length')
sel='.[] | select(.type == "file")'
[ "$have_st" -gt 0 ] && sel="$sel"' | select((.path | test("\\.(bin|pth|msgpack|h5|onnx|onnx_data|gguf)$")) | not)'
list=$(mktemp "${TMPDIR:-/tmp}/hfget.XXXXXX")
trap 'rm -f "$list"' EXIT INT TERM
printf '%s' "$listing" | jq -r "$sel"' | "\(.size)\t\(.path)"' > "$list"
n=$(wc -l < "$list")
[ "$n" -gt 0 ] || { echo "hfget: the listing has no files" >&2; exit 1; }
need=$(awk -F'\t' '{s += $1} END {printf "%.1f", s / 1073741824}' "$list")
mkdir -p "$dest"
free=$(df -BG --output=avail "$dest" | tail -1 | tr -dc '0-9')
echo "hfget: $n file(s), $need GiB -> $dest"
[ "$have_st" -gt 0 ] && echo "hfget: safetensors present, pickle weight formats skipped"
echo "hfget: $free GiB free on the destination filesystem"
awk -v n="$need" -v f="$free" 'BEGIN { if (n > f) { print "hfget: THE DOWNLOAD DOES NOT FIT"; exit 1 } }'
export HFGET_CHILD=1 HFGET_DEST="$dest" HFGET_REPO="$repo" HFGET_REV="$rev"
self=$(cd "$(dirname "$0")" && pwd)/$(basename "$0")
tr '\n' '\0' < "$list" | xargs -0 -P "$jobs" -I{} sh "$self" "{}" \
|| { echo "hfget: incomplete -- re-run to resume" >&2; exit 1; }
echo "hfget: complete -- $(du -sh "$dest" | cut -f1) in $dest"
+460
View File
@@ -0,0 +1,460 @@
{%- set template_version = "qwen3.8-froggeric-v22.5" %}
{%- set _tool_format = tool_call_format if tool_call_format is defined else 'xml' %}
{%- set image_count = namespace(value=0) %}
{%- set video_count = namespace(value=0) %}
{%- set add_vision_id = add_vision_id if add_vision_id is defined else false %}
{%- set enable_thinking = enable_thinking if enable_thinking is defined else true %}
{%- set auto_disable_thinking_with_tools = auto_disable_thinking_with_tools if auto_disable_thinking_with_tools is defined else false %}
{%- if preserve_reasoning is defined and preserve_reasoning is not none %}
{%- set _preserve_thinking = preserve_reasoning %}
{%- elif preserve_thinking is defined and preserve_thinking is not none %}
{%- set _preserve_thinking = preserve_thinking %}
{%- else %}
{%- set _preserve_thinking = true %}
{%- endif %}
{%- set max_tool_arg_chars = max_tool_arg_chars if max_tool_arg_chars is defined else 0 %}
{%- set max_tool_response_chars = max_tool_response_chars if max_tool_response_chars is defined else 0 %}
{%- set _default_reasoning_effort = 'medium' %}
{%- set _has_tools = (tools is defined and tools and tools is iterable and tools is not mapping) %}
{%- set _effort_raw = (reasoning_effort | string | lower) if reasoning_effort is defined and reasoning_effort is not none else _default_reasoning_effort %}
{%- set _initial_thinking = enable_thinking %}
{%- if _effort_raw in ('none', 'off') %}
{%- set _initial_thinking = false %}
{%- set _initial_effort = 'medium' %}
{%- elif _effort_raw in ('minimal', 'low') %}
{%- set _initial_effort = 'low' %}
{%- elif _effort_raw in ('high', 'xhigh', 'max', 'ultracode', 'extreme') %}
{%- set _initial_effort = 'xhigh' %}
{%- else %}
{%- set _initial_effort = 'medium' %}
{%- endif %}
{%- set ns_state = namespace(thinking=_initial_thinking, effort=_initial_effort) %}
{%- if auto_disable_thinking_with_tools and _has_tools %}
{%- set ns_state.thinking = false %}
{%- endif %}
{%- for msg in messages %}
{%- if msg.role == 'system' or msg.role == 'developer' or msg.role == 'user' %}
{%- if msg.content is string %}
{%- if '<|think_off|>' in msg.content %}
{%- set ns_state.thinking = false %}
{%- elif '<|think_on|>' in msg.content %}
{%- set ns_state.thinking = true %}
{%- elif '<|think_xhigh|>' in msg.content or '<|think_high|>' in msg.content or '<|think_ultracode|>' in msg.content or '<|think_extreme|>' in msg.content or '<|think_max|>' in msg.content %}
{%- set ns_state.thinking = true %}
{%- set ns_state.effort = 'xhigh' %}
{%- elif '<|think_low|>' in msg.content or '<|think_minimal|>' in msg.content %}
{%- set ns_state.thinking = true %}
{%- set ns_state.effort = 'low' %}
{%- elif '<|think_medium|>' in msg.content %}
{%- set ns_state.thinking = true %}
{%- set ns_state.effort = 'medium' %}
{%- endif %}
{%- elif msg.content is iterable and msg.content is not mapping %}
{%- for item in msg.content %}
{%- if item is string %}
{%- set _item_text = item %}
{%- elif item is mapping and 'text' in item and item.text is string %}
{%- set _item_text = item.text %}
{%- else %}
{%- set _item_text = '' %}
{%- endif %}
{%- if _item_text %}
{%- if '<|think_off|>' in _item_text %}
{%- set ns_state.thinking = false %}
{%- elif '<|think_on|>' in _item_text %}
{%- set ns_state.thinking = true %}
{%- elif '<|think_xhigh|>' in _item_text or '<|think_high|>' in _item_text or '<|think_ultracode|>' in _item_text or '<|think_extreme|>' in _item_text or '<|think_max|>' in _item_text %}
{%- set ns_state.thinking = true %}
{%- set ns_state.effort = 'xhigh' %}
{%- elif '<|think_low|>' in _item_text or '<|think_minimal|>' in _item_text %}
{%- set ns_state.thinking = true %}
{%- set ns_state.effort = 'low' %}
{%- elif '<|think_medium|>' in _item_text %}
{%- set ns_state.thinking = true %}
{%- set ns_state.effort = 'medium' %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- set reasoning_instructions = '' %}
{%- if ns_state.thinking %}
{%- if ns_state.effort == 'xhigh' %}
{%- set reasoning_instructions = 'Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.' %}
{%- elif ns_state.effort == 'low' %}
{%- set reasoning_instructions = 'Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration.' %}
{%- endif %}
{%- endif %}
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
{%- if content is string %}
{{- content }}
{%- elif content is iterable and content is not mapping %}
{%- for item in content %}
{%- if item is mapping %}
{%- if item.type == 'image' or 'image' in item or 'image_url' in item %}
{%- if is_system_content %}
{{- raise_exception('System message cannot contain images.') }}
{%- endif %}
{%- if do_vision_count %}
{%- set image_count.value = image_count.value + 1 %}
{%- endif %}
{%- if add_vision_id %}
{{- 'Picture ' ~ image_count.value ~ ': ' }}
{%- endif %}
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
{%- elif item.type == 'video' or 'video' in item or 'video_url' in item %}
{%- if is_system_content %}
{{- raise_exception('System message cannot contain videos.') }}
{%- endif %}
{%- if do_vision_count %}
{%- set video_count.value = video_count.value + 1 %}
{%- endif %}
{%- if add_vision_id %}
{{- 'Video ' ~ video_count.value ~ ': ' }}
{%- endif %}
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
{%- elif 'text' in item %}
{{- item.text }}
{%- else %}
{{- raise_exception('Unexpected item type in content.') }}
{%- endif %}
{%- else %}
{{- item | string }}
{%- endif %}
{%- endfor %}
{%- elif content is none or content is undefined %}
{{- '' }}
{%- else %}
{{- raise_exception('Unexpected content type.') }}
{%- endif %}
{%- endmacro %}
{%- if not messages %}
{{- raise_exception('No messages provided.') }}
{%- endif %}
{%- set head = namespace(count=0, seen_non_system=false) %}
{%- for message in messages %}
{%- set _is_sys = (message.role == 'system' or message.role == 'developer') %}
{%- if _is_sys and not head.seen_non_system %}
{%- set head.count = head.count + 1 %}
{%- else %}
{%- set head.seen_non_system = true %}
{%- endif %}
{%- endfor %}
{%- set sys_state = namespace(content='') %}
{%- for message in messages[:head.count] %}
{%- set _part = render_content(message.content, false, true) | trim %}
{%- if '<|think_off|>' in _part %}{%- set _part = _part.split('<|think_off|>') | join('') | trim %}{%- endif %}
{%- if '<|think_on|>' in _part %}{%- set _part = _part.split('<|think_on|>') | join('') | trim %}{%- endif %}
{%- if '<|think_xhigh|>' in _part %}{%- set _part = _part.split('<|think_xhigh|>') | join('') | trim %}{%- endif %}
{%- if '<|think_high|>' in _part %}{%- set _part = _part.split('<|think_high|>') | join('') | trim %}{%- endif %}
{%- if '<|think_ultracode|>' in _part %}{%- set _part = _part.split('<|think_ultracode|>') | join('') | trim %}{%- endif %}
{%- if '<|think_extreme|>' in _part %}{%- set _part = _part.split('<|think_extreme|>') | join('') | trim %}{%- endif %}
{%- if '<|think_max|>' in _part %}{%- set _part = _part.split('<|think_max|>') | join('') | trim %}{%- endif %}
{%- if '<|think_medium|>' in _part %}{%- set _part = _part.split('<|think_medium|>') | join('') | trim %}{%- endif %}
{%- if '<|think_low|>' in _part %}{%- set _part = _part.split('<|think_low|>') | join('') | trim %}{%- endif %}
{%- if '<|think_minimal|>' in _part %}{%- set _part = _part.split('<|think_minimal|>') | join('') | trim %}{%- endif %}
{%- if _part %}
{%- if sys_state.content %}
{%- set sys_state.content = sys_state.content ~ '\n\n' ~ _part %}
{%- else %}
{%- set sys_state.content = _part %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- set _sc = sys_state.content %}
{%- set _msgs = messages[head.count:] %}
{%- if _has_tools %}
{{- '<|im_start|>system\n' }}
{%- if reasoning_instructions %}
{{- reasoning_instructions + '\n\n' }}
{%- endif %}
{{- '# Tools\n\nYou have access to the following functions:\n\n<tools>' }}
{%- for tool in tools %}
{{- '\n' }}
{{- tool | tojson }}
{%- endfor %}
{{- '\n</tools>' }}
{%- if _tool_format == 'json' %}
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n' }}
{%- if ns_state.thinking %}
{{- '<think>\nBrief explanation of tool call\n</think>\n' }}
{%- endif %}
{{- '<tool_call>\n{"name": "example_function_name", "arguments": {"example_parameter_1": "value_1", "example_parameter_2": "This is the value for the second parameter"}}\n</tool_call>\n\n<IMPORTANT>\nReminder:\n' }}
{%- if ns_state.thinking %}
{{- '- You can use the <think></think> block to plan your next tool call OR to synthesize data and formulate your final response to the user.\n- ALL explanation and reasoning MUST be placed strictly inside the <think></think> block.\n' }}
{%- endif %}
{{- '- Function calls MUST follow the specified format: a single JSON object with "name" and "arguments" keys inside <tool_call></tool_call> XML tags.\n' }}
{%- if ns_state.thinking %}
{{- '- If you choose to call a tool, you MUST output the <tool_call> block IMMEDIATELY after thinking, with NO conversational text before it.\n' }}
{%- else %}
{{- '- If you choose to call a tool, you MUST output the <tool_call> block IMMEDIATELY, with NO conversational text before it.\n' }}
{%- endif %}
{{- '- The <tool_call> tag MUST be at the very beginning of a new line, with NO spaces or indentation before it.\n- To call multiple functions, output a separate, completely closed <tool_call></tool_call> block for EACH function. Do NOT nest <tool_call> blocks.\n- If you have all necessary data, provide your final answer directly to the user without any tool call.\n</IMPORTANT>' }}
{%- else %}
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n' }}
{%- if ns_state.thinking %}
{{- '<think>\nBrief explanation of tool call\n</think>\n' }}
{%- endif %}
{{- '<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n' }}
{%- if ns_state.thinking %}
{{- '- You can use the <think></think> block to plan your next tool call OR to synthesize data and formulate your final response to the user.\n- ALL explanation and reasoning MUST be placed strictly inside the <think></think> block.\n' }}
{%- endif %}
{{- '- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> XML tags.\n' }}
{%- if ns_state.thinking %}
{{- '- If you choose to call a tool, you MUST output the <tool_call> block IMMEDIATELY after thinking, with NO conversational text before it.\n' }}
{%- else %}
{{- '- If you choose to call a tool, you MUST output the <tool_call> block IMMEDIATELY, with NO conversational text before it.\n' }}
{%- endif %}
{{- '- The <tool_call> and <function> tags MUST be at the very beginning of a new line, with NO spaces or indentation before them.\n- To call multiple functions, output a separate, completely closed <tool_call></tool_call> block for EACH function. Do NOT nest <tool_call> blocks.\n- If you have all necessary data, provide your final answer directly to the user without any tool call.\n</IMPORTANT>' }}
{%- endif %}
{%- if _sc %}
{{- '\n\n' + _sc }}
{%- endif %}
{{- '<|im_end|>\n' }}
{%- else %}
{%- if _sc %}
{{- '<|im_start|>system\n' + (reasoning_instructions + '\n\n' if reasoning_instructions else '') + _sc + '<|im_end|>\n' }}
{%- elif reasoning_instructions %}
{{- '<|im_start|>system\n' + reasoning_instructions + '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- set _last_idx = _msgs | length - 1 %}
{%- set ns = namespace(multi_step_tool=true, last_query_index=_last_idx) %}
{%- for message in _msgs[::-1] %}
{%- set index = (_msgs | length - 1) - loop.index0 %}
{%- if ns.multi_step_tool and message.role == 'user' %}
{%- set _rc = render_content(message.content, false) | trim %}
{%- if not (_rc.startswith('<tool_response>') and _rc.endswith('</tool_response>')) %}
{%- set ns.multi_step_tool = false %}
{%- set ns.last_query_index = index %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if ns.multi_step_tool %}
{%- if _last_idx > 50 %}
{%- set ns.last_query_index = _last_idx %}
{%- else %}
{%- set ns.last_query_index = 0 %}
{%- endif %}
{%- endif %}
{%- set ns2 = namespace(prev_role='', consecutive_failures=0) %}
{%- for message in _msgs %}
{%- set is_system = (message.role == "system" or message.role == "developer") %}
{%- set content = render_content(message.content, true, is_system) | trim %}
{%- if is_system or message.role == 'user' %}
{%- if '<|think_off|>' in content %}{%- set content = content.split('<|think_off|>') | join('') | trim %}{%- endif %}
{%- if '<|think_on|>' in content %}{%- set content = content.split('<|think_on|>') | join('') | trim %}{%- endif %}
{%- if '<|think_xhigh|>' in content %}{%- set content = content.split('<|think_xhigh|>') | join('') | trim %}{%- endif %}
{%- if '<|think_high|>' in content %}{%- set content = content.split('<|think_high|>') | join('') | trim %}{%- endif %}
{%- if '<|think_ultracode|>' in content %}{%- set content = content.split('<|think_ultracode|>') | join('') | trim %}{%- endif %}
{%- if '<|think_extreme|>' in content %}{%- set content = content.split('<|think_extreme|>') | join('') | trim %}{%- endif %}
{%- if '<|think_max|>' in content %}{%- set content = content.split('<|think_max|>') | join('') | trim %}{%- endif %}
{%- if '<|think_medium|>' in content %}{%- set content = content.split('<|think_medium|>') | join('') | trim %}{%- endif %}
{%- if '<|think_low|>' in content %}{%- set content = content.split('<|think_low|>') | join('') | trim %}{%- endif %}
{%- if '<|think_minimal|>' in content %}{%- set content = content.split('<|think_minimal|>') | join('') | trim %}{%- endif %}
{%- endif %}
{%- if is_system %}
{{- '<|im_start|>system\n' + content + '<|im_end|>\n' }}
{%- elif message.role == 'user' %}
{%- set ns2.consecutive_failures = 0 %}
{{- '<|im_start|>user\n' + content + '<|im_end|>\n' }}
{%- elif message.role == 'assistant' %}
{%- set reasoning_content = '' %}
{%- set _explicit_reasoning = '' %}
{%- if message.reasoning_content is defined and message.reasoning_content is not none %}
{%- if message.reasoning_content is string %}
{%- set _explicit_reasoning = message.reasoning_content %}
{%- else %}
{%- set _explicit_reasoning = message.reasoning_content | string %}
{%- endif %}
{%- elif message.thinking is defined and message.thinking is not none %}
{%- if message.thinking is string %}
{%- set _explicit_reasoning = message.thinking %}
{%- else %}
{%- set _explicit_reasoning = message.thinking | string %}
{%- endif %}
{%- elif message.reasoning is defined and message.reasoning is not none %}
{%- if message.reasoning is string %}
{%- set _explicit_reasoning = message.reasoning %}
{%- else %}
{%- set _explicit_reasoning = message.reasoning | string %}
{%- endif %}
{%- endif %}
{%- if _explicit_reasoning %}
{%- set _lead_end = '' %}
{%- if content.startswith('<think>') and '</think>' in content %}
{%- set _lead_end = '</think>' %}
{%- elif content.startswith('<thinking>') and '</thinking>' in content %}
{%- set _lead_end = '</thinking>' %}
{%- elif content.startswith('</think>') %}
{%- set _lead_end = '</think>' %}
{%- elif content.startswith('</thinking>') %}
{%- set _lead_end = '</thinking>' %}
{%- endif %}
{%- if _lead_end %}
{%- set content = content.split(_lead_end)[-1].lstrip('\n') %}
{%- endif %}
{%- set reasoning_content = _explicit_reasoning %}
{%- else %}
{%- set _think_end = '' %}
{%- if content.startswith('</think>') %}
{%- set _think_end = '</think>' %}
{%- elif content.startswith('</thinking>') %}
{%- set _think_end = '</thinking>' %}
{%- elif '\n</think>' in content %}
{%- set _think_end = '\n</think>' %}
{%- elif '\n</thinking>' in content %}
{%- set _think_end = '\n</thinking>' %}
{%- elif '\n</ think>' in content %}
{%- set _think_end = '\n</ think>' %}
{%- elif '\n</think >' in content %}
{%- set _think_end = '\n</think >' %}
{%- elif content.startswith('<think>') and '</think>' in content %}
{%- set _think_end = '</think>' %}
{%- elif content.startswith('<thinking>') and '</thinking>' in content %}
{%- set _think_end = '</thinking>' %}
{%- endif %}
{%- if _think_end %}
{%- if 'thinking' in _think_end %}
{%- set _think_start = '<thinking>' %}
{%- else %}
{%- set _think_start = '<think>' %}
{%- endif %}
{%- set reasoning_content = content.split(_think_end)[0].rstrip('\n') %}
{%- if _think_start in reasoning_content %}
{%- set reasoning_content = reasoning_content.split(_think_start)[-1].lstrip('\n') %}
{%- endif %}
{%- set content = content.split(_think_end)[-1].lstrip('\n') %}
{%- endif %}
{%- endif %}
{%- set reasoning_content = reasoning_content | trim %}
{%- if (_preserve_thinking or loop.index0 > ns.last_query_index) %}
{{- '<|im_start|>assistant\n<think>\n' + reasoning_content + '\n</think>\n\n' + content }}
{%- else %}
{{- '<|im_start|>assistant\n' + content }}
{%- endif %}
{%- if message.tool_calls is defined and message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined and tool_call.function is not none %}
{%- set tc = tool_call.function %}
{%- else %}
{%- set tc = tool_call %}
{%- endif %}
{%- set tc_name = tc.name if (tc.name is defined and tc.name is not none) else '' %}
{%- if _tool_format == 'json' %}
{%- if loop.first %}
{%- if content | trim %}
{{- '\n\n' }}
{%- endif %}
{%- else %}
{{- '\n' }}
{%- endif %}
{%- set _args = '{}' %}
{%- if tc.arguments is defined and tc.arguments is not none %}
{%- if tc.arguments is mapping %}
{%- set _args = tc.arguments | tojson %}
{%- elif tc.arguments is string %}
{%- if tc.arguments %}
{%- set _args = tc.arguments %}
{%- endif %}
{%- else %}
{%- set _args = tc.arguments | tojson %}
{%- endif %}
{%- endif %}
{{- '<tool_call>\n{"name": ' }}{{- tc_name | tojson }}{{- ', "arguments": ' }}{{- _args }}{{- '}\n</tool_call>' }}
{%- else %}
{%- if loop.first %}
{%- if content | trim %}
{{- '\n\n<tool_call>\n<function=' + tc_name + '>\n' }}
{%- else %}
{{- '<tool_call>\n<function=' + tc_name + '>\n' }}
{%- endif %}
{%- else %}
{{- '\n<tool_call>\n<function=' + tc_name + '>\n' }}
{%- endif %}
{%- if tc.arguments is defined and tc.arguments is not none %}
{%- if tc.arguments is mapping %}
{%- for args_name, args_value in tc.arguments.items() %}
{{- '<parameter=' + args_name + '>\n' }}
{%- if args_value is string %}
{%- set _av = args_value %}
{%- else %}
{%- set _av = args_value | tojson %}
{%- endif %}
{%- if max_tool_arg_chars > 0 and _av | length > max_tool_arg_chars %}
{{- _av[:max_tool_arg_chars] + '\n[TRUNCATED - original length ' ~ (_av | length | string) ~ ' chars]' }}
{%- else %}
{{- _av }}
{%- endif %}
{{- '\n</parameter>\n' }}
{%- endfor %}
{%- else %}
{%- if tc.arguments is string %}
{%- set _raw_args = tc.arguments %}
{%- else %}
{%- set _raw_args = tc.arguments | tojson %}
{%- endif %}
{%- if _raw_args %}
{%- if max_tool_arg_chars > 0 and _raw_args | length > max_tool_arg_chars %}
{{- _raw_args[:max_tool_arg_chars] + '\n[TRUNCATED - original length ' ~ (_raw_args | length | string) ~ ' chars]' }}
{%- else %}
{{- _raw_args }}
{%- endif %}
{%- endif %}
{%- endif %}
{%- endif %}
{{- '</function>\n</tool_call>' }}
{%- endif %}
{%- endfor %}
{%- endif %}
{{- '<|im_end|>\n' }}
{%- elif message.role == 'tool' %}
{%- set _content_lower = content | lower %}
{%- set _content_head = _content_lower[:120] %}
{%- set _is_code_or_grep = ('throw new ' in _content_lower or 'throw error' in _content_lower or 'console.error' in _content_lower or 'logger.error' in _content_lower or 'logging.error' in _content_lower or 'import ' in _content_head or 'def ' in _content_head or 'function ' in _content_head) %}
{%- set _exit_code_zero = ('exit code: 0' in _content_head or 'process exited with code 0' in _content_head) %}
{%- set _error_field_ok = ('"error": null' in _content_head or '"error":null' in _content_head or '"error": false' in _content_head or '"error":false' in _content_head or '"error": ""' in _content_head or '"error":""' in _content_head) %}
{%- set _strong_error = (('"error":' in _content_head and not _error_field_ok) or '"status": "error"' in _content_head or '"status":"error"' in _content_head or 'traceback (most recent call last):' in _content_head or 'command not found' in _content_head or 'invalid syntax' in _content_head or 'fatal:' in _content_head or (('exit code: ' in _content_head or 'process exited with code' in _content_head) and not _exit_code_zero) or _content_head.startswith('exception:') or _content_head.startswith('failed to ')) %}
{%- set _weak_error = ('error:' in _content_head or 'err!' in _content_head) %}
{%- set _weak_suppressed = ('$ ' in _content_head or 'took ' in _content_head or content | length >= 600) %}
{%- if not _is_code_or_grep and (_strong_error or (_weak_error and not _weak_suppressed)) %}
{%- set ns2.consecutive_failures = ns2.consecutive_failures + 1 %}
{%- else %}
{%- set ns2.consecutive_failures = 0 %}
{%- endif %}
{%- if ns2.prev_role != 'tool' %}
{{- '<|im_start|>user' }}
{%- endif %}
{%- set _is_json_payload = (_tool_format == 'json' and content | trim | length > 0 and (content | trim)[:1] in ('{', '[')) %}
{%- if not _is_json_payload and max_tool_response_chars > 0 and content | length > max_tool_response_chars %}
{%- set content = content[:max_tool_response_chars] + '\n[TRUNCATED - original length ' ~ (content | length | string) ~ ' chars]' %}
{%- endif %}
{{- '\n<tool_response>\n' + content }}
{%- if ns2.consecutive_failures >= 2 %}
{{- '\n\n⚠️ SYSTEM WARNING: ' ~ ns2.consecutive_failures ~ ' consecutive tool errors detected. Your previous approach is incorrect. You MUST use a fundamentally different approach or corrected arguments.' }}
{%- elif ns2.consecutive_failures == 1 %}
{{- '\n\n⚠️ SYSTEM WARNING: The previous tool call returned an error. Diagnose the failure and retry with completely corrected arguments.' }}
{%- endif %}
{{- '\n</tool_response>' }}
{%- if loop.last %}
{{- '<|im_end|>\n' }}
{%- else %}
{%- set _next_role = _msgs[loop.index0 + 1].role %}
{%- if _next_role != 'tool' %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- else %}
{{- '<|im_start|>user\n[' + message.role + ']: ' + content + '<|im_end|>\n' }}
{%- endif %}
{%- set ns2.prev_role = message.role %}
{%- endfor %}
{%- if add_generation_prompt %}
{{- '<|im_start|>assistant\n' }}
{%- if not ns_state.thinking %}
{{- '<think>\n\n</think>\n\n' }}
{%- else %}
{{- '<think>\n' }}
{%- endif %}
{%- endif %}
+118 -64
View File
@@ -1,30 +1,32 @@
#!/usr/bin/env bash
# serve-frank-large.sh — frank-large endpoint: halogen (closed-source
# C++/HIP engine, image ghcr.io/peonist-ai/halogen, digest-pinned in
# frank-large.env) serving peonist-ai/halogen-qwen3.8-27b on Dankclaw's
# Strix Halo iGPU, host :7770. 2026-10-05 cutover from the ROCmFPX
# llama.cpp shape — revert the swap commit's trio to roll back (payload
# and fork build stay on disk).
# serve-frank-large.sh — frank-large endpoint: halogen-flash-server 0.16.2
# (closed-source engine, image ghcr.io/peonist-ai/halogen-flash-server,
# digest-pinned in frank-large.env) serving the Quat3rnion abliterated
# Swift1.5 finetune of peonist's Qwen3.8-Flash-Next v2 checkpoint on
# Dankclaw's Strix Halo iGPU, host :7770. 2026-10-06 cutover from the
# halogen 27B-dense shape — revert the swap commit's trio to roll back
# (the 27B payload stays on disk).
#
# The engine is driven by `podman run` DIRECTLY (the fleet's frank-primary
# pattern): the image entrypoint `halogen-docker` in default "all" mode
# starts engine + OpenAI frontend in one container. The frontend listens on
# 8731 inside (HALOGEN_API_PORT default) and is published as host 7770; the
# engine's own token port (8730, no auth) stays unpublished on the
# container's loopback. The image ships NO default checkpoint:
# HALOGEN_CHECKPOINT selects the file inside the /models mount.
# pattern): the image starts engine + OpenAI frontend in one container.
# The frontend listens on 8731 inside (HALOGEN_API_PORT default) and is
# published as host 7770; the engine's own token port (8730, no auth)
# stays unpublished on the container's loopback. HALOGEN_CHECKPOINT
# selects the file inside the /models mount; the n-gram lookup table
# (HALOGEN_NGRAM_TABLE, the card sets it explicitly though the documented
# fallback is "beside the checkpoint") and the vision tower
# (HALOGEN_VISION_TOWER) ride in the same directory.
#
# Gates mirror the deployer EXACTLY (the 2026-10-01 crash-loop lesson —
# serve gate and deploy gate disagreeing = green deploy, red unit):
# tokenizer directory present with every named file a FLAT REGULAR FILE —
# the model card ships `tokenizer/` as a flat directory and the engine
# defaults HALOGEN_TOKENIZER to /tokenizer, so a HF cache snapshot's
# symlinks into blobs/ dangle inside the mount; GPU device nodes present.
# checkpoint/n-gram/vision byte-exact, tokenizer directory present with
# every named file a FLAT REGULAR FILE (a HF cache snapshot's symlinks
# into blobs/ dangle inside the mount), GPU device nodes present.
#
# Why containers instead of the bare binary (the old frank-large shape):
# the payload is .hgn and only halogen reads it; halogen ships as a
# container; the root unit keeps LimitMEMLOCK=infinity for parity and to
# outlive the login session, exactly like frank-primary's.
# Why containers instead of a bare binary: the payload is .hgn and only
# the flash-server line reads it; it ships as a container; the root unit
# keeps LimitMEMLOCK=infinity for parity and to outlive the login session,
# exactly like frank-primary's.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -44,11 +46,16 @@ set +a
: "${FRANK_LARGE_MODEL_DIR:?FRANK_LARGE_MODEL_DIR must name the payload dir}"
: "${FRANK_LARGE_CHECKPOINT:?FRANK_LARGE_CHECKPOINT must name the .hgn inside it}"
: "${FRANK_LARGE_CHECKPOINT_BYTES:?FRANK_LARGE_CHECKPOINT_BYTES must pin the size}"
: "${FRANK_LARGE_NGRAM_TABLE:?FRANK_LARGE_NGRAM_TABLE must name the lookup table}"
: "${FRANK_LARGE_NGRAM_BYTES:?FRANK_LARGE_NGRAM_BYTES must pin the table size}"
: "${FRANK_LARGE_VISION_TOWER:?FRANK_LARGE_VISION_TOWER must name the tower}"
: "${FRANK_LARGE_VISION_BYTES:?FRANK_LARGE_VISION_BYTES must pin the tower size}"
: "${FRANK_LARGE_TOKENIZER_FILES:?FRANK_LARGE_TOKENIZER_FILES must list the tokenizer dir contents}"
: "${HALOGEN_MODEL_ID:?HALOGEN_MODEL_ID must be the served alias (frank-large)}"
: "${HALOGEN_SLOT_CTX:?HALOGEN_SLOT_CTX must be set}"
: "${HALOGEN_KV_SLOTS:?HALOGEN_KV_SLOTS must be set (1 keeps the drafter on)}"
: "${HALOGEN_DRAFTER:?HALOGEN_DRAFTER must be set (2 = DFlash2)}"
: "${HALOGEN_CTX:?HALOGEN_CTX must be set (per-slot window)}"
: "${HALOGEN_KV_POOL_POSITIONS:?HALOGEN_KV_POOL_POSITIONS must be set}"
: "${HALOGEN_KV_SLOTS:?HALOGEN_KV_SLOTS must be set}"
: "${HALOGEN_SPEC_ADAPT:?HALOGEN_SPEC_ADAPT must be set (0 = head always on)}"
# Capture podman's output BEFORE grepping: under `set -o pipefail` the
# `podman images | grep -q` pattern races (grep exits on first match,
@@ -61,36 +68,41 @@ if ! grep -qF "${FRANK_LARGE_IMAGE#*@}" <<<"$DIGESTS"; then
exit 1
fi
# The checkpoint gate is size-exact: a truncated curl exits 0 on some
# failures and a partial .hgn loads far enough to look alive, so the byte
# count from the model card is the contract (the fleet fetch law).
CKPT="$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_CHECKPOINT"
if [ ! -f "$CKPT" ]; then
echo "frank-large: checkpoint $CKPT missing;" >&2
echo " fetch it: bash scripts/download-frank-large-halogen.sh" >&2
exit 1
fi
CKPT_SIZE="$(stat -c %s "$CKPT")"
if [ "$CKPT_SIZE" != "$FRANK_LARGE_CHECKPOINT_BYTES" ]; then
echo "frank-large: checkpoint is $CKPT_SIZE bytes, expected $FRANK_LARGE_CHECKPOINT_BYTES (partial download?)" >&2
exit 1
fi
# Size-exact gates on all three payload files: a truncated curl exits 0 on
# some failures and a partial .hgn loads far enough to look alive, so the
# byte counts from the Hub tree API are the contract (fleet fetch law).
ck() {
# ck <env-name> <path> <expected-bytes>
if [ ! -f "$2" ]; then
echo "frank-large: $1 $2 missing;" >&2
echo " fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
exit 1
fi
local sz
sz="$(stat -c %s "$2")"
if [ "$sz" != "$3" ]; then
echo "frank-large: $1 is $sz bytes, expected $3 (partial download?)" >&2
exit 1
fi
}
ck checkpoint "$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_CHECKPOINT" "$FRANK_LARGE_CHECKPOINT_BYTES"
ck n-gram-table "$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_NGRAM_TABLE" "$FRANK_LARGE_NGRAM_BYTES"
ck vision-tower "$FRANK_LARGE_MODEL_DIR/$FRANK_LARGE_VISION_TOWER" "$FRANK_LARGE_VISION_BYTES"
# The payload ships the tokenizer as a FLAT DIRECTORY (model card:
# "tokenizer/ flat tokenizer directory"). A HuggingFace cache snapshot
# symlinks each file into a sibling blobs/, and those symlinks dangle
# inside the container mount — so the directory must be real and every
# named file must be a regular file, not a link.
# The payload ships the tokenizer as a FLAT DIRECTORY. A HuggingFace cache
# snapshot symlinks each file into a sibling blobs/, and those symlinks
# dangle inside the container mount — so the directory must be real and
# every named file must be a regular file, not a link.
TOK_DIR="$FRANK_LARGE_MODEL_DIR/tokenizer"
if [ -L "$TOK_DIR" ] || [ ! -d "$TOK_DIR" ]; then
echo "frank-large: tokenizer dir $TOK_DIR missing or a symlink (HF cache snapshot?);" >&2
echo " fetch it: bash scripts/download-frank-large-halogen.sh" >&2
echo " fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
exit 1
fi
for TF in $FRANK_LARGE_TOKENIZER_FILES; do
if [ ! -f "$TOK_DIR/$TF" ] || [ -L "$TOK_DIR/$TF" ]; then
echo "frank-large: tokenizer file $TOK_DIR/$TF missing or a symlink;" >&2
echo " fetch it: bash scripts/download-frank-large-halogen.sh" >&2
echo " fetch it: bash scripts/download-frank-large-flashnext.sh" >&2
exit 1
fi
done
@@ -103,23 +115,55 @@ if [ ! -e "$DEV_KFD" ] || [ ! -d "$DEV_DRI" ]; then
echo "frank-large: $DEV_KFD or $DEV_DRI missing — no gfx1151 to serve on" >&2
exit 1
fi
# Host-RAM preflight — the engine's pin gate, checked BEFORE podman run.
# The engine refuses to pin when MemAvailable is below the checkpoint plus
# a 16 GiB floor: pinned pages cannot be swapped, and under the floor the
# n-gram lookup pages in from disk on every long prompt. A RAM-short host
# (a co-tenant spike, a sibling engine dying) crash-loops the unit — each
# boot dies in ~5 s at the pin gate — until the RAM clears. The 2026-10-06
# cutover hit this: 8 boots, MemAvailable 74.77 GiB vs the 78.11 GiB needed,
# self-resolved in ~2 min when the dying 27B engine freed RAM. Check here and
# WAIT for the RAM to clear, so a transient dip boots once instead of
# crash-looping; if it never clears, refuse loudly.
MEMINFO="${FRANK_LARGE_MEMINFO:-/proc/meminfo}"
PIN_KB=$(( FRANK_LARGE_CHECKPOINT_BYTES / 1024 ))
NEED_KB=$(( PIN_KB + 16 * 1048576 ))
ram_avail_kb() { awk '/^MemAvailable:/{print $2; exit}' "$MEMINFO"; }
MEMAVAIL_KB="$(ram_avail_kb)" || MEMAVAIL_KB=""
if [ -n "$MEMAVAIL_KB" ] && [ "$MEMAVAIL_KB" -lt "$NEED_KB" ]; then
WAIT_S="${FRANK_LARGE_RAM_WAIT:-600}"
GIB() { awk -v kb="$1" 'BEGIN{printf "%.2f", kb/1048576}'; }
echo "frank-large: host RAM below the pin gate — MemAvailable $(GIB "$MEMAVAIL_KB") GiB < needed $(GIB "$NEED_KB") GiB ($(GIB "$PIN_KB") GiB checkpoint + 16.00 GiB floor); waiting up to ${WAIT_S}s for host RAM to clear" >&2
deadline=$(( $(date +%s) + WAIT_S ))
while [ "$(date +%s)" -lt "$deadline" ]; do
sleep 5
MEMAVAIL_KB="$(ram_avail_kb)" || MEMAVAIL_KB=""
if [ -n "$MEMAVAIL_KB" ] && [ "$MEMAVAIL_KB" -ge "$NEED_KB" ]; then
break
fi
done
if [ -z "$MEMAVAIL_KB" ] || [ "$MEMAVAIL_KB" -lt "$NEED_KB" ]; then
echo "frank-large: MemAvailable $(GIB "${MEMAVAIL_KB:-0}") GiB after ${WAIT_S}s; refusing to start (the engine would refuse the pin)" >&2
exit 1
fi
echo "frank-large: MemAvailable cleared to $(GIB "$MEMAVAIL_KB") GiB; starting" >&2
fi
for CTR in ${FRANK_LARGE_LEGACY_CONTAINERS:-} "$FRANK_LARGE_CONTAINER"; do
podman rm -f "$CTR" >/dev/null 2>&1 || true
done
LOG="${FRANK_LARGE_LOG:-/home/dank/logs/frank_large.log}"
mkdir -p "$(dirname "$LOG")"
# for the udev-granted supplementary group, seccomp unconfined for the ROCm
# ioctl surface, and unbounded memlock (the unit's LimitMEMLOCK=infinity is
# inherited, so runc never has to RAISE it). NO --ulimit nofile: Dankclaw's
# hard limit is 524288, rootless runc cannot raise a hard limit and dies
# with "error setting rlimit type 7: operation not permitted" (measured
# 2026-10-05 cutover), and 524288 fds is already ~100x what the engine
# wants; the model card and both sibling serve scripts pass no nofile
# either. No comment lines inside the backslash
# chain — a mid-chain # terminates the continuation (the 2026-10-05
# exit-127 crash loop on the sibling serve script).
podman run -d --name "$FRANK_LARGE_CONTAINER" \
# --ipc=host for the udev-granted supplementary group, seccomp unconfined
# for the ROCm ioctl surface, and unbounded memlock (the unit's
# LimitMEMLOCK=infinity is inherited, so runc never has to RAISE it). NO
# --ulimit nofile: Dankclaw's hard limit is 524288, rootless runc cannot
# raise a hard limit and dies with "error setting rlimit type 7:
# operation not permitted" (measured 2026-10-05 cutover); the model card
# and both sibling serve scripts pass no nofile either. No comment lines
# inside the backslash chain — a mid-chain # terminates the continuation
# (the 2026-10-05 exit-127 crash loop on the sibling serve script).
podman run --name "$FRANK_LARGE_CONTAINER" \
--publish "$FRANK_LARGE_PUBLISH" \
--ipc=host \
--ulimit memlock=-1:-1 \
@@ -128,19 +172,29 @@ podman run -d --name "$FRANK_LARGE_CONTAINER" \
--security-opt seccomp=unconfined \
-e HALOGEN_MODEL_ID="$HALOGEN_MODEL_ID" \
-e HALOGEN_CHECKPOINT="/models/$FRANK_LARGE_CHECKPOINT" \
-e HALOGEN_SLOT_CTX="$HALOGEN_SLOT_CTX" \
-e HALOGEN_KV_SLOTS="${HALOGEN_KV_SLOTS:-1}" \
-e HALOGEN_DRAFTER="$HALOGEN_DRAFTER" \
-e HALOGEN_NGRAM_TABLE="/models/$FRANK_LARGE_NGRAM_TABLE" \
-e HALOGEN_VISION_TOWER="/models/$FRANK_LARGE_VISION_TOWER" \
-e HALOGEN_TOKENIZER=/tokenizer \
-e HALOGEN_CTX="$HALOGEN_CTX" \
-e HALOGEN_KV_POOL_POSITIONS="$HALOGEN_KV_POOL_POSITIONS" \
-e HALOGEN_KV_SLOTS="$HALOGEN_KV_SLOTS" \
-e HALOGEN_HOST_RESERVE_GIB="${HALOGEN_HOST_RESERVE_GIB:-20}" \
-e HALOGEN_SPEC_ADAPT="$HALOGEN_SPEC_ADAPT" \
-e HALOGEN_MAX_TOK="${HALOGEN_MAX_TOK:-8192}" \
-e HALOGEN_MAX_TOKENS_CAP="${HALOGEN_MAX_TOKENS_CAP:-65536}" \
-e HALOGEN_QUEUE_TIMEOUT="${HALOGEN_QUEUE_TIMEOUT:-7200}" \
${HALOGEN_CACHE_MB:+-e HALOGEN_CACHE_MB="$HALOGEN_CACHE_MB"} \
-e HALOGEN_TEMPERATURE="${HALOGEN_TEMPERATURE:-1.0}" \
-e HALOGEN_TOP_P="${HALOGEN_TOP_P:-0.95}" \
-e HALOGEN_TOP_K="${HALOGEN_TOP_K:-20}" \
${HALOGEN_WEIGHTS_LOCK:+-e HALOGEN_WEIGHTS_LOCK="$HALOGEN_WEIGHTS_LOCK"} \
-v "$FRANK_LARGE_MODEL_DIR:/models:ro" \
-v "$FRANK_LARGE_MODEL_DIR/tokenizer:/tokenizer:ro" \
"$FRANK_LARGE_IMAGE" \
> "$LOG" 2>&1
# This script's lifetime IS the container's: podman wait returns the
# container's exit code, so a crash is a nonzero ExecStart and the unit's
# Restart=always is real. systemd stop goes through the unit's ExecStop
# (podman wait alone cannot stop the child).
exec podman wait "$FRANK_LARGE_CONTAINER"
# FOREGROUND `podman run`: the engine's stdout/stderr are this script's
# output, so the boot ledger (kv pool, prompt cache, MTP head, any clamp)
# lands in $LOG as documented. podman run returns the container's exit
# code, so a crash is a nonzero ExecStart and the unit's Restart=always
# stays real; systemd stop goes through the unit's ExecStop and the run
# client forwards SIGTERM to the engine, which drains on it.
+151 -79
View File
@@ -1,38 +1,33 @@
#!/usr/bin/env bash
# serve-frank-primary.sh — frank-primary endpoint (FreeToken PR #587 ROCm
# build, orcarouter Qwen3.8-Flash-Next-Uncensored-FP8, ONE R9700 gfx1201,
# dankserver :7770; 2026-10-05 cutover from the tcclaviger vLLM fork — git
# tag frank-primary-vllm-shapeG-20261003 is the pre-cutover state).
# serve-frank-primary.sh — frank-primary endpoint: radiance (StillDeadcode,
# Apache-2.0) serving the OFFICIAL Qwen/Qwen3.8-Flash-Next-FP8 checkpoint at
# NATIVE block-FP8 precision from a rad-convert container
# (/home/dank/models/rad/qwen4exp-fp8-native.rad), on BOTH R9700s (TP=2,
# wht6 wire), dankserver :7770. 2026-10-05 cutover from FreeToken (PR #587,
# one card; git tag frank-primary-freetoken-pr587-20261005 is the
# pre-cutover state).
#
# The run line below IS the engine argv: the image's entrypoint is the
# engine and its tools sit beside it on PATH. Unlike every predecessor
# there is no per-boot VRAM planner to second-guess: the boot ledger in the
# log states every budget before anything is allocated (weights vs KV
# split, the expert host pool's coverage of the worst case, the PLE pin,
# the P2P answer per pair) — read it after every resize. First-boot shape:
# 2 decode slots (Frank's call), 40 GiB/rank pinned expert pool, fp8 KV,
# MTP depth 3, prefix cache host 4 GiB + disk 64 GiB, PLE pinned in RAM.
#
# The engine is driven by `podman run` DIRECTLY and the run line below IS
# the engine argv: the image carries FreeToken installed at pinned PR head
# 77a7201 (scripts/docker/frank-primary-freetoken.Dockerfile builds it from
# the PR's own docs/install_amd.md stack). Unlike the vLLM fork there is no
# per-boot planner: experts live in pinned host RAM with an LRU GPU cache,
# KV is an explicit token pool, and the boot ledger in the log names
# Shape: 1 GPU (first-boot conservative — the second R9700 sits idle;
# --tensor-parallel-size EXISTS in FreeToken, so a 2-card TP shape is future
# tuning once this single-card shape has a decode baseline), KV pool 300,000
# tokens (24 KiB/token f16 across the 12 full-attention layers = 7.2 GiB;
# clears the harness worst case of 177,356 tokens and admits one NATIVE
# 262,144-token conversation), MoE LRU at 10% of all experts (~12 GiB),
# 2 decode slots. First-boot conservative; `ft ctl cache --kv/--moe` grows
# pools live, and the env comment block carries the resize law. Flag
# surface verified against docs/cli.md of the pinned head AND against
# `ft serve --help` inside the built image; every value here is env-driven
# (frank-primary.env).
# The root unit (scripts/frank-primary.service, User=dank,
# LimitMEMLOCK=infinity) grants the pin budget: ~120 GiB of expert weights
# plus the ~47.7 GiB PLE table pin in host RAM. A plain shell session also
# works, but the unit is the fleet-parity boot-persistent shape.
# LimitMEMLOCK=infinity) grants the pin budget: 2 x 40 GiB expert pools +
# the 47.7 GiB PLE table + 4 GiB prefix host tier — ~132 GiB of pinned
# pages, which is ALSO why this script refuses to start when ttm's
# pages_limit/pages_max cap is under FRANK_PRIMARY_TTM_MIN_MIB (see below).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${FRANK_PRIMARY_ENV_FILE:-$SCRIPT_DIR/frank-primary.env}"
if [ ! -f "$ENV_FILE" ]; then
echo "serve-frank-primary: env file missing: $ENV_FILE" >&2
echo "ERROR: frank-primary env file absent: $ENV_FILE" >&2
exit 1
fi
set -a
@@ -40,58 +35,119 @@ set -a
source "$ENV_FILE"
set +a
: "${FRANK_PRIMARY_IMAGE:?FRANK_PRIMARY_IMAGE must name the FreeToken image}"
: "${FRANK_PRIMARY_MODEL_DIR:?FRANK_PRIMARY_MODEL_DIR must name the FP8 payload}"
: "${FRANK_PRIMARY_MODEL_SHARDS:?FRANK_PRIMARY_MODEL_SHARDS must give the shard count}"
: "${FRANK_PRIMARY_IMAGE:?FRANK_PRIMARY_IMAGE must name the radiance image}"
: "${FRANK_PRIMARY_MODEL_DIR:?FRANK_PRIMARY_MODEL_DIR must name the .rad dir}"
: "${FRANK_PRIMARY_MODEL_FILE:?FRANK_PRIMARY_MODEL_FILE must name the .rad file}"
: "${FRANK_PRIMARY_STATE_DIR:?FRANK_PRIMARY_STATE_DIR must name the /data state dir}"
: "${FRANK_PRIMARY_CONTAINER:?FRANK_PRIMARY_CONTAINER must name the container}"
: "${FRANK_PRIMARY_SERVED_NAME:?FRANK_PRIMARY_SERVED_NAME must name the alias}"
: "${FRANK_PRIMARY_PUBLISH:?FRANK_PRIMARY_PUBLISH must be host:container}"
: "${FRANK_PRIMARY_FT_GPU:?FRANK_PRIMARY_FT_GPU must name the GPU index}"
: "${FRANK_PRIMARY_FT_NUM_TOKENS:?FRANK_PRIMARY_FT_NUM_TOKENS must size the KV pool}"
: "${FRANK_PRIMARY_FT_MAX_RUNNING:?FRANK_PRIMARY_FT_MAX_RUNNING must be set}"
: "${FRANK_PRIMARY_FT_MAX_OUTPUT_TOKENS:?FRANK_PRIMARY_FT_MAX_OUTPUT_TOKENS must be set}"
: "${FRANK_PRIMARY_FT_MOE_RATE:?FRANK_PRIMARY_FT_MOE_RATE must be set}"
: "${FRANK_PRIMARY_PORT:?FRANK_PRIMARY_PORT must be the port}"
: "${FRANK_PRIMARY_TEMPLATE:?FRANK_PRIMARY_TEMPLATE must name the chat template}"
: "${FRANK_PRIMARY_TP:?FRANK_PRIMARY_TP must be set}"
: "${FRANK_PRIMARY_TP_WIRE:?FRANK_PRIMARY_TP_WIRE must be set}"
: "${FRANK_PRIMARY_MAX_SEQS:?FRANK_PRIMARY_MAX_SEQS must be set}"
: "${FRANK_PRIMARY_MAX_MODEL_LEN:?FRANK_PRIMARY_MAX_MODEL_LEN must be set}"
: "${FRANK_PRIMARY_PLACEMENT:?FRANK_PRIMARY_PLACEMENT must be set}"
: "${FRANK_PRIMARY_HOST_POOL_MIB:?FRANK_PRIMARY_HOST_POOL_MIB must be set}"
: "${FRANK_PRIMARY_GPU_HEADROOM_MIB:?FRANK_PRIMARY_GPU_HEADROOM_MIB must be set}"
: "${FRANK_PRIMARY_EXPERT_VS_CACHE:?FRANK_PRIMARY_EXPERT_VS_CACHE must be set}"
: "${FRANK_PRIMARY_KV_DTYPE:?FRANK_PRIMARY_KV_DTYPE must be set}"
: "${FRANK_PRIMARY_SPEC_TOKENS:?FRANK_PRIMARY_SPEC_TOKENS must be set}"
: "${FRANK_PRIMARY_MAX_BATCHED_TOKENS:?FRANK_PRIMARY_MAX_BATCHED_TOKENS must be set}"
: "${FRANK_PRIMARY_PREFIX_HOST_MIB:?FRANK_PRIMARY_PREFIX_HOST_MIB must be set}"
: "${FRANK_PRIMARY_PREFIX_DISK_MIB:?FRANK_PRIMARY_PREFIX_DISK_MIB must be set}"
: "${FRANK_PRIMARY_NGRAM_PLACEMENT:?FRANK_PRIMARY_NGRAM_PLACEMENT must be set}"
: "${FRANK_PRIMARY_TTM_MIN_MIB:?FRANK_PRIMARY_TTM_MIN_MIB must be set}"
: "${FRANK_PRIMARY_RAD_MIN_MIB:?FRANK_PRIMARY_RAD_MIN_MIB must be set}"
# The image gate is EXISTENCE IN STORAGE, not a registry digest: the tag is
# built ON THE HOST (podman build from
# scripts/docker/frank-primary-freetoken.Dockerfile) and local builds carry
# no RepoDigests entry — the 362-line vLLM-era RepoDigests grep refused
# exactly this shape. Pinning lives in the Dockerfile instead: the FreeToken
# source is checked out at a hardcoded PR-head SHA and the base image is
# digest-pinned there. `podman image exists` accepts both a local tag and a
# name@digest ref, and it is one command — no pipefail/grep race (the
# 2026-09-30 lesson) for either shape.
# The image gate is the digest-pinned ref itself: a name@sha256 ref exists
# in storage only if THAT manifest was pulled — a retagged look-alike or a
# newer latest does not satisfy it. `podman image exists` takes a
# name@digest ref in one command — no pipefail/grep race (the 2026-09-30
# lesson). Runs as dank: the unit's process uses dank's ROOTLESS podman
# storage (root unit, User=dank — the fleet's frank-primary shape).
if ! podman image exists "$FRANK_PRIMARY_IMAGE"; then
echo "serve-frank-primary: image absent from rootless storage: $FRANK_PRIMARY_IMAGE" >&2
echo " build it: podman build -t $FRANK_PRIMARY_IMAGE -f scripts/docker/frank-primary-freetoken.Dockerfile scripts/docker/" >&2
echo "ERROR: radiance image absent: $FRANK_PRIMARY_IMAGE" >&2
echo "frank-primary: podman pull docker://stilldeadcode/radiance:latest (as dank)" >&2
exit 1
fi
if [ ! -f "$FRANK_PRIMARY_MODEL_DIR/config.json" ]; then
echo "serve-frank-primary: payload config missing: $FRANK_PRIMARY_MODEL_DIR/config.json" >&2
# The payload gate: the .rad container file, sized. A truncated convert
# would otherwise buy a guaranteed long-timeout boot instead of this fast
# failure. The staging dir / raw checkpoint are NOT this script's concern
# (scripts/build-frank-primary-rad.sh built the file; its rad-info output
# is the record).
RAD="$FRANK_PRIMARY_MODEL_DIR/$FRANK_PRIMARY_MODEL_FILE"
if [ ! -f "$RAD" ]; then
echo "ERROR: .rad container missing: $RAD" >&2
echo "frank-primary: run scripts/build-frank-primary-rad.sh" >&2
exit 1
fi
SHARDS="$(find "$FRANK_PRIMARY_MODEL_DIR" -maxdepth 1 -name 'model-*-of-*.safetensors' | wc -l)"
if [ "$SHARDS" != "$FRANK_PRIMARY_MODEL_SHARDS" ]; then
echo "serve-frank-primary: payload shard count wrong: $FRANK_PRIMARY_MODEL_DIR" >&2
echo " found $SHARDS safetensors, want $FRANK_PRIMARY_MODEL_SHARDS" >&2
echo " re-fetch: scripts/download-frank-primary.sh" >&2
RAD_MIB=$(( $(stat -c %s "$RAD") / 1048576 ))
if [ "$RAD_MIB" -lt "$FRANK_PRIMARY_RAD_MIN_MIB" ]; then
echo "ERROR: .rad container too small: $RAD is ${RAD_MIB} MiB, want >= $FRANK_PRIMARY_RAD_MIN_MIB" >&2
echo "frank-primary: the convert was truncated — rebuild it" >&2
exit 1
fi
if [ ! -f "$FRANK_PRIMARY_MODEL_DIR/model.safetensors.index.json" ]; then
echo "serve-frank-primary: weight-map index missing: $FRANK_PRIMARY_MODEL_DIR/model.safetensors.index.json" >&2
if [ ! -f "$FRANK_PRIMARY_TEMPLATE" ]; then
echo "ERROR: chat template missing: $FRANK_PRIMARY_TEMPLATE" >&2
exit 1
fi
mkdir -p "$FRANK_PRIMARY_STATE_DIR/kvcache/frank-primary"
# The engine's create is NOT recursive: with only the state dir mounted, its
# "cannot create /data/kvcache/frank-primary: No such file or directory"
# left the SSD prefix tier off (measured 2026-10-06 boot ledger). Pre-create
# the full path on the host so the tier opens on first boot.
mkdir -p "$FRANK_PRIMARY_STATE_DIR/tune"
# rad-tune's kernel-selection cache (see the runbook's "Tuning levers"):
# the engine's selector reads $RADIANCE_HOME/tune/<machine>.tune at boot;
# $RADIANCE_HOME defaults to /opt/radiance, which lives in the image, so
# the cache must ride a host mount or it dies with the --rm container
# that wrote it. The dir is pre-created for the same reason as kvcache
# (the engine's create is not recursive).
if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then
echo "serve-frank-primary: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
echo "ERROR: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
exit 1
fi
# TTM preflight: the pinned ledger (~2 x 40 GiB pools + 47.7 GiB PLE + 4 GiB
# prefix host = ~132 GiB) rides TTM's shared pages, capped by ttm's
# pages_limit (the current kernel exposes NO pages_max; the cap function
# takes the min over the knobs that exist). Below FRANK_PRIMARY_TTM_MIN_MIB
# the load dies mid-pin with an allocator error instead of this named
# refusal — raise the knob first (runtime sysfs write; kernel cmdline
# fallback + one reboot only if the file is read-only), see the runbook.
# RAISED 2026-10-05 pre-cutover to 52,428,800 pages = 200 GiB.
ttm_cap_mib() {
# min over the knobs THIS kernel exposes — the current kernel has
# pages_limit only (no pages_max); a knob that is absent is skipped,
# not a zero (a zero would refuse a healthy cap).
local v cur=""
for p in /sys/module/ttm/parameters/pages_limit /sys/module/ttm/parameters/pages_max; do
[ -r "$p" ] || continue
v="$(cat "$p" 2>/dev/null)" || continue
[ -n "$v" ] || continue
v=$(( v * 4 / 1024 ))
if [ "$v" -lt "${cur:-999999999}" ]; then cur=$v; fi
done
echo "${cur:-0}"
}
TTM_CAP="$(ttm_cap_mib)"
if [ "$TTM_CAP" -lt "$FRANK_PRIMARY_TTM_MIN_MIB" ]; then
echo "ERROR: ttm pages cap is ${TTM_CAP} MiB, the pinned ledger needs >= $FRANK_PRIMARY_TTM_MIN_MIB" >&2
echo "frank-primary: raise /sys/module/ttm/parameters/pages_limit (and pages_max if the kernel exposes one) to >= $FRANK_PRIMARY_TTM_MIN_MIB in 4 KiB pages" >&2
exit 1
fi
# A crashed/killed unit can leave the container name registered (systemd
# SIGKILLs conmon before cleanup runs). Clear OUR name plus the legacy
# identities (the vLLM-era frank-primary-qwen, the retired R9V container and
# the ad hoc experiment containers all hold this GPU); all idempotent.
# identities (the FreeToken-era frank-primary-qwen, the retired R9V
# container and the ad hoc experiment containers all hold this GPU); all
# idempotent.
for CTR in $FRANK_PRIMARY_LEGACY_CONTAINERS "$FRANK_PRIMARY_CONTAINER"; do
podman rm -f "$CTR" >/dev/null 2>&1 || true
done
@@ -99,30 +155,46 @@ done
LOG=/home/dank/logs/frank_primary.log
mkdir -p "$(dirname "$LOG")"
# --device /dev/dri is the WHOLE DIRECTORY (individual renderD nodes enumerate
# without the card nodes and HIP refuses contexts). --ipc=host for the
# engine's shared memory; memlock unbounded for the ~120 GiB pinned expert
# rows + the 47.7 GiB PLE pin (the unit's LimitMEMLOCK=infinity is the other
# half of that law).
# HIP_VISIBLE_DEVICES pins visibility to ONE card so in-container index 0 is
# the right R9700; --gpu 0 selects it inside the visible set.
# Publisher-vetted container contract (docs/DOCKER.md, compose/common.yaml):
# --network host (the server binds the host's addresses directly, no NAT),
# --init (the engine releases the cards on SIGTERM), seccomp=unconfined
# (Docker/podman's default profile refuses io_uring, which the n-gram
# table's reads go through, and the NUMA binding calls the ROCm runtime
# makes for host memory), memlock unbounded (the pinned ledger — legal
# because the root unit grants LimitMEMLOCK=infinity to this dank process),
# /dev/kfd + /dev/dri (render/video groups are moot for a root container).
# The template rides a single-file mount; the .rad dir is ro; /data holds
# the prefix-cache disk tier.
podman run -d --name "$FRANK_PRIMARY_CONTAINER" \
--publish "$FRANK_PRIMARY_PUBLISH" \
--ipc=host --ulimit memlock=-1:-1 \
--cap-add SYS_PTRACE \
--device /dev/kfd --device /dev/dri --device /dev/mem \
-e HIP_VISIBLE_DEVICES="$FRANK_PRIMARY_FT_GPU" \
--network host \
--init \
--security-opt seccomp=unconfined \
--ulimit memlock=-1:-1 \
--device /dev/kfd --device /dev/dri \
-v "$FRANK_PRIMARY_MODEL_DIR":/models:ro \
-v "$FRANK_PRIMARY_STATE_DIR":/data \
-v "$FRANK_PRIMARY_STATE_DIR/tune":/opt/radiance/share/radiance/tune \
-v "$FRANK_PRIMARY_TEMPLATE":/templates/template.jinja:ro \
"$FRANK_PRIMARY_IMAGE" \
ft serve --model /models \
--served-model-name "$FRANK_PRIMARY_SERVED_NAME" \
--host 0.0.0.0 --port 7770 \
--gpu "$FRANK_PRIMARY_FT_GPU" \
--num-tokens "$FRANK_PRIMARY_FT_NUM_TOKENS" \
--max-running-requests "$FRANK_PRIMARY_FT_MAX_RUNNING" \
--max-output-tokens "$FRANK_PRIMARY_FT_MAX_OUTPUT_TOKENS" \
--moe-cache-rate "$FRANK_PRIMARY_FT_MOE_RATE" \
${FRANK_PRIMARY_FT_EXTRA_ARGS:+$FRANK_PRIMARY_FT_EXTRA_ARGS} \
--model "/models/$FRANK_PRIMARY_MODEL_FILE" \
--host :: --port "$FRANK_PRIMARY_PORT" \
--served-model-name "$FRANK_PRIMARY_SERVED_NAME" \
--override-chat-template /templates/template.jinja \
--tp "$FRANK_PRIMARY_TP" --tp-wire "$FRANK_PRIMARY_TP_WIRE" \
--max-num-seqs "$FRANK_PRIMARY_MAX_SEQS" \
--max-model-len "$FRANK_PRIMARY_MAX_MODEL_LEN" \
--placement "$FRANK_PRIMARY_PLACEMENT" \
--host-pool-mib "$FRANK_PRIMARY_HOST_POOL_MIB" \
--gpu-headroom-mib "$FRANK_PRIMARY_GPU_HEADROOM_MIB" \
--expert-vs-cache-ratio "$FRANK_PRIMARY_EXPERT_VS_CACHE" \
--kv-cache-dtype "$FRANK_PRIMARY_KV_DTYPE" \
--prefix-cache-host-mib "$FRANK_PRIMARY_PREFIX_HOST_MIB" \
--prefix-cache-dir /data/kvcache/frank-primary \
--prefix-cache-disk-mib "$FRANK_PRIMARY_PREFIX_DISK_MIB" \
--ngram-placement "$FRANK_PRIMARY_NGRAM_PLACEMENT" \
--num-speculative-tokens "$FRANK_PRIMARY_SPEC_TOKENS" \
--max-num-batched-tokens "$FRANK_PRIMARY_MAX_BATCHED_TOKENS" \
${FRANK_PRIMARY_EXTRA_ARGS:+$FRANK_PRIMARY_EXTRA_ARGS} \
> "$LOG" 2>&1
# From here on, this script's lifetime IS the container's: wait returns the
+120
View File
@@ -0,0 +1,120 @@
#!/usr/bin/env bash
# Serve ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF
# (RCO-IQ3_XXS, **MTP head in the file**) as frank-secondary on Dankclaw's
# RTX 3090 via llama.cpp (master build, CUDA sm_86).
#
# 2026-10-06 (Frank): TWO 131,072 conversations (262144 / 2) at the 128k
# cap, ubatch 512, and MTP BACK ON (draft-mtp, depth 3 — the payload is
# 1.56 GiB lighter than the IQ3_S-mtp it replaced, which is what funds
# the draft ctx plus the second slot; ngram stays skipped — on a qwen35
# hybrid it disables recurrent-state rollback). The q8_0 KV contract
# (FP8 class) is unchanged; the per-tensor flags stay explicit either
# way.
#
# Architecture: qwen35 hybrid — 64 real layers, 16 full-attention + 48
# Gated DeltaNet (full_attention_interval 4), native 262144 window, plus
# the blk.64 MTP head (nextn_predict_layers=1). KV lives only in the 16
# attention layers (34 KiB/token at q8_0); the DeltaNet state is per-slot
# fixed size.
# Vision comes from the projector file (the GGUF carries no vision
# tensors; the publisher SHIPS mmproj-BF16.gguf in the same repo), so
# --mmproj is what gives this endpoint eyes; MMPROJ_FILE="" is the
# documented text-only opt-out.
#
# Sampling: llama.cpp takes it per request (the fleet harness sends its
# recipe from models.yml); greedy is the harness default.
#
# Tunables come from frank-secondary.env unless FRANK_SECONDARY_ENV_FILE overrides.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${FRANK_SECONDARY_ENV_FILE:-$SCRIPT_DIR/frank-secondary.env}"
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
# --- defaults (used when the env file does not set them) -------------------
: "${LLAMA_BIN:=/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server}"
: "${MODEL_FILE:=/home/dank/models/Swift15-UncIQ3XXS/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf}"
: "${ALIAS:=frank-secondary}"
: "${BIND_ADDR:=0.0.0.0}"
: "${LISTEN_PORT:=7771}"
# ctx-size is the POOL llama.cpp divides across slots: 262144 / 2 gives
# TWO 131,072 conversations — Frank's 128k cap each. 2026-10-06 (Frank):
# drop MTP, drop ubatch to 512, take two slots; the fit is tight by his
# explicit order (frank-secondary.env carries the arithmetic and the floor
# violation).
: "${CTX_SIZE:=262144}"
: "${PARALLEL:=2}"
# Compute-buffer geometry. Ubatch is a prefill knob (decode never moves).
# Frank's 2026-10-06 tight-fit order ships 4096/512 (claws back ~0.3 GiB
# toward the second slot); ubatch 1024 is the first A/B back if a future
# boot lands with headroom (a miss fails loud at graph capture). The
# logical batch is VRAM- and throughput-neutral at a fixed ubatch.
: "${BATCH_SIZE:=4096}"
: "${UBATCH_SIZE:=512}"
# KV at FP8 class = q8_0 on llama.cpp (1.0625 B/elem => 8.5 effective bits;
# there is no true fp8 KV dtype). Explicit either way so the contract is
# visible in the argv.
: "${CACHE_TYPE_K:=q8_0}"
: "${CACHE_TYPE_V:=q8_0}"
# Speculative decoding is ON by default (Frank, 2026-10-06 evening):
# draft-mtp against the head that is IN the file. SPEC_TYPE none strips
# BOTH speculative flags (flags move as a set) — a no-spec baseline must
# not carry half a spec pair.
: "${SPEC_TYPE:=draft-mtp}"
: "${SPEC_DRAFT_N_MAX:=3}"
: "${NGL:=99}"
# Vision projector. No colon on purpose: an explicitly empty value means
# "text-only, on purpose" and must NOT be replaced by the default path.
: "${MMPROJ_FILE=/home/dank/models/Swift15-UncIQ3XXS/mmproj-BF16.gguf}"
# Fail loudly rather than half-loading: a missing file is a download that
# has not finished, and Restart=always would turn it into a crash loop that
# reads like a runtime fault.
for path in "$LLAMA_BIN" "$MODEL_FILE"; do
[[ -e "$path" ]] || { echo "frank-secondary: missing $path" >&2; exit 1; }
done
EXTRA_ARGS=()
if [[ -n "$MMPROJ_FILE" ]]; then
[[ -e "$MMPROJ_FILE" ]] || { echo "frank-secondary: missing $MMPROJ_FILE" >&2; exit 1; }
EXTRA_ARGS+=(--mmproj "$MMPROJ_FILE")
fi
# Speculative flags move as a set: they reach the argv ONLY when a drafter
# is named. SPEC_TYPE=none (the shipped contract) strips both — a no-spec
# baseline must not carry half a spec pair.
if [[ -n "$SPEC_TYPE" && "$SPEC_TYPE" != "none" ]]; then
EXTRA_ARGS+=(--spec-type "$SPEC_TYPE" --spec-draft-n-max "$SPEC_DRAFT_N_MAX")
fi
# The UVM device node is created lazily by nvidia-modprobe on the first CUDA
# open after boot; make sure it exists before the server needs it.
nvidia-modprobe -u -c=0 2>/dev/null || true
# Flash attention stays pinned on: it is REQUIRED for quantized V (q8_0),
# and the build carries GGML_CUDA_FA_ALL_QUANTS=ON (since 2026-09-17), so
# the q8_0 KV contract in frank-secondary.env is a real path — without this
# flag or that build option the engine silently falls back and the
# contract becomes a lie.
ARGS=(
--model "$MODEL_FILE"
--alias "$ALIAS"
--host "$BIND_ADDR"
--port "$LISTEN_PORT"
--ctx-size "$CTX_SIZE"
--parallel "$PARALLEL"
--batch-size "$BATCH_SIZE"
--ubatch-size "$UBATCH_SIZE"
--n-gpu-layers "$NGL"
--flash-attn on
--cache-type-k "$CACHE_TYPE_K"
--cache-type-v "$CACHE_TYPE_V"
--jinja
--reasoning auto
--reasoning-format deepseek
)
exec "$LLAMA_BIN" "${ARGS[@]}" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}
-99
View File
@@ -1,99 +0,0 @@
#!/usr/bin/env bash
# Serve ornith-ai/Ornith-1.5-9B-GGUF (Q8_0) as frank-small on
# Dankclaw's RTX 3090 via llama.cpp (master build, CUDA sm_86).
#
# 2026-10-01 night (Frank): swapped from SGLang + INT8-W8A16-AutoRound
# safetensors to the llama.cpp GGUF path, because llama.cpp is the only
# runtime on this host that moves K and V independently — the asymmetric
# f16-K + q8_0-V contract that endpoint was built for. 2026-10-03 (Frank):
# the pair is now f16/f16 — two wide slots freed the VRAM, so the values are
# unquantized too. The per-tensor flags stay explicit either way.
#
# Architecture: qwen3_5 hybrid — 32 layers, 8 full-attention + 24 Gated
# DeltaNet, native 262144 window. KV lives only in the 8 attention layers;
# the DeltaNet state is per-slot fixed size. Vision comes from the repo's
# separate projector (the Q6_K GGUF carries no `v.*` tensors at all), so
# --mmproj is what gives this endpoint eyes; MMPROJ_FILE="" is the documented
# text-only opt-out.
#
# Sampling: llama.cpp takes it per request (the fleet harness sends its
# recipe from models.yml); greedy is the harness default.
#
# Tunables come from frank-small.env unless FRANK_SMALL_ENV_FILE overrides.
set -euo pipefail
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
ENV_FILE="${FRANK_SMALL_ENV_FILE:-$SCRIPT_DIR/frank-small.env}"
if [[ -f "$ENV_FILE" ]]; then
set -a
# shellcheck disable=SC1090
source "$ENV_FILE"
set +a
fi
# --- defaults (used when the env file does not set them) -------------------
: "${LLAMA_BIN:=/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server}"
: "${MODEL_FILE:=/home/dank/models/Ornith-Q8/Ornith-1.5-9B-Q8_0.gguf}"
: "${ALIAS:=frank-small}"
: "${BIND_ADDR:=0.0.0.0}"
: "${LISTEN_PORT:=7771}"
# ctx-size is the POOL llama.cpp divides across slots: 401408 / 2 gives
# 200,704 per slot — two independent conversations, each one wide enough to
# hold a whole frank-primary-sized prompt in a single shot (the native 262,144
# no longer fits twice on a 24 GiB card once the vision tower is loaded;
# frank-small.env carries the three measurements that set this number).
: "${CTX_SIZE:=401408}"
: "${PARALLEL:=2}"
# Compute-buffer geometry. Before 2026-10-03 this endpoint never passed either
# flag, so it ran on llama.cpp's own defaults (2048/512). The pair is 4096/2048
# since 2026-10-05: on this CUDA card the prefill curve climbs all the way to
# ubatch 2048 (unlike the R9700 layer-split sibling, where 512 is the peak),
# and the logical batch is VRAM- and throughput-neutral. frank-small.env
# carries the sweep tables and the VRAM risk note.
: "${BATCH_SIZE:=4096}"
: "${UBATCH_SIZE:=2048}"
: "${CACHE_TYPE_K:=f16}"
: "${CACHE_TYPE_V:=f16}"
: "${NGL:=99}"
# Vision projector. No colon on purpose: an explicitly empty value means
# "text-only, on purpose" and must NOT be replaced by the default path.
: "${MMPROJ_FILE=/home/dank/models/Ornith-Q8/mmproj-Ornith-1.5-9B-BF16.gguf}"
# Fail loudly rather than half-loading: a missing file is a download that
# has not finished, and Restart=always would turn it into a crash loop that
# reads like a runtime fault.
for path in "$LLAMA_BIN" "$MODEL_FILE"; do
[[ -e "$path" ]] || { echo "frank-small: missing $path" >&2; exit 1; }
done
EXTRA_ARGS=()
if [[ -n "$MMPROJ_FILE" ]]; then
[[ -e "$MMPROJ_FILE" ]] || { echo "frank-small: missing $MMPROJ_FILE" >&2; exit 1; }
EXTRA_ARGS+=(--mmproj "$MMPROJ_FILE")
fi
# The UVM device node is created lazily by nvidia-modprobe on the first CUDA
# open after boot; make sure it exists before the server needs it.
nvidia-modprobe -u -c=0 2>/dev/null || true
# Flash attention stays pinned on: it is the fast path for f16 KV, and it is
# the only path that could serve quantized V at all — so if CACHE_TYPE_V is
# ever set back to q8_0 this flag must not be lost, or the build falls back
# silently and the KV contract in frank-small.env becomes a lie.
ARGS=(
--model "$MODEL_FILE"
--alias "$ALIAS"
--host "$BIND_ADDR"
--port "$LISTEN_PORT"
--ctx-size "$CTX_SIZE"
--parallel "$PARALLEL"
--batch-size "$BATCH_SIZE"
--ubatch-size "$UBATCH_SIZE"
--n-gpu-layers "$NGL"
--flash-attn on
--cache-type-k "$CACHE_TYPE_K"
--cache-type-v "$CACHE_TYPE_V"
--jinja
--reasoning auto
--reasoning-format deepseek
)
exec "$LLAMA_BIN" "${ARGS[@]}" ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}
+556
View File
@@ -0,0 +1,556 @@
#!/usr/bin/env python3
"""stack-qwen38-official-fp8.py — reshape the official Qwen/Qwen3.8-Flash-Next-FP8
checkpoint into the form the shipped radiance qwen4exp plugin maps.
Run INSIDE the staging dir's tree (python3 + numpy on the host; data never
leaves the box). Two transforms, both VALUE-PRESERVING:
1. EXPERTS: the shipped plugin's stacked layout names ONE tensor per family
(`mlp.experts.gate_up_proj` [E, 2F, H], `mlp.experts.down_proj` [E, H, F])
plus a `<family>_scale_inv` plane at the same index (NO `.weight` in the
stacked name — scale_name() appends _scale_inv to the tensor name verbatim),
while the official FP8 repo ships per-expert unfused tensors
(`experts.<e>.{gate,up,down}_proj.weight` + scale). Stacking is pure byte
concatenation along dim 0 — gate rows
first inside each expert's gate_up slab, which is the order the block
wants (rad_block_moe_fp8.h: "a stacked gate_up is ALREADY FUSED in the
order this block wants"). No requantisation: bytes are moved, not decoded.
2. PLE TABLE: the plugin's fp8 branch wants the table's codes beside a
ONE-ELEMENT bf16 scale plane (`blk.%d.ple_ngram.scale`, view {1}); a
per-shard-blocked scale can never fit that view, and `rtn` re-encoding the
RAW scaleless codes would overflow (v / 1.99e-4 exceeds e4m3's 448 max).
So staging decodes the scaleless shards with the checkpoint's OWN bf16
scale into a bf16 table (exact: every e4m3 code value and the scale are
exactly representable in bf16 to within the code's own 2^-3 grid), and the
convert's rtn rule re-encodes at that same scale — the nearest-code
distance is 2^-3 while the bf16 rounding perturbs by 2^-9, so every
original code is restored exactly and the container gets the checkpoint's
own codes plus the one-element scale plane.
The convert then runs with NO recipe except:
blk.*.ple_ngram.weight=rtn:codes=fp8_e4m3,block=*x*,scale=bf16,rule=fixed,scale_value=<s>
with <s> read from the checkpoint and written to ple_scale.txt.
Idempotent: re-runs skip when the staged index already carries the stacked
names, the merged bf16 shard_0, and split_ngram_parts == 1. --force redoes.
"""
import argparse
import json
import mmap
import multiprocessing as mp
import os
import re
import struct
import sys
import time
import numpy as np
F8, BF16 = "F8_E4M3", "BF16"
def log(msg):
print(msg, flush=True)
# ---------------------------------------------------------------- safetensors
def parse_header(path):
"""Return (header_len, {name: (dtype, shape, data_offsets)})."""
with open(path, "rb") as fh:
(hlen,) = struct.unpack("<Q", fh.read(8))
hdr = json.loads(fh.read(hlen))
return hlen, hdr
def write_safetensors(path, tensors, data_iter):
"""tensors: [(name, dtype, shape, nbytes)]; data_iter yields bytes per tensor."""
hdr = {}
off = 0
for name, dtype, shape, nbytes in tensors:
hdr[name] = {"dtype": dtype, "shape": list(shape),
"data_offsets": [off, off + nbytes]}
off += nbytes
blob = json.dumps(hdr, separators=(",", ":")).encode("utf-8")
pad = (-len(blob)) % 8 # keep the data start 8-byte aligned
blob += b" " * pad
with open(path, "wb") as fh:
fh.write(struct.pack("<Q", len(blob)))
fh.write(blob)
for _, _, _, nbytes in tensors:
fh.write(next(data_iter).tobytes())
fh.flush()
os.fsync(fh.fileno())
return 8 + len(blob) + sum(t[3] for t in tensors)
# ------------------------------------------------------------------- preflight
def build_meta(stage, index):
"""name -> (file, global_byte_offset, nbytes, dtype, shape) for every tensor."""
files = sorted(set(index["weight_map"].values()))
meta = {}
for f in files:
path = os.path.join(stage, f)
hlen, hdr = parse_header(path)
for name, e in hdr.items():
if name == "__metadata__":
continue
b0, b1 = e["data_offsets"]
meta[name] = (f, 8 + hlen + b0, b1 - b0, e["dtype"], e["shape"])
return meta
def e4m3_lut():
lut = np.zeros(256, np.float32)
for b in range(256):
sign = -1.0 if b & 0x80 else 1.0
e = (b >> 3) & 0xF
m = b & 0x7
if e == 15 and m == 7:
v = np.float32(np.nan) # OCP e4m3fn: only 0x7F/0xFF are NaN
elif e == 0:
v = sign * (m / 8.0) * 2.0 ** -6
else:
v = sign * (1.0 + m / 8.0) * 2.0 ** (e - 7)
lut[b] = v
assert lut[0x38] == 1.0 and lut[0x7E] == 448.0
assert np.isnan(lut[0x7F]) and lut[0x00] == 0.0
assert lut[0x08] == 2.0 ** -6 and lut[0x80] == 0.0
return lut
def f32_to_bf16_rne(v):
"""float32 array -> uint16 bf16 bits, round-to-nearest-even."""
q = np.ascontiguousarray(v, dtype=np.float32)
q[~np.isfinite(q)] = 0.0
bits = q.view(np.uint32)
r = bits + 0x7FFF + ((bits >> 16) & 1)
return (r >> 16).astype(np.uint16)
# ------------------------------------------------------------------- workers
def stack_prefix(job):
"""Byte-stack one layer's 512 experts into a 4-tensor safetensors file."""
(stage, pre, idx, n_e, shapes, meta) = job
(f2, h) = shapes["ffn2_h"] # 2*ffn, n_embd
(f,) = shapes["ffn"] # ffn
gs, ds = shapes["gs"], shapes["ds"] # scale shapes [5,20] and [20,5]
# byte widths: fp8 is 1 B/element, bf16 2 B; the fused gate_up scale is
# the gate and up scale planes concatenated (gate rows first)
g_bytes = (f2 // 2) * h
gs_bytes = gs[0] * gs[1] * 2 # ONE projection's scale plane
gus_bytes = 2 * gs_bytes # fused scale per expert
dn_bytes = h * f
ds_bytes = ds[0] * ds[1] * 2
gu = np.empty(n_e * f2 * h, np.uint8)
gus = np.empty(n_e * gus_bytes, np.uint8)
dn = np.empty(n_e * dn_bytes, np.uint8)
dns = np.empty(n_e * ds_bytes, np.uint8)
mmaps = {}
def view(name):
f_, off, ln, dt, shp = meta[name]
if f_ not in mmaps:
fh = open(os.path.join(stage, f_), "rb")
mmaps[f_] = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
return memoryview(mmaps[f_])[off:off + ln], (dt, shp)
def need(name, dt, shp):
mv, (got_dt, got_shp) = view(name)
assert got_dt == dt and list(got_shp) == list(shp), f"{name}: {got_dt}{got_shp}"
return mv
for e in range(n_e):
base = f"{pre}.mlp.experts.{e}"
slab = gu[e * f2 * h:(e + 1) * f2 * h]
slab[:g_bytes] = need(f"{base}.gate_proj.weight", F8, [f2 // 2, h])
slab[g_bytes:] = need(f"{base}.up_proj.weight", F8, [f2 // 2, h])
sslab = gus[e * gus_bytes:(e + 1) * gus_bytes]
sslab[:gs_bytes] = need(f"{base}.gate_proj.weight_scale_inv", BF16, gs)
sslab[gs_bytes:] = need(f"{base}.up_proj.weight_scale_inv", BF16, gs)
dn[e * dn_bytes:(e + 1) * dn_bytes] = need(f"{base}.down_proj.weight", F8, [h, f])
dns[e * ds_bytes:(e + 1) * ds_bytes] = need(f"{base}.down_proj.weight_scale_inv", BF16, ds)
for mm in mmaps.values():
mm.close()
# byte-identity check on two sampled experts (first and last), all four
# tensors — the stack IS the checkpoint bytes, so they must compare equal
for e in (0, n_e - 1):
base = f"{pre}.mlp.experts.{e}"
s = e * f2 * h
assert bytes(gu[s:s + g_bytes]) == bytes(stack_src(f"{base}.gate_proj.weight", meta, stage))
assert bytes(gu[s + g_bytes:s + f2 * h]) == bytes(stack_src(f"{base}.up_proj.weight", meta, stage))
t = e * gus_bytes
assert bytes(gus[t:t + gs_bytes]) == bytes(stack_src(f"{base}.gate_proj.weight_scale_inv", meta, stage))
assert bytes(gus[t + gs_bytes:t + gus_bytes]) == bytes(stack_src(f"{base}.up_proj.weight_scale_inv", meta, stage))
d = e * dn_bytes
assert bytes(dn[d:d + dn_bytes]) == bytes(stack_src(f"{base}.down_proj.weight", meta, stage))
u = e * ds_bytes
assert bytes(dns[u:u + ds_bytes]) == bytes(stack_src(f"{base}.down_proj.weight_scale_inv", meta, stage))
out = os.path.join(stage, f"stacked.{idx:03d}.safetensors")
tensors = [
(f"{pre}.mlp.experts.gate_up_proj", F8, [n_e, f2, h], n_e * f2 * h),
(f"{pre}.mlp.experts.gate_up_proj_scale_inv", BF16,
[n_e, gs[0] * 2, gs[1]], n_e * gus_bytes),
(f"{pre}.mlp.experts.down_proj", F8, [n_e, h, f], n_e * h * f),
(f"{pre}.mlp.experts.down_proj_scale_inv", BF16,
[n_e, ds[0], ds[1]], n_e * ds_bytes),
]
bufs = iter([gu, gus, dn, dns])
written = write_safetensors(out, tensors, bufs)
return (pre, written)
def stack_src(name, meta, stage):
"""Re-open a source tensor for the sampled-expert check."""
f_, off, ln, dt, shp = meta[name]
fh = open(os.path.join(stage, f_), "rb")
mm = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
fh.close() # mmap keeps the mapping alive
return mm[off:off + ln]
def fill_ple_shard(job):
"""Decode one fp8 shard with the table scale, write bf16 rows into the mmap."""
(src_path, src_off, src_rows, dst_path, dst_off, scale_f32,
cols, chunk_rows) = job
lut = e4m3_lut()
with open(src_path, "rb") as fh:
sm = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
with open(dst_path, "r+b") as dfh:
dm = mmap.mmap(dfh.fileno(), 0, access=mmap.ACCESS_WRITE)
for r0 in range(0, src_rows, chunk_rows):
r1 = min(r0 + chunk_rows, src_rows)
codes = np.frombuffer(sm, dtype=np.uint8,
count=(r1 - r0) * cols,
offset=src_off + r0 * cols)
q = lut.take(codes)
del codes # release the mmap export before sm.close()
q[~np.isfinite(q)] = 0.0
q *= np.float32(scale_f32)
packed = f32_to_bf16_rne(q)
dm[dst_off + r0 * cols * 2: dst_off + r1 * cols * 2] = packed.tobytes()
dm.flush()
os.fsync(dfh.fileno())
dm.close()
sm.close()
# ---------------------------------------------------------------------- main
def main():
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--src", default=None,
help="raw checkpoint dir (documentation only; unused)")
ap.add_argument("--stage", required=True, help="staging dir to reshape in place")
ap.add_argument("--force", action="store_true", help="redo even if already staged")
ap.add_argument("--workers", type=int, default=min(8, os.cpu_count() or 1))
args = ap.parse_args()
stage = args.stage
t0 = time.time()
index_path = os.path.join(stage, "model.safetensors.index.json")
cfg_path = os.path.join(stage, "config.json")
if not (os.path.isfile(index_path) and os.path.isfile(cfg_path)):
log(f"ERROR: {stage} is not a staged checkpoint (missing index/config)")
return 2
with open(index_path) as fh:
index = json.load(fh)
with open(cfg_path) as fh:
cfg = json.load(fh)
wm = index["weight_map"]
meta = build_meta(stage, index)
# ---- collect prefixes -------------------------------------------------
pre_re = re.compile(r"^(?P<pre>.+)\.mlp\.experts\.(?P<e>\d+)\.gate_proj\.weight$")
pres, n_e = {}, 0
for k in wm:
m = pre_re.match(k)
if m:
pres.setdefault(m.group("pre"), set()).add(int(m.group("e")))
n_e = max(n_e, int(m.group("e")) + 1)
prefixes = sorted(pres)
if not prefixes:
log("ERROR: no per-expert tensors found in the staged index")
return 2
mtp = [p for p in prefixes if p.startswith("mtp.")]
trunk = [p for p in prefixes if p.startswith("model.language_model.layers.")]
log(f"expert prefixes: {len(prefixes)} (trunk {len(trunk)}, mtp {len(mtp)}), "
f"{n_e} experts each")
# shapes from expert 0 of the first prefix
pre0 = prefixes[0]
shapes = {}
gdt, gsh = meta[f"{pre0}.mlp.experts.0.gate_proj.weight"][3:5]
ddt, dsh = meta[f"{pre0}.mlp.experts.0.down_proj.weight"][3:5]
gsd, gssh = meta[f"{pre0}.mlp.experts.0.gate_proj.weight_scale_inv"][3:5]
dsd, dssh = meta[f"{pre0}.mlp.experts.0.down_proj.weight_scale_inv"][3:5]
scale_law = (gdt == F8 and ddt == F8 and gsd == BF16 and dsd == BF16
and list(gssh) == [(gsh[0] + 127) // 128, (gsh[1] + 127) // 128]
and list(dssh) == [(dsh[0] + 127) // 128, (dsh[1] + 127) // 128])
if not scale_law:
log(f"ERROR: unexpected expert shapes: gate {gdt}{gsh} scale {gsd}{gssh} "
f"down {ddt}{dsh} scale {dsd}{dssh} (scale law 128x128)")
return 2
shapes = {"ffn2_h": (gsh[0] * 2, gsh[1]), "ffn": (dsh[1],),
"gs": tuple(gssh), "ds": tuple(dssh)}
f2, h = shapes["ffn2_h"]
f = shapes["ffn"][0]
log(f"shapes: gate_up [{n_e},{f2},{h}] F8, down [{n_e},{h},{f}] F8, "
f"scales {gssh}x2 / {dssh} bf16")
# ---- PLE table --------------------------------------------------------
ple_re = re.compile(r"^(?P<pre>.+)\.ple\.ple_embedding\.ngram_embedding\.shard_(?P<i>\d+)\.weight$")
ple_pre, shard_ids = None, {}
for k in wm:
m = ple_re.match(k)
if m:
ple_pre = m.group("pre")
shard_ids[int(m.group("i"))] = k
if ple_pre is None:
log("ERROR: no PLE table shards found")
return 2
n_shards = len(shard_ids)
if sorted(shard_ids) != list(range(n_shards)):
log(f"ERROR: PLE shard ids not contiguous 0..{n_shards - 1}")
return 2
sc_name = f"{ple_pre}.ple.ple_embedding.ngram_embedding.weight_scale"
sdt, ssh, sln = meta[sc_name][3], meta[sc_name][4], meta[sc_name][2]
if sdt != BF16 or list(ssh) != [1]:
log(f"ERROR: PLE scale is {sdt}{ssh}, expected BF16 [1]")
return 2
s_bits = struct.unpack("<H", bytes(view_src_tuple(meta, sc_name, stage)))[0]
scale_f32 = struct.unpack("<f", struct.pack("<I", s_bits << 16))[0]
log(f"PLE: {ple_pre}, {n_shards} shards, scale bf16 = {scale_f32!r}")
rows = [meta[shard_ids[i]][4][0] for i in range(n_shards)]
total_rows = sum(rows)
hd = meta[shard_ids[0]][4][1]
if any(meta[shard_ids[i]][4][1] != hd for i in range(n_shards)):
log("ERROR: PLE shards disagree on HD")
return 2
if hd % 128 != 0 and hd != 160:
log(f"WARNING: PLE HD={hd} is neither 160 nor a multiple of 128")
# ---- idempotence gate -------------------------------------------------
stacked_names = [f"{p}.mlp.experts.gate_up_proj" for p in prefixes]
merged_name = f"{ple_pre}.ple.ple_embedding.ngram_embedding.shard_0.weight"
already = (all(n in wm for n in stacked_names)
and merged_name in wm
and find_split(cfg) is not None and read_split(cfg) == 1)
if already and not args.force:
log("already staged (stacked names, merged shard_0 and split_ngram_parts=1 "
"all present) — nothing to do")
return 0
# ---- disk gate --------------------------------------------------------
stacked_bytes = len(prefixes) * n_e * (f2 * h + h * f)
ple_bytes = total_rows * hd * 2
need = stacked_bytes + ple_bytes
st = os.statvfs(stage)
avail = st.f_bavail * st.f_frsize
if avail < need * 1.05:
log(f"ERROR: {avail / 2**30:.1f} GiB free < {need / 2**30:.1f} GiB needed (+5%)")
return 2
log(f"disk: {avail / 2**30:.1f} GiB free, need {need / 2**30:.1f} GiB "
f"(experts {stacked_bytes / 2**30:.1f}, ple {ple_bytes / 2**30:.1f})")
# ---- stage experts ----------------------------------------------------
jobs = []
for idx, pre in enumerate(prefixes):
jmeta = {n: meta[n] for n in meta if n.startswith(f"{pre}.mlp.experts.")}
jobs.append((stage, pre, idx, n_e, shapes, jmeta))
ctx = mp.get_context("fork")
written_total = 0
with ctx.Pool(args.workers) as pool:
for pre, nbytes in pool.imap_unordered(stack_prefix, jobs):
written_total += nbytes
log(f" stacked {pre} ({nbytes / 2**30:.2f} GiB)")
log(f"experts stacked: {written_total / 2**30:.2f} GiB in {len(prefixes)} files")
# ---- stage PLE --------------------------------------------------------
# The file name sorts BEFORE the model-000NN shards ON PURPOSE: the reader
# opens every index-named file in sorted order and emplaces EVERY tensor
# it physically holds, first-wins (checkpoint.cpp add_shard). The raw
# shards carry tensors of the SAME name as the merged one
# (...ple.ple_embedding.ngram_embedding.shard_0.weight, [2500012,160]
# fp8), so ple.merged.safetensors lost the race and the plan read one raw
# shard as the codes. "00-" wins the sort and the name.
out_name = "00-ple.merged.safetensors"
out_path = os.path.join(stage, out_name)
data_bytes = total_rows * hd * 2
hdr = {merged_name: {"dtype": BF16, "shape": [total_rows, hd],
"data_offsets": [0, data_bytes]}}
blob = json.dumps(hdr, separators=(",", ":")).encode("utf-8")
pad = (-len(blob)) % 8
blob += b" " * pad
hlen = len(blob)
with open(out_path, "wb") as fh:
fh.write(struct.pack("<Q", hlen))
fh.write(blob)
fh.truncate(8 + hlen + data_bytes)
jobs = []
row0 = 0
for i in range(n_shards):
f_, off, ln, _, shp = meta[shard_ids[i]]
jobs.append((os.path.join(stage, f_), off, shp[0], out_path,
8 + hlen + row0 * hd * 2, scale_f32, hd, 131072))
row0 += shp[0]
assert row0 == total_rows
with ctx.Pool(args.workers) as pool:
pool.map(fill_ple_shard, jobs)
log(f"PLE merged: {data_bytes / 2**30:.2f} GiB bf16 "
f"({total_rows} rows x {hd} cols)")
# ---- post-verify ------------------------------------------------------
verify(stage, prefixes, n_e, shapes, meta, shard_ids, rows, hd,
scale_f32, out_path, hlen)
# ---- rewrite index + config -------------------------------------------
new_wm = {k: v for k, v in wm.items() if k not in set(shard_ids.values())}
for idx, pre in enumerate(prefixes):
fname = f"stacked.{idx:03d}.safetensors"
for t in (f"{pre}.mlp.experts.gate_up_proj",
f"{pre}.mlp.experts.gate_up_proj_scale_inv",
f"{pre}.mlp.experts.down_proj",
f"{pre}.mlp.experts.down_proj_scale_inv"):
new_wm[t] = fname
new_wm[merged_name] = out_name
total = sum(meta_size(meta, k) for k in new_wm
if k in meta) # originals
# new tensors' sizes
total += written_total + data_bytes
index["weight_map"] = new_wm
if "metadata" in index and isinstance(index["metadata"], dict):
index["metadata"]["total_size"] = total
# ATOMIC REPLACE, never open(path, "w"): the stage's index is a symlink
# to the RAW checkpoint (staging symlinks everything but config.json) —
# open("w") would rewrite the raw file through the link. os.replace
# swaps the link itself for a real file; the raw tree stays pristine.
for path, payload in ((index_path, json.dumps(index, indent=2)),
(cfg_path, json.dumps(cfg, indent=2))):
tmp = path + ".tmp"
with open(tmp, "w") as fh:
fh.write(payload)
os.replace(tmp, path)
n = set_split(cfg, 1)
log(f"config: split_ngram_parts=1 written at {n} location(s)")
with open(os.path.join(stage, "ple_scale.txt"), "w") as fh:
fh.write(repr(scale_f32) + "\n")
dt = time.time() - t0
log(f"staged in {dt / 60:.1f} min: {written_total / 2**30:.1f} GiB experts + "
f"{data_bytes / 2**30:.1f} GiB PLE; scale {scale_f32!r} -> ple_scale.txt")
return 0
def meta_size(meta, name):
_, _, ln, _, _ = meta[name]
return ln
def view_src_tuple(meta, name, stage):
f_, off, ln, _, _ = meta[name]
with open(os.path.join(stage, f_), "rb") as fh:
fh.seek(off)
return fh.read(ln)
def find_split(cfg):
"""Locate split_ngram_parts keys (recursive); return list of paths."""
hits = []
def walk(d, path):
if isinstance(d, dict):
for k, v in d.items():
if k == "split_ngram_parts":
hits.append(path + [k])
walk(v, path + [k])
walk(cfg, [])
return hits
def read_split(cfg):
hits = find_split(cfg)
if not hits:
return None
d = cfg
for k in hits[0][:-1]:
d = d[k]
return d[hits[0][-1]]
def set_split(cfg, value):
hits = find_split(cfg)
for path in hits:
d = cfg
for k in path[:-1]:
d = d[k]
d[path[-1]] = value
if not hits: # plugin's meta_int2 falls back to the top-level key
cfg["split_ngram_parts"] = value
hits.append(["split_ngram_parts"])
return hits
def verify(stage, prefixes, n_e, shapes, meta, shard_ids, rows, hd,
scale_f32, out_path, hlen):
"""Cheap strong checks: headers parse, sizes match, sampled bytes identical."""
f2, h = shapes["ffn2_h"]
f = shapes["ffn"][0]
gs, ds = shapes["gs"], shapes["ds"]
# headers of every new file
for idx, pre in enumerate(prefixes):
path = os.path.join(stage, f"stacked.{idx:03d}.safetensors")
hlen2, hdr = parse_header(path)
want = {
f"{pre}.mlp.experts.gate_up_proj": (F8, [n_e, f2, h]),
f"{pre}.mlp.experts.gate_up_proj_scale_inv":
(BF16, [n_e, gs[0] * 2, gs[1]]),
f"{pre}.mlp.experts.down_proj": (F8, [n_e, h, f]),
f"{pre}.mlp.experts.down_proj_scale_inv": (BF16, [n_e, ds[0], ds[1]]),
}
for name, (dt, shp) in want.items():
e = hdr[name]
assert e["dtype"] == dt and list(e["shape"]) == shp, name
assert e["data_offsets"][1] - e["data_offsets"][0] == \
int(np.prod(shp)) * (1 if dt == F8 else 2), name
_, phdr = parse_header(out_path)
e = phdr[next(iter(phdr))]
assert e["dtype"] == BF16 and list(e["shape"]) == [sum(rows), hd]
# sampled PLE rows: recompute and compare bytes exactly
lut = e4m3_lut()
out_off = 8 + hlen
for i in (0, len(shard_ids) // 2, len(shard_ids) - 1):
name = shard_ids[i]
f_, off, ln, _, shp = meta[name]
with open(os.path.join(stage, f_), "rb") as fh:
mm = mmap.mmap(fh.fileno(), 0, access=mmap.ACCESS_READ)
for r in (0, shp[0] - 1):
codes = np.frombuffer(mm, dtype=np.uint8, count=hd,
offset=off + r * hd)
q = lut.take(codes) * np.float32(scale_f32)
del codes # release the mmap export before mm.close()
want = f32_to_bf16_rne(q).tobytes()
got_row = sum(rows[:i]) + r
with open(out_path, "rb") as ofh:
ofh.seek(out_off + got_row * hd * 2)
got = ofh.read(hd * 2)
assert got == want, f"PLE row {got_row} mismatch"
mm.close()
log("verify: new-file headers, sizes and sampled PLE rows all exact; "
"expert slabs were byte-checked in-stack")
if __name__ == "__main__":
sys.exit(main())
+3 -3
View File
@@ -204,12 +204,12 @@ for path in "${CHANGED[@]}"; do
scripts/qwen-sharp-v22.4.0.jinja)
add_test tests/frank_large_unit.bats
;;
# frank-small is llama.cpp on the 3090 as a ROOT unit: a native CUDA
# frank-secondary is llama.cpp on the 3090 as a ROOT unit: a native CUDA
# binary, so no generated quadlet is involved. Its env, unit and serve
# script all live in scripts/, which is why this case has to precede
# the generic scripts/*.service and scripts/* fallbacks below.
scripts/frank-small.*|scripts/serve-frank-small.sh)
add_test tests/frank_small_unit.bats
scripts/frank-secondary.*|scripts/serve-frank-secondary.sh)
add_test tests/frank_secondary_unit.bats
add_test tests/llm_units.bats
add_test tests/deployment_scoping.bats
add_test tests/scripts_syntax.bats
+2 -2
View File
@@ -35,12 +35,12 @@ encodes):
1. **Source trigger** = `thresholdPercent × source contextWindow`
(0.70 × 200,704 = 140,492 for frank-primary on the current shape)
2. **Output budget** = capped at the target model's `maxTokens`
(32,768 for frank-small; 36,864 frank-primary; 57,344 frank-large — the
(32,768 for frank-secondary; 36,864 frank-primary; 57,344 frank-large — the
largest, because halogen's `HALOGEN_MAX_TOKENS_CAP` must stay ≥ it)
3. **REQUIREMENT**: prompt (≤ trigger) + budget ≤ the target's per-slot
window. Worked pairs from the live registry: 140,492 + 36,864 = 177,356 ≤
200,704 (frank-primary); 140,490 + 57,344 = 197,834 ≤ 200,700
(frank-large); 140,492 + 32,768 = 173,260 ≤ 200,704 (frank-small).
(frank-large); 140,492 + 32,768 = 173,260 ≤ 200,704 (frank-secondary).
Procedure (recompute from the LIVE files every time — windows rotate with
every swap; the numbers above are the 2026-10-05 state):
@@ -63,7 +63,7 @@ service on the other host.
| Dankclaw compose services | `podman-compose -f dankclaw/compose.yaml up -d --no-deps NAME` | Recreates only `NAME`; Caddy is handled separately because it fronts both hosts. |
| SearXNG, Gluetun, Valkey, OpenWebUI, Pi-hole, Gitea database/server | user Quadlet + `systemctl --user` | Installs the selected unit and restarts only the required dependency-coupled units. Gitea's web proxy is the canonical Caddy compose service. |
| `frank-large` | systemd + `systemctl` (root unit) | Relinks and restarts only the large inference service; waits up to 20 minutes for `/health`. |
| `frank-small` | systemd + `systemctl` (root unit) | Relinks and restarts only the small inference service; waits up to 20 minutes for `/health`. |
| `frank-secondary` | systemd + `systemctl` (root unit) | Relinks and restarts only the small inference service; waits up to 20 minutes for `/health`. |
| `frank-medium` | rootless Quadlet + `systemctl --user` | Installs `dankserver/frank-medium/quadlet/frank-medium.container`; on first deploy retires the llama.cpp root unit and the vLLM replica quadlets into the home trash, then restarts the generated unit. Waits up to 20 minutes for `/health`. |
| Dankserver media stack (RomM trio, Jellyfin, Calibre-Web, Copyparty) | user Quadlet + `systemctl --user` | Installs `dankserver/<svc>/quadlet/*.container`, restarts the generated unit. The first cutover per service is idempotent: it retires the compose-era bare container, rescues any anonymous volume into the data root, and merge-moves `/opt/<svc>` to `/home/dank/Containers/<svc>` (mapped-uid roots move without chown). Steady state is restart-only. |
@@ -0,0 +1,121 @@
---
type: howto
description: "Qwen-Image 2.1 workflows on Dankclaw: two proven txt2img workflows (art + legible text), the API recipe, and which models/LoRAs to add next."
tags:
- dankclaw
- comfyui
- qwen-image
timestamp: 2026-10-05
---
# ComfyUI Qwen-Image Workflows
Two ready-to-load text-to-image workflows for the Qwen-Image 2.1 payload
already on Dankclaw, both proven end-to-end on 2026-10-05 by POSTing to the
live API on port 8188 and verifying the rendered PNG. The repo copies are the
source of truth; the host copies under `/home/dank/comfyui-data/workflows/`
are for convenience.
## What is on disk
| Path (under `/home/dank/comfyui-data/models/`) | Size | Role |
|---|---|---|
| `diffusion_models/qwen_image_2.1_int8_convrot.safetensors` | 6.8 GB | **The one the workflows use** — int8-convrot quant, ComfyUI-core native |
| `diffusion_models/qwen_image_2.1_bf16.safetensors` | 14 GB | Full-precision fallback |
| `diffusion_models/qwen-image-2.1-Q8_0.gguf` (+ `-arch`) | 7.1 GB | GGUF alternative (needs ComfyUI-GGUF custom node) |
| `text_encoders/qwen3vl_8b_w4a8.safetensors` | 5.9 GB | **The one the workflows use** — 4-bit-weights/8-bit-act Qwen3-VL 8B encoder |
| `text_encoders/qwen3vl_8b_int8_convrot.safetensors` | 8.8 GB | Mid quant |
| `text_encoders/qwen3vl_8b_bf16.safetensors` | 17 GB | Full precision |
| `vae/qwen_image_2.1_vae_bf16.safetensors` | 645 MB | 2.1 VAE (used) |
| `vae/qwen_image_vae.safetensors` | 243 MB | Smaller VAE |
No LoRAs are on disk yet — see [Recommended additions](#recommended-additions).
## The workflows
Both are API-format JSON (ComfyUI 0.37 loads them via the UI's **Load** dialog
too): `dankclaw/comfyui/workflows/` in the repo,
`/home/dank/comfyui-data/workflows/` on the host. Node chain in both:
```
UNETLoader (int8_convrot) ──┐
CLIPLoader (qwen_image, w4a8) ─┤ CLIPTextEncode pos/neg
VAELoader (2.1 bf16) ────────┤
EmptySD3LatentImage ─────────┴─ KSampler ─ VAEDecode ─ SaveImage
```
**Common sampler settings:** `cfg 1.0` (Qwen-Image 2.1 expects CFG-free
sampling), `euler` + `simple`, seed fixed in the file.
| Workflow | File | Shape | Steps | Made for |
|---|---|---|---|---|
| Art | `qwen-image-2.1-t2i-art.json` | 1024×1024 | 20 | General illustration/prose prompts |
| Legible text | `qwen-image-2.1-t2i-legible-text.json` | 832×1216 | 25 | Posters, captions, any in-image text |
### Legible-text prompt recipe
Qwen-Image 2.1 is one of the best open-weight text renderers, but only if the
prompt is written for it:
1. **Quote the exact strings**: `large bold white text reading "SOLIDARITY OF
THE SOVIET COMPUTER SYSTEMS"` — quoted text is rendered literally.
2. **Describe layout, not vibes**: say *centered / below it / large vs small*,
the typeface family (sans-serif), and the color.
3. **Say what NOT to draw**: end with `no other words, no watermark`.
4. Keep the negative side on `misspelled, warped letters, extra text` (already
in the workflow).
5. More steps help text: the text workflow uses 25 vs 20.
Verified render: `SOLIDARITY OF THE SOVIET COMPUTER SYSTEMS` + `DANKCLAW
DANKSERVER EST 2026` came back crisp and correctly spelled at 832×1216
(`qwen21-text_00001_.png`, 953 KB). One caveat from the same render: a
shorter secondary line is where misspellings creep in — proofread the first
render before shipping.
## Driving it from the API
ComfyUI 0.37 **requires the graph wrapped in a `prompt` key** — a bare
`{node-id: node}` body gets `{"error": {"type": "no_prompt"}}` (verified
2026-10-05):
```bash
curl -s -X POST http://127.0.0.1:8188/prompt -H 'Content-Type: application/json' \
-d "$(python3 -c 'import json;print(json.dumps({"prompt": json.load(open("dankclaw/comfyui/workflows/qwen-image-2.1-t2i-art.json"))}))')"
# -> {"prompt_id": "...", ...}
curl -s http://127.0.0.1:8188/history/<prompt_id> # status.status_str == "success"
```
Outputs land in `/home/dank/comfyui-data/output/`. The queue is FIFO — the
two workflows above ran back-to-back unattended.
**Note:** the container's UI-saved workflows live in the ephemeral
`/opt/ComfyUI/user` layer and do NOT survive an image rebuild — hence the
host-side `workflows/` dir and the repo copy.
## Recommended additions
Quantized preferred, sized for the 8060S iGPU (GTT-backed; the RTX 3090 is
frank-secondary's). All repos verified 2026-10-05. None are on disk yet.
| # | What | Repo | Grab | Size | Why |
|---|---|---|---|---|---|
| 1 | **Lightning 8-step LoRA** (T2I speed) | `lightx2v/Qwen-Image-Lightning` | `qwen_image_lightning_8steps_*.safetensors` (LoRA) | ~1–2 GB | 20 steps → 8 with a `LoraLoader` node; 2–3× faster renders on the iGPU, negligible quality loss |
| 2 | **Qwen-Image-Edit-2511 GGUF** (image editing) | `unsloth/Qwen-Image-Edit-2511-GGUF` | Q4_K_M (~13.2 GB) or Q4_0 (~11.9 GB) | ~12–13 GB | Unlocks img2img "edit this photo" workflows — a capability class the fleet has zero of; Q4 keeps it iGPU-friendly |
| 3 | **DiffSynth ControlNets** (structure control) | `Comfy-Org/Qwen-Image-DiffSynth-ControlNets` | `split_files/model_patches/` canny + depth | ~2–3 GB each | Pose/edge/depth-guided layouts — pairs with the legible-text workflow for exact poster composition. The `QwenImageDiffsynthControlnet` node is already in the running ComfyUI |
| 4 | **Wan 2.2 TI2V-5B GGUF** (first video model) | `QuantStack/Wan2.2-TI2V-5B-GGUF` | Q4/Q5 GGUF (~6–8 GB) | ~6–8 GB | T2V+I2V in one dense 5B model; the gentlest on-ramp into video generation, runs in ComfyUI core (`wan` CLIP type is already in this build's loader enum) |
Placement: #1 → `models/lora/` (create it), #2 → `models/diffusion_models/`,
#3 → `models/controlnet/` (create it), #4 → `models/diffusion_models/`.
Fetch with `scripts/download-frank-*.sh` discipline (`-f` + byte-count +
sha256 checks) or a plain `hf download` with the token in the environment —
never on the command line.
Skip list: the original `city96/Qwen-Image-gguf` conversions (superseded by
the 2.1 files already on disk), and any BF16 of #2/#4 (the iGPU does not need
them at GTT capacity, and they double the disk bill).
## See Also
- [[ComfyUI]] — the service itself (image, unit, GPU access)
- [[ComfyUI ROCm and Vulkan]] — the live-verified GPU path
- [[Dankclaw Stack]] — the host
@@ -110,5 +110,6 @@ The web UI responds on `/`. ComfyUI also exposes `/system_stats`. On this APU, `
- [[Dankclaw Stack]]
- [[ComfyUI ROCm and Vulkan]] — the live-verified GPU path research
- [[ComfyUI Qwen-Image Workflows]] — the Qwen-Image 2.1 payload, proven workflows, and recommended model/LoRA additions
- [[Lemonade and sd.cpp]] — the ggml-family alternative
- Knowledge base: [[ComfyUI]] (tools)
+1 -1
View File
@@ -189,7 +189,7 @@ What actually breaks, and the fix:
auxiliary:
title_generation:
provider: dankserver-small # names track the live config; diff first
model: frank-small
model: frank-secondary
api_mode: codex_responses # real per-task key
extra_body:
reasoning:
+5 -5
View File
@@ -24,7 +24,7 @@ https://github.com/open-webui/open-webui
| Upstream latest | 0.11.4 (2026-09-21) — security/access-control advisory; `:main` is the rolling image, not a stable pin. Imagegen endpoint is `POST /api/v1/images/generations` (the `comfyui` engine is still upstream). |
| Port | 3005 (host) → 8080 (container). Port 3000 is Gitea's HTTP, so it is not usable. |
| Data volume | `/home/dank/Containers/open-webui/data` → `/app/backend/data` |
| Backends | Three fleet endpoints: `frank-large` (host.containers.internal:7770), `frank-medium` (192.168.0.235:7770), `frank-small` (host.containers.internal:7771) — see [The LLM backends](#the-llm-backends) |
| Backends | Three fleet endpoints: `frank-large` (host.containers.internal:7770), `frank-medium` (192.168.0.235:7770), `frank-secondary` (host.containers.internal:7771) — see [The LLM backends](#the-llm-backends) |
| Network | Pinned bridge `systemd-open-webui` (`10.89.3.0/24`, gateway `10.89.3.1`) — see [History](#history) |
| Persistence | Quadlet unit `open-webui.service`; `[Install] WantedBy=default.target` |
| Deploy | `scripts/deploy-open-webui-quadlet.sh dankclaw` |
@@ -39,14 +39,14 @@ discovery — so the picker shows exactly the models seeded there.
|--------------|----------------|---------|------|
| `frank-large` | `http://host.containers.internal:7770/v1` | unsloth llama.cpp (Strix Halo iGPU, 200k ctx) | Dankclaw |
| `frank-medium` | `http://192.168.0.235:7770/v1` | vllm-radiance TP=2 (FP8, DFlash2, 262k ctx) | Dankserver |
| `frank-small` | `http://host.containers.internal:7771/v1` | llama.cpp (RTX 3090, Gemma4-12B, 262k ctx) | Dankclaw |
| `frank-secondary` | `http://host.containers.internal:7771/v1` | llama.cpp (RTX 3090, Gemma4-12B, 262k ctx) | Dankclaw |
Two gotchas make the wiring non-obvious:
- **llama.cpp `/v1/models` is Ollama-shaped.** llama.cpp returns
`{"models":[{"name":...}]}`; OpenWebUI's `extract_data()` only reads the
OpenAI `{"data":[{"id":...}]}` shape, so connection discovery silently misses
the llama.cpp endpoints (frank-large, frank-small). The fix is to set
the llama.cpp endpoints (frank-large, frank-secondary). The fix is to set
`model_ids` in each connection's `api_config` — when non-empty, OpenWebUI
builds the model list directly and never fetches `/v1/models`. frank-medium
(vLLM) serves the OpenAI shape and would discover fine, but uses `model_ids`
@@ -218,13 +218,13 @@ on large sizes ⇒ check ComfyUI resources; 401 ⇒ the token is bad.
- The image tracks upstream `main`; pin a digest in the quadlet unit if reproducible rollback matters more than tracking. (The old "CPU-only host" framing is stale: this APU runs the gfx1151 ROCm build — see [[ComfyUI]].)
- The default admin account is created on first login; secure it immediately.
- If the backend is loading (3–8 minute model load), OpenWebUI shows connection errors until the model is ready.
- The on-host LLM services (frank-large `:7770`, frank-small `:7771`) are reached via `host.containers.internal`, NOT the host LAN IP: the container's netavark bridge cannot hairpin to its own host's non-published ports via `192.168.0.143` (connection refused), and the bridge gateway (`10.89.3.1`) lives in the rootless netns while llama-server runs in the host netns, so it refuses too. `host.containers.internal` (169.254.1.2) resolves to the host on the bridge and works — verified end-to-end for both endpoints. The Dankserver backend (frank-medium) is a different host, reached via its LAN IP `192.168.0.235:7770`.
- The on-host LLM services (frank-large `:7770`, frank-secondary `:7771`) are reached via `host.containers.internal`, NOT the host LAN IP: the container's netavark bridge cannot hairpin to its own host's non-published ports via `192.168.0.143` (connection refused), and the bridge gateway (`10.89.3.1`) lives in the rootless netns while llama-server runs in the host netns, so it refuses too. `host.containers.internal` (169.254.1.2) resolves to the host on the bridge and works — verified end-to-end for both endpoints. The Dankserver backend (frank-medium) is a different host, reached via its LAN IP `192.168.0.235:7770`.
## History
- **2026-08-22 — slirp4netns → pinned bridge.** The unit ran on `Network=slirp4netns`. The slirp4netns helper process died (no OOM, no kill entry, no crash log found), and the container kept running with only `lo` in its netns: published port 3005 accepted TCP and reset every request, so chat served empty responses for ~2 days. Nothing logged an error. Moving to a kernel bridge removes the per-container helper entirely, and the healthcheck's interface check makes the zombie state visible if it ever returns.
- **2026-09-21 — model picker rebuilt for the 3-model fleet.** The picker carried 39 stale rows (37 dead Ollama models + 2 `frank-default-*`) and was missing `frank-medium`. Rebuilt the `model` table to exactly `frank-large`/`frank-medium`/`frank-small` and set `model_ids` on each connection (llama.cpp's Ollama-shaped `/v1/models` is invisible to OpenWebUI's discovery). Pointed the two on-host endpoints at `host.containers.internal` — the bridge refuses the host LAN IP for non-published ports. All three verified end-to-end via `/openai/chat/completions`.
- **2026-09-21 — model picker rebuilt for the 3-model fleet.** The picker carried 39 stale rows (37 dead Ollama models + 2 `frank-default-*`) and was missing `frank-medium`. Rebuilt the `model` table to exactly `frank-large`/`frank-medium`/`frank-secondary` and set `model_ids` on each connection (llama.cpp's Ollama-shaped `/v1/models` is invisible to OpenWebUI's discovery). Pointed the two on-host endpoints at `host.containers.internal` — the bridge refuses the host LAN IP for non-published ports. All three verified end-to-end via `/openai/chat/completions`.
- **2026-09-24 — SSO via authentik.** OIDC authorization-code flow wired per the [SSO (authentik)](#sso-authentik) section: application/provider/entitlements created in authentik, client secret in the host env file, roles from the `roles` scope claim. E2E coverage in `tests/e2e/specs/18-openwebui-sso.spec.ts`.
@@ -23,7 +23,7 @@ The homepage is live on Dankclaw. `./scripts/deploy.sh dankclaw` copies the stat
page (`shared/homepage/docs.html`) — access walkthrough, the running-model
guide, media how-tos, and troubleshooting. Per-service `/docs/<svc>/` pages
remain a separate thread. The three LLM cards (`frank-large`, `frank-medium`,
`frank-small`) moved from admin-only to the **user** page with live model
`frank-secondary`) moved from admin-only to the **user** page with live model
descriptions and a `docs` link to `/docs#models`.
**Sections + WIP split (2026-09-21):** both pages now group their panels
@@ -111,7 +111,7 @@ flowchart TD
## Service classification
**User services** (root page), in section order:
- **AI** — OpenWebUI, and the three LLM fleet cards (frank-large, frank-medium, frank-small — status + docs link, no Open link).
- **AI** — OpenWebUI, and the three LLM fleet cards (frank-large, frank-medium, frank-secondary — status + docs link, no Open link).
- **Web Apps** — SearXNG, Gitea, Copyparty, Marreta.
- **Media Library Apps** — Calibre-Web, RomM, Plex, Jellyfin, Seerr.
@@ -38,7 +38,7 @@ Authentik; post-auth the Caddy vhost routes straight to `prometheus:9090`) —
| `node-dankserver` | `192.168.0.235:9100` | host metrics from [[Node-Exporter]] — a different machine, so the LAN address works |
| `grafana`, `loki` | `grafana:3000`, `loki:3100` | container names on the `dankclaw_default` network; both serve real `/metrics` |
| `frank-medium` | `192.168.0.235:7770` | the LLM fleet — `vllm:` series; a different machine, so the LAN address works |
| `frank-large`, `frank-small` | `host.containers.internal:7770`, `:7771` | the LLM fleet — `llamacpp:` series; same-host net=host binds, so via the gateway |
| `frank-large`, `frank-secondary` | `host.containers.internal:7770`, `:7771` | the LLM fleet — `llamacpp:` series; same-host net=host binds, so via the gateway |
**Three jobs read down, and they are not outages** (audited 2026-09-16):
@@ -215,9 +215,9 @@ The DFlash draft is not a standalone model. It must pair with its target model.
## Deployment on Dankserver (NVIDIA RTX 3090)
The Dankserver llama.cpp deployment (`frank-small`, Qwen3.6-35B-A3B) is **active** on Dankserver (port 7770) as of the 2026-08-17 redo (the earlier 2026-08-16/17 retirement of this service was reversed when the PP split was retired). The vLLM material below documents the archived AWQ/DFlash path on the same hardware.
The Dankserver llama.cpp deployment (`frank-secondary`, Qwen3.6-35B-A3B) is **active** on Dankserver (port 7770) as of the 2026-08-17 redo (the earlier 2026-08-16/17 retirement of this service was reversed when the PP split was retired). The vLLM material below documents the archived AWQ/DFlash path on the same hardware.
> **DFlash retired from active service.** `DFlash` (draft-dflash) was tested initially but did not deliver the desired speed gains under the `frank-small` workload (see ngram-mod benchmarks below). The `Alittlehammmer/Qwen3.6-35B-A3B-DFlash` draft GGUF remains in `/home/dank/models/` as reference material only.
> **DFlash retired from active service.** `DFlash` (draft-dflash) was tested initially but did not deliver the desired speed gains under the `frank-secondary` workload (see ngram-mod benchmarks below). The `Alittlehammmer/Qwen3.6-35B-A3B-DFlash` draft GGUF remains in `/home/dank/models/` as reference material only.
The vLLM material below documents the archived AWQ/DFlash path on the same hardware. The RTX 3090 is Ampere (compute capability 8.6). Use these conditions:
@@ -315,7 +315,7 @@ not switch without re-measuring".
The A/B vehicle for ANY single-GPU llama.cpp knob (batch/ubatch, KV type,
slot split, quant rung — not just spec flags) is the transient systemd
drop-in procedure in [[frank-large-benchmark]] and [[frank-small-benchmark]].
drop-in procedure in [[frank-large-benchmark]] and [[frank-secondary-benchmark]].
## Is it worth it? (arithmetic before deployment)
@@ -419,4 +419,4 @@ measurement that would settle it.
- [[GGUF Compatibility]] — header parsing and custom tensor-type lock-in
- [[Llama Cpp Serving Sizing]] — slot-pool and drafter KV sizing arithmetic
- [[Llama Cpp Multi-Device Splits]] — where the drafter's KV actually lands
- [[frank-large-benchmark]] and [[frank-small-benchmark]] — the single-GPU env drop-in A/B vehicle
- [[frank-large-benchmark]] and [[frank-secondary-benchmark]] — the single-GPU env drop-in A/B vehicle
@@ -46,7 +46,7 @@ Dankclaw is an AMD APU server for CIRU ROCmFPX llama.cpp inference. The host run
| Runtime | FastFlowLM 0.9.45 + XRT (installed 2026-08-16; **retired 2026-09-11** with the `amd_iommu=off` decision) |
| Status | Not in service. Research only. See [[FastFlowLM NPU Inference]] |
The NPU is retired (2026-09-11). The `frank-small` name now serves Gemma4-12B QAT on the RTX 3090 via llama.cpp (see [[frank-small-gemma4-12b]]).
The NPU is retired (2026-09-11). The `frank-secondary` name now serves Gemma4-12B QAT on the RTX 3090 via llama.cpp (see [[frank-secondary-gemma4-12b]]).
### ROCm
@@ -17,7 +17,7 @@ timestamp: 2026-09-20
Dankserver is a media/library server and the `frank-medium` LLM host. The
host runs Ubuntu 26.04 (Resolute Raccoon). The RTX 3090 that served the
FreeToken/CUDA `frank-small` era was removed on 2026-09-15 and replaced with
FreeToken/CUDA `frank-secondary` era was removed on 2026-09-15 and replaced with
two AMD Radeon AI PRO R9700 cards.
## Hardware Specifications
@@ -107,8 +107,8 @@ hijack `libhsa-runtime64` were purged on 2026-09-15 — see
The active inference service is `frank-medium.service`: mainline llama-server on
port 7770, layer-split 32:33 across both R9700s with the DFlash2 drafter pinned to
ROCm1. See [[frank-medium-llamacpp-dflash2]]. The former
`frank-small.service` (FreeToken/CUDA) was retired with the 2026-09-15 GPU swap;
its unit is archived in `~/.AGENTTRASH/20260915-frank-small-retire/host-units/`,
`frank-secondary.service` (FreeToken/CUDA) was retired with the 2026-09-15 GPU swap;
its unit is archived in `~/.AGENTTRASH/20260915-frank-secondary-retire/host-units/`,
and the vllm-radiance replica trio in `~/.AGENTTRASH/20260916-vllm-radiance-retire/`.
See [[deployment]].
@@ -13,12 +13,12 @@ timestamp: 2026-09-03
> **SUPERSEDED 2026-08-17.** The Frank redo retired this topology.
> See [[frank-redo-2026-08-17]] and [[deployment]] for the active configuration.
> The `frank-small` name and port 52625 then belonged to the vLLM
> The `frank-secondary` name and port 52625 then belonged to the vLLM
> service on Dankclaw (nameistoken Quark W8A8); this page is archive.
# FastFlowLM NPU Inference
FastFlowLM (FLM) runs LLMs entirely on the AMD Ryzen AI NPU. It targets XDNA2 chips: Strix, Strix Halo, Kraken, and Gorgon Point. The project describes itself as "Ollama, but deeply optimized for NPUs." As of 2026-08-16, FLM is installed on Dankclaw, the NPU validates, and `frank-small` (port 52625) serves in production. See Promotion.
FastFlowLM (FLM) runs LLMs entirely on the AMD Ryzen AI NPU. It targets XDNA2 chips: Strix, Strix Halo, Kraken, and Gorgon Point. The project describes itself as "Ollama, but deeply optimized for NPUs." As of 2026-08-16, FLM is installed on Dankclaw, the NPU validates, and `frank-secondary` (port 52625) serves in production. See Promotion.
## Verdict (2026-08-16)
@@ -203,16 +203,16 @@ Notes:
## Promotion (2026-08-16)
`frank-small` now runs the NPU endpoint on Dankclaw (was the Dankserver llama.cpp service). Unit: `scripts/frank-small.service` (symlinked into `/etc/systemd/system`), wrapper `scripts/serve-flm-small.sh`, tunables `scripts/frank-small.env`. `LimitMEMLOCK=infinity` is set in the unit. Default model: `gemma4-it:e4b`. Swap `FLM_MODEL` in the env to switch.
`frank-secondary` now runs the NPU endpoint on Dankclaw (was the Dankserver llama.cpp service). Unit: `scripts/frank-secondary.service` (symlinked into `/etc/systemd/system`), wrapper `scripts/serve-flm-small.sh`, tunables `scripts/frank-secondary.env`. `LimitMEMLOCK=infinity` is set in the unit. Default model: `gemma4-it:e4b`. Swap `FLM_MODEL` in the env to switch.
### Boot policy (2026-08-17)
The `frank-small` unit is disabled for boot. Only the PP pair (`frank-large` + `frank-pp-worker`) starts at boot. The unit file was removed from `/etc/systemd/system`; the running FLM service keeps serving until stopped or rebooted. To restore the endpoint, run these commands on Dankclaw:
The `frank-secondary` unit is disabled for boot. Only the PP pair (`frank-large` + `frank-pp-worker`) starts at boot. The unit file was removed from `/etc/systemd/system`; the running FLM service keeps serving until stopped or rebooted. To restore the endpoint, run these commands on Dankclaw:
```
sudo ln -sf /home/dank/vllmScripts/scripts/frank-small.service /etc/systemd/system/frank-small.service
sudo ln -sf /home/dank/vllmScripts/scripts/frank-secondary.service /etc/systemd/system/frank-secondary.service
sudo systemctl daemon-reload
sudo systemctl enable frank-small
sudo systemctl enable frank-secondary
```
Live config (2026-08-16): `FLM_PMODE=turbo`, `FLM_ASR=1` (loads Whisper-V3-Turbo-NPU2; exposes `/v1/audio/transcriptions`), `FLM_CTX_LEN=131072` (verified max — Gemma4 natively supports 128K and the NPU budget fits it; KV occupancy measured at 0.11% for a 123-token exchange, decode 12.0 t/s, 13 GiB headroom).
@@ -353,7 +353,7 @@ installed) against `/opt/fastflowlm/share/flm/xclbins/` (available).
Symptom: a client on dankware could not reach `qwen3.6-moe:35b-a3b` on port 52625. The journal shows three separate causes in the 14:19-15:03 window.
1. **EXEC 203 crash loop (14:19-14:20).** The exec bit was lost on `serve-flm-small.sh` on the target. The unit failed `status=203/EXEC: Permission denied` four times, then started after the bit was restored. The git mode is `100755` (commit `3f42210`), and a `git pull` restores the bit. Verify with `git ls-files -s scripts/serve-flm-small.sh`.
2. **Wrong model ID (14:29-14:52).** The client config still used the model ID `frank-small` (a service alias). FLM rejects that tag with `[ERROR] Model tag 'frank-small' is not supported.` Each such request also triggered the unknown-tag fallback below.
2. **Wrong model ID (14:29-14:52).** The client config still used the model ID `frank-secondary` (a service alias). FLM rejects that tag with `[ERROR] Model tag 'frank-secondary' is not supported.` Each such request also triggered the unknown-tag fallback below.
3. **Swap churn (14:48-15:03).** The swap `gemma4-it:e4b` → `qwen3.6-moe:35b-a3b` restarted the service several times. Each load of the 23 GB model takes about 1.5-2.5 minutes at 262144 ctx. The endpoint does not serve during a load.
Fix: the client model ID must be the FLM registry tag `qwen3.6-moe:35b-a3b`. The working pi agent entry in `~/.pi/agent/models.json` is `baseUrl http://192.168.0.143:52625/v1`, `id qwen3.6-moe:35b-a3b`, `contextWindow 262144`, `reasoning true`, `input [text, image]`. Verified end-to-end from dankware (192.168.0.226) at 15:32 on 2026-08-16.
@@ -390,7 +390,7 @@ pi sessions failed against this endpoint with `Stream ended without finish_reaso
- **The FLM serve process is not under systemd.** A reboot of Dankclaw kills it. The PP head service is enabled and returns. The FLM process must be restarted by hand. A unit file is a natural next step.
- **gpt-oss reasoning budget.** The model can spend its full token budget on `reasoning_content` and return empty content. Clients must handle or cap this.
- **No llama.cpp GGUF path.** FLM uses its own pre-compiled kernels. Our UD-Q8_K_XL and HauhauCS builds are not importable. NPU work means FLM-specific model tags.
- **NPU production role (2026-08-16).** `frank-small` serves `qwen3.6-moe:35b-a3b` on port 52625 (swap churn and the unknown-tag incident are recorded above). The remaining NPU content (benchmarks, runbook steps) is still research until a step lands and is recorded.
- **NPU production role (2026-08-16).** `frank-secondary` serves `qwen3.6-moe:35b-a3b` on port 52625 (swap churn and the unknown-tag incident are recorded above). The remaining NPU content (benchmarks, runbook steps) is still research until a step lands and is recorded.
## Citations
@@ -23,7 +23,7 @@ read the returned content:
```python
import json, urllib.request
req = urllib.request.Request("http://<host>:<port>/v1/chat/completions",
json.dumps({"model": "frank-small", "messages": [{"role": "user",
json.dumps({"model": "frank-secondary", "messages": [{"role": "user",
"content": "Hi"}]}).encode(), {"Content-Type": "application/json"})
print(json.load(urllib.request.urlopen(req, timeout=600)))
```
@@ -27,7 +27,7 @@ wedging a host until power cycle. Reference harnesses:
with `container state improper`.
2. **Boot the arm as the service, not a scratch port** (no scratch ports on
LLM hosts — one card, one server). The serve script takes its config via
env-file override (`FRANK_SMALL_ENV_FILE`, `FRANK_PRIMARY_AR_ENV_FILE`)
env-file override (`FRANK_SECONDARY_ENV_FILE`, `FRANK_PRIMARY_AR_ENV_FILE`)
pointing at a staged copy; the checkout is never mutated.
3. **Fast-fail the health wait**: poll `podman ps | grep -qx <name>` inside
the loop — a refused boot dies in one poll instead of 20 min. Capture the
@@ -80,7 +80,7 @@ co-tenants.
- **Traps:** mmap'd PLE is reclaimable page cache; `-ot` makes it pinned —
"move layers to the card" converts reclaimable cache into device memory and
frees nothing for co-tenants. A served model's footprint ≠ weight size
(frank-small: 22.6 of 24.1 GiB for an 11.29 GiB model). Do the capacity
(frank-secondary: 22.6 of 24.1 GiB for an 11.29 GiB model). Do the capacity
subtraction before designing: 12 attn layers × 1.62 GiB + 200k f16 KV =
28.6 GiB > 24.1 GiB usable → KV-in-VRAM on that pair only reaches ~64k.
- **Context lengths: derive, never remember.** `llama-bench` computes
@@ -157,7 +157,7 @@ in the timings. Context cap for full-model residency on that shape stayed
## Related
- [[GGUF Compatibility]] — what a payload is before it is priced
- [[frank-small-llama-mimo-q6k-2x200704]] — the current two-boot priced shape
- [[frank-secondary-llama-mimo-q6k-2x200704]] — the current two-boot priced shape
- [[frank-medium-llamacpp-dflash2]] — where the 68 KiB/token datum was measured
- [[DFlash DSpark Speculative Decoding]] — the spec A/B protocol on llama.cpp
- [[Dual-R9700-Cooperation]] / [[x4-pair-experiment]] — the multi-device side
@@ -8,7 +8,7 @@ timestamp: 2026-10-05T00:00:00-07:00
# Model Swap and Rollback
Applies to every endpoint swap on the fleet (frank-small / frank-large /
Applies to every endpoint swap on the fleet (frank-secondary / frank-large /
frank-primary). Repo law: all config flows through git; the live models are
host state under `/home/dank/models/<name>/`.
@@ -48,10 +48,10 @@ state, verified against the live journal and output smoke tests
The PP pair is the only inference service enabled for boot. Both units
(`frank-large` on Dankclaw, `frank-pp-worker` on Dankserver) are
enabled. The NPU unit (`frank-small` on Dankclaw) and all retired
enabled. The NPU unit (`frank-secondary` on Dankclaw) and all retired
units are disabled. Restart order: worker first, then head. The Dankserver
helper `frank-restart-small.sh` is obsolete (it restarts the removed
`frank-small` unit); restart the worker with
`frank-secondary` unit); restart the worker with
`sudo systemctl restart frank-pp-worker`. The Dankclaw helper
`frank-restart-large.sh` (sync + cache drop + restart) is correct for the
head. `deploy.sh dankserver` is broken until retargeted. See
@@ -65,6 +65,6 @@ graph LR
## See Also
- [[DFlash DSpark Speculative Decoding]]
- [[frank-small-benchmark]]
- [[frank-secondary-benchmark]]
- [[Dankserver System Specs]]
- [[Dankclaw System Specs]]
@@ -247,4 +247,4 @@ see [[dankserver-qwen38-fp8-expert-offload-tuning]] for why it was rejected);
measured campaigns
- [[PCIe Link and Atomics]] — why decode is gather-bound and what cannot fix it
- [[RDNA4 MoE Expert Offload VRAM Budget]] — arena/plan arithmetic deep-dive
- [[frank-large-benchmark]] and [[frank-small-benchmark]] — window discipline on live hosts
- [[frank-large-benchmark]] and [[frank-secondary-benchmark]] — window discipline on live hosts
@@ -122,7 +122,7 @@ export HF_HOME=/mnt/entropy/huggingface
### Active Service Conflict
The retired `frank-small` service (llama.cpp) used about 22 GB of the 24 GB VRAM. The current PP worker holds ~23.8 GiB. The `vllm-awq-qwen36` (Podman) and `vllm-qwen36` units are disabled. Before starting any research service, verify that the GPU is free:
The retired `frank-secondary` service (llama.cpp) used about 22 GB of the 24 GB VRAM. The current PP worker holds ~23.8 GiB. The `vllm-awq-qwen36` (Podman) and `vllm-qwen36` units are disabled. Before starting any research service, verify that the GPU is free:
```
nvidia-smi # verify GPU is free
+1 -1
View File
@@ -268,7 +268,7 @@ ad hoc qwen-fp8 container (the same fork, same payload, same TP=2):
**There is no `--ubatch-size` equivalent in vLLM.** `--ubatch-size` is a
llama.cpp compute-buffer concept (`--batch-size`/`--ubatch-size`); the tcclaviger
fork has no ubatch knob at all. The llama.cpp compute-buffer pair that frank-small
fork has no ubatch knob at all. The llama.cpp compute-buffer pair that frank-secondary
was doubled on (2026-10-05, +4.6 % prefill at 8,928 tokens for 616 MiB) has no
counterpart here, and the two backends should not be reasoned about by analogy.
@@ -1,7 +1,7 @@
---
type: reference
title: Dankserver VRAM and RAM Growth Under Load
description: Investigation of the recurring cuBLAS crash on frank-small. What grows, what does not, and what to do next.
description: Investigation of the recurring cuBLAS crash on frank-secondary. What grows, what does not, and what to do next.
tags:
- dankserver
- llamacpp
@@ -18,7 +18,7 @@ timestamp: 2026-09-03
# Dankserver VRAM and RAM Growth Under Load
This page records the investigation of the recurring cuBLAS crash on
`frank-small` (llama.cpp, RTX 3090 24 GiB). The service aborts
`frank-secondary` (llama.cpp, RTX 3090 24 GiB). The service aborts
with `CUDA error: an unsupported value or parameter was passed to the
function` in `cublasSgemm_v2` at `ggml-cuda.cu:1541` during
`update_slots` -> `decode` -> `process_ubatch`. It crashed on 2026-08-13
@@ -116,7 +116,7 @@ Verified 2026-08-14 after deploy: flash attention enabled, warmup clean, decode
## Mitigation Options
The documentation for the benchmark ([[frank-small-benchmark]])
The documentation for the benchmark ([[frank-secondary-benchmark]])
prescribes the first option for this crash class. None of these change
the model.
@@ -135,6 +135,6 @@ PR #25649 (slot checkpoints freed on prompt clear, merged), issue #26119
## See Also
- [[frank-small-benchmark]]
- [[frank-secondary-benchmark]]
- [[Dankserver System Specs]]
- [[deployment]]
@@ -1,7 +1,7 @@
---
type: reference
title: dealign Ornith-1.5-35B-A3B CRACK on Dankserver
description: dealignai/Ornith-1.5-35B-A3B-UNCENSORED (CRACK Q6_K) served by upstream llama.cpp on the RTX 3090. MoE model with ngram-mod speculative decoding; selected expert weights offloaded to CPU with an explicit --override-tensor. frank-small.
description: dealignai/Ornith-1.5-35B-A3B-UNCENSORED (CRACK Q6_K) served by upstream llama.cpp on the RTX 3090. MoE model with ngram-mod speculative decoding; selected expert weights offloaded to CPU with an explicit --override-tensor. frank-secondary.
tags:
- llamacpp
- cuda
@@ -14,12 +14,12 @@ timestamp: 2026-08-24
# dealign Ornith-1.5-35B-A3B CRACK on Dankserver
> **Active 2026-08-25.** `frank-small` serves this model. It
> **Active 2026-08-25.** `frank-secondary` serves this model. It
> replaced the CRACK Q8_0 file on the same upstream llama.cpp binary (b10121).
> The Q6_K file was downloaded from the model repository and validated with
> `llama-gguf` before installation.
`frank-small` (port 7770) serves
`frank-secondary` (port 7770) serves
`Ornith-1.5-35B-A3B-CRACK-Q6_K.gguf` from `dealignai` on Hugging Face. It is
a **MoE** model (256 experts, 8 active) with a hybrid GatedDeltaNet (SSM) +
attention architecture, abliterated (uncensored). The GGUF also contains a
+9 -9
View File
@@ -21,7 +21,7 @@ SearXNG, Caddy, or another container service does **not** restart inference.
|---|---|---|---:|
| Dankserver | `dank@dankserver` | `frank-medium` | 7770 |
| Dankclaw | `dank@dankclaw` | `frank-large` | 7770 |
| Dankclaw | `dank@dankclaw` | `frank-small` | 7771 |
| Dankclaw | `dank@dankclaw` | `frank-secondary` | 7771 |
## Scoped workflow
@@ -53,13 +53,13 @@ on the selected host. The following files select the corresponding service:
checkout, so any file there changes deployed state.
- `scripts/frank-large.env`, `frank-large.service`, and
`serve-frank-large.sh` select `frank-large` on Dankclaw.
- `scripts/frank-small.env`, `frank-small.service`, and
`serve-frank-small.sh` select `frank-small` on Dankclaw.
- `scripts/frank-secondary.env`, `frank-secondary.service`, and
`serve-frank-secondary.sh` select `frank-secondary` on Dankclaw.
- Documentation, tests, deployment tooling, and unrelated container changes
do not select any inference service.
The target-side `scripts/deploy-remote.sh` installs the selected service.
`frank-large` and `frank-small` are **root units**: the handler relinks the
`frank-large` and `frank-secondary` are **root units**: the handler relinks the
repo file into `/etc/systemd/system/` (`relink_unit`), reloads systemd,
enables it and restarts it. `frank-medium` is a **rootless Quadlet**: the
handler retires its predecessors (the replica Quadlets and the retired llama
@@ -67,7 +67,7 @@ root unit), stops the service's own unit, waits for the published port to
clear, installs the `.container` and restarts the generated unit. Each
handler then waits on that service's health endpoint —
`http://127.0.0.1:7770/health` for frank-large and frank-medium,
`http://127.0.0.1:7771/health` for frank-small. The wait budget is 20 minutes
`http://127.0.0.1:7771/health` for frank-secondary. The wait budget is 20 minutes
because cold model loads are slow. A failed handler does not advance its
per-service state.
@@ -76,7 +76,7 @@ per-service state.
The root units point into the stable target checkout:
```text
/home/dank/serverAppConfigs/scripts/frank-large.service
/home/dank/serverAppConfigs/scripts/frank-small.service
/home/dank/serverAppConfigs/scripts/frank-secondary.service
```
`frank-medium` is a Quadlet under `dankserver/frank-medium/quadlet/`
(`frank-medium.container`) rather than a unit in `scripts/`.
@@ -110,7 +110,7 @@ change in that service's inputs.
If the LLM handler fails after the target pull:
1. Inspect `journalctl -u frank-large -n 200` or `journalctl -u frank-small
1. Inspect `journalctl -u frank-large -n 200` or `journalctl -u frank-secondary
-n 200` (root units), or `journalctl --user -u frank-medium -n 200`
(rootless Quadlet).
2. Confirm the model files, the runtime binary and (for frank-medium) the
@@ -120,13 +120,13 @@ If the LLM handler fails after the target pull:
```bash
./scripts/deploy.sh dankserver --service frank-medium
./scripts/deploy.sh dankclaw --service frank-large
./scripts/deploy.sh dankclaw --service frank-small
./scripts/deploy.sh dankclaw --service frank-secondary
```
4. Verify the matching health endpoint before declaring recovery.
Do not stop the other host's inference service during recovery. The two
Dankclaw endpoints are independent processes on different devices: restarting
frank-large does not disturb frank-small, and the reverse holds too — with one
frank-large does not disturb frank-secondary, and the reverse holds too — with one
exception worth knowing, that the deployer waits for port 7770 to be released
before it starts the replacement container. For benchmark work, follow the
separate stop/benchmark/restart procedure in [[Testing Policy]] and the
@@ -0,0 +1,191 @@
# frank-large — halogen-flash-server 0.16.2, Qwen3.8-Flash-Next v2 Swift1.5 ABLITERATED (Dankclaw, Strix Halo iGPU)
**ACTIVE since 2026-10-06.** Rollback = revert the swap commit's trio
(`scripts/frank-large.env`, `scripts/serve-frank-large.sh`,
`scripts/frank-large.service`); the retired 27B payload stays on disk at
`/home/dank/models/halogen-qwen3.8-27b/` and its fetch script
(`scripts/download-frank-large-halogen.sh`) stays in the repo.
## Why this shape exists
Frank's ask: replace the frank-large model with
`https://huggingface.co/Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated`.
That payload is an **abliterated Swift1.5 finetune of peonist's
Qwen3.8-Flash-Next v2 checkpoint in the v2 `.hgn` format** — a file that
loads ONLY in the **halogen-flash-server** engine line, NOT in the halogen
0.1.4 container that served the 27B dense checkpoint. The model move is
therefore a **full engine migration**, and the accepted-product facts flip
twice in the fleet's favour:
- the base is **abliterated** (the 27B halogen endpoint was a finetune of
the censored `Qwen/Qwen3.8-27B`; refusals were an accepted regression there),
- **vision is back** — the v2 vision tower loads beside the checkpoint and
the engine runs it (the 27B halogen engine silently ignored its encoder),
- **MTP speculative decoding returns** with the head inside the checkpoint
itself (the 27B shipped a DFlash2 drafter instead; comparable purpose,
different file plumbing),
- the card claims **53–57 t/s greedy decode, ~1,640 t/s prefill at 34k, ~72 %
fewer thinking tokens** than the censored 27B (905 mean), and **262,144
native context**. Publisher-card numbers — verify against live traffic
before quoting further (see Verification).
The engine is **closed-source**, ships as `ghcr.io/peonist-ai/
halogen-flash-server:0.16.2` (the version the model card's quickstart pins),
digest-pinned:
```
ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a
```
(3.6 GB image; RepoDigest captured at pull 2026-10-06. Card-tested fallback
if a future pin misbehaves: 0.15.1.)
## The payload — a triangle of files, all sha256-verified at fetch
| File | Bytes | Source repo @ revision | sha256 (LFS) |
|---|---|---|---|
| `qwen38-flash-next-v2-swift15-abliterated.hgn` | 66,687,678,464 | Quat3rnion/halogen-swift1.5-qwen3.8-flash-next-v2-abliterated @ main (`69a85db`) | `9c880b8ba48deabfa06172eff1e7572e4fe465e09e97faec6e50c3f6fdcac037` |
| `qwen38-flash-next-ngram.hgn` (REQUIRED) | 51,200,246,144 | peonist-ai/halogen-qwen3.8-flash-next @ `5cc17cea` | `3450acada94e19aabad88bd49b45eb70304820949c2240fc178067b20ae5dffe` |
| `qwen38-flash-next-vision.hgn` (enabled) | 897,916,416 | peonist-ai/halogen-qwen3.8-flash-next @ `e053f488` | `d62e0ae553fe88afd3833733d4a4c669f34d20fd8dfce4b9610525bed2134b10` |
| `tokenizer/` (6 files, flat dir) | 22.4 MB total | peonist @ `e053f488` | `tokenizer.json` is LFS `0997f410…` (byte-identical to the 27B-era copy); the rest are plain git blobs, size-checked |
Payload notes worth keeping:
- The deployed checkpoint is a **compacted r2** of the card's append-only
original (89,775,868,032 B → 66,687,678,464 B): the abliteration workflow
appends and the repo's `tooling/` compact drops dead padding; live tensors
are byte-identical, the LFS sha256 is the contract, and the 24 GiB saving
is why it fits.
- **`HALOGEN_MTP_HEAD` stays UNSET**: the MTP head is INSIDE the v2
checkpoint (the separate `qwen38-flash-next-mtp.hgn` sidecar is for a
different assembly and is deliberately not fetched).
- The **n-gram lookup table is REQUIRED**. The engine reads it beside the
checkpoint (documented fallback); the card's quickstart also names it
explicitly via `HALOGEN_NGRAM_TABLE`, and the serve script passes both —
the env var is belt-and-braces over the documented beside-the-checkpoint
default, and both mechanisms resolve to the same file.
- The tokenizer is fetched fresh from peonist @ `e053f488` into
`/home/dank/models/halogen-flashnext-v2/tokenizer/` — same flat-directory,
no-symlink law as every other payload (an HF cache snapshot's symlinks
dangle inside the container mount; both the serve gate and the deployer
refuse them).
Fetch: `scripts/download-frank-large-flashnext.sh` (`--verify-only` to
re-check; `-f` + exact byte counts + LFS sha256 + resolve-the-CDN-then-`-C -`
resume law throughout; no token — the repos are public).
## Shape — the knobs that are not free
`scripts/frank-large.env` carries the full rationale inline; the table:
| Env | Value | Why |
|---|---|---|
| `HALOGEN_CTX` | 200192 | per-slot HARD ceiling (a longer prompt is a 400 naming the limit, never truncation). Harness compaction: 70 % = 140,134; 140,134 + 57,344 = 197,478 ≤ 200,192 |
| `HALOGEN_KV_SLOTS` | 2 | the 27B engine's "slots>1 disables the drafter" law does NOT carry over — on this engine batching and speculation coexist (the card's quickstart ships 2 slots WITH the head). 2 slots = concurrent capacity |
| `HALOGEN_KV_POOL_POSITIONS` | 400384 | explicit form of the default (2 × ctx), so the seam test pins the product; ~28 KiB/position ⇒ ~10.7 GiB KV |
| `HALOGEN_HOST_RESERVE_GIB` | 20 | keeps the host honest (the w4b-era lesson) |
| `HALOGEN_SPEC_ADAPT` | **0** | MANDATORY (card): the default adaptive policy turns the MTP head off for whole requests after low acceptance and **halves decode**. The head stays on; temp-1.0 traffic pays some acceptance for it — accepted trade |
| `HALOGEN_MAX_TOK` / `HALOGEN_MAX_TOKENS_CAP` / `HALOGEN_QUEUE_TIMEOUT` | 8192 / 65536 / 7200 | 8192 is only the per-request DEFAULT (the harness always sends 57,344); the cap/timeout pair is COUPLED per FLAGS.md (65536 at ~10 t/s serial needs > 6550 s) |
| `HALOGEN_TEMPERATURE` / `TOP_P` / `TOP_K` | 1.0 / 0.95 / 20 | the card's thinking recipe as SERVER-SIDE defaults — the engine HAS a sampler now, so an absent temperature is no longer greedy; models.yml extraBody matches it (consistent, not load-bearing) |
| `HALOGEN_WEIGHTS_LOCK` | 1 | card's belt-and-braces; the engine pins weights by default on v2 (`HALOGEN_FLASH_PIN_TRUNK=0` is a w4b-only knob and is REFUSED by v2) |
| `HALOGEN_MODEL_ID` / publish | frank-large / 7770:8731 | unchanged aliases; engine port 8730 never published |
| MTP depth | default 2 | FLAGS.md: 2 is best all-round, 3 for code — Frank's traffic is agent/prose |
**Memory ledger** (121.2 GiB unified, the referee is the boot ledger):
weights ~62 GiB resident + KV ~10.7 + arena ~8 (`3.8 + 8.4 × 8192/16384`)
+ vision ~2 ≈ **82.7 GiB engine-side**, + 20 host reserve = ~103 with
~18 GiB margin over the box's baseline co-tenancy. The 51.2 GB n-gram
table rides as mmap'd page cache beside the checkpoint — the card's own
quickstart arithmetic does not bill it as resident, but WATCH the ledger
at boot and re-size if the engine prints a clamp. Prompt cache is ON by
default (`PROMPT_CACHE=2`): a follow-up turn at 100k ctx costs ~2 s
instead of ~88 s cold — the big-TTFT win the 27B engine needed a manual
`HALOGEN_CACHE_MB` pin for is built in here.
## Engine contract facts (FLAGS.md of the flash-server repo)
- OpenAI chat **and** `/v1/responses` **and** `/v1/messages` **and**
`/v1/completions`; tool calls verified on the card; `response_format`
still unsupported (no constrained decoding).
- Thinking → **`reasoning_content`** (same field as every engine this
endpoint has run); the v2 chat template rejects `reasoning_effort`
outside `xhigh|medium|low` — the models.yml fold (high→xhigh) stays
mandatory.
- Vision: OpenAI `image_url` content parts only (the fleet's standard
contract; the legacy top-level `images` field is the frank-secondary trap).
≥256 px probe images or the model answers "video".
- `HALOGEN_MAX_TOKENS_CAP` over-cap is a 400 naming the limit — never a
truncation. `max_tokens` INCLUDES thinking.
- All flags read once at startup; every flag is BITWISE except
`HALOGEN_CK_OVERLAY`, `HALOGEN_MATMUL_TUNING_FILE`,
`HALOGEN_INDEXER_BUDGET`, `HALOGEN_COMPOSABLE_CONTEXT`. Unset
`HALOGEN_CHECKPOINT` defaults to `/models/qwen38-flash-next-v2.hgn`
with the lookup table beside it.
- Prompt-lookup drafting (3,3 window) rides beside the MTP head by
default and is byte-identical to greedy verification where it fires.
- Watchdog: `HALOGEN_ENGINE_WATCHDOG_S=180` with defer-logic for memory
stalls — on a shared host the defaults are the safe choice; the
restart policy stays real through the root unit's `Restart=always`.
Kernel: `CONFIG_HSA_AMD_SVM=y` (CachyOS ships it); `amdgpu.noretry=-1`
(auto) — for this IP auto resolves to retry-on, the same effective state
the 27B engine ran on. Only revisit if the ledger shows SVM complaints.
## Mechanics
- **Root systemd unit** (`scripts/frank-large.service`, `User=dank`,
`LimitMEMLOCK=infinity`, `Restart=always`, `ExecStop=podman stop -t 60
frank-large-halogen`), driven by `scripts/serve-frank-large.sh` — the
fleet's `podman run`-direct pattern. The script runs the container
FOREGROUND so the boot ledger (weights resident, pool fit, MTP head,
prompt cache, any clamp) lands in `/home/dank/logs/frank_large.log`.
- Serve gates mirror the deployer EXACTLY (2026-10-01 crash-loop law):
image digest in podman storage, byte-exact checkpoint + n-gram table +
vision tower, flat tokenizer with all six files as regular files,
GPU nodes present.
- **Host-RAM pin-gate preflight** (added 2026-10-06): the engine
refuses to pin when `MemAvailable` is below the checkpoint (62.11 GiB)
plus a **16 GiB floor** — pinned pages cannot be swapped, and under the
floor the n-gram lookup pages in from disk on every long prompt. A
RAM-short host crash-loops the unit (each boot dies in ~5 s at the pin
gate) until the RAM clears; the 2026-10-06 cutover hit this (8 boots,
MemAvailable 74.77 GiB vs the 78.11 GiB needed, self-resolved in ~2 min
when the dying 27B engine freed RAM). The serve script now checks
`MemAvailable` BEFORE `podman run` and WAITS up to `FRANK_LARGE_RAM_WAIT`
(default 600 s) for the RAM to clear, so a transient dip boots once
instead of crash-looping; if it never clears it refuses loudly. The 16
GiB floor is the engine's (not `HALOGEN_HOST_RESERVE_GIB=20` — a
separate, higher, overall-budget knob).
- Engine stdout goes to the log, NOT the journal. The FIRST request after
a (re)start can sit silent ~45–60 s on this engine class (health
`engine_unresponsive`, 503s) and self-recovers — do not restart over it.
## Verification checklist (run after any redeploy)
1. Boot ledger greps in `/home/dank/logs/frank_large.log`: resident
weights (~62 GiB), KV pool line, MTP/drafter line, prompt-cache line,
**no clamp named**.
2. `curl http://192.168.0.143:7770/health` → 200.
3. Text completion probe: reasoning lands in `reasoning_content`,
`timings` honest (generate ≥128 tokens; at 8 the inter_token number is
first-token noise).
4. Vision probe: a 512×512 solid-red PNG through an `image_url` part →
"Red" (≥256 px, one word expected).
5. Spec decode: `draft_n`/`draft_n_accepted` present in timings on
real-shaped traffic (SPEC_ADAPT=0 means the head is always engaged).
## Client contract (harness)
`~/.omp/agent/models.yml` — provider `llamacpp (dankclaw)` @
`http://192.168.0.143:7770/v1`: `contextWindow: 200192`,
`maxTokens: 57344`, `input: [text, image]`,
`reasoningContentField: "reasoning_content"`, the `high→xhigh` effort
fold, `extraBody` temperature 1.0 / top_p 0.95. Compaction still rides
frank-secondary (the `compactionModel` steering is unchanged).
## Rollback
Revert the swap commit and redeploy — the 27B trio is intact on disk
(payload at `~/models/halogen-qwen3.8-27b/`, fetch script, and the halogen
0.1.4 image digest still in podman storage). The older ROCmFPX rollback
door (`build-rocmfpx.sh` pin) is untouched.
@@ -25,7 +25,7 @@ confirmed by Frank before deploy:
1. **Text-only.** halogen loads the language tower only; README "honest
limits": *"The vision encoder is ignored; image inputs are not supported."*
The mmproj-era vision role moves wholly to frank-small.
The mmproj-era vision role moves wholly to frank-secondary.
2. **Censored base.** The checkpoint is a finetune of the censored
`Qwen/Qwen3.8-27B` base, built to make its DFlash2 head strong. The
ROCmFPX AGENT file was an *Uncensored* build; refusals can return.
@@ -54,6 +54,15 @@ bearing numbers:
set + host baseline on the 121.2 GiB box. The engine sizes the pool itself
and **prints a boot ledger naming any clamp** — the post-deploy check reads
that ledger, not a formula.
- **`HALOGEN_CACHE_MB=16384` (pinned 2026-10-06)** — auto is OFF on this
box: the floor is 2 x a full-context entry (36.9 GB at 262k; one entry
is ~13.7 GB at our 200,700-slot x ~70 KB/token) and the ledger had
35.7 GB free, so the engine refused and every deep agent request
re-prefilled its whole 79-81k context — 84.9-179.8 s measured
(serve_api lines, 2026-10-06) for ~500-650 new tokens per turn. The
README prices the cache at ~20x time-to-first-token on a warm turn;
entries evict LRU by bytes; the reserve (HALOGEN_CACHE_RESERVE_MB,
default 8192) is re-checked against live MemAvailable on every store.
- **No server-side sampler:** an absent temperature is GREEDY. The card's
thinking recipe (temp 1.0 / top_p 0.95) lives in `models.yml` `extraBody`
— on the wire only. The template defaults `reasoning_effort` to **xhigh**
@@ -74,7 +83,7 @@ bearing numbers:
|thinking field|`reasoning_content` (same as before)|
|`reasoning_effort`|same Qwen3.8 template; outside {xhigh,medium,low} rejected — models.yml fold stays; unset defaults to xhigh, `none` disables thinking|
|tools|server-side tool calls advertised — round-trip MUST be re-probed after deploy|
|image input|ignored/error — route to frank-small|
|image input|ignored/error — route to frank-secondary|
## Deploy mechanics
@@ -94,14 +103,18 @@ bearing numbers:
those links dangle inside the container mount.
- **Unit:** `scripts/frank-large.service` (root, `User=dank`, relinked into
`/etc/systemd/system/`), `ExecStart` = serve script, `ExecStop` = `podman
stop -t 120 frank-large-halogen`, script `exec podman wait` so
`Restart=always` is real. Engine stdout goes to
`/home/dank/logs/frank_large.log` (the boot ledger lives there, not in the
journal).
stop -t 120 frank-large-halogen`, script runs `podman run` FOREGROUND
(2026-10-06: the 2026-10-05 `-d` + `exec podman wait` shape captured
only the container ID — the engine ledger existed nowhere on disk and
the post-deploy grep below read a one-line hex file; foreground run
returns the container's exit code so `Restart=always` is real and the
ledger lands in the log). Engine stdout goes to
`/home/dank/logs/frank_large.log` (the boot ledger lives there, not in
the journal).
- **Cutover:** `deploy_llm_large` in `scripts/deploy-remote.sh` sweeps the
legacy bare `llama-server --model /home/dank/models/kingjones-*` process and
the legacy `frank-large` container name by NAME (never `pkill -f
llama-server` — that hits frank-small), retires the old quadlet if the 2026-09
llama-server` — that hits frank-secondary), retires the old quadlet if the 2026-09
halogen era's unit lingers, waits port 7770 free, relinks, health-gates.
- **Bench law:** stop+mask the unit before any GPU experiment on this box
(AGENTS.md); `/health` answers 503 while the 33.4 GiB payload loads — that
@@ -109,8 +122,15 @@ bearing numbers:
## Verification (post-deploy checklist)
1. `grep -iE 'clamp|kv|pool|drafter|dflash' /home/dank/logs/frank_large.log`
— ledger shows the pool at the asked shape, DFlash2 active, NO clamp line.
1. `grep -iE 'clamp|kv|pool|drafter|dflash|cache' /home/dank/logs/frank_large.log`
— ledger shows the pool at the asked shape, DFlash2 active, the
prompt cache line naming the pinned budget, NO clamp line. EXPECT the
WARNING "smaller than ONE full-context entry (18.4 GB at
kMaxCtx=262144)": it sizes against the NATIVE window, not our
200,700 slot — the deepest possible entry (~14 GB at ~70 KB/token)
fits the 16 GB pin, and the 262k entry it fears cannot exist (over
slot is a 400). Probe-proven 2026-10-06: the warm turn hit with
cache_n=16,384.
2. `curl -s http://192.168.0.143:7770/health` -> 200.
3. Real completion with `reasoning_effort` + temp/top_p from the wire recipe;
`reasoning_content` populated; decode rate from the timings.
@@ -120,6 +140,56 @@ bearing numbers:
incl. a tool call (V1TOOLS).
6. Prompt > 200,700 tokens returns the 400 naming the limit (contract test).
## Tuning notes (2026-10-06)
Measured on real agent traffic (serve_api lines) the day after cutover:
- **Decode is depth-bound, and the KV path is the reason.** Shallow
prompts (< 1k tokens) decode 30-50 t/s at commit 4-6.3/round; 46k runs
5.5, 63-67k runs 4.1-6.5, 79-81k runs 3.3-3.8. The round-time slope is
~8 us/token of context: the KV pool reads at ~8 GB/s effective while
the weights stream ~180-240 GB/s (the engine's README puts batch-1
weight streaming at the 240 GB/s hardware wall). Commit also falls
from 4-6 to 2.5-3 accepted/round as the conversation grows.
- **No supported flag touches decode.** `docs/FLAGS.md`: HALOGEN_W4A4 /
W4A4_EXCL are prefill-only ("decode is unaffected either way"); the
undocumented kernel levers are declared unsupported and their defaults
are the measured winners. 0.1.4 is the latest release (CHANGELOG
head; kernels unchanged since 0.1.0). The publisher's tg bench never
measured depth — its tg rows run over the ten SHORT eval prompt
shapes — so the README's "decode barely degrades at depth" claim was
never exercised at 46-80k.
- **A/B'd the one FA lever with public precedent (2026-10-06 stop-window
probe): rejected.** Issue #12's `HALOGEN_FA_TQ=256` (the 64 GB-carve
crash workaround) vs the default tile — same 80k filler prompt,
128-token essays, the runtime's own `timings`: decode 6.35 vs 6.34 t/s
cold and 5.74 vs 5.76 warm (±0.3 % = noise; draft acceptance identical,
53/74 and 58/69), while prefill LOST 9.5 % on this topology (447.5 ->
404.8 t/s). The depth collapse is not an FA tile artifact — it sits
deeper than the knob. The engine binary advertises 40+ undocumented
`HALOGEN_*` kernel levers (FA/DN/MATMUL families, readable via
`grep -aoE "HALOGEN_[A-Z0-9_]+"` on the binary); `FA_TQ` is the only
one with public evidence, and it moved nothing that matters here.
- **Routing is the applied mitigation (2026-10-06):** the harness's
deep-conversation roles (`slow`, `advisor` in `~/.omp/agent/config.yml`)
moved to frank-primary (radiance on Dankserver: fp8 KV at
7.32 KiB/token = 8.7x less KV to read per round at depth, MTP depth 3,
200k window, prefix-cache host+disk tiers). frank-large keeps OpenWebUI
and shallow bursts, where it runs 30-50 t/s. frank-primary's
depth-decode rate is designed-for but not yet measured on record.
- **The lever that existed was the prompt cache, and it was OFF** (see
the shape bullet). The pin turns the 85-180 s re-prefill per deep
turn into a resume; decode itself is expected UNCHANGED by the pin —
the cache resumes prefill, it does not touch the round loop.
Probe-proven (two synthetic 17.3k turns, 2026-10-06): cold prefill
27.0 s (640 t/s, cache_n 0) -> warm turn cache_n 16,384, prompt_n 946
(the extension + one 2048-aligned chunk), prefill 1.7 s — 16x on the
pair, answer correct warm.
- **Everything else stays put.** dflash2 over mtp (publisher 31.71 vs
26.88 mean; at depth both pay the same KV term and dflash2's higher
commit wins), KV_SLOTS=1 (batch XOR speculation), SLOT_CTX=200700
(Frank's window), W4A4 shipped (+9 % prefill, decode unharmed).
## Rollback
Revert the frank-large swap commit and redeploy `dankclaw --service frank-large`:
@@ -148,7 +148,7 @@ in [[frank-large-rocmfpx-q8agent-mtp]] and still applies.
Sweep key for scratch servers is now the payload name:
`pkill -f 'Qwen3.8-27B-Uncensored-Q6_0_ROCMFPX_AGENT'` — never `pkill -f
llama-server` (frank-small is also one).
llama-server` (frank-secondary is also one).
## Operations
@@ -162,7 +162,7 @@ Three traps, all hit for real:
enough for the vision probe).
2. **`MTP boundary missing for seq_id=… ; resyncing after a non-token step`**
appears in the log under draft resets. Self-recovering; not an outage signal.
3. **Never `pkill -f llama-server` on Dankclaw** — frank-small is also a
3. **Never `pkill -f llama-server` on Dankclaw** — frank-secondary is also a
`llama-server`. The sweep key here is the payload name:
`pkill -f 'Qwen3.8-27B-Q8_0_ROCMFPX_AGENT'`.
@@ -35,7 +35,7 @@ timestamp: 2026-10-01
(`scripts/frank-large.service`, `User=dank`, `Restart=always`,
`ExecStop=podman stop -t 120`, `TimeoutStopSec=180` > the stop grace).
- **Model:** `XiaomiMiMo/MiMo-V2.6-Distill-Qwen-9B` in **full BF16**
(17.53 GiB, 4 shards) — the same checkpoint frank-small serves as an INT8
(17.53 GiB, 4 shards) — the same checkpoint frank-secondary serves as an INT8
W8A16 AutoRound conversion on the 3090, here unquantized.
- **Draft:** `Qwen/Qwen3.5-9B` (18 GiB on disk; only the 15 `mtp.*` tensors
are loaded into the draft pass).
@@ -82,7 +82,7 @@ Qwen3.5-9B that preserves the hidden-state geometry — identical
per-position acceptance is the direct measurement that the distillation
preserved more than geometry. MiMo itself ships **ZERO `mtp.*` tensors**
(verified in both the BF16 base and the INT8 conversion) — which is exactly
why frank-small's SGLang NEXTN draft is a 0.00-acceptance ghost: SGLang
why frank-secondary's SGLang NEXTN draft is a 0.00-acceptance ghost: SGLang
reads the head from the TARGET checkpoint, vLLM reads the draft from a
separate path. That difference is the whole trick:
@@ -183,4 +183,4 @@ unit/env/serve trio and redeploying returns the 27B endpoint.
- Engine image: https://hub.docker.com/r/kyuz0/vllm-therock-gfx1151
- Serve script: `scripts/serve-frank-large.sh`; tunables: `scripts/frank-large.env`
- Predecessor: [[frank-large-gufo-qwen38-27b]]; sibling on the same arch:
[[frank-small-sglang-mimo-v26]]
[[frank-secondary-sglang-mimo-v26]]
@@ -211,7 +211,7 @@ re-checking that 35:30 and 37:28 still fail so the gate keeps discriminating.
- [[frank-medium-llamacpp-dflash2]] — the deployment this tuned, and its split
history
- [[frank-small-benchmark]] — the same sweep on a CUDA 3090, where a 16k prompt
- [[frank-secondary-benchmark]] — the same sweep on a CUDA 3090, where a 16k prompt
at depth 0 made prefill climb all the way to ubatch 2048. Depth is the
difference: with no KV to attend over, per-pass overhead dominates and a
bigger micro-batch wins; at 64k of resident KV it does not.
@@ -312,7 +312,7 @@ came up healthy and *text-only*: image requests answered 500 "image input is
not supported — hint: … provide the mmproj" and the load log had no mmproj
line, so `llama-server` fb27a525 does not pick up `mmproj*.gguf` from the
model directory. The serve script now passes `--mmproj` explicitly (the
frank-small pattern), refuses to start without the file when vision is on, and
frank-secondary pattern), refuses to start without the file when vision is on, and
the deploy handler's preflight checks for it. Verified end-to-end after the
fix: a generated 64x32 test image was described from its pixels — with
thinking on the model reasoned about "the three horizontal bars" (its reading
@@ -724,7 +724,7 @@ fleet.
2026-09-15, whose `--split-mode layer` measurements this service reuses
- [[Dual-R9700-Cooperation]] — why the two cards cannot take part in a
collective, and every software escape that is closed
- [[frank-small-qwen38-gsq-rco]] — the fleet's other llama.cpp endpoint (RTX
- [[frank-secondary-qwen38-gsq-rco]] — the fleet's other llama.cpp endpoint (RTX
3090, MTP, the image path)
- [[Dankserver System Specs]] — hardware inventory
- [[frank-medium-batch-ubatch]] — the batch/ubatch sweep that set the deployed
@@ -144,7 +144,7 @@ ggml/src/ggml-backend.cpp:119: GGML_ASSERT(buffer) failed
First deploy (2026-09-15) also ran `retire_root_llm_small()` in
`scripts/deploy-remote.sh`: the CUDA-era `frank-small` unit was stopped,
disabled and moved to `~/.AGENTTRASH/20260915-frank-small-retire/host-units`
disabled and moved to `~/.AGENTTRASH/20260915-frank-secondary-retire/host-units`
(the host's `systemctl disable` has been observed to delete unit files,
2026-08-26 incident, so both outcomes are tolerated).
+2 -2
View File
@@ -55,9 +55,9 @@ image path is halogen's vision tower on `frank-large`.
| consumer | setting | now |
|---|---|---|
| Hermes | `auxiliary.vision` | `dankclaw-large` / `frank-large` (halogen vision tower) |
| Hermes | `auxiliary.title_generation` | `dankserver-small` / `frank-small` |
| Hermes | `auxiliary.title_generation` | `dankserver-small` / `frank-secondary` |
| harness (`~/.omp/agent/models.yml`) | `modelRoles.vision` | `llamacpp (dankclaw)` / `frank-large` |
| harness | `modelRoles.tiny` | `llamacpp (dankserver)` / `frank-small` |
| harness | `modelRoles.tiny` | `llamacpp (dankserver)` / `frank-secondary` |
| harness | `modelRoles.task` | `llamacpp (dankclaw)` / `frank-large:medium` |
The `flm (dankclaw-npu)` provider block was removed from the harness model
@@ -1,6 +1,10 @@
# frank-primary — FreeToken PR #587 ROCm (Qwen3.8-Flash-Next-Uncensored-FP8, 1× R9700)
**Status: ACTIVE 2026-10-05 →** · **Host:** Dankserver (192.168.0.235) · **Port:** 7770 · **GPU:** 1× Radeon AI PRO R9700 (gfx1201) — the second R9700 sits idle
**Status: RETIRED 2026-10-05 → (superseded the same day by the radiance
cutover — see [[frank-primary-radiance-qwen38-native-fp8]]; kept as the
rollback runbook: the image + orcarouter payload stay on disk until the
window closes)** · **Host:** Dankserver (192.168.0.235) · **Port:** 7770 ·
**GPU:** 1× Radeon AI PRO R9700 (gfx1201) — the second R9700 sits idle
The tcclaviger RDNA4 vLLM fork was replaced by **FreeToken at PR #587 head
`77a7201f98f00d93f618c6cf8b81ffe8360e0899`** (branch `rocm714_torch214`), built
@@ -254,7 +254,7 @@ refetch.
- [[frank-large-unsloth-qwen38-27b-q8xl]] — Dankclaw :7770, the same
b11030-mix binary family on gfx1151.
- [[frank-small-gemma4-12b]] — Dankclaw :7771, mainline llama.cpp on the
- [[frank-secondary-gemma4-12b]] — Dankclaw :7771, mainline llama.cpp on the
3090; the only audio-capable endpoint.
- The harness contract lives in `/home/blank/.omp/agent/models.yml`
(`llamacpp (dankserver)` provider, `contextWindow 262144`,
@@ -55,7 +55,7 @@ would drift from the qualification — don't.
## Spec decode: MTP4 only — the ngram-mod ask is NOT expressible
llama.cpp chains drafter types (`--spec-type draft-mtp,ngram-mod` on
frank-small/large). The fork's `speculative_config` is a **single dict**
frank-secondary/large). The fork's `speculative_config` is a **single dict**
(one method per engine): `mtp`, `ngram`, `ngram_gpu`, `dflash`, … are
mutually exclusive — no chaining seam exists in the fork. What IS active:
@@ -0,0 +1,297 @@
# frank-primary — Radiance (StillDeadcode), Qwen3.8-Flash-Next-FP8 NATIVE, 2× R9700 TP=2
**Status: ACTIVE 2026-10-05 →** · **Host:** Dankserver (192.168.0.235) · **Port:** 7770 · **GPU:** 2× Radeon AI PRO R9700 (gfx1201), TP=2
The FreeToken PR #587 endpoint was replaced by the **radiance engine** (C++/HIP,
libr4d kernels for gfx1201, no Python in the serving path) driving the
digest-pinned image `stilldeadcode/radiance@sha256:4c3f484564bb2872784e4…`
(v1.0.7 line, 287 MiB, `libr4d@0.5.0`) as a plain `podman run` through
`scripts/serve-frank-primary.sh` under the **root systemd unit**
(`frank-primary.service`, `User=dank`, `LimitMEMLOCK=infinity`). Same port,
same served alias (`frank-primary`), same container identity
(`frank-primary` = the unit name). Rollback: git tag
`frank-primary-freetoken-pr587-20261005` (FreeToken trio + Dockerfile), image
`localhost/frank-primary-freetoken:pr587-77a7201`, payload
`/home/dank/models/Qwen3.8-Flash-Next-Uncensored-FP8`.
## Why radiance
- **TP=2 is real here.** FreeToken's TP=2 dies in the `pynccl.cu` ROCm JIT
(`cuda_runtime.h` unresolvable for gfx1201 — probed 2026-10-06); the
second R9700 idled. Radiance's TP wire is libr4d's own one-shot
all-reduce (no RCCL), legal on this board since the 2026-09-20 swap
(ProArt B850, x8/x8 on CPU root ports, `Routing+`/`ReqEn+` everywhere).
- **Native FP8 end to end** (Frank's call): the official
`Qwen/Qwen3.8-Flash-Next-FP8` checkpoint keeps its modelopt block-FP8
encoding — no requantisation, no calibration run, no 4-bit codebook.
- **Every tier is explicit and measured**, not an estimator: placement
`expert_tiered` with a pinned host pool, `--ngram-placement ram` for the
47.7 GiB PLE table, `--expert-vs-cache-ratio 0.82` (the publisher's
measured 99 %-hit point; 0.87 already collapses to 0 %), fp8 KV.
## The payload: native FP8, reshaped then converted (one recipe rule)
- **Raw checkpoint**: `/home/dank/models/Qwen/Qwen3.8-Flash-Next-FP8`
(ungated repo, 172.8 GB, 131 shards + index + tokenizer + vision
configs). Fetched by `scripts/download-frank-primary.sh` (vendored
`hfget.sh`, curl+jq, no HF token needed) and **verified byte-for-byte**:
all 131 shards sha256-match the Hub LFS oids (2026-10-05, log
`/home/dank/logs/verify-qwen38-official-fp8.log`).
- **The container**: `/home/dank/models/rad/qwen4exp-fp8-native.rad`
(177,109 MiB measured = 173.0 GiB; floor
`FRANK_PRIMARY_RAD_MIN_MIB=177000` in the env gates a
truncated convert). Built by `scripts/build-frank-primary-rad.sh`:
1. stage `/home/dank/models/stage-qwen4exp-fp8` — `config.json` with
**`quantization_config` dropped** + relative symlinks into the raw
checkout;
2. `stack-qwen38-official-fp8.py` reshapes the staging dir — two
transforms, both value-preserving (script docstring carries the full
argument): the plugin maps the STACKED expert layout
(`mlp.experts.gate_up_proj` `[512,1280,2560]` + `_scale_inv` at the
same index) while the official repo ships per-expert unfused tensors,
so staging byte-stacks all 49 stacks' (48 trunk + MTP) 512 experts
with NO requantisation; and the PLE table ships as 128 **scaleless**
E4M3 shards + one bf16 scalar (1.9931793212890625e-04) while the
plugin's fp8 branch wants the codes beside a ONE-ELEMENT bf16 scale
plane — so staging decodes to a single bf16 `shard_0` with the
checkpoint's own scale (e4m3 decode and the scale are both exact in
bf16);
3. `rad-convert` with ONE `--quant` rule — pure copy for everything else
(every weight keeps the checkpoint's block-FP8 planes: `X.weight`
E4M3 codes + `X.weight_scale_inv` bf16 `[128×128]` scales) — and the
rtn rule re-encodes the bf16 PLE table at the same fixed scale, an
exact round-trip (nearest-code distance 2⁻³ vs the bf16 rounding
perturbation 2⁻⁹): the container gets the checkpoint's own PLE codes
plus the one-element scale plane (`blk.1.ple_ngram.scale`, view {1});
4. THREE LAWS the live runs proved (each cost a full verify+declare
cycle; all fixed in the two scripts, 2026-10-05/06):
- **Scale-name law**: `scale_name()` appends `_scale_inv` to the
TENSOR NAME verbatim — a stacked tensor named
`…experts.gate_up_proj` (no `.weight` suffix) has its scale plane
at `…experts.gate_up_proj_scale_inv`, NOT `…weight_scale_inv`.
- **File-shadow law**: the reader opens every index-named file in
SORTED order and emplaces EVERY tensor each file physically
holds, first-wins (`checkpoint.cpp add_shard`). The raw
model-000NN shards carry tensors NAMED exactly what the merged
PLE shard is named, so the merged file MUST sort first
(`00-ple.merged.safetensors`) or a raw fp8 shard wins the name
and the plan reads one shard as the codes.
- **Symlink-through law**: staging symlinks the raw index into the
stage; `open(path,"w")` writes THROUGH it and corrupts the raw
checkpoint (raw verify then fails 143/144). The stack script
writes index/config temp+`os.replace` — an atomic swap replaces
the symlink itself; the build script COPIES the index and
symlinks only the shards.
5. `rad-info` summary + size floor.
- **Why the staging dir is mandatory** (the descriptor law, spec §2.4):
the plugin is selected by the exact `(arch_id, quant descriptor)` pair
with no fallback. A raw official checkpoint carries
`quantization_config.quant_method: "fp8"` → descriptor `"fp8"` → **no
shipped plugin claims `(qwen4exp, "fp8")`** — raw serve AND unmodified
convert both refuse. The shipped `qwen4exp_fp8` plugin claims
`(qwen4exp, "")` (recipe-built containers). `rad_convert.cpp` writes
`w.set_quant(meta.quant)` into the header, so the staged config's empty
quant method flows through as the container descriptor while **every
weight stays FP8 data**. Native precision is untouched; only the
selection key differs.
- `--plan-only` first: the convert's declare phase is the engine's own,
so a plan that resolves every op will boot. The convert runs card-less
(declare = CPU-side layout queries); add `--device` only if a plugin
load ever demands it.
## The shape (`scripts/frank-primary.env`, every value commented there)
| Knob | Value | Why |
|---|---|---|
| Image | `stilldeadcode/radiance@sha256:4c3f4845…` | digest-pinned INDEX digest (RepoDigests also carry the per-arch manifest `1c582005…`; the image ID `69d05724…` is the config digest, NOT a manifest digest — pinning it sends `podman run` to the network); entrypoint = engine, tools (`rad-convert`/`rad-info`/`rad-tune`/`rad-kbench`) on PATH |
| Container | `frank-primary` | identity = service name across runtimes; legacy list retires `frank-primary-qwen frank-primary-r9v qwen-fp8 glm-probe` |
| `--tp 2 --tp-wire wht6` | both R9700s | fused WHT-6 lossy wire: +5.6–6.3 % prefill, ~−1.3 % 8-stream decode at TP2; `--tp-wire-min-kb` stays engine default (128 KiB) |
| `--max-num-seqs 2` | first boot | Frank: "parallelism = 2 to start". Raise only after a real-traffic look at slot occupancy + the ledger's KV headroom line. The fused all-reduce row count is `seqs × (1 + spec) = 8` |
| `--max-model-len 200000` | served window | native 262,144; the publisher's production value and what `models.yml` quotes. Longer prompt = refused, never truncated |
| `--placement expert_tiered --host-pool-mib 40960` | pinned expert pool **per rank** | native FP8 experts are 112.8 GiB total (56.4 GiB/rank slice) vs ~29 GiB at the 4-bit publisher shape — the pool is the price of native precision. Resize from the boot ledger's coverage line, never arithmetic |
| `--gpu-headroom-mib 96 --expert-vs-cache-ratio 0.82` | publisher values | 0.82 = measured 99 % expert-cache hit; 0.87 → 0 % (the cliff) |
| `--kv-cache-dtype fp8` | **7.32 KiB/token** | 12 QSA full-attention layers, 2 KV heads × 256 dim, K+V; a 200K-token sequence is 1.43 GiB. KV is sized from the VRAM the ratio leaves |
| `--num-speculative-tokens 3` | MTP head | the checkpoint carries `mtp.*` (3,101 tensors, `mtp_num_hidden_layers: 1`); the plugin's probe returns draft depth 3 — the publisher's operating point |
| `--max-num-batched-tokens 2048` | prefill chunk | A/B-verified optimum (1024/2048/4096 ladder, see "Tuning levers") |
| `--prefix-cache-host-mib 4096 --prefix-cache-dir /data/kvcache/frank-primary --prefix-cache-disk-mib 65536` | 3 tiers | host tier pinned (see the ledger), disk tier in `/home/dank/radiance-state` (mounted rw at `/data`; `/` is ext4, mapped-tier-eligible); a session snapshot moves with its blocks |
| `--ngram-placement ram` | **PLE pinned to host RAM** | Frank's pin law. The PLE n-gram table is 51.2 GB (320,001,536 rows × 160 dims = 29 % of all parameters) served via io_uring reads; `ram` **refuses the boot** if memlock is too small |
| `--override-chat-template /templates/template.jinja` | froggeric v22.5 | vendored `scripts/qwen38-froggeric-v22.5.jinja` (28,234 B, `qwen3.8-froggeric-v22.5`); rad-convert bakes the checkpoint's own template, this overrides it, and the engine derives the reasoning/tool-call reply format from it |
**Pinned-RAM ledger** (root unit + `LimitMEMLOCK=infinity` exist for this):
2 × 40 GiB expert pools + 47.7 GiB PLE + 4 GiB prefix host tier + ~2 GiB
spare ≈ **134 GiB**. `ttm.pages_limit` (this kernel has NO `pages_max` —
the two-knob story was stale) caps GPU-mappable pinned pages (default = half
of RAM): the pre-cutover cap was **123 GiB** — BELOW the ledger — so the
serve script's TTM preflight (`FRANK_PRIMARY_TTM_MIN_MIB=140000`) refuses to
start until it is raised. DONE 2026-10-05 pre-cutover: sysfs write of
**52,428,800 pages = 200 GiB** (`/sys/module/ttm/parameters/pages_limit`;
verified read-back; raising at runtime is harmless and allocates nothing).
PERSISTENCE: the sysfs write does not survive reboot — add
`ttm.pages_limit=52428800` to the kernel cmdline at the next scheduled
reboot.
**Per-rank VRAM** at TP2: expert slice 56.4 GiB total across VRAM + host
pool; trunk ~3.75 GiB/rank; token embedding vocab-parallel (~1.18 GiB/rank
— the bf16 table is 2.43 GiB, sharding hands the other half to KV); lm_head
row-sharded; recurrent state 54 MiB/copy/card (2 copies with speculation);
KV from the `0.82`-side remainder of claimed VRAM at 96 MiB headroom.
## The engine contract (all verified in source, not assumed)
- **`/health` == `/ping`**: 200 JSON (`model_id, in_flight, queue_depth`)
while serving, 503 only while stopping — **the listener binds AFTER the
full load**, so a bare `curl -fsS http://127.0.0.1:7770/health` gate is
honest (the deployer loops on it; the FreeToken-era `"status":"ok"` body
grep is gone). Boot budget 1800 s (publisher `start_period` 15 m);
measured 2026-10-06: health 200 ~2-3 min after start (~120 s warm) —
the big terms are the 125.26 GiB page-in (38.1 s at 3.5 GB/s) and the
host-pool pin (PLE 47.68 GiB in 43.9 s).
- **Sampling** (oai.cpp): `temperature, top_p, top_k, min_p, typical_p,
presence_penalty, frequency_penalty, repetition_penalty, repeat_last_n,
chat_template_kwargs, reasoning_effort, enable_thinking,
preserve_thinking` accepted; **unknown params are refused (400)** —
models.yml's extraBody set is fully inside it; nothing unverified rides
the wire. `max_tokens` is clamped, not refused; `seed` absent = pick
one; `response_format` supported; **`logprobs` is refused** (harness
never sends it).
- **Reasoning**: `--reasoning-format` defaults to `auto` → thinking in
`reasoning_content` (models.yml `reasoningContentField` matches). Tool
calls: the qwen3.8 dialect → `message.tool_calls` +
`finish_reason: tool_calls`. No parser flags anywhere — parsers are
auto.
- **Vision**: the official checkpoint carries the 27-layer vision tower;
the engine runs the Qwen-VL processor (patching, pad-token expansion,
3-part rotary are core). `models.yml` keeps `input: [text, image]`;
`--mm-max-patches` available.
- **`/metrics`**: vLLM-compatible names + `radiance:*` families — the
Dankclaw Prometheus job re-scrapes 192.168.0.235:7770 (plain LAN routing
reaches it; the netavark published-port quirk is same-host only).
`/stats`, `/sessions`, `/dashboard` for live internals; the repo ships
`deploy/grafana/radiance.json` if a dashboard is wanted.
- **`n` > 1 requests**: `n` defaults to `max(8, max-num-seqs)`; stop
strings ≤ 64 × 4096 B. `model` field not enforced.
## Deploy + verify
```bash
# one-time (already done 2026-10-05): payload fetch + sha256 verify + convert
ssh dankserver 'bash /home/dank/frank-primary-build/build-frank-primary-rad.sh --plan-only' # declare + plan, no bytes
ssh dankserver 'bash /home/dank/frank-primary-build/build-frank-primary-rad.sh' # full convert + rad-info
./scripts/deploy.sh dankserver --service frank-primary
```
Post-cutover checklist (each is observable, not assumed):
1. Unit: `is-active` active, `is-enabled` enabled, container answers
`/health` with 200 JSON.
2. Boot ledger (engine stdout → `/home/dank/logs/frank_primary.log`):
P2P probe per rank pair, TP=2 shard placement, expert pool coverage
line ("backs N % of the worst case … M full-length sessions"), KV pool
bytes, PLE pinned-RAM line, drafter depth 3, prefix tiers.
3. MTP live: a completion's `timings` object shows draft/accept counts;
`radiance:*spec*` metrics in `/metrics`.
4. Froggeric template: a probe prompt renders the v22.5 shape
(tool-call + thinking turn both exercise it).
5. Vision: a ≥256 px solid-color PNG answers the exact color through an
`image_url` content part.
6. Prometheus target `frank-primary` green (not never-green).
## Cutover measured (2026-10-06, all values observed live)
- Container: 1,857,127,997,440 B = 177,109 MiB = 173.0 GiB. Plan-only
priced 172.76 GiB — the DATA planes; the written container adds a
metadata tail. Floor gates must be set from the real file (the first
deploy refused on 178000 > 177109; floor is 177000).
- Boot: 125.26 GiB read in 38.1 s (3.5 GB/s), PLE pinned
"47.68 GiB in 43.9 s" (the 2 B scale plane with it), each rank's
mover holds 25.46 GiB static+experts (19.15 GiB resident experts,
79 %), 16,820 pinned pool slots, health 200 ~2-3 min after start
(~120 s warm). No 7-minute vLLM-style boot.
- KV ledger: kv_attn 99,956 blocks = 2.48 GiB (399,824 tokens),
kv_qsa_bkey 317.24 MiB, kv_gdn_state 4 slots × 54 MiB, PLE conv
states < 1 MiB; linear checkpoints 56.37 MiB × 8 slots (3 of 8
card-backed at boot, 5 withdrawn under the 208 MiB floor).
- MTP on real traffic (temp 0.7, 220-token ask): draft_n 225 /
draft_n_accepted 119 = 52.9 % acceptance, decode 23.8 t/s. ONE
sample — the shape's real number needs live-traffic measurement.
- Contracts: `reasoning_content` field (matches models.yml), usage
metering separates reasoning_tokens; vision — a 512² solid-red PNG
through `image_url` answers `Red` (279 prompt tokens; NOTE the
32-token budget is eaten by thinking — give vision probes ≥256
max_tokens); native `tool_calls` with finish_reason tool_calls.
- Boot defects fixed the same day: the SSD prefix tier opened only
after the serve script mkdir -p'd the full path (the engine's
create is NOT recursive: "cannot create /data/kvcache/..."); the
converter's scale-name law, the PLE file-shadow and the
symlink-through-raw-index bug are in the payload section above.
- Prometheus: `/metrics` answers 200 with vLLM-compatible families
(verified from Dankclaw); the `frank-primary` job needs a
prometheus-service deploy to go green (config pushed, reload
deferred behind a live parallel writer's dirty tree).
## Tuning levers (in order of likely use)
- **`FRANK_PRIMARY_MAX_SEQS`**: raise 2 → 3/4 only when the ledger's KV
headroom line and real slot occupancy agree; each slot buys a
recurrent-state copy (54 MiB/card) and shrinks the per-request KV share.
- **`FRANK_PRIMARY_HOST_POOL_MIB`**: the ledger's coverage line is the
oracle — it prints what the pool backs at the worst case and how many
full-length sessions fit. RAM budget: pools + PLE + prefix ≤ ~140 GiB of
the host's 246.
- **`FRANK_PRIMARY_EXPERT_VS_CACHE`**: 0.82 is the measured 99 %-hit
point; 0.87 zeroes it. Move only with a measured hit-rate reason.
- **`FRANK_PRIMARY_TP_WIRE`**: `wht6` vs `exact` — `exact` trades the
+5.6 % prefill for bit-exact reductions; the fused one-shot forms are
declared over the seqs × (1+spec) row bound.
- **`FRANK_PRIMARY_EXTRA_ARGS`**: the boot-ladder escape hatch, expanded
unquoted.
- **`rad-tune -m <rad> --tp 2 --num-speculative-tokens 3`**: benchmarks
the tunable space at this model's shapes on this machine, writes
`$RADIANCE_HOME/tune/<machine>.tune` which the engine's selector reads at
boot. Declare at the shapes you serve (`--tp 2
--num-speculative-tokens 3`), or you measure a program the serve never
runs. `$RADIANCE_HOME` is `/opt/radiance/share/radiance` — in the IMAGE — so a
`--rm` scratch container's cache dies with it: the serve script
bind-mounts `$FRANK_PRIMARY_STATE_DIR/tune` (`/home/dank/radiance-state/tune`)
at `/opt/radiance/share/radiance/tune`, so the cache (36 GEMM rows,
`gfx1201-x3-AMD_Radeon_Graphics.tune`, generated 2026-10-06 in a
stopped-service window) survives and is read at every production boot.
The two attention requests have no shape recipe (`attn_paged`,
`attn_paged_gate_quant` — "see tools/opshapes.h") and are not tuned.
`RADIANCE_TUNE` pins a kernel for an A/B — an instrument, not a setting.
- **`FRANK_PRIMARY_MAX_BATCHED_TOKENS` (MNBT)**: A/B'd 2026-10-06 in a
stopped-service window (real-text probes: 20,417-token cold prefill,
1,985-token warm prefill, 256-token decode). The prefill ladder is
NON-MONOTONIC with an interior optimum at 2048: 1024 → 2048 → 4096 =
231 → 460 → 448 t/s cold, 221 → 412 → 427 t/s warm. Decode is flat within
noise (24.0 / 25.1 / 24.3 t/s); draft acceptance 60.1 / 67.1 / 63.3 %.
**2048 wins** (peak prefill + peak acceptance) — it stays the production
value. 1024 halves prefill (the batch too small to saturate the card);
4096 is a hair off 2048 (the chunk past the kernel's sweet spot).
- **Diagnostics**: `rad-info` (container: encodings, recipe, profile),
`--debug-graph` (what declared vs what issued), `rad-kbench` (kernel
correctness/speed vs libref), `/stats` + `/sessions` (live), and the
publisher's `scripts/` harnesses (`fnpf`, `fnconc`, `cstep2`, `ident`,
`mctools`) — note `mcserve/fnstop`-family scripts `pkill` the engine
binary by name; on this host the service is a systemd unit, so use
`sudo systemctl stop frank-primary` instead.
## Rollback
| Layer | Path |
|---|---|
| FreeToken (one card, native FP8 safetensors) | `git checkout frank-primary-freetoken-pr587-20261005 -- scripts/frank-primary.env scripts/frank-primary.service scripts/serve-frank-primary.sh scripts/docker/ tests/frank_primary_unit.bats` + redeploy; image `localhost/frank-primary-freetoken:pr587-77a7201` and payload `/home/dank/models/Qwen3.8-Flash-Next-Uncensored-FP8` stay on disk until the window closes |
| tcclaviger vLLM fork (TP=2, shape G) | git tag `frank-primary-vllm-shapeG-20261003` (trio + env lore; image still in dank's rootless storage) |
| 4-bit StillDeadcode publisher shape | `qwen3.8-next-flash-fp8-iq4r-moe.rad` + `--host-pool-mib 12288` — the documented fallback if the native container ever proves unbootable |
Retire the FreeToken-era orcarouter payload to `~/.AGENTTRASH/` only after
the rollback window closes (trash policy, never `rm`).
## Links
- [[LLM/index]] — fleet index
- [[frank-primary-freetoken-pr587]] — the retired FreeToken era
- [[dankserver-qwen38-fp8-expert-offload-tuning]] — the retired tcclaviger vLLM era
- [[Dual-R9700-Cooperation]] — the board swap that made TP=2 legal here
- [[RDNA4 Container Serving]] — rootless-podman GPU laws on these hosts
- `serverAppConfigs-docs/Architecture/Testing Policy.md` — the seam contract
@@ -18,7 +18,7 @@ The old scripts moved to `old/scripts/` in this repository.
| Service | Host | Runtime | Model | Port |
|---------|------|---------|-------|------|
| frank-large | Dankserver | llama.cpp (patched FastMTP) | HauhauCS Qwen3.8-27B-Uncensored-Aggressive IQ4_XS | 7770 |
| frank-small | Dankclaw | vLLM ROCm | nameistoken Qwen3.6-35B-A3B Quark W8A8 INT8 | 52625 |
| frank-secondary | Dankclaw | vLLM ROCm | nameistoken Qwen3.6-35B-A3B Quark W8A8 INT8 | 52625 |
The RTX 3090 runs the whole 27B model standalone. No RPC worker, no
tensor split, no NPU.
@@ -40,7 +40,7 @@ tensor split, no NPU.
`--spec-draft-ngl all`, `--spec-draft-p-min 0`) then ngram-mod fallback
(48/64/24).
### frank-small — nameistoken/Qwen3.6-35B-A3B-Quark-W8A8-INT8
### frank-secondary — nameistoken/Qwen3.6-35B-A3B-Quark-W8A8-INT8
- Base: Qwen3.6-35B-A3B (hybrid MoE, 256 experts top-8, ~3B active, 40
layers: 10 blocks of 3 DeltaNet + 1 Attention, native 262144 ctx).
@@ -103,10 +103,10 @@ Production choice: **131072 (128K)** — the maximum that keeps about
5. Verify: `curl -sf http://127.0.0.1:7770/health` and a chat completion.
6. Confirm VRAM headroom with `nvidia-smi`.
### Dankclaw (frank-small)
### Dankclaw (frank-secondary)
1. Stop the old services: `sudo systemctl stop frank-large
frank-small` (retired PP head and NPU endpoint).
frank-secondary` (retired PP head and NPU endpoint).
2. Install uv and create the venv:
```
curl -LsSf https://astral.sh/uv/install.sh | sh
@@ -131,9 +131,9 @@ Production choice: **131072 (128K)** — the maximum that keeps about
vision encoder falls back to torch SDPA and engine init OOMs.
5. Symlink the unit, reload, enable, restart:
```
sudo ln -sf /home/dank/vllmScripts/scripts/frank-small.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable frank-small
sudo systemctl restart frank-small
sudo ln -sf /home/dank/vllmScripts/scripts/frank-secondary.service /etc/systemd/system/
sudo systemctl daemon-reload && sudo systemctl enable frank-secondary
sudo systemctl restart frank-secondary
```
6. Verify: `curl -sf http://127.0.0.1:52625/health` and a chat completion.
@@ -159,7 +159,7 @@ on gfx1151; newer vLLM releases may already include it.
Three changes landed after the initial deployment:
### 1. Tool calling on frank-small (HTTP 400 fix)
### 1. Tool calling on frank-secondary (HTTP 400 fix)
Clients (pi) send `tool_choice: "auto"`. vLLM rejects it with HTTP 400
unless the server enables tool calling. Fixes applied:
@@ -184,7 +184,7 @@ model behavior, which motivated the swap.
### 2. Model swap: Avesed INT8 W8A8 -> Quark W8A8 INT8
`frank-small` now serves
`frank-secondary` now serves
`nameistoken/Qwen3.6-35B-A3B-Quark-W8A8-INT8` (AMD Quark 0.11.1, ~35 GB,
vLLM-native expert layout). The Avesed compressed-tensors quant's
Qwen3-XML tool fidelity was poor. Verified with the Quark model: plain
@@ -1,7 +1,7 @@
---
timestamp: 2026-09-03
type: reference
title: frank-small Benchmark — ncmoe and Batch Tuning
title: frank-secondary Benchmark — ncmoe and Batch Tuning
description: llama-bench sweep of MoE CPU offload (ncmoe) and batch/ubatch on the RTX 3090 for Qwen3.6-35B-A3B Q6_K_P.
tags:
- benchmark
@@ -11,11 +11,11 @@ tags:
- dankserver
---
# frank-small Benchmark — ncmoe and Batch/Ubatch Tuning
# frank-secondary Benchmark — ncmoe and Batch/Ubatch Tuning
This page records a llama-bench sweep on Dankserver (RTX 3090, 24 GiB) for
the HauhauCS Qwen3.6-35B-A3B Q6_K_P model. The sweep decided the final
`--n-cpu-moe` and `--ubatch-size` values for `frank-small`.
`--n-cpu-moe` and `--ubatch-size` values for `frank-secondary`.
Run date: 2026-08-13. Build: llama.cpp `555881ebc` (10121). CUDA, 7 threads,
`--cache-type-k q8_0 --cache-type-v f16`, `--fit-target 0` (KV auto-sized to
@@ -106,7 +106,7 @@ service runs 700416 cells (13,680 MiB).
## Chosen State
> **Live drift (2026-08-22 audit):** the deployed `frank-small` now runs `--n-cpu-moe 38`, `--batch-size 4096`, `--ubatch-size 2048` (ncmoe raised from 36 for stability; batch lowered). The values below are the original benchmark recommendation.
> **Live drift (2026-08-22 audit):** the deployed `frank-secondary` now runs `--n-cpu-moe 38`, `--batch-size 4096`, `--ubatch-size 2048` (ncmoe raised from 36 for stability; batch lowered). The values below are the original benchmark recommendation.
Prefill speed is the priority, so the state keeps the top prefill tier and
moves one expert layer to CPU for stability:
@@ -1,10 +1,10 @@
---
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."
title: "frank-secondary on Dankclaw — llama.cpp, Gemma4-12B QAT Q4_K_M"
description: "The active frank-secondary 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
- frank-secondary
- dankclaw
- llama.cpp
- rtx3090
@@ -14,25 +14,25 @@ tags:
timestamp: 2026-09-17
---
# frank-small — llama.cpp Gemma4-12B QAT Q4_K_M (MTP + ngram-mod)
# frank-secondary — llama.cpp Gemma4-12B QAT Q4_K_M (MTP + ngram-mod)
> **ACTIVE since 2026-09-17.** Replaces [[frank-small-qwen38-gsq-rco]]
> **ACTIVE since 2026-09-17.** Replaces [[frank-secondary-qwen38-gsq-rco]]
> (ISTA-DASLab Qwen3.8-27B-GSQ-RCO IQ3_S, now the rollback target) and
> [[frank-small-vllm-gemma4]] (the same-day vLLM experiment, also the
> rollback target). Same port (7771), same alias (`frank-small`), same
> [[frank-secondary-vllm-gemma4]] (the same-day vLLM experiment, also the
> rollback target). Same port (7771), same alias (`frank-secondary`), same
> client contract surface.
## Identity
- **Endpoint:** `frank-small` at `192.168.0.143:7771` (Dankclaw).
- **Endpoint:** `frank-secondary` at `192.168.0.143:7771` (Dankclaw).
- **Runtime:** mainline llama.cpp master `37b53fd`, CUDA build for sm_86 with
`-DGGML_CUDA_FA_ALL_QUANTS=ON`
(`/home/dank/GitPackages/llama.cpp-mainline/build-cuda/bin/llama-server`).
The FA_ALL_QUANTS flag is required: without it, q8_0 KV + flash attention
collapses prefill ~12x on CUDA.
- **Service shape:** root systemd unit `scripts/frank-small.service`
(`User=dank`), launched by `scripts/serve-frank-small.sh` with tunables from
`scripts/frank-small.env`.
- **Service shape:** root systemd unit `scripts/frank-secondary.service`
(`User=dank`), launched by `scripts/serve-frank-secondary.sh` with tunables from
`scripts/frank-secondary.env`.
## Shape
@@ -90,7 +90,7 @@ fault).
## Client contract
- **Alias:** `frank-small` (the `id` at `/v1/models`).
- **Alias:** `frank-secondary` (the `id` at `/v1/models`).
- **Reasoning:** extracted to `reasoning_content` (`--reasoning-format
deepseek`, unlimited budget).
- **Tools:** supported (`--jinja` is pinned even though this build defaults
@@ -109,12 +109,12 @@ pp ~965 t/s, tg 257–302 t/s, idle 23,300 MiB of 24,576. Larger ubatches
## Rollback
Payload and runtime for [[frank-small-qwen38-gsq-rco]] (ISTA-DASLab
Qwen3.8-27B-GSQ-RCO IQ3_S) remain intact on disk. [[frank-small-vllm-gemma4]]
Payload and runtime for [[frank-secondary-qwen38-gsq-rco]] (ISTA-DASLab
Qwen3.8-27B-GSQ-RCO IQ3_S) remain intact on disk. [[frank-secondary-vllm-gemma4]]
is the same-day vLLM experiment (also a rollback target).
# Citations
- Model + mmproj + MTP sidecar: https://huggingface.co/HauhauCS
- llama.cpp: https://github.com/ggml-org/llama.cpp
- Serve script: `scripts/serve-frank-small.sh`; tunables: `scripts/frank-small.env`
- Serve script: `scripts/serve-frank-secondary.sh`; tunables: `scripts/frank-secondary.env`
@@ -1,10 +1,10 @@
---
type: reference
title: "frank-small on Dankclaw — llama.cpp, MiMo-V2.6-Distill-Qwen-9B Q5_K_M GGUF (retired)"
description: "The retired 2026-10-02/03 frank-small runbook — the Q5_K_M rung at two wide unquantized-KV slots on the RTX 3090, superseded 2026-10-03 by the Q6_K file. Kept as the rollback reference: it carries the measured boot-pair table that prices every pool shape on this card, the slot-window contract, and the misbehaves tree."
title: "frank-secondary on Dankclaw — llama.cpp, MiMo-V2.6-Distill-Qwen-9B Q5_K_M GGUF (retired)"
description: "The retired 2026-10-02/03 frank-secondary runbook — the Q5_K_M rung at two wide unquantized-KV slots on the RTX 3090, superseded 2026-10-03 by the Q6_K file. Kept as the rollback reference: it carries the measured boot-pair table that prices every pool shape on this card, the slot-window contract, and the misbehaves tree."
tags:
- llm
- frank-small
- frank-secondary
- dankclaw
- llama-cpp
- rtx3090
@@ -14,17 +14,17 @@ tags:
timestamp: 2026-10-03
---
# frank-small — llama.cpp MiMo-V2.6 Q5_K_M (RTX 3090, retired)
# frank-secondary — llama.cpp MiMo-V2.6 Q5_K_M (RTX 3090, retired)
> **RETIRED 2026-10-03** — superseded by
> [[frank-small-llama-mimo-q6k-2x200704]]: the ~3.5 GiB this shape left free
> [[frank-secondary-llama-mimo-q6k-2x200704]]: the ~3.5 GiB this shape left free
> bought the **Q6_K quant rung** (same runtime, same 2 × 200,704 pool, same
> f16 KV), which measures 2,849 MiB free and 94.44 tok/s — 10 % slower
> decode is the measured price of the higher rung on this bandwidth-bound
> card. This file remains the rollback reference and the owner of the
> measured boot-pair table.
> Its own history: one quant step down from [[frank-small-llama-mimo-q6k]]
> Its own history: one quant step down from [[frank-secondary-llama-mimo-q6k]]
> (2026-10-01), resized twice on 2026-10-02 (three contexts with vision, then
> the 2–3 GiB-band pool), and on 2026-10-03 morning reshaped to **two
> conversations of 200,704 tokens with the KV VALUES unquantized** —
@@ -36,11 +36,11 @@ timestamp: 2026-10-03
|Field|Value|
|---|---|
|Endpoint|`192.168.0.143:7771`, served alias `frank-small`|
|Endpoint|`192.168.0.143:7771`, served alias `frank-secondary`|
|Runtime|llama.cpp **master** (qwen35 model support), CUDA build `sm_86`, at `/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server` — unchanged across the quant swap|
|Payload|`bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF` **Q5_K_M**, 6,876,124,704 B (6.40 GiB), sha256 `04cf46f2b5584ef922697d6c063f94879434fb86c467f1a09e52daf0cddaf35f` + the repo's `mmproj-MiMo-V2.6-Distill-Qwen-9B-f16.gguf`, 918,166,048 B, sha256 `ff348f3180a63188aa7285db85f550fe38acb61dd013c599eb8bad08d2cc2576` — both at `/home/dank/models/MiMo-Q5KM/`, fetched and byte- and sha256-verified by `scripts/download-frank-small.sh`|
|Unit|root `frank-small.service` (repo `scripts/frank-small.service`, `User=dank`, relinked), env `scripts/frank-small.env`, serve `scripts/serve-frank-small.sh`|
|Repo pins|`tests/frank_small_unit.bats` (stub-argv seam + the VRAM arithmetic), `deploy_llm_small` gates in `scripts/deploy-remote.sh`|
|Payload|`bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF` **Q5_K_M**, 6,876,124,704 B (6.40 GiB), sha256 `04cf46f2b5584ef922697d6c063f94879434fb86c467f1a09e52daf0cddaf35f` + the repo's `mmproj-MiMo-V2.6-Distill-Qwen-9B-f16.gguf`, 918,166,048 B, sha256 `ff348f3180a63188aa7285db85f550fe38acb61dd013c599eb8bad08d2cc2576` — both at `/home/dank/models/MiMo-Q5KM/`, fetched and byte- and sha256-verified by `scripts/download-frank-secondary.sh`|
|Unit|root `frank-secondary.service` (repo `scripts/frank-secondary.service`, `User=dank`, relinked), env `scripts/frank-secondary.env`, serve `scripts/serve-frank-secondary.sh`|
|Repo pins|`tests/frank_secondary_unit.bats` (stub-argv seam + the VRAM arithmetic), `deploy_llm_secondary` gates in `scripts/deploy-remote.sh`|
## Shape and the KV contract
@@ -128,7 +128,7 @@ The per-slot window is the whole price, in both directions:
"Brown.", i.e. it generated from text alone. `/completion` takes `images`
but refuses a request without `messages` in this build, so chat is the only
vision path.
- Setting `MMPROJ_FILE=` (explicitly empty) in `scripts/frank-small.env` drops
- Setting `MMPROJ_FILE=` (explicitly empty) in `scripts/frank-secondary.env` drops
`--mmproj` and returns ~1.4 GiB of the card to KV — the documented text-only
opt-out. `MMPROJ_FILE` pointing at a file that does not exist is a boot
refusal, never a silent fallback to blindness.
@@ -145,15 +145,15 @@ traffic first. The bats seam fails if a draft flag ships unannounced.
|Target|How|
|---|---|
|Previous shape: 3 × 180,224 + q8_0 V (2026-10-02)|`scripts/frank-small.env` `CTX_SIZE=540672 PARALLEL=3 CACHE_TYPE_V=q8_0` (measured 21,684 MiB used, 2,441 free) and `contextWindow: 180224` in `models.yml` — choose it when three simultaneous conversations matter more than per-slot window or exact V|
|Q6_K, same runtime|`scripts/frank-small.env` `MODEL_FILE=/home/dank/models/MiMo-Q6K/MiMo-V2.6-Distill-Qwen-9B-Q6_K.gguf` + `MMPROJ_FILE` in that dir — **this is the deployed rung since 2026-10-03** ([[frank-small-llama-mimo-q6k-2x200704]]): at 2 × 200,704 it fits with 2,849 MiB free, no pool cut needed (the older "+1.4 GiB, drop 49,152 tokens" estimate held only for the 3-slot shapes)|
|Previous shape: 3 × 180,224 + q8_0 V (2026-10-02)|`scripts/frank-secondary.env` `CTX_SIZE=540672 PARALLEL=3 CACHE_TYPE_V=q8_0` (measured 21,684 MiB used, 2,441 free) and `contextWindow: 180224` in `models.yml` — choose it when three simultaneous conversations matter more than per-slot window or exact V|
|Q6_K, same runtime|`scripts/frank-secondary.env` `MODEL_FILE=/home/dank/models/MiMo-Q6K/MiMo-V2.6-Distill-Qwen-9B-Q6_K.gguf` + `MMPROJ_FILE` in that dir — **this is the deployed rung since 2026-10-03** ([[frank-secondary-llama-mimo-q6k-2x200704]]): at 2 × 200,704 it fits with 2,849 MiB free, no pool cut needed (the older "+1.4 GiB, drop 49,152 tokens" estimate held only for the 3-slot shapes)|
|Two 262,144 windows|`CTX_SIZE=524288 PARALLEL=2 CACHE_TYPE_V=q8_0` (measured 20,190 MiB used, 4,386 free **with q8_0 V** — with f16 V the same shape is 16,384 MiB of KV + ~7,986 non-KV and does not fit) and `contextWindow: 262144` in `models.yml` — choose it when a single conversation needs the native window|
|Vision off|`MMPROJ_FILE=` in the env file; the deployer gate then has to be satisfied too, so point `LLM_SMALL_MMPROJ` at any real projector file or drop the gate in the same commit|
|SGLang + INT8|[[frank-small-sglang-mimo-v26]] — venv and old unit/env/serve trio still in place; the INT8 payload was staged to `/home/dank/.AGENTTRASH/20261002-retired-llm-payloads/` on 2026-10-02, so restore it there or re-fetch with `old/download-frank-small-sglang-int8.sh`|
|Vision off|`MMPROJ_FILE=` in the env file; the deployer gate then has to be satisfied too, so point `LLM_SECONDARY_MMPROJ` at any real projector file or drop the gate in the same commit|
|SGLang + INT8|[[frank-secondary-sglang-mimo-v26]] — venv and old unit/env/serve trio still in place; the INT8 payload was staged to `/home/dank/.AGENTTRASH/20261002-retired-llm-payloads/` on 2026-10-02, so restore it there or re-fetch with `old/download-frank-secondary-sglang-int8.sh`|
## When it misbehaves
- Port 7771 dark: `journalctl -u frank-small -n 50`; the serve script exits 1
- Port 7771 dark: `journalctl -u frank-secondary -n 50`; the serve script exits 1
naming the missing file if the binary or GGUF moved.
- `No CUDA GPUs are available`: the 3090 vanished from the bus or the UVM node
is missing — `nvidia-modprobe -u -c=0` (the serve script runs it).
@@ -1,10 +1,10 @@
---
type: reference
title: "frank-small on Dankclaw — llama.cpp, MiMo-V2.6 Q6_K, 2 x 200704, f16 KV"
description: "The active frank-small runbook — llama.cpp master (CUDA sm_86) serving bartowski's MiMo-V2.6-Distill-Qwen-9B Q6_K GGUF on the RTX 3090: two conversations of 200,704 tokens with fully unquantized f16 KV, vision through the bundled projector, and the boot-pair arithmetic that prices every pool change."
title: "frank-secondary on Dankclaw — llama.cpp, MiMo-V2.6 Q6_K, 2 x 200704, f16 KV (RETIRED)"
description: "The RETIRED frank-secondary runbook (2026-10-03 → 10-05 evening, rollback target) — llama.cpp master (CUDA sm_86) serving bartowski's MiMo-V2.6-Distill-Qwen-9B Q6_K GGUF on the RTX 3090: two conversations of 200,704 tokens with fully unquantized f16 KV, vision through the bundled projector, and the boot-pair arithmetic that prices every pool change. Superseded by the Swift-1.5-27B IQ3_S-mtp swap."
tags:
- llm
- frank-small
- frank-secondary
- dankclaw
- llama-cpp
- gguf
@@ -12,29 +12,31 @@ tags:
timestamp: 2026-10-03
---
# frank-small — llama.cpp MiMo Q6_K, 2 × 200,704, f16 KV
# frank-secondary — llama.cpp MiMo Q6_K, 2 × 200,704, f16 KV
> **ACTIVE since 2026-10-03.** The third shape Frank bought in two days on
> this card: the freed third slot paid for **exact KV values**
> ([[frank-small-llama-mimo-q5km]] documents that step and remains the
> rollback), and the ~3.5 GiB still free then paid for the **Q6_K quant rung**
> — the same file this endpoint served on 2026-10-01, come back as an
> upgrade rather than a compromise. The pool held at 401,408 (2 × 200,704);
> only the weights moved up.
> **RETIRED 2026-10-05 (evening).** Superseded by
> [[frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688]] — the Swift-1.5-27B
> RCO-IQ3_XXS-mtp swap (uncensored, MTP drafted). Two facts for the trail:
> committed earlier the same day (77decb5..380aafa) was **never deployed** —
> THIS MiMo shape was the live payload when Swift cut over; (2) the KV
> contract Frank set here (f16 K + f16 V, "no further quantization on this
> endpoint") was superseded the same evening by the Swift swap's explicit
> q8_0/q8_0 order. This page stays as the rollback and the measured
> boot-pair record. Payload on disk: `/home/dank/models/MiMo-Q6K/`.
## Identity
| | |
|---|---|
| Host / device | Dankclaw (192.168.0.143), RTX 3090, 24,576 MiB |
| Endpoint | `http://192.168.0.143:7771` — alias `frank-small`, `/v1` OpenAI surface |
| Endpoint | `http://192.168.0.143:7771` — alias `frank-secondary`, `/v1` OpenAI surface |
| Runtime | llama.cpp master, CUDA sm_86 build `…/llama-master/build-cuda/bin/llama-server` (`GGML_CUDA_FA_ALL_QUANTS=ON`) |
| Unit | root systemd `frank-small.service` ([`scripts/frank-small.service`](../../scripts/frank-small.service)), `User=dank`, `Restart=on-failure`, `LimitMEMLOCK=infinity` |
| Serve script | [`scripts/serve-frank-small.sh`](../../scripts/serve-frank-small.sh), tunables [`scripts/frank-small.env`](../../scripts/frank-small.env) |
| Unit | root systemd `frank-secondary.service` ([`scripts/frank-secondary.service`](../../scripts/frank-secondary.service)), `User=dank`, `Restart=on-failure`, `LimitMEMLOCK=infinity` |
| Serve script | [`scripts/serve-frank-secondary.sh`](../../scripts/serve-frank-secondary.sh), tunables [`scripts/frank-secondary.env`](../../scripts/frank-secondary.env) |
| Weights | `/home/dank/models/MiMo-Q6K/MiMo-V2.6-Distill-Qwen-9B-Q6_K.gguf` — bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K**, 7,793,710,624 B, sha256 `ef96d05a…` (qwen3_5 hybrid: 32 layers, 8 full-attention + 24 Gated DeltaNet, native 262,144, no MTP tensors) |
| Projector | `…/MiMo-Q6K/mmproj-MiMo-V2.6-Distill-Qwen-9B-f16.gguf` — 918,166,048 B, sha256 `ff348f…` (hardlink of the Q5KM dir's copy, link-count-2 verified; the dir is self-contained) |
| Fetch | [`scripts/download-frank-small.sh`](../../scripts/download-frank-small.sh) — `-f`, byte count + LFS sha256 verified, `--verify-only`; both files verified against the pinned sha on the host before this deploy |
| Seam | `tests/frank_small_unit.bats` (argv assertions against a stub engine: payload path, slot geometry, KV pair, batch pair, projector opt-out) |
| Fetch | [`scripts/download-frank-secondary.sh`](../../scripts/download-frank-secondary.sh) — `-f`, byte count + LFS sha256 verified, `--verify-only`; both files verified against the pinned sha on the host before this deploy |
| Seam | `tests/frank_secondary_unit.bats` (argv assertions against a stub engine: payload path, slot geometry, KV pair, batch pair, projector opt-out) |
## Shape and KV contract
@@ -65,7 +67,7 @@ compaction-prompt fit by 1,236 tokens — or revert V to `q8_0`), and both
were declined. Re-open only on evidence the Q6_K residual matters.
**Pool prices on this card come from boots, never formulas** — the measured
boot-pair table lives in [[frank-small-llama-mimo-q5km]] (*Shape and KV
boot-pair table lives in [[frank-secondary-llama-mimo-q5km]] (*Shape and KV
contract*) and still prices every rollback shape.
## Compute-buffer geometry (2026-10-05)
@@ -76,7 +78,7 @@ measured rather than assumed, because the predecessor shape recorded **"ubatch
**That failure was a different shape, and it did not reproduce here.** The
service itself is the bench (one card holds one server, so no scratch port):
the old rung was re-booted through a transient `FRANK_SMALL_ENV_FILE` drop-in
the old rung was re-booted through a transient `FRANK_SECONDARY_ENV_FILE` drop-in
and reproduced the 2026-10-03 boot **byte-identically** — 21,276 MiB used /
2,849 MiB free — which makes the ubatch the only variable in the comparison.
@@ -97,7 +99,7 @@ Three readings:
- **The price is 616 MiB out of a 2.8 GiB margin.** 2,279 MiB free is still
inside the 2–3 GiB band, at its bottom edge. `4096/1536` is the middle rung,
and the pool's 12,288-token (384 MiB) ladder in
[[frank-small-llama-mimo-q5km]] is the other way to buy room back.
[[frank-secondary-llama-mimo-q5km]] is the other way to buy room back.
**Why 2048 is right HERE while 512 was right on the R9700 sibling: the curve is
backend-specific.** The 2× R9700 layer split peaks at ubatch 512 and is 10 %
@@ -123,7 +125,7 @@ that is the pair the curve was measured at.
## Client contract
llama.cpp surface, live-verified on this model family: model id `frank-small`,
llama.cpp surface, live-verified on this model family: model id `frank-secondary`,
native tools via `--jinja` (`tool_calls` in the response), thinking in
**`reasoning_content`**, vision via `image_url` content parts. Two vision
traps, both measured here:
@@ -145,13 +147,13 @@ so the harness compacts before llama.cpp answers a longer prompt with a 400.
|Target|How|
|---|---|
|Q5_K_M at the same shape|`scripts/frank-small.env` `MODEL_FILE=/home/dank/models/MiMo-Q5KM/MiMo-V2.6-Distill-Qwen-9B-Q5_K_M.gguf` (+ `MMPROJ_FILE` in that dir) and the deployer default — buys back 875 MiB (3,571 free measured) **and the 10 % decode rate** (104.84 tok/s measured). Payload sha-verified on disk|
|Q5_K_M at the same shape|`scripts/frank-secondary.env` `MODEL_FILE=/home/dank/models/MiMo-Q5KM/MiMo-V2.6-Distill-Qwen-9B-Q5_K_M.gguf` (+ `MMPROJ_FILE` in that dir) and the deployer default — buys back 875 MiB (3,571 free measured) **and the 10 % decode rate** (104.84 tok/s measured). Payload sha-verified on disk|
|3 × 180,224 + q8_0 V|`CTX_SIZE=540672 PARALLEL=3 CACHE_TYPE_V=q8_0` (measured 21,684 used / 2,441 free **with Q5_K_M** — Q6_K does NOT fit that shape inside the floor; pair them explicitly)|
|SGLang + INT8|[[frank-small-sglang-mimo-v26]] — payload staged to `/home/dank/.AGENTTRASH/20261002-retired-llm-payloads/`|
|SGLang + INT8|[[frank-secondary-sglang-mimo-v26]] — payload staged to `/home/dank/.AGENTTRASH/20261002-retired-llm-payloads/`|
## When it misbehaves
Same decision tree as [[frank-small-llama-mimo-q5km]] (*When it misbehaves*):
Same decision tree as [[frank-secondary-llama-mimo-q5km]] (*When it misbehaves*):
`No CUDA GPUs are available` ⇒ `nvidia-modprobe -u -c=0`; a 400 naming
200,704 ⇒ the conversation outgrew one slot, not the pool; slow decode after
a shape change ⇒ check `nvidia-smi` for KV pushed into unified memory; queue
@@ -1,10 +1,10 @@
---
type: reference
title: "frank-small on Dankclaw — llama.cpp, MiMo-V2.6-Distill-Qwen-9B Q6_K GGUF (retired)"
description: "The retired 2026-10-01 frank-small runbook — the first llama.cpp + GGUF shape on the RTX 3090: two native-window sessions, the asymmetric KV contract (f16 K + q8_0 V) that motivated the move off SGLang, the VRAM fit at Q6_K, and what to check when it misbehaves. Superseded the same night by the Q5_K_M payload."
title: "frank-secondary on Dankclaw — llama.cpp, MiMo-V2.6-Distill-Qwen-9B Q6_K GGUF (retired)"
description: "The retired 2026-10-01 frank-secondary runbook — the first llama.cpp + GGUF shape on the RTX 3090: two native-window sessions, the asymmetric KV contract (f16 K + q8_0 V) that motivated the move off SGLang, the VRAM fit at Q6_K, and what to check when it misbehaves. Superseded the same night by the Q5_K_M payload."
tags:
- llm
- frank-small
- frank-secondary
- dankclaw
- llama-cpp
- rtx3090
@@ -14,10 +14,10 @@ tags:
timestamp: 2026-10-01
---
# frank-small — llama.cpp MiMo-V2.6 Q6_K (RTX 3090)
# frank-secondary — llama.cpp MiMo-V2.6 Q6_K (RTX 3090)
> **RETIRED 2026-10-01 → 10-02** (one night): superseded by
> [[frank-small-llama-mimo-q5km]], the same runtime and shape one quant rung
> [[frank-secondary-llama-mimo-q5km]], the same runtime and shape one quant rung
> down. The Q6_K file stays at `/home/dank/models/MiMo-Q6K/` as the rollback
> target.
>
@@ -29,11 +29,11 @@ timestamp: 2026-10-01
|Field|Value|
|---|---|
|Endpoint|`192.168.0.143:7771`, served alias `frank-small`|
|Endpoint|`192.168.0.143:7771`, served alias `frank-secondary`|
|Runtime|llama.cpp **master** (qwen35 model support), CUDA build `sm_86`, at `/home/dank/GitPackages/llama-master/build-cuda/bin/llama-server`|
|Payload|`bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF` **Q6_K**, 7,793,710,624 B, sha256 `ef96d05a2ddf2cbb450d1af1ac3860ec769d3575bafa70692ee5609bda3fad7d`, at `/home/dank/models/MiMo-Q6K/`|
|Unit|root `frank-small.service` (repo `scripts/frank-small.service`, `User=dank`, relinked), env `scripts/frank-small.env`, serve `scripts/serve-frank-small.sh`|
|Repo pins|`tests/frank_small_unit.bats` (stub-argv seam), `deploy_llm_small` gates in `scripts/deploy-remote.sh`|
|Unit|root `frank-secondary.service` (repo `scripts/frank-secondary.service`, `User=dank`, relinked), env `scripts/frank-secondary.env`, serve `scripts/serve-frank-secondary.sh`|
|Repo pins|`tests/frank_secondary_unit.bats` (stub-argv seam), `deploy_llm_secondary` gates in `scripts/deploy-remote.sh`|
## Shape and the KV contract
@@ -100,7 +100,7 @@ use the parent of the 2026-10-01 night swap commit).
## When it misbehaves
- Port 7771 dark: `journalctl -u frank-small -n 50`; the serve script
- Port 7771 dark: `journalctl -u frank-secondary -n 50`; the serve script
exits 1 naming the missing file if the binary or GGUF moved.
- `No CUDA GPUs are available`: the 3090 vanished from the bus or the UVM
node is missing — `nvidia-modprobe -u -c=0` (the serve script runs it).
@@ -0,0 +1,338 @@
---
type: reference
title: "frank-secondary on Dankclaw — llama.cpp, Swift-1.5-27B Uncensored RCO IQ3_XXS-mtp, 2 x 114688, draft-mtp, q8_0 KV"
description: "The active frank-secondary runbook (live since 2026-10-05 evening; renamed from frank-small 2026-10-06, per-slot lowered to 112k and ubatch raised to 1024 the same evening) — llama.cpp master (CUDA sm_86) serving ajgazin's Swift-1.5-Qwen3.8-27B Uncensored RCO-IQ3_XXS (MTP head in the file, drafted) on the RTX 3090: TWO 114,688-token conversations at Frank's 112k cap, q8_0 K + q8_0 V (llama.cpp's FP8 class), vision through the publisher's own BF16 projector, and the full two-slot + drafter arithmetic."
tags:
- llm
- frank-secondary
- dankclaw
- llama-cpp
- gguf
- rtx3090
- qwen35
- mtp
- vision
timestamp: 2026-10-06
---
# frank-secondary — llama.cpp Swift-1.5-27B Uncensored RCO IQ3_XXS-mtp, 2 × 114,688, draft-mtp, q8_0 KV
> **ACTIVE since 2026-10-05 (evening); reshaped three times on
> 2026-10-06, renamed from frank-small the same night.** A 27B replaces
> the 9B on the same 24 GiB card: KV at FP8 class, vision on. The
> morning order (drop MTP, ubatch 512, TWO 128k contexts) bought the
> second slot — 1,397 MiB free. The evening order swapped to the
> UNCELERSED Swift-1.5 at RCO-IQ3_XXS and put MTP back ON — landing
> **489 MiB free**: the MTP draft ctx costs **~2.4 GiB, not the ~0.4
> GiB assumed** (the drafter's KV is built against the target's full
> context, not the 3-token proposal depth), paid for by the lighter
> quant (−1.56 GiB). The late order dropped the per-slot window
> 131,072 → **114,688** (Frank's 112k cap) and climbed the prefill ladder
> ubatch 512 → 1024 → **2048** — landing **1,005 MiB free** (1,751 at
> the 1024 rung; the 2048 rung cost 746 MiB, ~130 over the 616 MiB
> measured prediction). This supersedes BOTH earlier 2026-10-05
> payloads: the live MiMo-V2.6 Q6_K shape
> ([[frank-secondary-llama-mimo-q6k-2x200704]], now retired) and the
> Ornith-1.5-9B Q8_0 swap that was committed (77decb5..380aafa) but
> never deployed.
## Identity
| | |
|---|---|
| Host / device | Dankclaw (192.168.0.143), RTX 3090, 24,576 MiB |
| Endpoint | `http://192.168.0.143:7771` — alias `frank-secondary`, `/v1` OpenAI surface |
| Runtime | llama.cpp master (0.5.0-dev, commit 5fc4f3c), CUDA sm_86 build `…/llama-master/build-cuda/bin/llama-server` (`GGML_CUDA_FA_ALL_QUANTS=ON` — required for quantized V) |
| Unit | root systemd `frank-secondary.service` ([`scripts/frank-secondary.service`](../../scripts/frank-secondary.service)), `User=dank`, `Restart=on-failure`, `LimitMEMLOCK=infinity` |
| Serve script | [`scripts/serve-frank-secondary.sh`](../../scripts/serve-frank-secondary.sh), tunables [`scripts/frank-secondary.env`](../../scripts/frank-secondary.env) |
| Weights | `/home/dank/models/Swift15-UncIQ3XXS/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf` — ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF **RCO-IQ3_XXS -mtp**, 10,442,827,392 B, sha256 `e13d3e74…` (qwen35 hybrid: 64 real layers, 16 full-attention + 48 Gated DeltaNet, native 262,144; **MTP head in the file**: blk.64.nextn.*, `nextn_predict_layers=1`, and DRAFTED with it) |
| Projector | `…/Swift15-UncIQ3XXS/mmproj-BF16.gguf` — PUBLISHER-SHIPPED in the same repo, 931,145,888 B, sha256 `19acd7fc…` (27-layer ViT → 5120) |
| Fetch | [`scripts/download-frank-secondary.sh`](../../scripts/download-frank-secondary.sh) — `-f`, byte count + sha256 verified per file (Xet repo: the Hub APIs expose no LFS oid, so the shas were computed locally after the first verified fetch; byte counts match the Hub's declared sizes) |
| Seam | `tests/frank_secondary_unit.bats` (argv assertions against a stub engine: payload path, slot geometry, KV pair, spec pair (both flags present), batch pair, projector opt-out) |
## Why this payload (2026-10-06 evening)
Frank asked for the UNCELERSED Swift-1.5 with MTP back on. The RCO-IQ3_XXS
is 1.56 GiB lighter than the IQ3_S-mtp it replaced — and that lighter
weight is exactly what funds the MTP draft context (~2.4 GiB measured)
PLUS the second 131,072 slot, while keeping the q8_0 KV contract and the
two-slot shape Frank ordered that morning.
**Uncensored, intentionally:** the publisher reports 23/100 refusals vs
Swift 1.5's 98/100 (KL 0.0884 against the censored base). The refusal
escape hatch is the same as every payload here: revert and redeploy.
## Shape and KV contract
`--ctx-size 229376 --parallel 2` — TWO conversations at Frank's 112k
cap (2026-10-06 late: cut from the 128k the morning/evening shapes ran
at, to fund ubatch 1024). `CTX_SIZE` stays divisible by `PARALLEL`; the
seam enforces it, and the per-slot number (`229376 / 2 = 114,688`) is
the product contract — models.yml follows at 114,688 (a longer prompt
is a 400, never a truncation).
**KV at FP8 class: `q8_0` K + `q8_0` V (Frank, 2026-10-05 evening).**
llama.cpp has no true fp8 KV dtype; q8_0 is the 8-bit class (1.0625 B/elem
⇒ 8.5 effective bits) and the only FP8-class lever this runtime moves.
The order supersedes the same-day f16/f16 rule, which was written for the
Ornith 9B fit — at 64 KiB/token f16 the single 131,072 slot alone would
cost 8.0 GiB of KV. At q8_0 the rate is **34 KiB/token** (16 attn layers ×
4 KV heads × 256 dim × 1.0625 B, K and again V). `--flash-attn on` is
pinned: quantized V requires it, and the build's
`GGML_CUDA_FA_ALL_QUANTS=ON` (2026-09-17) makes the path real rather than
a silent f16 fallback.
**Two slots + drafter, by arithmetic and by order (2026-10-06 morning /
evening, the 2 × 131,072 era).** The morning order bought the second slot
by spending the drafter and ubatch headroom (1,397 MiB free). The evening
swap put the drafter BACK and changed the arithmetic: the draft ctx is
built against the target's FULL context (262,144 — the draft window, not
the 3-token proposal depth), so it costs **~2.4 GiB measured** (23,636 −
22,728 + 1,561), not the ~0.4 GiB the one-slot era assumed. The lighter
quant pays for it: **23,636 used / 489 MiB free** (nvidia-smi `free`
field) — the tightest fit in frank-secondary's history, the 2–3 GiB floor
knowingly violated, and the 512×512 vision transient (34 MiB) still fits
and recovers.
**The late reshape (2 × 114,688, ubatch 2048) is the current fit.** The
per-slot cut frees 2 × 16,384 tokens × 34 KiB ≈ 1.05 GiB of KV; the
ubatch rungs 512 → 1024 → 2048 spend it back on compute buffers (a
prefill knob — decode never moves with it; the rungs cost ~0.3 and
~0.75 GiB measured). Measured boot at 2048: **23,120 used / 1,005 MiB
free**. The drafter's own KV shrank with its target (draft window
229,376). Compute buffers allocate at graph capture during BOOT, so a
miss is loud and fast — the failure mode is a crash loop, not silent
corruption.
## Speculative decoding — ON since 2026-10-06 evening (draft-mtp, depth 3)
**Frank's evening order: MTP back on.** The MTP head ships IN the file —
the `-mtp` GGUF carries `blk.64.nextn.*` (`qwen35.nextn_predict_layers=1`)
— and `--spec-type draft-mtp` loads it against the main model's own
weights (no `--model-draft`, no second draft model — two context-backed
drafters cannot share a process). `SPEC_DRAFT_N_MAX=3` is the proposal
depth. **ngram-mod stays skipped, not swapped in:** on a qwen35 hybrid,
ngram speculation logs *"recurrent state rollback is not compatible with
'ngram-mod' — disabling rollback support"* (fleet lesson from
frank-large's chain measurement, 2026-10-03). A rejected draft on Gated
DeltaNet layers cannot be rewound by KV truncation — without the rollback
checkpoints the DeltaNet state carries phantom transitions and output
degrades silently. The flags move as a set: the serve script emits BOTH
`--spec-type` and `--spec-draft-n-max` together or neither
(`SPEC_TYPE=none` strips both).
## Vision contract
`--mmproj` is the entire vision path (the GGUF carries no vision tensors).
Probes: a solid-color PNG **≥ 256 px** answers its exact color — a tiny
probe undersupplies image tokens and the model invents ("video"); the
legacy top-level `images` field is IGNORED, only the OpenAI `image_url`
content part reaches the model. **New on this payload: the uncensored
model THINKS more than the censored Swift 1.5** — a 64-token budget was
eaten entirely by `reasoning_content` (`finish_reason: length`, empty
`content`); 128 tokens clears it. Token budgets for vision probes must
leave room for thinking.
## Measured: the 1 × 131,072 MTP boot (2026-10-05, historical)
- **Boot ledger**: `creating MTP draft context against the target model
'…/Swift-1.5-Qwen3.8-27B-GSQ-RCO-IQ3_S-mtp.gguf'`, `loaded multimodal
model, '…/Swift15-IQ3S/mmproj-Qwen3.8-27B-F16.gguf'`, `n_slots = 1,
n_ctx_slot = 131072` — every expected line, no q8→f16 fallback.
- **VRAM: 19,404 used / 4,721 MiB free** (nvidia-smi `free` field). The
4.5 GiB prediction held; f16 KV would have left ~700 MiB, so this free
number doubles as the q8_0 proof.
- **Decode + MTP acceptance** (draft counts from the live `timings`):
temp 0.8 → 66.1 tok/s, 182/349 accepted (52.1 %); temp 0 → 66.9 tok/s,
166/318; **deployed recipe (temp 1.0/top_p 0.95/top_k 20) → 58.9 tok/s,
159/355 (44.8 %)** — temperature buys fluency, costs acceptance and
rate. For scale: MiMo Q6_K measured 94.4 tok/s on the same card — this
27B at IQ3_S runs ~2/3 the rate with ~3× the capability class.
- **Vision**: 512×512 solid-red PNG → `Red`, `finish_reason: stop`.
- **Template gate**: `reasoning_effort: xhigh` → 200 with
`reasoning_content` populated; `high` → HTTP 500 (Jinja raise) — the
models.yml fold is mandatory, exactly as the MiMo eras found.
- Streaming completion: 200. No first-request silence window (the
halogen-era 45–60 s stall has no analogue here).
## Measured: the 2 × 131,072 no-spec boot (2026-10-06 morning, historical)
- **Boot ledger**: `loaded multimodal model, '…/Swift15-IQ3S/
mmproj-Qwen3.8-27B-F16.gguf'` and NO draft-context line (speculation
off). `/slots`: **2 slots, n_ctx 131,072 each** (pool 262,144).
- **VRAM: 22,728 used / 1,397 MiB free** (nvidia-smi `free` field) — the
predicted 0.3–1.3 GiB window held; the old 2-3 GiB floor is knowingly
violated by Frank's order. After a 512×512 image probe: 1,363 MiB —
the vision transients fit.
- **Decode without the drafter**: 48.0 tok/s (temp 0) / 47.8 (deployed
recipe temp 1.0). `draft_n` absent from `timings` (no drafter). The MTP
era's 58.9–66.9 tok/s is the honest price of the second context:
−18–28 %.
- **Vision**: 512×512 solid-red PNG → `Red` again on this shape.
- Deploy: commit 812d9eb, e2e gate 33/33 green on the live fleet.
## Measured: the 2 × 131,072 draft-mtp boot (2026-10-06 evening, historical)
- **Boot ledger**: `creating MTP draft context against the target model
'…/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-RCO-IQ3_XXS.gguf'`,
`loaded multimodal model, '…/Swift15-UncIQ3XXS/mmproj-BF16.gguf'`,
`n_slots = 2, n_ctx_slot = 131072` — every expected line, no q8→f16
fallback.
- **VRAM: 23,636 used / 489 MiB free** (nvidia-smi `free` field) — the
draft ctx measured ~2.4 GiB, ~6× the ~0.4 GiB the one-slot era
assumed; the lighter quant (−1.56 GiB) plus the morning's no-spec base
still fund both slots. After a 512×512 image probe: 489 MiB — the vision
transient (34 MiB) fits and recovers.
- **Decode + MTP acceptance** (draft counts from the live `timings`,
512-token essay): temp 0 → **72.9 tok/s**, 333/534 accepted (62.4 %);
deployed recipe (temp 1.0/top_p 0.95/top_k 20) → **63.3 tok/s**,
305/612 (49.8 %). e2e traffic in the boot window read 0.52–0.67
acceptance. Against the no-spec shape (48.0 tok/s): **+31 % at the
recipe temp** — the drafter earned its 2.4 GiB.
- **Vision**: 512×512 solid-red PNG → `Red` at a 128-token budget (64
tokens: all thinking, empty content). `prompt_tokens: 321` confirms the
image part reached the model.
- **Prompt cache**: `cached_tokens: 317` of 321 on the second probe — the
slot prefix cache is live.
- Deploy: commit da36fa3, e2e gate 33/33 green on the live fleet.
## Measured: the 2 × 114,688 / ubatch-1024 boot (2026-10-06 late, historical)
- **argv** (from `/proc/<MainPID>/cmdline`): `--ctx-size 229376
--parallel 2` (per-slot 114,688), `--batch-size 4096 --ubatch-size
1024`, `--spec-type draft-mtp`. Deploys 87721ab (the rename — old
`frank-small.service` stopped + disabled first, by hand: `disable` on
a relinked unit removes only its symlink) then f2575a1 (the shape);
health gate + MainPID check green, e2e 32 passed / 1 flaky-retry.
- **VRAM: 22,374 used / 1,751 MiB free** (nvidia-smi `free` field) —
the per-slot cut (≈1.05 GiB of KV) paid for the ubatch-1024 buffers
and netted back ~1.25 GiB against the evening's 489 MiB; back above
the morning's 1,397 MiB.
- **Decode + MTP acceptance** (live `timings`): recipe (temp 1.0) →
**58.2 t/s** (430/1005, 42.8 %); temp 0 → **66.9 t/s** (475/873,
54.4 %). The 2 × 131,072 boot measured 63.3 / 72.9 — −8–10 %, but
acceptance is text-dependent and these prompts differ from that
measurement's, so the comparison is CONFOUNDED, not a shape verdict;
a clean A/B needs stop + mask (bench policy).
- **Smoke**: `READY` completion + `reasoning_content` populated; the
harness reached :7771 under the new alias (models.yml provider/id
`frank-secondary`, config.yml modelRoles rewritten; per-slot
`contextWindow: 114688`).
## Measured: ubatch 2048 (2026-10-06 midnight, live)
- **argv**: `--ctx-size 229376 --parallel 2 --batch-size 4096
--ubatch-size 2048 --spec-type draft-mtp` (commit d8da7a9, Frank's
"set ubatch to 2k"; deploy green, e2e gate 33/33).
- **VRAM: 23,120 used / 1,005 MiB free at boot** — the 1024 → 2048 rung
cost **746 MiB**, ~130 over the 616 MiB the rung measured on
2026-10-05 (rung cost is shape-dependent, not a constant). The
512×512 solid-red vision probe (`Red`) then took ~240 MiB **and kept
it**: settled **763 MiB free** — on this boot the vision encoder's
buffers stay resident after the request (the evening boot saw its
34 MiB transient return).
- **Decode (ubatch-invariant knob, same prompt as the 1024 rung)**:
recipe → 52.0 t/s (388/1133, 34.2 %) vs 58.2 t/s / 42.8 % at 1024 —
acceptance variance at temp 1.0, not a ubatch effect.
- **Prefill (THE ubatch knob)**: **1,209 t/s @ 3,760 tokens** — first
recorded number for this shape; future rungs compare against this
(no same-prompt 1024 baseline exists; the law predicts ≈ +4.6 %
1024 → 2048).
- **Headroom left ~0.7–1.0 GiB.** ubatch 4096 (= the logical batch, no
micro-splitting) doubles the buffer growth again — not affordable;
**2048 is the ceiling for this shape.**
## Client contract
- Reasoning in `reasoning_content` (`--reasoning-format deepseek`);
thinking tokens COUNT against `max_tokens`. **This payload thinks more
than the censored Swift 1.5** (uncensored finetune) — short `max_tokens`
budgets can be eaten entirely by thinking (`finish_reason: length`,
empty content); the 32,768 `maxTokens` in models.yml is comfortably
above.
- The embedded chat template gates `reasoning_effort` to
`{xhigh, medium, low}` — anything else is a raise/HTTP 500, so
`models.yml` `compat.reasoningEffortMap` folds the harness levels onto
that set (mandatory, same as every Qwen3.8-template endpoint).
- Sampling rides the wire (`extraBody`): the publisher recipe temp 1.0 /
top_p 0.95 / top_k 20 / min_p 0 (evaluated at ctx 131,072).
- The RCO-IQ3_XXS tier is a **per-tensor allocation mix** (IQ3_S 97 /
BF16 96 / IQ3_XXS 83 / IQ2_S 72 / IQ4_XS 33 / IQ2_XS 32 / IQ2_XXS 30 /
Q2_K 28 / Q4_K 19 / Q6_K 8 / IQ1_S 4 / IQ1_M 4) — all stock quant types;
stock llama.cpp loads it. Publisher KLD vs Swift BF16: mean 0.002631,
90th 0.042136, max 34.49 — RCO is the publisher's "more experimental"
family.
## Rollback
**Predecessor shape (same 2 × 114,688, ubatch 1024) is ONE revert
away** — revert d8da7a9 and redeploy. The 2 × 131,072 / ubatch-512
era is one revert further (f2575a1). The 2 × 131,072 no-spec
IQ3_S-mtp shape is one revert further — revert da36fa3 next: the env
trio then ships `SPEC_TYPE=none`, `CTX_SIZE=262144`, `PARALLEL=2`,
`UBATCH_SIZE=512` against `/home/dank/models/Swift15-IQ3S/` (the
IQ3_S-mtp payload, still on disk, measured 48.0 tok/s no-spec). The
1 × 131,072 MTP shape is two reverts further back (812d9eb's parent):
the same IQ3_S-mtp file with `SPEC_TYPE=draft-mtp`, `UBATCH_SIZE=1024`,
`CTX_SIZE=131072`, `PARALLEL=1` (measured 58.9–66.9 tok/s with the head
drafting).
Three payloads on disk, all fetchable by the same script's git-history
recipes:
1. **Swift-1.5 IQ3_S-mtp** — the payload live until this cutover:
`/home/dank/models/Swift15-IQ3S/`; the IQ3_XXS swap above.
2. **MiMo-V2.6 Q6_K 2×200,704 f16 KV** — the payload that was LIVE until
the 2026-10-05 cutover: `/home/dank/models/MiMo-Q6K/`; revert
[[frank-secondary-llama-mimo-q6k-2x200704]] (env trio at commit e5893dc or
380aafa's parent), one `MODEL_FILE` + shape edit.
3. **Ornith-1.5-9B Q8_0** — the committed-but-never-deployed swap:
`/home/dank/models/Ornith-Q8/`; revert to 380aafa's tree (ONE native
262,144 slot, f16/f16 KV).
## Trail
- The Ornith-1.5-9B Q8_0 swap (77decb5 → 380aafa, earlier 2026-10-05) was
committed and pushed but its deploy never completed (blocked at the
whole-tree docs gate); the live service was still MiMo Q6_K when Swift
cut over. Both Ornith payloads were verified on disk before Swift.
- Swift header facts (block_count 65, interval 4, KV geometry, nextn) were
read from the Hub file by HTTP range + struct parse — no download needed
— before the payload was fetched.
- Cutover 2026-10-05 ~23:26: the radiance-cutover session (parallel)
label-committed both sets at Frank's direction — 1919ad7 (frank-primary
radiance) then baf33d0 (this swap: trio, seam, runbooks, fleet docs).
Deploy ran `deploy.sh dankclaw --service frank-secondary`; the service
dispatch and health gate went green; the first e2e run failed ONE spec
(12-crawl, `net::ERR_ABORTED` on `www.franksdank.cloud` — a transient,
unrelated to frank-secondary), green on rerun.
- The unit's `Description=` (MiMo era) was stale at cutover time and was
updated in-repo afterward — the relinked unit picks it up at the next
deploy/restart, not retroactively.
- 2026-10-06 (Frank): drop MTP, skip ngram, ubatch 512, TWO 128k contexts
(812d9eb). The tight fit landed: 1,397 MiB free, vision `Red`, decode
48.0 t/s no-spec. Model of the trade: the drafter's ~0.4 GiB + ubatch
~0.3 GiB bought the second 4,352 MiB KV slot.
- 2026-10-06 (Frank, evening): swap to the UNCELERSED Swift-1.5 at
RCO-IQ3_XXS, MTP back on (da36fa3). The lighter quant (−1.56 GiB) funds
the draft ctx (~2.4 GiB measured — the drafter's KV scales with the
target's full 262,144 context, not the 3-token proposal depth) plus the
second 131,072 slot: 489 MiB free, vision `Red` at a 128-token budget,
decode 63.3 t/s recipe / 72.9 temp 0 (48.0 no-spec).
- 2026-10-06 (late, Frank): the endpoint renamed frank-small →
**frank-secondary** (87721ab — scripts, unit, seam, runbooks,
identifiers; historical references to the OLD FreeToken/CUDA
frank-small, and to the Dankserver service that became frank-medium,
stay frank-small) and the per-slot window cut 131,072 → 114,688 to
fund ubatch 512 → 1024 (f2575a1). Live unit transition done by hand
before the deploy: `systemctl stop frank-small` + `systemctl disable
frank-small`, then `deploy.sh dankclaw --service frank-secondary`.
1,751 MiB free; decode 58.2 t/s recipe / 66.9 temp 0; models.yml +
config.yml retargeted.
- 2026-10-06 (midnight, Frank): "set ubatch to 2k" → `UBATCH_SIZE=2048`
(d8da7a9). Cost 746 MiB: 23,120 used / 1,005 free at boot, settling
**763 MiB free after a vision probe** (encoder buffers stay resident
on this boot). Decode 52.0 t/s recipe on the shared prompt (58.2 at
the 1024 rung — acceptance noise, decode is ubatch-invariant);
prefill 1,209 t/s @ 3,760 tokens (first recorded baseline). 2048 is
the ceiling for this shape; 4096 would not fit.
@@ -0,0 +1,15 @@
---
type: reference
title: "frank-secondary on Dankclaw — llama.cpp, Qwen3.8-27B GSQ-RCO IQ3_S"
description: "The RETIRED frank-secondary runbook (2026-09-16 → 09-17, rollback target) — mainline llama.cpp on Dankclaw's RTX 3090 serving ISTA-DASLab Qwen3.8-27B-GSQ-RCO at IQ3_S with the model's own in-file MTP head and the BF16 vision projector: identity, KV arithmetic, deployed flags, the MTP contract, the client contract, and what to check when it misbehaves."
tags:
- llm
- frank-secondary
- dankclaw
- llama.cpp
- rtx3090
- qwen35
- mtp
- vision
timestamp: 2026-09-16
---
@@ -1,10 +1,10 @@
---
type: reference
title: "frank-small on Dankclaw — SGLang, MiMo-V2.6-Distill-Qwen-9B INT8 W8A16 (RETIRED)"
description: "The RETIRED frank-small runbook (2026-10-01, hours) — SGLang in a self-contained python3.12 venv serving leteschlead MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound (qwen3_5 hybrid, 9B dense, in-checkpoint MTP + vision) on the RTX 3090: identity, payload, KV arithmetic, the NEXTN spec-decode ghost, the CUDA 13 nvcc toolchain the JIT needs, and the measured numbers kept for the record. Replaced by [[frank-small-llama-mimo-q6k]] the same night."
title: "frank-secondary on Dankclaw — SGLang, MiMo-V2.6-Distill-Qwen-9B INT8 W8A16 (RETIRED)"
description: "The RETIRED frank-secondary runbook (2026-10-01, hours) — SGLang in a self-contained python3.12 venv serving leteschlead MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound (qwen3_5 hybrid, 9B dense, in-checkpoint MTP + vision) on the RTX 3090: identity, payload, KV arithmetic, the NEXTN spec-decode ghost, the CUDA 13 nvcc toolchain the JIT needs, and the measured numbers kept for the record. Replaced by [[frank-secondary-llama-mimo-q6k]] the same night."
tags:
- llm
- frank-small
- frank-secondary
- dankclaw
- sglang
- rtx3090
@@ -15,10 +15,10 @@ tags:
timestamp: 2026-10-01
---
# frank-small — SGLang MiMo-V2.6-Distill-Qwen-9B INT8 W8A16 (NEXTN MTP)
# frank-secondary — SGLang MiMo-V2.6-Distill-Qwen-9B INT8 W8A16 (NEXTN MTP)
> **RETIRED 2026-10-01, hours after cutover.** Replaced by
> [[frank-small-llama-mimo-q6k]] — llama.cpp CUDA on the MiMo Q6_K GGUF,
> [[frank-secondary-llama-mimo-q6k]] — llama.cpp CUDA on the MiMo Q6_K GGUF,
> chosen for the asymmetric KV (f16 K + q8_0 V) SGLang cannot express.
> The venv (`/home/dank/GitPackages/sglang-cu128/`) stays in place; the INT8
> payload was staged to
@@ -28,13 +28,13 @@ timestamp: 2026-10-01
## Identity
- **Endpoint:** `frank-small` at `192.168.0.143:7771` (Dankclaw).
- **Endpoint:** `frank-secondary` at `192.168.0.143:7771` (Dankclaw).
- **Runtime:** SGLang (latest) in a self-contained python3.12 venv at
`/home/dank/GitPackages/sglang-cu128/` (torch 2.13.0+cu130,
transformers 5.12.1, flashinfer 0.6.18, sgl-kernel). No container — a root
systemd unit `scripts/frank-small.service` (`User=dank`,
`LimitMEMLOCK=infinity`) launched by `scripts/serve-frank-small.sh` with
tunables from `scripts/frank-small.env`.
systemd unit `scripts/frank-secondary.service` (`User=dank`,
`LimitMEMLOCK=infinity`) launched by `scripts/serve-frank-secondary.sh` with
tunables from `scripts/frank-secondary.env`.
- **Model:** `letechlead/MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound`
(9B dense, 12.0 GiB, INT8 W8A16 AutoRound group-128 symmetric).
@@ -55,7 +55,7 @@ timestamp: 2026-10-01
the two-way concurrency contract. **That compaction arithmetic is
specific to a 262,144-token slot**: the ACTIVE shape runs three slots of
180,224, which 183,501 tokens already exceeds — see *What three slots
cost* in [[frank-small-llama-mimo-q5km]].
cost* in [[frank-secondary-llama-mimo-q5km]].
- **KV:** `fp8_e4m3` — 16 KiB/token (8 attn layers x 2 x 4 heads x 256
dim x 1 B), pool 9.31 GiB.
- **The memory shape (the 4.5 GiB the naive math misses):** the DeltaNet
@@ -126,15 +126,15 @@ change lands.
Eight safetensors shards (12,891,718,440 B total) + tokenizer and config
files under `/home/dank/models/MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound/`,
all LFS sha256-verified by `old/download-frank-small-sglang-int8.sh`
all LFS sha256-verified by `old/download-frank-secondary-sglang-int8.sh`
(`--verify-only` re-checks) — the script moved to `old/` when the endpoint
cut over to llama.cpp + GGUF on 2026-10-01 night, and
`scripts/download-frank-small.sh` now fetches the GGUF quant instead. The
`scripts/download-frank-secondary.sh` now fetches the GGUF quant instead. The
serve script refuses to start with a missing shard.
## Client contract
- **Alias:** `frank-small` (the `id` at `/v1/models`).
- **Alias:** `frank-secondary` (the `id` at `/v1/models`).
- **Reasoning:** extracted to **`reasoning_content`** (live-verified
2026-10-01 — this SGLang build's qwen3 parser uses the same field name
as llama.cpp; models.yml sets `reasoningContentField:
@@ -166,8 +166,8 @@ serve script refuses to start with a missing shard.
## Rollback
[[frank-small-llama-mimo-q5km]] is the live shape two steps forward; the
practical rollback today is [[frank-small-llama-mimo-q6k]] (payload at
[[frank-secondary-llama-mimo-q5km]] is the live shape two steps forward; the
practical rollback today is [[frank-secondary-llama-mimo-q6k]] (payload at
`/home/dank/models/MiMo-Q6K/`). This era's own rollback, the llama.cpp
Gemma4-12B QAT payload, was staged to
`/home/dank/.AGENTTRASH/20261002-retired-llm-payloads/Gemma4-12B-QAT/` on
@@ -177,4 +177,4 @@ Gemma4-12B QAT payload, was staged to
- Model: https://huggingface.co/letechlead/MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound
- SGLang: https://github.com/sgl-project/sglang
- Serve script: `scripts/serve-frank-small.sh`; tunables: `scripts/frank-small.env`
- Serve script: `scripts/serve-frank-secondary.sh`; tunables: `scripts/frank-secondary.env`
@@ -1,10 +1,10 @@
---
type: reference
title: frank-small on Dankclaw — vLLM, Gemma4-12B QAT w4a16 (compressed-tensors)
description: "The RETIRED frank-small runbook (2026-09-19, same-day revert; rollback target) — vLLM on Dankclaw's RTX 3090 serving google/gemma-4-12B-it-qat-w4a16-ct (Marlin int4 weights, bf16 KV, 262k window) with Google's bf16 MTP assistant, the audio-capable derived image, the no-nvidia-container-toolkit device mounts, measured throughput, and the drafter-Marlin bug that killed the w4a16 assistant. Replaced by [[frank-small-gemma4-12b]] on the same day."
title: frank-secondary on Dankclaw — vLLM, Gemma4-12B QAT w4a16 (compressed-tensors)
description: "The RETIRED frank-secondary runbook (2026-09-19, same-day revert; rollback target) — vLLM on Dankclaw's RTX 3090 serving google/gemma-4-12B-it-qat-w4a16-ct (Marlin int4 weights, bf16 KV, 262k window) with Google's bf16 MTP assistant, the audio-capable derived image, the no-nvidia-container-toolkit device mounts, measured throughput, and the drafter-Marlin bug that killed the w4a16 assistant. Replaced by [[frank-secondary-gemma4-12b]] on the same day."
tags:
- llm
- frank-small
- frank-secondary
- dankclaw
- vllm
- rtx3090
@@ -15,12 +15,12 @@ tags:
timestamp: 2026-09-19
---
# frank-small — vLLM on the RTX 3090 (Gemma4-12B QAT w4a16)
# frank-secondary — vLLM on the RTX 3090 (Gemma4-12B QAT w4a16)
> **RETIRED 2026-09-19 (same-day revert).** This vLLM deployment was cut
> back to [[frank-small-gemma4-12b]] (llama.cpp, now the ACTIVE runbook)
> back to [[frank-secondary-gemma4-12b]] (llama.cpp, now the ACTIVE runbook)
> the same day it landed — this page is the rollback target. Same port
> (7771), same alias (`frank-small`), same client contract surface — the
> (7771), same alias (`frank-secondary`), same client contract surface — the
> runtime, the model payload, the drafter and the service shape (root
> systemd unit → rootless Quadlet) all changed.
@@ -28,12 +28,12 @@ timestamp: 2026-09-19
| what | value |
|---|---|
| runtime | vLLM `vllm/vllm-openai` dev build `gemma-x86_64-cu129` (base `sha256:e24d3d8e…001e`), audio extra pip-installed into `localhost/vllm/vllm-openai:frank-small` (`8dad46d7…`) |
| unit | **rootless Quadlet** `dankclaw/frank-small/quadlet/frank-small.container` (root systemd unit retired 2026-09-19) |
| runtime | vLLM `vllm/vllm-openai` dev build `gemma-x86_64-cu129` (base `sha256:e24d3d8e…001e`), audio extra pip-installed into `localhost/vllm/vllm-openai:frank-secondary` (`8dad46d7…`) |
| unit | **rootless Quadlet** `dankclaw/frank-secondary/quadlet/frank-secondary.container` (root systemd unit retired 2026-09-19) |
| model | `google/gemma-4-12B-it-qat-w4a16-ct` — compressed-tensors w4a16 (int4 Marlin weights, 10,264,229,896 B) |
| model dir | `/home/dank/models/gemma4-12b-qat-w4a16-ct/` |
| drafter | `google/gemma-4-12B-it-assistant` **bf16** (845,719,296 B) at `/home/dank/models/gemma4-12b-assistant-bf16/` |
| port | 7771 → 8000 (alias `frank-small`; `frank-large` keeps 7770) |
| port | 7771 → 8000 (alias `frank-secondary`; `frank-large` keeps 7770) |
| window | 262,144; KV pool **289,647 tokens** bf16; **2 concurrent full-context sessions** |
| multimodal | native: text + image + **audio** (derived image carries `av`/`scipy`/`soundfile`/`mistral_common[audio]`) |
@@ -113,7 +113,7 @@ trade for the native w4a16 format + audio + CUDA graphs.)
## The image
`dankclaw/frank-small/Dockerfile` (repo) — 16 comment lines + `FROM
`dankclaw/frank-secondary/Dockerfile` (repo) — 16 comment lines + `FROM
localhost/vllm/vllm-openai:gemma4-unified` + `RUN pip install
--no-cache-dir av scipy soundfile "mistral_common[audio]"`. **No COPY
lines** — the earlier `triton-sm89-sitecustomize.py` hack is gone:
@@ -144,9 +144,9 @@ not a hardware mystery.
## Deployed config
Repo truth: `scripts/frank-small.env` + `scripts/serve-frank-small.sh`
Repo truth: `scripts/frank-secondary.env` + `scripts/serve-frank-secondary.sh`
(mounted into the container at `/scripts/`, which is the entrypoint) +
`dankclaw/frank-small/quadlet/frank-small.container`. Key values:
`dankclaw/frank-secondary/quadlet/frank-secondary.container`. Key values:
`KV_CACHE_DTYPE=auto` (resolves to bf16 — fp8_e5m2 is refused by
compressed-tensors checkpoints and fp8 is hardware-dead on sm_86),
`ENFORCE_EAGER=0` (CUDA graphs on), `max_model_len=262144`, MTP via
@@ -186,27 +186,27 @@ container while it loads.
- **`sm_89` in an error** — stale arch override; `TRITON_OVERRIDE_ARCH`
must be absent. 8.6 is correct.
- **Audio 400/500** — the container is running the *base* image, not the
derived `:frank-small` (missing `av`). Check the deployed image digest
derived `:frank-secondary` (missing `av`). Check the deployed image digest
is `8dad46d7…`.
- **CUDA not visible in container** — a `Device=` line or a lib mount
dropped out of the unit after an edit; the unit is the source of truth,
the host-side generated unit is not.
- Logs: `journalctl --user -u frank-small` (rootless Quadlet; `podman
- Logs: `journalctl --user -u frank-secondary` (rootless Quadlet; `podman
logs` covers only the current `--replace --rm` instance).
## Rollback
[[frank-small-gemma4-12b]] — the llama.cpp deployment. Its weights,
[[frank-secondary-gemma4-12b]] — the llama.cpp deployment. Its weights,
mmproj and MTP sidecar are still on the host under
`/home/dank/models/Gemma4-12B-QAT/`, and the retired root unit is at
`scripts/.AGENTTRASH/frank-small.service.llama.cpp`. Restoring it means
`scripts/.AGENTTRASH/frank-secondary.service.llama.cpp`. Restoring it means
reinstalling that unit and stopping the Quadlet (one owner per port).
## References
- `scripts/serve-frank-small.sh` — the actual exec line
- `dankclaw/frank-small/Dockerfile` — the audio-derived image
- `dankclaw/frank-small/quadlet/frank-small.container` — the unit
- [[frank-small-gemma4-12b]] — RETIRED predecessor (rollback)
- `scripts/serve-frank-secondary.sh` — the actual exec line
- `dankclaw/frank-secondary/Dockerfile` — the audio-derived image
- `dankclaw/frank-secondary/quadlet/frank-secondary.container` — the unit
- [[frank-secondary-gemma4-12b]] — RETIRED predecessor (rollback)
- [[halogen-flash-server]] — the sibling endpoint on the iGPU
- [[VLLM Overview]] / [[VLLM Index]] — runtime background
@@ -1,15 +0,0 @@
---
type: reference
title: "frank-small on Dankclaw — llama.cpp, Qwen3.8-27B GSQ-RCO IQ3_S"
description: "The RETIRED frank-small runbook (2026-09-16 → 09-17, rollback target) — mainline llama.cpp on Dankclaw's RTX 3090 serving ISTA-DASLab Qwen3.8-27B-GSQ-RCO at IQ3_S with the model's own in-file MTP head and the BF16 vision projector: identity, KV arithmetic, deployed flags, the MTP contract, the client contract, and what to check when it misbehaves."
tags:
- llm
- frank-small
- dankclaw
- llama.cpp
- rtx3090
- qwen35
- mtp
- vision
timestamp: 2026-09-16
---
@@ -6,7 +6,7 @@ tags:
- freetoken
- benchmark
- dankserver
- frank-small
- frank-secondary
- qwen3.6
timestamp: 2026-09-02
---
@@ -1,27 +1,27 @@
---
type: configuration
title: FreeToken frank-small Runbook
description: RETIRED 2026-09-15 - FreeToken v0.1.2 serving batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8 as frank-small on Dankserver (RTX 3090 era).
title: FreeToken frank-secondary Runbook
description: RETIRED 2026-09-15 - FreeToken v0.1.2 serving batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8 as frank-secondary on Dankserver (RTX 3090 era).
tags:
- freetoken
- dankserver
- frank-small
- frank-secondary
- qwen3.6
- moe
- fp8
timestamp: 2026-08-30
---
# FreeToken frank-small Runbook
# FreeToken frank-secondary Runbook
> **RETIRED 2026-09-15.** The RTX 3090 left Dankserver and the successor
> [[frank-medium-rocmfpx]] (charlie12345/ROCmFPX llama.cpp on 2x Radeon AI
> PRO R9700) took the :7770 endpoint and the port. This page is kept as the
> retirement record of the FreeToken/CUDA era; the unit was archived to
> `~/.AGENTTRASH/20260915-frank-small-retire/host-units/` and the repo files
> `~/.AGENTTRASH/20260915-frank-secondary-retire/host-units/` and the repo files
> to `scripts/.AGENTTRASH/`.
The `frank-small` service on Dankserver is served by
The `frank-secondary` service on Dankserver is served by
**FreeToken v0.1.2** — an edge-native MoE serving engine that keeps
expert weights in host RAM with an LRU GPU cache. Model history on this
runtime: official `Qwen/Qwen3.6-35B-A3B-FP8` (since 2026-09-02) →
@@ -134,8 +134,8 @@ modelopt block-fp8 remains the only FP8 path that loads.
## Sampling
**frank-small samples by default, and our config never said so.**
`scripts/serve-frank-small.sh` passes no sampling arguments at all — only
**frank-secondary samples by default, and our config never said so.**
`scripts/serve-frank-secondary.sh` passes no sampling arguments at all — only
`--model`, `--served-model-name`, `--host`, `--port`, `--moe-cache-size`,
`--num-tokens` and `--max-running-requests`. The behaviour comes from the
runtime's own default:
@@ -160,7 +160,7 @@ would be byte-identical.
### This is the opposite of frank-large
| | frank-small (FreeToken) | frank-large (halogen) |
| | frank-secondary (FreeToken) | frank-large (halogen) |
|---|---|---|
| Silent client (no `temperature`) | **samples** at temp 1.0 / top_k 20 / top_p 0.95 | **greedy** — no server-side sampler defaults |
| Deterministic across runs | No | Yes |
@@ -168,7 +168,7 @@ would be byte-identical.
A client that talks to both endpoints and sends no sampler gets
non-deterministic answers from :7770 on Dankserver and reproducible ones from
:7770 on Dankclaw. If a workflow needs determinism from frank-small it must
:7770 on Dankclaw. If a workflow needs determinism from frank-secondary it must
send `temperature: 0` explicitly, or the service must be relaunched with
`--sampling-defaults none` (framework defaults only).
@@ -188,9 +188,9 @@ send `temperature: 0` explicitly, or the service must be relaunched with
| File | Purpose |
|------|---------|
| `scripts/serve-frank-small.sh` | Serve script (launches `ft serve`) |
| `scripts/frank-small.env` | Tunables (model path, pool sizes) |
| `scripts/frank-small.service` | Systemd unit |
| `scripts/serve-frank-secondary.sh` | Serve script (launches `ft serve`) |
| `scripts/frank-secondary.env` | Tunables (model path, pool sizes) |
| `scripts/frank-secondary.service` | Systemd unit |
## Limitations
@@ -209,7 +209,7 @@ send `temperature: 0` explicitly, or the service must be relaunched with
- **`model` field is required** in `/v1/chat/completions` bodies —
FreeToken rejects requests without it with a 422 (`Field required`),
unlike llama.cpp's server which defaults to the loaded model. Send
`"model": "frank-small"`.
`"model": "frank-secondary"`.
- Load on this swap took well under the old llama.cpp numbers:
weights → experts (parallel build) → CUDA graph capture → Uvicorn
in ~2 minutes; the 2026-08-30 swap verified ~40 tok/s decode on a
@@ -23,7 +23,7 @@ timestamp: 2026-09-13
> shape the fleet asked for: two 200k-token conversations rather than the
> native-context pair. Deployed as a rootless Quadlet container,
> `dankclaw/frank-large/quadlet/frank-large.container`, port 7770. The RTX 3090
> it does not touch now belongs to [[frank-small-qwen38-gsq-rco]].
> it does not touch now belongs to [[frank-secondary-qwen38-gsq-rco]].
The engine is a from-scratch serving runtime for the Qwen3.8-Flash-Next
architecture on AMD's gfx1151 (Strix Halo) — the same silicon this host runs.
@@ -98,9 +98,9 @@ upstream's defaults:
| `HALOGEN_CTX` | `200192` | **set 2026-09-16**: exactly half the pool, so two conversations of the full window are always resident. Not a round 200,000 — pool/2 is. The model's native window is 262,144, so this is a real reduction in the per-conversation ceiling, and a client asking for more gets a 400 (never a silent truncation). The harness declares 200192 for `frank-large` |
| `HALOGEN_KV_POOL_POSITIONS` | `400384` | **retuned 2026-09-16** (from 401,408): 2 x 200,192. Reserves ~11.0 GiB of KV, confirmed by the boot ledger as `400384 positions, ~27.2 GiB (16.2 GiB of it for everything but the pool, at max_tok 24576)` against a 33.5 GiB device budget — **no clamp**. Slots still oversubscribe the pool for any real request (admission reserves prompt + `max_tokens`), so a near-full pair that is both generating is pool-bound. `HALOGEN_KV_POOL_FIT` guards the boot: read the ledger, not this column |
| `HALOGEN_KV_SLOTS` | `2` | **lowered from 3 on 2026-09-16** — one slot per half of the pool, and two is the fleet's shape now that Dankclaw serves two endpoints. Each slot is 111.0 MiB of O(1) state, so this hands ~111 MiB back as well |
| `HALOGEN_MAX_TOK` | `24576` | **halved from the 32768 default on 2026-09-16** for frank-small, its co-tenant on the same host. The arena's variable term goes `8.40 x 2 → 8.40 x 1.5`, handing ~4.2 GiB back at roughly a 4-5 % prefill cost — the same lever at 16,384 costs 8-9 %. The vendor's own notes call 24,576 the largest arena that allocated with headroom to spare, so it stays inside their tested range. 65536 still cannot boot. It sizes a per-call PREFILL CHUNK, not the prompt ceiling: a 200k conversation is prefilled in arena-sized pieces |
| `HALOGEN_MAX_TOK` | `24576` | **halved from the 32768 default on 2026-09-16** for frank-secondary, its co-tenant on the same host. The arena's variable term goes `8.40 x 2 → 8.40 x 1.5`, handing ~4.2 GiB back at roughly a 4-5 % prefill cost — the same lever at 16,384 costs 8-9 %. The vendor's own notes call 24,576 the largest arena that allocated with headroom to spare, so it stays inside their tested range. 65536 still cannot boot. It sizes a per-call PREFILL CHUNK, not the prompt ceiling: a 200k conversation is prefilled in arena-sized pieces |
| `HALOGEN_HOST_RESERVE_GIB` | `20` | **raised from 15 with the same retune**, because the arena shrank: `budget = 121.2 − 67.7 − 20 = 33.5 GiB` against a measured need of ~27.2, so the fit check kept ~6 GiB of margin and did not clamp. It is **not** an allocation: raising it cannot return memory to the host, only take the pool away (see the ceiling section below) |
| `HALOGEN_VISION_TOWER` | *unset* | **removed 2026-09-16.** The tower charges 1.97 GiB against the same device budget as the pool and the arena, and the fleet's image path moved to [[frank-small-qwen38-gsq-rco]] (llama.cpp with `--mmproj`). This server refuses image content parts, and `models.yml` declares `frank-large` text-only to match |
| `HALOGEN_VISION_TOWER` | *unset* | **removed 2026-09-16.** The tower charges 1.97 GiB against the same device budget as the pool and the arena, and the fleet's image path moved to [[frank-secondary-qwen38-gsq-rco]] (llama.cpp with `--mmproj`). This server refuses image content parts, and `models.yml` declares `frank-large` text-only to match |
| `HALOGEN_DOWNLOAD` | `peonist-ai/halogen-qwen3.8-flash-next` | first-start fetch |
| `HALOGEN_VERBOSE` | `1` | verbose engine logging — the fullest startup account, on permanently so an incident report needs no restart-into-debug |
@@ -374,7 +374,7 @@ tables above.
**None here since 2026-09-16.** `HALOGEN_VISION_TOWER` is deliberately unset:
the tower is a 1.97 GiB charge against the same device budget as the pool and
the arena, and the fleet's image path moved to [[frank-small-qwen38-gsq-rco]],
the arena, and the fleet's image path moved to [[frank-secondary-qwen38-gsq-rco]],
which loads the BF16 projector alongside its own weights. This server refuses
image content parts and says why, rather than ignoring them:
@@ -1,7 +1,7 @@
---
type: reference
title: ik_llama.cpp on Dankserver (Ornith Q8_0)
description: ikawrakow/ik_llama.cpp replaces upstream llama.cpp for frank-small on the RTX 3090. Q8_0 model, KV K default f16 / V q6_0 with Hadamard on V only (iq5_k is not a KV-cache type in this build; q8_KV crashes on this arch), split-mode none, speculative decoding disabled, 3 parallel slots x 163840 ctx, the Qwen-Sharp template rendered with --jinja, and explicit CPU-expert offload. Includes the mandatory CUDA/IQK FA_ALL_QUANTS build flags.
description: ikawrakow/ik_llama.cpp replaces upstream llama.cpp for frank-secondary on the RTX 3090. Q8_0 model, KV K default f16 / V q6_0 with Hadamard on V only (iq5_k is not a KV-cache type in this build; q8_KV crashes on this arch), split-mode none, speculative decoding disabled, 3 parallel slots x 163840 ctx, the Qwen-Sharp template rendered with --jinja, and explicit CPU-expert offload. Includes the mandatory CUDA/IQK FA_ALL_QUANTS build flags.
tags:
- ikllama
- llamacpp
@@ -16,7 +16,7 @@ timestamp: 2026-08-26
# ik_llama.cpp on Dankserver (Ornith-1.5-35B-A3B CRACK)
> **Active 2026-08-26.** `frank-small` switched from upstream
> **Active 2026-08-26.** `frank-secondary` switched from upstream
> llama.cpp (b10121) to `ikawrakow/ik_llama.cpp` (commit `b166e269`) on the
> RTX 3090. This page documents the build, the mandatory FA build flags, and
> the MoE / CPU-offload runtime arguments.
@@ -101,8 +101,8 @@ a hard compile error.
## Runtime configuration
Files: `scripts/serve-frank-small.sh`, `scripts/frank-small.env`,
`scripts/frank-small.service` in the repo. The service just calls
Files: `scripts/serve-frank-secondary.sh`, `scripts/frank-secondary.env`,
`scripts/frank-secondary.service` in the repo. The service just calls
the serve script; the env file holds all tunables.
### KV cache: K default f16 / V q6_0 + Hadamard (V only)
@@ -241,7 +241,7 @@ apply.
The previous runtime `upstream llama.cpp b10121` and its binary
(`/home/dank/llama.cpp/build/bin/llama-server`) are **left untouched**. To
rollback to the old stack, change `LLAMA_SERVER=` in
`scripts/frank-small.env` back to that path and
`scripts/frank-secondary.env` back to that path and
`CACHE_TYPE_K/V` back to `f16` with Hadamard off, and re-enable the ngram
spec / `--spec-type` flags. The model file is not changed either way.
+132 -71
View File
@@ -1,25 +1,27 @@
# LLM Fleet — Knowledge Base
Documentation for the LLM inference fleet on the two hosts (2026-10-05):
**Dankserver runs FreeToken (PR #587 ROCm) on frank-primary** (the local
image `frank-primary-freetoken:pr587-77a7201` built on the host from
`scripts/docker/frank-primary-freetoken.Dockerfile` — FreeToken at PR head
`77a7201` in the PR's own ROCm 7.14 base image, torch 2.11.0+rocm7.14 —
driving `podman run` directly, serving orcarouter
/Qwen3.8-Flash-Next-Uncensored-FP8 on ONE R9700, :7770: all experts in
pinned host RAM behind a GPU LRU (`--moe-cache-rate 0.10`), KV pool
`--num-tokens 299968` (multiple-of-64 law of the qsa_sparse backend), 2
decode slots, parsers AUTO (thinking → `reasoning_content` natively;
measured 17.1 tok/s decode, TP=2 exists but does not boot on ROCm — see the
runbook). Rollback tag frank-primary-vllm-shapeG-20261003),
and Dankclaw
runs two endpoints — **halogen container (frank-large, port 7770:
ghcr.io/peonist-ai/halogen @ pinned digest serving peonist-ai
/halogen-qwen3.8-27b, a 33.4 GiB .hgn checkpoint that loads in no other
engine, DFlash2 spec decode, ONE slot at 200,700 ctx, text-only — vision
lives on frank-small)** and
**llama.cpp CUDA (frank-small, port 7771: MiMo-V2.6-Distill-Qwen-9B Q6_K
GGUF, 2 x 200,704, fully unquantized f16 KV, on the RTX 3090)**.
**Dankserver runs the radiance engine (StillDeadcode) on frank-primary**
(digest-pinned `stilldeadcode/radiance` image, engine as entrypoint,
driving `podman run` directly, root systemd unit, :7770): the OFFICIAL
`Qwen/Qwen3.8-Flash-Next-FP8` checkpoint at NATIVE block-FP8 precision
(converted no-recipe to `qwen4exp-fp8-native.rad` — see the runbook for
the descriptor-staging law), on BOTH R9700s TP=2 (`--tp-wire wht6`),
experts in a 40 GiB/rank pinned host pool + VRAM cache (ratio 0.82), the
47.7 GiB PLE table pinned to host RAM, fp8 KV, MTP spec decode depth 3,
2 × 200,000 first boot. Rollback tags frank-primary-freetoken-pr587-20261005
and frank-primary-vllm-shapeG-20261003),
and Dankclaw runs two endpoints — **halogen-flash-server 0.16.2
(frank-large, port 7770: ghcr.io/peonist-ai/halogen-flash-server @ pinned
digest serving the Quat3rnion ABLITERATED Swift1.5 finetune of
Qwen3.8-Flash-Next v2, a ~62 GiB v2 .hgn that loads in no other engine,
MTP spec decode always-on (HALOGEN_SPEC_ADAPT=0), TWO slots at 200,192
ctx, vision ON)** and
**llama.cpp CUDA (frank-secondary, port 7771: ajgazin Swift-1.5-Qwen3.8-27B
Uncensored Dynamic-MTP RCO-IQ3_XXS GGUF — 27B (the MTP head in the file
IS drafted, draft-mtp depth 3), TWO 131,072 conversations at Frank's
128k cap (the tightest fit in the endpoint's history — 489 MiB free),
q8_0 K + q8_0 V (FP8 class), vision via the publisher's own BF16
mmproj, on the RTX 3090)**.
> 2026-08-22: the former `vllmScripts` repository is merged into the
> serverAppConfigs repository. This knowledge base moved from the old repo's
@@ -33,25 +35,25 @@ graph LR
subgraph Dankserver[AMD Dankserver - Ubuntu 26.04]
A[2x Radeon AI PRO R9700
gfx1201, 32 GiB each]
B2[FreeToken PR587 ROCm, local image
B2[radiance StillDeadcode, digest-pinned image
podman run, frank-primary, port 7770]
C[Qwen3.8-Flash-Next FP8
1x R9700, experts in pinned host RAM + GPU LRU 10%,
KV pool 299,968 tokens, 2 slots, parsers AUTO,
TP=2 untested on ROCm; second R9700 idle]
C[Qwen3.8-Flash-Next FP8 OFFICIAL, native block-FP8
2x R9700 TP=2 wht6, experts 40 GiB/rank pinned pool + VRAM,
PLE 47.7 GiB pinned RAM, fp8 KV 7.32 KiB/tok, MTP depth 3,
2 x 200000, froggeric v22.5]
end
subgraph Dankclaw[AMD Strix Halo - Ubuntu 26.04]
E2[RTX 3090 (PCIe x4 dock) +
gfx1151 iGPU 114 GiB GTT]
E[halogen container gfx1151, digest-pinned image
E[flash-server container gfx1151, digest-pinned image
frank-large, port 7770]
F[halogen-qwen3.8-27b .hgn p1w4d-d2 + DFlash2 drafter
1 x 200700 ctx, d2 KV, text-only]
F[flash-server v2 abliterated Swift1.5 .hgn + ngram table + vision tower
2 x 200192 ctx, MTP always-on, image input]
G[llama.cpp master CUDA sm_86
frank-small, port 7771]
H[MiMo-V2.6 Q5_K_M GGUF
2 x 200704 ctx, f16 K + f16 V, flash-attn, mmproj vision]
frank-secondary, port 7771]
H[Swift-1.5-27B Uncensored RCO-IQ3_XXS-mtp GGUF
2 x 131072 ctx, q8_0 K + q8_0 V, draft-mtp depth 3, mmproj vision]
E2 --> E --> F
E2 --> G --> H
end
@@ -70,33 +72,91 @@ frank-small, port 7771]
## Active Deployment
- [[frank-large-halogen27b-dflash2]] — **ACTIVE since 2026-10-05**
- [[frank-large-flash-server-v2-swift15]] — **ACTIVE since 2026-10-06**
(root systemd unit + podman, port 7770): the digest-pinned
`ghcr.io/peonist-ai/halogen` image serving `peonist-ai/halogen-qwen3.8-27b`
(`qwen3.8-27b-p1w4d-d2.hgn`, 33.4 GiB) with the DFlash2 drafter on ONE slot
of 200,700 ctx (slots>1 disables speculation — engine law). Publisher card
31.7 t/s mean across eight text workloads. Accepted tradeoffs: TEXT-ONLY
(the engine ignores the vision encoder; frank-small owns images) and the
CENSORED base model (the ROCmFPX AGENT file was Uncensored by construction).
No server-side sampler — the thinking recipe rides the wire in models.yml.
Rollback: revert the swap commit; the ROCmFPX payload + fork build +
`build-rocmfpx.sh` pin all remain on disk
- [[frank-small-llama-mimo-q6k-2x200704]] — **ACTIVE since 2026-10-03**
(root systemd unit, port 7771): llama.cpp master (CUDA sm_86) serving
bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K** on the RTX 3090, TWO
slots of 200,704 tokens (pool 401,408) with FULLY UNQUANTIZED KV (f16 K +
f16 V) and the mmproj for vision — 94.4 tok/s decode (the rung costs a
measured -10 % vs Q5_K_M: weight-bandwidth-bound), batch 4096 / ubatch 2048
since 2026-10-05 (+4.6 % prefill, measured), 2,279 MiB of the card
left free. Probe-size trap (>=256 px images or the model says "video")
documented. Rollback: one MODEL_FILE line back to Q5_K_M
- [[frank-small-llama-mimo-q5km]] — **RETIRED 2026-10-02 → 10-03 (rollback
`ghcr.io/peonist-ai/halogen-flash-server` 0.16.2 engine serving the
Quat3rnion ABLITERATED Swift1.5 finetune of peonist's
Qwen3.8-Flash-Next v2 checkpoint (`qwen38-flash-next-v2-swift15-abliterated.hgn`,
66.7 GB compacted r2 of the card's 89.8 GB append-only original,
sha256-verified) with the REQUIRED n-gram lookup table (51.2 GB,
@peonist 5cc17cea) and the vision tower (897.9 MB, @e053f488) beside
it. TWO slots of 200,192 ctx, MTP head ALWAYS ON (`HALOGEN_SPEC_ADAPT=0`
— the adaptive default turns the head off per request and halves
decode), server-side sampler at the card's 1.0/0.95/20, coupled
cap/timeout 65536/7200, prompt cache ON by default (the 27B engine's
manual `HALOGEN_CACHE_MB` pin era is over — warm 100k turn ≈ 2 s vs
≈ 88 s cold). Vision ON (image_url parts only). Card: 53-57 t/s
greedy, ~1,640 t/s prefill at 34k, ~72 % fewer thinking tokens.
Rollback: revert the swap commit; the 27B payload + fetch script stay
on disk
- [[frank-large-halogen27b-dflash2]] — **RETIRED 2026-10-05 → 10-06
(rollback target; payload + fetch script + image on disk)**: the
digest-pinned `ghcr.io/peonist-ai/halogen` 0.1.4 engine serving
`peonist-ai/halogen-qwen3.8-27b` (`qwen3.8-27b-p1w4d-d2.hgn`, 33.4 GiB)
— DFlash2 drafter, ONE 200,700 slot, text-only (the engine ignored the
vision encoder), censored base. Owns the measured lessons: the prompt
cache pin (`HALOGEN_CACHE_MB=16384` — auto was OFF at the 36.9 GB
two-entry floor; a 17.3k cold prefill of 27.0 s resumed in 1.7 s warm),
the FA tile rejection (`HALOGEN_FA_TQ=256`: prefill −9.5 %, decode
±0.3 %), and deep decode at 3.3-3.8 t/s@80k being structural. The
foreground-`podman run` ledger capture and the deep-role rerouting
(`slow`/`advisor` → frank-primary) carried forward. Superseded by the
MODEL swap: the abliterated v2 .hgn loads only in flash-server, so the
engine moved with the model
- [[frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688]] — **ACTIVE since
2026-10-05 (evening); UNCECERSED RCO-IQ3_XXS with MTP back on since
2026-10-06 evening; RENAMED from frank-small and reshaped to 2 ×
114,688 / ubatch ladder 512 → 1024 → **2048** late 2026-10-06** (root
systemd unit, port
7771): llama.cpp master
(CUDA sm_86) serving ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-
GGUF **RCO-IQ3_XXS-mtp** (27B; the MTP head in the file is DRAFTED —
draft-mtp depth 3, ngram skipped on the recurrent-rollback law) on the
RTX 3090, TWO 114,688-token conversations at Frank's 112k cap (pool
229376 / parallel 2 — cut from 131,072 to fund the ubatch rungs;
**1,005 MiB free at boot, 763 settled after a vision probe**) with
q8_0 K + q8_0 V (FP8 class, 34 KiB/token vs 64 at f16) and
vision through the publisher's own mmproj-BF16.gguf. Prefill 1,209
t/s @ 3,760; decode ~52–58 t/s recipe (ubatch-invariant; acceptance
noise 34–54 %).
RCO-IQ3_XXS tier is a per-tensor allocation mix of stock types; the
payload is the UNCECERSED Swift 1.5 (23/100 refusals vs Swift 1.5's
98/100). Replaces BOTH same-day payloads: the live MiMo Q6_K shape
(below) and the Ornith-1.5-9B Q8_0 swap that was committed
(77decb5..380aafa) but never deployed
- [[frank-secondary-llama-mimo-q6k-2x200704]] — **RETIRED 2026-10-05 (evening;
rollback target; payload on disk)**: llama.cpp master (CUDA sm_86)
serving bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K**, TWO slots of
200,704 with FULLY UNQUANTIZED KV (f16 K + f16 V) and the mmproj for
vision — 94.4 tok/s decode (the rung costs a measured -10 % vs Q5_K_M:
weight-bandwidth-bound), batch 4096 / ubatch 2048 since 2026-10-05
(+4.6 % prefill, measured), 2,279 MiB of the card left free. This was
the LIVE payload when Swift cut over. The ubatch A/B table and the
probe-size trap (>=256 px images or the model says "video") live inside
- [[frank-secondary-llama-mimo-q5km]] — **RETIRED 2026-10-02 → 10-03 (rollback
target; payload on disk; owns the measured boot-pair table)**: the same
runtime and the same 2 x 200,704 + f16 KV shape one quant rung lower —
104.8 tok/s, 3,571 MiB true free
- [[frank-primary-freetoken-pr587]] — **ACTIVE since 2026-10-05**
(root systemd unit, port 7770): FreeToken at PR #587 head `77a7201`
(branch rocm714_torch214) built on the host into the local image
- [[frank-primary-radiance-qwen38-native-fp8]] — **ACTIVE since 2026-10-05**
(root systemd unit, port 7770): the StillDeadcode **radiance** engine
(C++/HIP, libr4d gfx1201 kernels, digest-pinned 287 MiB image, engine as
the image entrypoint) serving the OFFICIAL `Qwen/Qwen3.8-Flash-Next-FP8`
checkpoint at NATIVE precision — converted no-recipe to
`qwen4exp-fp8-native.rad` (~161 GiB) after dropping `quantization_config`
in a staging dir (the plugin descriptor-selection law: nothing claims
`(qwen4exp, "fp8")`, the shipped plugin claims `(qwen4exp, "")`;
per-weight encodings are data, not the descriptor) — on BOTH R9700s
**TP=2** (`--tp-wire wht6` fused all-reduce), experts in a 40 GiB/rank
pinned host pool + VRAM cache (ratio 0.82 = the measured 99 %-hit point),
the 47.7 GiB PLE n-gram table PINNED to host RAM (`--ngram-placement
ram`), fp8 KV 7.32 KiB/token, MTP spec decode depth 3, 2 × 200,000 first
boot, froggeric v22.5 template, vision + tools + `reasoning_content` auto,
`/metrics` scraped. Rollback: git tag
`frank-primary-freetoken-pr587-20261005` (FreeToken image + orcarouter
payload on disk); older rollback `frank-primary-vllm-shapeG-20261003`
- [[frank-primary-freetoken-pr587]] — **RETIRED 2026-10-05 (same day;
rollback target)**: FreeToken at PR #587 head `77a7201` (branch
rocm714_torch214) built on the host into the local image
`frank-primary-freetoken:pr587-77a7201` (Dockerfile: digest-pinned
rocm/pytorch rocm7.14 torch-2.11.0 base, libhiprtc dev-symlink law,
curated --no-deps install per docs/install_amd.md), serving the SAME
@@ -105,8 +165,9 @@ frank-small, port 7771]
(multiple-of-64 law), 2 slots, parsers AUTO (thinking →
`reasoning_content`). Measured: boot ~2.3 min, decode 17.1 tok/s
single-stream, vision "Red" probe passes, native tool_calls; TP=2 does
not boot on ROCm (pynccl JIT) — second card idle. Rollback: git tag
frank-primary-vllm-shapeG-20261003
not boot on ROCm (pynccl JIT) — second card idle. Superseded by the
radiance swap for TP=2; the image + payload stay on disk until the
rollback window closes
- [[dankserver-qwen38-fp8-expert-offload-tuning]] — **RETIRED 2026-10-05
(rollback target; the vLLM trio + env lore live in the tag)**: the
tcclaviger RDNA4
@@ -208,10 +269,10 @@ frank-small, port 7771]
kingjones777/Qwen3.8-27B-ROCmFPX-Q8_0-AGENT-GGUF; kept as the record of the
runtime it replaced
- [[freetoken-frank-small]] — RETIRED 2026-09-15, when the RTX 3090 moved to
frank-large: the FreeToken v0.1.2 frank-small runbook (batsclamp
frank-large: the FreeToken v0.1.2 frank-secondary runbook (batsclamp
Huihui-Qwen3.6-35B-A3B-abliterated-FP8), kept as the retirement record. The
NAME was reused on 2026-09-16 by a different service on Dankclaw — see
[[frank-small-qwen38-gsq-rco]]
[[frank-secondary-qwen38-gsq-rco]]
- [[freetoken-benchmark-sweep]] — KV pool vs decode speed sweep
(bs1/bs3/bs4/bs6, 8k–700k KV) on Dankserver RTX 3090
- [[deployment]] — deploy and update process for both servers
@@ -280,35 +341,35 @@ frank-small, port 7771]
.hgn + quality overlay) on the gfx1151 iGPU — two 200k conversations over one
400,384-position pool, deployed flags, memory model, client contract,
troubleshooting (2026-09-16 → 09-19)
- [[frank-small-llama-mimo-q6k]] — **RETIRED 2026-10-01 → 10-02** (the same
- [[frank-secondary-llama-mimo-q6k]] — **RETIRED 2026-10-01 → 10-02** (the same
rung returned ACTIVE 2026-10-03 at the 2 x 200,704 f16-KV shape —
[[frank-small-llama-mimo-q6k-2x200704]]): llama.cpp (master, CUDA sm_86
[[frank-secondary-llama-mimo-q6k-2x200704]]): llama.cpp (master, CUDA sm_86
build) serving
bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K** on the RTX 3090 — TWO
native 262,144-token sessions with ASYMMETRIC KV (K f16 unquantized + V
q8_0, which forces flash-attn), ~20.9/24.6 GiB measured, text-only (the
GGUF has no vision), reasoning_content, no spec decode; payload still at
`/home/dank/models/MiMo-Q6K/`
- [[frank-small-sglang-mimo-v26]] — RETIRED 2026-10-01 (hours; replaced
by [[frank-small-llama-mimo-q6k]] for the asymmetric KV SGLang cannot
- [[frank-secondary-sglang-mimo-v26]] — RETIRED 2026-10-01 (hours; replaced
by [[frank-secondary-llama-mimo-q6k]] for the asymmetric KV SGLang cannot
express; venv still on disk, INT8 payload staged to
`~dank/.AGENTTRASH/20261002-retired-llm-payloads/`): SGLang in a
self-contained python3.12 venv serving leteschlead MiMo-V2.6 INT8
W8A16 AutoRound, 2 x 262144 ctx, fp8 KV, in-checkpoint vision,
83.9 tok/s decode
- [[frank-small-gemma4-12b]] — RETIRED 2026-09-17 → 2026-10-01 (replaced by
[[frank-small-sglang-mimo-v26]] → the llama.cpp MiMo shape; payload staged
- [[frank-secondary-gemma4-12b]] — RETIRED 2026-09-17 → 2026-10-01 (replaced by
[[frank-secondary-sglang-mimo-v26]] → the llama.cpp MiMo shape; payload staged
to `~dank/.AGENTTRASH/20261002-retired-llm-payloads/Gemma4-12B-QAT/`):
mainline llama.cpp serving
HauhauCS/Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced Q4_K_M on the RTX
3090, with the separate MTP assistant sidecar chained behind ngram-mod,
q8_0 KV, and `--fit` sizing the full 262k window into ~12 GiB.
- [[frank-small-vllm-gemma4]] — RETIRED 2026-09-19 (same-day revert,
- [[frank-secondary-vllm-gemma4]] — RETIRED 2026-09-19 (same-day revert,
rollback target): vLLM serving google/gemma-4-12B-it-qat-w4a16-ct (w4a16
Marlin) with the bf16 MTP assistant on the RTX 3090 — bf16 KV, 289,647-
token pool, 2 concurrent full-context sessions, native text+image+audio.
- [[frank-small-qwen38-gsq-rco]] — RETIRED 2026-09-17 (rollback target): the
prior frank-small runbook (2026-09-16 → 09-17), ISTA-DASLab
- [[frank-secondary-qwen38-gsq-rco]] — RETIRED 2026-09-17 (rollback target): the
prior frank-secondary runbook (2026-09-16 → 09-17), ISTA-DASLab
Qwen3.8-27B-GSQ-RCO IQ3_S with the model's own in-file MTP head and
f16 KV at 131k
- [[x4-pair-experiment]] — MEASURED 2026-09-16: what mainline llama.cpp can and
@@ -339,9 +400,9 @@ frank-small, port 7771]
- [[eemin-rocmi4-mtp]] — the former caf-eemin Qwen3.8-27B embedded-MTP model
on Dankclaw (retired)
- [[dealign-ornith-crack]] — the retired Ornith-1.5-35B-A3B CRACK model on
Dankserver (frank-small, retired 2026-09-02)
Dankserver (frank-secondary, retired 2026-09-02)
- [[ikllama-ornith-dankserver]] — the retired ik_llama.cpp runtime for
frank-small (2026-08-26): q8_0 + Hadamard KV cache,
frank-secondary (2026-08-26): q8_0 + Hadamard KV cache,
split-mode none (single-GPU), mandatory FA_ALL_QUANTS build flags
- [[kingjones-strix-mtp]] — the former Q6 AGENT runbook (restored 2026-09-01
after Q8_0 failure, then superseded by the STRIX deployment on 2026-08-27)
@@ -351,7 +412,7 @@ frank-small, port 7771]
(deployed and reverted 2026-08-22)
- [[Dankserver System Specs]] — the 2x R9700 box (frank-primary)
- [[Dankclaw System Specs]] — the Strix Halo gfx1151 box (frank-large on
the unsloth b11030 runtime since 2026-09-21, frank-small on the RTX 3090)
the flash-server v2 runtime since 2026-10-06, frank-secondary on the RTX 3090)
- [[frank-npu]] — RETIRED 2026-09-11: the FLM NPU sight+sound endpoint
on Dankclaw (Gemma4 E4B vision+audio pack, XDNA2, port 7771), kept as
the retirement record — the IOMMU/SVA err=-19 root cause and where
@@ -375,7 +436,7 @@ frank-small, port 7771]
- [[VLLM Overview]] — installation, CLI, API, architecture, quantization, parallelism
- [[VLLM Index]] — quick reference with commands, endpoints, and parameter table
- [[Running Qwen3.6 35B A3B FP8 on Dankserver]] — Qwen3.6-35B-A3B hardware
notes and sizing (this architecture family is what the retired frank-small
notes and sizing (this architecture family is what the retired frank-secondary
served in the Huihui abliterated FP8 variant — see
[[freetoken-frank-small]]; the Ornith-1.5-35B-A3B CRACK checkpoint
that preceded it is archived at [[dealign-ornith-crack]])
@@ -204,4 +204,4 @@ clipping at depth) starts with sections 1–3 here before touching anything.
- [[RDNA4 MoE Expert Offload VRAM Budget]] — the plan arithmetic behind OOMs
- [[dankserver-qwen38-fp8-expert-offload-tuning]] — deployed shape and history
- [[frank-large-benchmark]] and [[frank-small-benchmark]] — probe/ladder hygiene on live hosts
- [[frank-large-benchmark]] and [[frank-secondary-benchmark]] — probe/ladder hygiene on live hosts
@@ -249,13 +249,13 @@ distinguishes a real split from a mislabelled sweep.
The fleet's answer is unchanged — **two independent endpoints**
([[halogen-flash-server]] on the iGPU via halogen,
[[frank-small-qwen38-gsq-rco]] holding a whole 27B in the 3090's 24 GB) — but two
[[frank-secondary-qwen38-gsq-rco]] holding a whole 27B in the 3090's 24 GB) — but two
claims from the first round of this experiment are now corrected:
1. **Cross-vendor layer split is possible** (measured: CUDA0 + Vulkan1, 27.23
tok/s and 268.5 t/s prefill at 64k). It is not a regime change (+7 % decode),
and it costs the 3090 ~15 GiB — which this fleet spends on the vision endpoint
(`frank-small` holds 22.4 of the card's 24.6 GiB). An operator who wants the
(`frank-secondary` holds 22.4 of the card's 24.6 GiB). An operator who wants the
7 % must give up the fleet's only image-capable endpoint.
2. **Per-tensor placement across two devices is impossible while `-ot` is used**,
for a scheduler-mechanical reason, regardless of driver or vendor mix.
@@ -309,8 +309,8 @@ Operational notes for the next run:
`GGML_CUDA_NO_PINNED_WEIGHTS`. Without it a CPU-overridden tensor can land in a
pinned `CUDA_Host` buffer — the 2026-09-15 108 GiB page-lock that starved this
host's co-tenants.
- `systemctl mask` is **refused** for a relinked root unit: frank-small's
`/etc/systemd/system/frank-small.service` is a symlink into the repo, so
- `systemctl mask` is **refused** for a relinked root unit: frank-secondary's
`/etc/systemd/system/frank-secondary.service` is a symlink into the repo, so
`mask` exits with "File ... already exists". Stop it and rely on the absence
of a restart trigger, or move the symlink aside.
- Restarting frank-large needs the quadlet source back in
@@ -217,7 +217,7 @@ done
```
Ground truth (verify, don't assume — IPs can move): 192.168.0.143 = dankclaw
= AI stack + `frank-large` :7770 + `frank-small` :7771 (RTX 3090 + Strix
= AI stack + `frank-large` :7770 + `frank-secondary` :7771 (RTX 3090 + Strix
Halo iGPU); 192.168.0.235 = dankserver = media stack + `frank-primary`
:7770 (2× R9700). Repo dirs + live hostnames agree; the deploy tooling is
hostname-anchored (`ssh dankclaw`/`ssh dankserver` via `~/.ssh/config`), so
@@ -12,7 +12,7 @@ timestamp: 2026-10-05T00:00:00-07:00
Use when `:7770`/`:7771` gives **connection refused** (not a timeout) and the
service was not crashed. Applies to `frank-medium` (dankserver, root unit),
`frank-small` (dankclaw, root unit), `frank-large` (dankclaw, rootless
`frank-secondary` (dankclaw, root unit), `frank-large` (dankclaw, rootless
Quadlet).
**1. Classify the stop before touching anything**
@@ -103,7 +103,7 @@ the sudo journal names who and when: `journalctl -b | grep sudo`.
## Scratch vLLM boot on a fleet GPU host — stop-first law
Any scratch/experimental vLLM container on a host that also runs a production
LLM service (`frank-primary`, `frank-large`, `frank-small`) MUST stop the
LLM service (`frank-primary`, `frank-large`, `frank-secondary`) MUST stop the
production unit first, and the window script must verify it. Two independent
failure modes if skipped:
@@ -64,7 +64,7 @@ done
Then **read every hit**; do not count them. Most are benign (comments,
retired fetch-script destinations, `retire_*_quadlet` helpers that move a
quadlet not a model) — but on Dankclaw this check found a live bug:
`serve-frank-small.sh`'s `: "${MODEL_DIR:=...}"` fallback still pointed at a
`serve-frank-secondary.sh`'s `: "${MODEL_DIR:=...}"` fallback still pointed at a
payload retired a week earlier. The env file always overrode it, so it was
invisible; a missing env file would have produced a "missing asset" naming a
model that no longer exists.
@@ -99,7 +99,7 @@ active).
## 5. Verify, then report — and do not purge
```sh
systemctl is-active frank-large frank-small
systemctl is-active frank-large frank-secondary
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:7770/health
```
@@ -191,7 +191,7 @@ Ports 8880–8883 avoid the fleet's LLM ports (7770/7771).
licenses (IndexTTS, Fish S2, XTTS v2). For a personal homelab voice, MIT /
Apache-2.0 (Chatterbox, Qwen3-TTS, GLM-TTS, VoxCPM2) is the safe set.
- **Co-locate on the 3090** with a low `--gpu-memory-utilization` (~0.35) so the
TTS engine shares the card with frank-small; move to the R9700s only if TTS
TTS engine shares the card with frank-secondary; move to the R9700s only if TTS
throughput demand grows.
- **Guard the endpoint like any other service:** if vhosted, it goes behind the
Anubis → Authentik chain; non-browser clients (OMP's `curl`) must target the
@@ -107,7 +107,7 @@ TTS), [Kokoro-82M](https://github.com/hexgrad/kokoro) (8,933, Apache-2.0, TTS),
the `Comfy-Org` org. Already running on this fleet (Qwen-Image 2.1 GGUF) and
wired as the Open WebUI image engine.
- **llama.cpp** — the reference GGUF inference engine; `llama-server` exposes an
OpenAI-compatible endpoint. Backbone of this fleet's frank-small/frank-large.
OpenAI-compatible endpoint. Backbone of this fleet's frank-secondary/frank-large.
- **vLLM** — high-throughput PagedAttention serving with an OpenAI-compatible
API; the multi-GPU option (frank-medium runs vLLM tensor-parallel across two
R9700s).
@@ -1,6 +1,6 @@
---
type: reference
description: "Consolidated research from the `frank-small` research subagents (2026-08-14) plus the pi agent knowledge base."
description: "Consolidated research from the `frank-secondary` research subagents (2026-08-14) plus the pi agent knowledge base."
tags:
- research
timestamp: 2026-09-03
@@ -8,7 +8,7 @@ timestamp: 2026-09-03
# Service Research Brief
Consolidated research from the `frank-small` research subagents (2026-08-14) plus the pi agent knowledge base. This is the source material behind the per-service docs pages.
Consolidated research from the `frank-secondary` research subagents (2026-08-14) plus the pi agent knowledge base. This is the source material behind the per-service docs pages.
## Method
@@ -227,7 +227,7 @@
<div class="app">
<div class="app-head"><h3>llama.cpp <span class="fit exc">EXCELLENT</span></h3><span class="tag"><span class="st">129,149★</span> · MIT · LLM runtime</span></div>
<p>The reference GGUF inference engine; <code>llama-server</code> exposes an OpenAI-compatible endpoint. Backbone of this fleet's frank-small/frank-large endpoints. Runs on CPU, CUDA, ROCm, Vulkan, Metal.</p>
<p>The reference GGUF inference engine; <code>llama-server</code> exposes an OpenAI-compatible endpoint. Backbone of this fleet's frank-secondary/frank-large endpoints. Runs on CPU, CUDA, ROCm, Vulkan, Metal.</p>
</div>
<div class="app">
+44 -22
View File
@@ -70,6 +70,7 @@ macOS mini) as quadlets inside its podman machine — not on Dankserver.
- [[Gluetun]] — the VPN tunnel container
- [[Valkey]] — SearXNG cache and limiter store
- [[ComfyUI]] — node-based AI image generation (ROCm GPU, gfx1151 build)
- [[ComfyUI Qwen-Image Workflows]] — the two proven Qwen-Image 2.1 workflows + what to add next (LoRAs, Edit, ControlNet, Wan video)
- [[OpenWebUI]] — chat UI for local LLMs
- [[Hermes]] — the Nous Research agent
- [[Hermes-Dashboard]] — the Hermes web management UI (config, keys, sessions)
@@ -181,11 +182,20 @@ The LLM inference fleet on the two hosts. Full index:
### Deployment and Topology
- [[deployment]] — unified deploy and update process for both servers
- [[frank-primary-freetoken-pr587]] — the ACTIVE frank-primary runbook
(2026-10-05): FreeToken built at PR #587 head `77a7201` on the host into
- [[frank-primary-radiance-qwen38-native-fp8]] — the ACTIVE frank-primary
runbook (2026-10-05): the StillDeadcode radiance engine (digest-pinned
287 MiB image, libr4d gfx1201 kernels) serving the OFFICIAL
`Qwen/Qwen3.8-Flash-Next-FP8` at native block-FP8 (no-recipe convert
after the descriptor-staging drop) on BOTH R9700s TP=2 `wht6`, experts
in a 40 GiB/rank pinned host pool, PLE 47.7 GiB pinned to RAM, fp8 KV
7.32 KiB/token, MTP depth 3, 2 × 200,000 first boot; rollback is the
FreeToken one-card era (tag `frank-primary-freetoken-pr587-20261005`),
then the tcclaviger shape-G era (tag `frank-primary-vllm-shapeG-20261003`)
- [[frank-primary-freetoken-pr587]] — RETIRED 2026-10-05 (superseded the
same day by the radiance cutover above; kept as the rollback runbook):
FreeToken built at PR #587 head `77a7201` on the host into
`frank-primary-freetoken:pr587-77a7201`, ONE R9700 with experts in pinned
host RAM behind a 0.10 GPU LRU, 2 slots, parsers AUTO; rollback is the
tcclaviger shape-G era (tag `frank-primary-vllm-shapeG-20261003`)
host RAM behind a 0.10 GPU LRU, 2 slots, parsers AUTO
- [[frank-redo-2026-08-17]] — the 2026-08-17 topology change
- [[frank-primary-r9v-radiance-qwen38-q8]] — RETIRED 2026-09-30 (two swaps
back; rollback target): the R9V radiance-vLLM fork (docker-pinned bundle
@@ -284,6 +294,9 @@ The LLM inference fleet on the two hosts. Full index:
disables speculation — engine law); TEXT-ONLY and censored base, both
accepted tradeoffs; no server-side sampler, so the thinking recipe rides
the wire
- [[frank-large-flash-server-v2-swift15]] — the ACTIVE frank-large runbook
since 2026-10-06 (supersedes halogen 0.1.4): halogen-flash-server 0.16.2
serving the abliterated Swift1.5 Qwen3.8-Flash-Next v2 (.hgn)
- [[frank-large-rocmfpx-q6agent-uncensored-mtp]] — RETIRED 2026-10-03 → 10-05
(rollback target; payload, fork build and build script on disk): the pinned
kingjones30/ROCmFPX fork (scripts/build-rocmfpx.sh) serving kingjones777/
@@ -359,40 +372,49 @@ The LLM inference fleet on the two hosts. Full index:
- [[ciru-activefpx-promptforge]] — the RETIRED v3 CIRU PromptForge runtime on Dankclaw (superseded 2026-08-24)
- [[kingjones-strix-mtp]] — the kingjones Qwen3.8-27B ROCmFP4 STRIX MTP model on Dankclaw (active)
- [[dealign-ornith-crack]] — the retired Ornith-1.5-35B-A3B CRACK model on Dankserver (retired 2026-09-02)
- [[frank-small-llama-mimo-q6k-2x200704]] — the ACTIVE frank-small runbook
since 2026-10-03: llama.cpp master (CUDA sm_86) serving
bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K** on Dankclaw's RTX 3090,
port 7771 — TWO conversations at 200,704 tokens each with fully
unquantized f16 KV and mmproj vision; 94.4 tok/s decode (the Q6_K rung
costs a measured −10 % vs Q5_K_M — weight-bandwidth), batch 4096 / ubatch
2048 (+4.6 % prefill, measured 2026-10-05), 2,279 MiB free, and
the ≥256 px image-probe trap (tiny images answer "video")
- [[frank-small-llama-mimo-q5km]] — RETIRED 2026-10-02 → 10-03 (rollback
- [[frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688]] — the ACTIVE
frank-secondary runbook since 2026-10-05 (evening; **UNCECERSED RCO-IQ3_XXS
with MTP back on since 2026-10-06 evening; renamed from frank-small and
reshaped to TWO 114,688-token conversations at Frank's 112k cap with
the ubatch ladder climbed to 2048 late 2026-10-06**): llama.cpp master (CUDA
sm_86) serving ajgazin's Swift-1.5-Qwen3.8-27B Uncensored RCO-IQ3_XXS
(the MTP head in the file is drafted) on the RTX 3090 — 1,005 MiB free
at boot (763 settled after a vision probe), q8_0 K + q8_0 V, vision
through the publisher's own projector, ~52–58 t/s recipe decode with MTP
- [[frank-secondary-llama-mimo-q6k-2x200704]] — RETIRED 2026-10-05 (evening,
superseded by swift15): llama.cpp master serving
bartowski/MiMo-V2.6-Distill-Qwen-9B-GGUF **Q6_K** — TWO conversations at
200,704 tokens each with fully unquantized f16 KV and mmproj vision;
94.4 tok/s decode (the Q6_K rung costs a measured −10 % vs Q5_K_M —
weight-bandwidth), batch 4096 / ubatch 2048 (+4.6 % prefill, measured
2026-10-05), 2,279 MiB free, and the ≥256 px image-probe trap (tiny
images answer "video")
- [[frank-secondary-llama-mimo-q5km]] — RETIRED 2026-10-02 → 10-03 (rollback
target, payload on disk; owns the measured boot-pair table): same runtime
and shape one rung lower — 104.8 tok/s, 3,571 MiB true free
- [[frank-small-llama-mimo-q6k]] — RETIRED 2026-10-01 (same day; that same
- [[frank-secondary-llama-mimo-q6k]] — RETIRED 2026-10-01 (same day; that same
Q6_K rung returned ACTIVE 2026-10-03 at the new shape, see
[[frank-small-llama-mimo-q6k-2x200704]]): the first llama.cpp + GGUF shape
[[frank-secondary-llama-mimo-q6k-2x200704]]): the first llama.cpp + GGUF shape
for this endpoint, Q6_K at 2 x 262,144 / f16-K + q8_0-V, 20.9 GiB measured
- [[frank-small-sglang-mimo-v26]] — RETIRED 2026-10-01 (rollback target):
- [[frank-secondary-sglang-mimo-v26]] — RETIRED 2026-10-01 (rollback target):
SGLang in a self-contained python3.12 venv serving
letechlead/MiMo-V2.6-Distill-Qwen-9B-INT8-W8A16-AutoRound (qwen3_5
hybrid, INT8 W8A16, in-checkpoint vision, NO MTP weights shipped) on
Dankclaw's RTX 3090, port 7771 — fp8 KV, pinned 606,208-token pool,
2 x 262,144 slots, bf16 mamba states, eager prefill
- [[frank-small-gemma4-12b]] — RETIRED 2026-10-01 (rollback target):
- [[frank-secondary-gemma4-12b]] — RETIRED 2026-10-01 (rollback target):
mainline llama.cpp serving
HauhauCS/Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced at Q4_K_M
(MTP assistant sidecar + ngram-mod, q8_0 KV) on
Dankclaw's RTX 3090, port 7771
- [[frank-small-vllm-gemma4]] — RETIRED 2026-09-19 (same-day revert,
- [[frank-secondary-vllm-gemma4]] — RETIRED 2026-09-19 (same-day revert,
rollback target): vLLM serving google/gemma-4-12B-it-qat-w4a16-ct (w4a16
Marlin) with the bf16 MTP assistant on the RTX 3090 — bf16 KV, 289,647-
token pool, 2 concurrent full-context sessions, native text+image+audio
- [[frank-small-qwen38-gsq-rco]] — RETIRED 2026-09-17 (rollback target):
the prior frank-small runbook (2026-09-16 → 09-17), ISTA-DASLab
- [[frank-secondary-qwen38-gsq-rco]] — RETIRED 2026-09-17 (rollback target):
the prior frank-secondary runbook (2026-09-16 → 09-17), ISTA-DASLab
Qwen3.8-27B-GSQ-RCO IQ3_S with own in-file MTP head + BF16 projector
- [[ikllama-ornith-dankserver]] — the retired ik_llama.cpp runtime for the old frank-small (q8_0 + Hadamard KV cache, split-mode none; 2026-08-26)
- [[ikllama-ornith-dankserver]] — the retired ik_llama.cpp runtime for the old frank-secondary (q8_0 + Hadamard KV cache, split-mode none; 2026-08-26)
- [[freetoken-frank-small]] — RETIRED 2026-09-15: the FreeToken/CUDA frank-small runbook, retired with the RTX 3090
- [[kairic-edge]] — the retired Kairic Edge IU4 experiment (2026-08-22)
- [[dflash2-large-vulkan]] — the retired DFlash2 Vulkan deployment (2026-08-21)
@@ -412,7 +434,7 @@ The LLM inference fleet on the two hosts. Full index:
- [[frank-large-benchmark]] — backend/spec benchmark behind the old head
- [[ubatch-benchmark-large]] — active-model llama-bench ubatch sweep plan and results
- [[frank-small-benchmark]] — the small-model benchmark suite
- [[frank-secondary-benchmark]] — the small-model benchmark suite
- [[frank-medium-batch-ubatch]] — the 2026-09-16 batch/ubatch sweep on the 2x
R9700 layer split: prefill PEAKS at ubatch 512 and falls on both sides, the
logical batch buys nothing measurable, decode never moves, and the ubatch's
+103 -27
View File
@@ -1,6 +1,82 @@
# Bundle Change Log
## 2026-10-06
- **frank-secondary (Dankclaw): ubatch 2048 — the second rung (llama.cpp,
RTX 3090).** "set ubatch to 2k" → commit d8da7a9: `UBATCH_SIZE`
1024 → 2048 at the 2×114,688 shape. **Cost 746 MiB**, not the 616 MiB
the same rung measured on the MiMo shape on 2026-10-05 (a rung's price
is shape-dependent; the 616 figure was not transferable). Boot:
**23,120 used / 1,005 MiB free**; a 512×512 vision probe then took
~240 MiB **and kept it** — settled **763 MiB free**, the encoder's
buffers staying resident across requests on this build, unlike the
evening boot where the 34 MiB transient returned. Decode 52.0 t/s on
the shared prompt (58.2 at the 1024 rung) with acceptance 34.2 % vs
42.8 %: ubatch does not move decode, so the gap is temp-1.0
acceptance noise, not a regression. **Prefill 1,209 t/s @ 3,760
tokens** — the first prefill number this endpoint has recorded, and
now the baseline future rungs compare against. `BATCH_SIZE` stays
4096, so the seam's batch ≥ ubatch guard holds; **2048 is the ceiling
for this shape** (4096 = no micro-split doubles the buffer growth
again against 0.7–1.0 GiB of headroom). e2e gate 33/33 green.
- **frank-secondary (Dankclaw): RENAMED from frank-small, then reshaped
to 2×114,688 / ubatch 1024 (llama.cpp, RTX 3090).** The rename (commit
87721ab, 87 files: serve trio, unit, bats seam, 9 runbooks, identifiers
`FRANK_SECONDARY_*`/`llm_secondary`/`deploy_llm_secondary`; historical
refs to the OLD FreeToken/CUDA frank-small and to the Dankserver
service that became frank-medium stay frank-small, as does
`freetoken-frank-small.md`). Live unit transition by hand before the
deploy: `systemctl stop frank-small` + `systemctl disable frank-small`
(a relinked unit — disable removes only its stale symlink) →
`deploy.sh dankclaw --service frank-secondary`. The reshape (commit
f2575a1, Frank's order "lower max context to 112k so we can increase
ubatch to at least 1k"): `CTX_SIZE 262144→229376` (per-slot
131,072→114,688), `UBATCH_SIZE 512→1024` → **22,374 used / 1,751 MiB
free** (the per-slot cut frees ~1.05 GiB of KV; ubatch 1024 spends
part, netting ~1.25 GiB back over the 489 MiB fit), decode 58.2 t/s
recipe / 66.9 temp 0 (acceptance 42.8–54.4 % — the drop vs the prior
boot's 63.3/72.9 is confounded by prompt text, not a shape verdict).
Harness retargeted: models.yml (id/name/compactionModels,
`contextWindow: 114688`) + config.yml modelRoles ×14. Runbook renamed:
`serverAppConfigs-docs/LLM/frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md`.
- **frank-secondary (Dankclaw): two Swift-1.5-27B reshapes (llama.cpp, RTX 3090).**
Morning: Frank's order "drop mtp, skip ngram, ubatch 512, two 128k
contexts" → 2×131,072 no-spec on the IQ3_S-mtp (commit 812d9eb):
22,728 used / 1,397 MiB free (the tight fit he ordered; the drafter's
~0.4 GiB + ubatch ~0.3 GiB bought the second 4,352 MiB KV slot), decode
48.0 t/s, vision `Red`. Evening: swap to
`ajgazin/Swift-1.5-Qwen3.8-27B-Uncensored-Dynamic-MTP-GGUF`
RCO-IQ3_XXS-mtp (10,442,827,392 B, sha256 `e13d3e74…`; 1.56 GiB lighter)
+ the publisher's own `mmproj-BF16.gguf`, MTP back on (draft-mtp depth
3) → commit da36fa3: **23,636 used / 489 MiB free** (the tightest fit in
the endpoint's history — the draft ctx measured ~2.4 GiB, built against
the target's full 262,144 context, not the 3-token proposal depth; the
lighter quant funds it), decode 63.3 t/s recipe / 72.9 temp 0 (acceptance
49.8–62.4 % vs 48.0 no-spec), vision `Red` at a 128-token budget (the
uncensored model thinks more — 64 tokens is all `reasoning_content`).
ngram-mod still skipped (recurrent-state rollback law on the qwen35
hybrid). Runbook renamed + rewritten:
`serverAppConfigs-docs/LLM/frank-secondary-llama-swift15-unc-iq3xxs-mtp-114688.md`.
- **frank-large (Dankclaw): halogen 27B-dense → halogen-flash-server
0.16.2 v2 Swift1.5 ABLITERATED (Strix Halo iGPU, port 7770).** Cutover
commit 31b1ec3 (03:07:40): the v2 `.hgn` payload loads ONLY in the
flash-server line, so the engine moved with the model. Abliterated base
(refusals gone), vision back (the v2 tower loads beside the checkpoint),
MTP head inside the checkpoint, 2×200,192 slots, `HALOGEN_SPEC_ADAPT=0`.
Runbook: `serverAppConfigs-docs/LLM/frank-large-flash-server-v2-swift15.md`.
**Incident (03:09:39–03:11:42):** the deploy started the unit while host
`MemAvailable` was ~74.7 GiB, 3.34 GiB short of the engine's pin-gate
requirement (62.11 GiB checkpoint + 16 GiB floor = 78.11 GiB); it
crash-looped 8 boots, each dying in ~5 s at the pin gate, and
self-resolved in ~2 min when the dying 27B engine freed RAM (boot #9
pinned clean at 88.4 GiB available). **Latent risk + fix:** the host was
left with only ~1.8 GiB `MemAvailable`, so a restart would crash-loop
again; `serve-frank-large.sh` now has a host-RAM preflight that checks
`MemAvailable` against the pin gate BEFORE `podman run` and WAITS up to
`FRANK_LARGE_RAM_WAIT` (600 s) for the RAM to clear, refusing loudly if
it never does (the 16 GiB floor is the engine's, not
`HALOGEN_HOST_RESERVE_GIB`).
## 2026-10-05
- **frank-primary: tcclaviger vLLM fork → FreeToken (PR #587 ROCm) on
dankserver.** Built FreeToken at PR head `77a7201` on the host into the
@@ -81,12 +157,12 @@
Live pass of the frankbook walk itself is deferred to the next deploy with
an awake host.
- **Catch while verifying: the 17:47 Ornith-1.5-9B swap (`77decb5`) had
left `frank_small_unit.bats` and the deployer's payload gate stale** —
left `frank_secondary_unit.bats` and the deployer's payload gate stale** —
the seam still expected MiMo-Q6K names and `deploy-remote.sh` gated
`LLM_SMALL_MODEL_FILE`/`LLM_SMALL_MMPROJ` on files the unit no longer
loads (the anti-drift seam doing its job). Both repointed to the
Ornith-Q8 paths; full `./tests/run.sh` back to green (366 tests).
- **frank-small: compute-buffer pair doubled to 4096/2048, and the prefill win
- **frank-secondary: compute-buffer pair doubled to 4096/2048, and the prefill win
measured rather than assumed.** Frank asked for the batch and ubatch doubled.
The blocker was on record: the Q5_K_M predecessor documented **"ubatch 2048
failed the scratch boot"**, and this shape carries 616 MiB less free space
@@ -96,7 +172,7 @@
`FRANK_SMALL_ENV_FILE` systemd drop-in, and it reproduced the 2026-10-03
boot **byte-identically** (21,276 used / 2,849 free), which is what makes
ubatch the only variable. Repo never saw a fake revert commit; scaffolding
staged to `~/.AGENTTRASH/20261005-frank-small-ubatch-ab/`.
staged to `~/.AGENTTRASH/20261005-frank-secondary-ubatch-ab/`.
- **At 8,928 prompt tokens: prefill 3,972/4,077 -> 4,207/4,212 t/s, +4.6 %,
for 616 MiB (free 2,849 -> 2,279).** Decode flat (91.9–93.0 vs 93.8–94.3 —
noise). The control that proves it is geometry and not drift: a 279-token
@@ -119,7 +195,7 @@
edge of the 2–3 GiB floor. Deployed `e5893dc`, e2e gate PASSED (2 mini specs
flaky-then-passed once the sleeping FrankBook-Pro woke mid-run — WoL took a
second burst and ~90 s). Docs:
[[frank-small-llama-mimo-q6k-2x200704]] (*Compute-buffer geometry*).
[[frank-secondary-llama-mimo-q6k-2x200704]] (*Compute-buffer geometry*).
## 2026-10-03
- **frank-primary: shape F then shape G — MNBT 2048, the CUDA-graph plan term
@@ -465,8 +541,8 @@
**sparse** file, 3.3 G apparent but 4 K real, so removing it frees
nothing.
- **A live reference check came first, and it earned its keep.**
`serve-frank-small.sh`'s *fallback* `MODEL_DIR` still pointed at the
retired GSQ-RCO payload frank-small served on 2026-09-16/17. The env
`serve-frank-secondary.sh`'s *fallback* `MODEL_DIR` still pointed at the
retired GSQ-RCO payload frank-secondary served on 2026-09-16/17. The env
file has always overridden it, so the drift was invisible — but a
missing or unreadable env file would have turned a config problem into a
"missing asset" naming a model that no longer exists. Repointed at
@@ -480,7 +556,7 @@
- Pre-existing: **118 G of trash from 2026-09-21** was already staged
(`20260921-frank-large-assembled` 88 G, `-redownload` 30 G) and never
purged. Total now staged on this host: **627 G**.
- Verified after the moves: frank-large and frank-small both `active`,
- Verified after the moves: frank-large and frank-secondary both `active`,
both `/health` 200, and all three live assets (Swift target, DFlash2
drafter, Gemma4 target) still present.
- **frank-large payload swapped to Swift-1.5 (uncensored)** (production
@@ -696,14 +772,14 @@
## 2026-09-25
- `/serverAppConfigs-docs/Dankserver/Jellyfin.md` — Jellyfin LDAP 401 resolved: the auth works via the modern `/Users/AuthenticateByName` endpoint (HTTP 200, alex authenticated, full plugin flow logged: service bind → search → ldapUid → user bind → token). The 401 was a test artifact — I had been probing the legacy `/Users/Auth/Password` route, which Jellyfin 12.1 removed (the `LegacyEmbyRouteRewriteMiddleware` is gone). The web UI uses the modern endpoint, so user logins are fine. No config change needed; the LDAP-Auth 23.0.0.0 plugin + authentik 2026.8.3 outpost are healthy.
- `/shared/homepage/services.json`, `/shared/homepage/docs.html` — Suggested uses added to the three LLM model descriptions (status cards + the `/docs` model guide + the "which one to pick" foot-note): frank-large → cybersecurity + debugging (use requires Frank's permission); frank-medium → the primary intelligent model (surpasses Claude Sonnet 5 per Artificial Analysis) for software engineering, sysadmin, instruction tasks; frank-small → the fast/cheap utility model (agentic search, RAG, research subagents, translation, roleplay, summarization). Deployed to `/srv/homepage`; bats 19/19 + non-Mac-mini e2e green (d3c8197).
- `/shared/homepage/{services.json,app.js,style.css,docs.html}` — Model cards reworked to a structured three-line shape: each LLM card now carries a `details` object (role / spec / use) rendered as a bold pale-green role, a bright-green spec line, and a dim "Use:" line (`app.js` `descHTML()` falls back to the plain `description` for non-LLM cards; new `.desc-role`/`.desc-spec`/`.desc-use` CSS + bare-mode reset). Roles made neutral (no nicknames): frank-large = deep reasoning, frank-medium = primary model, frank-small = fast utility model. The `/docs` model guide was also fixed — it had drifted to retired model info (frank-large listed as Qwen3.8-Flash-Next 87.3 GiB / 200k, frank-medium as FP8 ~31 GiB) — now Qwen3.8-27B UD-Q8_K_XL 31.5 GiB / 262k and MXFP4/W4A8 19.4 GiB, reordered large→medium→small. bats 30/30 + live browser render verified.
- `/shared/homepage/services.json`, `/shared/homepage/docs.html` — Suggested uses added to the three LLM model descriptions (status cards + the `/docs` model guide + the "which one to pick" foot-note): frank-large → cybersecurity + debugging (use requires Frank's permission); frank-medium → the primary intelligent model (surpasses Claude Sonnet 5 per Artificial Analysis) for software engineering, sysadmin, instruction tasks; frank-secondary → the fast/cheap utility model (agentic search, RAG, research subagents, translation, roleplay, summarization). Deployed to `/srv/homepage`; bats 19/19 + non-Mac-mini e2e green (d3c8197).
- `/shared/homepage/{services.json,app.js,style.css,docs.html}` — Model cards reworked to a structured three-line shape: each LLM card now carries a `details` object (role / spec / use) rendered as a bold pale-green role, a bright-green spec line, and a dim "Use:" line (`app.js` `descHTML()` falls back to the plain `description` for non-LLM cards; new `.desc-role`/`.desc-spec`/`.desc-use` CSS + bare-mode reset). Roles made neutral (no nicknames): frank-large = deep reasoning, frank-medium = primary model, frank-secondary = fast utility model. The `/docs` model guide was also fixed — it had drifted to retired model info (frank-large listed as Qwen3.8-Flash-Next 87.3 GiB / 200k, frank-medium as FP8 ~31 GiB) — now Qwen3.8-27B UD-Q8_K_XL 31.5 GiB / 262k and MXFP4/W4A8 19.4 GiB, reordered large→medium→small. bats 30/30 + live browser render verified.
- Fleet-wide container update (dankclaw + dankserver) — every pullable image brought current. Pinned bumps: authentik `2026.8.0`→`2026.8.3` (server/worker/outpost), tautulli `v2.18.1-ls242`→`ls245`, calibre-web `0.6.27-ls397`→`ls402`, plex `1.43.3.10896`→`1.43.4.10903`; seerr already at `v3.4.1`. Local-build rebuilds (forced `--no-cache`, the base images had moved): caddy (xcaddy porkbun), alloy, loki, comfyui-rocm (ComfyUI re-cloned), hermes ×3 (fresh `:latest`, `23986f31`→`e1e87aec`). anubis already at latest stable `v1.27.0` (only pre-releases exist beyond). Verified: all 16 floating-tag services CURRENT (zero stale), every real container active, caddy edge serving (auth 302 / ai 200), 340 bats green. Excluded: frank-medium/radiance-vllm (custom LLM build).
- `/dankserver/{tautulli,calibre-web,plex}/quadlet/*.container`, `/dankclaw/authentik/quadlet/*.container` — version pins bumped; docs (Tautulli.md, Calibre-Web.md, Plex.md, Authentik.md + server/worker, index.md) and the dead `dankserver/compose.yaml` plex entry synced (0b1ea2e).
## 2026-09-22
- `/serverAppConfigs-docs/**` — OKF v0.1 frontmatter migration: `type`/`description`/`tags`/`timestamp` added to the 65 non-reserved pages lacking them (timestamps from each file's last git commit); `timestamp` added to `LLM/frank-medium-batch-ubatch.md` and `LLM/frank-small-benchmark.md`; unquoted YAML scalars (colons, `*arr`) fixed in 13 pre-existing frontmatter blocks; frontmatter stripped from the reserved `LLM/Pipeline Parallelism/index.md`; `log.md` restructured to the OKF format (date-grouped, newest first, one line per change) from 40 narrative entries.
- `/serverAppConfigs-docs/**` — Fact-check pass against scripts/AGENTS.md ground truth: 6 false "Active" banners fixed on retired runbooks, 96→114 GiB GTT in `Shared Memory Architecture Dankclaw.md`, `deployment.md` + `Architecture/Deployment Flow.md` rewritten to the Quadlet/deploy-plan.py truth, `Calibre.md` 9.9.0→9.15.0 + CVE-2025-7404 conflation (it is the Calibre-Web CVE), `Copyparty.md` v1.20.24, `RomM.md` 5.3.0 breaking change; `*arr` docs moved to a new `FrankBook-Pro/` section (git mv, link-safe); active runbook bodies written for frank-large (unsloth UD-Q8_K_XL) and frank-small (Gemma4-12B).
- `/serverAppConfigs-docs/**` — OKF v0.1 frontmatter migration: `type`/`description`/`tags`/`timestamp` added to the 65 non-reserved pages lacking them (timestamps from each file's last git commit); `timestamp` added to `LLM/frank-medium-batch-ubatch.md` and `LLM/frank-secondary-benchmark.md`; unquoted YAML scalars (colons, `*arr`) fixed in 13 pre-existing frontmatter blocks; frontmatter stripped from the reserved `LLM/Pipeline Parallelism/index.md`; `log.md` restructured to the OKF format (date-grouped, newest first, one line per change) from 40 narrative entries.
- `/serverAppConfigs-docs/**` — Fact-check pass against scripts/AGENTS.md ground truth: 6 false "Active" banners fixed on retired runbooks, 96→114 GiB GTT in `Shared Memory Architecture Dankclaw.md`, `deployment.md` + `Architecture/Deployment Flow.md` rewritten to the Quadlet/deploy-plan.py truth, `Calibre.md` 9.9.0→9.15.0 + CVE-2025-7404 conflation (it is the Calibre-Web CVE), `Copyparty.md` v1.20.24, `RomM.md` 5.3.0 breaking change; `*arr` docs moved to a new `FrankBook-Pro/` section (git mv, link-safe); active runbook bodies written for frank-large (unsloth UD-Q8_K_XL) and frank-secondary (Gemma4-12B).
- `/serverAppConfigs-docs/Research/` — Three new research briefs (OKF): `Local TTS and Voice Cloning.md`, `AI Job Search and Resume Tools.md`, `Most Popular Self-Hosted AI Apps.md` (the 15 most popular self-hosted AI apps of Jun–Sep 2026, ranked by GitHub stars) + a self-contained dark-theme HTML report `popular-self-hosted-ai-apps.html`; upstream-version rows added to the ComfyUI/OpenWebUI/Hermes app docs (ComfyUI 0.37.0 native Qwen-Image 2.1, OpenWebUI 0.11.4 + `/api/v1/images/generations`, Hermes v0.21.4/CalVer v2026.9.21). All 336 tests pass; 136 pages, 0 unresolved wikilinks; docs-only deploy no-op.
## 2026-09-21
@@ -718,7 +794,7 @@
- `/dankserver/frank-medium/quadlet/frank-medium.container` — Checkpoint swapped MXFP4 → FP8 (`orcarouter/Qwen3.8-27B-Uncensored-FP8`, MTP self-speculation depth 8, acceptance 5.66/8); the swap is now one command via `scripts/hf-manifest.py` + `scripts/hf-fetch.sh` + `scripts/swap-frank-medium.sh`; A/B on the FP8 target: DFlash2 100.0 t/s decode beats MTP 85.9 (accept length is a proxy, not the metric) — DFlash2 is the deployed shape.
- `/dankserver/frank-medium/quadlet/frank-medium.container` — Cutover to vllm-radiance TP=2 (95.8 t/s / 0.21 s TTFT / 259.4 burst-4 vs llama layer split 45.7 / 0.72 / 71.7; the winning arm is also 4-bit, part of the margin is quant); the llama root unit moved to `scripts/.AGENTTRASH/20260920-llama-layersplit/` (never `systemctl disable`); KV pool auto-sized to 943,581 tokens (3.59× the window); tools, reasoning and vision verified live against the endpoint.
- `/scripts/build-llama-rdna4.sh` — Now takes `GGML_HIP_RCCL`; the RCCL build (`build-rdna4-rocm714-rccl`) made the tensor-mode bench arm fair (Ubuntu RCCL 2.27 carries gfx1201 device code; TheRock's does not — `invalid device function`).
- `/shared/prometheus/rules/llm-fleet.yml`, `/shared/prometheus/prometheus.yml`, `/shared/grafana/provisioning/` — Monitoring spans the `vllm:` + `llamacpp:` namespaces (6 alerts + 3 recording rules); frank-small/frank-large targets fixed to `host.containers.internal`; the prefix-cache reuse rule divides by `cached + prompt` (llama.cpp's `prompt_tokens_total` EXCLUDES cached tokens); prefix caching measured at 1,648-token block granularity (13,021-token prompt 2.30 s → 0.68 s, 76 % reused).
- `/shared/prometheus/rules/llm-fleet.yml`, `/shared/prometheus/prometheus.yml`, `/shared/grafana/provisioning/` — Monitoring spans the `vllm:` + `llamacpp:` namespaces (6 alerts + 3 recording rules); frank-secondary/frank-large targets fixed to `host.containers.internal`; the prefix-cache reuse rule divides by `cached + prompt` (llama.cpp's `prompt_tokens_total` EXCLUDES cached tokens); prefix caching measured at 1,648-token block granularity (13,021-token prompt 2.30 s → 0.68 s, 76 % reused).
- `/scripts/dankserver-route-primary.sh` — `eno1` pinned primary (metric 100), `eno2` retained failover (metric 200); applied with `nmcli device reapply` (no `connection reapply` in NM 1.54).
- Hardware — dankserver board swapped to the ASUS ProArt B850-Creator WiFi NEO: both R9700s on CPU root ports at Gen5 x8, every bridge `AtomicOpsCap: Routing+`, both endpoints `ReqEn+` — the PCIe-atomics wall is gone; GPU BDFs moved, kernel 7.0.0-31, SMT back on (16 CPUs).
- `/serverAppConfigs-docs/LLM/frank-medium-radiance-mxfp4.md` — Now the ACTIVE runbook (deployed flags, battery, capabilities, rollback); `frank-medium-llamacpp-dflash2.md` carries the SUPERSEDED banner; `Dual-R9700-Cooperation.md` states the measured verdict; `Dankserver System Specs.md` gained a Motherboard section; `frank-medium-model-swap.md` is the new swap runbook; AGENTS.md topology/sections/lessons flipped.
@@ -727,7 +803,7 @@
- `/scripts/frank-large.service`, `/scripts/serve-frank-large.sh` — frank-large: halogen → CIRU v4.4.1 Orca (jcbtc Qwen3.8-Flash-CIRU-STRIX-Orca w4b, 262144 ctx, MTP depth 4, 4096 MiB PLE cache, text only); halogen is the rollback target (b6ce3af, f68bda5).
- `/serverAppConfigs-docs/LLM/frank-large-ciru-orca.md` — New runbook.
- `/scripts/frank-medium.env`, `/scripts/serve-frank-medium.sh` — frank-medium: ajgazin UD-Q8_K_XL → orcarouter Q8_0 + z-lab DFlash2 Q4_K_M drafter; split re-balanced 35:30 → 32:33; pool 393216 → 327680 (d374ab9).
- `/scripts/frank-small.env`, `/scripts/serve-frank-small.sh` — frank-small: same-day vLLM detour reverted to mainline llama.cpp Gemma4-12B Q4_K_M + MTP sidecar + ngram-mod, q8_0 KV, 4 slots of 262144 (374041c).
- `/scripts/frank-secondary.env`, `/scripts/serve-frank-secondary.sh` — frank-secondary: same-day vLLM detour reverted to mainline llama.cpp Gemma4-12B Q4_K_M + MTP sidecar + ngram-mod, q8_0 KV, 4 slots of 262144 (374041c).
- Hardware — 3090 EVGA FTW3 iCX3 RGB set MODE_OFF and saved to EEPROM (persists across reboots, headless).
- AGENTS.md, both indexes, halogen runbook retirement banner (764cf41, 88331e2); 331 bats + 32 e2e green.
@@ -736,9 +812,9 @@
- Incident — frank-medium found stopped twice: (morning) a bench round ended without the protocol's restore half; (midday) an orphaned `llama-server` from `third_party/llamacpp/build-merge` (PPID 1, loopback :17770) held ~16 GiB on each R9700 and would have starved the 35:30 allocation — proven abandoned (frozen CPU ticks, zero connections) then killed; health 200 in ~20 s. Lesson: "GPUs idle" is a check, not an assumption.
## 2026-09-17
- `/scripts/frank-small.env`, `/scripts/serve-frank-small.sh` — frank-small: ISTA-DASLab GSQ-RCO IQ3_S → `HauhauCS/Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced` Q4_K_M + separate MTP assistant sidecar (`--spec-type draft-mtp,ngram-mod`); q8_0 KV (`build-cuda` rebuilt with `-DGGML_CUDA_FA_ALL_QUANTS=ON`, else ~12× prefill collapse); `--fit` chose the full 262144 window; audio transcribes verbatim; Gemma4 accepts every `reasoning_effort` level.
- `/scripts/frank-small.env` — `CTX_SIZE=1048576` / `PARALLEL=4` (four FULL 262144-token slots, the measured card ceiling: ~3,450 MiB/slot + ~8,546 fixed); ubatch 1024 deployed (pp ~965 / tg 226–302 at 253k; ubatch 2048 OOMs the MTP draft's pp buffer); draft n-max 4 + poll 100 measured WORSE at long ctx.
- `/serverAppConfigs-docs/LLM/frank-small-gemma4-12b.md` — New ACTIVE runbook; `frank-small-qwen38-gsq-rco.md` retired as rollback target.
- `/scripts/frank-secondary.env`, `/scripts/serve-frank-secondary.sh` — frank-secondary: ISTA-DASLab GSQ-RCO IQ3_S → `HauhauCS/Gemma4-12B-QAT-Uncensored-HauhauCS-Balanced` Q4_K_M + separate MTP assistant sidecar (`--spec-type draft-mtp,ngram-mod`); q8_0 KV (`build-cuda` rebuilt with `-DGGML_CUDA_FA_ALL_QUANTS=ON`, else ~12× prefill collapse); `--fit` chose the full 262144 window; audio transcribes verbatim; Gemma4 accepts every `reasoning_effort` level.
- `/scripts/frank-secondary.env` — `CTX_SIZE=1048576` / `PARALLEL=4` (four FULL 262144-token slots, the measured card ceiling: ~3,450 MiB/slot + ~8,546 fixed); ubatch 1024 deployed (pp ~965 / tg 226–302 at 253k; ubatch 2048 OOMs the MTP draft's pp buffer); draft n-max 4 + poll 100 measured WORSE at long ctx.
- `/serverAppConfigs-docs/LLM/frank-secondary-gemma4-12b.md` — New ACTIVE runbook; `frank-secondary-qwen38-gsq-rco.md` retired as rollback target.
- `/shared/homepage/` — The three LLM cards moved to the user page with live descriptions; new static `/docs` user page (access chain, model guide, how-tos) routed in the post-auth listener (32751c0).
- `/scripts/deploy-remote.sh` — The pending pass now prunes stale `pending` state entries for retired services into `~/.AGENTTRASH` and keeps going (a stale `frank-default-sightsound` entry made every `--pending` run exit 2) (02a7876).
- Fleet audit — Every vhost classified against the guard policy (no new exposure); `systemd-hermes` logged as a boot-persistence gap (running container, no quadlet); stray stopped `test-vllm` container on dankserver held for approval.
@@ -747,10 +823,10 @@
- `/scripts/bench-batch-ubatch.sh` — New harness: one `llama-bench` invocation per cell (a test that cannot allocate `exit(1)`s, taking a whole sweep with it), per-cell VRAM sampler, service stopped/started from a trap; must run from a transient unit (`LimitMEMLOCK=infinity` is load-bearing) (87845fc, 77a07ff).
- `/scripts/frank-medium.env` — `UBATCH_SIZE` 1536 → 512 (prefill peaks at 512: 1193 vs 1078 t/s at 2048; 3.71 GiB cheaper) and `TENSOR_SPLIT` 36,29 → 35,30 (ubatch buffers cost ROCm1 ~2× what they cost ROCm0) (a11fae0); `tests/frank_medium_unit.bats` balance seam re-fitted as a shape assertion.
- `/serverAppConfigs-docs/LLM/frank-medium-batch-ubatch.md` — New page: the sweep, the logical-batch irrelevance, the amdgpu settle-after-kill trap.
- `/dankclaw/frank-large/quadlet/frank-large.container` — halogen restored as frank-large, retuned: ctx 262144 → 200192, pool 401408 → 400384, slots 3 → 2, max-tok 32768 → 24576, host-reserve 15 → 20 GiB, vision tower unset (moved to frank-small); ledger 95.4 GiB held / 14.7 GiB spare, no clamp.
- `/scripts/frank-small.env`, `/scripts/frank-small.service`, `/scripts/serve-frank-small.sh` — New frank-small: mainline llama.cpp 37b53fd on the RTX 3090 serving ISTA-DASLab GSQ-RCO IQ3_S with the in-file MTP head (`draft-mtp`), BF16 projector, 131072 ctx, f16 KV, port 7771.
- `/dankclaw/frank-large/quadlet/frank-large.container` — halogen restored as frank-large, retuned: ctx 262144 → 200192, pool 401408 → 400384, slots 3 → 2, max-tok 32768 → 24576, host-reserve 15 → 20 GiB, vision tower unset (moved to frank-secondary); ledger 95.4 GiB held / 14.7 GiB spare, no clamp.
- `/scripts/frank-secondary.env`, `/scripts/frank-secondary.service`, `/scripts/serve-frank-secondary.sh` — New frank-secondary: mainline llama.cpp 37b53fd on the RTX 3090 serving ISTA-DASLab GSQ-RCO IQ3_S with the in-file MTP head (`draft-mtp`), BF16 projector, 131072 ctx, f16 KV, port 7771.
- `/scripts/deploy-remote.sh` — `deploy_llm_large` flipped back to halogen with a `wait_port_free 7770` gate (the first cutover died on `Address already in use` two seconds after the old server exited); `deploy_llm_small` added.
- `/serverAppConfigs-docs/LLM/frank-small-qwen38-gsq-rco.md`, `/serverAppConfigs-docs/LLM/halogen-flash-server.md` — New / re-activated runbooks; `frank-large-ik-uncensored.md` marked retired.
- `/serverAppConfigs-docs/LLM/frank-secondary-qwen38-gsq-rco.md`, `/serverAppConfigs-docs/LLM/halogen-flash-server.md` — New / re-activated runbooks; `frank-large-ik-uncensored.md` marked retired.
- `/serverAppConfigs-docs/LLM/x4-pair-experiment.md` — New page: 3090 + Strix Halo over the x4 link. iGPU-only Vulkan matches halogen's ~25 tok/s with the 3090 idle but commits 116 of 121 GiB (throughput parity, memory bankruptcy); per-tensor cross-device placement is killed by `-ot` itself (`has_tensor_overrides()` → `pipeline_parallel` → `n_copies == 1`); the first "CUDA + Vulkan cannot serve one model" conclusion was a llama-bench parsing artifact (`-dev A,B` is a SWEEP, not a list — correct `-dev A/B -ts x/y` gives 27.55 / 27.23 tok/s, +7 % decode / +16 % prefill vs iGPU-only).
- Incident — The rootless podman pause process died (trigger NOT established; no OOM kill in the kernel log): every `podman` command failed, all 28 quadlets looped, `podman system migrate` panics in 5.7.0; a reboot repaired it. The reboot exposed `hermes.franksdank.cloud` 502: the Caddyfile dialed `192.168.0.143:8787` (a host bind, not a published port) — fixed to `host.containers.internal:8787`, and the seam that had accepted any numeric upstream was strengthened.
- Host — Model cache cleared on operator instruction (298 → 895 GB free) and re-fetched: halogen's sidecars came back LARGER (in-place versioning — a stale copy is invisible from the file name); Q4_K_S vs Q4_K_M measured +2–3 % with ~5 GiB more host RAM; IQ4_XS fetched as the next comparison payload; `halogen-flash-server.md` gained a "Refreshing the weights" note.
@@ -789,7 +865,7 @@
- `/serverAppConfigs-docs/Infrastructure/FrankBook-Pro.md` — Rewritten to the active-tunnel reality (VM placement, WG gate, socat LaunchAgents, PBKDF2 seed); RDAP proved the "custom WG server" is a ProtonVPN Seattle server (full 65535-port scan: no admin surface anywhere).
- `/dankclaw/authentik/quadlet/authentik-outpost-ldap.container` — New LDAP outpost for Jellyfin (amd64 single-platform digest-pinned — podman 5.7.0 refuses the tag's nested manifest list); authentik objects provisioned via `ak shell` ORM; live bind tests proved two rules: the token intent must be `verification`, and the provider needs the `search_full_directory` object permission.
- `/serverAppConfigs-docs/Infrastructure/Authentik.md`, `/serverAppConfigs-docs/Dankclaw/Jellyfin.md` — LDAP-outpost section (objects table + rules) and the LDAP-Auth 23.0.0.0 plugin section (`LdapSearchAttributes` is comma-separated; username attribute `cn`).
- `/scripts/frank-small.env`, `/scripts/serve-frank-small.sh` — Two compressed-tensors FP8 swaps attempted on FreeToken and both failed to load (ornith-1.5: expert naming; HauhauCS aggressive: block scales do not broadcast in `_dequant_fp8_weight`); reverted to Huihui FP8 within the hour; `freetoken-frank-small.md` quant matrix updated (modelopt block-fp8 is the only FP8 path that loads); the deploy health gate passed for BOTH failed loads — a swap is only proven by the journal + a real completion.
- `/scripts/frank-secondary.env`, `/scripts/serve-frank-secondary.sh` — Two compressed-tensors FP8 swaps attempted on FreeToken and both failed to load (ornith-1.5: expert naming; HauhauCS aggressive: block scales do not broadcast in `_dequant_fp8_weight`); reverted to Huihui FP8 within the hour; `freetoken-frank-small.md` quant matrix updated (modelopt block-fp8 is the only FP8 path that loads); the deploy health gate passed for BOTH failed loads — a swap is only proven by the journal + a real completion.
- `/dankserver/{romm,jellyfin,calibre-web,copyparty}/quadlet/` — After a hard crash left every bare compose container dead at boot, the media stack standardized to quadlet: six units shipped + cutover handlers (retire bare containers, rescue RomM's anonymous volume, merge-move `/opt/<svc>` → `/home/dank/Containers/<svc>`); v1 generator quirks fixed (`Command=`→`Exec=`, `Device=`→`AddDevice=`, `HealthCmd=` space-splitting, the `HOME=/root` EnvironmentFile trap).
- `/dankclaw/` — Data roots of gitea, authentik, and the hermes trios moved to `/home/dank/Containers/<svc>/` while running (rename is inode-bound — zero downtime); `deploy_hermes` ordering fixed (generated units reload AFTER the unit files are copied).
- `/dankclaw/seerr/quadlet/seerr.container` — New Seerr v3.4.1 request manager: loopback-only 127.0.0.1:5055, guarded vhost + outpost provider `seerr-proxy`; `/tests/seerr_quadlet.bats` new seam; `/serverAppConfigs-docs/Dankclaw/Seerr.md` new page.
@@ -801,7 +877,7 @@
- `/scripts/frank-npu.env`, `/scripts/serve-frank-npu.sh`, `/scripts/frank-npu.service` — New `frank-npu` on Dankclaw: FLM v1.0.1 serving the whole local registry (36 tags) on 52626; first deploy crash-looped (`/dev/accel/accel0` err=-19) — root cause: the XDNA2 SVA bind fails whenever the AMD IOMMU is off; fixed with `iommu=pt` + reboot; vision + audio towers proven end-to-end; health probe is `/v1/version` (FLM has no `/health` route) (c3ed3a6, cf0c541).
- `/serverAppConfigs-docs/LLM/frank-npu.md` — New runbook; AGENTS.md fleet table + the IOMMU/SVA hard requirement.
- `/dankclaw/frank-large/quadlet/frank-large.container` — frank-large: swapped to `kingjones777/Qwen3.8-Flash-Next-ROCmFP4-STRIX-GGUF` on the pinned `kingjones30/ROCmFPX` fork (fa-on works UNPATCHED — the QSA F32 assert was agentionai-specific); ~225 s load, real 111k-token prefill at 213–232 t/s.
- `/scripts/frank-small.env` — frank-small (FreeToken): model swapped to `batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8` (native block-fp8); ~40 tok/s verified.
- `/scripts/frank-secondary.env` — frank-secondary (FreeToken): model swapped to `batsclamp/Huihui-Qwen3.6-35B-A3B-abliterated-FP8` (native block-fp8); ~40 tok/s verified.
- `/serverAppConfigs-docs/LLM/kingjones-strix-rocmfp4.md` — Reactivated as the large runbook; `agentionai-fast-imatrix.md` marked RETIRED; `freetoken-frank-small.md` model sections updated; LLM index + AGENTS fleet tables refreshed.
- `/serverAppConfigs-docs/Dankserver/prowlarr.md`, `/serverAppConfigs-docs/Dankserver/qbittorrent.md` — Rewritten: the arr↔Prowlarr feed contract (`/{indexerId}/api`, the global key — the hand-rolled `/torznab/` paths were Forms-auth catch-alls returning 302), TV + music chains proven live, first real grab (The Dark Knight 2160p, imported), Prowlarr auto-sync broken by version skew, qB persistent credentials (`WebUI\Password_PBKDF2`, host-local 0600), Readarr parked (abandoned upstream; its client cannot authenticate against qB 5).
@@ -1037,8 +1113,8 @@ any repo service.
## 2026-10-01 — frank-large cutover: gufo -> vLLM TheRock, MiMo-V2.6 BF16 + cross-model MTP
- **Question that started it:** can a Qwen3.5 MTP drafter attach to
frank-small, and how does vLLM run the FULL BF16 MiMo checkpoint? Answer 1:
not on frank-small — SGLang reads the MTP head from the TARGET checkpoint
frank-secondary, and how does vLLM run the FULL BF16 MiMo checkpoint? Answer 1:
not on frank-secondary — SGLang reads the MTP head from the TARGET checkpoint
and MiMo ships ZERO `mtp.*` tensors (base and INT8 alike). Answer 2: the
digest-pinned `kyuz0/vllm-therock-gfx1151` image (vLLM 0.30 / torch 2.11 /
TheRock ROCm 10) boots BF16 MiMo on the gfx1151 iGPU in ~200-240 s, and
@@ -1108,12 +1184,12 @@ bundled BF16 mmproj, and MTP from the repo's own `mtp-Qwen3.8-27B-Q4_0.gguf`
fetchers moved to `old/`. Runbook
[[frank-large-rocmfpx-q8agent-mtp]].
## 2026-10-02 — frank-small: Q5_K_M, then three contexts + vision, sized by two boots
## 2026-10-02 — frank-secondary: Q5_K_M, then three contexts + vision, sized by two boots
Three orders in one day on the same endpoint: `bartowski/…Q5_K_M` on llama.cpp
with two contexts ("whatever context size fits the 3090"), then **three
contexts** "as much as can fit while keeping overhead open space to 2-3gb on
the 3090", then **"frank-small should have vision"**.
the 3090", then **"frank-secondary should have vision"**.
- **Final shape: `CTX_SIZE=540672 PARALLEL=3` = 3 x 180,224**, f16 K + q8_0 V
under flash attention, `--mmproj` vision, all layers on the 3090. Measured
@@ -1151,7 +1227,7 @@ the 3090", then **"frank-small should have vision"**.
`~/.AGENTTRASH/20261002-retired-llm-payloads/` (~59 GiB of superseded
payloads). Kept in place for rollback: `MiMo-Q6K/`, `MiMo-Q5KM/`,
`Qwen3.8-27B-UD-Q4_K_XL/` and `Qwen3.8-27B-UD-Q8_K_XL/` (the latter holds the
gufo DFlash2 drafter). Runbook [[frank-small-llama-mimo-q5km]].
gufo DFlash2 drafter). Runbook [[frank-secondary-llama-mimo-q5km]].
## 2026-10-02 — frank-primary shape D: 3 x 200704, fp8, pool sized for 2 contexts
@@ -1221,7 +1297,7 @@ top of the proven-good base, root cause first.
- **Harness contract followed the window:** `models.yml` `contextWindow`
262144 -> **200704** (compaction trigger 140,492 + 36,864 = 177,356 <=
200,704). Side effect: frank-primary's compaction request now fits inside a
frank-small slot again, which it had stopped doing when frank-small went to
frank-secondary slot again, which it had stopped doing when frank-secondary went to
3 x 180,224.
- **Comment-only follow-up** `295d578` recorded the measured numbers and was
deployed so the per-service revision marker sits at HEAD; e2e 33/33 again.
@@ -1,7 +1,7 @@
{
"uid": "llm-fleet",
"title": "LLM fleet \u2014 capacity and reuse",
"description": "The signals that explain the LLM endpoints under load. One metric namespace: llamacpp:* across all three endpoints — frank-small (Dankclaw, RTX 3090), frank-large (Dankclaw, gfx1151 iGPU) and frank-primary (Dankserver, 2x R9700, llama.cpp again since the 2026-09-26 swap back from Lucebox DS4.1). The endpoint picker selects between them. See serverAppConfigs-docs/Infrastructure/Observability.md for the namespace split and the addressability rules.",
"description": "The signals that explain the LLM endpoints under load. One metric namespace: llamacpp:* across all three endpoints — frank-secondary (Dankclaw, RTX 3090), frank-large (Dankclaw, gfx1151 iGPU) and frank-primary (Dankserver, 2x R9700, llama.cpp again since the 2026-09-26 swap back from Lucebox DS4.1). The endpoint picker selects between them. See serverAppConfigs-docs/Infrastructure/Observability.md for the namespace split and the addressability rules.",
"tags": [
"llm",
"capacity"
@@ -166,7 +166,7 @@
"id": 5,
"type": "timeseries",
"title": "Decode steps and batch size",
"description": "Per-step counters: steps per second and the busy-slot count per step (the effective batch). With frank-primary's --parallel 1 the slot count stays at 0 or 1 and says nothing about the card; with frank-small's 4 slots it tracks the agent-fleet burst shape.",
"description": "Per-step counters: steps per second and the busy-slot count per step (the effective batch). With frank-primary's --parallel 1 the slot count stays at 0 or 1 and says nothing about the card; with frank-secondary's 4 slots it tracks the agent-fleet burst shape.",
"gridPos": {
"h": 8,
"w": 12,
+4 -4
View File
@@ -36,8 +36,8 @@
Pick a model in the chat's model selector — the three models the fleet runs are
described <a href="#models">below</a>. Open WebUI keeps your chat history; the raw
model endpoints do not.</p>
<p>Images go to <code>frank-primary</code> or <code>frank-small</code>
(both see them). Audio (WAV) goes to <code>frank-small</code> only.</p>
<p>Images go to <code>frank-primary</code> or <code>frank-secondary</code>
(both see them). Audio (WAV) goes to <code>frank-secondary</code> only.</p>
</section>
<section id="models">
@@ -62,7 +62,7 @@
<li><strong>Best for:</strong> software engineering, system administration, and instruction tasks.</li>
</ul>
<h2>frank-small — the fast utility model</h2>
<h2>frank-secondary — the fast utility model</h2>
<ul>
<li><strong>Model:</strong> Gemma4-12B QAT (Balanced, Q4_K_M, 7.4 GiB).</li>
<li><strong>Capacity:</strong> one million tokens of pooled context across four simultaneous conversations (up to 262k each); fastest at images.</li>
@@ -71,7 +71,7 @@
</ul>
<p class="foot-note">Which one to pick? Small utility work, RAG, search, and
screenshots: <code>frank-small</code>. Software engineering, sysadmin, and
screenshots: <code>frank-secondary</code>. Software engineering, sysadmin, and
instruction tasks: <code>frank-primary</code>. Cybersecurity and debugging:
<code>frank-large</code> (with Frank's permission).</p>
</section>
+1 -1
View File
@@ -298,7 +298,7 @@
"showLink": false
},
{
"name": "frank-small",
"name": "frank-secondary",
"category": "user",
"section": "AI",
"details": {
Loaded 100 of 109 files, more files were not shown because too many files have changed in this diff. Show more