frank-primary: R9V radiance cutover wiring (launcher-driven serve + state gates)

Replace the vLLM 0.30 UVA-offload wiring (never deployed; llama.cpp
b11030 endpoint replaced by Frank's order, downtime window open) with
the R9V radiance-vLLM shape on the same root unit:

- serve-frank-primary.sh: layer the on-host qualified setup state
  (104 tuned R9V_* keys, /home/dank/r9v-state/uncensored/setup.json)
  under repo production overrides, then exec the fork's own launcher
  through the docker->podman shim and block on docker wait so a crashed
  engine is a nonzero ExecStart (launch.sh --detach alone would fake
  green). Gates: pinned image by OCI manifest digest in RepoDigests,
  PLE at pinned byte count (stat -c %s), all GGUF shards + mmproj.
- frank-primary.env: thin production overrides only (LAN bind 0.0.0.0,
  :7770, alias frank-primary, container frank-primary-r9v, GPU BDFs);
  tuned engine shape stays in the setup state by design.
- frank-primary.service: same root-unit shape (User=dank,
  LimitMEMLOCK=infinity for the tiered-expert pin), plus ExecStop
  podman stop — docker wait cannot stop the container child.
- deploy-remote.sh deploy_llm_primary: state-driven gates (state ready,
  image digest present in rootless storage, PLE bytes, shard files)
  replace the fp030 shard-index gate; false-green MainPID check kept.
- tests/frank_primary_unit.bats: seam rewritten for the R9V shape.
This commit is contained in:
Frank
2026-09-27 04:05:46 -07:00
parent baba84fc90
commit 095128b669
5 changed files with 282 additions and 349 deletions
+55 -26
View File
@@ -1183,42 +1183,70 @@ deploy_llm_small() {
wait_for_health frank-small http://127.0.0.1:7771/health 900 wait_for_health frank-small http://127.0.0.1:7771/health 900
} }
# deploy_llm_primary — frank-primary is vLLM 0.30 (locally built ROCm image, # deploy_llm_primary — frank-primary is the R9V radiance-vLLM fork (docker-
# gfx1201) serving Qwen3.8-Flash-Next-FP8 on the two R9700s as a ROOT unit # pinned prebuilt, gfx1201, driven through the docker->podman shim) serving
# whose serve script execs a rootless-podman container as dank. Same port and # Qwen3.8-Flash-Next-Uncensored IQ4_XS with the MTP4 drafter on the two
# alias as the llama.cpp endpoint it replaces (2026-09-27 cutover); the # R9700s as a ROOT unit whose serve script execs the fork's launcher through
# harness models.yml and homepage cards are unchanged. # rootless podman as dank. Same port and alias as the llama.cpp endpoint it
# replaced (2026-09-27 cutover); the harness models.yml id is unchanged.
# #
# Gates: the image must exist in dank's rootless storage, and the payload # Gates: the on-host R9V setup state must exist and be ready (it carries the
# must be complete — every shard named by the model's own # pinned image digest and every payload path), the pinned runtime image must
# model.safetensors.index.json present on disk (131 shards, 172.78 GiB; # be loadable from dank's rootless storage by its OCI manifest digest, the
# per-shard sha256 + byte-count verification is download-frank-primary-fp8.sh's # PLE payload must match its pinned byte count, and every GGUF shard + the
# job at fetch time, never re-hashed here). # mmproj must exist (per-file sha256 verification was setup's job at fetch
LLM_PRIMARY_IMAGE=localhost/frank-primary:vllm030 # time, never re-hashed here).
LLM_PRIMARY_ROOT=/home/dank/models/Qwen3.8-Flash-Next-FP8 LLM_PRIMARY_STATE=/home/dank/r9v-state/uncensored/setup.json
LLM_PRIMARY_CONTAINER=frank-primary-vllm LLM_PRIMARY_CONTAINER=frank-primary-r9v
deploy_llm_primary() { deploy_llm_primary() {
# Refuse rather than restart-loop: a unit whose image or payload is # Refuse rather than restart-loop: a unit whose image or payload is
# missing never becomes healthy, and a green deploy would leave the # missing never becomes healthy, and a green deploy would leave the
# endpoint dark with nothing in the log to say why. # endpoint dark with nothing in the log to say why.
if ! podman image inspect "$LLM_PRIMARY_IMAGE" >/dev/null 2>&1; then if [[ ! -f "$LLM_PRIMARY_STATE" ]]; then
echo "ERROR: image absent: $LLM_PRIMARY_IMAGE" >&2 echo "ERROR: R9V setup state absent: $LLM_PRIMARY_STATE" >&2
echo "frank-primary: build the fp030 image first (see the runbook)" >&2 echo "frank-primary: run the profile setup on this host first" >&2
return 1 return 1
fi fi
local want_shards have_shards local state_json image_digest model_dir ple_path ple_bytes
want_shards="$(python3 - "$LLM_PRIMARY_ROOT/model.safetensors.index.json" <<'PY' state_exports="$(python3 - "$LLM_PRIMARY_STATE" <<'PY'
import json, sys import json, shlex, sys
print(len(set(json.load(open(sys.argv[1]))["weight_map"].values()))) state = json.load(open(sys.argv[1]))
config = state.get("config") or {}
if state.get("ready") is not True:
sys.exit("R9V setup state is not ready")
for key in ("R9V_IMAGE", "R9V_MODEL_DIR", "R9V_PLE_PATH", "R9V_PLE_EXPECTED_BYTES",
"R9V_TARGET_REL", "R9V_TARGET_SHARD2_REL", "R9V_TARGET_SHARD3_REL", "R9V_MMPROJ_REL"):
print(f"{key}={shlex.quote(config.get(key, ''))}")
PY PY
)" || return 1 )" || return 1
have_shards="$(find "$LLM_PRIMARY_ROOT" -maxdepth 1 -name '*.safetensors' | wc -l)" eval "$state_exports"
if [[ -z "$want_shards" || "$have_shards" != "$want_shards" ]]; then if [[ -z "$R9V_IMAGE" || -z "$R9V_MODEL_DIR" || -z "$R9V_PLE_PATH" ]]; then
echo "ERROR: payload incomplete at $LLM_PRIMARY_ROOT (have $have_shards shards, index names $want_shards)" >&2 echo "ERROR: R9V setup state is missing image/model/PLE paths" >&2
echo "frank-primary: run scripts/download-frank-primary-fp8.sh on this host" >&2
return 1 return 1
fi fi
if ! podman images --format '{{.RepoDigests}}' | grep -qF "${R9V_IMAGE#sha256:}"; then
echo "ERROR: pinned R9V runtime image absent from rootless storage: $R9V_IMAGE" >&2
echo "frank-primary: load the image bundle (./r9v setup ...)" >&2
return 1
fi
if [[ -n "$R9V_PLE_EXPECTED_BYTES" ]]; then
ple_bytes="$(stat -c %s "$R9V_PLE_PATH" 2>/dev/null || echo 0)"
if [[ "$ple_bytes" != "$R9V_PLE_EXPECTED_BYTES" ]]; then
echo "ERROR: PLE payload wrong size: $R9V_PLE_PATH (expected $R9V_PLE_EXPECTED_BYTES, found $ple_bytes)" >&2
echo "frank-primary: re-run the PLE derivation (./r9v setup ...)" >&2
return 1
fi
fi
local rel
for rel in "$R9V_TARGET_REL" "$R9V_TARGET_SHARD2_REL" "$R9V_TARGET_SHARD3_REL" "$R9V_MMPROJ_REL"; do
[[ -z "$rel" ]] && continue
if [[ ! -f "$R9V_MODEL_DIR/$rel" ]]; then
echo "ERROR: payload file missing: $R9V_MODEL_DIR/$rel" >&2
echo "frank-primary: re-run the profile setup to fetch/verify the payload" >&2
return 1
fi
done
# A stale container with the same name makes `podman run --name` fail at # A stale container with the same name makes `podman run --name` fail at
# start; remove any leftover from a failed previous attempt. # start; remove any leftover from a failed previous attempt.
@@ -1236,8 +1264,9 @@ PY
run_privileged systemctl daemon-reload run_privileged systemctl daemon-reload
run_privileged systemctl enable frank-primary.service run_privileged systemctl enable frank-primary.service
run_privileged systemctl restart frank-primary.service run_privileged systemctl restart frank-primary.service
# Cold start: triton/inductor compile on first boot plus reading+pinning # Cold start: the launcher's preflight doctor plus payload page-in
# ~173 GiB of weights. Parity with every predecessor on this port. # (~93 GiB cold; expert cache fills after /health). Parity with every
# predecessor on this port.
wait_for_health frank-primary http://127.0.0.1:7770/health 1800 || return 1 wait_for_health frank-primary http://127.0.0.1:7770/health 1800 || return 1
# False-green hardening (the 2026-09-26 lesson): a bare curl against the # False-green hardening (the 2026-09-26 lesson): a bare curl against the
# port cannot tell WHO answers — a stray probe holding :7770 satisfies # port cannot tell WHO answers — a stray probe holding :7770 satisfies
+19 -95
View File
@@ -1,96 +1,20 @@
# frank-primary.env — tunables for the vLLM 0.30 frank-primary endpoint # frank-primary.env — R9V radiance-vLLM production overrides (dankserver :7770).
# (Qwen3.8-Flash-Next-FP8, 2x R9700 gfx1201, dankserver :7770).
# #
# Paths and numbers only — NEVER credentials. Do not name a tunable TEMP, HOST # The full tuned engine shape (104 R9V_* keys: tiered-expert placement,
# or GPU_MAX_HEAP_SIZE: the HIP runtime reads those bare names (2026-08-13). # CPU offload budgets 146.25/78.75 GiB per rank, CED on, MTP4, TP=2,
# # qwen3_coder tool parser, 196608 model len) lives on-host in the R9V setup
# Container paths: the model tree is mounted at /models (host # state — /home/dank/r9v-state/uncensored/setup.json — written by the
# /home/dank/models); the compile caches live under /cache (host # profile's setup run (PASS=28/FAIL=0, all 20 artifacts sha256-verified).
# /home/dank/.cache/frank-primary-vllm, keyed on model + stack + graph knobs — # serve-frank-primary.sh layers that state first and THIS FILE second, so the
# never share it across configurations). # keys below win. Never copy tuned values from the state into this file —
# the state is the single source of truth for engine shape.
# --- identity --------------------------------------------------------------- R9V_HOME=/home/dank/GitPackages/radiance-vllm-qwen4exp
SERVED_NAME=frank-primary R9V_STATE_FILE=/home/dank/r9v-state/uncensored/setup.json
BIND_HOST=0.0.0.0 # Fleet identity: LAN bind, the single frank-primary port, the harness's
PORT=7770 # model id, our own container name (the launcher default r9v-qwen38-flash-next
# is the scratch identity).
# --- model ------------------------------------------------------------------ R9V_HOST_BIND=0.0.0.0
# Host-side dir: /home/dank/models/Qwen3.8-Flash-Next-FP8 (131 safetensors R9V_HOST_PORT=7770
# shards, 172.78 GiB, verified against model.safetensors.index.json). R9V_SERVED_MODEL_NAME=frank-primary
MODEL_DIR=/models/Qwen3.8-Flash-Next-FP8 R9V_CONTAINER_NAME=frank-primary-r9v
R9V_EXPECTED_GPU_BDFS=0000:03:00.0,0000:07:00.0
# --- placement --------------------------------------------------------------
# Fixed 2-card tensor parallel (the R9700 pair; the Raphael iGPU is index 2
# and excluded by the visibility pins).
ROCR_VISIBLE_DEVICES=0,1
HIP_VISIBLE_DEVICES=0,1
TP=2
# 0.97 of 31.86 GiB/card (34,208,743,424 B) — leaves ~1 GiB for the ROCm
# baseline. Raise only with a measured reason.
GPU_UTIL=0.97
# The model's native window.
CTX_SIZE=262144
# Sequences the scheduler may hold. 8 is the proven batch shape on this host
# (radiance era); revisit with a bench after bring-up.
MAX_SEQS=8
# --- weight offloading (the UVA shape) ---------------------------------------
# --offload-backend uva: matched weights live in PINNED host RAM and are read
# in place over PCIe by the kernels (zero-copy), instead of being resident
# on the cards. is_uva_available() = is_pin_memory_available() on ROCm; the
# pin needs the unit's LimitMEMLOCK=infinity (a root unit grants it via
# PID1 — the dankserver user session defaults to 8 MiB and cannot raise it).
# If pinning is unavailable the offloader falls back to a copy path — the
# load log MUST be checked for the UVA path, not just /health.
# --cpu-offload-gb is PER GPU (GiB). 2x R9700 total pinned budget ≈ 160 GiB of
# the 246 GiB host RAM; the rest covers the OS, llama.cpp-era page cache
# drain and the pinned staging buffers. Do not raise without checking
# MemAvailable at load time.
# --cpu-offload-params: name SEGMENTS (dot-delimited components) of tensors
# to offload. Measured from the payload headers (2026-09-26): `ple` matches
# 138 tensors = 131 n-gram table shards + 7 per-layer conv/proj weights,
# 47.75 GiB; `experts` matches 150,528 tensors, 114.86 GiB. Combined
# 162.60 GiB — with TP=2 each rank needs ~81.3 GiB of pinned host RAM,
# hence OFFLOAD_GB=82 per GPU (164 GiB total; 80/card left 2.6 GiB of
# experts resident, inflating the on-card weight share). Resident after
# the full offload: 10.16 GiB of dense/attention/MTP/ViT/lm_head weights.
# Matching is greedy-up-to-budget in traversal order; unmatched params are
# never offloaded.
OFFLOAD_BACKEND=uva
OFFLOAD_GB=82
OFFLOAD_PARAMS=ple,experts
# --- engine -----------------------------------------------------------------
# No aiter wheel in this image (gfx9-only upstream); the AMD qwen4_exp path
# runs its in-tree triton kernels. Deterministic rather than relying on the
# availability probe.
VLLM_ROCM_USE_AITER=0
# Compile/inductor/triton caches (container /cache, TRITON_CACHE_DIR etc.)
CACHE_ROOT=/cache
# --- parsers / template (the harness wire contract) ---------------------------
# vLLM 0.30 emits the thinking channel as `reasoning` (the deprecated
# `reasoning_content` is renamed server-side, chat_completion/protocol.py:554).
# The qwen3 reasoning parser extracts the template's think-block tags into
# that channel; the qwen3_xml tool parser structures tool-call XML output into
# tool_calls (auto-tool-choice enables the structured path for API clients).
# The family precedent is the host's own Ornith-era serve scripts
# (~/vllm/failedAttempts/OrnithFP8.sh); the FP8 tokenizer's template
# consumes enable_thinking + preserve_thinking (verified in tokenizer_config).
TOOL_PARSER=qwen3_xml
REASONING_PARSER=qwen3
CHAT_TEMPLATE_KWARGS='{"enable_thinking": true, "preserve_thinking": true}'
# --- speculative decode ------------------------------------------------------
# The hybrid stack (GatedDeltaNet + QSA short-conv) uses the v1 engine's
# mamba-capable attention backends, negotiated by the model, not by env.
# MTP drafting comes from the model's own mtp.* weights via
# --speculative-config, only when SPEC_CONFIG is non-empty. Bring-up runs
# with it empty on purpose: measure the base shape clean, then A/B; never
# change two knobs at once.
SPEC_CONFIG=
# --- container ---------------------------------------------------------------
IMAGE=localhost/frank-primary:vllm030
CONTAINER_NAME=frank-primary-vllm
+25 -19
View File
@@ -1,34 +1,40 @@
# frank-primary.service — vLLM 0.30 endpoint (Qwen3.8-Flash-Next-FP8, # frank-primary.service — R9V radiance-vLLM endpoint (Qwen3.8-Flash-Next-
# UVA offload shape) on the 2x R9700 pair, dankserver :7770. # Uncensored IQ4_XS, MTP4, TP=2, tiered experts) on the 2x R9700 pair,
# dankserver :7770.
# #
# Replaces the llama.cpp frank-primary unit of 2026-09-26 (same port, same # Replaces the llama.cpp frank-primary unit (same port, same served alias;
# served alias; harness models.yml unchanged). The serve script execs a # harness models.yml unchanged in id, retuned for the 196608 window). The
# podman container (image localhost/frank-primary:vllm030) as dank. # serve script drives the R9V fork's own launcher through the docker->podman
# shim and then waits on the container, so a crashed engine is a nonzero
# ExecStart and Restart=on-failure is real.
# #
# WHY A ROOT UNIT (not a rootless quadlet): the UVA offload pins ~160 GiB of # WHY A ROOT UNIT (not a rootless quadlet): the tiered-expert shape pins tens
# host RAM (PLE n-gram table + routed experts). Pinned pages are charged # of GiB of host RAM (the UVA expert host copy plus the 16 GiB PLE pinned
# against RLIMIT_MEMLOCK; dank's login session on this host carries 8 MiB and # reserve). Pinned pages are charged against RLIMIT_MEMLOCK; dank's login
# a user quadlet is capped by the user manager's own hard limit — raising # session on this host carries 8 MiB and a user quadlet is capped by the user
# that means restarting user@1000.service and bouncing every user container # manager's own hard limit — raising that means restarting user@1000.service
# on the host. A root unit grants LimitMEMLOCK=infinity to the dank process # and bouncing every user container on the host. A root unit grants
# via PID1, no user-manager restart, and the image stays in dank's rootless # LimitMEMLOCK=infinity to the dank process via PID1, and the image stays in
# podman storage (podman run as dank reads ~/.local/share/containers). # dank's rootless podman storage (the shim drives podman as dank).
[Unit] [Unit]
Description=frank-primary — vLLM 0.30 Qwen3.8-Flash-Next-FP8 (UVA offload) on 2x R9700 Description=frank-primary — R9V radiance-vLLM Qwen3.8-Flash-Next-Uncensored IQ4_XS on 2x R9700
After=network-online.target After=network-online.target
Wants=network-online.target Wants=network-online.target
[Service] [Service]
User=dank User=dank
Group=dank Group=dank
# The UVA offloader pins the offloaded weights in host RAM; an RLIMIT_MEMLOCK # The tiered-expert offloader pins host RAM; an RLIMIT_MEMLOCK smaller than
# smaller than the pinned set fails the load mid-start. PID1 grants infinity # the pinned set fails the load mid-start. PID1 grants infinity here —
# here — parity with the fleet's other LLM units. # parity with the fleet's other LLM units.
LimitMEMLOCK=infinity LimitMEMLOCK=infinity
Environment="FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env" Environment="FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env"
ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh
# Cold start: Triton/inductor compile (several minutes on first boot) plus # systemd stop: wait alone cannot stop the container child; stop it
# reading+pinning ~173 GiB of weights. Parity with the radiance-era timeout. # explicitly. vLLM 0.26 drains gracefully on SIGTERM.
ExecStop=/usr/bin/podman stop -t 120 frank-primary-r9v
# Boot cost: the launcher's preflight doctor (~1 min) after a warm page
# cache; cold (post-reboot) payload page-in of ~93 GiB dominates.
TimeoutStartSec=1800 TimeoutStartSec=1800
Restart=on-failure Restart=on-failure
RestartSec=10 RestartSec=10
+106 -97
View File
@@ -1,21 +1,29 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# serve-frank-primary.sh — frank-primary endpoint (vLLM 0.30, # serve-frank-primary.sh — frank-primary endpoint (R9V radiance-vLLM fork,
# Qwen3.8-Flash-Next-FP8 on the 2x R9700 pair, dankserver :7770). # Qwen3.8-Flash-Next-Uncensored IQ4_XS + MTP4 drafter, 2x R9700, dankserver
# :7770).
# #
# Validates the image, the model payload and the env tunables, then execs the # The engine is the upstream R9V launcher (scripts/launch.sh in the fork
# container. The unit (scripts/frank-primary.service, root systemd, User=dank, # checkout), NOT a hand-frozen podman argv: launch.sh owns the resolved
# LimitMEMLOCK=infinity) is what makes the UVA pin budget legal — a plain # docker/podman args (binds, overlays, device passthrough, publish lines)
# shell here carries the 8 MiB session limit and the pinned offload fails. # and the fork's qualification was measured through it. This script's job:
# #
# OFFLOAD SHAPE (the only legal fit for a ~125B-param FP8 model on 2x 32 GiB # 1. layer the on-host R9V setup state (the 104-key tuned shape the
# cards): --offload-backend uva pins the PLE n-gram table (51.2B params) and # profile setup qualified) under this repo's frank-primary.env
# the routed experts in host RAM and reads them in place over PCIe. The # overrides (bind/port/alias/container);
# first start MUST be watched for the UVA engagement log (not just /health): # 2. gate on the pinned runtime image (by its OCI manifest digest, the
# a silent fallback to the copy path turns the endpoint into a slideshow. # podman RepoDigest), the exact-size PLE payload, and the GGUF shards;
# 3. exec the launcher, then wait on the container: launch.sh --detach
# exits as soon as the container is created, so without the wait the
# unit would be "green" while the engine crashes — the wait is what
# keeps systemd's Restart=on-failure self-healing real.
# #
# NO --model-draft, no draft GGUF: vLLM's MTP drafting comes from the model's # The root unit (scripts/frank-primary.service, User=dank,
# own mtp.* weights via --speculative-config, and only when SPEC_CONFIG is # LimitMEMLOCK=infinity) grants the pin budget for the tiered-expert UVA
# non-empty. Bring-up runs with it empty on purpose. # host copy (~43 GiB pinned) and the 16 GiB PLE pinned reserve. A plain
# shell session also works (proven 2026-09-27: podman rootless inherits
# usable limits here), but the unit is the fleet-parity boot-persistent
# shape.
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
@@ -30,100 +38,101 @@ set -a
source "$ENV_FILE" source "$ENV_FILE"
set +a set +a
: "${SERVED_NAME:?SERVED_NAME must be set}" : "${R9V_HOME:?R9V_HOME must name the fork checkout}"
: "${BIND_HOST:?BIND_HOST must be set}" : "${R9V_STATE_FILE:?R9V_STATE_FILE must name the setup state}"
: "${PORT:?PORT must be set}" : "${R9V_HOST_BIND:?R9V_HOST_BIND must be set}"
: "${MODEL_DIR:?MODEL_DIR must be set}" : "${R9V_HOST_PORT:?R9V_HOST_PORT must be set}"
: "${TP:?TP must be set}" : "${R9V_SERVED_MODEL_NAME:?R9V_SERVED_MODEL_NAME must be set}"
: "${GPU_UTIL:?GPU_UTIL must be set}" : "${R9V_CONTAINER_NAME:?R9V_CONTAINER_NAME must be set}"
: "${CTX_SIZE:?CTX_SIZE must be set}"
: "${MAX_SEQS:?MAX_SEQS must be set}"
: "${OFFLOAD_BACKEND:?OFFLOAD_BACKEND must be set}"
: "${OFFLOAD_GB:?OFFLOAD_GB must be set}"
: "${OFFLOAD_PARAMS:?OFFLOAD_PARAMS must be set}"
: "${VLLM_ROCM_USE_AITER:?VLLM_ROCM_USE_AITER must be set}"
: "${IMAGE:?IMAGE must be set}"
: "${CONTAINER_NAME:?CONTAINER_NAME must be set}"
: "${ROCR_VISIBLE_DEVICES:?ROCR_VISIBLE_DEVICES must be set}"
: "${TOOL_PARSER:?TOOL_PARSER must be set}"
: "${REASONING_PARSER:?REASONING_PARSER must be set}"
: "${CHAT_TEMPLATE_KWARGS:?CHAT_TEMPLATE_KWARGS must be set}"
: "${CACHE_ROOT:?CACHE_ROOT must be set}"
if [ "$OFFLOAD_BACKEND" != "uva" ]; then LAUNCHER="$R9V_HOME/scripts/launch.sh"
echo "serve-frank-primary: OFFLOAD_BACKEND=$OFFLOAD_BACKEND — this deployment is only qualified for uva" >&2 if [ ! -x "$LAUNCHER" ]; then
echo "serve-frank-primary: R9V launcher missing: $LAUNCHER" >&2
exit 1 exit 1
fi fi
if ! podman image inspect "$IMAGE" > /dev/null 2>&1; then # The docker->podman shim is REQUIRED: the launcher and its doctor speak
echo "serve-frank-primary: image missing: $IMAGE — build it before serving" >&2 # docker (OCI-manifest-digest image IDs, LogConfig fields) that podman's
# native CLI answers differently. The shim carries those laws.
SHIM_DIR="$R9V_HOME/.podman-shim"
if [ ! -x "$SHIM_DIR/docker" ]; then
echo "serve-frank-primary: docker->podman shim missing: $SHIM_DIR/docker" >&2
exit 1
fi
export PATH="$SHIM_DIR:$PATH"
# --- layer 1: the qualified setup state (production overrides never come
# from here; keys already defined in the env file are skipped) -------------
STATE_FILE="$R9V_STATE_FILE"
if [ ! -f "$STATE_FILE" ]; then
echo "serve-frank-primary: R9V setup state missing: $STATE_FILE" >&2
echo " run the profile setup first (r9v setup qwen38-mtp4-uncensored ...)" >&2
exit 1
fi
STATE_EXPORTS="$(python3 - "$STATE_FILE" <<'PY'
import json, shlex, sys, os
state = json.load(open(sys.argv[1]))
config = state.get("config") or {}
if state.get("ready") is not True:
sys.exit("R9V setup state is not ready; rerun setup before serving")
env_keys = {k for k in os.environ if k.startswith("R9V_")}
for key in sorted(config):
if key.startswith("R9V_") and key not in env_keys:
print(f"export {key}={shlex.quote(config[key])}")
PY
)"
eval "$STATE_EXPORTS"
: "${R9V_IMAGE:?state must carry R9V_IMAGE}"
: "${R9V_MODEL_DIR:?state must carry R9V_MODEL_DIR}"
: "${R9V_PLE_PATH:?state must carry R9V_PLE_PATH}"
# --- gates: never serve a half-finished payload or a missing image --------
# Runtime image by pinned OCI manifest digest (podman keys images by config
# digest but preserves the manifest digest in RepoDigests — the shim's ID law).
if ! podman images --format '{{.RepoDigests}}' | grep -qF "${R9V_IMAGE#sha256:}"; then
echo "serve-frank-primary: pinned R9V image absent: $R9V_IMAGE" >&2
echo " load the bundle: ./r9v setup qwen38-mtp4-uncensored --state-dir ..." >&2
exit 1 exit 1
fi fi
# The model payload is verified by the fetch/verify step (131 shards, index # PLE payload at its pinned byte count (stat -c %s, never wc -c), plus the
# cross-checked). The serve-time check is the load-bearing minimum: the # three target GGUF shards and the mmproj the launcher mounts.
# wrapper config and the shard index must exist, or the container start is a PLE_BYTES="${R9V_PLE_EXPECTED_BYTES:-}"
# guaranteed 20-minute timeout instead of a fast failure. if [ -n "$PLE_BYTES" ]; then
HOST_MODEL_DIR="/home/dank/models/${MODEL_DIR#/models/}" ACTUAL="$(stat -c %s "$R9V_PLE_PATH" 2>/dev/null || echo 0)"
if [ ! -f "$HOST_MODEL_DIR/config.json" ] || [ ! -f "$HOST_MODEL_DIR/model.safetensors.index.json" ]; then if [ "$ACTUAL" != "$PLE_BYTES" ]; then
echo "serve-frank-primary: model payload incomplete at $HOST_MODEL_DIR (config.json or index missing)" >&2 echo "serve-frank-primary: PLE payload wrong size: $R9V_PLE_PATH" >&2
echo "serve-frank-primary: run scripts/download-frank-primary-fp8.sh on this host" >&2 echo " expected $PLE_BYTES, found $ACTUAL" >&2
exit 1 exit 1
fi
else
if [ ! -f "$R9V_PLE_PATH" ]; then
echo "serve-frank-primary: PLE payload missing: $R9V_PLE_PATH" >&2
exit 1
fi
fi fi
for rel in "$R9V_TARGET_REL" "$R9V_TARGET_SHARD2_REL" "$R9V_TARGET_SHARD3_REL" "$R9V_MMPROJ_REL"; do
if [ -z "$rel" ]; then continue; fi
if [ ! -f "$R9V_MODEL_DIR/$rel" ]; then
echo "serve-frank-primary: payload file missing: $R9V_MODEL_DIR/$rel" >&2
exit 1
fi
done
if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then if [ ! -e /dev/kfd ] || [ ! -d /dev/dri ]; then
echo "serve-frank-primary: /dev/kfd or /dev/dri missing — GPU stack not up" >&2 echo "serve-frank-primary: GPU device nodes absent (/dev/kfd, /dev/dri)" >&2
exit 1 exit 1
fi fi
mkdir -p /home/dank/.cache/frank-primary-vllm # A crashed/killed unit can leave the container name registered (systemd
# SIGKILLs conmon before cleanup runs); the launcher refuses an existing
SERVE_ARGS=("$MODEL_DIR" # name forever after. Clear any orphan of OUR name first — idempotent.
--served-model-name "$SERVED_NAME" docker rm -f "$R9V_CONTAINER_NAME" >/dev/null 2>&1 || true
--host "$BIND_HOST"
--port "$PORT"
--tensor-parallel-size "$TP"
--gpu-memory-utilization "$GPU_UTIL"
--max-model-len "$CTX_SIZE"
--max-num-seqs "$MAX_SEQS"
--offload-backend "$OFFLOAD_BACKEND"
--cpu-offload-gb "$OFFLOAD_GB"
--cpu-offload-params "$OFFLOAD_PARAMS"
--enable-auto-tool-choice --tool-call-parser "$TOOL_PARSER"
--reasoning-parser "$REASONING_PARSER"
--default-chat-template-kwargs "$CHAT_TEMPLATE_KWARGS"
)
# SPEC_CONFIG (optional, JSON) is how MTP drafting is enabled after the
# bring-up measurement: --speculative-config '{"method":"mtp",...}'.
if [ -n "${SPEC_CONFIG:-}" ]; then
SERVE_ARGS+=(--speculative-config "$SPEC_CONFIG")
fi
# A crashed/killed unit leaves the container name registered (systemd can
# SIGKILL conmon before the --rm cleanup runs); podman run would then die
# on name-in-use forever. Clear any orphan of OUR name first — idempotent.
podman rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
exec podman run --rm \
--name "$CONTAINER_NAME" \
--userns=keep-id \
--network=host \
--ipc=host \
--security-opt seccomp=unconfined \
--cap-add SYS_PTRACE \
--device /dev/kfd \
--device /dev/dri \
--group-add keep-groups \
-e ROCR_VISIBLE_DEVICES="$ROCR_VISIBLE_DEVICES" \
-e HIP_VISIBLE_DEVICES="$HIP_VISIBLE_DEVICES" \
-e VLLM_ROCM_USE_AITER="$VLLM_ROCM_USE_AITER" \
-e NCCL_P2P_DISABLE="$NCCL_P2P_DISABLE" \
-e TRITON_CACHE_DIR="$CACHE_ROOT/triton" \
-e TORCHINDUCTOR_CACHE_DIR="$CACHE_ROOT/inductor" \
-e HOME="$CACHE_ROOT" \
-e TRITON_ALLOW_NON_CONST_GLOBALS=1 \
--volume /home/dank/models:/models \
--volume /home/dank/.cache/frank-primary-vllm:"$CACHE_ROOT" \
"$IMAGE" \
"${SERVE_ARGS[@]}"
# launch.sh detaches after preflight; from here on, this script's lifetime
# IS the container's: wait returns the container's exit code, so a crash is
# a nonzero ExecStart and Restart=on-failure fires. (systemd stop goes
# through ExecStop in the unit — docker wait alone cannot stop the child.)
"$LAUNCHER"
exec docker wait "$R9V_CONTAINER_NAME"
+77 -112
View File
@@ -1,17 +1,17 @@
#!/usr/bin/env bats #!/usr/bin/env bats
# Seam: frank-primary — the Dankserver LLM endpoint on :7770. # Seam: frank-primary — the Dankserver LLM endpoint on :7770.
# #
# This file asserts the vLLM 0.30 ROOT-UNIT shape (the 2026-09-27 swap that # This file asserts the R9V radiance-vLLM ROOT-UNIT shape (the 2026-09-27
# replaced the llama.cpp b11030 endpoint): a locally built ROCm gfx1201 # swap that replaced the llama.cpp b11030 endpoint): the R9V fork's own
# image (localhost/frank-primary:vllm030) serving Qwen3.8-Flash-Next-FP8 # launcher (scripts/launch.sh in the pinned fork checkout) driven through a
# (Qwen4ExpForConditionalGeneration, 131 shards, 172.78 GiB) on the two # docker->podman shim, with the qualified on-host setup state layered under
# R9700s with the UVA offload shape. The seam pins what the flags alone do # the repo's production overrides. The seam pins what the flags alone do not
# not say: the offload budget arithmetic (the bandwidth-huge tensors — the # say: the state-layering contract (104 tuned R9V_* keys live on-host; the
# 47.75 GiB PLE family and the 114.86 GiB routed experts — are pinned in # repo env carries only production overrides), the self-healing law (the
# host RAM and read in place, never streamed), the memlock law that makes # launcher detaches, so the serve script must wait on the container for
# the pin legal, the parser contract that keeps the harness wire formats # Restart=on-failure to mean anything), the memlock law that makes the
# (tool_calls + the reasoning channel) working, HIP env hygiene, and the # tiered-expert pin legal, and the deployer's state/payload/image gates with
# deployer's image/payload gates with the false-green MainPID check. # the false-green MainPID check.
load helpers load helpers
@@ -28,9 +28,9 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
@test "frank-primary is boot-persistent, self-healing and unprivileged" { @test "frank-primary is boot-persistent, self-healing and unprivileged" {
# Root unit parity with frank-large/frank-small: enable is the [Install] # Root unit parity with frank-large/frank-small: enable is the [Install]
# section, the restart policy survives a crash, and the ~164 GiB UVA pin # section, the restart policy survives a crash, and the tiered-expert
# needs an unbounded memlock (a root unit grants it via PID1; the dank # pin needs an unbounded memlock (a root unit grants it via PID1; the
# session's own hard limit is 8 MiB and cannot be raised without # dank session's own hard limit is 8 MiB and cannot be raised without
# restarting user@1000.service). # restarting user@1000.service).
grep -q '^WantedBy=multi-user.target$' "$UNIT" grep -q '^WantedBy=multi-user.target$' "$UNIT"
grep -q '^Restart=on-failure$' "$UNIT" grep -q '^Restart=on-failure$' "$UNIT"
@@ -44,126 +44,91 @@ REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env' "$UNIT" grep -q 'FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env' "$UNIT"
} }
@test "the serve shape is vLLM: no llama.cpp-ism survives the cutover" { @test "the unit stops the container child explicitly" {
# Clean cutover: the GGUF era's flags would silently no-op or crash a vLLM # The serve script blocks on docker wait, which cannot stop the
# server, so their absence is part of the contract. # container child; systemd stop goes through ExecStop or the engine
# outlives the unit and holds both GPUs.
grep -q '^ExecStop=/usr/bin/podman stop' "$UNIT"
grep -q 'frank-primary-r9v' "$UNIT"
}
@test "the serve shape is the R9V launcher, not a frozen podman run" {
# The launcher owns the resolved container args (binds, overlays, device
# passthrough); a frozen copy of its argv would drift on every fork
# update. The llama.cpp era's flags would also silently no-op here.
grep -q 'launch.sh' "$SERVE"
! grep -q -- '--model-draft' "$SERVE" ! grep -q -- '--model-draft' "$SERVE"
! grep -q -- '--spec-type' "$SERVE" ! grep -q -- '--spec-type' "$SERVE"
! grep -q -- '--mmproj' "$SERVE" ! grep -q -- '--mmproj' "$SERVE"
! grep -q -- '--override-tensor' "$SERVE" ! grep -q -- '--override-tensor' "$SERVE"
! grep -q '\.gguf' "$SERVE" ! grep -q 'podman run' "$SERVE"
! grep -q '\.gguf' "$ENV_FILE"
} }
@test "the bandwidth-huge tensors are pinned in host RAM, not streamed" { @test "the tuned engine shape comes from the on-host setup state, not the repo" {
# The UVA shape (the x4-pair law: streaming experts over PCIe loses, # The profile setup qualified a 104-key shape (tiered-expert placement,
# computing them in host RAM does not care): --offload-backend uva pins # per-rank offload budgets, CED, MTP4, parser wiring). Copying it into
# the matched weights with zero-copy reads. Measured from the payload # the repo would drift; the serve script layers it and skips keys the
# headers (2026-09-26): `ple` matches 138 tensors (131 n-gram table # env file defines, so production overrides always win.
# shards + 7 per-layer conv/proj weights) = 47.75 GiB; `experts` matches grep -q 'R9V_STATE_FILE=/home/dank/r9v-state/uncensored/setup.json' "$ENV_FILE"
# 150,528 tensors = 114.86 GiB; combined 162.60 GiB. cpu-offload-gb is grep -q 'state must carry R9V_MODEL_DIR' "$SERVE"
# PER GPU: under TP=2 each rank needs ~81.3 GiB, hence 82 — 80/card grep -q 'is not ready' "$SERVE"
# would strand 2.6 GiB of experts on the cards. grep -q 'key not in env_keys' "$SERVE"
grep -q '^OFFLOAD_BACKEND=uva$' "$ENV_FILE"
grep -q '^OFFLOAD_GB=82$' "$ENV_FILE"
grep -q '^OFFLOAD_PARAMS=ple,experts$' "$ENV_FILE"
grep -q -- '--offload-backend "$OFFLOAD_BACKEND"' "$SERVE"
grep -q -- '--cpu-offload-gb "$OFFLOAD_GB"' "$SERVE"
grep -q -- '--cpu-offload-params "$OFFLOAD_PARAMS"' "$SERVE"
# auto/prefetch are the copy paths: this deployment is only qualified
# for uva, so the serve script refuses anything else.
grep -q 'only qualified for uva' "$SERVE"
} }
@test "the engine shape: TP=2 over both R9700s, full native window, batch 8" { @test "the serve script gates the pinned image, PLE bytes and GGUF shards" {
grep -q '^TP=2$' "$ENV_FILE" # The image is answered by its OCI manifest digest (podman keeps the
grep -q '^GPU_UTIL=0.97$' "$ENV_FILE" # manifest digest in RepoDigests, not as the image ID — the shim's ID
grep -q '^CTX_SIZE=262144$' "$ENV_FILE" # law). The PLE payload is checked at its pinned byte count
grep -q '^MAX_SEQS=8$' "$ENV_FILE" # (stat -c %s, never wc -c). A missing shard means a guaranteed
grep -q -- '--tensor-parallel-size "$TP"' "$SERVE" # long-timeout boot instead of a fast failure.
grep -q -- '--gpu-memory-utilization "$GPU_UTIL"' "$SERVE" grep -q 'R9V_PLE_EXPECTED_BYTES' "$SERVE"
grep -q -- '--max-model-len "$CTX_SIZE"' "$SERVE" grep -q 'stat -c %s' "$SERVE"
grep -q -- '--max-num-seqs "$MAX_SEQS"' "$SERVE" grep -q 'R9V_TARGET_SHARD2_REL' "$SERVE"
grep -q 'R9V_MMPROJ_REL' "$SERVE"
grep -q 'RepoDigests' "$SERVE"
grep -q '/dev/kfd' "$SERVE"
} }
@test "the payload is the FP8 tree, vision served from the model's own config" { @test "the serve script clears the stale container and self-heals via docker wait" {
# Qwen4ExpForConditionalGeneration: vLLM auto-loads the ViT and the # launch.sh detaches and refuses an existing container name; a crashed
# mtp.* weights from the payload config — no explicit projector flag # unit can leave the name registered forever after. Clear our own name
# exists in this runtime (the llama.cpp --mmproj trap does not apply). # first (idempotent), then block on the container so a crash is a
grep -q '^MODEL_DIR=/models/Qwen3.8-Flash-Next-FP8$' "$ENV_FILE" # nonzero ExecStart.
grep -q '^IMAGE=localhost/frank-primary:vllm030$' "$ENV_FILE" grep -q 'docker rm -f "$R9V_CONTAINER_NAME"' "$SERVE"
grep -q 'exec docker wait "$R9V_CONTAINER_NAME"' "$SERVE"
} }
@test "frank-primary holds the single fleet port under its own alias" { @test "frank-primary holds the single fleet port under its own alias" {
grep -q '^PORT=7770$' "$ENV_FILE" # The scratch identity (r9v-qwen38-flash-next on 127.0.0.1:7772) must
grep -q '^BIND_HOST=0.0.0.0$' "$ENV_FILE" # never leak into production: LAN bind, :7770, the harness's model id.
grep -q '^SERVED_NAME=frank-primary$' "$ENV_FILE" grep -q '^R9V_HOST_BIND=0.0.0.0$' "$ENV_FILE"
grep -q -- '--served-model-name "$SERVED_NAME"' "$SERVE" grep -q '^R9V_HOST_PORT=7770$' "$ENV_FILE"
grep -q -- '--host "$BIND_HOST"' "$SERVE" grep -q '^R9V_SERVED_MODEL_NAME=frank-primary$' "$ENV_FILE"
grep -q -- '--port "$PORT"' "$SERVE" grep -q '^R9V_CONTAINER_NAME=frank-primary-r9v$' "$ENV_FILE"
} grep -q '^R9V_EXPECTED_GPU_BDFS=0000:03:00.0,0000:07:00.0$' "$ENV_FILE"
@test "tool_calls and the reasoning channel are wired for the harness" {
# The wire contract: vLLM 0.30 emits the thinking channel as `reasoning`
# (the deprecated `reasoning_content` is renamed server-side), the qwen3
# parser extracts the template's think block into it, and qwen3_xml +
# auto-tool-choice structure tool calls. Precedent: the host's own
# Ornith-era scripts (~/vllm/failedAttempts/OrnithFP8.sh); the FP8
# template consumes enable_thinking/preserve_thinking (verified in
# tokenizer_config.json).
grep -q '^TOOL_PARSER=qwen3_xml$' "$ENV_FILE"
grep -q '^REASONING_PARSER=qwen3$' "$ENV_FILE"
grep -q -- '--enable-auto-tool-choice --tool-call-parser "$TOOL_PARSER"' "$SERVE"
grep -q -- '--reasoning-parser "$REASONING_PARSER"' "$SERVE"
# The template kwargs are JSON: the env value must be single-quoted or
# bash splits it and CHAT_TEMPLATE_KWARGS arrives empty (the -ot lesson).
grep -q "^CHAT_TEMPLATE_KWARGS='{\"enable_thinking\": true, \"preserve_thinking\": true}'$" "$ENV_FILE"
grep -q -- '--default-chat-template-kwargs "$CHAT_TEMPLATE_KWARGS"' "$SERVE"
}
@test "speculative decode is env-wired and OFF at bring-up" {
# MTP drafting comes from the model's own mtp.* weights via
# --speculative-config; bring-up measures the base shape clean, so
# SPEC_CONFIG is empty and the flag is only added when non-empty.
grep -q '^SPEC_CONFIG=$' "$ENV_FILE"
grep -q -- '--speculative-config "$SPEC_CONFIG"' "$SERVE"
}
@test "both R9700s are visible and the Raphael iGPU is not" {
grep -q '^ROCR_VISIBLE_DEVICES=0,1$' "$ENV_FILE"
grep -q '^HIP_VISIBLE_DEVICES=0,1$' "$ENV_FILE"
grep -q -- '-e ROCR_VISIBLE_DEVICES="$ROCR_VISIBLE_DEVICES"' "$SERVE"
grep -q -- '-e HIP_VISIBLE_DEVICES="$HIP_VISIBLE_DEVICES"' "$SERVE"
} }
@test "no bare HIP-reserved env var is defined" { @test "no bare HIP-reserved env var is defined" {
# HIP reads bare TEMP for its JIT cache path (2026-08-13: TEMP=1.0 # HIP reads bare TEMP for its JIT cache path (2026-08-13: TEMP=1.0
# segfaulted every load), and HOST/GPU_MAX_HEAP_SIZE are the same # segfaulted every load), and HOST/GPU_MAX_HEAP_SIZE are the same
# class. The bind address is BIND_HOST, not HOST. # class. The bind address is R9V_HOST_BIND, not HOST.
! grep -qE '^(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE" ! grep -qE '^(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE"
grep -q '^BIND_HOST=' "$ENV_FILE"
! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$SERVE" ! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$SERVE"
} }
@test "the serve script refuses to start on a half-finished payload or missing image" { @test "the deployer gates the setup state, image digest, PLE bytes and shards" {
# A payload missing its wrapper config or shard index turns the # The deployer refuses when the on-host setup state is missing or not
# container start into a guaranteed 20-minute timeout; the checks fail # ready, when the pinned image is absent from rootless storage, when
# fast with a pointer at the fetch script instead. # the PLE payload is off its pinned byte count, or when any GGUF shard
grep -q 'config.json or index missing' "$SERVE" # or the mmproj is missing — then clears stale containers, waits out
grep -q 'download-frank-primary-fp8.sh' "$SERVE" # the port, relinks/enables/restarts the root unit and health-gates it.
grep -q 'podman image inspect' "$SERVE" grep -q 'LLM_PRIMARY_STATE=/home/dank/r9v-state/uncensored/setup.json' "$REMOTE_DEPLOY"
} grep -q 'LLM_PRIMARY_CONTAINER=frank-primary-r9v' "$REMOTE_DEPLOY"
grep -q 'is not ready' "$REMOTE_DEPLOY"
@test "the deployer gates the image and the full shard set before starting" { grep -q "podman images --format '{{.RepoDigests}}'" "$REMOTE_DEPLOY"
# The deployer refuses when the image or any index-named shard is grep -q 'stat -c %s' "$REMOTE_DEPLOY"
# absent (never serve a partial download), clears any stale container grep -q 'R9V_TARGET_SHARD2_REL' "$REMOTE_DEPLOY"
# that would break `podman run --name`, and waits out the port before grep -q 'podman rm -f "$LLM_PRIMARY_CONTAINER"' "$REMOTE_DEPLOY"
# the new engine binds it.
grep -q 'LLM_PRIMARY_IMAGE=localhost/frank-primary:vllm030' "$REMOTE_DEPLOY"
grep -q 'LLM_PRIMARY_ROOT=/home/dank/models/Qwen3.8-Flash-Next-FP8' "$REMOTE_DEPLOY"
grep -q 'weight_map' "$REMOTE_DEPLOY"
grep -q 'download-frank-primary-fp8.sh' "$REMOTE_DEPLOY"
grep -q "podman rm -f \"\$LLM_PRIMARY_CONTAINER\"" "$REMOTE_DEPLOY"
grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY" grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY"
grep -q 'relink_unit frank-primary' "$REMOTE_DEPLOY" grep -q 'relink_unit frank-primary' "$REMOTE_DEPLOY"
grep -q 'systemctl enable frank-primary.service' "$REMOTE_DEPLOY" grep -q 'systemctl enable frank-primary.service' "$REMOTE_DEPLOY"