Skip to content
VLSI Mentor

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 buffersChapter 14.1 found where frames are lost and priced what a buffer buys.

It does not own PAUSEChapter 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 FIFODestinationEgress stateCan it go
headport 7congestedno
secondport 3idleno — blocked
thirdport 11idleno — blocked
fourthport 3idleno — 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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

endmodule

Classification: 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.

ThroughputCapacity lost
output-queued — no ingress FIFO100%0
input-queued, FIFO58.6%41.4%
input-queued, virtual output queues100%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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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;

endmodule

Classification: 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 itthat 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.

An ingress queue holds four frames in first-in-first-out order. The frame at the head is destined for port seven, which is congested, so it cannot be forwarded. Behind it are frames for ports three, eleven and three again, all of which are idle and could be served immediately. Because the queue is a first-in-first-out structure it can offer only its head, so all three runnable frames are blocked and ports three and eleven sit idle with work available for them. The queue is work-conserving, because it is never idle when its head can be served, and the system is not, because the throughput depends on a matching between queues and servers and a first-in-first-out queue has exactly one candidate to offer. Under uniform random traffic this reduces an input queued switch's achievable throughput to two minus the square root of two, which is fifty-eight point six percent, a result derived in 1987 that no buffer size, clock speed or scheduling policy changes.Head → port 7congested — cannot goNext → port 3idle — blockedNext → port 11idle — blockedNext → port 3idle — blockedPorts 3 and 11 idlewith work availableThe queue iswork-conservingnever idle when its headcan goThe system is not58.6% — Karol et al.,198712
Figure 1 — a frame whose destination is busy holds three frames whose destinations are idle; the queue is work-conserving and the system is not.

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:

HopThe head is blocked becauseFrames blocked behind it
0 — the originits egress is genuinely oversubscribeddestinations across this switch
1its egress link is PAUSEDdestinations across a different switch
2its egress link is paused by hop 1destinations 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 propagationIngress–egress pairs potentially affected
123
2529
312 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 correctChapter 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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

endmodule

Classification: 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.

EventFrames droppedPairs affectedWhat users report
a congested egress, no backpressuremany23 — one switch's ingresses"port 7 is slow"
the same, with backpressure, 1 hopfewup to 529"the network is slow"
the same, 2 hopsvery fewup 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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

endmodule

Classification: 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 fixEffect on the 58.6% bound
a deeper ingress queuenone — the head still blocks
a faster fabricnone — the constraint is not bandwidth
a better egress schedulernone — the work never reaches it
more buffer at the egressnoneChapter 14.1's delay, not throughput
PAUSEworse — Section 8's blast radius
one queue per destinationremoves 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:

PortsFIFO queuesVOQ queuesRatio
8864
242457624×
4848230448×

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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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);

endmodule

Classification: 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.

ApproachThroughputCost
FIFO — no matching58.6%none
maximal matching, one iteration~63%one round of request–grant–accept
iterative, log N rounds~99%log₂ 24 ≈ 5 rounds
maximum matching, exact100%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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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);

endmodule

Classification: 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.

A first-in-first-out ingress queue can offer only its head, so it makes exactly one request per slot and a blocked head blocks everything behind it, limiting an input queued switch to fifty-eight point six percent of capacity. Replacing it with one queue per destination gives the ingress port as many candidates as it has non-empty queues, so it requests every destination it holds a frame for. Each egress grants one requester, each ingress accepts one grant, and the round repeats. One round produces a maximal matching worth about sixty-three percent; five rounds, which is roughly the base two logarithm of twenty-four, produce close to a maximum matching worth about ninety-nine percent. The storage cost is small because the queues are pointer lists into the shared buffer pool rather than separate memories, at about two point three kibibytes for five hundred and seventy-six queues, and the real cost moves to the scheduler, which must compute a bipartite matching within the twenty-eight nanosecond per-frame budget.FIFO: one candidate58.6%VOQ: N candidatesone queue per destinationRequest everydestinationthe step a FIFO cannot doGrant, accept,repeat5 rounds = 10 nsOne round: ~63%maximal, not maximumFive rounds: ~99%36 points for 5 cycles2.3 KiB of pointersthe storage is not thecost12
Figure 2 — one queue per destination gives the ingress N candidates instead of one; the 41.4% is recovered by having something else to ask for.

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.

