Part 2 · Phases & Lifecycle · Intermediate

run_phase Parallel Execution: Every Component Forks Together

How UVM schedules run_phase across the component tree, what 'parallel' means in simulation, and why super.run_phase matters.

All run_phases start at the same simulation time

When UVM enters run_phase, it traverses the component tree and invokes every component's run_phase task. Each invocation is a separate thread of execution — they all begin at time 0 (or whatever time start_of_simulation left off) and run concurrently.

This is not sequential. The driver does not wait for the monitor to finish, and the test does not wait for the env to 'start'. Everyone begins together, coordinated only by objections (when to stop) and by the events/signals they share.

diagram
[PHASE][RUN] parallel fork picture

time 0ns
  test.run_phase          ─────────────────────────────► (raises, runs seq, drops)
  env.run_phase           ─────────────────────────────► (super only)
  agent.run_phase         ─────────────────────────────► (super only)
  driver.run_phase        ─────────────────────────────► (forever get/drive loop)
  monitor.run_phase       ─────────────────────────────► (forever sample loop)
  scoreboard.run_phase    ─────────────────────────────► (forever compare loop)

all threads alive simultaneously until objections drain
  • Every component with a run_phase override gets its own concurrent thread.

  • Parent components typically call super.run_phase(phase) to spawn children.

  • Coordination during run is via TLM, events, and objections — not phase ordering.


The super.run_phase pattern

In a parent component (env, agent), run_phase almost always delegates to children via super:

systemverilog
class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  axi_agent  axi;
  apb_agent  apb;
  scoreboard sb;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    axi = axi_agent::type_id::create("axi", this);
    apb = apb_agent::type_id::create("apb", this);
    sb  = scoreboard::type_id::create("sb", this);
  endfunction

  // Parent run_phase: spawn all children's run_phases
  task run_phase(uvm_phase phase);
    super.run_phase(phase);   // forks driver, monitor, scoreboard run_phases
  endtask
endclass

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  my_env env;

  task run_phase(uvm_phase phase);
    // super.run_phase spawns env (which spawns agents, scoreboard, etc.)
    fork
      super.run_phase(phase);
    join_none

    // Test owns the top-level objection and stimulus
    phase.raise_objection(this, "main test");
    my_vseq::type_id::create("vseq").start(env.v_sqr);
    phase.drop_objection(this, "main test done");
  endtask
endclass
diagram
[UVM][PHASE] hierarchy spawn chain

  test.run_phase
    └─ super  env.run_phase
                 └─ super  axi_agent.run_phase
                              ├─ driver.run_phase (forever)
                              └─ monitor.run_phase (forever)
                 └─ super  apb_agent.run_phase
                 └─ super  scoreboard.run_phase
  • super.run_phase(phase) is how parents launch child run_phases.

  • The test usually raises the top-level objection separately from super.

  • Components that only need passive observation still get a run_phase thread.


What runs where: active vs passive components

Not every run_phase does the same work. Understanding the division of labor prevents duplicate stimulus and race conditions:

diagram
[RUN] component roles during run_phase

  TEST          raises objection, starts virtual/main sequences
  ENV           super only (delegates to children)
  AGENT         super only (delegates driver + monitor)
  DRIVER        forever loop: get_next_item  drive  item_done
  MONITOR       forever loop: sample interface  ap.write(tr)
  SCOREBOARD    forever loop: dequeue expected/actual  compare
  COVERAGE      subscriber write() or dedicated run_phase sampler
systemverilog
// Driver: blocking on sequencer, not on test
task run_phase(uvm_phase phase);
  req_t req;
  forever begin
    seq_item_port.get_next_item(req);
    drive_transaction(req);
    seq_item_port.item_done();
  end
endtask

// Monitor: independent sampling thread
task run_phase(uvm_phase phase);
  forever begin
    @(posedge vif.clk);
    if (vif.valid && vif.ready) begin
      tr = sample_from_bus();
      ap.write(tr);
    end
  end
endtask
  • Drivers block on the sequencer; monitors block on the clock/interface.

  • Neither driver nor monitor should raise the top-level test objection.

  • The test (or a sequence via starting_phase) owns simulation duration.


Parallel pitfalls and debug anchors

Because everything runs in parallel, ordering bugs show up as race conditions or missed transactions — not as phase-order violations. Use timeline anchors in logs to correlate activity.

diagram
[PHASE][RUN] debug timeline anchors

  log at:  test objection raised
  log at:  first sequence item granted
  log at:  first monitor sample
  log at:  first scoreboard compare
  log at:  test objection dropped

  if 'first compare' never appears  monitor wiring or scoreboard thread issue
  if 'objection dropped' at 0ns    nobody raised or instant exit

Key takeaways

  • run_phase forks every component's task concurrently at the same simulation time.

  • Parents call super.run_phase to spawn children; tests own top-level objections.

  • Drivers, monitors, and checkers each have distinct run_phase responsibilities.

  • Parallel execution means coordination is via TLM and objections, not phase order.

Common pitfalls

  • Blocking in test.run_phase before super.run_phase — children never start.

  • Expecting driver to finish before monitor — both run forever until phase ends.

  • Raising objections in driver/monitor instead of test/sequence — unclear ownership.

  • Omitting super.run_phase in env/agent — children never enter run_phase.