UCIe · Module 17
Future Memory Architectures
Which architectural invariants survive as memory becomes tiered, composable, pooled and disaggregated — why migration is a state transfer rather than a pointer change, why a memory copy misses the newest value when a line is cached dirty, why one completion definition cannot serve a volatile and a durable tier, why a mapping decision in a shared pool is also an access-control decision, and why capacity isolation is not bandwidth isolation.
Module 17 has moved from a single memory chiplet, through remote capacity joining one map, through compute moving toward the data, to a many-channel subsystem behind a boundary. This chapter asks what stays true when memory keeps moving.
1. The One-Sentence Model
Future memory architectures are new placements of old obligations. Changing where state lives changes latency, bandwidth, persistence and failure domain — it never removes the need to know who owns that state, when ownership changes, and which path may observe it.
2. What This Chapter Is, and What It Refuses to Be
This is not a predictions article. It contains no forecast about which technology will win, no year, no market claim, and no assertion that any particular architecture is inevitable.
| Question | Where it is answered |
|---|---|
| The memory chiplet as a stateful endpoint | 17.1 |
| Remote capacity in one memory map; region tables, remap guards, tiers | 17.2 |
| Compute placed beside the controller | 17.3 |
| Many-channel HBM behind a boundary | 17.4 |
| Who holds the newest value across dies | 16.1 · 16.2 |
What is new here, and what makes the chapter durable rather than dated:
Migration as a state transfer (§11–§17). 17.2 established that a remap must be guarded and that an accepted request must never be rerouted. This chapter asks the harder question that follows: what does it take to actually move the data, and the answer involves five phases, a dirty-data problem that a memory copy cannot solve, and a commit that must come last.
Completion is not one event (§18–§21). A volatile tier and a durable tier can legitimately define the point of completion differently, and a single done bit cannot model both.
Mapping is access control (§25–§27). The moment memory is shared between clients, deciding where an address goes is inseparable from deciding who may go there — and a design that separates those two decisions has an isolation hole that routes perfectly.
And capacity isolation is not bandwidth isolation (§28–§31). Pooling solves a capacity problem and can create a performance one.
3. Sourcing, and the Evidence Ladder
4. The Invariants That Do Not Move
Every architecture below changes placement. None of them changes this list.
| Invariant | What it means, wherever the memory lives |
|---|---|
| Address ownership | exactly one target owns an address at a time (17.2 §10) |
| Latest-value ownership | the newest value has exactly one holder, which may not be the memory (16.2 §25) |
| Identity | a request's identity is the requester's and survives the round trip (17.1 §19) |
| Ordering | whatever dependencies the memory model requires are preserved (16.3 §17) |
| Configuration lifetime | a mapping is committed atomically and guarded against live work (17.2 §14) |
| Completion | a defined event, later than transmission, that discharges the obligation |
| Recovery | a transport event rebuilds transport state and changes nothing semantic (14.2 §4) |
| Observability | every stall is attributable to one cause (15.5 §10) |
A new memory architecture is a proposal about placement. It is not a proposal about any row of that table — and a design that quietly weakens one of them in exchange for a placement benefit has traded correctness for topology.
5. The Tier Picture
Read the descriptions, not the order. No claim is made that any real technology occupies any particular level, and a given system may have fewer levels, more, or a different arrangement. What the picture teaches is the correlation: as you move down, latency and capacity both grow, and so does the number of independent things that can fail.
6. Five Dimensions, Not a Roadmap
| Dimension | The architectural question it raises | Where this chapter treats it |
|---|---|---|
| Capacity tiering | which data belongs in which service class? | §7–§10 |
| Composability | memory attached, detached and reconfigured at runtime | §11–§17 |
| Pooling | several clients sharing one memory resource | §25–§31 |
| Disaggregation | memory further from local compute | §22–§24 |
| Persistence | a tier where completion may mean something stronger | §18–§21 |
| Near-memory compute | compute moved to the data | 17.3 — not repeated |
These are dimensions, not stages. A system can be tiered without being pooled, pooled without being persistent, composable without being disaggregated. Treating them as a sequence — "first we tier, then we pool, then we disaggregate" — is the shape of a roadmap article and it is not how systems are actually built.
7. The Tier Descriptor
// ILLUSTRATIVE memory-tier descriptor. These are ARCHITECTURAL PROPERTIES of a
// tier as this design chooses to model them — not fields of any standard, and
// not a claim about any technology (Section 3).
typedef struct packed {
logic valid;
logic [TIER_W-1:0] tier_id;
logic [LAT_W-1:0] latency_class; // service-class ordinal, not a value
logic [BW_W-1:0] bandwidth_class;
logic [CAP_W-1:0] capacity_class;
logic coherent; // does hardware coherence cover it?
logic durable; // does it survive power loss?
logic [POLICY_W-1:0] completion_policy; // Section 19
} mem_tier_desc_t;
mem_tier_desc_t tier_q [NUM_TIERS];Architecture. A small table describing service classes rather than devices. The classes are ordinals, not measurements — latency_class says "slower than tier 1, faster than tier 3", which is what a scheduler or a placement policy actually needs and is all a design can honestly assert.
State. NUM_TIERS entries of flops, written at configuration time and read by placement, scheduling and completion logic.
Cycle behaviour. Read combinationally, alongside the region lookup (§8). Written only at a configuration commit, never incrementally.
Contract. Three consumers depend on it: placement policy reads the classes, the completion logic reads completion_policy (§19), and any coherence decision reads coherent. Each must see a consistent descriptor, which is why it commits with the region table rather than separately.
Failure. Encoding a measured latency value rather than a class — which is then wrong the moment the device is migrated, degraded, or reached over a narrower link. §9 is that failure.
DV. Cover every tier as selected; assert that completion_policy is consistent with durable (§21).
8. The Region-to-Tier Map
// ILLUSTRATIVE. 17.2's region table with the tier made explicit. The commit
// discipline is 17.2 Section 12's and is not restated here.
typedef struct packed {
logic valid;
logic [ADDR_W-1:0] base; // inclusive
logic [ADDR_W-1:0] limit; // inclusive — one convention
logic [TIER_W-1:0] tier;
logic [TARGET_W-1:0] target; // WHICH device, within that tier
logic [EPOCH_W-1:0] epoch;
} tier_region_t;
tier_region_t active_region_q [NUM_REGIONS];
tier_region_t requested_region_q [NUM_REGIONS];Architecture. A region binds an address range to both a tier and a target. Those answer different questions: the tier says what kind of memory this is, and the target says which instance. A migration can change the target within a tier, or move a range between tiers, and the two cases have different consequences (§9, §11).
State. Two copies, committed atomically (17.2 §12).
Cycle behaviour. Compared against the incoming address; the resulting {tier, target, epoch} is captured with the request (17.2 §16) and never recomputed.
Contract. Everything from the completion policy to the quota accounting reads tier. A design that captures target and not tier cannot apply a tier-specific completion rule to a request in flight, which is §20.
Failure. §9.
DV. 17.2 §14's stability properties apply unchanged, plus a check that a captured tier matches the tier of its captured target.
9. Wrong Architecture — a Latency Class Derived From the Address, Forever
// WRONG — the performance expectation is baked into the address decode and
// never revisited.
localparam int FAST_REGION_TOP = 32'h4000_0000;
assign expected_latency_class = (addr < FAST_REGION_TOP) ? LAT_FAST : LAT_SLOW;Three ways this becomes false without the address changing:
| Event | What actually changed | What the constant still says |
|---|---|---|
| the range is migrated to another target (§11) | the device, and its service class | fast |
| the link to that target recovers narrower (14.4) | latency and bandwidth | fast |
| the target enters a degraded mode (§23) | service rate | fast |
Three consequences.
A scheduler misallocates work. Anything that prioritises, prefetches, or chooses placement based on expected_latency_class is now optimising against a fiction. The result is a large performance regression with correct data, which is the hardest kind to attribute.
And timeouts derived from it manufacture faults. 16.5 §27's failure, arriving through a different door: a bound sized for the fast class applied to a range that is no longer fast.
The fix is that a performance expectation is configuration state with a lifetime, exactly like a route. It is read from the tier descriptor of the captured tier, and it is re-derived when — and only when — the configuration commits:
// ILLUSTRATIVE. The expectation comes from the tier the request was accepted
// under, so it moves when the configuration moves and not before.
assign expected_latency_class = tier_q[txn_q[id].tier].latency_class;10. Performance Expectations Have Lifetimes Too
A generalisation worth stating on its own, because it recurs.
| Derived quantity | Must be re-derived when | If it is not |
|---|---|---|
| timeout bound | the tier, target, link width or rate changes | healthy accesses declared failed (16.5 §27) |
| required outstanding depth | round-trip latency changes | throughput silently collapses (17.2 §22) |
| prefetch distance | latency class changes | prefetches arrive too late or too early |
| placement policy input | a range's tier changes | hot data lands in the slow tier |
| queue and credit sizing | sustained rate changes | degradation becomes loss (17.4 §16) |
Every one of those is a number computed from a configuration. When the configuration commits, they are stale — and a design that recomputes the route on a commit but not the bounds has fixed half the problem.
11. Migration Is a State Transfer
17.2 §31 established that migration is a cutover problem and declined to build one. This chapter builds the phase structure, because that is what makes the hazards visible.
1. QUIESCE — stop accepting new work for the range; let outstanding work drain
2. COPY — transfer the authoritative contents from the old target to the new
3. DRAIN — reconcile anything that changed during the copy
4. COMMIT — atomically switch the map; advance the epoch
5. RETIRE — release the old target's copyThree properties of that ordering, and each is a section below.
COMMIT is fourth, not first. Everything before it must complete while the old target is still authoritative. §13 is the failure of committing early.
COPY is not sufficient by itself. If the data is cached and dirty somewhere, the old target's memory does not hold the newest value — so copying it copies a stale value. §15 is that problem.
And RETIRE is last for a reason. Releasing the old copy before the commit is durable leaves the system with no authoritative source if anything fails between the two. The old copy is the fallback until the new one is proven.
12. The Migration State Machine
// ILLUSTRATIVE migration control. Not a protocol state machine, and no
// specification defines these states (Section 3).
typedef enum logic [2:0] {
MIG_IDLE = 3'd0,
MIG_QUIESCE = 3'd1,
MIG_COPY = 3'd2,
MIG_DRAIN = 3'd3,
MIG_COMMIT = 3'd4,
MIG_RETIRE = 3'd5,
MIG_ABORT = 3'd6
} migration_state_e;
typedef struct packed {
logic active;
migration_state_e state;
logic [REG_W-1:0] region;
logic [TARGET_W-1:0] old_target;
logic [TARGET_W-1:0] new_target;
logic [ADDR_W-1:0] copy_cursor; // how far the copy has progressed
logic [EPOCH_W-1:0] start_epoch;
} migration_txn_t;
migration_txn_t mig_q;
always_comb begin
mig_nxt = mig_q.state;
unique case (mig_q.state)
MIG_IDLE: if (mig_start) mig_nxt = MIG_QUIESCE;
MIG_QUIESCE: if (mig_abort) mig_nxt = MIG_ABORT;
else if (region_outstanding == '0) mig_nxt = MIG_COPY;
MIG_COPY: if (mig_abort) mig_nxt = MIG_ABORT;
else if (copy_complete) mig_nxt = MIG_DRAIN;
MIG_DRAIN: if (mig_abort) mig_nxt = MIG_ABORT;
else if (reconcile_complete) mig_nxt = MIG_COMMIT;
MIG_COMMIT: if (commit_fire) mig_nxt = MIG_RETIRE;
MIG_RETIRE: if (old_copy_released) mig_nxt = MIG_IDLE;
MIG_ABORT: if (abort_complete) mig_nxt = MIG_IDLE;
default: mig_nxt = MIG_ABORT;
endcase
end
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) mig_q.state <= MIG_IDLE;
else mig_q.state <= mig_nxt;Architecture. One migration in flight per controller in this illustration, with an explicit abort path. MIG_ABORT is reachable from every phase before MIG_COMMIT and from none after — which is the state machine encoding §11's rule that the commit is the point of no return.
State. One record: the phase, the region, both targets, a copy cursor and the epoch the migration started under. The cursor is what makes the copy restartable across a link recovery (§37).
Cycle behaviour. One next-state owner, unique case, and mig_abort checked before progress in every abortable state — so an abort cannot be overtaken by a phase completing in the same cycle.
Contract. The region table's commit logic waits for MIG_COMMIT; the old target's release waits for MIG_RETIRE. Neither may act early, and §13 and §14 are those two rules asserted.
Failure. Allowing MIG_COMMIT → MIG_ABORT. After the commit, new work is already going to the new target, so "aborting" would leave writes on the new target and readers pointed at the old one — split ownership, which is the one thing §4's first invariant forbids.
DV. Cover every state and every transition including abort from each abortable phase; assert no transition out of MIG_COMMIT other than to MIG_RETIRE.
13. Wrong Migration — the Map Commits Before the Copy Completes
// WRONG — the map is switched as soon as the migration starts, "so new traffic
// lands in the right place while we copy".
always_ff @(posedge clk)
if (mig_start)
active_region_q[mig_region].target <= new_target; // ← before COPYThe reasoning sounds efficient and it produces split ownership.
1. The map now sends all reads and writes of the range to target B.
2. The copy from A to B is still in progress, at cursor X.
3. A read of an address ABOVE the cursor goes to B, which does not have it yet.
-> it returns whatever B holds: uninitialised, or another tenant's data.
4. A write to an address BELOW the cursor lands on B.
-> correct, and then the copy overwrites it with A's stale value.
5. A write ABOVE the cursor lands on B and is later overwritten by the copy.| Access | Address relative to cursor | Outcome |
|---|---|---|
| read | below | correct — already copied |
| read | above | returns whatever B holds — not the data |
| write | below | correct, then destroyed by the copy |
| write | above | correct, then destroyed by the copy |
Three properties.
Three of four rows are silent corruption. No error is reported anywhere; the transport is perfect; both devices behave correctly.
The failure is a function of a moving cursor, so it is timing-dependent and irreproducible. The same workload run twice corrupts different addresses.
And it looks like it should work if you only think about reads below the cursor. The mental model "copy from the front, serve from the front" is coherent for reads and completely wrong for writes — because the copy is a writer too, and it is writing older data.
The correct sequence keeps the old target authoritative until the commit (§11), which means new traffic goes to A throughout the copy, and the copy must reconcile anything A accepted while it ran (§14's MIG_DRAIN).
14. SVA — the Target Changes Only at the Commit
// MANDATORY. The active target for a migrating region changes only in MIG_COMMIT.
property p_target_changes_only_at_commit;
@(posedge clk) disable iff (!rst_n)
$changed(active_region_q[REG_UT].target)
|-> ($past(mig_q.state) == MIG_COMMIT) && $past(commit_fire);
endproperty
a_target_changes_only_at_commit:
assert property (p_target_changes_only_at_commit);
// The commit requires the copy and the reconciliation to have completed.
property p_commit_requires_copy_and_drain;
@(posedge clk) disable iff (!rst_n)
commit_fire |-> (copy_complete && reconcile_complete);
endproperty
a_commit_requires_copy_and_drain:
assert property (p_commit_requires_copy_and_drain);
// The old copy is not released before the commit.
property p_old_copy_held_until_commit;
@(posedge clk) disable iff (!rst_n)
old_copy_released |-> (mig_q.state == MIG_RETIRE);
endproperty
a_old_copy_held_until_commit: assert property (p_old_copy_held_until_commit);
// No abort after the commit — the commit is the point of no return (Section 12).
property p_no_abort_after_commit;
@(posedge clk) disable iff (!rst_n)
($past(mig_q.state) == MIG_COMMIT) |-> (mig_q.state != MIG_ABORT);
endproperty
a_no_abort_after_commit: assert property (p_no_abort_after_commit);Architecture. Four properties enforcing §11's ordering: commit only at the commit, commit only when the work is done, the fallback held until then, and no retreat afterwards.
Why the third matters as much as the first. A design can commit correctly and still release the old copy too early — and then a failure between the commit and the retirement leaves no authoritative source. The first three properties together are what make the migration recoverable at every point.
DV. All four always-on. The fourth needs an abort injected in each phase, and the test that matters is the one that tries to abort after the commit and must be refused.
15. The Dirty-Data Complication
The reason migration is not a memcpy.
1. Address X lives on target A. Its range is being migrated to B.
2. A COMPUTE CHIPLET holds X in its cache, DIRTY.
-> A's memory holds a STALE value. The newest value is in a cache.
3. The migration copies A's memory contents to B.
-> B now holds the stale value.
4. The map commits. Reads of X go to B and return the stale value.
5. Later the cache evicts its dirty line — to A, which is no longer the target.
-> the newest value is written to a device nobody reads.The newest value has been lost, and not one component did anything wrong.
| Where the newest value was | What the copy captured |
|---|---|
| in A's memory | ✓ the correct value |
| in a cache, dirty | ✗ A's stale value |
| in flight in a write that A has accepted but not applied | ✗ depends on timing |
A memory copy captures what the memory holds. Migration must capture what the system holds (16.1 §18's memory-value against latest-value distinction, with the highest possible stakes).
Two consequences.
Migration of a coherent range requires coherence participation. The dirty holders must be made to write back, or the migration must obtain ownership, before or during the copy. 16.2 is where those mechanisms live, and this chapter does not invent one — it states the requirement.
And the eviction in step 5 must be handled. A dirty line evicted after the commit is addressed to a target that is no longer authoritative. Either the caches are drained before the commit, or the old target must forward late writebacks — and choosing neither is how the failure above happens.
16. Wrong Migration — Copy the Physical Memory Contents
// WRONG for a coherent range — the copy engine reads the old target's memory
// and writes the new one, with no coherence participation.
always_ff @(posedge clk)
if (copy_active && copy_beat_fire) begin
write_target(new_target, copy_cursor, read_target(old_target, copy_cursor));
copy_cursor <= copy_cursor + BEAT_BYTES;
endThree properties.
It is correct for a non-coherent range — one that no cache holds, such as a device-private buffer. That is why it gets written: it works, in a test where nothing caches the range.
It silently loses every dirty line. The number of lost values equals the number of cached-dirty lines at copy time, which depends entirely on the workload. A migration performed on an idle system loses nothing; the same migration under load loses data.
And the loss is invisible until read. The migration reports success. The corruption surfaces later as a stale read, with no connection to the migration in any log.
The correct form makes the coherence requirement explicit in the code:
// ILLUSTRATIVE. A coherent range must obtain the authoritative value, not the
// memory's copy. The mechanism belongs to the coherence protocol (16.2).
assign copy_read_ok = tier_q[region_tier].coherent
? coherent_read_authoritative(old_target, copy_cursor)
: plain_read(old_target, copy_cursor);17. SVA — Migration Completeness
// MANDATORY. Every address in the range was copied before the commit.
property p_copy_covers_range;
@(posedge clk) disable iff (!rst_n)
commit_fire |-> (mig_q.copy_cursor > active_region_q[mig_q.region].limit);
endproperty
a_copy_covers_range: assert property (p_copy_covers_range);
// The cursor is monotonic — a restart after a recovery resumes, never rewinds
// past what is already reconciled (Section 37).
property p_copy_cursor_monotonic;
@(posedge clk) disable iff (!rst_n)
(mig_q.state == MIG_COPY) |-> (mig_q.copy_cursor >= $past(mig_q.copy_cursor));
endproperty
a_copy_cursor_monotonic: assert property (p_copy_cursor_monotonic);
// For a coherent range, the commit requires the coherence precondition to hold.
// The precondition itself is the coherence protocol's (16.2) — this asserts the
// migration WAITED for it, not what it is.
property p_coherent_range_reconciled;
@(posedge clk) disable iff (!rst_n)
(commit_fire && tier_q[region_tier].coherent) |-> no_dirty_holders_for_range;
endproperty
a_coherent_range_reconciled: assert property (p_coherent_range_reconciled);
// No request is ever accepted with a target that a migration is retiring.
property p_no_accept_to_retiring_target;
@(posedge clk) disable iff (!rst_n)
(req_accept_fire && (sel_region == mig_q.region))
|-> (sel_target != mig_q.old_target) || (mig_q.state < MIG_COMMIT);
endproperty
a_no_accept_to_retiring_target:
assert property (p_no_accept_to_retiring_target);Architecture. Four properties: coverage, monotonic progress, the coherence precondition, and no acceptance into a retiring target.
Why the third is written as "waited for", not "achieved". This chapter does not define what makes a range free of dirty holders — that is the coherence protocol's (16.2). What it can and must assert is that the migration did not commit until that condition was signalled, which is the boundary this chapter owns.
DV. The third needs a migration performed with a dirty line outstanding, which is §36's trace and never occurs spontaneously.
18. Completion Is Four Events, Not One
A distinction that becomes load-bearing the moment a tier is durable.
| Event | What is true afterwards | Enough for a volatile tier? | Enough for a durable tier? |
|---|---|---|---|
| transport delivered | the request crossed the link | no | no |
| controller accepted | the memory side owns the obligation | possibly — architecture-defined | no |
| volatile media updated | a read would now see the value | yes | no |
| durability achieved | the value survives power loss | n/a | this is the one |
Two consequences.
The gap between events can be large. Controller acceptance and durability may be separated by a substantial interval, and a design that treats them as one has either reported durability it does not have or waited for durability it did not need — a correctness bug in one direction and a performance bug in the other.
And the gap differs per tier. Which is exactly why §20's single done bit cannot work.
19. Tier-Specific Completion Policy
// ILLUSTRATIVE completion policy. NOT a standard-defined semantic (Section 3).
// The POLICY is per tier; the EVENTS are per request.
typedef enum logic [POLICY_W-1:0] {
CPL_ON_CONTROLLER = 'd0, // controller acceptance discharges the obligation
CPL_ON_MEDIA = 'd1, // a subsequent read must see it
CPL_ON_DURABLE = 'd2 // it must survive power loss
} completion_policy_e;
function automatic logic semantic_complete(
input completion_policy_e policy,
input logic controller_done,
input logic media_done,
input logic durable_done
);
unique case (policy)
CPL_ON_CONTROLLER: semantic_complete = controller_done;
CPL_ON_MEDIA: semantic_complete = media_done;
CPL_ON_DURABLE: semantic_complete = durable_done;
default: semantic_complete = 1'b0; // fail closed
endcase
endfunctionArchitecture. One function, three inputs, one policy selector taken from the captured tier of the request (§8). The policy is configuration; the events are per-request; and the function is the only place the two meet.
State. None — a pure function. The policy lives in the tier descriptor and the events come from the memory-side status path.
Cycle behaviour. Combinational, evaluated where the obligation would be retired. default returns false, so an unrecognised policy stalls rather than completing — fail-closed, because the failure mode of completing too early is unbounded and the failure mode of stalling is a visible hang.
Contract. The transaction table retires on this function's output and on nothing else. A design with any other retirement path has a second, unpoliced definition of completion.
Failure. §20. Also deriving the policy from the live tier table rather than the request's captured tier — which changes a request's completion rule underneath it if the configuration commits mid-flight, and is 17.2 §15's recompute bug applied to semantics rather than routing.
DV. Cover all three policies; assert the fail-closed default is unreachable in a correct configuration (§21).
20. Wrong RTL — One Completion Bit Across Tiers
// WRONG — one definition, applied everywhere.
assign txn_done = controller_accepted; // ← true for one tier, unsafe for another| Tier | What this reports | What is actually true |
|---|---|---|
| a tier whose architecture completes at controller acceptance | correct | correct |
| a tier requiring the media to be updated | complete | a subsequent read may not see it |
| a durable tier | complete | the value would not survive power loss |
Three properties.
It is correct for exactly one tier and reported identically for all of them. So a system that starts with one tier and later adds another inherits a silent correctness bug from a line that was right when it was written.
The failure is not observable at the interface. The requester is told the operation completed; it acts accordingly; and nothing detects the difference until a power event or a racing read.
And the reverse mistake costs performance, not correctness. A design that hardcodes the strongest policy for every tier is safe and slow — which is a legitimate conservative choice, and should be a stated decision rather than an accident (§19's function makes it one).
21. SVA — Completion Respects the Captured Policy
// MANDATORY. A transaction retires only when its OWN tier's policy is satisfied.
property p_retire_respects_policy;
@(posedge clk) disable iff (!rst_n)
txn_retire_fire |-> semantic_complete(
tier_q[txn_q[retire_id].tier].completion_policy,
txn_q[retire_id].controller_done,
txn_q[retire_id].media_done,
txn_q[retire_id].durable_done);
endproperty
a_retire_respects_policy: assert property (p_retire_respects_policy);
// The policy a request uses is the one captured at acceptance, not the live one.
property p_policy_captured_not_live;
@(posedge clk) disable iff (!rst_n)
txn_q[IDX].valid |-> $stable(txn_q[IDX].tier);
endproperty
a_policy_captured_not_live: assert property (p_policy_captured_not_live);
// A durable tier's descriptor and policy must agree — checked at commit.
property p_durable_tier_has_durable_policy;
@(posedge clk) disable iff (!rst_n)
tier_commit_fire |=> (tier_q[TIER_UT].durable
|-> (tier_q[TIER_UT].completion_policy == CPL_ON_DURABLE));
endproperty
a_durable_tier_has_durable_policy:
assert property (p_durable_tier_has_durable_policy);Architecture. Three properties: retirement respects the policy, the policy is captured, and the configuration is internally consistent.
Why the third fires at the commit. A tier marked durable with a weaker completion policy is a configuration error, and catching it when the configuration is written names the mistake. Catching it via the first property names only its first victim, thousands of cycles later (17.2 §10's argument).
DV. Cover a configuration with tiers of differing policies active simultaneously — which is the only condition under which §20's bug is distinguishable from correct behaviour.
22. Failure Domains Grow With Distance
| Memory location | Independent things that can fail |
|---|---|
| local media | media, local controller |
| on-package, across a link | + link, transport, remote controller (17.2 §4) |
| pooled or disaggregated | + fabric, allocation authority, configuration service, other clients |
Three consequences.
The failure rate of the whole is the composition, not the best component. A highly reliable memory device reached through a less reliable path is a less reliable memory. Availability is a property of the path, not of the device.
"Other clients" is a failure source, and it is the one people omit. A client that saturates a shared fabric degrades every other client (§28); a client that misconfigures shared state can affect others (§25). Neither is a hardware failure and both are availability events.
And more composability means more distributed configuration state, every piece of which has a lifetime and a commit discipline. The chapters that taught those — 14.4, 17.2, 17.4 — become more relevant as the architecture gets more flexible, not less.
23. Availability Is a State Machine, Not a Bit
// ILLUSTRATIVE per-target availability. Five states because five different
// responses are required. Not a normative model (Section 3).
typedef enum logic [2:0] {
TGT_AVAILABLE = 3'd0, // serving normally
TGT_DEGRADED = 3'd1, // serving, at a reduced service class <- Section 9
TGT_MIGRATING = 3'd2, // serving, but its range is being moved
TGT_UNAVAILABLE = 3'd3, // temporarily not serving — WAIT
TGT_LOST = 3'd4 // permanently unreachable — explicit failure
} target_avail_e;
target_avail_e avail_q [NUM_TARGETS];
logic [AGE_W-1:0] unavail_age_q [NUM_TARGETS]; // saturating| State | New requests | Outstanding requests | Expectations |
|---|---|---|---|
AVAILABLE | accepted | complete normally | as configured |
DEGRADED | accepted | complete, slower | must be re-derived (§9, §10) |
MIGRATING | accepted at the old target until commit | complete at the old target | unchanged |
UNAVAILABLE | held | remain live (17.2 §29) | unchanged |
LOST | failed explicitly | failed explicitly, with a report | n/a |
Architecture. Five states because there are five distinct correct responses. A design with fewer states must give two situations the same response, and every such merge is a bug in one of the two.
State. One three-bit register and one saturating age per target.
Cycle behaviour. DEGRADED and MIGRATING are serving states — the target still works. UNAVAILABLE is entered on recovery, maintenance or throttle and left on resumption, with the age promoting it to LOST past a bound.
Contract. Admission, the failure policy, the expectation derivation and the migration controller all read this. Four consumers, five states, and each consumer needs a different subset — which is precisely why one bit cannot serve them.
Failure. §24.
DV. Cover all five; cover the promotion from UNAVAILABLE to LOST; confirm outstanding transactions survive UNAVAILABLE and DEGRADED and are explicitly failed on LOST.
24. Wrong RTL — Availability as One Bit
// WRONG — one bit for five situations.
assign target_ok = !target_error;What the design must then choose, once, for all five:
If target_ok means "accept and wait" | If target_ok means "fail fast" |
|---|---|
| a permanently lost target hangs every request forever | a target in an ordinary maintenance window fails healthy requests |
| a degraded target is used with stale expectations (§9) | a migrating target's traffic is failed for no reason |
Neither column is acceptable, and the design must pick one. That is the whole argument for the state machine: the bit is not imprecise, it is insufficient — it cannot express a distinction the system genuinely has.
25. In a Shared Pool, Mapping Is Access Control
The invariant this section exists for:
Deciding which target an address routes to and deciding whether this client may reach that target are two questions. In a shared memory system they must both be answered, and answering only the first produces a route that is perfectly correct and completely unauthorised.
| Question | Answered by | If it is skipped |
|---|---|---|
| where does this address live? | the region map (§8) | the request goes nowhere, or to the wrong device |
| may this client access that region? | a permission check (§26) | isolation violation with a correct route |
Two properties of shared memory that make this unavoidable.
A stale mapping is an access-control hazard, not just a correctness one. If a region is reassigned from client A to client B and A retains a mapping, A's perfectly-formed requests reach B's data. The map is stale; the routing is correct; the isolation is gone.
And the check must be at the authoritative side, not only at the requester. A requester-side check is an optimisation. A requester that has a stale or malicious mapping is exactly the case the check exists for, so the decision has to be made where the resource lives.
26. Wrong RTL — a Valid Mapping Implies Permission
// WRONG — routing is treated as authorisation.
assign access_allowed = region_match; // ← "we found a target, so go"1. Region 4 is reassigned from client A to client B. The pool's authority
updates its allocation and B begins using the range.
2. Client A's local mapping is stale — it still names region 4.
3. A issues a read. region_match is TRUE: the region exists and names a target.
4. The read is routed correctly, transported correctly, and served correctly.
5. -> A reads B's data. Nothing failed.Three properties.
Every mechanism worked. The map matched, the route was right, the transport was clean, the memory returned exactly what it holds. There is no error to report anywhere in the path.
The requester cannot detect it, because from A's point of view the read succeeded and returned data. Only the resource's owner can detect it, and only if it checks.
And the fix is a second, independent decision:
// ILLUSTRATIVE. Two decisions, both required. The permission state is owned by
// whatever authority allocates the resource — NOT by the requester's map.
assign access_allowed =
region_match // WHERE — Section 8
&& region_perm_q[sel_region][req_domain] // WHETHER — owned elsewhere
&& (req_perm_epoch == region_perm_epoch_q[sel_region]); // and NOT staleThe epoch term is what closes the stale-mapping case. A permission granted under one allocation epoch is not a permission under the next. Without it, revocation is not enforceable — it only becomes effective when the client happens to refresh, which is not an enforcement mechanism.
27. SVA — Permission Is an Independent Decision
// MANDATORY. Access requires permission, and permission is not derived from the map.
property p_access_requires_permission;
@(posedge clk) disable iff (!rst_n)
req_accept_fire |-> region_perm_q[sel_region][req_domain];
endproperty
a_access_requires_permission: assert property (p_access_requires_permission);
// A stale permission epoch is refused.
property p_stale_permission_refused;
@(posedge clk) disable iff (!rst_n)
(req_offered && (req_perm_epoch != region_perm_epoch_q[sel_region]))
|-> !req_accept_fire;
endproperty
a_stale_permission_refused: assert property (p_stale_permission_refused);
// Revocation takes effect: after a permission epoch advances, no request from a
// revoked domain is accepted.
property p_revocation_effective;
@(posedge clk) disable iff (!rst_n)
(perm_commit_fire && !requested_perm[REG_UT][DOM_UT])
|=> always (!(req_accept_fire && (sel_region == REG_UT) && (req_domain == DOM_UT)));
endproperty
a_revocation_effective: assert property (p_revocation_effective);
// A permission check is never satisfied by the routing result alone.
property p_permission_not_derived_from_route;
@(posedge clk) disable iff (!rst_n)
(region_match && !region_perm_q[sel_region][req_domain]) |-> !req_accept_fire;
endproperty
a_permission_not_derived_from_route:
assert property (p_permission_not_derived_from_route);Architecture. Four properties: permission required, staleness refused, revocation effective, and the route explicitly insufficient.
Why the fourth is not a duplicate of the first. The first says permission was present when accepted. The fourth says the specific bad combination — route matched, permission absent — cannot be accepted, which is the exact shape of §26's bug and is what a reviewer looks for.
Why revocation needs its own property. Granting is easy to get right and revoking is not. A design that checks permission but never advances the epoch has an access-control system that can only add rights, and that is a common and serious hole.
DV. Inject a revocation with a client still holding a stale mapping — §34's coverage bin, and it must be constructed.
28. Capacity Isolation Is Not Bandwidth Isolation
Pooling solves a capacity problem. It can create a performance one.
| Isolation kind | What it guarantees | Provided by |
|---|---|---|
| capacity | client A cannot consume client B's allocated space | the allocation authority |
| bandwidth | client A cannot consume client B's service rate | a scheduling mechanism — and only if one exists |
| latency | client A cannot inflate client B's latency | a stronger scheduling mechanism |
Allocating capacity says nothing about who gets served. A client with a small allocation can saturate a shared fabric and degrade every other client's latency — using only memory it legitimately owns.
Two consequences.
A pool without bandwidth isolation has a noisy-neighbour problem by construction, and no amount of capacity accounting detects it. The symptom appears at the victim, whose own metrics all look normal except that everything is slow.
And the isolation mechanism must be per client, in the shared resource. Per-client counting at the requester is advisory. Enforcement has to happen where the contention is — which is §29.
29. A Per-Client Bandwidth Quota
// ILLUSTRATIVE token-bucket quota. Generic mechanism; NO standard is claimed to
// mandate it or any parameterisation of it (Section 3).
logic [TOK_W-1:0] tokens_q [NUM_CLIENTS];
logic [WIN_W-1:0] refill_q;
localparam int TOK_MAX = 64; // burst allowance, in service units
localparam int REFILL_RATE = 4; // tokens added per refill tick, per client
always_ff @(posedge clk or negedge rst_n)
if (!rst_n) begin
for (int c = 0; c < NUM_CLIENTS; c++) tokens_q[c] <= TOK_MAX[TOK_W-1:0];
refill_q <= '0;
end else begin
refill_q <= (refill_q == REFILL_PERIOD - 1) ? '0 : refill_q + 1'b1;
for (int c = 0; c < NUM_CLIENTS; c++) begin
// Refill and spend are INDEPENDENT events and can coincide.
unique case ({(refill_q == REFILL_PERIOD - 1),
(grant_fire && (grant_client == c[CLI_W-1:0]))})
2'b10: tokens_q[c] <= (tokens_q[c] + REFILL_RATE > TOK_MAX)
? TOK_MAX[TOK_W-1:0]
: tokens_q[c] + REFILL_RATE[TOK_W-1:0];
2'b01: tokens_q[c] <= tokens_q[c] - 1'b1;
2'b11: tokens_q[c] <= (tokens_q[c] + REFILL_RATE - 1 > TOK_MAX)
? TOK_MAX[TOK_W-1:0]
: tokens_q[c] + REFILL_RATE[TOK_W-1:0] - 1'b1;
default: ;
endcase
end
end
assign client_eligible = (tokens_q[req_client] != '0);Architecture. A bucket per client that fills at a rate and drains on service. The bucket size is the burst allowance and the refill rate is the sustained share — two independent knobs, which is why a token bucket is used rather than a simple rate counter.
State. NUM_CLIENTS counters plus one shared refill timer. Per client, in the shared resource (§28) — a bucket at the requester enforces nothing.
Cycle behaviour. Refill and spend are independent events that can occur in the same cycle, so the 2'b11 arm is written explicitly. Two separate if statements lose an update on that cycle, and the bucket drifts — slowly, in one direction, until a client is either permanently throttled or unthrottled.
Contract. The arbiter treats client_eligible as a gate. The guarantee delivered to each client is REFILL_RATE / REFILL_PERIOD of the resource, sustained, with a burst of TOK_MAX — and stating it in those terms is what makes it reviewable.
Failure. §30. Also allowing the bucket to saturate at a value large enough to permit an unbounded burst, which defeats the sustained guarantee for everyone else.
DV. §31's bounds and conservation. Drive one client at maximum and confirm the others still achieve their configured share.
30. Wrong RTL — One Quota for Everyone
// WRONG — a single shared bucket. It limits TOTAL traffic and isolates nobody.
logic [TOK_W-1:0] tokens_q;
assign client_eligible = (tokens_q != '0);| Client | Offered load | Tokens consumed | Service received |
|---|---|---|---|
| A (aggressive) | continuous | nearly all | nearly all |
| B (modest) | occasional | almost none | almost none — it arrives to an empty bucket |
| C (modest) | occasional | almost none | almost none |
Three properties.
The total is correctly limited, which is what makes it look like it works. Aggregate bandwidth is exactly the configured rate.
And the distribution is the opposite of isolation. The client that asks most often gets most, which is precisely the behaviour a quota is supposed to prevent. A shared bucket is a rate limiter, not an isolation mechanism, and the two are frequently conflated.
The victim has no local evidence. B's requests are simply not granted. Nothing in B's own counters distinguishes "the memory is slow" from "A took my share" — only a per-client view at the shared resource does (§32).
31. SVA — Quota Bounded and Conserved
// MANDATORY. Buckets are bounded on both sides.
property p_tokens_bounded(int c);
@(posedge clk) disable iff (!rst_n)
(tokens_q[c] <= TOK_MAX);
endproperty
a_tokens_bounded: assert property (p_tokens_bounded(CLI_UT));
// Service requires a token — the gate is real.
property p_grant_requires_token;
@(posedge clk) disable iff (!rst_n)
grant_fire |-> (tokens_q[grant_client] != '0);
endproperty
a_grant_requires_token: assert property (p_grant_requires_token);
// Simultaneous refill and spend nets to the right value (Section 29).
property p_refill_and_spend_consistent(int c);
@(posedge clk) disable iff (!rst_n)
(refill_tick && grant_fire && (grant_client == c))
|=> (tokens_q[c] == min(TOK_MAX, $past(tokens_q[c]) + REFILL_RATE - 1));
endproperty
a_refill_and_spend_consistent:
assert property (p_refill_and_spend_consistent(CLI_UT));
// Isolation, as a bounded liveness claim with assumptions:
// A1: the shared resource eventually serves a granted request
// A2: a client with tokens keeps requesting until served
property p_client_served_within_bound;
@(posedge clk) disable iff (!rst_n)
(client_req[CLI_UT] && (tokens_q[CLI_UT] != '0))
|-> ##[1:CLIENT_SERVICE_BOUND] (grant_fire && (grant_client == CLI_UT));
endproperty
a_client_served_within_bound: assert property (p_client_served_within_bound);Architecture. Bounds, gate enforcement, the simultaneous-event check, and the isolation guarantee as bounded liveness.
The fourth property is the one that means "isolation". Bounds and gates prove the mechanism exists; only the liveness bound proves that a well-behaved client gets served regardless of what other clients do — which is the entire point (§30).
DV. Prove the fourth with one client offering maximum load. Then replace the per-client buckets with a shared one and confirm it fails, which demonstrates that the property is testing what it claims.
32. Observability Across Tiers
// Diagnostic only. Per TIER, per TARGET and per CLIENT — because the three
// questions are different and an aggregate answers none of them.
logic [63:0] tier_requests_q [NUM_TIERS];
logic [63:0] tier_bytes_q [NUM_TIERS];
logic [63:0] tier_latency_sum_q [NUM_TIERS]; // /requests -> mean, per tier
logic [63:0] target_unavail_q [NUM_TARGETS];
logic [63:0] target_degraded_q [NUM_TARGETS]; // Section 23
logic [63:0] client_granted_q [NUM_CLIENTS];
logic [63:0] client_throttled_q [NUM_CLIENTS]; // token-starved cycles
logic [63:0] perm_denied_q [NUM_CLIENTS]; // Section 27
logic [63:0] migration_cycles_q;
logic [63:0] migration_stall_q; // work held for a quiesce| Counter | Answers | Without it |
|---|---|---|
tier_latency_sum_q per tier | is a tier meeting its service class? (§9) | tiers are averaged together and describe no real access |
client_throttled_q | is this client being throttled, or is the memory slow? | §30's victim has no evidence |
perm_denied_q | is something misconfigured, or is something probing? | denials are invisible |
target_degraded_q | how long has a target been below class? | expectations are never re-derived |
migration_stall_q | what does reconfiguration cost? | quiesce cost is blamed on the memory |
Two properties.
Three different keys, because three different questions. Per tier answers is this service class working; per target answers is this device working; per client answers am I getting my share. An aggregate counter answers none of them, and this is 15.3 §14's per-link argument generalised.
And client_throttled_q is the highest-value counter in the list. It converts §28's invisible failure into a measurement, at the cost of one comparator per client.
33. The Future-Memory Scoreboard
// Verification-only. FIVE models, because this architecture has five kinds of
// state that can independently be wrong.
class future_memory_scoreboard;
// ---- Layer 1: MAPPING model — address to tier/target, per epoch.
typedef struct {
int tier;
int target;
int epoch;
} map_model_t;
map_model_t maps [int][bit [ADDR_W-1:0]]; // [epoch][address] — all epochs kept
// ---- Layer 2: AUTHORITATIVE VALUE model. WHERE the newest value lives,
// which is NOT the same as what memory holds (Section 15).
typedef struct {
bit [DATA_W-1:0] value;
int holder; // -1 = memory; otherwise a cache agent id
bit dirty;
} authoritative_t;
authoritative_t authoritative [bit [ADDR_W-1:0]];
// ---- Layer 3: MIGRATION model.
typedef struct {
int region;
int old_target;
int new_target;
int state;
bit [ADDR_W-1:0] cursor;
bit committed;
} migration_model_t;
migration_model_t mig;
// ---- Layer 4: PERMISSION model.
bit perms [int][int]; // [region][domain]
int perm_epoch [int]; // [region]
// ---- Layer 5: COMPLETION model.
typedef struct {
int policy;
bit controller_done;
bit media_done;
bit durable_done;
bit retired;
} completion_model_t;
completion_model_t cpl [int]; // keyed by request identity
// ---- Catches Section 13 — split ownership during a migration.
function void check_no_split_ownership(bit [ADDR_W-1:0] a, int served_by);
int expect = mig.committed ? mig.new_target : mig.old_target;
if ((maps[mig_epoch][a].target == mig.region) && (served_by != expect))
$error("SPLIT OWNERSHIP addr %0h served by %0d, authoritative %0d (mig state %0d)",
a, served_by, expect, mig.state);
endfunction
// ---- Catches Section 16 — the copy captured a stale value.
function void check_migration_captured_authoritative(bit [ADDR_W-1:0] a,
bit [DATA_W-1:0] copied);
if (copied !== authoritative[a].value)
$error("MIGRATION COPIED STALE addr %0h: copied %0h, authoritative %0h held by %0d",
a, copied, authoritative[a].value, authoritative[a].holder);
endfunction
// ---- Catches Section 26 — a correct route with no permission.
function void check_access(int region, int domain, int epoch);
if (!perms[region][domain] || (epoch != perm_epoch[region]))
$error("UNAUTHORISED ACCESS region %0d domain %0d epoch %0d (current %0d)",
region, domain, epoch, perm_epoch[region]);
endfunction
// ---- Catches Section 20 — retired before the tier's policy was satisfied.
function void check_completion(int id);
bit ok;
case (cpl[id].policy)
0: ok = cpl[id].controller_done;
1: ok = cpl[id].media_done;
2: ok = cpl[id].durable_done;
default: ok = 1'b0;
endcase
if (cpl[id].retired && !ok)
$error("PREMATURE RETIREMENT id %0d policy %0d (ctrl %0b media %0b durable %0b)",
id, cpl[id].policy, cpl[id].controller_done,
cpl[id].media_done, cpl[id].durable_done);
endfunction
endclassArchitecture. Five models, because five kinds of state can independently be wrong: the map, the authoritative value, the migration, the permissions, and the completion.
Layer 2 is the one that makes this chapter's hardest bug detectable. It tracks where the newest value lives, not what memory contains. A model that only tracks memory contents cannot detect §16, because from memory's point of view the copy was faithful — it copied exactly what was there, and what was there was stale.
And every map epoch is retained, for the same reason 17.2 §35 keeps them: a request accepted under one configuration must be checked against that configuration.
34. Coverage
covergroup cg_future_memory @(posedge clk);
option.per_instance = 1;
// --- Tiering (Sections 7-10).
cp_tier : coverpoint sel_tier { bins each[] = {[0:NUM_TIERS-1]}; }
cp_tier_mix : coverpoint num_tiers_active {
bins one = {1}; bins two = {2}; bins many = {[3:$]}; // Section 21 needs >1
}
cp_expectation_rederived : coverpoint expectation_rederived_after_commit;
// --- Migration (Sections 11-17).
cp_mig_state : coverpoint mig_q_state { bins each[] = {[0:6]}; }
cp_mig_abort_phase : coverpoint abort_injected_in_phase {
bins quiesce = {1}; bins copy = {2}; bins drain = {3};
bins after_commit = {4}; // MUST be refused
}
cp_mig_with_dirty : coverpoint dirty_line_present_during_copy; // Section 15
cp_mig_access : coverpoint access_during_migration {
bins none = {0};
bins read_below_cursor = {1};
bins read_above_cursor = {2}; // Section 13's corruption
bins write_below_cursor = {3};
bins write_above_cursor = {4};
}
cp_recovery_during_mig : coverpoint link_recovery_during_migration;
// --- Completion (Sections 18-21).
cp_policy : coverpoint active_completion_policy {
bins on_controller = {0}; bins on_media = {1}; bins on_durable = {2};
}
cp_policy_gap : coverpoint controller_to_durable_gap_class {
bins zero = {0}; bins short = {[1:16]}; bins long = {[17:$]};
}
// --- Availability (Sections 22-24).
cp_avail : coverpoint avail_q_ut { bins each[] = {[0:4]}; }
cp_avail_promotion : coverpoint unavailable_promoted_to_lost;
// --- Isolation (Sections 25-31).
cp_perm : coverpoint permission_outcome {
bins granted = {0}; bins denied = {1}; bins stale_epoch = {2};
}
cp_revocation : coverpoint revocation_with_stale_client_map; // Section 27
cp_clients : coverpoint num_clients_requesting {
bins one = {1}; bins some = {[2:3]}; bins many = {[4:$]};
}
cp_throttled : coverpoint tokens_q_ut {
bins empty = {0}; // the client is being throttled
bins some = {[1:TOK_MAX-1]};
bins full = {TOK_MAX};
}
// --- Crosses that carry the information.
x_mig_access : cross cp_mig_state, cp_mig_access; // Section 13
x_mig_dirty : cross cp_mig_with_dirty, cp_mig_state; // Section 15
x_policy_tier : cross cp_policy, cp_tier; // Section 20
x_clients_thr : cross cp_clients, cp_throttled; // Section 30
x_avail_expect : cross cp_avail, cp_expectation_rederived;
endcovergroupSix bins worth calling out:
cp_mig_access.read_above_cursor and .write_below_cursor. §13's exact corruption cases. They require an access issued during a copy at a known cursor position, which no random test constructs.
cp_mig_with_dirty. §15 and §16. A migration on an idle system loses nothing, so this bin is the difference between a test that passes and a test that means something.
cp_mig_abort_phase.after_commit. Must be refused, not achieved — a detector bin (§12).
cp_tier_mix above one, crossed with cp_policy. §20's bug is invisible with a single tier, because one done bit is correct when there is one policy.
cp_revocation. §27's third property, requiring a client holding a stale mapping at the moment of revocation.
And cp_throttled.empty crossed with cp_clients.many. §30 — a client at zero tokens while several compete, which is the state in which a shared bucket and per-client buckets behave differently.
35. Flagship Trace 1 — a Correct Migration
Illustrative. Region 4 migrates from target A to target B. Cycle numbers illustrative.
| Cyc | Migration state | Region outstanding | Map target | Copy cursor | New reads go to | Note |
|---|---|---|---|---|---|---|
| 0 | IDLE | 3 | A | — | A | steady |
| 1 | QUIESCE | 3 | A | — | held | no new accepts for region 4 |
| 8 | QUIESCE | 1 | A | — | held | draining |
| 14 | COPY | 0 | A | base | A | A is still authoritative |
| 20 | COPY | 0 | A | base + 4K | A | copy progressing |
| 60 | COPY | 0 | A | base + 64K | A | — |
| 96 | DRAIN | 0 | A | > limit | A | reconciling |
| 104 | COMMIT | 0 | A | — | A | commit conditions checked |
| 105 | RETIRE | 0 | B | — | B | atomic, epoch advanced |
| 112 | IDLE | 0 | B | — | B | A's copy released |
Five readings.
Cycles 14 to 96: the map still says A. Every access during the copy is served by A, which holds the authoritative data throughout. §13's design flips the map at cycle 1 and corrupts everything above the cursor.
The quiesce costs 13 cycles here (1 to 14), waiting for three outstanding requests. That cost is measurable (migration_stall_q, §32) and is routinely mistaken for memory slowness.
The copy runs with zero outstanding requests to the region — which is what MIG_QUIESCE bought, and it is what makes the reconciliation in MIG_DRAIN small.
Cycle 105 is the only cycle in the trace where the target changes. §14's first property is exactly that.
And A's copy is released at cycle 112, seven cycles after the commit. Between 105 and 112 both copies exist. That overlap is deliberate — it is the fallback window (§11).
36. Flagship Trace 2 — a Migration With a Dirty Line
Same migration, with address X cached dirty on a compute chiplet.
| Cyc | Migration | X in A's memory | X in a cache | Copy captured | Correct? |
|---|---|---|---|---|---|
| 0 | IDLE | V_old | V_new, dirty | — | — |
| 14 | COPY | V_old | V_new, dirty | — | — |
| the wrong design (§16) | |||||
| 30 | COPY | V_old | V_new, dirty | V_old | ✗ stale |
| 105 | committed | — | V_new, dirty | B holds V_old | ✗ |
| 140 | — | — | evicted to A | B holds V_old | ✗ newest value written to a dead target |
| 200 | — | — | — | a read returns V_old | ✗ silent corruption |
| the correct design (§16) | |||||
| 20 | COPY | V_old | write-back forced | — | ✓ |
| 24 | COPY | V_new | invalid | — | ✓ |
| 30 | COPY | V_new | — | V_new | ✓ |
| 96 | DRAIN | — | — | — | no dirty holders (§17) |
| 105 | committed | — | — | B holds V_new | ✓ |
Four readings.
The wrong design's copy is faithful and wrong. It copied exactly what A's memory held. The failure is in what it chose to read, not in how it read it.
Cycle 140 is the second half of the loss. The cache eventually evicts to A — the old target, now retired — so the newest value is written somewhere nobody reads. Even if someone later noticed B was stale, the correct value is not recoverable from anywhere the system consults.
In the correct design the write-back happens at cycle 20, before the copy reaches X. The coherence mechanism belongs to 16.2; the migration's obligation is to require it and to wait (§17's third property).
And cycle 96 is the check that makes it provable. no_dirty_holders_for_range is a precondition of the commit, so a migration cannot complete with the problem outstanding.
37. Flagship Trace 3 — a Recovery During a Migration
| Cyc | Migration | Copy cursor | Link | Map target | Must be true |
|---|---|---|---|---|---|
| 40 | COPY | base + 32K | operational | A | — |
| 44 | COPY | base + 36K | error detected | A | — |
| 45 | COPY | base + 36K | recovery entered | A | nothing semantic changes |
| 46 | COPY | base + 36K | quiescing | A | the copy pauses; the cursor holds |
| 58 | COPY | base + 36K | recovered, x8 → x4 | A | capacity changed only |
| 59 | COPY | base + 36K | operational | A | copy resumes from the cursor |
| 60 | COPY | base + 40K | operational | A | progress continues |
| 96 | DRAIN | > limit | operational | A | — |
| 105 | RETIRE | — | operational | B | one commit, one migration |
Five readings.
Cycle 45: the migration does not restart and does not abort. It is a semantic operation; a link event is a transport event (14.2 §4). A design that aborts here has discarded 36K of completed copying for no reason — and worse, has taken an abort path whose correctness depends on nothing having been committed.
The cursor is what makes resumption possible, and §17's monotonicity property is what makes resumption safe: it may not rewind past what has already been reconciled.
Cycle 58: the link returns narrower. The copy will now take longer. The migration's own timeout — if it has one — must be rescaled, or the design aborts a healthy migration on a recovered link (16.5 §27).
The map says A throughout, including across the recovery. The two events are independent and neither may disturb the other.
And there is exactly one commit. A restart-on-recovery design under a flapping link performs the copy repeatedly and may never commit at all — liveness lost to a transport event, which is the failure mode of treating a semantic operation as retryable transport.
38. Debug Taxonomy
| Signature | Most likely cause | First instrument |
|---|---|---|
| Data corrupt only during a migration window | §13 — the map committed before the copy completed | when did the target change relative to the cursor? |
| A migrated range returns stale values, migration reported success | §15, §16 — the copy captured memory, not the authoritative value | were there dirty holders at copy time? |
| Correct data, large latency regression after a reconfiguration | §9, §10 — an expectation not re-derived | which bounds derive from the captured tier |
| A client reads another client's data, everything reports success | §26 — routing treated as authorisation | is there a permission check, and does it have an epoch? |
| Revoking access has no effect until the client restarts | §27 — the permission epoch never advances | does revocation commit an epoch? |
| One client is slow, its own metrics look normal | §28, §30 — a shared quota, or none | client_throttled_q against client_granted_q |
| High pooled capacity, poor throughput | §28 — capacity isolation without bandwidth isolation | per-client grant distribution |
| Recovery causes a migration to restart from the beginning | §37 — a semantic operation treated as retryable transport | does the copy cursor survive a link event? |
| A durable tier loses data on power loss despite reported completion | §20 — one completion definition across tiers | which policy did the retiring transaction use? |
| A permanently removed target hangs every request | §24 — availability collapsed to one bit | is LOST distinguishable from UNAVAILABLE? |
Row 2 is the hardest failure in the chapter. A migrated range returns stale values while the migration reported success has no error anywhere in any log, and only a model that tracks where the newest value lives can attribute it (§33's Layer 2).
39. Debug Checklist
- Which tier and which target does this address map to, under which epoch? (§8)
- Was the tier captured with the request, or read live? (§21)
- Which completion policy applies, and which events had fired at retirement? (§19, §21)
- Is a migration active for this region, and in which phase? (§12)
- Where is the copy cursor relative to this address? (§13)
- When did the map target change — and was it at the commit? (§14)
- Were there dirty holders for this range during the copy? (§15, §17)
- Did any cache evict to the old target after the commit? (§15)
- Was the old copy released before or after the commit? (§14)
- Did a link retry or recovery occur during the migration? (§37)
- Did the copy cursor survive it, or did the migration restart? (§37)
- Did the link return degraded, and were bounds rescaled? (§10, §37)
- What is the target's availability state, and for how long? (§23)
- Was the performance expectation re-derived after the last commit? (§9, §10)
- Which client and which domain issued this request? (§25)
- Was permission checked independently of the route? (§26, §27)
- What permission epoch did the request carry, against the current one? (§27)
- How many tokens does this client have, and how long has it been at zero? (§29, §32)
- What is the per-client grant distribution at the shared resource? (§30, §32)
- Which of the five scoreboard layers diverged first? (§33)
40. Common Misconceptions
"Future memory is mostly about new media technology." Media changes latency, bandwidth, density and persistence. It does not change address ownership, latest-value ownership, identity, ordering, configuration lifetime, completion or recovery — and the engineering that composes a system is almost entirely about those (§4).
"Migration is just remapping an address." It is a five-phase state transfer in which the commit comes fourth, the old target stays authoritative throughout the copy, and the old copy is held as a fallback until the new one is proven. Remapping first corrupts every address above the copy cursor, in three of four access cases, silently (§11, §13).
"A memory copy is enough to migrate a range." A copy captures what memory holds. If a line is cached dirty, memory holds a stale value — so the copy is faithful and wrong, and the newest value is later evicted to a target nobody reads (§15, §16).
"More capacity automatically improves performance." Usable bandwidth is bounded by the path, the outstanding depth and the workload's demand, not by capacity (17.2 §21). Pooling in particular solves a capacity problem and can create a performance one (§28).
"Persistence changes only software." It changes what completion means, and therefore which hardware event may retire an obligation. A single done bit that was correct for a volatile tier silently reports durability it does not have (§18, §20).
"Memory pooling solves bandwidth bottlenecks." It solves capacity allocation. Without a per-client scheduling mechanism in the shared resource, one client can consume the service rate while staying entirely within its capacity allocation — and the victim's own metrics all look normal (§28, §30).
"Transport reliability guarantees migration correctness." Every byte of a migration can cross with clean CRC while the migration copies stale data, commits early, or releases the old copy too soon. Transport correctness is a strictly weaker claim (16.5 §29).
"A shared memory pool is safe if the routing is correct." Routing answers where; authorisation answers whether. A stale mapping produces a perfectly-routed, perfectly-transported, perfectly-served read of another client's data — and only the resource's owner can detect it, and only if it checks (§25, §26).
"One completion definition works for every memory tier." It is correct for exactly one tier and reported identically for all of them, so adding a second tier inherits a silent bug from a line that was right when written (§20).
"Remote memory is just local memory with longer wires." It adds a link, a transport, a remote controller, and — when pooled — a fabric, an allocation authority and other clients. Availability is a property of the path, not of the device (§22).
"Predicting the architecture matters more than knowing the invariants." The architecture will change. The list in §4 is what a design is still judged against afterwards, which is why this chapter is organised around it and names no product.
41. Understanding Check
42. Module 17 Complete — and What Comes Next
Module 17 in five chapters:
| 17.1 | The memory chiplet | a stateful endpoint with scheduled unavailability, not a passive target |
| 17.2 | Expansion | remote capacity in one map, and a stall classifier that never says "memory is slow" |
| 17.3 | Near-memory compute | trading data movement for command movement, and paying for it correctly |
| 17.4 | HBM integration | many channels, and the concurrency needed to use them |
| 17.5 | Future memory | new placements of the same obligations |
One sentence holds across all five: memory is not a passive target, and every one of its properties has a lifetime the rest of the system must respect.
Placement changes; obligations do not. Address ownership, latest-value ownership, identity, ordering, configuration lifetime, completion, recovery and observability survive every architecture in this chapter.
Migration is a state transfer with the commit fourth, an old target that stays authoritative throughout, and a copy that must capture what the system holds rather than what memory holds.
Completion is four events, and one bit cannot express a distinction the system genuinely has — which is also why availability needs five states rather than one.
And in a shared pool, mapping is access control and capacity is not bandwidth. Both are places where a perfectly correct route produces a completely wrong outcome.
Module 17 asked what happens when memory becomes the interesting part of the package. Module 18 turns to the other side of the boundary: the compute that consumes all this bandwidth. The next chapter treats an AI accelerator not as a peak-throughput number but as a compute island with local state, local queues, a finite communication surface, and a job whose lifetime the host cannot take back.
- 18.1 — AI Chiplets — AI compute dies in a chiplet package.
Browse the full path on the UCIe tutorials index.