-
Notifications
You must be signed in to change notification settings - Fork 179
Only preempt simulator testbenches on explicit wait points #1231
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
Conversation
9562d00
to
b122484
Compare
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #1231 +/- ##
==========================================
+ Coverage 89.78% 89.80% +0.02%
==========================================
Files 43 43
Lines 9923 9923
Branches 2395 2400 +5
==========================================
+ Hits 8909 8911 +2
+ Misses 822 820 -2
Partials 192 192 ☔ View full report in Codecov by Sentry. |
285473b
to
dc62cea
Compare
980bf44
to
520e934
Compare
520e934
to
f86c4cb
Compare
Before this commit, testbenches (generators added with `add_testbench`) were not only preemptible after any `yield`, but were *guaranteed* to be preempted by another testbench after *every* yield. This is evil: if you have any race condition between testbenches, which is common, this scheduling strategy will maximize the resulting nondeterminism by interleaving your testbench with every other one as much as possible. This behavior is an outcome of the way `add_testbench` is implemented, which is by yielding `Settle()` after every command. One can observe that: - `yield value_like` should never preempt; - `yield assignable.eq()` in `add_process()` should not preempt, since it only sets a `next` signal state, or appends to `write_queue` of a memory state, and never wakes up processes; - `yield assignable.eq()` in `add_testbench()` should only preempt if changing `assignable` wakes up an RTL process. (It could potentially also preempt if that wakes up another testbench, but this has no benefit and requires `sim.set()` from RFC 36 to be awaitable, which is not desirable.) After this commit, `PySimEngine._step()` is implemented with two nested loops instead of one. The outer loop iterates through every testbench and runs it until an explicit wait point (`Settle()`, `Delay()`, or `Tick()`), terminating when no testbenches are runnable. The inner loop is the usual eval/commit loop, running whenever a testbench changes design state. `PySimEngine._processes` is a `set`, which doesn't have a deterministic iteration order. This does not matter for processes, where determinism is guaranteed by the eval/commit loop, but causes racy testbenches to pass or fail nondeterministically (in practice depending on the memory layout of the Python process). While it is best to not have races in the testbenches, this commit makes `PySimEngine._testbenches` a `list`, making the outcome of a race deterministic, and enabling a hacky work- around to make them work: reordering calls to `add_testbench()`. A potential future improvement is a simulation mode that, instead, randomizes the scheduling of testbenches, exposing race conditions early.
f86c4cb
to
c9ab69e
Compare
To me this is a requirement to do in the future and not optional. One of the problems with classic RTL simulation was that by trying to put determinism into situations where there is a race in the design that the problem was not seen during simulation but that the race actually shows up on a real chip. |
Are we talking about the same thing? The testbenches are very explicitly not a part of the design; they are a part of the test suite. There is nothing to back-annotate because testbenches are behavioral and can only be used to provide stimulus and simulate I/O. |
What I had in the back of my mind is the use of behavioral models for RTL blocks or full-custom blocks for speed reasons. |
These will not be using |
Is |
Yes, |
Then I agree |
Before this commit, testbenches (generators added with
add_testbench
) were not only preemptible after anyyield
, but were guaranteed to be preempted by another testbench after every yield. This is evil: if you have any race condition between testbenches, which is common, this scheduling strategy will maximize the resulting nondeterminism by interleaving your testbench with every other one as much as possible. This behavior is an outcome of the wayadd_testbench
is implemented, which is by yieldingSettle()
after every command.One can observe that:
yield value_like
should never preempt;yield assignable.eq()
inadd_process()
should not preempt, since it only sets anext
signal state, or appends towrite_queue
of a memory state, and never wakes up processes;yield assignable.eq()
inadd_testbench()
should only preempt if changingassignable
wakes up an RTL process. (It could potentially also preempt if that wakes up another testbench, but this has no benefit and requiressim.set()
from RFC 36 to be awaitable, which is not desirable.)After this commit,
PySimEngine._step()
is implemented with two nested loops instead of one. The outer loop iterates through every testbench and runs it until an explicit wait point (Settle()
,Delay()
, orTick()
), terminating when no testbenches are runnable. The inner loop is the usual eval/commit loop, running whenever a testbench changes design state.PySimEngine._processes
is aset
, which doesn't have a deterministic iteration order. This does not matter for processes, where determinism is guaranteed by the eval/commit loop, but causes racy testbenches to pass or fail nondeterministically (in practice depending on the memory layout of the Python process). While it is best to not have races in the testbenches, this commit makesPySimEngine._testbenches
alist
, making the outcome of a race deterministic, and enabling a hacky work- around to make them work: reordering calls toadd_testbench()
.A potential future improvement is a simulation mode that, instead, randomizes the scheduling of testbenches, exposing race conditions early.
Needs: