# ADR-0064: op 종류별 CPU 발행 비용 모델 (command construct + dispatch)

## Status

Proposed

> **ADR-0060**(AHBM GQA Fused Attention)의 보조 ADR. 거기서의 하이브리드
> 결정(GEMM은 `tl.composite`, softmax 머지는 커널)이 이기는 이유는
> **타일링을 PE_SCHEDULER에 offload하여 CPU가 coarse descriptor를 내리고
> 앞서 나가 엔진을 saturate**하기 때문이다. 그 이점이 현재 시뮬레이터에서는
> per-op CPU 발행 비용이 0이라 **보이지 않는다**. 본 ADR은 발행 비용을
> 실재화하고 **op 종류별로 차등**화하여, composite-vs-primitive 트레이드오프
> (그리고 CPU saturation)가 측정 가능하도록 한다.

## Context

### 현재 상태

- 모든 `tl.*` op은 커맨드를 emit하기 전에 `_emit_dispatch_overhead()`를
  호출하며(`tl_context.py:123-125, 190, 227, 235, 612, …`), 이는
  `dispatch_cycles > 0`일 때**만** `PeCpuOverheadCmd(cycles=dispatch_cycles)`를
  emit한다. 즉 발행 비용은 `tl.load`, `tl.dot`, MATH op, `tl.composite`에
  동일하게 적용되는 **단일 균일 노브**다.
- 그 노브는 두 실행 경로 모두에서 **0**으로 하드코딩되어 있다
  (`pe_cpu.py:101` greenlet runner, `:195` legacy replay). ⇒ 커맨드 발행
  — *descriptor를 construct하고 scheduler 큐에 push* — 이 현재 PE_CPU에서
  **0 ns**다.
- `PeCpuOverheadCmd`는 PE_CPU(`kernel_runner.py:131-132`)와
  PE_SCHEDULER(`pe_scheduler.py:97-100`)에서 `yield env.timeout(cmd.cycles)`로
  소비된다. 수동 `tl.cycles(n)`도 존재한다(`tl_context.py:695`).

### 본 ADR을 규정하는 두 가지 발견 (ADR-0060 검토에서)

- **Q1 — composite 발행 비용.** `tl.composite`를 construct + push 하는 비용은
  CPU 시간 기준 **~40 ns** 정도(descriptor 빌드 + 큐 push)로 예상된다. 현재는
  **0**이다. 훅은 존재하며, 값(그리고 op 종류별 차등)만 빠져 있다.
- **Q2 — scheduler dispatch vs DMA latency.** PE_SCHEDULER의 composite
  dispatch는 **non-blocking**이다: `_dispatch_composite`는 tile plan을 생성하고
  feeder에 enqueue한 뒤 즉시 반환한다(`pe_scheduler.py:104-121`). 실제 **DMA
  latency는 타일이 흐를 때 PE_DMA에서 부과**되며(`drain_ns =
  compute_drain_ns(path, nbytes)`, `pe_dma.py:89`), scheduler dispatch에
  덩어리로 들어가지 **않는다** ⇒ **중복 계상 없음**, DMA는 modeled component에
  머문다(SPEC §0.1). scheduler의 plan 생성 자체는 현재 sim 시간 **0**이다.

### 균일-그리고-0이 하이브리드에 틀린 이유

하이브리드의 논거 전체는 **하나의** composite descriptor가 `N_tiles`만큼의
GEMM 타일링을 offload하므로, CPU가 `O(N_tiles × ops/tile)`개의 fine 커맨드
대신 `O(1)`개의 coarse 커맨드를 낸다는 것이다. 발행 비용 = 0(그리고 균일)이면
모델은 다음을 보여줄 수 없다:
- CPU가 충분히 빨리 못 밀어넣을 때 primitive 경로가 엔진을 **saturate 못 할 수
  있음**(핵심 ADR-0060 §1 주장), 그리고
- composite가 단일 primitive보다 construct 비용이 **더 크지만** 그것이 대체하는
  다수의 primitive보다는 훨씬 작음.

단일 균일 `dispatch_cycles`로는 이를 표현할 수 없다: 실제 construct 비용에서
`tl.load` ≪ `tl.composite`이기 때문이다.

## Decision

### D1. PE_CPU의 op 종류별 발행 비용 테이블

단일 `dispatch_cycles` 스칼라를 **커맨드 종류별 비용 테이블**로 교체하고,
발행 시점(커맨드가 scheduler로 dispatch되기 전)에 PE_CPU에서 부과한다.
greenlet이 이 시간을 지불하며, 이것이 바로 CPU가 얼마나 빨리 work를 밀어넣을
수 있는지를 gate한다 — saturation 레버.

시작 추정치(추후 calibrate — 검토 항목 참조):

| 발행 op | CPU 발행 비용 (construct + push) |
|---|---|
| `tl.composite` (GEMM/MATH descriptor) | ~40 ns (Q1 추정) |
| `tl.load` / `tl.store` (DMA descriptor) | 작음 (수 ns) |
| `tl.dot` / MATH / IPCQ send/recv | 작음 (수 ns) |

요점은 **비율**이다: composite construct ≫ primitive 발행, 그러나 그것이
대체하는 primitive 발행들의 합 ≪. 정확한 ns는 설정 가능하다.

### D2. 실행 latency는 엔진에 유지 (Q2 — 변경 없음)

DMA/GEMM/MATH 실행 latency는 오늘처럼 PE_DMA / PE_GEMM / PE_MATH에서 부과된다.
발행 비용(D1)은 **CPU 측에만** 추가되며, `drain_ns`를 건드리지 않으므로 중복
계상이 없다. 이는 SPEC §0.1(latency는 modeled component에서)을 보존한다.

