22fd0d2b9d
- CLAUDE.md: add ADR Lifecycle subsection (superseded → docs/history/, immutable numbering, no renumber) - ADR-0011: merge ADR-0018 content as "Address Model: LA" section alongside PA / VA; status notes VA model is currently implemented - ADR-0018 / 0029 / 0031: moved to docs/history/ with status updates (0018 merged into 0011, 0029 superseded by 0032, 0031 absorbed into 0001 rev 2) - ADR-0019: rewrite Context as PE-HBM connectivity decision (self-contained, no LA model framing) - ADR-0019/0020/0021/0023/0025/0027: Status Proposed → Accepted (code verified) and prune Implementation Notes / Affected files / Test strategy / "현재 상태" sub-sections describing pre-impl state - ADR-0024/0026: same migration-flavor cleanup; 0026 also drops D6 Migration and D8 docs-update sub-decisions - ADR-0030: status simplified (blocker ADR-0031 now superseded) - SPEC.md: R10 + §0.2 reflect PA / VA / LA model names - ADR-0008/0012/0013: refresh ADR-0011 subtitle in Links 21 files changed, 553 insertions(+), 1290 deletions(-). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
427 lines
17 KiB
Markdown
427 lines
17 KiB
Markdown
# ADR-0021: PE 파이프라인 리팩토링 — 컴포넌트 분리 + Scheduler 기반 라우팅
|
|
|
|
## Status
|
|
|
|
Accepted
|
|
|
|
## Context
|
|
|
|
### 실제 하드웨어 구조
|
|
|
|
```
|
|
HBM ←(DMA)→ TCM ←(Fetch/Store Unit)→ Register File ←→ GEMM/MATH Engine
|
|
```
|
|
|
|
- DMA: HBM ↔ TCM 전송 (fabric 경유, 수십~수백 ns)
|
|
- Fetch/Store Unit: TCM ↔ Register File 전송 (BW 기반, 수 ns)
|
|
- GEMM/MATH Engine: Register File 간 연산 (cycle-accurate)
|
|
- 완료 신호: PE 내부 1-cycle wire signal (done pin assert)
|
|
|
|
---
|
|
|
|
## Decision
|
|
|
|
### D1. 각 블록을 독립 컴포넌트로 분리
|
|
|
|
pe_accel의 내부 블록을 **독립 PeEngineBase 컴포넌트**로 분리한다.
|
|
기존 5개 + Fetch/Store Unit 1개 = 6개 컴포넌트.
|
|
|
|
| 컴포넌트 | 역할 | HW 대응 |
|
|
|----------|------|---------|
|
|
| PE_SCHEDULER | plan 생성, tile 상태 관리, stage 라우팅 | Scheduler/Sequencer |
|
|
| PE_DMA | HBM ↔ TCM (fabric 경유) | DMA Engine |
|
|
| PE_FETCH_STORE | TCM ↔ Register File | Load/Store Unit |
|
|
| PE_GEMM | MAC compute (register only) | MAC Array |
|
|
| PE_MATH | element-wise/reduction (register only) | SIMD/Vector Unit |
|
|
| PE_TCM | BW-serialized scratchpad | SRAM Bank |
|
|
|
|
각 컴포넌트는 topology 노드로 존재하며, port/wire로 연결된다.
|
|
`impl`을 교체하면 개별 블록의 타이밍 모델을 변경할 수 있다.
|
|
|
|
### D2. Token Self-Routing — Scheduler는 dispatch + completion만
|
|
|
|
**컴포넌트가 매 stage마다 scheduler를 경유하지 않는다.**
|
|
Token이 plan을 가지고 있어 컴포넌트가 직접 다음 stage로 체이닝한다.
|
|
|
|
```
|
|
Scheduler → DMA → Fetch → GEMM → Math → Store → DMA_WB → (done) → Scheduler
|
|
↑ 체이닝: scheduler 안 거침 completion만
|
|
```
|
|
|
|
이는 실제 HW에서 각 블록의 done signal이 다음 블록에 직접 wire로 연결되어
|
|
있는 구조와 일치한다. Scheduler는 **초기 dispatch + completion aggregation만** 담당.
|
|
|
|
#### Stage 정의
|
|
|
|
```python
|
|
class StageType(Enum):
|
|
DMA_READ = 0
|
|
FETCH = 1
|
|
GEMM = 2
|
|
MATH = 3
|
|
STORE = 4
|
|
DMA_WRITE = 5
|
|
```
|
|
|
|
#### Plan 구조
|
|
|
|
Scheduler가 CompositeCmd를 받으면 **tile 단위 실행 plan**을 생성한다.
|
|
Plan은 각 tile의 **stage sequence**를 정의한다:
|
|
|
|
```python
|
|
@dataclass
|
|
class Stage:
|
|
stage_type: StageType
|
|
component: str # topology 노드 ID (e.g. "sip0.cube0.pe0.pe_dma")
|
|
params: dict # stage별 파라미터 (dynamic)
|
|
|
|
@dataclass(frozen=True)
|
|
class TilePlan:
|
|
tile_id: int
|
|
stages: tuple[Stage, ...] # 순서대로 실행할 stage 목록 (immutable)
|
|
```
|
|
|
|
Plan에 따라 stage sequence가 달라진다:
|
|
|
|
```python
|
|
# 일반 GEMM: HBM → TCM → Register → Compute → Register → TCM → HBM
|
|
stages = (DMA_READ, FETCH, GEMM, STORE, DMA_WRITE)
|
|
|
|
# TCM 데이터로 바로 GEMM (DMA read 생략):
|
|
stages = (FETCH, GEMM, STORE, DMA_WRITE)
|
|
|
|
# MATH element-wise:
|
|
stages = (DMA_READ, FETCH, MATH, STORE, DMA_WRITE)
|
|
|
|
# GEMM + accumulation (중간 K-tile, writeback 생략):
|
|
stages = (DMA_READ, FETCH, GEMM, STORE) # store to TCM only
|
|
```
|
|
|
|
**컴포넌트는 다음 컴포넌트를 하드코딩하지 않는다.**
|
|
Token의 plan에서 다음 stage를 읽고, out_port로 직접 전달한다.
|
|
네트워크 패킷이 라우팅 헤더를 가지고 있는 것과 같은 패턴이다.
|
|
|
|
#### Pipeline Context
|
|
|
|
```python
|
|
@dataclass
|
|
class PipelineContext:
|
|
id: str
|
|
total_tiles: int
|
|
completed_tiles: int = 0
|
|
done_event: simpy.Event = None # 모든 tile 완료 시 succeed
|
|
|
|
def complete_tile(self) -> None:
|
|
self.completed_tiles += 1
|
|
if self.completed_tiles == self.total_tiles:
|
|
self.done_event.succeed()
|
|
```
|
|
|
|
**Completion은 exactly-once contract**: 각 tile의 마지막 stage는 정확히 한 번만
|
|
`complete_tile()`을 호출해야 한다. 중복 호출은 버그이며, `done_event`는
|
|
단 한 번만 succeed되어야 한다 (SimPy Event 제약).
|
|
|
|
#### Scheduler 역할 (축소됨)
|
|
|
|
Scheduler는 CompositeCmd를 받으면 plan과 PipelineContext를 생성한 뒤,
|
|
이를 scheduler 내부의 `_pending_feeds` FIFO에 enqueue하고 즉시 리턴한다.
|
|
|
|
실제 tile 투입은 **단일 feeder process** (`_feed_loop`)가 담당한다.
|
|
이 feeder는 `_pending_feeds`를 FIFO 순서로 소비하며,
|
|
**composite command 간 tile feed interleaving은 허용하지 않는다.**
|
|
즉, 한 command의 모든 tile이 첫 stage queue에 투입된 후에만
|
|
다음 command의 feed가 시작된다.
|
|
|
|
Scheduler당 `_feed_loop`는 **정확히 하나만** 존재하며,
|
|
composite command의 tile feed는 이 단일 process를 통해서만 수행된다.
|
|
Command issue order는 **PE_SCHEDULER가 PeInternalTxn을 수신한 순서**를 의미한다.
|
|
|
|
이 구조는 command issue order를 유지하면서도, 첫 stage queue full 시
|
|
feeder process만 block되고 scheduler worker의 inbox 처리 자체는 멈추지 않도록 한다.
|
|
|
|
```python
|
|
class PeSchedulerV2(PeEngineBase):
|
|
_pipelines: dict[str, PipelineContext]
|
|
_pending_feeds: simpy.Store # FIFO of (plan, ctx)
|
|
|
|
def start(self, env):
|
|
super().start(env)
|
|
self._pending_feeds = simpy.Store(env)
|
|
env.process(self._feed_loop(env))
|
|
|
|
def _dispatch_composite(self, env, pe_txn, cmd):
|
|
plan = generate_plan(cmd)
|
|
ctx = PipelineContext(
|
|
id=next_id(),
|
|
total_tiles=len(plan.tiles),
|
|
done_event=pe_txn.done,
|
|
)
|
|
self._pipelines[ctx.id] = ctx
|
|
|
|
# feeder queue에 등록만 하고 즉시 리턴
|
|
yield self._pending_feeds.put((plan, ctx))
|
|
|
|
def _feed_loop(self, env):
|
|
"""단일 feeder process: composite command를 FIFO 순서로 feed.
|
|
|
|
Composite command 간 tile feed interleaving은 허용하지 않는다.
|
|
한 command의 모든 tile이 첫 stage queue에 투입된 후에만
|
|
다음 command의 feed가 시작된다.
|
|
|
|
첫 stage queue full 시 이 feeder만 block되며,
|
|
scheduler worker의 inbox 처리는 멈추지 않는다.
|
|
"""
|
|
while True:
|
|
plan, ctx = yield self._pending_feeds.get()
|
|
for tile in plan.tiles:
|
|
token = TileToken(
|
|
tile_id=tile.tile_id,
|
|
pipeline_ctx=ctx,
|
|
plan=tile,
|
|
stage_idx=0,
|
|
params=tile.stages[0].params,
|
|
)
|
|
yield self.out_ports[tile.stages[0].component].put(token)
|
|
# queue capacity = HW queue depth → full이면 feeder만 block
|
|
```
|
|
|
|
본 ADR에서 scheduler는 여러 composite command를 수용할 수 있으나,
|
|
tile submission order는 command 단위 FIFO를 따른다.
|
|
Command 내부에서는 tile-level pipeline overlap을 허용하지만,
|
|
command 간 tile feed interleaving은 허용하지 않는다.
|
|
|
|
### D3. 데이터 전달 vs 완료 신호 — HW 모델링 기준
|
|
|
|
| 통신 유형 | 방식 | HW 대응 |
|
|
|----------|------|---------|
|
|
| tile token (작업 지시) | message via out_port | command queue에 enqueue |
|
|
| stage 완료 → 다음 stage | 컴포넌트가 직접 out_port.put | done-triggered local enqueue |
|
|
| pipeline 완료 → scheduler | PipelineContext.complete_tile() | completion interrupt |
|
|
|
|
**Tile token**: out_port.put() 사용. SimPy Store capacity = HW queue depth.
|
|
|
|
**Intra-PE chaining latency**: 본 ADR 범위에서는 intra-PE stage trigger에
|
|
explicit latency model을 두지 않는다. 컴포넌트 간 체이닝은 PE 내부 wire에 해당하며,
|
|
scheduler 왕복이 없으므로 artificial hop cost가 발생하지 않는다.
|
|
|
|
**Pipeline 완료**: 마지막 stage의 컴포넌트가 `pipeline_ctx.complete_tile()` 호출.
|
|
모든 tile 완료 시 PipelineContext가 done_event.succeed().
|
|
|
|
### D4. 비동기 파이프라인 — 자연스러운 overlap
|
|
|
|
Scheduler는 CompositeCmd를 **비동기로** 처리한다.
|
|
다만 tile feed는 command마다 독립 process를 만들지 않고,
|
|
scheduler 내부의 **단일 feeder process**가 FIFO 순서로 수행한다.
|
|
따라서 scheduler는 다음 command를 계속 받을 수 있지만,
|
|
첫-stage tile 투입 순서는 command 단위로 보장된다.
|
|
|
|
**SimPy Store capacity = HW queue depth**이므로:
|
|
- queue가 차면 put()이 자연스럽게 block (backpressure)
|
|
- DMA가 tile 0을 처리하는 동안 GEMM은 이미 완료된 tile의 fetch를 시작
|
|
- 두 번째 CompositeCmd가 들어오면 DMA queue에 바로 이어서 투입
|
|
|
|
```
|
|
First-stage feed order (feeder → DMA queue):
|
|
[cmd1:t0][cmd1:t1][cmd1:t2]...[cmd1:tN] | [cmd2:t0][cmd2:t1]...
|
|
↑ cmd1 feed 완료 후 cmd2 시작
|
|
|
|
Runtime pipeline (downstream overlap):
|
|
PE_DMA: [cmd1:t0][cmd1:t1][cmd1:t2]...[cmd1:tN][cmd2:t0][cmd2:t1]...
|
|
PE_FETCH: [cmd1:t0][cmd1:t1]...
|
|
PE_GEMM: [cmd1:t0][cmd1:t1]...
|
|
↑ 같은 cmd 내부에서 pipeline overlap
|
|
```
|
|
|
|
이때 overlap은 서로 다른 command의 tile feed interleaving에서 오는 것이 아니라,
|
|
먼저 투입된 command의 tile들이 downstream stage로 진행되는 동안 feeder가
|
|
다음 tile들을 계속 투입하면서 자연스럽게 발생한다.
|
|
|
|
예를 들어 cmd1의 모든 tile이 첫 stage queue에 투입되기 전에는
|
|
cmd2의 tile feed는 시작되지 않는다. 그러나 cmd1.tile0이 이미 GEMM으로
|
|
진행한 상태에서 cmd1.tile1, cmd1.tile2가 DMA/FETCH에 남아 있을 수 있으므로,
|
|
**같은 command 내부에서는 pipeline overlap이 자연스럽게 발생**한다.
|
|
|
|
#### 컴포넌트 체이닝 패턴
|
|
|
|
모든 컴포넌트가 동일한 패턴을 따른다:
|
|
|
|
```python
|
|
def _pipeline_worker(self, env):
|
|
while True:
|
|
token = yield self._inbox.get()
|
|
|
|
# 자기 stage 처리
|
|
yield from self._process(env, token)
|
|
|
|
# 다음 stage로 체이닝 (plan에서 읽음)
|
|
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:
|
|
# 마지막 stage — pipeline completion
|
|
token.pipeline_ctx.complete_tile()
|
|
```
|
|
|
|
### D5. PE_FETCH_STORE — TCM ↔ Register File 전담
|
|
|
|
기존에 GemmBlock과 MathBlock이 각각 TCM read/write를 구현했으나,
|
|
이를 **PE_FETCH_STORE 컴포넌트**로 분리한다.
|
|
|
|
```python
|
|
# PE_FETCH_STORE._process()
|
|
def _process(self, env, token):
|
|
yield self.out_ports[tcm_id].put(TcmRequest(token.params["direction"], ...))
|
|
yield tcm_done
|
|
# 체이닝은 base class가 처리 (D4 패턴)
|
|
```
|
|
|
|
장점:
|
|
- GEMM/MATH는 **순수 compute만** — TCM 접근 로직 없음
|
|
- fetch/store BW 경합이 자연스럽게 모델링됨 (PE_TCM의 resource로 serialization)
|
|
- prefetch 전략 등 fetch unit 단독 교체로 실험 가능
|
|
|
|
### D6. 각 Compute 컴포넌트의 단순화
|
|
|
|
GEMM/MATH는 register 데이터가 이미 준비된 상태에서 compute만 수행.
|
|
**체이닝은 공통 패턴(D4)을 따르므로, _process()만 구현하면 된다:**
|
|
|
|
```python
|
|
# PE_GEMM._process()
|
|
def _process(self, env, token):
|
|
yield env.timeout(self._mac_latency(token.params))
|
|
|
|
# PE_MATH._process()
|
|
def _process(self, env, token):
|
|
yield env.timeout(self._simd_latency(token.params))
|
|
|
|
# PE_FETCH_STORE._process()
|
|
def _process(self, env, token):
|
|
yield self.out_ports[tcm_id].put(TcmRequest(token.params["direction"], ...))
|
|
yield tcm_done
|
|
|
|
# PE_DMA._process()
|
|
def _process(self, env, token):
|
|
yield from self._do_fabric_dma(token.params)
|
|
```
|
|
|
|
타이밍 모델만 교체하면 cycle-accurate든 analytical든 자유롭게 변경 가능.
|
|
체이닝 로직은 base class에 있으므로 각 컴포넌트는 순수 stage 로직만 구현.
|
|
|
|
### D7. Topology 변경
|
|
|
|
PE template에 PE_FETCH_STORE 추가:
|
|
|
|
```yaml
|
|
pe_template:
|
|
components:
|
|
pe_cpu: { kind: pe_cpu, impl: pe_cpu_v1, ... }
|
|
pe_scheduler: { kind: pe_scheduler, impl: pe_scheduler_v2, ... }
|
|
pe_dma: { kind: pe_dma, impl: pe_dma_v1, ... }
|
|
pe_fetch_store: { kind: pe_fetch_store, impl: pe_fetch_store_v1, ... }
|
|
pe_gemm: { kind: pe_gemm, impl: pe_gemm_v1, ... }
|
|
pe_math: { kind: pe_math, impl: pe_math_v1, ... }
|
|
pe_mmu: { kind: pe_mmu, impl: pe_mmu_v1, ... }
|
|
pe_tcm: { kind: pe_tcm, impl: pe_tcm_v1, ... }
|
|
links:
|
|
# 기존 links...
|
|
fetch_store_to_tcm_bw_gbs: 512.0
|
|
fetch_store_to_tcm_mm: 0.0
|
|
```
|
|
|
|
PE 내부 edge 연결:
|
|
```
|
|
PE_SCHEDULER → PE_DMA (초기 dispatch)
|
|
PE_SCHEDULER → PE_FETCH_STORE (초기 dispatch)
|
|
PE_SCHEDULER → PE_GEMM (초기 dispatch)
|
|
PE_SCHEDULER → PE_MATH (초기 dispatch)
|
|
PE_DMA → PE_FETCH_STORE (체이닝)
|
|
PE_FETCH_STORE → PE_GEMM (체이닝)
|
|
PE_FETCH_STORE → PE_MATH (체이닝)
|
|
PE_GEMM → PE_FETCH_STORE (store 체이닝)
|
|
PE_MATH → PE_FETCH_STORE (store 체이닝)
|
|
PE_FETCH_STORE → PE_DMA (writeback 체이닝)
|
|
PE_FETCH_STORE → PE_TCM (BW 요청)
|
|
```
|
|
|
|
Topology edge는 **control/dispatch visibility + runtime chaining** 양쪽을 포함한다.
|
|
Scheduler → 하위 컴포넌트 edge는 초기 dispatch 경로이며,
|
|
컴포넌트 간 edge는 token self-routing에 의한 runtime chaining 경로이다.
|
|
|
|
### D9. TileToken 메시지 정의
|
|
|
|
컴포넌트 간 tile 작업 전달에 사용하는 메시지.
|
|
Token이 plan과 stage index를 가지고 있어 self-routing이 가능하다.
|
|
|
|
```python
|
|
@dataclass
|
|
class TileToken:
|
|
tile_id: int
|
|
pipeline_ctx: PipelineContext # completion 추적
|
|
plan: TilePlan # 이 tile의 전체 stage sequence (immutable)
|
|
stage_idx: int # 현재 stage index in plan.stages
|
|
params: dict # current stage 파라미터 캐시 (canonical: plan.stages[stage_idx].params)
|
|
data_op: bool = True # op_log 기록 대상 (ADR-0020)
|
|
```
|
|
|
|
TileToken은 한 시점에 **하나의 컴포넌트에 의해서만 소유**되며,
|
|
동시에 여러 컴포넌트에 의해 참조되지 않는다 (single-owner).
|
|
|
|
Token lifecycle:
|
|
1. Scheduler가 stage_idx=0으로 생성, 첫 stage 컴포넌트에 put
|
|
2. 컴포넌트가 _process() 실행 후 stage_idx 증가, 다음 컴포넌트에 put
|
|
3. 마지막 stage 컴포넌트가 pipeline_ctx.complete_tile() 호출
|
|
4. 모든 tile 완료 시 PipelineContext가 done_event.succeed()
|
|
|
|
기존 PeInternalTxn과의 관계:
|
|
- PeInternalTxn: PE_CPU → PE_SCHEDULER 간 command 전달 (기존 유지)
|
|
- TileToken: PE_SCHEDULER → 하위 컴포넌트 간 tile 단위 작업 전달 (신규, self-routing)
|
|
|
|
---
|
|
|
|
## Non-goals
|
|
|
|
- **PE_CPU 변경**: PE_CPU → PE_SCHEDULER 인터페이스는 변경하지 않음
|
|
(PeInternalTxn 기반, ADR-0014 유지)
|
|
- **다중 pipeline 간 자원 경합 모델**: 현재 범위에서는 단일 pipeline의
|
|
정확한 모델링에 집중. 다중 pipeline 간 TCM bank conflict 등은 future work.
|
|
|
|
## Open Questions
|
|
|
|
- **Register File 용량 모델**: fetch unit이 register에 로드할 때 용량 제한을
|
|
모델링할지. 용량은 바이트 단위(register_file_bytes)로 표현하며,
|
|
동시에 보유 가능한 tile 수는 tile 크기에 따라 결정된다.
|
|
용량 초과 시 fetch가 stall되어 자연스러운 backpressure가 발생한다.
|
|
- **Prefetch 전략**: 본 ADR에서는 composite command 간 tile feed interleaving을
|
|
허용하지 않는다. 따라서 overlap은 command 간 선행 투입이 아니라,
|
|
같은 command 내부 tile들의 pipeline progression에서 자연스럽게 발생한다.
|
|
추가적인 prefetch가 필요하면 command 간 투입이 아니라, 같은 command 내부에서의
|
|
tile ordering 또는 fetch/store unit policy 차원에서 검토한다.
|
|
- **PE_DMA coalescing**: tile 단위 DMA는 fragmentation 발생 가능.
|
|
DMA 내부에서 merge/coalesce하되 scheduler는 관여하지 않는 방향.
|
|
- **동기 실행 모드**: 본 ADR에서는 비동기 pipeline을 기본/유일 execution model로
|
|
채택한다. 디버그 또는 validation 목적의 sync mode가 필요하면 future ADR에서 검토.
|
|
- **다중 pipeline 간 TCM bank conflict**: 현재 단일 pipeline 기준.
|
|
다중 pipeline이 동시에 TCM에 접근할 때의 bank conflict 모델은 future work.
|
|
|
|
---
|
|
|
|
## Consequences
|
|
|
|
### 긍정적
|
|
|
|
- 각 블록이 독립 컴포넌트 — 개별 교체 가능 (ADR-0015 준수)
|
|
- topology에서 PE 내부 구조 가시화
|
|
- 컴포넌트가 다음 컴포넌트를 모름 — plan 기반 라우팅으로 유연성 확보
|
|
- DMA와 compute의 자연스러운 파이프라인 overlap (SimPy Store backpressure)
|
|
- HW 모델링 정확도 향상 (done signal = Event, data transfer = message)
|
|
- fetch/store 분리로 TCM BW 경합 정확히 모델링
|
|
|
|
### 부정적
|
|
|
|
- PE 내부 컴포넌트 수 증가 (5 → 6) — topology 노드/edge 증가
|
|
- 컴포넌트 분리로 인해 intra-PE token forwarding이 이전 대비 더 명시적으로 드러남
|
|
|