Skip to content

UCIe · Module 18

AI Chiplets

What changes when the compute die is a replicated accelerator chiplet rather than one monolithic processor — a job as an obligation the host cannot take back, ping-pong buffers whose ownership no CRC protects, descriptors that must not drift from their data, result capacity reserved before dispatch, why queue depth is not service rate, why a barrier needs a bitmap and not a counter, and the six-term bound that decides whether adding chiplets adds throughput.

Module 17 treated memory as the interesting part of the package. This chapter turns to the other side of the boundary — and asks what a compute die becomes when there are several of them and a link between.

1. The One-Sentence Model

An AI chiplet is a compute island with local state, local queues, local memory demand, and a finite communication surface. Replicating the island multiplies the arithmetic; it does not multiply the surface, and almost everything hard about the architecture follows from that asymmetry.

2. What This Chapter Owns

QuestionWhere it is answered
What a compute chiplet is; replication as a scaling model2.2 — Compute Chiplets
Why accelerators suit the chiplet model; attach styles2.5 — Accelerator Chiplets
Memory behind the boundary — endpoints, expansion, HBM17.1 · 17.2 · 17.4
Compute placed on the memory chiplet17.3 — Near-Memory Compute
Transaction lifecycle, identity, generation quarantine, timeout ambiguity12.2 · 12.4
Several accelerators sharing one fabric18.2 — Accelerator Fabrics
CPU + GPU + AI in one package18.3 — Heterogeneous Compute (planned)

2.2 and 2.5 are Foundation chapters — around 200 lines each, no RTL. They establish why compute partitions and why accelerators suit chiplets. This chapter is the engineering layer beneath them:

A job is an obligation, not a message (§8–§13). Once a chiplet accepts work, the host cannot withdraw it — not by timing out, not by resetting, not because the link went away.

Local buffers have owners, and no CRC protects ownership (§18–§21). The ping-pong hazard is a purely local lifetime bug that transport correctness cannot detect.

Descriptors and data travel separately and must not drift (§22–§24). Job A's descriptor paired with job B's activation is structurally valid and produces confident nonsense.

Queue depth is not service rate (§29–§31). The most intuitive load-balancing signal actively makes the bottleneck worse.

A barrier needs a bitmap (§32–§35). A counter advances a phase while a participant is still running.

And scaling is a six-term minimum (§37–§40), not a multiplication.

3. Sourcing

4. The Compute Island

An AI chiplet system drawn as nine structures. A host or control die runs a work scheduler that dispatches job descriptors across a UCIe fabric to two AI chiplets. Inside the first AI chiplet, a command queue holds accepted descriptors, a data mover fetches activations and weights into local SRAM organised as ping pong buffers, a compute array reads whichever buffer it currently owns, and a completion queue holds results until they can be returned. Shared memory sits below the fabric and is reached by both chiplets. The point of the drawing is that the descriptor path and the bulk data path are separate, that the compute array and the data mover contend for the local buffers, and that the scheduler above the fabric cannot directly observe any of the chiplet's internal state.Work scheduleron the control dieUCIe fabricdescriptors and dataCommand queuethe obligation liveshereData moverfills local buffersPing-pong SRAMone owner at a timeCompute arrayreads the ownedbufferCompletion queuereserved beforedispatchAI chiplet Bidentical,independentShared memoryactivations andweights12
A work descriptor crosses the boundary; the activation data crosses separately and in bulk. Inside the chiplet, a command queue feeds a compute array through ping-pong local buffers, a data mover fills them, and a completion queue returns results. The scheduler above must reason about all of it without seeing any of it.

Read the two paths out of the fabric. A descriptor is small and control-like; the activations are bulk. They travel separately, they arrive independently, and §22 is what happens when a design assumes they stay together.

5. What the Boundary Adds

Replicating a compute die introduces eight things a monolithic processor did not have.

IntroducedWhy it existsWhere this chapter treats it
work partitioningthe work must be split before it can be spread§14
activation and weight movementoperands live somewhere else§15, §16
synchronisationphases must line up across chiplets§32–§35
collective communicationmany-to-one and all-to-all patterns§33, and 18.2
remote memory accessoperands may be behind another boundary17.2
completion trackingthe host must know what finished, exactly once§8–§13
fault isolationone chiplet's problem must not be all of them§41
finite communication surfacethe link does not scale with the arithmetic§37–§40

Seven of the eight are communication or bookkeeping. Exactly one — partitioning — is about the computation itself. That ratio is the chapter, and it is why a design reviewed on arithmetic throughput alone reviews an eighth of the system.

6. The Work Descriptor

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE work descriptor. NOT a UCIe, CXL or standard format, and no
// field corresponds to any specified field of any of them (Section 3).
typedef enum logic [OP_W-1:0] {
  AI_GEMM_TILE = 'd0,
  AI_ELEMENTWISE = 'd1,
  AI_REDUCE    = 'd2
} ai_op_e;
 
typedef struct packed {
  logic [JOB_ID_W-1:0]   job_id;        // the SCHEDULER's identity
  logic [GEN_W-1:0]      generation;    // Section 12
  ai_op_e                opcode;
  logic [ADDR_W-1:0]     input_base;
  logic [ADDR_W-1:0]     weight_base;
  logic [ADDR_W-1:0]     output_base;
  logic [SIZE_W-1:0]     work_size;     // bytes of input
  logic [SIZE_W-1:0]     output_size;   // how much result capacity to reserve (§26)
  logic [CHIPLET_W-1:0]  destination;
  logic [PHASE_W-1:0]    phase;         // which barrier phase this belongs to (§34)
} ai_work_desc_t;

Architecture. One descriptor carrying identity, operation, three address bases, both sizes, a destination and a phase. output_size and phase are the two fields most often omitted, and each has a section: without the first, result capacity cannot be reserved before dispatch (§26); without the second, a completion cannot be attributed to a barrier phase (§34).

State. One register per pipeline stage on the way out, plus an entry in the job table (§9).

Cycle behaviour. Formed at the scheduler and held stable while offered (12.1's handshake discipline). Nothing on the chiplet side recomputes any field.

Contract. The chiplet must be able to decide, from the descriptor alone, whether it can accept the obligation. A descriptor that omits output_size forces the chiplet to accept blind and discover a shortage after computing17.3 §24's deadlock, in a new place.

Failure. Omitting generation and relying on job_id alone (§12). Or deriving output_size from opcode and work_size, which couples admission to the operation set and breaks when an output size becomes data-dependent.

DV. Assert descriptor stability under stall; assert every field the chiplet uses was carried rather than derived.

7. Dispatch Transfers an Obligation

When a chiplet accepts a work descriptor, it owns an obligation the scheduler cannot withdraw. Not by timing out, not by clearing its own table, not because the link entered recovery.

EventDoes the chiplet's obligation end?
the scheduler's timeout expiresno
UCIe enters recoveryno — the chiplet keeps computing (§42)
the link returns degradedno
the scheduler frees its job entryno — and now nothing can receive the result (§11)
the chiplet completes and the result is consumedyes
the chiplet fails the job and reports ityes

Three consequences.

A job is not a packet. 14.3's replay machinery makes a transport object re-sendable because re-sending has no semantic effect. A job has semantic effect — it writes an output range — so the same reasoning does not transfer (§43).

The scheduler's entry must outlive its own uncertainty. §11 is the bug of freeing it early.

And the chiplet must be able to report failure even when it cannot deliver a result, or a job that failed and could not say so leaves both sides permanently unsure.

8. The Job Table

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE scheduler-side job state.
typedef enum logic [2:0] {
  JOB_FREE            = 3'd0,
  JOB_DISPATCHED      = 3'd1,   // sent; not yet known to be accepted
  JOB_ACCEPTED_REMOTE = 3'd2,   // the chiplet owns it (Section 7)
  JOB_EXECUTING       = 3'd3,
  JOB_RESULT_PENDING  = 3'd4,   // computed; result not yet consumed
  JOB_COMPLETE        = 3'd5,
  JOB_FAILED          = 3'd6
} job_state_e;
 
typedef struct packed {
  logic                 valid;
  job_state_e           state;
  logic [CHIPLET_W-1:0] chiplet;       // captured at dispatch, never recomputed
  logic [GEN_W-1:0]     generation;
  logic [PHASE_W-1:0]   phase;
  logic                 result_expected;
  logic                 result_reserved;
  logic [SIZE_W-1:0]    bytes_done;    // progress, monotonic (Section 36)
} ai_job_state_t;
 
ai_job_state_t job_q [MAX_JOBS];

Architecture. One entry per outstanding job. Seven states because the scheduler must distinguish DISPATCHED from ACCEPTED_REMOTE — before acceptance the job can legitimately be retried as a transport object; after acceptance it cannot, because the chiplet owns it (§43).

State. MAX_JOBS entries. The depth bounds dispatch concurrency, which §40 shows is one of the six terms bounding throughput.

Cycle behaviour. chiplet is written once at dispatch and never recomputed — the same captured-destination discipline as 17.2 §16, and for the same reason: a scheduler that re-derives the destination after a reconfiguration sends a completion query to the wrong chiplet.

Contract. The completion matcher, the barrier logic (§34) and the load balancer (§30) all read this. Three consumers, one table.

Failure. bytes_done advancing on dispatch rather than on reported progress, which makes the progress field lie in exactly the situation — a stalled job — where it is consulted.

DV. Assert bytes_done monotonic and bounded by work_size; assert chiplet immutable while valid.

9. The Job FSM, and Which States Are Semantic

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. One next-state owner. The comment on each arm says whether the
// transition is driven by a TRANSPORT event or a SEMANTIC one — Section 11 is
// what happens when the two are confused.
always_comb begin
  nxt = job_q[i].state;
  unique case (job_q[i].state)
    JOB_FREE:            if (dispatch_fire[i])      nxt = JOB_DISPATCHED;      // semantic
    JOB_DISPATCHED:      if (job_failed[i])         nxt = JOB_FAILED;
                         else if (accept_ack[i])    nxt = JOB_ACCEPTED_REMOTE; // semantic ack
    JOB_ACCEPTED_REMOTE: if (job_failed[i])         nxt = JOB_FAILED;
                         else if (exec_started[i])  nxt = JOB_EXECUTING;
    JOB_EXECUTING:       if (job_failed[i])         nxt = JOB_FAILED;
                         else if (exec_done[i])     nxt = JOB_RESULT_PENDING;
    JOB_RESULT_PENDING:  if (result_consumed[i])    nxt = JOB_COMPLETE;
    JOB_COMPLETE:        if (host_ack[i])           nxt = JOB_FREE;
    JOB_FAILED:          if (host_ack[i])           nxt = JOB_FREE;
    default:                                        nxt = JOB_FAILED;
  endcase
end
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) job_q[i].state <= JOB_FREE;
  else        job_q[i].state <= nxt;

