gqa(adr-0064/0065): flat-ops CompositeCmd (P1) + structural dispatch cost (ADR-0064 Rev2); promote ADR-0064
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>
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
"""Per-op-type CPU issue cost table (ADR-0064 D1).
|
||||
|
||||
Replaces the single uniform ``dispatch_cycles`` scalar with a cost table
|
||||
keyed by command kind. Charged on PE_CPU at issue time (before the command
|
||||
is dispatched to PE_SCHEDULER) so the hybrid's CPU-saturation win
|
||||
(ADR-0060 §1) becomes measurable.
|
||||
|
||||
The table is consulted by ``TLContext._emit_dispatch_overhead(kind)``;
|
||||
live PE_CPU paths (greenlet via ``kernel_runner.py``, legacy replay via
|
||||
``pe_cpu.py:_execute_legacy``) construct TLContext with
|
||||
``issue_cost_table=DEFAULT_CPU_ISSUE_COST`` so all benches see the cost.
|
||||
|
||||
Absolute ns values are provisional (ADR-0064 review item #1). The
|
||||
defensible claim is the **ratio** — composite ≫ primitive.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal
|
||||
|
||||
OpKind = Literal[
|
||||
"composite",
|
||||
"load",
|
||||
"store",
|
||||
"dot",
|
||||
"math",
|
||||
"ipcq_send",
|
||||
"ipcq_recv",
|
||||
"copy_to",
|
||||
]
|
||||
|
||||
|
||||
DEFAULT_CPU_ISSUE_COST: dict[str, int] = {
|
||||
"composite": 40,
|
||||
"load": 5,
|
||||
"store": 5,
|
||||
"dot": 5,
|
||||
"math": 5,
|
||||
"ipcq_send": 5,
|
||||
"ipcq_recv": 5,
|
||||
"copy_to": 5,
|
||||
}
|
||||
|
||||
|
||||
def get_issue_cost(kind: str, table: dict[str, int] | None = None) -> int:
|
||||
"""Return per-op-type CPU issue cost in ns.
|
||||
|
||||
Unknown kinds return 0 (no charge) so adding a new ``tl.*`` op kind
|
||||
doesn't accidentally over-charge before the table is updated.
|
||||
"""
|
||||
if table is None:
|
||||
table = DEFAULT_CPU_ISSUE_COST
|
||||
return table.get(kind, 0)
|
||||
@@ -123,6 +123,12 @@ class IpcqSendCmd:
|
||||
data: Any = None
|
||||
data_op: bool = True # ADR-0020 op_log recording flag
|
||||
|
||||
@property
|
||||
def logical_bytes(self) -> int:
|
||||
# framing + direction enum + src_addr + src_space enum + nbytes
|
||||
# + shape (len marker + 4·rank) + dtype tag (ADR-0064 D2).
|
||||
return 4 + 1 + 4 + 1 + 4 + (1 + 4 * len(self.shape)) + 1
|
||||
|
||||
|
||||
# ── D12: IpcqRecvCmd (PE_CPU → PE_IPCQ) ──────────────────────────────
|
||||
|
||||
@@ -155,6 +161,12 @@ class IpcqRecvCmd:
|
||||
data_op: bool = True
|
||||
consume: bool = True # DIAGNOSTIC: see docstring
|
||||
|
||||
@property
|
||||
def logical_bytes(self) -> int:
|
||||
# framing + direction enum + shape (len marker + 4·rank) + dtype tag
|
||||
# + dst_addr + dst_space enum (ADR-0064 D2).
|
||||
return 4 + 1 + (1 + 4 * len(self.shape)) + 1 + 4 + 1
|
||||
|
||||
|
||||
# ── D12: IpcqDmaToken (PE_IPCQ → PE_DMA, vc_comm) ───────────────────
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import simpy
|
||||
@@ -22,6 +22,24 @@ class Scope(Enum):
|
||||
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).
|
||||
@@ -33,9 +51,22 @@ class OpSpec:
|
||||
|
||||
kind: str # "gemm" | "bias" | "relu" | ...
|
||||
scope: "Scope" = Scope.OUTPUT_TILE
|
||||
operands: tuple[Any, ...] = () # tuple[TensorHandle, ...]
|
||||
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).
|
||||
@@ -103,6 +134,10 @@ class DmaReadCmd:
|
||||
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:
|
||||
@@ -113,6 +148,10 @@ class DmaWriteCmd:
|
||||
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:
|
||||
@@ -129,6 +168,10 @@ class GemmCmd:
|
||||
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:
|
||||
@@ -145,6 +188,13 @@ class MathCmd:
|
||||
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:
|
||||
@@ -161,6 +211,10 @@ class CopyCmd:
|
||||
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:
|
||||
@@ -168,20 +222,26 @@ class CompositeCmd:
|
||||
|
||||
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
|
||||
op: Literal["gemm", "math"]
|
||||
a: TensorHandle
|
||||
b: TensorHandle | None
|
||||
out_addr: int
|
||||
out_nbytes: int
|
||||
math_op: str | None = None # for op="math": which math operation
|
||||
data_op: bool = True
|
||||
# Multi-op composite (ADR-0014 D3.3): when non-empty, ops[0] is the
|
||||
# head and ops[1:] are epilogue stages with explicit scope. When empty,
|
||||
# the legacy single-op semantics (op/a/b/math_op) apply.
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Structural PE_CPU dispatch cost model (ADR-0064 Revision 2).
|
||||
|
||||
Replaces the Rev1 per-op-type calibration table (``cpu_issue_cost.py``,
|
||||
removed) with a structural formula derived from each command's
|
||||
``logical_bytes`` (ADR-0064 D2):
|
||||
|
||||
dispatch_cycles(cmd) = FIXED_PER_CMD + cmd.logical_bytes * R
|
||||
|
||||
The per-command ``FIXED_PER_CMD`` term models the queue-tail update /
|
||||
MMIO-class RTT / completion-event registration; the byte term ``R`` models
|
||||
queue-write bandwidth. The primary signal the model exposes is
|
||||
**command-count reduction** (FIXED-dominated); the byte term is a secondary
|
||||
refinement (ADR-0064 Context).
|
||||
|
||||
Knobs are cycle-domain only; cycle→ns uses the PE node's ``clock_freq_ghz``
|
||||
(ADR-0064 D3). Defaults anchor a typical 1-OpSpec GEMM composite (≈54 bytes)
|
||||
at ≈43 ns on a 16 B/cycle on-die descriptor queue.
|
||||
|
||||
A composite whose ``logical_bytes`` exceeds ``max_composite_logical_bytes``
|
||||
is rejected at emit time with a ``ValueError`` (ADR-0064 D7, revised: hard
|
||||
cap, no auto-segmentation).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PeCostModel:
|
||||
"""Cycle-domain dispatch cost knobs (ADR-0064 D3/D4)."""
|
||||
|
||||
fixed_per_cmd_cycles: float = 40.0
|
||||
byte_cycles_recip: float = 0.0625 # = 16 bytes/cycle
|
||||
max_composite_logical_bytes: int = 1024 # D7 hard cap
|
||||
|
||||
def dispatch_cycles(self, logical_bytes: int) -> float:
|
||||
"""FIXED + logical_bytes × R (ADR-0064 D1)."""
|
||||
return self.fixed_per_cmd_cycles + logical_bytes * self.byte_cycles_recip
|
||||
|
||||
|
||||
DEFAULT_PE_COST_MODEL = PeCostModel()
|
||||
|
||||
|
||||
def from_node_attrs(attrs: dict) -> PeCostModel:
|
||||
"""Build a PeCostModel from a PE node's ``pe_cost_model:`` attrs block
|
||||
(ADR-0064 D4). Missing keys fall back to defaults."""
|
||||
block = attrs.get("pe_cost_model", {}) or {}
|
||||
d = DEFAULT_PE_COST_MODEL
|
||||
return PeCostModel(
|
||||
fixed_per_cmd_cycles=float(
|
||||
block.get("fixed_per_cmd_cycles", d.fixed_per_cmd_cycles)),
|
||||
byte_cycles_recip=float(
|
||||
block.get("byte_cycles_recip", d.byte_cycles_recip)),
|
||||
max_composite_logical_bytes=int(
|
||||
block.get("max_composite_logical_bytes",
|
||||
d.max_composite_logical_bytes)),
|
||||
)
|
||||
Reference in New Issue
Block a user