# 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 경유 커널 런치 경로)