### D3. scheduler plan 생성 비용 — 0에서 시작, 재검토

PE_SCHEDULER의 tile-plan 생성은 오늘 sim 시간 0이다. 일단 유지한다(지배적
레버는 CPU 발행 비용 D1); scheduler 측 비용이 유의미하다고 판명되면 기존
`overhead_ns` node attr로 노출한다. 여기서 결정하지 않고 검토 항목으로 둔다.

### D4. 설정 가능한 값; 골든 재생성

비용 테이블은 설정 가능하다(per-topology / node attrs, 기본값은 D1 추정치).
발행 비용을 0이 아니게 하면 **모든** bench latency가 바뀌므로, 골든 latency를
한 번 **재생성**한다 — ADR-0062의 전역 lazy-load 회귀와 동일한 자세의 모델
충실도 개선이다.

## Alternatives

### A1. 단일 균일 `dispatch_cycles > 0` 유지

기각: op construct 비용은 자릿수 단위로 다르다(`tl.load` vs `tl.composite`).
균일값은 primitive를 과대 청구하거나 composite를 과소 청구하며, 어느 쪽이든
하이브리드가 의존하는 composite-vs-primitive 트레이드오프를 왜곡한다.

### A2. 발행 비용을 PE_CPU 대신 PE_SCHEDULER에 부과

기각: saturation 질문은 *"CPU가 엔진을 바쁘게 유지할 만큼 descriptor를 빨리
밀어넣을 수 있는가?"*이며 — 이는 **PE_CPU**의 issue-bandwidth 속성이다.
scheduler에 부과하면 CPU back-pressure를 모델하지 못한다.

### A3. DMA program/setup 시간을 별도의 고정 per-descriptor 비용으로 모델

실제 HW는 전송 시간과 별개로 DMA descriptor program 비용을 지불한다. 연기:
초기에는 descriptor-program 비용을 **발행 op**의 D1 비용에 합친다(DMA를
유발하는 `tl.load` / composite). calibration이 유의미함을 보이면 PE_DMA 고정
setup으로 분리한다. 검토 항목.

## Consequences

### Positive
- 하이브리드의 CPU-offload / saturation 이점(ADR-0060 §1)이 구조적일 뿐
  아니라 **측정 가능**해진다.
- composite-vs-primitive 및 타일링 granularity 트레이드오프가 latency에
  보여 eval 서사를 가능케 한다.
- 하드웨어에 더 충실(issue bandwidth는 실제 병목).

### Negative
- **모든** bench 골든이 이동 → 한 번의 재생성(D4); CI 골든 fixture 갱신.
- op 종류별 값은 calibration 필요; ~40 ns composite 수치는 추정이고
  primitive는 미지정 — 결과는 숫자만큼만 정확하다(calibrate 전까지 절대
  latency를 과대 주장 금지).
- 발행 경로에 비용 테이블 lookup 추가(런타임 영향 미미).

## Open review items (자율 결정; 검토 시 수정)

1. **op 종류별 값의 calibration 출처.** composite ≈ 40 ns는 작업 추정치;
   primitive 발행 비용은 placeholder. *권고:* 문서화된 가정(instruction-issue +
   큐 push)에서 값을 택하고, 실제 reference가 생기기 전까지 절대 latency를
   잠정으로 취급; **비율**을 방어 가능하게 유지.
2. **scheduler plan-gen 비용 (D3).** *권고:* 초기에는 0 유지; scheduler-bound
   동작을 보이는 workload가 나오면 `overhead_ns`로 노출.
3. **DMA program 시간 (A3).** *권고:* 먼저 발행 op의 비용에 합치고; 필요 시
   PE_DMA setup으로 분리.
4. **테이블 위치.** *권고:* 흩어진 node attr이 아니라, 커맨드 종류로 키잉되고
   per-topology 오버라이드 가능한 작은 중앙 cost 모듈 — 값을 한 곳에서 검토
   가능하도록.
5. **회귀 롤아웃.** *권고:* lazy-load(ADR-0062)와 본 ADR의 cost model을 한
   번의 골든 재생성 패스로 함께 도입하여 두 번의 churn을 피함.
6. **legacy replay 경로와의 상호작용**(`pe_cpu.py:_execute_legacy`) — greenlet과
   replay 경로가 동일한 cost 테이블을 읽어 결과가 일치하도록 보장. 검증 요.

## Test Requirements

1. **composite는 한 번, primitive는 op마다 청구.** `N_tiles`에 걸쳐 하나의
   `tl.composite`를 내는 커널은 PE_CPU에서 composite 발행 비용을 한 번 청구하고;
   동등한 `N_tiles × ops` primitive 커널은 op당 비용을 `N_tiles × ops`번 청구.
   PE_CPU busy time이 그에 따라 다름을 assert.
2. **DMA latency 불변 (Q2).** 고정된 전송에 대해 PE_DMA `drain_ns`는 ADR 이전
   값과 동일 — 발행 비용은 DMA에 합쳐지지 않고 PE_CPU에 가산(중복 계상 없음).
3. **saturation 관측 가능.** per-op 발행 비용이 0이 아니면, many-tile primitive
   sweep는 GEMM 엔진 idle(CPU-bound 발행)을 보이고 composite sweep는 바쁘게
   유지 — ADR-0060 §1 레버.
4. **결정성:** 동일 입력 → 동일 op_log + latency (SPEC §0.1).
5. **경로 일치:** greenlet과 legacy-replay 경로가 동일 커널에 대해 동일한 발행
   비용 회계를 산출.
