# ADR-0039: PE_MMU Component Model — 컴포넌트 + 유틸리티 이중 역할

## Status

Accepted (2026-05-20).

ADR-0011 (PA/VA/LA address model) 의 VA 모델에서 "PE_MMU가 VA→PA 변환"이라고만
선언되어 있는데, **PE_MMU 컴포넌트 자신의 동작 모델**을 별도로 못 박는 ADR.

## First action (제일 처음에 하는 일)

생성 시점에 `node.attrs["page_size"]` (default `2 MiB`) 와
`node.attrs["tlb_overhead_ns"]` (default `0.0`) 를 읽어 내부 `PeMMU` 객체
(`policy.address.pe_mmu.PeMMU`) 를 단 한 번 인스턴스화한다. 이 객체가 페이지
테이블·서브페이지 region 리스트·TLB 오버헤드의 단일 보유자(single owner)이다.

런타임에서의 첫 동작은 두 갈래로 갈린다:

- **컴포넌트 경로 (inbox 소비)**: `_worker`가 `_inbox`에서 Transaction을 한 건
  꺼내, 그 `request`가 `MmuMapMsg`이면 각 엔트리에 대해
  `self._mmu.map(va, pa, size)`를 호출하고 `txn.done.succeed()`.
  `MmuUnmapMsg`이면 `unmap(va, size)`, 그 외 타입이면 표준 `_forward_txn`으로
  떨군다. 즉 **MMU의 첫 일은 "map/unmap 명령을 페이지 테이블에 반영하는 것"**.
- **유틸리티 경로 (직접 호출)**: PE_DMA / PE_GEMM 같은 동일 PE 내부 엔진이
  `pe_mmu.mmu.translate(va)`를 직접 호출한다. 이 경로에서는 SimPy 이벤트가
  발생하지 않으며, 호출자가 (overhead_ns > 0인 경우) 본인 process에서
  `yield env.timeout(mmu.overhead_ns)`를 처리한다.

## Context

ADR-0011은 PA/VA/LA 세 가지 주소 모델을 정의하고 "VA 모델 = PE_MMU를 통한 변환"
이라고만 합의했다. 그러나 코드 상의 `PeMmuComponent`는 두 가지 상호 보완적인
역할을 동시에 수행한다:

1. **토폴로지 그래프 상의 컴포넌트**: cube NoC에서 `MmuMapMsg` / `MmuUnmapMsg`
   sideband 메시지를 수신하여 페이지 테이블을 갱신한다.
2. **PE-로컬 유틸리티 객체**: 동일 PE의 PE_DMA / PE_GEMM이 latency 0으로 (혹은
   호출자 측에서 `overhead_ns`만 부담하면서) 직접 `translate(va)`를 호출한다.

이 두 역할을 모두 다루는 ADR이 없어 다음 모호함이 발생한다:

- "왜 MMU 변환에 SimPy 이벤트가 안 잡히나?" (실제로는 호출자 측에서 잡고 있음)
- 서브페이지 region 모델은 무엇이고, 왜 그 모델인가? (코드 docstring에는 있으나
  ADR이 없음 — `project_mmu_subpage_stopgap`라는 memory note 참조만 존재)
- map/unmap 메시지가 **누구로부터** 와서 **언제까지** 갱신되어야 하는가
  (ordering 계약)?

또한 `PeMMU.map()` 은 "later append, last-write-wins (역방향 탐색)" 의미를 갖는데,
이것은 단순한 단일-PA 페이지 테이블 모델로는 표현 불가능한 DPPolicy의 서브페이지
샤딩 (예: 128B 페이로드 × 4KB 페이지) 시나리오를 위해 의도적으로 추가된
**stopgap**이다. 진짜 HW MMU와는 다른 단순화임을 ADR로 못 박을 필요가 있다.

## Decision

### D1. 이중 역할의 명시 — 컴포넌트와 유틸리티

`PeMmuComponent`는 단일 클래스 안에서 다음 두 인터페이스를 노출한다:

- 컴포넌트 인터페이스: `_inbox` 소비, `_worker` 루프 (MMU sideband 메시지 처리).
- 유틸리티 인터페이스: `pe_mmu.mmu` 속성으로 underlying `PeMMU` 객체를 노출 —
  PE_DMA / PE_GEMM이 이 객체를 직접 들고 `translate()`를 호출.

