-
Notifications
You must be signed in to change notification settings - Fork 298
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/SV] Add hw.inout
elimination pass
#5390
Conversation
6f8d234
to
4cf599c
Compare
@@ -0,0 +1,194 @@ | |||
//===- HWRaiseInOutPorts.cpp - Generator Callout Pass ---------------------===// |
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.
Looks good to me.
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.
This is very similar to the port mapping that I do in ESI's lower ports pass:
class ChannelRewriter { |
I put together a pretty-well (IMO) structured, pluggable infrastructure for solving this problem. It should be pulled out into a support library and made more generic. I think of it like a TypeConverter
which handles creating both input and output ports simultaneously. Could you refactor it into a support lib and use it? (Refactor as first PR, use here as second.) We discussed this earlier today, but looking this over makes me think it's silly to re-invent the wheel given that these problems are identical.
// which share subcircuits. | ||
llvm::DenseSet<InstanceGraphNode *> visited; | ||
for (InstanceGraphNode *topModule : res.value()) { | ||
// Visit the instance hierarchy in a depth-first manner, modifying child |
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.
nit: I don't think it's critical to do this DFS as your comment implies. I think this is only critical for "bubbling up" values, which this transform doesn't do.
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.
Doing this DFS ensures that all module instance uses of an inout value has been converted before the current instance use.
E.g. say you have
m1 -> m2 -> m3
where both m3 and m2 reads an inout value defined in m1. If we don't do DFS, and we just randomly pick a module, we have to e.g. select m2, see that it also passes that inout value to other module instances, processes those first (which may bubble up read/writes to that hw.inout op), and then process m2... which in essence is a DFS traversal. So in my mind, it seems more logical, and safer/simpler, to just go ahead and do the DFS to begin with, ensuring the invariant that all module instance uses of an inout value have been converted before converting any given module.
1db1c2d
to
256a7e2
Compare
@teqdruid massively simplified by being based on the |
Adds a pass which analyses the module hierarchy to remove `inout` ports in favor of: 1. poking holes through the module hierarchy (explicit in/output ports) 2. raising `sv.assign`/`sv.read_inout` operations to the definition point of the `hw.inout` value. This is unarbited, and only allows for multiple-readers, single-writer scenarios. Currently only supports `sv.assign`, but should probably also be able to raise `sv.passign`.
256a7e2
to
e63c9d8
Compare
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.
Thanks for this! Good to see that the port conversion infra helps here. My biggest problem is with the name.
include/circt/Dialect/SV/SVPasses.td
Outdated
@@ -173,4 +173,17 @@ def HWExportModuleHierarchy : Pass<"hw-export-module-hierarchy", | |||
let dependentDialects = ["circt::sv::SVDialect"]; | |||
} | |||
|
|||
def HWRaiseInOutPorts : Pass<"hw-raise-inout-ports", |
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.
Shouldn't this be called something like "LowerInOutPorts"? Or EliminateInOutPorts
? 'Breakout'? This doesn't seem like something which raises the level of abstraction, which is what the work 'raise' implies to me.
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'd favor Eliminate
here. Raise
was originally used since i was "raising" the sv.read/assign
ops upwards in the instance hierarchy.
} | ||
|
||
if (writers.size() > 1) | ||
converter.getModule()->emitWarning() |
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 think this should be an error rather than a warning (and it should fail the pass). Hardware designers tend to ignore warnings, so I tend to prefer avoiding warnings, especially when they have this big of an effect on design correctness.
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.
Agreed. Modified the infra slightly to allow PortConversion
s to provide a failable init
function.
void HWInOutPortConversion::mapOutputSignals( | ||
OpBuilder &b, Operation *inst, Value instValue, | ||
SmallVectorImpl<Value> &newOperands, ArrayRef<Backedge> newResults) { | ||
llvm_unreachable("hw.inout outputs not yet supported"); |
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.
Above you use an assert whereas here you use this. What should really be used? I think assert is more appropriate.
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.
See below - will keep the assert since i nevertheless have to have something in the implementation.
public: | ||
using PortConversionBuilder::PortConversionBuilder; | ||
FailureOr<std::unique_ptr<PortConversion>> build(hw::PortInfo port) override { | ||
if (port.direction == hw::PortDirection::INOUT) |
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.
Since we don't (yet) support output inouts, we should either ignore them or issue an error. Unsupported IR should never assert or segfault, as I think this results in.
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.
Won't segfault; this code already implies that they're being ignored - see below. This, however, is probably more telling about how the HWModuleOp
parses PortInfo
, in that input hwinout
has PortDirection::INOUT
and output inout
has PortDirection::OUTPUT
. This in turn means that hw.inout
gets handled as an untouched signal.
Added the below example as a test, mainly to serve as an early warning signal in case this logic changes.
hw.module @write(%a: !hw.inout<i42>) {
%0 = hw.constant 0 : i42
sv.assign %a, %0 : i42
}
hw.module @outputInout() -> (out : !hw.inout<i42>) {
%0 = sv.wire : !hw.inout<i42>
hw.output %0 : !hw.inout<i42>
}
hw.module @outputInoutDriver() {
%0 = hw.instance "outputInout" @outputInout() -> (out : !hw.inout<i42>)
hw.instance "write" @write(a : %0 : !hw.inout<i42>) -> ()
}
results in
hw.module @write() -> (a_wr: i42) {
%c0_i42 = hw.constant 0 : i42
hw.output %c0_i42 : i42
}
hw.module @outputInout() -> (out: !hw.inout<i42>) {
%0 = sv.wire : !hw.inout<i42>
hw.output %0 : !hw.inout<i42>
}
hw.module @outputInoutDriver() {
%outputInout.out = hw.instance "outputInout" @outputInout() -> (out: !hw.inout<i42>)
sv.assign %outputInout.out, %write.a_wr : i42
%write.a_wr = hw.instance "write" @write() -> (a_wr: i42)
hw.output
}
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.
Yep. I forgot that confusing behavior. I'm hoping that'll change with lenharth's moduletype.
// Visit the instance hierarchy in a depth-first manner, modifying child | ||
// modules and their ports before their parents. | ||
|
||
// Doing this DFS ensures that all module instance uses of an inout value has |
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.
Do we need to do some cycle detection here?
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.
Not that i'm aware of, post-order iteration should only visits each node once, even in the presence of cycles.
llvm::DenseSet<InstanceGraphNode *> visited; | ||
auto res = instanceGraph.getInferredTopLevelNodes(); | ||
|
||
// Visit the instance hierarchy in a depth-first manner, modifying child |
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.
Not for this PR, but this feels like something which could be pulled out. Perhaps a part of the PortConversion
framework... IIRC the ESI pass does something like this as well.
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.
that, or some kind of boring utility library (global/hierarchy modification) which can be coupled together with PortConvesion (local changes).
|
||
if (writers.size() > 1) | ||
converter.getModule()->emitWarning() | ||
<< "Multiple writers of inout port " << port.name |
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.
Not for this PR, but what is the behavior of multiple assignments to an inout
variable declared in the same module? Is it defined? What about PAssignOp
? Perhaps we should (in the future) try to emulate that behavior.
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.
what is the behavior of multiple assignments to an inout variable declared in the same module? Is it defined? What about PAssignOp?
IAFAIK, CIRCT won't complain but you're going to have a problem during synthesis given the multiple writers.
And yea, as mentioned in the PR, PAssignOp
is explicitly not covered by this pass.
hw.inout
elimination pass
Adds a pass which analyses the module hierarchy to eliminate `inout` ports in favor of: 1. poking holes through the module hierarchy (explicit in/output ports) 2. raising `sv.assign`/`sv.read_inout` operations to the definition point of the `hw.inout` value. This is unarbited, and only allows for multiple-readers, single-writer scenarios. Currently only supports `sv.assign`, as well as `hw.inout` input ports.
Adds a pass which analyses the module hierarchy to remove
inout
ports in favor of:sv.assign
/sv.read_inout
operations to the definition point of thehw.inout
value.This is unarbited, and only allows for multiple-readers, single-writer scenarios. Currently only supports
sv.assign
, but should probably also be able to raisesv.passign
.