diff --git a/include/circt/Dialect/HW/Passes.td b/include/circt/Dialect/HW/Passes.td index a1445d370679..6042f2a4fd5f 100644 --- a/include/circt/Dialect/HW/Passes.td +++ b/include/circt/Dialect/HW/Passes.td @@ -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()"; diff --git a/lib/Dialect/HW/Transforms/CMakeLists.txt b/lib/Dialect/HW/Transforms/CMakeLists.txt index 093632602fa7..d2c2f2a8d7e9 100644 --- a/lib/Dialect/HW/Transforms/CMakeLists.txt +++ b/lib/Dialect/HW/Transforms/CMakeLists.txt @@ -1,4 +1,5 @@ add_circt_dialect_library(CIRCTHWTransforms + HWOutlineOps.cpp HWPrintInstanceGraph.cpp HWSpecialize.cpp PrintHWModuleGraph.cpp diff --git a/lib/Dialect/HW/Transforms/HWOutlineOps.cpp b/lib/Dialect/HW/Transforms/HWOutlineOps.cpp new file mode 100644 index 000000000000..64752d402b1d --- /dev/null +++ b/lib/Dialect/HW/Transforms/HWOutlineOps.cpp @@ -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 + +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 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()) { + 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 resultNames; + // Get the result names from the op if it implements the OpAsmOpInterface. + if (auto asmNames = dyn_cast(op)) + asmNames.getAsmResultNames( + [&](Value value, StringRef name) { resultNames[value] = name; }); + nameOS << "_res"; + llvm::SmallVector resultTypes; + for (auto [idx, type] : + llvm::enumerate(opInfo.resultTypes.getAsRange())) { + 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 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(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( + 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 { +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 + outlinedModules; + DenseSet outlinedModulesSet; + + module.walk([&](Operation *op) { + // Skip modules which we have outlined. + if (outlinedModulesSet.contains(dyn_cast(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 operandTypes; + for (auto operandType : op->getOperandTypes()) + operandTypes.push_back(TypeAttr::get(operandType)); + SmallVector 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(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( + op->getLoc(), outlinedModule, + builder.getStringAttr("outlined_" + op->getName().getStringRef()), + SmallVector(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(); + }); +} diff --git a/test/Dialect/HW/outline_ops.mlir b/test/Dialect/HW/outline_ops.mlir new file mode 100644 index 000000000000..d55a9d70e017 --- /dev/null +++ b/test/Dialect/HW/outline_ops.mlir @@ -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: }