후자는 **layer skip이 아니다**: PE 내부는 ADR-0007이 정의한 "components" 레이어
하나 안의 sibling 관계이고, 같은 PE prefix에서 가져온 PE_MMU 객체에 대한 직접
호출은 cross-layer가 아니다. cross-layer 위반은 runtime API / sim_engine /
components 경계를 넘는 경우에만 적용된다.

### D2. Latency 모델: `translate()`는 순수 함수, overhead는 호출자 책임

`PeMMU.translate()`는 순수 함수이며 SimPy yield를 하지 않는다. 호출자(PE 엔진)
가 변환 후 `if self._mmu.overhead_ns > 0: yield env.timeout(self._mmu.overhead_ns)`
를 자기 process에서 발생시킨다.

이유: PE 엔진의 SimPy process는 이미 자체 record_start / record_end (op_log)
hook을 들고 있어 timing을 일관되게 잡을 수 있다. MMU가 별도의 process를 만들면
PE 엔진의 처리 흐름을 두 갈래로 쪼개 op_log/pipeline overlap 의미가 흐려진다.

#### D2.1. 현재 구현의 비대칭 — pipeline vs non-pipeline (Known asymmetry)

본 ADR 작성 시점의 `pe_dma.py` 구현은 두 호출 경로에서 overhead 처리가 다르다:

- **non-pipeline (`handle_command`)**: `translate()` 직후
  `if self._mmu.overhead_ns > 0: yield env.timeout(self._mmu.overhead_ns)` 를
  발생시킨다.
- **pipeline (`_do_pipeline_dma`)**: `translate()` 만 호출하고 overhead timeout을
  **생략**한다 — 함수 주석에 "same logic as non-pipeline path"라고 적혀 있으나
  실제로는 일치하지 않는다.

기본 토폴로지에서 `tlb_overhead_ns = 0.0` 이라 이 차이는 timing에 직접 드러나지
않으나, `tlb_overhead_ns > 0` 으로 설정한 시뮬레이션에서는 pipeline 경로의
GEMM/Math 가 non-pipeline 동일 워크로드 대비 MMU overhead 만큼 빠르게 측정된다.

D2의 계약은 "**모든** 호출자가 overhead를 책임진다" 이며, pipeline 경로의 누락은
**의도된 설계가 아니라 구현 비일관성**이다. ADR-0014 D6 (pipeline self-routing)
이 이 overhead를 면제한다고 명시한 부분은 없다.

조치 선택지(별도 Phase 1/2 제안 필요):

- (a) `_do_pipeline_dma` 에서도 `if mmu.overhead_ns > 0: yield env.timeout(...)`
  를 추가하여 D2 계약과 일치시킨다 — 권장.
- (b) D2 계약을 "non-pipeline 경로에만 적용" 으로 좁히고, pipeline 경로의 면제를
  ADR-0014 D6 갱신과 함께 정당화한다 — overhead 의미가 약해지므로 비권장.

본 ADR은 (a) 를 권장하며, accept 전 또는 직후의 별도 작은 변경으로 이를
교정하는 것을 가정한다.

### D3. 페이지 테이블 구조 — 서브페이지 region 리스트 (stopgap)

`self._table: dict[vpn, list[(start_in_page, end_in_page, pa_at_offset_zero)]]`
구조로 한 페이지 안에 여러 disjoint region을 보유할 수 있다.
- `map(va, pa, size)`: 페이지를 가로지르면 region들을 **append**한다.
- `translate(va)`: VPN으로 region 리스트를 가져온 후, **역방향**으로 순회하며
  처음 매칭되는 region을 채택 (last-write-wins).
- `unmap(va, size)`: extent가 unmap 범위에 **완전히 포함된** region만 제거한다.
  경계가 어긋난 부분 overlap은 그대로 남기며, 매핑 호출자는 mapping과 동일한
  경계로 unmap할 책임을 진다.

이는 진짜 HW MMU와는 다른 **시뮬레이터 stopgap**임을 ADR-0011 VA 모델 보강
요소로 명시한다. DPPolicy 서브페이지 샤딩 시 last-write-wins overwrite로 인한
조용한 미스라우팅을 방지하기 위함이다 (메모리 노트: project_mmu_subpage_stopgap).

### D4. PageFault는 PA fallback 신호다

매핑이 없는 VA로 `translate()`가 호출되면 `PageFault`가 발생한다. PE_DMA는 이
예외를 잡아 **원본 주소를 PA로 그대로 사용**한다 (ADR-0011의 PA fallback 호환
경로). 따라서 PageFault는 에러가 아닌 "VA 매핑 부재 시 PA로 해석한다"는 신호다.

