Loading…
gathering yarn balls…
Cat Game Research
Frontier
Dev-log transmissions from the expedition

State-of-the-Art CPU Optimization for Math Kernels and Thread Pools

A high-level look at how modern CPU inference engines squeeze performance out of SIMD microkernels, cache tiling, packing, and lock-free thread pools.

The Two Bottlenecks

Modern CPU inference for transformer-style models is rarely limited by raw arithmetic. The cores can issue fused multiply-add instructions every cycle, but only if the data is already in registers. The real bottlenecks are data movement and dispatch overhead: how many bytes travel between DRAM, cache, and registers, and how many times the runtime wakes threads up to do a small piece of work.

This post is a high-level survey of the techniques production CPU engines use to attack both problems: SIMD microkernels, cache tiling and packing, and lock-free thread pool scheduling. No source code, no proprietary internals — just the engineering shape of the solution.

~4-40×
SIMD+tiling speedups reported
8-16
FP32 values per AVX2/512 vector
~64
Floats held in an 8×8 register tile
1-4×
Threading gains until memory bound

1. The Memory Pyramid

Every optimized kernel starts by admitting a hardware fact: registers are fast and small, caches are bigger and slower, and DRAM is enormous and glacial. A vector floating-point operation on data already in registers takes about one cycle. A load from L1 takes four, L2 twelve, L3 forty, and DRAM hundreds.

Memory Hierarchy & LatencyRegisters ~1 cycleL1 Cache ~4 cyclesL2 Cache ~12 cyclesDRAM hundreds of cycles
Figure 1. The closer to the top, the faster and smaller the storage. Optimized kernels keep hot data near the top.

The goal of kernel design is to move each byte as few levels down as possible. Once a cache line is pulled into L1, the kernel should do every operation it can on that line before it is evicted. That principle drives everything that follows.

2. SIMD and the Microkernel

A microkernel is the smallest unit of compute that still exposes the architecture's vector width. On x86 with AVX2, one vector register holds eight 32-bit floats; with AVX-512 it holds sixteen. A microkernel multiplies and accumulates a small tile of the output matrix while streaming through the reduction dimension.

Register-Blocked 8×8 MicrokernelA rows [8 × K]broadcast A[:, k]×B row [K × 8]load B[k, :]=C [8 × 8] regs64 accumulators
Figure 2. One K-step: load one row of B, broadcast one column of A, update an 8×8 tile of C that stays in vector registers.

The classic 8×8 microkernel keeps all sixty-four output values in vector registers for the entire K-loop. The two source operands are streamed from packed buffers, so each load pulls a full cache line and every float is reused many times before it is evicted. The accumulator tile is written back to memory only once, at the very end. This is the same register-blocking strategy used by oneDNN and BLIS.

"A reference kernel proves the math. A production kernel has to prove the data movement."

C-Kernel-Engine · GEMM Optimization Deep Dive

3. Tiling and Packing

The microkernel alone only wins when its inputs are already contiguous. Real weight and activation tensors are rarely laid out that way. Tiling partitions the M, N, and K dimensions into blocks sized for each cache level. Packing copies those blocks into contiguous panels so the microkernel can stream them with unit stride.

Three-Level Cache BlockingOriginal A (strided)non-contiguous rows→Packed A panelssequential access→L1/L2/L3 tilessize per cache level
Figure 3. Strided tensors are repacked into contiguous panels, then traversed in tiles sized for each cache level.

The trade-off is extra memory traffic for the pack step. Packing only pays off when the packed buffer is reused enough times to amortize the copy. In matrix-matrix multiplication the weights are reused across the whole M-dimension, so the cost is tiny. In matrix-vector multiplication the reuse is much lower, which is why GEMV kernels use a different strategy than GEMM kernels.

Shape-aware dispatch matters. A kernel that wins on large prompt prefill (GEMM) can be poor on token decode (GEMV), and vice versa. Production engines keep both paths and choose based on batch and sequence length.

4. Thread Pool Execution

Parallelizing a kernel is the second bottleneck. The simplest approach is a static partition: split the output rows evenly across threads. That works for uniform, large tiles. It fails when the workload is small, irregular, or has dependencies. For those cases, work-stealing thread pools give better utilization by letting idle threads take tasks from busy peers.

Work-Stealing Thread PoolWorker 0push / pop from own queueWorker 1idle → steal from neighborWorker 2Worker 3steal task
Figure 4. Each worker owns a lock-free deque. Idle workers steal from the opposite end of a busy neighbor's queue.

