# ADR-0036: IO_CPU 컴포넌트 모델 ## Status Accepted ## Context IO_CPU는 시뮬레이션 그래프 내부의 IO 칩렛 호스트 대향 엔드포인트이다. PCIE_EP는 런타임 API로부터 호스트 메시지를 수신하여 io_noc를 통해 라우팅한다; 명령을 동반하는 요청(KernelLaunch, MmuMap/Unmap)의 경우 io_noc는 IO_CPU로 전달하며, IO_CPU는 다음을 수행한다: - 요청을 큐브별 M_CPU로 팬아웃. - 큐브별 응답을 단일 호스트 가시 완료로 집계. - 커널 런치의 경우, 타깃이 된 모든 큐브의 모든 PE가 동일한 시뮬레이션 시각에 커널 본체 실행을 시작하도록 전역 `target_start_ns` 배리어를 스탬프함(ADR-0009 D5). Memory R/W 트래픽은 ADR-0015 D4 / ADR-0016 D3에 따라 IO_CPU를 우회한다; 따라서 본 컴포넌트는 정상 동작에서 명령 평면 트래픽만을 처리한다. 본 ADR은 위의 책임을 실현하는 IO_CPU 컴포넌트 구현을 문서화한다. ## Decision ### D1. 역할 IO_CPU는 IO 칩렛의 호스트 대향 엔드포인트이다. 두 가지 주요 책임을 갖는다: 1. **멀티 큐브 팬아웃** — KernelLaunchMsg / MmuMapMsg / MmuUnmapMsg를 큐브별 M_CPU로 분배. 2. **응답 집계** — 큐브별 ResponseMsg를 수집하고, 타깃이 된 모든 큐브가 응답한 후 부모 `txn.done`을 시그널. 세 번째이자 더 좁은 책임은 KernelLaunchMsg에만 적용된다: **`target_start_ns` 전역 배리어 스탬핑**(D3). 본 컴포넌트는 다음을 하지 **않는다**: - 라우팅 결정 — 경로는 라우터에 의해 사전 계산된다(ADR-0002). - 텐서 또는 커널 내부 디코드 — 그러한 관심사는 M_CPU / PE_CPU / 엔진에 속한다. - PE 수준 팬아웃 처리 — M_CPU가 큐브 내에서 팬아웃한다(ADR-0009 D3). - Memory R/W 데이터 경로 처리 — ADR-0015 D4와 ADR-0016 D3에 따라 IO_CPU를 우회한다(`_resolve_cube_targets` 내의 Memory R/W 해석 코드는 방어적 폴백으로만 존재). 호출당(`run()`): 들어오는 Transaction마다 설정된 `overhead_ns`를 한 번 적용한다(D8). ### D2. 정방향 경로 — 멀티 큐브 팬아웃 응답이 아닌 Transaction이 도착하면, 워커는: 1. `run()`을 통해 `overhead_ns`를 지불. 2. `_resolve_cube_targets`를 호출하여 요청으로부터 `(sip, cube)` 타깃 리스트를 도출(D5). 3. 각 타깃에 대해: - `ctx.resolver.find_m_cpu(sip, cube)`를 통해 M_CPU 노드 id를 해석. - `ctx.router.find_node_path(io_cpu, m_cpu)`를 통해 경로를 해석. - `path`가 채워진 큐브별 서브 Transaction을 생성하여 `path[1]` (io_noc의 첫 홉)으로 전달. 4. 집계 상태 등록: `_pending[request_id] = (expected, received=0, parent_done)`. ### D3. KernelLaunch `target_start_ns` 전역 배리어 (ADR-0009 D5) IO_CPU는 `target_start_ns`의 정규 스탬퍼이다. 요청이 `KernelLaunchMsg`일 때, IO_CPU는 타깃이 된 모든 큐브의 모든 PE를 포괄하는 단일 전역 배리어를 계산한다: ```text for (sip, cube) in cube_targets: leg1 = compute_path_latency_ns(io_cpu → m_cpu(sip, cube), nbytes=0) for pe_id in target_pe_ids: leg2 = compute_path_latency_ns(m_cpu → pe_cpu(sip, cube, pe_id), nbytes=0) latency = leg1 + leg2 - io_overhead_ns - m_overhead_ns global_max = max(global_max, latency) target_start_ns = env.now + global_max ``` 이후 요청은 (`dataclasses.replace`를 통해) 교체되어 스탬프된 값이 팬아웃 전반에 전파된다. 두 가지 오버헤드 보정: - `io_overhead_ns`는 차감되는데, IO_CPU가 본 메서드 실행 전에 `run()`에서 이미 지불했기 때문이다. - `m_overhead_ns`는 한 번 차감되는데, 경로 레이턴시에서 leg1의 종단점인 동시에 leg2의 시작점으로 두 번 등장하지만 M_CPU는 런타임에 단 한 번만 지불하기 때문이다. 모든 다운스트림 PE_CPU는 커널 본체 실행을 시작하기 전 `target_start_ns` 까지 yield한다; 이를 통해 개별 디스패치 경로가 얼마나 오래 걸렸는지와 무관하게 모든 PE가 동일한 시뮬레이션 시각에 시작한다. ### D4. KernelLaunch 서브 Transaction은 `nbytes=0`을 운반 KernelLaunchMsg의 큐브별 서브 Transaction은 부모 `txn.nbytes`를 무시하고 `nbytes=0`을 강제한다: - 커널 런치는 제어 메시지이다; 데이터 패브릭 수준에서 페이로드 크기는 무관하다. - `nbytes > 0`이면 모든 큐브별 서브 트랜잭션이 io_noc의 공유 first-hop 패브릭 BW를 점유한다. 16개 큐브에서는 이로 인해 팬아웃이 직렬화되어 먼 M_CPU들이 `target_start_ns`를 지나치게 되고 D3 불변식이 깨진다. KernelLaunch가 아닌 서브 Transaction은 `txn.nbytes`를 보존한다(실제 페이로드 크기를 운반하는 방어적 Memory R/W 폴백 경로에만 관련됨). ### D5. 요청 타입별 큐브 타깃 해석 `_resolve_cube_targets`는 요청 타입에 따라 디스패치한다: | 요청 타입 | `(sip, cube)`의 출처 | `target_cubes="all"` 의미 | | --- | --- | --- | | `MemoryWriteMsg` | `dst_sip`, `dst_cube` (또는 `PhysAddr.decode(dst_pa).die_id` 폴백) | PA 디코드로 도출되는 단일 큐브 | | `MemoryReadMsg` | `src_sip`, `src_cube` (또는 `PhysAddr.decode(src_pa).die_id` 폴백) | PA 디코드로 도출되는 단일 큐브 | | `KernelLaunchMsg` | `shard.sip == my_sip`으로 필터링된 텐서 샤드 | 이 SIP 위에서 샤드를 소유하는 모든 큐브 | | `MmuMapMsg` / `MmuUnmapMsg` | 본 SIP로 필터링된 `target_cubes` 리스트 | 스펙으로부터 `range(cubes_per_sip)` | 각 IO_CPU 인스턴스는 자기 SIP 내에서만 팬아웃한다 — `_my_sip()`이 노드 id에서 SIP id를 파싱한다(예: `sip0.io0.io_cpu` → 0). Memory R/W 행은 방어적 완전성을 위해 존재한다; 엔진의 정상 경로는 Memory R/W를 `_process_memory_direct()` / `find_memory_path()`로 라우팅하여 IO_CPU를 완전히 우회한다(ADR-0015 D4 / ADR-0016 D3). ### D6. 응답 집계 `_pending: dict[request_id → (expected, received, parent_done)]`: - 디스패치 시: `(len(cube_targets), 0, txn.done)`을 등록. - `_worker`는 `is_response=True`로 응답을 인식하여 `_collect_response`로 라우팅한다. - `_collect_response`는 `received`를 증가시키며, `received >= expected`가 되면 `parent_done.succeed()`를 호출하고 엔트리를 `_pending`에서 제거한다. 이는 단순한 요청별 카운터이다. 큐브별 정체성 추적이나 부분 실패 처리는 없다 — 누락된 응답은 부모 done을 무기한 스톨시킨다. 프로덕션 스타일의 실패 경로는 현재 시뮬레이터 모델의 범위 밖이다. ### D7. `target_pe` 해석 헬퍼 `_resolve_pe_ids(target_pe)`: - `int` → `[target_pe]`. - `tuple[int, ...]` → `list(target_pe)`. - `"all"` → `range(n_slices)`, 여기서 `n_slices`는 큐브 `memory_map.hbm_slices_per_cube`(기본 8)에서 가져온다. D3의 배리어 계산에서 큐브별로 모든 PE 타깃을 열거하는 데 사용된다. ### D8. 설정 가능한 `overhead_ns` 단일 속성이 인스턴스별 레이턴시를 결정한다: | 사이트 | impl 이름 | overhead_ns | | --- | --- | --- | | IO 칩렛 `io_cpu` | `builtin.io_cpu` | 10.0 | Transaction마다 `run()`에서 한 번 적용된다. IO_CPU에서의 명령 해석 및 디스패치 결정 시간을 모델링한다. ## Consequences ### Positive - 크로스 큐브 및 크로스 SIP 커널 런치가 단일 전역 배리어를 공유한다 (D3 + D4) — 시작 시각의 큐브별 분기가 없다. - nbytes=0 불변식이 팬아웃을 공유 first-hop 패브릭 BW로부터 떼어내, 대규모(16 큐브)에서도 배리어의 정확도를 보존한다. - 단일 카운터를 통한 응답 집계 → 최소 상태, 결정론적 완료 순서. - SIP별 스코핑(`_my_sip()`)이 서로 다른 SIP의 IO_CPU들을 깨끗이 독립시킨다. ### Negative - 부분 실패 의미가 없음 — 누락된 큐브별 응답은 부모를 무기한 스톨시킨다. 시뮬레이션 용도로는 충분하나 프로덕션 스타일의 엔드포인트로는 적합하지 않다. - `_pending`은 일반 dict이다; in-flight 요청이 상태로 누적된다. 현재 벤치마크 워크로드(미해결 런치가 적음)에는 허용 가능하나, 원리적으로는 무한하다. - `_resolve_cube_targets`의 Memory R/W 해석 분기는 정상 엔진 경로에서 데드 코드이다. 방어적으로 남겨두었으나 우회 경로가 변경되면 드리프트 위험을 초래한다. ## Links - ADR-0002 (라우팅 거리 — 경로 계산) - ADR-0009 D1 (커널 런치는 IO_CPU에 대한 엔드포인트 요청) - ADR-0009 D3 (M_CPU는 큐브 내에서 팬아웃; IO_CPU는 큐브 사이에서 팬아웃) - ADR-0009 D5 (IO_CPU에서의 target_start_ns 정규 스탬핑) - ADR-0011 D-VA3 (MmuMapMsg가 큐브 팬아웃을 위해 IO_CPU를 경유) - ADR-0012 (호스트 ↔ IO_CPU 메시지 스키마) - ADR-0015 D4 (Memory R/W는 IO_CPU 우회; 커널 런치는 IO_CPU 경유) - ADR-0016 D1 (IO 칩렛 io_noc — IO_CPU가 여기 부착됨) - ADR-0016 D3 (Memory R/W 경로가 IO_CPU 우회) - ADR-0016 D4 (명령 해석을 위한 IO_CPU 경유 커널 런치 경로)