Compare commits

...
Author SHA1 Message Date
Frank 52dbe18486 docs(vault): the census was lying; corrected, and the honest result is flat docs
`scripts/port-census.py` built its corpus from the new directory, so each page
under test was held by itself: every era page reported 0 orphan lines. With the
corpus excluding the file under test, each candidate holds 14-63 sole-record
lines, so no era page is padding and none was trashed.

Deletions of restatement only:
- Multi-Node Runtimes.md (-257): restated launch material owned by
  Qwen3.8 PP Deployment.md; index rows and two Related bullets repointed.
- log.md: 66 headings folded into 35 dated entries (one entry per date, newest
  first), titled headings kept as `date — Title` per the spec's wording, and 78
  lines of transient deploy chatter dropped where every fact token in the line
  is held by an owner page (preserved in .AGENTTRASH/20261019-log-transient/).
- Authentic: the 5-page split reverted (-93); the parent carries
  `okf-exempt: length` and the three stub pages stay deleted.

check-okf.py accepts `## YYYY-MM-DD — Title`; port-census.py excludes the file
under test and gained a docstring note about the trap. One-off compaction tool
moved to ~/.AGENTTRASH. Full suite: 451 tests pass, 0 OKF violations,
155 pages, 0 unresolved links.
2026-10-19 10:17:52 -07:00
Frank 2a12487653 fix(vault): delete the restated consolidation page, not the vault
`Multi-Node Runtimes.md` (+257) restated launch material that
[[Qwen3.8 PP Deployment]] (build target, two-process RPC table, cache-prune
cron), [[Sample Script Set]] (runnable scripts), [[Ethernet Bandwidth Math]]
and [[Expected Performance]] already own, and mixed-vendor numbers owned by
[[x4-pair-experiment]]. Consolidation means the facts end up in ONE place, not
that a new page is written beside the old owners: the four stubs it replaced
were frontmatter-only (9 lines each) and needed no successor. Page trashed,
three inbound links repointed at the real owners.
2026-10-19 09:58:04 -07:00
Frank a483e51804 Merge origin/main: reconcile the frank-zealot reslice with the dedupe pass
main moved under this campaign — PR #25/#26 resliced the RTX 3090 endpoint.
The frontmatter conflict is resolved in favour of the live shape, confirmed
by re-probe the same day: `--ctx-size 602112 --parallel 3` (3 × 200,704),
`--cache-type-k f16 --cache-type-v q8_0`, `--spec-type draft-mtp`, `--jinja
--reasoning auto`, `--metrics`. The 262,144 figures that remain on the page
are era rows and the checkpoint's trained window, not the live shape.

log.md's 2026-10-19 fleet-state bullet now records both probes and the law
this taught: a docs pass that reports live shapes must re-probe before
writing the number, because main moving mid-campaign makes the first probe
stale.
2026-10-19 05:12:41 -07:00
dank b9d21ae947 Merge pull request 'docs(frank-zealot): boot ledger of ba69ea5, corrected ubatch and q8_0 credits' (#26) from chore/frank-zealot-mtp-reslice into main 2026-10-19 03:38:40 -07:00
Frank 30e8ed55a0 docs(frank-zealot): record the ba69ea5 boot ledger and correct the carried credits
The boot of record for the new shape, from the verbosity-4 journal plus
settled nvidia-smi: target KV 14,406.00 (K f16 9,408.00 + V q8_0 4,998.00),
draft KV 2,352.00 over the same 200,704 cells x 3 seqs, recurrent state
452.25, compute 1,665.00 + 424.88 (draft 480.97 + 424.86), weights 2,283.29
+ 257.66, projector 595.29 + 248.10 + 24.93, settled 22,870 used / 1,255
free with n_slots = 3 / n_ctx_slot = 200704.

Two inherited numbers did not survive it, and both are corrected in the env,
the serve rationale and the page:

  - The ubatch 2048 -> 1024 step was quoted at "-~1,306 MiB". That was the
    786,432 / q8_0-pair arm. At THIS shape the target's compute buffers went
    2,496.13 -> 2,089.88: 406 MiB, with CUDA0 going UP 257 while CUDA_Host
    went down 663. q8_0 V (4,410 MiB, not the 3,840 previously written) is
    what funds the drafter, alone.
  - The fit gate priced 22,428 against a real 22,870: conservative by 442
    MiB, so it stays honest as written.

And one finding stated plainly: this is the first boot on this card that
misses llama.cpp's own free-memory target - the engine printed "cannot meet
free memory target of 1,867 MiB, need to reduce device memory by 102 MiB"
and settled at 1,255 MiB free, under the ~2 GiB band every previous era
held. Cheapest rungs back are CTX_SIZE=401408 or SPEC_TYPE=none, both one
line.

Probes on the same boot go in too: chat turn (448 tokens, 1,755 chars of
thinking - empty content at max_tokens 64 is the documented trap), tools
(finish_reason tool_calls, {"a":24,"b":7}), vision (512 px solid red ->
"Red", 280 prompt tokens), and the drafter's own statistics (acceptance
0.706 / 0.763, mean accepted length 2.41 / 2.53).
2026-10-19 03:38:03 -07:00
dank ba69ea563e Merge pull request 'feat(frank-zealot): 3 x 200,704, q8_0 V, MTP armed' (#25) from chore/frank-zealot-mtp-reslice into main 2026-10-19 03:19:04 -07:00
Frank a7062d1cb3 docs(frank-zealot): carry the 3 x 200,704 shape, retire the exact-KV era row
The endpoint page owns its shape: the flags row, the budget equation behind
it (25,088 B/token, 14,406.00 MiB of cache + 2,352.00 MiB of draft cache
inside 24,123 MiB addressable), the "MTP is ON" section replacing the "OFF by
measurement" one, the harness contract at the per-slot 200,704, and the
fallback rungs rewritten as deltas FROM the shipped shape.

The 2026-10-18 exact-KV boot (KV 16,384.00 / RS 100.50 / compute 1,408.06 +
1,088.07 / mmproj 868.32 charged after the fit check, settled 21,176/2,949)
and its contended rate floors move into that era's Eras row with the one-line
rollback, so the two pages that carried them collapse to one. Rates for the
new shape are deliberately absent - no bench was ordered, and the fit gate,
the health gate and the boot ledger are the evidence a swap needs.

AGENTS.md follows the same numbers: contextWindow 200704 is the per-slot
window and the trio's shape line.
2026-10-19 03:16:48 -07:00
Frank 25a8b00043 feat(frank-zealot): reslice to 3 x 200,704 with q8_0 V and MTP armed
Frank's order, 2026-10-19: --parallel 3 at 200,704 a conversation (pool
602,112), K at the engine default (f16), V to q8_0, batch 1024x6 / ubatch
1024, and the MTP drafter armed explicitly.

The three requests are one budget equation. At 8,192 elements per token per
cache the pair now bills 25,088 B/token = 14,406.00 MiB over the 602,112
pool, and the armed draft context adds its own f16 KV over that whole pool
(4,096 B/position = 2,352.00 MiB) plus buffers and recurrent cells. Dropping
V to the 8-bit class frees 4,410 MiB against exact V and the ubatch drop
hands back ~1,306 - that is what pays for the third slot AND the drafter,
neither of which exact V could fund.

The serve script's written-down fallbacks move with the env so a missing env
file cannot boot the retired shape, and the seam now: asserts the per-slot
contract (divides evenly, <= the trained 262,144), bills an armed drafter in
the VRAM fit gate, proves the gate still REFUSES both boots this shape can
walk into, pins the shipped MTP set (and the absence of --spec-draft-model,
which would load the trunk twice), and pins SPEC_TYPE=none as the rollback
that strips the whole set.
2026-10-19 03:16:35 -07:00
14 changed files with 343 additions and 591 deletions

No files matched your search

