Compare commits

..
Author SHA1 Message Date
dank 127621bdd7 Merge pull request 'docs(vault)+fix(grafana): audit tranche 1 and dashboard data-flow fixes' (#67) from fix/dragoon-context-200k into main 2026-10-21 09:28:43 -07:00
Frank c96d0228a4 fix(grafana): dashboard census fixes — every panel now receives data
Census: all 5 provisioned dashboards, 56 targets, every expression
re-evaluated against the live Prometheus/Loki with template vars expanded
(queries must run from the host: both APIs bind 127.0.0.1; from the dev box
that is connection-refused, which is NOT a dashboard defect).

- host-system 'Load 5m / core' was STRUCTURALLY EMPTY: bare '/' drops
  non-matching pairs and the right side collapses to {job} while the left
  carries {job,instance} -> 0 series forever. Fixed with / on(job); proven
  live (2 series).
- containers 'Memory vs limit (%)' was STRUCTURALLY EMPTY: every
  container_spec_memory_limit_bytes on this fleet is 0 (no cgroup limits),
  so the '> 0' filter can never match. Rewritten to share-of-host-total via
  label_replace join of the node exporter's MemTotal by host (the parens
  form is a PromQL parse error: nothing may follow group_left); 62 series
  live. Panel unit percentunit kept -> expression returns a fraction.
- llm-fleet 'Firing fleet alerts' is legitimately empty when healthy
  (ALERTS carries only firing/pending series); description now says so,
  per the Stale Signal rule that 0-series-on-ALERTS is not a defect.

logs-overview and observability: all targets already returning data.
2026-10-21 02:45:57 -07:00
Frank a61308d72e docs(vault): audit tranche 1 — one-owner dedupe of the observability cluster, dead-shape numbers destroyed
Purpose-test pass over the audit findings (every verdict re-verified
against the file before touching it; three scout findings were hallucinated
— a nonexistent filename, my own changelog line, and sanctioned Eras rows):

- Observability Provisioning: destroyed the 63-line 'Current delivered
  state' block — chain topology/relay/delivery restates [[Observability]]
  and [[Alertmanager]] (those are the owners). Its three sole-custody laws
  MOVED to Stale Signal Audit as 'Three rules that outlive the incident':
  down-beside-healthy -> read lastError; Prometheus alert state in process
  vs Alertmanager's --storage.path persistence; candidate-arm metrics are
  never fleet state.
- RDNA4 MoE VRAM budget: kept the laws (registry over-prices the pool;
  never reuse a B/token ratio across geometries; pin pool bytes
  floor-sized), era-labelled the rates, destroyed the measured ladder
  table for the superseded shape-G/bf16 rungs.
- Stale Signal Audit: new rules section + Provisioning Related link.

