Skip to content

UVM

Out-of-Order Checking

The harder pairing — when the DUT may legally complete transactions out of order, the scoreboard pairs each output with its expected by a matching key (ID) via an associative array, tolerating legal reordering, and using a per-ID queue to handle ID reuse and intra-ID ordering.

Scoreboards · Module 18 · Page 18.6

The Engineering Problem

In-order checking pairs outputs to expected positionally via a FIFO — correct only if the DUT produces outputs in input order (Module 18.5). But many DUTs legally complete transactions out of order: a multi-channel block, a cache (a hit returns before an earlier miss), a variable-latency pipeline, an AXI interconnect (responses can return in any order across IDs). For these, the Nth output is not the Nth input's resultpositional pairing would flood false mismatches on legal reordering (the 18.5 DebugLab). So you need a different pairing: match each output to its expected by something other than position. That something is a key — an identifier (a transaction ID, a tag) that travels with the transaction and says which input this output is for. But keyed pairing brings its own complications: missing keys (an output for no expected), duplicate keys, and — the sharp one — ID reuse (a limited ID space means an ID is reused once its prior transaction completes, so a naive key→expected map collides). The problem this chapter solves is out-of-order pairing: matching by key, with an associative array, handling the cases.

Out-of-order checking pairs each observed output with its expected by a matching key (an ID), not by position — so it tolerates legal reordering. The predictor inserts expected into an associative array keyed by the ID; on each observed output, the scoreboard looks up the expected with the same ID, compares it (data checking, Module 18.4), and removes the entry. Because the pairing is by key lookup (not position), legal reordering does not cause mismatches (output with id=2 matches expected id=2, whatever order it arrives). It must handle the cases: a missing key (output with no matching expected → extra/wrong), a leftover at end of test (expected never matched → drops), and — criticallyID reuse: when IDs are reused (a limited space, like AXI IDs), a naive single-entry map collides (a reused ID overwrites an outstanding one), so you need a per-ID queue (a FIFO per key) to hold multiple outstanding same-ID transactions, matched in order within the ID. This chapter is out-of-order checking: the associative-array pattern, tolerating reordering, the cases, and ID reuse.

How does out-of-order checking pair each output with its expected by a matching key (via an associative array) so it tolerates legal reordering — and why does ID reuse force a per-ID queue rather than a single-entry-per-key map?

Motivation — why keyed pairing, and why IDs get reused

