Files
kernbench2/docs/adr-proposed/ADR-0064-perf-cpu-issue-cost-model-ko.md
T
ywkang 95cecccd8e gqa(adr): ADR-0064/0065 review fixes — composite size cap, type-aware logical_bytes, R recalibration
ADR-0064 Revision 2 review fixes:
- D7 NEW: composite size cap (MAX_COMPOSITE_LOGICAL_BYTES default 1024
  bytes). Oversized recipes deterministically segmented into N
  CompositeCmds; each segment incurs its own dispatch cost. Models real
  HW limits (descriptor queue entry, scheduler parser buffer, command
  SRAM) and prevents the model from rewarding pathologically-large
  fused composites.
- D2: type-aware extra-field byte counting (int/float=4, bool=1,
  tuple/list=1+4N, str=1) — replaces uniform 4 bytes per extra.
- D3: recalibrated defaults to FIXED=40 cycles, R=0.0625 cycles/byte
  (16 B/cycle — typical on-die descriptor queue width); anchor stays at
  ~43 ns for typical 1-OpSpec composite. Clarified anchor description:
  DMA stages do not appear in logical_bytes (auto-inserted by
  PE_SCHEDULER from operand.space per ADR-0065 D4).
- D4: removed clock_freq_ghz from pe_cost_model: override block;
  conversion uses the PE node's existing clock_freq_ghz attr. Added
  max_composite_logical_bytes knob.
- Context: emphasized command-count reduction (FIXED) as the primary
  signal; byte term as secondary refinement.
