UCIe · Module 9
The Streaming Model
Why UCIe offers a lightweight packet-stream transport for protocols that are neither PCIe nor CXL — raw mode's cost, what streaming flits buy, payload opacity, the packing shim and its deadlock, message framing, and inherited reliability's consequences.
Module 8 finished a link. Reset established local state, discovery found a peer, training qualified the physical resources, calibration chose an operating point, and the state machine reached ACTIVE. There is now a working, reliable, wide connection between two dies.
The question this module opens is what runs across it — and specifically, what runs across it when the answer is not "PCIe" and not "CXL".
That case is not a niche. Chapter 1.7 and the System Partitioning material argued that chiplet boundaries appear wherever a monolithic die was cut, and the traffic crossing those cuts is frequently an internal fabric protocol: a coherence protocol, a proprietary NoC, an accelerator command stream, something a company has been evolving for a decade. None of it is PCIe. All of it needs to cross a die boundary reliably.
The streaming model is UCIe's answer to that, and understanding why it exists tells you more about UCIe's design philosophy than any single mechanism so far.
1. The One-Sentence Model
Streaming lets a protocol that UCIe does not understand ride inside flits that UCIe does understand — so it inherits the Adapter's error detection and replay without the Adapter having to know what it is carrying.
Two halves, and both matter. Inherits is the value proposition: reliability is expensive to build and easy to get subtly wrong, and it already exists in the Adapter. Without knowing what it is carrying is the architectural discipline: the payload stays opaque, which is what keeps this a general mechanism rather than a series of special cases.
2. What Raw Mode Costs
To see why streaming exists, look at what came before it.
UCIe has always been able to carry arbitrary content — that is raw mode, and the official Consortium description of it is blunt: raw mode "bypasses many of the functions of UCIe, including error detection and replay."
Raw mode gives you the physical link. Everything Module 7 and Module 8 built — the trained lanes, the deskewed alignment, the calibrated operating point, the state machine — is yours to use. What you do not get is the Adapter's transport machinery.
The consequence for anyone carrying a custom protocol:
| Raw mode | What you must then build | |
|---|---|---|
| Physical link, trained and calibrated | provided | — |
| Framing of your content | your problem | your own framing scheme |
| Error detection | bypassed | your own CRC, sized and placed correctly |
| Replay on error | bypassed | retry buffer, sequence numbers, acknowledgement, recovery |
| Flow control | your problem | your own credit or backpressure scheme |
| Interoperability of any of the above | none | — |
That last row is the sharp one. Two teams both using raw mode have both built reliability, and they have built different reliability — so their chiplets cannot talk to each other even though both are "UCIe". Chapter 2.x's interoperability argument, arriving in a new place.
Raw mode is not a lesser mode. It is the right choice when you genuinely want the wires and nothing else. It is the wrong choice when what you actually wanted was reliability you did not feel like reimplementing.
3. What Streaming Adds
The Consortium's description of the UCIe 1.1 addition is precise and worth reading closely: support for streaming flits "allows other protocol messages to be mapped into the payload portion of flits that are compatible with PCIe and CXL", which "allows for streaming protocols to reuse the existing logic in UCIe for error detection and replay."
Unpack that.
"Mapped into the payload portion." Your protocol's messages go inside the payload field of a flit. They are not translated, not interpreted, not reformatted — they are carried.
"Flits that are compatible with PCIe and CXL." The flit itself has the same shape the Adapter already handles. That is the entire trick: by making the container familiar, the machinery built for PCIe and CXL applies unchanged.
"Reuse the existing logic for error detection and replay." The Adapter "inserts flit headers and cyclic redundancy check (CRC) information into the flits, as well as CRC detection and replay mechanisms." You do not implement CRC. You do not implement retry. You do not negotiate a replay protocol with the far die.
The motivation the Consortium gives is concrete — enabling "standardization on the means of chiplet communication of the CHI protocol over a highly reliable connection" — and CHI is exactly the shape of protocol this is for: a real, complex, non-PCIe fabric protocol that people want to run between chiplets.
4. Payload Opacity Is the Whole Design
The property that makes streaming general rather than a collection of special cases:
The Adapter protects the payload without interpreting it.
CRC is computed over bytes. Replay retransmits bytes. Neither operation needs to know whether those bytes are a coherence request, a command descriptor, or a compressed tensor. That is why one mechanism serves every protocol that fits the container.
The discipline that follows is strict, and it is worth stating as a rule because violating it is tempting:
The Adapter must not inspect the payload. The moment transport behaviour depends on payload content — a CRC that skips certain fields, a replay that reorders based on message type, a flow-control decision keyed on an opcode — the mechanism stops being general and becomes a second protocol mapping with extra steps.
The protocol must not depend on transport internals. Your fabric must not assume a particular flit size, a particular CRC polynomial, or a particular replay depth. Those are the Adapter's, they vary by revision and configuration, and depending on them re-couples what streaming decoupled.
This is Chapter 5.4's layer-ownership argument, arriving in its cleanest form. The interface between your protocol and UCIe is a payload boundary: you hand over bytes and a length, and you get bytes and a length at the far end, reliably. Everything else is not yours.
5. Where This Sits Architecturally
Chapter 5.2's layering, with streaming located in it:
| Layer | Role for streaming traffic |
|---|---|
| Your protocol | generates and consumes messages; owns their semantics entirely |
| Packing / shim (yours) | fits messages into payload-sized units; handles the size mismatch (§6) |
| D2D Adapter | inserts flit headers and CRC; detects errors; replays; flow-controls |
| Physical layer | everything in Modules 7 and 8 |
The row that is easy to miss is the shim, because it is not UCIe's and it is not your protocol's — it is the piece you have to write, and §6 is about why it is more interesting than it sounds.
What streaming does not give you. Being explicit prevents disappointment:
- Not semantics. UCIe will not order your messages by your rules, deduplicate them, or understand their dependencies. Chapter 9.3 examines what ordering the transport does provide.
- Not a protocol. You still design your own request/response structure, your own deadlock freedom, your own coherence — if any.
- Not zero overhead. Flit headers and CRC consume bandwidth. Chapter 7.4 §3's efficiency factor is not zero, and it is not free.
- Not automatic interoperability of your protocol. Two dies both using streaming interoperate at the transport level. Whether they understand each other's payloads is entirely up to them.
That last point deserves emphasis because it is the most common misreading: streaming standardises how your protocol crosses the link, not what your protocol is.
6. The Size Mismatch Is Yours
The first real engineering problem, and the one the shim exists for.
Your protocol has its own message sizes — probably several, probably not round numbers, probably not related to anything UCIe chose. The flit payload has a size the Adapter defines: the Consortium describes the streaming flit format as providing 236 or 250 bytes of payload for CHI packed flits. Those two numbers will not divide your messages evenly, and there is no reason they should.
So something must reconcile them, and that something is yours.
// Illustrative streaming shim RTL — not a UCIe-defined structure.
// Packs variable-size protocol messages into fixed-size flit payloads.
localparam int PAYLOAD_BYTES = 236; // revision/format specific
localparam int PAYLOAD_W = PAYLOAD_BYTES * 8;
localparam int OCC_W = $clog2(PAYLOAD_BYTES + 1);
logic [PAYLOAD_W-1:0] pack_buf_q; // payload under construction
logic [OCC_W-1:0] pack_occ_q; // bytes currently packed
logic payload_ready; // this payload may be handed down
// A payload is ready when it is full, or when the protocol says do not wait.
assign payload_ready = (pack_occ_q == OCC_W'(PAYLOAD_BYTES)) ||
(flush_request && (pack_occ_q != '0));Architecture. Two independent size regimes meet. Neither side can be told to change, so a buffer absorbs the mismatch.
State. A payload-sized assembly buffer and a byte occupancy counter.
Cycle behaviour. Messages append to the buffer and advance the occupancy; when the payload fills, or a flush is requested, it is handed down and the buffer restarts.
Contract. The Adapter receives a payload it does not interpret. The protocol receives, at the far end, the same bytes in the same order.
Failure — and this is the interesting one. Consider what happens with no flush path. A protocol message arrives, is packed, and the payload is not yet full. No further messages arrive because the protocol is waiting for a response to the message now sitting in the buffer. The link is idle, the buffer is partially full, and nothing will ever complete it. That is a deadlock created entirely by the shim, and it is invisible in any test that offers continuous traffic.
DV. Offer exactly one message and confirm it is transmitted. Then offer one message and stop, and confirm the same. A packing shim tested only with saturating traffic has not been tested.
The latency-versus-efficiency trade the flush exposes. Waiting to fill a payload maximises bandwidth efficiency and adds latency to whatever is already buffered. Flushing immediately minimises latency and wastes payload capacity on partially filled flits. Neither is universally right — a coherence protocol's latency-critical requests want the flush, a bulk stream wants the fill — and a good shim usually makes it adaptive: flush when the buffer has been non-empty for longer than a threshold, fill otherwise.
// Illustrative — a timer that turns "waiting for more" into "send what we have".
logic [FLUSH_W-1:0] pack_age_q;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) pack_age_q <= '0;
else if (pack_occ_q == '0) pack_age_q <= '0; // nothing waiting
else if (payload_ready) pack_age_q <= '0; // just handed down
else if (!(&pack_age_q)) pack_age_q <= pack_age_q + 1'b1;
end
assign flush_request = (&pack_age_q) || protocol_urgent;Why saturating rather than wrapping. Chapter 7.6 §11's rule: a wrapping age counter would periodically make a long-waiting payload look freshly arrived, so the flush would never fire — the exact failure mode the timer exists to prevent.
7. Message Boundaries Do Not Survive by Themselves
The second engineering problem, and the one people discover late.
The Adapter delivers payload bytes. It does not deliver your messages, because it does not know what a message is. If two messages were packed into one payload, or one message spans two payloads, then the receiver must reconstruct the boundaries — and nothing in the transport helps.
// Illustrative streaming shim RTL — not a UCIe-defined encoding.
// A minimal framing header the SHIM adds inside the payload, so the far end
// can find message boundaries. This is yours; UCIe does not define it.
typedef struct packed {
logic [15:0] msg_len; // bytes in this message
logic continues; // this message is continued in the next payload
logic first; // this is the start of a message
} shim_hdr_t;Architecture. Payload opacity cuts both ways: because the Adapter does not interpret your bytes, it cannot delimit them either. Framing is the price of opacity.
State. Per-message length and continuation tracking on both sides.
Cycle behaviour. The transmitter emits a header before each message; the receiver consumes headers to find boundaries and reassemble spanning messages.
Contract. Both ends must agree on this framing exactly — and note that this agreement is not UCIe's. It is a private contract between your two chiplets, which means it is also the thing that stops two different vendors' streaming chiplets from interworking even though both use UCIe correctly.
Failure. A framing disagreement produces the most confusing possible symptom: CRC passes, the transport reports success, and the payload is garbage. The Adapter did its job perfectly — the bytes are exactly the bytes that were sent — and the receiver's interpretation of them is wrong. Every transport-level diagnostic looks healthy.
If the transport reports clean and the data is nonsense, suspect framing before you suspect the link. This is the streaming-mode counterpart of Chapter 7.3's mapping signature, and it has the same character: deterministic corruption with zero error counters.
8. What the Adapter Gives You, and What That Implies
Inheriting reliability is not the same as not thinking about it. The Adapter's CRC and replay have properties your protocol must be designed around.
Replay retransmits. Which means the far end may see the same bytes twice if an acknowledgement was lost, or may see a gap filled later. Your protocol sits above a transport that recovers by resending, and it must tolerate the timing that implies. Chapter 9.4 covers the reliability contract properly.
CRC detects; it does not correct. An error means a retransmission, which means added latency at unpredictable moments — Chapter 7.6's error rate becoming Chapter 7.4's latency variance.
Replay consumes bandwidth. Retransmitted flits occupy the link. A link running near capacity with a non-trivial error rate spends real bandwidth on data already sent, which is one of the entries in Chapter 7.4 §19's low-bandwidth checklist.
Replay implies buffering. The Adapter retains transmitted flits until they are acknowledged, so there is a finite window of outstanding data — and when it fills, backpressure propagates to you. Chapter 9.5 covers flow control.
// Illustrative shim-side observability — not UCIe-defined registers.
// The shim cannot see inside the Adapter, but it can measure what it feels.
logic [31:0] payloads_sent_q; // saturating
logic [31:0] payload_stall_cycles_q; // cycles the Adapter refused a payload
logic [15:0] max_pack_age_q; // worst observed fill latency
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
payloads_sent_q <= '0;
payload_stall_cycles_q<= '0;
max_pack_age_q <= '0;
end else begin
if (payload_accepted && !(&payloads_sent_q))
payloads_sent_q <= payloads_sent_q + 1'b1;
if (payload_ready && !payload_accepted && !(&payload_stall_cycles_q))
payload_stall_cycles_q <= payload_stall_cycles_q + 1'b1;
if (pack_age_q > max_pack_age_q)
max_pack_age_q <= pack_age_q; // high-water mark
end
endArchitecture. Streaming deliberately hides the transport, so the shim's only view of link health is how the transport behaves toward it. Instrument that.
State. Two saturating counters and a high-water mark.
Cycle behaviour. Stall cycles accumulate whenever a payload is ready and not accepted; the age high-water mark records the worst fill latency observed.
Contract. Diagnostics only — never a functional dependency (Chapter 5.4's wide observability path).
Failure. Without them, "the link is slow" has no local evidence, and the investigation starts by opening the Adapter — which is exactly the layer streaming was supposed to let you stop thinking about. Sustained stall cycles mean the transport is backpressuring you; a high age high-water mark with low stalls means you are the one waiting, and the flush policy is the suspect.
9. Invariants Worth Asserting
// Illustrative — never hand down a payload claiming more bytes than were packed.
property p_payload_length_matches_occupancy;
@(posedge clk) disable iff (!rst_n)
payload_accepted |-> (payload_len == pack_occ_q);
endproperty
// Illustrative — a partially filled payload cannot wait forever.
property p_partial_payload_eventually_flushed;
@(posedge clk) disable iff (!rst_n)
(pack_occ_q != '0) |-> ##[1:MAX_FLUSH_WAIT] (payload_ready || (pack_occ_q == '0));
endproperty
// Illustrative — the shim never emits into a link that is not operational.
property p_no_payload_before_operational;
@(posedge clk) disable iff (!rst_n)
payload_accepted |-> link_operational_sync;
endproperty
// Illustrative — reassembly consumes exactly the bytes framing declared.
property p_reassembly_consumes_declared_length;
@(posedge clk) disable iff (!rst_n)
msg_complete |-> (bytes_consumed_q == declared_len_q);
endpropertyWhat each catches. The first catches a length field derived independently of the buffer — a classic two-sources-of-truth bug whose symptom is truncated or over-read messages. The second is the deadlock guard of §6, written as bounded liveness: it does not claim traffic flows, only that a non-empty buffer cannot sit indefinitely. The third is Chapter 8.5 §5's traffic gate, at this layer. The fourth catches framing drift — the §7 failure — at the point where it can still be attributed.
Note the bound in the second property. ##[1:MAX_FLUSH_WAIT] rather than ##[1:$] is deliberate: an unbounded eventuality is a formal property and vacuous in simulation, while a bounded one fails a simulation the moment the flush timer is misconfigured.
10. Verifying a Streaming Shim
What the scoreboard checks is message identity, not byte identity. A transport-level check that bytes arrived is the Adapter's business and is already covered. The shim's scoreboard tracks messages: every message offered at the transmitter appears exactly once at the receiver, with the same length and content, in the order the protocol requires.
The test cases that matter, and why:
| Case | What it exercises |
|---|---|
| One message, then idle | the flush path — the §6 deadlock |
| Messages exactly filling a payload | the boundary where fill and flush both trigger |
| One message spanning several payloads | continuation framing (§7) |
| Several small messages in one payload | multi-message framing |
| Maximum-size and minimum-size messages | length-field width and off-by-one |
| Sustained saturating traffic | efficiency and stall behaviour |
| Backpressure from the Adapter | the stall path, and that nothing is dropped |
| Error injection causing replay | that the shim tolerates retransmission timing |
| Link leaving ACTIVE mid-message | a partially transmitted message — see below |
That last row deserves its own thought. If the link goes to recovery while a message spans payloads, one part has been transmitted and the rest has not. What does the receiver do with the fragment? Discard on link event, or hold and expect continuation? Both are defensible and they must match, or a recovery event silently corrupts one message — Chapter 8.6 §14's ownership question, arriving at the shim.
// Illustrative streaming coverage — not UCIe-defined.
covergroup cg_streaming @(posedge clk iff payload_accepted);
cp_fill : coverpoint pack_occ_q {
bins tiny = {[1:16]};
bins partial = {[17:PAYLOAD_BYTES-1]};
bins full = {PAYLOAD_BYTES};
}
cp_reason : coverpoint flush_reason; // full / timer / urgent
cp_span : coverpoint msg_spans_payload; // did a message cross a boundary?
cp_stall : coverpoint was_stalled;
// Did a spanning message ever coincide with a timer-driven flush?
x_span_by_reason : cross cp_span, cp_reason;
// Were partially filled payloads ever sent under backpressure?
x_fill_by_stall : cross cp_fill, cp_stall;
endgroupWhy the first cross. A message spanning a payload boundary and a timer flush firing mid-message is where continuation state and flush state interact — two mechanisms that are individually simple and jointly the place framing bugs live.
10a. Choosing Between the Modes
A decision table, since this is the practical question a reader arrives with:
| If your protocol… | Use | Because |
|---|---|---|
| is PCIe or CXL | the mapped path | it is what those paths are for |
| is something else and needs reliable delivery | streaming | you inherit CRC and replay instead of rebuilding them |
| is something else and provides its own reliability end-to-end | raw is defensible | you would otherwise pay for reliability twice |
| needs absolute minimum latency and tolerates its own errors | raw, with care | you are trading reliability for the flit overhead |
| must interoperate with another vendor's chiplet | streaming or mapped | raw's reliability scheme is private by construction |
The row people get wrong is the third. Layering your own retry on top of the Adapter's replay is not twice as safe — it is two mechanisms with different timeouts interacting, and the usual result is one of them firing during the other's recovery. If you already have end-to-end reliability you trust, raw may genuinely be the right call.
11. Debug Checklist
- Is the link operational at all? Module 8's checklists come first — streaming problems and link problems look nothing alike, and this eliminates the latter.
- Are payloads being accepted by the Adapter? If stall cycles dominate, the transport is backpressuring and this is a flow-control question (Chapter 9.5).
- Are payloads being handed down at all? A zero send count with non-zero occupancy is the §6 deadlock — check the flush path.
- Does the far end report CRC errors? Errors mean the physical link; no errors with bad data means framing.
- Does the transport report success while the payload is nonsense? §7 — the framing contract, checked on both ends.
- Do the two ends agree on payload size? A revision or configuration difference silently misaligns every boundary.
- Are message lengths within the declared field width? An oversized message truncates its own length field.
- Did a link event occur mid-message? Check whether the fragment policy matches on both ends.
- Is the fill-versus-flush policy producing the latency you expected? A high age high-water mark with low stalls means the shim is waiting, not the link.
- Are you paying replay bandwidth? Retransmission consumes link capacity; a clean-but-slow link may be spending it on resends.
Steps 1 to 3 are local and cheap. Step 5 is the one that saves the most time, because the instinct on corrupt data is always to blame the physical link — and in streaming mode, a clean transport report actively points away from it.
12. Common Misconceptions
"Streaming is a protocol." It is a transport mode. It carries your protocol; it is not one (§1, §5).
"Raw mode is streaming without the extras." Raw bypasses error detection and replay entirely, so anything you build to replace them is private and non-interoperable (§2).
"The Adapter understands my messages." It protects opaque payload bytes. That opacity is exactly what makes the mechanism general — and exactly why framing is your problem (§4, §7).
"Streaming makes my chiplet interoperable with anyone's." It standardises how your protocol crosses the link, not what your protocol is. Two streaming chiplets interoperate at the transport level and understand each other only if their payloads agree (§5).
"Message boundaries survive the transport." The Adapter delivers payload bytes and has no concept of a message. Framing inside the payload is yours (§7).
"Inherited reliability means I need not think about errors." Replay costs bandwidth and adds latency variance, and retransmission timing is visible to your protocol (§8).
"A full payload is always the right thing to wait for." Waiting maximises efficiency and can deadlock a request/response protocol whose next message depends on a response to the one still buffered (§6).
"Adding my own retry on top makes it safer." Two reliability mechanisms with different timeouts interact, usually by one firing during the other's recovery (§10a).
"Clean CRC means the data is right." CRC proves the bytes arrived as sent. It says nothing about whether the receiver's framing interprets them correctly (§7).
13. Understanding Check
14. Summary and What Comes Next
Streaming lets a protocol UCIe does not understand ride inside flits it does — so it inherits the Adapter's error detection and replay without the Adapter interpreting the payload.
It exists because raw mode bypasses error detection and replay, leaving anyone with a custom protocol to rebuild framing, CRC, retry, and flow control — privately, and therefore non-interoperably. The Consortium describes streaming flits as allowing other protocol messages to be mapped into the payload portion of flits compatible with PCIe and CXL, so those protocols can reuse UCIe's existing error-detection and replay logic, with CHI as the motivating case.
Payload opacity is the whole design. The Adapter protects bytes without reading them, which is what makes one mechanism serve every protocol — and which imposes discipline in both directions: no transport behaviour keyed on payload content, and no protocol dependence on flit size, CRC choice, or replay depth.
Two problems remain yours, and they are where the engineering is. The size mismatch needs a packing shim, and a shim with no flush path deadlocks the moment a request/response protocol waits for a response to a message still in its buffer — invisible under saturating traffic. And message boundaries do not survive, so framing inside the payload is your contract with your own far end, with the distinctive failure of clean CRC over garbage data.
Inherited reliability still has consequences: replay costs bandwidth, adds latency variance, implies an outstanding window that backpressures you, and must not be layered under a second retry mechanism of your own.
This chapter established what streaming is and where it sits. The rest of Module 9 makes it concrete:
- 9.2 — Streaming Packet Transport — the packet format, framing, and header/payload structure that carry a stream across the link.
Browse the full path on the UCIe tutorials index.