Skip to content
VLSI Mentor

CXL · Module 4

Transaction Layer

The layer that knows what a request means, and where the three CXL protocol classes stop being interchangeable — different ordering requirements, different latency targets, and why they need separate resources.

Chapter 4.2 delivered units intact and said nothing about what they mean. That was the point — the link layer must not know.

This chapter is the layer that does know, and knowing turns out to be the place where the three protocol classes stop being interchangeable.

1. The Engineering Problem — One Layer, Three Different Contracts

Below this layer, everything is symmetric. The physical layer moves flits; the link layer delivers them intact. Neither distinguishes a configuration write from a coherent read, and neither should.

Above this layer, the differences are total. A configuration write to a device register and a coherent read of a cache line are not two instances of one thing with different payloads. They have different ordering requirements, different latency expectations, and different consequences when they are delayed.

So the transaction layer faces a problem the layers below never had: it must serve three classes that agree on almost nothing, over shared machinery, without letting the requirements of one become the requirements of all.

The failure mode when it gets this wrong is not incorrectness. A design that applies the strictest class's rules to everything is correct — and slow, in a way that looks like a hardware limitation rather than a policy choice. Section 9 measures exactly that.

2. The One-Sentence Model

The transaction layer is where meaning enters the stack, and meaning is what makes the three classes different: each needs its own ordering rule, its own resources and its own latency budget — so a transaction layer that treats them uniformly is not simpler, it is one that has silently adopted the strictest requirement for all three and the weakest guarantee for each.

Call it the three contracts model. One layer, three service agreements, and the engineering is in keeping them separate.

3. What This Chapter Owns

QuestionOwned by
Getting flits onto the wire4.1
Delivering them intact4.2
Transaction semantics and per-class divergencethis chapter
The three protocols compared in full4.4
Stack-level PCIe comparison4.5
Coherence semantics3.4

A deliberate division with 4.4. This chapter is about what the layer must do given that classes differ. Chapter 4.4 is about what the three classes are, side by side. Here they are a source of divergent requirements; there they are the subject.

4. Why the Classes Diverge

Start from what each class is for, using the Consortium's own descriptions from Chapter 3.2:

ClassConsortium saysImplies
.iodiscovery, register access, DMAorder matters to software
.cachedevice caches host memoryper line; latency critical
.memhost uses device memoryper addr; latency critical

Three consequences follow, and they are the chapter.

.io needs producer/consumer ordering. A register write that arms a device followed by one that triggers it must arrive in that order — reordering them is not slow, it is wrong. That requirement comes from software semantics that predate CXL entirely.

.cache and .mem need ordering per address, not globally. Chapter 3.4 established that coherence is a per-line property: two requests to different lines have no relationship and serialising them buys nothing. Forcing global order on coherent traffic is the single most expensive mistake available at this layer.

.cache and .mem have a latency target that .io does not. The Consortium states it directly:

5. The Mental Model — Three Contracts Over Shared Machinery

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
        .io                .cache              .mem
   ┌───────────┐       ┌───────────┐      ┌───────────┐
   │ in order  │       │ per-line  │      │ per-addr  │
   │ no target │       │ near-cache│      │ near-cache│
   │ own pool  │       │ own pool  │      │ own pool  │
   └─────┬─────┘       └─────┬─────┘      └─────┬─────┘
         └───────────────────┼──────────────────┘

                  shared link and PHY below

The three boxes must stay separate all the way down to the multiplexing point. Merge them earlier — one ordering rule, one resource pool, one latency assumption — and the merge is invisible until load, at which point the strictest rule applies to everything and the weakest guarantee applies to each.

Three columns, one per protocol class. Each shows its ordering rule, latency target and resource allocation. All three converge on a shared multiplexing point which feeds the link and physical layers below..ioconfig, registers, DMA.cachedevice caches host memory.memhost uses device memoryIn orderproducer/consumer mattersPer addressdifferent lines may passPer addressdifferent addresses maypassNo latency targetown resource allocationNear cache latencyown resource allocationNear cache latencyown resource allocationShared multiplexing,link and PHYmerge here and no earlier12
Figure 1 — three contracts over shared machinery. Each class carries its own ordering rule, its own resource allocation and its own latency obligation all the way down to the multiplexing point. Every place they merge earlier is a place where one class's requirements are imposed on another — which Sections 9 and 11 measure.

6. Quantitative Reasoning

Ordering costs concurrency

Under global ordering, a request waits for everything older. Under per-address ordering it waits only for older requests to the same address. With A distinct addresses in flight and requests uniformly spread:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
P(a given request is blocked)  ≈  1/A     (per-address)
P(a given request is blocked)  ≈  1       (global, if anything is outstanding)

So per-address ordering scales with address diversity and global ordering does not scale at all. Section 9 measures the extreme case: 2 proceeded versus 0 on identical stimulus.

