CXL · Module 28
CXL vs UCIe
The question is usually malformed: one is a transport and the other is a protocol. This chapter builds the layer separation, reach against density, energy per bit, what UCIe carries, the substrate profile, the latency tier, the yield problem, where each applies and what two stacks cost.
28.2 placed CXL against another coherency protocol. This chapter places it against something that is not a protocol at all — what is the difference between CXL and UCIe? — and the first job is to notice that the question, as usually asked, does not have an answer.
UCIe is CXL for chiplets. It names both, it locates them in the same conversation, and it is where most answers stop: no layer separation, no reach, no energy, nothing about what UCIe actually carries, no substrate and nothing about how a package fails.
1. The Engineering Problem — A Transport And A Protocol
The question is usually malformed. UCIe occupies two layers of a six-layer stack and CXL occupies four, sharing none — asking which to use is asking whether to use a wire or a language. Section 5.
The physical trade is reach against density. A two-millimetre crossing wanting a thousand wires has seven hundred and sixty-eight on a six-millimetre shoreline — two hundred and fifty-six short. Section 6.
Energy per bit is the headline and the reason both exist. A thousand gigabits at half a picojoule against five is five hundred milliwatts against five thousand — four and a half watts of difference on the same traffic. Section 7.
UCIe carries a protocol and does not replace one. Naming the transport leaves one of two decisions made, and a design that thinks otherwise has chosen a wire and called it an architecture. Section 8.
A package boundary is a yield problem, not an availability problem. Forty dies at eighty percent known-good assemble at sixty-four — fourteen packages scrapped — and no amount of hot-plug handling addresses it. Section 11.
This chapter against 28.2, stated precisely. That one compared two coherency protocols whose domains stop in different places. This one compares a protocol against a transport, which is a category difference rather than a design difference — and the interesting content is entirely in what follows from putting them at different layers.
2. The One-Sentence Model
A comparison of CXL against UCIe is sound when both are named, when the layers each occupies are distinguished, when the reach and wire density are stated, when what the transport carries is stated as a separate decision, when the substrate profile is named, and when the failure model is stated — and "UCIe is CXL for chiplets" is one of those six.
3. What This Chapter Owns
| Ground | Owner |
|---|---|
| CXL against a non-coherent bus | 28.1 |
| CXL against an on-die coherency fabric | 28.2 |
| What one coherent transaction does | 27.8 |
| Designing a CXL fabric | 27.7 |
| CXL against a die-to-die transport | this chapter |
Some vocabulary, because the two names sit at different heights and are used as though they were siblings.
UCIe is a die-to-die interconnect standard. It specifies a physical layer — bumps, lanes, training, sideband — and a die-to-die adapter that provides reliable delivery and flow control between two dies in one package. It does not define coherency, memory semantics or transaction types.
A protocol layer rides on top of it. UCIe explicitly carries PCIe and CXL as mapped protocols, plus a raw mode for anything else. That is the whole of the relationship: CXL is one of the things UCIe was built to transport.
A chiplet is a die designed to be one part of a package. The interesting property for this chapter is not that it is small — it is that the connection to its neighbour is millimetres of substrate rather than centimetres of board, which is section 6's subject.
The substrate is what the package is built on. A standard organic package and an advanced package with silicon bridges or interposers have very different bump pitches, and UCIe defines separate profiles for them. Section 9.
And known-good die is the yield question. A package with several dies in it is only good if every die in it is good, which is section 11 and has no analogue on a link.
4. Teaching-Model Boundary
Every model in this chapter is a teaching model, not a package planner. It computes the one relationship the section is about and nothing else. There is no PHY, no adapter and no substrate anywhere in this file.
Each model is built twice from one source. A parameter selects between the measured build, which counts what the two actually decide, and the pick-one build, which treats them as alternatives at one layer. Every section's headline number is the gap between them.
| The models do | The models do not |
|---|---|
| Compute one axis of the comparison | Implement either standard |
| Contrast a transport decision against a protocol one | Model bumps, lanes or training |
| Saturate and bound every count they publish | Predict any real package's numbers |
| Count how often each build was wrong | Replace either specification |
5. RTL 1 — The Question Is Usually Malformed
Start with the category error, because unlike the previous two chapters' weak definitions this one makes the question unanswerable rather than merely incomplete.
CXL and UCIe do not occupy the same layers. UCIe is a physical layer and a die-to-die adapter. CXL is a transaction protocol with coherency, memory semantics and an ordering model. A stack that uses both has UCIe underneath and CXL on top — which is not a choice between them, it is the two of them doing different jobs in the same design.
"Which should I use" therefore has no answer in the form it was asked. The answerable questions are which transport and which protocol, and they are section 8's subject.
// RTL 1 - the question is usually malformed. UCIe is a physical and link
// layer for die-to-die connections; CXL is a protocol. UCIe carries CXL, so
// asking which to use is asking whether to use a wire or a language.
module layer_confusion #(parameter int PICK_ONE = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] layers_total, layers_ucie, layers_cxl, layers_shared,
output logic [15:0] ucie_ok, cxl_ok, layers_apart, overlap_pct,
output logic same_layer,
output logic [7:0] n_evals, n_apart,
output logic layer_err
);
logic [31:0] o_q;
logic [15:0] shared_ok, true_apart;
logic truly_apart;
// Each protocol occupies some of the stack's layers, and the question is
// whether those sets overlap at all.
assign ucie_ok = (layers_ucie > layers_total) ? layers_total : layers_ucie;
assign cxl_ok = (layers_cxl > layers_total) ? layers_total : layers_cxl;
assign shared_ok = (layers_shared > ucie_ok) ? ucie_ok : layers_shared;
assign true_apart = ucie_ok - shared_ok;
assign layers_apart = (PICK_ONE != 0) ? 16'd0 : true_apart;
assign o_q = (ucie_ok == 16'd0) ? 32'd100
: (({16'd0, shared_ok} * 32'd100) / {16'd0, ucie_ok});
assign overlap_pct = o_q[15:0];
assign same_layer = (layers_apart == 16'd0) && (ucie_ok != 16'd0);
assign truly_apart = (true_apart != 16'd0);
assign layer_err = evaluate && truly_apart && same_layer;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_apart <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_apart) n_apart <= n_apart + 8'd1;
end
end
endmoduleSix layers with UCIe occupying two and CXL four, sharing none, is two layers that are UCIe's alone and no overlap at all — and the pick-one view reports a choice.
| Fact | Value |
|---|---|
| Layers in the stack | 6 |
| Occupied by UCIe | 2 |
| Occupied by CXL | 4 |
| Shared | 0 |
| UCIe layers with no CXL | 2 |
| Overlap | 0% |
Figure 1 — the category error, drawn. The upper path is the shape of the question as it is usually asked, and the answer it produces is not merely imprecise: it is an answer to a question with no content, because the two things it asks you to choose between are not substitutable for one another. The lower path asks which layers each occupies, and the zero it returns is the whole finding — there is nothing to trade off because there is no contested ground.
The second case is worth keeping because the model has to be able to report a genuine choice when there is one. Two things occupying the same layers completely — two competing physical layers, or two competing protocols — is a real comparison, and the model calls it one. The technique is not "protocols and transports are never comparable"; it is "check before comparing."
The sixth case is the interesting middle and it is the one that makes the section a measurement rather than a slogan. Partial overlap — one of UCIe's two layers having a CXL counterpart — is still not a choice, because the non-overlapping layer has to come from somewhere regardless of which you pick. A comparison is only a choice where the overlap is total, and anything less means the answer is "both, arranged like this."
The last case shows the direction the number moves at realistic depth. Eight UCIe layers and fourteen CXL layers sharing one gives twelve percent overlap on a twenty-layer stack — the deeper the stack, the smaller the contested fraction, and the more obviously the two are collaborators rather than rivals.
The degenerate case bounds the model: a comparison with no stack named reports complete overlap of nothing, which is an unnamed stack rather than a coincidence.
It is worth naming the layers concretely, because "two layers and four layers" is otherwise an abstraction. UCIe supplies a physical layer — bumps, lanes, clocking, training, repair, sideband — and a die-to-die adapter that adds CRC, retry and flow control so that whatever rides above sees a reliable link. That is the two.
CXL supplies a flit layer, three protocol layers and a coherency model. CXL.io's transaction semantics, CXL.cache's coherent device attach, CXL.mem's host-managed memory, and the ordering and state machinery underneath all three — 27.8's entire chapter. That is the four, and none of them has a UCIe counterpart because UCIe was not built to have one.
The mapping between them is a specified thing, not a coincidence. UCIe names the protocols it carries, and CXL is one of them, which means the stack in question is a designed composition rather than two standards that happen to coexist. A comparison that treats a specified composition as a rivalry has misread a dependency as a conflict, and that is a different kind of error from the incomplete answers the previous two chapters were about.
The third case is the clamp and it makes a point worth keeping. Occupying layers that do not exist earns no overlap, because the comparison is bounded by the stack that was actually named — and a great deal of confusion in this area comes from comparing one standard's real layers against another's imagined ones.
6. RTL 2 — Reach Against Density
The second thing, and the physical trade everything else follows from.
A die-to-die connection is short and wide. Millimetres of substrate, thousands of wires, no connectors, no cable, no retimer. A package-to-package link is long and narrow. Centimetres of board, tens of lanes, connectors and retiming in the path. Those are not two settings of one design; they are two different engineering problems, and the wire budget is the number that separates them.
The budget is a shoreline: a die can only bring out as many wires as its edge — times the wires per millimetre its substrate supports — will carry.
// RTL 2 - the physical difference is reach against density. A die-to-die
// connection is millimetres long and can afford thousands of wires; a package
// link is centimetres long and has tens. That trade decides everything the
// two transports can do.
module reach_density #(parameter int A_LINK_IS_A_LINK = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] reach_mm, wires_needed, wires_per_mm, width_mm,
output logic [15:0] wires_avail, wires_short, density_pct, reach_ok,
output logic width_fits,
output logic [7:0] n_evals, n_short,
output logic density_err
);
logic [31:0] w_q, d_q;
logic [15:0] true_avail, true_short;
logic truly_short;
// A connection's wire budget is its shoreline times the wires the substrate
// supports per millimetre of it, and reach cuts that budget sharply.
assign reach_ok = (reach_mm > 16'd999) ? 16'd999 : reach_mm;
assign w_q = {16'd0, width_mm} * {16'd0, wires_per_mm};
assign true_avail = (w_q > 32'd9999) ? 16'd9999 : w_q[15:0];
assign wires_avail = true_avail;
assign true_short = (wires_needed > true_avail) ? (wires_needed - true_avail) : 16'd0;
assign wires_short = (A_LINK_IS_A_LINK != 0) ? 16'd0 : true_short;
assign d_q = (wires_needed == 16'd0) ? 32'd100
: (({16'd0, true_avail} * 32'd100) / {16'd0, wires_needed});
assign density_pct = (d_q > 32'd100) ? 16'd100 : d_q[15:0];
assign width_fits = (wires_short == 16'd0) && (wires_needed != 16'd0);
assign truly_short = (true_short != 16'd0);
assign density_err = evaluate && truly_short && width_fits;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_short <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_short) n_short <= n_short + 8'd1;
end
end
endmoduleA connection wanting a thousand and twenty-four wires across a six-millimetre shoreline at a hundred and twenty-eight wires per millimetre has seven hundred and sixty-eight available — two hundred and fifty-six short, three quarters of what it wants.
| Fact | Value |
|---|---|
| Reach | 2 mm |
| Wires wanted | 1,024 |
| Shoreline | 6 mm |
| Wires per mm | 128 |
| Available | 768 |
| Density | 75% |
Figure 2 — the constraint a link never has and a die-to-die connection always does. A package link's width is set by how many lanes the connector and the board can carry, and it is small enough that nobody computes a shoreline. A die-to-die connection's width is set by the physical edge of the silicon, which is why UCIe specifies bump pitch and shoreline efficiency at all, and why the interesting failure is running out of die edge rather than running out of bandwidth.
The third case is worth reading because it says what this model deliberately does not do. A reach far past anything a die-to-die connection contemplates is clamped and changes nothing, because the model prices shoreline rather than distance. Reach and density are correlated in reality and independent in this model, and keeping them separate is what lets section 6's number be about width alone.
The sixth case drives both ceilings at once: a shoreline wide enough to saturate the wire count also saturates the density percentage at a hundred, and the model reports a fit rather than inventing a fault out of its own bounds.
The fifth case is the degenerate end and it is a real design state. Wires wanted against no shoreline at all is a connection with nowhere to land — a chiplet whose edge is already fully committed to something else — and it is a floorplan problem that no amount of protocol choice addresses.
The reason the two budgets differ by so much is worth making concrete, because "short and wide" is a description rather than an explanation. A package link's width is bounded by connectors and board routing. Sixteen lanes of differential signalling is sixty-four wires plus references and shields; the connector has a pin count, the board has layers, and the practical ceiling is tens of lanes. A die-to-die connection has no connector and no board. The wires are substrate traces a few micrometres wide, and the ceiling is how many bumps fit along the die edge — which is thousands.
Two orders of magnitude in width is what buys the energy figure in section 7. A wide, slow, single-ended interface moves the same bandwidth as a narrow, fast, differential one at a fraction of the energy, because the per-wire rate is low enough that the channel needs no equalisation. Width and energy are the same decision seen twice, and a design that has one has the other.
The second case is the well-sized design and it is worth stating as the target. A shoreline sized to its wire count reports a hundred percent density and a fit — which is what a floorplan looks like when the interface width was agreed before the die edge was allocated, rather than after.
7. RTL 3 — Energy Per Bit
The third thing, and the number that explains why two transports exist rather than one.
A package link spends most of its energy overcoming a channel it cannot see the end of. Equalisation, retiming, drivers sized for a lossy board — all of it is energy spent on getting a bit across an uncertain path. A die-to-die connection has a short, well-characterised channel and can move a bit for a fraction of the cost, which is the whole of the engineering argument for keeping traffic inside a package.
The figure is quoted in picojoules per bit, and the ratio between the two is large enough that it changes what a design can afford to move.
// RTL 3 - the headline number is energy per bit, and it is the reason the
// two transports exist separately. A die-to-die connection moves a bit for a
// fraction of what a retimed package link costs, because it does not have to
// equalise a channel it cannot see the end of.
module energy_per_bit #(parameter int ENERGY_IS_A_DETAIL = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] gbits_moved, d2d_tenth_pj, link_tenth_pj, power_budget_mw,
output logic [15:0] d2d_mw, link_mw, energy_gap, budget_pct,
output logic fits_budget,
output logic [7:0] n_evals, n_over,
output logic energy_err
);
logic [31:0] d_q, l_q, b_q;
logic [15:0] true_d2d, true_link, true_gap;
logic truly_over;
// Energy is carried in tenths of a picojoule per bit so the ratio survives
// integer arithmetic; the products are the milliwatts each transport costs.
assign d_q = ({16'd0, gbits_moved} * {16'd0, d2d_tenth_pj}) / 32'd10;
assign true_d2d = (d_q > 32'd9999) ? 16'd9999 : d_q[15:0];
assign l_q = ({16'd0, gbits_moved} * {16'd0, link_tenth_pj}) / 32'd10;
assign true_link = (l_q > 32'd9999) ? 16'd9999 : l_q[15:0];
assign d2d_mw = true_d2d;
assign link_mw = (ENERGY_IS_A_DETAIL != 0) ? true_d2d : true_link;
assign true_gap = (true_link > true_d2d) ? (true_link - true_d2d) : 16'd0;
assign energy_gap = (ENERGY_IS_A_DETAIL != 0) ? 16'd0 : true_gap;
assign b_q = (power_budget_mw == 16'd0) ? 32'd999
: (({16'd0, link_mw} * 32'd100) / {16'd0, power_budget_mw});
assign budget_pct = (b_q > 32'd999) ? 16'd999 : b_q[15:0];
assign fits_budget = (link_mw <= power_budget_mw) && (gbits_moved != 16'd0);
// No gbits_moved guard: the link power is scaled by the bits moved, so a
// transport carrying nothing already draws nothing and exceeds no budget.
assign truly_over = (true_link > power_budget_mw);
assign energy_err = evaluate && truly_over && fits_budget;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_over <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_over) n_over <= n_over + 8'd1;
end
end
endmoduleA thousand gigabits at half a picojoule die-to-die against five picojoules over a link is five hundred milliwatts against five thousand — four and a half watts of difference on the same traffic — and the energy-is-a-detail view charges the cheap figure and reports a quarter of the budget.
| Fact | Value |
|---|---|
| Gigabits moved | 1,000 |
| Die-to-die | 0.5 pJ/bit |
| Over a link | 5 pJ/bit |
| Die-to-die power | 500 mW |
| Link power | 5,000 mW |
| Budget | 2,000 mW |
The last case is the one that decides a design and it is worth stating as the section's practical output. A budget between the two figures — four thousand milliwatts, which the die-to-die path meets and the link does not — is the common situation, and it means the transport choice is the power budget's decision rather than the architect's. A design that has to move that much traffic inside its power envelope is a die-to-die design, and no protocol choice changes that.
The sixth case is the honest limit. Two transports of equal energy per bit remove the comparison entirely, and both views agree. That configuration does not exist in practice, but naming it is what makes the section a measurement: the gap is the difference between two figures, and where they do not differ there is no gap.
The third case is where the model's own ceiling defeats it, and it reports that rather than hiding it. Enough traffic that both figures saturate leaves no difference to report, because two numbers pinned at the same ceiling are indistinguishable. The gap is real and the model cannot see it, which is a limit of a bounded model and is stated.
The fifth case bounds it at the degenerate end: a design with no power budget written down is unbudgeted rather than over budget, and the model declines to call a missing constraint a satisfied one.
There is a second-order consequence that decides system architecture rather than interface design. Energy per bit multiplied by bandwidth is a thermal constraint, and in a package the thermal budget is shared. A die-to-die interface that moves a great deal of traffic cheaply is not merely efficient; it is what makes moving that traffic possible at all inside a package whose total power is bounded by what the lid can remove.
That is why the figure is quoted per bit rather than per interface. An architecture decides how many bits must cross a boundary, and the energy figure converts that into watts — which is then compared against a budget that somebody else owns. Section 7's arithmetic is the conversion, and its value is that it makes the two teams' numbers comparable.
The fourth case bounds it and is worth a line for what it declines to do. A transport carrying nothing has no energy story, and the model reports zero rather than an efficiency — because an efficiency with no traffic behind it is a specification claim rather than a measurement, and this model prices what moves.
8. RTL 4 — UCIe Carries A Protocol
The fourth thing, and the one that turns the category error into a practical omission.
Choosing UCIe settles the transport and leaves the protocol open. A UCIe link can carry PCIe, CXL, or a raw stream, and which one it carries is a separate decision with separate consequences — coherency or not, memory semantics or not, an ordering model or not. A design that has picked UCIe has answered one of two questions.
And on a link carrying several protocols at once, the choice is per stream rather than once for the interface.
// RTL 4 - UCIe carries a protocol and does not replace one. Choosing UCIe
// leaves the protocol question entirely open, and an answer that treats the
// transport decision as having settled it has one of two decisions made.
module protocol_carried #(parameter int THE_TRANSPORT_DECIDES = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] decisions, transport_named, protocol_named, streams,
output logic [15:0] made_ok, decisions_open, open_pct, streams_ok,
output logic fully_decided,
output logic [7:0] n_evals, n_open,
output logic protocol_err
);
logic [31:0] o_q;
logic [15:0] t_ok, p_ok, true_made, true_open;
logic truly_open;
assign t_ok = (transport_named > 16'd1) ? 16'd1 : transport_named;
assign p_ok = (protocol_named > 16'd1) ? 16'd1 : protocol_named;
assign true_made = t_ok + p_ok;
assign made_ok = (true_made > decisions) ? decisions : true_made;
assign true_open = decisions - made_ok;
assign decisions_open = (THE_TRANSPORT_DECIDES != 0) ? 16'd0 : true_open;
// A transport that carries several protocols has to say which one each
// stream is, and that is a per-stream decision rather than a single one.
assign streams_ok = (streams > 16'd999) ? 16'd999 : streams;
assign o_q = (decisions == 16'd0) ? 32'd0
: (({16'd0, true_open} * 32'd100) / {16'd0, decisions});
assign open_pct = o_q[15:0];
assign fully_decided = (decisions_open == 16'd0) && (decisions != 16'd0);
assign truly_open = (true_open != 16'd0);
assign protocol_err = evaluate && truly_open && fully_decided;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_open <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_open) n_open <= n_open + 8'd1;
end
end
endmoduleTwo decisions with the transport named and the protocol not is one still open — half the choice outstanding — and the transport-decides view reports a decided design.
| Fact | Value |
|---|---|
| Decisions | 2 |
| Transport named | yes |
| Protocol named | no |
| Made | 1 |
| Open | 1 |
| Outstanding | 50% |
The sixth case is the one that generalises the section past its own headline. A third decision the transport choice does not touch — the substrate, which is section 9 — leaves one of three open and shows that the transport decision settles exactly itself. Choosing a wire answers the question about the wire, and a design's remaining decisions are unaffected by how confidently that one was made.
The last case separates the structure from the traffic. A transport carrying no streams yet still has the protocol decision open, because the decision is about what the interface will be, not about what is flowing through it today.
The third case is the clamp, and it makes a small point precisely. Each choice named several times over is still two decisions, not ten — naming a thing repeatedly does not make additional decisions, which is worth saying in a field where a specification can be cited several times in one paragraph without a design being any more determined.
The degenerate case bounds it: a design with no decisions enumerated reports nothing open, which is an unwritten design rather than a decided one.
The per-stream point deserves its own paragraph because it is the part that catches designs out. A UCIe link can carry more than one protocol at once, with the adapter multiplexing them, which means "which protocol" is not necessarily answered once for the interface. A design carrying CXL.io for configuration and a raw stream for accelerator traffic has made two protocol decisions on one wire, and each carries its own semantics, ordering and error handling.
That is a strength of the transport and a trap for the architecture document, because an interface described as "UCIe" is under-specified in exactly the way an interface described as "a bus" would be. The model counts streams separately from decisions for this reason: the stream count says how many times the protocol question has to be answered.
The second case is the fully-specified design and it is short. Both decisions made — a named transport and a named protocol — closes the model, and it is worth noticing how little that is: two facts, and most architecture documents carry only one of them.
9. RTL 5 — The Substrate Decides The Profile
The fifth thing, and the one that ties the transport choice to a manufacturing decision.
UCIe defines more than one profile because packages differ. A standard organic package has a bump pitch measured in the low hundreds of micrometres; an advanced package — an interposer, or a silicon bridge — gets down to tens. That pitch decides how many bump rows fit along a given shoreline, which decides how many lanes the interface can have.
A design that names the transport without naming the substrate has not said which profile it gets, and the two are not interchangeable.
// RTL 5 - the substrate decides which profile is available. A standard
// organic package and an advanced package with fine bumps are two different
// wire budgets, and a design that names the transport without naming the
// substrate has not said which of them it gets.
module substrate_profile #(parameter int A_PROFILE_IS_A_PROFILE = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] bump_pitch_um, shoreline_mm, lanes_wanted, lanes_per_bump_row,
output logic [15:0] rows_avail, lanes_avail, lanes_short, profile_pct,
output logic profile_fits,
output logic [7:0] n_evals, n_short,
output logic profile_err
);
logic [31:0] r_q, l_q, p_q;
logic [15:0] true_rows, true_lanes, true_short;
logic truly_short;
// A finer bump pitch fits more rows into the same shoreline; the model
// works in micrometres of pitch against millimetres of edge.
assign r_q = (bump_pitch_um == 16'd0) ? 32'd0
: (({16'd0, shoreline_mm} * 32'd1000) / {16'd0, bump_pitch_um});
assign true_rows = (r_q > 32'd9999) ? 16'd9999 : r_q[15:0];
assign rows_avail = true_rows;
assign l_q = {16'd0, true_rows} * {16'd0, lanes_per_bump_row};
assign true_lanes = (l_q > 32'd9999) ? 16'd9999 : l_q[15:0];
assign lanes_avail = true_lanes;
assign true_short = (lanes_wanted > true_lanes) ? (lanes_wanted - true_lanes) : 16'd0;
assign lanes_short = (A_PROFILE_IS_A_PROFILE != 0) ? 16'd0 : true_short;
assign p_q = (lanes_wanted == 16'd0) ? 32'd100
: (({16'd0, true_lanes} * 32'd100) / {16'd0, lanes_wanted});
assign profile_pct = (p_q > 32'd100) ? 16'd100 : p_q[15:0];
assign profile_fits = (lanes_short == 16'd0) && (lanes_wanted != 16'd0);
assign truly_short = (true_short != 16'd0);
assign profile_err = evaluate && truly_short && profile_fits;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_short <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_short) n_short <= n_short + 8'd1;
end
end
endmoduleA two-millimetre shoreline at a hundred-and-ten-micrometre pitch gives eighteen bump rows and thirty-six lanes against two hundred and fifty-six wanted — fourteen percent of the interface.
| Fact | Value |
|---|---|
| Bump pitch | 110 µm |
| Shoreline | 2 mm |
| Rows available | 18 |
| Lanes per row | 2 |
| Lanes available | 36 |
| Lanes wanted | 256 |
The second case is what the advanced package buys and it is the section's point. A forty-five-micrometre pitch with six lanes per row gives forty-four rows and two hundred and sixty-four lanes on the same two millimetres of edge — the interface that did not fit now fits, and nothing about the protocol changed. The substrate is the variable, and it is chosen by a packaging engineer rather than by an architect.
The last case is the boundary. An interface sized exactly to the standard package fits, which is the design that was drawn against its substrate rather than against an aspiration — and it is the cheaper of the two outcomes by a wide margin.
The sixth case separates the two things a profile supplies. Bump rows that carry no lanes is shoreline without a connection: the edge exists, the bumps exist, and nothing has been assigned to them. That is a real intermediate state in a floorplan and the model reports the shortage rather than the rows.
The fourth case is the degenerate one and it is what an early architecture document looks like. An interface specified against no bump pitch carries nothing, because the substrate is the thing that supplies lanes and none has been chosen.
The cost difference between the two substrates is the part that makes this a decision rather than a preference. A standard organic package is the cheap, high-volume, well-understood option, and a great many products cannot justify anything else. An advanced package — an interposer, an embedded bridge — costs more per unit, has lower assembly yield, and has a narrower supplier base. Choosing it to get lanes is a commercial decision with a manufacturing tail, and it is made by people who are not in the protocol conversation.
Which is why section 9 belongs in this chapter rather than in a packaging appendix. The lane count an architecture assumes is a claim about the substrate, and a claim about the substrate is a claim about unit cost and supply. An interface drawn at two hundred and fifty-six lanes has quietly specified an advanced package, and saying so out loud is the whole of the section's practical advice.
The third case drives both ceilings and says the expected thing. A pitch and shoreline that saturate the row and lane counts describe a substrate beyond anything manufacturable, and the model reports a fit rather than a fault — a bound of the model, not a claim about silicon.
10. RTL 6 — Different Tiers, Again
The sixth thing, and the third time this module has had to say it.
A die crossing and a package crossing are in different latency tiers. A die-to-die hop is wire delay and a shallow adapter — picoseconds to low nanoseconds. A package link is serialisation, equalisation, a flit layer and possibly a retimer — tens of nanoseconds. The ratio is the same kind of change 28.1 section 8 describes, one level further down.
// RTL 6 - the latency difference is a tier, as it was in every other
// comparison in this module. A die-to-die crossing is picoseconds of wire and
// a shallow pipeline; a package link is a SerDes, a retimer and a flit layer.
module latency_tier #(parameter int A_CROSSING_IS_A_CROSSING = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] d2d_ps, link_ps, budget_ps, accesses,
output logic [15:0] tier_gap, d2d_over, link_over, ratio_pct,
output logic same_tier,
output logic [7:0] n_evals, n_tiered,
output logic tier_err
);
logic [31:0] r_q;
logic [15:0] true_gap;
logic truly_tiered;
assign true_gap = (link_ps > d2d_ps) ? (link_ps - d2d_ps) : 16'd0;
assign tier_gap = (A_CROSSING_IS_A_CROSSING != 0) ? 16'd0 : true_gap;
assign d2d_over = (d2d_ps > budget_ps) ? (d2d_ps - budget_ps) : 16'd0;
assign link_over = (link_ps > budget_ps) ? (link_ps - budget_ps) : 16'd0;
assign r_q = (d2d_ps == 16'd0) ? 32'd999
: (({16'd0, link_ps} * 32'd100) / {16'd0, d2d_ps});
assign ratio_pct = (r_q > 32'd999) ? 16'd999 : r_q[15:0];
assign same_tier = (tier_gap == 16'd0) && (accesses != 16'd0);
assign truly_tiered = (true_gap != 16'd0) && (accesses != 16'd0);
assign tier_err = evaluate && truly_tiered && same_tier;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_tiered <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_tiered) n_tiered <= n_tiered + 8'd1;
end
end
endmoduleEight hundred picoseconds against forty nanoseconds is thirty-nine nanoseconds of difference and a ratio that saturates the model — one crossing inside a five-nanosecond budget and the other thirty-five nanoseconds over it.
| Fact | Value |
|---|---|
| Die-to-die | 800 ps |
| Over a link | 40,000 ps |
| Gap | 39,200 ps |
| Budget | 5,000 ps |
| Die-to-die over | 0 |
| Link over | 35,000 ps |
The sixth case is the direction that is not rhetorical here. A link faster than a die crossing is reported as one tier, and although the numbers look contrived the situation is not: a die crossing through a congested on-die fabric with several hops can exceed a short, direct package link, which is 27.7 section 6's hop depth arriving inside the package.
The last case is the model's own boundary stated plainly. The tier question does not need a budget. With no budget both crossings are over it by their full latency and the gap is unchanged, because a tier is a relationship between two paths and a budget answers the different question of whether either is fast enough.
The fifth case is the degenerate one that still produces a real answer. A die-to-die figure nobody has measured makes the whole link latency the gap and saturates the ratio, which is the honest reading: an unmeasured path is not a matching one.
What sits inside each crossing is worth enumerating once, because the tier is a sum of parts rather than a property of a name. A die-to-die crossing is the wire delay across the substrate, the adapter's pipeline, and any clock-domain crossing between the two dies. A package link crossing is serialisation at the transmitter, the channel, equalisation and deserialisation at the receiver, the flit layer's framing, and a retimer if the reach demands one — plus the same clock-domain crossing at each end.
The retimer is the item that surprises people, because it is invisible in a block diagram and adds a full serialise-deserialise round on its own. A link that needed one has paid for two crossings rather than one, which is why reach and latency are correlated even though section 6's model keeps reach and width separate.
The second case is the sanity check the model needs to be able to pass: two crossings of the same latency are one tier, and both views agree. That configuration is what a very short, unretimed package link looks like, and it is the case where the comparison genuinely has no content.
11. RTL 7 — A Package Boundary Is A Yield Problem
The seventh thing, and the one with no analogue on either side of the previous two chapters.
A multi-die package is only good if every die in it is good. Two dies at ninety-five percent known-good assemble at about ninety; four at eighty assemble at sixty-four. That is a manufacturing cost that scales with the die count and is paid before the product ships — and it does not appear anywhere in a link-attached design, where each device was tested on its own and sold on its own.
The mirror is also true. A link-attached device can be removed, reset or lost while the system runs, which is 28.2 section 9's availability problem — and a packaged die cannot. Two different failure questions, and neither answers the other.
// RTL 7 - the failure models are different problems entirely. A die in a
// package is tested before assembly and is never removed; a device on a link
// is hot-pluggable and can leave. One is a yield question and the other is an
// availability question, and neither answers the other.
module failure_model #(parameter int A_FAILURE_IS_A_FAILURE = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] dies, kgd_pct, hotplug_paths, handled_paths,
output logic [15:0] assembled_pct, scrapped, unhandled, yield_loss_pct,
output logic one_problem,
output logic [7:0] n_evals, n_exposed,
output logic failure_err
);
logic [31:0] a_q, s_q;
logic [15:0] kgd_ok, handled_ok, true_scrapped, true_unhandled;
logic truly_exposed;
// Every die in a package has to be good for the package to be good, so the
// assembled yield falls as dies are added - which a link-attached design
// simply does not have as a failure mode.
assign kgd_ok = (kgd_pct > 16'd100) ? 16'd100 : kgd_pct;
assign a_q = (dies <= 16'd1) ? {16'd0, kgd_ok}
: (({16'd0, kgd_ok} * {16'd0, kgd_ok}) / 32'd100);
// No clamp: kgd_ok is already bounded at 100, so its square over 100 cannot
// exceed 100 either, and a ceiling here would be unreachable code.
assign assembled_pct = a_q[15:0];
assign s_q = (dies == 16'd0) ? 32'd0
: (({16'd0, dies} * (32'd100 - {16'd0, assembled_pct})) / 32'd100);
assign true_scrapped = (s_q > 32'd9999) ? 16'd9999 : s_q[15:0];
assign scrapped = (A_FAILURE_IS_A_FAILURE != 0) ? 16'd0 : true_scrapped;
assign handled_ok = (handled_paths > hotplug_paths) ? hotplug_paths : handled_paths;
assign true_unhandled = hotplug_paths - handled_ok;
assign unhandled = (A_FAILURE_IS_A_FAILURE != 0) ? 16'd0 : true_unhandled;
assign yield_loss_pct = 16'd100 - assembled_pct;
assign one_problem = (scrapped == 16'd0) && (unhandled == 16'd0) && (dies != 16'd0);
assign truly_exposed = (true_scrapped != 16'd0) || (true_unhandled != 16'd0);
assign failure_err = evaluate && truly_exposed && one_problem;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_exposed <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_exposed) n_exposed <= n_exposed + 8'd1;
end
end
endmoduleForty dies at eighty percent known-good assemble at sixty-four, scrapping fourteen packages, while four hot-plug recovery paths of six have no handler — and the a-failure-is-a-failure view reports one problem.
| Fact | Value |
|---|---|
| Dies | 40 |
| Known-good | 80% |
| Assembled yield | 64% |
| Scrapped | 14 |
| Hot-plug paths | 6 |
| Unhandled | 4 |
The sixth case is the one that stops the section becoming an argument for links. Perfect yield and no hot-plug path handled reports zero scrapped and six unhandled — a package with no yield exposure at all and a complete absence of the other question's answers. The two exposures are independent, and a design that has solved one has learned nothing about the other.
The fifth case is its mirror and is the packaged design's normal state. A packaged design with no hot-plug question at all still scraps fourteen packages, because the yield question does not go away when the availability question is absent.
The second case is the only configuration with neither exposure: a single die at perfect yield with every hot-plug path handled, which is a design that has either solved both problems or does not have them.
The third case drives both clamps and says something mild: a yield claimed above a hundred percent is not extra yield, and more handlers than paths is not extra coverage.
The model's yield arithmetic is deliberately the simplest thing that shows the shape, and it is worth saying what it leaves out so the number is not over-read. It assumes each die's goodness is independent and that assembly itself never fails. Real packages have an assembly yield of their own, dies can be repaired or binned rather than scrapped, and known-good-die testing is itself imperfect — so the true figure is worse than the product of the rates in some respects and better in others.
What survives all of that is the shape: the exposure multiplies with die count rather than adding, so the third and fourth dies cost far more than the first and second did. That is the fact an architecture needs, and it is the one that a protocol-level comparison never surfaces, because it is not about the interface at all.
The fourth case is the degenerate one and it is the state an early proposal is in. Hot-plug paths on a design with no dies counted reports the single-die yield and four unhandled paths — a design that has thought about the availability question and not yet about the packaging one, which is the more common order and the more expensive one to discover late.
12. RTL 8 — Each Applies Where Its Geometry Is
The eighth thing, and the direction a one-transport answer never goes.
A connection inside a package is a die-to-die problem whatever rides on it. Putting it on a package link costs the tier difference on every crossing, for no benefit — the two dies are already millimetres apart. A connection between packages was never a die-to-die candidate, whatever its bandwidth need, because there is no substrate between them.
The geometry is decided by the floorplan and the product, and it decides the transport before either standard is consulted.
// RTL 8 - each applies where its geometry is, and the geometry is decided
// before either is chosen. A connection inside a package is a die-to-die
// problem whatever protocol rides it; a connection between packages is a link
// problem whatever the bandwidth need.
module where_each_applies #(parameter int ONE_TRANSPORT_FITS = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] connections, held_in_package, d2d_ps, link_ps,
output logic [15:0] in_pkg_ok, spans_package, misplaced_ps, in_pkg_pct,
output logic one_transport,
output logic [7:0] n_evals, n_misplaced,
output logic fit_err
);
logic [31:0] m_q, p_q;
logic [15:0] true_spans, true_cost, penalty;
logic truly_misplaced;
assign in_pkg_ok = (held_in_package > connections) ? connections : held_in_package;
assign true_spans = connections - in_pkg_ok;
assign spans_package = (ONE_TRANSPORT_FITS != 0) ? 16'd0 : true_spans;
// Putting an in-package connection on a link costs the tier difference on
// every crossing it makes.
assign penalty = (link_ps > d2d_ps) ? (link_ps - d2d_ps) : 16'd0;
assign m_q = {16'd0, in_pkg_ok} * {16'd0, penalty};
assign true_cost = (m_q > 32'd9999) ? 16'd9999 : m_q[15:0];
assign misplaced_ps = (ONE_TRANSPORT_FITS != 0) ? 16'd0 : true_cost;
assign p_q = (connections == 16'd0) ? 32'd0
: (({16'd0, in_pkg_ok} * 32'd100) / {16'd0, connections});
assign in_pkg_pct = p_q[15:0];
assign one_transport = (misplaced_ps == 16'd0) && (spans_package == 16'd0)
&& (connections != 16'd0);
assign truly_misplaced = (true_cost != 16'd0) || (true_spans != 16'd0);
assign fit_err = evaluate && truly_misplaced && one_transport;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_misplaced <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_misplaced) n_misplaced <= n_misplaced + 8'd1;
end
end
endmoduleTen connections with seven inside the package is three spanning it and a misplacement cost that saturates the model — seventy percent in package, and the one-transport view reports a fit.
| Fact | Value |
|---|---|
| Connections | 10 |
| Inside the package | 7 |
| Spanning it | 3 |
| Die-to-die | 800 ps |
| Over a link | 40,000 ps |
| In package | 70% |
The fifth case is the honest reverse and it is worth stating as a positive. Everything spanning the package misplaces nothing at all, because no connection could have been inside — the population is a link population and die-to-die was never an option for it. That is not a defeat for one standard; it is the geometry having already decided.
The sixth case isolates the variable the way section 5 isolates overlap. A boundary that costs nothing removes the misplacement and leaves three connections still spanning — the population is still divided, and the division is structural even when the latency is not.
The second case is the only clean one, and it takes two conditions: everything inside the package and no tier penalty to pay. That is the configuration a one-transport answer is imagining, and the model shows how narrow it is.
The degenerate case bounds it: a comparison with no connections misplaces nothing and reports no fit.
The middle of this comparison is where most real products sit, and both extremes in the model are there to bracket it. A modern accelerator is several dies in one package with an external CXL or PCIe port, so it has in-package connections and spanning ones at the same time — and the right answer is both transports, each where its geometry puts it.
The failure the model is built to catch is not choosing wrongly; it is choosing once. A design that standardises on one transport for uniformity pays section 12's misplacement on whichever half of its connections is on the wrong side, and uniformity is a real engineering virtue that happens to be expensive here.
The third case is worth reading for what it clamps. More in-package connections claimed than exist still reports a saturated misplacement cost, because ten in-package connections routed over a link pay the tier on every one of them — the clamp bounds the arithmetic, not the mistake.
13. RTL 9 — The Stacks Are Not Alternatives
The ninth thing, and the one that shows up as a schedule number.
A design that needs both pays for both, minus what they share. A UCIe die-to-die stack carrying CXL and a CXL-over-PCIe-PHY stack have the same protocol layer and entirely different physical and link layers. The protocol block is built once; everything below it is built per stack.
That is not the sum of two stacks and it is not one stack either, and a plan that assumes either is wrong in a predictable direction.
// RTL 9 - the stacks are not alternatives and the cost is not a subtraction.
// A UCIe die-to-die stack carrying CXL is a physical layer, a link layer and
// a protocol layer; CXL over a PCIe physical layer is a different physical
// and link layer under the same protocol. The protocol block is shared.
module stack_cost #(parameter int ONE_REPLACES_THE_OTHER = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic [15:0] blocks_total, blocks_shared, block_cost, stacks_built,
output logic [15:0] shared_ok, blocks_fresh, build_cost, shared_pct,
output logic cost_is_one_stack,
output logic [7:0] n_evals, n_double,
output logic stack_err
);
logic [31:0] b_q, s_q, s_cost;
logic [15:0] true_fresh, true_cost, shared_only;
logic truly_double;
assign shared_ok = (blocks_shared > blocks_total) ? blocks_total : blocks_shared;
assign true_fresh = blocks_total - shared_ok;
// Building both stacks costs the shared blocks once and the distinct ones
// per stack, which is not the sum and is not one of them either.
assign b_q = ({16'd0, shared_ok}
+ ({16'd0, true_fresh} * {16'd0, stacks_built}))
* {16'd0, block_cost};
assign true_cost = (b_q > 32'd9999) ? 16'd9999 : b_q[15:0];
assign s_cost = {16'd0, shared_ok} * {16'd0, block_cost};
assign shared_only = (s_cost > 32'd9999) ? 16'd9999 : s_cost[15:0];
assign blocks_fresh = (ONE_REPLACES_THE_OTHER != 0) ? 16'd0 : true_fresh;
assign build_cost = (ONE_REPLACES_THE_OTHER != 0) ? shared_only : true_cost;
assign s_q = (blocks_total == 16'd0) ? 32'd100
: (({16'd0, shared_ok} * 32'd100) / {16'd0, blocks_total});
assign shared_pct = s_q[15:0];
assign cost_is_one_stack = (blocks_fresh == 16'd0) && (blocks_total != 16'd0);
assign truly_double = (true_fresh != 16'd0) && (stacks_built > 16'd1);
assign stack_err = evaluate && truly_double && cost_is_one_stack;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_double <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_double) n_double <= n_double + 8'd1;
end
end
endmoduleTwelve blocks with five shared, built as two stacks at forty units a block, is seven distinct blocks per stack and seven hundred and sixty units — where the one-replaces-the-other view charges the five shared blocks alone.
| Fact | Value |
|---|---|
| Blocks | 12 |
| Shared | 5 |
| Distinct | 7 |
| Stacks built | 2 |
| Cost per block | 40 |
| Build cost | 760 |
The sixth case is the boundary the model insists on and it is the common case. Only one stack actually built pays four hundred and eighty units — every block once — and the model declines to call it a doubled cost, because nothing was doubled. The penalty is for building both, and a design that has committed to one geometry does not pay it.
The second case is the ceiling of the sharing argument: two stacks that genuinely share everything cost one stack's worth, which is what a well-layered design approaches and never quite reaches.
The fifth case is the floor. Nothing shared at all is two entirely separate stacks at nine hundred and sixty units, which is what happens when the protocol layer is written twice because each transport team built its own.
The degenerate case bounds it: a comparison with no stack enumerated reports complete sharing of nothing, which is an unwritten plan.
Which blocks actually share is worth listing, because the ratio is the estimate. Shared: the CXL protocol layers, the coherency agent, the memory path, the register model, and most of the verification environment above the link — 27.9 section 12's argument applied to two transports instead of two protocols. Not shared: the physical layer, the training and repair logic, the link layer's framing and retry, the credit scheme, and the entire analogue and physical-design effort underneath.
The unshared half is the half with the long schedule, because physical layers are silicon-process work with their own tape-out risk. A plan that shares the protocol block and assumes that is most of the job has counted the part that was already finished.
The seventh case is the clamp and it is the realistic end for a first product. A block cost that saturates the build on two stacks with nothing shared is a team building two complete interfaces at once, which is a real and expensive thing to do and is usually the consequence of a decision nobody made deliberately.
14. RTL 10 — A CXL-Against-UCIe Comparison Assembled
Nine sections of inputs. This one puts them in one place and makes the confident answer visible as what it is: one bit of six.
// RTL 10 - a CXL-against-UCIe comparison assembled. Nine sections of inputs,
// one summary. "UCIe is CXL for chiplets" is bit 0: it names both, and it is
// one sixth of a comparison between things at different layers.
module comparison_signoff #(parameter int NAMING_BOTH_IS_COMPARING = 0) (
input logic clk, rst_n,
input logic evaluate,
input logic both_named, layers_distinguished, reach_stated,
input logic protocol_stated, substrate_stated, failure_stated,
output logic [5:0] fail_mask,
output logic [15:0] conditions_met, sound_pct,
output logic sound,
output logic [7:0] n_evals, n_sound, n_claimed,
output logic signoff_err
);
logic [31:0] s_q;
logic truly_sound, claimed;
assign fail_mask[0] = ~both_named;
assign fail_mask[1] = ~layers_distinguished;
assign fail_mask[2] = ~reach_stated;
assign fail_mask[3] = ~protocol_stated;
assign fail_mask[4] = ~substrate_stated;
assign fail_mask[5] = ~failure_stated;
assign conditions_met = {15'd0, both_named} + {15'd0, layers_distinguished}
+ {15'd0, reach_stated} + {15'd0, protocol_stated}
+ {15'd0, substrate_stated} + {15'd0, failure_stated};
assign s_q = ({16'd0, conditions_met} * 32'd100) / 32'd6;
// No clamp: conditions_met sums six one-bit values, so the quotient cannot
// exceed a hundred and a ceiling would be unreachable code.
assign sound_pct = s_q[15:0];
assign truly_sound = (fail_mask == 6'd0);
// The naming view reads bit 0 and stops.
assign claimed = (NAMING_BOTH_IS_COMPARING != 0) ? both_named : truly_sound;
assign sound = claimed;
assign signoff_err = evaluate && !truly_sound && claimed;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
n_evals <= 8'd0; n_sound <= 8'd0; n_claimed <= 8'd0;
end else if (evaluate) begin
n_evals <= n_evals + 8'd1;
if (truly_sound) n_sound <= n_sound + 8'd1;
if (claimed) n_claimed <= n_claimed + 8'd1;
end
end
endmoduleThe stimulus walks all six bits one at a time. When both have been named and any one of the other five fails, the assembled model reports that the comparison is not sound and the naming view reports a comparison.
| Bit | Condition, and the section that builds it |
|---|---|
| 0 | Both were named at all — §14 |
| 1 | The layers each occupies are distinguished — §5 |
| 2 | Reach and wire density are stated — §6 |
| 3 | What the transport carries is stated separately — §8 |
| 4 | The substrate profile is named — §9 |
| 5 | The failure model is stated — §11 |
Across the eight evaluations, the assembled model calls one comparison sound and the naming view calls six of them a comparison.
The bit order is by how much of the difference each condition carries, and bit 1 carries more than any other in this chapter — distinguishing the layers is what converts an unanswerable question into two answerable ones, and every remaining bit is a property of one layer or the other.
"UCIe is CXL for chiplets" is bit 0, and it is the weakest opening of the three comparison chapters. 28.1's "CXL is coherent PCIe" locates CXL correctly. 28.2's "they are both coherent" is true of both sides. This one is a category error wearing an analogy's clothes — it implies substitution between things that stack, and a reader who accepts it will look for a trade-off that does not exist.
The five other bits fail independently. Layers can be distinguished by somebody who has not looked at the wire budget. Reach can be stated with no energy figure, which is the most common half-answer from a floorplan background. What the transport carries is the bit a transport-first answer omits by construction. The substrate is a packaging decision that an architecture answer rarely reaches. And the failure model is last again, for the third chapter running.
Figure 4 — the mask ordered by how much of the difference each condition carries, and the first decision is unlike the first decision in either of the other comparison chapters. It does not narrow the answer; it changes the question. Until the layers are separated the remaining four boxes are being asked about a choice that has no sides, which is why this chapter's weak definition is the weakest of the three — it does not merely stop early, it points the reader at a trade-off that is not there.
15. Quantitative Reasoning
Two layers of UCIe's six-layer share with no CXL in them, and zero percent overlap — twelve percent on a twenty-layer stack, which falls as the stack deepens.
Two hundred and fifty-six wires short of a thousand and twenty-four, on a six-millimetre shoreline at a hundred and twenty-eight wires per millimetre.
Four and a half watts of difference on a thousand gigabits — five hundred milliwatts die-to-die against five thousand over a link.
One of two decisions made by naming the transport, and one of three when the substrate is counted too.
Thirty-six lanes against two hundred and fifty-six wanted on a standard package, and two hundred and sixty-four on an advanced one over the same two millimetres of edge.
Thirty-nine nanoseconds between the two crossings, one inside a five-nanosecond budget and the other thirty-five over it.
Sixty-four percent assembled yield from eighty percent known-good dies, fourteen packages scrapped, and four hot-plug paths of six with no handler.
Three connections of ten spanning the package, and a misplacement cost that saturates for the seven that did not need to.
Seven hundred and sixty units to build both stacks against four hundred and eighty for either alone, with five of twelve blocks shared.
One comparison of eight sound; the naming view counts six. The assembled model's summary, and the chapter's.
16. Assertions
The testbenches carry 560 checks across ten models.
Every output of every model is asserted as a value, in both builds. The output listing step reported nothing on either testbench, the sixth chapter running.
Both builds are asserted on every degenerate case. A comparison with no stack, a connection wanting no wires, a shoreline with no width, a transport carrying nothing, a design with no decisions, a substrate with no pitch, an interface wanting no lanes, a latency comparison with no traffic, an uncounted assembly, a comparison with no connections, and a stack with no blocks.
Every clamp that an input can reach is driven past its limit exactly once. More layers occupied than the stack has, a reach past its ceiling, a shoreline that saturates the wire count, a density above a hundred percent, two power figures that both saturate, a budget percentage that saturates, more streams than the model counts, a substrate that saturates the row and lane counts, a profile above a hundred percent, a latency ratio that saturates, a yield claimed above a hundred percent, more handlers than paths, a scrap count that saturates, a misplacement cost that saturates, and a build cost that saturates.
Every percentage whose numerator is clamped is asserted in the case that over-claims and in the degenerate case, which is batch 028's rule applied throughout rather than discovered by a survivor — with one exception that section 17 records.
Every error output is checked in both directions in every case. Section 5's second and third cases, section 6's third and sixth, section 7's second and sixth, section 8's second and third, section 9's second, third and last, section 10's second and sixth, section 11's second and third, section 12's second, and section 13's second, third and sixth exist to assert the quiet half. Each is a case where the pick-one view is right, and a model that alarmed on them would be unusable.
17. Mutation Testing
121 mutations, 121 killed. Fifty-nine against the first testbench, sixty-two against the second. The first run killed a hundred and sixteen and left five.
| Mutation family | Count, and what it breaks |
|---|---|
| Clamp inverted or removed | 26 — a bounded count reports the raw value, or wraps |
| Parameter-selected branches swapped | 20 — each build computes the other one's answer |
| Guard or zero-case result flipped | 14 — a degenerate input reports a confident answer |
| Boundary loosened or tightened | 3 — an equality lands on the wrong side |
| Conjunction turned into a disjunction | 9 — a two-part condition becomes a one-part one |
| Arithmetic reversed or wrong operator | 23 — a difference underflows, a product becomes a sum |
| Mask bit inverted | 6 — one condition reports the opposite of itself |
| Counter inverted or double-stepped | 20 — a decision is corrupted with no output changing |
Two of the five survivors were dominated code found before the campaign rather than by it, which is the batch-028 rules working. The assembled-yield ceiling could not be reached because a known-good rate clamped at a hundred has a square that is also bounded by a hundred, and the no-traffic guard on the energy overrun could not matter because a transport carrying nothing draws nothing. Both were removed with a comment, and neither matches domcheck.py's pattern — the first is a self-product and the second a scaled quantity, so both were hand analysis applying the rule the script automates.
Three were stimulus reaches, and one of them is the one worth recording. The single-die yield boundary — whether stacking costs anything at two dies — was untested because every case used one die or forty, and the case that fixes it turned out to be the most instructive number in section 11: two dies at eighty percent known-good assemble at sixty-four, so the second die costs thirty-six points of yield and every die after it multiplies again.
One survivor was the percentage rule, applied everywhere except one model. Section 13's sharing percentage was asserted in its degenerate case and not in its over-claim case. That is the fifth instance of this class across three batches, and it is now the single most reliable source of survivors in the whole method — which is an argument for checking it mechanically rather than by discipline, and the reason the batch prompt carries it as its own numbered rule.
18. Verification Strategy
Ask which layers each thing occupies before comparing them. Section 5. Where the overlap is zero, the question has no content.
Compute the shoreline against the wires wanted. Section 6. A die-to-die connection runs out of edge before it runs out of bandwidth.
Get the energy per bit for both paths. Section 7. It is the number that decides what a power envelope can afford to move.
Treat the transport and the protocol as two decisions. Section 8. Naming one settles one.
Name the substrate before believing a lane count. Section 9. A profile is a packaging decision.
Measure both crossings rather than assuming the tier. Section 10. Enough on-die hops can invert it.
Ask the yield question and the availability question separately. Section 11. Neither answers the other.
Check whether the connection could be in a package at all. Section 12. The geometry usually decided before either standard was consulted.
Count the shared blocks before costing two stacks. Section 13. It is neither the sum nor one of them.
19. Synthesis and Implementation Reality
UCIe's physical layer is a wide parallel interface with a sideband, trained and repaired rather than equalised, which is why its energy figure is in a different class from a SerDes.
The die-to-die adapter provides reliable delivery and flow control — CRC, retry and credits — so the protocol layer above it sees a link it can trust. That is the layer CXL is mapped onto, and it is why a CXL-over-UCIe stack has a different link layer from a CXL-over-PCIe-PHY stack while sharing the protocol block above it.
The two package profiles differ by roughly an order of magnitude in bump pitch, which is section 9's whole argument and the reason a design's lane count is not portable between them.
Known-good-die testing is what makes multi-die packages viable at all, and it is expensive: testing a bare die to the confidence a packaged part gets is a real capability that not every supply chain has.
And chiplet-based CXL devices exist: a device that is internally several dies connected die-to-die and externally a CXL endpoint on a link uses both, at different layers, exactly as section 5 describes.
The raw mode is worth knowing about because it changes what the comparison is for. UCIe can carry a stream with no standard protocol on it at all, which is what a vendor does when the two dies are its own and it wants neither PCIe's transaction semantics nor CXL's coherency. That is a legitimate and common choice, and it makes the protocol decision in section 8 a three-way one rather than a two-way one — and it is the option an architecture that only knows the two named protocols will not consider.
Retimers are the boundary case that connects the two halves of this chapter. A package link long enough to need one has effectively admitted that the channel is beyond what a direct connection can carry, which is the same admission section 6's shoreline arithmetic makes from the other direction. A design that finds itself adding retimers to an in-package connection has usually mis-drawn the floorplan rather than mis-chosen the transport.
20. Silicon Observability
Free, and from the specification. Which layers each standard defines. Section 5's whole argument is a reading exercise.
Free, and from the floorplan. Shoreline and bump pitch. Sections 6 and 9's supply side.
Cheap. The wires or lanes the interface wants. Section 6's demand side.
Cheap, and on paper. Energy per bit for both transports, from their respective specifications. Section 7.
Moderate. Measured latency across each crossing, which needs the observation points 27.9 section 11 argues for. Section 10.
Moderate, and owned by another team. Known-good-die rates. Section 11's input comes from test and packaging, not from design.
Expensive, and usually estimated. The block-level split between shared and distinct. Section 13, and it is the number a schedule is built from.
Unobtainable before the fact. Whether the product will still be one package in two years. Section 12's geometry is a product decision that outlives the architecture.
21. Debug Lab
A chiplet-based design is not meeting its power or performance targets.
Step 1 — check which connections are inside the package and which span it. Section 12. The geometry is the first thing to establish and it is free.
Step 2 — compute the energy per bit on the traffic that spans. Section 7. If in-package traffic has been routed off-package, that is the answer.
Step 3 — check the shoreline against the lanes the interface wanted. Section 6. An interface that had to narrow is a bandwidth ceiling nobody chose.
Step 4 — check which substrate profile the part actually got. Section 9. A design drawn for an advanced package and built on a standard one has a quarter of the lanes.
Step 5 — measure both crossings rather than assuming. Section 10. On-die congestion can invert the tier.
Step 6 — only then look at the protocol. 26.6. Coherency and ordering are real costs and they are rarely the difference between these two designs.
Steps 1 to 4 are reads from a floorplan and two specifications, which makes the first two thirds of this free.
22. Design Review
Which layers does each of them occupy, and is there any overlap at all?
How many wires does the connection want, and what does the shoreline supply?
What is the energy per bit on each path, and what is the power budget?
Which protocol will the transport carry, and is that decided per interface or per stream?
Which substrate, and therefore which profile?
What is the latency of each crossing, measured rather than assumed?
What is the assembled yield at this die count, and what does a scrapped package cost?
Which connections could not be inside a package at all?
How many blocks are shared between the two stacks, and how many stacks are actually being built?
23. How This Appears In Real Engineering
The comparison is made in a slide titled "CXL or UCIe", and the slide is the problem.
The most common shape is section 5 taken at face value. A team spends a review arguing about a choice that does not exist, reaches no conclusion because there is no trade-off to resolve, and leaves with the protocol decision — the one that actually mattered — still open. Nothing was decided badly; the wrong question absorbed the meeting.
The second is section 9 and it is a schedule story. A lane count is drawn against an advanced package and the product ships on a standard one, because the packaging decision was made later and by different people. The interface narrows, the bandwidth target moves, and the architecture is re-cut late.
The third is section 11 and it is the one that surprises architecture teams. The design works and the yield does not. Four dies at a known-good rate that was fine for one die assemble at a rate that is not, and the fix is a packaging and test programme rather than anything in the RTL.
The fourth is section 13. Both stacks get built — one for the in-package connection and one for the external CXL port — and the plan costed one, because the protocol block was shared and somebody counted the shared half.
The pattern is that this comparison's failures are mostly organisational: the layers, the substrate, the yield and the block split all belong to different teams, and the question as usually asked does not invite any of them into the room.
A fifth shape is worth recording because it is the one that produces the right answer for the wrong reason. A team picks UCIe because a competitor did, the product genuinely is a multi-die package, and the choice turns out correct — so the reasoning that produced it is never examined. The next product is a single die with an external port, the same reasoning is applied, and the transport is now wrong in a way nobody has a framework to notice.
The check that separates the two is section 12's geometry question, and it costs nothing: which of these connections could not be inside a package? A team that has that answer knows which of its choices were decided by physics and which by precedent.
24. Common Misconceptions
"UCIe is CXL for chiplets." One is a transport and the other is a protocol. Section 5.
"So which should we use?" Both, at different layers, or neither — the question needs splitting first. Section 8.
"It is just a shorter link." It is a different wire budget and a different energy class. Sections 6 and 7.
"We will get the lanes we need." From which bump pitch? Section 9.
"Die-to-die is always faster." Usually, and enough on-die hops can invert it. Section 10.
"Packaging is a manufacturing detail." It is a yield curve that multiplies per die. Section 11.
"We can move that connection out of the package later." At the tier difference on every crossing. Section 12.
"We share the protocol block, so it is nearly one stack." Seven of twelve blocks are not shared. Section 13.
25. Interview Reasoning
"What is the difference between CXL and UCIe?" They are not at the same layer, so the comparison as posed does not resolve. UCIe is a die-to-die physical layer and adapter; CXL is a transaction protocol. UCIe explicitly carries CXL as one of its mapped protocols, so a chiplet-based CXL design uses both. The answerable questions are which transport — die-to-die or a package link — and which protocol it carries, and they are decided by different constraints.
"When would you use UCIe?" When the connection is inside a package. That is a floorplan fact rather than a preference: millimetres of substrate let you spend thousands of wires and a fraction of a picojoule per bit, and a package link can do neither.
"Why is the energy figure so different?" Because a package link spends most of its energy overcoming a channel it cannot characterise — equalisation, retiming, drivers for a lossy board. A die-to-die channel is short and known, so the driver is small and the equalisation is minimal.
"What limits how wide a die-to-die interface can be?" The shoreline — the die edge available — times the wires per millimetre the bump pitch supports. That is why UCIe has separate standard and advanced package profiles, and why a lane count is not portable between them.
"What does a multi-die package cost you that a link-attached device does not?" Yield. Every die has to be good for the package to be good, so the assembled yield is the product rather than the average, and it falls geometrically with the die count. Against that, a packaged die cannot be hot-removed — so the whole availability problem a link-attached design has to solve simply does not arise.
"Your SoC has an in-package chiplet link and an external CXL port. How much of the stack is shared?" The protocol layer. The physical and link layers are entirely different, so the plan is the shared blocks once plus the distinct blocks per stack — not one stack, and not two.
26. Exercises
1. A stack has 9 layers; standard A occupies 3, standard B occupies 5, and they share 1. Compute the overlap and the layers apart. At what shared count does the comparison become a genuine choice?
2. A connection wants 2,048 wires over a 4 mm shoreline. Compute the wires per millimetre needed. What shoreline does a 100-wire-per-millimetre substrate require?
3. 4,000 gigabits at 0.4 pJ/bit and at 6 pJ/bit. Compute both powers and the gap. At what budget does the transport choice become forced?
4. A design names a transport, a protocol and a substrate out of five decisions. Compute the open fraction. Which of the two remaining would you expect to be hardest to close?
5. A 3 mm shoreline at 130 µm and at 40 µm pitch, 4 lanes per row. Compute the lanes at each. Which package does a 400-lane interface require?
6. A die crossing of 1,200 ps and a link of 55,000 ps against a 4,000 ps budget. Compute the gap, the ratio and both overruns.
7. 6 dies at 92% known-good. Compute the assembled yield and the loss. What known-good rate gives 80% assembled at 6 dies?
8. 20 connections, 6 of which must span the package, at 900 ps and 38,000 ps. Compute the misplacement if all 20 go on a link.
9. 18 blocks with 7 shared, 2 stacks, 55 units per block. Compute the build cost and compare it with one stack and with two independent ones.
10. Extend the assembled model with a seventh bit for a condition this chapter does not cover. Justify its position using the rule that the ordering is by how much of the difference each condition carries.
27. Summary
The question is usually malformed — UCIe is a transport and CXL is a protocol, and where the overlap is zero there is no trade-off to resolve.
The physical trade is reach against density, and a die-to-die connection runs out of die edge before it runs out of bandwidth.
Energy per bit is the reason both exist, and four and a half watts on a thousand gigabits is what a package link costs over a die crossing.
UCIe carries a protocol and does not replace one, so naming the transport settles one decision of two — or of three once the substrate is counted.
The substrate decides the profile, and the same two millimetres of edge give thirty-six lanes or two hundred and sixty-four depending on the bump pitch.
The crossings are in different latency tiers, and enough on-die hops can invert it.
A package boundary is a yield problem, geometric in the die count, and the availability problem that dominates a link-attached design does not arise at all.
Each applies where its geometry is, and the floorplan usually decided before either standard was consulted.
The stacks are not alternatives — seven of twelve blocks are distinct, so building both is neither the sum nor one of them.
Six bits, and "UCIe is CXL for chiplets" is one of them. One comparison of eight is sound; the naming view counts six.
Continue learning
Related tutorials
- Related topic
Packaging Fundamentals
Why the package is the interconnect fabric of a chiplet system rather than a container — the substrate, interposer, and bridge taxonomy, the reach-versus-density trade, and how routing, power delivery, and heat share one physical structure.
- Related topic
Proprietary Die-to-Die Links
Why private die-to-die links were the rational engineering choice, what owning both ends of an interface buys, and why they produced closed compatibility islands — including why opening a PHY specification turned out not to be sufficient.
- Related topic
Intel Chiplets on UCIe
Reading public evidence about Intel's chiplet architecture and UCIe with the same rigour Module 21 applied to a failing link — what founding membership, a donated PHY basis, and the Pike Creek interoperability test chip actually establish, why a chiplet-based product is not evidence of a UCIe link, and the difference between shipping, announced, demonstrated and inferred.
- Related topic
AMD Chiplets on UCIe
The incumbent-fabric problem — how a vendor with a mature, high-volume proprietary die-to-die fabric relates to an open standard, why UCIe's layer separation makes this a question about protocol rather than replacement, why coherence is the hard constraint, and how to read roadmap evidence without converting intent into deployment.
Standards & specifications
- Governing standard
- CXL Specification (CXL Consortium)(opens CXL Consortium in a new tab)
Defines CXL.io, CXL.cache and CXL.mem, and the coherence and memory-pooling behaviour built on them. System design and deployment topology are not mandated.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the CXL curriculum.
