Seed 7731 failed last night: a mismatch on the third random job. You rerun it with the same seed and it fails again, good. You add one line to the test to read and print one more register first, rerun, and the failure is gone. All three jobs now have different keys and lengths. Nothing in the DUT changed. Was the bug fixed by a register read? No: the read created a sequence object in the test's thread, and that drew from the thread's random generator. Why did one line move everything, when adding a whole new component would not have?
SystemVerilog gives each thread (process) and each object its own random number generator (RNG). When a thread starts or an object is constructed, it takes its seed from the RNG of the thread that created it. This is random stability: the values a thread draws depend only on its own seed and on the order of random calls and object creations inside that thread. Another thread doing more or less random work does not disturb it.
Think of a card table where every player gets a deck shuffled from the dealer's deck at the moment they sit down. A new player joining another table changes nothing for you. But if your own dealer deals one extra card before your hand, every card you get afterwards is different. UVM adds one more rule on top: it reshuffles each component's phase thread and each sequence and item using a hash of its name, so even the order in which players sit down stops mattering.
Three places in the library do the work. Read them once and the rules follow:
// snippet: from the UVM library source (1800.2-2020), shortened
int unsigned uvm_global_random_seed = $urandom; // uvm_misc.svh: set once from the simulator seed
// uvm_task_phase.svh: every component's run_phase (and other task phases) runs in its own thread
proc = process::self();
proc.srandom(uvm_create_random_seed(phase.get_type_name(), comp.get_full_name()));
// uvm_sequence_item.svh, set_item_context(): called when a sequence or item is started
reseed(); // if use_uvm_seeding: srandom(uvm_create_random_seed(get_type_name(), get_full_name()))
// uvm_create_random_seed(type_id, inst_id): a hash of type and instance name with the global
// seed, plus a counter that increments each time the same name asks again.A component's run_phase thread is seeded from its full name, not from its position in the tree. Adding a sibling component cannot move it.
A sequence or item is reseeded from its type, full name (sequencer path, sequence name, item name) and a counter. The k-th item named it in pkt on env.in_agt.sqr always gets the same seed.
A plain object such as dma_job_cfg is never reseeded by UVM. It takes its seed from the thread that constructs it, so anything else that thread does first, a $urandom or even constructing another object, moves it.
The scratch tests extend the bench's dma_rand_test (three random jobs) and add a subscriber that prints the first data word of each packet. One adds two components that draw random numbers of their own; one draws a single $urandom before the jobs; one starts the same packet sequence under another instance name.
// snippet: scratch run alongside the DMA bench
class s_seed_more_test extends s_seed_test; // s_seed_test = dma_rand_test + the word printer
`uvm_component_utils(s_seed_more_test)
s_noise noise; // its run_phase calls $urandom and randomize()
dma_activity_watchdog wd;
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
function void build_phase(uvm_phase phase);
super.build_phase(phase);
noise = s_noise::type_id::create("noise", this);
wd = dma_activity_watchdog::type_id::create("wd", this);
endfunction
endclass
class s_seed_call_test extends s_seed_test;
`uvm_component_utils(s_seed_call_test)
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
task run_phase(uvm_phase phase);
void'($urandom); // one extra draw in the test's own thread
super.run_phase(phase);
endtask
endclassAll runs are scratch runs on Verilator with the 1800.2 library. The baseline is exactly the bench's dma_rand_test log:
UVM_INFO dma_tb.sv(731) @ 0: uvm_test_top [JOB] key=90fdd8fd len=10 irq_en=1
UVM_INFO dma_tb.sv(731) @ 645000: uvm_test_top [JOB] key=0e564b57 len=1 irq_en=1
UVM_INFO dma_tb.sv(731) @ 885000: uvm_test_top [JOB] key=5a1793f4 len=3 irq_en=1
UVM_INFO adv_tb.sv(286) @ 1305000: uvm_test_top.words [WORDS] first word of each packet: 1364cd0b 194e3e45 6114f42cChange | Jobs (key, len) | First data words | Why |
|---|---|---|---|
Two extra components with their own randomness | unchanged | unchanged | The test thread and the items are seeded by name. |
One | all three changed ( | first unchanged ( | The job objects come from the test thread. Item seeds did not change, but longer packets shift the per-name counter. |
One debug | all three changed ( | first unchanged, then different | The read constructs an |
Delete the | all three changed ( | first unchanged, then different | The library constructs objects inside |
A printed | unchanged | unchanged | No change observed on this tool, although |
Packet sequence named | unchanged | all changed ( | Item full names changed, so their seeds did. |
| unchanged | all changed ( | Items now take their seed from the creating thread. |
| all changed ( | all changed | The global seed went from 4133899051 to 2516928731. |
Verilator matched UVM's model in every case here. Commercial simulators implement the IEEE 1800 stability rules too, but the actual values for a given seed differ between tools and versions: a seed replays on the same tool, version and build, not across them.
A seed alone reproduces nothing. Reproduction needs the same compiled bench, the same command line and the same seed on the same tool version. Keep a replay record with every failure: the simulator seed as the tool printed it, the full command line (+UVM_DUMP_CMDLINE_ARGS), the source revision and compile defines, and the tool and UVM library versions.
The replay record can also hold the random state itself. Every class object and thread has one: string s = obj.get_randstate(); captures it, and obj.set_randstate(s); restores it, so a debug run can snapshot a sequence's state just before the failing job, then replay only that job with extra prints, without re-running everything before it. It is per object or thread, not global, and only valid on the same tool and build that produced it.
When you debug the replay, keep edits out of the stimulus thread. Verbosity plusargs and new components or subscribers are safe, because UVM seeds them by name. Inside the thread, anything that constructs an object can move later values: the debug register read did. A printed uvm_info left the jobs unchanged on Verilator, but it constructs a report message object, and the IEEE 1800 object-stability rule seeds every new object from its thread, so do not rely on that across simulators; $display constructs nothing. Capture the values you need from the failing log before you edit.
Tool | Seed switch (check your version's manual) |
|---|---|
Siemens Questa |
|
Cadence Xcelium |
|
Synopsys VCS |
|
Verilator (this course) |
|
Fixed seeds for gates, random seeds for discovery. Smoke and merge-gate runs use a fixed list of seeds so a red result means the code changed. Nightly runs draw fresh seeds to find new bugs.
Record the seed the tool chose, not the one you meant to pass. With random the simulator picks; grep the value from the log into the result database.
Rerun failures with the same seed first, then with a handful of new seeds. A failure that appears on one seed in a thousand is still a bug; one that disappears on rerun with the same seed means the replay record is incomplete.
Name repeated starts distinctly. With one shared name, each start's seed depends on how many starts with that name came before it, so inserting one shifts all the rest.
Randomize stimulus in sequences and items, which UVM reseeds by name, rather than in plain objects in a long-lived test thread: the experiments above show which one survives an edit.
This is the hook, and it happens on real teams every month. A failing seed is rerun, it fails, and an engineer adds a few lines to the test's run_phase to dump state: a temporary object, a randomized helper, a $urandom to pick which register to read. The rerun passes. The conclusion "it was a flaky failure" goes into the ticket, and the bug ships.
The scratch runs show the mechanism. One debug register read at the top of the test turned job 1 from len=10 into len=7 and changed every key; one extra $urandom, or deleting one config_db::get, did the same. The bug needed the original stimulus, and the edit removed it. The fix is a habit: debug a replay only with edits that draw nothing from the stimulus thread, and if you must change the thread, first save the failing transactions (keys, lengths, data) from the log and drive them with a directed test.
axis_pkt_seq named pkt three times in a loop. A colleague renames it to $sformatf("pkt%0d", j) for clearer logs. What happens to the data words, and is it a problem?The items' full names change, so their seeds change and every data word differs for the same seed. That is not a bug, but every recorded failing seed from before the rename now produces different stimulus. Make such changes deliberately, and re-baseline the fixed gate seeds afterwards.
The simulator version (values for a seed are tool and version specific), the compile options and defines (a different build is a different program), and the full command line, including plusargs that change which code runs. Also check that the farm really used the recorded seed, not random.
dma_job_cfg's randomization move when the test thread draws one more random number, while axis_item data does not?dma_job_cfg is a plain object created and randomized in the test's run_phase thread; it takes its seed from that thread's RNG at construction, so an extra draw shifts it. Items are reseeded by set_item_context when they are started, from a hash of their type and full name plus a counter, independent of the creating thread.
Next: a seed replays only on the same library. UVM 1.2 to IEEE 1800.2: the delta table lists what changes when the library does.