Historical recipe — not current setup guidance. Measurements and implementation links below belong to their original workload and revision. Deployment labels and defaults may be obsolete. Use the reviewed performance recipes; browse historical evidence.
AVPlumber · Mixer optimization cookbook
One thread per GPU filter graph
Implementation links are pinned to develop at c20464a5 (2026-10-09). The mixer has changed since; this page was checked against develop at 8fa15c92 on 2026-10-13. Measurements retain their original workload and scope. Watch the current 192-input L4 demo.
Current scope. Current GPU filter graphs still use threads: 1. Per-slot scale/filter hooks have been removed; geometry belongs to the compositor or an upstream source transform. The pinned and hwupload raw paths use single-threaded FFmpeg graphs; the setup's direct path has none. A source whose colour stage would only tag has no colour graph: its fan-out stamps the tags (Colour tags at the fan-out).
The mixer runs filter graphs that only stamp metadata or call CUDA filters on one thread. FFmpeg would otherwise start an idle 16-thread slice pool for each of them.
Status: ImplementedDecoder options.threads=1 is a separate optimization: it avoids extra NVDEC surfaces reserved for frame threads. See the decoder baseline and low-DPB demo assets; both savings were already present before the CUarray comparison.
The idea
Each filter_video node builds an FFmpeg filter graph. By default FFmpeg gives every graph a slice-thread pool with one thread per CPU: 16 on the T4 test host. A graph of setparams or scale_cuda never uses that pool. The GPU does the pixel work, and the node's own thread calls the filters.
At a glance
- What
A new node parameter
threadsonfilter_videoandfilter_audio. It sets the graph'snb_threadsbefore parsing. The default, 0, keeps FFmpeg's choice, so other apps are unchanged. A negative count is rejected when the node is created: the node assignsnb_threadsdirectly, so FFmpeg's own range check for itsthreadsoption never runs.The mixer's CUDA backend passes
threads: 1for its GPU and metadata graphs: colour where needed, transition, aux SDR conversion, rendition conversion (the graph namedscale_<id>; a rendition of another size is scaled before it bycuda_transform, which has no filter graph), and the raw-filesetpts/hwuploadpair (the pinned raw chain also uses an FFmpeg upload filter, the direct raw chain has no filter graph; the browsersettb/setparamsgraph has since been dropped, and the browser chain has none either). - WhyFFmpeg starts one slice thread per CPU for every graph (16 on the T4 test host), even when the graph does no CPU pixel work. Measured live: 1,088 threads in
mixer_color_sdr, 448 inmixer_color_bro, 3,689 in the mixer process, mostly idle (12 % CPU). - Latency
Program: none, 0 frames.
Command-to-screen: none, 0 ms.
These graphs never hand work to the slice pool, so the same thread runs the same filters as before. Expected from the code path, not measured.
- Side effectsNone expected. To confirm on the test host, compare CPU per thread group and the thread count before and after. User filter graphs and the CPU wipe format conversion keep FFmpeg's default pool.
- Cost1 node parameter and a few lines of code. 0 nodes, 0 queues, 0 VRAM. Threads: about 1,600 idle threads gone.
- RiskA CPU filter placed in one of these graphs would run on one thread and could get slower. Per-slot filter hooks have been removed; upstream source filters must respect their processing budget. Apps that don't set
threadssee no change. - Switch it offDrop the
threadsparameter. For the whole mixer, setCudaMixerBackend.graph_threads = 0, which is FFmpeg's default.
Where in the code
- src/nodes/filters.cpp: reads
threads(default 0); source reject a negative count; source setnb_threadsbefore parsing, because FFmpeg starts the pool when it allocates the first filter; source is the member. - pyplumber/mixer/backends/cuda.py:
graph_threads = 1; source the transition graph. Contract: pyplumber/mixer/backend.py. - pyplumber/mixer/graph.py: colour graphs; source source colour routing; source source contracts.
- pyplumber/mixer/aux.py: aux SDR conversion.
- pyplumber/mixer/application.py: rendition conversion. The source normalisation of a large catalogue was a filter graph here too; it is a
cuda_transformnow (source) and has no graph. - pyplumber/mixer/dmabuf_inputs.py: the browser
settb/setparamsgraph has since been dropped; the browser chain has no filter graph of its own. - pyplumber/mixer/inputs.py: raw-file
setptsandhwupload. - Kept at FFmpeg's default: the wipe format conversion, pyplumber/mixer/graph.py.
- Tests: tests/mixer/test_backend.py, tests/mixer/test_graph.py.
- Docs: NODES.md · filter_video, filter_audio (the
threadsparameter, an int >= 0).
Source links identify the reviewed code, independently of deployment state.