How NUMA Impacts FFmpeg Encoding Performance

Non-Uniform Memory Access (NUMA) architecture significantly influences FFmpeg multi-threaded video encoding by dictating how CPU cores access local and remote system memory. When an FFmpeg encoding job spans multiple NUMA nodes—common in multi-socket servers and high-core-count processors—memory latency increases and interconnect bandwidth becomes saturated. This article explains the technical mechanics of NUMA, how it creates bottlenecks during multi-threaded video encoding, and the practical configurations required to maximize FFmpeg throughput on NUMA systems.

Understanding NUMA in Modern CPUs

In a Uniform Memory Access (UMA) design, all CPU cores share equal access time to system RAM through a centralized memory bus. In contrast, NUMA partitions physical cores and memory into separate "nodes." Cores access their local memory bank with low latency and high bandwidth. However, when a core needs data residing in another node's memory bank, it must travel across an interconnect (such as Intel Ultra Path Interconnect or AMD Infinity Fabric). This remote access introduces higher latency and consumes finite interconnect bandwidth.

Why Video Encoding Is Sensitive to Memory Architecture

Modern video codecs like H.264 (libx264), HEVC (libx265), and AV1 (libsvtav1) are memory-bandwidth-intensive. Encoders perform complex operations—such as motion estimation, macroblock analysis, and inter-frame reference lookups—requiring frequent reads and writes to large frame buffers stored in RAM.

When multi-threading is enabled in FFmpeg, threads actively share data:

  • Reference Frames: Threads analyzing future frames must reference decoded past frames held in memory.
  • Lookahead Threads: Slicing and rate-control threads read ahead to determine frame complexity.
  • Filtergraphs: FFmpeg pixel format conversions, scalers, and filters transfer raw, uncompressed frames through memory.

The Impact of NUMA on Multi-Threaded FFmpeg

1. Remote Memory Latency and Interconnect Saturation

If a single FFmpeg process is configured to use all available threads on a multi-node system, the operating system's scheduler distributes threads across all physical nodes. When a thread on Node 1 references a frame buffer allocated in Node 0's memory, every read traverses the inter-socket interconnect. This results in:

  • Increased CPU wait states (pipeline stalls while waiting for RAM).
  • Interconnect saturation, which slows down every process on the machine.
  • Sharply degraded encoding speed (frames per second) compared to theoretical core scaling.

2. Cache Invalidation and Coherency Overhead

Multi-threaded video encoders rely on CPU cache coherency protocols to ensure that all cores see the latest modifications to shared frame data. Across NUMA boundaries, maintaining cache coherency requires cross-node signaling. As the thread count increases across nodes, cache lines are frequently invalidated, causing threads to reload identical data from remote memory repeatedly.

3. Diminishing Returns and Negative Scaling

Video encoding exhibits diminishing returns with high thread counts due to synchronization overhead. Combining this software limit with NUMA latency often produces negative scaling: allocating 64 threads across two NUMA nodes can run noticeably slower than running 32 threads strictly confined to a single node.

Codec-Specific NUMA Behaviors

  • libx264: Uses a frame-based and slice-based multi-threading model. It scales well up to roughly 16–24 threads. Beyond this, scaling flattens. Spanning across NUMA nodes typically leads to CPU core starvation and stalls.
  • libx265: Features a native NUMA awareness system. By default, x265 attempts to detect NUMA nodes and allocate thread pools accordingly using the --pools parameter. If not explicitly tuned, cross-socket synchronization between frame analysis and wavefront processing (WPP) can still throttle throughput.
  • libsvtav1: Designed with modern high-core architectures in mind, SVT-AV1 scales better than older encoders but still suffers from latency penalties when frame buffers cross socket interconnects.

Optimizing FFmpeg on NUMA Systems

To achieve optimal performance on multi-socket or multi-node systems, avoid running a single FFmpeg process across the entire machine. Instead, apply node-pinning and process isolation strategies.

1. Pinning with numactl

Use the Linux utility numactl to pin FFmpeg to a specific node's CPU cores and its local memory.

# Run FFmpeg strictly on NUMA Node 0
numactl --cpunodebind=0 --membind=0 ffmpeg -i input.mp4 -c:v libx264 -crf 20 output0.mp4

This ensures that the encoder never makes remote memory calls, keeping cache access times predictable and low.

2. Parallel Multi-Instance Transcoding

Instead of encoding one file using all nodes (e.g., 64 threads on 2 sockets), split workloads to run concurrent encoding instances pinned to distinct nodes:

  • Instance 1: numactl --cpunodebind=0 --membind=0 ffmpeg -i file1.mkv ...
  • Instance 2: numactl --cpunodebind=1 --membind=1 ffmpeg -i file2.mkv ...

This approach leverages 100% of the hardware while entirely bypassing the cross-node latency penalty, yielding the highest aggregate throughput (total frames encoded per second across all jobs).

3. Encoder-Specific Thread Pool Isolation

For encoders that support internal pool partitioning, explicitly configure thread pools to prevent node hopping. For x265 via FFmpeg:

ffmpeg -i input.mp4 -c:v libx265 -x265-params pools=16,16 output.mp4

This instructs the encoder to split work into discrete pools corresponding to the hardware topology rather than allowing threads to float freely across all sockets.