llvm/mlir/include/mlir/Interfaces/TilingInterface.td

//===- TilingInterface.td - Interface for tiling operations *- tablegen -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
//
// This file contains an interface to allow operations to generate a tiled
// implementation of themselves.
//
//===----------------------------------------------------------------------===//

#ifndef MLIR_TILINGINTERFACE
#define MLIR_TILINGINTERFACE

include "mlir/IR/OpBase.td"

def TilingInterface : OpInterface<"TilingInterface"> {
  let description = [{
    This interface allows operations to expose information needed to tile them.

    The intent of this interface is to separate the generation of the loop
    structure (and constructs used for it) from the information needed from
    the operation to be able to tile them. As a result an implementation of
    the tiling algorithm (like `scf::tileUsingSCF`) can generate the inter-tile
    loop structure, and call into the methods of the interface to be able to
    tile any operation that implements the interface.

    This interface is also meant to help with "tile and fuse", i.e. the process
    of fusing a producer with a consumer by
      a) Tiling the consumer
      b) Based on the tile of the producer used by the tiled consumer,
         materialize the tiled implementation of a producer to generate that
         tile (and use it immediately in the consumer)
    You could also fuse a consumer with a producer by
      a) Tiling the producer
      b) Based on the tile produced, materialize the tiled implementation of
         a consumer that uses this tile.
    Note that the tile and fuse does not make any calculation on whether it
    is "profitable to do this", but simply provides a mechansim to implement
    the transformation when such a fusion is needed by the caller.

    For any operation to be tilable, an operation has to implement the
    following two methods (see description below)
      - `getLoopIteratorTypes`
      - `getIterationDomain`
      - `getTiledImplementation`
      - `getResultTilePosition`

    For an operation to be "tiled and fused" with its (already tiled) consumer,
    an operation has to implement the following additional method (see
    description below):
      - `generateResultTileValue`
      - `getIterationDomainTileFromResultTile`

    For an operation to be "tiled and fused" with its (already tiled) producer,
    an operation has to implement the following additional methods (see
    description below):
      - `getTiledImplementationFromOperandTile`
      - `getIterationDomainTileFromOperandTile`.
  }];
  let cppNamespace = "::mlir";
  let methods = [
      InterfaceMethod<
        /*desc=*/[{
          Returns a list of iterator types that describe the number of loops.
        }],
        /*retType=*/"SmallVector<utils::IteratorType>",
        /*methodName=*/"getLoopIteratorTypes",
        /*args=*/(ins),
        /*methodBody=*/"",
        /*defaultImplementation=*/"return {};"
      >,
      InterfaceMethod<
        /*desc=*/[{
          Returns a list of ranges that describe the loop bounds and
          step for the loops of the operation.
        }],
        /*retTy=*/"SmallVector<Range>",
        /*methodName=*/"getIterationDomain",
        /*args=*/(ins "OpBuilder &":$b),
        /*methodBody=*/"",
        /*defaultImplementation=*/"return {};"
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to generate the tiled implementation of an operation.

          Given a tile of the iteration space (as returned by
          `getIterationDomain`), generate in-place the code that represents
          the computation corresponding to that tile of the iteration space.
          It is the responsibility of the implementation of this method in
          the operation to generate the slices of the operands needed for the
          tiled implementation.
          - `offsets` provides the offset of the tile in the coordinate system
            of the original iteration space, i.e., if an iteration space
            dimension had non-zero offset, it will be included in the offset
            provided here (as opposed to zero-based offset "relative" to the
            iteration space).
          - `sizes` provides the size of the tile.

          The returned `TilingResult` must return for each result of the
          untiled operation, a `Value` that is the result of the tiled
          operation.
        }],
        /*retType=*/"FailureOr<::mlir::TilingResult>",
        /*methodName=*/"getTiledImplementation",
        /*args=*/(ins
            "OpBuilder &":$b,
            "ArrayRef<OpFoldResult> ":$offsets,
            "ArrayRef<OpFoldResult> ":$sizes),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return {};
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to return the position of the result tile computed by the
          tiled operation.

          For operations that return a value (typically a value of type
          `RankedTensorType`), the generated tiled computation has to also
          recompute a replacement for the results of the original operation.
          The tiled implementation of the operation returns a tile of the
          result(s). This methods returns information about what part of the
          result tensor is computed by the tiled implementation. The manner in
          which these tiles get put together to get the final result is upto
          the surrounding loop construct. If an operation has no results, (for
          example an operation that operates only on memrefs), then this method
          need not be implemented by the operation.
          - `resultNumber` is the result number of the original operation
            being processed.
          - `offsets` provides the offset of the tile in the coordinate system
            of the original iteration space, i.e., if an iteration space
            dimension had non-zero offset, it will be included in the offset
            provided here (as opposed to zero-based offset "relative" to the
            iteration space).
          - `sizes` provides the size of the tile.
          - `resultOffsets` is the offsets of the tile of the result generated
            by the tiled implementation (returned by value).
          - `resultSizes` is the size of the tile of the result generated
            by the tiled implementation (returned by value).

          Note: It is undefined behaviour if there is overlap between the
          tiles of the result generated by the tiled implementation.
        }],
        /*retType=*/"::llvm::LogicalResult",
        /*methodName=*/"getResultTilePosition",
        /*args=*/(ins
          "OpBuilder &":$b,
          "unsigned":$resultNumber,
          "ArrayRef<OpFoldResult> ":$offsets,
          "ArrayRef<OpFoldResult> ":$sizes,
          "SmallVector<OpFoldResult> &":$resultOffsets,
          "SmallVector<OpFoldResult> &":$resultSizes),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to generate the code that produces a tile of the result.

          This method is required to allow operations to be "tiled and fused"
          with an (already tiled) consumer. Typically, for two operations with
          producer -> consumer relation ship, to compute a tile of the
          consumer a `slice` of the producer is needed. This method allows
          computing that slice of the producer in-place, thereby "fusing"
          the operations at tile-granularity. This method is different from
          `getTiledImplementation`, which produces a tiled implementation
          for a tile of the iteration space. This method produces a tiled
          implementation based on the tile of producer required.
          - `resultNumber` is the result of the producer used by the consumer.
          - `offsets` is the offset of the slice of the producer result used by
            the tiled implementation of the consumer.
          - `sizes` is the size of the slice of the producer result used by the
            consumer.
          If fusion of the producer with the consumer is not legal for the
          operation/result, this method should return failure.

          Note: This method only deals with the mechanism of implementing the
          fusion. In general the fusion might result in recomputation (based on
          the way the result is produced by the producer and the access pattern
          used in the consumer to access). This is upto the caller to handle
          appropriately.
        }],
        /*retType=*/"FailureOr<::mlir::TilingResult>",
        /*methodName=*/"generateResultTileValue",
        /*args=*/(ins
          "OpBuilder &":$b,
          "unsigned":$resultNumber,
          "ArrayRef<OpFoldResult>":$offsets,
          "ArrayRef<OpFoldResult>":$sizes),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to generate the tiled implementation of an operation that uses
          exactly a tile of the given operand.

          This method is required to allow operations to be "tiled and fused"
          with an (already tiled) producer. Given a tile of the producer, this
          method generates the tile of the consumer that uses exactly this
          produced tile. In some sense it is the "reverse" of
          `generateResultTileValue`.
          - `operandNumber` is the result of the producer used by the consumer.
          - `offsets` is the offset of the slice of the producer result used by
            the tiled implementation of the consumer.
          - `sizes` is the size of the slice of the producer result used by the
            consumer.
          If it is illegal to fuse with a producer along the given operand for
          an operation, the implementation should return a failure.
        }],
        /*retType=*/"FailureOr<::mlir::TilingResult>",
        /*methodName=*/"getTiledImplementationFromOperandTile",
        /*args=*/(ins
          "OpBuilder &":$b,
          "unsigned":$operandNumber,
          "ArrayRef<OpFoldResult>":$offsets,
          "ArrayRef<OpFoldResult>":$sizes),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to return the tile of the iteration domain that uses a given
          tile of the operand.

          This method is required to allow operations to be "tiled and fused"
          with an (already tiled) producer. Given a tile of an operand,
          returns the tile of the iteration space that uses this tile.
          - `operandNumber` is the result of the producer used by the consumer.
          - `offsets` is the offset of the slice of the producer result used by
            the tiled implementation of the consumer.
          - `sizes` is the size of the slice of the producer result used by the
            consumer.
          If it is illegal to fuse with a producer along the given operand for
          an operation, or if this mapping cannot be computed, the
          implementation should return a failure.

          Note that unlike the "tile consumer and fuse producer" case, the
          "tile producer and fuse consumer" requires an additional method to get
          the iteration tile space that encompasses all uses of the given operand
          tile. The reason for this is, consider
          ```mlir
          %1 = scf.for...  {
            %2 = <tiled_producer_op>
            %3 = tensor.insert_slice %2 into ...
            scf.yield %3
          }
          %4 = <consumer_op>)(... %1... )
          ... <some_op>(... %4 ...)
          ```

          when fused this becomes
          ```
          %1 = scf.for...  {
            %2 = <tiled_producer_op>
            %3 = <tiled_consumer_op>(... %2...)
            %4 = tensor.insert_slice %3 into ...
            scf.yield %4
          }
          ... <some_op>(... %1 ...)
          ```

          i.e, when fusing the consumer, the replacement for the result of the
          consumer needs to be returned to replace the uses of the consumer.
          For the tile+fuse algorithm to do this it needs information about
          which tile of the iteration space encompasses all uses of the tile
          produced and use that to compute what are the results produced. Note
          that this iteration space might be the entire iteration space of the
          operation, or multiple operand tiles might map to intersecting
          iteration spaces. It is upto the caller to make sure that it is still
          fusable with producer in this scenario, or it must return a failure.

          Note that this method is only used as a way to implement the
          transformation. It does not provide guarantees on whether such a
          transformation is profitable.

          For most cases `getTiledImplementationFromOperandTile` could be a
          implemented using `getIterationDomainTileFromOperandTile` +
          `getTiledImplementation` methods.
        }],
        /*retType=*/"::llvm::LogicalResult",
        /*methodName=*/"getIterationDomainTileFromOperandTile",
        /*args=*/(ins
          "OpBuilder &":$b,
          "unsigned":$operandNumber,
          "ArrayRef<OpFoldResult> ":$offsets,
          "ArrayRef<OpFoldResult> ":$sizes,
          "SmallVectorImpl<OpFoldResult> &":$iterDomainOffsets,
          "SmallVectorImpl<OpFoldResult> &":$iterDomainSizes),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to return the tile of the iteration domain based
          on the given tile of the certain result.

          This method is required to allow operations to be "tiled and fused"
          with an (already tiled) consumer. Given a tile of an result,
          returns the tile of the iteration space that uses this tile.
          - `resultNumber` is the result of the producer used by the consumer.
          - `offsets` is the offset of the slice of the producer result used by
            the tiled implementation of the consumer.
          - `sizes` is the size of the slice of the producer result used by the
            consumer.
          If fusion of the producer with the consumer is not legal for the
          result, or if this mapping cannot be computed, the implementation
          should return a failure.

          For most cases `generateResultTileValue` could be a implemented using
          `getIterationDomainTileFromResultTile` + `getTiledImplementation`
          methods.
        }],
        /*retType=*/"::llvm::LogicalResult",
        /*methodName=*/"getIterationDomainTileFromResultTile",
        /*args=*/(ins
          "OpBuilder &":$b,
          "unsigned":$resultNumber,
          "ArrayRef<OpFoldResult> ":$offsets,
          "ArrayRef<OpFoldResult> ":$sizes,
          "SmallVectorImpl<OpFoldResult> &":$iterDomainOffsets,
          "SmallVectorImpl<OpFoldResult> &":$iterDomainSizes),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Generates the scalar implementation of the operation.

          Given the list `ivs` that represent points in the iteration space
          (as specified by `getIterationDomain()`) returns the scalar operations
          that represent the computation at that point in the iteration space.
          This method is typically used as the "exit path", i.e. once all
          transformations are done, this method can be used to lower to scalar
          code that can then be lowered to LLVM or SPIR-V dialects.
        }],
        /*retType=*/"::llvm::LogicalResult",
        /*methodName=*/"generateScalarImplementation",
        /*args=*/(ins
            "OpBuilder &":$b,
            "Location ":$loc,
            "ValueRange ":$ivs),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >
  ];
}

