# 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를 에러로 오인하여 방어 로직을 추가하는 일을 막는다.