Virtual Sequence and Sequencers:
In short: A virtual sequence is a top-level sequence that does not drive a driver itself. Instead it starts other sequences on several real sequencers and controls their order and timing. A virtual sequencer is a sequencer with no driver that just holds handles to those real sequencers. Together they let one test coordinate traffic across many agents, for example an AHB agent and an AXI agent, from a single place.
When you verify one simple protocol, a single sequencer feeding a single driver is enough, and you do not need any of this. The moment your testbench has more than one agent, for example after you integrate two IP blocks into an SoC, you need a way to say “start this on the AHB side, start that on the AXI side, and do them in this order.” That coordination job is what a virtual sequence and virtual sequencer are for. This post explains both, with a simple picture, two clean implementation styles, common mistakes, and an FAQ.
When do you actually need a virtual sequence?
For block-level verification of a single simple protocol, one sequencer sends items to one driver and the test runs sequences on that one sequencer. No virtual sequence is needed. You only reach for a virtual sequence when there are two or more sequencers to coordinate, which usually happens when you reuse block-level agents inside a larger SoC environment, or when a design has more than one interface port and therefore more than one agent.
In that multi-agent case, the test needs a single place that decides which agent’s sequence starts first and how the traffic on each side lines up. The virtual sequence is that place. It acts as the controller of the stimulus for the whole design.
A simple way to picture it
Think of an orchestra. Each section (violins, drums, trumpets) has its own players who can read and play their own sheet music. Those sections are the real agents and their sequencers. Now, if every section just played whenever it liked, you would get noise, not music. The conductor does not play any instrument. The conductor only decides who comes in, when, and for how long.
The virtual sequence is the conductor’s plan for the piece, and the virtual sequencer is the conductor’s podium that can see every section. Neither makes a sound on its own, yet together they turn separate players into one coordinated performance. That is exactly what a virtual sequence does with the AHB and AXI traffic in a testbench.
Two ways to implement a virtual sequence
UVM gives you two common styles:
- Approach 1, handles in the sequence: the virtual sequence itself holds the handles to the target sequencers, and the test assigns those handles before starting the sequence.
- Approach 2, handles in a virtual sequencer: a virtual sequencer holds the target sequencer handles, and the virtual sequence runs on that virtual sequencer and reads the handles from it.
Approach 2 is the more common and cleaner style for larger environments, because the wiring lives in the environment’s connect phase rather than in the test.
Approach 1: the virtual sequence holds the handles
As shown in the diagram below, the virtual sequence contains two sequencer handles, SQR_AHB and SQR_AXI. There are two agents, the AHB agent and the AXI agent, which physically contain the two real sequencers. Those real sequencers are assigned to the handles inside the virtual sequence from the test.

Here the base virtual sequence declares handles to the two target sequencers. The real virtual sequence creates the sub-sequences and starts them, and the test fills in the handles before starting it. Note the virtual sequence is started with null because it does not run on any single real sequencer.
// Base virtual sequence: it carries handles to the target sequencers.
class base_vseq extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(base_vseq)
// Target agent sequencers (filled in by the test).
uvm_sequencer #(ahb_txn) SQR_AHB;
uvm_sequencer #(axi_txn) SQR_AXI;
function new (string name = "base_vseq");
super.new(name);
endfunction : new
endclass : base_vseq
// The real virtual sequence: create the sub-sequences and start them.
class my_vseq extends base_vseq;
`uvm_object_utils(my_vseq)
function new (string name = "my_vseq");
super.new(name);
endfunction : new
task body();
ahb_sequence ahb_seq; // one sub-sequence per agent
axi_sequence axi_seq;
ahb_seq = ahb_sequence::type_id::create("ahb_seq");
axi_seq = axi_sequence::type_id::create("axi_seq");
// Run both sides in parallel; use fork/join to order them.
fork
ahb_seq.start(SQR_AHB);
axi_seq.start(SQR_AXI);
join
endtask : body
endclass : my_vseqThe test connects the handles and starts the virtual sequence. A small init_vseq helper keeps the handle assignment tidy.
// Base test: build the env and offer a helper to wire the handles.
class base_test extends uvm_test;
`uvm_component_utils(base_test)
top_env env;
function new (string name = "base_test", uvm_component parent = null);
super.new(name, parent);
endfunction : new
function void build_phase (uvm_phase phase);
super.build_phase(phase);
env = top_env::type_id::create("env", this);
endfunction : build_phase
// Assign the real sequencer handles into the virtual sequence.
function void init_vseq (base_vseq vseq);
vseq.SQR_AHB = env.ahb_agent.seqr;
vseq.SQR_AXI = env.axi_agent.seqr;
endfunction : init_vseq
endclass : base_test
// Main test: create the vseq, wire it, and start it with null.
class my_test extends base_test;
`uvm_component_utils(my_test)
function new (string name = "my_test", uvm_component parent = null);
super.new(name, parent);
endfunction : new
task run_phase (uvm_phase phase);
my_vseq vseq = my_vseq::type_id::create("vseq");
phase.raise_objection(this);
init_vseq(vseq); // connect the sequencer handles
vseq.start(null); // null: it runs on no single real sequencer
phase.drop_objection(this);
endtask : run_phase
endclass : my_testApproach 2: a virtual sequencer holds the handles
First, what is a virtual sequencer? It is a sequencer that is not connected to any driver. Its only job is to hold handles to the real sequencers so the virtual sequence can find them. As shown in the diagram below, the virtual sequencer sits inside the environment and holds the handles of the target sequencers, which physically live inside the AHB and AXI agents. The environment assigns those handles in its connect phase.

// Virtual sequencer: no driver, just handles to the real sequencers.
class virtual_seqr extends uvm_sequencer;
`uvm_component_utils(virtual_seqr)
ahb_seqr SQR_AHB; // handles to the real agent sequencers
axi_seqr SQR_AXI;
function new (string name = "virtual_seqr", uvm_component parent = null);
super.new(name, parent);
endfunction : new
endclass : virtual_seqrThe virtual sequence runs on the virtual sequencer. In its base body it casts m_sequencer (the sequencer it is running on) to the virtual sequencer type, then copies out the real handles.
// Base virtual sequence: grab the real handles from the virtual sequencer.
class base_vseq extends uvm_sequence #(uvm_sequence_item);
`uvm_object_utils(base_vseq)
virtual_seqr v_sqr; // the virtual sequencer we run on
ahb_seqr SQR_AHB; // real handles, copied from v_sqr
axi_seqr SQR_AXI;
function new (string name = "base_vseq");
super.new(name);
endfunction : new
task body();
// m_sequencer is the sequencer this sequence was started on.
if (!$cast(v_sqr, m_sequencer))
`uvm_fatal(get_full_name(), "Virtual sequencer cast failed")
SQR_AHB = v_sqr.SQR_AHB;
SQR_AXI = v_sqr.SQR_AXI;
endtask : body
endclass : base_vseq
// Real virtual sequence: call super.body() first, then run sub-sequences.
class my_vseq extends base_vseq;
`uvm_object_utils(my_vseq)
function new (string name = "my_vseq");
super.new(name);
endfunction : new
task body();
ahb_sequence ahb_seq;
axi_sequence axi_seq;
super.body(); // fills in SQR_AHB and SQR_AXI
ahb_seq = ahb_sequence::type_id::create("ahb_seq");
axi_seq = axi_sequence::type_id::create("axi_seq");
repeat (30) begin
fork
ahb_seq.start(SQR_AHB);
axi_seq.start(SQR_AXI);
join
end
endtask : body
endclass : my_vseqThe environment builds the virtual sequencer and both agents, then connects the real sequencer handles into the virtual sequencer during the connect phase.
// Environment: build the virtual sequencer and the two agents,
// then wire the real sequencer handles into the virtual sequencer.
class top_env extends uvm_env;
`uvm_component_utils(top_env)
virtual_seqr v_sqr;
ahb_agent ahb_agnt;
axi_agent axi_agnt;
function new (string name = "top_env", uvm_component parent = null);
super.new(name, parent);
endfunction : new
function void build_phase (uvm_phase phase);
super.build_phase(phase);
v_sqr = virtual_seqr::type_id::create("v_sqr", this);
ahb_agnt = ahb_agent::type_id::create("ahb_agnt", this);
axi_agnt = axi_agent::type_id::create("axi_agnt", this);
endfunction : build_phase
function void connect_phase (uvm_phase phase);
// m_sequencer is the default sequencer built inside an active agent.
v_sqr.SQR_AHB = ahb_agnt.m_sequencer;
v_sqr.SQR_AXI = axi_agnt.m_sequencer;
endfunction : connect_phase
endclass : top_envFinally the test builds the environment and starts the virtual sequence on the virtual sequencer.
// Test: build the env, then start the vseq on the virtual sequencer.
class my_test extends uvm_test;
`uvm_component_utils(my_test)
top_env env;
my_vseq vseq;
function new (string name = "my_test", uvm_component parent = null);
super.new(name, parent);
endfunction : new
function void build_phase (uvm_phase phase);
super.build_phase(phase);
env = top_env::type_id::create("env", this);
endfunction : build_phase
task run_phase (uvm_phase phase);
vseq = my_vseq::type_id::create("vseq");
phase.raise_objection(this);
vseq.start(env.v_sqr); // run it on the virtual sequencer
phase.drop_objection(this);
endtask : run_phase
endclass : my_testWhich approach should you pick?
Approach 1 keeps everything in the test, which is fine for small setups or a quick experiment. Approach 2 puts the handle wiring in the environment’s connect phase, so the test stays short and the same virtual sequencer is available to every virtual sequence. For any environment you plan to reuse, approach 2 is the usual choice.
Common mistakes
- Starting the virtual sequence on the wrong thing. In approach 1 start it with
null; in approach 2 start it on the virtual sequencer. - Forgetting to call
super.body()in approach 2, so the real sequencer handles are never copied out and stay null. - Casting
m_sequencerwithout checking the result. Always guard the$castand report a fatal if it fails. - Connecting the handles in the wrong phase. Handle wiring belongs in the connect phase, after the agents are built.
- Using a virtual sequence at all for a single-agent block. If there is one sequencer, run sequences on it directly.
- Mismatched class and type names (a very common typo) so the code does not compile. Keep the base and derived names consistent.
Key takeaways
- A virtual sequence coordinates sub-sequences across several real sequencers; it does not drive a driver itself.
- A virtual sequencer is a driverless sequencer that only holds handles to the real sequencers.
- Approach 1 keeps the handles in the sequence and starts it with
null. - Approach 2 keeps the handles in a virtual sequencer and starts the sequence on it; better for reuse.
- You only need this when there is more than one sequencer to coordinate.
Honesty note: the code follows a plain reading of the UVM LRM (IEEE 1800.2) and is written to show the idea, not captured from a specific simulator run. Confirm behaviour on your own tool or on EDA Playground before relying on it.
Related reading
- UVM sequencer and driver communication
- UVM sequence arbitration mechanism
- m_sequencer vs p_sequencer
- Typical UVM testbench architecture
What is a virtual sequence in UVM?
It is a top-level sequence that starts other sequences on several real sequencers and controls their order and timing. It does not send items to a driver itself.
What is a virtual sequencer?
A virtual sequencer is a sequencer that is not connected to any driver. It holds handles to the real sequencers so a virtual sequence running on it can start sub-sequences on them.
When do I need a virtual sequence?
When your testbench has two or more sequencers to coordinate, for example after integrating multiple agents into an SoC, or when a design has more than one interface port.
Do I always need a virtual sequencer to use a virtual sequence?
No. In approach 1 the virtual sequence holds the sequencer handles itself and is started with null. A virtual sequencer is used in approach 2, where the sequence runs on it and reads the handles from it.
Why do I start a virtual sequence with null in approach 1?
Because that virtual sequence does not run on any single real sequencer. It already carries the target sequencer handles, so it starts its sub-sequences on them directly.
What is m_sequencer?
m_sequencer is the handle to the sequencer that the current sequence was started on. In approach 2 you cast it to your virtual sequencer type to reach the real sequencer handles.
Where should I connect the target sequencer handles?
In approach 2, in the environment connect phase, after the virtual sequencer and the agents have been built. In approach 1, in the test before starting the virtual sequence.
Can a virtual sequence control the order of traffic on each agent?
Yes. That is its main purpose. Inside its body you decide which sub-sequence starts first, whether they run in parallel with fork/join, and how they repeat.



Thanks for the beautiful post!
Waiting for more such posts.
Thank you so much for going through the blog posts ๐