d282144339
Land the new GQA fused-attention kernels (ADR-0060) for prefill/decode
across long and short context, the TL discipline primitives they depend
on (ADR-0062 lazy load, ADR-0063 scratch_scope + copy_to), and the
per-op-type CPU issue cost model (ADR-0064). Remove the pre-ADR-0060
mesh-attention baseline now that the unified kernels supersede it.
ADR-0060 (long context)
- _gqa_decode.py: M-fold + 2-level chain reduce-to-root (Level-2
intra-CUBE row-then-col + Level-1 inter-CUBE) — root-only output.
- _gqa_prefill.py: head-parallel + Ring KV rotation around C CUBEs,
online-softmax merge per ring step, per-CUBE distributed output.
- Each merge stage wraps in scratch_scope() and persists running
(m, l, O) via copy_to() to lift the 1 MiB scratch ceiling.
ADR-0060 §B.split.2 (short context, kv_per_cube in {1,2,4,8})
- _gqa_decode_short.py / _gqa_prefill_short.py: no cube-SP; each CUBE
owns whole KV heads; PE-parallel heads with intra-group chain
reduce. Prefill has no Ring KV (each head fully resident).
ADR-0062 (lazy tl.load): future-bearing TensorHandle, auto-wait at
first consuming op (dot/MATH/store/send/copy_to/composite).
ADR-0063 (tl.scratch_scope + tl.copy_to): scoped per-tile arena with
copy_to writeback primitive for persistent running state.
ADR-0064 (CPU issue cost model)
- common/cpu_issue_cost.py: per-op-type table (composite=40 ns,
primitives=5 ns); ratios are load-bearing per D1.
- TLContext: issue_cost_table param; _emit_dispatch_overhead(kind)
consults table with dispatch_cycles fallback (ADR-0046 §D6
back-compat).
- Live PE_CPU paths (greenlet + legacy) construct TLContext with
DEFAULT_CPU_ISSUE_COST so saturation lever (ADR-0060 §1) is
measurable end-to-end.
P7 headline bench: milestone-gqa-headline writes per-panel
op_log_summary to 1H_milestone_output/gqa_headline/sweep.json. No
figure renderers yet (deferred).
Removals (pre-ADR-0060 baseline now superseded):
- benches: _attention_mesh_kv.py, _attention_mesh_mlo.py,
_attention_mesh_mlo_2d.py, milestone_gqa_llama70b.py
- tests: test_attention_*, test_mesh_*, test_milestone_gqa_llama70b
- topology: llama70b_4sip.yaml (only consumer was the deleted diag)
- artifacts: 1H_milestone_output/gqa/ (sweep.json + 5 PNGs)
- tests/gqa/ plot helper + test (broken on Windows Tcl/Tkinter)
- ADR-0060/0061 references to deleted file paths cleaned up
(EN + KO kept in sync).
Tests: 124/124 focused regression green (attention + Phase E + TL
discipline + triton_emu + pe_components). Full regression: 764 pass,
2 pre-existing test_bench_registry failures (stale EXPECTED_NAMES
across multiple benches, not introduced here).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
"""PE_SCHEDULER: plan generation + tile dispatch (ADR-0014 D6).
|
|
|
|
Receives PeInternalTxn from PE_CPU, routes to engines:
|
|
- Simple commands (DmaReadCmd, GemmCmd, etc.) → direct dispatch to engine
|
|
- CompositeCmd → generate TilePlan, feed tiles via _feed_loop
|
|
|
|
Composite pipeline uses token self-routing (ADR-0014 D6):
|
|
Scheduler only does initial dispatch + completion tracking.
|
|
Tiles chain through components based on their plan's stage sequence.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Generator
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
import simpy
|
|
|
|
from kernbench.components.base import ComponentBase
|
|
|
|
if TYPE_CHECKING:
|
|
from kernbench.common.pe_commands import PeInternalTxn
|
|
from kernbench.components.context import ComponentContext
|
|
from kernbench.topology.types import Node
|
|
|
|
|
|
class PeSchedulerComponent(ComponentBase):
|
|
"""PE_SCHEDULER: sole dispatcher inside a PE (ADR-0014 D1, D6).
|
|
|
|
Simple commands are forwarded to the appropriate engine.
|
|
CompositeCmd creates a TilePlan and feeds tiles into the pipeline.
|
|
|
|
Single _feed_loop process per scheduler ensures FIFO command ordering.
|
|
"""
|
|
|
|
TILE_M = 32
|
|
TILE_K = 64
|
|
TILE_N = 32
|
|
|
|
_CMD_DISPATCH: dict[type, str] = {}
|
|
|
|
@classmethod
|
|
def _ensure_dispatch_table(cls) -> None:
|
|
if cls._CMD_DISPATCH:
|
|
return
|
|
from kernbench.common.pe_commands import (
|
|
CopyCmd, DmaReadCmd, DmaWriteCmd, GemmCmd, MathCmd,
|
|
)
|
|
cls._CMD_DISPATCH = {
|
|
DmaReadCmd: "pe_dma",
|
|
DmaWriteCmd: "pe_dma",
|
|
GemmCmd: "pe_gemm",
|
|
MathCmd: "pe_math",
|
|
# ADR-0063 §D3.1: tl.copy_to → vector engine.
|
|
CopyCmd: "pe_math",
|
|
}
|
|
|
|
def __init__(self, node: Node, ctx: ComponentContext | None = None) -> None:
|
|
super().__init__(node, ctx)
|
|
self._pe_prefix = node.id.rsplit(".", 1)[0]
|
|
self._ensure_dispatch_table()
|
|
self._pending_feeds: simpy.Store | None = None
|
|
self._pipeline_counter = 0
|
|
|
|
def start(self, env: simpy.Environment) -> None:
|
|
self._pending_feeds = simpy.Store(env)
|
|
super().start(env)
|
|
env.process(self._feed_loop(env))
|
|
|
|
def run(self, env: simpy.Environment, nbytes: int) -> Generator:
|
|
overhead_ns = float(self.node.attrs.get("overhead_ns", 0.0))
|
|
yield env.timeout(overhead_ns)
|
|
|
|
def _worker(self, env: simpy.Environment) -> Generator:
|
|
from kernbench.common.pe_commands import PeInternalTxn
|
|
|
|
while True:
|
|
msg: Any = yield self._inbox.get()
|
|
if isinstance(msg, PeInternalTxn):
|
|
env.process(self._dispatch(env, msg))
|
|
else:
|
|
yield from self._forward_txn(env, msg)
|
|
|
|
def _dispatch(self, env: simpy.Environment, pe_txn: PeInternalTxn) -> Generator:
|
|
from kernbench.common.pe_commands import CompositeCmd, PeCpuOverheadCmd
|
|
|
|
yield from self.run(env, 0) # scheduler overhead
|
|
|
|
cmd = pe_txn.command
|
|
|
|
# Simple command dispatch
|
|
engine_suffix = self._CMD_DISPATCH.get(type(cmd))
|
|
if engine_suffix is not None:
|
|
yield self.out_ports[f"{self._pe_prefix}.{engine_suffix}"].put(pe_txn)
|
|
return
|
|
|
|
# CompositeCmd: generate plan and feed
|
|
if isinstance(cmd, CompositeCmd):
|
|
yield from self._dispatch_composite(env, pe_txn, cmd)
|
|
return
|
|
|
|
if isinstance(cmd, PeCpuOverheadCmd):
|
|
yield env.timeout(cmd.cycles)
|
|
pe_txn.done.succeed()
|
|
return
|
|
|
|
pe_txn.done.succeed()
|
|
|
|
def _dispatch_composite(
|
|
self, env: simpy.Environment, pe_txn: Any, cmd: Any,
|
|
) -> Generator:
|
|
"""Generate plan and enqueue to feeder. Non-blocking (ADR-0014 D6)."""
|
|
from kernbench.components.builtin.pe_types import PipelineContext
|
|
|
|
plan = self._generate_plan(cmd)
|
|
|
|
self._pipeline_counter += 1
|
|
ctx = PipelineContext(
|
|
id=f"p{self._pipeline_counter}",
|
|
total_tiles=len(plan.tiles),
|
|
done_event=pe_txn.done,
|
|
)
|
|
|
|
# Enqueue to feeder — scheduler worker returns immediately
|
|
assert self._pending_feeds is not None
|
|
yield self._pending_feeds.put((plan, ctx))
|
|
|
|
def _feed_loop(self, env: simpy.Environment) -> Generator:
|
|
"""Single feeder process: FIFO command ordering (ADR-0014 D6).
|
|
|
|
No tile feed interleaving between commands.
|
|
Queue full → only this process blocks.
|
|
"""
|
|
from kernbench.components.builtin.pe_types import TileToken
|
|
|
|
assert self._pending_feeds is not None
|
|
while True:
|
|
plan, ctx = yield self._pending_feeds.get()
|
|
for tile in plan.tiles:
|
|
first_stage = tile.stages[0]
|
|
token = TileToken(
|
|
tile_id=tile.tile_id,
|
|
pipeline_ctx=ctx,
|
|
plan=tile,
|
|
stage_idx=0,
|
|
params=first_stage.params,
|
|
)
|
|
yield self.out_ports[first_stage.component].put(token)
|
|
|
|
def _generate_plan(self, cmd: Any) -> Any:
|
|
"""Generate a PipelinePlan from CompositeCmd."""
|
|
from kernbench.components.builtin.tiling import (
|
|
generate_gemm_plan,
|
|
generate_math_plan,
|
|
)
|
|
|
|
pp = self._pe_prefix
|
|
bpe = 2 # default bytes per element (f16)
|
|
|
|
if cmd.op == "gemm" and cmd.b is not None:
|
|
a = cmd.a
|
|
b = cmd.b
|
|
M, K = a.shape[-2], a.shape[-1]
|
|
N = b.shape[-1]
|
|
# When CompositeCmd.ops is populated, ops[0] is the head and
|
|
# ops[1:] is the epilogue spec list. Empty ops → legacy path.
|
|
epi_specs = tuple(cmd.ops[1:]) if cmd.ops else ()
|
|
return generate_gemm_plan(
|
|
M=M, K=K, N=N,
|
|
tile_m=self.TILE_M, tile_k=self.TILE_K, tile_n=self.TILE_N,
|
|
bytes_per_element=bpe,
|
|
A_addr=a.addr, B_addr=b.addr, C_addr=cmd.out_addr,
|
|
pe_prefix=pp,
|
|
a_pinned=getattr(a, "pinned", False),
|
|
b_pinned=getattr(b, "pinned", False),
|
|
epilogue_specs=epi_specs,
|
|
)
|
|
else:
|
|
# Math composite
|
|
a = cmd.a
|
|
M = a.shape[-2] if len(a.shape) >= 2 else a.shape[0]
|
|
N = a.shape[-1] if len(a.shape) >= 2 else 1
|
|
return generate_math_plan(
|
|
M=M, N=N,
|
|
tile_m=self.TILE_M, tile_n=self.TILE_N,
|
|
bytes_per_element=bpe,
|
|
math_op=cmd.math_op or "identity",
|
|
src_addr=a.addr, dst_addr=cmd.out_addr,
|
|
pe_prefix=pp,
|
|
)
|