CXL · Module 8
Cache Ownership Transfer
What must be proven before writable ownership of a line may move: every required holder revoked, the authoritative value secured, conflicting work blocked, and a bounded abort. Why an acknowledgement bitmap is not enough once a late ack can arrive from a finished transfer. Eight RTL models simulated, twenty-two mutations, twenty-two killed.
Everything in this module has been building to one question, and it is the hardest in CXL.cache:
What must the system prove before writable ownership of a line moves?
1. Ownership Is Not Data Movement
The instinct is that transferring ownership means copying a line from A to B. It does not, and the distinction is the chapter.
A line's bytes may exist in several places at once. Several caches may hold copies; memory holds one too. Nothing about that is a problem — copies are how caching works.
What must be unique is permission to modify. Ownership transfer is therefore not a data-movement operation with a permission side-effect; it is a permission operation that sometimes has a data side-effect:
| Question | Answer |
|---|---|
| Where are the bytes? | possibly many places |
| Who may modify them? | exactly one agent, ever |
| What does transfer move? | the right, not the bytes |
| When must bytes move? | only when the old owner's copy is the newest |
A transfer that copies the line and updates a state field has done the easy half. The hard half is proving that nobody else can still write it, and that proof is what this chapter builds.
2. The One-Sentence Model
Revoke before grant. Writable ownership may be granted only once every other holder that could conflict has been revoked and has said so, and only once the authoritative value is safe — so a transfer is a proof obligation with two independent conditions, and a permission that moves before either is discharged has not been transferred, it has been duplicated.
Call it the proof obligation. The grant is not the transfer; the proof is.
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 around it | 8.4 |
| Proving a transfer is safe to commit | this chapter |
| The generic coherence conversation | 3.4 |
| Generic ownership theory | Module 13 |
| Annotated end-to-end transfer flows | Module 14 |
4. The Exchange
Architectural. The arrows are obligations, not named messages — this repository's source policy does not permit naming CXL.cache messages.
The final arrow is the whole chapter. It is the last thing to happen, and everything above it is the proof that it is safe.
5. The Ownership State Machine
Teaching model — these are not CXL.cache state encodings. Three things to read off it.
The two transient states are where the proof happens, and nobody holds writable permission in either. Not the old owner, not the new one. That is not a gap to be minimised; it is the correctness property, and §8 shows it as a flat pair of zeros in the waveform.
Every commit arrow carries the same two conditions, joined by AND. One is not enough, and §9 is about what happens when the second is dropped.
Every transient has an abort arrow. A transfer that cannot finish must return the line to a defined owner, or the line is locked forever — §11.
6. Teaching-model boundary
7. RTL 1 — Who Must Be Revoked
// The requester is not on its own invalidation list, and an agent that
// holds nothing is not either.
assign self_bit = (4'd1 << requester);
assign mask = INCLUDE_SELF ? sharers : (sharers & ~self_bit);=== EXP8: who must be revoked ===
sharers=1101 requester=0 : correct mask=1100 targets=2 | include-self mask=1101
the requester was excluded from its own list : ok
the include-self variant waits on itself forever : ok
only the requester holds it : targets=0 zero-target transfers=1
zero targets is legitimate and fast : ok
and the fast path was counted separately : okTwo results worth separating.
The requester must not be on its own list. It will never acknowledge its own invalidation, so a transfer that waits for it waits forever — a hang produced by an off-by-one in a mask.
Zero targets is legitimate and fast. If nobody else holds the line, there is nothing to revoke and the transfer can commit as soon as the value is secured. Counting that path separately matters because a workload with mostly zero-target transfers has cheap ownership movement, and one with many sharers does not — and the difference is invisible in an average latency.
8. RTL 2 — The Bitmap Is Not Enough
This is the chapter's central module and the place it goes beyond 3.4.
A set-based tracker handles duplicates correctly:
=== EXP2: a duplicate acknowledgement ===
set-based : awaiting=0100 remaining=1 duplicates=1 busy=1 epoch=2
a duplicate ack was idempotent: still waiting on 2 : ok
and it was classified as a duplicate, not progress : ok
count-based variant : remaining=0
the count-based variant declared itself complete : okThe count-based variant is complete after two acknowledgements from the same agent, because a counter knows how many and not who. That is 3.4's argument, confirmed.
Now the question a bitmap cannot answer. A line may be transferred many times. Suppose a transfer completes, a new one begins, and an acknowledgement from the previous transfer arrives late:
// An acknowledgement is usable only if it belongs to THIS transfer and
// names an agent this transfer is actually waiting on.
assign fresh = NO_EPOCH ? 1'b1 : (ack_epoch == epoch_q);
assign expected = awaiting_q[ack_from];=== EXP3: a LATE acknowledgement from a finished transfer ===
new transfer epoch=3 (previous was 2), awaiting=1010
stale ack from agent 1, epoch 2 : correct remaining=2 stale=1
no-epoch remaining=1
the epoch tag rejected the straggler : ok
without epochs it satisfied an unrelated obligation: okThe straggler belonged to epoch 2 and arrived during epoch 3. Agent 1 is legitimately on the new transfer's required list — so the bitmap sees a bit it is waiting for, from an agent it is waiting on, and clears it. An obligation was discharged by a message that had nothing to do with it.
Bit 2 means "agent 2". It does not mean "agent 2, for this transfer". The epoch supplies the missing half of the identity, and without it the transfer can commit while a real holder has not yet answered — which is the single-writer violation this whole chapter exists to prevent.
Note what is not wrong here. The bitmap is still right about duplicates, still names the non-responders, and is still better than a count. It is simply incomplete once a line can be transferred more than once — which is always.
9. Waveform — A Complete Transfer, With a Duplicate and a Straggler
Revoke two holders, absorb a duplicate and a stale ack, then commit
10 cyclesRead device_writable and host_writable together across cycles 2 to 7: both are zero for six consecutive cycles. That flat pair is the single-writer invariant made visible — during the proof, the right to modify belongs to nobody, and the system is strictly less capable than before the transfer began.
That is the cost of correctness, and it is why transfer latency matters: every cycle in the transient is a cycle in which the line cannot be written by anyone.
The measured tail confirms the two rejections:
two_writers=0 grant_before_acks=0 stale=1 dup=1 transfers=110. RTL 3 — Two Conditions, Joined by AND
// The single condition that authorises a commit. GRANT_EARLY removes it.
assign commit = GRANT_EARLY ? 1'b1 : (acks_complete && data_secured);=== EXP4: ownership commits only when both conditions hold ===
device asked : state=1 transient=1 dev_writable=0 host_writable=0
nobody holds writable permission mid-transfer : ok
nothing complete yet : correct state=1 | grant-early state=2
the correct FSM stayed transient : ok
the grant-early variant already committed : ok
acks done, data NOT secured : state=1 | grant-early state=2
acks alone are not sufficient to commit : ok
data secured too : state=2 dev_writable=1 transfers=1
both conditions met, ownership committed : ok
never two writable owners : okAcknowledgements alone are not sufficient. The middle case is the one designs get wrong: every holder has been revoked, so no one else can write the line — and the old owner's modified value has not yet been handed over. Committing there produces a line whose new owner cannot supply its own current value.
Permission and data are separate conditions with separate evidence, and the AND is not a formality.
11. RTL 4 — Permission May Not Move Ahead of the Value
=== EXP6: permission may not move ahead of the value ===
old owner dirty : data_secured=0 | ignore-dirty secured=1
not secure while the value is still owed : ok
the ignore-dirty variant moved permission anyway : ok
after handover : data_secured=1 with-data transfers=1
the value was handed over before permission moved : ok
the ignore-dirty variant lost the only copy : okNote the asymmetry with a clean old owner: nothing is owed, data_secured is immediately true, and the transfer costs only the revocation. Transfers from a clean owner are cheap; transfers from a modified owner carry a data movement, and the counters separate them because the two have different costs and different causes.
12. RTL 5 — A Transfer That Cannot Finish
if (abort) begin
n_abort_q <= n_abort_q + 8'd1;
// Naming the non-responders is what makes the abort actionable.
blame_q <= awaiting;=== EXP7: an abandoned transfer must not lock the line forever ===
agent 2 never answered : aborts=1 blame=0100 max_age=13
the transfer was aborted rather than left hanging : ok
and it named the agent that did not answer : ok
worst age was exactly 13 = limit plus the abort cycle: ok
without a bound the line stayed locked : okThis is the worst failure in the chapter if it is missing. An abandoned transfer leaves the line permanently transient, so every future request for it is deferred forever — the line becomes unusable for the life of the system, and nothing reports why.
The abort must also name the non-responders. blame=0100 says agent 2; without it the recovery knows a transfer failed and not which agent to reset or quiesce.
The measured worst age is 13, not 12. The age advances on the cycle the abort fires, so the peak includes that cycle. Asserting the exact value rather than a bound is what pins that off-by-one — see §14.
12b. RTL 6 — Block the Line, Not the Cache
// Only the SAME line is blocked. Blocking everything would serialise the
// whole cache behind one transfer.
assign conflict = line_transient && (req_line == busy_line);=== EXP5: a transient line blocks its own line only ===
same line, transient : correct accept=0 | no-block accept=1
the conflicting request was deferred : ok
the no-block variant let it through : ok
different line : accept=1
an unrelated line proceeded concurrently : okBoth halves are properties. Accepting a conflicting request on a transient line lets a second transaction act on a line whose ownership is mid-proof. Blocking unrelated lines is not a safety bug at all — it is a throughput disaster, serialising the entire cache behind one transfer, and a mutation that does it is caught by a test for the thing that should still work.
13. The Proof, as Structures
Two arrows enter the FSM as evidence and one as an escape. The design is legible as exactly that: two independent proofs that must both arrive, and a bound that ends the attempt if either never does.
14. Quantitative Reasoning — What a Transfer Costs
Illustrative. Decompose it:
T_transfer ≈ serialisation
+ mask computation
+ slowest required acknowledgement
+ value handover (only if the old owner is modified)
+ commitThe term that matters is the third, and it is a maximum, not a sum:
| Acknowledgements | Cost |
|---|---|
| Issued in parallel, answered independently | the slowest one |
| Issued serially, or answered through one path | the sum |
With N sharers each taking L cycles: parallel is L, serial is N × L. The difference is the entire scalability argument for ownership transfer — a design whose acknowledgements serialise degrades linearly in sharer count, and one whose acknowledgements are parallel does not degrade at all in the common case.
That is why the zero-target counter from §6 is worth having: it distinguishes transfers with no revocation at all — the cheapest case — from transfers whose cost is set by whoever is slowest.
Measured, using this chapter's teaching latencies:
=== EXP9: every started transfer completes or aborts ===
started=24 completed=15 aborted=6 with-sharers=8 with-dirty=5
transfer counts matched an independent oracle : ok
no transfer finished without starting : ok
mean latency 12 cycles, worst 20Transfers from a modified owner cost 20 against 8 for clean ones — two and a half times, entirely the value handover.
15. Assertions
Icarus Verilog 13.0 does not support concurrent SVA here, so every property is synthesisable checker logic verified in simulation. The assert property form states the intent.
Safety
| Property | Intent |
|---|---|
| Single writer | never device_writable && host_writable |
| Revoke before grant | commit |-> acks_complete |
| Value before permission | commit |-> data_secured |
| Stale acks are inert | ack_epoch != epoch |-> no progress |
| Duplicates are inert | !awaiting[from] |-> no progress |
| Unrequired acks are inert | classified as duplicate, not as progress |
| No self-invalidation | mask[requester] == 0 |
| Transient blocks its line | transient && same_line |-> !accept |
| Transient blocks only its line | different_line |-> accept |
| Owner is not a sharer | owned |-> !sharers[owner] |
| Transfer conservation | complete + abort <= start |
Liveness
| Property | Assumption it needs |
|---|---|
| A started transfer eventually ends | the bound fires if acknowledgements do not arrive |
| A deferred request eventually proceeds | the holding transfer ends |
| A line always has a defined owner | every abort returns it to one |
The first is the one the no-timeout variant violates. Note it is stated as ends, not completes — an abort is a legitimate ending, and conflating the two is how a design ends up with no bound at all.
Performance goals
| Goal | Measured by |
|---|---|
| Transfer latency bounded | max_age_q against the limit |
| Cheap transfers identified | zero-target count |
| Data cost separated | with-dirty vs clean transfer counts |
| Aborts rare | abort count against start count |
16. Mutation Testing
Twenty-two mutations. Twenty-two killed.
| Mutation | Result |
|---|---|
| Stale acknowledgement accepted | killed |
| Unexpected agent's ack clears a bit | killed |
| One acknowledgement clears every obligation | killed |
| Stale acknowledgements not counted | killed |
| Epoch never advances between transfers | killed |
| Commit before the value is secured | killed |
| Device writable during the transient | killed |
| Host writable through the transient | killed |
| Granting before acknowledgements not flagged | killed |
| Single-writer check disabled | killed |
| Transient line accepts a conflicting request | killed |
| Transient line blocks every other line | killed |
| Permission moves while the value is owed | killed |
| Losing the authoritative value not flagged | killed |
| Abandoned transfer never aborted | killed |
| Abort does not name the non-responders | killed |
| Max transfer age lags by one | killed |
| Requester on its own invalidation list | killed |
| Self-invalidation not flagged | killed |
| Zero-target transfers not counted | killed |
| Transfer conservation law disabled | killed |
| Owner also listed as a sharer | killed |
The first run scored 16 of 22. Two of the six survivors taught something.
"An unexpected agent's ack clears a bit" survived a test that looked like it covered it. The test drove an acknowledgement from an agent that was not on the required list and asserted remaining was unchanged — and it was unchanged, on both designs, because clearing an already-clear bit is invisible in the bitmap. The observable difference is classification: the correct design counts it as a duplicate, the mutant counts it as progress.
and it was classified as a duplicate, not progress : ok
exactly one genuine acknowledgement was counted : okA state change with no state change is still a bug, and only the counters can see it.
"Max transfer age lags by one" survived a bound. For the third time in this batch, an assertion said "no more than" where the design has an exact answer. Pinning it to 13 killed the mutation — and pinning it required first understanding why 13 rather than 12, which is itself worth knowing.
The other four were the now-familiar shapes: three checkers unreachable by construction, needing either a deliberately broken variant or an instance reserved for illegal stimulus.
17. Verification Plan
| Area | Approach |
|---|---|
| Revocation set | Sharers with and without the requester; zero-target case |
| Acknowledgements | Genuine, duplicate, unrequired, stale-epoch — each asserted |
| Epochs | A straggler from a finished transfer during a live one |
| Commit conditions | Neither, acks only, both — three separate checks |
| Single writer | Continuous, plus a variant that makes the checker fire |
| Transient blocking | Same line refused, different line accepted |
| Data authority | Clean owner and modified owner, counted separately |
| Abort | A never-answering agent; blame mask asserted |
| Conservation | Independent oracle; law proven reachable |
The coverage cross is sharer count against acknowledgement behaviour against owner cleanliness: zero / one / many sharers, crossed with prompt / duplicate / stale / missing acknowledgements, crossed with clean / modified old owner. The stale column is the one that does not exist in a single-transfer test plan, and it is where the epoch bug lives.
18. Silicon Observability
| Counter | Diagnoses |
|---|---|
| transfers started / completed / aborted | the health of the transfer path |
| abort count with blame mask | which agent stops answering |
| stale acknowledgement count | epoch or ordering problems in the fabric |
| duplicate acknowledgement count | a partner retransmitting |
| zero-target transfer count | how much ownership movement is cheap |
| with-dirty transfer count | how much carries a data handover |
| max transfer age | margin against the timeout |
| deferred same-line requests | contention on hot lines |
Stale acknowledgement count is the one that would otherwise be invisible. In a correct design it should be rare and non-zero — stragglers happen. A rising rate means the fabric is reordering more than expected or a partner is retransmitting, and it is the earliest warning that the epoch mechanism is doing real work rather than sitting idle.
19. Debug Lab
Two agents write the same line and one write disappears
EARLY-GRANT// Ownership requested — hand it over.
if (state == TO_DEVICE) state <= DEVICE_OWNED;Silent data loss on shared lines. Two agents each believe they own the line; one agent's writes survive and the other's vanish, and which one wins depends on timing. Single-agent tests pass perfectly.
nothing complete yet : correct state=1 | grant-early state=2
the grant-early variant already committed : okOwnership was granted without waiting for the required revocations. Issuing an invalidation is not the same as it having taken effect — until an agent acknowledges, it may still be writing the line.
The window is short, which is why it needs concurrency to appear, and the loss is silent because both writes were legal from their own agent's point of view.
assign commit = acks_complete && data_secured;
if (commit && !acks_complete) grant_before_acks_err <= 1'b1;Gate the commit on the proof, and keep the checker — in silicon an early grant has no other symptom.
Assert continuously that no two agents hold writable permission, and prove the assertion can fire with a variant that lets both leak into the transient. On a correct design the property is true by construction and verifies nothing until then.
A transfer completes while a real holder has not answered
STALE-ACKif (ack_valid && awaiting_q[ack_from]) awaiting_q[ack_from] <= 1'b0; // no epochExtremely rare single-writer violations on lines that are transferred frequently. Every acknowledgement in the trace is well-formed and from a legitimate agent. It correlates with transfer rate rather than with load, and never reproduces on a bench with deterministic timing.
stale ack from agent 1, epoch 2 : correct remaining=2 stale=1
no-epoch remaining=1An acknowledgement from a previous transfer arrived after that transfer finished and a new one had begun. The bitmap records identity by agent, not by agent-and-transfer, so it saw a bit it was waiting for from an agent it was waiting on, and cleared it.
An obligation was discharged by a message that had nothing to do with it, and the transfer then committed while a real holder had not yet answered.
assign fresh = (ack_epoch == epoch_q);
// ... and epoch_q advances on every new transfer
if (ack_valid && !fresh) n_stale_q <= n_stale_q + 8'd1; // count, do not applyTag each transfer with an epoch, carry it on the acknowledgement, and discard anything from a previous one — counting it, because a rising stale rate is diagnostic.
Test a straggler explicitly: complete a transfer, start another, then deliver an acknowledgement from the first. A single-transfer test plan cannot express this, which is why the bug survives thorough-looking verification.
A duplicate acknowledgement completes a transfer early
COUNT-NOT-SETif (ack_valid) ack_count <= ack_count - 1; // how many, not whoTransfers occasionally commit with a holder still active, producing the same silent double-writer loss as Debug Lab 1 but from a different cause. It correlates with any condition that makes an agent retransmit.
set-based : remaining=1 duplicates=1
count-based variant : remaining=0A counter knows how many acknowledgements arrived, not which agents sent them. Two acknowledgements from the same agent satisfy a count of two while one agent has never answered.
A set fixes this because clearing an already-clear bit is idempotent — this is 3.4's argument, and it remains correct. It is simply not sufficient once transfers repeat, which is Debug Lab 2.
if (ack_valid && fresh && awaiting_q[ack_from]) awaiting_q[ack_from] <= 1'b0;
else if (ack_valid && fresh) n_dup_q <= n_dup_q + 8'd1;Track who, and classify anything that is not progress as a duplicate rather than silently ignoring it.
Deliver two acknowledgements from one agent and assert both that the remaining set is unchanged and that the duplicate counter moved. The first check alone passes on designs that ignore the event entirely.
The new owner cannot supply the line's current value
VALUE-LOSTassign commit = acks_complete; // data not consideredThe new owner holds the line with full permission and stale contents. Reads return a previous version; the modification made by the old owner is gone. It only happens when the old owner had modified the line.
old owner dirty : data_secured=0 | ignore-dirty secured=1
the ignore-dirty variant lost the only copy : okPermission and data were treated as one thing. Revoking every holder proves nobody else can write the line; it says nothing about where the line's current value is. If the old owner held the only modified copy, that value must be handed over before its permission is dropped.
The transfer succeeded by every permission-related measure and destroyed the data.
assign commit = acks_complete && data_secured;
if (data_secured && owed_q) value_lost_err <= 1'b1;Two independent conditions, and a checker for the one whose failure is silent.
Test both a clean old owner and a modified one, and count them separately. A test where the old owner is always clean satisfies data_secured trivially and never exercises the condition.
A second request acts on a line whose ownership is mid-proof
TRANSIENT-BYPASSassign accept = req; // transient state not consultedCorruption on hot lines under concurrent access. Two transfers for one line interleave, and the resulting ownership state matches neither. It scales with contention and is essentially unreproducible in isolation.
same line, transient : correct accept=0 | no-block accept=1A line in a transient state has an in-progress proof: a required set partly acknowledged, a value possibly in flight. A second request accepted against that line begins a second proof over the same metadata, and the two interleave.
The transient is not a state the line is in so much as a claim the hardware is making, and accepting conflicting work invalidates the claim.
assign conflict = line_transient && (req_line == busy_line);
assign accept = req && !conflict;Defer the conflicting request — same line only.
Assert both halves: a conflicting request is refused, and an unrelated line is still accepted. Testing only the first passes on a design that blocks the entire cache.
One line becomes permanently unusable
NO-ABORT// Wait for acknowledgements.
if (all_acked) commit(); // and if they never arrive?A single address stops working. Every access to it is deferred forever; every other address is fine. The system does not report an error — it reports nothing at all, because from its point of view a transfer is simply still in progress.
agent 2 never answered : aborts=1 blame=0100 max_age=13
without a bound the line stayed locked : okNo bound on the transfer. An agent that never acknowledges — because it was reset, wedged, or the message was lost — leaves the line permanently transient, and the transient blocks every subsequent request for it.
This is worse than a hung transaction: the transaction is lost and the line is unusable for the life of the system.
assign abort = busy && (age_q >= LIMIT);
if (abort) begin blame_q <= awaiting; n_abort_q <= n_abort_q + 8'd1; endBound the transfer, return the line to a defined owner, and name the non-responders — an abort that does not say who failed is not actionable.
Test an agent that never answers, and assert both that the abort fires and that the blame mask identifies the right agent. Also assert an exact worst-case age; a loose bound hides an off-by-one in the timer.
A transfer waits forever for an acknowledgement from itself
SELF-INVALIDATEassign mask = sharers; // includes the requesterOwnership requests hang, but only when the requester already holds a shared copy of the line — which is the common case for a read-then-write sequence. If the requester holds nothing, the transfer works fine, so the bug looks data-dependent.
sharers=1101 requester=0 : correct mask=1100 | include-self mask=1101
the include-self variant waits on itself forever : okThe requester was included in its own invalidation set. It will never send itself an acknowledgement, so the transfer waits until the timeout and then aborts — and the retry does exactly the same thing.
The upgrade path is where this bites, because that is precisely when the requester is already a sharer.
assign mask = sharers & ~(1 << requester);
if (mask[requester]) self_invalidate_err <= 1'b1;Test the upgrade case specifically — a requester that already holds a shared copy. A test where the requester holds nothing produces the same mask under both designs.
Metadata says the owner is also a sharer
OWNER-AS-SHARERsharers_m[idx] <= sharers | (1 << new_owner); // owner added to the sharer setLater transfers of the same line behave strangely: the required set includes the current owner, so §7's self-invalidation hang appears one transfer later, with no obvious connection to the update that caused it.
abuse instance flagged an owner listed as a sharer: okOwner and sharer are different rights and the metadata represented both simultaneously. It is a contradiction — the writable owner is not one of the read-only holders — and it corrupts the next transfer's mask rather than the current one.
The delayed effect is what makes it hard: the update that created the contradiction has long since retired by the time anything goes wrong.
if (wr_owned && wr_sharers[wr_owner]) owner_also_sharer_err <= 1'b1;Assert the contradiction at the point of update, so it is caught where it is created rather than where it is felt.
Check metadata consistency on every write, not only on read. This one needs an instance reserved for illegal stimulus, because a correct design never produces the state.
20. Design Review
- What exactly is proven before a grant? If the answer is only "the acknowledgements arrived", the data condition is missing.
- Does the acknowledgement tracker record who, or how many?
- Can an acknowledgement from a previous transfer be distinguished from a current one?
- Is the requester excluded from its own invalidation set?
- Is anyone writable during the transient? Nobody should be.
- What bounds a transfer, and does the abort name the non-responders?
- Does a transient line block only its own line?
- Do the acknowledgements proceed in parallel or serially? That decides how the design scales with sharer count.
- Can the metadata represent a contradiction, and is that checked at the write?
- What happens on reset mid-transfer?
21. How This Appears in Real Engineering
Architecture. Whether acknowledgements are collected in parallel decides whether ownership transfer scales with sharer count. It is a topology and fabric decision as much as a protocol one.
RTL. The epoch is three or four bits and it is the difference between a design that works and one that fails rarely and inexplicably. It is also the field most likely to be removed by someone optimising for area who has not seen the failure.
DV. The stale-acknowledgement case does not exist in a single-transfer test plan. It has to be constructed deliberately, and it is the highest-value test in this chapter.
Post-silicon. The abort blame mask and the stale count are the two registers that turn "the machine hung on one address" into a specific agent and a specific mechanism.
Performance. Transfer latency is a maximum over acknowledgements, so the tail agent sets the cost. Optimising the mean does nothing.
22. Common Misconceptions
| Belief | Correction |
|---|---|
| Ownership transfer copies the line | It moves the right; data moves only sometimes |
| A bitmap solves acknowledgement identity | Not across transfers — bit N means "agent N", not "for this transfer" |
| Duplicate acks are the hard case | Stale acks from a finished transfer are harder |
| Acks complete means safe to grant | Not if the old owner holds the only modified copy |
| Somebody owns the line during the transfer | Nobody does, and that is the point |
| The transient should block conflicting work | Only on its own line |
| An abort is a failure | It is a required ending; the alternative is a locked line |
| N sharers costs N acknowledgement times | Only if they serialise |
23. Interview Reasoning
24. Exercises
-
Analysis. A machine reports
transfers=10000,aborts=3,stale_acks=0, and one address is permanently unusable. Explain whystale_acks=0is itself suspicious, and name the two faults consistent with all four facts. -
Design. Extend the transfer to support a downgrade — the owner keeps a clean copy rather than being fully invalidated. State what changes in the required set, what changes in the commit condition, and which existing assertion must be weakened.
-
RTL task. Make the epoch two bits wide and construct the sequence that defeats it. State the minimum epoch width for a given maximum message lifetime, and the counter that would warn you the margin is thin.
-
DV task. Write the coverage cross for ownership transfer, then explain why two of its points cannot be reached by a test plan organised around a single transfer.
-
Debug task. A design shows rare single-writer violations only on lines transferred more than once per microsecond. Give your investigation order and the one counter that confirms the cause.
-
Design review. A colleague proposes removing the epoch to save four bits per tracker entry, arguing that acknowledgements cannot outlive their transfer because the fabric is in-order. Give the strongest version of that argument, then name what must be true system-wide for it to hold and what happens the first time it does not.
25. Summary
Revoke before grant. The grant is not the transfer; the proof is.
- Ownership is a right, not a location. Bytes may be in many places; permission to modify is in exactly one.
- Nobody holds writable permission during the transfer — measured as both writable signals flat at zero through the entire transient.
- A set beats a count: duplicates are idempotent and the vector names who did not answer.
- A set is still not enough. Bit N means "agent N", not "agent N, for this transfer" — so a straggler from a finished transfer can discharge an unrelated obligation. An epoch supplies the missing identity.
- Two conditions, joined by AND: every required holder revoked, and the authoritative value secured. Acknowledgements alone commit a line whose new owner cannot supply its own value.
- The requester is not on its own invalidation list, or the transfer waits forever — and only on the upgrade path, which hides it.
- A transient blocks its own line only. Blocking the cache is safe and unusable.
- Every transfer must be bounded, and the abort must name the non-responders. An unbounded transfer makes one address permanently unusable and reports nothing.
- Latency is a maximum over acknowledgements, not a sum — if they parallelise. That single property decides how ownership transfer scales with sharer count.
- Measured: transfers from a modified owner cost 20 cycles against 8 for clean ones.
- Verification lesson: a state change with no state change is still a bug. A mutation survived a test that drove exactly the right stimulus, because the effect was visible only in the classification counters.
That completes Module 8 — CXL.cache. A device may borrow host memory; the host coordinates; the device must remain answerable; the accelerator around it must keep making progress; and moving the right to write requires proving that nobody else still has it.
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.
