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

GPU memory: frames no tick can show any more

Measured on 14 October 2026 on the 16 vCPU NVIDIA L4 host that runs the demo; GPU memory is the whole card as the driver reports it, unless a line says per process. Not on develop: the changes are on the avplumber branch mixer/vram at 7310870e and run in the demo show. Watch the current 192-input L4 demo.

Until every input of a compositor has a picture, at the start of a show, the compositor cannot draw a tick, and it queued up to eight frames of every other input meanwhile. Each of those frames is a GPU buffer from its source's pool, the pools grew to hold them, and such pools never shrink: the start set the memory the show then kept. The playout now lets a frame go as soon as a newer one is at or before the tick due now, which no tick can undo. What is drawn is unchanged. The show's steady GPU memory fell from 20.1 to 18.8 GB, and the start no longer runs the card out of memory.

Status: on a branch, running in the demo show
One input's playout queue in a compositor that cannot draw a tick yet Before: eight frames of the input are held until the compositor can tick, and the source's frame pool grows to cover them and keeps them. After: frames whose successor is at or before the tick due now go back to the source's pool at once; the newest frame at or before that tick and the ones after it stay. Illustration · one input's queue while the compositor waits for its last input Before · every frame held until the first tick 1 2 3 4 5 6 7 8 eight GPU buffers held; the source's pool grows to cover them and keeps them After · superseded frames go back at once 1 2 3 4 5 6 7 8 tick due now 7 is the newest frame a tick can still show; 1 to 6 are back in the source's pool held by the queue returned at once kept: a tick can still show it
Illustration, not a measurement. The rule is per input and runs before every attempt to draw: a frame whose successor is stamped at or before the earliest tick still to be drawn can never be shown. An input with a frame-rate cadence keeps one more, because a cadence can move queued frames one tick later.

Why a start decided the show's memory

Two kinds of GPU frame pool in the mixer behave differently, and both were hit at the start (from the code, checked against the measurements below):

A probe made it concrete: a decoder feeding a compositor that could not tick kept 33 MiB more GPU memory than one feeding a null sink, for the life of the process; with the change, 18 MiB. On the show, a start logged about 50 Cannot allocate memory lines, and two starts in a row failed outright when a raw reader's allocation found the card full.

The change

Before each attempt to draw, the playout drops, per input, every frame whose successor is at or before the tick due now. A tick shows the newest frame stamped at or before it, and the earliest tick still to be drawn is the one due now, so such a frame can never be drawn. Nothing is dropped while a decision awaits its commit. An equivalence test drew 3,000 ticks with mixed rates, offsets, stalls and late wakes with and without the rule: the same frames, tick for tick.

The start that followed logged no Cannot allocate memory line and peaked at 19.2 GB, against 22.4 GB on the start before it. Peaks vary from start to start: purging idle browser imports before the outputs go on air (on develop since 13 October) had brought an earlier start's peak from 22.4 to 19.6 GB.

Step by step

The running 192-source show at 1080p30 (105 HEVC files through NVDEC into CUDA arrays, 40 browser windows, 47 raw NV12 files, 24 encodes). Each reading was taken after a restart on that build; between two starts of the same show the whole-GPU figure has differed by up to 1.9 GB.

Build or settingWhole GPUMixer CPUWhat it is
After the input hops20.2 GB265 %the reference for this page
Fan-out on the pacing loops19.3–20.1 GB247 %a CPU change (its page)
decoded_frames_ahead: 120.1 GB247 %one decoded frame waits in front of pacing instead of three; the raw reader waits for GPU memory instead of failing
Playout drops superseded frames18.76 GB246 %the change above
Sources at extra_hw_frames: 817.78 GB248 %8 spare decoder arrays instead of 12
Sources at extra_hw_frames: 616.21 GB241 %the first start attempt failed twice with No decoder surfaces left; the show went back to 12

Every step: 0 missed deadlines and 0 output drops over the readings, no repeats of decoded or raw sources, no frame dropped at any of the 5,398 queues, and no decoder error at start or after, except where the table says otherwise.

Three settings, and what each is worth

What things cost, measured one at a time

Probe processes on the L4, per-process GPU memory, 14 October 2026:

UnitGPU memoryNote
CUDA context, no nodes186 MiBonce per process
HEVC decoder, 1080p, NVDEC71–74 MiBon the linear output path; the show uses CUDA arrays, not measured this way
Raw NV12 1080p source12 MiBa one-slot chain into a null sink; the show's fan-out holds more
NVENC H.264 1080p p3 with the AUX settings48 MiB34 MiB with dpb_size 1, which the AUX encoders get (one reference frame)

Before these changes the mixer process alone held 15,450 MiB of the show's 19.2 to 20.2 GB. The browser's GPU processes hold about 4.7 GB at a 16.2 GB whole-GPU reading, and the zero-copy export adds about 0.5 GB.

At a glance

Related pages

Where in the code

Source links are pinned to the branch commit that runs in the show.