[CodeGen][SPIRV] Improved Matmul Tiling Heuristic for VideoCoreVII (#24559)
The generic SPIR-V matmul configuration is not tuned for the VideoCore
VII. Empirically, the heuristic added here yields on-average better
results on ResNet-style workloads by choosing workgroup/thread/reduction
tilings that seem to favor this GPU.
Only matmul ops with 8/16/32-bit element types and 2D/3D iteration
(matmul / batch matmul) are handled; anything else returns `failure()`
and falls through to the default SPIRV configuration.
One that seems to get on average better results with resnet.
diff --git a/compiler/src/iree/compiler/Codegen/SPIRV/VideoCoreConfig.cpp b/compiler/src/iree/compiler/Codegen/SPIRV/VideoCoreConfig.cpp
index 229d85a..4ff8cb5 100644
--- a/compiler/src/iree/compiler/Codegen/SPIRV/VideoCoreConfig.cpp
+++ b/compiler/src/iree/compiler/Codegen/SPIRV/VideoCoreConfig.cpp
@@ -26,6 +26,180 @@
namespace mlir::iree_compiler::detail {
+/// Repeatedly halves `startValue` until it evenly divides `dim`, returning the
+/// result (at least 1). When `startValue` is a power of two this is the largest
+/// power-of-two <= `startValue` that divides `dim`. `startValue` is not
+/// required to be a power of two.
+static int64_t largestDivisorByHalving(int64_t dim, int64_t startValue) {
+ if (dim < 0) {
+ return 1;
+ }
+ while (dim % startValue != 0) {
+ startValue >>= 1;
+ }
+ return std::max(startValue, int64_t(1));
+}
+
+/// Sets a SPIR-V "BaseVectorize" lowering config for a (batch) matmul on the
+/// Broadcom VideoCore VII GPU.
+///
+/// The op is treated as the standard contraction (Bx)MxK * (Bx)KxN = (Bx)MxN,
+/// with the B/M/N/K loop indices. Only static shapes of f32 are handled
+/// everything else will be rejected.
+///
+/// Four levels of tiling are selected:
+/// 1. Workgroup size - aim for about ~256 invocations
+/// 2. Workgroup tile - the M tile is the largest power-of-two dividing M up
+/// to
+/// ~4x the workgroup X count. The N tile takes the rest of a 1024-element
+/// budget. This biases sequential loads toward M.
+/// 3. Thread tile - each invocation handles 1/16th of the workgroup tile in
+/// M and N (at least 1).
+/// 4. Reduction tile - the K loop is vectorized by the largest power-of-two
+/// dividing K, capped at 4.
+LogicalResult
+setMatmulOpVideoCoreConfig(IREE::GPU::TargetAttr target, linalg::LinalgOp op,
+ std::array<int64_t, 2> bestWorkgroupSizeXY,
+ std::array<int64_t, 3> bestThreadTileSizeMNK) {
+ LLVM_DEBUG(llvm::dbgs() << "trying to deduce config as matmul...\n");
+ OpOperand *lhs = op.getDpsInputOperand(0);
+ OpOperand *rhs = op.getDpsInputOperand(1);
+
+ // The following tiling heuristic will ignore any operations that do not
+ // have statically known shapes.
+ auto lhsType = llvm::cast<ShapedType>(lhs->get().getType());
+ auto rhsType = llvm::cast<ShapedType>(rhs->get().getType());
+ // This routine can in principle handle 8/16/32-bit element types, but the
+ // caller (setVideoCoreMatmulConfig) currently restricts inputs to f32.
+ auto elementBits =
+ static_cast<int>(IREE::Util::getTypeBitWidth(lhsType.getElementType()));
+ if (!llvm::is_contained({8, 16, 32}, elementBits)) {
+ return failure();
+ }
+
+ ArrayRef<int64_t> lhsShape = lhsType.getShape();
+ ArrayRef<int64_t> rhsShape = rhsType.getShape();
+ if (llvm::any_of(lhsShape, ShapedType::isDynamic)) {
+ return failure();
+ }
+ if (llvm::any_of(rhsShape, ShapedType::isDynamic)) {
+ return failure();
+ }
+
+ // Ensure that we only focus on batch matmuls or single matmuls, i.e. 2D or 3D
+ assert(llvm::is_contained({2u, 3u}, op.getNumParallelLoops()));
+
+ // Find the loop indices for the B, M, N, K dimensions. We tile the standard
+ // matmul (Bx)MxK * (Bx)KxN = (Bx)MxN, where M is the LHS parallel dim, N the
+ // RHS parallel dim and K the reduction dim (see getMatmulBMNKIndex).
+ int lastParallelDim = -1;
+ const auto [bIndex, mIndex, nIndex, kIndex] =
+ getMatmulBMNKIndex(op, &lastParallelDim);
+ if (mIndex < 0 || nIndex < 0 || kIndex < 0) {
+ return failure();
+ }
+ const bool isBM = bIndex >= 0;
+
+ // Get all the dimension sizes that we need for tiling.
+ SmallVector<int64_t> loopRanges = op.getStaticLoopRanges();
+ const unsigned numLoops = loopRanges.size();
+ const int64_t dimM = loopRanges[mIndex];
+ const int64_t dimN = loopRanges[nIndex];
+ const int64_t dimK = loopRanges[kIndex];
+
+ int64_t bestX = bestWorkgroupSizeXY[0], bestY = bestWorkgroupSizeXY[1];
+ LLVM_DEBUG({
+ llvm::dbgs() << "best thread tile size (M, N, K) = ("
+ << bestThreadTileSizeMNK[0] << ", " << bestThreadTileSizeMNK[1]
+ << ", " << bestThreadTileSizeMNK[2] << ")\n";
+ llvm::dbgs() << "best workgroup size (X, Y) = (" << bestX << ", " << bestY
+ << ")\n";
+ });
+
+ // Aim for ~256 invocations per workgroup, balanced to the output shape: the Y
+ // (N) dimension uses half of dimN capped at 16, and X (M) takes the rest of
+ // the ~256 budget (capped at dimM). These larger workgroups are load-bearing
+ // for the VideoCore VII -- sizing the workgroup down to match the tile
+ // measured several times slower on the V3D.
+ SmallVector<int64_t, 3> workgroupSize(3, 1); // (X, Y, Z)
+ workgroupSize[1] = std::min(dimN >> 1, int64_t(16));
+ workgroupSize[0] =
+ std::min(std::max((bestX * bestY) / workgroupSize[1], int64_t(1)), dimM);
+
+ SmallVector<int64_t> workgroupTileSizes(numLoops, 0);
+ // Batch is simply tiled to 1 for now. Could be improved if batch is
+ // large and the spatial dimensions are small compared to the amount
+ // of available threads.
+ if (isBM) {
+ workgroupTileSizes[bIndex] = 1;
+ }
+ // To maximize the number of elements loaded sequentially we give the M
+ // dimension the larger workgroup tile and hand the rest of the budget (1024
+ // elements total) to the N dimension.
+ workgroupTileSizes[mIndex] =
+ largestDivisorByHalving(dimM, workgroupSize[0] * 4);
+ workgroupTileSizes[nIndex] =
+ largestDivisorByHalving(dimN, 1024 / workgroupTileSizes[mIndex]);
+
+ // Thread Tiling
+ SmallVector<int64_t> threadTileSizes(numLoops, 0);
+ // Batch is simply tiled to 1 for now. Remember workgroup is 1 and tile size
+ // is 1
+ if (isBM) {
+ threadTileSizes[bIndex] = workgroupTileSizes[bIndex] / workgroupSize[2];
+ }
+ // Each thread is given 1/16th of what the workgroup was given. Which should
+ // be skewed in favour of the M dimension to allow for coalesced memory
+ // accesses.
+ threadTileSizes[mIndex] =
+ std::max(workgroupTileSizes[mIndex] / 16, int64_t(1));
+ threadTileSizes[nIndex] =
+ std::max(workgroupTileSizes[nIndex] / 16, int64_t(1));
+
+ // The reduction tiling determines the width of the vector multiply-add in the
+ // inner loop. The largest power-of-two that divides K (so the K loop tiles
+ // evenly), searched downward from maxVectorization and capped at 4.
+ SmallVector<int64_t> reductionTileSizes(numLoops, 0);
+ // 32 is an empirical upper bound from the original tuning. It has no
+ // known correspondence to a specific hardware property.
+ int64_t maxVectorization = 32;
+ reductionTileSizes[kIndex] =
+ std::min(largestDivisorByHalving(dimK, maxVectorization), int64_t(4));
+
+ workgroupTileSizes.resize(lastParallelDim + 1);
+ threadTileSizes.resize(lastParallelDim + 1);
+
+ TileSizesListType tileSizes;
+ llvm::append_values(tileSizes, workgroupTileSizes, threadTileSizes,
+ reductionTileSizes);
+
+ LLVM_DEBUG({
+ llvm::dbgs() << "workgroup size (X, Y, X) = (" << workgroupSize[0] << ", "
+ << workgroupSize[1] << ", " << workgroupSize[2] << ")\n";
+ llvm::dbgs() << "workgroup tiling (M, N) = (" << workgroupTileSizes[mIndex]
+ << ", " << workgroupTileSizes[nIndex] << ")\n";
+ llvm::dbgs() << "thread tiling (M, N) = (" << threadTileSizes[mIndex]
+ << ", " << threadTileSizes[nIndex] << ")\n";
+ llvm::dbgs() << "reduction tiling (M, N, k) = ("
+ << reductionTileSizes[mIndex] << ", "
+ << reductionTileSizes[nIndex] << ','
+ << reductionTileSizes[kIndex] << ")\n";
+ });
+
+ // Sets the workgroup size on the dispatch function and adds the tiling for
+ // the MatMul to the corresponding linalg operation.
+ MLIRContext *ctx = op->getContext();
+ auto config = IREE::Codegen::LoweringConfigAttr::get(ctx, tileSizes);
+ auto pipelineAttr = IREE::GPU::SPIRVPipelineAttr::get(
+ ctx, IREE::GPU::SPIRVLoweringPipeline::BaseVectorize);
+ auto translationInfo = IREE::Codegen::TranslationInfoAttr::get(
+ ctx, pipelineAttr, SymbolRefAttr(), workgroupSize, /*subgroupSize=*/0,
+ DictionaryAttr());
+ auto funcOp = op->getParentOfType<mlir::FunctionOpInterface>();
+ return setOpConfigAndEntryPointFnTranslation(funcOp, op, config,
+ translationInfo);
+}
+
static LogicalResult setVideoCoreMatmulConfig(linalg::LinalgOp op,
IREE::GPU::TargetAttr target) {
auto inputType =
@@ -37,7 +211,7 @@
}
const std::array<int64_t, 2> workgroupXY = {16, 16};
const std::array<int64_t, 3> threadMNK = {4, 4, 4};
- return setMatmulOpConfig(target, op, workgroupXY, threadMNK);
+ return setMatmulOpVideoCoreConfig(target, op, workgroupXY, threadMNK);
}
static int64_t getWorkgroupTiling(int64_t &remainingThreads,
diff --git a/compiler/src/iree/compiler/Codegen/SPIRV/test/config_broadcom_matmul.mlir b/compiler/src/iree/compiler/Codegen/SPIRV/test/config_broadcom_matmul.mlir
index fa28f5f..5d2568a 100644
--- a/compiler/src/iree/compiler/Codegen/SPIRV/test/config_broadcom_matmul.mlir
+++ b/compiler/src/iree/compiler/Codegen/SPIRV/test/config_broadcom_matmul.mlir
@@ -24,7 +24,7 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[64, 64], [4, 4], [0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[64, 16], [4, 1], [0, 0, 4]{{\]}}>
// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 16, 1]>
// CHECK: func.func @matmul_1024x2048x512()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
@@ -57,8 +57,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[64, 8], [1, 4], [0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [2, 64, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[2, 8], [1, 1], [0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [21, 12, 1]>
// CHECK: func.func @matmul_3136x24x96()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.matmul
@@ -91,7 +91,7 @@
}
// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[4, 64], [1, 4], [0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 4, 1]>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 16, 1]>
// CHECK: func.func @matmul_196x64x192()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.matmul
@@ -119,8 +119,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[128, 32], [4, 4], [0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [8, 32, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[64, 16], [4, 1], [0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 16, 1]>
// CHECK: func.func @matmul_12544x96x16()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.matmul
@@ -152,8 +152,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 32], [1, 4], [0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [8, 1, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 32], [1, 2], [0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 16, 1]>
// CHECK: func.func @matmul_49x160x576()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.matmul
@@ -191,8 +191,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[2, 128], [1, 4], [0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [32, 2, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[2, 512], [1, 32], [0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [2, 16, 1]>
// CHECK: func.func @matmul_2x1024x576()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.matmul
@@ -222,7 +222,7 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 64, 64], [1, 4, 4], [0, 0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 64, 16], [1, 4, 1], [0, 0, 0, 4]{{\]}}>
// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 16, 1]>
// CHECK: func.func @batch_matmul_4x384x384()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
@@ -256,8 +256,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 2, 8], [1, 1, 4], [0, 0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [2, 2, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 2, 8], [1, 1, 1], [0, 0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [2, 4, 1]>
// CHECK: func.func @batch_matmul_4x2x8()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.batch_matmul
@@ -298,8 +298,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 8, 64], [1, 1, 4], [0, 0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 8, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 8, 128], [1, 1, 8], [0, 0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [8, 16, 1]>
// CHECK: func.func @generic_batch_matmul_32x2x512()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.generic
@@ -351,8 +351,8 @@
return
}
-// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 4, 64], [1, 1, 4], [0, 0, 0, 4]{{\]}}>
-// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 4, 1]>
+// CHECK-DAG: #[[CONFIG:.+]] = #iree_codegen.lowering_config<tile_sizes = {{\[}}[1, 4, 256], [1, 1, 16], [0, 0, 0, 4]{{\]}}>
+// CHECK-DAG: #[[TRANSLATION:.+]] = #iree_codegen.translation_info<pipeline = #iree_gpu.spirv_pipeline<BaseVectorize> workgroup_size = [16, 16, 1]>
// CHECK: func.func @generic_batch_matmul_8x2500x512x4608()
// CHECK-SAME: translation_info = #[[TRANSLATION]]
// CHECK: linalg.generic