Architecture. Seven states, one owner, one unique case. Note what is absent: there is no arm driven by ucie_tx_fire. Transmission is a transport event and appears nowhere in this machine, which is §11's bug made structurally impossible.

State. Three bits per job. JOB_COMPLETE and JOB_FAILED both require a host acknowledgement before returning to JOB_FREE, which prevents the identity from being recycled while a completion is still in flight (12.2 §25).

Cycle behaviour. job_failed is checked before progress in every working state, so a failure cannot be overtaken by a completion in the same cycle.

Contract. The barrier reads JOB_COMPLETE; the balancer reads the count in EXECUTING; the result path reads RESULT_PENDING. Three consumers, three states, no overlap.

Failure. Merging DISPATCHED and ACCEPTED_REMOTE. Before acceptance, a re-send is a transport retry and is safe; after acceptance, it is a second job (§43). A design that cannot distinguish them must choose one behaviour for both, and either it never retries a lost dispatch or it double-executes an accepted one.

DV. Cover every state and legal transition; assert no illegal transition (12.4 §10).

10. Wrong RTL — Freeing the Job on Transmission

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the scheduler frees its entry when UCIe accepts the descriptor.
always_ff @(posedge clk)
  if (ucie_tx_fire)
    job_q[id].valid <= 1'b0;          // ← transport launch, not job completion

Two failures, and the second is worse.

The result arrives with no owner. The chiplet computes, returns a result naming id, and the scheduler's entry is gone. The work is discarded invisibly.

And then the identity is reused. id is free, so the scheduler allocates it to a new job.

CycleScheduler entry for idIn flightScheduler believes
10freed at ucie_tx_firejob A executingnothing outstanding
90reallocated to job BA executing, B dispatchedB is outstanding
140live (B)A's result returns naming idB completed
141freedB still executingB's real result finds nothing

Row 140 is a wrong answer delivered confidently. The scheduler consumes A's output as B's — different shape, different range, different phase — and nothing reports an error, because at transport level everything was delivered exactly once to a live identity.

The fix is two-part and both parts are needed: the entry is freed only at semantic completion (§7, §9), and the identity carries a generation so even a mistakenly early free cannot alias (§12).

11. SVA — Identity Is Not Recycled Under a Live Job

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. A job identity is unique among live jobs.
property p_job_id_unique_while_live;
  @(posedge clk) disable iff (!rst_n)
    dispatch_fire |-> !id_currently_live(dispatch_job_id);
endproperty
a_job_id_unique_while_live: assert property (p_job_id_unique_while_live);
 
// The scheduler entry is not freed by a transport event (Section 10).
property p_entry_not_freed_on_tx;
  @(posedge clk) disable iff (!rst_n)
    (ucie_tx_fire && !semantic_completion[ID]) |=> $stable(job_q[ID].valid);
endproperty
a_entry_not_freed_on_tx: assert property (p_entry_not_freed_on_tx);
 
// A completion must name a live job AND its current generation.
property p_completion_matches_live_generation;
  @(posedge clk) disable iff (!rst_n)
    completion_valid |-> (job_q[comp_id].valid
                       && (job_q[comp_id].generation == comp_gen));
endproperty
a_completion_matches_live_generation:
  assert property (p_completion_matches_live_generation);
 
// The captured chiplet never changes under a live job.
property p_chiplet_immutable;
  @(posedge clk) disable iff (!rst_n)
    job_q[ID].valid |-> $stable(job_q[ID].chiplet);
endproperty
a_chiplet_immutable: assert property (p_chiplet_immutable);

Architecture. Four properties: uniqueness, no transport-triggered free, generation-checked matching, and destination immutability.

Why the third is the safety net. The first two prevent §10 from arising; the third makes it detectable if it arises anyway — a stale result carrying generation 3 cannot match an entry now at generation 4, so the alias becomes a reported orphan rather than a wrong answer.

DV. The alias needs an early free, a reallocation, and a late result — a three-event sequence no random test produces (§44's cp_stale_completion).

12. Generation, Because an Identity Is Not Enough

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Identity plus generation — 12.2 Section 24's quarantine, applied
// to a semantic job rather than a transport response.
logic [GEN_W-1:0] job_gen_q [MAX_JOBS];
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n)
    for (int i = 0; i < MAX_JOBS; i++) job_gen_q[i] <= '0;
  else if (dispatch_fire)
    job_gen_q[dispatch_job_id] <= job_gen_q[dispatch_job_id] + 1'b1;

Architecture. A per-identity counter incremented at every dispatch. The pair {job_id, generation} is unique over a far longer window than job_id alone.

State. One counter per identity. GEN_W is not a free parameter — it must be wide enough that wrap is unreachable within the maximum time a completion can be delayed, and after a link recovery that can be very long.

Cycle behaviour. Incremented at dispatch, never elsewhere. The generation travels in the descriptor and returns in the completion.

Contract. The completion matcher compares both fields. A design that carries the generation but does not check it has paid the cost and kept none of the benefit.

Failure. A one-bit toggle, which distinguishes consecutive uses and fails on the third. Or resetting generations on a link recovery, which destroys the history exactly when stale completions are most likely (§42).

DV. Cover a completion arriving with a stale generation and confirm it is reported as an orphan.

13. Partitioning — Three Shapes, Three Communication Costs

Data parallelTensor / model parallelPipeline parallel
What is splitthe input batchone operation's operandsthe sequence of stages
What crosses the boundaryweights (once per phase), gradients or reductionspartial results, every operationactivations, stage to stage
Synchronisationat phase boundariesfrequent, fine-grainedpipelined, with a fill and drain
Sensitivity to link latencylowhighmoderate
Sensitivity to link bandwidthmoderatehighhigh
Scales well whenthe batch is largethe operation is large and the link is faststages balance

Two consequences.

The partitioning choice is the communication design. These are not three ways to write the same program; they are three different traffic patterns, and a chiplet system's fabric requirements follow from which one is used far more than from the arithmetic.

And tensor parallelism is the one that punishes a chiplet boundary hardest. It communicates within every operation rather than between phases, so the boundary's latency appears in the inner loop — which is §14's ratio at its worst.

14. Communication Intensity

The number that decides whether a chiplet boundary is affordable.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
communication_intensity = bytes crossing the boundary / useful compute performed
 
              [bytes] / [operations]   -> bytes per operation

Worked, with illustrative numbers. A tile of work performing 2 × 10⁹ operations and requiring 8 MiB of operands and 2 MiB of output to cross the boundary:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
crossing_bytes = 8 MiB + 2 MiB = 10,485,760 bytes
useful_ops     = 2e9 operations
 
communication_intensity = 10,485,760 / 2e9
                        ≈ 0.00524 bytes per operation

Now compare against what the link can sustain per operation. Illustratively, a chiplet capable of 5 × 10¹² operations/s behind a link delivering 400 GB/s of useful bandwidth:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bytes_available_per_op = 400e9 [bytes/s] / 5e12 [ops/s]
                       = 0.08 bytes per operation

