47e2c78c66
ADR-0065 P1: CompositeCmd -> flat ordered ops list (drop legacy op/a/b/out_addr fields); OpSpec.operands dict + out handle. Meaning-preserving (op_log byte-equal); pe_scheduler + op_log read the head op. ADR-0064 Rev2: replace Rev1 per-op cost table with structural FIXED + logical_bytes*R formula. logical_bytes on every PeCommand; new common/pe_cost_model.py; cost centralized in TLContext._emit (load/recv_async charge explicitly); pe_cpu/kernel_runner wire the per-PE model + clock. D7: cap exceeded -> ValueError (no auto-segmentation). Remove Rev1 cpu_issue_cost.py + its tests. No goldens churn. Promote ADR-0064 Rev2 Proposed->Accepted (docs/adr/ + docs/adr-ko/); amend D7 (error not segmentation) + record P1-before-P0 ordering in ADR-0064/0065 Migration notes (EN+KO). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
282 lines
8.9 KiB
Python
282 lines
8.9 KiB
Python
"""PE-internal command types and handles (ADR-0014).
|
|
|
|
Generated by triton_emu (TLContext) and consumed by PE component
|
|
implementations (PE_CPU, PE_SCHEDULER, PE_DMA, PE_GEMM, PE_MATH).
|
|
|
|
Command lifecycle:
|
|
Triton kernel → TLContext → [PeCommand list] → PE_CPU → PE_SCHEDULER → engines
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
import simpy
|
|
|
|
|
|
class Scope(Enum):
|
|
K_TILE = "k_tile"
|
|
OUTPUT_TILE = "output_tile"
|
|
KERNEL = "kernel"
|
|
|
|
|
|
def _extra_bytes(v: Any) -> int:
|
|
"""Type-aware HW-logical byte count for an OpSpec.extra value (ADR-0064 D2).
|
|
|
|
bool→1, int/float→4 (scalar), tuple/list→1 + 4·len (length marker + 4
|
|
per element, e.g. shape/axes), str→1 (opcode-like tag). Default→4.
|
|
``bool`` is checked first because it subclasses ``int``.
|
|
"""
|
|
if isinstance(v, bool):
|
|
return 1
|
|
if isinstance(v, (int, float)):
|
|
return 4
|
|
if isinstance(v, (tuple, list)):
|
|
return 1 + 4 * len(v)
|
|
if isinstance(v, str):
|
|
return 1
|
|
return 4
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class OpSpec:
|
|
"""One operation in a multi-op composite (head + epilogue, ADR-0014 D3.3).
|
|
|
|
The head op (first in CompositeCmd.ops) defines tile geometry; subsequent
|
|
ops are epilogue stages whose ``scope`` controls how often they fire
|
|
(per-K-tile, per-output-tile, or once per kernel).
|
|
"""
|
|
|
|
kind: str # "gemm" | "bias" | "relu" | ...
|
|
scope: "Scope" = Scope.OUTPUT_TILE
|
|
operands: dict[str, Any] = field(default_factory=dict) # name → TensorHandle
|
|
scalar: float | None = None
|
|
extra: dict[str, Any] = field(default_factory=dict)
|
|
out: "TensorHandle | None" = None # explicit write-back handle
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
"""HW-logical byte size (ADR-0064 D2). ``scalar`` is a transitional
|
|
field not part of the flat-ops model (ADR-0065 D2) — excluded,
|
|
pending its removal."""
|
|
return (
|
|
1 + 1 # opcode + scope enum
|
|
+ 1 + 8 * len(self.operands) # len marker + handles
|
|
+ (8 if self.out is not None else 0) # out handle
|
|
+ 1 + sum(_extra_bytes(v) for v in self.extra.values())
|
|
)
|
|
|
|
|
|
# Epilogue op contracts: kind → (required field names, default scope).
|
|
# Used by tl.composite to validate user-provided epilogue dicts at
|
|
# command-emit time so typos fail before reaching the scheduler.
|
|
EPILOGUE_OPS: dict[str, tuple[tuple[str, ...], "Scope"]] = {
|
|
"bias": (("bias",), Scope.OUTPUT_TILE),
|
|
"relu": ((), Scope.OUTPUT_TILE),
|
|
"gelu": ((), Scope.OUTPUT_TILE),
|
|
"sigmoid": ((), Scope.OUTPUT_TILE),
|
|
"scale": (("factor",), Scope.OUTPUT_TILE),
|
|
"clamp": (("lo", "hi"), Scope.OUTPUT_TILE),
|
|
"dequant": (("scale",), Scope.K_TILE),
|
|
"add": (("other",), Scope.OUTPUT_TILE),
|
|
}
|
|
|
|
|
|
# ── Handles ───────────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TensorHandle:
|
|
"""Opaque reference to a tensor residing in PE_TCM.
|
|
|
|
Returned by tl.load, tl.dot, tl.exp, etc.
|
|
Carries metadata for command generation; data field is reserved
|
|
for future validate mode (numpy array).
|
|
"""
|
|
|
|
id: str
|
|
addr: int # address (VA when MMU enabled, PA otherwise)
|
|
shape: tuple[int, ...]
|
|
dtype: str
|
|
nbytes: int # total byte size
|
|
data: object = None # reserved for validate mode
|
|
space: str = "tcm" # MemoryStore space ("tcm" | "hbm" | "sram")
|
|
pinned: bool = False # operand already DMA-staged in TCM (via tl.load)
|
|
# ADR-0062 §D2: lazy tl.load attaches a LoadFuture here. None for
|
|
# handles that have no in-flight DMA (constants, math outputs, etc.).
|
|
# Consumer ops call _await_pending() to yield on the future before
|
|
# emitting their own command. Excluded from eq/hash/repr so handle
|
|
# identity is unaffected by pending state.
|
|
pending: object = field(default=None, compare=False, hash=False, repr=False)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CompletionHandle:
|
|
"""Opaque handle for a non-blocking composite command.
|
|
|
|
Returned by tl.composite, consumed by tl.wait.
|
|
"""
|
|
|
|
id: str
|
|
|
|
|
|
# ── PE Commands ───────────────────────────────────────────────────
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DmaReadCmd:
|
|
"""DMA READ: HBM → PE_TCM. src_addr is VA (translated to PA by PE_DMA)."""
|
|
|
|
handle: TensorHandle
|
|
src_addr: int
|
|
nbytes: int
|
|
data_op: bool = True
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
return 4 + 8 + 4 + 4 # framing + handle + src_addr + nbytes
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class DmaWriteCmd:
|
|
"""DMA WRITE: PE_TCM → HBM. dst_addr is VA (translated to PA by PE_DMA)."""
|
|
|
|
handle: TensorHandle
|
|
dst_addr: int
|
|
nbytes: int
|
|
data_op: bool = True
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
return 4 + 8 + 4 + 4 # framing + handle + dst_addr + nbytes
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class GemmCmd:
|
|
"""GEMM engine command: matrix multiply on TCM data.
|
|
|
|
out = a @ b, all operands in TCM.
|
|
"""
|
|
|
|
a: TensorHandle
|
|
b: TensorHandle
|
|
out: TensorHandle
|
|
m: int
|
|
k: int
|
|
n: int
|
|
data_op: bool = True
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
return 4 + 8 * 3 + 4 * 3 # framing + 3 handles + m/k/n scalars
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MathCmd:
|
|
"""MATH engine command: unary/binary/reduction on TCM data.
|
|
|
|
op: "exp", "log", "sqrt", "abs", "sigmoid", "cos", "sin",
|
|
"add", "sub", "mul", "div", "where",
|
|
"sum", "max", "min"
|
|
"""
|
|
|
|
op: str
|
|
inputs: tuple[TensorHandle, ...]
|
|
out: TensorHandle
|
|
axis: int | None = None # for reductions
|
|
data_op: bool = True
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
return (
|
|
4 + 1 + 1 + 8 * len(self.inputs) + 8 # framing+opcode+len+inputs+out
|
|
+ (4 if self.axis is not None else 0)
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CopyCmd:
|
|
"""TCM-to-TCM byte copy (ADR-0063 §D3.1).
|
|
|
|
Emitted by ``tl.copy_to`` to persist a scoped result's bytes to an
|
|
outside-``scratch_scope`` (persistent) address — the two-arena
|
|
pattern for tiled flash attention. Runs on the vector engine;
|
|
op_log classifies as ``op_kind="math"``, ``op_name="copy"``.
|
|
"""
|
|
|
|
src: TensorHandle
|
|
dst: TensorHandle
|
|
nbytes: int
|
|
data_op: bool = True
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
return 4 + 8 * 2 + 4 # framing + src/dst handles + nbytes
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CompositeCmd:
|
|
"""Composite command: tiled pipeline of DMA_READ + COMPUTE + DMA_WRITE.
|
|
|
|
Non-blocking — submitted to PE_SCHEDULER which manages tile splitting
|
|
and pipeline overlaps (ADR-0014 D3.2).
|
|
|
|
Flat-ops shape (ADR-0065 D1): ``ops`` is an ordered list of OpSpecs.
|
|
The GEMM op (if any, ≤1) drives the tile loop; preceding/following
|
|
OpSpecs are placed by position + scope. ``rw_handles`` carries
|
|
cross-composite hazard metadata (ADR-0065 D6.3); unused in P1.
|
|
"""
|
|
|
|
completion: CompletionHandle
|
|
ops: tuple[OpSpec, ...] = ()
|
|
rw_handles: tuple["TensorHandle", ...] = ()
|
|
data_op: bool = True
|
|
|
|
@property
|
|
def logical_bytes(self) -> int:
|
|
"""HW-logical byte size (ADR-0064 D2). Per-op summation, no dedup."""
|
|
return (
|
|
4 # framing
|
|
+ 1 + sum(op.logical_bytes for op in self.ops)
|
|
+ 1 + 8 * len(self.rw_handles)
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class WaitCmd:
|
|
"""Wait for a specific composite or all pending composites."""
|
|
|
|
handle: CompletionHandle | None = None # None = wait all
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PeCpuOverheadCmd:
|
|
"""PE_CPU scalar execution overhead (cycles)."""
|
|
|
|
cycles: int
|
|
|
|
|
|
# Union type for all PE commands
|
|
PeCommand = (
|
|
DmaReadCmd | DmaWriteCmd | GemmCmd | MathCmd | CopyCmd
|
|
| CompositeCmd | WaitCmd | PeCpuOverheadCmd
|
|
)
|
|
|
|
|
|
@dataclass
|
|
class PeInternalTxn:
|
|
"""PE-internal message flowing PE_CPU → PE_SCHEDULER → engines.
|
|
|
|
Carries a single PeCommand and a completion event. PE_CPU creates one
|
|
PeInternalTxn per command during the replay phase and sends it to
|
|
PE_SCHEDULER, which routes it to the appropriate engine (PE_DMA,
|
|
PE_GEMM, PE_MATH). The engine signals ``done`` on completion.
|
|
"""
|
|
|
|
command: PeCommand
|
|
done: simpy.Event # succeeded when the engine completes this command
|
|
pe_prefix: str = "" # e.g. "sip0.cube0.pe0" — needed by PE_DMA for path resolution
|
|
result_data: dict[str, Any] = field(default_factory=dict)
|