Compare commits

..
Author SHA1 Message Date
Frank 19fdffe8bd docs(agents): a start-only quadlet handler no-ops an image bump
The law today's authentik bump paid for: start on an active unit is a no-op, so
the new digest installs and the old container keeps serving behind three green
gates. Restart applies a pin.
2026-10-30 15:34:10 -07:00
Frank f7aec7f154 fix(authentik): restart the units so an image bump actually applies
The handler called `systemctl --user start` on all three units. Start is a
no-op for an already-active unit, so installing a new digest under an active
authentik left the old container serving: today's 2026.8.3 -> 2026.8.5 bump
moved only the OUTPOST (whose handler restarts) and left server + worker on
2026.8.3 while the deploy, the health gate and the e2e suite were all green.
The generator re-renders from the .container, so a restart is the step that
applies a pin.
2026-10-30 15:34:02 -07:00
Frank e1d4d59773 docs(dragoon,zealot): record both runtime pins as they now run
frank-dragoon: engine 0.18.20 (image e35133b2…), the 18-model NPU catalog, the
re-cut qwen3-embedding-0.6b device programs, and the wire surface the new engine
actually answers (GET /tokenize is 405, /props is 200, /completion and /ping
stay 404). Eras gains the 2026-10-30 row.

frank-zealot: fork build 12118 (0.7.0-dev, 024474448) with the payload-load
evidence (202 pq2_0 tensors, 202 Hadamard-folded weights), plus the law a
rebuild pays for: cmake --install strips the rpath and installs bin/+lib/, so
the configure needs CMAKE_INSTALL_RPATH and LLAMA_BIN names …/bin/llama-server.
2026-10-30 15:27:21 -07:00
Frank acbb539b97 feat(frank-zealot): PrismML fork b10777 -> build 12118 (0.7.0-dev, 024474448)
Rebuilt the fork from origin/prism at 024474448 with the same configure the
10777 cutover used (Release, GGML_CUDA=ON, sm_86, gcc-13 host compiler, Ninja),
installed to /home/dank/GitPackages/llama-prism-0244744. The payload gate holds:
libllama carries 42 prism.hadamard strings (b10777 had 29) and libggml-base
still registers PQ2_0, so type 142 and the hadamard metadata still load.

LLAMA_BIN inherits a bin/lib install layout now, so the install needs an RPATH
($ORIGIN;$ORIGIN/../lib) — cmake's install step strips the build rpath and the
binary otherwise dies on libllama-server-impl.so.

The unit test that pinned the 10777 path now asserts the invariant its own
comment described (LLAMA_BIN names a llama-prism-* build) instead of a
directory one rebuild happens to install; the suite runs on a box with no
/home/dank, so existence is not checkable there.

Rollback: repoint LLAMA_BIN at llama-prism-b10777/llama-server (still on disk).
2026-10-30 15:25:24 -07:00
Frank 011ceeb344 fix(npu fetch): never resume into a file from another revision
A version bump re-cuts the device programs, so a staged file of a different
size is stale rather than partial. `curl -C -` against it appended the new
tail to the old bytes and produced the right BYTE COUNT with the wrong
content — the sha256 gate caught it (consts.hnpb, 2026-10-30), and the
failure reads as a corrupt program rather than a fetch fault. Resume only
when the staged file is smaller than the manifest expects.
2026-10-30 15:13:42 -07:00
Frank 33396d2daa feat(frank-dragoon): halogen-flash-server 0.16.2 -> 0.18.20, NPU lane re-pinned
The engine pin moves to the newest vendor release (0.18.20, image built
2026-10-30). The payload lock is the gate this deploy has to pass: the v2
.hgn loads only in the flash-server line, so /health plus the boot ledger
decide, and the rollback is the 0c61bf84 digest on the same env line.

0.18.20's NPU catalog is NOT the 0.16.2 one: it names 14 device programs
(u0..u13) and an 8,495,512 B consts.hnpb where 0.16.2 had 10 and 5,086,496 B,
and the entrypoint verifies size+sha256 for every staged file before it
starts. HALOGEN_NPU_MODELS names qwen3-embedding-0.6b, so the staging set is
re-pinned to revision 8fd44c9f with the new block — bumping the engine alone
would refuse the boot on the NPU lane.
2026-10-30 15:13:19 -07:00
30 changed files with 190 additions and 2994 deletions

No files matched your search

