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.
[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 drainEvery 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:
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[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_phasesuper.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:
[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// 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
endtaskDrivers 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.
[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 exitKey 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.