UCIe · Module 7
Link Widths
What x8, x16, x32, and x64 mean in UCIe — module widths and their degraded halves, raw versus useful bandwidth per direction, width versus rate, requested versus active width, datapath adaptation, and why widening a link often moves the bottleneck.
Chapter 7.3 built the machinery that turns a set of physical conductors into a stable logical structure. This chapter asks the question that machinery makes answerable: how many of them should the system actually use?
It is tempting to treat that as arithmetic. More lanes, more bandwidth, pick the biggest number the package allows. That answer is wrong often enough to be worth an entire chapter, because every lane you activate is a physical resource allocation — PHY area on both dies, die-edge perimeter, package routes, power, and a configuration state the test plan now has to cover. And because, as §14 shows, the extra bandwidth frequently ends up sitting behind something narrower that you did not change.
1. The One-Sentence Model
Link width is a physical-resource allocation decision. Every additional lane buys parallel bandwidth by consuming PHY area, die edge, package routes, power, and verification state.
The word doing the work is allocation. A width is not a setting you choose; it is a claim on resources that several other things also want, agreed with the package team, constrained by what routed, and validated against what actually trained. Chapter 6.1 ended by saying NUM_LANES is the end of a physical decision chain rather than an RTL choice. This chapter is that sentence in full.
2. What x8, x16, x32, and x64 Actually Mean
An accuracy gate, because the notation looks like PCIe's and does not mean the same thing.
These are module widths — the number of data lanes a single UCIe module provides in each direction. They are not arbitrary link widths you dial in.
| Module | Package class | Defined degraded width |
|---|---|---|
| x64 | advanced | x32 |
| x32 | advanced | — |
| x16 | standard | x8 |
| x8 | standard | x4 |
The set grew across revisions: the original physical layer interfaces were x16 and x64, with UCIe 1.1 adding an x32 module for the advanced package and an x8 module (degraded x4) for the standard package. UCIe also supports a degraded mode in which only half of a module is active when a failure is detected on the other half — which is what the pairings in the right-hand column express.
Three consequences that the bare notation hides:
Widths are module-shaped. You cannot configure a x24 link. The legal set is small and defined, which — per Chapter 7.3 §13 — is what makes two dies from different vendors able to converge on the same structure.
Some numbers appear twice with different meanings. x32 is both a module width in its own right and the degraded half of x64. x8 is both a standard-package module and the degraded half of x16. A design reporting "x32 active" has not told you whether that is a healthy x32 module or a x64 module running on half its lanes — and those have very different implications for what to do next.
Width and package class are coupled. x64 and x32 belong to the advanced package; x16 and x8 to the standard package. This is not a licensing distinction — it is Chapter 6.1's geometry. Bump pitch and routing density determine how many conductors escape a given stretch of die edge, and the module widths encode that.
And width is not the only axis. UCIe provides for multi-module configurations of 2 or 4 modules, on both package classes. So a link's total width is module width × module count, and both terms are drawn from small defined sets.
3. Raw Bandwidth, Symbolically First
For one direction of one module:
B_raw = N_lanes × R_laneN_lanes is the number of active data lanes, and R_lane is the per-lane transfer rate. Aggregating modules multiplies by the module count:
B_raw(link) = N_modules × N_lanes × R_laneUseful bandwidth is always less:
B_usable = B_raw × efficiencyThe efficiency term accounts for everything the payload does not get: protocol and framing overhead, control and status traffic, any error-detection or correction fields, periodic events the link inserts, and idle time when nothing is offered. Take that factor from the specification revision you are implementing and from your own traffic analysis. It is not a constant, it is not a rule of thumb, and inventing it is how a bandwidth plan acquires margin that does not exist.
4. Direction Is Not Optional
The most common way to be wrong by a factor of two.
Chapter 7.3 established that UCIe data lanes are unidirectional, with separate transmit and receive sets. A x64 module therefore has 64 lanes in each direction, and B_raw computed from 64 lanes is a per-direction number.
So state which you mean, always:
- Per-direction bandwidth — what one direction carries. This is the number that matters for a traffic flow, because a flow travels in one direction.
- Aggregate bidirectional bandwidth — the sum of both directions. Legitimate to quote, and only meaningful if both directions are actually loaded.
A number without a direction is not a bandwidth figure. Doubling for "bidirectional" is only honest when the traffic is genuinely symmetric and simultaneous — and most real traffic is neither.
The engineering consequence is not pedantic. A read-dominated flow loads one direction heavily and the other lightly; quoting the aggregate makes an underprovisioned link look comfortable. Chapter 6.4 §8's rule applies directly: budget against simultaneous peak demand on the edge that carries it, not against a total.
5. Worked Arithmetic
Using verified rates: UCIe supported rates up to 32 GT/s through UCIe 2.0, and UCIe 3.0 — released in August 2025 — doubles the maximum to 48 and 64 GT/s for both UCIe-S (standard package) and UCIe-A (advanced package). Substitute the rate table from your own revision.
| Configuration | Arithmetic | Per direction |
|---|---|---|
| x16 standard @ 32 GT/s | 16 × 32 = 512 Gb/s | 64 GB/s |
| x16 standard @ 64 GT/s | 16 × 64 = 1024 Gb/s | 128 GB/s |
| x64 advanced @ 32 GT/s | 64 × 32 = 2048 Gb/s | 256 GB/s |
| x64 advanced @ 64 GT/s | 64 × 64 = 4096 Gb/s | 512 GB/s |
| x64 degraded to x32 @ 64 GT/s | 32 × 64 = 2048 Gb/s | 256 GB/s |
| 4 × x64 advanced @ 64 GT/s | 4 × 64 × 64 = 16384 Gb/s | 2048 GB/s ≈ 2 TB/s |
Every one of those is raw, per direction, before efficiency. Three things to read off the table:
- Width and rate are interchangeable for raw bandwidth. A x64 at 32 GT/s and a x32 at 64 GT/s both give 256 GB/s per direction. They are not interchangeable for anything else — §6.
- Degradation halves it exactly. A x64 module falling back to x32 delivers precisely half. Useful, and worth knowing before you discover it in the field.
- Module aggregation is where the large numbers come from. Four advanced-package modules at the top rate reach the terabytes-per-second range, and that is also where the package footprint becomes the dominant design constraint.
6. Width Versus Rate
Two roads to the same bandwidth, and they cost completely different things.
| More lanes | Higher rate | |
|---|---|---|
| PHY area | grows with lane count | grows modestly |
| Die edge / bump area | grows directly | unchanged |
| Package routes | more conductors to escape | unchanged |
| Signal integrity | per-lane stress unchanged | much harder — loss, ISI, jitter all worsen |
| Clocking | more distribution, more skew management | higher frequency, tighter budgets |
| Power | more circuits switching | more power per lane; may be better per bit |
| Internal datapath | wider, harder to close timing | narrower, but faster |
| Repair exposure | more resources that must all work | fewer resources, less margin each |
UCIe's design point leans toward width — that is the whole reason for single-ended lanes and a forwarded clock over a short channel (Chapter 7.2 §3). But the trade is real in both directions, and UCIe 3.0's move to 48 and 64 GT/s is the industry deciding that at some point the rate axis is worth the extra signal-integrity work, because width is bounded by geometry and geometry does not improve as fast as circuits do.
The practical asymmetry: width is limited by things you cannot change late — die edge, bump pitch, package routing. Rate is limited by things you can improve with engineering effort — channel design, calibration, equalisation. So width tends to be decided early and frozen, while rate can move later. That asymmetry is why §8's parameters are elaboration-time and §11's rate is not.
7. The Package Sets the Ceiling
Chapter 6.1 derived the geometry: bump pitch and achievable feature size determine how many connections escape a given length of die edge, which determines what module width the packaging class can carry, and how much edge remains determines how many modules fit. Chapter 6.2 showed an interposer relaxing that limit; 6.3 showed a bridge relaxing it locally.
Nothing new to add — only the conclusion, in this chapter's terms:
PHY width options exist inside the envelope packaging permits. The advanced package offers x64 and x32 because its pitch supports that many conductors; the standard package offers x16 and x8 for the same reason in reverse. Choosing a width in RTL does not create conductors.
8. Width as Elaboration-Time Configuration
// Illustrative PHY RTL — not UCIe normative signal naming.
package link_width_pkg;
// Physical facts, signed off by the package team.
localparam int PACKAGE_MAX_LANES = 32; // what this package routes
localparam int LANE_W = 8; // bits per lane per transfer
// The width this instance is built for.
localparam int NUM_LANES = 32;
// Everything downstream is DERIVED. Never restate a width as a new constant.
localparam int LINK_W = NUM_LANES * LANE_W;
localparam int LANE_IDX_W = $clog2(NUM_LANES);
endpackageArchitecture. The package signoff produced a routed conductor count. The design is built for a width no greater than that, and every downstream width — datapath, index, buffer — is derived from the one parameter rather than restated.
State. None. These are elaboration-time constants, deliberately: physical routing does not change at run time, so a violation should be a build failure rather than a silicon surprise.
Cycle behaviour. None.
Contract. The PHY datapath, the buffering, the mapping table's index width, and the bandwidth model all derive from NUM_LANES. The rule is one source of truth — a second constant that happens to equal 32 is a bug waiting for someone to change one of them.
Failure. Duplicated width constants that drift apart produce a design that is internally inconsistent in a way no single module looks wrong. Typically one buffer is sized for the old width and everything else for the new one, and the symptom is corruption at a specific occupancy.
DV. Catch illegality at elaboration, where it costs nothing:
// Illustrative — width legality, checked at elaboration.
generate
if (NUM_LANES > PACKAGE_MAX_LANES)
$fatal(1, "NUM_LANES=%0d exceeds PACKAGE_MAX_LANES=%0d — the package does not route them.",
NUM_LANES, PACKAGE_MAX_LANES);
if (!(NUM_LANES inside {8, 16, 32, 64}))
$fatal(1, "NUM_LANES=%0d is not a supported module width.", NUM_LANES);
endgenerateWhy generate rather than initial. An initial block runs at simulation time, so the check happens after elaboration, is skipped entirely in synthesis, and produces a runtime message in a log nobody reads. A generate-scope conditional with $fatal is evaluated during elaboration, so an illegal configuration fails the build. The earlier a physical-impossibility check fires, the cheaper it is — and this one can fire before a simulation is ever run.
9. Requested Width Is Not Active Width
The systems lesson of the chapter, and the one that appears in interviews.
// WRONG — ignores everything that decides whether a width is achievable.
assign active_width = requested_width;A requested width is an intent, expressed by firmware, fuses, or a platform policy. Whether it can be honoured depends on four things the requester does not know:
- Package support — are those conductors routed on this SKU?
- Lane health — did enough lanes train, and did repair have enough spares (Chapter 7.3 §14)?
- Module training outcome — in a multi-module link, did every enabled module train?
- Peer capability — the far die has its own package, its own lane health, and its own configuration, and the link is the intersection.
// Illustrative PHY RTL — not UCIe normative signal naming.
typedef enum logic [2:0] {
W_X4 = 3'd0,
W_X8 = 3'd1,
W_X16 = 3'd2,
W_X32 = 3'd3,
W_X64 = 3'd4
} width_code_t;
width_code_t requested_width_q; // what configuration asked for
width_code_t active_width_q; // what bring-up actually achieved
logic width_degraded;
always_ff @(posedge phy_clk or negedge rst_n) begin
if (!rst_n) begin
active_width_q <= W_X4; // narrowest legal, safe default
width_degraded <= 1'b0;
end else if (width_commit) begin
active_width_q <= negotiated_width; // outcome of bring-up + negotiation
width_degraded <= (negotiated_width != requested_width_q);
end
endArchitecture. Width is an outcome, and the design needs to represent the outcome separately from the request so that software can see it, telemetry can report it, and the bandwidth model can use the real number.
State. Two width codes and a degraded flag. The flag exists because "we are running at x32" and "we asked for x64 and got x32" are different facts with different urgency — the first is a configuration, the second is a symptom.
Cycle behaviour. active_width_q changes on exactly one cycle, at a defined commit boundary — the same quiesced-boundary discipline the lane map needs (Chapter 7.3 §7), and for the same reason: changing width under in-flight data changes the structure the receiver is reconstructing.
Contract. The datapath, the lane map, the buffering, and any bandwidth accounting all read active_width_q. Nothing reads requested_width_q except reporting.
Failure. With active = requested, the design believes it has a width it does not have. The datapath stripes across lanes that are not carrying, the far end reconstructs from a different width, and every transfer is corrupt — deterministically, on a link that trained. Worse, the reporting is wrong too, so telemetry says x64 while the hardware runs x32, and the performance investigation starts from a false premise.
DV.
// Illustrative — the active width must be one this link can actually support.
property p_active_width_supported;
@(posedge phy_clk) disable iff (!rst_n)
(phy_state_q == PHY_ACTIVE) |->
(width_to_lanes(active_width_q) <= PACKAGE_MAX_LANES) &&
(width_to_lanes(active_width_q) <= usable_lane_count) &&
(active_width_q <= peer_max_width_q);
endproperty
a_active_width_supported :
assert property (p_active_width_supported)
else $error("Active width %0s exceeds package, lane, or peer capability.",
active_width_q.name());What it encodes: three independent physical facts — what the package routes, what actually trained, and what the far die can do. What it catches: any path that sets the active width from the request without intersecting it with reality, including debug overrides and firmware tables written for a different SKU.
10. Counting What Is Actually On
// Illustrative — active lane count, derived rather than assumed.
logic [NUM_LANES-1:0] lane_enable_q;
logic [$clog2(NUM_LANES+1)-1:0] active_lane_count;
assign active_lane_count = $countones(lane_enable_q);Architecture. Width configuration says how many lanes should be on. The enable mask says which ones are. Deriving the count from the mask means the two cannot disagree.
State. The enable mask; the count is combinational.
Cycle behaviour. Recomputed whenever the mask changes — which, per Chapter 7.3, is only in a quiesced state.
Contract. Bandwidth accounting, the flow-control model, power management, and telemetry all consume it.
Failure. Keeping a separate active_lane_count register updated alongside the mask creates two sources of truth. They drift, and the bandwidth model quietly plans for lanes that are off.
Note on $countones. It is synthesisable and maps to a population-count structure, but for a wide mask that is real logic with real depth. On a timing-critical path, register the result or compute it once at commit rather than combinationally every cycle. Convenient syntax is not free hardware — a good habit to keep when reading any concise SystemVerilog.
11. Width Changes the Datapath, but Not One-to-One
A x64 PHY presents far more bits per transfer than a x8 PHY. That does not mean the internal datapath must be that wide.
The naive assumption is that the physical interface width and the internal bus width are the same number. They frequently are not, because they are optimised against different constraints: the physical width is set by the package, and the internal width is set by timing closure, area, and the clock domain the rest of the die runs in. A very wide interface feeding a narrower internal bus at a higher clock, or a narrower interface feeding a wider bus at a lower clock, are both perfectly ordinary.
Whenever they differ, something must adapt — collecting several physical beats into one internal item, or splitting one internal item across several physical beats. That adaptation is a real piece of hardware with real state.
// Illustrative PHY RTL — not UCIe normative signal naming.
// The physical interface delivers PHY_W bits per beat; the internal datapath
// consumes INTERNAL_W bits per item, assembled from SEGMENTS beats.
localparam int PHY_W = NUM_LANES * LANE_W;
localparam int SEGMENTS = INTERNAL_W / PHY_W; // must divide exactly
localparam int SEG_IDX_W = $clog2(SEGMENTS);
logic [SEG_IDX_W-1:0] segment_idx_q;
logic [INTERNAL_W-1:0] assembled_q;
logic internal_valid_q;
always_ff @(posedge phy_clk or negedge rst_n) begin
if (!rst_n) begin
segment_idx_q <= '0;
assembled_q <= '0;
internal_valid_q <= 1'b0;
end else begin
internal_valid_q <= 1'b0; // default: one-cycle pulse
if (phy_beat_valid) begin
assembled_q[segment_idx_q*PHY_W +: PHY_W] <= phy_beat_data;
if (segment_idx_q == SEG_IDX_W'(SEGMENTS-1)) begin
segment_idx_q <= '0;
internal_valid_q <= 1'b1; // complete item next cycle
end else begin
segment_idx_q <= segment_idx_q + 1'b1;
end
end
// A link event invalidates a partially assembled item — it can never be
// completed, and presenting it would deliver a fragment as a whole.
if (link_event) begin
segment_idx_q <= '0;
internal_valid_q <= 1'b0;
end
end
endArchitecture. The physical width is a package outcome; the internal width is a timing and area decision. Neither should be forced to match the other, so an adapter absorbs the difference.
State. A segment index, an assembly register, and a valid pulse. The index is the only thing that knows how much of an item exists — which makes it the thing that must be reset on any event that invalidates the partial item.
Cycle behaviour. Each valid physical beat writes one slice and advances the index. On the last segment the index wraps and internal_valid_q asserts for exactly one cycle. SEGMENTS must divide exactly — a non-integer ratio needs a genuine gearbox with its own buffering, which is a different and larger design.
Contract. Downstream logic may consume assembled_q only when internal_valid_q is high. Whatever feeds phy_beat_valid must not deliver a partial item and then stop, or the adapter waits forever holding a fragment.
Failure. Without the link_event clear, a retrain partway through assembly leaves the index mid-count. Assembly resumes from the wrong offset when traffic restarts, so every subsequent item is misaligned — one event, permanent corruption, and nothing in the datapath ever reports an error.
The wrong version is the one written when someone assumes the widths match:
// WRONG — assumes one physical beat is one complete internal item.
assign internal_valid = phy_beat_valid;
assign internal_data = phy_beat_data;When SEGMENTS > 1, this presents a fragment as a whole item, every beat. Downstream sees valid data with the right handshake and the wrong contents, at SEGMENTS times the expected rate. It is a compile-clean, simulation-plausible way to destroy every transfer.
DV.
// Illustrative — an item is presented only when fully assembled.
property p_no_output_before_full_word;
@(posedge phy_clk) disable iff (!rst_n)
internal_valid_q |-> $past(segment_idx_q == SEG_IDX_W'(SEGMENTS-1)) &&
$past(phy_beat_valid);
endproperty
// And the counter never exceeds its range.
property p_segment_idx_bounded;
@(posedge phy_clk) disable iff (!rst_n)
segment_idx_q < SEG_IDX_W'(SEGMENTS);
endproperty12. Width and Buffering
A wider link drains a transmit queue faster and fills a receive queue faster. Both halves matter.
- Upstream, a wider link relieves backpressure — the reason to widen in the first place.
- Downstream, it delivers larger bursts into the internal fabric. A receiver sized for a x16 link sees four times the arrival rate on a x64 link, and if the fabric behind it did not change, the queue in front of that fabric becomes the new constraint.
- Buffer widths scale with the interface, so a wider link means physically wider FIFOs — more area, and often harder timing at the read and write ports.
Widening the link changes where the pressure appears. It does not, on its own, change how much total work the system can do.
13. Two Bottlenecks That Look Alike
Both present as "throughput below expectation with a clean link". The occupancy pattern separates them completely.
| Observation | Link too narrow | Downstream fabric too narrow |
|---|---|---|
| PHY error counters | zero | zero |
| Link training | clean | clean |
| Transmit-side queue | saturated | drains normally |
| Receive-side queue | drains normally | saturated |
| Backpressure origin | the link itself | inside the receiving die |
| Latency under load | rises at the sender | rises after the link |
| Effect of widening the link | improves throughput | no improvement; queue moves inward |
| First move | width and rate configuration | internal fabric, consumer, memory |
The last row is the whole point. If the receive-side queue is the saturated one, widening the link makes the bottleneck arrive faster and changes nothing about total throughput — you will have spent PHY area, die edge, package routes, and power to move a queue from one side of the interface to the other.
Measure both sides before changing width. Chapter 6.4 §11 built the occupancy counter that answers this; the only addition here is that the comparison between the two sides is the diagnostic, not either number alone.
14. Width and Power
More active lanes means more driver and receiver circuitry switching, more clock distribution to feed, and more static circuitry biased. None of that scales in a way worth quoting a formula for — it depends on the process, the rate, the swing, and the activity factor, and an invented coefficient would be false precision.
The architecturally useful facts:
- Unused lanes can often be left off, which is one of the reasons
enableis a separate mask fromgood(Chapter 7.3 §4). A wide PHY running narrow should not pay the full wide power. - Energy per bit and total power are different questions. A wider, slower link can have better energy per bit and higher total power simultaneously, because it is moving more bits. Which one matters depends on whether you are limited by a battery or by a thermal envelope — and Chapter 6.5 established that in a stacked package the thermal envelope is shared.
- Provisioning wide for peak and running narrow at idle is a legitimate strategy where the architecture supports it, and it is a reason the requested-versus-active distinction is useful beyond failure handling.
15. Width and Reliability
The naive claim — wider is less reliable, because more resources must all work — is only half true.
Wider does mean more physical resources, so more opportunities for one to be marginal. But wider also means more room for repair: a spare costs proportionally less on a x64 module than on a x8 one, and UCIe's defined degraded modes mean a x64 module with a failure on one half becomes a working x32 rather than a dead link. A narrow module has less to fall back to.
A wide link is more exposed to individual lane failures and better equipped to survive them. Which effect dominates depends on the repair and degradation architecture, not on the width alone.
16. Width and Floorplan
Every lane needs a bump, an escape route, and a place on the die edge, so PHY width is a claim on contiguous die perimeter — Chapter 6.1's scarcest resource. Consequences that reach back into Module 6:
- Chiplet adjacency. A wide PHY needs a long stretch of edge facing its peer, which constrains where both dies can sit (Chapter 6.4 §4).
- Route feasibility. More conductors means more escape congestion in the package, exactly where it is already worst.
- Neighbouring PHYs compete. Two wide interfaces on the same die edge may not both fit, so a width decision on one link is a constraint on another.
Which is why width is decided early with the package team and frozen, and why §8's PACKAGE_MAX_LANES is an elaboration-time constant rather than a register.
17. Configuration Coverage
// Illustrative width coverage — not UCIe-defined.
covergroup cg_link_width @(posedge phy_clk iff width_commit);
cp_requested : coverpoint requested_width_q;
cp_active : coverpoint active_width_q;
cp_modules : coverpoint active_module_count { bins one={1}; bins two={2}; bins four={4}; }
cp_lane_fail : coverpoint (failed_lane_count > 0);
cp_congested : coverpoint sustained_congestion;
// The valuable one: was a request ever NOT honoured, and at which widths?
x_req_vs_active : cross cp_requested, cp_active;
// Was a degraded width ever driven hard enough to congest?
x_active_vs_load : cross cp_active, cp_congested;
endgroupWhy the requested × active cross is the point. Every regression hits "requested x64, got x64" on the first run. The cases that ship in marginal parts are "requested x64, got x32" and "requested x32, got x16" — the fallback paths, which are the least-travelled code and the least-reviewed reasoning. A coverage model that reports only which widths were used cannot distinguish a fallback that was exercised from one that was never entered.
Why the second cross. A degraded width that was never loaded proves nothing about whether the system behaves acceptably at that width. Bugs concentrate where a constrained configuration meets real demand — the same argument Chapter 6.4 §17 made about package configurations.
18. Verifying Width: Two Different Activities
Functional verification proves correctness: data is delivered intact, nothing is lost or duplicated, the configuration is legal, the fallback paths reach legal configurations, and width changes happen only at defined boundaries. Per legal width, the test plan needs sustained traffic, bursts, backpressure, simultaneous transmit and receive, lane-disabled cases, and end-to-end integrity checked by a scoreboard.
Performance verification proves that it is fast enough: measured throughput against the architectural budget, latency under load, and the absence of avoidable bubbles.
A link can be functionally perfect and still miss its performance target, and functional regressions will report a clean pass the whole time.
A testbench monitor measures the second:
// Illustrative TESTBENCH MONITOR — not design RTL, not intended for silicon.
// A performance monitor may or may not ship; this one is a verification aid.
logic [63:0] transferred_bytes;
logic [63:0] elapsed_cycles;
always_ff @(posedge phy_clk or negedge rst_n) begin
if (!rst_n) begin
transferred_bytes <= '0;
elapsed_cycles <= '0;
end else begin
elapsed_cycles <= elapsed_cycles + 1'b1;
if (transfer_fire)
transferred_bytes <= transferred_bytes + 64'(BYTES_PER_TRANSFER);
end
end
// Compare against the budget the architecture assumed for THIS active width —
// not against the maximum the design could theoretically reach.Why this is labelled a monitor. It is not a design requirement. Shipping performance counters in silicon is a legitimate and often valuable choice — telemetry needs them — but that is a separate decision with its own area and power cost. Presenting testbench instrumentation as design RTL blurs a line that matters at design review.
The comparison that makes it useful is against the budget for the active width. Comparing against the maximum the design could reach means a link that correctly fell back to x32 fails a performance check for behaving correctly.
19. Debugging Low Bandwidth
In order, and the first two are free:
- What width was requested? Read the configuration, not the documentation.
- What width became active? If it differs, you have your answer and the question becomes why it fell back — §9's three causes.
- How many lanes are enabled? Cross-check against the active width; a disagreement is a configuration bug.
- Are any lanes failed or disabled? A repaired link may be running degraded entirely correctly.
- What rate is the link running at? Rate fallback and width fallback are independent, and either alone accounts for a factor of two.
- Is the PHY continuously ready? Intermittent readiness loss costs bandwidth without corrupting anything.
- Is the Adapter feeding fast enough? An idle link is not a slow link. Check transmit-side occupancy — if it is empty, the problem is upstream.
- Is the receiver backpressuring? Check receive-side occupancy. If it is saturated while the transmit side drains, §13's second column applies and widening will not help.
- Are errors or retries consuming bandwidth? Retransmission is bandwidth spent on data already sent.
- Is the package or signal integrity forcing a reduced mode? A link running at a lower rate for margin reasons is behaving correctly and reporting honestly.
Steps 1 and 2 resolve a surprising fraction of cases outright, and they take one register read each. Step 7 and step 8 together decide whether widening the link would help at all — which is worth knowing before respinning a package.
20. Common Misconceptions
"x64 is always better than x16." It consumes four times the lanes, and therefore the PHY area, die edge, package routes, and power. Whether that is worth it depends on whether anything can use the bandwidth (§1, §13).
"Doubling lanes doubles application throughput." It doubles raw link bandwidth. Whether throughput follows depends on the producer, the consumer, the internal fabric, and the memory behind it (§13).
"Requested width and active width are the same." Active width is an outcome of package support, lane health, module training, and peer capability. Treating them as equal corrupts both the datapath and the telemetry (§9).
"Package routing does not constrain RTL link width." Width options exist only inside what packaging permits, which is why the legality check belongs at elaboration (§7, §8).
"Wider links only affect the PHY." They change buffer widths, internal datapath width, timing closure, die-edge allocation, chiplet adjacency, and power (§11, §12, §16).
"More lanes never affect timing closure." A wider interface means a wider internal datapath or a width adapter, and both are timing work (§11).
"A clean link running narrower must be broken." It may be a correct degraded configuration after a lane failure — which is a feature. Check width_degraded before assuming a fault (§9, §15).
"Functional verification proves performance." Functional proves correct. A functionally perfect link can miss its throughput target with every regression passing (§18).
"One bandwidth number is enough." Without a direction and an efficiency assumption it is not a bandwidth figure. Doubling for bidirectional is only honest when traffic is symmetric and simultaneous (§3, §4).
"The internal datapath must equal the physical lane width." They are optimised against different constraints and frequently differ, which is exactly why width adapters exist (§11).
21. Understanding Check
22. Summary and What Comes Next
In UCIe, x8, x16, x32, and x64 are module widths — data lanes per module per direction — drawn from a small defined set and coupled to package class: x64 and x32 for the advanced package, x16 and x8 for the standard package. Each has a defined degraded half, so x32 and x8 each appear twice in the taxonomy with different meanings. Links aggregate 2 or 4 modules, and total width is module width times module count.
Bandwidth is N_modules × N_lanes × R_lane, and it is raw and per direction until you say otherwise. UCIe supported up to 32 GT/s through 2.0 and 48/64 GT/s from 3.0, so a x64 advanced module gives 256 GB/s per direction at 32 GT/s and 512 GB/s at 64 GT/s, before efficiency. Never quote a bandwidth without its direction, and never double for "bidirectional" unless the traffic is genuinely symmetric and simultaneous.
Width and rate reach the same raw number by different roads: width costs area, die edge, package routes, and internal datapath; rate costs signal integrity and clocking margin. Width is bounded by geometry you cannot change late, which is why it is decided early and why PACKAGE_MAX_LANES is an elaboration-time check rather than a register — a physical impossibility should fail the build.
The state distinction that matters most is requested versus active width. Active is an outcome of package support, lane health, module training, and peer capability, and conflating them corrupts the datapath and the telemetry that would have told you. Alongside it: derive the active lane count from the enable mask rather than duplicating it, and remember that the internal datapath need not equal the physical width — where they differ, a width adapter must present items only when complete and must reset its partial state on any link event.
And the systems lesson: widening a link moves the pressure, it does not create capability. Transmit-side saturation means the link is the constraint and widening helps; receive-side saturation means the constraint is inside the die and widening only makes the bottleneck arrive sooner. Measure both before spending die edge on the answer.
Width tells us how many parallel resources move data. Those resources still have to agree on when a value is valid — and with a forwarded clock rather than clock recovery, that agreement is a design discipline of its own:
- 7.5 — Clocking — the forwarded-clock, source-synchronous timing model that makes wide parallel UCIe signalling possible, and the on-die clocking that supports it.
Browse the full path on the UCIe tutorials index.