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.

diagram
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 behavior
  • Decoupling 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

systemverilog
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
diagram
[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.

systemverilog
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
diagram
[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 telemetry
  1. Use analysis path for fan-out semantics.

  2. Use FIFO pull path for checker pacing and isolation.

  3. Measure depth growth to detect stalled or overloaded checkers.


Throughput engineering and operational safeguards

diagram
[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 slow
systemverilog
task 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
diagram
[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 loss

Key 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().