이 호환 경로는 ADR-0011이 합의한 PA-only 모드와의 후방 호환을 유지하기 위한
의도된 동작이다.

### D5. MMU sideband 메시지의 수신 계약

`MmuMapMsg` / `MmuUnmapMsg`는 fabric을 통해 PE_MMU 컴포넌트의 `_inbox`로
도달한다 (R10이 명시하는 "MMU map 설치는 fabric latency를 따른다"). 메시지
schema는 runtime API (`runtime_api/kernel.py`) 가 정의하며, 현재 형식:

- `MmuMapMsg.entries: tuple[dict, ...]` — 각 dict는 `{"va": int, "pa": int,
  "size": int}` 키를 갖는다.
- `MmuUnmapMsg.entries: tuple[dict, ...]` — 각 dict는 `{"va": int, "size": int}`
  키를 갖는다.

PE_MMU 측 수신 처리:

1. `_worker` 가 `_inbox.get()` 에서 메시지 한 건을 꺼낸다.
2. `hasattr(msg, "request")` 로 Transaction wrapper 인지 확인.
3. `isinstance(msg.request, MmuMapMsg)` 이면 각 entry 에 대해
   `self._mmu.map(va=e["va"], pa=e["pa"], size=e["size"])`.
4. `isinstance(msg.request, MmuUnmapMsg)` 이면 각 entry 에 대해
   `self._mmu.unmap(va=e["va"], size=e["size"])`.
5. 둘 다 `msg.done.succeed()` 로 완료 통지.

외부 호출자(runtime API 측)가 `done`을 await하면 "매핑이 디바이스에 설치된
시점"이 SimPy 시간으로 보장된다 — 이 wait이 ADR-0011이 요구하는 "MMU map
installation incurs measured fabric latency" 의 실현이다.

이 ADR은 sideband 메시지의 **sender 와 fan-out 정책**을 정의하지 않는다 —
그것은 runtime API 책임이다. 본 ADR은 PE_MMU 측 수신 계약만 명시한다.

### D6. 비-MMU Transaction은 일반 forwarding으로 위임

`_worker`가 inbox에서 꺼낸 메시지의 `request`가 `MmuMapMsg` / `MmuUnmapMsg`가
아닌 경우 (또는 `request` 속성이 없는 경우) `_forward_txn`으로 떨군다. 이는
미래에 PE_MMU가 cube-internal NOC 상의 통과 노드로 사용될 가능성을 차단하지
않기 위함이다 (현재는 그런 통과 트래픽이 없으나, 토폴로지 변경에 대해 안전).

## Alternatives Considered

### A1. translate()를 SimPy generator로 만들기

기각. D2에서 설명한 대로, PE 엔진의 op_log/pipeline overlap 의미가 흐려진다.
호출자 측에서 timeout을 일으키는 현재 패턴이 op_log 회계와 일치한다.

### A2. 서브페이지 region 리스트 대신 페이지 크기 자체를 작게 하기 (예: 128B)

기각. 페이지 테이블 메모리 폭발과 cube-wide map message 크기 폭발을 초래한다.
DPPolicy 샤딩이 128B를 요구한다 해도 그 외 대다수 매핑은 2MiB 단위이므로,
페이지 크기를 작게 잡는 것은 평균 비용이 비대해진다.

### A3. PE_MMU를 컴포넌트가 아닌 PE_CPU의 내장 헬퍼로만 두기

기각. ADR-0011이 요구하는 "fabric을 통해 측정된 latency로 MMU map 설치"
(MmuMapMsg 경로)를 표현하려면 토폴로지 그래프 상의 노드여야 한다. 또한 cube NoC
visualizer에서 PE_MMU가 노드로 보여야 디버깅·진단이 일관된다.

## Consequences

- PE_MMU의 이중 역할(컴포넌트 + 유틸리티)이 ADR-level에서 정당화되어, 미래의
  refactor 압박 (둘 중 하나로 통일하라)에 대한 논거가 생긴다.
- 서브페이지 region 모델이 시뮬레이터 stopgap임을 ADR이 명시 — 이후 LA 모델
  (ADR-0011) 도입 시 이 stopgap 제거 가능성을 평가하는 기준이 된다.
- `translate()`가 yield하지 않는다는 계약이 ADR로 굳어지므로, 향후 누군가
  "MMU에 자체 timeout을 넣자"는 제안을 할 때 D2를 근거로 거절할 수 있다.
- PA fallback (D4) 이 정상 흐름임이 명시되어, PageFault를 에러로 오인하여
  방어 로직을 추가하는 일을 막는다.
