"""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)