CXL · Module 8
Coherent Accelerators
What changes inside an accelerator when its cache participates in host coherence: miss-status tracking, request merging, lane arbitration, outstanding-limited bandwidth, and stall counters that blame the right thing. Seven RTL models simulated, nineteen mutations, nineteen killed.
The previous three chapters built the coherence machinery. This one puts it underneath a compute pipeline and asks the question an architect actually has to answer: what does it cost, and where does the time go?
1. The Engineering Problem — Two Clients, One Cache
An accelerator's cache exists to serve the accelerator. Once it is coherent, that stops being true.
There is now a second client — the coherence path from 8.3 — and it differs from the first in every way that matters:
| Accelerator | Coherence | |
|---|---|---|
| Arrival rate set by | your workload | the host's activity |
| Can be deferred | yes, it just stalls | only briefly |
| Backs off under load | yes | no |
| You control its timing | yes | no |
The second client does not run your workload and does not care about your throughput. Every structure in this chapter exists because of that asymmetry: the cache port has to be shared, misses have to be tracked so the pipeline can continue past them, and the stall counters have to be able to say which client caused a given idle cycle.
2. The One-Sentence Model
Coherence is a second client of your cache, and it does not run your workload. So the accelerator must be built to make progress while another agent it does not control takes the cache port, answers on its own schedule, and occasionally takes a line away — which means outstanding-miss tracking, request merging, and stall accounting that can tell the two clients apart.
Call it the second client. Everything below follows from admitting it exists.
3. What This Chapter Owns
| Question | Owned by |
|---|---|
| What a borrowed line obliges | 8.1 |
| Finding the authoritative copy | 8.2 |
| Answering inbound coherence actions | 8.3 |
| The accelerator built on top of all three | this chapter |
| Moving writable ownership | 8.5 |
| Address translation | not this module |
| Generic coherency theory | Module 13 |
| CXL performance analysis in depth | Module 18 |
4. The Accelerator, End to End
The edge worth staring at is coherence agent → cache, because it is the only arrow in the picture whose rate the accelerator's designer does not control. Everything upstream of the cache is your workload; that one arrow is somebody else's.
5. Teaching-model boundary
6. RTL 1 — Misses in Flight, and the Second Miss That Costs Nothing
An accelerator with many lanes will miss the same line more than once — that is what shared data means. The structure that handles it is the miss-status tracker, and its most valuable behaviour is merging.
// A second miss to a line already in flight MERGES: one fetch, two waiters.
for (k = 0; k < NENT; k = k + 1)
if (live_q[k[1:0]] && (line_m[k] == miss_line)) begin
line_hit = 1'b1; hit_id = k[1:0];
end=== EXP1: a second miss to the same line MERGES ===
two lanes, one line : correct allocs=1 merges=1 occupancy=1
no-merge variant allocs=2 occupancy=2
one fetch serves both lanes : ok
the no-merge variant spent two entries and two fetches: ok
MSHR peak occupancy was exactly 1 : ok
fill wakes mask=00100010 (lanes 1 and 5 merged)
the fill woke both merged lanes, from one fetch : okMerging saves two scarce resources at once: a tracker entry, and an external fetch. On shared data the saving is large and it is invisible in a hit-rate number — the second lane's access was a miss, and it cost no external traffic at all.
The entry is released at the fill, never at the response:
=== EXP1b: an entry is held until the FILL, not the response ===
after response only : live=1000 occupancy=1
the entry survived the response : ok
and was released by the fill : okThe reason is sharper here than in earlier chapters: the entry holds the waiter set. Releasing at the response discards the record of which lanes are waiting, so the fill wakes nobody and those lanes hang forever.
7. Waveform — One Fetch, Two Lanes
Two lanes miss the same line; one external fetch serves both
10 cyclesallocs reaches one and stops. merges reaches one. occupancy never exceeds one. Two lane misses, one external fetch — and the wake mask at cycle 7 is 0x22, exactly lanes 1 and 5.
8. RTL 2 — Waking Exactly the Lanes That Waited
=== EXP2: the fill wakes exactly the lanes that waited ===
waiters=00100010 : correct wake=00100010 | wake-all wake=11111111
exactly the waiting lanes were woken : ok
the wake-all variant woke lanes that never asked : okWaking a lane that was not waiting delivers data to a lane that did not ask for it. On a wide machine the broken variant is not a small error — it wakes every lane on every fill, and each spurious wake is a lane resuming with data for an address it never requested.
9. RTL 3 — A Stall Has a Cause, and It Is Usually Not the One You Assume
This is the most valuable module in the chapter, because it changes what an architect does with the data.
if (BLAME_COMPUTE) begin
n_miss_q <= n_miss_q + 16'd1; // everything blamed on misses
end else begin
if (cause_miss) n_miss_q <= n_miss_q + 16'd1;
if (cause_coh) n_coh_q <= n_coh_q + 16'd1;
if (cause_mshr) n_mshr_q <= n_mshr_q + 16'd1;
endThirty stall cycles, evenly caused:
=== EXP3: a stall has a cause, and it is not always the cache ===
30 stall cycles : miss=10 coherence=10 mshr-full=10
every stall cycle attributed to exactly one cause : ok
blame-compute variant : miss=30 coherence=0 mshr-full=0
the broken variant blamed all 30 on cache misses : ok
it reports 100% miss-bound; the truth is 33%The broken counter reports the accelerator as 100% miss-bound when only a third of its stalls are misses. The other two thirds are coherence taking the cache port and the miss tracker being full — and those have completely different fixes:
| Dominant cause | The fix |
|---|---|
| Misses | a bigger cache, or better locality |
| Coherence | arbitration policy, or data placement |
| Tracker full | more MSHRs |
A team reading the inflated number buys a bigger cache. It does not help, because the cache was never the constraint.
10. RTL 4 — No Lane Is Structurally Last
Eight lanes, one cache port, forty-eight cycles of full contention:
=== EXP4: no lane is structurally last ===
8 lanes, 48 cycles : round-robin grants=48 max_lane_wait=7
fixed-priority grants=48 max_lane_wait=48
round-robin worst wait was exactly 7 = lanes minus 1: ok
fixed priority made one lane wait 48 cycles : ok
and flagged lane starvation : okBoth schedulers issued exactly the same number of grants — 48. Throughput is identical. What differs is the distribution: round-robin bounds the worst lane's wait at lanes minus one, while fixed priority left one lane waiting for the entire run.
Equal throughput, completely different behaviour. On an accelerator this matters because lanes usually have to converge — a kernel finishes when its slowest lane finishes, so the worst-case lane wait, not the average, sets the kernel's time.
11. RTL 5 — Hit Rate Is Not the Whole Story
=== EXP5: external traffic is misses MINUS merges ===
requests=40 hits=30 misses=10 merged=5 external=5
traffic split matched an independent oracle : ok
every miss either merged or went external : ok
hit rate 75%, but external traffic is only 12% of requestsA 75% hit rate suggests 25% of requests go out to the host. The measured external traffic is 12% — half the misses merged onto fetches already in flight.
The relationship worth remembering:
external fetches = requests × (1 − hit rate) − merged missesThe merge term is invisible in a hit-rate number and grows with lane count and with data sharing. A wide accelerator on shared data can have a mediocre hit rate and modest external traffic, and an architect who sizes the link from hit rate alone will over-provision it.
12. RTL 6 — The Counter That Has Now Broken Four Times
if (TWO_ASSIGNS) begin
// Both assign outstanding_q; the second one wins outright.
if (alloc) outstanding_q <= outstanding_q + 5'd1;
if (free) outstanding_q <= outstanding_q - 5'd1;
end else begin
case ({alloc, free})=== EXP6: allocate and free in the same cycle ===
4 allocated : correct=4 | two-assign variant=4
3 cycles of alloc+free together : correct=4 | two-assign=1
the case-based counter held steady : ok
the two-assign variant drifted down to 1 : okThis defect has now appeared in an event queue, a traversal work queue, an outstanding-transaction count and here — four times in this track. Two non-blocking assignments to one variable in one cycle: the second wins, so allocate-and-free together decrements instead of holding.
It survives fill-then-drain testing perfectly and appears only under steady-state traffic, which is the operating point an accelerator spends its life in.
13. RTL 7 — What Outstanding Capacity Buys
=== EXP7: outstanding capacity bounds throughput ===
4 outstanding, latency 8 : issued=36 stalled=44
unlimited : issued=80 stalled=0
the outstanding limit held every cycle : ok
more lanes cannot beat outstanding/latency : ok
achieved 36 issues in 102 cycles = 1 per 2 cycles (limit/latency = 1 per 2)
every offered cycle either issued or stalled : okFour outstanding misses and eight cycles of latency bound throughput at one issue per two cycles, and the measurement lands exactly there. Adding lanes changes nothing once that bound binds — the lanes simply stall earlier.
Illustrative worked example. Take 64 lanes, each issuing a 64-byte access every 4 cycles at 1 GHz:
offered = 64 / 4 = 16 accesses per cycle
= 16 × 64 B × 1e9 = 1024 GB/s offered
at 75% hit → 4 misses per cycle
with merging → measured 12% external ≈ 1.9 external fetches per cycleThat last number is the one to size the link against, and it is less than half what the hit rate alone suggests. Then the outstanding bound applies on top: to sustain 1.9 fetches per cycle at 200 cycles of round-trip latency needs roughly 380 outstanding misses. If the design has 64, it will sustain 64/200 ≈ 0.32 per cycle — a sixth of what the workload offers.
That arithmetic, not the hit rate, is what decides whether the accelerator is memory-bound.
14. Reading a Stall Cycle
The bottom row is why the attribution matters. Three causes, three unrelated fixes — and a merged counter picks one of them for you, usually the middle column, usually wrongly.
15. Assertions
Icarus Verilog 13.0 does not support concurrent SVA here; each property is synthesisable checker logic verified in simulation.
Safety
| Property | Intent |
|---|---|
| No entry reuse while live | alloc |-> !live[entry] |
| Fill wakes only waiters | wake |-> waiters |
| No orphan fill | fill |-> live[entry] |
| One cause per stall | exactly one of miss / coherence / tracker |
| Outstanding never negative | free && zero |-> error |
| Outstanding within limit | outstanding <= MAXO |
| Traffic conservation | misses == merged + external |
Liveness
| Property | Assumption |
|---|---|
| Every waiting lane is eventually woken | the fill arrives |
| Every lane eventually gets the port | round-robin, not fixed priority |
| A merged miss completes with its host entry | the entry is not released early |
Performance goals
| Goal | Measured by |
|---|---|
| Worst lane wait bounded | max_lane_wait_q = lanes − 1 |
| Throughput at the outstanding bound | issues per cycle vs MAXO/latency |
| Merge rate visible | merged vs external |
| Stalls attributable | the three cause counters |
16. Mutation Testing
Nineteen mutations. Nineteen killed — but this page needed the most repair of the batch.
| Mutation | Result |
|---|---|
| Second miss to a live line allocates again | killed |
| Merged lane not recorded as a waiter | killed |
| Entry freed at the response, not the fill | killed |
| Fill for a dead entry not flagged | killed |
| Miss refused for lack of an entry not counted | killed |
| MSHR peak occupancy lags by one | killed |
| Fill wakes every lane | killed |
| Spurious wakes not detected | killed |
| Coherence stalls not counted | killed |
| Unattributed stall not flagged | killed |
| Stall with two causes not flagged | killed |
| Round-robin degenerates to fixed priority | killed |
| Round-robin pointer never advances | killed |
| Max lane wait lags by one | killed |
| Miss = merged + external law disabled | killed |
| Every miss counted as external | killed |
| Outstanding counted with two assignments | killed |
| Outstanding limit not enforced | killed |
| Stalls waiting for a slot not counted | killed |
The first run scored 9 of 19 — the weakest result in the batch, and it was correct to be. The testbench was exercising the modules without checking most of what they produce. The eight escapes were:
- Three signals never asserted at all: MSHR peak, max lane wait, and the bandwidth model's stall count. The designs computed them; nothing looked.
- Two paths never stimulated: the response-before-fill sequence, and the merged waiter set surviving to the wake.
- Three checkers unreachable by construction: unattributed stall, two-cause stall, and the traffic conservation law — all true by construction under legal stimulus.
Two of the repairs are worth naming.
A bound became an equality, for the second time in this batch. The lane-wait assertion said "no more than 8"; the design has an exact answer of 7. 8.3 hit the identical problem on its coherence-wait counter. Twice in one batch is a pattern, not an accident: when a design has a computable exact value, asserting a bound is settling.
The merge test was checking the wrong end. It verified that a merge happened — allocation count stayed at one — without ever verifying that the merged lane was woken. A design that merges correctly and forgets the waiter passes the first check and hangs a lane forever. Asserting the wake mask closed it:
fill wakes mask=00100010 (lanes 1 and 5 merged)
the fill woke both merged lanes, from one fetch : ok17. Verification Plan
| Area | Approach |
|---|---|
| Merging | Two lanes, one line; wake mask asserted, not just the count |
| Entry lifetime | Response and fill driven separately |
| Capacity | Fill the tracker; refusals counted |
| Wake | Correct mask against a wake-all variant |
| Stall causes | All three, plus no-cause and two-cause on an abuse instance |
| Lane fairness | Sustained contention; exact worst-wait assertion |
| Traffic | Independent oracle; conservation proven reachable |
| Outstanding | Simultaneous allocate and free |
| Throughput | Measured against the outstanding/latency bound |
The coverage cross is lane concurrency against line sharing against coherence activity. The middle axis is the one that produces merging, and a testbench where every lane touches a distinct line will never exercise it — which is exactly the testbench most people write, because distinct addresses are easier to generate.
18. Silicon Observability
| Counter | Diagnoses |
|---|---|
| stalls split by cause | whether the cache, coherence or the tracker is the constraint |
| MSHR peak and full-refusals | whether outstanding capacity is the bound |
| merge rate | how much external traffic sharing is saving |
| external fetches vs misses | the real link demand |
| max lane wait | fairness, and kernel convergence time |
| spurious wakes | a lane-return bug, which is otherwise silent |
| outstanding count and peak | the bandwidth-delay working point |
The single most valuable is the stall split, because it is the one that redirects effort. Everything else refines a decision; that one decides which decision you are making.
19. Debug Lab
The accelerator runs beautifully and the rest of the system degrades
COH-STARVEDassign grant_coh = coh_req && !accel_req; // accelerator first, always
assign grant_accel = accel_req;Accelerator throughput counters look excellent. Host threads touching shared data stall for long periods. No errors anywhere, and the device reports itself busy and productive.
24 cycles of contention : fair accel=20 coh=4 max_coh_wait=4
: strict accel=24 coh=0 max_coh_wait=24Strict accelerator priority on the shared cache port. A busy accelerator always has work, so coherence never wins — measured as zero grants in twenty-four cycles of contention.
The accelerator's own metrics cannot show this, because from its point of view everything is going well. The failure is visible only in the rest of the system, which is why it is usually reported as a host problem.
assign promote = (coh_wait_q >= COH_AGE_LIMIT);
assign grant_coh = coh_req && (!accel_req || promote);Age the coherence request and promote it after a bounded wait. Measured cost: 24 accelerator grants falling to 20, in exchange for a stated worst-case coherence latency.
Assert an exact bound on the worst coherence wait, and run sustained contention with both clients asserting every cycle. A test where the accelerator idles occasionally lets coherence through and hides the policy entirely.
Lanes hang forever waiting for a fill that already happened
MSHR-EARLY-FREEif (rsp_valid) live_q[rsp_id] <= 1'b0; // released when answeredIndividual lanes stop making progress and the kernel never completes. The data did arrive — the cache holds the line — but the lanes that missed on it are still blocked. It worsens with lane count and with sharing.
after response only : live=1000 occupancy=1
the entry survived the response : okThe tracker entry holds the waiter set, and it was released at the response rather than at the fill. The waiter bitmap went with it, so when the fill completed there was no record of which lanes to wake.
This is worse than the equivalent bug in earlier chapters. There, an early release risked a late message landing on the wrong occupant; here it discards the list of who needs telling, so the lanes wait forever with nothing to time them out.
if (rsp_valid && live_q[rsp_id]) answered_q[rsp_id] <= 1'b1; // answered
if (fill_done) live_q[fill_id] <= 1'b0; // finishedTwo events, two flags, and the waiter set lives until the fill.
Drive the response and the fill as separate stimuli and assert occupancy after each. A testbench that models them as one event cannot distinguish the two designs.
A lane resumes with data for an address it never requested
SPURIOUS-WAKEassign wake = fill ? {NLANE{1'b1}} : '0; // wake everyoneSilent wrong results that scale with lane count and miss rate — worst when the machine is busiest. Nothing in the coherence path is violated and no error is set. Reducing lane count makes it rarer, which misleads the investigation toward a parallelism bug.
waiters=00100010 : correct wake=00100010 | wake-all wake=11111111
the wake-all variant woke lanes that never asked : okThe fill woke every lane rather than the recorded waiters. Lanes that were not blocked on this line resume anyway and consume the fill data as though it answered their access.
It is silent because a wake is not a data-integrity event in any checker — the line is valid, the coherence state is right, and only the routing of the response is wrong.
assign wake = fill ? waiters_m[fill_id] : '0;
if (wake[k] && !waiters[k]) spurious_wake_err <= 1'b1;Wake the recorded set, and assert that no woken lane was outside it.
Assert the wake mask against the waiter set on every fill, per lane. Counting wakes in aggregate passes on the broken design whenever the waiter count happens to match.
A second miss to a line already in flight allocates a second entry
NO-MERGEassign alloc_ok = miss && found; // no in-flight checkThe miss tracker fills far sooner than the miss rate predicts, external traffic is roughly double the model, and both worsen sharply on shared data. Stall counters blame the tracker being full.
two lanes, one line : correct allocs=1 merges=1 occupancy=1
no-merge variant allocs=2 occupancy=2Each miss allocated its own entry and issued its own fetch, even when a fetch for that line was already outstanding. Two scarce resources are consumed where one would do, and the duplication scales with how many lanes share data — exactly the workload coherence is for.
for (k = 0; k < NENT; k = k + 1)
if (live_q[k] && (line_m[k] == miss_line)) begin line_hit = 1'b1; hit_id = k; end
assign merged = miss && line_hit;
// attach this lane to the existing entry rather than allocatingGenerate deliberately shared addresses. A testbench where every lane touches a distinct line exercises the merge path zero times while reporting high coverage.
A miss is refused and the request is lost
MSHR-FULL-DROPif (miss && found) allocate(); // and if !found, nothing at allLanes occasionally hang under heavy miss bursts. No counter moves, no error is set, and the tracker is not full by the time anyone looks. It correlates with burstiness rather than with average miss rate.
5 distinct lines into 4 entries : occupancy=4 full-refusals=1
the fifth miss was refused and counted : okA miss arriving at a full tracker was silently discarded rather than stalled. The lane believes its access is in progress; nothing is tracking it.
The refusal must both stall the lane and increment a counter — the counter because a full tracker is a sizing signal, and the stall because the alternative is a lost request with no owner.
end else if (miss) begin
n_full_q <= n_full_q + 8'd1; // refused: count it, and stall the lane
endFill the tracker deliberately and assert that offered misses equal allocated plus merged plus refused. Conservation is what makes a silent drop visible.
Every performance investigation blames the cache
BLAME-COMPUTEif (stalled) n_miss_stall_q <= n_miss_stall_q + 1; // one bucketTelemetry reports the accelerator as almost entirely miss-bound. The cache is enlarged; throughput barely moves. The exercise repeats at the next size.
30 stall cycles : miss=10 coherence=10 mshr-full=10
blame-compute variant : miss=30 coherence=0 mshr-full=0
it reports 100% miss-bound; the truth is 33%All stall cycles were attributed to cache misses regardless of cause. Two thirds were coherence holding the port and the tracker being full — neither of which a bigger cache addresses.
The counter is not merely imprecise; it points the entire optimisation effort at the wrong subsystem, and it does so confidently.
if (cause_miss) n_miss_q <= n_miss_q + 1;
if (cause_coh) n_coh_q <= n_coh_q + 1;
if (cause_mshr) n_mshr_q <= n_mshr_q + 1;
if (hot != 1) attribution_err <= 1'b1; // exactly one cause per cycleAssert that the cause counters sum to the stall count, and prove the no-cause and two-cause checkers can fire using an instance driven with illegal stimulus. On legal stimulus they are true by construction and verify nothing.
Outstanding count drifts to zero under steady traffic
TWO-ASSIGN-COUNTERif (alloc) outstanding_q <= outstanding_q + 1;
if (free) outstanding_q <= outstanding_q - 1;The reported outstanding count falls steadily under sustained traffic until it reads near zero while the tracker is demonstrably busy. Any throttling driven by the count then misbehaves. Fill-then-drain tests pass perfectly.
3 cycles of alloc+free together : correct=4 | two-assign=1
the two-assign variant drifted down to 1 : okTwo non-blocking assignments to one variable in one cycle: both read the pre-edge value and the second wins, so allocate-and-free together decrements instead of holding.
This is the fourth appearance of this defect in this track — after an event queue, a traversal work queue and an outstanding-transaction count. It recurs because each path is written independently and each looks correct alone.
case ({alloc, free})
2'b10: outstanding_q <= outstanding_q + 1;
2'b01: outstanding_q <= outstanding_q - 1;
default: ; // both or neither: hold
endcaseExhaustive by construction, so the both-at-once case cannot be forgotten.
Test the cross of the two conditions, not each separately. Steady state is where accelerators live, and it is precisely where this defect shows.
20. Industry Roles
Accelerator architect. Sizes the cache, the MSHR count and the outstanding budget. The key realisation from §13 is that outstanding capacity, not cache size, is usually what binds on a latency-heavy link — and that the merge rate makes hit rate a poor proxy for link demand.
RTL engineer. Owns the tracker, the lane arbiter and the wake path. The two defects that bite are entry lifetime — release at fill, not response — and the waiter set surviving to the wake.
DV engineer. Must generate shared addresses to exercise merging, must drive coherence concurrently with compute, and must assert exact values where the design has them. A testbench of distinct addresses with an idle coherence path covers almost none of this chapter.
Performance engineer. Lives in the stall split and the outstanding/latency bound. The first question on any regression is which stall cause moved.
Firmware and runtime. Data placement decides the sharing pattern, which decides both the merge rate and the coherence traffic. It is often the cheapest lever available.
21. Design Review
- How many misses can be outstanding, and how does that compare to bandwidth × latency?
- Does a second miss to a line in flight merge, or allocate?
- When is an MSHR entry released — at the response or at the fill?
- Does the fill wake exactly the lanes that waited?
- Can coherence starve? Can a lane starve?
- What happens when the miss tracker fills — stall, or drop?
- Can a line be evicted while an MSHR references it?
- Which counters separate cache misses from coherence stalls?
- Is the worst lane wait bounded, and is the bound asserted exactly?
- What happens on reset with outstanding misses and waiting lanes?
22. Common Misconceptions
| Belief | Correction |
|---|---|
| The cache serves the accelerator | It serves two clients; one is not yours |
| Hit rate predicts link traffic | Merging can halve it |
| A stall means the cache is too small | A third of stalls here were coherence |
| More lanes means more throughput | Not past the outstanding/latency bound |
| Fixed priority and round-robin differ in throughput | Identical throughput; different worst case |
| An MSHR entry frees when the response arrives | It holds the waiter set until the fill |
| Merging is an optimisation | It is also what keeps the tracker from filling |
| Coherence traffic scales with your workload | It scales with the host's |
23. Interview Reasoning
24. Exercises
-
Analysis. An accelerator reports 90% hit rate, 40% stall cycles, MSHR peak equal to its depth, and zero coherence stalls. Name the constraint, the one change that would help, and the change that would not.
-
Design. Add per-lane fairness weighting so some lanes may be prioritised. State what breaks in the worst-lane-wait bound and what new counter is needed to detect it.
-
RTL task. Extend the MSHR so a merged waiter can attach to an entry whose response has already arrived but whose fill has not. State the new race and the assertion that catches it.
-
DV task. Write the address-generation policy that exercises merging, and explain why the natural policy — distinct addresses per lane — gives high coverage numbers while testing none of it.
-
Debug task. Lanes intermittently resume with data for addresses they never requested. Give your investigation order and the single counter that confirms the cause.
-
Design review. A colleague proposes removing merging because "it complicates the tracker and the hit rate is already good". Give the strongest version of that argument, then name the two resources it costs and the workload where it is most expensive.
25. Summary
Coherence is a second client of your cache, and it does not run your workload.
- The cache now has two clients with opposite characteristics: one you control and one you do not.
- MSHRs let the pipeline continue past a miss, and merging makes a second miss to an in-flight line cost nothing.
- An entry is released at the fill, never at the response — it holds the waiter set, and losing that hangs lanes.
- A fill must wake exactly the lanes that waited. Spurious wakes deliver data to lanes that never asked, silently.
- Measured: a broken stall counter reported 100% miss-bound where the truth was 33%, with the rest split between coherence and a full tracker — three causes, three different fixes.
- Measured: round-robin and fixed priority delivered identical throughput; the worst lane wait was 7 versus 48. Kernels converge, so the tail sets the runtime.
- Hit rate is a poor proxy for link traffic. A 75% hit rate produced 12% external traffic because half the misses merged.
- Measured: four outstanding at eight cycles of latency bounds throughput at one issue per two cycles, exactly. More lanes do not help past that.
- The two-assignment counter defect appeared for the fourth time in this track.
- Verification lesson: the first mutation run scored 9 of 19. Three signals the design computed were never named by any assertion, and the merge test verified the optimisation triggered without verifying the deferred work happened.
Chapter 8.5 takes the hardest question in the module: what must be proven before writable ownership of a line may move.
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.
