Dynamic epilogue fusion
Boundary tiles break in different places: M and N need masked stores, a K remainder needs zero-padded accumulation, not epilogue masks.
The epilogue sees a clamped tile
Fusion under dynamic shapes is the static epilogue with two clamps. M and N produce a tile whose row and column counts are affine.min values; the bias and activation apply to that tile, and the store writes exactly it:
%tm = affine.min affine_map<(d0)[s0] -> (64, s0 - d0)>(%i)[%M]
%tn = affine.min affine_map<(d0)[s0] -> (64, s0 - d0)>(%j)[%N]
%acc = tensor.extract_slice %C[%i, %j] [%tm, %tn] [1, 1]
: tensor<?x?xf32> to tensor<?x?xf32>
Two designs work. Keep the accumulator tile static at 64x64 and mask the store and the bias load at the edge, so the arithmetic stays vector-shaped and the discarded lanes never reach memory. Or let the tile be dynamic and mask every transfer. The static-accumulator design keeps the vector code; the dynamic one keeps the IR small.
The bias needs the same mask
The output store is the obvious edge. The bias load is the one that gets missed: bias[j + lane] reads past N on the last tile unless the load is masked or the bias is padded to a tile multiple. Padding the bias once at the boundary is a legitimate fix, and it turns the edge case into an interior case for the epilogue.
K is different. A K remainder is an accumulation problem, not an epilogue problem: the last K tile must be zero-padded or masked so the partial products contribute zero, and only then does the epilogue see a complete accumulator. Fusing an epilogue does not change that requirement.
Things that break
An unmasked bias load at the N edge reads out of bounds. Applying the activation before the bias computes relu(acc) + bias, which is a different function. And a K remainder filled with stale buffer contents adds garbage that no epilogue can remove. Confirm the mask and slice syntax on your pinned release before copying the snippet.
Sources
- MLIR linalg dialectdocs · LLVM 19
- MLIR tensor dialectdocs · LLVM 19
- MLIR scf dialectdocs · LLVM 19
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 does a K remainder have to be handled?
At the N edge, which load needs a mask besides the output store?
How do `relu(acc) + bias` and `relu(acc + bias)` differ?