This week I had to answer a deceptively simple question: should I send my app's audio-to-video encoding through a Tesla P40 GPU over the network, or just keep it on the CPU?
The app in question is naptune.xyz — a side project where people upload a song and an image and get back a video (think: a 1-fps slideshow with the song as audio). It's built on a tiny LXC on one of my Proxmox nodes. The encode is done with ffmpeg's libx264 at ultrafast preset, and it's fast enough — a 3-minute video encodes in about 5 seconds.
But I have a Tesla P40 sitting idle. The P40 is famous in the homelab community for running LLMs (I wrote about that before). What's less talked about is that it also has an NVENC encoder — hardware H.264 that doesn't touch the CPU at all. So the obvious idea: send the encode job to the P40 over the network and let it chew through it.
The problem: I had a feeling this was over-engineering. And my instincts were right — but only after I built the whole benchmark to prove it.
First, a quick detour: Intel iGPU passthrough to LXC
Before I could benchmark anything fairly, I needed a second hardware-encode candidate: my pve05 node has an Intel i7-13700T with a UHD 770 iGPU. Intel Quick Sync (QSV) is the other mainstream hardware encoder — it's built into every modern Intel CPU, uses almost no power, and supports both H.264 and HEVC.
The catch: my naptune-prod LXC doesn't have access to the iGPU at all. Proxmox LXCs don't pass GPUs through by default. So I built a fresh LXC on pve05 (VMID 505, "qsvbench") and passed /dev/dri into it.
The config lines that matter — in /etc/pve/lxc/505.conf:
features: nesting=1,keyctl=1
lxc.cgroup2.devices.allow: c 226:* rwm
lxc.cgroup2.devices.allow: c 29:* rwm
lxc.mount.entry: /dev/dri dev/dri none bind,optional,create=dir 0 0
lxc.mount.entry: /dev/fb0 dev/fb0 none bind,optional,create=file 0 0
lxc.apparmor.profile: unconfined
A few things I learned the hard way:
features: nesting=1,keyctl=1— needed because I'm running Docker inside the LXC. Without it, Docker fails to start.lxc.cgroup2.devices.allow: c 226:* rwm— device 226 is the DRM (Direct Rendering Manager) major. Without this cgroup rule, the container can't actually open the device even though it's bind-mounted.c 29:* rwmis for the framebuffer (/dev/fb0), which I also bound in.lxc.apparmor.profile: unconfined— the AppArmor confinement blocks GPU access in some setups. Setting it to unconfined is the pragmatic homelab answer.- Bind mounts are
bind,optional,create=dirso they don't crash the container boot if the device is absent.
After pct reboot 505, inside the container:
ls /dev/dri
Output: card0 renderD128 — the render node is there.
And vainfo (from intel-media-va-driver + i965-va-driver) showed the H.264 High Profile encoder available on renderD128.
For the P40 side, the config is the same idea but with NVIDIA's devices (/dev/nvidia0, /dev/nvidiactl, /dev/nvidia-uvm, plus c 195:* rwm, c 235:* rwm, c 238:* rwm cgroup rules). The P40 needed the NVIDIA userspace libraries copied into the container — libcuda.so.1, libnvidia-encode.so.1, libnvcuvid.so.1 — and the driver version must match the host's.
The benchmark itself
I replicated naptune's exact production ffmpeg command — image2 loop at 1 fps, scale-to-even, matroska output, AAC 128k audio — and ran it three ways:
- CPU —
libx264,ultrafast,crf 23(naptune's current path) - P40 NVENC —
h264_nvenc, presetp4,cq 23 - Intel UHD 770 VAAPI —
h264_vaapi,qp 23(VAAPI is the API; QSV is the marketing name — on Raptor Lake they share the same media engine)
Corpus: 15 images (3 colors × 720p/1080p/4K) × 27 audio files (3 × 3 resolutions × 30/90/180s). 108 runs total (54 on each benchmark LXC: CPU + hardware).
The results (mean wall-clock, seconds)
On the pve05 LXC (i7-13700T, UHD 770):
| res / dur | CPU (libx264) | UHD 770 VAAPI |
|---|---|---|
| 720p / 30s | 0.32 | 0.33 |
| 1080p / 90s | 0.87 | 0.94 |
| 4K / 180s | 2.70 | 3.21 |
| mean | 1.16 | 1.27 |
On the pve06 LXC (P40 + CPU):
| res / dur | CPU (libx264) | P40 NVENC |
|---|---|---|
| 720p / 30s | 0.83 | 1.10 |
| 1080p / 90s | 3.55 | 3.12 |
| 4K / 180s | 24.85 | 16.11 |
| mean | 6.71 | 5.08 |
Wait — why is pve06's CPU so slow? Because pve06 is a different machine with a much weaker CPU. On pve05 (which matches naptune-prod's i5-13500T architecture), the CPU crushes everything: a 4K/180s video encodes in 2.7 seconds on CPU, while VAAPI took 3.2s.
The verdict
Hardware encoding loses at naptune's scale. Here's why:
-
The workload is trivial for any encoder. A 1-fps static-image slideshow has almost no motion, so every frame is mostly identical.
libx264 ultrafasteats it for breakfast. The P40's raw speed only shows on heavy motion at 4K. -
The P40 "win" was an illusion. The P40 beat CPU on pve06 because pve06's CPU is weak. On the actual prod-class CPU (i5-13500T), CPU wins outright — the P40 would have to beat 2.7s plus network round-trip, API queue, auth, and failure handling. It can't.
-
NVENC session init costs ~0.3s on every run — real overhead on a 1-second job.
-
Intel VAAPI didn't win either — it was slower than CPU across the board (1.27s vs 1.16s mean). The iGPU is great, but for this workload the CPU is just faster.
The takeaway: hardware encoding is not free performance. It's a tool with an initialization cost and a narrow sweet spot. It shines when you have motion, high resolution, high volume, or a weak CPU — not when you're encoding a slideshow on a 14-core Raptor Lake chip.
What this means for naptune
I'm keeping naptune on CPU. It's local, zero network, zero queue, zero new failure surface, and it never breaks a sweat at current volume.
If naptune ever grows into real motion video or batch transcoding, the right move is Intel QSV/VAAPI on the same host's iGPU — it's local to the LXC (no network hop), supports both H.264 and HEVC, and sips power. The P40 stays where it's genuinely valuable: LLM inference.
Lessons learned
- Benchmark against your real workload, not the hardware's peak. The P40 can do 4K 60fps NVENC — but naptune doesn't need 4K 60fps.
- CPU is often the bottleneck fixer. A modern desktop CPU with
libx264 ultrafastis genuinely fast. Hardware encoding saves you when the CPU is busy or weak — not when it's idle and the encode is trivial. - The iGPU is the underrated homelab gem. It's already in the CPU, costs ~0 watts at idle, does H.264+HEVC, and with 6 lines of LXC config you can use it from any container. If you have a Proxmox box with a modern Intel CPU,
h264_vaapi(orh264_qsv) is your free hardware encoder. - Do the math on the network hop. Uploading a song + image and downloading the MP4 adds latency that can wipe out any encode savings. Keep the encode where the data is.
Next up: I'm going to try the same benchmark on real (motion) video instead of slideshows, and compare QSV's h264_qsv path vs VAAPI on the same iGPU. The iGPU might actually win there.