+15 -8
View File
@@ -26,8 +26,8 @@ merge absorbed the former `vllmScripts` repo, history preserved).
| Target | Service | Runtime | Payload | Port |
|--------|---------|---------|---------|------|
| Dankserver | `frank-archon` | radiance, digest-pinned, root unit, TP=2 | publisher `qwen3.8-next-flash-fp8-iq4r-moe.rad` | 7770 |
| Dankclaw | `frank-dragoon` | halogen-flash-server 0.16.2, digest-pinned, root unit | `qwen38-flash-next-v2-abliterated.hgn` + n-gram + vision tower | 7770 |
| Dankclaw | `frank-zealot` | llama.cpp PrismML fork build 10777, root unit — the only loader for this payload | Ternary-Bonsai-2-9B PQ2_0 + Q8_0 mmproj + rank-1 refusal adapter | 7771 |
| Dankclaw | `frank-dragoon` | halogen-flash-server 0.18.20, digest-pinned, root unit | `qwen38-flash-next-v2-abliterated.hgn` + n-gram + vision tower | 7770 |
| Dankclaw | `frank-zealot` | llama.cpp PrismML fork build 12118, root unit — the only loader for this payload | Ternary-Bonsai-2-9B PQ2_0 + Q8_0 mmproj + rank-1 refusal adapter | 7771 |
Payload identity, shape, eras, laws and rollback live in ONE page each:
[[frank-archon]], [[frank-dragoon]], [[frank-zealot]]. `scripts/<service>.env`
@@ -293,8 +293,8 @@ means off or NIC dead; a merely wedged kernel usually still answers ARP.
2. Bench against the service itself. No scratch ports on an LLM host — one
process owns the card. Use the runtime's own harness (`llama-bench`; a
live llama.cpp server also reports honest `timings` per request, including
`draft_n`/`draft_n_accepted` — but on frank-zealot's PrismML fork build
10777 that object only reaches you on **`/v1/chat/completions`**: a greedy
`draft_n`/`draft_n_accepted` — but on frank-zealot's PrismML fork that object
only reaches you on **`/v1/chat/completions`** (measured on build 10777): a greedy
`ignore_eos` run on the raw `/completion` path there answers **500 "The
model produced output that does not match the expected Content-only
format"** (measured 2026-10-18). halogen has no `/completion` path at all,
@@ -439,16 +439,16 @@ disagrees with it is the bug.
### frank-zealot — llama.cpp (PrismML fork), RTX 3090, :7771
- Owns: `scripts/frank-zealot.env`, `scripts/serve-frank-zealot.sh`,
`scripts/frank-zealot.service`; fork build `llama-prism-b10777` is the ONLY
loader (the payload carries the fork's custom PQ2_0 tensor type plus
`scripts/frank-zealot.service`; fork build `llama-prism-0244744` (build 12118)
is the ONLY loader — `llama-prism-b10777` stays on disk as its rollback (the payload carries the fork's custom PQ2_0 tensor type plus
`prism.hadamard.*` dequant metadata — stock llama.cpp refuses it, and a
loader ignoring that metadata dequantizes wrong); payload
`Bonsai2-9B-MTP/…PQ2_0.gguf` + the publisher's **Q8_0** mmproj (the BF16
tower can crash the server with concurrent image requests) + the rank-1
refusal-direction adapter applied at load by `--lora-scaled …:1`. The
adapter's rollback is `LORA_FILE=` (empty) in the env — one line, no rebuild
— and build 10777 is also the gate that lets it load: older forks refuse it
with `unsupported prism.hadamard.version: 2`.
— and the fork BUILD is also the gate that lets it load: anything older than
b10777 refuses it with `unsupported prism.hadamard.version: 2`.
- Shape: `--ctx-size 602112 --parallel 3` = **three conversations at
200,704 each**, `CACHE_TYPE_K=f16` + `CACHE_TYPE_V=q8_0` (asymmetric: 25,088
B/token, 14,406 MiB of pool), batch 6144 / ubatch 1024,
@@ -741,6 +741,13 @@ linked runbooks.
`/etc/default/grub`. It replaced `amd_iommu=off`, and the local A/B on byte-identical traffic
says identity mode costs nothing, so the XDNA2 NPU returned for free.
[[dankclaw-iommu-translation-off]]
- **A deploy handler that only `start`s a quadlet no-ops an image bump.**
`systemctl --user start` on an already-active unit does nothing, so installing
a new digest under a running service keeps the OLD container serving while the
deploy, its health gate and the Playwright suite all pass — the authentik
handler did exactly that on 2026-10-30 (2026.8.3 → 2026.8.5 moved only the
outpost, the one handler that restarted). A pin change is applied by
`restart`, never `start`.
- **Rootless Quadlet containers run with `--replace --rm`**: `podman logs` covers only the CURRENT
instance; history across recreations lives in `journalctl --user -u <name>.service`. Read
rootless storage WITHOUT sudo — it is per-user storage, so `sudo podman logs` finds nothing.
+7 -77
View File
@@ -22,17 +22,9 @@ Mechanics notes:
prompts do not bias the Hessians (the corpus is drawn round-robin over
source classes, so loss is class-neutral as long as failures are rare;
a systematic 4xx is loud in the log and must be fixed, not ignored).
- a round that seals no tap bytes is NORMAL and continues: RADIANCE_CALIB_TOKENS
is a pooled floor and this campaign's corpus is 10.03M tokens against a 12M
floor, so round 1 seals nothing by construction. A tap that LOSES bytes fails
the run, and a run that never sees the engine's completion line exits non-zero.
The first version failed on a flat round, which aborted exactly one round too
early (2026-10-29).
- before any prompt, the tap must carry the rig's own marker
(`--tap-marker`, written by run-qwen38-bf16.sh calib) naming THIS
container, and that container must be running. Measured 2026-10-29:
with neither check, four rounds of 10M tokens went into restored
production after the rig's boot had died, and recorded nothing.
- a round that seals no tap bytes is a FAILURE, not a retry: the engine
answering 200 while the tap directory is wrong, full or read-only is
how a corpus gets pushed four times for nothing.
Usage: calib-push.py [--port 7770] [--parallel 64] [--container arbench]
[--corpus /home/dank/corpora/out/calib.jsonl] [--max-rounds 4]
@@ -115,61 +107,6 @@ def tap_bytes(path):
return total
def round_verdict(before, after, done):
"""What one round's tap bytes mean.
'done' the engine logged that it sealed the tap — the run is over
'shrink' the tap LOST bytes: reset, pruned or replaced under us
'grew' sealed bytes this round
'flat' sealed nothing — which is legitimate: RADIANCE_CALIB_TOKENS is a
pooled floor, and a round that does not reach it has nothing to
write yet (the campaign's corpus is 10.03M tokens against a 12M
floor, so round 1 is always flat). Failing on it aborted the run
exactly one round too early.
"""
if done:
return "done"
if after < before:
return "shrink"
if after == before:
return "flat"
return "grew"
def require_rig(a):
"""Refuse unless the rig that opened this tap is up and serving right now.
Three facts, in order: the runner wrote its marker into the tap dir, the
marker names the container this pusher targets, and that container is
running. A marker without a live container is a boot that died; a live
container without a marker is some other engine on the port — on 2026-10-29
it was restored production, which took four rounds of 10M tokens before the
old driver admitted it had recorded nothing.
"""
marker = os.path.join(a.tap_dir, a.tap_marker)
if not os.path.isfile(marker):
qc.refuse(f"no rig marker at {marker}: this tap was not opened by "
"run-qwen38-bf16.sh calib, so a push here would feed whatever "
"answers the port and record nothing")
try:
lines = open(marker, encoding="utf-8", errors="replace").read().splitlines()
except OSError as exc:
qc.refuse(f"cannot read the rig marker {marker}: {exc}")
meta = dict(line.split("=", 1) for line in lines if "=" in line)
who = meta.get("container", "")
if who != a.container:
qc.refuse(f"the rig marker names container {who!r} and this pusher targets "
f"{a.container!r} — wrong tap, or a stale marker")
try:
p = subprocess.run(["podman", "inspect", "-f", "{{.State.Running}}", a.container],
capture_output=True, text=True, timeout=30)
except Exception as exc:
qc.refuse(f"cannot inspect the rig container {a.container}: {exc}")
if p.returncode != 0 or p.stdout.strip() != "true":
qc.refuse(f"the rig container {a.container} is not running — whatever is "
"answering the port, it is not the calibration rig")
def parse_args():
ap = argparse.ArgumentParser()
ap.add_argument("--port", type=int, default=7770)
@@ -177,8 +114,6 @@ def parse_args():
ap.add_argument("--container", default="arbench")
ap.add_argument("--model", default="frank-archon")
ap.add_argument("--tap-dir", default="/home/dank/campaign/qwen38-w5nl64/calib")
ap.add_argument("--tap-marker", default=".rig-boot",
help="the file run-qwen38-bf16.sh calib writes into the tap dir; without it the pusher refuses, because an unmarked tap is not a rig")
ap.add_argument("--corpus", default="/home/dank/corpora/out/calib.jsonl")
ap.add_argument("--max-rounds", type=int, default=4)
return ap.parse_args()
@@ -201,7 +136,6 @@ def main():
a = parse_args()
url = f"http://127.0.0.1:{a.port}/v1/completions"
prompts = load_prompts(a)
require_rig(a)
chars = sum(len(p) for p in prompts)
print(f"corpus: {len(prompts)} prompts, ~{chars // 4} tokens (chars/4)",
flush=True)
@@ -222,15 +156,11 @@ def main():
if tap_done(a.container):
break
after = tap_bytes(a.tap_dir)
verdict = round_verdict(before, after, False)
if verdict == "shrink":
print(f"FAIL: the tap at {a.tap_dir} LOST bytes this round "
f"({before} B before, {after} B after) — it was reset or replaced "
"under us", flush=True)
if after <= before:
print(f"FAIL: the tap at {a.tap_dir} sealed nothing this round "
f"({before} B before, {after} B after) — the engine is serving "
"without recording (wrong dir, full disk, read-only mount)", flush=True)
sys.exit(1)
if verdict == "flat":
print(f"round {rnd}: the tap sealed nothing yet (byte floor not reached; "
f"{before} B) — continuing", flush=True)
else:
print("max rounds exhausted, tap not done", flush=True)
sys.exit(1)
+1 -1
View File
@@ -21,7 +21,7 @@
# deployed knob has since dropped to 262144.
set -euo pipefail
IMG=ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a
IMG=ghcr.io/peonist-ai/halogen-flash-server@sha256:e35133b206da3094cc4d70c78800b26235bd9124b9aeb0e029d4a7c153b9cff0
CUR=/home/dank/models/halogen-flashnext-v2
NEW=/home/dank/models/halogen-v2-abliterated
CUR_CK=qwen38-flash-next-v2-swift15-abliterated.hgn
+10 -4
View File
@@ -1362,11 +1362,17 @@ deploy_authentik() {
# generated". Boot persistence comes from [Install] WantedBy=default.target
# inside each .container file, which the generator honors at user-manager
# start (linger must stay on for the deploy user).
systemctl --user start authentik-postgres.service
# RESTART, not start: `start` on an already-active unit is a no-op, so an
# image bump would install the new digest and keep serving the old
# container while every gate stayed green (measured 2026-10-30: server and
# worker stayed on the previous release beside an outpost that moved,
# because only the outpost handler restarts). The generator has already
# re-rendered the unit from the .container, so a restart is what applies it.
systemctl --user restart authentik-postgres.service
wait_container_healthy authentik-postgres 120 || return 1
systemctl --user start authentik-server.service
systemctl --user restart authentik-server.service
wait_container_healthy authentik-server 300 || return 1
systemctl --user start authentik-worker.service
systemctl --user restart authentik-worker.service
}
deploy_authentik_outpost() {
@@ -1482,7 +1488,7 @@ deploy_anubis() {
# 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_DRAGOON_IMAGE="${LLM_DRAGOON_IMAGE:-ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a}"
LLM_DRAGOON_IMAGE="${LLM_DRAGOON_IMAGE:-ghcr.io/peonist-ai/halogen-flash-server@sha256:e35133b206da3094cc4d70c78800b26235bd9124b9aeb0e029d4a7c153b9cff0}"
# The payload dir is its own twin because the NPU lane's containment gate below
# reads it; every artifact path under it stays a full literal as usual.
LLM_DRAGOON_MODEL_DIR="${LLM_DRAGOON_MODEL_DIR:-/home/dank/models/halogen-v2-abliterated}"
+33 -16
View File
@@ -8,8 +8,10 @@
# (/opt/halogen/npu/models.txt inside the image) and refuses to load a model
# whose files do not match.
#
# The manifest below IS that catalog block (dumped from the pinned 0.16.2
# image on 2026-10-19), so the files land in the layout the entrypoint
# The manifest below IS that catalog block (dumped from the pinned 0.18.20
# image on 2026-10-30; the 0.16.2 block this replaced named different device
# programs — 10 `u*.elf` and a 5,086,496 B consts.hnpb — so a version bump
# re-fetches the set or the entrypoint refuses the boot), so the files land in the layout the entrypoint
# expects — <dir>/<model-id>/devices/*, <dir>/<model-id>/<model-id>.hnpw and
# <dir>/<model-id>/tokenizer/tokenizer.json — and the endpoint can keep
# /models read-only and open no outbound connection at boot (HALOGEN_DOWNLOAD
@@ -57,21 +59,25 @@ manifest() {
case "$1" in
qwen3-embedding-0.6b)
NPU_REPO="peonist-ai/halogen-npu-qwen3-embedding-0.6b"
NPU_REV="4960fd9d4423dd15373ad21c83db48dbdb3b2798"
NPU_REV="8fd44c9f18861618a509bea75c51ff8a37e3bd5d"
FILES=(
"devices/NOTICE.md 3248 0f267b3d2523ddf14997ee58660a492a9eaf10ccf6b08bf19dfdc85b9a478b91"
"devices/consts.hnpb 5086496 ae430e41545a0b1012938cda373b0c450364d3be631111cca3a8c43679f93336"
"devices/devices.hnpm 24047 4e9bca9d95d084fb89ce5b99f579a00666c8ca1efae2613c53e3a4878cc9f174"
"devices/u0.elf 4931968 b38f165886cb037b7d3951d9cb878473eaba58a432acd45416ff85fd88aa4e46"
"devices/u1.elf 5456000 b1811feee851b8af833b37721da92099d89772110815bbbe2c3fa1c276f0ff42"
"devices/u2.elf 6505552 406ea9a95b470628a7382e376f4a72cdf4d7aa4c59b4ddf0a4bdd139d63d6ff2"
"devices/u3.elf 6817216 bc205d51b523d770151a0c61968f7c70bf624412827580bb6fe0aaf4650dc2ee"
"devices/u4.elf 7444032 a5a91338a2603acdcd5c11df532033fda2d3f5479ea44ae87f98fafbdff9a62d"
"devices/u5.elf 7432416 6c2bf41835569e0eba25ded7c05c7264ae69c6700bdeb4791057d884f2a4c126"
"devices/u6.elf 4941232 919d0038ca088eafddf195f3466ff38db27d3635be1cffa5d4d4b5140902a68e"
"devices/u7.elf 5488304 d904cf8afae9428dd167960549e18cce5441745f18c62f8c50975dffc077d3c2"
"devices/u8.elf 4941232 db3aef69b17825854ee0dd2df475125a3f5c872f95c686793d6af1ee83596fa7"
"devices/u9.elf 5488304 d182727e551084db8fb9600441d8aa373c7f09a3d267ae0a5e921ef92326db1c"
"devices/consts.hnpb 8495512 5af7c1f2f24d45a9054872bba882461cf7f6ffda832e785b700643d6467ad88b"
"devices/devices.hnpm 35465 85d870f6b900de1829659955f0dc701d57984580f5e5ec251add5d6969f03e3d"
"devices/u0.elf 2975008 f74df12eb8c79e4e90cbedcc1dd8f142219772d400e3a142fde8a95378b105eb"
"devices/u1.elf 3266848 ac5a3f89f934951b315b62ab53e1d0a74dbe6396134192641c405d08da332754"
"devices/u2.elf 4015328 5e4f0d3342f262816289866d7e8c966f56a9f938c2e12e7341718cc7312aa480"
"devices/u3.elf 4326976 94cf5d269fa286d91c26bf4654cedf101c047d23c4495b74c9c129cdc33b210f"
"devices/u4.elf 4953792 cbd2796050d4c675f5cb3da117111087d510173e26f262f1b36aab66dd49dc91"
"devices/u5.elf 4940112 7bf6a4470546bb888da02af395c07d92a24fb6d43aef27eb2151b3a9eda19e4d"
"devices/u6.elf 6174544 10b9d50b635993e1f9c23775c0b94901d922c1e433cd10910e9f97174235f5ac"
"devices/u7.elf 2983776 e0b06063b7379d05c85b3f435b819d96fda1223d8025d6d15958854b5711ace9"
"devices/u8.elf 3298656 3bad204f782a68b97e27c1172530b13d7fbb0b495fa95f9504530a1048560644"
"devices/u9.elf 2983776 875e313280358ce116e2a51a29a2c42794b5a9fadb3d48db702c90d18d838c2c"
"devices/u10.elf 3298656 ef2e0f399f149a5476955bcf233c6fa2e896fcef1f86ecb0d8113c952c41a8b2"
"devices/u11.elf 3298656 68a2519eee2d6a61ccc4ca2007e3186d44c9eb4e81162d742fe2f1756ec5a528"
"devices/u12.elf 3298656 2bdc6abfa58cac1d5795a3ef52fe8a9ab4e7c2a4792822d615499a913401f728"
"devices/u13.elf 3298656 4a3ce14f7ba15987b9bcbb4a010083e8aef68cd87c20cbe43403a98263179fe2"
"qwen3-embedding-0.6b.hnpw 813032248 b9f9901429df7b46ec17153aa3899f738adbea8e4356080da0c6e6586fb750d1"
"tokenizer/tokenizer.json 11423705 def76fb086971c7867b829c23a26261e38d9d74e02139253b38aeb9df8b4b50a"
) ;;
@@ -99,7 +105,18 @@ for MODEL in ${MODELS//,/ }; do
# 62 GiB fetch hit this on 2026-10-18).
url=$(curl -sfIL -o /dev/null -w '%{url_effective}' "$HF/$NPU_REPO/resolve/$NPU_REV/$f") \
|| { echo "cannot resolve $NPU_REPO/$f" >&2; exit 1; }
curl -fL --retry 5 -C - -o "$path" "$url"
# Resume ONLY into a genuinely partial file. A staged file of a
# DIFFERENT size is an artifact of another revision (a version
# bump re-cuts the device programs), and `-C -` against it
# appends the tail to the old bytes: the result is the right
# BYTE COUNT with the wrong content, which reads as a corrupt
# program, not as a fetch fault (measured 2026-10-30 on
# consts.hnpb). Fetch those from zero.
resume=()
if [ -f "$path" ] && [ "$(stat -c %s "$path")" -lt "$want" ]; then
resume=(-C -)
fi
curl -fL --retry 5 "${resume[@]+"${resume[@]}"}" -o "$path" "$url"
fi
fi
[ -f "$path" ] || { echo "MISSING $path (run without --verify-only)" >&2; exit 1; }
+5 -2
View File
@@ -10,8 +10,11 @@
# The .hgn format is the lock: it loads ONLY in the flash-server line, so the
# engine moves with the payload, never ahead of it.
# Image digest-pinned (a tag pull silently re-points; the digest is what was
# qualified — 0.16.2 is the version the model card names as its quickstart pin).
FRANK_DRAGOON_IMAGE=ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a
# qualified). 0.18.20 since 2026-10-30 (was 0.16.2, the model card's quickstart
# pin) — the vendor has moved two minor versions and the payload lock is what
# the deploy's health gate plus the boot ledger decide; rollback is the
# previous digest, 0c61bf84…, on this same line.
FRANK_DRAGOON_IMAGE=ghcr.io/peonist-ai/halogen-flash-server@sha256:e35133b206da3094cc4d70c78800b26235bd9124b9aeb0e029d4a7c153b9cff0
# Container identity kept from the 27B era: the unit's ExecStop names it.
FRANK_DRAGOON_CONTAINER=frank-dragoon-halogen
# Predecessor identity the serve script clears if it is still holding the GPUs.
+5 -4
View File
@@ -1,12 +1,12 @@
# frank-zealot — llama.cpp (PrismML fork build 10777, CUDA) on the RTX 3090,
# :7771. Payload: Ternary-Bonsai-2-9B PQ2_0 with the MTP head in the file, plus
# frank-zealot — llama.cpp (PrismML fork build 12118 = 0.7.0-dev, commit
# 024474448, CUDA) on the RTX 3090, :7771. Payload: Ternary-Bonsai-2-9B PQ2_0 with the MTP head in the file, plus
# the refusal-direction adapter applied at load.
#
# ONE OWNER: shape, payload identity, eras, laws and every measured number
# live in serverAppConfigs-docs/LLM/frank-zealot.md. This file carries the
# knobs and only the reason a reader editing THIS line cannot recover
# elsewhere. Where the two disagree, this file wins on values.
LLAMA_BIN=/home/dank/GitPackages/llama-prism-b10777/llama-server
LLAMA_BIN=/home/dank/GitPackages/llama-prism-0244744/bin/llama-server
# The fork is a LOCK, not a preference: custom tensor type 142 + prism.hadamard.*
# metadata. Stock llama.cpp refuses the file; a loader ignoring the Hadamard
# metadata dequantizes wrong, silently.
@@ -64,7 +64,8 @@ CACHE_TYPE_V=q8_0
# weights, so there is no --spec-draft-model. Its bill is pool × 4,096 B of
# draft KV plus ~0.45 GiB of buffers and a tripled recurrent state — paid for
# by CACHE_TYPE_V=q8_0 and UBATCH_SIZE=1024 above. Build gate: quantized V
# beside MTP needs fork build >= b10776; LLAMA_BIN is 10777.
# beside MTP needs fork build >= b10776; LLAMA_BIN is 12118 (b10777 is kept on
# disk as the rollback path).
# SPEC_TYPE=none is the rollback and strips the WHOLE set (--spec-type and
# every --spec-draft-*); half a set has segfaulted at load.
SPEC_TYPE=draft-mtp
+50 -114
View File
@@ -1,53 +1,44 @@
#!/usr/bin/env bash
# kld-gate-qwen38.sh — RUNS ON DANKSERVER (or anywhere the reports are). The campaign's
# acceptance gate: one verdict from the KLD reports of the containers that ride one reference
# recording.
# acceptance gate: three KLD reports against ONE reference recording, one verdict.
#
# The reference is the BF16 container built from the vendor checkpoint (run-qwen38-bf16.sh
# kldref, in the same drained window that records the calibration grams). Every scored
# container is measured against that one recording, so the KL scale runs from 0 = the
# checkpoint itself downward in faithfulness.
# checkpoint itself downward in faithfulness. Three containers ride it:
#
# TWO MODES, because the disk decides how many anchors can exist at once:
# fp8.json the published FP8-native release — the achievable end of the interval
# iq4r.json the payload frank-archon serves — the bar to beat
# cand.json the arm under test — the subject
#
# gate IQ4R.json CAND.json candidate vs the served payload
# gate FP8.json IQ4R.json CAND.json the same, inside the absolute interval
# The FP8 number is what makes the interval absolute instead of relative: "half the gap"
# is half of the distance between the 4-bit payload and the near-lossless release, not half
# of an invented distance to the checkpoint. That is also why the FP8 payload is FETCHED and
# scored, never converted: it is a second opinion about what this checkpoint tolerates, not a
# step of the campaign. It lands LAST (disk sequencing) — by then the 335 GiB bf16 container
# is gone, so its 172.8 GiB fits.
#
# iq4r.json is the payload frank-archon serves — the bar to beat. When the FP8-native release
# (172.8 GiB, fetched last because of the peak-disk law) is on disk, pass its report first:
# "half the gap" is then half of the distance between the 4-bit payload and the near-lossless
# release, not half of an invented distance to the checkpoint. Without it the gate still
# answers the campaign's question — does the candidate beat what is served — and says so.
#
# The law, on the summary of one slice (--slice all | a corpus source):
#
# candidate vs served (2 reports)
# 1. mean KL(cand) < mean KL(iq4r) the served payload is beaten on the corpus
# 2. p99.9(cand) <= p99.9(iq4r) and the tail is no worse — a mean can hide a few
# positions taking the whole hit
# 3. top-1(cand) >= top-1(iq4r) the agreement the harness keys on does not fall
# 4. every source slice passes 1-3 a win bought by regressing one distribution
# (code, prose, tools) is not a win
#
# inside the interval (3 reports), laws 1-4 above (1-3 renumbered to the interval and the
# recovery fraction, as agreed 2026-10-27) plus:
# 5. p99.9(cand) <= p99.9(iq4r) the tail law
# 6. every source slice passes the slice law
# The law, in order of severity, on the mean KL of one slice (--slice all | a corpus source):
# 1. KL(fp8) < KL(iq4r) the anchors are ordered. If the near-lossless
# release does NOT sit closer to the checkpoint than
# the 4-bit payload, the corpus, the tokenizer or the
# boot shape is wrong and no arm number means anything.
# 2. KL(fp8) < KL(cand) < KL(iq4r) the candidate lies strictly inside the interval —
# outside it, something is mis-measured, not better
# 3. KL(cand) <= KL(iq4r) - R x gap it closes at least R (--recover, default 0.5) of the
# gap = KL(iq4r) - KL(fp8): at least half the way from
# what is served toward the release (agreed 2026-10-27)
# 4. top-1(cand) >= top-1(iq4r) the agreement the harness keys on does not fall
#
# Booting the containers is NOT this script's job: `run-qwen38-bf16.sh score RAD OUT.json`
# writes the reports and owns the single engine argv (same pin, --tp 2, --kv-cache-dtype fp8,
# same step size, --deterministic, no speculation) that makes the numbers comparable.
# same step size, --deterministic, no speculation) that makes the three numbers comparable.
# This script is arithmetic, so it re-runs against old reports and is testable without a GPU.
#
# Usage: kld-gate-qwen38.sh IQ4R.json CAND.json [--slice NAME]
# kld-gate-qwen38.sh FP8.json IQ4R.json CAND.json [--slice NAME] [--recover F]
# Usage: kld-gate-qwen38.sh FP8.json IQ4R.json CAND.json [--slice NAME] [--recover F]
set -uo pipefail
if [ "$#" -lt 2 ]; then
echo "usage: kld-gate-qwen38.sh IQ4R.json CAND.json [--slice NAME]" >&2
echo " kld-gate-qwen38.sh FP8.json IQ4R.json CAND.json [--slice NAME] [--recover F]" >&2
exit 2
fi
[ "$#" -ge 3 ] || { echo "usage: kld-gate-qwen38.sh FP8.json IQ4R.json CAND.json [--slice NAME] [--recover F]" >&2; exit 2; }
python3 - "$@" <<'PY'
import json, sys
@@ -61,15 +52,13 @@ while i < len(a):
else:
files.append(a[i])
i += 1
if len(files) not in (2, 3):
sys.exit("usage: kld-gate-qwen38.sh IQ4R.json CAND.json [--slice NAME]\n"
" kld-gate-qwen38.sh FP8.json IQ4R.json CAND.json [--slice NAME] [--recover F]")
if len(files) != 3:
sys.exit("usage: kld-gate-qwen38.sh FP8.json IQ4R.json CAND.json [--slice NAME] [--recover F]")
o = dict(zip(opts[0::2], opts[1::2]))
slice_ = o.get("--slice", "all")
recover = float(o.get("--recover", 0.5))
if not 0.0 < recover <= 1.0:
sys.exit(f"--recover must be in (0,1], got {recover}")
interval = len(files) == 3
def die(msg):
@@ -77,15 +66,11 @@ def die(msg):
raise SystemExit(2)
def load(path):
def read(path):
try:
return json.load(open(path))
d = json.load(open(path))
except Exception as e:
die(f"cannot read the KLD report {path}: {e}")
def summary(path, d):
"""The slice this gate grades: 'all', or one corpus source."""
if slice_ == "all":
g = d.get("all")
else:
@@ -95,25 +80,23 @@ def summary(path, d):
f"{', '.join(sorted(d.get('by_source') or {})) or '(none)'}")
if g is None or "kld" not in g or "top1_agreement" not in g:
die(f"{path} has no {slice_!r} summary — wrong report version?")
return g
return d, g
docs = {label: load(path) for label, path in
zip(("fp8", "iq4r", "cand") if interval else ("iq4r", "cand"), files)}
rows = []
for label, path in zip(("fp8", "iq4r", "cand") if interval else ("iq4r", "cand"), files):
g = summary(path, docs[label])
rows.append((label, path, docs[label].get("model", "?"), g["kld"]["mean"],
g["kld"]["p99_9"], g["top1_agreement"], g["positions"]))
for label, path in zip(("fp8", "iq4r", "cand"), files):
d, g = read(path)
rows.append((label, path, d.get("model", "?"), g["kld"]["mean"], g["kld"]["p99_9"],
g["top1_agreement"], g["positions"]))
print(f"reference: BF16 container; slice: {slice_}; "
+ ("inside the FP8 interval" if interval else "candidate vs served (no FP8 anchor)"))
print(f"reference: BF16 container; slice: {slice_}")
print(f"{'':6} {'model':38} {'KL mean':>10} {'KL p99.9':>10} {'top-1':>8} positions")
for label, path, model, kl, p999, top1, n in rows:
print(f"{label:6} {str(model)[:38]:38} {kl:10.6f} {p999:10.4f} {100*top1:7.2f}% {n}")
r = {label: (kl, p999, top1) for label, _p, _m, kl, p999, top1, _n in rows}
kl_cand, p_cand, t_cand = r["cand"]
kl_iq4r, p_iq4r, t_iq4r = r["iq4r"]
r = {label: (kl, top1) for label, _p, _m, kl, _q, top1, _n in rows}
kl_fp8, kl_iq4r, kl_cand = r["fp8"][0], r["iq4r"][0], r["cand"][0]
t_iq4r, t_cand = r["iq4r"][1], r["cand"][1]
gap = kl_iq4r - kl_fp8
ok = True
@@ -123,64 +106,17 @@ def law(n, text, passed):
print(f" [{'PASS' if passed else 'FAIL'}] {n}. {text}")
def tail_law(n):
law(n, f"candidate KL p99.9 {p_cand:.4f} <= served iq4r {p_iq4r:.4f}",
p_cand <= p_iq4r)
# --- the slice law: every corpus source must pass the metric laws -------------
def slice_law(n):
"""A win that regresses one source class is not a win: check them all."""
mine = docs["cand"].get("by_source") or {}
theirs = docs["iq4r"].get("by_source") or {}
if not mine or not theirs:
law(n, "no per-source summaries to check", True)
return
missing = sorted(set(theirs) - set(mine))
extra = sorted(set(mine) - set(theirs))
if missing or extra:
law(n, "same source slices in both reports"
+ (f" (missing from cand: {', '.join(missing)})" if missing else "")
+ (f" (only in cand: {', '.join(extra)})" if extra else ""), False)
return
bad = []
for src in sorted(theirs):
c, q = mine[src], theirs[src]
worse_mean = c["kld"]["mean"] >= q["kld"]["mean"]
worse_tail = c["kld"]["p99_9"] > q["kld"]["p99_9"]
worse_top1 = c["top1_agreement"] < q["top1_agreement"]
if worse_mean or worse_tail or worse_top1:
why = ",".join(m for m, w in (("mean", worse_mean), ("p99.9", worse_tail),
("top-1", worse_top1)) if w)
bad.append(f"{src} ({why})")
law(n, "every source slice passes (mean, p99.9, top-1)"
+ (f" — regressed: {'; '.join(bad)}" if bad else ""), not bad)
if interval:
kl_fp8 = r["fp8"][0]
gap = kl_iq4r - kl_fp8
law(1, f"anchors ordered: fp8 {kl_fp8:.6f} < iq4r {kl_iq4r:.6f} (gap {gap:.6f})",
gap > 0)
law(2, f"candidate inside the interval: {kl_fp8:.6f} < {kl_cand:.6f} < {kl_iq4r:.6f}",
kl_fp8 < kl_cand < kl_iq4r)
need = kl_iq4r - recover * gap
law(3, f"candidate closes {100*max((kl_iq4r - kl_cand)/gap, 0):.1f}% of the "
f"{gap:.6f} gap (needs {100*recover:.0f}%, i.e. KL <= {need:.6f})"
if gap > 0 else "gap is not positive; cannot score recovery",
gap > 0 and kl_cand <= need)
law(4, f"candidate top-1 {100*t_cand:.2f}% >= served iq4r top-1 {100*t_iq4r:.2f}%",
t_cand >= t_iq4r)
tail_law(5)
slice_law(6)
else:
law(1, f"candidate beats the served payload: KL {kl_cand:.6f} < iq4r {kl_iq4r:.6f}",
kl_cand < kl_iq4r)
tail_law(2)
law(3, f"candidate top-1 {100*t_cand:.2f}% >= served iq4r top-1 {100*t_iq4r:.2f}%",
t_cand >= t_iq4r)
slice_law(4)
law(1, f"anchors ordered: fp8 {kl_fp8:.6f} < iq4r {kl_iq4r:.6f} (gap {gap:.6f})",
gap > 0)
law(2, f"candidate inside the interval: {kl_fp8:.6f} < {kl_cand:.6f} < {kl_iq4r:.6f}",
kl_fp8 < kl_cand < kl_iq4r)
need = kl_iq4r - recover * gap
law(3, f"candidate closes {100*max((kl_iq4r - kl_cand)/gap, 0):.1f}% of the "
f"{gap:.6f} gap (needs {100*recover:.0f}%, i.e. KL <= {need:.6f})"
if gap > 0 else "gap is not positive; cannot score recovery",
gap > 0 and kl_cand <= need)
law(4, f"candidate top-1 {100*t_cand:.2f}% >= served iq4r top-1 {100*t_iq4r:.2f}%",
t_cand >= t_iq4r)
print("KLD GATE " + ("PASS" if ok else "FAIL"))
sys.exit(0 if ok else 1)
PY
-122
View File
@@ -1,122 +0,0 @@
#!/usr/bin/env bash
# qwen38-calib-window.sh — RUNS ON DANKSERVER. The calibration window, and nothing else:
# boot the BF16 rig, wait until it is actually serving, push the corpus until the engine says
# the tap is full, stop the rig, restore frank-archon, and bank the reference container.
#
# The reference recording is already taken (kld-ref/kld.json exists), so this window is the
# calibration tap alone — the one job that needs both cards after it. The kldref phase is NOT
# re-run here: the chained launcher that used to do that moved the banked kld-ref into trash
# first, which is how a 16-hour phase gets paid for twice.
#
# Every phase is gated on the previous one, because the last time this ran unguarded
# (2026-10-29) the rig's boot died of an OOM, the health loop quietly fell through, and the
# pusher fed four rounds of 10M tokens into RESTORED PRODUCTION with nothing recorded:
#
# 1. the runner must still be alive and its container must answer /health, or this script
# kills it, restores the service and exits non-zero — no push, ever;
# 2. the pusher refuses on its own unless the rig wrote its marker into the tap dir, the
# marker names this container, and the container is running (scripts/calib-push.py
# --tap-marker), so a second lock survives a mistake in this file;
# 3. the reference container is banked into trash only when BOTH the serve and the push
# exited 0 and the tap dir holds grams. The checkpoint that reproduces it stays.
#
# knobs: Q38_WINDOW_CALIB_CORPUS (calib-7168.jsonl — the re-chunked file the rig's pool can
# hold), Q38_WINDOW_HEALTH_TIMEOUT (seconds to wait for /health), Q38_WINDOW_ROUNDS
# (pusher rounds; one is 10M tokens), Q38_WINDOW_KEEP_RAD=1 to leave the .rad in place.
# Usage: scripts/qwen38-calib-window.sh [--dry-run]
set -uo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")/.." || exit 9
DRY=0
[ "${1:-}" = "--dry-run" ] && DRY=1
say() { printf '%s %s\n' "$(date -Is)" "$*"; }
MODELS="${FRANK_QWEN38_MODELS:-/home/dank/models}"
CAMPAIGN="${FRANK_QWEN38_CAMPAIGN:-/home/dank/campaign/qwen38-w5nl64}"
RAD="${FRANK_QWEN38_RAD:-$MODELS/rad/qwen38-bf16.rad}"
CORPUS="${Q38_WINDOW_CALIB_CORPUS:-/home/dank/corpora/out/calib-7168.jsonl}"
KLDREF="$CAMPAIGN/kld-ref/kld.json"
CALIB="$CAMPAIGN/calib"
HEALTH_TIMEOUT="${Q38_WINDOW_HEALTH_TIMEOUT:-3600}"
ROUNDS="${Q38_WINDOW_ROUNDS:-6}"
RIG=qwen38-bf16
SERVICE=frank-archon
PY="${PY:-python3}"
# ---- preflight: everything this window needs, before a card is touched -------
[ -f "$KLDREF" ] || { echo "REFUSED: no reference recording at $KLDREF — record it first (scripts/run-qwen38-bf16.sh kldref); this window only fills the calibration tap" >&2; exit 2; }
[ -f "$RAD" ] || { echo "REFUSED: no BF16 reference container at $RAD — nothing to serve; scripts/build-qwen38-bf16-rad.sh" >&2; exit 2; }
[ -f "$CORPUS" ] || { echo "REFUSED: no calibration corpus at $CORPUS" >&2; exit 2; }
[ -d "$CALIB" ] || { echo "REFUSED: no tap dir at $CALIB" >&2; exit 2; }
[ -w "$CALIB" ] || { echo "REFUSED: tap dir $CALIB is not writable by $(id -un)" >&2; exit 2; }
! [[ "$HEALTH_TIMEOUT" =~ ^[0-9]+$ ]] && { echo "REFUSED: Q38_WINDOW_HEALTH_TIMEOUT must be seconds (got: $HEALTH_TIMEOUT)" >&2; exit 2; }
! [[ "$ROUNDS" =~ ^[0-9]+$ ]] && { echo "REFUSED: Q38_WINDOW_ROUNDS must be a round count (got: $ROUNDS)" >&2; exit 2; }
if [ "$DRY" = 1 ]; then
echo "dry run: calib window — the service is left running, nothing is mounted or written"
echo "would run: bash scripts/run-qwen38-bf16.sh calib (rig boots, writes $CALIB/.rig-boot)"
echo "would wait: /health up within ${HEALTH_TIMEOUT}s, else kill the rig, restore $SERVICE, exit 1"
echo "would run: $PY scripts/calib-push.py --container $RIG --corpus $CORPUS --max-rounds $ROUNDS"
echo "would bank: $RAD -> ~/.AGENTTRASH/$(date +%Y%m%d)-qwen38-bf16-rad/ (only if both phases exit 0)"
exit 0
fi
say "calib window start pid=$$ corpus=$CORPUS rounds=$ROUNDS"
[ -n "$(ls -A "$CALIB" 2>/dev/null)" ] && say "tap dir $CALIB is not empty; the engine's token floor is pooled, so an earlier partial tap still counts"
# ---- phase 1: the rig -------------------------------------------------------
say "phase=calib serve begin"
bash scripts/run-qwen38-bf16.sh calib & spid=$!
rig_up() {
podman inspect -f '{{.State.Running}}' "$RIG" 2>/dev/null | grep -qx true \
&& curl -sf --max-time 10 -o /dev/null http://127.0.0.1:7770/health
}
waited=0
until rig_up; do
if ! kill -0 "$spid" 2>/dev/null; then
wait "$spid"; crc=$?
say "phase=calib serve end rc=$crc (the rig exited before it ever answered /health)"
echo "FAIL: the rig boot died (rc=$crc) — nothing was pushed; read /home/dank/logs/qwen38_bf16_calib.log" >&2
exit 1
fi
if [ "$waited" -ge "$HEALTH_TIMEOUT" ]; then
say "phase=calib health never came after ${HEALTH_TIMEOUT}s; killing the rig"
podman stop "$RIG" >/dev/null 2>&1 || true
wait "$spid"
echo "FAIL: the rig never answered /health within ${HEALTH_TIMEOUT}s — nothing was pushed" >&2
exit 1
fi
sleep 15
waited=$((waited + 15))
done
say "phase=calib health ok waited=${waited}s"
# ---- phase 2: the tap -------------------------------------------------------
say "phase=calib push begin"
"$PY" scripts/calib-push.py --port 7770 --parallel 8 --container "$RIG" \
--tap-dir "$CALIB" --corpus "$CORPUS" --max-rounds "$ROUNDS"
prc=$?
say "phase=calib push end rc=$prc"
podman stop "$RIG" >/dev/null 2>&1 || true
wait "$spid"; crc=$?
say "phase=calib serve end rc=$crc"
if [ "$prc" -ne 0 ] || [ "$crc" -ne 0 ]; then
echo "FAIL: serve rc=$crc push rc=$prc — the tap is NOT banked and $RAD stays: $(basename "$RAD")" >&2
exit 1
fi
[ -n "$(ls -A "$CALIB" 2>/dev/null)" ] || { echo "FAIL: both phases exited 0 but the tap dir $CALIB is empty" >&2; exit 1; }
say "TAP BANKED: $(ls -1 "$CALIB" | wc -l) file(s) in $CALIB"
# ---- phase 3: bank the reference container ----------------------------------
if [ "${Q38_WINDOW_KEEP_RAD:-0}" = "1" ]; then
say "reference .rad kept (Q38_WINDOW_KEEP_RAD=1)"
else
T="$HOME/.AGENTTRASH/$(date +%Y%m%d)-qwen38-bf16-rad"
mkdir -p "$T" || { echo "FAIL: cannot create trash dir $T" >&2; exit 1; }
mv "$RAD" "$T/" && say "reference .rad banked in $T (the checkpoint that rebuilds it stays)"
fi
say "calib window end"
+3 -59
View File
@@ -92,8 +92,7 @@
# the container reads through /models), FRANK_QWEN38_LOG_DIR, and the step
# and slot counts inherited as FRANK_ARCHON_MAX_BATCHED_TOKENS /
# FRANK_ARCHON_MAX_SEQS — no per-mode literals (the audit law: a mode that
# carries its own step measures a shape nothing serves; calib is exempt, see
# Q38_CALIB_MNBT / Q38_CALIB_SEQS below — its output is never compared) —
# carries its own step measures a shape nothing serves) —
# and FRANK_QWEN38_MAX_MODEL_LEN — the window every mode boots at (32,768;
# see the corpus census above, and raise it together with the corpus).
# kldref refuses (exit 2) when the longest record of the corpus it is about to
@@ -186,24 +185,6 @@ if ! [[ "$MNBT" =~ ^[0-9]+$ ]]; then
exit 2
fi
# Calib's shape is NOT a measuring shape. kldref and score produce KL numbers that are only
# comparable across boots at an identical shape, which is why they inherit production's step and
# slots to the letter. The tap's output is layer Hessians, never a number any report compares, so
# its step and slot count are its own — and they are the tap's dominant cost: a 7,168-token
# document cut into four 2,048-token steps re-streams each layer's routed expert union four
# times from the container, so one wide step is worth up to ~4x the read volume. Measured
# 2026-10-29 at the inherited 2,048/4: prefill 71 tok/s and a 47-hour tap; the probe exists to
# find out what the wide step is worth before the campaign pays for two days of drained serving.
# Both default to the inherited value, so nothing moves unless a probe asks it to.
CALIB_MNBT="${Q38_CALIB_MNBT:-$MNBT}"
CALIB_SEQS="${Q38_CALIB_SEQS:-$MAX_SEQS}"
for pair in "Q38_CALIB_MNBT=$CALIB_MNBT" "Q38_CALIB_SEQS=$CALIB_SEQS"; do
if ! [[ "${pair#*=}" =~ ^[0-9]+$ ]]; then
echo "REFUSED: ${pair%%=*} must be a number (got: ${pair#*=})" >&2
exit 2
fi
done
# The window every mode boots at. Over-length is a REFUSAL, not a truncation (the
# env file says so for production), so this is the number the corpus must fit in.
MAX_MODEL_LEN="${FRANK_QWEN38_MAX_MODEL_LEN:-32768}"
@@ -212,25 +193,6 @@ if ! [[ "$MAX_MODEL_LEN" =~ ^[0-9]+$ ]]; then
exit 2
fi
# Where the n-gram table lives. On the bf16 payload it is 95.37 GiB and `ram`
# PINS it: the calibration boot died at "startup stopped: out of memory" with
# 230.22 GiB pinned of 246.07 GiB and 8.06 GiB available (measured 2026-10-29),
# while the reference recording and every score boot had already succeeded with
# the same pool and slots. The difference that matters is the job: calib only
# needs the routed weights prefilled, so it reads the table from the container's
# own file tier and the pin never happens; kldref and score keep production's
# placement, because their numbers are only comparable when every other term of
# the shape is identical. FRANK_QWEN38_NGRAM_PLACEMENT overrides either default.
case "$MODE" in
calib) NGRAM_PLACEMENT="${FRANK_QWEN38_NGRAM_PLACEMENT:-disk}" ;;
*) NGRAM_PLACEMENT="${FRANK_QWEN38_NGRAM_PLACEMENT:-ram}" ;;
esac
case "$NGRAM_PLACEMENT" in
auto|vram|ram|disk) ;;
*) echo "REFUSED: FRANK_QWEN38_NGRAM_PLACEMENT must be auto|vram|ram|disk (got: $NGRAM_PLACEMENT)" >&2
exit 2 ;;
esac
# Every container this campaign boots is read through the ro /models mount, so
# the host path must live under it; the container-side spelling is what goes
# into the argv. Same rule for the container a score pass opens.
@@ -268,7 +230,7 @@ ARGS=(--tp 2 --placement expert_tiered --weights-disk-tier
--host-pool-mib "$HOST_POOL_MIB" --gpu-headroom-mib "$GPU_HEADROOM_MIB"
--deterministic --num-speculative-tokens 0
--kv-cache-dtype fp8 --max-model-len "$MAX_MODEL_LEN"
--ngram-placement "$NGRAM_PLACEMENT"
--ngram-placement ram
--override-chat-template /models/tpl.jinja)
MOUNTS=(-v "$MODELS:/models:ro")
TAPENV=()
@@ -315,7 +277,7 @@ case "$MODE" in
# VRAM knob. The slots follow frank-archon.env's FRANK_ARCHON_MAX_SEQS
# through MAX_SEQS, exactly as kldref and score do.
ARGS+=(--model "$MODEL_IN" --port 7770
--max-num-seqs "$CALIB_SEQS" --max-num-batched-tokens "$CALIB_MNBT")
--max-num-seqs "$MAX_SEQS" --max-num-batched-tokens "$MNBT")
# The tap is only real if this directory exists and the engine can write it:
# without the check a typo'd CALIB_DIR auto-creates a root-owned empty dir in
# whatever path was given, and the run burns hours serving prompts into a tap
@@ -327,17 +289,6 @@ case "$MODE" in
MOUNTS+=(-v "$CALIB_DIR:/data/calib")
TAPENV=(-e RADIANCE_CALIB_DIR=/data/calib
-e "RADIANCE_CALIB_TOKENS=${FRANK_QWEN38_CALIB_TOKENS:-12000000}")
# The tap's own authorization. A pusher must see this marker before it sends
# a prompt (calib-push.py --tap-marker), it names the container so a push
# aimed anywhere else refuses, and a boot that fails removes it again below.
# Measured 2026-10-29: with no marker, the chain pushed 4 rounds x 10M tokens
# into RESTORED PRODUCTION after the rig's boot died, and nothing recorded.
if [ "$DRY" = 1 ]; then
:
else
printf 'container=%s\nimage=%s\nmode=calib\nstarted=%s\npid=%s\n' \
"$C" "$IMAGE" "$(date -Is)" "$$" > "$CALIB_DIR/.rig-boot"
fi
;;
esac
@@ -426,12 +377,5 @@ podman rm -f "$C" >/dev/null 2>&1 || true
"${CMD[@]}" 2>&1 | tee "$LOG"
rc=${PIPESTATUS[0]}
# A boot that failed must not leave the tap marked as rig-authorized: the marker
# is what calib-push.py trusts, and the point of it is that a dead rig can never
# be pushed into again.
if [ "$MODE" = calib ] && [ "$rc" -ne 0 ] && [ -f "$CALIB_DIR/.rig-boot" ]; then
mv -f "$CALIB_DIR/.rig-boot" "$CALIB_DIR/.rig-boot.failed-$(date +%Y%m%d-%H%M%S)" 2>/dev/null || true
echo "the rig boot failed (rc=$rc): the tap marker is retired, so no pusher can take this tap for a live rig"
fi
echo "run-qwen38-bf16 $MODE exited rc=$rc; $SERVICE restored; log: $LOG"
exit "$rc"
+1 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# serve-frank-dragoon.sh — frank-dragoon endpoint: halogen-flash-server 0.16.2
# serve-frank-dragoon.sh — frank-dragoon endpoint: halogen-flash-server 0.18.20
# (closed-source engine, image ghcr.io/peonist-ai/halogen-flash-server,
# digest-pinned in frank-dragoon.env) serving Quat3rnion's NATIVE v2
# conversion (2026-10-11) of the abliterated Qwen3.8-Flash-Next on Dankclaw's
+3 -3
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Serve prism-ml/Ternary-Bonsai-2-9B-gguf (PQ2_0 ternary quant WITH the MTP head
# in the file) as frank-zealot on Dankclaw's RTX 3090 via llama.cpp (PrismML
# vendor build 10777, CUDA sm_86).
# vendor build 12118 (0.7.0-dev, commit 024474448), CUDA sm_86).
#
# 2026-10-18 (Frank): Bonsai-2 27B PQ2_0 → the 9B MTP export, then the same
# evening TWO 262,144-token conversations (--ctx-size 524288 --parallel 2) over
@@ -78,7 +78,7 @@ if [[ -f "$ENV_FILE" ]]; then
fi
# --- defaults (used when the env file does not set them) -------------------
: "${LLAMA_BIN:=/home/dank/GitPackages/llama-prism-b10777/llama-server}"
: "${LLAMA_BIN:=/home/dank/GitPackages/llama-prism-0244744/bin/llama-server}"
: "${MODEL_FILE:=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0.gguf}"
: "${ALIAS:=frank-zealot}"
: "${BIND_ADDR:=0.0.0.0}"
@@ -119,7 +119,7 @@ fi
# above are what pay for it. The draft cache dtype IS a further knob
# (--spec-draft-type-k / --spec-draft-type-v, both default f16 — verified in this
# build's --help) and stays at its default here. Card gate: quantized V alongside
# MTP needs fork build >= prism-b10776, which is why LLAMA_BIN is the 10777
# MTP needs fork build >= prism-b10776; LLAMA_BIN is 12118 (b10777 kept as rollback)
# prebuilt; the type-142 loader lock keeps it pinned regardless.
# SPEC_TYPE=none is the rollback and strips the whole set (flags move together).
: "${SPEC_TYPE:=draft-mtp}"
@@ -10,7 +10,7 @@ timestamp: 2026-09-03
## Server Overview
Dankclaw is an AMD APU + RTX 3090 inference host. It ran CIRU ROCmFPX llama.cpp inference through 2026-08-22 ([[frank-dragoon]], retired). The host runs Ubuntu 26.04 with an AMD Ryzen AI MAX+ 395 Strix Halo APU. The APU uses unified memory — CPU and GPU share the same physical RAM pool. Today it serves [[frank-dragoon]] (halogen-flash-server 0.16.2 on the iGPU, port 7770) and [[frank-zealot]] (llama.cpp PrismML fork on the RTX 3090, port 7771).
Dankclaw is an AMD APU + RTX 3090 inference host. It ran CIRU ROCmFPX llama.cpp inference through 2026-08-22 ([[frank-dragoon]], retired). The host runs Ubuntu 26.04 with an AMD Ryzen AI MAX+ 395 Strix Halo APU. The APU uses unified memory — CPU and GPU share the same physical RAM pool. Today it serves [[frank-dragoon]] (halogen-flash-server 0.18.20 on the iGPU, port 7770) and [[frank-zealot]] (llama.cpp PrismML fork on the RTX 3090, port 7771).
## Hardware Specifications
### CPU
@@ -105,7 +105,7 @@ cable between claw and server is required for 10G.
- Unified memory eliminates separate VRAM limits. The GPU uses system RAM.
- The GPU budget is the GTT aperture: ~114 GiB (`mem_info_gtt_total` in `/sys/class/drm/card1/device/` = 122,402,963,456 B; `card1` is the amdgpu iGPU, `PCI_ID=1002:1586`; `llama-server --list-devices` reports 116,732 MiB). rocm-smi's smaller SVM/dedicated carve-out is **not** the budget — the 2026-08 "64 GiB rocm-smi" lesson is stale. Do not use `free` or `dmidecode` for the GPU budget.
- The active services are `frank-dragoon` on 7770 — **halogen-flash-server 0.16.2** serving the native v2 abliterated `.hgn` in a rootless podman container ([[frank-dragoon]]) — and `frank-zealot` on 7771 ([[frank-zealot]]). The gufo v0.1.1 / UD-Q8_K_XL runtime this page once described is retired (2026-09-28 → 10-01, [[frank-dragoon]]); the gufo build tree is still on disk at `/home/dank/GitPackages/gufo-v0.1.1/`.
- The active services are `frank-dragoon` on 7770 — **halogen-flash-server 0.18.20** serving the native v2 abliterated `.hgn` in a rootless podman container ([[frank-dragoon]]) — and `frank-zealot` on 7771 ([[frank-zealot]]). The gufo v0.1.1 / UD-Q8_K_XL runtime this page once described is retired (2026-09-28 → 10-01, [[frank-dragoon]]); the gufo build tree is still on disk at `/home/dank/GitPackages/gufo-v0.1.1/`.
- The gufo-era note that the active runtime links against the host ROCm (`/opt/rocm` → `/etc/alternatives/rocm` → 7.2.4) through its RPATH belongs to that retired source build. The serving runtimes do not depend on the host ROCm that way: halogen runs inside its digest-pinned image, and `frank-zealot` is a CUDA build on the 3090. The retired CIRU venv's TheRock 7.15 SDK (`LD_LIBRARY_PATH`) was never in the serving path either.
- The retired vLLM venvs (`/home/dank/vllm-venv`, `/home/dank/vllm-vk-venv`) were moved to `/home/dank/.AGENTTRASH/` on 2026-08-18.
- 424 GB free on `/` (`df -h`, 2026-10-18) is ample for model storage.
@@ -1,287 +0,0 @@
---
type: "howto"
title: "Radiance Architecture Plugins — declaring a model"
description: "Writing a radiance architecture plugin: declaring weights, buffers, KV groups and ops, then issuing the step."
tags:
- llm
- radiance
- plugins
- howto
timestamp: 2026-10-30
---
# Radiance architecture plugins
An architecture plugin declares a model in two phases: `rad_arch_declare` names every weight,
buffer, KV group and op **once**, against the model's metadata; `rad_arch_step` issues the ops for
one step. The engine picks the plugin by the exact `(rad_arch_id, rad_arch_quant)` pair — no
fallback — so one family at two weight formats is two `.so` files, and branching on a quant
descriptor inside one plugin is the anti-pattern the split exists to prevent.
The in-tree plugins are the templates: `arch/llama_fp8/llama_fp8.cpp` (the smallest complete
decoder), `arch/gemma3`, `arch/qwen35_bf16`, and the header-only blocks in `arch/common/`
(`rad_block_attn*.h`, `rad_block_mlp*.h`, `rad_block_moe_fp8.h`, `rad_block_gdn*.h`,
`rad_block_dflash2.h`, `rad_block_mtp*.h`, `rad_block_vit.h`, `rad_block_ple.h`,
`rad_block_glm_*.h`, `rad_qsa.h`, …). Include those by `<arch/rad_arch.h>` — they are installed
beside the ABI and the in-tree plugins include them by that spelling.
## Exports
```c
const char* rad_arch_id(void); /* "llama", "qwen35", ... */
const char* rad_arch_quant(void); /* "" unquantised, else the descriptor served */
int rad_arch_serves_imports(void); /* optional: 0 declines an imported quantisation */
int rad_arch_probe(const RadModelMeta* meta, RadArchProbe* out); /* optional */
int rad_arch_declare(RadBuilder* b, const RadModelMeta* meta, const RadBuildCtx* ctx);
void rad_arch_step(struct RadCtx* c, const struct RadBatch* batch);
const RadChatFormat* rad_arch_chat_format(const RadModelMeta* meta); /* optional */
const RadAudioFormat* rad_arch_audio_format(const RadModelMeta* meta); /* optional */
```
Everything but declare/step is optional. `RAD_ARCH_PLUGIN(NS, ID, QUANT, VERSION, DESC)` in
`arch/common/rad_arch.h` emits the whole export block, including `rad_plugin_info` (kind
`RAD_PLUGIN_ARCH`). The plugin's **name must be unique**; the architecture id may repeat.
`rad_arch_serves_imports` exists because a checkpoint whose quantisation was imported weight by
weight (Quark, ParoQuant, GPTQ, NVFP4) names its producing tool, which no plugin claims: the family's
one quantised plugin serves it, and two are refused by name. A second quantised plugin that reads
only its own containers returns 0.
## Declare: the metadata
`RadModelMeta` is what the loader read from the container's header before any plugin ran: `arch_id`,
`name`, `quant`, `n_layers`, `n_embd`, `n_head`, `n_head_kv`, **`head_dim`**, `n_ff`, `n_vocab`,
`n_ctx_train`, expert counts, `rms_eps`, `rope_theta`, and a free-form `kv_key`/`kv_val` table for
everything else (`rad_meta_geti/getf/gets`). Read dimensions from here — one plugin covers every size
in a family.
**`head_dim` is read, never derived.** On the Qwen hybrids it is 256 while `hidden / heads` is 213;
a derived value silently breaks projection widths, KV page size and the attention kernel's band.
`RadBuildCtx` is the deployment the plugin must declare against:
| field | meaning |
|---|---|
| `rank`, `world_size` | declare runs once per rank; **dimensions are already divided** — the core inserts no collectives |
| `max_tok`, `max_seqs`, `max_ctx`, `max_spec` | the step bounds: buffer sizing, kernel bands, the scheduler's window |
| `max_mixed_tok` | the most a multi-sequence step may carry; a wider step is one sequence (prefill span) |
| `tp_wire_lossy`, `tp_wire_min_bytes` | invert into `all_reduce`'s `exact`; below the floor a message must go exact |
| `kv_dtype`, `kv_quant` | the attention cache width and, under fp8, its narrow layout (`RAD_KVQ_V4`, `RAD_KVQ_K6V4`) |
| `attn_prefill_fp8` | `RAD_ATTN_FP8_*` bits to pass as `attn_paged`'s `prefill_fp8` |
| `kv_resident_blocks`, `kv_stream_ok` | KV streaming: the ring depth, and permission to declare the streamed-only ops |
| `state_dtype` | `RAD_F16` when `--state-cache-dtype f16` is served by this plugin |
| `is_draft`, `scope` | a drafter/encoder declare; `scope` prefixes declared names (`draft`, `vision`, `audio`) |
| `shape_probe` | a **sizing** declare at a smaller `max_tok`; must not touch state the real declare wrote |
| `max_enc_patches`, `max_audio_frames` | the encoder pass sizes |
| `max_out_rows` | logits rows the KL mode needs (0 = the plugin's own rule) |
| `cvec` | the loaded control vector, or null |
| `tp_attn_rows` | row-split prefill attention requested |
| `draft_coupling`, `draft_tree` | declare the coupled/tree forms |
| `weight_prefetch_off` | declare no weight prefetch beside collectives |
## Declare: weights
```c
static const RadWeightDecl d = {
.dtype = RAD_BF16, .rank = 2, .shape = {n_embd, n_ff},
.access = RAD_ACCESS_PER_TOKEN, .shard = RAD_SHARD_ROW,
.group = grp_layer(l), .optional = 0,
.n_row_parts = 0, .source = nullptr, .planes = nullptr,
};
rad_weight w = rad_decl_weight(b, "blk.0.mlp.gate.weight", &d);
```
- **`access`** tells the placement planner how the weight is read: `PER_TOKEN` (dense),
`PER_REQUEST` (an encoder tower, read once per request), `CONDITIONAL` (routed experts),
`RARE`, `VOCAB` (lm_head/embedding: read every token but huge, and one is a gather).
- **`shard`**: `NONE` (replicated), `ROW` (split dim 0), `COL` (split dim 1, needs a following
all-reduce). The plugin says; the core does not infer. Uneven splits use
`rad_weight_shard_span`/`_parts` (a width that does not divide into whole 128-column blocks; the
delta net's `[q|k|v|z]` of unequal part widths).
- **`group`** is the unit of movement (`layer`, `expert`, `slot`); an expert's gate/up/down share one,
because the unit of transfer is an expert, not a tensor.
- **`row_parts`** describes what a `SHARD_ROW` weight stacks along dim 0 (`[gate|up]`,
`[q|k|v|z]`). **A tensor-parallel rank owns a slice of each stacked projection**, never a
contiguous slice of the stack; the parts cannot be inferred from a fused container tensor, and a
contiguous slice produces fluent wrong text.
- **`source`/`planes`** view an existing stored weight: a block-fp8 linear's `codes,scale` are one
logical weight taken by a GEMM as two operands. Null `source` is the weight of its own name; null
`planes` takes every plane.
- A dtype other than the plane's is legal only as an exact widening (a bf16 norm read as f32).
The **name map** (`rad_decl_name_map`) is the plugin's map from checkpoint tensor names to declared
weight names, and it is the only place convert-time fusions live: `COPY`, `CONCAT`, `WNORM` (a
weight-normalised pair folded to the plain weight), `ROWS`, `TRANSPOSE`, `EMA` (an EnCodec codebook),
`TAP`, `COLS`. `rad-convert` reads this map from the plugin, so it needs no knowledge of the model.
`RAD_MAP_COPY` with several sources means "first present wins" (a tied lm_head); `CONCAT` requires
all. Ask for a weight's encoding with `rad_weight_encoding` **after** its map entry when the graph
depends on the format (fp8 GEMM vs plain, rotated expert kernels).
## Declare: buffers and KV groups
`rad_decl_buffer` names every activation: `RAD_BUF_TRANSIENT` (liveness-analysed, may share bytes),
`RAD_BUF_PERSIST` (across steps: a calibration accumulator, a routing histogram), or
`RAD_BUF_DERIVED` (computed once per step by the core from the batch — `derive` names the
derivation, e.g. `conv_state_index`). Buffers live in one arena sized once, not an allocator per
step.
`rad_decl_kv_group` declares each state kind: `RAD_KV_FULL` (paged per-token, growing),
`RAD_KV_WINDOW` (bounded by a window), `RAD_KV_LINEAR` (per-sequence fixed state, GDN),
`RAD_KV_CONV` (a rolling window of `width-1+num_spec`), `RAD_KV_RING` (window floor set at runtime).
The **block size is not declared** — it comes from the resolved attention kernel's constraints; read
it back with `rad_kv_block_size`. `rad_bind_layer_kv` binds each layer so the block manager knows how
many state instances a sequence owns; `rad_kv_group_zero` (a rank with no heads),
`rad_kv_group_replicated` (every rank holds the same bytes), and `rad_kv_group_dense_read` (read over
the whole history every step, so it cannot stream) are the three refinements.
## Declare: ops
```c
rad_op h = RAD_OP(b, "gemm_w4a8_nt",
RAD_PARAMS(RAD_RANGE("M", 1, max_tok), RAD_INT("N", 8192), RAD_INT("K", 5120)),
RAD_WEIGHTS(w));
rad_op_reads (b, h, RAD_BUFS(b_x), n);
rad_op_writes(b, h, RAD_BUFS(b_y), n);
```
- Parameter names and operand order come from `docs/OPS.md`; a new name is allowed only when the
vocabulary has none, and the first plugin to declare it fixes its schema.
- `RAD_RANGE` on exactly one parameter per op gives the selector its bucket table. The core carries
every parameter opaquely except `RAD_PROLE_SEQ_CHUNK` (cut steps on it) and `RAD_PROLE_CAPACITY`.
- `rad_op_reads`/`rad_op_writes` are the buffer planner's liveness. An undeclared use is assumed live
for the whole program: sound but wasteful.
- **Fusion is selection.** Ask for the fused op; if `rad_op_resolved` is false, emit the chain. A
fusion's `replaces` hook is a checkable equivalence, not a rewrite.
- `rad_op_declared(b, "op")` asks whether any library declares an optional op; use it before
emitting a fused form only an optional library defines.
- Buffers touched from the **second lane** go through `rad_buf_concurrent`, and an architecture whose
lane-1 work lies in one stretch declares `rad_lane1_window_begin/end` around it. The runtime
refuses a lane-1 op outside the window; the window lets buffers used only outside it share bytes.
Step output: `rad_declare_logits` names the buffer the sampler reads (its dim 0 bounds `n_out`; a
[n_tok, vocab] logits buffer at a large vocabulary is gigabytes — declare the gather +
`logits_gemm` shape instead). An embedding program declares `rad_declare_pooled` instead (one row a
sequence, no sampler, no generation). `rad_declare_scores` opts into host-side scored steps.
## Declare: drafters, encoders, generators
- `rad_declare_drafter(RadDrafterDecl)` — depth, block, proposal shape. `rad_drafter_shared_kv` (a
drafter with no cache of its own), `rad_drafter_tree` (a tree over the proposals),
`rad_drafter_short_block` (a second, shorter pass) are refinements. Declare the drafter **last**.
- `rad_declare_encoder(RadEncoderDecl)` — a vision/audio tower, media preprocessed by the core.
- `rad_declare_generator` / `rad_declare_audio_generator` / `rad_declare_video_generator` — image,
audio and video generation programs; the engine sizes the step from the probe's `gen_*` fields
before declare.
- Optional format exports let a model state what it knows and the server learn nothing new:
`rad_arch_chat_format` (a data description of reasoning/tool-call markup, replacing template
analysis), `rad_arch_audio_format` (how a speech model hears: clip tokens, prompt, language,
transcript start), `rad_arch_text_prepare` (rewrite request text into training form),
`rad_arch_video_positions`/`rad_arch_video_pack` (video pass geometry and packing),
`rad_arch_encode_geom` (encoder pass geometry). Each struct grows only at its end behind
`struct_size`, so adding a field does not move the ABI.
**Media preprocessing is the core's**, not the plugin's: resizing, patching, placeholder expansion
and positions in six processor families (Qwen-VL, Gemma 4, LFM2-VL, Unlimited-OCR, GLM-5.3, Pixtral
for Nemotron), chosen by `vision_config.model_type` plus owning-model checks; audio through Gemma 4's
extractor or Qwen3-ASR's Whisper one, chosen by `audio_config.model_type`. A vision model with another
pipeline needs a core change.
## Step: issuing the graph
`RadBatch` is built once per step and shared by every block: `phase` (`PREFILL`/`DECODE`/`MIXED`),
`n_tok`, `n_seq`, `token_ids`, `positions`, `cu_seqlens`, `seq_ids`, `lora_slot`, `n_out`/`out_ids`,
`q_lens`/`ctx_lens`/`max_q_len`/`max_ctx_len`, the KV-group array, the speculative fields
(`n_spec`, `draft_pass`, …).
```c
static void step(RadCtx* c, const RadBatch* batch) {
if (batch->draft_pass > 0) { m.ds.step(c, batch); return; } /* query pass */
if (batch->draft_pass < 0) { m.ds.context(c, batch); return; } /* context pass */
RAD_ISSUE(c, m.op_embed, praw(batch->token_ids, RAD_I32, batch->n_tok),
RAD_W(m.w_tok), brows(m.b_x, batch->n_tok));
for (auto& layer : m.layers) { layer.attn.step(c, batch); layer.mlp.step(c, batch); }
/* ...heads; the lm_head only over batch->n_out rows... */
}
```
- `rad_issue(c, op, operands…, n)` / `RAD_ISSUE` (n = the batch's ranged extent) /
`RAD_ISSUE_N` (an explicit row count). `rad_step_fail(c, what)` sets the status helpers return.
- **A mixed step is the trap.** Continuous batching produces steps with a prefill chunk beside decode
rows. A block that issues one path for the whole step sends its decode rows down the chunked path,
which commits recurrent state directly and takes no `num_accepted`; a rejected draft cannot be
rolled back, and the model stays fluent while drifting. Issue the recurrent path over
`RadBatch::n_seq_decode` sequences and the chunked path over the rest (the core orders steps
decode-first, so each half is contiguous; `cu_seqlens` holds absolute offsets, so pass
`cu + n_seq_decode` with the token operands at their base).
- A step issued on lane 1 outside the declared window fails by name. An op the core appends behind a
declared one (runtime LoRA) counts at that op's position.
- **Zero rows is not a call**: issuing an op over zero rows (a zero ranged extent, or all-empty
sequences) returns `RAD_E_SHAPE` and writes nothing. The caller skips the issue, as the
architectures' decode and prefill halves do.
- `n_out` is 0 for a chunk that finishes no sequence — do not run the lm_head.
## Probe: what the plugin measured
`rad_arch_probe(meta, out)` runs before declare. Its fields are the engine's permissions:
| field | effect |
|---|---|
| `draft_depth`, `draft_depth_fixed` | the container's spec depth; a block drafter's trained block is the only depth it serves |
| `shape_probe_ok` | the plugin tolerates a sizing declare, so the activation arena can be lent |
| `enc_merge`, `audio_frames` | encoder pass granularity; longest audio clip |
| `gen_rows`, `gen_patches`, `gen_audio`, `gen_ctx`, `gen_video` | generator sizing before declare |
| `kv_quant_ok` | bits of the narrow KV layouts this model's kernels serve |
| `chain_pays`, `tree_pays` | the plugin measured chained launches / draft trees faster |
| `kv_stream_ok` | the architecture can stream a KV group (sparse, block-addressed, with `kv_resolve`) |
| `control_vector_ok` | the plugin applies `RadBuildCtx::cvec` |
| `tp_attn_rows_ok` | the plugin serves row-split prefill attention |
| `state_f16_ok` | the plugin declares recurrent state at f16 and its kernels read it |
A flag whose probe bit is 0 is **refused at startup with the plugin named** (`--kv-cache-dtype
fp8_v4`, `--control-vector`, `--tp-attn-split rows`, `--state-cache-dtype f16`, `--kv-resident`).
That is the design: a deployment switch the plugin cannot honour is an error, never a silent
substitution.
## Worked pattern: the smallest complete plugin
`arch/llama_fp8/llama_fp8.cpp` builds twice from one source — `llama_fp8` (quant `fp8_e4m3`) and
`llama_dense_fp8` (quant `""`), differing only in the descriptor and the graph-dump text. It shows
the whole shape:
1. `struct Layer { AttnFP8 attn; MlpFP8 mlp; }`, `struct Model` holding `Names`, `Geom`, buffer and
op handles.
2. `declare()`: name map entries first, then weights (with `rad_weight_encoding` where the head's
format decides the kernel), buffers, KV group, the embedding and its all-reduce, then each layer's
`declare()` from the shared blocks, the final norm, gather and lm_head, **the drafter last**, and
`rad_note` lines explaining the shape.
3. `step()`: draft passes first (sign selects query/context), then embedding (+ exact all-reduce
when needed), each layer's `step()`, the final norm, and the head only over `n_out`.
4. `probe()`: reads `draft.block_size` from the metadata and refuses any other depth for a block
drafter.
5. `RAD_ARCH_PLUGIN(...)` at the bottom.
Add a new family by copying a neighbour, replacing the name map and geometry, and composing the
blocks you need; add a new *format* by cloning the plugin with another `(id, quant)` pair and the
ops that format needs.
## Verifying a new plugin
```sh
radiance --model m.rad --debug-graph # every op, its resolved kernel, and why
radiance --model m.rad --debug-placement # the plan
rad-convert $SRC -o m.rad --plan-only -v # a convert that plans will boot
rad-kbench -m m.rad --kernels mylib,libr4d,libref --report k.md --bench
rad-tune -m m.rad --tp 2 # only at the shapes you serve
```
`tests/arch_test.cpp` drives a plugin's builder the way the core does, including lane-window
liveness, so the pinned graphs and steps are the ones the core plans. A plugin that declares a weight
form no kernel `layout` accepts is a declare-time error naming the weight, encoding and kernel — the
same check `rad-convert` runs before writing.
## Related
- [[Radiance Plugins]] — kernel libraries, encodings, the build path, ABI law.
- [[Radiance Conversion]] — how the name map drives a convert, and `--set`/descriptor selection.
- [[Radiance Model Families]] — the existing `(id, quant)` pairs and their recipes.
- [[Radiance Tools]] — `rad-kbench`, `rad-tune`, `rad-info --plugins`.
@@ -1,354 +0,0 @@
---
type: "howto"
title: "Radiance Conversion — checkpoints into .rad containers"
description: "Turning a checkpoint into a .rad container: rad-convert, the recipe language, quantisers, reuse and export."
tags:
- llm
- radiance
- quantization
- howto
timestamp: 2026-10-30
---
# Radiance conversion
`rad-convert` turns a Hugging Face checkpoint into a `.rad` container, choosing each weight's
encoding from a **recipe**. `rad-export` wraps it for finetunes (a merged checkpoint or a PEFT
adapter) with the stock model's recipe. Neither opens a GPU: conversion is CPU-only and safe beside a
running server.
Fetch checkpoints with `scripts/hfget.sh <org/repo> <dir>` (resumable; reads `.safetensors`,
skips `.bin`/ONNX/GGUF when safetensors exist; `HF_TOKEN` for gated repos; writes a
`.hfget-manifest` of content keys). `radiance --model <owner/name>` runs the same fetch itself.
## What a conversion does
1. Read `config.json` (or GGUF metadata), and the shard index if present.
2. Load the architecture, kernel and quantiser plugins and run **declare**, as the engine does.
3. Resolve every op and ask each resolved kernel's `layout` hook about the planned encoding. A
recipe no kernel can serve is refused **here**, before any I/O.
4. Write each logical weight once: the checkpoint's bytes where no rule matches, otherwise the
planes the matching quantiser writes, a block of rows at a time.
5. Measure every quantised weight against its source through the core's decoder and print the worst.
6. Bake the tokeniser, chat template, metadata, recipe and any draft head.
```sh
build/bin/rad-convert /srv/models/hf/Qwen/Qwen3.8-27B -o qwen3.8-27b-bf16.rad \
--recipe data/recipes/q38-27b-bf16.recipe
build/bin/rad-convert /srv/models/hf/Qwen/Qwen3.8-Flash-Next -o flashnext-bf16.rad --max-ctx 4096
```
## Every `rad-convert` option
| option | meaning |
|---|---|
| `<input>` | a `.gguf`, a `.safetensors`, an `index.json`, or a checkpoint directory |
| `-o, --out FILE` | the container to write |
| `--recipe FILE` | one rule a line: `PATTERN QUANTISER key=value …`; repeatable; first match decides |
| `--quant RULE` | `PATTERN=QUANTISER[:key=value,…]`, placed **ahead of** the recipe's rules; repeatable |
| `--list-quantizers` | every loaded quantiser and the options it takes, then exit |
| `--arch ID` | override the architecture id read from the checkpoint |
| `--home DIR` | `$RADIANCE_HOME` (kernels, architectures, quantizers) |
| `--kernels A:B` | kernel libraries to load, first wins; declare checks the recipe against them |
| `--tokenizer F` | `tokenizer.json`; its declared chain is baked instead of reconstructed from a pre-tokenizer name. Required for a GGUF, without it the chain is a guess |
| `--tokenizer2 F` | a second `tokenizer.json` baked as the second vocab (a generator reading its prompt through two tokenisers); keep `tokenizer_config.json` beside it |
| `--add FILE` | merge one more `.safetensors`, tensor names kept; repeatable; a name in two files is refused |
| `--config DIR` | read `config.json`, `generation_config.json`, `tokenizer.json` from DIR for a single-file checkpoint |
| `--chat-template F` | a Jinja template baked in, overriding the checkpoint's (or its absence); trailing newlines stripped; without one `/v1/chat/completions` answers 501 |
| `--imatrix F` | llama.cpp imatrix: expert-popularity profile, per-column importances, error weighting; required by `ggml` types `iq2_xxs`, `iq2_xs`, `iq1_s` |
| `--set K=V` | a metadata key, seen by declare **and** written into the container; repeatable |
| `--max-tok N` | the `max_tok` declare runs at (default 8192, or a generator's largest pass) |
| `--max-ctx N` | the context declare runs at (default: the checkpoint's training context) |
| `--max-spec N` | the `max_spec` declare runs at (default 4, or the block a drafter states); `0` leaves a draft head out |
| `--draft-model DIR` | a drafter shipped as its own repository, merged under `draft.` |
| `--kv-cache-dtype T` | `bf16` or `fp8`: the cache width declare runs at; the container is identical at either |
| `--reuse FILE` | copy every weight an earlier container holds with the same encoding, quantiser, options and shape |
| `--changed FILE` | with `--reuse`: checkpoint tensors (one name a line) that differ; every weight reading one is converted |
| `--in-place` | with `--reuse`: extend that container; new weights go past its end, the header last; keeps `<file>.pre-append` |
| `--lora FILE\|DIR` | merge a PEFT adapter into the checkpoint tensors before converting (`W + strength · scale · B·A`); repeatable, merged in order; each merged tensor joins `--changed` |
| `--lora-strength F` | strength for every `--lora` (default 1) |
| `--lora-map FILE` | JSON map for adapters whose module names differ: `{"strip":[prefix…],"modules":{"<pat>":[target…]}}`; one `*` matches a block index; several targets mean a fused module (rows split in order) |
| `--plan-only` | declare, plan every encoding, print the plan, stop before writing |
| `--report N` | print the N worst quantisation errors (default 20) |
| `-v, --verbose` | per-weight detail |
`--reuse` matches a weight on its **encoding, quantiser, options and shape — not on its source
bytes** — and carries whatever calibration made the old planes. Use it only when source and
calibration are unchanged, or name the changed tensors with `--changed`. `--in-place` refuses to
change a weight the container holds or to add an expert.
## The recipe language
```
# PATTERN QUANTISER OPTIONS
blk.*.ffn_*_exps.*.weight gptq codes=i4 group=128 scale=bf16 transform=fwht128 calib=$CALIB
blk.*.ple_ngram.weight rtn codes=fp8_e4m3 block=*x* scale=bf16 rule=fixed scale_value=1.99317932128906e-4
output.weight rtn codes=fp8_e4m3 block=128x128 scale=bf16
blk.*.weight rtn codes=fp8_e4m3 block=128x128 scale=bf16
```
- One rule a line: `PATTERN QUANTISER [key=value …]`. `#` starts a comment.
- `PATTERN` is a glob over the **whole logical weight name**: `*` crosses dots, `?` is one character.
- **The first matching rule decides.** No rule → the checkpoint's own encoding, which must be one
the engine serves as is: `bf16`, `f16`, `f32`, or block-scaled FP8 (`X.weight` with
`X.weight_scale_inv`). Anything else is named and refused.
- `$NAME` / `${NAME}` in a value comes from the environment; an **unset variable is refused**, never
expanded empty (so an empty `$CALIB` cannot silently produce an uncalibrated convert).
- A quantiser may **decline** a weight (`RAD_E_UNSUPPORTED`), which keeps the checkpoint's encoding
— `rtn` declines a rank-1 tensor, so a pattern that also matches a norm leaves the norm alone.
- An option the quantiser does not declare is refused, by name, before anything is quantised.
- Two plugins naming the same quantiser are refused, both named.
## Quantisers
| quantiser | what it does |
|---|---|
| `rtn` | round to nearest on a grid; each block's scale chosen by `rule` |
| `gptq` | error feedback against the layer's input Hessian (`calib=DIR`, damped by `damp` = 0.01 of the mean diagonal by default); `act_order=1` quantises columns by descending input energy and stores the permutation; `cd=N` adds coordinate-descent sweeps |
| `awq` | per-input-column scaling to a searched power, then a per-block clip search, both against the Hessian; column scales are the last scale level |
| `autoround` | sign-gradient descent on a rounding offset per value and a range factor per block; `iters` steps; slow (a 27B dense model takes most of a working day at defaults; `iters=50` a quarter) |
| `paroquant` | channel scaling plus `stages` Givens rotations learned against the Hessian, then AutoRound on the rotated weight; the `givens` transform |
| `m2xfp4` | M2XFP's weight-side Sg-EM: MXFP4 plus one metadata byte per group of 32; its own `m2xfp4` scheme; `adaptive=0` pins the bias, `wide=1` widens it |
| `ggml` | a llama.cpp format (`q4_0`, `q4_1`, `q5_0`, `q5_1`, `q8_0`, `q2_K`…`q6_K`, `iq4_nl`, `iq4_xs`, `iq2_xxs`, `iq2_xs`, `iq2_s`, `iq3_xxs`, `iq3_s`, `iq1_s`, `iq1_m`), by llama.cpp's own search, imatrix-weighted, codes byte for byte |
| `cast` | a float weight in another float dtype (`dtype=` f32, bf16, f16, fp8_e4m3, fp8_e5m2), no scale |
| `import` | rad-convert's own, not a plugin: write the numbers a quantised release ships, in the spelling the kernels read, instead of quantising again |
Every quantiser except `m2xfp4`, `ggml`, `cast` and `import` takes the grid options:
| option | values |
|---|---|
| `codes` | `i2`…`i8`, `i16`, `i32` symmetric; `u1`…`u8`, `u16` with `zero=`; `fp8_e4m3`, `fp8_e5m2`, `fp6_e2m3`, `fp6_e3m2`, `fp4_e2m1` |
| `table` | a codebook the codes index: `nf4`, `iq4nl`, `w4nl`, `w5nl`, `binary`, `ternary`, or `;`-separated values |
| `group=N` / `block=RxC` | where the scales sit; `*` is a whole extent |
| `scale` | f32, bf16, f16, fp8_e4m3, e8m0; under `scale2` also an integer sub-scale `u4`…`u8`, `i6`, `i8` |
| `scale2`, `group2` / `block2` | a second scale level the first is relative to (default the whole tensor) |
| `scale2_value` | the second level fixed at this value, exact in `scale2`'s dtype |
| `zero` | an integer zero point per block — an asymmetric grid |
| `transform=fwhtN` | rotate each row by the Hadamard first |
| `rule` | `absmax`, `mx` (OCP shared exponent), `fixed` (`scale_value=`), `search` (least squared error over candidates and refits, imatrix-weighted) |
| `clamp` | a signed code's range: `full` or `sym` |
Named grids: MXFP4 = `codes=fp4_e2m1 group=32 scale=e8m0`; NVFP4 =
`codes=fp4_e2m1 group=16 scale=fp8_e4m3 scale2=f32`; NF4 = `table=nf4 group=64`; a K-quant-like grid
= `codes=i4 group=32 scale=u6 scale2=f16 group2=256`; a ternary BitNet weight = `table=ternary`.
**A container can hold only what some kernel reads.** rad-convert checks every planned encoding
against the `layout` hooks of the kernels declare resolved and refuses one no kernel accepts. libr4d
reads the plain floats; block-scaled fp8 and fp8 rows; int8 block rows plain or rotated; routed
experts as fp8, int4 (`i4*bf16[1x128]/fwht128`) or the `w4nl`/`w5nl` codebooks under E4M3 scales;
the dense w4, w8 and mxfp4 GEMM rows; and the 2-bit draft head. See [[Radiance Plugins]] for the
encoding model.
## Importing a release someone else quantised
These forms are read weight by weight and written in the spelling the kernels read; a recipe rule
re-quantises from the exact dequantisation where the family's kernels want another form.
| `quantization_config` | read as | notes |
|---|---|---|
| `quark` MXFP4 (fp4, group 32, e8m0) | `fp4_e2m1*e8m0[1x32]` | byte for byte; also servable as it stands |
| `paroquant_mxfp4` | `fp4_e2m1*e8m0[1x32]/givens` | codes/exponents byte for byte plus learned rotations as `givens` tables |
| `paroquant`, `int5-bitplane`, plain `paroquant` | `u5*f16[1x128]-u8[1x128]/givens`, `u4*…` | AWQ-packed nibbles re-laid as a code bit stream; f16 scales, integer zeros, rotations |
| `compressed-tensors` `pack-quantized`, 4-bit `int`, group, act-order none/static/weight | `i4*<scale>[1xG]` | `weight_packed` made signed beside `weight_scale`; group/dynamic reorder is refused |
| same with `symmetric:false` (AWQ) | `u4*<scale>[1xG]-u4[1xG]` | nibbles plus `weight_zero_point`, bit for bit |
| `auto-round` `auto_round:auto_gptq`, `gptq` v1 | `i4*bf16[1x128]` or `i8*bf16[1x128]` | scales rounded to bf16; convert-time; `import` keeps shipped f16 |
| `nvfp4-pack-quantized` | `fp4_e2m1*f32[1x16]` | `weight_scale / weight_global_scale`; **no kernel serves NVFP4**, so it always takes a rule |
| `modelopt` NVFP4 / FP8 / MIXED_PRECISION | NVFP4 `fp4_e2m1*f32[1x16]`; FP8 `fp8_e4m3*f32[1x*]` | ModelOpt's second scale **multiplies**; `input_scale` is not read |
| compressed-tensors all-FP8 groups | `fp8_e4m3*<scale>[1x*]` or `[RxC]` | nothing imported: stock FP8 with its scale; a per-tensor scale is written once a row |
| GGUF `Q8_0`, `Q4_K`, `Q6_K` | `i8*f16[1x32]`, affine u4/u6…, `i6*i8…` | blocks split bit for bit; **not servable as they stand** |
The width is read **per tensor**, so a mixed release imports int4 experts and int8 projections
together. What an import cannot read (an asymmetric GPTQ zero point, a group-reordering act-order
index, a GPTQ group under 128, a missing/zero global scale, an unlisted format) is refused by name.
The release's `quantization_config` names the tool, not a served form, so it is kept as
`imported_quantization_config.*`.
`import` options: `codes=uN` makes a symmetric grid unsigned under a zero point of `2^(N-1)`;
`zero=` the zero plane's dtype (`u4`, `u8`); `scale=` f16/bf16/f32, each shipped scale rounded to
nearest; `block=RxC` repeats an FP8 scale shipped per row/tensor over that grid. It reads an imported
integer grid or FP8 with a per-row/per-tensor scale and keeps a plain float weight as it is.
**NVFP4 to MXFP4 rounds twice** (e4m3 per 16 → power of two per 32), so it loses more than MXFP4 made
from bf16. Prefer a calibrated MXFP4 container when one exists; the quality verdict is a KL gate, not
a file size.
## The descriptor law (`--set`)
The architecture plugin is chosen by the exact `(arch id, quant descriptor)` pair. A convert-time
choice that must also hold at serve time is stated once with `--set`, which hands it to declare and
writes it into the container. Storage format is not such a choice (the encoding is in the container
and the plugin reads it there), but the **serving plugin** is: a `quantization_config.*` key
re-derives the descriptor.
```sh
rad-convert <bf16 Qwen3.8-27B> -o q38-27b-fp8.rad \
--recipe data/recipes/q38-27b-fp8block-df2.recipe \
--set quantization_config.quant_method=fp8 --set quantization_config.fmt=e4m3 \
--set "quantization_config.weight_block_size=128 128" \
--set quantization_config.activation_scheme=dynamic
```
A bf16 checkpoint whose recipe writes Qwen's block FP8 must therefore be declared *as that format*;
otherwise the selection pair does not match any plugin and the convert (or the boot) is refused. The
same law bites the other way when a release's own `quant_method` names a pair no plugin claims.
## Inspect and validate the result
```sh
rad-info out.rad # architecture, source quant, recipe, sections
rad-info --recipe out.rad # the expanded rules — did the intended one win?
rad-info -v out.rad | head -40 # every tensor, its encoding, and what made it
rad-kbench -m out.rad --home $RADIANCE_HOME --report kbench.md
radiance --model out.rad --debug-graph # every op resolves
```
The converter's own error report (the worst `--report N` weights) measures each quantised weight
against its source through the core decoder. `rad-kbench` adds the artifact test: weight operands
read from the container and passed through each kernel's `layout`/`relayout` hooks. Both are on
[[Radiance Tools]].
## Calibration (GPTQ, AWQ, AutoRound, ParoQuant)
`calib=DIR` is a directory an engine run wrote with `RADIANCE_CALIB_DIR` set: each layer's **Gram
matrices of its input**, in the domain the kernel multiplies it in. A block that characterises an
activation declares an f32 `[K,K]` PERSIST buffer and a `gram_accum` op; at `RADIANCE_CALIB_TOKENS`
(default 65536) the run writes the files.
Three-step bootstrap: convert with plain `rtn` → serve that container with `RADIANCE_CALIB_DIR` →
push real prompts through prefill → convert again with the Hessians. The second moment is robust to
the first container's small quantisation error. A weight with no file is quantised uncalibrated and
the converter says so; `awq`, `autoround` and `paroquant` fall back to the imatrix diagonal when one
is given.
- The accumulator must be **PERSIST**: a transient buffer may share bytes and be overwritten between
the launches it sums.
- Files are per rank; the consumer sums them. Under expert parallelism a rank's `down` input covers
only its experts' tokens; `gate_up` input is the full token set on every rank, so the sum scales
`H` by world size — GPTQ is invariant to a positive scale.
- Under tensor parallelism a column-split linear sees only its rank's columns, so a tap may write
`gram.r<rank>.<stem>.part.bin`; libquant places the parts on the diagonal in rank order, leaving
cross-rank products zero. They must cover K and be in the model's own domain.
- `--imatrix` supplies the expert-popularity profile the placement planner ranks units by, the
per-column importances a quantiser uses, and the error report's weighting.
## Reuse, drafters, merging
```sh
# re-quantise only what changed
rad-convert $SRC -o new.rad --recipe new.recipe --reuse old.rad --changed changed.txt
# merge a drafter shipped as its own repo (DFlash2, DSpark, MTP assistant)
rad-convert $SRC -o out.rad --draft-model /srv/models/z-lab/Qwen3.8-27B-DFlash2 \
--recipe data/recipes/q38-27b-mxfp4-df2.recipe
# merge a PEFT adapter at convert time
rad-convert $SRC -o tuned.rad --lora ~/run/adapter --lora-strength 1.0 \
--recipe data/recipes/q38-27b-mxfp4-df2.recipe
```
- A drafter's tensors go in under `draft.`, its config under `draft.` keys. It has no embedding and
no `lm_head` of its own; it uses the target's.
- `--lora-map` exists for adapters trained on names the checkpoint does not use; a fused target
(`qkv`) takes several targets and splits the adapter's rows in order.
- `--add` + `--config` + `--tokenizer2` assemble a ComfyUI-style release of separate single files
(Anima: diffusion model, text encoder, VAE). A name in two files is refused.
## `rad-export` — finetunes without hand-writing the command
```sh
rad-export ~/outputs/my-run/checkpoint-400 --dry-run # what it would run
rad-export ~/outputs/my-run/checkpoint-400 # adapter -> <name>.rad
rad-export ~/exports/merged -o ~/models/mine.rad --recipe q38-27b-fp8block-df2
```
| flag | default | meaning |
|---|---|---|
| `SRC` | required | a merged 16-bit checkpoint (`config.json` + `*.safetensors`) or a PEFT adapter (`adapter_config.json` + `adapter_model.safetensors`) |
| `-o, --out FILE` | SRC's folder name + `.rad`, beside SRC or in `out_dir` | the container |
| `--recipe NAME\|FILE\|auto` | `auto` | a recipe name, a file, or the one recipe whose header names the base model, takes a bf16 checkpoint and needs no calibration |
| `--base DIR` | the adapter's `base_model_name_or_path`, from disk or the HF cache, else downloaded | the base a LoRA merges into |
| `--draft DIR\|ORG/NAME` | the config, else the Hub repo the recipe's usage line names | the drafter when the recipe merges one; a usage line with a placeholder asks for `--draft` |
| `--image IMAGE` | `$RADIANCE_IMAGE`, the config's, then `stilldeadcode/radiance` | run `rad-convert` in this image (docker, CPU-only, no network, **not pulled**) |
| `--rad-convert PATH` | — | run this binary instead of docker; recipes come from its `share/radiance` |
| `--memory M` | — | docker memory limit for the convert |
| `--dry-run` | — | resolve recipe, drafter and paths; print the `rad-convert` line; write nothing |
| `--keep-merged` | — | keep the merged checkpoint beside OUT |
| `--config FILE` | `~/.config/rad-export/config.json` | the config below |
| `--list-recipes [--json]` | — | list the recipes the image/installed tool carries |
- Input: a merged 16-bit checkpoint (`config.json` + `*.safetensors`) or a PEFT LoRA adapter
(`adapter_config.json` + `adapter_model.safetensors`).
- An adapter is merged **exactly as PEFT's `merge_and_unload`** computes it on CPU: `scale` =
alpha/r (alpha/√r with rsLoRA), `rank_pattern`/`alpha_pattern` honoured, delta in fp32 rounded to
the adapter's dtype. It streams tensor by tensor into 1 GiB shards, so a 27B merge needs a few GiB
of RAM, not the model twice. Plain linear LoRA only: embedding LoRA, trained biases, DoRA, saved
modules, convolution LoRA, a rank other than the config's, and a missing target are refused before
anything is written. The adapter's tokeniser/template replace the base's where shipped.
- The recipe comes from the image (`/opt/radiance/share/radiance/recipes`) or the installed
`rad-convert`'s share; `auto` matches the base model's name in recipe headers and asks when several
fit. A recipe needing calibration is listed but never picked by `auto` — a finetune's Hessians are
not the stock model's.
- It runs `rad-convert` on the CPU, under Docker as the caller with no network, and writes the log to
`OUT.log`.
- `~/.config/rad-export/config.json` pins what auto cannot know: `image`, `out_dir`,
`recipes{model:recipe}`, `drafts{recipe:dir}`, `bases{repo:dir}`.
- It is Python 3 with no dependencies; merging needs `torch`/`safetensors`, fetching a missing base or
drafter needs `huggingface_hub`. The image carries no Python; extract the script with
`docker run --rm --entrypoint busybox stilldeadcode/radiance cat /opt/radiance/bin/rad-export`.
## Scoring a container (the KL mode)
```sh
# once, on the bf16 container
radiance --model bf16.rad --kld-record /srv/kld/q38 --kld-corpus corpus.jsonl
# each candidate, same flags otherwise
radiance --model quant.rad --kld-ref /srv/kld/q38 --kld-out report.json
```
- The corpus is JSONL, one `{"prompt", "score_from"?, "source"?}` a line; only `prompt` is required.
Positions `[score_from, n-1)` are scored; `source` groups the report. Keep the scoring corpus
disjoint from the calibration corpus.
- `--kld-record` writes the reference's log-probabilities (f16 over the vocabulary, the 16 most
likely tokens exactly, the token ids). DIR is never overwritten and is read only once `kld.json`
exists, which is written last.
- `--kld-ref` scores this model's f32 logits against stored rows: mean, median, 99/99.9/99.99th
percentile KL, top-1 agreement, and both models' perplexity. `--kld-out` writes the report; `FILE.rows`
is `[positions, 4]` f32 (KL, both NLLs, argmax agreement) for position-by-position comparison.
- The mode turns speculation and the prefix cache off, turns the step pipeline off, and refuses an
explicit spec depth above 0. The logits buffer holds a row per token of a step, so a large
`--max-num-batched-tokens` costs pinned host memory as well as VRAM.
- `--kld-lora NAME` scores every document through a served adapter, against a reference recorded
from the merged checkpoint; `--kld-lora-sources PREFIX` runs only matching documents through it,
the rest as the base, in the same steps — each document's rows should be byte-identical to the
all-adapter and all-base runs.
The gate the fleet uses (mean KL ≤ 0.03, top-1 ≥ 95 %) is on [[frank-archon]] and
[[Quantization Production]]; the mode here is only the instrument.
## Convert laws paid for in full
- **`--plan-only` first.** A plan that resolves every op will boot; it costs seconds against an hour
of I/O on a large checkpoint.
- **Declare runs at the step and context bounds, and some graphs depend on them.** Qwen4-Exp sizes
its QSA buffers by `--max-ctx` and refuses a `--max-tok`/context pair whose score plane would pass
2 GiB; the defaults (8192 tokens, the checkpoint's training context) fit most models, and a
generator's largest pass may override `--max-tok`.
- **Scale names follow the tensor.** `scale_name()` appends `_scale_inv` to the *tensor* name, so a
stacked `…experts.gate_up_proj` keeps its scales at `…gate_up_proj_scale_inv`.
- **File shadowing: first wins.** The reader opens index-named files sorted and emplaces every tensor
each file physically holds, first-wins. A merged PLE shard must sort first
(`00-ple.merged.safetensors`) or a raw shard wins the name.
- **Never write through a staging symlink.** A checkpoint may symlink its raw index; `open(path,"w")`
writes through it and corrupts the checkpoint. Write a temp file and `os.replace`.
- **One encoding per expert table.** A whole expert kept bf16 is the only thing legal beside a
table; a split tier (down=fp8, gate_up=w4) is unservable.
- **The descriptor pair has no fallback** (above), and **convert is card-less**: GPTQ solves on CPU
BLAS, so the GPU is irrelevant to production and the card only decides what can be decoded.
## Related
- [[Radiance Model Families]] — the (arch id, quant descriptor) table and the command per model.
- [[Radiance Plugins]] — quantiser and kernel-plugin contracts behind these options.
- [[Radiance Tools]] — `rad-info`, `rad-kbench`, `rad-tune`.
- [[Quantization Production]] — the fleet's calibration/KLD campaign and its gates.
- [[frank-archon]] — the deployment whose recipes and convert laws these are.
@@ -1,140 +0,0 @@
---
type: "reference"
title: "Radiance Engine — the inference server behind frank-archon"
description: "The radiance inference engine: its shape, plugin model, container format, and how the reference pages fit together."
tags:
- llm
- radiance
- reference
timestamp: 2026-10-30
---
# Radiance Engine — shape, plugin model, container
Radiance is StillDeadcode's inference server for AMD GPUs, written in C++20 and HIP: language
models, plus image, video, speech and music generation and embeddings. One HTTP front end speaks
OpenAI's chat, completions, Responses, images, audio and video APIs, Anthropic's Messages API and
llama-server's native one. Nothing in the serving path is Python.
The fleet's only radiance endpoint is [[frank-archon]] (2× R9700, TP=2, Dankserver). **This page
and its siblings own the engine reference; the live deployment shape, its Eras and its measured
laws stay on the endpoint page.** The numbers here describe upstream `main` as read 2026-10-30
(release 1.14.0, plugin ABI 31). The fleet pin is older (1.8.1): a flag below that post-dates the pin
is a boot refusal, never a silent no-op, and the endpoint page records the CLI families that pin
itself retired relative to its own rollback target.
Source: `https://codeberg.org/StillDeadcode/radiance` at `8da043c`. Engine docs live in `docs/`
(`GUIDE.md` for serving, `TOOLS.md` for tools, `PLUGIN.md` for plugins, `ARCHITECTURES.md` per
family, `OPS.md` for the op vocabulary, `spec.md` for the design).
## The four moving parts
```mermaid
graph LR
A[Checkpoint<br/>safetensors / GGUF] -->|rad-convert + recipe| B[.rad container]
B --> C[radiance engine]
C --> D[OpenAI / Anthropic /<br/>llama-server wire APIs]
P1[kernel library .so<br/>libr4d, libavx, libref] --> C
P2[architecture .so<br/>one per arch id × quant descriptor] --> C
P3[quantiser .so<br/>libquant] -->|rad-convert| A
```
- **Engine** — `core/`: declare phase, placement/memory planner, scheduler, KV manager, sampler,
tokeniser, HTTP server, media preprocessing.
- **Kernel libraries** — implement ops for hardware. `libr4d` targets RDNA4 (`gfx1200`,
`gfx1201`); `libavx` serves the host backend (AVX-512/AVX2) with no GPU at all; `libref` is the
slow reference implementation every other library is checked against.
- **Architecture plugins** — declare one model family at one weight format: its weights, buffers,
KV groups and the ops it issues each step. The engine picks one by an **exact (architecture id,
quantisation descriptor) pair, with no fallback**.
- **Quantiser plugins** — `libquant`'s `rtn`, `gptq`, `awq`, `autoround`, `paroquant`, `m2xfp4`,
`ggml`, `cast`, `import`; they write a weight's numbers into an encoding's planes.
- **Tools** — `rad-info`, `rad-convert`, `rad-export`, `rad-kbench`, `rad-tune`, `rad-schemas`,
`rad-tokcheck`, plus `rad-chatparse`, `rad-avx-check`, `r4d_selftest` in the build tree.
## Two phases, one static graph
`rad_arch_declare` runs once per rank and names every weight, buffer, KV group and op, in order.
The core resolves every op against the kernel hierarchy per band, per domain, and completes the
whole plan before a byte of weights is read. `rad_arch_step` then only *issues* handles obtained at
declare; it may not allocate and may not synchronise. A model whose declare succeeds is fully known
statically, which is what makes `--debug-graph`, the buffer liveness plan and `rad-convert`'s
plan-only check possible.
Consequences a plugin author must respect:
- An op that never resolves is `RAD_NULL_HANDLE` and only fails if issued. A fusion is a *selection*:
ask for the fused op, emit the chain when `rad_op_resolved` is false.
- Declaration order is the buffer planner's liveness. A drafter must be declared after the trunk
it runs behind, or trunk and drafter buffers silently share bytes.
- Every op in the graph must have a row in a loaded kernel library. `libref` implements the
conventional vocabulary in `docs/OPS.md`; device-only ops (most fusions) exist only in `libr4d`.
## The `.rad` container (RAD2)
A container is one file holding everything the server needs: weights as **canonical encoding
planes**, the tokeniser (and optionally a second vocab), the chat template, metadata, the recipe it
was made with, and any draft head. It names no kernel library.
- Planes are kernel-independent: codes, scales, zero points, codebooks — row-major, bit-packed
sub-byte where needed. A kernel rearranges them into its own layout at load (`layout`/`relayout`
hooks), losslessly, so a kernel library can change layouts without reconverting the model.
- Encodings are data, not an enum: `RadEncoding` names the scheme (`plain`, `affine`, or a
quantiser's own), the planes by role, the scale grouping, and a transform (`fwhtN`, `perm`,
`givens`). That is how one file can hold block-FP8, int4, MXFP4, NVFP4-imported, ParoQuant
rotations and llama.cpp grids.
- `rad-info` is the reader: sections (strings, metadata, directory, planes, encodings, vocab,
profile, data), `--meta`, `--vocab`, `--recipe`, `--plugins`, and `-v` for every tensor with its
encoding and what made it.
See [[Radiance Conversion]] for producing one and [[Radiance Tools]] for inspecting it.
## Selection key and version law
- A checkpoint's architecture id is its `config.json` `model_type` without underscores; `rad-convert
--arch ID` overrides it. `rad_arch_quant` supplies the quantisation descriptor (empty for an
unquantised checkpoint).
- **No fallback.** A raw official FP8 checkpoint whose `quantization_config.quant_method` is `fp8`
is not the pair the `qwen4exp_fp8` plugin claims (`(qwen4exp, "")`), so both serve and convert
refuse it until the descriptor is changed with `--set` or a staged `config.json`. The deployment
lesson is on [[frank-archon]]; the mechanism is on [[Radiance Conversion]].
- `RAD_ABI_VERSION` covers every header a plugin compiles against. A plugin reporting a different
number is refused at load, with both numbers printed. It moves on any shape change, not on a new
optional export; rebuild when it changes. Release version and ABI are independent: `/version`
reports the release, `rad-info --plugins` reports what loaded.
## Environment
| variable | meaning |
|---|---|
| `RADIANCE_HOME` | plugin search path: `:`-separated homes, each holding `kernels/`, `architectures/`, `quantizers/`; first wins, same-name files shadow later homes. Compiled-in default is `<prefix>/share/radiance` |
| `RADIANCE_KERNELS` | default for `--kernels`: ordered kernel libraries, first wins; unnamed libraries are not loaded |
| `RADIANCE_TUNE` | pin a kernel's tuned axes for a measurement (a measurement aid, not a deployment setting) |
| `RADIANCE_CALIB_DIR` | a serve run records per-layer input Gram matrices here (the GPTQ calibration tap) |
| `RADIANCE_CALIB_TOKENS` | tokens after which the tap stops and writes (default 65536) |
| `RADIANCE_LOG`, `RADIANCE_LOG_STEPS` | log level; per-step token counts |
| `RADIANCE_DEBUG_RESID` | residual-stream probe per (layer, site) |
| `RADIANCE_DFLASH_Q_TEMP` | temperature of a DFlash2 coupled walk's candidate softmax |
The full list of debug knobs (`RADIANCE_DEBUG_*`, `RADIANCE_DUMP_*`, `RADIANCE_OP_ORACLE`, …) is in
the source's `getenv` calls; none is a deployment setting.
## Where the pages are
| Page | Owns |
|---|---|
| [[Radiance Serve Options]] | every serve flag, grouped, with defaults and the law behind each knob |
| [[Radiance HTTP API]] | endpoints, request fields, streaming, tool calls, refusal rules |
| [[Radiance Conversion]] | `rad-convert`, the recipe language, quantisers, imports, reuse, `rad-export`, KL scoring |
| [[Radiance Model Families]] | which plugin claims which model, and the conversion command per family |
| [[Radiance Plugins]] | kernel and quantiser plugins: ABI, rows, hooks, encodings, build and checks |
| [[Radiance Architecture Plugins]] | declaring a model: weights, buffers, KV groups, ops, step, probe |
| [[Radiance Tools]] | `rad-info`, `rad-kbench`, `rad-tune`, `rad-schemas`, `rad-tokcheck`, harnesses |
| [[Radiance Operations]] | the container contract, host prerequisites, readiness, ledgers, restarts |
| [[frank-archon]] | the fleet's live radiance endpoint: shape, Eras, rollback, measured laws |
## Sources
- `README.md`, `docs/GUIDE.md` §1–§4, `docs/TOOLS.md`, `docs/PLUGIN.md`, `docs/DOCKER.md`,
`docs/ARCHITECTURES.md`, `docs/OPS.md`, `spec.md` §2–§4, `abi/*.h`, `core/config.cpp`,
`core/rad_core.h` at commit `8da043c` (codeberg.org/StillDeadcode/radiance).
@@ -1,198 +0,0 @@
---
type: "reference"
title: "Radiance HTTP API — endpoints, fields, refusals"
description: "The HTTP surface radiance serves: endpoints, request fields, streaming, tools, and refusal rules."
tags:
- llm
- radiance
- api
- reference
timestamp: 2026-10-30
---
# Radiance HTTP API
One server serves one model. It answers OpenAI's chat/completions/Responses/images/audio/video APIs,
Anthropic's Messages API and llama-server's native one, so their clients and SDKs work unchanged.
`model` in a request is accepted and **not enforced**.
## Endpoints
| route | what it serves |
|---|---|
| `POST /v1/chat/completions` | chat: tools, reasoning, streaming, grammars |
| `POST /v1/completions` | raw text completion |
| `POST /v1/embeddings` | an embedding head, else 501 |
| `POST /v1/images/generations`, `/v1/images/edits` | OpenAI images API; needs an image model, else 501 |
| `POST /v1/systemone` | LiquidAI d1 decisions: named yes/no, choice and score questions as probabilities; needs a d1 container, else 501 |
| `POST /v1/audio/transcriptions` | OpenAI speech-to-text; needs a transcribing plugin, else 501 |
| `GET /v1/realtime` (WebSocket) | streaming transcription session |
| `POST /v1/audio/speech` | speech (Breeze-TTS-2, drama-3) or a song (MiniMax-Music3), whole or streamed |
| `GET /v1/audio/speech/stream` (WebSocket) | text streamed in, each sentence's audio out |
| `POST /v1/videos`; `GET /v1/videos[/:id[/content]]`; `DELETE /v1/videos/:id` | video jobs made in the background, downloaded as MP4 |
| `POST /v1/messages`, `/v1/messages/count_tokens` | Anthropic Messages, for Claude Code and the Anthropic SDKs |
| `POST /v1/responses`, `/v1/responses/input_tokens` | OpenAI Responses, for Codex and SDK clients |
| `POST /completion`, `/completions`, `/infill` | llama-server native completion and fill-in-the-middle |
| `GET /v1/models`, `/v1/models/:id` | what is loaded, with `max_model_len`, `root`, `capabilities` |
| `GET /health`, `/v1/health`, `/ping`, `POST /ping` | liveness |
| `GET /load`, `/version`, `/props`, `/server_info`, `/diagnostics` | load; release version; llama-server props; full configuration; the startup report as JSON (`/diagnostics` is same-machine-only without an API key) |
| `POST /apply-template`, `/v1/chat/completions/input_tokens` | the rendered prompt; its token count |
| `GET /metrics` | Prometheus |
| `POST /tokenize`, `/detokenize`; `GET /get_tokenizer_info` | vLLM's shape and llama-server's at once |
| `POST /reset_prefix_cache`; `DELETE /sessions/<label>`, `/sessions?id=<id>` | drop every cached prefix; forget a stored conversation |
| `GET /`, `/dashboard`; `GET /stats`, `/graph`, `/sessions` | the dashboard and the JSON it is drawn from |
Unversioned shorthands exist for chat, chat input tokens, embeddings, images, audio transcriptions,
audio speech, models, responses and completions, for a client given the bare host as base URL.
`/completions` **without** `/v1` is llama-server's native completion. A path nothing serves is a JSON
404.
A text prompt to `/v1/completions`, `/v1/embeddings` or `/tokenize` reads control tokens as tokens
(as vLLM and llama.cpp do), so a client rendering its own template can send `<|im_start|>user\n…`.
The model's declared BOS is prepended unless the prompt already opens with it.
## Chat
```sh
curl -s localhost:8000/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"m","messages":[{"role":"user","content":"hi"}],"max_tokens":200,"temperature":0.7}'
```
- **No chat template → 501** on this route (the container carries one; `--override-chat-template`
replaces it). Use `/v1/completions` instead.
- Thinking off has three spellings: `chat_template_kwargs.enable_thinking=false`,
top-level `enable_thinking=false`, or `reasoning_effort:"none"`.
- Reasoning returns in `choices[].message.reasoning_content`, separate from `content`.
**Reasoning counts against `max_tokens`**; a reply that runs out inside it has empty `content` and
`finish_reason:"length"`. `usage.completion_tokens_details.reasoning_tokens` tells that case from a
model that answered nothing.
- `thinking_token_budget` (aliases `thinking_budget`, `thinking_budget_tokens`,
`reasoning_budget_tokens`) closes the reasoning block at N tokens and forces the close, so it holds
under speculation and at any temperature. It needs a one-token end marker; otherwise the response
has no `completion_tokens_details` and a budget is a 400.
- `continue_final_message` continues a conversation ending in an assistant message instead of
answering it; `add_generation_prompt` opens a new turn.
- Tool calls: `tools`, `tool_choice` (`auto`, `none`, `required`, named function),
`parallel_tool_calls`. `required`/named hold the whole reply to calls after the reasoning block and
cannot combine with `response_format` or `grammar`.
- Structured output: `response_format` (`json_object` or `json_schema`), llama-server's `json_schema`
shorthand, or a raw GBNF `grammar`. A chat template that drops the grammar makes the request a
refusal.
## Streaming
```sh
curl -N localhost:8000/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"model":"m","messages":[{"role":"user","content":"Count to ten."}],
"max_tokens":100,"stream":true,"stream_options":{"include_usage":true}}'
```
Server-sent events in OpenAI's delta format, terminated by `data: [DONE]`. **Nothing is sent while
the prompt is read**: the first event (the `role: assistant` chunk) goes out with the first generated
token, so a client timing its first event measures time-to-first-token. `return_progress` adds
llama-server's `prompt_progress` chunks during prefill; `timings_per_token` adds `timings` to every
chunk.
## Completions, embeddings, images, audio, video, tokens
- **Completions** — `/v1/completions` takes `prompt` (string or token ids), `echo`, `logprobs`,
`preserved_tokens`. `/completion` is llama-server's native route; `/infill` is fill-in-the-middle.
- **Embeddings** — `input` is text, a batch, or token ids; `encoding_format` `float`/`base64`;
`dimensions` truncates to the first N and renormalises; `prompt_name` prepends the model's named
prompt. Pooling is the model's; the engine does not renormalise.
- **Images** — `/v1/images/generations` returns `b64_json`; `size` defaults to 2048×2048. Edits take
one picture or up to ten (`image[]`), multipart as OpenAI's SDKs send them, with an optional mask
(repaint only where transparent). `partial_images` streams previews as SSE.
- **Speech to text** — `/v1/audio/transcriptions`; `/v1/realtime` streams audio in and words back as
they are spoken. The model's prompt shape comes from its plugin's audio format, not from the server.
- **Speech and music** — `/v1/audio/speech` (whole or streamed) with `instructions` describing a voice
(Breeze-TTS-2) or speakers written in the input (`<|speaker:1|>`, `[laughs]` for drama-3); a music
model takes the lyrics as `input` and the description as `instructions`. `/v1/audio/speech/stream`
streams text in and audio out as each sentence completes.
- **Video** — `/v1/videos` is a job: a video with its stereo soundtrack from a prompt, 5–14 s, up to
1344×768, made in the background; `GET /v1/videos/:id/content` downloads the MP4.
- **Tokenising** — `POST /tokenize` accepts vLLM's shape (`prompt` or `messages` in, `prompt` out) and
llama-server's (`content` in/out, `with_pieces`). `POST /detokenize` reverses it.
`GET /get_tokenizer_info` reports the tokeniser configuration.
## Request fields
Accepted fields, in OpenAI's, vLLM's and llama-server's spellings:
| field | notes |
|---|---|
| `model` | accepted, not enforced |
| `messages` / `prompt` / `input` | chat / completion / embedding |
| `max_tokens`, `max_completion_tokens`, `n_predict` | default is what the context leaves; `-1` is llama-server's "no limit"; clamped to context and to `--max-tokens-cap` |
| `temperature`, `top_p`, `top_k`, `min_p`, `typical_p` | sampler |
| `presence_penalty`, `frequency_penalty`, `repetition_penalty`/`repeat_penalty`, `repeat_last_n` | penalties |
| `xtc_probability`, `xtc_threshold`, `dry_*` | XTC and DRY |
| `seed` | absent means "pick one", not 0 |
| `n` | 1..`--max-n`; prefix cache makes the fan-out nearly free after the first |
| `stop` | string or array, bounded by `--max-stop-strings`/`--max-stop-bytes` |
| `priority` | scheduler hint |
| `ignore_eos` | generate to `max_tokens` whatever the model emits |
| `stream`, `stream_options.include_usage`, `timings_per_token`, `return_progress` | streaming controls |
| `samplers` | llama-server's order; this engine's chain is penalties, DRY, top-k, top-p, min-p, typical, XTC, with temperature first or last; any other list is refused by name |
| `session_id` | labels the conversation on `/sessions`, dashboard and `DELETE` |
| `control_vector` | `true` applies the deployment's vector to this request; absent takes the deployment default; a deployment with no vector refuses `true` |
| `logprobs`, `top_logprobs`, `n_probs` | up to 20 alternatives; not with `echo` |
| `tools`, `tool_choice`, `parallel_tool_calls`, legacy `functions`/`function_call` | chat only |
| `add_generation_prompt`, `chat_template_kwargs`, `continue_final_message` | chat only |
| `reasoning_format`, `reasoning_effort`, `enable_thinking`, `thinking_token_budget`, `preserve_thinking` | chat only; levels are the template's (`capabilities.reasoning_efforts`); `none`/`minimal` turn thinking off |
| `response_format`, `json_schema`, `grammar` | structured output; `grammar` and `response_format` are mutually exclusive |
| `echo`, `preserved_tokens` | `/v1/completions` only |
| `encoding_format`, `dimensions`, `prompt_name`, `image_tokens` | `/v1/embeddings` only |
| `images_config` | image models only: a named processor mode or an object restating served sizes |
## What is refused, ignored, and clamped
The contract: **a 200 means every field that could change the answer was honoured.** Fields fall
into four classes.
- **Inert** — accepted and ignored because they cannot change the answer: OpenAI/OpenRouter
bookkeeping (`user`, `metadata`, `store`, `service_tier`, `prompt_cache_key`, …) and llama-server's
own speed/slot knobs (`cache_prompt`, `id_slot`, `n_cache_reuse`, `backend_sampling`,
`speculative.*`, …). The prefix cache and speculation here are the server's and never change output.
- **Guarded** — known, unimplemented, and able to change the answer: accepted at their off value,
refused by name otherwise. `logit_bias` (empty), `mirostat` (0), `top_n_sigma` (≤0),
`dynatemp_range` (≤0), `min_keep` (0), `post_sampling_probs` (false), `best_of` (≤1), `suffix`
(empty), `lora` (empty — an adapter is chosen by `model`), `modalities`/`audio` (`["text"]`/null),
`n_indent` (0), `t_max_predict_ms` (≤0), `return_tokens` (false), `grammar_lazy`,
`grammar_triggers`, `preserved_tokens` (empty on chat).
- **Ignored** — anything else unknown, logged at debug. An unknown field is far more often a client's
extension than a misunderstood request.
- **`max_tokens` is the exception**: if prompt + `max_tokens` exceeds the context, it is **clamped**
and served, ending `finish_reason:"length"`, because refusing would produce no tokens instead of
fewer. A prompt that does not fit on its own is a 400 `context_length_exceeded` naming
`n_prompt_tokens`, `n_ctx`, and how many tokens must go.
## Anthropic, Responses, llama-server
- `/v1/messages` is translated into a chat request, so chat defaults, refusals and limits apply.
Streamed (`message_start`, content blocks, `message_delta`, `message_stop`) or not:
`ANTHROPIC_BASE_URL=http://host:8000 ANTHROPIC_API_KEY=$KEY claude` works. `/v1/messages/count_tokens`
returns the prompt length.
- `/v1/responses` maps `instructions`/`input` (strings, messages, `function_call`,
`function_call_output`, `reasoning` items) into the chat request; `max_output_tokens`,
`reasoning.effort`, `text.format` map to chat fields; undefined fields pass through. The event stream
carries `output_index`, `content_index`, `sequence_number`, and opens with `response.created` and
`response.in_progress`. A reply cut at `max_output_tokens` is `incomplete`. Function tools a
`namespace` groups are named `<namespace>__<name>`; built-in tools and freeform custom tools are
left out.
- `/completion`, `/completions`, `/infill` are llama-server's own shapes, for llama.vim/llama.vscode.
## Auth, CORS, errors
- `--api-key KEY` requires `Authorization: Bearer KEY` or `X-Api-Key: KEY` on every request except
`/health`, `/v1/health`, `/ping` and the dashboard page. An empty key is refused at startup.
- CORS headers are sent unless `--no-cors`.
- Errors are JSON. Over-context is 400 `context_length_exceeded`; a full queue is 429 with
`Retry-After`; a capability the model lacks is 501; a guarded field is 400 naming the field and its
accepted off value.
## Related
- [[Radiance Serve Options]] — the defaults every omitted field takes.
- [[Radiance Operations]] — `/health` semantics, metrics, dashboard.
- [[frank-archon]] — the harness contract built on this surface.
@@ -1,142 +0,0 @@
---
type: "reference"
title: "Radiance Model Families — arch ids, descriptors and recipes"
description: "Which radiance plugin claims which model family, and the conversion command and recipe of record for each."
tags:
- llm
- radiance
- models
- reference
timestamp: 2026-10-30
---
# Radiance model families
The selection key is the exact **(architecture id, quantisation descriptor)** pair, so a family at
two weight formats is two `.so` files. A checkpoint's id is its `config.json` `model_type` without
underscores (`qwen3_5` → `qwen35`); `rad-convert --arch ID` overrides it. `data/recipes/` is the
authority: **read the recipe's header** — it names the exact `rad-convert` line, its `--set` keys
and whether it needs calibration.
## The plugin table
| plugin | id | quant | what it serves |
|---|---|---|---|
| `llama_fp8` | `llama` | `fp8_e4m3` | plain LLaMA decoder (MiniCPM5, Llama 3, derivatives reporting `model_type: llama`) from a checkpoint holding E4M3 codes + `weight_scale_inv` |
| `llama_dense_fp8` | `llama` | *(empty)* | the same graph from a bf16 checkpoint, linears made block fp8 by a recipe |
| `qwen35_bf16` | `qwen35` | *(empty)* | Qwen3.5/3.6/3.8 dense hybrid (GDN + gated attention), bf16 |
| `qwen35_fp8` | `qwen35` | `fp8_e4m3` | the same, block-scaled FP8 weights and activations |
| `qwen35_w8a8` | `qwen35` | `w8a8_fwht128` | int8 trunk: i8 weights, bf16 scale per 128 of K, 128-wide Hadamard, int8 activations |
| `qwen35moe_fp8` | `qwen35moe` | `fp8_e4m3` | `qwen35_fp8` with a routed feed-forward |
| `qwen4exp_fp8` | `qwen4exp` | *(empty)* | Qwen4-Exp / Qwen3.8-Flash-Next: a bf16 checkpoint quantised at convert, or Qwen's FP8 release as it ships |
| `glm5next` | `glm5next` | *(empty)* | GLM-5.3-Flash: four mHC residual streams, 34 KDA + 11 sparse MLA layers with a DSA indexer, 288 MXFP4 experts top-8, block-FP8 attention/dense at f32 scales, MTP drafter, vision tower |
| `hyv3_bf16` | `hyv3` | *(empty)* | Tencent Hy-Memory-30B-A3B bf16; bf16/W4/W8A8/block-FP8/MXFP4 by recipe |
| `hyv3_fp8` | `hyv3` | `fp8` | Hy-Memory-30B-A3B-FP8: imported per-tensor E4M3, one dense layer then sigmoid-routed MoE |
| `hyv3_gguf` | `hyv3` | `gguf_ftype_15` | a llama.cpp Hy-Memory Q4_K_M GGUF, dequantised and recipe-converted |
| `gemma4_bf16` | `gemma4` | `compressed-tensors` | Gemma 4 31B dense (and E2B/E4B at one rank): sliding-window + proportional-RoPE global attention, GELU, softcapped logits; int4/Q8 codes per 128; assistant (shared-KV), DFlash or DFlash2 drafter; vision |
| `gemma4` | `gemma4` | *(empty)* | Gemma 4 text from Google's bf16 checkpoint (26B-A4B MoE, dense sizes) with z-lab's DFlash |
| `gemma3` | `gemma3` | *(empty)* | Gemma 3 dense (TranslateGemma 27B): sliding-window + global attention, GemmaRMSNorm, GELU; bf16 or int8/MXFP4/asym-int4 linears; DFlash2 drafter |
| `lfm2_bf16` | `lfm2` | *(empty)* | LiquidAI LFM2 / LFM2-VL, bf16, one rank |
| `embgemma2` | `embeddinggemma2` | *(empty)* | EmbeddingGemma 2: bidirectional encoder with Gemma 4 vision tower and audio conformer, pooled |
| `nomicbert` | `nomicbert` | *(empty)* | nomic-embed-text-v2-moe: bidirectional post-norm encoder, 8 experts top-2 on every second layer, mean-pooled |
| `nemotron35_embed` | `mistral3` | *(empty)* | Nemotron 3.5 Embed 1B/8B and Rerank 8B: bf16 text/image retrieval with a Pixtral tower |
| `qwenimage21` | `qwenimage21` | *(empty)* | Qwen-Image 2.1 / 2.1-Turbo image generator and editor (diffusers pipeline) |
| `krea2` | `krea2` | *(empty)* | Krea 2 Raw/Turbo: Qwen3-VL-4B tapped layers, 12.9B single-stream DiT, Wan 2.1 VAE |
| `sdxl` | `sdxl` | *(empty)* | Stable Diffusion XL and finetunes (Illustrious, NoobAI, Pony), single-file checkpoint |
| `flux3` | `flux3` | *(empty)* | FLUX.3 [klein] and base: image generation and edits from up to ten pictures |
| `flux3distilled` | `flux3distilled` | *(empty)* | the same plugin under the step-distilled release's own pipeline name |
| `anima` | `anima` | *(empty)* | Anima Base/Aesthetic/Turbo from the release's three single files |
| `qwen3asr` | `qwen3asr` | *(empty)* | Qwen3-ASR: Whisper-style audio encoder over a dense Qwen3 decoder, speech to text |
| `minimaxmusic3` | `minimaxmusic3` | *(empty)* | MiniMax-Music3: Qwen3-8B frame model, depth decoder, flow-matching DiT, DAC vocoder |
| `breeze` | `breeze` | *(empty)* | Breeze-TTS-2: T5Gemma 2 text encoder, Qwen3 backbone, 16-codebook depth decoder, Qwen3-TTS 12 Hz codec |
| `fishomni` | `fishomni` | `bsgemm-moe` | Fish Audio drama-3: Qwen3.5-35B-A3B backbone, block-FP8 experts at f32 scales, 13-layer codebook tower, Fish S2 DAC/codec |
| `minimaxh3` | `minimaxh3` | *(empty)* | MiniMax-H3 video with stereo sound: Qwen3-VL-32B text encoder, 33B DiT, tiled ViT decoder, BigVGAN audio |
| `ltx2` | `ltx2` | *(empty)* | LTX-2.5 base/distilled: Gemma 4 LM + text connectors, duration head, audio-video DiT, causal video VAE, audio VAE |
## Recipes of record, by family
`$SRC` is a downloaded checkpoint; `$CALIB` is a calibration directory (see
[[Radiance Conversion]]). Paths are shortened to the recipe name.
| target | command |
|---|---|
| Qwen3.8-27B bf16 | `rad-convert $SRC -o q38-27b-bf16.rad --recipe data/recipes/q38-27b-bf16.recipe` |
| Qwen3.8-27B block fp8 + DFlash2 | `rad-convert $SRC -o q38-27b-fp8.rad --recipe data/recipes/q38-27b-fp8block-df2.recipe --draft-model $DFLASH2 --set quantization_config.quant_method=fp8 --set quantization_config.fmt=e4m3 --set "quantization_config.weight_block_size=128 128" --set quantization_config.activation_scheme=dynamic` |
| Qwen3.8-27B MXFP4 + DFlash2 | `rad-convert $SRC -o q38-27b-mxfp4.rad --recipe data/recipes/q38-27b-mxfp4-df2.recipe --draft-model $DFLASH2 --set quantization_config.quant_method=fp8 --set quantization_config.fmt=e4m3` |
| Qwen3.8-27B w4a16 (an int4 release) | `rad-convert $SRC -o q38-27b-w4a16.rad --recipe data/recipes/q38-27b-w4a16.recipe` |
| Qwen3.8-27B Quark AWQ-MXFP4 | `rad-convert $SRC -o q38-27b-quark.rad --recipe data/recipes/q38-27b-quark-mxfp4-df2.recipe --draft-model $DFLASH2` |
| Qwen3.8-27B ParoQuant int4/int5/MXFP4 | `rad-convert $SRC -o q38-27b-paro.rad --recipe data/recipes/q38-27b-paro-mxfp4-df2.recipe --draft-model $DFLASH2` |
| Qwen3.8-27B NVFP4 (compressed-tensors or ModelOpt) | `--recipe data/recipes/q38-27b-nvfp4-df2.recipe` or `q38-27b-modelopt-nvfp4.recipe`; every NVFP4 weight must take a rule (no kernel serves NVFP4) |
| Qwen3.8-Flash-Next bf16 reference | `rad-convert $SRC -o flashnext-bf16.rad --max-ctx 4096` (no recipe; served with `--weights-disk-tier`) |
| Qwen3.8-Flash-Next four-bit production | `CALIB=$CALIB rad-convert $SRC -o w4nl64-i8lm.rad --recipe data/recipes/qwen4exp-w4nl64-i8-hc8m.recipe` (GPTQ experts + int8 trunk) |
| Qwen3.8-Flash-Next uncalibrated four-bit | `--recipe data/recipes/qwen4exp-w4.recipe` (rtn, no calibration) |
| Flash-Next from an AutoRound int4 release | `--recipe data/recipes/qwen4exp-autoround-i8-hc8m.recipe` |
| Gemma 4 31B w4a8 (no calibration) | `--recipe data/recipes/gemma4-31b-dense-bf16-w4a8.recipe --set quantization_config.quant_method=compressed-tensors --draft-model $ASSISTANT` |
| Gemma 4 31B w4a8 + DFlash / DFlash2 | `gemma4-31b-dense-bf16-w4a8-df.recipe` / `-w4a8-df2.recipe`, same `--set` |
| Gemma 4 31B Q8 grid | `gemma4-31b-dense-bf16-q8-df.recipe` |
| Gemma 4 26B-A4B + DFlash | `gemma4-26b-a4b-df.recipe`; `-pad768` when the experts were padded to 768 (down may then be block fp8) |
| Gemma 4 E4B (Nemotron Bolt) | `gemma4-e4b-bf16-df.recipe`, `-i8g-`, `-a4-`, `-w4a8-`, `-w8a8-` variants |
| TranslateGemma 27B | `translategemma-27b-i8a8.recipe` (calibration container), then `CALIB=$CALIB translategemma-27b-mxfp4-gptq.recipe` (or `-df2`) |
| GLM-5.3-Flash | `rad-convert $AMD_QUARK -o glm53.rad --recipe data/recipes/glm53-mxfp4.recipe` |
| Hy-Memory-30B-A3B | `hy-memory-30b-a3b-bf16-fp8.recipe`, `-mxfp4`, `-w4`, `-w8`, `hy-memory-30b-a3b-fp8.recipe`, `hy-memory-30b-a3b-gguf-q4k-to-w4.recipe` |
| MiniCPM5-2B | `minicpm5-2b-dspark.recipe` (or `-bf16-dspark`) with `--draft-model $DSPARK` |
| Qwen3.6-35B-A3B | `q36-35b-a3b-fp8.recipe` |
| Qwen-Image 2.1 | `qwen-image-2.1-fp8.recipe` (Turbo uses the same file) or `-bf16` |
| Krea 2 | `krea-2-fp8.recipe`, `krea-2-bf16.recipe`, `krea-2-fp8-unscaled.recipe` for a plain-FP8 finetune file |
| FLUX.3 [klein] | `flux-3-klein-fp8.recipe` |
| SDXL / finetunes | `sdxl-bf16.recipe --tokenizer <openai/clip-vit-large-patch14>/tokenizer.json` |
| Anima | `anima-fp8.recipe` (or `-bf16`) + `--add` ×2 + `--config` + `--tokenizer2` + `--arch anima --set anima.variant=aesthetic` |
| LTX-2.5 | `ltx-2.5-fp8.recipe` (or `-bf16`); full DiT: symlink a folder whose `transformer/` is `transformer_full/` and `--set scheduler.use_dynamic_shifting=1 --set scheduler.shift_terminal=0.1` |
| MiniMax-H3 / FastH3 | `minimax-h3-fp8.recipe`, `minimax-h3-w8a8.recipe`, `minimax-h3-curve-*` for curve-form checkpoints |
| MiniMax-Music3 | `minimax-music3-bf16.recipe` |
| Breeze-TTS-2 | `breeze-tts2-bf16.recipe` |
| drama-3 | `drama3-fp8.recipe` |
| nomic-embed-v2-moe | `nomic-embed-v2-moe-bf16.recipe` |
| Unlimited-OCR | `unlimited-ocr-bf16.recipe` |
| Qwen3-ASR | no container: serve the checkpoint directory as it ships |
| EmbeddingGemma 2, Nemotron Embed/Rerank | serve the checkpoint, or convert bf16 with no recipe |
A checkpoint served at its own precision needs no container at all:
`radiance --model /srv/models/hf/Qwen/Qwen3.8-27B-FP8`. Convert when you want a tier only a container
has (quantised encodings, a drafter, a second vocab) or a smaller file.
## Family-specific traps
- **`head_dim` is read, never derived.** On the Qwen hybrids it is 256 while `hidden_size /
num_attention_heads` is 213; a derived value silently breaks projection widths, KV page size and the
attention kernel's band.
- **A fused projection shards by part, not by contiguous rows.** A tensor-parallel rank owns a slice
of *each* stacked projection in a fused `[gate|up]` or `[q|k|v|z]`; a contiguous slice would hand
rank 0 the whole gate, and the model would produce fluent wrong text. This is declared through
`RadWeightDecl::row_parts`, never inferred from the container.
- **Gemma 4 26B-A4B:** the routed experts' `gate_up` may be block fp8, but `down` stays bf16 unless the
experts were padded to 768 — hence the `-pad768` recipe.
- **GLM-5.3-Flash:** every weight is served as stored except the latent attention's small `kv_b`,
decoded to bf16. Its KV cache is bf16 only (the plugin refuses `fp8` at startup), and at 172 GiB it
needs `--tp 8`.
- **TranslateGemma 27B:** the checkpoint's template takes structured turns naming two languages and
refuses the plain turn the server renders at startup. The container therefore serves a plain Gemma 3
template, and a client sends on `/v1/completions` the prompt the checkpoint's own template renders.
- **ParoQuant containers made before 1.5.0 are refused at startup.** Convert again with the old
container as `--reuse`; every tensor that did not change is copied.
- **SDXL carries no tokeniser.** Pass CLIP's `tokenizer.json` beside the single file. A Krea 2
finetune shipped as one DiT file in Krea's key names is rewritten into a directory whose
`transformer/` holds it (`scripts/krea2_native.py`), with the release's other parts copied in.
- **Anima** ships as three single files: `--add` the text encoder and VAE, `--config` a Qwen3-0.6B-Base
directory (config + tokenizer), `--tokenizer2` ComfyUI's T5 tokenizer, `--arch anima`,
`--set anima.variant=…`.
- **drama-3's codec is `codec.pth`**, a `torch.save` file read as data, never unpickled.
- **Generators that load LoRA files declare their module aliases**; the checkpoint and a ComfyUI port
disagree in ways that load without error and generate garbage. The per-plugin alias table is in
`docs/ARCHITECTURES.md` ("Adapter key names of the generators").
- **Licences travel with the checkpoint, not the engine.** Qwen-Image 2.1 is non-commercial;
MiniMax-H3's licence excludes the EU, UK, Republic of Korea and USA; drama-3's community licence
needs a commercial licence to serve speech to others, and cloning a real person needs consent;
Breeze-TTS-2 is research/non-commercial.
## Related
- [[Radiance Conversion]] — the tool, recipe language and quantisers used above.
- [[Radiance Serve Options]] — the flags each family's compose file uses.
- [[Radiance Architecture Plugins]] — writing a plugin for a family not in this table.
- [[Quantization Production]] — the fleet's campaign and gates.
@@ -1,240 +0,0 @@
---
type: "howto"
title: "Radiance Operations — running, reading, restarting"
description: "Running a radiance server: the container contract, host prerequisites, readiness, ledgers and safe restarts."
tags:
- llm
- radiance
- operations
- howto
timestamp: 2026-10-30
---
# Radiance operations
This is the engine-level runbook. The fleet's deployed shape, its unit and its deploy path live on
[[frank-archon]]; the flags themselves on [[Radiance Serve Options]].
## Install
```sh
git clone https://codeberg.org/StillDeadcode/radiance.git && cd radiance
./build.sh # configure, build for the cards present, run fast tests
./build.sh --no-hip # host backend only
cmake -S . -B build -G Ninja -DRAD_GPU_TARGETS='gfx1201' # cards this machine lacks
cmake --install build # after -DCMAKE_INSTALL_PREFIX=/opt/radiance at first configure
```
Needs Linux, git, a C++20 compiler, CMake 3.21+, Ninja; for device kernels ROCm 10.1 or 7.2
(`hipcc`, `ROCM_PATH` if not `/opt/rocm`).
- **Name the prefix at the first configure.** The default plugin path `<prefix>/share/radiance` is
compiled in from that configure; `cmake --install build --prefix DIR` alone gives a tree that looks
for its plugins elsewhere.
- The prefix gets `bin/`, `include/radiance/`, `lib/cmake/radiance/`, `libexec/radiance/`,
`share/radiance/` (plugins and recipes) and `share/doc/radiance/`.
- With ROCm 10.1 the build turns VOPD pairing off because its LLVM 24 pairs in heap-address order, so
two builds of one source can differ. The Docker image's compiler has the fix and keeps pairing
(faster fp8 prefill GEMMs); declare such a compiler with `-DRAD_HIP_VOPD_REPEATABLE=ON`. ROCm 7.2 is
unaffected.
Run without Docker:
```sh
export RADIANCE_HOME=$PWD/build/radiance_home # plugins; an install finds its own
build/bin/radiance --model /srv/models/rad/minicpm5-2b-fp8.rad --port 8000 <flags>
build/bin/radiance --model StillDeadcode/minicpm5-2b-fp8 --port 8000 <flags>
```
`--model` also takes a repo id: the server runs `scripts/hfget.sh` into `--download-dir`, serves the
one `.rad` at the repo's top level, or the directory as a checkpoint when it holds none. A repo with
several containers is refused, naming them. Every start checks the hub; a start that cannot reach it
serves the copy a complete earlier fetch left, and a transfer that fails part-way stops startup.
## Container
The published image is `stilldeadcode/radiance`, kernels built for `gfx1201`. The host needs Docker
and an `amdgpu` driver supporting ROCm 10.1 — **not** ROCm itself.
```sh
docker run -d --name radiance --network host --init --ulimit memlock=-1 \
--device /dev/kfd --device /dev/dri --security-opt seccomp=unconfined \
--group-add "$(getent group render | cut -d: -f3)" \
--group-add "$(getent group video | cut -d: -f3)" \
-v /srv/models/rad:/models:ro \
radiance --model /models/m.rad --tp 2 --host :: --port 8000
```
Each option is load-bearing:
| option | why |
|---|---|
| `--device /dev/kfd --device /dev/dri`, `render`/`video` groups | the cards |
| `--security-opt seccomp=unconfined` | Docker's default profile refuses io_uring (a disk-tier n-gram table's reads) and the NUMA calls the ROCm runtime makes for host memory |
| `--init` | an init process as PID 1 forwards SIGTERM; the engine finishes its step and releases the cards |
| `--ulimit memlock=-1` | a host-side pinned n-gram copy can be tens of GiB, far past Docker's default |
| `--network host` | the server binds the host's addresses directly, IPv4 and IPv6, no NAT |
The image is `FROM scratch` around the stripped install tree: busybox as the only shell, plus `curl`,
`jq` and a CA bundle for `hfget.sh`; **no Python, no package manager**. Tools (`rad-convert`,
`rad-info`, `rad-kbench`, `rad-tune`, `rad-schemas`, `rad-tokcheck`) are on `PATH`; recipes are in
`/opt/radiance/share/radiance/recipes/`. Its entrypoint is `radiance`. It sets
`GPU_MAX_ALLOC_PERCENT=100` and `GPU_MAX_HEAP_SIZE=100` because the HIP runtime's defaults cap one
allocation and the heap below what the placement plan hands out.
Build your own with `docker/build.sh` (`-t 'gfx1200;gfx1201'` for targets, `-j` compile jobs, `-E`
to skip tests, `-s host` to build remotely). The context is `git archive` of the commit, so the image
holds only committed content. The `rootfs` stage fails the build if any ELF cannot be resolved inside
a chroot of the tree.
Compose: one file per model plus `common.yaml`, copied to the serving host with an `.env` setting the
image, models directory (mounted read-only at `/models`), API key, port, user and GPU groups. Every
model listens on the same port, and most use both cards, so **run one at a time**. The models
directory must be on a local filesystem that does direct I/O (ext4, XFS, btrfs); on btrfs with
compression, a container copied into a `chattr +C` directory is stored uncompressed and direct reads
work.
A plugin of your own is mounted and put first on the search path; nothing in the image changes:
```sh
-v /srv/radiance-plugins:/plugins:ro -e RADIANCE_HOME=/plugins:/opt/radiance/share/radiance
```
## Host prerequisites (upstream's two-R9700 checklist)
| item | setting |
|---|---|
| firmware | Above 4G Decoding on (Resizable BAR needs it); CSM off (pure UEFI) |
| kernel command line | `iommu=pt amdgpu.ppfeaturemask=0xfff7ffff amdgpu.ras_enable=0` |
| power/clock | OverDrive bit needed for `ppfeaturemask`; apply a power cap only while no process holds `/dev/kfd` |
| memlock | `--ulimit memlock=-1` (container) / `LimitMEMLOCK=infinity` (systemd) when host memory is pinned |
| two engines, one tmpfs table | `amdgpu.no_system_mem_limit=1`, or the second pin can be refused |
| storage | a local fs with O_DIRECT for the container; a tmpfs sized for the table for `--ngram-placement ram=DIR` |
Why each: `iommu=pt` keeps DMA identity so the cards can reach each other's memory and pinned host
pages; `ppfeaturemask` enables the power controls; `ras_enable=0` turns VRAM ECC off (ECC reserves
about 2 GiB a card). Verify peer access with a rate in `lspci`/the card tables — a missing cell means
no peer access; check `iommu=pt` and the BAR size. The fleet's own boot-line law for Dankclaw
(`iommu=pt`, tracked in the repo) is on [[dankclaw-iommu-translation-off]]; Dankserver's cards are a
different box and its posture is on [[Dankserver System Specs]].
## Readiness: the receipt, then `/health`
Startup states every budget **before anything is allocated**, then loads. `/health` (and
`/v1/health`, `/ping`) answers only once the load completes, so a bare health gate is honest. While it
loads, expect 503 for minutes; do not restart over it. `--live` draws the live view (redirected, one
`greppable key=value` line). `/diagnostics` returns the startup report as JSON — **same-machine-only
without an API key** — with every option and its source and the hardware read again now.
Read the receipt in this order:
1. `elastic` — what the experts and KV cache share after non-expert weights.
2. The split: `experts … / kv cache …`, and `unclaimed` if any.
3. `kv: the pool backs N % of the worst case … M full-length sessions` — the referee for whether the
demand signal (`--max-model-len` × `--max-num-seqs`) is funded.
4. `activation arena` and what each size lends; `prefill staging`; `hardware queues` and their VRAM.
5. `host_pool` opened/used, `vram_weights`, the PLE or n-gram placement, the vision tower's placement.
6. `device: the N cards reach each other's memory directly` or the failed pairs;
`expert prefetch on/off`; `chunk geometry`; `speculation:`; `chained launches`; `draft trees`.
7. `card N settled at … free` — memory taken after the budget read comes out of headroom; a warning
names the `--gpu-headroom-mib` that clears it.
A boot refusal prints the reason and exits nonzero; when it happens inside `podman run`, the
container stops at once and the unit's restart policy re-runs the same refusal. A unit in
`activating (auto-restart)` with a fast nonzero exit is therefore a **preflight/refusal or a boot
refusal**, not an engine fault: read the journal line and the argv, not the GPU.
## Lifecycle and restart
- SIGTERM finishes the current step and releases the cards. In a container, `--init` forwards it;
`podman stop -t N`/`docker stop` is the graceful path.
- A systemd unit should `exec podman wait` (or run in the foreground) so the script's lifetime is the
container's: a crash is a nonzero exit and the restart policy is real. Set `TimeoutStopSec` above
the stop grace, or systemd records `Failed with result timeout` on a healthy stop.
- `Restart=always` rather than `on-failure` for a server whose front end can exit cleanly after a
worker fault.
- Model load is slow (minutes cold). The deployer's LLM wait budget is on [[Deployment Flow]]; a
container restart loop during a legitimate load is worse than waiting.
## Two engines on one set of cards
- `ROCR_VISIBLE_DEVICES` picks each engine's cards explicitly.
- `--weight-prefetch off`: a weight read beside a collective puts a queue switch into every decode
step when another server shares the cards.
- `--ngram-placement ram=DIR`: two engines on separate card sets hold one table in host memory
(`DIR/ngram-<id>.tab` in a tmpfs, `MAP_SHARED`, pinned). The `<id>` hashes the container path, size,
mtime and the table's place, so a file never serves another model. The first engine fills it; a
second verifies samples and only pins it; the last to leave deletes it. The driver counts a shared
page against every process that pins it, so the pinned total is twice the table.
- The engine prefers discrete cards (all of the first one's architecture), integrated only when
there is nothing else; on integrated graphics device memory is host memory and the budget leaves an
eighth of host RAM to the host.
## Logs and diagnostics
- `-v` / `-vv`; `RADIANCE_LOG` sets the level without a flag. `RADIANCE_LOG_STEPS=1` prints every
step's token count (how a chunk geometry is confirmed in practice).
- The engine writes to stderr. Under a container, the journal holds only the container's own output;
a rootless `podman logs` covers only the current instance across recreations.
- `/metrics` is Prometheus text: engine steps by kind and their seconds, decode/prefill rates,
prefill chunks held back, steps held by interleave, draft acceptance and depth, prompt-lookup
proposals held, KV bytes and the tokens they address, stream misses and bytes, prefix hits and
evictions, preemptions and queue depth. `/stats`,
`/graph`, `/sessions` back the dashboard. `scripts/statsdump.py` records `/stats` to JSONL with
time and gap, for a machine with no scraper (its key comes from `--api-key` or
`RADIANCE_API_KEY`).
- `RADIANCE_PROFILE_EVERY`, `RADIANCE_DUMP_BUF`/`RADIANCE_DUMP_DIR`, `RADIANCE_OP_ORACLE`(+) and the
`RADIANCE_DEBUG_*` family are debug instruments; they are not deployment settings.
## Verifying a deploy (fleet law)
`deploy.sh` green is not proof of what is running. A leftover systemd drop-in or a stale container
identity can serve a different model at a different shape while `/health` is 200. After deploying
any recently-benched service:
1. `systemctl show <svc> -p DropInPaths` — must be empty.
2. `tr '\0' ' ' </proc/$(pidof radiance)/cmdline` (or the container's `Config.Cmd`) — the argv you
meant.
3. The boot ledger — the shape you meant.
4. A behavioural probe — `/v1/models` for the served id, one chat turn, one tool call, and a vision
probe with a solid colour and an exact expected word.
`/v1/models` is an echo on some runtimes; on radiance it is authoritative for the alias, but the
argv and the ledger are what prove the payload and shape. The three-pin revert discipline for a
payload swap is on [[Model Swap and Rollback]]; the live deployment is [[frank-archon]].
## Security posture
- `--api-key` requires a bearer token or `X-Api-Key` on every route except `/health`, `/v1/health`,
`/ping` and the dashboard; the key is a command-line argument, so it is readable in
`/proc/<pid>/cmdline` by any local user of that host. On a single-tenant inference box the network
boundary is the real control; do not run one where untrusted local users share the host.
- `/diagnostics` is same-machine-only without a key, because it prints the whole configuration.
- `--security-opt seccomp=unconfined` is a genuine widening, required by io_uring and the ROCm
runtime's NUMA calls; it belongs on a dedicated model host, not a general-purpose one.
- Mount model directories read-only; a container with `/models` writable adds nothing a serve needs.
- A control vector can remove refusal behaviour. Loading one is a safety decision, not a
performance one; the flag is `--control-vector`, and `--control-vector disabled` is the default.
## Failure signatures at startup
| what you see | what it is |
|---|---|
| unknown/retired flag, or a flag refused by name | a boot refusal — the pin does not have it; change the trio, not the invocation |
| "no plugin claims `<arch>` at `<quant>`; served at: …" | the (id, descriptor) pair has no plugin; fix the descriptor with `--set`/convert |
| "reports ABI N, engine M" | rebuild the plugin against this engine |
| budget refusal / `host_pool` short | the shape does not fit this card/host; lower demand or pin more host RAM |
| `--ngram-placement ram` refuses | memlock short; raise it or use `auto`/`disk` |
| `--kv-cache-dtype fp8_v4`/`k6_v4` refused, plugin named | no kernels for this geometry |
| `--p2p on` refuses, pairs named | the driver has no peer access; use `auto`/`off` |
| first launch of a never-run kernel faults | too little `--gpu-headroom-mib` for its code object |
| unit loop, no container, no engine log | a preflight refusal (TTM/memlock/digest/model) re-run by `Restart=always` |
## Related
- [[Radiance Serve Options]] — every knob and its law.
- [[Radiance HTTP API]] — readiness and the wire.
- [[Radiance Tools]] — how to corroborate a receipt.
- [[frank-archon]] — the fleet's radiance unit, guard chain and Eras.
- [[Model Swap and Rollback]] — the swap discipline this page's verification serves.
- [[Deployment Flow]] — how a committed change reaches the host.
@@ -1,371 +0,0 @@
---
type: "howto"
title: "Radiance Plugins — kernel libraries and quantisers"
description: "Writing radiance kernel and quantiser plugins: the ABI, kernel rows, hooks, encodings, and the build path."
tags:
- llm
- radiance
- plugins
- howto
timestamp: 2026-10-30
---
# Radiance plugins — kernels and quantisers
Three kinds of `.so` extend the engine, each exporting a few C symbols and linking **nothing of
radiance's** (they compile against the installed `abi/` headers):
| kind | value | contract |
|---|---|---|
| kernel library | `RAD_PLUGIN_KERNEL` (1) | ops for some hardware: a card family, the CPU, an oracle |
| architecture | `RAD_PLUGIN_ARCH` (2) | one model family at one weight format — see [[Radiance Architecture Plugins]] |
| quantiser | `RAD_PLUGIN_QUANT` (3) | weights → encoding planes, for `rad-convert` |
In-tree plugins use the same interface a third party does: each builds with `rad_add_plugin` and is
found and ranked by what it declares, never by its name. `ctest -L oot` rebuilds every bundled
plugin out of tree against an install.
## Identity and ABI
```c
#include "rad_abi.h" /* required; pulls rad_types.h and rad_encoding.h */
uint32_t rad_plugin_abi_version(void) { return RAD_ABI_VERSION; }
const RadPluginInfo* rad_plugin_info(void) {
static const RadPluginInfo i = {
RAD_PLUGIN_KERNEL, "mykernels", "0.1", "a GEMM or two", "gfx1100",
};
return &i;
}
```
- `RAD_ABI_VERSION` (31 at this commit) is bumped on any shape change in a header a plugin compiles
against. Return the macro; a mismatch is refused at load with both numbers printed.
- `build_target` is printed, not trusted: the targets the library was built for (`"gfx1100"`,
`"gfx11-generic"`, several separated by spaces/commas), `"host"`, or `""` for portable. The engine
checks the library's actual code objects instead (§"A library for a new card").
- Optional exports, resolved by exact name: `rad_plugin_open` (return `RAD_E_UNSUPPORTED` to decline
this machine and unload), `rad_plugin_close`, `rad_kernel_concurrent`, `rad_kernel_collective`,
`rad_plugin_reference`, `rad_kernel_reference`, `rad_kernel_chain`.
## What a kernel library exports
```c
int rad_kernel_schema_count(void);
const RadOpSchema* rad_kernel_schema_at(int i);
int rad_kernel_count(void);
const RadKernelInfo* rad_kernel_at(int i);
```
A row declares one op implementation and what it can serve:
```c
static const RadConstraint c_gemm[] = {
RAD_CLE("M", 64), /* M <= 64 */
RAD_CDIV("K", 128), /* K divisible by 128 */
RAD_CIN("dtype", "fp8a8 w4a8") /* dtype in that set */
};
static const RadKernelInfo k[] = {{
.name = "mygemm_m64", .op = "gemm_nt_q", .family = "gemm",
.computes = "row-major A times column-major B, fp8 in, bf16 out",
.shape = "M<=64, K%128==0",
.dtypes = "fp8a8, w4a8",
.domain = RAD_DOMAIN_DEVICE,
.priority = 50,
.constraints = c_gemm, .n_constraints = 3,
.launch = my_launch,
}};
```
- Operators: `RAD_CEQ`, `RAD_CLE`, `RAD_CGE`, `RAD_CDIV`, `RAD_CIN`. The engine takes bucket
boundaries from `LE`/`GE`/`EQ` and resolves once per band; `DIV` and `IN` are not intervals.
- **Declare only the band you serve.** The engine believes the claim; a kernel that says `M <= 2048`
but is correct to 64 is wrong at concurrency.
- `device.*` keys are matched against what the engine measured about the ranks' cards, not against
op parameters: `RAD_CEQ(RAD_FACT_P2P, 1)` means the kernel touches another card's memory. libr4d
pairs every collective with a `_host` twin requiring `device.p2p == 0`, so exactly one matches any
machine.
- **Priority breaks ties within one plugin**, highest first, then declaration order. **Between
plugins the hierarchy is absolute**: the first library in `--kernels` order with any matching row
supplies the op, however specialised a later library's row is. That makes a user's library an
override, not a competitor.
- A derivative parameter (`RAD_DERIVED`) is supplied by the kernel; a constraint on a key the
geometry omits does not hold during matching, and the winner's `RAD_C_EQ` writes it into the
resolved geometry. `RAD_PROLE_SEQ_CHUNK` marks the parameter the scheduler must cut steps on;
`RAD_PROLE_CAPACITY` marks a count the kernel only bounds itself by.
## Operands and extents
`RadArgs.t` is positional in the schema's order; each `RadTensor` carries the extent the caller
narrowed it to, usually smaller than the buffer. Size loops and grids from those extents, never from
a declared maximum. Where an op takes `cu_seqlens`, derive the sequence count from its own length
(`n = len - 1`) and each sequence's rows from `cu[n]` and `cu[n+1]`.
An absent optional operand is a **null `RadTensor.data`**, not a shorter operand count.
`rad_arg_in(a, i)` fetches one correctly; `rad_tensor_rows`, `rad_tensor_lastdim`,
`rad_tensor_row_pitch`, `rad_args_geti_or`, `rad_args_getf_or`, `rad_args_gets_or` are the other
everyday helpers in `rad_plugin.h`.
## Optional hooks
| hook | meaning |
|---|---|
| `init` / `fini` | per-resolved-instance state; `RadArgs.instance` hands it back. `init` runs once per (op, band, domain) after selection and before load |
| `scratch` | bytes of scratch one launch needs; sized at declare from the band's upper bound |
| `layout` / `relayout` | the kernel stores this weight in its own order, and how the loader produces it from the canonical planes |
| `unrelayout` | the inverse, so a tool holding stored bytes can compare against a dense oracle |
| `opd_shape` | size and fill every operand, so a tool can call the kernel cold |
| `replaces` | the chain of conventional ops this kernel computes in one launch |
| `tunables` / `tune_valid` | the instantiation axes, and which combinations are legal at a shape |
`replaces` must be **exact** and is compared byte for byte against the chain — a kernel that is only
close publishes nothing. `tunables` declares axes and legal values, not named variants; the core
takes the cross product, and `deflt` must be launchable (the loader refuses the plugin otherwise).
`rad-tune` measures survivors and writes the cache the selector reads; the chosen point arrives as
ordinary parameters (`rad_args_geti("sk")`), with no variant index.
`layout` answers: `RAD_OK` (own form, `relayout` will produce it), `RAD_E_UNSUPPORTED` (stored as
is — the common case, and the only form that can be read in place from the file/mapped tier),
`RAD_E_DTYPE` (this encoding is not mine — a declare-time error), `RAD_E_SHAPE`, or `RAD_LAYOUT_ANY`
(reads the bytes without interpreting them; claims no storage). A weight only `LAYOUT_ANY` readers
touch is stored as it is.
## What a launch may do
On a card the `RadStream` is **the engine's own AQL queue**, not a `hipStream_t`. The engine binary
defines and serves these HIP entry points:
- `hipLaunchKernel` (and `<<<>>>`, `hipLaunchKernelGGL`);
- `hipMemcpyAsync`, `hipMemcpy2DAsync`, `hipMemsetAsync`;
- `hipStreamSynchronize` (not from a launch — a launch must not synchronise);
- `hipMemset` (runs on the engine's queue and waits);
- `hipGetLastError` / `hipPeekAtLastError`, with HIP's semantics.
**Nothing else in HIP takes an engine stream.** `hipModuleLaunchKernel`, `hipExtLaunchKernel`,
cooperative launches, `hipLaunchHostFunc`, and libraries that launch through them (rocBLAS,
hipBLASLt, MIOpen, hipRTC) would hand HIP a queue it does not know. Header-only device code
(Composable Kernel templates, rocPRIM primitives) compiled into your `.hip` works. `hipEventRecord`
and `hipStreamWaitEvent` on an engine stream are refused by name.
The engine also reads the code object, which limits a kernel to: **no hostcall** (`printf`/`assert`
are refused at the first launch), **no dynamic stack**, **≤ 768 bytes of kernel arguments**, **≤ 64
KiB static+dynamic LDS per dispatch**, and an **uncompressed fat binary**. Device memory a kernel
needs comes from the `scratch` hook, never `hipMalloc` — the engine's allocator knows what is left.
`rad_plugin.h`'s conversions define the result: the reference contract is f32 accumulation, IEEE
round-to-nearest-even, **no FMA and no reassociation**. A plugin whose bf16 narrowing truncates
cannot be checked against the reference. `rad_load_f32`/`rad_store_f32`/`rad_load_bits`/
`rad_store_bits` read and write any core dtype from a plane.
## Colours of agreement
- `rad_kernel_concurrent(i)` — kernel `i` touches only its operands and scratch, so independent
launches may overlap.
- `rad_kernel_collective(i)` — it exchanges data with other ranks; the engine keeps bulk link
traffic away. `2` promises more: the launch ends the hold itself once the other ranks' bytes have
landed, through a `link_gate` word it reaches at system scope; returning `RAD_LINK_ENDED` says it
did.
- `rad_kernel_chain(i)` — the kernel can wait in its own prologue instead of behind the packet
processor's barrier bit. Chained forms are recognised by `r4dchain` in the name and use a
`RAD_CHAIN_REC_BYTES` record at `RAD_CHAIN_KARG_OFF`.
- `rad_plugin_reference` / `rad_kernel_reference` — an oracle library (or an oracle row beside a
device row). A reference library ranks below every unnamed library, is refused for serving unless
`--debug-accept-reference-kernels`, and is what `rad-kbench` checks against.
## Defining a new op
Only when `docs/OPS.md` has no name for it. The vocabulary document owns the rules: what a schema
must declare, and that **the first plugin in hierarchy order to declare an op fixes its schema** — a
later plugin declaring it differently is refused at load, both named. `ctest -R ops_doc` checks the
document against the registries, so the rules live there only. An op in `docs/OPS.md` is describable
by `libref`, and therefore checkable, even when your kernel cannot describe itself; an op you invent
runs only on a library that implements it.
A schema is positional data, not code:
```c
static const RadParamSpec params[] = {
{"M", RAD_P_INT, RAD_REQUIRED, RAD_PROLE_NONE}, /* the ranged extent */
{"n", RAD_P_INT, RAD_REQUIRED, RAD_PROLE_NONE},
{"eps", RAD_P_F64, RAD_REQUIRED, RAD_PROLE_NONE},
{"dtype", RAD_P_STR, RAD_REQUIRED, RAD_PROLE_NONE},
};
static const RadOperandSpec opds[] = {
{"x", RAD_OPD_IN, 0}, {"w", RAD_OPD_WEIGHT, 1}, {"y", RAD_OPD_OUT, 0},
};
static const RadOpSchema schemas[] = {
{"my_op", params, 4, opds, 3, "what it computes, for a human"},
};
```
`rad_kernel_schema_count/at` return this table. `RAD_REQUIRED`/`RAD_OPTIONAL`/`RAD_DERIVED` and the
operand roles (`IN`, `OUT`, `INOUT`, `WEIGHT`, `WTAB`) are the whole vocabulary; the kernel's
constraints and launch do the rest.
## Quantiser plugins
`abi/rad_quant.h` is the whole contract:
```c
int rad_quant_count(void);
const RadQuantizerInfo* rad_quant_at(int i);
typedef struct RadQuantizerInfo {
const char* name; /* what a recipe names */
const char* doc;
const RadQuantOption* options; /* key, RAD_P_INT|F64|STR, default text, doc */
int n_options;
RadQuantEncodingFn encoding; /* the encoding this quantiser would write */
RadQuantRowBlockFn row_block; /* rows per quantize() call; 0 = whole weight */
RadQuantizeFn quantize; /* logical rows -> planes */
RadQuantDecodeFn decode; /* optional, only for a scheme of its own */
} RadQuantizerInfo;
```
The flow for each weight a recipe rule picks: ask `encoding` (given name, shape, and — at declare —
no movement group), let the architecture declare against that encoding, then hand the weight over in
row blocks, so a 51 GB table streams. Other calibration data is the quantiser's own option
(`calib=DIR`); the output is **canonical planes only**, so every kernel that accepts the encoding and
the core's decoder can read it — that is how the converter measures error without knowing the method.
A quantiser's own scheme must be decoded by its `decode` hook (the oracle, `rad-kbench` and the
error report call it), and must return `RAD_E_UNSUPPORTED` for an encoding it did not write. Serving a
new encoding also needs a kernel library whose `layout` accepts it and an architecture that declares
the weight at it — a new format is a new kernel row plus a plugin, never a converter-only change.
Two plugins naming the same quantiser are refused, both named.
### The encoding model (`abi/rad_encoding.h`)
An encoding is the code type, an optional codebook, the grouping of scales/zero points/minimums, and
a transform. It is **data, not an enum**, so a plugin can invent one. The core can size, slice and
place any encoding from the struct alone, and decodes the `plain` and `affine` schemes itself:
```
q = code codes TILED, block [1x1], or [1xV] with a grid
q = grid[code][k] grid TABLE [entries x V]
q = table[code] table TABLE [1 x entries]
q = -q if the sign bit is set signs TILED u1
q = q - zero zero TILED
v = q * scale * scale.1 * ... every scale level
v = v - min * min.1 * ... a minimum in the value's domain
w = v . T^-1 the transform, per row over columns
```
So NVFP4 is fp4 codes, e4m3 scale per 16, f32 `scale.1` per tensor; Q4_K is u4 codes, u6 scale and
u6 min per 32 each times an f16 per 256; IQ2_XXS is u8 codes per 8 into a 256×8 grid, a sign bit per
value and two scale levels.
- Planes are **TILED** (cover the weight, element `(i,j)` covering a `[br,bc]` block; `0` extent means
the whole extent) or **TABLE** (a codebook, grid, rotation parameter; every slice carries it).
- Sub-byte elements are a bit stream along the row, lowest bits first; every row starts on a byte.
- Transforms: `""`, `fwhtN` (Hadamard before quantisation; the `1/N` lives in the stored scale),
`perm` (GPTQ act-order, a TABLE `t.perm`), `givens` (ParoQuant rotations: `t.pairs`, `t.angle`,
optional `t.scale`, per-part `t.part`, composed `t.dense`).
A kernel only *rearranges* planes (`relayout`); choosing numbers is the quantiser's job. That split
is why a container never has to be reconverted when a kernel library changes its layout.
## Build, install, search path
```cmake
cmake_minimum_required(VERSION 3.21)
project(mykernels LANGUAGES CXX HIP) # CXX alone for a host-only plugin
find_package(radiance REQUIRED)
rad_plugin_defaults() # C++20, optimising build unless asked otherwise
rad_add_plugin(mykernels kernels SOURCES rows.cpp HIP_SOURCES kernels.hip)
```
The kind is `kernels`, `architectures` or `quantizers` — the directory the engine loads it from.
Only `HIP_SOURCES` compile as HIP: for `RAD_GPU_TARGETS` or a `GPU_TARGETS` list, with `-DNDEBUG`
outside Debug, uncompressed, against the HIP runtime the installed radiance found. `find_package`
also sets `RADIANCE_ABI_VERSION` (to fail at configure time rather than at `dlopen`),
`RADIANCE_HOME`, `RADIANCE_KERNELS_DIR`, `RADIANCE_ARCHITECTURES_DIR`,
`RADIANCE_QUANTIZERS_DIR`. `cmake --install` puts the plugin under the home of the radiance you built
against; `RAD_PLUGIN_INSTALL_HOME` overrides that.
**`$RADIANCE_HOME` is a search path**: homes separated by `:`, the first winning; each may hold
`kernels/`, `architectures/`, `quantizers/`, and an earlier home's same-name file shadows a later
one. Your plugins go ahead of an installation without copying it:
```sh
RADIANCE_HOME=$HOME/radiance-plugins:/opt/radiance/share/radiance radiance --model m.rad
# in the container:
-v /my/plugins:/plugins -e RADIANCE_HOME=/plugins:/opt/radiance/share/radiance
```
The **kernel hierarchy** is `--kernels` (or `$RADIANCE_KERNELS`): `--kernels mykernels,libr4d` puts
yours before libr4d. An entry matches a plugin's name or its file (`libr4d`, `r4d`, `libr4d.so`, a
path). An entry matching nothing is warned about by name. With no list, every library found loads,
reference last.
## A library for a new card
The engine's own device code is compiled for every AMD family's generic target
(`gfx9-generic`, `gfx9-4-generic`, `gfx10-1-generic`, `gfx10-3-generic`, `gfx11-generic`,
`gfx12-generic`) beside the build's targets, so one engine binary starts anywhere. What it computes
there depends on the installed kernel library.
- Configure with `-DRAD_GPU_TARGETS=gfx942` (or `gfx11-generic`). At load the engine asks the runtime
which targets the card accepts, in order (chip, then family: `gfx1036`, then `gfx10-3-generic`),
with the card's modes (`gfx942:sramecc+:xnack-`), and takes the best fit in the fat binary.
- **A library with nothing for this card is left out, not failed**: its device rows leave selection,
its host rows stay, and the log says what it carries versus what the card runs. One installation
can carry a library per card. `rad_plugin_open` may decline explicitly with `RAD_E_UNSUPPORTED`.
- The engine uses discrete cards if any exist (all of the first one's architecture), integrated only
when there is nothing else; `ROCR_VISIBLE_DEVICES` chooses explicitly. On integrated graphics,
device memory is host memory and the budget leaves an eighth of host RAM to the host.
- **fp8 bytes are OCP E4M3** (`e4m3fn`, bias 7, max 448) wherever the engine reads them: container
weights, an fp8 KV cache, activations. A card whose fp8 is `e4m3fnuz` (gfx942) converts in its
`relayout` hook (the codes keep their bits except `0x80` and decode to half the value, so the scale
doubles) and keeps activations/KV in OCP bytes, or converts at its own boundaries.
- **Tensor parallelism is ops you supply.** `all_reduce`, `all_gather` and the fused collectives are
rows like any other; the engine inserts none and assumes no link.
## Checking your work
```sh
rad-kbench --kernels mylib,libr4d,libref --report mylib.md --bench
```
- It replays a recorded fixture of `libref`'s answers over real model geometries
(`$RADIANCE_HOME/kernels.rkb`, plus `kernels_moe.rkb`), running every library with a row for a
case. Correctness uses sampled values **and** the full-tensor norm **and** seeded projections;
completeness is the op-by-library matrix; speed is timed on the same bytes.
- Red zones: every device allocation carries a poisoned 256-byte margin, verified after the launch.
- Fusion: a row with `replaces` is run against its named chain, out of your library, compared with
`memcmp`.
- A skip is named, not failed — usually a missing `opd_shape`. `--strict` also fails a stale fixture.
- `rad-avx-check --ref libref.so --avx mylib.so` checks a **host** library without a card, op by op,
at every ISA level and with parallel regions forced onto several threads (for a library exporting
`avx_isa_force`/`avx_par_force`).
- `rad-schemas mylib.so libref.so` prints every schema disagreement at once (it exits 1 on any), and
works on a plugin the engine cannot load.
- `--debug-selection` prints which row took each band and which constraint refused each other
candidate; `/graph` on a running server shows the declared graph and which declarations the run
phase issued.
- `rad-info --plugins` lists every plugin on the search path as the engine loads it: kind, version,
declarations, reference status, whether its device code runs on this card, and its file.
## A worked example to copy
`plugin/spechub/` is the smallest real kernel plugin in the tree: two ops (`mtp_couple_propose`,
`mtp_couple_verify`), each with a device row and a host reference row. Read it in this order:
- `exports.cpp` — the whole export block: info, ABI, schema count/at, kernel count/at, plus
`rad_kernel_reference` classifying device rows 0 and host rows 1 so the reference gate does not
refuse the device rows.
- `spechub.cpp` — the rows, their constraints and their argument readers.
- `spechub.hip` / `ref_spechub.cpp` — the device launch and the host oracle.
- `CMakeLists.txt` — the `rad_add_plugin` call, the optional `enable_language(HIP)`, and
`-ffp-contract=off` because an FMA in the ordered FP64 mass sums would move a token off the host
reference.
## Related
- [[Radiance Architecture Plugins]] — the third plugin kind, one level up.
- [[Radiance Conversion]] — how a quantiser is driven by a recipe.
- [[Radiance Tools]] — `rad-kbench`, `rad-tune`, `rad-schemas`, `rad-avx-check` in full.
- [[Radiance Serve Options]] — `--kernels`, `--radiance-home`, the tuning aids.
@@ -1,240 +0,0 @@
---
type: "reference"
title: "Radiance Serve Options — every flag and the law behind it"
description: "Every radiance serve flag grouped by task, with its default and the law that decides when to move it."
tags:
- llm
- radiance
- reference
- serving
timestamp: 2026-10-30
---
# Radiance serve options
`radiance --help` prints this table. Every numeric option takes a whole number (not `12k`, not
`auto`, unless the option names those); a malformed value exits 2. Upstream `main` at 2026-10-30;
the fleet is pinned older — see the pin note on [[Radiance Engine]].
The one thing to read before any table: **the engine derives the VRAM split; no flag sets either
pool.** The startup receipt prints every term before anything is allocated.
## Model, plugins, container
| flag | default | meaning |
|---|---|---|
| `--model PATH` | required | a `.rad` container, a checkpoint served as it stands, or a Hugging Face repo id (`owner/name`) fetched before startup |
| `--download-dir DIR` | `$XDG_CACHE_HOME/radiance/hf`, else `~/.cache/radiance/hf` | where a repo id is fetched |
| `--radiance-home DIRS` | compiled prefix | plugin homes, `:`-separated, first wins |
| `--kernels LIST` | every library found | kernel libraries, `,`/`:` separated, first wins; a library not named is not loaded |
| `--served-model-name NAME` | container's name | the id `/v1/models` lists and responses carry; a request may name any model |
| `--override-chat-template PATH` | container's | a Jinja template served instead; an unreadable or empty file stops startup |
| `--generation-config PATH` | none | sampler defaults from a `generation_config.json` (else `<model>.generation.json`, then the container's `generation.*` metadata) |
| `--control-vector off\|on\|disabled` | `disabled` | load the GGUF control vector found for the model and start requests off/on; `disabled` declares no projection and refuses a request that asks |
| `--api-key KEY` | none | require `Authorization: Bearer KEY` or `X-Api-Key: KEY`, except `/health`, `/v1/health`, `/ping`, the dashboard |
| `--mm-max-patches N` | `auto` (16384 with a vision tower) | patches one encoder pass carries: the largest image, 16×16 px a patch (14×14 on GLM-5.3). `0` serves text only and keeps the tower off the card |
## Batch shape, context, scheduler
| flag | default | meaning |
|---|---|---|
| `--max-num-batched-tokens N` | 2048 | the largest token count one step may carry; on MoE the knob that sets prefill chunk size, and the activation arena's size |
| `--max-num-seqs N` | 256 | the most sequences one step may carry; the cache's **demand**, never its funding |
| `--max-model-len N` | 0 = model's training context | context bound per request. Longer is a 400, never a truncation. Reserves nothing per request; it is the worst case the pool is sized against |
| `--busy-prefill-chunk auto\|off\|N` | `auto` | a prefill chunk's share of a step that has other work: `auto` leaves decode rows and waiting requests room, `off` takes the whole step, `N` caps it |
| `--prefill-interleave N` | 0 (off) | decode-only steps after each chunk taken beside decoders, at most 64; buys decoder cadence with the prompt's TTFT |
| `--prefill-span N` | 1 | checkpoint intervals a prefill **alone in its step** may carry, from its second interval; needs `--max-num-batched-tokens` of N intervals. Every other step is planned at the shared budget; checkpoints inside a span are not written |
| `--preempt-wait-ms N` | 5000 | how long a preempted prefill waits for the whole-recompute gate before reclaiming blocks and preempting lower-priority work; `0` falls back next step |
| `--rope-scaling SPEC` | the checkpoint's | `yarn:FACTOR[:ORIG]` writes static YaRN over the container's own rotary keys with no reconvert; `none` takes a checkpoint's YaRN off |
| `--max-queued-requests N` | 8 × `--max-num-seqs` | admitted, unfinished requests; past it the answer is 429 |
| `--retry-after S` | 1 | the `Retry-After` a 429 carries |
## Memory and placement
| flag | default | meaning |
|---|---|---|
| `--placement MODE` | `auto` | `all_vram`, `layer_offload`, `expert_tiered`, `auto` |
| `--gpu-headroom-mib N` | 96 | VRAM left unclaimed on every card; too little makes the first launch of a never-run kernel fail inside HIP |
| `--host-pool-mib N` | 0 | pinned host tier for weights, **per rank**; also the KV lever under expert tiering (below) |
| `--host-headroom-mib N` | 4096 | host RAM left free when startup pins memory; a snapshot store's floor takes the shortfall back only down to 1 GiB |
| `--kv-cache-dtype T` | `bf16` | `bf16`, `fp8`, and on Qwen3.8-27B/-Flash-Next `fp8_v4` (V int4) or `k6_v4` (K int6 + V int4). A real quantisation; GLM-5.3 is bf16-only |
| `--state-cache-dtype T` | `f32` | `f32` or `f16`: the recurrent delta-net state cache; f16 halves state pool, checkpoints and traffic and is lossy — `qwen35` plugins only |
| `--attn-prefill-fp8 MODE` | `off` | `off`, `qk`, `pv`, `both`: prefill attention products in E4M3 over an `fp8`/`fp8_v4`/`k6_v4` cache; lossy, Qwen3.8-27B only where kernels exist |
| `--kv-resident N` | 0 (off) | stream the sparse attention group's KV from pinned host RAM, keeping an N-token ring on the card. Raised to 32768 and to one decode step's selection; ignored with a warning where unsupported |
| `--embedding-placement ram\|vram` | `ram` | token embedding pinned in host memory and read in place, or on the card. A tied head or a host that cannot pin keeps it on the card; an explicit `ram` refuses instead |
| `--vision-tower-placement vram\|ram` | `vram` | encoder towers on the card, or pinned in host memory and read once per image, giving their VRAM to the KV cache |
| `--ngram-placement auto\|vram\|ram\|disk` | `auto` | where an n-gram embedding table lives: all of it on every card, a pinned host copy the card reads, or rows from the container through a row cache. `auto` takes the first that fits. `ram=DIR` makes the pinned copy a tmpfs file shared by two engines |
| `--weights-disk-tier` | off | read weights that fit neither VRAM nor the host pool from the container with O_DIRECT; every step is paced by the drive |
| `--row-prefetch on\|off` | on | ask for a host gather's pages before its kernel; output-neutral |
| `--mapped-rewarm on\|off` | on | at startup, re-ask the page cache for a mapped table's pages an earlier run left behind |
| `--deterministic` | off | pin placement to the loader's plan so the resident set cannot move under the heat engine; it does **not** make output bit-identical across batch positions |
## KV cache, host tier, checkpoints
| flag | default | meaning |
|---|---|---|
| `--prefix-cache-mib N` | 0 (off) | copy finished conversations' KV to host memory so a return restores instead of re-prefilling; LRU when full; off while a KV group streams |
| `--prefix-cache-ttl S` | 3600 | seconds an unused cached conversation is kept; `0` keeps it until memory is wanted |
| `--no-prefix-cache` | cache on | disable prefix caching |
| `--checkpoint-interval N` | 2048 | linear-attention state checkpoint interval, in tokens; also clamps a hybrid model's prefill chunk |
| `--checkpoint-slots N` | `auto` | snapshots held **in total**, in pinned host memory; `auto` = per-policy need for each sequence within an eighth of host RAM; `0` keeps none |
| `--checkpoint-policy NAME` | `geometric-backoff` | also `tip-only`, `keep-all`; retention only — writes happen at every interval unless a span skips them |
| `--prefix-resume exact\|nearest` | `exact` | `exact` resumes only where a cold prefill starts a chunk, so a hit answers bit-identically; `nearest` takes the deepest snapshot, up to a chunk less re-prefilled |
## Speculation
| flag | default | meaning |
|---|---|---|
| `--num-speculative-tokens N` | `auto` (the drafter's own point; 0 with no head) | speculative window; 0 disables. The ceiling under `adaptive` |
| `--spec-ngram K` | 0 (off) | prompt lookup alone, up to K drafts a step from the sequence's own n-grams; leaves a container's draft head out; sets the window to K |
| `--spec-ngram-min N` / `--spec-ngram-max N` | 2 / 4 (max 15) | pattern lengths prompt lookup tries, longest first |
| `--draft-window fixed\|adaptive` | `fixed` | `adaptive` follows each sequence's acceptance and the measured step cost |
| `--draft-short-from N` | `auto` | a block drafter that declares a shorter block runs it from N decoding sequences; `0` never; `auto` = never for a chain, from 2 for a tree |
| `--draft-coupling off\|on` | off | sample each proposal from the drafter's own shortlist and couple the target's draw to it; same output law, different tokens for a fixed seed, pays at high temperature |
| `--draft-model PATH` | — | **not implemented** at serve: merge the drafter at convert time (`rad-convert --draft-model`) |
## Tensor parallelism and collectives
| flag | default | meaning |
|---|---|---|
| `--tp N` | 1 | tensor-parallel ranks; declare divides dimensions by world size and inserts no collective |
| `--tp-wire exact\|wht6` | `exact` | `wht6` is lossy: rotated 6-bit at 2 ranks, tiered int8 over a two-shot at 3–4, unavailable at 8 |
| `--tp-wire-min-kb N` | 128 | smallest all-reduce (KiB) the lossy wire compresses; keeps single-sequence decode exact |
| `--tp-attn-split heads\|rows` | `heads` | a prefill step's attention split: `rows` gives each rank all heads of its rows plus an all-gather; faster where attention dominates, not bit-identical, plugin-gated |
| `--weight-prefetch on\|off` | on | read a GEMM's weight beside the collective before it; `off` when another server shares the cards |
| `--p2p auto\|on\|off` | `auto` | `auto` asks the driver; `on` refuses to start without peer access; `off` exchanges through pinned host memory |
## LoRA adapters
| flag | default | meaning |
|---|---|---|
| `--lora NAME=PATH` | none | serve an adapter: a PEFT directory as model NAME (text), or a `.safetensors` a request names in `loras` (image/video). Repeatable |
| `--lora-dir DIR` | none | generators: any `NAME.safetensors` under DIR may be requested |
| `--max-loras N` | count of `--lora`, 2 with `--lora-dir` alone | adapter slots resident at once |
| `--max-lora-rank R` | 16 text / 64 generator | slot rank; a larger text adapter is refused, a larger generator combination truncated |
| `--lora-target-modules LIST` | the adapters' own | HF module leaf names adapters may target; needed with `--max-loras` and no `--lora` |
## Request defaults and bounds
A request that names a field wins. These set defaults for requests that do not.
| flag | default | meaning |
|---|---|---|
| `--temp F`, `--top-k N`, `--top-p F`, `--min-p F` | from the model | sampler defaults; `top_k` 0 is the 1024 most likely candidates |
| `--typical-p F` | 1 | typical sampling |
| `--presence-penalty F`, `--frequency-penalty F` | 0 | range [-2, 2] |
| `--repetition-penalty F`, `--repeat-last-n N` | 1, 64 | `-1` looks back over the whole context |
| `--dry-multiplier F`, `--dry-base F`, `--dry-allowed-length N`, `--dry-penalty-last-n N` | 0, 1.75, 2, -1 | DRY; multiplier 0 is off |
| `--dry-sequence-breakers JSON` | `["\n", ":", "\"", "*"]` | at most 16 strings |
| `--xtc-probability F`, `--xtc-threshold F` | 0, 0.1 | range [0, 1] |
| `--default-max-tokens N` | `auto` | `max_tokens` for a request that sends none; `auto` is what the context leaves |
| `--max-tokens-cap N` | 0 (none) | a larger request is **cut to it**, not refused; reply ends `finish_reason: "length"` |
| `--max-n N` | max(8, `--max-num-seqs`) | the largest fan-out `n` |
| `--max-stop-strings N`, `--max-stop-bytes N` | 64, 4096 | stop-string limits |
| `--chat-template-kwargs JSON` | none | variables for the template on every chat request; a JSON object or `@FILE`; repeatable |
| `--reasoning-effort S` | template's own | default `reasoning_effort`; passed to the template verbatim, so the legal set is the template's |
| `--reasoning-effort-map MAP` | none | `FROM=TO,...`, checked against the template at startup; `none`/`minimal` turn thinking off |
| `--thinking-token-budget N` | none | default budget: at N reasoning tokens the block is closed and the answer follows |
| `--reasoning-format auto\|none` | `auto` | `auto` returns reasoning as `reasoning_content`; `none` leaves it inline |
## Server and HTTP
| flag | default | meaning |
|---|---|---|
| `--host ADDR` | 0.0.0.0 | listen address |
| `--port N` | 8000 | listen port |
| `--http-threads N` | one per hardware thread, at least 4 | workers kept while idle |
| `--read-timeout S` | 30 | a client may go silent this long while sending |
| `--write-timeout S` | 600 | one write to a client may block this long |
| `--keep-alive-timeout S` | 5 | idle connection hold |
| `--max-body-mib N` | 512 | largest body; images and video ride base64 inside it |
| `--no-cors` | CORS on | send no CORS headers |
## Media preprocessing
Each replaces what the container's preprocessor config states, and does nothing for a model that
takes no media.
| flag | default | meaning |
|---|---|---|
| `--image-min-pixels N`, `--image-max-pixels N` | container's | the band an image resizes into; one encoder pass still bounds the top |
| `--video-min-pixels N`, `--video-max-pixels N` | container's | same, for a video's sampled frames together |
| `--video-fps F` | container's | frames sampled per second |
| `--video-min-frames N`, `--video-max-frames N` | container's | frame-count bounds |
| `--video-max-frame-tokens N` | container's | the most prompt tokens one frame becomes; `0` no cap; a token-budget processor takes 70/140/280/560/1120 |
| `--image-tokens N` | container's | a token-budget processor's image budget (Gemma 4's fixed set; GLM-5.3's ceiling ≤ `--mm-max-patches`/4) |
| `--max-source-pixels N` | 67108864 | largest input the decoder accepts before resize |
## Quality measurement (instead of serving)
| flag | meaning |
|---|---|
| `--kld-record DIR` | run `--kld-corpus` through prefill and write this model's per-position log-probabilities to DIR: the reference |
| `--kld-ref DIR` | score this model against the reference: mean/median/percentile KL and top-1 agreement |
| `--kld-corpus FILE` | JSONL, one `{"prompt", "score_from"?, "source"?}` a line; `--kld-record` only |
| `--kld-out FILE` | the report as JSON; `FILE.rows` holds every position's numbers |
| `--kld-lora NAME`, `--kld-lora-sources PREFIX` | score through a served adapter, all documents or a `source` prefix |
The mode turns speculation and the prefix cache off and refuses `--num-speculative-tokens > 0`.
See [[Radiance Conversion]] for the corpus and the scoring discipline.
## Diagnostics
| flag | meaning |
|---|---|
| `--debug-graph` | print the declared graph, every op's resolved kernel and why, then exit |
| `--debug-selection` | the selection table only |
| `--debug-placement` | the placement plan only |
| `--debug-tape-dump` | the first recorded pass of each kind (`TAPEDUMP` on stderr, every rank) |
| `--debug-draft-oracle FILE` | verify-path test: prompt lookup proposes from FILE with 30 % corrupted drafts; the text must not move |
| `--debug-accept-reference-kernels` | serve even with `libref` rows — not a production configuration |
| `--profile-ops` | per-op device timing |
| `--expert-stats FILE` | routing counts per layer and step kind, JSON every 30 s and at shutdown |
| `--expert-profile FILE` | rank the expert plane by a stats file's `decode` counts instead of the container profile |
| `--expert-pin-top F` | pin the hottest fraction F of routed experts so heat never demotes them |
| `--draft-expert-placement host\|auto` | pin a draft head's experts in the host pool before any strategy runs |
| `--live` | the terminal live view; redirected, one `key=value` line |
| `-v`, `-vv` | verbose, very verbose |
## The laws behind the knobs
**The split is derived, and the receipt is the referee.** Non-expert weights come off the top of
the card; the remainder (`elastic`) is shared by the expert slab and the KV cache. `--max-model-len`
× `--max-num-seqs` is the cache's **demand**; the funding is `elastic − expert_floor`. Under a
lending cache (`auto`/`expert_tiered`, no `--deterministic`, VMM present) the cache takes what it
can address and lends unused blocks back to experts. **Every MiB added to `--host-pool-mib` lowers
the expert floor one-for-one and the cache takes it** — until the cache already holds its whole
ceiling, where the extra host room shows up as unclaimed. Read the boot line
`kv: the pool backs N % of the worst case …`, never arithmetic.
**The activation arena comes off the budget before the split**, so both sides pay for
`--max-num-batched-tokens`. While steps are small most of it is lent back to the expert slab; a
larger step reclaims it synchronously. That is why a bigger chunk can cost decode residency. On a
hybrid model the chunk is clamped to `--checkpoint-interval` before the token budget, and startup
prints which bound applies.
**The KV dtype law.** `bf16` and `fp8` are engine-wide; `fp8_v4`/`k6_v4` are narrower layouts whose
kernels exist only for particular attention geometries, and the server refuses them elsewhere by
plugin name. A drafter's cache stays fp8. Recurrent state and the QSA indexer keep their own widths.
**The slot law.** A slot costs recurrent state, block tables at full context, snapshot-store RAM
and all-reduce rows — never speed by itself. Raising `--max-num-seqs` raises demand; whether it is
funded is the resolver's answer.
**The spec law.** Measure speculation by step time, not tok/s. Prompt lookup pays where the reply
copies its context and costs a verify row elsewhere. `adaptive` moves the depth only after eight
steps and a 2 % gain, because a new window is a new pass shape.
**Batch shape is part of the configuration.** A different chunk, span or spec depth changes the
summation order, so greedy text can differ at a tie. Reproducibility holds per configuration, never
across one.
**Unknown request fields are ignored; known-but-unimplemented fields are guarded** (accepted at
their off value, refused otherwise) — the list is on [[Radiance HTTP API]].
## Related
- [[Radiance HTTP API]] — what the flags expose on the wire.
- [[Radiance Operations]] — the container contract and readiness.
- [[Radiance Plugins]] — how a flag reaches a plugin (`RadBuildCtx`).
- [[frank-archon]] — the measured deployment of this resolver on two R9700s.
-322
View File
@@ -1,322 +0,0 @@
---
type: "reference"
title: "Radiance Tools — inspect, check, tune, tokenise"
description: "The radiance inspection, measurement and tokeniser tools, with the flags and verdicts each one reports."
tags:
- llm
- radiance
- tools
- reference
timestamp: 2026-10-30
---
# Radiance tools
Six binaries and `rad-export` are installed beside `radiance`; `rad-avx-check`, `r4d_selftest` and
`rad-chatparse` live in the build tree. Every tool that loads plugins honours `RADIANCE_HOME`,
`RADIANCE_KERNELS` and `--kernels` as the engine does. A numeric option takes a whole number: `12k`,
`auto` (where the option does not name it) or an empty value exits 2.
`rad-convert`, `rad-kbench` and `rad-tune` run the engine's startup with a different last step: the
same plugins, in the same hierarchy, through the same declare phase, so they resolve kernels exactly
as a serve would.
## `rad-info` — what a container holds
```
rad-info [options] <model.rad>
rad-info --plugins [--home DIRS]
-v, --verbose list every tensor, not just the per-unit summary
--meta metadata table, then stop
--vocab vocab summary and normaliser chain, then stop
--profile N top N expert-profile entries (default 20, 0 = all)
--recipe the recipe the container was converted with, then stop
--plugins every plugin the search path offers, as the engine loads it
--home DIRS search path for --plugins (default $RADIANCE_HOME)
```
It opens the container read-only and loads no plugins (except for `--plugins`): a container names no
kernel library. Output is aligned; byte counts are binary units and each count says what it counts.
```sh
rad-info m.rad # format, architecture, model, source quant, recipe, sections
rad-info --recipe m.rad # the recipe, expanded, one rule a line
rad-info -v m.rad | head -40 # every tensor, encoding, shape and what made it
rad-info --plugins # kind, version, declarations, reference, device fit, file
```
The section table (strings, metadata, directory, planes, encodings, vocab, profile, data) is the
container's layout; the metadata table, the weight directory grouped by layer and movement unit, the
vocab, and the expert profile follow.
## `rad-kbench` — correctness, completeness, speed
```
rad-kbench [options] replay the recorded fixture (no model)
rad-kbench -m <model.rad> [options] check against a real container
rad-kbench -m <model.rad> --record FILE record a new fixture
rad-kbench -m <model.rad> --skeleton FILE write a metadata-only copy to record from
```
**Replay** needs no model, weights or card. Geometries, seeds and `libref`'s reduced answers are
checked in as `data/kernels.rkb` (dense model, block drafter, gqa 6) and `data/kernels_moe.rkb`
(routed MoE, MTP head, gqa 8). `--fixture` is repeatable; `ctest` runs three replays — the other
libraries against each recording, and `libref` against both at tolerance zero (its regression test,
since a live comparison moves both sides together).
Each library is asked for its row as the engine would ask it with that library alone: same
constraints, derived parameters and weight-encoding filter, so a row the engine would pass over does
not run here either (`--all-rows` runs every row whose constraints hold).
**Against a container** (`-m`), weight operands are read from it: each weight's canonical planes,
which the kernel gets through its own `layout`/`relayout` hooks and the oracle gets decoded, and
every expert of a weight table. This is the only test of the artifact and of the layout hooks.
**Recording** takes the reference's answer once at every point of every declared op, whichever
library resolved it. Weights are drawn from a seed, not read from the container, so any machine can
reproduce the reference and no card is needed. A skeleton (tables intact, data a hole) records the
same fixture in a few megabytes.
```sh
rad-kbench -m model.rad --skeleton model.skel.rad
rad-kbench -m model.skel.rad --max-cases 1 --record data/kernels.rkb
rad-kbench -m moe.rad --max-cases 1 --record data/kernels_moe.rkb
```
### Verdicts
- An output is compared on sampled positions, the full-tensor norm, and 32 seeded random-sign
projections (which estimate rel_l2 to about 25 %). The largest of the three is the error.
- Tolerances are per op and activation dtype (`dtype`, else `q_dtype`, else `kv_dtype`). Copies,
gathers, scatters and cache stores are exact. Every op in the fixtures has a row of its own.
- Ids are compared exactly; where the contract leaves a tie open (`sample_argmax`, `router_topk`,
`sample_topk`), an id indexing a value equal to the reference id's is accepted.
- A case whose reference and output are both identically zero checked nothing: **vacuous**, not
passed.
- A failure, a red-zone overrun, a fusion that differs from its chain, or a host kernel that wrote
into an input exits 1. `--strict` also fails a stale fixture.
- `--coverage DIR` keeps one file per fixture/library of the rows a replay reaches
(`data/kbench_rows/`); a listed row not reached fails, unless a memory skip kept its cases out.
- Every pass ends with a line saying where its time went (draw, hashing, layout, upload, launch,
read-back, comparison, cache, timing).
### Timing
`--bench` times every case that passes, on the bytes it was checked on.
- Device cases are timed **after** the checks, operands still on the device, after one sustained
warm-up. A time is device timestamps around a batch of back-to-back launches divided by the launch
count; the batch is queued behind a gate the host opens once submission is done, so neither the
host rate nor a sync is in the interval. Two untimed launches precede every batch; batches repeat
until the median's 95 % interval is within 1 % or `--bench-reps` is reached.
- **Cold by rotation**: weights and inputs of 1/32 of the footprint or more are read from copies
spanning the footprint (`--bench-flush`, default 256 MiB, at least twice an R9700's caches), so
each launch reads them from memory as streamed weights do. Smaller inputs stay warm, as the
previous op leaves them.
- In-place operands are restored from a pristine copy before every launch; the restores are timed
alone and subtracted.
- Timing draws expert offsets/routing skewed (Zipf 0.5) and block tables as permutations; the
checked launch keeps the recorded draw.
- The header prints the calibration: launch floor and streaming-read bandwidth warm and cold. If a
smaller cold probe runs >1.5× the largest's rate the rotation was cache-served and device times
are withheld. Throttler bits that came on during a batch repeat it, up to three times.
- Host cases are timed in their own column, after an untimed launch, weights rotated over at least
256 MiB (twice LLC), for at most two seconds a case.
### Flags
| flag | meaning |
|---|---|
| `--fixture FILE` | replay recording (default `$RADIANCE_HOME/kernels.rkb`); repeatable |
| `--record FILE` | with `-m`: write a fixture |
| `--skeleton FILE` | with `-m`: write a sparse container copy |
| `--rerecord FILE` | replay: write the fixtures' cases again with the reference's answers |
| `--record-rows FILE` | replay: record a row-driven fixture from the cases |
| `-m, --model FILE` | the `.rad` to read weight operands from |
| `--home DIR`, `--kernels A:B` | plugin home; kernel libraries (default every one found) |
| `--op NAME` | only this op (repeatable prefix match) |
| `--ref NAME` | the oracle plugin's name (default: the library declaring itself a reference) |
| `--no-ref` | replay without the reference among the tested libraries |
| `--strict` | replay: a stale fixture fails |
| `--all-rows` | run every row whose constraints hold |
| `--coverage DIR`, `--coverage-write` | the row-coverage ratchet |
| `--no-cache`, `--cache-dir DIR` | run the reference instead of reusing cached answers; cache location |
| `--seed N` | base seed; every case derives its own |
| `--tol X` | override every tolerance (bisecting only) |
| `--max-ctx N` | deepest context considered (declare and the attention sweep) |
| `--max-tok N` | `max_tok` declare runs at (default 512, small on purpose) |
| `--mm-max-patches N` | encoder pass declare runs at (default 2048 when a tower is present; 0 leaves it out) |
| `--kv-cache-dtype T` | `bf16` or `fp8`, as on the engine |
| `--num-speculative-tokens N` | draft depth to declare at (default: the container's) |
| `--max-cases N` | runs per distinct case (default 4; 0 = every one) |
| `--max-bytes MIB` | skip a case needing more than this, by name (default: half available memory, 512–8192) |
| `--no-fusion` | skip the fusion oracle |
| `--bench`, `--bench-iters N`, `--bench-reps N`, `--bench-warm-ms N`, `--bench-flush MIB` | timing |
| `--timing-only` | time every device kernel, check nothing (refuses `--record`) |
| `--report FILE`, `--analysis FILE` | markdown report; splice in `tools/kbench-analysis.sh` output |
| `-v` | every case, not only failures |
Notes: `--max-bytes` matters on a loaded machine — one case holds each operand as drawn, as the
kernel's copy, and on the device; without the cap a small machine is OOM-killed with no indication of
the op. `--bench` times cold on purpose: an R9700's 52 MB bf16 weight read warm comes from cache at
1.45 TB/s against 565 GB/s from memory, so warm times rank shapes but do not size a roofline. A
kernel that cannot lay out an encoding is a **skip**, not a failure (`RAD_E_DTYPE`), and the report
names the kernel, operand and encoding.
## `rad-tune` — measure the instantiation space
```
rad-tune -m <model.rad> [options]
-m, --model FILE the .rad whose declared graph supplies the shapes
--home DIR $RADIANCE_HOME; cache at <home>/tune/<machine>.tune
--kernels A:B kernel libraries to load, first wins
--op NAME only ops whose name starts with NAME
--machine KEY override the machine key (writing a cache for another machine)
--reps N timing reps, median reported (default 5)
--iters N launches per rep (default 32)
--max-tok N max_tok declare runs at (default 8192)
--tp N tensor-parallel width to declare at (default 1)
--num-speculative-tokens N|auto draft depth to declare at
--dry-run list what would be measured
-v, --verbose every candidate, not just the winner
```
Kernels declare tunable axes, not named variants; `rad-tune` walks the cross product at this model's
shapes and writes the winner per (op, geometry) to the cache the selector reads at startup.
- **Declare at the shapes you serve**: `--tp N` divides every sharded N; a declare at world size 1
measures a program no `--tp 2` serve runs. `--num-speculative-tokens` puts the drafter's kernels in
the measured graph.
- At `--tp > 1` every rank declares at once, each on its own thread and device: a collective's
`init` is a rendezvous, so declaring rank 0 alone hangs.
- A collective is measured as the exchange: per-rank operands (own seeds, so identical halves cannot
pass), own scratch and stream, own instance; every rank fires at once and the clock stops when the
last stream drains. Non-collective rows run on rank 0 alone.
- `RADIANCE_TUNE` pins a kernel's axes to compare a tuned cache against the defaults — a measurement
aid, not a deployment setting.
## `rad-schemas` — the op vocabulary diff
```
rad-schemas libref.so print one plugin's vocabulary
rad-schemas libr4d.so libref.so diff them, in hierarchy order
```
The first plugin in hierarchy order to declare an op fixes its schema; a later plugin declaring the
same op differently is refused at load, both named (arguments are positional, so disagreeing schemas
would make one call mean different things). `rad-schemas` prints every disagreement at once, so a new
plugin can be conformed before it is loaded beside another. It exits 1 on any disagreement. It links
no core — it `dlopen`s plugins and reads static tables, so it works on a plugin the engine cannot
load.
## `rad-avx-check` — host libraries without a card
```
rad-avx-check --ref libref.so --avx LIB.so [options]
--ref PATH the oracle plugin (default libref.so)
--avx PATH the plugin under test (default libavx.so)
--op NAME only this op
--level N only this ISA level (0 scalar, 1 avx, 2 avx2, 3 avx512)
--threads N rerun every case with each parallel region on N threads (default 4; 0 skips)
--tol X override every tolerance
-v every case
```
Checks a host library op by op against `libref`, on the CPU, with neither engine nor card: two
`dlopen`s, a geometry table (`libavx/avx_cases.cpp`), libref's operand descriptions and hand-built
cases. For every case: schemas as the loader compares them, outputs within `rad-kbench`'s tolerances,
guard bytes, inputs unchanged, and an oracle output that is not just the draw. `libavx` is also
checked at every ISA level the CPU executes and with parallel regions forced onto several threads.
A new host library is checked with the same command; it is treated as a host library (not `libavx`),
so libref ops it has no row for are listed rather than failed, and the ISA/thread sweeps run only if
it exports `avx_isa_force`/`avx_par_force`. A row returning `RAD_E_UNSUPPORTED` for a geometry libref
serves fails: the selector would resolve to it and the step would fail at issue.
## `r4d_selftest` — libr4d's own check and timer
```
r4d_selftest <libr4d.so> [--case <row>]
r4d_selftest <libr4d.so> --perf N K M [iters] (and the other --perf* modes in its usage)
```
With no mode it runs the catalogue and static checks, then each row's smoke and numeric cases in a
child process of its own, with fusion claims compared byte for byte (`ctest` labels `slow`, `gpu`).
`--case` runs one row in-process as the reproducer. `--perf` covers the fp8 decode GEMMs and head;
`--perfmxd` the dense OCP-MXFP4 rows with the weight bandwidth each reaches; `--perfmx` the MXFP4
expert GEMM; `--cold MIB` times each launch alone behind a cache flush. `--perfar8` prints the
eight-rank all-reduce shard weights and times `lockstep`/`ring`/`split` forms at every piece count;
`R4D_SELFTEST_ONE_CARD=1` runs them on one card for their bits only.
## `rad-tokcheck` — tokeniser against its references
```
rad-tokcheck [options] <tokenizer.json> <corpus-file>...
--hf-dir DIR compare against HuggingFace ids in DIR/<corpus basename>.ids
(little-endian int32, encode(text, add_special_tokens=False))
--no-ref skip the reference encoder (slow)
--user encode with parse_special off
--turns N turns in the multi-turn measurement (default 40; 0 skips)
--repeat N timing repeats, best taken (default 3)
--context N ids shown either side of a difference (default 6)
```
Per corpus file: ids match `tools/tokref`'s reference encoder (llama.cpp's splitters and
`std::regex`); ids match HuggingFace `tokenizers` read from files made elsewhere; and speed — MB/s
for both encoders, plus the cost of re-tokenising a growing conversation with and without the
segment cache. Any difference prints the ids and surrounding text and exits nonzero, so it gates a
change.
## `rad-chatparse` — where a tool call goes wrong
```
rad-chatparse --request REQ.json --rad MODEL.rad --reply R.txt
rad-chatparse --render-raw T.jinja --context CTX.json
```
Renders one OpenAI chat request as the server would (container template or
`--override-chat-template`, `--chat-template-kwargs`, the plugin's declared reply format, or the
template's own with `--no-plugin`), then reads each reply file as the model's output: parsed whole,
parsed through the SSE stream as a client reassembles it, and run through the request's tool-call
grammar. It prints one JSON object; exit 1 when the two parses disagree or the grammar refuses a
token. No weights are loaded.
## The KL mode
`radiance --kld-record/--kld-ref/--kld-corpus/--kld-out/--kld-lora` is a mode of the engine, not a
separate tool: same placement and kernels, prefill instead of listening, then exit. The flags and the
corpus format are on [[Radiance Conversion]].
## Shell harnesses (`scripts/`)
They drive a running engine: bring a model up, measure it, assert on what came back. Each header
records why it measures the way it does.
| script | what it does |
|---|---|
| `mcserve.sh`, `fnserve.sh` | bring MiniCPM5-2B+DSpark / Flash-Next up and leave it running (env knobs: `P TP MLEN SEQS M KVD HR MNBT EXTRA …`) |
| `fnstop.sh` | stop whichever is running and wait for exit |
| `convdspark.sh`, `convfn.sh` | rebuild the two reference containers (`convfn.sh` needs `CALIB=`) |
| `hfget.sh` | resumable Hugging Face fetch with a content manifest |
| `fnpf.sh`, `fnconc.sh`, `cstep2.sh`, `ident.sh`, `cardclock.py` | attach to a live server: prefill rate; prefill-vs-decode contention; steady-state decode ms/step per concurrency; run-to-run hashes; which clock limit binds and which rank the TP step waits for |
| `fndrop.sh` | disconnect a client mid-request and check every other answer against a reference |
| `tierident.sh`, `tiercross.sh` | whether a session that left VRAM returns byte for byte (attention and recurrent state), including two sessions in flight |
| `mctools.sh`, `mcstream.sh`, `mcagent.sh`, `mcdecl.sh` | tool-call parsing shapes; streamed tool calls on raw SSE; speculation on an agentic workload; whether the drafter proposes |
| `dprofd.sh`, `fntrace.sh`, `mctrace.sh`, `oracle.sh` | per-op device timing; rocprofv3 traces; every op against libref on the operands the model issued |
| `kv-parity.sh`, `kv-stage-parity-64k.sh` | KV streaming: covering ring and staged prefill are byte-identical to streaming off |
**Not all of them leave a server alone.** `fnserve.sh`, `mcserve.sh`, `fnstop.sh`, `fntrace.sh`,
`dprofd.sh`, `mctrace.sh`, `mcagent.sh` and `mcdecl.sh` kill any running engine (`pkill` on the
binary), whatever port it is on; `oracle.sh` stops whatever listens on its own port. `cstep2.sh`,
`ident.sh`, `tierident.sh`, `tiercross.sh`, `mctools.sh`, `mcstream.sh`, `hfget.sh`, `convfn.sh`,
`convdspark.sh`, `fnpf.sh`, `fnconc.sh`, `fndrop.sh` and `cardclock.py` leave it running
(`fndrop.sh` deliberately disconnects clients). Never point the stopping ones at a production port.
## Related
- [[Radiance Conversion]] — `rad-convert`, `rad-export`, the KL mode.
- [[Radiance Plugins]] — what `rad-kbench`/`rad-tune`/`rad-schemas` check.
- [[Radiance Operations]] — the startup receipt these tools corroborate.
+7 -36
View File
@@ -562,29 +562,6 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
fork bomb inside the container. Kill with `podman rm -f` (the cgroup owns every generation)
never pattern-kill an interpreter name on this host; use `TORCH_LOGS` for dynamo questions.
### The quantization pipeline
- **A calibration tap is only a tap when the rig that opened it is running.** The runner writes
`.rig-boot` into the tap dir (container, image, pid) and retires it when its boot exits non-zero;
`calib-push.py` refuses without that marker, refuses when it names another container, and refuses
when the named container is not running; `qwen38-calib-window.sh` stops on a failed phase instead
of falling through its health wait. All three exist because on 2026-10-29 the unguarded chain's
rig boot died of an OOM, the health loop expired without failing, and the pusher fed four rounds
of 10M tokens into **restored production** — 86 minutes of live prefill that recorded nothing.
- **The n-gram table's pin is what starves a rig boot, and the tier is a knob.** On the BF16 payload
the table is 95.37 GiB, `--ngram-placement ram` pins it, and the calibration boot died at
`startup stopped: out of memory` with 230.22 GiB pinned of 246.07 GiB and 8.06 GiB available —
while the reference recording and every score boot succeeded at the same pool and slot count.
Calibration only needs the routed weights prefilled, so it reads the table from the container's
file tier (`--ngram-placement disk`); kldref and score keep production's placement, because their
numbers are only comparable at an identical shape.
- **Acceptance is three metrics and every slice, not a mean.** `scripts/kld-gate-qwen38.sh` grades
mean KL, the p99.9 tail, top-1 agreement and every `by_source` slice (code, prose, tools):
a mean can hide a few positions taking the whole hit, and an overall win can be bought by
regressing one distribution. The FP8-native anchor is an interval, not a requirement — without
the 172.8 GiB fetch the gate runs in two-report mode (candidate vs the served payload) and says
so on its first line.
## Contract (the wire)
- **Live surface:** `/health` (== `/ping`), `/v1/models`, `/v1/chat/completions`
@@ -661,19 +638,14 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
rebuild), `scripts/download-frank-archon-swift15.sh` (the BF16 source a w4dn8 re-convert
needs), `scripts/hfget.sh` / `scripts/hf-fetch.sh`; template
`scripts/qwen38-froggeric-v22.5.jinja`.
- The campaign (qwen38-w5nl64, live): `scripts/run-qwen38-bf16.sh` (kldref / calib / score — the
ONE measuring shape), `scripts/qwen38-calib-window.sh` (the gated calibration window),
`scripts/convert-qwen38-w5nl64.sh` with
`scripts/recipes/qwen4exp-qwen38-w5nl64-bf16trunk.recipe` (the arm driver and its template),
`scripts/kld-gate-qwen38.sh` (acceptance), `scripts/calib-push.py`,
- Convert campaign (dormant, kept for a future quant): `scripts/convert-swift15-w4dn8.sh`
`scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe`, `scripts/run-swift15-bf16.sh`
`scripts/kld-gate-swift15.sh`, `scripts/kld-then-calib.sh`, `scripts/calib-push.py`
`scripts/make-calib-corpus.py`, `scripts/rechunk-kld-corpus.py`,
`scripts/qwen38_common.py` (the tokenizer, JSONL and refusal plumbing those three
corpus tools share — one `n_tokens`, so every tool counts the same way),
`scripts/derive-protected-experts.py`, `scripts/apply-protected-cast.sh`.
- Retired swift15-era campaign scripts, superseded by the above and kept only as reference:
`scripts/convert-swift15-w4dn8.sh`, `scripts/recipes/qwen4exp-swift15-w4dn8-i8-hc8m.recipe`,
`scripts/run-swift15-bf16.sh`, `scripts/kld-gate-swift15.sh`, `scripts/kld-then-calib.sh`,
`scripts/verify-swift15-ple.py`.
`scripts/derive-protected-experts.py`
`scripts/apply-protected-cast.sh`, `scripts/verify-swift15-ple.py`.
- Probes: `scripts/probe-frank-archon-kv.py` (reads slots/window/dtype from the deployed env so
it cannot drift; leg D = decode), `scripts/frank-archon-expert-activity.py`
`scripts/autoresearch-frank-archon.sh` + `scripts/autoresearch/frank-archon-measure.py`.
@@ -683,6 +655,5 @@ vllm-radiance era (this table opens on 09-26; the era page claims 10-10).
- Retired-era leftovers in-tree: `scripts/patches/glm53-*.patch`
`scripts/patches/gpu_worker-kv-cap.patch` (fork archaeology; none applies byte-exact today).
Related: [[frank-dragoon]] (the abliterated family), [[frank-zealot]], and the engine reference —
[[Radiance Engine]], [[Radiance Serve Options]], [[Radiance Conversion]], [[Radiance Plugins]],
[[Radiance Operations]].
Related: [[frank-dragoon]] (the abliterated family), [[frank-zealot]],
---
+21 -14
View File
@@ -1,6 +1,6 @@
---
type: reference
title: "frank-dragoon on Dankclaw — halogen-flash-server 0.16.2, native v2 abliterated Qwen3.8-Flash-Next (canonical)"
title: "frank-dragoon on Dankclaw — halogen-flash-server 0.18.20, native v2 abliterated Qwen3.8-Flash-Next (canonical)"
description: "The iGPU endpoint: live shape, every era with its rollback path, the laws it paid for, and the files that own it."
tags:
- llm
@@ -10,10 +10,10 @@ tags:
- gfx1151
- halogen
- endpoint
timestamp: 2026-10-21
timestamp: 2026-10-30
---
# frank-dragoon — halogen-flash-server 0.16.2, native v2 abliterated (Dankclaw iGPU)
# frank-dragoon — halogen-flash-server 0.18.20, native v2 abliterated (Dankclaw iGPU)
Strix Halo (gfx1151) endpoint `http://192.168.0.143:7770`, served id `frank-dragoon`: a podman container
under a root `User=dank` unit. Live shape since 2026-10-17, pool raised to 401,408 on 2026-10-18
@@ -33,8 +33,8 @@ flags and nothing else. Payload `/home/dank/models/halogen-v2-abliterated/`, ver
| `qwen38-flash-next-vision.hgn` | 897,916,416 | `d62e0ae5…` | peonist `@e053f488`, hardlinked — vision ON |
| `tokenizer/` (6 flat files) | ~22 MB | `tokenizer.json` LFS | peonist `@e053f488`, hardlinked |
Image `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b86806702d7039e5ebc51cae4c66621b92fe04a`
(0.16.2, closed source), container `frank-dragoon-halogen`, published `7770:8731`; the engine's
Image `ghcr.io/peonist-ai/halogen-flash-server@sha256:e35133b206da3094cc4d70c78800b26235bd9124b9aeb0e029d4a7c153b9cff0`
(0.18.20, closed source), container `frank-dragoon-halogen`, published `7770:8731`; the engine's
unauthenticated token port 8730 stays on container loopback. Engine stdout →
`/home/dank/logs/frank_dragoon.log`, **not** the journal.
@@ -74,6 +74,7 @@ rollback plan — recoverable only until the trash is purged.
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|---|---|---|---|---|---|
| 2026-10-30 → | same v2 abliterated `.hgn` trio (payload untouched) | flash-server **0.18.20** `e35133b2…` | unchanged (2 slots, 262,144 ceiling, pool 401,408, MTP always-on) | vendor moved two minor versions past the 0.16.2 quickstart pin; the payload lock **held** — the same boot served chat, a `tool_calls` turn, vision ("Red", 132 prompt tokens) and `/v1/embeddings`, with the NPU lane's 19-file set verified against the new catalog | env line back to `0c61bf84…` + redeploy |
| 2026-10-06 → 10-17 | Swift1.5 abliterated v2 `.hgn` r2 66,687,678,464 B `9c880b8b…` + same trio | flash-server 0.16.2, same digest | 2 slots, ctx 262,144, pool 524,288 (14.6 GiB), MTP always-on | — | `/home/dank/models/halogen-flashnext-v2/` + `scripts/download-frank-dragoon-flashnext.sh` |
| 2026-10-05 → 10-06 | peonist `halogen-qwen3.8-27b` `qwen3.8-27b-p1w4d-d2.hgn` 35,865,565,184 B `274c3fc7…` | halogen 0.1.4 `sha256:dc0a39a0…` | 1 slot × 200,700, DFlash2 drafter, `HALOGEN_CACHE_MB=16384`, text-only, censored base | flash-server v2 restored vision, abliteration and MTP | `/home/dank/models/halogen-qwen3.8-27b/` + `scripts/download-frank-dragoon-halogen.sh`; 0.1.4 digest still in podman storage |
| 2026-10-03 → 10-05 | kingjones777 Uncensored `Q6_0_ROCMFPX_AGENT.gguf` 24,856,607,008 B `e966deb3…` + `mtp-Q4_0` `051a1764…` + BF16 mmproj | `kingjones30/ROCmFPX` @ `36e9acd` (`scripts/build-rocmfpx.sh`) | 1 × 262,144 f16 KV, `draft-mtp` n-max 4, vision on | — | `/home/dank/models/kingjones-Qwen3.8-27B-Uncensored-ROCmFP4-STRIX-MTP/` + `/home/dank/GitPackages/kingjones-ROCmFPX/build` + `download-frank-dragoon-kingjones-uncensored.sh` |
@@ -190,9 +191,11 @@ created it — earlier topologies in .
## Contract (wire)
- Live: `/v1/models`, `/health`, `/metrics`, POST `/v1/chat/completions`; `404`: `/ping`, `/props`
`/completion`, `/tokenize`. **No `Server-Timing` header and no `/completion` timing object** — measure
wall-clock plus `usage.prompt_tokens`.
- Live: `/v1/models`, `/health`, `/metrics`, `/props` (model_alias, total_slots, chat_template),
POST `/v1/chat/completions`, POST `/v1/embeddings` when the NPU lane is armed; `404`: `/ping`,
`/completion`; `/tokenize` answers **405 to a GET** (the route exists and wants POST). **No
`Server-Timing` header and no `/completion` timing object** — measure wall-clock plus
`usage.prompt_tokens`.
- Thinking lands in `reasoning_content` and **counts against `max_tokens`**: a budget that dies
mid-thought returns `finish_reason: "length"` with empty `content`, so clients read both fields; size
vision probes at ≥512 tokens or the model looks blind. Budget for the base reasoning long: 1,476
@@ -232,11 +235,15 @@ created it — earlier topologies in .
XDNA2 executes only vendor-precompiled AIE partition programs — that is the wall the TTS attempt died at
([[Frank TTS]]) — and flash-server ships those programs. `HALOGEN_NPU_MODELS=<names>` starts a second engine **in the
same container behind the same port**, and a request picks one by its `model`; leave the variable out and the server
is exactly what runs today. The pinned 0.16.2 image already carries the five-program catalog
(`/opt/halogen/npu/models.txt`: `decider-0.8b`, `qwen3-embedding-0.6b`, `qwen3-reranker-0.6b`, `qwen3guard-gen-0.6b`,
`qwen3.5-2b`) and `docs/NPU.md` at tag `v0.16.2` documents it, so **`/v1/embeddings` is one env var and three mounts
away — no runtime bump needed.** Wider catalog: `qwen3-embedding-4b` and `decider-4b` from 0.17.4, `embeddinggemma-2`
(text ≤ 8,192 tokens, pictures and audio in one vector space) and `wemm-embedding-4b` from 0.18.3.
is exactly what runs today. The pinned 0.18.20 image carries an 18-model catalog
(`/opt/halogen/npu/models.txt`: `decider-0.8b`, `decider-4b`, `qwen3-embedding-0.6b`, `qwen3-embedding-4b`,
`qwen3-reranker-0.6b`, `qwen3-reranker-4b`, `qwen3guard-gen-0.6b`, `qwen3guard-stream-0.6b`,
`embeddinggemma-2`, `wemm-embedding-4b`, `qwen3.5-2b`, `flux2-klein-4b`, `qwen3-asr-0.6b`, `qwen3-asr-1.7b`,
`d3-lite`, `d3-nano`, `qwen3-tts-0.6b-customvoice`, `qwen3-tts-1.7b-customvoice`), so **`/v1/embeddings` is one
env var and three mounts away — no runtime bump needed.** The catalog is **not** version-stable: 0.18.20
re-cut `qwen3-embedding-0.6b`'s device programs (14 `u*.elf` + an 8,495,512 B `consts.hnpb`, where 0.16.2
had 10 and 5,086,496 B) and the entrypoint verifies size+sha256 per file before it starts, so **an engine
bump re-fetches the staged set** — `scripts/download-frank-dragoon-npu.sh` carries the catalog block to do it.
Embedding contract on that lane: `POST /v1/embeddings`, `model: "qwen3-embedding-0.6b"`, `input` a list of **texts**
(not token ids), 1024 unit-length floats back, the Qwen instruct prefix goes inside the string
@@ -282,7 +289,7 @@ lane is **armed and serving** since 2026-10-19 (`HALOGEN_NPU_MODELS=qwen3-embedd
unset, so the container opens no outbound connection; `scripts/download-frank-dragoon-npu.sh` pins every file by
size and sha256 against the catalog and the payload mount carries the tree in.
Armed, the boot ledger reads (`/home/dank/logs/frank_dragoon.log`, 2026-10-19): the GPU fabric clock is held at its top level (2000 MHz) **before** the NPU starts; `/dev/accel/accel0` is `crw-rw-rw- nobody:nogroup` under driver `amdxdna` with the distro XRT and its NPU plugin; the program arrives as `15 files checked against this image's record`; then `npu_api: qwen3-embedding-0.6b (embeddings) on the NPU engine at 127.0.0.1:8740 (buckets u0…u9; inputs up to 4096 tokens)` and `up to 64 passes queued … up to 7200 s`. Callers never see `:8740` — the lane answers on the same `:8731` as the Flash model and a request picks the engine by its `model` field, so `/v1/models` lists `frank-dragoon` **and** `qwen3-embedding-0.6b`. The fleet's consumer is open-notebook's embedding default ([[Open Notebook]]); that page owns the app-side shapes (the URL guard, defaults-by-id, the `/api/embed` 500 that still writes the vector).
Armed, the boot ledger reads (`/home/dank/logs/frank_dragoon.log`, 2026-10-19): the GPU fabric clock is held at its top level (2000 MHz) **before** the NPU starts; `/dev/accel/accel0` is `crw-rw-rw- nobody:nogroup` under driver `amdxdna` with the distro XRT and its NPU plugin; the program arrives as `19 files checked against this image's record`; then `npu_api: qwen3-embedding-0.6b (embeddings) on the NPU engine at 127.0.0.1:8740 (buckets u0, u1, u10, u11, u12, u13, u2, u3, u4, u5, u6, u7, u8, u9; inputs up to 4096 tokens)` and `up to 64 passes queued … up to 7200 s`. Callers never see `:8740` — the lane answers on the same `:8731` as the Flash model and a request picks the engine by its `model` field, so `/v1/models` lists `frank-dragoon` **and** `qwen3-embedding-0.6b`. The fleet's consumer is open-notebook's embedding default ([[Open Notebook]]); that page owns the app-side shapes (the URL guard, defaults-by-id, the `/api/embed` 500 that still writes the vector).
Price: the programs and their weights sit in host RAM beside the locked weights ("a few GB in all" for the default
set; ~1.1 GiB for `embeddinggemma-2`), and **the Flash model decodes slower while the NPU works** — same memory bus,
+11 -3
View File
@@ -1,6 +1,6 @@
---
type: reference
title: "frank-zealot on Dankclaw — llama.cpp (PrismML fork 10777), Ternary-Bonsai-2-9B PQ2_0-MTP + refusal adapter, 3 × 200704, f16 K + q8_0 V, MTP armed"
title: "frank-zealot on Dankclaw — llama.cpp (PrismML fork 12118), Ternary-Bonsai-2-9B PQ2_0-MTP + refusal adapter, 3 × 200704, f16 K + q8_0 V, MTP armed"
description: "The RTX 3090 endpoint: live shape exactly as the env ships it, every era with its rollback path, and its card laws."
tags:
- llm
@@ -31,7 +31,7 @@ is the bug.
| Projector | `…/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf` — 624,229,664 B, sha256 `e0cb6f6988cea323…` — **Q8_0 is mandatory**, not a preference (see Contract) |
| Adapter | `…/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf` — **3,155,424 B**, sha256 `861b9c099fb5cd81…` — a rank-1 refusal-direction LoRA (`adapter.type lora`, 128 tensors, every transformer layer, embedding untouched), applied at load by `--lora-scaled <file>:1`. Its metadata names `Ternary-Bonsai-2-9B-PTQ1_0.gguf` (`3561df7a…`) as the pack it was fitted against — **not this packing, and not on this host**; see Laws |
| Fetch | `scripts/download-frank-zealot-bonsai9.sh [DEST]` (payload + projector) · `scripts/download-frank-zealot-uncensored-lora.sh [DEST]` (the adapter) — both `--verify-only`-able, `-f`, byte count and sha256 per file |
| Runtime | `/home/dank/GitPackages/llama-prism-b10777/llama-server` — PrismML **fork**, `0.2.0-dev (build 10777, commit e3de71522)`; the only loader for this payload |
| Runtime | `/home/dank/GitPackages/llama-prism-0244744/bin/llama-server` — PrismML **fork**, `0.7.0-dev (build 12118, commit 024474448)`; the only loader for this payload |
| Trio | `scripts/frank-zealot.service` (root, relinked, `User=dank`, `Restart=on-failure`, `RestartSec=10`, `LimitMEMLOCK=infinity`, `TimeoutStopSec=180`) · `scripts/frank-zealot.env` · `scripts/serve-frank-zealot.sh` |
| Address / alias | `BIND_ADDR=0.0.0.0`, `LISTEN_PORT=7771`, `ALIAS=frank-zealot` (a flag, not a fact) |
| Flags | `--ctx-size 602112 --parallel 3` ⇒ 200,704 per slot · `--cache-type-k f16 --cache-type-v q8_0` (asymmetric, **14,406.00 MiB**) · `--flash-attn on` · `--batch-size 6144 --ubatch-size 1024` · `--n-gpu-layers 99` · `--jinja --reasoning auto --reasoning-format deepseek` · `--metrics` · `--mmproj` · **`--lora-scaled Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf:1`** · `--log-verbosity 4` · `--presence-penalty 1.0` · **`--spec-type draft-mtp`** with `--spec-draft-n-max 2 --spec-draft-n-min 0 --spec-draft-p-min 0` |
@@ -136,6 +136,7 @@ with 448 completion tokens (at `max_tokens: 64` the same request returns EMPTY c
| Window | Payload | Runtime | Shape | Replaced because | Rollback |
|---|---|---|---|---|---|
| 2026-10-30 → | same 9B MTP file, Q8_0 mmproj and refusal adapter (payload untouched) | PrismML fork **12118** (`0.7.0-dev`, `024474448`) | unchanged (3 × 200,704; f16 K + q8_0 V; MTP depth 2) | fork rebuilt from `origin/prism`; the payload gate held — 202 `pq2_0` tensors and 202 Hadamard-folded weights loaded, MTP drafting engaged, chat/tools/vision answered | `LLAMA_BIN` back to `llama-prism-b10777/llama-server` (kept on disk) + redeploy |
| 2026-10-19 → | the same 9B MTP file and projector, **plus** `Ternary-Bonsai-2-9B-Uncensored-LoRA.gguf` (3,155,424 B `861b9c09…`) applied by `--lora-scaled …:1` | PrismML fork 10777 (its verified build) | — (shape unchanged; the adapter is behaviour) | Frank ordered the refusal direction removed on this endpoint | one env line — `LORA_FILE=` (or `LORA_SCALE=0`) — and redeploy; the plain pack is the same file it already loads |
| 2026-10-18 21:06 → 2026-10-19 | the same 9B MTP file and projector | PrismML fork 10777 | — | Frank ordered the third conversation back AND the drafter armed, and exact V funds neither: it bills 18,816 MiB at the pool that pays for the third slot, leaving nothing for the draft's 2,352 | one line each — `CTX_SIZE=524288 PARALLEL=2 CACHE_TYPE_V=f16 BATCH_SIZE=4096 UBATCH_SIZE=2048 SPEC_TYPE=none` — and redeploy; no payload move |
| 2026-10-18 13:19 → same evening (the boot `68afc6d` replaced) | the same 9B MTP file and projector | PrismML fork 10777 | — | Frank ordered unquantized KV, and that pool cannot survive it: 24,576 MiB of f16 cache > 24,123 addressable | one env line — `CTX_SIZE=786432 PARALLEL=3 CACHE_TYPE_K/V=q8_0` — and redeploy; no payload move |
@@ -201,7 +202,14 @@ with 448 completion tokens (at `max_tokens: 64` the same request returns EMPTY c
- `serve-frank-zealot.sh` is a direct `ExecStart`, so a missing shebang is a `203/EXEC` boot loop at `Mem peak: 1.9M` with the GPU untouched: `head -1` is a deploy check.
- `No CUDA GPUs are available` ⇒ `nvidia-modprobe -u -c=0`; KV spilled into unified memory halves decode silently, so re-read `nvidia-smi` after any shape change.
- **An adapter's provenance is metadata, not a gate.** Its GGUF carries `prism.abliterate.base_gguf = Ternary-Bonsai-2-9B-PTQ1_0.gguf` and that pack's sha256; the endpoint applies the file to the PQ2_0+MTP packing anyway, because the loader matches **tensor names, not hashes**. So an adapter's effect is only ever proven behaviourally (a refusal probe against the live endpoint), never by the card, and an adapter whose rank landed on `token_embd` would be a crash rather than a degradation — the publisher leaves the embedding untouched for exactly that reason.
- **The fork pin now has a third reason.** The payload's custom type 142, the b10776 floor that pairs MTP with a quantized V, and now this: older PrismML builds (e.g. `prism-b10709`) refuse the adapter outright with `unsupported prism.hadamard.version: 2`, so a runtime that can load the pack can still be unable to carry the adapter.
- **A fork rebuild needs an install RPATH and a bin/lib-aware `LLAMA_BIN`.** `cmake --install` strips the
build-tree rpath, so the installed `llama-server` dies on `libllama-server-impl.so` unless the configure
carries `-DCMAKE_INSTALL_RPATH='$ORIGIN;$ORIGIN/../lib'`; the installed layout is `bin/` + `lib/`, not the
flat directory b10777 shipped, so the env's `LLAMA_BIN` has to name `…/bin/llama-server`. Build 12118 was
made from `origin/prism` with the 10777 configure (Release, `GGML_CUDA=ON`, `sm_86`,
`CMAKE_CUDA_HOST_COMPILER=/usr/bin/gcc-13`, Ninja) — gcc 15 is the box default and nvcc 12.4 rejects it.
- **The fork pin now has a third reason.** The payload's custom type 142, the b10776 floor that pairs MTP with a quantized V, and now this: older PrismML builds (e.g. `prism-b10709`) refuse the adapter outright with `unsupported prism.hadamard.version: 2`, so a runtime that can load the pack can still be unable to carry the adapter. Build 12118 clears all
three.
## Contract
-14
View File
@@ -13,20 +13,6 @@ the endpoint's **Eras** table, and the numbers a retired era measured are gone.
| [[frank-dragoon]] | The iGPU endpoint: live shape, every era with its rollback path, the laws it paid for, and the files that own it. |
| [[frank-zealot]] | The RTX 3090 endpoint: live shape exactly as the env ships it, every era with its rollback path, and its card laws. |
## Radiance engine reference
| Page | Owns |
|---|---|
| [[Radiance Engine]] | The radiance inference engine: its shape, plugin model, container format, and how the reference pages fit together. |
| [[Radiance Serve Options]] | Every radiance serve flag grouped by task, with its default and the law that decides when to move it. |
| [[Radiance HTTP API]] | The HTTP surface radiance serves: endpoints, request fields, streaming, tools, and refusal rules. |
| [[Radiance Conversion]] | Turning a checkpoint into a .rad container: rad-convert, the recipe language, quantisers, reuse and export. |
| [[Radiance Model Families]] | Which radiance plugin claims which model family, and the conversion command and recipe of record for each. |
| [[Radiance Plugins]] | Writing radiance kernel and quantiser plugins: the ABI, kernel rows, hooks, encodings, and the build path. |
| [[Radiance Architecture Plugins]] | Writing a radiance architecture plugin: declaring weights, buffers, KV groups and ops, then issuing the step. |
| [[Radiance Tools]] | The radiance inspection, measurement and tokeniser tools, with the flags and verdicts each one reports. |
| [[Radiance Operations]] | Running a radiance server: the container contract, host prerequisites, readiness, ledgers and safe restarts. |
## Retired endpoints
| Page | Owns |
-9
View File
@@ -162,15 +162,6 @@ is a locator: one row, one clause, no numbers.
| [[PCIe Link and Atomics]] | Link probing, the streaming-versus-compute ceiling arithmetic, and the atomics wall that decides multi-GPU collectives. |
| [[Quantization by Hardware Architecture]] | This page maps LLM quantization formats to the GPU and CPU architectures that support them. |
| [[Quantization Production]] | Making a quantized payload here: the conversion recipe, the scoring gate, and the campaign ladder behind a shipped file. |
| [[Radiance Architecture Plugins]] | Writing a radiance architecture plugin: declaring weights, buffers, KV groups and ops, then issuing the step. |
| [[Radiance Conversion]] | Turning a checkpoint into a .rad container: rad-convert, the recipe language, quantisers, reuse and export. |
| [[Radiance Engine]] | The radiance inference engine: its shape, plugin model, container format, and how the reference pages fit together. |
| [[Radiance HTTP API]] | The HTTP surface radiance serves: endpoints, request fields, streaming, tools, and refusal rules. |
| [[Radiance Model Families]] | Which radiance plugin claims which model family, and the conversion command and recipe of record for each. |
| [[Radiance Operations]] | Running a radiance server: the container contract, host prerequisites, readiness, ledgers and safe restarts. |
| [[Radiance Plugins]] | Writing radiance kernel and quantiser plugins: the ABI, kernel rows, hooks, encodings, and the build path. |
| [[Radiance Serve Options]] | Every radiance serve flag grouped by task, with its default and the law that decides when to move it. |
| [[Radiance Tools]] | The radiance inspection, measurement and tokeniser tools, with the flags and verdicts each one reports. |
| [[RDNA4 MoE Expert Offload VRAM Budget]] | Expert-offload arithmetic on the RDNA4 fork: movable terms, the utilisation law, graph charge, ceilings, context ladder. |
| [[Shared Memory Architecture Dankclaw]] | Dankclaw uses AMD Strix Halo APU with unified memory architecture. |
+1 -1
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bats
# Seam: frank-dragoon serves Quat3rnion's NATIVE v2 conversion (2026-10-11)
# of the abliterated Qwen3.8-Flash-Next (a .hgn flash-server payload) on the
# Strix Halo iGPU through the halogen-flash-server CONTAINER (0.16.2,
# Strix Halo iGPU through the halogen-flash-server CONTAINER (0.18.20,
# digest-pinned), driven by the root unit's serve script — 2 slots over ONE
# 401,408-position KV pool at the native 262,144 ceiling, MTP head always on (HALOGEN_SPEC_ADAPT=0)
# and the vision tower mounted. 2026-10-17 cutover from the Swift1.5-finetune
+15 -4
View File
@@ -126,11 +126,22 @@ run_stage() {
@test "the shipped env points at the fork runtime, the MTP pair and the refusal adapter" {
# The payload path IS the endpoint's identity, and the runtime is part of
# it here: the file's custom tensor type 142 + prism.hadamard.* metadata
# make the PrismML build the only loader, and this build must be >= b10776
# make the PrismML build the only loader, and that build must be >= b10776
# because MTP beside q8_0 V is illegal below it. A retarget that moves the
# model but leaves the old build behind ships an endpoint that cannot load
# (type 142) or cannot run the documented MTP arm (the build gate).
grep -qE '^LLAMA_BIN=/home/dank/GitPackages/llama-prism-b10777/llama-server$' "$ENV_FILE"
# model but leaves a stock llama.cpp behind ships an endpoint that cannot
# load (type 142) or cannot run the documented MTP arm (the build gate).
#
# Assert the fork FAMILY, not the directory one rebuild happens to install:
# a literal path turns every runtime move into a test edit, and the suite
# runs on a box that has no /home/dank at all, so existence cannot be
# checked here either.
local bin
bin="$(sed -n 's/^LLAMA_BIN=//p' "$ENV_FILE")"
if [[ ! "$bin" =~ ^/home/dank/GitPackages/llama-prism-[^/]+/llama-server$ ]] &&
[[ ! "$bin" =~ ^/home/dank/GitPackages/llama-prism-[^/]+/bin/llama-server$ ]]; then
echo "LLAMA_BIN does not name a PrismML fork build: '$bin'" >&2
return 1
fi
grep -qE '^MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0\.gguf$' "$ENV_FILE"
grep -qE '^MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0\.gguf$' "$ENV_FILE"
grep -qE '^LORA_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-Uncensored-LoRA\.gguf$' "$ENV_FILE"
-206
View File
@@ -200,9 +200,6 @@ DERIVE="$REPO_ROOT/scripts/derive-protected-experts.py"
APPLY="$REPO_ROOT/scripts/apply-protected-cast.sh"
CONVERT="$REPO_ROOT/scripts/convert-qwen38-w5nl64.sh"
GATE="$REPO_ROOT/scripts/kld-gate-qwen38.sh"
RUNNER="$REPO_ROOT/scripts/run-qwen38-bf16.sh"
PUSHER="$REPO_ROOT/scripts/calib-push.py"
WINDOW="$REPO_ROOT/scripts/qwen38-calib-window.sh"
TEMPLATE="$REPO_ROOT/scripts/recipes/qwen4exp-qwen38-w5nl64-bf16trunk.recipe"
# make_gram <dir> <layer> <expert> <energy> <rows> — a RADGRAM2 down-Gram whose
@@ -528,206 +525,3 @@ gate_case() { # gate_case <kl fp8> <kl iq4r> <kl cand> <top1 cand> [extra flags.
[[ "$output" == *"has no slice 'code'"* ]]
[[ "$output" == *"it has: ops"* ]]
}
# --- the n-gram tier: the pin that starved the calibration boot ---------------
@test "the calibration boot reads the n-gram table from the file tier" {
# 2026-10-29: 95.37 GiB pinned beside a 65.59 GiB/rank pool left 8.06 GiB of
# RAM and the boot died "startup stopped: out of memory". calib only needs the
# routed weights prefilled, so it reads the table off the container's file tier.
run bash "$RUNNER" calib --dry-run
[ "$status" -eq 0 ]
grep -A1 -x -- '--ngram-placement' <<<"$output" | grep -qx 'disk'
}
@test "the recording and every score pass keep production's n-gram placement" {
# Those numbers are only comparable when the shape is identical; the tier is
# part of the shape.
run bash "$RUNNER" kldref --dry-run
[ "$status" -eq 0 ]
grep -A1 -x -- '--ngram-placement' <<<"$output" | grep -qx 'ram'
}
@test "an unknown n-gram placement is refused by name before the boot" {
run env FRANK_QWEN38_NGRAM_PLACEMENT=ssd bash "$RUNNER" calib --dry-run
[ "$status" -eq 2 ]
[[ "$output" == *"auto|vram|ram|disk"* ]]
}
# --- the pusher: a tap without a live rig is not a tap ------------------------
# stub_podman <dir> <running|absent> — a podman that answers the one question the
# pusher asks: is the rig container running?
stub_podman() {
mkdir -p "$1/bin"
if [ "$2" = running ]; then
printf '#!/usr/bin/env bash\necho true\n' > "$1/bin/podman"
elif [ "$2" = stopped ]; then
printf '#!/usr/bin/env bash\necho false\n' > "$1/bin/podman"
else
printf '#!/usr/bin/env bash\nexit 125\n' > "$1/bin/podman"
fi
chmod +x "$1/bin/podman"
}
pusher_fixture() { # pusher_fixture <marker-container or empty>
D="$BATS_TEST_TMPDIR/push"
mkdir -p "$D/tap" "$D/bin"
printf '{"prompt":"hello"}\n' > "$D/corpus.jsonl"
if [ -n "$1" ]; then
printf 'container=%s\nimage=radiance@sha256:test\nmode=calib\npid=1\n' "$1" \
> "$D/tap/.rig-boot"
fi
echo "$D"
}
@test "the pusher refuses a tap that carries no rig marker: production is not a rig" {
# With no marker, the old driver fed four rounds of 10M tokens into restored
# production after the rig's boot had died, and recorded nothing.
D="$(pusher_fixture '')"
run python3 "$PUSHER" --corpus "$D/corpus.jsonl" --tap-dir "$D/tap" --port 1
[ "$status" -eq 2 ]
[[ "$output" == *"no rig marker"* ]]
}
@test "the pusher refuses a marker that names another container" {
D="$(pusher_fixture arbench)"
run python3 "$PUSHER" --corpus "$D/corpus.jsonl" --tap-dir "$D/tap" \
--container qwen38-bf16 --port 1
[ "$status" -eq 2 ]
[[ "$output" == *"names container 'arbench'"* ]]
[[ "$output" == *"targets 'qwen38-bf16'"* ]]
}
@test "the pusher refuses when the rig container is not running" {
D="$(pusher_fixture qwen38-bf16)"
stub_podman "$D" stopped
run env PATH="$D/bin:$PATH" python3 "$PUSHER" --corpus "$D/corpus.jsonl" \
--tap-dir "$D/tap" --container qwen38-bf16 --port 1
[ "$status" -eq 2 ]
[[ "$output" == *"is not running"* ]]
}
# --- the acceptance gate without an FP8 anchor -------------------------------
# kld_report_slices <path> <model> <kl all> <kl code> <top1 all> <top1 code>
kld_report_slices() {
python3 - "$@" <<'PY'
import json, sys
path, model, kl_all, kl_code, t_all, t_code = sys.argv[1:7]
def g(kl, t, n=25000):
return {"kld": {"mean": float(kl), "p99_9": float(kl) * 4},
"top1_agreement": float(t), "positions": n}
json.dump({"model": model, "all": g(kl_all, t_all),
"by_source": {"code": g(kl_code, t_code), "ops": g(kl_all, t_all)}},
open(path, "w"))
PY
}
gate2() { # gate2 <kl iq4r> <kl cand> <top1 iq4r> <top1 cand> [extra]
D="$BATS_TEST_TMPDIR/kl2"
mkdir -p "$D"
kld_report_slices "$D/iq4r.json" iq4r-moe "$1" "$1" "$3" "$3"
kld_report_slices "$D/cand.json" w5nl64 "$2" "$2" "$4" "$4"
run bash "$GATE" "$D/iq4r.json" "$D/cand.json" "${@:5}"
}
@test "a candidate that beats the served payload on mean, tail and top-1 passes without an anchor" {
gate2 0.0190 0.0090 0.962 0.971
[ "$status" -eq 0 ]
[[ "$output" == *"candidate vs served (no FP8 anchor)"* ]]
[[ "$output" == *"KLD GATE PASS"* ]]
}
@test "a better mean with a worse tail fails: the tail is where a mean hides its damage" {
D="$BATS_TEST_TMPDIR/kl3"
mkdir -p "$D"
kld_report_slices "$D/iq4r.json" iq4r-moe 0.0190 0.0190 0.962 0.962
python3 - "$D/cand.json" <<'PY'
import json, sys
# mean better than the served payload, p99.9 worse: the shape a mean-only gate waves through
g = {"kld": {"mean": 0.0090, "p99_9": 0.4000}, "top1_agreement": 0.971, "positions": 25000}
json.dump({"model": "w5nl64", "all": g, "by_source": {"code": dict(g), "ops": dict(g)}},
open(sys.argv[1], "w"))
PY
run bash "$GATE" "$D/iq4r.json" "$D/cand.json"
[ "$status" -eq 1 ]
[[ "$output" == *"[FAIL] 2."* ]]
[[ "$output" == *"KL p99.9"* ]]
}
@test "a candidate that wins overall but regresses one source slice fails" {
D="$BATS_TEST_TMPDIR/kl4"
mkdir -p "$D"
kld_report_slices "$D/iq4r.json" iq4r-moe 0.0190 0.0190 0.962 0.962
# 'all' is better, 'code' is worse: exactly the trade a mean-only gate cannot see
kld_report_slices "$D/cand.json" w5nl64 0.0090 0.0300 0.971 0.950
run bash "$GATE" "$D/iq4r.json" "$D/cand.json"
[ "$status" -eq 1 ]
[[ "$output" == *"[PASS] 1."* ]]
[[ "$output" == *"[FAIL] 4."* ]]
[[ "$output" == *"regressed: code"* ]]
}
# --- the calibration window: gated, and it says so ---------------------------
window_fixture() {
D="$BATS_TEST_TMPDIR/win"
mkdir -p "$D/campaign/kld-ref" "$D/campaign/calib" "$D/rad" "$D/corpora"
echo '{}' > "$D/campaign/kld-ref/kld.json"
: > "$D/rad/qwen38-bf16.rad"
: > "$D/corpora/calib.jsonl"
echo "$D"
}
@test "the calibration window refuses before touching a card without a reference recording" {
D="$BATS_TEST_TMPDIR/win-less"
mkdir -p "$D/campaign/calib"
run env FRANK_QWEN38_CAMPAIGN="$D/campaign" bash "$WINDOW"
[ "$status" -eq 2 ]
[[ "$output" == *"no reference recording"* ]]
}
@test "the calibration window's dry run prints the gated plan, including what it would bank" {
D="$(window_fixture)"
run env FRANK_QWEN38_CAMPAIGN="$D/campaign" \
FRANK_QWEN38_RAD="$D/rad/qwen38-bf16.rad" \
Q38_WINDOW_CALIB_CORPUS="$D/corpora/calib.jsonl" bash "$WINDOW" --dry-run
[ "$status" -eq 0 ]
[[ "$output" == *"run-qwen38-bf16.sh calib"* ]]
[[ "$output" == *"else kill the rig"* ]]
[[ "$output" == *"calib-push.py"* ]]
[[ "$output" == *"AGENTTRASH"* ]]
}
@test "a flat calibration round is not a failure: the tap's floor is pooled" {
# round 1 of a 10.03M-token corpus against a 12M floor seals nothing, and the
# first version of this gate aborted the run there — one round too early.
run python3 - "$PUSHER" <<'PY'
import importlib.util, sys
spec = importlib.util.spec_from_file_location("cp", sys.argv[1])
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
print(m.round_verdict(8192, 8192, False), # flat: continue
m.round_verdict(8192, 4096, False), # shrink: fail
m.round_verdict(8192, 20000, False), # grew: continue
m.round_verdict(0, 0, True)) # done: stop
PY
[ "$status" -eq 0 ]
[ "$output" = "flat shrink grew done" ]
}
@test "the calibration boot's step is its own knob; the KL modes keep production's" {
# Calib is the only phase whose cost is read volume per document: four 2,048-token steps
# re-stream each layer's routed experts four times. Measured 2026-10-29: 71 tok/s, 47 h tap.
run env Q38_CALIB_MNBT=8192 bash "$RUNNER" calib --dry-run
[ "$status" -eq 0 ]
grep -A1 -x -- '--max-num-batched-tokens' <<<"$output" | grep -qx '8192'
# and the same knob must NOT reach the measuring shape those numbers are compared in
run env Q38_CALIB_MNBT=8192 bash "$RUNNER" kldref --dry-run
[ "$status" -eq 0 ]
! grep -qx '8192' <<<"$output"
run env Q38_CALIB_MNBT=wide bash "$RUNNER" calib --dry-run
[ "$status" -eq 2 ]
[[ "$output" == *"Q38_CALIB_MNBT must be a number"* ]]
}