Skip to content

[mlir][bufferization] Generalize tensor slice rules to subset ops #65619

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ add_mlir_doc(BufferizationOps BufferizationOps Dialects/ -gen-dialect-doc)
add_mlir_interface(AllocationOpInterface)
add_mlir_interface(BufferDeallocationOpInterface)
add_mlir_interface(BufferizableOpInterface)
add_mlir_interface(SubsetInsertionOpInterface)

set(LLVM_TARGET_DEFINITIONS BufferizationEnums.td)
mlir_tablegen(BufferizationEnums.h.inc -gen-enum-decls)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//===- SubsetInsertionOpInterface.h - Tensor Subsets ------------*- 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
//
//===----------------------------------------------------------------------===//

#ifndef MLIR_DIALECT_BUFFERIZATION_IR_SUBSETINSERTIONOPINTERFACE_H_
#define MLIR_DIALECT_BUFFERIZATION_IR_SUBSETINSERTIONOPINTERFACE_H_

#include "mlir/IR/OpDefinition.h"

namespace mlir {
namespace bufferization {
namespace detail {

/// Return the destination/"init" operand of the op if it implements the
/// `DestinationStyleOpInterface` and has exactly one "init" operand. Asserts
/// otherwise.
OpOperand &defaultGetDestinationOperand(Operation *op);

} // namespace detail
} // namespace bufferization
} // namespace mlir

#include "mlir/Dialect/Bufferization/IR/SubsetInsertionOpInterface.h.inc"

#endif // MLIR_DIALECT_BUFFERIZATION_IR_SUBSETINSERTIONOPINTERFACE_H_
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
//===-- SubsetInsertionOpInterface.td - Tensor Subsets -----*- 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
//
//===----------------------------------------------------------------------===//

#ifndef SUBSET_INSERTION_OP_INTERFACE
#define SUBSET_INSERTION_OP_INTERFACE

include "mlir/IR/OpBase.td"

def SubsetInsertionOpInterface : OpInterface<"SubsetInsertionOpInterface"> {
let description = [{
This interface can be implemented by ops that insert a source tensor into
a destination tensor.

The elements in the destination tensor that are overwritten by this
insertion are called the "subset". How the subset is defined is up to the
op. E.g., "tensor.insert_slice" defines the subset via a hyperrectangular
slice. A scatter operation could define the subset via a list of indices.

Ops that deal with tensor subsets come in two flavours:
- Insertion flavor: Ops that insert a source tensor into a destination
tensor at the specified subset. Such ops usually return a new destination
tensor and implement the `DestinationStyleOpInterface`. Insertion ops can
implement the `SubsetInsertionOpInterface`. Example: "tensor.insert_slice"
- Extraction flavor: Ops that define a tensor subset. They extract a
specified subset from a tensor. There is currently no op interface for
such ops. Example: "tensor.extract_slice"

This interface provides helper methods for efficient bufferization of
subset-based tensor IR. Tensor subsets can bufferize to buffer "views"/
"aliases" (in contrast to one or multiple less efficient buffer allocation).

This interface is queried by One-Shot Bufferize to detect cases where a
seeming read-after-write is not actually a conflict because the respective
ops are operating on equivalent subsets. More details can be found in the
documentation of One-Shot Analysis (see `areNonConflictingSubsets`).

Note: This interface currently assumes that a subset op inserts a single
tensor (source) into a destination tensor at a single subset.
}];
let cppNamespace = "::mlir::bufferization";
let methods = [
InterfaceMethod<
/*desc=*/[{
Return the source tensor operand.
}],
/*retType=*/"::mlir::OpOperand &",
/*methodName=*/"getSourceOperand",
/*args=*/(ins)
>,
InterfaceMethod<
/*desc=*/[{
Return the destination tensor operand.
}],
/*retType=*/"::mlir::OpOperand &",
/*methodName=*/"getDestinationOperand",
/*args=*/(ins),
/*methodBody=*/"",
/*defaultImplementation=*/[{
return ::mlir::bufferization::detail::defaultGetDestinationOperand(
$_op.getOperation());
}]
>,
InterfaceMethod<
/*desc=*/[{
Return "true" if this operation inserts into a subset that is
equivalent to the subset defined by `candidate`.

Two subsets are "equivalent" and "same" if they can bufferize to the
same buffer views/aliases. If they are "equivalent", the tensor IR
may be expressed in terms of different SSA values (but they could
bufferize to MemRef SSA values that can CSE without breaking
correctness). `equivalenceFn` should return "true" if the two given
values are equivalent.

Example:
```
// The subset of the SubsetInsertionOpInterface op %1 is equivalent to
// the subset defined by %2 (but not "same"):
%0 = arith.select %c, %t, %t : tensor<?xf32>
%1 = tensor.insert_slice %x into %0[0][5][1]
: tensor<5xf32> into tensor<?xf32>
%2 = tensor.extract_slice %t[0][5][1] : tensor<?xf32> to tensor<5xf32>

// The subset of the SubsetInsertionOpInterface op %1 is equivalent to
// and "same" as the subset defined by %2.
%1 = tensor.insert_slice %x into %t[0][5][1]
: tensor<5xf32> into tensor<?xf32>
%2 = tensor.extract_slice %t[0][5][1] : tensor<?xf32> to tensor<5xf32>
```
}],
/*retType=*/"bool",
/*methodName=*/"isEquivalentSubset",
/*args=*/(ins
"::mlir::Value":$candidate,
"::llvm::function_ref<bool(Value, Value)>":$equivalenceFn)
>,
];

let extraClassDeclaration = [{
/// Return "true" if this operation inserts into the same subset as defined
/// by `candidate`.
///
/// Note: This function is useful outside of bufferization, where no tensor
/// equivalence information is available.
bool isSameSubset(OpResult candidate) {
auto subsetOp = cast<::mlir::bufferization::SubsetInsertionOpInterface>(
getOperation());
return subsetOp.isEquivalentSubset(
candidate, [](Value v1, Value v2) { return v1 == v2; });
}
}];
}

