Skip to content

PCIe · Module 21

Packet Forwarding — Moving a TLP Across a Switch

Routing picks the port; forwarding is everything that must happen afterwards. A switch terminates two independent Links, so a TLP is received, owned, buffered and re-transmitted rather than passed through a wire.

Chapter 21.1 answered "which port does this leave by?" — a lookup, resolved combinationally, in one cycle.

Answering it is not the same as delivering the packet. Between the routing decision and the packet appearing on another Link there may be hundreds of cycles, during which the packet must be stored, arbitrated for, credited and scheduled — and any of those can stall.

The reason it is genuinely hard is that a switch is not a wire. It terminates one Link and originates on another. The two Links have their own flow control, their own acknowledgements, and their own replay histories — so a packet that was safely received is not yet a packet that has been safely sent.

How does a TLP cross that boundary without being lost, duplicated, corrupted, or transmitted illegally?

1. The Verified Sources

2. Routing Decided; Forwarding Delivers

The boundary this chapter starts at.

21.1 Routing21.2 Forwarding
Questionwhich port?how does it get there?
Naturea lookupa pipeline with ownership
Durationcombinationalmany cycles, unbounded
Can fail bywrong port, ambiguity, no matchloss, duplication, corruption, illegal transmission

A switch can route perfectly and still be broken. The routing table can be correct, the decision can be correct, and the packet can still be freed too early (§6), interleaved with another (§9), or transmitted without credit (§8).

§15's first debugging scenario is exactly this: "routing says the correct port, the packet never leaves" — and the answer is never in the routing table.

4. DLLPs Are Not Forwarded

§1 is explicit about what happens to a DLLP: a flow-control DLLP "is written to the Egress Credit Handler"; "Other DLLPs are handled within the DLL logic"; and a NAK causes "the Sequence Number and a NAK indicator" to go "to the TL egress module for re-transmission"on that same Link.

DLLPs are consumed where they arrive. They are Data Link Layer packets, and the Data Link Layer is per-Link.

So the following are all separate:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Endpoint A → Switch      A's TLP acknowledged by the SWITCH's ingress DLL
Switch    → Endpoint B   a DIFFERENT transmission, with its own sequence
                         number, its own credits, and its own ACK from B

And a NAK on either Link causes a replay on that Link only (§5).

What is forwarded is TLPs — Requests, Completions and Messages — at the Transaction Layer. Chapter 21.1 §7's implicitly-routed Messages are TLPs, not DLLPs, which is why they traverse the fabric while an ACK does not.

6. Store-and-Forward Ownership

§1 sources both models, and this chapter's canonical one is store-and-forward — because §1's own caution explains why.

The ownership sequence:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ingress accepts a COMPLETE TLP

an internal packet record owns it        header + payload handle + route (§5)

the egress arbiter grants it             §9

the egress transfer completes            all beats accepted

the internal owner retires               and only now is the buffer freed

On cut-through, bounded as §1 sources it: it forwards after the header is decoded rather than storing the whole TLP, which reduces latency. Its cost is stated in the same source: "the TLP is not known to be good until the last byte"so a packet already partly forwarded cannot be un-sent if it turns out to be bad. This chapter teaches store-and-forward as the canonical ownership model and notes cut-through as the latency-versus-integrity trade it is.

