Skip to content

UCIe · Module 14

Retry Mechanisms

The Adapter's retry system as a whole — three independent ring pointers rather than two, acknowledgement processing and the retirement advance, why an ACK for one entry must never retire another, the lost-acknowledgement case that defines exactly-once, replay-versus-new-traffic arbitration and the starvation it can create, sizing the retry window against acknowledgement round-trip, replay-full backpressure, attempt counters, and the escalation boundary where retry hands off to recovery.

Chapter 14.2 rebuilt a link whose operating assumptions had become unsafe. It stopped at a boundary it named explicitly: recovery may re-drive transport and may never re-execute semantics. The machinery that makes re-driving transport safe is this chapter.

1. The One-Sentence Model

Retry is not retransmission alone. Retry is retransmission plus retained identity plus controlled retirement — and a sender cannot retry what it has already forgotten.

Three components, and dropping any one produces a different failure. Retransmission without retained identity gives the receiver no way to tell a repeat from a new object, so it executes twice. Retransmission without controlled retirement frees the copy before it is safe, so a later failure has nothing to re-send. And identity plus retirement without retransmission is just bookkeeping.

2. What Chapter 9.4 Established, and What This Chapter Adds

Chapter 9.4 — Streaming Reliability already built the replay machine for one buffer, one sender, one receiver, and built it thoroughly. It established that reliability is memory; the retirement point and why it is later than instinct suggests; the replay buffer and its entry lifecycle; that retry is not allocation, with the occupancy properties that prove it; that retry must preserve ordering; that readiness must include reliability capacity; that duplicates must not reach the consumer; how to compare wrapping identities; retry storms; and how to verify exactly-once.

None of that is repeated here. Where this chapter needs one of those mechanisms it links and moves on.

What this chapter adds is the thing 9.4 could not, because it was scoped to a single buffer with an abstract retry_trigger:

A retry system is not a replay buffer. It is three independent pointers, an acknowledgement path with its own pipeline and its own alignment hazard, an arbitration decision between replay and new traffic, a window sized against the acknowledgement round trip, and an escalation boundary where retry stops and recovery begins.

Topic9.414.3 — this chapter
Pointersallocate and retireplus an independent replay read pointer (§5) — what makes replay concurrent with new traffic
Failure signalan abstract retry_triggeracknowledgement processing as a mechanism: what an ACK identifies, how it advances retirement (§13–§16)
AlignmentCRC aligned to the object it describesthe ACK aligned to the entry it retires (§17) — a different pipeline, a different bug
Window"readiness must include reliability capacity"sizing it against acknowledgement round trip, and the throughput collapse before it is full (§26–§28)
Schedulingnot addressedreplay against new traffic, priority, and replay starvation (§32–§35)
Stormsescalation existsthe hand-off contract to 14.2 (§37)
Exactly-onceverified for a corrupted packetdeveloped for the lost acknowledgement (§20), which is the case that defines it

Read 9.4 for the replay buffer. Read this for the retry system around it.

3. Sourcing

4. Semantic Object, Replay Entry, Attempt

The three-level distinction the entire chapter rests on, and the one that makes exactly-once expressible at all.

LevelWhat it isHow many existLifetime
Semantic objectone transaction or message meaning — a write, a response, a snoopexactly one, foreveruntil the operation completes
Replay entrythe retained transport representation of that objectexactly one per semantic objectallocation to retirement
Attemptone physical transmission of that entryone or moreone transmission

Attempts may be many. Replay entries are one. Semantic objects are one. Every bug in this chapter is a design that has collapsed two of these three levels into one.

Each collapse has a name and a section:

CollapseConsequenceSection
Attempt treated as a new replay entryoccupancy grows with every retry; the buffer fills with copies of itself§8
Attempt treated as a new semantic objectthe operation executes twice§20, §22
Replay entry freed at the attemptnothing left to re-send when the attempt fails§15
Acknowledgement applied to the wrong entrythe wrong entry retires; a needed one is lost§17

And the reason the distinction is hard to hold onto in RTL is that at the wire, all three look identical: the same bytes leave the same port. Only the sender's retained state knows the difference, which is precisely why the sender's state is where the correctness lives.

5. Three Pointers, Not Two

Chapter 9.4 §4 built the buffer with an allocate pointer and a retire pointer. That is sufficient for a design that stops sending new traffic while replaying. A design that does not stop needs a third.

PointerQuestion it answersMoves when
rp_alloc (tail)where does the next new object go?a new semantic object is allocated — never on a retry
rp_retire (head)what is the oldest unretired entry?an acknowledgement proves an entry no longer needs retention
rp_replay (read)which entry is being re-sent right now?during a replay sweep — and it is bounded by the other two

The invariant that ties them together:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
rp_retire  ≤  rp_replay  ≤  rp_alloc        (modulo the ring)

Read it as two claims. rp_replay never falls behind rp_retire, because an entry that has retired must not be re-sent — that is §16's "retired objects cannot replay again". And rp_replay never runs past rp_alloc, because an entry that was never allocated has nothing to send.

Why the third pointer is genuinely necessary rather than convenient. With two pointers, "where am I replaying from" has to be encoded in the retire pointer, which means either retirement is blocked during a replay sweep or the sweep loses its position when an acknowledgement arrives. Both are real designs and both are worse: the first stalls retirement exactly when the buffer is under most pressure, and the second can restart a sweep from the wrong place. A separate read pointer decouples "what is safe to forget" from "what am I currently re-sending", and those are genuinely independent questions.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE three-pointer replay ring. The third pointer is what permits
// replay and retirement to proceed concurrently.
localparam int REPLAY_DEPTH = 32;
localparam int PTR_W  = $clog2(REPLAY_DEPTH);
localparam int CNT_W  = $clog2(REPLAY_DEPTH + 1);    // +1: DEPTH must be held
 
logic [PTR_W-1:0] rp_alloc_q;    // next free slot
logic [PTR_W-1:0] rp_retire_q;   // oldest unretired entry
logic [PTR_W-1:0] rp_replay_q;   // entry currently being re-sent
logic [CNT_W-1:0] rp_count_q;    // occupancy — the authority on full/empty

Architecture. Three pointers and one count. The count is not redundant with the pointers — §31 is why: pointer equality alone cannot distinguish full from empty in a ring, and the count is the cheapest disambiguation that is also directly assertable.

State. All four have replay-window lifetime and all wrap. rp_replay_q additionally has a validity notion — it is only meaningful while a sweep is in progress — which is why §32's FSM owns when it is read.

Cycle behaviour. rp_alloc_q advances on allocation only. rp_retire_q advances on acknowledgement processing only. rp_replay_q advances during a sweep only. Three pointers, three disjoint advance conditions, and no pointer is advanced by more than one event.

Contract. The ordering invariant above is what every consumer assumes. It is assertable directly (§9) and it is the first thing to check when the buffer misbehaves.

Failure. Overloading one pointer for two roles — usually using rp_retire_q as the replay position — which either blocks retirement during a sweep or corrupts the sweep when an acknowledgement lands mid-sweep.

DV. Drive a replay sweep and an acknowledgement in the same cycle and confirm both pointers move independently and correctly. That single directed test is what the third pointer exists to make pass.

6. The Replay Entry

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE replay entry. Consortium material describes an 8-bit sequence
// number in the flit header; its SEMANTICS are not published (Section 3), so it
// is carried here as an opaque identity that the peer echoes back.
//
// SEQ_W = 8 IS AN ILLUSTRATIVE PARAMETER. Do not read it as a UCIe-defined
// sequence width unless verified from the applicable specification.
localparam int SEQ_W = 8;                        // ILLUSTRATIVE
localparam int TRY_W = 4;
 
typedef struct packed {
  logic                  valid;         // this slot holds an unretired object
  logic [SEQ_W-1:0]      seq;           // transport identity
  logic [TRY_W-1:0]      attempt_count; // how many PHYSICAL attempts (Section 36)
  logic                  sent_once;     // has any attempt been made yet?
  transport_obj_t        obj;           // the retained transport representation
} replay_entry_t;
 
replay_entry_t replay_mem [REPLAY_DEPTH];

Architecture. One entry per semantic object, holding everything needed to re-send it without consulting any other structure. That self-sufficiency is the point: a replay path that must reach back into the Protocol Layer to reconstruct an object has not retained it, and the Protocol Layer has by then moved on.

State. Per replay entry, from allocation to retirement. Note what is not here: no semantic transaction identity, no protocol-level state. The replay buffer stores transport, not semantics — it re-sends bytes, it does not re-execute operations, and keeping semantic state out of it is what structurally prevents §4's second collapse.

Cycle behaviour. Written once at allocation. attempt_count and sent_once update per attempt. obj is never rewritten — a replay entry that can be modified after allocation is a replay entry whose retransmission may differ from its original transmission, which defeats duplicate detection at the receiver.

Contract. The entry must contain everything the transmit path needs. If the framing logic requires a field that lives elsewhere, that field must be captured here at allocation, or a replay reconstructed under a different configuration will differ from the original — which is 14.4 §23's configuration-version hazard arriving through the retry path.

