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.

diagram
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.

systemverilog
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
end

What 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.

systemverilog
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"));
end

What 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.

diagram
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 only

The 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.