# ADR-0014: PE 파이프라인 실행 모델 ## Status Accepted ## Context 본 ADR은 PE 내부 커널 실행 모델을 정의한다: - PE 내부 컴포넌트의 역할 분담 - 명령 디스패치 경로 (simple / composite / epilogue를 포함한 multi-op composite) - TileToken 기반 자가-라우팅 파이프라인 (스케줄러는 디스패치와 완료 처리만 담당) - 레지스터 파일을 매개로 한 TCM 중심 데이터플로우 - 엔진 자원 모델 - 관측 가능성 및 트레이스 계약 - 토폴로지 표현 PE 내부 구조 (본 ADR 범위 7개 컴포넌트 + 외부 참조 2개): - `pe_cpu`, `pe_scheduler`, `pe_dma`, `pe_fetch_store`, `pe_gemm`, `pe_math`, `pe_tcm` — 본 ADR에서 정의 - `pe_mmu` — VA 모델, ADR-0011 D-VA에서 정의 - `pe_ipcq` — 집합 통신, ADR-0023에서 정의 목표는 결정론적이고 트레이스 친화적인 실행 계약을 통해 각 블록이 독립적으로 교체 가능하도록 유지하는 것이다. ## Decision ### D1. PE 내부 컴포넌트의 역할 **PE_CPU** - 커널 명령어 스트림 / 제어 로직을 실행한다. - PE 명령을 생성하여 `PE_SCHEDULER`에 제출한다 (`PeInternalTxn`을 통해). - 엔진 큐에 직접 작업을 넣지 않는다. **PE_SCHEDULER** - PE 내부의 유일한 디스패처. - `PE_CPU`로부터 명령을 수신한다. 명령 타입별 디스패치: - Simple 명령 (`DmaReadCmd`, `DmaWriteCmd`, `GemmCmd`, `MathCmd`) → 대상 엔진으로 직접 전달. - `CompositeCmd` → `TilePlan`을 생성하고, 단일 `_feed_loop`를 통해 파이프라인에 타일을 공급한다 (D6). - composite 내부의 stage-to-stage 체이닝에는 관여하지 않는다; 이는 토큰 자가-라우팅(D6)으로 처리된다. **PE_DMA** - 큐브 NoC를 통해 TCM과 외부 메모리 도메인(HBM, 공유 SRAM, 큐브 간 UCIe) 사이의 메모리 전송을 처리한다. - 두 개의 실행 채널: - `DMA_READ` (capacity = 1) 및 `DMA_WRITE` (capacity = 1) — D4 참조. - 추가 가상 채널: - `vc_compute` — GEMM/MATH 타일의 load/store/writeback 트래픽. - `vc_comm` — IPCQ 집합 통신 송신 데이터 (ADR-0023 D8에서 정의). **PE_FETCH_STORE** - TCM ↔ 레지스터 파일 전송 유닛. - 레지스터 파일 접근 시맨틱을 컴퓨트 엔진으로부터 격리하여 GEMM/MATH가 순수한 컴퓨트 컴포넌트로 유지되도록 한다. - BW 기반 레이턴시 모델; TCM 접근 경합은 `PE_TCM`의 BW 자원을 통해 자연스럽게 직렬화된다. **PE_GEMM** - MAC 어레이. 레지스터 파일에서 피연산자를 읽고, 결과를 레지스터 파일에 쓴다. `PE_TCM`에 직접 접근하지 않는다. **PE_MATH** - 원소별 / 리덕션 / SIMD 유닛. 레지스터 파일을 읽고 쓴다. **PE_TCM** - BW로 직렬화된 접근을 갖는 tightly-coupled 스크래치패드. 소유권에 따라 두 개의 논리 영역으로 분할된다 (D5 참조). **외부 참조 컴포넌트** (다른 곳에서 정의됨): - `pe_mmu` — 접근마다 VA→PA 변환 (ADR-0011 D-VA). - `pe_ipcq` — 집합 통신 링 버퍼와 피어 엔드포인트 메타데이터 (ADR-0023). ### D2. 명령 생명주기와 큐 `PE_SCHEDULER`는 세 개의 논리적 구조를 유지한다: **SubmissionQueue** — `PE_CPU`가 쓰고, 스케줄러가 소비한다. **InflightTable** — `PE_SCHEDULER`만 소유하고 변경한다; 전개된 sub-command, 의존성 상태, 엔진 할당, 완료 상태를 추적한다. **CompletionQueue** — `PE_SCHEDULER`가 쓴다; 최종 완료 레코드를 보관한다. **Single-writer 규칙**: `PE_SCHEDULER`만이 명령 완료 상태를 변경한다. 엔진은 명시적 이벤트 / 메시지로 완료를 보고하며, 이는 스케줄러가 소비한다. **명령 완료**: 모든 sub-command가 완료되면 `PE_SCHEDULER`가 완료 레코드를 발행한다. ### D3. 디스패치 모드 #### D3.1 Simple 명령 simple 명령은 정확히 하나의 엔진 sub-command로 전개된다: - `DmaReadCmd` / `DmaWriteCmd` → `PE_DMA` - `GemmCmd` → `PE_GEMM` - `MathCmd` → `PE_MATH` 흐름: ```text PE_CPU → SubmissionQueue → PE_SCHEDULER → engine queue → engine execution → completion → PE_SCHEDULER → CompletionQueue ``` #### D3.2 Composite 명령 (단일-op 타일 파이프라인) 기본 `CompositeCmd`는 단일 컴퓨트 op를 타일 파이프라인 시퀀스로 실행한다: ```text DMA_READ → FETCH (TCM → RF) → COMPUTE (GEMM | MATH) → STORE (RF → TCM) → DMA_WRITE ``` `PE_SCHEDULER`는 DMA 페이로드를 하드웨어 타일로 분할하고, 단조 증가하는 `tile_id`를 갖는 `TileToken`을 타일마다 하나씩 발행한다. 타일 의존성 (단일 타일 `t` 내부): ```text DMA_READ(t) → FETCH(t) → COMPUTE(t) → STORE(t) → DMA_WRITE(t) ``` 엔진 자원이 허용하는 한 타일 간 오버랩이 허용된다 (D4가 제약을 규정): ```text DMA_READ(t+1) ∥ COMPUTE(t) DMA_WRITE(t-1) ∥ COMPUTE(t) ``` #### D3.3 Multi-op composite (스코프를 갖는 head + epilogue) `CompositeCmd`는 `ops: tuple[OpSpec, ...]`를 운반하여 multi-op 파이프라인을 표현할 수 있다: ```python @dataclass(frozen=True) class OpSpec: kind: str # "gemm" | "math.exp" | "math.bias_add" | ... scope: Scope # "per_k_tile" | "per_output_tile" | "once" ... ``` - `ops[0]` (head)이 타일 기하 구조를 정의한다 (예: head GEMM이 M/K/N 분할을 결정). - `ops[1:]` (epilogue)는 후속 stage이며 `scope`에 따라 실행 빈도가 결정된다: - `per_k_tile` — 모든 K-리덕션 스텝마다. - `per_output_tile` — 출력 타일당 한 번. - `once` — 커널당 한 번. 크로스-엔진 체인(예: GEMM head → MATH epilogue)은 자연스럽다 — 각 stage는 토큰 자가-라우팅(D6)을 통해 디스패치되므로, GEMM과 MATH는 동일한 컴퓨트 슬롯(D4)을 공유하더라도 동일 composite 내에서 직렬적으로 참여한다. 비어 있는 `ops` 형식은 레거시 단일-op 경로이다. ### D4. 엔진 자원 모델 **DMA 엔진**: - `DMA_READ`: `simpy.Resource(capacity=1)`. - `DMA_WRITE`: `simpy.Resource(capacity=1)`. - 두 채널은 동시에 실행된다 (READ ∥ WRITE 허용). - 채널 내부에서는 요청이 직렬화된다 (READ ∥ READ 불가; WRITE도 동일). - `vc_comm`은 IPCQ 트래픽을 위한 직교 채널로 ADR-0023 D8에서 정의됨 — 본 ADR 범위 밖. **컴퓨트 엔진**: - `accel_slot`: `PE_GEMM`과 `PE_MATH`가 공유하는 `simpy.Resource(capacity=1)`. - PE 내에서 동시에 최대 한 개의 컴퓨트 op만 실행된다. - Multi-op composite 체인(D3.3)은 이 슬롯을 통해 컴퓨트 stage를 직렬로 실행한다; 토큰 자가-라우팅(D6)이 이전 컴퓨트가 슬롯을 해제한 후에만 다음 stage가 시작되도록 보장한다. **엔진 완료**: 각 엔진은 완료 이벤트를 발행하며, 이는 스케줄러 / `PipelineContext`(D6)가 소비한다. ### D5. 데이터플로우 **입력 경로 (HBM 소스)**: ```text HBM → cube NOC → PE_DMA (DMA_READ) → PE_TCM PE_TCM → PE_FETCH_STORE → Register File Register File → PE_GEMM | PE_MATH ``` **입력 경로 (공유 SRAM 소스)**: ```text Shared SRAM → cube NOC → PE_DMA (DMA_READ) → PE_TCM PE_TCM → PE_FETCH_STORE → Register File ``` **출력 경로 (HBM 목적지)**: ```text Register File → PE_FETCH_STORE → PE_TCM PE_TCM → PE_DMA (DMA_WRITE) → cube NOC → HBM ``` GEMM/MATH는 `PE_TCM`에 직접 접근하지 않는다 — `PE_FETCH_STORE`가 TCM↔레지스터 파일의 유일한 게이트웨이이다. 이를 통해 TCM BW 경합이 명시적으로 드러나며, fetch 유닛 정책(예: 프리패치)을 컴퓨트 엔진과 독립적으로 교체할 수 있다. #### D5.1 PE_TCM 분할 `PE_TCM`은 두 개의 논리 영역으로 분할된다: **SchedulerReservedTCM** - `PE_SCHEDULER`가 단독으로 소유한다. - composite 명령의 타일 버퍼를 보관한다. - `PE_SCHEDULER`가 이 영역을 분할하고, DMA_READ / COMPUTE / DMA_WRITE stage마다 버퍼를 할당하며, 입출력 분리를 보장하고, 타일-버퍼 수명을 관리한다. **AllocatableTCM** - `PEMemAllocator`가 관리하는 범용 영역. - 호스트 / DP 가시 할당에 사용된다. **가시성 규칙 (강한 격리)**: `PEMemAllocator`는 `SchedulerReservedTCM`을 보거나 그 내부에 할당해서는 안 된다. 예약 영역은 구성 시점에 할당자가 관리하는 범위에서 제외된다. **타일 버퍼 규칙**: - 타일이 활성 수명 동안 `SchedulerReservedTCM` 내부의 입력 버퍼와 출력 버퍼는 겹쳐서는 안 된다. - 타일 버퍼는 해당 `DMA_WRITE`가 완료될 때까지 유효하다. - 버퍼 재사용은 소비하는 타일의 수명이 끝난 후에만 허용된다. ### D6. TileToken 자가-라우팅 파이프라인 composite의 stage-to-stage 진행은 스케줄러를 거치지 **않고** 일어난다. 각 컴포넌트는 토큰의 `plan`을 사용해 토큰을 다음 stage의 컴포넌트로 직접 전달한다: ```text Scheduler → DMA → Fetch → GEMM → Math (epi) → Store → DMA_WB → (complete) ↑ chaining: no scheduler hop ↑ PipelineContext.complete_tile() ``` 이는 실제 HW의 done-wire 체인을 반영한다. 스케줄러는 **초기 디스패치 + 완료 집계**만 담당한다. #### TilePlan / Stage ```python class StageType(Enum): DMA_READ = 0 FETCH = 1 GEMM = 2 MATH = 3 STORE = 4 DMA_WRITE = 5 @dataclass(frozen=True) class Stage: stage_type: StageType component: str # topology node id (e.g., "sip0.cube0.pe0.pe_dma") params: dict # stage-specific parameters @dataclass(frozen=True) class TilePlan: tile_id: int stages: tuple[Stage, ...] ``` #### TileToken ```python @dataclass class TileToken: tile_id: int pipeline_ctx: PipelineContext plan: TilePlan stage_idx: int params: dict # cached current stage params data_op: bool = True # op_log opt-in (ADR-0020 D4) ``` 단일 소유자 불변식: 토큰은 한 시점에 정확히 한 컴포넌트가 소유한다. 생명주기: 스케줄러가 `stage_idx=0`으로 생성 → 컴포넌트 `_process()` → `stage_idx` 증가 → 다음 stage의 `in_port`에 put → 마지막 stage가 `pipeline_ctx.complete_tile()` 호출. #### PipelineContext (정확히 한 번 완료) ```python @dataclass class PipelineContext: id: str total_tiles: int completed_tiles: int = 0 done_event: simpy.Event = None def complete_tile(self) -> None: self.completed_tiles += 1 if self.completed_tiles == self.total_tiles: self.done_event.succeed() ``` 각 타일의 마지막 stage는 `complete_tile()`을 정확히 한 번 호출해야 한다. 중복 호출은 버그이다 (SimPy `Event`는 최대 한 번만 succeed 가능). #### Feed 순서 `PE_SCHEDULER`는 `_pending_feeds` FIFO를 소비하는 `_feed_loop` 프로세스를 정확히 하나 갖는다. composite 명령은 제출 순서대로 인큐되며, 한 명령의 타일 feed는 다음 명령의 feed가 시작되기 전에 완료까지 실행된다. **명령 간 타일-feed 인터리빙은 허용되지 않는다.** 단일 명령의 타일들 내부에서는 다운스트림 파이프라인 오버랩이 자연스럽게 발생한다 — 이전 타일이 후행 stage를 진행하는 동안 feeder는 남은 타일을 첫 stage 큐로 계속 푸시한다 (SimPy Store 백프레셔가 흐름 제어를 관장한다). 첫 stage 큐가 가득 차면 feeder만 블록되며, 스케줄러 워커의 inbox 처리는 계속된다. #### 토큰 라우팅 패턴 (기본 클래스) ```python def _pipeline_worker(self, env): while True: token = yield self._inbox.get() yield from self._process(env, token) # stage-specific logic next_idx = token.stage_idx + 1 if next_idx < len(token.plan.stages): next_stage = token.plan.stages[next_idx] token.stage_idx = next_idx token.params = next_stage.params yield self.out_ports[next_stage.component].put(token) else: token.pipeline_ctx.complete_tile() ``` 각 컴포넌트는 `_process()`만 구현한다; 체이닝은 기본 클래스에 존재한다. ### D7. 관측 가능성 및 트레이스 계약 시뮬레이터는 결정론적 트레이스 이벤트를 발행한다: - `command_submitted` - `sub_command_dispatched` - `engine_start` - `engine_complete` - `tile_ready` - `command_complete` 동일한 입력에 대해 트레이스 순서는 결정론적이어야 한다. ### D8. 토폴로지 표현 PE 내부 컴포넌트는 `cube.pe_template`에 선언된다: ```yaml pe_template: components: pe_cpu: { kind: pe_cpu, impl: builtin.pe_cpu, attrs: { overhead_ns: ... } } pe_scheduler: { kind: pe_scheduler, impl: builtin.pe_scheduler, attrs: { overhead_ns: ... } } pe_dma: { kind: pe_dma, impl: builtin.pe_dma, attrs: { rd_engines: 1, wr_engines: 1 } } pe_fetch_store: { kind: pe_fetch_store, impl: builtin.pe_fetch_store, attrs: { ... } } pe_gemm: { kind: pe_gemm, impl: builtin.pe_gemm, attrs: { shared_resource: accel_slot, ... } } pe_math: { kind: pe_math, impl: builtin.pe_math, attrs: { shared_resource: accel_slot, ... } } pe_tcm: { kind: pe_tcm, impl: builtin.pe_tcm, attrs: { size_mb: ..., read_bw_gbs: ..., write_bw_gbs: ... } } pe_mmu: { kind: pe_mmu, impl: builtin.pe_mmu, attrs: { ... } } # ADR-0011 D-VA pe_ipcq: { kind: pe_ipcq, impl: builtin.pe_ipcq, attrs: { ... } } # ADR-0023 links: # Scheduler dispatch edges (initial) scheduler_to_dma_mm: 0.0 scheduler_to_fetch_store_mm: 0.0 scheduler_to_gemm_mm: 0.0 scheduler_to_math_mm: 0.0 # Pipeline chaining edges (token self-routing per D6) dma_to_fetch_store_mm: 0.0 fetch_store_to_gemm_mm: 0.0 fetch_store_to_math_mm: 0.0 gemm_to_fetch_store_mm: 0.0 gemm_to_math_mm: 0.0 math_to_fetch_store_mm: 0.0 fetch_store_to_dma_mm: 0.0 fetch_store_to_tcm_bw_gbs: ... ``` 템플릿은 PE마다 한 번 인스턴스화된다. PE 인스턴스는 `cube.pe_layout` (코너 배치)으로부터 파생된다. 외부 연결성(PE_DMA ↔ cube NoC ↔ HBM 등)은 큐브 수준에서 모델링된다 (ADR-0017 D4). ## Consequences ### Positive - 각 블록이 독립적인 토폴로지 노드이다 — DI(ADR-0015)를 통해 개별 교체 가능하다. - PE 내부 구조가 토폴로지 그래프에 가시화된다. - 컴포넌트는 자신의 다운스트림을 알지 못한다 — plan 기반 라우팅이 유연성을 제공한다 (예: epilogue 체인에 스케줄러 변경이 불필요). - DMA와 컴퓨트가 SimPy Store 백프레셔를 통해 자연스럽게 오버랩된다. - Multi-op composite가 융합 연산(예: GEMM + bias_add)을 엔진 수준 결합 없이 표현한다. - TCM 접근 경합이 현실적이다 — `PE_FETCH_STORE`가 TCM↔RF의 유일한 게이트웨이이다. ### Negative - PE 내부 컴포넌트 수가 더 거친 모델보다 많다 (기본 7개 + 외부 참조 2개) — 더 많은 토폴로지 노드/엣지. - PE 내부 토큰 전달이 트레이스에 명시적으로 드러난다 (HW 충실도와의 허용 가능한 trade-off). ## Links - ADR-0011 D-VA (PE_MMU 컴포넌트, VA 변환) - ADR-0015 D4 (컴포넌트 포트/와이어 모델) - ADR-0020 (greenlet 커널 실행 / two-pass) - ADR-0023 (PE_IPCQ + PE_DMA 가상 채널) - SPEC R3, R4