Latency budget is a maximum, not a mean

For a class targeting near-cache latency, the mean is nearly useless. A distribution with a good mean and a long tail fails the requirement on every tail event, and the tail is what software notices.

Section 12 measures a run whose mean is 8 and whose maximum is 25 against a budget of 20 — the mean passes comfortably and two transactions exceed the budget. Reporting the mean would have declared the design compliant.

Outstanding depth, per class

Each class needs enough entries for its own rate and latency, by the same Little's Law argument as Chapter 3.2 and Chapter 4.2:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
entries_class  ≈  rate_class  ×  latency_class

The classes have different rates and different latencies, so they need different depths — which is a second, independent reason not to share a pool. A shared pool cannot be sized correctly for three different products of rate and latency simultaneously.

7. Teaching-model boundary

8. RTL 1 — Ordering, Per Class

Purpose

Apply each class's actual ordering requirement, and no more.

class_ordering.sv — the strictest rule is not the safe default
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Different protocol classes need different ordering guarantees, and a layer
// that applies one rule to all of them either over-serialises the classes that
// do not need it or under-serialises the ones that do.
//
//   .io       -- producer/consumer ordering matters; keep it in order
//   .cache    -- ordering is per address; independent lines may pass
//   .mem      -- ordering is per address; independent addresses may pass
//
// ARCHITECTURAL TEACHING MODEL. No CXL ordering rule, transaction-layer
// requirement or completion rule is modelled or claimed.
module class_ordering #(
  parameter bit ORDER_EVERYTHING = 1'b0    // 1 = one rule for all classes
) (
  input  logic        clk,
  input  logic        rst_n,
  input  logic        req_valid,
  input  logic [1:0]  req_class,          // 0=io, 1=cache, 2=mem
  input  logic [7:0]  req_addr,
  input  logic        older_outstanding,  // any older request still in flight
  input  logic        older_same_addr,    // an older request to THIS address
  output logic        may_proceed,
  output logic        blocked,
  output logic [15:0] n_proceed_q,
  output logic [15:0] n_blocked_q,
  output logic        order_violation_err
);
  logic needs_strict, needs_addr;
 
  assign needs_strict = ORDER_EVERYTHING || (req_class == 2'd0);
  assign needs_addr   = !ORDER_EVERYTHING && (req_class != 2'd0);
 
  // Strict ordering waits for EVERYTHING older. Address ordering waits only
  // for an older request to the same address -- which is what lets independent
  // lines proceed in parallel.
  assign may_proceed = req_valid &&
                       !(needs_strict && older_outstanding) &&
                       !(needs_addr   && older_same_addr);
  assign blocked     = req_valid && !may_proceed;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin /* ... */ end
    else begin
      if (may_proceed) n_proceed_q <= n_proceed_q + 16'd1;
      if (blocked)     n_blocked_q <= n_blocked_q + 16'd1;
      // The invariant that must hold whatever the policy: two requests to the
      // same address never proceed out of order.
      if (may_proceed && older_same_addr) order_violation_err <= 1'b1;
    end
  end
endmodule

The invariant is written against the address, not against the policy. order_violation_err fires whenever a request proceeds past an older one to the same address, regardless of which class it is or which mode the module is in. That is deliberate: it is the one rule no policy may break, so it must be checkable independently of the policy that is supposed to enforce it.

Synthesis. Two comparisons and a small amount of gating — the module's cost is trivial. The expensive part is what feeds it: older_same_addr requires comparing against every outstanding address, which is a CAM-style structure whose width and depth are the real design decision. That is why per-address ordering is architecturally cheap and structurally expensive, and why a design might reasonably choose a coarser granularity than exact address match.

Simulation evidence

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP1: ordering requirements differ by class ===
  .io, older request outstanding            per-class: proceed=0 | one-rule: proceed=0
  .cache, older req to a DIFFERENT address  per-class: proceed=1 | one-rule: proceed=0
  .mem, older req to a DIFFERENT address    per-class: proceed=1 | one-rule: proceed=0
  .cache, older req to the SAME address     per-class: proceed=0 | one-rule: proceed=0
  .mem, older req to the SAME address       per-class: proceed=0 | one-rule: proceed=0
 
  proceeded: per-class=2 one-rule=0 | blocked: per-class=3 one-rule=5

Rows one, four and five are identical. Both policies block .io behind older traffic, and both block coherent traffic behind an older request to the same address — so on any stimulus containing only those cases the two designs are indistinguishable.

Rows two and three are the entire difference, and they are the common case. Coherent traffic to different addresses is what a working system mostly does, and the uniform policy blocks all of it. Two proceeded versus zero, on five requests.

Neither design violated the address invariant. The uniform policy is not incorrect — it is correct and it has thrown away the concurrency that made per-line coherence worth building. That is the shape to recognise: a policy failure that no correctness property can detect.

9. RTL 2 — Matching a Response to Its Request

Purpose

The transaction layer's defining job, and the term people drop.

req_rsp_match.sv — tag AND class
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The transaction layer's defining job: a response must find the request that
// produced it, and must arrive on the same protocol class it left on.
//
// ARCHITECTURAL TEACHING MODEL. NOT a CXL transaction-ID or completion
// mechanism; no CXL field, encoding or matching rule is modelled.
module req_rsp_match #(
  parameter int unsigned NTAG         = 8,
  parameter bit          IGNORE_CLASS = 1'b0   // 1 = match on tag alone
) (
  input  logic                     clk,
  input  logic                     rst_n,
  input  logic                     issue,
  input  logic [1:0]               issue_class,
  input  logic                     rsp_valid,
  input  logic [$clog2(NTAG)-1:0]  rsp_tag,
  input  logic [1:0]               rsp_class,
  output logic                     issue_ok,
  output logic [$clog2(NTAG)-1:0]  issue_tag,
  output logic [1:0]               matched_class,
  output logic                     rsp_matched,
  output logic                     class_mismatch_err,
  output logic                     orphan_rsp_err,
  output logic [7:0]               outstanding_q
);
  assign matched_class = cls_q[rsp_tag];
  assign class_ok      = IGNORE_CLASS || (rsp_class == matched_class);
  // A response matches only when the tag is live AND the class agrees.
  assign rsp_matched   = rsp_valid && live_q[rsp_tag] && class_ok;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin /* ... */ end
    else begin
      if (issue_ok) begin
        live_q[issue_tag] <= 1'b1;
        cls_q[issue_tag]  <= issue_class;   // remember which contract it was
      end
      if (rsp_valid) begin
        if (!live_q[rsp_tag])                 orphan_rsp_err     <= 1'b1;
        else if (rsp_class != cls_q[rsp_tag]) class_mismatch_err <= 1'b1;
        if (rsp_matched) live_q[rsp_tag] <= 1'b0;
      end
      outstanding_q <= outstanding_q + {7'b0, issue_ok} - {7'b0, rsp_matched};
    end
  end
endmodule

Class is stored per tag because the tag namespaces may overlap. If each class has its own tag space — which is the natural design, since each has its own resources — then tag 1 exists in all three, and matching on tag alone will happily resolve a .cache response against a .mem request. Storing the class is what makes the tag meaningful.

Two distinct errors again. An orphan response means the tag is not live; a class mismatch means it is live and belongs to a different contract. Different causes, different fixes, and a design reporting one code for both loses the distinction.

Simulation evidence

Two requests issued on different classes, then a response arriving on the wrong one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP2: a response must match tag AND class ===
  2 issued: outstanding=2
  response tag 0 class .cache : class-aware matched=1 | tag-only matched=1
  response tag 1 class .cache : class-aware matched=0 | tag-only matched=1  <-- WRONG class
    (tag 1 was issued on .mem)
 
  class-aware class_mismatch_err=1 | tag-only=1

The tag-only matcher accepted a .cache response for a .mem request. The tag was live, so from its perspective everything was fine — and the request it retired was not the one that completed. The class-aware matcher refused and flagged it.

Both designs set the mismatch flag, which is worth noticing for the same reason as Chapter 4.1's unknown-flit counter: the tag-only matcher detected the mismatch and accepted the response anyway. Detection wired to a status bit rather than to the decision is a recurring shape in this course, and it is always the same fix — put the term in the decision.

10. RTL 3 — Resources, Per Class

Purpose

Protect the classes with a latency target from the class without one.

class_resources.sv — three products of rate and latency
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Each protocol class gets its own transaction-layer resources, because they
// have different latency obligations. The Consortium states the design target
// for .cache and .mem as near CPU cache latency; .io has no such target.
//
// ARCHITECTURAL TEACHING MODEL. Entry counts are teaching values; no CXL
// resource, credit or buffer allocation is modelled.
module class_resources #(
  parameter int unsigned N_IO    = 6,
  parameter int unsigned N_CACHE = 6,
  parameter int unsigned N_MEM   = 6,
  parameter bit          SHARED  = 1'b0
) (
  input  logic       clk,
  input  logic       rst_n,
  input  logic       req_io, req_cache, req_mem,
  input  logic       ret_io, ret_cache, ret_mem,
  output logic       can_io, can_cache, can_mem,
  output logic [7:0] used_io_q, used_cache_q, used_mem_q,
  output logic [7:0] pool_q,
  output logic       latency_class_blocked_err,
  output logic       overflow_err
);
  localparam logic [7:0] POOL = N_IO[7:0] + N_CACHE[7:0] + N_MEM[7:0];
 
  assign can_io    = SHARED ? (pool_q < POOL) : (used_io_q    < N_IO[7:0]);
  assign can_cache = SHARED ? (pool_q < POOL) : (used_cache_q < N_CACHE[7:0]);
  assign can_mem   = SHARED ? (pool_q < POOL) : (used_mem_q   < N_MEM[7:0]);
 
  // The specific harm a shared pool does: a latency-critical class refused
  // while the entries reserved for it are sitting free.
  assign latency_class_blocked_err =
      SHARED && ((!can_cache && (used_cache_q < N_CACHE[7:0])) ||
                 (!can_mem   && (used_mem_q   < N_MEM[7:0])));
 
  // ... occupancy maintained per class and as a pool, each in one assignment ...
