# ADR-0042: Tile Plan Generators — GEMM/Math 파이프라인 plan 빌더 ## Status Accepted (2026-05-20). 본 ADR은 `tiling.py`가 SimPy 컴포넌트가 아니라 **plan-generator 모듈**임을 명시한다. ADR-0014 (PE Pipeline Execution Model) 의 D6 (tile plan / self-routing) 가 tile-plan 생성 알고리즘을 직접 정의하지 않으므로, 본 ADR이 그 비어 있는 자리를 채운다. ## First action (제일 처음에 하는 일) `generate_gemm_plan(M, K, N, tile_m, tile_k, tile_n, ..., pe_prefix, a_pinned, b_pinned, epilogue_specs)`이 호출되면 가장 먼저 하는 일은 **타일 수 계산과 컴포넌트 ID 문자열 구성**이다: ``` M_tiles = max(1, ceil(M / tile_m)) K_tiles = max(1, ceil(K / tile_k)) N_tiles = max(1, ceil(N / tile_n)) dma_id = f"{pe_prefix}.pe_dma" fetch_id = f"{pe_prefix}.pe_fetch_store" gemm_id = f"{pe_prefix}.pe_gemm" math_id = f"{pe_prefix}.pe_math" ``` 즉 **plan generator의 첫 일은 "타일 개수를 ceiling으로 산출하고, 이 PE의 sub-component ID 4개를 한 번에 짜놓는 것"**이다. SimPy 이벤트나 환경 객체는 일절 다루지 않는다 — 이 모듈은 순수 함수다. `generate_math_plan(M, N, tile_m, tile_n, ..., math_op, src_addr, dst_addr, pe_prefix)` 도 마찬가지로 `M_tiles`, `N_tiles` 산출과 component ID 3개 (`dma_id`, `fetch_id`, `math_id`) 구성이 첫 일이다. ## Context ADR-0014 D6은 "PE_SCHEDULER가 CompositeCmd를 받으면 TilePlan을 생성하고 self-routing tile token을 피드한다"고만 합의했다. 그러나 코드에서는 **plan 생성 알고리즘의 구체적 내용**이 `src/kernbench/components/builtin/tiling.py` 모듈에 자리잡고 있고, 이 모듈은: - 컴포넌트가 아니라 **순수 함수**의 모음이다 (`generate_gemm_plan`, `generate_math_plan`). - SimPy 환경, 큐, op_log, hook 등에 의존하지 않는다. - 결과로 `PipelinePlan` (dataclass) 를 돌려준다. 기존 G4 분석은 `tiling.py`를 컴포넌트로 잘못 가정했으나, 실제는 PE_SCHEDULER에 주입되는 plan-builder 함수다. 이 차이는 ADR-0014 의 D6 와 짝을 이루는 별도 ADR로 못 박혀야 한다 — 그렇지 않으면: - "tile plan을 만드는 책임이 PE_SCHEDULER인가 별도 모듈인가" 가 모호. - GEMM plan과 Math plan의 stage sequence 가 일관성 있는지 (예: FETCH/STORE 위치) 의사결정 근거가 흩어진다. - `a_pinned` / `b_pinned` / `epilogue_specs` 같은 옵션이 왜 plan 단에서 분기되는지 근거 없음. ## Decision ### D1. tiling은 순수 plan-generator 모듈이며 컴포넌트가 아니다 `components/builtin/tiling.py`는 ComponentBase 하위 클래스를 정의하지 않는다. 모듈-레벨 함수 두 개만 노출한다: - `generate_gemm_plan(...) -> PipelinePlan` - `generate_math_plan(...) -> PipelinePlan` 토폴로지 그래프에서 `tiling` 이라는 노드는 존재하지 않는다. 명명상 `builtin/` 디렉터리에 있는 이유는 PE_SCHEDULER (ADR-0014 D6) 의 직접 helper이기 때문이며, 의미상으로는 PE_SCHEDULER 내부 utility에 가깝다. ### D2. GEMM plan의 stage 시퀀스 — `M → N → K` order 각 (m, n, k) 타일에 대한 stage 시퀀스 (operand pinning과 epilogue 미적용 기본): ``` [DMA_READ(A)] → [DMA_READ(B)] → FETCH → GEMM ↑ ↓ (last k tile only) [MATH(output_tile)]* → STORE → DMA_WRITE ``` `k_tile` epilogue는 매 K-타일마다 GEMM 직후, `output_tile` epilogue는 (m,n)당 마지막 K-타일에서 STORE/DMA_WRITE 직전에 한 번. K-루프 누적자(accumulator) 는 RegFile에 남아 K 타일들 사이에 STORE/DMA_WRITE가 발생하지 않는다 (last_k에서만 출력). ### D3. Operand pinning — `a_pinned` / `b_pinned` 호출자가 `a_pinned=True`로 호출하면 **모든 (m, n, k) 타일에서 A DMA_READ를 생략**한다. 의미: 호출자(예: `tl.composite`)가 사전에 `tl.load`로 A 전체를 TCM에 한 번 적재했음을 plan generator에 알리는 신호. 이 분기는 plan 단에서 결정한다 (런타임 분기 아님). 따라서 op_log 상의 stage record 수는 pinning에 따라 결정적으로 달라지며, sweep 분석 측 (예: gemm_sweep 의 stage record count) 이 이 결정을 그대로 본다. ### D4. Epilogue scope — `k_tile` vs `output_tile` `epilogue_specs`는 op-spec 객체의 iterable이다. 각 op 객체는 다음 속성을 갖는 다고 가정한다: - `op.kind: str` — math op 이름 (예: `"dequant"`, `"bias"`, `"relu"`, `"scale"`). stage의 `params["op_kind"]` 로 들어간다. - `op.scope: Scope` — `Scope.K_TILE` 또는 `Scope.OUTPUT_TILE` (`Scope` 는 `kernbench.common.pe_commands` 에 정의된 enum). - op-별 추가 필드 (예: `bias`, `scale`, `factor`) — 현재 plan generator는 사용 하지 않으며 런타임 (PE_MATH) 측이 소비. plan generator는 `getattr(o, "scope", None)` 기준으로 두 그룹으로 분기: - `scope == Scope.K_TILE`: 매 K-타일 GEMM 직후 MATH stage 추가. - `scope == Scope.OUTPUT_TILE`: (m, n)당 마지막 K-타일 STORE 직전 MATH stage 추가. `scope` 속성이 없거나 두 enum 어느 쪽도 아닌 op는 **plan에 포함되지 않는다** (`getattr(..., None) == Scope.X` 가 둘 다 False). 기본값(`output_tile`) 채택은 **호출자(예: `tl.composite`) 측 책임**이며, plan generator는 이미 채워진 scope 값을 보고 분기할 뿐이다 (ADR-0014 의 composite epilogue 계약과 정렬). `Scope` 임포트는 `pe_commands ← pe_types ← tiling` 의 순환 참조를 피하기 위해 함수 내부에서 lazy import 한다. 이는 의도된 패턴이며 개선 대상이 아니다 (D1의 "tiling은 PE_SCHEDULER의 utility" 관점에서, pe_commands에 대한 컴파일타임 의존 이 없는 편이 모듈 경계를 깔끔히 유지함). ### D5. Math plan의 stage 시퀀스 — `M → N` order 각 (m, n) 타일에 대한 stage 시퀀스: ``` DMA_READ → FETCH → MATH → STORE → DMA_WRITE ``` K 차원이 없으므로 epilogue / accumulator residency 같은 개념은 적용되지 않는다. PE_FETCH_STORE의 register-file 회계는 GEMM plan과 동일한 방식으로 다뤄진다. ### D6. plan은 데이터다 — SimPy 의존성 없음 `PipelinePlan` 은 `pe_types.py`에 정의된 dataclass로, `tiles: list[TilePlan]`을 보유. 각 `TilePlan` 은 `stages: tuple[Stage, ...]` 를 보유. plan 자체는 immutable에 가까운 데이터 구조이며 (Stage 의 `params: dict` 만 mutable), SimPy 객체나 event를 갖지 않는다. 런타임 시점에 PE_SCHEDULER가 plan 의 첫 stage를 보고 `TileToken`을 생성하여 파이프라인에 피드하며, TileToken 이 `plan: TilePlan`, `stage_idx: int`, `params: dict` 를 들고 다닌다. self-routing은 `TileToken.advance()` 가 다음 stage의 `params`를 캐시하는 방식으로 진행된다 (ADR-0014 D6). ### D7. plan generator의 contract — pure, deterministic, idempotent 같은 입력으로 두 번 호출하면 같은 PipelinePlan을 돌려준다 (`TilePlan.stages`의 순서까지 deterministic). 이 contract는 ADR-0014 D6 의 "결정적 tile dispatch 순서" 요구와 정렬된다. 부수효과(SimPy event, file I/O, 글로벌 상태) 없음 — 테스트에서 환경 객체 없이 호출 가능 (`tests/test_pe_pipeline.py`의 일부 케이스가 이 방식 사용). ## Alternatives Considered ### A1. tiling을 컴포넌트로 만들기 (e.g., PE_PLANNER) 기각. plan 생성은 SimPy 시간을 소비하지 않는 결정 알고리즘이다. 컴포넌트로 만들면 (a) inbox·자원 등 불필요한 인프라가 따라붙고, (b) PE_SCHEDULER 가 "plan 받기" → "tile 피드" 두 단계를 분리해 받게 되어 의미 없는 hop이 생긴다. ### A2. plan 생성을 PE_SCHEDULER 클래스 메서드로 옮기기 기각 (현재). 모듈 분리가 (1) 테스트 용이성, (2) 다른 plan 알고리즘 (예: DTensor-aware plan) 도입 시 추가 함수만 정의하면 되는 확장성을 준다. 만약 향후 plan 종류가 많아져 명시적 dispatch가 필요해지면, 그때 PE_SCHEDULER에 plan factory를 두는 것을 별도 ADR로 도입한다. ### A3. plan을 immutable로 강제 (frozen dataclass + tuple) 부분 채택. `Stage` 와 `TilePlan` 은 dataclass지만 frozen은 아니다. 이유: `Stage.params: dict` 가 plan generator 시점에 채워지고 런타임에서 읽히기만 한다 (TileToken 이 advance 시 캐시할 뿐). 완전 frozen은 dict → frozendict 마이그레이션 비용 대비 이득이 적다. 다만 plan 단계 외에는 mutation 하지 말 것을 컨벤션으로 유지한다. ## Consequences - `tiling.py`가 컴포넌트가 아니라 plan-generator 모듈임이 ADR-level에서 명시되어, G4 같은 미래의 "이 컴포넌트는 ADR이 없다"는 분석을 차단한다. - GEMM plan의 stage sequence (D2) 와 pinning/epilogue 분기 (D3·D4) 가 ADR로 굳어지므로, sweep 분석 (`scripts/gemm_sweep.py`)의 stage record count 해석 근거가 명확해진다. - plan generator의 pure contract (D7) 덕분에 테스트가 환경 없이 plan 검증 가능 — ADR-0013 (verification strategy) 의 "behavior validated by tests with meaningful input cases" 정신과 정렬. - 향후 DTensor-aware plan, K-major plan 등 새 plan 종류 추가 시 본 ADR이 baseline 역할 — 새 함수만 추가하고 D1·D6·D7을 따른다.