UVM
Request Management
How the sequencer pairs requests with responses — routing each response by its ID into the originating sequence's bounded response queue, retrieved with get_response, and the overflow that follows when responses aren't consumed.
Sequencers · Module 11 · Page 11.3
The Engineering Problem
Module 11.2 showed how the sequencer's engine grants requests from its arbitration queue. But a request is only half the story when the driver answers — a read returns data, a transfer returns an ack. Those responses have to find their way back to the exact sequence (and the exact request) that originated them, across a sequencer that may be juggling many sequences and many outstanding requests. And there's a subtler problem: responses arrive on the driver's schedule, but the sequence consumes them on its own — so the sequencer must buffer them, and a buffer is bounded. Get the bookkeeping wrong and a response goes to the wrong sequence; get the buffering wrong and responses overflow and are lost.
This chapter is the sequencer's request/response management: how each response is paired with its request and routed to the right place, and how responses are buffered until consumed. The driver stamps a response with the request's IDs (set_id_info, Module 8.2) and returns it; the sequencer routes it — by sequence_id — into the originating sequence's response queue (a per-sequence, bounded buffer); and the sequence retrieves it with get_response (optionally matching by transaction_id). The catch is the bound: if responses arrive faster than get_response drains them — or the sequence never consumes them — the response queue overflows. This chapter is that machinery: the pairing, the routing, the per-sequence response queue, and why an unconsumed response queue overflows.
How does the sequencer pair each request with its response and route it back — the IDs, the per-sequence response queue,
get_response— and why does an unconsumed (or slowly-consumed) response queue overflow?
Motivation — why responses need per-sequence buffered routing
The response side needs routing and buffering because of how producers and consumers are decoupled across the sequencer:
- Many sequences share the sequencer, so responses must be routed. A response could belong to any of several running sequences. To deliver it to the right one, it must carry the originating sequence's identity (
sequence_id) — which the driver stamps withset_id_info— and the sequencer routes by that ID. Without routing, a response could reach the wrong sequence. - A sequence may have many outstanding requests, so responses need a per-request ID. A single sequence can issue several requests before any responds. To match a response to the specific request it answers, the
transaction_iddistinguishes them, soget_responsecan fetch the response for a given request. - Producer and consumer run on different schedules, so responses are buffered. The driver returns responses when it finishes driving; the sequence consumes them when it gets around to it. These rates differ, so the sequencer buffers responses per sequence — a response queue — bridging the schedules.
- Buffers are bounded, so consumption must keep up. A response queue can't be infinite, so it has a bounded depth. If responses arrive faster than they're consumed, the queue fills and overflows — so the sequence must drain it (or a handler must process responses as they arrive).
- Not every flow needs responses, so the response path is optional. A fire-and-forget sequence neither expects nor consumes responses. If the driver returns them anyway, they pile up — so the response path must be matched: return-and-consume, or neither.
The motivation, in one line: responses must reach the exact originating sequence and request across a shared sequencer, on a different schedule than they're consumed — so the sequencer routes them by ID into per-sequence bounded buffers that the sequence drains, and the bound means consumption must keep up or the queue overflows.
Mental Model
Hold request management as a mailroom with a bounded inbox per recipient:
The sequencer is a mailroom: every response is a labeled letter, sorted into the recipient's inbox, and each inbox holds only so much — so a recipient who never picks up their mail overflows it. You send out a request (place an order). When the answer comes back, the sender of the answer (the driver) writes the original sender's address on it (
set_id_info— thesequence_id) plus a reference number for which order it answers (transaction_id). The mailroom (the sequencer) sorts the letter by address into the recipient's inbox (the originating sequence's response queue). The recipient collects their mail (get_response) — and can ask for the letter matching a specific reference number (get_responsebytransaction_id) if they have several outstanding. But each inbox is bounded — it holds only a handful of letters. If letters arrive faster than the recipient collects them, or the recipient never collects, the inbox overflows and new letters are dropped (with a "mailbox full" notice). The remedies are the familiar ones: collect your mail (callget_response), set up forwarding/auto-processing (a response handler), get a bigger box (raise the queue depth), or — if you don't want mail at all — tell the sender to stop replying.
So request management is address, sort, collect, within a bounded box: the driver addresses the response by ID, the sequencer sorts it into the originating sequence's response queue, the sequence collects it — and an uncollected box overflows, which you fix by collecting, forwarding, enlarging, or stopping the replies.
Visual Explanation — the request/response round trip with the response queue
The defining picture is the round trip: a request goes out, the driver answers, and the response is routed by ID into the originating sequence's response queue, which the sequence drains.
The figure shows the full round trip and where the response queue sits. The sequence sends a request carrying its sequence_id and transaction_id (Module 8.2). The driver drives it, builds a response, stamps it with the request's IDs (set_id_info(req) — copying the identity so the response can be routed), and returns it. The sequencer routes the response by sequence_id into the originating sequence's response queue — and this is the piece this chapter adds: a per-sequence, bounded buffer that holds responses for that sequence. The sequence then drains the queue with get_response (optionally get_response(rsp, transaction_id) to fetch the response for a specific request). The crucial structural fact is the queue in the middle: it bridges two schedules — the driver returns responses when it finishes driving, and the sequence consumes them when it calls get_response. Because these schedules differ, the queue buffers; because the buffer is bounded (the warning coloring), the sequence must keep up draining it. So the round trip is send → drive → stamp → route-into-queue → drain, and the response queue is the per-sequence holding area that makes the decoupled timing work — and that overflows if the draining lags (the DebugLab). Everything about request management is getting the response into the right queue (routing by ID) and getting it out in time (draining before overflow).
RTL / Simulation Perspective — stamping, routing, and draining
Request management surfaces in three places: the driver stamps and returns the response, the sequencer routes it (automatically, by ID), and the sequence drains it with get_response. The code shows the driver and sequence ends.
// ── DRIVER: build a response, STAMP it with the request's IDs, return it ──
task my_driver::run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req);
drive_on_pins(req);
rsp = bus_item::type_id::create("rsp");
rsp.set_id_info(req); // ← stamp sequence_id + transaction_id (so it routes back)
rsp.rdata = sampled_data;
seq_item_port.item_done(rsp); // return the response (routed into req's sequence's queue)
end
endtask
// the SEQUENCER routes rsp by sequence_id into the originating sequence's RESPONSE QUEUE
// (per-sequence, bounded — default depth ~8)
// ── SEQUENCE: DRAIN the response queue — one get_response per request ──
task my_seq::body();
repeat (N) begin
req = bus_item::type_id::create("req");
start_item(req); assert(req.randomize()); finish_item(req);
get_response(rsp); // ← drain ONE response (else the queue fills → overflow)
// get_response(rsp, req.get_transaction_id()); // match a SPECIFIC request's response
end
endtask
// ── alternatives if you can't drain in lockstep ──
// use_response_handler(1); // process responses via a callback as they arrive (no polling)
// set_response_queue_depth(64); // size the queue larger
// set_response_queue_error_report_disabled(1); // (if responses are intentionally dropped)The code shows the three roles. On the driver side: it builds a response, calls rsp.set_id_info(req) to stamp it with the request's IDs (Module 8.2 — this is what lets the sequencer route it), and returns it with item_done(rsp). The sequencer then automatically routes the response by sequence_id into the originating sequence's response queue — a per-sequence, bounded buffer (default depth ~8). On the sequence side: after each finish_item, it calls get_response(rsp) to drain one response from its queue — and crucially, this should keep pace with the requests, one get_response per request that's answered, or the queue fills (the DebugLab). get_response(rsp, transaction_id) fetches the response for a specific request (when several are outstanding). The alternatives (the comments) handle cases where lockstep draining isn't practical: use_response_handler(1) processes responses via a callback as they arrive (no polling — the queue doesn't fill); set_response_queue_depth(64) enlarges the buffer; set_response_queue_error_report_disabled(1) silences the overflow warning if responses are intentionally dropped. The shape to carry: driver stamps and returns (set_id_info + item_done(rsp)), sequencer routes by ID into the per-sequence response queue, and sequence drains (get_response) — keeping up or the bounded queue overflows.
Verification Perspective — pairing a response to its request by ID
The routing depends entirely on IDs: sequence_id gets the response to the right sequence, and transaction_id matches it to the right request. Understanding the two-level pairing is understanding how responses find home.
The pairing is two-level, and both levels come from the same set_id_info. Level one — sequence_id (which sequence): the driver's set_id_info copies the request's sequence_id onto the response, and the sequencer uses it to route the response into the originating sequence's response queue. This is what gets the response to the right sequence among the many sharing the sequencer — without it, the response is an orphan (Module 8.2's hang). Level two — transaction_id (which request): set_id_info also copies the transaction_id, which distinguishes which of a sequence's (possibly several) outstanding requests this response answers. The sequence then drains its queue: get_response(rsp) fetches the next response (fine when there's one outstanding, or order doesn't matter), while get_response(rsp, transaction_id) fetches the response matching a specific request — essential when a sequence has multiple outstanding requests and must pair each answer to its request (a pipelined sequence sending several reads, then collecting each read's data). So sequence_id selects the queue, transaction_id selects the request within it — a two-level address, both halves stamped by set_id_info. The verification value is precision: in a pipelined flow with many outstanding requests, plain get_response returns them in arrival order, which may not match the request order — so get_response by transaction_id is how you reliably pair this answer to this request. The IDs aren't just for routing to the sequence; they're for matching within it.
Runtime / Execution Flow — the response queue and overflow
At run time, the response queue fills as the driver returns responses and drains as the sequence consumes them. When the fill rate exceeds the drain rate, the bounded queue overflows — the central runtime concern of request management.
The runtime behavior is a producer/consumer balance on a bounded buffer. Fill (step 1): the driver returns responses on its schedule — each item_done(rsp) routes a response into the sequence's bounded response queue. Drain (step 2): the sequence consumes them on its schedule — each get_response removes one. Healthy (step 3): if the sequence drains as fast as responses arrive — one get_response per answered request, in pace — the bounded queue stays within depth, and everything flows. Overflow (step 4): if responses arrive faster than they're consumed — the sequence sends many requests but drains slowly, or never calls get_response at all — the queue fills to its depth and overflows: the sequencer issues a "response queue overflow" warning and excess responses are lost. The remedies map to rebalancing: drain in pace (call get_response per answered request — the simplest fix), use a response handler (use_response_handler(1) — responses are processed by a callback as they arrive, so the queue doesn't accumulate), enlarge the depth (set_response_queue_depth — more headroom if bursts are temporary), or — if responses are genuinely not needed — have the driver not return them (or disable the warning, accepting the drop). The key runtime insight is that the response queue is a bounded buffer between mismatched schedules, so request management is fundamentally a flow-control problem: the sequence must consume responses at the rate they're produced, or the buffer overflows — which is exactly the DebugLab.
Waveform Perspective — draining vs overflowing
The response queue's health is visible on a timeline: responses arriving (fill) against get_response calls (drain), with the queue occupancy rising toward overflow when draining lags.
Response queue: healthy draining vs overflow when get_response lags
12 cyclesThe waveform shows the buffer balance and its failure. In the HEALTHY region, rsp_in (a response returned by the driver — fill) is matched by get_rsp (the sequence draining — drain), so the queue occupancy q_depth stays low — responses arrive and are consumed in pace. Then the sequence stops draining (no more get_rsp pulses) while the driver keeps returning responses (rsp_in continues): now fill with no drain, so q_depth climbs — 1, 2, 3… toward the bound. When it reaches the bound (here ~6), the OVERFLOW fires (ovf pulse): the queue is full, the sequencer warns, and excess responses are lost. The visual story is a bounded buffer filling: as long as drains keep pace with fills, occupancy stays low; the moment draining lags (or stops), occupancy rises until it hits the bound and overflows. The picture makes the flow-control reality concrete — the response queue is finite, so the sequence must consume responses at the rate they're produced. The ovf pulse is the symptom the DebugLab dissects: it's the queue saying "I'm full, and I'm dropping responses," which happens precisely when the sequence sends-and-doesn't-drain. The remedy is visible too: resume the get_rsp pulses (drain in pace), and q_depth falls back — consume the responses, and the queue stays healthy.
DebugLab — the response queue that overflowed
A 'response queue overflow' flood because the sequence sent requests but never consumed responses
A sequence sent a long stream of requests, and the driver returned a response for each (item_done(rsp)). The log flooded with "response queue overflow" warnings, and downstream the responses seemed to go missing — a scoreboard that checked responses saw fewer than expected, as if the driver had dropped them. The requests all drove correctly; only the responses were warned about and lost. The sequence worked (it sent all its requests) but never seemed to use the responses.
The driver returned a response for every request, but the sequence never called get_response — so the responses accumulated in the bounded response queue until it overflowed:
driver: item_done(rsp); // returns a response for EVERY request (fill)
sequence: repeat (N) begin
start_item(req); ... finish_item(req);
// ✗ MISSING: get_response(rsp); ← never drains the response queue
end
→ responses fill the bounded response queue (default depth ~8) with NO draining
→ queue reaches depth → "response queue overflow" → excess responses DROPPED
→ the scoreboard sees fewer responses than requests (the dropped ones)
fix (pick one, by intent):
(a) DRAIN: consume one response per request → get_response(rsp); after each finish_item
(b) HANDLER: process responses as they arrive → use_response_handler(1); + response_handler()
(c) DON'T RETURN: if responses aren't needed, have the driver NOT return them (item_done; no rsp)
(d) SIZE/SILENCE: set_response_queue_depth(...) / set_response_queue_error_report_disabled(1)This is the unconsumed-response-queue overflow, the central request-management bug — and the opposite of Module 8.2's missing-response hang. There, the driver forgot set_id_info, so a response never arrived and get_response hung. Here, the driver correctly returns responses, but the sequence never consumes them: it sent its requests and never called get_response, so the per-sequence response queue — a bounded buffer (default depth ~8) — filled and overflowed, the sequencer warned, and excess responses were dropped (which is why the scoreboard saw fewer than it expected). The root issue is a producer/consumer mismatch: the driver produces responses (fill) but the sequence doesn't consume them (no drain), and the bounded queue can't hold an unbounded backlog. The fix depends on intent: if the sequence needs the responses, it must drain them — call get_response once per answered request (option a), or register a response handler so they're processed as they arrive (option b, which avoids polling); if the sequence doesn't need responses, the driver shouldn't return them (option c — item_done with no response), so they never pile up; and if dropping is acceptable, you can enlarge the queue or disable the warning (option d). The general lesson, and the request-management point: every response the driver returns must be consumed — by get_response or a handler — because the response queue is bounded, so a sequence that sends-and-doesn't-drain overflows it and loses responses. Match the response path: return-and-consume, or neither.
The tell is a flood of "response queue overflow" with responses going missing. Diagnose response-queue bugs:
- Confirm the driver returns responses. If the driver calls
item_done(rsp)(returns a response) but the sequence doesn't consume, responses accumulate — the overflow setup. - Check whether the sequence calls
get_response. A sequence that sends requests but never (or rarely) callsget_responsedoesn't drain the queue — the overflow cause. - Correlate dropped responses with the warning. A scoreboard seeing fewer responses than requests, alongside the overflow warning, confirms responses are being dropped by the full queue.
- Decide intent. Determine whether the sequence needs responses (then drain or handle them) or doesn't (then the driver shouldn't return them) — the fix follows from the intent.
Match the response path — return-and-consume, or neither:
- Drain every response the driver returns. Call
get_responseonce per answered request (in pace), so the bounded queue stays healthy. - Use a response handler for high-rate or async responses.
use_response_handler(1)+response_handler()processes responses as they arrive, so the queue doesn't accumulate while the sequence does other work. - Don't return responses the sequence won't use. For fire-and-forget flows, have the driver call
item_donewith no response — so nothing piles up. - Size or silence deliberately, not as a crutch.
set_response_queue_depth/set_response_queue_error_report_disabledare for genuine bursts or intentional drops — not a substitute for matching the response path.
The one-sentence lesson: the sequencer buffers each returned response in a bounded per-sequence response queue, so a sequence that sends requests but never consumes the responses (no get_response) overflows the queue and loses responses — match the response path by draining each response (get_response), handling them (use_response_handler), or not returning responses the sequence won't use.
Common Mistakes
- Sending requests but never calling
get_response. Returned responses fill the bounded queue and overflow; drain each (or use a handler), or don't return responses you won't use. - Returning responses a fire-and-forget sequence won't consume. They pile up and overflow; for flows that don't need responses, the driver should
item_donewith no response. - Using plain
get_responsewith multiple outstanding requests. It returns responses in arrival order, which may not match request order; useget_response(rsp, transaction_id)to pair a specific request. - Treating the response queue as unbounded. It has a bounded depth (default ~8); consumption must keep pace with production, or it overflows.
- Silencing the overflow warning instead of fixing the flow. Disabling the report hides a real producer/consumer mismatch; fix the draining unless drops are intentional.
- Forgetting
set_id_infoon the response. Without it the response isn't routed (Module 8.2's hang) — the opposite failure from overflow, but also a request-management break.
Senior Design Review Notes
Interview Insights
Through a two-level identity, both halves stamped by the driver with set_id_info. When a sequence sends a request, the item carries a sequence_id, which identifies the originating sequence, and a transaction_id, which identifies the specific request within that sequence. When the driver builds a response, it calls set_id_info on the response, copying both IDs from the request. The sequencer then uses the sequence_id to route the response into the originating sequence's response queue — a per-sequence buffer — so the response reaches the right sequence among the many that may share the sequencer. Within the sequence, get_response drains the next response from its queue, or get_response with a transaction_id argument fetches the response matching a specific outstanding request. So the pairing is two-level: sequence_id selects which sequence's queue the response goes into, and transaction_id selects which request the response answers within that sequence. This matters most in pipelined flows where a sequence has several outstanding requests at once: plain get_response returns responses in arrival order, which may not match the order the requests were issued, so to reliably pair each answer with its request you use get_response by transaction_id. The whole mechanism depends on set_id_info being called on the response — without it, the response has no identity, so the sequencer can't route it to the originating sequence and get_response hangs waiting for a response that never arrives. So the request-response pairing is the IDs carried on the request, copied to the response by the driver, used by the sequencer to route and by the sequence to match.
The response queue is a per-sequence, bounded buffer inside the sequencer that holds responses routed to a sequence until the sequence consumes them. It exists because the driver returns responses on its own schedule — when it finishes driving each request — while the sequence consumes them on its schedule, when it calls get_response, and those rates differ, so the sequencer buffers responses to bridge the gap. The queue has a bounded depth, around 8 by default. It overflows when responses arrive faster than the sequence drains them, or when the sequence never calls get_response at all: the driver keeps returning responses that fill the queue, and with no draining the occupancy climbs to the depth limit, at which point the sequencer issues a response queue overflow warning and excess responses are dropped. The classic scenario is a sequence that sends a stream of requests and never consumes the responses, so a downstream check sees fewer responses than requests because the dropped ones never made it. The remedies follow from the cause: drain the queue by calling get_response once per answered request so consumption keeps pace; or register a response handler with use_response_handler so responses are processed by a callback as they arrive, which avoids accumulation; or, if the sequence doesn't need responses at all, have the driver not return them so nothing piles up; or, for genuine temporary bursts, enlarge the queue depth. The key point is that the response queue is a bounded buffer between mismatched producer and consumer schedules, so request management is a flow-control problem — consume responses at the rate they're produced, or the buffer overflows.
Exercises
- Trace the round trip. Describe a response's path from the driver building it to the sequence consuming it, naming
set_id_info, the response queue, andget_response. - Fix the overflow. A sequence sends requests, the driver returns responses, and the log floods with overflow warnings. Give two fixes for when the sequence needs the responses and one for when it doesn't.
- Pair the pipelined responses. A sequence has three outstanding read requests. Explain how to pair each returned response to its request, and why plain
get_responsemay not suffice. - Hang vs overflow. Contrast a
get_responsehang with a response-queue overflow — the cause and the fix of each.
Summary
- Request management is how the sequencer pairs each request with its response and buffers it: the driver stamps the response with the request's IDs (
set_id_info—sequence_id,transaction_id) and returns it, the sequencer routes it bysequence_idinto the originating sequence's response queue, and the sequence drains it withget_response. - The pairing is two-level:
sequence_idselects the queue (which sequence),transaction_idselects the request within it — soget_response(rsp, transaction_id)pairs a specific answer to a specific request (essential with multiple outstanding requests). - The response queue is a per-sequence, bounded buffer (default depth ~8) bridging the driver's fill schedule and the sequence's drain schedule — so consumption must keep pace with production.
- An unconsumed (or slowly-drained) response queue overflows: a sequence that sends requests but never calls
get_responsefills the queue → "response queue overflow" → responses lost — the opposite of Module 8.2's missing-response hang. - The durable rule of thumb: match the response path — every response the driver returns must be consumed (
get_response, or ause_response_handlercallback) or not returned at all — and pair multiple outstanding requests bytransaction_id, because the per-sequence response queue is bounded, so sending-and-not-draining overflows it and loses responses.
Next — Sequence Scheduling: the engine grants and manages requests one at a time; the next chapter steps back to scheduling — how the sequencer interleaves multiple sequences over time, the order their items reach the driver, and the scheduling behavior that emerges from arbitration, locks, and relevance together.