AMBA CHI · Module 18 · CHI Debugging
Lost Ownership
This module debugs the failure families behind almost every coherence bug, starting with lost ownership. A line has an owner whenever a cache holds it dirty, obligated to eventually write it back or forward it. Lost ownership is when the last dirty copy is removed without preserving the data: the owner goes invalid, but no writeback or forward carries the value, so it vanishes and memory stays stale. The symptom is a stale read after a confirmed write. The signature: the owner's state goes invalid, but the response carries no data and the data channel is empty. The diagnosis: find the last owner, find the transaction that released it, and check whether it carried data. Representative model, not the specification.
Advanced16 min readAMBA CHIDebuggingLost OwnershipDirty DataWaveform
Module 18 · Chapter 18.1 · CHI Debugging
Project thread — Module 17 verified the design. 18.1 debugs lost ownership; 18.2 debugs incorrect state transitions.
1. Learning Outcomes
By the end of this chapter you should be able to:
- Define ownership — a cache holding a line dirty (newer than memory).
- Recognize the symptom of lost ownership — a stale read after a write.
- Read the waveform signature — a dirty owner going invalid with no data transfer.
- Trace from the stale read back to the transaction that released the last owner.
- Classify the root cause — a data-less snoop response, a dropped writeback, a lost forward.
- Implement a representative lost-ownership detector in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
Coherence bugs are notoriously hard to debug because the symptom appears far from the cause — a core reads a stale value long after, and far away from, the transaction that actually dropped the data. Debugging them efficiently means recognizing a small number of failure families by their signature, then running a methodical trace back to the root cause. This module is those families; lost ownership is the first and one of the most common.
Lost ownership has a clear definition and a clear signature. A line's owner is whichever cache holds it dirty — the only copy newer than memory — and that owner is obligated to eventually preserve the value (write it back or forward it). Lost ownership is when the last dirty copy is removed without that obligation being met: the owner goes invalid, but no data goes to memory or another cache. The value is gone; memory serves the stale one. Learning to spot the signature — an owner leaving dirty with no accompanying data — and to trace it back turns a baffling stale-read symptom into a five-minute diagnosis. This chapter is that skill.
3. Key Terms
4. Previous Chapter Connection
This chapter debugs the bugs Module 16 could introduce and Module 17 should catch. The root causes are ones you have seen from the designer's side: a snoop response that omitted the dirty data (Chapter 16.2), an eviction that dropped its writeback (Chapter 16.4), or a forward that lost the dirty status (Chapter 13.6). This chapter approaches them from the debugger's side — symptom-first, working backward.
That reversal is the skill the whole module teaches. A designer reasons forward (mechanism → effect); a debugger reasons backward (symptom → cause). The ownership and dirty-data concepts from Module 10 and Chapter 13.6 are the map, but here they are used to navigate from a failure — a stale read — to the transaction that broke the invariant. The coherency checker of Chapter 17.2 (SWMR) is a sibling: it flags the state violation; this chapter flags the data loss and shows how to trace it. Together, verification catches the bug and debugging locates it.
5. Core Concept — the last dirty copy left without its data
Lost ownership is the failure where the last dirty copy of a line is removed without the data being preserved — so the newest value is gone and memory is stale.
- A dirty owner holds the only current value. A line held dirty (UD/SD) is newer than memory — the owner's copy is the only up-to-date one (Chapter 10.5).
- Removing it demands preserving the data. When the owner gives up the line (invalidated, evicted, or downgraded away from dirty), the data must be preserved — written back to memory or forwarded to another cache.
- Lost ownership skips the preservation. The owner transitions away from dirty but no writeback and no forward carries the value — the data is destroyed.
- The result is a stale read. With no dirty copy and stale memory, a later read returns memory's old value — the newest write is lost.
The synthesis:
Lost ownership occurs when the last dirty copy of a line is removed (owner → I or downgrade) with the data not preserved — no writeback to memory, no forward to another cache. The newest value is destroyed; memory serves the stale value, and a later read is stale. The signature is an owner leaving its dirty state with no accompanying data transfer.
6. Engineering Mental Model — the only signed original, shredded
Think of the single signed original of a document (the dirty line) that someone is responsible for filing (writing back).
- Whoever holds the signed original owns it — they must eventually file it in the cabinet (memory) or hand it to a colleague who takes over the filing duty.
- Correct hand-off: they either file it, or physically hand the signed pages to a colleague. The original survives — it is in the cabinet or in another's hands.
- Lost ownership: they shred the signed original (go invalid) without filing it and without handing it over. The signed version is gone forever — the cabinet still holds the old, unsigned template (stale memory).
- Later, someone pulls the document from the cabinet and gets the unsigned template, unaware the signed version ever existed. That is the stale read.
The signed original is the dirty value; filing is a writeback, handing over is a forward. Lost ownership is shredding the only signed copy without filing or handing it over — and the stale read is someone later trusting the unsigned template.
7. Engineering Diagram — the lost-ownership scenario
RN0 left its dirty state (UD → I) but returned a data-less SnpResp — the dirty value was never captured. The later memory read returns the stale value. The failure is at the SnpResp: it should have been SnpRespData. The debugger recognizes this by the owner leaving dirty with no data.
8. Waveform Signature
Lost ownership: owner leaves UD with no data transfer
6 cyclesThe signature is unmistakable once you know it: RN0.state leaves UD (dirty), but the DAT channel is empty and the RSP is a plain SnpResp. A correct transfer would show SnpRespData on RSP and a beat on DAT in the same window. An empty DAT when a dirty owner departs is lost ownership.
9. Diagnosis Path
The methodical trace from symptom to root cause.
| Step | Action | What it finds |
|---|---|---|
| 1. Symptom | a read returns a stale value | confirm value ≠ last write |
| 2. Last owner | find the last cache to hold the line dirty | the owner that should have preserved it |
| 3. Release event | find the transaction that took the owner out of dirty | the suspect transaction |
| 4. Data check | did that transaction carry data (SnpRespData / writeback)? | no → lost ownership |
| 5. Root cause | which stage dropped the data | 16.2 / 16.4 / 13.6 |
The rule to carry: trace ownership, not the read. The stale read is the symptom, but the bug is at the transaction that released the last owner — potentially thousands of cycles earlier. The diagnosis follows the ownership of the line backward: who held it dirty last, and did the transaction that removed them preserve the data? The moment you find a dirty owner leaving with an empty data channel, you have the bug — and step 5 (which stage) points to the RTL fix.
10. Tracing a Lost-Ownership Bug — a worked trace
A core reads address X and gets 0x11; the log shows a write of 0x99 to X earlier.
- Symptom: stale read. The read of X returns 0x11, but 0x99 was written and confirmed. Value mismatch — start the trace.
- Find the last owner. The write of 0x99 was performed by RN0, which held X UD (dirty, value 0x99). RN0 was the last owner.
- Find the release event. RN0's X transitioned UD → I on a SnpUnique (another core's ReadUnique). That snoop is the suspect transaction.
- Check the data. RN0's response to the snoop was a plain SnpResp — no SnpRespData, no DAT beat. The dirty 0x99 was not preserved. Lost ownership confirmed.
- Root cause. RN0's snoop-response logic sent a data-less SnpResp for a dirty line — the Chapter 16.2 bug. The fix is to emit SnpRespData when the snooped line is dirty.
The stale read at the end traced back to a data-less snoop response near the beginning — the owner left dirty with an empty data channel. The DebugLab formalizes this trace.
11. Detector View — a lost-ownership detector
Track the per-line dirty-copy count; when a removal brings it to zero without the data being preserved, flag lost ownership. Representative.
// Representative lost-ownership detector (educational).
// A line's dirty-copy count must not reach zero via a removal that DROPS the data. When
// a dirty copy is removed (owner -> I or downgrade), the data must be PRESERVED (written
// back or forwarded). If the last dirty copy leaves with data_preserved == 0 -> LOST.
module chi_lost_owner_detect (
input logic clk, rst_n,
input logic dirty_gained, // a cache became a dirty owner (I/clean -> dirty)
input logic dirty_removed, // a dirty copy was removed (dirty -> I / downgrade)
input logic data_preserved, // that removal carried the data (writeback or forward)
output logic lost_ownership // the last dirty copy left without preserving the data
);
logic [3:0] dirty_cnt; // number of caches holding the line dirty (small)
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) dirty_cnt <= 4'd0;
else begin
case ({dirty_gained, dirty_removed})
2'b10: dirty_cnt <= dirty_cnt + 4'd1;
2'b01: dirty_cnt <= dirty_cnt - 4'd1;
default: dirty_cnt <= dirty_cnt;
endcase
end
end
// Lost ownership: the removal takes the LAST dirty copy (cnt was 1) AND the data was
// not preserved -> nobody holds the dirty value and memory was not updated.
assign lost_ownership = dirty_removed && (dirty_cnt == 4'd1) && !data_preserved;
endmoduleThe same behavior in Verilog-2001:
// Representative lost-ownership detector (Verilog-2001).
module chi_lost_owner_detect (
input clk, rst_n, dirty_gained, dirty_removed, data_preserved,
output lost_ownership
);
reg [3:0] dirty_cnt;
always @(posedge clk or negedge rst_n) begin
if (!rst_n) dirty_cnt <= 4'd0;
else if (dirty_gained & ~dirty_removed) dirty_cnt <= dirty_cnt + 4'd1;
else if (~dirty_gained & dirty_removed) dirty_cnt <= dirty_cnt - 4'd1;
end
assign lost_ownership = dirty_removed & (dirty_cnt == 4'd1) & ~data_preserved;
endmoduleAnd in VHDL:
-- Representative lost-ownership detector (VHDL).
library ieee;
use ieee.std_logic_1164.all;
use ieee.numeric_std.all;
entity chi_lost_owner_detect is
port (
clk, rst_n : in std_logic;
dirty_gained : in std_logic;
dirty_removed : in std_logic;
data_preserved : in std_logic;
lost_ownership : out std_logic
);
end entity;
architecture rtl of chi_lost_owner_detect is
signal dirty_cnt : unsigned(3 downto 0) := (others => '0');
begin
process (clk, rst_n)
begin
if rst_n = '0' then
dirty_cnt <= (others => '0');
elsif rising_edge(clk) then
if dirty_gained = '1' and dirty_removed = '0' then
dirty_cnt <= dirty_cnt + 1;
elsif dirty_gained = '0' and dirty_removed = '1' then
dirty_cnt <= dirty_cnt - 1;
end if;
end if;
end process;
lost_ownership <= '1' when (dirty_removed = '1' and dirty_cnt = 1 and data_preserved = '0')
else '0';
end architecture;All three flag lost_ownership when the last dirty copy (count was 1) is removed with data_preserved = 0 — the exact signature. Bind this to the line's state/response signals in simulation and it fires at the bug, not at the far-away stale read.
12. Assertion View — a dirty copy is never dropped
The properties formalize the invariant: removing the last dirty copy requires preserving the data.
// Bind to chi_lost_owner_detect (and the line's coherence signals).
// 1. Removing the last dirty copy without preserving the data is a violation.
property p_no_lost_ownership;
@(posedge clk) disable iff (!rst_n)
(dirty_removed && (dirty_cnt == 1)) |-> data_preserved;
endproperty
// 2. The dirty count never goes negative (a removal implies a prior owner).
property p_count_nonneg;
@(posedge clk) disable iff (!rst_n)
dirty_removed |-> (dirty_cnt != 0);
endproperty
// 3. If a dirty owner exists, memory is NOT the current value (there IS a newer copy).
// (state property: dirty_cnt > 0 -> a cache holds a value newer than memory)The system point, beyond the checks:
Lost ownership is a conservation violation, and framing it that way is what makes the debug tractable. The dirty value is a conserved quantity: it can move (writeback to memory, forward to a peer) but must never be destroyed while it is the only current copy. Every transaction that touches a dirty line either preserves the value (moves it) or is a bug (destroys it). So the debug question reduces to a single, checkable predicate at each ownership-releasing transaction: did the value move, or vanish? — and the detector answers it directly. This is why a conservation-based detector fires at the cause while the symptom (stale read) appears at an arbitrary later point: the conservation law is violated at a specific transaction, and the detector watches exactly that transaction class (a dirty removal) for exactly that predicate (data preserved). The broader debugging lesson, which recurs across this whole module, is to identify the invariant each failure family breaks — here, dirty-data conservation — and build a detector that checks the invariant at the point it can break, so the tool localizes the bug in time rather than leaving you to trace backward from a distant symptom by hand.
- What it proves: the last dirty copy is never removed without preserving the data.
- What it does not prove: the preserved data is the correct value — that is data corruption (Chapter 18.4).
- Bug signature:
lost_ownershipasserted — a dirty removal withdata_preservedlow.
13. Testbench — dropping the last dirty copy must be flagged
Makes a cache the dirty owner, then removes it without preserving the data, and checks the detector fires.
module tb_chi_lost_owner_detect;
logic clk = 0, rst_n = 0, dirty_gained, dirty_removed, data_preserved;
logic lost_ownership;
int errors = 0;
chi_lost_owner_detect dut (.*);
always #5 clk = ~clk;
initial begin
dirty_gained = 0; dirty_removed = 0; data_preserved = 0;
@(posedge clk) rst_n = 1;
// A cache becomes the dirty owner (writes the line).
@(posedge clk) dirty_gained = 1;
@(posedge clk) dirty_gained = 0;
// CORRECT: the owner is removed but the data IS preserved (writeback/forward).
@(posedge clk) begin dirty_removed = 1; data_preserved = 1; end
#1;
if (lost_ownership) begin errors++; $display("FAIL false lost-ownership on preserved data"); end
else $display("PASS dirty removed WITH data preserved -> no loss");
@(posedge clk) begin dirty_removed = 0; data_preserved = 0; end
// Owner again.
@(posedge clk) dirty_gained = 1;
@(posedge clk) dirty_gained = 0;
// BUG: the last dirty copy is removed WITHOUT preserving the data (the 16.2 bug).
@(posedge clk) begin dirty_removed = 1; data_preserved = 0; end
#1;
if (!lost_ownership) begin errors++; $display("FAIL lost ownership NOT detected!"); end
else $display("PASS lost ownership detected: dirty removed, data NOT preserved");
@(posedge clk) dirty_removed = 0;
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS dirty removed WITH data preserved -> no loss
PASS lost ownership detected: dirty removed, data NOT preserved
ALL TESTS PASSED14. DebugLab — a stale read traced to lost ownership
A stale read traced to lost ownership
STALE READ AFTER A WRITE -> LAST DIRTY OWNER RELEASED WITH NO DATA -> DIRTY VALUE DESTROYED (LOST OWNERSHIP)A read returns a stale value — the old contents of a line, not the value another core recently wrote and confirmed. There is no error or hang; the data is simply wrong. It correlates with write-shared lines that were snooped away from a writer, not with private or read-only lines.
Tracing ownership backward finds a dirty owner released with no data:
symptom: read X returns 0x11; log shows write X = 0x99 (confirmed) earlier
step 2 - last owner: RN0 held X = UD (dirty, value 0x99)
step 3 - release: RN0's X went UD -> I on a SnpUnique (another core's ReadUnique)
step 4 - data check: RN0's response = SnpResp (NO data); DAT channel EMPTY that window
-> dirty 0x99 neither written back nor forwarded -> DESTROYED when RN0 went I
-> memory still holds 0x11 -> later read returns 0x11 (stale)
signature: RN0.state UD->I with empty DAT and plain SnpResp
correct: SnpRespData carrying 0x99 -> home captures it -> value preservedThe owner left its dirty state with an empty data channel — the lost-ownership signature.
The transaction that took RN0 out of its dirty state (the SnpUnique) returned a data-less response — the value diverged from correct at that transaction, cycles or milliseconds before the stale read surfaced.
The last dirty copy was removed without preserving the data, because the snoop-response path omitted the data for a dirty line — a conservation violation that destroys the only current value. A dirty line is the only copy newer than memory, so removing it demands writing it back or forwarding it; a data-less response on a dirty invalidation drops the value, and memory's stale copy then satisfies later reads. The detector localizes this by watching each dirty removal for data_preserved — firing at the SnpResp, not the distant stale read. The root cause is the Chapter 16.2 snoop-response bug (a dirty line must return SnpRespData); related causes are a dropped writeback on eviction (Chapter 16.4) or a forward that lost dirty status (Chapter 13.6). The debug method is to trace ownership, not the read, and check preservation at the release.
Make the ownership-releasing transaction preserve the data: emit SnpRespData (carrying the value) whenever a dirty line is invalidated or downgraded, as Chapter 16.2 requires — so the value reaches the home before the owner releases it. Confirm with the detector that no dirty removal ever has data_preserved low. The dirty value must move, never vanish.
15. Common Mistakes
- Debugging the read, not ownership. Assumption: the bug is near the stale read. Bug: chasing the wrong point. Prevention: trace ownership backward.
- Ignoring the data channel on a snoop. Assumption: SnpResp is enough. Bug: missed data-less release. Prevention: check DAT on every dirty release.
- Assuming memory is current. Assumption: memory always has the value. Bug: stale behind a dirty line. Prevention: a dirty owner means memory is stale.
- Overlooking eviction writebacks. Assumption: only snoops release owners. Bug: a dropped eviction writeback. Prevention: check writeback on dirty eviction.
- Not counting owners. Assumption: one release is harmless. Bug: last-copy loss unseen. Prevention: track the dirty-copy count.
- Confusing with data corruption. Assumption: same as wrong data. Bug: different family. Prevention: lost ownership = value destroyed; corruption = value mangled (Chapter 18.4).
16. Engineering Checklist
- On a stale read, trace the line's ownership backward, not the read.
- Find the last dirty owner and the transaction that released it.
- Confirm the release preserved the data (SnpRespData / writeback / forward).
- Recognize the signature — a dirty owner → I with an empty DAT channel.
- Bind a dirty-copy-count detector that flags a last-copy removal without data.
- Classify the root cause — snoop response (16.2), eviction (16.4), or forward (13.6).
17. Key Takeaways
- An owner holds a line dirty — the only copy newer than memory.
- Lost ownership removes the last dirty copy without preserving the data.
- The symptom is a stale read after a write.
- The signature is a dirty owner → I with no SnpRespData and an empty DAT.
- Trace ownership, not the read; the bug is at the release transaction.
- The dirty value must move, never vanish; the model here is representative.
18. Quick Revision
Lost ownership. A line's owner is whichever cache holds it dirty (UD/SD) — the only copy newer than memory — and it is obligated to eventually preserve the value (write it back or forward it). Lost ownership is the failure where the last dirty copy is removed (owner → I or downgraded away from dirty) with the data not preserved — no writeback, no forward — so the newest value is destroyed and memory serves the stale one. The symptom is a stale read after a confirmed write, appearing far from the cause. The waveform signature is precise: the owner's state leaves UD/SD but the RSP is a plain SnpResp (not SnpRespData) and the DAT channel is empty — a dirty owner departing with no data transfer. The diagnosis traces ownership, not the read: find the last dirty owner, find the transaction that released it, and check whether it carried data — the moment you find a dirty release with an empty DAT, that is the bug, potentially thousands of cycles before the stale read. Root causes: a data-less snoop response (Chapter 16.2), a dropped eviction writeback (Chapter 16.4), or a forward that lost dirty status (Chapter 13.6). Lost ownership is a conservation violation — the dirty value must move, never vanish. Representative model; 18.2 debugs incorrect state transitions.
Coming Next
Chapter 18.2 — Incorrect State Transition. Lost ownership is a data failure; the next family is a state-machine failure. Chapter 18.2 covers incorrect state transitions — a cache stuck in the wrong state or taking an illegal transition, the waveform signature of an event that should have moved the state but did not, and the diagnosis path of comparing the actual state against a golden reference FSM to find the missing or wrong transition arc.