UVM Sequence Arbitration Mechanism
In short: When several sequences run on one sequencer at the same time, the sequencer must decide whose item goes to the driver next. That decision is arbitration. UVM offers six built-in policies, set with set_arbitration, ranging from strict round-robin to weighted and priority-based schemes. Sequences can also raise their priority, or use lock and grab to take exclusive control for a while.
Most simple tests run one sequence at a time, so arbitration never comes up. The moment you run two or more sequences on the same sequencer, though, the sequencer has to pick which item reaches the driver on each handshake. UVM gives you six arbitration policies plus priority, locking, and grabbing to control that choice. This guide explains each policy with a table, shows how to set them, walks through priority and lock and grab with runnable code, and lists the mistakes that lead to starved or stalled sequences. The running example is two sequences sharing one bus sequencer: a background traffic sequence and an urgent interrupt sequence.
A simple way to picture it
Think of a single checkout lane at a shop with several queues feeding into it. The cashier can only serve one customer at a time, so there has to be a rule for who goes next. A fair rule alternates between queues in turn (round-robin). A weighted rule lets a busy queue send more customers than a quiet one. A priority rule always serves the fast-track queue first. And sometimes one customer says please serve only me until I am finished, which is a lock. The sequencer is the cashier, the sequences are the queues, and the arbitration policy is the rule the cashier follows.
The six arbitration policies
These are the policies you can set on any sequencer. The default is UVM_SEQ_ARB_FIFO, plain first-come order.
| Policy | How it picks the next item |
|---|---|
| UVM_SEQ_ARB_FIFO | Strict first-in first-out order (the default) |
| UVM_SEQ_ARB_WEIGHTED | Random choice weighted by each sequence priority |
| UVM_SEQ_ARB_RANDOM | Purely random among waiting sequences, ignoring priority |
| UVM_SEQ_ARB_STRICT_FIFO | Highest priority first; ties broken by FIFO order |
| UVM_SEQ_ARB_STRICT_RANDOM | Highest priority first; ties broken randomly |
| UVM_SEQ_ARB_USER | Your own rule, by overriding user_priority_arbitration |
Two families are worth naming. The FIFO and RANDOM variants treat sequences roughly equally, while the STRICT variants respect priority strongly, always serving the highest-priority waiting sequence first. WEIGHTED sits in between, giving higher-priority sequences a better chance without shutting the others out entirely.
Setting the policy
You set the policy on the sequencer, usually in the test or environment, with set_arbitration. Here we tell our bus sequencer to respect priority strictly.
class prio_test extends base_test;
`uvm_component_utils(prio_test)
function new(string n, uvm_component p); super.new(n,p); endfunction
function void end_of_elaboration_phase(uvm_phase phase);
// respect sequence priority; break ties in FIFO order
env.agt.sqr.set_arbitration(UVM_SEQ_ARB_STRICT_FIFO);
endfunction
endclassSetting sequence priority
Priority only matters for the WEIGHTED and STRICT policies. The default priority is 100, and higher numbers mean more important. You can set a priority when you start a sequence, or change it while the sequence runs. Here the interrupt sequence starts at a higher priority than the background traffic sequence.
// run two sequences on the same sequencer with different priority
task run_phase(uvm_phase phase);
bg_seq bg;
irq_seq irq;
phase.raise_objection(this);
bg = bg_seq ::type_id::create("bg");
irq = irq_seq::type_id::create("irq");
fork
bg.start (env.agt.sqr, null, 100); // normal priority
irq.start(env.agt.sqr, null, 300); // higher priority
join
phase.drop_objection(this);
endtaskWith a STRICT policy, the sequencer will always take the interrupt items ahead of background items whenever both are waiting. A sequence can also change its own priority mid-run using set_priority, which is useful when a sequence becomes urgent partway through.
task body();
// start low, then escalate once a condition is met
set_priority(50);
send_setup_traffic();
set_priority(400); // now jump ahead of others
send_critical_traffic();
endtaskLock and grab: taking exclusive control
Sometimes priority is not enough and a sequence needs the sequencer entirely to itself for a stretch, for example to run an atomic read-modify-write that no other sequence may interrupt. UVM offers two ways to do this, and the difference between them is timing.
| Method | When it takes control | Use it for |
|---|---|---|
| lock | Politely: waits its turn in arbitration, then holds exclusive access | Exclusive access that can wait for a clean handoff |
| grab | Aggressively: jumps ahead of all waiting sequences immediately | Urgent exclusive access, such as a reset or interrupt |
Both are released with unlock (or ungrab). The key difference: lock respects the current arbitration order and waits for its normal turn before taking hold, while grab cuts to the front at once. Always pair a lock or grab with an unlock, or the sequencer stays blocked for everyone else.
// atomic read-modify-write that no other sequence may split
task body();
bus_item rd, wr;
lock(m_sequencer); // wait for turn, then hold exclusively
// read
rd = bus_item::type_id::create("rd");
start_item(rd); assert(rd.randomize() with { addr==16'h200; is_write==0; }); finish_item(rd);
// modify and write back, guaranteed uninterrupted
wr = bus_item::type_id::create("wr");
start_item(wr); assert(wr.randomize() with { addr==16'h200; is_write==1; data==rd.data + 1; }); finish_item(wr);
unlock(m_sequencer); // release for others
endtask// urgent interrupt handler cuts to the front
task body();
grab(m_sequencer); // jump ahead immediately
send_interrupt_ack();
ungrab(m_sequencer);
endtaskA custom arbitration rule
When none of the built-in policies fit, set UVM_SEQ_ARB_USER and override user_priority_arbitration on your sequencer. It receives the list of waiting sequence request indexes and returns the one to serve next. This example always favors any sequence whose name starts with irq, then falls back to the first waiting request.
class my_sequencer extends uvm_sequencer #(bus_item);
`uvm_component_utils(my_sequencer)
function new(string n, uvm_component p); super.new(n,p); endfunction
function integer user_priority_arbitration(integer avail_sequences[$]);
foreach (avail_sequences[i]) begin
uvm_sequence_base s = arb_sequence_q[avail_sequences[i]].sequence_ptr;
if (s != null && s.get_name().substr(0,2) == "irq")
return avail_sequences[i]; // serve interrupt sequences first
end
return avail_sequences[0]; // otherwise the first waiting one
endfunction
endclassHow arbitration fits the handshake
Arbitration happens at exactly one point: when the driver asks for the next item. Each time the driver calls get_next_item, the sequencer looks at every sequence currently blocked in start_item, applies the arbitration policy, and lets one of them proceed. So arbitration is not continuous; it is a decision made once per item, right at the handshake described in our sequencer and driver communication guide.
Common mistakes to avoid
- Expecting priority to matter under the FIFO or RANDOM policies. Priority only affects WEIGHTED and the STRICT policies; the others ignore it.
- Grabbing or locking and forgetting to ungrab or unlock, which blocks every other sequence on that sequencer for the rest of the test.
- Giving one sequence a very high priority under a STRICT policy, which can starve lower-priority sequences that then never run.
- Using grab when lock is enough. grab cuts the line immediately and can disrupt in-progress ordering; prefer lock unless the access is truly urgent.
- Assuming arbitration runs continuously. It only happens when the driver requests the next item, so a stalled driver means no arbitration at all.
Keep learning
Arbitration only matters once you understand the single-item handshake, so start with UVM sequencer and driver communication. See how sequences reach the sequencer in m_sequencer vs p_sequencer, and see the sequencer inside the full picture in the UVM testbench architecture guide. More is on the UVM category page, and arbitration comes up often in our interview questions.
Frequently asked questions
What is sequence arbitration in UVM?
It is the decision the sequencer makes about which waiting sequence gets to send its item to the driver next. When only one sequence runs, there is nothing to arbitrate, but with several sequences competing on one sequencer, the arbitration policy decides the order.
What is the default arbitration policy?
The default is UVM_SEQ_ARB_FIFO, which serves sequences in strict first-come order and ignores priority. If you want priority to take effect, switch to a WEIGHTED or STRICT policy using set_arbitration.
What is the difference between lock and grab?
Both give a sequence exclusive access to the sequencer, but lock waits for its normal turn in the arbitration order before taking hold, while grab cuts to the front immediately. Use grab only for urgent access such as an interrupt or reset; otherwise prefer lock.
Can a high-priority sequence starve the others?
Yes, under a STRICT policy. If a high-priority sequence always has items ready, the strict rule keeps serving it and lower-priority sequences may never run. WEIGHTED avoids this by giving everyone some chance, weighted by priority.
How do I set the priority of a sequence?
Pass a priority as the third argument to start, for example seq.start(sqr, null, 300), or call set_priority inside the sequence to change it while running. Higher numbers mean higher priority, and the default is 100.
When is the arbitration decision actually made?
Only when the driver requests the next item through get_next_item. At that moment the sequencer looks at all sequences waiting in start_item, applies the policy, and lets one proceed. It is a per-item decision, not a continuous background process.


