-
Notifications
You must be signed in to change notification settings - Fork 308
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
[FIRRTL] Improve canonicalization involving invalidvalue
#2251
Conversation
d240ffa
to
87a6917
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.
My gut instinct is that folding more is fine, so long as the fold involves an invalid interpreted as zero. Any error of this form is likely a missing fold in the SFC. (The first failure in invalid-reg-pass.fir
is related to "convert to signed/cvt" not folding away. This just seems to be missing as SFC doesn't touch cvt(UInt<1>(0))
.) This type of deviation shouldn't change functional correctness except for pre-reset/cycle zero behavior.
Any fold involving invalid propagation (foo(_, invalid) -> invalid
) needs to be handled extremely carefully as the SFC never does this. This type of interpretation can introduce functional differences in the output.
That makes a lot of sense @seldridge. That would mean some of the test cases 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.
This approach looks good to me!
lib/Dialect/FIRRTL/FIRRTLFolds.cpp
Outdated
// InvalidValue inputs simply read as zero. | ||
if (auto result = constant.dyn_cast_or_null<InvalidValueAttr>()) { | ||
llvm::errs() << "Treating " << operand << " as zero value\n"; | ||
return APSInt(destWidth, result.getType().isa<UIntType>()); |
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 invalid's work with reset/clock/arrays and other types? I am pretty sure it works with aggregates, so you might need to check this (and just return {}
)
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.
getExtendedConstant
is intended to be used only for IntType
. I'll add an assert to make this assumption more visible.
I agree with @seldridge here that more folding is probably ok. That said, I don't think it is important to invest effort in being super aggressive either. Do we have a good list of things that really should be preserved with invalid? Documenting that will help put guard rails on this and set expectations. I think the model is pretty simple:
Does this sound right to you? |
Not exactly other than:
This roughly aligns with what you're saying, but has a lot of pesky properties. A byproduct of this is that the FIRRTL to HW conversion should see no invalid values if optimizations are on. |
sounds good to me |
I'll do another pass over this with the findings from the discussion here and elsewhere regarding invalid propagation. |
87a6917
to
760d069
Compare
invalidvalue
invalidvalue
@lattner, @seldridge, I've reworked this and extended it to cover all the prim ops. I've also added a bunch of tests for the newly-covered cases, and was able to get rid of @seldridge's XFAIL tests entirely (with a caveat that MFC is now more aggressive in some cases than SFC). |
This looks structurally fine to me, but i'd prefer @seldridge to do a detailed walkthrough |
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 looks awesome. I love how everything unifies around isConstant
.
I will open issues about all of this on the SFC. The only ones that I'm slightly nervous about is places where we use specialconstant
for clock and reset literals. The SFC doesn't have these, so there will always be asClock
or asAsyncReset
casts that it can't remove. (This is a place where these ops would block register removal.) This isn't enough to block these until we have a problem, thouhg.
if (auto type = attr.getType().dyn_cast<IntType>()) | ||
return APSInt(type.getWidth().getValueOr(1), type.isUnsigned()); |
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.
Can getWidth().getValue()
fail here? I assume this is catching the case of this running on an uninferred width?
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.
Yeah exactly. My thinking was that there's this getAPSInt()
call below if we're dealing with an IntegerAttr
, which would have a width assigned even in the case that the FIRRTLType itself had an uninferred width (just some random width that is sufficient to hold the constant). So in analogy I just made the invalidvalue fold to a zero of at least one bit.
TypeSwitch<Operation *>(defOp) | ||
.Case<ConstantOp, SpecialConstantOp, InvalidValueOp>( | ||
[&](auto op) { attr = op.fold({}).template get<Attribute>(); }); |
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.
MSVC may not like the template inside a lambda. You may want to do a special run with the MSVC action using this branch to make sure it compiles. If this is a problem, I will usually work around it by removing the auto
. In this case it's necessary as there's no parent type that can be used here. Worst case, this could use three cases with explicit types instead of auto
.
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.
Good idea, will do a run!
lib/Dialect/FIRRTL/FIRRTLFolds.cpp
Outdated
// rem(invalid, x) -> 0 | ||
/// rem(x, x) -> 0 | ||
/// | ||
/// Division by zero is undefined in the FIRRTL specification. This fold | ||
/// exploits that fact to optimize self division remainder to zero. Note: | ||
/// this should supersede any division with invalid or zero. Remainder of | ||
/// division of invalid by invalid should be zero. | ||
if (lhs() == rhs()) | ||
return getIntZerosAttr(getType()); | ||
|
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 references division? Do we actually do this fold for remainder, too?
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.
Yeah, there's a div(x, x) -> 1
and a rem(x, x) -> 0)
fold which cater to the same idea. This is one of the things that aren't there in SFC, and it would be an easy fix to just disable the folds here to make MFC match up with SFC.
return lhs(); | ||
|
||
if (auto rhsCst = operands[1].dyn_cast_or_null<IntegerAttr>()) { | ||
if (auto rhsCst = getConstant(operands[1])) { |
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 now only has to check the RHS because or(invalid, x)
is properly canonicalized to or(x, invalid)
because invalid is zero-like?
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.
Yeah exactly. The OrPrimOp
is marked as commutative, and the InvalidValueOp
folds to a InvalidValueAttr
(which I guess MLIR interprets as "the constant"), so a builtin pattern will swap the operands on that OrPrimOp
.
Tracking issue on the SFC about the observed differences: chipsalliance/firrtl#2432 |
760d069
to
b8072b2
Compare
There is a bug in HEAD compiling regression/test3 that this might fix, firrtl.circuit "Y" {
firrtl.module @Y(in %i: !firrtl.sint<1>, out %o: !firrtl.sint<15>) {
%invalid_si1 = firrtl.invalidvalue : !firrtl.sint<1>
%0 = firrtl.pad %invalid_si1, 15 : (!firrtl.sint<1>) -> !firrtl.sint<15>
firrtl.connect %o, %0 : !firrtl.sint<15>, !firrtl.sint<15>
}
} Any chance you could check if this is working now? Edit: Yeah, this does fix both the test case above and |
This extends the FIRRTL fold and canonicalization patterns to treat `invalidvalue` as a zero if it is an input to a primary operation. This is inline with what the Scala implementation of the FIRRTL compiler does. Doing this removes quite a few of the special cases where certain ops are aware of `invalidvalue`s and will fold as if it was a zero. The addition of the `getConstant`, `isConstantZero`, and `canonicalizePrimOp` helpers allow for most folders and canonicalizers to not having to bother with `invalidvalue`s, as these helpers implicitly map `invalidvalue`s to zero constants. This also subsumes the work done in llvm#2278 and llvm#2198, which selectively added handling for `invalidvalue`s to the `PadPrimOp`. The extension of the folders and canonicalizers now makes all the cases in `invalid-reg-fail.fir` pass, albeit some only after accepting that the MLIR implementation folds *additional* cases where the Scala one might just leave an operation untouched. These are marked with a `<-- fixed; upstream to Scala FIRRTL impl?` and should probably be upstreamed as additional canonicalizations on the Scala side as well.
b8072b2
to
b8053cc
Compare
Thanks for checking @youngar. I've added your test case as a regression test to |
This extends the FIRRTL fold and canonicalization patterns to treat
invalidvalue
as a zero if it is an input to a primary operation. This is inline with what the Scala implementation of the FIRRTL compiler does.Doing this removes quite a few of the special cases where certain ops are aware of
invalidvalue
s and will fold as if it was a zero. The addition of thegetConstant
,isConstantZero
, andcanonicalizePrimOp
helpers allow for most folders and canonicalizers to not having to bother withinvalidvalue
s, as these helpers implicitly mapinvalidvalue
s to zero constants.This also subsumes the work done in #2278 and #2198, which selectively added handling for
invalidvalue
s to thePadPrimOp
.The extension of the folders and canonicalizers now makes all the cases in
invalid-reg-fail.fir
pass, albeit some only after accepting that the MLIR implementation folds additional cases where the Scala one might just leave an operation untouched. These are marked with a<-- fixed; upstream to Scala FIRRTL impl?
and should probably be upstreamed as additional canonicalizations on the Scala side as well.Original PR:
This draft PR is an effort to make
invalidvalue
behave as zero during canonicalization, while still maintaining thefirrtl.invalidvalue
operations in the IR, which we use in a canonicalization ofMuxPrimOp
to properly doWhenOp
expansion.With a few additional helpers, we can make all the folds and canonicalizations work with
invalidvalue
as zero. However as a result some of the integration tests ininvalid-reg-pass.fir
start to fail, which I think indicates thatfirtool
now folds things more aggressively than the Scala implementation.I don't know if it's worthwhile pursuing this effort, or whether we should rather try to match the Scala impl exactly (which would make logical equivalence pass, but is pretty tricky because we have to be bug-for-bug compatible).