# 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:101` greenlet, `:195` replay). - ⇒ 명령 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 룰로 계산, Python `sys.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` 재귀 합산: ```python @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`: ```python @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: ```yaml 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_merge` 8-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_bytes` property 추가. ## Open review items 1. **FIXED 와 R 의 calibration 출처.** default 는 "일반 composite = 45 ns + on-die 큐 4 bytes/cycle"; on-die producer→consumer 큐로 합리. HW reference 등장 시 재방문. 2. **Scheduler plan-gen 비용.** 0 유지 — D5 가 PE_SCHEDULER 의 plan 생성을 dispatch 공식 밖에 둠. scheduler-bound 행동이 보이면 기존 `overhead_ns` 로 노출. 3. **Override 위치.** topology yaml 의 PE 노드 attrs 아래 `pe_cost_model:` block — 모든 knob 을 한 곳에, 리뷰 가능. 4. **경로 parity.** greenlet (`_execute_legacy`, `kernel_runner`) 와 replay 둘 다 같은 cost model 읽어야 함. 검증. ## Test Requirements 1. **Anchor 보존.** 일반 DMA→GEMM→DMA composite (1 op, `logical_bytes ≈ 52`) 가 default 값에서 45 ns 에 dispatch. 2. **구조적 ratio.** opt3 vs opt2 dispatch (ADR-0065 §verification 의): default calibration 에서 `opt3 / opt2 ≈ 2.4×`. 3. **Override 경로.** topology yaml 의 `pe_cost_model:` block 이 per-PE dispatch cost 변경; block 누락 시 default 복귀. 4. **`PeCpuOverheadCmd` 우회.** 수동 `tl.cycles(n)` 는 정확히 `n` cycles, `n + dispatch_cycles(...)` 아님. 5. **double-count 없음.** PE_DMA `drain_ns`, PE_GEMM/MATH `_compute_ns` 가 pre-ADR 값과 동일. 6. **결정성.** 동일 입력 → 동일 op_log + latency (SPEC §0.1). 7. **경로 parity.** greenlet 과 replay 가 동일 커널에 대해 동일 dispatch-cycle 회계. ## Migration ADR-0064 Revision 2 는 단일 PR 로 land: - 각 `PeCommand` dataclass 의 `logical_bytes` property - `pe_cpu.py` dispatch 경로의 공식 적용 - PE_CPU init 의 `pe_cost_model:` override read - 일회성 골든 재생성 land 후 ADR-0065 가 위에 쌓임 — 기존 bench 추가 골든 churn 없음 (ADR-0065 는 기존 경로의 `CompositeCmd` 의미 보존 refactor; opt2 만 새 bench).