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