Part 3 · Constraint Randomization · Intermediate

Randomizing Scenarios, Not Just Items

Sequence-level randomization — transaction counts, ordering, gaps, rand object arrays, and constraining relationships between consecutive transactions.

The bugs live between transactions

Per-item randomization explores the space of single transactions. But the bugs that survive to silicon overwhelmingly involve relationships across transactions : a read immediately after a write to the same address, back-to-back transactions with zero gap, a burst that hits a FIFO exactly at the high-water mark set by the previous ten items. Independent per-item randomize() calls hit such patterns only by luck, because nothing correlates consecutive items. Scenario randomization fixes this by making the collection the randomized object: a scenario class holds a rand array of transactions plus rand shape parameters (count, gaps, ordering), and constraints relate elements to each other with foreach.

diagram
ITEM vs SCENARIO RANDOMIZATION

  PER-ITEM (uncorrelated)
    loop: txn=new; txn.randomize(); drive(txn);
    item[i] independent of item[i-1]
    P(read-after-write same addr) ~ 1/2^32 - never happens

  SCENARIO (one solve over the set)
    scenario.randomize() where scenario contains:
      rand txn q[];  rand n; rand gaps[];
      foreach relates q[i] to q[i-1]
    +---------------------------------------------+
    | n=6   pattern: WR A, WR B, RD A, WR C, ...  |   relationships
    | gaps: 0, 0, 3, 12, 0                        |   GUARANTEED by
    | q[2].addr == q[0].addr  (RAW hazard) <======|== constraint,
    +---------------------------------------------+   not luck
    then: foreach (q[i]) begin drive(q[i]); repeat(gaps[i]) @(posedge clk); end

The scenario class: rand arrays of rand objects

systemverilog
class bus_txn;
  rand bit [31:0] addr;
  rand bit        write;
  rand bit [3:0]  len;
  constraint valid_c { len inside {[1:15]}; addr[1:0] == 0; }
endclass