endmodule

latency_class_blocked_err names the harm rather than the mechanism. It does not say "the pool is full" — it says a class with a latency target was refused while its own reservation was free. That distinction is what makes it a useful diagnostic rather than a restatement of !can_cache.

Simulation evidence

Twenty cycles of .io traffic with no returns, then .cache and .mem ask:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP3: bulk .io against latency-critical classes ===
  after a 20-cycle .io flood:
    split  : io=6 cache=0 mem=0 | can_cache=1 can_mem=1
    shared : io=18 cache=0 mem=0 | can_cache=0 can_mem=0  latency_blocked=1
 
  .cache and .mem now ask:
    split  : cache accepted=1 mem accepted=1
    shared : cache accepted=0 mem accepted=0  <-- refused

The split configuration capped .io at its own six entries. The shared configuration let .io take eighteen — the entire pool — and then refused both latency-critical classes, with zero entries used by either.

Read the harm precisely: the two classes the Consortium targets at near-CPU-cache latency were blocked by the one class with no latency target, and they were blocked while the resources notionally reserved for them sat empty. This is Chapter 2.4's shared-budget coupling at the transaction layer, and the reason it matters more here is the latency target: a .io transfer delayed by 10 cycles is unmeasurable, and a coherent access delayed by 10 cycles may be the difference between meeting the target and missing it.