0.00524 needed against 0.08 available — the tile fits comfortably, with roughly a 15× margin.

Now a tile one-eighth the size in each dimension, so operations fall by 8³ = 512 and operand bytes by 8² = 64:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
useful_ops     = 2e9 / 512      ≈ 3.91e6
crossing_bytes = 10,485,760/64  ≈ 163,840
 
communication_intensity = 163,840 / 3.91e6 ≈ 0.0419 bytes per operation

Still under 0.08, but the margin has collapsed from 15× to under 2×.

Three readings.

Communication intensity scales roughly with the inverse of tile size, because operations grow faster with tile dimension than surface bytes do. Small tiles are what make a chiplet boundary expensive, and the effect is dramatic rather than gradual.

The comparison is against bytes-per-operation available, not against bandwidth. Quoting a link's bandwidth without dividing by the arithmetic rate compares two numbers that are not comparable (15.1 §4's units discipline).

And a design can fail this test with excellent hardware on both sides. A fast chiplet raises the denominator and lowers the bytes available per operation. Making the compute faster can make the boundary the constraint — which is the single most counter-intuitive result in the chapter.

15. Overlap — Computing on One Buffer While Filling Another

The mechanism that hides §14's transfer time.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
without overlap:  [ fill 0 ][ compute 0 ][ fill 1 ][ compute 1 ]  -> serial
with overlap:     [ fill 0 ][ fill 1     ][ fill 2     ]
                            [ compute 0  ][ compute 1  ]          -> max(), not sum

Overlap turns a sum into a maximum. If filling and computing take comparable time, it roughly halves the elapsed time; if one dominates, it hides the other entirely. And it requires two buffers with strictly separated ownership, which is §16.

16. Ping-Pong Buffer Ownership

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE double-buffer ownership. The OWNER of each buffer is explicit
// state, because Section 18 is what happens when it is implicit.
typedef enum logic [1:0] {
  BUF_EMPTY    = 2'd0,   // nobody owns it; the mover may fill it
  BUF_FILLING  = 2'd1,   // the data mover owns it
  BUF_READY    = 2'd2,   // filled; the compute array may take it
  BUF_COMPUTING= 2'd3    // the compute array owns it — DO NOT WRITE
} buf_state_e;
 
buf_state_e buf_state_q [2];
logic       active_buf_q;          // which buffer compute is using
 
// The mover may only target a buffer it owns or may claim.
assign mover_target_ok = (buf_state_q[mover_buf] == BUF_EMPTY);
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    buf_state_q[0] <= BUF_EMPTY;
    buf_state_q[1] <= BUF_EMPTY;
    active_buf_q   <= 1'b0;
  end else begin
    for (int b = 0; b < 2; b++) begin
      unique case (buf_state_q[b])
        BUF_EMPTY:     if (mover_claim && (mover_buf == b[0]))  buf_state_q[b] <= BUF_FILLING;
        BUF_FILLING:   if (fill_done  && (mover_buf == b[0]))   buf_state_q[b] <= BUF_READY;
        BUF_READY:     if (compute_claim && (compute_buf == b[0]))
                                                                buf_state_q[b] <= BUF_COMPUTING;
        BUF_COMPUTING: if (compute_done && (compute_buf == b[0]))
                                                                buf_state_q[b] <= BUF_EMPTY;
        default: ;
      endcase
    end
 
    if (compute_claim) active_buf_q <= compute_buf;
  end

Architecture. Four states per buffer with exactly one owner in each. The states are not decoration — EMPTY and READY are the only two in which a transfer of ownership may occur, and the two owners can never both be in a writing state.

State. Two two-bit registers plus one selector. Tiny, and it is the only thing standing between the design and §18.

Cycle behaviour. One unique case per buffer with one owner. mover_target_ok gates the data mover — the mover cannot begin filling a buffer that is READY or COMPUTING, which is the enforcement rather than a convention.

Contract. The compute array reads a buffer for the duration of a tile and relies on it not changing. That reliance is invisible at the array's interface — it has no signal saying "do not disturb" — which is exactly why the state must be explicit and asserted (§17).

Failure. §18. Also allowing BUF_COMPUTING → BUF_FILLING directly, which skips the release and lets the mover start while compute is finishing its last read.

DV. Assert the mover never writes a COMPUTING or READY buffer (§17). Cover both buffers in every state and the full ping-pong cycle, which requires at least three tiles to observe.

17. SVA — a Buffer Is Not Written While Compute Owns It

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The property that no transport check can substitute for.
property p_no_write_to_computing_buffer;
  @(posedge clk) disable iff (!rst_n)
    mover_write_fire |-> (buf_state_q[mover_buf] == BUF_FILLING);
endproperty
a_no_write_to_computing_buffer:
  assert property (p_no_write_to_computing_buffer);
 
// Compute reads only a buffer it owns.
property p_compute_reads_owned_buffer;
  @(posedge clk) disable iff (!rst_n)
    compute_read_fire |-> (buf_state_q[compute_buf] == BUF_COMPUTING);
endproperty
a_compute_reads_owned_buffer: assert property (p_compute_reads_owned_buffer);
 
// The two buffers are never owned by the same agent in a writing sense.
property p_ownership_exclusive;
  @(posedge clk) disable iff (!rst_n)
    !((buf_state_q[0] == BUF_FILLING) && (buf_state_q[1] == BUF_FILLING))
      || (NUM_MOVERS > 1);
endproperty
 
// A buffer's contents are stable while compute owns it.
property p_buffer_stable_while_computing;
  @(posedge clk) disable iff (!rst_n)
    (buf_state_q[BUF_UT] == BUF_COMPUTING) |=> $stable(buf_mem[BUF_UT]);
endproperty
a_buffer_stable_while_computing:
  assert property (p_buffer_stable_while_computing);

Architecture. Four properties: no write to an owned buffer, no read of an unowned one, exclusivity, and content stability.

Why the fourth is worth its cost. The first three check the protocol; the fourth checks the effect. A design can satisfy the ownership protocol and still corrupt a buffer through a debug path, a second mover, or an aliased address — and only a stability check on the contents catches that.

DV. All four always-on. Inject a mover write to a COMPUTING buffer and confirm the first fires — which is §18, and it is the only detector there is.

18. Wrong RTL — the Producer Overwrites the Active Buffer

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the data mover fills whichever buffer is not "active", with no
// ownership state.
assign mover_buf = ~active_buf_q;
 
always_ff @(posedge clk)
  if (mover_data_valid)
    buf_mem[mover_buf][mover_addr] <= mover_data;   // ← no ownership check

This is correct exactly while the ping-pong alternates perfectly, and it does not.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. Compute is using buffer 0. active_buf_q = 0. The mover fills buffer 1.
2. Compute finishes tile N and claims buffer 1. active_buf_q = 1.
3. The mover begins filling buffer 0 for tile N+2.
4. But compute's LAST READS of buffer 0 are still in its pipeline —
   the array has a read latency, and `compute_done` was asserted at the
   last read ISSUE, not the last read RETURN.
5. -> the mover overwrites data the array is still consuming.

Four properties.

No CRC catches it. The data crossed the link perfectly, was written to SRAM perfectly, and is exactly what was sent. This is a purely local lifetime bug, and transport verification is blind to it by construction.

The corruption is partial and pipeline-depth dependent. Only the last few elements of a tile are affected, so the result is nearly right — which for a numerical workload can pass a tolerance check and fail a bit-exact one, making it look like a precision issue rather than a data-integrity one.

It appears only when the mover is fast enough to start early, so it is load- and bandwidth-dependent. A faster link makes it more likely, which is a genuinely surprising direction.

And active_buf_q is not ownership. It says which buffer compute selected; it says nothing about when compute is done with the other one. The fix is the explicit four-state ownership of §16, where the release happens at compute_done defined as the last read return.

19. Descriptors and Data Travel Separately

A descriptor is small and control-shaped; activations are bulk. They will take different paths, different queues, and possibly different traffic classes — which is 18.2's subject.

Their arrival order carries no information. A design that pairs "the descriptor at the head of the command queue" with "the data at the head of the data queue" has assumed a relationship the fabric never promised.

20. Wrong RTL — Two Queues Advancing Independently

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — descriptor and data are paired by queue position.
always_ff @(posedge clk)
  if (desc_valid && data_valid) begin
    launch_compute(desc_fifo[desc_rd], data_fifo[data_rd]);   // ← paired by position
    desc_rd <= desc_rd + 1'b1;
    data_rd <= data_rd + 1'b1;
  end

One reordering anywhere and every subsequent pair is wrong.

Arrival orderDescriptorsDataPaired as
idealA, B, CA, B, C✓ A-A, B-B, C-C
data class fasterA, B, CB, A, C✗ A-B, B-A, C-C
one descriptor retriedB, A, CA, B, C✗ B-A, A-B, C-C

