1 //===- LinalgInterfaces.cpp - Linalg interfaces implementation ------------===// 2 // 3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 4 // See https://llvm.org/LICENSE.txt for license information. 5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 6 // 7 //===----------------------------------------------------------------------===// 8 9 #include "mlir/Dialect/Linalg/IR/LinalgInterfaces.h" 10 11 #include "mlir/Dialect/Affine/IR/AffineOps.h" 12 #include "mlir/Dialect/MemRef/IR/MemRef.h" 13 #include "mlir/IR/AffineExprVisitor.h" 14 #include "mlir/IR/AffineMap.h" 15 #include "llvm/ADT/SmallSet.h" 16 17 using namespace mlir; 18 using namespace mlir::linalg; 19 20 /// Include the definitions of the copy operation interface. 21 #include "mlir/Dialect/Linalg/IR/LinalgInterfaces.cpp.inc" 22 23 //===----------------------------------------------------------------------===// 24 // ContractionOpInterface implementation 25 //===----------------------------------------------------------------------===// 26 27 /// Return true if the use-def chain from `v` to `from` consists of 0 or more 28 /// unary single-operand operations. 29 // TODO: relax to multi-operands with constants, which are technically unary ops 30 // as needed (e.g. add5). 31 static bool isChainOfUnaryOpsFrom(Value v, Value from) { 32 while (true) { 33 if (v == from) 34 return true; 35 Operation *op = v.getDefiningOp(); 36 if (!op || op->getNumOperands() != 1) 37 return false; 38 v = op->getOperand(0); 39 }; 40 } 41 42 /// Return the unique instance of OpType in `block` if it is indeed unique. 43 /// Return null if none or more than 1 instances exist. 44 template <typename OpType> 45 static OpType getSingleOpOfType(Block &block) { 46 OpType res = nullptr; 47 block.walk([&](OpType op) { 48 if (res) { 49 res = nullptr; 50 return WalkResult::interrupt(); 51 } 52 res = op; 53 return WalkResult::advance(); 54 }); 55 return res; 56 } 57 58 /// Detect whether res is any permutation of `u5(u1(c) + u2(u3(a) * u4(b)))` 59 /// on the field (AddOpType, MulOpType), where u1, u2, u3, u4 and u5 represent 60 /// unary operations that may change the type. 61 template <typename AddOpType, typename MulOpType> 62 static bool isAddMul(Block &block) { 63 if (block.getNumArguments() != 3) 64 return false; 65 Operation *yieldOp = block.getTerminator(); 66 if (yieldOp->getNumOperands() != 1) 67 return false; 68 69 AddOpType addOp = getSingleOpOfType<AddOpType>(block); 70 MulOpType mulOp = getSingleOpOfType<MulOpType>(block); 71 if (!addOp || !mulOp) 72 return false; 73 74 Value argA = block.getArgument(0), argB = block.getArgument(1); 75 Value a = mulOp->getOperand(0), b = mulOp->getOperand(1); 76 Value mul = mulOp->getResult(0); 77 Value argC = block.getArgument(2); 78 Value c1 = addOp->getOperand(0), c2 = addOp->getOperand(1); 79 Value add = addOp->getResult(0); 80 Value res = yieldOp->getOperand(0); 81 // Result traces back to add. 82 auto un = isChainOfUnaryOpsFrom; 83 bool success = un(res, add); 84 // One of the operands of add traces back to argC, the other to the mul. 85 success |= (un(c1, argC) && un(c2, mul)) || ((un(c1, mul)) && un(c2, argC)); 86 // One of the operands of mul traces back to argA, the other to argB. 87 success |= (un(a, argA) && un(b, argB)) || ((un(a, argB)) && un(b, argA)); 88 return success; 89 } 90 91 enum MatchContractionResult { 92 Success = 0, 93 NotLinalgOp, 94 WrongNumOperands, 95 NoReduction, 96 NotProjectedPermutations, 97 NotAddMul 98 }; 99 static MatchContractionResult isContractionInterfaceImpl(Operation *op) { 100 auto linalgOp = dyn_cast<linalg::LinalgOp>(op); 101 if (!linalgOp) 102 return MatchContractionResult::NotLinalgOp; 103 if (linalgOp.getNumInputs() != 2 || linalgOp.getNumOutputs() != 1) 104 return MatchContractionResult::WrongNumOperands; 105 auto mapRange = linalgOp.indexing_maps().getAsValueRange<AffineMapAttr>(); 106 if (linalgOp.getNumReductionLoops() == 0) 107 return MatchContractionResult::NoReduction; 108 if (llvm::any_of(mapRange, 109 [](AffineMap m) { return !m.isProjectedPermutation(); })) 110 return MatchContractionResult::NotProjectedPermutations; 111 // TODO: more fields than add/mul. 112 if (!isAddMul<AddFOp, MulFOp>(linalgOp->getRegion(0).front()) && 113 !isAddMul<AddIOp, MulIOp>(linalgOp->getRegion(0).front())) 114 return MatchContractionResult::NotAddMul; 115 return MatchContractionResult::Success; 116 } 117 118 bool mlir::linalg::isaContractionOpInterface(LinalgOp linalgOp) { 119 if (!linalgOp) 120 return false; 121 Operation *op = linalgOp.getOperation(); 122 return isa<ContractionOpInterface>(op) || 123 (isContractionInterfaceImpl(op) == MatchContractionResult::Success); 124 } 125 126 /// Verify that a LinalgOp `op` is a contraction. 127 /// A Linalg contraction is defined in general terms: 128 /// 1. Has 2 input and 1 output shapes. 129 /// 2. Has at least one reduction dimension. 130 /// 3. Has only projected permutation indexing maps. 131 /// 4. its body computes `u5(u1(c) + u2(u3(a) * u4(b)))` on some field 132 /// (AddOpType, MulOpType), where u1, u2, u3, u4 and u5 represent scalar unary 133 /// operations that may change the type (e.g. for mixed-precision). 134 /// As a consequence, when vectorization of such an op occurs, the only special 135 /// behavior is that the (unique) MulOpType is vectorized into a 136 /// `vector.contract`. All other ops are handled in a generic fashion. 137 /// In the future, we may wish to allow more input arguments and elementwise and 138 /// constant operations that do not involve the reduction dimension(s). 139 LogicalResult mlir::linalg::detail::verifyContractionInterface(Operation *op) { 140 auto res = isContractionInterfaceImpl(op); 141 if (res == MatchContractionResult::NotLinalgOp) 142 return op->emitError("expected a LinalgOp"); 143 if (res == MatchContractionResult::WrongNumOperands) 144 return op->emitError("expected op with 2 inputs and 1 outputs"); 145 if (res == MatchContractionResult::NoReduction) 146 return op->emitError("expected at least a reduction loop"); 147 if (res == MatchContractionResult::NotProjectedPermutations) 148 return op->emitError("expected all indexings to be projected permutations"); 149 if (res == MatchContractionResult::NotAddMul) 150 return op->emitError("(add, mul) operations not found"); 151 return success(); 152 } 153 154 //===----------------------------------------------------------------------===// 155 // StructuredOpInterface implementation 156 //===----------------------------------------------------------------------===// 157 158 /// Fully compose map with operands and canonicalize the result. 159 /// Return the `createOrFold`'ed AffineApply op. 160 static Value createFoldedComposedAffineApply(OpBuilder &b, Location loc, 161 AffineMap map, 162 ValueRange operandsRef) { 163 SmallVector<Value, 4> operands(operandsRef.begin(), operandsRef.end()); 164 fullyComposeAffineMapAndOperands(&map, &operands); 165 canonicalizeMapAndOperands(&map, &operands); 166 return b.createOrFold<AffineApplyOp>(loc, map, operands); 167 } 168 169 SmallVector<Value, 4> mlir::linalg::applyMapToValues(OpBuilder &b, Location loc, 170 AffineMap map, 171 ValueRange values) { 172 SmallVector<Value, 4> res; 173 res.reserve(map.getNumResults()); 174 unsigned numDims = map.getNumDims(), numSym = map.getNumSymbols(); 175 // For each `expr` in `map`, applies the `expr` to the values extracted from 176 // ranges. If the resulting application can be folded into a Value, the 177 // folding occurs eagerly. 178 for (auto expr : map.getResults()) { 179 AffineMap map = AffineMap::get(numDims, numSym, expr); 180 res.push_back(createFoldedComposedAffineApply(b, loc, map, values)); 181 } 182 return res; 183 } 184 185 SmallVector<Value, 4> LinalgOp::createFlatListOfOperandDims(OpBuilder &b, 186 Location loc) { 187 SmallVector<Value, 4> res; 188 for (Value v : getShapedOperands()) { 189 ShapedType t = v.getType().template cast<ShapedType>(); 190 for (unsigned i = 0, e = t.getRank(); i < e; ++i) 191 res.push_back(b.createOrFold<memref::DimOp>(loc, v, i)); 192 } 193 return res; 194 } 195 196 SmallVector<int64_t, 4> LinalgOp::createFlatListOfOperandStaticDims() { 197 SmallVector<int64_t, 4> res; 198 for (Value v : getShapedOperands()) { 199 ShapedType t = v.getType().template cast<ShapedType>(); 200 assert(t.hasStaticShape() && "expected operands to have static shapes"); 201 llvm::append_range(res, t.getShape()); 202 } 203 return res; 204 } 205 206 SmallVector<Range, 4> LinalgOp::createLoopRanges(OpBuilder &b, Location loc) { 207 AffineMap map = getLoopsToShapesMap(); 208 unsigned numDims = map.getNumDims(), numRes = map.getNumResults(); 209 auto viewSizes = createFlatListOfOperandDims(b, loc); 210 SmallVector<Range, 4> res(numDims); 211 Value zeroVal = b.create<ConstantIndexOp>(loc, 0); 212 Value oneVal = b.create<ConstantIndexOp>(loc, 1); 213 for (unsigned idx = 0; idx < numRes; ++idx) { 214 auto result = map.getResult(idx); 215 if (auto d = result.dyn_cast<AffineDimExpr>()) { 216 if (res[d.getPosition()].offset) 217 continue; 218 res[d.getPosition()] = Range{zeroVal, viewSizes[idx], oneVal}; 219 } 220 } 221 return res; 222 } 223 224 SmallVector<int64_t, 4> LinalgOp::computeStaticLoopSizes() { 225 AffineMap map = getLoopsToShapesMap(); 226 unsigned numDims = map.getNumDims(), numRes = map.getNumResults(); 227 SmallVector<int64_t, 4> allShapeSizes = createFlatListOfOperandStaticDims(); 228 SmallVector<int64_t, 4> res(numDims, 0); 229 for (unsigned idx = 0; idx < numRes; ++idx) { 230 auto result = map.getResult(idx); 231 if (auto d = result.dyn_cast<AffineDimExpr>()) 232 res[d.getPosition()] = allShapeSizes[idx]; 233 } 234 return res; 235 } 236 237 /// Visitor to check if any of the given set of positions from AffineDimExprs 238 /// are used within an AffineExpr. 239 struct HasAffineDimExprVisitor 240 : public AffineExprVisitor<HasAffineDimExprVisitor, bool> { 241 HasAffineDimExprVisitor(llvm::SmallSet<unsigned, 4> &positions) 242 : positions(positions) {} 243 244 bool visitAffineBinaryOpExpr(AffineBinaryOpExpr binaryOpExpr) { 245 return visit(binaryOpExpr.getLHS()) || visit(binaryOpExpr.getRHS()); 246 } 247 248 bool visitDimExpr(AffineDimExpr dimExpr) { 249 return positions.count(dimExpr.getPosition()); 250 } 251 252 bool visitConstantExpr(AffineConstantExpr constExpr) { return false; } 253 254 bool visitSymbolExpr(AffineSymbolExpr symbolExpr) { return false; } 255 256 private: 257 llvm::SmallSet<unsigned, 4> positions; 258 }; 259 260 LogicalResult LinalgOp::reifyReturnTypeShapesPerResultDim( 261 OpBuilder &b, SmallVectorImpl<SmallVector<Value>> &reifiedReturnShapes) { 262 // An example that helps understand the logic below. 263 // Consider the following expression O(i+j, j) += A(i,k) * B(k, j) 264 // We want to express the shape of dim 0 of O in terms of shape of the inputs. 265 // This is achieved as follows. 266 // loopsToShapesMap = (d0, d1, d2) -> (d0, d2, d2, d1, d0 + d1, d1) 267 // subMapOfResultShapes = (d0, d1, d2) -> (d0 + d1, d1) 268 // shapesToLoopsMap = (d0, d2, d2, d3, d4, d5) -> (d0, d3, d2) 269 // resultShapesFromInputShapes = subMapOfResultDim.compose(shapesToLoopMap) 270 // = (d0, d1, d2, d3, d4, d5) -> (d0 + d1, d1) 271 AffineMap loopsToShapesMap = getLoopsToShapesMap(); 272 273 // Find the position in the above map that represents the shape of the 274 // result:dim being inferred. 275 auto resultShapesSubMapPos = getResultsPositionInLoopsToShapeMap(); 276 277 /// From loopsToShapesMap extract the submap that represents the shape of the 278 /// (resultIdx, dim) needed. 279 SmallVector<unsigned, 4> resultPosRange = 280 llvm::to_vector<4>(llvm::seq<unsigned>(resultShapesSubMapPos.first, 281 resultShapesSubMapPos.second)); 282 AffineMap loopToResultsShapeMap = loopsToShapesMap.getSubMap(resultPosRange); 283 AffineMap resultShapesFromInputShapesMap = 284 loopToResultsShapeMap.compose(getShapesToLoopsMap()); 285 286 // Check that the result dim map does not contain the positions corresponding 287 // to the outputs. 288 llvm::SmallSet<unsigned, 4> outputDims; 289 llvm::for_each(resultPosRange, 290 [&outputDims](unsigned dim) { outputDims.insert(dim); }); 291 HasAffineDimExprVisitor checkDimExpr(outputDims); 292 Location loc = getOperation()->getLoc(); 293 auto allResultDimValues = 294 applyMapToValues(b, loc, resultShapesFromInputShapesMap, 295 createFlatListOfOperandDims(b, loc)); 296 unsigned pos = 0; 297 ArrayRef<AffineExpr> shapeExprs = resultShapesFromInputShapesMap.getResults(); 298 for (auto resultIdx : llvm::seq<unsigned>(0, getNumOutputs())) { 299 ShapedType resultType = getOutputShapedType(resultIdx); 300 SmallVector<Value> shapes; 301 for (unsigned dim : llvm::seq<unsigned>(0, resultType.getRank())) { 302 if (checkDimExpr.visit(shapeExprs[pos])) 303 shapes.push_back( 304 b.createOrFold<memref::DimOp>(loc, getOutput(resultIdx), dim)); 305 else 306 shapes.push_back(allResultDimValues[pos]); 307 pos++; 308 } 309 reifiedReturnShapes.emplace_back(std::move(shapes)); 310 } 311 return success(); 312 } 313 314 LogicalResult mlir::linalg::detail::verifyStructuredOpInterface(Operation *op) { 315 LinalgOp linalgOp = cast<LinalgOp>(op); 316 // Expect at least one shaped operand. 317 // This means an op that constructs a tensor out of indices cannot be a 318 // LinalgOp at the moment. For now this will have to be a special op until we 319 // have output shape operands that are not tensors. 320 auto nShapedOperands = linalgOp.getNumShapedOperands(); 321 if (nShapedOperands == 0) 322 return linalgOp.emitOpError("expected at least 1 Shaped operand"); 323 if (failed(OpTrait::impl::verifyAtLeastNOperands(op, nShapedOperands))) 324 return failure(); 325 // Should have at least one output tensor per result tensor. 326 // Can also have outbut buffers that do not correspond to results. 327 if (op->getNumResults() > linalgOp.getNumOutputTensors()) 328 return op->emitError("unexpected #results > #outputs"); 329 330 // Before checking indexing maps, we need to make sure the attributes 331 // referenced by it are valid. 332 if (linalgOp.hasDynamicIndexingMaps()) 333 if (failed(linalgOp.verifyIndexingMapRequiredAttributes())) 334 return failure(); 335 336 // All shaped operands must be indexed. 337 if (linalgOp.indexing_maps().size() != linalgOp.getNumShapedOperands()) 338 return linalgOp.emitOpError("expected the number of indexing_map (") 339 << linalgOp.indexing_maps().size() 340 << ") to be equal to the number of shaped operands (" 341 << linalgOp.getNumShapedOperands() << ")"; 342 343 SmallVector<AffineMap, 4> indexingMaps; 344 indexingMaps.reserve(linalgOp.indexing_maps().size()); 345 for (auto en : llvm::enumerate(linalgOp.indexing_maps())) { 346 auto idx = en.index(); 347 auto m = en.value().template cast<AffineMapAttr>().getValue(); 348 indexingMaps.push_back(m); // Save reference to map for further checks. 349 auto shapedValue = linalgOp.getShapedType(idx); 350 351 // Symbols disallowed. 352 if (m.getNumSymbols() != 0) 353 return linalgOp.emitOpError("unexpected symbols in indexing_map #") 354 << idx; 355 356 // Domain must be consistent. 357 auto nLoops = linalgOp.getNumLoops(); 358 if (m.getNumDims() != nLoops) 359 return linalgOp.emitOpError("expected indexing_map #") 360 << idx << " to have " << nLoops 361 << " dim(s) to match the number of loops"; 362 363 if (m.getNumResults() != shapedValue.getRank()) 364 return linalgOp.emitOpError("expected shaped value rank (") 365 << shapedValue.getRank() 366 << ") to match the result rank of indexing_map #" << idx << " (" 367 << m.getNumResults() << ")"; 368 } 369 370 SmallVector<AffineExpr, 4> redDims; 371 linalgOp.getReductionDims(redDims); 372 373 // Simplifying assumption: either full tensor or full buffer mode. 374 // This allows simpler verification of output operands vs result types 375 // without premature tracking of which operand is what in mixed-mode. 376 // TODO: relax when mixed-mode needs to pass verification. 377 if (linalgOp.getNumOutputBuffers() > 0 && linalgOp.getNumOutputTensors() > 0) 378 return op->emitError("expected output operands to all have tensor type or " 379 "all have buffer type"); 380 381 for (auto it : 382 llvm::zip(linalgOp.getOutputOpOperands(), op->getResultTypes())) { 383 if (!std::get<0>(it).get().getType().isa<RankedTensorType>()) 384 continue; 385 if (std::get<0>(it).get().getType() != std::get<1>(it)) 386 return op->emitError("expected type of operand #") 387 << std::get<0>(it).getOperandNumber() << " (" 388 << std::get<0>(it).get().getType() << ")" 389 << " to match type of corresponding result (" << std::get<1>(it) 390 << ")"; 391 } 392 393 // Output tensor indexing map may not depend on reduction indices. 394 for (OpOperand &opOperand : linalgOp.getOutputOpOperands()) { 395 AffineMap outputMap = linalgOp.getIndexingMap(opOperand.getOperandNumber()); 396 for (auto expr : outputMap.getResults()) { 397 for (auto dim : redDims) { 398 unsigned pos = dim.cast<AffineDimExpr>().getPosition(); 399 if (expr.isFunctionOfDim(pos)) { 400 std::string exprStr; 401 { 402 llvm::raw_string_ostream os(exprStr); 403 os << expr; 404 } 405 return op->emitError( 406 "unexpected output tensor expression in indexing map #") 407 << (opOperand.getOperandNumber() - linalgOp.getNumInputs()) 408 << " a.k.a '" << exprStr 409 << "' is function of reduction iterator 'd" << pos << "'"; 410 } 411 } 412 } 413 } 414 415 // Named ops that are defined manually have a region builder but no region at 416 // this time. Assume the region is well-formed by specification. 417 // TODO: use linalg-ods-gen for all ops when we have enough expressive power. 418 if (linalgOp->getNumRegions() == 0) { 419 assert(!linalgOp.getRegionBuilder() && "regionBuilder but no region"); 420 return success(); 421 } 422 423 auto ®ion = linalgOp->getRegion(0); 424 if (linalgOp->getNumRegions() > 1 || !llvm::hasSingleElement(region)) 425 return op->emitOpError("expected 1 region with 1 block"); 426 427 if (!linalgOp.getShapesToLoopsMap()) 428 return op->emitOpError("expected the shape-to-loops map to be non-null"); 429 430 // Simplifying assumption: bbargs match 1-1 with shape operands elemental 431 // types. 432 // TODO: once ranked shape types are plugged in, we may want to drop the 433 // corresponding bbargs, that can never be read from. This will be subject to 434 // consistency discussions (i.e. what to do with output tensors whose bbarg is 435 // not used). 436 Block &block = linalgOp->getRegion(0).front(); 437 unsigned numBBIvs = linalgOp.getNumPayloadInductionVariables(); 438 439 if (linalgOp.getNumShapedOperands() + numBBIvs != block.getNumArguments()) 440 return op->emitError("expected as many non-induction variable region " 441 "arguments as the number of shaped operands"); 442 443 // Note: the number and type of yield values are checked in the YieldOp. 444 for (unsigned i = 0; i < numBBIvs; ++i) 445 if (!block.getArgument(i).getType().isIndex()) 446 return op->emitOpError("expected index block argument #") << i; 447 448 unsigned idx = 0; 449 for (auto it : llvm::zip(linalgOp.getShapedOperandTypes(), 450 block.getArguments().drop_front(numBBIvs))) { 451 if (std::get<0>(it).getElementType() != std::get<1>(it).getType()) 452 return op->emitError("expected type of bb argument #") 453 << (idx + numBBIvs) << " (" << std::get<1>(it).getType() << ")" 454 << " to match element type of corresponding shaped operand (" 455 << std::get<0>(it).getElementType() << ")"; 456 ++idx; 457 } 458 459 // Check if given shapes match to inferred shapes. 460 Optional<SmallVector<int64_t, 4>> loopRanges = linalgOp.getStaticLoopRanges(); 461 if (!loopRanges) 462 return linalgOp.emitError("unable to find loop range for operation"); 463 464 // Verify only static cases since we can't get exact dimension sizes and loop 465 // ranges for dynamic cases in this stage. 466 if (llvm::none_of(*loopRanges, [](int64_t &range) { 467 return range == ShapedType::kDynamicSize; 468 })) { 469 for (int64_t &range : *loopRanges) 470 range -= 1; 471 for (const auto &en : llvm::enumerate(linalgOp.getShapedOperandTypes())) { 472 auto indices = indexingMaps[en.index()].compose(*loopRanges); 473 for (auto j : llvm::seq<unsigned>(0, en.value().getRank())) { 474 475 // Ignore dynamic dimension or the case that the inferred last index is 476 // zero. The index is increasing or decreasing in Linalg, for example, 477 // the last index should be `0` or `size-1`. We only check the cases 478 // that are non-zero because most of cases are increasing and it is too 479 // expensive to find the shape of decreasing cases. 480 if (en.value().isDynamicDim(j) || indices[j] == 0) 481 continue; 482 483 // The size of shaped operands and inferred dimension size should be 484 // same. But, for now we check if the inferred sizes are in boundary of 485 // shaped operands' size or not in case that Affine Expressions are 486 // complicated such as d0 * 3 + d1 since it is not easy to handle the 487 // issues. 488 auto inferredSize = indices[j] + 1; 489 auto shapedDimSize = en.value().getDimSize(j); 490 if (indexingMaps[en.index()].getResult(j).dyn_cast<AffineDimExpr>()) { 491 if (inferredSize != shapedDimSize) { 492 return linalgOp.emitOpError("inferred shaped operand #") 493 << en.index() << " has shape's dimension #" << j << " to be " 494 << inferredSize << ", but found " << shapedDimSize; 495 } 496 } else { 497 if (inferredSize > shapedDimSize) { 498 return linalgOp.emitOpError("inferred shaped operand #") 499 << en.index() << " has shape's dimension #" << j 500 << " to be greater than or equal to " << inferredSize 501 << ", but found " << shapedDimSize; 502 } 503 } 504 } 505 } 506 } 507 508 return success(); 509 } 510