SystemVerilog defines random stability: values come from a hierarchy of generators, not one global stream. The simulation seed starts the root, each thread gets its own generator seeded at creation, and each object gets one seeded by the constructing thread.
Every obj.randomize call draws from the object generator. $urandom, $urandom_range and std::randomize draw from the calling thread generator instead.
One global stream would make any tiny edit shift every later value. The hierarchy localizes the damage: an object stream depends only on the chain of seedings that built it.
Call | Draws from |
|---|---|
obj.randomize() | The object generator, seeded when new() ran |
$urandom and $urandom_range | The calling thread generator |
std::randomize | The calling thread generator |
Seeds are dealt from the parent generator in creation order. Adding, removing or reordering a thread creation or an object construction shifts the seeds of everything created after it in the same parent.
Already-created siblings are untouched. Only the later ones change, which is exactly why an unrelated edit can flip a regression result.
// snippet: monitor_task() stands for any task the debug edit forks
class pkt;
rand bit [7:0] addr;
endclass
module top;
initial begin
pkt a, b; // declare first: an initializer here would run at
// time 0, before any statement of this block
a = new(); // seeded with parent draw #1
// ---- edit: someone adds a debug fork here ----
fork
monitor_task(); // NEW thread: consumes parent draw #2
join_none
// ----------------------------------------------
b = new(); // was draw #2, NOW seeded with draw #3
// every b.randomize result changes
assert (a.randomize()); // UNCHANGED: a was sealed at its new()
assert (b.randomize()); // different values than before the edit
end
endmoduleThe fork consumed one parent draw, so b was constructed with a different seed. The same happens for an inserted new, an extra $urandom call, or reordered declarations.
Construct objects and spawn helper threads in a stable order, early, before conditional or debug paths.
Give long-lived components a seed derived from their hierarchical name, decoupling them from creation order entirely.
Keep debug $urandom calls out of shared threads, or snapshot and restore the stream around them.
Three manual controls override automatic seeding. srandom reseeds an object or thread generator, and get and set randstate snapshot and restore the full generator state as an opaque string.
A snapshot captures the mid-stream position, which is finer than a seed. Name-based seeding is the production pattern because hierarchical names survive edits that creation order does not.
fork is inserted between a = new() and b = new(). Do a.randomize() results change? Do b.randomize() results?a is unchanged: its generator was seeded when it was built. b changes: the fork consumed one draw from the parent, so b was seeded differently.
The bench checks each control: a reseed replays a draw, a snapshot rewinds one, and a thread reseed replays $urandom.
class gen;
rand bit [15:0] v;
endclass
module tb_pin_stream;
gen g = new();
string s;
bit [15:0] first, x, again;
int unsigned u1, u2;
process p;
initial begin
// 1. Deterministic object stream, immune to creation order:
g.srandom(32'hDEAD_BEEF);
if (!g.randomize()) $fatal(1, "FAIL: randomize failed");
first = g.v;
g.srandom(32'hDEAD_BEEF); // same seed again
if (!g.randomize()) $fatal(1, "FAIL: randomize failed");
if (g.v !== first) $fatal(1, "FAIL: reseed did not replay the draw");
// 2. Snapshot and replay an exact point in the stream:
s = g.get_randstate();
if (!g.randomize()) $fatal(1, "FAIL: randomize failed");
x = g.v; // draw X
g.set_randstate(s); // rewind the object generator
if (!g.randomize()) $fatal(1, "FAIL: randomize failed");
again = g.v; // draws X again: identical
if (again !== x) $fatal(1, "FAIL: set_randstate did not rewind");
// 3. Reseed the current THREAD ($urandom stream):
p = process::self();
p.srandom(1234);
u1 = $urandom;
p.srandom(1234);
u2 = $urandom;
if (u1 !== u2) $fatal(1, "FAIL: thread reseed did not replay $urandom");
// 4. Name-based seeding decouples the stream from creation order:
// g.srandom(string_hash("env.agent0.gen"));
$display("PASS: srandom replayed, set_randstate rewound, thread reseed replayed");
$finish;
end
endmodulePASS: srandom replayed, set_randstate rewound, thread reseed replayedEvery regression run logs its simulation seed. A failing seed is re-run with the tool flag on unchanged code, which replays the identical streams for deterministic debugging.
Tool | Seed flag |
|---|---|
VCS | +ntb_random_seed=N |
Xcelium | -svseed N |
Questa | -sv_seed N |
Verilator | +verilator+seed+N, given to the built simulation at run time |
The nightly regression runs many seeds and logs the seed of every run.
Take the failing seed and rerun it with the flag above on unchanged code: same stimulus, same failure.
Add displays or waves and rerun. If it still fails, debug it.
If it no longer fails, the edit consumed randomness (a new object, thread or $urandom call shifted later seeds). Pin the stream with srandom or a saved randstate, or debug from waves only.
The right-hand branch is the fork effect from the second section. Passive instrumentation such as waves or displays of existing values is safe, but anything that consumes randomness can erase the reproduction.
The instrumentation consumed randomness: a new thread, object construction or $urandom call shifted creation-order seeds downstream. Re-check with passive waves only, or pin the streams first.
srandom restarts a generator from a chosen seed value. set_randstate restores the exact mid-stream position previously captured by get_randstate.
Run with Verilator 5.048 (--binary --timing) on 2026-10-18; every bench printed the PASS line shown. Verilator prints a harmless WIDTHTRUNC warning for if (!obj.randomize()), because randomize() returns an int, and an UNSATCONSTR warning when a solve fails on purpose. Icarus 13 does not support constraints.
The Verilator seed flag was checked on 2026-10-18: +verilator+seed+7 gave the same values on two runs and +verilator+seed+8 gave different ones.