Skip to content

Commit

Permalink
[HW] Add pass to outline certain ops: hw-outline-ops
Browse files Browse the repository at this point in the history
This pass outlines all hw::HWModuleOp 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 observed.
- Dialect attributes are, however, inherited by the module
  instances.
  • Loading branch information
teqdruid committed Dec 16, 2024
1 parent 87af795 commit 9167f05
Show file tree
Hide file tree
Showing 4 changed files with 295 additions and 0 deletions.
20 changes: 20 additions & 0 deletions include/circt/Dialect/HW/Passes.td
Original file line number Diff line number Diff line change
Expand Up @@ -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.">
];
}

def FlattenIO : Pass<"hw-flatten-io", "mlir::ModuleOp"> {
let summary = "Flattens hw::Structure typed in- and output ports.";
let constructor = "circt::hw::createFlattenIOPass()";
Expand Down
1 change: 1 addition & 0 deletions lib/Dialect/HW/Transforms/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
add_circt_dialect_library(CIRCTHWTransforms
HWOutlineOps.cpp
HWPrintInstanceGraph.cpp
HWSpecialize.cpp
PrintHWModuleGraph.cpp
Expand Down
228 changes: 228 additions & 0 deletions lib/Dialect/HW/Transforms/HWOutlineOps.cpp
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 {
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;
}
};
} // namespace

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] == '!')
os << StringRef(typeName).drop_front(1);
else
os << typeName;
};

// Create the module name.
std::string name;
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()) {
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();
});
}
46 changes: 46 additions & 0 deletions test/Dialect/HW/outline_ops.mlir
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: }

0 comments on commit 9167f05

Please sign in to comment.