Skip to content

Inconsistent lowering of 3D and 4D matmuls #4610

Description

@rengolin

When lowering KernelBench level1/10_3D_tensor_matrix_multiplication.py and level1/11_4D_tensor_matrix_multiplication.py I get very different lowering styles.

Both kernels implement a similar einsum:

  • 3D: Bmk,kn -> Bmn
  • 4D: Bbmk,kn -> Bbmn

Reproducer

Clone Lighthouse

  • Init uv as in README
  • Run: $ uv run examples/KernelBench/test-kernel-bench.py --kernel level1/10_ --print-original-module
  • Run: $ uv run examples/KernelBench/test-kernel-bench.py --kernel level1/11_ --print-original-module
  • Compare the IRs

3D matmul

The 3D lowering materializes a broadcast of the B tensor to Bkn and then applies a batch_matmul:

  // Broadcast B into an empty
  %bcast = linalg.generic {...} ins(%arg1 : tensor<2048x768xf32>) outs(%empty : tensor<16x2048x768xf32>) {
    ^bb0(%in: f32, %out: f32):
      linalg.yield %in : f32
    } -> tensor<16x2048x768xf32>

  // Batch matmul
  %mm = linalg.batch_matmul ins(%arg0, %bcast : tensor<16x1024x2048xf32>, tensor<16x2048x768xf32>) outs(%5 : tensor<16x1024x768xf32>) -> tensor<16x1024x768xf32>

While this is correct, I now have to elide the broadcast and convert the whole pattern to a contraction with broadcast maps. This could be trivially lowered to linalg.contract with #map2 being a "broadcast-B" map:

  linalg.contract { #map1, #map2, #map3 } ins(%arg0, %arg1 : tensor<16x1024x2048xf32>, tensor<2048x768xf32>) outs(%5 : tensor<16x1024x768xf32>) -> tensor<16x1024x768xf32>

4D matmul

The 4D lowering is even weirder. It collapses the 4D A shape to a 2D (as [0,1,2][3]) and then expands the shape of both B and the collapsed A to add a unit dimension, and then call a batch_matmul on that.

// Shape fiddling
%col_a = tensor.collapse_shape %0 [[0, 1, 2], [3]] : tensor<8x256x512x256xf32> into tensor<1048576x256xf32>
%ex_a = tensor.expand_shape %collapsed [[0, 1], [2]] output_shape [1, 1048576, 256] : tensor<1048576x256xf32> into tensor<1x1048576x256xf32>
%ex_b = tensor.expand_shape %1 [[0, 1], [2]] output_shape [1, 256, 768] : tensor<256x768xf32> into tensor<1x256x768xf32>

// Batch matmul
linalg.batch_matmul ins(%ex_a, %ex_b : tensor<1x1048576x256xf32>, tensor<1x256x768xf32>) outs(%empty : tensor<1x1048576x768xf32>) -> tensor<1x1048576x768xf32>

Again, this would be a trivial linalg.contract pattern, with #map2 being a "broadcast-B" map:

  linalg.contract { #map1, #map2, map3 } ins(%ex_a, %ex_b : tensor<8x256x512x256xf32>, tensor<256x768xf32>) outs(%empty : tensor<8x256x512x768xf32>) -> tensor<8x256x512x768xf32>

Linalg Contract

I understand this pattern has been created well before linalg.contract existed, so it may not be trivial or immediate to convert this lowering pattern. But at least have a common solution to ND (N>2) lowering to either collapse/expand shapes OR materialize broadcasts, not both.

The more patterns we have for this, the harder is the compiler's job to deal with different situations.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions