UVM
run_phase
The one time-consuming phase where all behaviour happens — every component's run_phase runs concurrently in parallel (no ordering), and objections control end-of-test.
UVM Phasing · Module 5 · Page 5.6
The Engineering Problem
Every phase so far has been zero-time setup. run_phase is where the simulation actually happens — it is the single phase that consumes simulation time, the one in which stimulus is driven, the DUT operates, monitors observe, scoreboards check, and coverage samples. It is the phase the whole environment was built and wired to support.
But run_phase has a property that sets it apart from every phase before it, and that is the source of a whole class of subtle bugs: it runs concurrently across all components. The function phases (build, connect, …) are sequential traversals — the framework runs each component's method one at a time, top-down or bottom-up. run_phase is different: the framework forks every component's run_phase to run in parallel, all at once, for the whole duration. The driver, the monitor, the scoreboard — their run_phase tasks all execute simultaneously, which is exactly what you want (they must operate together), but it means you cannot assume any ordering between them. Code that relies on one component's run_phase running "before" another's races, and fails intermittently. This chapter is about the phase where time and behaviour live, its concurrency, and the objection mechanism that controls when it ends.
What is
run_phase— the one time-consuming phase — why does every component'srun_phaserun concurrently rather than in sequence, what bugs does that concurrency cause, and how do objections control when it ends?
Motivation — why concurrency, and why objections
run_phase being concurrent and objection-bounded is exactly what behaviour requires:
- Behaviour is inherently concurrent. A driver driving and a monitor observing and a scoreboard checking all happen at the same time, over the same cycles — that's what a running testbench is. So
run_phaseforks every component's run task to execute in parallel; if they ran one-at-a-time (like the function phases), the testbench couldn't function. Concurrency isn't a complication; it's the point. - No ordering means you must synchronise explicitly. Because all
run_phasetasks start together with no guaranteed order, you can't assume one component has done something before another runs. Shared state two components both touch must be set up in an ordered phase (build/connect) or synchronised with explicit events — not left to the accident of run-phase scheduling. This is the source of the most common run-phase bug. - The run must have a controlled end. Behaviour runs until the work is done, not for a fixed time. Objections provide that: the phase stays alive while any component objects to ending it, and ends when all objections drop — so the run lasts exactly as long as there's stimulus to drive (Module 2.9). This is why end-of-test logic lives in
run_phase. - It's the only phase that consumes time. Everything time-related — driving over cycles, waiting for events, the DUT's operation — lives here, because the function phases can't consume time.
run_phaseis where the simulation's duration entirely lives.
The motivation, in one line: behaviour is concurrent and bounded, so run_phase forks all components' run tasks to run in parallel (with no ordering you can rely on) and uses objections to control when the whole thing ends — the phase where time, behaviour, and the race-condition pitfalls all live.
Mental Model
Hold run_phase as the orchestra all playing at once, until the piece ends:
run_phaseis the moment the conductor's baton drops and the whole orchestra plays — simultaneously. The setup phases were the musicians filing in and tuning, one section at a time, in order (the sequential function phases). But the performance itself is every musician playing at once, in parallel, for the duration of the piece — the strings, brass, and percussion all sounding together, because music is concurrent. You cannot assume the violins "go before" the cellos; they play together, and if you wrote your part assuming another section had already finished theirs, you'd be wrong. The piece lasts as long as there's music to play — it ends when the last held note is released (the last objection dropped), not at a fixed clock time. Then the baton lowers and the hall falls silent (the run ends, cleanup begins).
So in a run_phase, you write one component's part, knowing every other component's part plays at the same time. Don't assume ordering between parts — synchronise on shared cues (events) or set the stage in the ordered setup phases (build/connect). And hold your objection while you have music to play, releasing it when you're done, so the piece lasts exactly as long as the work.
Visual Explanation — run_phase, the one time-consuming phase
run_phase is the single phase where time advances and behaviour happens — the home of stimulus, checking, and coverage, all running together over simulation time.
This is the phase the whole schedule exists to reach. Everything before it (build, connect, end_of_elaboration, start_of_simulation) was zero-time preparation; run_phase is where the prepared environment runs. All five Module-1 concerns happen here, simultaneously: stimulus (the sequence/sequencer/driver path), the DUT's operation, observation (monitors), checking (scoreboards/assertions), and measurement (coverage) — over the same cycles, because verification is these things happening together. And the phase is bounded by objections, not by a fixed time: it lasts exactly as long as some component objects to its ending, which is how the run lasts precisely as long as there's stimulus to drive (the end-of-test mechanism from Module 2.9). run_phase is, simply, where the simulation is.
RTL / Simulation Perspective — concurrent run_phase tasks
Each component's run_phase is a task, and the framework runs them all concurrently. The driver's loop, the monitor's loop, and the scoreboard's logic all execute in parallel over the same time — there is no "first" among them.
task my_driver::run_phase(uvm_phase phase);
forever begin
seq_item_port.get_next_item(req); // pull a transaction
drive(req); // drive it over cycles (consumes time)
seq_item_port.item_done();
end
endtask
task my_monitor::run_phase(uvm_phase phase);
forever begin
collect(t); // observe the pins (consumes time)
ap.write(t); // broadcast the observed transaction
end
endtask
// The framework FORKS all components' run_phase tasks to run IN PARALLEL when the run begins.
// driver.run_phase, monitor.run_phase, scoreboard.run_phase all execute CONCURRENTLY —
// there is NO guaranteed order between them. They operate over the same simulation time.The crucial fact is in the comment: when run_phase begins, the framework forks every component's run_phase task so they run simultaneously, for the whole duration. The driver's forever loop pulls and drives transactions; the monitor's forever loop observes and broadcasts — and these run at the same time, which is correct, because the monitor must be watching the pins while the driver drives them. This is fundamentally unlike the function phases: build_phase ran each component's method one at a time in a top-down sweep, but run_phase launches them all in parallel. The consequence you must internalise: there is no ordering between components' run_phase tasks. The scoreboard's run_phase does not run "after" the monitor's; they start together. Any code that assumes otherwise — that one component's run has "already" set something up — is a race (the DebugLab). Each run_phase is written as one independent, concurrent task.
Verification Perspective — sequential function phases vs concurrent run
The defining contrast in UVM phasing is that the function phases are sequential (one component at a time, in traversal order) while run_phase is concurrent (all components at once). This difference is why run-phase bugs are about races, not ordering.
This contrast governs how you write each kind of phase. In a function phase, the framework visits components one at a time in a defined direction (top-down for build, bottom-up for connect), so ordering is guaranteed — a parent's build_phase definitely runs before its child's, which is exactly why configure-from-above works (Module 5.2). In run_phase, the framework forks all the tasks to run in parallel, so there is no ordering — every component's run task starts together and runs concurrently. The practical rule that follows: anything requiring a definite order between components — setting up shared state, establishing a handle one component needs from another — must be done in the ordered phases (build/connect), not in run_phase, where it would race. And anything two concurrent run tasks must coordinate on during the run must use explicit synchronisation (events, mailboxes, TLM handshakes), not the accident of scheduling. Sequential setup, concurrent behaviour — and the bugs come from treating the concurrent phase as if it were ordered.
Runtime / Execution Flow — objections bound the concurrent run
All those concurrent run_phase tasks run until the phase ends, and what ends it is objections: the phase stays alive while any objection is raised and ends when all are dropped. This is how a concurrent, open-ended set of tasks gets a controlled, deterministic termination.
Objections are the bridge between concurrent, open-ended behaviour and a deterministic end. The component run tasks are typically forever loops — a driver pulls transactions forever, a monitor observes forever — so they have no natural end on their own. Objections impose one: while any component holds a raised objection (the test, while its sequence is running), the phase stays alive and all the concurrent tasks keep going; when the last objection drops (the sequence finishes, the test drops its objection), run_phase ends, the framework stops all the run tasks, and cleanup begins. This is the end-of-test mechanism (Module 2.9) seen from the phase's perspective: it's specifically the run phase that objections gate, because it's the only phase with a duration and the only one whose tasks would otherwise run forever. Raise to keep the concurrent run alive; drop to bring it to a coordinated close.
Waveform Perspective — concurrent activity, bounded by an objection
On the waveform, run_phase is the wide span where time advances, multiple components are active concurrently, and an objection is held throughout — dropping at the end to terminate the phase.
run_phase — concurrent component activity over time, bounded by an objection
10 cyclesThe trace shows the phase's two signatures together. The concurrency is in valid/data (the driver driving) and mon_obs (the monitor observing) being active over the same cycles — both components' run_phase tasks executing simultaneously, which is the normal state of a running testbench (the monitor watches while the driver drives). The objection is held high throughout the driven traffic and dropped when it's done, bounding the phase: while it's high, the concurrent tasks run; when it drops at cycle 6, run_phase ends and the run tasks stop. This is run_phase in one picture — the wide time-consuming span (contrast the zero-time setup phases) where multiple components act in parallel, with an objection marking exactly how long the whole concurrent activity lasts.
DebugLab — the intermittent null from assuming run_phase order
A scoreboard that sometimes saw uninitialised state — it assumed the monitor's run_phase ran first
A scoreboard occasionally — not always — failed at the very start of the run with a null or uninitialised value that a monitor's run_phase was responsible for setting up. The same test passed on some seeds/runs and failed on others, with no change to the code. The failure was intermittent, always near time zero of the run, and always involved state the monitor's run_phase initialised.
The scoreboard's run_phase assumed the monitor's run_phase had already run and initialised shared state — but run_phase tasks run concurrently, so there is no such ordering, and sometimes the scoreboard's task ran first:
monitor.run_phase: at startup, initialises shared_state = ... (concurrent task)
scoreboard.run_phase: at startup, reads shared_state (concurrent task)
run_phase scheduling: ALL run tasks forked in parallel — NO guaranteed order
result: sometimes scoreboard reads shared_state BEFORE monitor sets it → null
(intermittent: depends on the simulator's scheduling of the forked tasks)
fix: initialise shared state in an ORDERED phase (build/connect), not run_phase;
or synchronise: scoreboard waits on an event the monitor triggersBecause the framework forks all run_phase tasks to run in parallel, the relative startup order of the monitor's and scoreboard's tasks is not defined — it can vary by simulator or seed. The code assumed a sequence ("monitor sets up, then scoreboard reads") that the concurrent phase never guarantees, so it raced: sometimes the scoreboard won the race and read uninitialised state. The non-determinism is the signature of relying on run-phase ordering.
The tell is an intermittent failure near the start of the run involving state another component's run_phase sets up — a run-phase race. Diagnose ordering assumptions:
- Look for cross-component dependencies at run startup. If one component's
run_phasereads state another'srun_phaseinitialises, and there's no synchronisation, it's a race — they run concurrently with no order. - Note the intermittency. A bug that comes and goes by seed or simulator, near time zero, is the signature of relying on the scheduling order of forked run tasks. Deterministic ordering bugs are usually phase-misuse; intermittent ones are usually run-phase races.
- Check where shared state is initialised. Shared state two components depend on should be set in an ordered phase (build/connect), not in a
run_phase, where initialisation order isn't guaranteed.
Treat run_phase as concurrent with no ordering — synchronise explicitly:
- Initialise shared state in ordered phases. Set up anything two components depend on in
build_phase/connect_phase(which run in a defined order), not inrun_phase, so it's ready before any run task reads it. - Synchronise concurrent run tasks explicitly. If one run task must wait for another, use an explicit mechanism — a
uvm_event, a TLM handshake, a flag with a wait — never the assumption that onerun_phase"runs first." - Never assume run-phase ordering between components. All
run_phasetasks start together; design each as an independent concurrent task that coordinates with others only through explicit synchronisation.
The one-sentence lesson: every component's run_phase runs concurrently with no guaranteed order, so code that assumes one component's run task ran "before" another's races and fails intermittently — initialise shared state in the ordered build/connect phases, or synchronise concurrent run tasks explicitly.
Common Mistakes
- Assuming an order between components'
run_phasetasks. They run concurrently, forked in parallel, with no guaranteed order — assuming one runs "first" is a race that fails intermittently. Initialise shared state in build/connect (ordered) or synchronise explicitly. - No objection in
run_phase. Without a raised objection, the phase ends at time zero (Module 2.9) — a hollow pass. Bracket stimulus withraise_objection/drop_objection. - Dropping the objection too early. Dropping before the work is done ends the run mid-stimulus. The objection's lifetime must match the work.
- Putting time-consuming work elsewhere. All time-consuming behaviour belongs in
run_phase(or run-time sub-phases); the function phases can't consume time. Don't try to wait or drive in build/connect. - A
foreverloop with no way to stop. Run tasks are oftenforeverloops; they're stopped when the phase ends (objections drop). Don't add ad-hoc termination that fights the objection mechanism. - Sharing state between concurrent run tasks without synchronisation. Two run tasks touching the same variable without a
uvm_event, mailbox, or proper handshake race. Synchronise concurrent access explicitly.
Senior Design Review Notes
Interview Insights
run_phase is the single phase that consumes simulation time — the one where all behaviour happens: stimulus is driven, the DUT operates, monitors observe, scoreboards check, and coverage samples, over many clock cycles. Two things make it different from every other phase. First, it's a task (the function phases are zero-time functions), so it's the only phase that can consume time, wait, and drive over cycles — all time-related behaviour lives here. Second, and most importantly, it runs concurrently: the framework forks every component's run_phase task to run in parallel, all at once, for the whole duration. The function phases are sequential traversals — the framework runs each component's method one at a time in order (top-down for build, bottom-up for connect) — but run_phase launches all the tasks simultaneously, which is necessary because behaviour is concurrent (the monitor must watch while the driver drives). The consequence is that there's no guaranteed order between components' run tasks, so code that assumes one runs before another races. Its duration is controlled by objections: the phase stays alive while any objection is raised and ends when all are dropped.
Exercises
- Sequential or concurrent? State whether each runs sequentially (ordered) or concurrently (no order):
build_phase,connect_phase,run_phase. For the concurrent one, give one consequence for how you write it. - Diagnose the intermittent null. A scoreboard sometimes reads null state at run startup that a monitor's
run_phasesets. Name the root cause and two fixes (one using an ordered phase, one using synchronisation). - Bound the run. Write the
run_phaseof a test that runs a sequence, with the objection calls in the right places, and explain what happens if each is omitted. - Place the work. For each, name the phase: (a) initialise a shared lookup both a driver and monitor use; (b) drive stimulus over cycles; (c) coordinate two run tasks so one waits for the other; (d) keep the run alive while a sequence runs.
Summary
run_phaseis the single time-consuming phase (atask) where all behaviour happens — stimulus driven, DUT operating, monitors observing, scoreboards checking, coverage sampling — over simulation time. It's the phase the whole schedule prepares for.- It runs concurrently: the framework forks every component's
run_phasetask to run in parallel for the duration — unlike the sequential function phases. There is no guaranteed order between components' run tasks. - That concurrency means you cannot assume run-phase ordering: code that relies on one component's run task running before another's races and fails intermittently (the signature is a non-deterministic near-time-zero failure). Initialise shared state in the ordered build/connect phases, or synchronise concurrent run tasks explicitly.
- Its duration is bounded by objections: the phase stays alive while any objection is raised and ends when all are dropped — the end-of-test mechanism, gating specifically the run phase because it's the only one with a duration and whose
forevertasks would otherwise never stop. - The durable rule of thumb:
run_phaseis the orchestra playing at once — all components' run tasks run concurrently with no ordering, so synchronise explicitly and set shared state up in the ordered phases; hold an objection while there's work and drop it when done, so the concurrent run lasts exactly as long as the stimulus.
Next — extract_phase: the run is over; time stops advancing and the cleanup phases begin. extract_phase is the first — where each component gathers its final results and state from the completed run, before they're checked and reported. The next chapter starts the wind-down.