#endif // SUBSET_INSERTION_OP_INTERFACE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//===- SubsetInsertionOpInterfaceImpl.h - Tensor subsets ------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#ifndef MLIR_DIALECT_TENSOR_SUBSETINSERTIONOPINTERFACEIMPL_H
#define MLIR_DIALECT_TENSOR_SUBSETINSERTIONOPINTERFACEIMPL_H

namespace mlir {
class DialectRegistry;

namespace tensor {
void registerSubsetInsertionOpInterfaceExternalModels(
DialectRegistry &registry);
} // namespace tensor
} // namespace mlir

#endif // MLIR_DIALECT_TENSOR_SUBSETINSERTIONOPINTERFACEIMPL_H
2 changes: 2 additions & 0 deletions mlir/include/mlir/InitAllDialects.h
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
#include "mlir/Dialect/Tensor/IR/ValueBoundsOpInterfaceImpl.h"
#include "mlir/Dialect/Tensor/TransformOps/TensorTransformOps.h"
#include "mlir/Dialect/Tensor/Transforms/BufferizableOpInterfaceImpl.h"
#include "mlir/Dialect/Tensor/Transforms/SubsetInsertionOpInterfaceImpl.h"
#include "mlir/Dialect/Tosa/IR/TosaOps.h"
#include "mlir/Dialect/Transform/IR/TransformDialect.h"
#include "mlir/Dialect/Transform/PDLExtension/PDLExtension.h"
Expand Down Expand Up @@ -158,6 +159,7 @@ inline void registerAllDialects(DialectRegistry &registry) {
tensor::registerBufferizableOpInterfaceExternalModels(registry);
tensor::registerFindPayloadReplacementOpInterfaceExternalModels(registry);
tensor::registerInferTypeOpInterfaceExternalModels(registry);
tensor::registerSubsetInsertionOpInterfaceExternalModels(registry);
tensor::registerTilingInterfaceExternalModels(registry);
tensor::registerValueBoundsOpInterfaceExternalModels(registry);
vector::registerBufferizableOpInterfaceExternalModels(registry);
Expand Down
1 change: 1 addition & 0 deletions mlir/lib/Dialect/Bufferization/IR/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ add_mlir_dialect_library(MLIRBufferizationDialect
BufferDeallocationOpInterface.cpp
BufferizationOps.cpp
BufferizationDialect.cpp
SubsetInsertionOpInterface.cpp
UnstructuredControlFlow.cpp

ADDITIONAL_HEADER_DIRS
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//===- SubsetInsertionOpInterface.cpp - Tensor Subsets --------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

#include "mlir/Dialect/Bufferization/IR/SubsetInsertionOpInterface.h"
#include "mlir/Interfaces/DestinationStyleOpInterface.h"

#include "mlir/Dialect/Bufferization/IR/SubsetInsertionOpInterface.cpp.inc"

using namespace mlir;

OpOperand &bufferization::detail::defaultGetDestinationOperand(Operation *op) {
auto dstOp = dyn_cast<DestinationStyleOpInterface>(op);
assert(dstOp && "getDestination must be implemented for non-DPS ops");
assert(
dstOp.getNumDpsInits() == 1 &&
"getDestination must be implemented for ops with 0 or more than 1 init");
return *dstOp.getDpsInitOperand(0);
}
106 changes: 106 additions & 0 deletions mlir/lib/Dialect/Bufferization/Transforms/OneShotAnalysis.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

#include "mlir/Dialect/Bufferization/IR/BufferizableOpInterface.h"
#include "mlir/Dialect/Bufferization/IR/Bufferization.h"
#include "mlir/Dialect/Bufferization/IR/SubsetInsertionOpInterface.h"
#include "mlir/Dialect/Bufferization/Transforms/Bufferize.h"
#include "mlir/Dialect/Bufferization/Transforms/Transforms.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
Expand Down Expand Up @@ -531,6 +532,105 @@ static bool hasEquivalentValueInReverseUseDefChain(AnalysisState &state,
.empty();
}

/// Return "true" if `value` is originating from a subset that is equivalent to
/// the subset that `subsetOp` inserts into.
static bool matchesInsertDestination(const AnalysisState &state, Value value,
SubsetInsertionOpInterface subsetOp) {
auto matchingSubset = [&](Value val) {
if (auto opResult = dyn_cast<OpResult>(val))
if (subsetOp.isEquivalentSubset(opResult, [&](Value v1, Value v2) {
return state.areEquivalentBufferizedValues(v1, v2);
}))
return true;
return false;
};
// There may be multiple leaves at which the reverse SSA use-def chain lookup
// terminates. All of them must be equivalent subsets.
SetVector<Value> backwardSlice =
state.findValueInReverseUseDefChain(value, matchingSubset);
return static_cast<bool>(llvm::all_of(backwardSlice, matchingSubset));
}

/// Return "true" if the given "read" and potentially conflicting "write" are
/// not conflicting due to their subset relationship. The comments in this
/// function are expressed in terms of tensor.extract_slice/tensor.insert_slice
/// pairs, but apply to any subset ops that implement the
/// `SubsetInsertionOpInterface`.
static bool areNonConflictingSubsets(OpOperand *uRead,
OpOperand *uConflictingWrite,
const AnalysisState &state) {
Operation *readingOp = uRead->getOwner();
Operation *conflictingWritingOp = uConflictingWrite->getOwner();

// Special rules for matching ExtractSliceOp/InsertSliceOp pairs. If
// uRead is an InsertSliceOp...
if (auto subsetOp = dyn_cast<SubsetInsertionOpInterface>(readingOp)) {
// As an example, consider the following IR.
//
// %0 = tensor.extract_slice %t[%a, %b][%c, %d][1, 1] {inplace = [true] }
// %1 = linalg.fill %cst, %0 {inplace= [true] }
// %2 = tensor.insert_slice %1 into %t[%a, %b][%c, %d][1, 1]
// {inplace= [true] }

if (uRead == &subsetOp.getDestinationOperand() &&
matchesInsertDestination(state, uConflictingWrite->get(), subsetOp))
// Case 1: The main insight is that InsertSliceOp reads only part of
// the destination tensor. The overwritten area is not read. If
// uConflictingWrite writes into exactly the memory location that is
// being read by uRead, this is not a conflict.
//
// In the above example:
// uRead = OpOperand 1 (%t) of tensor.insert_slice
// uConflictingWrite = OpOperand 1 (%0) of linalg.fill
//
// The read of %t does not conflict with the write of the FillOp
// (same aliases!) because the area that the FillOp operates on is
// exactly the one that is *not* read via %t.
return true;

if (uRead == &subsetOp.getSourceOperand() &&
uConflictingWrite == &subsetOp.getDestinationOperand() &&
matchesInsertDestination(state, uRead->get(), subsetOp))
// Case 2: The read of the source tensor and the write to the dest
// tensor via an InsertSliceOp is not a conflict if the read is
// reading exactly that part of an equivalent tensor that the
// InsertSliceOp is writing.
//
// In the above example:
// uRead = OpOperand 0 (%1) of tensor.insert_slice
// uConflictingWrite = OpOperand 1 (%t) of tensor.insert_slice
return true;
}

// If uConflictingWrite is an InsertSliceOp...
if (auto subsetOp =
dyn_cast<SubsetInsertionOpInterface>(conflictingWritingOp))
// As an example, consider the following IR.
//
// %0 = tensor.extract_slice %t[%a, %b][%c, %d][1, 1] {inplace = [true] }
// %1 = linalg.fill %cst, %0 {inplace= [true] }
// %2 = tensor.insert_slice %1 into %t[%a, %b][%c, %d][1, 1]
// {inplace= [true] }
// %3 = vector.transfer_read %1, %cst
//
// In the above example:
// uRead = OpOperand 0 (%1) of vector.transfer_read
// uConflictingWrite = OpOperand 1 (%t) of tensor.insert_slice
// definition = %1
//
// This is not a conflict because the InsertSliceOp overwrites the
// memory segment of %1 with the exact same data. (Effectively, there
// is no memory write here.)
if (uConflictingWrite == &subsetOp.getDestinationOperand() &&
state.areEquivalentBufferizedValues(
uRead->get(), subsetOp.getSourceOperand().get()) &&
matchesInsertDestination(state, subsetOp.getSourceOperand().get(),
subsetOp))
return true;

return false;
}

/// Given sets of uses and writes, return true if there is a RaW conflict under
/// the assumption that all given reads/writes alias the same buffer and that
/// all given writes bufferize inplace.
Expand Down Expand Up @@ -684,6 +784,12 @@ hasReadAfterWriteInterference(const DenseSet<OpOperand *> &usesRead,
}
}

// No conflict if the operands are non-conflicting subsets.
if (areNonConflictingSubsets(uRead, uConflictingWrite, state)) {
LLVM_DEBUG(llvm::dbgs() << " no conflict: non-conflicting subsets\n");
continue;
}

// No conflict if the op interface says so.
if (auto bufferizableOp = options.dynCastBufferizableOp(readingOp)) {
if (bufferizableOp.isNotConflicting(uRead, uConflictingWrite, state)) {
Expand Down
Loading