Failure. Storing a pointer to the payload rather than the payload. The pointed-to buffer is reused, and the replay transmits whatever now occupies it — a corruption with a clean CRC, because the CRC is recomputed over the wrong data (14.1 §23's storage-corruption class).

DV. Allocate, overwrite every upstream structure the entry might have referenced, then force a replay and check the transmitted bytes match the original exactly.

7. Occupancy Counts Objects, Not Attempts

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE occupancy. Increments on ALLOCATION of a new semantic object.
// It does NOT increment on a physical retransmission — Section 8.
logic [CNT_W-1:0] rp_count_q;
 
wire alloc_fire  = new_object_accepted;                    // a NEW object
wire retire_fire = ack_retires_entry;                      // an ACK freed one
// NOTE: replay_fire is deliberately ABSENT from this block.
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    rp_count_q <= '0;
  end else begin
    unique case ({alloc_fire, retire_fire})
      2'b10  : rp_count_q <= rp_count_q + CNT_W'(1);
      2'b01  : rp_count_q <= rp_count_q - CNT_W'(1);
      default: rp_count_q <= rp_count_q;      // includes 2'b11 — net zero
    endcase
  end
end

Architecture. One counter with exactly two events. The most important line in the block is the comment saying replay_fire is absent — occupancy is a count of retained objects, and a retransmission does not create one.

State. CNT_W = $clog2(REPLAY_DEPTH+1), the +1 for the same reason 13.2 §5 gave: REPLAY_DEPTH itself must be representable, and a width one bit short silently aliases a full buffer to empty.

Cycle behaviour. The 2'b11 arm falls into default — allocate and retire in the same cycle is a net-zero change (§30). Writing it as two separate statements loses one of the two events, which is the most-repeated bug in resource counters across this entire curriculum (13.1 §9, 13.2 §6, 13.5 §16).

Contract. rp_count_q is the authority on full and empty (§31), and it gates admission (§28). Every other structure trusts it, so an occupancy that drifts from reality is not a local bug — it is a bug in the admission decision of the whole reliable path.

Failure. §8.

DV. 9.4 §8's p_retry_preserves_occupancy is exactly the assertion, and it belongs in every regression that has a replay path.

8. Wrong RTL — Retry Allocates a New Entry

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a retry treated as a new object. Occupancy grows with every attempt.
if (retry_trigger) begin
  replay_mem[rp_alloc_q] <= replay_mem[rp_replay_q];   // a second copy
  rp_alloc_q <= rp_alloc_q + PTR_W'(1);
  rp_count_q <= rp_count_q + CNT_W'(1);                // ← the bug
end

The progression, with an error rate that retries each object twice on average:

AttemptCopies of this object in the bufferEffective buffer capacity
1132 objects
2216 objects
33~10 objects
448 objects

Four consequences, in increasing severity.

Effective capacity collapses in proportion to the retry rate. A 32-entry buffer under a workload retrying twice holds sixteen distinct objects. The retry window (§26) is halved, so throughput falls — before any error is reported and with the buffer reporting itself as full and healthy.

Admission stalls on a buffer full of redundancy. §28's backpressure engages because rp_count_q says full. It is full — of duplicates of work already retained.

And then the duplicates are transmitted. The allocate pointer eventually wraps to the copies, and they are sent as though they were new objects with new identities. The receiver has no way to recognise them as repeats, because they were allocated as distinct entries with distinct sequence values. The semantic operation executes twice.

This is §4's first collapse producing §4's second. An attempt was treated as a replay entry, and the replay entry was then treated as a semantic object. One line of RTL walks all the way down the chain.

9. SVA — Replay Does Not Allocate, and the Pointers Stay Ordered

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Chapter 9.4 Section 8 established the occupancy form; these add
// the pointer-ordering claims the third pointer makes necessary.
 
// A replay changes no pointer except the replay pointer itself.
property p_replay_does_not_allocate;
  @(posedge clk) disable iff (!rst_n)
    (replay_fire && !alloc_fire) |=> $stable(rp_alloc_q);
endproperty
a_replay_does_not_allocate: assert property (p_replay_does_not_allocate);
 
property p_replay_preserves_occupancy;
  @(posedge clk) disable iff (!rst_n)
    (replay_fire && !alloc_fire && !retire_fire) |=> $stable(rp_count_q);
endproperty
a_replay_preserves_occupancy: assert property (p_replay_preserves_occupancy);
 
// The ring ordering of Section 5, expressed with modular distance so it is
// correct across a wrap.
function automatic logic [PTR_W-1:0] ring_dist(logic [PTR_W-1:0] a,
                                               logic [PTR_W-1:0] b);
  return b - a;                                    // wraps naturally
endfunction
 
property p_replay_within_window;
  @(posedge clk) disable iff (!rst_n)
    replay_active |-> (ring_dist(rp_retire_q, rp_replay_q)
                       <= ring_dist(rp_retire_q, rp_alloc_q));
endproperty
a_replay_within_window: assert property (p_replay_within_window);
 
// Occupancy and pointers must agree — the check that catches a pointer that
// moved without its counter, or the reverse.
property p_count_matches_pointers;
  @(posedge clk) disable iff (!rst_n)
    (rp_count_q == CNT_W'(ring_dist(rp_retire_q, rp_alloc_q)))
    || (rp_count_q == CNT_W'(REPLAY_DEPTH));       // the full case aliases to 0
endproperty
a_count_matches_pointers: assert property (p_count_matches_pointers);

Architecture. Four properties: replay is pointer-neutral except for its own pointer, replay is occupancy-neutral, the replay position stays inside the live window, and the count agrees with the pointers.

Why the fourth is worth more than it looks. It is the reconciliation between two independent representations of the same fact. A design where the count and the pointers can disagree has two sources of truth, and every downstream decision — admission, full, empty, window size — depends on which one it happened to read. The property forces them to be one.

Why ring_dist uses modular subtraction. Comparing raw pointer magnitudes inverts across a wrap, which is 14.1 §30's lesson and 9.4 §13's. In a replay ring the wrap happens constantly rather than rarely, so the naive form fails quickly — which is fortunate, because the same mistake in a sequence-number comparison fails once every 256 objects and looks like an intermittent channel fault.

Contract. All four are safety properties over the buffer's own state, needing no reference model. They should be permanently enabled.

DV. They require replay sweeps that wrap, and sweeps that coincide with allocations and retirements. Cover the pointer-wrap crosses of §42 explicitly.

10. The Entry Lifecycle

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ALLOCATED ─→ SENT ─→ WAIT_ACK ─┬─→ ACKED ─→ RETIRED

                               └─→ RETRY_PENDING ─→ REPLAYING ─→ WAIT_ACK …

Illustrative state names. UCIe publishes no per-entry state machine (§3).

StateWhat it meansCan the entry be freed?
ALLOCATEDretained, not yet transmittedno
SENTone attempt madeno — transmission is not delivery (§15)
WAIT_ACKawaiting the outcomeno — this is the whole point
RETRY_PENDINGa retry is required, not yet scheduledno
REPLAYINGan attempt is in progressno
ACKEDthe transport contract says retention is no longer neededyes
RETIREDfreed; the slot may be reallocatedalready gone

The right-hand column is the chapter in one table: exactly one state permits freeing, and it is not any of the states about transmission. SENT, REPLAYING and WAIT_ACK are all states in which bytes have left the die and nothing is yet known about their fate.

And the loop back from REPLAYING to WAIT_ACK is where §4's distinction lives. An entry can traverse that loop many times. It is the same entry each time — the same slot, the same identity, the same bytes — and the only thing that changes is attempt_count.

11. Acknowledgement — What Is Verified

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The verified facts: a 2-bit Ack/Nak COMMAND and an 8-bit SEQUENCE NUMBER
// sit in the flit header (Section 3). What the four command values MEAN is
// NOT published, and NO ENCODING IS INVENTED HERE.
//
// This enum is a SYMBOLIC interface. Substituting your revision's actual
// encoding changes this decode and nothing else in the chapter.
typedef enum logic [1:0] {
  ACK_NONE    = 2'b00,   // SYMBOLIC — no acknowledgement in this flit
  ACK_POSITIVE= 2'b01,   // SYMBOLIC — positive acknowledgement
  ACK_NEGATIVE= 2'b10,   // SYMBOLIC — negative / retry indication
  ACK_RSVD    = 2'b11    // SYMBOLIC
} ack_kind_e;
 
// Decode is deliberately isolated in one function so the symbolic boundary
// is a single point of substitution.
function automatic ack_kind_e decode_ack(logic [1:0] hdr_cmd);
  return ack_kind_e'(hdr_cmd);          // SUBSTITUTE your revision's mapping
endfunction
 
wire ack_kind_e  rx_ack_kind = decode_ack(rx_hdr_ack_cmd);
wire [SEQ_W-1:0] rx_ack_seq  = rx_hdr_seq;        // SEQ_W illustrative (Sec 3)

Architecture. A one-function symbolic boundary. Every mechanism downstream is written against ack_kind_e rather than against bit patterns, so a reader with the specification substitutes one function and the rest of the chapter is correct for their revision.

Why this is the honest structure rather than a limitation. Two of the four values are architecturally necessary in any acknowledged transport — something must mean received and something must mean resend. What is not knowable without the specification is the mapping, and inventing one would produce RTL that looks conformant and is not.

Contract. The acknowledgement rides in the header of a flit travelling the other direction. That has a consequence 13.1 §11 already developed for credits and which applies identically here: if acknowledgements are only ever carried on reverse traffic and there is none, acknowledgement latency is unbounded — and §27's window sizing is against that latency, not the physical round trip.

Failure. Building the mechanism around a guessed encoding. When the real encoding differs, the change is not local — a design that pattern-matches raw bits across ten modules has ten places to fix, and will miss one.

DV. Drive every value of the 2-bit field including the reserved one, and confirm the design's behaviour on the reserved value is defined rather than accidental.

12. Cumulative or Selective — a Comparison, Not a Claim

CumulativeSelective
One acknowledgement means"everything up to and including N is safe""N specifically is safe"
Retirement advancefrom the head up to N — many entries at onceone entry, possibly out of order
A lost acknowledgementself-healing — the next one covers itnot self-healing — that entry is never retired
Retirement structurea pointer advancea per-entry status bit and a scan
Buffer occupancystrictly a contiguous windowcan be sparse, with holes
Costcannot retire out of ordermore state; more complex retirement

Two structural consequences worth carrying regardless of which applies.

Cumulative acknowledgement makes lost acknowledgements cheap and makes head-of-line blocking real. If entry 5 is unacknowledged, entries 6 through 20 cannot retire even if they all arrived perfectly — their acknowledgement is implied by 5's, which has not come. The buffer fills behind one entry.

Selective acknowledgement makes the buffer sparse and makes the pointer model insufficient. The three-pointer ring of §5 assumes a contiguous live window; a selective scheme retires holes in the middle, so rp_retire_q can only advance past a contiguous run of acknowledged entries and a scan is needed to find it. The pointers stop being sufficient and per-entry status becomes necessary.

13. Acknowledgement Processing and the Retirement Advance

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE acknowledgement processing, written for a CUMULATIVE scheme
// (Section 12) — the selective case needs per-entry status and a scan.
//
// The retirement advance is a MODULAR distance, never a magnitude comparison
// (Section 25).
wire [SEQ_W-1:0] ack_distance = rx_ack_seq - replay_mem[rp_retire_q].seq;
wire             ack_in_window = (ack_distance < SEQ_W'(rp_count_q));
 
// How many entries this acknowledgement retires.
wire [CNT_W-1:0] retire_n = ack_in_window ? CNT_W'(ack_distance + 1) : '0;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    rp_retire_q <= '0;
  end else if ((rx_ack_kind == ACK_POSITIVE) && ack_in_window && ack_applies) begin
    // Free every entry from the head through the acknowledged one.
    for (int i = 0; i < REPLAY_DEPTH; i++)
      if (i < int'(retire_n))
        replay_mem[(rp_retire_q + PTR_W'(i))].valid <= 1'b0;
    rp_retire_q <= rp_retire_q + PTR_W'(retire_n);
  end
end

Architecture. An acknowledgement identifies a point in the retained sequence; retirement advances the head to it. The whole mechanism is a bounded distance computation plus a pointer move.

State. rp_retire_q and the valid bits, both per replay entry. Note the entries are invalidated and the pointer moves — either alone leaves the two representations disagreeing, which §9's fourth property catches.

Cycle behaviour. ack_in_window is the guard that makes this safe, and it is doing real work: an acknowledgement for a sequence outside the live window is either stale, corrupt, or from a previous link epoch, and applying it would retire entries that have not been acknowledged. Note the comparison uses ack_distance < rp_count_q — a modular distance against the live occupancy, not a magnitude test.

Contract. ack_applies is §17's alignment check and it is not optional. An acknowledgement without a verified binding to the entry it describes is §18's bug.

Failure. Three. Omitting ack_in_window retires arbitrary entries on a corrupt acknowledgement. Using a magnitude comparison breaks at the sequence wrap (§25). And advancing the pointer without clearing valid leaves stale entries that a later replay sweep will re-send.

DV. Acknowledge the head only; acknowledge several at once; acknowledge across a sequence wrap; deliver an acknowledgement outside the window and confirm it is rejected; and deliver one for an already-retired sequence and confirm nothing moves.

14. The Retirement Point

Stated separately because it is the single decision that determines whether the mechanism works.

A replay entry may be freed only when the transport contract proves it will never need retransmission. Not when it was enqueued, not when it was transmitted, not when it physically arrived — but when the acknowledgement condition defined by the contract is satisfied.

Chapter 9.4 §1 established this and is the reference. What 14.3 adds is which event, in a system with a real acknowledgement path, and the answer has an important negative:

Candidate eventDoes it prove retention is unnecessary?Why not
Object enqueued into the transmit pathnoit has not even been sent
tx_fire — the transmit handshake completedno§15 — it left the die; nothing more is known
Physically received at the far dienoit may have arrived corrupt, and the receiver may discard it
CRC passed at the receivernofor the senderthe sender does not know this yet; the knowledge is at the wrong end
The acknowledgement condition is satisfiedyesthis is the only event that carries the receiver's verdict back

The fourth row is the one worth pausing on. The object arrived, its CRC passed, and the receiver has delivered it. It is still not safe to free the entry, because the sender has not been told. Retention is not about whether the object is safe — it is about whether the sender knows the object is safe. Reliability is memory, and memory is only releasable against information you actually have.

15. Wrong RTL — Free on tx_fire

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the entry is freed when the transmit handshake completes.
if (tx_fire)
  replay_mem[tx_idx].valid <= 1'b0;

Why this is the most tempting wrong answer. tx_fire is a clean, local, unambiguous event. It is exactly the right event for freeing a transmit queue slot. It is the wrong event for freeing retained state, and the difference is the entire reason retained state exists.

The failure needs one error to appear. The object is transmitted, the entry is freed, the slot is reallocated to a new object, and then the far side reports the first object as corrupt. There is nothing to re-send. The retry mechanism receives a retry indication for an object it no longer has.

Three things then happen, and none of them is good.

Best case: the design detects that it cannot honour the retry and escalates to recovery (§37). The link recovers, and the semantic object is lost — which is 14.2 §11's silent loss reached through a different door.

Worse: the design replays whatever now occupies the slot. A different object is transmitted under the failed object's identity. The receiver accepts it as the retry of the first object, and a semantic operation is executed with the wrong payload — an association failure with a clean CRC, 14.1 §6's undetectable row.

And the timing makes it a low-rate bug. The window between freeing and reallocation is short, and the error must land inside it. At the specified BER — one bit error per lane every ~15.6 s at 64 GT/s (13.5 §29) — this can survive an enormous amount of testing and then corrupt data in the field. Error injection correlated with tx_fire is the only thing that finds it reliably, which is §41's injection point.

16. SVA — An Unretired Entry Remains Stored

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The retention contract, expressed positively.
property p_unretired_entry_remains_valid;
  @(posedge clk) disable iff (!rst_n)
    (replay_mem[idx].valid && !entry_acked(idx)) |=> replay_mem[idx].valid;
endproperty
 
generate for (genvar i = 0; i < REPLAY_DEPTH; i++) begin : g_retain
  a_unretired_entry_remains_valid:
    assert property (@(posedge clk) disable iff (!rst_n)
      (replay_mem[i].valid && !entry_acked(i)) |=> replay_mem[i].valid);
end endgenerate
 
// The negative form: transmission alone never frees anything.
property p_tx_does_not_retire;
  @(posedge clk) disable iff (!rst_n)
    (tx_fire && !retire_fire) |=> $stable(rp_retire_q);
endproperty
a_tx_does_not_retire: assert property (p_tx_does_not_retire);
 
// And a retired entry can never be replayed — Section 5's lower bound.
property p_retired_never_replays;
  @(posedge clk) disable iff (!rst_n)
    replay_fire |-> replay_mem[rp_replay_q].valid;
endproperty
a_retired_never_replays: assert property (p_retired_never_replays);

Architecture. Three properties covering retention from three angles: an unacknowledged entry stays, transmission does not retire, and a freed entry is never re-sent.

Why the second exists when the first covers it. The first is per-entry and needs entry_acked, a function of the acknowledgement state. The second is a single cheap always-on property over two signals, and it fires on §15's bug immediately with a two-cycle counterexample. Cheap specific properties that name a known bug are worth having alongside general ones.

Why the third matters more in a three-pointer design. With a separate replay pointer, "replay position" and "oldest live entry" are decoupled, so a sweep that is not correctly bounded can walk into retired territory. In a two-pointer design that is structurally impossible; in a three-pointer design it needs an assertion. This is the cost the third pointer's flexibility buys.

DV. All three need a replay path that is active while acknowledgements are arriving. A regression that replays only on an otherwise idle link satisfies them trivially.

17. Acknowledgement Alignment — the ACK for N Must Not Retire N+1

The strongest RTL lesson in the chapter, and structurally the same hazard 14.1 §15 found in the CRC path — appearing here on a completely different pipeline.

The setup. An acknowledgement arrives in a flit header, is decoded, is validated, and is applied to the retirement pointer. If those steps are pipelined and the acknowledgement's identity is not carried alongside its effect, the effect lands on the wrong entry.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the retirement decision and the identity it applies to are computed
// in different pipeline stages with no binding between them.
always_ff @(posedge clk) begin
  ack_valid_q <= rx_ack_valid;                  // stage 1: an ACK exists
  ack_seq_q   <= rx_ack_seq;                    // stage 1: which sequence
  if (ack_valid_q)
    rp_retire_q <= retire_target_q;             // stage 2: computed from a
                                                //  DIFFERENT cycle's sequence
end

The trace. Acknowledgements arrive for sequences 0x10 and 0x14 on consecutive cycles.

CycleACK arrivingack_seq_qretire_target_qRetirement appliedResult
0seq 0x10
1seq 0x140x10(computing for 0x10)
20x14target for 0x10advance to 0x14's targetentries through 0x14 retired on 0x10's authority

Four entries — 0x11 through 0x14 — have been retired by an acknowledgement that only covered up to 0x10.

Three consequences, and the first is silent.

Retention is lost for entries that were never acknowledged. If any of 0x11 to 0x14 later needs retransmission, it is gone — §15's failure, reached through the acknowledgement path instead of the transmit path.

The buffer under-reports occupancy, so admission opens earlier than it should and the retry window is effectively larger than the design can honour. Everything works until an error lands in the over-retired range.

And in the other direction it loses retirement. If the sequences arrive in the other order — a larger one followed by a smaller one — the smaller target is applied to the larger acknowledgement, so entries that were acknowledged are not retired. The buffer fills, admission stalls, and throughput collapses with no error anywhere — §44's "ACK received but occupancy never falls".

18. The Fix — Bind the Acknowledgement to Its Effect

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. The acknowledgement's identity travels WITH its decoded
// effect, and the consumer checks the binding before acting.
typedef struct packed {
  logic              valid;
  ack_kind_e         kind;
  logic [SEQ_W-1:0]  seq;         // WHICH sequence this decision is about
  logic [CNT_W-1:0]  retire_n;    // computed FROM that same seq
  logic [EPOCH_W-1:0] epoch;      // which link epoch (14.2 Section 23)
} ack_decision_t;
 
ack_decision_t ack_dec_q;
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    ack_dec_q.valid <= 1'b0;
  end else begin
    // Everything captured together, in one cycle, from one source.
    ack_dec_q.valid    <= rx_ack_valid;
    ack_dec_q.kind     <= rx_ack_kind;
    ack_dec_q.seq      <= rx_ack_seq;
    ack_dec_q.retire_n <= compute_retire_n(rx_ack_seq);   // SAME cycle's seq
    ack_dec_q.epoch    <= link_epoch_q;
  end
end
 
// The consumer verifies the binding before acting on it.
assign ack_applies = ack_dec_q.valid
                  && (ack_dec_q.epoch == link_epoch_q)   // not from a dead epoch
                  && ack_seq_in_live_window(ack_dec_q.seq);

Architecture. One structure, captured atomically, carrying the decision and the identity it was derived from. The retirement count is computed from the same cycle's sequence value, so the two cannot separate.

State. Per acknowledgement, one pipeline stage deep here and extendable — the structure is what makes it extendable safely, because adding a stage moves the whole bundle.

Cycle behaviour. All fields written in one always_ff from one set of sources. The moment one field is computed in a different stage from another, the binding is broken and the bug is back.

Contract. The epoch field is doing real work and connects directly to 14.2 §23: an acknowledgement generated before a recovery, arriving after it, refers to a link configuration that no longer exists and a sequence space that may have been re-baselined. Applying it retires entries on the authority of a dead epoch. The check costs one comparison.

Failure. Omitting the epoch check specifically — the alignment is correct within an epoch and wrong across one, so the bug only appears in tests that recover while acknowledgements are in flight, which is a directed case (§42).

Any decision that travels through a pipeline separately from the identity it applies to will eventually be applied to the wrong thing. Bundle at the point of computation; check at the point of use.

19. SVA — Retirement Is Only Ever Justified

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The property that catches Section 17. Needs a verification-only record of
// what was actually acknowledged, because the design cannot know it alone.
property p_retire_only_acked;
  @(posedge clk) disable iff (!rst_n)
    retire_fire |-> tb_seq_was_acknowledged(replay_mem[rp_retire_q].seq);
endproperty
a_retire_only_acked: assert property (p_retire_only_acked);
 
// Local companion needing no reference model: retirement is bounded by the
// acknowledgement actually being applied this cycle.
property p_retire_matches_decision;
  @(posedge clk) disable iff (!rst_n)
    retire_fire |-> (ack_applies
                     && (rp_retire_q + PTR_W'(ack_dec_q.retire_n) == rp_retire_next));
endproperty
a_retire_matches_decision: assert property (p_retire_matches_decision);
 
// Retirement never overtakes allocation — the ring's upper bound.
property p_retire_never_passes_alloc;
  @(posedge clk) disable iff (!rst_n)
    retire_fire |-> (rp_count_q >= CNT_W'(ack_dec_q.retire_n));
endproperty
a_retire_never_passes_alloc: assert property (p_retire_never_passes_alloc);

Architecture. One reference-model property stating the real claim, and two local ones that are cheap and always-on.

Why the local companions matter. p_retire_only_acked is the truth but needs the testbench. p_retire_matches_decision needs nothing and catches the misalignment directly, because it asserts the pointer moved by exactly the amount the bound decision specified — which §17's design cannot satisfy, since its move is derived from a different cycle's sequence.

p_retire_never_passes_alloc catches the over-retirement direction, where a corrupt or stale acknowledgement claims more entries than exist. That is the property that turns a would-be buffer corruption into an immediate, localised failure.

DV. Requires acknowledgements arriving back to back with different sequences — a testbench that spaces them out satisfies all three while never exercising the pipeline hazard.

20. The Lost Acknowledgement

The canonical case, and the one that defines what exactly-once means.

The scenario. Object A is transmitted. It arrives intact. Its CRC passes. The receiver delivers it to the semantic consumer — the write is applied, the response is generated, the operation has happened. The receiver sends an acknowledgement. The acknowledgement is lost or corrupted.

What each side now believes:

SenderReceiver
Was A delivered?unknownyes, and acted upon
Should A be re-sent?yes — no acknowledgement arrivedirrelevant; it already has it
Is a second delivery safe?cannot knowno — it would execute twice

The sender is obliged to retry. It has no way to distinguish "A never arrived" from "A arrived and the acknowledgement was lost", and one of those two requires a retry. 14.2 §22's timeout ambiguity, arriving at the transport layer.

So the retry happens, and it must not cause a second semantic delivery.

Exactly-once is not a property of the sender or the receiver alone. It is the conjunction of: the sender retransmits the same object under the same identity, and the receiver recognises that identity as one it has already acted upon.

Three things follow, and the second is the one designs get wrong.

The sender must not change anything on a retry. Same bytes, same identity, same everything (§6). A retry that regenerates the object — recomputing a timestamp, taking a fresh sequence number, re-reading a payload buffer — is a new object as far as the receiver can tell, and the receiver will execute it.

Duplicate suppression is the receiver's obligation, and it cannot be inferred from the payload. Two identical writes to the same address are legitimately two operations. Only the transport identity distinguishes a repeat from a genuine repetition, which is why §22 insists suppression is a history question rather than a content question.

And the correct outcome is invisible. When it works, the retry arrives, is recognised, is not delivered again, and is acknowledged. Nothing anywhere reports an event. The only evidence the mechanism functioned is a coverage bin and a scoreboard counter — which is why §41's scoreboard tracks semantic_deliveries explicitly, and why the bin proving a duplicate was suppressed must be non-zero.

21. The Retry Sequence

A sender allocates object A into its replay buffer and transmits it. In the first case the receiver detects a CRC failure, does not deliver the object, and returns a retry indication; the sender replays A from retained state without allocating a new entry; the receiver now accepts and delivers A once and returns a positive acknowledgement, which retires the entry. In the second case A arrives intact and is delivered to the semantic consumer, but the acknowledgement is lost in transit; the sender, unable to distinguish a lost object from a lost acknowledgement, replays A; the receiver recognises A as an identity it has already acted upon and suppresses the duplicate without a second semantic delivery, returning a fresh acknowledgement that finally retires the entry.Retry, and the lost acknowledgement that defines exactly-onceSenderReplay bufferReceiverConsumerallocate A (once)attempt 1: ACRC fails: notdeliveredretry indicationre-read A, noallocateattempt 2: same Adeliver A (firsttime)ack A — LOSTattempt 3: same Aidentity alreadyacted onack Aretire A
Figure 1 — the two retry paths that matter. Above: an object fails at the receiver, a retry indication returns, the sender replays from retained state, and the acknowledgement finally retires the entry. Below: the object arrived and was delivered, but the acknowledgement was lost — so the sender retries a delivery that already happened, and the receiver must recognise the repeat rather than act on it twice.

Read the figure for one asymmetry. Attempts 1, 2 and 3 are identical on the wire. The receiver's response differs each time — discard, deliver, suppress — and the difference comes entirely from state the receiver holds, not from anything in the object. That is why duplicate suppression cannot be a payload comparison.

22. Duplicate Suppression — Where It Lives

The requirement, stated as precisely as the evidence allows:

The receiver must be able to distinguish a repeated physical attempt at an object it has already acted upon from a new semantic object. That distinction must come from transport identity and history, never from payload content.

Three properties of the requirement.

It is a history question, so it needs retained state at the receiver. 14.1 §27's expected-sequence tracking is the structure — an expectation plus a record of what was last committed — and its critical rule applies here with full force: it must advance only on units that were actually accepted (14.1 §28). A receiver whose history advanced on a discarded unit will reject the legitimate retry as a duplicate, and the link stalls forever.

Content comparison is not a substitute and is actively wrong. Two identical writes to the same address with the same data are two legitimate operations. A receiver that suppressed the second because it looked like the first would drop real work. The identity is the only discriminator.

And suppression is a delivery decision, not a discard. The duplicate must not be delivered a second time — but it must still be acknowledged, or the sender never retires it and retries forever. Suppressing delivery while also suppressing the acknowledgement converts a lost acknowledgement into an infinite retry loop, which is one of the more elegant ways to build a livelock out of two correct-looking behaviours.

23. SVA — Semantic Delivery At Most Once

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// THE most important property in the chapter. Uses a VERIFICATION-ONLY
// semantic identity, because the protocol carries transport identity and the
// mapping from transport identity to semantic object is the testbench's
// knowledge, not a wire.
int unsigned tb_deliveries[int];      // semantic id -> delivery count
 
// Sampled at each semantic delivery.
always @(posedge clk) if (semantic_deliver)
  tb_deliveries[tb_semantic_id_of(delivered_transport_id)]++;
 
property p_semantic_delivery_at_most_once(int unsigned sid);
  @(posedge clk) disable iff (!rst_n)
    (tb_deliveries[sid] <= 1);
endproperty
 
generate for (genvar s = 0; s < MAX_SEMANTIC; s++) begin : g_once
  a_at_most_once: assert property (p_semantic_delivery_at_most_once(s));
end endgenerate
 
// The companion that makes at-most-once meaningful: it must also happen.
// Bounded liveness under the assumption that the channel eventually delivers.
assume property (@(posedge clk) disable iff (!rst_n)
  ##[1:CHANNEL_BOUND] clean_attempt_possible);
 
property p_semantic_delivery_at_least_once(int unsigned sid);
  @(posedge clk) disable iff (!rst_n)
    tb_allocated[sid] |-> ##[1:DELIVERY_BOUND] (tb_deliveries[sid] == 1);
endproperty

Architecture. Two properties which together are exactly-once: at most once is safety, at least once is liveness, and they need different treatment.

Why at-most-once is the one that must never be relaxed. A missed delivery is recoverable — the mechanism retries. A double delivery is not recoverable: a write applied twice cannot be un-applied, a coherence transition applied twice has advanced the protocol on false information (14.2 §21). The two failure directions are not symmetric, and the safety property protects the unrecoverable one.

Why tb_semantic_id_of must be verification-only. The wire carries a transport identity. The mapping from transport identity to semantic object — the knowledge that attempts 1, 2 and 3 in §21 are one object — is the testbench's, because it generated them. Synthesising that mapping to assert against would be building a copy of the scoreboard in RTL, which then shares its bugs.

Contract. MAX_SEMANTIC bounds the generate loop; in practice this is written as a class-based check rather than a generate over every possible identity, and the form above is shown for clarity of the claim.

DV. The at-most-once property is satisfied vacuously by any test that never produces a duplicate. The bin that matters is a suppressed duplicate with tb_deliveries == 1 — the mechanism visibly working (§42).

24. Sequence Identity

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Consortium material describes an 8-bit sequence number in the flit header
// (Section 3). The SEMANTICS — advance rule, wrap rule, window rule — are NOT
// published, so this is an ILLUSTRATIVE transport-history model and SEQ_W is
// an illustrative parameter rather than a UCIe-defined width.
localparam int SEQ_W = 8;                        // ILLUSTRATIVE
 
logic [SEQ_W-1:0] next_seq_q;         // sender: the identity for the NEXT object
 
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n)                 next_seq_q <= '0;
  else if (epoch_reinit)      next_seq_q <= epoch_initial_seq;   // 14.2 Sec 23
  else if (alloc_fire)        next_seq_q <= next_seq_q + SEQ_W'(1);
  // NOTE: NOT advanced on a replay. A replay reuses the entry's stored seq.
end

Architecture. One counter, advanced on allocation only. The NOTE is the mechanism: a retry transmits replay_mem[idx].seq, the identity captured at allocation, never a fresh value. That single fact is what makes the receiver's duplicate detection possible at all.

State. Link-epoch lifetime — the same class 14.2 §23 established for the lane map and permission, and for the same reason: it describes a transport relationship that a re-initialisation re-baselines.

Cycle behaviour. Advances on alloc_fire, which is the semantic object event, not the attempt event. Advancing it on transmission would give every retry a new identity and destroy exactly-once at its source.

Contract. The sequence space bounds the window (§25). And the epoch reinitialisation must be coordinated with the receiver, or the receiver treats fresh sequences as stale ones — which is 14.1 §27's epoch lifetime seen from the sender's side.

Failure. Advancing on replay, which is §4's second collapse in one line.

DV. Replay an object several times and assert its transmitted sequence is identical every time. Then wrap the sequence space and repeat.

25. Modular Comparison, and the Window It Bounds

A finite sequence counter wraps, and comparison across the wrap must be modular. 14.1 §30 and 9.4 §13 both establish the rule; what 14.3 adds is the sizing relationship it forces.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Modular distance — correct across a wrap. NEVER a magnitude comparison.
wire [SEQ_W-1:0] seq_distance = candidate_seq - replay_mem[rp_retire_q].seq;
wire             seq_in_window = (seq_distance < SEQ_W'(rp_count_q));

The constraint this imposes on the design:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
REPLAY_DEPTH  <  2**(SEQ_W-1)

For an illustrative modulo-N sequence scheme, the unambiguous outstanding window is commonly constrained to less than half the sequence space. For example, an 8-bit illustrative sequence number gives a space of 256, so fewer than 128 unresolved identities may be outstanding under that design rule. Do not interpret 8 bits as a UCIe-defined sequence width, and do not interpret the sub-half-space rule as a UCIe requirement, unless both are verified from the applicable specification — Consortium material describes the field's existence and width but publishes no ambiguity rule and no window relationship (§3).

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The relationship, checked at elaboration rather than assumed.
initial assert (REPLAY_DEPTH < (1 << (SEQ_W-1)))
  else $fatal(1, "replay depth too large for the sequence space — comparisons alias");

Two consequences worth stating.

The sequence width and the replay depth are not independent parameters. A team that grows the replay buffer to improve throughput (§27) without checking this creates an aliasing hazard, where an old sequence is mistaken for a new one. The elaboration assertion is the cheapest possible guard and it costs one line.

And the failure it prevents is periodic and low-rate. Aliasing occurs roughly once per traversal of the sequence space — once every 256 objects for the illustrative SEQ_W = 8 — which presents as a rare intermittent fault with no correlation to any physical variable. 14.1 §45's taxonomy lists exactly this signature, and it is one of the hardest to attribute without knowing to look for it.

26. The Retry Window

The retry window is the amount of transport work that can be unresolved at one time. It is a concurrency pool, exactly like credits and outstanding slots, and it obeys the same rate × latency law.

Chapter 13.5 §17's concurrency conjunction has four terms, and this is the third:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
effective_concurrency ≤ min(
    available_remote_credits,     // 13.1
    outstanding_slots,            // Module 12
    replay_entries,               // ← this chapter
    local_queue_capacity          // 13.2
)

What makes the replay term distinctive is the latency it is sized against:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
required_replay_window  ≳  transport_rate × acknowledgement_round_trip

And the acknowledgement round trip is not the data round trip. It includes:

ComponentWhy it is there
forward transitthe object must arrive
receiver processingframing, CRC, sequence checks (14.1 §17) — the checker's latency is in this loop
acknowledgement schedulingthe acknowledgement rides a reverse-direction flit header (§11), so it waits for one
reverse transitit must come back
sender processingdecode, validate, apply (§18)

The third row is the one that surprises people, and it is 13.1 §11's piggyback hazard in a new setting: if acknowledgements ride only on reverse traffic, then in a heavily asymmetric workload the acknowledgement latency is set by the reverse traffic rate rather than by the physical round trip. In the extreme — no reverse traffic at all and no mechanism to send a bare acknowledgement — the latency is unbounded and no replay window is large enough.

27. Sizing, Worked

Illustrative throughout. Transport rate 1 object per cycle; acknowledgement round trip 40 cycles.

REPLAY_DEPTHSustainable rateUtilisation of a 1-per-cycle path
88/40 = 0.2020%
160.4040%
320.8080%
401.00100% — exactly rate × latency
641.00100% — the surplus is headroom

Three readings.

At depth 8 the link runs at a fifth of its capability with no error, no stall reported by any queue, and every assertion passing. The signature is 13.5 §39's: credits abundant, tracking table free, downstream ready, link idle — the replay-space stall counter is the only instrument that identifies it.

Depth 40 is a floor, not a target. It suffices for a perfectly smooth sender, a perfectly regular acknowledgement latency and no errors. Every retry extends an entry's residency, so the effective requirement rises with the error rate — which is why the surplus at depth 64 is headroom rather than waste.

And the depth is bounded above by §25's design rule. For the illustrative SEQ_W = 8, depth would stay below 128. So under a modulo-N scheme the sequence width can become the binding constraint on throughput, which is a genuinely surprising coupling: a small header field limits how much work can be in flight, and therefore the achievable rate at a given acknowledgement latency. The coupling is the lesson; the specific numbers are illustrative (§25).

28. Replay-Full Backpressure

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE admission gate for reliable traffic. Replay space is a
// CORRECTNESS term, not a performance one — accepting without it means
// transmitting something that cannot be retained.
assign can_allocate = (rp_count_q < CNT_W'(REPLAY_DEPTH));
 
assign reliable_admit = can_allocate          // ← this chapter
                     && credit_available      // 13.1
                     && os_slots_free         // Module 12
                     && queue_space           // 13.2
                     && !recovery_pending;    // 14.2 Section 10

Architecture. One more named term in the conjunction that already exists. Named, not folded in13.5 §35's attribution depends on each term being separately observable.

The cross-resource lesson, stated plainly:

A full replay buffer must stop admission of reliable traffic even when the PHY is ready, the remote has credits, the queue has room, and a tracking slot is free. Reliability capacity is not one resource among four — it is the one whose absence makes transmission incorrect rather than merely premature.

Why it is a correctness term. Credits protect the receiver's storage; a credit shortage means "there is nowhere to put it". Replay space protects the sender's ability to honour its own contract; a replay shortage means "I can send it, and if it fails I cannot fix it." The link would be operating as an unreliable link while claiming to be reliable.

Failure. §29.

DV. Fill the replay buffer while leaving every other resource abundant, and confirm admission stops. Then confirm the stall reason reported is replay and not something else — a design that stalls for the right reason and reports the wrong one wastes a debug session.

29. Wrong RTL — Credits Checked, Replay Space Not

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — admission checks the remote's capacity and not the local ability
// to retain what is sent.
assign reliable_admit = credit_available && queue_space;   // no replay term

What happens. The object is admitted, transmitted, and not retained — there was no slot. Three possible implementations, all bad:

If the design…Consequence
silently skips the allocationthe object is unprotected. A retry indication for it cannot be honoured
overwrites the oldest entryan unretired, unacknowledged object is destroyed — §15's failure, on a different entry
asserts and stopscorrect behaviour for a bug, but it is a run-time failure of something that should have been prevented

And the first is the common one, because it is what happens by omission. No code says "skip the allocation" — the allocation simply writes to a slot that a wrap has made ambiguous, or the count saturates and the pointer keeps moving.

The signature is highly diagnostic once known: the link operates normally, error rates are normal, and specific objects are unrecoverable when they fail. Because it only manifests when an error lands on an unretained object, the failure rate is the product of two low probabilities — buffer-full occupancy fraction × error rate — which can be extremely small and still catastrophic in the field.

A reliable link is only reliable while it can retain what it has sent. The moment it transmits something it cannot re-send, it is an unreliable link that has not noticed.

30. Allocate and Retire in the Same Cycle

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The simultaneous case, done correctly. Handled in Section 7's count; here
// is the POINTER behaviour, which is the part usually missed.
always_ff @(posedge clk or negedge rst_n) begin
  if (!rst_n) begin
    rp_alloc_q  <= '0;
    rp_retire_q <= '0;
  end else begin
    // The two pointers are independent. BOTH may move on the same cycle,
    // and each is gated ONLY by its own event.
    if (alloc_fire)  rp_alloc_q  <= rp_alloc_q  + PTR_W'(1);
    if (retire_fire) rp_retire_q <= rp_retire_q + PTR_W'(ack_dec_q.retire_n);
  end
end

Architecture. Two independent pointers with two independent conditions — and that independence is exactly what makes the simultaneous case correct without special handling. The count (§7) is where the simultaneity needs explicit treatment, because it is one register affected by both events.

Cycle behaviour. On a same-cycle allocate and retire, both pointers advance and rp_count_q is unchanged. The buffer has a different object set and the same occupancy — which is correct and is exactly what a steady-state full-rate link does every cycle.

Contract. The full check (§28) reads rp_count_q before the update, so a same-cycle retire does not open an admission slot in the same cycle it frees one. That is a deliberate one-cycle conservatism: allowing it would require the admission decision to depend on the acknowledgement decode, lengthening a path that is already on the critical loop. A cycle of latency is the right price.

Failure. Gating one pointer on the other's event — a design that only retires when not allocating stalls retirement at full rate, and a design that only allocates when not retiring halves the throughput. Both are written by someone worried about a conflict that does not exist, because the two pointers touch different registers.

DV. A directed test that sustains simultaneous allocation and retirement for many consecutive cycles, checking that occupancy is stable and both pointers advance every cycle.

31. Pointer Wrap, Full and Empty

A ring where rp_alloc == rp_retire is either completely full or completely empty, and the pointers alone cannot distinguish them. Three standard resolutions:

StrategyCostVerdict here
An occupancy countone counter of $clog2(DEPTH+1) bitsused here — directly assertable and already needed for §28
An extra pointer bit (phase)one bit per pointerworks; the comparison is less readable
Leave one slot emptyone wasted entrywastes an entry of a resource that directly sets throughput (§27)

The count is chosen because it is needed anyway. The admission gate reads occupancy, the window sizing reasons about occupancy, and the stall attribution reports occupancy. A design that has a count and also uses pointer equality for full/empty has two sources of truth — §9's fourth property exists precisely to force them to agree.

Why replay rings wrap far more often than most. The buffer is small and every object passes through it, so a 32-entry ring wraps every 32 objects. At full rate that is every 32 cycles, which means wrap-related bugs surface quickly rather than rarely — a genuine advantage over the sequence-space wrap of §25, which happens once every 256 objects and hides.

The three wraps that must be tested separately: the allocate pointer wrapping, the retire pointer wrapping, and — the one usually missed — the replay pointer wrapping mid-sweep, which only occurs when a sweep starts near the top of the ring and continues past it.

32. The Replay FSM

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE replay controller. Compact deliberately — it sequences the
// sweep; the buffer and the acknowledgement path own their own state.
typedef enum logic [2:0] {
  RP_IDLE     = 3'd0,   // no replay outstanding; new traffic may flow
  RP_SEND_NEW = 3'd1,   // transmitting newly allocated objects
  RP_WAIT     = 3'd2,   // awaiting acknowledgement outcomes
  RP_REPLAY   = 3'd3,   // a sweep is in progress
  RP_DRAIN    = 3'd4,   // sweep complete; awaiting its outcomes
  RP_ESCALATE = 3'd5    // attempts exhausted — hand off to 14.2
} replay_state_t;
 
replay_state_t rp_state_q, rp_state_d;
 
always_comb begin
  rp_state_d = rp_state_q;
  unique case (rp_state_q)
    RP_IDLE     : if (retry_required)      rp_state_d = RP_REPLAY;
                  else if (alloc_pending)  rp_state_d = RP_SEND_NEW;
    RP_SEND_NEW : if (retry_required)      rp_state_d = RP_REPLAY;   // replay wins
                  else if (!alloc_pending) rp_state_d = RP_WAIT;
    RP_WAIT     : if (retry_required)      rp_state_d = RP_REPLAY;
                  else if (alloc_pending)  rp_state_d = RP_SEND_NEW;
                  else if (rp_count_q=='0) rp_state_d = RP_IDLE;
    RP_REPLAY   : if (attempts_exhausted)  rp_state_d = RP_ESCALATE;
                  else if (sweep_done)     rp_state_d = RP_DRAIN;
    RP_DRAIN    : if (retry_required)      rp_state_d = RP_REPLAY;
                  else if (sweep_acked)    rp_state_d = RP_WAIT;
    RP_ESCALATE : rp_state_d = RP_ESCALATE;      // 14.2 takes over
    default     : rp_state_d = RP_ESCALATE;      // fail loudly
  endcase
end
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) rp_state_q <= RP_IDLE;
  else        rp_state_q <= rp_state_d;

Architecture. Six states whose structure encodes one policy decision: retry_required transitions to RP_REPLAY from every non-terminal state, so a replay pre-empts new transmission wherever the machine currently is. §33 is the argument.

State. Replay-episode lifetime. RP_IDLE is the resting value and is reachable only when the buffer is empty — note the rp_count_q == '0 guard, which means the machine does not return to idle merely because nothing is pending; it returns when nothing is unresolved.

Cycle behaviour. unique case with a default to RP_ESCALATE — the same reasoning 14.2 §9 gave: a reliability mechanism reaching an illegal encoding must not silently declare everything fine.

Contract. RP_ESCALATE is the hand-off to 14.2, and it is terminal here by design — the retry mechanism has exhausted what it can do and the decision now belongs to the recovery controller. §37 develops the contract.

Failure. Making RP_SEND_NEW un-preemptible, which is §34.

DV. Cover every arc into RP_REPLAY, including from RP_SEND_NEW mid-transmission and from RP_DRAIN — a second failure during the drain of a first sweep, which is the arc random stimulus is least likely to reach.

33. Replay Against New Traffic

When a replay is pending and new objects are also ready, something must choose. Three policies:

PolicyEffect on the failed objectEffect on new trafficRisk
Replay first, strictlyresolved as fast as possibledelayednew traffic starves if replays are continuous
New traffic firstdelayed indefinitely under loadmaximal§34 — the failed object may never be retried
Weighted / interleavedbounded delaybounded delaymore state; both bounds must be sized

The argument for replay priority is not fairness — it is progress.

Head-of-line dependency. With cumulative acknowledgement (§12), an unacknowledged entry blocks the retirement of everything behind it. So delaying a replay delays the retirement of every younger entry, which fills the buffer, which stalls admission (§28), which stops the new traffic that was being prioritised. Prioritising new traffic over replay is self-defeating under load.

Ordering. If the protocol requires the failed object to be delivered before younger ones (12.2), then younger objects transmitted ahead of it either violate ordering or must be held at the receiver — and holding them is receiver buffering the sender's scheduling decision has just consumed.

And the window shrinks while the replay waits. Every cycle the failed object goes un-retried is a cycle its entry — and every entry behind it — stays occupied. The retry window (§26) is effectively reduced by the scheduling delay, which reduces throughput.

34. Wrong Policy — New Traffic Starves the Replay

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — new traffic is unconditionally preferred, so a pending replay is
// only scheduled when the sender happens to have nothing new.
assign tx_select = alloc_pending ? TX_NEW : TX_REPLAY;

The failure under sustained load. The sender always has new objects. alloc_pending is therefore always true. TX_REPLAY is never selected.

CycleNew pending?SelectedReplay pending?Failed object retried?
0yesNEWyesno
1yesNEWyesno
yesNEWyesno
10000yesNEWyesno

And then it stops by itself, which is the interesting part. The failed entry never retires. Neither does anything behind it, under cumulative acknowledgement. Occupancy climbs to REPLAY_DEPTH, can_allocate goes false (§28), and alloc_pending finally goes false — because nothing new can be admitted. Only then is the replay scheduled.

So the design does not deadlock; it converts a one-object retry into a full-buffer stall. The failed object is retried after REPLAY_DEPTH more allocations have been blocked, and the throughput cost is the entire buffer's worth of stalled admission rather than one retransmission.

Three properties of this bug.

It is a liveness bug that self-resolves, which makes it hard to classify. Nothing is stuck forever, so a deadlock detector finds nothing. The bounded-fairness property of §35 catches it because the bound is enormously exceeded, not because progress never happens.

Its severity scales with buffer depth, perversely: a deeper replay buffer — which §27 says improves throughput — makes this bug worse, because more allocations must be blocked before the replay is scheduled.

And it is invisible at low load. With gaps in the traffic, alloc_pending goes false regularly and replays are scheduled promptly. The bug requires sustained full-rate traffic to appear, which is the opposite of the low-load condition that reveals 13.5 §20's batching bug — between them they argue for a regression matrix that spans both extremes.

35. SVA — a Pending Replay Is Eventually Scheduled

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// LIVENESS, with the assumption stated.
//
//   A1: the transmit path makes progress (something is sent when able)
//
// The bound is what makes it checkable, and choosing it is a design statement:
// it is the maximum acceptable delay between a retry becoming required and
// the retransmission starting.
localparam int REPLAY_SCHED_BOUND = 16;   // ILLUSTRATIVE
 
assume property (@(posedge clk) disable iff (!rst_n)
  tx_able |-> ##[0:1] tx_fire);
 
property p_pending_replay_is_scheduled;
  @(posedge clk) disable iff (!rst_n)
    (retry_required && !attempts_exhausted)
      |-> ##[1:REPLAY_SCHED_BOUND] replay_fire;
endproperty
a_pending_replay_is_scheduled: assert property (p_pending_replay_is_scheduled);
 
// Safety companion, needing no assumption: a replay in progress must make
// monotone progress through the window rather than restarting.
property p_replay_sweep_progresses;
  @(posedge clk) disable iff (!rst_n)
    (rp_state_q == RP_REPLAY) && replay_fire
      |=> (rp_replay_q != $past(rp_replay_q));
endproperty
a_replay_sweep_progresses: assert property (p_replay_sweep_progresses);

Architecture. A bounded liveness property with its environment assumption, plus a safety companion that needs none.

Why the bound is a design statement rather than a tuning parameter. It says how long the design promises to wait before retransmitting. That promise propagates: it bounds the retry latency, which bounds the acknowledgement round trip, which sizes the retry window (§26). A design that has never chosen this number has an unbounded term in its own sizing equation.

Why the safety companion is worth having. p_replay_sweep_progresses catches a sweep that restarts from the beginning on each acknowledgement — a real bug in designs that overload the retire pointer as the replay position (§5), and one the liveness property misses because replays are firing, just always on the same entry.

DV. The liveness property needs sustained new traffic concurrent with a pending replay — precisely §34's condition. A regression whose replays occur on an otherwise idle link satisfies it trivially and proves nothing about the scheduler.

36. Attempt Counters

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Two counters, two lifetimes, two questions.
// NO UCIe retry limit is published (Section 3); MAX_ATTEMPTS is a design choice.
localparam int MAX_ATTEMPTS = 8;         // ILLUSTRATIVE
 
// Per entry: how many times has THIS object been attempted?
always_ff @(posedge clk)
  if (replay_fire && !(&replay_mem[rp_replay_q].attempt_count))
    replay_mem[rp_replay_q].attempt_count <= replay_mem[rp_replay_q].attempt_count + TRY_W'(1);
 
assign attempts_exhausted =
  (replay_mem[rp_replay_q].attempt_count >= TRY_W'(MAX_ATTEMPTS));
 
// Lifetime: how many retransmissions has this link performed, ever?
logic [31:0] retry_total_q;
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n)                                    retry_total_q <= '0;
  else if (replay_fire && !(&retry_total_q))     retry_total_q <= retry_total_q + 32'd1;
 
initial assert (MAX_ATTEMPTS < (1 << TRY_W))
  else $fatal(1, "MAX_ATTEMPTS unreachable for TRY_W — escalation is dead code");

Architecture. Two counters answering "should this object give up?" and "how healthy is this link?" — and they must be separate, because the first resets and the second must not.

State. attempt_count is per replay entry, cleared when the slot is reallocated. retry_total_q is sticky for the life of the part. Both saturate: 14.1 §33's rule — saturate diagnostics, never saturate accounting. A wrapping attempt counter would clear attempts_exhausted mid-episode and retry forever, defeating the escalation it exists to trigger.

Cycle behaviour. attempt_count increments per physical attempt — the one place in the chapter where counting attempts rather than objects is correct, and worth noting precisely because everything else counts objects.

Contract. attempts_exhausted drives RP_ESCALATE (§32), and the elaboration assertion guarantees the threshold is reachable. An unreachable threshold makes escalation dead code and the design retries indefinitely on a permanently broken channel.

Failure. Clearing attempt_count on each sweep rather than on reallocation. Then an object that fails once per sweep never accumulates attempts, and a persistently failing object retries forever with the counter oscillating between 0 and 1.

DV. Fail an object exactly MAX_ATTEMPTS - 1 times then succeed, checking the count clears only on reallocation. Then fail once more and check RP_ESCALATE.

37. Retry Storms and the Hand-Off to Recovery

Retry has a bounded remit, and knowing where it ends is as important as knowing what it does.

ConditionOwnerWhy
An object fails and is retried successfullyretrythis is the mechanism working
Objects fail intermittently at a low rateretrythe error rate is within what retry absorbs
One object exhausts its attempt budgethand off to 14.2the channel is not delivering; retrying the same object again will not change that
The retry rate is sustained and highhand off to 14.2a physical condition retry cannot fix (13.4 §26)
A specific lane's errors dominatehand off to 14.4this is a repair question, not a retransmission one

The hand-off contract, and both directions matter:

Retry to recovery. RP_ESCALATE asserts a recovery trigger and then does nothing further. It does not clear the buffer, does not abandon entries, does not reset its pointers. The entries are exactly what 14.2 §21's safe-action matrix says must be preserved, and the recovery controller's quiesce phase depends on finding them intact.

Recovery back to retry. After a recovery commits a new configuration, 14.2 §28's resume gate waits on reliability_resolved — which is this mechanism reporting that its outstanding obligations have been re-driven and acknowledged under the new epoch. Retry resumes before general traffic does, which is precisely 14.2 §29's ordering requirement.

Retry escalates by asking for help, not by giving up. A retry mechanism that clears its buffer on escalation has destroyed the state the recovery it just requested needs in order to succeed.

And the storm case deserves naming. A sustained high retry rate is not merely a throughput problem — 13.4 §25 established that retries consume replay residency, arbitration opportunities and bandwidth, so a storm amplifies into a congestion collapse. The escalation threshold is therefore a performance decision as much as a reliability one, and a design that only escalates on per-object exhaustion will not escalate at all during a storm where every object eventually succeeds on its third attempt.

38. Retry and Ordering

Transport retry and protocol ordering cannot be designed independently.

Object A fails; object B is younger. Three cases, and they need different answers:

If the protocol requires…Then transport must…Consequence for the scheduler
A delivered before Bensure A's semantic delivery precedes B'seither replay A before sending B, or the receiver holds B
no ordering between A and Bnothing — B may proceedreplay A when convenient
A and B in the same ordered stream, both already sentensure the receiver's delivery order is correctthe replay range is the question §39 defers to the specification

The key distinction is between transmission order and delivery order. Transport may retransmit in any order it likes; what the protocol constrains is the order in which the receiver's semantic consumer sees objects. A design that satisfies ordering by never reordering transmissions is taking the simplest sufficient path, not the only one — and a receiver that reorders on delivery is another, at the cost of receive buffering.

Chapter 9.4 §9 established the requirement for the streaming case. What matters at the Adapter is that the ordering contract is an input to the replay scheduler, not something checked afterwards.

39. The Replay Range on a Retry Indication

The dependency, stated generally:

  • If the receiver may accept objects out of order and reorder or hold them, retransmitting only the failing object is sufficient. The receiver already has the younger ones.
  • If the receiver discards everything after a failure — a common and much simpler receiver — then retransmitting only the failing object leaves a permanent gap, because the younger objects were discarded and will never be re-sent. Transmission must resume from the failing object forward.

So the replay range is determined by the receiver's discard policy, not by the sender's preference, and the two must agree. A sender that replays one object to a receiver that discarded the rest produces a permanent sequence gap — which 14.1 §26's history checking detects as loss, correctly, forever.

This is the clearest example in the module of a rule that cannot be chosen locally. It is a property of the sender-receiver pair, it must come from the specification, and guessing it produces a design that interoperates with itself and nothing else.

40. Retry and Credits

Chapter 13.1 covers this, and covers it with a correction that is worth respecting here.

The generic invariant is sound and is worth stating:

Retransmitting an object that already holds a resource reservation must not create a second reservation. One object, one reservation, however many attempts.

What 13.1 §12 established, and what it did not. The reasoning above is a resource-accounting design rule derived from the conservation law — given a scheme where one credit reserves one slot, and given a replay mechanism that retransmits the same object, it is the only accounting that conserves. It is not verified UCIe credit semantics, and 13.1 was explicitly corrected to say so: how a replayed flit interacts with UCIe credit consumption is not published.

So this chapter does not re-assert it. What it adds is one practical observation for the retry designer: the replay path is a place where resource accounting is easy to get wrong in both directions, and both directions have distinct signatures (13.4 §25) — charging on replay makes credits fall while remote occupancy stays low, whereas releasing on discard eventually over-advertises. Instrument the credit count alongside the retry count, because the pair distinguishes them and neither alone does.

41. The Replay Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Verification-only reference model. Not synthesisable.
//
// The scoreboard's job is to hold the SEMANTIC identity that the wire does not
// carry, and to check the three-level distinction of Section 4.
class replay_scoreboard;
 
  typedef struct {
    int  allocations;        // MUST be exactly 1
    int  attempts;           // MAY be >= 1
    int  receiver_seen;      // how many attempts the receiver observed
    int  semantic_deliveries;// MUST be <= 1, and eventually == 1
    bit  acknowledged;
    bit  retired;
    int  seq;                // the transport identity it was allocated with
  } obj_t;
 
  obj_t objects[int];        // keyed by SEMANTIC id — the testbench's knowledge
 
  // ---- Invariant 1: one semantic object, one allocation. Catches Section 8.
  function void on_allocate(int sid, int seq);
    objects[sid].allocations++;
    objects[sid].seq = seq;
    if (objects[sid].allocations > 1)
      $error("DOUBLE ALLOCATION: semantic object %0d allocated %0d times",
             sid, objects[sid].allocations);
  endfunction
 
  // ---- Invariant 2: every attempt carries the SAME transport identity.
  //      Catches Section 24's advance-on-replay bug at its source.
  function void on_attempt(int sid, int seq_on_wire);
    objects[sid].attempts++;
    if (seq_on_wire != objects[sid].seq)
      $error("IDENTITY CHANGED ON RETRY: object %0d allocated as %0h, sent as %0h",
             sid, objects[sid].seq, seq_on_wire);
  endfunction
 
  // ---- Invariant 3: at most one semantic delivery. THE property.
  function void on_semantic_deliver(int sid);
    objects[sid].semantic_deliveries++;
    if (objects[sid].semantic_deliveries > 1)
      $error("EXACTLY-ONCE VIOLATED: object %0d delivered %0d times",
             sid, objects[sid].semantic_deliveries);
  endfunction
 
  // ---- Invariant 4: retirement only against a real acknowledgement.
  function void on_retire(int sid);
    if (!objects[sid].acknowledged)
      $error("PREMATURE RETIREMENT: object %0d retired unacknowledged", sid);
    if (objects[sid].retired)
      $error("DOUBLE RETIREMENT: object %0d retired twice", sid);
    objects[sid].retired = 1;
  endfunction
 
  // ---- Invariant 5: a retired object never replays again.
  function void check_no_replay_after_retire(int sid);
    if (objects[sid].retired)
      $error("REPLAY AFTER RETIREMENT: object %0d re-sent after being freed", sid);
  endfunction
 
  // ---- End of test: the liveness half of exactly-once.
  function void final_check();
    foreach (objects[sid]) begin
      if (objects[sid].allocations != 1)
        $error("object %0d: %0d allocations", sid, objects[sid].allocations);
      if (objects[sid].attempts < 1)
        $error("object %0d: never attempted", sid);
      if (objects[sid].semantic_deliveries != 1)
        $error("object %0d: %0d semantic deliveries (expected exactly 1)",
               sid, objects[sid].semantic_deliveries);
      if (!objects[sid].retired)
        $error("object %0d: never retired — replay buffer leak", sid);
    end
  endfunction
 
endclass

Architecture. Five running invariants and a final census, structured around §4's three levels.

Invariant 2 is the one most scoreboards omit and it is cheap. Checking that every attempt carries the identity captured at allocation catches §24's advance-on-replay bug at the wire, on the first retry, rather than downstream as a duplicate delivery. The earlier a scoreboard catches something, the smaller the counterexample.

Invariant 3 is the chapter's central claim and it must be keyed by semantic identity. The wire carries transport identity; the mapping is the testbench's knowledge because the testbench generated the traffic (§23).

The final census is the liveness half. At-most-once is checked continuously; at-least-once can only be checked at the end, and the !retired case is the replay-buffer leak — an object that was allocated, delivered and never freed, which is invisible during the run and fatal to a long one.

Why receiver_seen is tracked separately from attempts. They differ when an attempt is lost in the channel, and the difference is exactly what distinguishes "the channel dropped it" from "the receiver discarded it" — two conditions with the same symptom at the sender and completely different root causes.

42. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_retry @(posedge clk);
  option.per_instance = 1;
 
  // --- Attempts per object. "none" must dominate; the others must occur.
  cp_attempts : coverpoint object_attempt_count_at_retire {
    bins none    = {1};              // succeeded first time
    bins one     = {2};
    bins several = {[3:MAX_ATTEMPTS-1]};
    bins maxed   = {MAX_ATTEMPTS};   // escalation path
  }
 
  // --- WHY the retry happened. The lost-ACK bin is the one that matters.
  cp_retry_cause : coverpoint retry_cause {
    bins nak       = {RC_NAK};
    bins timeout   = {RC_ACK_TIMEOUT};
    bins lost_ack  = {RC_LOST_ACK};   // Section 20 — needs directed injection
  }
 
  // --- Did a duplicate actually get SUPPRESSED? The bin that proves
  //     exactly-once ran rather than merely being asserted.
  cp_dup_suppressed : coverpoint duplicate_suppressed_at_rx;
 
  // --- Buffer occupancy, including full.
  cp_occupancy : coverpoint rp_count_q {
    bins empty     = {0};
    bins low       = {[1 : REPLAY_DEPTH/2]};
    bins high      = {[REPLAY_DEPTH/2+1 : REPLAY_DEPTH-1]};
    bins full      = {REPLAY_DEPTH};      // Section 28's backpressure
  }
 
  // --- Simultaneous events (Section 30) and pointer wraps (Section 31).
  cp_alloc_retire : coverpoint {alloc_fire, retire_fire} {
    bins neither = {2'b00}; bins alloc = {2'b10};
    bins retire  = {2'b01}; bins both  = {2'b11};   // must be non-zero
  }
  cp_wrap : coverpoint wrap_event {
    bins alloc_wrap  = {WRAP_ALLOC};
    bins retire_wrap = {WRAP_RETIRE};
    bins replay_wrap = {WRAP_REPLAY};   // the one usually missed
  }
 
  // --- Where in the window the retried entry sat.
  cp_retry_position : coverpoint retry_entry_position {
    bins at_head = {0};
    bins middle  = {[1 : REPLAY_DEPTH-2]};
    bins at_tail = {REPLAY_DEPTH-1};
  }
 
  // --- Acknowledgement shape.
  cp_ack_span : coverpoint ack_retire_n {
    bins one   = {1};
    bins few   = {[2:4]};
    bins many  = {[5:$]};              // cumulative retiring a run
  }
 
  // --- Scheduling contention (Sections 33, 34).
  cp_contention : coverpoint replay_vs_new_contention;
 
  // --- Escalation and recovery interaction (Section 37).
  cp_escalate     : coverpoint rp_state_q { bins esc = {RP_ESCALATE}; }
  cp_recovery_mid : coverpoint recovery_started_with_replay_outstanding;
 
  // --- Crosses that carry the information.
  x_lost_ack_dup    : cross cp_retry_cause, cp_dup_suppressed;   // Section 20
  x_full_retry      : cross cp_occupancy, cp_retry_cause;
  x_wrap_retry      : cross cp_wrap, cp_retry_position;
  x_contend_attempts: cross cp_contention, cp_attempts;          // Section 34
  x_recovery_replay : cross cp_recovery_mid, cp_occupancy;       // 14.2 hand-off
endcovergroup

Six bins whose value is being non-zero, each proving a mechanism ran rather than merely existing:

x_lost_ack_dup with lost_ack and a suppressed duplicate. This is §20, and it is the only evidence that exactly-once works. The at-most-once assertion passes vacuously in any test that never produces a duplicate — so a regression with this bin at zero has asserted exactly-once and never tested it.

cp_alloc_retire.both. Simultaneous allocation and retirement, §30's case and the steady state of a full-rate link.

cp_wrap.replay_wrap. A replay sweep crossing the ring boundary — the wrap that only occurs when a sweep starts near the top, and the one two-pointer designs never encounter.

cp_occupancy.full. §28's backpressure, without which the correctness term in the admission gate has never been the binding one.

cp_attempts.maxed and cp_escalate. The escalation path to 14.2, which never occurs spontaneously.

x_recovery_replay. A recovery starting with replay entries outstanding — the hand-off of §37 and 14.2 §21's safe-action matrix, and the case that finds a recovery controller that clears the replay buffer.

43. Flagship Trace — Three Objects, One Failure

Illustrative. REPLAY_DEPTH = 8, cumulative acknowledgement, acknowledgement latency 4 cycles. Objects A (seq 0x20), B (0x21), C (0x22).

Cyccountheadtailreplay ptrTXACK/NAK inStateSemantic deliveries
0000IDLE
1101A (0x20)SEND_NEW
2202B (0x21)SEND_NEW
3202WAIT
4202WAITA delivered
5112ACK 0x20WAITA
6213C (0x22)SEND_NEWA
7213NAK 0x21REPLAYA — B was corrupt, not delivered
82131B (0x21) againREPLAYA
92132C (0x22) againREPLAYA
10213DRAINA
11213DRAINB delivered
12213DRAINB — C recognised as duplicate, not delivered twice
13033ACK 0x22WAIT
14033IDLEA, B, C — each exactly once

Seven readings, and the non-events matter most.

Cycles 1–2 and 6: count and tail advance together on allocation. Three allocations, three increments. count never increments anywhere else in the trace — not at cycle 8, not at cycle 9, both of which are transmissions. §8's bug is the version of this table where cycles 8 and 9 show count 3 and 4.

Cycle 5: one acknowledgement, one retirement. ACK 0x20 retires exactly the head entry; head moves 0→1 and count falls 2→1. The acknowledgement's target and its effect are bound (§18).

Cycle 7: the NAK arrives for B, and C had already been sent at cycle 6. This is the ordering situation of §39: C was transmitted before B's failure was known.

Cycles 8–9: the sweep replays B and C. The replay pointer walks 1→2, re-sending both from retained state. This is the from-the-failing-object-forward behaviour, which §39 explicitly declines to assert as UCIe's rule — it is shown here because it is the choice that is safe against a receiver that discards after a failure, and the trace notes the dependency rather than the rule.

Cycle 9 sends C a second time, and cycle 12 does not deliver it a second time. C arrived correctly the first time and was delivered at neither cycle — it was held pending B, then recognised at cycle 12 as an identity already seen. Two physical attempts, one semantic delivery (§20, §22).

Cycle 13: one cumulative acknowledgement retires both B and C. head jumps 1→3, count falls 2→0. §12's cumulative behaviour doing exactly what it is for — and §17's bug is the version where this jump lands on the wrong entry.

And the Semantic-deliveries column ends at "A, B, C — each exactly once" after five physical transmissions of three objects. That ratio — five attempts, three deliveries — is the mechanism working, and it is what the scoreboard's attempts >= 1, semantic_deliveries == 1 invariant expresses.

44. Lost-Acknowledgement Trace

Illustrative. Object D (seq 0x30) arrives intact and is delivered; its acknowledgement is lost.

CyccountSender believesTXReceiver stateACK inSemantic deliveries of D
01D sent, unresolvedD (0x30)0
11D sent, unresolvedD arrives, CRC clean0
21D sent, unresolvedD delivered1
31D sent, unresolvedack 0x30 sent1
41D sent, unresolved(lost)1
1D sent, unresolved1
121timeout: D must be retried1
131retryingD (0x30) again1
141retryingD arrives, CRC clean1
151retrying0x30 already acted upon → suppress1
161retryingack 0x30 sent again1
170D resolvedACK 0x301

Six readings.

Cycles 2 and 15: the object arrives twice and is delivered once. That single fact is exactly-once, and it is produced by two independent behaviours: the sender retransmitted the same identity (§24), and the receiver recognised it (§22). Either alone is insufficient.

Cycles 4–12: the sender is wrong for eight cycles and cannot know it. Its belief — "D is unresolved" — is false from cycle 2 onward. There is no mechanism by which it could learn otherwise, which is 14.2 §22's ambiguity in its purest form: the absence of an acknowledgement is consistent with both "never arrived" and "arrived, acknowledgement lost."

Cycle 13: the retry is correct behaviour, not a defensive hack. The sender is obliged to retry. A sender that reasoned "it probably arrived" and retired the entry would be guessing, and would lose D in the case where it genuinely had not arrived.

Cycle 15 is the whole mechanism, and it produces no event. No error is logged. No counter increments except a duplicate-suppression coverage bin. The correct outcome is silence, which is why §42 insists on a coverage bin proving it happened — otherwise the mechanism is untested and indistinguishable from broken.

Cycle 16: the duplicate is acknowledged. This is the step §22 warns about: suppressing the delivery is required, and suppressing the acknowledgement as well would leave the sender retrying forever. Suppress the effect, not the reply.

Cycle 17: count finally falls to 0, fifteen cycles after the object was delivered. The retention was necessary for every one of those cycles from the sender's point of view, and unnecessary from the receiver's. That asymmetry — retention is about what the sender knows, not about what is true (§14) — is the reason the retirement point is where it is.

45. Debug Taxonomy

SignatureMost likely causeFirst instrument
Replay count grows on every retry§8 — retry allocating a new entryrp_count_q against retry events
A semantic operation occurs twiceduplicate suppression boundary: identity changed on retry (§24), or receiver history advanced on a discarded unit (14.1 §28)is the transmitted sequence identical across attempts?
Buffer full with few semantic objects outstanding§8, or a pointer/count divergence (§9)reconcile rp_count_q against ring_dist(head, tail)
ACK received, occupancy never fallsretirement not happening: §17's misalignment in the loss direction, or ack_in_window rejecting valid acknowledgementsacknowledgement decode against the retirement pointer move
The wrong entry disappears§17 — the acknowledgement applied to a different entry than it identifieddoes the decision structure carry the sequence it was computed from?
Retries high, escalation never fires§36 — the threshold unreachable, or attempt_count cleared per sweepis MAX_ATTEMPTS < 2**TRY_W? when is attempt_count cleared?
Throughput collapses before the buffer is full§27 — the window is too small for the acknowledgement round tripreplay-space stall cycles (13.5 §34)
A failed object is retried only after a long delay under load§34 — new traffic starving the replaythe interval between retry_required and replay_fire
Specific objects unrecoverable when they fail, others fine§29 — admission without a replay-space termwas the object retained at all?
Rare intermittent failures, roughly one per 256 objects§25 — magnitude comparison across the sequence wrapthe comparison expression
After a recovery, entries replay that were already retiredepoch not checked on the acknowledgement (§18) or on the replay boundlink_epoch_q against the decision structure's epoch
Retries succeed but the link stalls forever after one CRC errornot this chapter — 14.1 §28's receiver expectation advancing on a discarded unitthe receiver's expected sequence across the failure

46. Debug Checklist

  1. Which semantic object is failing? Not which entry — the object, because entries are reused.
  2. Which replay entry owns it, and is that entry still valid? §15's failure is that it is not.
  3. How many physical attempts have been made? attempt_count, per entry.
  4. Has the receiver seen it? The distinction between "lost in the channel" and "discarded at the receiver" — the scoreboard's receiver_seen against attempts.
  5. How many semantic deliveries occurred? The exactly-once check; anything above 1 stops the investigation.
  6. Is the transmitted sequence identical on every attempt? If not, §24, and the receiver cannot possibly suppress the duplicate.
  7. What acknowledgement was observed, and for which sequence?
  8. Which entry did that acknowledgement actually retire? If it is not the one the acknowledgement identified, §17.
  9. Did anything allocate on a replay? §8 — compare rp_count_q before and after a retry.
  10. Do the count and the pointers agree? §9's fourth property; a disagreement invalidates every subsequent conclusion.
  11. Did a pointer wrap during the event? Especially the replay pointer mid-sweep (§31).
  12. Was the buffer full, and was admission correctly stopped? §28 — and was the stall attributed to replay?
  13. Did younger traffic overtake the replay? §34's scheduling delay; measure retry_required to replay_fire.
  14. Did the attempt count escalate, and did the hand-off to recovery happen? §37 — and did the buffer survive it?
  15. Does the replay scoreboard balance? One allocation, at least one attempt, at most one delivery, retirement only after acknowledgement, and nothing left unretired at the end.

47. Common Misconceptions

"Retry is just sending the packet twice." It is retransmission plus retained identity plus controlled retirement. Retransmission without retained identity means the receiver executes twice; without controlled retirement there is nothing left to send when it fails (§1).

"A retry needs a new replay entry." A retry re-transmits from retained state; it does not create retained state. Allocating on retry collapses effective capacity in proportion to the error rate and eventually transmits duplicates as new objects (§8).

"tx_fire means the replay storage can be freed." Transmission proves the bytes left the die. It proves nothing about arrival, integrity, or delivery — and the sender is the only party that can still fix it (§14, §15).

"ACK and semantic completion are the same event." An acknowledgement is a transport fact about retention; completion is a semantic fact about the operation. In the lost-acknowledgement case the object is delivered at cycle 2 and acknowledged at cycle 17, and both facts are correct (§44).

"A lost ACK means the receiver should execute again." It means the sender must retry, because it cannot distinguish a lost object from a lost acknowledgement. The receiver must recognise the repeat and suppress the delivery — while still acknowledging it, or the sender retries forever (§20, §22).

"Credits alone determine whether reliable traffic can be accepted." Credits protect the receiver's storage; replay space protects the sender's ability to honour its own contract. Sending without retention is not premature — it is incorrect (§28, §29).

"Sequence wrap can be compared with normal integer ordering." A magnitude comparison inverts across the wrap and fails roughly once per traversal of the space — once every 256 objects for an illustrative 8-bit field — presenting as a rare intermittent fault with no physical correlation (§25).

"Replay priority is only a performance choice." Under cumulative acknowledgement a delayed replay blocks the retirement of everything behind it, filling the buffer and stalling the very traffic that was prioritised. Prioritising new traffic over replay is self-defeating under load (§33, §34).

"A retry storm is just a throughput issue." Retries consume replay residency, arbitration opportunities and bandwidth, so a storm amplifies into congestion collapse — and a design that escalates only on per-object exhaustion will not escalate during a storm where every object eventually succeeds (§37).

"Replay buffers only store payload." They store everything the transmit path needs to reproduce the transmission exactly, including framing metadata — otherwise a replay under a changed configuration differs from the original (§6).

"If the CRC is correct on the retry, exactly-once follows automatically." A CRC proves the bytes are intact. It says nothing about whether this is the first arrival or the third, and a bit-perfect duplicate passes every integrity check (14.1 §6, §22).

"Escalating to recovery means giving up on the buffered objects." Escalation is asking for help. A retry mechanism that clears its buffer on escalation destroys the state the recovery it just requested needs in order to succeed (§37).

48. Understanding Check

49. Summary and What Comes Next

Retry is retransmission plus retained identity plus controlled retirement. Drop any one and a different thing breaks: without identity the receiver executes twice, without controlled retirement there is nothing left to send, without retransmission it is bookkeeping.

Three levels, and every bug is a collapse of two into one. One semantic object, one replay entry, many attempts. At the wire they are indistinguishable — only the sender's retained state knows the difference, which is why the correctness lives there.

Three pointers, not two. Allocate, retire, and an independent replay read pointer, ordered modularly. The third decouples what is safe to forget from what am I currently re-sending, which are genuinely independent questions — and the flexibility costs an assertion that a replayed entry is still valid.

The retirement point is the acknowledgement condition and nothing earlier, because retention is about what the sender knows, not about what is true. tx_fire is the right event for a transmit queue and the wrong one for retained state.

An acknowledgement must be bound to the entry it retires. Compute the retirement count from the same cycle's sequence, carry them together with the link epoch, and check the binding at the point of use — or one misalignment silently loses retention in one direction and stalls throughput in the other.

The lost acknowledgement is what exactly-once means. The object arrives, is delivered, and the acknowledgement is lost, so the sender is obliged to retry a delivery that already happened. It holds only because the sender retransmits an unchanged identity and the receiver recognises it — and because the receiver suppresses the delivery while still sending the acknowledgement, since suppressing both is a livelock.

The retry window is a concurrency pool sized against the acknowledgement round trip, which includes the checker's latency and the reverse-traffic scheduling delay. Under an illustrative modulo-N scheme it is bounded above by the sequence width — an illustrative 8 bits would allow under 128 unresolved objects — so a small header field can become the throughput limit. Treat the coupling as the lesson and the numbers as illustrative (§25).

Replay space is a correctness term in the admission gate, not a performance one. Transmitting something that cannot be retained is not premature; it is a reliable link operating unreliably without noticing.

And retry escalates by asking for help, not by giving up. When attempts are exhausted or the retry rate is sustained, the decision belongs to recovery — and the replay buffer must survive the hand-off intact, because the recovery that was just requested depends on finding it.

Retry assumes the physical path is still worth using. The next chapter handles the harder case: when part of the physical link itself is degrading, how do we reduce capability, retrain, and continue without corrupting transport state?

  • 14.4 — Link Robustness — lane health, masking and retraining, width and rate reduction, and committing a new physical configuration without losing what is in flight.

Browse the full path on the UCIe tutorials index.