MLIR 24.0.0git
Specialize.cpp
Go to the documentation of this file.
1//===- Specialize.cpp - linalg generic ops to named ops ------------------===//
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// This file implements a method to specialize generic operations to named
10// operations. Conceptually it is the opposite of generalize.cpp.
11//
12//===----------------------------------------------------------------------===//
13
23
24namespace mlir {
25#define GEN_PASS_DEF_LINALGSPECIALIZEGENERICOPSPASS
26#include "mlir/Dialect/Linalg/Passes.h.inc"
27} // namespace mlir
28
29#define DEBUG_TYPE "linalg-specialization"
30
31using namespace mlir;
32using namespace mlir::linalg;
33
34//===----------------------------------------------------------------------===//
35// Specialize linalg generic to elementwise ops.
36//===----------------------------------------------------------------------===//
37
38// Given an elementwise single binary linalg generic op, checks whether the
39// binary op accesses operands as swapped. e.g.
40// this differentiates between a linalg-generic body that contains:
41// ^bb0(%a: f32, %b: f32, %c : f32):
42// %0 = arith.subf %a, %b : f32
43// linalg.yield %0: f32
44// against:
45// ^bb0(%a: f32, %b: f32, %c : f32):
46// %0 = arith.subf %b, %a : f32
47// linalg.yield %0: f32
48// Former is linalg.sub(a,b), latter is linalg.sub(b,a).
49static bool areBinOpsSwapped(GenericOp genericOp) {
50 Block *body = genericOp.getBody();
51 Operation *op = &body->front();
52 bool swapped = false;
53 if (op->getOpOperand(0).get() != body->getArgument(0)) {
54 swapped = true;
55 assert(op->getOpOperand(0).get() == body->getArgument(1) &&
56 op->getOpOperand(1).get() == body->getArgument(0) &&
57 "binary op uses just one block arg");
58 }
59 return swapped;
60}
61
62// Given an elementwise single unary linalg generic op whose body operation is a
63// binary operation, check if one of its operands is a scalar value defined
64// outside the generic op, set its index, and return true. Otherwise return
65// false. The index is unique because the block argument is used at
66// least by one operand, as checked in `isaElemwiseSingleUnaryOpInterface`.
67//
68// Example:
69// %cst = arith.constant 3.14 : f32
70// %0 = linalg.generic { indexing_maps = [#mapA, #mapRes], ... }
71// ins(%A : tensor<?xf32>) outs(...) {
72// ^bb0(%a: f32, %out : f32):
73// %0 = arith.mulf %a, %cst : f32
74// linalg.yield %0: f32
75// } -> tensor<?xf32>
76// Here, the returned index is 1, and the generic op can be represented as
77// %0 = linalg.elementwise <mul>
78// indexing_maps = [#mapA, affine_map<(d0) -> ()>, #mapRes]
79// ins(%A, %cst : tensor<?xf32>, f32) outs(...) -> tensor<?xf32>
80static bool findIndexOfScalarOperand(GenericOp genericOp, int &index) {
81 Block *body = genericOp.getBody();
82 Operation *op = &body->front();
83 for (auto [i, v] : llvm::enumerate(op->getOperands())) {
84 if (auto blockArg = dyn_cast<BlockArgument>(v);
85 blockArg && blockArg.getOwner() == body)
86 continue; // not an outside value...
87 index = i;
88 return true;
89 }
90 return false;
91}
92
93// Attempt to specialize unary or binary linalg.generic ops to named elementwise
94// ops or linalg.elementwise.
95//
96// Example:
97// %0 = linalg.generic {
98// indexing_maps = [affine_map<(d0, d1) -> (d0, d1)>,
99// affine_map<(d0, d1) -> (d0, d1)>],
100// iterator_types = ["parallel", "parallel"]
101// } ins(%In : tensor<?x?xf32>) outs(%Out : tensor<?x?xf32>) {
102// ^bb0(%in: f32, %out: f32):
103// %1 = math.exp %in : f32
104// linalg.yield %1 : f32
105// } -> tensor<?x?xf32>
106//
107// is specialized to
108// linalg.elementwise <exp> ...
109//
110// A named op is emitted instead for binary/ternary ops that still have a
111// linalg.* named equivalent (e.g. linalg.add).
112//
113// Only the category op can carry non-identity indexing maps; these are
114// transferred verbatim from the `genericOp`.
115//
116// In addition to the canonical forms used by the generalization path, this
117// function can handle the following variations:
118//
119// 1) Swapped operands in binary ops (see the `areBinOpsSwapped` helper)
120// 2) Unary generic ops with a binary body op (see the
121// `findIndexOfScalarOperand` helper)
122static FailureOr<LinalgOp> specializeLinalgElementwise(RewriterBase &rewriter,
123 GenericOp genericOp,
124 bool emitCategoryOp) {
125 bool hasNonIdentityMaps =
126 !llvm::all_of(genericOp.getIndexingMapsArray(),
127 [](AffineMap map) { return map.isIdentity(); });
128
129 // Early exit: Named ops cannot carry user-defined maps.
130 if (hasNonIdentityMaps && !emitCategoryOp)
131 return rewriter.notifyMatchFailure(
132 genericOp,
133 "non-identity indexing maps prevent specialization to named op");
134
135 // Classify the generic op.
136 bool isUnary = genericOp.getNumDpsInputs() == 1;
137 bool isBinary = genericOp.getNumDpsInputs() == 2;
138
139 // Will inspect the body operation to determine named op or elementwise kind.
140 Operation *op = &genericOp.getBody()->front();
141
142 // Detect variations from canonical forms.
143 bool hasSwappedOperands = isBinary && areBinOpsSwapped(genericOp);
144 int scalarOprIdx = -1;
145 bool hasScalarOperand = isUnary && op->getNumOperands() == 2 &&
146 findIndexOfScalarOperand(genericOp, scalarOprIdx);
147
148 // Helper to dispatch between named op and `linalg.elementwise`.
149 // Lambdas with explicit template parameter list are a C++20 feature, hence
150 // the dummy op object.
151 auto replaceOp = [&](auto namedOp, ElementwiseKind kind,
152 bool mayHoistScalarOperand = true) -> LinalgOp {
153 SmallVector<Value> inputs = genericOp.getDpsInputs();
154 if (hasSwappedOperands)
155 std::swap(inputs[0], inputs[1]);
156
157 LinalgOp newOp;
158 using NamedOpTy = decltype(namedOp);
159 // A null named op means the op only has a category form; emit
160 // `linalg.elementwise` regardless of the requested output form.
161 if (!std::is_null_pointer_v<NamedOpTy>) {
162 if constexpr (!std::is_null_pointer_v<NamedOpTy>)
163 newOp = NamedOpTy::create(rewriter, genericOp.getLoc(), inputs,
164 genericOp.getDpsInits(),
166 } else {
167 SmallVector<AffineMap> indexingMaps = genericOp.getIndexingMapsArray();
168 // Swap indexing maps, too.
169 if (hasSwappedOperands)
170 std::swap(indexingMaps[0], indexingMaps[1]);
171
172 // Represent unary generic op as a binary `linalg.elementwise` with a
173 // scalar operand and broadcasting map.
174 if (hasScalarOperand && mayHoistScalarOperand) {
175 // Adjust inputs and indexing maps accordingly.
176 inputs.insert(inputs.begin() + scalarOprIdx,
177 op->getOperand(scalarOprIdx));
178 auto scalarBroadcastMap =
179 AffineMap::get(genericOp.getNumParallelLoops(), /*symbolCount=*/0,
180 rewriter.getContext());
181 indexingMaps.insert(indexingMaps.begin() + scalarOprIdx,
182 scalarBroadcastMap);
183 }
184 newOp = ElementwiseOp::create(
185 rewriter, genericOp.getLoc(), inputs, genericOp.getDpsInits(), kind,
186 rewriter.getAffineMapArrayAttr(indexingMaps));
187 }
188
189 rewriter.replaceOp(genericOp, newOp);
190 return newOp;
191 };
192
193 // There are no named ops for these elementwise operations; can only emit the
194 // category form.
195 if (emitCategoryOp) {
196 if (isa<math::ExpOp>(op))
197 return replaceOp(nullptr, ElementwiseKind::exp);
198 if (isa<math::AbsFOp>(op))
199 return replaceOp(nullptr, ElementwiseKind::abs);
200 if (isa<math::CeilOp>(op))
201 return replaceOp(nullptr, ElementwiseKind::ceil);
202 if (isa<math::FloorOp>(op))
203 return replaceOp(nullptr, ElementwiseKind::floor);
204 if (isa<arith::NegFOp>(op))
205 return replaceOp(nullptr, ElementwiseKind::negf);
206 if (auto divOp = dyn_cast<arith::DivFOp>(op)) {
207 if (auto constOp = dyn_cast_if_present<arith::ConstantOp>(
208 divOp.getLhs().getDefiningOp()))
209 if (cast<FloatAttr>(constOp.getValue()).getValue().isExactlyValue(1.0))
210 return replaceOp(nullptr, ElementwiseKind::reciprocal,
211 /*mayHoistScalarOperand=*/false);
212 }
213 if (isa<math::RoundOp>(op))
214 return replaceOp(nullptr, ElementwiseKind::round);
215 if (isa<math::SqrtOp>(op))
216 return replaceOp(nullptr, ElementwiseKind::sqrt);
217 if (isa<math::RsqrtOp>(op))
218 return replaceOp(nullptr, ElementwiseKind::rsqrt);
219 if (auto mulOp = dyn_cast<arith::MulFOp>(op);
220 mulOp && mulOp.getLhs() == mulOp.getRhs())
221 return replaceOp(nullptr, ElementwiseKind::square);
222 if (isa<math::TanhOp>(op))
223 return replaceOp(nullptr, ElementwiseKind::tanh);
224 if (isa<math::ErfOp>(op))
225 return replaceOp(nullptr, ElementwiseKind::erf);
226 if (isa<math::SinOp>(op))
227 return replaceOp(nullptr, ElementwiseKind::sin);
228 if (isa<math::CosOp>(op))
229 return replaceOp(nullptr, ElementwiseKind::cos);
230 if (isa<math::TanOp>(op))
231 return replaceOp(nullptr, ElementwiseKind::tan);
232 if (isa<math::AcosOp>(op))
233 return replaceOp(nullptr, ElementwiseKind::acos);
234 if (isa<math::AcoshOp>(op))
235 return replaceOp(nullptr, ElementwiseKind::acosh);
236 if (isa<math::AsinOp>(op))
237 return replaceOp(nullptr, ElementwiseKind::asin);
238 if (isa<math::AsinhOp>(op))
239 return replaceOp(nullptr, ElementwiseKind::asinh);
240 if (isa<math::AtanOp>(op))
241 return replaceOp(nullptr, ElementwiseKind::atan);
242 if (isa<math::AtanhOp>(op))
243 return replaceOp(nullptr, ElementwiseKind::atanh);
244 if (isa<math::LogOp>(op))
245 return replaceOp(nullptr, ElementwiseKind::log);
246 if (isa<math::Log10Op>(op))
247 return replaceOp(nullptr, ElementwiseKind::log10);
248 if (isa<math::Log1pOp>(op))
249 return replaceOp(nullptr, ElementwiseKind::log1p);
250 if (isa<math::Log2Op>(op))
251 return replaceOp(nullptr, ElementwiseKind::log2);
252
253 // The remaining kinds are binary. A single-input generic can only be
254 // represented as a binary elementwise if it has a scalar operand to hoist;
255 // otherwise (e.g. a body reusing a block argument twice) it has no
256 // category form.
257 if (isUnary && !hasScalarOperand)
258 return rewriter.notifyMatchFailure(
259 genericOp, "unary elementwise operation cannot be specialized to a "
260 "category op");
261
262 // Boolean-typed `linalg.add` and `linalg.mul` require special handling.
263 bool allBool = llvm::all_of(
264 op->getOperands(), [](Value v) { return v.getType().isInteger(1); });
265
266 if (isa<arith::AddFOp, arith::AddIOp, complex::AddOp>(op) ||
267 (allBool && isa<arith::OrIOp>(op)))
268 return replaceOp(nullptr, ElementwiseKind::add);
269 if (isa<arith::SubIOp, arith::SubFOp, complex::SubOp>(op))
270 return replaceOp(nullptr, ElementwiseKind::sub);
271 if (isa<arith::MulIOp, arith::MulFOp, complex::MulOp>(op) ||
272 (allBool && isa<arith::AndIOp>(op)))
273 return replaceOp(nullptr, ElementwiseKind::mul);
274 if (isa<arith::DivSIOp, arith::DivFOp, complex::DivOp>(op))
275 return replaceOp(nullptr, ElementwiseKind::div);
276 if (isa<arith::DivUIOp>(op))
277 return replaceOp(nullptr, ElementwiseKind::div_unsigned);
278 if (isa<arith::MaxSIOp, arith::MaximumFOp>(op))
279 return replaceOp(nullptr, ElementwiseKind::max_signed);
280 if (isa<arith::MinSIOp, arith::MinimumFOp>(op))
281 return replaceOp(nullptr, ElementwiseKind::min_signed);
282 if (isa<math::PowFOp>(op))
283 return replaceOp(nullptr, ElementwiseKind::powf);
284 // No named ops for unsigned maximum/minimum.
285 if (isa<arith::MaxUIOp>(op))
286 return replaceOp(nullptr, ElementwiseKind::max_unsigned);
287 if (isa<arith::MinUIOp>(op))
288 return replaceOp(nullptr, ElementwiseKind::min_unsigned);
289 }
290
291 return rewriter.notifyMatchFailure(
292 genericOp,
293 "elementwise operation cannot be specialized to named or category op");
294}
295
296//===----------------------------------------------------------------------===//
297// Specialize linalg generic to matmul variants.
298//===----------------------------------------------------------------------===//
299/// Identifies linalg.generic that is essentially named op of the form:
300// ` linalg.{batch_}?matmul{_transpose_a | _transpose_b}? `
301//
302// It is possible that a linalg.generic may be implementing a matmul but not
303// in a straight-forward way e.g. below is matrix multiply over some slice
304// ```
305// %0 = linalg.generic {
306// indexing_maps = [affine_map<(d0, d1, d2) -> (3, d1, d0)>,
307// affine_map<(d0, d1, d2) -> (d0, 5, d2)>,
308// affine_map<(d0, d1, d2) -> (d2, d1, 13)>],
309// iterator_types = ["parallel", "parallel", "parallel"]}
310// ins(%A, %B : tensor<20x20x20xf32>, tensor<20x20x20xf32>)
311// outs(%C : tensor<20x20x20xf32>) {
312// ^bb0(%a: f32, %b: f32, %c : f32):
313// %mul = arith.mulf %a, %b : f32
314// %add = arith.addf %mul, %c : f32
315// linalg.yield %add : f32
316// } -> tensor<20x20x20xf32>
317// ```
318// It is not possible to represent above as named op.
319// e.g. linalg.batch_matmul(%A, %B : tensor<20x20x20xf32>, ...) is
320// not the same as linalg.generic above.
321namespace {
322enum class IndexMatchResult {
323 Match = 0, // identity map.
324 Transposed, // transposed map.
325 Mismatch // none of the above.
326};
327
328// Checks whether the input Affine `map` contains two consecutive dims that
329// can be interpreted as accessing a 2D matrix. It is assumed that the row
330// column dimension are adjacent axis (in this order) and start at
331// `rowDimIdx` in the input map.
332//
333// e.g. consider A matrix in `C[M,N] = A[M,K] * B[K,N]`. We will check
334// whether the map of A is identity (match), transposed, or something
335// completely different (mis-match). Similar for B and C.
336static IndexMatchResult matchOperandMap(AffineMap map, unsigned rowDimIdx,
337 unsigned expectedPosOfRowDim,
338 unsigned expectedPosOfColDim) {
339 // Get the matrix multiply indices. They are past the batch indices.
340 auto exprOfRowDim = map.getResults()[rowDimIdx];
341 auto exprOfColDim = map.getResults()[rowDimIdx + 1];
342
343 // They should be pure dimension ids.
344 if (exprOfRowDim.getKind() != AffineExprKind::DimId ||
345 exprOfColDim.getKind() != AffineExprKind::DimId)
346 return IndexMatchResult::Mismatch;
347
348 auto posRowDim = cast<AffineDimExpr>(exprOfRowDim).getPosition();
349 auto posColDim = cast<AffineDimExpr>(exprOfColDim).getPosition();
350
351 if (expectedPosOfRowDim == posRowDim && expectedPosOfColDim == posColDim)
352 return IndexMatchResult::Match;
353
354 if (expectedPosOfRowDim == posColDim && expectedPosOfColDim == posRowDim)
355 return IndexMatchResult::Transposed;
356
357 return IndexMatchResult::Mismatch;
358}
359
360// Replaces genericOp with `NamedOpTy` op, supplied as a template arg.
361// All the variants expressed as pseudo regular expression:
362// `linalg.{batch_}?matmul` have same number of ins/out, so it's easy to
363// stamp different versions.
364// `castTy` is an optional type function that indicates whether (and which) cast
365// attribute is needed for the named matmul op variant.
366template <typename NamedOpTy>
367static LinalgOp replaceWithMatmulVariant(RewriterBase &rewriter, GenericOp op,
368 std::optional<TypeFn> castTy,
369 ArrayRef<AffineMap> indexingMaps) {
371 // Only explicitly specify the cast attribute for unsigned cast; signed is
372 // the default for linalg.matmul/linalg.batch_matmul.
373 if (castTy.has_value() && *castTy == TypeFn::cast_unsigned) {
374 auto castAttr = rewriter.getNamedAttr(
375 "cast", TypeFnAttr::get(rewriter.getContext(), *castTy));
376 attributes.push_back(castAttr);
377 }
378
379 // Set the original generic's maps to preserve operand indexing semantics like
380 // transposition.
381 SmallVector<Attribute, 3> indexingMapsAttrVal =
382 llvm::map_to_vector(indexingMaps, [](AffineMap map) -> Attribute {
383 return AffineMapAttr::get(map);
384 });
385 auto indexingMapsAttr = rewriter.getNamedAttr(
386 "indexing_maps", rewriter.getArrayAttr(indexingMapsAttrVal));
387 attributes.push_back(indexingMapsAttr);
388
389 LinalgOp namedOp = rewriter.replaceOpWithNewOp<NamedOpTy>(
390 op, ValueRange{op.getDpsInputs()[0], op.getDpsInputs()[1]},
391 ValueRange{op.getDpsInits()[0]}, attributes);
392
393 return namedOp;
394}
395
396// Returns the cast type to use for a matmul-like named op. If the generic
397// contains casts that cannot be represented (e.g. output casts or mixed
398// signedness), return std::nullopt.
399static std::optional<TypeFn> getCastTypeForMatmulLikeOp(GenericOp genericOp) {
400 bool foundCastForMatmulOutput = false;
401 SmallVector<TypeFn> castTyFns;
402 genericOp.getBody()->walk([&](CastOpInterface castOp) {
403 // Collect forward slice of the cast op to check if it is for the matmul
404 // output.
405 SetVector<Operation *> forwardSlice;
406 getForwardSlice(castOp, &forwardSlice);
407
408 // If there is no multiplication op in the forward slice, then this cast
409 // op is for the matmul output. Cast ops on matmul output cannot be
410 // expressed by the matmul op variant.
411 if (!llvm::any_of(forwardSlice, [](Operation *op) {
412 // We check explicitly for these multiplication ops in
413 // `specializeLinalgContractions()` to infer matmul-like ops.
414 return isa<arith::MulIOp, arith::MulFOp, complex::MulOp>(op);
415 })) {
416 foundCastForMatmulOutput = true;
417 return WalkResult::interrupt();
418 }
419
420 // Determine the cast type.
421 if (isa<arith::ExtUIOp, arith::UIToFPOp, arith::FPToUIOp>(castOp))
422 castTyFns.push_back(TypeFn::cast_unsigned);
423 else if (isa<arith::ExtSIOp, arith::SIToFPOp, arith::FPToSIOp>(castOp))
424 castTyFns.push_back(TypeFn::cast_signed);
425
426 return WalkResult::advance();
427 });
428
429 if (foundCastForMatmulOutput)
430 return std::nullopt;
431
432 if (!castTyFns.empty()) {
433 // If there were multiple different cast types found, then we can't express
434 // them using matmul-like ops. They only allow a single cast type for all
435 // inputs.
436 if (!llvm::all_equal(castTyFns))
437 return std::nullopt;
438 return castTyFns.front();
439 }
440
441 // Default to signed cast for matmul-like ops.
442 return TypeFn::cast_signed;
443}
444
445static FailureOr<LinalgOp> specializeLinalgMmt4D(RewriterBase &rewriter,
446 GenericOp genericOp,
447 std::optional<TypeFn> castTy,
448 ContractionDimensions &dims) {
449 // Should all be rank 4 and dim 6
450 auto indexingMaps = genericOp.getIndexingMapsArray();
451 if (llvm::any_of(indexingMaps, [](AffineMap m) {
452 return m.getResults().size() != 4 || m.getNumDims() != 6;
453 }))
454 return failure();
455
456 auto aOuter = matchOperandMap(indexingMaps[0], 0, dims.m[0], dims.k[0]);
457 auto aInner = matchOperandMap(indexingMaps[0], 2, dims.m[1], dims.k[1]);
458
459 auto bOuter = matchOperandMap(indexingMaps[1], 0, dims.k[0], dims.n[0]);
460 auto bInner = matchOperandMap(indexingMaps[1], 2, dims.k[1], dims.n[1]);
461
462 auto cOuter = matchOperandMap(indexingMaps[2], 0, dims.m[0], dims.n[0]);
463 auto cInner = matchOperandMap(indexingMaps[2], 2, dims.m[1], dims.n[1]);
464
465 if (llvm::is_contained({aOuter, bOuter, cOuter}, IndexMatchResult::Mismatch))
466 return failure();
467 if (llvm::is_contained({aInner, bInner, cInner}, IndexMatchResult::Mismatch))
468 return failure();
469
470 SmallVector<AffineMap> namedOpMaps = {indexingMaps[0], indexingMaps[1],
471 indexingMaps[2]};
472
473 return replaceWithMatmulVariant<Mmt4DOp>(rewriter, genericOp, castTy,
474 namedOpMaps);
475}
476
477static bool isSupportedContractionPair(Operation *first, Operation *second) {
478 if (isa<arith::MulFOp>(first) && isa<arith::AddFOp>(second))
479 return true;
480 if (isa<arith::MulIOp>(first) && isa<arith::AddIOp>(second))
481 return true;
482 if (isa<complex::MulOp>(first) && isa<complex::AddOp>(second))
483 return true;
484 if (isa<arith::AndIOp>(first) && isa<arith::OrIOp>(second) &&
485 first->getResult(0).getType().isInteger(1))
486 return true;
487
488 return false;
489}
490
491// Converts linalg.generic to named linalg.*matmul* where possible.
492static FailureOr<LinalgOp> specializeLinalgContractions(RewriterBase &rewriter,
493 GenericOp genericOp,
494 bool emitCategoryOp) {
495 if (genericOp.getNumDpsInputs() != 2 || genericOp.getNumDpsInits() != 1)
496 return failure();
497
498 // Early exit if not projected permutations.
499 auto mapRange = genericOp.getIndexingMapsArray();
500 if (llvm::any_of(mapRange,
501 [](AffineMap m) { return !m.isProjectedPermutation(); }))
502 return failure();
503
504 // Only contractions that can be represented by named linalg ops are
505 // eligible for specialization:
506 // - mul + add (floating-point, integer, complex)
507 // - and + or (bool)
508 if (!mlir::linalg::detail::isContractionBody(*genericOp.getBlock(),
509 isSupportedContractionPair))
510 return failure();
511
512 // Determine the cast type for the named matmul op, or bail out if casts
513 // cannot be represented by the named op.
514 std::optional<TypeFn> castTy = getCastTypeForMatmulLikeOp(genericOp);
515 if (!castTy)
516 return rewriter.notifyMatchFailure(
517 genericOp, "contains invalid cast ops for the named matmul op");
518
519 // In case of category op, wider range of variants is supported.
520 if (emitCategoryOp)
521 return replaceWithMatmulVariant<ContractOp>(
522 rewriter, genericOp, castTy, genericOp.getIndexingMapsArray());
523
524 // Further checks for named variants.
525 //
526 // Linalg generic contraction can be across multiple axis e.g.
527 // ```
528 // linalg.generic
529 // {indexing_maps = [affine_map<(m, n, k1, k2) -> (m, k1, k2)>,
530 // affine_map<(m, n, k1, k2) -> (k2, k1, n)>,
531 // affine_map<(m, n, k1, k2) -> (m, n)>],
532 // iterator_types = ["parallel", "parallel",
533 // "reduction", "reduction"]}
534 // ins(%A, %B : tensor<10x20x30xf32>, tensor<30x20x40xf32>)
535 // outs(%C : tensor<10x40xf32>) {
536 // ^bb0(%a: f32, %b: f32, %c: f32):
537 // %1 = arith.mulf %a, %b : f32
538 // %2 = arith.addf %c, %1 : f32
539 // linalg.yield %2 : f32
540 // } -> tensor<10x40xf32>
541 // ```
542 // In above contraction, there are two reduction dimensions {k1, k2}
543 // and although a valid linalg contraction, it is not a named-op
544 // matrix multiply kind. Therefore, reject multi-dim reduction.
545 auto res = inferContractionDims(genericOp);
546 if (!succeeded(res))
547 return failure();
548 auto dims = *res;
549 if (dims.m.size() == 2 && dims.n.size() == 2 && dims.k.size() == 2)
550 return specializeLinalgMmt4D(rewriter, genericOp, castTy, dims);
551 if (dims.m.size() != 1 || dims.n.size() != 1 || dims.k.size() != 1)
552 return failure();
553
554 // Check rank of operands
555 auto indexingMaps = genericOp.getIndexingMapsArray();
556 if (llvm::any_of(indexingMaps, [&dims](AffineMap m) {
557 return m.getResults().size() !=
558 dims.batch.size() + 2 /* any two of {m,n,k} */;
559 }))
560 return failure();
561
562 auto numOfBatchDims = dims.batch.size();
563 if (indexingMaps[0].getNumDims() != numOfBatchDims + 3)
564 return failure();
565
566 if (numOfBatchDims) {
567 // Each operand in a linalg generic contraction could express different
568 // permutations for its batch dimension. But for named op it must be
569 // identity since separate maps are not specified.
570 if (llvm::any_of(indexingMaps, [numOfBatchDims](AffineMap m) {
571 for (unsigned i = 0; i < numOfBatchDims; ++i) {
572 auto expr = m.getResults()[i];
573 if (expr.getKind() != AffineExprKind::DimId ||
574 cast<AffineDimExpr>(expr).getPosition() != i)
575 return true;
576 }
577 return false;
578 }))
579 return failure();
580 }
581
582 auto a =
583 matchOperandMap(indexingMaps[0], numOfBatchDims, dims.m[0], dims.k[0]);
584 auto b =
585 matchOperandMap(indexingMaps[1], numOfBatchDims, dims.k[0], dims.n[0]);
586 auto c =
587 matchOperandMap(indexingMaps[2], numOfBatchDims, dims.m[0], dims.n[0]);
588
589 if (llvm::is_contained({a, b, c}, IndexMatchResult::Mismatch))
590 return failure();
591
592 // Build indexing maps for the named op in its canonical dimension ordering
593 auto *ctx = genericOp.getContext();
594 unsigned numLoopDims = numOfBatchDims + 3;
595 unsigned mIdx = numOfBatchDims;
596 unsigned nIdx = mIdx + 1;
597 unsigned kIdx = mIdx + 2;
598
599 // TODO: add support for indexing_maps with broadcasts.
600 auto makeMap = [&](IndexMatchResult match, unsigned rowIdx, unsigned colIdx) {
601 SmallVector<unsigned> tensorDims;
602 for (unsigned i = 0; i < numOfBatchDims; ++i)
603 tensorDims.push_back(i);
604 if (match == IndexMatchResult::Transposed)
605 llvm::append_values(tensorDims, colIdx, rowIdx);
606 else
607 llvm::append_values(tensorDims, rowIdx, colIdx);
608 return AffineMap::getMultiDimMapWithTargets(numLoopDims, tensorDims, ctx);
609 };
610
611 auto mapA = makeMap(a, mIdx, kIdx);
612 auto mapB = makeMap(b, kIdx, nIdx);
613 auto mapC = makeMap(c, mIdx, nIdx);
614
615 SmallVector<AffineMap> namedOpMaps = {mapA, mapB, mapC};
616
617 // Codegen the different matmul variants.
618 if (numOfBatchDims) {
619 return replaceWithMatmulVariant<BatchMatmulOp>(rewriter, genericOp, castTy,
620 namedOpMaps);
621 }
622 return replaceWithMatmulVariant<MatmulOp>(rewriter, genericOp, castTy,
623 namedOpMaps);
624}
625
626/// Utility to specialize a `genericOp` with a convolution op of type `ConvOpTy`
627/// with `dilations` and `strides`.
628template <typename ConvOpTy>
629static FailureOr<LinalgOp>
630specializeToConvOp(RewriterBase &rewriter, GenericOp genericOp,
631 ArrayRef<int64_t> dilations, ArrayRef<int64_t> strides) {
632 SmallVector<Value> inputs = genericOp.getDpsInputs();
633 ValueRange outputs = genericOp.getDpsInits();
634 SmallVector<Type> resultTypes = genericOp.hasPureTensorSemantics()
635 ? TypeRange(ValueRange(outputs))
636 : TypeRange{};
637 LinalgOp namedOp;
638 // Ops with no dilations and no strides.
639 if constexpr (std::is_same_v<ConvOpTy, linalg::Conv1DOp> ||
640 std::is_same_v<ConvOpTy, linalg::Conv2DOp> ||
641 std::is_same_v<ConvOpTy, linalg::Conv3DOp>) {
642 namedOp = rewriter.replaceOpWithNewOp<ConvOpTy>(genericOp, resultTypes,
643 inputs, outputs);
644 } else {
645 Attribute stridesAttr = rewriter.getI64TensorAttr(strides);
646 Attribute dilationsAttr = rewriter.getI64TensorAttr(dilations);
647 namedOp = rewriter.replaceOpWithNewOp<ConvOpTy>(
648 genericOp, resultTypes, inputs, outputs, stridesAttr, dilationsAttr);
649 }
650 return namedOp;
651}
652
653/// Converts linalg.generic to named linalg.*conv/pooling* where possible.
654static FailureOr<LinalgOp> specializeLinalgConvolutions(RewriterBase &rewriter,
655 GenericOp genericOp) {
656#define CONV_OP_SPECIALIZER(ConvOpTy) \
657 if (std::optional<DilationsAndStrides> convParams = \
658 matchConvolutionOpOfType<ConvOpTy>(genericOp)) \
659 return specializeToConvOp<ConvOpTy>( \
660 rewriter, genericOp, convParams->dilations, convParams->strides); \
661 // -----------------------------
662 // Convolution ops.
663 // -----------------------------
664 CONV_OP_SPECIALIZER(linalg::Conv1DOp);
665 CONV_OP_SPECIALIZER(linalg::Conv1DNwcWcfOp);
666 CONV_OP_SPECIALIZER(linalg::Conv1DNcwFcwOp);
667 CONV_OP_SPECIALIZER(linalg::Conv2DOp);
668 CONV_OP_SPECIALIZER(linalg::Conv2DNhwcHwcfOp);
669 CONV_OP_SPECIALIZER(linalg::Conv2DNhwcHwcfQOp);
670 CONV_OP_SPECIALIZER(linalg::Conv2DNhwcFhwcOp);
671 CONV_OP_SPECIALIZER(linalg::Conv2DNhwcFhwcQOp);
672 CONV_OP_SPECIALIZER(linalg::Conv2DNchwFchwOp);
673 CONV_OP_SPECIALIZER(linalg::Conv2DNchwFchwQOp);
674 CONV_OP_SPECIALIZER(linalg::Conv2DNgchwFgchwOp);
675 CONV_OP_SPECIALIZER(linalg::Conv2DNgchwGfchwOp);
676 CONV_OP_SPECIALIZER(linalg::Conv2DNgchwGfchwQOp);
677 CONV_OP_SPECIALIZER(linalg::Conv2DNhwgcGfhwcOp);
678 CONV_OP_SPECIALIZER(linalg::Conv2DNhwgcGfhwcQOp);
679 CONV_OP_SPECIALIZER(linalg::Conv3DOp);
680 CONV_OP_SPECIALIZER(linalg::Conv3DNdhwcDhwcfOp);
681 CONV_OP_SPECIALIZER(linalg::Conv3DNdhwcDhwcfQOp);
682 CONV_OP_SPECIALIZER(linalg::Conv3DNcdhwFcdhwOp);
683 // -----------------------------
684 // Depthwise Convolution ops.
685 // -----------------------------
686 CONV_OP_SPECIALIZER(linalg::DepthwiseConv1DNcwCwOp);
687 CONV_OP_SPECIALIZER(linalg::DepthwiseConv1DNwcWcOp);
688 CONV_OP_SPECIALIZER(linalg::DepthwiseConv1DNwcWcmOp);
689 CONV_OP_SPECIALIZER(linalg::DepthwiseConv2DNchwChwOp);
690 CONV_OP_SPECIALIZER(linalg::DepthwiseConv2DNhwcHwcOp);
691 CONV_OP_SPECIALIZER(linalg::DepthwiseConv2DNhwcHwcQOp);
692 CONV_OP_SPECIALIZER(linalg::DepthwiseConv2DNhwcHwcmOp);
693 CONV_OP_SPECIALIZER(linalg::DepthwiseConv2DNhwcHwcmQOp);
694 CONV_OP_SPECIALIZER(linalg::DepthwiseConv3DNdhwcDhwcOp);
695 CONV_OP_SPECIALIZER(linalg::DepthwiseConv3DNcdhwCdhwOp);
696 CONV_OP_SPECIALIZER(linalg::DepthwiseConv3DNdhwcDhwcmOp);
697 // -----------------------------
698 // Pooling ops.
699 // -----------------------------
700 CONV_OP_SPECIALIZER(linalg::PoolingNhwcMaxOp);
701 CONV_OP_SPECIALIZER(linalg::PoolingNhwcMinOp);
702 CONV_OP_SPECIALIZER(linalg::PoolingNhwcSumOp);
703 CONV_OP_SPECIALIZER(linalg::PoolingNhwcMaxUnsignedOp);
704 CONV_OP_SPECIALIZER(linalg::PoolingNhwcMinUnsignedOp);
705 CONV_OP_SPECIALIZER(linalg::PoolingNchwSumOp);
706 CONV_OP_SPECIALIZER(linalg::PoolingNchwMaxOp);
707 CONV_OP_SPECIALIZER(linalg::PoolingNwcSumOp);
708 CONV_OP_SPECIALIZER(linalg::PoolingNcwSumOp);
709 CONV_OP_SPECIALIZER(linalg::PoolingNwcMaxOp);
710 CONV_OP_SPECIALIZER(linalg::PoolingNwcMaxUnsignedOp);
711 CONV_OP_SPECIALIZER(linalg::PoolingNcwMaxOp);
712 CONV_OP_SPECIALIZER(linalg::PoolingNwcMinOp);
713 CONV_OP_SPECIALIZER(linalg::PoolingNwcMinUnsignedOp);
714 CONV_OP_SPECIALIZER(linalg::PoolingNdhwcSumOp);
715 CONV_OP_SPECIALIZER(linalg::PoolingNdhwcMaxOp);
716 CONV_OP_SPECIALIZER(linalg::PoolingNdhwcMinOp);
717#undef CONV_OP_SPECIALIZER
718 return failure();
719}
720
721} // namespace
722
723//===----------------------------------------------------------------------===//
724// Categorize linalg generic to named op where possible.
725//===----------------------------------------------------------------------===//
727 RewriterBase &rewriter, GenericOp genericOp,
729 // Elementwise - e.g. exp, add
730 if (isaElemwiseSingleUnaryOpInterface(genericOp, options.emitCategoryOps) ||
731 isaElemwiseSingleBinaryOpInterface(genericOp, options.emitCategoryOps)) {
732 return specializeLinalgElementwise(rewriter, genericOp,
733 options.emitCategoryOps);
734 }
735
736 // Contraction - e.g. matmul
737 if (isaContractionOpInterface(genericOp)) {
738 return specializeLinalgContractions(rewriter, genericOp,
739 options.emitCategoryOps);
740 }
741
742 // Early exit in case of category specialization.
743 // TODO: Remove when matches for other ops account for both named and
744 // category.
745 if (options.emitCategoryOps)
746 return rewriter.notifyMatchFailure(
747 genericOp, "no matching category op specialization");
748
749 // Copy
750 if (isaCopyOpInterface(genericOp)) {
751 LinalgOp namedOp = rewriter.replaceOpWithNewOp<CopyOp>(
752 genericOp, genericOp.getDpsInputs()[0], genericOp.getDpsInits()[0]);
753 return namedOp;
754 }
755
756 // Fill
757 if (std::optional<Value> fillValue = isaFillOpInterface(genericOp)) {
758 // Always use the detected fill value, regardless of pattern
759 LinalgOp namedOp = rewriter.replaceOpWithNewOp<FillOp>(
760 genericOp, *fillValue, genericOp.getDpsInits()[0]);
761 return namedOp;
762 }
763
764 // Broadcast
765 std::optional<SmallVector<int64_t>> equivalentToBroadcast =
766 isaBroadcastOpInterface(genericOp);
767 if (equivalentToBroadcast) {
768 auto dims = *equivalentToBroadcast;
769 LinalgOp namedOp = rewriter.replaceOpWithNewOp<BroadcastOp>(
770 genericOp, genericOp.getDpsInputs()[0], genericOp.getDpsInits()[0],
771 dims);
772 return namedOp;
773 }
774
775 // Transpose
776 std::optional<SmallVector<int64_t>> equivalentToTranspose =
777 isaTransposeOpInterface(genericOp);
778 if (equivalentToTranspose) {
779 auto permutation = *equivalentToTranspose;
780 LinalgOp namedOp = rewriter.replaceOpWithNewOp<TransposeOp>(
781 genericOp, genericOp.getDpsInputs()[0], genericOp.getDpsInits()[0],
782 permutation);
783 return namedOp;
784 }
785
786 // Convolution - e.g. *conv/pooling*
787 if (isaConvolutionOpInterface(genericOp))
788 return specializeLinalgConvolutions(rewriter, genericOp);
789
790 return rewriter.notifyMatchFailure(genericOp,
791 "no matching named op specialization");
792}
793
794namespace {
795struct LinalgSpecializeGenericOpsPass
796 : public impl::LinalgSpecializeGenericOpsPassBase<
797 LinalgSpecializeGenericOpsPass> {
798
799 using impl::LinalgSpecializeGenericOpsPassBase<
800 LinalgSpecializeGenericOpsPass>::LinalgSpecializeGenericOpsPassBase;
801 void runOnOperation() override;
802};
803} // namespace
804
805void LinalgSpecializeGenericOpsPass::runOnOperation() {
806 RewritePatternSet patterns(&getContext());
809
810 if (failed(applyPatternsGreedily(getOperation(), std::move(patterns))))
811 signalPassFailure();
812}
813
b
Return true if permutation is a valid permutation of the outer_dims_perm (case OuterOrInnerPerm::Oute...
b getContext())
static llvm::ManagedStatic< PassManagerOptions > options
static bool findIndexOfScalarOperand(GenericOp genericOp, int &index)
#define CONV_OP_SPECIALIZER(ConvOpTy)
static bool areBinOpsSwapped(GenericOp genericOp)
static FailureOr< LinalgOp > specializeLinalgElementwise(RewriterBase &rewriter, GenericOp genericOp, bool emitCategoryOp)
A multi-dimensional affine map Affine map's are immutable like Type's, and they are uniqued.
Definition AffineMap.h:46
static AffineMap get(MLIRContext *context)
Returns a zero result affine map with no dimensions or symbols: () -> ().
bool isProjectedPermutation(bool allowZeroInResults=false) const
Returns true if the AffineMap represents a subset (i.e.
unsigned getNumDims() const
ArrayRef< AffineExpr > getResults() const
static AffineMap getMultiDimMapWithTargets(unsigned numDims, ArrayRef< unsigned > targets, MLIRContext *context)
Returns an affine map with numDims input dimensions and results specified by targets.
Attributes are known-constant values of operations.
Definition Attributes.h:25
Block represents an ordered list of Operations.
Definition Block.h:33
BlockArgument getArgument(unsigned i)
Definition Block.h:153
Operation & front()
Definition Block.h:177
DenseIntElementsAttr getI64TensorAttr(ArrayRef< int64_t > values)
Definition Builders.cpp:194
ArrayAttr getArrayAttr(ArrayRef< Attribute > value)
Definition Builders.cpp:275
MLIRContext * getContext() const
Definition Builders.h:56
NamedAttribute getNamedAttr(StringRef name, Attribute val)
Definition Builders.cpp:102
ArrayAttr getAffineMapArrayAttr(ArrayRef< AffineMap > values)
Definition Builders.cpp:327
IRValueT get() const
Return the current value being used by this operand.
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
Value getOperand(unsigned idx)
Definition Operation.h:375
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:432
unsigned getNumOperands()
Definition Operation.h:371
operand_range getOperands()
Returns an iterator on the underlying Value's.
Definition Operation.h:403
OpOperand & getOpOperand(unsigned idx)
Definition Operation.h:413
MLIRContext * getContext() const
RewritePatternSet & add(ConstructorArg &&arg, ConstructorArgs &&...args)
Add an instance of each of the pattern types 'Ts' to the pattern list with the given arguments.
This class coordinates the application of a rewrite on a set of IR, providing a way for clients to tr...
virtual void replaceOp(Operation *op, ValueRange newValues)
Replace the results of the given (original) operation with the specified list of values (replacements...
std::enable_if_t<!std::is_convertible< CallbackT, Twine >::value, LogicalResult > notifyMatchFailure(Location loc, CallbackT &&reasonCallback)
Used to notify the listener that the IR failed to be rewritten because of a match failure,...
OpTy replaceOpWithNewOp(Operation *op, Args &&...args)
Replace the results of the given (original) op with a new op that is created without verification (re...
This class provides an abstraction over the various different ranges of value types.
Definition TypeRange.h:40
bool isInteger() const
Return true if this is an integer type (with the specified width).
Definition Types.cpp:58
This class provides an abstraction over the different types of ranges over Values.
Definition ValueRange.h:389
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
Type getType() const
Return the type of this value.
Definition Value.h:105
static WalkResult advance()
Definition WalkResult.h:47
static WalkResult interrupt()
Definition WalkResult.h:46
bool isContractionBody(Block &block, function_ref< bool(Operation *, Operation *)> isaPair, llvm::raw_ostream &errs=mlir::thread_safe_nulls())
Returns true if the block contains a contraction of the following form:
std::optional< SmallVector< int64_t > > isaTransposeOpInterface(GenericOp genericOp)
Checks whether genericOp is semantically equivalent to a linalg.transpose.
bool isaCopyOpInterface(LinalgOp linalgOp)
Checks whether linalgOp is semantically equivalent to a linalg.copyOp.
bool isaElemwiseSingleBinaryOpInterface(GenericOp genericOp, bool allowNonIdentityMaps=false)
Checks whether genericOp is semantically equivalent to a single linalg elementwise binary op e....
void populateDecomposeProjectedPermutationPatterns(RewritePatternSet &patterns)
Add patterns to make explicit broadcasts and transforms in the input operands of a genericOp.
FailureOr< LinalgOp > specializeGenericOp(RewriterBase &rewriter, GenericOp genericOp, const GenericOpSpecializationOptions &options={})
Replace the given GenericOp with a namedOp or categoryOp.
bool isaConvolutionOpInterface(LinalgOp linalgOp, bool allowEmptyConvolvedDims=false)
Checks whether linalgOp conforms to ConvolutionOpInterface.
std::optional< SmallVector< int64_t > > isaBroadcastOpInterface(LinalgOp linalgOp)
Checks whether linalgOp is semantically equivalent to a broadcast operation.
FailureOr< ContractionDimensions > inferContractionDims(LinalgOp linalgOp)
Find at least 2 parallel (m and n) and 1 reduction (k) dimension candidates that form a matmul subcom...
bool isaContractionOpInterface(LinalgOp linalgOp)
Checks whether linalgOp conforms to ContractionOpInterface.
bool isaElemwiseSingleUnaryOpInterface(GenericOp genericOp, bool allowNonIdentityMaps=false)
Checks whether a given genericOp is semantically equivalent to a single linalg elementwise unary op,...
std::optional< Value > isaFillOpInterface(GenericOp genericOp)
Checks whether genericOp is semantically equivalent to a linalg.fill.
void populateLinalgGenericOpsSpecializationPatterns(RewritePatternSet &patterns, const GenericOpSpecializationOptions &options={})
Populates patterns with patterns to convert linalg.generic ops to named or category ops where possibl...
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:732
Include the generated interface declarations.
LogicalResult applyPatternsGreedily(Region &region, const FrozenRewritePatternSet &patterns, GreedyRewriteConfig config=GreedyRewriteConfig(), bool *changed=nullptr)
Rewrite ops in the given region, which must be isolated from above, by repeatedly applying the highes...
@ DimId
Dimensional identifier.
Definition AffineExpr.h:59
llvm::SetVector< T, Vector, Set, N > SetVector
Definition LLVM.h:125
void getForwardSlice(Operation *op, SetVector< Operation * > *forwardSlice, const ForwardSliceOptions &options={})
Fills forwardSlice with the computed forward slice (i.e.
Positions of a Linalg op loops that correspond to different kinds of a contraction dimension.
SmallVector< unsigned, 2 > batch