Skip to content

Commit 5b0f67b

Browse files
committed
[mlir][bufferization] Don't clone on unknown ownership and verify function boundary ABI
Inserting clones requires a lot of assumptions to hold on the input IR, e.g., all writes to a buffer need to dominate all reads. This is not guaranteed by one-shot bufferization and isn't easy to verify, thus it could quickly lead to incorrect results that are hard to debug. This commit changes the mechanism of how an ownership indicator is materialized when there is not already a unique ownership present. Additionally, we don't create copies of returned memrefs anymore when we don't have ownership. Instead, we insert assert operations to make sure we have ownership at runtime, or otherwise report to the user that correctness could not be guaranteed.
1 parent adb555e commit 5b0f67b

24 files changed

+321
-243
lines changed

mlir/include/mlir/Dialect/Bufferization/IR/BufferDeallocationOpInterface.h

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,20 @@ struct DeallocationOptions {
9595
// A pass option indicating whether private functions should be modified to
9696
// pass the ownership of MemRef values instead of adhering to the function
9797
// boundary ABI.
98-
bool privateFuncDynamicOwnership = false;
98+
bool privateFuncDynamicOwnership = true;
99+
100+
/// Inserts `cf.assert` operations to verify the function boundary ABI at
101+
/// runtime. Currently, it is only checked that the ownership of returned
102+
/// MemRefs is 'true'. This also ensures that the returned memref does not
103+
/// originate from the same allocation as a function argument.
104+
/// Note: The function boundary ABI is disabled for non-external private
105+
/// functions if `privateFuncDynamicOwnership` is enabled and thus this option
106+
/// does not apply to them.
107+
/// TODO: check that returned MemRefs don't alias each other.
108+
/// If it can be determined statically that the ABI is not adhered
109+
/// to, an error will already be emitted at compile time. This cannot be
110+
/// changed with this option.
111+
bool verifyFunctionBoundaryABI = true;
99112
};
100113

101114
/// This class collects all the state that we need to perform the buffer
@@ -138,12 +151,12 @@ class DeallocationState {
138151
void getLiveMemrefsIn(Block *block, SmallVectorImpl<Value> &memrefs);
139152

140153
/// Given an SSA value of MemRef type, this function queries the ownership and
141-
/// if it is not already in the 'Unique' state, potentially inserts IR to get
142-
/// a new SSA value, returned as the first element of the pair, which has
143-
/// 'Unique' ownership and can be used instead of the passed Value with the
144-
/// the ownership indicator returned as the second element of the pair.
145-
std::pair<Value, Value>
146-
getMemrefWithUniqueOwnership(OpBuilder &builder, Value memref, Block *block);
154+
/// if it is not already in the 'Unique' state, potentially inserts IR to
155+
/// determine the ownership (which might involve expensive aliasing checks at
156+
/// runtime).
157+
Value materializeMemRefOwnership(const DeallocationOptions &options,
158+
OpBuilder &builder, Value memref,
159+
Block *block);
147160

148161
/// Given two basic blocks and the values passed via block arguments to the
149162
/// destination block, compute the list of MemRefs that have to be retained in
@@ -220,6 +233,16 @@ FailureOr<Operation *>
220233
insertDeallocOpForReturnLike(DeallocationState &state, Operation *op,
221234
ValueRange operands,
222235
SmallVectorImpl<Value> &updatedOperandOwnerships);
236+
237+
/// Materializes IR that extracts the allocated pointers of the MemRef operands
238+
/// of the defining operation of `memref` as indices and compares them. The
239+
/// ownership of the first one that matches is returned and intended to be
240+
/// assigned to `memref`.
241+
Value defaultComputeMemRefOwnership(const DeallocationOptions &options,
242+
DeallocationState &state,
243+
OpBuilder &builder, Value memref,
244+
Block *block);
245+
223246
} // namespace deallocation_impl
224247

225248
} // namespace bufferization

