yah264

yah264 field guide

How many threads?

Giving a video encoder every core can make it slower.

1The wavefront

yah264 parallelizes inside a frame. Macroblocks are coded in raster order. Each one needs neighbors finished before it can start. That dependency is what threading has to work around, and it is also what limits it.

A second thread can begin row 1 once row 0 is two macroblocks ahead. A third can begin row 2 once row 1 is two ahead of it. The work advances as a diagonal wave rolling down the picture.

t0t1t2t3t4t5t6
Each row starts two macroblocks behind the one above it. The colored band is the work in flight. Everything left of it is done, everything right of it is waiting. Seven threads fit here because the picture is wide enough to hold seven staggered rows at once.

The width of that wave is what limits it. Threads fill in behind the diagonal, so the useful number of them is set by how many rows the diagonal can span before the first thread runs out of picture. A wide frame has a long diagonal and room for many threads. A narrow one does not.

Past that width, the extra threads are still real threads doing real synchronization. They wake each other, they wait, and they do not shorten the critical path they are all queued behind.

2What a picture can absorb

yah264_frame_thread_cap(width, height) returns that limit. It is a property of the picture alone, and no amount of machine changes it. A 64-core server encoding CIF gets 12 threads on one frame, because the picture cannot hold more of the wave at once.

3The ceiling

What a picture can use is not always what an encoder should ask for. Every added thread joins the same dependency chain, so each helps less than the last, and on a machine with efficiency cores a slow thread can make the whole frame take longer.

So the automatic budget stops at 16. On an 18-core Apple Silicon machine encoding 1080p that costs nothing. Capped and uncapped finished within a hundredth of a second, while occupancy fell from 13.9 cores to 12.1. Those last cores had been spinning, not working.

The ceiling applies to the automatic budget only. Ask for a specific number and you get it, still clamped by what the picture can keep busy.

4Both together

The resolved width is the smaller of what you asked for and what the picture can use, where "what you asked for" defaults to the smaller of your core count and 16.

resolutioncap4812163264
QCIF 176x1445455555
CIF 352x288124812121212
720p 1280x720214812161616
1080p 1920x1080324812161616
4K 3840x2160634812161616

picture-limited: the frame cannot feed more ceiling-limited: the machine could, we choose not to

Read it in two directions. Down a column, small machines use everything they have, since a ceiling of 16 never applies to them. Along a row, small pictures clamp below the ceiling on their own, because the cap is reached first.

On Apple Silicon the core count comes from enumerating hw.nperflevels and summing each level. We don't assume a fixed split of performance and efficiency cores.

5Filling a bigger machine

A 64-core server encoding 1080p will use 16 cores for one encode. The other 48 aren't wasted. This layer just doesn't address them.

Frame-level threading is one of two kinds of parallelism an H.264 encoder has. The other is running several encoders at once over different parts of the sequence. That kind scales with the machine, not with the picture. We keep it in the caller: the library hands you one encoder instance and one budget, and an application that wants a whole server busy runs several instances, or simply several jobs. The command-line tool does exactly that, and so would a transcoding farm.

We put it there for a measured reason. Splitting one sequence across instances makes every split point a keyframe, and keyframes cost bits. On 1080p a single instance with a wide wavefront finished faster than a split across instances and spent 4.6% fewer bits. The split forced a keyframe the continuous encoder would not have chosen.

6Setting it yourself

Through ffmpeg, -threads N does what it says, and omitting it gets you auto:

ffmpeg -i in.mp4 -c:v libyah264 -crf 25 out.mp4
ffmpeg -i in.mp4 -c:v libyah264 -crf 25 -threads 8 out.mp4

Through the library, param.threads is the budget for one encoder instance: 0 asks it to decide, 1 is serial, N means up to N. yah264_threads_auto() reports what 0 would resolve to, so an application splitting work across instances can size its own budget from the same number the library would have used.

--threads 1 is worth knowing for its own sake. At one thread the encoder turns on a quality mode a threaded encode cannot use, so a serial encode spends its bits slightly better.