Skip to content
VLSI Mentor

CXL · Module 15

Resource Discovery

Discovery is a walk over a fabric that changes while you are walking it. This chapter builds the walk, what a device is allowed to claim about itself, how an inventory goes stale, telling absent from slow, and the invariant that a published inventory is one the fabric actually held.

15.1 built the manager. 15.2 built the shape it manages.

Both assumed the manager knows what is out there. This chapter is where that knowledge comes from, and why it is harder than a scan.

1. The Engineering Problem — The Fabric Does Not Hold Still While You Walk It

Discovery sounds like a loop over slots. Five things make it not that.

A walk that skips is a walk that reports a smaller fabric with complete confidence. The property worth checking is not "did it find devices" but "did it visit every slot" — and those are different claims with different failure modes. Section 5 builds both.

Everything a device says about itself is a claim. Its type, its capacity, its capabilities. A manager that binds on an unverified claim binds capacity a host will fault on. Section 6 separates what was verified from what was asserted.

The fabric changes during the scan. A device arriving behind the cursor is missed; one leaving in front of it is recorded as present. Either way the published inventory describes a fabric that existed at no single instant. Section 8 builds the detection and the restart.

An inventory has an age, and age is not the only way it goes wrong. An inventory seconds old is already wrong if something changed; one hours old is still correct if nothing did. Section 10 makes freshness two conditions rather than one.

An element that does not answer is either absent or slow, and the two demand opposite responses. Section 11 builds the retry budget that separates them.

And one that surprises people: a device reachable by two paths is found twice. Counting it twice inflates the fabric's capacity by exactly the capacity that does not exist. Section 12 builds identity properly.

This chapter against 15.1, stated precisely. That chapter owns changing the fabric safely. This one owns knowing what the fabric contains before you change it. If a section here could be moved into 15.1 without loss, it is in the wrong chapter.

2. The One-Sentence Model

Discovery is a complete walk, over a fabric that is moving, producing a versioned inventory in which what was verified is recorded separately from what was claimed — and every failure below is one of complete, moving, versioned or verified missing.

3. What This Chapter Owns

GroundOwner
The manager's authority and configuration machinery15.1
The fabric's shape and its six properties15.2
Pool architecture: many hosts, many devices, one fabric12.1
Learning what is present, and how far that knowledge can be trustedthis chapter

Deferred:

Deferred groundOwner
Binding a discovered device to a host15.4
What composable infrastructure needs of all this15.5
Device internals and capability negotiationModules 20 and 21

4. Teaching-Model Boundary

Eight slots, four device types, four generations. A real fabric has hundreds of endpoints and discovery takes far longer relative to the rate of change — which makes every problem in this chapter worse, not better.

What is faithful: the completeness check, the claim-versus-verification split, the cursor-relative change detection, the two-condition freshness rule, the retry budget, identity by device rather than by path, and the generation on the published inventory.

What is not: the slot count, the timeout of four cycles, the capacities in gigabytes, and every width.

Every model is parameterised so the correct behaviour and a specific plausible failure are the same source under a different parameter, instantiated together, driven by one stimulus stream.