def PartialReductionOpInterface : OpInterface<"PartialReductionOpInterface"> {
  let description = [{
    Interface for allowing operations to expose information needed to
    tile reductions using partial reduction followed by merge. This is
    complementary to TilingInterface to tile reductions.
  }];
  let cppNamespace = "::mlir";
  let methods = [
      InterfaceMethod<
        /*desc=*/[{
          Method to generate a tensor initalized with the identity value of the
          operation reduction. The tensor shape is equal to operation result
          shape with new dimension for each non zero tile size.
        }],
        /*retType=*/"FailureOr<SmallVector<Value>>",
        /*methodName=*/"generateInitialTensorForPartialReduction",
        /*args=*/(ins
            "OpBuilder &":$b,
            "Location":$loc,
            "ArrayRef<OpFoldResult>":$sizes,
            "ArrayRef<int>":$reductionDim),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to generate a tiled version of the operation where the tiled
          reduction dimension are converted to parallel dimensions with a size
          less or equal to the tile size. This is meant to be used with
          `mergeReductions` method which will combine the partial reductions.
        }],
        /*retType=*/"FailureOr<TilingResult>",
        /*methodName=*/"tileToPartialReduction",
        /*args=*/(ins
            "OpBuilder &":$b,
            "Location ":$loc,
            "ValueRange":$init,
            "ArrayRef<OpFoldResult>":$offsets,
            "ArrayRef<OpFoldResult>":$sizes,
            "ArrayRef<int>":$reductionDims),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >,
      InterfaceMethod<
        /*desc=*/[{
          Method to merge partial reductions for an operation that has been
          tiled along the reduction dimensions. This will only apply the
          reduction the operation.
        }],
        /*retType=*/"FailureOr<MergeResult>",
        /*methodName=*/"mergeReductions",
        /*args=*/(ins
            "OpBuilder &":$b,
            "Location ":$loc,
            "ValueRange":$partialReduce,
            "ArrayRef<int>":$reductionDim),
        /*methodBody=*/"",
        /*defaultImplementation=*/[{
          return failure();
        }]
      >
  ];
}
#endif // MLIR_TILINGINTERFACE