# 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