Component24-port switchAgainst what
VOQ pointer state — 24 × 24 × 32 bits2.3 KiBChapter 14.1's 12 MiB pool — 0.02%
the frames themselvesunchangedthey were already in the pool
scheduler request matrix — 24 × 24 bits72 octetstrivial
scheduler comparator logic24 × 24 grant arbitersthe real cost
scheduler time — 5 rounds at 500 MHz10 nsof 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:

RoundsCycles at 500 MHzThroughputMarginal gain
12 ns~63%
24 ns~85%+22
36 ns~94%+9
510 ns~99%+5
816 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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;

endmodule

Classification: 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 switchacross a link
the blocking unitan ingress FIFOa paused link
what it holds backframes for other egress portsframes for other switches
the fixVOQs — one queue per destination?
does the fix applyyes, completelyno

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, undivideda link with 8 classes
what a PAUSE stopseverythingone class
collateral, 1 of 8 congested88%0%
the field that names itnone existsChapter 13.2's PCP
the mechanism802.3xChapter 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%.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// -----------------------------------------------------------------------
// 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);

endmodule

Classification: 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 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_pctand 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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);
A verification plan checks each component and concludes the system is correct. Each ingress queue is work conserving, each link runs at line rate, the fabric is non-blocking for any permutation, every forwarding gate is correct and every conformance monitor reports success. The system nevertheless delivers fifty-eight point six percent of its capacity, because work conservation is a property of a queue with respect to the work it can offer, which for a first-in-first-out structure is one frame, while the system's throughput depends on a matching between queues and servers that a single-candidate structure cannot participate in. The property the system needed, that an ingress can offer any of its frames, was never asked of any component because in isolation it is not a meaningful requirement. The same failure applies to non-blocking, which does not survive two fabrics in series, to lossless, where per-hop flow control moves the backlog rather than removing it, and to fairness, where two fair schedulers in series are not fair.Every queuework-conservingverified — trueFabric non-blockingverified — trueSix gates correctverified — trueTherefore line ratedoes not follow58.6%no assertion firesThe missing propertythe ingress can offer anyframeAlso: non-blocking,lossless, fairnone composes12
Figure 3 — every premise verified, the conclusion false: work-conserving, non-blocking, lossless and fair all break under composition.

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

#ScenarioExpected
1Head destined for a congested port, three behind it for idle portsall four blocked
2Samethe queue is work-conserving throughout
3Samerunnable_behind_head = 3
4Sameports 3 and 11 idle with work available
5Head's destination freesthe head goes; the rest follow in order
6A FIFO ingress, any trafficexactly one candidate offered
7Uniform random traffic, 24 ports, FIFOthroughput → 58.6%
8Same41.4% of capacity unreachable
9Samezero frames dropped
10Same, with a single hot destinationbelow 58.6% — the bound is a best case
11Deeper ingress queueno change — the head still blocks
12Faster fabricno change — the constraint is not bandwidth
13Better egress schedulerno change — the work never reaches it

The bound, measured

#ScenarioExpected
14Throughput between 55% and 62%at_the_bound — uniform traffic, discipline is the limit
15Throughput below 55%below_the_bound — non-uniform traffic
16idle_with_work_pct under loadthe theorem, observed
17A FIFO switch at 58.6%conformant high — the discipline is not a fault
18hol_loss_pctnon-zero while c_drops is unchanged
19Output-queued switch, same traffic100%

Backpressure propagation

#ScenarioExpected
20Head destined for a genuinely congested egressHB_EGRESS_BUSY — first order
21Head destined for a paused egressHB_EGRESS_XOFF — second order
22The ingress's view of the twoindistinguishable — the reason comes from the egress
23Ingress queue reaching HIGHpauses the link that feeds it
24This switch receiving and generating pausepropagating — a conduit
25second_order_pct near 0this switch's own congestion — fix here
26second_order_pct near 100somebody else's — follow it upstream
27Three hops of propagationup to 12 167 pairs potentially affected
28Backpressure enabledfewer drops, more conversations affected
29Sameusers report the network is slow, not port 7 is slow

Blast radius

