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
Ideas we measured and dropped
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. These are historical decisions under the workloads shown. The demo now reads raw NV12 and P010 files with one node, raw_video_file_cuda, which uploads from page-locked memory on a private stream as the pinned filter does; the earlier custom uploader nodes are gone. Input normalization has since been removed from the mixer without skipping native input work.
Every source is a unique, independent input of at least 1080p25, processed every frame; twelve ideas broke that rule, saved almost nothing, or made cuts, pictures or the pipeline slower, so we dropped them.
Status: RejectedThe rule, and where each shortcut would cut it
Why reusing compositor B's picture costs more
The twelve ideas
The cost model said no
- 1Subcomposition reuse
IdeaDraw the Program preview's PVW quadrant from compositor B's picture. Share scaled renditions between compositors.
DroppedCompositor cost follows drawn output pixels, not layer count. Scenes cover only ~49 % of the portrait canvas on average, so copying B's full quadrant draws more pixels than re-compositing. Best case, reuse saves ~0.01 GFLOP/s of the T4's 8.1 TFLOPS. A whole full-canvas compositor costs ~0.56 GFLOP/s.
KeptThe PGM tile reuses the finished program. Compositor B idles when no preview is armed.
They would break the rule
- 2Share identical media between sources
IdeaOne decode chain per file, fanned out to every source that plays it.
DroppedIt would cut NVDEC sessions from 40 to 7 and save 1.5–4 GB. Real inputs are all different, so the saving is fake.
- 3720p browser pages or 720p test clips
DroppedReal pixels stay 1080p. Smaller inputs make every stage look cheaper than a real show.
- 4Decode-side resize to 1080×608
IdeaLet NVDEC write a 1920×1080 clip at the size a full-canvas box shows it on a 1080×1920 canvas.
DroppedIt keeps fewer pixels than the source. Dropped for the capacity demo; it is not in the code.
- 5Keep looped test clips on the GPU
DroppedReal inputs deliver a new frame every tick. Replaying held frames skips decode work a real show must pay.
- 7Pause outputs nobody watches
IdeaA Janus viewer poller stops the monitor and clean-feed encoders while no one views them.
DroppedIt saved encoder work only while unwatched, and hid real load. A viewer saw ~1 s of black or frozen picture on resume. A clean-feed consumer that is not a Janus viewer, such as a recorder, would lose picture. More moving parts. Removed from the code.
- 10Browser sources at 30 fps in a 60 fps show
DroppedEvery demo source is a real 60 fps input. A production knob, not a capacity result.
Ruled out for the 60 fps show
- 8
__GL_YIELD=USLEEPDroppedChromium's GPU processes made 0
sched_yieldcalls in 5 s: no spinning. Their CPU is ~12k futex/s thread hand-offs and ~1.1k ioctl/s. - 9Fewer, bigger browser workers
DroppedCost is per window per delivered frame, not per process; fewer workers concentrate VizCompositor load (Browser CPU is per painted frame).
SinceA later audit found that 36–44 % of the cost in the GPU and browser processes is per tick, not per delivered frame. Whether that part is per window or per worker cannot be told while every worker holds 8 windows, so the idea was not re-opened and not re-tested (Per tick and per paint).
- 11cpuset split between containers
DroppedCores pinned to one side idle while the other is short: it idles capacity.
- 12FFmpeg
hwuploadfor raw sources at 60 fpsDropped69 % of program frames missed their deadline at 68 inputs; NVDEC fell from ~85 to ~26 % because the pipeline stalled (Raw frames read straight into the memory the GPU reads).
The trade was cut latency
- 6Narrow prewarm, idle off-screen sources
DroppedDirect cuts get 80–120 ms slower. Without prewarm, a cut resets the incoming source's playout queue and waits for fresh frames.
At a glance
- What
One rule for capacity numbers: every source is a unique, independent input of at least 1080p25, processed every frame. Ideas 2–5 and 7 break it. Idea 1 saves nothing. Idea 6 costs cut latency. Ideas 8, 9 and 11 gained nothing; 10 breaks the rule; 12 stalled the pipeline.
- Why
Capacity numbers must describe real shows. A shortcut that works only because test inputs repeat, loop, shrink or go unwatched makes the numbers wrong.
- Latency
- Program latency: unchanged; nothing here shipped.
- Idea 6 would add 80–120 ms to direct cuts.
- Idea 7 would add ~1 s of black or frozen picture when a viewer opens a paused output.
- Kept reuse: the PGM tile runs one aux frame behind the other tiles; the program is not delayed.
- Side effects
None on the program path. The honest load stays visible: all sources decode and convert all the time, and every output encodes every frame, watched or not.
- Cost
- 0 new nodes, queues or threads; the pause poller's code is gone.
- GPU: budget for the whole source catalogue, not the scene on air.
- Forgone savings: NVDEC 40 → 7 and 1.5–4 GB (idea 2); encoder work on unwatched outputs (idea 7).
- Risk
Headline capacity is lower than a demo with shortcuts would show. That is the point.
- Switch it off
- Nothing to switch off: ideas 1, 4, 5, 7–9 and 11 are not in the code; 10 is a browser source's
fpsfield (config.md · sources), which the demo leaves at the canvas rate; 12 is the library default, off in setup-built shows, which read raw files withraw_upload: "direct". - Repeated files: a repeat is rejected at load; mark both declarations
"independent": truefor separate chains. - Cut prewarm: the demo runs with
--prewarm-cut-scene '*'(every scene). The mixer's own default is off; naming fewer scenes narrows it.
- Nothing to switch off: ideas 1, 4, 5, 7–9 and 11 are not in the code; 10 is a browser source's
An earlier proposal: shorter decoded frame queues
Not one of the twelve: it was never built or measured. The proposal was to plan each NVDEC source's decoded edge at capacity 1 instead of the instance default of 3 when a show runs at 30 fps or below. NVDEC decodes ahead of real time, so that edge is always full, and two fewer parked 1080p frames per source were estimated to free about 255 MB across 40 sources. It was not selected: one frame leaves 33 to 40 ms of slack before a decode hiccup repeats a frame, and the comparison of repeat rates the proposal asked for was not run. The NVDEC CUarray path then saved 4.88 GiB in the matched 192-input, 20-output comparison while keeping all three decoded queue slots. Every NVDEC source's decoded edge still has the default of 3 (planCapacity("*", 3) in application.py). A browser window's frame queue is a different queue: Browser frames go back one per new frame.
Since 14 October the proposal exists as an option on an avplumber branch, canvas.decoded_frames_ahead. Run at 1 on the 192-source show it did not lower GPU memory (20.1 GB, within the previous build's 19.3 to 20.1): on the CUDA array path a waiting frame is a reference to one of the decoder's fixed arrays, not a buffer of its own. The comparison of repeat rates has still not been run (GPU memory: frames no tick can show any more).
Measured on 13 and 14 October, with no gain
- NVENC
delay: 1instead of 0. No change at 1080p30, 1.6 to 2.05 % per encoder thread either way; the encoder threads' wakes are NVIDIA's own wait. Left at 0 (Input, decode and wake-ups). - Native-handle browser capture without the zero-copy export. Still cost the show 13 missed deadlines at 12 windows after its pool was fixed; not explained, and the export replaces it (Chromium paints into the buffer the mixer reads).
- Six spare decoder arrays instead of twelve. 16.21 against 18.76 GB of GPU memory, but the first start attempt failed twice with
No decoder surfaces left; the show stays at 12 (GPU memory). - Fan-out on the event loops as a CPU saving. It removed 192 threads and is kept for that, but the CPU moved onto the loops: no saving by itself.
Where in the code
- PGM tile is the finished program: aux.py (last input,
contain), matched one frame back at mixer_compositor.cpp - Idle compositor slot, no surface and no kernel: mixer_compositor.cpp; empty mask means an idle PVW slot: cuda_rect_overlay.cpp
- Bilinear, 4 taps per sample: cuda_rect_scale.cu:81–86
- One chain per source: inputs.py (
build_input); repeated locations andindependent: config.py - Own media per generated source, id burned in: prepare_demo.py
- Cut prewarm: scene.cpp (
prewarmCuts), Playout.hpp (setPrewarm; a cold input resets its queue), application.py (CLI flag) - Outputs that encode every frame: janus.py (
build_janus_output), clean feed split at dsk.py - Docs: config.md · Known limitations (all sources run all the time), config.md · sources (
independent), config.md · aux_buses (PGM tile one frame behind), recipe.md · Generated media, mixer_cut_latency.md · Optional direct-cut source prewarm
The GFLOP/s figures and the NVDEC and memory savings are model estimates for the T4 test host, not measurements. The ~49 % coverage is an average over scene layouts; single scenes vary.