| //===- TilingInterfaceImpl.cpp - Implementation of TilingInterface -------===// |
| // |
| // Part of the LLVM Project, 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 "mlir/Dialect/Linalg/Transforms/TilingInterfaceImpl.h" |
| |
| #include "mlir/Analysis/SliceAnalysis.h" |
| #include "mlir/Dialect/Affine/IR/AffineOps.h" |
| #include "mlir/Dialect/Affine/Utils.h" |
| #include "mlir/Dialect/Arith/IR/Arith.h" |
| #include "mlir/Dialect/Arith/Utils/Utils.h" |
| #include "mlir/Dialect/Linalg/IR/Linalg.h" |
| #include "mlir/Dialect/Linalg/Utils/Utils.h" |
| #include "mlir/Dialect/MemRef/IR/MemRef.h" |
| #include "mlir/Dialect/SCF/IR/SCF.h" |
| #include "mlir/Dialect/Tensor/IR/Tensor.h" |
| #include "mlir/Dialect/Utils/IndexingUtils.h" |
| #include "mlir/Dialect/Utils/StaticValueUtils.h" |
| #include "mlir/Dialect/Utils/StructuredOpsUtils.h" |
| #include "mlir/IR/BuiltinTypeInterfaces.h" |
| #include "mlir/Interfaces/TilingInterface.h" |
| #include "mlir/Interfaces/ValueBoundsOpInterface.h" |
| #include "llvm/ADT/SmallVectorExtras.h" |
| #include "llvm/Support/Debug.h" |
| #include <optional> |
| |
| #define DEBUG_TYPE "linalg-tiling-interface-impl" |
| |
| using namespace mlir; |
| using namespace mlir::linalg; |
| |
| //===----------------------------------------------------------------------===// |
| // Utility methods for implementation of Tiling Interface for Linalg ops |
| //===----------------------------------------------------------------------===// |
| |
| /// Return the SSA values that represent the data point accessed using a given |
| /// `indexingMap` for a given point in the iteration space represented by `ivs`. |
| static SmallVector<Value> getIndicesForAccess(OpBuilder &b, Location loc, |
| AffineMap indexingMap, |
| ValueRange ivs) { |
| SmallVector<Value> indices; |
| indices.reserve(indexingMap.getNumResults()); |
| for (auto result : indexingMap.getResults()) { |
| AffineMap m = AffineMap::get(indexingMap.getNumDims(), |
| indexingMap.getNumSymbols(), result); |
| Value v = affine::AffineApplyOp::create(b, loc, m, ivs); |
| indices.push_back(v); |
| } |
| return indices; |
| } |
| |
| /// Method to inline the payload of a `linalgOp` given the iteration space |
| /// point and values for the arguments of the payload. |
| static LogicalResult inlinePayload(OpBuilder &b, LinalgOp linalgOp, |
| ValueRange ivs, ValueRange argValues) { |
| Block *body = linalgOp.getBlock(); |
| IRMapping map; |
| map.map(body->getArguments(), argValues); |
| for (auto &op : body->without_terminator()) { |
| if (auto indexOp = dyn_cast<IndexOp>(&op)) { |
| map.map(indexOp.getResult(), ivs[indexOp.getDim()]); |
| continue; |
| } |
| b.clone(op, map); |
| } |
| |
| Operation *terminator = body->getTerminator(); |
| Location loc = terminator->getLoc(); |
| for (const auto &operand : llvm::enumerate(terminator->getOperands())) { |
| Value toStore = map.lookupOrDefault(operand.value()); |
| OpOperand *storeInto = linalgOp.getDpsInitOperand(operand.index()); |
| auto indices = getIndicesForAccess( |
| b, loc, linalgOp.getMatchingIndexingMap(storeInto), ivs); |
| memref::StoreOp::create(b, loc, toStore, |
| linalgOp.getDpsInitOperand(operand.index())->get(), |
| indices); |
| } |
| return success(); |
| } |
| |
| /// Verify that tiling can be applied in presence of semi-affine maps. |
| static LogicalResult |
| validateTilingSemiAffineMaps(LinalgOp linalgOp, ArrayRef<OpFoldResult> sizes) { |
| // Precompute each dimension's constant tile-size upper bound once. |
| // A failed entry marks a dynamic tile with no static bound. |
| SmallVector<FailureOr<int64_t>> tileSizeBounds = |
| llvm::map_to_vector(sizes, [](OpFoldResult size) { |
| return ValueBoundsConstraintSet::computeConstantBound( |
| presburger::BoundType::UB, size, |
| /*stopCondition=*/nullptr, ValueBoundsOptions{/*closedUB=*/true}); |
| }); |
| SmallVector<int64_t> loopRanges = linalgOp.getStaticLoopRanges(); |
| |
| // Dynamic tiles or dynamic loop ranges are conservatively treated as tiled. |
| SmallVector<bool> tiledDims(loopRanges.size(), false); |
| for (auto [pos, tileSize] : llvm::enumerate(tileSizeBounds)) { |
| if (failed(tileSize)) { |
| tiledDims[pos] = true; |
| continue; |
| } |
| if (*tileSize == 0) |
| continue; |
| tiledDims[pos] = |
| ShapedType::isDynamic(loopRanges[pos]) || *tileSize < loopRanges[pos]; |
| } |
| |
| for (AffineMap map : linalgOp.getIndexingMapsArray()) { |
| for (AffineExpr result : map.getResults()) { |
| WalkResult status = result.walk([&](AffineExpr expr) -> WalkResult { |
| auto binExpr = dyn_cast<AffineBinaryOpExpr>(expr); |
| if (!binExpr) |
| return WalkResult::advance(); |
| AffineExprKind kind = binExpr.getKind(); |
| if (kind != AffineExprKind::Mod && kind != AffineExprKind::FloorDiv && |
| kind != AffineExprKind::CeilDiv) |
| return WalkResult::advance(); |
| |
| // Skip if the semi-affine expression does not involve any tiled |
| // dimension: an untiled dimension keeps its full extent in every tile, |
| // so re-applying the map on the slice is exact. |
| bool involvesTiledDim = expr.walk([&](AffineExpr e) -> WalkResult { |
| auto dim = dyn_cast<AffineDimExpr>(e); |
| if (dim && tiledDims[dim.getPosition()]) |
| return WalkResult::interrupt(); |
| return WalkResult::advance(); |
| }) |
| .wasInterrupted(); |
| if (!involvesTiledDim) |
| return WalkResult::advance(); |
| |
| // Allow only `d OP C` map where `d` is a dimension and `C` is a |
| // constant. A compound LHS (e.g. `(d0 + d1)`, `(d0 * 2)`, a nested |
| // semi-affine expression) or a non-constant step is not provably safe, |
| // so reject it. |
| auto dimExpr = dyn_cast<AffineDimExpr>(binExpr.getLHS()); |
| auto stepExpr = dyn_cast<AffineConstantExpr>(binExpr.getRHS()); |
| if (!dimExpr || !stepExpr || stepExpr.getValue() <= 0) { |
| linalgOp.emitOpError() |
| << "tiling is not supported for the semi-affine indexing map: " |
| "only a single iteration dimension divided by a positive " |
| "constant step can be tiled over a tiled dimension"; |
| return WalkResult::interrupt(); |
| } |
| |
| // Tiles are spaced by the full tile size, so tile origins are its |
| // multiples (0, tileSize, 2*tileSize, ...). |
| // A tile's indices are `origin + d'`, with `origin` the tile's start |
| // and `0 <= d' < tileSize`. A trailing partial tile is a full tile |
| // truncated at the same origin, spanning a subset of the same `d'`, so |
| // full-tile validity implies partial-tile validity and validating the |
| // upper-bound tile size suffices. |
| unsigned dimPos = dimExpr.getPosition(); |
| FailureOr<int64_t> tileSize = tileSizeBounds[dimPos]; |
| |
| // Dynamic tile sizes are assumed to be valid. |
| // Unit tile is always valid. |
| if (failed(tileSize) || *tileSize == 1) |
| return WalkResult::advance(); |
| |
| // Tiled op reuses the same map on a slice whose base offset is |
| // `m(origin) - m(0)`, so it is correct only when |
| // `m(origin + d') == (m(origin) - m(0)) + m(d')` for every `d'`. |
| // Slice origins are tile-size multiples, so this reduces to a relation |
| // between the tile size and the step `C`: |
| // - `floordiv`/`mod` are locally affine within a step window (floordiv |
| // is constant, mod is linear), so they compose when the origin is |
| // step-aligned (`C | tileSize`) or the whole tile fits in one window |
| // (`tileSize | C`); |
| // - `ceildiv` jumps at `k * C + 1` instead of `k * C`, so a |
| // non-step-aligned origin already straddles the jump. It composes |
| // only from a step-aligned origin, i.e. `C | tileSize`. |
| int64_t step = stepExpr.getValue(); |
| bool isCeil = kind == AffineExprKind::CeilDiv; |
| bool safe = *tileSize % step == 0 || (!isCeil && step % *tileSize == 0); |
| if (!safe) { |
| linalgOp.emitOpError() |
| << "tiling is not supported for the semi-affine indexing map: " |
| "tile size " |
| << *tileSize << " for dimension d" << dimPos |
| << (isCeil ? " must be a multiple of the step " |
| : " must divide or be divisible by the step ") |
| << step; |
| return WalkResult::interrupt(); |
| } |
| return WalkResult::advance(); |
| }); |
| if (status.wasInterrupted()) |
| return failure(); |
| } |
| } |
| return success(); |
| } |
| |
| //===----------------------------------------------------------------------===// |
| // External Model for implementing `TilingInterface` for `LinalgOp`s. |
| //===----------------------------------------------------------------------===// |
| |
| namespace { |
| /// Operation-independent implementation shared by the external models for |
| /// LinalgOps. External models are used for now until `TilingInterface` is |
| /// on-par with the current Linalg tiling and fusion patterns. It may then be |
| /// possible to move this into the op definitions, though there are advantages |
| /// to leaving it as an external model. |
| struct LinalgOpTilingInterfaceImpl { |
| /// Return the iteration domain range. |
| SmallVector<Range> getIterationDomain(Operation *op, OpBuilder &b) const { |
| OpBuilder::InsertionGuard g(b); |
| b.setInsertionPoint(op); |
| Location loc = op->getLoc(); |
| LinalgOp linalgOp = cast<LinalgOp>(op); |
| SmallVector<OpFoldResult> allShapesSizes = |
| linalgOp.createFlatListOfOperandDims(b, loc); |
| AffineMap map = linalgOp.getShapesToLoopsMap(); |
| |
| return llvm::map_to_vector(map.getResults(), [&](AffineExpr loopExpr) { |
| OpFoldResult ofr = affine::makeComposedFoldedAffineApply(b, loc, loopExpr, |
| allShapesSizes); |
| return Range{b.getIndexAttr(0), ofr, b.getIndexAttr(1)}; |
| }); |
| } |
| |
| /// Instantiate the tiled implementation of the operation. |
| FailureOr<TilingResult> |
| getTiledImplementation(Operation *op, OpBuilder &b, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| // Leave the `sizeBounds` value empty. That is only needed when the `sizes` |
| // specified could lead to out of bounds accesses. |
| Location loc = op->getLoc(); |
| LinalgOp linalgOp = cast<LinalgOp>(op); |
| // In case of a semi-affine expression, generalized tracking of tiles would |
| // require a per-tile-position shift that cannot be expressed by the |
| // symbol-free indexing maps. |
| // Thus, tiling is allowed only when the semi-affine maps can be proven safe |
| // for the current tiling configuration. Otherwise, tiling can end up |
| // producing incorrect results. |
| if (failed(validateTilingSemiAffineMaps(linalgOp, sizes))) |
| return failure(); |
| SmallVector<Value> valuesToTile = linalgOp->getOperands(); |
| SmallVector<Value> tiledOperands = makeTiledShapes( |
| b, loc, linalgOp, valuesToTile, offsets, sizes, {}, true); |
| SmallVector<Operation *> generatedSlices = llvm::map_to_vector( |
| llvm::make_filter_range( |
| tiledOperands, |
| [](Value v) -> bool { |
| return isa_and_nonnull<tensor::ExtractSliceOp, memref::SubViewOp>( |
| v.getDefiningOp()); |
| }), |
| [](Value v) -> Operation * { return v.getDefiningOp(); }); |
| |
| SmallVector<Type> resultTensorTypes = |
| getTensorOutputTypes(linalgOp, tiledOperands); |
| |
| Operation *tiledOp = clone(b, linalgOp, resultTensorTypes, tiledOperands); |
| offsetIndices(b, cast<LinalgOp>(tiledOp), offsets); |
| |
| return TilingResult{ |
| {tiledOp}, SmallVector<Value>(tiledOp->getResults()), generatedSlices}; |
| } |
| |
| /// Utility to fetch the offsets and sizes when applied as per the indexing |
| /// map of the linalg op. This helps in fusing the linalg op as a consumer of |
| /// a given slice op. |
| static LogicalResult |
| getMappedOffsetAndSize(LinalgOp linalgOp, OpBuilder &b, |
| ArrayRef<AffineMap> indexingMaps, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| SmallVectorImpl<OpFoldResult> &mappedOffsetsVec, |
| SmallVectorImpl<OpFoldResult> &mappedSizesVec) { |
| DenseMap<unsigned, OpFoldResult> mappedOffsets, mappedSizes; |
| |
| for (auto [indexingMap, offsets, sizes] : |
| llvm::zip_equal(indexingMaps, allOffsets, allSizes)) { |
| for (auto [resultExpr, offset, size] : |
| llvm::zip_equal(indexingMap.getResults(), offsets, sizes)) { |
| auto dimExpr = dyn_cast<AffineDimExpr>(resultExpr); |
| if (!dimExpr) |
| return failure(); |
| unsigned position = dimExpr.getPosition(); |
| auto it = mappedOffsets.find(position); |
| if (it != mappedOffsets.end()) { |
| OpFoldResult seenOffset = it->second; |
| OpFoldResult seenSize = mappedSizes.lookup(position); |
| if (seenOffset != offset || seenSize != size) { |
| LLVM_DEBUG({ |
| llvm::dbgs() << "inconsistent iteration space mapping from " |
| "offsets/sizes of operands/results"; |
| }); |
| return failure(); |
| } |
| } else { |
| mappedOffsets[position] = offset; |
| mappedSizes[position] = size; |
| } |
| } |
| } |
| |
| // Aggregate from the given operand offsets and sizes, or default to |
| // iteration space values. |
| SmallVector<Range> iterationDomain = |
| cast<TilingInterface>(linalgOp.getOperation()).getIterationDomain(b); |
| mappedOffsetsVec.resize(iterationDomain.size()); |
| mappedSizesVec.resize(iterationDomain.size()); |
| for (auto [index, domain] : llvm::enumerate(iterationDomain)) { |
| auto it = mappedOffsets.find(index); |
| if (it != mappedOffsets.end()) { |
| mappedOffsetsVec[index] = it->second; |
| mappedSizesVec[index] = mappedSizes.lookup(index); |
| continue; |
| } |
| mappedOffsetsVec[index] = domain.offset; |
| mappedSizesVec[index] = domain.size; |
| } |
| return success(); |
| } |
| |
| /// Method to return the position of the result tile computed by the tiled |
| /// operation. |
| LogicalResult getIterationDomainTileFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| SmallVectorImpl<OpFoldResult> &iterDomainOffsets, |
| SmallVectorImpl<OpFoldResult> &iterDomainSizes) const { |
| auto linalgOp = cast<LinalgOp>(op); |
| |
| SmallVector<AffineMap> indexingMaps = |
| llvm::map_to_vector(operandNumbers, [&](unsigned operandNumber) { |
| OpOperand &opOperand = linalgOp->getOpOperand(operandNumber); |
| return linalgOp.getMatchingIndexingMap(&opOperand); |
| }); |
| if (failed(getMappedOffsetAndSize(linalgOp, b, indexingMaps, allOffsets, |
| allSizes, iterDomainOffsets, |
| iterDomainSizes))) { |
| return failure(); |
| } |
| return success(); |
| } |
| |
| /// Return the details of the output tile generated by the tiled |
| /// implementation. |
| LogicalResult |
| getResultTilePosition(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, |
| SmallVector<OpFoldResult> &resultOffsets, |
| SmallVector<OpFoldResult> &resultSizes) const { |
| Location loc = op->getLoc(); |
| LinalgOp linalgOp = cast<LinalgOp>(op); |
| |
| AffineExpr d0; |
| bindDims(b.getContext(), d0); |
| SmallVector<OpFoldResult> subShapeSizes = |
| llvm::map_to_vector(sizes, [&](OpFoldResult ofr) { |
| return affine::makeComposedFoldedAffineApply(b, loc, d0 - 1, ofr); |
| }); |
| |
| OpOperand *outOperand = linalgOp.getDpsInitOperand(resultNumber); |
| SliceParameters sliceParams = computeSliceParameters( |
| b, loc, outOperand->get(), sizes, |
| linalgOp.getMatchingIndexingMap(outOperand), offsets, |
| /*ubs*/ {}, subShapeSizes, true); |
| resultOffsets = sliceParams.offsets; |
| resultSizes = sliceParams.sizes; |
| return success(); |
| } |
| |
| LogicalResult getIterationDomainTileFromResultTile( |
| Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, ArrayRef<OpFoldResult> sizes, |
| SmallVectorImpl<OpFoldResult> &iterDomainOffsets, |
| SmallVectorImpl<OpFoldResult> &iterDomainSizes) const { |
| auto linalgOp = cast<LinalgOp>(op); |
| |
| // Check that the indexing map used for the output is a projected |
| // permutation. This could be relaxed with a more general approach that can |
| // map the offsets and sizes from the result to iteration space tiles |
| // (filling in full extent for dimensions not used to access the result). |
| AffineMap indexingMap = |
| linalgOp.getIndexingMapMatchingResult(op->getResult(resultNumber)); |
| if (!indexingMap.isProjectedPermutation()) { |
| return op->emitOpError( |
| "unhandled tiled implementation generation when result is not " |
| "accessed using a permuted projection"); |
| } |
| |
| SmallVector<OpFoldResult> allOffsets = llvm::to_vector(offsets); |
| SmallVector<OpFoldResult> allSizes = llvm::to_vector(sizes); |
| auto status = |
| getMappedOffsetAndSize(linalgOp, b, indexingMap, {allOffsets}, |
| {allSizes}, iterDomainOffsets, iterDomainSizes); |
| (void)status; |
| assert(succeeded(status) && "unexpected error in offset calculation"); |
| return success(); |
| } |
| |
| FailureOr<TilingResult> |
| generateResultTileValue(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| SmallVector<OpFoldResult> mappedOffsets, mappedSizes; |
| if (failed(getIterationDomainTileFromResultTile( |
| op, b, resultNumber, offsets, sizes, mappedOffsets, mappedSizes))) { |
| return failure(); |
| } |
| auto tilingInterfaceOp = cast<TilingInterface>(op); |
| FailureOr<TilingResult> tilingResult = |
| tilingInterfaceOp.getTiledImplementation(b, mappedOffsets, mappedSizes); |
| |
| if (failed(tilingResult)) |
| return failure(); |
| |
| if (tilingResult->tiledOps.size() != 1) |
| return op->emitOpError("failed to generate tiled implementation"); |
| |
| return TilingResult{ |
| tilingResult->tiledOps, |
| SmallVector<Value>{tilingResult->tiledValues[resultNumber]}, |
| tilingResult->generatedSlices}; |
| } |
| |
| /// Method to generate the tiled implementation of an operation from the tile |
| /// of the operand. |
| FailureOr<TilingResult> getTiledImplementationFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes) const { |
| SmallVector<OpFoldResult> mappedOffsets, mappedSizes; |
| if (failed(getIterationDomainTileFromOperandTiles( |
| op, b, operandNumbers, allOffsets, allSizes, mappedOffsets, |
| mappedSizes))) { |
| return failure(); |
| } |
| return getTiledImplementation(op, b, mappedOffsets, mappedSizes); |
| } |
| |
| LogicalResult generateScalarImplementation(Operation *op, OpBuilder &builder, |
| Location loc, |
| ValueRange ivs) const { |
| auto linalgOp = cast<LinalgOp>(op); |
| if (!linalgOp.hasPureBufferSemantics()) |
| return op->emitOpError("expected operation to have buffer semantics"); |
| |
| SmallVector<Value> indexedValues; |
| indexedValues.reserve(linalgOp->getNumOperands()); |
| Location linalgOpLoc = op->getLoc(); |
| /// Load the data corresponding to the block arguments that |
| /// represent input operands. |
| for (OpOperand &operand : linalgOp->getOpOperands()) { |
| if (!linalgOp.payloadUsesValueFromOperand(&operand)) { |
| indexedValues.push_back(nullptr); |
| continue; |
| } |
| if (linalgOp.isScalar(&operand)) { |
| indexedValues.push_back(operand.get()); |
| continue; |
| } |
| SmallVector<Value> indices = getIndicesForAccess( |
| builder, linalgOpLoc, linalgOp.getMatchingIndexingMap(&operand), ivs); |
| Value load = |
| memref::LoadOp::create(builder, linalgOpLoc, operand.get(), indices); |
| indexedValues.push_back(load); |
| } |
| |
| /// Inline the op payload and store the result. |
| return inlinePayload(builder, linalgOp, ivs, indexedValues); |
| } |
| |
| bool isOpFusableWithConsumerSlice(Operation *op, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| // The verifier gives all the necessary requirements for consumer fusion. |
| return true; |
| } |
| |
| bool isOpFusableWithProducerSlices( |
| Operation *op, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes) const { |
| |
| auto linalgOp = cast<LinalgOp>(op); |
| SmallVector<AffineMap> indexingMaps = |
| llvm::map_to_vector(operandNumbers, [&](unsigned operandNumber) { |
| OpOperand &opOperand = linalgOp->getOpOperand(operandNumber); |
| return linalgOp.getMatchingIndexingMap(&opOperand); |
| }); |
| // Check that offsets/sizes are consistent across all operands. |
| OpBuilder b(op); |
| SmallVector<OpFoldResult> mappedOffsets, mappedSizes; |
| return succeeded(getMappedOffsetAndSize(linalgOp, b, indexingMaps, |
| allOffsets, allSizes, mappedOffsets, |
| mappedSizes)); |
| } |
| }; |
| |
| template <typename LinalgOpTy> |
| struct LinalgOpTilingInterfaceModel |
| : public TilingInterface::ExternalModel< |
| LinalgOpTilingInterfaceModel<LinalgOpTy>, LinalgOpTy>, |
| public LinalgOpTilingInterfaceImpl { |
| using ExternalModel = |
| TilingInterface::ExternalModel<LinalgOpTilingInterfaceModel<LinalgOpTy>, |
| LinalgOpTy>; |
| |
| using LinalgOpTilingInterfaceImpl::generateScalarImplementation; |
| using LinalgOpTilingInterfaceImpl::getIterationDomain; |
| using LinalgOpTilingInterfaceImpl::getIterationDomainTileFromResultTile; |
| using LinalgOpTilingInterfaceImpl::getResultTilePosition; |
| using LinalgOpTilingInterfaceImpl::isOpFusableWithConsumerSlice; |
| using LinalgOpTilingInterfaceImpl::isOpFusableWithProducerSlices; |
| |
| /// Return the loop iterator type without a dynamic LinalgOp interface lookup. |
| SmallVector<utils::IteratorType> getLoopIteratorTypes(Operation *op) const { |
| return cast<LinalgOpTy>(op).getIteratorTypesArray(); |
| } |
| |
| // Preserve the hint-bearing ExternalModel defaults while routing the |
| // no-hint overloads through the shared implementation. |
| using ExternalModel::generateResultTileValue; |
| FailureOr<TilingResult> |
| generateResultTileValue(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| return LinalgOpTilingInterfaceImpl::generateResultTileValue( |
| op, b, resultNumber, offsets, sizes); |
| } |
| |
| using ExternalModel::getIterationDomainTileFromOperandTiles; |
| LogicalResult getIterationDomainTileFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| SmallVectorImpl<OpFoldResult> &iterDomainOffsets, |
| SmallVectorImpl<OpFoldResult> &iterDomainSizes) const { |
| return LinalgOpTilingInterfaceImpl::getIterationDomainTileFromOperandTiles( |
| op, b, operandNumbers, allOffsets, allSizes, iterDomainOffsets, |
| iterDomainSizes); |
| } |
| |
| using ExternalModel::getTiledImplementation; |
| FailureOr<TilingResult> |
| getTiledImplementation(Operation *op, OpBuilder &b, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| return LinalgOpTilingInterfaceImpl::getTiledImplementation(op, b, offsets, |
| sizes); |
| } |
| |
| using ExternalModel::getTiledImplementationFromOperandTiles; |
| FailureOr<TilingResult> getTiledImplementationFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes) const { |
| return LinalgOpTilingInterfaceImpl::getTiledImplementationFromOperandTiles( |
| op, b, operandNumbers, allOffsets, allSizes); |
| } |
| }; |
| |
| //===----------------------------------------------------------------------===// |
| // External Model for implementing `PartialReductionInterface` for `LinalgOp`s. |
| //===----------------------------------------------------------------------===// |
| |
| /// In a given set vector, get the position of a particular element. |
| std::optional<int> getPositionIn(const llvm::SetVector<unsigned> &reductionDims, |
| unsigned value) { |
| for (auto [index, reductionDim] : llvm::enumerate(reductionDims)) { |
| if (reductionDim == value) { |
| return index; |
| } |
| } |
| return std::nullopt; |
| } |
| |
| /// Return an AffineMaps to use for the `outs` operands of the linalg op |
| /// generated for partial results. The new AffineMap is the AffineMap of the |
| /// untiled op with reduction dimensions appended at end in order in which they |
| /// were specified during tiling. |
| static SmallVector<AffineMap> |
| getPartialResultAffineMaps(LinalgOp linalgOp, |
| const SetVector<unsigned> &reductionDims) { |
| auto partialReductionMaps = llvm::map_to_vector( |
| linalgOp.getDpsInitsMutable(), [&](OpOperand &opOperand) { |
| AffineMap map = linalgOp.getMatchingIndexingMap(&opOperand); |
| for (auto redPos : reductionDims) { |
| map = |
| map.insertResult(getAffineDimExpr(redPos, linalgOp.getContext()), |
| map.getNumResults()); |
| } |
| return map; |
| }); |
| return partialReductionMaps; |
| } |
| |
| struct InitSliceInfo { |
| SmallVector<int64_t> resultShape; |
| SmallVector<OpFoldResult> offsets; |
| SmallVector<OpFoldResult> sizes; |
| SmallVector<OpFoldResult> strides; |
| }; |
| |
| /// Return the result shape, offsets, sizes and strides of the slice of the |
| /// `initValue` to use as the destination of the partial reduction op generated |
| /// with outer reduction strategy. |
| static InitSliceInfo getInitSliceInfoForOuterReduction( |
| MLIRContext *context, ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, const SetVector<unsigned> &reductionDims, |
| ArrayRef<OpFoldResult> splitReductionIvs, AffineMap partialReductionMap, |
| ArrayRef<OpFoldResult> initOperandShape) { |
| int64_t initRank = partialReductionMap.getNumResults(); |
| SmallVector<OpFoldResult> initOffsets, initSizes; |
| Attribute zero = IntegerAttr::get(IndexType::get(context), 0); |
| Attribute one = IntegerAttr::get(IndexType::get(context), 1); |
| SmallVector<OpFoldResult> initStrides(initRank, one); |
| for (auto [resultIdx, dimExpr] : |
| llvm::enumerate(partialReductionMap.getResults())) { |
| if (isa<AffineConstantExpr>(dimExpr)) { |
| // A constant index in the output map accesses a fixed position; keep |
| // the full output dimension to match the original output operand shape. |
| initOffsets.push_back(zero); |
| initSizes.push_back(initOperandShape[resultIdx]); |
| continue; |
| } |
| unsigned dim = cast<AffineDimExpr>(dimExpr).getPosition(); |
| if (reductionDims.contains(dim)) { |
| initOffsets.push_back(zero); |
| } else { |
| initOffsets.push_back(offsets[dim]); |
| } |
| initSizes.push_back(sizes[dim]); |
| } |
| SmallVector<int64_t> resultShape; |
| std::tie(resultShape, std::ignore) = decomposeMixedValues(initSizes); |
| return {resultShape, initOffsets, initSizes, initStrides}; |
| } |
| |
| /// Return the result shape, offsets, sizes and strides of the slice of the |
| /// `initValue` to use as destination of the partial reduction op generated with |
| /// outer parallel strategy. |
| static InitSliceInfo getInitSliceInfoForOuterParallel( |
| MLIRContext *context, ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, const SetVector<unsigned> &reductionDims, |
| ArrayRef<OpFoldResult> splitReductionIvs, AffineMap partialReductionMap, |
| ArrayRef<OpFoldResult> initOperandShape) { |
| int64_t initRank = partialReductionMap.getNumResults(); |
| SmallVector<OpFoldResult> initOffsets, initSizes; |
| Attribute zero = IntegerAttr::get(IndexType::get(context), 0); |
| Attribute one = IntegerAttr::get(IndexType::get(context), 1); |
| SmallVector<OpFoldResult> initStrides(initRank, one); |
| SmallVector<OpFoldResult> resultShape; |
| for (auto [resultIdx, dimExpr] : |
| llvm::enumerate(partialReductionMap.getResults())) { |
| if (isa<AffineConstantExpr>(dimExpr)) { |
| // A constant index accesses a fixed position; keep the full output |
| // dimension to match the original output operand shape. |
| initOffsets.push_back(zero); |
| initSizes.push_back(initOperandShape[resultIdx]); |
| resultShape.push_back(initOperandShape[resultIdx]); |
| continue; |
| } |
| unsigned dim = cast<AffineDimExpr>(dimExpr).getPosition(); |
| if (std::optional<unsigned> dimPos = getPositionIn(reductionDims, dim)) { |
| initOffsets.push_back(splitReductionIvs[dimPos.value()]); |
| initSizes.push_back(one); |
| } else { |
| initOffsets.push_back(offsets[dim]); |
| initSizes.push_back(sizes[dim]); |
| resultShape.push_back(sizes[dim]); |
| } |
| } |
| SmallVector<int64_t> staticShapes; |
| std::tie(staticShapes, std::ignore) = decomposeMixedValues(resultShape); |
| return {staticShapes, initOffsets, initSizes, initStrides}; |
| } |
| |
| /// Return the result shape, offsets, sizes and strides of the slice of the |
| /// `initValue` to use as destination of the partial reduction op. |
| static InitSliceInfo getInitSliceInfo(MLIRContext *context, |
| ReductionTilingStrategy strategy, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, |
| const SetVector<unsigned> &reductionDims, |
| ArrayRef<OpFoldResult> splitReductionIvs, |
| AffineMap partialReductionMap, |
| ArrayRef<OpFoldResult> initOperandShape) { |
| if (strategy == ReductionTilingStrategy::PartialReductionOuterReduction) { |
| return getInitSliceInfoForOuterReduction( |
| context, offsets, sizes, reductionDims, splitReductionIvs, |
| partialReductionMap, initOperandShape); |
| } |
| assert(strategy == ReductionTilingStrategy::PartialReductionOuterParallel && |
| "unexpected ReductionTilingStrategy"); |
| return getInitSliceInfoForOuterParallel( |
| context, offsets, sizes, reductionDims, splitReductionIvs, |
| partialReductionMap, initOperandShape); |
| } |
| |
| /// Operation-independent implementation shared by the |
| /// PartialReductionInterface external models for LinalgOps. |
| struct LinalgOpPartialReductionInterfaceImpl { |
| FailureOr<SmallVector<Value>> generateInitialTensorForPartialReduction( |
| Operation *op, OpBuilder &b, Location loc, ArrayRef<OpFoldResult> sizes, |
| const SetVector<unsigned> &reductionDims) const { |
| auto linalgOp = cast<LinalgOp>(op); |
| |
| OpBuilder::InsertionGuard guard(b); |
| if (linalgOp.hasPureBufferSemantics()) |
| return op->emitOpError("expected operation to have tensor semantics"); |
| |
| SmallVector<AffineMap> partialResultMaps = |
| getPartialResultAffineMaps(linalgOp, reductionDims); |
| |
| SmallVector<Value> inits; |
| for (auto [initIdx, result, partialMap] : |
| llvm::enumerate(linalgOp->getResults(), partialResultMaps)) { |
| SmallVector<Operation *, 4> combinerOps; |
| if (!matchReduction(linalgOp.getRegionOutputArgs(), initIdx, |
| combinerOps) || |
| combinerOps.size() != 1) |
| return op->emitOpError("Failed to anaysis the reduction operation."); |
| |
| Operation *reductionOp = combinerOps[0]; |
| std::optional<TypedAttr> identity = arith::getNeutralElement(reductionOp); |
| if (!identity.has_value()) |
| return op->emitOpError( |
| "Failed to get an identity value for the reduction operation."); |
| |
| // Append the new partial result dimensions. |
| SmallVector<OpFoldResult> partialResultShape; |
| Value initValue = linalgOp.getDpsInits()[initIdx]; |
| SmallVector<OpFoldResult> initShape = |
| tensor::getMixedSizes(b, loc, initValue); |
| for (auto [resultIdx, dimExpr] : |
| llvm::enumerate(partialMap.getResults())) { |
| if (isa<AffineConstantExpr>(dimExpr)) { |
| // A constant index in the output map accesses a fixed position; use |
| // the actual output dimension size (not a hardcoded 1). |
| partialResultShape.push_back(initShape[resultIdx]); |
| continue; |
| } |
| auto dim = cast<AffineDimExpr>(dimExpr); |
| partialResultShape.push_back(sizes[dim.getPosition()]); |
| } |
| |
| Type elType = getElementTypeOrSelf(result.getType()); |
| Value emptyTensor = |
| tensor::EmptyOp::create(b, loc, partialResultShape, elType); |
| Value constantOp = arith::ConstantOp::create(b, loc, *identity); |
| auto identityTensor = |
| linalg::FillOp::create(b, loc, constantOp, emptyTensor); |
| inits.push_back(identityTensor.getResult(0)); |
| } |
| |
| return inits; |
| } |
| |
| FailureOr<TilingResult> |
| tileToPartialReduction(Operation *op, OpBuilder &b, Location loc, |
| ReductionTilingStrategy tilingStrategy, |
| ValueRange init, ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, |
| const SetVector<unsigned> &reductionDims, |
| ArrayRef<OpFoldResult> splitReductionIvs) const { |
| OpBuilder::InsertionGuard guard(b); |
| auto linalgOp = cast<LinalgOp>(op); |
| |
| SmallVector<AffineMap> partialReductionMaps = |
| getPartialResultAffineMaps(linalgOp, reductionDims); |
| |
| // Step 1. Extend init maps to have reduction dimension dims, since we |
| // are converting them to parallel dimensions. |
| SmallVector<AffineMap> newInitMaps; |
| if (tilingStrategy == |
| ReductionTilingStrategy::PartialReductionOuterReduction) { |
| newInitMaps = llvm::to_vector(partialReductionMaps); |
| } else { |
| newInitMaps = llvm::map_to_vector( |
| linalgOp.getDpsInitsMutable(), [&](OpOperand &opOperand) { |
| return linalgOp.getMatchingIndexingMap(&opOperand); |
| }); |
| } |
| |
| // Step 2a: Extract a slice of the input operands. |
| SmallVector<Value> tiledInputs = makeTiledShapes( |
| b, loc, linalgOp, linalgOp.getDpsInputs(), offsets, sizes, {}, true); |
| SmallVector<Operation *> generatedSlices = llvm::map_to_vector( |
| llvm::make_filter_range( |
| tiledInputs, [](Value v) -> bool { return v.getDefiningOp(); }), |
| [](Value v) -> Operation * { return v.getDefiningOp(); }); |
| |
| // Step 2b: Extract a slice of the init operands. |
| SmallVector<Value, 1> tiledInits; |
| for (auto [partialReductionMap, valueToTile, initOperandValue] : |
| llvm::zip_equal(partialReductionMaps, init, linalgOp.getDpsInits())) { |
| // Compute the actual shape of the original init operand for handling |
| // constant expressions in the partial reduction map. |
| SmallVector<OpFoldResult> initOperandShape = |
| tensor::getMixedSizes(b, loc, initOperandValue); |
| InitSliceInfo sliceInfo = getInitSliceInfo( |
| b.getContext(), tilingStrategy, offsets, sizes, reductionDims, |
| splitReductionIvs, partialReductionMap, initOperandShape); |
| auto valueToTileType = cast<RankedTensorType>(valueToTile.getType()); |
| RankedTensorType sliceResultType = RankedTensorType::get( |
| sliceInfo.resultShape, valueToTileType.getElementType(), |
| valueToTileType.getEncoding()); |
| auto sliceOp = tensor::ExtractSliceOp::create( |
| b, loc, sliceResultType, valueToTile, sliceInfo.offsets, |
| sliceInfo.sizes, sliceInfo.strides); |
| tiledInits.push_back(sliceOp.getResult()); |
| generatedSlices.push_back(sliceOp); |
| } |
| |
| // Update the indexing maps. |
| SmallVector<AffineMap> newMaps = linalgOp.getIndexingMapsArray(); |
| for (auto [initOperand, newInitMap] : |
| llvm::zip_equal(linalgOp.getDpsInitsMutable(), newInitMaps)) { |
| int mapIdx = linalgOp.getIndexingMapIndex(&initOperand); |
| newMaps[mapIdx] = newInitMap; |
| } |
| |
| // Step 3. Change the reduction dim iterator types. |
| SmallVector<utils::IteratorType> newIteratorTypes = |
| linalgOp.getIteratorTypesArray(); |
| if (tilingStrategy == |
| ReductionTilingStrategy::PartialReductionOuterReduction) { |
| for (int dim : reductionDims) |
| newIteratorTypes[dim] = utils::IteratorType::parallel; |
| } |
| |
| // Step 4. Create the new generic op. |
| Operation *partialReductionOp; |
| auto resultTypes = ValueRange(tiledInits).getTypes(); |
| if (tilingStrategy == |
| ReductionTilingStrategy::PartialReductionOuterReduction) { |
| auto genericOp = GenericOp::create(b, loc, resultTypes, tiledInputs, |
| tiledInits, newMaps, newIteratorTypes); |
| IRMapping mapping; |
| op->getRegion(0).cloneInto(&genericOp.getRegion(), |
| genericOp.getRegion().begin(), mapping); |
| offsetIndices(b, genericOp, offsets); |
| partialReductionOp = genericOp.getOperation(); |
| } else { |
| SmallVector<Value> operands = std::move(tiledInputs); |
| llvm::append_range(operands, tiledInits); |
| partialReductionOp = mlir::clone(b, op, resultTypes, operands); |
| offsetIndices(b, cast<LinalgOp>(partialReductionOp), offsets); |
| } |
| return TilingResult{ |
| {partialReductionOp}, |
| llvm::map_to_vector(partialReductionOp->getResults(), |
| [](OpResult r) -> Value { return r; }), |
| generatedSlices}; |
| } |
| |
| FailureOr<MergeResult> |
| mergeReductions(Operation *op, OpBuilder &b, Location loc, |
| ValueRange partialReduce, |
| const SetVector<unsigned> &reductionDims) const { |
| auto linalgOp = cast<LinalgOp>(op); |
| SmallVector<AffineMap> partialReductionMaps = |
| getPartialResultAffineMaps(linalgOp, reductionDims); |
| |
| // Permute the reduction dims as permuted by the partial result map. |
| SmallVector<Operation *> mergeOperations; |
| SmallVector<Value> replacements; |
| for (auto [idx, init, partialResult, partialMap] : llvm::enumerate( |
| linalgOp.getDpsInits(), partialReduce, partialReductionMaps)) { |
| unsigned initIdx = idx; |
| // linalg.reduce's iteration space is the tiled result's iteration space |
| // (and not the tiled operation's iteration space). To account for this, |
| // permute the reduction dimensions based on the partial result map of the |
| // tiled result. |
| SmallVector<int64_t> partialReductionDims; |
| for (auto [resultNum, dimExpr] : |
| llvm::enumerate(partialMap.getResults())) { |
| if (isa<AffineConstantExpr>(dimExpr)) |
| continue; // Constant dims are never reduction dims. |
| unsigned dim = cast<AffineDimExpr>(dimExpr).getPosition(); |
| if (llvm::is_contained(reductionDims, dim)) { |
| partialReductionDims.push_back(resultNum); |
| } |
| } |
| |
| auto reduction = linalg::ReduceOp::create( |
| b, loc, partialResult, init, partialReductionDims, |
| [&linalgOp, &initIdx](OpBuilder &b, Location loc, ValueRange inputs) { |
| // Get the combiner op. |
| SmallVector<Operation *, 4> combinerOps; |
| matchReduction(linalgOp.getRegionOutputArgs(), initIdx, |
| combinerOps); |
| Operation *clonedReductionOp = b.clone(*combinerOps[0]); |
| // Combine the input at idx and output at numInits + idx. |
| clonedReductionOp->setOperand(0, inputs[0]); |
| clonedReductionOp->setOperand(1, inputs[1]); |
| linalg::YieldOp::create(b, loc, clonedReductionOp->getResult(0)); |
| }); |
| |
| mergeOperations.push_back(reduction); |
| replacements.push_back(reduction->getResult(0)); |
| } |
| |
| return MergeResult{mergeOperations, replacements}; |
| } |
| |
| LogicalResult getPartialResultTilePosition( |
| Operation *op, OpBuilder &b, unsigned resultNumber, |
| ReductionTilingStrategy tilingStrategy, ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, const SetVector<unsigned> &reductionDims, |
| ArrayRef<OpFoldResult> splitReductionIvs, |
| SmallVector<OpFoldResult> &resultOffsets, |
| SmallVector<OpFoldResult> &resultSizes) const { |
| auto linalgOp = cast<LinalgOp>(op); |
| SmallVector<AffineMap> partialReductionMaps = |
| getPartialResultAffineMaps(linalgOp, reductionDims); |
| // Compute the actual shape of the init operand for handling constant |
| // expressions in the partial reduction map. |
| Value initOperandValue = linalgOp.getDpsInits()[resultNumber]; |
| Location loc = op->getLoc(); |
| SmallVector<OpFoldResult> initOperandShape = |
| tensor::getMixedSizes(b, loc, initOperandValue); |
| InitSliceInfo sliceInfo = |
| getInitSliceInfo(b.getContext(), tilingStrategy, offsets, sizes, |
| reductionDims, splitReductionIvs, |
| partialReductionMaps[resultNumber], initOperandShape); |
| std::swap(resultOffsets, sliceInfo.offsets); |
| std::swap(resultSizes, sliceInfo.sizes); |
| |
| return success(); |
| } |
| }; |
| |
| template <typename LinalgOpTy> |
| struct LinalgOpPartialReductionInterfaceModel |
| : public PartialReductionOpInterface::ExternalModel< |
| LinalgOpPartialReductionInterfaceModel<LinalgOpTy>, LinalgOpTy>, |
| public LinalgOpPartialReductionInterfaceImpl { |
| using LinalgOpPartialReductionInterfaceImpl:: |
| generateInitialTensorForPartialReduction; |
| using LinalgOpPartialReductionInterfaceImpl::getPartialResultTilePosition; |
| using LinalgOpPartialReductionInterfaceImpl::mergeReductions; |
| using LinalgOpPartialReductionInterfaceImpl::tileToPartialReduction; |
| }; |
| |
| template <typename OpTy> |
| static SmallVector<Range> getPackUnPackIterationDomain(OpTy op, |
| OpBuilder &builder) { |
| static_assert(llvm::is_one_of<OpTy, PackOp, UnPackOp>::value, |
| "applies to only pack or unpack operations"); |
| OpBuilder::InsertionGuard g(builder); |
| int64_t rank = (std::is_same<OpTy, PackOp>::value) ? op.getSourceRank() |
| : op.getDestRank(); |
| OpFoldResult zero = builder.getIndexAttr(0); |
| OpFoldResult one = builder.getIndexAttr(1); |
| ReifiedRankedShapedTypeDims resultShape; |
| (void)op.reifyResultShapes(builder, resultShape); |
| SmallVector<Range> loopBounds(rank); |
| for (auto dim : llvm::seq<int64_t>(0, rank)) { |
| loopBounds[dim].offset = zero; |
| loopBounds[dim].stride = one; |
| loopBounds[dim].size = resultShape[0][dim]; |
| } |
| return loopBounds; |
| } |
| |
| static void applyPermToRange(SmallVector<OpFoldResult> &offsets, |
| SmallVector<OpFoldResult> &sizes, |
| ArrayRef<int64_t> permutation) { |
| if (permutation.empty()) |
| return; |
| applyPermutationToVector<OpFoldResult>(offsets, permutation); |
| applyPermutationToVector<OpFoldResult>(sizes, permutation); |
| } |
| |
| /// Compute the permutation vector to interchange `elements` such that the |
| /// elements at positions in `dimsPos` are moved to the positions `[0, ..., |
| /// dimsPos.size())` in order. |
| static SmallVector<int64_t> |
| computeInterchangeFromDimPos(ArrayRef<int64_t> dimsPos, int64_t rank) { |
| SmallVector<int64_t> interchangeVector; |
| interchangeVector.reserve(dimsPos.size()); |
| // First map dims and their position. For example, dims_pos = [2, 0] will map |
| // to: |
| // [ |
| // [ key: 2, value: 0] |
| // [ key: 0, value: 1] |
| // ] |
| // where key is the idx in dims_pos while value its position in dims_pos. |
| DenseMap<int64_t, int64_t> dimsAndPosMapping; |
| for (int64_t dimsIdx = 0, end = dimsPos.size(); dimsIdx < end; dimsIdx++) |
| dimsAndPosMapping[dimsPos[dimsIdx]] = dimsIdx; |
| |
| // Scan the position in order and insert the value in the map |
| // to compute the interchange vector. |
| for (int64_t dimsIdx = 0; dimsIdx < rank; dimsIdx++) { |
| if (dimsAndPosMapping.count(dimsIdx)) |
| interchangeVector.push_back(dimsAndPosMapping[dimsIdx]); |
| } |
| return interchangeVector; |
| } |
| |
| /// Permute the elements of `vec` starting at position `offset` according to |
| /// `interchangeVector`. The permutation maps position `i` in the permuted range |
| /// to position `interchangeVector[i]` in the original range. Elements before |
| /// `offset` are unchanged. |
| /// |
| /// Example: interchange([a, b, c, d, e], [2, 0, 1], offset=2) |
| /// returns [a, b, e, c, d] (permutes the suffix [c, d, e]) |
| /// |
| /// Note: This is similar to `applyPermutationToVector` but supports an offset |
| /// for permuting a suffix of the vector. It is only used for pack/unpack scalar |
| /// implementation where we need to permute inner tile dimensions which are |
| /// stored at the end of the index vector. |
| template <typename T> |
| static SmallVector<T> interchange(ArrayRef<T> elements, |
| ArrayRef<int64_t> interchangeVector, |
| int offset = 0) { |
| SmallVector<T> vec = llvm::to_vector(elements); |
| for (auto [idx, val] : llvm::enumerate(interchangeVector)) |
| vec[idx + offset] = elements[val + offset]; |
| return vec; |
| } |
| |
| /// Generate the body of the innermost loop of the scalar implementation |
| /// of `pack` operation. |
| static void generatePackOpScalarImplementationBody(PackOp packOp, |
| OpBuilder &builder, |
| Location loc, |
| ValueRange ivs) { |
| // Note: `ivs` are already in the correct order, possibly interchanged based |
| // on `dims_pos`. However, connecting the loops with the access patterns is |
| // difficult - What is the relation between the position of the tile loop and |
| // the point loop? However, if we interchange `ivs` once more to go to the |
| // canonical blocking format: ABCabc, this connection becomes trivial: Each |
| // point loop is pointLoopsOffset + inputRank away from the tiled loop. |
| ArrayRef<int64_t> dimsToInnerBlock = packOp.getInnerDimsPos(); |
| ArrayRef<int64_t> dimsToOuterBlock = packOp.getOuterDimsPerm(); |
| |
| SmallVector<Value> interchangedIvs = ivs; |
| SmallVector<int64_t> interchangeVector = |
| computeInterchangeFromDimPos(dimsToInnerBlock, packOp.getSourceRank()); |
| interchangedIvs = interchange<Value>(interchangedIvs, interchangeVector, |
| /*offset=*/packOp.getSourceRank()); |
| if (!dimsToOuterBlock.empty()) { |
| interchangeVector = |
| computeInterchangeFromDimPos(dimsToOuterBlock, packOp.getSourceRank()); |
| interchangedIvs = |
| interchange<Value>(interchangedIvs, interchangeVector, /*offset=*/0); |
| } |
| DenseMap<int64_t, OpFoldResult> dimAndTileMapping = |
| packOp.getDimAndTileMapping(); |
| SmallVector<OpFoldResult> sourceIndices; |
| size_t pointLoopsOffset = 0; |
| int64_t sourceRank = packOp.getSourceRank(); |
| for (auto dim : llvm::seq<int64_t>(0, sourceRank)) { |
| if (dimAndTileMapping.contains(dim)) { |
| AffineExpr i, j, tile; |
| bindDims(builder.getContext(), i, j); |
| bindSymbols(builder.getContext(), tile); |
| OpFoldResult sourceIndex = affine::makeComposedFoldedAffineApply( |
| builder, loc, i * tile + j, |
| ArrayRef<OpFoldResult>{ |
| interchangedIvs[dim], |
| interchangedIvs[pointLoopsOffset + packOp.getSourceRank()], |
| dimAndTileMapping[dim]}); |
| sourceIndices.push_back(sourceIndex); |
| ++pointLoopsOffset; |
| } else { |
| sourceIndices.push_back(interchangedIvs[dim]); |
| } |
| } |
| |
| auto createLoad = [&]() -> Value { |
| return memref::LoadOp::create( |
| builder, loc, packOp.getSource(), |
| getValueOrCreateConstantIndexOp(builder, loc, sourceIndices)); |
| }; |
| Value scalar; |
| if (auto paddingValue = packOp.getPaddingValue()) { |
| ArithBuilder arithBuilder(builder, loc); |
| Value isInBounds; |
| for (auto dim : llvm::seq<int64_t>(0, sourceRank)) { |
| Value idx = |
| getValueOrCreateConstantIndexOp(builder, loc, sourceIndices[dim]); |
| Value cond = arithBuilder.slt( |
| idx, createOrFoldDimOp(builder, loc, packOp.getSource(), dim)); |
| isInBounds = dim == 0 ? cond : arithBuilder._and(isInBounds, cond); |
| } |
| scalar = scf::IfOp::create( |
| builder, loc, isInBounds, /*thenBuilder=*/ |
| [&](OpBuilder &b, Location l) { |
| scf::YieldOp::create(b, l, createLoad()); |
| }, |
| /*elseBuilder=*/ |
| [&](OpBuilder &b, Location l) { |
| scf::YieldOp::create(b, l, paddingValue); |
| }) |
| .getResult(0); |
| } else { |
| scalar = createLoad(); |
| } |
| |
| memref::StoreOp::create(builder, loc, scalar, packOp.getDest(), ivs); |
| } |
| |
| struct PackOpTiling |
| : public TilingInterface::ExternalModel<PackOpTiling, linalg::PackOp> { |
| using Base = TilingInterface::ExternalModel<PackOpTiling, linalg::PackOp>; |
| using Base::getTiledImplementation; |
| |
| SmallVector<utils::IteratorType> getLoopIteratorTypes(Operation *op) const { |
| // Note that here we only consider untiled dimensions and outer tiled data |
| // dimensions, the inner tiled data dimensions are materialized when |
| // building the body of the operation. |
| auto packOp = cast<PackOp>(op); |
| SmallVector<utils::IteratorType> iteratorTypes( |
| packOp.getSourceRank(), utils::IteratorType::parallel); |
| return iteratorTypes; |
| } |
| |
| SmallVector<Range> getIterationDomain(Operation *op, OpBuilder &b) const { |
| return getPackUnPackIterationDomain<PackOp>(cast<PackOp>(op), b); |
| } |
| |
| FailureOr<TilingResult> |
| getTiledImplementation(Operation *op, OpBuilder &b, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| auto packOp = cast<PackOp>(op); |
| // TODO: Support Memref PackOp. Temporarily return failure. |
| if (!packOp.hasPureTensorSemantics()) |
| return failure(); |
| |
| Location loc = packOp.getLoc(); |
| |
| // The tiling is applied on interchanged dimensions. We have to undo the |
| // interchange to map sizes and offsets to the original input. |
| int64_t inputRank = packOp.getSourceRank(); |
| SmallVector<OpFoldResult> origOffsets(offsets); |
| SmallVector<OpFoldResult> origSizes(sizes); |
| applyPermToRange(origOffsets, origSizes, |
| invertPermutationVector(packOp.getOuterDimsPerm())); |
| |
| DenseMap<int64_t, OpFoldResult> dimAndTileMapping = |
| packOp.getDimAndTileMapping(); |
| SmallVector<OpFoldResult> srcDimValues = |
| tensor::getMixedSizes(b, loc, packOp.getSource()); |
| SmallVector<OpFoldResult> inputIndices, inputSizes; |
| for (auto dim : llvm::seq<int64_t>(0, inputRank)) { |
| using AV = affine::AffineValueExpr; |
| affine::AffineBuilder ab(b, loc); |
| AffineExpr dim0, dim1, sym; |
| bindDims(b.getContext(), dim0, dim1); |
| bindSymbols(b.getContext(), sym); |
| if (dimAndTileMapping.count(dim)) { |
| // If the data dimension is tiled, the i-th index is the product of |
| // offset_i and tile_i, and the i-th size is the product of sizes_i and |
| // tile_i. |
| auto avOffset = AV(dim0).bind(origOffsets[dim]); |
| auto avSize = AV(dim0).bind(origSizes[dim]); |
| auto avTileSize = AV(sym).bind(dimAndTileMapping[dim]); |
| inputIndices.push_back(ab.mul(avOffset, avTileSize)); |
| inputSizes.push_back(ab.mul(avSize, avTileSize)); |
| } else { |
| inputIndices.push_back(origOffsets[dim]); |
| inputSizes.push_back(origSizes[dim]); |
| } |
| |
| // Limit the size of the input operand for incomplete tiles. |
| if (packOp.getPaddingValue()) { |
| OpFoldResult dimSize = srcDimValues[dim]; |
| auto avDimSize = AV(dim0).bind(dimSize); |
| auto avInputIdx = AV(dim1).bind(inputIndices.back()); |
| inputSizes.back() = |
| ab.min({inputSizes.back(), ab.sub(avDimSize, avInputIdx)}); |
| } |
| } |
| |
| auto oneAttr = b.getI64IntegerAttr(1); |
| SmallVector<OpFoldResult> strides(inputRank, oneAttr); |
| |
| SmallVector<Value> tiledOperands; |
| auto sourceSlice = tensor::ExtractSliceOp::create( |
| b, loc, packOp.getSource(), inputIndices, inputSizes, strides); |
| tiledOperands.push_back(sourceSlice); |
| |
| SmallVector<OpFoldResult> outputOffsets, outputSizes; |
| if (failed(getResultTilePosition(op, b, 0, offsets, sizes, outputOffsets, |
| outputSizes))) |
| return {}; |
| |
| strides.append(packOp.getDestRank() - inputRank, oneAttr); |
| auto outSlice = tensor::ExtractSliceOp::create( |
| b, loc, packOp.getDest(), outputOffsets, outputSizes, strides); |
| tiledOperands.push_back(outSlice); |
| |
| if (auto val = packOp.getPaddingValue()) |
| tiledOperands.push_back(val); |
| for (auto tile : packOp.getInnerTiles()) |
| tiledOperands.push_back(tile); |
| |
| PackOp tiledPackOp = |
| PackOp::create(b, loc, TypeRange{outSlice.getType()}, tiledOperands, |
| packOp.getProperties(), |
| packOp->getDiscardableAttrDictionary().getValue()); |
| |
| return TilingResult{ |
| {tiledPackOp}, |
| SmallVector<Value>(tiledPackOp->getResults()), |
| llvm::to_vector(ArrayRef<Operation *>{sourceSlice, outSlice})}; |
| } |
| |
| LogicalResult |
| getResultTilePosition(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, |
| SmallVector<OpFoldResult> &resultOffsets, |
| SmallVector<OpFoldResult> &resultSizes) const { |
| // The iteration domain is over outer dimensions of packed layout. In this |
| // context, the outer dimensions of `resultOffsets` are `offsets`. The |
| // inner dimensions of `resultOffsets` are zeros because tiling is not |
| // applied to them. |
| auto packOp = cast<PackOp>(op); |
| int64_t inputRank = packOp.getSourceRank(); |
| int64_t outputRank = packOp.getDestRank(); |
| auto zeroAttr = b.getI64IntegerAttr(0); |
| resultOffsets.assign(offsets.begin(), offsets.end()); |
| resultOffsets.append(outputRank - inputRank, zeroAttr); |
| |
| ReifiedRankedShapedTypeDims outputShape; |
| (void)reifyResultShapes(b, packOp, outputShape); |
| resultSizes.assign(sizes.begin(), sizes.end()); |
| for (auto dataTileDim : llvm::seq<unsigned>(inputRank, outputRank)) |
| resultSizes.push_back(outputShape[0][dataTileDim]); |
| |
| return success(); |
| } |
| |
| FailureOr<TilingResult> |
| generateResultTileValue(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| return generateResultTileValue(op, b, resultNumber, offsets, sizes, |
| /*innerTileAlignments=*/{}); |
| } |
| |
| FailureOr<TilingResult> generateResultTileValue( |
| Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, ArrayRef<OpFoldResult> sizes, |
| ArrayRef<InnerTileAlignment> innerTileAlignments) const { |
| auto packOp = cast<PackOp>(op); |
| int64_t numTiles = packOp.getInnerDimsPos().size(); |
| |
| // linalg.pack op is fusible (as a producer) only if full inner tiles are |
| // iterated or inner dims are not tiled. Otherwise, it will generate a |
| // sequence of non-trivial ops (for partial tiles). |
| for (auto offset : offsets.take_back(numTiles)) |
| if (!isZeroInteger(offset)) |
| return failure(); |
| |
| // Each requested inner-dim size must cover a full inner tile. A caller may |
| // instead assert this via an `Equal` alignment hint. The hint is indexed by |
| // source dim, matching the consumer-fusion path. |
| ArrayRef<int64_t> innerDimsPos = packOp.getInnerDimsPos(); |
| SmallVector<OpFoldResult> mixedTiles = packOp.getMixedTiles(); |
| ArrayRef<OpFoldResult> innerSizes = sizes.take_back(numTiles); |
| for (auto [i, pos] : llvm::enumerate(innerDimsPos)) { |
| InnerTileAlignment alignment = |
| pos < static_cast<int64_t>(innerTileAlignments.size()) |
| ? innerTileAlignments[pos] |
| : InnerTileAlignment::Unknown; |
| if (alignment != InnerTileAlignment::Equal && |
| !isEqualConstantIntOrValue(mixedTiles[i], innerSizes[i])) |
| return failure(); |
| } |
| |
| FailureOr<TilingResult> tilingResult = getTiledImplementation( |
| op, b, offsets.drop_back(numTiles), sizes.drop_back(numTiles)); |
| if (failed(tilingResult)) |
| return failure(); |
| return tilingResult.value(); |
| } |
| |
| LogicalResult generateScalarImplementation(Operation *op, OpBuilder &builder, |
| Location loc, |
| ValueRange ivs) const { |
| auto packOp = cast<PackOp>(op); |
| assert(packOp.hasPureBufferSemantics() && |
| "expected operation to have buffer semantics"); |
| OpBuilder::InsertionGuard g(builder); |
| // The `ivs` already represent the position into the output for the non |
| // data-tile dimensions. |
| SmallVector<Value> ivVec(ivs); |
| |
| // Get output shape - for memrefs, get dimensions from dest directly. |
| SmallVector<OpFoldResult> outputShape; |
| Value dest = packOp.getDest(); |
| for (auto dim : llvm::seq<int64_t>(0, packOp.getDestRank())) |
| outputShape.push_back(createOrFoldDimOp(builder, loc, dest, dim)); |
| |
| // Generate the loops that iterate over the data tile. |
| Value zero = arith::ConstantIndexOp::create(builder, loc, 0); |
| Value one = arith::ConstantIndexOp::create(builder, loc, 1); |
| |
| // All loops except the innermost are simple loops that just iterate |
| // over the tile dimensions. |
| for (auto dataTileDim : llvm::seq<unsigned>(packOp.getSourceRank(), |
| packOp.getDestRank() - 1)) { |
| Value ub = getValueOrCreateConstantIndexOp(builder, loc, |
| outputShape[dataTileDim]); |
| scf::ForOp loop = scf::ForOp::create(builder, loc, zero, ub, one); |
| builder.setInsertionPointToStart(loop.getBody()); |
| ivVec.push_back(loop.getInductionVar()); |
| } |
| // The body of the innermost loops does the actual data movement. |
| scf::ForOp::create( |
| builder, loc, zero, |
| getValueOrCreateConstantIndexOp(builder, loc, outputShape.back()), one, |
| ValueRange{}, |
| [&](OpBuilder &bodyBuilder, Location bodyLoc, Value iv, |
| ValueRange regionIterArgs) { |
| ivVec.push_back(iv); |
| generatePackOpScalarImplementationBody(packOp, bodyBuilder, bodyLoc, |
| ivVec); |
| scf::YieldOp::create(bodyBuilder, bodyLoc); |
| }); |
| return success(); |
| } |
| |
| LogicalResult getIterationDomainTileFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| SmallVectorImpl<OpFoldResult> &resultOffsets, |
| SmallVectorImpl<OpFoldResult> &resultSizes) const { |
| return getIterationDomainTileFromOperandTiles( |
| op, b, operandNumbers, allOffsets, allSizes, resultOffsets, resultSizes, |
| /*innerTileAlignments=*/{}); |
| } |
| |
| /// Method to return the position of iteration domain tile computed by the |
| /// tiled operation. In current `linalg.pack` context, the `resultOffsets` and |
| /// `resultSizes` only cover outer dimensions. |
| LogicalResult getIterationDomainTileFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| SmallVectorImpl<OpFoldResult> &resultOffsets, |
| SmallVectorImpl<OpFoldResult> &resultSizes, |
| ArrayRef<InnerTileAlignment> innerTileAlignments) const { |
| if (operandNumbers.size() != 1 || operandNumbers[0] != 0) { |
| LLVM_DEBUG( |
| { llvm::dbgs() << "unsupported operands for consumer fusion"; }); |
| return failure(); |
| } |
| |
| ArrayRef<OpFoldResult> offsets(allOffsets[0]); |
| ArrayRef<OpFoldResult> sizes(allSizes[0]); |
| auto packOp = cast<PackOp>(op); |
| Location loc = packOp.getLoc(); |
| SmallVector<OpFoldResult> outerDimOffsets, outerDimSizes; |
| DenseMap<int64_t, OpFoldResult> dimAndTileMapping = |
| packOp.getDimAndTileMapping(); |
| SmallVector<int64_t> outerShapeWithoutTranspose( |
| packOp.getDestType().getShape().take_front(packOp.getSourceRank())); |
| if (!packOp.getOuterDimsPerm().empty()) { |
| applyPermutationToVector( |
| outerShapeWithoutTranspose, |
| invertPermutationVector(packOp.getOuterDimsPerm())); |
| } |
| for (auto dim : llvm::seq<int64_t>(packOp.getSourceRank())) { |
| if (dimAndTileMapping.count(dim)) { |
| FailureOr<int64_t> cstTileSize = |
| ValueBoundsConstraintSet::computeConstantBound( |
| presburger::BoundType::UB, sizes[dim], |
| /*stopCondition=*/nullptr, |
| ValueBoundsOptions{/*closedUB=*/true}); |
| std::optional<int64_t> cstInnerSize = |
| getConstantIntValue(dimAndTileMapping[dim]); |
| |
| // A caller-supplied alignment hint (see InnerTileAlignment) asserts |
| // that this packed dimension is tiled and how its loop tile size |
| // relates to the pack op inner tile size. |
| InnerTileAlignment innerTileAlignment = |
| dim < static_cast<int64_t>(innerTileAlignments.size()) |
| ? innerTileAlignments[dim] |
| : InnerTileAlignment::Unknown; |
| |
| // If a dimension is not tiled, it is always valid to fuse the pack op, |
| // even if the op has padding semantics. Because it always generates a |
| // full slice along the dimension. The tile sizes are for unpacked |
| // domain, i.e., `srcDimSize`, so `tileSize < srcDimSize` means that the |
| // dimension is tiled. |
| // TODO: It could be untiled if the `srcDimSize` is dynamic. It is a |
| // hard check to determine if a dimension is tiled or not. |
| // A non-`Unknown` hint also means the caller asserts the dimension is |
| // tiled: `cstTileSize` is an upper bound, so a scalable/`min`-shaped |
| // tile (whose bound equals `srcDimSize`) would otherwise be mistaken |
| // for untiled and bypass the hint below. |
| int64_t srcDimSize = packOp.getSourceType().getDimSize(dim); |
| int64_t destDimSize = outerShapeWithoutTranspose[dim]; |
| bool isTiled = innerTileAlignment != InnerTileAlignment::Unknown || |
| failed(cstTileSize) || |
| ShapedType::isDynamic(srcDimSize) || |
| cstTileSize.value() < srcDimSize; |
| if (!isTiled) { |
| outerDimOffsets.push_back(offsets[dim]); |
| if (ShapedType::isStatic(destDimSize)) { |
| outerDimSizes.push_back(b.getIndexAttr(destDimSize)); |
| } else { |
| outerDimSizes.push_back( |
| b.createOrFold<tensor::DimOp>(loc, packOp.getDest(), dim)); |
| } |
| continue; |
| } |
| |
| // Currently fusing `packOp` as consumer only expects perfect tiling |
| // scenario because even if without padding semantic, the `packOp` may |
| // also yield incomplete tiles. E.g. tensor<30xf32> -> tensor<5x6xf32>, |
| // where the `tileSize` from operand of `packOp` is 5, which is not |
| // exactly divided by `innerTile`(=6) of `packOp`. As the result: |
| // 1. the first slice is extracted from (0) to (4) and inserted into |
| // (0,0)~(0,4) at first row. |
| // 2. the second slice is extracted from (5) to (9) and SHOULD BE |
| // respectively inserted into two rows with different length, including |
| // first row: (0,5) and second row (1,0)~(1,3). It is hard to coordinate |
| // them, thus adding below constraint to bypass them temporarily. In |
| // another word, we can only support tiling with consumer if the tile |
| // size for the producer is a multiple of the inner tile size for the |
| // packed dimensions at this moment. |
| |
| // The caller may assert how this packed dimension's loop tile size |
| // relates to the inner tile size via `innerTileAlignments` (see |
| // InnerTileAlignment). The hint is the source of truth and is honored |
| // when present. When both sizes are also statically known we assert the |
| // hint agrees with them (a contradicting hint is a caller bug). When |
| // the hint is `Unknown`, fall back to requiring a statically-provable |
| // multiple. |
| bool assumeInnerTileSizesMatchTiles = |
| innerTileAlignment == InnerTileAlignment::Equal; |
| bool staticallyDecidable = |
| !failed(cstTileSize) && cstInnerSize.has_value(); |
| if (innerTileAlignment == InnerTileAlignment::Unknown) { |
| if (!staticallyDecidable || *cstTileSize % *cstInnerSize != 0) |
| return failure(); |
| } else if (staticallyDecidable) { |
| assert(*cstTileSize % *cstInnerSize == 0 && |
| "InnerTileAlignment hint contradicts statically known tile " |
| "sizes"); |
| assert((innerTileAlignment != InnerTileAlignment::Equal || |
| *cstTileSize == *cstInnerSize) && |
| "InnerTileAlignment::Equal contradicts statically known tile " |
| "sizes"); |
| } |
| |
| using AV = affine::AffineValueExpr; |
| affine::AffineBuilder ab(b, loc); |
| AffineExpr dim0, sym; |
| bindDims(b.getContext(), dim0); |
| bindSymbols(b.getContext(), sym); |
| auto avOffset = AV(dim0).bind(offsets[dim]); |
| auto avSize = AV(dim0).bind(sizes[dim]); |
| auto avTileSize = AV(sym).bind(dimAndTileMapping[dim]); |
| outerDimOffsets.push_back(ab.floor(avOffset, avTileSize)); |
| // If the tile size equals the inner tile size, the outer dims are |
| // always 1. |
| outerDimSizes.push_back(assumeInnerTileSizesMatchTiles |
| ? b.getIndexAttr(1) |
| : ab.ceil(avSize, avTileSize)); |
| } else { |
| outerDimOffsets.push_back(offsets[dim]); |
| outerDimSizes.push_back(sizes[dim]); |
| } |
| } |
| applyPermToRange(outerDimOffsets, outerDimSizes, packOp.getOuterDimsPerm()); |
| resultOffsets = outerDimOffsets; |
| resultSizes = outerDimSizes; |
| return success(); |
| } |
| |
| FailureOr<TilingResult> getTiledImplementationFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes) const { |
| return getTiledImplementationFromOperandTiles(op, b, operandNumbers, |
| allOffsets, allSizes, |
| /*innerTileAlignments=*/{}); |
| } |
| |
| /// Method to return the tiled implementation of linalg.pack as a consumer. |
| FailureOr<TilingResult> getTiledImplementationFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| ArrayRef<InnerTileAlignment> innerTileAlignments) const { |
| if (operandNumbers.size() != 1 || operandNumbers[0] != 0) { |
| LLVM_DEBUG({ llvm::dbgs() << "unhandled operands for consumer fusion"; }); |
| return failure(); |
| } |
| |
| ArrayRef<OpFoldResult> offsets(allOffsets[0]); |
| ArrayRef<OpFoldResult> sizes(allSizes[0]); |
| |
| auto packOp = cast<PackOp>(op); |
| // TODO: Support Memref UnPackOp. Temporarily return failure. |
| if (!packOp.hasPureTensorSemantics()) |
| return failure(); |
| |
| Location loc = packOp.getLoc(); |
| |
| int64_t inputRank = packOp.getSourceRank(); |
| auto oneAttr = b.getI64IntegerAttr(1); |
| SmallVector<OpFoldResult> strides(inputRank, oneAttr); |
| |
| SmallVector<Value> tiledOperands; |
| auto sourceSlice = tensor::ExtractSliceOp::create( |
| b, loc, packOp.getSource(), offsets, sizes, strides); |
| tiledOperands.push_back(sourceSlice); |
| |
| SmallVector<OpFoldResult> outerDimOffsets, outerDimSizes; |
| if (failed(getIterationDomainTileFromOperandTiles( |
| op, b, operandNumbers, allOffsets, allSizes, outerDimOffsets, |
| outerDimSizes, innerTileAlignments))) |
| return failure(); |
| |
| SmallVector<OpFoldResult> outputOffsets, outputSizes; |
| if (failed(getResultTilePosition(op, b, 0, outerDimOffsets, outerDimSizes, |
| outputOffsets, outputSizes))) |
| return failure(); |
| |
| strides.append(packOp.getDestRank() - inputRank, oneAttr); |
| auto outSlice = tensor::ExtractSliceOp::create( |
| b, loc, packOp.getDest(), outputOffsets, outputSizes, strides); |
| tiledOperands.push_back(outSlice); |
| |
| if (auto val = packOp.getPaddingValue()) |
| tiledOperands.push_back(val); |
| for (auto tile : packOp.getInnerTiles()) |
| tiledOperands.push_back(tile); |
| |
| PackOp tiledPackOp = |
| PackOp::create(b, loc, TypeRange{outSlice.getType()}, tiledOperands, |
| packOp.getProperties(), |
| packOp->getDiscardableAttrDictionary().getValue()); |
| |
| return TilingResult{ |
| {tiledPackOp}, |
| SmallVector<Value>(tiledPackOp->getResults()), |
| llvm::to_vector(ArrayRef<Operation *>{sourceSlice, outSlice})}; |
| } |
| }; |
| |
| struct UnpackTileDimInfo { |
| bool isAlignedToInnerTileSize; |
| OpFoldResult sourceOffset; |
| OpFoldResult sourceSize; |
| OpFoldResult resultOffset; |
| OpFoldResult destExpandedSize; |
| }; |
| |
| /// Returns the needed information for tiling unpack op on `tileDim` with given |
| /// `tileOffset` and `tileSize`. For more details, see the comment of the |
| /// `getTiledImplementation`. |
| static UnpackTileDimInfo |
| getUnpackTileDimInfo(OpBuilder &b, UnPackOp unpackOp, int64_t tileDim, |
| OpFoldResult tileOffset, OpFoldResult tileSize, |
| InnerTileAlignment innerTileAlignment) { |
| UnpackTileDimInfo info; |
| Attribute zeroAttr = b.getIndexAttr(0); |
| Attribute oneAttr = b.getIndexAttr(1); |
| DenseMap<int64_t, OpFoldResult> dimAndTileMapping = |
| unpackOp.getDimAndTileMapping(); |
| // The dimension is not one of packed data dimension. |
| if (!dimAndTileMapping.count(tileDim)) { |
| info.isAlignedToInnerTileSize = true; |
| info.sourceOffset = tileOffset; |
| info.sourceSize = tileSize; |
| info.resultOffset = zeroAttr; |
| info.destExpandedSize = tileSize; |
| return info; |
| } |
| |
| Location loc = unpackOp.getLoc(); |
| using AV = affine::AffineValueExpr; |
| affine::AffineBuilder ab(b, loc); |
| AffineExpr dim0, dim1, sym0; |
| bindDims(b.getContext(), dim0, dim1); |
| bindSymbols(b.getContext(), sym0); |
| |
| OpFoldResult innerTileSize = dimAndTileMapping[tileDim]; |
| |
| info.isAlignedToInnerTileSize = false; |
| FailureOr<int64_t> cstSize = ValueBoundsConstraintSet::computeConstantBound( |
| presburger::BoundType::UB, tileSize, |
| /*stopCondition=*/nullptr, ValueBoundsOptions{/*closedUB=*/true}); |
| std::optional<int64_t> cstInnerSize = getConstantIntValue(innerTileSize); |
| // The caller may assert how this dimension's loop tile size relates to the |
| // op's inner tile size via `innerTileAlignment` (see InnerTileAlignment). The |
| // hint is the source of truth and is honored when present: `Equal`/`Multiple` |
| // both mean the tile is aligned to (a multiple of) the inner tile, and |
| // `Equal` additionally collapses the source slice to a single inner tile. |
| // When both sizes are also statically known we assert the hint agrees with |
| // them (a contradicting hint is a caller bug). When `Unknown`, fall back to |
| // the static upper-bound path below. |
| bool assumeInnerTileSizesMatchTiles = |
| innerTileAlignment == InnerTileAlignment::Equal; |
| bool staticallyDecidable = !failed(cstSize) && cstInnerSize.has_value(); |
| if (innerTileAlignment != InnerTileAlignment::Unknown) { |
| info.isAlignedToInnerTileSize = true; |
| if (staticallyDecidable) { |
| assert(*cstSize % *cstInnerSize == 0 && |
| "InnerTileAlignment hint contradicts statically known tile sizes"); |
| assert((innerTileAlignment != InnerTileAlignment::Equal || |
| *cstSize == *cstInnerSize) && |
| "InnerTileAlignment::Equal contradicts statically known tile " |
| "sizes"); |
| } |
| } |
| if (info.isAlignedToInnerTileSize || (!failed(cstSize) && cstInnerSize)) { |
| if (!info.isAlignedToInnerTileSize && *cstSize % *cstInnerSize == 0) |
| info.isAlignedToInnerTileSize = true; |
| |
| // If the tiling size equals to the inner tiling size, the outer dims are |
| // always 1. |
| if (assumeInnerTileSizesMatchTiles || |
| (cstInnerSize && !failed(cstSize) && *cstInnerSize == *cstSize)) { |
| auto lhs = AV(dim0).bind(tileOffset); |
| auto rhs = AV(dim1).bind(innerTileSize); |
| info.sourceOffset = ab.floor(lhs, rhs); |
| info.sourceSize = oneAttr; |
| info.resultOffset = zeroAttr; |
| info.destExpandedSize = tileSize; |
| return info; |
| } |
| } |
| |
| if (info.isAlignedToInnerTileSize) { |
| info.sourceOffset = |
| ab.floor(AV(dim0).bind(tileOffset), AV(dim1).bind(innerTileSize)); |
| info.resultOffset = zeroAttr; |
| info.destExpandedSize = tileSize; |
| |
| // The ceilDiv is needed here because there could be incomplete tile even |
| // it is perfect tiling cases. E.g., |
| // %0 = unpack tensor<33x2xf32> into tensor<64xf32> |
| // If the tiling size is 32, there will be 3 tiles. Two of them have |
| // size=32; one of them have size=2. The size is represented using |
| // affine_min op; we need ceilDiv. |
| info.sourceSize = |
| ab.ceil(AV(dim0).bind(tileSize), AV(dim1).bind(innerTileSize)); |
| return info; |
| } |
| |
| affine::DivModValue firstCoord = affine::getDivMod( |
| b, loc, getValueOrCreateConstantIndexOp(b, loc, tileOffset), |
| getValueOrCreateConstantIndexOp(b, loc, innerTileSize)); |
| OpFoldResult tileExclusiveBound = |
| ab.add(AV(dim0).bind(tileOffset), AV(dim1).bind(tileSize)); |
| affine::DivModValue lastCoord = affine::getDivMod( |
| b, loc, |
| getValueOrCreateConstantIndexOp( |
| b, loc, |
| ab.sub(AV(dim0).bind(tileExclusiveBound), AV(dim1).bind(oneAttr))), |
| getValueOrCreateConstantIndexOp(b, loc, innerTileSize)); |
| |
| OpFoldResult lengthMinusOne = ab.sub(AV(dim0).bind(lastCoord.quotient), |
| AV(dim1).bind(firstCoord.quotient)); |
| info.sourceSize = |
| ab.add(AV(dim0).bind(lengthMinusOne), AV(dim1).bind(oneAttr)); |
| info.sourceOffset = firstCoord.quotient; |
| info.resultOffset = firstCoord.remainder; |
| // Do not create an Affine ops for expanded size because the affine op is too |
| // complicated which would trigger an issue in affine ops simplification. |
| info.destExpandedSize = b.createOrFold<arith::MulIOp>( |
| loc, getValueOrCreateConstantIndexOp(b, loc, info.sourceSize), |
| getValueOrCreateConstantIndexOp(b, loc, innerTileSize)); |
| return info; |
| } |
| |
| struct UnPackOpTiling |
| : public TilingInterface::ExternalModel<UnPackOpTiling, linalg::UnPackOp> { |
| using Base = TilingInterface::ExternalModel<UnPackOpTiling, linalg::UnPackOp>; |
| using Base::getIterationDomainTileFromOperandTiles; |
| |
| SmallVector<utils::IteratorType> getLoopIteratorTypes(Operation *op) const { |
| auto unpackOp = cast<UnPackOp>(op); |
| SmallVector<utils::IteratorType> iteratorTypes( |
| unpackOp.getDestRank(), utils::IteratorType::parallel); |
| return iteratorTypes; |
| } |
| |
| SmallVector<Range> getIterationDomain(Operation *op, OpBuilder &b) const { |
| return getPackUnPackIterationDomain<UnPackOp>(cast<UnPackOp>(op), b); |
| } |
| |
| /// There are two cases in tiling unpack ops. If the tiling size is aligned to |
| /// the inner tile size, the corresponding tiles of source are all complete. |
| /// Otherwise, there are in-complete tiles. We will need to expand the slice |
| /// of source for getting complete tiles. The tiled unpack op unpacks more |
| /// data from source, so We'll need an extract_slice op to shift and truncate |
| /// the output. |
| /// Take Nn_to_N as an example. Say that N=32, n=8, and tiling_size=15. The |
| /// coordinates of second tile (i.e., result[15..31]) are |
| /// [(1, 7), (2, 0,), (2, 1) ... (3, 6), (3, 7)]. The first row and the last |
| /// row are incomplete tiles. To represent the unpack op, we have to complete |
| /// the rows. I.e., the input coordinates would start with (1, 0); end with |
| /// (3, 7). In this context, the tiled unpack produces a (3 * n) elements |
| /// because there are 3 rows in total. Follow by a tensor.extract_slice op, we |
| /// can get the actual result. |
| FailureOr<TilingResult> |
| getTiledImplementation(Operation *op, OpBuilder &b, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| return getTiledImplementation(op, b, offsets, sizes, |
| /*innerTileAlignments=*/{}); |
| } |
| |
| FailureOr<TilingResult> getTiledImplementation( |
| Operation *op, OpBuilder &b, ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, |
| ArrayRef<InnerTileAlignment> innerTileAlignments) const { |
| auto unpackOp = cast<UnPackOp>(op); |
| // TODO: Support Memref UnPackOp. Temporarily return failure. |
| if (!unpackOp.hasPureTensorSemantics()) |
| return failure(); |
| |
| int64_t srcRank = unpackOp.getSourceRank(); |
| int64_t destRank = unpackOp.getDestRank(); |
| int64_t numInnerTiles = srcRank - destRank; |
| Location loc = unpackOp.getLoc(); |
| |
| // The perfect tiling case indicates that the tiling sizes are multiple of |
| // inner_tile_size. In this context, no extra data is needed when |
| // representing the tiled unpack op. |
| bool isPerfectTilingCase = true; |
| Attribute oneAttr = b.getIndexAttr(1); |
| SmallVector<OpFoldResult> sliceSrcStrides(destRank, oneAttr); |
| SmallVector<OpFoldResult> sliceSrcIndices, sliceSrcSizes; |
| SmallVector<OpFoldResult> destExpandedSizes, resultOffsetsFromDest; |
| for (auto dim : llvm::seq<int64_t>(0, destRank)) { |
| UnpackTileDimInfo info = getUnpackTileDimInfo( |
| b, unpackOp, dim, offsets[dim], sizes[dim], |
| dim < static_cast<int64_t>(innerTileAlignments.size()) |
| ? innerTileAlignments[dim] |
| : InnerTileAlignment::Unknown); |
| if (!info.isAlignedToInnerTileSize) |
| isPerfectTilingCase = false; |
| sliceSrcIndices.push_back(info.sourceOffset); |
| sliceSrcSizes.push_back(info.sourceSize); |
| destExpandedSizes.push_back(info.destExpandedSize); |
| resultOffsetsFromDest.push_back(info.resultOffset); |
| } |
| |
| // The tiling is applied on destination dimensions. We have to apply the |
| // interchange on source dimensions if outer_dims_perm is set. |
| applyPermToRange(sliceSrcIndices, sliceSrcSizes, |
| unpackOp.getOuterDimsPerm()); |
| Attribute zeroAttr = b.getIndexAttr(0); |
| sliceSrcIndices.append(numInnerTiles, zeroAttr); |
| sliceSrcSizes.append(unpackOp.getMixedTiles()); |
| sliceSrcStrides.append(numInnerTiles, oneAttr); |
| SmallVector<Operation *> generatedSlices; |
| tensor::ExtractSliceOp sliceSource = tensor::ExtractSliceOp::create( |
| b, loc, unpackOp.getSource(), sliceSrcIndices, sliceSrcSizes, |
| sliceSrcStrides); |
| generatedSlices.push_back(sliceSource); |
| |
| SmallVector<OpFoldResult> destStrides(destRank, oneAttr); |
| Value sliceDest; |
| if (isPerfectTilingCase) { |
| auto destSliceOp = tensor::ExtractSliceOp::create( |
| b, loc, unpackOp.getDest(), offsets, sizes, destStrides); |
| sliceDest = destSliceOp; |
| generatedSlices.push_back(destSliceOp); |
| } else { |
| sliceDest = tensor::EmptyOp::create( |
| b, loc, destExpandedSizes, unpackOp.getDestType().getElementType()); |
| } |
| |
| SmallVector<Value> tiledOperands = {sliceSource.getResult(), sliceDest}; |
| for (auto tile : unpackOp.getInnerTiles()) |
| tiledOperands.push_back(tile); |
| |
| UnPackOp tiledUnpackOp = |
| UnPackOp::create(b, loc, TypeRange{sliceDest.getType()}, tiledOperands, |
| unpackOp.getProperties(), |
| unpackOp->getDiscardableAttrDictionary().getValue()); |
| |
| if (isPerfectTilingCase) |
| return TilingResult{{tiledUnpackOp}, |
| SmallVector<Value>(tiledUnpackOp->getResults()), |
| generatedSlices}; |
| |
| auto extractSlice = tensor::ExtractSliceOp::create( |
| b, loc, tiledUnpackOp->getResult(0), resultOffsetsFromDest, sizes, |
| destStrides); |
| return TilingResult{ |
| {tiledUnpackOp}, {extractSlice.getResult()}, generatedSlices}; |
| } |
| |
| LogicalResult |
| getResultTilePosition(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes, |
| SmallVector<OpFoldResult> &resultOffsets, |
| SmallVector<OpFoldResult> &resultSizes) const { |
| resultOffsets = llvm::to_vector(offsets); |
| resultSizes = llvm::to_vector(sizes); |
| return success(); |
| } |
| |
| FailureOr<TilingResult> |
| generateResultTileValue(Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, |
| ArrayRef<OpFoldResult> sizes) const { |
| return generateResultTileValue(op, b, resultNumber, offsets, sizes, |
| /*innerTileAlignments=*/{}); |
| } |
| |
| FailureOr<TilingResult> generateResultTileValue( |
| Operation *op, OpBuilder &b, unsigned resultNumber, |
| ArrayRef<OpFoldResult> offsets, ArrayRef<OpFoldResult> sizes, |
| ArrayRef<InnerTileAlignment> innerTileAlignments) const { |
| FailureOr<TilingResult> tilingResult = |
| getTiledImplementation(op, b, offsets, sizes, innerTileAlignments); |
| if (failed(tilingResult)) |
| return failure(); |
| return tilingResult.value(); |
| } |
| |
| LogicalResult generateScalarImplementation(Operation *op, OpBuilder &builder, |
| Location loc, |
| ValueRange ivs) const { |
| auto unpackOp = cast<UnPackOp>(op); |
| assert(unpackOp.hasPureBufferSemantics() && |
| "expected operation to have buffer semantics"); |
| assert(ivs.size() == unpackOp.getDestRank() && |
| "number of ivs must match the rank of the output tensor"); |
| OpBuilder::InsertionGuard g(builder); |
| |
| DenseMap<int64_t, OpFoldResult> dimAndTileMapping = |
| unpackOp.getDimAndTileMapping(); |
| // Untiled loops and tile loops induction variables. |
| SmallVector<Value> inputIvs; |
| // Point loops induction variables. |
| SmallVector<Value> inputIvsPointLoops; |
| inputIvs.reserve(unpackOp.getDestRank()); |
| inputIvsPointLoops.reserve(dimAndTileMapping.size()); |
| for (auto dim : llvm::seq<int64_t>(0, unpackOp.getDestRank())) { |
| if (dimAndTileMapping.count(dim)) { |
| affine::DivModValue divMod = |
| affine::getDivMod(builder, loc, ivs[dim], |
| getValueOrCreateConstantIndexOp( |
| builder, loc, dimAndTileMapping[dim])); |
| inputIvsPointLoops.push_back(divMod.remainder); |
| inputIvs.push_back(divMod.quotient); |
| } else { |
| inputIvs.push_back(ivs[dim]); |
| } |
| } |
| |
| // TODO: (lorenzo) simplify the logic a bit. There is `ivs`, |
| // `inputIvsPointLoops` and `inputIvs`. |
| assert(inputIvsPointLoops.size() + inputIvs.size() == |
| unpackOp.getSourceRank() && |
| "expect same number of induction variables equals to input rank"); |
| // Interchange the point loops induction variables based on `inner_dim_pos`. |
| ArrayRef<int64_t> innerDims = unpackOp.getInnerDimsPos(); |
| SmallVector<int64_t> interchangeVector = |
| computeInterchangeFromDimPos(innerDims, unpackOp.getDestRank()); |
| SmallVector<Value> interchangedInputIvsPointLoops = inputIvsPointLoops; |
| interchangedInputIvsPointLoops = interchange<Value>( |
| interchangedInputIvsPointLoops, interchangeVector, /*offset=*/0); |
| // Interchange the tiled loops induction variables based on |
| // `outer_dims_perm`. |
| ArrayRef<int64_t> outerDims = unpackOp.getOuterDimsPerm(); |
| if (!outerDims.empty()) |
| inputIvs = interchange<Value>(inputIvs, outerDims, /*offset=*/0); |
| |
| llvm::append_range(inputIvs, interchangedInputIvsPointLoops); |
| Value scalar = |
| memref::LoadOp::create(builder, loc, unpackOp.getSource(), inputIvs); |
| memref::StoreOp::create(builder, loc, scalar, unpackOp.getDest(), ivs); |
| return success(); |
| } |
| |
| /// Method to return the position of iteration domain tile computed by the |
| /// tiled operation. |
| LogicalResult getIterationDomainTileFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| SmallVectorImpl<OpFoldResult> &resultOffsets, |
| SmallVectorImpl<OpFoldResult> &resultSizes) const { |
| if (operandNumbers.size() != 1) { |
| LLVM_DEBUG({ llvm::dbgs() << "unable to handle multiple operands"; }); |
| return failure(); |
| } |
| auto unPackOp = cast<UnPackOp>(op); |
| unsigned operandNumber = operandNumbers[0]; |
| ArrayRef<OpFoldResult> offsets(allOffsets[0]); |
| ArrayRef<OpFoldResult> sizes(allSizes[0]); |
| |
| // If the operand tile is the dest, then no adjustment is needed. |
| if (operandNumber == unPackOp.getDestMutable().getOperandNumber()) { |
| resultOffsets = llvm::to_vector(offsets); |
| resultSizes = llvm::to_vector(sizes); |
| return success(); |
| } |
| Location loc = unPackOp.getLoc(); |
| |
| int64_t numTiles = unPackOp.getInnerDimsPos().size(); |
| auto destOffsets = offsets.drop_back(numTiles); |
| auto destSizes = sizes.drop_back(numTiles); |
| // The tiling is applied on interchanged dimensions. We have to undo the |
| // interchange to map sizes and offsets to the original input. |
| int64_t outputRank = unPackOp.getDestRank(); |
| ReifiedRankedShapedTypeDims reifiedReturnShapes; |
| if (failed(reifyResultShapes(b, unPackOp, reifiedReturnShapes))) |
| return failure(); |
| SmallVector<OpFoldResult> outputMixedSizes = reifiedReturnShapes.front(); |
| SmallVector<OpFoldResult> origOffsets(destOffsets); |
| SmallVector<OpFoldResult> origSizes(destSizes); |
| applyPermToRange(origOffsets, origSizes, |
| invertPermutationVector(unPackOp.getOuterDimsPerm())); |
| |
| DenseMap<int64_t, OpFoldResult> dimAndTileMapping = |
| unPackOp.getDimAndTileMapping(); |
| |
| for (auto dim : llvm::seq<int64_t>(0, outputRank)) { |
| using AV = affine::AffineValueExpr; |
| affine::AffineBuilder ab(b, loc); |
| AffineExpr dim0, dim1, sym0; |
| bindDims(b.getContext(), dim0, dim1); |
| bindSymbols(b.getContext(), sym0); |
| if (dimAndTileMapping.count(dim)) { |
| // If the data dimension is tiled, the i-th index is the product of |
| // offset_i and tile_i, and the i-th size is the product of sizes_i and |
| // tile_i. The sizes must be clamped to the sizes of the unpack result. |
| auto avOffset = AV(dim0).bind(origOffsets[dim]); |
| auto avSize = AV(dim0).bind(origSizes[dim]); |
| auto avTileSize = AV(sym0).bind(dimAndTileMapping[dim]); |
| auto avResultSize = AV(dim0).bind(outputMixedSizes[dim]); |
| resultOffsets.push_back(ab.mul(avOffset, avTileSize)); |
| auto avResultOffset = AV(dim1).bind(resultOffsets.back()); |
| resultSizes.push_back(ab.min({ab.mul(avSize, avTileSize), |
| ab.sub(avResultSize, avResultOffset)})); |
| } else { |
| resultOffsets.push_back(origOffsets[dim]); |
| resultSizes.push_back(origSizes[dim]); |
| } |
| } |
| return success(); |
| } |
| |
| FailureOr<TilingResult> getTiledImplementationFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes) const { |
| return getTiledImplementationFromOperandTiles(op, b, operandNumbers, |
| allOffsets, allSizes, |
| /*innerTileAlignments=*/{}); |
| } |
| |
| /// Method to return the tiled implementation of linalg.unpack as a consumer. |
| FailureOr<TilingResult> getTiledImplementationFromOperandTiles( |
| Operation *op, OpBuilder &b, ArrayRef<unsigned> operandNumbers, |
| ArrayRef<SmallVector<OpFoldResult>> allOffsets, |
| ArrayRef<SmallVector<OpFoldResult>> allSizes, |
| ArrayRef<InnerTileAlignment> innerTileAlignments) const { |
| if (operandNumbers.size() != 1 || operandNumbers[0] != 0) { |
| LLVM_DEBUG({ llvm::dbgs() << "unhandled operands for consumer fusion"; }); |
| return failure(); |
| } |
| auto unPackOp = cast<UnPackOp>(op); |
| // TODO: Support Memref UnPackOp. Temporarily return failure. |
| if (!unPackOp.hasPureTensorSemantics()) |
| return failure(); |
| |
| ArrayRef<OpFoldResult> offsets(allOffsets[0]); |
| ArrayRef<OpFoldResult> sizes(allSizes[0]); |
| |
| // linalg.unpack op is fusible (as a consumer) only if the inner dims are |
| // not tiled, i.e. each inner-dim loop tile size equals the inner tile size. |
| // The caller may assert this per inner dim via InnerTileAlignment::Equal; |
| // otherwise we require a statically-provable equality. |
| int64_t numTiles = unPackOp.getInnerDimsPos().size(); |
| ArrayRef<int64_t> innerDimsPos = unPackOp.getInnerDimsPos(); |
| SmallVector<OpFoldResult> mixedTiles = unPackOp.getMixedTiles(); |
| ArrayRef<OpFoldResult> innerSizes = sizes.take_back(numTiles); |
| for (int64_t i = 0; i < numTiles; ++i) { |
| // `innerTileAlignments` is indexed by the unpack iteration domain (the |
| // dest dims); the i-th inner tile lives on dest dim `innerDimsPos[i]`. |
| int64_t destDim = innerDimsPos[i]; |
| bool hintedEqual = |
| destDim < static_cast<int64_t>(innerTileAlignments.size()) && |
| innerTileAlignments[destDim] == InnerTileAlignment::Equal; |
| // The hint is the source of truth: honor a caller `Equal` assertion. When |
| // both sizes are also statically known, assert the hint agrees with them |
| // (a contradicting hint is a caller bug) rather than silently ignoring |
| // it. Without an `Equal` hint, require a statically-provable equality |
| // (the inner dim must not be tiled). |
| if (hintedEqual) { |
| assert((!getConstantIntValue(mixedTiles[i]) || |
| !getConstantIntValue(innerSizes[i]) || |
| isEqualConstantIntOrValue(mixedTiles[i], innerSizes[i])) && |
| "InnerTileAlignment::Equal contradicts statically known tile " |
| "sizes"); |
| continue; |
| } |
| if (isEqualConstantIntOrValue(mixedTiles[i], innerSizes[i])) |
| continue; |
| return failure(); |
| } |
| |
| Location loc = unPackOp.getLoc(); |
| |
| // Fetch offset/size for creating the slice of the dest operand of |
| // unpack op. |
| SmallVector<OpFoldResult> outputOffsets, outputSizes; |
| if (failed(getIterationDomainTileFromOperandTiles( |
| op, b, operandNumbers, allOffsets, allSizes, outputOffsets, |
| outputSizes))) |
| return failure(); |
| |
| auto oneAttr = b.getI64IntegerAttr(1); |
| int64_t outputRank = unPackOp.getDestRank(); |
| SmallVector<OpFoldResult> strides(outputRank, oneAttr); |
| |
| SmallVector<Value> tiledOperands; |
| // Create slice of the dest operand. |
| auto extractDestSlice = tensor::ExtractSliceOp::create( |
| b, loc, unPackOp.getDest(), outputOffsets, outputSizes, strides); |
| tiledOperands.push_back(extractDestSlice); |
| |
| strides.append(unPackOp.getSourceRank() - outputRank, oneAttr); |
| // Create slice of the source operand. |
| auto extractSourceSlice = tensor::ExtractSliceOp::create( |
| b, loc, unPackOp.getSource(), offsets, sizes, strides); |
| tiledOperands.insert(tiledOperands.begin(), extractSourceSlice); |
| for (auto tile : unPackOp.getInnerTiles()) |
| tiledOperands.push_back(tile); |
| |
| // Create tiled unpack op. |
| UnPackOp tiledUnPackOp = |
| UnPackOp::create(b, loc, TypeRange{extractDestSlice.getType()}, |
| tiledOperands, unPackOp.getProperties(), |
| unPackOp->getDiscardableAttrDictionary().getValue()); |
| |
| return TilingResult{{tiledUnPackOp}, |
| SmallVector<Value>(tiledUnPackOp->getResults()), |
| llvm::to_vector(ArrayRef<Operation *>{ |
| extractSourceSlice, extractDestSlice})}; |
| } |
| }; |
| |
| } // namespace |
| |
| template <typename OpType> |
| static void registerOne(MLIRContext *ctx) { |
| OpType::template attachInterface<LinalgOpTilingInterfaceModel<OpType>>(*ctx); |
| OpType::template attachInterface< |
| LinalgOpPartialReductionInterfaceModel<OpType>>(*ctx); |
| } |
| |
| /// Variadic helper function. |
| template <typename... OpTypes> |
| static void registerAll(MLIRContext *ctx) { |
| (registerOne<OpTypes>(ctx), ...); |
| } |
| |
| #define GET_OP_LIST |
| |
| void mlir::linalg::registerTilingInterfaceExternalModels( |
| DialectRegistry ®istry) { |
| registry.addExtension(+[](MLIRContext *ctx, linalg::LinalgDialect *dialect) { |
| registerOne<linalg::GenericOp>(ctx); |
| linalg::PackOp::attachInterface<PackOpTiling>(*ctx); |
| linalg::UnPackOp::attachInterface<UnPackOpTiling>(*ctx); |
| registerAll< |
| #include "mlir/Dialect/Linalg/IR/LinalgStructuredOps.cpp.inc" |
| >(ctx); |
| }); |
| } |
| |
| void mlir::linalg::registerTilingInterfaceExternalModelsForPackUnPackOps( |
| DialectRegistry ®istry) { |
| registry.addExtension(+[](MLIRContext *ctx, LinalgDialect *dialect) { |
| linalg::PackOp::attachInterface<PackOpTiling>(*ctx); |
| linalg::UnPackOp::attachInterface<UnPackOpTiling>(*ctx); |
| }); |
| } |