mlir/include/mlir/Dialect/Bufferization/IR/BufferDeallocationOpInterface.td

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,17 +55,47 @@ def BufferDeallocationOpInterface :
5555
ownership indicator when needed, it should be implemented using this
5656
method (which is especially important if operations are created that
5757
cannot be easily canonicalized away anymore).
58+
Ownership indicators have to be materialized when
59+
* needed for the condition operands of a `bufferization.dealloc` op
60+
* passing them as additional operands to nested regions (e.g.,
61+
init_args of `scf.for`)
62+
* passing them as additional operands to a call operation when
63+
`private-function-dynamic-ownership` is enabled
64+
65+
In the following example, the deallocation pass would add an
66+
additional block argument to `^bb1` for passing the ownership of `%0`
67+
along and thus the ownership indicator has to be materialized before
68+
the `cf.br` operation and added as a forwarded operand.
69+
```mlir
70+
%0 = arith.select %cond, %m1, %m2 : memref<f32>
71+
cf.br ^bb1(%0 : memref<f32>)
72+
^bb1(%arg0: memref<f32>)
73+
...
74+
```
75+
The `arith.select` operation could implement this interface method to
76+
materialize another `arith.select` operation to select the
77+
corresponding ownership indicator.
78+
```mlir
79+
%0 = arith.select %cond, %m1, %m2 : memref<f32>
80+
%0_ownership = arith.select %cond, %m1_ownership, %m2_ownership : i1
81+
cf.br ^bb1(%0, %0_ownership : memref<f32>, i1)
82+
^bb1(%arg0: memref<f32>, %arg1: i1)
83+
...
84+
```
85+
86+
The default implementation assumes that all MemRef operands already
87+
have 'Unique' ownership.
5888
}],
59-
/*retType=*/"std::pair<Value, Value>",
89+
/*retType=*/"Value",
6090
/*methodName=*/"materializeUniqueOwnershipForMemref",
6191
/*args=*/(ins "DeallocationState &":$state,
6292
"const DeallocationOptions &":$options,
6393
"OpBuilder &":$builder,
6494
"Value":$memref),
6595
/*methodBody=*/[{}],
6696
/*defaultImplementation=*/[{
67-
return state.getMemrefWithUniqueOwnership(
68-
builder, memref, memref.getParentBlock());
97+
return deallocation_impl::defaultComputeMemRefOwnership(
98+
options, state, builder, memref, memref.getParentBlock());
6999
}]>,
70100
];
71101
}

mlir/include/mlir/Dialect/Bufferization/Pipelines/Passes.h

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
namespace mlir {
1919
namespace bufferization {
20+
struct DeallocationOptions;
2021

2122
/// Options for the buffer deallocation pipeline.
2223
struct BufferDeallocationPipelineOptions
@@ -27,7 +28,23 @@ struct BufferDeallocationPipelineOptions
2728
"Allows to add additional arguments to private functions to "
2829
"dynamically pass ownership of memrefs to callees. This can enable "
2930
"earlier deallocations."),
30-
llvm::cl::init(false)};
31+
llvm::cl::init(true)};
32+
PassOptions::Option<bool> verifyFunctionBoundaryABI{
33+
*this, "verify-function-boundary-abi",
34+
llvm::cl::desc(
35+
"Inserts `cf.assert` operations to verify the function boundary ABI "
36+
"at runtime. Currently, it is only checked that the ownership of "
37+
"returned MemRefs is 'true'. This makes sure that ownership is "
38+
"yielded and the returned MemRef does not originate from the same "
39+
"allocation as a function argument. If it can be determined "
40+
"statically that the ABI is not adhered to, an error will already be "
41+
"emitted at compile time. This cannot be changed with this option."),
42+
llvm::cl::init(true)};
43+
44+
/// Convert this BufferDeallocationPipelineOptions struct to a
45+
/// DeallocationOptions struct to be passed to the
46+
/// OwnershipBasedBufferDeallocationPass.
47+
DeallocationOptions asDeallocationOptions() const;
3148
};
3249

3350
//===----------------------------------------------------------------------===//

mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#ifndef MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_PASSES_H
22
#define MLIR_DIALECT_BUFFERIZATION_TRANSFORMS_PASSES_H
33

4+
#include "mlir/Dialect/Bufferization/IR/BufferDeallocationOpInterface.h"
45
#include "mlir/Pass/Pass.h"
56

