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.
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); endThe scenario class: rand arrays of rand objects
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
endmoduleThe 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
// 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
}
endclassPattern 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.