We have provided a generic buffer assignment transformation ported from TensorFlow. This generic transformation pass automatically analyzes the values and their aliases (also in other blocks) and returns the valid positions for Alloc and Dealloc operations. To find these positions, the algorithm uses the block Dominator and Post-Dominator analyses. In our proposed algorithm, we have considered aliasing, liveness, nested regions, branches, conditional branches, critical edges, and independency to custom block terminators. This implementation doesn't support block loops. However, we have considered this in our design. For this purpose, it is only required to have a loop analysis to insert Alloc and Dealloc operations outside of these loops in some special cases. Differential Revision: https://reviews.llvm.org/D78484
447 lines
18 KiB
C++
447 lines
18 KiB
C++
//===- BufferPlacement.cpp - the impl for buffer placement ---------------===//
|
|
//
|
|
// 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 implements logic for computing correct alloc and dealloc positions.
|
|
// The main class is the BufferPlacementPass class that implements the
|
|
// underlying algorithm. In order to put allocations and deallocations at safe
|
|
// positions, it is significantly important to put them into the correct blocks.
|
|
// However, the liveness analysis does not pay attention to aliases, which can
|
|
// occur due to branches (and their associated block arguments) in general. For
|
|
// this purpose, BufferPlacement firstly finds all possible aliases for a single
|
|
// value (using the BufferPlacementAliasAnalysis class). Consider the following
|
|
// example:
|
|
//
|
|
// ^bb0(%arg0):
|
|
// cond_br %cond, ^bb1, ^bb2
|
|
// ^bb1:
|
|
// br ^exit(%arg0)
|
|
// ^bb2:
|
|
// %new_value = ...
|
|
// br ^exit(%new_value)
|
|
// ^exit(%arg1):
|
|
// return %arg1;
|
|
//
|
|
// Using liveness information on its own would cause us to place the allocs and
|
|
// deallocs in the wrong block. This is due to the fact that %new_value will not
|
|
// be liveOut of its block. Instead, we have to place the alloc for %new_value
|
|
// in bb0 and its associated dealloc in exit. Using the class
|
|
// BufferPlacementAliasAnalysis, we will find out that %new_value has a
|
|
// potential alias %arg1. In order to find the dealloc position we have to find
|
|
// all potential aliases, iterate over their uses and find the common
|
|
// post-dominator block. In this block we can safely be sure that %new_value
|
|
// will die and can use liveness information to determine the exact operation
|
|
// after which we have to insert the dealloc. Finding the alloc position is
|
|
// highly similar and non- obvious. Again, we have to consider all potential
|
|
// aliases and find the common dominator block to place the alloc.
|
|
//
|
|
// TODO:
|
|
// The current implementation does not support loops and the resulting code will
|
|
// be invalid with respect to program semantics. The only thing that is
|
|
// currently missing is a high-level loop analysis that allows us to move allocs
|
|
// and deallocs outside of the loop blocks.
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "mlir/Transforms/BufferPlacement.h"
|
|
#include "mlir/IR/Function.h"
|
|
#include "mlir/IR/Operation.h"
|
|
#include "mlir/Pass/Pass.h"
|
|
#include "mlir/Transforms/Passes.h"
|
|
|
|
using namespace mlir;
|
|
|
|
namespace {
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferPlacementAliasAnalysis
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// A straight-forward alias analysis which ensures that all aliases of all
|
|
/// values will be determined. This is a requirement for the BufferPlacement
|
|
/// class since you need to determine safe positions to place alloc and
|
|
/// deallocs.
|
|
class BufferPlacementAliasAnalysis {
|
|
public:
|
|
using ValueSetT = SmallPtrSet<Value, 16>;
|
|
|
|
public:
|
|
/// Constructs a new alias analysis using the op provided.
|
|
BufferPlacementAliasAnalysis(Operation *op) { build(op->getRegions()); }
|
|
|
|
/// Finds all immediate and indirect aliases this value could potentially
|
|
/// have. Note that the resulting set will also contain the value provided as
|
|
/// it is an alias of itself.
|
|
ValueSetT resolve(Value value) const {
|
|
ValueSetT result;
|
|
resolveRecursive(value, result);
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
/// Recursively determines alias information for the given value. It stores
|
|
/// all newly found potential aliases in the given result set.
|
|
void resolveRecursive(Value value, ValueSetT &result) const {
|
|
if (!result.insert(value).second)
|
|
return;
|
|
auto it = aliases.find(value);
|
|
if (it == aliases.end())
|
|
return;
|
|
for (Value alias : it->second)
|
|
resolveRecursive(alias, result);
|
|
}
|
|
|
|
/// This function constructs a mapping from values to its immediate aliases.
|
|
/// It iterates over all blocks, gets their predecessors, determines the
|
|
/// values that will be passed to the corresponding block arguments and
|
|
/// inserts them into the underlying map.
|
|
void build(MutableArrayRef<Region> regions) {
|
|
for (Region ®ion : regions) {
|
|
for (Block &block : region) {
|
|
// Iterate over all predecessor and get the mapped values to their
|
|
// corresponding block arguments values.
|
|
for (auto it = block.pred_begin(), e = block.pred_end(); it != e;
|
|
++it) {
|
|
unsigned successorIndex = it.getSuccessorIndex();
|
|
// Get the terminator and the values that will be passed to our block.
|
|
auto branchInterface =
|
|
dyn_cast<BranchOpInterface>((*it)->getTerminator());
|
|
if (!branchInterface)
|
|
continue;
|
|
// Query the branch op interace to get the successor operands.
|
|
auto successorOperands =
|
|
branchInterface.getSuccessorOperands(successorIndex);
|
|
if (successorOperands.hasValue()) {
|
|
// Build the actual mapping of values to their immediate aliases.
|
|
for (auto argPair : llvm::zip(block.getArguments(),
|
|
successorOperands.getValue())) {
|
|
aliases[std::get<1>(argPair)].insert(std::get<0>(argPair));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Maps values to all immediate aliases this value can have.
|
|
llvm::DenseMap<Value, ValueSetT> aliases;
|
|
};
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferPlacementPositions
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// Stores correct alloc and dealloc positions to place dialect-specific alloc
|
|
/// and dealloc operations.
|
|
struct BufferPlacementPositions {
|
|
public:
|
|
BufferPlacementPositions()
|
|
: allocPosition(nullptr), deallocPosition(nullptr) {}
|
|
|
|
/// Creates a new positions tuple including alloc and dealloc positions.
|
|
BufferPlacementPositions(Operation *allocPosition, Operation *deallocPosition)
|
|
: allocPosition(allocPosition), deallocPosition(deallocPosition) {}
|
|
|
|
/// Returns the alloc position before which the alloc operation has to be
|
|
/// inserted.
|
|
Operation *getAllocPosition() const { return allocPosition; }
|
|
|
|
/// Returns the dealloc position after which the dealloc operation has to be
|
|
/// inserted.
|
|
Operation *getDeallocPosition() const { return deallocPosition; }
|
|
|
|
private:
|
|
Operation *allocPosition;
|
|
Operation *deallocPosition;
|
|
};
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferPlacementAnalysis
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// The main buffer placement analysis used to place allocs and deallocs.
|
|
class BufferPlacementAnalysis {
|
|
public:
|
|
using DeallocSetT = SmallPtrSet<Operation *, 2>;
|
|
|
|
public:
|
|
BufferPlacementAnalysis(Operation *op)
|
|
: operation(op), liveness(op), dominators(op), postDominators(op),
|
|
aliases(op) {}
|
|
|
|
/// Computes the actual positions to place allocs and deallocs for the given
|
|
/// value.
|
|
BufferPlacementPositions
|
|
computeAllocAndDeallocPositions(OpResult result) const {
|
|
if (result.use_empty())
|
|
return BufferPlacementPositions(result.getOwner(), result.getOwner());
|
|
// Get all possible aliases.
|
|
auto possibleValues = aliases.resolve(result);
|
|
return BufferPlacementPositions(getAllocPosition(result, possibleValues),
|
|
getDeallocPosition(result, possibleValues));
|
|
}
|
|
|
|
/// Finds all associated dealloc nodes for the alloc nodes using alias
|
|
/// information.
|
|
DeallocSetT findAssociatedDeallocs(AllocOp alloc) const {
|
|
DeallocSetT result;
|
|
auto possibleValues = aliases.resolve(alloc);
|
|
for (Value alias : possibleValues)
|
|
for (Operation *user : alias.getUsers()) {
|
|
if (isa<DeallocOp>(user))
|
|
result.insert(user);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// Dumps the buffer placement information to the given stream.
|
|
void print(raw_ostream &os) const {
|
|
os << "// ---- Buffer Placement -----\n";
|
|
|
|
for (Region ®ion : operation->getRegions())
|
|
for (Block &block : region)
|
|
for (Operation &operation : block)
|
|
for (OpResult result : operation.getResults()) {
|
|
BufferPlacementPositions positions =
|
|
computeAllocAndDeallocPositions(result);
|
|
os << "Positions for ";
|
|
result.print(os);
|
|
os << "\n Alloc: ";
|
|
positions.getAllocPosition()->print(os);
|
|
os << "\n Dealloc: ";
|
|
positions.getDeallocPosition()->print(os);
|
|
os << "\n";
|
|
}
|
|
}
|
|
|
|
private:
|
|
/// Finds a correct placement block to store alloc/dealloc node according to
|
|
/// the algorithm described at the top of the file. It supports dominator and
|
|
/// post-dominator analyses via template arguments.
|
|
template <typename DominatorT>
|
|
Block *
|
|
findPlacementBlock(OpResult result,
|
|
const BufferPlacementAliasAnalysis::ValueSetT &aliases,
|
|
const DominatorT &doms) const {
|
|
// Start with the current block the value is defined in.
|
|
Block *dom = result.getOwner()->getBlock();
|
|
// Iterate over all aliases and their uses to find a safe placement block
|
|
// according to the given dominator information.
|
|
for (Value alias : aliases)
|
|
for (Operation *user : alias.getUsers()) {
|
|
// Move upwards in the dominator tree to find an appropriate
|
|
// dominator block that takes the current use into account.
|
|
dom = doms.findNearestCommonDominator(dom, user->getBlock());
|
|
}
|
|
return dom;
|
|
}
|
|
|
|
/// Finds a correct alloc position according to the algorithm described at
|
|
/// the top of the file.
|
|
Operation *getAllocPosition(
|
|
OpResult result,
|
|
const BufferPlacementAliasAnalysis::ValueSetT &aliases) const {
|
|
// Determine the actual block to place the alloc and get liveness
|
|
// information.
|
|
Block *placementBlock = findPlacementBlock(result, aliases, dominators);
|
|
const LivenessBlockInfo *livenessInfo =
|
|
liveness.getLiveness(placementBlock);
|
|
|
|
// We have to ensure that the alloc will be before the first use of all
|
|
// aliases of the given value. We first assume that there are no uses in the
|
|
// placementBlock and that we can safely place the alloc before the
|
|
// terminator at the end of the block.
|
|
Operation *startOperation = placementBlock->getTerminator();
|
|
// Iterate over all aliases and ensure that the startOperation will point to
|
|
// the first operation of all potential aliases in the placementBlock.
|
|
for (Value alias : aliases) {
|
|
Operation *aliasStartOperation = livenessInfo->getStartOperation(alias);
|
|
// Check whether the aliasStartOperation lies in the desired block and
|
|
// whether it is before the current startOperation. If yes, this will be
|
|
// the new startOperation.
|
|
if (aliasStartOperation->getBlock() == placementBlock &&
|
|
aliasStartOperation->isBeforeInBlock(startOperation))
|
|
startOperation = aliasStartOperation;
|
|
}
|
|
// startOperation is the first operation before which we can safely store
|
|
// the alloc taking all potential aliases into account.
|
|
return startOperation;
|
|
}
|
|
|
|
/// Finds a correct dealloc position according to the algorithm described at
|
|
/// the top of the file.
|
|
Operation *getDeallocPosition(
|
|
OpResult result,
|
|
const BufferPlacementAliasAnalysis::ValueSetT &aliases) const {
|
|
// Determine the actual block to place the dealloc and get liveness
|
|
// information.
|
|
Block *placementBlock = findPlacementBlock(result, aliases, postDominators);
|
|
const LivenessBlockInfo *livenessInfo =
|
|
liveness.getLiveness(placementBlock);
|
|
|
|
// We have to ensure that the dealloc will be after the last use of all
|
|
// aliases of the given value. We first assume that there are no uses in the
|
|
// placementBlock and that we can safely place the dealloc at the beginning.
|
|
Operation *endOperation = &placementBlock->front();
|
|
// Iterate over all aliases and ensure that the endOperation will point to
|
|
// the last operation of all potential aliases in the placementBlock.
|
|
for (Value alias : aliases) {
|
|
Operation *aliasEndOperation =
|
|
livenessInfo->getEndOperation(alias, endOperation);
|
|
// Check whether the aliasEndOperation lies in the desired block and
|
|
// whether it is behind the current endOperation. If yes, this will be the
|
|
// new endOperation.
|
|
if (aliasEndOperation->getBlock() == placementBlock &&
|
|
endOperation->isBeforeInBlock(aliasEndOperation))
|
|
endOperation = aliasEndOperation;
|
|
}
|
|
// endOperation is the last operation behind which we can safely store the
|
|
// dealloc taking all potential aliases into account.
|
|
return endOperation;
|
|
}
|
|
|
|
/// The operation this transformation was constructed from.
|
|
Operation *operation;
|
|
|
|
/// The underlying liveness analysis to compute fine grained information about
|
|
/// alloc and dealloc positions.
|
|
Liveness liveness;
|
|
|
|
/// The dominator analysis to place allocs in the appropriate blocks.
|
|
DominanceInfo dominators;
|
|
|
|
/// The post dominator analysis to place deallocs in the appropriate blocks.
|
|
PostDominanceInfo postDominators;
|
|
|
|
/// The internal alias analysis to ensure that allocs and deallocs take all
|
|
/// their potential aliases into account.
|
|
BufferPlacementAliasAnalysis aliases;
|
|
};
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferPlacementPass
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// The actual buffer placement pass that moves alloc and dealloc nodes into
|
|
/// the right positions. It uses the algorithm described at the top of the file.
|
|
// TODO: create a templated version that allows to match dialect-specific
|
|
// alloc/dealloc nodes and to insert dialect-specific dealloc node.
|
|
struct BufferPlacementPass
|
|
: mlir::PassWrapper<BufferPlacementPass, FunctionPass> {
|
|
void runOnFunction() override {
|
|
// Get required analysis information first.
|
|
auto &analysis = getAnalysis<BufferPlacementAnalysis>();
|
|
|
|
// Compute an initial placement of all nodes.
|
|
llvm::SmallDenseMap<Value, BufferPlacementPositions, 16> placements;
|
|
getFunction().walk([&](AllocOp alloc) {
|
|
placements[alloc] = analysis.computeAllocAndDeallocPositions(
|
|
alloc.getOperation()->getResult(0));
|
|
return WalkResult::advance();
|
|
});
|
|
|
|
// Move alloc (and dealloc - if any) nodes into the right places
|
|
// and insert dealloc nodes if necessary.
|
|
getFunction().walk([&](AllocOp alloc) {
|
|
// Find already associated dealloc nodes.
|
|
auto deallocs = analysis.findAssociatedDeallocs(alloc);
|
|
if (deallocs.size() > 1) {
|
|
emitError(alloc.getLoc(),
|
|
"Not supported number of associated dealloc operations");
|
|
return WalkResult::interrupt();
|
|
}
|
|
|
|
// Move alloc node to the right place.
|
|
BufferPlacementPositions &positions = placements[alloc];
|
|
Operation *allocOperation = alloc.getOperation();
|
|
allocOperation->moveBefore(positions.getAllocPosition());
|
|
|
|
// If there is an existing dealloc, move it to the right place.
|
|
if (deallocs.size()) {
|
|
Operation *nextOp = positions.getDeallocPosition()->getNextNode();
|
|
assert(nextOp && "Invalid Dealloc operation position");
|
|
(*deallocs.begin())->moveBefore(nextOp);
|
|
} else {
|
|
// If there is no dealloc node, insert one in the right place.
|
|
OpBuilder builder(alloc);
|
|
builder.setInsertionPointAfter(positions.getDeallocPosition());
|
|
builder.create<DeallocOp>(allocOperation->getLoc(), alloc);
|
|
}
|
|
return WalkResult::advance();
|
|
});
|
|
};
|
|
};
|
|
|
|
} // end anonymous namespace
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferAssignmentPlacer
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// Creates a new assignment placer.
|
|
BufferAssignmentPlacer::BufferAssignmentPlacer(Operation *op) : operation(op) {}
|
|
|
|
/// Computes the actual position to place allocs for the given value.
|
|
OpBuilder::InsertPoint
|
|
BufferAssignmentPlacer::computeAllocPosition(OpResult result) {
|
|
Operation *owner = result.getOwner();
|
|
return OpBuilder::InsertPoint(owner->getBlock(), Block::iterator(owner));
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// FunctionAndBlockSignatureConverter
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
// Performs the actual signature rewriting step.
|
|
LogicalResult FunctionAndBlockSignatureConverter::matchAndRewrite(
|
|
FuncOp funcOp, ArrayRef<Value> operands,
|
|
ConversionPatternRewriter &rewriter) const {
|
|
if (!converter) {
|
|
funcOp.emitError("The type converter has not been defined for "
|
|
"FunctionAndBlockSignatureConverter");
|
|
return failure();
|
|
}
|
|
// Converting shaped type arguments to memref type.
|
|
auto funcType = funcOp.getType();
|
|
TypeConverter::SignatureConversion conversion(funcType.getNumInputs());
|
|
for (auto argType : llvm::enumerate(funcType.getInputs()))
|
|
conversion.addInputs(argType.index(),
|
|
converter->convertType(argType.value()));
|
|
// Adding function results to the arguments of the converted function as
|
|
// memref type. The converted function will be a void function.
|
|
for (Type resType : funcType.getResults())
|
|
conversion.addInputs(converter->convertType((resType)));
|
|
rewriter.updateRootInPlace(funcOp, [&] {
|
|
funcOp.setType(
|
|
rewriter.getFunctionType(conversion.getConvertedTypes(), llvm::None));
|
|
rewriter.applySignatureConversion(&funcOp.getBody(), conversion);
|
|
});
|
|
return success();
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferAssignmentTypeConverter
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
/// Registers conversions into BufferAssignmentTypeConverter
|
|
BufferAssignmentTypeConverter::BufferAssignmentTypeConverter() {
|
|
// Keep all types unchanged.
|
|
addConversion([](Type type) { return type; });
|
|
// A type conversion that converts ranked-tensor type to memref type.
|
|
addConversion([](RankedTensorType type) {
|
|
return (Type)MemRefType::get(type.getShape(), type.getElementType());
|
|
});
|
|
}
|
|
|
|
//===----------------------------------------------------------------------===//
|
|
// BufferPlacementPass construction
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
std::unique_ptr<Pass> mlir::createBufferPlacementPass() {
|
|
return std::make_unique<BufferPlacementPass>();
|
|
}
|