67
namespace mlir {
@@ -31,7 +32,7 @@ std::unique_ptr<Pass> createBufferDeallocationPass();
3132
/// Creates an instance of the OwnershipBasedBufferDeallocation pass to free all
3233
/// allocated buffers.
3334
std::unique_ptr<Pass> createOwnershipBasedBufferDeallocationPass(
34-
bool privateFuncDynamicOwnership = false);
35+
const DeallocationOptions &options = DeallocationOptions());
3536

3637
/// Creates a pass that optimizes `bufferization.dealloc` operations. For
3738
/// example, it reduces the number of alias checks needed at runtime using
@@ -134,8 +135,9 @@ func::FuncOp buildDeallocationLibraryFunction(OpBuilder &builder, Location loc,
134135
LogicalResult deallocateBuffers(Operation *op);
135136

136137
/// Run ownership basedbuffer deallocation.
137-
LogicalResult deallocateBuffersOwnershipBased(FunctionOpInterface op,
138-
bool privateFuncDynamicOwnership);
138+
LogicalResult deallocateBuffersOwnershipBased(
139+
FunctionOpInterface op,
140+
const DeallocationOptions &options = DeallocationOptions());
139141

140142
/// Creates a pass that moves allocations upwards to reduce the number of
141143
/// required copies that are inserted during the BufferDeallocation pass.

mlir/include/mlir/Dialect/Bufferization/Transforms/Passes.td

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,10 +219,20 @@ def OwnershipBasedBufferDeallocation : Pass<
219219
}];
220220
let options = [
221221
Option<"privateFuncDynamicOwnership", "private-function-dynamic-ownership",
222-
"bool", /*default=*/"false",
222+
"bool", /*default=*/"true",
223223
"Allows to add additional arguments to private functions to "
224224
"dynamically pass ownership of memrefs to callees. This can enable "
225225
"earlier deallocations.">,
226+
Option<"verifyFunctionBoundaryABI", "verify-function-boundary-abi",
227+
"bool", /*default=*/"true",
228+
"Inserts `cf.assert` operations to verify the function boundary ABI "
229+
"at runtime. Currently, it is only checked that the ownership of "
230+
"returned MemRefs is 'true'. This makes sure that ownership is "
231+
"yielded and the returned MemRef does not originate from the same "
232+
"allocation as a function argument. "
233+
"If it can be determined statically that the ABI is not adhered "
234+
"to, an error will already be emitted at compile time. This cannot "
235+
"be changed with this option.">,
226236
];
227237
let constructor = "mlir::bufferization::createOwnershipBasedBufferDeallocationPass()";
228238

mlir/lib/Dialect/Arith/Transforms/BufferDeallocationOpInterfaceImpl.cpp

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -53,25 +53,21 @@ struct SelectOpInterface
5353
return op; // nothing to do
5454
}
5555

56-
std::pair<Value, Value>
57-
materializeUniqueOwnershipForMemref(Operation *op, DeallocationState &state,
58-
const DeallocationOptions &options,
59-
OpBuilder &builder, Value value) const {
56+
Value materializeUniqueOwnershipForMemref(Operation *op,
57+
DeallocationState &state,
58+
const DeallocationOptions &options,
59+
OpBuilder &builder,
60+
Value value) const {
6061
auto selectOp = cast<arith::SelectOp>(op);
6162
assert(value == selectOp.getResult() &&
6263
"Value not defined by this operation");
6364

6465
Block *block = value.getParentBlock();
65-
if (!state.getOwnership(selectOp.getTrueValue(), block).isUnique() ||
66-
!state.getOwnership(selectOp.getFalseValue(), block).isUnique())
67-
return state.getMemrefWithUniqueOwnership(builder, value,
68-
value.getParentBlock());
69-
7066
Value ownership = builder.create<arith::SelectOp>(
7167
op->getLoc(), selectOp.getCondition(),
7268
state.getOwnership(selectOp.getTrueValue(), block).getIndicator(),
7369
state.getOwnership(selectOp.getFalseValue(), block).getIndicator());
74-
return {selectOp.getResult(), ownership};
70+
return ownership;
7571
}
7672
};
7773

mlir/lib/Dialect/Bufferization/IR/BufferDeallocationOpInterface.cpp

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -132,30 +132,83 @@ void DeallocationState::getLiveMemrefsIn(Block *block,
132132
memrefs.append(liveMemrefs);
133133
}
134134

