Part 4 · TLM & Analysis · Intermediate
Producer-Consumer Patterns: Decoupled TLM Pipelines
Architectural patterns for decoupling producer and consumer timing with buffered queues, staged workers, and explicit throughput/back-pressure policies.
Why decoupling matters
Real verification components run at different effective rates: monitors may observe bursts, reference models may be compute-heavy, and scoreboards may compare with variable latency. Direct synchronous calls can over-couple these rates.
Producer-consumer patterns introduce buffers and stage boundaries so each role progresses at its own cadence while preserving transaction order and observability.
Legend: [UVM] [TLM] [CHECK]
[TLM] decoupled pipeline
producer ----put----> stage_fifo ----get----> transformer ----put----> check_fifo ----get----> checker
fast bursts elastic buffer variable latency elastic buffer steady compare
each boundary can expose:
depth metrics, timeout policy, drop policy, back-pressure behaviorDecoupling prevents one slow stage from freezing all upstream activity.
Buffers provide elasticity for burst absorption.
Per-stage metrics make throughput bottlenecks explicit.
Pattern A: explicit put/get stages
class stage_fifo extends uvm_component;
`uvm_component_utils(stage_fifo)
uvm_blocking_put_imp #(pkt, stage_fifo) put_imp;
uvm_blocking_get_imp #(pkt, stage_fifo) get_imp;
pkt q[$];
int unsigned max_depth = 64;
function new(string name, uvm_component parent);
super.new(name, parent);
put_imp = new("put_imp", this);
get_imp = new("get_imp", this);
endfunction
task put(pkt p);
wait (q.size() < max_depth);
q.push_back(p);
endtask
task get(output pkt p);
wait (q.size() > 0);
p = q.pop_front();
endtask
endclass
class producer_stage extends uvm_component;
`uvm_component_utils(producer_stage)
uvm_blocking_put_port #(pkt) out_port;
function new(string name, uvm_component parent);
super.new(name, parent);
out_port = new("out_port", this);
endfunction
task run_phase(uvm_phase phase);
pkt p;
forever begin
p = pkt::type_id::create("p");
assert(p.randomize());
out_port.put(p);
#1ns;
end
endtask
endclass
class consumer_stage extends uvm_component;
`uvm_component_utils(consumer_stage)
uvm_blocking_get_port #(pkt) in_port;
function new(string name, uvm_component parent);
super.new(name, parent);
in_port = new("in_port", this);
endfunction
task run_phase(uvm_phase phase);
pkt p;
forever begin
in_port.get(p);
heavy_process(p);
end
endtask
task heavy_process(pkt p);
#20ns;
endtask
endclass[UVM] wiring
prod.out_port.connect(buffer.put_imp);
cons.in_port.connect(buffer.get_imp);
Result:
producer throughput is limited by buffer depth + consumer service rate,
not by direct call latency per item.Buffer depth is an architectural knob, not a random constant.
Blocking boundaries preserve order naturally.
Max-depth wait behavior models finite-resource back-pressure.
Pattern B: monitor to scoreboard via analysis FIFO
For passive observation, monitors should still publish via analysis write(), but scoreboards can decouple processing using an intermediate analysis FIFO and pull semantics.
class packet_monitor extends uvm_monitor;
`uvm_component_utils(packet_monitor)
uvm_analysis_port #(pkt) ap;
function new(string name, uvm_component parent);
super.new(name, parent);
ap = new("ap", this);
endfunction
task run_phase(uvm_phase phase);
pkt p;
forever begin
p = sample_packet();
ap.write(p);
end
endtask
endclass
class packet_scoreboard extends uvm_scoreboard;
`uvm_component_utils(packet_scoreboard)
uvm_tlm_analysis_fifo #(pkt) act_fifo;
function new(string name, uvm_component parent);
super.new(name, parent);
act_fifo = new("act_fifo", this);
endfunction
task run_phase(uvm_phase phase);
pkt p;
forever begin
act_fifo.get(p); // pull at checker pace
compare_with_model(p);
end
endtask
endclass
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
mon.ap.connect(scb.act_fifo.analysis_export);
endfunction[CHECK] benefit profile
monitor:
quick write(), minimal perturbation of sampling thread
scoreboard:
independent pull loop
can spend variable compute per item
can expose queue depth telemetryUse analysis path for fan-out semantics.
Use FIFO pull path for checker pacing and isolation.
Measure depth growth to detect stalled or overloaded checkers.
Throughput engineering and operational safeguards
[TLM] pipeline health metrics
per stage monitor:
ingress_rate items/us
egress_rate items/us
queue_depth instantaneous + max
wait_time average + tail percentiles
drop_count if policy allows drops
alerts:
sustained depth growth -> downstream bottleneck
zero egress with non-zero ingress -> stalled consumer
frequent full condition -> depth too small or service too slowtask monitor_depth(stage_fifo f);
int unsigned max_seen = 0;
forever begin
if (f.q.size() > max_seen)
max_seen = f.q.size();
if ((f.q.size() > (f.max_depth * 9) / 10))
`uvm_warning("PIPE", $sformatf("fifo near full depth=%0d", f.q.size()))
#100ns;
end
endtask[UVM] policy choices
overflow policy:
A) block producer until space available
B) drop newest with counter
C) drop oldest with counter
verification default:
prefer A (no silent data loss) unless scenario explicitly models lossKey takeaways
Decoupled producer-consumer pipelines improve robustness under bursty or uneven workloads.
Choose stage boundaries intentionally and monitor them with depth/rate metrics.
Default to no-loss blocking policies unless loss is part of modeled behavior.
Analysis FIFO patterns combine passive observation with pull-based checker control.
Common pitfalls
Adding buffers without telemetry, hiding throughput regressions.
Allowing unbounded queue growth with no watchdogs.
Choosing drop policies without explicit accounting and test intent.
Over-coupling stages again by doing heavy work inside monitor write().