- Open review: added large-composite scheduler-cost stress test.
- Test req: added composite-size-cap (#8) and R-sensitivity sweep (#9).

ADR-0065 + DDD-0065 follow-on updates:
- opt2 vs opt3 dispatch ratio updated 2.4× → ≈4.0× under new defaults
  (FIXED-dominated, reflecting the corrected framing).
- Test req #9: decode opt2 composite fits within 1024-byte cap; no
  segmentation needed for the GQA workload.
- DDD §6: TLContext lowering checks logical_bytes against cap (step 8).
- DDD §11: performance model recomputed with new defaults + sensitivity
  table across R ∈ {0.25, 0.0625, 0.03125} confirming opt2 < opt3 holds.
- DDD §9 P6 gate: ratio band 2.4×±10% → 4.0×±15%; sensitivity sweep added.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-10 16:21:58 -07:00

338 lines
15 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.
### 이 모델이 실제로 노출하는 것
**1차** 신호는 per-cmd FIXED 비용에 의한 **command-count 감소**.
**byte** 항은 *비정상적으로 큰* composite 가 공짜로 보이지 않도록 막는
2차 보정. 현실적 on-die 큐 대역폭 (16 B/cycle, D3) 에서 FIXED 가
decode opt2 의 opt3 (≈10 cmds/tile) vs opt2 (≈2 cmds/tile) 의 dispatch
cost 차이 중 ≥85% 차지.
이 framing 이 모델의 한계도 정의: 단일 composite 가 무한히 커지면 byte
항만으로는 HW 가 받지 못할 정도의 fused command 를 모델이 보상하는 것을
막지 못함 — 그래서 **D7** 의 descriptor 크기 cap 이 필요.
## 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(_extra_bytes(v) for v in self.extra.values())
)
def _extra_bytes(v) -> int:
"""OpSpec.extra 값의 타입-aware byte 카운트."""
if isinstance(v, bool): return 1
if isinstance(v, (int, float)): return 4
if isinstance(v, (tuple, list)): return 1 + 4 * len(v) # shape, axes, …
if isinstance(v, str): return 1 # opcode-like tag
return 4 # default scalar
# extra 의 예시 타입:
# m, k, n int → 4 each
# reduce_axis int → 4
# shape=(64, 64) tuple → 1 + 8 = 9
# factor=1.0 float → 4
```
(`DmaReadCmd`, `MathCmd` 등도 같은 룰 — dataclass 당 property 1 개,
약 3 줄.)
### D3. Default — 일반 composite ≈ 43 ns 기준
Anchor: **DMA-staged GEMM 경로용 단일-OpSpec composite**
`OpSpec(kind="gemm", ...)` 1 개; DMA stage 는 PE_SCHEDULER 가 operand
`space` 에서 자동 삽입 (ADR-0065 D4), **`logical_bytes` 에 등장하지 않음**
(커널이 별도 cmd 로 emit 안 함). 분해:
```
framing 4
ops tuple length 1
GEMM OpSpec 40 (opcode 1 + scope 1 + 1 + 2 handles 16
+ out 8 + 1 + extra m/k/n 12)
rw_handles tuple length 1
rw_handles content 8 (출력 RW 핸들 1 개)
─────────────────────────────
total ~54 bytes
```
Dispatch 목표 ≈43 ns. on-die producer→consumer 큐 16 bytes/cycle
(일반 on-die descriptor queue 폭) 가정.
```
FIXED_PER_CMD = 40 cycles
R = 0.0625 cycles/byte (= 16 bytes/cycle)
```
검증: `40 + 54 × 0.0625 = 43.375 cycles ≈ 43 ns`
Cycle→ns 변환은 PE 노드의 기존 `clock_freq_ghz` attr (PE_MATH `_compute_ns`
가 쓰는 같은 것) 사용. cost-model knob 은 **cycle-domain 만** — clock
설정 중복 안 함.
### D4. topology config override
default 는 `pe_cpu.py` 에 내장. topology yaml 의 PE 노드 attrs 아래
`pe_cost_model:` 절로 override (cycle-domain knob 만; clock 은 PE 의
기존 `clock_freq_ghz` 사용):
```yaml
pe:
attrs:
clock_freq_ghz: 1.0 # 기존, cycle→ns 변환용
pe_cost_model:
fixed_per_cmd_cycles: 40
byte_cycles_recip: 0.0625 # = 16 bytes/cycle
max_composite_logical_bytes: 1024 # D7 — descriptor 크기 cap
```
누락된 키는 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 측정에 적용.
### D7. Composite 크기 cap (deterministic segmentation)
`CompositeCmd``logical_bytes`**`MAX_COMPOSITE_LOGICAL_BYTES`**
(default **1024 bytes**, D4 로 override) 로 제한. 초과하는 cmd 는
*emitter* (host-side TLContext, ADR-0065 D5) 가 N 개 연속 `CompositeCmd`
로 deterministic 하게 **segment** — 각 ≤ cap.
- 각 segment 가 자기 `completion: CompletionHandle` 보유.
- 각 segment 가 자기 dispatch cost — `total = sum(FIXED + bytes_i × R) =
N × FIXED + total_bytes × R`. FIXED 항이 segment 당.
- segment 가 `rw_handles` 공유 — **strict FIFO** (ADR-0065 D6.3) 가
write-after-write 순서 자동 보존.
- segmentation 알고리즘 결정적 (emit 순서로 op 인덱스 greedy); host
emitter 의 일부, PE_SCHEDULER 가 아님.
이게 필요한 이유. 실제 하드웨어는 descriptor queue entry 크기, scheduler
parser buffer, command SRAM, firmware 입력의 하드 제한 보유. cap 없으면
`FIXED + bytes × R` 모델이 하드웨어가 받지 못할 정도로 큰 fused composite
를 보상 (예: primitive 100 개를 composite 1 개로 fuse, FIXED 1 회만 지불).
~30 bytes 짜리 op 100 개의 composite = ~3000 bytes > 1024 cap → 3 개로
segment → 3 × FIXED, 정직한 회계 회복.
Decode opt2 의 `#2` composite (10 ops, ~310 bytes) 는 1024 cap 안에
편안히 — GQA workload 에 segmentation 없음.
## 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 ≈ 43 ns
+ on-die 큐 16 bytes/cycle"; on-die descriptor 큐로 합리.
HW reference 등장 시 재방문.
2. **대형 composite 에서 Scheduler plan-gen 비용.** 0 유지 — D5 가
PE_SCHEDULER 의 plan 생성을 dispatch 공식 밖에 둠. D7 cap (1024 bytes
≈ 3035 OpSpec) 이 최악을 묶지만, cap 근처 composite 도 현재
zero-cost 모델에서 1-op composite 와 같은 scheduler 비용. stress test
(대형 composite microbench) 가 scheduler-bound 행동을 보이면
per-op-count `overhead_ns` 노출.
3. **Override 위치.** topology yaml 의 PE 노드 attrs 아래 `pe_cost_model:`
block — 모든 knob 을 한 곳에, 리뷰 가능. clock 은 PE 의 기존
`clock_freq_ghz` 에서 — 여기 중복 안 함.
4. **경로 parity.** greenlet (`_execute_legacy`, `kernel_runner`) 와
replay 둘 다 같은 cost model 읽어야 함. 검증.
5. **R 에 대한 결론의 민감도.** opt2 < opt3 가 합리적 `R` 범위 (큐 대역폭
가정) 에서 유지되어야 함. 민감도 sweep 이 Test Requirements (#9) 의
일부.
## Test Requirements
1. **Anchor 보존.** DMA-staged GEMM 경로용 단일-OpSpec composite
(`logical_bytes ≈ 54`) 가 default 값에서 ≈43 ns 에 dispatch (±2 ns).
2. **구조적 ratio.** opt3 vs opt2 dispatch (ADR-0065 §verification 의):
default calibration 에서 `opt3 / opt2 ≈ 4.0×`.
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 회계.
8. **Composite 크기 cap (D7).** `logical_bytes >
MAX_COMPOSITE_LOGICAL_BYTES` 가 될 recipe 가 N 개 연속 `CompositeCmd`
로 segment; 총 dispatch = segment dispatch 의 합; segment 간 RW 순서
strict FIFO 로 보존.
9. **민감도 sweep.** `R ∈ {0.25, 0.0625, 0.03125}` cycles/byte
(= 4 / 16 / 32 bytes/cycle) 에서 *opt2 per-tile dispatch
< opt3 per-tile dispatch* 결론 유지 (ratio 가 R 감소 시 단조 증가
— FIXED 가 더 지배).
## Migration
ADR-0064 Revision 2 는 단일 PR 로 land:
- 각 `PeCommand` dataclass 의 `logical_bytes` property (D2 의 타입-aware
extra 카운트)
- `pe_cpu.py` dispatch 경로의 공식 적용
- PE_CPU init 의 `pe_cost_model:` override read (cycle-domain knob +
`max_composite_logical_bytes`)
- **composite 크기 cap (D7)** — TLContext-side segmentation 로직,
`MAX_COMPOSITE_LOGICAL_BYTES` default 1024; 기존 bench 에 필요 없음
(현재 최대 composite 가 200 bytes 훨씬 아래), 하지만 ADR-0065 의 10-op
decode-opt2 composite (~310 bytes) + 더 큰 recipe 가 나타날 때를 위해
메커니즘 land.
- 일회성 골든 재생성
land 후 ADR-0065 가 위에 쌓임 — 기존 bench 추가 골든 churn 없음
(ADR-0065 는 기존 경로의 `CompositeCmd` 의미 보존 refactor; opt2 만 새 bench).