Historical evidence, superseded guidance. This is the earlier article, preserved with its original workload, measurements and revision links. Its defaults, branch/deployment labels and recommendations are not current. Read the current recipe.
AVPlumber · Mixer optimization cookbook
One main CUDA stream, one per monitor
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. AUX streams isolate stream ordering, not GPU capacity: kernels, memory bandwidth and encoders still compete with program. CUarray decode and raw uploads (the direct node, the pinned filter and v210) also have producer-stream synchronization requirements.
The program runs on one CUDA stream; each aux monitor gets its own non-blocking stream in the same CUDA context, so a slow monitor cannot hold up the program or the other monitor.
Status: ImplementedThe diagram below shows the linear decode layout. Opt-in NVDEC CUarray output adds a private producer stream per decoder in the same context. Compositors wait on each source's advertised stream before reading its array; program and AUX output streams keep their existing roles.
At a glance
- What
Almost all mixer GPU work (NVDEC output copies, scaling and format conversion, browser page imports, M/E compositors A and B, wipe, keyer, program and clean encoders) runs on the default stream of the
mixer_gpucontext. Each aux monitor wraps the same context with its own non-blocking stream, publishes it asaux_<id>_gpu, and runs its compositor, colour conversion and NVENC encoder on it. - Why
Non-blocking means no implicit ordering: the default stream does not wait for an aux stream, and an aux stream does not wait for the default stream. A slow monitor cannot stall the program or the other monitor. Sharing the context avoids the memory of a second context: about 200 MB (estimate).
- Latency
Program and command-to-screen: 0 frames added, by construction, not measured. The program stream never waits for an aux stream. On one shared stream, a slow monitor frame would queue ahead of the next program tick instead. Monitors: the stream split adds no wait of its own.
- Side effects
Streams isolate waiting, not capacity: every stream shares the same GPU engines and NVENC. Any FFmpeg filter fed aux frames inherits the aux stream automatically, because FFmpeg uses the stream of the frames' device. Tearing down an aux bus synchronizes and destroys its stream.
- Cost
One CUDA stream and one CUDA event per aux bus (the two monitors in the base demo, 22 buses in the recorded 192-source show). No extra nodes, queues or threads. One 33-line header. No extra context VRAM. CPU: each aux frame ends with a
cuStreamSynchronizeon the aux stream only. - Risk
Because nothing is ordered implicitly, an aux reader must wait for its inputs explicitly. The aux compositor records an event on each input's producer stream and makes its own stream wait on it; the program stream is never synchronized. A new aux consumer that skips this wait could read a half-written frame.
- Switch it off
No runtime switch. Without
aux_busesno aux stream exists. Running the aux path on the default stream is a code change: stop wrapping the device inaux_mode.
Why not a stream per node?
Most of the graph's GPU work sharing one stream is not the bottleneck by itself. The limit measured at high source counts was a lock inside the CUDA library, not stream ordering. Uploads queue for it (Why uploads limit this mix), and at 192 sources its holder was the browser importer's release (Browser frames go back one per new frame).
Raw uploads are another exception to the single stream. The patched FFmpeg hwupload_cuda v210 path creates a private non-blocking stream per source and
synchronizes it before publishing, so consumers get a complete frame. NV12 and P010 sources do the same on both paths that avoid the stock upload:
canvas.raw_upload: "direct", which every show the setup builds uses and where raw_video_file_cuda does it in CudaPinnedUploader, and "pinned", the patched filter. With the library
default, "hwupload", NV12 and P010 upload through FFmpeg hwupload on the default stream; see Raw frames read straight into the memory the GPU reads.
Where in the code
src/nodes/hwaccel/cuda_stream.hppmakeCudaStreamDevice: same context, new stream (cuStreamCreateflag0x1at line 26)src/nodes/mixer_compositor.cppaux_modewraps the device and publishes it underoutput_hwaccelsrc/nodes/mixer_compositor.cppthe aux stream waits on each producer stream's eventpyplumber/mixer/aux.pynames the stream deviceaux_<id>_gpu; used by the compositor (186), colour conversion (194) and NVENC (204)pyplumber/mixer/application.pycreates the main device with no stream of its own, so FFmpeg uses the default streamdeps/ffmpeg/9/0017-avfilter-hwupload-cuda-pinned.patch:86private upload stream for pinned raw uploads and v210 unpackingsrc/nodes/hwaccel/CudaPinnedUploader.hppthe same sequence for thedirectraw path: a private stream that waits for the device's stream, synchronized before the frame is handed on (pinned todevelopat 8fa15c92)src/nodes/hwaccel/cuda_rect_overlay.cppthe genericcuda_rect_overlaytakesoutput_hwaccelas well: it then draws on a private stream and publishes that device, so the branch below it leaves the parent's stream. No mixer graph sets it on that node; cuda_transform has no such option- Docs: config.md · aux_buses