Files
serverAppConfigs/tests/comfyui_rocm.bats
T
Frank 4b443c86d8 comfyui: bake qwen_image21 patch into image via startup wrapper
The qwen_image21 detection check and GGUF node arch list patch are
applied by a startup wrapper (comfyui-start.sh) that runs before
ComfyUI starts. The patch script and wrapper are baked into the image
so they survive container recreations. The quadlet's Exec now points
at the wrapper instead of main.py directly.
2026-09-21 19:55:00 -07:00

108 lines
5.6 KiB
Bash

#!/usr/bin/env bats
# Seam: ComfyUI ROCm GPU configuration on Dankclaw.
# The gfx1151-enabled PyTorch build is the only verified GPU path on Strix
# Halo. The compose must:
# - build from a Containerfile based on the gfx1151 ROCm torch image
# - pass /dev/kfd and /dev/dri to the container
# - NOT pass --cpu
# - grant GPU access via world-writable compute nodes (udev MODE 0666), not
# named-group membership, because systemd user-unit spawns carry NO
# supplementary groups at spawn time
#
# STATUS (2026-09-16): ComfyUI is a Quadlet unit, not a compose service. The
# build still comes from Containerfile.comfyui (AMD's gfx1151 torch base) via
# the deployer's build_compose_image, and the wiring now lives in
# dankclaw/comfyui/quadlet/comfyui.container: /dev/kfd + /dev/dri, keep-groups
# only, and the udev rule at dankclaw/udev/99-comfyui-gpu-access.rules (MODE
# 0666 on kfd + renderD*) for compute-node access, because systemd user-unit
# spawns carry no supplementary groups. The move happened after the compose
# container went stale — created before the GPU declarations existed, then
# restarted 9,146 times without them. See
# serverAppConfigs-docs/Research/ComfyUI ROCm and Vulkan.md.
load helpers
@test "comfyui builds from the gfx1151 ROCm Containerfile" {
# The image is built by the deployer (build_compose_image comfyui, which
# resolves dankclaw/Containerfile.comfyui) and referenced by the unit.
grep -q "comfyui)" "$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'image=comfyui-rocm:gfx1151' "$REPO_ROOT/scripts/deploy-remote.sh"
grep -q 'containerfile=dankclaw/Containerfile.comfyui' "$REPO_ROOT/scripts/deploy-remote.sh"
local unit="$REPO_ROOT/dankclaw/comfyui/quadlet/comfyui.container"
grep -q '^Image=localhost/comfyui-rocm:gfx1151$' "$unit" \
|| { echo "unit must run the locally built image"; return 1; }
# One owner per container name: a surviving compose service would fight the
# unit for the container, which is how the stale one stayed alive. The file
# itself is retired (observability_quadlet.bats asserts that), so here the
# regression to catch is a re-added entry — the check must not go hollow
# just because the file is absent.
local cf="$REPO_ROOT/dankclaw/compose.yaml"
[ ! -f "$cf" ] || ! grep -qE '^\s+comfyui:' "$cf"
}
@test "comfyui Containerfile is based on the gfx1151 torch image" {
# Resolve the Containerfile from the deployer's declaration — the file the
# build actually uses — not from the filesystem. A `find … | head -1` here
# once returned whichever Containerfile the directory listing happened to
# put first (dankclaw/alloy/Containerfile), so the test failed for sessions
# while the real ComfyUI Containerfile was correct, and it would equally
# have passed on a wrong file. Compose no longer names it: ComfyUI is a
# Quadlet unit and the image is built by deploy_comfyui.
local cf="$REPO_ROOT/dankclaw/Containerfile.comfyui"
grep -q 'containerfile=dankclaw/Containerfile.comfyui' "$REPO_ROOT/scripts/deploy-remote.sh" \
|| { echo "deployer no longer builds that Containerfile"; return 1; }
[ -f "$cf" ] || { echo "$cf does not exist"; return 1; }
grep -q "gfx1151" "$cf" || { echo "$cf does not reference gfx1151"; return 1; }
grep -q "FROM" "$cf" || { echo "$cf has no FROM"; return 1; }
}
@test "comfyui passes /dev/kfd and /dev/dri to the container" {
# Read the Quadlet unit, not compose: ComfyUI left compose on 2026-09-16
# after a stale, device-less container was restarted 9,146 times by
# `restart: unless-stopped` (see comfyui/quadlet/comfyui.container).
local unit="$REPO_ROOT/dankclaw/comfyui/quadlet/comfyui.container"
[ -f "$unit" ]
grep -q '^AddDevice=/dev/kfd$' "$unit" || { echo "no /dev/kfd"; return 1; }
grep -q '^AddDevice=/dev/dri$' "$unit" || { echo "no /dev/dri"; return 1; }
}
@test "comfyui does not run with --cpu" {
local unit="$REPO_ROOT/dankclaw/comfyui/quadlet/comfyui.container"
local wrapper="$REPO_ROOT/dankclaw/comfyui-start.sh"
local exec_line
exec_line=$(grep -E '^Exec=' "$unit")
# The exec line points at the startup wrapper; the actual flags live there.
local target
case "$exec_line" in
*comfyui-start.sh*) target="$wrapper" ;;
*) target="$unit" ;;
esac
case "$(grep -E 'main\.py|--cpu' "$target")" in
*--cpu*) echo "comfyui must not pass --cpu (ROCm GPU build)"; return 1 ;;
esac
case "$(grep -E 'main\.py|--listen' "$target")" in
*--listen*) ;;
*) echo "comfyui must listen on 0.0.0.0"; return 1 ;;
esac
}
@test "comfyui GPU access uses keep-groups + udev MODE 0666, not named groups" {
python3 - "$REPO_ROOT/dankclaw/comfyui/quadlet/comfyui.container" "$REPO_ROOT/dankclaw/udev/99-comfyui-gpu-access.rules" <<'PYEOF'
import sys, re
unit = open(sys.argv[1]).read()
groups = re.findall(r"^GroupAdd=(.*)$", unit, re.M)
# User-unit spawns carry NO supplementary groups, so named group_add entries
# (video, render) can never be honored at spawn time. The deployed contract is
# keep-groups plus world-writable compute nodes via the udev rule; a future
# edit that drops either half must fail here.
assert groups == ["keep-groups"], \
f"comfyui GroupAdd must be exactly ['keep-groups']; got: {groups}"
rule = open(sys.argv[2]).read()
assert 'KERNEL=="kfd", MODE="0666"' in rule, \
"udev rule must grant MODE 0666 on kfd with NO SUBSYSTEM guard (misc-class devices have no subsystem to match)"
assert 'KERNEL=="renderD*", SUBSYSTEM=="drm", MODE="0666"' in rule, \
"udev rule must grant MODE 0666 on renderD* (SUBSYSTEM drm guard)"
print("GPU access contract OK")
PYEOF
}