Modern work-stealing queues are lock-free, often based on the Chase-Lev deque. Owners push and pop from one end; thieves steal from the other. A subtle but important choice is continuation stealing versus child stealing. Continuation stealing keeps the current execution path on the same worker and pushes the continuation onto the queue, which tends to preserve depth-first traversal and better memory behavior. Child stealing does the opposite and can create breadth-first expansion. Libraries like libfork use continuation stealing with C++20 coroutines to keep memory usage low, reporting order-of-magnitude reductions in memory compared to OpenMP and TBB on fork-join benchmarks.

5. Putting It Together: Dispatch Policy

The final piece is deciding which kernel and which thread schedule to run for each operator. A production inference engine treats dispatch as a first-class concern:

01
Classify the workload. Is it GEMM (many outputs per weight) or GEMV (one output per weight)? Is the batch large enough to amortize threading overhead?
↓
02
Pick a microkernel. Match the register tile to the SIMD width and the operator shape. Use 4×8 or 8×8 tiles when the dimensions allow; fall back to smaller tiles or scalar for remainders.
↓
03
Tile for cache. Choose M, N, and K panels that fit L1/L2/L3. Pack weights and activations when reuse justifies the copy.
↓
04
Schedule threads. Use static splitting for large uniform tiles and work stealing for small or irregular tasks. Keep task granularity above the dispatch overhead threshold.
↓
05
Protect parity. Always keep a reference scalar path and compare outputs before enabling an optimized kernel for a new shape or format.

Recent research reinforces this layered approach. The EXO compiler generates micro-kernels from user schedules, showing that auto-generated C can match hand-written intrinsic code. IntrinsicML demonstrates that a transparent, well-documented AVX2 GEMM can reach 40–50% of OpenBLAS single-thread performance while remaining understandable. KleidiAI ships Arm microkernels as standalone .c/.h files with no dependencies, no memory management, and no scheduler, letting the framework decide how to call them. And X-OpenMP reports dramatic reductions in tasking overhead on fine-grained workloads through lock-less work stealing on many-core systems.

6. Reported Landscape

Project / LibraryWhat It OptimizesNotable Claim
BLIS / OpenBLASGEMM macro-kernel + micro-kernel tilingMicro-kernel centric; macro-kernel handles cache blocking and dispatch around a small hand-tuned core.
Intel oneDNNx86 GEMM, convolutions, activations8×8 AVX register tiles and packed weight layouts are central to its GEMM design.
Arm KleidiAIArm CPU micro-kernels for MLStateless, dependency-free micro-kernels; one .c/.h pair per variant.
ExoUser-schedulable kernel generationGenerated C micro-kernels competitive with hand-written intrinsics/assembly.
libforkFork-join scheduling with C++20 coroutines~7.2× faster than OpenMP and ~2.7× faster than TBB on reported benchmarks, with far lower memory use.
X-OpenMPLock-less OpenMP taskingUp to 40× faster than GNU OpenMP on fine-grained many-core workloads.
IntrinsicMLTransparent AVX2 ML kernelsGEMM 256×256 ~11× over scalar; still within 40–50% of OpenBLAS 1T, with full source transparency.
Typical Speedup Stack (reported ranges)1×~3×~6×~12×~20×+scalarSIMD+ tiling+ packing+ threading
Figure 5. Cumulative effect of SIMD, cache tiling, packing, and threading. Exact ratios depend on problem size and hardware.

Conclusion

State-of-the-art CPU kernel performance is not a single trick. It is a stack: vector arithmetic at the bottom, register blocking above it, cache tiling and packing above that, and a carefully chosen thread schedule at the top. Each layer has to fit the others. A great microkernel is wasted on strided data; a perfect packing strategy is wasted if threads spend more time waking up than computing.

The modern landscape offers two complementary paths. One is libraries of hand-optimized or auto-generated microkernels — BLIS, oneDNN, KleidiAI, Exo — that expose small, fast building blocks. The other is runtime research — libfork, X-OpenMP, work-stealing deques, continuation stealing — that reduces the cost of orchestrating those blocks. The engines that exceed general-purpose framework throughput are the ones that design every layer for the exact shapes, formats, and batch sizes they actually see.

"More SIMD instructions will not fix a layout problem. Fast data movement beats fast arithmetic when the data is already in the wrong shape."

C-Kernel-Engine · GEMM Optimization Deep Dive
← All transmissions