CXL · Module 9
Host Access to Device Memory
What the host must build so an ordinary load can reach CXL memory: exactly one target per address, a tag pool that gates issue, out-of-order response matching, link credits, per-access timeouts, and near-versus-far latency measured apart. Seven RTL models simulated, nineteen mutations, nineteen killed.
Chapter 9.1 built the device's half of the window contract. This chapter is the other half: what the host must do so that an ordinary load instruction — one that knows nothing about CXL — arrives at that device and comes back correctly.
1. The Engineering Problem — The Instruction Does Not Know
A core executes a load. That instruction contains an address and nothing else. It does not name a device, it does not know a link exists, and it cannot be told to wait longer because the data is far away.
Everything that makes CXL memory reachable therefore happens after the instruction and before the data comes back:
| The instruction provides | The host must supply |
|---|---|
| an address | which target owns it |
| — | a transaction identity |
| — | somewhere to put the answer when it arrives |
| — | a bound on how long to wait |
| — | permission to send at all |
Miss any of those and the failure is not a slow load. It is a core that stalls forever, a value delivered to the wrong register, or two targets answering one address.
And the host cannot retry in software. There is no driver in this path. Whatever the hardware does is what happens.
2. The One-Sentence Model
The host turns an address into an obligation it must track. Decode picks exactly one target, a tag makes the request identifiable, credits make it sendable, and a deadline makes its failure detectable — and the tag is not free again until the core has the value, not merely until the data arrived.
Call it the tracked obligation. The device in 9.1 validates identity; the host allocates it, and that difference drives everything here.
3. What This Chapter Owns
| Question | Owned by |
|---|---|
| Generic host decode, outstanding table, home agent | 3.1 |
| The device's window contract | 9.1 |
| The host's path to device memory | this chapter |
| Ordering, atomicity, the coherence contract | 9.3 |
| Read flows end to end | 9.4 |
| Write flows and completion ordering | 9.5 |
| Latency/throughput cost of CXL memory | 9.6 |
| Latency anatomy and performance modelling | Module 18 |
Deliberately not repeated: 3.1 already builds a host address decoder and outstanding-transaction table as generic structures. This chapter asks what changes when one of the targets is across a link — which turns out to be the tag pool, response reordering, credits and timeouts, none of which local DRAM needs.
4. The Exchange
Architectural. The arrows are obligations, not named messages.
The two loads at the top and bottom are the same instruction with different addresses. Everything that distinguishes them happens in the host, and the bottom path has no tag, no credit and no deadline because none is needed for a target that cannot disappear.
5. Teaching-model boundary
6. RTL 1 — Exactly One Target
// Exactly one target must claim an address. Two is a configuration error
// that produces silent corruption; zero is an abort, which is loud.
assign n_targets = 2'd0 + in_dram + in_cxl;
assign to_dram = in_dram && (OVERLAP_ALLOWED || !in_cxl);
assign to_cxl = in_cxl && (OVERLAP_ALLOWED || !in_dram);
assign unclaimed = req_valid && (n_targets == 2'd0);=== EXP1: one instruction, one target ===
local DRAM address : dram=1 cxl=0 targets=1
a local address goes to local memory : ok
CXL address : dram=0 cxl=1 targets=1
a CXL address goes to the CXL target : ok
the SAME load instruction reached both : ok
unowned address : unclaimed=1 routed=0
an address nobody owns is refused, not guessed : ok
every probe classified into exactly one target : okZero targets and two targets fail in opposite directions. Zero is an abort — loud, immediate, and the system tells you. Two is silent: writes land in one place and reads come from another, and no error is raised anywhere.
So the design refuses rather than guesses:
=== EXP1b: when two ranges overlap, refuse — do not guess ===
overlapped address : targets=2 dram=0 cxl=0
both ranges claimed it: a configuration error : ok
and it was routed nowhere rather than guessed : ok
overlap was reported, not silently resolved : okNote that this experiment needed a router deliberately configured wrong. Overlapping ranges are a system misconfiguration; driving them into the instance under test would trip that instance's own checker and be scored a routing defect. That distinction — my fault versus the configuration's fault — has now come up in every chapter of this module.
7. RTL 2 — The Tag Pool Gates Issue
The host allocates transaction identity, so the tag pool is a hard limit on how many accesses can exist at once — and running out is a core stall that never reaches the link.
end else if (issue) begin
// No free tag. The core stalls; nothing reaches the link.
n_tagstall_q <= n_tagstall_q + 16'd1;
end=== EXP2: the host owns the tag pool ===
8 issues into 8 tags : outstanding=8 can_issue=0 issued=8
with every tag live the host cannot issue : ok
9th issue attempt : tag stalls=1
the core stalled; nothing reached the link : ok
peak outstanding was exactly the tag count, 8 : okA tag stall is invisible from the device. The link goes quiet, the device sees no traffic, and its own counters show it idle — while the host is stalled. This is the single most misleading performance situation in the module, and the only counter that identifies it lives on the host side.
8. RTL 3 — A Tag Frees When the Core Has the Value
=== EXP3: a tag frees when the CORE has the value ===
data back only : correct outstanding=8 | free-on-data outstanding=7
data arriving does not free the tag : ok
the broken variant reclaimed it early : ok
the core consuming the value freed it : ok
and the broken variant then freed a dead tag : okData arriving at the host is not the end of the transaction — the value still has to reach the instruction that asked for it. Freeing at data-arrival lets the next load claim a tag whose value is still in flight to a register.
This is the third distinct place in Modules 8–9 where the same rule appears: release the resource at completion, not at the answer. Here the resource is the tag and the completion is the core consuming the value.
9. Waveform — Tag Exhaustion and an Out-of-Order Answer
Four tags, five loads, and a response that arrives out of order
11 cyclesRead issued against tag_stalls at cycle 6: the counter that moved is on the host. Nothing appeared on the link, so a device-side trace of this moment shows a perfectly idle device. That is why the tag-stall counter exists.
Then read data_tag at cycle 7 — it is 2, not 0. Tag 0 was issued first and has not answered. The host must be built for that.
10. RTL 4 — Responses Return Out of Order
assign eff_tag = MATCH_BY_ORDER ? oldest_q : rsp_tag;
...
// The value must reach the request that asked for it.
if (rsp_data != exp_m[eff_tag[2:0]]) mismatch_err <= 1'b1;=== EXP4: responses come back out of order ===
tag 2 answers first : correct delivers=2 | by-order delivers=0
the tag chose the destination, not arrival order : ok
every value reached the request that asked : ok
the host observed and tolerated reordering : ok
matching by arrival order delivered wrongly : okReordering is not an exceptional case here. Two requests can land in different banks, hit different queue depths, or one can find the media busy — and the second issued is routinely the first answered. A host that assumes order works perfectly against local DRAM and corrupts data against a CXL device.
n_reorder_q counts how often a response was not the oldest outstanding. It is worth exposing: a rising reorder rate means the device's internal parallelism is being exercised, which is usually good news, and a rate of zero on a busy link suggests the device is serialising internally.
11. RTL 5 — Credits Carry Back-Pressure Across a Link
A combinational ready signal cannot cross a link — by the time it arrives, the sender has already sent. Credits solve that by telling the sender in advance how much room exists.
assign send = want_send && (NO_CREDIT_CHECK || (credits_q != 4'd0));
case ({send, credit_return})
2'b10: credits_q <= credits_q - 4'd1;
2'b01: credits_q <= credits_q + 4'd1;
default: ; // both or neither: hold
endcase=== EXP5: credits carry back-pressure across a link ===
12 sends, 8 credits : sent=8 blocked=4 credits=0 min=0
the host sent exactly its credit budget, then stopped: ok
credit low-water mark reached exactly zero : ok
no-credit-check variant : sent=12
the unchecked variant sent past the device's room : ok
and flagged an overrun the device cannot absorb : ok
returned credits restored send capacity : okmin_credits_q is the counter worth having in silicon. Credits sitting near their maximum means the link is over-provisioned for the workload; credits pinned at zero means the device is the constraint and the host is throttled. The low-water mark distinguishes those, and an average does not.
Note the exhaustive case again — the sixth appearance in this track of the two-assignment counter defect, this time on credits, where the consequence is over-sending into a device with no room.
12. RTL 6 — Which Access Is Late
=== EXP6: which access is late ===
tag 5 hung : timeouts=1 reported=5 max_age=21
the per-access timer named the access that hung : ok
and it named tag 5 specifically : ok
shared timer reported tag 2
one shared timer blamed a healthy access : okThe shared timer reported tag 2 — an access issued long after tag 5 and progressing normally. The per-access timer reported tag 5, which is the one that never answered.
This matters more on the host than anywhere else in the module, because the recovery action is to fail a load that a core is waiting on. Failing the wrong one takes down healthy work and leaves the real hang in place.
13. RTL 7 — A Memory Access Is Not a Register Access
Both are loads from software's point of view, and they behave completely differently.
// Register access serialises; memory access pipelines. That is the whole
// performance difference, and it is invisible to a functional test.
assign may_pipeline = use_mem_path;=== EXP7: a memory access is not a register access ===
device memory : correct mem_path=1 pipeline=1 | mmio-variant mem_path=0 pipeline=0
a memory access pipelines : ok
routed as a register access it serialises : ok
a genuine register access does not pipeline : ok
the mis-routing variant flagged a serialised access : okThis is a functionally invisible catastrophe. A device-memory access routed down the register path returns the right value every time. Every functional test passes. What changes is that accesses stop overlapping — throughput collapses to one access per round trip, which at 250 ns is roughly 4 million accesses per second instead of tens of millions.
The distinction is 7.1's: CXL.io reaches device registers, CXL.mem reaches device memory. Getting the routing wrong does not break anything a test can see.
14. Quantitative Reasoning — Tags, Credits and the Two Limits
Illustrative. 9.1 established that filling a 32 GB/s link at 250 ns needs 125 outstanding requests. The host must supply that concurrency, and it has two independent ways to fall short:
| Limit | Set by | Symptom |
|---|---|---|
| tag pool | host outstanding capacity | core stalls, link idle |
| credits | device's advertised room | host blocked, link idle |
Both look identical on the link — quiet — and they have different owners. A tag shortage is the host's problem; a credit shortage means the device is full. The counters that separate them are n_tagstall_q on the host and min_credits_q on the link.
Measured in this chapter's teaching configuration:
8 issues into 8 tags : outstanding=8 can_issue=0
9th issue attempt : tag stalls=1
12 sends, 8 credits : sent=8 blocked=4 credits=0 min=0Near and far, measured apart
=== EXP8: near memory and far memory, measured apart ===
local : n=30 sum=240 max=8
CXL : n=10 sum=250 max=25
the target split matched an independent oracle : ok
far memory measured slower than near, per access : ok
10 CXL accesses cost 250 cycles; 30 local cost 240
tag stalls counted exactly (5 of 40) : okTen CXL accesses cost slightly more total time than thirty local ones. At roughly 3× the per-access latency, a quarter of the traffic consumed just over half the time.
That ratio is the number software people need, and it is why the split must be measured per target. An aggregate mean over this mix reports 12.25 cycles — a number that describes no access that actually happened, and that hides both the 8-cycle local case and the 25-cycle far case.
15. The Two Ceilings
All three must independently cover the bandwidth-delay product, and sizing two of them correctly buys nothing if the third is short. The design value of giving each its own counter is that the binding one becomes identifiable instead of inferred — and two of the three live on the host, which is the half teams instrument least.
16. Assertions
Icarus Verilog 13.0 does not support concurrent SVA here, so every property is synthesisable checker logic verified in simulation.
Safety
| Property | Intent |
|---|---|
| One target per address | n_targets <= 1 on a sane configuration |
| No routing of an unowned address | unclaimed |-> !to_dram && !to_cxl |
| No unknown data | data_back |-> tag live |
| Value reaches its requester | deliver |-> data == expected[tag] |
| No tag freed twice | consumed |-> tag live |
| Never send without credit | send |-> credits > 0 |
| Timeout names a late access | timeout |-> age[tag] >= LIMIT |
| One path per request | never both memory and register path |
Liveness
| Property | Assumption it needs |
|---|---|
| An issued access eventually completes or times out | the deadline fires |
| A stalled issue eventually proceeds | a tag frees |
| A blocked send eventually goes | credits are returned |
Each names its assumption. The tag-stall property in particular is only true if something does complete — a host whose device never answers stalls permanently and violates no safety property at all.
Performance goals
| Goal | Measured by |
|---|---|
| Tag pool sized to bandwidth-delay | peak_q against the 9.1 calculation |
| Credits not the binding limit | min_credits_q above zero |
| Reordering tolerated, not merely survived | n_reorder_q non-zero with zero mismatches |
| Near/far cost visible | latency split by target |
17. Mutation Testing
Nineteen mutations. Nineteen killed.
| Mutation | Result |
|---|---|
| Overlapping ranges both route | killed |
| An unowned address is not detected | killed |
| Two claims on one address not flagged | killed |
| Tag-exhaustion stalls not counted | killed |
| Tag frees on data, not on consume | killed |
| Data for an unknown tag not flagged | killed |
| Outstanding peak lags by one | killed |
| Responses delivered in arrival order | killed |
| Misdelivered data not detected | killed |
| Reordering not observed | killed |
| Host sends with no credit | killed |
| Credits counted with two assignments | killed |
| Credit-blocked sends not counted | killed |
| Any outstanding access may be declared late | killed |
| Reported timeout tag not checked | killed |
| CXL-sourced accesses not counted | killed |
| Tag stalls not counted | killed |
| Memory always uses the memory path | killed |
| Serialised memory access not flagged | killed |
The first run scored 16 of 19, and the two escapes were instructive in different ways.
"Overlapping ranges both route" survived because the good instance never saw an overlap. The mutation removes the !in_cxl guard, which is unobservable while the configured ranges are disjoint — and mine were. The repair was not more stimulus but a differently configured instance, which is a distinct idea from the abuse instances used elsewhere: the stimulus was legal, the configuration was the thing that had to be wrong.
"Data for an unknown tag" needed illegal stimulus, and got a dedicated instance asserting both that the stray data is flagged and that it completes nothing.
There is also a repair worth recording that mutation testing did not find — my own baseline caught it. Driving the overlapping configuration into the instance under test made that instance's own checker fire, and the testbench scored it as a routing failure. The correct device was failing its own test. That is now the fourth chapter in which the fix was to separate "a fault I am responsible for" from "a fault I am correctly reporting".
18. Verification Plan
| Area | Approach |
|---|---|
| Routing | Local, CXL, unowned, and overlapping (dedicated misconfigured instance) |
| Tag pool | Exhaust it; assert the stall and the exact peak |
| Tag lifetime | Data-arrival and consume driven separately |
| Reordering | Three outstanding, answered 2-0-1, distinguishable data per tag |
| Credits | Over-send past the budget; low-water mark asserted exactly |
| Timeout | A hung access with healthy traffic present |
| Path selection | Memory, register, and a mis-routing variant |
| Latency | Independent oracle on the near/far split |
The coverage cross is target × response order × resource state: local / CXL / unowned, crossed with in-order / out-of-order / never, crossed with tags-available / tags-exhausted / credits-exhausted. The tags-exhausted column is the one a testbench with a generous tag pool never reaches, and it is where the most misleading performance bug lives.
19. Silicon Observability
| Counter | Diagnoses |
|---|---|
| routed-to-DRAM vs routed-to-CXL | how much traffic is actually far |
| unclaimed addresses | a decode or configuration gap |
overlap_err | two targets claiming one address — must be zero forever |
| tag stalls | host-side concurrency shortage |
| outstanding peak | whether the tag pool is sized to bandwidth-delay |
min_credits_q | whether the device is the constraint |
| reorder count | device internal parallelism |
| timeout count with tag | which access hung |
| latency split by target | near/far cost, for software placement |
The pair that resolves the most common "CXL is slow" escalation is tag stalls against minimum credits. Both produce a quiet link and low throughput; one means the host cannot track more accesses and the other means the device cannot hold more. They have different owners and different fixes, and an aggregate utilisation number distinguishes neither.
20. Debug Lab
Writes land in one place and reads come from another
OVERLAPassign to_dram = in_dram; // no exclusivity check
assign to_cxl = in_cxl;Silent corruption confined to one address range. Data written is read back as something else, or a value appears to change without being written. No error anywhere, and it only occurs after a firmware or configuration change.
overlapped address : targets=2 dram=0 cxl=0
both ranges claimed it: a configuration error : okTwo decode ranges overlapped and both claimed the address, so the request went to whichever path won — potentially a different one for reads and writes.
The mechanism is a misconfiguration, but the design fault is that it was resolved silently instead of refused. Hardware that guesses when the configuration is ambiguous converts a loud setup error into silent data corruption.
assign n_targets = in_dram + in_cxl;
assign to_dram = in_dram && !in_cxl;
assign to_cxl = in_cxl && !in_dram;
if (n_targets > 1) overlap_err <= 1'b1;Refuse and report. An unrouted access is an abort the system can act on; a guessed one is not.
Test with a deliberately misconfigured decoder as its own instance, and assert both that overlap is detected and that nothing is routed. Note this is configuration-space coverage, not stimulus coverage — a correct configuration can never reach it.
Throughput is low and the link looks idle
TAG-STARVEDhost_outstanding #(.NTAG(8)) u_track (...); // sized for local DRAMBandwidth to CXL memory is a fraction of the link's capability. Link utilisation is low, the device reports itself idle, and its queues are empty. Every device-side counter says there is no problem.
8 issues into 8 tags : outstanding=8 can_issue=0
9th issue attempt : tag stalls=1The tag pool is smaller than the bandwidth-delay product. Once every tag is live the core stalls, and nothing reaches the link — so the shortage is completely invisible from the device.
A pool sized for local DRAM is badly wrong for CXL: the same eight tags that cover an 80 ns local access cover only a fraction of a 250 ns remote one.
Size from the arithmetic in 9.1 — bandwidth × latency ÷ transaction size — and expose n_tagstall_q and peak_q so the sizing is evidence rather than opinion.
Always instrument the initiator side. A device-side-only view of a memory path cannot distinguish "nobody is asking" from "the asker cannot ask".
A load returns the value belonging to a different load
ORDER-MATCHdeliver_tag <= oldest_q; // assume responses return in order
oldest_q <= oldest_q + 1;Wrong data with every counter balancing — issued equals delivered, no timeouts, no errors. It never happens with one access outstanding and gets worse as concurrency rises. It does not reproduce against local memory at all.
tag 2 answers first : correct delivers=2 | by-order delivers=0The host assumed responses return in issue order. A CXL device has internal parallelism — banks, queues, varying media latency — so the second request issued is routinely the first answered.
The design works perfectly against local DRAM, which is why the assumption survives into a CXL-capable host.
assign eff_tag = rsp_tag; // identity, not order
if (rsp_data != exp_m[eff_tag]) mismatch_err <= 1'b1;Match on the tag, and keep a per-tag expectation so misdelivery is detectable at all.
Return responses deliberately out of order with distinguishable data per tag. A testbench model that answers in order cannot fail this test, and in-order is the easiest model to write.
A tag is reused while its value is still in flight
FREE-ON-DATAif (data_back) busy_q[data_tag] <= 1'b0; // freed when data arrivesRare wrong values under high concurrency, worse when the tag pool is small. The failing load is well-formed and its data arrived correctly; the corruption appears in a different load issued shortly afterwards.
data back only : correct outstanding=8 | free-on-data outstanding=7
and the broken variant then freed a dead tag : okData arriving at the host is not the end of the transaction — the value still has to reach the register that asked for it. Freeing the tag at data-arrival lets the next load allocate it while the previous value is still being delivered.
The reuse distance is one tag-pool depth, which is why a small pool makes it far more likely.
if (data_back) arrived_q[data_tag] <= 1'b1; // answered
if (consumed) busy_q[consumed_tag] <= 1'b0; // finished — only nowTwo events, two flags. The tag lives until the core has the value.
Drive data-arrival and consume as separate stimuli and assert outstanding after each. A testbench that models them as one event cannot tell the two designs apart.
The host over-sends into a device with no room
NO-CREDITassign send = want_send; // credits ignoredRequests are lost or the link errors under sustained load. It appears only when the device is slower than the host — a faster device masks it entirely — so it can pass on one platform and fail on another with identical firmware.
12 sends, 8 credits : sent=8 blocked=4
the unchecked variant sent past the device's room : okA combinational ready signal cannot cross a link: by the time the sender sees it, the request is already in flight. Credits exist to give the sender advance permission, and ignoring them means sending into a device with nowhere to put the request.
assign send = want_send && (credits_q != 0);
if (send && (credits_q == 0)) overrun_err <= 1'b1;Spend a credit per send, restore on return, and expose the low-water mark so the budget can be validated against real traffic.
Test with a device deliberately slower than the host. A testbench whose device always has room never exercises the credit path at all.
A timeout fails a healthy load and the real hang persists
SHARED-TIMERif (|wait_q) gtimer_q <= gtimer_q + 1;
if (gtimer_q >= LIMIT) begin timeout <= 1; timeout_tag <= any_waiting; endTimeout errors name loads issued moments earlier that were progressing normally. Recovery fails the wrong access; the genuinely hung one is still outstanding, so the error recurs against arbitrary different tags.
tag 5 hung : timeouts=1 reported=5 max_age=21
shared timer reported tag 2
one shared timer blamed a healthy access : okOne shared deadline knows that something is late and cannot know which. The reported tag is whichever the picker happened to select — measured here as tag 2, an access issued long after the one that actually hung.
On the host this is worse than elsewhere, because the recovery action fails a load a core is waiting on.
for (k = 0; k < NTAG; k++) if (wait_q[k]) age_m[k] <= age_m[k] + 1;
if (wait_q[k] && (age_m[k] >= LIMIT)) begin hit = 1; pick = k; end
if (hit && age_m[pick] < LIMIT) wrong_tag_err <= 1'b1;An age per access, and a checker written about the reported tag's own age.
Test a hung access with healthy traffic present. One outstanding access cannot distinguish the two designs, and it is the natural test to write.
Correct data at a fraction of the expected throughput
MEM-AS-MMIO// Device address — use the register path.
assign use_reg_path = req_valid && addr_is_device;Every value is correct. Every functional test passes. Bandwidth to device memory is one access per round trip — roughly two orders of magnitude below expectation — and no error or counter indicates anything is wrong.
device memory : correct mem_path=1 pipeline=1 | mmio-variant mem_path=0 pipeline=0
routed as a register access it serialises : okDevice memory was routed down the device register path. Register access is uncached and serialising by design — one at a time, each completing before the next begins — while memory access is meant to pipeline.
The results are identical, which is why this survives every functional test. Only a throughput measurement reveals it.
assign use_mem_path = req_valid && addr_is_mem;
assign may_pipeline = use_mem_path;
if (addr_is_mem && !may_pipeline) mem_serialised_err <= 1'b1;Route by what the address is, and assert that a memory access can pipeline.
Assert a performance property, not just a functional one: a memory access must be able to have another outstanding behind it. Functional correctness is not enough to detect this class at all.
21. Design Review
- Can two decode ranges claim one address, and what happens if they do?
- Is the tag pool sized from bandwidth × latency, or inherited from the local-DRAM design?
- When does a tag free — on data arrival or on consumption?
- Does response matching use identity or arrival order?
- What stops the host sending into a device with no room?
- Is the timeout per-access, and does it name the tag?
- Does device memory pipeline, and what asserts that it does?
- Which counter distinguishes a tag shortage from a credit shortage?
- Is latency measured separately for near and far targets?
- What happens on reset with loads outstanding?
22. How This Appears in Real Engineering
Architecture. The tag pool is the host-side twin of the device's outstanding table, and both must cover the same bandwidth-delay product. Sizing one and not the other caps throughput just as effectively.
RTL. Response matching by tag rather than order is a small change that is easy to get wrong and impossible to catch functionally against local memory.
DV. Out-of-order responses, an exhausted tag pool and a deliberately slow device are all directed scenarios. None arises naturally from a well-behaved model.
Post-silicon. Tag stalls versus minimum credits is the first diagnostic split on any "CXL is slow" report, and both counters must exist before tape-out.
Software and placement. The near/far latency split is what tells a runtime whether a hot data structure has landed in far memory — a placement decision, not a hardware one, and usually the cheapest thing to change.
23. Common Misconceptions
| Belief | Correction |
|---|---|
| The load instruction knows about CXL | It carries an address and nothing else |
| Decode ambiguity is harmless | Two claims is silent corruption; refuse instead |
| A quiet link means the device is slow | It may be a host tag shortage |
| Data arriving ends the transaction | The core must receive the value |
| Responses come back in order | They routinely do not |
| Ready signals work across a link | Credits exist because they do not |
| One timeout is enough | It cannot say which access hung |
| A correct value means a correct path | Mis-routing to MMIO is functionally invisible |
24. Interview Reasoning
25. Exercises
-
Calculation. A host has 16 tags and issues 64-byte accesses to a device with a 300 ns round trip. Compute the maximum achievable bandwidth. Then state how many tags are needed to reach 24 GB/s, and which counter would confirm the shortage in silicon.
-
Analysis. A system reports
tag_stalls=0,min_credits=0,reorder_count=0, and low CXL throughput. Explain what each of the three readings rules out, and name the remaining candidate. -
RTL task. Extend
target_routeto three targets with a programmable priority for ambiguous addresses. State why priority is a worse answer than refusal, and what you would have to add to make it defensible. -
Assertion task. Write the property that catches a tag freed before the core consumed the value, and explain why it must span two events rather than one cycle.
-
Debug task. Bandwidth to CXL memory is two orders of magnitude below expectation and every functional test passes. Give your investigation order and the single property that identifies the cause.
-
Design review. A colleague proposes matching responses by arrival order "because the device answers in order in practice". Give the strongest version of that argument, then name what makes it false and the one workload that exposes it fastest.
26. Summary
The host turns an address into an obligation it must track.
- A load instruction carries an address and nothing else. Everything else is hardware.
- Decode must yield exactly one target. Zero aborts loudly; two corrupts silently, so ambiguity must be refused rather than resolved.
- The host allocates transaction identity; the device validates it. The tag pool is therefore a hard ceiling on concurrency.
- A tag shortage is invisible from the device — nothing reaches the link, and every device counter reports idle.
- A tag frees when the core consumes the value, not when data arrives.
- Responses return out of order as normal operation. Matching by arrival order works against local DRAM and corrupts against CXL.
- Credits carry back-pressure across a link, because a ready signal cannot. The two-assignment counter defect appeared for the sixth time in this track.
- Timeouts must be per-access. The shared timer blamed tag 2 while tag 5 was the one that hung.
- Routing device memory down the register path is functionally invisible and costs roughly two orders of magnitude of throughput.
- Measured: 10 CXL accesses cost more total time than 30 local ones — which is why latency must be split by target rather than averaged.
- Verification lesson: one mutation survived because the configuration, not the stimulus, was too well-behaved — configuration space needs coverage too.
Chapter 9.3 asks what the access actually guarantees: ordering, atomicity, and the coherence contract that makes device memory usable as memory rather than merely reachable.
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.