Four properties, and this is a flagship failure.

Everything is structurally valid. Job A's descriptor is a well-formed descriptor; job B's activation is well-formed data. The compute engine has no way to know they do not belong together — it computes A's operation on B's operands and returns a confident, well-formed, entirely wrong result.

No CRC, no checksum and no transport check detects it, because nothing was corrupted. This is an association error, the same class as 17.4 §26's FIFO-position return matching.

And it is systematic, not intermittent. After one swap, every subsequent pair is offset — so the failure rate is 100% and the symptom is "the accelerator returns garbage", which is easy to misattribute to the arithmetic.

The fix is to bind by identity, not by position:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Data carries the job identity it belongs to, and the launch
// requires an explicit match against a live descriptor.
assign pair_ok = desc_q[desc_idx].valid
              && (desc_q[desc_idx].job_id     == data_hdr.job_id)
              && (desc_q[desc_idx].generation == data_hdr.generation);

21. The Tile Object

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Data carries enough identity to be matched, and enough
// structure to be reassembled. NOT a UCIe format (Section 3).
typedef struct packed {
  logic [JOB_ID_W-1:0] job_id;      // which job this belongs to (Section 20)
  logic [GEN_W-1:0]    generation;
  logic [TILE_W-1:0]   tile_id;     // which tile within the job
  logic [TILE_W-1:0]   tile_count;  // how many to expect
  logic [DATA_W-1:0]   data;
  logic                last;        // last beat of this tile
} ai_tile_t;

Architecture. Identity, position, extent, payload and a terminator. tile_count is what lets the receiver know it is finished without a separate message, and tile_id is what makes out-of-order arrival survivable.

State. One register per pipeline stage, plus a per-job reception bitmap if tiles may arrive out of order.

Cycle behaviour. Fields travel as one packed object through every stage — the same discipline as 16.4 §20, and for the same reason: a field that advances separately can be associated with the wrong payload.

Contract. The pairing check (§20) reads job_id and generation. A tile without them cannot be matched to anything and must be paired by position, which is §20's bug re-entered by omission.

Failure. Omitting tile_count and inferring completion from last alone — which cannot detect a missing tile, only a present one, so a job whose middle tile was lost waits forever with no indication of why.

DV. Assert every accepted tile names a live job and generation; cover out-of-order tile arrival and a missing tile.

22. Local Capacity Decides How Much Crosses

§14's ratio is not fixed by the workload — it is partly a function of the chiplet's local storage.

Local buffer capacityConsequence
holds a whole tile plus its operandsoperands cross once per tile
holds part of a tilethe same operands cross repeatedly
holds several tilesreuse across tiles becomes possible
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ILLUSTRATIVE. A tile needing 8 MiB of operands, with 4 MiB of local buffer:
 
  the tile must be processed in 2 passes
  operands crossing: 8 MiB in pass 1 + 8 MiB re-fetched in pass 2 = 16 MiB
  communication intensity: DOUBLED, with no change to the workload

Two consequences.

Local capacity buys back boundary bandwidth, which is 2.5 §3's observation with a number attached. The trade is silicon area against link traffic, and §14's arithmetic is how a design decides which is cheaper.

And the effect is a cliff, not a slope. A buffer just large enough is dramatically better than one just too small. So a capacity decision made on an average tile size fails on the tail, and the failure is a doubling of traffic rather than a small regression.

23. Admission Reserves Everything the Job Will Need

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Every resource the job requires, checked BEFORE accepting the
// obligation (Section 7). 17.3 Section 24 is the deadlock this prevents.
assign job_admit =
      cmd_slot_available                        // a table entry
   && (input_buf_free >= desc_in.work_size)     // Section 22 — room for operands
   && (result_slots_q >= desc_in.output_size)   // Section 26 — room for the OUTPUT
   && compute_context_available                 // the array can be configured for it
   && !chiplet_quiescing;                       // not draining for a reconfiguration

Architecture. Five terms. The third is the one designs omit, and omitting it is a deadlock rather than a performance issue.

State. input_buf_free and result_slots_q are the stateful terms.

Cycle behaviour. Evaluated at acceptance, and both reservations are taken in the same cycle as the acceptance. Reserve-then-accept, never accept-then-hope.

Contract. Accepting a job promises it can be fed, executed, and its output held. The scheduler relies on that promise to know its job will make progress.

Failure. §24 for the input buffer, §25's family for the output.

DV. Force each term false alone and confirm no admission — five directed tests.

24. Wrong Admission — Only the Compute Slot Is Checked

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the array is free, so accept.
assign job_admit = compute_context_available;
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. The compute array is idle, so the job is accepted.
2. Both local buffers are still holding data for previous jobs.
3. The data mover has nowhere to put the incoming activations.
4. -> it stalls holding fabric resources, or it overwrites a live buffer
     (Section 18), or the incoming data is dropped.

Three properties.

All three outcomes are bad and they look different. A stall appears as a fabric backpressure problem; an overwrite appears as numerical corruption; a drop appears as a hung job. One admission bug, three unrelated-looking symptoms.

The compute array being free is the most visible signal and the least sufficient. It answers one of five questions (§23) and is routinely mistaken for all of them — the sixth time this curriculum has made that observation, which is a measure of how often the shortcut is written.

And it fails only under concurrency. With one job in flight the buffers are always free. It appears exactly when the pipeline is full, which is the operating point the design exists for.

25. Result Capacity Is Reserved Before Dispatch

17.3 §23 established the deadlock at a near-memory engine. At a chiplet the shape is identical and the scope is larger:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. Result capacity is finite — output buffers, or completion-queue slots.
2. Jobs are dispatched without checking it.
3. Enough jobs complete that all result capacity holds outputs the host
   has not yet consumed.
4. Another job finishes computing. It has nowhere to put its result.
5. It cannot complete, so it does not release its command slot, its
   input buffers, or its compute context.
6. The host is willing to consume — but the return path or the completion
   ordering needs the blocked job to move first.
7. -> deadlock, with every component behaving correctly.

And there is a chiplet-specific aggravation. The blocked job holds a compute context, so the array is idle and unavailable. The system's most expensive resource is stalled by a buffer accounting error, which is why this is worth checking at the scheduler as well as at the chiplet.

26. Result-Slot Accounting

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Reserve at dispatch, release at consumption. Simultaneous
// events in ONE place with ONE owner.
logic [RES_CNT_W-1:0] result_slots_q;
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    result_slots_q <= RESULT_SLOTS[RES_CNT_W-1:0];
  end else begin
    unique case ({dispatch_fire, result_consumed_fire})
      2'b10: result_slots_q <= result_slots_q - dispatch_result_need;
      2'b01: result_slots_q <= result_slots_q + freed_result_slots;
      2'b11: result_slots_q <= result_slots_q - dispatch_result_need + freed_result_slots;
      default: ;
    endcase
  end

Architecture. One counter, one owner, four explicit arms. The 2'b11 arm is written out because that is the cycle where two independent if statements lose an update.

State. One counter per chiplet, initialised to RESULT_SLOTS.

Cycle behaviour. Decremented by the job's stated need (§6's output_size), not by one. A design that reserves one slot per job and then produces a multi-slot result has reserved the wrong quantity and reaches §25 anyway.

Contract. §23's admission reads it. The counter is the only thing between the design and §25, so its accuracy is a correctness property.

Failure. Two independent ifs. The drift is monotonic, so the design eventually either refuses all jobs or admits past capacity.

DV. Bounds and conservation, checked every cycle; cover the simultaneous dispatch-and-consume cycle explicitly.

27. Backpressure Is a Chain, Not a Signal

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. The scheduler's readiness is a conjunction across five owners.
assign scheduler_can_dispatch =
      job_table_space                         // scheduler-local
   && chiplet_cmd_credit_q[target] != '0      // the chiplet's command queue
   && chiplet_buf_credit_q[target] != '0      // its input buffers  (Section 22)
   && chiplet_res_credit_q[target] != '0      // its result capacity (Section 25)
   && ucie_transport_ready[target];           // the link
TermOwned byBecomes false when
job_table_spacethe schedulertoo many jobs outstanding
chiplet_cmd_credit_qthe chiplet's command queueit is saturated with work
chiplet_buf_credit_qthe chiplet's input buffersoperands are backed up (§22)
chiplet_res_credit_qthe chiplet's result capacityoutputs are unconsumed (§25)
ucie_transport_readythe linkcongestion, retry, recovery

Five owners, five timescales, five different fixes. Collapsing them to ucie_link_active answers one of five questions — and a dispatch admitted with no job-table entry has no owner for its result at all.

28. Chiplets Are Not Interchangeable

Even identical dies deliver different service rates.