5. RTL 1 — A Walk That Visits Everything

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module enum_walk #(parameter int SKIP_SUBTREE = 0) (
  input  logic clk, rst_n,
  input  logic       start, step_en,
  input  logic [7:0] populated,        // which of 8 slots actually hold a device
  output logic [2:0] cursor,
  output logic [7:0] visited, found,
  output logic       walking, done,
  output logic       missed_err,       // a populated slot never visited
  output logic [7:0] n_walks, n_found, n_visited
);
  // The check that makes a walk trustworthy: after it finishes, every
  // populated slot must be in the VISITED set -- not merely in the found one.
  assign missed_err = done_q && ((populated & ~vis_q) != 8'd0);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      cur_q <= 3'd0; vis_q <= 8'd0; fnd_q <= 8'd0;
      walk_q <= 1'b0; done_q <= 1'b0;
      n_walks <= 8'd0; n_found <= 8'd0; n_visited <= 8'd0;
    end else if (start) begin
      // A new walk starts from nothing. Carrying the last walk's visited set
      // forward means the second scan of a shrunken fabric never notices.
      cur_q <= 3'd0; vis_q <= 8'd0; fnd_q <= 8'd0;
      walk_q <= 1'b1; done_q <= 1'b0;
      n_walks <= n_walks + 8'd1;
    end else if (walk_q && step_en) begin
      // SKIP_SUBTREE stops when the cursor reaches the second branch: a walk
      // that descends one subtree fully and never comes back for the other.
      if ((SKIP_SUBTREE != 0) && (cur_q == 3'd4)) begin
        walk_q <= 1'b0; done_q <= 1'b1;
      end else begin
        vis_q[cur_q] <= 1'b1;
        n_visited <= n_visited + 8'd1;
        if (populated[cur_q]) begin
          fnd_q[cur_q] <= 1'b1;
          n_found <= n_found + 8'd1;
        end
        if (cur_q == 3'd7) begin walk_q <= 1'b0; done_q <= 1'b1; end
        else cur_q <= cur_q + 3'd1;
      end
    end
  end
endmodule

Eight slots, four of them populated, two of those in the far half:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  walk    : visited=8 of 8 found=4 missed=0 | skipping walk visited=4 found=2

The skipping build reports itself finished. That is the whole point of the section: it sets done, it has a coherent found-set, and it is wrong about half the fabric. Nothing about its output says so except missed_err, which exists precisely because "the walk completed" and "the walk visited everything" are different facts.

Two details are load-bearing and each was a mutation that survived until the testbench was fixed:

  • The monitor must not fire mid-walk. During the walk, populated slots are legitimately unvisited. It is gated on done_q, and the testbench samples it two slots in to prove the gate is doing work.
  • A new walk starts from nothing. The stimulus runs a second walk over a different fabric — two devices where there were four — and asserts the visited and found sets begin empty. A walk that inherits the previous one's state never notices a fabric that shrank.
A sequence diagram with three lifelines: the fabric manager, a switch, and a device. The manager asks the switch to enumerate its ports. The switch reports which ports are populated. The manager then probes a specific device, which reports its own type and capacity. The manager separately verifies that capacity against what it can observe, and records only the verified amount. Finally the manager publishes the inventory with a generation number.Enumerate, identify, verify, publishmanagerswitchdeviceenumerate your portsthese are populatedidentify yourselftype and capacity --a CLAIMverify that capacitywhat is actuallyobservablepublish: generationN, verified only
Figure 1 — The claim and the verification are separate messages because they are separate facts. Section 6 records both and binds only the second.

6. RTL 2 — Believing What A Device Says About Itself

Finding a device is not knowing what it is.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module dev_identify #(parameter int TRUST_DEVICE = 0) (
  input  logic clk, rst_n,
  input  logic       identify,
  input  logic [2:0] rpt_type,      // 1 = mem, 2 = accel, 3 = switch
  input  logic [7:0] rpt_capacity,  // in GB, as the device reports it
  input  logic [7:0] observed_cap,  // what the manager can actually see
  output logic       ident_ok,
  output logic [7:0] accepted_cap,
  output logic       unknown_type_err,
  output logic       overclaim_err,
  output logic [7:0] n_ident, n_rejected, n_overclaim, total_cap
);
  logic known_type, capacity_ok;
  assign known_type  = (rpt_type == 3'd1) || (rpt_type == 3'd2) || (rpt_type == 3'd3);
  // A device claiming more capacity than the manager can observe is not an
  // error in the device's arithmetic -- it is capacity the fabric will bind
  // and a host will fault on.
  assign capacity_ok = (rpt_capacity != 8'd0) && (rpt_capacity <= observed_cap);
  assign ident_ok = identify && (TRUST_DEVICE != 0 || (known_type && capacity_ok));
  assign accepted_cap = (TRUST_DEVICE != 0) ? rpt_capacity
                      : ((rpt_capacity <= observed_cap) ? rpt_capacity : observed_cap);
  assign unknown_type_err = identify && !known_type;
  assign overclaim_err    = identify && (rpt_capacity > observed_cap);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_ident <= 8'd0; n_rejected <= 8'd0; n_overclaim <= 8'd0; total_cap <= 8'd0;
    end else if (identify) begin
      n_ident <= n_ident + 8'd1;
      // Capacity is accumulated only for a device that identified.
      if (ident_ok) total_cap <= total_cap + accepted_cap;
      else          n_rejected <= n_rejected + 8'd1;
      if (overclaim_err) n_overclaim <= n_overclaim + 8'd1;
    end
  end
endmodule

Five devices are presented: an honest one, an overclaiming one, a switch, an unknown type, and one reporting no capacity at all.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  identify: 5 seen, 3 refused, 80GB accepted | trusting build recorded 240GB, overclaim=1

80GB against 240GB. The trusting build's inventory is three times the size of the fabric, and every gigabyte of the difference is capacity a host will be given and will fault on.

Three separate refusal reasons are kept separate because they call for different responses:

ReasonWhat it meansWhat to do
Unknown typea device newer than the managerupdate the manager, do not bind
Overclaimreported capacity exceeds observablebind the observable amount, investigate
Zero capacitya device that identified but offers nothingnot a type problem — a broken device

All three are checked against an oracle written as three plain-integer rules with no reference to the design's expression, and the boundary case is driven explicitly: a device claiming exactly what is observable is accepted. <=, not <. One off here refuses every honest device that fills its capacity exactly.

7. Waveform — Nine Cycles Of A Walk Under Change

Transcribed from the printed trace. Both builds see one stimulus stream.

A fabric changing behind the cursor, and a walk that skips

9 cycles
A fabric changing behind the cursor, and a walk that skipsslot 1: already passedslot 1: already passedthe scan is dirtythe scan is dirtyslot 7: still aheadslot 7: still aheadno-rescan publishes anywayno-rescan publishes anywayclkcursor012345677visited012345678skipwalk012344444changebehindrescantornt0t1t2t3t4t5t6t7t8
Figure 2 — The visited row is the correct walk's slot count, climbing to 8; the skipwalk row is the skipping build's, which stops at 4 and never moves again. The change at cycle 6 is ahead of the cursor and harmless — the walk will reach it. The one at cycle 4 is behind and is not.

Read behind against change. Two changes, one cycle apart in effect, and only one of them matters — because only one of them landed where the walk had already been.

8. RTL 3 — The Fabric Changing Under The Walk

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module disc_change #(parameter int NO_RESCAN = 0) (
  input  logic clk, rst_n,
  input  logic       scanning,
  input  logic [2:0] cursor,
  input  logic       change_ev,
  input  logic [2:0] change_slot,
  input  logic       scan_done,
  output logic       change_behind, change_ahead,
  output logic       rescan_req,
  output logic       torn_inventory_err,
  output logic [7:0] n_changes, n_behind, n_rescans, n_torn
);
  logic dirty_q;
  // Behind the cursor is the dangerous half: the walk will not come back.
  assign change_behind = scanning && change_ev && (change_slot < cursor);
  assign change_ahead  = scanning && change_ev && (change_slot >= cursor);
  assign rescan_req    = dirty_q && (NO_RESCAN == 0);
  // An inventory published while a slot behind the cursor has changed is an
  // inventory of a fabric that never existed at any instant.
  assign torn_inventory_err = scan_done && dirty_q && (NO_RESCAN != 0);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      dirty_q <= 1'b0;
      n_changes <= 8'd0; n_behind <= 8'd0; n_rescans <= 8'd0; n_torn <= 8'd0;
    end else begin
      if (change_ev && scanning) n_changes <= n_changes + 8'd1;
      if (change_behind) begin
        dirty_q <= 1'b1;
        n_behind <= n_behind + 8'd1;
      end
      if (scan_done) begin
        if (torn_inventory_err) n_torn <= n_torn + 8'd1;
        if (rescan_req)         n_rescans <= n_rescans + 8'd1;
        // Cleared, or every scan after a dirty one restarts forever.
        dirty_q <= 1'b0;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  change  : behind=1 ahead=1 rescans=1 | no-rescan build published a torn inventory=1

Three boundaries, all of them driven:

  • The slot at the cursor is not behind it. <, not <=. That slot is being visited now, so a change there is seen. A comparison one off here restarts a scan for every change that the scan was about to pick up anyway.
  • A change outside a scan is neither behind nor ahead. There is no cursor to be relative to. The testbench drives a change with scanning low and asserts both outputs stay clear.
  • A clean scan is clean. After the dirty scan restarts, the next one runs undisturbed and requests no rescan. Without that check, a dirty flag that is never cleared makes every subsequent scan restart forever.

The word for what NO_RESCAN publishes is torn: an inventory assembled from slots read at different times, describing a fabric that held that configuration at no single instant. It is not "slightly out of date". Section 15 makes not publishing one the assembled machine's invariant.

9. RTL 4 — What A Device May Claim Versus What May Be Bound

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module claim_check #(parameter int MERGE_CLAIMS = 0) (
  input  logic clk, rst_n,
  input  logic       assess,
  input  logic [7:0] claimed_cap, verified_cap,
  input  logic       claims_coherent, verified_coherent,
  output logic [7:0] bindable_cap,
  output logic       bindable_coherent,
  output logic       unverified_bind_err,
  output logic [7:0] n_assessed, n_gaps, total_bindable, total_claimed
);
  // What may be bound is what was verified.
  assign bindable_cap = (MERGE_CLAIMS != 0) ? claimed_cap
                      : ((claimed_cap <= verified_cap) ? claimed_cap : verified_cap);
  assign bindable_coherent = (MERGE_CLAIMS != 0) ? claims_coherent
                                                 : verified_coherent;
  assign unverified_bind_err = assess &&
    ((bindable_cap > verified_cap) || (bindable_coherent && !verified_coherent));
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_assessed <= 8'd0; n_gaps <= 8'd0;
      total_bindable <= 8'd0; total_claimed <= 8'd0;
    end else if (assess) begin
      n_assessed     <= n_assessed + 8'd1;
      // Both numbers are recorded. Only the first may be bound.
      total_bindable <= total_bindable + bindable_cap;
      total_claimed  <= total_claimed + claimed_cap;
      if (claimed_cap != verified_cap) n_gaps <= n_gaps + 8'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  claims  : bindable=160GB of 224GB claimed, gaps=1 | merging build binds 224GB, err=1

The section exists to make one distinction structural: the inventory records both numbers, and only one of them may be bound.

n_gaps counts the devices where they differ, and that count is more useful than either total. A fabric where claim and verification always agree needs no policy; a fabric where they diverge on one device in twenty has something systematic going on, and the count is what surfaces it.

The coherency half of the check is the one worth naming. A device claiming it supports coherent access, bound into a coherency domain on that claim alone, is 13.3's problem arriving through a door that chapter did not guard. The check has two halves and the testbench drives each independently, because a mutation removing either one leaves the other passing.

10. RTL 5 — An Inventory Going Out Of Date

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module inv_freshness #(parameter int NO_EXPIRY = 0) (
  input  logic clk, rst_n,
  input  logic       scan_complete,
  input  logic       fabric_event,     // something changed out there
  input  logic       use_inventory,    // the manager acts on it
  output logic [7:0] age, events_since,
  output logic       fresh,
  output logic       stale_use_err,
  output logic [7:0] n_uses, n_stale_uses, max_age, max_events
);
  localparam logic [7:0] MAX_AGE = 8'd8;
  logic [7:0] age_q, ev_q;
  assign age          = age_q;
  assign events_since = ev_q;
  // Fresh is TWO conditions. An inventory can be seconds old and already
  // wrong, or hours old and still correct.
  assign fresh = (NO_EXPIRY != 0) || ((age_q < MAX_AGE) && (ev_q == 8'd0));
  assign stale_use_err = use_inventory && !fresh;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      age_q <= 8'd0; ev_q <= 8'd0;
      n_uses <= 8'd0; n_stale_uses <= 8'd0; max_age <= 8'd0; max_events <= 8'd0;
    end else begin
      if (scan_complete) begin
        // A completed scan clears BOTH -- the events are now accounted for.
        age_q <= 8'd0; ev_q <= 8'd0;
      end else begin
        age_q <= age_q + 8'd1;
        if (age_q + 8'd1 > max_age) max_age <= age_q + 8'd1;
        if (fabric_event) begin
          ev_q <= ev_q + 8'd1;
          if (ev_q + 8'd1 > max_events) max_events <= ev_q + 8'd1;
        end
      end
      if (use_inventory) begin
        n_uses <= n_uses + 8'd1;
        if (!fresh) n_stale_uses <= n_stale_uses + 8'd1;
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  freshness: fresh=1 stale by age=1 stale by event=1 | no-expiry build stale uses=0 of 3

Both staleness paths are driven separately. The inventory expires on age with no events at all, and it expires on a single event while still young. Neither condition subsumes the other, and a design with only one of them is wrong in a different direction:

  • Age only: a fabric that changed a millisecond after the scan is trusted for the full window.
  • Events only: an inventory nobody refreshed is trusted forever as long as nothing was noticed.

The monitor fires on the use, not on the age. An old inventory nobody consulted has misled nothing, and the testbench asserts exactly that — fresh low and stale_use_err low at the same instant. That mutation survived until the check was added, and the distinction is the difference between a metric that alarms on real harm and one that alarms on a timer.

A flowchart deciding whether an inventory is fresh. Starting from a read of the inventory, the first decision asks whether it is older than the age limit; if so it is stale because it is too old. If not, a second decision asks whether anything in the fabric has changed since the scan; if so it is stale because the fabric changed. Only an inventory that is both young and undisturbed is fresh.yesnoyesnoa read of the inventoryolder than theage limit?anything changedsince the scan?stale: too oldstale: changedfresh
Figure 3 — Two independent conditions. An inventory seconds old fails on the right-hand branch; one hours old and undisturbed passes both. Neither condition subsumes the other, and a design with only one is wrong in a nameable direction.

A completed scan clears both the age and the event count, and the testbench accumulates two events before the scan to prove the second half happens.

11. RTL 6 — Absent, Or Merely Slow

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module probe_timeout #(parameter int NO_RETRY = 0) (
  input  logic clk, rst_n,
  input  logic       probe, response,
  output logic       answered, timed_out, retrying,
  output logic       declared_absent,
  output logic       premature_absent_err,
  output logic [7:0] wait_q_o, attempts, n_answered, n_absent, max_wait
);
  localparam logic [7:0] TIMEOUT = 8'd4;
  localparam logic [7:0] RETRIES = 8'd2;
  logic [7:0] w_q, try_q;
  logic       pending_q;
  assign wait_q_o = w_q;
  assign attempts = try_q;
  assign answered = pending_q && response;
  assign timed_out = pending_q && (w_q >= TIMEOUT);
  assign retrying  = timed_out && (NO_RETRY == 0) && (try_q < RETRIES);
  // Absent is a CONCLUSION, and it needs every retry spent before it is drawn.
  assign declared_absent = timed_out && !retrying;
  assign premature_absent_err = declared_absent && (try_q < RETRIES);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      w_q <= 8'd0; try_q <= 8'd0; pending_q <= 1'b0;
      n_answered <= 8'd0; n_absent <= 8'd0; max_wait <= 8'd0;
    end else begin
      // A probe request while one is outstanding must not restart it, or the
      // wait never expires and a slow device is re-probed forever.
      if (probe && !pending_q) begin
        pending_q <= 1'b1; w_q <= 8'd0; try_q <= 8'd0;
      end else if (pending_q) begin
        if (response) begin
          pending_q <= 1'b0;
          n_answered <= n_answered + 8'd1;
        end else if (w_q >= TIMEOUT) begin
          if (retrying) begin
            // The retry starts its wait again, or the budget is spent at once.
            try_q <= try_q + 8'd1; w_q <= 8'd0;
          end else begin
            pending_q <= 1'b0;
            n_absent <= n_absent + 8'd1;
          end
        end else begin
          w_q <= w_q + 8'd1;
          if (w_q + 8'd1 > max_wait) max_wait <= w_q + 8'd1;
        end
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  timeout  : waited=4 retried=2 absent=1 | no-retry build declared absent early=1

The NO_RETRY build declares the device absent on its first timeout, with both retries unspent. A device that was merely slow is removed from the fabric, and everything downstream — allocation, binding, capacity planning — proceeds on an inventory missing a device that is sitting right there.

A state machine for probing an element. From idle, a probe makes the state pending. From pending, a response moves to found. If no answer arrives within the timeout, the state moves to timed out. From timed out, if retries remain the probe returns to pending with its wait restarted; only once every retry is spent does it move to absent.idlependt/oabsentfoundprobe sentprobe sentansweredansweredno answerno answerretryretryretries spentretries spent
Figure 4 — Absent is a conclusion, not an observation. The edge from timed-out back to pending is what separates a slow element from a missing one, and the build without it goes straight to absent on the first timeout.

Three properties the testbench had to be strengthened to cover, each a mutation that survived:

  • A probe request while one is outstanding does not restart it. Otherwise a slow device is re-probed forever and never concluded absent — the timeout never expires because the wait keeps resetting. The stimulus asserts a second probe at wait 2 and checks the wait reaches 3, not 0.
  • A retry restarts the wait. A retry that inherits the expired wait times out again immediately, which spends the whole retry budget in three cycles and produces the same premature absence by a different route.
  • An answer closes the probe. The stimulus lets a device answer late, then runs sixteen idle cycles — long enough for every retry an unclosed probe would have spent — and asserts nothing times out and nothing is declared absent.

12. RTL 7 — One Device, Two Paths

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module dup_detect #(parameter int PATH_KEYED = 0) (
  input  logic clk, rst_n,
  input  logic        record,
  input  logic [3:0]  dev_uid,       // the device's own identity
  input  logic [1:0]  via_port,      // the path it was found through
  input  logic [7:0]  capacity,
  output logic        is_new, is_dup,
  output logic        double_count_err,
  output logic [15:0] seen_mask,
  output logic [7:0]  n_records, n_unique, n_dups, total_cap
);
  logic [15:0] seen_q, key_bit;
  // Identity is the DEVICE, not the path it was found through.
  assign key_bit = (PATH_KEYED != 0)
                 ? (16'd1 << {2'd0, dev_uid[1:0], via_port})
                 : (16'd1 << {12'd0, dev_uid});
  assign is_dup  = record && ((seen_q & key_bit) != 16'd0);
  assign is_new  = record && !is_dup;
  assign double_count_err = is_new && ((seen_q & (16'd1 << {12'd0, dev_uid})) != 16'd0);
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      seen_q <= 16'd0;
      n_records <= 8'd0; n_unique <= 8'd0; n_dups <= 8'd0; total_cap <= 8'd0;
    end else if (record) begin
      n_records <= n_records + 8'd1;
      if (is_new) begin
        // Both keys are recorded: the path it came in on, and who it IS.
        seen_q <= seen_q | key_bit | (16'd1 << {12'd0, dev_uid});
        n_unique <= n_unique + 8'd1;
        total_cap <= total_cap + capacity;
      end else n_dups <= n_dups + 8'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  duplicate: 2 unique 96GB from 4 sightings | path-keyed build 3 unique 160GB, double-count=1

Same four sightings. One fabric has 96GB in it and the path-keyed build's inventory says 160GB.

The failure is silent all the way until something is bound to the half that does not exist, and then it presents as a device that "lost" capacity — which sends everyone to the device.

double_count_err is the chapter's unreachable monitor. In the correct build is_new implies the device's identity bit is clear, so it cannot fire; testing it required the PATH_KEYED build, and sampling it in the delta before the edge that records the sighting. That is the general shape from 15.1 section 18 again: a monitor that says nothing was counted twice will survive every mutation until you build something that counts twice.

The seen-set is also asserted to remember. The stimulus records the first device again after two others have been recorded, and checks it is still a duplicate. A seen-set overwritten rather than accumulated passes every two-sighting test there is.

13. RTL 8 — What A Scan Costs The Fabric

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module scan_cost (
  input  logic clk, rst_n,
  input  logic        full_scan_done, incr_scan_done,
  input  logic [7:0]  full_cycles, incr_cycles,
  input  logic        fabric_cycle,
  output logic [15:0] n_full, n_incr, total_scan, n_fabric,
  output logic [7:0]  mean_full, mean_incr, scan_share_pct, incr_saving_pct
);
  logic [31:0] weighted_scan, weighted_save;
  logic [15:0] full_total, incr_total;
  assign weighted_scan = {16'd0, total_scan} * 32'd100;
  assign scan_share_pct = (n_fabric == 16'd0) ? 8'd0
                        : (weighted_scan / {16'd0, n_fabric});
  assign mean_full = (n_full == 16'd0) ? 8'd0 : (full_total / n_full);
  assign mean_incr = (n_incr == 16'd0) ? 8'd0 : (incr_total / n_incr);
  // What the incremental scan saves, as a share of the full one.
  assign weighted_save = ({24'd0, mean_full} - {24'd0, mean_incr}) * 32'd100;
  assign incr_saving_pct = (mean_full == 8'd0) ? 8'd0
                         : (weighted_save / {24'd0, mean_full});
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      n_full <= 16'd0; n_incr <= 16'd0; total_scan <= 16'd0; n_fabric <= 16'd0;
      full_total <= 16'd0; incr_total <= 16'd0;
    end else begin
      if (full_scan_done) begin
        n_full     <= n_full + 16'd1;
        full_total <= full_total + {8'd0, full_cycles};
        total_scan <= total_scan + {8'd0, full_cycles};
      end
      if (incr_scan_done) begin
        n_incr     <= n_incr + 16'd1;
        incr_total <= incr_total + {8'd0, incr_cycles};
        total_scan <= total_scan + {8'd0, incr_cycles};
      end
      if (fabric_cycle) n_fabric <= n_fabric + 16'd1;
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  cost    : full mean=18 incr mean=3 | scanning was 42% of fabric time, incremental saves 83%

Two numbers, two different arguments.

83 percent is the case for incremental discovery, and it is the number that gets quoted. 42 percent is the case for discovering less often, and it is the one that matters more: the fabric spent nearly half its time being scanned. Reducing the cost of each scan does not help nearly as much as scanning when something changed rather than on a timer — which is exactly what section 10's event counter enables.

Both guards return 0, not 100, on an empty sample, and both are asserted before any scan is driven. With no fabric time measured the scan share is not 100 percent, and with no full scan ever run there is no baseline for the incremental one to have saved anything against.

14. RTL 9 — The Inventory As A Versioned Structure

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module inventory #(parameter int NO_GENERATION = 0) (
  input  logic clk, rst_n,
  input  logic       publish, read_inv,
  input  logic [3:0] reader_gen,     // the generation the reader last saw
  input  logic [7:0] new_count,
  output logic [3:0] generation,
  output logic [7:0] dev_count,
  output logic       read_current,
  output logic       stale_read_err,
  output logic [7:0] n_publishes, n_reads, n_stale_reads, max_lag
);
  logic [3:0] gen_q, lag;
  logic [7:0] cnt_q;
  assign generation = gen_q;
  assign dev_count  = cnt_q;
  assign lag = gen_q - reader_gen;
  // A reader holding an older generation is acting on a fabric that has been
  // re-enumerated since. NO_GENERATION cannot tell.
  assign read_current   = (NO_GENERATION != 0) || (reader_gen == gen_q);
  assign stale_read_err = read_inv && !read_current;
 
  always_ff @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
      gen_q <= 4'd0; cnt_q <= 8'd0;
      n_publishes <= 8'd0; n_reads <= 8'd0; n_stale_reads <= 8'd0; max_lag <= 8'd0;
    end else begin
      if (publish) begin
        gen_q <= gen_q + 4'd1;
        cnt_q <= new_count;
        n_publishes <= n_publishes + 8'd1;
      end
      if (read_inv) begin
        n_reads <= n_reads + 8'd1;
        if (!read_current) begin
          n_stale_reads <= n_stale_reads + 8'd1;
          // One generation behind is a race; several is a consumer that
          // stopped refreshing.
          if ({4'd0, lag} > max_lag) max_lag <= {4'd0, lag};
        end
      end
    end
  end
endmodule
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  version : gen=4 stale reads=2 worst lag=3 | unversioned build stale reads=0

The lag is what makes this more than a boolean, and its two values in the transcript mean different things:

  • One generation behind is a race. The inventory was republished between the reader taking it and acting on it, and that will happen occasionally in any system.
  • Three generations behind is a consumer that stopped refreshing. It is not a race; something is holding a reference and never checking it, and it will keep making decisions on that view indefinitely.

The peak is latched, and the testbench brings the reader up to date and asserts the latched worst case does not fall.

As in section 10, the monitor fires on the read, not on the lag. A reader that is behind and does not read has misled nothing, and the testbench asserts read_current low with stale_read_err low simultaneously.

15. RTL 10 — Discovery Assembled

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
module disc_top #(parameter int PUBLISH_DIRTY = 0) (
  input  logic clk, rst_n,
  input  logic       start, walk_done, ident_done, verify_done,
  input  logic       fabric_changed,
  output logic [2:0] phase,     // 0 idle 1 walk 2 identify 3 verify 4 publish
  output logic       publishing, dirty,
  output logic       published_dirty_err,
  output logic [7:0] n_published, n_restarted, scan_cycles, max_scan
);
  assign publishing = (ph_q == PUB);
  // Publishing an inventory built across a change is publishing a fabric that
  // never existed at any single instant.
  assign published_dirty_err = publishing && dirty_q && (PUBLISH_DIRTY != 0);
  ...
      if (fabric_changed && (ph_q != IDLE)) dirty_q <= 1'b1;
      case (ph_q)
        IDLE:   if (start) begin ph_q <= WALK; sc_q <= 8'd0; dirty_q <= 1'b0; end
        WALK:   if (walk_done)   ph_q <= IDENT;
        IDENT:  if (ident_done)  ph_q <= VERIFY;
        VERIFY: if (verify_done) begin
                  if (dirty_q && (PUBLISH_DIRTY == 0)) begin
                    ph_q <= WALK; sc_q <= 8'd0; dirty_q <= 1'b0;
                    n_restarted <= n_restarted + 8'd1;
                  end else ph_q <= PUB;
                end
        PUB:    begin ph_q <= IDLE; n_published <= n_published + 8'd1; end
        default: ph_q <= IDLE;
      endcase

And the interval measurement running underneath it:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
      if (ph_q != IDLE) begin
        sc_q <= sc_q + 8'd1;
        if (sc_q + 8'd1 > max_scan) max_scan <= sc_q + 8'd1;
      end
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  assembled: restarted=1 published=1 longest scan=8 | dirty-publish build err=1

The fabric changes during the verify phase. The correct build throws the scan away and starts again; the other publishes what it has. One stimulus, one parameter apart.

Every guard in the case statement is held open by the testbench to prove it is load-bearing: the machine sits in WALK until the walk finishes, in IDENT until identification does. And the restart clears the dirty flag, which the testbench checks — a restart that inherits the flag restarts forever, and a discovery that never completes is worse than one that publishes something slightly wrong.

The cost is measured, not assumed: the longest scan was eight cycles, latched, and the restart doubled the work for that round.

16. Quantitative Reasoning

QuantityValue, and where it comes from
Slots visited by a complete walk8 of 8 — the completeness check
Devices found4 — of eight slots
Slots visited by the skipping walk4 — and it reports itself finished
Devices it found2 — half the fabric, full confidence
Devices presented for identification5 — honest, overclaim, switch, unknown, zero
Refused3 — three different reasons
Capacity accepted80GB — verified
Capacity the trusting build recorded240GB — three times the fabric
Changes during the scan3 — two ahead, one behind
Changes behind the cursor1 — the only one that matters
Rescans requested1 — by the correct build
Torn inventories published0 / 1 — correct build / no-rescan build
Capacity claimed across three devices224GB — asserted
Capacity bindable160GB — verified
Devices where the two differed1 — the count that surfaces a pattern
Inventory age limit8 cycles — one of two freshness conditions
Stale uses, correct build2 of 3 — one by age, one by event
Stale uses, no-expiry build0 — it cannot tell
Probe timeout4 cycles — per attempt
Retries before declaring absence2 — spent in full
Premature absences, no-retry build1 — a slow device removed
Sightings recorded4 — two of them duplicates
Unique devices, correct2 (96GB) — identity by device
Unique devices, path-keyed3 (160GB) — identity by path
Mean full scan18 cycles — two samples
Mean incremental scan3 cycles — two samples
Share of fabric time spent scanning42% — the cost imposed
Saving from incremental scanning83% — per scan
Inventory generations published4 — versioned
Stale reads2 — worst lag 3
Scans restarted1 — rather than published torn

Three worth a sentence.

80GB against 240GB. One parameter apart, same five devices. Every gigabyte of the difference is capacity something will be allocated and will fault on.

42 percent against 83 percent. The saving per scan is the number that gets quoted; the share of fabric time is the number that should drive the decision, and it argues for scanning less often rather than scanning faster.

Lag 1 against lag 3. Same counter, two different diagnoses — a race, and a consumer that stopped refreshing. Without the lag, both read as "stale".

17. Assertions

Every property is an immediate check written as cond !== 1'b1 and sampled after a settle.

#PropertyModel
1The walk starts with nothing visitedwalk
2Mid-walk, unvisited populated slots are not "missed"walk
3A complete walk visits every slotwalk
4And finds exactly the populated oneswalk
5With nothing missedwalk
6The skipping walk reports itself finishedwalk
7Having visited only the first halfwalk
8And found half the deviceswalk
9Which is a populated slot it never visitedwalk
10A second walk starts from nothingwalk
11And finds exactly what is there nowwalk
12Identification matches an independent oracleidentify
13A verified claim is bindable in fullidentify
14A device claiming more than is observable is refusedidentify
15And the manager takes what it can seeidentify
16The trusting build records capacity that does not existidentify
17A switch is a type the fabric recognisesidentify
18An unrecognised type is refused and namedidentify
19A zero-capacity device is refused — not a type problemidentify
20Only verified capacity is accumulatedidentify
21A change ahead of the cursor is still to be visitedchange
22A change behind it has already been passedchange
23The slot at the cursor is not behind itchange
24A change outside a scan is neitherchange
25A dirty scan must be repeatedchange
26The no-rescan build publishes a torn inventorychange
27A second, undisturbed scan starts cleanchange
28The bindable capacity matches an independent oracleclaims
29Only what was verified may be boundclaims
30The merging build binds the claimclaims
31An unverified coherency claim is not bindableclaims
32Both halves of the check fire independentlyclaims
33Claimed and bindable totals are recorded separatelyclaims
34A fresh scan is age zero with no eventsfreshness
35Freshness matches an independent two-condition oraclefreshness
36It expires on age with no events at allfreshness
37And on an event while still youngfreshness
38A stale inventory nobody reads has misled nothingfreshness
39A completed scan clears the age and the eventsfreshness
40The no-expiry build sees nothing wrongfreshness
41A probe's wait counts uptimeout
42A second probe does not restart an outstanding onetimeout
43Four cycles with no answer is a timeouttimeout
44The correct build retries rather than concludingtimeout
45A retry starts its wait again from the beginningtimeout
46The no-retry build declares absence with retries unspenttimeout
47Absence is concluded only after every retrytimeout
48A device answering late is found, not removedtimeout
49And an answered probe never times out latertimeout
50The first sighting of a device is newduplicate
51The same device down a second path is a duplicateduplicate
52Its capacity is not added againduplicate
53The path-keyed build counts it twiceduplicate
54And is about to double-count before the edge that records itduplicate
55A different device is a different deviceduplicate
56A device seen earlier is still rememberedduplicate
57Before any fabric time the scan share is 0, not 100cost
58With no full scan there is no baseline savingcost
59Full and incremental means divide by their own countscost
60The scan share is measured against fabric timecost
61The first published inventory is generation 1inventory
62A reader holding the current generation is currentinventory
63A reader one behind is stale — a raceinventory
64A reader three behind stopped refreshinginventory
65A stale view nobody reads has misled nothinginventory
66A refreshed reader is current againinventory
67Without lowering the latched worst laginventory
68The unversioned build cannot tellinventory
69The assembled scan waits in each gated phaseassembled
70A change mid-scan marks it dirtyassembled
71The correct build restarts rather than publishingassembled
72With the scan clean againassembled
73The other build publishes a torn inventoryassembled
74The clean scan reaches publishassembled
75And the longest scan was latchedassembled

18. Mutation Testing

110 mutations, one at a time, each required to make the baseline print RESULT: FAIL.

110 of 110 were killed.

The first run killed 94 and left 16 survivors. The classification:

ClassCountThe fix
Stimulus gap11drive the case
Unreachable checker2sample the broken build, in the right delta
Provably equivalent1replace the mutation
Second-order state gap2run the thing twice

The last row is new in this chapter and worth naming. Two mutations — a walk that does not clear its visited set, and a dirty flag that is never cleared — are invisible to any test that runs the machinery once. Both need a second scan to expose, and both are exactly the kind of defect that survives bring-up and appears in production, because bring-up scans once and production scans forever.

The two unreachable checkers were double_count_err and, in a subtler way, the timing of its sample. Getting it observable required both the PATH_KEYED build and sampling in the delta before the recording edge — after the edge, the sighting is already recorded and is_new has gone low. A monitor can be reachable and still unobservable if you look one cycle late.

A representative sample:

MutationResult
A missed slot is never flaggedKILLED
The missed test fires before the walk finishesKILLED
A new walk does not clear the visited setKILLED
The walk stops one slot earlyKILLED
An empty slot is recorded as holding a deviceKILLED
The skipping build walks the whole fabricKILLED
Any type is a known typeKILLED
A device may report more than is observableKILLED
The capacity test is exclusive at the boundaryKILLED
The manager records the reported capacityKILLED
Capacity is recorded for a refused deviceKILLED
Behind and ahead are the wrong way roundKILLED
The slot at the cursor counts as behind itKILLED
Changes are detected outside a scan tooKILLED
The dirty flag is never clearedKILLED
Age alone never expires the inventoryKILLED
An event does not make it staleKILLED
Staleness is flagged without a useKILLED
A completed scan does not clear the eventsKILLED
A device is declared absent while retries remainKILLED
A retry does not reset the waitKILLED
A new probe restarts one already outstandingKILLED
A response does not clear the probeKILLED
Identity includes the path it was found throughKILLED
The seen set is overwritten each timeKILLED
A double count is never flaggedKILLED
The scan share is measured against the scanningKILLED
The no-baseline guard returns 100 not 0KILLED
The lag is computed the wrong way roundKILLED
A dirty inventory is published anywayKILLED

19. Verification Strategy

Parameterised twin builds. SKIP_SUBTREE, TRUST_DEVICE, NO_RESCAN, MERGE_CLAIMS, NO_EXPIRY, NO_RETRY, PATH_KEYED, NO_GENERATION, PUBLISH_DIRTY. Nine failures, each the same source under one parameter, each measured against a correct build driven by the identical stimulus.

Independent oracles. Identification (three rules), freshness (two conditions) and bindable capacity (the smaller of two) are each checked against a plain-integer function that shares no structure with the design's expression.

Run it twice. A second walk over a different fabric, a second scan after a dirty one, a fourth sighting of the first device. Three distinct state-reset defects live only in the second run.

Sample in the right delta. double_count_err is asserted in the cycle before the edge that records the sighting. Sampled after, the sighting is recorded and the monitor has correctly gone low — the check would pass on a broken design.

Fire on the harm, not on the condition. Both stale_use_err and stale_read_err are gated on an actual use or read, and both are asserted low while their underlying condition is true. A stale inventory nobody consulted has misled nothing, and a monitor that alarms on the condition alone alarms constantly and gets disabled.

Boundaries in both directions. A device claiming exactly what is observable is accepted. The slot at the cursor is ahead, not behind. An empty sample returns 0, not 100.

Delta discipline. Every combinational sample follows a settle.

20. Synthesis and Implementation Reality

Most of this chapter is firmware, and being precise about which parts are not is the useful content.

The walk is software. A cursor over a slot list, a visited bitmask, a completeness check at the end. It runs on the fabric manager, and the reason it appears here as RTL is that expressing it as executable, mutation-tested logic is the only way to make "the scan was complete" a claim that can be wrong.

The completeness check should nonetheless be code, not a comment. populated & ~visited is one instruction. It is the difference between a discovery that can report a fabric half its real size and one that cannot, and it is routinely absent because a walk that finishes feels like a walk that worked.

The claim-versus-verification split is a data-structure decision made once and very hard to retrofit. If the inventory has one capacity field, every consumer downstream has already lost the distinction, and adding it later means auditing every reader. Two fields from the start costs a word per device.

The generation counter is the cheapest thing in the chapter and the one that makes every stale-view bug diagnosable rather than mysterious. Four bits, and every consumer records which generation it acted on.

What is genuinely hardware is thin: the timers that produce a probe timeout, and the counters. A real timeout is orders of magnitude longer than four cycles and is a programmable register, because the right value depends on the device and nobody knows it at design time.

The divisions are not synthesisable as written, and here that matters less than usual — every ratio in this chapter is computed by the manager's software anyway.

21. Silicon Observability

SignalWhy it is worth recording
missed_err per scana scan that finished without visiting everything
n_visited against slot countthe completeness ratio, per scan
n_overclaimdevices reporting more than is observable
n_gapsdevices where claim and verification differ at all
n_behindchanges that landed behind the cursor
n_restartedscans thrown away — the real cost of a moving fabric
n_torninventories published across a change
max_age, max_eventshow stale an inventory was allowed to get
n_stale_usesdecisions made on a stale view
max_wait, n_absentprobe latency, and devices concluded missing
premature_absent_errdevices removed with retries unspent
n_dupsdevices found down more than one path
double_count_errcapacity counted twice
max_laghow far behind the worst consumer got
scan_share_pctfabric time spent being discovered

Three to alarm on.

n_overclaim non-zero means a device is misreporting itself. It may be benign, and it may be the first sign of a device that will fault when the claimed region is touched — but either way, the manager caught it and something should know.

max_lag greater than one is a consumer that stopped refreshing. One is a race; more is a component holding a reference forever, and it will keep deciding on that view until someone notices.

premature_absent_err non-zero means a working device was removed from the fabric. Everything downstream will be correct about an inventory that is missing something.

22. Debug Lab

22.1 The manager reports fewer devices than are installed

Symptom. Discovery completes successfully. Some installed devices do not appear.

The wrong move. Debug the missing devices. They may be fine.

The signature. missed_err and n_visited.

ReadingDiagnosis
missed_err set, n_visited below the slot countthe walk is incomplete — it never looked
n_visited complete, devices still missingthe walk looked; identification refused them
n_visited complete, n_absent non-zerothey did not answer in time
n_visited complete, n_dups unexpectedly highthey were found and folded into another device

Four causes, four different investigations, and the first two are distinguished by one counter. Without it, "a device is missing" sends everyone to the device, and in the first row the device was never asked anything at all.

22.2 A host was given memory that faults when touched

Symptom. A host is allocated capacity, and accesses beyond some offset fault.

The reading. n_overclaim and the gap between claimed and bindable in the inventory.

The diagnosis. The device reported more capacity than the manager could verify, and something bound the claim rather than the verification. The offset where faults begin is the verified capacity.

Why the counter matters more than the fault. The fault happens at allocation time plus however long until a workload reaches that offset — potentially days. n_overclaim fires at discovery time, and it names the device.

22.3 The fabric's total capacity is wrong and no device is wrong

Symptom. Summed capacity across the inventory exceeds what is installed. Every individual device reports correctly.

The reading. n_dups, n_unique against the physical device count, and double_count_err.

The diagnosis. A device reachable by two paths was recorded twice. Its own reporting is perfectly correct; the inventory keyed it by path rather than by identity.

The confirmation. n_unique exceeds the number of physical devices. That comparison is the whole diagnosis, and it needs a source of truth outside the inventory — which is why the physical device count belongs in the fabric's configuration rather than being derived from a scan.

22.4 Allocations keep landing on devices that have moved

Symptom. The manager binds a device to a host and the binding fails, or lands on the wrong resource. Re-running the operation works.

The reading. n_stale_uses, max_lag, and max_events.

The diagnosis, in three sub-cases.

If max_lag is 1, this is a race: the inventory was republished between a consumer reading it and acting on it. The fix is re-validating the generation at the point of action, not at the point of read.

If max_lag is large, a consumer stopped refreshing and is deciding on a view several generations old. The fix is in that consumer.

If max_events is large while max_age is small, the fabric is changing faster than the scan interval and every inventory is stale on arrival. That is not a consumer problem and no consumer-side fix will touch it — the discovery cadence has to follow the events rather than a timer, and section 13's 42 percent is what that costs if you simply scan more often instead.

23. Design Review

1. Does the scan verify that it visited every slot, or only that it finished? One instruction apart, and completely different guarantees.

2. Does the inventory record what a device claimed separately from what was verified? If there is one capacity field, the distinction was lost at the first write and every consumer inherited the loss.

3. What happens if the fabric changes during a scan? "It gets picked up next time" is only true for changes ahead of the cursor.

4. Can a published inventory describe a state the fabric never held? If a change behind the cursor does not force a restart, yes.

5. How does the inventory expire — by age, by event, or both? Either alone is wrong in a specific, nameable direction.

6. Does the staleness monitor fire on the age, or on a use? Firing on the age alarms constantly and gets turned off.

7. How many times is an element probed before it is declared absent, and does each retry restart its wait? Both, or a slow device is removed as missing.

8. Is a device identified by what it is, or by where it was found? Two paths to one device is not exotic in a redundant fabric — it is what redundancy means.

9. Does the inventory carry a generation, and does every consumer record which one it acted on? Four bits, and it is the difference between a diagnosable stale-view bug and a mysterious one.

10. What fraction of fabric time is spent scanning, and is discovery triggered by events or by a timer? The second question is usually the answer to the first.

24. How This Appears In Real Engineering

Discovery is written for a static fabric and then deployed on a dynamic one. Every model in this chapter is trivial if nothing moves. The whole difficulty is that composable infrastructure means things move by design, and the discovery code usually predates that requirement.

The claim/verification split is the one that gets collapsed. It is two fields instead of one, and the second is only interesting when they differ — which is rarely, until a device misreports and there is no record of what it said versus what was checked.

Timeouts get tuned by making them longer. A device that occasionally does not answer produces a longer timeout, then a longer one. The retry budget is the structural fix and it is a different shape of change: it separates "slow" from "gone" instead of accommodating both.

Duplicate detection is discovered after a redundant fabric is deployed. In a tree there is one path to everything and identity by path works perfectly. Adding the second path for redundancy — which is 15.2 section 10's fix for single points of failure — is what breaks it, and the two changes are usually made by different people.

Nobody versions the inventory until a stale-view bug costs a week. The generation is trivial to add up front and requires touching every consumer to add later.

25. Common Misconceptions

"The scan completed, so we know what is out there." Completed and complete are different claims. The skipping build in section 5 completes, sets done, and is wrong about half the fabric.

"The device reported its capacity, so that is its capacity." It is what the device claims. Section 6's trusting build recorded 240GB in an 80GB fabric on exactly that reasoning.

"Changes during a scan get picked up next time." Only changes ahead of the cursor. A change behind it is not merely missed — it makes the current inventory describe a fabric that never existed.

"The inventory is only a few seconds old, so it is fine." Age is one of two conditions. An inventory seconds old is wrong if something changed in those seconds, which is precisely when discovery matters.

"An old inventory is a problem." Only when something acts on it. Both staleness monitors here fire on the use, because a metric that alarms on the condition alone alarms constantly.

"It did not answer, so it is not there." It did not answer yet. Section 11's no-retry build removes working devices on that reasoning, and everything downstream is then correct about an inventory missing a device.

"A device found twice is obviously a bug we would notice." It is silent until something is bound to the half that does not exist, and it then presents as a device that lost capacity — which sends everyone to the device.

"Incremental scanning solves the cost problem." It saves 83 percent per scan and the fabric still spent 42 percent of its time being scanned. Scanning less often is the larger lever, and it needs the event counter from section 10 to be safe.

26. Interview Reasoning

Q1. What is the property that makes a discovery scan trustworthy? That it visited every slot, not that it found devices or that it finished. Those are three different claims, and only the first rules out reporting a fabric half its real size.

Q2. How do you check it? populated & ~visited at the end of the walk. One instruction, and it is the difference between a discovery that can silently under-report and one that cannot.

Q3. Why must that monitor be gated on the walk having finished? Because mid-walk, populated slots are legitimately unvisited. Ungated it fires on every scan from the first cycle and is disabled within a week.

Q4. A walk finishes and reports half the fabric. What does its output look like? Entirely healthy. It sets done, its found-set is internally consistent, and every device in it is real. Nothing distinguishes it from a correct scan except the completeness check.

Q5. What is a device allowed to tell you about itself? Anything. The question is what you record and what you bind. Record both the claim and the verification; bind only the verification.

Q6. A device reports 128GB and 64GB is observable. What do you do? Bind 64, record both numbers, and count it as an overclaim. Refusing the device entirely loses 64GB that is genuinely there; binding 128 gives a host memory that faults.

Q7. Why keep three refusal reasons separate? Unknown type means update the manager. Overclaim means investigate the device and bind less. Zero capacity means the device is broken. Merging them into "rejected" loses the response.

Q8. Why is <= rather than < load-bearing in the capacity check? Because a device claiming exactly what is observable is honest. One off refuses every device that fills its capacity exactly, which is most of them.

Q9. What is a torn inventory? One assembled from slots read at different times, describing a configuration the fabric held at no single instant. It is not "slightly out of date" — it never existed.

Q10. Why does a change behind the cursor matter more than one ahead? The walk will reach a slot ahead of it. It will not come back for one behind. Same event, two positions, entirely different consequences.

Q11. Is a change at the cursor behind it or ahead? Ahead. That slot is being visited now, so the change is seen. < not <=, or every change the scan was about to pick up anyway triggers a restart.

Q12. Restart the scan, or publish and fix up? Restart. The fix-up path requires knowing which slots were read before the change, which is exactly the state a scan does not keep — and a partially corrected inventory is still an inventory of a fabric that never existed.

Q13. What does it cost? The scan again. Section 15 measures it: one restart doubled that round's work. That is the price of the invariant, and it is worth stating rather than hiding.

Q14. What makes an inventory stale? Two independent conditions: age, and events since the scan. Neither subsumes the other, and a design with only one is wrong in a specific direction.

Q15. Which direction, for each? Age only: a fabric that changed immediately after the scan is trusted for the whole window. Events only: an inventory nobody refreshed is trusted forever as long as nothing was noticed.

Q16. Should the staleness monitor fire when the inventory goes stale, or when something uses it? On the use. A stale inventory nobody consulted has misled nothing, and a monitor firing on the condition alone alarms constantly and gets disabled — which is worse than not having it.

Q17. An element does not answer. Absent or slow? Unknown until the retry budget is spent. Concluding either one early is a distinct failure: declaring absence removes a working device, and never concluding means discovery never finishes.

Q18. Why must a retry restart the wait? A retry inheriting the expired wait times out again immediately, spending the whole budget in a few cycles. It produces the same premature absence by a different route, and it looks like retries were tried.

Q19. Why must a probe request not restart an outstanding probe? Because the wait never expires, the timeout never fires, and a slow device is re-probed forever. That is the mirror image of Q18 and it is just as fatal.

Q20. A device is reachable by two paths. How many devices is it? One. Identity is the device, not the path. This is not an edge case — it is what redundancy means, and 15.2 section 10 is where the second path came from.

Q21. What does keying by path cost? Exactly the capacity that does not exist. Section 12: 160GB recorded in a 96GB fabric, silently, until something is bound to the phantom half.

Q22. How would you catch it? Compare unique devices against the physical count from somewhere other than the scan. An inventory cannot detect its own double-counting without an external source of truth.

Q23. Why does the inventory need a generation? So a consumer can say which view it acted on. Without it, a decision made on a stale inventory is indistinguishable from a decision made on a current one, forever.

Q24. Lag of 1 versus lag of 3 — same problem? No. One is a race and will happen occasionally in any system. Three is a consumer holding a reference and never refreshing, and it will keep deciding on that view indefinitely. Different fixes.

Q25. Full scans cost 18 cycles, incremental 3. What is the more important number? Neither. The 42 percent share of fabric time is, because it argues for scanning less often rather than faster — and the event counter is what makes that safe.

Q26. Why must an empty-sample guard return zero rather than a hundred? A fabric that has done nothing has not spent all its time scanning, and a scan that never ran has not saved anything. Both are asserted before any stimulus, which is the only moment the guard is reachable.

Q27. How do you test a monitor that says "nothing was counted twice"? Build something that counts twice. And sample it in the right delta — after the recording edge, is_new has gone low and the monitor has correctly cleared, so the check would pass on a broken design.

Q28. What class of defect does running a model once never find? Anything that should be reset between runs: a visited set, a dirty flag, a seen-set, a cursor. Two mutations here lived entirely in that gap, and both are the shape of bug that survives bring-up and appears in production.

Q29. The manager reports fewer devices than are installed. First reading? missed_err and n_visited. If the walk was incomplete, the missing devices were never asked anything and debugging them is wasted. If the walk was complete, identification, timeouts or duplicates folded them away, and each is a different investigation.

Q30. If you could add one field to the inventory, which? The generation. It is four bits, it makes every stale-view bug diagnosable instead of mysterious, and it is the one field that cannot be added later without auditing every consumer.

27. Exercises

1. Add a PARTIAL_ANSWER parameter to dev_identify where a device reports type but not capacity. Decide whether it is identified, and justify the decision from what could be bound.

2. Extend disc_change to track which slots changed behind the cursor, and implement a targeted re-scan of only those. Measure the saving against a full restart and decide whether the extra state is worth it.

3. Make inv_freshness expire on a weighted combination of age and events. Choose the weights from section 16's numbers and show a case where your rule is better than either condition alone.

4. Give probe_timeout an exponential backoff. Show that the total time to declare absence grows, and argue whether that is acceptable for a device that is genuinely gone.

5. Extend dup_detect to report the set of paths each device was found through. Show that this is strictly more useful than a duplicate count, and say what it enables in 15.2's SPOF analysis.

6. Add a reader to inventory that re-validates the generation at the point of action rather than the point of read. Show it closes the lag-1 race and does not close the lag-3 one.

7. Drive disc_top with a fabric that changes every scan. Show that it never publishes, and design the escape hatch — then argue for what it should publish when it takes it.

8. Take the 110-mutation suite and remove the second walk from the testbench. Confirm the two state-reset mutations return, and write down what each would have cost in a fabric that scans continuously.

28. Summary

Discovery is a complete walk over a moving fabric producing a versioned inventory in which what was verified is separate from what was claimed.

  • Complete: the skipping walk reported itself finished having visited four slots of eight. Completed is not complete, and populated & ~visited is the whole difference.
  • Moving: three changes during one scan, and only the one behind the cursor mattered. The correct build threw the scan away; the other published a fabric that existed at no instant.
  • Versioned: four generations, two stale reads, and a worst lag of three — a race and a consumer that stopped refreshing, told apart by one counter.
  • Verified: 80GB accepted against 240GB recorded by the trusting build, and 96GB against 160GB when identity was keyed by path instead of by device.

And the cost, measured rather than assumed: 42 percent of fabric time spent scanning, which argues for discovering when something changes rather than on a timer — and that is only safe because section 10 counts the changes.

110 mutations, 110 killed. The two worth remembering are the ones a single run could never find: a walk that does not clear its visited set, and a dirty flag that is never cleared. Both work perfectly the first time. Run it twice.

15.4 takes the inventory as given and binds something to a host.

Continue learning

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.