| # Chapter 4: Matching Payload with Transform Operations | |
| **Check the continuously-tested version of MLIR files under | |
| [mlir/test/Examples/transform/Ch4](https://github.com/llvm/llvm-project/tree/main/mlir/test/Examples/transform/Ch4).** | |
| Up until now, we were applying transform dialect scripts under the assumption | |
| that specific payload operations are identified by the caller when the transform | |
| dialect interpreter is invoked. This may be seen as contrary to the idea of | |
| driving transformations from a dialect since the transformation targets must be | |
| identified through mechanisms external to the transform dialect interpreter, for | |
| example, when invoking the interpreter programmatically in C++ or through pass | |
| arguments as seen in previous chapters. It also adds practical overhead due to | |
| increased interaction with the interpreter in C++, and cognitive overhead of | |
| manipulating two interfaces at once. To remedy this, Transform dialect proposes | |
| a subset of operations for _matching_ payload operations that need to be | |
| transformed. | |
| _Match_ operations are simply transform operations with some additional | |
| guarantees. In particular, they are not expected to modify the payload IR and | |
| are expected to fail if their operands (typically payload operation handles) are | |
| not associated with payload IR objects having desired properties, such as | |
| operation names or kinds of arguments. Using simple combinator operations, it | |
| becomes possible to set up a higher-level match and rewrite infrastructure | |
| directly within the transform dialect. | |
| ## Simple match | |
| Let us reconsider the “fully connected layer” example from [Chapter | |
| 1](Ch1.md/#chaining-transformations-with-handles), reproduced below for | |
| convenience. | |
| ```mlir | |
| // Original function to optimize. | |
| func.func @fc_relu(%lhs: tensor<512x512xf32>, %rhs: tensor<512x512xf32>, | |
| %bias: tensor<512x512xf32>, %output: tensor<512x512xf32>) | |
| -> tensor<512x512xf32> { | |
| // Matrix-matrix multiplication. | |
| %matmul = linalg.matmul | |
| ins(%lhs, %rhs: tensor<512x512xf32>, tensor<512x512xf32>) | |
| outs(%output: tensor<512x512xf32>) -> tensor<512x512xf32> | |
| // Elementwise addition. | |
| %biased = linalg.elementwise <add> | |
| ins(%matmul, %bias : tensor<512x512xf32>, tensor<512x512xf32>) | |
| outs(%output : tensor<512x512xf32>) -> tensor<512x512xf32> | |
| // Elementwise max with 0 (ReLU). | |
| %c0f = arith.constant 0.0 : f32 | |
| %relued = linalg.elementwise <max_signed> | |
| ins(%biased, %c0f : tensor<512x512xf32>, f32) | |
| outs(%output : tensor<512x512xf32>) -> tensor<512x512xf32> | |
| func.return %relued : tensor<512x512xf32> | |
| } | |
| ``` | |
| In Chapter 1, we were calling the test transform interpreter pass with | |
| additional arguments, `bind-first-extra-to-ops=linalg.matmul | |
| bind-second-extra-to-ops=linalg.elementwise`, to provide initial | |
| associations for operation handles. Instead, we can use match operations to | |
| discover relevant operations in the payload IR. Match operations can be combined | |
| with “regular” transform operations using, e.g., the | |
| `transform.collect_matching` combinator operation that leverages the concept of | |
| named sequences to organize matchers. | |
| ```mlir | |
| // The module containing named sequences must have an attribute allowing them | |
| // to enable verification. | |
| module @transforms attributes { transform.with_named_sequence } { | |
| // Entry point. This takes as the only argument the root operation (typically | |
| // pass root) given to the transform interpreter. | |
| transform.named_sequence @__transform_main( | |
| %root: !transform.any_op {transform.readonly}) { | |
| // Collect operations that match the criteria specified in named sequence. | |
| // If the named sequence fails with a silenceable failure, silences it (the | |
| // message is forwarded to the debug stream). If the named sequence | |
| // succeeds, appends its results to the results of this operation. | |
| %elemwise = transform.collect_matching @match_elemwise in %root | |
| : (!transform.any_op) -> !transform.any_op | |
| %matmul = transform.collect_matching @match_matmul in %root | |
| : (!transform.any_op) -> !transform.any_op | |
| transform.include @print_elemwise failures(propagate) (%elemwise) | |
| : (!transform.any_op) -> () | |
| transform.include @print_matmul failures(propagate) (%matmul) | |
| : (!transform.any_op) -> () | |
| transform.yield | |
| } | |
| // This is a matcher sequence. It is given an operation to match and the | |
| // match is considered successful unless any nested operation produces a | |
| // failure. The values yielded by this operation will be forwarded to the | |
| // rewriter sequence on success. | |
| transform.named_sequence @match_elemwise( | |
| %entry: !transform.any_op {transform.readonly}) -> !transform.any_op { | |
| transform.match.operation_name %entry ["linalg.elementwise"] | |
| : !transform.any_op | |
| transform.yield %entry : !transform.any_op | |
| } | |
| transform.named_sequence @match_matmul( | |
| %entry: !transform.any_op {transform.readonly}) -> !transform.any_op { | |
| transform.match.operation_name %entry ["linalg.matmul"] : !transform.any_op | |
| transform.yield %entry : !transform.any_op | |
| } | |
| // This is a rewriter sequence. | |
| transform.named_sequence @print_elemwise( | |
| %elemwise_binary: !transform.any_op {transform.readonly}) { | |
| transform.debug.emit_remark_at | |
| %elemwise_binary, "elementwise binary" : !transform.any_op | |
| transform.yield | |
| } | |
| transform.named_sequence @print_matmul( | |
| %matmul: !transform.any_op {transform.readonly}) { | |
| transform.debug.emit_remark_at %matmul, "matmul" : !transform.any_op | |
| transform.yield | |
| } | |
| } | |
| ``` | |
| This script can be executed using the non-test interpreter pass running on the | |
| root operation of the translation unit without additional flags: `mlir-opt | |
| --transform-interpreter`. It will emit corresponding remarks at | |
| `linalg.elementwise` and `linalg.matmul` operations. In debug builds, the | |
| infrastructure provides a convenient method to understand the matching process | |
| by passing `-debug-only=transform-matcher` to `mlir-opt` or a derived tool. It | |
| will print the silenceable failure messages produced by the match operations | |
| into the debug stream, for example: | |
| ``` | |
| <...> | |
| [transform-matcher] matching %0 = linalg.matmul ins(%arg0, %arg1 : tensor<512x512xf32>, tensor<512x512xf32>) outs(%arg3 : tensor<512x512xf32>) -> tensor<512x512xf32> @0x5622eee08410 | |
| [transform-matcher] matcher match_elemwise failed: wrong operation name | |
| <...> | |
| ``` | |
| This is now sufficient to run the rest of the transform script from Chapter 1, | |
| substituting `%arg1` with `%matmul` and `%arg2` with `%elemwise`. | |
| ## Matching Chains of Operations | |
| The matcher above remains naive as it matches _all_ operations of the certain | |
| kind under the payload root. These operations may or may not be related, and | |
| may, for example, belong to different functions. Even if they are in a single | |
| function, if there are multiple groups of such operations, we wouldn’t be able | |
| to differentiate them with this approach. In reality, we want to match a | |
| specific group of operations where a `matmul` operation produces a result that | |
| is used by an elementwise operation, which in turn feeds another elementwise | |
| operation in a similar way. | |
| This can be achieved using the following matcher sequence. | |
| ```mlir | |
| // This is also a matcher sequence. It is similarly given an operation to | |
| // match and nested operations must succeed in order for a match to be deemed | |
| // successful. It starts matching from the last operation in the use-def chain | |
| // and goes back because each operand (use) has exactly one definition. | |
| transform.named_sequence @match_matmul_elemwise( | |
| %last: !transform.any_op {transform.readonly}) | |
| -> (!transform.any_op, !transform.any_op, !transform.any_op) { | |
| // The last operation must be an elementwise binary. | |
| transform.match.operation_name %last ["linalg.elementwise"] | |
| : !transform.any_op | |
| // Its first operand must be defined by another operation, to which we | |
| // will get a handle here. We are guaranteed that the first operand exists | |
| // because we know the operation is binary, but even in absence of such a | |
| // guarantee, this operation would have produced a silenceable failure when | |
| // `%last` does not have enough operands. | |
| %middle = transform.get_producer_of_operand %last[0] | |
| : (!transform.any_op) -> !transform.any_op | |
| // The defining operation must itself be an elementwise binary. | |
| transform.match.operation_name %middle ["linalg.elementwise"] | |
| : !transform.any_op | |
| // And the first operand of that operation must be defined by yet another | |
| // operation. | |
| %matmul = transform.get_producer_of_operand %middle[0] | |
| : (!transform.any_op) -> !transform.any_op | |
| // And that operation is a matmul. | |
| transform.match.operation_name %matmul ["linalg.matmul"] : !transform.any_op | |
| // We will yield the handles to the matmul and the two elementwise | |
| // operations separately. | |
| transform.yield %matmul, %middle, %last | |
| : !transform.any_op, !transform.any_op, !transform.any_op | |
| } | |
| ``` | |
| This matcher is applicable in presence of other `elemwise` and `matmul` | |
| operations and will return the triple of _related_ operations rather than | |
| operations in the order in which they are found. It can be exercised similarly | |
| to the previous incarnation, as follows. | |
| ```mlir | |
| // Alternative entry point. | |
| transform.named_sequence @__transform_main( | |
| %root: !transform.any_op {transform.readonly}) { | |
| // Collect groups of operations that match the criteria specified in the | |
| // named sequence. | |
| %matmul, %el1, %el2 = transform.collect_matching @match_matmul_elemwise in %root | |
| : (!transform.any_op) -> (!transform.any_op, !transform.any_op, !transform.any_op) | |
| %elemwise = transform.merge_handles %el1, %el2 : !transform.any_op | |
| transform.include @print_elemwise failures(propagate) (%elemwise) | |
| : (!transform.any_op) -> () | |
| transform.include @print_matmul failures(propagate) (%matmul) | |
| : (!transform.any_op) -> () | |
| transform.yield | |
| } | |
| ``` | |
| ## Defining Match Operations | |
| The matcher of a chain of operations is correct in presence of other operations, | |
| but is still insufficiently robust for many cases of interest. In particular, | |
| using `transform.get_producer_of_operand %last[0]` requires that the _first_ | |
| operand of elementwise operations is produced by another operation. The same | |
| transformation strategy may however apply regardless of the operand position: | |
| many binary operations are associative. Let us use this opportunity to introduce | |
| a new match operation. Specifically, we would like this operation to succeed if | |
| _any_ of the operands satisfies certain conditions that can be expressed as | |
| other match operations. We also want it to return some of the state and the | |
| position of the matched operand in the operand list. | |
| Match operations are defined similarly to other transform operations, with the | |
| only difference of additionally implementing the `MatchOpInterface`. Note that | |
| this interface has _no additional methods_ (though it may add some eventually) | |
| and is only used as a verification contract that the operation is intended for | |
| matching and will not attempt to transform the payload. The minimal definition | |
| of our operation is as follows. | |
| ```tablegen | |
| // Define the new operation. By convention, prefix its name with `match` | |
| // followed by the name of the dialect extension. | |
| def HasOperandSatisfyingOp : TransformDialectOp<"match.my.has_operand_satisfying", | |
| [DeclareOpInterfaceMethods<MemoryEffectsOpInterface>, | |
| DeclareOpInterfaceMethods<TransformOpInterface>, | |
| // Indicate that the operation implements MatchOpInterface in addition to | |
| // the TransformOpInterface. This interface is only used as a tag at this | |
| // point and has no methods that are mandatory to implement. | |
| MatchOpInterface, | |
| SingleBlockImplicitTerminator<"::mlir::transform::YieldOp">]> { | |
| let summary = "Succeed if any of the operands matches all nested criteria"; | |
| let arguments = (ins TransformHandleTypeInterface:$op); | |
| let results = (outs TransformParamTypeInterface:$position, | |
| Variadic<Transform_AnyHandleOrParamType>:$results); | |
| // Match operations can be arbitrarily complex, e.g., containing regions. | |
| let regions = (region SizedRegion<1>:$body); | |
| let hasVerifier = 1; | |
| let assemblyFormat = [{ | |
| $op `:` functional-type($op, results) attr-dict-with-keyword $body | |
| }]; | |
| } | |
| ``` | |
| It takes as argument the handle associated with the payload operations whose | |
| operands it will match, has an associated single-block region containing the | |
| match criteria, and returns the position of the matched operand as well as any | |
| other transform value yielded from the body on the successful match. | |
| The matching logic is implemented in the `apply` method of the | |
| `TransformOpInterface` and is easily composable with other transform operations. | |
| All facilities for managing the interpreter state and recursively entering the | |
| blocks are available in the same way as they are for “regular” transform | |
| operations. Match operations are expected to return a silenceable failure to | |
| indicate failure to match, and to immediately propagate definite failures. If | |
| they have nested operations, they are expected to handle and, in most cases, | |
| silence the silenceable failures produced when applying those operations. For | |
| our operation, the matching is essentially a loop iterating over all operands of | |
| the (single) payload operation and applying nested transform ops until they all | |
| succeed for one of the operands. | |
| ```cpp | |
| // Matcher ops implement `apply` similarly to other transform ops. They are not | |
| // expected to modify payload, but use the tri-state result to signal failure or | |
| // success to match, as well as potential irrecoverable errors. | |
| mlir::DiagnosedSilenceableFailure | |
| mlir::transform::HasOperandSatisfyingOp::apply( | |
| mlir::transform::TransformRewriter &rewriter, | |
| mlir::transform::TransformResults &results, | |
| mlir::transform::TransformState &state) { | |
| // For simplicity, only handle a single payload op. Actual implementations | |
| // can use `SingleOpMatcher` trait to simplify implementation and document | |
| // this expectation. | |
| auto payloadOps = state.getPayloadOps(getOp()); | |
| if (!llvm::hasSingleElement(payloadOps)) | |
| return emitSilenceableError() << "expected single payload"; | |
| // Iterate over all operands of the payload op to see if they can be matched | |
| // using the body of this op. | |
| Operation *payload = *payloadOps.begin(); | |
| for (OpOperand &operand : payload->getOpOperands()) { | |
| // Create a scope for transform values defined in the body. This corresponds | |
| // to the syntactic scope of the region attached to this op. Any values | |
| // associated with payloads from now on will be automatically dissociated | |
| // when this object is destroyed, i.e. at the end of the iteration. | |
| // Associate the block argument handle with the operand. | |
| auto matchScope = state.make_region_scope(getBody()); | |
| if (failed(state.mapBlockArgument(getBody().getArgument(0), | |
| {operand.get()}))) { | |
| return DiagnosedSilenceableFailure::definiteFailure(); | |
| } | |
| // Iterate over all nested matchers with the current mapping and see if they | |
| // succeed. | |
| bool matchSucceeded = true; | |
| for (Operation &matcher : getBody().front().without_terminator()) { | |
| // Matcher ops are applied similarly to any other transform op. | |
| DiagnosedSilenceableFailure diag = | |
| state.applyTransform(cast<TransformOpInterface>(matcher)); | |
| // Definite failures are immediately propagated as they are irrecoverable. | |
| if (diag.isDefiniteFailure()) | |
| return diag; | |
| // On success, keep checking the remaining conditions. | |
| if (diag.succeeded()) | |
| continue; | |
| // Report failure-to-match for debugging purposes and stop matching this | |
| // operand. | |
| assert(diag.isSilenceableFailure()); | |
| DEBUG_MATCHER(DBGS_MATCHER() | |
| << "failed to match operand #" << operand.getOperandNumber() | |
| << ": " << diag.getMessage()); | |
| (void)diag.silence(); | |
| matchSucceeded = false; | |
| break; | |
| } | |
| // If failed to match this operand, try other operands. | |
| if (!matchSucceeded) | |
| continue; | |
| // If we reached this point, the matching succeeded for the current operand. | |
| // Remap the values associated with terminator operands to be associated | |
| // with op results, and also map the parameter result to the operand's | |
| // position. Note that it is safe to do here despite the end of the scope | |
| // as `results` are integrated into `state` by the interpreter after `apply` | |
| // returns rather than immediately. | |
| SmallVector<SmallVector<MappedValue>> yieldedMappings; | |
| transform::detail::prepareValueMappings( | |
| yieldedMappings, getBody().front().getTerminator()->getOperands(), | |
| state); | |
| results.setParams(cast<OpResult>(getPosition()), | |
| {rewriter.getI32IntegerAttr(operand.getOperandNumber())}); | |
| for (auto &&[result, mapping] : llvm::zip(getResults(), yieldedMappings)) | |
| results.setMappedValues(result, mapping); | |
| return DiagnosedSilenceableFailure::success(); | |
| } | |
| // If we reached this point, none of the operands succeeded the match. | |
| return emitSilenceableError() | |
| << "none of the operands satisfied the conditions"; | |
| } | |
| ``` | |
| By convention, operations implementing `MatchOpInterface` must not modify | |
| payload IR and must therefore specify that they only read operand handles and | |
| payload as their effects. | |
| ```cpp | |
| void transform::CollectMatchingOp::getEffects( | |
| SmallVectorImpl<MemoryEffects::EffectInstance> &effects) { | |
| onlyReadsHandle(getRoot(), effects); | |
| producesHandle(getResults(), effects); | |
| onlyReadsPayload(effects); | |
| } | |
| ``` | |
| This operation can now be included in a transform dialect extension, loaded and | |
| used in our matcher. Specifically, we will use it to indicate that either of the | |
| operands of the “max” elementwise operation in our example can be produced by | |
| the previous elementwise operation. The previous operation will still require | |
| the matmul to produce the first operand for simplicity. The updated matcher | |
| sequence looks as follows. | |
| ```mlir | |
| transform.named_sequence @match_matmul_elemwise( | |
| %last: !transform.any_op {transform.readonly}) | |
| -> (!transform.any_op, !transform.any_op, !transform.any_op, | |
| !transform.param<i32>) { | |
| // The last operation must be an elementwise binary. | |
| transform.match.operation_name %last ["linalg.elementwise"] | |
| : !transform.any_op | |
| // One of its operands must be defined by another operation, to which we | |
| // will get a handle here. This is achieved thanks to a newly defined | |
| // operation that tries to match operands one by one using the match | |
| // operations nested in its region. | |
| %pos, %middle = transform.match.my.has_operand_satisfying %last | |
| : (!transform.any_op) -> (!transform.param<i32>, !transform.any_op) { | |
| ^bb0(%operand: !transform.any_value): | |
| // The operand must be defined by an operation. | |
| %def = transform.get_defining_op %operand | |
| : (!transform.any_value) -> !transform.any_op | |
| // The defining operation must itself be an elementwise binary. | |
| transform.match.operation_name %def ["linalg.elementwise"] | |
| : !transform.any_op | |
| transform.yield %def : !transform.any_op | |
| } | |
| // And the first operand of that operation must be defined by yet another | |
| // operation. | |
| %matmul = transform.get_producer_of_operand %middle[0] | |
| : (!transform.any_op) -> !transform.any_op | |
| // And that operation is a matmul. | |
| transform.match.operation_name %matmul ["linalg.matmul"] : !transform.any_op | |
| // We will yield the handles to the matmul and the two elementwise | |
| // operations separately. | |
| transform.yield %matmul, %middle, %last, %pos | |
| : !transform.any_op, !transform.any_op, !transform.any_op, | |
| !transform.param<i32> | |
| } | |
| ``` | |
| This achieves the desired effect and matches both `max(add(matmul(...), bias), | |
| 0)` and `max(0, add(matmul(...), bias))` in the same values. The `%pos` value is | |
| a transform dialect _parameter_, which is used to store lists of entities known | |
| to be constant throughout the transform application. Most often, parameters are | |
| numeric values, but they can generally be any MLIR attributes. | |
| In order to demonstrate that groups of operations are matched independently of | |
| each other, let us use the `transform.foreach_match` operation that allows one | |
| to implement a simple high-level pattern rewriting approach within the transform | |
| dialect (for advanced or lower-level pattern rewriting, consider PDL(L) or C++ | |
| rewriting APIs). It maps a matcher named sequence to an action named sequence, | |
| and the latter gets invoked whenever the former succeeds. | |
| ```mlir | |
| // Traverses the payload IR associated with the operand handle, invoking | |
| // @match_matmul_elemwise on each of the operations. If the named sequence | |
| // succeeds, i.e., if none of the nested match (transform) operations | |
| // produced a silenceable failure, invokes @print_matmul_elemwise and | |
| // forwards the values yielded as arguments of the new invocation. If the | |
| // named sequence fails with a silenceable failure, silences it (the message | |
| // is forwarded to the debug stream). Definite failures are propagated | |
| // immediately and unconditionally, as usual. | |
| transform.foreach_match in %root | |
| @match_matmul_elemwise -> @print_matmul_elemwise | |
| : (!transform.any_op) -> !transform.any_op | |
| ``` | |
| The `@print_matmul_elemwise` named sequence, available in `multiple.mlir`, will | |
| use the parameter with the position of the operand to differentiate the two | |
| groups. | |
| ## Matchers for Inferred Features | |
| The matcher sequences described above, although useful to drive transformations | |
| from within the transform dialect interpreter, are rather basic since they | |
| mostly rely on operation names and use-def chains. Alternative implementations | |
| using APIs or various declarative rewrite rules are barely less expressive and | |
| sometimes more concise. The real power of transform dialect matcher ops lies in | |
| the possibility to define matchers of _inferred properties_ of payloads, i.e., | |
| properties that are not directly accessible as an attribute of an operation or | |
| any straightforward relation between IR components. | |
| The utility of such matchers can be easily demonstrated by slightly modifying | |
| our original example. If matrix multiplication is expressed as a special case of | |
| tensor contraction using `linalg.generic` instead of `linalg.matmul`, the | |
| operation name-based matcher no longer applies. Yet such a representation is | |
| very common and can appear both in the original input and during the course of | |
| transformation, e.g., where a higher-dimensional contraction is decomposed into | |
| loops around a matrix multiplication. | |
| In order to be a (potentially transposed) matrix multiplication, the | |
| `linalg.generic` operation must have the following features: | |
| * Total rank of 3. | |
| * Two inputs accessed as projected permutation of iteration dimensions. | |
| * One output accessed as projected permutation of iteration dimensions. | |
| * Iteration dimensions can be subdivided into LHS parallel, RHS parallel and reduction dimensions. | |
| * The body block consists of a multiplication and an addition. | |
| Most of these features can be derived from the properties of the operation, | |
| e.g., the total rank corresponds to the number of entries in the `iterators` | |
| attribute, but almost none of them are immediately accessible in the IR or in | |
| any declarative form, which is usually limited to checking the presence or the | |
| exact match of an attribute or a type. The transform dialect allows these | |
| features to be implemented in the `apply` method of a matcher op and reused | |
| across multiple matching cases. For structured linear algebra payload | |
| operations, many such match operations are readily available in the `structured` | |
| extension. They are sufficient to implement a matrix multiplication matcher | |
| using the features listed above almost verbatim. | |
| ```mlir | |
| transform.named_sequence @match_generic_matmul( | |
| %candidate: !transform.any_op {transform.readonly}) -> !transform.any_op { | |
| // Match a structured linear algebra operation. | |
| transform.match.structured %candidate : !transform.any_op { | |
| ^bb0(%c: !transform.any_op): | |
| // With a rank equal to 3. | |
| %rank = transform.match.structured.rank %c | |
| : (!transform.any_op) -> !transform.param<i64> | |
| %c3 = transform.param.constant 3 : i64 -> !transform.param<i64> | |
| transform.match.param.cmpi eq %rank, %c3 : !transform.param<i64> | |
| // With 2 inputs. | |
| %n_ins = transform.match.structured.num_inputs %c | |
| : (!transform.any_op) -> !transform.param<i64> | |
| %c2 = transform.param.constant 2 : i64 -> !transform.param<i64> | |
| transform.match.param.cmpi eq %n_ins, %c2 : !transform.param<i64> | |
| // With 1 output (note that structured ops in destination passing style | |
| // has as many inits as outputs). | |
| %n_inits = transform.match.structured.num_inits %c | |
| : (!transform.any_op) -> !transform.param<i64> | |
| %c1 = transform.param.constant 1 : i64 -> !transform.param<i64> | |
| transform.match.param.cmpi eq %n_inits, %c1 : !transform.param<i64> | |
| // All inputs and inits are accessed with a projected permutation. | |
| transform.match.structured.input %c[all] {projected_permutation} | |
| : !transform.any_op | |
| transform.match.structured.init %c[0] {projected_permutation} | |
| : !transform.any_op | |
| // The body is a mulf/addf contraction with appropriate dimensions. | |
| transform.match.structured.body %c | |
| { contraction = ["arith.mulf", "arith.addf"] } : !transform.any_op | |
| %batch, %lhs, %rhs, %reduction = | |
| transform.match.structured.classify_contraction_dims %c | |
| : (!transform.any_op) | |
| -> (!transform.param<i64>, !transform.param<i64>, !transform.param<i64>, | |
| !transform.param<i64>) | |
| // There is one of lhs, rhs and reduction dimensions and zero batch | |
| // dimensions. | |
| %n_batch = transform.num_associations %batch | |
| : (!transform.param<i64>) -> !transform.param<i64> | |
| %n_lhs = transform.num_associations %lhs | |
| : (!transform.param<i64>) -> !transform.param<i64> | |
| %n_rhs = transform.num_associations %rhs | |
| : (!transform.param<i64>) -> !transform.param<i64> | |
| %n_reduction = transform.num_associations %reduction | |
| : (!transform.param<i64>) -> !transform.param<i64> | |
| %c0 = transform.param.constant 0 : i64 -> !transform.param<i64> | |
| transform.match.param.cmpi eq %n_batch, %c0 : !transform.param<i64> | |
| transform.match.param.cmpi eq %n_lhs, %c1 : !transform.param<i64> | |
| transform.match.param.cmpi eq %n_rhs, %c1 : !transform.param<i64> | |
| transform.match.param.cmpi eq %n_reduction, %c1 : !transform.param<i64> | |
| } | |
| transform.yield %candidate : !transform.any_op | |
| } | |
| ``` | |
| While this example leverages the contraction-specific matchers that have a | |
| rather non-trivial C++ implementation, the transform dialect is sufficiently | |
| flexible to implement this reasoning directly if desired. One could, for | |
| example, obtain the access map of each input as a parameter and extract the | |
| accessed dimensions as other parameters that can be compared with each other to | |
| ensure the subscripts are `m,k` for LHS, `k,n` for RHS and `m,n` for the | |
| init/result given the `m,n,k` notation for loops. | |
| ## Appendix: Autogenerated Documentation | |
| [include "Tutorials/transform/MyExtensionCh4.md"] | |