AMBA CHI · Module 8 · Request Flows
ReadUnique Flow
ReadShared took a copy without taking anyone else's; ReadUnique is its opposite. A core issues a ReadUnique to fetch a line it intends to write, and the flow grants what a writer needs: sole ownership. The home invalidates every other copy with SnpUnique, forwards any dirty data, and installs the requester Unique — clean, or dirty if it inherited a modified copy. The property that defines it is the single-writer invariant: after a ReadUnique, no other cache holds the line at all. That is why coverage matters — every holder must be invalidated, not just one; leave a sharer behind and it reads stale the moment the new owner writes — the mirror of ReadShared. Representative model, not the specification.
Intermediate16 min readAMBA CHIReadUniqueUniqueSnpUniqueSingle Writer
Module 8 · Chapter 8.2 · Request Flows
Project thread — 8.1 walked ReadShared: downgrade, coexist, end SC. This chapter is its mirror — ReadUnique: invalidate, own, end Unique. Read the two together. 8.3 is ReadClean.
1. Learning Outcomes
By the end of this chapter you should be able to:
- Trace the ReadUnique flow to its end state — the requester Unique (UC or UD), writable.
- State the defining property: ReadUnique invalidates every other copy — the single-writer invariant.
- Distinguish the cases by holder state — uncached, shared, unique clean, unique dirty.
- Explain how a dirty holder's data is forwarded to the requester, which ends in UD.
- Diagnose why leaving one sharer un-invalidated breaks coherence.
- Implement a representative ReadUnique outcome in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
Every store to shared memory begins with a ReadUnique (or a close relative). Before a core can write a line, it must become the sole owner — no other cache may hold a copy, or those copies would go stale the instant the write lands. ReadUnique is the flow that establishes that ownership, so it underpins every write, every lock release, every producer publishing data.
It is the exact counterpart to ReadShared, and the contrast is the fastest way to understand both. Where ReadShared downgrades and coexists, ReadUnique invalidates and owns. The subtle, expensive mistake here is one of coverage: a line may be shared by several caches, and ReadUnique must invalidate all of them. Miss one and you have a stale reader sitting beside a fresh writer — a silent coherence violation. This is the flow where the single-writer half of coherence lives.
3. Key Terms
4. Previous Chapter Connection
Chapter 8.1 walked ReadShared: fetch into SC, downgrade holders with SnpShared, never invalidate — readers coexist. This chapter is the mirror.
ReadUnique starts identically — a REQ to the home for a line — but its intent is to write, so its effect on other caches is opposite. The home uses SnpUnique (Chapter 7.4), which invalidates every holder to I, and grants the requester a Unique, writable copy. Same request shape, same channels, same tracker lifecycle (Module 7) — but where ReadShared preserved other copies, ReadUnique erases them. Holding the two flows side by side is the point: downgrade versus invalidate, share versus own.
5. Core Concept — fetch into Unique, invalidate every other copy
A ReadUnique brings a line to a requester in a Unique state — writable — and leaves no other cache holding it. The flow varies by what the directory finds, but the end state is invariant: the requester owns it, alone.
- Uncached. No cache holds the line. The home fetches from memory and installs the requester in UC (Unique Clean). No snoop.
- Held shared. One or more caches hold the line in SC. The home sends SnpUnique to every sharer — all go to I — and installs the requester in UC. Coverage matters: all sharers, not one.
- Held unique clean (UC). A single holder in UC. The home SnpUniques it to I; the requester ends UC.
- Held unique dirty (UD). A holder in UD. The home SnpUniques it to I; the holder forwards its dirty data (SnpRespData), and the requester installs UD — inheriting the modified line, ready to write.
Across all cases, two things are always true: no other cache holds the line (all invalidated), and the requester ends Unique (writable) — UC if clean, UD if it inherited dirty data.
The synthesis:
ReadUnique fetches a line into Unique (UC or UD) and invalidates every other copy with SnpUnique. A dirty holder forwards its data, so the requester inherits UD; otherwise the requester ends UC. The invariant that names the flow: after a ReadUnique, the requester is the sole holder, writable — the single-writer state a store requires.
6. Engineering Mental Model — taking sole custody to edit
Back to the office reference book — but now you intend to edit it, not just read it.
- If nobody has it, you take the only copy from the archive and hold it exclusively (UC), free to write in it.
- If colleagues are reading copies, you cannot edit safely while they read — so you recall and shred every one of their copies (SnpUnique → I). Not downgrade to read-only: gone. Only then do you hold the sole copy.
- If one colleague had it exclusively with edits (UD), they hand you their annotated version (dirty forwarded) and destroy their copy; you now hold the latest, edited text, alone (UD).
The rule that bites: you must collect every outstanding copy. Leave one colleague reading their old copy while you edit yours, and they are working from stale text the moment your pen touches the page. Editing demands sole custody — of all copies, without exception.
7. Engineering Diagram — ReadUnique with a dirty holder
Read top to bottom, and compare to the ReadShared figure: same request, same holder, but an invalidating snoop. RN1 went UD → I; RN0 went I → UD. One owner, writable; nobody else holds it.
8. The Cases by Holder State
The middle of the flow depends on what the directory finds — but every holder ends in I.
| Holder state | Home action | Snoop | Data source | Requester ends |
|---|---|---|---|---|
| Uncached (I) | fetch from memory | none | memory | UC |
| Shared (SC), one or many | invalidate all sharers | SnpUnique ×N | memory | UC |
| Unique Clean (UC) | invalidate holder | SnpUnique | memory or snoop | UC |
| Unique Dirty (UD) | invalidate holder, take data | SnpUnique | forwarded (dirty) | UD |
The rule to carry: the requester always ends Unique, and every holder ends I. Whether the requester ends UC or UD depends on one thing — did it inherit dirty data. A clean fetch is UC; a forwarded dirty line is UD. And when the line was shared by many, the snoop is issued to all of them — coverage is not optional.
9. ReadUnique Invalidates Everyone — the single-writer invariant
This is the property to isolate, because it is the exact inverse of ReadShared's restraint.
- Invalidate, not downgrade. Every other holder goes to I — it loses the line entirely, not just write permission. That is SnpUnique, never SnpShared.
- All copies, not one. A line shared by several caches requires every sharer invalidated. One survivor breaks the invariant.
- The requester gets write permission. It ends Unique (UC/UD) — writable (UD immediately; UC after a silent upgrade), because it is now the sole holder.
- Dirty is inherited, not lost. A dirty holder forwards its data so the requester owns the latest value as UD — the write it is about to do builds on the correct data.
The point to carry:
ReadUnique establishes the single-writer state: exactly one cache holds the line, writably, and no other holds it at all. That is the precondition for a store — a write is only safe when no stale copy can survive it. So the flow's defining act is total invalidation of every other copy, and its defining risk is incomplete invalidation. Where ReadShared's whole point was coexistence, ReadUnique's whole point is exclusivity.
10. Flow Walkthrough — ReadUnique, dirty holder
RN0 (in I) issues a ReadUnique for a line homed at HN; RN1 holds it in UD.
- REQ. RN0 sends ReadUnique, allocating a tracker. It intends to write — it needs the line Unique.
- Directory lookup. HN sees RN1 in UD, and any other sharers. Every holder must be invalidated, and RN1's dirty data rescued.
- SnpUnique. HN sends SnpUnique to RN1 (and to any other sharer). RN1 invalidates UD → I and forwards its dirty data as SnpRespData.
- CompData. HN returns CompData carrying the dirty data to RN0, which installs UD — sole owner of the latest value. The directory now lists RN0 unique, everyone else I.
- CompAck. RN0 closes with CompAck, and may now write the line.
End state: one owner, RN0 in UD; every other cache in I; the dirty data preserved in RN0's copy. Compare 8.1's identical start under ReadShared, where RN1 ended SC and both coexisted — here RN1 is gone.
11. RTL / Hardware View — ReadUnique outcome logic
The ReadUnique flow's decisions reduce to a small combinational function of the holder state: snoop if held, invalidate the holder, forward dirty data, and set the requester's unique state. Representative — the requester always ends Unique and every holder ends I.
// Representative ReadUnique outcome logic (educational).
// From the current holder state, decide: snoop needed? forward dirty? resulting
// states. Invariants of ReadUnique: a held line is ALWAYS invalidated (holder ->
// INV), and the requester ends UNIQUE -- UD if it inherited dirty data, else UC.
module chi_readunique_outcome (
input logic [2:0] holder_state, // INV, SC, UC, UD
output logic do_snoop, // SnpUnique to the holder(s)?
output logic fwd_dirty, // dirty data forwarded to the requester?
output logic [2:0] holder_final, // holder's state after the flow
output logic [2:0] req_final // requester's state after the flow
);
localparam logic [2:0] INV = 3'd0, SC = 3'd3, UC = 3'd1, UD = 3'd2;
logic held, is_dirty;
assign held = (holder_state != INV);
assign is_dirty = (holder_state == UD);
// Any holder must be snooped and invalidated; uncached needs no snoop.
assign do_snoop = held;
// A dirty holder forwards its data to the requester.
assign fwd_dirty = is_dirty;
// ReadUnique ALWAYS invalidates a holder -- it never survives.
assign holder_final = INV;
// Requester ends UNIQUE: UD if it inherited dirty data, else UC.
assign req_final = is_dirty ? UD : UC;
endmoduleThe same behavior in Verilog-2001:
// Representative ReadUnique outcome logic (Verilog-2001).
module chi_readunique_outcome (
input [2:0] holder_state,
output do_snoop,
output fwd_dirty,
output [2:0] holder_final,
output [2:0] req_final
);
localparam INV = 3'd0, SC = 3'd3, UC = 3'd1, UD = 3'd2;
wire held = (holder_state != INV);
wire is_dirty = (holder_state == UD);
assign do_snoop = held;
assign fwd_dirty = is_dirty;
assign holder_final = INV;
assign req_final = is_dirty ? UD : UC;
endmoduleAnd in VHDL:
-- Representative ReadUnique outcome logic (VHDL).
library ieee;
use ieee.std_logic_1164.all;
entity chi_readunique_outcome is
port (
holder_state : in std_logic_vector(2 downto 0);
do_snoop : out std_logic;
fwd_dirty : out std_logic;
holder_final : out std_logic_vector(2 downto 0);
req_final : out std_logic_vector(2 downto 0)
);
end entity;
architecture rtl of chi_readunique_outcome is
constant INV : std_logic_vector(2 downto 0) := "000";
constant SC : std_logic_vector(2 downto 0) := "011";
constant UC : std_logic_vector(2 downto 0) := "001";
constant UD : std_logic_vector(2 downto 0) := "010";
signal held, is_dirty : boolean;
begin
held <= (holder_state /= INV);
is_dirty <= (holder_state = UD);
do_snoop <= '1' when held else '0';
fwd_dirty <= '1' when is_dirty else '0';
holder_final <= INV;
req_final <= UD when is_dirty else UC;
end architecture;All three set holder_final to INV and end the requester Unique (UD if dirty, else UC). That "always INV" is the ReadUnique invariant — the exact inverse of ReadShared's "never INV" — but it must be applied to every holder, as the DebugLab shows.
12. Verification View — ends Unique, holders invalidated
The properties that define ReadUnique: the requester ends writable-unique, and any holder is invalidated.
// Bind to chi_readunique_outcome.
// 1. The requester always ends UNIQUE — UC or UD, a writable-ownership state.
property p_req_ends_unique;
@(*) (req_final == 3'd1 /*UC*/) || (req_final == 3'd2 /*UD*/);
endproperty
// 2. A holder is ALWAYS invalidated — ReadUnique never leaves another copy.
property p_holder_invalidated;
@(*) (holder_state != 3'd0 /*INV*/) |-> (holder_final == 3'd0 /*INV*/);
endproperty
// 3. A dirty holder's data is forwarded, and the requester inherits UD.
property p_dirty_forwarded;
@(*) (holder_state == 3'd2 /*UD*/) |-> (fwd_dirty && req_final == 3'd2 /*UD*/);
endpropertyThe system point, beyond the checks:
ReadUnique's correctness is one permission granted and one guarantee enforced. The requester gains write ownership (Unique), and every other cache is cleared (Invalid) — those two together are the single-writer state. But the invariant
p_holder_invalidatedis written per-holder for a reason: it must hold for each cache that had the line. A flow that snoops one holder and forgets another satisfies the property for the one it touched and silently violates it for the one it missed. Coherence for writable data is the absence of any other copy; a partial invalidation leaves exactly the stale copy the flow existed to remove.
- What it proves: the requester ends Unique, a snooped holder ends Invalid, dirty data is forwarded to UD.
- What it does not prove: that all sharers were snooped — coverage is a flow-level obligation (the DebugLab).
- Bug signature: a surviving holder (not INV) after a ReadUnique — a stale reader beside a fresh writer.
13. Testbench — every holder-state case
Drives each starting holder state and checks the snoop, forwarding, and resulting states — especially that a holder always ends Invalid.
module tb_chi_readunique_outcome;
logic [2:0] holder_state, holder_final, req_final;
logic do_snoop, fwd_dirty;
int errors = 0;
localparam INV = 3'd0, UC = 3'd1, UD = 3'd2, SC = 3'd3;
chi_readunique_outcome dut (.*);
task automatic check(input logic [2:0] hs, input logic exp_snoop, exp_fwd,
input logic [2:0] exp_rf, input string name);
holder_state = hs; #1;
if (do_snoop !== exp_snoop || fwd_dirty !== exp_fwd ||
req_final !== exp_rf || (hs != INV && holder_final !== INV)) begin
errors++; $display("FAIL %s: snoop=%0b fwd=%0b hf=%0d rf=%0d",
name, do_snoop, fwd_dirty, holder_final, req_final);
end else $display("PASS %s: snoop=%0b fwd=%0b hf=%0d rf=%0d",
name, do_snoop, fwd_dirty, holder_final, req_final);
endtask
initial begin
check(INV, 1'b0, 1'b0, UC, "uncached -> memory, req UC");
check(SC, 1'b1, 1'b0, UC, "shared -> invalidate, req UC");
check(UC, 1'b1, 1'b0, UC, "uniqueCln -> invalidate, req UC");
check(UD, 1'b1, 1'b1, UD, "uniqueDty -> invalidate + fwd, req UD");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS uncached -> memory, req UC: snoop=0 fwd=0 hf=0 rf=1
PASS shared -> invalidate, req UC: snoop=1 fwd=0 hf=0 rf=1
PASS uniqueCln -> invalidate, req UC: snoop=1 fwd=0 hf=0 rf=1
PASS uniqueDty -> invalidate + fwd, req UD: snoop=1 fwd=1 hf=0 rf=2
ALL TESTS PASSED14. DebugLab — leaving one sharer un-invalidated
Leaving one sharer un-invalidated
READUNIQUE INVALIDATES ONE SHARER, NOT ALL -> STALE SHARERAfter a core writes a line, another core reads a stale value — but only sometimes, and only for lines that were shared by three or more caches before the write. Two-way sharing is fine; wide sharing exposes it.
Not every sharer was snooped:
before: RN0 = I, RN1 = SC, RN2 = SC, RN3 = SC (three sharers)
RN0 issues ReadUnique -> HN snoops RN1 only (SnpUnique)
RN1: SC -> I RN2: SC (untouched!) RN3: SC (untouched!)
after: RN0 = UC, RN2 = SC, RN3 = SC <-- stale sharers survive
RN0 writes X -> RN2/RN3 still read the OLD valueThe requester became "unique" while RN2 and RN3 still held valid copies — the invalidation covered one of three.
The flow snooped a holder rather than every holder. From that point the single-writer invariant was false: the directory listed multiple sharers, but only one received a SnpUnique, so two stale readers survived the transition to unique ownership.
ReadUnique must invalidate the entire sharer set, not a single holder. Unique ownership means no other copy exists anywhere; a line can be shared by many caches, so the home must issue SnpUnique to all of them. This is a coverage failure, distinct from issuing the wrong snoop type (Chapter 8.1) or a snooped node mishandling a snoop (Chapter 7.4): each snoop was correct, but the set was incomplete, and coherence lives in completeness.
Snoop every cache the directory lists as a holder, invalidating each to I, before granting the requester Unique ownership. The per-holder invariant holder_final == INV must hold for all holders, not one — the flow is not done until the sharer set is empty. Only then is the requester truly the sole owner and safe to write.
15. Common Mistakes
- Invalidating one sharer, not all. Assumption: snoop a holder. Bug: stale sharer survives (the DebugLab). Prevention: invalidate the entire sharer set.
- Downgrading instead of invalidating. Assumption: SnpShared suffices. Bug: a reader survives a write-intent fetch. Prevention: ReadUnique uses SnpUnique.
- Dropping forwarded dirty data. Assumption: the requester will overwrite anyway. Bug: lost data on a partial write. Prevention: forward dirty; requester ends UD.
- Installing the requester as Shared. Assumption: a fetched line is SC. Bug: no write permission when one was needed. Prevention: ReadUnique ends Unique (UC/UD).
- Snooping when uncached. Assumption: every case snoops. Bug: needless snoops. Prevention: no holder, no snoop — fetch from memory.
- Forgetting CompAck. Assumption: data ends the read. Bug: transaction never closes (Chapter 7.1). Prevention: close with CompAck.
16. Engineering Checklist
- End the requester Unique — UD if it inherited dirty data, else UC.
- Snoop with SnpUnique — invalidate, never downgrade.
- Invalidate every holder the directory lists — not just one.
- Forward a dirty holder's data so the requester ends UD.
- Do not snoop when the line is uncached — fetch from memory.
- Close with CompAck; the requester may write only once it is sole owner.
17. Key Takeaways
- ReadUnique fetches a line into a Unique state (UC or UD) — writable, sole ownership.
- Its defining property: it invalidates every other copy (SnpUnique → I) — the single-writer invariant.
- Cases by holder state — uncached (UC), shared/unique clean (invalidate, UC), unique dirty (invalidate, forward, UD).
- A dirty holder forwards its data, so the requester inherits UD, ready to write.
- Invalidation must cover all sharers — leaving one survivor is a silent coherence violation.
- It is the mirror of ReadShared: invalidate versus downgrade, own versus share; the model here is representative.
18. Quick Revision
ReadUnique flow. ReadUnique fetches a line to write, so it grants sole ownership: the requester ends Unique (UC or UD) and every other copy is invalidated. It is the mirror of ReadShared — same request, opposite effect, using SnpUnique (invalidate to I) where ReadShared used SnpShared (downgrade to SC). Cases by holder: uncached → memory fetch, requester UC; shared → SnpUnique to all sharers, requester UC; unique clean → invalidate, UC; unique dirty → invalidate and forward the dirty data, requester inherits UD. The defining invariant is single-writer: after the flow, no other cache holds the line at all. The defining risk is coverage — a line shared by many must have every sharer invalidated; leave one and it reads stale the instant the new owner writes, a silent coherence violation distinct from a wrong snoop type. Invalidate all, forward dirty, end Unique. Representative model; 8.3 is the ReadClean flow — read without taking ownership.
Coming Next
Chapter 8.3 — ReadClean Flow. ReadShared shared a copy; ReadUnique took sole ownership. ReadClean sits between — it fetches a clean readable copy without obliging the requester to hold the line dirty. Chapter 8.3 walks the flow and its niche: when a requester wants to read but leave the responsibility for dirty data with the home or another cache, how the end state differs from ReadShared, and where ReadClean is the right choice over the two flows you now know as a pair.