+10 -8
View File
@@ -388,10 +388,11 @@ the publisher's own **Q8_0** mmproj passed as `--mmproj` — Q8_0 is the
publisher's requirement for concurrent image requests (the card states the publisher's requirement for concurrent image requests (the card states the
BF16 tower can crash `llama-server` when several image requests run at once, BF16 tower can crash `llama-server` when several image requests run at once,
and this endpoint runs several slots). `frank-zealot`'s `contextWindow` is and this endpoint runs several slots). `frank-zealot`'s `contextWindow` is
**262144** — the PER-SLOT window (`--ctx-size 524288 --parallel 2` since the **200704** — the PER-SLOT window (`--ctx-size 602112 --parallel 3` since the
2026-10-18 reslice to exact f16 KV), which EQUALS the model's 2026-10-19 reslice that funded a third slot AND an armed MTP drafter),
native window; llama.cpp answers a longer prompt with a 400 naming the limit, deliberately below the model's native window; llama.cpp answers a longer prompt
never a truncation, so the harness must compact before it. with a 400 naming the limit, never a truncation, so the harness must compact
before it.
Note also Note also
that that
the reasoning channel differs by runtime: llama.cpp AND SGLang (qwen3 the reasoning channel differs by runtime: llama.cpp AND SGLang (qwen3
@@ -439,10 +440,11 @@ disagrees with it is the bug.
loader ignoring that metadata dequantizes wrong); payload loader ignoring that metadata dequantizes wrong); payload
`Bonsai2-9B-MTP/…PQ2_0.gguf` + the publisher's **Q8_0** mmproj (the BF16 `Bonsai2-9B-MTP/…PQ2_0.gguf` + the publisher's **Q8_0** mmproj (the BF16
tower can crash the server with concurrent image requests). tower can crash the server with concurrent image requests).
- Shape: `--ctx-size 524288 --parallel 2` = **two conversations at the - Shape: `--ctx-size 602112 --parallel 3` = **three conversations at
model's native 262,144**, `CACHE_TYPE_K/V=f16`, batch 4096 / ubatch 2048, 200,704 each**, `CACHE_TYPE_K=f16` + `CACHE_TYPE_V=q8_0` (asymmetric: 25,088
`SPEC_TYPE=none`, `--flash-attn on`, `--jinja`, `--metrics`. B/token, 14,406 MiB of pool), batch 6144 / ubatch 1024,
- Harness/gate contract: `contextWindow: 262144` is the **per-slot** window `SPEC_TYPE=draft-mtp` (depth 2), `--flash-attn on`, `--jinja`, `--metrics`.
- Harness/gate contract: `contextWindow: 200704` is the **per-slot** window
(`n_ctx_slot = CTX_SIZE / PARALLEL`), and llama.cpp answers a longer prompt (`n_ctx_slot = CTX_SIZE / PARALLEL`), and llama.cpp answers a longer prompt
with a 400 rather than truncating. `--metrics` is mandatory or Prometheus with a 400 rather than truncating. `--metrics` is mandatory or Prometheus
reads 501 = DOWN beside a healthy service. Presence penalty has no GGUF reads 501 = DOWN beside a healthy service. Presence penalty has no GGUF
+8 -3
View File
@@ -12,7 +12,8 @@ guidance, so "conforms to OKF" is a test and not a claim:
TIMESTAMP `timestamp` present, ISO 8601, not in the future TIMESTAMP `timestamp` present, ISO 8601, not in the future
LENGTH lines between 1 and 500 inclusive LENGTH lines between 1 and 500 inclusive
INDEX-FM no index.md may carry frontmatter (index.md is reserved) INDEX-FM no index.md may carry frontmatter (index.md is reserved)
LOG-DATE log.md date headings start `## YYYY-MM-DD`, newest first LOG-DATE log.md date headings are `## YYYY-MM-DD` or `## YYYY-MM-DD — Title`,
one entry per date, newest first
INDEX-COVER every index.md links each of its sibling pages; the root index INDEX-COVER every index.md links each of its sibling pages; the root index
links every page in the bundle links every page in the bundle
INDEX-NUM an index table row restates a fleet quantity — a digit run of INDEX-NUM an index table row restates a fleet quantity — a digit run of
@@ -143,8 +144,12 @@ def check_file(path: Path, root: Path, length_exempt=frozenset()):
if not DATE_RE.match(head): if not DATE_RE.match(head):
continue continue
dates.append(head[:10]) dates.append(head[:10])
if head != head[:10]: # OKF requires the heading to USE the date format; a title after an
v.append(("LOG-DATE", rel, f"date heading carries a title: '## {head}'")) # em-dash is allowed and costs zero lines, so `## 2026-08-17 — Why the
# pair was rebuilt` is conformant and `## 2026-08-17Rebuilt` is not.
if head != head[:10] and not re.match(r"^\d{4}-\d{2}-\d{2}\s+—\s+\S", head):
v.append(("LOG-DATE", rel,
f"date heading must be the date alone or `date — Title`: '## {head}'"))
if len(dates) != len(set(dates)): if len(dates) != len(set(dates)):
v.append(("LOG-DATE", rel, "duplicate date heading")) v.append(("LOG-DATE", rel, "duplicate date heading"))
if dates != sorted(dates, reverse=True): if dates != sorted(dates, reverse=True):
+1 -1
View File
@@ -1459,7 +1459,7 @@ LLM_ZEALOT_RUNTIME="${LLM_ZEALOT_RUNTIME:-/home/dank/GitPackages/llama-prism-b10
# image path — a deploy that starts without it silently ships a text-only # image path — a deploy that starts without it silently ships a text-only
# endpoint. Checked unconditionally, like frank-dragoon's. Q8_0 is MANDATORY, # endpoint. Checked unconditionally, like frank-dragoon's. Q8_0 is MANDATORY,
# not a preference: the card says the BF16 tower can crash llama-server when # not a preference: the card says the BF16 tower can crash llama-server when
# several image requests run at once, and this endpoint runs --parallel 2. Its # several image requests run at once, and this endpoint runs --parallel 3. Its
# 868.32 MiB worst-case VRAM (595.29 model + 248.10 compute in the boot ledger) # 868.32 MiB worst-case VRAM (595.29 model + 248.10 compute in the boot ledger)
# lands AFTER the engine's fit check, so it is not inside the q8_0 budget. # lands AFTER the engine's fit check, so it is not inside the q8_0 budget.
LLM_ZEALOT_MMPROJ="${LLM_ZEALOT_MMPROJ:-/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf}" LLM_ZEALOT_MMPROJ="${LLM_ZEALOT_MMPROJ:-/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf}"
+37 -21
View File
@@ -11,35 +11,51 @@ LLAMA_BIN=/home/dank/GitPackages/llama-prism-b10777/llama-server
# metadata dequantizes wrong, silently. # metadata dequantizes wrong, silently.
MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0.gguf MODEL_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-PQ2_0-MTP-Q8_0.gguf
# Q8_0 IS MANDATORY: the publisher states the BF16 tower can crash llama-server # Q8_0 IS MANDATORY: the publisher states the BF16 tower can crash llama-server
# under concurrent image requests, which is exactly --parallel 2. # under concurrent image requests, which is exactly --parallel 3.
MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf MMPROJ_FILE=/home/dank/models/Bonsai2-9B-MTP/Ternary-Bonsai-2-9B-mmproj-Q8_0.gguf
# Served id — the harness/family name, not a fact about the payload. # Served id — the harness/family name, not a fact about the payload.
# Id-keyed consumers: OpenWebUI (model row, access_grant, api_configs), models.yml. # Id-keyed consumers: OpenWebUI (model row, access_grant, api_configs), models.yml.
ALIAS=frank-zealot ALIAS=frank-zealot
BIND_ADDR=0.0.0.0 BIND_ADDR=0.0.0.0
LISTEN_PORT=7771 LISTEN_PORT=7771
# CTX_SIZE is the POOL llama.cpp divides across slots: 524,288 / 2 = 262,144 a # CTX_SIZE is the POOL llama.cpp divides across slots: 602,112 / 3 = 200,704 a
# conversation, each still at the trained window. MUST stay divisible by # conversation. MUST stay divisible by PARALLEL (the seam enforces it) and
# PARALLEL (the seam enforces it) and models.yml contextWindow quotes the # models.yml contextWindow quotes the PER-SLOT number — a longer prompt is a
# PER-SLOT number — a longer prompt is a 400, never a truncation. The pool # 400, never a truncation. The pool is bought, not granted: K+V at 25,088
# cannot go back to 786,432 under exact f16 KV: 24,576 MiB of cache against the # B/token is 14,406.00 MiB here and the MTP draft context bills its own f16 KV
# 24,123 MiB the driver leaves addressable, before a weight loads. # over the WHOLE pool (4,096 B/position ⇒ 2,352.00 MiB), so the old 786,432
CTX_SIZE=524288 # pool cannot carry a drafter at any KV dtype — 18,816 + 3,072 of cache alone
PARALLEL=2 # against the 24,123 MiB the driver leaves addressable.
BATCH_SIZE=4096 CTX_SIZE=602112
PARALLEL=3
# The logical batch is VRAM- and throughput-neutral at a fixed ubatch (the law
# on [[frank-zealot]]); 1024×6 keeps it a clean multiple of the ubatch.
BATCH_SIZE=6144
# Ubatch is a prefill knob (decode never moves with it) and it sets the # Ubatch is a prefill knob (decode never moves with it) and it sets the
# compute-buffer size — which is what decides whether a drafter fits. # compute-buffer size, which decides whether a drafter fits. 1024 is the rung
# UBATCH_SIZE=1024 is the documented MTP arm. # every measured MTP arm here ran on (213.5 t/s against 109.2 at 2048). Its
UBATCH_SIZE=2048 # VRAM credit is SHAPE-specific: the 786,432-pool arm census showed ~1,306 MiB
# Exact K and V: the retrieval-fidelity ask, paid with one conversation of # back, this boot measured 2,089.88 MiB of target compute against 2,496.13 at
# concurrency. Rate law and the arithmetic: [[frank-zealot]] → Laws. # ubatch 2048 — 406 MiB back, with the split moving (CUDA0 +257, CUDA_Host
# −663). The drafter is funded by CACHE_TYPE_V below, not by this knob.
UBATCH_SIZE=1024
# K exact, V in the 8-bit class: 16,384 + 8,704 = 25,088 B/token (8,192
# elements per token per cache × 2 B for f16, × 1.0625 for q8_0 — there is no
# true fp8 KV dtype in this build). That 7,680 B/token saved against exact V is
# what funds BOTH the third conversation and the drafter. Quantized V is
# illegal without --flash-attn, which the serve script pins. Rate law and the
# arithmetic: [[frank-zealot]] → Laws.
CACHE_TYPE_K=f16 CACHE_TYPE_K=f16
CACHE_TYPE_V=f16 CACHE_TYPE_V=q8_0
# Speculation OFF — measured, not assumed: the draft context bills its own f16 # Speculation ON (Frank, 2026-10-19): the head is in the file
# KV over the WHOLE pool, and there is no room beside exact KV (the arm census # (qwen35.nextn_predict_layers = 1, blk.32.nextn.*) and rides the MAIN model's
# is on the page). SPEC_TYPE=none strips the WHOLE set (--spec-type and every # weights, so there is no --spec-draft-model. Its bill is pool × 4,096 B of
# --spec-draft-*); half a set has segfaulted at load. # draft KV plus ~0.45 GiB of buffers and a tripled recurrent state — paid for
SPEC_TYPE=none # 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.
# 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
SPEC_DRAFT_N_MAX=2 SPEC_DRAFT_N_MAX=2
SPEC_DRAFT_N_MIN=0 SPEC_DRAFT_N_MIN=0
SPEC_DRAFT_P_MIN=0 SPEC_DRAFT_P_MIN=0
+24 -8
View File
@@ -50,23 +50,39 @@ def held(tok, corpus):
def main(argv): def main(argv):
new_dir = pathlib.Path(argv[1]) new_dir = pathlib.Path(argv[1])
corpus = [
p.read_text(errors="replace") def corpus_excluding(exclude):
for p in new_dir.rglob("*.md") """Every vault page except the one under test.
if ".AGENTTRASH" not in str(p) and ".obsidian" not in str(p)
] The corpus is what must still hold a fact AFTER the file is gone, so a
blob = "\n".join(corpus) file that lives inside the scanned directory has to be removed from it —
lost = 0 otherwise every token is "held" by the file itself and the census proves
checked = 0 nothing (it reports 0 losses for every page, which is the bug this
guards).
"""
out = []
for p in new_dir.rglob("*.md"):
s = str(p)
if ".AGENTTRASH" in s or ".obsidian" in s:
continue
if exclude is not None and p.resolve() == exclude:
continue
out.append(p.read_text(errors="replace"))
return out
lost = checked = 0
for spec in argv[2:]: for spec in argv[2:]:
if spec.startswith("git-"): if spec.startswith("git-"):
ref, path = spec[4:].split(":", 1) ref, path = spec[4:].split(":", 1)
old = subprocess.run(["git", "show", f"{ref}:{path}"], old = subprocess.run(["git", "show", f"{ref}:{path}"],
capture_output=True, text=True).stdout capture_output=True, text=True).stdout
label = f"{ref}:{path}" label = f"{ref}:{path}"
exclude = None
else: else:
old = pathlib.Path(spec).read_text(errors="replace") old = pathlib.Path(spec).read_text(errors="replace")
label = spec label = spec
exclude = pathlib.Path(spec).resolve()
blob = "\n".join(corpus_excluding(exclude))
for line in old.splitlines(): for line in old.splitlines():
s = line.strip().strip("->*|# ").strip() s = line.strip().strip("->*|# ").strip()
if len(s) < 30 or s.startswith("```"): if len(s) < 30 or s.startswith("```"):
+69 -60
View File
@@ -5,33 +5,45 @@
# #
# 2026-10-18 (Frank): Bonsai-2 27B PQ2_0 → the 9B MTP export, then the same # 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 # evening TWO 262,144-token conversations (--ctx-size 524288 --parallel 2) over
# an UNQUANTIZED f16 KV cache. The 9B bills 8,192 elements per token per cache # an UNQUANTIZED f16 KV cache.
# (8 full-attention layers instead of the 27B's 16), so exact K and V costs #
# 32,768 B/token — 16,384.00 MiB at the 524,288 pool, which is what a 24,123 MiB # 2026-10-19 (Frank): THREE 200,704-token conversations over a 602,112 pool
# addressable card can carry beside the trunk. Verbosity 4 ships so the VRAM-fit # (--ctx-size 602112 --parallel 3), K left at the engine's own default (f16),
# forensics (KV layout, buffer sizes, graph capture) land in the journal. # V dropped to q8_0, batch 1024×6 / ubatch 1024, and the MTP drafter ARMED
# (--spec-type draft-mtp). The 9B bills 8,192 elements per token per cache (8
# full-attention layers instead of the 27B's 16), so f16 K is 16,384 B/token and
# q8_0 V is 8,704: 25,088 B/token = 14,406.00 MiB at the 602,112 pool. That
# 7,680 B/token against exact V is exactly what funds the third conversation
# AND a draft context that bills f16 KV over the whole pool. Verbosity 4 ships
# so the VRAM-fit forensics (KV layout, buffer sizes, graph capture) land in
# the journal.
# #
# Architecture (engine ledger, not the marketing page): qwen35 hybrid, 32 # Architecture (engine ledger, not the marketing page): qwen35 hybrid, 32
# layers with qwen35.full_attention_interval = 4 ⇒ 8 full-attention + 24 Gated # layers with qwen35.full_attention_interval = 4 ⇒ 8 full-attention + 24 Gated
# DeltaNet, 4 KV heads × 256 dim, native 262,144. The KV pool scales with the 8 # DeltaNet, 4 KV heads × 256 dim, native 262,144. The KV pool scales with the 8
# full layers (16,384.00 MiB of f16 at the 524,288 pool); the DeltaNet layers # full layers (14,406.00 MiB of f16 K + q8_0 V at the 602,112 pool); the
# cost a fixed recurrent state per SLOT — ~50.25 MiB a cell, so 100.50 MiB for # DeltaNet layers cost a fixed recurrent state per SLOT — ~50.25 MiB a cell, so
# two. The runtime is a hard lock, not a preference: custom GGML type 142 plus # 150.75 MiB for three, and an armed depth-2 drafter triples that again. The
# prism.hadamard.* metadata, which only the vendor build registers (the b10776 # runtime is a hard lock, not a preference: custom GGML type 142 plus
# gate that moved the prebuilt 10754 → 10777 was MTP beside quantized V, which # prism.hadamard.* metadata, which only the vendor build registers — and the
# exact KV now makes moot — the type-142 lock alone keeps the build pinned). # b10776 gate that moved the prebuilt 10754 → 10777 (MTP beside a quantized V)
# is LOAD-BEARING again on this shape, not moot as it was under exact KV.
# Vision comes from the publisher's Q8_0 projector (the BF16 one can crash the # Vision comes from the publisher's Q8_0 projector (the BF16 one can crash the
# server on concurrent images, and this endpoint runs two slots), so --mmproj is # server on concurrent images, and this endpoint runs three slots), so --mmproj is
# what gives this endpoint eyes; MMPROJ_FILE="" is the documented text-only # what gives this endpoint eyes; MMPROJ_FILE="" is the documented text-only
# opt-out. # opt-out.
# #
# Speculative decoding ships OFF, measured: the head loads and accepts well # Speculative decoding ships ON (Frank, 2026-10-19). The head is IN the file and
# (0.798/0.617 per position, mean length 2.41) but the draft context bills its # accepts well (0.798/0.617 per position, mean length 2.41, measured 2026-10-18);
# own f16 KV over the WHOLE pool — 4,096 B/position × 524,288 = 2,048 MiB plus # what made it lose that evening was the bill, not the drafts — the draft context
# buffers — and the trunk already projects 21,234 MiB with exact KV. Exact KV # is built against the target's WHOLE pool, so blk.32 adds 4,096 B/position
# and MTP cannot share this card; frank-zealot.env carries the q8_0-era arm table # (2,352.00 MiB at 602,112) plus 480.97 MiB of its own compute buffers on the
# and the two doors back to the drafter (one window, or KV back to q8_0 at # card, and it triples the recurrent state per slot (452.25 MiB for three cells
# --ubatch-size 1024 for 213.5 t/s). # against 50.25 a cell with the drafter off). CACHE_TYPE_V=q8_0 is what pays
# that: 7,680 B/token cheaper than exact V, 4,410 MiB at this pool. The ubatch
# drop to 1024 contributes ~406 MiB here, NOT the ~1,306 the 786,432-pool census
# showed — that figure does not transfer between shapes. SPEC_TYPE=none is
# the rollback and strips the whole flag set.
# #
# Sampling: the checkpoint carries the publisher's thinking recipe as GGUF # Sampling: the checkpoint carries the publisher's thinking recipe as GGUF
# metadata (general.sampling.* = temp 1.0 / top_p 0.95 / top_k 20 / min_p 0 / # metadata (general.sampling.* = temp 1.0 / top_p 0.95 / top_k 20 / min_p 0 /
@@ -58,49 +70,46 @@ fi
: "${ALIAS:=frank-zealot}" : "${ALIAS:=frank-zealot}"
: "${BIND_ADDR:=0.0.0.0}" : "${BIND_ADDR:=0.0.0.0}"
: "${LISTEN_PORT:=7771}" : "${LISTEN_PORT:=7771}"
# ctx-size is the POOL llama.cpp divides across slots: 524,288 / 2 = 262,144 # ctx-size is the POOL llama.cpp divides across slots: 602,112 / 3 = 200,704 a
# per conversation — every slot still at the model's trained window. The harness # conversation. The harness contextWindow MUST quote the PER-SLOT number — a
# contextWindow MUST quote the PER-SLOT number — a longer prompt is answered # longer prompt is answered with a 400, never truncated. The pool is priced by
# with a 400, never truncated. Under exact KV the pool cannot stay at 786,432: # the KV bill, not by the trained window: at 25,088 B/token (f16 K + q8_0 V) the
# 32,768 B/token × 786,432 = 24,576 MiB, more than the card's whole addressable # 602,112 pool costs 14,406.00 MiB and the armed drafter adds its own f16 KV over
# 24,123 MiB before a single weight loads. # the whole pool (2,352.00 MiB). Exact V at this pool (18,816 MiB) or the old
: "${CTX_SIZE:=524288}" # 786,432 pool at any dtype cannot boot inside the 24,123 MiB the driver leaves
: "${PARALLEL:=2}" # addressable.
: "${CTX_SIZE:=602112}"
: "${PARALLEL:=3}"
# Compute-buffer geometry. Ubatch is a prefill knob (decode never moves). # Compute-buffer geometry. Ubatch is a prefill knob (decode never moves).
# 2048 is the measured CUDA optimum (MiMo era, same arch/shape: +4.6 % prefill # The logical batch is VRAM- and throughput-neutral at a fixed ubatch, so 6144
# at 8.9k depth, decode flat, 616 MiB). The logical batch is VRAM- and # (1024×6) is free; the ubatch is what sets the compute-buffer size, and 1024 is
# throughput-neutral at a fixed ubatch. It ALSO sets the compute-buffer size, # the rung every measured MTP arm here ran on (213.5 t/s at 1024 against 109.2
# which is the term that decides whether an MTP draft context fits beside a KV # at 2048) — worth ~406 MiB at THIS shape, not the ~1,306 the old pool showed.
# pool: 1024 hands back 1,306 MiB and was the arm that ran the drafter at three : "${BATCH_SIZE:=6144}"
# q8_0 windows (frank-zealot.env carries the table). : "${UBATCH_SIZE:=1024}"
: "${BATCH_SIZE:=4096}" # Asymmetric KV: f16 K (the engine's own default, spelled out so the contract is
: "${UBATCH_SIZE:=2048}" # visible in argv) + q8_0 V. 8,192 elements per token per cache (8 full-attention
# Exact KV: f16 K + f16 V — no quantization error anywhere in the attention # layers × 4 KV heads × 256 dim) ⇒ 16,384 B/token for K and 8,704 for V (q8_0 is
# state. 32 KiB/token at this geometry (8 full-attention layers × 4 KV heads × # 1.0625 B/element — llama.cpp has no true fp8 KV dtype), 25,088 B/token and
# 256 dim × 2 B × 2 caches); 16,384.00 MiB at the 524,288 pool. That is the whole # 14,406.00 MiB at the 602,112 pool. That is the halfway door between exact KV
# reason the slot count is 2: the same dtype at the old 786,432 pool is 24,576 # (32,768 B/token, which costs the third conversation and the drafter both) and
# MiB and dies at cudaMalloc. q8_0 K + q8_0 V (17,408 B/token, 13,056.00 MiB at # the fully quantized pair (17,408 B/token, which costs retrieval fidelity in the
# 786,432) is the documented fallback rung and funds three windows; CACHE_TYPE_V # keys); --flash-attn stays pinned because quantized V is illegal without it.
# alone is the halfway door. Explicit either way so the contract is visible in
# the argv, and --flash-attn stays pinned because quantized V — the fallback —
# is illegal without it (no true fp8 KV dtype exists in this build).
: "${CACHE_TYPE_K:=f16}" : "${CACHE_TYPE_K:=f16}"
: "${CACHE_TYPE_V:=f16}" : "${CACHE_TYPE_V:=q8_0}"
# Speculative decoding defaults OFF, measured on this card and shape: the head # Speculative decoding defaults ON: the head IS in the checkpoint
# IS in the checkpoint (qwen35.nextn_predict_layers = 1 — MTP is a property of # (qwen35.nextn_predict_layers = 1 — MTP is a property of the FILE, so the plain
# the FILE, so the plain PQ2_0 sibling could not run it at any flag value), it # PQ2_0 sibling could not run it at any flag value) and the draft context rides
# loads, and it accepts well (mean accepted length 2.41) — but the draft context # the MAIN model's weights, so there is no --spec-draft-model. Its bill is the
# bills its own f16 KV over the WHOLE pool (4,096 B/position, 2,048 MiB at the # target's whole pool at 4,096 B/position plus ~0.45 GiB of buffers at this
# 524,288 pool) and at ubatch 2048 that squeeze halves decode: 167.2 → 109.2 t/s # ubatch and a tripled recurrent state; CACHE_TYPE_V=q8_0 and UBATCH_SIZE=1024
# on the q8_0 3-slot shape. The draft cache dtype IS a knob (--spec-draft-type-k # above are what pay for it. The draft cache dtype IS a further knob
# / --spec-draft-type-v, both default f16 — verified in this build's --help), so # (--spec-draft-type-k / --spec-draft-type-v, both default f16 — verified in this
# an armed drafter can halve its own bill; there is no --spec-draft-model because # build's --help) and stays at its default here. Card gate: quantized V alongside
# the draft rides the main model's weights. Card gate: quantized V alongside MTP # MTP needs fork build >= prism-b10776, which is why LLAMA_BIN is the 10777
# needs fork build >= prism-b10776, which is why LLAMA_BIN moved to the 10777 # prebuilt; the type-142 loader lock keeps it pinned regardless.
# prebuilt; exact KV removes that gate but NOT the type-142 loader lock. # SPEC_TYPE=none is the rollback and strips the whole set (flags move together).
# SPEC_TYPE=draft-mtp + UBATCH_SIZE=1024 is the documented MTP arm (213.5 t/s); : "${SPEC_TYPE:=draft-mtp}"
# SPEC_TYPE=none strips the whole set (flags move together).
: "${SPEC_TYPE:=none}"
: "${SPEC_DRAFT_N_MAX:=2}" : "${SPEC_DRAFT_N_MAX:=2}"
: "${SPEC_DRAFT_N_MIN:=0}" : "${SPEC_DRAFT_N_MIN:=0}"
: "${SPEC_DRAFT_P_MIN:=0}" : "${SPEC_DRAFT_P_MIN:=0}"
@@ -13,7 +13,7 @@ timestamp: 2026-08-14
This page estimates the speed of a 25-40B model split across two machines This page estimates the speed of a 25-40B model split across two machines
over ethernet. It uses the bandwidth math in [[Ethernet Bandwidth Math]] over ethernet. It uses the bandwidth math in [[Ethernet Bandwidth Math]]
and the mixed-vendor measurements in [[Multi-Node Runtimes]]. and the mixed-vendor measurements in [[x4-pair-experiment]].
## The Bandwidth Ceiling Is Not the Limit ## The Bandwidth Ceiling Is Not the Limit
@@ -93,5 +93,5 @@ roughly 200k-260k tokens across both nodes.
## See Also ## See Also
- [[Ethernet Bandwidth Math]] - [[Ethernet Bandwidth Math]]
- [[Multi-Node Runtimes]] - [[Qwen3.8 PP Deployment]]
- [[Sample Script Set]] - [[Sample Script Set]]
@@ -1,257 +0,0 @@
---
type: reference
title: Multi-Node Runtimes
description: "How each two-machine runtime is built and launched, what it can combine with, and the mixed-vendor verdicts."
tags:
- pipeline-parallelism
- llamacpp
- vllm
- speculative
timestamp: 2026-10-19
---
# Multi-Node Runtimes
Two runtimes can put one model on two machines: llama.cpp over the ggml-rpc
backend, and vLLM with pipeline parallelism. This page owns the cross-runtime
facts — how each is built and launched, what each can and cannot be combined
with, and the mixed-vendor verdicts. Facts owned by a sibling page are linked,
not restated. Bytes-per-token arithmetic and link budgets belong to
[[Ethernet Bandwidth Math]], throughput estimates to [[Expected Performance]],
the real deployment and its verified gotchas to [[Qwen3.8 PP Deployment]], the
educational scripts to [[Sample Script Set]], drafter mechanisms to
[[DFlash DSpark Speculative Decoding]], single-host multi-GPU placement to
[[Llama Cpp Multi-Device Splits]].
## What each runtime is
Both distribute by **layers**, not inside layers.
| | llama.cpp over ggml-rpc | vLLM pipeline parallelism |
| --- | --- | --- |
| unit of distribution | a contiguous slice of layers (`--split-mode layer`, the default) | a contiguous slice of layers (`--pipeline-parallel-size`) |
| process model | one `llama-server`/`llama-cli` on the head; a `ggml-rpc-server` per worker | one `vllm serve` per node, one engine across them |
| what crosses the wire | whatever graph work the head offloads to the remote devices | the hidden state at each stage boundary, point to point |
| KV cache | distributed with the weights; the KV of a layer lives with the owner of that layer | each stage holds the KV of the layers it owns |
| speculation together with the split | yes in llama.cpp (see below) | no, per vLLM's own docs |
Upstream's own summary is why layer-wise is the ethernet choice: pipeline
parallel "minimizes data transfers between GPUs but requires many tokens to
scale well", while tensor parallel "does multiple cross-GPU reductions per
layer … much more bottlenecked by the GPU interconnect speed".
Do not carry the tensor path across machines: `--split-mode tensor` is
experimental, needs flash attention, rejects quantized KV, and is not
implemented for the MoE and hybrid architectures the guide lists — and
per-tensor placement across vendors is not expressible (§1 of [[Llama Cpp Multi-Device Splits]]).
## llama.cpp over ggml-rpc
### Build and launch
Both sides take `-DGGML_RPC=ON` next to that host's own backend:
```bash
cmake .. -DGGML_CUDA=ON -DGGML_RPC=ON # a CUDA worker; -DGGML_HIP=ON for an AMD one
cmake --build . --config Release
```
Worker side — `ggml-rpc-server` exposes ggml devices on the remote host:
```bash
bin/ggml-rpc-server # endpoint 127.0.0.1:50052 by default
bin/ggml-rpc-server --device CUDA0 -c # or CUDA_VISIBLE_DEVICES=0; -c = local cache
```
- It exposes every accelerator it can see; a host with no accelerator exposes a
single `CPU` device. `--device` and `CUDA_VISIBLE_DEVICES` are equivalent
ways to narrow that set.
- `-c` keeps large tensors in `$HOME/.cache/llama.cpp/rpc` (redirect with
`LLAMA_CACHE`) so a big model does not re-stream per load; that cache
filling the worker disk is a recorded fleet incident ([[Qwen3.8 PP Deployment]]).
Head side — build the local backends plus `-DGGML_RPC=ON`, then name the
workers:
```bash
llama-server -m model.gguf --rpc 192.168.88.10:50052,192.168.88.11:50052 -ngl 99
```
`--rpc` is a comma-separated list of `host:port`, one entry per
`ggml-rpc-server`; the head's own devices stay in the pool next to the remote
ones. Launch scripts for this shape: [[Sample Script Set]].
### Split, context, KV
- Default placement: weights **and** KV across all devices, local and remote,
"in proportion to each device's available memory". Override with
`--tensor-split`, whose proportions follow the order in `--device`. Why the
fleet always pins it (a Strix reports a huge GTT window, so the automatic
split lands on the slow device) is [[Qwen3.8 PP Deployment]]'s.
- `-sm layer` (the default) is pipeline parallelism: each device holds a
contiguous slice of layers and the KV cache of a layer lives on the device
that owns that layer.
- KV types are the usual `-ctk` / `-ctv`, `f16` by default. `--fit` is on by
default and sizes unset arguments to device memory; turn it off to hold a
context you set on purpose. Context is one global `--ctx-size` — the RPC
README documents no per-node context or per-node KV knob.
- Upstream's out-of-memory ladder (stated for tensor mode, useful order
anyway): lower `--ctx-size`, then `--parallel`, then `--n-gpu-layers`.
- Transport is TCP; RDMA is negotiated at the handshake with no flag changes
and falls back to TCP unless both peers can use it (Linux RoCEv2 via
`libibverbs`, macOS RDMA over Thunderbolt 5 via `librdma`). It is point to
point — name the peer's RDMA-capable address in `--rpc` or the session stays
on TCP. `GGML_RPC_NO_RDMA=1` forces TCP; `GGML_RPC_DEBUG` traces the backend.
Upstream calls the backend a proof of concept, "fragile and insecure" (see the
Security section). Two limits shape the design: decode is bound by the per-step
round trip, not by the link's bytes ([[Ethernet Bandwidth Math]]), and every
node must run the identical build or the handshake hangs ([[Qwen3.8 PP Deployment]]).
## vLLM pipeline parallelism
> **Provenance.** Flags and mechanics below were recorded 2026-08-14 and are
> not re-checked upstream in this pass (see "Unverified in this pass"); the
> incompatibility lines, the method catalog and the issue history are re-verified.
- `--pipeline-parallel-size` divides the layers into contiguous stages;
`--tensor-parallel-size` still works inside a node, so the canonical two-node
shape is tp = GPUs per node, pp = nodes.
- Two backends: Ray (`--distributed-executor-backend ray`, the worker joining
a `ray start --head`) or multiprocessing (`--distributed-executor-backend mp`
with `--nnodes`, `--node-rank`, `--master-addr`, `--headless` on the
follower). `mp` avoids the Ray dependency; Ray brings the cluster manager.
- What the interconnect must carry: one activation tensor per stage boundary
per forward pass, point to point — not a per-layer reduction. That is the
whole cross-node traffic story; the byte arithmetic and link budgets are
[[Ethernet Bandwidth Math]]. Pin the NIC vLLM talks on
(`NCCL_SOCKET_IFNAME`) rather than letting it guess an interface.
- The PP bubble: with N stages and one sequence in flight, one stage works
while N-1 idle, so pp=2 costs half the machine at batch one. Batching is the
cure — pipeline parallelism "requires many tokens to scale well" — and the
microbatch formula is [[Ethernet Bandwidth Math]] §5.3.
- KV and context: each stage keeps the KV of its own layers, so the pool grows
with the stage count, and `--max-model-len` is not reduced by the split
(recorded 2026-08-14, unverified here).
- What it cannot combine with, in vLLM's words: "Pipeline parallelism is not
composable with speculative decoding as of `vllm<=0.15.0`", and "Speculative
decoding with a draft models is not supported in `vllm<=0.10.0`". Composing
the two has been requested since 2024 and closed stale (#6911, #10615); PP
also carries a Qwen3.5 bug report closed stale without a fix (#36643).
- Method catalog at v0.25.1: EAGLE, MTP, draft model, PARD, MLP, n-gram, suffix
decoding, hidden-state extraction, a custom proposer class, dynamic
speculative decoding; `dflash` is an accepted `method` value.
## Speculative decoding across the split
llama.cpp chains several implementations behind one comma-separated
`--spec-type` list; the split does not change the chain, only where its passes
run. Mechanisms, chaining laws and drafter KV sizing are
[[DFlash DSpark Speculative Decoding]]'s; only the split interaction is here.
| Method | llama.cpp type | Over ggml-rpc | With vLLM PP | Why |
| --- | --- | --- | --- | --- |
| MTP | `draft-mtp` | works; not documented upstream, but run this way on this fleet's split head with acceptance measured ([[Qwen3.8 PP Deployment]]) | no | Context-backed drafter: it needs a draft context and runs in the head process, so RPC devices are simply more devices under it |
| DFlash | `draft-dflash` | same gate, same status as MTP | no | Same draft-context gate (`ctx_dft`); the drafter's KV lands on whichever device the draft context is built on — the one thing to check on a split |
| ngram-mod | `ngram-mod` | safest of the four: no draft model, no draft context, no remote draft pass | no | Draftless — proposals come from cached token n-grams in the head, so the split adds no device work |
| DSpark | `draft-dspark` | **unproven over RPC** — not a missing feature | no | `draft-dspark` exists upstream; nobody has measured it with the target split across two machines |
**Correction (2026-10-19).** A retired stub in this directory asserted "DSpark
is vLLM-only." False. `draft-dspark` is in upstream llama.cpp — parsed in
`common/arg.cpp`, mapped to `COMMON_SPECULATIVE_TYPE_DRAFT_DSPARK` in
`common/speculative.cpp`, where it carries a Markov head and an anchor-first
block layout and aborts with `DSpark draft has no confidence head: please set
--spec-draft-p-min 0` when the drafter has no confidence head. What is unproven
is running it over RPC, and that is the honest limit of the claim.
The vLLM column is uniform because that statement is blanket: it names pipeline
parallelism, not a method, so n-gram and suffix are excluded as written. vLLM
gives no mechanism with it; "the drafter must sit next to the target's layers,
which a stage split separates" is a reading, not upstream's words.
Upstream likewise documents neither for nor against `--rpc` plus
`--spec-type`. What the implementation does say is structural: every
speculative implementation is built in the head process against the head's
device set, and draftless types need no draft context at all. Routing draft
passes to a remote worker, however, has never been tested.
## Mixed-vendor reality
What works today:
- **One vendor per node.** RPC is backend-agnostic by construction: upstream's
own diagram has a host exposing CUDA0+CUDA1, a host exposing Metal, and a
host exposing CUDA0+CPU, all driven by one main host over TCP. Each node
builds its own backend plus `-DGGML_RPC=ON`; nothing speaks CUDA-to-ROCm.
- **Identical build tag on every node**, worker restarted before head. The
verified version-skew gotcha is [[Qwen3.8 PP Deployment]]'s.
- **Speculation on the head** while the split carries the target (table above).
- **RDMA when both peers have it**, auto-negotiated at the handshake; the size
of the gain is third-party and unverified here.
Unproven or broken:
- **One binary holding both CUDA and ROCm for a same-host layer split**:
reported to compile and then list only CUDA devices (issue #11506,
third-party, unverified). Vulkan is the reported single-binary cross-vendor
path, and single-host placement is [[Llama Cpp Multi-Device Splits]]'s, whose
verdict is that per-tensor placement across a vendor mix never works.
- **vLLM across a CUDA node and a ROCm node**: no known experiment; both ends
run the same backend (recorded 2026-08-14). AMD's own two-Halo clustering
guide is tensor parallelism over RCCL, not a mixed-vendor split.
- **DSpark over RPC**: unproven, see the correction above.
- **The exact NVIDIA-desktop + Strix-Halo-NUC pair has no published
benchmark**, and the third-party mixed-vendor figures it is estimated from
(Metal+CUDA over 10 GbE, two Strix Halos over USB4, a three-card CUDA+ROCm set
over 1 GbE, a Vulkan triple in one box, an AMD four-node Strix cluster) live on
[[Expected Performance]] and [[Ethernet Bandwidth Math]], each named with its
model and hardware. None was re-verified in this pass.
## Security
`ggml-rpc-server` has **no authentication and no encryption**. Upstream's own
warning: the backend is "in a proof-of-concept development stage. As such, the
functionality is fragile and insecure. Never run the RPC server on an open
network or in a sensitive environment!" It binds `127.0.0.1:50052` by default —
safe by accident. Bind a routable address and any host that can reach the port
can allocate memory there and supply the tensors it computes on. Trusted LAN
only, firewalled or tunnelled; never a public address.
## Unverified in this pass
Every item was first recorded 2026-08-14, in the research that preceded this
page. Nothing is silently deleted; none is backed by the sources cited below.
- **RPC protocol version and handshake rule** — recorded as protocol `5.0.0`
with "major must match, server minor must be ≥ client". The README's own
banner prints `Starting RPC server v3.0.0`; the conflict is unresolved.
- **RPC internals** — a hard cap of 16 servers, synchronous graph compute, no
tensor copy between two RPC servers, workers loading tensors on first use, a
hash-keyed cache above roughly 10 MB, asserts on quantized tensors whose rows
are not 512-aligned, and `-rpc` on `llama-bench` (the README names only
`llama-cli` and `llama-server`).
- **vLLM flag surface** — `--pipeline-parallel-size`, `--tensor-parallel-size`,
`--distributed-executor-backend {ray,mp}`, `--nnodes`, `--node-rank`,
`--master-addr`, `--headless`, the `ray start --head` join,
`NCCL_SOCKET_IFNAME`.
- **vLLM mechanics** — one activation handoff per stage boundary per forward
pass, a worked ~512 MB handoff for a hidden-5120 model at batch 8 and 2048
positions, `--max-model-len` unchanged by PP, ~50% idle at pp=2 batch one.
- **vLLM throughput verdicts** for a 27B across two nodes over gigabit-class
links; superseded as an estimate by [[Expected Performance]].
- **Mixed-vendor claims** — a CUDA+HIP build listing only CUDA devices
(#11506), Vulkan as the universal cross-vendor backend, AMD's two-Halo
clustering guide being RCCL tensor parallelism, WiFi costing about ten times
wired, the slowest hop setting the decode pace, RDMA worth a few percent.
- **Draft over RPC** — that the draft model stays on the head, and that routing
draft passes to a remote worker has never been confirmed.
## Citations
- [llama.cpp RPC README](https://github.com/ggml-org/llama.cpp/blob/master/tools/rpc/README.md), accessed 2026-10-19 — server usage and device exposure, `-DGGML_RPC=ON` on both sides, the `--rpc host:port` list, proportional weight+KV split and `--tensor-split`, the `-c` local cache, RDMA negotiation and `GGML_RPC_NO_RDMA`, the proof-of-concept and no-authentication warning.
- [llama.cpp multi-GPU guide](https://github.com/ggml-org/llama.cpp/blob/master/docs/multi-gpu.md), accessed 2026-10-19 — split modes (`-sm layer` = pipeline parallelism, KV with the layer owner), `-ts`/`-ngl`/`-ctk`/`-ctv`/`-fit`, the pipeline-versus-tensor summary, the tensor-mode limits.
- [vLLM speculative decoding docs, v0.25.1](https://docs.vllm.ai/en/v0.25.1/features/speculative_decoding/), accessed 2026-10-19 — the pipeline-parallelism incompatibility statement, the draft-model version floor, the method catalog.
- [llama.cpp `common/speculative.cpp`](https://github.com/ggml-org/llama.cpp/blob/master/common/speculative.cpp), accessed 2026-10-19 — the speculative implementation set including `draft-dspark`, its draft-context gate, its confidence-head error.
- vLLM issue #6911, "[Feature]: Combine pipeline parallelism with speculative decoding", opened 2024-07-29, closed not-planned 2025-03-28, accessed 2026-10-19.
- vLLM issue #10615, "[Usage]: Does speculative decoding support pipeline parallelism?", opened 2024-11-25, closed not-planned 2025-04-18, accessed 2026-10-19.
- vLLM issue #36643, "[Bug]: Qwen3.5 does not work with pipeline parallelism", opened 2026-03-10, closed not-planned 2026-09-24, accessed 2026-10-19.
@@ -255,7 +255,7 @@ is 30 GiB.
## See Also ## See Also
- [[Multi-Node Runtimes]] - [[Qwen3.8 PP Deployment]]
- [[Expected Performance]] - [[Expected Performance]]
- [[Ethernet Bandwidth Math]] - [[Ethernet Bandwidth Math]]
- [[DFlash DSpark Speculative Decoding]] - [[DFlash DSpark Speculative Decoding]]
@@ -69,4 +69,3 @@ graph LR
| Page | Owns | | Page | Owns |
|---|---| |---|---|
| [[Multi-Node Runtimes]] | How each two-machine runtime is built and launched, what it can combine with, and the mixed-vendor verdicts. |
+68 -48
View File
@@ -1,6 +1,6 @@
--- ---
type: reference type: reference
title: "frank-zealot on Dankclaw — llama.cpp (PrismML fork 10777), Ternary-Bonsai-2-9B PQ2_0-MTP, 2 × 262144, exact f16 KV" title: "frank-zealot on Dankclaw — llama.cpp (PrismML fork 10777), Ternary-Bonsai-2-9B PQ2_0-MTP, 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." description: "The RTX 3090 endpoint: live shape exactly as the env ships it, every era with its rollback path, and its card laws."
tags: tags:
- llm - llm
@@ -13,10 +13,11 @@ tags:
timestamp: 2026-10-18 timestamp: 2026-10-18
--- ---
# frank-zealot — llama.cpp (PrismML) Bonsai-2-9B PQ2_0-MTP, 2 × 262,144, exact f16 KV # frank-zealot — llama.cpp (PrismML) Bonsai-2-9B PQ2_0-MTP, 3 × 200,704, f16 K + q8_0 V, MTP armed
Dankclaw's RTX 3090 endpoint — `http://192.168.0.143:7771`, wire id `frank-zealot`, the fleet's **compaction hub** Dankclaw's RTX 3090 endpoint — `http://192.168.0.143:7771`, wire id `frank-zealot`, the fleet's **compaction hub**
(widest window it has). One page per endpoint: eras are rows below, not pages. Siblings: [[frank-archon]], (three conversations at 200,704 each, the window the other two endpoints also cap at). One page per
endpoint: eras are rows below, not pages. Siblings: [[frank-archon]],
[[frank-dragoon]]. Deploy with `./scripts/deploy.sh dankclaw --service frank-zealot`; swap mechanics in [[frank-dragoon]]. Deploy with `./scripts/deploy.sh dankclaw --service frank-zealot`; swap mechanics in
[[Model Swap and Rollback]], sizing math in [[Llama Cpp Serving Sizing]]. `scripts/frank-zealot.env` and [[Model Swap and Rollback]], sizing math in [[Llama Cpp Serving Sizing]]. `scripts/frank-zealot.env` and
`scripts/serve-frank-zealot.sh` are the authority for the shape; where any prose disagrees with them, the prose `scripts/serve-frank-zealot.sh` are the authority for the shape; where any prose disagrees with them, the prose
@@ -32,72 +33,87 @@ is the bug.
| 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-b10777/llama-server` — PrismML **fork**, `0.2.0-dev (build 10777, commit e3de71522)`; 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` | | 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) | | Address / alias | `BIND_ADDR=0.0.0.0`, `LISTEN_PORT=7771`, `ALIAS=frank-zealot` (a flag, not a fact) |
| Flags | `--ctx-size 524288 --parallel 2` ⇒ 262,144 per slot · `--cache-type-k f16 --cache-type-v f16` (exact, **16,384 MiB**) · `--flash-attn on` · `--batch-size 4096 --ubatch-size 2048` · `--n-gpu-layers 99` · `--jinja --reasoning auto --reasoning-format deepseek` · `--metrics` · `--mmproj` · `--log-verbosity 4` · `--presence-penalty 1.0` · **`SPEC_TYPE=none`** | | 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` · `--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` |
**Why this shape.** `general.architecture qwen35`: 33 blocks (32 trunk + `blk.32` = the MTP block), **Why this shape.** `general.architecture qwen35`: 33 blocks (32 trunk + `blk.32` = the MTP block),
`full_attention_interval 4` ⇒ 8 full-attention + 24 Gated DeltaNet, 4 KV heads × 256 dim, `nextn_predict_layers 1` `full_attention_interval 4` ⇒ 8 full-attention + 24 Gated DeltaNet, 4 KV heads × 256 dim, `nextn_predict_layers 1`
(the head **is in the file**), trained window 262,144, `general.sampling.*` 1.0/0.95/20. Only the 8 full layers bill (the head **is in the file**), trained window 262,144, `general.sampling.*` 1.0/0.95/20. Only the 8 full layers bill
per token (32,768 B at exact f16 ⇒ 16,384 MiB over the pool) and the DeltaNet layers add ~50 MiB of recurrent state per token and the DeltaNet layers add ~50 MiB of recurrent state **per slot**, so the shape is a single budget
**per slot**: exact KV costs the third conversation and nothing else. equation at 25,088 B/token (f16 K 16,384 + q8_0 V 8,704): **14,406.00 MiB** of cache over a 602,112 pool +
**2,352.00 MiB** of draft cache (blk.32 at its own f16 rate, billed over the WHOLE pool) + ~7.7 GiB of weights,
projector, buffers and states, inside the 24,123 MiB the driver leaves addressable. Exact V (32,768 B/token ⇒
18,816 MiB here) buys neither the third conversation nor the drafter; the fully quantized pair (17,408 B/token)
buys both and costs the keys their precision.
**MTP is OFF by measurement, not by absence of a head.** The drafter is *good* — 2026-10-18 measured **MTP is ON (Frank, 2026-10-19).** The drafter was always good — 2026-10-18 measured acceptance 0.798 / 0.617 per
acceptance 0.798 / 0.617 per position, mean accepted length 2.41, and the publisher's card says depth 2 works position, mean accepted length 2.41, and the publisher's card says depth 2 works best — and arming it is a
best — and it still loses here, because the draft context is built against the *target's* pool: `blk.32` bills budget decision, not a flag: the draft context is built against the *target's* pool, so `blk.32` bills 4,096
4,096 B/position at its default f16 ⇒ 2,048 MiB at 524,288 positions on a trunk that projects 21,234 MiB of the B/position (2,352.00 MiB at 602,112) plus 480.97 MiB of its own CUDA0 buffers, and it triples the recurrent
24,123 MiB addressable (that projection predicts 2,889 MiB free; replace it with the measured pair). Exact KV and a state (452.25 MiB for three cells against 150.75 with the drafter off). `CACHE_TYPE_V=q8_0` is what pays that:
drafter cannot share this card. `--spec-draft-type-k`/`--spec-draft-type-v` (default f16, verified in this build's 7,680 B/token cheaper than exact V = **4,410.00 MiB** at this pool. `UBATCH_SIZE=1024` — the rung every measured
`--help`) would cut the draft to 1,088 MiB — legal on b10777, **never booted beside exact KV**. There is no MTP arm here ran on — contributes only ~406 MiB at THIS shape, not the ~1,306 the 786,432-pool census showed,
`--spec-draft-model`: the MTP draft rides the main model's weights. The env carries the recipe and `UBATCH_SIZE=2048` beside an armed drafter is still the shape that halved decode (167.2 → 109.2 t/s).
(`SPEC_DRAFT_N_MAX=2`, `SPEC_DRAFT_N_MIN=0`, `SPEC_DRAFT_P_MIN=0`); `SPEC_TYPE=none` never sends it. `--spec-draft-type-k`/`--spec-draft-type-v` (default f16, verified in this build's `--help`) would cut the draft's bill again and stay at their default; there is no
`--spec-draft-model` — the draft rides the main model's weights, so a draft file would load the trunk twice.
`SPEC_TYPE=none` is the rollback and strips the whole set.
**Harness contract** (`~/.omp/agent/models.yml`, provider `llamacpp (dankclaw-3090)`, id `frank-zealot`): **Harness contract** (`~/.omp/agent/models.yml`, provider `llamacpp (dankclaw-3090)`, id `frank-zealot`):
`contextWindow: 262144` — **the per-slot window**, because llama.cpp divides `--ctx-size` across slots and answers a `contextWindow: 200704` — **the per-slot window**, because llama.cpp divides `--ctx-size` across slots and answers a
longer prompt with a 400, never a truncation — plus `maxTokens: 16384`, `input: [text, image]`, longer prompt with a 400, never a truncation — plus `maxTokens: 16384`, `input: [text, image]`,
`compat.reasoningContentField: reasoning_content`, `compat.reasoningEffortMap` (**load-bearing**: the template takes `compat.reasoningContentField: reasoning_content`, `compat.reasoningEffortMap` (**load-bearing**: the template takes
only `xhigh|medium|low` and raises on anything else), `extraBody` 1.0 / 0.95 / 20 / `min_p 0`, and the only `xhigh|medium|low` and raises on anything else), `extraBody` 1.0 / 0.95 / 20 / `min_p 0`, and the
`compactionModel` of [[frank-archon]], [[frank-dragoon]] **and** frank-zealot all pointing here. `compactionModel` of [[frank-archon]], [[frank-dragoon]] **and** frank-zealot all pointing here.
**The exact-KV boot, measured** (2026-10-18 evening, under the unit, verbosity 4). KV **16,384.00 MiB** = **No rates, by law.** Frank ordered the reslice, not a number: the evidence a swap needs is
32,768 B/token × 524,288 positions, to the MiB — rate derivation and ledger agree with no slop, which is what the seam's fit gate, the deployer's health gate, and the boot ledger below. The previous era's
buries the doc's old "f16 needs 26,112 MiB and cannot boot" figure for good: that was the 27B's 34,816 B/token exact-KV boot and its rate floors live in that era's row; those floors were measured while
applied to the 9B. RS **100.50 MiB** (2 cells, one 50.25 MiB cell lighter than the q8_0 era), compute another consumer drove the other slot (56 prompts in three minutes, 96 % GPU-util), the exact
1,408.06 + 1,088.07 CUDA_Host, mmproj worst case **868.32 MiB** charged *after* the engine's own fit check, condition a bench window exists to exclude.
settled `nvidia-smi` **21,176 used / 2,949 free** of 24,123, `n_slots = 2`, `n_ctx_slot = 262144`,
`DropInPaths` empty, no clamp line, no `cudaMalloc` error — within 24 MiB of the pre-flight projection. That is
a desktop margin: the ~2 GiB of slack is what an image storm has to live inside.
**Rates on this shape are FLOORS, not the shape's rates** — measured the same evening greedy with `ignore_eos` **The 2026-10-19 boot, measured** — verbosity-4 ledger of `ba69ea5`, started 03:21:56 PDT.
at 512 tokens, first request discarded, while another consumer drove the other slot (56 new prompts in three Recorded here because projections drift, and nowhere else:
minutes, 96 % GPU-util): the exact condition the bench window exists to exclude, and one contended deep arm
even came back truncated (a 512 KB body accepted as `prompt_n = 4`).
| Arm | q8_0 3-slot record | f16 2-slot, contended | | Term | Ledger |
|---|---|---| |---|---|
| single-stream decode | 168.7 | **134.1 – 141.5** (145.8 and 163.2 in a lighter minute) | | Target KV over the 602,112 pool | **14,406.00 MiB** — K (f16) 9,408.00 + V (q8_0) 4,998.00, `200704 cells, 8 layers, 3/3 seqs` |
| concurrent aggregate | ≈300 across 3 (102.8 each) | **256.6 / 262.4 / 265.1** across 2 (≈128–133 each) | | Draft KV (`blk.32`, its own f16 context) | **2,352.00 MiB** — 1,176.00 + 1,176.00, `200704 cells, 1 layers, 3/3 seqs`: the whole pool, again |
| prefill at ~106k | 2,877 at 106,418 | **3,159.5 at 106,282** | | Recurrent state | **452.25 MiB** — `3 cells, 32 layers, 3 seqs, 2 rs_seq`, R (f32) 20.25 + S (f32) 432.00 |
| decode at 106k depth | never measured | **71.5** | | Compute buffers | target 1,665.00 CUDA0 + 424.88 CUDA_Host; draft context 480.97 + 424.86 |
| Weights | 2,283.29 CUDA0 + 257.66 CPU-mapped |
| Projector | 595.29 model + 248.10 compute + 24.93 CPU (its 868.32 worst case is still charged AFTER the engine's fit check) |
| Engine's own verdict | projected 21,639 vs 23,404 free, then **"cannot meet free memory target of 1,867 MiB, need to reduce device memory by 102 MiB"** |
| Settled | **22,870 used / 1,255 free**, `n_slots = 3`, `n_ctx_slot = 200704`, `kv_unified = false` |
What the floors already support: per-conversation concurrency **improved** (two streams hold ~130 t/s each where This is the first boot on this card that does **not** meet llama.cpp's own free-memory target,
three held 102.8), endpoint aggregate sits ~12 % under the 3-slot record, prefill beat the record while and 1,255 MiB settled sits under the ~2 GiB band every previous era held. It boots, `/health`
contended, and single-stream decode is between parity and −20 %. The 1.882× KV byte bill is real; its true size answers, and the seam's gate still passes — it priced 22,428 MiB, conservative by 442 against
on this card is still unmeasured, so quoting 134 or 163 t/s as "the" number is a guess in either direction — this ledger. The cheapest rungs back into the band are `CTX_SIZE=401408` (two conversations) or
re-run the same arms with the box quiet before a figure is carried anywhere else. `SPEC_TYPE=none`; both are one line and neither changes the payload.
**The drafter is engaged, measured on the same boot** (three ordinary requests, not a bench):
acceptance 0.706 and 0.763 accepted-per-generated, mean accepted length 2.41 / 2.53,
per-position rates (0.882, 0.765) and (0.871, 0.699) — depth 2 earning its keep at 0.3–1.5 MiB
of draft state per saved prompt. The three probes that prove a swap: chat → `"zealot alive"`
with 448 completion tokens (at `max_tokens: 64` the same request returns EMPTY content with
`finish_reason: length` — the thinking-budget trap, not a fault); tools →
`finish_reason: tool_calls` with `{"a":24,"b":7}`; vision → a 512 px solid-red PNG answers
"Red" at `prompt_tokens` 280.
**Fallback rungs**, cheapest first — one line of `frank-zealot.env` each, and each must keep the seam's fit gate green: **Fallback rungs**, cheapest first — one line of `frank-zealot.env` each, and each must keep the seam's fit gate green:
| Knob | Effect | | Knob | Effect from the shipped shape |
|---|---| |---|---|
| `UBATCH_SIZE=1024` | hands back 1,306 MiB of compute buffers; prefill −17 %; it is also the arm that ran the drafter at three windows (213.5 t/s) | | `SPEC_TYPE=none` | the drafter off: hands back its 2,352.00 MiB of cache, 480.97 MiB of CUDA0 buffers and the 301.50 MiB of rollback planes it adds to the recurrent state (~3.1 GiB), at the cost of the ~+28 % decode the 2026-10-18 census measured whenever the card has room |
| `CACHE_TYPE_V=q8_0` | exact K + quantized V: KV 16,384 → **12,544 MiB**, freeing 3,840 (llama.cpp has no true fp8 KV dtype — q8_0 is the 8-bit class) | | `UBATCH_SIZE=2048` | +4.6 % prefill and only ~406 MiB of compute buffers at THIS shape (the ~1,306 MiB the 786,432-pool census showed does not transfer) — and the rung that HALVED decode beside an armed drafter (167.2 → 109.2 t/s) |
| `CTX_SIZE=401408` | two 200,704 conversations, exact KV at 12,544 MiB (same 3,840 freed) — `contextWindow` MUST follow the per-slot number | | `CTX_SIZE=401408` | two 200,704 conversations: frees 4,802 MiB of cache — `contextWindow` MUST follow the per-slot number |
| `CTX_SIZE=786432 PARALLEL=3 CACHE_TYPE_K/V=q8_0` | the era below this one: 13,056 MiB of cache, the third conversation back | | `CACHE_TYPE_V=f16` | exact V: +4,410 MiB at this pool with the drafter armed, which is a `cudaMalloc` death the seam refuses — legal only once the drafter or a slot is gone |
| `CACHE_TYPE_K/V=q4_0` | 10,240 B/token ⇒ 5,120 MiB (the 27B era measured q4_0 at 16,392 used / 7,733 free at 2 × 200,704) | | `CACHE_TYPE_K=q8_0` | the fully quantized pair (17,408 B/token): frees 4,410 MiB and moves the quantization error into the keys |
## Eras ## Eras
| Window | Payload | Runtime | Shape | Replaced because | Rollback | | Window | Payload | Runtime | Shape | Replaced because | Rollback |
|---|---|---|---|---|---| |---|---|---|---|---|---|
| 2026-10-18 21:06 → 2026-10-19 | the same 9B MTP file and projector | PrismML fork 10777 | 2 × 262,144 (pool 524,288), exact f16 K+V = 16,384.00 MiB, `SPEC_TYPE=none`, batch 4096 / ubatch 2048; boot ledger KV 16,384.00 / RS 100.50 / compute 1,408.06 + 1,088.07 CUDA_Host / mmproj worst case 868.32 charged after the fit check, settled **21,176 used / 2,949 free**, `n_ctx_slot = 262144`, within 24 MiB of the projection; floors 134.1–141.5 t/s single-stream, 256.6–265.1 aggregate over two, 3,159.5 t/s prefill at 106,282, 71.5 t/s decode at 106k depth (contended — floors, not the shape's rates) | 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 | 3 × 262,144 (pool 786,432), q8_0 K+V = 13,056 MiB, settled 17,956 / 6,169 MiB; 168.7 t/s decode (three runs 168.4–168.7), ~300 t/s over three streams (102.8 each), 2,877 t/s prefill at 106,418 | 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 | | 2026-10-18 13:19 → same evening (the boot `68afc6d` replaced) | the same 9B MTP file and projector | PrismML fork 10777 | 3 × 262,144 (pool 786,432), q8_0 K+V = 13,056 MiB, settled 17,956 / 6,169 MiB; 168.7 t/s decode (three runs 168.4–168.7), ~300 t/s over three streams (102.8 each), 2,877 t/s prefill at 106,418 | 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 |
| 2026-10-18 03:14 → 13:19 (arm under the unit, then the leftover drop-in) | `Bonsai2-PTQ1_0/Ternary-Bonsai-2-27B-PTQ1_0.gguf` 5,946,648,928 B + hardlinked Q8 mmproj | PrismML fork 10777 | arm: 2 × 200,704, batch/ubatch 3072/1536. The leftover `frank-zealot.service.d/ptq1.conf` then made the 13:16 cutover deploy boot THIS payload at 3 × 133,888 (`new slot, n_ctx = 133888` x3, journal read 2026-10-19) until the 13:19 boot | Frank scratched the 27B plan — ~48 t/s decode vs 76.6 for PQ2_0 on the same weights | `/home/dank/models/Bonsai2-PTQ1_0/` + `scripts/download-frank-zealot-bonsai2-ptq1.sh` | | 2026-10-18 03:14 → 13:19 (arm under the unit, then the leftover drop-in) | `Bonsai2-PTQ1_0/Ternary-Bonsai-2-27B-PTQ1_0.gguf` 5,946,648,928 B + hardlinked Q8 mmproj | PrismML fork 10777 | arm: 2 × 200,704, batch/ubatch 3072/1536. The leftover `frank-zealot.service.d/ptq1.conf` then made the 13:16 cutover deploy boot THIS payload at 3 × 133,888 (`new slot, n_ctx = 133888` x3, journal read 2026-10-19) until the 13:19 boot | Frank scratched the 27B plan — ~48 t/s decode vs 76.6 for PQ2_0 on the same weights | `/home/dank/models/Bonsai2-PTQ1_0/` + `scripts/download-frank-zealot-bonsai2-ptq1.sh` |
| 2026-10-17 → 10-18 | Ternary-Bonsai-2-**27B** PQ2_0 7,206,168,928 B `3907dc16…` + Q8_0 mmproj 629,246,976 B | PrismML fork **10754** (`~/GitPackages/llama-prism/`) | 2 × 200,704 (pool 401,408), q8_0 K+V = 13,328 MiB, settled 22,664 / 1,461; 76.6 t/s decode, 109.4 aggregate over two streams, 691.7 t/s prefill at 166,027; MTP impossible — no head in the file | the 9B bills half the KV per token (8 full layers, not 16), which funded three slots at the native window and beat it on every rate | `/home/dank/models/Bonsai2-PQ2_0/` + the 10754 fork tree, `scripts/download-frank-zealot-bonsai2.sh`, revert the swap commit | | 2026-10-17 → 10-18 | Ternary-Bonsai-2-**27B** PQ2_0 7,206,168,928 B `3907dc16…` + Q8_0 mmproj 629,246,976 B | PrismML fork **10754** (`~/GitPackages/llama-prism/`) | 2 × 200,704 (pool 401,408), q8_0 K+V = 13,328 MiB, settled 22,664 / 1,461; 76.6 t/s decode, 109.4 aggregate over two streams, 691.7 t/s prefill at 166,027; MTP impossible — no head in the file | the 9B bills half the KV per token (8 full layers, not 16), which funded three slots at the native window and beat it on every rate | `/home/dank/models/Bonsai2-PQ2_0/` + the 10754 fork tree, `scripts/download-frank-zealot-bonsai2.sh`, revert the swap commit |
@@ -139,6 +155,10 @@ re-run the same arms with the box quiet before a figure is carried anywhere else
`tests/frank_zealot_unit.bats`'s fit gate can see a boot that would die at `cudaMalloc`, so every retune keeps it `tests/frank_zealot_unit.bats`'s fit gate can see a boot that would die at `cudaMalloc`, so every retune keeps it
green — and the first verbosity-4 ledger plus settled `nvidia-smi` of a new dtype belongs on this page, because green — and the first verbosity-4 ledger plus settled `nvidia-smi` of a new dtype belongs on this page, because
projections drift. projections drift.
The same trap one knob down: the ubatch 2048 → 1024 step handed back **~1,306 MiB** at the 786,432 / q8_0-pair
arm and **406 MiB** at the 602,112 / f16-K + q8_0-V boot (target compute 2,496.13 → 2,089.88, with CUDA0 going
UP 257 while CUDA_Host went down 663). Changing the dtype or the pool moves the compute buffers too, so a
credit measured at one shape is not a budget line at the next.
The two boots that priced non-KV: 2 × 262,144 **without** the projector — 20,190 used, KV 12,800 ⇒ non-KV The two boots that priced non-KV: 2 × 262,144 **without** the projector — 20,190 used, KV 12,800 ⇒ non-KV
**7,390 MiB**; 3 × 196,608 **with** it — 22,972 used, KV 14,113 ⇒ non-KV **8,859 MiB**. The difference prices the **7,390 MiB**; 3 × 196,608 **with** it — 22,972 used, KV 14,113 ⇒ non-KV **8,859 MiB**. The difference prices the
vision tower at **~1.4 GiB for a 918 MB file**, because the encoder's activation and compute buffers ride with it. vision tower at **~1.4 GiB for a 918 MB file**, because the encoder's activation and compute buffers ride with it.
@@ -163,7 +183,7 @@ re-run the same arms with the box quiet before a figure is carried anywhere else
## Contract ## Contract
- `/health` → `{"status":"ok"}`; `/props` reports `n_ctx` and `presence_penalty`; `/slots` shows occupancy; a third - `/health` → `{"status":"ok"}`; `/props` reports `n_ctx` and `presence_penalty`; `/slots` shows occupancy; a fourth
concurrent conversation **waits** for a slot — the slot count is the concurrency contract. concurrent conversation **waits** for a slot — the slot count is the concurrency contract.
- `/metrics` answers 200 with 15 `llamacpp:*` families **only with `--metrics`**; without it llama.cpp answers **501**, which Prometheus reads as DOWN while `/health` stays 200. The scrape target is `host.containers.internal:7771/metrics` and the oldest retained sample of `up{job="frank-zealot"}` (2026-10-11) is already 0 — this target was never successfully scraped, across every payload era. `METRICS=0` is the documented opt-out and re-breaks it; the counters cost nothing on the request path. - `/metrics` answers 200 with 15 `llamacpp:*` families **only with `--metrics`**; without it llama.cpp answers **501**, which Prometheus reads as DOWN while `/health` stays 200. The scrape target is `host.containers.internal:7771/metrics` and the oldest retained sample of `up{job="frank-zealot"}` (2026-10-11) is already 0 — this target was never successfully scraped, across every payload era. `METRICS=0` is the documented opt-out and re-breaks it; the counters cost nothing on the request path.
- Reasoning arrives in **`reasoning_content`** (`--reasoning-format deepseek`) and **counts against `max_tokens`**: a short budget yields empty `content` with `finish_reason: "length"`, which reads as a broken model, not a mid-thought one. The slot prefix cache is live (`cached_tokens` in `usage`). - Reasoning arrives in **`reasoning_content`** (`--reasoning-format deepseek`) and **counts against `max_tokens`**: a short budget yields empty `content` with `finish_reason: "length"`, which reads as a broken model, not a mid-thought one. The slot prefix cache is live (`cached_tokens` in `usage`).
@@ -171,9 +191,9 @@ re-run the same arms with the box quiet before a figure is carried anywhere else
(1.0 / 0.95 / 20). `presence_penalty` is the one recipe value with no GGUF metadata key, so it must reach argv; (1.0 / 0.95 / 20). `presence_penalty` is the one recipe value with no GGUF metadata key, so it must reach argv;
the serve script uses `${PRESENCE_PENALTY=1.0}`, deliberately **not** `:=`, because `""` is the documented opt-out. the serve script uses `${PRESENCE_PENALTY=1.0}`, deliberately **not** `:=`, because `""` is the documented opt-out.
- Vision: only the OpenAI **`image_url`** content part reaches the model — the legacy top-level `images` field bumps `usage.prompt_tokens` while the model invents an answer. Probe with a **≥ 256 px** solid color (a postage stamp answers "video") and budget **≥ 512** tokens; a 512 px image bills ~280 prompt tokens. - Vision: only the OpenAI **`image_url`** content part reaches the model — the legacy top-level `images` field bumps `usage.prompt_tokens` while the model invents an answer. Probe with a **≥ 256 px** solid color (a postage stamp answers "video") and budget **≥ 512** tokens; a 512 px image bills ~280 prompt tokens.
- **Q8_0 mmproj, not BF16**: the publisher warns the BF16 tower "can crash llama-server when several image requests run at once", which is exactly the `--parallel 2` shape. `MMPROJ_FILE=` (explicitly empty) is the text-only opt-out and returns ~1.4 GiB; a path at a missing file is a boot **refusal**, never silent blindness. - **Q8_0 mmproj, not BF16**: the publisher warns the BF16 tower "can crash llama-server when several image requests run at once", which is exactly the `--parallel 3` shape. `MMPROJ_FILE=` (explicitly empty) is the text-only opt-out and returns ~1.4 GiB; a path at a missing file is a boot **refusal**, never silent blindness.
- Tools: native via `--jinja`; `finish_reason: tool_calls` with valid JSON arguments. - Tools: native via `--jinja`; `finish_reason: tool_calls` with valid JSON arguments.
- Two non-faults: a `400` naming 262,144 is one conversation outgrowing its **slot**, not pool exhaustion; and - Two non-faults: a `400` naming 200,704 is one conversation outgrowing its **slot**, not pool exhaustion; and
the raw `/completion` path is a **dead instrument on this fork build** — a greedy `ignore_eos` run answers the raw `/completion` path is a **dead instrument on this fork build** — a greedy `ignore_eos` run answers
**500 "The model produced output that does not match the expected Content-only format"**, because build 10777 **500 "The model produced output that does not match the expected Content-only format"**, because build 10777
guards the raw path the moment reasoning markup leaks (older builds only logged `failed to parse reasoning guards the raw path the moment reasoning markup leaks (older builds only logged `failed to parse reasoning
-1
View File
@@ -169,7 +169,6 @@ is a locator: one row, one clause, no numbers.
|---|---| |---|---|
| [[Ethernet Bandwidth Math]] | Bytes-per-token math for pipeline versus tensor parallelism, and the link budgets that follow from it. | | [[Ethernet Bandwidth Math]] | Bytes-per-token math for pipeline versus tensor parallelism, and the link budgets that follow from it. |
| [[Expected Performance]] | Estimated split-model throughput over ethernet, with the benchmark anchors the estimates rest on. | | [[Expected Performance]] | Estimated split-model throughput over ethernet, with the benchmark anchors the estimates rest on. |
| [[Multi-Node Runtimes]] | How each two-machine runtime is built and launched, what it can combine with, and the mixed-vendor verdicts. |
| [[Qwen3.8 PP Deployment]] | The real split deployment: which node is head and which is worker, the flags, and the verified gotchas. | | [[Qwen3.8 PP Deployment]] | The real split deployment: which node is head and which is worker, the flags, and the verified gotchas. |
| [[Sample Script Set]] | An educational two-node RPC split script set, for reading only — never run it or deploy it. | | [[Sample Script Set]] | An educational two-node RPC split script set, for reading only — never run it or deploy it. |
+27 -113
View File
@@ -43,9 +43,32 @@
holds `StillDeadcode`, `rad`, `tpl.jinja` — so the MXFP4 checkpoint that two holds `StillDeadcode`, `rad`, `tpl.jinja` — so the MXFP4 checkpoint that two
pages called the "on-disk rollback" is a re-fetch, now stated once, on the era pages called the "on-disk rollback" is a re-fetch, now stated once, on the era
page that owns that payload. page that owns that payload.
- **Fleet state confirmed live, not from files**: frank-archon 3 × 200,000 fp8 KV, - **Fleet state must be probed, and probed again at the end.** The first pass read
frank-dragoon 2 slots × 200,704, frank-zealot 2 × 262,144; all three answer frank-archon 3 × 200,000 fp8 KV, frank-dragoon 2 × 200,704, frank-zealot
`/health` 200 and every env value matches its page. 2 × 262,144 and every env value matched its page. `origin/main` then moved under
the campaign — PR #25/#26 resliced frank-zealot — and the live re-probe on the
same day says **`--ctx-size 602112 --parallel 3` ⇒ 3 × 200,704, f16 K + q8_0 V,
`--spec-type draft-mtp`, `--jinja --reasoning`, `--metrics`**, matching
[[frank-zealot]] exactly. **Law: a documentation pass that reports live shapes is
stale the moment main moves; re-probe `/slots` and the running cmdline before
writing the number, not after.**
- **A census that includes the file under test proves nothing.** `scripts/port-census.py`
takes the new directory as its corpus, so every era page checked against
`serverAppConfigs-docs` was trivially held by *itself*: ten pages reported
`0 unheld tokens`, including `kingjones-q8agent-rocmfpx` at 481 lines whose own load
ledger cannot live anywhere else. Real numbers after the corpus excluded the page under
test: **14–63 sole-record lines per page — no era page in this vault is padding**, each
the only copy of a campaign ledger, which is what *Documentation Policy* protects.
**Law: a tool's own docstring promise ("excluding it") is not a check; assert the file
is absent from the corpus before believing a zero.** Same shape as *Key Lessons* → "A
scout verdict is a lead, never a finding": a mechanical tool is a scout too.
- So "compact the docs" reduces to deleting **restatement**, which is what landed:
`Multi-Node Runtimes.md` (−257, it restated launch material owned by
[[Qwen3.8 PP Deployment]]), 78 lines of transient deploy chatter out of this log (dropped
only where every fact token in the line is held by an owner page, preserved under
`.AGENTTRASH/20261019-log-transient/`), and 148 lines out of `AGENTS.md`. Docs flat,
operator doc down; the growth is the two enforcement tools.
## 2026-10-18 ## 2026-10-18
- **frank-zealot resliced to two windows of exact KV (Frank's call, PR #21).** - **frank-zealot resliced to two windows of exact KV (Frank's call, PR #21).**
@@ -73,7 +96,6 @@
**issue #3 research: AMD PerfOpt retires the reason for `amd_iommu=off`** **issue #3 research: AMD PerfOpt retires the reason for `amd_iommu=off`**
- Ask: does a recent kernel change remove the IOMMU translation penalty - Ask: does a recent kernel change remove the IOMMU translation penalty
that justifies the 2026-09-11 `amd_iommu=off` boot line, i.e. can the that justifies the 2026-09-11 `amd_iommu=off` boot line, i.e. can the
XDNA2 NPU come back? Answered and documented; **no host change** at the XDNA2 NPU come back? Answered and documented; **no host change** at the
@@ -107,11 +129,9 @@
`frank-npu.md` § *Why it is retired* corrected — the "`pt` is not a `frank-npu.md` § *Why it is retired* corrected — the "`pt` is not a
middle ground" law is dead — the AGENTS.md key-lesson bullet re-keyed, and middle ground" law is dead — the AGENTS.md key-lesson bullet re-keyed, and
a 4-step maintenance-window procedure that measures locally instead of a 4-step maintenance-window procedure that measures locally instead of
inheriting the vendor number. Nothing deployed — no service was touched.
**boot line flipped: Dankclaw now boots `iommu=pt`** **boot line flipped: Dankclaw now boots `iommu=pt`**
- Window executed 01:24–01:35 on Frank's go-ahead, with the A/B the - Window executed 01:24–01:35 on Frank's go-ahead, with the A/B the
research demanded. Provenance first: `/etc/default/grub` had never been in research demanded. Provenance first: `/etc/default/grub` had never been in
the repo while the IOMMU line was the most consequential kernel decision the repo while the IOMMU line was the most consequential kernel decision
@@ -119,7 +139,6 @@
byte-exact) and moves only through `scripts/set-dankclaw-bootline.sh` — byte-exact) and moves only through `scripts/set-dankclaw-bootline.sh` —
diff, install 0644 root:root, `update-grub`, **verify the default diff, install 0644 root:root, `update-grub`, **verify the default
`grub.cfg` entry**, reboot on `--reboot` only. The deployer plans nothing `grub.cfg` entry**, reboot on `--reboot` only. The deployer plans nothing
under `dankclaw/host/`: kernel state is not a container restart.
- **Verdict: identity DMA costs nothing on this host.** Byte-identical - **Verdict: identity DMA costs nothing on this host.** Byte-identical
traffic, +2.0 % cold prefill going OFF→PT; the 13–16 % that justified traffic, +2.0 % cold prefill going OFF→PT; the 13–16 % that justified
`amd_iommu=off` for five weeks never existed here. Decode's apparent `amd_iommu=off` for five weeks never existed here. Decode's apparent
@@ -127,7 +146,6 @@
same boot spread 31.8–47.8 t/s. same boot spread 31.8–47.8 t/s.
- **The XDNA2 NPU is back:** `amdxdna` bound `0000:c6:00.1`, - **The XDNA2 NPU is back:** `amdxdna` bound `0000:c6:00.1`,
`/dev/accel/accel0` `crw-rw---- root:render 261,0` with `dank` in `/dev/accel/accel0` `crw-rw---- root:render 261,0` with `dank` in
`render`. No endpoint deployed — revival needs a quadlet with
`AddDevice=/dev/accel/accel0` + `GroupAdd=keep-groups`. `AddDevice=/dev/accel/accel0` + `GroupAdd=keep-groups`.
- Deliberately NOT done: the `pp_dpm_fclk` level-7 pin (it changes iGPU - Deliberately NOT done: the `pp_dpm_fclk` level-7 pin (it changes iGPU
timing for every workload and nothing co-runs on the NPU yet) and any timing for every workload and nothing co-runs on the NPU yet) and any
@@ -136,7 +154,6 @@
- Gates after boot: 0 failed units, 32 IOMMU groups (31 identity + 1 DMA = - Gates after boot: 0 failed units, 32 IOMMU groups (31 identity + 1 DMA =
the Genesys Logic SD reader, a kernel quirk), interrupt remapping on; the Genesys Logic SD reader, a kernel quirk), interrupt remapping on;
frank-dragoon's ledger byte-for-byte the same shape and Gitea / Caddy frank-dragoon's ledger byte-for-byte the same shape and Gitea / Caddy
edge / Grafana answered 200. New tool `scripts/stage-frank-dragoon-eval.sh`
rebuilds the A/B corpus byte-identically — run it through `bash -lc`, `uv` rebuilds the A/B corpus byte-identically — run it through `bash -lc`, `uv`
is in `~/.local/bin`. Docs re-keyed: the decision record is in `~/.local/bin`. Docs re-keyed: the decision record
`dankclaw-iommu-translation-off.md` (new § *execution*), `frank-npu.md` `dankclaw-iommu-translation-off.md` (new § *execution*), `frank-npu.md`
@@ -165,15 +182,11 @@
`SPEC_TYPE=draft-mtp` drop the flag. And `up{job="frank-zealot"}` had read `SPEC_TYPE=draft-mtp` drop the flag. And `up{job="frank-zealot"}` had read
0 for the **entire retained Prometheus history**: llama.cpp answers 0 for the **entire retained Prometheus history**: llama.cpp answers
**501** on `/metrics` unless `--metrics` is passed, so the scrape failed **501** on `/metrics` unless `--metrics` is passed, so the scrape failed
beside a healthy `/health` and `LlmEndpointDown` fired for days.
`METRICS=1` is now the default and the seam pins it. `METRICS=1` is now the default and the seam pins it.
- Deployed 13:16 (`deploy.sh dankclaw --service frank-zealot`) — and the
first boot was **not the shipped payload**: a leftover first boot was **not the shipped payload**: a leftover
`frank-zealot.service.d/ptq1.conf` from the bench window pointed `frank-zealot.service.d/ptq1.conf` from the bench window pointed
`FRANK_ZEALOT_ENV_FILE` at `/tmp/zealot-arm-current.env`, so the unit `FRANK_ZEALOT_ENV_FILE` at `/tmp/zealot-arm-current.env`, so the unit
booted that arm. `/health` 200, `/v1/models` echoed the alias, Playwright
33/33 — **nothing in the gate reads the payload.** Drop-in moved to 33/33 — **nothing in the gate reads the payload.** Drop-in moved to
`~/.AGENTTRASH/`, `daemon-reload`, restart: the shipped trio booted. (The
drop-in was not a rogue — see the PTQ1_0 entry below.) drop-in was not a rogue — see the PTQ1_0 entry below.)
- Measurement law: the first battery reported 117 and 72 t/s because it - Measurement law: the first battery reported 117 and 72 t/s because it
timed natural generations that stopped at 24 tokens — wall-clock aggregate timed natural generations that stopped at 24 tokens — wall-clock aggregate
@@ -212,7 +225,6 @@
**frank-dragoon: the 401,408-position pool goes live; docs re-keyed to the measured boot** **frank-dragoon: the 401,408-position pool goes live; docs re-keyed to the measured boot**
- `./scripts/deploy.sh dankclaw --service frank-dragoon` — e2e 33/33, and - `./scripts/deploy.sh dankclaw --service frank-dragoon` — e2e 33/33, and
the drop-in check the zealot incident bought ran FIRST: `DropInPaths` the drop-in check the zealot incident bought ran FIRST: `DropInPaths`
empty, so the repo env governed. 2026-10-18 — frank-dragoon: the KV pool empty, so the repo env governed. 2026-10-18 — frank-dragoon: the KV pool
@@ -226,7 +238,6 @@
page cache loses. page cache loses.
- Every document quoted **~10.8 GiB**, the ~28 KiB/position arithmetic; the - Every document quoted **~10.8 GiB**, the ~28 KiB/position arithmetic; the
engine reserves **11.0 GiB** and its preflight bills **11.3**. The measured engine reserves **11.0 GiB** and its preflight bills **11.3**. The measured
figure wins, and every "committed but not yet deployed" clause was retired
with the measurement (AGENTS.md, the env lore block, both index pages, the with the measurement (AGENTS.md, the env lore block, both index pages, the
iommu page, the runbook). iommu page, the runbook).
- PTQ1_0 provenance corrected (see the dedicated entry): the arm that - PTQ1_0 provenance corrected (see the dedicated entry): the arm that
@@ -237,7 +248,6 @@
MiB freed, then scratched by Frank's 03:33 order in favour of the 9B. MiB freed, then scratched by Frank's 03:33 order in favour of the 9B.
Nothing is owed on it. Nothing is owed on it.
- Census of the halogen HTTP surface while doing it, because the fleet bench - Census of the halogen HTTP surface while doing it, because the fleet bench
law quoted the wrong engine: frank-dragoon answers `/health`, `/metrics`,
`/v1/models`, `/v1/chat/completions`(POST) and 404s `/ping`, `/props`, `/v1/models`, `/v1/chat/completions`(POST) and 404s `/ping`, `/props`,
`/completion`, `/tokenize`; the chat response carries **no `/completion`, `/tokenize`; the chat response carries **no
`Server-Timing`** header. So the `/completion` `timings` object and the `Server-Timing`** header. So the `/completion` `timings` object and the
@@ -245,15 +255,12 @@
dragoon's. AGENTS.md's Benchmarking Policy re-keyed to say which engine dragoon's. AGENTS.md's Benchmarking Policy re-keyed to say which engine
owns which instrument. owns which instrument.
**frank-tts phase 1: the cloned-voice endpoint, named and tested (not deployed)**
- Voice-over goal, phase 1 (Gitea issue #2). New service **frank-tts** on - Voice-over goal, phase 1 (Gitea issue #2). New service **frank-tts** on
Dankclaw: `dankclaw/voiceservice/` (stdlib-only `app.py` + `Containerfile` Dankclaw: `dankclaw/voiceservice/` (stdlib-only `app.py` + `Containerfile`
+ rootless quadlet), `scripts/frank-tts.env`, `tests/frank_tts_unit.bats`. + rootless quadlet), `scripts/frank-tts.env`, `tests/frank_tts_unit.bats`.
One OpenAI-shaped `POST /v1/audio/speech` surface; `voice=<name>` is One OpenAI-shaped `POST /v1/audio/speech` surface; `voice=<name>` is
honoured only when `/voices/<name>/reference.wav` exists, otherwise a 400 honoured only when `/voices/<name>/reference.wav` exists, otherwise a 400
listing the voices that are — **no silent fallback voice**; oversized listing the voices that are — **no silent fallback voice**; oversized
input is a 414, not a half-narrated sentence; `/health` reports backend +
voice list; no audio ever reaches the log. voice list; no audio ever reaches the log.
- **Naming law applied (Frank's call):** treat it as a first-class model - **Naming law applied (Frank's call):** treat it as a first-class model
endpoint → alias-named `frank-tts` (TTS endpoint, so not `frank-asr`), endpoint → alias-named `frank-tts` (TTS endpoint, so not `frank-asr`),
@@ -303,7 +310,6 @@
only and a bridge container cannot reach a host `127.0.0.1` bind — the only and a bridge container cannot reach a host `127.0.0.1` bind — the
lesson that 502'd hermes — so `host.containers.internal:8880`, which lesson that 502'd hermes — so `host.containers.internal:8880`, which
`scripts/frank-tts.env` documents, is correct only for a routable bind. `scripts/frank-tts.env` documents, is correct only for a routable bind.
The deployed pair shares one quadlet network and the façade addresses the
engine by container name. engine by container name.
- **PR #12** (`feat/open-notebook-podcast` @ `114dd7a`) is **merged** as - **PR #12** (`feat/open-notebook-podcast` @ `114dd7a`) is **merged** as
`1eb8b21` (Frank's OK): open-notebook + SurrealDB quadlets on the private `1eb8b21` (Frank's OK): open-notebook + SurrealDB quadlets on the private
@@ -313,9 +319,6 @@
runnable: nothing listens on 7772 until the voice stack lands, and the runnable: nothing listens on 7772 until the voice stack lands, and the
episode destination stays interim pending Gitea #7. episode destination stays interim pending Gitea #7.
**open-notebook: the podcast generator's config lands (nothing deployed)**
- Goal: **open-notebook** as the fleet's podcast generator on Dankclaw — - Goal: **open-notebook** as the fleet's podcast generator on Dankclaw —
notes and sources in, a multi-speaker episode out, script written by notes and sources in, a multi-speaker episode out, script written by
**frank-zealot**, voices rendered by **frank-tts**, no cloud provider **frank-zealot**, voices rendered by **frank-tts**, no cloud provider
@@ -326,7 +329,6 @@
frank-dragoon's pinning style, because `:latest` moving underneath a frank-dragoon's pinning style, because `:latest` moving underneath a
rootless service is how you get an unbootable pod after an unrelated rootless service is how you get an unbootable pod after an unrelated
`systemctl-daemon-reload`. Both health probes are argument-only `systemctl-daemon-reload`. Both health probes are argument-only
(`curl -fsS …/health`, `/surreal is-ready`), and `is-ready` refuses to
pass without `SURREAL_PASS` in the environment — a credential that must pass without `SURREAL_PASS` in the environment — a credential that must
never ride the command line, so `Exec=` carries flags only. never ride the command line, so `Exec=` carries flags only.
- **Host 5055 was already spoken for** — seerr has held `127.0.0.1:5055` — - **Host 5055 was already spoken for** — seerr has held `127.0.0.1:5055` —
@@ -363,20 +365,16 @@
`podman unshare`. One mount line; one deletion undoes it. `podman unshare`. One mount line; one deletion undoes it.
- **Two UI buttons will fail, and that is cosmetic:** Test Connection does - **Two UI buttons will fail, and that is cosmetic:** Test Connection does
`GET <base>/models` and the TTS probe synthesises voice `alloy`; `GET <base>/models` and the TTS probe synthesises voice `alloy`;
frank-tts serves `/health` and `/v1/audio/speech` only. Model rows are
created *by name* and the voice field is free text, so `narrator1/2/3` is created *by name* and the voice field is free text, so `narrator1/2/3` is
typed in and never discovered. Documented, not worked around. typed in and never discovered. Documented, not worked around.
- Three deployer seams cost time and are now law-shaped: a service must be - Three deployer seams cost time and are now law-shaped: a service must be
registered in **five** places — `SERVICE_ORDER`, `DANKCLAW_SERVICES`, the registered in **five** places — `SERVICE_ORDER`, `DANKCLAW_SERVICES`, the
`deploy-remote.sh` dispatch case, its pre-deploy requirement, and
`deploy.sh`'s dispatch regex, where an omission silently plans an empty `deploy.sh`'s dispatch regex, where an omission silently plans an empty
set for `--only`; a `dankclaw/<svc>/*` case must precede the set for `--only`; a `dankclaw/<svc>/*` case must precede the
`dankclaw/*.container` glob because that `*` crosses `/`; and the `dankclaw/*.container` glob because that `*` crosses `/`; and the
changed-path routing lives *inside* the per-path loop, after the changed-path routing lives *inside* the per-path loop, after the
`scripts/*.env)` case, not at the top of the file. `scripts/*.env)` case, not at the top of the file.
**frank-tts-engine: the arm A engine becomes deployable (nothing deployed)**
- Goal: turn the engine the day's A/B picked into a **repo-owned, - Goal: turn the engine the day's A/B picked into a **repo-owned,
boot-persistent, self-healing container** — `frank-tts-engine`, boot-persistent, self-healing container** — `frank-tts-engine`,
`qwentts.cpp` commit `5916bf7c79dbb4e66fc059cdc507a79a23a28bb4`, resident `qwentts.cpp` commit `5916bf7c79dbb4e66fc059cdc507a79a23a28bb4`, resident
@@ -389,7 +387,6 @@
containers therefore sit on one committed quadlet network, containers therefore sit on one committed quadlet network,
`frank-tts-engine.network` at **10.89.5.0/24** (the next free /24 after `frank-tts-engine.network` at **10.89.5.0/24** (the next free /24 after
open-notebook's 10.89.4.0/24), the façade addresses the engine by open-notebook's 10.89.4.0/24), the façade addresses the engine by
container name, and diagnostics — including the deployer's health gate
(`wait_container_healthy`, there is no URL to dial) — run through (`wait_container_healthy`, there is no URL to dial) — run through
`podman exec`. `podman exec`.
- **The boot warmer is a unit hook, not an operator habit.** The registry is - **The boot warmer is a unit hook, not an operator habit.** The registry is
@@ -397,7 +394,6 @@
reference, so `ExecStartPost=-/usr/bin/podman exec frank-tts-engine reference, so `ExecStartPost=-/usr/bin/podman exec frank-tts-engine
/app/warm-voices.sh` with `TimeoutStartSec=900`; the `-` prefix is what /app/warm-voices.sh` with `TimeoutStartSec=900`; the `-` prefix is what
stops a failed warm from crash-looping the endpoint under stops a failed warm from crash-looping the endpoint under
`Restart=always`, and it logs `cold=` and `warm=` per voice on every boot.
- **Thread law recorded before anyone tunes it away:** no `--threads` - **Thread law recorded before anyone tunes it away:** no `--threads`
exists; `src/backend.h` uses `hardware_concurrency()/2`, so a cpuset pin exists; `src/backend.h` uses `hardware_concurrency()/2`, so a cpuset pin
silently changes the thread count. Nothing pins this container. silently changes the thread count. Nothing pins this container.
@@ -411,7 +407,6 @@
**frank-tts: the 7772 endpoint gets its deploy wiring, and voice naming is written down** **frank-tts: the 7772 endpoint gets its deploy wiring, and voice naming is written down**
- Stage 2 of the voice stack: the **façade** in front of the engine is now a - Stage 2 of the voice stack: the **façade** in front of the engine is now a
deployable service too. Runbook: [[Frank TTS]]. deployable service too. Runbook: [[Frank TTS]].
- **Two attached networks, in a meaningful order.** `dankclaw_default` - **Two attached networks, in a meaningful order.** `dankclaw_default`
@@ -440,14 +435,12 @@
- **No Prometheus job, on purpose.** The façade exports no `/metrics`, - **No Prometheus job, on purpose.** The façade exports no `/metrics`,
Prometheus 3 refuses HTML/JSON content types (the `comfyui` and `services` Prometheus 3 refuses HTML/JSON content types (the `comfyui` and `services`
probes are documented down for exactly that), and `LlmEndpointDown` is probes are documented down for exactly that), and `LlmEndpointDown` is
scoped `up{job=~"frank-.*"}` — so a `/health` scrape would page on Signal
every few minutes for a healthy endpoint. That is the frank-archon every few minutes for a healthy endpoint. That is the frank-archon
never-green lesson pointed the other way, written in the page instead of never-green lesson pointed the other way, written in the page instead of
quietly ignored. quietly ignored.
**voice-pack builder: the 10-second source-material gate becomes code** **voice-pack builder: the 10-second source-material gate becomes code**
- Third slice of the voice stack: `scripts/build-voice-pack.sh` turns a - Third slice of the voice stack: `scripts/build-voice-pack.sh` turns a
source recording plus a crop offset into a deployable pack — source recording plus a crop offset into a deployable pack —
`reference.wav` (24 kHz mono `pcm_s16le`, 18–25 s), the engine's own `reference.wav` (24 kHz mono `pcm_s16le`, 18–25 s), the engine's own
@@ -499,18 +492,14 @@
skips `podman build` on a matching `cfhash` label, and that hash covered skips `podman build` on a matching `cfhash` label, and that hash covered
the `Containerfile` alone — so editing `app.py` or `tts-engine-warm.sh` the `Containerfile` alone — so editing `app.py` or `tts-engine-warm.sh`
(both `COPY`'d) skipped the build while the deploy reported success and (both `COPY`'d) skipped the build while the deploy reported success and
`/health` answered with old code. Each case arm now hashes the files its
`Containerfile` copies. `Containerfile` copies.
- Docs carry the lessons where an operator will hit them: a **Locally built - Docs carry the lessons where an operator will hit them: a **Locally built
images** section in [[deployment]], two new troubleshooting lines in images** section in [[deployment]], two new troubleshooting lines in
[[Frank TTS Engine]] (all-voices-cold = quoted `WARM_TEXT`; façade 502 [[Frank TTS Engine]] (all-voices-cold = quoted `WARM_TEXT`; façade 502
beside 200 = model loaded ≠ voices registered), and `Voice Packs.md` beside 200 = model loaded ≠ voices registered), and `Voice Packs.md`
step 4 corrected — the warm hook re-registers **every** pack on every run, step 4 corrected — the warm hook re-registers **every** pack on every run,
because the engine replaces a name it already holds, so a restart is what
an engine bump needs and not what a rebuilt pack needs. an engine bump needs and not what a rebuilt pack needs.
**frank-tts-engine deployed: it builds, it serves, and the warm hook died on the kernel's argument cap**
- Merged as **#14** (`747c665`) and deployed `--service frank-tts-engine`; - Merged as **#14** (`747c665`) and deployed `--service frank-tts-engine`;
the image built clean — the first real proof of the `useradd` fix — and the the image built clean — the first real proof of the `useradd` fix — and the
unit was `healthy` after 31 s. unit was `healthy` after 31 s.
@@ -520,7 +509,6 @@
a single argument at 131,072 bytes (`MAX_ARG_STRLEN`) and a shipped ~955 KB a single argument at 131,072 bytes (`MAX_ARG_STRLEN`) and a shipped ~955 KB
`reference.wav` base64's past that, so curl never executed and the hook `reference.wav` base64's past that, so curl never executed and the hook
logged that as "HTTP no response". Fourth silent fault of the voice stack, logged that as "HTTP no response". Fourth silent fault of the voice stack,
same species as the other three: `/health` stayed green throughout. Fix:
the body goes to a `mktemp` file and curl gets `@file`; the regression test the body goes to a `mktemp` file and curl gets `@file`; the regression test
is the honest shape — real listener, real `curl`, a 300 KB fixture pack — is the honest shape — real listener, real `curl`, a 300 KB fixture pack —
so it fails with the same kernel error the host produced. Every shipped so it fails with the same kernel error the host produced. Every shipped
@@ -546,7 +534,6 @@
- Merged **#15** (`0315a62`, the warmer's argument-cap fix) and **#16** - Merged **#15** (`0315a62`, the warmer's argument-cap fix) and **#16**
(`d2ce669`, the comment sweep), then deployed both voice services in order (`d2ce669`, the comment sweep), then deployed both voice services in order
(`--service frank-tts-engine`, then `--service frank-tts`); Playwright gate (`--service frank-tts-engine`, then `--service frank-tts`); Playwright gate
33/33 twice. Deployed-state proof from a LAN client: `/health` →
`omni-http` with all three voices, and the same sentence in `narrator1` vs `omni-http` with all three voices, and the same sentence in `narrator1` vs
`narrator2` came back with different sha256 and different RMS — the name `narrator2` came back with different sha256 and different RMS — the name
actually selects a voice. actually selects a voice.
@@ -558,7 +545,6 @@
errors instead of degrading; and `models.yml` loads at process start while errors instead of degrading; and `models.yml` loads at process start while
`config.yml` is read live, so a session older than the `frank-tts` row `config.yml` is read live, so a session older than the `frank-tts` row
fails exactly like a dead endpoint. fails exactly like a dead endpoint.
- Corrected the same evening, by breaking it in a restarted session: a
harness call that names no voice reaches 7772 with **no `voice` key at harness call that names no voice reaches 7772 with **no `voice` key at
all** — `400 unknown voice None; available voices: …` — because no setting all** — `400 unknown voice None; available voices: …` — because no setting
feeds one. The endpoint therefore grew `DEFAULT_VOICE` (its own PR): a feeds one. The endpoint therefore grew `DEFAULT_VOICE` (its own PR): a
@@ -609,7 +595,6 @@
- Also in the window: **PR #22 merged** (`0c7fe8b`, `dank/command-center`) so - Also in the window: **PR #22 merged** (`0c7fe8b`, `dank/command-center`) so
`agent/extensions/dankvoice.ts` landed on the auto-load path — proven in a `agent/extensions/dankvoice.ts` landed on the auto-load path — proven in a
plain `omp` session with no flags: `/say merged build check` → WAV to the plain `omp` session with no flags: `/say merged build check` → WAV to the
player, temp dir cleaned. Sessions open before the merge need a restart to
pick the command up. pick the command up.
**frank-zealot: 2 × 262,144 over unquantized f16 KV** **frank-zealot: 2 × 262,144 over unquantized f16 KV**
@@ -632,7 +617,6 @@
## 2026-10-17 ## 2026-10-17
**frank-zealot: Bonsai-2-27B PQ2_0 trial + cutover (2 × 200,704, q8_0 KV)** **frank-zealot: Bonsai-2-27B PQ2_0 trial + cutover (2 × 200,704, q8_0 KV)**
- 2026-10-17 — frank-zealot: **Ternary-Bonsai-2-27B (PQ2_0) cutover**, two - 2026-10-17 — frank-zealot: **Ternary-Bonsai-2-27B (PQ2_0) cutover**, two
slots at 200,704 over q8_0 K + q8_0 V on the RTX 3090, fetched by slots at 200,704 over q8_0 K + q8_0 V on the RTX 3090, fetched by
`scripts/download-frank-zealot-bonsai2.sh` (exact bytes + sha256, `scripts/download-frank-zealot-bonsai2.sh` (exact bytes + sha256,
@@ -666,12 +650,10 @@
`serve-frank-zealot.sh` still DEFAULTED to the retired payload's paths `serve-frank-zealot.sh` still DEFAULTED to the retired payload's paths
(a defaulted run would have served the old model — now re-keyed to the (a defaulted run would have served the old model — now re-keyed to the
shipped trio), and the OpenWebUI `model` row's `meta.description` is live shipped trio), and the OpenWebUI `model` row's `meta.description` is live
DB state (re-keyed with a DB backup; no `open-webui` restart forced —
forcing it logs every family session out for one cosmetic string). forcing it logs every family session out for one cosmetic string).
**dankserver disk purge: Swift w4dn8 rollback + orphan caches (docs re-key)** **dankserver disk purge: Swift w4dn8 rollback + orphan caches (docs re-key)**
- Ask: "dump the swift rollback and the GLM GGUF — make sure the model - Ask: "dump the swift rollback and the GLM GGUF — make sure the model
caches and agenttrash are empty as well." Preflight first: `lsof` empty caches and agenttrash are empty as well." Preflight first: `lsof` empty
on both payloads, the serving trio names only the live path, and a podman on both payloads, the serving trio names only the live path, and a podman
@@ -683,7 +665,6 @@
rollback is now a **re-download, not a local file**. Corpora (grams, rollback is now a **re-download, not a local file**. Corpora (grams,
kld-ref, calibration JSONL) deliberately KEPT: they are the re-convert kld-ref, calibration JSONL) deliberately KEPT: they are the re-convert
and KLD-gate inputs. and KLD-gate inputs.
- Post-checks: frank-archon active and `/health` ok, serving payload
byte-size intact, the live kvcache tier untouched, no process references byte-size intact, the live kvcache tier untouched, no process references
a deleted path. a deleted path.
- Docs re-keyed: AGENTS.md fleet row + frank-archon section + both runbook - Docs re-keyed: AGENTS.md fleet row + frank-archon section + both runbook
@@ -695,7 +676,6 @@
**frank-dragoon cutover: native v2 abliterated payload, card's memory-saving pool** **frank-dragoon cutover: native v2 abliterated payload, card's memory-saving pool**
- 2026-10-17 — frank-dragoon: cutover to the native v2 abliterated halogen - 2026-10-17 — frank-dragoon: cutover to the native v2 abliterated halogen
payload with the card's memory-saving KV pool; census first: the n-gram payload with the card's memory-saving KV pool; census first: the n-gram
table was ALREADY disk-backed and the only real lever was the KV pool. table was ALREADY disk-backed and the only real lever was the KV pool.
@@ -727,7 +707,6 @@
**frank-dragoon: ceiling and pool cut to 200,704; crawl reset fix** **frank-dragoon: ceiling and pool cut to 200,704; crawl reset fix**
- 2026-10-17 — frank-dragoon: context ceiling and KV pool cut to 200,704 - 2026-10-17 — frank-dragoon: context ceiling and KV pool cut to 200,704
(49 × 4096, the per-slot rung frank-zealot runs; nothing in the fleet (49 × 4096, the per-slot rung frank-zealot runs; nothing in the fleet
needed the bigger window). See [[frank-dragoon]]. needed the bigger window). See [[frank-dragoon]].
@@ -741,7 +720,6 @@
- Probes on the live endpoint: over the ceiling → `400 … tokens over` in - Probes on the live endpoint: over the ceiling → `400 … tokens over` in
0.86 s — preflight rejects before any prefill, so probing costs the 0.86 s — preflight rejects before any prefill, so probing costs the
endpoint nothing. endpoint nothing.
- Deploy verdict: the endpoint dispatched healthy (`/health` 200), but the
`[3/3]` Playwright gate failed on `12-crawl` and the deployer correctly `[3/3]` Playwright gate failed on `12-crawl` and the deployer correctly
said **DEPLOY INCOMPLETE**. Root cause was NOT the endpoint — it was Plex said **DEPLOY INCOMPLETE**. Root cause was NOT the endpoint — it was Plex
Web, signed out in the `qa-e2e` profile, running its own sign-in flow, Web, signed out in the `qa-e2e` profile, running its own sign-in flow,
@@ -760,7 +738,6 @@
## 2026-10-15 ## 2026-10-15
**frank-archon: publisher iq4r payload swap (MODEL_FILE-only)** **frank-archon: publisher iq4r payload swap (MODEL_FILE-only)**
- 2026-10-15 — frank-archon: publisher iq4r payload swap, `MODEL_FILE`-only - 2026-10-15 — frank-archon: publisher iq4r payload swap, `MODEL_FILE`-only
— `StillDeadcode/qwen3.8-next-flash-fp8-iq4r-moe`, the engine author's — `StillDeadcode/qwen3.8-next-flash-fp8-iq4r-moe`, the engine author's
published rad-convert of the OFFICIAL Qwen3.8-Flash-Next, **no other published rad-convert of the OFFICIAL Qwen3.8-Flash-Next, **no other
@@ -773,19 +750,15 @@
- Fetch: new `scripts/download-frank-archon-iq4r.sh` (hfget, exact bytes + - Fetch: new `scripts/download-frank-archon-iq4r.sh` (hfget, exact bytes +
Hub LFS sha256, `--verify-only` default) as a transient unit. The cutover Hub LFS sha256, `--verify-only` default) as a transient unit. The cutover
**self-severed by design** — a detached `deploy.sh` survived the session **self-severed by design** — a detached `deploy.sh` survived the session
death when frank-archon restarted; engine healthy in ~100 s, and the
prefix tier RECOVERED across the swap (arch+tokenizer unchanged). prefix tier RECOVERED across the swap (arch+tokenizer unchanged).
- e2e gate went red on three dankclaw specs — NOT the swap: dankclaw - e2e gate went red on three dankclaw specs — NOT the swap: dankclaw
rebooted 01:41 and the gluetun boot-race dependency-failed `searxng`, rebooted 01:41 and the gluetun boot-race dependency-failed `searxng`,
`marreta`, and `marreta-browser` (jobs consumed while gluetun's start job `marreta`, and `marreta-browser` (jobs consumed while gluetun's start job
retried; `Restart=always` never armed because the services never ran).
Cascade restarted; skill codified
(`gluetun-dependent-boot-race-recovery`). frank-archon's own health gate (`gluetun-dependent-boot-race-recovery`). frank-archon's own health gate
passed every deploy. passed every deploy.
**authentik: two more human accounts (Dankclaw)** **authentik: two more human accounts (Dankclaw)**
- **Live — mitch (pk 22) and eric (pk 23) provisioned via `ak shell` ORM, - **Live — mitch (pk 22) and eric (pk 23) provisioned via `ak shell` ORM,
no repo objects**, each `<user>@franksdank.cloud` in the human-user no repo objects**, each `<user>@franksdank.cloud` in the human-user
template shape (password via `set_password` on stdin only — never argv, template shape (password via `set_password` on stdin only — never argv,
@@ -799,8 +772,6 @@
power failure on dankserver reset `/sys/module/ttm/parameters/pages_limit` power failure on dankserver reset `/sys/module/ttm/parameters/pages_limit`
from the manual 200 GiB raise back to its default (half of RAM = 125,988 MiB), from the manual 200 GiB raise back to its default (half of RAM = 125,988 MiB),
below the ~147 GiB pinned-RAM ledger the engine needs; the serve script's below the ~147 GiB pinned-RAM ledger the engine needs; the serve script's
preflight named the refusal and exited 1, and `Restart=always` re-ran it
**6,919 times in ~20 h** — `activating (auto-restart)`, exit 1 in 50 ms, no
container, no engine log. Law adopted: **a sysfs raise is a countdown to the container, no engine log. Law adopted: **a sysfs raise is a countdown to the
next reboot**, so the gate raises the knob itself. New next reboot**, so the gate raises the knob itself. New
`scripts/ttm-preflight.sh` is sourced by the serve script (verify, raise, `scripts/ttm-preflight.sh` is sourced by the serve script (verify, raise,
@@ -873,7 +844,6 @@
(4,379 t/s), so the cliff is real and sits at ~26 GiB. The 2-slot arm that (4,379 t/s), so the cliff is real and sits at ~26 GiB. The 2-slot arm that
would clear it (pool 3,072 + slab 27,897) remains untested — and the would clear it (pool 3,072 + slab 27,897) remains untested — and the
paragraph above is amended: that arm's payoff is +22 % prefill at 2 slots, paragraph above is amended: that arm's payoff is +22 % prefill at 2 slots,
not the +8.9 % the deployed pair ever showed (24,000 → 25,600 = 0 %).
- **Frank's decode challenge, answered with numbers**: 512 tokens, greedy, - **Frank's decode challenge, answered with numbers**: 512 tokens, greedy,
cold — **decode 99.0 t/s single-stream at 43.5k and 93.3 at 197k** (decode cold — **decode 99.0 t/s single-stream at 43.5k and 93.3 at 197k** (decode
is nearly window-free: −6 % for 4.5× depth), while **3 concurrent streams is nearly window-free: −6 % for 4.5× depth), while **3 concurrent streams
@@ -887,10 +857,7 @@
tokens, per-stream + aggregate) so no future shape change can be priced on tokens, per-stream + aggregate) so no future shape change can be priced on
prefill alone. prefill alone.
## 2026-10-11 ## 2026-10-11 — frank-archon: unquantized (bf16) KV at 2 × 200,000
**frank-archon: unquantized (bf16) KV at 2 × 200,000**
- 2026-10-11 — frank-archon: **unquantized (bf16) KV at 2 × 200,000** on - 2026-10-11 — frank-archon: **unquantized (bf16) KV at 2 × 200,000** on
exact attention state, with pool, slab and host pool rebalanced around exact attention state, with pool, slab and host pool rebalanced around
the bigger cache. See [[frank-archon]]. the bigger cache. See [[frank-archon]].
@@ -935,7 +902,6 @@
`frank-archon` active/enabled (container `frank-archon`, :7770), `frank-archon` active/enabled (container `frank-archon`, :7770),
`frank-dragoon` active/enabled (container `frank-dragoon-halogen`, `frank-dragoon` active/enabled (container `frank-dragoon-halogen`,
:7770), `frank-zealot` active/enabled (journal shows 87–93 t/s serving :7770), `frank-zealot` active/enabled (journal shows 87–93 t/s serving
compaction on :7771). `/health` 200 ×3; `/v1/models` reports the three
ids; host checkouts grep zero residue; e2e passed 32+1flaky, 32+1flaky, ids; host checkouts grep zero residue; e2e passed 32+1flaky, 32+1flaky,
33; Prometheus `frank-archon`/`frank-dragoon` up. 33; Prometheus `frank-archon`/`frank-dragoon` up.
- **Pre-existing gap found by the verification, NOT a rename regression:** - **Pre-existing gap found by the verification, NOT a rename regression:**
@@ -944,7 +910,6 @@
it, so `up{job="frank-secondary"}` had NO series at all in the last 24 h it, so `up{job="frank-secondary"}` had NO series at all in the last 24 h
(the zealot metrics + the queue-depth alerts on it have been dead since (the zealot metrics + the queue-depth alerts on it have been dead since
the Qwen3.5 swap). Fix = one flag in `serve-frank-zealot.sh` + a ~1 min the Qwen3.5 swap). Fix = one flag in `serve-frank-zealot.sh` + a ~1 min
zealot restart; not applied in the rename window.
## 2026-10-09 ## 2026-10-09
- **Live — five authentik human accounts provisioned (Dankclaw, `ak shell` - **Live — five authentik human accounts provisioned (Dankclaw, `ak shell`
@@ -980,7 +945,6 @@
`webui.db.bak-20261009`): `model` row (`frank-zealot` → `frank-zealot`, `webui.db.bak-20261009`): `model` row (`frank-zealot` → `frank-zealot`,
params + `meta.description` preserved), the `access_grant` row params + `meta.description` preserved), the `access_grant` row
(`model`/`frank-zealot`/`user`/`*`/`read`), and `openai.api_configs` (`model`/`frank-zealot`/`user`/`*`/`read`), and `openai.api_configs`
`model_ids` on all three backends; `systemctl --user restart
open-webui.service`. Family-account proof, in a real browser as `qa-e2e`: open-webui.service`. Family-account proof, in a real browser as `qa-e2e`:
the welcome screen selects `frank-zealot` and the welcome screen selects `frank-zealot` and
`POST /api/v1/chat/completions {"model":"frank-zealot"}` answers `POST /api/v1/chat/completions {"model":"frank-zealot"}` answers
@@ -991,7 +955,6 @@
- **Live — Hermes re-keyed** (`config.yaml` `frank-dragoon` → `frank-dragoon` - **Live — Hermes re-keyed** (`config.yaml` `frank-dragoon` → `frank-dragoon`
in the provider block, `model.default` and the three `dankclaw-large` in the provider block, `model.default` and the three `dankclaw-large`
auxiliary routes; backup `config.yaml.bak-20261009`; `hermes.service` + auxiliary routes; backup `config.yaml.bak-20261009`; `hermes.service` +
`hermes-webui.service` restarted, gateway log clean, `/health` ok) and the
repo baseline `dankclaw/hermes/config.yaml.example` synced to the live repo baseline `dankclaw/hermes/config.yaml.example` synced to the live
shape (context 262144). **Flagged, not fixed:** Hermes still routes its shape (context 262144). **Flagged, not fixed:** Hermes still routes its
`dankserver-small` auxiliary tasks at the id `frank-medium` on `dankserver-small` auxiliary tasks at the id `frank-medium` on
@@ -1000,13 +963,10 @@
they need a routing decision (archon capacity), not a rename. they need a routing decision (archon capacity), not a rename.
- **Deployer blind spot found by accident:** every - **Deployer blind spot found by accident:** every
`deploy.sh dankserver --service frank-archon` since 2026-10-08 04:09 `deploy.sh dankserver --service frank-archon` since 2026-10-08 04:09
restarted the **staging worktree**, not the deploy checkout — a campaign
drop-in `frank-archon.service.d/bench.conf` overrode `Environment=` and drop-in `frank-archon.service.d/bench.conf` overrode `Environment=` and
`ExecStart`. The health gate went green each time. Verified parity before
removing it (serve script + unit byte-identical, env identical except the removing it (serve script + unit byte-identical, env identical except the
alias, staging HEAD an ancestor of `main`), moved the drop-in to alias, staging HEAD an ancestor of `main`), moved the drop-in to
`/home/dank/.AGENTTRASH/20261009-frank-archon-bench-dropin/`, and `/home/dank/.AGENTTRASH/20261009-frank-archon-bench-dropin/`, and
restarted: `/v1/models` then answered `frank-archon`. Post-deploy
behavioural proof is now part of the runbook. behavioural proof is now part of the runbook.
- Repo: seams re-pinned (`frank_zealot`/`frank_dragoon`/`frank_archon` - Repo: seams re-pinned (`frank_zealot`/`frank_dragoon`/`frank_archon`
unit bats), fleet harnesses retargeted (`bench-probe`, `deep-probe`, unit bats), fleet harnesses retargeted (`bench-probe`, `deep-probe`,
@@ -1126,13 +1086,11 @@
(refusals gone), vision back (the v2 tower loads beside the checkpoint), (refusals gone), vision back (the v2 tower loads beside the checkpoint),
MTP head inside the checkpoint, 2×200,192 slots, `HALOGEN_SPEC_ADAPT=0`. MTP head inside the checkpoint, 2×200,192 slots, `HALOGEN_SPEC_ADAPT=0`.
Runbook: `serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md`. Runbook: `serverAppConfigs-docs/LLM/frank-dragoon-flash-server-v2-swift15.md`.
**Incident (03:09:39–03:11:42):** the deploy started the unit while host
`MemAvailable` was ~74.7 GiB, 3.34 GiB short of the engine's pin-gate `MemAvailable` was ~74.7 GiB, 3.34 GiB short of the engine's pin-gate
requirement (62.11 GiB checkpoint + 16 GiB floor = 78.11 GiB); it requirement (62.11 GiB checkpoint + 16 GiB floor = 78.11 GiB); it
crash-looped 8 boots, each dying in ~5 s at the pin gate, and crash-looped 8 boots, each dying in ~5 s at the pin gate, and
self-resolved in ~2 min when the dying 27B engine freed RAM (boot #9 self-resolved in ~2 min when the dying 27B engine freed RAM (boot #9
pinned clean at 88.4 GiB available). **Latent risk + fix:** the host was pinned clean at 88.4 GiB available). **Latent risk + fix:** the host was
left with only ~1.8 GiB `MemAvailable`, so a restart would crash-loop
again; `serve-frank-dragoon.sh` now has a host-RAM preflight that checks again; `serve-frank-dragoon.sh` now has a host-RAM preflight that checks
`MemAvailable` against the pin gate BEFORE `podman run` and WAITS up to `MemAvailable` against the pin gate BEFORE `podman run` and WAITS up to
`FRANK_DRAGOON_RAM_WAIT` (600 s) for the RAM to clear, refusing loudly if `FRANK_DRAGOON_RAM_WAIT` (600 s) for the RAM to clear, refusing loudly if
@@ -1154,7 +1112,6 @@
`reasoning_content`; models.yml flipped accordingly). Container renamed `reasoning_content`; models.yml flipped accordingly). Container renamed
to the unit name (Frank's call; `frank-archon-qwen` → legacy list); to the unit name (Frank's call; `frank-archon-qwen` → legacy list);
deployer image gate → `podman image exists` (local builds carry no deployer image gate → `podman image exists` (local builds carry no
RepoDigests) and health gate reads the BODY — FreeToken 200s with
`{"status":"loading"}` mid-load, which false-greened a deploy at 6 s `{"status":"loading"}` mid-load, which false-greened a deploy at 6 s
(vLLM 503'd; the bare curl was honest there). Verified: FLEET-OK (vLLM 503'd; the bare curl was honest there). Verified: FLEET-OK
completion, reasoning split (17×24 → "408"), native `tool_calls`, vision completion, reasoning split (17×24 → "408"), native `tool_calls`, vision
@@ -1275,11 +1232,9 @@
7,653 slots, budget 31.29. 7,653 slots, budget 31.29.
- Shape G v1 (0a4ff30) tried util 1.0: **both workers refused init before a - Shape G v1 (0a4ff30) tried util 1.0: **both workers refused init before a
single weight loaded** — `ValueError: Free memory on device cuda:N single weight loaded** — `ValueError: Free memory on device cuda:N
(31.29/31.86 GiB ...)`, NRestarts=2 crash loop, zero payload progress.
The CLI field is `Field(gt=0, le=1)`, but the worker demands free > The CLI field is `Field(gt=0, le=1)`, but the worker demands free >
budget, so budget == total is a trap, not a knife-edge. Frank: ".982 is budget, so budget == total is a trap, not a knife-edge. Frank: ".982 is
the law, never 1.00" — now with a second proof. the law, never 1.00" — now with a second proof.
- Shape G v2 (c67a2ae, deployed green incl. e2e 33/33): util back to
0.982, graph base to 0 (the shape F boot reported a NEGATIVE -0.06 GiB 0.982, graph base to 0 (the shape F boot reported a NEGATIVE -0.06 GiB
capture delta, so the 256 MiB base was fiction; 0 + 12 MiB/graph still capture delta, so the 256 MiB base was fiction; 0 + 12 MiB/graph still
prices a future 18-graph spec-decode shape at 0.21 GiB), KV_BYTES prices a future 18-graph spec-decode shape at 0.21 GiB), KV_BYTES
@@ -1299,7 +1254,6 @@
## 2026-10-02 ## 2026-10-02
**frank-dragoon cutover: vLLM MiMo -> ROCmFPX Q8_0_ROCMFPX_AGENT + MTP** **frank-dragoon cutover: vLLM MiMo -> ROCmFPX Q8_0_ROCMFPX_AGENT + MTP**
- 2026-10-02 — frank-dragoon: vLLM MiMo → ROCmFPX `Q8_0_ROCMFPX_AGENT` - 2026-10-02 — frank-dragoon: vLLM MiMo → ROCmFPX `Q8_0_ROCMFPX_AGENT`
payload + MTP from the repo's own bundled head, one session at the native payload + MTP from the repo's own bundled head, one session at the native
262,144. See [[frank-dragoon]]. 262,144. See [[frank-dragoon]].
@@ -1318,7 +1272,6 @@
**frank-zealot: Q5_K_M, then three contexts + vision, sized by two boots** **frank-zealot: Q5_K_M, then three contexts + vision, sized by two boots**
- 2026-10-02 — frank-zealot: Q5_K_M payload, then three contexts + vision - 2026-10-02 — frank-zealot: Q5_K_M payload, then three contexts + vision
in one day, sized by two boots (`CTX_SIZE=540672 PARALLEL=3`, f16 K + in one day, sized by two boots (`CTX_SIZE=540672 PARALLEL=3`, f16 K +
q8_0 V under flash attention, `--mmproj`). See [[frank-zealot]]. q8_0 V under flash attention, `--mmproj`). See [[frank-zealot]].
@@ -1347,7 +1300,6 @@
**frank-archon shape D: 3 x 200704, fp8, pool sized for 2 contexts** **frank-archon shape D: 3 x 200704, fp8, pool sized for 2 contexts**
Frank's order: shrink frank-archon to a 200704-token window with about two Frank's order: shrink frank-archon to a 200704-token window with about two
resident full-length contexts, cut the KV pool, and drop MNBT to 1024. The resident full-length contexts, cut the KV pool, and drop MNBT to 1024. The
first attempt (`30904ba`, 2 x 200704 + SEQS=2) **crash-looped**; the revert first attempt (`30904ba`, 2 x 200704 + SEQS=2) **crash-looped**; the revert
@@ -1356,7 +1308,6 @@ top of the proven-good base, root cause first.
- **ROOT CAUSE: `--max-num-seqs 2` cannot compile this model. It was never - **ROOT CAUSE: `--max-num-seqs 2` cannot compile this model. It was never
OOM, never MNBT, never the KV dtype.** Four single-variable boot probes OOM, never MNBT, never the KV dtype.** Four single-variable boot probes
against the then-deployed shape (production stopped for each, candidate
booted AS the service): 262144 x SEQS 2 fails at BOTH MNBT rungs with booted AS the service): 262144 x SEQS 2 fails at BOTH MNBT rungs with
`torch.fx.experimental.symbolic_shapes.ConstraintViolationError: `torch.fx.experimental.symbolic_shapes.ConstraintViolationError:
Constraints violated (L['query_start_loc'].size()[0]) ... specialized it Constraints violated (L['query_start_loc'].size()[0]) ... specialized it
@@ -1392,13 +1343,11 @@ top of the proven-good base, root cause first.
- **Harness contract followed the window:** `models.yml` `contextWindow` - **Harness contract followed the window:** `models.yml` `contextWindow`
262144 -> **200704**; side effect: frank-archon's compaction request fits 262144 -> **200704**; side effect: frank-archon's compaction request fits
inside a frank-zealot slot again. A comment-only follow-up `295d578` was inside a frank-zealot slot again. A comment-only follow-up `295d578` was
deployed so the per-service revision marker sits at HEAD. Rollback for
the pre-shape-D fp8+MTP4 era stays git tag the pre-shape-D fp8+MTP4 era stays git tag
`frank-archon-fp8-mtp-20261001`. Runbook [[frank-archon]]. `frank-archon-fp8-mtp-20261001`. Runbook [[frank-archon]].
**plex: the readiness gate probed HTTP against a TLS-only service** **plex: the readiness gate probed HTTP against a TLS-only service**
Found by accident, and it had been latent since the secure-connections posture Found by accident, and it had been latent since the secure-connections posture
was set: `deploy_plex` gated on `curl -fsS http://127.0.0.1:32400/identity`, was set: `deploy_plex` gated on `curl -fsS http://127.0.0.1:32400/identity`,
which **cannot** succeed. PMS drops plain HTTP off-loopback - the probe's which **cannot** succeed. PMS drops plain HTTP off-loopback - the probe's
@@ -1422,18 +1371,14 @@ and then answers with nothing, and curl reports "Empty reply from server" (52).
got the same treatment. Fixed to `curl -fsSk --max-time 3 got the same treatment. Fixed to `curl -fsSk --max-time 3
https://127.0.0.1:32400/identity` (`-k`: PMS serves its own self-signed https://127.0.0.1:32400/identity` (`-k`: PMS serves its own self-signed
cert on 32400). Re-deployed `39f0e0d`: plex recorded deployed, e2e cert on 32400). Re-deployed `39f0e0d`: plex recorded deployed, e2e
33/33, frank-archon untouched (`health=200`, NRestarts=0).
- **Operating lesson: `deploy.sh <target>` with no `--service` is a - **Operating lesson: `deploy.sh <target>` with no `--service` is a
fleet-wide drain.** It restarts every service whose recorded revision is
behind HEAD and whose paths changed since. To touch one service, name it. behind HEAD and whose paths changed since. To touch one service, name it.
**frank-archon shape E: `--expert-precise-memory` (+1,026 arena slots), a mounted compile cache, and the P2P answer** **frank-archon shape E: `--expert-precise-memory` (+1,026 arena slots), a mounted compile cache, and the P2P answer**
Shape D left one question open — the plan's overhead term was reserving 2.35 GiB Shape D left one question open — the plan's overhead term was reserving 2.35 GiB
of compile headroom the boot never used — and the answer to Frank's free-VRAM of compile headroom the boot never used — and the answer to Frank's free-VRAM
question arrived together with a boot-cost bug that had been costing 181 s on question arrived together with a boot-cost bug that had been costing 181 s on
every restart since the cutover.
- **`--expert-precise-memory` is real and it fits (option A).** Boot-probed on - **`--expert-precise-memory` is real and it fits (option A).** Boot-probed on
the shape-D base with production stopped, the candidate booted AS the service the shape-D base with production stopped, the candidate booted AS the service
@@ -1457,11 +1402,9 @@ every restart since the cutover.
1.38x` — measured, rejected as the shipped value, kept as the conservative 1.38x` — measured, rejected as the shipped value, kept as the conservative
fallback. With 3.15e9/rank the endpoint keeps shape D's 1.99x concurrency and fallback. With 3.15e9/rank the endpoint keeps shape D's 1.99x concurrency and
gains the arena, which is the shape D follow-up had actually been looking for. gains the arena, which is the shape D follow-up had actually been looking for.
- **THE COMPILE CACHE WAS BEING THROWN AWAY ON EVERY RESTART — 181 s each.**
Measured: a fresh container has **no `/root/.cache` at all** (`No such file or Measured: a fresh container has **no `/root/.cache` at all** (`No such file or
directory`); after our boot it holds `/root/.cache/vllm` at directory`); after our boot it holds `/root/.cache/vllm` at
**1,154,714,333 B in 4,665 files** plus `/root/.cache/comgr` at 229 MB, inside **1,154,714,333 B in 4,665 files** plus `/root/.cache/comgr` at 229 MB, inside
the ephemeral layer, and `podman rm -f` (every deploy, restart, reboot)
discards them. The engine names the loss itself: `Compiling a graph for compile discards them. The engine names the loss itself: `Compiling a graph for compile
range (1, 1024) takes 173.19 s` ... `torch.compile took 181.20 s in total` ... range (1, 1024) takes 173.19 s` ... `torch.compile took 181.20 s in total` ...
`init engine (profile, create kv cache, warmup model) took 217.26 s `init engine (profile, create kv cache, warmup model) took 217.26 s
@@ -1514,7 +1457,6 @@ every restart since the cutover.
not a crash — the container and its serve script must be polled by recorded id not a crash — the container and its serve script must be polled by recorded id
and their logs dumped on the exit path, or a failure looks like a bad flag. and their logs dumped on the exit path, or a failure looks like a bad flag.
The flag was fine both times. The flag was fine both times.
- Live: `health=200`, `NRestarts=0`, arena 17.22 GiB / 7,522 slots, KV 398,658
tokens / 1.99x. Docs: [[frank-archon]]. tokens / 1.99x. Docs: [[frank-archon]].
- **Deployed `1ac4d48`, e2e 33/33, and the live plan matched the probe exactly:** - **Deployed `1ac4d48`, e2e 33/33, and the live plan matched the probe exactly:**
`'expert_precise_memory': True`, `runtime overhead … 5.25 GiB`, `expert cache `'expert_precise_memory': True`, `runtime overhead … 5.25 GiB`, `expert cache
@@ -1523,7 +1465,6 @@ every restart since the cutover.
precise overhead and the arena growth are now production numbers, not a precise overhead and the arena growth are now production numbers, not a
probe's projection. probe's projection.
- **The compile-cache fix was then proven on the same endpoint, two consecutive - **The compile-cache fix was then proven on the same endpoint, two consecutive
boots of the deployed shape:** cold `Compiling a graph for compile range
(1, 1024) takes 179.24 s` → `torch.compile took 188.26 s`; warm `Directly load (1, 1024) takes 179.24 s` → `torch.compile took 188.26 s`; warm `Directly load
the compiled graph(s) for compile range (1, 1024) from the cache, took the compiled graph(s) for compile range (1, 1024) from the cache, took
2.143 s` → `torch.compile took 3.97 s in total` → `init engine (profile, 2.143 s` → `torch.compile took 3.97 s in total` → `init engine (profile,
@@ -1554,14 +1495,11 @@ every restart since the cutover.
**frank-archon RESTORED to fp8 3 x 262144 (bf16 boot fails)** **frank-archon RESTORED to fp8 3 x 262144 (bf16 boot fails)**
bf16 (--kv-cache-dtype auto) at 2 x 262144 did not stay up, with OR without bf16 (--kv-cache-dtype auto) at 2 x 262144 did not stay up, with OR without
--expert-precise-memory: --expert-precise-memory:
- with precise: NRestarts 1->3, each attempt ~6.7 min then
`Deactivated successfully` (API server drains after an EngineCore fatal `Deactivated successfully` (API server drains after an EngineCore fatal
error) — cause not captured, the serve script rm'd the container. error) — cause not captured, the serve script rm'd the container.
- without precise (default 7.60 GiB overhead, arena 14.3 GiB): NRestarts
climbing, container `Exited (1)`, and the journal finally showed the real climbing, container `Exited (1)`, and the journal finally showed the real
error: error:
`RuntimeError: Worker failed with error 'Constraints violated `RuntimeError: Worker failed with error 'Constraints violated
@@ -1582,12 +1520,9 @@ Next: bisect bf16 vs SEQS in a window (bf16 + SEQS=3 isolates the dtype).
**frank-archon: --expert-precise-memory + bf16 = boot crash loop** **frank-archon: --expert-precise-memory + bf16 = boot crash loop**
**Reverted.** The two changes Frank asked for were applied together and the **Reverted.** The two changes Frank asked for were applied together and the
result crash-looped: `NRestarts` climbed 1 -> 2 -> 3, each attempt running
~6.7 minutes then logging `Deactivated successfully` (vLLM's API server drains ~6.7 minutes then logging `Deactivated successfully` (vLLM's API server drains
and exits 0 after an EngineCore fatal error, so `podman wait` returns 0; the and exits 0 after an EngineCore fatal error, so `podman wait` returns 0; the
new `Restart=always` turned a silent death into a visible loop).
The budget arithmetic explains it without needing the error text, because the The budget arithmetic explains it without needing the error text, because the
arena is DEVICE-resident (the fp8 3-window shape's plan predicts 28.27 GiB and arena is DEVICE-resident (the fp8 3-window shape's plan predicts 28.27 GiB and
@@ -1615,7 +1550,6 @@ by paying for it elsewhere (smaller pool, fewer sessions, or a shorter context).
**frank-archon: 2 bf16 windows + --expert-precise-memory** **frank-archon: 2 bf16 windows + --expert-precise-memory**
Frank's final shape for the night. Two changes at once, both measured in Frank's final shape for the night. Two changes at once, both measured in
principle before the boot: principle before the boot:
@@ -1641,7 +1575,6 @@ planned arena; it comes out of free VRAM instead.
**frank-dragoon: Q4_K_XL payload + 4 x 163840 sessions** **frank-dragoon: Q4_K_XL payload + 4 x 163840 sessions**
Frank's call. Payload swapped from ajgazin's **abliterated** Swift-1.5 Frank's call. Payload swapped from ajgazin's **abliterated** Swift-1.5
UD-Q8_K_XL (29.3 GiB) to **unsloth/Qwen3.8-27B-GGUF UD-Q4_K_XL** (16.35 GiB, UD-Q8_K_XL (29.3 GiB) to **unsloth/Qwen3.8-27B-GGUF UD-Q4_K_XL** (16.35 GiB,
sha256 3f227079…b01e, vanilla/Apache-2.0 — the endpoint is no longer sha256 3f227079…b01e, vanilla/Apache-2.0 — the endpoint is no longer
@@ -1663,7 +1596,6 @@ weights 16.35 + KV 40.0 + drafter 1.07 + linear state 0.56 + runtime ~4 =
**e2e gate flakiness (12-crawl) — do not panic at the banner** **e2e gate flakiness (12-crawl) — do not panic at the banner**
`specs/12-crawl.spec.ts:23` ("every user open link reaches its app") failed the `specs/12-crawl.spec.ts:23` ("every user open link reaches its app") failed the
deploy gate twice and then passed cleanly on a manual re-run: deploy gate twice and then passed cleanly on a manual re-run:
- run 1 (deploy): `Expected "plex.franksdank.cloud"` / `Received "app.plex.tv"` - run 1 (deploy): `Expected "plex.franksdank.cloud"` / `Received "app.plex.tv"`
@@ -1682,7 +1614,6 @@ any repo service.
**frank-archon bf16 no-spec cutover** **frank-archon bf16 no-spec cutover**
- `/scripts/frank-archon.env` — bf16 no-spec cutover, FINAL shape after the - `/scripts/frank-archon.env` — bf16 no-spec cutover, FINAL shape after the
night ladder: MAXLEN **241664** x SEQS 3, KV_BYTES **10,184,000,000**/rank, night ladder: MAXLEN **241664** x SEQS 3, KV_BYTES **10,184,000,000**/rank,
KV_DTYPE auto (bf16), MNBT 1024, GATHER CHUNKS=32/LANES=64, UTIL 0.98; KV_DTYPE auto (bf16), MNBT 1024, GATHER CHUNKS=32/LANES=64, UTIL 0.98;
@@ -1719,7 +1650,6 @@ any repo service.
- **Client contract fixed (thinking + tools).** The live endpoint returned a - **Client contract fixed (thinking + tools).** The live endpoint returned a
null `reasoning` field and dumped the thinking text INLINE in `content` null `reasoning` field and dumped the thinking text INLINE in `content`
with a dangling `</think>` (measured: `'We need answer…Brief.\n</think>\n\n391'`) with a dangling `</think>` (measured: `'We need answer…Brief.\n</think>\n\n391'`)
— the malformed thinking block clients choke on — because the deployed argv
had no `--reasoning-parser`. The GLM rollback (`d91379d`) had dropped all had no `--reasoning-parser`. The GLM rollback (`d91379d`) had dropped all
three contract flags and the qwen rollback inherited the gap. Restored from three contract flags and the qwen rollback inherited the gap. Restored from
`ecaad36`'s values, confirmed against this fork's registries: `ecaad36`'s values, confirmed against this fork's registries:
@@ -1749,11 +1679,9 @@ any repo service.
knife-edge shape change, send a large-`max_tokens` request before calling it knife-edge shape change, send a large-`max_tokens` request before calling it
done. FOUR slots x 262144 fp8 (KV_BYTES 8,311,013,376/rank) returns ~1.93 done. FOUR slots x 262144 fp8 (KV_BYTES 8,311,013,376/rank) returns ~1.93
GiB per rank for activations/graph capture. Frank's call 2026-10-01. GiB per rank for activations/graph capture. Frank's call 2026-10-01.
- `/scripts/frank-archon.service` — **Restart=always, not on-failure** (plus
`StartLimitIntervalSec=0` and `OOMScoreAdjust=-500`). A runtime worker fault `StartLimitIntervalSec=0` and `OOMScoreAdjust=-500`). A runtime worker fault
makes vLLM's API server exit CLEAN, so ExecStart returned 0, systemd logged makes vLLM's API server exit CLEAN, so ExecStart returned 0, systemd logged
"Deactivated successfully", and on-failure NEVER fired: the endpoint stayed "Deactivated successfully", and on-failure NEVER fired: the endpoint stayed
down until a human noticed. An explicit `systemctl stop` still wins, so
bench windows and deploys are unaffected. bench windows and deploys are unaffected.
- **Final shape (Frank's call, 2026-10-01): 3 x 262144, pool at the FLOOR, - **Final shape (Frank's call, 2026-10-01): 3 x 262144, pool at the FLOOR,
MNBT 2048, rest to the arena.** MAXLEN 262144 x SEQS 3 = 786,432 tokens of MNBT 2048, rest to the arena.** MAXLEN 262144 x SEQS 3 = 786,432 tokens of
@@ -1773,7 +1701,6 @@ any repo service.
**frank-dragoon cutover: gufo -> vLLM TheRock, MiMo-V2.6 BF16 + cross-model MTP** **frank-dragoon cutover: gufo -> vLLM TheRock, MiMo-V2.6 BF16 + cross-model MTP**
- **Question that started it:** can a Qwen3.5 MTP drafter attach to - **Question that started it:** can a Qwen3.5 MTP drafter attach to
frank-zealot, and how does vLLM run the FULL BF16 MiMo checkpoint? Answer 1: frank-zealot, and how does vLLM run the FULL BF16 MiMo checkpoint? Answer 1:
not on frank-zealot — SGLang reads the MTP head from the TARGET checkpoint not on frank-zealot — SGLang reads the MTP head from the TARGET checkpoint
@@ -1787,7 +1714,6 @@ any repo service.
prefill / 512 decode):** base 17,876 t/s prefill / 13.1 t/s decode (13.1 prefill / 512 decode):** base 17,876 t/s prefill / 13.1 t/s decode (13.1
is ~94% of the LPDDR5x BF16 roof - decode here is bandwidth, not a bug); is ~94% of the LPDDR5x BF16 roof - decode here is bandwidth, not a bug);
+DFlash-8 7,976 / 17.4 (acceptance 59%/26% per position); +MTP-2 7,380 / +DFlash-8 7,976 / 17.4 (acceptance 59%/26% per position); +MTP-2 7,380 /
19.8 (71%/44%, token rate 57.4%). Deployed MTP-2: +51% decode. Both spec
arms pay ~55-59% of prefill. `num_speculative_tokens` beyond 2 unmeasured. arms pay ~55-59% of prefill. `num_speculative_tokens` beyond 2 unmeasured.
- **Measured numerics law:** spec arms do NOT reproduce the no-spec greedy - **Measured numerics law:** spec arms do NOT reproduce the no-spec greedy
text (bf16 batched verification reorders reductions; near-tie argmaxes text (bf16 batched verification reorders reductions; near-tie argmaxes
@@ -1798,9 +1724,7 @@ any repo service.
`reasoningContentField: "reasoning"`, and the extraBody moved to the `reasoningContentField: "reasoning"`, and the extraBody moved to the
Qwen3.5-family recipe (0.7/0.8) with `chat_template_kwargs` Qwen3.5-family recipe (0.7/0.8) with `chat_template_kwargs`
(a top-level `preserve_thinking` is ignored by vLLM). (a top-level `preserve_thinking` is ignored by vLLM).
- Deployer gap found pre-deploy: the spec-draft gate demanded a single
`model.safetensors` (the dflash layout) and would have rejected the `model.safetensors` (the dflash layout) and would have rejected the
deployed MTP arm (4 shards behind an index); fixed to accept both
layouts, gated by bats. `z-lab/Qwen3.5-9B-DFlash`'s "vLLM PR #40898 layouts, gated by bats. `z-lab/Qwen3.5-9B-DFlash`'s "vLLM PR #40898
unmerged" card claim is FALSE - `vllm/v1/spec_decode/dflash.py` ships in unmerged" card claim is FALSE - `vllm/v1/spec_decode/dflash.py` ships in
0.30 (verified in-image); the drafter stays on disk as the alternative. 0.30 (verified in-image); the drafter stays on disk as the alternative.
@@ -1839,7 +1763,6 @@ any repo service.
cutover), TP=2, 131,072 ctx x 2 seqs, bf16 KV, util 0.98, NBATCH 2048, cutover), TP=2, 131,072 ctx x 2 seqs, bf16 KV, util 0.98, NBATCH 2048,
`--enable-expert-offload --expert-precise-memory`, prefix caching, vision + `--enable-expert-offload --expert-precise-memory`, prefix caching, vision +
`glm47` tool and reasoning parsers, MTP OFF, prefetch OFF. `glm47` tool and reasoning parsers, MTP OFF, prefetch OFF.
- Measured on the deployed engine: **decode 9.9-10.0 tok/s, prefill 918 tok/s**
(warm, 2.6k prompt), KV 138,561 tokens; arena 14.51 GiB = 2,383 slots per (warm, 2.6k prompt), KV 138,561 tokens; arena 14.51 GiB = 2,383 slots per
rank over 12,096 host-backed experts (73.64 GiB pinned per rank). rank over 12,096 host-backed experts (73.64 GiB pinned per rank).
- **The wall — and a corrected one.** The first account of the shortfall - **The wall — and a corrected one.** The first account of the shortfall
@@ -1945,7 +1868,6 @@ any repo service.
per-sequence KDA state and the MTP draft's KV, and `gpu_worker` caps KV at per-sequence KDA state and the MTP draft's KV, and `gpu_worker` caps KV at
that reserve, making a legal `--max-model-len` unbootable. Shipped that reserve, making a legal `--max-model-len` unbootable. Shipped
`scripts/patches/gpu_worker-kv-cap.patch` (lets an explicit operator KV `scripts/patches/gpu_worker-kv-cap.patch` (lets an explicit operator KV
request raise the cap) — NOT needed by the deployed shape.
- Rollback: commit `2339fae` (qwen FP8 + MTP4); the qwen payload and the R9V - Rollback: commit `2339fae` (qwen FP8 + MTP4); the qwen payload and the R9V
state dirs are intact on disk. state dirs are intact on disk.
@@ -2125,7 +2047,6 @@ any repo service.
(`20260921-frank-dragoon-assembled` 88 G, `-redownload` 30 G) and never (`20260921-frank-dragoon-assembled` 88 G, `-redownload` 30 G) and never
purged. Total now staged on this host: **627 G**. purged. Total now staged on this host: **627 G**.
- Verified after the moves: frank-dragoon and frank-zealot both `active`, - Verified after the moves: frank-dragoon and frank-zealot both `active`,
both `/health` 200, and all three live assets (Swift target, DFlash2
drafter, Gemma4 target) still present. drafter, Gemma4 target) still present.
- **frank-dragoon payload swapped to Swift-1.5 (uncensored)** (production - **frank-dragoon payload swapped to Swift-1.5 (uncensored)** (production
change, Dankclaw). Frank asked whether the UD Q8 of change, Dankclaw). Frank asked whether the UD Q8 of
@@ -2242,7 +2163,6 @@ any repo service.
8 s, drafter 2 s, session 3 s) against a 20-minute `LLM_WAIT` budget; 8 s, drafter 2 s, session 3 s) against a 20-minute `LLM_WAIT` budget;
RSS 30.6 GiB; a live short-prompt request returned RSS 30.6 GiB; a live short-prompt request returned
`Server-Timing: ttft;dur=549.8, inter_token;dur=24.2` (~41 tok/s). `Server-Timing: ttft;dur=549.8, inter_token;dur=24.2` (~41 tok/s).
Verified live: `/health` 200, `/ready` 200, `/v1/models` →
`frank-dragoon` at `context_length 262144`, a chat completion carrying `frank-dragoon` at `context_length 262144`, a chat completion carrying
`reasoning_content`, a `tool_calls` response with `reasoning_content`, a `tool_calls` response with
`finish_reason: tool_calls`, and SSE streaming. `finish_reason: tool_calls`, and SSE streaming.
@@ -2309,9 +2229,6 @@ any repo service.
- **LANDMINE, not changed here:** `/home/dank/models` was cleared by Frank - **LANDMINE, not changed here:** `/home/dank/models` was cleared by Frank
mid-session to free space. frank-archon is still answering `:7770` from mid-session to free space. frank-archon is still answering `:7770` from
mmapped-but-unlinked inodes (`/models` is empty inside the running mmapped-but-unlinked inodes (`/models` is empty inside the running
container) — **the endpoint cannot be restarted until the 175 GiB payload
is re-fetched.** A reboot or a container restart takes frank-archon down.
- Deploy-state reconciliation (Frank's call: no restart) — the autoresearch trio
(`f187165` PLE 1800, `41e9189` prefix caching, `ba2100e` docs) was found already (`f187165` PLE 1800, `41e9189` prefix caching, `ba2100e` docs) was found already
MERGED to main + pushed AND already live on frank-archon: the running MERGED to main + pushed AND already live on frank-archon: the running
`frank-archon-r9v` container (booted 12:13 from the `ba2100e` checkout) carries `frank-archon-r9v` container (booted 12:13 from the `ba2100e` checkout) carries
@@ -2324,9 +2241,7 @@ any repo service.
failure `11-admin` is the FrankBook-Pro Mac mini (`192.168.0.112:8989`) offline failure `11-admin` is the FrankBook-Pro Mac mini (`192.168.0.112:8989`) offline
(100 % ICMP loss + TCP timeout), the documented asleep-host condition, not a (100 % ICMP loss + TCP timeout), the documented asleep-host condition, not a
regression. regression.
- `scripts/frank-archon.env`, runbook — **prefix caching DEPLOYED on
frank-archon** (Frank's go-ahead; commit `41e9189`): E8-qualified, zero frank-archon** (Frank's go-ahead; commit `41e9189`): E8-qualified, zero
VRAM cost, 9x/63x reuse TTFT. Verified on :7770 (argv flag, health 200,
completion "PREFIX-CACHE-LIVE"). Gotcha re-confirmed: the container is completion "PREFIX-CACHE-LIVE"). Gotcha re-confirmed: the container is
in dank's ROOTLESS podman store (`sudo podman` inspects an empty store in dank's ROOTLESS podman store (`sudo podman` inspects an empty store
and reports nothing); probe via the shim's `docker` or the unit MainPID. and reports nothing); probe via the shim's `docker` or the unit MainPID.
@@ -2458,7 +2373,6 @@ any repo service.
## 2026-08-26 ## 2026-08-26
- `/dankclaw/Caddyfile` — Both Plex vhosts proxy to PMS over a TLS upstream (`insecure_skip_verify`); PMS drops plain-HTTP connections from non-loopback sources (empty-body 502s). - `/dankclaw/Caddyfile` — Both Plex vhosts proxy to PMS over a TLS upstream (`insecure_skip_verify`); PMS drops plain-HTTP connections from non-loopback sources (empty-body 502s).
- `/dankclaw/gitea/quadlet/gitea-caddy.container` — Standalone Gitea Caddy quadlet retired; web proxy ownership moved to canonical Caddy; deploy-plan/deploy.sh/deploy-remote.sh + tests updated for the migration. - `/dankclaw/gitea/quadlet/gitea-caddy.container` — Standalone Gitea Caddy quadlet retired; web proxy ownership moved to canonical Caddy; deploy-plan/deploy.sh/deploy-remote.sh + tests updated for the migration.
- Incident — Post-reboot: Caddy (a bare compose container — `restart: unless-stopped` does not survive shutdown's explicit stop) and Pi-hole (quadlet crash-loop, restart counter 1645) were both down. Root cause: `net.ipv4.ip_unprivileged_port_start` reset to 80 by a later-sorted sysctl drop-in AND a stale line in `/etc/sysctl.conf`, which systemd-sysctl applies AFTER all of `sysctl.d/`.
- `/AGENTS.md` — New MANDATORY "Boot Persistence & Self-Healing" section. - `/AGENTS.md` — New MANDATORY "Boot Persistence & Self-Healing" section.
- `/dankclaw/caddy/quadlet/caddy.container`, `/dankclaw/caddy/env.example` — Caddy is now a boot-persistent quadlet (joins `dankclaw_default`, `Pull=never`, env from `/home/dank/Containers/caddy/env`); removed from compose.yaml. - `/dankclaw/caddy/quadlet/caddy.container`, `/dankclaw/caddy/env.example` — Caddy is now a boot-persistent quadlet (joins `dankclaw_default`, `Pull=never`, env from `/home/dank/Containers/caddy/env`); removed from compose.yaml.
- `/dankclaw/pihole/sysctl/99-dnsmasq.conf` — Repo source of truth for `ip_unprivileged_port_start=53`; `deploy_pihole` stages conflicting drop-ins, comments out the stale sysctl.conf line, and applies `sysctl --system` (the boot code path). - `/dankclaw/pihole/sysctl/99-dnsmasq.conf` — Repo source of truth for `ip_unprivileged_port_start=53`; `deploy_pihole` stages conflicting drop-ins, comments out the stale sysctl.conf line, and applies `sysctl --system` (the boot code path).
+96 -67
View File
@@ -4,15 +4,15 @@
# qwen35 hybrid with 32 layers (8 full-attention + 24 Gated DeltaNet) and the # qwen35 hybrid with 32 layers (8 full-attention + 24 Gated DeltaNet) and the
# MTP draft head INSIDE the file — via the PrismML fork build of llama.cpp # MTP draft head INSIDE the file — via the PrismML fork build of llama.cpp
# (vendor build 10777), the ONLY loader that registers the payload's custom # (vendor build 10777), the ONLY loader that registers the payload's custom
# tensor type 142 and honours its prism.hadamard.* metadata: TWO 262,144-token # tensor type 142 and honours its prism.hadamard.* metadata: THREE 200,704-token
# conversations (--ctx-size 524288 --parallel 2) over an UNQUANTIZED f16 KV # conversations (--ctx-size 602112 --parallel 3) over an f16 K + q8_0 V cache —
# cache. Exact KV bills 32,768 B/token here, so the pool is what pays for it — # 25,088 B/token, 14,406.00 MiB at this pool — with the MTP drafter ARMED
# the same dtype at the old 786,432 pool is 24,576 MiB against the 24,123 MiB # (--spec-type draft-mtp). The asymmetry IS the shape: exact V (32,768 B/token)
# the driver leaves addressable, and the slot count is the term that moved. The # leaves nothing for a draft context that bills its own f16 KV over the WHOLE
# publisher's own Q8_0 vision projector rides along (the BF16 one can crash on # pool (4,096 B/position, 2,352.00 MiB here), and the 786,432 pool this shape
# concurrent images), and MTP stays OFF: the head is present, but exact KV # replaced cannot carry a drafter at any dtype inside the 24,123 MiB the driver
# leaves no room for a draft context that bills f16 KV over the whole pool. Log # leaves addressable. The publisher's own Q8_0 vision projector rides along (the
# verbosity 4 ships so the VRAM-fit forensics land in the journal. # BF16 one can crash on concurrent images). Log
# This seam runs the serve script against a stub engine and asserts on the # This seam runs the serve script against a stub engine and asserts on the
# argv it actually execs, so what is pinned is the command the operator gets. # argv it actually execs, so what is pinned is the command the operator gets.
@@ -143,7 +143,7 @@ run_stage() {
done done
} }
@test "two conversations share the pool at a 262144 window each" { @test "three conversations share the pool at a 200704 window each" {
serve_argv serve_argv
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ "$(argv_value --alias)" = "frank-zealot" ] [ "$(argv_value --alias)" = "frank-zealot" ]
@@ -152,30 +152,36 @@ run_stage() {
[ "$(argv_value --model)" = "$MODEL_DIR/$MODEL_STUB" ] [ "$(argv_value --model)" = "$MODEL_DIR/$MODEL_STUB" ]
[ "$(argv_value --n-gpu-layers)" -gt 0 ] [ "$(argv_value --n-gpu-layers)" -gt 0 ]
# llama.cpp divides --ctx-size across slots, so the observable contract is # llama.cpp divides --ctx-size across slots, so the observable contract is
# the per-slot window. Exact KV costs the SLOT COUNT, not the window: every # the per-slot window: harness contextWindow, Prometheus dashboards and every
# conversation still gets the model's TRAINED 262,144, and the harness # consumer's compaction trigger read THAT number, and a longer prompt is a
# contextWindow must quote that number — the pool and the slot count are # 400, never a truncation. Two invariants the pool/slot split has to keep:
# neither, and a longer prompt is a 400, never a truncation. # it divides evenly (llama.cpp floors it otherwise) and a slot never exceeds
[ "$(argv_value --parallel)" -eq 2 ] # the model's TRAINED window (there is no YaRN here, so the engine would be
[ "$(argv_value --ctx-size)" -eq 524288 ] # quoting a window it cannot fill).
local pool slots [ "$(argv_value --parallel)" -eq 3 ]
[ "$(argv_value --ctx-size)" -eq 602112 ]
local pool slots window
pool="$(argv_value --ctx-size)"; slots="$(argv_value --parallel)" pool="$(argv_value --ctx-size)"; slots="$(argv_value --parallel)"
[ $((pool % slots)) -eq 0 ] [ $((pool % slots)) -eq 0 ]
[ $((pool / slots)) -eq 262144 ] window=$((pool / slots))
[ "$window" -eq 200704 ]
[ "$window" -le 262144 ]
} }
@test "the KV dtype pair follows the env file and flash-attn stays pinned on" { @test "the KV dtype pair follows the env file and flash-attn stays pinned on" {
# Asserted against the env rather than a literal so a retune moves one # Asserted against the env rather than a literal so a retune moves one
# file, not two. The shipped pair is f16/f16: exact attention state, and the # file, not two. The shipped pair is ASYMMETRIC — f16 K (the engine's own
# slot count is what makes it affordable — this payload bills 8,192 elements # default) plus q8_0 V: this payload bills 8,192 elements per token per cache
# per token per cache (8 full-attention layers × 4 KV heads × 256 dim), so # (8 full-attention layers × 4 KV heads × 256 dim), so K costs 16,384 B/token
# exact KV is 32,768 B/token = 16,384.00 MiB at the 524,288 pool, where the # and V 8,704 (q8_0 is 1.0625 B/element; there is no true fp8 KV dtype in
# q8_0 pair this file used to ship billed 17,408 B/token = 13,056.00 MiB at # this build) = 25,088 B/token = 14,406.00 MiB at the 602,112 pool. Exact V
# the old 786,432 pool (ledger-exact both times). The 24 DeltaNet layers # would bill 32,768 B/token and the all-q8_0 pair this file used to ship
# billed 17,408 — the halfway rung is what pays for the third slot AND the
# armed drafter (the fit gate below prices both). The 24 DeltaNet layers
# bill a fixed recurrent state per slot (~50.25 MiB a cell), which is why # bill a fixed recurrent state per slot (~50.25 MiB a cell), which is why
# pool arithmetic alone under-prices the shape. flash-attn is not # pool arithmetic alone under-prices the shape. flash-attn is not
# decoration: the quantized-V fallback is only legal with it, so it stays # decoration: quantized V is only legal with it, so it stays explicit and a
# explicit and a dtype retune cannot silently fall back. # dtype retune cannot silently fall back.
local env_k env_v local env_k env_v
env_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")" env_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")"
env_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")" env_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")"
@@ -192,30 +198,47 @@ run_stage() {
# The fit is arithmetic off the payload geometry plus the non-KV terms # The fit is arithmetic off the payload geometry plus the non-KV terms
# measured off the boot ledger (weights 2,036.71 + compute buffers 1,409.02 # measured off the boot ledger (weights 2,036.71 + compute buffers 1,409.02
# + projector 595 + 248.10 + CUDA context ≈ 460 = 4,749 MiB, dtype- and # + projector 595 + 248.10 + CUDA context ≈ 460 = 4,749 MiB, dtype- and
# slot-independent at this ubatch), plus one ~51 MiB recurrent-state cell # slot-independent at ubatch 2048), plus one ~51 MiB recurrent-state cell per
# per slot. 24,123 MiB is what the driver leaves addressable (nvidia-smi # slot. That constant is deliberately NOT credited with the ~1,306 MiB the
# reports it as used + free; its total column reads 24,576). # shipped ubatch 1024 hands back: the gate stays conservative.
local env_k env_v pool slots rate_k rate_v kv total # An ARMED drafter is billed, because it is the term this shape pays
# CACHE_TYPE_V=q8_0 to afford: the draft context is built against the
# TARGET's whole pool at its own f16 rate (measured 4,096 B/position at
# blk.32) plus buffers and its own recurrent cells — the measured spec-ON
# against spec-OFF delta at the 786,432 / ubatch-1024 arm was 3,524 MiB
# beside 3,072 MiB of draft KV, i.e. ~452 MiB, doubled here for the third
# slot's extra cells. 24,123 MiB is what the driver leaves addressable
# (nvidia-smi reports it as used + free; its total column reads 24,576).
local env_k env_v pool slots rate_k rate_v kv total spec f16 exact_pool exact_v
env_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")" env_k="$(sed -nE 's/^CACHE_TYPE_K=(.+)$/\1/p' "$ENV_FILE")"
env_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")" env_v="$(sed -nE 's/^CACHE_TYPE_V=(.+)$/\1/p' "$ENV_FILE")"
pool="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")" pool="$(sed -nE 's/^CTX_SIZE=([0-9]+)$/\1/p' "$ENV_FILE")"
slots="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")" slots="$(sed -nE 's/^PARALLEL=([0-9]+)$/\1/p' "$ENV_FILE")"
spec="$(sed -nE 's/^SPEC_TYPE=(.+)$/\1/p' "$ENV_FILE")"
rate_k="$(kv_bytes_per_token "$env_k")" rate_k="$(kv_bytes_per_token "$env_k")"
rate_v="$(kv_bytes_per_token "$env_v")" rate_v="$(kv_bytes_per_token "$env_v")"
f16="$(kv_bytes_per_token f16)"
kv="$(( pool * (rate_k + rate_v) / 1048576 ))" kv="$(( pool * (rate_k + rate_v) / 1048576 ))"
total="$(( kv + 4749 + 51 * slots ))" total="$(( kv + 4749 + 51 * slots ))"
if [ "$spec" != "none" ]; then
total="$(( total + pool * 4096 / 1048576 + 768 ))"
fi
# The shipped arm has to leave real room: the projector's worst case is # The shipped arm has to leave real room: the projector's worst case is
# charged AFTER the engine's own fit check, so a projection that lands on # charged AFTER the engine's own fit check, so a projection that lands on
# the ceiling is a crash waiting for the first image request. # the ceiling is a crash waiting for the first image request.
[ "$total" -le $(( 24123 - 1024 )) ] [ "$total" -le $(( 24123 - 1024 )) ]
# And the gate must be able to REFUSE: the same exact KV at the pool this # And the gate must be able to REFUSE, or passing above means nothing. Two
# shape left behind is 24,576 MiB of cache alone — a boot that dies before # boots this shape can walk straight into: the retired dtype pair at the
# /health ever answers. Proving the gate bites is what makes passing one # retired pool (exact f16 at 786,432 = 24,576 MiB of cache alone, which died
# meaningful. # at cudaMalloc), and the shipped pool with V moved back to f16 while the
local over # drafter stays armed — 18,816 MiB of cache plus the draft's 2,352. Both die
over="$(( 786432 * (rate_k + rate_v) / 1048576 + 4749 + 51 * 3 ))" # before /health ever answers, and no health gate can see either.
[ "$over" -gt 24123 ] exact_pool="$(( 786432 * (rate_k + f16) / 1048576 + 4749 + 51 * 3 ))"
[ "$exact_pool" -gt 24123 ]
exact_v="$(( pool * (rate_k + f16) / 1048576 + 4749 + 51 * slots
+ pool * 4096 / 1048576 + 768 ))"
[ "$exact_v" -gt 24123 ]
} }
@test "log verbosity follows the env file" { @test "log verbosity follows the env file" {
@@ -277,48 +300,54 @@ run_stage() {
argv_value --ubatch-size | grep -qE '^[0-9]+$' argv_value --ubatch-size | grep -qE '^[0-9]+$'
} }
@test "speculation is off: no drafter flag reaches the argv" { @test "the armed drafter ships the whole MTP set and no second copy of the trunk" {
# Frank, 2026-10-18: this file DOES carry the MTP head (the boot arms prove # Frank, 2026-10-19: the drafter is ON. The head is IN this file
# it loads and drafts, acceptance 0.798/0.617 per position, mean accepted # (qwen35.nextn_predict_layers = 1, blk.32.nextn.*) and it accepted well
# length 2.41) and it is still off, because the draft context bills its own # when it was measured off (0.798/0.617 per position, mean accepted length
# f16 KV over the WHOLE pool — 4,096 B/position × 524,288 = 2,048 MiB plus # 2.41); what it now pays for is CACHE_TYPE_V=q8_0 plus UBATCH_SIZE=1024,
# buffers — on a trunk that already projects 21,234 MiB with exact KV. The # which is why the dtype pair and the batch pair are pinned next door.
# MTP arms in frank-zealot.env are all q8_0-era measurements; the doors back # The observable contract is the SET: --spec-type plus every --spec-draft-*
# to the drafter are one window per conversation, or KV back to q8_0 with # the env names, and NO --spec-draft-model — the MTP draft rides the MAIN
# UBATCH_SIZE=1024 (213.5 t/s). The flags move as a set: # model's weights, so a draft file would load the trunk a second time.
# SPEC_TYPE=none strips --spec-type, --spec-draft-n-max AND local n_max
# --spec-draft-model together, so a no-spec boot cannot carry half a spec n_max="$(sed -nE 's/^SPEC_DRAFT_N_MAX=([0-9]+)$/\1/p' "$ENV_FILE")"
# pair (the ROCmFPX-era baseline arm segfaulted at load doing that).
serve_argv serve_argv
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ "$(argv_value --spec-type)" = "draft-mtp" ]
[ "$(argv_value --spec-draft-n-max)" = "$n_max" ]
[ "$(argv_value --spec-draft-n-min)" = "0" ]
[ "$(argv_value --spec-draft-p-min)" = "0" ]
! grep -qxF -- '--spec-draft-model' <<<"$output"
}
@test "the rollback knob strips the entire spec set" {
# SPEC_TYPE=none is the documented retreat (and the shape this endpoint
# shipped before 2026-10-19). The law it exists to obey: the flags move as a
# SET — a no-spec boot that keeps --spec-draft-n-max, or a baseline arm that
# hands over --model-draft with no --spec-type, has segfaulted at load with
# no diagnostic. So the rollback is proven by ABSENCE of all of them.
serve_argv
sed -i 's/^SPEC_TYPE=.*/SPEC_TYPE=none/' "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
! grep -qxF -- '--spec-type' <<<"$output" ! grep -qxF -- '--spec-type' <<<"$output"
! grep -qxF -- '--spec-draft-n-max' <<<"$output" ! grep -qxF -- '--spec-draft-n-max' <<<"$output"
! grep -qxF -- '--spec-draft-n-min' <<<"$output"
! grep -qxF -- '--spec-draft-p-min' <<<"$output"
! grep -qxF -- '--spec-draft-model' <<<"$output" ! grep -qxF -- '--spec-draft-model' <<<"$output"
grep -qE '^SPEC_TYPE=none$' "$ENV_FILE"
} }
@test "naming a drafter ships the MTP set, with or without a separate draft file" { @test "an external drafter still reaches the engine as a complete set" {
# The documented MTP arm (frank-zealot.env): SPEC_TYPE=draft-mtp ALONE is # The other arm the ROCmFPX-era crash was about: a named draft FILE must
# legal here because the head is IN the checkpoint and the draft context # arrive alongside --spec-type and the depth recipe, never alone. This
# rides the main model's weights — so --spec-draft-model must NOT appear, # endpoint does not use it (the head is in the trunk), so the knob ships
# or the trunk would be loaded twice. # empty and only this test sets it.
serve_argv serve_argv
{
echo 'SPEC_TYPE=draft-mtp'
echo 'SPEC_DRAFT_N_MAX=2'
} >> "$STAGE/test.env"
run_stage
[ "$status" -eq 0 ]
[ "$(argv_value --spec-type)" = "draft-mtp" ]
[ "$(argv_value --spec-draft-n-max)" = "2" ]
! grep -qxF -- '--spec-draft-model' <<<"$output"
# And an external drafter still reaches the engine as a complete set — the
# pair the ROCmFPX-era crash was about.
echo "SPEC_DRAFT_FILE=$MODEL_DIR/$DRAFT_STUB" >> "$STAGE/test.env" echo "SPEC_DRAFT_FILE=$MODEL_DIR/$DRAFT_STUB" >> "$STAGE/test.env"
run_stage run_stage
[ "$status" -eq 0 ] [ "$status" -eq 0 ]
[ "$(argv_value --spec-draft-model)" = "$MODEL_DIR/$DRAFT_STUB" ] [ "$(argv_value --spec-draft-model)" = "$MODEL_DIR/$DRAFT_STUB" ]
[ "$(argv_value --spec-type)" = "draft-mtp" ]
} }
@test "vision ships by default and MMPROJ_FILE empty opts out of the image path" { @test "vision ships by default and MMPROJ_FILE empty opts out of the image path" {