Commit Graph
654 Commits
Author SHA1 Message Date
Frank d8835e69c6 docs: six observability units across two hosts, and what the reboot cost
The page said "all five are rootless Quadlet units on Dankclaw", which stopped
being true when Dankserver's node-exporter was restored to a unit. It now names
both hosts and both deployments, records why the second one happened (the same
compose-only reboot argument, a day and a half of missing host metrics, and a
wildcard bind), and states the exposure rule for a two-listener fleet with the
`ss` evidence that the wildcard is gone.
2026-09-16 07:09:55 -07:00
Frank da9cff046e observability: give Dankserver its node-exporter back as a Quadlet
Dankserver's node-exporter has been dead since a reboot: `podman ps -a` showed
it `Exited (143)` and Prometheus' `node-dankserver` job has been scraping
nothing. It ran only from `dankserver/compose.yaml`, which is the failure mode
this repository's boot-persistence rule exists for (rootless Podman does not
restart bare containers after the stop that shutdown performs), and the compose
form bound the wildcard address — the unauthenticated-metrics-API-on-every-
interface posture already removed on Dankclaw.

  - dankserver/observability/quadlet/node-exporter.container — mirrors the
    Dankclaw unit, listening on exactly 127.0.0.1:9100 (host-local probes) and
    192.168.0.235:9100 (Prometheus on Dankclaw), with Restart=always and
    WantedBy=default.target.
  - the compose service is deleted: one owner per container name.
  - deploy_observability now installs from "$TARGET/observability/quadlet"; it
    hardcoded the Dankclaw path, so a Dankserver unit could not be installed by
    the shared dispatcher at all.
  - planner: "node-exporter" joins the Dankserver service set, with a path rule
    for dankserver/observability/**; the dispatcher's allowlist gains
    dankserver:node-exporter.

Seams cover the silent halves: both listen addresses present and the wildcard
absent, the compose service gone, the allowlist entry present, and the planner
mapping the path to the service.
2026-09-16 07:07:18 -07:00
Frank d104eef4e0 bench: commit the harnesses the frank-medium numbers came from
Every measurement behind the frank-medium runbook was taken with scripts that
lived in /tmp on two hosts — reproducible only until the next reboot, and
invisible to anyone reading the repository. This moves the useful four into
scripts/, where this repo already keeps its bench harnesses:

  bench-vllm-corpus.py    code-shaped corpus from the repo's own sources, because
                          synthetic random prompts misreport draft acceptance
                          badly (2.9-3.6 tok/update against ~5.8 on code)
  bench-vllm-serve.sh     concurrency sweep; encodes the two traps that produced
                          silent garbage — the tokenizer argument, without which
                          the client dies resolving "frank-medium" on HF and
                          reports no numbers at all, and the --ignore-eos
                          semantics that must not be compared across
  bench-turn-latency.py   per-turn TTFT for an agent-loop shape; the instrument
                          that decided the routing policy, and the one that shows
                          cold-vs-warm prefill directly
  bench-llm-soak.py       sustained multi-family load; what found the capacity
                          wall (three 30k-token families, 75% cold re-prefills,
                          zero failures)

Each header carries its traps and the interpretation; the runbook's Benchmarks
section now points at them instead of describing methodology with no tooling.

Also adds the missing half of the script seam: scripts/*.py are compiled, not
just scripts/*.sh syntax-checked — a harness with a syntax error could otherwise
ship unnoticed until someone needed it during an incident.

Both python harnesses were smoke-tested against the live fleet before commit:
the corpus builder wrote 15 prompts, and the turn harness measured 12.5 s cold
against 1.4 s warm on the same prompt — the cache behaviour the runbook claims.
2026-09-16 06:59:44 -07:00
Frank ef7755f2b3 chore: retire the unreferenced ComfyUI Containerfile
dankclaw/comfyui/Containerfile is referenced by nothing — compose builds from
dankclaw/Containerfile.comfyui, deploy-remote.sh copies that one, and the
ComfyUI doc names it — and its content is an older iteration (torch
2.9.1+rocm7.12.0rc1 against the deployed gfx1151 base). It also caused real
damage: the comfyui test's directory-listing selector picked non-comfyui files
and reported a red suite for sessions.

Staged in .AGENTTRASH/ rather than deleted, so it stays recoverable and visible
until Frank decides it can go for good. This is also why the earlier test fix
had to resolve the Containerfile from compose rather than from the filesystem:
the directory is not the contract.
2026-09-16 06:57:25 -07:00
Frank 3d5b1f44a6 tests: fix the comfyui Containerfile test instead of living with it red
This test failed on a clean tree for many sessions, and the working belief
became "test 73 is a known failure, ignore it" — which is exactly what a
permanently red suite teaches. The cause was a selector bug: `find
dankclaw -name 'Containerfile*' | head -1` returned whichever Containerfile the
directory listing happened to put first (dankclaw/alloy/Containerfile), so the
assertion read a file that never claimed to be ComfyUI's. It would equally have
passed on a wrong file.

The test now resolves the Containerfile the way the build does — build.context
joined with build.dockerfile, both read from dankclaw/compose.yaml — so it
checks the file the deploy actually builds (dankclaw/Containerfile.comfyui, the
one compose, deploy-remote.sh and the ComfyUI doc all name) and follows a future
rename instead of guessing.

Suite: ALL TESTS PASSED (310), no exceptions to explain away.
2026-09-16 06:57:22 -07:00
Frank 56edfabb19 homepage: the Dankserver LLM card was labelled with a retired name
The card probing 192.168.0.235:7770 — frank-medium's endpoint since the
radiance cutover — was still called "frank-small", a service retired on
2026-09-15. The probe target was right; only the identity was wrong, which is
the worse half: a green card named after a service that no longer exists teaches
the wrong thing about the fleet. It is now frank-medium, described as what it
actually is (Qwen3.8-27B Uncensored MXFP4, vllm-radiance, two R9700s behind one
endpoint), and frank-large's description stops crediting the retired halogen
runtime for work ik_llama.cpp does.

The seam added to homepage_config.bats pins the cross-file contract that let
this drift: a card's probe target and its name must agree with the fleet, and a
retired service must not keep a card.
2026-09-16 06:53:20 -07:00
Frank 823814920b docs: record the alerting rules and what they still cannot do
Both pages claimed there was no alerting; there are six rules now, so they say
which rules exist, that promtool and /api/v1/rules both confirm them, and — in
the same breath — that no rule reaches a person, because this stack has no
Alertmanager and no contact point. The dashboard's ALERTS panel is named as the
only place an alert becomes visible without opening Prometheus.
2026-09-16 06:51:23 -07:00
Frank af05c890fd observability: LLM alerting rules, evaluated but not yet delivered
The scraped metrics have no rules over them, so every graph was read after the
fact by a human. This adds shared/prometheus/rules/llm-fleet.yml, loaded via
rule_files and installed by the same copier that ships prometheus.yml (a rule
file nothing loads evaluates nothing, and Prometheus reports no error for it):

  LlmKvPoolSaturated      kv pool > 90% for 10m — the 122,636-token wall
  LlmRequestsQueueing     waiting > 0 for 10m — MAXSEQS=4 reached
  LlmPrefixCacheCollapse  reuse < 20% for 15m with traffic — the thrash signature
  LlmEndpointDown         scrape target down 2m — both replicas gone
  LlmLatencyHigh          p95 end-to-end > 60s for 10m — what an operator feels
  job:llm_prefix_cache_reuse:rate5m — recording rule shared by the alert and the
                          dashboard, so the two cannot drift

Every threshold is tied to something measured on this fleet, not to a guess,
and the file says plainly at the top that **nothing is notified**: there is no
Alertmanager here, so a firing alert reaches Prometheus' /alerts page, the
ALERTS metric and the dashboard panel added alongside it — and nowhere else.
Choosing how Frank actually gets paged is his call, not a missing config line.

Seam covers the silent failure mode: rules exist, the config references the
directory, the deployer installs it, and the dashboard graphs ALERTS.
2026-09-16 06:45:03 -07:00
Frank c272407e6e docs: observability state after the Grafana provisioning
The page said Grafana has zero datasources and zero dashboards; that is now
false, so it says what exists (Prometheus datasource, LLM fleet dashboard), how
it was verified (the API — a provisioned dashboard does not show up in the
legacy dashboard table on this Grafana version, which is a trap worth recording)
and what is still missing (nothing alerted on, Loki unread, correlation manual).
The section heading drops "UNCONFIGURED" because it no longer is, entirely.
2026-09-16 06:42:24 -07:00
Frank 6a191430ca observability: provision Grafana from git — datasource plus the LLM dashboard
Grafana has been running with zero datasources and zero dashboards, so the
metrics scraped last commit were only reachable through the Prometheus API.
This wires the plumbing and one dashboard, both declared in
shared/grafana/provisioning/ and installed to /opt/grafana/provisioning by the
deployer (the unit already mounted that directory read-only, so nothing about
the container changes):

- datasources/prometheus.yml — Prometheus at http://prometheus:9090, the
  container name, not the loopback publish, with a fixed uid so dashboards
  reference it without a lookup.
- dashboards/llm-fleet.json — KV pool fill, queue depth, prefix-cache reuse
  rate, e2e latency p95, and decode/prefill throughput, with an endpoint
  variable covering both frank-medium and frank-large. Every series name was
  checked against the live /metrics endpoint: this fleet already shipped one
  comment naming a metric that does not exist, and a dashboard full of those
  would be worse.

Dashboards stay editable in the UI (allowUiUpdates) because the panels are a
starting point, but the file is the source of truth and the deployer
overwrites Grafana's copy on every deploy.

Planner and seams follow the existing pattern: shared/grafana/** selects the
grafana service, and the tests pin the container-name address plus the
provisioning contract so a rename cannot silently empty the dashboard.
2026-09-16 06:39:15 -07:00
Frank 3037240f4f docs: point the frank-medium runbook and observability page at the new scrape
The capacity work in this runbook was done with ad-hoc probes; the same signals
now have history in Prometheus, so the page says which series to watch and
admits the honest gap — this stack has no alerting, so a saturated pool shows up
on a graph after the fact rather than paging anyone.
2026-09-16 06:37:39 -07:00
Frank 0f291115bd observability: name the prefix-cache series that actually exist
vllm:prefix_cache_hit_rate does not exist in this build; the exported pair is
vllm:prefix_cache_hits_total / vllm:prefix_cache_queries_total, whose rates give
the hit rate. A comment naming a series nobody can query is worse than no
comment.
2026-09-16 06:34:53 -07:00
Frank 42b47063ec observability: scrape the LLM fleet into Prometheus
Neither LLM endpoint was scraped, and frank-medium exports 418 vllm: series —
including the ones this fleet argues about under load: kv_cache_usage_perc (how
full the per-card pool is), prefix_cache_hit_rate (whether pinned conversations
are being reused), num_requests_waiting (queue depth) and the request-latency
histograms. Everything diagnosed tonight by hand — 122,636 KV tokens per card,
thrash when three large contexts overlap, cold-vs-warm prefix reuse — is a
graph now, on the LAN addresses rather than the guarded vhosts.

frank-large comes along in the same change: llama.cpp exposes throughput
counters under llamacpp:, and watching one LLM endpoint without the other was
arbitrary.

The seam added to observability_quadlet.bats pins the cross-file contract that
breaks silently: if a scrape target drifts from the :7770 the units publish,
the graphs vanish with no error anywhere.
2026-09-16 06:32:33 -07:00
Frank 4f2f49d2d0 frank-medium: measure admission control and cache health, keep neither change
Two more hypotheses, both measured on the same three-family workload (one
~80k-token family plus two ~50k-token families, each turn re-sending its
document):

1. Long-prefix caching is fine. One card, idle engine, a ~50k-token prompt
   sent three times: 22.3 s cold, then 1.4 s, 1.4 s — with a varying tail too
   (22.4 / 1.41 / 1.41). So the engine's lifetime "Prefix cache hit rate: 29%"
   is an average across every prompt that replica ever saw, not a verdict on
   any one request, and the pinning policy is safe.

2. Serialising large requests does not fix the overload. Admission control
   (at most one >16k-token request per card, small requests untouched) gave no
   clear win on small samples — heavy 25.4 s -> 48.2 s median, mediums
   34.7/45.2 s -> 22.7/22.6 s, cache-hit rates 14-33 % both ways — so it was
   reverted like the placement change before it.

The arithmetic now closes: with three families on two cards, one card holds two
families and must carry 2 x 50k of cached prefix plus the 50k of the sequence
generating — 150k against 122,636 tokens. Those co-tenants evict each other
regardless of concurrency limits. The wall is capacity, and the runbook says so
with the numbers.
2026-09-16 06:31:01 -07:00
Frank 36007666cf frank-medium: measure load-balanced placement, find it does not help, revert
The obvious next lever for cache thrash was placement: send a new conversation
to the card carrying less context instead of alternating. Implemented it
(decayed prompt-token estimate per replica, continuations still pinned) and
A/B'd it against the counter on the workload it targets — one ~80k-token
family plus two ~50k-token families, each turn re-sending the same document:

  counter          heavy 13 turns median 25.4 s (23% cached)
                   medium 34.7 s / 45.2 s (14% / 15% cached)
  balanced by load heavy  5 turns median 46.7 s (20% cached)
                   medium 23.4 s / 34.3 s (0% cached, small sample)

No improvement, so the change is reverted and routing.py keeps the counter.
The reason is arithmetic: a running request's live sequence and its cached
prefix draw on the same pool, so a card running two 50k-token generations has
already spent its 122,636 tokens before any cache entry is considered.
Placement chooses which card is over capacity, not whether one is.

Third hypothesis measured and rejected tonight (with SPEC depth kept and CHUNK
disproved); the runbook records all three so the next operator starts from
evidence rather than from these ideas looking obvious.
2026-09-16 06:20:19 -07:00
Frank 99ed1aca9a docs: reboot-tested the frank-medium trio
Rebooted dankserver unattended and verified without any manual step: all three
units active 59 s after boot, both replicas healthy ~3.5 min after boot, real
completion through :7770 — the failure mode this repo has been bitten by before
(a service that only ever ran from compose and stayed dark after a reboot).
Records why it works: [Install] WantedBy=default.target wired into the
generator's default.target wants, plus Linger=yes, plus a disk-backed compile
cache so a cold boot needs no warm-up beyond the model load.
2026-09-16 06:00:15 -07:00
Frank 24687f561f docs: frank-medium capacity budget and the CHUNK sweet spot
A 25-minute soak with three concurrent large-context agent loops (each turn
re-sending a ~30k-token document, growing history) came back clean — 155
requests, 155 answers, no errors, no failover, both replicas steady — but only
25% of turns hit the prefix cache, the rest paying a 13.9 s median re-prefill.
That is the capacity budget showing: 122,636 KV tokens per card is about two
30k-token conversations with their cached prefixes, and a third family on the
same card evicts.

Chasing the obvious lever disproved it. Available KV per card is 5.44 GiB at
CHUNK=8192, 4.22 GiB at 16384 and 4.16 GiB at 4096 — where the engine refuses
to start. The budget is not monotonic in the chunk size, so 8192 is a sweet
spot rather than a compromise and freeing KV needs another card, not a knob.
2026-09-16 05:49:54 -07:00
Frank d8189b65f4 docs: record the proxy's failover behaviour and how to verify it 2026-09-16 05:29:45 -07:00
Frank 3946ce9a3a frank-medium: fail over to the other replica instead of returning 502
The proxy trusted its health table, which the watcher refreshes every 10 s. A
replica that restarts — and a deploy restarts both — therefore produced a
window in which requests routed to the dead card got a 502 back, even though
the other card was serving the whole time.

A failed connection now demotes that replica immediately and retries the
request on another live one, before anything is written to the client.
vLLM's OpenAI endpoints are stateless, so re-sending a request that never
reached an engine is safe; a failure after streaming has begun is still not
retried. The failover is logged, because a silent one hides a card that is
down.

Measured with card 1 stopped mid-flight: six requests, six 200s, one failover
line in the log, no client-visible error. Before this commit that burst
returned 502s.
2026-09-16 05:29:08 -07:00
Frank 4d37f8ca65 frank-medium: measure speculative depth on the repo's own code corpus
Upstream ships dflash depth 7 as a content-dependent default measured for TP=2
on their corpus. Re-measured on one card here, two runs per setting, 16
repo-derived prompts at 512 output tokens:

  SPEC=7  conc1 46.2 tok/s, TPOT 20.8 ms, 3.86 tok/update
          conc4 118.7 / 127.0 tok/s
  SPEC=5  conc1 41.5 tok/s, TPOT 23.1 ms, 3.41 tok/update
          conc4 136.1 / 129.4 tok/s

Step time is nearly identical at both depths (~79 ms vs ~80 ms single stream):
7 wins by landing more tokens per step, 5 wins at conc 4 by ~8% because more
narrow verify steps pack better. Depth 7 stays — this fleet is interactive
agent loops where per-turn latency is what is felt — and the runbook now
records the numbers and the one-line switch for a batch-first workload.
2026-09-16 05:23:09 -07:00
Frank 8b79766d81 docs: record why CHUNK stays at 8192 on the single-GPU replicas
Raising the prefill chunk to 16384 grows the arena enough that the KV cache
falls to 4.22 GiB against the 5.21 GiB a 114688-token context needs, so the
engine refuses to start. Measured single-card prefill at 8192 is ~2.5k tok/s
(~15k-token prompt, 5992 ms TTFT); the trade would mean cutting context for
unmeasured prefill gain, so the default stands.
2026-09-16 04:58:47 -07:00
Frank 83625e84a7 frank-medium: route by workload — spread batches, pin agent loops
The two replicas behind :7770 were on plain round-robin, which throws away
prefix-cache hits: an agent loop re-sends its system prompt and file context
every turn, and alternating replicas means every other turn pays a cold
prefill. Measured with 90k-token contexts, warm-turn TTFT was 2676 ms.

Pinning everything to one replica is not the answer either — a cold batch
(the shape a benchmark or a burst of parallel subagents produces) then loses a
card's worth of prefill: 16 requests took 30.1 s pinned against 18.6 s spread.

So the policy routes by request shape. A continuation (more than one message —
the agent-loop signature) is pinned to the replica that served its prefix;
anything else spreads across the cards by counter. Measured: 19.97 s for the
burst and 1095 ms warm-turn TTFT — the best of both, without a regression on
either workload.

The policy lives in proxy/routing.py, separate from the HTTP plumbing in
proxy/affinity-proxy.py, so it can be tested without aiohttp, a network or a
GPU. tests/frank_medium_proxy.bats covers it, and it earned its keep: the
first version hashed fresh prefixes to pick a card, which sends every member of
a same-prefix burst to one card (29.8 s, measured), and a stale index in the
pin path returned 500 on every continuation. Both are now assertions.

ROUTING=roundrobin and ROUTING=affinity stay available to re-measure the
choice on this host; the default is adaptive.
2026-09-16 04:45:21 -07:00
Frank 7ce6e96bc5 docs: record the KV_MEM=0 passthrough trap in the frank-medium runbook 2026-09-16 03:58:48 -07:00
Frank df260dd419 frank-medium: pass KV_MEM=0 through as --kv-cache-memory 0
The single-GPU replicas died with torch.OutOfMemoryError during KV
allocation (2.16 GiB requested, 116 MiB free, 29.34 GiB already held by
PyTorch) because the entrypoint blanked KV_MEM=0 into an omitted flag. The
image's patched KV sizing reads an explicit 0 as 'profile this shape'; with
the flag absent vLLM sizes the cache from --gpu-memory-utilization alone and
overshoots. Passing 0 literally is the shape the bench numbers were measured
on, and MAXSEQS gets its default back under set -u.
2026-09-16 03:54:18 -07:00
Frank 4fca739bf8 frank-medium: deploy two single-GPU replicas — TP=2 is blocked by PCIe atomics
The radiance MXFP4 stack cannot run TP=2 on this host. RCCL's collective
kernels carry a hidden hostcall-buffer parameter, and ROCr refuses to
dispatch them on a device without PCIe atomics: the AQL dispatch fails with
hipErrorIllegalState ("the operation cannot be performed in the present
state"), which is what every two-rank collective hit. The second R9700 sits
behind the chipset bridges 05:00.0/06:09.0, which advertise
`AtomicOpsCap: Routing-` — a read-only capability, so no software can enable
atomic routing on that path and the driver's
pci_enable_atomic_ops_to_root() fails for that card. The first card, on CPU
root port 00:01.1, has atomics enabled.

Ruled out: kernel version (7.0.0-28/-30/-31 identical, after a full upgrade),
iommu=pt, P2P hardware (45.3 GB/s between the cards), the shipped librccl (a
from-source rebuild reproduces the failure at the same call path), vLLM and
pynccl (a bare two-rank torch.distributed allreduce fails the same way), all
NCCL transport knobs, and single-rank tests (NCCL short-circuits at
world_size=1, so one-card success proves nothing).

Deployment: three rootless Quadlets — frank-medium-a (:7771, card 1),
frank-medium-b (:7772, card 2), both TP=1 at 114688 context (262144 and
131072 do not fit one card's KV) with vLLM-profiled KV — and
frank-medium-proxy (:7770), a round-robin that preserves the fleet contract
of one endpoint and one served model name. frank-medium.container stays as
the TP=2 target for a host whose second card is atomic-capable; the deploy
handler does not install it and trashes any stale copy.

Measured through the proxy (vllm bench serve, random dataset, --ignore-eos):
conc 1 42.8 tok/s, conc 4 147.6 tok/s, conc 8 230.8 tok/s, 32k prefill at
~2.8k tok/s per card, TPOT p50 23.6-28.8 ms.

Also: healthcheck reads PORT so one probe serves all three units, the hermes
example and the harness models.yml drop to the real 114688 context, and the
runbook records the full evidence chain.
2026-09-16 03:47:11 -07:00
Frank ad7a9c42f9 log: record the 2026-09-15/16 ik cutover and decode campaign 2026-09-16 02:44:21 -07:00
Frank 795b0b3a5e frank-large: document the ik_llama.cpp deployment and the decode performance campaign
- Runbook with the measured envelope (prefill ~192 t/s at the VNNI ceiling;
  decode ~3.8-4.3 t/s gated by 100 backend splits/token, GPU 95% idle), the
  root-cause evidence chain, tuning levers, and co-tenancy traps
- AGENTS.md: fleet topology row + Active Services section for the ik
  deployment; halogen demoted to rollback; bench policy unit type fixed
- LLM + root indexes relinked (frank-large-ik-uncensored ACTIVE,
  halogen-flash-server retired)
2026-09-16 02:42:41 -07:00
Frank 0c1e10a5ef frank-large: retire the halogen quadlet test seam (ik_llama.cpp cutover) 2026-09-16 00:38:34 -07:00
Frank f2277863ae docs: rewrite AGENTS.md — dedupe fleet history into lessons + runbook index
- 734 -> 412 lines; active topology table replaces the swap-history log
- retired-runtime detail collapsed into a runbook pointer table
  (serverAppConfigs-docs/LLM/ holds the full runbooks)
- halogen section condensed to invariants that survive config changes
  (pool/reserve matched pair, free-lies rule, client contract)
- new Key Lessons Learned section: ROCm/layer-split, HIP env collisions,
  mmap/PLE constraints, crash diagnosis, swap ordering, podman/quadlet
  gotchas, ops guards
- fixed the merged/broken row in the Summary table
2026-09-16 00:20:58 -07:00
Frank f7cda17518 frank-large: restore serve-script defaults eaten by the guard edit 2026-09-16 00:06:46 -07:00
Frank 0022ba96ad frank-large: mmap the CPU-overridden weights (no CUDA_Host pinning) 2026-09-15 23:58:34 -07:00
Frank fe3fc62c41 frank-large: swap halogen Quadlet for ik_llama.cpp (3090 + Strix Halo CPU path) 2026-09-15 23:20:19 -07:00
Frank 4183e31fd1 frank-medium: default RADIANCE_NORMQUANT_FUSION under set -u 2026-09-15 21:02:50 -07:00
Frank 6b647c1558 frank-medium: swap ROCmFPX llama.cpp runtime for vllm-radiance MXFP4
Rootless Quadlet on dankserver serving
just1moremodel/Qwen3.8-27B-Uncensored-MXFP4-awq (the radiance stack's
designated alternate target; ships its fp8 MTP head, no rewrite step) with
the DFlash2-FP8 drafter at SPEC=7 on the 2x R9700s (TP=2, host network,
:7770, alias frank-medium unchanged).

- dankserver/frank-medium/: quadlet unit, container entrypoint (fork of
  serve-mxfp4.sh's patch block at pinned clone commit 97353293), healthcheck
- dispatcher retires the root llama.cpp unit, re-tags the registry image to
  the localhost alias, installs the quadlet, waits on /health
- planner maps dankserver/frank-medium/* to frank-medium; scripts-era
  mappings removed
- ROCmFPX-era env/serve/unit/download scripts staged in scripts/.AGENTTRASH
  with .rocmfpx suffix; seams updated (frank_medium_quadlet.bats new)
2026-09-15 20:59:55 -07:00
Frank db29c4d227 hermes: sync sanitized config example to frank-medium cutover 2026-09-15 19:31:41 -07:00
Frank d4024a9180 docs: frank-medium runbook, fleet topology and client cutover
New runbook page serverAppConfigs-docs/LLM/frank-medium-rocmfpx.md records
the hardware swap, the self-contained gfx1201 build, the measured layer-vs-
tensor decision (llama-bench multi-device syntax gotcha included), the
tensor-mode draft abort, served-path numbers, and the post-cleanup host ROCm
posture. AGENTS.md, README, docs indexes, Dankserver System Specs,
deployment/Hermes/OpenWebUI/Plex/Stack pages follow the rename;
freetoken-frank-small is marked RETIRED and kingjones-q8agent SUPERSEDED.
Work log entry added.
2026-09-15 19:27:04 -07:00
Frank d53962fec5 frank-medium: serve the new R9700s, retire frank-small
The RTX 3090 left Dankserver; two AMD Radeon AI PRO R9700 cards (Navi 48,
gfx1201, 32,624 MiB each) replaced it. FreeToken/CUDA cannot run there at all,
so the `frank-small` service is retired and `frank-medium` takes the endpoint.

New service: charlie12345/ROCmFPX llama.cpp (built for gfx1201 against the
fork's self-contained ROCm 7.14 toolchain, so the host's stale /opt/rocm-6.3.3
never participates) serving kingjones777/Qwen3.8-27B-ROCmFPX-Q8_0-AGENT-GGUF --
a Q8_0_ROCMFPX_AGENT (ftype 115) checkpoint that stock llama.cpp cannot load.
Placeholder config: --split-mode tensor across both cards, 131072 context,
1 slot, f16 KV, MTP draft head on ROCm0.

deploy-remote.sh grows retire_root_llm_small(): the CUDA-era unit is stopped,
disabled and moved to ~/.AGENTTRASH/20260915-frank-small-retire/host-units
before frank-medium starts, because `systemctl disable` has been observed to
delete the unit file itself on this host (2026-08-26 incident).

The retired frank-small repo files (env, unit, serve script) move to
scripts/.AGENTTRASH/, and the planner, dispatcher, service allow-list and the
deployment test seams now name frank-medium.

Follow-up (same session, measured): llama-bench on the host with the correct
multi-device syntax (-dev ROCm0/ROCm1) overturned the tensor choice:

  single R9700   pp4096 1160.9 / tg128 19.42  (d32768: 512 / 18.4)
  layer  2x      pp4096 1991.2 / tg128 18.76  (d32768: 859 / 17.6)
  tensor 2x       pp4096  271.8 / tg128 26.26 (d32768: 229 / 25.1)

This build has no RCCL, so tensor mode falls back to a "meta-backend
butterfly" allreduce and prefill collapses 7x. Served path with MTP:
layer 35.7-46.2 tok/s vs tensor 28.7. SPLIT_MODE=layer, draft pinned to
ROCm1 (the card holding the last target layers). Served prefix cache with
the draft head verified live: turn 2 reprocessed 22 tokens instead of 949.
2026-09-15 19:03:30 -07:00
Frank 6085a99dd8 frank-medium: serve the new R9700s, retire frank-small
The RTX 3090 left Dankserver; two AMD Radeon AI PRO R9700 cards (Navi 48,
gfx1201, 32,624 MiB each) replaced it. FreeToken/CUDA cannot run there at all,
so the `frank-small` service is retired and `frank-medium` takes the endpoint.

New service: charlie12345/ROCmFPX llama.cpp (built for gfx1201 against the
fork's self-contained ROCm 7.14 toolchain, so the host's stale /opt/rocm-6.3.3
never participates) serving kingjones777/Qwen3.8-27B-ROCmFPX-Q8_0-AGENT-GGUF --
a Q8_0_ROCMFPX_AGENT (ftype 115) checkpoint that stock llama.cpp cannot load.
Placeholder config: --split-mode tensor across both cards, 131072 context,
1 slot, f16 KV, MTP draft head on ROCm0.

deploy-remote.sh grows retire_root_llm_small(): the CUDA-era unit is stopped,
disabled and moved to ~/.AGENTTRASH/20260915-frank-small-retire/host-units
before frank-medium starts, because `systemctl disable` has been observed to
delete the unit file itself on this host (2026-08-26 incident).

The retired frank-small repo files (env, unit, serve script) move to
scripts/.AGENTTRASH/, and the planner, dispatcher, service allow-list and the
deployment test seams now name frank-medium.
2026-09-15 18:53:20 -07:00
Frank 27e63359d6 frank-large: correct docs to measured 401408 ledger figures
Predicted ~10.8 GiB KV / ~33.4 need / ~10.5 host-left; the boot ledger
printed ~11.0 / ~31.5 / 100.0 GiB held with 9.1 GiB + 1396 contiguous
2 MiB blocks left. Stall NOTE no longer prints at this footprint. Also
documents the ~45-60 s first-request PING silence after a restart (the
healthcheck's HealthStartPeriod covers it; do not restart the unit).
2026-09-13 23:02:16 -07:00
Frank 91406af2a1 frank-large: trim KV pool 524288 -> 401408 for co-tenant headroom
The 09-12 doubled pool fit only at reserve 15, leaving 7.1 GiB / 310
contiguous 2 MiB blocks for the ~28 co-tenant containers and making the
engine print its minutes-long-stall warning on every boot. 401,408
positions (~10.8 GiB KV) hands ~3.4 GiB back while keeping MAX_TOK=32768
and reserve 15: modelled need ~33.4 GiB vs budget 38.5, so the fit check
should not clamp. Capacity becomes one full 262k conversation resident
beside a ~139k one, or three concurrent 131k chats (three slots).

Docs: AGENTS.md fleet table + halogen section, runbook flags table /
co-tenancy tense / admission-arithmetic paragraph updated for the trim.
2026-09-13 22:57:25 -07:00
Frank 025216fc84 docs(frank-small): document that FreeToken samples by default from the checkpoint
Answering "does the FreeToken runtime use sampling arguments?" -- yes, and
nothing in our config revealed it. serve-frank-small.sh passes no sampling
flags; the behaviour is the runtime default:

    freetoken/server/args.py:31  sampling_defaults: str = "model"

which fills temperature/top_k/top_p from the checkpoint's
generation_config.json -- this one ships do_sample:true, temp 1.0, top_k 20,
top_p 0.95. Proved live: three identical requests with no sampling params
produced three different reasoning traces (487/540/577 chars).

The asymmetry with frank-large is the part that will bite someone: a silent
client samples on frank-small and decodes greedy on frank-large, so the same
request is reproducible on one endpoint and not the other. Documented with the
determinism options (explicit temperature:0, or --sampling-defaults none).

Also recorded two request-level traps found during the probe: thinking eats the
whole max_tokens budget and returns empty content with finish_reason length,
and the API accepts unknown fields with a 200, so a 200 proves nothing about
whether a key was understood.
2026-09-12 23:40:34 -07:00
Frank 4e3d79388b docs(frank-large): resync slots=3 across AGENTS and runbook
Ledger confirms the change landed: "3 slot(s) over one 524288-position pool",
"slots: 1 -> 3 POOLED, 111.0 MiB each", held 103.5 -> 103.4 GiB.

Recorded the oversubscription nuance that the old row missed: the slots were
never the binding constraint on context. 4 x 262144 exceeds the 524288 pool,
so admission was pool-bound all along -- two full-length conversations, or
four at 131k. Lowering the cap trims simultaneous requests, not capacity.
2026-09-12 23:26:03 -07:00
Frank 6169278911 frank-large: lower KV slots 4 -> 3
Concurrency cap trimmed to the real fleet size. Each slot is 111.0 MiB of
O(1) state, so this hands ~111 MiB back on a host sitting at 7.1 GiB left
for everything else.

Note the slots already oversubscribed the pool (4 x 262144 > 524288), so
admission was pool-bound regardless: at most two full-length conversations,
or four at 131k. This does not change how much context fits -- it caps
simultaneous requests at three, and a fourth now queues.
2026-09-12 23:20:16 -07:00
Frank 5dfcde6c8b docs(dankclaw): record the 64 GiB host swapfile and its safe resize ordering
Raised /swap.img from 8 GiB to 64 GiB on Dankclaw. Host-level state with no
repo file, so it was invisible to the repo until now: fstab line 12 drives the
generated swap.img.swap unit, ext4, fallocated hole-free, swappiness 60,
zswap off. 748 GiB still free on /.

The reusable part is the ordering. The old file held 4 GiB of live pages and
MemAvailable read 73 GiB while only ~5 GiB was genuinely free -- the pinned
model weights are counted as reclaimable page cache, so the headline lies by
the size of the model. Swapping off before the replacement is online would have
pushed those pages into RAM that does not exist. Creating and enabling the new
swap first let them migrate instead: swapoff took 65 s and the host never
wobbled.

New page serverAppConfigs-docs/Dankclaw/Swap Configuration.md, linked from
the index, plus a pointer in the AGENTS.md memory-model section so nobody
reasons about headroom without checking swapon.

Old 8 GiB file staged at dankclaw:~/.AGENTTRASH/20260912-swapfile-8g/
pending approval to purge.
2026-09-12 22:03:24 -07:00
Frank a45edbcc6d docs(halogen): record the reserve ceiling and that the reserve is not an allocation
Seven restarts measured. New section "The reserve cannot buy air": reserve 16
was deployed and held exactly what 15 holds — 103.5 GiB, 7.1 GiB left to the
host. The flag is a term in the fit-check inequality, not a reservation, so it
has only two regimes: below the pool's requirement it changes nothing
observable, above it the pool silently disappears.

Sizing rule now defers to the engine's printed requirement (budget >= 36.5 for
524288 at max_tok 32768, from "need ~35.0 GiB plus 1.5 of margin") over my
modelled 37.0, and the ledger table carries the reserve-16 row.
2026-09-12 21:32:08 -07:00
Frank 5c4e8a9600 frank-large: revert reserve to 15 — the knob cannot buy host headroom
Tested 16 live. Pool stayed 524288 (no clamp), server held 103.5 GiB and
reported "7.1 GiB left for everything else" at BOTH 15 and 16 — identical
footprint. HALOGEN_HOST_RESERVE_GIB feeds the fit-check budget only; it is not
an allocation, so raising it cannot give RAM back to the co-tenants. Its sole
observable effect above 16 is to take the KV pool away.

16 therefore cost half the fit margin (1.0 vs 2.0 against the engine's 36.5 GiB
requirement) and bought nothing. 15 is strictly better at the same cost.

Recorded the ceiling (16) and the real air levers in the unit comment: pool
262144 frees 7.2 GiB, max_tok 16384 frees 8.4.
2026-09-12 21:30:15 -07:00
Frank b87f18569a frank-large: reserve 15 -> 16 for co-tenant headroom
Operator asked for 18. 18 is not available: the engine's own fit-check line,
read from a clamped boot rather than modelled, requires budget >= 36.5 GiB to
keep 524288 at max_tok 32768 ("524288 positions need ~35.0 GiB (plus 1.5 of
margin)"). budget(18) = 35.5, so 18 silently un-doubles the pool we just paid
the reserve for. 17 fits by 0.02, inside the noise of the 67.7 resident-weights
term. 16 is the largest reserve that survives the check with real margin:
37.5 budget vs 36.5 required.

Recorded the ceiling and both remedies in the unit comment. The reserve is now
spent, so a future stall is answered by the pool or the arena, not here.
2026-09-12 21:27:58 -07:00
Frank 2969e08616 frank-large: document reserve 15 result — both features in force, stall risk logged
Ledger confirms the doubled pool is actually reserved (524288, 13312.0 MiB)
at max_tok 32768 with no clamp and no WARNING: 38.5 budget vs 37.0 needed.

Recorded the cost honestly. The server now holds 103.5 of 121.2 GiB and the
engine reports only 310 contiguous 2 MiB blocks / 7.1 GiB left for everything
else (24-reserve baseline: 2081 blocks / 12.1 GiB). All 28 co-tenants stayed
up, but the engine volunteers a new note: when memory runs out, frank-large
AND its neighbours "can stall for minutes at a time at full CPU with no disk
activity and no output, which is not a crash and needs no restart". That is
the least diagnosable of the three failure modes seen today, and `free` hides
it — MemAvailable still reads ~74 GiB because the pinned weights count as
reclaimable page cache.

Sizing rule now expresses the budget as a function of reserve, shows both
endpoints (24 -> 29.5, 15 -> 38.5), and the max_tok ceiling is labelled as
reserve-conditional (<= 18,130 at 24; <= 35,700 at 15).
2026-09-12 21:22:36 -07:00
Frank 8a79f9a709 frank-large: reserve 24 -> 15, buying the doubled pool AND full prefill speed
Neither lever alone could give both features. At reserve 24 the device budget
is 29.5 GiB and max_tok 32768's 37.0 GiB need always lost the doubling to the
fit-check clamp; at max_tok 16384 the doubling survived but prefill paid 8-9%.

Dropping the reserve to 15 moves the budget to 38.5:
  121.2 host - 67.7 resident weights - 15 reserve = 38.5
  need = 14.4 pool + 20.6 arena + 1.97 vision    = 37.0   -> 1.5 GiB spare

So the payment is page cache for the n-gram lookup table rather than either
feature. 15 GiB, against a table of 47.7 GiB read through the file cache.
Two reasons this is a survivable risk rather than a cliff:

  - The engine's own ledger reported only 12-17 GiB left at the previous 24
    setting, so the declared reserve was never held literally; 15 may cost
    less real cache than it reads as.
  - The failure mode is legible: long prompts fault heavily against a busy
    disk, and raising the reserve is the documented remedy. Not the silent
    clamp, and not the 65536 no-boot mode.

1.5 GiB of headroom is thin and deliberate. Documented in the unit as a
matched pair with the pool: raise the reserve back to 24 and the doubling
quietly goes away again.
2026-09-12 21:17:16 -07:00
Frank 65d1be06ac frank-large: record the 32k outcome — the doubling is clamped away, docs resynced
Deployed HALOGEN_MAX_TOK=32768 by operator call (upstream default, full prefill
speed) and confirmed the prediction from the ledger:

  kv pool: 524288 positions need ~35.0 GiB ... LOWERING THE POOL TO 262144 (~27.8 GiB)
  memory: 68.0 weights + 7.2 KV pool + 21.1 working = 96.3 GiB in all

so the effective pool is 262144 again and the doubling this task set out to do
is not in force. Healthy, completion verified. Left the pool requesting 524288
deliberately: with KV_POOL_FIT on that is a grow-if-RAM-frees-up declaration,
not a mistake.

Two corrections to what I wrote earlier today:

1. The engine's sizing block, and its "WARNING even 262144 ... host RAM cannot
   spare it" line, print ONLY when the fit check had to clamp something. The
   09-11 baseline boot prints no kv pool: sizing lines at all. So the absence
   of a warning in old logs was never evidence of headroom.

2. The budget needs a vision term. Actual need is pool + arena + 1.97 GiB of
   vision tower, not pool + a 1.5 GiB nominal margin. That makes the ceiling
   for a doubled pool max_tok <= 18,130 rather than 19,115, and it means the
   32768/262144 corner this box has run in since 09-11 is ~0.3 GiB OVER its own
   stated budget -- and has been the whole time. It boots and serves with
   12-14 GiB genuinely free because the 24 GiB reserve is generous. A soft
   floor, not a wall.

The arena model also reproduces upstream's three published "on the device"
figures exactly (35 / 42 / 41 GiB), confirming they are pool+arena totals at
the default max_tok and not pool-only -- which is the misreading that made the
doubling look affordable in my first estimate.
2026-09-12 20:12:03 -07:00