Skip to content

hyper1 iGPU for Plex and Tdarr

The hyper1 integrated GPU is passed through to genesis-worker-01 (VM 134). Plex and Tdarr use it for Intel Quick Sync hardware transcoding through the Intel GPU device plugin.

Every command is a single line, because heredocs and backslash continuations get mangled when pasted into a tmux session.

Facts

Fact Value
Device 00:02.0 Intel RocketLake-S GT1 [UHD Graphics 730], 8086:4c8b
Subsystem Lenovo 17aa:31a7
Host CPU i5-11400T, 11th gen. hyper2 and hyper3 are 8th gen
Quick Sync H.264, HEVC 8/10-bit, VP9, AV1 decode. hyper1 is the only host with AV1
IOMMU Enabled by default, no kernel cmdline change needed
IOMMU group Group 0, the GPU is the only device in it
SR-IOV Not advertised
Render node renderD128, mode crw-rw-rw-, so no privileged container or extra groups needed

Constraints

Rocket Lake is Gen12. Intel dropped GVT-g after Gen11 and the device does not advertise SR-IOV, so the GPU cannot be shared between VMs. It is full VFIO passthrough to exactly one VM, and hyper1 gives it up entirely. It goes to a worker rather than genesis-ctrl-01, which also lives on hyper1, so the control plane has no dependency on one physical machine.

Passthrough pins the guest's entire RAM. hyper1 has 31 GiB; genesis-worker-01 (16 GiB) and genesis-ctrl-01 (8 GiB) leave the host a thin margin, so raising either VM's memory_mb takes it from the host.

The host reboot must come before the Terraform attach. Adding hostpci0 is not a staged config edit: the provider stops the VM, writes the config and starts it, and that start fails unless the device is already bound to vfio-pci.

Rebuild hyper1

Host preparation

On hyper1. Do not reboot yet.

echo 'options vfio-pci ids=8086:4c8b disable_vga=1' > /etc/modprobe.d/vfio.conf
echo 'blacklist i915' > /etc/modprobe.d/blacklist-i915.conf
printf 'vfio\nvfio_iommu_type1\nvfio_pci\n' >> /etc/modules
update-initramfs -u -k all

Read that output. It lists every installed kernel, and GRUB boots the newest by default on a machine with no IPMI.

PCI mapping

The igpu-hyper1 mapping is declared in terraform/proxmox/hyper-cluster/datacenter/hardware.tf and applied from that stack. Its name must match pci_mapping in the Talos terraform.tfvars. A mapping is used rather than a raw PCI id because the provider's hostpci.id does not work with API token auth.

Both iommu_group and subsystem_id are required. Proxmox accepts a mapping without them, then rejects it at VM start with missing expected property, one property per failed start, and the VM does not boot. The values come from ls /sys/kernel/iommu_groups/0/devices/ and the Subsystem: line of lspci -nnk -s 00:02.0. Check what Proxmox holds:

pvesh get /cluster/mapping/pci --output-format json

Drain and reboot

kubectl drain genesis-worker-01 --ignore-daemonsets --delete-emptydir-data

Then reboot on hyper1. That takes down genesis-ctrl-01 and genesis-worker-01 together; etcd keeps quorum on the other two control planes. Plex and Tdarr stay down until the node is back, since only that node offers the GPU.

Verify the host

On hyper1, lspci -nnk -s 00:02.0 must show Kernel driver in use: vfio-pci, and ls /dev/dri must fail with "No such file or directory".

Terraform attach

proxmox-vms.tf in terraform/proxmox/hyper-cluster/k8s/talos has a dynamic "hostpci" block that only materialises for nodes that set pci_mapping, and terraform.tfvars sets pci_mapping = "igpu-hyper1" on genesis-worker-01. It uses pcie = true, which needs the q35 machine type these VMs already have.

The plan must say updated in-place and 0 to destroy; a replacement wipes Talos and etcd on that node. The apply stops and starts VM 134. Then check the GPU reached the guest, expecting an Intel RocketLake-S GT1 entry:

talosctl -e 10.3.10.30 -n 10.3.10.34 get pcidevices

If the VM will not start

The Proxmox start button keeps failing while an unusable hostpci0 is in the config. Detach it so the node boots, then read the task viewer for the real cause:

