Files
kernbench2/docs/adr-proposed/DDD-0065-flat-ops-composite-softmax-merge-detailed-design.md
T
ywkang a8c50238c6 gqa(adr): ADR-0064 Rev2 (structural dispatch cost) + ADR-0065/DDD-0065 (flat-ops composite + softmax_merge recipe)
ADR-0064 Revision 2: replace per-op-type calibration table with a
structural formula `FIXED_PER_CMD + cmd.logical_bytes × R`, defaults
anchored at typical composite ≈ 45 ns. Topology yaml override under
`pe_cost_model:` block; logical_bytes property per PE command.

ADR-0065: implement ADR-0060 §5.6 / §8 item 4 carve-out as a flat-ops
CompositeCmd (no head/epilogue structural fields — position + scope
drives placement) + first stateful recipe `softmax_merge` (MATH-only
8-step). RECIPE_DESCRIPTORS lives in TLContext-adjacent module only;
PE_SCHEDULER stays recipe-free and auto-inserts DMAs from operand
`space`. Strict-FIFO RW hazard tracker; ≤1 GEMM per composite
invariant. User-facing `tl.composite(prologue=[...], op=, epilogue=[...])`
API preserved; existing benches unchanged (meaning-preserving refactor).

DDD-0065: implementation-ready phased plan (P0=ADR-0064 Rev2 first,
P1-P6 for ADR-0065), file plan, recipe engine sequence, scheduler
plan-gen algorithm, RW tracker design, test matrix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 16:11:20 -07:00

