Skip to content
VLSI Mentor

CXL · Module 10

Type 1 Devices

The device that borrows host memory and offers none of its own: what a caching accelerator must contain, why every eviction is an obligation to the host, and why caching devices are the scarce resource in a topology. Seven RTL models, twenty-four mutations, twenty-four killed.

Module 9 ended by pricing what CXL.mem guarantees cost. Module 10 changes the question — from what must a device guarantee to what kind of device is it, and the answer turns out to be determined entirely by which protocols it speaks.

This chapter takes the first of the three: a device that caches host memory and owns none of its own.

1. The Engineering Problem — Compute That Lives on Someone Else's Data

An accelerator has a job to do on data the host owns. It has three options, and two of them are bad.

It can read the data over and over. Every access is a full round trip, and the accelerator spends its life waiting rather than computing.

It can copy the data into private memory. Now it is fast, and the copy is a snapshot: the moment the host writes, the accelerator is working on data that is quietly wrong. Every DMA-offload programming model in history has spent its complexity budget on exactly this problem — explicit copies, explicit invalidations, and a class of bug where the answer is stale rather than incorrect.

Or it can hold coherent copies of host lines, close enough to compute against, with the system responsible for telling it when a line it holds has changed.

The third option is a Type 1 device, and the whole of its architecture follows from that one decision.

2. The One-Sentence Model

A Type 1 device borrows. Every line it holds belongs to the host, it must give each one back when asked, it exposes no memory of its own, and its cache is finite — so it must also give lines back when it simply runs out of room.

Call it the borrower. That single word decides the hardware: something to hold the borrowed lines, something to track what has been borrowed, something to return them, and nothing at all on the other side — because a borrower lends nothing.

3. What This Chapter Owns

Three published chapters already touch device types, and the boundaries are worth stating before anything else.

GroundOwner
The engine frame — a device as optional contents3.3
The CXL.cache protocol itself — flows, ownership, transferModule 8
Deriving a protocol set from requirements6.4
The discipline of selecting protocols and pricing them6.5
What a Type 1 device must contain, and what it must notthis chapter
Type 2, Type 3, and choosing between them10.2 · 10.3 · 10.4

3.3 says it deliberately "introduces the device types precisely enough to reason about hardware, and stops there", and defers the full treatment here. Module 8 taught CXL.cache as a protocol; this chapter is about the device that speaks it — the structures it must build, the obligations those structures create, and the ones it is forbidden to have.

4. The Type Is Derived, Not Declared

