Program Listing for File CollapseUnitDims.cpp
↰ Return to documentation for file (lib/Dialect/DFG/Transforms/CollapseUnitDims.cpp)
#include "laksa-mlir/Dialect/DFG/IR/DFG.h"
#include "laksa-mlir/Dialect/DFG/IR/DFGOps.h"
#include "laksa-mlir/Dialect/DFG/Transforms/Passes.h"
#include "laksa-mlir/Dialect/EmitHLS/IR/EmitHLS.h"
#include "laksa-mlir/IR/LaksaAttributes.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/IR/IRMapping.h"
#include "mlir/IR/PatternMatch.h"
#include "mlir/Pass/Pass.h"
#include <llvm/ADT/DenseMap.h>
#include <llvm/ADT/STLExtras.h>
#include <llvm/ADT/SmallVector.h>
#include <llvm/Support/Debug.h>
#define DEBUG_TYPE "dfg-collapse-unit-dims"
#define LAKSA_DEBUG(X) \
LLVM_DEBUG( \
llvm::dbgs() << "[dfg-collapse-unit-dims] "; X; llvm::dbgs() << "\n")
using namespace mlir;
using namespace mlir::dfg;
namespace mlir {
namespace dfg {
#define GEN_PASS_DEF_DFGCOLLAPSEUNITDIMS
#include "laksa-mlir/Dialect/DFG/Transforms/Passes.h.inc"
} // namespace dfg
} // namespace mlir
namespace {
// Old-axis indices that survive the collapse, in order.
using KeptAxes = SmallVector<int64_t>;
static KeptAxes computeKeptAxes(ArrayRef<int64_t> shape)
{
KeptAxes kept;
for (auto [axis, size] : llvm::enumerate(shape))
if (size != 1) kept.push_back(static_cast<int64_t>(axis));
return kept;
}
static void setIdentity(KeptAxes &kept, int64_t rank)
{
kept.clear();
kept.reserve(rank);
for (int64_t axis = 0; axis < rank; ++axis) kept.push_back(axis);
}
static bool isIdentity(const KeptAxes &kept, int64_t rank)
{
if (static_cast<int64_t>(kept.size()) != rank) return false;
for (auto [axis, value] : llvm::enumerate(kept))
if (static_cast<int64_t>(axis) != value) return false;
return true;
}
// Picks the kept-axis entries , remapping each one through mapper
static SmallVector<Value>
mapAndSelect(IRMapping &mapper, ValueRange values, const KeptAxes &kept)
{
SmallVector<Value> result;
result.reserve(kept.size());
for (int64_t axis : kept)
result.push_back(mapper.lookupOrDefault(values[axis]));
return result;
}
static SmallVector<int32_t>
selectInt32(ArrayRef<int32_t> values, const KeptAxes &kept)
{
SmallVector<int32_t> result;
result.reserve(kept.size());
for (int64_t axis : kept) result.push_back(values[axis]);
return result;
}
// Drops axes from a ShapedType (memref or emithls::ArrayType)
static Type dropAxes(Type type, const KeptAxes &kept)
{
auto shaped = cast<ShapedType>(type);
SmallVector<int64_t> newShape;
newShape.reserve(kept.size());
for (int64_t axis : kept) newShape.push_back(shaped.getShape()[axis]);
// rebuild new memref
if (auto memrefTy = dyn_cast<MemRefType>(type))
return MemRefType::get(newShape, memrefTy.getElementType());
return shaped.clone(newShape);
}
// Drops axes from a dfg::OutputType/InputType port, collapsing to a scalar port
// when every axis is dropped.
template<typename PortTy>
static PortTy dropAxesPort(PortTy port, const KeptAxes &kept)
{
if (kept.empty()) return PortTy::get(port.getElementType());
SmallVector<int64_t> newShape;
newShape.reserve(kept.size());
for (int64_t axis : kept) newShape.push_back(port.getShape()[axis]);
return PortTy::get(newShape, port.getElementType());
}
static unsigned findPortIndex(ValueRange ports, Value v)
{
for (auto [index, candidate] : llvm::enumerate(ports))
if (candidate == v) return index;
llvm_unreachable("port not found in its own port list");
}
static SmallVector<Value> mapValues(IRMapping &mapper, ValueRange values)
{
return llvm::map_to_vector(values, [&](Value v) {
return mapper.lookupOrDefault(v);
});
}
// Per-process analysis
struct ProcessCollapsePlan {
SmallVector<KeptAxes> inputKept; // parallel to op.getInputPorts()
SmallVector<KeptAxes> outputKept; // parallel to op.getOutputPorts()
DenseMap<Operation*, KeptAxes> allocKept;
bool anyDrop = false;
};
// Conservatively verifies that every op consuming a port-derived shaped value
// is one this pass knows how to rewrite.
static bool isSupportedProcessBody(ProcessOp op)
{
DenseSet<Value> tainted;
for (Value v : op.getInputPorts()) tainted.insert(v);
for (Value v : op.getOutputPorts()) tainted.insert(v);
bool ok = true;
op.walk([&](Operation* o) {
if (o == op.getOperation()) return WalkResult::advance();
bool touchesTainted = llvm::any_of(o->getOperands(), [&](Value v) {
return tainted.contains(v);
});
if (!touchesTainted) return WalkResult::advance();
if (isa<LoopOp,
emithls::ForOp,
emithls::IfOp,
emithls::ExpressionOp,
emithls::YieldOp,
PullOp,
PushOp,
PushMemRefOp,
emithls::HelperAccumulateOp,
emithls::ArrayReadOp,
emithls::ArrayWriteOp>(o))
return WalkResult::advance();
if (auto pull = dyn_cast<PullAsMemRefOp>(o)) {
tainted.insert(pull.getResult());
return WalkResult::advance();
}
if (auto linebuf = dyn_cast<emithls::HelperLineBufferOp>(o)) {
auto tokenRank =
cast<ShapedType>(linebuf.getTokenRef().getType()).getRank();
if (static_cast<int64_t>(linebuf.getNumChan().size())
!= tokenRank) {
linebuf.emitRemark(
"dfg-collapse-unit-dims: num_chan does not cover the "
"full rank of its token operand; process left "
"unchanged");
ok = false;
return WalkResult::interrupt();
}
tainted.insert(linebuf.getBufRef());
return WalkResult::advance();
}
if (auto window = dyn_cast<emithls::HelperWindowOp>(o)) {
tainted.insert(window.getWinRef());
return WalkResult::advance();
}
o->emitRemark(
"dfg-collapse-unit-dims: unsupported op consumes a port-derived "
"shaped value; process left unchanged");
ok = false;
return WalkResult::interrupt();
});
return ok;
}
static LogicalResult buildAllocKeptMap(
ProcessOp op,
ValueRange outputPorts,
ArrayRef<KeptAxes> outputKept,
DenseMap<Operation*, KeptAxes> &allocKept)
{
LogicalResult result = success();
op.walk([&](PushMemRefOp pushOp) {
unsigned idx = findPortIndex(outputPorts, pushOp.getWritePort());
int64_t rank = static_cast<int64_t>(
cast<InputType>(pushOp.getWritePort().getType()).getShape().size());
if (isIdentity(outputKept[idx], rank)) return WalkResult::advance();
Value tokenMemref = pushOp.getTokenMemref();
if (tokenMemref.getDefiningOp<PullAsMemRefOp>())
return WalkResult::advance();
auto allocOp = tokenMemref.getDefiningOp<memref::AllocOp>();
if (!allocOp) {
pushOp.emitRemark(
"dfg-collapse-unit-dims: push_memref operand is not a "
"directly-defined memref.alloc or dfg.pull_as_memref; process "
"left unchanged");
result = failure();
return WalkResult::interrupt();
}
allocKept[allocOp.getOperation()] = outputKept[idx];
return WalkResult::advance();
});
return result;
}
static FailureOr<ProcessCollapsePlan> analyzeProcess(ProcessOp op)
{
LAKSA_DEBUG(
llvm::dbgs() << "Analyzing process \"" << op.getNodeName() << "\"");
ProcessCollapsePlan plan;
FunctionType fnType = op.getFunctionType();
for (Type t : fnType.getInputs())
plan.inputKept.push_back(
computeKeptAxes(cast<OutputType>(t).getShape()));
for (Type t : fnType.getResults())
plan.outputKept.push_back(
computeKeptAxes(cast<InputType>(t).getShape()));
SmallVector<Value> inputPorts = op.getInputPorts();
SmallVector<Value> outputPorts = op.getOutputPorts();
// A port ever pulled/pushed as a whole memref cannot collapse to a scalar
op.walk([&](PullAsMemRefOp pull) {
unsigned idx = findPortIndex(inputPorts, pull.getReadPort());
if (plan.inputKept[idx].empty())
setIdentity(
plan.inputKept[idx],
static_cast<int64_t>(
cast<OutputType>(fnType.getInput(idx)).getShape().size()));
});
op.walk([&](PushMemRefOp push) {
unsigned idx = findPortIndex(outputPorts, push.getWritePort());
if (plan.outputKept[idx].empty())
setIdentity(
plan.outputKept[idx],
static_cast<int64_t>(
cast<InputType>(fnType.getResult(idx)).getShape().size()));
});
for (auto [i, kept] : llvm::enumerate(plan.inputKept))
if (!isIdentity(
kept,
static_cast<int64_t>(
cast<OutputType>(fnType.getInput(i)).getShape().size())))
plan.anyDrop = true;
for (auto [i, kept] : llvm::enumerate(plan.outputKept))
if (!isIdentity(
kept,
static_cast<int64_t>(
cast<InputType>(fnType.getResult(i)).getShape().size())))
plan.anyDrop = true;
// A linebuf's own num_chan can carry a trivial single-channel entry that
// has nothing to do with any port shape.
if (!plan.anyDrop) {
op.walk([&](emithls::HelperLineBufferOp linebuf) {
for (Attribute a : linebuf.getNumChan())
if (cast<IntegerAttr>(a).getInt() == 1) plan.anyDrop = true;
});
}
if (!plan.anyDrop) {
LAKSA_DEBUG(
llvm::dbgs() << " no unit-size axes found, nothing to collapse");
return plan;
}
if (!isSupportedProcessBody(op)) {
LAKSA_DEBUG(
llvm::dbgs() << " body contains unsupported ops, skipping");
return failure();
}
if (failed(buildAllocKeptMap(
op,
outputPorts,
plan.outputKept,
plan.allocKept))) {
LAKSA_DEBUG(llvm::dbgs() << " alloc bookkeeping incomplete, skipping");
return failure();
}
LAKSA_DEBUG(llvm::dbgs() << " plan accepted");
return plan;
}
// Per-process body rewrite
class ProcessBodyRewriter {
public:
explicit ProcessBodyRewriter(const ProcessCollapsePlan &plan) : plan(plan)
{}
ProcessOp rewriteProcess(ProcessOp op, IRRewriter &rewriter);
private:
void rewriteBlock(Block &oldBlock, OpBuilder &b);
void rewritePull(PullOp old, OpBuilder &b);
void rewritePush(PushOp old, OpBuilder &b);
void rewritePullAsMemRef(PullAsMemRefOp old, OpBuilder &b);
void rewritePushMemRef(PushMemRefOp old, OpBuilder &b);
void rewriteLinebuf(emithls::HelperLineBufferOp old, OpBuilder &b);
void rewriteWindow(emithls::HelperWindowOp old, OpBuilder &b);
void rewriteArrayRead(emithls::ArrayReadOp old, OpBuilder &b);
void rewriteArrayWrite(emithls::ArrayWriteOp old, OpBuilder &b);
void rewriteAccumulate(emithls::HelperAccumulateOp old, OpBuilder &b);
void rewriteAlloc(memref::AllocOp old, OpBuilder &b);
KeptAxes traceOrIdentity(Value v) const;
const ProcessCollapsePlan &plan;
IRMapping mapper;
DenseMap<Value, KeptAxes> trace;
SmallVector<Value> oldInputPorts;
SmallVector<Value> oldOutputPorts;
};
KeptAxes ProcessBodyRewriter::traceOrIdentity(Value v) const
{
auto it = trace.find(v);
if (it != trace.end()) return it->second;
KeptAxes identity;
setIdentity(identity, cast<ShapedType>(v.getType()).getRank());
return identity;
}
void ProcessBodyRewriter::rewritePull(PullOp old, OpBuilder &b)
{
unsigned idx = findPortIndex(oldInputPorts, old.getReadPort());
const KeptAxes &kept = plan.inputKept[idx];
Value newPort = mapper.lookupOrDefault(old.getReadPort());
SmallVector<Value> newIndices =
mapAndSelect(mapper, old.getIndices(), kept);
auto newOp = PullOp::create(b, old.getLoc(), newPort, newIndices);
mapper.map(old.getResult(), newOp.getResult());
}
void ProcessBodyRewriter::rewritePush(PushOp old, OpBuilder &b)
{
unsigned idx = findPortIndex(oldOutputPorts, old.getWritePort());
const KeptAxes &kept = plan.outputKept[idx];
Value newToken = mapper.lookupOrDefault(old.getToken());
Value newPort = mapper.lookupOrDefault(old.getWritePort());
SmallVector<Value> newIndices =
mapAndSelect(mapper, old.getIndices(), kept);
PushOp::create(b, old.getLoc(), newToken, newPort, newIndices);
}
void ProcessBodyRewriter::rewritePullAsMemRef(PullAsMemRefOp old, OpBuilder &b)
{
unsigned idx = findPortIndex(oldInputPorts, old.getReadPort());
const KeptAxes &kept = plan.inputKept[idx];
Value newPort = mapper.lookupOrDefault(old.getReadPort());
auto newOp = PullAsMemRefOp::create(b, old.getLoc(), newPort);
mapper.map(old.getResult(), newOp.getResult());
trace[old.getResult()] = kept;
}
void ProcessBodyRewriter::rewritePushMemRef(PushMemRefOp old, OpBuilder &b)
{
Value newMemref = mapper.lookupOrDefault(old.getTokenMemref());
Value newPort = mapper.lookupOrDefault(old.getWritePort());
PushMemRefOp::create(b, old.getLoc(), newMemref, newPort);
}
void ProcessBodyRewriter::rewriteLinebuf(
emithls::HelperLineBufferOp old,
OpBuilder &b)
{
Value oldTokenRef = old.getTokenRef();
KeptAxes tokKept = traceOrIdentity(oldTokenRef);
int64_t oldTokenRank = cast<ShapedType>(oldTokenRef.getType()).getRank();
int64_t oldResultRank =
cast<ShapedType>(old.getBufRef().getType()).getRank();
SmallVector<int32_t> oldNumChan;
for (Attribute a : old.getNumChan())
oldNumChan.push_back(
static_cast<int32_t>(cast<IntegerAttr>(a).getInt()));
// A channel count of 1 is redundant on its own, whether or not the token
// itself was already narrowed by port-shape tracing, drop it here too so
// num_chan and the linebuf's own result shape never carry a dead
// single-channel axis.
KeptAxes chanKept;
for (int64_t pos : tokKept)
if (oldNumChan[pos] != 1) chanKept.push_back(pos);
KeptAxes resultKept = chanKept;
for (int64_t axis = oldTokenRank; axis < oldResultRank; ++axis)
resultKept.push_back(axis);
SmallVector<int32_t> newNumChan = selectInt32(oldNumChan, chanKept);
Value newTokenRef = mapper.lookupOrDefault(oldTokenRef);
Value newIndex =
old.getIndex() ? mapper.lookupOrDefault(old.getIndex()) : Value();
Type newResultTy = dropAxes(old.getBufRef().getType(), resultKept);
auto newOp = emithls::HelperLineBufferOp::create(
b,
old.getLoc(),
newTokenRef,
newIndex,
newResultTy,
newNumChan,
old.getNumLine());
mapper.map(old.getBufRef(), newOp.getBufRef());
trace[old.getBufRef()] = resultKept;
}
void ProcessBodyRewriter::rewriteWindow(
emithls::HelperWindowOp old,
OpBuilder &b)
{
Value oldBufRef = old.getBufRef();
KeptAxes bufKept = traceOrIdentity(oldBufRef);
Value newBufRef = mapper.lookupOrDefault(oldBufRef);
// Window indices are spatial offsets, not one-per-axis, so they never need
// a dropped entry
SmallVector<Value> newIndices = mapValues(mapper, old.getIndices());
Type newWinTy = dropAxes(old.getWinRef().getType(), bufKept);
auto newOp = emithls::HelperWindowOp::create(
b,
old.getLoc(),
newBufRef,
newIndices,
newWinTy);
mapper.map(old.getWinRef(), newOp.getWinRef());
trace[old.getWinRef()] = bufKept;
}
void ProcessBodyRewriter::rewriteArrayRead(
emithls::ArrayReadOp old,
OpBuilder &b)
{
Value oldArray = old.getArray();
KeptAxes arrKept = traceOrIdentity(oldArray);
int64_t rank = cast<ShapedType>(oldArray.getType()).getRank();
if (isIdentity(arrKept, rank)) {
b.clone(*old.getOperation(), mapper);
return;
}
Value newArray = mapper.lookupOrDefault(oldArray);
SmallVector<Value> newIndices =
mapAndSelect(mapper, old.getIndices(), arrKept);
auto newOp = emithls::ArrayReadOp::create(
b,
old.getLoc(),
old.getResult().getType(),
newArray,
newIndices);
mapper.map(old.getResult(), newOp.getResult());
}
void ProcessBodyRewriter::rewriteArrayWrite(
emithls::ArrayWriteOp old,
OpBuilder &b)
{
Value oldArray = old.getArray();
KeptAxes arrKept = traceOrIdentity(oldArray);
int64_t rank = cast<ShapedType>(oldArray.getType()).getRank();
if (isIdentity(arrKept, rank)) {
b.clone(*old.getOperation(), mapper);
return;
}
Value newValue = mapper.lookupOrDefault(old.getValue());
Value newArray = mapper.lookupOrDefault(oldArray);
SmallVector<Value> newIndices =
mapAndSelect(mapper, old.getIndices(), arrKept);
emithls::ArrayWriteOp::create(
b,
old.getLoc(),
newValue,
newArray,
newIndices);
}
void ProcessBodyRewriter::rewriteAccumulate(
emithls::HelperAccumulateOp old,
OpBuilder &b)
{
Value oldAccuRef = old.getAccuRef();
KeptAxes accuKept = traceOrIdentity(oldAccuRef);
int64_t rank = cast<ShapedType>(oldAccuRef.getType()).getRank();
if (isIdentity(accuKept, rank)) {
b.clone(*old.getOperation(), mapper);
return;
}
Value newAccuRef = mapper.lookupOrDefault(oldAccuRef);
Value newValue = mapper.lookupOrDefault(old.getValue());
SmallVector<Value> newIndices =
mapAndSelect(mapper, old.getIndices(), accuKept);
emithls::HelperAccumulateOp::create(
b,
old.getLoc(),
newAccuRef,
newIndices,
old.getOpCode(),
newValue);
}
void ProcessBodyRewriter::rewriteAlloc(memref::AllocOp old, OpBuilder &b)
{
auto it = plan.allocKept.find(old.getOperation());
int64_t rank = old.getType().getRank();
if (it == plan.allocKept.end() || isIdentity(it->second, rank)) {
b.clone(*old.getOperation(), mapper);
return;
}
Type newType = dropAxes(old.getType(), it->second);
auto newOp =
memref::AllocOp::create(b, old.getLoc(), cast<MemRefType>(newType));
newOp->setAttrs(old->getAttrs());
mapper.map(old.getResult(), newOp.getResult());
trace[old.getResult()] = it->second;
}
void ProcessBodyRewriter::rewriteBlock(Block &oldBlock, OpBuilder &b)
{
for (Operation &oldOp : oldBlock) {
if (auto forOp = dyn_cast<emithls::ForOp>(&oldOp)) {
emithls::ForOp::create(
b,
forOp.getLoc(),
forOp.getLowerBoundAttr(),
forOp.getUpperBoundAttr(),
forOp.getStepAttr(),
[&](OpBuilder &nb, Location, ValueRange ivs) {
mapper.map(forOp.getInductionVariable(), ivs.front());
rewriteBlock(forOp.getBody().front(), nb);
});
} else if (auto ifOp = dyn_cast<emithls::IfOp>(&oldOp)) {
Value newCond = mapper.lookupOrDefault(ifOp.getCondition());
bool hasElse = !ifOp.getElseRegion().empty();
emithls::IfOp::create(
b,
ifOp.getLoc(),
newCond,
[&](OpBuilder &nb, Location) {
rewriteBlock(ifOp.getThenRegion().front(), nb);
},
hasElse
? function_ref<void(OpBuilder &, Location)>(
[&](OpBuilder &nb, Location) {
rewriteBlock(ifOp.getElseRegion().front(), nb);
})
: function_ref<void(OpBuilder &, Location)>(nullptr));
} else if (auto exprOp = dyn_cast<emithls::ExpressionOp>(&oldOp)) {
auto newOp = emithls::ExpressionOp::create(
b,
exprOp.getLoc(),
exprOp.getResult().getType(),
[&](OpBuilder &nb, Location) {
rewriteBlock(exprOp.getBody().front(), nb);
});
mapper.map(exprOp.getResult(), newOp.getResult());
} else if (auto pullOp = dyn_cast<PullOp>(&oldOp)) {
rewritePull(pullOp, b);
} else if (auto pushOp = dyn_cast<PushOp>(&oldOp)) {
rewritePush(pushOp, b);
} else if (auto pullMemrefOp = dyn_cast<PullAsMemRefOp>(&oldOp)) {
rewritePullAsMemRef(pullMemrefOp, b);
} else if (auto pushMemrefOp = dyn_cast<PushMemRefOp>(&oldOp)) {
rewritePushMemRef(pushMemrefOp, b);
} else if (
auto linebufOp = dyn_cast<emithls::HelperLineBufferOp>(&oldOp)) {
rewriteLinebuf(linebufOp, b);
} else if (auto windowOp = dyn_cast<emithls::HelperWindowOp>(&oldOp)) {
rewriteWindow(windowOp, b);
} else if (auto accOp = dyn_cast<emithls::HelperAccumulateOp>(&oldOp)) {
rewriteAccumulate(accOp, b);
} else if (auto readOp = dyn_cast<emithls::ArrayReadOp>(&oldOp)) {
rewriteArrayRead(readOp, b);
} else if (auto writeOp = dyn_cast<emithls::ArrayWriteOp>(&oldOp)) {
rewriteArrayWrite(writeOp, b);
} else if (auto allocOp = dyn_cast<memref::AllocOp>(&oldOp)) {
rewriteAlloc(allocOp, b);
} else {
b.clone(oldOp, mapper);
}
}
}
ProcessOp
ProcessBodyRewriter::rewriteProcess(ProcessOp op, IRRewriter &rewriter)
{
LAKSA_DEBUG(
llvm::dbgs()
<< "Collapsing unit dims in process \"" << op.getNodeName() << "\"");
FunctionType fnType = op.getFunctionType();
SmallVector<Type> newInputs, newOutputs;
for (auto [i, ty] : llvm::enumerate(fnType.getInputs()))
newInputs.push_back(
dropAxesPort(cast<OutputType>(ty), plan.inputKept[i]));
for (auto [i, ty] : llvm::enumerate(fnType.getResults()))
newOutputs.push_back(
dropAxesPort(cast<InputType>(ty), plan.outputKept[i]));
auto newFnType = rewriter.getFunctionType(newInputs, newOutputs);
LAKSA_DEBUG(llvm::dbgs() << " new signature: " << newFnType);
rewriter.setInsertionPoint(op);
auto newProcessOp = ProcessOp::create(
rewriter,
op.getLoc(),
op.getSymName(),
newFnType,
op.getMultiplicity());
oldInputPorts = op.getInputPorts();
oldOutputPorts = op.getOutputPorts();
for (auto [oldArg, newArg] : llvm::zip(
op.getBody().getArguments(),
newProcessOp.getBody().getArguments()))
mapper.map(oldArg, newArg);
rewriter.setInsertionPointToStart(&newProcessOp.getBody().front());
for (Operation &top : op.getBody().front()) {
if (auto loopOp = dyn_cast<LoopOp>(&top)) {
LoopOp::create(
rewriter,
loopOp.getLoc(),
newProcessOp.getInputPorts(),
newProcessOp.getOutputPorts(),
[&](OpBuilder &nb, Location) {
rewriteBlock(loopOp.getBody().front(), nb);
});
} else {
rewriter.clone(top, mapper);
}
}
LAKSA_DEBUG(llvm::dbgs() << " replaced process body");
rewriter.replaceOp(op, newProcessOp);
return newProcessOp;
}
// Region/channel wiring
//
// Topology and op order never change here, only per-port shapes shrink
// identically on both ends of a channel, so this walks the original body in
// place.
class RegionRewriter {
public:
explicit RegionRewriter(RegionOp op) : op(op) {}
LogicalResult rewrite(IRRewriter &rewriter);
private:
bool needsRewrite();
static FailureOr<InputType> resolveNewChannelType(ChannelOp channelOp);
RegionOp op;
};
// A channel's new type is derived from its producer only - both ends of a
// channel always agree, since axis-dropping is a pure function of a shape
// and a channel's two ports necessarily start out with the same shape.
FailureOr<InputType> RegionRewriter::resolveNewChannelType(ChannelOp channelOp)
{
auto producerInst =
dyn_cast<InstantiateOp>(channelOp.getInputConnectedOp());
if (!producerInst)
return channelOp.emitError(
"dfg-collapse-unit-dims: expected a channel to be connected to "
"a dfg.instantiate");
auto producerProcess =
dyn_cast_or_null<ProcessOp>(producerInst.getInstantiatedOperation());
if (!producerProcess)
return producerInst.emitError(
"dfg-collapse-unit-dims: expected a dfg.process to be "
"instantiated");
unsigned portIndex =
findPortIndex(producerInst.getOutputs(), channelOp.getInputPort());
return cast<InputType>(
producerProcess.getFunctionType().getResult(portIndex));
}
// Check instantiate op's signature
bool RegionRewriter::needsRewrite()
{
for (Operation* node : op.getGraphNodes()) {
auto instantiateOp = dyn_cast<InstantiateOp>(node);
if (!instantiateOp) continue;
auto processOp = dyn_cast_or_null<ProcessOp>(
instantiateOp.getInstantiatedOperation());
if (!processOp) continue;
FunctionType fnType = processOp.getFunctionType();
for (auto [i, v] : llvm::enumerate(instantiateOp.getInputs()))
if (v.getType() != fnType.getInput(i)) return true;
for (auto [i, v] : llvm::enumerate(instantiateOp.getOutputs()))
if (v.getType() != fnType.getResult(i)) return true;
}
return false;
}
LogicalResult RegionRewriter::rewrite(IRRewriter &rewriter)
{
if (!needsRewrite()) {
LAKSA_DEBUG(
llvm::dbgs()
<< "Region \"" << op.getNodeName() << "\" unchanged, skipping");
return success();
}
LAKSA_DEBUG(
llvm::dbgs() << "Rewiring region \"" << op.getNodeName()
<< "\" for collapsed process port shapes");
unsigned numBoundaryInputs = op.getNumInputPorts();
unsigned numBoundaryOutputs = op.getNumOutputPorts();
SmallVector<Type> newBoundaryInputTypes(numBoundaryInputs);
SmallVector<Type> newBoundaryOutputTypes(numBoundaryOutputs);
SmallVector<bool> boundaryInputFound(numBoundaryInputs, false);
SmallVector<bool> boundaryOutputFound(numBoundaryOutputs, false);
for (Operation* node : op.getGraphNodes()) {
auto instantiateOp = dyn_cast<InstantiateOp>(node);
if (!instantiateOp) continue;
auto processOp = dyn_cast_or_null<ProcessOp>(
instantiateOp.getInstantiatedOperation());
if (!processOp) continue;
FunctionType fnType = processOp.getFunctionType();
for (auto [i, v] : llvm::enumerate(instantiateOp.getInputs())) {
auto blockArg = dyn_cast<BlockArgument>(v);
if (!blockArg) continue;
newBoundaryInputTypes[blockArg.getArgNumber()] = fnType.getInput(i);
boundaryInputFound[blockArg.getArgNumber()] = true;
}
for (auto [j, v] : llvm::enumerate(instantiateOp.getOutputs())) {
auto blockArg = dyn_cast<BlockArgument>(v);
if (!blockArg) continue;
unsigned idx = blockArg.getArgNumber() - numBoundaryInputs;
newBoundaryOutputTypes[idx] = fnType.getResult(j);
boundaryOutputFound[idx] = true;
}
}
for (unsigned k = 0; k < numBoundaryInputs; ++k)
if (!boundaryInputFound[k])
return op.emitError() << "dfg-collapse-unit-dims: boundary input "
<< k << " is not connected to any process";
for (unsigned k = 0; k < numBoundaryOutputs; ++k)
if (!boundaryOutputFound[k])
return op.emitError() << "dfg-collapse-unit-dims: boundary output "
<< k << " is not connected to any process";
auto newRegionFnType =
rewriter.getFunctionType(newBoundaryInputTypes, newBoundaryOutputTypes);
LAKSA_DEBUG(
llvm::dbgs() << " new boundary signature: " << newRegionFnType);
rewriter.setInsertionPoint(op);
LogicalResult bodyResult = success();
auto newRegionOp = RegionOp::create(
rewriter,
op.getLoc(),
op.getSymName(),
newRegionFnType,
[&](OpBuilder &b, Location, ValueRange newBoundaryArgs) {
IRMapping mapper;
for (auto [oldArg, newArg] :
llvm::zip(op.getBody().getArguments(), newBoundaryArgs))
mapper.map(oldArg, newArg);
for (Operation &oldOp : op.getBody().front()) {
auto channelOp = dyn_cast<ChannelOp>(&oldOp);
if (!channelOp) {
b.clone(oldOp, mapper);
continue;
}
auto newTypeOrFailure = resolveNewChannelType(channelOp);
if (failed(newTypeOrFailure)) {
bodyResult = failure();
continue;
}
InputType newType = *newTypeOrFailure;
auto newChannelOp = ChannelOp::create(
b,
oldOp.getLoc(),
newType.getShape(),
newType.getElementType(),
channelOp.getBufferSize());
mapper.map(
channelOp.getInputPort(),
newChannelOp.getInputPort());
mapper.map(
channelOp.getOutputPort(),
newChannelOp.getOutputPort());
}
});
if (failed(bodyResult)) return failure();
LAKSA_DEBUG(llvm::dbgs() << " replaced region body");
if (Attribute rootAttr = op->getAttr(laksa::kRootAttrName))
newRegionOp->setAttr(laksa::kRootAttrName, rootAttr);
rewriter.replaceOp(op, newRegionOp);
return success();
}
struct DFGCollapseUnitDimsPass
: public dfg::impl::DFGCollapseUnitDimsBase<DFGCollapseUnitDimsPass> {
void runOnOperation() override
{
auto moduleOp = cast<ModuleOp>(getOperation());
IRRewriter rewriter(&getContext());
SmallVector<ProcessOp> processes;
moduleOp.walk([&](ProcessOp op) { processes.push_back(op); });
for (ProcessOp proc : processes) {
auto planOrFailure = analyzeProcess(proc);
if (failed(planOrFailure)) continue;
if (!planOrFailure->anyDrop) continue;
ProcessBodyRewriter(*planOrFailure).rewriteProcess(proc, rewriter);
}
SmallVector<RegionOp> regions;
moduleOp.walk([&](RegionOp op) { regions.push_back(op); });
for (RegionOp region : regions)
if (failed(RegionRewriter(region).rewrite(rewriter)))
return signalPassFailure();
}
};
} // namespace
std::unique_ptr<Pass> mlir::dfg::createDFGCollapseUnitDimsPass()
{ return std::make_unique<DFGCollapseUnitDimsPass>(); }