This test failed on a clean tree for many sessions, and the working belief became "test 73 is a known failure, ignore it" — which is exactly what a permanently red suite teaches. The cause was a selector bug: `find dankclaw -name 'Containerfile*' | head -1` returned whichever Containerfile the directory listing happened to put first (dankclaw/alloy/Containerfile), so the assertion read a file that never claimed to be ComfyUI's. It would equally have passed on a wrong file. The test now resolves the Containerfile the way the build does — build.context joined with build.dockerfile, both read from dankclaw/compose.yaml — so it checks the file the deploy actually builds (dankclaw/Containerfile.comfyui, the one compose, deploy-remote.sh and the ComfyUI doc all name) and follows a future rename instead of guessing. Suite: ALL TESTS PASSED (310), no exceptions to explain away.
107 lines
4.7 KiB
Bash
107 lines
4.7 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-08-31): the GPU migration is applied. The compose builds
|
|
# comfyui-rocm:gfx1151 from Containerfile.comfyui (AMD's gfx1151 torch base),
|
|
# passes /dev/kfd + /dev/dri, and relies on the udev rule at
|
|
# dankclaw/udev/99-comfyui-gpu-access.rules (MODE 0666 on kfd + renderD*) for
|
|
# compute-node access; group_add is keep-groups only. The four compose-wiring
|
|
# tests below are active (un-skipped). See
|
|
# serverAppConfigs-docs/Research/ComfyUI ROCm and Vulkan.md.
|
|
|
|
load helpers
|
|
|
|
@test "comfyui builds from the gfx1151 ROCm Containerfile" {
|
|
local compose="$REPO_ROOT/dankclaw/compose.yaml"
|
|
python3 - "$compose" <<'PYEOF'
|
|
import sys, yaml
|
|
with open(sys.argv[1]) as fh:
|
|
data = yaml.safe_load(fh)
|
|
svc = data["services"]["comfyui"]
|
|
assert svc.get("build"), "comfyui must have a build: section (Containerfile)"
|
|
assert svc.get("image", "").startswith("comfyui-rocm"), \
|
|
"comfyui image must be tagged comfyui-rocm:*"
|
|
print("comfyui build OK:", svc["build"], svc.get("image"))
|
|
PYEOF
|
|
}
|
|
|
|
@test "comfyui Containerfile is based on the gfx1151 torch image" {
|
|
# Resolve the Containerfile the way the build does — build.context joined
|
|
# with build.dockerfile — rather than asking the filesystem. A
|
|
# `find … | head -1` here 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.
|
|
local cf
|
|
cf=$(python3 - "$REPO_ROOT/dankclaw/compose.yaml" "$REPO_ROOT/dankclaw" <<'PYEOF'
|
|
import os, sys, yaml
|
|
with open(sys.argv[1]) as fh:
|
|
svc = yaml.safe_load(fh)["services"]["comfyui"]
|
|
build = svc.get("build") or {}
|
|
# Compose resolves dockerfile relative to context, context relative to the
|
|
# compose file's own directory.
|
|
print(os.path.normpath(os.path.join(
|
|
sys.argv[2], build.get("context", "."), build.get("dockerfile", "Containerfile"))))
|
|
PYEOF
|
|
)
|
|
[ -n "$cf" ] || { echo "could not resolve the comfyui Containerfile"; return 1; }
|
|
[ -f "$cf" ] || { echo "$cf (from compose build) 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" {
|
|
python3 - "$REPO_ROOT/dankclaw/compose.yaml" <<'PYEOF'
|
|
import sys, yaml
|
|
with open(sys.argv[1]) as fh:
|
|
data = yaml.safe_load(fh)
|
|
devices = data["services"]["comfyui"].get("devices", [])
|
|
joined = "\n".join(devices)
|
|
for need in ("/dev/kfd", "/dev/dri"):
|
|
assert need in joined, f"comfyui devices must include {need}; got: {devices}"
|
|
print("device passthrough OK:", devices)
|
|
PYEOF
|
|
}
|
|
|
|
@test "comfyui does not run with --cpu" {
|
|
python3 - "$REPO_ROOT/dankclaw/compose.yaml" <<'PYEOF'
|
|
import sys, yaml
|
|
with open(sys.argv[1]) as fh:
|
|
data = yaml.safe_load(fh)
|
|
cmd = data["services"]["comfyui"].get("command", [])
|
|
joined = " ".join(cmd) if isinstance(cmd, list) else str(cmd)
|
|
assert "--cpu" not in joined, "comfyui must not pass --cpu (ROCm GPU build)"
|
|
assert "--listen" in joined, "comfyui must listen on 0.0.0.0"
|
|
print("command OK:", cmd)
|
|
PYEOF
|
|
}
|
|
|
|
@test "comfyui GPU access uses keep-groups + udev MODE 0666, not named groups" {
|
|
python3 - "$REPO_ROOT/dankclaw/compose.yaml" "$REPO_ROOT/dankclaw/udev/99-comfyui-gpu-access.rules" <<'PYEOF'
|
|
import sys, yaml
|
|
with open(sys.argv[1]) as fh:
|
|
data = yaml.safe_load(fh)
|
|
groups = data["services"]["comfyui"].get("group_add", [])
|
|
# 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 list(groups) == ["keep-groups"], \
|
|
f"comfyui group_add 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
|
|
}
|