class rw_hazard_scenario;
  rand int unsigned     n;          // how many transactions
  rand bus_txn          q[];        // the transactions themselves
  rand bit [3:0]        gap[];      // idle cycles after each txn

  constraint shape_c {
    n inside {[4:12]};
    q.size()   == n;
    gap.size() == n;
  }
  constraint gap_c {
    foreach (gap[i]) gap[i] inside {[0:12]};
    gap.sum() with (int'(item)) <= 40;        // bound total test time
  }
  // Inter-item relationships - the whole point:
  constraint hazard_c {
    // txn 0 is a write; some later txn reads the SAME address
    q[0].write == 1;
    q[n-1].write == 0;
    q[n-1].addr == q[0].addr;                  // read-after-write pair
    // consecutive txns: alternate-ish direction, addresses nearby
    foreach (q[i]) if (i > 0)
      q[i].addr inside {[q[i-1].addr - 64 : q[i-1].addr + 64]};
  }

  function void pre_randomize();
    // CRITICAL: allocate and construct BEFORE the solve.
    // randomize() will NOT call new() for null elements - null
    // handles in a rand array are silently skipped.
    q = new[12];                       // max of n's range
    foreach (q[i]) q[i] = new();
    gap = new[12];
  endfunction
endclass

module run;
  initial begin
    rw_hazard_scenario s = new();
    repeat (20) begin
      assert(s.randomize());
      $display("scenario: n=%0d  RAW addr=%h", s.n, s.q[0].addr);
    end
  end
endmodule

The pre_randomize allocation is the step everyone misses. Dynamic-array size() can be constrained and the solver will resize the array — but resizing only constructs element storage for handles, not objects . Null elements are silently skipped per the LRM rule from the silent-failures lesson, so the robust idiom is: allocate to the maximum count and construct every element in pre_randomize; the solver then picks n and solves all constructed elements (constraints index only [0:n-1]; surplus elements get harmless values, or gate their constraints on i < n). Also note q[i-1] references inside foreach — cross-element constraints are ordinary constraints; the solver sees one big conjoined system over every element's fields.


Constraining order and structure

systemverilog
// Pattern A: rand ordering via index permutation
class ordered_scenario;
  rand bus_txn   q[8];
  rand bit [2:0] order[8];            // drive q[order[0]], q[order[1]]...
  constraint perm_c { unique {order}; }  // a permutation of 0..7
  // e.g. constrain "all writes before all reads" in DRIVE order:
  rand int unsigned n_wr;
  constraint split_c {
    n_wr inside {[1:7]};
    foreach (order[k])
      q[order[k]].write == (k < n_wr);  // first n_wr driven are writes
  }
endclass

// Pattern B: phase structure inside one scenario
class burst_then_drain;
  rand bus_txn q[16];
  rand int unsigned burst_n;
  constraint phase_c {
    burst_n inside {[4:12]};
    foreach (q[i]) {
      (i <  burst_n) -> (q[i].write == 1 && q[i].len >= 8);  // flood
      (i >= burst_n) -> (q[i].write == 0 && q[i].len == 1);  // drain
    }
  }
endclass

// Pattern C: walking pattern across items
class walking_addr;
  rand bus_txn q[8];
  constraint walk_c {
    q[0].addr == 32'h1000_0000;
    foreach (q[i]) if (i > 0)
      q[i].addr == q[i-1].addr + (q[i-1].len << 2);  // contiguous stream
  }
endclass

Pattern A is worth dwelling on: unique {order} makes order a random permutation, separating the transaction contents from their drive order — one solve produces both, and ordering properties (writes-before-reads, hazard adjacency) become constraints on the permutation. Pattern B builds multi-phase shape into one scenario, guaranteeing the flood actually precedes the drain. Pattern C chains a recurrence through foreach — each address derived from the previous element's fields — producing a perfectly contiguous stream no independent randomization would ever emit.


Scenario cadence and the two-level solve

  • Solve cost: a scenario is one big constraint system (n transactions x fields each). Keep n in the tens, not thousands — for bulk traffic, randomize a scenario per window, not per test.

  • Cadence layering mirrors knobs: scenario-level rand (shape, counts, hazard placement) per scenario; item fields solved within it; config knobs above both. Three cadences, three kinds of variation.

  • Replay/debug: a scenario randomize is one solver call — one seed reproduces the entire correlated pattern, which is far easier to replay than N coupled per-item calls.

  • Coverage hookup: sample scenario-level covergroups (hazard distance, gap histogram, phase lengths) in the scenario's post_randomize — these are properties no per-item covergroup can see.

  • UVM mapping: this class is exactly what a uvm_sequence body computes — randomize the scenario object first, then foreach start_item/finish_item each element. The constraint architecture is identical outside UVM.

Interview angle

The question appears as “how would you guarantee a read-after-write hazard / back-to-back stress / specific traffic shape?” Weak answers loop per-item randomize and hope; the strong answer is the scenario object: rand array of transactions plus rand shape variables, foreach constraints relating q[i] to q[i-1], solved in one call so the relationship is guaranteed by construction. Two depth signals to volunteer: the pre_randomize allocation requirement (null elements in a rand array are silently skipped — the solver never calls new), and unique{} over an index array for randomized ordering as a constraint-visible permutation. Close with cadence: knobs per phase, scenarios per window, items within scenarios — three nested levels of controlled variation.

Key takeaways

  • Make the collection the rand object: rand txn arrays + rand shape vars, one solve, relationships guaranteed.

  • foreach constraints freely reference q[i-1] — cross-element relations are ordinary solver constraints.

  • Allocate and construct array elements in pre_randomize — the solver never news null handles, it skips them.

  • unique{} on an index array yields a constrained random permutation — contents and order decoupled.

  • Keep scenarios tens-of-items sized; layer cadences: knobs > scenarios > items.

Common pitfalls

  • Constraining q.size() but never constructing elements — null handles silently skipped, scenario half-random.

  • Per-item randomize in a loop expecting correlated patterns — independence makes hazards vanishingly rare.

  • Thousand-item scenarios — solver blow-up; window the traffic instead.

  • Ordering by procedural shuffle after the solve — ordering properties can no longer be constrained or guaranteed.

  • Forgetting gap/timing as rand state — perfectly correlated data with uncontrolled timing still misses the bug.