Files
Ege Beysel 3824108316 [mlir][linalg/scf/transform] scalable tiling and fusion for pack/unpack ops (#204007)
# Inner tile alignment hints for scalable `linalg.pack`/`linalg.unpack`
tiling and fusion

## Overview

Tiling and fusing `linalg.pack`/`linalg.unpack` produces a clean result
only when the tiling implementation can tell how a loop tile size
relates to the op's inner tile size. When both are statically known this
is decided by comparing the constants. But with **scalable** (and, more
generally, dynamic) sizes, e.g. a loop tile of `8 * vscale` against an
inner tile of `8 * vscale`, that relationship is symbolic and cannot
cleanly be recovered from the IR, so the implementation conservatively
falls back to a dynamic, over-allocated tile. See #150185 for more
details.

This PR adds an optional **inner tile alignment hint**: a per-dimension
caller assertion about that relationship, threaded from the transform-op
surface through the SCF driver into the pack/unpack `TilingInterface`
implementations. The caller is already aware of both the loop tile
sizes, and the inner tile sizes, and therefore can set these hints
easily. With the hint, scalable pack/unpack tiling and fusion produce
the same tight IR they produce for the statically-aligned case.

## The inner tile alignment hint

`InnerTileAlignment` is a per-iteration-domain-dimension enum:

- `Unknown` (`0`) — no information; the implementation keeps its prior
behavior.
- `Multiple` (`1`) — the loop tile size is an integer multiple of the
inner tile.
- `Equal` (`2`) — the loop tile size equals the inner tile size.

It is a *caller assertion*, not a checked fact. It is consulted only
when the relationship cannot be decided from the IR; when the tile and
inner-tile sizes are both statically known, that static comparison wins
and a contradicting hint is ignored rather than allowed to corrupt the
result. Otherwise the hint is not verified, so an incorrect assertion
silently produces invalid tiling, its correctness is owned by the
caller. Only pack/unpack consult it; every other op ignores it.

## TilingInterface API

Four methods gain a second overload taking an extra
`ArrayRef<InnerTileAlignment>` hint:

- `getTiledImplementation`
- `generateResultTileValue`
- `getTiledImplementationFromOperandTiles`
- `getIterationDomainTileFromOperandTiles`

The hint-bearing overload defaults to forwarding to the hint-less one,
so the change is additive: existing implementers and callers that never
mention the hint are unaffected, and the rest of the interface is
untouched.

### Where it helps

The loop tiles the packed dimension by `8 * vscale`, so the size handed
to the pack/unpack is a **full-or-remainder** `affine.min` against that
step:

```mlir
//   #map = affine_map<(d0)[s0] -> (-d0 + 4096, s0)>   // min(4096 - iv, 8*vscale)
%ts = affine.min #map(%iv)[%c8_vscale]
// ... %ts feeds the extracted slice fed to the pack/unpack, whose inner tile
//     on this dimension is also 8*vscale.
```

To fold the tiled op's outer (tile-count) dimension to a static `1`, the
current implementation asks
`ValueBoundsConstraintSet::computeConstantBound` for a *constant* upper
bound on `%ts` and compares it against the inner tile size, essentially
`UB(%ts) % innerTile == 0`. With a scalable `8 * vscale` inner tile,
neither the upper bound of `%ts` nor the inner tile is a constant, so
the comparison is undecidable and the tile conservatively falls back to
a dynamic outer dim (`tensor<?x...>`), over-allocating the tile.

`inner_tile_alignments = [Equal]` asserts `%ts == 8 * vscale`, so the
outer dim folds to a static `1` (`tensor<1x...>`); `[Multiple]` instead
takes the `ceilDiv` branch. The hint thus supplies exactly the
relationship the upper-bound comparison cannot recover for symbolic
sizes.

### How the hint is arranged

- One entry per **iteration-domain dimension**, in **pre-interchange**
order (`interchange` only reorders the generated loops, not the hint).
- Entries are **not** remapped through indexing maps or
`outer_dims_perm`: for a transposing pack they stay in source
(pre-permutation) order.
- For the consumer-fusion (`*FromOperandTiles`) methods, the indexing
follows the op's iteration domain: for a pack it coincides with the
unpacked source dims; for an unpack the entry for the i-th inner tile
sits at its destination dimension `inner_dims_pos[i]`.

The caller must pre-arrange the array to match this order. See the tests
for more examples.

### Out-of-tree implementers

Because the two overloads share a source name:

- An op that lists one of the four methods in
`DeclareOpInterfaceMethods` now has **both** arities declared on it, so
it must also define the hint-bearing overload (typically a one-line
forward to the hint-less one).
- An `ExternalModel` that defines the hint-less overload **hides** the
inherited defaulted hint-bearing overload (C++ name hiding); add `using
Base::<method>;` to keep it visible.

Implementers that do not care about the hint need no functional change
beyond the above visibility boilerplate.

## SCF tiling driver API

The producer- and consumer-fusion driver entry points
(`tileAndFuseProducerOfSlice`, `tileAndFuseConsumerOfSlices`,
`tileAndFuseConsumer`) and `SCFTilingOptions` accept an optional
**control function** rather than a single fixed array.

A single tile-and-fuse invocation can tile/fuse several ops with
**different iteration domains**, so no single array can be correct for
all of them. The driver therefore invokes the control function once per
op being tiled/fused and
expects the returned array to be in *that op's own* domain, it does not
remap it.

### Why a control function and not one array

A single tile-and-fuse can pull in more than one pack/unpack, and each
consults the hint in *its own* iteration domain:

```mlir
%unpacked = linalg.unpack %p0 inner_dims_pos = [0] inner_tiles = [%c8_vscale] ... // domain A
%mul      = linalg.generic ins(%unpacked, ...) ...                                // tiled root
%packed   = linalg.pack   %mul inner_dims_pos = [1] inner_tiles = [%c4_vscale] ... // domain B
// One tile-and-fuse: tile %mul, fuse %unpacked (producer) and %packed (consumer).
```

Both `%unpacked` and `%packed` consume the hint, but their domains
differ, a different dimension is tiled and the inner-tile arrangement is
different, so the array indexed in `%unpacked`'s domain is simply wrong
for `%packed`. No single fixed array can be correct for both. The driver
therefore invokes the control function once per op being tiled/fused and
the caller returns each op's array in its own domain.

### Why the function gets slices (not tile sizes) for fusion

For pure tiling the driver knows the explicit tile sizes and passes
them. In fusion there are no caller-specified tile sizes for the fused
op, its tile is induced by the `tensor.extract_slice` (producer) or
`insert_slice` (consumer) being fused. The control function is therefore
handed those slices, from which the caller can derive the alignment in
the fused op's domain.

## Transform ops

`transform.structured.tile_using_for`, `transform.structured.fuse`, and
`transform.structured.fuse_into_containing_op` gain an optional
`inner_tile_alignments` array attribute (empty by default, preserving
prior behavior). It is wired to the driver as a **constant** control
function (the same array for every op). This is sufficient because each
of these ops drives tiling/fusion at single-op granularity; the per-op
control function remains available at the library/driver level for
callers that fuse multiple ops with differing domains. The test-only
`transform.test.fuse_consumer` is wired the same way.

## Testing

- **`linalg.unpack`**: standalone tiling, producer fusion
(`structured.fuse`, `fuse_into_containing_op`), and consumer fusion
(`test.fuse_consumer`), with both `Multiple` and `Equal` hints.
- **`linalg.pack`**: standalone tiling, producer fusion, and consumer
fusion (`test.fuse_consumer`), with both `Multiple` and `Equal` hints.
- Negative cases: missing hint falls back to the conservative tiling; a
hint that contradicts statically-known sizes is ignored; transposing
`outer_dims_perm`/consumers confirm source-order (un-permuted) indexing.
- Transform-op attribute verification (`inner_tile_alignments`
validity).


Assisted-by: Claude Code

---------

Signed-off-by: Ege Beysel <beyselege@gmail.com>
2026-07-03 09:53:27 +00:00

96 lines
3.4 KiB
C++

//===- TilingInterface.cpp - Tiling interface -------------------*- C++ -*-===//
//
// 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 the definitions of the interface in `TilingInterface.td`.
//
//===----------------------------------------------------------------------===//
#include "mlir/Interfaces/TilingInterface.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/SmallVectorExtras.h"
#include "llvm/ADT/StringSwitch.h"
using namespace mlir;
LogicalResult mlir::verifyInnerTileAlignments(Operation *op,
ArrayRef<int64_t> alignments) {
for (int64_t a : alignments)
if (!isValidInnerTileAlignment(a))
return op->emitOpError()
<< "expected inner_tile_alignments entries to be one of 0 "
"(Unknown), 1 (Multiple) or 2 (Equal), but got "
<< a;
return success();
}
SmallVector<InnerTileAlignment>
mlir::convertInnerTileAlignments(ArrayRef<int64_t> alignments) {
return llvm::map_to_vector(alignments, [](int64_t v) {
assert(isValidInnerTileAlignment(v) &&
"invalid InnerTileAlignment; should be rejected by the verifier");
return static_cast<InnerTileAlignment>(v);
});
}
StringRef mlir::stringifyInnerTileAlignment(InnerTileAlignment alignment) {
switch (alignment) {
case InnerTileAlignment::Unknown:
return "Unknown";
case InnerTileAlignment::Multiple:
return "Multiple";
case InnerTileAlignment::Equal:
return "Equal";
}
llvm_unreachable("unknown InnerTileAlignment");
}
std::optional<InnerTileAlignment>
mlir::symbolizeInnerTileAlignment(StringRef keyword) {
return llvm::StringSwitch<std::optional<InnerTileAlignment>>(keyword)
.Case("Unknown", InnerTileAlignment::Unknown)
.Case("Multiple", InnerTileAlignment::Multiple)
.Case("Equal", InnerTileAlignment::Equal)
.Default(std::nullopt);
}
ParseResult mlir::parseInnerTileAlignmentArray(OpAsmParser &parser,
DenseI64ArrayAttr &alignments) {
SmallVector<int64_t> values;
auto parseEntry = [&]() -> ParseResult {
StringRef keyword;
llvm::SMLoc loc = parser.getCurrentLocation();
if (parser.parseKeyword(&keyword))
return failure();
std::optional<InnerTileAlignment> alignment =
symbolizeInnerTileAlignment(keyword);
if (!alignment)
return parser.emitError(loc)
<< "expected one of 'Unknown', 'Multiple' or 'Equal', but got '"
<< keyword << "'";
values.push_back(static_cast<int64_t>(*alignment));
return success();
};
if (parser.parseCommaSeparatedList(AsmParser::Delimiter::Square, parseEntry))
return failure();
alignments = DenseI64ArrayAttr::get(parser.getContext(), values);
return success();
}
void mlir::printInnerTileAlignmentArray(OpAsmPrinter &printer, Operation *,
DenseI64ArrayAttr alignments) {
printer << "[";
llvm::interleaveComma(alignments.asArrayRef(), printer, [&](int64_t value) {
printer << stringifyInnerTileAlignment(
static_cast<InnerTileAlignment>(value));
});
printer << "]";
}
#include "mlir/Interfaces/TilingInterface.cpp.inc"