perf(video): decode HLS transcode sources on the GPU #167

Merged
cameron merged 1 commits from perf/hls-gpu-decode into master 2026-10-13 16:13:00 +00:00
Owner

Summary

The HLS transcode encoded on NVENC but decoded the source on the CPU. For 4K sources, decoding is most of the work.

 ffmpeg -y
+  -hwaccel cuda            # only when NVENC is available (same gate as preview clips)
   -i <source>
   -c:v h264_nvenc ... -vf scale=...   # unchanged; frames come back to system memory

Stream copy and the libx264 fallback are unchanged.

Evidence

During the post-#166 rebuild, 3 concurrent 4K transcodes ran at 210–240% CPU each, with the CPU at 0% idle, NVENC at 13% and NVDEC at 0%.

Timings for the same ffmpeg args on 10 s of 2025-USAF-MuseumRace.mp4 (4K H.264, ~117 Mbps), measured while those 3 rebuilds were also running:

  • Before: cpu-decode: wall=9.60s user=27.30s → 1920×1080, 245 frames, segments 3.003 3.003 3.003 1.21
    After: -hwaccel cuda: wall=3.64s user=6.14s → 1920×1080, 245 frames, segments 3.003 3.003 3.003 1.21

Edge cases, both with -hwaccel cuda:

  • A rotated phone clip (1440×1080, rotation 90) comes out at 1080×1440, upright, with 233/233 frames. Autorotate still applies.
  • ProRes, which NVDEC doesn't support, falls back to software decode and produces correct output.

Clippy is clean, and DATABASE_URL=":memory:" cargo test passes 1531/1531.

Merge Danger

Door: two-way

A single ffmpeg input flag. Reverting the commit restores CPU decoding. Playlists generated with it are identical to CPU-decoded ones, so they don't need regenerating either way.

Blast Radius: transcodes

This only affects HLS transcodes on hosts with NVENC. Possible risks: GPU memory pressure if HLS_CONCURRENCY is high next to other GPU work. Each decode session uses a few hundred MB; the 3 concurrent encodes used about 1.3 GB in total on the 3090. Also, a source that NVDEC accepts but decodes incorrectly would fail where CPU decoding succeeds. None of the tested sources did. Deploying restarts the service, which kills in-flight encodes. Their playlists were never written, so the next scan requeues them.

🤖 Generated with Claude Code

## Summary The HLS transcode encoded on NVENC but decoded the source on the CPU. For 4K sources, decoding is most of the work. ```diff ffmpeg -y + -hwaccel cuda # only when NVENC is available (same gate as preview clips) -i <source> -c:v h264_nvenc ... -vf scale=... # unchanged; frames come back to system memory ``` Stream copy and the libx264 fallback are unchanged. ## Evidence During the post-#166 rebuild, 3 concurrent 4K transcodes ran at 210–240% CPU each, with the CPU at 0% idle, NVENC at 13% and NVDEC at 0%. Timings for the same ffmpeg args on 10 s of `2025-USAF-MuseumRace.mp4` (4K H.264, ~117 Mbps), measured while those 3 rebuilds were also running: - **Before:** `cpu-decode: wall=9.60s user=27.30s` → 1920×1080, 245 frames, segments `3.003 3.003 3.003 1.21` **After:** `-hwaccel cuda: wall=3.64s user=6.14s` → 1920×1080, 245 frames, segments `3.003 3.003 3.003 1.21` Edge cases, both with `-hwaccel cuda`: - A rotated phone clip (1440×1080, rotation 90) comes out at 1080×1440, upright, with 233/233 frames. Autorotate still applies. - ProRes, which NVDEC doesn't support, falls back to software decode and produces correct output. Clippy is clean, and `DATABASE_URL=":memory:" cargo test` passes 1531/1531. ## Merge Danger **Door:** two-way A single ffmpeg input flag. Reverting the commit restores CPU decoding. Playlists generated with it are identical to CPU-decoded ones, so they don't need regenerating either way. **Blast Radius:** transcodes This only affects HLS transcodes on hosts with NVENC. Possible risks: GPU memory pressure if `HLS_CONCURRENCY` is high next to other GPU work. Each decode session uses a few hundred MB; the 3 concurrent encodes used about 1.3 GB in total on the 3090. Also, a source that NVDEC accepts but decodes incorrectly would fail where CPU decoding succeeds. None of the tested sources did. Deploying restarts the service, which kills in-flight encodes. Their playlists were never written, so the next scan requeues them. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
cameron added 1 commit 2026-10-13 16:12:19 +00:00
When NVENC is available the HLS transcode encoded on the GPU but still
decoded on the CPU, since the command had no -hwaccel. Rebuilding the
4K homemade videos ran three ffmpegs at 210-240% CPU each with the
machine at 0% idle, while NVDEC sat at 0%.

Pass -hwaccel cuda when transcoding with NVENC, as generate_preview_clip
already does. Frames return to system memory, so autorotate and the
scale filter work unchanged; NVDEC-unsupported codecs fall back to
software decode.

10 s of 4K H.264 (2025-USAF-MuseumRace.mp4): 27.3 s -> 6.1 s CPU,
9.6 s -> 3.6 s wall, identical output (1920x1080, 245 frames, 3 s
segments). Also checked a 90-degree-rotated phone clip (comes out
1080x1440 upright, 233/233 frames) and ProRes (software fallback,
correct output).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cameron merged commit 4745a945fe into master 2026-10-13 16:13:00 +00:00
cameron deleted branch perf/hls-gpu-decode 2026-10-13 16:13:01 +00:00
Sign in to join this conversation.