CXL · Module 6
Protocol Use-Case Taxonomy
Which CXL protocol combination solves which problem, derived from five requirement questions rather than from a device category: why local memory does not imply CXL.mem, why reuse and not device class decides CXL.cache, and how to measure whether the choice was right. Five RTL models simulated, eleven mutations, eleven killed.
Chapter 6.1 established what each protocol obliges you to build. Chapter 6.2 established what it costs to run them together.
This chapter answers the question those two make askable: given a device to build, which of the three does it actually need?
1. The Engineering Problem — The Category Is Not the Requirement
A specification arrives: "a CXL accelerator with 64 GB of local memory." Three protocol decisions look settled by that sentence. None of them are.
- It is an accelerator, so it caches host memory — only if the workload reuses what it fetches. Streaming data through once needs bandwidth, not coherence.
- It has local memory, so it exposes it over
.mem— only if the host needs to reach it. Private working memory the host never touches needs no CXL protocol at all. - It is CXL, so it needs
.io— yes. This is the one the category actually decides, and only because.iois mandatory for every device.
Chapter 3.2 went device type → engines. That is the right direction for understanding a device someone hands you. It is the wrong direction for designing one, because it starts from a label. This chapter runs the arrow backwards: requirements → protocol set → type, with the type as an output nobody chooses.
2. The One-Sentence Model
Ask five questions about the workload, and the protocol set falls out —
.ioalways;.cacheonly if the device must reach host memory and hold it coherently;.memonly if local memory must be host-visible. The device type is whatever those answers imply, and a protocol with no requirement behind it is cost with no owner.
Call it requirement-driven, not category-driven. RTL 1 is that sentence in logic, and its broken parameterisation is the category-driven alternative.
3. What This Chapter Owns
| Question | Owned by |
|---|---|
| Device types and their engines | 3.2 |
| Direction — who may ask | 4.4 |
| State obligation per protocol | 6.1 |
| Coexistence, arbitration, starvation | 6.2 |
| Requirement → protocol set | this chapter |
| The decision method and its costs | 6.4 |
The boundary with 6.4. This chapter derives what a workload needs. The next one is about how an architect runs that decision — the cost accounting, the over- and under-provision reviews, and the distinction between capability and enablement. Here the question is "which set?"; there it is "how do you defend the answer?".
4. The Wrong Way: Deriving Protocols From a Category
Before the framework, the failure it replaces. RTL 1 carries both, so the difference is measurable rather than asserted.
device class | required type | category-driven
managed device, no memory | 001 io-only | 001
caching accelerator | 011 Type 1 | 011
accelerator + visible memory | 111 Type 2 | 111
memory expander | 101 Type 3 | 101
private local mem, no coherence| 001 io-only | 111 <-- category says 111The first four rows agree, which is exactly why category-driven selection survives so long: it is right whenever the category was chosen because of the requirements. The fifth row is the case that separates them — a device with local memory that the host never touches and host-memory access that needs no coherence. The requirement-driven answer is .io alone. The category-driven answer is all three.
That is two engines, two sets of state obligations on both ends, and two more classes contending in 6.2's arbiter, none of which any workload asked for.
5. The Five Questions
Everything in this chapter reduces to five questions about the workload, not the product.
| # | Question | If yes |
|---|---|---|
| 1 | Must the device be discovered and configured? | .io — and it is required anyway |
| 2 | Must the device reach host memory? | necessary for .cache, not sufficient |
| 3 | ...and hold it coherently, with reuse? | .cache |
| 4 | Does the device have local memory? | necessary for .mem, not sufficient |
| 5 | ...must the host be able to reach it? | .mem |
The structure is two conjunctions, and both halves matter:
.cache = needs host memory AND needs coherence
.mem = has local memory AND host must see itQuestion 2 without question 3 is a device that reads host memory once and streams it — that needs bandwidth, and a bulk transfer over .io may serve it better than a coherent engine. Question 4 without question 5 is private working memory: HBM on an accelerator that the host never addresses is not a .mem use case, it is just memory on a chip.
Mutations M1 and M2 delete one half of each conjunction, and both produce plausible, wrong device architectures.
Both optional protocols hang off a conjunction, and both conjunctions have a first half that is necessary and not sufficient. Every arrow points downward: nothing in this diagram lets the device type influence the mask.
6. The Decision Table
| Need | Protocols | Type |
|---|---|---|
| configuration only | .io | none |
| device caches host mem | .io .cache | 1 |
| host reaches device mem | .io .mem | 3 |
| both of the above | all three | 2 |
| local mem, host-private | .io | none |
| host mem, streamed once | .io | none |
The last two rows are the ones a category-driven process gets wrong, and they are not exotic. A device can have gigabytes of local memory and still be an .io-only device, if the host never addresses that memory.
Note also that "none" appears three times. A device with no CXL device type is not a failure — it is a PCIe device on a CXL link, and 6.1 showed the three types enumerate only the coherent combinations.
7. Teaching-model boundary
8. RTL 1 — Requirements In, Protocol Mask Out
module requirement_vector #(
parameter bit CATEGORY_DRIVEN = 1'b0 // 1 = choose from the device category
) (
input logic clk, rst_n,
input logic need_control, // must be discovered/configured
input logic need_host_mem, // device must reach HOST memory
input logic need_coherent, // ...and hold it coherently
input logic has_local_mem, // device has attached memory
input logic need_host_visible, // ...that the HOST must reach
input logic [1:0] marketing_category,
input logic valid,
output logic [2:0] required_mask, // {mem, cache, io}
output logic [1:0] implied_type,
output logic io_omitted_err, cache_without_need_err,
output logic mem_without_visibility_err
);
logic need_io_c, need_cache_c, need_mem_c;
logic [2:0] derived, category_mask;
// .io is required by ANY device: discovery and configuration are not optional.
assign need_io_c = 1'b1;
// .cache is required only if the device must COHERENTLY reach host memory.
// Reaching host memory without needing coherence is not a .cache requirement.
assign need_cache_c = need_host_mem && need_coherent;
// .mem is required only if local memory must be HOST-VISIBLE.
// Local memory the host never touches needs no CXL protocol at all.
assign need_mem_c = has_local_mem && need_host_visible;
assign derived = {need_mem_c, need_cache_c, need_io_c};
// The bug shape: pick the mask from the product category.
always_comb begin
case (marketing_category)
2'd1: category_mask = 3'b011; // "accelerator" -> io+cache
2'd2: category_mask = 3'b111; // "smart device" -> everything
2'd3: category_mask = 3'b101; // "memory" -> io+mem
default: category_mask = 3'b001;
endcase
end
assign required_mask = valid ? (CATEGORY_DRIVEN ? category_mask : derived) : 3'b000;
always_comb begin
if (required_mask[1] && required_mask[2]) implied_type = 2'd2;
else if (required_mask[1]) implied_type = 2'd1;
else if (required_mask[2]) implied_type = 2'd3;
else implied_type = 2'd0;
end
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
io_omitted_err <= 1'b0; cache_without_need_err <= 1'b0;
mem_without_visibility_err <= 1'b0;
end else if (valid) begin
if (!required_mask[0]) io_omitted_err <= 1'b1;
// A protocol in the mask with no requirement behind it.
if (required_mask[1] && !need_cache_c) cache_without_need_err <= 1'b1;
if (required_mask[2] && !need_mem_c) mem_without_visibility_err <= 1'b1;
end
end
endmodulePurpose. To make protocol selection a derivation that can be checked, rather than a judgement recorded in a slide.
Invariants. Three, and each is a distinct review question. .io is present unconditionally; nothing appears in the mask without its requirement; and the type is an output.
Synthesis. Trivial — a handful of gates. This module is not really hardware; it is a specification made executable, and its value is that cache_without_need_err fires in simulation when someone widens the mask without widening the requirement.
hostmem coh localmem visible | mask type
0 0 0 0 | 001 io-only
0 0 1 1 | 101 Type 3
1 1 0 0 | 011 Type 1
1 1 1 1 | 111 Type 2
.io is in all 16 masks; .cache needs host-mem AND coherence;
.mem needs local memory AND host visibility9. RTL 2 — Under-Provision and Over-Provision Are Different Answers
module requirement_vs_capability (
input logic clk, rst_n,
input logic [2:0] required_mask, device_mask,
input logic valid,
output logic [2:0] unmet, // needed and absent
output logic [2:0] spare, // present and not needed
output logic satisfied, over_provisioned, usable,
output logic unmet_but_usable_err, spare_counted_as_unmet_err
);
assign unmet = valid ? (required_mask & ~device_mask) : 3'b000;
assign spare = valid ? (device_mask & ~required_mask) : 3'b000;
assign satisfied = valid && (unmet == 3'b000);
assign over_provisioned = valid && (spare != 3'b000);
assign usable = satisfied;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) begin
unmet_but_usable_err <= 1'b0; spare_counted_as_unmet_err <= 1'b0;
end else if (valid) begin
if (usable && (unmet != 3'b000)) unmet_but_usable_err <= 1'b1;
// Spare capability must never be reported as a gap.
if ((unmet & device_mask) != 3'b000) spare_counted_as_unmet_err <= 1'b1;
end
end
endmodule need=111 dev=111 : unmet=000 spare=000 satisfied=1 over=0
need=111 dev=011 : unmet=100 spare=000 satisfied=0 over=0 <-- gap
need=101 dev=111 : unmet=000 spare=010 satisfied=1 over=1 <-- spareunmet and spare are computed separately and mean opposite things. Unmet is a functional gap — the device cannot do the job. Spare is a cost with no owner — the device does the job and carries an engine nobody asked for, with all the state, timing and verification burden 6.1 enumerated.
Mutation M4 replaces the AND-NOT with XOR, which merges them: a spare protocol starts being reported as a missing one. The device is then rejected for having more capability than required, which is a spectacular way to fail a deployment check.
10. RTL 3 — Where the Crossover Actually Is
The question "does this device need coherent access or a bulk copy?" has a numeric answer, and it depends on reuse rather than on what the device is called.
module access_cost_model #(
parameter int unsigned SETUP_COPY = 200, // teaching units
parameter int unsigned LAT_LOCAL = 1,
parameter int unsigned LAT_REMOTE = 8,
parameter bit IGNORE_SETUP = 1'b0 // 1 = forget the copy's setup
) (
input logic clk, rst_n,
input logic [15:0] n_access,
input logic valid,
output logic [31:0] cost_copy, cost_cache,
output logic [1:0] best,
output logic copy_wins, cost_order_err
);
logic [31:0] c_copy, c_cache;
// copy: pay setup once, then every access is local
assign c_copy = (IGNORE_SETUP ? 32'd0 : SETUP_COPY) + ({16'd0, n_access} * LAT_LOCAL);
// cache: no setup, every access pays the remote cost
assign c_cache = {16'd0, n_access} * LAT_REMOTE;
assign cost_copy = valid ? c_copy : 32'd0;
assign cost_cache = valid ? c_cache : 32'd0;
assign copy_wins = valid && (c_copy < c_cache);
assign best = copy_wins ? 2'd0 : 2'd1;
always_ff @(posedge clk or negedge rst_n) begin
if (!rst_n) cost_order_err <= 1'b0;
else if (valid) begin
// Stated about the COST, not about which side wins: with the setup
// dropped both costs are zero at n=0 and a strict "copy_wins" comparison
// is never true, so the check would be unreachable.
if ((n_access == 16'd0) && (cost_copy == 32'd0)) cost_order_err <= 1'b1;
end
end
endmodule accesses | copy cost cache cost cheaper
1 | 201 8 coherent
20 | 220 160 coherent
28 | 228 224 coherent
29 | 229 232 copy
100 | 300 800 copy
500 | 700 4000 copy
crossover: below ~29 accesses coherent access wins; above it, copy winsThe crossover is where SETUP + n·LAT_LOCAL = n·LAT_REMOTE, that is n = SETUP / (LAT_REMOTE − LAT_LOCAL) = 200/7 ≈ 28.6, so 29 is the first access count at which the copy wins. Both sides of that line are real device architectures, and the same silicon can be on the wrong side of it for a different workload.
The direction is worth stating carefully because it inverts a common intuition:
- Low reuse — a few accesses per fetched line — favours coherent access. Paying a bulk-transfer setup to touch data twice is waste.
- High reuse — many accesses per line — favours copying it local, because the setup amortises and every subsequent access is cheap.
So "the accelerator does a lot of work on the data, therefore it needs .cache" is backwards. Heavy reuse is the case that argues for moving the data; light, scattered, unpredictable access is the case that argues for coherence. Which is exactly why .cache is defined as caching host memory: it earns its area when the device cannot predict what it will need.
11. RTL 4 — Did the Workload Match the Assumption?
The taxonomy is a prediction made before silicon. This is how you find out whether it was right.
module workload_profile #(
parameter bit MISCOUNT = 1'b0 // 1 = drop device-memory events
) (
input logic clk, rst_n, ev_valid,
input logic [1:0] ev_kind, // 0=control 1=host-mem 2=device-mem
input logic ev_reused,
output logic [15:0] n_control_q, n_host_mem_q, n_dev_mem_q, n_reuse_q, n_total_q,
output logic cache_justified, mem_justified, accounting_err
);
/* ... counters, with n_total == control + host_mem + dev_mem ... */
// Coherent caching of host memory earns its area only if the device both
// touches host memory and REUSES it. Streaming once does not need a cache.
assign cache_justified = (n_host_mem_q > 16'd0) && (n_reuse_q > 16'd0);
assign mem_justified = (n_dev_mem_q > 16'd0);
endmodule streaming workload : control=30 host-mem=30 dev-mem=30 reuse=0
cache_justified=0 mem_justified=1
after 30 reusing accesses : reuse=30 cache_justified=1A device can touch host memory constantly and still not justify a cache. Thirty host-memory accesses with zero reuse leaves cache_justified low — because the measurement that matters is not whether host memory is touched but whether it is touched again.
Mutation M6 drops the reuse term, so any host-memory traffic justifies the engine. That is precisely the argument that gets a .cache engine into a design that streams, and the counter exists to refute it with data.
12. RTL 5 — Refuse the Use Case You Cannot Serve
assign ok = deploy && ((required_mask & ~device_mask) == 3'b000);
assign admit = deploy && (ATTEMPT_ANYWAY ? 1'b1 : ok);
assign reject = deploy && !admit; need=111 dev=101 : admit=0 reject=1 | attempt-anyway admit=1
need=111 dev=111 : admit=1 reject=0A Type 3 device offered a Type 2 use case must be refused at admission, not discovered at first coherent operation. Mutation M11 replaces the subset test with an overlap test — (required & device) != 0 — which admits any device sharing a single protocol with the requirement. Since .io is in every mask, that admits everything.
13. Assertions
Concurrent SVA execution: NOT SUPPORTED BY Icarus Verilog. Not executed; each maps to a procedural stand-in and a mutation.
// SAFETY -------------------------------------------------------------------
// V1 — .io is in every required mask, unconditionally.
a_io_always: assert property (@(posedge clk) disable iff (!rst_n)
valid |-> required_mask[0]);
// V2 — .cache appears only with BOTH halves of its conjunction.
a_cache_needs_both: assert property (@(posedge clk) disable iff (!rst_n)
required_mask[1] |-> (need_host_mem && need_coherent));
// V3 — .mem appears only with BOTH halves of its conjunction.
a_mem_needs_both: assert property (@(posedge clk) disable iff (!rst_n)
required_mask[2] |-> (has_local_mem && need_host_visible));
// V4 — INDEPENDENCE: local memory alone never implies .mem.
// The negative form is what catches an over-coupled derivation.
a_local_mem_insufficient: assert property (@(posedge clk) disable iff (!rst_n)
(has_local_mem && !need_host_visible) |-> !required_mask[2]);
// V5 — the implied type matches the mask.
a_type_follows_mask: assert property (@(posedge clk) disable iff (!rst_n)
(required_mask[1] && required_mask[2]) |-> (implied_type == 2));
// V6 — unmet and spare are disjoint by construction.
a_unmet_spare_disjoint: assert property (@(posedge clk) disable iff (!rst_n)
valid |-> ((unmet & spare) == '0));
// V7 — spare capability is never reported as a gap.
a_spare_not_gap: assert property (@(posedge clk) disable iff (!rst_n)
valid |-> ((unmet & device_mask) == '0));
// V8 — a device is usable only if nothing is unmet.
a_usable_iff_satisfied: assert property (@(posedge clk) disable iff (!rst_n)
usable |-> (unmet == '0));
// V9 — a copy is never free.
a_copy_costs: assert property (@(posedge clk) disable iff (!rst_n)
valid |-> (cost_copy > 0));
// V10 — CONSERVATION: workload events partition by kind.
a_workload_conserved: assert property (@(posedge clk) disable iff (!rst_n)
n_total_q == n_control_q + n_host_mem_q + n_dev_mem_q);
// V11 — a use case whose needs exceed the device is refused.
a_admission: assert property (@(posedge clk) disable iff (!rst_n)
admit |-> ((required_mask & ~device_mask) == '0));V4 is the one worth copying into other work. V2 and V3 say what a protocol requires; V4 says what a signal is not sufficient for. Chapter 6.1 needed the same shape for a different reason — verification plans are full of "X requires Y" and almost empty of "Y alone does not give X", and that is where over-coupled derivations survive.
14. Mutation Testing
Eleven mutations. Clean code restored after each.
| ID | Mutation | Result |
|---|---|---|
| M1 | host-memory access alone implies .cache | KILLED — 16-case sweep |
| M2 | local memory alone implies .mem | KILLED — 16-case sweep |
| M3 | .io required only if control requested | KILLED — added stimulus |
| M4 | spare capability counted as a gap | KILLED — spare_counted_as_unmet_err |
| M5 | every device usable for every requirement | KILLED — unmet_but_usable_err |
| M6 | touching host memory justifies a cache | KILLED — reuse check |
| M7 | workload conservation disabled | KILLED — miscounting variant |
| M8 | the copy's setup cost dropped | KILLED — cost_order_err |
| M9 | copy always wins regardless of reuse | KILLED — zero-access check |
| M10 | an unsupported use case admitted | KILLED — admission check |
| M11 | any protocol overlap counts as support | KILLED — admission check |
11/11 killed, 0 escapedTwo escaped on the first run.
M3 was a missing stimulus of an unusual shape — an input that was wired in and never varied. need_control was held high for the whole run, so making .io conditional on it changed nothing. §8's callout draws the general rule: an input present in the port list and constant in the testbench is not verified.
M7 was the unreachable-checker category. accounting_err can only fire if a counter is already wrong, so disabling it changes nothing on a correct design. Notably, adding an independent reference for every workload count did not kill it — the reference and the DUT both agreed, because the counts were right. Only a deliberately miscounting variant, whose accounting_err is required to fire, exercises the check.
That is worth separating clearly, because the two techniques get conflated:
An independent reference proves the counts are right. A positive test proves the checker works. They catch different bugs, and neither substitutes for the other.
15. Debug Lab
A memory expander ships without CXL.mem
REQUIREMENT-NOT-DERIVED// It's a memory product; the category says io + mem... but the mask was
// hand-written from the block diagram.
assign required_mask = 3'b001; // io onlyA memory expander enumerates, reports its capacity in a vendor register, and the host cannot address a byte of it as system memory. The memory controller works; the DRAM trains; nothing errors.
need=101 dev=001 : unmet=100 spare=000 satisfied=0
a device without .mem cannot serve a host-visible-memory use caseThe requirement — the host must address this memory — was never written down as a protocol requirement, so nothing derived .mem from it. The device has memory and a way to describe it, and no protocol by which the host can use it.
This is the under-provision half of §9: unmet is non-empty, and the device is simply not usable for the job it was built for.
assign need_mem_c = has_local_mem && need_host_visible; // derive it
assign required_mask = {need_mem_c, need_cache_c, 1'b1};
// and check the device against it before deployment:
assign unmet = required_mask & ~device_mask;Write the requirement as a requirement, then derive. A protocol mask transcribed from a block diagram records what someone drew, not what the workload needs, and the two diverge silently. The check that catches it is cheap and mechanical: derive the mask from the requirement vector and compare it against what the device implements.
An accelerator carries a coherent cache its workload never uses
CACHE-WITHOUT-REUSE// It's an accelerator, so it needs .cache.
assign need_cache_c = need_host_mem;Not a functional failure. Silicon ships, works, and the coherent engine shows near-zero traffic in production. The area, the timing pressure on the sub-200 ns path, and the verification effort all happened.
streaming workload : host-mem=30 reuse=0
cache_justified=0The conjunction lost a half. Reaching host memory is necessary for .cache and not sufficient — a device that streams host memory through once gains nothing from holding it coherently, because it never looks at the line again.
§10's cost model gives the shape of the mistake: coherent access wins at low reuse, and a workload with heavy reuse is better served by moving the data. "The accelerator does a lot of work on the data" is an argument for copying, not for caching.
assign need_cache_c = need_host_mem && need_coherent;
// and measure it after silicon:
assign cache_justified = (n_host_mem_q > 0) && (n_reuse_q > 0);Reuse, not device class, decides whether coherence earns its area. The engine costs state on both ends (6.1) and a quarter of the link under contention (6.2). The per-class counters that would prove it was worth having must ship in the current part, because the decision they inform is taken during the next one.
Private working memory is exposed over CXL.mem
LOCAL-MEMORY-ASSUMED-VISIBLE// The device has memory, so expose it.
assign need_mem_c = has_local_mem;An accelerator with private working memory is built as a Type 2 device. The host maps memory it never uses. The design carries an HDM decode path, host-managed coherency for a region nothing shares, and the full Type 2 verification matrix.
private local mem, no coherence | requirement-driven: 001 | category-driven: 111
local memory alone does NOT imply .mem : okThe second half of the .mem conjunction was dropped. Local memory is necessary for .mem and not sufficient — the question is whether the host must reach it. HBM used only by the accelerator's own datapath is memory on a chip, not a CXL resource.
The cost is not just the engine. Exposing memory makes it host-managed, which pulls in coherence obligations for a region that had none, and moves the device from "no CXL type" to Type 2 — the hardest class to verify.
assign need_mem_c = has_local_mem && need_host_visible;
if (required_mask[2] && !need_mem_c) mem_without_visibility_err <= 1'b1;Ownership is not visibility. A device owning memory says nothing about whether anyone else needs to address it, and conflating the two is the single most expensive step in this taxonomy — it is the difference between an .io-only device and a Type 2. The assertion form is the independence property: local memory without host visibility must not produce .mem.
Protocol set chosen from the product category
CATEGORY-DRIVEN-SELECTIONcase (marketing_category)
2'd2: required_mask = 3'b111; // "smart device" -> everything
...
endcaseA device is specified with all three protocols because of what it is called. Two engines have no workload behind them. The failure appears at schedule review, when the verification matrix for a Type 2 device is estimated for a device whose workload is .io-only.
private local mem, no coherence | required 001 | category-driven 111
category-driven picks 3 protocols, needs 1The category was used as the input to the protocol decision rather than as a label applied afterwards. It survives because categories are usually chosen for the right reasons — §4 shows four of five archetypes agreeing — so the process appears to work until it meets a device whose category and requirements diverge.
// Derive from requirements; let the category be the output's name.
assign required_mask = {has_local_mem && need_host_visible,
need_host_mem && need_coherent,
1'b1};
if (required_mask[1] && !need_cache_c) cache_without_need_err <= 1'b1;A category is a summary of a decision, not an input to it. The tell in a review is a protocol whose justification is a noun — "it's an accelerator", "it's a memory device" — rather than a sentence about what the workload does. Chapter 6.4 turns that tell into a standing review question.
A device is rejected for having too much capability
SPARE-COUNTED-AS-GAP// Anything that differs is a mismatch.
assign unmet = required_mask ^ device_mask;A Type 2 device is refused for a memory-expansion deployment that needs only .io + .mem. The device is fully capable and carries .cache as well. Deployment tooling reports a capability mismatch and will not admit it.
need=101 dev=111 : unmet=000 spare=010 satisfied=1 over=1 (correct)
XOR version : unmet=010 -> reported as a gapunmet was computed with XOR rather than AND-NOT, which conflates two opposite conditions. A protocol the device has and the requirement does not need is spare — a cost, possibly a bad architectural choice, and not a reason to refuse a working deployment.
The two are computed separately for exactly this reason: they mean opposite things and lead to opposite actions.
assign unmet = required_mask & ~device_mask; // needed and absent
assign spare = device_mask & ~required_mask; // present and not needed
if ((unmet & device_mask) != 3'b000) spare_counted_as_unmet_err <= 1'b1;Under-provision and over-provision are separate answers to separate questions. A single "match" boolean cannot express either, and XOR is the specific bug that turns surplus into deficit. The diagnostic that catches it — a bit reported unmet that the device demonstrably has — is a one-line invariant.
A workload change invalidates the protocol choice after tapeout
ASSUMPTION-NEVER-MEASURED// Count work done.
if (ev_valid) n_total_q <= n_total_q + 16'd1;Two years after shipping, the workload has shifted from scattered host-memory access to bulk streaming. Nobody can say whether the .cache engine is still earning its place, because the only telemetry is a total event count.
merged counter : total=90 and nothing else
per-kind : control=30 host-mem=30 dev-mem=30 reuse=0The architectural assumption — that the device reuses host memory — was never instrumented. It was true when the part was specified and stopped being true, and no measurement existed to notice.
Note the specific gap: reuse is the term that decides, and it is the one a generic transaction counter never captures. Host-memory access counts alone cannot distinguish a workload that justifies coherence from one that does not.
case (ev_kind)
2'd0: n_control_q <= n_control_q + 16'd1;
2'd1: n_host_mem_q <= n_host_mem_q + 16'd1;
2'd2: n_dev_mem_q <= n_dev_mem_q + 16'd1;
endcase
if (ev_reused) n_reuse_q <= n_reuse_q + 16'd1;
assign cache_justified = (n_host_mem_q > 0) && (n_reuse_q > 0);Instrument the assumption, not just the traffic. Every protocol in the mask rests on a claim about the workload, and each claim needs a counter that could falsify it. The specific one here is reuse — without it, a design can watch heavy host-memory traffic and conclude, wrongly, that its coherent engine is busy doing useful work.
16. Verification Plan
| Item | Approach and goal |
|---|---|
| Requirement derivation | sweep all 16 requirement combinations — mask matches the conjunctions |
Unconditional .io | drive need_control = 0 — .io still present |
| Independence | local memory without visibility — .mem must not appear |
| Category divergence | a case where category and requirement disagree — they must differ |
| Unmet vs spare | exact match, gap, and surplus — three distinct classifications |
| Cost crossover | sweep access counts across the crossover — the winner flips once |
| Zero accesses | n_access = 0 — a copy is never free |
| Workload measurement | streaming then reusing — cache_justified flips only on reuse |
| Independent reference | every workload count vs a stimulus-derived reference |
| Admission | a device missing a required protocol — refused, not attempted |
| Diagnostic liveness | broken variants incl. a miscounting profiler — each observed firing |
Rows 2, 3 and 9 are this chapter's additions. Row 2 exists because a constant input is an unverified input; row 3 because independence properties are the ones nobody writes; row 9 because an independent reference and a positive test on a checker catch different bugs, and this chapter needed both.
17. Design Review
- For each protocol in the mask, what sentence about the workload justifies it? A noun is not an answer.
- Does anything derive
.cachefrom host-memory access alone, or.memfrom local memory alone? - Is the device type an input anywhere, or is it always an output?
- Are under-provision and over-provision reported as separate results?
- What is the expected reuse, and which counter will confirm it after silicon?
- If the workload shifts, which measurement tells you the protocol choice has gone stale?
- Is a device with surplus capability admitted or refused? Which is intended?
18. How This Appears in Real Engineering
Category-driven selection survives because it is usually right. Four of §4's five archetypes agree with the requirement-driven answer. The process only fails on the device whose category was chosen for marketing reasons — and that is the device where the cost is largest, because the extra engines have no workload at all.
The cost model's direction surprises people. Heavy reuse argues for copying, not caching. Teams reasoning that "our accelerator works hard on the data, so it needs coherent access" have the crossover backwards, and the resulting .cache engine sits idle while a DMA path does the work.
Private memory becomes public by default. Debug Lab 3 is common because exposing device memory sounds free — it is "just" advertising an address range. It moves the device from no CXL type to Type 2 and pulls in host-managed coherency for a region nothing shares.
Nobody instruments reuse. Transaction counters are standard; reuse counters are not. The question that decides the next generation's protocol set is exactly the one the current generation cannot answer.
19. Common Misconceptions
| Claim | Why it is wrong |
|---|---|
"An accelerator needs .cache" | Only if it reaches host memory and needs coherence. Streaming once needs bandwidth. |
"A device with local memory needs .mem" | Only if the host must reach that memory. Private working memory needs no CXL protocol. |
| "Heavy data reuse argues for coherent access" | Backwards. Heavy reuse amortises a copy; low, unpredictable reuse is what coherence serves. |
| "The device type tells you the protocols" | The protocols tell you the type. Running it the other way starts from a label. |
".io follows from a stated control requirement" | .io is mandatory for all devices, requirement or not. |
| "More capability is never a problem" | Spare capability is state, timing pressure and DV surface with no workload owner. |
| "A capability mismatch means the device cannot be used" | Only if something is unmet. Surplus is not a gap. |
| "The Consortium's usage lists are a taxonomy" | They are examples of what people built, not a derivation of what makes a device need a protocol. |
20. Interview Reasoning
21. Exercises
-
Derive. A device must: be configured by the host; read host-resident lookup tables with heavy reuse; and hold 32 GB of packet buffers the host never addresses. Give the required protocol mask, the implied type, and the one requirement change that would move it to Type 2.
-
Calculate. Using §10's model, find the crossover access count for
SETUP = 500,LAT_LOCAL = 2,LAT_REMOTE = 10. Then state what happens to the crossover as the remote/local latency ratio falls, and what that implies for a device on a lower-latency link. -
DV task. Write the two independence properties for this chapter's conjunctions. Explain why every positive "X requires Y" test in the plan passes on a design that violates them.
-
RTL task. Extend
requirement_vectorso a requirement can be anticipated but not current — a workload the device should support next generation. State which existing diagnostic becomes ambiguous and what new one is needed. -
Debug task. A deployment tool refuses a Type 2 device for a memory-expansion role. Give your investigation order, and name the single expression most likely at fault.
-
Critique. Argue that a device should implement
.memwhenever it has local memory, on the grounds that host visibility might be wanted later. Give the strongest case, then price it using 6.1's state obligations and 6.2's arbitration shares.
22. Summary
The protocol set is derived from the workload, and the device type is an output.
- Five questions, forming two conjunctions and one unconditional:
.ioalways;.cacheneeds host-memory access and coherence;.memneeds local memory and host visibility. - Local memory does not imply
.mem. Ownership is not visibility, and conflating them is the step between an.io-only device and a Type 2. - Reuse, not device class, decides
.cache— and the crossover runs the counterintuitive way: low reuse favours coherent access, heavy reuse favours copying. - Under-provision and over-provision are different answers. A device with surplus capability is usable; XOR is the bug that turns surplus into deficit.
- The Consortium's usage lists are examples for checking a derivation, never a substitute for one.
- Instrument the assumption, not the traffic — reuse is the counter that decides, and it is the one nobody adds.
- Verification lessons: an input that is never varied is not verified; independence properties catch what positive tests cannot; and an independent reference and a positive test on a checker catch different bugs.
Chapter 6.4 closes Module 6 by turning this derivation into a decision method — what each capability costs, how to detect over- and under-provision in a real architecture, and why "hardware supports it" and "the system enables it" are different statements.
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.
