The vllm-radiance replica pair could not use both cards for one model: TP=2
needs a cross-card collective and the second R9700 cannot dispatch one. The
same wall blocks vLLM's pipeline parallelism, whose stage handoff is an RCCL
send/recv of the hidden state (distributed/parallel_state.py) -- a fact worth
writing down, because the obvious reading of the ask ('use PP to dodge the
atomics problem') is exactly backwards.
llama.cpp's layer split needs no collective at all: layers are distributed and
the boundary activation is a per-backend copy, once per forward pass. One
process, both cards, one model. The runtime is mainline llama.cpp at fb27a525
(the ref is pinned by DFlash2 AND qwen35 MTP; the ROCmFPX fork this host had
before has neither), built for gfx1201 against the private ROCm 7.14 toolchain
the earlier build already used, and bundled next to the binary.
Server: ajgazin Swift-Qwen3.8-27B-Uncensored UD-Q8_K_XL with the model's MTP
head in the file, plus the z-lab DFlash2 Q8_0 block-diffusion drafter at depth
7 (its block is 8, so 7 is the ceiling llama.cpp allows). Text only: vision
stays on frank-small.
Retired into the trash rather than deleted: the three vLLM quadlets, the
radiance entrypoint/healthcheck, the routing proxy and its tests, and the two
bench scripts that drove 'vllm bench serve' inside the retired image. The
deploy handler now stops them, waits for 7770 to be released, installs the
ROOT unit and refuses to start anything when the runtime or payload is
missing. The planner maps the scripts/ trio to frank-medium only, and the
scoping seam asserts the retired service dir is gone rather than merely
unmapped.
224 lines
9.1 KiB
Bash
224 lines
9.1 KiB
Bash
#!/usr/bin/env bats
|
|
# Seam: service-scoped deployment planning.
|
|
# A repository commit can contain changes for several workloads while only a
|
|
# subset is safe to restart. The planner must keep inference isolated from
|
|
# unrelated service changes and treat docs-only changes as a no-op.
|
|
|
|
load helpers
|
|
|
|
PLANNER="$REPO_ROOT/scripts/deploy-plan.py"
|
|
REMOTE_DEPLOY="$REPO_ROOT/scripts/deploy-remote.sh"
|
|
|
|
@test "deployment planner maps SearXNG changes to SearXNG only" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
shared/searxng/settings.yml serverAppConfigs-docs/Dankclaw/SearXNG.md
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "searxng" ]
|
|
}
|
|
|
|
@test "deployment planner maps large-model changes to the large model only" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
dankclaw/frank-large/quadlet/frank-large.container
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "frank-large" ]
|
|
}
|
|
|
|
@test "the retired NPU endpoint is absent from the deployment inventory" {
|
|
! grep -q '"frank-npu"' "$PLANNER"
|
|
! grep -q 'frank_npu\|frank-npu' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "the retired ik chat template plans nothing, and its archive is still here" {
|
|
# scripts/chat-template.jinja existed only for the ik_llama.cpp runtime
|
|
# (retired 2026-09-16 in favour of halogen-flash-server again). The
|
|
# llama.cpp frank-small runtime serves the template the GGUF itself
|
|
# carries, which is the one with reasoning_effort, preserve_thinking and
|
|
# the tool-call grammar. Editing the archive must deploy nothing -- and the
|
|
# archive must still be present, so this cannot pass by the file having
|
|
# vanished.
|
|
[ -f "$REPO_ROOT/scripts/.AGENTTRASH/chat-template.jinja.ik-llama-qwen-sharp" ]
|
|
run python3 "$PLANNER" --target dankclaw --paths scripts/chat-template.jinja
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
|
|
run python3 "$PLANNER" --target dankserver --paths scripts/chat-template.jinja
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "deployment planner maps the frank-medium scripts trio to frank-medium only" {
|
|
run python3 "$PLANNER" --target dankserver --paths \
|
|
scripts/frank-medium.env scripts/frank-medium.service scripts/serve-frank-medium.sh
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "frank-medium" ]
|
|
|
|
# The path is not the assertion: with the vllm-radiance service dir
|
|
# retired into the trash, a planner rule still naming dankserver/frank-medium
|
|
# would plan nothing and look identical to a correct one. Assert the
|
|
# invariant (the dir is gone) as well as the mapping above.
|
|
[ ! -d "$REPO_ROOT/dankserver/frank-medium" ]
|
|
run python3 "$PLANNER" --target dankserver --paths dankserver/frank-medium/quadlet/frank-medium-a.container
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "the small endpoint is Dankclaw's again, and Dankserver's copy stays retired" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
scripts/frank-small.env scripts/frank-small.service scripts/serve-frank-small.sh
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "frank-small" ]
|
|
|
|
# frank-small runs on Dankclaw on the RTX 3090; the Dankserver endpoint of
|
|
# that name was retired with the FreeToken runtime on 2026-09-15 and must
|
|
# not come back through a planner mapping.
|
|
run python3 "$PLANNER" --target dankserver --paths \
|
|
scripts/frank-small.env scripts/frank-small.service scripts/serve-frank-small.sh
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "documentation-only changes plan no runtime deployment" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
serverAppConfigs-docs/Architecture/Deployment\ Flow.md serverAppConfigs-docs/Dankclaw/SearXNG.md
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "deployment planner maps shared config to one service" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
shared/prometheus/prometheus.yml
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "prometheus" ]
|
|
}
|
|
|
|
@test "deployment planner maps shared observability configs independently" {
|
|
run python3 "$PLANNER" --target dankclaw --paths shared/loki/loki-config.yaml
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "loki" ]
|
|
|
|
run python3 "$PLANNER" --target dankclaw --paths shared/alloy/config.alloy
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "alloy" ]
|
|
}
|
|
|
|
@test "deployment planner maps service-owned non-compose files" {
|
|
run python3 "$PLANNER" --target dankclaw --paths dankclaw/hermes/config.yaml.example
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "hermes" ]
|
|
}
|
|
|
|
@test "homepage is owned by Dankclaw's consolidated proxy" {
|
|
run python3 "$PLANNER" --target dankserver --paths shared/homepage/index.html
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "compose planner narrows a compose edit to the changed service" {
|
|
local base head
|
|
base=$(git -C "$REPO_ROOT" rev-parse HEAD)
|
|
head=$(git -C "$REPO_ROOT" rev-parse HEAD)
|
|
run python3 "$PLANNER" --target dankclaw --repo "$REPO_ROOT" \
|
|
--base "$base" --head "$head" --paths dankclaw/compose.yaml
|
|
[ "$status" -eq 0 ]
|
|
[ -z "$output" ]
|
|
}
|
|
|
|
@test "network-coupled SearXNG config changes include the required peers" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
dankclaw/searxng/quadlet/gluetun.container
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = $'gluetun\nsearxng\nmarreta' ]
|
|
}
|
|
|
|
@test "retiring the standalone Gitea proxy deploys the canonical Caddy" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
dankclaw/gitea/quadlet/gitea-caddy.container
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "caddy" ]
|
|
}
|
|
|
|
@test "Gitea network changes exclude the retired standalone proxy" {
|
|
run python3 "$PLANNER" --target dankclaw --paths \
|
|
dankclaw/gitea/quadlet/gitea.network
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = $'gitea-postgres\ngitea-server' ]
|
|
}
|
|
|
|
@test "frankbook-pro gluetun changes carry the whole netns family; arr changes stay scoped" {
|
|
run python3 "$PLANNER" --target frankbook-pro --paths \
|
|
frankbook-pro/gluetun/quadlet/gluetun.container
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = $'gluetun\nprowlarr\nqbittorrent\nradarr\nsonarr\nlidarr\nreadarr' ]
|
|
run python3 "$PLANNER" --target frankbook-pro --paths \
|
|
frankbook-pro/sonarr/quadlet/sonarr.container
|
|
[ "$status" -eq 0 ]
|
|
[ "$output" = "sonarr" ]
|
|
}
|
|
|
|
@test "remote Caddy deployment no longer blocks on the retired Gitea proxy" {
|
|
! grep -q 'refusing to replace it with caddy' "$REMOTE_DEPLOY"
|
|
! grep -q 'podman container exists gitea-caddy &&' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "remote Caddy deployment retires the installed Gitea proxy" {
|
|
grep -q 'retire_legacy_gitea_caddy' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl --user stop gitea-caddy.service' "$REMOTE_DEPLOY"
|
|
grep -q 'systemctl --user disable gitea-caddy.service' "$REMOTE_DEPLOY"
|
|
grep -q 'gitea-caddy.container' "$REMOTE_DEPLOY"
|
|
grep -q 'HOME/.AGENTTRASH' "$REMOTE_DEPLOY"
|
|
grep -q 'stage_legacy_gitea_caddy_state' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "remote deployment inventory no longer accepts gitea-caddy as a service" {
|
|
! grep -qE 'dankclaw:gitea-caddy|gitea-caddy\) deploy' "$REPO_ROOT/scripts/deploy.sh" "$REPO_ROOT/scripts/deploy-remote.sh"
|
|
! grep -q '"gitea-caddy"' "$REPO_ROOT/scripts/deploy-plan.py"
|
|
}
|
|
|
|
@test "remote deployer carries per-service deployment state" {
|
|
[ -x "$REMOTE_DEPLOY" ]
|
|
grep -q 'deploy-state' "$REMOTE_DEPLOY"
|
|
grep -q 'STATE\[' "$REMOTE_DEPLOY"
|
|
grep -q 'mark_status.*pending\|status=pending' "$REMOTE_DEPLOY"
|
|
grep -q 'mark_status.*deployed\|status=deployed' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "remote deployer routes inference restarts through selected handlers" {
|
|
# The runner may restart inference only from the service-dispatch path. It
|
|
# must not contain a host-wide restart loop.
|
|
grep -q 'deploy_llm_large' "$REMOTE_DEPLOY"
|
|
grep -q 'deploy_llm_medium' "$REMOTE_DEPLOY"
|
|
grep -q 'deploy_service' "$REMOTE_DEPLOY"
|
|
! grep -qE 'for .*frank-(small|large|npu)-?' "$REMOTE_DEPLOY"
|
|
}
|
|
|
|
@test "remote runner rejects services belonging to the other host" {
|
|
run env DEPLOY_STATE_DIR="$(mktemp -d)" REMOTE_DIR="$REPO_ROOT" \
|
|
bash "$REMOTE_DEPLOY" dankserver "$(git rev-parse HEAD)" frank-default-large
|
|
[ "$status" -eq 2 ]
|
|
}
|
|
|
|
@test "remote runner recognizes a pending service at the same revision" {
|
|
local state_dir="$(mktemp -d)"
|
|
printf 'revision=%s\nstatus=pending\n' "$(git rev-parse HEAD)" > "$state_dir/unknown-service"
|
|
run env DEPLOY_STATE_DIR="$state_dir" REMOTE_DIR="$REPO_ROOT" \
|
|
bash "$REMOTE_DEPLOY" dankclaw "$(git rev-parse HEAD)" --pending
|
|
[ "$status" -eq 2 ]
|
|
[[ "$output" == *"unknown-service"* || "$output" == *"not deployed"* ]]
|
|
}
|
|
|
|
@test "top-level deployer delegates to the service-scoped remote runner" {
|
|
grep -q 'deploy-remote.sh' "$REPO_ROOT/scripts/deploy.sh"
|
|
grep -qE 'SERVICE|service' "$REPO_ROOT/scripts/deploy.sh"
|
|
grep -q 'OLD_REVISION' "$REPO_ROOT/scripts/deploy.sh"
|
|
grep -q 'OLD_REVISION\|old checkout revision' "$REPO_ROOT/scripts/deploy.sh"
|
|
}
|
|
|
|
@test "legacy service deploy wrappers delegate to the scoped deployer" {
|
|
for script in deploy-searxng-quadlet.sh deploy-pihole-quadlet.sh deploy-open-webui-quadlet.sh; do
|
|
grep -q 'deploy.sh' "$REPO_ROOT/scripts/$script" || {
|
|
echo "$script does not delegate to deploy.sh"
|
|
return 1
|
|
}
|
|
done
|
|
}
|