23992548f7
Three coupled fixes that recover small-tile GEMM pipeline efficiency from 53% to 88% (32x3072x32 load_ref, composite_window basis). 1. PE_DMA channel-hold (ADR-0014 D4 clarified): both the _handle_with_hooks (PeInternalTxn) and _pipeline_process (TileToken) paths used to hold the cap=1 DMA channel through the full HBM round-trip, which double-serialized with the HBM_CTRL's own per-PC `available_at` model and prevented back-to-back tile DMAs from amortizing their per-request head latency. Channel is now released after the request is enqueued onto the next hop; HBM serialization is HBM_CTRL's responsibility alone. Tests: new test_pe_dma_back_to_back_pipelining as the oracle (asserts wall < 75% of strict-serialized N x single_op). Existing test_pe_dma_record_start_after_channel_acquire rewritten to assert t_start clustering (channel released fast) instead of the old round-trip-hold invariant. test_pe_dma_same_channel_serializes still passes — HBM_CTRL preserves ordering. Probe regression: PE→local-HBM 32 KiB stays at 141 ns (single-request, unaffected). 2. milestone_1h_gemm bench: matmul_composite was reading MATMUL_M/K/N env vars at module load, so every sweep row replayed the cached 256³ result; values now read inside run(). Drops the stale sys.modules deletion hack. 3. Analytic ideal-pipeline model: dropped the (n_mn-1)·dma_w_per_pair penalty (over-pessimistic for under-tile shapes — it pushed measured > theoretical) and replaced the D_STAGES-derived head with empirical T_PIPELINE_FILL=60 ns / T_PIPELINE_TAIL=30 ns. Max analytic-vs-measured gap across all 7 swept shapes now 2.2 ppt (was 9-44 ppt under the old constants). Paper updates: - §3 (GEMM): 78%→88% measured at 48 tiles, 23%→15% at 1 tile, stage breakdown numbers refreshed (DMA in / Fetch / GEMM all ~785 ns at K=3072), analytic-vs-measured agreement tightened to "within 2.2 ppt". - §2.4 (Accuracy): GEMM tracking claim refreshed accordingly. - §5 (GQA): restore long-ctx 4-cases figures into figures/ (they were dropped from bench output dir as derived artifacts in92b9221/e45626cbut §5 still cites them by name). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
459 lines
15 KiB
Markdown
459 lines
15 KiB
Markdown
# ADR-0014: PE Pipeline Execution Model
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
This ADR defines the PE-internal kernel execution model:
|
|
|
|
- Role decomposition of PE-internal components
|
|
- Command dispatch paths (simple / composite / multi-op composite with epilogue)
|
|
- TileToken-based self-routing pipeline (scheduler does dispatch + completion only)
|
|
- TCM-centric dataflow with a register-file intermediary
|
|
- Engine resource model
|
|
- Observability and trace contract
|
|
- Topology representation
|
|
|
|
PE-internal structure (7 components in scope; 2 cross-referenced):
|
|
|
|
- `pe_cpu`, `pe_scheduler`, `pe_dma`, `pe_fetch_store`, `pe_gemm`, `pe_math`,
|
|
`pe_tcm` — defined here
|
|
- `pe_mmu` — VA model, defined in ADR-0011 D-VA
|
|
- `pe_ipcq` — collective communication, defined in ADR-0023
|
|
|
|
The goal is a deterministic, trace-friendly execution contract that keeps
|
|
each block independently swappable.
|
|
|
|
## Decision
|
|
|
|
### D1. PE-internal component roles
|
|
|
|
**PE_CPU**
|
|
|
|
- Executes kernel instruction stream / control logic.
|
|
- Generates PE commands and submits them to `PE_SCHEDULER` (via
|
|
`PeInternalTxn`).
|
|
- Does NOT enqueue work directly into engine queues.
|
|
|
|
**PE_SCHEDULER**
|
|
|
|
- Sole dispatcher inside a PE.
|
|
- Receives commands from `PE_CPU`. Dispatch by command type:
|
|
- Simple command (`DmaReadCmd`, `DmaWriteCmd`, `GemmCmd`, `MathCmd`)
|
|
→ forward directly to the target engine.
|
|
- `CompositeCmd` → generate a `TilePlan`, feed tiles into the pipeline
|
|
via a single `_feed_loop` (D6).
|
|
- Does not participate in stage-to-stage chaining within a composite;
|
|
that is handled by token self-routing (D6).
|
|
|
|
**PE_DMA**
|
|
|
|
- Handles memory transfers between TCM and external memory domains
|
|
(HBM, shared SRAM, cross-cube UCIe) through the cube NOC.
|
|
- Two execution channels:
|
|
- `DMA_READ` (capacity = 1) and `DMA_WRITE` (capacity = 1) — see D4.
|
|
- Additional virtual channels:
|
|
- `vc_compute` — load/store/writeback traffic for GEMM/MATH tiles.
|
|
- `vc_comm` — IPCQ collective send data (defined in ADR-0023 D8).
|
|
|
|
**PE_FETCH_STORE**
|
|
|
|
- TCM ↔ Register File transfer unit.
|
|
- Isolates register-file access semantics from compute engines so that
|
|
GEMM/MATH stay pure compute components.
|
|
- BW-based latency model; TCM access contention naturally serializes
|
|
through `PE_TCM`'s BW resource.
|
|
|
|
**PE_GEMM**
|
|
|
|
- MAC array. Reads operands from the register file; writes results to
|
|
the register file. Does not touch `PE_TCM` directly.
|
|
|
|
**PE_MATH**
|
|
|
|
- Element-wise / reduction / SIMD unit. Reads / writes the register file.
|
|
|
|
**PE_TCM**
|
|
|
|
- Tightly-coupled scratchpad with BW-serialized access. Two logical
|
|
regions partitioned by ownership (see D5).
|
|
|
|
**Cross-referenced components** (defined elsewhere):
|
|
|
|
- `pe_mmu` — VA→PA translation per access (ADR-0011 D-VA).
|
|
- `pe_ipcq` — collective ring buffers and peer endpoint metadata
|
|
(ADR-0023).
|
|
|
|
### D2. Command lifecycle and queues
|
|
|
|
`PE_SCHEDULER` maintains three logical structures:
|
|
|
|
**SubmissionQueue** — written by `PE_CPU`; consumed by the scheduler.
|
|
|
|
**InflightTable** — owned and mutated only by `PE_SCHEDULER`; tracks
|
|
expanded sub-commands, dependency state, engine assignment, and
|
|
completion status.
|
|
|
|
**CompletionQueue** — written by `PE_SCHEDULER`; holds final completion
|
|
records.
|
|
|
|
**Single-writer rule**: only `PE_SCHEDULER` mutates command completion
|
|
state. Engines report completion via explicit events / messages
|
|
consumed by the scheduler.
|
|
|
|
**Command completion**: when all sub-commands complete, `PE_SCHEDULER`
|
|
publishes a completion record.
|
|
|
|
### D3. Dispatch modes
|
|
|
|
#### D3.1 Simple command
|
|
|
|
A simple command expands to exactly one engine sub-command:
|
|
|
|
- `DmaReadCmd` / `DmaWriteCmd` → `PE_DMA`
|
|
- `GemmCmd` → `PE_GEMM`
|
|
- `MathCmd` → `PE_MATH`
|
|
|
|
Flow:
|
|
|
|
```text
|
|
PE_CPU → SubmissionQueue → PE_SCHEDULER → engine queue → engine execution
|
|
→ completion → PE_SCHEDULER → CompletionQueue
|
|
```
|
|
|
|
#### D3.2 Composite command (single-op tiled pipeline)
|
|
|
|
The default `CompositeCmd` runs a single compute op as a tile-pipelined
|
|
sequence:
|
|
|
|
```text
|
|
DMA_READ → FETCH (TCM → RF) → COMPUTE (GEMM | MATH) → STORE (RF → TCM) → DMA_WRITE
|
|
```
|
|
|
|
`PE_SCHEDULER` splits the DMA payload into hardware tiles and emits one
|
|
`TileToken` per tile with a monotonically increasing `tile_id`.
|
|
|
|
Tile dependency (within one tile `t`):
|
|
|
|
```text
|
|
DMA_READ(t) → FETCH(t) → COMPUTE(t) → STORE(t) → DMA_WRITE(t)
|
|
```
|
|
|
|
Inter-tile overlap is allowed wherever engine resources permit
|
|
(D4 governs the constraints):
|
|
|
|
```text
|
|
DMA_READ(t+1) ∥ COMPUTE(t)
|
|
DMA_WRITE(t-1) ∥ COMPUTE(t)
|
|
```
|
|
|
|
#### D3.3 Multi-op composite (head + epilogue with scope)
|
|
|
|
A `CompositeCmd` MAY carry `ops: tuple[OpSpec, ...]` to express a
|
|
multi-op pipeline:
|
|
|
|
```python
|
|
@dataclass(frozen=True)
|
|
class OpSpec:
|
|
kind: str # "gemm" | "math.exp" | "math.bias_add" | ...
|
|
scope: Scope # "per_k_tile" | "per_output_tile" | "once"
|
|
...
|
|
```
|
|
|
|
- `ops[0]` (head) defines tile geometry (e.g., the head GEMM determines
|
|
M/K/N partition).
|
|
- `ops[1:]` (epilogue) are subsequent stages whose `scope` decides how
|
|
often they fire:
|
|
- `per_k_tile` — every K-reduction step.
|
|
- `per_output_tile` — once per output tile.
|
|
- `once` — once per kernel.
|
|
|
|
Cross-engine chains (e.g., GEMM head → MATH epilogue) are natural —
|
|
each stage is dispatched via token self-routing (D6), so GEMM and MATH
|
|
participate serially within the same composite even though they share
|
|
the compute slot (D4).
|
|
|
|
The empty-`ops` form is the legacy single-op path.
|
|
|
|
### D4. Engine resource model
|
|
|
|
**DMA engine**:
|
|
|
|
- `DMA_READ`: `simpy.Resource(capacity=1)`.
|
|
- `DMA_WRITE`: `simpy.Resource(capacity=1)`.
|
|
- Both channels run concurrently (READ ∥ WRITE allowed).
|
|
- Within a channel, requests serialize **at the issue path** (READ ∥ READ
|
|
issued out-of-order is disallowed; same for WRITE). The channel is
|
|
held only until the request is enqueued onto the next hop (router),
|
|
then released — it does **not** block until the HBM round-trip
|
|
completes. Multiple in-flight requests are therefore allowed, and
|
|
HBM-level serialization is the responsibility of the HBM controller's
|
|
per-PC `available_at` timestamps (ADR-0033 D1). This is what allows
|
|
back-to-back small-tile DMAs to amortize the per-request head latency
|
|
through the fabric.
|
|
- `vc_comm` is an orthogonal channel for IPCQ traffic defined in
|
|
ADR-0023 D8 — out of scope for this ADR.
|
|
|
|
**Compute engine**:
|
|
|
|
- `accel_slot`: `simpy.Resource(capacity=1)` shared by `PE_GEMM` and
|
|
`PE_MATH`.
|
|
- At most one compute op runs at a time within a PE.
|
|
- Multi-op composite chains (D3.3) execute their compute stages serially
|
|
through this slot; token self-routing (D6) ensures the next stage
|
|
starts only after the previous compute releases the slot.
|
|
|
|
**Engine completion**: each engine emits a completion event consumed by
|
|
the scheduler / `PipelineContext` (D6).
|
|
|
|
### D5. Dataflow
|
|
|
|
**Input path (HBM source)**:
|
|
|
|
```text
|
|
HBM → cube NOC → PE_DMA (DMA_READ) → PE_TCM
|
|
PE_TCM → PE_FETCH_STORE → Register File
|
|
Register File → PE_GEMM | PE_MATH
|
|
```
|
|
|
|
**Input path (shared SRAM source)**:
|
|
|
|
```text
|
|
Shared SRAM → cube NOC → PE_DMA (DMA_READ) → PE_TCM
|
|
PE_TCM → PE_FETCH_STORE → Register File
|
|
```
|
|
|
|
**Output path (HBM destination)**:
|
|
|
|
```text
|
|
Register File → PE_FETCH_STORE → PE_TCM
|
|
PE_TCM → PE_DMA (DMA_WRITE) → cube NOC → HBM
|
|
```
|
|
|
|
GEMM/MATH never touch `PE_TCM` directly — `PE_FETCH_STORE` is the
|
|
single TCM↔register-file gateway. This makes TCM BW contention
|
|
explicit and lets fetch unit policies (e.g., prefetch) be replaced
|
|
independently of compute engines.
|
|
|
|
#### D5.1 PE_TCM partitioning
|
|
|
|
`PE_TCM` is split into two logical regions:
|
|
|
|
**SchedulerReservedTCM**
|
|
|
|
- Owned exclusively by `PE_SCHEDULER`.
|
|
- Holds composite-command tile buffers.
|
|
- `PE_SCHEDULER` partitions this region, assigns buffers per DMA_READ /
|
|
COMPUTE / DMA_WRITE stage, guarantees input/output separation, and
|
|
manages tile-buffer lifetimes.
|
|
|
|
**AllocatableTCM**
|
|
|
|
- General-purpose region managed by `PEMemAllocator`.
|
|
- Used for host / DP-visible allocations.
|
|
|
|
**Visibility rule (hard isolation)**: `PEMemAllocator` MUST NOT see or
|
|
allocate inside `SchedulerReservedTCM`. The reserved region is excluded
|
|
from allocator-managed ranges by construction.
|
|
|
|
**Tile buffer rules**:
|
|
|
|
- Input and output buffers within `SchedulerReservedTCM` MUST NOT
|
|
overlap during a tile's active lifetime.
|
|
- A tile buffer remains valid until the corresponding `DMA_WRITE`
|
|
completes.
|
|
- Buffer reuse is permitted only after the consuming tile's lifetime
|
|
ends.
|
|
|
|
### D6. TileToken self-routing pipeline
|
|
|
|
A composite's stage-to-stage progression happens **without** routing
|
|
through the scheduler. Each component forwards the token directly to
|
|
the next stage's component using the token's `plan`:
|
|
|
|
```text
|
|
Scheduler → DMA → Fetch → GEMM → Math (epi) → Store → DMA_WB → (complete)
|
|
↑ chaining: no scheduler hop ↑
|
|
PipelineContext.complete_tile()
|
|
```
|
|
|
|
This mirrors real-HW done-wire chains. The scheduler handles only
|
|
**initial dispatch + completion aggregation**.
|
|
|
|
#### TilePlan / Stage
|
|
|
|
```python
|
|
class StageType(Enum):
|
|
DMA_READ = 0
|
|
FETCH = 1
|
|
GEMM = 2
|
|
MATH = 3
|
|
STORE = 4
|
|
DMA_WRITE = 5
|
|
|
|
@dataclass(frozen=True)
|
|
class Stage:
|
|
stage_type: StageType
|
|
component: str # topology node id (e.g., "sip0.cube0.pe0.pe_dma")
|
|
params: dict # stage-specific parameters
|
|
|
|
@dataclass(frozen=True)
|
|
class TilePlan:
|
|
tile_id: int
|
|
stages: tuple[Stage, ...]
|
|
```
|
|
|
|
#### TileToken
|
|
|
|
```python
|
|
@dataclass
|
|
class TileToken:
|
|
tile_id: int
|
|
pipeline_ctx: PipelineContext
|
|
plan: TilePlan
|
|
stage_idx: int
|
|
params: dict # cached current stage params
|
|
data_op: bool = True # op_log opt-in (ADR-0020 D4)
|
|
```
|
|
|
|
Single-owner invariant: a token is owned by exactly one component at a
|
|
time. Lifecycle: scheduler creates with `stage_idx=0` → component
|
|
`_process()` → increment `stage_idx` → put to next stage's `in_port` →
|
|
last stage calls `pipeline_ctx.complete_tile()`.
|
|
|
|
#### PipelineContext (exactly-once completion)
|
|
|
|
```python
|
|
@dataclass
|
|
class PipelineContext:
|
|
id: str
|
|
total_tiles: int
|
|
completed_tiles: int = 0
|
|
done_event: simpy.Event = None
|
|
|
|
def complete_tile(self) -> None:
|
|
self.completed_tiles += 1
|
|
if self.completed_tiles == self.total_tiles:
|
|
self.done_event.succeed()
|
|
```
|
|
|
|
Each tile's last stage MUST call `complete_tile()` exactly once.
|
|
Duplicate calls are bugs (SimPy `Event` can succeed at most once).
|
|
|
|
#### Feed ordering
|
|
|
|
`PE_SCHEDULER` has exactly one `_feed_loop` process consuming a
|
|
`_pending_feeds` FIFO. Composite commands are enqueued in submission
|
|
order; tile feed for a command runs to completion before the next
|
|
command's feed begins. **Tile-feed interleaving between commands is
|
|
disallowed.**
|
|
|
|
Within a single command's tiles, downstream pipeline overlap arises
|
|
naturally — earlier tiles progress through later stages while the feeder
|
|
keeps pushing remaining tiles into the first stage queue (SimPy Store
|
|
backpressure governs flow control). If the first-stage queue is full,
|
|
only the feeder blocks; the scheduler worker's inbox processing
|
|
continues.
|
|
|
|
#### Token routing pattern (base class)
|
|
|
|
```python
|
|
def _pipeline_worker(self, env):
|
|
while True:
|
|
token = yield self._inbox.get()
|
|
yield from self._process(env, token) # stage-specific logic
|
|
next_idx = token.stage_idx + 1
|
|
if next_idx < len(token.plan.stages):
|
|
next_stage = token.plan.stages[next_idx]
|
|
token.stage_idx = next_idx
|
|
token.params = next_stage.params
|
|
yield self.out_ports[next_stage.component].put(token)
|
|
else:
|
|
token.pipeline_ctx.complete_tile()
|
|
```
|
|
|
|
Each component implements only `_process()`; chaining lives in the
|
|
base class.
|
|
|
|
### D7. Observability and trace contract
|
|
|
|
The simulator emits deterministic trace events:
|
|
|
|
- `command_submitted`
|
|
- `sub_command_dispatched`
|
|
- `engine_start`
|
|
- `engine_complete`
|
|
- `tile_ready`
|
|
- `command_complete`
|
|
|
|
For identical inputs, trace ordering MUST be deterministic.
|
|
|
|
### D8. Topology representation
|
|
|
|
PE-internal components are declared in `cube.pe_template`:
|
|
|
|
```yaml
|
|
pe_template:
|
|
components:
|
|
pe_cpu: { kind: pe_cpu, impl: builtin.pe_cpu, attrs: { overhead_ns: ... } }
|
|
pe_scheduler: { kind: pe_scheduler, impl: builtin.pe_scheduler, attrs: { overhead_ns: ... } }
|
|
pe_dma: { kind: pe_dma, impl: builtin.pe_dma, attrs: { rd_engines: 1, wr_engines: 1 } }
|
|
pe_fetch_store: { kind: pe_fetch_store, impl: builtin.pe_fetch_store, attrs: { ... } }
|
|
pe_gemm: { kind: pe_gemm, impl: builtin.pe_gemm, attrs: { shared_resource: accel_slot, ... } }
|
|
pe_math: { kind: pe_math, impl: builtin.pe_math, attrs: { shared_resource: accel_slot, ... } }
|
|
pe_tcm: { kind: pe_tcm, impl: builtin.pe_tcm, attrs: { size_mb: ..., read_bw_gbs: ..., write_bw_gbs: ... } }
|
|
pe_mmu: { kind: pe_mmu, impl: builtin.pe_mmu, attrs: { ... } } # ADR-0011 D-VA
|
|
pe_ipcq: { kind: pe_ipcq, impl: builtin.pe_ipcq, attrs: { ... } } # ADR-0023
|
|
links:
|
|
# Scheduler dispatch edges (initial)
|
|
scheduler_to_dma_mm: 0.0
|
|
scheduler_to_fetch_store_mm: 0.0
|
|
scheduler_to_gemm_mm: 0.0
|
|
scheduler_to_math_mm: 0.0
|
|
# Pipeline chaining edges (token self-routing per D6)
|
|
dma_to_fetch_store_mm: 0.0
|
|
fetch_store_to_gemm_mm: 0.0
|
|
fetch_store_to_math_mm: 0.0
|
|
gemm_to_fetch_store_mm: 0.0
|
|
gemm_to_math_mm: 0.0
|
|
math_to_fetch_store_mm: 0.0
|
|
fetch_store_to_dma_mm: 0.0
|
|
fetch_store_to_tcm_bw_gbs: ...
|
|
```
|
|
|
|
Template is instantiated once per PE. PE instances are derived from
|
|
`cube.pe_layout` (corner placement). External connectivity (PE_DMA ↔
|
|
cube NOC ↔ HBM, etc.) is modeled at the cube level (ADR-0017 D4).
|
|
|
|
## Consequences
|
|
|
|
### Positive
|
|
|
|
- Each block is an independent topology node — individually swappable
|
|
via DI (ADR-0015).
|
|
- PE-internal structure is visible in the topology graph.
|
|
- Components do not know their downstream — plan-based routing gives
|
|
flexibility (e.g., epilogue chains require no scheduler change).
|
|
- DMA and compute overlap naturally via SimPy Store backpressure.
|
|
- Multi-op composite expresses fused operations (e.g., GEMM + bias_add)
|
|
without engine-level coupling.
|
|
- TCM access contention is realistic — `PE_FETCH_STORE` is the single
|
|
TCM↔RF gateway.
|
|
|
|
### Negative
|
|
|
|
- Intra-PE component count is higher than a coarser model (7 base + 2
|
|
cross-referenced) — more topology nodes/edges.
|
|
- Intra-PE token forwarding is explicit in traces (acceptable trade for
|
|
HW fidelity).
|
|
|
|
## Links
|
|
|
|
- ADR-0011 D-VA (PE_MMU component, VA translation)
|
|
- ADR-0015 D4 (component port/wire model)
|
|
- ADR-0020 (greenlet kernel execution / two-pass)
|
|
- ADR-0023 (PE_IPCQ + PE_DMA virtual channels)
|
|
- SPEC R3, R4
|