Skip to content

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 and 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 re-creatable, especially if the failure turns out to be a RTL bug. The process then is to fix the RTL and re-run the failing test with the same random seed which revealed the bug, to verify if the bug is fixed. So, you should care about Random Stability because to sucessfully re-create that failing test and verify that the RTL bug is after all 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've convincingly established that re-creating 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 program-block, 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

  1. Object Stability
  2. Thread Stability
  3. Hierarchical Seeding
  4. 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).

Did the above sentence make your head ache a little? 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:

  1. Process is an in-built static class
  2. You can get the handle to a thread's, program block's or function's process by calling Process::self() within it.
  3. 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().
  4. 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.


Subscribe

Get Notified when a new article is published!


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

  1. For a given initial seed, an RNG train generates the same track (or sequence) of random numbers.
  2. Every thread and object in Systemverilog has its own RNG (train).
  3. 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.

Random Stability Train Analogy

Random Stability Train Analogy

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.


Subscribe

Every month or so I send out a newsletter with lessons from my experience, notable technical papers, and notifications about new articles.

If you found this content useful then please consider supporting this site! 🫶

Buy Me A Coffee