gqa(adr-0064/0065): flat-ops CompositeCmd (P1) + structural dispatch cost (ADR-0064 Rev2); promote ADR-0064
ADR-0065 P1: CompositeCmd -> flat ordered ops list (drop legacy op/a/b/out_addr fields); OpSpec.operands dict + out handle. Meaning-preserving (op_log byte-equal); pe_scheduler + op_log read the head op. ADR-0064 Rev2: replace Rev1 per-op cost table with structural FIXED + logical_bytes*R formula. logical_bytes on every PeCommand; new common/pe_cost_model.py; cost centralized in TLContext._emit (load/recv_async charge explicitly); pe_cpu/kernel_runner wire the per-PE model + clock. D7: cap exceeded -> ValueError (no auto-segmentation). Remove Rev1 cpu_issue_cost.py + its tests. No goldens churn. Promote ADR-0064 Rev2 Proposed->Accepted (docs/adr/ + docs/adr-ko/); amend D7 (error not segmentation) + record P1-before-P0 ordering in ADR-0064/0065 Migration notes (EN+KO). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,377 +0,0 @@
|
||||
# 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 줄.)
|
||||
|
||||
**카운팅 룰 — per-op 합산, dedup 없음.** `OpSpec` 안의 각 `TensorHandle`
|
||||
참조는 독립적으로 카운트 — 같은 핸들이 여러 OpSpec 에 등장하거나
|
||||
`rw_handles` 에도 있어도 마찬가지. `rw_handles` block 은 cross-composite
|
||||
해저드 추적 (ADR-0065 D6.3) 의 메타데이터로 `ops` 의 operand 참조와
|
||||
**별도** 카운트. 중복 제거 없음. HW 와 일치: descriptor 가 각 operand
|
||||
slot 을 독립 address field 로 인코딩하고 dispatcher 가 `rw_handles` 를
|
||||
별도 metadata block 으로 추적. 예: `OpSpec(kind="mul_bcast", operands=
|
||||
{"src_a": O, "src_b": corr}, out=O)` 는 핸들 `O` 를 *두 번* 카운트
|
||||
(`src_a` 한 번, `out` 한 번); 만약 `O` 가 둘러싼 CompositeCmd 의
|
||||
`rw_handles` 에도 있으면 세 번째 카운트.
|
||||
|
||||
### 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 가 아님.
|
||||
|
||||
**cap 이 필요한 이유.** 실제 하드웨어는 descriptor queue entry 크기,
|
||||
scheduler parser buffer, command SRAM, firmware 입력의 하드 제한 보유.
|
||||
cap 없으면 `FIXED + bytes × R` 모델이 하드웨어가 받지 못할 정도로 큰
|
||||
fused composite 를 보상 (예: primitive 100 개를 composite 1 개로 fuse,
|
||||
FIXED 1 회만 지불).
|
||||
|
||||
**1024 bytes 라는 *특정* 숫자의 근거.** 이는 측정된 HW 숫자가 아니라
|
||||
**safe engineering limit** — kernbench 코드베이스의 현재 모든 알려진
|
||||
composite (가장 큰 것이 decode opt2 의 `#2` ~322 bytes) 보다 훨씬 위이면서
|
||||
미래 recipe 도 지켜야 할 *유한* descriptor capacity 를 의도적으로 표현.
|
||||
값은 topology 별 override (D4); 실제 HW reference 등장 시 재캘리브레이션.
|
||||
이 default 의 역할은 cap 을 *원칙으로 존재시키는 것*, 특정 HW 에 맞추는
|
||||
것이 아님.
|
||||
|
||||
**segment 간 순서 — strict FIFO 가 주체, `rw_handles` 아님.** segment 들은
|
||||
emit 순서로 PE_CPU → PE_SCHEDULER 큐에 들어감. strict-FIFO dispatch
|
||||
(ADR-0065 D6.3) 가 *순서의 주체* — segment 가 큐 진입 순서로 실행. 각
|
||||
segment 의 `rw_handles` block 은 cross-composite 해저드 추적기의 **의존성
|
||||
메타데이터** — 그 자체로 inter-segment 순서를 보장하지 **않음**. 미래
|
||||
scheduler 가 FIFO 를 완화 (예: RW-aware reorder, ADR-0065 A4) 한다면
|
||||
segmenter 는 (a) segment 를 단일 CompositeCmd 로 merge 또는
|
||||
(b) 명시적 completion-handle 의존 체인 도입 필요. Phase 1 에선 무관 —
|
||||
strict FIFO 가 유효.
|
||||
|
||||
Decode opt2 의 `#2` composite (10 ops, ~322 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
|
||||
≈ 30–35 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
|
||||
|
||||
테스트는 specific 숫자 anchor 가 아닌 **공식 (D1)** 에 대해 작성 —
|
||||
calibration 이 바뀌거나 OpSpec/CompositeCmd 필드가 추가되어도 유효.
|
||||
|
||||
1. **공식 보존.** 임의 `CompositeCmd` `c` 에 대해 PE_CPU 의 기록된
|
||||
dispatch overhead 가 `FIXED_PER_CMD + c.logical_bytes × R`
|
||||
(floor/round-off 의 ±1 cycle 내). 여러 composite parametrize:
|
||||
1-OpSpec GEMM composite, 5-op MATH chain, 10-op recipe composite.
|
||||
Default-calibration 숫자 (1-OpSpec composite 의 anchor ≈43 ns) 는
|
||||
*informative* 참고, 테스트 gate 가 아님 — gate 는 공식 등가.
|
||||
2. **정성적 ratio (robust).** opt3 per-tile PE_CPU dispatch 가 opt2
|
||||
per-tile dispatch 를 최소 2× 차이로 엄격히 초과 — `opt3 > 2 × opt2`.
|
||||
Default-calibration 모델은 ≈4× 예측; gate 는 느슨한 2× 한계라
|
||||
calibration 이동 (예: HW reference 가 default 대체) 시 깨지지 않음.
|
||||
Informative 숫자 — ADR-0065 §verification 와 DDD-0065 §11 의 모델
|
||||
기대치 참조.
|
||||
3. **Override 경로.** topology yaml 의 `pe_cost_model:` block 이 per-PE
|
||||
dispatch cost 변경; block 누락 시 default 복귀. #1 의 공식 등가가
|
||||
override 값에서도 성립.
|
||||
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; `sum(segment.logical_bytes) == original logical_bytes`;
|
||||
총 dispatch = segment dispatch 의 합 (FIXED 가 segment 당 지불);
|
||||
inter-segment 순서 strict FIFO 로 보존 (`rw_handles` 단독 아님).
|
||||
9. **민감도 (정성적).** `R ∈ {0.25, 0.0625, 0.03125}` cycles/byte 에서
|
||||
3 지점 모두 `opt3 > opt2`. 방향 (R 감소 시 ratio 단조 증가) 도 단언,
|
||||
단 절대 ratio 값은 *불필요*.
|
||||
|
||||
## 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).
|
||||
@@ -1,410 +0,0 @@
|
||||
# ADR-0064: Structural CPU dispatch cost model (`logical_bytes` + FIXED + R)
|
||||
|
||||
## Status
|
||||
|
||||
Proposed (Revision 2)
|
||||
|
||||
> Supporting ADR for **ADR-0060** (AHBM GQA Fused Attention) and **ADR-0065**
|
||||
> (flat-ops composite + first stateful recipe). The hybrid decision there
|
||||
> (GEMMs via `tl.composite`, softmax merge in the kernel) wins by
|
||||
> **offloading tiling to PE_SCHEDULER so the CPU issues coarse descriptors
|
||||
> and runs ahead, keeping the engines saturated**. That win is currently
|
||||
> **invisible in the simulator** because per-op CPU issue cost is zero.
|
||||
>
|
||||
> Revision 2 replaces the **op-type calibration table** (the original
|
||||
> proposal) with a **structural formula** derived from each command's
|
||||
> `logical_bytes` — no per-op-type calibration needed; new op kinds are
|
||||
> covered automatically.
|
||||
|
||||
## Context
|
||||
|
||||
### What exists today
|
||||
|
||||
- Every `tl.*` op calls `_emit_dispatch_overhead()` before emitting its
|
||||
command (`tl_context.py:196-212`), which emits
|
||||
`PeCpuOverheadCmd(cycles=dispatch_cycles)` **only if** `dispatch_cycles
|
||||
> 0`. The knob is **uniform** across op kinds and hardcoded to **0**
|
||||
in both live execution paths (`pe_cpu.py:101` greenlet, `:195` replay).
|
||||
- ⇒ issuing a command — *constructing the descriptor and pushing it to
|
||||
the scheduler queue* — currently costs **0 ns** on PE_CPU.
|
||||
- `PeCpuOverheadCmd` is consumed as `yield env.timeout(cmd.cycles)` on
|
||||
PE_CPU (`kernel_runner.py:131-132`).
|
||||
|
||||
### Why uniform-and-zero is wrong for the hybrid
|
||||
|
||||
ADR-0060 §1's argument is that **one** composite descriptor offloads
|
||||
`N_tiles` worth of GEMM tiling, so the CPU issues `O(1)` coarse commands
|
||||
instead of `O(N_tiles × ops/tile)` fine ones. With issue cost = 0, the
|
||||
model cannot show:
|
||||
- that the primitive path may **fail to saturate** the engines when the
|
||||
CPU cannot push fast enough, nor
|
||||
- that a composite **costs more to construct** than a single primitive
|
||||
but far less than the many primitives it replaces.
|
||||
|
||||
### Why per-op-type calibration (Revision 1) was over-shaped
|
||||
|
||||
The original proposal had a `cost_table[kind]` keyed by op kind
|
||||
(`composite`, `load`, `dot`, `math`, …). That required:
|
||||
- a value per kind (calibration cost ≥ |kinds|),
|
||||
- a new entry every time a new kind appears,
|
||||
- and yet the *ratio* it tried to capture — "composite ≫ primitive,
|
||||
but ≪ the primitives it replaces" — is structurally a function of
|
||||
**how many fields the command carries**, not of the op kind.
|
||||
|
||||
A command's *byte footprint* is the natural proxy: a composite carrying
|
||||
N OpSpecs has ~N× the bytes of a primitive op with one OpSpec. The
|
||||
*fixed* part (queue head update, completion register, MMIO-class
|
||||
latency) is per-command. The two together compose: `FIXED + bytes × R`.
|
||||
|
||||
### What this model actually exposes
|
||||
|
||||
The **primary** signal is **command-count reduction** through the
|
||||
per-command FIXED cost. The **byte** term is a secondary refinement
|
||||
that prevents pathologically-large composites from looking free. With
|
||||
realistic on-die queue bandwidth (16 B/cycle, D3), FIXED accounts for
|
||||
≥85% of the dispatch cost differential between opt3 (≈10 cmds/tile)
|
||||
and opt2 (≈2 cmds/tile) for decode opt2.
|
||||
|
||||
This framing also bounds the model: if a single composite were allowed
|
||||
to grow unboundedly large, the byte term alone would not stop the
|
||||
formula from rewarding ever-bigger fused commands beyond what real HW
|
||||
supports — hence the descriptor-size cap in **D7**.
|
||||
|
||||
## Decision
|
||||
|
||||
### D1. Structural dispatch cost formula
|
||||
|
||||
Each PE command going to PE_SCHEDULER incurs PE_CPU dispatch cycles:
|
||||
|
||||
```
|
||||
dispatch_cycles(cmd) = FIXED_PER_CMD + cmd.logical_bytes × R
|
||||
```
|
||||
|
||||
where:
|
||||
- `FIXED_PER_CMD` (cycles per command) models queue-tail update, MMIO-class
|
||||
RTT, completion-event registration — fixed per command regardless of size.
|
||||
- `R` (cycles per byte) models the queue-write bandwidth — bytes of the
|
||||
command serialized into the scheduler queue.
|
||||
- `cmd.logical_bytes` (int) is each command's *HW-logical* byte size,
|
||||
computed from D2 below — not Python's `sys.getsizeof`.
|
||||
|
||||
PE_CPU emits `PeCpuOverheadCmd(cycles=dispatch_cycles(cmd))` before
|
||||
dispatching, exactly as the existing hook (`tl_context.py:_emit_dispatch_
|
||||
overhead`) — only the cycle value changes.
|
||||
|
||||
### D2. `logical_bytes` rule
|
||||
|
||||
Each PE command dataclass exposes `logical_bytes: int` (property). The
|
||||
counting rule (HW-friendly, ignores Python overhead):
|
||||
|
||||
| Field kind | Bytes |
|
||||
|---|---|
|
||||
| Command framing (cmd-type discriminator + completion id ref) | 4 |
|
||||
| Opcode (op kind enum) | 1 |
|
||||
| Enum (scope, etc.) | 1 |
|
||||
| `TensorHandle` reference (address only — shape/dtype assumed in descriptor table) | 8 |
|
||||
| Scalar (int/float) | 4 |
|
||||
| Tuple length marker | 1 |
|
||||
|
||||
`CompositeCmd` recursively sums its `ops` and `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:
|
||||
"""Type-aware byte count for OpSpec.extra values."""
|
||||
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
|
||||
|
||||
|
||||
# Example types in extra:
|
||||
# m, k, n int → 4 each
|
||||
# reduce_axis int → 4
|
||||
# shape=(64, 64) tuple → 1 + 8 = 9
|
||||
# factor=1.0 float → 4
|
||||
```
|
||||
|
||||
(Identical rule for `DmaReadCmd`, `MathCmd`, etc. — one property per
|
||||
dataclass, ~3 lines each.)
|
||||
|
||||
**Counting rule — per-op summation, no deduplication.** Each
|
||||
`TensorHandle` reference inside an `OpSpec` is counted independently,
|
||||
even when the same handle appears in multiple OpSpecs or in
|
||||
`rw_handles`. The `rw_handles` block is metadata for cross-composite
|
||||
hazard tracking (ADR-0065 D6.3) and is counted separately from
|
||||
operand references in `ops`. There is no deduplication. This matches
|
||||
HW reality: the descriptor encodes each operand slot as an independent
|
||||
address field, and the dispatcher tracks `rw_handles` as a distinct
|
||||
metadata block. Example: `OpSpec(kind="mul_bcast", operands={"src_a":
|
||||
O, "src_b": corr}, out=O)` counts handle `O` *twice* (once for
|
||||
`src_a`, once for `out`); if `O` is also in the enclosing
|
||||
CompositeCmd's `rw_handles`, it is counted a third time.
|
||||
|
||||
### D3. Defaults — anchored on a typical composite ≈ 43 ns
|
||||
|
||||
Anchor: a **single-OpSpec composite for a DMA-staged GEMM path** —
|
||||
one `OpSpec(kind="gemm", ...)`; DMA stages are auto-inserted by
|
||||
PE_SCHEDULER from operand `space` (ADR-0065 D4) and **do not appear in
|
||||
`logical_bytes`** (the kernel does not issue them as separate cmds).
|
||||
Breakdown:
|
||||
|
||||
```
|
||||
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 (one RW handle for the output)
|
||||
─────────────────────────────
|
||||
total ~54 bytes
|
||||
```
|
||||
|
||||
Target dispatch = ~43 ns. On-die producer→consumer queue at 16 bytes/cycle
|
||||
(typical on-die descriptor queue width).
|
||||
|
||||
```
|
||||
FIXED_PER_CMD = 40 cycles
|
||||
R = 0.0625 cycles/byte (= 16 bytes/cycle)
|
||||
```
|
||||
|
||||
Verification: `40 + 54 × 0.0625 = 43.375 cycles ≈ 43 ns` ✓
|
||||
|
||||
Cycle→ns conversion uses the PE node's existing `clock_freq_ghz` attr
|
||||
(the same one used by PE_MATH `_compute_ns`). The cost-model knobs are
|
||||
**cycle-domain only** — they do not duplicate the clock setting.
|
||||
|
||||
### D4. Topology config override
|
||||
|
||||
Defaults are baked into `pe_cpu.py`. Topology yaml may override under a
|
||||
`pe_cost_model:` section at the PE node attrs (cycle-domain knobs only;
|
||||
clock comes from the PE's existing `clock_freq_ghz`):
|
||||
|
||||
```yaml
|
||||
pe:
|
||||
attrs:
|
||||
clock_freq_ghz: 1.0 # existing, used for 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 size cap
|
||||
```
|
||||
|
||||
Missing keys fall back to defaults. The dispatch formula reads from
|
||||
`node.attrs["pe_cost_model"]` at PE_CPU init.
|
||||
|
||||
### D5. Scope — what does and does not pay
|
||||
|
||||
| Path | Pays dispatch cost? |
|
||||
|---|---|
|
||||
| PE_CPU → PE_SCHEDULER for any `PeCommand` | **Yes** |
|
||||
| `PeCpuOverheadCmd` itself (already cycles-explicit) | **No** (formula bypass) |
|
||||
| Stages auto-generated by PE_SCHEDULER (DMA_READ/WRITE/FETCH/STORE) | **No** (PE_SCHEDULER-internal) |
|
||||
| Engine compute latency (DMA `drain_ns`, GEMM/MATH `_compute_ns`) | **No change** — stays on engines (SPEC §0.1) |
|
||||
|
||||
This preserves the "latency on modelled components" invariant — dispatch
|
||||
cost is *additional* CPU-side time, not folded into engine times.
|
||||
|
||||
### D6. Configurable values; goldens regenerate
|
||||
|
||||
Turning issue cost non-zero changes **every** bench's latency. Golden
|
||||
latencies are **regenerated once** when this ADR lands — same posture as
|
||||
ADR-0062 D3 lazy-load. After regeneration, the same calibration is in
|
||||
effect for ADR-0065 opt2 measurement.
|
||||
|
||||
### D7. Composite size cap (deterministic segmentation)
|
||||
|
||||
Each `CompositeCmd`'s `logical_bytes` is capped at
|
||||
**`MAX_COMPOSITE_LOGICAL_BYTES`** (default **1024 bytes**, overridable
|
||||
per D4). Oversized commands are deterministically **segmented** by the
|
||||
*emitter* (host-side TLContext, ADR-0065 D5) into N consecutive
|
||||
`CompositeCmd`s, each ≤ cap.
|
||||
|
||||
- Each segment carries its own `completion: CompletionHandle`.
|
||||
- Each segment incurs its own dispatch cost — `total =
|
||||
sum(FIXED + bytes_i × R) = N × FIXED + total_bytes × R`. The FIXED
|
||||
term is paid per segment.
|
||||
- Segments share `rw_handles` where applicable; **strict FIFO**
|
||||
(ADR-0065 D6.3) preserves write-after-write ordering automatically.
|
||||
- The segmentation algorithm is deterministic (greedy by op index in
|
||||
emit order); it is part of the host emitter, not PE_SCHEDULER.
|
||||
|
||||
**Why a cap is needed.** Real hardware imposes hard limits — descriptor
|
||||
queue entry size, scheduler parser buffer, command SRAM, firmware
|
||||
input. Without a cap, the `FIXED + bytes × R` model would reward
|
||||
arbitrarily large fused composites beyond what hardware accepts (e.g.,
|
||||
fusing 100 primitive ops into one composite, paying one `FIXED`).
|
||||
|
||||
**Why 1024 bytes specifically.** This is a **safe engineering limit**,
|
||||
not a measured HW number — intentionally chosen to be well above all
|
||||
currently known composites (decode opt2's `#2` at ~322 bytes is the
|
||||
largest in the kernbench codebase) while still representing a *finite*
|
||||
descriptor capacity that future recipes must respect. The number is
|
||||
overridable per topology (D4); when a real HW reference appears, the
|
||||
value should be recalibrated. The role of this default is to make the
|
||||
cap *exist as a discipline*, not to fit a specific HW.
|
||||
|
||||
**Ordering of segments — driven by strict FIFO, not `rw_handles`.**
|
||||
Segments are emitted into the PE_CPU → PE_SCHEDULER queue in their
|
||||
emit order. Strict-FIFO dispatch (ADR-0065 D6.3) is the *ordering
|
||||
source*: segments execute in the order they enter the queue. The
|
||||
`rw_handles` block on each segment is **dependency metadata** for the
|
||||
cross-composite hazard tracker — it does **not** by itself guarantee
|
||||
inter-segment ordering. If a future scheduler relaxed FIFO (e.g., to
|
||||
RW-aware reorder, ADR-0065 A4), the segmenter would need to either
|
||||
(a) merge segments into a single CompositeCmd, or (b) introduce an
|
||||
explicit completion-handle dependency chain. For Phase 1 this does
|
||||
not arise: strict FIFO is in effect.
|
||||
|
||||
Decode opt2's `#2` composite (10 ops, ~322 bytes) sits comfortably
|
||||
inside the 1024 cap — no segmentation for the GQA workload.
|
||||
|
||||
## Alternatives
|
||||
|
||||
### A1. Keep Revision 1's op-type calibration table
|
||||
|
||||
Rejected: calibration cost scales with |kinds|, and the *ratio* the
|
||||
table tried to capture is structurally a function of cmd size. The
|
||||
structural formula reaches the same qualitative behaviour with two
|
||||
calibratable numbers instead of N.
|
||||
|
||||
### A2. Byte-only formula (no FIXED term)
|
||||
|
||||
Rejected. With FIXED = 0, opt2 (Option Y per ADR-0065) does **not** win
|
||||
over opt3 — the total *bytes* dispatched per tile are similar (opt3 ≈ 232,
|
||||
opt2 ≈ 380); the win is entirely in *fewer per-cmd fixed costs*. A
|
||||
byte-only formula erases the very signal the model needs to expose.
|
||||
|
||||
### A3. Charge dispatch on PE_SCHEDULER instead of PE_CPU
|
||||
|
||||
Rejected: the saturation question is *"can the CPU push descriptors fast
|
||||
enough to keep the engines busy?"* — that is a **PE_CPU** issue-bandwidth
|
||||
property. Charging on the scheduler would not model CPU back-pressure.
|
||||
|
||||
### A4. Model DMA program/setup time as a separate fixed per-descriptor cost
|
||||
|
||||
Deferred: initially fold the descriptor-program cost into the **issuing
|
||||
op's** dispatch cost. Split it out to a PE_DMA fixed setup only if
|
||||
calibration shows it matters.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
- Hybrid's CPU-offload / saturation win (ADR-0060 §1) becomes
|
||||
**measurable**, with a structurally honest model (no calibration table).
|
||||
- Adding new op kinds (e.g., ADR-0065's `softmax_merge` 8-step recipe)
|
||||
costs zero — they fit the same formula automatically.
|
||||
- More faithful to hardware (queue-head MMIO RTT + queue-write bandwidth).
|
||||
|
||||
### Negative
|
||||
- **All** bench goldens shift → one-time regeneration (D6); CI golden
|
||||
fixtures update.
|
||||
- Two calibration knobs (FIXED, R) need values; defaults are anchored on
|
||||
a documented assumption — treat absolute latencies as provisional until
|
||||
a reference exists; keep the **ratios** defensible.
|
||||
- Adds a small `logical_bytes` property to each PE command dataclass.
|
||||
|
||||
## Open review items
|
||||
|
||||
1. **Calibration source for FIXED and R.** Defaults from "typical
|
||||
composite ≈ 43 ns + on-die queue 16 bytes/cycle"; reasonable for an
|
||||
on-die descriptor queue. Revisit when a HW reference appears.
|
||||
2. **Scheduler plan-gen cost vs large composites.** Stays 0 — D5 keeps
|
||||
PE_SCHEDULER's plan-generation outside the dispatch formula. The
|
||||
D7 cap (1024 bytes ≈ 30–35 OpSpecs) bounds the worst case, but a
|
||||
composite near the cap still costs the scheduler the same as a 1-op
|
||||
composite under the current zero-cost model. If a stress test
|
||||
(large-composite microbench) shows scheduler-bound behaviour,
|
||||
expose `overhead_ns` per-op-count.
|
||||
3. **Where the override lives.** `pe_cost_model:` block under PE node
|
||||
attrs in topology yaml — keeps all knobs in one place, reviewable.
|
||||
Clock comes from the PE's existing `clock_freq_ghz` attr, not
|
||||
duplicated here.
|
||||
4. **Path parity.** Both greenlet (`_execute_legacy` and `kernel_runner`)
|
||||
and replay paths must read the same cost model. Verify.
|
||||
5. **Sensitivity of conclusions to R.** opt2 < opt3 must hold across a
|
||||
reasonable range of `R` (queue-bandwidth assumption). Sensitivity
|
||||
sweep is part of Test Requirements (#9).
|
||||
|
||||
## Test Requirements
|
||||
|
||||
Tests are written against the **formula** (D1), not against specific
|
||||
numeric anchors, so they remain valid when calibration changes or when
|
||||
OpSpec/CompositeCmd fields are added.
|
||||
|
||||
1. **Formula preservation.** For any `CompositeCmd` `c`, PE_CPU's
|
||||
recorded dispatch overhead equals `FIXED_PER_CMD + c.logical_bytes
|
||||
× R` (within ±1 cycle for floor/round-off). Parametrized over
|
||||
several composites: a 1-OpSpec GEMM composite, a 5-op MATH chain,
|
||||
and a 10-op recipe composite. Default-calibration numbers (anchor
|
||||
≈43 ns for the 1-OpSpec composite) are informative reference, not
|
||||
the test gate — the test gate is the formula equality.
|
||||
2. **Qualitative ratio (robust).** opt3 per-tile PE_CPU dispatch
|
||||
strictly exceeds opt2 per-tile dispatch by at least a 2× margin —
|
||||
`opt3 > 2 × opt2`. The default-calibration model predicts ≈4×;
|
||||
the gate is the loose 2× bound so the test does not break when
|
||||
calibration is moved (e.g., when a HW reference replaces the
|
||||
default). Informative numbers — see ADR-0065 §verification and
|
||||
DDD-0065 §11 for the model expectation.
|
||||
3. **Override path.** Topology yaml `pe_cost_model:` block changes the
|
||||
per-PE dispatch cost; default is recovered when block is missing.
|
||||
The formula identity from #1 must hold with the override values.
|
||||
4. **`PeCpuOverheadCmd` bypass.** Manual `tl.cycles(n)` issues exactly `n`
|
||||
cycles, not `n + dispatch_cycles(...)`.
|
||||
5. **No double-count.** PE_DMA `drain_ns`, PE_GEMM/MATH `_compute_ns`
|
||||
identical to pre-ADR values.
|
||||
6. **Determinism.** Identical inputs → identical op_log + latency
|
||||
(SPEC §0.1).
|
||||
7. **Path parity.** Greenlet and replay paths produce identical
|
||||
dispatch-cycle accounting for the same kernel.
|
||||
8. **Composite size cap (D7).** A recipe that would emit `logical_bytes
|
||||
> MAX_COMPOSITE_LOGICAL_BYTES` is segmented into N consecutive
|
||||
`CompositeCmd`s; `sum(segment.logical_bytes) == original
|
||||
logical_bytes`; total dispatch = sum of segment dispatches (FIXED
|
||||
paid per segment); inter-segment ordering preserved by strict FIFO
|
||||
(not by `rw_handles` alone).
|
||||
9. **Sensitivity (qualitative).** At `R ∈ {0.25, 0.0625, 0.03125}`
|
||||
cycles/byte, `opt3 > opt2` at *all* three points. Direction (ratio
|
||||
monotonically increases as R decreases) is also asserted, but
|
||||
absolute ratio values are *not* required.
|
||||
|
||||
## Migration
|
||||
|
||||
ADR-0064 Revision 2 lands as a single PR with:
|
||||
- `logical_bytes` property on each `PeCommand` dataclass (type-aware
|
||||
extra-field counting per D2)
|
||||
- formula application in `pe_cpu.py` dispatch path
|
||||
- `pe_cost_model:` override read at PE_CPU init (cycle-domain knobs +
|
||||
`max_composite_logical_bytes`)
|
||||
- **composite size cap (D7)** — TLContext-side segmentation logic with
|
||||
`MAX_COMPOSITE_LOGICAL_BYTES` default 1024; not needed for any
|
||||
existing bench (largest current composite is well under 200 bytes),
|
||||
but the mechanism lands so it is in place when ADR-0065's
|
||||
10-op decode-opt2 composite (~310 bytes) and larger recipes appear.
|
||||
- one-time goldens regeneration
|
||||
|
||||
After this lands, ADR-0065 builds on top with no further goldens churn
|
||||
in existing benches (ADR-0065 is a meaning-preserving refactor of
|
||||
`CompositeCmd` for the existing path; only opt2 is a new bench).
|
||||
@@ -372,7 +372,16 @@ scratch 에서 읽는 형태 — 순환 tile-loop 의존성. **기각 (incorrect
|
||||
|
||||
## Migration
|
||||
|
||||
Land 순서:
|
||||
> **구현 순서 노트.** 아래의 ADR-0064-우선 순서가 원래 계획이었음.
|
||||
> 실제 구현은 **1↔2 단계를 뒤집어** — ADR-0065 Phase 1 (평평화
|
||||
> `CompositeCmd`) 이 ADR-0064 Rev2 *보다 먼저* land. 이유: `logical_bytes`
|
||||
> (ADR-0064 D2) 가 평평화 형태 위에서 자연스럽게 정의되므로, refactor 를
|
||||
> 먼저 하면 버려지는 transitional `logical_bytes` 를 피함. 각 단계는
|
||||
> 독립적으로 안전: P1 은 기존 cost 경로 하에서 의미 보존 refactor
|
||||
> (op_log byte-equal); ADR-0064 Rev2 가 이미 평평화된 형태 위에 구조적
|
||||
> 공식을 교체. 최종 상태는 어느 쪽이든 동일.
|
||||
|
||||
Land 순서 (원래 계획; as-built 1↔2 뒤집힘은 위 노트 참조):
|
||||
1. **ADR-0064 Revision 2** (별도 PR): 구조적 dispatch cost +
|
||||
`logical_bytes` property + topology config override + 일회성 골든 재생성.
|
||||
2. **ADR-0065 Phase 1** (본 ADR, PR a): `CompositeCmd` 평평화 refactor +
|
||||
|
||||
@@ -413,7 +413,17 @@ input from #1's epilogue scratch — circular tile-loop dependency.
|
||||
|
||||
## Migration
|
||||
|
||||
Land order:
|
||||
> **Implementation ordering note.** The ADR-0064-first ordering below was
|
||||
> the original plan. The actual implementation **reversed steps 1 and 2** —
|
||||
> ADR-0065 Phase 1 (flat-ops `CompositeCmd`) landed *before* ADR-0064 Rev2.
|
||||
> Reason: `logical_bytes` (ADR-0064 D2) is naturally defined on the flat-ops
|
||||
> shape, so doing the refactor first avoids a throwaway transitional
|
||||
> `logical_bytes`. Each step is independently safe: P1 is a meaning-
|
||||
> preserving refactor (op_log byte-equal) under the pre-existing cost path;
|
||||
> ADR-0064 Rev2 then swaps in the structural formula on the already-flat
|
||||
> shape. The end state is identical either way.
|
||||
|
||||
Land order (original plan; see note above for the as-built reversal of 1↔2):
|
||||
1. **ADR-0064 Revision 2** (separate PR): structural dispatch cost +
|
||||
`logical_bytes` property + topology config override + one-time
|
||||
goldens regeneration.
|
||||
|
||||
Reference in New Issue
Block a user