On-Demand GPU Workload Offloading (Distributed Tdarr)
Tdarr in the CineVault stack normally encodes on the Intel N100's integrated QuickSync (QSV) chip — see Tdarr Transcoding for the core setup. This page documents the on-demand offload topology: a transient laptop equipped with an AMD Radeon RX 7600M XT joins the Tdarr cluster as a remote GPU node whenever the operator wants extra encode capacity, then detaches cleanly when the work is done.
!!! warning "Distinct plugin stack required"
The remote node uses an AMD VAAPI plugin stack. It must never run
the host's QSV stack (Boosh-Transcode using QSV GPU & FFMPEG) — QSV
targets Intel hardware and will fail (or silently fall back to software
encoding) on the Radeon GPU.
1. Topology
┌─────────────────── The Host: Intel N100 (Debian 13) ───────────────────┐
│ │
│ tdarr (server + internal QSV node) nfs-kernel-server │
│ ├── :8265 Web UI └── exports /opt/mediastack/data │
│ └── :8266 Control plane ←───────────────────────────┐ │
│ │ │
└────────────────────────────────────────────────────────┼────────────────┘
│ NFSv4.2 (2049) + TCP 8266
restricted to the auto-detected LAN subnet (UFW)
│
┌────────────────────── Transient AMD Laptop Node ───────┼────────────────┐
│ │ │
│ /mnt/n100_data ← NFS mount of The Library │ │
│ tdarr-node container (Radeon RX 7600M XT passthrough) ┘ │
└─────────────────────────────────────────────────────────────────────────┘
Three components carry the offload traffic, all of which Ansible configures automatically on The Host (see Bare-Metal Provisioning):
| Component | Port / Path | Restriction |
|---|---|---|
| NFS export | :2049 (tcp+udp) |
LAN subnet only (auto-detected), all_squash → anonuid=5000,anongid=5000 |
| Tdarr control plane | :8266 (tcp) |
LAN subnet only (auto-detected) |
| NFS payload | /opt/mediastack/data |
rw,sync,no_subtree_check,all_squash,anonuid=5000,anongid=5000,fsid=1 |
Zero-trust UID/GID parity: every NFS client is squashed to the
mediasvc identity (UID/GID 5000), so files written by the laptop node land
in The Library with the same ownership as every other container on The Host.
This is what makes instant moves and hardlinks work across the whole stack —
and why the node container runs with PUID=5000 / PGID=5000.
2. Host-Side Prerequisites
Everything below is applied by the provision_host.yml playbook — no manual
host configuration is required:
- NFS server —
nfs-kernel-serverinstalled, enabled and started. - Export —
/etc/exportscontains:
text
/opt/mediastack/data <auto-detected-subnet>(rw,sync,no_subtree_check,all_squash,anonuid=5000,anongid=5000,fsid=1)
The subnet is derived from The Host's own network facts at play time; no IP is hardcoded in the playbook.
- Firewall — UFW allows
2049/tcp,2049/udpand8266/tcpfrom the auto-detected LAN subnet only. The default deny-inbound policy remains untouched.
Re-run the playbook if any of these are missing:
ansible-playbook -i ansible/inventory/hosts.ini ansible/playbooks/provision_host.yml -K
3. Laptop Execution
!!! note "Prerequisites on the laptop"
Docker Engine and an NFS client are required:
sudo apt install nfs-common (Debian/Ubuntu) or
sudo dnf install nfs-utils (Fedora). Root/sudo access is mandatory.
Start the node
sudo scripts/laptop-node/run-tdarr-node.sh
The script is fully self-contained and idempotent:
- Resolves
mediacenter.localvia mDNS and mounts<resolved-ip>:/opt/mediastack/datato/mnt/n100_data(NFSv4.2 — only port 2049 is used, no rpcbind/mountd ports). - Creates a laptop-local scratch cache
/var/tmp/tdarr-node-cache(transcode intermediates never traverse the network). - Launches
ghcr.io/haveagitgat/tdarr_node:latestwith AMD GPU passthrough (--device /dev/dri,--device /dev/kfd,--group-add video,--group-add render),PUID/PGID=5000, and the Tdarr server connection parameters (serverIP,serverPort=8266,nodeID=AMD-Laptop-Node).
If --server-ip is omitted the script resolves mediacenter.local via mDNS
(thanks to avahi-daemon on The Host). There is no stale-IP fallback:
if resolution fails — e.g. a router isolating Wi-Fi multicast traffic — the
script exits with an actionable error and asks for an explicit
--server-ip <THE_HOST_LAN_IP>.
Stop / tear down
sudo scripts/laptop-node/run-tdarr-node.sh --stop
Stops and removes the container (30-second graceful timeout), unmounts the NFS share and cleans the scratch cache. Idempotent — safe to run when nothing is running.
4. Tdarr UI Configuration (Path Translators & Workers)
Perform these steps once in the Tdarr Web UI (http://<host-ip>:8265)
while the laptop node is running.
4.1 Node worker allocation
- Open the Nodes tab.
- Locate the
AMD-Laptop-Nodecard (it registers automatically). - Allocate 1 GPU worker and 1 CPU worker — the identical requirement documented for the internal node in Tdarr Transcoding. The AMD Radeon has one encode engine, so a single GPU worker is correct.
4.2 AMD VAAPI plugin stack
Create a node-specific plugin stack for the laptop node (replacing the QSV encoder with the VAAPI variant):
1. Migz-Remove image formats from video
2. Migz-Clean audio streams
3. Boosh-Transcode using VAAPI GPU & FFMPEG (default settings)
!!! danger "Do not reuse the QSV stack"
The host's Boosh-Transcode using QSV GPU & FFMPEG plugin targets Intel
hardware only. On the AMD node it will fall back to CPU encoding or fail
entirely. Keep the two stacks separate: QSV for the internal node, VAAPI
for AMD-Laptop-Node.
4.3 Path Translators
The node container mounts the laptop's /mnt/n100_data as /data
inside the container, so Tdarr's node-internal paths are identical to the
server paths — the translator is a one-to-one mapping:
| Server Path | Node Path | Purpose |
|---|---|---|
/data |
/data |
The Library (media tree) via the NFS mount |
/temp |
/temp |
Transcode scratch (laptop-local, not network) |
Configure these under Libraries → Transcode Options → Path Translators (or the node's settings) so the node resolves server-side file locations to its own mount namespace correctly.
5. Verification
- Node registration —
AMD-Laptop-Nodeappears in the Nodes tab and reports healthy (green). - Worker type — on the Tdarr dashboard, the laptop node's transcode
stage shows Transcode GPU with
Boosh-Transcode using VAAPI GPU & FFMPEG. - FFmpeg command — expanding the worker reveals VAAPI flags, e.g.:
text
-hwaccel vaapi -vaapi_device /dev/dri/renderD128 ... -c:v hevc_vaapi
- GPU utilisation on the laptop — while a job runs:
bash
radeontop
The Radeon RX 7600M XT should show sustained utilisation. Flat-zero
utilisation means the container is not reaching /dev/dri (group mapping
issue) or the job silently fell back to CPU encoding.
6. Troubleshooting
| Symptom | Likely cause | Resolution |
|---|---|---|
| Node never registers in the UI | TCP 8266 blocked | Check UFW on The Host: sudo ufw status numbered — the rule must scope the LAN subnet, not Anywhere. |
| NFS mount hangs or dies mid-session | Network stall | The script already mounts soft,timeo=30,retrans=3; avoid moving the laptop between APs mid-job. |
"Permission denied" writing to /data |
Squash mapping wrong | Verify /etc/exports contains all_squash,anonuid=5000,anongid=5000. |
| GPU node starts, jobs use CPU | Group/devices not passed | Confirm the container was launched by the script (it includes --device /dev/dri, --device /dev/kfd, --group-add render). |
mount.nfs: not found |
Missing NFS client | sudo apt install nfs-common (or nfs-utils on Fedora). |
Unmount fails on --stop |
Mount still busy | Stop any process using /mnt/n100_data, then re-run --stop. |