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