CauseTimescaleVisible in
workload mix — some jobs are heavierper jobjob durations
thermal throttlingsecondssustained rate falling
a degraded link (14.4)until repairedtransfer time
memory locality — operands nearer or furtherper jobmemory stall cycles
maintenance on its memory (17.1 §11)periodicmemory stall cycles

Two consequences.

Static round-robin distributes work evenly and completes it unevenly. With a slow chiplet, an equal share means it becomes the critical path for every barrier (§32) — so the whole system runs at the slowest chiplet's rate.

And the differences are not observable from outside. A scheduler sees dispatches and completions. It cannot see thermal state, memory stalls, or link width unless the chiplets report them (§31).

29. Wrong Scheduler — Choose by Queue Depth

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — the shortest queue wins.
always_comb begin
  best = 0;
  for (int c = 1; c < NUM_CHIPLETS; c++)
    if (queue_depth_q[c] < queue_depth_q[best]) best = c[CHIPLET_W-1:0];
end

A short queue has two opposite explanations.

Why the queue is shortWhat more work does
the chiplet is fast and drained itgood — it can absorb more
the chiplet is stalled on memory or a degraded link, so nothing enteredbad — it piles onto a bottleneck

Illustrative. Chiplet A is waiting on remote memory; its queue drained because nothing is being issued, not because work is being completed.

Chiplet A (stalled)Chiplet B (healthy)
queue depth26
jobs completed, last window19
service rate0.05 jobs/kcycle0.45 jobs/kcycle
what a depth-only scheduler doessends the next job hereskips it

Three properties.

It is a positive feedback loop. More work at A makes A's backlog worse, which does not raise its queue depth quickly because A is not accepting, so the scheduler keeps choosing it. Congestion amplifies (13.4 §26).

Queue depth measures occupancy, not throughput. They are related through service rate, and service rate is the quantity actually wanted — depth is a proxy that inverts precisely when it matters.

And the fix does not need a complex model. A completions-per-window estimate is enough (§30), and it is two counters.

30. A Service-Rate Scheduler

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE. Choose by ESTIMATED SERVICE RATE and current backlog, not by
// depth alone. Simple on purpose — the point is which signals are used.
logic [63:0] jobs_completed_q [NUM_CHIPLETS];
logic [63:0] busy_cycles_q    [NUM_CHIPLETS];
logic [63:0] mem_stall_q      [NUM_CHIPLETS];
logic [63:0] ucie_stall_q     [NUM_CHIPLETS];
 
logic [RATE_W-1:0] rate_est_q [NUM_CHIPLETS];   // completions per window
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    for (int c = 0; c < NUM_CHIPLETS; c++) rate_est_q[c] <= RATE_INIT;
  end else if (window_tick) begin
    for (int c = 0; c < NUM_CHIPLETS; c++)
      // Exponential smoothing: half the old estimate plus half this window.
      rate_est_q[c] <= (rate_est_q[c] >> 1)
                     + (completions_this_window[c][RATE_W-1:0] >> 1);
  end
 
// Expected time to drain: backlog divided by rate. Lower is better.
always_comb begin
  best = 0;
  for (int c = 1; c < NUM_CHIPLETS; c++)
    if ((queue_depth_q[c] * rate_est_q[best]) <
        (queue_depth_q[best] * rate_est_q[c]))         // cross-multiply: no divide
      best = c[CHIPLET_W-1:0];
end

Architecture. A smoothed completions-per-window estimate per chiplet, and a selection by expected drain time — backlog divided by rate — rather than by backlog alone. The cross-multiplication avoids a divider and is exact for the comparison.

State. One rate estimate and four diagnostic counters per chiplet. The estimate is small; the counters are wide and diagnostic-only.

Cycle behaviour. The estimate updates once per window; the selection is combinational. Exponential smoothing rather than an instantaneous rate, because a single window with no completions would otherwise take a chiplet out of service entirely.

Contract. The estimate must be initialised optimistically (RATE_INIT), or a newly-added or newly-recovered chiplet is never selected, never completes anything, and never raises its estimate — a self-fulfilling exclusion that is a classic feedback bug in adaptive schedulers.

Failure. Using an instantaneous rate (above). Or letting the estimate reach zero, which makes the cross-multiplied comparison degenerate.

DV. Inject a chiplet that stalls on memory and confirm the scheduler stops selecting it; then release the stall and confirm it resumes within a bounded number of windows.

31. Diagnostics the Scheduler Needs

CounterAnswersWithout it
jobs_completed_qwhat is this chiplet's service rate?§29's inversion
busy_cycles_qis the array actually working?idle and stalled look identical
mem_stall_qis it memory-bound?a memory problem is blamed on the fabric
ucie_stall_qis it communication-bound?§40's binding term is unknown
barrier wait cyclesis it the critical path for a phase?§32's imbalance is invisible

Two properties.

These are chiplet-side counters reported to a scheduler that cannot otherwise see them (§28). The reporting path is part of the architecture, not an afterthought — a scheduler with no telemetry can only use depth, which is §29.

And mem_stall_q against ucie_stall_q is the pair that names the constraint. 17.3 §38's argument: two counters that look like one problem and have different fixes.

32. Barriers — Why a Phase Cannot Advance Early

Multi-chiplet work often requires every participant to finish phase N before any starts phase N+1.

A barrier is a distributed agreement, and the failure mode is not a hang — it is a phase that advances while a participant is still computing, which corrupts the next phase's inputs silently.

Barrier implementationDuplicate completionMissing completion
counteradvances early — corruptionhangs
bitmapidempotent — harmlesshangs

Both hang on a missing completion, which is detectable. Only the counter advances early on a duplicate, which is not.

33. Barrier RTL — a Bitmap

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// ILLUSTRATIVE barrier. A BITMAP, not a counter — Section 35 is why.
logic [NUM_CHIPLETS-1:0] pending_q;         // who has NOT finished this phase
logic [PHASE_W-1:0]      phase_q;
 
always_ff @(posedge clk or negedge rst_n)
  if (!rst_n) begin
    pending_q <= '0;
    phase_q   <= '0;
  end else if (phase_start) begin
    pending_q <= participants_mask;          // set at phase start
    phase_q   <= phase_q + 1'b1;
  end else begin
    // GUARDED clear: a duplicate completion finds the bit already clear and
    // changes nothing. This is what makes it idempotent.
    if (phase_complete_valid
        && (phase_complete_phase == phase_q)   // the CURRENT phase only
        && pending_q[phase_complete_chiplet])
      pending_q[phase_complete_chiplet] <= 1'b0;
  end
 