7. RTL — Packet Record and Slot Allocator

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SYNTHESIZABLE. Normalized internal switch packet.
// NOT A TLP WIRE FORMAT. The routing decision is already made
// (Chapter 21.1) and travels WITH the packet from here on.
package switch_fwd_pkg;
 
  parameter int N_PORT = 4;
  parameter int PORT_W = (N_PORT <= 1) ? 1 : $clog2(N_PORT);
  parameter int SLOTS  = 8;
  parameter int SLOT_W = (SLOTS  <= 1) ? 1 : $clog2(SLOTS);
  parameter int LEN_W  = 12;
 
  // Section 1: the ingress credit handler has separate thresholds for
  // Posted, Non-Posted and Completion. The class travels with the packet
  // because egress credit is per-class too.
  typedef enum logic [1:0] { FC_P = 2'd0, FC_NP = 2'd1, FC_CPL = 2'd2 } fc_class_e;
 
  typedef struct packed {
    logic                valid;
    logic [PORT_W-1:0]   ingress_port;
    logic [PORT_W-1:0]   egress_port;    // THE SNAPSHOTTED ROUTE (§5)
    logic                to_upstream;    // 21.1's default direction
    fc_class_e           fc_class;
    logic [2:0]          tc;
    logic [LEN_W-1:0]    payload_bytes;
    logic [SLOT_W-1:0]   payload_handle; // NOT the payload itself
  } switch_pkt_t;
 
endpackage
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import switch_fwd_pkg::*;
 
// SYNTHESIZABLE. Own the payload buffer slots.
// FREED AT RETIREMENT ONLY (§6). Section 14 measured freeing at grant:
// 100.0% of stalled sequences had the payload overwritten underneath a
// packet that was still in flight.
module pkt_slot_alloc #(
  parameter int N = SLOTS,
  parameter int W = (N <= 1) ? 1 : $clog2(N)
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic         alloc_req,
  output logic         alloc_valid,
  output logic [W-1:0] alloc_slot,
 
  // Asserted ONLY when the egress transfer has fully completed.
  input  logic         retire_req,
  input  logic [W-1:0] retire_slot,
 
  output logic [N-1:0] busy_map,
  output logic         no_slots,
  output logic         err_bad_free
);
  generate if (N < 1) $error("SLOTS must be at least 1"); endgenerate
 
  logic [N-1:0] busy_q; logic bad_q;
  assign busy_map     = busy_q;
  assign no_slots     = (&busy_q);
  assign err_bad_free = bad_q;
 
  logic [W-1:0] pick; logic found;
  always_comb begin
    pick='0; found=1'b0;
    for (int i = N-1; i >= 0; i--) if (!busy_q[i]) begin pick=W'(i); found=1'b1; end
  end
  assign alloc_valid = alloc_req && found;
  assign alloc_slot  = pick;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin busy_q <= '0; bad_q <= 1'b0; end
    else begin
      // Retire first, so a slot freed this cycle is immediately reusable --
      // the steady state of a busy switch.
      if (retire_req) begin
        if (retire_slot < W'(N) && busy_q[retire_slot]) busy_q[retire_slot] <= 1'b0;
        else bad_q <= 1'b1;                  // double or out-of-range free
      end
      if (alloc_valid) busy_q[alloc_slot] <= 1'b1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import switch_fwd_pkg::*;
 
// SYNTHESIZABLE. Accept a complete TLP at ingress and take ownership.
// THE ROUTE IS CAPTURED WITH THE PACKET (§5). Section 14 measured
// re-reading the routing table while buffered: the egress port changed
// under stall in 56.7% of cases.
module ingress_owner (
  input  logic clk,
  input  logic rst_n,
 
  input  logic              in_valid,
  output logic              in_ready,
  input  logic [PORT_W-1:0] in_ingress_port,
  input  fc_class_e         in_class,
  input  logic [LEN_W-1:0]  in_bytes,
 
  // From Chapter 21.1's decoder -- consulted ONCE, at acceptance.
  input  logic              route_valid,
  input  logic [PORT_W-1:0] route_port,
  input  logic              route_upstream,
  input  logic              route_ambiguous,
 
  input  logic              slot_valid,
  input  logic [SLOT_W-1:0] slot_id,
  output logic              slot_req,
 
  output switch_pkt_t       pkt,
  input  logic              pkt_taken,        // egress retired it
 
  output logic              err_no_route
);
  switch_pkt_t p_q; logic err_q;
  assign pkt          = p_q;
  assign err_no_route = err_q;
 
  // ==================================================================
  // NO ACCEPTANCE WITHOUT A ROUTE AND A SLOT.
  //
  // Accepting a packet with nowhere to put it, or with no decided
  // destination, means either dropping it later or holding it in a
  // register that the next packet overwrites (§13, mutation 11).
  // ==================================================================
  wire route_ok = (route_valid || route_upstream) && !route_ambiguous;
  assign slot_req = in_valid && !p_q.valid && route_ok;
  assign in_ready = !p_q.valid && route_ok && slot_valid;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin p_q <= '0; err_q <= 1'b0; end
    else begin
      if (in_valid && in_ready) begin
        // SNAPSHOT: header metadata, class, and THE ROUTE, in one
        // assignment. Nothing re-reads the routing table afterwards.
        p_q.valid          <= 1'b1;
        p_q.ingress_port   <= in_ingress_port;
        p_q.egress_port    <= route_port;
        p_q.to_upstream    <= route_upstream;
        p_q.fc_class       <= in_class;
        p_q.payload_bytes  <= in_bytes;
        p_q.payload_handle <= slot_id;
      end else if (p_q.valid && pkt_taken) begin
        p_q.valid <= 1'b0;
      end
      if (in_valid && route_ambiguous) err_q <= 1'b1;   // 21.1 §8
    end
  end
endmodule

Classification: all three synthesizable.

Verified (§14): the allocator matched an independent set model across 600,000 operations at 1, 2, 3 and 8 slots — 0 disagreements.

Failure — five. Freeing at grant (§6's 100.0%). Re-reading the route (56.7%). Accepting without a slot, forcing a later drop. Accepting an ambiguous route (Chapter 21.1 §8). And SLOTS = 1 without the guarded width.

8. Forwarding Needs More Than a Buffer

A packet sitting in a slot with a known egress is still not legal to send.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
forward_allowed = packet_available
               && egress_link_operational        Module 18
               && egress_fc_grant                Module 16, per class

§1's architecture separates these deliberately — an Egress Credit Unit distinct from the ingress one, and per-class thresholds. Credits are per-Link and per-class, so a packet whose class has no credit waits even if the Link is idle for other classes.

And §1's credit analogy is worth carrying: initial allocations are "reservations", the shared storage is a "common credit pool", and "the time the table is occupied is the wait for ACK" — credit is not returned the instant a packet is transmitted, but when the receiving Link layer has released it.

This chapter does not reimplement any of that. §11's egress consumes a normalized grant, exactly as Chapter 20.5 §13 did — and for the same reason (§10 there): a shared decrementing resource must be granted once, not observed by several producers.

9. Contention, Locking and Head-of-Line Blocking

Several ingress ports may target one egress:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
Ingress 0 ─┐
Ingress 1 ─┼──►  Egress 3     only one may transfer at a time
Ingress 2 ─┘

Two rules govern the arbiter, and both are measured.

The grant is held until the transfer completes — the same law as Chapter 20.5 §16's counterexample.

And the packet is locked from first beat to last (§12's RTL). §14 measured an arbiter that re-picks every cycle: 67.0% of two-stream runs produced malformed packets — beats from two different TLPs interleaved into one.

10. The Forwarding Exchange

A TLP crossing a switch. Endpoint A transmits a TLP to the switch ingress port. The switch ingress Data Link Layer acknowledges it to A, and A releases its replay copy. The packet is stored in an internal buffer with its route attached. The egress port waits for flow control credit and arbitration. The switch then transmits the packet to Endpoint B as a separate Link transmission, and Endpoint B acknowledges it to the switch. A replay on either link involves only that link.Endpoint ASwitch ingressportInternal packetbufferSwitch egressportEndpoint BTLP (A's sequencenumber)ACK - the SWITCHreceived itstore with routeattachedarbitration grantwait for egresscreditTLP (SWITCH'ssequence number)ACK - B received itretire - only nowfree the slot
Figure 1 — one TLP crossing a switch. Endpoint A's transmission is received and acknowledged by the switch's own ingress Data Link Layer; A's involvement ends there. The packet is stored with its routing decision attached, waits for egress credit and arbitration, and is then transmitted to Endpoint B as a new Link-layer transmission with its own sequence number and its own acknowledgement. A never waits for B, and a replay on either Link involves only that Link.

Three things to read out of the figure.

A's ACK comes from the switch, not from B — §3, and the second message in the diagram.

The two TLP messages carry different sequence numbers, because they are different Link-layer transmissions (§5).

And the slot is freed at the last arrow, not at the grant (§6).

11. RTL — Egress Request Matrix and Packet-Locked Arbiter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import switch_fwd_pkg::*;
 
// SYNTHESIZABLE. Which ingress wants which egress.
// A MATRIX, NOT ONE GLOBAL ARBITER: independent egress ports can transfer
// concurrently, and a single arbiter would serialize a switch that has no
// reason to be serial.
module egress_request_matrix (
  input  switch_pkt_t [N_PORT-1:0] ingress_pkt,
  output logic [N_PORT-1:0][N_PORT-1:0] request   // [egress][ingress]
);
  always_comb begin
    request = '0;
    for (int i = 0; i < N_PORT; i++)
      if (ingress_pkt[i].valid)
        request[ingress_pkt[i].egress_port][i] = 1'b1;
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
import switch_fwd_pkg::*;
 
// SYNTHESIZABLE. THE FLAGSHIP BLOCK. One egress port, several ingress
// requesters, multi-beat packets.
//
// TWO OWNERSHIP RULES, both measured in section 14:
//   grant held until the transfer completes  -- else the request mutates
//   packet locked SOP through EOP            -- else beats interleave (67.0%)
module egress_arbiter #(
  parameter int N = N_PORT,
  parameter int IW = (N <= 1) ? 1 : $clog2(N)
) (
  input  logic clk,
  input  logic rst_n,
 
  input  logic [N-1:0] request,        // one bit per ingress wanting THIS egress
  input  logic [N-1:0] beat_valid,
  input  logic [N-1:0] beat_sop,
  input  logic [N-1:0] beat_eop,
 
  // Egress legality (section 8) -- a normalized grant, not a credit counter.
  input  logic         link_operational,
  input  logic         fc_grant,
 
  input  logic         out_ready,
  output logic         out_valid,
  output logic [IW-1:0] owner,
  output logic          owner_valid,
  output logic          retire_pulse    // last beat accepted -> free the slot
);
  typedef enum logic { S_IDLE, S_LOCKED } st_e;
  st_e            st_q;
  logic [IW-1:0]  own_q, rr_q;
 
  assign owner       = own_q;
  assign owner_valid = (st_q == S_LOCKED);
 
  // ==================================================================
  // A PACKET IS ONLY OFFERED WHEN IT IS LEGAL TO SEND (section 8).
  // Routing decided WHERE; credit and link state decide WHETHER.
  // ==================================================================
  assign out_valid = (st_q == S_LOCKED) && beat_valid[own_q]
                                        && link_operational && fc_grant;
 
  assign retire_pulse = out_valid && out_ready && beat_eop[own_q];
 
  // Round-robin from the last owner. Fairness is implementation policy;
  // PCIe mandates no particular arbitration here.
  logic [IW-1:0] pick; logic found;
  always_comb begin
    pick='0; found=1'b0;
    for (int k = N-1; k >= 0; k--) begin
      int idx = (int'(rr_q) + k) % N;
      if (request[idx] && beat_sop[idx]) begin pick = IW'(idx); found = 1'b1; end
    end
  end
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin st_q <= S_IDLE; own_q <= '0; rr_q <= '0; end
    else begin
      unique case (st_q)
        S_IDLE :
          // ==========================================================
          // SELECTION HAPPENS ONLY AT A START-OF-PACKET.
          //
          // Choosing mid-packet is how beats from two TLPs end up
          // interleaved into one malformed packet -- section 14 measured
          // 67.0% of two-stream runs.
          // ==========================================================
          if (found) begin own_q <= pick; st_q <= S_LOCKED; end
 
        S_LOCKED :
          // ==========================================================
          // LOCKED UNTIL THE LAST BEAT IS ACCEPTED. Not until the
          // requester deasserts, not on a timer, and NOT re-evaluated
          // when another ingress becomes higher priority.
          // ==========================================================
          if (out_valid && out_ready && beat_eop[own_q]) begin
            st_q <= S_IDLE;
            rr_q <= (own_q == IW'(N-1)) ? '0 : (own_q + IW'(1));
          end
 
        default : st_q <= S_IDLE;
      endcase
    end
  end
endmodule

Classification: both synthesizable.

Verified (§14): across 13,500 runs at every ingress×egress combination from 2×2 to 4×4, no egress ever had two owners and transfers never exceeded offers. And the packet lock produced 0 malformed packets where re-picking every cycle produced 67.0%.

Failure — five. Selecting mid-packet (67.0%). Releasing the lock on anything but an accepted EOP. Re-evaluating priority under stall. One global arbiter serializing independent egresses. And offering a packet without fc_grant (§8).

12. Assertions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SVA over the forwarding blocks. LOCAL contract only. Nothing asserts
// that an egress becomes ready, that credit is granted, or that any packet
// is ever delivered.
 
// ---- SLOT OWNERSHIP ---------------------------------------------------
 
// P1: a slot is allocated only if it was free -- no double allocation.
property p_alloc_was_free;
  @(posedge clk) disable iff (!rst_n) alloc_valid |-> !busy_map[alloc_slot];
endproperty
a_alloc : assert property (p_alloc_was_free);
 
// P2: an invalid or out-of-range free is REPORTED, not applied.
property p_bad_free;
  @(posedge clk) disable iff (!rst_n)
  (retire_req && ((retire_slot >= W'(N)) || !busy_map[retire_slot])) |=> err_bad_free;
endproperty
a_free : assert property (p_bad_free);
 
// P3: OUTSTANDING PACKETS EQUAL LIVE SLOTS -- a drift here is a leak that
// stays invisible until the switch stops accepting.
property p_slot_population;
  @(posedge clk) disable iff (!rst_n)
  $countones(busy_map) == live_packet_count;
endproperty
a_pop : assert property (p_slot_population);
 
// ---- PACKET METADATA --------------------------------------------------
 
// P4: THE ROUTE IS STABLE WHILE THE PACKET IS OWNED. Section 14 measured
// re-reading the routing table: 56.7% changed destination under stall.
property p_route_stable;
  @(posedge clk) disable iff (!rst_n)
  (pkt.valid && !pkt_taken) |=> (pkt.valid && $stable(pkt.egress_port)
                                           && $stable(pkt.to_upstream));
endproperty
a_route : assert property (p_route_stable);
 
// P4b: and so is the payload handle -- metadata and payload must not drift
// apart (mutation 22).
property p_handle_stable;
  @(posedge clk) disable iff (!rst_n)
  (pkt.valid && !pkt_taken) |=> $stable(pkt.payload_handle);
endproperty
a_handle : assert property (p_handle_stable);
 
// P5: a packet is accepted only with a decided route and an owned slot.
property p_accept_needs_route_and_slot;
  @(posedge clk) disable iff (!rst_n)
  (in_valid && in_ready) |-> ((route_valid || route_upstream)
                              && !route_ambiguous && slot_valid);
endproperty
a_accept : assert property (p_accept_needs_route_and_slot);
 
// ---- ARBITRATION ------------------------------------------------------
 
// P6: THE SLOT IS FREED ONLY AT RETIREMENT. Section 14: freeing at grant
// corrupted the payload in 100.0% of stalled sequences.
property p_free_on_retire;
  @(posedge clk) disable iff (!rst_n)
  retire_req |-> (out_valid && out_ready && beat_eop[owner]);
endproperty
a_retire : assert property (p_free_on_retire);
 
// P7: AT MOST ONE INGRESS OWNS AN EGRESS at a time.
property p_single_owner;
  @(posedge clk) disable iff (!rst_n) $onehot0(grant_vec);
endproperty
a_one : assert property (p_single_owner);
 
// P7b: and one packet is never granted to two egresses.
property p_one_destination;
  @(posedge clk) disable iff (!rst_n)
  $onehot0({egress_owns[0][i], egress_owns[1][i],
            egress_owns[2][i], egress_owns[3][i]});
endproperty
a_dest : assert property (p_one_destination);
 
// P8: THE GRANT IS STABLE UNDER STALL.
property p_grant_held;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready) |=> (owner_valid && $stable(owner));
endproperty
a_hold : assert property (p_grant_held);
 
// P9: THE PACKET OWNER IS STABLE FROM SOP TO EOP -- no mid-packet source
// switch. Section 14: 67.0% malformed without this.
property p_packet_locked;
  @(posedge clk) disable iff (!rst_n)
  (owner_valid && !(out_valid && out_ready && beat_eop[owner]))
    |=> (owner_valid && $stable(owner));
endproperty
a_lock : assert property (p_packet_locked);
 
// P9b: selection happens only at a start-of-packet.
property p_select_at_sop;
  @(posedge clk) disable iff (!rst_n)
  $rose(owner_valid) |-> $past(beat_sop[pick]);
endproperty
a_sop : assert property (p_select_at_sop);
 
// ---- LEGALITY ---------------------------------------------------------
 
// P10: NO TRANSFER WITHOUT AN OPERATIONAL EGRESS LINK.
property p_link_required;
  @(posedge clk) disable iff (!rst_n) out_valid |-> link_operational;
endproperty
a_link : assert property (p_link_required);
 
// P11: NO TRANSFER WITHOUT THE EGRESS FLOW-CONTROL GRANT. Routing never
// overrides credit (section 8).
property p_credit_required;
  @(posedge clk) disable iff (!rst_n) out_valid |-> fc_grant;
endproperty
a_credit : assert property (p_credit_required);
 
// P12: a packet is never transmitted twice.
property p_no_duplicate;
  @(posedge clk) disable iff (!rst_n)
  retire_pulse |=> !(out_valid && (owner == $past(owner))
                                && (pkt_id == $past(pkt_id)));
endproperty
a_dup : assert property (p_no_duplicate);
 
// P13: a packet is not retired before its final transfer.
property p_no_early_retire;
  @(posedge clk) disable iff (!rst_n)
  pkt_taken |-> $past(retire_pulse);
endproperty
a_early : assert property (p_no_early_retire);
 
// P14: beat data is stable while the egress stalls.
property p_beat_stable;
  @(posedge clk) disable iff (!rst_n)
  (out_valid && !out_ready) |=> $stable(out_beat);
endproperty
a_beat : assert property (p_beat_stable);
 
// ---- CROSS-LINK INDEPENDENCE ------------------------------------------
 
// P15: NO DLLP IS FORWARDED. Section 1: DLLPs are handled within the DLL
// logic of the port that received them (§4).
property p_no_dllp_forward;
  @(posedge clk) disable iff (!rst_n)
  dut_ingress.dllp_received |-> !dut_egress.tlp_offered_this_cycle;
endproperty
a_dllp : assert property (p_no_dllp_forward);
 
// P16: an ingress ACK does not depend on the egress transfer (§3).
property p_ack_independent;
  @(posedge clk) disable iff (!rst_n)
  dut_ingress.ack_sent |-> !$past(egress_transfer_required);
endproperty
a_ack : assert property (p_ack_independent);
 
// P17: reset clears ownership -- no live slot, no locked owner.
property p_reset;
  @(posedge clk)
  !rst_n |=> ((busy_map == '0) && !owner_valid && !pkt.valid);
endproperty
a_reset : assert property (p_reset);

P6, P8 and P9 are the three ownership properties this chapter turns on, and each has a measured failure rate behind it: 100.0%, the grant-mutation class, and 67.0%.

P4 with P4b keep metadata coherent — the route and the payload handle must not drift apart from the packet or from each other.

P15 and P16 are the cross-Link properties. They are unusual in asserting what does not happen, and they exist because §3's confusion is the most common one in switch debugging.

No liveness. "The egress becomes ready", "credit is granted" and "a packet is eventually delivered" are all environment properties — §9's head-of-line blocking is precisely a case where a packet legitimately waits indefinitely.

13. Verification, Fault Injection, and Model Verification

Executed before publication, and before the mutation table was written.

Slot allocator — 600,000 operations

Across SLOTS = 1, 2, 3, 8 against an independent set model: 0 disagreements, no double allocation, no invalid free accepted.

N×M arbitration — 13,500 runs

Every ingress×egress combination from 2×2 to 4×4 with random requests and stalls:

CheckViolations
two ingress granted to one egress simultaneously0
transfers exceeding offers (duplication)0

Packet lock — 40,000 two-stream runs

Multi-beat packets from two ingress ports into one egress:

ArbiterMalformed (interleaved) packets
packet-locked (§11)0
re-picks every cycle26,783 — 67.0%

Route snapshot — 60,000 stall/update sequences

ImplementationEgress changed under stall
route snapshotted at acceptance0
route re-read from the table33,993 — 56.7%

Buffer release point — 60,000 sequences

Allocate, grant, stall 1–5 cycles, transfer, at two slots:

Release pointSequences with a corrupted payload
packet retirement (§6)0
arbiter grant60,000 — 100.0%

One note on that 100%. The first version of this model reported 0 for the buggy variant — it granted and retired in the same step, so no window existed. That was a measurement failure, not a result, and the corrected model introduces the stall the bug actually needs. The published figure is from the corrected run.

Directed tests

  • One ingress, one egress — baseline; single-beat and long multi-beat packets.
  • Two ingress contending for one egress — verify one owner and fair alternation (P7, P8). Required.
  • Two independent egresses — verify concurrent transfer (the matrix's purpose).
  • Egress stalled 1, 2, 50 cycles — verify grant, owner and beat stability (P8, P9, P14). Required.
  • fc_grant low with a buffered packet — verify no transfer (P11). Required.
  • Egress Link not operational — verify no transfer (P10).
  • Routing table rewritten while a packet is buffered — verify the egress is unchanged (P4). Required.
  • Slot exhaustion — verify ingress backpressures rather than accepting and dropping (P5). Required.
  • Retirement and allocation of the same slot in one cycle (§ same-cycle audit).
  • Head-of-line scenario — a blocked destination ahead of a free one; verify the second waits (§9, and that this is correct behaviour for a single queue).
  • Reset with buffered packets (P17).

The scoreboard assigns each injected TLP a unique test-only packet ID and checks that every accepted packet is forwarded exactly once, to its snapshotted egress — it never reads busy_map, owner or the packet record.

Mutations

#MutationCaught bySymptom
1slot freed when the route is selectedP6payload overwritten before transmission
2slot freed at arbiter grantP6payload corrupted — 100.0% (measured)
3route re-read after bufferingP4packet leaves by the wrong port — 56.7% (measured)
4selected ingress changes under stallP8the request that transfers is not the one offered
5source changes mid-packetP9malformed TLP from two sources — 67.0% (measured)
6lock released on something other than EOPP9truncated packet, then a spurious fragment
7two ingress granted one egressP7interleaved or lost packets
8one packet granted to two egressesP7bpacket duplicated into the fabric
9flow-control grant ignoredP11transmission without credit — a protocol violation
10egress link state ignoredP10TLP offered to a Link not in L0
11packet accepted with no free slotP5accept-then-drop
12invalid free aliases slot 0P2a live packet's buffer released
13non-power-of-two slot width aliasesP1two packets share a slot
14packet count increments on validP3slot population drifts; switch wedges
15packet retired at SOPP13slot reused while the packet is still transmitting
16reset leaves a live slotP17a stale packet forwarded after reset
17ingress ACK withheld pending egress transferP16source stalls on an unrelated Link's congestion (§3)
18a DLLP forwarded as a TLPP15a flow-control update injected into another Link
19egress replay treated as a source retransmissionscoreboardwrong Link blamed; wrong cable replaced (§5)
20fixed priority starves one ingressreview + P8one port never progresses under sustained load
21routing update mutates an in-flight packetP4same as 3, from the software side
22payload handle changes while stalledP4bmetadata and payload describe different packets
23head-of-line blocking described as a PCIe rulereview + §9queue architecture presented as protocol
24egress credit observed by two producers20.5 P5one credit spent twice
25metadata and payload owned by different retirement pointsP4b, P6slot freed while metadata still live

Same-cycle audit

CaseDeclared resolution
packet arrival + slot allocationallocation is a precondition of acceptance (P5); no accept without a slot
retirement + allocation of the same slotretire first, so the slot is immediately reusable (§7)
two ingress requests for one egressone owner, chosen at SOP (P7, P9b)
selected packet + egress stallgrant, owner and beats all hold (P8, P9, P14)
final EOP + a new requestretirement completes; selection happens the following cycle
egress link-down + a selected packetno transfer (P10); the packet stays owned
routing-table update + a buffered packetthe packet's snapshot wins (P4)
reset + a buffered packetreset wins (P17)

14. Debugging

Symptom → routing or forwarding? → which Link? → signal.

The first question is always which of §2's two stages, and the second is which Link — because §3 means an observation on one says little about the other.

Routing says the correct port and the packet never leaves

Routing is not the problem (§2). Read, in order:

CheckMeaning
fc_grant for the packet's classcredit-blocked (§8) — the commonest cause
egress link_operationalthe Link is not in L0 (18.6 §7)
arbiter owneranother ingress holds the egress
head-of-line (§9)a packet ahead of it is blocked

Note the packet's class matters. Per-class credit (§1's INCH thresholds) means a Completion can flow while a Posted write waits, on the same Link.

The packet appears twice downstream

The retirement boundary — mutations 8 or 15.

Either one packet was granted to two egresses (P7b), or it was retired at SOP and the slot reallocated while it was still transmitting (P13).

The distinguishing experiment: tag packets with a test-only ID and check whether the duplicate carries the same ID (one packet, two transfers) or a different one (a slot reused too early, so the second packet inherited the first's metadata).

Corruption only under contention

Three candidates, all §13-measured, and they are distinguished by what is wrong.

Header right, payload from another packet → mutation 2, the slot freed at grant (100.0%).

Beats visibly interleaved → mutation 5, no packet lock (67.0%).

Correct packet, wrong port → mutation 3, the route re-read (56.7%).

All three appear only with concurrent traffic, which is why single-stream testing passes.

The analyzer shows A's TLP acknowledged and B never received it

Not a contradiction (§3), and this is the single most useful entry in this ladder.

A's ACK came from the switch's ingress Data Link Layer — it means the switch received the TLP correctly, and that A may free its replay copy. It says nothing about the egress side.

The packet may still be buffered, waiting for credit or arbitration (§8). Read the switch's internal state, not A's Link.

Expected, and localizing (§5). Each Link has its own replay history.

A rising replay count on Switch↔B describes the Switch↔B channel — signal integrity, connector, or Link margin on that segment. It is not evidence about A, and replacing A's cable is a wasted afternoon.

Failure only after software changes routing windows

In-flight route snapshots — mutation 21, and §13 measured the exposure at 56.7%.

The tell is the timing: steady-state traffic is fine, and only packets buffered across a configuration change go wrong. The distinguishing experiment is to stall an egress and rewrite the routing table during the stall (P4).

15. Common Misconceptions

  • "A switch forwards bits from port to port." It terminates one Link and originates on another (§3).
  • "Routing and forwarding are the same operation." One is a lookup; the other is a pipeline with ownership (§2).
  • "DLLPs pass through a switch." They are consumed by the port that receives them (§1, §4).
  • "An upstream ACK means the endpoint received it." It means the switch did (§3).
  • "An egress replay means the source retransmitted." Replay is Link-local (§5).
  • "The route can be re-evaluated while the packet waits." 56.7% change destination (§13).
  • "The buffer can be freed when the arbiter grants." 100.0% payload corruption (§13).
  • "An arbiter may re-pick while the output is stalled." The grant must be owned (P8).
  • "Beats from different packets can interleave." 67.0% malformed (§13).
  • "Once buffered, flow control no longer applies." Egress credit is required to transmit (§8).
  • "One FIFO per ingress has no performance cost." §1's own worked example shows the blocking (§9).
  • "Store-and-forward means no internal pipelining." It describes the ownership boundary, not the implementation.
  • "Cut-through is strictly better because it is faster." §1's caution: a bad TLP may already be gone (§6).

16. Understanding Check

17. What's Next

Forwarding is where the routing decision becomes a delivered packet, and it is a different problem entirely.

The switch terminates one Link and originates on another (§3), so credits, ACKs and replay are per-Link — which resolves the most persistent confusion in switch debugging (§5).

Three ownership rules carry the chapter, each measured: the route is packet metadata (56.7% without), the buffer is held until retirement (100.0% without), and the packet is locked SOP to EOP (67.0% without).

And routing never overrides credit (§8): a buffered packet with a known destination is still not legal to send.

Chapter 21.3 — Fabric Scaling asks what changes when one switch becomes a tree. The same virtual-bridge rules compose recursively — but bus ranges and address windows must nest correctly across levels, every hop adds latency and another place to stall, and a shared uplink means the downstream Links do not add up.

The idea to carry forward: an arbiter must own its decision until the transfer completes.