UCIe · Module 13
Credit-Based Flow Control
Credit accounting across a whole UCIe stack rather than one sender and receiver — multiple independent flow-control domains at FDI, the link, and RDI, why a credit in one is not a credit in another, what a one-bit header field can and cannot carry, why a retransmitted object should not consume a second reservation, credits versus outstanding-transaction slots, class pools, credit epochs, and the stacked conservation law.
Module 12 proved that a transaction can be tracked correctly: allocated once, delivered once, retired once, with its bytes intact. Every trace in it assumed a resource was available at the moment it was needed — a credit, a replay entry, a queue slot — and treated each as a precondition to check rather than a mechanism to build.
Module 13 builds those mechanisms. This chapter builds the first one, and it has to start by being clear about what it is not going to re-derive.
1. What Chapter 9.5 Established, and What This Chapter Adds
Chapter 9.5 already built the credit machine for one sender and one receiver, and built it thoroughly. It established that a credit is permission backed by real storage; the conservation invariant for a single buffer; that the sender must not assume the receiver's depth; that consume and return in the same cycle need one assignment and not two; that underflow is a fault rather than something to saturate; that a credit returns when the slot is free rather than when the packet arrives; the bandwidth-delay product; and how leakage and duplication present.
None of that is repeated here. Where this chapter needs one of those mechanisms it links to it and moves on.
What this chapter adds is the thing 9.5 could not, because it was scoped to one buffer:
A layered die-to-die stack does not have one flow-control domain. A representative implementation has several, stacked, with different capacities, different round trips, and different owners — and a credit in one of them is not a credit in another.
So the new material is: where independent flow-control domains appear in a layered stack; why the stacked conservation law is several equations rather than one; how credits interact with replay, which did not exist in 9.5's streaming model; how they interact with traffic classes when a shared resource is multiplexed; how a credit differs from an outstanding-transaction slot from Module 12; and what a credit epoch must do across a recovery.
A word on what kind of claim this chapter makes. The domain decomposition below is a representative layered architecture, not a recitation of specification-defined credit domains. §3 states exactly what official UCIe material does and does not establish, and every quantity in the chapter is symbolic. The engineering — the conservation laws, the accounting discipline, the failure modes — is what transfers to whatever your revision defines.
2. The One-Sentence Model
A credit is a reservation on one specific resource, in one specific domain, held by one specific owner — and permission in one domain grants nothing in any other.
9.5's model was a credit is permission backed by storage. This chapter's addition is the three qualifiers, because almost every bug below is a design treating one domain's permission as though it were another's.
3. Sourcing — and an Honest Statement About L-Credits
4. Several Flow-Control Domains in One Stack
The structural idea this chapter exists for.
A flow-controlled boundary needs permission wherever a producer can outrun a consumer across an interface it cannot observe directly. A layered die-to-die stack has storage at every boundary, so it has that condition at every boundary.
A representative layered implementation therefore contains multiple independent flow-control domains at these three boundaries. This is a design decomposition, not a set of UCIe-defined credit domains (§3) — the names below are this chapter's labels for the boundaries, and the ownership column is representative architecture rather than a specification rule.
| Domain (this chapter's label) | Between | What a permission reserves | Round trip | Who would advertise |
|---|---|---|---|---|
| FDI-side flow control | Protocol Layer ↔ D2D Adapter | storage inside the Adapter, on the same die | short — a few cycles, on-die | the Adapter |
| Link flow control | local Adapter ↔ remote Adapter | storage in the far Adapter, on the other die | long — a package round trip | the remote Adapter |
| RDI-side flow control | D2D Adapter ↔ Physical Layer | storage inside the PHY, on the same die | short — on-die | the PHY |
What is verified and what is not, precisely. That FDI and RDI exist as internal interfaces at those two boundaries is official (§3), and that credit exchange is an encoded UCIe mechanism is official. That each of the three boundaries operates its own credit pool with the ownership tabulated above is not established by any official source I could reach — it is the architecture a layered stack with storage at each boundary tends to require, which is a strong engineering argument and not a normative one. Treat the table as a design you should verify against your revision, and take the invariant instead:
Permission for one resource does not grant permission for another. Wherever your implementation actually places its flow-control boundaries, that sentence governs how you must account for them.
Three observations, and each generates a section.
Only the middle one has a long round trip. 9.5's bandwidth-delay-product analysis applies to the link domain and barely applies to the two on-die domains, whose feedback is a few cycles. So the three domains need very different amounts of credit for the same throughput, and sizing them all from one formula over-provisions two and under-provisions none — expensive rather than wrong.
Only the middle one crosses a reliability boundary. Where a link-level retry mechanism is present, it is the link boundary that retransmits — the two on-die interfaces are inside the reliability domain rather than across it, so the same object is not re-offered to them as a fresh transfer. §12 is that difference, and it is the sharpest new idea in the chapter.
Two qualifications, both from §3. First, link-level reliability and replay are treated in this chapter as a mechanism separate from the local-interface flow-control examples — that separation is a teaching boundary this chapter draws, not a specification statement that on-die interfaces "cannot replay." Second, CRC and link-level retry are described by official material as optional Adapter functions, present "if applicable" and removed entirely in UCIe-3D. So §12 is conditional throughout: where a replay mechanism exists, here is how it must interact with accounting.
And they fail independently. A stack can be starved at FDI while the link has abundant credit, or vice versa. §22's taxonomy is organised around telling those apart, because the symptom — "the Protocol Layer cannot send" — is identical.
Headroom — why the round trip is a floor and not a target.
Chapter 9.5 §11 derived the steady-state minimum: to keep a loop busy at rate R with feedback latency L, roughly R × L permissions must exist. That figure is the point at which a perfectly smooth producer, a perfectly regular consumer and a fixed return latency stop stalling. A real domain has none of those three properties, and the gap between the smooth-case minimum and what the design actually provisions is headroom:
Headroom is capacity beyond the steady-state minimum, provisioned to absorb the variation the steady-state calculation assumes away.
Six things consume it, and they are additive rather than alternatives:
| Source of variation | What it does to the loop | Which domain feels it most |
|---|---|---|
| Producer burstiness | offers N objects back to back against an average rate far below N | all three; worst where the producer is a packetiser emptying a queue |
| Arbitration delay | a granted object waits behind other classes or other traffic before it is launched | wherever a shared resource is arbitrated — the link domain typically |
| Return quantisation (where the return mechanism quantises — §10) | releases are reported in groups, so permission arrives in steps rather than continuously | the link domain, where reporting bandwidth is expensive |
| Variable consume latency | the consumer drains at a rate that depends on what it is draining | the FDI and RDI domains, where the consumer is another layer with its own stalls |
| Clock-domain crossing | synchroniser depth adds latency to the return path, and adds uncertainty to it | any boundary that crosses domains |
| Service jitter | the same operation takes a different number of cycles on different occasions | all three |
Two consequences worth stating plainly.
Headroom is not free — it is receive storage. Every extra credit must be backed by a real slot (§2), so headroom is paid for in area at the consumer. That is why "just add more credits" is not a universal answer and why §24's misconception exists: beyond the point where the loop never idles, extra credits buy nothing and cost silicon.
And the three domains need very different amounts of it. The link domain has the long round trip and any return quantisation (§10) and the arbitration — so it needs the largest absolute pool and the largest headroom fraction. The on-die domains have a few cycles of feedback and mostly need enough to cover burstiness and the consumer's own stalls. Sizing all three from the link domain's number over-provisions two of them; sizing all three from an on-die number under-provisions the one that matters.
How to measure whether the headroom is right, rather than guessing. §18's min_credit_q low-water mark is exactly the instrument: run the intended traffic profile and read the minimum the counter ever reached. A low-water mark that never approaches zero means the pool is larger than the workload needs; one that sits at zero for extended periods means the loop is the bottleneck and the pool is too small. A low-water mark that touches zero only in bursts is the intended operating point — the headroom is being used, which is what it is for.
5. The Accounting Loop, Per Domain
Read the figure three times, once per domain, changing only two things: what "consumer-side storage" is, and how long the return path takes. At FDI it is Adapter storage a few cycles away. On the link it is remote-die storage a package round trip away. At RDI it is PHY storage a few cycles away. The topology is identical and the numbers are not, which is exactly why one credit counter cannot serve all three (§8).
6. A Credit in One Domain Is Not a Credit in Another
The mistake this chapter exists to prevent, stated before the RTL that makes it.
Holding a link credit means the remote Adapter has storage reserved. It says nothing about whether:
- the local Adapter has room to accept the object from the Protocol Layer at FDI;
- the local PHY has room to accept it at RDI;
- a replay entry exists so the object can be retained (Chapter 9.4 §11);
- the remote Protocol Layer will drain the remote Adapter's storage at all.
That last one deserves emphasis, because it is the limit of what credits can promise. A credit reserves storage; it does not promise the storage will ever be released. If the remote Protocol Layer stops draining, the remote Adapter's buffer fills, returns stop, and the local sender starves — with every credit correctly accounted for at every instant. Credits guarantee safety, never liveness, and §19 is where that becomes a verification statement rather than a slogan.
Permission is per-domain, per-resource, and per-owner. Composing a stack means composing several permissions, and the binding constraint is whichever is scarcest at that moment.
7. The Stacked Conservation Law
9.5 gave the conservation law for one buffer. Stacking it is not a matter of adding terms — it is one equation per domain, and they do not imply one another.
For each domain d:
credits_held_by_producer[d]
+ objects_in_flight_toward_consumer[d]
+ slots_occupied_at_consumer[d]
= advertised_capacity[d]Three equations, checked independently. And the important negative result:
All three can hold while the stack makes no progress, because each equation constrains one domain's accounting and none of them says anything about the others, or about whether any consumer is draining.
Two further subtleties that only appear once the domains are stacked.
An object occupies storage in more than one domain at once. While a flit sits in the local Adapter's storage awaiting transmission, it occupies an FDI-domain slot and has a link-domain credit committed to it and may hold a replay entry. Three reservations, one object — which is Chapter 11.5 §12's four-tracking-entries argument arriving in the resource plane, and the reason §8's single-counter design is not merely imprecise but wrong.
The domains' capacities are not required to be equal, and usually are not. FDI storage sized for a few cycles of on-die feedback might be two or four entries; link credits sized for a package round trip must be far more. A design that advertises the same number in both has sized one of them wrongly.
8. Wrong RTL — One Credit Counter for the Whole Stack
// WRONG — one counter standing for "can we send", decremented whenever an
// object leaves the Protocol Layer and incremented on any return event.
logic [CREDIT_W-1:0] credit_q;
always_ff @(posedge clk) begin
if (obj_leaves_protocol_layer) credit_q <= credit_q - 1'b1;
else if (any_credit_return) credit_q <= credit_q + 1'b1;
endArchitecture. It collapses three domains with three capacities, three round trips, and three owners into one number. The else if also serialises consume and return, which is a second bug — Chapter 9.5 §6 covers that one and §9 revisits it per-domain.
Cycle behaviour. One number falls when the Protocol Layer sends and rises on any return.
Failure, and there are three distinct ones.
It is initialised from one capacity, so two domains are mis-sized. Whichever capacity was chosen, one domain is over-advertised — and over-advertising means the producer is permitted to create occupancy that does not exist. If the number came from the link domain's larger capacity, the local Adapter's small FDI storage overflows long before the link is stressed.
Returns from different domains are indistinguishable. An FDI-domain return and a link-domain return both increment the same counter, so draining the local Adapter appears to grant permission to occupy remote storage. That is the most dangerous single consequence: local progress manufacturing remote permission.
And the diagnosis is destroyed. With one counter, "why is the sender stalled" has one possible answer — the counter is zero — and no way to say which resource is actually scarce. §22's taxonomy becomes unusable.
The right shape is one counter per domain, and the acceptance gate is a conjunction over them rather than a comparison against one:
// Illustrative — one counter per domain, each with its own capacity and its
// own return event. Symbolic widths; not UCIe field naming.
typedef enum logic [1:0] { DOM_FDI = 2'd0, DOM_LINK = 2'd1, DOM_RDI = 2'd2 } cdomain_t;
localparam int NUM_DOM = 3;
logic [CREDIT_W-1:0] credit_q [NUM_DOM]; // permission held, per domain
logic [CREDIT_W-1:0] cap_q [NUM_DOM]; // advertised capacity, per domain
logic cap_valid_q [NUM_DOM]; // advertisement received
// A domain grants permission only if it has been initialised AND has credit.
function automatic logic domain_permits(cdomain_t d);
return cap_valid_q[d] && (credit_q[d] != '0);
endfunction// Illustrative — a per-domain assertion, elaborated for each domain, so a
// failure names the domain rather than "credits".
property p_credit_bounded_by_capacity;
@(posedge clk) disable iff (!rst_n)
credit_q[d] <= cap_q[d];
endproperty
a_credit_bounded_by_capacity: assert property (p_credit_bounded_by_capacity);
// And the one that catches §8's cross-domain contamination directly: a return
// in one domain must not change another domain's count.
property p_return_affects_one_domain;
@(posedge clk) disable iff (!rst_n)
credit_return_fire[d] |=> (credit_q[e] == $past(credit_q[e])); // e != d
endproperty
a_return_affects_one_domain: assert property (p_return_affects_one_domain);p_return_affects_one_domain is the property worth having and the one nobody writes. It is trivially true in a correct design and it is the only check that fires on a shared-counter or mis-wired-return implementation — where the accounting stays bounded, so the capacity assertion passes.
9. Per-Domain Consume and Return
Chapter 9.5 §6 established why consume and return need one assignment with all four cases enumerated. Here it is per domain, and the reason it is worth restating is the indexing, which is where the stacked version goes wrong.
// Illustrative per-domain credit maintenance. The four-case discipline is
// Chapter 9.5 §6's; what is new is that the domain index must come from the
// EVENT, never from a shared register that a second event may have changed.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int d = 0; d < NUM_DOM; d++) begin
credit_q[d] <= '0;
cap_q[d] <= '0;
cap_valid_q[d] <= 1'b0;
end
end else begin
for (int d = 0; d < NUM_DOM; d++) begin
if (cap_advert_valid[d]) begin
credit_q[d] <= advertised_capacity[d];
cap_q[d] <= advertised_capacity[d];
cap_valid_q[d] <= 1'b1;
end else begin
unique case ({credit_consume[d], credit_return[d]})
2'b10 : credit_q[d] <= credit_q[d] - CREDIT_W'(1);
2'b01 : credit_q[d] <= credit_q[d] + CREDIT_W'(1);
2'b11 : credit_q[d] <= credit_q[d]; // spent and regained — net zero
default: credit_q[d] <= credit_q[d]; // idle
endcase
end
end
end
endArchitecture. A per-domain counter with per-domain events. The loop makes the independence structural: there is no expression in which domain d's update can read domain e's event.
State. NUM_DOM counters plus capacities and validity bits. Per-domain lifetime, and specifically per-domain-epoch — the link domain's credits are invalidated by a link recovery while the FDI domain's are not (§15).
Cycle behaviour. The 2'b11 arm is unchanged, per Chapter 9.5 §6. What is new is that all three domains can have activity in the same cycle, in any combination — so the arm matters three times per cycle, not once.
Contract. Each consumer relies on its own producer never exceeding its own advertisement. Three separate safety properties, not one.
Failure. Indexing a shared "current domain" register rather than the event's own domain produces a decrement charged to the wrong domain — which keeps the total right and both individual counts wrong, so §8's bounded-by-capacity assertion can still pass on both while one domain is over-committed and the other is under-used.
DV. Drive activity in all three domains in the same cycle, in every combination of consume and return. That is 4³ = 64 combinations of the case arms, and the interesting ones are those where two domains do opposite things simultaneously.
On underflow. Chapter 9.5 §8 made the argument that an unsigned credit counter must never be allowed to wrap and must not be saturated, because saturation hides a correctness failure while a wrap converts zero into maximum permission. That holds unchanged per domain — with one addition worth stating: the assertion must name the domain, because the recovery action differs. A link-domain underflow means the far die may already be overflowing; an FDI-domain underflow is contained on this die.
// Illustrative — never consume without permission, per domain. Note the check
// is on the CURRENT value, before the decrement takes effect.
property p_no_consume_without_credit;
@(posedge clk) disable iff (!rst_n)
credit_consume[d] |-> (cap_valid_q[d] && (credit_q[d] != '0));
endproperty
a_no_consume_without_credit: assert property (p_no_consume_without_credit);10. What One Bit Can and Cannot Carry
Now the verified fact, and — carefully — what does and does not follow from it.
The verified part. The Consortium's flit-header description gives the header two bytes, of which one bit is Credit (§3). A one-bit field cannot itself encode the value "three". That much is arithmetic on a verified fact.
The part this chapter does not infer. It is tempting to conclude from a one-bit field that credit returns must therefore be batched into fixed quanta. That conclusion does not follow, and this chapter does not draw it. A single-bit field is equally consistent with several mechanisms, and official material does not say which applies:
- a fixed quantum — the bit means "return k credits", for some architecturally fixed k, so returns are quantised and the producer's count moves in steps of k;
- a one-per-flit return — the bit means "return one", which needs no batching at all and is perfectly expressible in one bit;
- a presence indication — the bit means "credit information is carried elsewhere", with the quantity in a payload or control structure the Consortium description does not enumerate.
A single bit proves none of: a fixed quantum, accumulation, batching, a return count, or any return timing. Official material does establish that credit-exchange encodings exist (§3), but not their semantics.
Three design consequences follow from the quantised-return assumption, and all three are engineering rather than specification.
Effective return latency rises. A receiver that frees one slot cannot return one credit immediately; it must wait until it has a quantum's worth, or until a flit is going anyway (§11). So the round trip that sizes the credit pool is longer than the physical round trip, by up to the accumulation time.
The producer's count is coarse near zero. With a quantum of k, a producer at zero credits stays at zero until k slots are free. Small transfers can therefore stall behind a quantum boundary even though capacity exists — a throughput effect with no correctness symptom.
And the receiver needs its own accumulator, which is state with a lifetime:
// ILLUSTRATIVE ONLY — a receiver-side return accumulator for a quantised
// return scheme. This is a generic design pattern, not a UCIe mechanism.
//
// RETURN_QUANTUM IS ILLUSTRATIVE ONLY AND IS NOT A UCIe-DEFINED VALUE.
// UCIe does not publish a credit-return quantum; 4 is chosen here purely
// so the arithmetic below is concrete. Do not read it as "UCIe uses
// four-credit batches" — see the callout above and Section 3.
localparam int RETURN_QUANTUM = 4; // ILLUSTRATIVE
localparam int ACC_W = $clog2(RETURN_QUANTUM+1); // +1: must hold QUANTUM
logic [ACC_W-1:0] return_acc_q; // slots freed but not yet reported
logic return_send;
assign return_send = (return_acc_q == ACC_W'(RETURN_QUANTUM));
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
return_acc_q <= '0;
end else begin
// One writer, all four cases — the same discipline as the credit counter.
unique case ({slot_freed, return_send})
2'b10 : return_acc_q <= return_acc_q + ACC_W'(1);
2'b01 : return_acc_q <= return_acc_q - ACC_W'(RETURN_QUANTUM);
2'b11 : return_acc_q <= return_acc_q - ACC_W'(RETURN_QUANTUM) + ACC_W'(1);
default: return_acc_q <= return_acc_q;
endcase
end
endArchitecture. A small accumulator converting individual slot releases into quantised reports.
State. One counter with per-link-epoch lifetime — and note it must be cleared on re-initialisation, because slots freed under the previous epoch have already been accounted for by the re-advertisement (§15).
Cycle behaviour. The 2'b11 arm is the one to get right and the easiest to omit: a slot freed in the same cycle a quantum is reported must still be counted. Writing this as two statements loses the release.
Contract. The producer relies on every freed slot eventually being reported. The accumulator is where credits leak if that is violated.
Failure — and this is a leak with a distinctive signature. If the accumulator can hold a residue below the quantum indefinitely, then up to QUANTUM-1 credits are permanently withheld. The producer's usable pool is smaller than advertised by a fixed amount, forever. It is not a drift — it is a constant offset, which is what distinguishes it from Chapter 9.5 §16's classic leak.
The mitigation is a flush, and it needs a condition: report a partial quantum when the producer is starved, or on a timer, or when the buffer becomes empty. Which condition is an architectural choice; having none is a bug.
DV. Free exactly QUANTUM-1 slots and verify the credits eventually return; free slots one at a time across a quantum boundary with a simultaneous report; and verify the accumulator clears on re-initialisation.
The opposite fault — one release, two returns.
The accumulator is also where the mirror-image bug originates, and it is the more dangerous of the two.
// WRONG. The release event is counted on two paths: once where the slot is
// popped, and once where the entry's valid bit is cleared. In a design where
// those are the same event they are the same credit, and this returns two.
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) return_acc_q <= '0;
else begin
if (slot_popped) return_acc_q <= return_acc_q + ACC_W'(1);
if (entry_invalidated) return_acc_q <= return_acc_q + ACC_W'(1);
end
endArchitecture. One physical release must generate exactly one credit, because a credit is a reservation on one slot (§2). Two returns for one release manufacture permission that no storage backs.
State. The same accumulator — but now over-counting rather than withholding.
Cycle behaviour. Note that this is not the two-independent-assignments drift of Chapter 9.5 §6, where the last assignment wins and one event is lost. Here both paths increment the same direction, so a Verilog last-assignment-wins actually hides the bug in this snippet — and the same two paths written as return_acc_q + (a ? 1 : 0) + (b ? 1 : 0), which is what a careful engineer would write, exposes it fully. The safer-looking code is the one that double-counts.
Contract. The consumer promises the producer that advertised capacity is real. Duplication breaks that promise silently.
Failure — and the signature is the point. Compare the two directions:
| Leakage (returns lost) | Duplication (returns manufactured) | |
|---|---|---|
| Sender's belief | pessimistic — fewer credits than real capacity | optimistic — more credits than real capacity |
| Immediate symptom | throughput falls; sender stalls with an empty consumer | none |
| Self-limiting? | yes — the sender simply sends less | no — the sender sends more than the storage can hold |
| When it surfaces | soon, as reduced bandwidth | later, as consumer overflow, arbitrarily far from the cause |
| Data integrity | preserved | lost, if the overflow is not otherwise guarded |
Leakage costs bandwidth; duplication costs correctness, and it costs it at a time and place that has no visible connection to the return path that manufactured the credit. A duplicate generated during a quiet period may not overflow anything until a burst hours later.
Which is exactly why the capacity bound of §8 is worth asserting even though it looks redundant. Nothing in normal operation should push the count above the advertisement, so the property appears to assert the obvious — and it is the only check that fires near the duplication rather than near the overflow.
// The over-return check, close to the source. The capacity bound of §8 catches
// duplication only once the count has climbed past the advertisement, which may
// take many spare credits. This fires on the first duplicate.
// Illustrative — the join key is the verification-only monitor tag of §17.
property p_one_return_per_release;
@(posedge clk) disable iff (!rst_n)
slot_release_event |-> (returns_generated_for_slot == 1);
endproperty
a_one_return_per_release: assert property (p_one_return_per_release);DV. Inject a duplicate return directly and confirm two things: that §8's capacity bound eventually fires, and how many cycles late it is. That number is the argument for the check above. Then run the same injection with the pool deliberately far from full and confirm the capacity bound does not fire at all — which is the case that reaches silicon.
11. Piggybacked Return and the Self-Sustaining Stall
A hazard created by the credit field living in the flit header, which is where Chapter 12.1 §12 first noted it and where it can now be developed.
The credit field is in the flit header. A header exists only when a flit is sent. So credit return in that direction requires traffic in that direction.
Now consider a link where direction A→B is heavily loaded and B→A is idle. B has freed slots and wants to return credits to A. But B has no flits to send, so there is no header to carry the return.
If the design has no way to send a credit-only flit, the following is stable:
- A exhausts its credits and stops sending.
- B has capacity and has freed slots, but no traffic in which to report them.
- A does not send, so B's receive buffer stays as it is.
- Nothing changes. Both sides are healthy; the link is idle; every counter is legal.
This is a livelock built from two reasonable local behaviours — "only send when you have something to send" and "carry credit returns in the header you were sending anyway" — and it satisfies every safety property in the chapter.
The escapes, and a design needs at least one:
| Mechanism | Cost |
|---|---|
| A credit-only flit the receiver can send with no payload | consumes link bandwidth when idle; needs a header format that permits it |
| The Consortium's flit-format description notes a payload field carrying zeros when the Protocol Layer supplies no flit (Ch 12.1 §12), so a flit is framed regardless | the transmit path must not be gated on having payload |
| A timer that forces a return when a residue has waited too long | adds latency in the common case unless the threshold is tuned |
The second row is the important one and it is verified. Because a flit is framed whether or not the Protocol Layer supplied one, the header — and therefore the credit field — flows continuously while the link is operational. The livelock above is not a property of UCIe as described; it is a property of an implementation that gates its transmit path on having payload, which is exactly the optimisation Chapter 12.1 §12 warned against.
Do not gate the transmit path on having payload. The credit machinery rides on the flit stream, so stopping the stream stops the mechanism that would let it restart.
// Illustrative — a diagnostic that makes the livelock visible if it occurs.
// Not a protocol mechanism; a counter that turns an invisible stall into
// evidence (Ch 9.5 §11's argument, applied to the reverse direction).
logic no_return_progress;
assign no_return_progress = (credit_q[DOM_LINK] == '0) // starved
&& obj_pending // work waiting
&& link_operational // link is fine
&& !credit_return[DOM_LINK] // nothing coming back
&& (rx_return_residue != '0); // and slots ARE free
logic [31:0] livelock_cycles_q; // saturating, sticky, diagnostic lifetime12. Credits and Replay Are Different Reservations
The sharpest new idea in the chapter, and it exists only in the link domain because only the link domain has retry.
A replayed flit is retransmitted. In a correctly accounted design it must not consume a second credit.
The reason is the conservation law of §7. The credit was consumed when the object was first sent, and it reserved a slot in the remote receiver. If the flit is retransmitted — because its CRC failed, or because a confirmation was lost — the reservation it made is still the reservation it needs. Consuming a second credit would reserve two slots for one object, and the accounting would drift downward: the producer would run out of credits while the receiver had space.
// WRONG — credit consumed whenever a flit is driven onto the link.
assign credit_consume[DOM_LINK] = flit_transmitted;Failure. Every replay consumes an extra credit. Under a raised error rate the producer's credit count falls monotonically toward zero while the receiver is not filling, so the link throttles itself in proportion to the error rate — and then stops. The signature is distinctive: credits fall, receiver occupancy is low, and retry counts are high. That triple is the fingerprint, and no single counter shows it.
And the mirror-image bug is worse. If a design instead returns a credit when a flit is discarded for CRC failure — reasoning that the object did not arrive, so its reservation is free — then a replay of that object consumes a fresh credit and the accounting balances by luck, until a lost-confirmation retry occurs. In that case the original did arrive and occupy a slot, the credit was returned, and the replay consumes another: two credits returned and one slot occupied, so the producer now believes in capacity that does not exist, and the receiver overflows later. This is the over-advertisement direction, which Chapter 9.5 §2 established is the catastrophic one.
The correct shape ties the credit to the object, not to the transmission:
// Illustrative — credit is consumed once per object, at the point the object
// is committed to the link domain, and NOT on retransmission. The replay
// mechanism re-sends without re-consuming, exactly as it re-sends without
// re-allocating a replay entry (Ch 9.4 §8).
assign credit_consume[DOM_LINK] = link_obj_committed && !is_replay;// Illustrative — a replay consumes no credit. Uses the verification-only
// monitor tag, because a replayed object is the SAME object and the protocol
// carries no field that says so.
property p_replay_consumes_no_credit;
@(posedge clk) disable iff (!rst_n)
(flit_transmitted && is_replay) |-> !credit_consume[DOM_LINK];
endproperty
a_replay_consumes_no_credit: assert property (p_replay_consumes_no_credit);
// Illustrative — exactly one credit consumed per object over its whole life,
// however many times it crossed the link.
property p_one_credit_per_object;
@(posedge clk) disable iff (!rst_n)
credit_consume[DOM_LINK] |-> !credit_charged_mon[link_obj_mon_id];
endproperty
a_one_credit_per_object: assert property (p_one_credit_per_object);And the relationship between the two reservations is worth stating explicitly, because they are easy to conflate:
| Credit | Replay entry | |
|---|---|---|
| Reserves | storage at the far receiver | storage at the local sender |
| Consumed | when the object is committed to the link | when the Adapter accepts the object |
| Released | when the far consumer frees the slot | when transport confirms delivery |
| Affected by a replay | no — one per object | no — one per object (9.4 §8) |
| Runs out means | the far side may be full | this side cannot guarantee recovery |
Both are per-object and both survive replay unchanged. That symmetry is not a coincidence — it is the same principle, that a physical attempt is not a new object, applied to two different resources.
13. Credits and Protocol Classes
Verified: when multiple protocols are supported, the Adapter defines the underlying arbitration mechanism. So a stack carrying CXL.io, CXL.mem and CXL.cache simultaneously has several traffic classes contending for one link — and the question of whether they contend for one credit pool is a design decision with real consequences.
And there is one place where official material does describe per-class credits, which is worth having as an existence proof. Of the UCIe Management Transport specifically, the UCIe 2.0 white paper states: "We define up to eight independent virtual channels to provide quality of service, each with ordered or unordered semantics. Packets are exchanged based on credits, which are initially negotiated during link training." Two things are worth extracting and one is worth refusing:
- Extract: UCIe does, somewhere, combine multiple independent channels, quality of service, and credit-based exchange — so the pool-structure question below is a real UCIe design question and not an imported analogy.
- Extract: in that transport, credits are "initially negotiated during link training" — permission is established at initialisation rather than assumed, which is Chapter 9.5 §7's don't-assume-the-depth rule appearing in official text.
- Refuse: this is the management fabric, not the protocol-layer flit path. Eight virtual channels is a management-transport figure and is not extended here to protocol-layer credit pools, and nothing below depends on it.
Three pool structures, and this is generic microarchitecture rather than a UCIe claim:
| Structure | Utilisation | Isolation | Failure mode |
|---|---|---|---|
| Shared — one pool for all classes | best; any class can use idle capacity | none | one class monopolises, starving others |
| Partitioned — a pool per class | worst; a class's unused credits are stranded | complete | capacity idle while another class starves for want of it |
| Hybrid — a reserved minimum per class plus a shared remainder | good | bounded | more state; the reservation must be sized |
Why this is a correctness question and not only a performance one. Chapter 11.3 §28 established that CXL.cache's response and data channels are pre-allocated and required to make progress, while request channels are credited and may block indefinitely. A shared credit pool at the link domain can break that: if request traffic consumes every credit, a coherence response has no credit to travel on, and Chapter 11.4 §22 showed where that ends — a coherence timeout with no safe recovery.
A shared pool converts a protocol-layer guarantee about forward progress into an implementation-layer race. Where the carried protocol distinguishes classes that must make progress from classes that may block, the credit structure must preserve that distinction.
// WRONG — one shared pool with no reservation, where a class that must make
// progress can be starved by one that may legitimately block.
assign class_may_send[c] = (shared_credit_q != '0);// Illustrative hybrid pool. RESERVED[c] is a per-class minimum; the remainder
// is shared. Symbolic sizes — no UCIe class structure is asserted (§3).
logic [CREDIT_W-1:0] reserved_q [NUM_CLASS]; // per-class guaranteed minimum
logic [CREDIT_W-1:0] shared_q; // common remainder
// A class may send from its reservation, or from the shared pool. Draining the
// reservation first is deliberate: it keeps shared capacity available for
// whichever class needs it, rather than letting one class hoard it.
function automatic logic class_permits(int c);
return (reserved_q[c] != '0) || (shared_q != '0);
endfunction
// And the invariant that makes the reservation meaningful.
property p_reservation_not_borrowed;
@(posedge clk) disable iff (!rst_n)
class_send_fire[c] |-> ((reserved_q[c] != '0) || (shared_q != '0));
endproperty
// The stronger one: a class that must make progress always has SOME permission
// available, under the assumption that its own consumer is draining.
property p_progress_class_never_starved;
@(posedge clk) disable iff (!rst_n || error_injection_active)
(must_progress_class[c] && class_obj_pending[c])
|-> ##[1:MAX_CLASS_WAIT] class_send_fire[c];
endpropertyp_progress_class_never_starved is liveness, with the assumption stated — and §19 is about why that distinction has to be explicit rather than implied.
14. A Credit Is Not an Outstanding-Transaction Slot
A conflation worth its own section, because both are finite, both gate acceptance, and Module 12 spent five chapters on the second one.
| Credit | Outstanding-transaction entry | |
|---|---|---|
| Reserves | storage in a consumer across an interface | a tracking record on the requester's own die |
| Granularity | a transport unit — symbolic, per §3 | one semantic operation |
| Count relationship | several credits may be consumed by one transaction's payload | one entry per transaction regardless of payload size |
| Released | when the consumer frees the slot | when the response is consumed (12.4 §14) |
| Lifetime | link-epoch state in a representative design — re-established rather than carried forward (§15) | per-transaction; survives recovery (12.4 §24) |
| Running out means | "the far storage may be full" | "we cannot track another operation" |
Four consequences of the difference.
They retire at completely different times. A credit for a request's flit is released when the far Adapter's buffer drains — possibly long before the transaction completes. The transaction entry is released when the response is consumed. So credit occupancy and transaction occupancy are unrelated numbers, and comparing them is Chapter 11.5 §12's mistake in the resource plane.
A multi-beat write consumes many credits and one transaction entry. So a workload of large writes exhausts credits first; a workload of small reads exhausts transaction entries first. The binding resource depends on the traffic shape, which is why the acceptance gate must test both and the diagnostics must distinguish them.
The two have different recovery lifetimes, and the direction of the difference matters more than the exact rule. Whatever permission described the far side's storage refers to a link that has just re-initialised, while a transaction entry refers to an operation the requester still owes a response to — Chapter 12.4 §24's table applies, and §15 states what is and is not verified about which state UCIe retains. A design that lumps them into one reset domain either destroys transactions on a link event or fails to re-establish permission.
And they fail with different symptoms. Credit starvation stalls the link; transaction-table exhaustion stalls acceptance while the link may be idle. §22's taxonomy separates them, and a design with one combined "cannot accept" signal cannot.
// Illustrative — the acceptance gate names every resource separately, so a
// stall has an attributable cause. This is Chapter 12.1 §10's gate with the
// credit term expanded per domain.
assign accept_new_operation =
txn_table_space // Ch 12.4 §21 — per-transaction
&& id_available // Ch 12.4 §12 — identity space
&& domain_permits(DOM_FDI) // §8 — local Adapter storage
&& domain_permits(DOM_LINK) // §8 — remote Adapter storage
&& replay_space // Ch 9.4 §11 — local retention
&& link_operational;15. Credit Epochs Across Recovery
Chapter 9.5 §13 established that credits are link-epoch state and must be re-established when the link re-initialises. Stacking the domains adds two things it could not say.
Different flow-control state can have different recovery lifetimes — and the implementation must not mix them. This is the conceptual lesson, and it is the one to take away:
- Some flow-control state is link-epoch state. Whatever permission was established for the far side's storage describes a link that has just been re-initialised, so it has to be re-established rather than carried forward.
- Some local resource state may legitimately survive. An on-die interface whose storage was never disturbed still has real, occupied slots; re-advertising its full capacity would claim space that is currently in use.
- The unsafe outcome is a mixture. A design must never operate on a combination of stale permission from before the event and newly initialised permission from after it, in either direction: stranded permission underuses the link, and duplicated permission over-advertises capacity that does not exist — Chapter 9.5 §2's catastrophic direction.
And a stale return can arrive after re-initialisation. A credit return generated under the old epoch may be in flight when the new advertisement lands. If it is applied, the producer's count exceeds the new capacity — the over-advertisement direction.
// Illustrative epoch tagging. The EPOCH IS A LOCAL VERIFICATION AND CONTROL
// CONSTRUCT — no claim is made that UCIe transports such a field (§3).
logic [EPOCH_W-1:0] credit_epoch_q [NUM_DOM];
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
for (int d = 0; d < NUM_DOM; d++) credit_epoch_q[d] <= '0;
end else begin
// Only the domain whose capacity is being re-advertised advances.
if (cap_advert_valid[DOM_LINK])
credit_epoch_q[DOM_LINK] <= credit_epoch_q[DOM_LINK] + EPOCH_W'(1);
end
end
// A return is applied only if it belongs to the current epoch. Where the epoch
// cannot be carried with the return, the equivalent is to DISCARD returns for
// a defined window after re-advertisement — a coarser but implementable rule.
assign credit_return[DOM_LINK] = raw_return_valid[DOM_LINK]
&& (raw_return_epoch == credit_epoch_q[DOM_LINK]);Architecture. One counter per domain, advanced only when that domain re-initialises. It exists to make "this return belongs to a link that no longer exists" a decidable question.
State. Per-domain, per-epoch lifetime — and it must not be reset by anything narrower than the event that re-advertises capacity.
Cycle behaviour. Advanced on re-advertisement; compared on every return.
Contract. The receiver's accumulator (§10) must also clear on re-initialisation, or residues from the old epoch are reported into the new one.
Failure. §16.
DV. Force a re-advertisement with returns in flight, and verify the count equals the newly advertised capacity exactly — not capacity plus the stragglers.
// Illustrative — a stale return does not inflate the count.
property p_stale_return_rejected;
@(posedge clk) disable iff (!rst_n)
(raw_return_valid[d] && (raw_return_epoch != credit_epoch_q[d]))
|-> !credit_return[d];
endproperty
a_stale_return_rejected: assert property (p_stale_return_rejected);
// Illustrative — after re-advertisement the count is exactly the advertisement.
property p_readvert_sets_exact_capacity;
@(posedge clk) disable iff (!rst_n)
cap_advert_valid[d] |=> (credit_q[d] == $past(advertised_capacity[d]));
endproperty
a_readvert_sets_exact_capacity: assert property (p_readvert_sets_exact_capacity);16. Wrong RTL — Re-Initialising One Side Only
// WRONG — the sender resets its credits on a link event; the receiver keeps
// its buffer contents and its occupancy.
always_ff @(posedge clk) begin
if (link_recovery_event) credit_q[DOM_LINK] <= cap_q[DOM_LINK]; // "full again"
endArchitecture. One side of a two-sided contract re-initialised unilaterally. The credit count is a replica of a fact that lives at the receiver (Chapter 9.5 §2), and resetting a replica does not reset the fact.
Failure. The receiver still holds n undrained entries. The sender now believes the full capacity is free. It sends up to capacity more objects into capacity - n slots, and the receiver overflows by exactly the number of entries it was holding. The overflow happens promptly, under load, with no credit assertion firing — because from the sender's own accounting nothing was exceeded.
The mirror error is quieter and also wrong: the receiver flushes its buffer on the recovery and the sender keeps its old, partially-spent count. Now permission is permanently short by the number of slots that were occupied, so throughput is reduced forever with nothing reporting it.
The rule that covers both:
Capacity is re-established by agreement, not by assumption. Either both sides re-initialise as part of one defined sequence, or neither does. A design in which one side can re-initialise alone has no conservation law.
And note which reset scope this belongs to. Chapter 12.4 §25's three reset kinds apply: this is link reset territory, so link-domain credits are in scope and FDI/RDI credits and transaction state are not.
17. The Multi-Domain Credit Scoreboard
Chapter 9.5 §18 built the reference credit machine for one domain. The stacked version is that model instanced per domain, plus three checks that only exist once there is more than one.
JOIN KEY — the verification-only monitor tag, because a replayed object is the
same object and no protocol field says so (§12).
PER DOMAIN d:
advertised[d] — from the observed advertisement, per epoch
consumes[d] — counted at the consume event
returns[d] — counted at the applied return event
expected_credit[d] = advertised[d] - consumes[d] + returns[d]
occupancy[d] — consumer-side, observed independently
in_flight[d] — consumed but not yet occupying
PER OBJECT (by monitor tag):
credits_charged[] — per domain; each must be exactly 1 for domains it crossed
transmissions — may exceed 1 under replay
DIAGNOSTIC (sticky, survives recovery):
min_credit[d], max_credit[d], underflow_attempts[d], overflow_attempts[d],
epoch_count[d], last_consume_cycle[d], last_return_cycle[d],
return_residue_high_waterThe six checks, and what each catches.
credit_q[d] == expected_credit[d], every cycle, per domain. The base check from 9.5, now three times. It catches the two-independent-assignments drift and the mis-indexed decrement of §9.
The conservation equation per domain: held + in flight + occupied equals advertised. Catches a return generated for a slot that was not freed, and a consume that reserved nothing.
No domain's counters move on another domain's event. Catches §8's shared counter and a mis-wired return, both of which keep every count bounded so the capacity assertion passes.
credits_charged[DOM_LINK] == 1 per object, with transmissions >= 1. Catches §12 in both directions — the extra consume on replay, and the return-on-discard that later over-advertises. The diagnostic value is in objects where transmissions exceed one and charges equal one, which is the mechanism visibly working.
After every re-advertisement, the count equals the advertisement exactly. Catches §16 and stale returns.
And return_residue_high_water is below the quantum at end of test. Catches §10's constant-offset leak, which no drift check finds because the offset never grows.
On what the model must not do. It must not derive occupancy[d] from the design's own occupancy counter — that is Chapter 12.3 §21's suspect. Observe pushes and pops at the interface.
18. Diagnostics Worth Building
// ILLUSTRATIVE diagnostic state. Not a UCIe mechanism. Each exists to make one
// invisible condition attributable, and all of it has DIAGNOSTIC lifetime —
// surviving link recovery, cleared only by a broad deliberate reset.
logic [CREDIT_W-1:0] min_credit_q [NUM_DOM]; // low-water mark
logic [31:0] starved_cyc_q [NUM_DOM]; // saturating, per domain
logic [31:0] underflow_q [NUM_DOM]; // attempts, not wraps
logic [15:0] epoch_count_q [NUM_DOM];
logic [2:0] first_starve_domain_q; // WRITE-ONCE — see below
logic first_starve_valid_q;Architecture. Per-domain instrumentation, because "the sender is stalled" needs a domain before it becomes a diagnosis.
first_starve_domain_q is the one to notice. Starvation cascades: the link domain runs dry, so the Adapter stops draining its FDI storage, so the FDI domain runs dry too. By the time anyone looks, every domain is starved and the last one to starve is the most visible. A write-once field capturing which domain starved first is the difference between a diagnosis and a survey — the same argument Chapter 12.4 §19 makes for first-failure preservation.
State. Diagnostic lifetime throughout. min_credit_q in particular must survive a recovery, because a low-water mark reached before the recovery is exactly the evidence a post-recovery investigation needs.
Failure. Saturating without a sticky flag makes a saturated counter indistinguishable from a stopped one. Recording the latest starving domain destroys the attribution.
19. Safety and Liveness, Separated
Credits provide safety and cannot provide liveness, and it is worth being precise about the boundary because it determines what can be asserted.
Safety — assertable, and every one of these is an invariant:
- no consume without permission, per domain (§9);
- the count never exceeds advertised capacity, per domain (§8);
- no object is charged twice, however many times it is transmitted (§12);
- a stale return is never applied (§15);
- after re-advertisement the count is exactly the advertisement (§15).
Liveness — requires assumptions, and they must be stated:
- if the consumer drains and the link is operational, permission eventually returns;
- a class that must make progress is eventually granted (§13);
- a starved producer with pending work eventually sends.
Why the split is not academic. §11's livelock satisfies every safety property above, forever. No consume without permission — there are no consumes. Count within capacity — the count is zero. No double charge, no stale return. The link is idle and correct, and only a liveness property with stated assumptions, or a diagnostic counter, distinguishes that from a link with nothing to do.
// Illustrative liveness, with assumptions explicit. The bound is an
// engineering choice, not a UCIe value.
//
// ASSUMPTIONS: the consumer is draining, the link is operational, the peer is
// returning credit for freed slots, and no error injection is active.
property p_starved_producer_eventually_sends;
@(posedge clk) disable iff (!rst_n || error_injection_active)
(obj_pending && link_operational && consumer_draining)
|-> ##[1:MAX_STARVE_CYCLES] obj_sent;
endpropertyOn the temptation to drop the disable iff. A liveness property without its assumptions fires during every injection test, gets waived, and then protects nothing — which is worse than not writing it. State the assumptions in the property and in a comment.
20. Coverage
// Illustrative credit coverage. Not UCIe-defined. Every bin reaches a failure
// named in this chapter.
covergroup cg_credit @(posedge clk iff credit_event);
cp_domain : coverpoint event_domain { bins fdi = {DOM_FDI}; bins link = {DOM_LINK}; bins rdi = {DOM_RDI}; }
cp_level : coverpoint credit_q[event_domain] {
bins zero = {0}; bins one = {1}; bins mid = {[2:$-1]}; bins max = {CREDIT_MAX};
}
cp_event : coverpoint {credit_consume[event_domain], credit_return[event_domain]} {
bins idle = {2'b00}; bins consume = {2'b10}; bins ret = {2'b01}; bins both = {2'b11};
}
cp_replay : coverpoint object_was_replayed; // §12
cp_residue : coverpoint return_acc_q; // §10 — every value to QUANTUM
cp_epoch : coverpoint credit_reinit_seen; // §15
cp_stale : coverpoint stale_return_seen; // §15 — MUST be non-zero
cp_class : coverpoint sending_class; // §13
cp_starve : coverpoint first_starve_domain_q; // §18
cp_livelock : coverpoint idle_link_with_free_slots; // §11
// Simultaneous consume and return at every level, per domain — the arm that
// the two-assignment bug loses.
x_both_level : cross cp_event, cp_level, cp_domain;
// A replay at every credit level — §12's extra-consume bug bites hardest
// near zero, where it converts into an immediate stall.
x_replay_level : cross cp_replay, cp_level;
// Re-initialisation with returns in flight — §15 and §16.
x_epoch_stale : cross cp_epoch, cp_stale;
// Two domains active in the same cycle doing opposite things — §9's indexing.
x_two_domains : cross cp_domain, cp_event;
// A residue below the quantum at end of test is §10's constant-offset leak.
x_residue_starve : cross cp_residue, cp_starve;
endgroupTwo bins that must be non-zero rather than zero, because their value is proof a defence ran: cp_stale (a stale return was actually generated and rejected) and the combination of a replay with a charge count of one. A regression reporting zero for either has not tested the mechanism, and that is indistinguishable from the mechanism being broken.
And one that must stay at zero: cp_livelock. An idle link with free slots and pending work should never be observed; writing the bin converts an assumption into a checked fact.
21. Flagship Trace — Two Domains and a Replay
Illustrative throughout. FDI capacity 2, link capacity 4, return quantum 2, link round trip 3 cycles.
| Cyc | FDI cr | Link cr | FDI occ | In flight | Remote occ | Residue | Event |
|---|---|---|---|---|---|---|---|
| 0 | 2 | 4 | 0 | — | 0 | 0 | both domains advertised |
| 1 | 1 | 3 | 1 | A | 0 | 0 | A accepted at FDI, committed to link |
| 2 | 0 | 2 | 2 | A, B | 0 | 0 | B likewise — FDI now full |
| 3 | 0 | 2 | 2 | A, B | 0 | 0 | Protocol Layer blocked at FDI, link has credit |
| 4 | 1 | 2 | 1 | A, B | 1 | 0 | A leaves FDI storage → FDI credit returns |
| 5 | 0 | 1 | 2 | B, C | 1 | 0 | C accepted and committed |
| 6 | 0 | 1 | 2 | B, C | 2 | 0 | B arrives remotely |
| 7 | 1 | 1 | 1 | C | 2 | 0 | B left FDI storage earlier; FDI return lands |
| 8 | 1 | 0 | 1 | C, D | 2 | 0 | D committed — link credits exhausted |
| 9 | 1 | 0 | 1 | C, D | 2 | 0 | C's flit fails CRC — discarded |
| 10 | 1 | 0 | 1 | C, D | 2 | 0 | replay: C re-sent, no credit consumed |
| 12 | 1 | 0 | 1 | D | 3 | 0 | C arrives on the retry |
| 13 | 1 | 0 | 0 | — | 4 | 0 | D arrives — remote buffer full |
| 14 | 1 | 0 | 0 | — | 3 | 1 | remote consumer drains one; residue 1 |
| 15 | 1 | 0 | 0 | — | 2 | 2 | drains another → quantum reached |
| 16 | 1 | 0 | 0 | — | 2 | 0 | return of 2 sent; residue cleared |
| 18 | 1 | 2 | 0 | — | 2 | 0 | return lands — 2 credits at once |
Seven things to read off it, and together they are the chapter.
Cycle 3 is the section-6 lesson made concrete. The link has two credits and the Protocol Layer cannot send, because FDI is the binding constraint. A design with one counter (§8) cannot express this state at all, and a diagnostic that reports "credits available: 2" is actively misleading.
Cycle 4 shows a return in one domain and nothing in the other. FDI credit returns when the Adapter forwards the object onward; the link credit is still committed because the remote slot is still reserved. Two domains, two independent release events for one object.
Cycle 8: link credits reach zero while FDI has one. The binding constraint has swapped. Over the trace it swaps twice, which is why the acceptance gate is a conjunction re-evaluated every cycle rather than a comparison against the scarcest resource at design time.
Cycles 9–10 are §12. C's flit is discarded and re-sent, and no credit is consumed for the retransmission. With the wrong RTL, link credits would go to −1 and wrap to maximum.
Cycles 14–16 are §10. The consumer frees two slots and no credit returns until the quantum is reached. For one cycle at row 14 the receiver has three free slots and the sender has zero credits and cannot use any of them.
Cycle 18: two credits arrive at once. Permission is coarse. A sender needing one credit waited for two to become available.
And the row that never appears: no cycle where a count exceeds its capacity, and no cycle where remote occupancy plus in-flight plus held credits differs from 4 in the link domain. Check row 13: 0 held + 0 in flight + 4 occupied = 4. The invariant holds at every row, per domain, which is what §17's model verifies continuously.
22. Debug Taxonomy
| Symptom | Domain | Diagnosis |
|---|---|---|
| Protocol Layer stalled, link credits available | FDI | local Adapter storage is the constraint — §21 cycle 3, not a link problem |
| Link credits at zero, remote buffer nearly empty | link | credit leakage (9.5 §16), or §10's residue below quantum |
| Credits fall steadily, retries high, remote occupancy low | link | §12 — a replay consuming a credit. This triple is the fingerprint. |
| Remote overflow with no credit assertion firing | link | over-advertisement: §16's one-sided reinit, or §12's return-on-discard |
| Credits usable only in steps | link | §10's quantised return — expected, and it sizes the pool |
| Link idle, both sides healthy, work pending | link | §11's livelock — a transmit path gated on having payload |
| One protocol class never progresses, others fine | class pool | §13 — shared pool with no reservation. Not a deadlock. |
| Throughput a fraction of rated, everything correct | any | under-credited for the round trip (9.5 §11), or §10's accumulation latency |
| Stall immediately after a link recovery | link epoch | §15 — the accumulator or the count not re-initialised consistently |
| Cannot accept new operations, link idle | not a credit problem | §14 — transaction table or identity space is the constraint |
The third and last rows are the two worth memorising. The triple — falling credits, high retries, low remote occupancy — has exactly one cause and it is not visible in any single counter. And the last row is the misdiagnosis this chapter exists to prevent: an acceptance stall with an idle link is usually not about credits at all.
23. Debug Checklist
- Which domain is starved? §18's first-starve field, not the currently-starved one.
- What is the credit unit in that domain, and what storage backs it? §3 — and if the answer is not written down, that is the finding.
- What was the advertised capacity, and when? Per domain, per epoch.
- What does the count read, and what does the reference equation predict? §17.
- When was the last consume, and the last return? A long gap on the return side points at §10 or §11.
- Did a consume and a return coincide? Chapter 9.5 §6 — and check the domain indexing (§9).
- Did the count hit zero, and is that legitimate? Zero with an empty consumer is a leak; zero with a full consumer is the mechanism working.
- Was there an underflow attempt? Not a wrap — the attempt counter.
- Did the count ever exceed capacity? §8's bound.
- What is the consumer's occupancy, independently observed? Not from the design's counter.
- Is the return accumulator holding a residue? §10 — and is there a flush condition at all?
- Did a replay occur, and was a credit consumed for it? §12's fingerprint.
- Did a link recovery occur, and did both sides re-initialise? §16.
- Could a return be stale? §15.
- Is the constraint actually a credit? §14 — or is it the transaction table, the identity space, or replay capacity?
- Does the conservation equation balance, per domain? §7 — three equations, checked separately.
Step 1 and step 15 are the two that save the most time. Step 1 because starvation cascades and the visible domain is usually not the causal one; step 15 because the most common "credit problem" is not one.
24. Common Misconceptions
"A credit is just a delayed ready signal." A ready signal reports whether a local sink can accept now. A credit is a reservation on storage across an interface that the producer cannot observe, updated by messages that have already crossed. Chapter 9.5 §12 developed this; the addition here is that a stack has several such reservations and they are not interchangeable (§6).
"One credit counter is enough for the stack." Three domains with three capacities, three round trips and three owners. One counter mis-sizes two of them and makes local progress appear to grant remote permission (§8).
"A credit is always one packet, or one flit." The granularity is a specification matter, and this chapter could not verify UCIe's (§3). A multi-beat payload may consume many credits and occupy one transaction entry (§14).
"Return a credit when the object arrives." It returns when the slot is free — Chapter 9.5 §9. And over the link the return is additionally quantised, so it may not return even then (§10).
"A replay should consume another credit." A replay is another physical attempt at the same object, so its reservation is the reservation it already holds. Charging again throttles the link in proportion to the error rate; returning on discard is worse, because a lost-confirmation retry then over-advertises (§12).
"A shared credit pool is always better utilisation." It is, and it can break a protocol-layer forward-progress guarantee: where the carried protocol distinguishes channels that must make progress from channels that may block, a shared pool lets the second starve the first (§13).
"Correct credit accounting guarantees forward progress." Credits guarantee safety only. A credit reserves storage; it does not promise the storage will ever be released, and §11's livelock satisfies every safety property in the chapter indefinitely (§6, §19).
"Credit starvation and deadlock are the same." Starvation is one participant losing repeatedly while others progress (9.5 §15). §11's livelock is neither — it is a stable state with no cyclic dependency and no participant progressing, created by an optimisation.
"More credits always improve throughput." Up to the bandwidth-delay product, yes. Beyond it, extra credits buy nothing and cost receive storage — and they worsen the blast radius of an over-advertisement bug, because the sender can commit more before anything notices.
"Resetting one side's credit state is harmless." The count is a replica of a fact at the receiver. Re-initialising the replica alone over-advertises by exactly the receiver's occupancy, and the overflow follows promptly with no credit assertion firing (§16).
"Saturating the credit counter makes it safe." It makes the failure invisible. Chapter 9.5 §8 is the argument; the addition here is that the assertion must name the domain, because a link-domain underflow implicates the far die and an FDI-domain underflow does not (§9).
"The bandwidth-delay product is the pool size." It is the floor for a smooth producer, a regular consumer and a fixed return latency — three things no real domain has. Headroom covers burstiness, arbitration delay, return batching, variable consume latency, clock crossings and service jitter (§4), and the low-water mark is how you find out whether you provisioned enough.
"A duplicated credit is the mirror image of a leaked one." They are not symmetric. Leakage is pessimistic, self-limiting and shows up promptly as reduced bandwidth; duplication is optimistic, unbounded, silent until a consumer overflows, and surfaces arbitrarily far from the return path that caused it (§10). One costs bandwidth; the other costs correctness.
25. Understanding Check
26. Summary and What Comes Next
A credit is a reservation on one specific resource, in one specific domain, held by one specific owner — and permission in one domain grants nothing in any other.
Chapter 9.5 built the credit machine for one sender and receiver. This chapter's addition is the stack: several independent flow-control domains — in a representative layered implementation, at FDI, the link, and RDI — with different capacities, different round trips, and different owners; one conservation equation per domain, none of which implies the others; and one object holding reservations in several domains at once. The decomposition is architecture (§4); what official UCIe material does and does not establish is stated in §3.
The mechanisms that only exist once the domains are stacked: a counter per domain, because a shared counter lets local progress manufacture remote permission. A replay consumes no credit, because a physical attempt is not a new object — and returning one on discard is worse, because a lost-confirmation retry then over-advertises. Class-aware pools, because a shared pool can break a protocol-layer forward-progress guarantee. A per-domain epoch, because only the link domain re-initialises and a stale return inflates the count. And credits distinguished from outstanding-transaction slots, because the most common credit stall is not one.
The verified fact that shapes the rest: the flit header carries a one-bit Credit field. One bit cannot carry the value "three" — but that alone does not prove returns are batched, and §10 refuses the inference. Where a return mechanism does quantise, the consequences are worth knowing as a pattern: a longer effective round trip, permission that is coarse near zero, and a residue that needs a flush condition or it withholds credits permanently.
And the limit worth carrying: credits provide safety and cannot provide liveness. A credit reserves storage; it does not promise the storage will ever be released. §11's idle link satisfies every safety property in this chapter forever.
Credits are only as correct as the storage behind them, and everything above has treated that storage as a number — an advertised capacity, a depth, an occupancy. The next chapter opens it up: how deep that storage must actually be, how its occupancy is tracked safely, and why physically free is not the same as allocatable:
- 13.2 — Buffer Management — sizing and policing receive-side buffers.
Browse the full path on the UCIe tutorials index.