Ethernet · Module 14
Backpressure and Head-of-Line Blocking
Every queue is work-conserving, every link runs at line rate, every switch is non-blocking — and the composition delivers 58.6% of capacity. The bound has been known since 1987.
Chapter 12.1 §6 rejected backpressure in one sentence: it converts a local congestion into a global one, throttling conversations bound for idle ports along with the one that is busy. Chapter 14.2 §15 traced the first two hops of that happening. This chapter follows it the rest of the way and puts a number on it.
The number is 2 − √2 = 0.5858 — 58.6% of capacity — and it has been known since 1987.
It is the throughput of a switch whose ingress ports hold their frames in first-in-first-out queues, under uniform random traffic, with no congestion, no oversubscription and no faults. Every queue is work-conserving. Every link runs at line rate. The fabric is non-blocking. And 41.4% of the switch's capacity is unreachable, because a frame at the head of an ingress queue that cannot be forwarded blocks every frame behind it — including the ones whose destinations are idle.
That is head-of-line blocking, and backpressure is the mechanism that spreads it across a network.
1. Scope — What This Chapter Owns
This chapter owns the composition: what head-of-line blocking is, the 58.6% bound and where it comes from, how backpressure propagates it across a topology, how far the damage reaches, and the structural fix.
It does not own the buffers — Chapter 14.1 found where frames are lost and priced what a buffer buys.
It does not own PAUSE — Chapter 14.2 built the frame, the dead time and the headroom. This chapter takes a working PAUSE and asks what happens when it is used.
And it does not own the fix's per-class version. Virtual output queueing removes head-of-line blocking within a switch; Chapter 14.4 removes the per-class collateral between switches, using Chapter 13.2's PCP field. This chapter builds the first and states the requirement for the second.
2. One Frame at the Head
Head-of-line blocking is a single sentence and the sentence is easy to underestimate.
A frame at the head of a FIFO that cannot be forwarded blocks every frame behind it, regardless of where those frames are going.
Concretely, on a 24-port switch:
| Position in the ingress FIFO | Destination | Egress state | Can it go |
|---|---|---|---|
| head | port 7 | congested | no |
| second | port 3 | idle | no — blocked |
| third | port 11 | idle | no — blocked |
| fourth | port 3 | idle | no — blocked |
Three frames with idle destinations are held by one frame with a busy one, and the queue is doing exactly what a FIFO does. Nothing is broken.
And the loss is not merely those three frames' latency. The egress ports 3 and 11 are idle with work available for them — so the switch's throughput is below its capacity while every queue is non-empty, which is the precise condition a work-conserving system is supposed to avoid.
The work-conservation property does hold, and that is what makes the result surprising. The queue is work-conserving: it is never idle when its head can be served. The system is not, because the queue's discipline prevents it from offering the work it has to the servers that are free.
Which is the composition failure the chapter is about, and the arithmetic is Section 4's.
3. RTL 1 — The Queue Whose Head Blocks
A FIFO, and one output that says how much work it is holding back.
// -----------------------------------------------------------------------
// hol_pkg -- shared types for head-of-line blocking and backpressure.
// -----------------------------------------------------------------------
package hol_pkg;
localparam int N_PORTS = 24;
localparam int PORT_W = 5;
// Why a frame at the head could not be forwarded. The distinction
// matters because only the first is congestion; the others are the
// composition failing.
typedef enum logic [2:0] {
HB_NONE = 3'd0,
HB_EGRESS_BUSY = 3'd1, // the destination is genuinely congested
HB_EGRESS_XOFF = 3'd2, // the destination is PAUSED by ITS neighbour
HB_NO_BUFFER = 3'd3, // Chapter 14.1's pool
HB_BLOCKED = 3'd4 // this frame is behind one of the above
} hol_reason_e;
typedef enum logic [1:0] {
QD_FIFO = 2'd0, // one queue, head blocks -- 58.6%
QD_VOQ = 2'd1 // one queue per destination -- Section 11
} queue_discipline_e;
endpackage// -----------------------------------------------------------------------
// input_queue_fifo -- a single ingress queue, and the measurement that
// makes its blocking visible.
//
// The queue is work-conserving and correct. What it cannot do is offer
// the frame at position two to a server that is free, and that inability
// is the whole of head-of-line blocking.
// -----------------------------------------------------------------------
module input_queue_fifo
import hol_pkg::*;
#(
parameter int DEPTH = 64,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic enq_valid,
input logic [PORT_W-1:0] enq_dest,
output logic enq_ready,
input logic [N_PORTS-1:0] egress_available, // which servers are free
output logic head_valid,
output logic [PORT_W-1:0] head_dest,
output logic head_can_go,
input logic head_taken,
output logic [7:0] occupancy,
output logic [7:0] runnable_behind_head, // THE MEASUREMENT
output hol_reason_e reason,
output logic [CNT_W-1:0] c_blocked_cycles,
output logic [CNT_W-1:0] c_runnable_blocked,
output logic [15:0] hol_loss_pct
);
logic [PORT_W-1:0] q_dest [DEPTH];
logic [7:0] head_ptr, tail_ptr, count;
assign enq_ready = (count < 8'(DEPTH));
assign head_valid = (count != 8'd0);
assign head_dest = q_dest[head_ptr];
// The head can be served if its destination is free. Nothing else in
// the queue is consulted, which is the definition of a FIFO.
assign head_can_go = head_valid && egress_available[head_dest];
// THE MEASUREMENT THIS MODULE EXISTS FOR. How many frames BEHIND the
// head have destinations that are free right now? Each one is work the
// switch is holding while a server sits idle -- and no other signal in
// a FIFO design reports it.
always_comb begin
runnable_behind_head = 8'd0;
if (head_valid && !head_can_go) begin
for (int i = 1; i < DEPTH; i++)
if ((8'(i) < count) &&
egress_available[q_dest[(head_ptr + 8'(i)) % 8'(DEPTH)]])
runnable_behind_head = runnable_behind_head + 8'd1;
end
end
always_comb begin
reason = HB_NONE;
if (head_valid && !head_can_go) reason = HB_EGRESS_BUSY;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
head_ptr <= '0; tail_ptr <= '0; count <= '0;
for (int i = 0; i < DEPTH; i++) q_dest[i] <= '0;
c_blocked_cycles <= '0;
c_runnable_blocked <= '0;
hol_loss_pct <= '0;
end else begin
if (enq_valid && enq_ready) begin
q_dest[tail_ptr] <= enq_dest;
tail_ptr <= (tail_ptr + 8'd1) % 8'(DEPTH);
count <= count + 8'd1;
end
if (head_taken && head_valid) begin
head_ptr <= (head_ptr + 8'd1) % 8'(DEPTH);
count <= count - 8'd1;
end
if (head_valid && !head_can_go) begin
c_blocked_cycles <= c_blocked_cycles + 1'b1;
// Cycles in which the switch had runnable work and could not
// reach it. This is the capacity Section 4's bound describes.
if (runnable_behind_head != 8'd0)
c_runnable_blocked <= c_runnable_blocked + CNT_W'(runnable_behind_head);
end
if (c_blocked_cycles[15:0] == 16'hFFFF)
hol_loss_pct <= 16'((c_runnable_blocked * CNT_W'(100)) /
(c_blocked_cycles + CNT_W'(1)));
end
end
endmoduleClassification: behavioural — the runnable_behind_head scan over the whole queue is a measurement aid rather than something a real ingress path computes every cycle.
What it teaches: that runnable_behind_head is the only signal in a FIFO design that reports the loss. Occupancy says the queue is full; the drop counter says nothing has been dropped; the egress ports say they are idle. None of them says this switch is holding four frames it could deliver right now and cannot reach them — and that quantity is precisely the capacity Section 4's bound describes.
And it teaches that the queue is behaving correctly throughout. It is work-conserving: it is never idle when its head can be served. The property holds and the system still loses 41.4% of its capacity, because work-conservation is a statement about the queue's own idleness and not about the servers'.
Deliberately simplified: egress_available as a combinational bitmap of every port's state. A real ingress path sees a delayed, per-port grant rather than an instantaneous view — which makes the blocking slightly worse than modelled, because the head's destination may free up before the ingress learns of it.
Production implication: hol_loss_pct is a number almost no switch reports and it is the one that says whether a design's ingress discipline is costing capacity. A value near zero means the traffic is nearly uniform in a way that avoids blocking — mostly one destination, or very light load. A value approaching 41% means the switch is at Section 4's bound, and no amount of buffer, bandwidth or scheduling tuning recovers it; only Section 11's structural change does.
4. The 58.6% Bound
Karol, Hluchyj and Morgan derived it in 1987 for an input-queued switch with FIFO ingress queues under uniform random traffic, and the value is exact.
2 − √2 = 0.585786…
The argument, in outline. Under uniform traffic each ingress queue's head selects a destination uniformly at random. Several heads may select the same egress, and only one can be served — so the others' queues stall for that slot and the frames behind them stall too, whatever their destinations. As the switch grows, the fraction of slots in which a given queue's head is blocked converges, and the achievable throughput converges to 2 − √2.
| Throughput | Capacity lost | |
|---|---|---|
| output-queued — no ingress FIFO | 100% | 0 |
| input-queued, FIFO | 58.6% | 41.4% |
| input-queued, virtual output queues | 100% | 0 — Section 11 |
Three facts about the bound are worth stating precisely, because each is routinely misremembered.
It is not a congestion result. The derivation assumes uniform random traffic with no oversubscription — every egress port is offered exactly its fair share. The 41.4% is lost on a switch that is not congested at all.
It is not an implementation artefact. No buffer size, clock speed, fabric width or scheduling policy changes it, because the constraint is that a FIFO can offer only its head. The bound is a property of the discipline, not of the hardware.
And it gets worse under non-uniform traffic, which is what real traffic is. A single hot destination raises the probability that heads collide, and the achievable throughput falls below 58.6% — the bound is a best case for FIFO ingress queueing rather than a typical one.
5. RTL 2 — Measuring the Blocking
The bound is a theorem; a design needs a measurement, and it costs one comparison per queued frame.
// -----------------------------------------------------------------------
// hol_detector -- how much throughput is this FIFO discipline costing?
//
// Section 4's bound says 41.4% under uniform traffic. This module reports
// what THIS switch is actually losing, which under non-uniform traffic is
// worse and under a single hot destination is much worse.
// -----------------------------------------------------------------------
module hol_detector
import hol_pkg::*;
#(
parameter int CNT_W = 32,
parameter int WIN = 1_000_000
)(
input logic clk,
input logic rst_n,
input logic [N_PORTS-1:0] head_valid,
input logic [N_PORTS-1:0] head_blocked,
input logic [N_PORTS-1:0] egress_idle,
input logic [7:0] runnable_behind [N_PORTS],
input logic [N_PORTS-1:0] grant,
output logic window_valid,
output logic [15:0] throughput_pct,
output logic [15:0] hol_loss_pct,
output logic [15:0] idle_with_work_pct,
output logic at_the_bound, // near 58.6%
output logic below_the_bound, // non-uniform traffic
output logic [PORT_W-1:0] hottest_dest,
output logic [CNT_W-1:0] c_slots,
output logic [CNT_W-1:0] c_grants
);
logic [CNT_W-1:0] win_slots, win_grants, win_idle_work, win_runnable;
logic [CNT_W-1:0] dest_hits [N_PORTS];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
win_slots <= '0; win_grants <= '0; win_idle_work <= '0; win_runnable <= '0;
for (int p = 0; p < N_PORTS; p++) dest_hits[p] <= '0;
c_slots <= '0; c_grants <= '0;
window_valid <= 1'b0;
throughput_pct <= 16'd100;
hol_loss_pct <= '0;
idle_with_work_pct <= '0;
at_the_bound <= 1'b0;
below_the_bound <= 1'b0;
hottest_dest <= '0;
end else begin
window_valid <= 1'b0;
win_slots <= win_slots + 1'b1;
c_slots <= c_slots + 1'b1;
win_grants <= win_grants + CNT_W'($countones(grant));
c_grants <= c_grants + CNT_W'($countones(grant));
// THE CONDITION THE BOUND IS ABOUT. An egress port idle while some
// ingress queue holds a frame for it -- behind a head it cannot
// pass. Every one of these is capacity that exists and cannot be
// reached.
for (int p = 0; p < N_PORTS; p++) begin
if (head_valid[p] && head_blocked[p] && (runnable_behind[p] != 8'd0)) begin
win_idle_work <= win_idle_work + 1'b1;
win_runnable <= win_runnable + CNT_W'(runnable_behind[p]);
end
end
if (win_slots >= CNT_W'(WIN)) begin
// Throughput as a share of the N_PORTS grants per slot that a
// fully utilised switch would issue.
throughput_pct <= 16'((win_grants * CNT_W'(100)) /
(win_slots * CNT_W'(N_PORTS)));
hol_loss_pct <= 16'((win_runnable * CNT_W'(100)) /
(win_slots * CNT_W'(N_PORTS)));
idle_with_work_pct <= 16'((win_idle_work * CNT_W'(100)) / win_slots);
// Section 4's bound, as two bits. AT the bound means the traffic
// is roughly uniform and the discipline is the limit. BELOW it
// means the traffic is non-uniform, which is worse and is what
// real traffic does.
at_the_bound <= (throughput_pct >= 16'd55) &&
(throughput_pct <= 16'd62);
below_the_bound <= (throughput_pct < 16'd55);
begin
automatic logic [CNT_W-1:0] hi = '0;
automatic logic [PORT_W-1:0] hp = '0;
for (int p = 0; p < N_PORTS; p++)
if (dest_hits[p] > hi) begin hi = dest_hits[p]; hp = PORT_W'(p); end
hottest_dest <= hp;
for (int p = 0; p < N_PORTS; p++) dest_hits[p] <= '0;
end
win_slots <= '0; win_grants <= '0;
win_idle_work <= '0; win_runnable <= '0;
window_valid <= 1'b1;
end
end
end
wire _unused = |egress_idle;
endmoduleClassification: synthesizable, with the per-port scan pipelined in a real design.
What it teaches: that at_the_bound and below_the_bound are different findings. At the bound — 55% to 62% — the traffic is roughly uniform and the FIFO discipline is the limit, which is Section 4's theorem observed rather than assumed. Below it, the traffic is non-uniform, which is what real traffic is: a single hot destination raises the probability of head collisions and pushes throughput under the bound.
And it teaches that idle_with_work_pct is the operational form of the theorem. An egress port was idle while some ingress queue held a frame for it — that is the capacity the bound describes, expressed as a fraction of slots, and it is the number that turns a mathematical result into something a switch can report about itself.
Deliberately simplified: grants counted as an aggregate rather than matched against a maximum matching. A rigorous measurement would compare the grants issued against the size of the maximum bipartite matching available in that slot — which is what a VOQ scheduler computes and a FIFO cannot.
Production implication: throughput_pct sitting near 58 on a switch with no congestion, no errors and no dropped frames is not a fault to be investigated — it is the design's discipline being what it is. The finding is that the number exists at all, because without it the same switch reports full links, empty drop counters and unexplained slowness, and every hypothesis points at something that is not wrong.
6. Backpressure Turns One Switch's Blocking Into a Network's
Section 4's bound is about one switch. Chapter 14.2's PAUSE is what carries the condition across a link, and the mechanism is the same one seen from a different scale.
A paused link is an egress port that cannot be served. From the perspective of the upstream switch's ingress queue, a PAUSE and a congested destination are indistinguishable — the head cannot go, and everything behind it waits.
So each hop reproduces Section 2's table with a wider blast radius:
| Hop | The head is blocked because | Frames blocked behind it |
|---|---|---|
| 0 — the origin | its egress is genuinely oversubscribed | destinations across this switch |
| 1 | its egress link is PAUSED | destinations across a different switch |
| 2 | its egress link is paused by hop 1 | destinations two switches away |
| 3 | … | … |
And the reach compounds. At hop 0 the blocking affects conversations through one switch. At hop 1 it affects conversations through the switch that pauses, which has its own 23 ports and its own destinations — none of which is the congested one.
| Hops of propagation | Ingress–egress pairs potentially affected |
|---|---|
| 1 | 23 |
| 2 | 529 |
| 3 | 12 167 |
Those are upper bounds rather than predictions, and the real figure depends on the topology and the traffic matrix. What is not an upper bound is the direction: every hop widens the set of conversations affected by a congestion none of them is part of.
Which is Chapter 12.1 §6's sentence, unpacked: backpressure converts a local congestion into a global one. Each hop's decision is correct — Chapter 14.2 §15 established that — and the aggregate is a network in which one oversubscribed port slows conversations that never touch it.
7. RTL 3 — Propagating Backpressure
A paused egress makes an ingress queue's head unservable, which fills the queue, which triggers a pause upstream. Three lines of logic and a topology-wide consequence.
// -----------------------------------------------------------------------
// backpressure_propagator -- how a pause at one egress becomes a pause on
// an upstream link.
//
// Nothing here is a policy decision. Each step follows from the previous
// one, and the module exists to make the chain visible rather than to
// choose anything.
// -----------------------------------------------------------------------
module backpressure_propagator
import hol_pkg::*;
#(
parameter int HIGH_CELLS = 3072,
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic [N_PORTS-1:0] egress_paused, // by OUR neighbours
input logic [N_PORTS-1:0] egress_congested, // genuinely oversubscribed
input logic [PORT_W-1:0] head_dest [N_PORTS],
input logic [N_PORTS-1:0] head_valid,
input logic [15:0] ingress_occupancy [N_PORTS],
output logic [N_PORTS-1:0] head_blocked,
output hol_reason_e block_reason [N_PORTS],
output logic [N_PORTS-1:0] send_pause_upstream,
output logic [CNT_W-1:0] c_blocked_by_congestion,
output logic [CNT_W-1:0] c_blocked_by_pause, // second-order
output logic [CNT_W-1:0] c_pauses_generated,
output logic [15:0] second_order_pct,
output logic propagating
);
always_comb begin
for (int p = 0; p < N_PORTS; p++) begin
head_blocked[p] = 1'b0;
block_reason[p] = HB_NONE;
if (head_valid[p]) begin
if (egress_congested[head_dest[p]]) begin
// FIRST ORDER. This is real congestion at the destination.
head_blocked[p] = 1'b1;
block_reason[p] = HB_EGRESS_BUSY;
end else if (egress_paused[head_dest[p]]) begin
// SECOND ORDER. The destination is not congested -- it has
// been PAUSED by its own neighbour. The congestion this queue
// is suffering belongs to a switch it may never send to.
head_blocked[p] = 1'b1;
block_reason[p] = HB_EGRESS_XOFF;
end
end
// And the consequence: a blocked head fills this ingress queue,
// which pauses the link that feeds it -- one more hop.
send_pause_upstream[p] = (ingress_occupancy[p] >= 16'(HIGH_CELLS));
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
c_blocked_by_congestion <= '0;
c_blocked_by_pause <= '0;
c_pauses_generated <= '0;
second_order_pct <= '0;
propagating <= 1'b0;
end else begin
for (int p = 0; p < N_PORTS; p++) begin
if (block_reason[p] == HB_EGRESS_BUSY)
c_blocked_by_congestion <= c_blocked_by_congestion + 1'b1;
if (block_reason[p] == HB_EGRESS_XOFF)
c_blocked_by_pause <= c_blocked_by_pause + 1'b1;
if (send_pause_upstream[p])
c_pauses_generated <= c_pauses_generated + 1'b1;
end
// THE NUMBER THAT SAYS WHOSE PROBLEM THIS IS. Blocking caused by
// a PAUSE rather than by congestion is congestion that arrived
// from somewhere else, and this switch is a conduit rather than a
// cause.
if (c_blocked_by_congestion + c_blocked_by_pause != '0)
second_order_pct <= 16'((c_blocked_by_pause * CNT_W'(100)) /
(c_blocked_by_congestion + c_blocked_by_pause));
propagating <= (|send_pause_upstream) && (|egress_paused);
end
end
endmoduleClassification: synthesizable.
What it teaches: that HB_EGRESS_BUSY and HB_EGRESS_XOFF are first-order and second-order congestion, and separating them tells a switch whether it is a cause or a conduit. A head blocked because its destination is genuinely oversubscribed is this switch's congestion. A head blocked because its destination has been paused by its own neighbour is congestion that arrived from a switch this one may never send to.
And propagating is the bit that says the chain is active here. High means this switch is both receiving backpressure and generating it — it is one link of the chain rather than either end — and every conversation crossing it is being slowed by a congestion it is not part of.
Deliberately simplified: egress_congested and egress_paused as separate inputs. A real design derives both from the same egress state machine, and distinguishing them requires the egress to remember why it is unavailable — which is the one piece of state that makes the first-order/second-order split possible at all.
Production implication: second_order_pct is the number that ends an investigation at the right switch. Near zero, this switch's blocking is its own congestion and the remedy is here. Near 100, this switch is a conduit — its queues are full because a downstream link is paused, and every minute spent examining its traffic is spent at the wrong device. The number is available at every hop, so following it upstream leads to the origin in as many steps as the chain is long.
8. How Far the Damage Reaches
Section 6 gave upper bounds. What a design can actually measure is how many distinct conversations were affected, and it costs a bitmap.
The quantity worth tracking is the set of (ingress, egress) pairs that were blocked during a congestion event — because that set, not the number of dropped frames, is what an operator experiences as the network was slow.
| Event | Frames dropped | Pairs affected | What users report |
|---|---|---|---|
| a congested egress, no backpressure | many | 23 — one switch's ingresses | "port 7 is slow" |
| the same, with backpressure, 1 hop | few | up to 529 | "the network is slow" |
| the same, 2 hops | very few | up to 12 167 | "everything is slow" |
The inversion in the first two columns is the finding. Backpressure reduces the drop count — that is what it is for — and increases the number of conversations affected by orders of magnitude.
So a network with flow control enabled reports fewer drops and more complaints, and the two facts are consistent: the loss was converted into delay and the delay was distributed to conversations that were not involved.
Which is why Chapter 12.1 §6 called backpressure a strictly worse pathology than dropping, and why the argument survives everything Chapter 14.2 built: PAUSE works exactly as specified, and working correctly is what produces this.
9. RTL 4 — Measuring the Blast Radius
A bitmap per ingress port, set when a pair is blocked, and cleared at the end of an event.
// -----------------------------------------------------------------------
// blast_radius_tracker -- how many distinct conversations did this
// congestion event affect?
//
// The frame count says how much was lost. This says how much was
// TOUCHED, and Section 8 shows the two move in opposite directions when
// backpressure is enabled.
// -----------------------------------------------------------------------
module blast_radius_tracker
import hol_pkg::*;
#(
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic event_active, // any head blocked anywhere
input logic [N_PORTS-1:0] head_blocked,
input logic [PORT_W-1:0] head_dest [N_PORTS],
input hol_reason_e block_reason [N_PORTS],
output logic [15:0] pairs_affected,
output logic [15:0] worst_pairs_affected,
output logic [15:0] ingress_ports_affected,
output logic [15:0] egress_ports_affected,
output logic [15:0] second_order_pairs,
output logic [CNT_W-1:0] c_events,
output logic [CNT_W-1:0] c_pair_seconds,
output logic wide_event // beyond one switch's worth
);
logic [N_PORTS-1:0] pair_seen [N_PORTS]; // [ingress][egress]
logic [N_PORTS-1:0] pair_2nd [N_PORTS];
logic active_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int i = 0; i < N_PORTS; i++) begin
pair_seen[i] <= '0; pair_2nd[i] <= '0;
end
active_q <= 1'b0;
pairs_affected <= '0;
worst_pairs_affected <= '0;
ingress_ports_affected <= '0;
egress_ports_affected <= '0;
second_order_pairs <= '0;
c_events <= '0;
c_pair_seconds <= '0;
wide_event <= 1'b0;
end else begin
active_q <= event_active;
if (event_active) begin
for (int i = 0; i < N_PORTS; i++)
if (head_blocked[i]) begin
pair_seen[i][head_dest[i]] <= 1'b1;
// A pair blocked by a PAUSE rather than by congestion is a
// conversation affected by somebody else's problem entirely.
if (block_reason[i] == HB_EGRESS_XOFF)
pair_2nd[i][head_dest[i]] <= 1'b1;
// Pair-seconds: the integral of affected conversations over
// time, which is the closest single number to "how bad was
// it" that a switch can compute.
c_pair_seconds <= c_pair_seconds + 1'b1;
end
end
// At the end of an event, report and reset.
if (active_q && !event_active) begin
automatic logic [15:0] pairs = 16'd0;
automatic logic [15:0] snd = 16'd0;
automatic logic [15:0] ing = 16'd0;
automatic logic [N_PORTS-1:0] egr = '0;
for (int i = 0; i < N_PORTS; i++) begin
if (pair_seen[i] != '0) ing = ing + 16'd1;
egr = egr | pair_seen[i];
pairs = pairs + 16'($countones(pair_seen[i]));
snd = snd + 16'($countones(pair_2nd[i]));
end
pairs_affected <= pairs;
second_order_pairs <= snd;
ingress_ports_affected <= ing;
egress_ports_affected <= 16'($countones(egr));
if (pairs > worst_pairs_affected) worst_pairs_affected <= pairs;
// MORE PAIRS THAN THIS SWITCH HAS INGRESS PORTS means the event
// touched more conversations than a single congested egress
// could -- Section 8's compounding, observed.
wide_event <= (pairs > 16'(N_PORTS));
if (!(&c_events)) c_events <= c_events + 1'b1;
for (int i = 0; i < N_PORTS; i++) begin
pair_seen[i] <= '0; pair_2nd[i] <= '0;
end
end
end
end
endmoduleClassification: synthesizable, at N_PORTS² bits of state — 576 bits for a 24-port switch, which is trivial.
What it teaches: that c_pair_seconds is the closest single number to how bad was it that a switch can compute. A drop count says how much was lost; a duration says how long it lasted; the integral of affected conversations over time says how much was touched, which is what an operator's inbox measures.
And second_order_pairs separates the conversations affected by this switch's congestion from those affected by somebody else's. A large second-order share means this switch inflicted delay on its own users because of a problem it does not have — which is Section 8's inversion, measured at the device that is transmitting the harm rather than causing it.
Deliberately simplified: a bitmap cleared per event, where an event is any period with a blocked head anywhere. Production tracking uses a windowed decay rather than a hard reset, because a switch under intermittent congestion never has a clean event boundary.
Production implication: worst_pairs_affected exceeding N_PORTS is the signature of backpressure rather than of local congestion. A single congested egress on this switch can block at most 23 ingress heads — 23 pairs. More than that means several egress ports were unavailable at once, and on a switch that is not itself oversubscribed that means they were paused — Section 7's second-order condition, arriving as a count.
10. What the Fix Has to Be
Section 4's bound is a property of the discipline, so the fix is a different discipline. Every other lever has been tried and none of them touches it.
| Attempted fix | Effect on the 58.6% bound |
|---|---|
| a deeper ingress queue | none — the head still blocks |
| a faster fabric | none — the constraint is not bandwidth |
| a better egress scheduler | none — the work never reaches it |
| more buffer at the egress | none — Chapter 14.1's delay, not throughput |
| PAUSE | worse — Section 8's blast radius |
| one queue per destination | removes it entirely |
The last row is virtual output queueing, and the idea is one sentence: instead of one FIFO per ingress port, keep one FIFO per (ingress, egress) pair.
Then a blocked destination blocks only the queue for that destination. The frames for idle ports sit in their own queues and can be offered independently — so the ingress port has N candidates to present rather than one, and a scheduler can build a matching.
The cost is N queues per ingress port instead of one:
| Ports | FIFO queues | VOQ queues | Ratio |
|---|---|---|---|
| 8 | 8 | 64 | 8× |
| 24 | 24 | 576 | 24× |
| 48 | 48 | 2304 | 48× |
And the cost is smaller than it looks, because the queues share Chapter 14.1 §5's pool. A VOQ is a list of pointers — a head, a tail and a count — not a separate memory. For 576 queues at 32 bits of state each that is 2.3 KiB of pointers against a 12 MiB pool.
What is genuinely expensive is the scheduler, and Section 13 is about that: choosing which of 576 queues to serve in each slot is a bipartite matching problem, solved once per frame time, at Chapter 12.1 §12's 28 ns.
11. RTL 5 — Virtual Output Queues
One queue per destination, and the only thing that changes is how many candidates the ingress can offer.
// -----------------------------------------------------------------------
// voq_bank -- N queues at one ingress port, one per egress destination.
//
// The queues are POINTER LISTS into Chapter 14.1 Section 5's shared pool,
// not separate memories. What costs is not the storage -- 32 bits of
// state per queue -- but Section 13's scheduler, which must now choose
// among N candidates per ingress instead of accepting the one it had.
// -----------------------------------------------------------------------
module voq_bank
import hol_pkg::*;
#(
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic enq_valid,
input logic [PORT_W-1:0] enq_dest,
input logic [11:0] enq_handle, // Chapter 14.1's pool handle
input logic [N_PORTS-1:0] serve, // scheduler's selection
output logic [11:0] serve_handle,
output logic [N_PORTS-1:0] nonempty, // THE N CANDIDATES
output logic [7:0] depth [N_PORTS],
output logic [7:0] total_depth,
output logic [PORT_W-1:0] deepest_voq,
output logic [CNT_W-1:0] c_enq,
output logic [CNT_W-1:0] c_served,
output logic [CNT_W-1:0] bits_of_state,
output logic head_of_line_possible // permanently 0
);
logic [11:0] head [N_PORTS];
logic [11:0] tail [N_PORTS];
logic [7:0] cnt [N_PORTS];
always_comb begin
for (int d = 0; d < N_PORTS; d++) nonempty[d] = (cnt[d] != 8'd0);
end
// THE PROPERTY THIS STRUCTURE EXISTS FOR, as a wire. With one queue per
// destination, a blocked destination blocks only its own queue -- there
// is no position in which a frame can be held behind a frame for a
// different destination.
assign head_of_line_possible = 1'b0;
always_comb begin
serve_handle = 12'd0;
for (int d = 0; d < N_PORTS; d++)
if (serve[d]) serve_handle = head[d];
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int d = 0; d < N_PORTS; d++) begin
head[d] <= '0; tail[d] <= '0; cnt[d] <= '0;
end
c_enq <= '0; c_served <= '0;
deepest_voq <= '0;
end else begin
if (enq_valid) begin
// The frame joins the queue for ITS destination. Nothing else in
// the bank is affected, which is the whole difference.
tail[enq_dest] <= enq_handle;
cnt[enq_dest] <= cnt[enq_dest] + 8'd1;
if (cnt[enq_dest] == 8'd0) head[enq_dest] <= enq_handle;
c_enq <= c_enq + 1'b1;
end
for (int d = 0; d < N_PORTS; d++)
if (serve[d] && (cnt[d] != 8'd0)) begin
cnt[d] <= cnt[d] - 8'd1;
c_served <= c_served + 1'b1;
end
begin
automatic logic [7:0] hi = 8'd0;
automatic logic [PORT_W-1:0] hp = '0;
for (int d = 0; d < N_PORTS; d++)
if (cnt[d] > hi) begin hi = cnt[d]; hp = PORT_W'(d); end
deepest_voq <= hp;
end
end
end
always_comb begin
total_depth = 8'd0;
for (int d = 0; d < N_PORTS; d++) begin
depth[d] = cnt[d];
total_depth = total_depth + cnt[d];
end
end
// Head, tail and count per queue. 576 queues at 32 bits is 2.3 KiB
// against Chapter 14.1's 12 MiB pool -- the storage is not the cost.
assign bits_of_state = CNT_W'(N_PORTS) * CNT_W'(N_PORTS) * CNT_W'(32);
endmoduleClassification: synthesizable, with the queues as pointer lists rather than storage.
What it teaches: that the structural change is small and the consequence is total. A frame joins the queue for its destination, and nothing else in the bank is affected — so there is no position in which a frame can be held behind a frame for a different destination. head_of_line_possible is tied to zero and it is a claim about the structure rather than about the traffic.
And it teaches that the storage is not the cost. 576 queues of {head, tail, count} is 2.3 KiB of pointers against Chapter 14.1 §6's 12 MiB pool — 0.02%. The frames themselves live in the pool exactly as before; only their organisation changed.
Deliberately simplified: one VOQ set per ingress port with no priority dimension. Chapter 14.4 adds priority, which multiplies the queue count by the class count — 24 × 24 × 8 = 4608 queues on a 24-port switch with 8 classes — and that is where the pointer state stops being negligible.
Production implication: deepest_voq names the destination this ingress port is queueing most for, which is a per-ingress view of where the congestion is — and it is available at every ingress port simultaneously. Twenty-four ingress ports all reporting the same deepest_voq is an oversubscribed egress; twenty-four reporting different ones is normal traffic, and the pattern is visible in one read without any per-flow tracking.
12. The Scheduler Is Where the Cost Moved
Virtual output queues do not remove the difficulty; they relocate it. A FIFO ingress had one candidate and needed no decision. A VOQ bank has N candidates and needs a matching.
The problem, stated precisely: in each slot, choose a set of (ingress, egress) pairs such that no ingress appears twice and no egress appears twice, maximising the number of pairs. That is a maximum bipartite matching, and it must be computed in Chapter 12.1 §12's 28 ns.
| Approach | Throughput | Cost |
|---|---|---|
| FIFO — no matching | 58.6% | none |
| maximal matching, one iteration | ~63% | one round of request–grant–accept |
iterative, log N rounds | ~99% | log₂ 24 ≈ 5 rounds |
| maximum matching, exact | 100% | too slow for 28 ns |
The third row is what real switches build, and the algorithm family is well known: each unmatched ingress requests every destination it has a frame for; each egress grants one requester; each ingress accepts one grant; repeat. Five rounds at 500 MHz is 10 ns, inside the budget.
And the second row is why one round is not enough. A single request–grant–accept pass produces a maximal matching — one that cannot be extended without removing a pair — which is not the same as a maximum one, and the difference is worth 36 percentage points.
Which relocates Section 4's bound rather than removing it. The FIFO's 58.6% was a limit on what the queue could offer. A VOQ switch's throughput is a limit on what the scheduler can match in the time available — and a design that builds VOQs and then schedules them in one round has spent 576 queues to get from 58.6% to 63%.
13. RTL 6 — Matching Ingress to Egress
Request, grant, accept — repeated. Five rounds at 500 MHz is 10 ns, and the number of rounds is the whole design.
// -----------------------------------------------------------------------
// voq_scheduler -- iterative request-grant-accept matching.
//
// One round produces a MAXIMAL matching, worth about 63%. Log N rounds
// produce something close to MAXIMUM, worth about 99%. The difference is
// 36 percentage points and five cycles.
// -----------------------------------------------------------------------
module voq_scheduler
import hol_pkg::*;
#(
parameter int ROUNDS = 5, // ~log2(24)
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input logic start,
input logic [N_PORTS-1:0] voq_nonempty [N_PORTS], // [ingress][egress]
output logic done,
output logic [N_PORTS-1:0] grant_egress [N_PORTS], // the matching
output logic [7:0] match_size,
output logic [7:0] max_possible,
output logic [CNT_W-1:0] c_rounds_used,
output logic [15:0] match_efficiency_pct,
output logic [7:0] rounds_configured
);
logic [N_PORTS-1:0] ing_matched, egr_matched;
logic [N_PORTS-1:0] request [N_PORTS];
logic [N_PORTS-1:0] grant [N_PORTS];
logic [2:0] round_q;
logic busy_q;
// Round-robin pointers, so that repeated slots do not always favour the
// same pair. Without them the matching is stable and starves whoever
// loses the first tie.
logic [PORT_W-1:0] ing_ptr [N_PORTS];
logic [PORT_W-1:0] egr_ptr [N_PORTS];
always_comb begin
// REQUEST. Every unmatched ingress asks every destination it has a
// frame for. This is the step a FIFO cannot perform -- it has one
// candidate and therefore one request.
for (int i = 0; i < N_PORTS; i++)
request[i] = ing_matched[i] ? '0 : (voq_nonempty[i] & ~egr_matched);
// GRANT. Each unmatched egress picks one requester, starting from
// its round-robin pointer.
for (int e = 0; e < N_PORTS; e++) begin
grant[e] = '0;
if (!egr_matched[e]) begin
for (int k = N_PORTS-1; k >= 0; k--) begin
automatic int i = (int'(egr_ptr[e]) + k) % N_PORTS;
if (request[i][e]) grant[e] = (N_PORTS'(1) << i);
end
end
end
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
ing_matched <= '0; egr_matched <= '0;
for (int i = 0; i < N_PORTS; i++) begin
grant_egress[i] <= '0; ing_ptr[i] <= '0; egr_ptr[i] <= '0;
end
round_q <= '0; busy_q <= 1'b0; done <= 1'b0;
match_size <= '0; c_rounds_used <= '0;
match_efficiency_pct <= 16'd100;
end else begin
done <= 1'b0;
if (start) begin
ing_matched <= '0; egr_matched <= '0;
for (int i = 0; i < N_PORTS; i++) grant_egress[i] <= '0;
round_q <= '0; busy_q <= 1'b1;
end else if (busy_q) begin
// ACCEPT. Each ingress with grants takes one, and the pair is
// removed from both sides for the remaining rounds.
for (int i = 0; i < N_PORTS; i++) begin
for (int e = N_PORTS-1; e >= 0; e--)
if (grant[e][i] && !ing_matched[i]) begin
ing_matched[i] <= 1'b1;
egr_matched[e] <= 1'b1;
grant_egress[i][e] <= 1'b1;
ing_ptr[i] <= PORT_W'(e) + PORT_W'(1);
egr_ptr[e] <= PORT_W'(i) + PORT_W'(1);
end
end
round_q <= round_q + 3'd1;
c_rounds_used <= c_rounds_used + 1'b1;
if (round_q == 3'(ROUNDS - 1)) begin
busy_q <= 1'b0;
done <= 1'b1;
match_size <= 8'($countones(ing_matched));
end
end
end
end
// The size of the matching a perfect scheduler could have found: the
// number of ingress ports with any work, capped by the number of
// egress ports with any demand.
always_comb begin
automatic logic [7:0] ing_any = 8'd0;
automatic logic [N_PORTS-1:0] egr_any = '0;
for (int i = 0; i < N_PORTS; i++) begin
if (voq_nonempty[i] != '0) ing_any = ing_any + 8'd1;
egr_any = egr_any | voq_nonempty[i];
end
max_possible = (ing_any < 8'($countones(egr_any)))
? ing_any : 8'($countones(egr_any));
end
assign rounds_configured = 8'(ROUNDS);
endmoduleClassification: synthesizable; the nested loops elaborate into fixed comparator trees rather than into sequential logic.
What it teaches: that the request step is the one a FIFO cannot perform. An ingress with a VOQ bank requests every destination it has a frame for; an ingress with a FIFO has one candidate and therefore one request. Everything else in the algorithm is identical — the grant, the accept, the round-robin pointers — and the entire 41.4% is recovered by having more than one thing to ask for.
And it teaches why the round-robin pointers are not optional. Without them the matching is stable: the same ties resolve the same way every slot, and whoever loses the first tie loses every subsequent one. Chapter 12.1 §9's arbiter made the identical argument, and the failure has the same shape — a scheduler that is fair in aggregate and starves a specific pair indefinitely.
Deliberately simplified: a fixed round count and combinational grant logic. Production schedulers pipeline the rounds across successive frame times and use the previous slot's matching as a starting point, which converges faster because traffic is correlated between slots.
Production implication: match_size against max_possible is the scheduler's efficiency, and it is the number that says whether ROUNDS is set correctly. A ratio near 1 means the matching is finding nearly everything available; a ratio near 0.63 means one round's worth of matching, and the remedy is more rounds rather than more queues. A design that built 576 VOQs and configured one round has spent the storage and kept most of the bound.
14. The Cost of the Fix, Accounted
Sections 11 and 13 built the two halves. Put their costs side by side, because the intuition about which one is expensive is wrong.
| Component | 24-port switch | Against what |
|---|---|---|
VOQ pointer state — 24 × 24 × 32 bits | 2.3 KiB | Chapter 14.1's 12 MiB pool — 0.02% |
| the frames themselves | unchanged | they were already in the pool |
scheduler request matrix — 24 × 24 bits | 72 octets | trivial |
| scheduler comparator logic | 24 × 24 grant arbiters | the real cost |
| scheduler time — 5 rounds at 500 MHz | 10 ns | of Chapter 12.1 §12's 28 ns |
The storage is negligible and the time is 36% of the per-frame budget, which is the honest summary: VOQs cost almost no memory and a substantial fraction of the switch's tightest timing path.
And the rounds-versus-throughput curve is where a design actually chooses:
| Rounds | Cycles at 500 MHz | Throughput | Marginal gain |
|---|---|---|---|
| 1 | 2 ns | ~63% | — |
| 2 | 4 ns | ~85% | +22 |
| 3 | 6 ns | ~94% | +9 |
| 5 | 10 ns | ~99% | +5 |
| 8 | 16 ns | ~99.9% | +0.9 |
Most of the recovery is in the first three rounds — 63% to 94% for 6 ns — and the last percentage point costs as much as the first twenty. A design constrained on timing takes three rounds and 94%; one with margin takes five and stops there, because the eighth round costs 6 ns of a 28 ns budget for nine tenths of a percent.
Which reframes Section 12's conclusion. The bound did not disappear; it became a tunable trade between timing margin and throughput, and a design can now choose where on that curve to sit. A FIFO ingress offered no such choice — 58.6%, always, for free.
15. RTL 7 — Head-of-Line Telemetry
Six mechanisms, and one place to read whether this switch is limited by its discipline, its scheduler, or somebody else's congestion.
// -----------------------------------------------------------------------
// hol_telemetry -- which of the three limits is binding?
//
// A switch below its line rate has three candidate explanations and they
// are fixed in three different places. Distinguishing them is the whole
// value of the module.
// -----------------------------------------------------------------------
module hol_telemetry
import hol_pkg::*;
#(
parameter int CNT_W = 32,
parameter int WIN = 1_000_000
)(
input logic clk,
input logic rst_n,
input queue_discipline_e discipline,
input logic [15:0] throughput_pct,
input logic [7:0] match_size,
input logic [7:0] max_possible,
input logic [15:0] second_order_pct,
input logic [15:0] pairs_affected,
input logic [7:0] rounds_configured,
input logic slot,
output logic window_valid,
output logic [15:0] match_efficiency_pct,
output logic limited_by_discipline, // FIFO -- 58.6%
output logic limited_by_scheduler, // too few rounds
output logic limited_by_upstream, // somebody else's congestion
output logic none_of_the_above, // genuine offered load
output logic [CNT_W-1:0] c_slots
);
logic [CNT_W-1:0] win, win_match, win_max;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
win <= '0; win_match <= '0; win_max <= '0; c_slots <= '0;
window_valid <= 1'b0;
match_efficiency_pct <= 16'd100;
limited_by_discipline <= 1'b0;
limited_by_scheduler <= 1'b0;
limited_by_upstream <= 1'b0;
none_of_the_above <= 1'b1;
end else begin
window_valid <= 1'b0;
if (slot) begin
win <= win + 1'b1;
c_slots <= c_slots + 1'b1;
win_match <= win_match + CNT_W'(match_size);
win_max <= win_max + CNT_W'(max_possible);
end
if (win >= CNT_W'(WIN)) begin
match_efficiency_pct <= (win_max == '0) ? 16'd100
: 16'((win_match * CNT_W'(100)) / win_max);
// THREE DIAGNOSES, MUTUALLY EXCLUSIVE. A FIFO near 58.6% is the
// discipline. A VOQ with a poor match is the scheduler. Blocking
// that is mostly second-order is somebody else's congestion
// arriving through this switch.
limited_by_discipline <= (discipline == QD_FIFO) &&
(throughput_pct < 16'd65);
limited_by_scheduler <= (discipline == QD_VOQ) &&
(match_efficiency_pct < 16'd90);
limited_by_upstream <= (second_order_pct > 16'd50);
none_of_the_above <= (throughput_pct >= 16'd90) &&
(second_order_pct <= 16'd50);
win <= '0; win_match <= '0; win_max <= '0;
window_valid <= 1'b1;
end
end
end
wire _unused = |pairs_affected | |rounds_configured;
endmoduleClassification: synthesizable.
What it teaches: that a switch below its line rate has three candidate explanations and they are fixed in three different places. The discipline — a FIFO ingress, Section 4's bound, fixed by VOQs. The scheduler — VOQs with too few matching rounds, fixed by more rounds. And upstream congestion — Section 7's second-order blocking, fixed at a different switch entirely.
And the three are mutually exclusive by construction, which matters because a single "the switch is slow" observation supports all three. limited_by_discipline on a switch that has VOQs is impossible; limited_by_scheduler on a FIFO switch is meaningless; and limited_by_upstream overrides both, because a switch whose blocking is mostly second-order is not the device to change.
Deliberately simplified: thresholds as constants. Production telemetry would compare throughput_pct against a computed expectation from the traffic matrix, because 58.6% is the uniform-traffic bound and real traffic is worse — so a FIFO switch at 50% may be at its bound rather than below it.
Production implication: none_of_the_above is the state that ends an investigation correctly. Throughput above 90%, second-order blocking below half — the switch is doing its job and the offered load is simply what it is. Without it, every "slow network" report reopens the same three hypotheses, and the fourth possibility — that nothing is wrong here — has no signal to represent it.
16. What VOQs Do Not Fix
Virtual output queues remove head-of-line blocking inside a switch. Section 6's propagation is between switches, and the two are different problems.
| inside one switch | across a link | |
|---|---|---|
| the blocking unit | an ingress FIFO | a paused link |
| what it holds back | frames for other egress ports | frames for other switches |
| the fix | VOQs — one queue per destination | ? |
| does the fix apply | yes, completely | no |
The right-hand column's question is the one this module ends on, and the answer is not VOQs.
A PAUSE stops a link, and a link is one thing. There is no way to give an ingress port several candidate links to the same neighbour — the link is the resource, and it is stopped or it is not. Chapter 14.2 §6 established that 802.3x has no field for which traffic to stop, and the VOQ trick — offer several candidates — has nothing to be offered.
Which leaves exactly one axis on which the link can be subdivided: the traffic class.
| a link, undivided | a link with 8 classes | |
|---|---|---|
| what a PAUSE stops | everything | one class |
| collateral, 1 of 8 congested | 88% | 0% |
| the field that names it | none exists | Chapter 13.2's PCP |
| the mechanism | 802.3x | Chapter 14.4 |
And that is the argument for the next chapter, arrived at rather than asserted. Chapter 13.2 §15 established that PCP selects when a frame leaves and not where it may go, and Chapter 13.4 §9 built the map from PCP into egress queues. Per-priority flow control uses the same three bits to subdivide the link — turning one stoppable resource into eight — which is exactly the VOQ move applied to a link instead of to an ingress port.
With one new hazard that neither VOQs nor 802.3x has. Eight independently-stoppable classes across a topology can form a cycle in which each class waits on the next, and Chapter 14.4 owns that deadlock and the mechanisms that prevent it.
17. RTL 8 — Conformance for a Composition
The monitor's difficulty is the chapter's thesis: every component-level property holds, so a monitor built from component checks reports success on a system delivering 58.6%.
// -----------------------------------------------------------------------
// hol_conformance_monitor -- checks properties of the COMPOSITION.
//
// Every component check in Modules 12 to 14 passes on a FIFO-ingress
// switch losing 41.4% of its capacity. What this module checks is the
// system-level condition those checks cannot see: an egress port idle
// while an ingress queue holds work for it.
// -----------------------------------------------------------------------
module hol_conformance_monitor
import hol_pkg::*;
#(
parameter int CNT_W = 32
)(
input logic clk,
input logic rst_n,
input queue_discipline_e discipline,
input logic [N_PORTS-1:0] egress_idle,
input logic [N_PORTS-1:0] voq_nonempty [N_PORTS],
input logic [N_PORTS-1:0] grant_egress [N_PORTS],
input logic [7:0] match_size,
input logic [7:0] max_possible,
input logic head_of_line_possible,
output logic [CNT_W-1:0] v_idle_with_work, // THE system check
output logic [CNT_W-1:0] v_double_grant_ing,
output logic [CNT_W-1:0] v_double_grant_egr,
output logic [CNT_W-1:0] v_grant_without_work,
output logic [CNT_W-1:0] v_hol_in_voq, // must be impossible
output logic voq_removes_hol, // a structural claim
output logic conformant
);
// A STRUCTURAL PROPERTY, not a history. With one queue per destination
// there is no position in which a frame can be held behind a frame for
// a different destination -- so head-of-line blocking is impossible by
// construction rather than absent by observation.
assign voq_removes_hol = (discipline == QD_VOQ) && !head_of_line_possible;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
v_idle_with_work <= '0;
v_double_grant_ing <= '0;
v_double_grant_egr <= '0;
v_grant_without_work <= '0;
v_hol_in_voq <= '0;
end else begin
// THE COMPOSITION CHECK. An egress port idle while SOME ingress
// holds a frame for it. On a VOQ switch with a good scheduler this
// must be zero; on a FIFO switch it is the 41.4%, and no
// component-level check reports it.
for (int e = 0; e < N_PORTS; e++) begin
automatic logic wanted = 1'b0;
for (int i = 0; i < N_PORTS; i++)
if (voq_nonempty[i][e]) wanted = 1'b1;
if (egress_idle[e] && wanted)
if (!(&v_idle_with_work))
v_idle_with_work <= v_idle_with_work + 1'b1;
end
// The matching is a matching: no ingress twice, no egress twice.
for (int i = 0; i < N_PORTS; i++)
if ($countones(grant_egress[i]) > 1)
if (!(&v_double_grant_ing))
v_double_grant_ing <= v_double_grant_ing + 1'b1;
begin
automatic logic [N_PORTS-1:0] egr_used = '0;
for (int i = 0; i < N_PORTS; i++) begin
if ((egr_used & grant_egress[i]) != '0)
if (!(&v_double_grant_egr))
v_double_grant_egr <= v_double_grant_egr + 1'b1;
egr_used = egr_used | grant_egress[i];
end
end
// A grant to a queue with nothing in it.
for (int i = 0; i < N_PORTS; i++)
if ((grant_egress[i] & ~voq_nonempty[i]) != '0)
if (!(&v_grant_without_work))
v_grant_without_work <= v_grant_without_work + 1'b1;
// Head-of-line blocking observed on a VOQ switch is a structural
// impossibility, so a non-zero count here means the queues are not
// actually per-destination.
if ((discipline == QD_VOQ) && head_of_line_possible)
if (!(&v_hol_in_voq)) v_hol_in_voq <= v_hol_in_voq + 1'b1;
end
end
assign conformant = (v_double_grant_ing == '0) &&
(v_double_grant_egr == '0) &&
(v_grant_without_work == '0) &&
(v_hol_in_voq == '0) &&
((discipline == QD_FIFO) || voq_removes_hol);
endmoduleClassification: synthesizable, with the per-egress scan pipelined.
What it teaches: that v_idle_with_work is deliberately outside the conformant conjunction, and the reason is the chapter's thesis. On a FIFO switch it is non-zero constantly and the switch is behaving exactly as specified — the discipline is the limit, not a fault. On a VOQ switch with a well-configured scheduler it should be near zero, and a large value means the scheduler is under-provisioned rather than that anything is broken.
So the counter is a capacity measurement rather than a correctness one, and putting it inside a conformance bit would make a correct FIFO switch report non-conformant forever.
And voq_removes_hol is a structural claim rather than an observation. It says head-of-line blocking is impossible by construction — one queue per destination admits no position in which a frame waits behind one for elsewhere — and v_hol_in_voq is what catches a design that claims VOQs and did not build them.
Deliberately simplified: a full N² scan every cycle. A production monitor samples rather than scans, because 576 comparisons per cycle is not free at Chapter 12.1 §12's rates.
Production implication: conformant here means the matching is a valid matching and the structure is what it claims to be. It does not mean the throughput is good — that is match_efficiency_pct — and it does not mean the switch is not limited, because a FIFO switch at 58.6% is fully conformant. Section 18's rejected property is exactly the conflation of those two.
18. Properties Worth Asserting, and One Worth Refusing
Every property here is either about a component or explicitly about the composition, and the rejected one confuses the two.
The FIFO and its blocking
// P1. The queue is WORK-CONSERVING: it presents its head whenever the
// head can be served. This holds, and it is not enough.
property p_fifo_is_work_conserving;
@(posedge clk) disable iff (!rst_n)
(head_valid && egress_available[head_dest]) |-> head_can_go;
endproperty
a_work_conserving: assert property (p_fifo_is_work_conserving);
// P2. A FIFO offers exactly ONE candidate. This is the constraint the
// bound is about, and it is a property of the structure.
property p_fifo_offers_one;
@(posedge clk) disable iff (!rst_n)
head_valid |-> ($countones(candidates) == 1);
endproperty
a_one_candidate: assert property (p_fifo_offers_one);
// P3. A blocked head blocks everything behind it, whatever its
// destination -- the definition of head-of-line blocking.
property p_blocked_head_blocks_all;
@(posedge clk) disable iff (!rst_n)
(head_valid && !head_can_go) |-> !head_taken;
endproperty
a_head_blocks: assert property (p_blocked_head_blocks_all);
// P4. THE MEASUREMENT. Runnable frames behind a blocked head are counted
// -- no other signal in a FIFO design reports the loss.
property p_runnable_counted;
@(posedge clk) disable iff (!rst_n)
(head_valid && !head_can_go && (runnable_behind_head != 8'd0))
|=> (c_runnable_blocked > $past(c_runnable_blocked));
endproperty
a_runnable_counted: assert property (p_runnable_counted);
// P5. Enqueue is refused only when the queue is genuinely full.
property p_enq_refused_only_when_full;
@(posedge clk) disable iff (!rst_n)
(enq_valid && !enq_ready) |-> (count >= 8'(DEPTH));
endproperty
a_enq_full: assert property (p_enq_refused_only_when_full);The bound and its measurement
// P6. THE COMPOSITION CONDITION. An egress idle while some ingress holds
// work for it -- this is the capacity the bound describes, and no
// component-level check reports it.
property p_idle_with_work_detected;
@(posedge clk) disable iff (!rst_n)
(egress_idle[e] && some_ingress_wants[e])
|=> (v_idle_with_work > $past(v_idle_with_work));
endproperty
a_idle_with_work: assert property (p_idle_with_work_detected);
// P7. At the bound and below it are DIFFERENT findings. At it, the
// traffic is uniform and the discipline is the limit; below it, the
// traffic is non-uniform, which is worse and is what real traffic does.
property p_bound_states_exclusive;
@(posedge clk) disable iff (!rst_n)
window_valid |-> !(at_the_bound && below_the_bound);
endproperty
a_bound_exclusive: assert property (p_bound_states_exclusive);
// P8. A FIFO switch near 58.6% is CONFORMANT. The bound is the
// discipline being what it is, not a fault.
property p_fifo_at_bound_is_conformant;
@(posedge clk) disable iff (!rst_n)
((discipline == QD_FIFO) && at_the_bound) |-> conformant;
endproperty
a_bound_is_ok: assert property (p_fifo_at_bound_is_conformant);
// P9. The loss is reported as a percentage of slots, not as a drop
// count -- nothing was dropped.
property p_loss_is_not_drops;
@(posedge clk) disable iff (!rst_n)
(window_valid && (hol_loss_pct != 16'd0)) |-> (c_drops == $past(c_drops));
endproperty
a_loss_not_drops: assert property (p_loss_is_not_drops);Backpressure propagation
// P10. FIRST ORDER AND SECOND ORDER ARE SEPARATED. A head blocked by a
// congested destination is this switch's congestion; one blocked by a
// PAUSED destination is somebody else's, arriving here.
property p_block_reason_classified;
@(posedge clk) disable iff (!rst_n)
head_blocked[p] |-> (block_reason[p] inside {HB_EGRESS_BUSY, HB_EGRESS_XOFF});
endproperty
a_reason_classified: assert property (p_block_reason_classified);
// P11. A paused egress blocks a head exactly as a congested one does --
// the ingress cannot tell them apart, which is why the reason must be
// carried from the egress.
property p_pause_blocks_like_congestion;
@(posedge clk) disable iff (!rst_n)
(head_valid[p] && egress_paused[head_dest[p]] &&
!egress_congested[head_dest[p]])
|-> (head_blocked[p] && (block_reason[p] == HB_EGRESS_XOFF));
endproperty
a_pause_blocks: assert property (p_pause_blocks_like_congestion);
// P12. A full ingress queue pauses the link that feeds it -- one more
// hop of propagation, and the step is not a policy decision.
property p_full_ingress_pauses_upstream;
@(posedge clk) disable iff (!rst_n)
(ingress_occupancy[p] >= 16'(HIGH_CELLS)) |-> send_pause_upstream[p];
endproperty
a_propagates: assert property (p_full_ingress_pauses_upstream);
// P13. propagating is high when this switch is BOTH receiving and
// generating backpressure -- one link of the chain rather than an end.
property p_propagating_means_conduit;
@(posedge clk) disable iff (!rst_n)
propagating |-> ((|send_pause_upstream) && (|egress_paused));
endproperty
a_conduit: assert property (p_propagating_means_conduit);
// P14. second_order_pct says whether this switch is a cause or a
// conduit, and following it upstream reaches the origin.
property p_second_order_reported;
@(posedge clk) disable iff (!rst_n)
(c_blocked_by_pause > c_blocked_by_congestion) |-> (second_order_pct > 16'd50);
endproperty
a_second_order: assert property (p_second_order_reported);Blast radius
// P15. Every blocked pair is recorded exactly once per event.
property p_pair_recorded_once;
@(posedge clk) disable iff (!rst_n)
(event_active && head_blocked[i]) |=> pair_seen[i][$past(head_dest[i])];
endproperty
a_pair_recorded: assert property (p_pair_recorded_once);
// P16. More pairs than this switch has ingress ports means several
// egress ports were unavailable at once -- on a switch that is not
// oversubscribed, that means they were PAUSED.
property p_wide_event_flagged;
@(posedge clk) disable iff (!rst_n)
(pairs_affected > 16'(N_PORTS)) |-> wide_event;
endproperty
a_wide_event: assert property (p_wide_event_flagged);
// P17. Pair-seconds integrate affected conversations over time -- the
// closest single number to "how bad was it" a switch can compute.
property p_pair_seconds_accumulates;
@(posedge clk) disable iff (!rst_n)
(event_active && (|head_blocked)) |=> (c_pair_seconds > $past(c_pair_seconds));
endproperty
a_pair_seconds: assert property (p_pair_seconds_accumulates);Virtual output queues
// P18. THE STRUCTURAL CLAIM. With one queue per destination there is no
// position in which a frame waits behind one for elsewhere.
property p_voq_has_no_hol;
@(posedge clk) disable iff (!rst_n)
(discipline == QD_VOQ) |-> !head_of_line_possible;
endproperty
a_no_hol_in_voq: assert property (p_voq_has_no_hol);
// P19. A frame joins the queue for ITS destination and affects nothing
// else in the bank.
property p_enq_touches_one_queue;
@(posedge clk) disable iff (!rst_n)
enq_valid |=> ((cnt[$past(enq_dest)] == $past(cnt[$past(enq_dest)]) + 8'd1));
endproperty
a_one_queue_touched: assert property (p_enq_touches_one_queue);
// P20. The ingress offers N candidates, not one. This is the whole
// difference from P2.
property p_voq_offers_many;
@(posedge clk) disable iff (!rst_n)
(discipline == QD_VOQ) |-> (nonempty == candidates);
endproperty
a_many_candidates: assert property (p_voq_offers_many);
// P21. The queues are POINTER LISTS -- the frames stay in Chapter 14.1's
// pool and only their organisation changed.
property p_voq_state_is_pointers;
@(posedge clk) disable iff (!rst_n)
(bits_of_state == (CNT_W'(N_PORTS) * CNT_W'(N_PORTS) * CNT_W'(32)));
endproperty
a_pointer_state: assert property (p_voq_state_is_pointers);The scheduler
// P22. THE MATCHING IS A MATCHING. No ingress appears twice.
property p_no_double_grant_ingress;
@(posedge clk) disable iff (!rst_n)
done |-> ($countones(grant_egress[i]) <= 1);
endproperty
a_ingress_once: assert property (p_no_double_grant_ingress);
// P23. No egress appears twice.
property p_no_double_grant_egress;
@(posedge clk) disable iff (!rst_n)
done |-> (v_double_grant_egr == $past(v_double_grant_egr));
endproperty
a_egress_once: assert property (p_no_double_grant_egress);
// P24. A grant goes only to a non-empty queue.
property p_grant_has_work;
@(posedge clk) disable iff (!rst_n)
done |-> ((grant_egress[i] & ~voq_nonempty[i]) == '0);
endproperty
a_grant_has_work: assert property (p_grant_has_work);
// P25. THE ROUND-ROBIN POINTERS ADVANCE. Without them the matching is
// stable and whoever loses the first tie loses every subsequent one --
// Chapter 12.1 Section 9's argument, one level up.
property p_pointers_advance;
@(posedge clk) disable iff (!rst_n)
(grant[e][i] && !ing_matched[i]) |=> (egr_ptr[e] != $past(i));
endproperty
a_pointers_advance: assert property (p_pointers_advance);
// P26. The scheduler completes within its configured rounds -- and the
// rounds must fit Chapter 12.1 Section 12's 28 ns budget.
property p_scheduler_completes;
@(posedge clk) disable iff (!rst_n)
start |-> ##[1:ROUNDS] done;
endproperty
a_scheduler_bounded: assert property (p_scheduler_completes);
// P27. One round produces a MAXIMAL matching, not a maximum one, and the
// difference is 36 percentage points.
property p_one_round_is_maximal_only;
@(posedge clk) disable iff (!rst_n)
((ROUNDS == 1) && done) |-> (match_size <= max_possible);
endproperty
a_maximal_not_maximum: assert property (p_one_round_is_maximal_only);Diagnosis and conformance
// P28. THE THREE DIAGNOSES ARE MUTUALLY EXCLUSIVE. Discipline,
// scheduler and upstream are fixed in three different places.
property p_diagnoses_exclusive;
@(posedge clk) disable iff (!rst_n)
window_valid |-> ($countones({limited_by_discipline, limited_by_scheduler,
limited_by_upstream, none_of_the_above}) <= 1);
endproperty
a_diagnoses_exclusive: assert property (p_diagnoses_exclusive);
// P29. v_idle_with_work is OUTSIDE the conformance conjunction. On a
// FIFO switch it is non-zero constantly and the switch is correct.
property p_idle_with_work_not_a_violation;
@(posedge clk) disable iff (!rst_n)
((discipline == QD_FIFO) && (v_idle_with_work != '0)) |-> conformant;
endproperty
a_capacity_not_correctness: assert property (p_idle_with_work_not_a_violation);
// P30. Conformance means the matching is valid and the structure is what
// it claims -- never that the throughput is good.
property p_conformant_definition;
@(posedge clk) disable iff (!rst_n)
conformant |-> ((v_double_grant_ing == '0) && (v_double_grant_egr == '0) &&
(v_grant_without_work == '0) && (v_hol_in_voq == '0));
endproperty
a_conformant_def: assert property (p_conformant_definition);
// P31. A design claiming VOQs must actually have them -- head-of-line
// blocking observed on a VOQ switch is a structural impossibility.
property p_voq_claim_is_checked;
@(posedge clk) disable iff (!rst_n)
((discipline == QD_VOQ) && head_of_line_possible)
|=> (v_hol_in_voq > $past(v_hol_in_voq));
endproperty
a_voq_claim_checked: assert property (p_voq_claim_is_checked);19. Verification Scenarios
Sixty-four scenarios. Several have expected outcomes in which the switch delivers 58.6% of its capacity and every check passes.
Head-of-line blocking
| # | Scenario | Expected |
|---|---|---|
| 1 | Head destined for a congested port, three behind it for idle ports | all four blocked |
| 2 | Same | the queue is work-conserving throughout |
| 3 | Same | runnable_behind_head = 3 |
| 4 | Same | ports 3 and 11 idle with work available |
| 5 | Head's destination frees | the head goes; the rest follow in order |
| 6 | A FIFO ingress, any traffic | exactly one candidate offered |
| 7 | Uniform random traffic, 24 ports, FIFO | throughput → 58.6% |
| 8 | Same | 41.4% of capacity unreachable |
| 9 | Same | zero frames dropped |
| 10 | Same, with a single hot destination | below 58.6% — the bound is a best case |
| 11 | Deeper ingress queue | no change — the head still blocks |
| 12 | Faster fabric | no change — the constraint is not bandwidth |
| 13 | Better egress scheduler | no change — the work never reaches it |
The bound, measured
| # | Scenario | Expected |
|---|---|---|
| 14 | Throughput between 55% and 62% | at_the_bound — uniform traffic, discipline is the limit |
| 15 | Throughput below 55% | below_the_bound — non-uniform traffic |
| 16 | idle_with_work_pct under load | the theorem, observed |
| 17 | A FIFO switch at 58.6% | conformant high — the discipline is not a fault |
| 18 | hol_loss_pct | non-zero while c_drops is unchanged |
| 19 | Output-queued switch, same traffic | 100% |
Backpressure propagation
| # | Scenario | Expected |
|---|---|---|
| 20 | Head destined for a genuinely congested egress | HB_EGRESS_BUSY — first order |
| 21 | Head destined for a paused egress | HB_EGRESS_XOFF — second order |
| 22 | The ingress's view of the two | indistinguishable — the reason comes from the egress |
| 23 | Ingress queue reaching HIGH | pauses the link that feeds it |
| 24 | This switch receiving and generating pause | propagating — a conduit |
| 25 | second_order_pct near 0 | this switch's own congestion — fix here |
| 26 | second_order_pct near 100 | somebody else's — follow it upstream |
| 27 | Three hops of propagation | up to 12 167 pairs potentially affected |
| 28 | Backpressure enabled | fewer drops, more conversations affected |
| 29 | Same | users report the network is slow, not port 7 is slow |
Blast radius
| # | Scenario | Expected |
|---|---|---|
| 30 | One congested egress, no backpressure | at most 23 pairs — one switch's ingresses |
| 31 | Same, with backpressure at one hop | wide_event — more than N_PORTS pairs |
| 32 | Any blocked pair | recorded once per event |
| 33 | c_pair_seconds | integrates affected conversations over time |
| 34 | second_order_pairs large | this switch inflicted delay for a problem it does not have |
| 35 | Event boundary | bitmap reported, then cleared |
| 36 | Bitmap state, 24 ports | 576 bits — trivial |
Virtual output queues
| # | Scenario | Expected |
|---|---|---|
| 37 | VOQ bank, head destination congested | only that queue is held |
| 38 | Same, frames for idle ports | served immediately |
| 39 | VOQ discipline | head_of_line_possible = 0, structurally |
| 40 | An enqueue | touches exactly one queue |
| 41 | VOQ ingress | offers N candidates |
| 42 | 24-port switch | 576 queues |
| 43 | Pointer state | 2.3 KiB — 0.02% of a 12 MiB pool |
| 44 | The frames themselves | unchanged — still in the pool |
| 45 | A design claiming VOQs with a shared FIFO | v_hol_in_voq |
| 46 | 8 classes as well as 24 destinations | 4608 queues — where pointer state stops being free |
The scheduler
| # | Scenario | Expected |
|---|---|---|
| 47 | One round of request–grant–accept | maximal, ~63% |
| 48 | Five rounds | ~99% |
| 49 | Five rounds at 500 MHz | 10 ns of a 28 ns budget |
| 50 | Three rounds | ~94% — most of the recovery, 6 ns |
| 51 | Eight rounds | ~99.9% — 6 ns more for 0.9 points |
| 52 | Any completed matching | no ingress twice, no egress twice |
| 53 | A grant to an empty queue | v_grant_without_work |
| 54 | Round-robin pointers removed | the matching is stable — a pair starves |
| 55 | match_size against max_possible | the scheduler's efficiency |
| 56 | 576 VOQs with one scheduling round | the storage spent, the bound mostly kept |
Diagnosis and conformance
| # | Scenario | Expected |
|---|---|---|
| 57 | FIFO switch below 65% | limited_by_discipline |
| 58 | VOQ switch, match efficiency under 90% | limited_by_scheduler |
| 59 | second_order_pct above 50 | limited_by_upstream |
| 60 | Throughput above 90%, second order below 50 | none_of_the_above — nothing is wrong |
| 61 | The four diagnoses | mutually exclusive |
| 62 | v_idle_with_work on a FIFO switch | non-zero, and conformant stays high |
| 63 | Every component-level check | passes on a switch delivering 58.6% |
| 64 | Healthy VOQ run, one million slots | conformant high, match_efficiency_pct near 100 |
20. Debugging a Composition
Every row produces a switch whose components are all correct. Several produce one that is limited at a device other than the one being examined.
| Symptom | Likely cause | The observable that decides it |
|---|---|---|
| Throughput ~58%, no drops, no errors | FIFO ingress at Section 4's bound | limited_by_discipline, at_the_bound |
| Throughput below 55%, no drops | the same, with non-uniform traffic | below_the_bound — the bound is a best case |
| VOQs built, throughput ~63% | one scheduling round | match_efficiency_pct, rounds_configured |
| VOQs built, blocking still observed | the queues are not per-destination | v_hol_in_voq |
| Queues full, egress ports idle | head-of-line blocking, by definition | v_idle_with_work, runnable_behind_head |
| Slowness with no local congestion | backpressure from downstream | second_order_pct near 100 |
| Slowness on conversations that avoid the busy port | Section 6's propagation | propagating, wide_event |
| Fewer drops and more complaints after enabling PAUSE | expected — Section 8's inversion | pairs_affected against c_drops |
| One ingress–egress pair permanently starved | round-robin pointers not advancing | Chapter 12.1 §9's argument, one level up |
| Every assertion passes and the switch is slow | the composition, not a component | v_idle_with_work is the only signal |
| A "slow network" report with three plausible causes | they are separable | the three limited_by_* bits are exclusive |
| Nothing is wrong and nobody believes it | none_of_the_above | the state that ends an investigation correctly |
21. Common Misconceptions
1 — "Head-of-line blocking is a congestion problem."
The wrong model: it happens when the switch is overloaded.
What it costs: the 41.4% is never looked for, because the switch is not congested. Section 4's derivation assumes uniform random traffic with no oversubscription — every egress offered exactly its fair share, nothing dropped, no errors — and 41.4% of capacity is still unreachable.
The corrected model: it is a property of the discipline. A FIFO can offer one candidate, so several ingress heads competing for one egress stall queues whose own destinations are free. It gets worse under congestion and it does not need congestion to exist.
2 — "A deeper buffer or a faster fabric will help."
The wrong model: throughput problems are resource problems.
What it costs: money and no improvement. No buffer size, clock speed, fabric width or egress scheduler changes the bound, because the constraint is that a FIFO can offer only its head. The work never reaches the resources being added.
The corrected model: the fix is structural — one queue per destination, so the ingress has N candidates and a scheduler can build a matching. The storage is 2.3 KiB of pointers; what costs is the scheduler's 10 ns of a 28 ns budget.
3 — "Every component is verified, so the system is correct."
The wrong model: verification composes.
What it costs: Section 18's rejected property, and it is the most reasonable-looking error in the track. Every premise is true — each queue work-conserving, each link at line rate, the fabric non-blocking, every gate correct — and the switch delivers 58.6% with no assertion firing.
The corrected model: work-conserving, non-blocking, lossless and fair are all properties that do not survive composition. The system needed the ingress can offer any of its frames, which was never asked of any component because in isolation it is not a meaningful requirement. The composition condition has to be asserted at the system level, where it lives.
4 — "PAUSE prevents loss, so it makes things better."
The wrong model: fewer drops is strictly an improvement.
What it costs: Section 8's inversion. Backpressure reduces the drop count and increases the number of conversations affected by orders of magnitude — from 23 pairs to potentially 12 167 across three hops. A network with flow control enabled reports fewer drops and more complaints, and both facts are correct.
The corrected model: the loss was converted into delay and the delay was distributed to conversations that were not involved. Chapter 12.1 §6 called this a strictly worse pathology than dropping, and the argument survives everything Chapter 14.2 built — because PAUSE working correctly is what produces it.
5 — "Virtual output queues solve the problem."
The wrong model: VOQs remove head-of-line blocking, therefore the problem is gone.
What it costs: the scheduler. VOQs relocate the difficulty rather than removing it — a FIFO ingress had one candidate and needed no decision; a VOQ bank has N and needs a maximum bipartite matching in 28 ns. A design that builds 576 queues and schedules them in one round gets 63%, having spent the storage and kept most of the bound.
The corrected model: VOQs make a full matching possible; the scheduler decides how much of it is found. One round is maximal, five rounds is nearly maximum, and the difference is 36 percentage points for 8 ns.
6 — "VOQs fix backpressure too."
The wrong model: if per-destination queues fix blocking inside a switch, they fix it between switches.
What it costs: an expectation the mechanism cannot meet. A PAUSE stops a link, and a link is one resource — there is no way to offer an ingress several candidate links to the same neighbour. The VOQ trick has nothing to be offered.
The corrected model: the only axis on which a link can be subdivided is the traffic class, using Chapter 13.2's PCP field — which is the VOQ move applied to a link instead of an ingress port, and which is Chapter 14.4's subject. It turns 88% collateral into 0% for one congested class and introduces a deadlock risk neither VOQs nor 802.3x has.
22. Interview Reasoning
Q1 — "An input-queued switch with FIFO ingress queues. What throughput can it achieve?"
Reason through it. 2 − √2 = 58.6%, under uniform random traffic — Karol, Hluchyj and Morgan, 1987. The strong answer states what the derivation assumes and does not assume: no congestion, no oversubscription, no faults — every egress offered exactly its fair share and 41.4% of capacity still unreachable. And it names why nothing conventional helps: the constraint is that a FIFO can offer only its head, so a deeper queue, a faster fabric and a better egress scheduler all change nothing. It also notes the bound is a best case — a single hot destination raises head collisions and pushes throughput below it, which is what real traffic does.
Q2 — "Every queue is work-conserving. Why isn't the switch?"
Reason through it. Because work-conservation is a property of a queue with respect to the work it can offer, which for a FIFO is one frame. The queue is never idle when its head can be served — that holds. The system's throughput depends on a matching between queues and servers, and a structure with one candidate cannot participate in a matching. The strong answer generalises it: the property the system needed — the ingress can offer any of its frames — was never asked of any component, because in isolation a queue's job is to hold frames in order. And it names the pattern: work-conserving, non-blocking, lossless and fair all fail to compose, each because the composition introduces a coupling the component specification had no reason to mention.
Q3 — "How do virtual output queues fix it, and what do they cost?"
Reason through it. One queue per destination, so a blocked destination blocks only its own queue and the ingress can offer N candidates rather than one. The storage is negligible — 576 queues of {head, tail, count} is 2.3 KiB of pointers against a 12 MiB pool, because the frames stay in Chapter 14.1's shared buffer and only their organisation changed. The strong answer names where the cost actually went: the scheduler must now solve a bipartite matching in Chapter 12.1 §12's 28 ns. One round of request–grant–accept gives a maximal matching worth ~63%; five rounds give ~99% for 10 ns — and a design that built the queues and configured one round spent the storage and kept most of the bound.
Q4 — "Backpressure reduced our drops and users complain more. Explain."
Reason through it. Both are correct and they are the same event. PAUSE converts loss into delay — that is what it is for — and distributes the delay to conversations that were not involved. A congested egress with no backpressure blocks at most 23 ingress heads on one switch. With backpressure at one hop it can affect up to 529 ingress–egress pairs, and at two hops up to 12 167 — every one of them a conversation that never touches the congested port. The strong answer names the measurement: pairs_affected and c_pair_seconds rather than a drop count, because what an operator's inbox measures is how much was touched, not how much was lost — and it notes this is exactly what Chapter 12.1 §6 predicted when it called backpressure a strictly worse pathology than dropping.
Q5 — "Your switch is slow. Every assertion passes. Where do you look?"
Reason through it. At the composition, because that is the one thing no assertion covers. The component checks are all true — queues work-conserving, links at line rate, fabric non-blocking, gates correct — and 41.4% can be unreachable with none of them firing. The strong answer names the one observable: an egress port idle while some ingress holds work for it, which is v_idle_with_work, deliberately kept outside the conformance conjunction because on a FIFO switch it is non-zero constantly and the switch is correct. It then separates the three candidate limits: limited_by_discipline (FIFO, fixed by VOQs), limited_by_scheduler (VOQs with too few rounds), and limited_by_upstream (second-order blocking, fixed at a different switch entirely) — mutually exclusive, and each fixed in a different place.
Q6 — "Why don't VOQs fix backpressure between switches?"
Reason through it. Because a PAUSE stops a link, and a link is one resource. The VOQ trick is offer several candidates, and there is nothing to offer — an ingress port cannot have several candidate links to the same neighbour. The strong answer names the only axis available: the traffic class, using Chapter 13.2's three PCP bits to turn one stoppable resource into eight — which is the VOQ move applied to a link rather than an ingress port, and which turns Chapter 14.2 §6's 88% collateral into 0% for one congested class. And it names the new hazard: eight independently-stoppable classes across a topology can form a cycle in which each waits on the next, which is a deadlock neither VOQs nor 802.3x can produce.
23. Understanding Check
24. What's Next
Module 14 has one chapter left, and this chapter has assembled its entire argument.
Chapter 14.1 found where frames are lost. Chapter 14.2 built the mechanism that stops them being lost and measured what it costs the link. And this chapter followed that cost across a topology — 58.6% inside one switch, and a blast radius that compounds hop by hop between them.
Two fixes were found and one gap remains.
Inside a switch, virtual output queues remove the blocking entirely — Section 11 — at the price of a matching problem the scheduler solves in 10 ns.
Between switches, nothing here helps, because a PAUSE stops a link and a link is one resource. Section 16 established that the only axis available is the traffic class.
Chapter 14.4 — Priority Flow Control and Lossless Ethernet takes it. Chapter 13.2's three PCP bits subdivide the link into eight independently-stoppable classes, turning Chapter 14.2 §6's 88% collateral into zero for a single congested class — and making the lossless fabrics that storage and compute interconnects require actually buildable.
With one hazard this module has not had to consider. Eight classes that can each be stopped independently, across a topology in which each switch's classes depend on its neighbours', can form a cycle in which every class is waiting for the next — a deadlock, which neither dropping nor 802.3x can produce, and which is the price of the last of Section 10's fixes.
Continue learning
Related tutorials
- Related topic
Full Duplex and What It Removed from the MAC
Full duplex makes five of the half-duplex MAC's six blocks unreachable and collapses the transmit state machine from five states to two. It also removes a constraint that had been throttling senders by accident, which is why link-level flow control had to be invented to replace it.
- Related topic
What Determinism Requires
Five of a hop's seven latency terms are bounded and sum to 37 microseconds. The other two have no bound at all, and no amount of engineering gives them one.
- Related topic
Time-Aware Shaping (802.1Qbv)
A schedule turns an unbounded interference term into a configuration parameter, and then spends up to 19.5% of the cycle on a guard band to stay safe.
- Related topic
The Shared-Medium Problem
Why several independent transmitters on one medium is a distributed timing problem, not a formatting problem. Propagation delay makes every station's view of the medium stale, so two locally correct decisions can still collide — and that is the constraint the Ethernet MAC was built around.
Standards & specifications
- Governing standard
- IEEE Std 802.3 (Ethernet)(opens IEEE in a new tab)
Defines the Ethernet MAC, the media-independent interfaces and the physical-layer sublayers, including framing, access control, auto-negotiation and per-rate PHY specifications. VLAN tagging, priority and time-sensitive shaping are defined by IEEE 802.1, not by 802.3.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the Ethernet curriculum.
