New runbook frank-primary-freetoken-pr587 (build laws, shape, measured cutover numbers, TP=2 non-boot signature, rollback); LLM/index.md intro + mermaid + active/retired entries; AGENTS.md fleet row + section + bench and model-config mentions + runbook index; log.md entry. Seam pins the measured KV pool 299968 with the multiple-of-64 guard. NOTE: intended PGP signature skipped — key 0x76EA601A11B178D7 has no secret key in this machine's keyring (fresh ~/.gnupg); the PGP identity lives elsewhere.
262 lines
14 KiB
Bash
Executable File
262 lines
14 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# Seam: frank-primary — the Dankserver LLM endpoint on :7770.
|
|
#
|
|
# This file asserts the FreeToken (PR #587 ROCm) ROOT-UNIT shape (the
|
|
# 2026-10-05 cutover from the tcclaviger RDNA4 vLLM fork, git tag
|
|
# frank-primary-vllm-shapeG-20261003): the serve script drives `podman run`
|
|
# DIRECTLY (the image carries `ft serve`, built on the host from
|
|
# scripts/docker/frank-primary-freetoken.Dockerfile), one R9700, with every
|
|
# engine value carried in the repo env file. The seam pins what flags alone
|
|
# do not say: the KV-pool math (300k tokens at 24 KiB/token clears the
|
|
# harness worst case of 177,356), the deployed single-GPU shape
|
|
# (--tensor-parallel-size EXISTS in FreeToken — a 2-card TP shape is future
|
|
# tuning; HIP_VISIBLE_DEVICES pins THIS shape to one card), the host-RAM
|
|
# law (experts + PLE pin ~170 GiB — the memlock law that makes the pin
|
|
# legal), the self-healing law (the run returns before the engine is up, so
|
|
# the serve script must wait on the container for Restart=always to mean
|
|
# anything), the image-existence gate (a local build carries no
|
|
# RepoDigests — the old digest grep refused exactly this shape; pinning
|
|
# lives in the Dockerfile), the deployer's payload gates with the
|
|
# false-green MainPID check, and the client contract (parsers AUTO:
|
|
# reasoning_content emission is the engine's native behavior — models.yml
|
|
# maps reasoningContentField on the harness side, not here).
|
|
|
|
load helpers
|
|
|
|
UNIT="$REPO_ROOT/scripts/frank-primary.service"
|
|
SERVE="$REPO_ROOT/scripts/serve-frank-primary.sh"
|
|
ENV_FILE="$REPO_ROOT/scripts/frank-primary.env"
|
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|
DOWNLOAD="$REPO_ROOT/scripts/download-frank-primary.sh"
|
|
DOCKERFILE="$REPO_ROOT/scripts/docker/frank-primary-freetoken.Dockerfile"
|
|
|
|
@test "frank-primary unit, serve script, env and Dockerfile exist" {
|
|
[ -f "$UNIT" ]
|
|
[ -x "$SERVE" ]
|
|
[ -f "$ENV_FILE" ]
|
|
[ -f "$DOCKERFILE" ]
|
|
}
|
|
|
|
@test "frank-primary unit is boot-persistent, self-healing and unprivileged" {
|
|
# Root unit parity with frank-large/frank-small: enable is the [Install]
|
|
# section, the restart policy survives a crash, and the pinned host RAM
|
|
# (expert rows + PLE table) needs an unbounded memlock (a root unit
|
|
# grants it via PID1; the dank session's own hard limit is 8 MiB).
|
|
grep -q '^WantedBy=multi-user.target$' "$UNIT"
|
|
# Restart=always, not on-failure: the vLLM-era lesson carries over — a
|
|
# clean front-end exit after a worker fault reads as success to systemd,
|
|
# so on-failure never fires.
|
|
grep -q '^Restart=always$' "$UNIT"
|
|
! grep -q '^Restart=on-failure$' "$UNIT"
|
|
# ExecStop runs `podman stop -t 120`, so the stop timeout MUST exceed
|
|
# 120 s or systemd kills the stop and records 'Failed with result
|
|
# timeout' (measured 2026-10-01). Guard the relationship, not the
|
|
# literal.
|
|
stop_t=$(awk -F= '/^TimeoutStopSec=/{print $2}' "$UNIT")
|
|
[ -n "$stop_t" ] && [ "$stop_t" -gt 120 ]
|
|
grep -q '^ExecStop=.*podman stop -t 120' "$UNIT"
|
|
# StartLimitIntervalSec belongs to [Unit]; a copy in [Service] is an
|
|
# 'Unknown key' warning systemd logs on every load.
|
|
[ "$(grep -c '^StartLimitIntervalSec=' "$UNIT")" = "1" ]
|
|
awk '/^\[Service\]/{in_svc=1} /^StartLimitIntervalSec=/{if(in_svc) exit 1}' "$UNIT"
|
|
grep -q '^RestartSec=' "$UNIT"
|
|
grep -q '^User=dank$' "$UNIT"
|
|
grep -q '^LimitMEMLOCK=infinity$' "$UNIT"
|
|
}
|
|
|
|
@test "frank-primary runs the repo's own serve script with the repo's env" {
|
|
grep -q '^ExecStart=/home/dank/serverAppConfigs/scripts/serve-frank-primary.sh$' "$UNIT"
|
|
grep -q 'FRANK_PRIMARY_ENV_FILE=/home/dank/serverAppConfigs/scripts/frank-primary.env' "$UNIT"
|
|
}
|
|
|
|
@test "the unit stops the container child explicitly — container identity is the unit name" {
|
|
# The serve script blocks on podman wait, which cannot stop the
|
|
# container child; systemd stop goes through ExecStop or the engine
|
|
# outlives the unit and holds the GPU. Frank's 2026-10-05 call: the
|
|
# container name IS the service name (frank-primary), and the vLLM-era
|
|
# name frank-primary-qwen moved into the legacy cleanup list.
|
|
grep -q '^ExecStop=/usr/bin/podman stop' "$UNIT"
|
|
grep -q 'podman stop -t 120 frank-primary$' "$UNIT"
|
|
grep -q '^FRANK_PRIMARY_CONTAINER=frank-primary$' "$ENV_FILE"
|
|
grep -q 'frank-primary-qwen' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the serve shape is direct podman run, not a launcher, shim, or the vLLM fork" {
|
|
grep -q 'podman run' "$SERVE"
|
|
! grep -q 'launch.sh' "$SERVE"
|
|
! grep -q 'podman-shim' "$SERVE"
|
|
! grep -q 'R9V_STATE_FILE' "$SERVE"
|
|
# The tcclaviger vLLM fork is RETIRED — its image name and its
|
|
# fork-specific flags must not survive in the FreeToken run line
|
|
# (TP=2 is gone with the fork; FreeToken serves ONE GPU).
|
|
! grep -q 'tcclaviger' "$SERVE"
|
|
! grep -q 'tcclaviger' "$ENV_FILE"
|
|
! grep -q -- '--tensor-parallel-size' "$SERVE"
|
|
! grep -q -- '--kv-cache-dtype' "$SERVE"
|
|
! grep -q -- '--max-num-seqs' "$SERVE"
|
|
}
|
|
|
|
@test "the FreeToken engine shape is in the env file" {
|
|
# KV pool: TOKENS, not bytes. 24 KiB/token (12 full-attention layers x
|
|
# 2 KV heads x 256 head_dim x K+V x f16) — the 36 GDN layers use fixed
|
|
# per-seq state, not per-token KV. 299,968 tokens = 7.0 GiB and clears
|
|
# the harness worst case (140,492-token prompt + 36,864 max_tokens =
|
|
# 177,356); one NATIVE 262,144-token conversation + full budget also
|
|
# fits. The value must stay a MULTIPLE OF 64: the auto attention
|
|
# backend is qsa_sparse, which overrides page size to 64 (measured
|
|
# first boot — 300,000 was refused). The arithmetic guard keeps future
|
|
# resizes honest.
|
|
grep -q '^FRANK_PRIMARY_FT_NUM_TOKENS=299968$' "$ENV_FILE"
|
|
nt=$(awk -F= '/^FRANK_PRIMARY_FT_NUM_TOKENS=/{print $2}' "$ENV_FILE")
|
|
[ "$nt" -ge 177356 ]
|
|
[ $((nt % 64)) -eq 0 ]
|
|
# First-boot conservative shape: 2 decode slots, 10% expert LRU
|
|
# (~12 GiB of the ~120 GiB fp8 expert set), output ceiling = the
|
|
# harness maxTokens, single R9700 at index 0.
|
|
grep -q '^FRANK_PRIMARY_FT_MAX_RUNNING=2$' "$ENV_FILE"
|
|
grep -q '^FRANK_PRIMARY_FT_MOE_RATE=0.10$' "$ENV_FILE"
|
|
grep -q '^FRANK_PRIMARY_FT_MAX_OUTPUT_TOKENS=36864$' "$ENV_FILE"
|
|
grep -q '^FRANK_PRIMARY_FT_GPU=0$' "$ENV_FILE"
|
|
grep -q '^FRANK_PRIMARY_MODEL_SHARDS=131$' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the serve script wires the engine flags from the env" {
|
|
grep -q 'ft serve --model /models' "$SERVE"
|
|
grep -q -- '--served-model-name "$FRANK_PRIMARY_SERVED_NAME"' "$SERVE"
|
|
grep -q -- '--host 0.0.0.0 --port 7770' "$SERVE"
|
|
grep -q -- '--gpu "$FRANK_PRIMARY_FT_GPU"' "$SERVE"
|
|
grep -q -- '--num-tokens "$FRANK_PRIMARY_FT_NUM_TOKENS"' "$SERVE"
|
|
grep -q -- '--max-running-requests "$FRANK_PRIMARY_FT_MAX_RUNNING"' "$SERVE"
|
|
grep -q -- '--max-output-tokens "$FRANK_PRIMARY_FT_MAX_OUTPUT_TOKENS"' "$SERVE"
|
|
grep -q -- '--moe-cache-rate "$FRANK_PRIMARY_FT_MOE_RATE"' "$SERVE"
|
|
# Single-GPU law: visibility is pinned to one card so in-container
|
|
# index 0 is the right R9700 (mirrors the env's FT_GPU).
|
|
grep -q -- '-e HIP_VISIBLE_DEVICES="$FRANK_PRIMARY_FT_GPU"' "$SERVE"
|
|
# The boot-ladder escape hatch is opt-in: unset renders NO extra argv.
|
|
grep -q -- '${FRANK_PRIMARY_FT_EXTRA_ARGS:+$FRANK_PRIMARY_FT_EXTRA_ARGS}' "$SERVE"
|
|
! grep -q '^FRANK_PRIMARY_FT_EXTRA_ARGS=.' "$ENV_FILE"
|
|
}
|
|
|
|
@test "parsers are AUTO — the engine infers them natively, the harness maps the field" {
|
|
# FreeToken infers the qwen3.8 reasoning/tool dialects from the model
|
|
# family and emits thinking in `reasoning_content` natively — NO
|
|
# --reasoning-parser/--tool-call-parser override flags. The vLLM-era
|
|
# client-contract wiring (qwen3 parser, qwen3_xml tool parser,
|
|
# default-chat-template-kwargs) is gone; models.yml's
|
|
# reasoningContentField flip is the harness-side contract and lives in
|
|
# the operator's home, not this repo.
|
|
! grep -q -- '--reasoning-parser' "$SERVE"
|
|
! grep -q -- '--tool-call-parser' "$SERVE"
|
|
! grep -q -- '--enable-auto-tool-choice' "$SERVE"
|
|
! grep -q -- '--default-chat-template-kwargs' "$SERVE"
|
|
! grep -q 'FRANK_PRIMARY_REASONING_PARSER' "$ENV_FILE"
|
|
! grep -q 'FRANK_PRIMARY_TOOL_PARSER' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the serve script gates the image, the payload and the GPU nodes" {
|
|
# The image gate is EXISTENCE (podman image exists): the FreeToken
|
|
# image is built ON THE HOST — a local tag carries no RepoDigests — and
|
|
# pinning lives in the Dockerfile (pinned PR-head SHA, base image).
|
|
# The payload gate checks config.json and the exact shard count from
|
|
# the env — a missing shard means a guaranteed long-timeout boot
|
|
# instead of a fast failure.
|
|
grep -q 'FRANK_PRIMARY_IMAGE' "$SERVE"
|
|
grep -q 'podman image exists' "$SERVE"
|
|
grep -q 'FRANK_PRIMARY_MODEL_SHARDS' "$SERVE"
|
|
grep -q 'config.json' "$SERVE"
|
|
grep -q 'model.safetensors.index.json' "$SERVE"
|
|
grep -q '/dev/kfd' "$SERVE"
|
|
grep -q -- '--device /dev/dri' "$SERVE"
|
|
}
|
|
|
|
@test "the serve script clears stale containers and self-heals via podman wait" {
|
|
# A crashed unit can leave the container name registered forever after;
|
|
# the legacy identities (the vLLM-era frank-primary-qwen container, the
|
|
# retired R9V container and the ad hoc experiment containers) hold the
|
|
# same GPU and die here too. Then block on the container so a crash is
|
|
# a nonzero ExecStart.
|
|
grep -q 'FRANK_PRIMARY_LEGACY_CONTAINERS' "$ENV_FILE"
|
|
grep -q 'frank-primary-qwen' "$ENV_FILE"
|
|
grep -q 'frank-primary-r9v' "$ENV_FILE"
|
|
grep -q 'qwen-fp8' "$ENV_FILE"
|
|
grep -q 'exec podman wait "$FRANK_PRIMARY_CONTAINER"' "$SERVE"
|
|
}
|
|
|
|
@test "frank-primary holds the single fleet port under its own alias" {
|
|
# LAN publish of :7770, the harness's model id.
|
|
grep -q '^FRANK_PRIMARY_PUBLISH=7770:7770$' "$ENV_FILE"
|
|
grep -q '^FRANK_PRIMARY_SERVED_NAME=frank-primary$' "$ENV_FILE"
|
|
}
|
|
|
|
@test "the model id matches the payload's served alias wiring" {
|
|
# The payload dir and its shard count are the fetch script's output
|
|
# layout; the fetch script must write where the env points.
|
|
grep -q '^FRANK_PRIMARY_MODEL_DIR=/home/dank/models/Qwen3.8-Flash-Next-Uncensored-FP8$' "$ENV_FILE"
|
|
grep -q '^FRANK_PRIMARY_MODEL_SHARDS=131$' "$ENV_FILE"
|
|
grep -q 'Qwen3.8-Flash-Next-Uncensored-FP8' "$DOWNLOAD"
|
|
grep -q 'orcarouter/Qwen3.8-Flash-Next-Uncensored-FP8' "$DOWNLOAD"
|
|
}
|
|
|
|
@test "no bare HIP-reserved env var is defined" {
|
|
# 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
|
|
# class. Every key here is FRANK_PRIMARY_-prefixed.
|
|
! grep -qE '^(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$ENV_FILE"
|
|
! grep -qE '^(export )?(TEMP|HOST|GPU_MAX_HEAP_SIZE)=' "$SERVE"
|
|
}
|
|
|
|
@test "the Dockerfile pins the FreeToken source and follows the ROCm install law" {
|
|
# The engine is the PR #587 head, NOT a moving branch: a future push to
|
|
# rocm714_torch214 would otherwise silently change the deployed engine.
|
|
grep -q '77a7201f98f00d93f618c6cf8b81ffe8360e0899' "$DOCKERFILE"
|
|
grep -q 'pull/587/head' "$DOCKERFILE"
|
|
# docs/install_amd.md's build law: --no-build-isolation --no-deps (a
|
|
# plain `pip install -e .` on ROCm can REPLACE the ROCm torch with a
|
|
# CUDA build via its NVIDIA-authored dependency set), curated portable
|
|
# deps, flashlib --no-deps.
|
|
grep -q -- '--no-build-isolation --no-deps -e .' "$DOCKERFILE"
|
|
grep -q 'flashlib==0.3.0' "$DOCKERFILE"
|
|
# The CUDA-only backends have no ROCm builds and are intentionally
|
|
# absent (the PR's 12 pytest failures are exactly those backends;
|
|
# FreeToken falls back to its Triton kernels).
|
|
! grep -q 'flashinfer' "$DOCKERFILE"
|
|
! grep -q 'sgl_kernel' "$DOCKERFILE"
|
|
! grep -q 'nvidia-cutlass-dsl' "$DOCKERFILE"
|
|
# The base image is digest-pinned (the registry tag is not trusted).
|
|
grep -qE '^FROM rocm/pytorch:.*@sha256:[0-9a-f]{64}$' "$DOCKERFILE"
|
|
}
|
|
|
|
@test "the deployer gates the image, payload and containers, then health-checks" {
|
|
# The deployer refuses when the image is absent from rootless storage
|
|
# or the payload is missing/wrong-sharded — then clears stale containers
|
|
# (including the legacy identities), waits out the port,
|
|
# relinks/enables/restarts the root unit and health-gates it.
|
|
grep -q 'LLM_PRIMARY_CONTAINER=frank-primary$' "$REMOTE_DEPLOY"
|
|
grep -q 'FRANK_PRIMARY_IMAGE' "$REMOTE_DEPLOY"
|
|
grep -q 'podman image exists' "$REMOTE_DEPLOY"
|
|
grep -q 'FRANK_PRIMARY_MODEL_SHARDS' "$REMOTE_DEPLOY"
|
|
grep -q 'config.json' "$REMOTE_DEPLOY"
|
|
grep -q 'frank-primary-qwen' "$REMOTE_DEPLOY"
|
|
grep -q 'qwen-fp8' "$REMOTE_DEPLOY"
|
|
grep -q 'glm-probe' "$REMOTE_DEPLOY"
|
|
grep -q 'wait_port_free 7770' "$REMOTE_DEPLOY"
|
|
grep -q 'relink_unit frank-primary' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl enable frank-primary.service' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl restart frank-primary.service' "$REMOTE_DEPLOY"
|
|
# The health gate reads the BODY, not just the HTTP code: FreeToken
|
|
# returns 200 with {"status":"loading"} while the engine loads (the
|
|
# FastAPI front end binds before the scheduler is up — a bare 200
|
|
# false-greened a deploy at 6 s while 131 shards were still paging in,
|
|
# measured 2026-10-06). Only status:"ok" counts.
|
|
grep -q 'curl -fsS http://127.0.0.1:7770/health' "$REMOTE_DEPLOY"
|
|
grep -q 'never reached /health status ok' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "the deployer will not report green on a stray port-holder" {
|
|
# The 2026-09-26 false green: a leftover probe held :7770 and satisfied
|
|
# the health curl while the real unit was stopped. The deploy is only
|
|
# green when the unit's own MainPID is live behind the health answer.
|
|
grep -q 'systemctl show -p MainPID --value frank-primary.service' "$REMOTE_DEPLOY"
|
|
grep -q 'stray process holds the port' "$REMOTE_DEPLOY"
|
|
}
|