All ~173 managed skills read in full and merged, one polished reference page per skill-cluster: - LLM cross-cutting (17 pages): GGUF Compatibility, Llama Cpp Serving Sizing, Llama Cpp Multi-Device Splits, PCIe Link and Atomics, RDNA4 Container Serving, RDNA4 MoE Expert Offload VRAM Budget, vLLM Boot Forensics, vLLM Porting and Compile Triage, LLM Bench Windows, LLM Performance Diagnosis, Model Swap and Rollback, Tcclaviger Fork Ops, Heterogeneous Split Triage, FreeToken Builds + deltas folded into the retired runbooks (GLM DFlash2 port laws, Lucebox dual-R9700, R9V qualification pipeline, FLM pack registration, ik hybrid envelope, frank-medium split rebalance, llama.cpp GGUF preflight, batch-ubatch split section, bench-window D-state/stale-read notes, second-endpoint bring-up). - Architecture (3): Stale Signal Audit, Quadlet Lifecycle, Deploy State Triage. - Operations (6 + index): Remote Job Runner, Payload and Disk Reclaim, Container Host Reachability, Fleet Ops Playbook, Fleet Audit and Posture, SSH and Remote Access. - Infrastructure: Observability Provisioning + Homepage Status Probes; updates to Authentik, Anubis, LAN-DNS (per-client DoH bypass, Porkbun zone ops), Swap Configuration, OpenWebUI, Hermes (pre-existing page preserved and extended). - FrankBook-Pro: ARR-Stack filled (API split, schema-first probing, auth-posture, auth-config trap); qBittorrent E2E verification. - Desktop/Agent/Research: Noctalia and Niri, OMP Harness, Agent Docs and Edits, Command Center Dotfiles, PICO-8 Asset Standards. - Indexes: LLM Cross-Cutting References section, Operations/ and Agent/ index pages, root-index Skill Distillation section, AGENTS.md pointer. - log.md carries the wave plan. check-vault-links.py 0 unresolved (185 pages); ./tests/run.sh 366/366.
15 KiB
type, title, description, tags, timestamp
| type | title | description | tags | timestamp | ||||||
|---|---|---|---|---|---|---|---|---|---|---|
| reference | LLM Bench Windows and Measurement | Stopping-and-restoring benchmark windows on the fleet LLM hosts (llama-bench sweeps, HTTP param benches, multi-arm vLLM offload sweeps), honest benchmark methodology, and the usage-counter measurement law under speculative decoding. |
|
2026-10-05T00:00:00-07:00 |
LLM Bench Windows and Measurement
Fleet law first: STOP the production service before any GPU experiment
(AGENTS.md Benchmarking Policy) — a bench beside a live service starves the
GPU and corrupts results; an unisolated GPU experiment has precedent for
wedging a host until power cycle. Reference harnesses:
scripts/bench-batch-ubatch.sh, scripts/autoresearch.sh,
scripts/autoresearch/frank-primary-arms.sh.
1. The window structure (one config per arm, a driver owns the window)
- Stop + gate before every arm —
wait_for_clear: unit NOT active AND no RUNNING container of that name (podman ps, not-a;-acounts EXITED containers and can never clear —rm -fthe exited one first) AND port free (ss -ltn). The graceful-stop race cost a whole arm 2026-10-01:ExecStop=podman stop -t 120drains up to 2 min; a driver that sleeps 3 s then polls/healthgets 200 from the still-running PRODUCTION engine — the arm "passes" with production's boot log, thenpodman execdies withcontainer state improper. - Boot the arm as the service, not a scratch port (no scratch ports on
LLM hosts — one card, one server). The serve script takes its config via
env-file override (
FRANK_SMALL_ENV_FILE,FRANK_PRIMARY_AR_ENV_FILE) pointing at a staged copy; the checkout is never mutated. - Fast-fail the health wait: poll
podman ps | grep -qx <name>inside the loop — a refused boot dies in one poll instead of 20 min. Capture the serve script's FULL stdout on failure (atail -8loses engine-init root causes; raise happens above the tail). - Measure (§4), capture boot-log engine facts (KV pool, arena slots, SpecDecoding lines) — they survive measure crashes.
- Teardown + converge: kill by explicit PID/pgid (never pattern kills),
podman rm -fscratch, waitMemAvailable ≥ ~230 GiBon dankserver before the next boot; restore production from the EXIT trap and verify the conjunction oracle: health 200 +is-active+is-enabled+ mask-symlink clean. A baresystemctl startis not a restore oracle. (systemctl maskFAILS on a relinked unit — log it, explicit stop is enough: systemd never auto-restarts an explicitly stopped unit.) - Warm boots cycle ~2 min (page cache), cold ~8 min — size timeouts for mixed speeds.
Destructive tails (2026-10-01): a scratch script whose tail does
systemctl stop + podman rm -f <PRODUCTION container> fires later if a
probe loop survived the driver kill — it can destroy a just-deployed
production engine (signature: deploy exits green, then seconds-to-minutes
later Exited (0), Deactivated successfully, clean exit = removed, not
crashed). Before ANY deploy: pgrep -af every scratch script of the session
and kill strays by explicit PID; killing the dev-side job does NOT kill the
host-side deploy-remote.sh (two of them once managed the same unit).
Two failure shapes inside the window that read as "the bench died":
- Stale reads from a stopping container.
podman stopanswers while the engine inside is still flushing; apodman logstaken in that window returns the boot-prefix only. Pollpodman psforExited (0)(orpodman wait) before reading logs, or you will write "no results" into the table for a run that has them. - A D-state worker wedges the stop. A bench child that wedged in the
kernel ignores SIGTERM, so
podman stop -t Nburns its full timeout and then the container isStoppingforever. Kill by container first (podman kill <ctr>→ SIGKILL path), verify the process is gone frompodman psANDnvidia-smi, and only then re-serve. Never let the window script proceed to a redeploy with a zombie bench worker holding VRAM.
2. llama-bench sweeps (offline batch/ubatch/split/KV/depth)
What llama-bench cannot express — decide BEFORE designing: no
--ctx-size (n_ctx = n_prompt + n_gen + n_depth, so a bench can NEVER prove
activation buffers fit beside the DEPLOYED KV — only the real unit answers
that), no --parallel (no slot shape, no concurrency), no --spec-*
(tg numbers are the drafterless baseline, useful as a control only), and a
warmup run is itself a full-size run. -d N is a depth PREFIX (paid once per
cell via a llama_state_seq_* save/restore); without it a tg run is at
~zero depth and meaningless for long conversations.
The separator trap: / separates devices WITHIN one configuration, ,
separates sweep values — for both -dev and -ts. -ts 36,29 parses as the
single value 36 → everything on one card. --tensor-split 36,29 (server) →
-ts 36/29 (bench). Ranges exist: -ub 512-2048+256.
Harness shape: one invocation per cell (an infeasible cell calls
exit(1) and kills every later cell of a cross-product invocation — and
infeasibility is usually the question); TSV row per cell as it completes +
bench.json (-o json) + bench.log (--progress) + a 2 s rocm-smi --showmeminfo vram VRAM trace per cell (a cell that allocated less than
intended looks fast); resume by skipping non-empty cell outputs; stop the
service in the harness, restore from trap … EXIT, verify afterwards. Match
the deployment's load flags exactly (-ngl -sm -ts -fa -ctk/-ctv -t).
Run under a transient unit, not a shell:
sudo systemd-run --unit=<name> --collect --no-block --property=Type=oneshot \
--property=LimitMEMLOCK=infinity --property=User=dank \
/home/dank/serverAppConfigs/scripts/<harness>.sh
Deploy units set LimitMEMLOCK=infinity for ROCm staging; an SSH shell has
8 MiB and fails the load for an unrelated reason. This also survives ssh
disconnects. Boot rungs under the production environment, not a bench
shell — a rootless shell probe booted 3×262144 bf16 that production refused
(systemd User= + memlock change what the pinned-expert path holds):
sudo systemd-run --uid=<user> --unit=<name> -p LimitMEMLOCK=infinity -p Restart=no env VARFILE=… /path/serve.sh.
Verify before burning hours: dry-run against a STUB binary with
SERVICE=<nonexistent-name> (the trap/stop never reached), tiny grid,
including one forced-exit 3 cell — prove the TSV row, failure capture,
resume logic. Validate the real flag set in ~1 s by pointing -m at a
nonexistent file. Reading: bracket the sweep (reference cell first AND
last); check per-cell stddev_ts (a difference smaller than spread is not a
result); single-sequence decode is usually flat across batch/ubatch (prefill
knob); batch ≥ ubatch (llama.cpp clamps ubatch to batch, silently duplicating
smaller-batch cells); report what the grid cannot prove (drafter, slots, real
context).
3. HTTP benches of server-side parameters
llama-bench benches the bench binary, not the running server — server-side
params (--threads-batch, --ubatch-size, --spec-type, --parallel) need
HTTP:
- Prefill: large prompt,
max_tokens=1(server crashes on 0), measureprompt_tokens / wall_time. - Decode:
max_tokens=16384minimum, NEVER 1 (warmup, cache growth and spec acceptance all stabilize over thousands of tokens); measurepredicted_per_secondfromtimingsorcompletion_tokens / wall_time; temperature 0.
Known fleet harnesses: scripts/bench-threads-batch-http.py,
scripts/bench-decode-threads-batch.py, scripts/bench-spec-decode.py.
sglang-specific: max_tokens=1 on /v1/chat/completions with the qwen3
reasoning parser streams zero deltas — use /v1/completions.
4. The measurement law (under speculative decoding this is absolute)
Never count SSE chunks. Rejected drafts break the 1:1 chunk↔token mapping; chunk counting reported 8.3 tok/s for a config whose server counters said 23–26 — a 3x false regression that nearly abandoned the change that worked. Always:
decode_tok_s = usage.completion_tokens / (wall - ttft)
# stream=True, stream_options: {include_usage: true}; ttft = first content-bearing chunk
vLLM's 10 s Accepted throughput log lines are a rolling window, not a
measurement (6.2 → 13.1 across adjacent windows on identical settings) — use
them for acceptance STATISTICS only (Avg Draft acceptance rate, Mean acceptance length, per-position rates). Warm pass + ≥3 runs, median;
temperature 0; ≥256 tokens. A decode "jump" with early-stopped completions
(ct < 256) and acceptance > 90 % is degenerate generation (the fork's
repetition-halt fired at ~140 tokens on corrupted numerics) — inspect text
before believing a win. Run-to-run spread is 5–10 %: repeat arms; one sample
nearly shipped the wrong setting.
5. Honest methodology
- Match the corpus to the workload. Token soup under-reports speculative
decoding (2.9–3.6 tok/update vs 3.86–5.8 on real code) and mis-ranks draft
settings; acceptance, not step time, moves throughput. Build the corpus
from the repo's own sources:
{"prompt": "<instruction>\n\n<file body>"},--custom-output-len. - Salt every run with a unique per-arm token in the first bytes — it is the prefix-cache key (and a proxy routing key). Without it arm 2 measures arm 1's warm cache.
- Two workload shapes need two clients: distinct prompts (batch behaviour, prefill parallelism) and sequential turns with growing history (TTFT of the first streamed chunk per turn — the only measurement that exposes prefix-cache behaviour). These pull in opposite directions: a cold batch spreads prefill across replicas, a conversation's next turn only pays if it lands where its prefix is cached.
- Read the engine's ledger, not the card:
Available KV cache memory/GPU KV cache size: N tokens/Maximum concurrency for M tokens per request: X.Xxis the capacity number. vLLM'sPrefix cache hit rate:line is a LIFETIME average across every prompt the replica ever saw — send the same prompt twice to an idle engine and compare TTFTs (measured 22.3 s cold → 1.4 s warm for a 50k prefix).GPU KV cache usagesays the pool is not full; it does NOT say a prefix survived. The KV budget is NOT monotonic in chunk size (5.44 GiB at CHUNK 8192, 4.22 at 16384, 4.16 at 4096) — measure three points before treating a knob as a lever. - Soak before believing: 15–30 min of concurrent families, count failures AND latency; report hit/miss split and medians with min/max — the median of a bimodal distribution describes neither mode.
- Verification that is not the benchmark: run the client the fleet actually uses; and a deployed unit is not proven until an unattended reboot brings it back.
- Record the "no" answers with numbers and mechanism — placement balance, concurrency admission and chunk-size tweaks all measured flat or worse here; the wall was arithmetic (cached prefixes + live sequences share one pool).
6. Sweep hygiene (the arms that produced wrong verdicts)
- Prefetch uniformity: every arm in a sweep carries the same expert-prefetch setting. The 2026-09-29 campaign measured losing arms with prefetch ON and incumbents OFF; untaxed, verdicts inverted (no-spec 12.11 → 41.47; gather 32×64 parity → +18.7 %). Never re-read a taxed verdict.
- Knob-landed preflight: for every
-eknob, confirmpodman exec <ctr> printenv <VAR>— a knob the fork silently ignores produces a "parity" arm that is really the default (v1 of the gather sweep lost all 3 arms this way; serve-script hooks are additive and can drop vars). - Always run an explicit OFF arm: the 2026-09-29 campaign's only win was
deleting a baseline's silent
--expert-prefetch 1(+40–51 %). Challengers compare against default-off, not "incumbent + one flag". - Pin the capacity bar:
--kv-cache-memory-bytes(contract) rather than autofit (remainder) so pool numbers are reproducible; compute need = tokens × measured B/token (hybrid models: KV dtype does NOT double mamba states — measure, don't assume 2x). - Report decode AND prefill (TTFT) per rung: a decode-only view nearly deployed a 2x-prefill-tax config. Per-depth tables, never a single median — one fork's gather win grows with depth (54 t/s @1k, 39.5 @short).
- Container recreate (
podman rm -f) wipespodman cp'd harnesses — re-copymtp_measure.py/bench_iter.pyafter EVERY recreate, or mount them.
7. Bash laws that ate windows (each cost an arm)
${VAR:+SPEC=$1}in front of a command expands to a bare word →SPEC=3: command not found, nothing runs. Pass literal assignments:SPEC="${1:-}" bash serve.sh; the callee's${SPEC:+--flag}skips empty.${2WAY:-0}is fatalbad substitution(digit-leading name), and inside an if/else it aborts ONLY that compound statement — the script CONTINUES, every arm tears down clean with zero measurements. Detection law:lsthe per-arm.measure.txtfiles before trusting any summary; arm echoes are not evidence.local a=$1 b=$xexpands$xbefore the builtin assigns — underset -uit aborts AFTER the harness stopped production. Two statements.sed -irenames the file; already-running bash keeps the OLD inode — mid-flight patches miss arms launched in the preceding minutes. Patch early or run a make-up window.- Never heredoc python through ssh→zsh→bash — write to a file, ship it
(
ssh host 'cat > /path && chmod +x /path' < local.sh); nested quoting fails silently. Same trap ingit commit -m: backticks become command substitution. - No comments INSIDE a backslash-continued command (the continuation joins
the comment); verify multi-line command construction with
bash -nAND a render test.
8. Results location and recovery (frank-primary arms)
| copy | path |
|---|---|
| per-arm evidence | $ARMS_DIR = ~/.jobs/<run-stamp>/ (.rc, .done beside it) |
| summary | $ARMS_DIR/summary.tsv = arm rc decode_tps decode_tps_t0.7 kv_tokens boot_s + <arm>.out/.env/.overlay/.rc/.metrics |
| server-side last window | /home/dank/.autoresearch-frank-primary/run.json |
| durable memo | .../last/run.json + last/hash (promoted only after bench AND verified restore) |
| fallback | ~/.omp/agent/sessions/<ws>/<session>/<N>.{shake,bash}.log (permanent) |
/tmp/frank-primary-arms-* and /tmp/armstub/ are stub self-tests —
constants 99.9/88.8 identical across arms by construction; identical
numbers across arms == stub or bug, never a measurement. After compaction,
re-harvest with grep -rhoE 'decode_tps[a-z0-9_.]*=[0-9.]+' over the session
logs. A window costs ~20 min warm; a detached runner's timeout ≥ 3600 s per
window.
Related
- RDNA4 MoE Expert Offload VRAM Budget — the plan arithmetic being swept
- DFlash DSpark Speculative Decoding — spec A/B protocol and shapes
- LLM Performance Diagnosis — profiling and attribution methods
- Remote Job Runner — detached execution of long sweeps
- frank-medium-llamacpp-dflash2 — the measured ubatch-optimum history