Part 3 · Constraint Randomization · Intermediate
Random Stability & Seeding
Thread and object seed hierarchy, srandom(), why adding a fork shifts downstream values, reproducing failures with seeds, and get/set_randstate.
The seed hierarchy
SystemVerilog defines random stability : random values are not drawn from one global stream but from a hierarchy of per-thread and per-object random number generators (RNGs). The simulation seed initializes the root; every thread (initial/always block, each fork branch) gets its own RNG seeded from its parent thread's RNG at creation; every class object gets its own RNG seeded from the RNG of the thread that constructed it. All obj.randomize() calls draw from the object's RNG; $urandom and std::randomize draw from the calling thread's RNG.
RANDOM STABILITY HIERARCHY
simulation seed (+ntb_random_seed / -svseed / tool option)
│
▼
root RNG of the program/module
│ seeds at creation, in source/creation order
├──────────────┬─────────────────────┐
▼ ▼ ▼
initial #1 RNG initial #2 RNG always RNG
│
│ fork: each child seeded from parent RNG in spawn order
├────────────────┬──────────────┐
▼ ▼ ▼
child A RNG child B RNG parent continues
│
│ obj = new() → object RNG seeded from constructing
▼ thread's RNG at construction moment
object RNG ◄── obj.randomize() draws ONLY from here
thread RNG ◄── $urandom, $urandom_range, std::randomize draw here
STABILITY GUARANTEE: a thread/object's stream depends only on its
own seeding moment — siblings created AFTER it cannot disturb it.Why this design: with one global RNG, ANY change anywhere (one extra $urandom in a debug print) would shift every subsequent random value in the simulation, making failures impossible to reproduce after the smallest edit. The hierarchy localizes the damage: an object's stream depends only on the path of seedings that led to its construction.
Why adding a fork changes downstream randomization
The stability guarantee has a sharp edge: seeds are dealt from the parent RNG in creation order . Adding, removing, or reordering a thread creation or an object construction shifts the seeds dealt to everything created AFTER it in the same parent. The streams of already-created siblings are untouched — but later siblings all change.
initial begin
pkt 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
// ----------------------------------------------
pkt b = new(); // was draw #2, NOW seeded with draw #3
// → every b.randomize() result changes!
void'(a.randomize()); // UNCHANGED — a's RNG was sealed at its new()
void'(b.randomize()); // different values than before the edit
endWhat happened: the fork consumed one draw from the parent RNG, so b was constructed with a different seed, so b's entire object stream differs — even though nothing about b's class or constraints changed. The same applies to inserting an extra new(), an extra $urandom call, or reordering declarations. This is why a passing regression can start failing (or a failing seed stop failing) after an apparently unrelated testbench edit — and why seed-reproduced failures should be re-confirmed after ANY code change.
Minimizing the blast radius
Construct objects and spawn helper threads in a stable order, early, before conditional/debug code paths.
Give long-lived components their own srandom() seed derived from their hierarchical name — decouples them from creation order entirely (this is what UVM does via reseeding).
Keep debug-only $urandom calls out of shared threads, or snapshot/restore the stream around them with get/set_randstate.
srandom(), get_randstate(), set_randstate()
Three manual controls override the automatic seeding. obj.srandom(seed) reseeds an OBJECT's RNG; process::self().srandom(seed) reseeds the calling THREAD's RNG. get_randstate() / set_randstate(s) (on objects and on processes) snapshot and restore the full RNG state as an opaque string — finer than a seed, because it captures mid-stream position.
class gen;
rand bit [15:0] v;
endclass
gen g = new();
initial begin
// 1. Deterministic object stream regardless of creation order:
g.srandom(32'hDEAD_BEEF);
void'(g.randomize()); // same v on every run, every edit
// 2. Snapshot / replay an exact point in the stream:
string s = g.get_randstate();
void'(g.randomize()); // draw X
g.set_randstate(s); // rewind the object's RNG
void'(g.randomize()); // draws X again — identical
// 3. Reseed the current THREAD ($urandom stream):
process::self().srandom(1234);
$display("%0d", $urandom); // deterministic from here on
// 4. Name-based seeding (UVM-style decoupling from creation order):
// derive the seed from a stable identity, not from parent draws
g.srandom(string_hash("env.agent0.gen"));
endWhat each does to reproducibility: srandom makes the stream a pure function of the seed value — creation-order immunity; randstate snapshot/restore lets you replay one randomize() exactly (useful when bisecting which call produced a bad value); name-based seeding is the production-grade pattern because hierarchical names survive code edits that creation order does not.
Reproducing regression failures
The standard workflow: every regression run logs its simulation seed; a failing test is re-run with +ntb_random_seed=N (VCS), -svseed N (Questa/Xcelium-style flows), reproducing the identical random stream — provided the code is unchanged . Then debugging proceeds on a deterministic failure.
SEED-BASED FAILURE REPRODUCTION
nightly regression repro & debug
──────────────────── ─────────────────────────────
run 500 seeds take failing seed 173042
log seed per run ──► rerun: simv +ntb_random_seed=173042
seed 173042 FAILS identical stimulus → same failure
│
▼
add $display / waves and rerun
│
STILL REPRODUCES?
yes │ │ no
▼ ▼
debug it the edit disturbed seeding!
(new object/thread/$urandom
shifted creation-order seeds)
→ use srandom/randstate pins,
or debug via waves onlyThe trap in the right-hand branch is exactly the fork effect from earlier: adding instrumentation that creates a thread, constructs an object, or calls $urandom can shift downstream seeds and make the failure vanish. Passive instrumentation (waves, $display of existing values) is safe; anything that consumes randomness is not.
Interview angle
“You add a $display and the failing seed passes — why?” — $display alone cannot do it, but the edit likely added an object/thread/$urandom that shifted creation-order seeding downstream. Classic stability question.
“Where does obj.randomize() get its randomness vs $urandom?” — object RNG (seeded at construction) vs calling thread's RNG. Different streams.
“How does UVM make component randomization independent of build order?” — it reseeds each component's RNG from a hash of its hierarchical name (name-based srandom).
“Difference between srandom and set_randstate?” — srandom restarts a stream from a seed; set_randstate restores an exact mid-stream position captured by get_randstate.
“Two identical objects constructed in the same thread — same random values?” — no; each new() consumes a parent draw, so they get different seeds.
Key takeaways
Randomness is hierarchical: thread RNGs seed child threads and constructed objects in creation order; obj.randomize() uses the object RNG, $urandom the thread RNG.
Adding/removing any thread, object construction, or $urandom call shifts the seeds of everything created after it in that thread.
srandom() pins a stream to a chosen seed; name-based seeding (UVM-style) decouples streams from creation order entirely.
get_randstate/set_randstate snapshot and restore exact stream positions — replay a single randomize() deterministically.
Reproduce failures by rerunning the logged simulation seed on UNCHANGED code; randomness-consuming instrumentation can destroy the repro.
Common pitfalls
Debug edits that construct objects or call $urandom in shared threads — downstream seeds shift and the failure evaporates.
Assuming one global random stream — leads to wrong expectations about which edits affect which values.
Relying on creation order for stream identity in long-lived components — any refactor reshuffles them; use name-based srandom.
Reseeding with srandom mid-test without recording the seed — you have destroyed reproducibility instead of improving it.
Confusing the simulation seed (whole-run) with object seeds (per-object) — rerunning the sim seed only reproduces if code is identical.