CXL · Module 11
Why Memory Expansion Matters
Capacity is welded to the socket: a product of channels, slots and density that no workload respects. What the ceiling costs, how pressure is measured, why free memory on the wrong socket is stranded, and the evidence an architect brings to a capacity review.
Module 10 finished the device taxonomy — what kind of thing a CXL device is, and what each type obliges. Module 11 turns from the device to the system it exists to build, and it starts with the question that has to be answered before any of it is worth doing.
Not what can CXL memory do. What is actually wrong with the memory a server already has?
1. The Engineering Problem — Buying Two Things in a Fixed Ratio
A server is bought as a bundle. Cores and memory capacity arrive welded together, because the memory is attached to the socket and the socket has a fixed number of DDR channels.
The workload does not know this. Some workloads want enormous capacity and modest compute — an in-memory database, a recommendation model's embedding tables, a large graph. Others want the reverse.
Neither can be bought. You buy the ratio the socket offers, and then one of two things happens:
- Memory runs out first, so cores sit idle in a machine that is "full".
- Cores run out first, so memory sits unused in a machine that is "full".
Both are the same failure wearing different clothes: a fixed ratio meeting a workload that does not share it. And the second case is worse than it sounds, because that unused memory is not merely idle — it is unreachable by the machine next door that needs it.
That is the problem Module 11 is about. Expansion is not "more RAM". It is decoupling a ratio that hardware welded together.
2. The One-Sentence Model
Capacity is welded to the socket. It is the product of channels, slots per channel and per-module density — three numbers fixed when the board was designed — so capacity cannot be raised later at any price, and capacity that goes unused on one socket cannot serve demand on another.
Call it the welded ratio. Everything in this module is about unwelding it, and everything in this chapter is about proving the weld is real and measuring what it costs.
3. What This Chapter Owns
Memory expansion touches a lot of later curriculum, so the boundaries matter from the first page.
| Ground | Owner |
|---|---|
| Why the ceiling exists, and what it costs | this chapter |
| How CXL raises addressable capacity | 11.2 |
| Sharing one device between a few hosts | 11.3 |
| Server designs built around expansion | 11.4 |
| What AI workloads demand of it | 11.5 |
And the deferrals:
| Deferred ground | Owner |
|---|---|
| Pools, many hosts, many devices | Module 12 |
| Fabric managers, switches | Modules 15, 16 |
| Real expander products and media | Module 17 |
| Latency anatomy, throughput modelling | Module 18 |
| Rack and datacentre architecture | Module 23 |
This chapter quantifies capacity pressure. It deliberately does not decompose latency — that is 18.1's job, and Module 9 already priced what CXL.mem's guarantees cost in 9.6.
The two boxes on the bottom row are the same failure. A fixed ratio met a workload that did not share it, and whichever resource ran out first determined which half of the machine was wasted. Neither outcome is a purchasing mistake — both are forced by the weld.
4. Teaching-model boundary
5. RTL 1 — Capacity Is a Product, Not a Preference
The weld, in hardware.
// The ceiling is a product of three physical facts, fixed at board build.
localparam logic [15:0] CEILING_GB = CHANNELS * SLOTS_PER * DENSITY_GB;
always_comb begin
fits = ((committed_q + req_gb) <= CEILING_GB);
grant = req_valid && (ELASTIC || fits);
refuse = req_valid && !ELASTIC && !fits;
end=== EXP1: locally attached capacity is a product, not a preference ===
installed = channels x slots x density = 8 x 2 x 64 = 1024 GB
the ceiling is exactly 1024 GB : ok
6 x 200 GB + exact 24 GB : committed=1024 refused=1 peak=1024
independent oracle : committed=1024 refused=1
committed capacity matched an independent oracle : ok
the last 24 GB landed exactly on the ceiling : ok
peak recorded the value being created, not the previous one : ok
elastic variant : committed=1224 overcommit flag=1
the elastic variant committed beyond physical memory : okThree multiplicands, and you can change none of them after the board exists. Channels are a property of the CPU and the board. Slots per channel are a property of the board. Density is a property of the modules you can buy — and it is the only one with any headroom, which is why "just buy bigger DIMMs" is the first thing every team tries and the first thing that runs out.
The ELASTIC variant is the marketing model of memory, and it is included because it is how capacity is described in every planning conversation that has not yet met the board. It commits 1224 GB into 1024 GB of physical DRAM and the overcommit checker catches it. Real hardware has no such variant: the request simply fails.
Note where the checker sits. overcommit_err is written without a guard on ELASTIC — guarding it would switch the checker off on the only configuration that can ever trip it, which is the self-disabling-checker defect this track has now seen three times.
6. RTL 2 — The Number That Matters Is Distinct Pages
Sizing memory from access rate is the most common way a capacity estimate goes wrong, in both directions.
assign first_touch = acc_valid && !seen_q[page_id];
// What the counter advances on: a first touch (correct) or any access.
assign advance = COUNT_ACCESS ? acc_valid : first_touch;=== EXP2: the number that matters is DISTINCT pages, not accesses ===
20 accesses over 4 pages : distinct=4 accesses=20 | count-access variant distinct=20
independent oracle : distinct=4
distinct-page count matched an independent oracle : ok
4 distinct pages from 20 accesses : ok
the count-access variant reported 20 -- 5x the true working set : ok
and exceeded the page count, which is impossible for a working set : ok
after a window roll, same 4 pages re-touched : distinct=4
the window rolled and the seen-set cleared : okTwenty accesses, four pages. A design sized from the access count provisions five times the memory this workload actually needs resident.
The error runs the other way too. A workload that touches a million distinct pages once each has a low access rate per page and an enormous working set — and an access-rate estimate will under-provision it just as badly.
The impossible-value check is the cheap one to keep. A distinct-page count can never exceed the number of pages that exist, so the count-access variant tripped miscount_err the moment it passed 16. That is one comparator, and it catches a whole class of metric that has quietly stopped meaning what its name says.
The window matters as much as the counter. Working set is distinct pages in a window, and the window length is a policy choice: too short and everything looks hot, too long and everything looks like the working set. The roll test proves the seen-set clears — a window that never resets reports the workload's lifetime footprint and calls it a working set.
7. RTL 3 — Pressure Is a State, Not a Comparison
case (st_q)
P_EASY: if (occupancy_pct >= HI_ENTER) st_n = P_TIGHT;
P_TIGHT: begin
if (occupancy_pct >= CR_ENTER) st_n = P_CRITICAL;
else if (occupancy_pct < HI_EXIT_EFF) st_n = P_EASY;
end
P_CRITICAL: if (occupancy_pct < CR_EXIT_EFF) st_n = P_TIGHT;=== EXP3: pressure is a state with hysteresis, not a comparison ===
exactly 80% entered TIGHT (the threshold is inclusive) : ok
97% entered CRITICAL : ok
leaving CRITICAL at 85% : state=1 (1=TIGHT)
leaving CRITICAL lands in TIGHT, never straight to EASY : ok
occupancy 75% (between exit 70 and enter 80) : correct=1 no-hyst=0
75% stayed TIGHT -- inside the hysteresis band : ok
8 cycles oscillating 81/79 : correct transitions=3 | no-hyst transitions=12 osc flag=1
the no-hysteresis variant transitioned far more often : okTwelve transitions against three, on identical input. Every action a system takes on capacity pressure — reclaiming, migrating a page, refusing an allocation, waking a daemon — is expensive, and a single threshold makes all of them fire on the noise around the boundary.
Hysteresis is not a smoothing filter. It is the statement that entering a state and leaving it are different decisions with different costs. Entering TIGHT at 80% and leaving at 70% means the system commits to a ten-point band of stability before it will reverse a decision it has already paid for.
Leaving CRITICAL lands in TIGHT, never in EASY. A system that drops two states at once discards the intermediate response entirely — it goes from emergency reclaim to doing nothing, and arrives back at the emergency shortly afterwards.
8. Waveform — The Same Occupancy, Two Behaviours
Teaching-model timing derived from the simplified RTL in this chapter. Not CXL wire timing.
Read cycles 4 to 6. Occupancy moves 79 → 81 → 75, which is noise around a threshold, and the two designs disagree completely: hyst_state holds TIGHT throughout while flat_state flips EASY → TIGHT → EASY.
Then read cycle 7. Occupancy hits 96 — a genuine event — and both designs react. Hysteresis did not make the system less responsive to real pressure. It made it unresponsive to noise, which is the entire distinction.
9. RTL 4 — Two Stalls With Opposite Remedies
// Capacity is checked first: if the page has nowhere to live, credit is
// irrelevant. Merging the two loses exactly this ordering.
cap_stall = want_issue && no_capacity;
bw_stall = want_issue && !no_capacity && no_credit;=== EXP4: capacity stalls and bandwidth stalls have opposite fixes ===
20 cycles : capacity stalls=5 bandwidth stalls=5 issued=10
oracle : capacity=5 bandwidth=5
both stall causes matched an independent oracle : ok
exactly 10 cycles actually issued : ok
merged variant : one bucket=10, other=0 (diagnosis unavailable)
loose-bw abuse instance : misattribution flag=1
no request offered, both blockers asserted : capacity stalls=5
no request offered means no stall recorded : ok"The workload is stalled" is not a diagnosis, and this is the counter split that turns it into one.
| Stall cause | What it means | The remedy |
|---|---|---|
| capacity | the page has nowhere to live | more memory |
| bandwidth | the path is saturated | more concurrency |
Those remedies cost different money and land in different budgets. A team that reports one aggregate number will buy whichever the loudest person believes, and the merged variant shows exactly what that costs: 10 in one bucket, 0 in the other, diagnosis unavailable.
The ordering is the subtle part. Capacity is tested first because it dominates: if there is nowhere to put the data, credit availability is irrelevant. The LOOSE_BW abuse instance drops that exclusion and is immediately caught reporting a bandwidth stall while capacity was the real blocker — the same event, attributed to the wrong budget.
And a stall requires a request. With want_issue low and both blockers asserted, nothing is recorded: a system that is not asking for memory is not stalled on memory, however full it is.
10. RTL 5 — Free Memory on the Wrong Socket
This is the model that explains why expansion is an architecture change rather than a purchase.
// Only as much surplus as there is demand to meet is usable; the rest
// is stranded behind a socket boundary.
uf_q <= (surplus_sum < deficit_sum) ? surplus_sum : deficit_sum;
st_q <= (surplus_sum > deficit_sum) ? (surplus_sum - deficit_sum) : 16'd0;=== EXP5: free memory on the wrong socket is stranded ===
4 nodes : unmet demand=30 usable free=30 stranded=30
one node is short by exactly 30 : ok
only 30 of the surplus could ever be used : ok
30 units are stranded behind socket boundaries : ok
pooled view : usable=60 stranded=0 hidden flag=1
the pooled view reported zero stranded and was caught : okFour nodes. One is 30 short. The other three have 60 spare between them. The fleet has ample free memory and one node cannot run.
The pooled view reports zero stranded, because it sums free capacity across the fleet and concludes there is plenty. That number is not wrong arithmetically — it is answering a question nobody asked. Free memory is only useful if the demand can reach it, and a socket boundary is exactly what prevents that.
This is why fleet-average utilisation is such a misleading metric. A fleet at 70% average utilisation can be simultaneously wasting a third of its memory and refusing allocations, and the average will report neither. The two numbers worth reporting are unmet demand and stranded surplus — and their coexistence is the entire business case for expansion and, later, pooling.
11. RTL 6 — The Ceiling Is Felt as a Failure Path
alloc_done = active_q && space_available;
alloc_failed = active_q && !space_available && !RETRY_FOREVER
&& (retry_q >= RETRY_LIMIT[7:0]);=== EXP6: a capacity ceiling is felt as a failure path ===
no space, retry limit 4 : retries=4 failures=1 | retry-forever failures=0 retries=18
the bounded path failed exactly once : ok
after exactly 4 retries : ok
the retry-forever variant never failed : ok
and was caught livelocking : ok
satisfied, then space removed and idle : failures still=1 retries=4Nobody experiences a capacity ceiling as missing bytes. They experience it as an allocation that does not return, a job that will not schedule, or a process killed by something that had no idea why.
The failure has to be bounded and visible. The RETRY_FOREVER variant never fails — it simply retries, forever, and the livelock checker catches it. A system that behaves this way converts a diagnosable capacity problem into an undiagnosable hang, and the hang is reported against whatever was unlucky enough to be waiting.
The release test matters more than it looks. After a successful allocation, space is removed and the model idles: no new failure appears, because the satisfied request released. A design that leaves the request active keeps retrying against the new shortage and fails for a request that was already served.
12. RTL 7 — The Evidence for a Capacity Review
=== EXP7: the evidence an architect brings to a capacity review ===
requests=30 granted=24 refused=6 pressure cycles=17
oracle : requests=30 granted=24 refused=6 pressure=17
every request resolved as exactly one outcome : ok
the conservation law held : ok
abuse: grants with no requests : flag=1Three numbers decide a capacity argument, and none of them is "we feel short of memory":
| Number | What it proves |
|---|---|
| refusals | demand the machine could not accept |
| pressure time | how long it ran near the ceiling |
| residency grants | what it did manage to hold |
The conservation law is what makes them trustworthy. Every request resolves as exactly one of granted or refused, so the two outcomes can never exceed the requests behind them. Without it, a refusal counter can double-count retries and turn a modest shortage into a fabricated crisis — and capacity budgets are approved on these numbers.
13. Quantitative Reasoning
Illustrative, with stated assumptions. None of these figures describes a real product.
The ceiling
capacity = channels x slots_per_channel x module_density
= 8 x 2 x 64 GB
= 1024 GBDensity is the only term with headroom, and it moves in generational steps rather than continuously. Doubling density doubles capacity and changes nothing about the ratio — the socket still offers one capacity for one core count.
What the ratio costs
Take a workload needing 2 TB resident and 32 cores, on sockets offering 1 TB and 64 cores:
capacity-driven: 2 sockets -> 2 TB, 128 cores (96 cores idle)
compute-driven: 1 socket -> 1 TB, 64 cores (workload does not fit)You buy two machines to use half of one. The 96 idle cores are not a rounding error; they are the price of the weld, paid on every node that has this shape.
Stranding
From §10: unmet demand 30, surplus 60, stranded 30.
fleet free memory = 60
fleet unmet demand = 30
free memory that helps = 30
free memory stranded = 30 (50% of all free memory)Half the free memory in that fleet cannot be used by the thing that needs memory. Scale the shape, not the numbers: the stranded fraction grows with how unevenly demand is distributed, which is why heterogeneous fleets strand more than uniform ones.
Working set versus access rate
From §6: 20 accesses over 4 pages.
sized from access count : 20 pages
sized from working set : 4 pages
over-provision factor : 5xThe two estimates disagree by the reuse factor, and reuse varies by orders of magnitude across workloads. That is why capacity planning needs a distinct-page measurement and not a bandwidth counter.
How long the ceiling is felt
From §12: 17 pressure cycles in 30, 6 refusals in 30.
time near the ceiling = 17/30 = 57%
refusal rate = 6/30 = 20%A machine spending 57% of its life under pressure is not occasionally short of memory — it is running in the regime where every capacity-dependent decision is expensive, which is the argument a capacity review actually needs.
14. Assertions
Icarus Verilog 13.0 is the only simulator installed here, and it does not execute concurrent SVA properties — unique/priority case qualities are parsed but ignored, and property blocks are not supported. Every property below is therefore implemented as synthesisable checker logic verified procedurally in simulation, not as executed SVA. 47 assertions.
Safety
| Property | Intent |
|---|---|
| Ceiling honoured | committed capacity never exceeds the physical product |
| Peak accuracy | peak records the value the current edge creates |
| Working-set sanity | distinct pages never exceed pages that exist |
| Window integrity | the seen-set clears on every window roll |
| Pressure legality | CRITICAL exits to TIGHT, never straight to EASY |
| Stall exclusivity | a bandwidth stall is never recorded while capacity blocks |
| Stall requires demand | no request offered means no stall recorded |
| Stranding honesty | usable free never exceeds unmet demand |
| Bounded failure | allocation fails within the retry limit |
| Release | a satisfied allocation deactivates |
| Conservation | granted + refused ≤ requested |
Liveness
| Property | Assumption it needs |
|---|---|
| An allocation request eventually resolves | the retry limit is finite |
| Pressure eventually leaves CRITICAL | occupancy eventually falls |
Performance goals — not correctness
| Goal | Measured by |
|---|---|
| Time under pressure bounded | pressure cycles over total |
| Refusal rate bounded | refusals over requests |
| Stranded fraction low | stranded over total free |
These belong in a report that trends, never in an assertion. A performance goal in an assertion produces a regression that fails on a busy day, and a team that learns to ignore that failure will ignore a real one later.
15. Mutation Testing
Twenty-seven mutations. Twenty-seven killed.
| Mutation | Result |
|---|---|
| Ceiling comparator off by one | killed |
| Slots-per-channel dropped from the capacity product | killed |
| Refused capacity requests not counted | killed |
| Overcommit checker guarded by the fault it detects | killed |
| Peak committed lags the value being created | killed |
| Every access treated as a first touch | killed |
| Touched pages never recorded | killed |
| Window roll does not clear the seen set | killed |
| Impossible distinct count not reported | killed |
| Pressure entry threshold off by one | killed |
| Hysteresis band removed | killed |
| Oscillation not reported | killed |
| Critical drops straight to easy, skipping tight | killed |
| Bandwidth stall counted while capacity blocked | killed |
| Capacity stall counted with no request offered | killed |
| Stall misattribution not reported | killed |
| Stalled cycles counted as issues | killed |
| All surplus assumed usable | killed |
| Stranded capacity always reported as zero | killed |
| Pooled view not flagged for hiding stranding | killed |
| Retry limit off by one | killed |
| Allocation failures not counted | killed |
| Livelock not reported | killed |
| A satisfied allocation never releases the request | killed |
| Offered requests counted as grants | killed |
| Conservation law disabled | killed |
| Pressure time measured as request count | killed |
The first run scored 17 of 27, and the ten escapes sorted exactly as the previous batch predicted they would:
| Cause of escape | Count |
|---|---|
| Stimulus never reached the state | 6 |
| Assertion displayed a value but never checked it | 2 |
| Checker unreachable without an abuse instance | 2 |
Not one escape was a missing checker. Batch 010 closed with the standing lesson that on a mutation escape you should ask what state was never reached and was the assertion exact before adding a check. Applying that rule first, rather than reflexively adding assertions, fixed all ten.
Four are worth recording individually.
A boundary that stimulus never touched. The ceiling comparator <= versus < is invisible unless a request lands exactly on the ceiling. Six 200 GB requests into 1024 GB never do. Adding a final request of exactly 24 GB — taking the total to precisely 1024 — separated them in one cycle.
A value displayed but not checked. peak_committed was printed in the transcript and never asserted, so a lagging implementation shipped through. Printing a number is not checking it, and a transcript line is the easiest place in a whole batch to mistake one for the other.
And the peak needed the creating edge. Even once asserted, the lagged version passed — because committed_q holds the new value on the following cycle, so one idle cycle lets the lag catch up. The fix was to sample the peak on the edge that created it, before any further cycle. This is the third batch in which that exact shape has appeared.
A checker unreachable by construction. The misattribution check can never fire on a correct design, because bw_stall excludes no_capacity by definition. Rather than delete it, a LOOSE_BW abuse instance was added that drops the exclusion, making the checker reachable without driving illegal behaviour into the instance under test.
16. Verification Strategy
| Area | Approach |
|---|---|
| Ceiling | Requests below, exactly on, and beyond the ceiling; an elastic abuse variant |
| Working set | Distinct-page stimulus with heavy reuse; window-roll crossing; a count-access variant |
| Pressure | Exact thresholds; band noise; the CRITICAL path; a no-hysteresis variant |
| Stalls | Both causes together and separately; no-request stimulus; merged and loose abuse instances |
| Stranding | Uneven demand across nodes; a pooled-view variant |
| Failure path | Unsatisfiable then satisfiable allocation; a retry-forever variant |
| Counters | Independent oracle; a conservation abuse instance |
The independent oracle is the load-bearing part. Each counter is checked against a value the testbench computes by a different method than the design — a running total in the testbench against the design's registered accumulator, a bit array of seen pages against the design's seen_q vector. A testbench that reproduced the design's own expression would agree with it while both were wrong.
The coverage cross that matters is occupancy × request outcome × pressure state. Below/at/above the ceiling, crossed with granted/refused, crossed with EASY/TIGHT/CRITICAL. The at-the-ceiling × refused × CRITICAL point is where every boundary bug in this chapter lives, and random stimulus reaches it essentially never.
17. Synthesis and Implementation Reality
| Structure | Implementation consequence |
|---|---|
CEILING_GB product | resolved at elaboration; no multiplier in hardware |
| Capacity comparator | a 16-bit adder plus compare in the grant path |
seen_q bit-vector | one flop per tracked page; a real design needs a hash or CAM |
| Distinct counter | width must cover the page count, or it wraps and lies |
| Pressure FSM | 2 flops and two comparators; trivial area, high leverage |
| Hysteresis thresholds | two constants rather than one — no runtime cost at all |
| Stall counters | one flop-bank per cause; the cost of the diagnosis is linear in causes |
| Stranded ledger | per-node subtract and compare; scales with node count, not capacity |
The most important line in that table is the hysteresis one. Two constants instead of one costs nothing in area, nothing in timing, and it is the difference between a system that reclaims on real pressure and one that thrashes on noise. Cheap structural decisions with large behavioural consequences are the ones worth arguing for in review.
The seen_q row is the honest limitation. One flop per page is fine for a 16-page teaching model and impossible for a real address space; production working-set estimation uses sampling, hashing or aging counters. The invariant taught — distinct, not total — survives the change of mechanism.
18. Silicon Observability
| Counter | Diagnoses |
|---|---|
| committed vs installed | how close to the ceiling the machine runs |
| peak committed | the worst moment, not the average |
| allocation refusals | demand the machine could not accept |
| retries before failure | whether the failure path is bounded |
| time in each pressure state | how long it runs hot |
| pressure transitions | thrash, if it is high with low residency |
| distinct pages per window | the actual working set |
| capacity stalls vs bandwidth stalls | which budget to spend |
| stranded surplus | capacity that exists and cannot be reached |
overcommit, livelock, miscount | correctness alarms — must be zero forever |
The pair to fight for is capacity stalls against bandwidth stalls. They are two counters and one comparator, and they decide whether the next purchase is memory or interconnect. Everything else on this list refines an answer that pair gives at the top level.
Pressure transitions with low residency is the thrash signature — many transitions, little time actually spent in the state. It is the counter that would have caught the no-hysteresis design in the field, and it costs one register.
19. Debug Lab
A capacity estimate that was five times too large
ACCESS-RATE-SIZINGA service is provisioned from a memory-bandwidth trace and the machines arrive with five times the memory the workload ever uses. The capacity is real, the utilisation is 20%, and the finance review asks why.
20 accesses over 4 pages : distinct=4 accesses=20
the count-access variant reported 20 -- 5x the true working set : okCompare distinct pages touched in a window against total accesses in the same window. If the ratio is large, the workload has high reuse and an access-rate estimate has over-provisioned by exactly that factor.
Sizing from a bandwidth counter; a window long enough that the lifetime footprint is reported as the working set; a distinct counter that never resets.
Instrument distinct-page count per window. Sweep the window length and watch where the number stabilises — that plateau is the working set. Then compare against what was purchased.
Access rate and residency are different quantities. Twenty accesses to four pages need four pages resident, not twenty. The same error under-provisions a low-reuse workload just as badly.
assign first_touch = acc_valid && !seen_q[page_id];
assign advance = first_touch; // not acc_valid
if (cnt_q > PAGES[7:0]) miscount_err <= 1'b1; // impossible-value guardAssert the impossible value. A distinct count above the number of pages that exist is a broken metric, and one comparator catches it before anyone provisions from it.
The reclaim daemon runs constantly and reclaims nothing
NO-HYSTERESISCPU time in a memory-management thread is high and steady. Occupancy hovers near a threshold. Almost no memory is actually reclaimed, and latency is worse than when the machine was fuller.
8 cycles oscillating 81/79 : correct transitions=3 | no-hyst transitions=12 osc flag=1
and was caught oscillating : okCount state transitions and time-in-state together. Many transitions with little residency is thrash; the state is being entered and abandoned before its action completes.
One threshold used for both entering and leaving; a threshold placed where the workload's steady state sits; a sampling period shorter than the action's completion time.
Plot occupancy against the threshold. If it straddles it, the design has no hysteresis band. Confirm by checking whether any state is left on the cycle after it is entered.
Entering and leaving a state were the same decision. Every crossing of the boundary triggered an expensive action that the next sample immediately reversed.
localparam logic [7:0] HI_ENTER = 8'd80;
localparam logic [7:0] HI_EXIT = 8'd70; // a band, not a line
if (st_n != st_q && since_q == 8'd0) oscillation_err <= 1'b1;Assert that no state is left on the cycle after entry. Two constants cost nothing and the flag makes the failure visible from the field.
The team bought memory and the stall did not move
MERGED-STALL-COUNTERA workload shows heavy stalling. Memory is doubled. Throughput improves by a few percent, and the stall counter reads almost exactly what it did before.
20 cycles : capacity stalls=5 bandwidth stalls=5 issued=10
merged variant : one bucket=10, other=0 (diagnosis unavailable)Split stall cycles by cause. If capacity stalls are near zero, the shortage was never capacity — the path was saturated, and the remedy was concurrency.
One aggregate stall counter; a bandwidth stall recorded while capacity was the real blocker; stalls counted with no request offered.
Add the two counters. Re-run. Compare their ratio before and after any purchase — a remedy that does not move its own counter did not address the cause.
The two causes were merged, so the diagnosis was unavailable and the purchase was made on intuition.
cap_stall = want_issue && no_capacity;
bw_stall = want_issue && !no_capacity && no_credit; // exclusion mattersNever attribute a stall to a mechanism without a counter that names it. Ordering the causes matters as much as separating them.
A fleet at 70% utilisation that cannot schedule a job
STRANDED-CAPACITYFleet dashboards show ample free memory. A specific job repeatedly fails to schedule. Capacity planning insists there is no shortage, and both parties have correct data.
4 nodes : unmet demand=30 usable free=30 stranded=30
pooled view : usable=60 stranded=0 hidden flag=1Report unmet demand and stranded surplus per node, not fleet totals. Their simultaneous presence is the whole finding: memory exists, and the demand cannot reach it.
Fleet-average utilisation used as the capacity metric; free memory summed across sockets that cannot serve each other; heterogeneous demand across identical nodes.
For each node compute surplus and deficit separately. Sum them separately. If both are non-zero, the fleet is stranding capacity regardless of what the average says.
Capacity is welded to a socket, so surplus behind one boundary cannot meet demand behind another. An average erases the boundary that causes the problem.
usable = min(surplus_sum, deficit_sum);
stranded = (surplus_sum > deficit_sum) ? surplus_sum - deficit_sum : 0;Never report a fleet average without also reporting the spread. An average is only a capacity metric when demand is uniform, which it never is.
An allocation that never returns
RETRY-LIVELOCKA process hangs. No error is logged. No allocation failure is recorded. The machine is at its capacity ceiling, and the only symptom is something that will not make progress.
no space, retry limit 4 : retries=4 failures=1 | retry-forever failures=0 retries=18
and was caught livelocking : okCompare retry counts against the declared limit. Retries climbing without a failure ever being recorded is a livelock, not slowness.
An unbounded retry loop; a failure path that exists but is unreachable; a request that is never released after being satisfied.
Instrument retries and failures separately. If failures stay at zero while retries grow without bound, the failure path is not reachable.
The ceiling was real, and the design responded by retrying forever — converting a diagnosable capacity shortage into an undiagnosable hang attributed to whatever was waiting.
alloc_failed = active_q && !space_available && (retry_q >= RETRY_LIMIT);
if (retry_q > RETRY_LIMIT + 8'd8) livelock_err <= 1'b1;Every retry loop needs a bound and a counter. An unbounded retry is a hang with better manners.
Capacity overcommitted in a planning model that hardware refuses
ELASTIC-CAPACITYA deployment plan allocates more resident memory than the machines physically contain. It passes every planning check. It fails on the first node it touches, and the failure looks like a scheduling bug.
elastic variant : committed=1224 overcommit flag=1
the elastic variant committed beyond physical memory : okCompare committed capacity against the physical product of channels, slots and density. Any excess is a plan that hardware cannot honour.
A capacity model with no ceiling; density assumed at a value not actually populated; a checker guarded by the very condition that would trip it.
Derive installed capacity from the populated configuration rather than from a constant. Then assert committed never exceeds it, on every configuration including the unlimited one.
The model treated capacity as elastic. Hardware does not, and the request fails rather than costing more.
// Unguarded on purpose: guarding with !ELASTIC would switch the checker off
// on the only configuration that can ever trip it.
if (committed_q > CEILING_GB) overcommit_err <= 1'b1;For every checker ask which configuration makes it fire. If the answer is "none we run", it is decoration.
A working-set number that only ever grows
WINDOW-NEVER-CLEARSA working-set estimate rises steadily through a run and never falls, eventually reporting the entire address space the process has ever touched. Provisioning from it is absurd, so the metric is abandoned.
after a window roll, same 4 pages re-touched : distinct=4
the window rolled and the seen-set cleared : okCross a window boundary and re-touch pages already seen. They must count as first touches again. If the number does not reset, the estimator is reporting a lifetime footprint.
The seen-set not cleared on window roll; the window counter never reaching its limit; a window chosen longer than the run.
Force a roll and re-touch a known page set. Compare the reported distinct count before and after.
Working set is distinct pages in a window. Without the reset, the window is infinite and the metric measures something else entirely.
if (window_roll) begin
seen_q <= '0; // both must clear
cnt_q <= '0;
endTest the roll explicitly. A short run never reaches the boundary, so the defect ships.
Refusal counts that justified a purchase nobody needed
COUNTER-CONSERVATIONA capacity review is presented with a refusal rate far above what the workload's behaviour suggests. Memory is purchased. The refusal rate barely moves.
requests=30 granted=24 refused=6
every request resolved as exactly one outcome : ok
abuse: grants with no requests : flag=1Check that granted plus refused never exceeds requested. If it does, retries are being counted as fresh demand and the shortage is overstated.
Retries counted as new requests; offered requests counted as grants; outcomes recorded with no request behind them.
Add the conservation check and re-run the same workload. The corrected refusal rate is the one the budget should see.
The evidence had no conservation law, so a derived metric drifted in the direction that flattered the argument.
if ((granted + refused) > requested) conservation_err <= 1'b1;Give every derived metric a conservation law. Capacity budgets are approved on these numbers.
20. Design Review
- What are the three multiplicands of this platform's capacity, and which can still change?
- Is the working set measured as distinct pages in a window, or inferred from access rate?
- What window length was used, and where does the distinct count plateau?
- Does pressure have a hysteresis band, and how wide?
- Can any pressure state be left on the cycle after it is entered?
- Are capacity stalls and bandwidth stalls counted separately, and in that order?
- What happens to an allocation that cannot be satisfied — and how many retries first?
- Does a satisfied allocation release its request?
- What is the stranded surplus across the fleet, not the average utilisation?
- Do granted and refused conserve against requested?
- Which of these counters exist in silicon rather than only in a model?
21. How This Appears in Real Engineering
Architect. Owns the ratio argument: capacity per core, and what the workload actually needs resident. Needs distinct-page data from software and the capacity product from the board team, and produces the refusal and stranding evidence that justifies expansion.
RTL designer. Owns the counters that make the argument checkable — bounded, conserved, and cheap. The pressure FSM and the stall split are a few dozen flops that decide a purchasing conversation.
DV engineer. Owns the boundary cases: exactly at the ceiling, exactly at a threshold, window rolls, and the no-request case. Every one of those is where this chapter's mutations survived until directed stimulus reached them.
Firmware engineer. Owns what the platform reports as installed and available, and must derive it from the populated configuration rather than a constant — the elastic-capacity defect starts here.
OS/runtime engineer. Owns the allocation failure path and the reclaim policy that pressure state drives. Needs the band, not the line, or the reclaim thread burns CPU without reclaiming.
Performance engineer. Owns the distinction between the two stalls and refuses to let one aggregate number stand in for the diagnosis.
Silicon validation. Owns whether the counters are trustworthy under load — conservation laws hold, peaks record the creating edge, and no counter has quietly stopped meaning its name.
What each needs from the others: the architect needs the software team's working set, the RTL team's counters and the firmware team's honest capacity report. None of the three is useful alone, and the capacity argument fails whenever one is missing.
22. Common Misconceptions
| Belief | Correction |
|---|---|
| Memory expansion means "more RAM" | It means unwelding capacity from the socket |
| Capacity can be increased by spending more | Channels and slots are fixed once the board exists |
| Access rate indicates how much memory is needed | Residency does; they differ by the reuse factor |
| A fleet at 70% utilisation has spare memory | It may be stranding half its free memory |
| One threshold is enough for pressure | It makes every reclaim decision fire on noise |
| "The workload is stalled" is a diagnosis | Capacity and bandwidth stalls have opposite fixes |
| An unbounded retry is safer than failing | It converts a diagnosable shortage into a hang |
| A displayed counter is a verified counter | Printing a value is not checking it |
23. Interview Reasoning
24. Exercises
-
Calculation. A platform offers 12 channels, 1 slot per channel and 96 GB modules. Compute installed capacity. A workload needs 1.5 TB resident and 40 cores; the socket provides 64 cores. Compute how many sockets a capacity-driven purchase requires, how many cores are then idle, and what the idle fraction would be if module density doubled.
-
Analysis. A fleet reports 68% average memory utilisation and a 12% allocation-failure rate. Explain how both can be true simultaneously, name the two counters that resolve it, and state what values you would expect from each if the cause is stranding rather than genuine shortage.
-
RTL task. Extend
working_set_probeto maintain two windows of different lengths simultaneously and report both distinct counts. State the storage cost, and explain what the divergence between the two numbers tells you about the workload that either number alone does not. -
Assertion task. Write the property that proves a pressure state is never left on the cycle after it is entered. Then explain why this property, not a threshold check, is the one that detects a missing hysteresis band — and construct a design that satisfies a threshold check while thrashing.
-
Testbench design. Design the stimulus that distinguishes a
<=ceiling comparator from a<one. Explain why a sequence of equal-sized requests will usually fail to distinguish them, and state the general rule this implies for testing any bound. -
Debug task. A machine shows high stall time, and doubling its memory changes nothing. Give your investigation order across this chapter's counters, the single ratio that would have predicted the outcome before the purchase, and the remedy the evidence actually supports.
-
Design review. A colleague proposes reporting a single "memory pressure" percentage to the OS instead of a state with hysteresis, arguing it is simpler and gives software more information. Give the strongest version of that argument, then state what breaks, which measured result demonstrates it, and what you would propose instead.
-
Coverage design. Define the coverage cross for this chapter's capacity model across occupancy, request outcome and pressure state. Identify which point is reachable only by directed stimulus, and explain what defect class lives there.
25. Summary
Capacity is welded to the socket.
- Installed capacity is a product — channels × slots × density — fixed when the board was designed. Only density has headroom, and it moves in steps.
- The ratio is the real problem: memory and compute are bought together, and no workload respects the ratio the socket offers.
- Working set is distinct pages in a window, not access rate. The measured run showed 20 accesses over 4 pages — a 5× over-provision if sized from the wrong number.
- Pressure is a state with hysteresis, not a comparison. Identical occupancy produced 3 transitions with a band and 12 without.
- Capacity stalls and bandwidth stalls have opposite remedies. One aggregate counter makes the diagnosis unavailable and sends the money to the wrong budget.
- Free memory on the wrong socket is stranded. 30 short on one node, 60 spare across three, and half of all free memory unusable — invisible to any fleet average.
- A ceiling is felt as a failure path. It must be bounded and visible; retrying forever converts a diagnosable shortage into a hang.
- The evidence for a capacity review is refusals, pressure time and residency, each protected by a conservation law.
- Verification: 27 of 27 mutations killed, 47 assertions. All ten first-run escapes were stimulus or assertion-precision problems, not missing checkers — six states never reached, two values printed but never asserted, two checkers needing abuse instances.
Next: 11.2 — Capacity Scaling, which asks what actually changes when capacity arrives over a link rather than over channels — beginning with the address map, because memory the host cannot address is not capacity at all.
Continue learning
Related tutorials
- Related topic
Memory Expansion for AI
Parameters are one term of four. This chapter builds the true working set, the tier blend, capacity against bandwidth, hot/cold placement, expansion value, batch sizing, page migration, expansion economics, the scale-out alternative and the assembled model.
- Related topic
Why CXL Matters
Why PCIe transaction semantics are insufficient for coherent memory and accelerator attach — CXL.io, CXL.cache and CXL.mem as the CXL Consortium defines them, device types, why coherence needs distributed per-line state, memory-window routing, what coherence costs, and why a clean transport proves nothing about coherence.
- Related topic
Memory Expansion Over CXL
How memory on another chiplet becomes host-visible memory — HDM versus private device memory, host physical address decode and window ownership, one-hot route validation, interleaving as a deterministic address function, outstanding-request lifetime across a UCIe recovery, and the semantic memory model a transport scoreboard cannot replace.
- Related topic
GPU Memory Bottlenecks
Why a device with enormous local bandwidth still stalls: capacity and bandwidth are independent failures with different symptoms and different fixes. Working-set arithmetic, oversubscription cost, and a simulated device memory manager that measures the difference.
Standards & specifications
- Governing standard
- CXL Specification (CXL Consortium)(opens CXL Consortium in a new tab)
Defines CXL.io, CXL.cache and CXL.mem, and the coherence and memory-pooling behaviour built on them. System design and deployment topology are not mandated.
This page also covers RTL structure, verification approach and debugging technique. Those are engineering practice built on the standard, not requirements the standard itself imposes.
Where this fits
Part of the CXL curriculum.