135-
std::pair<Value, Value>
136-
DeallocationState::getMemrefWithUniqueOwnership(OpBuilder &builder,
137-
Value memref, Block *block) {
138-
auto iter = ownershipMap.find({memref, block});
139-
assert(iter != ownershipMap.end() &&
140-
"Value must already have been registered in the ownership map");
141-
142-
Ownership ownership = iter->second;
143-
if (ownership.isUnique())
144-
return {memref, ownership.getIndicator()};
145-
146-
// Instead of inserting a clone operation we could also insert a dealloc
147-
// operation earlier in the block and use the updated ownerships returned by
148-
// the op for the retained values. Alternatively, we could insert code to
149-
// check aliasing at runtime and use this information to combine two unique
150-
// ownerships more intelligently to not end up with an 'Unknown' ownership in
151-
// the first place.
152-
auto cloneOp =
153-
builder.create<bufferization::CloneOp>(memref.getLoc(), memref);
154-
Value condition = buildBoolValue(builder, memref.getLoc(), true);
155-
Value newMemref = cloneOp.getResult();
156-
updateOwnership(newMemref, condition);
157-
memrefsToDeallocatePerBlock[newMemref.getParentBlock()].push_back(newMemref);
158-
return {newMemref, condition};
135+
Value DeallocationState::materializeMemRefOwnership(
136+
const DeallocationOptions &options, OpBuilder &builder, Value memref,
137+
Block *block) {
138+
// NOTE: Starts at the operation defining `memref` and performs a DFS along
139+
// the reverse def/use chain until MemRef values with 'Unique' ownership are
140+
// found. For the operation being currently processed:
141+
// * if none of the operands have the same allocated pointer (i.e., originate
142+
// from the same allocation), a new memref was allocated and thus the
143+
// operation should have the allocate side-effect defined on that result
144+
// value and thus the correct unique ownership is pre-populated by the
145+
// ownership pass (unless an interface implementation is incorrect). Note
146+
// that this is problematic for operations of unregistered dialects because
147+
// the allocation side-effect cannot be represented in the assembly format.
148+
// * if exactly one operand has the same allocated pointer, this returnes the
149+
// ownership of exactly that operand
150+
// * if multiple operands match the allocated pointer of the result, the
151+
// ownership indicators of all of them always have to evaluate to the same
152+
// value because no dealloc operations may be present and because of the
153+
// rules they are passed to nested regions and successor blocks. This could
154+
// be verified at runtime by inserting `cf.assert` operations, but would
155+
// require O(|operands|^2) additional operations to check and is thus not
156+
// implemented yet (would need to insert a library function to avoid
157+
// code-size explosion which would make the deallocation pass a module pass)
158+
auto ipSave = builder.saveInsertionPoint();
159+
SmallVector<Value> worklist;
160+
worklist.push_back(memref);
161+
162+
while (!worklist.empty()) {
163+
Value curr = worklist.back();
164+
165+
// If the value already has unique ownership, we don't have to process it
166+
// anymore.
167+
Ownership ownership = getOwnership(curr, block);
168+
if (ownership.isUnique()) {
169+
worklist.pop_back();
170+
continue;
171+
}
172+
173+
// Check if all operands of MemRef type have unique ownership.
174+
Operation *defOp = curr.getDefiningOp();
175+
assert(defOp &&
176+
"the ownership-based deallocation pass should be written in a way "
177+
"that pre-populates ownership for block arguments");
178+
179+
bool allKnown = true;
180+
for (Value val : llvm::make_filter_range(defOp->getOperands(), isMemref)) {
181+
Ownership ownership = getOwnership(val, block);
182+
if (ownership.isUnique())
183+
continue;
184+
185+
worklist.push_back(val);
186+
allKnown = false;
187+
}
188+
189+
// If all MemRef operands have unique ownership, we can check if the op
190+
// implements the BufferDeallocationOpInterface and call that or, otherwise,
191+
// we call the generic implementation manually here.
192+
if (allKnown) {
193+
builder.setInsertionPointAfter(defOp);
194+
if (auto deallocInterface =
195+
dyn_cast<BufferDeallocationOpInterface>(defOp);
196+
deallocInterface && curr.getParentBlock() == block)
197+
ownership = deallocInterface.materializeUniqueOwnershipForMemref(
198+
*this, options, builder, curr);
199+
else
200+
ownership = deallocation_impl::defaultComputeMemRefOwnership(
201+
options, *this, builder, curr, block);
202+
203+
// Ownership is already 'Unknown', so we need to override instead of
204+
// joining.
205+
resetOwnerships(curr, block);
206+
updateOwnership(curr, ownership, block);
207+
}
208+
}
209+
210+
builder.restoreInsertionPoint(ipSave);
211+
return getOwnership(memref, block).getIndicator();
159212
}
160213

161214
void DeallocationState::getMemrefsToRetain(
@@ -313,3 +366,25 @@ FailureOr<Operation *> deallocation_impl::insertDeallocOpForReturnLike(
313366

314367
return op;
315368
}
369+
370+
Value deallocation_impl::defaultComputeMemRefOwnership(
371+
const DeallocationOptions &options, DeallocationState &state,
372+
OpBuilder &builder, Value memref, Block *block) {
373+
Operation *defOp = memref.getDefiningOp();
374+
SmallVector<Value> operands(
375+
llvm::make_filter_range(defOp->getOperands(), isMemref));
376+
Value resultPtr = builder.create<memref::ExtractAlignedPointerAsIndexOp>(
377+
defOp->getLoc(), memref);
378+
Value ownership = state.getOwnership(operands.front(), block).getIndicator();
379+
380+
for (Value val : ArrayRef(operands).drop_front()) {
381+
Value operandPtr = builder.create<memref::ExtractAlignedPointerAsIndexOp>(
382+
defOp->getLoc(), val);
383+
Value isSameBuffer = builder.create<arith::CmpIOp>(
384+
defOp->getLoc(), arith::CmpIPredicate::eq, resultPtr, operandPtr);
385+
Value newOwnership = state.getOwnership(val, block).getIndicator();
386+
ownership = builder.create<arith::SelectOp>(defOp->getLoc(), isSameBuffer,
387+
newOwnership, ownership);
388+
}
389+
return ownership;
390+
}

mlir/lib/Dialect/Bufferization/Pipelines/BufferizationPipelines.cpp

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,22 +8,34 @@
88

99
#include "mlir/Dialect/Bufferization/Pipelines/Passes.h"
1010

11+
#include "mlir/Dialect/Bufferization/IR/BufferDeallocationOpInterface.h"
1112
#include "mlir/Dialect/Bufferization/Transforms/Passes.h"
1213
#include "mlir/Dialect/Func/IR/FuncOps.h"
1314
#include "mlir/Dialect/MemRef/Transforms/Passes.h"
1415
#include "mlir/Pass/PassManager.h"
1516
#include "mlir/Transforms/Passes.h"
1617

18+
using namespace mlir;
19+
using namespace bufferization;
20+
1721
//===----------------------------------------------------------------------===//
1822
// Pipeline implementation.
1923
//===----------------------------------------------------------------------===//
2024

25+
DeallocationOptions
26+
BufferDeallocationPipelineOptions::asDeallocationOptions() const {
27+
DeallocationOptions opts;
28+
opts.privateFuncDynamicOwnership = privateFunctionDynamicOwnership.getValue();
29+
opts.verifyFunctionBoundaryABI = verifyFunctionBoundaryABI.getValue();
30+
return opts;
31+
}
32+
2133
void mlir::bufferization::buildBufferDeallocationPipeline(
2234
OpPassManager &pm, const BufferDeallocationPipelineOptions &options) {
2335
pm.addPass(memref::createExpandReallocPass(/*emitDeallocs=*/false));
2436
pm.addPass(createCanonicalizerPass());
2537
pm.addPass(createOwnershipBasedBufferDeallocationPass(
26-
options.privateFunctionDynamicOwnership.getValue()));
38+
options.asDeallocationOptions()));
2739
pm.addPass(createCanonicalizerPass());
2840
pm.addPass(createBufferDeallocationSimplificationPass());
2941
pm.addPass(createLowerDeallocationsPass());

0 commit comments

Comments
 (0)