Note that the split configuration also throttled .io. That is the cost, and it is real — bulk traffic gets less than it could take. Whether that is acceptable depends entirely on whether the latency target is a requirement or an aspiration.

11. RTL 4 — Latency Against a Budget

Purpose

Make "near CPU cache latency" into something a regression can fail.

txn_latency.sv — the maximum is the requirement
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Per-class latency accounting, because the classes have different targets.
//
// ARCHITECTURAL TEACHING MODEL. The budget is a parameter supplied by the
// instantiator; no CXL latency figure is asserted.
module txn_latency #(
  parameter int unsigned BUDGET = 20
) (
  input  logic        clk,
  input  logic        rst_n,
  input  logic        start,
  input  logic        done,
  output logic [15:0] n_done_q,
  output logic [31:0] lat_sum_q,
  output logic [15:0] max_lat_q,
  output logic [15:0] n_over_budget_q,
  output logic [15:0] inflight_q
);
  logic [15:0] age_q;
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin /* ... */ end
    else begin
      inflight_q <= inflight_q + {15'b0, start} - {15'b0, done};
      if (inflight_q != 0) age_q <= age_q + 16'd1;
      else                 age_q <= '0;
      if (done) begin
        n_done_q  <= n_done_q + 16'd1;
        lat_sum_q <= lat_sum_q + {16'b0, age_q};
        if (age_q > max_lat_q)    max_lat_q       <= age_q;
        // A maximum matters more than a mean for a latency target.
        if (age_q > BUDGET[15:0]) n_over_budget_q <= n_over_budget_q + 16'd1;
      end
    end
  end
endmodule

n_over_budget_q is the counter that matters and it is the one usually missing. A mean and a maximum together still do not tell you how often the budget was missed — and for a latency target, frequency is the requirement. One excursion in a million is a different product from one in ten.

Simulation evidence

Ten transactions, eight short and two deliberately long, against a 20-cycle budget:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
=== EXP4: latency against a stated budget ===
  10 completed: max latency=25, over a budget of 20: 2
  mean = 82/10 = 8  <-- the mean hides both outliers

A mean of 8 against a budget of 20 looks like a design with 60% headroom. The maximum is 25 and two of ten transactions missed the budget entirely. Any report built on the mean would have declared this compliant.

That is the general result for latency-targeted traffic, and it is why Chapter 3.6 argued for maxima at every stage: a mean describes the typical case, and a latency target is a claim about the worst one.

Same stream, two ordering policies

10 cycles
Ten cycles. The class rotates between io, mem and cache, and addresses cycle through 0, 1 and 2. The older-same-address signal is high at cycles 3, 6 and 9. The per-class proceed signal is high at cycles 0, 1, 2, 5 and 7; the one-rule proceed signal is high only at cycle 0.per-class admits, uniform does notper-class admits,uniform does notcoherent, different address — passescoherent, different address— passessame address — both blocksame address — both block.io behind older work — both block.io behind older work —both blockclkclassiomemcachememiomemcachememiomemaddr0120120120older_any0111111111older_same0001001001per_class_go1110010100one_rule_go1000000000t0t1t2t3t4t5t6t7t8t9
Figure 2 — RTL-derived waveform transcribed from the EXP5 simulation. A mixed stream of classes and addresses runs through both policies. The per-class policy admits coherent traffic to unblocked addresses at cycles 1, 2, 5 and 7 that the uniform policy blocks; both policies agree wherever the address genuinely conflicts, at cycles 3, 6 and 9. Cycle numbers are the simulation's own.

12. Assertions

Concurrent SVA execution: NOT SUPPORTED BY Icarus Verilog. Not executed; each property maps to its procedural stand-in and to the mutation that proves it fires.

txn_sva.sv — bind-ready properties
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// SAFETY -------------------------------------------------------------------
// T1 — two requests to the same address never proceed out of order. Written
// against the ADDRESS, not the policy, so it holds whatever the policy is.
a_addr_order: assert property (@(posedge clk) disable iff (!rst_n)
  may_proceed |-> !older_same_addr);
 
// T2 — a response is accepted only when tag AND class agree.
a_rsp_class: assert property (@(posedge clk) disable iff (!rst_n)
  rsp_matched |-> (rsp_class == matched_class));
 
// T3 — a response is accepted only against a live request.
a_rsp_live: assert property (@(posedge clk) disable iff (!rst_n)
  rsp_matched |-> live_q[rsp_tag]);
 
// T4 — outstanding count tracks accepted issues minus MATCHED responses.
// Decrementing on rsp_valid instead would leak on every rejected response.
a_outstanding_conserved: assert property (@(posedge clk) disable iff (!rst_n)
  outstanding_q == ($countones(live_q)));
 
// T5 — no class exceeds its own allocation.
a_class_bounded: assert property (@(posedge clk) disable iff (!rst_n)
  (used_io_q <= N_IO) && (used_cache_q <= N_CACHE) && (used_mem_q <= N_MEM));
 
// T6 — in a split configuration, a class is refused ONLY when its own
// entries are exhausted. This is what a copy-paste gate breaks.
a_own_gate: assert property (@(posedge clk) disable iff (!rst_n)
  (!SHARED && !can_cache) |-> (used_cache_q >= N_CACHE));
 
// T7 — a maximum never decreases.
a_max_monotonic: assert property (@(posedge clk) disable iff (!rst_n)
  max_lat_q >= $past(max_lat_q));
 
// LIVENESS -----------------------------------------------------------------
// T8 — a blocked request eventually proceeds once its blocker retires.
// ASSUMPTION: every outstanding request eventually completes. That is a
// constraint on the layers below, not on this module.
a_eventually_proceeds: assert property (@(posedge clk) disable iff (!rst_n)
  (blocked && !older_same_addr && !older_outstanding) |-> ##[1:2] may_proceed);
 
// GOAL / PERFORMANCE -------------------------------------------------------
// T9 — the latency-targeted classes meet their budget. Not a correctness
// property: a design can be entirely correct and fail this.
a_latency_budget: assert property (@(posedge clk) disable iff (!rst_n)
  (n_done_q > 0) |-> (n_over_budget_q == 0));
 
// T10 — a latency-targeted class is never refused while its own reservation
// is free. This is the shared-pool harm, stated as a property.
a_no_latency_starve: assert property (@(posedge clk) disable iff (!rst_n)
  !latency_class_blocked_err);
SVAClassResult
T1safetyheld by both
T2safetyheld; tag-only accepted it
T3safetyheld
T4safetyconserved
T5safetybounded
T6safetyheld; copy-paste gate flagged
T7safetymonotonic
T8livenessproceeded
T9goal2 of 10 over budget
T10goalfired, as designed

T9 and T10 are goal properties and both are informative rather than fatal. T9 failed on a run whose mean was well inside budget; T10 fired on the shared configuration, which is the configuration being criticised. Neither indicates a bug — both indicate a design outside its intended envelope, which is exactly what a goal property is for and why it must be labelled differently from a safety property.

13. Mutation Testing

Five mutations injected into the exact tutorial RTL, compiled and simulated; good RTL restored and re-verified.

#MutationDetected by
P1same-address ordering droppedsame-address ordering violated
P2response matched on tag alonematcher accepted a wrong-class response
P3cache gate reads the .io countersplit gate refused .cache with own entries free
P4latency max tracks the last, not the worstmax latency decreased
P5shared-pool harm flag stubbed to zerodiagnostic did not fire when it should have

P3 also required a strengthened checker, and the first version of P3 was itself a bad mutation — changing the split gate to read the shared pool did not bite, because the .io gate still limited the pool. Replacing it with the realistic copy-paste bug (the .cache gate reading used_io_q) exposed the gap immediately. A mutation that does not change behaviour tests nothing, and choosing mutations that correspond to plausible mistakes is part of the technique.

14. Debug Lab

1

Coherent throughput collapses and every ordering check passes

STRICTEST-RULE-APPLIED-TO-ALL
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Ordering is safety-critical, so enforce it everywhere.
assign may_proceed = req_valid && !older_outstanding;
Symptom

Correct data, correct ordering, and coherent traffic that will not overlap. Both policies on identical stimulus:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  .cache, older req to a DIFFERENT address  per-class: proceed=1 | one-rule: proceed=0
  .mem, older req to a DIFFERENT address    per-class: proceed=1 | one-rule: proceed=0
 
  proceeded: per-class=2 one-rule=0
Root Cause

.io's producer/consumer requirement was applied to classes whose ordering is per address. Chapter 3.4 established that coherence is a per-line property — two requests to different lines have no relationship, and serialising them buys nothing while costing all the concurrency the per-line design was for.

No correctness property can catch this. The uniform policy satisfies every ordering invariant, including the same-address one; it is strictly more conservative than required. The failure is a policy failure, visible only as throughput.

Fix

Give each class its actual requirement:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign needs_strict = (req_class == CLASS_IO);
assign needs_addr   = (req_class != CLASS_IO);
assign may_proceed  = req_valid && !(needs_strict && older_outstanding)
                                && !(needs_addr   && older_same_addr);

Prevention. Keep the safety property written against the address rather than the policy, so it holds under any policy and cannot be satisfied by simply blocking everything. Then measure concurrency: a goal property on proceed-versus-blocked ratio under multi-address stimulus is what turns this from invisible into reportable.

2

A response retires the wrong request

MATCHED-ON-TAG-ALONE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The tag identifies the transaction.
assign rsp_matched = rsp_valid && live_q[rsp_tag];
Symptom

A .cache response retires a .mem request. The tag was live, so nothing looked wrong at the moment of the match:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  response tag 1 class .cache : class-aware matched=0 | tag-only matched=1  <-- WRONG class
    (tag 1 was issued on .mem)
 
  class-aware class_mismatch_err=1 | tag-only=1
Root Cause

The classes have separate tag spaces — which is the natural consequence of giving them separate resources — so tag 1 exists in all three and a tag alone does not identify a transaction. The matcher was missing the term that disambiguates.

Note that the tag-only design also raised the mismatch flag and accepted the response anyway. Detection was wired to a status bit rather than to the decision, which is the same shape as Chapter 4.1's unknown-flit counter and Chapter 3.3's computed-and-ignored permission.

Fix

Store the class with the tag, and require both:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (issue_ok) cls_q[issue_tag] <= issue_class;
assign rsp_matched = rsp_valid && live_q[rsp_tag] && (rsp_class == cls_q[rsp_tag]);

Prevention. Assert rsp_matched |-> (rsp_class == matched_class), and issue on multiple classes concurrently so tags collide across classes — a single-class regression cannot produce the stimulus.

3

Latency-critical traffic is blocked by traffic with no latency target

SHARED-TRANSACTION-RESOURCES
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// One pool is simpler and uses the entries better.
assign can_cache = (pool_q < POOL);
assign can_mem   = (pool_q < POOL);
Symptom

Coherent and memory access latency degrades whenever bulk I/O is active, with the entries notionally reserved for them completely unused:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  after a 20-cycle .io flood:
    split  : io=6  cache=0 mem=0 | can_cache=1 can_mem=1
    shared : io=18 cache=0 mem=0 | can_cache=0 can_mem=0  latency_blocked=1
Root Cause

A shared pool couples classes with incompatible requirements. The Consortium targets .cache and .mem at near CPU cache latency; .io has no such target and unbounded volume. Sharing lets the class with no target consume the entries of the two that have one.

There is a second, independent reason sharing cannot work here: each class needs rate × latency entries, and the three classes have different rates and different latencies. A single pool cannot be sized correctly for three different products at once.

Fix

Separate allocations per class:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
assign can_io    = (used_io_q    < N_IO);
assign can_cache = (used_cache_q < N_CACHE);
assign can_mem   = (used_mem_q   < N_MEM);

Prevention. Instrument the harm directly — a latency-targeted class refused while its own entries are free — rather than inferring it from occupancy. And be explicit about the cost: the split configuration also capped .io at six, which is real throughput given up in exchange for a latency guarantee.

4

A latency budget is reported as met by a design that misses it

MEAN-REPORTED-FOR-A-TAIL-REQUIREMENT
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Report average latency.
mean_latency = lat_sum_q / n_done_q;      // and nothing else
Symptom

The regression passes and software reports stalls. Measured over ten transactions against a 20-cycle budget:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  10 completed: max latency=25, over a budget of 20: 2
  mean = 82/10 = 8  <-- the mean hides both outliers
Root Cause

A latency target is a claim about the worst case and the mean describes the typical one. A mean of 8 against a budget of 20 reads as 60% headroom on a design where 20% of transactions exceeded the budget.

The deeper error is treating a performance requirement as an average. "Near CPU cache latency" is a bound, and a distribution meets a bound or it does not — the mean is not evidence either way.

Fix

Record the maximum and the frequency of excursions:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if (age_q > max_lat_q)    max_lat_q       <= age_q;
if (age_q > BUDGET)       n_over_budget_q <= n_over_budget_q + 1;

Prevention. Make the budget a goal property — (n_done_q > 0) |-> (n_over_budget_q == 0) — and label it as a goal rather than a correctness check, so a failure reads as "outside the envelope" rather than "broken". Also assert the maximum is monotonic: a max that can decrease is tracking the last value, which mutation P4 confirmed a plain maximum check misses.

5

An error flag is stubbed to zero and the regression stays green

DIAGNOSTIC-NEVER-TESTED-POSITIVE
Buggy Code
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Simplify: this condition never happens in our configuration.
assign latency_class_blocked_err = 1'b0;
Symptom

Nothing. Every test passes, and the system has lost its only indication that latency-critical traffic is being blocked by bulk traffic. The condition still occurs — measured, the shared configuration blocks both latency classes — and now nothing reports it.

Root Cause

The diagnostic was only ever verified negatively: tests confirmed it stayed low on the correct configuration. Nothing confirmed it went high on the configuration where the condition genuinely exists.

A flag observed only in its inactive state has not been verified. It is logic like any other, and stubbing it to a constant is a one-line change that a large regression will not notice — which is exactly what mutation P5 demonstrated.

Fix

Test the diagnostic positively, under the condition it names:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// The shared configuration is SUPPOSED to raise this here.
if (!shared_latency_blocked) begin
  $display("FAIL diagnostic did not fire when it should have");
  errors = errors + 1;
end

Prevention. Make it a rule: every error output needs at least one test that asserts it fires. Functional coverage on the flag's active state is the cheap version — an uncovered "flag asserted" bin is the same information, and it shows up in a coverage report rather than requiring someone to think of the mutation.

15. Verification Plan

Reference model. A per-class transaction model holding, for each outstanding tag, its class, its address and its issue cycle. Three things fall out: an ordering oracle (may this proceed given what is outstanding?), a matching oracle (does this response correspond to this request?), and a latency distribution per class. The scoreboard is keyed on class and tag together, because that pair is what actually identifies a transaction — as Debug Lab 2 measured.

Directed tests. Every class against every ordering condition (nothing outstanding, older to a different address, older to the same address); a response on the wrong class; a response for a tag never issued; a duplicate response; single-class floods against each of the other two; resource exhaustion per class; and a latency excursion that exceeds the budget.

Constrained-random dimensions. Class mix, address diversity (which drives the ordering-concurrency result directly), issue rate per class, response latency distribution per class, and return timing.

Functional coverage:

DimensionBins
class.io, .cache, .mem
ordering conditionnone outstanding, older different addr, older same addr
response outcomematched, wrong class, orphan, duplicate
per-class occupancy0, 1..N-1, N
latency vs budgetwell under, near, over
diagnostic flagseach error output observed both low AND high

The cross worth taking is class × ordering condition, because that is where the per-class policy lives and it is only nine bins. The bolded row is Debug Lab 5's lesson made mechanical: a coverage model that requires each error flag to be seen asserted turns "we never tested the diagnostic" into a visible hole.

Error injection. Wrong-class responses, orphan responses, duplicate responses, and a class held at full occupancy while another requests.

16. Design Review

On ordering. Does each class have its own rule, or does one rule cover all three? Is the safety property written against the address or against the policy — because a policy-shaped property is satisfied by blocking everything? What structure produces older_same_addr, and what is its depth and width, since that is where per-address ordering actually costs?

On matching. Is class stored alongside the tag? Do the classes share a tag space or have separate ones — and if separate, does anything match on tag alone? Are orphan and class-mismatch distinguishable? Is the mismatch wired to the decision or only to a status bit?

On resources. Are allocations per class or pooled? Was each class's depth derived from its own rate and latency? Is there a diagnostic for a latency class refused while its own entries are free? And what does the split cost the bulk class — because it does cost something, and the number should be known.

On latency. Is there a maximum, and a count of budget excursions? Is the budget expressed as a property? Is the maximum asserted monotonic?

On diagnostics. For each error output: is there a test that makes it fire? This is Debug Lab 5, and it is worth asking as a standing review question rather than a per-design one.

And the structural question. Where in this design do the three classes merge, and is that as late as it can be? Every point above the multiplexer where they share something is a point where one class's requirements have been imposed on another.

17. How This Appears in Real Engineering

CXL / protocol architect

The per-class divergence is the design. Ordering granularity, resource allocation and latency budget are three independent decisions that must be made three times, and the temptation to make them once is the main risk. The Consortium's near-CPU-cache-latency target for .cache and .mem is what makes the asymmetry non-negotiable.

RTL engineer

Four disciplines from the measured runs: give each class its own ordering rule; store class with tag and require both to match; allocate resources per class; and record maxima and budget excursions rather than means.

DV engineer

Two findings to carry. The scoreboard key is class-and-tag, not tag. And every error output needs a positive test — mutation P5 stubbed a diagnostic to zero and the entire regression stayed green until a test was added that required it to fire.

Performance engineer

Three numbers per class, not one aggregate: proceed-versus-blocked ratio (which measures whether ordering policy is costing concurrency), per-class occupancy against allocation, and the count of budget excursions. A mean latency figure is actively misleading for the two classes with a target — measured, a mean of 8 accompanied two excursions past a budget of 20.

Firmware and system software

.io ordering is what your driver relies on: a register write that arms followed by one that triggers must arrive in that order. .cache and .mem make no such promise across different addresses and do not need to — which is why memory barriers exist and why assuming global ordering from a coherent interconnect is a portability bug waiting for a faster machine.

Silicon debug

The transaction layer's most valuable counters are per-class blocked cycles and per-class budget excursions. Together they distinguish three different problems that present identically as "slow": an ordering policy that is too strict, a resource allocation that is too small, and a downstream latency that is too long.

18. Common Misconceptions

19. Interview Reasoning

20. Exercises

  1. Explain. The uniform ordering policy satisfies every safety property in Section 12 and is strictly worse. Name the property class that would catch it, and write one.

  2. Calculate. With 8 distinct addresses in flight and uniformly distributed requests, estimate the fraction of coherent requests blocked under per-address ordering. Then under global ordering. At what address diversity does the difference stop mattering?

  3. Calculate. .cache sustains 0.4 requests per cycle with a 30-cycle round trip; .io sustains 0.05 with a 400-cycle round trip. How many entries does each need? What single pool depth would serve both, and what does that tell you about pooling?

  4. RTL modification. Change class_ordering so .mem uses global ordering while .cache stays per-address. Predict the effect on the EXP1 counts, then verify. Which of Section 12's properties changes?

  5. DV task. Write the functional-coverage bin set that would have caught mutation P5 without anyone thinking of the mutation. How many bins, and what does it cost?

  6. Debug task. A design reports mean latency 12, max 14, budget excursions 0, against a budget of 20 — and software still reports stalls on coherent access. The latency counters are correct. Name two places the delay could be that this layer's instrumentation would not see.

21. Summary

The transaction layer is where meaning enters the stack, and meaning is what makes the three classes stop being interchangeable. Three contracts, one layer.

Ordering differs by class. .io needs producer/consumer order because software depends on it; .cache and .mem need ordering per address because coherence is a per-line property. Measured, applying one rule to all three let 0 of 5 requests proceed where the per-class policy let 2 — and the requests that differed were coherent traffic to different addresses, which is the common case. No safety property distinguishes the two policies.

A tag does not identify a transaction. With separate tag spaces per class, matching on tag alone retired a .mem request against a .cache response — and the broken matcher raised its mismatch flag while accepting the response, which is detection wired to a status bit instead of to the decision.

Resources must be per class, for two independent reasons. Coupling: measured, .io consumed all 18 shared entries and both latency-targeted classes were refused with zero entries used. And sizing: each class needs rate × latency entries, and the three products differ.

A latency target is a claim about the worst case. Measured, a mean of 8 against a budget of 20 accompanied a maximum of 25 and two excursions in ten transactions. The Consortium targets .cache and .mem at near CPU cache latency, and a mean is not evidence about a bound.

Two verification results carry forward. A diagnostic needs a positive test — mutation P5 stubbed an error flag to zero and the whole regression stayed green, because every test had only ever confirmed it stayed low. And a mutation that does not change behaviour tests nothing: the first version of P3 was itself a bad mutation, and replacing it with the realistic copy-paste bug exposed a real gap immediately.

22. What Comes Next

This chapter treated the three classes as a source of divergent requirements. Chapter 4.4 treats them as the subject: what each one actually is, side by side, and what a device implements when it implements one.

For adjacent material: Data Link Layer has the delivery guarantee this layer builds on, Coherent Communication Model has the per-line semantics that justify per-address ordering, and The CXL Device has the device-side view of which classes exist. The path is on the CXL tutorials index.

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.