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>
9.7 KiB
ADR-0064: 구조적 CPU dispatch cost 모델 (logical_bytes + FIXED + R)
Status
Proposed (Revision 2)
ADR-0060 (AHBM GQA Fused Attention) 와 ADR-0065 (flat-ops composite + 첫 stateful recipe) 의 보조 ADR. 그 hybrid 의 핵심 — "GEMM 은
tl.composite로, softmax merge 는 커널에서" — 은 PE_SCHEDULER 로 tiling 을 offload 해서 CPU 가 굵은 descriptor 만 issue 하고 앞서가며 엔진을 포화시킴 으로 이깁니다. 그 win 이 현재 시뮬레이터에선 보이지 않습니다 (per-op CPU issue cost = 0).Revision 2 는 원안의 op-type calibration 표 를 logical_bytes 기반 구조 공식 으로 대체합니다 — op-type 별 calibration 불필요, 새 op_kind 도 자동 적용.
Context
현재 코드
- 모든
tl.*op 가 cmd emit 전에_emit_dispatch_overhead()호출 (tl_context.py:196-212) →dispatch_cycles > 0일 때만PeCpuOverheadCmd(cycles=dispatch_cycles)발행. knob 은 op kind 무관 uniform 이며 두 실행 경로 모두 0 하드코딩 (pe_cpu.py:101greenlet,:195replay). - ⇒ 명령 issue (descriptor 구성 + scheduler 큐 push) 가 PE_CPU 에서 0 ns 비용.
PeCpuOverheadCmd는 PE_CPU 에서yield env.timeout(cmd.cycles)로 소비 (kernel_runner.py:131-132).
uniform-zero 가 hybrid 에 부적합한 이유
ADR-0060 §1 의 핵심: composite 1 개가 N_tiles 분량의 GEMM tiling 을
offload → CPU 는 O(1) 개의 굵은 cmd 만 issue, primitive 경로는
O(N_tiles × ops/tile). issue cost = 0 이면 모델은:
- primitive 경로의 CPU 가 push 를 못 따라가 엔진 idle 을 못 보임
- composite 의 구성 비용 ≫ primitive 1 개이지만 ≪ 대체된 primitive 들의 합 을 못 보임
Revision 1 의 op-type calibration 이 과한 이유
원안은 cost_table[kind] (kind = composite, load, dot, math, …)
형태였습니다. 비용:
- kind 마다 값 필요 (calibration ≥ |kinds|)
- 새 kind 추가 시마다 entry 추가
- 그런데 잡으려던 ratio — "composite ≫ primitive, ≪ 대체된 primitive 들의 합" — 는 op kind 가 아니라 cmd 가 들고 있는 필드 수 의 함수.
cmd 의 byte 발자국 이 자연 proxy: N OpSpec composite 는 1 OpSpec primitive 의 ~N 배 bytes.
고정 부분 (큐 tail 업데이트, completion 등록, MMIO-급 latency) 은 cmd 마다.
둘 합치면: FIXED + bytes × R.
Decision
D1. 구조적 dispatch cost 공식
PE_SCHEDULER 로 가는 모든 PE command 가 PE_CPU 에서 dispatch cycles 소모:
dispatch_cycles(cmd) = FIXED_PER_CMD + cmd.logical_bytes × R
FIXED_PER_CMD(cycles/cmd): 큐 tail 업데이트, MMIO-급 RTT, completion-event 등록 — cmd 크기와 무관 고정 비용.R(cycles/byte): scheduler 큐에 cmd 를 직렬화하는 큐-write 대역폭.cmd.logical_bytes(int): cmd 의 HW-논리 byte 수 — D2 룰로 계산, Pythonsys.getsizeof가 아님.
PE_CPU 는 기존 hook 그대로 PeCpuOverheadCmd(cycles=dispatch_cycles(cmd))
를 dispatch 전에 발행 — 사이클 값만 변경.
D2. logical_bytes 룰
각 PE command dataclass 가 logical_bytes: int (property) 노출. 룰
(HW 친화적, Python overhead 무시):
| 필드 종류 | bytes |
|---|---|
| cmd framing (cmd 종류 discriminator + completion id 참조) | 4 |
| Opcode (op kind enum) | 1 |
| Enum (scope 등) | 1 |
TensorHandle 참조 (address only — shape/dtype 은 descriptor table 가정) |
8 |
| Scalar (int/float) | 4 |
| Tuple length marker | 1 |
CompositeCmd 는 ops 와 rw_handles 재귀 합산:
@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)
)
OpSpec:
@property
def logical_bytes(self) -> int:
return (
1 + 1 # opcode + scope
+ 1 + 8 * len(self.operands) # named operand handles
+ (8 if self.out is not None else 0) # out handle
+ 1 + sum(4 for _ in self.extra.values()) # extra scalars
)
(DmaReadCmd, MathCmd 등도 같은 룰 — dataclass 당 property 1 개,
약 3 줄.)
D3. Default — 일반 composite ≈ 45 ns 기준
anchor: 일반 1-op DMA→GEMM→DMA composite 의
logical_bytes ≈ 52 (framing 4 + GEMM OpSpec 39 + rw_handles 9). dispatch
목표 45 ns. on-die producer→consumer 큐 4 bytes/cycle 가정.
clock = 1 GHz # 1 cycle = 1 ns
FIXED_PER_CMD = 32 cycles
R = 0.25 cycles/byte
검증: 32 + 52 × 0.25 = 45 cycles ≈ 45 ns ✓
D4. topology config override
default 는 pe_cpu.py 에 내장. topology yaml 의 PE 노드 attrs 아래
pe_cost_model: 절로 override:
pe:
attrs:
pe_cost_model:
fixed_per_cmd_cycles: 32
byte_cycles_recip: 0.25
clock_freq_ghz: 1.0
누락된 키는 default. dispatch 공식은 PE_CPU init 시
node.attrs["pe_cost_model"] 에서 읽음.
D5. Scope — 무엇이 cost 를 내고 무엇이 안 내나
| 경로 | dispatch cost? |
|---|---|
PE_CPU → PE_SCHEDULER 의 모든 PeCommand |
예 |
PeCpuOverheadCmd 자체 (이미 cycles 명시) |
아니오 (공식 우회) |
| PE_SCHEDULER 가 자동 생성한 Stage (DMA_READ/WRITE/FETCH/STORE) | 아니오 (PE_SCHEDULER 내부) |
엔진 compute latency (PE_DMA drain_ns, GEMM/MATH _compute_ns) |
변화 없음 — 엔진에 그대로 (SPEC §0.1) |
"latency 는 모델링된 컴포넌트에서" invariant 유지 — dispatch cost 는 추가 CPU-side 시간, 엔진 시간에 fold 안 함.
D6. 설정 가능 값; goldens 재생성
issue cost 를 0 → non-zero 로 바꾸면 모든 bench 의 latency 변화. 이 ADR land 시 골든 latency 한 번 재생성 — ADR-0062 D3 lazy-load 와 같은 패턴. 재생성 후 동일 calibration 이 ADR-0065 opt2 측정에 적용.
Alternatives
A1. Revision 1 의 op-type calibration 표 유지
기각: calibration 비용이 |kinds| 에 비례, 그리고 표가 잡으려던 ratio 는 구조적으로 cmd 크기의 함수. 구조 공식은 같은 정성적 동작을 N 개가 아닌 2 개의 calibratable 숫자로 달성.
A2. byte-only 공식 (FIXED 항 없음)
기각. FIXED = 0 이면 opt2 (ADR-0065 Option Y) 가 opt3 를 이기지 못함 — per-tile dispatch 총 bytes 가 비슷 (opt3 ≈ 232, opt2 ≈ 380); win 은 전적으로 per-cmd fixed cost 횟수 감소 에서 옴. byte-only 는 모델이 보여야 할 신호를 지움.
A3. PE_SCHEDULER 에 dispatch 비용 부과
기각: saturation 질문은 "CPU 가 descriptor 를 push 하는 속도가 엔진을 계속 바쁘게 할 수 있는가?" — PE_CPU 의 issue-bandwidth 특성. scheduler 에 부과하면 CPU back-pressure 모델링 안 됨.
A4. DMA program/setup 시간을 별도 fixed per-descriptor 비용으로 모델
연기: 처음엔 descriptor-program 비용을 issuing op 의 dispatch cost 에 fold. calibration 이 분리 필요성을 보이면 PE_DMA fixed setup 으로 분리.
Consequences
Positive
- hybrid 의 CPU-offload / saturation win (ADR-0060 §1) 이 측정 가능, 구조적으로 정직한 모델 (calibration 표 없음).
- 새 op_kind 추가 (예: ADR-0065 의
softmax_merge8-step recipe) 가 zero cost — 같은 공식 자동 적용. - 하드웨어에 더 충실 (queue-head MMIO RTT + queue-write 대역폭).
Negative
- 모든 bench goldens 변화 → 일회성 재생성 (D6); CI 골든 fixtures 업데이트.
- 두 calibration knob (FIXED, R) 필요; default 는 문서화된 가정에 기반 — HW reference 없는 동안엔 절대 latency 를 잠정 취급, ratio 만 방어.
- 각 PE command dataclass 에 작은
logical_bytesproperty 추가.
Open review items
- FIXED 와 R 의 calibration 출처. default 는 "일반 composite = 45 ns
- on-die 큐 4 bytes/cycle"; on-die producer→consumer 큐로 합리. HW reference 등장 시 재방문.
- Scheduler plan-gen 비용. 0 유지 — D5 가 PE_SCHEDULER 의 plan
생성을 dispatch 공식 밖에 둠. scheduler-bound 행동이 보이면 기존
overhead_ns로 노출. - Override 위치. topology yaml 의 PE 노드 attrs 아래
pe_cost_model:block — 모든 knob 을 한 곳에, 리뷰 가능. - 경로 parity. greenlet (
_execute_legacy,kernel_runner) 와 replay 둘 다 같은 cost model 읽어야 함. 검증.
Test Requirements
- Anchor 보존. 일반 DMA→GEMM→DMA composite (1 op,
logical_bytes ≈ 52) 가 default 값에서 45 ns 에 dispatch. - 구조적 ratio. opt3 vs opt2 dispatch (ADR-0065 §verification 의):
default calibration 에서
opt3 / opt2 ≈ 2.4×. - Override 경로. topology yaml 의
pe_cost_model:block 이 per-PE dispatch cost 변경; block 누락 시 default 복귀. PeCpuOverheadCmd우회. 수동tl.cycles(n)는 정확히ncycles,n + dispatch_cycles(...)아님.- double-count 없음. PE_DMA
drain_ns, PE_GEMM/MATH_compute_ns가 pre-ADR 값과 동일. - 결정성. 동일 입력 → 동일 op_log + latency (SPEC §0.1).
- 경로 parity. greenlet 과 replay 가 동일 커널에 대해 동일 dispatch-cycle 회계.
Migration
ADR-0064 Revision 2 는 단일 PR 로 land:
- 각
PeCommanddataclass 의logical_bytesproperty pe_cpu.pydispatch 경로의 공식 적용- PE_CPU init 의
pe_cost_model:override read - 일회성 골든 재생성
land 후 ADR-0065 가 위에 쌓임 — 기존 bench 추가 골든 churn 없음
(ADR-0065 는 기존 경로의 CompositeCmd 의미 보존 refactor; opt2 만 새 bench).