SystemVerilog Random Stability¶
Preview¶
Random number generators (RNG) are an important piece in the SystemVerilog language and its various verification methodologies. We dive deep into
- How threads and objects generate random numbers
- How ordering of objects and threads in your code affect randomization
- What you should keep in mind as you develop your testbench
All code presented here can be downloaded from GitHub
Introduction¶
Why should I care about Random Stability?¶
As part of the constraint based random verification methodology, during ASIC/FPGA development a suite of tests are regressed nightly and the regression runs with a different seed every night. This is so that every time a test runs with a different random seed, the $urandom, $urandom_range & randomize()
calls in the testbench generate different random numbers, thereby leading to different stimulus and configuration of the DUT.
Any test failures from these regressions have to be recreatable, especially if the failure turns out to be a RTL bug. The process then is to fix the RTL and rerun the failing test with the same random seed which revealed the bug, to verify if the bug is fixed. So, to sucessfully recreate that failing test and verify that the RTL bug is indeed fixed, you need to understand Random Stability and an important property called Hierarchical Seeding.
Understanding Random Stability is key to a successful contraint based random verification strategy
What is Random Stability?¶
Above we have established that the ability to recreate a test failure is important. Now, in order to do that testbenches have to exhibit predictable RNG (Random Number Generator) behavior i.e., the sequence of random numbers generated within the tb_top
, threads and objects should be the same every time for a given seed.
Random Stability basically explains how random numbers are generated in SystemVerilog to ensure re-creatability. It is described by 4 properties
- Object Stability
- Thread Stability
- Hierarchical Seeding
- Initialization RNG
Object & Thread Stability¶
To understand object and thread stability we need to first understand a crucial concept.
- In SystemVerilog everything is a Process - the program-block, every thread, object, function or task call is a separate process and
- An important property of SystemVerilog processes is that they each have an independent RNG (Random Number Generator).
Were the above statements confusing? No worries! There's a lot going on here ... Spend a little time examining Example 1.1 and its corresponding Console Output. Especially observe variables pp
and pt
which are of type process
.
Example 1.1
/* Example 1.1 */
program automatic test;
class pkt;
function int unsigned getrand();
process local_proc;
// Get handle to function's process
local_proc = process::self();
$display("class-function randstate %s",
local_proc.get_randstate());
endfunction
endclass: pkt
initial begin
int unsigned var_a;
pkt p;
process pp, pt;
string pp_randstate, pc_randstate, pt_randstate;
// Get handle to program-block's process
pp = process::self();
// Get and print program-block's RNG's randstate
pp_randstate = pp.get_randstate();
$display("program-block randstate %s", pp_randstate);
fork
begin
// Get handle to thread's process
pt = process::self();
// Get thread's internal RNG randstate
pt_randstate = pt.get_randstate();
$display("thread-block randstate %s",
pt_randstate);
end
join
p = new();
// Classes have an in-built process defined.
// .. You can get object's internal RNG's
// .. randstate by calling obj.get_randstate.
pc_randstate = p.get_randstate();
$display("class-object randstate %s", pc_randstate);
// This function call is a process in itself.
// .. Within this function we can fetch the
// .. process handle and print randstate
p.getrand();
end
endprogram
CONSOLE OUTPUT - Mentor Questa Sim
# program-block randstate MSe2aab990e149a42b970a1cd680bde03f
# thread-block randstate MS438ef57f498295b9ba42179530d3415c
# class-object randstate MS9d943356938f2b6cf29ab3e32f1fd461
# class-function randstate MSd3b01445af5da33c46529df6a5d82faa
CONSOLE OUTPUT - Synopsys VCS
program-block randstate 0000000000000000000000000000000000111010101110111011000011010010
thread-block randstate 0000000000000000000000000000000001011100110000001011100011110010
class-object randstate 00Z1ZZZ1Z0ZZ11XZX11Z1ZX1XZ1Z0X01XZZZZZXZXZXZZZZXXXXZZXZZXXZZXXZZ
class-function randstate 0Z110X0X0Z0XZ1XZZ111ZZ1ZZ11Z1XXZZXXXZXZZXXXZZZXZXXZZZZZZXZXXXZZZ
This is what we see:
- Process is an in-built static class
- You can get the handle to a thread's, program block's or function's process by calling
Process::self()
within it. - I mentioned that every Process has its own independent internal RNG. RNGs have a property called random state. This state dictates the next random number generated by the process's RNG. The current state of the RNG can be queried by using
get_randstate()
. - This random state is represented by a long string. How this string looks is implementation dependent, i.e., it varies from one simulator to another.
The main take away from this example is that threads and objects have their own RNGs. This makes Objects and Threads exhibit deterministic and stable behavior since the $urandom
and randomize()
calls within one thread or object will not affect another. If 10 threads are forked out the order of execution of the threads will not affect random number generation.
This behavior where the randomize()
method in one object instance is independent of calls to randomize()
in other instances is called Object Stability. Similarly, Thread stability is the property by which the $urandom(), $urandom_range()
or std::randomize()
calls within one thread does not affect another thread, hence order of thread execution does not affect random number generation.
Hierarchical Seeding¶
The RNGs of threads and objects (i.e., each process) are hierarchically seeded. When a thread is created, or when an object is new-ed, the random state of its internal RNG is initialized using the next random value from the parent-block's RNG as seed. Let's look at an example.
Example 2.1 shows 5 random numbers being generated before object p
is new-ed. Example 2.2 shows the same for-loop
generating 5 random numbers after the object is newed. Observe the console output - do you see a pattern? In example 2.2, the very first random number (0x1c211259)
was used to seed object p
's internal RNG, the for-loop
after that resumes producing the same sequence of numbers. This is hierarchical seeding in action.
Example 2.1
program automatic test;
class pkt;
rand logic [31:0] saddr, daddr, etype;
endclass: pkt
initial begin
pkt p, p_th;
int unsigned var_a, var_b, var_c;
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("urandom before object new 0x%x", var_a);
end
p = new(); // Object new after for-loop
end
endprogram
/* Example 2.2 */
program automatic test;
class pkt;
rand logic [31:0] saddr, daddr, etype;
endclass: pkt
initial begin
pkt p, p_th;
int unsigned var_a, var_b, var_c;
p = new(); // Object new before for-loop
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("urandom before object new 0x%x", var_a);
end
end
endprogram
CONSOLE OUTPUT
/* Example 2.1 */
# urandom before object new 0x1c211259
# urandom before object new 0x762e4948
# urandom before object new 0x3e10f810
# urandom before object new 0x5fd6b9d8
# urandom before object new 0x1bfad73e
/* Example 2.2 */
# urandom after object new 0x762e4948
# urandom after object new 0x3e10f810
# urandom after object new 0x5fd6b9d8
# urandom after object new 0x1bfad73e
# urandom after object new 0xdae9cf76
Let's look an example with a thread. When compared to the output from example 2.1, in example 2.3 the 1st random number produced by the program-block's RNG (0x1c211259)
is used to seed object p
's RNG, the 2nd number (0x762e4948)
is used to seed the thread's internal RNG. After the threads have joined the program-block's for loop resumes producing the next random numbers in the sequence (0x3e10f810)
. Also, notice how the for-loop
within the thread doesn't affect the program-block's for-loop
outside the thread. This is Random Stability in action.
Example 2.3
program automatic test;
class pkt;
rand logic [31:0] saddr, daddr, etype;
endclass: pkt
initial begin
pkt p, p_th;
int unsigned var_a, var_b, var_c;
// 1. object new
p = new();
p.randomize();
$display("p->saddr 0x%x daddr 0x%x etype 0x%x", p.saddr, p.daddr, p.etype);
// 2. fork thread
fork begin
for (int ii=0; ii<5; ii++) begin
var_b = $urandom();
$display("thread-block urandom 0x%x", var_b);
end
end join
// 3. program for-loop
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("program-block urandom 0x%x", var_a);
end
end
endprogram
CONSOLE OUTPUT
# p->saddr 0x1ff13f40 daddr 0xd87f903a etype 0xd2b1ebf1
#
# thread-block urandom 0x294f77ba
# thread-block urandom 0x60323223
# thread-block urandom 0x4279ce38
# thread-block urandom 0xb50c1a80
# thread-block urandom 0x6bc779b7
#
# program-block urandom 0x3e10f810
# program-block urandom 0x5fd6b9d8
# program-block urandom 0x1bfad73e
# program-block urandom 0xdae9cf76
# program-block urandom 0xdcf9d962
Now that we understand hierarchical seeding we can deduce the following
- In order to re-create our failing test, object and thread creation have to be done in the same order as before.
- Adding code to your testbench could alter the sequence of random numbers generated because you altered the sequence of hierarchical seeding (by creating new objects and threads in between your old code).
- So, in order to recreate a test simulation, new threads, objects and randomize methods have to be added at the end of a code block.
Download example code of the above section
Initialization RNG¶
The next obvious question - if threads and objects are seeded by its parent then who seeds the program-block? The program block has something called an Initialization RNG. You can think of this as the "Root-RNG". These are seeded by the command line arguments +ntb_random_seed
for Synopsys VCS and -sv_seed
for Mentor Graphics Questasim.
Apart from the program block, each module instance, interface instance and package instance have an initialization RNG.
In a Nutshell¶
The train analogy¶
Just so we make sure you've understood this concept - the 4 properties of random stability can be compared to the following train analogy. If you consider a Random Number Generator (RNG) as a train and the sequence of random numbers it generates as its tracks, then
- For a given initial seed, an RNG train generates the same track (or sequence) of random numbers.
- Every thread and object in Systemverilog has its own RNG (train).
- When a thread is created or an object is newed you are creating a new RNG train and this train is initialized with the next random number from its parent train.
Re-creating a test failure¶
- In order to re-create a test failure program, object and thread stability have to be preserved.
- Program, object and thread stability can be achieved as long as object creation, thread creation and random number generation are done in the same order as before.
- In order to maintain random number stability, new objects, threads, and random numbers can be created after existing objects are created.
- If a test failure is due to a RTL bug, due to Random Stability changing the RTL to fix the bug will not affect our capability to re-create the failure. But, if the test sequence or testbench has to be modified, care should be taken preserve program, object and thread stability. - Add any new class member variables to the end - If possible push new objects or threads to the end of the parent class or program block.
Seeding¶
Now that we understand that everything in SV is a Process and every process has an internal RNG, we can play around with the random-state of this RNG.
There are 2 ways to seed the internal RNG of these processes:
srandom()
¶
Example 4.1 should be self explanatory. You can re-seed an object or thread's RNG using process::srandom(seed) within the thread or call obj.srandom(seed) for the object.
Example 4.1
/* Example 4.1 */
program automatic test;
class pkt;
rand int randvar;
endclass:pkt
initial begin
int unsigned var_a;
process pp, pt, pc;
string pp_randstate, pt_randstate, pc_randstate;
pkt p;
fork
begin
pt = process::self();
$display("thread-block randstate %s", pt.get_randstate());
pt.srandom(1234);
$display("Changing thread-block randstate %s", pt.get_randstate());
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("var_a %d", var_a);
end
end
join
p = new();
$display("object's randstate %s", p.get_randstate());
pt.srandom(6666);
$display("Changing object's randstate %s", p.get_randstate());
for(int ii=0; ii<5; ii++) begin
p.randomize();
$display("p: p.randvar %d", p.randvar);
end
end
endprogram
CONSOLE OUTPUT:
# thread-block randstate MS438ef57f498295b9ba42179530d3415c
# Changing thread-block randstate MS72dfdf2597f7d0779f2e5602c960a1e5
# var_a 3151364325
# var_a 797357057
# var_a 1347950301
# var_a 4120409912
# var_a 2561059293
# object's randstate MS9d943356938f2b6cf29ab3e32f1fd461
# Changing object's randstate MS9d943356938f2b6cf29ab3e32f1fd461
# p: p.randvar 1042171828
# p: p.randvar 438788930
# p: p.randvar 1389555781
# p: p.randvar 1120102834
# p: p.randvar 820092897
set_randstate()
¶
This one's for extra credit.
In example 1.1 we saw how you can fetch the randstate of a process's RNG using get_randstate()
. You can also set the randstate of a process's RNG using set_randstate()
.
In example 3.1 we keep it simple - 5 random numbers are first generated within the program block and then 5 more within a thread. This gives us 10 distinct random numbers.
Now let us play around with the thread's RNG and change its state using set_randstate()
. In example 3.2 we record the program block's RNG randstate right at the beginning and then set the thread's RNG to that state before generating the 5 numbers. Look what happens - the thread generates the same random numbers as the program block!
This basically confirms that random numbers returned by randomize method calls ($urandom, $urandom_range, randomize
) solely depend on the state of the process's Random Number Generator at that moment.
Example 3.1 and 3.2
/* Example 3.1 */
program automatic test;
initial begin
int unsigned var_a;
process pp, pt, pc;
string pp_randstate, pt_randstate, pc_randstate;
// Record the state of the program block's RNG right
// at the beginning of the initial block
pp = process::self();
pp_randstate = pp.get_randstate();
$display("program-block randstate %s", pp_randstate);
// Generate 5 random numbers using the program-block's
// Random Number Generator (RNG)
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("program-block var_a %d", var_a);
end
fork
begin
// Since 5 random numbers were generated using
// the program-block's RNG,
// this thread's RNG state gets initialized by the
// 6th random number of the program-block's RNG
pt = process::self();
pt_randstate = pt.get_randstate();
$display("thread-block randstate %s", pt_randstate);
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("thread var_a %d", var_a);
end
end
join
end
endprogram
/* Example 3.2 */
program automatic test;
class pkt;
rand int randvar;
endclass:pkt
initial begin
int unsigned var_a;
process pp, pt, pc;
string pp_randstate, pt_randstate, pc_randstate;
pkt p;
// Record the state of the program block's RNG
// at the beginning of the initial block
pp = process::self();
pp_randstate = pp.get_randstate();
$display("program-block randstate %s", pp_randstate);
// Generate 5 random numbers using the program-block's
// Random Number Generator (RNG)
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("program-block var_a %d", var_a);
end
fork
begin
// This thread's RNG state gets initialized by the
// 6th random number of the program-block's RNG,
// but we over-write this RNG's state to
// "pp_randstate" which was the program-block's
// state at the start of execution.
// Observe the output the 5 random numbers generated
// below should be the same as the above for-loop.
pt = process::self();
pt.set_randstate(pp_randstate);
pt_randstate = pt.get_randstate();
$display("thread-block randstate %s", pt_randstate);
for (int ii=0; ii<5; ii++) begin
var_a = $urandom();
$display("thread var_a %d", var_a);
end
end
join
end
endprogram
/* Example 3.1 Output */
# program-block randstate MS0d46fe96c77f6e86d3ff4d7c45fa86ee
# program-block var_a 750503525
# program-block var_a 354101471
# program-block var_a 1016169009
# program-block var_a 1500937797
# program-block var_a 3106080629
# thread-block randstate MSb0eb5ff259752d3100c01bb5dd17e861
# thread var_a 1381377367
# thread var_a 335209292
# thread var_a 4170423523
# thread var_a 626467677
# thread var_a 1536526296
/* Example 3.2 Output */
# program-block randstate MS0d46fe96c77f6e86d3ff4d7c45fa86ee
# program-block var_a 750503525
# program-block var_a 354101471
# program-block var_a 1016169009
# program-block var_a 1500937797
# program-block var_a 3106080629
# thread-block randstate MS0d46fe96c77f6e86d3ff4d7c45fa86ee
# thread var_a 750503525
# thread var_a 354101471
# thread var_a 1016169009
# thread var_a 1500937797
# thread var_a 3106080629
With that we've pretty much covered everything there is to discuss about Random Stability.