-
Notifications
You must be signed in to change notification settings - Fork 461
[HW] Enhance FlattenModules pass with configurable inlining heuristics #9224
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
Changes from all commits
b0b5439
ee11a09
b2dea8d
f6ba7cc
cab3c4e
3e69b18
a0229ce
bc0082d
ae0b612
b31b202
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 |
|---|---|---|
|
|
@@ -9,13 +9,16 @@ | |
| #include "circt/Dialect/HW/HWInstanceGraph.h" | ||
| #include "circt/Dialect/HW/HWOps.h" | ||
| #include "circt/Dialect/HW/HWPasses.h" | ||
| #include "circt/Dialect/HW/InnerSymbolNamespace.h" | ||
| #include "circt/Dialect/Seq/SeqOps.h" | ||
| #include "circt/Support/BackedgeBuilder.h" | ||
| #include "mlir/IR/AttrTypeSubElements.h" | ||
| #include "mlir/IR/Builders.h" | ||
| #include "mlir/IR/IRMapping.h" | ||
| #include "mlir/Pass/Pass.h" | ||
| #include "mlir/Transforms/Inliner.h" | ||
| #include "mlir/Transforms/InliningUtils.h" | ||
| #include "llvm/ADT/PostOrderIterator.h" | ||
| #include "llvm/Support/Debug.h" | ||
|
|
||
| #define DEBUG_TYPE "hw-flatten-modules" | ||
|
|
||
|
|
@@ -32,19 +35,48 @@ using namespace igraph; | |
| using mlir::InlinerConfig; | ||
| using mlir::InlinerInterface; | ||
|
|
||
| using HierPathTable = DenseMap<hw::InnerRefAttr, SmallVector<hw::HierPathOp>>; | ||
|
|
||
| namespace { | ||
|
|
||
| // Cache the inner symbol attribute name to avoid repeated lookups | ||
| static const StringRef innerSymAttrName = | ||
| InnerSymbolTable::getInnerSymbolAttrName(); | ||
| struct FlattenModulesPass | ||
| : public circt::hw::impl::FlattenModulesBase<FlattenModulesPass> { | ||
| using Base::Base; | ||
|
|
||
| void runOnOperation() override; | ||
|
|
||
| private: | ||
| /// Determine if a module should be inlined based on various heuristics. | ||
| bool shouldInline(HWModuleOp module, igraph::InstanceGraphNode *instanceNode, | ||
| size_t bodySize); | ||
| }; | ||
|
|
||
| /// A simple implementation of the `InlinerInterface` that marks all inlining as | ||
| /// legal since we know that we only ever attempt to inline `HWModuleOp` bodies | ||
| /// at `InstanceOp` sites. | ||
| struct PrefixingInliner : public InlinerInterface { | ||
| StringRef prefix; | ||
| PrefixingInliner(MLIRContext *context, StringRef prefix) | ||
| : InlinerInterface(context), prefix(prefix) {} | ||
| InnerSymbolNamespace *ns; | ||
| HWModuleOp parentModule; | ||
| HWModuleOp sourceModule; | ||
| DenseMap<StringAttr, StringAttr> *symMapping; | ||
| mlir::AttrTypeReplacer *replacer; | ||
| HierPathTable *pathsTable; | ||
| hw::InnerRefAttr instanceRef; | ||
|
|
||
| PrefixingInliner(MLIRContext *context, StringRef prefix, | ||
| InnerSymbolNamespace *ns, HWModuleOp parentModule, | ||
| HWModuleOp sourceModule, | ||
| DenseMap<StringAttr, StringAttr> *symMapping, | ||
| mlir::AttrTypeReplacer *replacer, HierPathTable *pathsTable, | ||
| hw::InnerRefAttr instanceRef) | ||
| : InlinerInterface(context), prefix(prefix), ns(ns), | ||
| parentModule(parentModule), sourceModule(sourceModule), | ||
| symMapping(symMapping), replacer(replacer), pathsTable(pathsTable), | ||
| instanceRef(instanceRef) {} | ||
|
Comment on lines
+71
to
+79
Member
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. This constructor may be unnecessary if you use struct initialization. E.g., |
||
|
|
||
| bool isLegalToInline(Region *dest, Region *src, bool wouldBeCloned, | ||
| IRMapping &valueMapping) const override { | ||
|
|
@@ -64,7 +96,35 @@ struct PrefixingInliner : public InlinerInterface { | |
| void processInlinedBlocks( | ||
| iterator_range<Region::iterator> inlinedBlocks) override { | ||
| for (Block &block : inlinedBlocks) | ||
| block.walk([&](Operation *op) { updateNames(op); }); | ||
| block.walk([&](Operation *op) { | ||
| updateNames(op); | ||
| updateInnerSymbols(op); | ||
| }); | ||
|
|
||
| // Update hierarchical paths that reference the inlined instance | ||
| updateHierPaths(); | ||
| } | ||
|
|
||
| void updateHierPaths() const { | ||
| // If the instance has an inner symbol, update any hierarchical paths | ||
| // that reference it | ||
| if (!instanceRef) | ||
| return; | ||
|
|
||
| auto it = pathsTable->find(instanceRef); | ||
| if (it == pathsTable->end()) | ||
| return; | ||
|
|
||
| // For each hierarchical path that references this instance | ||
| for (hw::HierPathOp path : it->second) { | ||
| SmallVector<Attribute, 4> newPath; | ||
| for (auto elem : path.getNamepath()) { | ||
| // Skip the instance reference being inlined | ||
| if (elem != instanceRef) | ||
| newPath.push_back(replacer->replace(elem)); | ||
| } | ||
| path.setNamepathAttr(ArrayAttr::get(path.getContext(), newPath)); | ||
| } | ||
| } | ||
|
|
||
| StringAttr updateName(StringAttr attr) const { | ||
|
|
@@ -88,19 +148,85 @@ struct PrefixingInliner : public InlinerInterface { | |
| } | ||
| } | ||
|
|
||
| void updateInnerSymbols(Operation *op) const { | ||
| // Rename inner symbols to avoid conflicts | ||
| if (auto innerSymAttr = | ||
| op->getAttrOfType<hw::InnerSymAttr>(innerSymAttrName)) { | ||
| StringAttr symName = innerSymAttr.getSymName(); | ||
| auto it = symMapping->find(symName); | ||
| if (it != symMapping->end()) | ||
| op->setAttr(innerSymAttrName, hw::InnerSymAttr::get(it->second)); | ||
| } | ||
|
|
||
| // Apply attribute replacements for InnerRefAttr | ||
| replacer->replaceElementsIn(op); | ||
| } | ||
|
|
||
| bool allowSingleBlockOptimization( | ||
| iterator_range<Region::iterator> inlinedBlocks) const final { | ||
| return true; | ||
| } | ||
| }; | ||
| } // namespace | ||
|
|
||
| bool FlattenModulesPass::shouldInline(HWModuleOp module, | ||
| igraph::InstanceGraphNode *instanceNode, | ||
| size_t bodySize) { | ||
|
prithayan marked this conversation as resolved.
|
||
| // If inlineAll is enabled, inline everything (default behavior) | ||
| if (this->inlineAll) | ||
| return true; | ||
|
|
||
| // Check whether the module should be inlined based on heuristics. | ||
| bool isEmpty = bodySize == 1; | ||
| bool hasNoOutputs = module.getNumOutputPorts() == 0; | ||
| bool hasOneUse = instanceNode->getNumUses() == 1; | ||
| bool hasState = false; | ||
| module.walk([&](Operation *op) { | ||
| // Check for stateful operations (registers and memories) | ||
| if (isa<seq::FirRegOp, seq::CompRegOp, seq::CompRegClockEnabledOp, | ||
| seq::ShiftRegOp, seq::FirMemOp, seq::HLMemOp>(op)) { | ||
| hasState = true; | ||
| return WalkResult::interrupt(); | ||
| } | ||
| return WalkResult::advance(); | ||
| }); | ||
|
|
||
| // Don't inline modules with state unless explicitly allowed | ||
| if (hasState && !this->inlineWithState) | ||
| return false; | ||
|
|
||
| // Inline if any of the enabled conditions are met: | ||
| return (this->inlineEmpty && isEmpty) || | ||
| (this->inlineNoOutputs && hasNoOutputs) || | ||
| (this->inlineSingleUse && hasOneUse) || | ||
| (this->inlineSmall && bodySize < this->smallThreshold); | ||
| } | ||
|
|
||
| void FlattenModulesPass::runOnOperation() { | ||
| auto &instanceGraph = getAnalysis<hw::InstanceGraph>(); | ||
| DenseSet<Operation *> handled; | ||
|
|
||
| InlinerConfig config; | ||
|
|
||
| // Build a mapping of hierarchical path ops. | ||
| DenseSet<StringAttr> leafModules; | ||
| HierPathTable pathsTable; | ||
| for (auto path : getOperation().getOps<hw::HierPathOp>()) { | ||
| // Record leaf modules to be banned from inlining. | ||
| if (path.isModule()) | ||
| leafModules.insert(path.leafMod()); | ||
|
|
||
| // For each instance in the path, record the path | ||
| for (auto name : path.getNamepath()) { | ||
| if (auto ref = dyn_cast<hw::InnerRefAttr>(name)) | ||
| pathsTable[ref].push_back(path); | ||
| } | ||
| } | ||
|
|
||
| // Cache InnerSymbolNamespace objects per parent module to avoid | ||
| // recreating them for each instance in the same parent. | ||
| DenseMap<HWModuleOp, std::unique_ptr<InnerSymbolNamespace>> nsCache; | ||
|
|
||
| // Iterate over all instances in the instance graph. This ensures we visit | ||
| // every module, even private top modules (private and never instantiated). | ||
| for (auto *startNode : instanceGraph) { | ||
|
|
@@ -118,13 +244,35 @@ void FlattenModulesPass::runOnOperation() { | |
| if (numUsesLeft == 0) | ||
| continue; | ||
|
|
||
| for (auto *instRecord : node->uses()) { | ||
| // Only inline private `HWModuleOp`s (no extern or generated modules). | ||
| auto module = | ||
| dyn_cast_or_null<HWModuleOp>(node->getModule().getOperation()); | ||
| if (!module || !module.isPrivate()) | ||
| continue; | ||
| // Only inline private `HWModuleOp`s (no extern or generated modules). | ||
| auto module = | ||
| dyn_cast_or_null<HWModuleOp>(node->getModule().getOperation()); | ||
| if (!module || !module.isPrivate()) | ||
| continue; | ||
|
|
||
| // Do not inline a module if it is targeted by a module NLA. | ||
| if (leafModules.count(module.getNameAttr())) | ||
| continue; | ||
|
|
||
| // Check if module should be inlined based on heuristics | ||
| auto *body = module.getBodyBlock(); | ||
| size_t bodySize = std::distance(body->begin(), body->end()); | ||
|
Member
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. Okay, I think this is number of ops, right, as this is iterator distance? That seems to make sense.
Contributor
Author
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. Right, its the number of ops. |
||
| if (!shouldInline(module, node, bodySize)) | ||
| continue; | ||
|
|
||
| // Build symbol mapping for the module before inlining any instances | ||
| DenseMap<StringAttr, StringAttr> inlineModuleInnerSyms; | ||
| mlir::AttrTypeReplacer innerRefReplacer; | ||
|
|
||
| // Scan the module body to collect all inner symbols that need renaming | ||
| module.walk([&](Operation *op) { | ||
| if (auto innerSymAttr = | ||
| op->getAttrOfType<hw::InnerSymAttr>(innerSymAttrName)) | ||
| inlineModuleInnerSyms.insert( | ||
| {innerSymAttr.getSymName(), StringAttr()}); | ||
| }); | ||
|
|
||
| for (auto *instRecord : node->uses()) { | ||
| // Only inline at plain old HW `InstanceOp`s. | ||
| auto inst = dyn_cast_or_null<InstanceOp>( | ||
| instRecord->getInstance().getOperation()); | ||
|
|
@@ -133,7 +281,47 @@ void FlattenModulesPass::runOnOperation() { | |
|
|
||
| bool isLastModuleUse = --numUsesLeft == 0; | ||
|
|
||
| PrefixingInliner inliner(&getContext(), inst.getInstanceName()); | ||
| // Get the parent module | ||
| HWModuleOp parentModule = inst->getParentOfType<HWModuleOp>(); | ||
|
|
||
| // Get or create the InnerSymbolNamespace for the parent module | ||
| auto &nsPtr = nsCache[parentModule]; | ||
| if (!nsPtr) | ||
| nsPtr = std::make_unique<InnerSymbolNamespace>(parentModule); | ||
|
|
||
| // Create fresh symbol names for this instance | ||
| DenseMap<StringAttr, StringAttr> oldToNewInnerSyms; | ||
| for (auto [oldSym, _] : inlineModuleInnerSyms) | ||
| oldToNewInnerSyms.insert( | ||
| {oldSym, StringAttr::get(&getContext(), | ||
| nsPtr->newName(oldSym.getValue()))}); | ||
|
|
||
| // Setup the replacer for InnerRefAttr | ||
| mlir::AttrTypeReplacer instanceReplacer; | ||
| instanceReplacer.addReplacement( | ||
| [&](InnerRefAttr attr) -> std::pair<Attribute, WalkResult> { | ||
| if (attr.getModule() != module.getModuleNameAttr()) | ||
| return {attr, WalkResult::skip()}; | ||
|
|
||
| auto it = oldToNewInnerSyms.find(attr.getName()); | ||
| if (it == oldToNewInnerSyms.end()) | ||
| return {attr, WalkResult::skip()}; | ||
|
|
||
| auto newAttr = InnerRefAttr::get(parentModule.getModuleNameAttr(), | ||
| it->second); | ||
| return {newAttr, WalkResult::skip()}; | ||
| }); | ||
|
|
||
| // Get the instance's inner reference if it has one | ||
| hw::InnerRefAttr instanceRef; | ||
| if (auto sym = inst.getInnerSymAttr()) | ||
| instanceRef = inst.getInnerRef(); | ||
|
|
||
| PrefixingInliner inliner(&getContext(), inst.getInstanceName(), | ||
| nsPtr.get(), parentModule, module, | ||
| &oldToNewInnerSyms, &instanceReplacer, | ||
| &pathsTable, instanceRef); | ||
|
|
||
| if (failed(mlir::inlineRegion(inliner, config.getCloneCallback(), | ||
| &module.getBody(), inst, | ||
| inst.getOperands(), inst.getResults(), | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| // RUN: circt-opt %s --pass-pipeline='builtin.module(hw-flatten-modules{hw-inline-all=false hw-inline-single-use=false hw-inline-small=false hw-inline-empty=false hw-inline-no-outputs=false})' | FileCheck %s --check-prefix=NONE | ||
| // RUN: circt-opt %s --pass-pipeline='builtin.module(hw-flatten-modules{hw-inline-all=false hw-inline-single-use=true hw-inline-small=false hw-inline-empty=false hw-inline-no-outputs=false})' | FileCheck %s --check-prefix=SINGLE | ||
| // RUN: circt-opt %s --pass-pipeline='builtin.module(hw-flatten-modules{hw-inline-all=false hw-inline-single-use=false hw-inline-small=true hw-inline-empty=false hw-inline-no-outputs=false})' | FileCheck %s --check-prefix=SMALL | ||
| // RUN: circt-opt %s --pass-pipeline='builtin.module(hw-flatten-modules{hw-inline-all=false hw-small-threshold=3 hw-inline-single-use=false})' | FileCheck %s --check-prefix=THRESHOLD | ||
| // RUN: circt-opt %s --pass-pipeline='builtin.module(hw-flatten-modules{hw-inline-with-state=true})' | FileCheck %s --check-prefix=STATE | ||
|
|
||
| // Test that all inlining heuristics can be controlled via command line options | ||
|
|
||
| // NONE-LABEL: hw.module @TestSingleUse | ||
| // SINGLE-LABEL: hw.module @TestSingleUse | ||
| hw.module @TestSingleUse(in %x: i4, out y: i4) { | ||
| // NONE-NEXT: hw.instance "large" @LargeModule | ||
| // SINGLE-NEXT: %[[V0:.+]] = comb.add %x, %x | ||
| // SINGLE-NEXT: %[[V1:.+]] = comb.and %[[V0]], %[[V0]] | ||
| // SINGLE-NEXT: %[[V2:.+]] = comb.or %[[V1]], %[[V1]] | ||
| // SINGLE-NEXT: %[[V3:.+]] = comb.xor %[[V2]], %[[V2]] | ||
| // SINGLE-NEXT: %[[V4:.+]] = comb.mul %[[V3]], %[[V3]] | ||
| // SINGLE-NEXT: %[[V5:.+]] = comb.add %[[V4]], %[[V4]] | ||
| // SINGLE-NEXT: %[[V6:.+]] = comb.sub %[[V5]], %[[V5]] | ||
| // SINGLE-NEXT: %[[V7:.+]] = comb.add %x, %x | ||
| // SINGLE-NEXT: hw.output %[[V7]] | ||
| %0 = hw.instance "large" @LargeModule(a: %x: i4) -> (b: i4) | ||
| hw.output %0 : i4 | ||
| } | ||
|
prithayan marked this conversation as resolved.
|
||
|
|
||
| hw.module private @SmallModule(in %a: i4, out b: i4) { | ||
| %0 = comb.add %a, %a : i4 | ||
| hw.output %0 : i4 | ||
| } | ||
|
|
||
| // SMALL-LABEL: hw.module @TestSmall | ||
| hw.module @TestSmall(in %x: i4, out y: i4) { | ||
| // SMALL-NEXT: %[[V0:.+]] = comb.add %x, %x | ||
| // SMALL-NEXT: hw.output %[[V0]] | ||
| %0 = hw.instance "small" @SmallModule(a: %x: i4) -> (b: i4) | ||
| hw.output %0 : i4 | ||
| } | ||
|
|
||
|
|
||
| // THRESHOLD-LABEL: hw.module @TestThreshold | ||
| hw.module @TestThreshold(in %x: i4, out y: i4) { | ||
| // THRESHOLD-NEXT: hw.instance "medium" @MediumModule | ||
| %0 = hw.instance "medium" @MediumModule(a: %x: i4) -> (b: i4) | ||
| hw.output %0 : i4 | ||
| } | ||
|
|
||
| // This module has 5 operations (4 comb ops + 1 hw.output) | ||
| // With threshold=3, it should NOT be inlined | ||
| hw.module private @MediumModule(in %a: i4, out b: i4) { | ||
| %0 = comb.add %a, %a : i4 | ||
| %1 = comb.mul %0, %a : i4 | ||
| %2 = comb.xor %1, %a : i4 | ||
| %3 = comb.or %2, %a : i4 | ||
| hw.output %3 : i4 | ||
| } | ||
|
|
||
| // STATE-LABEL: hw.module @TestState | ||
| hw.module @TestState(in %clk: !seq.clock, in %x: i4, out y: i4) { | ||
| // STATE-NEXT: %[[REG:.+]] = seq.firreg %x clock %clk | ||
| // STATE-NEXT: hw.output %[[REG]] | ||
| %0 = hw.instance "reg" @RegModule(clk: %clk: !seq.clock, a: %x: i4) -> (b: i4) | ||
| hw.output %0 : i4 | ||
| } | ||
|
|
||
| hw.module private @RegModule(in %clk: !seq.clock, in %a: i4, out b: i4) { | ||
| %0 = seq.firreg %a clock %clk : i4 | ||
| hw.output %0 : i4 | ||
| } | ||
|
|
||
| // This module has 9 operations (8 comb ops + 1 hw.output), exceeding the default threshold of 8 | ||
| // It should NOT be inlined based on size alone, but WILL be inlined if single-use is enabled | ||
| hw.module private @LargeModule(in %a: i4, out b: i4) { | ||
| %0 = comb.add %a, %a : i4 | ||
| %1 = comb.and %0, %0 : i4 | ||
| %2 = comb.or %1, %1 : i4 | ||
| %3 = comb.xor %2, %2 : i4 | ||
| %4 = comb.mul %3, %3 : i4 | ||
| %5 = comb.add %4, %4 : i4 | ||
| %6 = comb.sub %5, %5 : i4 | ||
| %7 = comb.add %a, %a : i4 | ||
| hw.output %7 : i4 | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.