-
-
Notifications
You must be signed in to change notification settings - Fork 337
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
[WIP] Add library features for formal testing against streams and PipelinedMemoryBus #1637
base: dev
Are you sure you want to change the base?
[WIP] Add library features for formal testing against streams and PipelinedMemoryBus #1637
Conversation
…d testing for each
this line set default behaviour of reset to synchronous one. in both cases. checker could not work with async version. |
Also I don't think mix the verification code into function ones is a good idea, even when the output synthesible code is consistent. The coupling is not preferred in modern engineering, which make things hard to read and debug. |
For verification of StreamFifo, see this file. The Fifo is the first component verified by an inductive prover. |
Should the SpinalFormal config just use sync resets across the board then? I get this behavior even with formal tests I didn't modify; so I think something must be different on my setup than say the CI server.
Where should it go then? I considered making a trait with a function like The main use case I want to enable here is that if someone wants to perform formal analysis on their design which incorporates library components like these, it's easy and doesn't require calling them out individually in the higher levels test harness. With even the BMC formal methods, the higher level component doesn't need those asserts at all but
That isn't an inductive proof of StreamFifo, it's a unit test for it -- which is great but not needed for the induction case. The inductive proof for StreamFifo is here: https://github.com/SpinalHDL/SpinalHDL/blob/dev/tester/src/test/scala/spinal/tester/scalatest/FormalFifoTester.scala. Part of the idea with this PR is that the asserts here: https://github.com/SpinalHDL/SpinalHDL/blob/dev/tester/src/test/scala/spinal/tester/scalatest/FormalFifoTester.scala#L104 being only in the test harness is not useful if you have a StreamFifo in your design and want to run For instance, the added inductive proof for |
If I remember right, it is merged in the latest 1.11.0 or 1.10.2a. Where I have fixed some conceptual bugs on.
I sugguest to define formalXXXX function into corresponding component, and call it in your doVerify implementation, or called by other formalYYYY method using this component.
Right, that's the place.
I can understand that, and agree these part is important for usage. Put some commonly required assertion into formalXXXX method is for this purpose.
|
@@ -675,6 +675,7 @@ object StreamArbiter { | |||
for(bitId <- maskLocked.range){ | |||
maskLocked(bitId) init(Bool(bitId == maskLocked.length-1)) | |||
} | |||
assert(CountOne(maskLocked) <= 1, "Mask locked must be a proper one hot vector") |
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 mean may be put this lines into formalAsserts is a good option, but mixed them into the design.
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 latest commit, but I wasn't sure how to handle this with the formalAsserts
method -- the data type of arbitration
is just Area
. I can probably apply this assert across the board for any arbitrator? But also maybe for stuff like this it makes sense to just let it be inline?
@@ -730,6 +731,9 @@ class StreamArbiter[T <: Data](dataType: HardType[T], val portCount: Int)(val ar | |||
val maskLocked = Reg(Vec(Bool(),portCount)) | |||
val maskRouted = Mux(locked, maskLocked, maskProposal) | |||
|
|||
if(globalData.config.formalAsserts) { | |||
assert(CountOne(maskRouted) <= 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.
I suggest not to use formalAsserts in config directly. It's for library's internal usage.
put all asserts related into one method which is called only in doVerify might be a good practice.
@@ -1403,45 +1410,143 @@ class StreamFifo[T <: Data](val dataType: HardType[T], | |||
} | |||
} | |||
|
|||
val asserts = globalData.config.formalAsserts generate formalAsserts() |
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.
It's best not to define a variable which just have one char different from the keywords assert
.
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'll take these out
@@ -1403,45 +1410,143 @@ class StreamFifo[T <: Data](val dataType: HardType[T], | |||
} | |||
} | |||
|
|||
val asserts = globalData.config.formalAsserts generate formalAsserts() | |||
|
|||
def formalAsserts() = new Area { |
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.
As I understand, from BMC to prove stage, there only required some assertion on internal states to synchronize the ramdomly initialized registers. If you could I suggest to add a small part assertion first. I think there are too many logics in this formalXXXX functions.
Btw, I am happy to see someone is using and contributing code to the formal related functions. I can also help, when I find enough time to reevaluate this part, later.
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 can refactor this into something more cohesive for sure. I think this is about the right amount of assertions to constrain the fifo though.
My work flow for this is to run it, see where induction fails, patch the faulty state with an assert, and repeat as necessary. I'll also go through when cleaning up and comment out assertions and re-run it to see if they were necessary or not.
val willOverflowIfInc = mayOverflow && !decrementIt | ||
val willOverflow = willOverflowIfInc && incrementIt | ||
|
||
val willUnderflowIfDec = mayUnderflow && !incrementIt |
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.
If this is not only for verification refactor, please open a PR individually.
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.
Will do
@@ -42,6 +42,40 @@ case class PipelinedMemoryBus(config : PipelinedMemoryBusConfig) extends Bundle | |||
val cmd = Stream(PipelinedMemoryBusCmd(config)) | |||
val rsp = Flow(PipelinedMemoryBusRsp(config)) | |||
|
|||
def readRequestFire = cmd.fire && !cmd.write | |||
lazy val formalContract = new Composite(this, "formalContract") { |
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.
Is this contract means to the bus's contract? Why it should invariantCmd be created?
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.
Part of this is nailing down what exactly the semantics are for PMB cmd/rsp. I'm guessing a lot of perfectly functional code would trip up if the stream contract was applied with payload invariance on cmd directly -- when write
is false it seems fine to just not drive data
or mask
.
Does the pattern in 3109aa5 work? I didn't do it to everything, just enough to get a feel for how it'd work (StreamFifo, StreamArbiter, PMBArbiter). Generate notes:
I'm working off of |
@@ -9,7 +9,28 @@ object FormalDut{ | |||
c.withAutoPull() | |||
c.setFormalTester() | |||
} | |||
|
|||
dut match { |
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.
So do you want to run the formal asserts automatically?
IMO explicit is better than implicit.
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.
Sort of? I was thinking if the user was calling FormalDut
on something; that was an explicit action. Otherwise you end up with a lot of little boilerplate code around it. Tastes vary around this kind of thing though; I'll take it out if you want
…s is to temporarilly fix a bug where asserts under an async reset break sby
…ce reset is in unknown state
I've been testing this locally on one of my own designs / test benches and have some notes on the approach:
I think a reasonable approach is for those small load bearing things needed in the various utility areas ( I'll probably rename |
That's why I suggest designing a calling tree to formalAsserts method explicitly.
According to tutorial -> multi-clocked section from ZipCPU, even verification of asynchronous design is using clocks and resets synchronized with the globalclk. I rememeber that there is a demo checker for FIFOCC. So the default clock domain of FormalDut has been set to SYNC kind in formal verification.
The initial reset status could be done by assumeInitial I suppose. From the link above, the default reset is set when the default domain's reset active, and fixed to high level kind, it seems that not decoupled with clock domain.
Because the keyword
|
As far as I know, this would require adding a mechanism to link areas to their refOwner component in the general case; which doesn't exist currently. In the simple case where you have a handle to the area -- such as CounterUpDown this could be made to work maybe but there are a ton of examples where you wont necessarily have that handle or type erasure kicks in -- stream arbiters have a few of these kinds of areas, For child components, I have it using
AFAIK,
The first option seems like a substantial change that I'm sure has a bunch of corner cases to add and consider. The second makes it much harder to add formal tests to a user component and maybe impossible in some cases (for instance if you have something like an interconnect which creates areas with state). The third option has the downside of the assumptions being present everywhere but I can't think of any way this breaks any existing designs or simulations.
I'm not sure on this. The FifoCC formal test doesn't work if you set it to a SYNC reset. I didn't dig a ton into why this was the case. Since StreamFifoCC doesn't have inline asserts, it works in ASYNC mode. I think if you walked the design and put only the asserts under a sync clock, it seems like this might also miss real issues in a design around async resets. I think a default SYNC reset for formal methods here is pragmatic given the limitations in |
How about add an optional function for internal formal assertions and run them during the FormalPhase.
When you use the FormalDut, it will generate SYNC reset even you use Clock Domain with async reset, it's for purpose. There are functions in GlobalClock to synchronize the async resets, if you do not call that functions, the resets are async theoritically.
Overall, I think your most important pain point is the lack of assertion on internal signal's synchronization of facilities provide in lib. You want to call those assertion methods automatically by default, so that you can concentrate on the verification by interface. Did I get it right? If so, how about add a traits with optional function which can be executed at FormalPhase for all components and areas. For now, the method name could be formalAsserts, as it is already in StreamFifoCC and StreamTransactionCounter The only problem is that formalAsserts is also used by Axi4's formalContext which is a composite generating helper logic for bus verification. If we change the define of Composite by adding trait with optional formalAsserts and run by default, then this code might be affected. At last, the most important if we want to run the formalAsserts by default, as I described firstly? |
I think if you want any hope in scaling a formal proof to a complicated design I think it only works if you can do two things: Assuming that the sub-components properties are actually valid, I would of course expect whether it passes or fails induction to be the same but when set as
This is mostly right. Calling them automatically isn't a goal in and of itself for me though -- it is just the most straight forward way to invoke them in instances where there is no handle to call it explicitly. I guess to take a step back -- is there a way to traverse a components areas like there is for traversing a components nested components? I did not find one and assume one doesn't exist. But if there was a
The PR as it is now has that function. I think the usages of
I do think we should only ever do I did add another optional function to that trait -- SpinalHDL/core/src/main/scala/spinal/core/formal/Misc.scala Lines 39 to 48 in e5d10af
This makes it more straight forward to add asserts at the interface, but also offers a place on components to formally specify any particularities in what they expect on their interface. For |
I remember that there is a discussion on the ZipCPU's blog about this kind of hierarchical verification in practice, may be it worth to read. |
I had read that before but hadn't internalized it properly I think. It's a good point. I swapped the SpinalHDL/core/src/main/scala/spinal/core/formal/Misc.scala Lines 47 to 50 in d4c39ac
In this way it never assumes something unless the input to the component is valid. To prove it, I also added a test case loosely based on the blog post that shows it fails when the top level input constraints aren't applied. I'd have to benchmark to be sure but I think this still has a large performance improvement. Since we typically will assert that the inputs into a nested component are valid, it will demonstrate the predicate is an invariant to the system (or fail to do so and error out) and then add all the given assumptions as known lemmas as opposed to trying to prove them all out individually. For the implicit / explicit areas thing -- if I added |
…; Add env variable to force asserts for benchmarking
So the target could be add an option If so extend Component and Area's definition to extend |
The principal issue though is that there isn't a list / tree of areas we can walk through. Whether we do it from the Phase context or the top level design object we would need such a mechanism? Having that be a part of Component seems more and more like it makes sense to me. I did benchmark it loosely, it's about twice as fast with the PipelinedMemoryBus formal testers to allow it to use assumptions for nested components instead of asserts. |
The first possible thing is to change the define of Area and Component to
In the FormalAssertable, we define a formalAssertInputs method only, so that in FormalPhase we might do following things.
@Dolu1990 is that possible to apply such a big change to Area and Component? The second is to define formalAssertInputs in Area case by case.
The shortage of this way is that some object apply method just return the output Stream which do not hold the instance, so that it could not call the formalAssertInputs correspondingly. |
This is why I've been sort of fixated on being able to walk through created areas -- there are enough of these patterns (rightly so -- they are much cleaner overall) in play that it seems required.
I can make this name change if desired; let me know what works for everyone. I don't think we should extend Area and Component with the trait though. The way it works in the PR now it just attaches to Component / Areas that need it and that seems to work well. You can still run that exact match code block this way too. I think the big change required though is maintaining a tree of areas for a component. Not sure if that is too big of a change. Quasi related -- I get that there isn't a solid spec written around |
The problem is that we need to change the interface of facilities involved one by one. There might be a lot.
In formal phase, you can traverse all statements, where all assignment, init and related strings are structed and can be accessed.
I suggest to split this PR into several ones, we have talked too much in this thread. |
Motivation
In order to run induction based formal methods on custom components that use various library components, additional asserts are required.
I started with trying to instrument from the outside but I think it'd be better overall to have them in spinal lib proper since a lot of the registers aren't easily accessible from the outside. The objective of this PR is to add in useful asserts and testing and to establish a pattern / methodology for adding ones in future PRs.
This adds self contained asserts for the following components / bundles:
Open Questions
I put the bulk of the new asserts that are really only useful for induction behind a predicate for formalAsserts but not all of them. I'm pretty sure most of the asserts are just discarded trivially at synthesis but I think some of the more complicated ones make sense to not be emitted for general usage.
assume
s? This would speed things up insmtbmc
for complicated designsNotes
On my setup for formal testing, I need
with
I did not check this in since I think this is probably a problem on my setup and didn't want to break CI.
Impact on code generation
Some additional asserts are added to generated code, No behavioral changes should be present in the changeset.
Checklist
/** */
?