diff --git a/Plugin/unraid/Partnership/containers.sh b/Plugin/unraid/Partnership/containers.sh
index 4f9ee5e..1da07d4 100755
--- a/Plugin/unraid/Partnership/containers.sh
+++ b/Plugin/unraid/Partnership/containers.sh
@@ -18,6 +18,112 @@ TEMPLATES_DIR="/boot/config/plugins/dockerMan/templates-user"
_STACK_DEPLOYED=0
_STACK_FAILED=0
+_REMOTE_GPU_TYPE="" # cached after first detection
+
+# ==============================================================================================
+# ── Detect GPU type on a remote host ─────────────────────────────────────────────────────────
+#
+# Returns one of: nvidia | intel | amd | dri | none
+# nvidia — /dev/nvidia0 present (NVIDIA driver loaded)
+# intel — /dev/dri present, vendor 0x8086
+# amd — /dev/dri present, vendor 0x1002 (also exposes /dev/kfd)
+# dri — /dev/dri present but vendor unreadable
+# none — no GPU device found
+#
+# Result is cached in _REMOTE_GPU_TYPE for the session — SSH'd once per onboard run.
+# ==============================================================================================
+detect_remote_gpu() {
+ local remote_ip="$1" ssh_key="$2"
+
+ if [[ -n "$_REMOTE_GPU_TYPE" ]]; then
+ echo "$_REMOTE_GPU_TYPE"
+ return 0
+ fi
+
+ local result
+ result=$(timeout 10 ssh -i "$ssh_key" \
+ -o ConnectTimeout=10 -o BatchMode=yes root@"$remote_ip" '
+ if [ -c /dev/nvidia0 ]; then
+ echo nvidia
+ elif [ -d /dev/dri ]; then
+ vendor=""
+ for f in /sys/class/drm/card*/device/vendor; do
+ [ -f "$f" ] && { vendor=$(cat "$f" 2>/dev/null); break; }
+ done
+ case "$vendor" in
+ 0x8086) echo intel ;;
+ 0x1002) echo amd ;;
+ *) echo dri ;;
+ esac
+ else
+ echo none
+ fi
+ ' 2>/dev/null)
+
+ _REMOTE_GPU_TYPE="${result:-none}"
+ echo "$_REMOTE_GPU_TYPE"
+}
+
+# ==============================================================================================
+# ── Rewrite GPU config in an XML for a target GPU type ───────────────────────────────────────
+#
+# Called when deploying to a remote whose GPU differs from the owner's. Takes the owner's
+# XML (NVIDIA-configured) and rewrites it for the remote's hardware without modifying the
+# original on disk.
+#
+# Returns the path to a temp file — caller must clean it up.
+# Returns the original path unchanged if the XML has no NVIDIA markers (not GPU-aware).
+#
+# Transforms applied:
+# nvidia → nvidia: replace UUID with "all" so any NVIDIA GPU is accepted
+# nvidia → intel/amd/dri: strip --runtime=nvidia + NVIDIA_VISIBLE_DEVICES,
+# inject /dev/dri Device Config (+ /dev/kfd for AMD)
+# nvidia → none: strip --runtime=nvidia + NVIDIA_VISIBLE_DEVICES, no device added
+# ==============================================================================================
+transform_xml_for_gpu() {
+ local src_xml="$1" gpu_type="$2"
+
+ # Only transform GPU-aware XMLs (containers with NVIDIA config)
+ if ! grep -qE 'runtime=nvidia|NVIDIA_VISIBLE_DEVICES' "$src_xml" 2>/dev/null; then
+ echo "$src_xml"
+ return 0
+ fi
+
+ local tmp_xml
+ tmp_xml=$(mktemp /tmp/vv_xml_gpu_XXXXXX.xml)
+
+ case "$gpu_type" in
+ nvidia)
+ # Same vendor — normalise UUID to "all" so any NVIDIA card is accepted
+ sed 's/\(Target="NVIDIA_VISIBLE_DEVICES"[^>]*>\)[^<]*/\1all/' "$src_xml" > "$tmp_xml"
+ ;;
+ intel|dri)
+ # Strip NVIDIA params, add /dev/dri device
+ sed \
+ -e 's/--runtime=nvidia[[:space:]]*//' \
+ -e '/Target="NVIDIA_VISIBLE_DEVICES"/d' \
+ "$src_xml" > "$tmp_xml"
+ sed -i 's|| /dev/dri\n|' "$tmp_xml"
+ ;;
+ amd)
+ # AMD needs /dev/dri for VA-API and /dev/kfd for ROCm/OpenCL
+ sed \
+ -e 's/--runtime=nvidia[[:space:]]*//' \
+ -e '/Target="NVIDIA_VISIBLE_DEVICES"/d' \
+ "$src_xml" > "$tmp_xml"
+ sed -i 's|| /dev/dri\n /dev/kfd\n|' "$tmp_xml"
+ ;;
+ none)
+ # No GPU — strip all GPU params, no device added
+ sed \
+ -e 's/--runtime=nvidia[[:space:]]*//' \
+ -e '/Target="NVIDIA_VISIBLE_DEVICES"/d' \
+ "$src_xml" > "$tmp_xml"
+ ;;
+ esac
+
+ echo "$tmp_xml"
+}
# ==============================================================================================
# ── Wait for a container on the remote to be healthy/running ─────────────────────────────────
@@ -69,6 +175,14 @@ deploy_container_from_xml() {
local xml_name
xml_name=$(basename "$xml_file")
+ # GPU transform — rewrite GPU params for the remote's hardware before parsing or SCP.
+ # detect_remote_gpu is cached after the first SSH call.
+ local _gpu_type _transformed_xml _gpu_tmp=""
+ _gpu_type=$(detect_remote_gpu "$remote_ip" "$ssh_key")
+ _transformed_xml=$(transform_xml_for_gpu "$xml_file" "$_gpu_type")
+ [[ "$_transformed_xml" != "$xml_file" ]] && _gpu_tmp="$_transformed_xml"
+ xml_file="$_transformed_xml"
+
local name repo network extra privileged
name=$( awk 'match($0,/([^<]+)<\/Name>/, a){print a[1];exit}' "$xml_file")
repo=$( awk 'match($0,/([^<]+)<\/Repository>/,a){print a[1];exit}' "$xml_file")
@@ -77,15 +191,18 @@ deploy_container_from_xml() {
privileged=$( awk 'match($0,/([^<]+)<\/Privileged>/,a){print a[1];exit}' "$xml_file")
if [[ -z "$name" || -z "$repo" ]]; then
+ rm -f "$_gpu_tmp"
warn " Cannot parse Name/Repository from $xml_name — skipping"
return 1
fi
+ [[ -n "$_gpu_tmp" ]] && log " GPU: ${_gpu_type} (rewritten from owner NVIDIA config)"
log "Deploying $name..."
if [[ "$DRY_RUN" == false ]]; then
timeout "$SSH_TIMEOUT" scp -i "$ssh_key" -o ConnectTimeout="$SSH_TIMEOUT" \
"$xml_file" "root@${remote_ip}:${TEMPLATES_DIR}/${xml_name}" 2>/dev/null || {
+ rm -f "$_gpu_tmp"
warn " SCP failed for $xml_name — skipping $name"
return 1
}
@@ -148,7 +265,7 @@ deploy_container_from_xml() {
if [[ "$DRY_RUN" == true ]]; then
warn " DRY RUN — would deploy $name on $MIRROR"
- rm -f "$tmp_script"
+ rm -f "$tmp_script" "$_gpu_tmp"
return 0
fi
@@ -160,11 +277,11 @@ deploy_container_from_xml() {
"bash '$remote_script' 2>&1; rc=\$?; rm -f '$remote_script'; exit \$rc" 2>/dev/null | \
grep -q "deployed:${name}"; then
echo " $name deployed ✅"
- rm -f "$tmp_script"
+ rm -f "$tmp_script" "$_gpu_tmp"
return 0
else
warn " $name deployment failed — check $MIRROR manually"
- rm -f "$tmp_script"
+ rm -f "$tmp_script" "$_gpu_tmp"
return 1
fi
}