| // Copyright 2020 The IREE Authors |
| // |
| // Licensed under the Apache License v2.0 with LLVM Exceptions. |
| // See https://llvm.org/LICENSE.txt for license information. |
| // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| |
| #include "iree/compiler/Codegen/LLVMCPU/KernelDispatch.h" |
| |
| #include "iree/compiler/Codegen/Common/TileSizeSelection.h" |
| #include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenAttrs.h" |
| #include "iree/compiler/Codegen/Interfaces/PartitionableLoopsInterface.h" |
| #include "iree/compiler/Codegen/LLVMCPU/TargetMLTransformInfo.h" |
| #include "iree/compiler/Codegen/LLVMCPU/Utils.h" |
| #include "iree/compiler/Codegen/Utils/CPUUtils.h" |
| #include "iree/compiler/Codegen/Utils/LinalgOpInfo.h" |
| #include "iree/compiler/Codegen/Utils/Utils.h" |
| #include "iree/compiler/Dialect/HAL/IR/HALTypes.h" |
| #include "iree/compiler/Dialect/LinalgExt/IR/LinalgExtOps.h" |
| #include "iree/compiler/Dialect/LinalgExt/Utils/IndexingUtils.h" |
| #include "llvm/ADT/SmallVectorExtras.h" |
| #include "llvm/ADT/TypeSwitch.h" |
| #include "llvm/Support/CommandLine.h" |
| #include "llvm/Support/Debug.h" |
| #include "llvm/Support/MathExtras.h" |
| #include "mlir/Dialect/Linalg/IR/Linalg.h" |
| #include "mlir/Dialect/Linalg/IR/LinalgInterfaces.h" |
| #include "mlir/Dialect/Linalg/Transforms/Transforms.h" |
| #include "mlir/Dialect/MemRef/IR/MemRef.h" |
| #include "mlir/Dialect/MemRef/Transforms/Transforms.h" |
| #include "mlir/Dialect/Utils/IndexingUtils.h" |
| #include "mlir/Dialect/Utils/StaticValueUtils.h" |
| #include "mlir/IR/Matchers.h" |
| #include "mlir/IR/OpDefinition.h" |
| #include "mlir/IR/TypeUtilities.h" |
| #include "mlir/Interfaces/FunctionInterfaces.h" |
| #include "mlir/Transforms/GreedyPatternRewriteDriver.h" |
| |
| #include <numeric> |
| |
| #define DEBUG_TYPE "kernel-dispatch" |
| #define KD_DBGS() (llvm::dbgs() << '[' << DEBUG_TYPE << "] ") |
| |
| namespace mlir::iree_compiler { |
| |
| /// NOTE: None of these flags are supported in any form long term. This are |
| /// temporary hooks added for development purposes. They could be |
| /// changed/modified at any time. |
| /// TODO: Find a way to plumb this through to not rely on these flags. |
| |
| static llvm::cl::opt<int> clNumberOfRuntimeThreads( |
| "iree-llvmcpu-number-of-threads", |
| llvm::cl::desc("number of threads that are used at runtime if codegen " |
| "thread distribution is enabled"), |
| llvm::cl::init(8)); |
| |
| static llvm::cl::opt<bool> clDisableDistribution( |
| "iree-llvmcpu-disable-distribution", |
| llvm::cl::desc("disable thread distribution in codegen"), |
| llvm::cl::init(false)); |
| |
| static llvm::cl::opt<int> |
| clDefaultDistTileSize("iree-llvmcpu-distribution-size", |
| llvm::cl::desc("default distribution tile size"), |
| llvm::cl::init(64)); |
| |
| static llvm::cl::opt<int> clNarrowMatmulTileBytes( |
| "iree-llvmcpu-narrow-matmul-tile-bytes", |
| llvm::cl::desc( |
| "target distribution tile size for wide matrix operand of narrow " |
| "matmuls, expressed in bytes. Currently only used in data-tiled " |
| "matmuls (mmt4d). Since this is only used for narrow matmuls, which " |
| "traverse their wide matrix operand once, there is no reuse here and " |
| "this doesn't have to be sized to fit in some CPU cache. This is more " |
| "about distributing work to threads."), |
| llvm::cl::init(64 * 1024)); |
| |
| static llvm::cl::opt<int> clGeneralMatmulTileBytes( |
| "iree-llvmcpu-general-matmul-tile-bytes", |
| llvm::cl::desc("target distribution tile size for matrix operands of " |
| "general matmuls, expressed in bytes. Currently only used " |
| "in data-tiled matmuls (mmt4d)."), |
| llvm::cl::init(64 * 1024)); |
| |
| static llvm::cl::opt<bool> clDisableVectorPeeling( |
| "iree-llvmcpu-disable-vector-peeling", |
| llvm::cl::desc("Disable peeling as a pre-processing step for " |
| "vectorization (only relevant when using compiler " |
| "heuristics to select the strategy)."), |
| llvm::cl::init(false)); |
| |
| static llvm::cl::opt<bool> clEnableScalableVectorization( |
| "iree-llvmcpu-enable-scalable-vectorization", |
| llvm::cl::desc("Enable scalable vectorization if it is supported by the " |
| "target (e.g., +sve, +sve2 and/or +sme feature flags)"), |
| llvm::cl::init(false)); |
| |
| static llvm::cl::opt<bool> clDisableArmSMETiling( |
| "iree-llvmcpu-disable-arm-sme-tiling", |
| llvm::cl::desc("Disables tiling for SME even if it is supported by the " |
| "target (i.e., when the +sme feature flag is present)"), |
| llvm::cl::init(false)); |
| |
| static llvm::cl::opt<bool> clEnableRiscvAggressiveDist( |
| "iree-llvmcpu-riscv-aggressive-distribution", |
| llvm::cl::desc( |
| "Enable aggressive method for distribution tile size. " |
| "It is only applied for linalg contraction ops now. " |
| "If distConfig.minTileSizes[i] >= distConfig.maxTileSizes[i], " |
| "set distConfig.maxTileSizes[i] to 2 * distConfig.minTileSizes[i]."), |
| llvm::cl::init(false)); |
| |
| using IREE::Codegen::DispatchLoweringPassPipeline; |
| |
| // Encodes the pre-processing strategy to be applied on a Linalg operation |
| // before vectorization. |
| enum class VectorPreProcStrategy { |
| // Peel iterations from the vector dimensions so that they become multiple of |
| // the vector length. |
| Peeling, |
| // Compute vector dimensions assuming vector masking support. Vector sizes may |
| // be rounded up to the nearest power of two and out-of-bounds elements would |
| // be masked-out. |
| Masking, |
| // Do not apply any vectorization pre-processing transformation. |
| None |
| }; |
| |
| // Use this flag to override IREE's heuristics for selecting the pre-processing |
| // strategy. |
| static llvm::cl::opt<VectorPreProcStrategy> clPProcStrategy( |
| "iree-llvmcpu-vector-pproc-strategy", |
| llvm::cl::desc("Set the strategy for pre-processing Linalg operation " |
| "before vectorization:"), |
| llvm::cl::values( |
| clEnumValN(VectorPreProcStrategy::Peeling, "peel", |
| "Peel iterations from the vector dimensions so that they " |
| "become multiple of the vector length"), |
| clEnumValN( |
| VectorPreProcStrategy::Masking, "mask", |
| " Compute vector dimensions assuming vector masking support. " |
| "Vector sizes may be rounded up to the nearest power of two " |
| "and out-of-bounds elements would be masked-out."), |
| clEnumValN( |
| VectorPreProcStrategy::None, "none", |
| "Do not apply any vectorization pre-processing transformation."))); |
| |
| // TODO(dcaballe): Move operator<< to DebugUtils.h. |
| static llvm::raw_ostream &operator<<(llvm::raw_ostream &os, |
| const VectorPreProcStrategy &strategy) { |
| switch (strategy) { |
| case VectorPreProcStrategy::Peeling: |
| os << "Peeling"; |
| break; |
| case VectorPreProcStrategy::Masking: |
| os << "Masking"; |
| break; |
| case VectorPreProcStrategy::None: |
| os << "None"; |
| break; |
| } |
| return os; |
| } |
| |
| template <typename T> |
| static llvm::raw_ostream &operator<<(llvm::raw_ostream &os, |
| const llvm::SmallVectorImpl<T> &vector) { |
| for (T element : vector) { |
| os << element << " "; |
| } |
| |
| return os; |
| } |
| |
| static llvm::raw_ostream & |
| operator<<(llvm::raw_ostream &os, |
| const mlir::iree_compiler::TileSizesListType &tileSizeList) { |
| os << "["; |
| for (auto &tuple : tileSizeList) { |
| os << "[" << tuple << "]"; |
| } |
| os << "]"; |
| |
| return os; |
| } |
| |
| /// Splits the iteration ranges from `op` and returns the `lbs` and the `ubs` as |
| /// separate lists. |
| static void getRangeBounds(TilingInterface op, SmallVectorImpl<int64_t> &lb, |
| SmallVectorImpl<int64_t> &ub) { |
| OpBuilder builder(op.getContext()); |
| builder.setInsertionPoint(op); |
| SmallVector<Range> loopRange = op.getIterationDomain(builder); |
| auto getStaticValue = [](OpFoldResult ofr) -> int64_t { |
| std::optional<int64_t> intVal = getConstantIntValue(ofr); |
| if (!intVal) |
| return ShapedType::kDynamic; |
| return intVal.value(); |
| }; |
| lb = llvm::map_to_vector(loopRange, |
| [&](Range r) { return getStaticValue(r.offset); }); |
| ub = llvm::map_to_vector(loopRange, |
| [&](Range r) { return getStaticValue(r.size); }); |
| } |
| |
| /// Returns the vectorization pre-processing strategy (peeling, masking) for the |
| /// given LinalgOp. It is based on either (in the priority order): |
| /// * user-specified value, or |
| /// * IREE's heuristics (e.g. the op traits and the target architecture). |
| static VectorPreProcStrategy |
| getVectorPreProcStrategy(linalg::LinalgOp linalgOp) { |
| // If set, use the strategy selected by a user. |
| if (clPProcStrategy.getNumOccurrences()) { |
| return clPProcStrategy; |
| } |
| |
| // TODO: Implement heuristics for Convs |
| if (isa<linalg::ConvolutionOpInterface>(linalgOp.getOperation())) { |
| return VectorPreProcStrategy::None; |
| } |
| |
| // Select a strategy based on heuristics. |
| if (linalgOp.hasPureBufferSemantics()) { |
| return VectorPreProcStrategy::None; |
| } |
| |
| // Walk the linalgOp code, if there is any instruction that could result in |
| // undefined behavior in mask strategy, fall back to using peel strategy. |
| bool usePeelingStrategy = false; |
| linalgOp.walk([&](Operation *op) -> WalkResult { |
| if (mlir::iree_compiler::mayHaveUndefinedBehaviorInMasking(op)) { |
| usePeelingStrategy = true; |
| return WalkResult::interrupt(); |
| } |
| return WalkResult::advance(); |
| }); |
| if (usePeelingStrategy) { |
| return VectorPreProcStrategy::Peeling; |
| } |
| |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(linalgOp); |
| bool isLinalgGeneric = isa<linalg::GenericOp>(linalgOp.getOperation()); |
| |
| // Default X86 specific strategy. |
| if (isX86(targetAttr)) { |
| if (isLinalgGeneric) { |
| return VectorPreProcStrategy::Masking; |
| } |
| |
| if (!clDisableVectorPeeling) { |
| return VectorPreProcStrategy::Peeling; |
| } |
| } |
| |
| // Default RISC-V specific strategies. |
| if (isRISCV(targetAttr)) { |
| if (isLinalgGeneric) { |
| return VectorPreProcStrategy::Masking; |
| } |
| |
| if (!clDisableVectorPeeling) { |
| return VectorPreProcStrategy::Peeling; |
| } |
| } |
| |
| // Default AArch64 specific strategies. |
| if (isAArch64(targetAttr)) { |
| if (clEnableScalableVectorization && hasAnySVEFeature(targetAttr)) { |
| return VectorPreProcStrategy::Masking; |
| } |
| |
| if (!clDisableVectorPeeling) { |
| return VectorPreProcStrategy::Peeling; |
| } |
| } |
| |
| return VectorPreProcStrategy::None; |
| } |
| |
| static DictionaryAttr getPipelineConfWithPeelingAttr(MLIRContext *context) { |
| auto enableLoopPeelingAttrName = getEnableLoopPeelingAttrName(context); |
| auto unitAttr = UnitAttr::get(context); |
| |
| return DictionaryAttr::get( |
| context, ArrayRef<NamedAttribute>({enableLoopPeelingAttrName, unitAttr})); |
| } |
| |
| static DictionaryAttr |
| getPipelineConfWithDecompositionAttr(MLIRContext *context) { |
| auto attrName = getEnableDecompositionAttrName(context); |
| auto unitAttr = UnitAttr::get(context); |
| return DictionaryAttr::get(context, |
| ArrayRef<NamedAttribute>({attrName, unitAttr})); |
| } |
| |
| /// Looks for the `native_vector_size` attribute in the hal.executable.target |
| /// looked up from this op. |
| static int64_t |
| getNativeVectorSizeInBytes(mlir::FunctionOpInterface entryPointFn) { |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| auto nativeVectorSizeAttr = |
| getConfigIntegerAttr(targetAttr, "native_vector_size"); |
| if (nativeVectorSizeAttr) { |
| int64_t nativeVectorSizeVal = nativeVectorSizeAttr->getInt(); |
| if (nativeVectorSizeVal) { |
| return nativeVectorSizeVal; |
| } |
| } |
| |
| assert(0 && "Missing 'native_vector_size' attribute"); |
| return 0; |
| } |
| |
| /// For a given `shapedType` or (`byteWidth` of element type) return the number |
| /// of elements that correspond to the native vector size. Returns 1 as the |
| /// fallback. |
| static int64_t getVectorSize(mlir::FunctionOpInterface entryPointFn, |
| unsigned byteWidth) { |
| return getNativeVectorSizeInBytes(entryPointFn) / byteWidth; |
| } |
| static int64_t getVectorSize(mlir::FunctionOpInterface entryPointFn, |
| ShapedType shapedType) { |
| Type elementType = shapedType.getElementType(); |
| if (!elementType.isIntOrFloat()) |
| return 1; |
| unsigned byteWidth = IREE::Util::getRoundedElementByteWidth(elementType); |
| return getVectorSize(entryPointFn, byteWidth); |
| } |
| |
| /// Returns true if the operation is a GenericOp implementing a supported |
| /// transposition: |
| /// 1. The op has a single input and a single output. |
| /// 2. One of the indexing_map is identity and the other is a permutation. |
| static bool x86TransposeLoweringPrecondition(linalg::GenericOp genericOp) { |
| // Check that the op has at least 2 dimensions. |
| if (genericOp.getNumLoops() < 2) { |
| return false; |
| } |
| |
| // Check that the op has only one input and one output. |
| // TODO(diegocaballero): Generalize to multiple inputs. |
| if ((genericOp.getNumDpsInputs() != 1) || (genericOp.getNumDpsInits() != 1)) { |
| return false; |
| } |
| |
| // Check that all the iterators are parallel. |
| if (genericOp.getNumParallelLoops() != genericOp.getNumLoops()) { |
| return false; |
| } |
| |
| // Check that the two indexing maps are a permutation of each other. |
| auto indexingMaps = genericOp.getIndexingMapsArray(); |
| return !indexingMaps[0].isEmpty() && !indexingMaps[1].isEmpty() && |
| ((indexingMaps[0].isIdentity() && !indexingMaps[1].isIdentity() && |
| indexingMaps[1].isPermutation()) || |
| (!indexingMaps[0].isIdentity() && indexingMaps[0].isPermutation() && |
| indexingMaps[1].isIdentity())); |
| } |
| |
| /// Returns minimum tiling sizes for each dimension. One dimension is possible |
| /// to access at different element types. It determines the tiling sizes by |
| /// looking into all the operands. |
| // TODO(diegocaballero): Refactor this logic to a method that computes the final |
| // tile sizes for vectorization/unrolling in one shot. |
| static SmallVector<int64_t> |
| getMinTilingSizesForEachDim(mlir::FunctionOpInterface entryPointFn, |
| linalg::LinalgOp op, |
| const LinalgOpInfo &linalgOpInfo, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| unsigned numLoops = op.getNumLoops(); |
| SmallVector<int64_t> minTileSizes(numLoops, 1); |
| auto inputOutputOpOperands = op->getOpOperands(); |
| |
| for (auto [index, map] : llvm::enumerate(op.getIndexingMapsArray())) { |
| // Check the fastest varying dimension of the operand. Set the vector size |
| // of the corresponding loop to the vector size. |
| if (map.getNumResults() == 0) |
| continue; |
| auto fastestVaryingDimExpr = |
| dyn_cast<AffineDimExpr>(map.getResults().back()); |
| if (!fastestVaryingDimExpr) |
| continue; |
| unsigned fastestVaryingDim = fastestVaryingDimExpr.getPosition(); |
| |
| // If the indexing map has result it has to be a shaped type. |
| auto operandType = |
| llvm::cast<ShapedType>(inputOutputOpOperands[index].get().getType()); |
| int64_t tileSize = getVectorSize(entryPointFn, operandType); |
| |
| minTileSizes[fastestVaryingDim] = |
| std::max<int64_t>(minTileSizes[fastestVaryingDim], tileSize); |
| } |
| |
| // Limit unroll factor. For now, we assume the rightmost non-one tiled |
| // dimension is for vectorization and any other non-one dimension is for |
| // unrolling. The util limits the second rightmost non-one tiled dimension |
| // to be not larger than `maxUnrollFactor` and others tiled dimension to 1. |
| auto limitUnrollFactor = [&](int64_t maxUnrollFactor) { |
| int vecDim; |
| for (vecDim = minTileSizes.size() - 1; vecDim >= 0; --vecDim) { |
| if (minTileSizes[vecDim] > 1) { |
| break; |
| } |
| } |
| bool seen = false; |
| for (int unrollDim = vecDim - 1; unrollDim >= 0; --unrollDim) { |
| if (minTileSizes[unrollDim] <= 1) { |
| continue; |
| } |
| int64_t factor = seen ? 1LL : maxUnrollFactor; |
| seen = true; |
| LLVM_DEBUG(KD_DBGS() << "Adjusted min tile sizes: " |
| << minTileSizes[unrollDim] |
| << " with factor=" << factor << "\n"); |
| minTileSizes[unrollDim] = |
| std::min<int64_t>(minTileSizes[unrollDim], factor); |
| } |
| }; |
| |
| auto genericOp = dyn_cast<linalg::GenericOp>(op.getOperation()); |
| if (linalgOpInfo.isTranspose() && genericOp && |
| x86TransposeLoweringPrecondition(genericOp)) { |
| // TODO(dcaballe): Consider input and output transposes. |
| limitUnrollFactor(targetMLTransInfo.defaultMaxTransposeUnrollFactor); |
| } else if (linalgOpInfo.isReduction()) { |
| limitUnrollFactor(targetMLTransInfo.defaultMaxReductionUnrollFactor); |
| } else { |
| limitUnrollFactor(targetMLTransInfo.defaultMaxElementwiseUnrollFactor); |
| } |
| |
| return minTileSizes; |
| } |
| |
| // Reduces the number of workgroups in cases where we are dividing the work too |
| // much. Over-provision the number of workgroups to twice the number of |
| // threads. |
| static void reduceDistributionWorkgroups( |
| ArrayRef<int64_t> workload, SmallVectorImpl<int64_t> &distributedTileSizes, |
| std::optional<ArrayRef<int64_t>> maxTileSizes = std::nullopt, |
| std::optional<ArrayRef<int64_t>> vectorSizeHints = std::nullopt) { |
| assert(workload.size() == distributedTileSizes.size()); |
| SmallVector<int64_t> numWorkgroupsPerDim(workload.size(), 1); |
| for (auto [idx, value] : llvm::enumerate(workload)) { |
| if (distributedTileSizes[idx] == 0 || ShapedType::isDynamic(value)) { |
| continue; |
| } |
| numWorkgroupsPerDim[idx] = |
| llvm::divideCeil(value, distributedTileSizes[idx]); |
| } |
| |
| int64_t numWorkgroupsLimit = 2 * clNumberOfRuntimeThreads; |
| int64_t numWorkgroups = |
| std::accumulate(numWorkgroupsPerDim.begin(), numWorkgroupsPerDim.end(), |
| 1LL, std::multiplies<int64_t>{}); |
| unsigned currDim = workload.size(); |
| while (numWorkgroups > numWorkgroupsLimit && currDim > 0) { |
| unsigned index = currDim - 1; |
| int64_t currSize = distributedTileSizes[index]; |
| if (ShapedType::isDynamic(workload[index]) || currSize == 0 || |
| (maxTileSizes && currSize >= maxTileSizes.value()[index]) || |
| currSize >= workload[index]) { |
| currDim--; |
| continue; |
| } |
| |
| int64_t newSize = std::min<int64_t>(currSize * 2, workload[index]); |
| int64_t vectorSize = vectorSizeHints ? vectorSizeHints.value()[index] : 0; |
| |
| // Chech if it's the ideal size with vector size hint. And skip if the new |
| // size will break the ideal size. |
| if (vectorSize > 1 && |
| (currSize % vectorSize == 0 && workload[index] % currSize == 0) && |
| (newSize % vectorSize != 0 || workload[index] % newSize != 0)) { |
| currDim--; |
| continue; |
| } |
| |
| distributedTileSizes[index] = newSize; |
| int64_t nwg = |
| llvm::divideCeil(workload[index], distributedTileSizes[index]); |
| if (nwg < numWorkgroupsPerDim[index]) { |
| numWorkgroups /= numWorkgroupsPerDim[index]; |
| numWorkgroupsPerDim[index] = nwg; |
| numWorkgroups *= nwg; |
| } else { |
| currDim--; |
| } |
| } |
| |
| // Final fixup for dividing workload evenly. |
| for (auto i : llvm::seq<unsigned>(0, distributedTileSizes.size())) { |
| if (distributedTileSizes[i] == 0 || ShapedType::isDynamic(workload[i])) { |
| continue; |
| } |
| |
| int64_t nwg = llvm::divideCeil(workload[i], distributedTileSizes[i]); |
| int64_t newSize = llvm::divideCeil(workload[i], nwg); |
| |
| // Chech if it's the ideal size with vector size hint. And skip if the new |
| // size will break the ideal size. |
| int64_t vectorSize = vectorSizeHints ? vectorSizeHints.value()[i] : 0; |
| if (vectorSize > 1 && |
| (newSize % vectorSize != 0 || workload[i] % newSize != 0)) { |
| continue; |
| } |
| |
| distributedTileSizes[i] = newSize; |
| } |
| } |
| |
| /// Returns the default tile sizes to use for the loops that are distributed. |
| static SmallVector<int64_t> |
| getDefaultDistributionTileSizes(ArrayRef<int64_t> lbs, ArrayRef<int64_t> ubs, |
| ArrayRef<int64_t> minTileSizes, |
| ArrayRef<int64_t> maxTileSizes, |
| ArrayRef<int64_t> vectorSizeHints) { |
| assert(lbs.size() == ubs.size() && lbs.size() == minTileSizes.size() && |
| lbs.size() == maxTileSizes.size() && |
| "expected all vectors to be of equal size"); |
| |
| size_t numDims = lbs.size(); |
| // Set all the distribution tile sizes to zero if thread distribution is |
| // disabled. |
| if (clDisableDistribution) { |
| return SmallVector<int64_t>(numDims, 0); |
| } |
| |
| SmallVector<int64_t> distributedTileSizes(numDims, 1); |
| SmallVector<int64_t> workload(numDims, 1); |
| for (auto i : llvm::seq<size_t>(0, numDims)) { |
| if (maxTileSizes[i] == 0 || ShapedType::isDynamic(lbs[i]) || |
| ShapedType::isDynamic(ubs[i])) { |
| distributedTileSizes[i] = maxTileSizes[i]; |
| workload[i] = ShapedType::kDynamic; |
| continue; |
| } |
| |
| assert(lbs[i] <= ubs[i]); |
| workload[i] = ubs[i] - lbs[i]; |
| int64_t candidateTileSize = 1; |
| int64_t targetSize = |
| std::min(workload[i] / clNumberOfRuntimeThreads, maxTileSizes[i]); |
| int64_t vectorSize = vectorSizeHints[i]; |
| if (vectorSize > 1) { |
| // Pick the factor of dim which is closest to the target tile size and |
| // is a multiplier of vector size. |
| for (int64_t k = vectorSize; k <= targetSize; k += vectorSize) { |
| if (workload[i] % k == 0 && k >= minTileSizes[i]) { |
| candidateTileSize = k; |
| } |
| } |
| } |
| // Fallback to power of 2 if there's no hint or can't find the ideal size. |
| if (vectorSize <= 1 || candidateTileSize == 1) { |
| candidateTileSize = std::max<int64_t>( |
| llvm::bit_floor<uint64_t>(targetSize), minTileSizes[i]); |
| } |
| |
| // Limit the workload per workgroup to the default being the max to keep the |
| // work per invocation reasonable. |
| distributedTileSizes[i] = |
| std::min<int64_t>(candidateTileSize, maxTileSizes[i]); |
| } |
| |
| reduceDistributionWorkgroups(workload, distributedTileSizes, maxTileSizes, |
| vectorSizeHints); |
| |
| return distributedTileSizes; |
| } |
| |
| /// Returns the nearest power of two of `size` if `predicate` is true. |
| /// Otherwise, returns `size`. |
| static int64_t roundUpToPow2(int64_t size, bool predicate) { |
| if (!predicate) { |
| return size; |
| } |
| assert(size > 0 && "Negative size"); |
| return llvm::PowerOf2Ceil(size); |
| } |
| |
| /// Computes the maximum tile size that can be used to distribute a dimension |
| /// based on its number of iterations and the native vector size used of the |
| /// target. The resulting tile size will be a multiple of the provided vector |
| /// size, except when `allowIncompleteTile` is set to true. |
| static int64_t getMaxDistributionTileSize(int64_t lb, int64_t ub, |
| int64_t maxSize, int64_t vectorSize, |
| bool allowIncompleteTile = false) { |
| if (ShapedType::isDynamic(ub) || ShapedType::isDynamic(lb)) { |
| return maxSize; |
| } |
| int64_t numIters = ub - lb; |
| if (numIters <= maxSize && numIters < vectorSize) { |
| return numIters; |
| } |
| |
| int64_t scaledUB = std::min(maxSize, numIters) / vectorSize * vectorSize; |
| for (int64_t i = scaledUB; i > 0; i -= vectorSize) { |
| if (numIters % i == 0) { |
| return i; |
| } |
| } |
| if (allowIncompleteTile) { |
| // Set bound to half to avoid too many workgroup. |
| int64_t start = std::min(maxSize, numIters); |
| int64_t end = start / 2; |
| for (int64_t i = start; i >= end; --i) { |
| if (numIters % i == 0) { |
| return i; |
| } |
| } |
| return maxSize; |
| } |
| // If it can't be a multiple of `vectorSize`, let's choose a factor of |
| // `numIters` sizes heuristically. |
| int64_t start = std::min(maxSize, numIters); |
| for (int64_t i = start; i > 0; --i) { |
| if (numIters % i == 0) { |
| return i; |
| } |
| } |
| return 1; |
| } |
| |
| /// Computes the maximum tile size that can be used to vectorize (or unroll) a |
| /// dimension based on its number of elements and the native vector size of |
| /// the target. If `enforcePowerOfTwo` is set to true, the resulting tile size |
| /// will be a power of two. |
| static int64_t getMaxVectorTileSize(int64_t numElem, int64_t tileSize, |
| int64_t vectorSize, |
| bool enforcePowerOfTwo = false) { |
| if (ShapedType::isDynamic(numElem)) { |
| return roundUpToPow2(tileSize, enforcePowerOfTwo); |
| } |
| if (numElem <= tileSize && numElem < vectorSize) { |
| return roundUpToPow2(numElem, enforcePowerOfTwo); |
| } |
| |
| // Return the largest suitable power of two if power of two is enforced. |
| if (enforcePowerOfTwo) { |
| return roundUpToPow2(std::min(tileSize, numElem), enforcePowerOfTwo); |
| } |
| |
| // Try to find a tile size that is multiple of the vector size. |
| int64_t scaledUB = std::min(tileSize, numElem) / vectorSize * vectorSize; |
| for (int64_t i = scaledUB; i > 0; i -= vectorSize) { |
| if (numElem % i == 0) { |
| return i; |
| } |
| } |
| |
| // If it can't be a multiple of `vectorSize`, let's choose a factor of |
| // `numElem` sizes heuristically. |
| int64_t start = std::min(tileSize, numElem); |
| for (int64_t i = start; i > 0; --i) { |
| if (numElem % i == 0) { |
| return i; |
| } |
| } |
| |
| return 1; |
| } |
| |
| // Returns true if `map` is a function of `dim` and is not a function of any |
| // more inner dimensions past `dim`. |
| static bool isInnerMostDimThatMapIsFunctionOf(AffineMap map, int dim) { |
| if (!map.isFunctionOfDim(dim)) { |
| return false; |
| } |
| for (int d = dim + 1; d < map.getNumDims(); ++d) { |
| if (map.isFunctionOfDim(d)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| // Clamps in-place `vecTileSizes`, ensuring that the resulting vector tile sizes |
| // for each opearand of `op` satisfy two requirements: |
| // 1. No resulting operand tile size exceeds `eachOperandMaxTileBits`. |
| // 2. The sum of all resulting operand tile size does not exceed |
| // `allOperandsMaxTileBits`. |
| static void limitVectorTileSizes(linalg::LinalgOp op, |
| SmallVectorImpl<int64_t> &vecTileSizes, |
| int64_t eachOperandMaxTileBits, |
| int64_t allOperandsMaxTileBits) { |
| int numLoops = op.getNumLoops(); |
| assert(numLoops == vecTileSizes.size()); |
| auto indexingMaps = op.getIndexingMapsArray(); |
| auto operandTypes = op->getOperandTypes(); |
| int numOperands = operandTypes.size(); |
| |
| SmallVector<int64_t> operandElemBits = |
| llvm::map_to_vector(op->getOperandTypes(), [](Type t) -> int64_t { |
| return IREE::Util::getTypeBitWidth(getElementTypeOrSelf(t)); |
| }); |
| |
| // For each operand, we track how big the tile is going to be based on the |
| // dimensions that we have already accounted for. Here we initialize this to |
| // just elemBitWidth, having not yet accounted for any array dimension. |
| SmallVector<int64_t> tileBits; |
| for (auto bits : operandElemBits) { |
| if (bits > eachOperandMaxTileBits) { |
| // eachOperandMaxTileBits is too small for this requirement to be |
| // satisfied. Early-returning. This might conceivably happen due to the |
| // tail-recursion in this function, where eachOperandMaxTileBits is |
| // halved. |
| return; |
| } |
| tileBits.push_back(bits); |
| } |
| |
| // For each loop, we must make sure that if it's the inner-most dimension that |
| // concerns an operand with a not-multiple-of-8-bit element type, we preserve |
| // the requirement that the inner-most tile dimension is a multiple of 8 bits. |
| // For example, if the element type is `i2`, the minimum inner tile size is |
| // 8/2 = 4. If the element type if i6, since gcd(8, 6) = 2, we are in the same |
| // situation and the minimum inner tile size is again 8/2 = 4. |
| // TODO(#17178): check this actually works when we have such element types. |
| SmallVector<int64_t> minVecTileSizes(numLoops, 1); |
| for (int loopNum : llvm::seq<int>(0, numLoops)) { |
| for (int i : llvm::seq<int>(0, numOperands)) { |
| if (isInnerMostDimThatMapIsFunctionOf(indexingMaps[i], loopNum)) { |
| if (operandElemBits[i] % 8) { |
| minVecTileSizes[loopNum] = std::max( |
| minVecTileSizes[loopNum], 8 / std::gcd(8, operandElemBits[i])); |
| } |
| } |
| } |
| } |
| |
| // For each loop starting from the inner-most, clamp vecTileSizes to honor |
| // `eachOperandMaxTileBits`, to the extent permitted by `minVecTileSizes` |
| // (which is a harder requirement). |
| for (int loopNum : llvm::reverse(llvm::seq<int>(0, numLoops))) { |
| // Skip 0 vecTileSizes, want to preserve them as 0. |
| if (vecTileSizes[loopNum] == 0) { |
| continue; |
| } |
| for (int i : llvm::seq<int>(0, numOperands)) { |
| // Check if this operand is concerned with this loop. |
| if (!indexingMaps[i].isFunctionOfDim(loopNum)) { |
| continue; |
| } |
| int64_t oldVal = vecTileSizes[loopNum]; |
| int64_t maxVal = std::max(minVecTileSizes[loopNum], |
| eachOperandMaxTileBits / tileBits[i]); |
| int64_t adjustedVal = std::min(oldVal, maxVal); |
| // If we are adjusting a tile size, make sure that the adjusted tile size |
| // is a power-of-two, as introducing non-power-of-two tile sizes can be a |
| // major performance regression, e.g. a 30% e2e regression in MobileNetV2 |
| // just from a single occurrence in a DepthwiseConv (example discussed |
| // below in this comment). |
| // |
| // We round the adjustedVal to the nearest power of two /below/ not |
| // /above/ because doing that could make the entire adjustment a |
| // no-operation (e.g. from 8 to 5 back to 8) and then would trigger the |
| // tail recursion with halved eachOperandMaxTileBits, which would result |
| // in more aggressive tile size limitation. E.g. in MobileNetV2 on AVX-512 |
| // we have a DepthwiseConv with |
| // vecTileSizes = [1 1 8 16 1 3], |
| // without this adjustment, this becomes |
| // vecTileSizes = [1 1 5 16 1 3]. |
| // If we round this 5 back to 8, letting this function tail-recurse, we |
| // end up with this much narrower final result: |
| // vecTileSizes = [1 1 2 16 1 3]. |
| // It is best to round this 5 to 4, obtaining |
| // vecTileSizes = [1 1 4 16 1 3]. |
| // |
| // The above example also shows that some non-power-of-two tile sizes |
| // are not getting adjusted, and should not be rounded to nearest |
| // power-of-two: the inner-most dimension size 3 above. |
| if (adjustedVal != oldVal) { |
| // Round to nearest power of 2, rounding down. |
| adjustedVal = 1ll << llvm::Log2_64(adjustedVal); |
| } |
| vecTileSizes[loopNum] = adjustedVal; |
| tileBits[i] *= adjustedVal; |
| } |
| } |
| |
| // At this point we have satisfied our entry-wise requirements on |
| // `vecTileSizes`, but there is still the possibility that the sum of all |
| // operand tiles' bit sizes exceeds `allOperandsMaxTileBits`. Tail-recurse |
| // with halved `eachOperandMaxTileBits` until that requirement is satisfied. |
| // Note that if `eachOperandMaxTileBits` falls below some element type bit |
| // width, it will trigger an early-return above, so we don't need to worry |
| // about that here. |
| if (std::reduce(tileBits.begin(), tileBits.end()) > allOperandsMaxTileBits) { |
| limitVectorTileSizes(op, vecTileSizes, eachOperandMaxTileBits / 2, |
| allOperandsMaxTileBits); |
| } |
| } |
| |
| // Returns the size in bits of SIMD register space, or 0 if it can't be |
| // determined (e.g. Arm SVE). |
| static int getRegisterSpaceBitsIfKnown(IREE::HAL::ExecutableTargetAttr target) { |
| if (isX86(target)) { |
| if (hasFeature(target, "+avx512f")) { |
| return 32 * 512; |
| } else if (hasFeature(target, "+avx") || hasFeature(target, "+avx2")) { |
| return 16 * 256; |
| } else { |
| return 16 * 128; |
| } |
| } else if (isAArch64(target)) { |
| // 32 NEON/SVE registers (at least 128-bit each, returns the base size for |
| // SVE). |
| return 32 * 128; |
| } else { |
| // Don't know register space size as a compile-time constant on other |
| // architectures. |
| return 0; |
| } |
| } |
| |
| // Clamps in-place vecTileSizes to ensure that the tile sizes of all operands of |
| // `op` can simultaneously be allocated in SIMD registers. Does nothing when |
| // SIMD register space can't be determined as a compile-time constant (e.g. Arm |
| // SVE). |
| static void limitVectorTileSizes(linalg::LinalgOp op, |
| SmallVectorImpl<int64_t> &vecTileSizes) { |
| if (int registerSpaceBits = getRegisterSpaceBitsIfKnown( |
| IREE::HAL::ExecutableTargetAttr::lookup(op))) { |
| limitVectorTileSizes(op, vecTileSizes, registerSpaceBits, |
| registerSpaceBits); |
| } |
| } |
| |
| /// Struct that holds factors for heuristic distribution tile sizes selection. |
| /// The `minTileSizes`, `maxTileSizes` and `vectorSizeHints` can be empty or |
| /// as many as the number of loops. |
| struct DistributionHeuristicConfig { |
| // TODO(hanchung): Remove `allowIncompleteTile` option after codegen can |
| // vectorize all the shapes. Allowing incomplete tile is critical for odd |
| // shapes (e.g., some dim sizes could be prime number). |
| bool allowIncompleteTile = false; |
| |
| SmallVector<int64_t> minTileSizes; |
| SmallVector<int64_t> maxTileSizes; |
| |
| // On the dimensions where the hints != 1, it will try to find the tile sizes |
| // which are multipliers of the hints. |
| SmallVector<int64_t> vectorSizeHints; |
| }; |
| |
| /// Returns the tile size to use for distribution. The `op` needs to be a |
| /// TilingInterface op. |
| static SmallVector<int64_t> |
| getDefaultDistributedLevelTileSizes(Operation *op, |
| const DistributionHeuristicConfig &config) { |
| SmallVector<int64_t> lbs, ubs; |
| getRangeBounds(cast<TilingInterface>(op), lbs, ubs); |
| int64_t numLoops = lbs.size(); |
| |
| assert( |
| (config.minTileSizes.empty() || config.minTileSizes.size() == numLoops) && |
| "min tile sizes should be empty or equal to the number of loops"); |
| assert( |
| (config.maxTileSizes.empty() || config.maxTileSizes.size() == numLoops) && |
| "max tile sizes should be empty or equal to the number of loops"); |
| assert((config.vectorSizeHints.empty() || |
| config.vectorSizeHints.size() == numLoops) && |
| "vector size hints should be empty or equal to the number of loops"); |
| |
| // Only set values when the loop is partitionable. |
| SmallVector<int64_t> adjustedMinTileSizes(numLoops, 0); |
| SmallVector<int64_t> adjustedMaxTileSizes(numLoops, 0); |
| SmallVector<int64_t> adjustedVectorSizeHints(numLoops, 1); |
| SmallVector<unsigned> partitionableLoops = |
| cast<PartitionableLoopsInterface>(op).getPartitionableLoops(std::nullopt); |
| for (auto i : partitionableLoops) { |
| adjustedMinTileSizes[i] = |
| config.minTileSizes.empty() ? 1 : config.minTileSizes[i]; |
| adjustedMaxTileSizes[i] = config.maxTileSizes.empty() |
| ? clDefaultDistTileSize |
| : config.maxTileSizes[i]; |
| adjustedVectorSizeHints[i] = |
| config.vectorSizeHints.empty() ? 1 : config.vectorSizeHints[i]; |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Adjusted min tile sizes: " << adjustedMinTileSizes |
| << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Adjusted max tile sizes: " << adjustedMaxTileSizes |
| << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Adjusted vector size hints: " |
| << adjustedVectorSizeHints << "\n"); |
| |
| SmallVector<int64_t> distributedTileSizes = getDefaultDistributionTileSizes( |
| lbs, ubs, adjustedMinTileSizes, adjustedMaxTileSizes, |
| adjustedVectorSizeHints); |
| |
| LLVM_DEBUG(KD_DBGS() << "Distributed tile sizes before fixups: " |
| << distributedTileSizes << "\n"); |
| |
| // Final fix up of the tile sizes to make sure that they divide the problem |
| // size to make it vectorizable. |
| for (auto i : llvm::seq<unsigned>(0, distributedTileSizes.size())) { |
| if (!distributedTileSizes[i]) |
| continue; |
| distributedTileSizes[i] = getMaxDistributionTileSize( |
| lbs[i], ubs[i], distributedTileSizes[i], adjustedMinTileSizes[i], |
| config.allowIncompleteTile); |
| } |
| LLVM_DEBUG(KD_DBGS() << "Distributed tile sizes after fixups: " |
| << distributedTileSizes << "\n"); |
| return distributedTileSizes; |
| } |
| |
| /// Splits the tile sizes in `parallelSizes` into `reductionSizes` for the |
| /// reduction loops. |
| static void splitParallelAndReductionTiles( |
| Operation *op, SmallVectorImpl<int64_t> ¶llelSizes, |
| SmallVectorImpl<int64_t> &reductionSizes, |
| SmallVectorImpl<bool> *parallelScalableFlags = nullptr, |
| SmallVectorImpl<bool> *reductionScalableFlags = nullptr) { |
| reductionSizes.assign(parallelSizes.begin(), parallelSizes.end()); |
| if (reductionScalableFlags) { |
| assert(parallelScalableFlags && "expected parallel scalable flags!"); |
| reductionScalableFlags->assign(parallelScalableFlags->begin(), |
| parallelScalableFlags->end()); |
| } |
| TilingInterface tilingOp = cast<TilingInterface>(op); |
| for (auto [index, iteratorType] : |
| llvm::enumerate(tilingOp.getLoopIteratorTypes())) { |
| if (iteratorType == utils::IteratorType::parallel) { |
| reductionSizes[index] = 0; |
| if (reductionScalableFlags) |
| (*reductionScalableFlags)[index] = false; |
| } else { |
| parallelSizes[index] = 0; |
| if (parallelScalableFlags) |
| (*parallelScalableFlags)[index] = false; |
| } |
| } |
| } |
| |
| static void setAlwaysVectorizeSizes(linalg::LinalgOp op, |
| SmallVectorImpl<int64_t> ¶llelSizes, |
| SmallVectorImpl<int64_t> &reductionSizes) { |
| SmallVector<int64_t> staticLoopRanges = op.getStaticLoopRanges(); |
| for (auto [index, valuePair] : llvm::enumerate( |
| llvm::zip_equal(staticLoopRanges, op.getIteratorTypesArray()))) { |
| auto [size, iterType] = valuePair; |
| if (!ShapedType::isDynamic(size)) |
| continue; |
| if (iterType == utils::IteratorType::parallel) { |
| parallelSizes[index] = 1; |
| } else { |
| reductionSizes[index] = 1; |
| } |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Set always-vectorize parallel sizes: " |
| << parallelSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Set always-vectorize reduction sizes: " |
| << reductionSizes << "\n"); |
| } |
| |
| static void |
| setVectorSizesForDynamicShapes(linalg::LinalgOp op, |
| VectorPreProcStrategy vecPreProcStrategy, |
| SmallVectorImpl<int64_t> ¶llelSizes, |
| SmallVectorImpl<int64_t> &reductionSizes) { |
| // Masking doesn't need any dim set to 1. |
| if (vecPreProcStrategy == VectorPreProcStrategy::Masking) { |
| return; |
| } |
| |
| SmallVector<int64_t> origParallelSizes(parallelSizes.begin(), |
| parallelSizes.end()); |
| SmallVector<int64_t> origReductionSizes(reductionSizes.begin(), |
| reductionSizes.end()); |
| setAlwaysVectorizeSizes(op, parallelSizes, reductionSizes); |
| |
| if (llvm::all_of(parallelSizes, [](int64_t size) { return size <= 1; })) { |
| // Make sure we vectorize at least the first innermost parallel dim with a |
| // vector size greater than one. |
| for (int i = origParallelSizes.size() - 1; i >= 0; --i) { |
| if (origParallelSizes[i] > 1) { |
| parallelSizes[i] = origParallelSizes[i]; |
| break; |
| } |
| } |
| } else if (llvm::all_of(reductionSizes, |
| [](int64_t size) { return size <= 1; })) { |
| // Make sure we vectorize at least the first innermost reduction dim with a |
| // vector size greater than one. |
| for (int i = origReductionSizes.size() - 1; i >= 0; --i) { |
| if (origReductionSizes[i] > 1) { |
| reductionSizes[i] = origReductionSizes[i]; |
| break; |
| } |
| } |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Parallel sizes for dynamic sizes: " << parallelSizes |
| << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Reduction sizes for dynamic sizes: " |
| << reductionSizes << "\n"); |
| return; |
| } |
| |
| /// Returns the default cache-level tile sizes for a matmul op and a specific |
| /// target. There shouldn't be proper heuristics here, just fixed values. |
| static SmallVector<int64_t> getDefaultMatmulCacheSizes(linalg::LinalgOp op, |
| bool isQuantized) { |
| unsigned numLoops = op.getNumLoops(); |
| SmallVector<int64_t> noCacheLevelTiling(numLoops, 0); |
| |
| // Cache-level tiling is only supported for 2-D matmuls. |
| if (numLoops < 3) { |
| return noCacheLevelTiling; |
| } |
| |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(op); |
| if (isX86(targetAttr)) { |
| if (isQuantized) { |
| return noCacheLevelTiling; |
| } |
| |
| SmallVector<int64_t> defaultCacheTileSizes(numLoops - 3, 0); |
| defaultCacheTileSizes.append({8, 128, 16}); |
| return defaultCacheTileSizes; |
| } |
| |
| return noCacheLevelTiling; |
| } |
| |
| static LogicalResult setMatmulPeelingRootConfig( |
| mlir::FunctionOpInterface entryPointFn, linalg::ContractionOpInterface op, |
| ArrayRef<int64_t> distTileSizes, ArrayRef<int64_t> cacheTileSizes, |
| ArrayRef<bool> inputVecScalableTileFlags, ArrayRef<int64_t> vecTileSizes, |
| int vectorSize) { |
| |
| // 0. Preprocess for scalable vectors |
| SmallVector<int64_t> roundedVecTileSizes(vecTileSizes); |
| |
| // The LLVM backend struggles to legalize non-power-of-two scalable vectors, |
| // hence the extra rounding up. |
| for (const auto &[index, size] : llvm::enumerate(roundedVecTileSizes)) { |
| if (!size) |
| continue; |
| roundedVecTileSizes[index] = |
| roundUpToPow2(size, |
| /*predicate=*/inputVecScalableTileFlags[index]); |
| } |
| |
| // 1. Compute tile sizes for all tiling levels. |
| // The tiling for parallel dims (M and N) and reduction dim (K) should be |
| // separated, so we move K dim from parallel tile sizes to reduction tile |
| // sizes. |
| int64_t numTilingDims = vecTileSizes.size(); |
| SmallVector<int64_t> cacheParallelTileSizes(cacheTileSizes.begin(), |
| cacheTileSizes.end()); |
| SmallVector<int64_t> cacheReductionTileSizes(numTilingDims, 0); |
| std::swap(cacheParallelTileSizes.back(), cacheReductionTileSizes.back()); |
| |
| SmallVector<int64_t> vectorParallelTileSizes(roundedVecTileSizes.begin(), |
| roundedVecTileSizes.end()); |
| SmallVector<int64_t> vectorReductionTileSizes(numTilingDims, 0); |
| std::swap(vectorParallelTileSizes.back(), vectorReductionTileSizes.back()); |
| |
| TileSizesListType tileSizes = { |
| SmallVector<int64_t>(distTileSizes), cacheParallelTileSizes, |
| cacheReductionTileSizes, vectorParallelTileSizes, |
| vectorReductionTileSizes}; |
| // No need for tiling inner parallel dims. |
| tileSizes.emplace_back(numTilingDims, 0); |
| |
| // 2. Set scalable flags for all the tiling levels. |
| SmallVector<bool> parallelScalableFlags(inputVecScalableTileFlags.begin(), |
| inputVecScalableTileFlags.end()); |
| SmallVector<bool> reductionScalableFlags(numTilingDims, false); |
| std::swap(parallelScalableFlags.back(), reductionScalableFlags.back()); |
| |
| ScalableTileFlagsListType newScalableTileFlags; |
| // No scalable: |
| // * distribution, |
| // * cache parallel, and |
| // * cache reduction |
| // tile sizes. |
| newScalableTileFlags.emplace_back(numTilingDims, false); |
| newScalableTileFlags.emplace_back(numTilingDims, false); |
| newScalableTileFlags.emplace_back(numTilingDims, false); |
| |
| newScalableTileFlags.push_back(parallelScalableFlags); |
| newScalableTileFlags.push_back(reductionScalableFlags); |
| |
| // No scalable inner parallel dims. |
| newScalableTileFlags.emplace_back(numTilingDims, false); |
| |
| LLVM_DEBUG(KD_DBGS() << "Final tile sizes for contraction: " << tileSizes |
| << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Final tile scalable flags for contraction: " |
| << newScalableTileFlags << "\n"); |
| |
| DictionaryAttr pipelineConfig = |
| getPipelineConfWithPeelingAttr(op.getContext()); |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, op, tileSizes, newScalableTileFlags, |
| DispatchLoweringPassPipeline::CPUDoubleTilingExpert, |
| /*workgroupSize=*/{}, /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| static LogicalResult |
| setMatmulRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::ContractionOpInterface op, |
| const TileSizesListTypeRef inputTileSizes, |
| const ScalableTileFlagsListTypeRef inputScalableTileFlags, |
| int vectorSize, VectorPreProcStrategy vecPreProcStrategy) { |
| auto linalgOp = cast<linalg::LinalgOp>(op.getOperation()); |
| SmallVector<int64_t> shape = linalgOp.getStaticLoopRanges(); |
| |
| // The tiling for parallel dims and reduction dims are separated. |
| const SmallVectorImpl<int64_t> &inputVecTileSizes = inputTileSizes.back(); |
| const SmallVectorImpl<bool> &vecScalableDims = inputScalableTileFlags.back(); |
| SmallVector<int64_t> vecTileSizes; |
| SmallVector<bool> parallelScalableFlags; |
| int numScalableDims = llvm::count(vecScalableDims, true); |
| |
| for (auto [index, tileSize] : llvm::enumerate(inputVecTileSizes)) { |
| int64_t sz = tileSize; |
| bool isScalable = vecScalableDims[index]; |
| // The backend struggles to legalize non-power-of-two scalable vectors. |
| bool enforcePowerOfTwo = isScalable; |
| |
| // Ad-hoc: Don't attempt to resize scalable tiles when numScalableDims >= 2. |
| // For ArmSME (the only current user of 2D scalable vectors), tile sizes |
| // must match SME tiles (and cannot be arbitrarily resized). |
| if (sz != 0 && (numScalableDims < 2 || !isScalable)) { |
| sz = getMaxVectorTileSize( |
| /*numElem=*/shape[index], |
| /*tileSize=*/sz, vectorSize, enforcePowerOfTwo); |
| } |
| vecTileSizes.push_back(sz); |
| // 1x scalable vectors e.g. vector<[1]xty> are also poorly supported, so |
| // fallback to fixed vectorization if they occur: |
| parallelScalableFlags.push_back(sz > 1 ? isScalable : false); |
| } |
| limitVectorTileSizes(cast<linalg::LinalgOp>(op.getOperation()), vecTileSizes); |
| SmallVector<int64_t> parallelTileSizes = vecTileSizes; |
| SmallVector<int64_t> reductionTileSizes; |
| SmallVector<bool> reductionScalableFlags; |
| splitParallelAndReductionTiles(op, parallelTileSizes, reductionTileSizes, |
| ¶llelScalableFlags, |
| &reductionScalableFlags); |
| |
| if (vecPreProcStrategy == VectorPreProcStrategy::None) { |
| setVectorSizesForDynamicShapes(cast<linalg::LinalgOp>(op.getOperation()), |
| vecPreProcStrategy, parallelTileSizes, |
| reductionTileSizes); |
| } |
| |
| // Ensure there's no zero scalable dims. |
| int64_t numTilingDims = parallelTileSizes.size(); |
| for (unsigned i = 0; i < numTilingDims; i++) { |
| if (reductionTileSizes[i] == 0) |
| reductionScalableFlags[i] = false; |
| if (parallelTileSizes[i] == 0) |
| parallelScalableFlags[i] = false; |
| } |
| |
| TileSizesListType newTileSizes; |
| // Copy all the tile size levels except the vector tile sizes which are split |
| // into parallel and reduction. |
| std::copy(inputTileSizes.begin(), inputTileSizes.end() - 1, |
| std::back_inserter(newTileSizes)); |
| newTileSizes.push_back(parallelTileSizes); |
| newTileSizes.push_back(reductionTileSizes); |
| // No need for tiling inner parallel dims. |
| newTileSizes.emplace_back(numTilingDims, 0); |
| |
| // Mirror the same layout for the scalable dims. |
| ScalableTileFlagsListType newScalableTileFlags; |
| std::copy(inputScalableTileFlags.begin(), inputScalableTileFlags.end() - 1, |
| std::back_inserter(newScalableTileFlags)); |
| newScalableTileFlags.push_back(parallelScalableFlags); |
| newScalableTileFlags.push_back(reductionScalableFlags); |
| // No scalable inner parallel dims. |
| newScalableTileFlags.emplace_back(numTilingDims, false); |
| |
| LLVM_DEBUG(KD_DBGS() << "Final tile sizes for contraction: " << newTileSizes |
| << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Final tile scalable flags for contraction: " |
| << newScalableTileFlags << "\n"); |
| |
| auto pipeline = DispatchLoweringPassPipeline::CPUDoubleTilingExpert; |
| DictionaryAttr pipelineConfig; |
| if (vecPreProcStrategy == VectorPreProcStrategy::Peeling) { |
| pipelineConfig = getPipelineConfWithPeelingAttr(op.getContext()); |
| } |
| |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, op, newTileSizes, newScalableTileFlags, pipeline, |
| /*workgroupSize=*/{}, /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| /// Returns default hard-coded vector sizes for a give target. No smartness |
| /// should be introduced in this utility. |
| static void |
| getDefaultMatmulVectorSizes(linalg::LinalgOp op, int64_t vectorSize, |
| SmallVectorImpl<int64_t> &sizes, |
| SmallVectorImpl<bool> &scalableSizeFlags) { |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(op); |
| if (isX86(targetAttr)) { |
| if (hasAVX512fFeature(targetAttr)) { |
| sizes.append({8, 32, 16}); |
| } else { |
| sizes.append({1, 1, vectorSize}); |
| } |
| return; |
| } |
| |
| if (isAArch64(targetAttr)) { |
| sizes.append({8, 16, 1}); |
| |
| // Specialisation for scalable vectorization. |
| if (clEnableScalableVectorization && hasAnySVEFeature(targetAttr)) { |
| // Mark middle dimensions as scalable, so sizes are (8, [16], 1). |
| scalableSizeFlags.append({false, true, false}); |
| } |
| return; |
| } |
| |
| if (isRISCV(targetAttr)) { |
| // RISC-V natively supports scalar x vector operations so we don't have to |
| // vectorize dimension k. Vectorizing dimension k results in a vector load |
| // and a sequence of vrgather ops to implemement the broadcast explicitly. |
| // We should tile and/or unroll that dimension without vectorization, which |
| // is not possible right now. |
| sizes.append({8, 32, 1}); |
| return; |
| } |
| |
| // Fallback to use vectorSize for unknown arch. |
| sizes.append(3, vectorSize); |
| return; |
| } |
| |
| /// Checks if the input and output types of a linalg op are the same, and if so |
| /// returns the type. Otherwise, returns failure. |
| static FailureOr<Type> nonWideningLinalgElementType(linalg::LinalgOp op) { |
| SmallVector<Type, 3> inputAndOutputElementTypes; |
| for (Value v : |
| llvm::concat<Value>(op.getRegionInputArgs(), op.getRegionOutputArgs())) { |
| inputAndOutputElementTypes.push_back(v.getType()); |
| } |
| assert(!inputAndOutputElementTypes.empty() && |
| "expected linalg op to have input and output types"); |
| if (!llvm::all_equal(inputAndOutputElementTypes)) |
| return failure(); |
| return inputAndOutputElementTypes[0]; |
| } |
| |
| /// Compute or adjust existing vector sizes using a generic heuristic that will |
| /// aim to fill at least one full vector register for all the element types of |
| /// the matmul. For now, the current heuristics only look at the N dimension but |
| /// we would introduce logic to also consider unrolling trade-offs between the |
| /// M, N and K. |
| /// |
| /// Example: for an (i32 <- i8, i8) matmul and a 128-bit vector register, vector |
| /// size N would be at least 128/8=16. |
| /// |
| /// NOTE: This function should not contain target-specific conditional code. |
| /// TODO: Currently it's only use on Aarch64. We should generalize it to other |
| /// targets. |
| static void getMatmulVectorSizesUsingFullVectorHeuristics( |
| mlir::FunctionOpInterface entryPointFn, linalg::LinalgOp op, |
| int64_t vectorSize, SmallVectorImpl<int64_t> &sizes, |
| SmallVectorImpl<bool> &scalableSizeFlags) { |
| if (sizes.empty()) |
| getDefaultMatmulVectorSizes(op, vectorSize, sizes, scalableSizeFlags); |
| |
| // Find the smallest type size in the matmul. |
| SmallVector<Type> matmulTypes; |
| auto operandTypes = op->getOperandTypes(); |
| matmulTypes.append(operandTypes.begin(), operandTypes.end()); |
| auto resultTypes = op->getResultTypes(); |
| matmulTypes.append(resultTypes.begin(), resultTypes.end()); |
| |
| int64_t minSize = std::numeric_limits<int64_t>::max(); |
| for (Type mmType : matmulTypes) { |
| if (auto shType = dyn_cast<ShapedType>(mmType)) |
| mmType = shType.getElementType(); |
| |
| if (mmType.isSignlessIntOrFloat()) |
| minSize = std::min<int64_t>(minSize, mmType.getIntOrFloatBitWidth()); |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Smallest type found: " << minSize << " bits\n"); |
| assert(minSize > 0 && minSize < std::numeric_limits<int64_t>::max() && |
| "Min size couldn't be computed"); |
| |
| // Make sure that the smallest type can at least fill a full vector register |
| // given the tile size of the main vector dimension (N). |
| constexpr int64_t byteSizeInBits = 8; |
| int64_t minNumElements = |
| (getNativeVectorSizeInBytes(entryPointFn) * byteSizeInBits) / minSize; |
| sizes[1] = std::max<int64_t>(sizes[1], minNumElements); |
| } |
| |
| /// Utility to compute the tile sizes for RISC-V Vector. |
| /// For now, it only supports nonWideningLinalgElementType float. |
| /// TileSize is set to m = 7, n = maxNumberElementsForLMUL4, and k = 1. |
| /// |
| /// Example: for an pure f32-matmul and a 512-bit vector register. |
| /// nativeVectorSize is equal to VLEN * LMUL2 / 8, so it's 128. |
| /// maxNumberElementsForLMUL4 = 128 * 2 * 8 / 32 = 64. |
| /// |
| /// TODO: Currently it only supports for nonWideningLinalgElementType. |
| static void |
| getMatmulRISCVVectorSizes(mlir::FunctionOpInterface entryPointFn, |
| linalg::LinalgOp op, int64_t vectorSize, |
| SmallVectorImpl<int64_t> &sizes, |
| SmallVectorImpl<bool> &scalableSizeFlags) { |
| if (sizes.empty()) |
| getDefaultMatmulVectorSizes(op, vectorSize, sizes, scalableSizeFlags); |
| // TODO: support widening matmul. |
| // Determines n dimension tile size with VLEN for |
| // nonWideningLinalgElementType. |
| FailureOr<Type> elementType = nonWideningLinalgElementType(op); |
| if (failed(elementType)) |
| return; |
| |
| // nativeVectorSize is cacluated with VLEN and LMUL=2. |
| int64_t nativeVectorSize = getNativeVectorSizeInBytes(entryPointFn); |
| int64_t elementSize; |
| if (elementType->isF16()) { |
| elementSize = 16; |
| } else if (elementType->isF32()) { |
| elementSize = 32; |
| } else if (elementType->isF64()) { |
| elementSize = 64; |
| } else { |
| // TODO: support int data type |
| return; |
| } |
| FailureOr<linalg::ContractionDimensions> cDims = |
| linalg::inferContractionDims(op); |
| if (failed(cDims) || cDims->m.size() != 1) |
| return; |
| // Use 7 x lmul4 to fully utilize vector registers. |
| sizes[0] = 7; |
| // Calculate tile size for the main vector dimension (N). |
| constexpr int64_t kByteSizeInBits = 8; |
| int64_t maxNumberElementsForLMUL4 = |
| (nativeVectorSize * 2 * kByteSizeInBits) / elementSize; |
| sizes[1] = maxNumberElementsForLMUL4; |
| sizes[2] = 1; |
| ArrayRef<int64_t> lhsShape = op.getShape(op.getDpsInputOperand(0)); |
| // If m = 1, set tile size to 1 x lmul8 |
| if (lhsShape[cDims->m[0]] == 1) { |
| sizes[0] = 1; |
| sizes[1] *= 2; |
| } |
| } |
| |
| /// Utility to compute the tile sizes for AArch64 SME. Unlike other targets, the |
| /// tile sizes picked here must exactly match multiples of the SME hardware |
| /// virtual tiles, as there is currently no support for lowering non-standard |
| /// shapes. |
| static void |
| getMatmulAArch64SMEVectorSizes(linalg::LinalgOp op, |
| SmallVectorImpl<int64_t> &sizes, |
| SmallVectorImpl<bool> &scalableSizeFlags) { |
| // Double-check the operation is one that is supported for lowering to ArmSME. |
| if (!llvm::isa<linalg::MatmulOp, linalg::MatmulTransposeAOp>(op)) |
| return; |
| |
| auto elementType = nonWideningLinalgElementType(op); |
| if (failed(elementType)) |
| return; |
| |
| // TODO(macdue): Come up with some heuristics to pick the appropriate tiling |
| // for SME, i.e. optimal layout based on static sizes. |
| |
| if (elementType->isF32()) { |
| // Tile for [8]x[8], this results in equal loads from both the A and B |
| // matrices and will use all four [4]x[4] 32-bit SME accumulators. |
| sizes.append({8, 8, 1}); |
| scalableSizeFlags.append({true, true, false}); |
| } |
| |
| if (elementType->isF64()) { |
| // Tile for [4]x[8], this results in loading twice as much from matrix B |
| // than and will use all eight [2]x[2] 64-bit SME accumulators. |
| // The B dimension is larger as it is known to be contiguous. |
| sizes.append({4, 8, 1}); |
| scalableSizeFlags.append({true, true, false}); |
| } |
| |
| // TODO(macdue): Other element types (there is little support for anything |
| // other than f32 and f64 yet). |
| } |
| |
| /// Main utility to compute the vectorization/unrolling tile sizes. |
| static SizesAndScalableFlags |
| getMatmulVectorSizes(mlir::FunctionOpInterface entryPointFn, |
| linalg::LinalgOp op, int64_t vectorSize, |
| bool isQuantized) { |
| SmallVector<int64_t> matmulTileSizes; |
| SmallVector<bool> matmulScalableFlags; |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| |
| // TODO: Compute vector tile sizes using heuristics. |
| |
| if (isAArch64(targetAttr)) { |
| if (clEnableScalableVectorization && !clDisableArmSMETiling && |
| hasSMEFeature(targetAttr)) { |
| // Note: This may not pick any sizes (which will fallback to the scalable |
| // vectorization heuristics below). |
| getMatmulAArch64SMEVectorSizes(op, matmulTileSizes, matmulScalableFlags); |
| } |
| |
| // Try to maximize the vector register utilization for all the matmul |
| // element types. |
| if (matmulTileSizes.empty()) { |
| getMatmulVectorSizesUsingFullVectorHeuristics( |
| entryPointFn, op, vectorSize, matmulTileSizes, matmulScalableFlags); |
| } |
| } |
| |
| if (isRISCV(targetAttr) && hasAnyVFeature(targetAttr)) { |
| // Use default tile size for matmul_transpose_b & |
| // batch_matmul_transpose_b to avoid performance drop. |
| if (!isa<linalg::MatmulTransposeBOp, linalg::BatchMatmulTransposeBOp>(op)) { |
| // Try to maximize the vector register utilization rate for matmul. |
| getMatmulRISCVVectorSizes(entryPointFn, op, vectorSize, matmulTileSizes, |
| matmulScalableFlags); |
| } |
| } |
| |
| // If tile sizes were not computed by previous heuristics, use default |
| // hard-coded tile sizes. |
| if (matmulTileSizes.empty()) { |
| getDefaultMatmulVectorSizes(op, vectorSize, matmulTileSizes, |
| matmulScalableFlags); |
| } |
| // Pad the scalable flags with false to match the tile sizes. |
| matmulScalableFlags.resize(matmulTileSizes.size()); |
| |
| SmallVector<int64_t> tileSizes; |
| SmallVector<bool> scalableTileFlags; |
| unsigned numLoops = op.getNumLoops(); |
| if (numLoops > 3) { |
| tileSizes.append(numLoops - 3, 1); |
| tileSizes.append(matmulTileSizes.begin(), matmulTileSizes.end()); |
| scalableTileFlags.append(numLoops - 3, false); |
| scalableTileFlags.append(matmulScalableFlags.begin(), |
| matmulScalableFlags.end()); |
| } else { |
| tileSizes.append(matmulTileSizes.begin() + (3 - numLoops), |
| matmulTileSizes.end()); |
| scalableTileFlags.append(matmulScalableFlags.begin() + (3 - numLoops), |
| matmulScalableFlags.end()); |
| } |
| |
| // For proper 2-D or higher order matmuls, make sure we don't use a tile size |
| // greater than the static dim size for dims that are only unrolled, i.e., N |
| // and batch dims. |
| |
| int numScalableDims = llvm::count(scalableTileFlags, true); |
| SmallVector<int64_t> staticShape = op.getStaticLoopRanges(); |
| if (numLoops >= 3) { |
| for (int i = 0; i < (numLoops - 2); ++i) { |
| int64_t dimSize = staticShape[i]; |
| int64_t tileSize = tileSizes[i]; |
| if (tileSize == 0 || ShapedType::isDynamic(dimSize)) { |
| continue; |
| } |
| // Ad-hoc: Don't attempt to resize scalable tiles when numScalableDims |
| // >= 2. For ArmSME (the only current user of 2D scalable vectors), tile |
| // sizes must match SME tiles (and cannot be arbitrarily resized). |
| if (numScalableDims >= 2 && scalableTileFlags[i]) { |
| continue; |
| } |
| tileSizes[i] = std::min<int64_t>(tileSize, dimSize); |
| } |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Matmul vector sizes: " << tileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Matmul vector scalable flags: " << scalableTileFlags |
| << "\n"); |
| return std::make_pair(tileSizes, scalableTileFlags); |
| } |
| |
| /// Adjust cache-level tile sizes based on the op shape. |
| static SmallVector<int64_t> |
| getMatmulCacheTileSizesForShape(ArrayRef<int64_t> inputTileSizes, |
| ArrayRef<int64_t> inputShape) { |
| // Make sure the tile sizes are not larger than the dim sizes. |
| int numDims = inputShape.size(); |
| SmallVector<int64_t> outputTileSizes(numDims); |
| for (int i = 0, end = numDims; i < end; ++i) { |
| outputTileSizes[i] = |
| (ShapedType::isDynamic(inputShape[i]) || inputShape[i] == 0) |
| ? inputTileSizes[i] |
| : std::min(inputTileSizes[i], inputShape[i]); |
| } |
| |
| // TODO: Enable caching for reduction dims. |
| outputTileSizes.back() = 0; |
| |
| return outputTileSizes; |
| } |
| |
| /// Sets the lowering configuration for dispatch region with root op that |
| /// implements the contraction operation interface. |
| static LogicalResult |
| setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::ContractionOpInterface contractionOp) { |
| assert(!getLoweringConfig(contractionOp) && |
| "expected lowering_config is not set"); |
| auto linalgOp = cast<linalg::LinalgOp>(contractionOp.getOperation()); |
| unsigned numLoops = linalgOp.getNumLoops(); |
| { |
| SmallVector<unsigned> dims; |
| linalgOp.getReductionDims(dims); |
| if (dims.size() != 1 || dims[0] != numLoops - 1) { |
| return contractionOp.emitOpError( |
| "expected to have exactly one reduction dim, and it is the innermost " |
| "dim"); |
| } |
| } |
| |
| // Consider all element types and use the smallest vector size. The tiling |
| // sizes are chosen based on the vector size. |
| auto lhsShapedType = llvm::cast<ShapedType>(contractionOp.lhs().getType()); |
| auto rhsShapedType = llvm::cast<ShapedType>(contractionOp.rhs().getType()); |
| auto resShapedType = |
| llvm::cast<ShapedType>(linalgOp.getDpsInitOperand(0)->get().getType()); |
| int64_t vectorSize = getVectorSize(entryPointFn, lhsShapedType); |
| vectorSize = std::min(vectorSize, getVectorSize(entryPointFn, rhsShapedType)); |
| vectorSize = std::min(vectorSize, getVectorSize(entryPointFn, resShapedType)); |
| bool isQuantized = |
| lhsShapedType.getElementType() != resShapedType.getElementType(); |
| |
| auto [vecTileSizes, vecScalableFlags] = |
| getMatmulVectorSizes(entryPointFn, linalgOp, vectorSize, isQuantized); |
| |
| auto vecPreProcStrategy = getVectorPreProcStrategy(linalgOp); |
| bool usePeelingPipeline = |
| vecPreProcStrategy == VectorPreProcStrategy::Peeling; |
| |
| LLVM_DEBUG(KD_DBGS() << "Vector pre-processing strategy: " |
| << vecPreProcStrategy << "\n"); |
| |
| DistributionHeuristicConfig distConfig; |
| distConfig.maxTileSizes.resize(numLoops, clDefaultDistTileSize); |
| distConfig.allowIncompleteTile = |
| vecPreProcStrategy != VectorPreProcStrategy::None; |
| distConfig.vectorSizeHints.resize(numLoops, vectorSize); |
| bool isBM = isa<linalg::BatchMatmulOp>(contractionOp.getOperation()); |
| if (isBM) { |
| distConfig.maxTileSizes[0] = 1; |
| distConfig.vectorSizeHints[0] = 1; |
| } |
| |
| // Compute cache-level tile sizes. Cache a dimension only if there are |
| // enough iterations. |
| SmallVector<int64_t> cacheTileSizes; |
| cacheTileSizes = getDefaultMatmulCacheSizes(linalgOp, isQuantized); |
| cacheTileSizes = getMatmulCacheTileSizesForShape( |
| cacheTileSizes, linalgOp.getStaticLoopRanges()); |
| |
| // Choose the next non-zero tile size immediately after the distribution |
| // level to help compute the distribution tile sizes. |
| for (auto [cacheTileSize, vecTileSize] : |
| llvm::zip_equal(cacheTileSizes, vecTileSizes)) { |
| int64_t minTileSize = cacheTileSize != 0 ? cacheTileSize : vecTileSize; |
| distConfig.minTileSizes.push_back(minTileSize); |
| } |
| // FIXME: Apply maxTileSize modification for all targets. |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| if (isRISCV(targetAttr) && hasAnyVFeature(targetAttr)) { |
| LLVM_DEBUG(KD_DBGS() << "RISC-V Aggressive Distribution: " |
| << clEnableRiscvAggressiveDist << "\n"); |
| for (auto loopNum : |
| llvm::seq<unsigned>(static_cast<unsigned>(isBM), numLoops)) { |
| if (clEnableRiscvAggressiveDist) { |
| if (distConfig.maxTileSizes[loopNum] <= |
| distConfig.minTileSizes[loopNum]) { |
| distConfig.maxTileSizes[loopNum] = |
| 2 * distConfig.minTileSizes[loopNum]; |
| } |
| } else { |
| distConfig.maxTileSizes[loopNum] = std::max( |
| distConfig.maxTileSizes[loopNum], distConfig.minTileSizes[loopNum]); |
| } |
| } |
| } |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(linalgOp, distConfig); |
| |
| // TODO: We set cache tile sizes to the distribution sizes for now (no-op) to |
| // make sure there are no performance changes. This will let us change the |
| // distribution sizes while still preserving the cache behavior of the |
| // original sizes. When we set proper sizes, we should call again |
| // `getMatmulCacheTileSizesForShape(cacheTileSizes, distTileSizes);` here as |
| // the `getDefaultDistributedLevelTileSizes` above may return sizes that are |
| // smaller than `minTileSizes`, so we have to adjust the cache sizes again. |
| cacheTileSizes = distTileSizes; |
| |
| SmallVector<bool> distScalableTileFlags(distTileSizes.size(), false); |
| ScalableTileFlagsListType scalableTileFlags = {distScalableTileFlags, |
| vecScalableFlags}; |
| |
| LLVM_DEBUG(KD_DBGS() << "Distribution tile sizes: " << distTileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Distribution scalable tile sizes: " |
| << distScalableTileFlags << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Cache tile sizes: " << cacheTileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Vector tile sizes: " << vecTileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Vector scalable tile flags: " << vecScalableFlags |
| << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Vector size: " << vectorSize << "\n"); |
| |
| if (usePeelingPipeline) { |
| return setMatmulPeelingRootConfig( |
| entryPointFn, contractionOp, distTileSizes, cacheTileSizes, |
| vecScalableFlags, vecTileSizes, vectorSize); |
| } |
| |
| TileSizesListType tileSizes = {distTileSizes, vecTileSizes}; |
| return setMatmulRootConfig(entryPointFn, contractionOp, tileSizes, |
| scalableTileFlags, vectorSize, vecPreProcStrategy); |
| } |
| |
| static TileSizesListType getMmt4dTileSizes(linalg::LinalgOp op) { |
| DistributionHeuristicConfig distConfig; |
| distConfig.allowIncompleteTile = true; |
| distConfig.minTileSizes.resize(op.getNumLoops(), 0); |
| distConfig.maxTileSizes.resize(op.getNumLoops(), 0); |
| |
| Value lhs = op.getDpsInputs()[0]; |
| Value rhs = op.getDpsInputs()[1]; |
| ShapedType lhsType = cast<ShapedType>(lhs.getType()); |
| ShapedType rhsType = cast<ShapedType>(rhs.getType()); |
| int mmt4dDimBase = 0; |
| if (isa<linalg::BatchMmt4DOp>(op)) { |
| mmt4dDimBase = 1; |
| distConfig.minTileSizes[0] = 1; |
| distConfig.maxTileSizes[0] = 1; // Force batch dimension tile size 1. |
| } |
| distConfig.minTileSizes[mmt4dDimBase + 0] = 1; |
| distConfig.minTileSizes[mmt4dDimBase + 1] = 1; |
| auto lhsShape = lhsType.getShape(); |
| auto rhsShape = rhsType.getShape(); |
| int64_t M1 = lhsShape[mmt4dDimBase + 0]; |
| int64_t N1 = rhsShape[mmt4dDimBase + 0]; |
| int64_t K1 = lhsShape[mmt4dDimBase + 1]; |
| int64_t M0 = lhsShape[mmt4dDimBase + 2]; |
| int64_t N0 = rhsShape[mmt4dDimBase + 2]; |
| int64_t K0 = lhsShape[mmt4dDimBase + 3]; |
| // Unfortunately we have to compute some tile size at compile-time, even |
| // though that can't be done meaningfully in general, unless specializing the |
| // compilation to fine details of the runtime workload including number of |
| // threads and cache sizes. Another thing that we need to know and can't |
| // really know at compile time is the values of dynamic sizes. Here we have to |
| // guess a reasonable default for the reduction dimension size. |
| int64_t reductionSize = ShapedType::isDynamic(K1) ? 1024 : K0 * K1; |
| auto getMatmulTileSize = [](int64_t targetTileBytes, int bitWidth, |
| int64_t reductionSize, int64_t tile0Size) { |
| int64_t targetRhsTileElems = targetTileBytes * 8 / bitWidth; |
| int64_t targetRhsTileNSize = targetRhsTileElems / reductionSize; |
| int64_t tileSize = llvm::divideCeil(targetRhsTileNSize, tile0Size); |
| tileSize = std::max<int64_t>(tileSize, 1); |
| return tileSize; |
| }; |
| int64_t tileBytes = |
| (M1 == 1 || N1 == 1) ? clNarrowMatmulTileBytes : clGeneralMatmulTileBytes; |
| distConfig.maxTileSizes[mmt4dDimBase + 0] = |
| M1 == 1 ? 1 |
| : getMatmulTileSize(tileBytes, lhsType.getElementTypeBitWidth(), |
| reductionSize, M0); |
| distConfig.maxTileSizes[mmt4dDimBase + 1] = |
| N1 == 1 ? 1 |
| : getMatmulTileSize(tileBytes, rhsType.getElementTypeBitWidth(), |
| reductionSize, N0); |
| |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(op, distConfig); |
| // Cache-level sizes are set to the distribution tile sizes for now. This will |
| // allow us to change distribution tile sizes while still preserving the |
| // existing cache behavior to some extent. |
| unsigned numLoops = op.getNumLoops(); |
| SmallVector<int64_t> cacheParallelTileSizes(distTileSizes.begin(), |
| distTileSizes.end()); |
| SmallVector<int64_t> cacheReductionTileSizes(numLoops, 0); |
| |
| SmallVector<int64_t> vecTileSizes(numLoops, 1); |
| assert(vecTileSizes.size() == mmt4dDimBase + 6); |
| vecTileSizes[mmt4dDimBase + 3] = M0; |
| vecTileSizes[mmt4dDimBase + 4] = N0; |
| vecTileSizes[mmt4dDimBase + 5] = K0; |
| limitVectorTileSizes(op, vecTileSizes); |
| SmallVector<int64_t> parallelTileSizes = vecTileSizes; |
| SmallVector<int64_t> reductionTileSizes; |
| splitParallelAndReductionTiles(op, parallelTileSizes, reductionTileSizes); |
| |
| return {distTileSizes, parallelTileSizes, reductionTileSizes}; |
| } |
| |
| /// Sets the lowering configuration for dispatch region for linalg.mmt4d |
| /// root op |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::Mmt4DOp Mmt4dOp) { |
| assert(!getLoweringConfig(Mmt4dOp) && "expected lowering_config is not set"); |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, Mmt4dOp, getMmt4dTileSizes(Mmt4dOp), |
| DispatchLoweringPassPipeline::Mmt4dTilingExpert); |
| } |
| |
| /// Sets the lowering configuration for dispatch region for linalg.batch_mmt4d |
| /// root op |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::BatchMmt4DOp batchMmt4dOp) { |
| assert(!getLoweringConfig(batchMmt4dOp) && |
| "expected lowering_config is not set"); |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, batchMmt4dOp, getMmt4dTileSizes(batchMmt4dOp), |
| DispatchLoweringPassPipeline::Mmt4dTilingExpert); |
| } |
| |
| static bool isPackMatmulLHS(tensor::PackOp op) { |
| // linalg.batch_matmul LHS shape |
| if (op.getSourceRank() == 3 && op.getInnerDimsPos().size() == 2 && |
| op.getInnerDimsPos()[0] == 1 && op.getInnerDimsPos()[1] == 2) { |
| return true; |
| } |
| // linalg.matmul LHS shape |
| return op.getSourceRank() == 2 && op.getInnerDimsPos().size() == 2 && |
| op.getInnerDimsPos()[0] == 0 && op.getInnerDimsPos()[1] == 1; |
| } |
| |
| /// Returns vectorization tile sizes for a pack op. It is driven by pack op |
| /// configurations and target CPU features. |
| static SmallVector<int64_t> |
| getPackVectorTileSizes(mlir::FunctionOpInterface entryPointFn, |
| tensor::PackOp op) { |
| SmallVector<int64_t> tileSizes(op.getSourceRank(), 1); |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| int64_t vectorSize = getVectorSize(entryPointFn, op.getSourceType()); |
| if (!hasAVX512fFeature(targetAttr) || !isPackMatmulLHS(op)) { |
| return tileSizes; |
| } |
| if (op.getSourceType().getElementType().isF32()) { |
| tileSizes.back() = vectorSize; |
| } |
| // TODO(#16314): Generate efficient tile sizes for non-f32 cases. |
| if (op.getSourceType().getElementType().isF16()) { |
| // We adjust the vector size to half to use the same lowering strategy as |
| // f32. |
| tileSizes.back() = vectorSize / 2; |
| } |
| return tileSizes; |
| } |
| |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| tensor::PackOp op) { |
| assert(!getLoweringConfig(op) && "expected lowering_config is not set"); |
| |
| int srcRank = op.getSourceRank(); |
| SmallVector<int64_t> innerTiles = op.getStaticTiles(); |
| ArrayRef<int64_t> dimPos = op.getInnerDimsPos(); |
| int64_t vectorSize = getVectorSize(entryPointFn, op.getSourceType()); |
| |
| DistributionHeuristicConfig distConfig; |
| distConfig.maxTileSizes.resize(srcRank, clDefaultDistTileSize); |
| distConfig.allowIncompleteTile = true; |
| distConfig.vectorSizeHints.resize(srcRank, 1); |
| for (auto pos : dimPos) { |
| distConfig.vectorSizeHints[pos] = vectorSize; |
| } |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(op, distConfig); |
| |
| // The default function aims to returns the number of workload per workgroup, |
| // but it does not know that it is working on packed domain. We need to take |
| // inner tile sizes into account and adjust the distribution tile sizes. |
| for (auto [pos, size] : llvm::zip_equal(dimPos, innerTiles)) { |
| if (distTileSizes[pos] == 0 || ShapedType::isDynamic(size)) |
| continue; |
| distTileSizes[pos] = distTileSizes[pos] / size; |
| distTileSizes[pos] = std::max<int64_t>(distTileSizes[pos], 1); |
| } |
| |
| // Dynamic inner tiles lead to unbounded stack allocation (which is introduced |
| // by tensor.pad op), so we do not decompose the cases. The x86 and risc-v |
| // backends prefer to not decompose the ops. |
| DictionaryAttr pipelineConfig; |
| auto target = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| bool hasDynamicInnerTile = llvm::any_of( |
| op.getMixedTiles(), [](OpFoldResult ofr) { return ofr.is<Value>(); }); |
| if (!hasDynamicInnerTile && !isX86(target) && !isRISCV(target)) { |
| pipelineConfig = getPipelineConfWithDecompositionAttr(op.getContext()); |
| } |
| |
| SmallVector<int64_t> vecTileSizes = getPackVectorTileSizes(entryPointFn, op); |
| TileSizesListType tileSizesList = {distTileSizes, vecTileSizes}; |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, op, tileSizesList, |
| DispatchLoweringPassPipeline::CPUDataTiling, /*workgroupSize=*/{}, |
| /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| tensor::UnPackOp op) { |
| DistributionHeuristicConfig distConfig; |
| distConfig.maxTileSizes.resize(op.getDestRank(), clDefaultDistTileSize); |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(op, distConfig); |
| |
| // Fixup for making distTileSizes be multiple of inner_tile_sizes. |
| SmallVector<int64_t> innerTiles = op.getStaticTiles(); |
| ArrayRef<int64_t> dimPos = op.getInnerDimsPos(); |
| for (auto [pos, size] : llvm::zip_equal(dimPos, innerTiles)) { |
| if (distTileSizes[pos] == 0 || ShapedType::isDynamic(size)) |
| continue; |
| distTileSizes[pos] = llvm::alignTo(distTileSizes[pos], size); |
| } |
| |
| SmallVector<int64_t> tileSizes(op.getDestRank(), 1); |
| for (auto [pos, size] : llvm::zip_equal(dimPos, innerTiles)) { |
| tileSizes[pos] = ShapedType::isDynamic(size) ? 1 : size; |
| } |
| |
| // Dynamic inner tiles lead to unbounded stack allocation (which is introduced |
| // by tensor.pad op), so we do not decompose the cases. The x86 and risc-v |
| // backends prefer to not decompose the ops. |
| DictionaryAttr pipelineConfig; |
| auto target = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| bool hasDynamicInnerTile = llvm::any_of( |
| op.getMixedTiles(), [](OpFoldResult ofr) { return ofr.is<Value>(); }); |
| if (!hasDynamicInnerTile && !isX86(target) && !isRISCV(target)) { |
| pipelineConfig = getPipelineConfWithDecompositionAttr(op.getContext()); |
| } |
| |
| TileSizesListType tileSizesList = {distTileSizes, tileSizes}; |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, op, tileSizesList, |
| DispatchLoweringPassPipeline::CPUDataTiling, /*workgroupSize=*/{}, |
| /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| IREE::LinalgExt::AttentionOp attnOp) { |
| FailureOr<IREE::LinalgExt::AttentionOpDetail> maybeOpInfo = |
| IREE::LinalgExt::AttentionOpDetail::get( |
| attnOp.getQueryMap(), attnOp.getKeyMap(), attnOp.getValueMap(), |
| attnOp.getOutputMap()); |
| assert(succeeded(maybeOpInfo) && "failed to infer attention dims"); |
| auto opInfo = maybeOpInfo.value(); |
| |
| SmallVector<int64_t> lbs, ubs; |
| getRangeBounds(attnOp, lbs, ubs); |
| |
| LLVM_DEBUG({ |
| KD_DBGS() << "Attention Detail:\n"; |
| KD_DBGS() << "Batch: ["; |
| llvm::interleaveComma(opInfo.getBatchDims(), llvm::dbgs()); |
| llvm::dbgs() << "]\n"; |
| KD_DBGS() << "M: ["; |
| llvm::interleaveComma(opInfo.getMDims(), llvm::dbgs()); |
| llvm::dbgs() << "]\n"; |
| KD_DBGS() << "K1: ["; |
| llvm::interleaveComma(opInfo.getK1Dims(), llvm::dbgs()); |
| llvm::dbgs() << "]\n"; |
| KD_DBGS() << "K2: ["; |
| llvm::interleaveComma(opInfo.getK2Dims(), llvm::dbgs()); |
| llvm::dbgs() << "]\n"; |
| KD_DBGS() << "N: ["; |
| llvm::interleaveComma(opInfo.getNDims(), llvm::dbgs()); |
| llvm::dbgs() << "]\n"; |
| }); |
| |
| // Batch, M and N (parallel dimensions) are distributed on workgroups. |
| DistributionHeuristicConfig config; |
| int64_t vectorSize = |
| getVectorSize(entryPointFn, attnOp.getOutput().getType()); |
| config.maxTileSizes.resize(opInfo.getDomainRank(), clDefaultDistTileSize); |
| config.vectorSizeHints.resize(opInfo.getDomainRank(), vectorSize); |
| // Distribute batch dimensions completely on workgroups (tile_size = 1). |
| for (int batch : opInfo.getBatchDims()) { |
| config.maxTileSizes[batch] = 1; |
| config.vectorSizeHints[batch] = 1; |
| } |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(attnOp, config); |
| |
| // Batch, M and N (parallel dimensions) are distributed on workgroups. |
| SmallVector<int64_t> vecTileSizes(attnOp.getIterationDomainRank(), 1); |
| // Due to the way attention works, K1 dimensions cannot be tiled. Mark k1 |
| // reduction dimensions not to distribute. |
| for (int i : opInfo.getK1Dims()) { |
| vecTileSizes[i] = 0; |
| } |
| for (auto i : llvm::seq<unsigned>(0, vecTileSizes.size())) { |
| // Do not tile reduction dimensions. |
| if (vecTileSizes[i] == 0) { |
| continue; |
| } |
| auto tileSize = distTileSizes[i] ? distTileSizes[i] : ubs[i]; |
| // TODO: Use native tile size here once bufferization is fixed for scf. |
| vecTileSizes[i] = getMaxVectorTileSize( |
| /*numElem=*/tileSize, vectorSize, vectorSize); |
| } |
| |
| // Tile the M dimension completely. |
| // TODO: This is a hack to prevent too large vector sizes. The largest vector |
| // generally produced is the Q vector, which is of shape: BATCH x M x K1. |
| // Since K1 cannot be tiled, the heuristics don't properly account for tiling |
| // M such that Q doesn't grow too large. |
| // Ideally, we should use something like limitVectorTileSizes, to fixup tile |
| // sizes. Currently, limitVectorTileSizes ignores static dimensions which are |
| // not tiled, which is why it's not currently used here. |
| for (int i : opInfo.getMDims()) { |
| vecTileSizes[i] = 1; |
| } |
| |
| SmallVector<int64_t> parallelTileSizes = vecTileSizes; |
| SmallVector<int64_t> reductionTileSizes; |
| splitParallelAndReductionTiles(attnOp, parallelTileSizes, reductionTileSizes); |
| |
| LLVM_DEBUG(KD_DBGS() << "Vectorization/unrolling tile sizes (parallel): " |
| << parallelTileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Vectorization/unrolling tile sizes (reduction): " |
| << reductionTileSizes << "\n"); |
| |
| TileSizesListType tileSizes = {distTileSizes, parallelTileSizes, |
| reductionTileSizes}; |
| |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, attnOp, tileSizes, |
| DispatchLoweringPassPipeline::CPULinalgExtTileAndVectorize); |
| } |
| |
| /// Sets the lowering configuration for dispatch region for linalg_ext.fft |
| /// root op. |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| IREE::LinalgExt::FftOp fftOp) { |
| assert(!getLoweringConfig(fftOp) && "expected lowering_config is not set"); |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(fftOp, DistributionHeuristicConfig{}); |
| auto rank = fftOp.getOperandRank(); |
| if (distTileSizes.size() >= rank && distTileSizes[rank - 1] != 0) { |
| APInt value; |
| if (matchPattern(fftOp.getStage(), m_ConstantInt(&value))) { |
| distTileSizes[rank - 1] = 1ll << value.getSExtValue(); |
| distTileSizes[rank - 1] = std::max( |
| distTileSizes[rank - 1], static_cast<int64_t>(clDefaultDistTileSize)); |
| } else { |
| return fftOp.emitOpError("non-constant stage might not work for fft op"); |
| } |
| } |
| TileSizesListType tileSizes = {distTileSizes}; |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, fftOp, tileSizes, DispatchLoweringPassPipeline::CPUDefault); |
| } |
| |
| /// Sets the lowering configuration for dispatch region for winograd ops: |
| /// linalg_ext.winograd.filter_transform |
| /// linalg_ext.winograd.input_transform |
| /// linalg_ext.winograd.output_transform |
| /// The vector tile sizes should be 1 for each dim here, because |
| /// the winograd decomposition relies on these unit dimensions. |
| template <typename WinogradOp> |
| static LogicalResult |
| setWinogradRootConfig(mlir::FunctionOpInterface entryPointFn, |
| WinogradOp winogradOp) { |
| static_assert( |
| std::is_same<WinogradOp, IREE::LinalgExt::WinogradInputTransformOp>() || |
| std::is_same<WinogradOp, |
| IREE::LinalgExt::WinogradOutputTransformOp>() || |
| std::is_same<WinogradOp, |
| IREE::LinalgExt::WinogradFilterTransformOp>(), |
| "op expected to be a winograd op"); |
| assert(!getLoweringConfig(winogradOp) && |
| "expected lowering_config is not set"); |
| auto iterationRank = winogradOp.getIterationDomainRank(); |
| SmallVector<int64_t> vecSizeHints(iterationRank, 1); |
| DistributionHeuristicConfig distConfig; |
| distConfig.vectorSizeHints = vecSizeHints; |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(winogradOp, distConfig); |
| TileSizesListType tileSizes; |
| tileSizes.push_back(distTileSizes); |
| SmallVector<int64_t> vecTileSizes(iterationRank, 1); |
| tileSizes.push_back(vecTileSizes); |
| // Dummy tiling config for reduction level. |
| SmallVector<int64_t> reductionTileSizes(iterationRank, 0); |
| tileSizes.push_back(reductionTileSizes); |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, winogradOp, tileSizes, |
| DispatchLoweringPassPipeline::CPULinalgExtTileAndVectorize); |
| } |
| |
| static void setVectorTileSizes(linalg::LinalgOp op, |
| ArrayRef<int64_t> distTileSizes, |
| ArrayRef<int64_t> minTileSizes, |
| ArrayRef<int64_t> maxTileSizes, |
| VectorPreProcStrategy vecPreProcStrategy, |
| SmallVectorImpl<int64_t> &vecTileSizes) { |
| int numLoops = op.getNumLoops(); |
| vecTileSizes.append(numLoops, 0); |
| SmallVector<int64_t> staticLoopRanges = op.getStaticLoopRanges(); |
| for (auto loopNum : llvm::seq<int>(0, numLoops)) { |
| if (distTileSizes[loopNum]) { |
| vecTileSizes[loopNum] = getMaxVectorTileSize( |
| distTileSizes[loopNum], minTileSizes[loopNum], minTileSizes[loopNum], |
| /*enforcePowerOfTwo=*/vecPreProcStrategy == |
| VectorPreProcStrategy::Masking); |
| } else { |
| // If the distribution tile size is zero, and static loop range is 0 as |
| // well, set the tile sizes here to zero as well. |
| vecTileSizes[loopNum] = |
| staticLoopRanges[loopNum] == 1 ? 0 : minTileSizes[loopNum]; |
| } |
| } |
| } |
| |
| /// Sets the default lowering configuration for a generic op to use |
| /// CPUDoubleTilingExpert pipeline. |
| static LogicalResult |
| setDefaultGenericOpRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::GenericOp genericOp, |
| const LinalgOpInfo &linalgOpInfo, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| assert(!getLoweringConfig(genericOp) && |
| "expected lowering_config is not set"); |
| LLVM_DEBUG(KD_DBGS() << "Setting default generic op root configuration\n"); |
| |
| // If there are no loops, there is nothing to do. |
| unsigned numLoops = genericOp.getNumLoops(); |
| if (numLoops == 0) { |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, genericOp, TileSizesListType{{}}, |
| DispatchLoweringPassPipeline::CPUDefault); |
| } |
| |
| DistributionHeuristicConfig distConfig; |
| // For generic ops we'll use the default divided by 2 to control the stack |
| // allocation limit See #9469 for example. |
| distConfig.maxTileSizes.append(numLoops, clDefaultDistTileSize / 2); |
| |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(genericOp, distConfig); |
| |
| LLVM_DEBUG(KD_DBGS() << "Final tile sizes for distribution: " << distTileSizes |
| << "\n"); |
| |
| auto vecPreProcStrategy = getVectorPreProcStrategy(genericOp); |
| LLVM_DEBUG(KD_DBGS() << "Vectorization pre-processing strategy " |
| << vecPreProcStrategy << "\n"); |
| |
| // Set the next level tile sizes. |
| SmallVector<int64_t> vecTileSizes; |
| setVectorTileSizes(genericOp, distTileSizes, |
| getMinTilingSizesForEachDim(entryPointFn, genericOp, |
| linalgOpInfo, |
| targetMLTransInfo), |
| distConfig.maxTileSizes, vecPreProcStrategy, vecTileSizes); |
| limitVectorTileSizes(genericOp, vecTileSizes); |
| SmallVector<int64_t> parallelTileSizes = vecTileSizes; |
| SmallVector<int64_t> reductionTileSizes; |
| splitParallelAndReductionTiles(genericOp, parallelTileSizes, |
| reductionTileSizes); |
| setVectorSizesForDynamicShapes(genericOp, vecPreProcStrategy, |
| parallelTileSizes, reductionTileSizes); |
| |
| LLVM_DEBUG(KD_DBGS() << "Vectorization/unrolling tile sizes (parallel): " |
| << parallelTileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Vectorization/unrolling tile sizes (reduction): " |
| << reductionTileSizes << "\n"); |
| |
| TileSizesListType tileSizes = {distTileSizes, parallelTileSizes, |
| reductionTileSizes}; |
| // No need for tiling inner parallel dims. |
| tileSizes.emplace_back(numLoops, 0); |
| |
| // For non-tensor based ops use the Buffer ops pipeline. |
| DispatchLoweringPassPipeline passPipeline; |
| DictionaryAttr pipelineConfig; |
| if (genericOp.hasPureTensorSemantics()) { |
| passPipeline = DispatchLoweringPassPipeline::CPUDoubleTilingExpert; |
| if (vecPreProcStrategy == VectorPreProcStrategy::Peeling) { |
| pipelineConfig = getPipelineConfWithPeelingAttr(genericOp.getContext()); |
| } |
| } else { |
| passPipeline = DispatchLoweringPassPipeline::CPUBufferOpsTileAndVectorize; |
| } |
| |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, genericOp, tileSizes, passPipeline, /*workgroupSize=*/{}, |
| /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| /// Utility to return the transpose vector `sizes` for X86. Empty `sizes` on |
| /// return indicates failure. |
| static void getTransposeX86VectorSizes( |
| linalg::GenericOp genericOp, IREE::HAL::ExecutableTargetAttr targetAttr, |
| ArrayRef<int64_t> minTileSizes, SmallVectorImpl<int64_t> &sizes) { |
| if (!hasAVX2Feature(targetAttr) || |
| !x86TransposeLoweringPrecondition(genericOp)) |
| return; |
| |
| if (llvm::count_if(minTileSizes, |
| [](int64_t tileSize) { return tileSize > 1; }) != 2) { |
| // Transpose patterns are not applicable if vectorizing more or less than |
| // two dims. |
| return; |
| } |
| |
| // Make sure that the original tile sizes are greater than or equal to the |
| // tile sizes to be used for the transpose op (e.g., 8x8, 16x16, etc). |
| int64_t targetVectorSize = 8; |
| if (llvm::any_of(minTileSizes, [&](int64_t tileSize) { |
| return tileSize > 1 && tileSize < targetVectorSize; |
| })) { |
| return; |
| } |
| |
| // Target 16x16 tile sizes if there are AVX512 features and all the tile sizes |
| // are greater than or equal to 16. |
| if (hasAVX512fFeature(targetAttr) && |
| llvm::all_of(minTileSizes, [](int64_t tileSize) { |
| return tileSize == 1 || tileSize >= 16; |
| })) { |
| targetVectorSize = 16; |
| } |
| |
| // Replace dims to be vectorized with the new tile sizes. |
| sizes.assign(minTileSizes.begin(), minTileSizes.end()); |
| std::replace_if( |
| sizes.begin(), sizes.end(), [](int64_t tileSize) { return tileSize > 1; }, |
| targetVectorSize); |
| } |
| |
| /// Utility to return the transpose vector `sizes` for AArch64. Empty `sizes` on |
| /// return indicates failure. |
| /// NOTE: only SME is currently supported. |
| static void getTransposeAArch64VectorSizes( |
| linalg::GenericOp genericOp, IREE::HAL::ExecutableTargetAttr targetAttr, |
| SmallVectorImpl<int64_t> &sizes, SmallVectorImpl<bool> &scalableFlags) { |
| if (!isLinalgGeneric2DTranspose(genericOp)) |
| return; |
| |
| auto elementType = nonWideningLinalgElementType(genericOp); |
| if (failed(elementType)) |
| return; |
| |
| if (hasSMEFeature(targetAttr) && clEnableScalableVectorization && |
| !clDisableArmSMETiling) { |
| if (elementType->isF32()) { |
| sizes.append({4, 4}); |
| } else if (elementType->isF64()) { |
| sizes.append({2, 2}); |
| } else { |
| return; |
| } |
| scalableFlags.append({true, true}); |
| } |
| } |
| |
| /// Main utility to compute the transpose vector sizes. |
| static std::optional<SizesAndScalableFlags> |
| getTransposeVectorSizes(mlir::FunctionOpInterface entryPointFn, |
| linalg::GenericOp genericOp, |
| const LinalgOpInfo &linalgOpInfo, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| SmallVector<int64_t> tileSizes; |
| SmallVector<bool> scalableFlags; |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| if (isX86(targetAttr)) { |
| SmallVector<int64_t> minTileSizes = getMinTilingSizesForEachDim( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo); |
| getTransposeX86VectorSizes(genericOp, targetAttr, minTileSizes, tileSizes); |
| } else if (isAArch64(targetAttr)) { |
| getTransposeAArch64VectorSizes(genericOp, targetAttr, tileSizes, |
| scalableFlags); |
| } |
| |
| if (tileSizes.empty()) |
| return std::nullopt; |
| |
| // If scalable flags are empty, assume target doesn't care about scalability. |
| if (scalableFlags.empty()) |
| scalableFlags = SmallVector<bool>(tileSizes.size(), false); |
| |
| LLVM_DEBUG(KD_DBGS() << "Transpose vector sizes: " << tileSizes << "\n"); |
| LLVM_DEBUG(KD_DBGS() << "Transpose vector scalable flags: " << scalableFlags |
| << "\n"); |
| return std::make_pair(tileSizes, scalableFlags); |
| } |
| |
| /// Sets the lowering configuration for a generic op implementing a |
| /// transposition to use CPUDoubleTilingExpert pipeline. |
| static LogicalResult |
| setTransposeLikeOpRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::GenericOp genericOp, |
| const LinalgOpInfo &linalgOpInfo, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| assert(!getLoweringConfig(genericOp) && |
| "expected lowering_config is not set"); |
| |
| if (!linalgOpInfo.isTranspose()) |
| return failure(); |
| |
| LLVM_DEBUG(KD_DBGS() << "Setting transpose-like op root configuration\n"); |
| |
| std::optional<SizesAndScalableFlags> vecDims = getTransposeVectorSizes( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo); |
| if (!vecDims) |
| return failure(); |
| |
| auto [vecSizes, vecScalableDims] = *vecDims; |
| |
| DistributionHeuristicConfig distConfig; |
| distConfig.minTileSizes = vecSizes; |
| auto vecPreProcStrategy = getVectorPreProcStrategy(genericOp); |
| LLVM_DEBUG(KD_DBGS() << "Vectorization pre-processing strategy " |
| << vecPreProcStrategy << "\n"); |
| if (vecPreProcStrategy != VectorPreProcStrategy::None) { |
| distConfig.allowIncompleteTile = true; |
| } |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(genericOp, distConfig); |
| SmallVector<int64_t> parallelTileSizes = distConfig.minTileSizes; |
| |
| TileSizesListType tileSizes = {distTileSizes, parallelTileSizes}; |
| // No need for tiling reduction dims and inner parallel dims. |
| int64_t numTilingDims = parallelTileSizes.size(); |
| tileSizes.emplace_back(numTilingDims, 0); |
| tileSizes.emplace_back(numTilingDims, 0); |
| |
| ScalableTileFlagsListType scalableTileFlags; |
| scalableTileFlags.emplace_back(numTilingDims, false); |
| scalableTileFlags.emplace_back(vecScalableDims); |
| |
| // For non-tensor based ops use the Buffer ops pipeline. |
| auto passPipeline = |
| genericOp.hasPureTensorSemantics() |
| ? DispatchLoweringPassPipeline::CPUDoubleTilingExpert |
| : DispatchLoweringPassPipeline::CPUBufferOpsTileAndVectorize; |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, genericOp, tileSizes, scalableTileFlags, passPipeline); |
| } |
| |
| /// Sets elementwise dispatches to use peeling approach. It scales the number of |
| /// workload per workgroup to a larger number, which prevents runtime overheads |
| /// from tiny dispatches. |
| static LogicalResult setElementwiseGenericOpRootConfig( |
| mlir::FunctionOpInterface entryPointFn, linalg::GenericOp genericOp, |
| const LinalgOpInfo &linalgOpInfo, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| assert(!getLoweringConfig(genericOp) && |
| "expected lowering_config is not set"); |
| |
| LLVM_DEBUG( |
| KD_DBGS() << "Setting elementwise generic op root configuration\n"); |
| |
| unsigned numLoops = genericOp.getNumLoops(); |
| if (numLoops == 0) |
| return failure(); |
| if (!linalg::isElementwise(genericOp)) |
| return failure(); |
| |
| DistributionHeuristicConfig distConfig; |
| distConfig.allowIncompleteTile = true; |
| distConfig.minTileSizes = getMinTilingSizesForEachDim( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo); |
| distConfig.maxTileSizes.append(numLoops, clDefaultDistTileSize); |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(genericOp, distConfig); |
| |
| // TODO(dcaballe): The logic below is disconnected from the main tile size |
| // selection logic in getMaxTileSize. We should either port it there or remove |
| // it. |
| // Adjust the number of workload per workgroup to at least 4096. This |
| // prevents the runtime overheads domiating the execution time. The number is |
| // derived from experimients. We should be able to make it related to target. |
| constexpr int64_t kMinimumWorkload = 4096; |
| auto shape = genericOp.getStaticLoopRanges(); |
| int64_t numWorkload = 1; |
| for (const auto &[index, size] : llvm::enumerate(shape)) { |
| if (ShapedType::isDynamic(size)) { |
| numWorkload = ShapedType::kDynamic; |
| break; |
| } |
| numWorkload *= distTileSizes[index] ? distTileSizes[index] : size; |
| } |
| for (unsigned currDim = 0; |
| numWorkload < kMinimumWorkload && currDim < numLoops;) { |
| int64_t currSize = distTileSizes[currDim]; |
| if (currSize == shape[currDim] || currSize == 0 || |
| ShapedType::isDynamic(shape[currDim]) || |
| ShapedType::isDynamic(numWorkload)) { |
| currDim++; |
| continue; |
| } |
| int64_t newSize = std::min<int64_t>(currSize * 2, shape[currDim]); |
| numWorkload = numWorkload / currSize * newSize; |
| distTileSizes[currDim] = newSize; |
| } |
| |
| auto vecPreProcStrategy = getVectorPreProcStrategy(genericOp); |
| LLVM_DEBUG(KD_DBGS() << "Vector pre-processing strategy: " |
| << vecPreProcStrategy << "\n"); |
| |
| // Adjust tiling sizes of vector levels to avoid large unroll factors. Most of |
| // the cases are f32 and i32, so we divide it by 4. |
| int64_t vecSize = getNativeVectorSizeInBytes(entryPointFn) / 4; |
| SmallVector<int64_t> vecTileSizes = distConfig.minTileSizes; |
| for (auto &i : vecTileSizes) { |
| i = roundUpToPow2(std::min(i, vecSize), |
| vecPreProcStrategy == VectorPreProcStrategy::Masking); |
| } |
| |
| // Setting reduction tile sizes is a workaround to kick in peeling transform. |
| // The tiling won't happen because the sizes are zeros. Also, no need for |
| // further tiling inner parallel dims, so the 4-th list is also zeros. |
| SmallVector<int64_t> zeros(numLoops, 0); |
| TileSizesListType tileSizes = {distTileSizes, vecTileSizes, zeros, zeros}; |
| |
| LLVM_DEBUG(KD_DBGS() << "Final tile sizes for element-wise op: " << tileSizes |
| << "\n"); |
| |
| DispatchLoweringPassPipeline passPipeline; |
| DictionaryAttr pipelineConfig; |
| if (genericOp.hasPureBufferSemantics()) { |
| passPipeline = DispatchLoweringPassPipeline::CPUBufferOpsTileAndVectorize; |
| } else { |
| passPipeline = DispatchLoweringPassPipeline::CPUDoubleTilingExpert; |
| } |
| |
| if (vecPreProcStrategy == VectorPreProcStrategy::Peeling) { |
| pipelineConfig = getPipelineConfWithPeelingAttr(genericOp.getContext()); |
| } |
| |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, genericOp, tileSizes, passPipeline, /*workgroupSize=*/{}, |
| /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| /// Sets the lowering configuration for a generic op to use |
| /// CPUDoubleTilingExpert pipeline. |
| static LogicalResult |
| setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::GenericOp genericOp, const LinalgOpInfo &linalgOpInfo, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| assert(!getLoweringConfig(genericOp) && |
| "expected lowering_config is not set"); |
| |
| if (succeeded(setTransposeLikeOpRootConfig( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo))) { |
| return success(); |
| } |
| if (succeeded(setElementwiseGenericOpRootConfig( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo))) { |
| return success(); |
| } |
| if (succeeded(setDefaultGenericOpRootConfig( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo))) { |
| return success(); |
| } |
| return failure(); |
| } |
| |
| static bool is2DConvOp(Operation *op) { |
| return isa<linalg::Conv2DNhwcHwcfOp, linalg::Conv2DNchwFchwOp>(op); |
| } |
| |
| static bool is2DDepthConvOp(Operation *op) { |
| return isa<linalg::DepthwiseConv2DNhwcHwcOp>(op); |
| } |
| |
| static bool is2DPoolingOp(Operation *op) { |
| return isa<linalg::PoolingNhwcSumOp, linalg::PoolingNhwcMaxOp, |
| linalg::PoolingNhwcMaxUnsignedOp, linalg::PoolingNhwcMinOp, |
| linalg::PoolingNhwcMinUnsignedOp, linalg::PoolingNchwSumOp, |
| linalg::PoolingNchwMaxOp>(op); |
| } |
| |
| /// Helper enum to represent conv2d input traversal order. |
| enum class Conv2DDimOrder { |
| // Corresponds to operation that traverses the input in (n, c, h, w) order. |
| Nchw, |
| // Corresponds to operation that traverses the input in (n, h, w, c) order. |
| Nhwc |
| }; |
| |
| static Conv2DDimOrder getConv2DDimOrder(Operation *op) { |
| if (isa<linalg::Conv2DNchwFchwOp, linalg::PoolingNchwSumOp, |
| linalg::PoolingNchwMaxOp>(op)) |
| return Conv2DDimOrder::Nchw; |
| if (isa<linalg::Conv2DNhwcHwcfOp, linalg::PoolingNhwcSumOp, |
| linalg::PoolingNhwcMaxOp, linalg::PoolingNhwcMaxUnsignedOp, |
| linalg::PoolingNhwcMinOp, linalg::PoolingNhwcMinUnsignedOp, |
| linalg::DepthwiseConv2DNhwcHwcOp>(op)) |
| return Conv2DDimOrder::Nhwc; |
| llvm::llvm_unreachable_internal("unsupported conv op"); |
| } |
| |
| /// Sets lowering configuration for conv ops. See below for supported conv ops. |
| static LogicalResult |
| setConvRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::LinalgOp convOp, ArrayRef<int64_t> targetTileSizes, |
| int64_t vectorSize, |
| VectorPreProcStrategy vecPreProcStrategy) { |
| if (!is2DConvOp(convOp) && !is2DDepthConvOp(convOp) && |
| !is2DPoolingOp(convOp)) { |
| return failure(); |
| } |
| assert(!getLoweringConfig(convOp) && "expected lowering_config is not set"); |
| |
| unsigned numLoops = convOp.getNumLoops(); |
| DistributionHeuristicConfig distConfig; |
| |
| // Give the vector size hint on OC. |
| distConfig.vectorSizeHints.append(numLoops, 1); |
| distConfig.vectorSizeHints[3] = vectorSize; |
| |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(convOp, distConfig); |
| |
| // Shapes of N, OH, OW, OC, KH, KW, (IC) |
| SmallVector<int64_t> shapes = convOp.getStaticLoopRanges(); |
| SmallVector<int64_t> vecTileSizes(targetTileSizes.begin(), |
| targetTileSizes.end()); |
| for (auto i : llvm::seq<unsigned>(0, vecTileSizes.size())) { |
| auto tileSize = distTileSizes[i] ? distTileSizes[i] : shapes[i]; |
| // If the tile size is intended to be 1, do not adjust it to `vectorSize`. |
| // The ops will be decomposed to lower-rank named ops. |
| if (vecTileSizes[i] != 1) { |
| vecTileSizes[i] = |
| getMaxVectorTileSize(tileSize, vecTileSizes[i], vectorSize); |
| } |
| } |
| limitVectorTileSizes(convOp, vecTileSizes); |
| SmallVector<int64_t> parallelTileSizes = vecTileSizes; |
| SmallVector<int64_t> reductionTileSizes; |
| splitParallelAndReductionTiles(convOp, parallelTileSizes, reductionTileSizes); |
| setAlwaysVectorizeSizes(convOp, parallelTileSizes, reductionTileSizes); |
| |
| TileSizesListType tileSizes = {distTileSizes, parallelTileSizes, |
| reductionTileSizes}; |
| // No need for tiling inner parallel dims. |
| int64_t numTilingDims = parallelTileSizes.size(); |
| tileSizes.emplace_back(numTilingDims, 0); |
| |
| // Set "scalable" flags |
| ScalableTileFlagsListType scalableTileFlags; |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| if (isAArch64(targetAttr) && hasAnySVEFeature(targetAttr) && |
| clEnableScalableVectorization && |
| isa<linalg::DepthwiseConv2DNhwcHwcOp>(convOp)) { |
| |
| auto dims = linalg::inferConvolutionDims(convOp); |
| // Level 1: Distribution |
| scalableTileFlags.emplace_back(numTilingDims, false); |
| // Level 2: Parallel |
| SmallVector<bool> parallelScalableFlags(numTilingDims, false); |
| // Make the channel dim scalable |
| parallelScalableFlags[dims->depth[0]] = true; |
| scalableTileFlags.emplace_back(parallelScalableFlags); |
| // Level 3: Reduction |
| scalableTileFlags.emplace_back(numTilingDims, false); |
| // Level 4: Inner parallel |
| scalableTileFlags.emplace_back(numTilingDims, false); |
| } |
| |
| DictionaryAttr pipelineConfig; |
| if (vecPreProcStrategy == VectorPreProcStrategy::Peeling) { |
| pipelineConfig = getPipelineConfWithPeelingAttr(convOp.getContext()); |
| } |
| |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, convOp, tileSizes, scalableTileFlags, |
| DispatchLoweringPassPipeline::CPUConvTileAndDecomposeExpert, |
| /*workgroupSize=*/{}, /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| /// Main utility to compute the vectorization/unrolling tile sizes. |
| /// Note that this only works for NHWC input and HWCF kernel/filter |
| /// convolutions, where the shape is [N, OH, OW, OC, KH, KW, (IC)]. |
| static SmallVector<int64_t> |
| getNhwcConvVectorSizes(mlir::FunctionOpInterface entryPointFn, |
| linalg::ConvolutionOpInterface op, int64_t vectorSize) { |
| bool isSupported = is2DConvOp(op) || is2DDepthConvOp(op) || is2DPoolingOp(op); |
| (void)isSupported; |
| assert(isSupported && "conv op is not supported"); |
| |
| SmallVector<int64_t> tileSizes; |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| |
| if (isX86(targetAttr)) { |
| if (is2DConvOp(op)) |
| return {1, 1, 8, vectorSize, 1, 1, 8}; |
| if (is2DDepthConvOp(op)) |
| return {1, 1, 8, vectorSize, 1, 3}; |
| if (is2DPoolingOp(op)) |
| return {1, 1, 8, vectorSize, 1, 8}; |
| llvm_unreachable("unsupported conv"); |
| } |
| if (isRISCV(targetAttr)) { |
| if (is2DConvOp(op)) |
| return {1, 1, 8, vectorSize * 2, 1, 1, 8}; |
| if (is2DDepthConvOp(op)) |
| return {1, 1, 8, vectorSize, 1, 3}; |
| if (is2DPoolingOp(op)) |
| return {1, 1, 8, vectorSize * 2, 1, 8}; |
| llvm_unreachable("unsupported conv"); |
| } |
| if (isAArch64(targetAttr)) { |
| if (is2DConvOp(op)) |
| return {1, 1, 32, 64, 1, 1, 16}; |
| if (is2DDepthConvOp(op)) |
| return {1, 1, 4, 4, 1, 4}; |
| if (is2DPoolingOp(op)) |
| return {1, 1, 32, 64, 1, 16}; |
| llvm_unreachable("unsupported conv"); |
| } |
| |
| // Get default hard-coded tile sizes if we couldn't compute anything |
| // better. |
| if (is2DConvOp(op)) |
| return {1, 1, vectorSize, vectorSize, 1, 1, vectorSize}; |
| if (is2DDepthConvOp(op)) |
| return {1, 1, vectorSize, vectorSize, 1, vectorSize}; |
| if (is2DPoolingOp(op)) |
| return {1, 1, vectorSize, vectorSize, 1, vectorSize}; |
| llvm_unreachable("unsupported conv"); |
| } |
| |
| static LogicalResult |
| setConvInterfaceRootConfig(mlir::FunctionOpInterface entryPointFn, |
| linalg::ConvolutionOpInterface convOp) { |
| int64_t vectorSize = getVectorSize( |
| entryPointFn, cast<ShapedType>(convOp->getResultTypes()[0])); |
| SmallVector<int64_t> targetTileSizes = |
| getNhwcConvVectorSizes(entryPointFn, convOp, vectorSize); |
| |
| // The tiling sizes are for NHWC layout. We need to apply a permutation if |
| // they are in other layout format. |
| Conv2DDimOrder order = getConv2DDimOrder(convOp); |
| switch (order) { |
| case Conv2DDimOrder::Nhwc: |
| break; |
| case Conv2DDimOrder::Nchw: |
| SmallVector<int64_t> perm; |
| if (is2DConvOp(convOp)) { |
| // D.n, D.oh, D.ow, D.f, D.kh, D.kw, D.c -> |
| // D.n, D.f, D.oh, D.ow, D.c, D.kh, D.kw |
| perm = {0, 3, 1, 2, 6, 4, 5}; |
| } else if (is2DPoolingOp(convOp)) { |
| // D.n, D.oh, D.ow, D.c, D.kh, D.kw -> |
| // D.n, D.c, D.oh, D.ow, D.kh, D.kw |
| perm = {0, 3, 1, 2, 4, 5}; |
| } else if (is2DDepthConvOp(convOp)) { |
| llvm_unreachable("Not implemented yet"); |
| } |
| applyPermutationToVector(targetTileSizes, perm); |
| break; |
| } |
| |
| auto vecPreProcStrategy = |
| getVectorPreProcStrategy(cast<linalg::LinalgOp>(convOp.getOperation())); |
| return setConvRootConfig(entryPointFn, |
| cast<linalg::LinalgOp>(convOp.getOperation()), |
| targetTileSizes, vectorSize, vecPreProcStrategy); |
| } |
| |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| tensor::PadOp padOp) { |
| SmallVector<int64_t> lbs, ubs; |
| getRangeBounds(cast<TilingInterface>(padOp.getOperation()), lbs, ubs); |
| |
| int64_t numLoops = lbs.size(); |
| unsigned typeWidthInBytes = IREE::Util::getRoundedElementByteWidth( |
| padOp.getResultType().getElementType()); |
| int64_t typeVectorSize = getVectorSize(entryPointFn, typeWidthInBytes); |
| DistributionHeuristicConfig distConfig; |
| distConfig.vectorSizeHints.append(numLoops, 1); |
| if (!ShapedType::isDynamic(ubs.back())) { |
| distConfig.vectorSizeHints.back() = std::min(typeVectorSize, ubs.back()); |
| } |
| |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(padOp, distConfig); |
| // No further tiling for reduction and inner parallel loops. |
| SmallVector<int64_t> zeros(numLoops, 0); |
| TileSizesListType tileSizes = {distTileSizes, distConfig.vectorSizeHints, |
| zeros, zeros}; |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, padOp, tileSizes, |
| DispatchLoweringPassPipeline::CPUDoubleTilingExpert); |
| } |
| |
| /// Set the default configuration for operations that implement the |
| /// `TiledOpInterface`. |
| static LogicalResult setRootConfig(mlir::FunctionOpInterface entryPointFn, |
| TilingInterface op) { |
| assert(!getLoweringConfig(op) && "expected lowering_config is not set"); |
| SmallVector<int64_t> distTileSizes = |
| getDefaultDistributedLevelTileSizes(op, DistributionHeuristicConfig{}); |
| TileSizesListType tileSizes = {distTileSizes}; |
| SmallVector<int64_t> vecTileSizes = distTileSizes; |
| |
| // Add an extra level of tiling. |
| // TODO: Limit vector tile sizes for other TilingInterface ops. |
| if (auto linalgOp = dyn_cast<linalg::LinalgOp>(*op)) { |
| limitVectorTileSizes(linalgOp, vecTileSizes); |
| } |
| tileSizes.push_back(vecTileSizes); |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, op, tileSizes, DispatchLoweringPassPipeline::CPUDefault); |
| } |
| |
| /// Redirects to methods that set the configuration based on operation type. |
| static LogicalResult |
| setRootConfigImpl(mlir::FunctionOpInterface entryPointFn, Operation *op, |
| const TargetMLTransformInfo &targetMLTransInfo) { |
| auto setRootConfigFn = [&](Operation *op) -> LogicalResult { |
| return TypeSwitch<Operation *, LogicalResult>(op) |
| .Case<linalg::GenericOp>([&](auto op) { |
| return setRootConfig(entryPointFn, op, LinalgOpInfo(op), |
| targetMLTransInfo); |
| }) |
| .Case<IREE::LinalgExt::CustomOp>([&](auto op) { |
| return setDefaultCustomOpLoweringConfig(entryPointFn, op, |
| initCPULaunchConfig); |
| }) |
| .Case<IREE::LinalgExt::AttentionOp, IREE::LinalgExt::FftOp, |
| tensor::PackOp, tensor::PadOp, tensor::UnPackOp, linalg::Mmt4DOp, |
| linalg::BatchMmt4DOp>( |
| [&](auto op) { return setRootConfig(entryPointFn, op); }) |
| .Case<IREE::LinalgExt::WinogradFilterTransformOp, |
| IREE::LinalgExt::WinogradInputTransformOp, |
| IREE::LinalgExt::WinogradOutputTransformOp>( |
| [&](auto op) { return setWinogradRootConfig(entryPointFn, op); }) |
| .Case<linalg::Conv2DNhwcHwcfOp, linalg::Conv2DNchwFchwOp, |
| linalg::PoolingNhwcSumOp, linalg::PoolingNhwcMaxOp, |
| linalg::PoolingNhwcMaxUnsignedOp, linalg::PoolingNhwcMinOp, |
| linalg::PoolingNhwcMinUnsignedOp, linalg::PoolingNchwSumOp, |
| linalg::PoolingNchwMaxOp, linalg::DepthwiseConv2DNhwcHwcOp>( |
| [&](auto op) { |
| return setConvInterfaceRootConfig(entryPointFn, op); |
| }) |
| .Case<linalg::ContractionOpInterface>( |
| [&](auto op) { return setRootConfig(entryPointFn, op); }) |
| .Case<TilingInterface>( |
| [&](auto op) { return setRootConfig(entryPointFn, op); }) |
| .Default([&](Operation *op) { return success(); }); |
| }; |
| return setRootConfigFn(op); |
| } |
| |
| /// Update the distribution tile sizes and parallel vector tile sizes to ensure: |
| /// 1. Distribution tile sizes and parallel vector tile sizes are aligned to the |
| /// inner tile sizes of the pack op. |
| /// 2. Parallel vector tile sizes are set with getPackVectorTileSizes to get |
| /// good performance on the pack op (e.g. 16x16 tile size on AVX512 for good |
| /// transpose codegen on the pack op). |
| /// For example: |
| /// Given the tile sizes for a Matmul RHS pack op is [1, 1, 1] and its inner |
| /// tile size is 16x1. We set the parallel vector tile sizes to [1, 1, 16], |
| /// which will be translated to tile sizes [1, 1, 1] on the pack op in |
| /// setLoweringConfigForComputeOps due to its affine map. At the same time, |
| /// its producer will have the parallel tile sizes [1, 1, 16], which is how the |
| /// pack op wants to tile-and-fuse it. |
| static LogicalResult |
| adjustTileSizesForPackOp(mlir::FunctionOpInterface entryPointFn, |
| tensor::PackOp packOp, |
| SmallVector<int64_t> &distTileSizes, |
| SmallVector<int64_t> ¶llelVecTileSizes) { |
| |
| ArrayRef<int64_t> innerDimsPos = packOp.getInnerDimsPos(); |
| ArrayRef<int64_t> innerTiles = packOp.getStaticInnerTiles(); |
| // Currently we only handle pack op with static inner tile sizes. |
| if (llvm::any_of(innerTiles, |
| [](int64_t size) { return ShapedType::isDynamic(size); })) { |
| return failure(); |
| } |
| // Pack op requires special vector tile sizes to achieve good performance. |
| // Override the parallel vector tile sizes from pack op. |
| auto vecTileSizes = getPackVectorTileSizes(entryPointFn, packOp); |
| auto outerDimsPerm = packOp.getOuterDimsPerm(); |
| if (!outerDimsPerm.empty()) { |
| auto invertedPerm = invertPermutationVector(outerDimsPerm); |
| applyPermutationToVector(vecTileSizes, invertedPerm); |
| } |
| // Scale to actual tile sizes with the pack op's inner tile sizes. |
| for (auto [pos, size] : llvm::zip_equal(innerDimsPos, innerTiles)) { |
| vecTileSizes[pos] *= size; |
| } |
| for (auto [pos, size] : llvm::enumerate(vecTileSizes)) { |
| if (!size) |
| continue; |
| if (!parallelVecTileSizes[pos]) { |
| parallelVecTileSizes[pos] = size; |
| continue; |
| } |
| // If other ops already set a smaller tile size, don't override it to avoid |
| // too large tile size on them. |
| parallelVecTileSizes[pos] = std::min(parallelVecTileSizes[pos], size); |
| } |
| // Align the tile sizes to the pack op's inner tile sizes, so we can derive |
| // the outer tile sizes for pack ops later in setLoweringConfigForComputeOps |
| // by dividing with inner tile sizes. |
| for (auto [pos, size] : llvm::zip_equal(innerDimsPos, innerTiles)) { |
| if (distTileSizes[pos]) |
| distTileSizes[pos] = llvm::alignTo(distTileSizes[pos], size); |
| if (parallelVecTileSizes[pos]) |
| parallelVecTileSizes[pos] = |
| llvm::alignTo(parallelVecTileSizes[pos], size); |
| } |
| return success(); |
| } |
| |
| /// Adjusts the tile sizes (carried by `rootOp`) to be aligned with |
| /// tensor.unpack inner tile sizes, if there are tensor.unpack producers. If the |
| /// tile sizes are not aligned, a stack buffer is needed because of |
| /// tensor.unpack tiling implementations. |
| static LogicalResult |
| adjustTileSizesForUnPackOp(mlir::FunctionOpInterface entryPointFn, |
| Operation *rootOp) { |
| auto linalgOp = dyn_cast<linalg::LinalgOp>(rootOp); |
| if (!linalgOp) |
| return success(); |
| |
| auto loweringConfig = |
| getLoweringConfig<IREE::Codegen::LoweringConfigAttr>(linalgOp); |
| TileSizesListType tileSizesList = loweringConfig.getTileSizeVals(); |
| |
| bool foundUnPackOp = false; |
| SmallVector<int64_t> alignedSizes(linalgOp.getNumLoops(), 1); |
| for (OpOperand *opOperand : linalgOp.getDpsInputOperands()) { |
| auto unpackOp = opOperand->get().getDefiningOp<tensor::UnPackOp>(); |
| if (!unpackOp) |
| continue; |
| |
| foundUnPackOp = true; |
| auto idxMap = linalgOp.getMatchingIndexingMap(opOperand); |
| LLVM_DEBUG(KD_DBGS() << "Find unpack op candidate: " << unpackOp << "\n" |
| << "The corresponding indexing map is: " << idxMap |
| << "\n"); |
| |
| SmallVector<int64_t> innerTiles = unpackOp.getStaticTiles(); |
| ArrayRef<int64_t> dimPos = unpackOp.getInnerDimsPos(); |
| for (auto [pos, size] : llvm::zip_equal(dimPos, innerTiles)) { |
| if (ShapedType::isDynamic(size)) |
| continue; |
| auto dimExpr = dyn_cast<AffineDimExpr>(idxMap.getResult(pos)); |
| if (!dimExpr) |
| return failure(); |
| int mappedPos = dimExpr.getPosition(); |
| alignedSizes[mappedPos] = std::lcm(alignedSizes[mappedPos], size); |
| } |
| } |
| |
| if (!foundUnPackOp) |
| return success(); |
| |
| LLVM_DEBUG( |
| KD_DBGS() << "The tile sizes for each dimension should be aligned to " |
| << alignedSizes); |
| |
| // Fixup for making tileSizes be multiple of inner_tile_sizes. |
| for (SmallVectorImpl<int64_t> &tileSizes : tileSizesList) { |
| for (auto idx : llvm::seq<int64_t>(0, tileSizes.size())) { |
| if (tileSizes[idx] == 0) |
| continue; |
| tileSizes[idx] = llvm::alignTo(tileSizes[idx], alignedSizes[idx]); |
| } |
| } |
| |
| auto tInfo = getTranslationInfo(entryPointFn); |
| auto pipeline = tInfo.getPassPipeline().getValue(); |
| auto pipelineConfig = tInfo.getConfiguration(); |
| if (isOptEnabled(entryPointFn, getEnableLoopPeelingStr())) { |
| // See #16406 |
| LLVM_DEBUG(KD_DBGS() << "unpack fusion does not work with peeling, falling " |
| "back to non-peeling path"); |
| pipeline = DispatchLoweringPassPipeline::CPUDoubleTilingExpert; |
| |
| // Remove the "enable_loop_peeling" attr from pipelineConfig |
| auto enableLoopPeelingAttrName = |
| getEnableLoopPeelingAttrName(rootOp->getContext()); |
| auto newPipelineConfigEntries = llvm::filter_to_vector( |
| pipelineConfig.getValue(), [&](NamedAttribute entry) { |
| return entry.getName() != enableLoopPeelingAttrName; |
| }); |
| |
| pipelineConfig = |
| DictionaryAttr::get(rootOp->getContext(), newPipelineConfigEntries); |
| } |
| |
| return setOpConfigAndEntryPointFnTranslation( |
| entryPointFn, rootOp, tileSizesList, |
| loweringConfig.getScalableTileFlagVals(), pipeline, /*workgroupSize=*/{}, |
| /*subgroupSize=*/{}, pipelineConfig); |
| } |
| |
| /// Get tile sizes for the generic op and fill into the parallel vector tile |
| /// sizes if the tile size on a dimension is missing. Also get the tile sizes on |
| /// the reduction dimensions. This makes sure there is a tile size set for each |
| /// dimension of the generic op. |
| /// For example: |
| /// The root op has iterator types: parallel, reduction, reduction and the |
| /// parallel tile sizes from the root op is [X, 0, 0]. The generic op's iterator |
| /// types are: parallel, parallel, reduction. After the update, the parallel |
| /// tile sizes become [X, Y, 0] while the Y is set by the generic op. The |
| /// function also returns the reduction tile sizes for the generic op [0, 0, Z]. |
| static LogicalResult |
| adjustTileSizesForGenericOp(mlir::FunctionOpInterface entryPointFn, |
| linalg::GenericOp genericOp, |
| SmallVector<int64_t> ¶llelVecTileSizes, |
| SmallVector<int64_t> &reductionTileSizes, |
| SmallVector<bool> ¶llelScalableFlags, |
| SmallVector<bool> &reductionScalableFlags) { |
| auto linalgOpInfo = LinalgOpInfo(genericOp); |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| auto targetMLTransInfo = |
| TargetMLTransformInfo::getTargetMLTransformInfo(targetAttr); |
| SmallVector<int64_t> vecTileSizes = getMinTilingSizesForEachDim( |
| entryPointFn, genericOp, linalgOpInfo, targetMLTransInfo); |
| |
| auto vecPreProcStrategy = getVectorPreProcStrategy(genericOp); |
| int64_t vecSize = getNativeVectorSizeInBytes(entryPointFn) / 4; |
| |
| for (auto &vecTileSize : vecTileSizes) { |
| vecTileSize = |
| roundUpToPow2(std::min(vecTileSize, vecSize), |
| vecPreProcStrategy == VectorPreProcStrategy::Masking); |
| } |
| limitVectorTileSizes(genericOp, vecTileSizes); |
| splitParallelAndReductionTiles(genericOp, vecTileSizes, reductionTileSizes, |
| ¶llelScalableFlags, |
| &reductionScalableFlags); |
| setVectorSizesForDynamicShapes(genericOp, vecPreProcStrategy, vecTileSizes, |
| reductionTileSizes); |
| for (auto [pos, tileSize] : llvm::enumerate(vecTileSizes)) { |
| // Generic op vector parallel tile size is low priority. Only use if no |
| // other op has set the tile size. |
| if (tileSize == 0 || parallelVecTileSizes[pos] != 0) |
| continue; |
| parallelVecTileSizes[pos] = tileSize; |
| } |
| return success(); |
| } |
| |
| /// Set the lowering configs for all the compute ops. The lowering config is |
| /// already set on `rootOperation`. We will duplicate the tile sizes of |
| /// distribution and common parallel dims to other compute ops (so they have |
| /// consistent configurations); set the tile size for the rest of dims. E.g., it |
| /// sets the lowering_config for reduction + broadcast + pack op like: |
| /// |
| /// %11 = linalg.fill ... -> tensor<384xf32> |
| /// %12 = linalg.generic { |
| /// indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, |
| /// affine_map<(d0, d1) -> (d0)>, |
| /// affine_map<(d0, d1) -> (d0)>], |
| /// iterator_types = ["parallel", "reduction"]} |
| /// ins(%5, %6 : tensor<384x528xf32>, tensor<384xf32>) |
| /// outs(%11 : tensor<384xf32>) { |
| /// ^bb0(%in: f32, %in_2: f32, %out: f32): |
| /// ... |
| /// } -> tensor<384xf32> |
| /// %13 = linalg.generic { |
| /// indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>, |
| /// affine_map<(d0, d1) -> (d0)>, |
| /// affine_map<(d0, d1) -> (d0, d1)>], |
| /// iterator_types = ["parallel", "parallel"]} |
| /// ins(%4, %12 : tensor<384x1024xf32>, tensor<384xf32>) |
| /// outs(%9 : tensor<384x1024xf32>) { |
| /// ^bb0(%in: f32, %in_2: f32, %out: f32): |
| /// ... |
| /// } -> tensor<384x1024xf32> |
| /// %pack = tensor.pack %13 |
| /// inner_dims_pos = [0, 1] |
| /// inner_tiles = [16, 1] |
| /// into %14 : tensor<384x1024xf32> -> tensor<24x1024x16x1xf32> |
| /// |
| /// The lowering config is set on `rootOperation`, which is the reduction ops in |
| /// this case. The configuration is [[X, 0], [Y, 0], [0, Z], [0, 0]]. The `d0` |
| /// is the common parallel dims for all the ops. The lowering config from |
| /// `rootOperation` sets the tile sizes for `d0` and the rest of dims in |
| /// reduction. But it does not have tile sizes for the rest of dims in |
| /// elementwise op and pack ops. This method sets the vectorization tile sizes |
| /// for other compute ops. E.g., [[X, 0], [Y, 0], [0, 0], [0, 16]] for the |
| /// elementwise operations and [[X, 0], [Y, 0], [0, 0], [0, 1]] for the pack op. |
| static LogicalResult |
| setLoweringConfigForComputeOps(mlir::FunctionOpInterface entryPointFn, |
| ArrayRef<Operation *> computeOps, |
| Operation *rootOperation) { |
| if (isa<linalg::ConvolutionOpInterface>(rootOperation)) { |
| // TODO(dcaballe): We don't know yet how to properly propagate the lowering |
| // config of a convolution. |
| return success(); |
| } |
| |
| auto ctx = entryPointFn.getContext(); |
| auto rootLoweringConfig = |
| getLoweringConfig<IREE::Codegen::LoweringConfigAttr>(rootOperation); |
| TilingConfig tilingConfig(rootLoweringConfig); |
| SmallVector<int64_t> distTileSizes, parallelVecTileSizes; |
| SmallVector<bool> distScalableTileSizes, parallelVecScalableTileSizes; |
| if (tilingConfig.getNumTilingLevels() > 0) { |
| distTileSizes = tilingConfig.getDistributionTileSizes(); |
| } |
| if (tilingConfig.getNumTilingLevels() > 1) { |
| std::tie(parallelVecTileSizes, parallelVecScalableTileSizes) = |
| tilingConfig.getVectorCommonParallelSizes(); |
| } |
| |
| size_t maxLoopNums = 0; |
| for (auto op : computeOps) { |
| // Multi-lowering config works only if all the operations can share the same |
| // distribution and parallel tile sizes from the root op. |
| auto iterTypes = cast<TilingInterface>(op).getLoopIteratorTypes(); |
| for (auto [idx, iterType] : llvm::enumerate(iterTypes)) { |
| if (idx >= parallelVecTileSizes.size()) |
| break; |
| if (iterType == utils::IteratorType::parallel) |
| continue; |
| if (distTileSizes[idx] || parallelVecTileSizes[idx]) |
| return success(); |
| } |
| maxLoopNums = std::max(maxLoopNums, iterTypes.size()); |
| } |
| |
| // Adjust the distribution tile sizes and join parallel vector tile sizes from |
| // other ops. The results of parallel vector tile sizes might overlap |
| // reduction dimensions on some ops, so it will be splitted into common vector |
| // tile sizes and inner vector tile sizes later. |
| // |
| // This step is to ensure all ops are using an equivalent set of parallel tile |
| // sizes. |
| // |
| // Here we use the assumption in FormDispatchRegions that all ops in a |
| // dispatch have identity mapping between their parallel dimensions. So we |
| // don't need to handle the permutation on dimensions between ops except for |
| // the pack op. |
| // |
| // For example: |
| // Given there are 3 generic ops in the dispatch: |
| // %rootOp = linalg.generic {iterator_types = ["reduction", "parallel"]} ... |
| // %2 = linalg.generic {iterator_types = ["parallel", "parallel"]} |
| // %3 = tensor.pack %2 |
| // Assume the distribution and parallel vector tile sizes from %rootOp is: |
| // [[X1, 0], [X2, 0]] |
| // Then the generic op %2 set the missing parallel vector tile sizes on its |
| // parallel dims: |
| // [[X1, 0], [X2, Y2]] |
| // Then the pack op %3 updates the distribution and parallel vector tile sizes |
| // based on its requirement: |
| // [[X1', Z1], [X2', Y2']] |
| // which is the final parallel tile sizes for all ops. |
| llvm::SmallDenseMap<Operation *, SmallVector<int64_t>> reductionTileSizeMap; |
| llvm::SmallDenseMap<Operation *, SmallVector<bool>> |
| reductionScalableFlagseMap; |
| distTileSizes.resize(maxLoopNums); |
| parallelVecTileSizes.resize(maxLoopNums); |
| parallelVecScalableTileSizes.resize(maxLoopNums); |
| bool hasSeenPackOp = false; |
| for (auto op : computeOps) { |
| assert(!hasSeenPackOp && "Pack op must be the last op"); |
| if (hasSeenPackOp) |
| return failure(); |
| |
| // Tile sizes have been initialized from rootOperation, so we skip it. |
| if (op == rootOperation) |
| continue; |
| |
| if (auto packOp = dyn_cast<tensor::PackOp>(op)) { |
| if (failed(adjustTileSizesForPackOp(entryPointFn, packOp, distTileSizes, |
| parallelVecTileSizes))) { |
| return failure(); |
| } |
| hasSeenPackOp = true; |
| } else if (auto genericOp = dyn_cast<linalg::GenericOp>(op)) { |
| SmallVector<int64_t> reductionTileSizes; |
| SmallVector<bool> reductionScalableFlags; |
| if (failed(adjustTileSizesForGenericOp( |
| entryPointFn, genericOp, parallelVecTileSizes, reductionTileSizes, |
| parallelVecScalableTileSizes, reductionScalableFlags))) { |
| return failure(); |
| } |
| reductionTileSizeMap[op] = reductionTileSizes; |
| reductionScalableFlagseMap[op] = reductionScalableFlags; |
| } |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Parallel vector tile sizes: " << parallelVecTileSizes |
| << "\n"); |
| |
| // Split parallel vector tile sizes into common parts and op-specific parts. |
| SmallVector<int64_t> commonVecTileSizes = parallelVecTileSizes; |
| SmallVector<bool> commonVecScalableTileFlags = parallelVecScalableTileSizes; |
| SmallVector<int64_t> innerVecTileSizes(maxLoopNums, 0); |
| SmallVector<bool> innerVecScalableTileFlags(maxLoopNums, false); |
| for (auto op : computeOps) { |
| auto iterTypes = cast<TilingInterface>(op).getLoopIteratorTypes(); |
| for (auto [idx, iterType] : llvm::enumerate(iterTypes)) { |
| if (iterType == utils::IteratorType::reduction) { |
| innerVecTileSizes[idx] = parallelVecTileSizes[idx]; |
| innerVecScalableTileFlags[idx] = parallelVecScalableTileSizes[idx]; |
| commonVecTileSizes[idx] = 0; |
| commonVecScalableTileFlags[idx] = false; |
| } |
| } |
| } |
| |
| // Make sure the innermost tile size times element size is multiple |
| // of byte bits. This is required for now because we do not fully |
| // support sub-byte vector stores. Once vector stores are supported |
| // then this can be eliminated. Note that emulating sub-byte sized vector |
| // loads and stores will have a performance impact. |
| auto resultTypes = rootOperation->getResultTypes(); |
| if (commonVecTileSizes.size() != 0 && !resultTypes.empty()) { |
| Type elementType = cast<ShapedType>(resultTypes[0]).getElementType(); |
| unsigned int elementTypeSize; |
| if (auto complexType = llvm::dyn_cast<ComplexType>(elementType)) { |
| elementTypeSize = |
| 2 * complexType.getElementType().getIntOrFloatBitWidth(); |
| } else { |
| elementTypeSize = elementType.getIntOrFloatBitWidth(); |
| } |
| // for now just enable for i1 |
| if (elementTypeSize == 1) { |
| auto innermostTileSize = commonVecTileSizes.back(); |
| commonVecTileSizes.back() = |
| llvm::alignTo(innermostTileSize * elementTypeSize, 8) / |
| elementTypeSize; |
| } |
| } |
| |
| // Set the lowering configs with new tile sizes. |
| for (auto op : computeOps) { |
| int numLoops = cast<TilingInterface>(op).getLoopIteratorTypes().size(); |
| TileSizesListType tileSizesList; |
| ScalableTileFlagsListType scalableTileFlagsList; |
| |
| // For root op, we patch the adjusted tile sizes on its original tiling |
| // config. |
| if (op == rootOperation) { |
| tileSizesList = rootLoweringConfig.getTileSizeVals(); |
| scalableTileFlagsList = rootLoweringConfig.getScalableTileFlagVals(); |
| if (tilingConfig.getNumTilingLevels() > 0) { |
| tileSizesList[tilingConfig.getDistributionLevel()] = distTileSizes; |
| scalableTileFlagsList[tilingConfig.getDistributionLevel()] = |
| distScalableTileSizes; |
| } |
| if (tilingConfig.getNumTilingLevels() > 1) { |
| tileSizesList[tilingConfig.getVectorCommonParallelLevel()] = |
| commonVecTileSizes; |
| scalableTileFlagsList[tilingConfig.getVectorCommonParallelLevel()] = |
| commonVecScalableTileFlags; |
| } |
| } else { |
| // Build 4-level lowering configs for other ops. |
| tileSizesList = {distTileSizes, commonVecTileSizes}; |
| SmallVector<int64_t> zeros(numLoops, 0); |
| SmallVector<bool> falseVec(numLoops, 0); |
| // No scalable tiling for the distribution |
| scalableTileFlagsList.push_back(falseVec); |
| scalableTileFlagsList.push_back(commonVecScalableTileFlags); |
| bool setUpOK = |
| TypeSwitch<Operation *, bool>(op) |
| .Case<tensor::PackOp>([&](auto packOp) { |
| for (auto flags : |
| rootLoweringConfig.getScalableTileFlagVals()) { |
| // TODO: Handle scalable flags |
| if (llvm::any_of(flags, [&](bool flag) { return flag; })) |
| return false; |
| } |
| tileSizesList.push_back(zeros); |
| tileSizesList.push_back(innerVecTileSizes); |
| // Scale and permutate the outer dim tiles for pack op. |
| ArrayRef<int64_t> innerTiles = packOp.getStaticInnerTiles(); |
| ArrayRef<int64_t> dimPos = packOp.getInnerDimsPos(); |
| auto outerDimsPerm = packOp.getOuterDimsPerm(); |
| for (auto &tileSizes : tileSizesList) { |
| for (auto [pos, size] : llvm::zip_equal(dimPos, innerTiles)) { |
| if (tileSizes[pos] == 0 || ShapedType::isDynamic(size)) |
| continue; |
| tileSizes[pos] = tileSizes[pos] / size; |
| } |
| if (!outerDimsPerm.empty()) { |
| tileSizes.resize(numLoops, 0); |
| applyPermutationToVector(tileSizes, outerDimsPerm); |
| } |
| } |
| |
| return true; |
| }) |
| .Default([&](auto) { |
| if (reductionTileSizeMap.contains(op)) { |
| tileSizesList.push_back(reductionTileSizeMap[op]); |
| scalableTileFlagsList.push_back( |
| reductionScalableFlagseMap[op]); |
| } else { |
| tileSizesList.push_back(zeros); |
| scalableTileFlagsList.push_back(falseVec); |
| } |
| // Only copy the inner vector tile sizes on parallel dims. |
| SmallVector<int64_t> vecTileSizes(numLoops, 0); |
| SmallVector<bool> vecScalableTileFlags(numLoops, false); |
| auto iterTypes = |
| cast<TilingInterface>(op).getLoopIteratorTypes(); |
| for (auto [idx, iterType] : llvm::enumerate(iterTypes)) { |
| if (iterType == utils::IteratorType::parallel) { |
| vecTileSizes[idx] = innerVecTileSizes[idx]; |
| vecScalableTileFlags[idx] = innerVecScalableTileFlags[idx]; |
| } |
| } |
| tileSizesList.push_back(vecTileSizes); |
| scalableTileFlagsList.push_back(vecScalableTileFlags); |
| |
| return true; |
| }); |
| |
| // TODO: (awarzynski) This is effectively tracking the case of |
| // tensor.pack + scalable flags, which is not support ATM (see TODO |
| // above). Remove once that's implemented. |
| if (!setUpOK) |
| return failure(); |
| } |
| |
| for (auto &ts : tileSizesList) |
| ts.resize(numLoops, 0); |
| for (auto &ts : scalableTileFlagsList) |
| ts.resize(numLoops, 0); |
| auto config = IREE::Codegen::LoweringConfigAttr::get(ctx, tileSizesList, |
| scalableTileFlagsList); |
| setLoweringConfig(op, config); |
| } |
| |
| return success(); |
| } |
| |
| /// Helper method to set the dispatch to be lowered through the default |
| /// pipeline. |
| static LogicalResult |
| lowerUsingDefaultPipeline(mlir::FunctionOpInterface entryPointFn) { |
| // If there is a translation info set, do nothing. |
| if (getTranslationInfo(entryPointFn)) { |
| return success(); |
| } |
| // Otherwise lower using default pipeline. |
| auto translationInfo = IREE::Codegen::TranslationInfoAttr::get( |
| entryPointFn->getContext(), DispatchLoweringPassPipeline::CPUDefault); |
| return setTranslationInfo(entryPointFn, translationInfo); |
| } |
| |
| /// Sets the translation information to use for a dispatch region. |
| static LogicalResult |
| setTranslationInfoAndRootConfig(mlir::FunctionOpInterface entryPointFn, |
| ArrayRef<Operation *> computeOps) { |
| // Make sure that lowering_config is not preset on any compute ops. |
| for (auto computeOp : computeOps) { |
| if (getLoweringConfig(computeOp)) |
| return failure(); |
| } |
| |
| FailureOr<Operation *> rootOp = getRootOperation(computeOps); |
| if (failed(rootOp)) |
| return failure(); |
| Operation *rootOperation = rootOp.value(); |
| |
| // Handle the case with no known root operation. |
| if (!rootOperation) { |
| return lowerUsingDefaultPipeline(entryPointFn); |
| } |
| |
| LLVM_DEBUG(KD_DBGS() << "Root op: " << *rootOperation << "\n"); |
| |
| auto targetAttr = IREE::HAL::ExecutableTargetAttr::lookup(entryPointFn); |
| auto targetMLTransInfo = |
| TargetMLTransformInfo::getTargetMLTransformInfo(targetAttr); |
| if (failed( |
| setRootConfigImpl(entryPointFn, rootOperation, targetMLTransInfo))) { |
| return failure(); |
| } |
| |
| // The transform dialect codegen has differnet logics and codegen flow. |
| // Ignore the tile sizes adjustment. |
| auto pipeline = getTranslationInfo(entryPointFn).getPassPipeline().getValue(); |
| if (pipeline != DispatchLoweringPassPipeline::TransformDialectCodegen) { |
| if (failed(adjustTileSizesForUnPackOp(entryPointFn, rootOperation))) { |
| return failure(); |
| } |
| |
| // Avoid this for ops within a custom_op since those ops have already their |
| // configuration set. |
| auto prunedComputeOps = |
| llvm::to_vector(llvm::make_filter_range(computeOps, [](Operation *op) { |
| return !isa_and_nonnull<IREE::LinalgExt::CustomOp>( |
| op->getParentOp()) || |
| getLoweringConfig<IREE::Codegen::LoweringConfigAttr>(op) == |
| nullptr; |
| })); |
| if (failed(setLoweringConfigForComputeOps(entryPointFn, prunedComputeOps, |
| rootOperation))) { |
| return failure(); |
| } |
| } |
| |
| return success(); |
| } |
| |
| LogicalResult initCPULaunchConfig(FunctionOpInterface funcOp) { |
| if (getTranslationInfo(funcOp)) { |
| return success(); |
| } |
| |
| // For now pick the default for functions with control flow, cause |
| // the currently built pipelines dont work so well with control flow. |
| if (funcOp.empty() || !llvm::hasSingleElement(funcOp.getFunctionBody())) { |
| return lowerUsingDefaultPipeline(funcOp); |
| } |
| |
| SmallVector<Operation *> computeOps = getComputeOps(funcOp); |
| if (failed(setTranslationInfoAndRootConfig(funcOp, computeOps))) { |
| return failure(); |
| } |
| |
| // The root configuration setting introduces `tensor.dim` operations. |
| // Resolve those away. |
| RewritePatternSet patterns(funcOp.getContext()); |
| memref::populateResolveRankedShapedTypeResultDimsPatterns(patterns); |
| return applyPatternsGreedily(funcOp, std::move(patterns)); |
| } |
| |
| } // namespace mlir::iree_compiler |