The two replicas behind :7770 were on plain round-robin, which throws away prefix-cache hits: an agent loop re-sends its system prompt and file context every turn, and alternating replicas means every other turn pays a cold prefill. Measured with 90k-token contexts, warm-turn TTFT was 2676 ms. Pinning everything to one replica is not the answer either — a cold batch (the shape a benchmark or a burst of parallel subagents produces) then loses a card's worth of prefill: 16 requests took 30.1 s pinned against 18.6 s spread. So the policy routes by request shape. A continuation (more than one message — the agent-loop signature) is pinned to the replica that served its prefix; anything else spreads across the cards by counter. Measured: 19.97 s for the burst and 1095 ms warm-turn TTFT — the best of both, without a regression on either workload. The policy lives in proxy/routing.py, separate from the HTTP plumbing in proxy/affinity-proxy.py, so it can be tested without aiohttp, a network or a GPU. tests/frank_medium_proxy.bats covers it, and it earned its keep: the first version hashed fresh prefixes to pick a card, which sends every member of a same-prefix burst to one card (29.8 s, measured), and a stale index in the pin path returned 500 on every continuation. Both are now assertions. ROUTING=roundrobin and ROUTING=affinity stay available to re-measure the choice on this host; the default is adaptive.
165 lines
7.9 KiB
Bash
Executable File
165 lines
7.9 KiB
Bash
Executable File
#!/usr/bin/env bats
|
|
# Seam: the frank-medium service on dankserver.
|
|
# frank-medium became a rootless Quadlet deployment on 2026-09-15 when the
|
|
# vllm-radiance MXFP4 stack replaced the charlie12345/ROCmFPX llama.cpp
|
|
# runtime on the 2x Radeon AI PRO R9700. The intended shape is one
|
|
# tensor-parallel process across both cards (frank-medium.container, kept as
|
|
# the target); it cannot run on this host because RCCL's kernels require PCIe
|
|
# atomics and the second card's chipset path advertises AtomicOpsCap: Routing-.
|
|
# The deployed shape is therefore two single-GPU replicas behind a round-robin
|
|
# proxy, which keeps the fleet contract of one :7770 endpoint. This seam pins
|
|
# the properties that make that work: GPU access on the fixed device indices,
|
|
# the upstream launcher's podman posture, host networking with the fleet's
|
|
# client port and alias, per-replica resource shape, boot persistence, and a
|
|
# deploy handler that moves the unusable TP=2 unit into the trash.
|
|
|
|
load helpers
|
|
|
|
UNIT_A="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium-a.container"
|
|
UNIT_B="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium-b.container"
|
|
UNIT_PROXY="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium-proxy.container"
|
|
UNIT_TP2="$REPO_ROOT/dankserver/frank-medium/quadlet/frank-medium.container"
|
|
ENTRYPOINT="$REPO_ROOT/dankserver/frank-medium/radiance-entrypoint.sh"
|
|
HEALTHCHECK="$REPO_ROOT/dankserver/frank-medium/frank-medium-healthcheck.py"
|
|
PROXY="$REPO_ROOT/dankserver/frank-medium/proxy/affinity-proxy.py"
|
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|
|
|
@test "the frank-medium units live under dankserver/frank-medium/quadlet/" {
|
|
[ -f "$UNIT_A" ]
|
|
[ -f "$UNIT_B" ]
|
|
[ -f "$UNIT_PROXY" ]
|
|
}
|
|
|
|
@test "the TP=2 unit is kept as the documented target, not deleted" {
|
|
# It becomes runnable again only on a host whose second card sits on an
|
|
# atomic-capable root port, so the artifact stays in the repo.
|
|
[ -f "$UNIT_TP2" ]
|
|
grep -q '^Environment=TP=2$' "$UNIT_TP2"
|
|
}
|
|
|
|
@test "every frank-medium unit is boot-persistent (WantedBy=default.target)" {
|
|
# Generated units cannot be enabled on the podman 5.x v1 quadlet
|
|
# generator; [Install] plus linger is the entire boot-persistence story.
|
|
for unit in "$UNIT_A" "$UNIT_B" "$UNIT_PROXY"; do
|
|
grep -q '^WantedBy=default.target$' "$unit"
|
|
done
|
|
}
|
|
|
|
@test "every frank-medium unit is self-healing" {
|
|
for unit in "$UNIT_A" "$UNIT_B" "$UNIT_PROXY"; do
|
|
grep -q '^Restart=on-failure$' "$unit"
|
|
grep -q '^RestartSec=' "$unit"
|
|
done
|
|
}
|
|
|
|
@test "frank-medium units pin the image tag and never pull at boot" {
|
|
for unit in "$UNIT_A" "$UNIT_B" "$UNIT_PROXY"; do
|
|
grep -Eq '^Image=localhost/radiance-vllm:[0-9]+\.[0-9]+\.[0-9]+$' "$unit"
|
|
grep -q '^Pull=never$' "$unit"
|
|
done
|
|
}
|
|
|
|
@test "the replicas each get exactly one GPU on the fixed device index" {
|
|
grep -q '^AddDevice=/dev/kfd$' "$UNIT_A"
|
|
grep -q '^AddDevice=/dev/dri$' "$UNIT_A"
|
|
# One index per replica: ROCR filters first and HIP indexes into the
|
|
# filtered list, so setting both to the same non-zero value leaves an
|
|
# empty device set.
|
|
grep -q '^Environment=HIP_VISIBLE_DEVICES=0$' "$UNIT_A"
|
|
grep -q '^Environment=HIP_VISIBLE_DEVICES=1$' "$UNIT_B"
|
|
! grep -q '^Environment=ROCR_VISIBLE_DEVICES=' "$UNIT_A"
|
|
! grep -q '^Environment=ROCR_VISIBLE_DEVICES=' "$UNIT_B"
|
|
}
|
|
|
|
@test "the replicas run with the upstream launcher's podman posture" {
|
|
for unit in "$UNIT_A" "$UNIT_B"; do
|
|
grep -q -- '--privileged' "$unit"
|
|
grep -q -- '--ipc=host' "$unit"
|
|
grep -q -- '--security-opt seccomp=unconfined' "$unit"
|
|
grep -q -- '--network=host' "$unit"
|
|
done
|
|
}
|
|
|
|
@test "the replicas serve single-GPU shapes that fit one card" {
|
|
for unit in "$UNIT_A" "$UNIT_B"; do
|
|
grep -q '^Environment=TP=1$' "$unit"
|
|
# 262144 does not fit one card (needs 9.61 GiB of KV, 5.14 available)
|
|
# and 131072 does not either (5.58 needed vs 5.22 available); 114688
|
|
# is the largest measured shape that boots.
|
|
grep -q '^Environment=MAXLEN=114688$' "$unit"
|
|
# KV_MEM=0 makes vLLM profile this shape: the 2x7551-32624 pin is a
|
|
# TP=2/262144 measurement and invalid here.
|
|
grep -q '^Environment=KV_MEM=0$' "$unit"
|
|
grep -q '^Environment=SNAP=/models/Qwen3.8-27B-Uncensored-MXFP4-awq$' "$unit"
|
|
grep -q '^Environment=SPEC_METHOD=dflash$' "$unit"
|
|
grep -q '^Environment=SPEC=7$' "$unit"
|
|
done
|
|
}
|
|
|
|
@test "the proxy keeps the fleet endpoint: host-network port 7770, alias frank-medium" {
|
|
grep -q '^Environment=PORT=7770$' "$UNIT_PROXY"
|
|
grep -q '^Environment=UPSTREAMS=http://127.0.0.1:7771,http://127.0.0.1:7772$' "$UNIT_PROXY"
|
|
grep -q -- '--network=host' "$UNIT_PROXY"
|
|
[ "$(grep -c '^PublishPort=' "$UNIT_PROXY")" -eq 0 ]
|
|
# The replicas answer as the same served model name, so a client cannot
|
|
# tell which one served a request.
|
|
grep -q '^Environment=SERVED_NAMES=frank-medium$' "$UNIT_A"
|
|
grep -q '^Environment=SERVED_NAMES=frank-medium$' "$UNIT_B"
|
|
grep -q '^Environment=PORT=7771$' "$UNIT_A"
|
|
grep -q '^Environment=PORT=7772$' "$UNIT_B"
|
|
}
|
|
|
|
@test "frank-medium entrypoint is a syntax-clean fork of the upstream launcher" {
|
|
[ -f "$ENTRYPOINT" ]
|
|
bash -n "$ENTRYPOINT"
|
|
grep -q '9735329348ae1c9319f01cbe5f7004ec5f4dba63' "$ENTRYPOINT"
|
|
}
|
|
|
|
@test "frank-medium healthcheck probes the serving endpoint on its own port" {
|
|
[ -f "$HEALTHCHECK" ]
|
|
python3 -m py_compile "$HEALTHCHECK"
|
|
# One probe serves the replicas, the proxy and the TP=2 target because it
|
|
# reads PORT instead of hardcoding 7770.
|
|
grep -q 'os.environ.get("PORT"' "$HEALTHCHECK"
|
|
for unit in "$UNIT_A" "$UNIT_B" "$UNIT_PROXY"; do
|
|
grep -q 'frank-medium-healthcheck.py' "$unit"
|
|
grep -q '^HealthStartPeriod=' "$unit"
|
|
done
|
|
}
|
|
|
|
@test "the proxy unit mounts the routing policy and the server" {
|
|
# The policy lives in its own file so it can be tested without aiohttp;
|
|
# the server imports it by name from its own directory, so both must be
|
|
# mounted or the container dies on ModuleNotFoundError at start.
|
|
grep -q 'proxy/routing.py:/usr/local/bin/routing.py:ro' "$UNIT_PROXY"
|
|
grep -q 'proxy/affinity-proxy.py:/usr/local/bin/affinity-proxy.py:ro' "$UNIT_PROXY"
|
|
# Routing behaviour itself is covered by tests/frank_medium_proxy.bats,
|
|
# which drives the policy directly.
|
|
[ -f "$REPO_ROOT/tests/frank_medium_proxy.bats" ]
|
|
}
|
|
@test "the deploy handler installs the trio and retires the TP=2 unit" {
|
|
grep -q 'install_quadlet frank-medium-a dankserver/frank-medium/quadlet frank-medium-a' "$REMOTE_DEPLOY"
|
|
grep -q 'install_quadlet frank-medium-b dankserver/frank-medium/quadlet frank-medium-b' "$REMOTE_DEPLOY"
|
|
grep -q 'install_quadlet frank-medium-proxy dankserver/frank-medium/quadlet frank-medium-proxy' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl --user restart frank-medium-a.service frank-medium-b.service frank-medium-proxy.service' "$REMOTE_DEPLOY"
|
|
# The TP=2 unit cannot become healthy on this host; a leftover copy would
|
|
# leave the generator restarting it forever. It moves to the trash.
|
|
grep -q 'mv -- "$quadlet_unit_dir/frank-medium.container"' "$REMOTE_DEPLOY"
|
|
! grep -qE '(^|[^[:alnum:]_])rm (-[a-zA-Z]+ )?"?\$quadlet_unit_dir/frank-medium' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "the deploy handler never tries to enable a generated unit" {
|
|
! grep -q 'systemctl --user enable frank-medium' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "the deploy handler retires the old root unit without deleting it" {
|
|
grep -q 'retire_root_llm_medium' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl stop frank-medium.service' "$REMOTE_DEPLOY"
|
|
grep -q 'mv -- /etc/systemd/system/frank-medium.service' "$REMOTE_DEPLOY"
|
|
! grep -qE '(^|[^[:alnum:]_])rm (-[a-zA-Z]+ )?/etc/systemd/system/frank-medium' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "the deploy handler re-tags the registry image to the localhost alias" {
|
|
grep -q 'podman tag docker.io/stilldeadcode/vllm-radiance:0.9.3 localhost/radiance-vllm:0.9.3' "$REMOTE_DEPLOY"
|
|
}
|