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

15 KiB
Raw Blame History

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

CompositeCmdopsrw_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(_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 compositeOpSpec(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 사용):

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)

CompositeCmdlogical_bytesMAX_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).