Out-of-order checking exists because legal reordering needs a pairing that survives it — and the reality of limited ID spaces forces per-ID queues. The reasons:

  • Many DUTs legally reorder. A cache (hit before earlier miss), a multi-channel block, a variable-latency pipeline, an AXI fabric — all legally complete transactions out of order. Positional pairing (in-order, Module 18.5) false-mismatches on this legal reordering.
  • A key identifies which input an output is for. When order can't be relied on, each transaction carries a key (an ID, a tag) that says which input an output belongs to. Pairing by key matches an output to its expected regardless of order.
  • The associative array makes keyed pairing natural. A hash map (expected[id]) inserts by key (predict) and looks up by key (on output) — O(1) pairing by ID, no positional assumption.
  • IDs are a limited resource — they get reused. An ID space is finite (e.g., AXI has a bounded number of IDs). So an ID is reused: once a transaction with id=5 completes, a new transaction can also use id=5. If both are outstanding at once (the new one issued before the old completes — or the old's response is in flight), a single-entry expected[5] collides.
  • A per-ID queue handles reuse (and intra-ID order). Holding a FIFO per ID (expected[id] is a queue) lets multiple outstanding same-ID transactions coexist, matched in order (which also enforces intra-ID ordering, often required by protocols like AXI: same-ID responses must come back in order).

The motivation, in one line: legal reordering needs pairing that survives it — a key (ID) that identifies which input an output is for, looked up in an associative array (not paired by position) — but because ID spaces are finite, IDs are reused, so a single-entry-per-key map collides on outstanding reuse, requiring a per-ID queue that holds multiple same-ID transactions in order.

Mental Model

Hold out-of-order checking as a coat check with numbered tickets — look up the matching number, regardless of return order, with a small stack per number if numbers repeat:

Out-of-order checking is a coat check with numbered tickets: each coat gets a numbered ticket, and when a coat comes back — in any order — you look up the matching numbered ticket, not the next one on the stack. Numbers tolerate out-of-order return. But if the cloakroom reuses numbers (a small numbered rack), you must keep a small stack of tickets per number, or a reused number would lose the earlier coat. Picture the coat check again — but now with numbered tickets (vs the stacked, order-based tickets of in-order checking, Module 18.5). Each coat (input) gets a numbered ticket (a key/ID), and you file the ticket by its number (insert into the associative array). When a coat is returned — in any order (a fastest-ready-first valet) — you read its number and look up the matching ticket (by key), not the next one on a stack. The number pairs the coat to its ticket regardless of return order — so out-of-order return is fine (it's legal for this valet). Three cases. A coat with a number you have no ticket for (a missing key): an extra or wrong coat. Leftover tickets at closing (keys never matched): lost coats (drops). And the sharp one: if the cloakroom reuses numbers (a limited numbered rack — number 5 is handed out again once that coat is returned), you can't keep just one slot per number — because two coats might both have number 5 at once (the old one's return in flight, the new one just checked in). A single slot would have the new ticket overwrite the old, losing the old coat. So you keep a small stack of tickets per number (a per-ID queue) — and when a number-5 coat returns, you take the oldest number-5 ticket (matching same-number coats in order). The numbered tickets tolerate out-of-order across numbers, and the per-number stack handles reused numbers in order within a number.

So out-of-order checking is a coat check with numbered tickets: file tickets by number (insert by key into the associative array), and look up the matching number for each returned coat (any order). It tolerates out-of-order return (the number pairs regardless of order). It handles missing numbers (extra/wrong), leftover tickets (drops), and — because numbers are reused — keeps a small stack per number (a per-ID queue) so multiple same-number coats are matched in order. Look up the number, not the next ticket — and keep a stack per number when numbers repeat.

Visual Explanation — the associative-array (key lookup) pattern

The defining picture is the keyed map: the predictor inserts expected by ID, and the scoreboard looks up by the output's ID — pairing by key, not position.

The predictor inserts expected by ID; the scoreboard looks up by the output's ID and comparesinsertexpected[id]observed output (has id)observedoutput (has…lookupexpected[output.id]compare + removePredictorcomputes expectedAssociative arrayexpected keyed by IDOutput monitoroutputs (any order)Scoreboardlookup by id, pairCompare + verdictmatch / mismatch12
Figure 1 — the associative-array pattern of out-of-order checking. The predictor inserts each expected into an associative array keyed by the transaction's ID. On each observed output, the scoreboard reads the output's ID, looks up the expected with the same ID, compares it (data checking), and removes the entry. The pairing is by key lookup, not position, so outputs arriving in any order are each matched to their correct expected. A lookup that finds nothing is a missing key (extra); entries left at end of test are drops.

The figure shows the associative-array patternkeyed pairing. The predictor computes the expected and inserts it into an associative array keyed by the transaction's ID (expected[id]). The output monitor observes outputs (in any order), each carrying its ID. On each observed output, the scoreboard reads the output's ID, looks up the expected with the same ID (expected[output.id]), compares it (data checking, Module 18.4), and removes the entry → match / mismatch. The crucial reading is that the pairing is by key lookup, not position: the warning-colored associative array is indexed by ID, so an output's ID directly retrieves its expectedregardless of what order the output arrived. This is the fundamental difference from in-order checking (Module 18.5): in-order pops the front (positional, FIFO); out-of-order looks up by key (associative array). The consequence is that legal reordering is tolerated — outputs arriving in any order are each matched to their correct expected (output id=2 → expected id=2, whatever order). The cases fall out of the lookup: a lookup that finds nothing (the ID is not in the map) is a missing key (an extra/wrong output); entries left in the map at end of test are drops (expected that never got a matching output). The success-colored scoreboard consumes outputs by looking up and removing their keyed expected. The diagram is the anatomy of out-of-order checking: an associative array keyed by ID, inserted by the predictor and looked up by the scoreboard per outputkeyed pairing that tolerates reordering, with missing-lookup = extra and leftover = drops. The shift from position (FIFO) to key (map) is the whole idea.

RTL / Simulation Perspective — lookup by key, and the per-ID queue for reuse

In code, the scoreboard uses an associative array keyed by ID — and, to handle ID reuse, a queue per ID (FIFO per key). The code shows both.

out-of-order checking: lookup by ID; a per-ID QUEUE handles ID reuse + intra-ID order
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
class ooo_scoreboard extends uvm_scoreboard;
  // a QUEUE of expected PER ID (handles reuse: multiple outstanding same-ID, matched in order)
  bus_txn expected_by_id[bit [3:0]][$];      // associative array: id -> FIFO of expected
 
  // EXPECTED from the predictor: push onto the per-ID queue (keyed insert)
  function void write_expected(bus_txn e);  expected_by_id[e.id].push_back(e);  endfunction
 
  // OBSERVED output: look up by ID, pop the oldest for that ID, compare
  function void write_actual(bus_txn observed);
    if (!expected_by_id.exists(observed.id) || expected_by_id[observed.id].size() == 0) begin
      `uvm_error("SB", $sformatf("MISSING KEY: output id=%0h has no matching expected", observed.id))
      return;                                  // extra / wrong id
    end
    bus_txn expected = expected_by_id[observed.id].pop_front();  // oldest expected for THIS id
    if (!observed.compare(expected))           // data checking (Module 18.4)
      `uvm_error("SB", $sformatf("MISMATCH id=%0h: got data=%0h exp=%0h", observed.id, observed.data, expected.data))
    if (expected_by_id[observed.id].size() == 0) expected_by_id.delete(observed.id);  // tidy empty key
  endfunction
 
  // END OF TEST: any leftover expected across all IDs = DROPS
  function void check_phase(uvm_phase phase);
    int drops = 0;
    foreach (expected_by_id[id]) drops += expected_by_id[id].size();
    if (drops != 0) `uvm_error("SB", $sformatf("DROPS: %0d expected outputs never arrived", drops))
  endfunction
endclass
// ✗ a SINGLE entry per id (expected_by_id[id] = e) COLLIDES on ID reuse — overwrites the outstanding one (DebugLab)

The code shows the keyed lookup with the per-ID queue. The scoreboard holds an associative array of queues (expected_by_id[id] is a FIFO of expected for that id). write_expected pushes the predictor's expected onto the per-ID queue (keyed insert). write_actual, on each observed output, looks up by ID: if the ID is not present (or its queue is empty), it's a missing key (uvm_error — extra/wrong); otherwise it pops the oldest expected for that ID (pop_front() — matching same-ID transactions in order), compares (data checking), and tidies the empty key. check_phase (end of test) sums leftover expected across all IDs = drops. The closing comment marks the cardinal difference: a single entry per ID (expected_by_id[id] = e) collides on ID reuse — a new same-ID transaction overwrites the outstanding one, losing it (the DebugLab) — which is why the value is a queue, not a single entry. The shape to carry: out-of-order checking is insert-by-key / lookup-by-key with a per-ID queue — the predictor pushes expected onto expected_by_id[id], the scoreboard looks up by the output's id, pops the oldest for that id, and compares, with missing-key (extra) and end-of-test leftover (drops) checks. The per-ID queue (vs a single entry) is what handles ID reuse (multiple outstanding same-ID, matched in order) and intra-ID ordering (same-ID expected matched in push order). The associative array gives the keyed (order-tolerant) pairing; the per-ID queue gives the reuse-safe, intra-ID-ordered matching.

The defining property of out-of-order checking is that keyed pairing tolerates legal reordering — where positional (in-order) pairing would false-mismatch. Seeing the contrast shows why keyed pairing is necessary for an out-of-order DUT.

Positional pairing false-mismatches on reordering; keyed pairing matches each output by ID correctlypositional (FIFO)id=2 vs front id=1 → false mismatchid=2 vs frontid=1 → false…keyed(lookup)id=2 → expected id=2 → matchid=2 →expected id=…Out-of-order outputsid=2, id=1, id=3 (legal)Positional pairingin-order / FIFOFalse mismatchwrong-expected pairingKeyed pairinglookup by IDCorrect matchtolerates reordering12
Figure 2 — keyed pairing tolerates legal reordering; positional pairing does not. The DUT legally produces outputs out of order: id=2, then id=1, then id=3. In-order (positional, FIFO) pairing matches output id=2 against the front expected (id=1) — a false mismatch — because it pairs by position. Out-of-order (keyed) pairing looks up each output by its ID: output id=2 matches expected id=2, output id=1 matches expected id=1 — correct, no mismatch — because it pairs by key, regardless of order. For an out-of-order DUT, keyed pairing is the only correct approach.

The figure contrasts positional and keyed pairing on a legally-reordered output stream. The DUT legally produces outputs out of order: id=2, then id=1, then id=3. With positional (in-order / FIFO) pairing, output id=2 is matched against the front expected (id=1) — a false mismatch — because it pairs by position (the 18.5 DebugLab). With keyed pairing, the scoreboard looks up each output by its ID: output id=2 matches expected id=2, output id=1 matches expected id=1correct, no mismatch — because it pairs by key, regardless of order. The verification insight is that keyed pairing is the only correct approach for an out-of-order DUT: when reordering is legal, the only way to pair correctly is by something that travels with the transaction and survives reordering — the key (ID). Positional pairing fails because position is exactly what reordering changes; keyed pairing succeeds because the key is invariant under reordering. The warning-colored out-of-order outputs break the default-colored positional pairing (false mismatch) but are handled by the brand-colored keyed pairing (correct match). This is the whole reason out-of-order checking exists: it replaces the position-based pairing (which reordering destroys) with a key-based pairing (which reordering can't). The trade-off (vs in-order, Module 18.5): out-of-order checking tolerates reordering by design, so it doesn't catch reordering as a bugcorrect, because reordering is legal for this DUT; but if a protocol requires intra-ID ordering (AXI: same-ID responses in order), the per-ID queue re-introduces an in-order check within each ID (Figure 1's queue). The figure is the justification for keyed pairing: for a DUT that legally reorders, pair by the key that survives reordering, not the position that reordering changeskeyed pairing is correct, positional is false-mismatch. Match by the invariant (the ID), not the variant (the position).

Runtime / Execution Flow — keyed checking with reuse and the end check

At run time, out-of-order checking runs a per-output loop (lookup by key, pop the per-ID queue, compare) plus the end-of-test leftover check. The flow shows it.

Out-of-order checking: lookup by ID, pop the per-ID queue, compare or flag missing key; at end, flag leftover dropsper output: lookup per-ID queue by output.id → empty? missing key : pop oldest for that id + compare — end of test: leftover across IDs? dropsper output: lookup per-ID queue by output.id → empty? missing key : pop oldest for that id + compare — end of test: leftover across IDs? drops1Output arrives: look up its ID's queueindex the associative array by the output's ID to find its per-IDqueue of expected.2Empty or absent? missing keyno expected for this ID — an extra or wrong-ID output; report it.3Pop the oldest for that ID, comparematch same-ID in order; data-check the pair — pass or precisemismatch (Module 18.4).4End of test: leftover across IDs? dropssum the remaining expected across all IDs — those outputs neverarrived.
Figure 3 — out-of-order checking per output, plus the end-of-test check. On each observed output: look up the per-ID queue by the output's ID. If it's empty or absent, the output is a missing key (extra/wrong). Otherwise, pop the oldest expected for that ID (matching same-ID transactions in order), and compare it (data checking) — pass or precise mismatch. At end of test, any expected left across all IDs are drops. The per-ID queue handles ID reuse (multiple outstanding same-ID) and intra-ID ordering, while the keyed lookup tolerates cross-ID reordering.

The flow shows out-of-order checking's two parts with the per-ID queue. Lookup (step 1): on each output, index the associative array by the output's ID to find its per-ID queue of expected. Missing key (step 2): if the queue is empty or absent, there's no expected for this ID — an extra or wrong-ID output; report it. Compare (step 3): pop the oldest expected for that ID (matching same-ID transactions in order) and data-check the pair (Module 18.4) — pass or precise mismatch. End of test (step 4): sum the remaining expected across all IDs = drops. The runtime insight is the two layers of the structure: the associative array (keyed by ID) gives the cross-ID order tolerance (look up any ID regardless of arrival order), and the per-ID queue (FIFO per key) gives the intra-ID ordering and reuse handling (multiple outstanding same-ID matched in order). So out-of-order checking is in-order within each key, out-of-order across keys — which is exactly the semantics of many real protocols (AXI: responses for different IDs in any order, responses for the same ID in order). The per-output loop handles the bulk (lookup, pop, compare, missing-key), and the end check handles completeness (leftover = drops) — mirroring in-order checking's structure (Module 18.5) but with keyed lookup replacing positional pop. This is why out-of-order checking is in-order checking generalized: replace the single FIFO (positional) with a map of FIFOs (keyed) — the map tolerates cross-key reordering, each FIFO preserves intra-key order, and the completeness checks (missing-key, leftover) carry over. The flow is the complete out-of-order check: for each output, look up its ID's queue and pop-and-compare (or flag missing); at end, flag leftover dropskeyed, reuse-safe, intra-ID-ordered, and cross-ID-order-tolerant.

Waveform Perspective — outputs in any order, each matched by ID

Out-of-order checking is visible on a timeline: expected are inserted by ID, outputs arrive out of order, and each is looked up by its ID and matched — legal reordering causing no mismatch. The waveform shows it.

Out-of-order outputs, each matched by ID lookup — legal reordering causes no mismatch

12 cycles
Out-of-order outputs, each matched by ID lookup — legal reordering causes no mismatchoutput id=2 (first, out of order) → lookup expected[2] → matchoutput id=2 (first, ou…output id=1 → lookup expected[1] → match (order doesn't matter)output id=1 → lookup e…output id=3 → lookup expected[3] → match — all matched by ID, no false mismatchoutput id=3 → lookup e…clkout_validout_id--2--1--3------------lookup_hitsb_matcht0t1t2t3t4t5t6t7t8t9t10t11
Figure 4 — keyed pairing tolerates reordering. Expected are inserted by ID: expected for id=1, id=2, id=3 (the map holds all three). The DUT produces outputs OUT OF ORDER: out_id=2 first, then out_id=1, then out_id=3. Each output is looked up by its ID and matched to the expected with the same ID: out_id=2 matches expected id=2 (sb_match), out_id=1 matches expected id=1, out_id=3 matches expected id=3 — all match, despite the reordering. The keyed lookup pairs by ID, not position, so the legal out-of-order completion produces no false mismatch.

The waveform shows keyed pairing tolerating reordering. Expected are inserted by ID (for id=1, id=2, id=3 — the map holds all three). The DUT produces outputs out of order: out_id=2 first, then out_id=1, then out_id=3. Each output is looked up by its ID (lookup_hit) and matched to the expected with the same ID: out_id=2 matches expected id=2 (sb_match), out_id=1 matches expected id=1, out_id=3 matches expected id=3all match, despite the reordering. The crucial reading is that the arrival order (2, 1, 3) is different from the insertion order (1, 2, 3), yet every output matches — because the lookup is by ID, not position. The out_id=2 arriving first is not a problem: the scoreboard looks up expected[2] (not the front of a queue) and matches it. The picture to carry is the order-independence of keyed pairing: the ID on each output directly retrieves its expected, so the order the outputs arrive in doesn't matterlegal reordering produces no false mismatch (contrast in-order, Module 18.5, where this exact reordering would flood mismatches). The counterfactual (a missing key): if an output arrived with out_id=7 (not in the map), lookup_hit would be low and the scoreboard would flag a missing key (extra/wrong). Reading this waveform — is each output looked up by its ID and matched, regardless of arrival order? do legal reorderings cause no mismatch? — confirms the out-of-order check. The outputs arriving out of order, each matched by ID lookup with no false mismatch is the signature of out-of-order checking: keyed pairing that tolerates the legal reordering an out-of-order DUT producesmatch by the ID, regardless of order.

DebugLab — the scoreboard that lost transactions on ID reuse

Drops and mismatches because a single-entry-per-ID map collided on reused IDs

Symptom

An out-of-order scoreboard worked on tests with few outstanding transactions — but on high-throughput tests (many transactions in flight at once), it reported intermittent drops (expected that "never arrived") and mismatchesinconsistent with the DUT, which was functionally correct. The failures correlated with ID reuse: they appeared when the same ID was issued again while a prior transaction with that ID was still outstanding (its response in flight). With unique IDs (low throughput), no failures; with reused IDs (high throughput), failures.

Root cause

The scoreboard used a single entry per ID (expected[id] = e) — so when an ID was reused while a prior same-ID transaction was outstanding, the new expected overwrote the old one in the map, losing it (→ a drop) and mis-pairing the next same-ID output (→ a mismatch):

why a single-entry-per-ID map collides on reuse
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
✗ SINGLE entry per ID → a reused ID OVERWRITES the outstanding one (collision):
  expected[e.id] = e;            // ✗ one slot per id
  // id=5 issued (expected E5a stored).  id=5 REUSED while E5a still outstanding:
  expected[5] = E5b;             //   ← OVERWRITES E5a → E5a LOST (drop), and the next id=5 output
  //   gets paired with E5b instead of E5a → mismatch.  Both failures stem from the collision.
 
✓ PER-ID QUEUE → multiple outstanding same-ID coexist, matched in order:
  expected_by_id[e.id].push_back(e);                 // a FIFO per id
  // id=5 issued: queue[5] = [E5a].  id=5 reused: queue[5] = [E5a, E5b]  (both held)
  // first id=5 output → pop E5a (oldest); second id=5 output → pop E5b → correct, in order

This is the ID-reuse collision bug — using a single entry per key when IDs are reused, so a reused ID overwrites an outstanding one, losing it. The scoreboard keyed by ID with one slot per ID (expected[id] = e), which assumes at most one outstanding transaction per ID at a time. That holds only when IDs are unique (or not reused while outstanding) — true at low throughput. But at high throughput, the same ID gets issued again before its prior transaction completes (the prior's response is still in flight) — so two transactions with the same ID are outstanding at once. The new one's expected[5] = E5b overwrites the old E5a in the single-slot map — losing E5a (which then never matches its output → a drop) and mis-pairing: the next id=5 output (the response for E5a, arriving first) gets paired with E5b (the only entry) → a mismatch. Both the drops and the mismatches stem from the collision. The tell is exactly the correlation with throughput / ID reuse: unique IDs (low throughput) → no failures; reused IDs (high throughput) → failures — because the collision only happens when two same-ID transactions overlap. The fix is a per-ID queue (a FIFO per key): expected_by_id[id].push_back(e) holds multiple outstanding same-ID transactions (queue[5] = [E5a, E5b]), and each id=5 output pops the oldest (E5a first, then E5b) — matching same-ID transactions in order, correctly, regardless of reuse. The general lesson, and the chapter's thesis: out-of-order checking by ID assumes the ID uniquely identifies an outstanding transaction — but ID spaces are finite, so IDs are reused, and a single-entry-per-ID map collides when a reused ID is outstanding alongside its prior (overwriting and losing it); use a per-ID queue (FIFO per key) so multiple outstanding same-ID transactions coexist and are matched in order (which also enforces intra-ID ordering, often required by the protocol). When IDs repeat, key to a stack, not a slot. And the tell of this bug is failures that correlate with throughput / ID reuse: unique IDs fine, reused IDs broken.

Diagnosis

The tell is failures that correlate with ID reuse / throughput. Diagnose ID-reuse collisions:

  1. Check whether failures scale with throughput. Drops and mismatches that appear only at high throughput (many outstanding) point at ID-reuse collisions.
  2. Confirm the ID space is reused. If IDs are reused while prior same-ID transactions are outstanding, a single-entry-per-ID map collides.
  3. Look for a single slot per ID. A map storing one expected per ID (rather than a queue) loses an outstanding transaction when its ID is reused.
  4. Check the protocol's ID semantics. Whether same-ID transactions can overlap and must complete in order is a spec property that dictates the per-ID queue.
Prevention

Key to a per-ID queue, not a single slot:

  1. Use a per-ID queue. Store a FIFO of expected per ID, so multiple outstanding same-ID transactions coexist and match in order.
  2. Match the protocol's ID and ordering semantics. If same-ID responses must come back in order, the per-ID FIFO enforces it; if not, adjust accordingly.
  3. Test with ID reuse at high throughput. Exercise many outstanding transactions with reused IDs to surface collisions.
  4. Treat the ID space as finite. Never assume IDs are unique over the run; design for reuse from the start.

The one-sentence lesson: out-of-order checking by ID must use a per-ID queue (a FIFO per key), not a single entry per ID — because ID spaces are finite and IDs are reused, so a reused ID outstanding alongside its prior collides in a single-slot map (overwriting and losing it, causing drops and mismatches); a per-ID queue holds multiple outstanding same-ID transactions and matches them in order, with the tell being failures that correlate with ID reuse at high throughput.

Common Mistakes

  • A single entry per ID. When IDs are reused, a single-slot map collides — a reused ID overwrites the outstanding one; use a per-ID queue.
  • Assuming IDs are unique. ID spaces are finite and reused over a run; design for reuse, not uniqueness.
  • Forgetting the missing-key check. An output whose ID isn't in the map is an extra or wrong-ID output; flag it.
  • Forgetting the end-of-test leftover check. Expected left across all IDs at end are drops; sum and report them.
  • Ignoring intra-ID ordering. If the protocol requires same-ID responses in order, the per-ID FIFO enforces it; a plain map or set doesn't.
  • Using out-of-order checking on an in-order DUT. It works but gives up catching illegal reordering as a bug; use in-order checking when the DUT is in-order.

Senior Design Review Notes

Interview Insights

Out-of-order checking pairs each observed output with its expected by a matching key, an ID, rather than by position, so it tolerates a DUT that legally completes transactions in a different order than they arrived. It differs from in-order checking in the pairing mechanism. In-order checking uses a FIFO: the predictor pushes expected in input order, and the scoreboard pops the front for each output, pairing by position — correct only if the DUT produces outputs in input order. Out-of-order checking uses an associative array keyed by ID: the predictor inserts each expected keyed by the transaction's ID, and on each output the scoreboard looks up the expected with the same ID, compares, and removes it. Because the pairing is by key lookup, not position, outputs arriving in any order are each matched to their correct expected — legal reordering causes no mismatch. You need this for DUTs that legally reorder: caches, where a hit returns before an earlier miss; multi-channel blocks; variable-latency pipelines; AXI fabrics, where responses can return in any order across IDs. For these, positional pairing would flood false mismatches on legal reordering, because position is exactly what reordering changes, while the key is invariant under reordering. The mental model is a coat check with numbered tickets versus stacked tickets: in-order matches the next ticket on the stack, out-of-order looks up the matching number regardless of return order. The cases out-of-order checking handles are a missing key, an output whose ID isn't in the map, which is an extra or wrong; leftover entries at end of test, which are drops; and critically ID reuse, which forces a per-ID queue rather than a single entry. So out-of-order checking is in-order checking generalized: replace the single positional FIFO with a map of FIFOs, keyed by ID, which tolerates cross-key reordering while each per-key FIFO preserves intra-key order.

Because ID spaces are finite, so IDs get reused, and if a reused ID is outstanding at the same time as its prior transaction, a single entry per ID collides — the new expected overwrites the old, losing it. A naive out-of-order scoreboard keys by ID with one slot per ID: expected of id equals e. This assumes at most one outstanding transaction per ID at a time, which holds when IDs are unique or not reused while outstanding — true at low throughput. But IDs are a limited resource; an ID space is finite, like AXI's bounded number of IDs. So at high throughput, the same ID gets issued again before its prior transaction completes — the prior's response is still in flight — so two transactions with the same ID are outstanding at once. With a single slot, storing the new expected overwrites the old one in the map. The old expected is lost, so when its output eventually arrives, there's nothing to match — a drop. And the next same-ID output gets paired with the new expected instead of the old, a mismatch. Both failures stem from the collision. The tell is that failures correlate with throughput and ID reuse: unique IDs at low throughput work fine, reused IDs at high throughput break, because the collision only happens when two same-ID transactions overlap. The fix is a per-ID queue: store a FIFO of expected per ID. Then two outstanding id-5 transactions both live in queue of 5 as a list, and same-ID outputs pop the oldest first, matching them in order. This handles reuse — multiple outstanding same-ID coexist — and it also enforces intra-ID ordering, which protocols like AXI require: same-ID responses must come back in order. So the per-ID queue is necessary because IDs repeat, and a single slot can't hold the multiple outstanding same-ID transactions that reuse creates. The principle is: when keys repeat, key to a stack, not a slot.

Because the key is invariant under reordering, while position is exactly what reordering changes. Positional pairing, as in in-order FIFO checking, pairs the Nth output with the Nth expected — it relies on the output's position in the stream corresponding to its input's position. Reordering breaks this directly: when the DUT produces outputs in a different order, the Nth output is no longer the Nth input's result, so pairing by position matches each output against the wrong expected, producing false mismatches. The position is the very thing that changed, so a position-based pairing fails. Keyed pairing, by contrast, matches each output to its expected by an ID that travels with the transaction. The ID identifies which input an output belongs to, and it doesn't change when the DUT reorders the outputs. So no matter what order the outputs arrive in, looking up an output's ID retrieves its correct expected. Output id-2 matches expected id-2 whether it arrives first, last, or in the middle. The key is invariant under reordering, so a key-based pairing survives it. This is why keyed pairing is the only correct approach for an out-of-order DUT: when reordering is legal, you must pair by something that reordering can't disturb, and that's the key, not the position. The trade-off is that out-of-order checking tolerates reordering by design, so it doesn't catch reordering as a bug — which is correct, because reordering is legal for this DUT. If the protocol requires intra-ID ordering, you re-introduce an in-order check within each ID via the per-ID queue, so same-ID responses must still come in order while different-ID responses can be reordered. So the rule is to match by the invariant, the ID, not the variant, the position, which is what makes keyed pairing tolerate the legal reordering that positional pairing would false-flag.

Out-of-order checking must handle missing keys, leftover drops, ID reuse, and intra-ID ordering. A missing key is an output whose ID isn't in the map, or whose per-ID queue is empty. This means there's no expected for this output — an extra, a duplicate, or a wrong-ID output. You handle it by checking, on each output, whether the ID's queue exists and is non-empty before popping; if not, you report a missing-key error. Leftover drops are expected that were inserted but never matched by an output. At end of test, you sum the remaining expected across all IDs; any leftover means those outputs never arrived, which you report as drops. This mirrors in-order checking's end-of-test non-empty-queue check, but across all keys. ID reuse is the critical case: because ID spaces are finite, the same ID gets reused, and two same-ID transactions can be outstanding at once. You handle this with a per-ID queue rather than a single entry per ID, so multiple outstanding same-ID transactions coexist, each matched in order. A single slot would collide, overwriting and losing the older one. Intra-ID ordering is the related requirement: many protocols, like AXI, require that same-ID responses come back in order, even though different-ID responses can be reordered. The per-ID FIFO handles this naturally — by popping the oldest expected for an ID, it matches same-ID outputs in their insertion order, which enforces intra-ID ordering. So the per-ID queue serves double duty: handling reuse and enforcing intra-ID order. The overall structure is an associative array of queues: the array keyed by ID tolerates cross-ID reordering, and each queue preserves intra-ID order and holds multiple outstanding same-ID transactions. With the missing-key check on each output and the leftover check at end, this covers extras, drops, reuse, and ordering — the complete set of cases for out-of-order checking.

You use in-order checking when the DUT produces outputs in input order, and out-of-order checking when it may legally reorder, and out-of-order checking is essentially in-order checking generalized. The choice comes from the DUT's ordering semantics, a spec property. An in-order DUT guarantees the Nth output is the Nth input's result, so positional FIFO pairing is correct, simple, and complete. An out-of-order DUT may complete transactions in a different order, so you must pair by key. Structurally, out-of-order checking generalizes in-order checking. In-order checking is a single FIFO: push expected in order, pop the front per output. Out-of-order checking is a map of FIFOs, keyed by ID: insert expected into the per-ID queue, look up by the output's ID, pop the oldest for that ID. If you think of in-order checking as the special case where there's effectively one key — all transactions share a single ordering — then out-of-order checking with one ID degenerates to in-order checking. And within each ID, out-of-order checking does in-order checking via the per-ID FIFO. So the relationship is that out-of-order checking is in-order within each key and out-of-order across keys, which captures both: a single-key out-of-order checker behaves like an in-order checker, and a multi-key one tolerates cross-key reordering while preserving intra-key order. The completeness checks carry over too: in-order's end-of-test non-empty-queue check becomes out-of-order's leftover-across-all-keys check, and in-order's empty-queue-on-arrival extra check becomes out-of-order's missing-key check. The practical guidance is to use in-order checking for in-order DUTs because it's simpler and catches illegal reordering as a bug, and out-of-order checking for reordering DUTs because positional pairing would false-mismatch. Choosing the wrong one is a problem: in-order on a reordering DUT floods false mismatches, and out-of-order on an in-order DUT gives up reordering detection. So match the checker to the DUT's ordering semantics, with out-of-order being the more general structure.

Exercises

  1. Trace the map. For inputs with IDs 1, 2, 3 and outputs arriving 2, 1, 3, trace the associative-array lookups and the matches.
  2. Show the collision. Demonstrate how a single entry per ID loses a transaction when ID 5 is reused while outstanding, and fix it with a per-ID queue.
  3. Detect the missing key. Explain what happens when an output arrives with an ID not in the map, and how it's reported.
  4. Relate the two. Explain how out-of-order checking generalizes in-order checking, and what a single-ID out-of-order checker reduces to.

Summary

  • Out-of-order checking pairs each observed output with its expected by a matching key (an ID), not by position — via an associative array (insert by key, look up by the output's key, compare, remove) — so it tolerates legal reordering.
  • Keyed pairing is necessary for an out-of-order DUT: position is what reordering changes, but the key (ID) is invariant under reordering, so an output's ID retrieves its correct expected regardless of order (where positional pairing would false-mismatch).
  • It handles the cases: a missing key (output with no matching expected → extra/wrong), a leftover at end of test (expected never matched → drops), and — criticallyID reuse.
  • ID reuse forces a per-ID queue (a FIFO per key), not a single entry per ID: because ID spaces are finite and IDs are reused, a reused ID outstanding alongside its prior collides in a single-slot map (overwriting and losing it); the per-ID queue holds multiple outstanding same-ID transactions, matched in order (also enforcing intra-ID ordering).
  • The durable rule of thumb: for an out-of-order DUT, pair by key (an ID) via an associative array — look up each output's expected by its ID, tolerating legal reordering — and use a per-ID queue (FIFO per key), never a single slot, because finite ID spaces mean reused IDs can be outstanding at once and would collide (drops and mismatches that correlate with throughput); the per-ID queue holds multiple outstanding same-ID transactions in order, making out-of-order checking in-order within each key and out-of-order across keys.

Next — Scoreboard Debugging: the module closes with the methodology — how to debug a scoreboard failure: telling a real DUT bug from a checker bug (predictor, reference model, pairing, or comparison), reading a mismatch report and the surrounding context, and a systematic process from a reported mismatch to its true cause.