#ScenarioExpected
30One congested egress, no backpressureat most 23 pairs — one switch's ingresses
31Same, with backpressure at one hopwide_event — more than N_PORTS pairs
32Any blocked pairrecorded once per event
33c_pair_secondsintegrates affected conversations over time
34second_order_pairs largethis switch inflicted delay for a problem it does not have
35Event boundarybitmap reported, then cleared
36Bitmap state, 24 ports576 bits — trivial

Virtual output queues

#ScenarioExpected
37VOQ bank, head destination congestedonly that queue is held
38Same, frames for idle portsserved immediately
39VOQ disciplinehead_of_line_possible = 0, structurally
40An enqueuetouches exactly one queue
41VOQ ingressoffers N candidates
4224-port switch576 queues
43Pointer state2.3 KiB — 0.02% of a 12 MiB pool
44The frames themselvesunchanged — still in the pool
45A design claiming VOQs with a shared FIFOv_hol_in_voq
468 classes as well as 24 destinations4608 queues — where pointer state stops being free

The scheduler

#ScenarioExpected
47One round of request–grant–acceptmaximal, ~63%
48Five rounds~99%
49Five rounds at 500 MHz10 ns of a 28 ns budget
50Three rounds~94% — most of the recovery, 6 ns
51Eight rounds~99.9% — 6 ns more for 0.9 points
52Any completed matchingno ingress twice, no egress twice
53A grant to an empty queuev_grant_without_work
54Round-robin pointers removedthe matching is stable — a pair starves
55match_size against max_possiblethe scheduler's efficiency
56576 VOQs with one scheduling roundthe storage spent, the bound mostly kept

Diagnosis and conformance

#ScenarioExpected
57FIFO switch below 65%limited_by_discipline
58VOQ switch, match efficiency under 90%limited_by_scheduler
59second_order_pct above 50limited_by_upstream
60Throughput above 90%, second order below 50none_of_the_above — nothing is wrong
61The four diagnosesmutually exclusive
62v_idle_with_work on a FIFO switchnon-zero, and conformant stays high
63Every component-level checkpasses on a switch delivering 58.6%
64Healthy VOQ run, one million slotsconformant 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.

SymptomLikely causeThe observable that decides it
Throughput ~58%, no drops, no errorsFIFO ingress at Section 4's boundlimited_by_discipline, at_the_bound
Throughput below 55%, no dropsthe same, with non-uniform trafficbelow_the_bound — the bound is a best case
VOQs built, throughput ~63%one scheduling roundmatch_efficiency_pct, rounds_configured
VOQs built, blocking still observedthe queues are not per-destinationv_hol_in_voq
Queues full, egress ports idlehead-of-line blocking, by definitionv_idle_with_work, runnable_behind_head
Slowness with no local congestionbackpressure from downstreamsecond_order_pct near 100
Slowness on conversations that avoid the busy portSection 6's propagationpropagating, wide_event
Fewer drops and more complaints after enabling PAUSEexpected — Section 8's inversionpairs_affected against c_drops
One ingress–egress pair permanently starvedround-robin pointers not advancingChapter 12.1 §9's argument, one level up
Every assertion passes and the switch is slowthe composition, not a componentv_idle_with_work is the only signal
A "slow network" report with three plausible causesthey are separablethe three limited_by_* bits are exclusive
Nothing is wrong and nobody believes itnone_of_the_abovethe state that ends an investigation correctly
A shared resource always separates the place a shortage is felt from the place it is caused, and this track has met the pattern at three scales. In Chapter 12.1 an egress port's drop counter says that port is oversubscribed, while drops attributed by ingress port name the conversation actually filling it. In Chapter 14.1 a port's drop counter says that port could not allocate a buffer cell, while pool drops attributed to the holder name the port that was holding the shared memory. In this chapter a blocked ingress head looks identical whether its destination is genuinely congested or has merely been paused by its own neighbour, and separating the two says whether this switch is a cause or a conduit. In all three cases the obvious counter reports where the shortage was felt, the useful counter names somebody else, and the cost is one extra attribution recorded alongside an existing count.A shared resourcea wire, a pool, a linkWhere it is feltthe obvious counterWhere it is causedthe useful counterOne extraattributionalongside an existingcount12.1:drops_by_ingresswhich conversation14.1:pool_drops_causedwhich port held thememory14.3:second_order_pctwhich switch12
Figure 4 — three separations at three scales, and in every one the useful counter names a party other than the one reporting.

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 frameswas 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

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.