Files
2026-07-13 13:13:07 +08:00

373 lines
16 KiB
Bash
Executable File

#!/usr/bin/env bash
#
# GPU acceleration integration tests for smolvm.
#
# Tests the --gpu flag across two tiers:
# 1. virtio-gpu device enumeration (Alpine — fast, no Mesa needed)
# 2. Full Vulkan workloads via patched Mesa on Fedora 42
#
# The suite is SKIPPED (not failed) if GPU support is not compiled into
# libkrun or virglrenderer is unavailable on the host. This keeps CI green
# on machines without GPU infrastructure.
#
# Usage:
# ./tests/test_gpu.sh
# ./tests/run_tests.sh gpu
source "$(dirname "$0")/common.sh"
init_smolvm
log_info "Pre-flight cleanup: killing orphan processes..."
kill_orphan_smolvm_processes
# Name for the shared Fedora machine used across Vulkan workload tests.
GPU_FEDORA_MACHINE="gpu-fedora-$$"
cleanup_gpu() {
"$SMOLVM" machine stop --name "$GPU_FEDORA_MACHINE" 2>/dev/null || true
"$SMOLVM" machine delete --name "$GPU_FEDORA_MACHINE" -f 2>/dev/null || true
}
trap cleanup_gpu EXIT
echo ""
echo "=========================================="
echo " smolvm GPU Tests"
echo "=========================================="
echo ""
# =============================================================================
# GPU availability probe
# =============================================================================
# Run a trivial --gpu command in Alpine to confirm GPU support is compiled in
# and virglrenderer is present. If either is missing, smolvm emits one of:
# - "libkrun was built without GPU support (krun_set_gpu_options2 not found)"
# - "krun_set_gpu_options2 failed ... Check that virglrenderer is installed."
# On those messages we skip the whole suite (not fail) — GPU is optional.
#
# Note: --net is required here because image pulls happen inside the VM.
log_info "Probing GPU support..."
_PROBE_EXIT=0
_PROBE_OUT=$(run_with_timeout 120 "$SMOLVM" machine run --gpu --net --image alpine:latest -- true 2>&1) \
|| _PROBE_EXIT=$?
if [[ $_PROBE_EXIT -ne 0 ]]; then
if echo "$_PROBE_OUT" | grep -qE "krun_set_gpu_options2|without GPU support|virglrenderer"; then
echo ""
log_skip "GPU not available on this host — skipping all GPU tests"
log_skip " Cause: $(echo "$_PROBE_OUT" | grep -E "krun_set_gpu_options2|without GPU|virglrenderer" | head -1)"
log_skip " To enable: rebuild libkrun with GPU=1 and install virglrenderer"
echo ""
print_summary "GPU Tests"
exit 0
fi
# Probe failed for an unrelated reason (e.g. transient network). Proceed
# and let the individual tests surface a meaningful error.
log_info "Probe failed (exit $_PROBE_EXIT) — not a GPU capability error, proceeding"
else
log_info "GPU probe passed — virglrenderer and krun_set_gpu_options2 both available"
fi
# =============================================================================
# Helper
# =============================================================================
# Returns 0 if the shared Fedora GPU machine is currently running.
_fedora_running() {
"$SMOLVM" machine status --name "$GPU_FEDORA_MACHINE" 2>&1 | grep -q "running"
}
# =============================================================================
# Section 1: virtio-gpu device enumeration (Alpine, no Mesa)
# =============================================================================
# When --gpu is passed, libkrun creates a virtio-gpu PCI device in the VM.
# The guest kernel (CONFIG_DRM_VIRTIO_GPU=y in libkrunfw) initialises the DRM
# subsystem and creates two device nodes:
# /dev/dri/renderD128 — render node (Vulkan/OpenGL compute, no modesetting)
# /dev/dri/card0 — DRM card (modesetting, display)
#
# The agent's add_gpu_devices_if_available() (crates/smolvm-agent/src/oci.rs)
# then forwards both nodes into the OCI container spec with mode 0o666 so
# unprivileged processes can open them.
#
# Alpine has no Vulkan userspace, but the kernel device nodes are enough to
# verify the full host→guest→container forwarding pipeline.
echo ""
echo "Running virtio-gpu device tests (Alpine)..."
test_dri_renderD128_present() {
local out
out=$(run_with_timeout 120 "$SMOLVM" machine run --gpu --net --image alpine:latest -- \
ls /dev/dri/renderD128 2>&1) || return 1
[[ "$out" == *"renderD128"* ]] || { echo "FAIL: renderD128 absent (got: $out)"; return 1; }
}
test_dri_card0_present() {
local out
out=$(run_with_timeout 120 "$SMOLVM" machine run --gpu --net --image alpine:latest -- \
ls /dev/dri/card0 2>&1) || return 1
[[ "$out" == *"card0"* ]] || { echo "FAIL: card0 absent (got: $out)"; return 1; }
}
test_renderD128_world_readable() {
# The OCI spec sets mode 0o666 on the render node. Verify an unprivileged
# stat succeeds (open would need actual Vulkan libs, but stat is sufficient).
local out
out=$(run_with_timeout 120 "$SMOLVM" machine run --gpu --net --image alpine:latest -- \
stat /dev/dri/renderD128 2>&1) || { echo "FAIL: stat failed (got: $out)"; return 1; }
[[ "$out" == *"renderD128"* ]] || { echo "FAIL: stat output unexpected (got: $out)"; return 1; }
}
test_interactive_run_has_dri() {
# `-i`/`-t` uses the agent's interactive OCI bundle path. It must wire
# GPU devices just like non-interactive runs, otherwise shells started with
# `-it` cannot use Vulkan even though the VM itself has virtio-gpu.
local out
out=$(run_with_timeout 120 "$SMOLVM" machine run --gpu --net -i --image alpine:latest -- \
ls /dev/dri/renderD128 2>&1) || { echo "FAIL: interactive run missing renderD128 (got: $out)"; return 1; }
[[ "$out" == *"renderD128"* ]] || { echo "FAIL: interactive run output unexpected (got: $out)"; return 1; }
}
test_no_dri_without_gpu() {
# Without --gpu, no virtio-gpu device → no /dev/dri in guest → no DRI
# devices in OCI container spec. ls /dev/dri should fail or produce nothing.
local out exit_code=0
out=$(run_with_timeout 120 "$SMOLVM" machine run --net --image alpine:latest -- \
ls /dev/dri 2>&1) || exit_code=$?
if [[ $exit_code -eq 0 ]] && echo "$out" | grep -qE "render|card"; then
echo "FAIL: /dev/dri device nodes visible without --gpu: $out"
return 1
fi
}
test_gpu_vram_zero_rejected() {
# Validation in src/data/resources.rs rejects gpu_vram_mib == 0 before
# any VM starts, so --net is not required but included for clarity.
local out exit_code=0
out=$("$SMOLVM" machine run --gpu --gpu-vram 0 --net --image alpine:latest -- true 2>&1) \
|| exit_code=$?
[[ $exit_code -ne 0 ]] || { echo "FAIL: --gpu-vram 0 was accepted (exit 0)"; return 1; }
echo " (correctly rejected with exit $exit_code)"
}
test_named_machine_gpu_persists() {
# Create a named machine with --gpu, stop+start it, verify DRI still present.
# This exercises the full DB round-trip: VmResources{gpu:true} → sqlite →
# deserialise → launcher → krun_set_gpu_options2.
local name="gpu-named-$$"
"$SMOLVM" machine stop --name "$name" 2>/dev/null || true
"$SMOLVM" machine delete --name "$name" -f 2>/dev/null || true
"$SMOLVM" machine create --name "$name" --gpu --net --image alpine:latest 2>&1 || return 1
"$SMOLVM" machine start --name "$name" 2>&1 || {
"$SMOLVM" machine delete --name "$name" -f 2>/dev/null; return 1
}
local out rc=0
out=$("$SMOLVM" machine exec --name "$name" -- ls /dev/dri/renderD128 2>&1) || rc=$?
"$SMOLVM" machine stop --name "$name" 2>/dev/null || true
"$SMOLVM" machine delete --name "$name" -f 2>/dev/null || true
[[ $rc -eq 0 ]] && [[ "$out" == *"renderD128"* ]] || {
echo "FAIL: renderD128 absent in named GPU machine (got: $out, exit $rc)"
return 1
}
}
run_test "GPU: /dev/dri/renderD128 present with --gpu" test_dri_renderD128_present || true
run_test "GPU: /dev/dri/card0 present with --gpu" test_dri_card0_present || true
run_test "GPU: renderD128 accessible (stat succeeds)" test_renderD128_world_readable || true
run_test "GPU: interactive run exposes /dev/dri" test_interactive_run_has_dri || true
run_test "GPU: no /dev/dri without --gpu (isolation)" test_no_dri_without_gpu || true
run_test "GPU: --gpu-vram 0 rejected by validation" test_gpu_vram_zero_rejected || true
run_test "GPU: named machine DB persistence of --gpu flag" test_named_machine_gpu_persists || true
# =============================================================================
# Section 1b: pack create --gpu (manifest embedding)
# =============================================================================
# pack create --gpu must write gpu=true into the .smolmachine manifest so the
# packed binary boots with a virtio-gpu device. We verify the round-trip:
# pack create --gpu → .smolmachine → pack run → /dev/dri/renderD128 visible.
echo ""
echo "Running pack create --gpu tests..."
test_pack_create_gpu_manifest() {
local tmp_dir
tmp_dir=$(mktemp -d)
local out_path="$tmp_dir/gpu-alpine"
echo " Packing alpine:latest with --gpu..."
run_with_timeout 300 "$SMOLVM" pack create \
--image alpine:latest --gpu \
--output "$out_path" 2>&1 || {
rm -rf "$tmp_dir"
return 1
}
[[ -f "$out_path.smolmachine" ]] || {
echo "FAIL: no .smolmachine produced"
rm -rf "$tmp_dir"
return 1
}
# Run the packed binary and verify /dev/dri/renderD128 is present.
# pack run reads manifest.gpu and passes it to VmResources.
echo " Running packed binary, checking for /dev/dri/renderD128..."
local run_out rc=0
run_out=$(run_with_timeout 120 "$SMOLVM" pack run \
--sidecar "$out_path.smolmachine" -- \
ls /dev/dri/renderD128 2>&1) || rc=$?
rm -rf "$tmp_dir"
[[ $rc -eq 0 ]] && [[ "$run_out" == *"renderD128"* ]] || {
echo "FAIL: renderD128 not present in GPU-packed binary (exit $rc, got: $run_out)"
return 1
}
}
test_pack_create_no_gpu_manifest() {
# Without --gpu, the packed binary must NOT have /dev/dri.
local tmp_dir
tmp_dir=$(mktemp -d)
local out_path="$tmp_dir/nogpu-alpine"
run_with_timeout 300 "$SMOLVM" pack create \
--image alpine:latest \
--output "$out_path" 2>&1 || {
rm -rf "$tmp_dir"
return 1
}
local run_out exit_code=0
run_out=$(run_with_timeout 120 "$SMOLVM" pack run \
--sidecar "$out_path.smolmachine" -- \
ls /dev/dri 2>&1) || exit_code=$?
rm -rf "$tmp_dir"
if [[ $exit_code -eq 0 ]] && echo "$run_out" | grep -qE "render|card"; then
echo "FAIL: /dev/dri present in non-GPU packed binary: $run_out"
return 1
fi
}
run_test "GPU: pack create --gpu embeds gpu=true in manifest" test_pack_create_gpu_manifest || true
run_test "GPU: pack create without --gpu has no /dev/dri (isolation)" test_pack_create_no_gpu_manifest || true
# =============================================================================
# Section 2: Vulkan workloads (Fedora 42 + patched Mesa)
# =============================================================================
# Standard Fedora Mesa has a 16KB page-alignment bug that crashes Venus ICD
# initialisation on Apple Silicon (host pages are 16KB; guest expects 4KB).
# The slp/mesa-libkrun-vulkan COPR carries the upstream patch.
#
# We create one shared Fedora 42 machine and install Mesa once, then run all
# Vulkan assertions against the same running container to avoid paying the
# dnf install cost (~60s) for each individual test.
#
# Key commands:
# vulkaninfo --summary → lists ICDs, device names, API versions
# stat /dev/dri/renderD128 → confirms DRI forwarding inside Fedora container
echo ""
echo "Running Vulkan workload tests (Fedora 42 + patched Mesa)..."
test_fedora_gpu_setup() {
"$SMOLVM" machine stop --name "$GPU_FEDORA_MACHINE" 2>/dev/null || true
"$SMOLVM" machine delete --name "$GPU_FEDORA_MACHINE" -f 2>/dev/null || true
echo " Creating Fedora 42 GPU machine (this pulls ~600 MB on first run)..."
"$SMOLVM" machine create --name "$GPU_FEDORA_MACHINE" \
--image fedora:42 --gpu --net 2>&1 || return 1
echo " Starting machine..."
"$SMOLVM" machine start --name "$GPU_FEDORA_MACHINE" 2>&1 || {
"$SMOLVM" machine delete --name "$GPU_FEDORA_MACHINE" -f 2>/dev/null
return 1
}
echo " Enabling slp/mesa-libkrun-vulkan COPR (Apple Silicon 16KB page-alignment fix)..."
run_with_timeout 120 "$SMOLVM" machine exec --name "$GPU_FEDORA_MACHINE" -- \
dnf copr enable -y slp/mesa-libkrun-vulkan 2>&1 || {
echo "FAIL: dnf copr enable failed"
return 1
}
echo " Installing mesa-vulkan-drivers + vulkan-tools (~60s)..."
run_with_timeout 300 "$SMOLVM" machine exec --name "$GPU_FEDORA_MACHINE" -- \
dnf install -y --allowerasing mesa-vulkan-drivers vulkan-tools 2>&1 || {
echo "FAIL: dnf install failed"
return 1
}
echo " Fedora GPU machine ready."
}
test_vulkaninfo_venus_icd() {
# Venus is the guest-side Vulkan-over-virtio-gpu driver in Mesa.
# It should appear as an ICD in vulkaninfo --summary output.
_fedora_running || { echo "SKIP: Fedora GPU machine not running (setup failed)"; return 1; }
local out
out=$("$SMOLVM" machine exec --name "$GPU_FEDORA_MACHINE" -- \
vulkaninfo --summary 2>&1) || { echo "FAIL: vulkaninfo exited non-zero"; echo "$out"; return 1; }
echo "$out" | grep -qi "Venus" || {
echo "FAIL: 'Venus' not found in vulkaninfo --summary"
echo "$out" | head -30 | sed 's/^/ /'
return 1
}
}
test_vulkaninfo_virtio_gpu_device() {
# The device name seen by the guest reflects the libkrun virtio-gpu backend.
_fedora_running || { echo "SKIP: Fedora GPU machine not running (setup failed)"; return 1; }
local out
out=$("$SMOLVM" machine exec --name "$GPU_FEDORA_MACHINE" -- \
vulkaninfo --summary 2>&1) || { echo "FAIL: vulkaninfo exited non-zero"; echo "$out"; return 1; }
echo "$out" | grep -qi "Virtio-GPU" || {
echo "FAIL: 'Virtio-GPU' not found in vulkaninfo --summary"
echo "$out" | head -30 | sed 's/^/ /'
return 1
}
}
test_vulkan_api_version() {
# Venus exposes Vulkan 1.2+ through the virtio-gpu transport.
_fedora_running || { echo "SKIP: Fedora GPU machine not running (setup failed)"; return 1; }
local out
out=$("$SMOLVM" machine exec --name "$GPU_FEDORA_MACHINE" -- \
vulkaninfo --summary 2>&1) || { echo "FAIL: vulkaninfo exited non-zero"; echo "$out"; return 1; }
echo "$out" | grep -qiE "apiVersion|Vulkan [0-9]+\." || {
echo "FAIL: No Vulkan API version in vulkaninfo --summary"
echo "$out" | head -30 | sed 's/^/ /'
return 1
}
}
test_render_node_in_fedora_container() {
# Confirms that add_gpu_devices_if_available() forwards /dev/dri into the
# Fedora OCI container, not just Alpine. Different base images, same result.
_fedora_running || { echo "SKIP: Fedora GPU machine not running (setup failed)"; return 1; }
local out rc=0
out=$("$SMOLVM" machine exec --name "$GPU_FEDORA_MACHINE" -- \
stat /dev/dri/renderD128 2>&1) || rc=$?
[[ $rc -eq 0 ]] && [[ "$out" == *"renderD128"* ]] || {
echo "FAIL: /dev/dri/renderD128 inaccessible in Fedora container (exit $rc, got: $out)"
return 1
}
}
test_fedora_gpu_cleanup() {
"$SMOLVM" machine stop --name "$GPU_FEDORA_MACHINE" 2>&1 || true
"$SMOLVM" machine delete --name "$GPU_FEDORA_MACHINE" -f 2>/dev/null || true
}
run_test "GPU: Fedora 42 + patched Mesa setup" test_fedora_gpu_setup || true
run_test "GPU: vulkaninfo reports Venus ICD" test_vulkaninfo_venus_icd || true
run_test "GPU: vulkaninfo reports Virtio-GPU device name" test_vulkaninfo_virtio_gpu_device || true
run_test "GPU: Vulkan API version reported (1.x)" test_vulkan_api_version || true
run_test "GPU: /dev/dri/renderD128 accessible in Fedora container" test_render_node_in_fedora_container || true
run_test "GPU: Fedora cleanup" test_fedora_gpu_cleanup || true
print_summary "GPU Tests"