$ mlir-opt 03_transform_dialect.mlir --transform-interpreter --test-transform-dialect-erase-schedule --canonicalize --cse > out/tiled.mlir && sed -n '/scf.forall/,/parallel_insert_slice/p' out/tiled.mlir
what it printed on the machine that wrote this lab
%1 = scf.forall (%arg3, %arg4) in (8, 8) shared_outs(%arg5 = %0) -> (tensor<64x256xf32>) {
%2 = affine.apply #map(%arg3)
%3 = affine.apply #map1(%arg4)
%extracted_slice = tensor.extract_slice %arg0[%2, 0] [8, 128] [1, 1] : tensor<64x128xf32> to tensor<8x128xf32>
%extracted_slice_0 = tensor.extract_slice %arg1[0, %3] [128, 32] [1, 1] : tensor<128x256xf32> to tensor<128x32xf32>
%extracted_slice_1 = tensor.extract_slice %arg5[%2, %3] [8, 32] [1, 1] : tensor<64x256xf32> to tensor<8x32xf32>
%4 = linalg.fill ins(%cst : f32) outs(%extracted_slice_1 : tensor<8x32xf32>) -> tensor<8x32xf32>
%5 = linalg.matmul ins(%extracted_slice, %extracted_slice_0 : tensor<8x128xf32>, tensor<128x32xf32>) outs(%4 : tensor<8x32xf32>) -> tensor<8x32xf32>
%extracted_slice_2 = tensor.extract_slice %arg2[%3] [32] [1] : tensor<256xf32> to tensor<32xf32>
%6 = linalg.generic {indexing_maps = [#map2, #map3, #map2], iterator_types = ["parallel", "parallel"]} ins(%5, %extracted_slice_2 : tensor<8x32xf32>, tensor<32xf32>) outs(%extracted_slice_1 : tensor<8x32xf32>) {
^bb0(%in: f32, %in_3: f32, %out: f32):
%7 = arith.addf %in, %in_3 : f32
%8 = arith.maximumf %7, %cst : f32
linalg.yield %8 : f32
} -> tensor<8x32xf32>
scf.forall.in_parallel {
tensor.parallel_insert_slice %6 into %arg5[%2, %3] [8, 32] [1, 1] : tensor<8x32xf32> into tensor<64x256xf32>
READ IT LINE BY LINE — this is the most important IR in the whole lab:
scf.forall (%arg3, %arg4) in (8, 8) 64 independent tiles (64/8 x 256/32).
"forall" = parallel; later lowered to
threads (IREE: workgroups; vendor
backends: their own pass) or a plain loop.
extract_slice %arg0[%2, 0] [8, 128] the matmul tile needs 8 ROWS of A (all of K)...
extract_slice %arg1[0, %3] [128, 32] ...and 32 COLUMNS of B. Tiling math done for you
from the indexing maps (lab 01).
linalg.fill / linalg.matmul 8x32 the producer now computes ONLY this tile.
linalg.generic 8x32 bias+relu consumes it immediately, while hot in cache.
parallel_insert_slice each tile writes its piece of the result.
The 64x256 matmul result is never materialised in memory. Before, it was
written out (64 KiB) and read back by relu. After, each 8x32 tile (1 KiB)
stays in L1/registers. That's FUSION, and on memory-bound kernels (most of
what Triton users write) it's the biggest single win.