A diagram showing that the protocol set determines the device type. A base row containing link logic, class dispatch and an outstanding table is present in every device. Above it sit two optional engines: a cache engine and a memory engine. Choosing the cache engine alone yields Type 1, both engines yields Type 2, and the memory engine alone yields Type 3. Choosing neither leaves an io-only device, which has no CXL device type at all.every CXL devicelink, dispatch, .iocache engineholds host linesmemory engineoffers its ownType 1.io + .cacheType 2both enginesType 3.io + .memalonealone12
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // .io is present in every CXL device; the coherent engines decide the type.
  assign proto_mask = {HAS_MEM, HAS_CACHE, 1'b1};
 
  wire [1:0] derived = ( HAS_CACHE &&  HAS_MEM) ? 2'd2 :
                       ( HAS_CACHE && !HAS_MEM) ? 2'd1 :
                       (!HAS_CACHE &&  HAS_MEM) ? 2'd3 : 2'd0;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP1: the device type is derived from the engines ===
  cache only        : mask=011 type=1
  cache + memory    : mask=111 type=2
  memory only       : mask=101 type=3
  neither           : mask=001 type=0 (io-only, not a CXL device type)
  cache without memory derives Type 1                  : ok
  cache with memory derives Type 2                     : ok
  memory without cache derives Type 3                  : ok
  neither engine is io-only, not a type                : ok
  .io is present in every device                       : ok
  declared Type 2, no engines : type=2 mismatch=1
  a declared type with no engines was caught           : ok

3.3 put this well: you do not choose a type and then implement it; you choose which engines to build, and the type is what you get. The RTL makes that literal — dev_type is a function of the parameters, not a stored field.

The last two lines are why that matters in hardware. The abuse instance declares Type 2 with no cache engine and no memory engine anywhere in it, and the mismatch checker catches it. A design that stores its type in a separate register can disagree with itself, and the disagreement is discovered by software that enables a protocol the silicon cannot speak.

Note the fourth row. Neither engine gives .io alone — a PCIe-style device on a CXL link. It has no CXL device type at all, because the types enumerate the coherent combinations. That is not a gap in the taxonomy; it is the taxonomy being about coherence.

5. Teaching-model boundary

6. RTL 2 — The Cache Is Finite, and Eviction Is an Obligation

This is the structure that makes the device a Type 1, and its most important property is the one people design around rather than for: it runs out.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      if (need_evict) begin
        // Evicting a modified line without telling the host loses the only
        // up-to-date copy in the system.
        if (!SILENT_EVICT) begin
          evict_notify <= 1'b1;
          evict_addr   <= tag_q[victim_q];
        end else if (mod_q[victim_q]) begin
          n_silent_q    <= n_silent_q + 16'd1;
          lost_line_err <= 1'b1;
        end
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP2: a Type 1 cache is finite, and eviction is an obligation ===
  8 fills into 8 lines : occupancy=8 peak=8 evictions=0
  the array filled to exactly 8 lines                  : ok
  no eviction was needed while lines were free         : ok
  9th fill : evict_notify=1 evict_addr=0 evictions=1
  the 9th fill raised an eviction notification         : ok
  silent-evict variant : evictions=1 silent losses=1 lost-line flag=1
  the silent variant notified nobody                   : ok
  it dropped exactly one modified line                 : ok
  and the lost-line checker caught the only copy going away : ok
  an evicting fill is one in and one out, so occupancy held : ok
  three more evictions : victims were 1, 2 and 3 in order : ok

In a private cache, an eviction is housekeeping. You drop a line, and if it was dirty you write it back to memory you own. Nothing outside the device has an opinion.

In a coherent device cache, an eviction is a message. The line belongs to the host, the host is tracking that this device holds it, and dropping it without saying so leaves the system believing a copy exists where none does — and if the line was modified, the device has just destroyed the only up-to-date copy in the system.

That is the difference this chapter exists to make, and it is why evict_notify is an output rather than an internal signal.

Occupancy held at 8 through four evictions

The assertion c_occ == 8 after the evicting fills is not bookkeeping trivia. An evicting fill is one in and one out, so a design that counts it as a net gain over-reports its own occupancy and will size the next generation's array from a number that was never real. The mutation that made an evicting fill increment occupancy was killed by exactly that assertion.

Why the victims had to be 1, 2, 3 — and not merely different

The rotation check was the last mutation to fall, and it taught something worth keeping.

The first attempt asserted the three evicted addresses were distinct. That passes on a design whose victim pointer never rotates — because a stuck pointer keeps evicting the line it just filled, and those addresses are all different too. Distinctness was true and useless.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
    // Distinctness is NOT enough: a stuck victim pointer also produces three
    // different addresses, because it keeps re-evicting the line it just
    // filled. Only the exact victims prove the pointer rotated.
    chk((ev1==8'd1) && (ev2==8'd2) && (ev3==8'd3), "the victims were lines 1, 2 and 3 in order");

A stuck victim turns an 8-line cache into a 1-line cache with seven lines of dead storage, and every occupancy counter reports it as full and healthy.

A sequence diagram with three lifelines: the host, the device cache, and the accelerator compute. The device requests a line and the host provides it, which the host records as a copy now held by this device. The accelerator computes on the line and modifies it. Later the cache runs out of room and must evict that line. The correct path sends an eviction notification to the host so the modified data returns and the host stops tracking the copy. A second path shows the same eviction performed silently, where the host still believes the device holds the line and the only up-to-date copy has been destroyed.A borrowed line, and the two ways it can leavehostdevice cacheacceleratorrequest a lineline provided — hostrecords the copyresident: computeproceedsline modifiedarray full — mustevicteviction notified —data returnsor: dropped silently— only copy gone

The last two rows are the same hardware event with opposite consequences. The notification is not an optimisation or a courtesy — without it the host still believes this device holds the line, and the modified data has ceased to exist anywhere in the system.

7. RTL 3 — Every Line It Holds Was Borrowed

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      if (rsp_valid) begin
        // A response whose tag is not live belongs to nothing.
        if (!live_q[rsp_tag] && !(do_req && (req_tag == rsp_tag)))
          orphan_rsp_err <= 1'b1;
        live_q[rsp_tag] <= 1'b0;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP3: every line a Type 1 holds was borrowed ===
  4 borrows outstanding : live=00001111 requests=4
  four tags are live                                   : ok
  all returned          : live=00000000 responses=4 orphan=0 dup=0
  every borrow was returned                            : ok
  abuse: response for an unissued tag : orphan=1
  a response matching nothing was caught               : ok
  abuse: same tag issued twice while live : duplicate=1
  reusing a live tag was caught                        : ok

Same transaction-identity machinery as 9.2's host-side table, on the other end of the link — and the symmetry is the point. 9.2's host tracked its accesses into device memory; here a device tracks its borrows from host memory. Same obligation, opposite direction, and a Type 1 device is the side that only ever does the second.

Note the same-cycle exemption in the orphan check: a response arriving in the cycle its request is issued is legal, and a checker that ignores that possibility fires on correct behaviour — the defect 9.5 hit from the other direction.

Both illegal-stimulus cases needed an abuse instance. Driving a response for an unissued tag, or reusing a live tag, into the instance under test would trip that instance's own checker and score as a failure. This is now standing practice across the track: illegal stimulus gets its own DUT.

8. RTL 4 — What a Type 1 Must Not Have

The defining negative. A Type 1 device offers the host no memory, so an inbound host-memory access has no correct answer — and the tempting wrong behaviour is to produce one anyway.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  // A Type 1 device has no host-visible memory, so only configuration space
  // is answerable. Serving a memory request implies storage it does not have.
  assign serve  = cfg_req || (SERVE_ANYWAY && host_mem_req);
  assign refuse = host_mem_req && !SERVE_ANYWAY;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP4: a Type 1 exposes no memory to the host ===
  config access : serve=0 refuse=0 cfg count=1
  configuration space is answerable                    : ok
  host memory read : correct serve=0 refuse=1 | serve-anyway serve=1
  a host memory access was refused                     : ok
  the serve-anyway variant answered it                 : ok
  refusals=1 | serve-anyway flagged serving absent memory=1
  the variant flagged answering for memory it does not have : ok

A device that answers for memory it does not have is worse than one that fails. It returns something — whatever its buffers held — and the requester has no way to know. The refusal is the correct behaviour precisely because it is visible.

This is also the cleanest way to state the Type 1/Type 2 boundary in hardware terms. The difference is not that Type 2 is bigger. It is that Type 2 has an answer to this request and Type 1 does not, and that single asymmetry is what 10.2 will show costs so much.

9. RTL 5 — The Cache Is a Performance Structure, Not Only a Correctness One

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  assign compute      = work_valid && (NO_STALL || line_present);
  assign request_line = work_valid && !line_present;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP5: compute stalls on a line the device does not hold ===
  5 resident + 3 missing : computes=5 misses=3 stalls=3
  compute happened only on resident lines              : ok
  three misses each stalled the pipeline               : ok
  no-stall variant       : computes=8 absent-data flag=1
  the no-stall variant computed on all eight           : ok
  ...and flagged producing a result from data it never had : ok

Everything above is about correctness. This module is about why anyone builds the thing: the accelerator's throughput is gated by residency. Work arrives every cycle; compute happens only on cycles where the line is present.

The no-stall variant is the accelerator version of 9.6's no-merge write. It computes on all eight — a 60% throughput improvement on every counter — by producing results from data it never had. The performance gain and the wrong answer are the same event, and this is the third time in this track that the fastest variant has been the incorrect one.

10. Waveform — Residency Gates Compute

Work is available every cycle; compute is not

9 cycles
Nine clock cycles traced from the RTL. Work is valid on every cycle. The line is resident at cycles one and two and from cycle six onward, and absent at cycles three, four and five. Compute follows residency exactly: it is high at cycles one and two, low at three through five, and high again from six. During the absent cycles the device requests the line instead, and the cache occupancy rises from zero to two as fills land. The compute count stalls at two across the miss window and then resumes.resident — computingresident — computingnot resident — stalled, fetchingnot resident — stalled,fetchingfilled — computing againfilled — computing againwork available, data absent: compute stopswork available, dataabsent: compute stopsthe fill lands and compute resumesthe fill lands and computeresumesclkwork_validline_presentcomputerequest_linecomputes012222345stalls000123333cache_occupancy000112222t0t1t2t3t4t5t6t7t8

Teaching-model timing derived from the simplified RTL in this chapter. Not CXL wire timing.

work_valid is high for all nine cycles and computes advances on six of them. The cache is why that number is six rather than two, and it is also why it is six rather than nine.

Read cache_occupancy alongside stalls: the array is filling exactly while the pipeline is stalled. That is the accelerator's entire performance story in one picture — the device is not computing, it is borrowing, and the ratio of those two activities is what a Type 1 design is optimising.

11. RTL 6 — Caching Devices Are the Scarce Resource

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      // Exceeding the DECLARED limit is always reportable -- guarding this with
      // the check-enable would switch it off on the only case that can trip it.
      if (n_caching_q > LIMIT[7:0]) over_limit_err <= 1'b1;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP6: caching devices are the scarce resource ===
  6 attach attempts, limit 4 : caching=4 admitted=4 refused=2 peak=4
  exactly four were admitted                           : ok
  the other two were refused, not silently accepted    : ok
  peak attachment was exactly the limit                : ok
  no-check variant : caching=6 over-limit flag=1
  the unchecked variant went past its declared limit   : ok
  and the over-limit checker reported it               : ok
  after one detach : caching=3
  3 cycles of attach+detach together : caching=3
  simultaneous attach and detach left the count unchanged : ok

This is a topology-level property that constrains a device-level design, and it is specific to caching devices — memory devices do not carry it.

The published figure is that CXL 3.0 fanout supports up to 16 CXL.cache devices. The reason a limit exists at all is asymmetric coherence: the host resolves coherence and tracks which devices hold which lines, so every caching device adds tracking state on the host side. A memory expander does not, which is why the two classes scale so differently and why 10.3 will look so much easier to deploy in quantity.

The simultaneous attach-and-detach check exists because the attachment count is the tenth appearance of the two-assignment counter defect in this track. One device leaving as another arrives must leave the count unchanged; two separate non-blocking assignments make it fall by one, and the topology slowly under-reports how many caching devices it is tracking.

12. RTL 7 — Is the Cache Earning Its Area?

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP7: is the cache earning its area? ===
  accesses=40 hits=26 fills=14 evictions=3 writebacks=2
  oracle: accesses=40 hits=26 fills=14 evictions=3
  accesses and hits matched an independent oracle      : ok
  hits are strictly fewer than accesses                : ok
  both conservation laws held                          : ok
  abuse: hits with no accesses, evictions with no fills : flags=1 1

A 65% hit rate on 40 accesses. The two conservation laws — you cannot hit more often than you looked, and you cannot evict more lines than you filled — are what make the hit rate trustworthy rather than merely reported.

The mutation that counted fills as hits is the one to remember. It reports a 100% hit rate on the same workload. Every fill is a miss being serviced, so counting it as a hit means the counter reports its best number precisely when the cache is performing worst. That is not a rounding error; it is a metric that inverts under load, and a team tuning against it will optimise in the wrong direction.

13. Quantitative Reasoning

Illustrative, with assumptions stated.

What residency is worth

From §9: 8 work items, 5 computed immediately, 3 stalled. Suppose a miss costs a full round trip of 250 ns and a hit costs 1 cycle at 1 GHz.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
hit rate 0.65 : 0.65 x 1 ns + 0.35 x 250 ns = 88 ns per access
hit rate 0.90 : 0.90 x 1 ns + 0.10 x 250 ns = 25.9 ns per access
hit rate 0.99 : 0.99 x 1 ns + 0.01 x 250 ns =  3.5 ns per access

Going from 65% to 90% is a 3.4× improvement; from 90% to 99% is another 7.4×. The curve is dominated entirely by the miss term, which is why accelerator cache sizing is so sensitive near the working-set boundary — and why the honest question is never "how big is the cache" but "does the working set fit".

What the cache costs

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
storage = lines x (line bytes + tag bits + state bits)

For 1024 lines of 64 bytes with a 40-bit tag and 3 state bits:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
data  = 1024 x 64 B      = 64 KB
meta  = 1024 x 43 bits   = 5.4 KB
total ~ 69.4 KB, of which 8% is metadata

The metadata fraction is the number worth carrying: 8% overhead to make 64 KB coherent. That is the price of the third option in §1, against a private copy that costs nothing extra in hardware and everything in software correctness.

What the fanout limit means for a rack

With a published limit of 16 CXL.cache devices in a CXL 3.0 fanout, a design that needs 24 caching accelerators does not need a bigger switch — it needs a different device type for some of them, or more root complexes. That is an architecture decision forced by a protocol limit, and it is the kind of constraint that is much cheaper to discover at architecture time than at bring-up.

14. Assertions

Icarus Verilog 13.0 does not support concurrent SVA here, so every property is synthesisable checker logic verified in simulation. 50 assertions.

Safety

PropertyIntent
Type derivationdev_type is a function of the engines present
Type agreementa declared type must equal the derived one
.io universalevery device's mask includes .io
Eviction notifieddropping a held line raises a notification
No lost linea modified line is never dropped silently
Victim rotationsuccessive evictions choose successive victims
Occupancy integrityan evicting fill is one in and one out
Borrow identityno orphan response, no duplicate live tag
No absent memorya Type 1 never serves a host-memory access
No absent datacompute never happens on a line not held
Fanout boundattachments never exceed the declared limit
Counter conservationhits ≤ accesses, evictions ≤ fills

Liveness

PropertyAssumption it needs
A borrowed line is eventually returnedthe device makes progress
A stalled work item eventually computesits line is eventually filled
A refused attachment eventually succeedssome device detaches

Performance goals

GoalMeasured by
Hit rate high enough to justify the arrayhits over accesses
Stall fraction boundedstalls over work items
Eviction rate not thrashingevictions over fills

15. Mutation Testing

Twenty-four mutations. Twenty-four killed.

MutationResult
Type 1 and Type 3 swappedkilled
.io dropped from the protocol maskkilled
Declared/derived mismatch never reportedkilled
Eviction never notifiedkilled
Silently dropped modified line not flaggedkilled
Victim pointer never rotateskilled
An evicting fill counted as a net gainkilled
Every fill evicts, even with lines freekilled
Duplicate live tag not reportedkilled
Orphan response not reportedkilled
Borrow never recorded as outstandingkilled
Host memory access neither served nor refusedkilled
A Type 1 answers for memory it does not havekilled
Serving absent memory not reportedkilled
Compute proceeds without the linekilled
A line is requested even when residentkilled
Computing on absent data not reportedkilled
Fanout limit off by onekilled
Refused attachments not countedkilled
Attach count uses two assignmentskilled
Over-limit checker guarded by the fault it detectskilled
Fills counted as hitskilled
Hit conservation law disabledkilled
Eviction conservation law disabledkilled

The first run scored 21 of 24, and the three escapes are worth separating because only one was a missing check.

Two needed stimulus that did not exist: a duplicate live tag, and a simultaneous attach and detach. Both went to abuse instances.

One was an assertion that was true and useless — the victim-rotation check described in §6. Asserting the evicted addresses were distinct passes on a design whose victim pointer is frozen, because a stuck pointer re-evicts the line it just filled and those addresses differ too. Only the exact victims — 1, 2, 3 — distinguish the two designs. This is the "assert the exact value, not a bound" rule from earlier batches appearing in a new disguise: here the useless assertion was a property rather than a bound, and it was equally hollow.

Note also over_limit_err, written deliberately without a guard on the check-enable parameter. That is the self-disabling-checker defect from 9.6 and 9.1, and the mutation that adds the guard back is killed by an abuse instance that exceeds its own declared limit.

16. Verification Plan

AreaApproach
Type derivationAll four engine combinations; a declared-mismatch abuse instance
Cache capacityFill to capacity, then beyond; occupancy across evicting fills
EvictionModified victims; a silent-evict variant; victim rotation by exact index
BorrowingTag issue and return; orphan and duplicate abuse instances
Absent memoryConfig versus memory access; a serve-anyway variant
ResidencyResident and missing work; a no-stall variant
FanoutAttach past the limit, detach, simultaneous attach and detach; an unchecked abuse instance
CountersIndependent oracle; two conservation abuse instances

The coverage cross is cache state × line state × request type: free / full / evicting, crossed with clean / modified, crossed with fill / probe / config. The full × modified × fill point is where the only-copy-in-the-system risk lives, and it must be directed — a random workload reaches it rarely and never labels it.

17. Silicon Observability

CounterDiagnoses
hit ratewhether the array is earning its area
fills and evictionscapacity pressure
eviction rate versus fill ratethrashing — a working set that does not fit
writebackshow much modified data the device is holding
stall cycles waiting on residencythe accelerator's real throughput limit
outstanding borrows, peakwhether the request port is the limit
refused attachmentstopology fanout pressure
lost_linecorrectness alarm — must be zero forever
orphan_rsp, dup_tagcorrectness alarms — must be zero forever
served_absent, computed_absentcorrectness alarms — must be zero forever

The most valuable pair is eviction rate against fill rate. When they converge, the working set does not fit and the device is thrashing — every fill immediately evicts something that will be needed again. Hit rate alone shows this only indirectly and late; the ratio shows it immediately and says what to do about it.

18. Debug Lab

1

An 8-line cache behaving like a 1-line cache

STUCK-VICTIM
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
victim_q <= victim_q;   // rotation dropped in a late edit
Symptom

Hit rate collapses on any working set larger than one line. Occupancy reports the array full and healthy. Every fill is immediately followed by an eviction. Cache size increases do nothing at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  three more evictions : victims were 1, 2, 3
  the victims were lines 1, 2 and 3 in order           : ok
Evidence

Log the evicted addresses across several evictions. If they are the addresses that were just filled, the pointer is frozen — the array is re-evicting its own newest line and seven lines are dead storage.

Root Cause

The victim pointer never advances, so one entry absorbs every eviction. Occupancy is genuinely 8 and utilisation is genuinely 1.

Distinct evicted addresses do not disprove this — a stuck pointer produces different addresses too, because the line it evicts changes each time. That is why the assertion checks the exact victims.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
victim_q <= (victim_q == (LINES-1)) ? 4'd0 : victim_q + 4'd1;

And assert the exact victim sequence, not merely that victims differ.

Prevention

Compare eviction rate against fill rate in silicon. Convergence means thrashing, and a frozen victim is the extreme case.

2

The only up-to-date copy in the system, silently discarded

SILENT-EVICT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// line is clean enough -- just drop it
vld_q[victim_q] <= 1'b0;
Symptom

Data written by the accelerator is missing later. No error anywhere. The host reads the address and gets the old value. It correlates with cache pressure, so it appears only on large working sets.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  silent-evict variant : evictions=1 silent losses=1 lost-line flag=1
  it dropped exactly one modified line                 : ok
Evidence

Compare evictions against eviction notifications. They must be equal. A gap means lines left the device without the system being told.

Root Cause

In a private cache, dropping a clean line is free housekeeping. In a coherent device cache the line belongs to the host and the host is tracking that this device holds it — and if the line was modified, the device just destroyed the only current copy.

The bug is a correct instinct applied in the wrong context, which is why it survives review by anyone whose cache experience is private-cache experience.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
evict_notify <= 1'b1;
evict_addr   <= tag_q[victim_q];

Every eviction is a message. Assert evictions == notifications continuously.

Prevention

Fill the array entirely with modified lines before forcing eviction. A test that fills with clean lines cannot distinguish the two designs — which is exactly what happened on this chapter's first run.

3

A device that answers for memory it does not have

SERVE-ABSENT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign serve = cfg_req || host_mem_req;   // "just respond to everything"
Symptom

A host access to an address the device does not own returns data — whatever the response buffers held. No error, no timeout. The corruption appears far from the device, in whatever consumed the fabricated value.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  host memory read : correct serve=0 refuse=1 | serve-anyway serve=1
  the variant flagged answering for memory it does not have : ok
Evidence

Issue a host memory read to a Type 1 device and check for a refusal, not merely for a response. "A response came back" passes on the broken design.

Root Cause

Responding is the default behaviour of a request handler, and refusing is an explicit case someone has to write. A Type 1 device has no host-visible memory, so the request has no correct answer and the only correct action is a visible refusal.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign serve  = cfg_req;
assign refuse = host_mem_req;
if (host_mem_req && serve) served_absent_err <= 1'b1;
Prevention

Test what the device must not do, not only what it must. A Type 1 device's negative obligations are half its specification and are routinely untested.

4

A 60% throughput gain that produced wrong answers

COMPUTE-ON-ABSENT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign compute = work_valid;   // don't gate on residency, it's slow
Symptom

Accelerator throughput improves markedly. Results are intermittently wrong in a way that correlates with cache pressure, so small test inputs are always correct and production inputs are not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  5 resident + 3 missing : computes=5 misses=3 stalls=3
  no-stall variant       : computes=8 absent-data flag=1
  ...and flagged producing a result from data it never had : ok
Evidence

Assert compute implies line_present. Then compare compute count against hit count — they cannot exceed residency.

Root Cause

The pipeline computed on lines the device did not hold, so it operated on stale or uninitialised buffer contents. The speedup and the wrong answer are the same event — the third time in this track that the fastest variant was the incorrect one.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign compute = work_valid && line_present;
if (compute && !line_present) computed_absent_err <= 1'b1;
Prevention

Treat any optimisation that beats the reference by doing less as a correctness question first. Ask what work disappeared and who depended on it.

5

A hit rate that improves as the cache gets worse

FILLS-AS-HITS
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire counted_hit = hit || fill;   // "a fill satisfies the access too"
Symptom

Reported hit rate is 100% on every workload, including ones that obviously thrash. Cache sizing experiments show no difference between 8 lines and 8192, so the team concludes the cache does not matter.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  accesses=40 hits=26 fills=14 evictions=3
  accesses and hits matched an independent oracle      : ok
  hits are strictly fewer than accesses                : ok
Evidence

Check hits < accesses strictly on any workload with a miss. A hit rate that never moves is not a good cache; it is a broken counter.

Root Cause

Every fill is a miss being serviced. Counting it as a hit means the metric reports its best number exactly when the cache is performing worst — the counter inverts under load, so tuning against it moves the design the wrong way.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire counted_hit = hit;
if (n_hit_q > n_access_q) hit_exceeds_access_err <= 1'b1;
Prevention

Give every derived metric a conservation law. Hits cannot exceed accesses; evictions cannot exceed fills. Both are one comparator and both catch a whole class of counter fiction.

6

The topology quietly forgot how many caching devices it had

TWO-ASSIGN-ATTACH
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (admit)     n_caching_q <= n_caching_q + 8'd1;
if (do_detach) n_caching_q <= n_caching_q - 8'd1;
Symptom

The count of attached caching devices drifts downward over a long uptime with hot-plug activity. Eventually the topology admits more devices than it can track. Attach-then-detach test sequences pass perfectly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  3 cycles of attach+detach together : caching=3
  simultaneous attach and detach left the count unchanged : ok
Evidence

Drive attach and detach in the same cycle and check the count holds. Any drift means one of the two paths lost.

Root Cause

Two non-blocking assignments to one counter: the second wins, so a cycle with both an arrival and a departure decrements instead of holding.

Tenth appearance of this defect family in this track, and the consequence here is admitting more caching devices than the host can track — which is a correctness failure, not a statistics one.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
case ({admit, do_detach})
  2'b10: n_caching_q <= n_caching_q + 8'd1;
  2'b01: n_caching_q <= n_caching_q - 8'd1;
  default: ;                                  // both or neither: hold
endcase
Prevention

Test the cross of the two events, not the sequence. Attach-then-detach passes on both designs; attach-with-detach passes on only one.

7

Software enabled a protocol the silicon does not implement

DECLARED-TYPE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
localparam [1:0] DEVICE_TYPE = 2'd2;   // a stored field, set by hand
Symptom

Enumeration succeeds and the device presents as Type 2. Software enables the memory path. Accesses to the device's advertised memory return nothing coherent, and the failure looks like a host or switch problem because the device reported itself correctly.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  declared Type 2, no engines : type=2 mismatch=1
  a declared type with no engines was caught           : ok
Evidence

Compare the declared type against one derived from which engines are actually instantiated. If a design cannot derive it, that is the finding.

Root Cause

The type was stored rather than derived, so it could disagree with the silicon. A hand-set constant survived an engine being removed for area.

The type is a consequence of the engines, not an input to them — and a design that treats it as an input can lie about itself.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
wire [1:0] derived = (HAS_CACHE && HAS_MEM) ? 2'd2 : ...;
if (dev_type != derived) type_mismatch_err <= 1'b1;

Derive it from the same parameters that instantiate the engines.

Prevention

Never store a fact that can be computed from the configuration. Anything stored can drift from what it describes.

8

A response arrived for a borrow that no longer existed

ORPHAN-RESPONSE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (do_req) live_q[req_tag] <= 1'b1;   // reused without checking
Symptom

The device occasionally installs a cache line at the wrong address. It requires the tag pool to wrap under load, so it appears only at high outstanding counts and never in directed tests.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  abuse: response for an unissued tag : orphan=1
  abuse: same tag issued twice while live : duplicate=1
  reusing a live tag was caught                        : ok
Evidence

Check whether a tag is live before issuing on it, and whether an arriving response's tag is live. Either violation means an identity collision.

Root Cause

A tag was reused while its earlier borrow was still outstanding. When the first response arrives it is matched to the second request, and the line lands under the wrong address.

Same identity machinery as 9.2's host table, on the device side of the link.

Fix
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (live_q[req_tag]) dup_tag_err <= 1'b1;
if (!live_q[rsp_tag] && !(do_req && (req_tag == rsp_tag)))
  orphan_rsp_err <= 1'b1;

Note the same-cycle exemption: a response arriving in the cycle its request issues is legal.

Prevention

Drive both violations into a separate abuse instance. Injecting them into the instance under test trips its own checker and scores as a failure.

19. Design Review

  1. Is the device type derived from the engines, or stored somewhere it can drift?
  2. What happens on the fill that finds every line valid?
  3. Is every eviction notified, including of clean lines?
  4. What happens when a modified line is evicted, and what proves it?
  5. Does the victim pointer rotate, and what assertion proves it rather than merely suggesting it?
  6. What does the device do with an inbound host-memory access?
  7. Can compute proceed on a line the device does not hold?
  8. Can a tag be reused while its borrow is outstanding?
  9. What is the hit rate, and what conservation law makes it trustworthy?
  10. How many caching devices does the topology admit, and what happens to the one after that?
  11. What state survives reset while lines are still borrowed?

20. How This Appears in Real Engineering

Architecture. The Type 1/Type 2 decision is settled by one question — does anything outside the device need to see its memory? If not, the memory engine is pure cost, and 6.5 prices it.

RTL. The cache array is small and carries four of this chapter's alarms. The eviction path is where private-cache instincts do the most damage.

DV. Fill-with-modified-lines-then-evict is the directed test that matters most, and a clean-line test cannot substitute for it.

Performance. Hit rate drives everything, and the miss term dominates the average so completely that working-set fit is the only sizing question worth asking.

Firmware. The device type determines which enable paths are legal; a stored type that disagrees with the silicon fails here first and looks like someone else's bug.

Post-silicon. Eviction rate against fill rate identifies thrashing immediately, where hit rate identifies it slowly and ambiguously.

21. Common Misconceptions

BeliefCorrection
You choose a device type and implement itYou choose engines; the type is the consequence
A Type 1 device has a small amount of memoryIt has none the host can see — that is the definition
Evicting a clean line is free housekeepingThe host is tracking that copy; the eviction is a message
A cache miss is only a performance eventComputing through one is a correctness event
Distinct evicted addresses prove the victim rotatesA stuck pointer produces distinct addresses too
A fill satisfies the access, so it counts as a hitA fill is a miss; counting it inverts the metric
More caching devices is a switch capacity questionIt is a host tracking-state question, and it is bounded
Type 2 is just Type 1 with more memoryIt is Type 1 plus an obligation the two engines create together

22. Interview Reasoning

23. Exercises

  1. Calculation. A Type 1 accelerator has a 2048-line, 64-byte cache with 40-bit tags and 3 state bits. Compute total storage and the metadata fraction. Then compute average access time at hit rates of 0.7, 0.9 and 0.98 given a 1 ns hit and a 250 ns miss, and state which improvement is worth more.

  2. Analysis. A device reports a 99% hit rate, a fill count equal to its eviction count, and poor accelerator throughput. Explain why those three facts are consistent, name the defect, and give the counter that distinguishes it from a genuinely well-performing cache.

  3. RTL task. Extend dev_cache_array to track, per line, whether the host has requested it back but the eviction has not yet completed. State the new invariant that creates and the failure mode if the state is omitted.

  4. Assertion task. Write the property that proves the victim pointer rotates. Explain why a property over the distinctness of evicted addresses does not prove it, and construct the design that satisfies distinctness while being wrong.

  5. Waveform analysis. Using §10's trace, compute the achieved compute rate over the nine cycles and what it would have been with a hit on every cycle. Then state what the trace would look like if the fill latency were four cycles rather than one, and which counter would reveal it.

  6. Debug task. An accelerator produces intermittently wrong results that correlate with input size. Give your investigation order across the four correctness alarms in this chapter, and the single directed test that separates a silent eviction from a compute-on-absent defect.

  7. Design review. A colleague proposes adding a small .mem engine to a Type 1 device to expose its scratchpad, arguing it is "only a few registers of extra state". Give the strongest version of that argument, then state precisely what it changes about the device's obligations and what it makes the device.

24. Summary

A Type 1 device borrows.

  • The type is derived from the engines present, never declared — a stored type can disagree with the silicon and enumerate as something the hardware cannot serve.
  • .io is in every device; the coherent engines decide the type, and neither engine means no CXL device type at all.
  • The cache is finite, so it must evict — and every eviction is a message, because the host is tracking that copy. Dropping a modified line silently destroys the only up-to-date copy in the system.
  • An evicting fill is one in and one out. Counting it as a net gain over-reports occupancy and mis-sizes the next generation.
  • Every line it holds was borrowed, tracked by identity — the mirror of 9.2's host-side table, on the other end of the link.
  • A Type 1 has no host-visible memory, so an inbound memory access must be refused, not answered. That single asymmetry is the whole Type 1 / Type 2 boundary.
  • Residency gates compute: work valid on nine cycles, compute on six. The cache is a performance structure, and the miss term dominates so heavily that working-set fit is the only sizing question.
  • Caching devices are the scarce resource — every one adds host-side tracking state, and published fanout is up to 16 CXL.cache devices.
  • Verification: 24 of 24 mutations killed, 50 assertions. The last to fall taught that an assertion can be true and useless — distinct evicted addresses do not prove a victim pointer rotates, because a frozen pointer produces distinct addresses too.
  • The two-assignment counter defect appeared for the tenth time, here under-counting attached caching devices.

Next: 10.2 — Type 2 Devices, where the device both caches host memory and offers its own, and the two relationships interact in a way that makes it the hardest class in the taxonomy.

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.