525 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Detailed Design Document — Flat-ops `CompositeCmd` + `softmax_merge` recipe (ADR-0065)
**Status:** Draft (companion to ADR-0065, Proposed)
**Scope:** implementation-ready plan for the `CompositeCmd` flat-ops
restructure, the `softmax_merge` recipe, and the decode opt2 variant in
`_gqa_decode_long.py`.
**Audience:** reviewer / implementer resuming after the ADR-0065 design
round. Read **ADR-0065 first** (boundary D5 + D7 set the layering), and
**ADR-0064 Revision 2** (dispatch cost model that opt2's win depends on).
---
## 1. Goal and success criteria
**Goal:** make decode opt2 *runnable*, *numerically equivalent to opt3*,
and *measurably cheaper in PE_CPU dispatch* under the ADR-0064 Rev2 cost
model. Existing benches keep their (post-ADR-0064 Rev2) goldens
unchanged.
**Success criteria:**
1. **Refactor safety.** `CompositeCmd` flat-ops restructure preserves the
op_log byte-for-byte for every existing bench
(`tests/<existing>` all green, no goldens diff).
2. **Recipe correctness.** opt2 final `(m, l, O)` matches opt3 within fp
tolerance in data mode (`enable_data=True`).
3. **Boundary preservation.** PE_SCHEDULER imports nothing recipe-related;
PE_MATH/PE_GEMM/PE_DMA see only `Stage.params["op_kind"]` (no new
engine code).
4. **Dispatch ratio.** Per-tile PE_CPU dispatch cycles
`ratio(opt3 / opt2) ≈ 2.4×` at default ADR-0064 calibration.
5. **K-before-V invariant.** op_log of decode opt2 shows zero V-related
DMA during #2's MATH prologue.
**Non-goals:** prefill (causal `if` in kernel control flow, separate
work); RW-aware scheduler reorder (strict FIFO ships); user-defined
recipes; new engine micro-ops in PE_MATH (it stays op_kind-opaque).
---
## 2. Where the design sits in the current code
```
runtime_api (host)
RuntimeContext.zeros/empty/launch context.py
│ KernelLaunchMsg
sim_engine
GraphEngine(enable_data=…) engine.py
MemoryStore memory_store.py
components / PE pipeline (per PE)
PE_CPU → PE_SCHEDULER → PE_{DMA,GEMM,MATH,IPCQ,TCM}
greenlet kernel ↔ SimPy kernel_runner.py, pe_cpu.py
CompositeCmd → tile plan → engines pe_scheduler.py (ADR-0014 D6)
tl programming model tl_context.py
composite / dot / load / store / scratch_scope
── ADR-0065 NEW ──────────────────────────
tl_recipes.py RECIPE_DESCRIPTORS, EngineOp,
RecipeDescriptor, PrimaryOutSpec
```
The new module `tl_recipes.py` sits beside `tl_context.py`. PE_SCHEDULER
(`pe_scheduler.py`) and `tiling.py` extend to consume the flat ops list
without importing `tl_recipes`.
---
## 3. File plan
### 3.1 Modified production files
| File | Change | ADR ref |
|---|---|---|
| `src/kernbench/common/pe_commands.py` | `CompositeCmd` flat-ops dataclass; `OpSpec.operands: dict`; new `Scope` value semantics (KERNEL position-based); `logical_bytes` property on every `PeCommand`. | 0065 D1/D2, 0064 D2 |
| `src/kernbench/triton_emu/tl_context.py` | `composite(prologue=[...], out=TensorHandle, …)`; recipe lowering pass (RECIPE_DESCRIPTORS lookup, scratch alloc, OpSpec expansion, primary_out auto-bind); legacy API preserved (`op="gemm", a, b, epilogue=[...]` still works, internally lowers to flat ops). | 0065 D1/D5 |
| `src/kernbench/triton_emu/tl_recipes.py` (NEW) | `RecipeDescriptor`, `PrimaryOutSpec`, `EngineOp` dataclasses; `RECIPE_DESCRIPTORS["softmax_merge"]` entry; expansion helper. | 0065 D5 |
| `src/kernbench/components/builtin/pe_scheduler.py` | `_generate_plan` rewritten for flat ops: scan for GEMM (≤1), position-based KERNEL placement, OUTPUT_TILE/K_TILE epilogue, strict-FIFO RW tracker keyed by handle id. | 0065 D3/D6 |
| `src/kernbench/components/builtin/tiling.py` | `generate_plan_from_ops(ops, rw_handles, pe_prefix)` replaces `generate_gemm_plan` / `generate_math_plan` (or wraps them). DMA auto-insertion from `handle.space`. Epilogue `add` stage emits `other_handle` in params (GAP 9 fix). | 0065 D3/D4 |
| `src/kernbench/components/builtin/pe_cpu.py` | dispatch cost from `cmd.logical_bytes` via ADR-0064 Rev2 formula; topology config override. | 0064 D1/D4 |
| topology yaml (e.g., `topologies/llama70b_4sip.yaml`) | optional `pe_cost_model:` block under PE node attrs. | 0064 D4 |
| `src/kernbench/benches/_gqa_decode_long.py` | opt2 variant — `tl.composite(prologue=[{"op":"softmax_merge", …}], op="gemm", b=V_ref, out=O, epilogue=[{"op":"add", "other":O}])`. | 0065 §Migration P3 |
### 3.2 New tests
| File | Covers |
|---|---|
| `tests/test_composite_flat_ops_refactor.py` | every existing bench's op_log byte-equal pre/post refactor (parametrized over bench list) |
| `tests/test_tl_recipe_softmax_merge.py` | recipe lowering — given inputs, asserts CompositeCmd has 10 ops in expected order + `rw_handles == (m, l, O)` |
| `tests/test_pe_scheduler_strict_fifo.py` | two consecutive composites sharing rw_handles complete in dispatch order; no stage interleave |
| `tests/test_pe_scheduler_auto_dma.py` | `space=hbm` operand → DMA_READ stage; `space=tcm` → none; `out.space=hbm` → DMA_WRITE |
| `tests/test_pe_cost_model.py` | ADR-0064 Rev2 — typical 1-GEMM composite at 45 ns; topology override path; `PeCpuOverheadCmd` bypass |
| `tests/attention/test_gqa_decode_opt2.py` | opt2 numeric parity with opt3; K-before-V DMA invariant; per-tile dispatch ratio measurement |
---
## 4. Data model — `CompositeCmd` flat-ops
### 4.1 New shape
```python
@dataclass(frozen=True)
class CompositeCmd:
completion: CompletionHandle
ops: tuple[OpSpec, ...] # ordered MATH/GEMM ops
rw_handles: tuple[TensorHandle, ...] = () # cross-composite hazard
data_op: bool = True
@property
def logical_bytes(self) -> int:
return (
4 # framing
+ 1 + sum(op.logical_bytes for op in self.ops)
+ 1 + 8 * len(self.rw_handles)
)
```
The previous `op`, `a`, `b`, `out_addr`, `out_nbytes`, `math_op` fields
are **removed**. All info lives in `ops[i]`.
### 4.2 `OpSpec` extended
```python
@dataclass(frozen=True)
class OpSpec:
kind: str # "gemm" | "rmax" | "exp_diff" | ...
scope: Scope # KERNEL | K_TILE | OUTPUT_TILE
operands: dict[str, TensorHandle]
extra: dict[str, Any] = field(default_factory=dict)
out: TensorHandle | None = None
@property
def logical_bytes(self) -> int:
return (
1 + 1 # opcode + scope enum
+ 1 + 8 * len(self.operands) # named handles
+ (8 if self.out is not None else 0)
+ 1 + 4 * len(self.extra) # scalars
)
```
Backward compat: existing `operands: tuple[Any, ...]` use sites
(epilogue `bias`, `scale`, `add`, etc.) migrate to `operands={"other":
h}` / `operands={"src": h}` per a small lookup in
`pe_commands.EPILOGUE_OPS`.
### 4.3 `Scope` semantics (position-based for KERNEL)
`Scope` keeps its three values (`KERNEL`, `K_TILE`, `OUTPUT_TILE`).
What changes: `KERNEL` no longer means "once anywhere"; it now means
"execute at this op's position in the sequence — once". Combined with
position relative to the GEMM op:
- KERNEL ops *before* GEMM → pre-tile-loop (single shot)
- KERNEL ops *after* GEMM → post-tile-loop (single shot)
- K_TILE / OUTPUT_TILE ops *after* GEMM → inside tile loop as before
---
## 5. Recipe: `softmax_merge`
### 5.1 `tl_recipes.py` definitions
```python
@dataclass(frozen=True)
class PrimaryOutSpec:
from_shape: str # operand name to copy shape from
from_dtype: str # operand name to copy dtype from
transform: str # "identity" | "trans" (forward-compat)
@dataclass(frozen=True)
class EngineOp:
engine: Literal["MATH", "GEMM", "DMA"]
op_kind: str # PE_MATH op_kind label (opaque to engine)
# Slot names: any of "src", "src_a", "src_b", "src_c", "dst" — these
# are RecipeDescriptor.operands names or internal scratch names.
src: str | None = None
src_a: str | None = None
src_b: str | None = None
src_c: str | None = None
dst: str | None = None
reduce_axis: int | None = None
bcast_axis: int | None = None
@dataclass(frozen=True)
class RecipeDescriptor:
operands: dict[str, str] # name → "R" | "RW"
primary_out: PrimaryOutSpec | None # implicit output spec
tile_alignment: Literal["single_shot", "tile_aligned"]
internal_scratch_bytes_fn: Callable[..., int]
engine_seq: tuple[EngineOp, ...]
RECIPE_DESCRIPTORS: dict[str, RecipeDescriptor] = { ... }
```
### 5.2 Engine sequence (8 MATH ops)
| # | engine | op_kind | reads (slots) | writes (slots) | extras |
|---|---|---|---|---|---|
| 1 | MATH | `rmax` | s | m_loc | `reduce_axis=-1` |
| 2 | MATH | `max_elem` | m, m_loc | m_new | — |
| 3 | MATH | `exp_diff` | m, m_new | corr | — |
| 4 | MATH | `exp_diff` | s, m_new | P (primary_out)| `bcast_axis=0` |
| 5 | MATH | `rsum` | P | l_loc | `reduce_axis=-1` |
| 6 | MATH | `fma` | l, corr, l_loc | l | — |
| 7 | MATH | `mul_bcast` | O, corr | O | `bcast_axis=1` |
| 8 | MATH | `copy` | m_new | m | — |
Intermediates `m_loc, m_new, corr, P, l_loc` are scratch slots allocated
by TLContext before lowering. `P` is the recipe's primary output; auto-
bound to the head GEMM's `operands["a"]`.
### 5.3 PE_MATH op_kind additions
PE_MATH is op_kind-opaque ([pe_math.py:90-99](src/kernbench/components/builtin/pe_math.py#L90-L99)).
Latency = `ceil(num_elements / vector_width) / clock`. Adding new
op_kind labels (rmax, max_elem, exp_diff, rsum, fma, mul_bcast, copy)
requires **no PE_MATH code changes** — they pass through as string
labels in `Stage.params["op_kind"]`. Latency stays consistent for the
same `num_elements`.
(Reductions like `rmax` / `rsum` have log-depth final stages in real HW
not modelled by `ceil(num_elements / vector_width)`; this is an existing
approximation, not introduced by ADR-0065.)
---
## 6. TLContext lowering pipeline
Input (user-facing API, preserved):
```python
tl.composite(
op="gemm", b=tl.ref(v_tile(j), (TILE, d)), out=O,
prologue=[{"op": "softmax_merge", "s": Sj, "m": m, "l": l, "O": O}],
epilogue=[{"op": "add", "other": O}],
)
```
Lowering steps in `TLContext.composite()`:
1. **Validate prologue items.** For each, lookup
`RECIPE_DESCRIPTORS[item["op"]]`. Check all operands present, type
tags (R / RW) match handle states (e.g., RW must not be `tl.ref`).
2. **Allocate internal scratch.** Call
`recipe.internal_scratch_bytes_fn(G, TILE, d, bpe)`; allocate via
`scratch_scope` helper (ADR-0063). Get slot addresses for `m_loc,
m_new, corr, P, l_loc`.
3. **Derive primary_out handle.** From `recipe.primary_out`: `P` handle =
`TensorHandle(shape=Sj.shape, dtype=Sj.dtype, addr=<P slot>,
space="tcm")`.
4. **Expand `engine_seq` → flat OpSpecs.** For each `EngineOp`:
- Resolve slot names (operands or scratch) to `TensorHandle`s.
- Build `OpSpec(kind=op_kind, scope=Scope.KERNEL, operands={...},
extra={...}, out=...)`.
- `out` is the handle for `dst` slot; `operands` is `{src*: handle}`.
5. **Auto-bind head GEMM.** GEMM OpSpec built from `op="gemm"`,
`operands={"a": P_handle, "b": V_handle}`, `out=<output handle>`,
`extra={"m": G, "k": TILE, "n": d}`.
6. **Build epilogue OpSpecs.** Each `epilogue=[{...}]` entry maps to
`OpSpec(kind=..., scope=Scope.OUTPUT_TILE, operands={...}, ...)`.
The `add` epilogue specifically gets `operands={"other": O_handle}`
so the Stage later carries `other_handle` (GAP 9 fix).
7. **Compute `rw_handles`.** Collect all handles tagged RW in prologue
recipes + the head GEMM's `out` (if accumulating) + any RW epilogue
targets. For softmax_merge: `rw_handles = (m, l, O)`.
8. **Emit `CompositeCmd`.** `ops = (8 MATH + 1 GEMM + 1 MATH(add))`,
`rw_handles = (m, l, O)`, `completion = new`.
**Legacy lowering.** Pre-existing
`tl.composite(op="gemm", a=A, b=B, epilogue=[{"op":"bias","bias":h}])`
goes through the same emit path: no prologue, head OpSpec from `op="gemm"`,
epilogue OpSpecs built from `epilogue=[...]`. Output `CompositeCmd` has
flat ops list; semantics preserved.
---
## 7. PE_SCHEDULER tile plan generation (flat-ops)
```python
def _generate_plan(cmd: CompositeCmd) -> PipelinePlan:
pp = self._pe_prefix
bpe = 2
# 1. Identify GEMM op (≤1).
gemm_idx = next(
(i for i, o in enumerate(cmd.ops) if o.kind == "gemm"),
None,
)
# 2. MATH-only composite (no GEMM): single tile of all ops in order.
if gemm_idx is None:
return _math_only_plan(cmd.ops, pp)
gemm = cmd.ops[gemm_idx]
M, K, N = gemm.extra["m"], gemm.extra["k"], gemm.extra["n"]
# 3. Partition ops by position.
pre_loop_ops = cmd.ops[:gemm_idx] # KERNEL scope
after_ops = cmd.ops[gemm_idx+1:] # mixed scopes
# 4. Pre-loop MATH Stages (single-shot, before tile loop).
pre_stages = [_math_stage_from_op(op, pp) for op in pre_loop_ops]
# 5. Tile loop (existing generate_gemm_plan pattern, but:
# - DMA_READ inserted only when operand.space == "hbm"
# - K_TILE epilogue from after_ops with scope == K_TILE
# - OUTPUT_TILE epilogue from after_ops with scope == OUTPUT_TILE
# - DMA_WRITE inserted only when out.space == "hbm")
tile_plans = _generate_tile_loop(
gemm, M, K, N, after_ops, pp, bpe,
)
# 6. Post-loop KERNEL Stages (after tile loop completes).
post_loop_ops = [op for op in after_ops if op.scope == Scope.KERNEL]
post_stages = [_math_stage_from_op(op, pp) for op in post_loop_ops]
return PipelinePlan(
prologue_stages=tuple(pre_stages),
tiles=tile_plans,
epilogue_stages=tuple(post_stages),
m_tiles=..., k_tiles=..., n_tiles=...,
)
```
`PipelinePlan` gets two new fields:
```python
@dataclass(frozen=True)
class PipelinePlan:
tiles: list[TilePlan]
m_tiles: int
k_tiles: int
n_tiles: int
prologue_stages: tuple[Stage, ...] = ()
epilogue_stages: tuple[Stage, ...] = ()
```
Feeder change (`_feed_loop`): yield `prologue_stages` Stages first, then
tile loop as today, then `epilogue_stages`. For the single-Stage case
(no prologue/epilogue), behaviour identical to today.
---
## 8. Strict-FIFO RW hazard tracker
```python
class _RwHazardTracker:
def __init__(self):
self._inflight: list[tuple[CompletionHandle, set[str]]] = []
# (completion, set of handle ids written or RW)
def admit(self, env, cmd: CompositeCmd) -> Generator:
"""Block until no in-flight composite has overlapping RW."""
rw_ids = {h.id for h in cmd.rw_handles}
# Strict FIFO: wait for ALL prior composites whose rw_set
# intersects, in order.
while True:
blocking = [c for c, s in self._inflight if s & rw_ids]
if not blocking:
break
# Wait on the *first* blocking composite's completion.
yield blocking[0].done_event
self._inflight.append((cmd.completion, rw_ids))
def retire(self, completion: CompletionHandle) -> None:
self._inflight = [
(c, s) for c, s in self._inflight if c != completion
]
```
`PE_SCHEDULER` invokes `admit()` before queueing a CompositeCmd's tile
plan and `retire()` upon `done_event.succeed()`. Strict FIFO: even when
overlap is partial, the new composite waits for all prior overlapping
composites to *fully* drain — simple, easy to reason about, easy to
test. (RW-aware reorder deferred per ADR-0065 A4.)
---
## 9. Phased implementation plan (test-first per CLAUDE.md)
Each phase is an independent Phase-1 → Phase-2 cycle per CLAUDE.md Part 1.
| Phase | Deliverable | Gate |
|---|---|---|
| **P0 (= ADR-0064 Rev2)** | `logical_bytes` property + dispatch formula + topology override; one-time goldens regeneration | typical composite ≈ 45 ns; dispatch ratio test infrastructure in place |
| **P1** | `CompositeCmd` flat-ops dataclass refactor + `OpSpec.operands: dict` + legacy `tl.composite` lowering preserved | every existing bench's op_log byte-equal pre/post |
| **P2** | `tl_recipes.py` + `RECIPE_DESCRIPTORS["softmax_merge"]` + TLContext lowering for `prologue=[...]` | recipe lowering test (10 ops, rw_handles, addresses) passes |
| **P3** | PE_SCHEDULER `_generate_plan` flat-ops + `PipelinePlan.prologue_stages` + DMA auto-insertion + `_feed_loop` extension | all existing bench Stage sequences unchanged; new MATH chain visible in op_log for opt2 mock |
| **P4** | Strict-FIFO RW hazard tracker + `_RwHazardTracker` integration | two-composite RW conflict test serializes correctly |
| **P5** | `_gqa_decode_long.py` opt2 variant; data-mode numeric parity check; K-before-V invariant check | opt2 matches opt3 within fp tolerance; no V DMA during prologue |
| **P6** | Dispatch-ratio measurement: opt3 vs opt2 per-tile PE_CPU cycles | ratio ≈ 2.4× ± 10% at default calibration |
P0 must land first (separate ADR). P1 is pure refactor (safest). P2P4
build the new path without touching opt3. P5 enables opt2. P6 closes
the measurement loop.
---
## 10. Verification plan (concrete)
Mirrors ADR-0065 §Test Requirements; grounded in SPEC R2/R5, ADR-0023/
0025, ADR-0046, ADR-0054.
**Refactor safety (P1):**
- Parametrize over `tests/<existing bench>.py` baseline op_logs.
- Pre/post refactor diff must be empty for *every* bench.
- Failure → revert P1 atomically; do not land partial.
**Recipe lowering (P2):**
- Given `Sj=(8,64), m=(8,), l=(8,), O=(8,128)`, lower
`tl.composite(prologue=[{"op":"softmax_merge", "s":Sj,...}], op="gemm",
b=V_ref, out=O, epilogue=[{"op":"add", "other":O}])`.
- Assert `len(cmd.ops) == 10`, `cmd.ops[0..7].kind == [rmax, max_elem,
exp_diff, exp_diff, rsum, fma, mul_bcast, copy]`,
`cmd.ops[8].kind == "gemm"`, `cmd.ops[9].kind == "add"`.
- Assert `cmd.rw_handles == (m, l, O)`.
- Assert all OpSpec addresses are concrete (no None).
**Scheduler plan (P3):**
- For the above CompositeCmd, assert PipelinePlan has 8 prologue_stages
(MATH), tile loop with FETCH/GEMM/MATH(add), no epilogue_stages.
- Assert no DMA_READ for prologue MATH stages (operands TCM-resident).
- Assert one DMA_READ for V (`b.space == "hbm"`).
- Assert no DMA_WRITE (`out.space == "tcm"`).
**RW hazard (P4):**
- Submit composite A (rw_handles=(O,)) then composite B (operands
reading O). Assert B's Stages start only after A's done_event.
**Numeric parity (P5):**
- In data mode, run opt2 and opt3 for `(G=8, T_q=1, S_kv=64, C=1, P=1)`.
- Assert `O_opt2 ≈ O_opt3` within `fp16` tolerance.
**K-before-V invariant (P5):**
- op_log inspection: for each tile's #2 CompositeCmd, no
`memory/dma_read` events between #2 start and the GEMM compute begin
that are not the V tile DMA itself.
**Dispatch ratio (P6):**
- For `S_kv=64, n_tiles=16`, measure total PE_CPU dispatch cycles for
opt3 vs opt2 paths.
- Assert ratio in `[2.0, 2.8]` (centre 2.4×).
**Determinism (all phases):**
- Identical inputs → identical op_log + latency (SPEC §0.1).
---
## 11. Performance model (expected)
Per-tile PE_CPU dispatch cycles, ADR-0064 Rev2 default calibration:
```
opt3: 10 cmds × 32 + total_bytes(≈232) × 0.25 = 320 + 58 ≈ 378 ns
opt2: 2 cmds × 32 + total_bytes(≈380) × 0.25 = 64 + 95 ≈ 159 ns
ratio = 378 / 159 ≈ 2.4×
```
Per-tile engine timing dominates total latency; dispatch is one
component. The ratio above is the *dispatch-only* relative cost. Total
latency improvement depends on whether dispatch was on the critical
path — measured per workload in P6.
---
## 12. Open items (status)
Most ADR-0065 §Open review items map to specific implementation choices
already documented in §§58. Live remaining items:
| Item | Resolution venue |
|---|---|
| Non-identity `PrimaryOutSpec.transform` (e.g., `"trans"`) | Future recipe addition; current code uses string dispatch in step 3 of §6 |
| `_gqa_decode_long.py` opt2 — TILE size sweep | Open (existing §B-3, §9 of ADR-0060) |
| Larger composite descriptors (10+ OpSpecs) and `logical_bytes` growth | Monitored in P6; if `R` term dominates, calibration revisit |
| `scratch_scope` budget vs softmax_merge intermediates (G·TILE for P) | ADR-0063 sizing check at S_kv = 256K |
---
## 13. Risks
| Risk | Likelihood | Mitigation |
|---|---|---|
| `CompositeCmd` refactor (P1) changes op_log for some bench | med | Parametrized regression over all benches; revert atomically |
| `OpSpec.operands` migration (positional → dict) breaks epilogue site | med | Per-EPILOGUE_OPS migration table; existing epilogue tests cover |
| `_RwHazardTracker` false-positive serializes too aggressively | low | Strict FIFO is the safe direction; RW-aware reorder is a future ADR |
| ADR-0064 Rev2 and ADR-0065 P1 land in wrong order → stale goldens | med | PR ordering enforced; CI gate requires both to be green |
| `softmax_merge` scratch budget exceeds `scratch_scope` at long context | med | ADR-0063 sizing check; if needed, tile_alignment promotion (future) |
| PE_MATH op_kind label collision with new strings | low | Labels are opaque strings — no enum conflict; just add to allowed set if validated |
---
## 14. Glossary & references
- **flat ops list** — `CompositeCmd.ops: tuple[OpSpec, ...]` with no
head/epilogue field; placement determined by op position + scope.
- **prologue (user API)** — `tl.composite(prologue=[...])` kwarg for
recipe ops that lower into pre-GEMM KERNEL-scope OpSpecs. *HW command
does not carry this concept.*
- **recipe** — a named macro op (e.g., `softmax_merge`) that TLContext
expands to multiple flat OpSpecs at emit time. Registry =
`RECIPE_DESCRIPTORS`.
- **primary_out** — recipe's implicit output (e.g., `P` for
softmax_merge). Auto-bound to head GEMM's first matrix operand.
- **handle-identity strict FIFO** — cross-composite serialization rule:
a composite waits for all prior composites whose `rw_handles`
intersect with its own to complete.
- **logical_bytes** — HW-logical byte size of a PE command, derived
from a structural rule (ADR-0064 D2), not from Python's
`sys.getsizeof`.
Source anchors: `tl_context.py` (tl API), `pe_scheduler.py:104-143`
(composite tile plan), `pe_dma.py:45,89` (read channel), `pe_math.py`
(op_kind opaque latency), `pe_cpu.py` (dispatch hook),
`pe_commands.py` (CompositeCmd, OpSpec, EPILOGUE_OPS).
ADRs: **0065** (this design), **0064 Revision 2** (dispatch cost),
**0060** (GQA fused attention, §5.6 / §8 item 4 carve-out), **0063**
(scratch_scope), **0046** (tl_context contract), **0042** (tile plan
generators), **0014** (PE pipeline execution model).