Skip to content

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 Compnot 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

A MakeUnique flow upgrading RN0 from Shared to Unique. RN0 holds the line in Shared Clean and wants to write; RN2 also holds Shared Clean. RN0 sends a REQ MakeUnique to the Home Node. The Home Node snoops RN2 with a SNP SnpMakeInvalid, taking RN2 to Invalid with no data returned. The Home Node returns a RSP Comp, not CompData, and RN0 transitions from Shared Clean to Unique Clean. RN0 sends a RSP CompAck. No DAT beats occur because no line data moves.MakeUnique — invalidate sharers, upgrade SC to UC, no dataRN0 · requester (SC)HN · homeRN2 · sharer (SC)REQ: MakeUniqueSNP: SnpMakeInvalidRSP: SnpResp (SC toI, no data)RSP: Comp (SC to UC,no data)RSP: CompAck
Figure 1 — a MakeUnique upgrading RN0 from Shared to Unique. RN0 holds the line in SC and wants to write; RN2 also holds it in SC. RN0 sends MakeUnique on REQ; the Home Node snoops RN2 with SnpMakeInvalid, taking it to I with no data; the Home Node returns Comp — not CompData — and RN0 transitions SC to UC; RN0 sends CompAck. There are no DAT beats: no line data moves, because RN0 keeps the copy it already holds.

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.

AspectMakeUnique (8.8)ReadUnique (8.2)
Requester startsholding SC (valid data)may hold nothing
Data transferrednonethe line (CompData)
Other copiesinvalidatedinvalidated
Costcheap — no line transfera full cache-line fetch
Requester endsUC (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.

  1. REQ. RN0 holds a valid SC copy — the precondition is met. It sends MakeUnique; it will not re-fetch the data.
  2. Invalidate sharers. HN snoops RN2 with SnpMakeInvalid: RN2 goes SC → I and returns SnpResp with no data (the line is clean).
  3. Comp, no data. HN returns Comp — not CompData. RN0 transitions SC → UC, now the sole owner. No line data crossed the fabric.
  4. Write. RN0 writes the line, transitioning UC → UD locally.
  5. 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
endmodule

The same behavior in Verilog-2001:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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;
endmodule

And in VHDL:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
-- 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// 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*/);
endproperty

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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
endmodule

Expected output:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 PASSED

14. DebugLab — MakeUnique without a valid copy

1

MakeUnique without a valid copy

MAKEUNIQUE WITH NO VALID COPY + PARTIAL WRITE -> GARBAGE BYTES
Symptom

After 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.

Evidence

Ownership was granted with no data behind it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]  -> corrupt

RN0 owned the line but never had its data; the partial write exposed the missing bytes.

First Divergence

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.

Root Cause

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.

Fix

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.