-
Notifications
You must be signed in to change notification settings - Fork 309
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
[HW] Add pass to outline certain ops: hw-outline-ops #7991
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -30,6 +30,26 @@ def PrintHWModuleGraph : Pass<"hw-print-module-graph", "mlir::ModuleOp"> { | |
]; | ||
} | ||
|
||
def HWOutlineOps : Pass<"hw-outline-ops", "mlir::ModuleOp"> { | ||
let summary = "Outlines operations into separate HW modules."; | ||
let description = [{ | ||
This pass outlines all of the selected operations into separate modules, | ||
uniquifying the modules. This is useful for debugging and cutting down RTL | ||
size for certain high level ops. | ||
|
||
Notes: | ||
- Does not support ops with regions. | ||
- In uniquifying the modules, dialect attributes are ignored. Only | ||
properties are respected. | ||
- Dialect attributes are, however, inherited by the module instances. | ||
}]; | ||
|
||
let options = [ | ||
ListOption<"opNames", "op-names", "std::string", | ||
"List of operation names to outline. If empty, this pass is a no-op."> | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to foolproof this, show an example as well (to remove ambiguity about whether or not to include the dialect name ( |
||
]; | ||
} | ||
|
||
def FlattenIO : Pass<"hw-flatten-io", "mlir::ModuleOp"> { | ||
let summary = "Flattens hw::Structure typed in- and output ports."; | ||
let constructor = "circt::hw::createFlattenIOPass()"; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,228 @@ | ||
//===- HWOutlineOps.cpp ---------------------------------------------------===// | ||
// | ||
// 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 | ||
// | ||
//===----------------------------------------------------------------------===// | ||
// | ||
// Pass which moves certain ops out of hw.module bodies into separate modules. | ||
// It also uniquifies them by types and attributes. | ||
// | ||
//===----------------------------------------------------------------------===// | ||
|
||
#include "circt/Dialect/HW/HWOps.h" | ||
#include "circt/Dialect/HW/HWPasses.h" | ||
#include "circt/Support/Namespace.h" | ||
#include "mlir/IR/Builders.h" | ||
#include "mlir/IR/OpImplementation.h" | ||
#include "mlir/IR/PatternMatch.h" | ||
#include "mlir/Pass/Pass.h" | ||
#include "mlir/Transforms/DialectConversion.h" | ||
|
||
#include <map> | ||
|
||
namespace circt { | ||
namespace hw { | ||
#define GEN_PASS_DEF_HWOUTLINEOPS | ||
#include "circt/Dialect/HW/Passes.h.inc" | ||
} // namespace hw | ||
} // namespace circt | ||
|
||
using namespace circt; | ||
using namespace hw; | ||
|
||
namespace { | ||
/// Information about an operation which has been marked for outlining. Used for | ||
/// both uniquing and creating the outlined module. | ||
struct OpInfo { | ||
OpInfo() = default; | ||
OpInfo(const OpInfo &other) = default; | ||
|
||
StringAttr name; | ||
ArrayAttr operandTypes; | ||
ArrayAttr resultTypes; | ||
DictionaryAttr attributes; | ||
}; | ||
|
||
/// Hash the OpInfo. | ||
struct OpInfoHash { | ||
size_t operator()(const OpInfo &opInfo) const { | ||
return llvm::hash_combine(opInfo.name, opInfo.operandTypes, | ||
opInfo.resultTypes, opInfo.attributes); | ||
} | ||
}; | ||
|
||
/// Compare two OpInfos. | ||
struct OpInfoEqual { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is this better than just specializing std::equal_to? |
||
size_t operator()(const OpInfo &a, const OpInfo &b) const { | ||
return a.name == b.name && a.operandTypes == b.operandTypes && | ||
a.resultTypes == b.resultTypes && a.attributes == b.attributes; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. On second thought, isn't this just the same as the default operator== that would be generated for the type and used by the default std::equal_to? |
||
} | ||
}; | ||
} // namespace | ||
|
||
namespace { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: member definitions don't need to be in a namespace. |
||
/// Create a module containing only the given operation and ports corresponding | ||
/// to its operands and results. | ||
hw::HWModuleOp buildOutlinedModule(OpInfo opInfo, Operation *op, | ||
Operation *top) { | ||
MLIRContext *context = top->getContext(); | ||
OpBuilder builder(context); | ||
SmallVector<PortInfo> ports; | ||
|
||
// Print the type identifier with a leading underscore and potentially | ||
// removing the leading '!'. | ||
auto emitTypeName = [](Type type, llvm::raw_ostream &os) { | ||
os << "_"; | ||
std::string typeName; | ||
llvm::raw_string_ostream(typeName) << type; | ||
if (typeName[0] == '!') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
os << StringRef(typeName).drop_front(1); | ||
else | ||
os << typeName; | ||
}; | ||
|
||
// Create the module name. | ||
std::string name; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. super nit: SmallString might be sizable to prevent allocation. |
||
llvm::raw_string_ostream nameOS(name); | ||
nameOS << "outlined_" << opInfo.name.getValue(); | ||
|
||
// Create the module input ports and append info to the module name. | ||
nameOS << "_opers"; | ||
for (auto type : opInfo.operandTypes.getAsRange<TypeAttr>()) { | ||
emitTypeName(type.getValue(), nameOS); | ||
// Unfortunately, we don't have access to the operand names here, so we just | ||
// let downstream generate a port name. | ||
ports.push_back(PortInfo{{builder.getStringAttr(""), type.getValue(), | ||
ModulePort::Direction::Input}}); | ||
} | ||
|
||
// Create the module output ports and append info to the module name. | ||
DenseMap<Value, StringRef> resultNames; | ||
// Get the result names from the op if it implements the OpAsmOpInterface. | ||
if (auto asmNames = dyn_cast<mlir::OpAsmOpInterface>(op)) | ||
asmNames.getAsmResultNames( | ||
[&](Value value, StringRef name) { resultNames[value] = name; }); | ||
nameOS << "_res"; | ||
llvm::SmallVector<Type, 16> resultTypes; | ||
for (auto [idx, type] : | ||
llvm::enumerate(opInfo.resultTypes.getAsRange<TypeAttr>())) { | ||
emitTypeName(type.getValue(), nameOS); | ||
resultTypes.push_back(type.getValue()); | ||
auto name = resultNames.lookup(op->getResult(idx)); | ||
if (name.empty()) | ||
name = ""; | ||
ports.push_back(PortInfo{{builder.getStringAttr(name), type.getValue(), | ||
ModulePort::Direction::Output}}); | ||
} | ||
|
||
// Append only the inherent attributes to the module name. | ||
nameOS << "_attrs"; | ||
if (auto regOp = op->getRegisteredInfo()) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: A bit misleading to call this |
||
DenseSet<StringAttr> opAttrNames; | ||
for (auto attrName : regOp->getAttributeNames()) | ||
opAttrNames.insert(attrName); | ||
|
||
for (auto attr : opInfo.attributes) { | ||
if (!opAttrNames.contains(attr.getName())) | ||
continue; | ||
nameOS << "_" << attr.getName().strref() << "_"; | ||
|
||
// Print some attribute values specially. | ||
if (auto i = dyn_cast<IntegerAttr>(attr.getValue())) | ||
nameOS << i.getValue(); | ||
else | ||
nameOS << attr.getValue(); | ||
} | ||
} | ||
|
||
// Create a module for the outlined operation. | ||
builder.setInsertionPointToEnd(&top->getRegion(0).front()); | ||
auto module = builder.create<hw::HWModuleOp>( | ||
top->getLoc(), builder.getStringAttr(name), ports); | ||
auto *body = module.getBodyBlock(); | ||
|
||
// Create the outlined operation in the module. | ||
builder.setInsertionPointToStart(body); | ||
OperationState state(top->getLoc(), opInfo.name.getValue(), | ||
body->getArguments(), resultTypes, | ||
opInfo.attributes.getValue()); | ||
Operation *outlinedOp = builder.create(state); | ||
|
||
// Set the hw.outputs to the results of the outlined operation. | ||
body->getTerminator()->setOperands(outlinedOp->getResults()); | ||
|
||
return module; | ||
} | ||
} // namespace | ||
|
||
namespace { | ||
class HWOutlineOpsPass : public impl::HWOutlineOpsBase<HWOutlineOpsPass> { | ||
public: | ||
using HWOutlineOpsBase::HWOutlineOpsBase; | ||
void runOnOperation() override; | ||
}; | ||
} // namespace | ||
|
||
void HWOutlineOpsPass::runOnOperation() { | ||
auto module = getOperation(); | ||
MLIRContext *context = &getContext(); | ||
|
||
StringSet<> opNamesToOutline; | ||
for (auto opName : opNames) | ||
opNamesToOutline.insert(opName); | ||
|
||
OpBuilder builder(context); | ||
std::unordered_map<OpInfo, hw::HWModuleOp, OpInfoHash, OpInfoEqual> | ||
outlinedModules; | ||
DenseSet<hw::HWModuleOp> outlinedModulesSet; | ||
|
||
module.walk<mlir::WalkOrder::PreOrder>([&](Operation *op) { | ||
// Skip modules which we have outlined. | ||
if (outlinedModulesSet.contains(dyn_cast<hw::HWModuleOp>(op))) | ||
return WalkResult::skip(); | ||
// Skip ops which we don't want to outline. | ||
if (!opNamesToOutline.contains(op->getName().getStringRef()) || | ||
op->getNumRegions() > 0) | ||
return WalkResult::advance(); | ||
|
||
// Put together the OpInfo for this op. | ||
SmallVector<Attribute> operandTypes; | ||
for (auto operandType : op->getOperandTypes()) | ||
operandTypes.push_back(TypeAttr::get(operandType)); | ||
SmallVector<Attribute> resultTypes; | ||
for (auto resultType : op->getResultTypes()) | ||
resultTypes.push_back(TypeAttr::get(resultType)); | ||
OpInfo opInfo = { | ||
StringAttr::get(context, op->getName().getStringRef()), | ||
ArrayAttr::get(context, operandTypes), | ||
ArrayAttr::get(context, resultTypes), | ||
cast_or_null<DictionaryAttr>(op->getPropertiesAsAttribute()), | ||
}; | ||
|
||
// Look up the module in the cache and build it if it doesn't exist. | ||
hw::HWModuleOp outlinedModule; | ||
auto moduleIter = outlinedModules.find(opInfo); | ||
if (moduleIter == outlinedModules.end()) { | ||
outlinedModule = buildOutlinedModule(opInfo, op, module); | ||
outlinedModules.emplace(opInfo, outlinedModule); | ||
outlinedModulesSet.insert(outlinedModule); | ||
} else { | ||
outlinedModule = moduleIter->second; | ||
} | ||
|
||
// Replace the op with an instance of the outlined module. | ||
builder.setInsertionPoint(op); | ||
auto inst = builder.create<hw::InstanceOp>( | ||
op->getLoc(), outlinedModule, | ||
builder.getStringAttr("outlined_" + op->getName().getStringRef()), | ||
SmallVector<Value>(op->getOperands())); | ||
inst->setDialectAttrs(op->getDialectAttrs()); | ||
op->replaceAllUsesWith(inst.getResults()); | ||
op->erase(); | ||
|
||
// Since the op was erased, we need to skip the walk. | ||
return WalkResult::skip(); | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
// RUN: circt-opt %s --hw-outline-ops=op-names=seq.fifo | FileCheck %s | ||
|
||
// CHECK-LABEL: hw.module @fifo3a(in %clk : !seq.clock, in %rst : i1, in %in : i32, in %rdEn : i1, in %wrEn : i1, out out : i32) { | ||
// CHECK-NEXT: %outlined_seq.fifo.out, %outlined_seq.fifo.full, %outlined_seq.fifo.empty = hw.instance "outlined_seq.fifo" @outlined_seq.fifo_opers_i32_i1_i1_seq.clock_i1_res_i32_i1_i1_attrs_depth_3("": %in: i32, "": %rdEn: i1, "": %wrEn: i1, "": %clk: !seq.clock, "": %rst: i1) -> (out: i32, full: i1, empty: i1) | ||
// CHECK-NEXT: hw.output %outlined_seq.fifo.out : i32 | ||
hw.module @fifo3a(in %clk : !seq.clock, in %rst : i1, in %in : i32, in %rdEn : i1, in %wrEn : i1, out out : i32) { | ||
%out, %full, %empty = seq.fifo depth 3 in %in rdEn %rdEn wrEn %wrEn clk %clk rst %rst : i32 | ||
hw.output %out : i32 | ||
} | ||
|
||
// CHECK-LABEL: hw.module @fifo3b(in %clk : !seq.clock, in %rst : i1, in %in : i32, in %rdEn : i1, in %wrEn : i1, out out : i32) { | ||
// CHECK-NEXT: %outlined_seq.fifo.out, %outlined_seq.fifo.full, %outlined_seq.fifo.empty = hw.instance "outlined_seq.fifo" @outlined_seq.fifo_opers_i32_i1_i1_seq.clock_i1_res_i32_i1_i1_attrs_depth_3("": %in: i32, "": %rdEn: i1, "": %wrEn: i1, "": %clk: !seq.clock, "": %rst: i1) -> (out: i32, full: i1, empty: i1) | ||
// CHECK-NEXT: hw.output %outlined_seq.fifo.out : i32 | ||
hw.module @fifo3b(in %clk : !seq.clock, in %rst : i1, in %in : i32, in %rdEn : i1, in %wrEn : i1, out out : i32) { | ||
%out, %full, %empty = seq.fifo depth 3 in %in rdEn %rdEn wrEn %wrEn clk %clk rst %rst : i32 | ||
hw.output %out : i32 | ||
} | ||
|
||
// CHECK-LABEL: hw.module @fifo7a(in %clk : !seq.clock, in %rst : i1, in %in : i32, in %rdEn : i1, in %wrEn : i1, out out : i32) { | ||
// CHECK-NEXT: %outlined_seq.fifo.out, %outlined_seq.fifo.full, %outlined_seq.fifo.empty = hw.instance "outlined_seq.fifo" @outlined_seq.fifo_opers_i32_i1_i1_seq.clock_i1_res_i32_i1_i1_attrs_depth_7("": %in: i32, "": %rdEn: i1, "": %wrEn: i1, "": %clk: !seq.clock, "": %rst: i1) -> (out: i32, full: i1, empty: i1) | ||
// CHECK-NEXT: hw.output %outlined_seq.fifo.out : i32 | ||
hw.module @fifo7a(in %clk : !seq.clock, in %rst : i1, in %in : i32, in %rdEn : i1, in %wrEn : i1, out out : i32) { | ||
%out, %full, %empty = seq.fifo depth 7 in %in rdEn %rdEn wrEn %wrEn clk %clk rst %rst : i32 | ||
hw.output %out : i32 | ||
} | ||
|
||
// CHECK-LABEL: hw.module @fifo3a_i8(in %clk : !seq.clock, in %rst : i1, in %in : i8, in %rdEn : i1, in %wrEn : i1, out out : i8) { | ||
// CHECK-NEXT: %outlined_seq.fifo.out, %outlined_seq.fifo.full, %outlined_seq.fifo.empty = hw.instance "outlined_seq.fifo" @outlined_seq.fifo_opers_i8_i1_i1_seq.clock_i1_res_i8_i1_i1_attrs_depth_3("": %in: i8, "": %rdEn: i1, "": %wrEn: i1, "": %clk: !seq.clock, "": %rst: i1) -> (out: i8, full: i1, empty: i1) | ||
// CHECK-NEXT: hw.output %outlined_seq.fifo.out : i8 | ||
hw.module @fifo3a_i8(in %clk : !seq.clock, in %rst : i1, in %in : i8, in %rdEn : i1, in %wrEn : i1, out out : i8) { | ||
%out, %full, %empty = seq.fifo depth 3 in %in rdEn %rdEn wrEn %wrEn clk %clk rst %rst : i8 | ||
hw.output %out : i8 | ||
} | ||
|
||
// CHECK: hw.module @outlined_seq.fifo_opers_i32_i1_i1_seq.clock_i1_res_i32_i1_i1_attrs_depth_3(in [[R0:%.]] "" : i32, in [[R1:%.]] "" : i1, in [[R2:%.]] "" : i1, in [[R3:%.]] "" : !seq.clock, in [[R4:%.]] "" : i1, out out : i32, out full : i1, out empty : i1) { | ||
// CHECK-NEXT: %out, %full, %empty = seq.fifo depth 3 in [[R0]] rdEn [[R1]] wrEn [[R2]] clk [[R3]] rst [[R4]] : i32 | ||
// CHECK-NEXT: hw.output %out, %full, %empty : i32, i1, i1 | ||
// CHECK-NEXT: } | ||
// CHECK: hw.module @outlined_seq.fifo_opers_i32_i1_i1_seq.clock_i1_res_i32_i1_i1_attrs_depth_7(in [[R0:%.]] "" : i32, in [[R1:%.]] "" : i1, in [[R2:%.]] "" : i1, in [[R3:%.]] "" : !seq.clock, in [[R4:%.]] "" : i1, out out : i32, out full : i1, out empty : i1) { | ||
// CHECK-NEXT: %out, %full, %empty = seq.fifo depth 7 in [[R0]] rdEn [[R1]] wrEn [[R2]] clk [[R3]] rst [[R4]] : i32 | ||
// CHECK-NEXT: hw.output %out, %full, %empty : i32, i1, i1 | ||
// CHECK-NEXT: } | ||
// CHECK: hw.module @outlined_seq.fifo_opers_i8_i1_i1_seq.clock_i1_res_i8_i1_i1_attrs_depth_3(in [[R0:%.]] "" : i8, in [[R1:%.]] "" : i1, in [[R2:%.]] "" : i1, in [[R3:%.]] "" : !seq.clock, in [[R4:%.]] "" : i1, out out : i8, out full : i1, out empty : i1) { | ||
// CHECK-NEXT: %out, %full, %empty = seq.fifo depth 3 in [[R0]] rdEn [[R1]] wrEn [[R2]] clk [[R3]] rst [[R4]] : i8 | ||
// CHECK-NEXT: hw.output %out, %full, %empty : i8, i1, i1 | ||
// CHECK-NEXT: } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how this is in general safe. There is no reason to believe a dialect attribute can be moved from a module to an instance. Take any lowering options or name hints, for example.