Epilogue concept
An epilogue transforms the accumulator tile after the K loop, so bias and activation need no extra memory pass.
Where the accumulator is finally correct
The K loop ends, the accumulator tile holds complete sums in registers, and everything the output needs happens before the single store:
tile loop: acc = 0
for k: acc += A[i, k] * B[k, j]
acc = epilogue(acc, bias[j]) // registers only
C[i, j] = acc // one store
Bias, activation, scaling and dtype conversion all belong in that gap. The store happens once because after the epilogue there is nothing left to compute.
Why it cannot move earlier
Inside the K loop the accumulator holds a partial sum. relu of a partial sum is not the relu of the sum, and a bias added on every K iteration is added K times. Both mistakes type-check, both produce finite numbers, and both happen to be correct when K is 1.
What it removes
The unfused chain touches the output once per stage: the matmul writes 4 bytes per f32 element, the bias pass reads and writes 8, and relu reads and writes 8, for 20 bytes per element against 4 in the fused path. Those are traffic counts, not timings, and they are an upper bound on what the memory system sees: if the intermediate stays in cache, most of those bytes never reach DRAM.
In MLIR
A linalg.generic body has no after-the-reduction phase; it runs once per iteration of the whole nest, reduction dimension included. So the epilogue is a second op on the tile, fused at tile level so the intermediate is a tile rather than a tensor, or it is written into the microkernel. This is where the intrinsics path stops being a detour and starts being the place where the schedule is expressible.
Things that break
The epilogue is folded into the matmul’s own body. The reference check passes at K=1 and fails at K=3, with the bias multiplied by K. Run the same shape twice, once with K=1 and once with K=3, and compare against the unfused chain: agreement at both means the epilogue is outside the reduction, and agreement only at K=1 identifies this bug in one step.
Sources
- Halide: A Language and Compiler for Optimizing Parallelism, Locality, and Recomputation in Image Processing Pipelinespaper · PLDI 2013
- TVM: An Automated End-to-End Optimizing Compiler for Deep Learningpaper
- MLIR linalg dialect documentationdocs · LLVM 23
A version records the documentation the claim was checked against, not a house pin. Confirm flags with --help on the release you installed.
Check yourself
Where in the loop nest does the epilogue belong?
What does the fused epilogue remove, per f32 output element, compared with matmul then bias then relu as separate passes?
Can a single `linalg.generic` express matmul, bias and relu in one body correctly?