That is exactly what I am trying to do, split the fuse and tile process into 2 parts:
- First part is just doing an analysis (I called this planning in my original post) and deciding which ops to tile and what tile size to use without any changes to the IR
- Second part is to make the changes in the IR based on the decisions from the analysis.
Sure.
If you consider the following IR
%A = linalg.elemwise_unary ins(%arg0 : tensor<8x16x32xf32>)
outs(%arg0: tensor<8x16x32xf32>) -> tensor<8x16x32xf32>
%B = linalg.elemwise_unary ins(%arg1 : tensor<8x32x14xf32>)
outs(%arg1: tensor<8x32x14xf32>) -> tensor<8x32x14xf32>
%1 = linalg.batch_matmul ins(%A , %B: tensor<8x16x32xf32>, tensor<8x32x14xf32>)
outs(%C : tensor<8x16x14xf32>) -> tensor<8x16x14xf32>
There is several different possibilities regarding how to construct the loop nesting if we would like to tile and fuse these ops.
One option could be to tile all 3 dimensions of the linalg.batch_matmul output, such a decisions could lead to the following IR:
%0 = scf.for %arg3 = %c0 to %c8 step %c1 iter_args(%arg4 = %arg2) -> (tensor<8x16x14xf32>) {
%1 = scf.for %arg5 = %c0 to %c16 step %c4 iter_args(%arg6 = %arg4) -> (tensor<8x16x14xf32>) {
%2 = scf.for %arg7 = %c0 to %c14 step %c7 iter_args(%arg8 = %arg6) -> (tensor<8x16x14xf32>) {
%extracted_slice = tensor.extract_slice %arg0[%arg3, %arg5, 0] [1, 4, 32] [1, 1, 1] : tensor<8x16x32xf32> to tensor<1x4x32xf32>
%extracted_slice_0 = tensor.extract_slice %arg0[%arg3, %arg5, 0] [1, 4, 32] [1, 1, 1] : tensor<8x16x32xf32> to tensor<1x4x32xf32>
%3 = linalg.elemwise_unary ins(%extracted_slice : tensor<1x4x32xf32>) outs(%extracted_slice_0 : tensor<1x4x32xf32>) -> tensor<1x4x32xf32>
%extracted_slice_1 = tensor.extract_slice %arg1[%arg3, 0, %arg7] [1, 32, 7] [1, 1, 1] : tensor<8x32x14xf32> to tensor<1x32x7xf32>
%extracted_slice_2 = tensor.extract_slice %arg1[%arg3, 0, %arg7] [1, 32, 7] [1, 1, 1] : tensor<8x32x14xf32> to tensor<1x32x7xf32>
%4 = linalg.elemwise_unary ins(%extracted_slice_1 : tensor<1x32x7xf32>) outs(%extracted_slice_2 : tensor<1x32x7xf32>) -> tensor<1x32x7xf32>
%extracted_slice_3 = tensor.extract_slice %arg8[%arg3, %arg5, %arg7] [1, 4, 7] [1, 1, 1] : tensor<8x16x14xf32> to tensor<1x4x7xf32>
%5 = linalg.batch_matmul ins(%3, %4 : tensor<1x4x32xf32>, tensor<1x32x7xf32>) outs(%extracted_slice_3 : tensor<1x4x7xf32>) -> tensor<1x4x7xf32>
%inserted_slice = tensor.insert_slice %5 into %arg8[%arg3, %arg5, %arg7] [1, 4, 7] [1, 1, 1] : tensor<1x4x7xf32> into tensor<8x16x14xf32>
scf.yield %inserted_slice : tensor<8x16x14xf32>
}
scf.yield %2 : tensor<8x16x14xf32>
}
scf.yield %1 : tensor<8x16x14xf32>
}
When looking at the produced IR you can see that the linalg.batch_matmul uses all of the induction variables, but that each of the linalg.elemwise_unary uses only 2 of the induction variables. This could lead to each of linalg.elemwise_unary performing recomputes. The first one would be computed twice since it doesn’t use the inner loop induction variable, the second one would be computed four times since it doesn’t use the middle loop induction variable.
If we want to avoid such recomputations, one way to do this could be to tile only the first dimension of the linalg.batch_matmul output.
That could lead us to the following IR:
%0 = scf.for %arg3 = %c0 to %c8 step %c1 iter_args(%arg4 = %arg2) -> (tensor<8x16x14xf32>) {
%extracted_slice = tensor.extract_slice %arg0[%arg3, 0, 0] [1, 16, 32] [1, 1, 1] : tensor<8x16x32xf32> to tensor<1x16x32xf32>
%extracted_slice_0 = tensor.extract_slice %arg0[%arg3, 0, 0] [1, 16, 32] [1, 1, 1] : tensor<8x16x32xf32> to tensor<1x16x32xf32>
%1 = linalg.elemwise_unary ins(%extracted_slice : tensor<1x16x32xf32>) outs(%extracted_slice_0 : tensor<1x16x32xf32>) -> tensor<1x16x32xf32>
%extracted_slice_1 = tensor.extract_slice %arg1[%arg3, 0, 0] [1, 32, 14] [1, 1, 1] : tensor<8x32x14xf32> to tensor<1x32x14xf32>
%extracted_slice_2 = tensor.extract_slice %arg1[%arg3, 0, 0] [1, 32, 14] [1, 1, 1] : tensor<8x32x14xf32> to tensor<1x32x14xf32>
%2 = linalg.elemwise_unary ins(%extracted_slice_1 : tensor<1x32x14xf32>) outs(%extracted_slice_2 : tensor<1x32x14xf32>) -> tensor<1x32x14xf32>
%extracted_slice_3 = tensor.extract_slice %arg4[%arg3, 0, 0] [1, 16, 14] [1, 1, 1] : tensor<8x16x14xf32> to tensor<1x16x14xf32>
%3 = linalg.batch_matmul ins(%1, %2 : tensor<1x16x32xf32>, tensor<1x32x14xf32>) outs(%extracted_slice_3 : tensor<1x16x14xf32>) -> tensor<1x16x14xf32>
%inserted_slice = tensor.insert_slice %3 into %arg4[%arg3, 0, 0] [1, 16, 14] [1, 1, 1] : tensor<1x16x14xf32> into tensor<8x16x14xf32>
scf.yield %inserted_slice : tensor<8x16x14xf32>
}
I prefer that the outcome of tile and fuse for these ops will be the second option and am trying to find the right tools the achieve that outcome.
One way to achieve it could be to try and tile all the dimensions of the output of the linalg.batch_matmul and then look at the generated IR (the first example) and see which induction variables are used by which ops. Such an analysis would lead to understanding that only the induction variable of the outermost loop is used by all operations. Given that information, I could erase/rollback the generated IR and then tile the original ops again only on the first dimension of the output.
But, I’m trying to achieve this outcome without making any changes to the IR and then analyzing to outcome. Instead I would like to understand how the outcome would look just from analyzing the connections between operation values and operation iteration domain. linalg dialect seems to provide a way to do that via the mapIterationSpaceDimToAllOperandDims interface method and another one or two methods. If I use the information from this mapping for all the ops in the example I could understand that only the outer iteration dimension of the matmul is also an iteration dimension for the other 2 operations, and that each of the two inner iteration dimensions of the matmul is used only by one of the other operations.
Currently using this approach is limited to pretiling analysis for linalg dialect operations only, but if such mapping method/s would be added as part of the tiling interface then the approach could be used for all operations that implement the interface.