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 &region = 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