check-okf 109/0, vault links 0 unresolved, docs seams green.
2026-10-21 02:35:44 -07:00
dank ccb805cf0b Merge pull request 'fix(frank-dragoon): mount the distro XRT at the third prefix the NPU child searches' (#66) from fix/dragoon-context-200k into main 2026-10-21 02:25:42 -07:00
Frank c6e2887f94 fix(frank-dragoon): mount the distro XRT at the third prefix the NPU child searches
The lane's child dlopens libxrt_core.so.2 from /opt/xilinx/lib/<multiarch>/
as well; our mounts filled only /opt/xilinx/xrt/lib and the install path.
Since the host's libxrt2 was reconfigured 2026-10-20 the child dies naming
the missing prefix ('No such library
/opt/xilinx/lib/x86_64-linux-gnu/libxrt_core.so.2') and the engine
supervisor takes the WHOLE container down on a dead lane — measured
2026-10-21: frank-dragoon boot-looped 88 times with chat dead while the
GPU lines looked healthy.

Each XRT library now mounts THREE times from the same real file (never a
symlink); the multiarch name derives from FRANK_DRAGOON_XRT_DIR's basename
so the bats fixture proves the derivation. Proven end-to-end before commit:
production-shaped scratch boot + the three extra mounts answers the full
boot ledger, npu_api buckets u0..u9, and /health ok at the 262144 ceiling.

A dead lane is not survivable on this engine — arm it only with all three
prefixes (page law updated).
2026-10-21 02:21:53 -07:00
dank 9eb53ef937 Merge pull request 'feat(frank-dragoon): 200,000-token harness window via the native 262,144 ceiling' (#65) from fix/dragoon-context-200k into main 2026-10-21 01:40:40 -07:00
Frank 688928d489 docs(dragoon): the 262144 ceiling, the 200,000 window, and the 95 % trigger law
Shape + harness contract on the endpoint page, AGENTS.md, and the OMP
Harness worked example recomputed to the live pair; the 400-zone law now
names the 85 %-comments vs 95 %-config disagreement as the recurring
cause and points at the seam that enforces the arithmetic.
2026-10-21 01:34:28 -07:00
Frank 56503dbad1 feat(frank-dragoon): raise HALOGEN_CTX to the native 262144 ceiling
The 200,704 ceiling squeezed the harness contextWindow down to 155,648
(the 400-zone fit law needs ~45k of gap under the ceiling), which is why
long dragoon sessions kept hitting the 400 zone: the harness clamps
max_tokens to window - est - 4096, its chars/4 est under-counts ~3 %,
and config.yml compacts at 95 % while the pair was designed for 85 %.

Ceiling -> checkpoint's native 262,144 (no RAM move: the KV pool stays
401,408 and is what the ledger bills; the band ctx <= pool <= slots*ctx
still holds). The harness window becomes 200,000 with a 62,144 gap.

The fit-law seam now reads the declared window from the vault page (the
one owner) and checks the arithmetic at the live 95 % trigger, so a
ceiling/window pair that drifts back together fails tests/run.sh
instead of a live session.
2026-10-21 01:34:28 -07:00
dank fbeb9701e4 Merge pull request 'fix(observability): serve every exporter bind; scrape same-host cadvisor via gateway' (#64) from fix/gpu-serve-and-cadvisor-target into main 2026-10-21 00:39:54 -07:00
Frank 650b6e0281 fix(observability): serve every exporter bind; scrape same-host cadvisor via gateway
Two shipped-2026-10-20 bugs that left gpu-dankclaw, gpu-dankserver and
cadvisor-dankclaw DOWN beside a green deploy:

- gpu-exporter.py built one ThreadingHTTPServer per bind but ran
  serve_forever() on servers[0] only: the LAN-class socket sat listen()ing
  but never accept()ed, loopback deploy gate passed, every scrape that
  reached the other bind timed out on the SYN backlog. Now one thread per
  extra bind. Regression test spins --serve on two loopback binds and
  requires BOTH to answer /metrics.

- cadvisor-dankclaw scraped its own host's LAN IP. Measured from inside the
  Prometheus container: a bridge container cannot dial its own host's LAN
  address at all (hairpin refuses published ports too); the podman gateway
  answers. Target moved to host.containers.internal:8080; the seam that
  pinned the LAN address (asserting the wrong doctrine) is re-pinned to the
  gateway plus a negative pin against the own-host LAN dial. Observability
  page records the measured reachability law replacing the inferred one.
2026-10-21 00:34:17 -07:00
dank b62a05837f Merge pull request 'fix(deploy): client allowlist missing cadvisor/gpu-exporter/prowlarr' (#63) from fix/deploy-sh-allowlist-drift into main 2026-10-21 00:15:45 -07:00
19 changed files with 257 additions and 210 deletions
+11 -10
View File
@@ -419,16 +419,17 @@ disagrees with it is the bug.
digest-pinned `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`. digest-pinned `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84…`.
Engine stdout (the boot ledger: weights, KV pool, clamp) goes to Engine stdout (the boot ledger: weights, KV pool, clamp) goes to
`/home/dank/logs/frank_dragoon.log`, NOT the journal. `/home/dank/logs/frank_dragoon.log`, NOT the journal.
- Shape: `HALOGEN_CTX=200704` is the **per-request ceiling** (a longer prompt - Shape: `HALOGEN_CTX=262144` (the checkpoint's native window) is the
is a 400, never a truncation); `HALOGEN_KV_SLOTS=2` over ONE **per-request ceiling** (a longer prompt is a 400, never a truncation);
`HALOGEN_KV_POOL_POSITIONS=401408` pool — pool positions buy concurrency, `HALOGEN_KV_SLOTS=2` over ONE `HALOGEN_KV_POOL_POSITIONS=401408` pool —
the per-request window never moves; `HALOGEN_SPEC_ADAPT=0` keeps the MTP pool positions buy concurrency, the per-request window never moves;
drafter always armed (the adaptive default disarms it and halves decode); `HALOGEN_SPEC_ADAPT=0` keeps the MTP drafter always armed (the adaptive
server sampler 1.0/0.95/20. default disarms it and halves decode); server sampler 1.0/0.95/20.
- Harness contract: `models.yml` `contextWindow: 155648` = ceiling − 45,056. - Harness contract: `models.yml` `contextWindow: 200000` — ~45k+ under the
The clamp is `min(45056, window − est − 4096)` and the chars/4 estimate ceiling. The clamp is `min(45056, window − est − 4096)` and the chars/4
under-counts, so a harness window equal to the ceiling makes every long estimate under-counts (trigger is 95 %), so a harness window at the ceiling
request a guaranteed 400. Reasoning arrives in `reasoning_content`; the makes every long request a guaranteed 400.
Reasoning arrives in `reasoning_content`; the
template rejects `reasoning_effort` outside `xhigh|medium|low`; images are template rejects `reasoning_effort` outside `xhigh|medium|low`; images are
OpenAI `image_url` parts (probe ≥256 px); `response_format` unsupported; OpenAI `image_url` parts (probe ≥256 px); `response_format` unsupported;
`/completion`, `/ping`, `/props`, `/tokenize` are 404. `/completion`, `/ping`, `/props`, `/tokenize` are 404.
+4 -3
View File
@@ -1401,9 +1401,10 @@ deploy_anubis() {
# conversion (2026-10-11) of the abliterated Qwen3.8-Flash-Next — # conversion (2026-10-11) of the abliterated Qwen3.8-Flash-Next —
# qwen38-flash-next-v2-abliterated.hgn (66,687,678,432 B, ~62 GiB resident) # qwen38-flash-next-v2-abliterated.hgn (66,687,678,432 B, ~62 GiB resident)
# + the REQUIRED n-gram lookup table + the vision tower — MTP spec decode # + the REQUIRED n-gram lookup table + the vision tower — MTP spec decode
# (HALOGEN_SPEC_ADAPT=0), 2 slots over ONE 401,408-position KV pool (2 × ctx, # (HALOGEN_SPEC_ADAPT=0), 2 slots over ONE 401,408-position KV pool at the
# raised 2026-10-18 so BOTH slots can hold a maximum-length request) at the # native ctx 262144 ceiling (raised 2026-10-21 from 200704 so the harness can
# ctx 200704 ceiling, image input — on the Strix Halo iGPU. It stays a # run a 200,000 window with its ~45k fit gap; the pool still carries two
# 200k conversations at once), image input — on the Strix Halo iGPU. It stays a
# ROOT unit because the serve script drives `podman run` directly (the frank-archon pattern): # ROOT unit because the serve script drives `podman run` directly (the frank-archon pattern):
# relink_unit points /etc/systemd/system/frank-dragoon.service at the repo # relink_unit points /etc/systemd/system/frank-dragoon.service at the repo
# file, boot persistence is that unit's [Install], and LimitMEMLOCK=infinity # file, boot persistence is that unit's [Install], and LimitMEMLOCK=infinity
+22 -12
View File
@@ -49,14 +49,21 @@ FRANK_DRAGOON_TOKENIZER_FILES="tokenizer.json chat_template.jinja tokenizer_conf
# stays at the image default, which is what was qualified) ------------------ # stays at the image default, which is what was qualified) ------------------
# Served id. OpenWebUI api_configs model_ids for the :7770 backend MUST name it. # Served id. OpenWebUI api_configs model_ids for the :7770 backend MUST name it.
HALOGEN_MODEL_ID=frank-dragoon HALOGEN_MODEL_ID=frank-dragoon
# PER-REQUEST ceiling: a longer prompt is a 400 naming the limit, never a # PER-REQUEST ceiling, set to the checkpoint's NATIVE window (262,144 — a
# truncation. The harness window must sit ~45k UNDER this (the 400-zone law on # longer prompt is a 400 naming the limit, never a truncation; stretching
# the page) — a window equal to the ceiling makes every long request a 400. # past native would need HALOGEN_ROPE_YARN and is not on plan). The harness
HALOGEN_CTX=200704 # `contextWindow` must sit ~45k under THIS number (the 400-zone law on the
# ONE pool behind both slots, sized so BOTH can carry a maximum-length request # page): with the window at 200,000 the gap is 62,144, which absorbs the
# at once. Pool positions buy concurrency; the per-request window never moves # chars/4 under-count. A window at or near the ceiling makes every long
# with them. Legal band: ctx <= pool <= slots * ctx (the seam enforces it), and # request a guaranteed 400.
# RAM the pool takes is RAM the n-gram table's page cache loses. HALOGEN_CTX=262144
# ONE pool behind both slots. The harness window is 200,000, so 401,408
# still carries TWO maximum harness-length conversations at once
# (2 x 200,000 + one 45,056 budget <= pool + cap headroom); two literal
# ceiling-length requests would queue instead — buying those means raising
# the pool, and RAM the pool takes is RAM the n-gram table's page cache
# loses. Pool positions buy concurrency; the per-request window never moves
# with them. Legal band: ctx <= pool <= slots * ctx (the seam enforces it).
HALOGEN_KV_POOL_POSITIONS=401408 HALOGEN_KV_POOL_POSITIONS=401408
# Two concurrent conversations: the MTP drafter is not slot-locked on this # Two concurrent conversations: the MTP drafter is not slot-locked on this
# engine, so the second slot is capacity, not a speculation tax. # engine, so the second slot is capacity, not a speculation tax.
@@ -107,10 +114,13 @@ HALOGEN_NPU_MODELS=qwen3-embedding-0.6b
FRANK_DRAGOON_NPU_DIR=/home/dank/models/halogen-v2-abliterated/npu FRANK_DRAGOON_NPU_DIR=/home/dank/models/halogen-v2-abliterated/npu
# Node paths and the XRT directory are variables so the bats seam can drive # Node paths and the XRT directory are variables so the bats seam can drive
# these gates on a box with no NPU. Ubuntu's distro XRT (libxrt2 + libxrt-npu2) # these gates on a box with no NPU. Ubuntu's distro XRT (libxrt2 + libxrt-npu2)
# is mounted FILE BY FILE and TWICE EACH — once where the image looks for XRT, # is mounted FILE BY FILE and THREE TIMES EACH — where the image looks for XRT
# once at its own path, because those libraries find each other at their install # (/opt/xilinx/xrt/lib), at its own path (those libraries find each other at
# path; the vendor forbids mounting an /opt/xilinx/xrt of symlinks into the # their install path), and at /opt/xilinx/lib/<multiarch>/ (the NPU child
# system dir, which dangle inside the container. # searches that third prefix; without it the lane dies naming it and the
# supervisor takes the container down — 2026-10-21 boot loop). The basename of
# this directory IS the multiarch name. The vendor forbids mounting an
# /opt/xilinx/xrt of symlinks into the system dir, which dangle in the container.
FRANK_DRAGOON_DEV_ACCEL=/dev/accel/accel0 FRANK_DRAGOON_DEV_ACCEL=/dev/accel/accel0
FRANK_DRAGOON_XRT_DIR=/usr/lib/x86_64-linux-gnu FRANK_DRAGOON_XRT_DIR=/usr/lib/x86_64-linux-gnu
# sysfs root the fabric-clock preflight reads (same knob the vendor's # sysfs root the fabric-clock preflight reads (same knob the vendor's
+1 -1
View File
@@ -1,5 +1,5 @@
[Unit] [Unit]
Description=halogen-flash-server container (Quat3rnion NATIVE v2 abliterated Qwen3.8-Flash-Next, MTP spec decode, 2 slots over ONE 401408-position pool at ctx 200704, vision ON), root systemd unit (dankclaw, frank-dragoon, Strix Halo iGPU) Description=halogen-flash-server container (Quat3rnion NATIVE v2 abliterated Qwen3.8-Flash-Next, MTP spec decode, 2 slots over ONE 401408-position pool at ctx 262144, vision ON), root systemd unit (dankclaw, frank-dragoon, Strix Halo iGPU)
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
+6
View File
@@ -32,6 +32,7 @@ import glob
import os import os
import subprocess import subprocess
import sys import sys
import threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
FAMILIES = [ FAMILIES = [
@@ -198,6 +199,11 @@ def main(argv):
return 2 return 2
servers.append(ThreadingHTTPServer((b, args.port), Handler)) servers.append(ThreadingHTTPServer((b, args.port), Handler))
sys.stderr.write("gpu-exporter serving :%d on %s\n" % (args.port, ",".join(binds))) sys.stderr.write("gpu-exporter serving :%d on %s\n" % (args.port, ",".join(binds)))
# EVERY socket must be served: a listen()ed-but-never-accept()ed bind looks
# healthy in `ss` yet times out every scrape once its SYN backlog fills —
# and the loopback deploy gate cannot see it. One thread per extra bind.
for s in servers[1:]:
threading.Thread(target=s.serve_forever, daemon=True).start()
servers[0].serve_forever() servers[0].serve_forever()
return 0 return 0
+14 -5
View File
@@ -136,10 +136,17 @@ fi
# The engine re-checks size+sha256 against that catalog at boot; it cannot # The engine re-checks size+sha256 against that catalog at boot; it cannot
# fetch anything here because /models is read-only and HALOGEN_DOWNLOAD # fetch anything here because /models is read-only and HALOGEN_DOWNLOAD
# stays unset — no outbound connection at boot), # stays unset — no outbound connection at boot),
# 2. the distro XRT's three libraries, each mounted twice (where the image # 2. the distro XRT's three libraries, each mounted THREE times: where the
# looks for XRT, and at its own path — the libraries find each other there; # image looks for XRT (/opt/xilinx/xrt/lib), at their own path (the
# an /opt/xilinx/xrt of symlinks into the system dir dangles inside the # libraries find each other there), and at /opt/xilinx/lib/<multiarch>
# container, which the vendor calls out explicitly), # — the NPU child searches that third prefix too, and since the host's
# libxrt2 was reconfigured 2026-10-20 it is the ONLY prefix the child
# names: omitting it crashes the lane with `No such library
# '/opt/xilinx/lib/x86_64-linux-gnu/libxrt_core.so.2'` and the engine
# supervisor takes the dead lane down on the whole container (measured
# 2026-10-21: 88-restart boot loop). Real files only — an /opt/xilinx/xrt
# of symlinks into the system dir dangles inside the container, which the
# vendor calls out explicitly),
# 3. the GPU fabric clock HELD: on Strix Halo the iGPU and the NPU co-running # 3. the GPU fabric clock HELD: on Strix Halo the iGPU and the NPU co-running
# hang the machine while FCLK changes speed, and the engine refuses to # hang the machine while FCLK changes speed, and the engine refuses to
# start when it moves — it cannot read host sysfs from inside the # start when it moves — it cannot read host sysfs from inside the
@@ -190,7 +197,9 @@ if [ -n "$NPU_MODELS" ]; then
exit 1 exit 1
fi fi
REAL="$(readlink -f "$XRT_DIR/$L")" REAL="$(readlink -f "$XRT_DIR/$L")"
NPU_ARGS+=(-v "$REAL:/opt/xilinx/xrt/lib/$L:ro" -v "$REAL:$XRT_DIR/$L:ro") MULTIARCH="${XRT_DIR##*/}"
NPU_ARGS+=(-v "$REAL:/opt/xilinx/xrt/lib/$L:ro" -v "$REAL:$XRT_DIR/$L:ro"
-v "$REAL:/opt/xilinx/lib/$MULTIARCH/$L:ro")
done done
HELD=0 HELD=0
for D in "$SYSFS"/class/drm/card*/device; do for D in "$SYSFS"/class/drm/card*/device; do
+18 -14
View File
@@ -52,28 +52,31 @@ Where the contract lives:
The arithmetic (the coupled contract every endpoint's maxTokens comment The arithmetic (the coupled contract every endpoint's maxTokens comment
encodes). The authoritative `contextWindow`/`maxTokens` pair per model is on the encodes). The authoritative `contextWindow`/`maxTokens` pair per model is on the
endpoint page ([[frank-archon]], [[frank-dragoon]], [[frank-zealot]]); what endpoint page ([[frank-archon]], [[frank-dragoon]], [[frank-zealot]]); what
follows is a **worked example** recomputed from the live files on 2026-10-18 — follows is a **worked example** recomputed from the live files on 2026-10-21 —
a snapshot, not fleet state — with windows 200,000 / 155,648 / 200,704 and a snapshot, not fleet state — with windows 200,000 / 200,000 / 200,704 and
maxTokens 16,384 / 45,056 / 16,384 (archon / dragoon / zealot): maxTokens 16,384 / 45,056 / 16,384 (archon / dragoon / zealot):
1. **Source trigger** = `thresholdPercent × source contextWindow`; at the live 1. **Source trigger** = `thresholdPercent × source contextWindow`; at the live
95 % that is 190,000 (archon), 147,866 (dragoon), 190,669 (zealot). The 95 % that is 190,000 (archon and dragoon), 190,669 (zealot). The
bundle clamps the percentage to 1–99 and converts it to a token threshold. bundle clamps the percentage to 1–99 and converts it to a token threshold.
2. **Output budget** = capped at the target model's `maxTokens` — every role 2. **Output budget** = capped at the target model's `maxTokens` — every role
here compacts to frank-zealot, so the budget is its 16,384. here compacts to frank-zealot, so the budget is its 16,384.
3. **REQUIREMENT**: prompt (≤ trigger) + budget ≤ the target's per-slot 3. **REQUIREMENT**: prompt (≤ trigger) + budget ≤ the fitting window. For a
window. **At 95 % this no longer holds**: 190,000 + 16,384 = 206,384 > dragoon session the fitting window is the ENGINE ceiling, not a slot: the
200,704. The registry's own comments design around an 85 % trigger (archon: 190,000 trigger is 62,144 under the 262,144 ceiling, so even ×1.03 est
170,000 est × 1.03 + 16,384 ≈ 191,500 ≤ 200,704, `models.yml:49-56`), which error plus the 45,056 request budget lands inside — that headroom is the
does hold. While `config.yml` and those comments disagree, treat 85 % as the reason the ceiling moved to native on 2026-10-21. For a session compacting
design point and re-run this arithmetic after ANY window change — a 400 from INTO frank-zealot the window is its 200,704-token slot, and **at 95 % this
the compaction target mid-conversation is the symptom. does not hold for archon/zealot sources**: 190,000 + 16,384 = 206,384 >
200,704. Treat 85 % as the design point until config.yml and the registry
comments agree, and re-run this arithmetic after ANY window change — a 400
from the compaction target mid-conversation is the symptom.
(Historic 2026-10-05 pairs, at `thresholdPercent: 70`: 140,492 + 36,864 = (Historic 2026-10-05 pairs, at `thresholdPercent: 70`: 140,492 + 36,864 =
177,356 ≤ 200,704 archon; 140,490 + 57,344 ≤ 200,700 dragoon; 140,492 + 177,356 ≤ 200,704 archon; 140,490 + 57,344 ≤ 200,700 dragoon; 140,492 +
32,768 ≤ 200,704 zealot.) 32,768 ≤ 200,704 zealot.)
Procedure (recompute from the LIVE files every time — windows rotate with Procedure (recompute from the LIVE files every time — windows rotate with
every swap; the block above is the 2026-10-18 state): every swap; the block above is the 2026-10-21 state):
1. Read the `config.yml` compaction block + `models.yml` entries for source 1. Read the `config.yml` compaction block + `models.yml` entries for source
and target. Every endpoint's `contextWindow` comment carries its own and target. Every endpoint's `contextWindow` comment carries its own
@@ -94,9 +97,10 @@ Known reconciliation trap: comments in models.yml have described compaction
as running "on the dankserver endpoint" while `remoteEndpoint` pointed at as running "on the dankserver endpoint" while `remoteEndpoint` pointed at
dankclaw — the comment/config disagreed. The comments now name the real dankclaw — the comment/config disagreed. The comments now name the real
target (`compaction.remoteEndpoint` = dankclaw :7771). **The config is target (`compaction.remoteEndpoint` = dankclaw :7771). **The config is
truth; fix comments in the same change.** A fresh instance of the same trap is truth; fix comments in the same change.** The 85 %-comments vs 95 %-config
open 2026-10-18: every `maxTokens` comment still says "compaction fires at disagreement is not academic: it is why long dragoon sessions kept drawing
85 % of the window" while `config.yml:84` sets `thresholdPercent: 95`. 400s at the old 200,704 ceiling — the trigger fires late AND the est
under-counts, and a window parked near the ceiling has no room for either.
`maxTokens` never changes for a reslice — it caps output; `contextWindow` caps `maxTokens` never changes for a reslice — it caps output; `contextWindow` caps
input. Boot-shape OOM laddering is a different problem input. Boot-shape OOM laddering is a different problem
([[Llama Cpp Serving Sizing]]). ([[Llama Cpp Serving Sizing]]).
@@ -116,6 +116,20 @@ as working when they could never report up. **Clean cutover includes prose** —
docs, unit docstrings, and the repo's rule file (the most expensive place to docs, unit docstrings, and the repo's rule file (the most expensive place to
be stale). be stale).
## Three rules that outlive the incident that taught them
* **A target can read down beside a healthy service.** Read `lastError` from
`/api/v1/targets` before believing an outage — llama.cpp refuses `/metrics` at the
protocol layer without `--metrics` (501) while `/health` answers 200 for weeks.
* **Alert state lives where the tool says it does.** Prometheus keeps rule state in
process memory — a restart re-stamps `activeAt`, so a "fresh" alert after a reboot is
not evidence of a fresh fault. Alertmanager **does** persist (`--storage.path` on a
host volume: silences + notification log, [[Alertmanager]]).
* **Never quote a candidate arm's flags or VRAM as fleet state.** A stopped production
unit (`failed` after `stop-sigterm timed out. Killing` — the known shutdown shape,
[[frank-zealot]]) beside an answering port means the metric measured a scratch boot;
the process shape can change between two censuses ten minutes apart.
## Order and verification ## Order and verification
1. Enumerate the surfaces (status board, metrics targets, alert rules, test 1. Enumerate the surfaces (status board, metrics targets, alert rules, test
@@ -3,7 +3,7 @@ type: reference
title: Observability Provisioning title: Observability Provisioning
description: "Monitoring config as code: the config triad, deploy copiers, planner mappings, and where each change type lands." description: "Monitoring config as code: the config triad, deploy copiers, planner mappings, and where each change type lands."
tags: [observability, prometheus, grafana, provisioning, deployment] tags: [observability, prometheus, grafana, provisioning, deployment]
timestamp: 2026-10-05T00:00:00-07:00 timestamp: 2026-10-21T00:00:00-07:00
--- ---
# Observability Provisioning # Observability Provisioning
@@ -112,73 +112,10 @@ unified storage — a file existing on disk is not proof the running server
loaded it. See [[Stale Signal Audit]] for the never-green-target decision loaded it. See [[Stale Signal Audit]] for the never-green-target decision
table (refused = outage, 404/HTML = config defect). table (refused = outage, 404/HTML = config defect).
## Current delivered state (delivery chain live 2026-09-17, re-verified 2026-10-18)
Prometheus → Alertmanager (`dankclaw/observability/quadlet/alertmanager.container`
loopback `:9093`) → host-side Signal relay (`scripts/signal-relay.py`, user
unit, `192.168.0.143:18093` — the LAN IP, the one bind a container can reach
via `host.containers.internal`) → signal-cli, addressed by number
`+12062476336` (`scripts/signal-relay.py:36-43`). Every hop re-checked
2026-10-18: `ss -ltnp` shows `python3` on `192.168.0.143:18093`
`curl -s http://192.168.0.143:18093/health` → `ok`, and
`journalctl --user -u signal-relay` ends with successful `sent to +12062476336:`
lines for `LlmEndpointDown`, `LlmRequestsQueueing`, `LlmSpecDecodeOff` and
`LlmPrefixCacheCollapse` (firing and resolved). Some targets read down for
structural reasons (HTML-scraped "probes", Gitea metrics off); the remedy is
the blackbox exporter. See [[Prometheus]] → Targets.
**A llama.cpp endpoint is invisible unless it opts in.** Until 2026-10-18,
`up{job="frank-zealot"}` was 0 for the entire retained history of the metric —
the oldest sample, 2026-10-11, is already 0 — while the service itself answered
`/health` 200 the whole time. Cause: llama.cpp only exports `/metrics` when it
is started with the flag, so the scrape was refused at the protocol layer (the
flag and its opt-out are on [[frank-zealot]]) and `LlmEndpointDown` (critical)
stayed firing:
the relay re-sent it on the 4 h `repeat_interval` for days (`journalctl --user
-u signal-relay` — firing sends at 01:37, 05:42, 09:47, 13:52, 17:57 and 22:02
on 2026-10-12). It finally resolved at `2026-10-18T01:53:21-07:00`, after the
metrics opt-in landed that day; the target
reports up since (`curl -s http://127.0.0.1:9090/api/v1/targets` 2026-10-18).
How many payload generations it spanned is not readable from any host
artifact. *(unverified 2026-10-18)*
Two laws fall out of it. A target can read down beside a healthy service, so
read `lastError` from `/api/v1/targets` before believing an outage. And rule
state lives in Prometheus' process, not on disk — a restart re-stamps
`activeAt` on a long-standing alert, so a "fresh" alert after a reboot is not
evidence of a fresh fault. (Alertmanager by contrast **does** persist:
`--storage.path=/alertmanager` on a host volume keeps silences and the
notification log — `dankclaw/observability/quadlet/alertmanager.container:39-42`
`/opt/alertmanager/data`.)
Remaining known downs are the structural ones above (HTML-scraped probes, Gitea
metrics, ComfyUI), and `frank-dragoon` exports `/metrics` natively
([[frank-dragoon]]), which is why
`frank-dragoon` has always been green. The active-alert count is a
moment-in-time value and should never be documented as a constant:
`/api/v1/alerts` was empty at the 2026-10-18 morning census, held one firing
`LlmPrefixCacheCollapse` (`activeAt` 11:10:48Z, value 2.6e-4) when sampled
minutes later, and was empty again by 11:56Z; the relay logged the matching
firing and resolved sends at `04:26:21-07:00` and `04:41:21-07:00`.
**What that zealot scrape measured on 2026-10-18.** As of ~04:30 PDT the
production `frank-zealot` root unit was stopped **on purpose** for a benchmark
window on the 3090 — the repo Benchmarking Policy says stop the production
service before benching — and `:7771` answered from a manually booted candidate
arm (`systemctl is-active frank-zealot` → `failed` after
`stop-sigterm timed out. Killing`, which is llama.cpp ignoring SIGTERM past
`TimeoutStopSec`, the known shutdown shape for these engines). So
`up{job="frank-zealot"} == 1` that day measured the trial, not the production
shape in `scripts/frank-zealot.env` — that shape is [[frank-zealot]]'s to state,
and it is the repo contract.
The unit stayed `enabled`; `./scripts/deploy.sh dankclaw --service frank-zealot`
reinstalls and starts it, restoring boot persistence and self-healing. Never
quote a candidate arm's flags or VRAM as fleet state — the process and its shape
changed between two censuses ten minutes apart.
## Related ## Related
- [[Observability]] — stack topology and the alert delivery chain - [[Observability]] — stack topology and the alert delivery chain
- [[Stale Signal Audit]] — classifying down targets before "fixing" them - [[Stale Signal Audit]] — classifying down targets before "fixing" them, and the
down-beside-healthy / alert-state-persistence / candidate-arm rules
- [[Quadlet Lifecycle]] — the units that host these services - [[Quadlet Lifecycle]] — the units that host these services
- [[Deploy State Triage]] — how the planner selects these services - [[Deploy State Triage]] — how the planner selects these services
@@ -67,10 +67,18 @@ Alertmanager has no vhost at all. node-exporter is the exception that must live
in the host network namespace, and it binds exactly two addresses rather than in the host network namespace, and it binds exactly two addresses rather than
the wildcard — see [[Node-Exporter]]. the wildcard — see [[Node-Exporter]].
The relay's bind is the one address a bridge container can reach on the host: **Same-host reachability from a bridge container is measured, not inferred**
`192.168.0.143:18093`, with iptables INPUT admitting loopback, the host itself (2026-10-21, from inside the Prometheus container): `host.containers.internal`
and the podman bridge subnet `10.89.0.0/16` before the DROP (the podman gateway) reaches host services whether they bind host-net or are
(`scripts/deploy-remote.sh:255-262`). published ports — node `:9100`, cadvisor `:8080`, gpu `:9401` all answer there
— while the host's own LAN IP is refused (`192.168.0.143:8080`, `:9401`), so
every same-host Prometheus target uses the gateway and only cross-host targets
(dankserver) use plain LAN. Whether LAN works at all is network-specific: the
relay at `192.168.0.143:18093` IS reached from alertmanager's network, with
iptables INPUT admitting loopback, the host itself and the podman bridge subnet
`10.89.0.0/16` before the DROP (`scripts/deploy-remote.sh:255-262`). Never
point a scrape target at a host LAN IP from its own host on the strength of
one working example; dial the gateway and probe.
## Deployment ## Deployment
@@ -70,12 +70,12 @@ back toward 52. Check `VLLM_CUDAGRAPH_*` actually present in
`--kv-cache-memory-bytes` does NOT appear in the arena arithmetic — the plan `--kv-cache-memory-bytes` does NOT appear in the arena arithmetic — the plan
reserves the registry price, not the actual pool. Raising the pool consumes reserves the registry price, not the actual pool. Raising the pool consumes
real free VRAM without growing the arena; the registry over-prices the pool real free VRAM without growing the arena; the registry OVER-PRICES the pool
(9190 B/token planned vs 7,891.8 measured at 200704 fp8 — 7,701 at 262144; (era G measured ~14 % under the planned B/token — the gap is the lesson, not
**never reuse a B/token ratio across geometries**, and subtract ~3.87 MB the number; **never reuse a B/token ratio across geometries**), and there is a
block-alignment loss when converting a token target to bytes). The ask lands ~4 MB block-alignment loss to subtract when converting a token target to
block-aligned: 3,166,500,000 B → 401,408 tokens; verify with bytes. The ask lands block-aligned: verify against the engine's own
`GPU KV cache size: N tokens, Maximum concurrency … X.Xx`. `GPU KV cache size: N tokens, Maximum concurrency … X.Xx` line at boot.
**Driver-reported free VRAM after hours of traffic is NOT allocatable **Driver-reported free VRAM after hours of traffic is NOT allocatable
arena.** It is the estimator gap: the registry over-prices KV and charges arena.** It is the estimator gap: the registry over-prices KV and charges
@@ -117,11 +117,13 @@ done, and read post-boot free VRAM per rank (`rocm-smi --showmeminfo vram`)
**KV accounting — pin the pool BYTES, floor-sized:** set **KV accounting — pin the pool BYTES, floor-sized:** set
`--kv-cache-memory-bytes` PER RANK to the contract floor `--kv-cache-memory-bytes` PER RANK to the contract floor
(`seqs × MAXLEN × measured B/token`), not the native window — extra pool bytes (`seqs × MAXLEN × measured B/token`), not the native window — extra pool bytes
come out of the arena/headroom and buy nothing. Measured B/token on this come out of the arena/headroom and buy nothing. The B/token rate is a property
hybrid: **7,701 fp8 / 13,839 bf16** (decomposed: attention 6,138/12,276 + of the GEOMETRY: era G (3 × 200,704 fp8, vLLM fork) measured **7,891.8**;
dtype-independent mamba/side 1,563). Measured rates are shape-dependent — the radiance-era hybrid measured ~7,700 fp8 / ~13,800 bf16 at its own shape.
7,891.8 at the 200704×3 shape. Capacity does NOT scale with `SEQS`: the Read your live rate from the boot ledger every time; the decomposition (a
planner's KV reserve is for ONE max-model-len request. dtype-independent mamba/side stream rides on top of the attention KV) is why
the ratio moves. Capacity does NOT scale with `SEQS`: the planner's KV reserve
is for ONE max-model-len request.
**Parallelism ceilings you cannot flag past:** **Parallelism ceilings you cannot flag past:**
@@ -179,18 +181,6 @@ Cutover order: ladder → winner passes and serves → repo env + seam + harness
(`git stash -u`) if the tree is dirty → deploy (one restart) → verify health (`git stash -u`) if the tree is dirty → deploy (one restart) → verify health
+ engine flags in the log. + engine flags in the log.
**Measured ladder on this box (200704×3, fp8 KV, precise memory):**
| shape | util | graphs | arena | result |
|---|---|---|---|---|
| E | 0.98 | 0.55 | 17.22 GiB / 7,522 slots | boot ok |
| F | 0.982 | 0.32 | 17.52 GiB / 7,653 slots | boot ok |
| 1.0 | 1.00 | 0.07 | — | UNBOOTABLE (worker-init ValueError, both ranks) |
| G (deployed) | 0.982 | 0.07 | 17.77 GiB / 7,762 slots | KV 401,408 tokens / 2.00x; 0.89 GiB real free/card held through a live 4,096-token generation at ~38–39 tok/s |
bf16 ceilings at util 0.98: 3 seqs → 241,664 passes / 245,760 and 262,144 OOM
at graph capture; 6 seqs fp8 → 10.70 GiB ask with 9.18 GiB free (fails) —
~207k per context max.
## 7. Arena audit procedure (when asked for "more experts") ## 7. Arena audit procedure (when asked for "more experts")
+34 -21
View File
@@ -10,14 +10,15 @@ tags:
- gfx1151 - gfx1151
- halogen - halogen
- endpoint - endpoint
timestamp: 2026-10-19 timestamp: 2026-10-21
--- ---
# frank-dragoon — halogen-flash-server 0.16.2, native v2 abliterated (Dankclaw iGPU) # frank-dragoon — halogen-flash-server 0.16.2, native v2 abliterated (Dankclaw iGPU)
Strix Halo (gfx1151) endpoint `http://192.168.0.143:7770`, served id `frank-dragoon`: a podman container Strix Halo (gfx1151) endpoint `http://192.168.0.143:7770`, served id `frank-dragoon`: a podman container
under a root `User=dank` unit. Live shape since 2026-10-17, pool raised to 401,408 on 2026-10-18 under a root `User=dank` unit. Live shape since 2026-10-17, pool raised to 401,408 on 2026-10-18
(`2b12716`). This page replaces every per-era dragoon runbook; the Eras table is the rollback record. (`2b12716`), ceiling raised to the native 262,144 on 2026-10-21 so the harness window is 200,000.
This page replaces every per-era dragoon runbook; the Eras table is the rollback record.
## Current shape ## Current shape
@@ -37,10 +38,12 @@ Image `ghcr.io/peonist-ai/halogen-flash-server@sha256:0c61bf84ac22308a53f5d1ca6b
unauthenticated token port 8730 stays on container loopback. Engine stdout → unauthenticated token port 8730 stays on container loopback. Engine stdout →
`/home/dank/logs/frank_dragoon.log`, **not** the journal. `/home/dank/logs/frank_dragoon.log`, **not** the journal.
- Shape: `HALOGEN_CTX=200704` hard per-request ceiling · `HALOGEN_KV_SLOTS=2` · - Shape: `HALOGEN_CTX=262144` hard per-request ceiling = the checkpoint's native window (raised
`HALOGEN_KV_POOL_POSITIONS=401408` = slots × ctx, the top of the band `tests/frank_dragoon_unit.bats` 2026-10-21 from 200704; `HALOGEN_ROPE_YARN` beyond native is not on plan) · `HALOGEN_KV_SLOTS=2` ·
enforces (`ctx ≤ pool ≤ slots × ctx`) · KV ~28 KiB/position → **11.0 GiB reserved** · no KV-dtype knob `HALOGEN_KV_POOL_POSITIONS=401408` inside the band `tests/frank_dragoon_unit.bats` enforces
exists here, the pool is the only KV lever · native window 262,144, `HALOGEN_ROPE_YARN` beyond it. (`ctx ≤ pool ≤ slots × ctx`) · KV ~28 KiB/position → **11.0 GiB reserved** · no KV-dtype knob
exists here, the pool is the only KV lever · the pool carries TWO maximum harness-length (200k)
conversations at once; two literal ceiling-length requests would queue — that is a pool raise.
- Knobs: `HALOGEN_MAX_TOK=8192` sizes the prefill arena (5.7 GiB) and is only the default for requests - Knobs: `HALOGEN_MAX_TOK=8192` sizes the prefill arena (5.7 GiB) and is only the default for requests
omitting `max_tokens` · `HALOGEN_MAX_TOKENS_CAP=65536` + `HALOGEN_QUEUE_TIMEOUT=7200` are a coupled omitting `max_tokens` · `HALOGEN_MAX_TOKENS_CAP=65536` + `HALOGEN_QUEUE_TIMEOUT=7200` are a coupled
pair · MTP depth 2 with the head INSIDE the checkpoint (**`HALOGEN_MTP_HEAD` stays unset**) and pair · MTP depth 2 with the head INSIDE the checkpoint (**`HALOGEN_MTP_HEAD` stays unset**) and
@@ -53,10 +56,11 @@ unauthenticated token port 8730 stays on container loopback. Engine stdout →
(Pin::None)`; no clamp line. Host `MemAvailable` read 22 GiB right after the 2026-10-18 raise — (Pin::None)`; no clamp line. Host `MemAvailable` read 22 GiB right after the 2026-10-18 raise —
~20 GiB above the 20 GiB reserve, against 10–15 GiB of baseline co-tenancy (frank-zealot, ComfyUI, ~20 GiB above the 20 GiB reserve, against 10–15 GiB of baseline co-tenancy (frank-zealot, ComfyUI,
Caddy/observability). Caddy/observability).
- Harness (`~/.omp/agent/models.yml`, provider `llamacpp (dankclaw)`): `contextWindow: 155648` - Harness (`~/.omp/agent/models.yml`, provider `llamacpp (dankclaw)`): `contextWindow: 200000`
(= ceiling − 45k per the 400-zone law, never higher), `maxTokens: 45056`, `input: [text, image]` — sits 62,144 under the ceiling, clearing the ~45k fit-law gap (400-zone law below);
`reasoningContentField: "reasoning_content"`, `high→xhigh` fold, `extraBody` 1.0/0.95. Compaction `tests/frank_dragoon_unit.bats` fails if the page's declared window and `HALOGEN_CTX` drift back
rides [[frank-zealot]]. together. `maxTokens: 45056`, `input: [text, image]`, `reasoningContentField: "reasoning_content"`,
`high→xhigh` fold, `extraBody` 1.0/0.95. Compaction rides [[frank-zealot]].
- Deploy `./scripts/deploy.sh dankclaw --service frank-dragoon`; gate = boot-ledger greps (weights, - Deploy `./scripts/deploy.sh dankclaw --service frank-dragoon`; gate = boot-ledger greps (weights,
pool, MTP, prompt cache, **no clamp**) + `/health` 200 + a tool call + the 512 px red-PNG probe. pool, MTP, prompt cache, **no clamp**) + `/health` 200 + a tool call + the 512 px red-PNG probe.
Kernel: `CONFIG_HSA_AMD_SVM=y` (CachyOS ships it) and `amdgpu.noretry=0` — the env says check it at Kernel: `CONFIG_HSA_AMD_SVM=y` (CachyOS ships it) and `amdgpu.noretry=0` — the env says check it at
@@ -124,14 +128,18 @@ created it — earlier topologies in .
prompt tokens. halogen 0.1.4's auto-sizing refused here and re-prefilled every 79–81k turn in prompt tokens. halogen 0.1.4's auto-sizing refused here and re-prefilled every 79–81k turn in
85–180 s until `HALOGEN_CACHE_MB=16384` was pinned. What the pool takes is what the table loses, so 85–180 s until `HALOGEN_CACHE_MB=16384` was pinned. What the pool takes is what the table loses, so
re-measure the warm ratio after any pool change. re-measure the warm ratio after any pool change.
- **400-zone law:** the harness window MUST sit ~45k under the engine ceiling (`contextWindow: - **400-zone law:** the harness window MUST sit ~45k under the engine ceiling, because the harness
155648` = 200,704 − 45,056) because the harness clamps `max_tokens` to `contextWindow − est − 4096` clamps `max_tokens` to `contextWindow − est − 4096`, its chars/4 `est` UNDER-counts code/agent
its chars/4 `est` under-counts code/agent traffic by ~3 %, and compaction only fires at 85 %. The traffic by ~3 %, and the compaction trigger fires at 95 % of the window (`config.yml
3 % is measured, not theory: 2026-10-07 at the old 200,192 ceiling, est 152,938 vs server 157,388 — thresholdPercent`, while the comments designed around 85 % — check the trigger BEFORE touching a
over by 354, a 400. At the live shape compaction fires at 132,301 est ≈ 136k measured (the 180k window; that disagreement is why long sessions kept drawing 400s). The 3 % is measured, not
target of the 262,144 era is unreachable — the window itself is 155,648), and the worst-case request theory: 2026-10-07 at the old 200,192 ceiling, est 152,938 vs server 157,388 — over by 354, a 400.
is 155,648 − 4,096 + 3 % ≈ 156.2k ≤ 200,704: ~9.5× the error of margin, robust to a ~25 % At the live pair (ceiling 262,144 / window 200,000) the worst-case ordinary request is
under-count. Compaction rides [[frank-zealot]] at ≈ threshold − keepRecent(20k) ≈ 112k input. 200,000 − 4,096 + 3 % ≈ 201.8k, and the trigger point is 190,000 est × 1.03 + 45,056 ≈ 240.7k —
both inside the ceiling with ~60k of margin. Compaction rides [[frank-zealot]] at
≈ threshold − keepRecent(20k) ≈ 175k measured against its 200,704-token slot. The seam in
`tests/frank_dragoon_unit.bats` re-runs this arithmetic at the 95 % trigger and reads the window
from THIS page, so ceiling/window pairs that drift back together fail the suite, not a session.
- **Admission reserves prompt + `max_tokens`:** an un-fittable request waits in arrival order, then - **Admission reserves prompt + `max_tokens`:** an un-fittable request waits in arrival order, then
503s `engine_busy` for the whole queue after `QUEUE_TIMEOUT` — and the cap/timeout pair moves 503s `engine_busy` for the whole queue after `QUEUE_TIMEOUT` — and the cap/timeout pair moves
together: 65536 tokens at ~10 t/s serial needs >6550 s, which is why 7200 is pinned (env, 2026-10-17). together: 65536 tokens at ~10 t/s serial needs >6550 s, which is why 7200 is pinned (env, 2026-10-17).
@@ -248,9 +256,14 @@ lane is **armed and serving** since 2026-10-19 (`HALOGEN_NPU_MODELS=qwen3-embedd
(`dankclaw/udev/98-halogen-npu-access.rules`, installed and re-triggered by the deployer, `0666` like the GPU (`dankclaw/udev/98-halogen-npu-access.rules`, installed and re-triggered by the deployer, `0666` like the GPU
compute nodes beside it); the serve script refuses any node that is not other-accessible, so the fault cannot compute nodes beside it); the serve script refuses any node that is not other-accessible, so the fault cannot
return as a crash-loop. return as a crash-loop.
2. The host's XRT reachable inside: there is **no `/opt/xilinx/xrt`** here — distro packages put the three files the 2. The host's XRT reachable inside, at **THREE prefixes per library**: there is **no `/opt/xilinx/xrt`** here —
container needs (`libxrt_coreutil.so.2`, `libxrt_core.so.2`, `libxrt_driver_xdna.so.2`) in distro packages put the three files the container needs (`libxrt_coreutil.so.2`, `libxrt_core.so.2`,
`/usr/lib/x86_64-linux-gnu`, where all three exist. `libxrt_driver_xdna.so.2`) in `/usr/lib/x86_64-linux-gnu`. The serve script mounts each REAL file (never a
symlink) at `/opt/xilinx/xrt/lib/`, at its own path, AND at `/opt/xilinx/lib/<multiarch>/` — the NPU child
searches that third prefix and a missing one is fatal: since the host's `libxrt2` was reconfigured
2026-10-20 the child died with `No such library '/opt/xilinx/lib/x86_64-linux-gnu/libxrt_core.so.2'` and the
engine supervisor took the WHOLE container down on it (88-restart loop, chat dead beside a healthy GPU line).
A dead lane is not survivable here — arm the lane only with all three prefixes mounted.
3. **The GPU fabric clock held at its top level, or the engine refuses to start.** GPU and NPU work share the die: 3. **The GPU fabric clock held at its top level, or the engine refuses to start.** GPU and NPU work share the die:
while `pp_dpm_fclk` is changing speed, concurrent GPU work can hang the machine and corrupt NPU results. The while `pp_dpm_fclk` is changing speed, concurrent GPU work can hang the machine and corrupt NPU results. The
engine holds the clock itself only when it runs as root with `/sys` mounted; `frank-dragoon` is a **rootless** engine holds the clock itself only when it runs as root with `/sys` mounted; `frank-dragoon` is a **rootless**
@@ -104,14 +104,14 @@
{ {
"id": 6, "id": 6,
"type": "timeseries", "type": "timeseries",
"title": "Memory vs limit (%)", "title": "Memory vs host total (%)",
"description": "Working set over the cgroup memory limit (container_spec_memory_limit_bytes). Only containers that carry a limit are shown; approaching 1.0 means an OOM is near. Containers with no limit are omitted.", "description": "Working set over the host's total RAM. The fleet sets NO cgroup memory limits (every container_spec_memory_limit_bytes is 0 — a limit-share panel is structurally empty here), so the honest pressure view is share of the host. The join labels the node exporter's MemTotal onto the cadvisor series by host.",
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 11 }, "gridPos": { "h": 9, "w": 12, "x": 12, "y": 11 },
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "unit": "percentunit", "min": 0, "max": 1, "custom": { "fillOpacity": 8, "lineWidth": 1, "stacking": { "mode": "none" } } }, "overrides": [] }, "fieldConfig": { "defaults": { "unit": "percentunit", "min": 0, "max": 1, "custom": { "fillOpacity": 8, "lineWidth": 1, "stacking": { "mode": "none" } } }, "overrides": [] },
"targets": [ "targets": [
{ {
"expr": "(container_memory_working_set_bytes{job=~\"cadvisor-.*\", host=~\"$host\", container=~\"$container\"} / on (job, host, container) group_left container_spec_memory_limit_bytes{job=~\"cadvisor-.*\", host=~\"$host\", container=~\"$container\"}) and (container_spec_memory_limit_bytes{job=~\"cadvisor-.*\", host=~\"$host\", container=~\"$container\"} > 0)", "expr": "container_memory_working_set_bytes{job=~\"cadvisor-.*\", host=~\"$host\", container=~\"$container\", container!=\"\"} / on(host) group_left label_replace(node_memory_MemTotal_bytes{job=~\"node-.*\"}, \"host\", \"$1\", \"job\", \"node-(.*)\")",
"legendFormat": "{{host}} / {{container}}", "legendFormat": "{{host}} / {{container}}",
"refId": "A" "refId": "A"
} }
@@ -54,7 +54,7 @@
"gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 }, "gridPos": { "h": 4, "w": 6, "x": 12, "y": 0 },
"datasource": { "type": "prometheus", "uid": "prometheus" }, "datasource": { "type": "prometheus", "uid": "prometheus" },
"fieldConfig": { "defaults": { "unit": "short", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 0.7 }, { "color": "red", "value": 1 }] } }, "overrides": [] }, "fieldConfig": { "defaults": { "unit": "short", "thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }, { "color": "yellow", "value": 0.7 }, { "color": "red", "value": 1 }] } }, "overrides": [] },
"targets": [{ "expr": "node_load5{job=~\"$node\"} / count by (job) (node_cpu_seconds_total{job=~\"$node\",mode=\"idle\"})", "legendFormat": "{{job}}", "refId": "A" }] "targets": [{ "expr": "node_load5{job=~\"$node\"} / on(job) count by (job) (node_cpu_seconds_total{job=~\"$node\",mode=\"idle\"})", "legendFormat": "{{job}}", "refId": "A" }]
}, },
{ {
"id": 4, "type": "stat", "title": "Open file handles", "id": 4, "type": "stat", "title": "Open file handles",
@@ -194,7 +194,7 @@
"id": 6, "id": 6,
"type": "timeseries", "type": "timeseries",
"title": "Firing fleet alerts", "title": "Firing fleet alerts",
"description": "The LLM-fleet rules from shared/prometheus/rules/llm-fleet.yml. A firing alert is delivered to Signal: Prometheus -> Alertmanager (dankclaw quadlet) -> the host-side signal-relay user unit -> signal-cli, since 2026-09-17.", "description": "The LLM-fleet rules from shared/prometheus/rules/llm-fleet.yml. EMPTY IS THE HEALTHY STATE: zero series means nothing is firing (ALERTS only carries active/pending alert series — do not read this panel as broken). A firing alert is delivered to Signal: Prometheus -> Alertmanager (dankclaw quadlet) -> the host-side signal-relay user unit -> signal-cli, since 2026-09-17.",
"gridPos": { "gridPos": {
"h": 8, "h": 8,
"w": 12, "w": 12,
+7 -4
View File
@@ -140,12 +140,15 @@ scrape_configs:
# system.slice daemons and machine.slice scopes carry no such name and are # system.slice daemons and machine.slice scopes carry no such name and are
# dropped, as is the per-CPU-core split (only cpu="total" survives), so the # dropped, as is the per-CPU-core split (only cpu="total" survives), so the
# scrape stays small. cadvisor cannot bind per-interface, so it is reached at # scrape stays small. cadvisor cannot bind per-interface, so it is reached at
# the host's PUBLISHED LAN port: unlike node-exporter's host-net bind (which # the podman gateway like every same-host target: a BRIDGE CONTAINER CANNOT
# is why node-dankclaw scrapes via host.containers.internal), a published port # DIAL ITS OWN HOST'S LAN IP AT ALL — the netavark hairpin gap refuses the
# IS reachable from this bridge container at the host LAN address. # connection whether the port is a host-net bind or a published port (the
# original note here claimed published ports were exempt; measured 2026-10-21
# from inside this container: LAN refused, gateway answers). Cross-host
# targets (dankserver) have no hairpin problem and stay plain LAN.
- job_name: "cadvisor-dankclaw" - job_name: "cadvisor-dankclaw"
static_configs: static_configs:
- targets: ["192.168.0.143:8080"] - targets: ["host.containers.internal:8080"]
labels: labels:
host: dankclaw host: dankclaw
metric_relabel_configs: metric_relabel_configs:
+50 -35
View File
@@ -3,7 +3,7 @@
# of the abliterated Qwen3.8-Flash-Next (a .hgn flash-server payload) on the # of the abliterated Qwen3.8-Flash-Next (a .hgn flash-server payload) on the
# Strix Halo iGPU through the halogen-flash-server CONTAINER (0.16.2, # Strix Halo iGPU through the halogen-flash-server CONTAINER (0.16.2,
# digest-pinned), driven by the root unit's serve script — 2 slots over ONE # digest-pinned), driven by the root unit's serve script — 2 slots over ONE
# 200,704-position KV pool with the MTP head always on (HALOGEN_SPEC_ADAPT=0) # 401,408-position KV pool at the native 262,144 ceiling, MTP head always on (HALOGEN_SPEC_ADAPT=0)
# and the vision tower mounted. 2026-10-17 cutover from the Swift1.5-finetune # and the vision tower mounted. 2026-10-17 cutover from the Swift1.5-finetune
# shape, which itself cut over the halogen 27B-dense shape on 2026-10-06. # shape, which itself cut over the halogen 27B-dense shape on 2026-10-06.
# #
@@ -85,7 +85,7 @@ good_env() {
"FRANK_DRAGOON_VISION_BYTES=0" "FRANK_DRAGOON_VISION_BYTES=0"
"FRANK_DRAGOON_TOKENIZER_FILES=tok.json" "FRANK_DRAGOON_TOKENIZER_FILES=tok.json"
"HALOGEN_MODEL_ID=frank-dragoon" "HALOGEN_MODEL_ID=frank-dragoon"
"HALOGEN_CTX=200704" "HALOGEN_CTX=262144"
"HALOGEN_KV_POOL_POSITIONS=401408" "HALOGEN_KV_POOL_POSITIONS=401408"
"HALOGEN_KV_SLOTS=2" "HALOGEN_KV_SLOTS=2"
"HALOGEN_SPEC_ADAPT=0" "HALOGEN_SPEC_ADAPT=0"
@@ -159,9 +159,11 @@ good_env() {
reserve="$(sed -nE 's/^HALOGEN_HOST_RESERVE_GIB=([0-9]+)$/\1/p' "$ENV_FILE")" reserve="$(sed -nE 's/^HALOGEN_HOST_RESERVE_GIB=([0-9]+)$/\1/p' "$ENV_FILE")"
# The pool is ONE budget behind the slots (boot ledger form: "2 slots over # The pool is ONE budget behind the slots (boot ledger form: "2 slots over
# ONE <pool>-position KV pool, each request up to HALOGEN_CTX"), NOT # ONE <pool>-position KV pool, each request up to HALOGEN_CTX"), NOT
# a per-slot private pool. Since 2026-10-18 it is sized pool = slots * ctx # a per-slot private pool. Since 2026-10-21 the ceiling is the checkpoint's
# (401,408 = 2 x 200,704), so two MAXIMUM-length conversations can be # native 262,144 while the pool stays 401,408 (raised 2026-10-18): two
# resident at once — frank-zealot parity on total in-flight context. What # MAXIMUM harness-length (200,000-token) conversations are resident at once;
# two literal ceiling-length requests would queue — widening that is a pool
# raise, not a window move.
# still binds: the pool must back ONE full-length request (below that the # still binds: the pool must back ONE full-length request (below that the
# advertised per-request ceiling would be unreachable, hence pool >= ctx), # advertised per-request ceiling would be unreachable, hence pool >= ctx),
# it must not pretend to be more than slots x ctx (pool <= slots * ctx), and # it must not pretend to be more than slots x ctx (pool <= slots * ctx), and
@@ -287,7 +289,7 @@ $(cat "$TEST_TMP/serve.log")"
[[ "$output" == *"--name frank-dragoon-halogen"* ]] [[ "$output" == *"--name frank-dragoon-halogen"* ]]
[[ "$output" == *"--publish 7770:8731"* ]] [[ "$output" == *"--publish 7770:8731"* ]]
[[ "$output" == *"-e HALOGEN_MODEL_ID=frank-dragoon"* ]] [[ "$output" == *"-e HALOGEN_MODEL_ID=frank-dragoon"* ]]
[[ "$output" == *"-e HALOGEN_CTX=200704"* ]] [[ "$output" == *"-e HALOGEN_CTX=262144"* ]]
[[ "$output" == *"-e HALOGEN_KV_POOL_POSITIONS=401408"* ]] [[ "$output" == *"-e HALOGEN_KV_POOL_POSITIONS=401408"* ]]
[[ "$output" == *"-e HALOGEN_KV_SLOTS=2"* ]] [[ "$output" == *"-e HALOGEN_KV_SLOTS=2"* ]]
[[ "$output" == *"-e HALOGEN_SPEC_ADAPT=0"* ]] [[ "$output" == *"-e HALOGEN_SPEC_ADAPT=0"* ]]
@@ -306,27 +308,32 @@ $(cat "$TEST_TMP/serve.log")"
@test "the shipped env keeps the harness fit law inside the ceiling" { @test "the shipped env keeps the harness fit law inside the ceiling" {
# HALOGEN_CTX is the hard per-request ceiling (a longer prompt is a 400 # HALOGEN_CTX is the hard per-request ceiling (a longer prompt is a 400
# naming the limit, never a silent truncation). The law is INVARIANT to # naming the limit, never a silent truncation). The law: the harness
# the value: the harness window must sit ~45k UNDER it, because the # window must sit ~45k UNDER the ceiling, because the harness clamps
# harness clamps max_tokens = min(45056, window − est − 4096), compacts # max_tokens = min(45056, window − est − 4096), compacts at the trigger
# at 85 % of its declared window, and its chars/4 est UNDER-counts the # percent of its declared window, and its chars/4 est UNDER-counts the
# rendered prompt by ~3 % on code/agent traffic (measured 4,450 tokens at # rendered prompt by ~3 % on code/agent traffic (measured 4,450 tokens
# est 152,938 on 2026-10-07 — with window == ceiling, every request over # at est 152,938 on 2026-10-07 — with window ≈ ceiling, every long
# ~65 % of the window was a guaranteed 400 before compaction could fire). # request is a guaranteed 400 before compaction can fire). The 2026-10-21
# The shipped pair is ctx 200704 / window 155648 (lowered from # pair: ceiling 262144 (native) under a harness window of 200,000.
# 262144/217088 on 2026-10-17); the gap must absorb the error at BOTH # The declared window is read from the endpoint page — the one owner —
# monotone maxima of est + budget (est ≤ window − 4096): # so env, models.yml and page that disagree FAIL HERE, not as a 400
# full budget at the compaction threshold: 1.03*132300 + 45056 # mid-session (the recurring failure this seam exists for).
# clamped budget at the clamp's zero point: 1.03*151552 + 0 grep -qE '^HALOGEN_CTX=262144$' "$ENV_FILE"
# and the compaction request that rides on frank-zealot (its ctx="$(sed -nE 's/^HALOGEN_CTX=([0-9]+)$/\1/p' "$ENV_FILE")"
# 200,704-token slot) must fit: input ≈ threshold − keepRecent(20k). page="$ROOT/serverAppConfigs-docs/LLM/frank-dragoon.md"
grep -qE '^HALOGEN_CTX=200704$' "$ENV_FILE" grep -qE 'contextWindow: 200000' "$page"
ctx=200704 hw="$(grep -oE 'contextWindow: [0-9]+' "$page" | head -1 | grep -oE '[0-9]+')"
hw=155648; mt=45056 mt=45056
thr=$(( hw * 85 / 100 )) # Invariant checked at the live 95 % trigger (config.yml
# thresholdPercent) — strictly stronger than the 85 % design point.
# Both monotone maxima of est + budget (est ≤ window − 4096):
thr=$(( hw * 95 / 100 ))
[ $(( thr * 103 / 100 + mt )) -le "$ctx" ] [ $(( thr * 103 / 100 + mt )) -le "$ctx" ]
zero=$(( hw - 4096 )) zero=$(( hw - 4096 ))
[ $(( zero * 103 / 100 )) -le "$ctx" ] [ $(( zero * 103 / 100 )) -le "$ctx" ]
# The compaction request rides on frank-zealot's 200,704-token slot:
# input ≈ threshold − keepRecent(20k).
[ $(( (thr - 20000) * 103 / 100 )) -le 200704 ] [ $(( (thr - 20000) * 103 / 100 )) -le 200704 ]
} }
@@ -423,9 +430,12 @@ npu_fixture() {
# the stock root:render 0660 mode, and the serve gate checks exactly that. # the stock root:render 0660 mode, and the serve gate checks exactly that.
touch "$TEST_TMP/accel0" touch "$TEST_TMP/accel0"
chmod 666 "$TEST_TMP/accel0" chmod 666 "$TEST_TMP/accel0"
mkdir -p "$TEST_TMP/xrt" # basename carries a multiarch shape: the third mount prefix is derived
# from the XRT dir's basename (the NPU child searches
# /opt/xilinx/lib/<multiarch>/, measured 2026-10-21).
mkdir -p "$TEST_TMP/x86_64-linux-gnu"
for l in libxrt_coreutil.so.2 libxrt_core.so.2 libxrt_driver_xdna.so.2; do for l in libxrt_coreutil.so.2 libxrt_core.so.2 libxrt_driver_xdna.so.2; do
touch "$TEST_TMP/xrt/$l" touch "$TEST_TMP/x86_64-linux-gnu/$l"
done done
mkdir -p "$TEST_TMP/npu/qwen3-embedding-0.6b/devices" mkdir -p "$TEST_TMP/npu/qwen3-embedding-0.6b/devices"
mkdir -p "$TEST_TMP/sys/class/drm/card1/device" mkdir -p "$TEST_TMP/sys/class/drm/card1/device"
@@ -435,7 +445,7 @@ npu_fixture() {
"HALOGEN_NPU_MODELS=qwen3-embedding-0.6b" "HALOGEN_NPU_MODELS=qwen3-embedding-0.6b"
"FRANK_DRAGOON_NPU_DIR=$TEST_TMP/npu" "FRANK_DRAGOON_NPU_DIR=$TEST_TMP/npu"
"FRANK_DRAGOON_DEV_ACCEL=$TEST_TMP/accel0" "FRANK_DRAGOON_DEV_ACCEL=$TEST_TMP/accel0"
"FRANK_DRAGOON_XRT_DIR=$TEST_TMP/xrt" "FRANK_DRAGOON_XRT_DIR=$TEST_TMP/x86_64-linux-gnu"
"FRANK_DRAGOON_SYSFS=$TEST_TMP/sys" "FRANK_DRAGOON_SYSFS=$TEST_TMP/sys"
) )
} }
@@ -465,11 +475,15 @@ $(cat "$TEST_TMP/serve.log")"
! grep -q 'accel0' "$TEST_TMP/serve.log" ! grep -q 'accel0' "$TEST_TMP/serve.log"
} }
@test "the lane arms the accel node, the staged model dir and each XRT library twice" { @test "the lane arms the accel node, the staged model dir and each XRT library three times" {
# Each library mounts at the image's XRT path AND at its own path: the # Each library mounts at the image's XRT path, at its own path (the distro
# distro libraries find each other where they were installed, and an # libraries find each other where they were installed), and at
# /opt/xilinx/xrt of symlinks into the system dir dangles inside the # /opt/xilinx/lib/<multiarch>/ — the prefix the NPU child searches and
# container (the vendor calls that out by name). # names when it is missing (`No such library
# '/opt/xilinx/lib/x86_64-linux-gnu/libxrt_core.so.2'`, measured
# 2026-10-21: the lane died and the supervisor took the whole container
# with it, 88-restart loop). An /opt/xilinx/xrt of symlinks into the
# system dir dangles inside the container (the vendor calls that out).
podman_stub podman_stub
good_env good_env
npu_fixture npu_fixture
@@ -484,8 +498,9 @@ $(cat "$TEST_TMP/serve.log")"
[[ "$output" != *":/models/npu"* ]] [[ "$output" != *":/models/npu"* ]]
[[ "$output" == *"-e HALOGEN_NPU_MODELS=qwen3-embedding-0.6b"* ]] [[ "$output" == *"-e HALOGEN_NPU_MODELS=qwen3-embedding-0.6b"* ]]
for l in libxrt_coreutil.so.2 libxrt_core.so.2 libxrt_driver_xdna.so.2; do for l in libxrt_coreutil.so.2 libxrt_core.so.2 libxrt_driver_xdna.so.2; do
[[ "$output" == *"-v $TEST_TMP/xrt/$l:/opt/xilinx/xrt/lib/$l:ro"* ]] [[ "$output" == *"-v $TEST_TMP/x86_64-linux-gnu/$l:/opt/xilinx/xrt/lib/$l:ro"* ]]
[[ "$output" == *"-v $TEST_TMP/xrt/$l:$TEST_TMP/xrt/$l:ro"* ]] [[ "$output" == *"-v $TEST_TMP/x86_64-linux-gnu/$l:$TEST_TMP/x86_64-linux-gnu/$l:ro"* ]]
[[ "$output" == *"-v $TEST_TMP/x86_64-linux-gnu/$l:/opt/xilinx/lib/x86_64-linux-gnu/$l:ro"* ]]
done done
# The Flash payload still rides the same container: the lane is additive. # The Flash payload still rides the same container: the lane is additive.
[[ "$output" == *"-e HALOGEN_CHECKPOINT=/models/qwen.hgn"* ]] [[ "$output" == *"-e HALOGEN_CHECKPOINT=/models/qwen.hgn"* ]]
@@ -560,7 +575,7 @@ $(cat "$TEST_TMP/serve.log")"
podman_stub podman_stub
good_env good_env
npu_fixture npu_fixture
rm "$TEST_TMP/xrt/libxrt_driver_xdna.so.2" rm "$TEST_TMP/x86_64-linux-gnu/libxrt_driver_xdna.so.2"
run_with_log run_with_log
[ "$status" -ne 0 ] [ "$status" -ne 0 ]
[[ "$output" == *"libxrt_driver_xdna.so.2 missing"* ]] [[ "$output" == *"libxrt_driver_xdna.so.2 missing"* ]]
+30
View File
@@ -195,3 +195,33 @@ for want in ('gpu_utilization_percent', 'gpu_memory_used_bytes',
print('gpu dashboard panels ok') print('gpu dashboard panels ok')
" "
} }
@test "--serve answers on EVERY declared bind, not just the first" {
# The deployed unit serves loopback + the host LAN address, and Prometheus
# reaches the host through the gateway — which dials the LAN-class bind.
# A server that only runs serve_forever() on the first socket leaves the
# second one listen()ing-but-never-accepting: the deploy health gate on
# loopback passes, every scrape on the other bind dies on the SYN backlog,
# and no gate but this one sees it. (This exact bug shipped 2026-10-21 and
# left both gpu-* targets down beside a green deploy.)
mk_amd 0 42 1073741824 34359738368 "" 71000
port=$((19400 + RANDOM % 500))
python3 "$EXPORTER" --serve --amdgpu-sysfs "$SYS" --nvidia-cmd false \
--bind 127.0.0.1 --bind 127.0.0.2 --port "$port" \
> "$BATS_TEST_TMPDIR/serve.log" 2>&1 &
local pid=$!
trap 'kill "$pid" 2>/dev/null' EXIT
local ok=0
for _ in $(seq 1 20); do
curl -fsS -m2 "http://127.0.0.1:$port/metrics" >/dev/null 2>&1 && { ok=1; break; }
sleep 0.25
done
[ "$ok" -eq 1 ] || { kill "$pid" 2>/dev/null; return 1; }
# Both binds must ANSWER within 2 s each — the second one is the bug.
for b in 127.0.0.1 127.0.0.2; do
curl -fsS -m2 "http://$b:$port/metrics" | grep -q '^gpu_utilization_percent{' \
|| { echo "bind $b did not answer /metrics"; kill "$pid" 2>/dev/null; return 1; }
done
kill "$pid" 2>/dev/null
trap - EXIT
}
+13 -7
View File
@@ -595,16 +595,22 @@ assert any('ALERTS' in e for e in exprs), exprs
} }
@test "Prometheus scrapes cadvisor where it is published and relabels the cgroup path to a container name" { @test "Prometheus scrapes cadvisor where it is published and relabels the cgroup path to a container name" {
# Cross-file contract: cadvisor publishes the LAN port; a cadvisor scrape # Cross-file contract, measured 2026-10-21 from inside the Prometheus
# job pointing anywhere else reads nothing (unlike node-exporter's host-net # container: a bridge container cannot dial its OWN host's LAN IP — the
# bind, a published port IS reachable from this bridge container at the LAN # hairpin refuses published ports exactly like host-net binds, so
# address). And the whole dashboard depends on the id -> container relabel: # cadvisor-dankclaw must scrape via the podman gateway (the original pin
# cadvisor names nothing itself, the quadlet unit name is only in the cgroup # here demanded the LAN address and shipped a permanently-down target).
# path. If either half changes alone, the Containers dashboard is blank. # dankserver is a different machine: plain LAN there. And the whole
# dashboard depends on the id -> container relabel: cadvisor names nothing
# itself, the quadlet unit name is only in the cgroup path. If either half
# changes alone, the Containers dashboard is blank.
local cfg="$REPO_ROOT/shared/prometheus/prometheus.yml" local cfg="$REPO_ROOT/shared/prometheus/prometheus.yml"
grep -q 'job_name: "cadvisor-dankclaw"' "$cfg" || { echo "no cadvisor-dankclaw job"; return 1; } grep -q 'job_name: "cadvisor-dankclaw"' "$cfg" || { echo "no cadvisor-dankclaw job"; return 1; }
grep -q 'job_name: "cadvisor-dankserver"' "$cfg" || { echo "no cadvisor-dankserver job"; return 1; } grep -q 'job_name: "cadvisor-dankserver"' "$cfg" || { echo "no cadvisor-dankserver job"; return 1; }
grep -q '"192.168.0.143:8080"' "$cfg" || { echo "cadvisor-dankclaw does not scrape the published LAN port"; return 1; } grep -q '"host.containers.internal:8080"' "$cfg" \
|| { echo "cadvisor-dankclaw does not scrape via the podman gateway"; return 1; }
! grep -q '"192.168.0.143:8080"' "$cfg" \
|| { echo "cadvisor-dankclaw dials its own host LAN IP — hairpin refuses it"; return 1; }
grep -q '"192.168.0.235:8080"' "$cfg" || { echo "cadvisor-dankserver does not scrape the published LAN port"; return 1; } grep -q '"192.168.0.235:8080"' "$cfg" || { echo "cadvisor-dankserver does not scrape the published LAN port"; return 1; }
# The relabel that turns …/app.slice/<name>.service… into a `container`. # The relabel that turns …/app.slice/<name>.service… into a `container`.
grep -q 'app\\.slice/(\[\^\.\]+)\\.service' "$cfg" || { echo "no cgroup-path -> container relabel"; return 1; } grep -q 'app\\.slice/(\[\^\.\]+)\\.service' "$cfg" || { echo "no cgroup-path -> container relabel"; return 1; }