assign barrier_satisfied = (pending_q == '0);

Architecture. One bit per participant, set at phase start and cleared by that participant's completion. The guard — pending_q[chiplet] in the condition — is what makes a duplicate harmless, and it is one term.

State. NUM_CHIPLETS bits plus a phase counter. The phase counter is not decoration: a completion from a previous phase, arriving late after a recovery, must not clear a bit in the current one (§42).

Cycle behaviour. Set as a whole at phase_start; cleared one bit at a time. A completion for the wrong phase is ignored entirely — not deferred, not queued, ignored, because the phase it belonged to is over.

Contract. Everything downstream waits on barrier_satisfied. A false assertion of it corrupts the next phase's inputs, and there is no recovery, because the corrupted values are consumed as legitimate data.

Failure. §35. Also omitting the phase check, which allows a stale completion to satisfy a current barrier — the same class as 16.4 §27's stale-epoch acceptance.

DV. §34's properties. Cover a duplicate completion and a stale-phase completion, both of which must be injected.

34. SVA — Only a Pending Participant Can Complete

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. The barrier's safety and its idempotence.
property p_only_pending_can_clear;
  @(posedge clk) disable iff (!rst_n)
    (phase_complete_valid && !pending_q[phase_complete_chiplet])
      |=> $stable(pending_q);
endproperty
a_only_pending_can_clear: assert property (p_only_pending_can_clear);
 
// A completion for a non-current phase changes nothing.
property p_stale_phase_ignored;
  @(posedge clk) disable iff (!rst_n)
    (phase_complete_valid && (phase_complete_phase != phase_q))
      |=> $stable(pending_q);
endproperty
a_stale_phase_ignored: assert property (p_stale_phase_ignored);
 
// The barrier is satisfied only when every participant has completed.
property p_barrier_requires_all;
  @(posedge clk) disable iff (!rst_n)
    barrier_satisfied |-> (pending_q == '0);
endproperty
a_barrier_requires_all: assert property (p_barrier_requires_all);
 
// No participant is still executing when the next phase starts.
property p_no_execution_across_barrier;
  @(posedge clk) disable iff (!rst_n)
    phase_start |-> (all_participants_idle_for_phase($past(phase_q)));
endproperty
a_no_execution_across_barrier:
  assert property (p_no_execution_across_barrier);

Architecture. Four properties: guarded clearing, stale-phase rejection, the barrier condition, and the end-to-end guarantee.

Why the fourth is not implied by the third. The third checks the bitmap; the fourth checks reality — that no participant is actually still computing. A design whose completion signal is asserted before the last write drains satisfies the third and violates the fourth, and only the fourth catches it.

DV. The fourth needs a model of what each participant is actually doing, which is §38's scoreboard.

35. Wrong RTL — a Barrier Counter

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a counter cannot tell a new completion from a repeat.
logic [CNT_W-1:0] pending_cnt_q;
 
always_ff @(posedge clk)
  if (phase_start)             pending_cnt_q <= NUM_CHIPLETS[CNT_W-1:0];
  else if (phase_complete_valid) pending_cnt_q <= pending_cnt_q - 1'b1;
 
assign barrier_satisfied = (pending_cnt_q == '0);

Illustrative, four participants, and chiplet B's completion is transported twice.

EventCounterReality
phase start4A, B, C, D running
A completes3B, C, D running
B completes2C, D running
B's completion replayed1C, D still running
C completes0D STILL RUNNING
barrier satisfied✗ phase N+1 starts

Four properties.

Phase N+1 begins while D is still writing phase N's output. The next phase reads a mixture of new and old values — silent corruption whose magnitude depends on how far D had progressed.

The duplicate is not a bug anywhere else. It is a transport retry doing exactly what 14.3 specifies, or a chiplet re-sending after an ambiguous acknowledgement. The counter is the only incorrect component.

And the bitmap fix is free. NUM_CHIPLETS bits instead of log2(NUM_CHIPLETS), and one extra term in the clear condition. The counter is not cheaper in any way that matters, which makes it a pure loss.

One more note: the same argument appeared at 16.2 §21 for coherence responses. It is the same bug, and it recurs because a counter looks like the obvious way to track "how many are left".

36. SVA — Job Progress and Conservation

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// MANDATORY. Progress is monotonic and bounded.
property p_progress_bounded_monotonic;
  @(posedge clk) disable iff (!rst_n)
    job_q[ID].valid |-> (job_q[ID].bytes_done <= job_work_size[ID])
                     && (job_q[ID].bytes_done >= $past(job_q[ID].bytes_done));
endproperty
a_progress_bounded_monotonic: assert property (p_progress_bounded_monotonic);
 
// A job is retired only after its result was consumed, or it failed explicitly.
property p_retire_after_result;
  @(posedge clk) disable iff (!rst_n)
    ((job_q[ID].state == JOB_FREE) && $past(job_q[ID].valid))
      |-> $past(result_consumed[ID] || job_failed_reported[ID]);
endproperty
a_retire_after_result: assert property (p_retire_after_result);
 
// Result slots are conserved (Section 26).
property p_result_slots_conserved;
  @(posedge clk) disable iff (!rst_n)
    (result_slots_q + total_reserved_by_live_jobs() == RESULT_SLOTS);
endproperty
a_result_slots_conserved: assert property (p_result_slots_conserved);
 
// Semantic execution at most once — the reference-model property (Section 43).
int unsigned tb_semantic_executions [int];
property p_at_most_one_execution(int unsigned rid);
  @(posedge clk) disable iff (!rst_n)
    (tb_semantic_executions[rid] <= 1);
endproperty
a_at_most_one_execution: assert property (p_at_most_one_execution(REF_UT));

Architecture. Four properties: progress, retirement discipline, resource conservation, exactly-once semantics.

Why the third matters most in practice. Bounds catch gross errors; conservation catches the slow drift from §26's two-if bug, which stays within bounds indefinitely and eventually deadlocks the system for no visible reason.

Why the fourth must be verification-only. The wire carries {job_id, generation}. The knowledge that a re-dispatched job is semantically the same work belongs to whatever generated it — synthesising it into the design would build a duplicate detector with the same blind spots as the thing it checks.

DV. The fourth needs a recovery, a timeout and a re-dispatch in sequence (§46).

37. Scaling Efficiency

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
scaling_efficiency(N) = throughput(N chiplets) / (N × throughput(1 chiplet))

Three things reduce it, and all three are communication or synchronisation.

CauseEffect on efficiency
communication that does not shrink with Na fixed cost divided by more workers = a growing fraction
barriersevery phase runs at the slowest participant's rate (§28)
shared resources — memory, fabricmore chiplets contending for the same cut (15.3 §11)

None of the three is about arithmetic. Doubling the arithmetic per chiplet does not improve any of them and makes the first worse, because §14's bytes-available-per-operation falls.

38. A Worked Scaling Example

Illustrative numbers throughout. No source is claimed for any figure.

Assume per-chiplet compute of 100 units of work per 1000 cycles, a per-phase communication cost of 200 cycles that does not shrink with N, and a barrier cost equal to the slowest chiplet's excess, illustratively 50 cycles per phase.

NCompute cycles per phaseCommunicationBarrierTotalThroughputEfficiency
11000001000100/1000 = 0.100100%
250020050750100/750 = 0.13367%
425020050500100/500 = 0.20050%
812520050375100/375 = 0.26733%

Check the arithmetic. At N = 4: compute per chiplet is 1000/4 = 250; total 250 + 200 + 50 = 500 cycles; throughput 100 units / 500 cycles = 0.200; ideal would be 4 × 0.100 = 0.400; efficiency 0.200 / 0.400 = 50%.

Four readings.

Throughput still improves at every step — 0.100 → 0.133 → 0.200 → 0.267. Adding chiplets is not useless; it is just far less than proportional, and reporting only the throughput improvement while omitting the efficiency is how a 33%-efficient system is described as a success.

Communication dominates at N = 8: 200 of 375 cycles, 53% of the elapsed time, for a fixed cost that never shrinks. That is the term to attack, and §22's local capacity and §15's overlap are the two ways to attack it.

The barrier cost is constant here and its relative weight grows — 5% at N = 1's equivalent, 13% at N = 8. A cost that does not scale becomes a scaling problem.

And at large N the compute term vanishes into the fixed costs. Beyond some N, adding chiplets changes the total by almost nothing. Finding that N before building the package is what this arithmetic is for.

39. What Actually Bounds an AI Chiplet System

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
achieved_throughput <= min(
    compute throughput,              // the number usually quoted
    local buffer bandwidth,          // Section 22
    UCIe useful bandwidth,           // Section 14
    memory bandwidth,                // Module 17
    scheduler issue rate,            // Section 27
    synchronisation rate             // Sections 32-35
)

Worked, illustratively, with the §38 configuration at N = 4:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
compute            : 4 × 100 units/1000 cycles = 0.400 units/cycle
local buffer BW    : ample by assumption
UCIe useful BW     : limits the phase to 200 cycles of transfer  -> effective 0.200
memory BW          : ample by assumption
scheduler issue    : ample by assumption
synchronisation    : barrier adds 50 cycles/phase
 
achieved = 100 units / (250 + 200 + 50) = 0.200 units/cycle   <- communication binds

Three readings.

Compute is 0.400 and achieved is 0.200 — the arrays are idle half the time. Adding arithmetic raises the first term and changes nothing.

The binding term is communication, and it is the one a datasheet does not describe. A system specified on compute alone is specified on a term that is not binding.

And the terms change with N. At N = 1 compute bound; at N = 8 communication bound. A design verified at one scale has not characterised the others, which is 15.5 §27's bottleneck-migration method applied to a chiplet count.

40. Degradation Changes the Rate, Not the Semantics

One chiplet's link recovers at reduced width (14.4).

The scheduler mustThe scheduler must not
lower its service-rate estimate for that chiplet (§30)assume its outstanding jobs failed
rescale any per-job timeout (16.5 §27)re-dispatch anything (§43)
expect it to be the barrier's critical path (§28)remove it from the participant set mid-phase
report the degradationtreat it as a fault

Transport degradation changes performance. It changes no job's identity, no job's output range, and no job's completion. A design in which a width change alters a result has coupled a performance property to a correctness property.

And the barrier interaction is the sharp edge. A degraded chiplet becomes the slowest participant, so every barrier now runs at its rate (§37). The correct response is a partitioning change — give it less work — not a membership change, because removing a participant mid-phase leaves its already-dispatched jobs with no one waiting for them.

41. Recovery While a Job Executes

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
1. A job is dispatched and accepted. The chiplet begins computing.
2. UCIe enters recovery.
3. THE CHIPLET KEEPS COMPUTING. It has its operands in local SRAM;
   the link is irrelevant to its progress.
4. The job completes. Its result sits in a reserved buffer.
5. The completion cannot be delivered until the link returns.
6. The scheduler's timeout expires somewhere between steps 2 and 5.
StateOwnerSurvives?
The job's obligationthe chipletyes — and it may already be discharged
The command-table entrythe chipletyes
The reserved result buffer and the resultthe chipletyes — losing it wastes the whole execution
Local buffer ownership (§16)the chipletyes — the array is mid-tile
The scheduler's job entrythe scheduleryes — freeing it is §10
The generationthe scheduleryes — resetting it destroys §12
The barrier bitmap and phasethe scheduleryes
UCIe replay entries, credits, link statetransportrebuilt (14.2 §6)

A timeout tells the scheduler no completion arrived. It never says the job did not execute (12.4 §16).

42. Wrong Recovery — Blind Re-Dispatch

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// WRONG — a timeout is treated as evidence the job did not run.
always_ff @(posedge clk)
  if (job_q[id].valid && (age_q[id] >= JOB_TIMEOUT))
    redispatch(id);            // ← it may already have executed. Once.

Illustrative, with a job that accumulates into an output range.

EventOutput range
beforeV
first executionV + Δ
timeout during a link recoveryV + Δ — unchanged and correct
re-dispatch
second executionV + 2Δ
scheduler receives one completionbelieves V + Δ

Four properties.

Every component behaved correctly. The chiplet executed the job it was given, twice, because it was given it twice. The link recovered successfully. The timeout fired at a reasonable bound.

The corruption is silent and unbounded. Nothing detects a double application, and a flapping link can cause many.

And a barrier makes it worse. The re-dispatched job may complete in a later phase, so its completion arrives with a stale phase — which §33's phase check rejects, leaving the current phase's bit set and the barrier hung. One bug, two symptoms, in different runs.

The correct behaviour depends on idempotence (§43), and for a non-idempotent job it is to query, not re-dispatch: the scheduler re-establishes the job's state from the chiplet using {job_id, generation}, and re-dispatches only if the chiplet confirms it never accepted it — which is exactly what JOB_DISPATCHED versus JOB_ACCEPTED_REMOTE (§8) exists to distinguish.

43. Idempotent and Non-Idempotent Jobs

IdempotentNon-idempotent
Definitionre-execution reaches the same final statere-execution changes the state again
Generic exampleswrite a computed tile to a distinct output range; fill a rangeaccumulate into an output; any read-modify-write; append
Safe response to a timeoutre-dispatchquery, then decide (§42)
Recovery costlowrequires retained state on the chiplet

Three consequences.

Idempotence is a property of the operation and its operands, not of the opcode. A tile computation writing to a distinct output range is idempotent; the same computation accumulating into a shared output is not. A design classifying by opcode alone gets some jobs wrong, and the wrong direction is silent corruption.

So classify conservatively. When in doubt, treat a job as non-idempotent: the cost is a query path; the cost of the opposite error is unbounded.

And accumulation is extremely common in this domain, which is why this distinction is not a corner case here. A design whose jobs are predominantly non-idempotent needs the query path from the start, not as a later hardening.

44. The Job Scoreboard

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
// Verification-only. FOUR models, because four kinds of state can independently
// be wrong.
class ai_chiplet_scoreboard;
 
  // ---- Layer 1: JOB SEMANTIC model.
  typedef struct {
    int  job_id;
    int  generation;
    int  assigned_chiplet;
    int  phase;
    bit  accepted;
    int  semantic_executions;   // MUST be <= 1 (Section 36)
    int  input_tiles_expected;
    int  input_tiles_seen;
    int  output_tiles_expected;
    int  output_tiles_seen;
    bit  idempotent;            // from operands, not opcode (Section 43)
    bit  retired;
    bit  spanned_recovery;
  } job_model_t;
  job_model_t jobs [int];       // keyed by {job_id, generation}
 
  // ---- Layer 2: BUFFER OWNERSHIP model, per chiplet. Catches Section 18.
  typedef struct {
    int  owner;                 // 0 = none, 1 = mover, 2 = compute
    int  owning_job;
    bit  written_while_computing;
  } buf_model_t;
  buf_model_t bufs [int][2];    // [chiplet][buffer]
 
  // ---- Layer 3: BARRIER model. Catches Section 35.
  typedef struct {
    int  phase;
    bit [63:0] pending_mask;
    bit [63:0] actually_still_running;   // TRUTH, not the design's belief
    bit  advanced;
  } barrier_model_t;
  barrier_model_t barrier;
 
  // ---- Layer 4: MEMORY EFFECT model. What the outputs SHOULD contain.
  bit [DATA_W-1:0] expected_out [bit [ADDR_W-1:0]];
  bit              expected_written [bit [ADDR_W-1:0]];
 
  // ---- Catches Section 20 — descriptor paired with the wrong data.
  function void check_pairing(int key, int data_job_key);
    if (key != data_job_key)
      $error("PAIRING ERROR: descriptor key %0d launched with data key %0d "
           , "(Section 20)", key, data_job_key);
  endfunction
 
  // ---- Catches Section 18 — no CRC can.
  function void check_buffer_ownership(int chiplet, int buf);
    if (bufs[chiplet][buf].written_while_computing)
      $error("BUFFER OVERWRITE chiplet %0d buf %0d written while compute owned it "
           , "(Section 18)", chiplet, buf);
  endfunction
 
  // ---- Catches Section 35 — the barrier advanced while someone ran.
  function void check_barrier();
    if (barrier.advanced && (barrier.actually_still_running != '0))
      $error("BARRIER ADVANCED EARLY phase %0d, still running mask %0h (Section 35)",
             barrier.phase, barrier.actually_still_running);
  endfunction
 
  // ---- Catches Section 42.
  function void check_exactly_once(int key);
    if (jobs[key].semantic_executions > 1)
      $error("JOB %0d gen %0d executed %0d times (must be <= 1) — Section 42",
             jobs[key].job_id, jobs[key].generation, jobs[key].semantic_executions);
  endfunction
 
  // ---- Catches a job writing outside its declared output range.
  function void check_no_collateral(bit [ADDR_W-1:0] a);
    if (!expected_written.exists(a))
      $error("ADDRESS %0h written but no job declared it as output", a);
  endfunction
 
endclass

Architecture. Four models keyed by job-and-generation, by chiplet-and-buffer, by phase, and by address.

Layer 2 exists because no other layer can see §18. Buffer ownership is entirely local to a chiplet, crosses no interface, and is protected by no CRC. A verification plan without a buffer-ownership model has no detector for that failure at all.

Layer 3 tracks actually_still_running separately from pending_mask. The first is truth; the second is the design's belief. §35's bug is precisely the two diverging, and a model that only mirrors the design's bitmap reproduces the bug and passes.

And the job key is {job_id, generation}, because §10's alias is exactly a completion binding to the right identity and the wrong instance.

45. Coverage

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
covergroup cg_ai_chiplet @(posedge clk);
  option.per_instance = 1;
 
  // --- Scale and distribution (Sections 28-30, 37).
  cp_chiplets_active : coverpoint num_chiplets_with_work {
    bins one = {1}; bins some = {[2:NUM_CHIPLETS-1]}; bins all = {NUM_CHIPLETS};
  }
  cp_distribution : coverpoint job_distribution_class {
    bins balanced = {0}; bins skewed = {1}; bins single_hotspot = {2};
  }
  cp_rate_divergence : coverpoint chiplet_rate_spread_class {
    bins uniform = {0}; bins mild = {1}; bins one_stalled = {2};   // Section 29
  }
 
  // --- Buffers (Sections 16-18, 22).
  cp_buf_state : coverpoint buf_state_q_ut { bins each[] = {[0:3]}; }
  cp_pingpong_cycles : coverpoint completed_pingpong_cycles {
    bins none = {0}; bins one = {1}; bins several = {[2:$]};      // Section 16
  }
  cp_tile_fits : coverpoint tile_fits_in_local_buffer {
    bins fits = {1}; bins multi_pass = {0};                        // Section 22
  }
  cp_overwrite_injected : coverpoint mover_write_to_computing_injected;
 
  // --- Association (Sections 19-21).
  cp_pairing : coverpoint desc_data_arrival {
    bins in_order = {0}; bins data_first = {1}; bins desc_reordered = {2};
  }
  cp_tile_order : coverpoint tile_arrival_order {
    bins in_order = {0}; bins reordered = {1}; bins missing = {2};
  }
 
  // --- Resources (Sections 23-27).
  cp_job_slots : coverpoint job_table_occupancy {
    bins empty = {0}; bins mid = {[1:MAX_JOBS-1]}; bins full = {MAX_JOBS};
  }
  cp_result_slots : coverpoint result_slots_q {
    bins none = {0};                       // admission MUST refuse here
    bins few  = {[1:2]};
    bins many = {[3:$]};
  }
  cp_admit_refused : coverpoint admit_refusal_reason {
    bins cmd = {0}; bins input_buf = {1}; bins result = {2}; bins context = {3};
  }
 
  // --- Barriers (Sections 32-35).
  cp_barrier : coverpoint barrier_event {
    bins none = {0}; bins satisfied = {1};
    bins duplicate_completion = {2};       // Section 35
    bins stale_phase_completion = {3};     // Section 33
  }
  cp_barrier_critical : coverpoint barrier_critical_chiplet_is_degraded;
 
  // --- Transport composition (Sections 41-43).
  cp_link_event : coverpoint link_event_during_job {
    bins none = {0};
    bins retry = {1};
    bins recovery_before_accept = {2};     // re-dispatch IS safe here
    bins recovery_after_accept  = {3};     // THE case — Section 42
    bins degraded_return = {4};
  }
  cp_idempotent : coverpoint job_idempotence { bins idem = {0}; bins non_idem = {1}; }
  cp_stale_completion : coverpoint stale_generation_completion_arrived;
 
  // --- Crosses that carry the information.
  x_recovery_idem  : cross cp_link_event, cp_idempotent;        // Section 42
  x_barrier_dup    : cross cp_barrier, cp_chiplets_active;      // Section 35
  x_rate_dist      : cross cp_rate_divergence, cp_distribution; // Section 29
  x_result_admit   : cross cp_result_slots, cp_admit_refused;   // Section 25
endcovergroup

Seven bins worth calling out:

cp_link_event.recovery_after_accept crossed with cp_idempotent.non_idem. §42's flagship failure, and the single most valuable bin in the chapter.

cp_link_event.recovery_before_accept as the contrast. Re-dispatch is safe there, which is why the job FSM distinguishes the two states (§8).

cp_barrier.duplicate_completion. §35, and it must be injected — a duplicate does not occur spontaneously.

cp_overwrite_injected with the assertion enabled. §18's only detector, proven active.

cp_pairing.data_first and .desc_reordered. §20's preconditions, which an in-order environment never produces.

cp_rate_divergence.one_stalled crossed with cp_distribution. §29 — one chiplet stalled while the scheduler distributes, which is the exact state where depth-based and rate-based schedulers behave oppositely.

And cp_tile_fits.multi_pass. §22's cliff — a tile that does not fit locally and doubles the crossing traffic.

46. Debug Taxonomy

SignatureMost likely causeFirst instrument
Compute arrays idle, UCIe saturated§14, §39 — communication bounducie_stall_q per chiplet; bytes per operation
UCIe idle and compute idle§27, §39 — scheduler issue rate or job-table depthjob_table_occupancy; is it ever full?
A completion attached to the wrong job§10, §12 — early free plus identity reuse, no generationis the generation carried and checked?
Results wrong only with several chiplets§32–§35 — barrier advanced earlybarrier model's still-running mask at each advance
Results slightly wrong, near the end of each tile§18 — buffer overwritten by the moverbuffer-ownership model; is release at last read return?
The accelerator returns confident garbage, systematically§20 — descriptor paired with the wrong datapairing check; is the launch bound by identity?
Scaling stalls beyond N chiplets§37, §39 — a fixed communication or barrier costefficiency at each N; which term binds
One chiplet gets more and more work while falling behind§29 — scheduling by queue depthcompletions per window against depth
A newly added chiplet never receives work§30 — the rate estimate initialised at zeroRATE_INIT; can the estimate recover?
Recovery duplicates a job's output§42 — a timeout treated as evidencesemantic_executions against completions
A barrier hangs after a recovery§42's second symptom — a re-dispatched job completing in a later phasewhich phase the pending completion carried
Jobs stall after computing, nothing recovers§25 — result capacity not reserved at dispatchis the check at dispatch or at result time?

Row 5 is the one worth memorising. Results slightly wrong, concentrated at the end of each tile is §18's signature, and it is routinely misread as a numerical-precision issue rather than a data-integrity one.

47. Debug Checklist

  1. Which job — identity and generation? (§12)
  2. Which chiplet was it dispatched to, and was that captured? (§8)
  3. Was it accepted, or only dispatched? (§8, §42)
  4. What state is it in, and for how long? (§9)
  5. Was result capacity reserved at dispatch? (§23, §26)
  6. Was input buffer capacity reserved? (§23, §24)
  7. Which local buffer does compute own, and which is the mover filling? (§16)
  8. Is compute_done asserted at the last read issue or the last read return? (§18)
  9. Was the descriptor paired with its own data, by identity? (§20)
  10. Did all tiles arrive, and in what order? (§21)
  11. Does the tile fit in the local buffer, or is it multi-pass? (§22)
  12. What is this job's communication intensity against the bytes available per operation? (§14)
  13. Which barrier phase, and which participants are pending? (§33)
  14. Is the barrier a bitmap or a counter? (§35)
  15. Did any completion arrive twice, or with a stale phase? (§33, §35)
  16. What is each chiplet's completions-per-window, against its queue depth? (§29, §30)
  17. Is any chiplet degraded, and is it the barrier's critical path? (§28, §40)
  18. Did a UCIe retry or recovery occur, and before or after acceptance? (§41, §42)
  19. Did the scheduler re-dispatch, or query? (§42)
  20. Is this job idempotent — judged from its operands? (§43)
  21. Which of the six bounding terms is the minimum, at this N? (§39)
  22. Which of the four scoreboard layers diverged first? (§44)

48. Common Misconceptions

"More AI chiplets means linear performance." Efficiency falls with N because communication that does not shrink becomes a growing fraction, barriers run at the slowest participant's rate, and shared resources contend. In the worked example, throughput improves from 0.100 to 0.267 while efficiency falls from 100% to 33% — both statements are true and only one usually gets reported (§37, §38).

"Peak arithmetic throughput determines package performance." It is one of six terms in a minimum, and at realistic scales it is frequently not the binding one. Making the compute faster lowers the bytes available per operation and can make the boundary the constraint (§14, §39).

"A UCIe link is just a faster DMA path." It is a finite communication surface that does not grow when the arithmetic does, and it carries seven of the eight things the chiplet boundary introduced — only one of which is the computation (§5).

"Queue depth is enough for load balancing." A short queue means either a fast chiplet or a stalled one, and sending more work to the second creates a positive feedback loop. Depth measures occupancy; the quantity wanted is service rate (§29, §30).

"A job is complete when it is transmitted." Transmission is a transport event. The result arrives with no owner, the identity is reused, and the next job's completion binds to the wrong instance — a wrong answer delivered confidently with no error anywhere (§10).

"A duplicate completion is harmless." With a counter it advances a barrier while a participant is still computing, so the next phase reads a mixture of new and old values. With a bitmap it is idempotent and genuinely harmless — the difference is one term in a clear condition (§35).

"Local SRAM only affects latency." It decides how many times the same operands cross the boundary. A tile that does not fit is processed in multiple passes and its operands cross repeatedly, doubling communication intensity with no change to the workload (§22).

"Barriers are a software concern." A barrier is a distributed agreement implemented in hardware state, and its failure mode is not a hang but a phase advancing early — silent corruption of the next phase's inputs (§32, §35).

"Recovery means re-dispatch the job." The chiplet has its operands locally and does not stop when the link does; it may have completed already. For a non-idempotent job — and accumulation is common — a blind re-dispatch applies the operation twice, silently (§41, §42).

"Chiplet scaling problems are mostly PHY problems." The failures in this chapter are a buffer with an implicit owner, two queues paired by position, a counter used where a bitmap was needed, a scheduler using the wrong signal, and a timeout mistaken for evidence. Not one of them is a PHY problem, and none is detectable by transport verification (§18, §20, §35, §29, §42).

49. Understanding Check

50. Summary and What Comes Next

An AI chiplet is a compute island with local state, local queues, local memory demand and a finite communication surface. Replication multiplies the arithmetic and not the surface, and seven of the eight things the boundary introduces are communication or bookkeeping.

A job is an obligation the scheduler cannot withdraw. Its entry outlives the scheduler's own uncertainty, its identity carries a generation, and its FSM has no arm driven by a transport event.

Local buffers have owners and no CRC protects ownership. The overwrite is partial, pipeline-depth dependent, made more likely by a faster link, and detectable only by an ownership model.

Descriptors and data travel separately and must be bound by identity. Paired by position, one reordering makes every subsequent pair wrong — structurally valid, systematically nonsense.

Reserve before you dispatch. Result capacity checked at dispatch is the difference between backpressure and a deadlock holding the system's most expensive resource idle.

Queue depth is not service rate, and choosing by depth feeds the bottleneck it should avoid.

A barrier needs a bitmap, because a counter advances a phase while a participant is still writing — and the duplicate that causes it is a correct transport retry.

And scaling is a six-term minimum, not a multiplication. Throughput can rise while efficiency falls to a third, communication that does not shrink becomes the majority of elapsed time, and the term a datasheet describes is usually not the binding one.

This chapter treated one chiplet at a time, with the fabric between them assumed to work. The next chapter removes that assumption and looks at what happens when several accelerators contend for one on-package communication resource — where routing, arbitration, credits, multicast and ordering become a distributed resource-allocation problem, and where the failures are starvation and deadlock rather than corruption.

Browse the full path on the UCIe tutorials index.