qm set 134 --delete hostpci0 && qm start 134 && qm status 134

Talos: the i915 extension

/dev/dri does not exist inside Talos without siderolabs/i915. It is in the fleet-wide talos_image_factory_schematic.this in talos-cluster.tf and harmless on nodes without a GPU. Changing the extension list changes the schematic ID, which rewrites machine.install.image on all six nodes.

Applying a schematic change hits a provider bug: Terraform plans the machine config with the old schematic ID, computes a new one during apply, and aborts with Provider produced inconsistent final plan. The schematic resource is updated anyway, so plan and apply a second time.

Then reinstall the GPU node on the new image. Same Talos version, different image, so only that node reboots. Take the image from local.talos_installer_image in upgrade-talos.tf, which is factory.talos.dev/installer/<schematic id>:<talos_version>:

talosctl -e 10.3.10.30 -n 10.3.10.34 upgrade --image <installer image> --preserve --wait
talosctl -e 10.3.10.30 -n 10.3.10.34 ls /dev/dri

Expect card0 and renderD128. Never install a one-off image without changing the Terraform schematic; the fleet-wide install.image pin reverts it at the next install.

Kubernetes

Workloads reach the iGPU through the Intel GPU device plugin in k8s/talos/infra/intel-gpu-plugin/, not a hostPath mount. The DaemonSet runs on the GPU node, advertises gpu.intel.com/i915, and injects /dev/dri into any container that requests it. It runs with -shared-dev-num=2 so Plex and Tdarr can each claim the device.

Talos enforces PodSecurity baseline cluster-wide, which forbids hostPath. Only intel-gpu-plugin, a namespace holding nothing but the DaemonSet, runs enforce: privileged, with audit and warn left at baseline. Do not raise plex-media-stack or arr-stack to privileged to mount /dev/dri: they hold other apps that need none of it, seerr among them, which answers from the internet. A pod rejected at admission shows in the ReplicaSet events, not the Deployment, and kubectl diff stays clean.

daemonset.yaml is vendored from upstream deployments/gpu_plugin/base; re-sync it when Renovate moves the image tag. The local change is the nodeSelector, which adds the GPU label to upstream's kubernetes.io/arch: amd64 so the plugin does not hostPath-create /dev/dri on the five nodes without a GPU.

Node selection

Selection is by capability label, not hostname. talos-cluster.tf sets hardware.nordbye.it/gpu=intel-quicksync on any node with a pci_mapping. The plugin selects on that label and so does Plex. The gpu.intel.com/i915 request alone is enough to place a pod, since only the plugin's node advertises it; that is how Tdarr lands there.

kubectl get nodes -L hardware.nordbye.it/gpu
kubectl get node genesis-worker-01 -o jsonpath='{.status.allocatable.gpu\.intel\.com/i915}'

When changing the GPU resource, ship the plugin first and the workloads after. Plex and Tdarr are strategy: Recreate, so switching them to a resource the node does not advertise yet leaves them Pending with the old pod already gone.

Hardware transcoding in Plex

Settings, Transcoder, "Use hardware acceleration when available" needs Plex Pass. It is stored as HardwareAcceleratedCodecs in Preferences.xml on the config PVC, not in this repo, so a rebuild from git does not restore it. Confirm a transcode actually uses the GPU:

kubectl exec -n plex-media-stack deploy/plex -- sh -c 'grep "TPU: hardware transcoding" "/config/Library/Application Support/Plex Media Server/Logs/Plex Media Server.log" | tail -3'

Both ends must say final decoder: vaapi, final encoder: vaapi. Empty values mean it fell back to software. Tdarr's GPU worker settings are in ../../apps/media-stack/README.md.

Rollback

Remove pci_mapping from the Talos terraform.tfvars and apply, then on hyper1:

rm -f /etc/modprobe.d/blacklist-i915.conf /etc/modprobe.d/vfio.conf && update-initramfs -u -k all && reboot

The host reclaims the GPU through i915. The Talos schematic can stay as it is. Plex and Tdarr must drop their gpu.intel.com/i915 requests first, or they stay Pending.

Open items

Tracked in docs/backlog/README.md. A one-off QEMU internal-error on VM 134 is recorded in ../observability/incidents.md.