AMBA CHI · Module 8 · Request Flows
MakeUnique Flow
Evict and WriteBack ended a line's residence; MakeUnique changes its permission in place. A core holding a line Shared that wants to write it needs Unique ownership — but ReadUnique would fetch the whole line again, though the requester already has a good copy. MakeUnique is the shortcut: it invalidates the other sharers and grants Unique ownership without transferring data — the requester keeps its copy and gains the right to write, saving a full cache-line transfer. But the no-data optimization has a precondition: because nothing is returned, the requester must already hold a valid copy, or intend to overwrite the whole line — otherwise it owns a line whose contents it does not have. Representative model, not the specification.
Intermediate15 min readAMBA CHIMakeUniqueUpgradeUniqueNo Data
Module 8 · Chapter 8.8 · Request Flows
Project thread — the request flows close here. MakeUnique is the efficient reader-to-writer upgrade, the counterpart to ReadUnique. Module 9 next dives into snoops themselves, starting with why they exist.
1. Learning Outcomes
By the end of this chapter you should be able to:
- State that MakeUnique upgrades a holder to Unique by invalidating other sharers, with no data transfer.
- Trace the flow — REQ → invalidate sharers → Comp → CompAck — with no DAT beats.
- Explain why it is cheaper than ReadUnique — it moves no line data.
- State the precondition: the requester must hold a valid copy or write the whole line.
- Diagnose why issuing MakeUnique without valid data corrupts the line.
- Implement a representative MakeUnique eligibility check in SystemVerilog, Verilog-2001, and VHDL.
2. Why Should I Learn This?
The most common write pattern is: a core reads a line, then decides to modify it. It already holds the data in Shared state — identical to memory and to every other sharer. To write, it needs Unique ownership, but it does not need the data again; it has it. Fetching the whole line over the fabric just to gain write permission would be pure waste.
MakeUnique is the optimization that avoids that waste, and it is everywhere — every read-modify-write, every lock acquire that spins then writes, every counter update from a cached value. Its lesson is that coherence and data are separable: you can change who is allowed to write a line without moving a single byte. But separating them has a catch — the flow returns no data, so it is only safe when the requester already has the data. Understanding MakeUnique is understanding when a permission upgrade is enough and when you actually need a fetch.
3. Key Terms
4. Previous Chapter Connection
Chapter 8.2's ReadUnique took Unique ownership and fetched the line's data — for a requester that did not have it, or wanted it fresh. MakeUnique is the same ownership move minus the data fetch.
It reuses the invalidation you already know: the home snoops every other holder to I, establishing the single-writer state (Chapter 8.2). What it drops is the CompData — no line data comes back, because the requester keeps its existing copy. So MakeUnique is ReadUnique with the data transfer removed, valid precisely when the requester does not need that transfer. It closes Module 8 as the efficient path from a reader that already holds a line to a writer that owns it.
5. Core Concept — gain Unique ownership without moving data
A MakeUnique upgrades a requester to Unique ownership by invalidating the other sharers, and crucially transfers no data — the requester uses the copy it already holds.
- Request. A core holding the line in SC (Shared Clean) wants to write it. It sends MakeUnique to the home.
- Invalidate the other sharers. The home snoops every other holder with SnpMakeInvalid, taking them to I. These snoops request no data — the line is clean, so all copies agree; nothing needs to be returned.
- Complete, no data. The home returns Comp — not CompData. No line data crosses the fabric. The requester transitions SC → UC, now the sole owner, and may write (→ UD).
- Acknowledge. The requester sends CompAck, closing the transaction (Chapter 7.1).
The whole exchange carries no DAT beats — only requests, snoops, and responses. That absence is the point: the requester already had the bytes; only the permission changed.
The synthesis:
MakeUnique upgrades SC to UC with no data transfer: the home invalidates the other sharers, returns Comp (not CompData), and the requester keeps its existing copy. It is ReadUnique minus the fetch — cheaper by a full cache line — and valid because a clean shared copy already matches memory and every other sharer. Coherence changed; data did not move.
6. Engineering Mental Model — you already have the book, get exclusive rights
The office document once more — you are reading a copy (SC), and it is identical to the master and to everyone else's copy (clean, shared).
- You decide to edit it. You do not need a fresh copy — yours is current. You just need exclusive editing rights so no one reads a version you are about to change.
- You tell the office "make my copy the only one" (MakeUnique). The office recalls and shreds everyone else's copies (invalidate the sharers) and grants you sole editing rights — but sends you no new pages, because yours are already correct.
- Now you are the only holder, free to write.
The efficiency is obvious: no pages were copied or mailed — only permissions changed. But the catch is equally clear: this only works because your copy was already good. If you had no copy — or a stale one — getting "sole rights" with no pages would leave you editing a document you do not actually have.
7. Engineering Diagram — a MakeUnique upgrade
Read top to bottom, and note what is absent: there are no DAT messages. The sharers are invalidated, the requester is upgraded, and the requester keeps the bytes it already had. Compare ReadUnique's figure, which carried CompData — that transfer is exactly what MakeUnique saves.
8. MakeUnique versus ReadUnique
The two ways to reach Unique ownership, distinguished by whether data moves.
| Aspect | MakeUnique (8.8) | ReadUnique (8.2) |
|---|---|---|
| Requester starts | holding SC (valid data) | may hold nothing |
| Data transferred | none | the line (CompData) |
| Other copies | invalidated | invalidated |
| Cost | cheap — no line transfer | a full cache-line fetch |
| Requester ends | UC (then UD on write) | UC / UD |
The rule to carry: both invalidate every other copy — the ownership move is identical — but MakeUnique moves no data while ReadUnique fetches the line. Choose MakeUnique when the requester already holds the line (or will overwrite it entirely); choose ReadUnique when it needs the data. The saving is a whole cache line per upgrade, which is why the common read-then-write pattern uses MakeUnique.
9. The No-Data Precondition — hold valid data or write the whole line
Because MakeUnique returns no data, it has a precondition, and it is the crux of the chapter.
- The requester supplies the data. MakeUnique changes permission only; the line's contents come entirely from the requester's existing copy.
- So it must hold a valid copy. A clean SC (or UC) copy is valid — it matches memory and every sharer. Upgrading it is safe.
- Or it must overwrite the whole line. If the requester will write every byte (a full-line write), it does not need the old data at all — so no copy is required.
- Otherwise, use ReadUnique. If the requester holds no valid copy and will do a partial write, it needs the current data. MakeUnique would leave it owning a line whose un-written bytes are unknown — it must fetch with ReadUnique instead.
The point to carry:
MakeUnique separates permission from data, and that separation is safe only when the data is already accounted for — either the requester holds it (a valid copy) or it will not need it (a full-line write). Miss the precondition — upgrade with no data while holding none, then partially write — and the requester becomes the unique owner of bytes it never had. The optimization is real, but it is conditional: no data returned means the requester must already have what it needs.
10. Flow Walkthrough — MakeUnique upgrading SC to UC
RN0 holds a line in SC and wants to write it; RN2 also holds it in SC.
- REQ. RN0 holds a valid SC copy — the precondition is met. It sends MakeUnique; it will not re-fetch the data.
- Invalidate sharers. HN snoops RN2 with SnpMakeInvalid: RN2 goes SC → I and returns SnpResp with no data (the line is clean).
- Comp, no data. HN returns Comp — not CompData. RN0 transitions SC → UC, now the sole owner. No line data crossed the fabric.
- Write. RN0 writes the line, transitioning UC → UD locally.
- CompAck. RN0 closes with CompAck.
End state: RN0 owns the line uniquely, every other copy is I, and no data transfer occurred — RN0 used the copy it already had. Had RN0 held no valid copy and done a partial write, this same no-data flow would have left its un-written bytes garbage — the DebugLab.
11. RTL / Hardware View — a MakeUnique eligibility check
The key decision is combinational: is MakeUnique safe (no data needed), or must the requester fetch with ReadUnique? It turns on whether the requester holds valid data or will write the whole line. Representative.
// Representative MakeUnique eligibility check (educational).
// MakeUnique upgrades to UC with NO data transfer, so it is safe only if the
// requester already holds a VALID copy (SC/UC) OR will overwrite the whole line.
// Otherwise it must fetch the data with ReadUnique. Either way, other copies are
// invalidated to reach Unique.
module chi_makeunique_check (
input logic [2:0] req_state, // requester's current state: INV, SC, UC
input logic full_write, // will overwrite the ENTIRE line
output logic has_data, // requester already holds a valid copy
output logic makeunique_ok, // safe to upgrade WITHOUT a data transfer
output logic must_readunique,// needs data -> use ReadUnique instead
output logic snoop_others, // invalidate other sharers (always, to be unique)
output logic [2:0] req_final // requester state after the upgrade
);
localparam logic [2:0] INV = 3'd0, UC = 3'd1, SC = 3'd3;
// A clean SC or UC copy is valid data the requester can keep.
assign has_data = (req_state == SC) || (req_state == UC);
// No data is needed if the requester holds valid data OR will write the whole line.
assign makeunique_ok = has_data || full_write;
assign must_readunique = !makeunique_ok; // else it must fetch the line
// Becoming Unique always invalidates the other copies.
assign snoop_others = 1'b1;
// On a safe upgrade the requester ends Unique Clean (then UD when it writes).
assign req_final = UC;
endmoduleThe same behavior in Verilog-2001:
// Representative MakeUnique eligibility check (Verilog-2001).
module chi_makeunique_check (
input [2:0] req_state,
input full_write,
output has_data,
output makeunique_ok,
output must_readunique,
output snoop_others,
output [2:0] req_final
);
localparam INV = 3'd0, UC = 3'd1, SC = 3'd3;
assign has_data = (req_state == SC) || (req_state == UC);
assign makeunique_ok = has_data || full_write;
assign must_readunique = !makeunique_ok;
assign snoop_others = 1'b1;
assign req_final = UC;
endmoduleAnd in VHDL:
-- Representative MakeUnique eligibility check (VHDL).
library ieee;
use ieee.std_logic_1164.all;
entity chi_makeunique_check is
port (
req_state : in std_logic_vector(2 downto 0);
full_write : in std_logic;
has_data : out std_logic;
makeunique_ok : out std_logic;
must_readunique : out std_logic;
snoop_others : out std_logic;
req_final : out std_logic_vector(2 downto 0)
);
end entity;
architecture rtl of chi_makeunique_check is
constant INV : std_logic_vector(2 downto 0) := "000";
constant UC : std_logic_vector(2 downto 0) := "001";
constant SC : std_logic_vector(2 downto 0) := "011";
signal has_data_i : std_logic;
begin
has_data_i <= '1' when (req_state = SC or req_state = UC) else '0';
has_data <= has_data_i;
makeunique_ok <= '1' when (has_data_i = '1' or full_write = '1') else '0';
must_readunique <= '0' when (has_data_i = '1' or full_write = '1') else '1';
snoop_others <= '1';
req_final <= UC;
end architecture;All three deem MakeUnique safe only when the requester holds valid data or writes the whole line, always invalidate the other copies, and end the requester UC. When neither holds, must_readunique says: fetch the data instead. The DebugLab shows the corruption when that guard is ignored.
12. Verification View — no-data only when safe, always invalidate
The properties that keep MakeUnique safe: it is eligible only with valid data or a full write, always invalidates others, and ends Unique.
// Bind to chi_makeunique_check.
// 1. MakeUnique is eligible (no data) exactly when the requester holds valid data
// or will overwrite the whole line — never otherwise.
property p_ok_iff_data_or_full;
@(*) makeunique_ok == (has_data || full_write);
endproperty
// 2. If not eligible, the requester must fetch data (ReadUnique) instead.
property p_unsafe_needs_readunique;
@(*) (!has_data && !full_write) |-> must_readunique;
endproperty
// 3. Reaching Unique always invalidates the other copies, and the requester ends UC.
property p_invalidate_and_unique;
@(*) (snoop_others == 1'b1) && (req_final == 3'd1 /*UC*/);
endpropertyThe system point, beyond the checks:
MakeUnique is the clearest case in the protocol of coherence being separable from data. Ownership — who may write — is a permission the directory grants by invalidating others; the line's contents are a separate thing that may or may not need to move. MakeUnique grants the permission and moves nothing, which is the whole efficiency. But the two are only separable when the data is already where it needs to be: in the requester's valid copy, or about to be entirely overwritten. The precondition is the bridge — it guarantees that skipping the data transfer leaves no gap between the ownership the requester gains and the data it holds.
- What it proves: MakeUnique is eligible only with valid data or a full write, always invalidates, ends UC.
- What it does not prove: the requester's copy actually stayed valid through the flow — a mid-flight invalidation is a separate ordering concern.
- Bug signature: MakeUnique issued with no valid copy and a partial write — ownership of unknown bytes.
13. Testbench — eligible and ineligible cases
Drives each starting state and write kind, checking eligibility, the fetch fallback, and the invalidation.
module tb_chi_makeunique_check;
logic [2:0] req_state, req_final;
logic full_write, has_data, makeunique_ok, must_readunique, snoop_others;
int errors = 0;
localparam INV = 3'd0, UC = 3'd1, SC = 3'd3;
chi_makeunique_check dut (.*);
task automatic check(input logic [2:0] rs, input logic fw,
input logic exp_ok, input string name);
req_state = rs; full_write = fw; #1;
if (makeunique_ok !== exp_ok || must_readunique === exp_ok ||
snoop_others !== 1'b1 || req_final !== UC) begin
errors++; $display("FAIL %s: ok=%0b ru=%0b", name, makeunique_ok, must_readunique);
end else $display("PASS %s: ok=%0b ru=%0b", name, makeunique_ok, must_readunique);
endtask
initial begin
check(SC, 1'b0, 1'b1, "SC holder, partial -> MakeUnique (has data)");
check(UC, 1'b0, 1'b1, "UC holder, partial -> MakeUnique (has data)");
check(INV, 1'b1, 1'b1, "no copy, full write -> MakeUnique (overwrites)");
check(INV, 1'b0, 1'b0, "no copy, partial -> must ReadUnique");
if (errors == 0) $display("ALL TESTS PASSED");
else $display("%0d FAILURE(S)", errors);
$finish;
end
endmoduleExpected output:
PASS SC holder, partial -> MakeUnique (has data): ok=1 ru=0
PASS UC holder, partial -> MakeUnique (has data): ok=1 ru=0
PASS no copy, full write -> MakeUnique (overwrites): ok=1 ru=0
PASS no copy, partial -> must ReadUnique: ok=0 ru=1
ALL TESTS PASSED14. DebugLab — MakeUnique without a valid copy
MakeUnique without a valid copy
MAKEUNIQUE WITH NO VALID COPY + PARTIAL WRITE -> GARBAGE BYTESAfter a core writes part of a line, the untouched bytes of that line are wrong — stale or garbage — even though the write only meant to change a few. It happens only when the writing core did not already have the line cached, and only for partial writes.
Ownership was granted with no data behind it:
RN0 state = I (no valid copy), intends a PARTIAL write
RN0 issues MakeUnique -> HN invalidates other sharers, returns Comp (NO data)
RN0 now UC, but holds NO valid line contents
RN0 partial-writes bytes 0-1 -> bytes 2-3 = whatever was in the line buffer (garbage)
result: line = [garbage][garbage][new][new] -> corruptRN0 owned the line but never had its data; the partial write exposed the missing bytes.
MakeUnique was issued while the requester held no valid copy and would do a partial write. From that point it owned a line whose un-written bytes had no source — MakeUnique returns none, and the requester had none.
MakeUnique moves no data, so the requester must already have it. The optimization is valid only when the line's contents are accounted for — a valid cached copy, or a full-line overwrite that needs no old data. With neither, gaining Unique ownership leaves the requester holding permission but not data, and a partial write reveals the bytes it never had. The correct op when data is needed is ReadUnique (Chapter 8.2), which fetches the line while taking ownership.
Gate MakeUnique on the precondition: issue it only when the requester holds a valid copy (SC/UC) or will overwrite the whole line — exactly makeunique_ok = has_data || full_write. When neither holds, use ReadUnique to fetch the data. Then the requester always has the line's contents before it writes.
15. Common Mistakes
- MakeUnique with no valid copy on a partial write. Assumption: ownership is enough. Bug: garbage bytes (the DebugLab). Prevention: require valid data or a full write.
- Using ReadUnique when you hold SC. Assumption: always fetch to write. Bug: a wasted cache-line transfer. Prevention: MakeUnique upgrades in place.
- Skipping the invalidation. Assumption: no data means no snoop. Bug: a stale sharer survives the write. Prevention: MakeUnique still invalidates every other copy.
- Expecting CompData. Assumption: it is a read. Bug: waiting for data that never comes. Prevention: MakeUnique returns Comp, no data.
- Forgetting CompAck. Assumption: the upgrade self-completes. Bug: transaction never closes (Chapter 7.1). Prevention: close with CompAck.
- Relying on a copy invalidated mid-flight. Assumption: SC stays valid. Bug: stale data after a racing snoop. Prevention: reconcile a lost copy (fetch data).
16. Engineering Checklist
- Use MakeUnique to upgrade a valid SC/UC copy to Unique — no data fetch.
- Ensure the precondition: hold valid data or perform a full-line write.
- Otherwise use ReadUnique to fetch the line while taking ownership.
- Still invalidate every other copy — MakeUnique reaches the single-writer state.
- Expect Comp, not CompData — no line data crosses the fabric.
- Close with CompAck; write only once Unique.
17. Key Takeaways
- MakeUnique upgrades a holder to Unique by invalidating other sharers, with no data transfer.
- The requester keeps its existing copy; the flow carries no DAT beats — only REQ, SNP, and RSP.
- It is ReadUnique minus the fetch — cheaper by a full cache line, for the common read-then-write pattern.
- Its precondition: the requester must hold valid data or overwrite the whole line.
- Issuing it with no valid copy and a partial write leaves the un-written bytes garbage — use ReadUnique instead.
- Upgrade in place, invalidate others, mind the precondition; the model here is representative.
18. Quick Revision
MakeUnique flow. MakeUnique obtains Unique ownership without a data transfer — the efficient reader-to-writer upgrade. A core holding SC that wants to write sends MakeUnique; the home invalidates every other sharer (SnpMakeInvalid → I, no data) and returns Comp, not CompData; the requester transitions SC → UC, keeping its existing copy, and closes with CompAck. The whole flow carries no DAT beats — only requests, snoops, and responses — which is exactly the saving over ReadUnique, which fetches the full line. Both invalidate the other copies; only ReadUnique moves data. The precondition that keeps MakeUnique safe: because it returns no data, the requester must already hold a valid copy (SC/UC) or intend to overwrite the whole line — otherwise it owns a line whose un-written bytes it never had, and a partial write corrupts them. When data is needed, use ReadUnique. Upgrade in place, invalidate others, honour the precondition. Representative model; Module 9 opens the snoop flows with why snoops exist.
Coming Next
Chapter 9.1 — Why Snoops Exist. Module 8 built the request flows, each of which leaned on snoops to reach other caches; Module 9 turns to the snoops themselves. Chapter 9.1 steps back to the foundational question — why a coherent system needs snoops at all: how they are the mechanism that lets one cache's request reach into another's contents, and why coherence is impossible without them. It reframes everything you have seen from the request side as the snoop side, opening the module that dissects each snoop type in full.