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.
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.
| resolution | cap | 4 | 8 | 12 | 16 | 32 | 64 |
|---|---|---|---|---|---|---|---|
| QCIF 176x144 | 5 | 4 | 5 | 5 | 5 | 5 | 5 |
| CIF 352x288 | 12 | 4 | 8 | 12 | 12 | 12 | 12 |
| 720p 1280x720 | 21 | 4 | 8 | 12 | 16 | 16 | 16 |
| 1080p 1920x1080 | 32 | 4 | 8 | 12 | 16 | 16 | 16 |
| 4K 3840x2160 | 63 | 4 | 8 | 12 | 16 | 16 | 16 |
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.