# ADR-0051: Routing Helper API — `AddressResolver` + `PathRouter` ## Status Accepted (2026-05-22). `policy/routing/router.py` 가 노출하는 두 helper 클래스 (`AddressResolver`, `PathRouter`) 의 모든 public API, 인자, 반환 값, 그리고 네 가지 다른 adjacency graph 의 사용처를 명시한다. ADR-0002 가 routing distance 와 ordering, bypass 규칙을 정의하나, **helper API 표면 자체** 는 ADR-level 에 정리된 적이 없다. ## First action (제일 처음에 하는 일) ### `AddressResolver(graph)` 생성 즉시 다음 두 가지를 캐시한다: 1. `self._node_ids = set(graph.nodes)` — 모든 node id 의 set (lookup 용). 2. `self._hbm_slice_bytes = hbm_total_gb * (1 << 30) // slices_per_cube` — `graph.spec.cube.memory_map` 으로부터 산출 (기본 `48 GB / 8 slices = 6 GB`). 이 값이 `resolve()` 가 HBM PA 의 `hbm_offset` 에서 `pe_id` 를 복원하는 데 쓰인다. 즉, **AddressResolver 의 첫 일은 "전체 node id 집합과 HBM slice 크기를 미리 계산해 두는 것"** 이다. graph 자체는 보유하지 않는다. ### `PathRouter(graph)` 생성 즉시 **네 개의 별도 adjacency graph 를 동시 구축**한다: 1. `self._adj_all`: 모든 edge 포함 (component-to-component routing 용). 2. `self._adj`: `kind != "command"` 인 edge 만 (PE DMA / 일반 data path). 3. `self._adj_mcpu_dma`: `_MCPU_DMA_EXCLUDE = {"pe_internal", "pe_to_router"}` 를 제외 (M_CPU DMA 가 PE pipeline 노드로 잘못 라우팅 되지 않게). 4. `self._adj_local`: `_UCIE_KINDS` 8 종을 제외 (cube-local routing 용 — UCIe 가 zero-distance bus 처럼 보여 Dijkstra 가 mesh 보다 선호하는 것을 막음). 각 그래프는 `defaultdict(list)` of `(neighbor, weight)` 형태이며, `edge.routing_weight_mm or edge.distance_mm` 이 weight 로 쓰인다. 즉, **PathRouter 의 첫 일은 "topology edge 들을 4개의 다른 정책으로 동시 분류하여 4 개의 인접 리스트로 구축하는 것"**. 매 `find_*()` 호출 시 적절 한 그래프를 골라 Dijkstra 를 돌린다. ## Context `policy/routing/router.py` 는 다음 두 책임을 함께 수행한다: - **이름 매핑**: 토폴로지 명명 규칙 (`sip{S}.cube{C}.`, `sip{S}.io{I}.pcie_ep` 등) 의 단일 소유자. 컴포넌트 / probe / IPCQ install / runtime API 가 이름 문자열을 직접 만들지 않고 helper 를 호출. - **경로 결정**: edge 의 `kind` 에 따른 정책 분리. 같은 src→dst 라도 routing 의도 (PE DMA vs M_CPU DMA vs general component routing) 에 따라 다른 adjacency 를 사용해야 결과가 달라진다. 이 helper API 가 코드 전반에서 광범위하게 소비되는데도 (probe.py / distributed.py / install.py / 각종 component / tests), ADR-level 에서 **정확한 시그너처 / 반환 의미 / 어떤 adjacency 를 쓰는지** 가 한 곳에 정리되어 있지 않다. 본 ADR 이 그 빈자리를 채운다. ## Decision ### D1. `AddressResolver` 의 5 개 public API #### D1.1. `resolve(addr: PhysAddr) -> str` `PhysAddr` 인스턴스를 토폴로지의 destination node id 로 변환. ``` addr.kind == "hbm" → f"sip{s}.cube{d}.hbm_ctrl.pe{pe_id}" where pe_id = addr.hbm_offset // self._hbm_slice_bytes (ADR-0017 D4/D9) addr.kind == "pe_resource": addr.unit_type == PE → f"sip{s}.cube{d}.pe{addr.pe_id}.pe_tcm" addr.unit_type == SRAM → f"sip{s}.cube{d}.sram" addr.unit_type == MCPU → f"sip{s}.cube{d}.m_cpu" 그 외 → RoutingError("unsupported unit_type") 다른 kind → RoutingError("unsupported address kind") ``` 산출된 node id 가 `self._node_ids` 에 없으면 `RoutingError(f"node {node_id} not found in topology")`. 즉, address 의 syntax 가 valid 해도 topology 에 실제로 매핑되는 노드가 없으면 fail-loud. #### D1.2. `find_m_cpu(sip, cube) -> str` `f"sip{sip}.cube{cube}.m_cpu"`. 없으면 `RoutingError`. #### D1.3. `find_pcie_ep(sip, io_id="io0") -> str` `f"sip{sip}.{io_id}.pcie_ep"`. 없으면 `RoutingError`. #### D1.4. `find_io_cpu(sip, io_id="io0") -> str` `f"sip{sip}.{io_id}.io_cpu"`. 없으면 `RoutingError`. #### D1.5. `find_all_pcie_eps() -> list[str]` 전 SIP 의 PCIE_EP node id 를 정렬된 리스트로 반환. `endswith(".pcie_ep")` 필터링. cross-SIP IPCQ 가 모든 PCIE_EP 를 enumerate 할 때 사용. 명명 규칙 (`sip{S}.cube{C}.`, `sip{S}.{io_id}.`) 의 단일 소유자가 이 클래스다 (ADR-0015 D4). 토폴로지 빌더가 같은 명명 규칙으로 노드를 만들고, 컴포넌트는 이름 문자열을 절대 직접 구성하지 않는다 — 모두 helper 를 거친다. ### D2. `PathRouter` 의 4 개 adjacency graph 생성자가 한 번에 구축. edge `kind` 가 정책을 결정: | graph | 제외 edge kinds | 용도 | |-------------------|-----------------------------------------------|--------------------------------------------| | `_adj_all` | (none) | M_CPU↔NOC command 포함, IO_CPU/M_CPU routing | | `_adj` | `"command"` | PE DMA / 일반 data path | | `_adj_mcpu_dma` | `"pe_internal"`, `"pe_to_router"` | M_CPU DMA (PE pipeline 우회) | | `_adj_local` | `_UCIE_KINDS` (`ucie_internal`, `ucie_conn_to_router`, `router_to_ucie_conn`, `ucie_conn_to_noc`, `noc_to_ucie_conn`, `ucie_mesh`, `io_to_cube`, `cube_to_io`) | same-cube routing (UCIe bus 우회) | 각 그래프는 `dict[node_id, list[(neighbor, weight)]]` 이며, weight 는 `edge.routing_weight_mm or edge.distance_mm`. command edge 의 routing 영향력을 명시적으로 가르고, UCIe 의 "0-distance bus" 가 mesh 보다 선호 되는 것을 막기 위한 `_adj_local` 분리가 ADR-0017 D7 의 cross-PE-slice mesh-distance 요구와 정합. ### D3. `PathRouter` 의 6 개 public API (+ 2 backward-compat) #### D3.1. `find_path(src_pe: str, dst_node: str) -> list[str]` **PE DMA routing**. `src_pe` 는 PE prefix (예: `"sip0.cube0.pe0"`) 이며, 함수가 `.pe_dma` 를 자동으로 prepend 하여 실제 시작 노드를 `"sip0.cube0.pe0.pe_dma"` 로 설정. cube-local 여부 (`_same_cube`) 에 따라 adjacency 선택: - **same-cube** (src 와 dst 가 `sip{S}.cube{C}.` prefix 공유): `_adj_local` 사용. UCIe 우회를 막아 cross-PE-slice 가 mesh 거리를 정확 히 지불 (ADR-0017 D7). - **cross-cube**: `_adj` 사용. UCIe 가 자연스럽게 cross-cube path 의 최적 선택지로 포함됨. #### D3.2. `find_path_with_distance(src_pe, dst_node) -> tuple[list[str], float]` D3.1 과 동일한 adjacency 정책을 사용하나, 결과로 `(path, total_distance)` 를 함께 반환. probe / 분석 도구에서 distance 메트릭이 필요할 때 사용. #### D3.3. `find_mcpu_dma_path(m_cpu_id: str, dst_hbm_id: str) -> list[str]` **M_CPU DMA path**. cube 가 같으면 `_adj_local` (mesh 안에서 마무리), 다르 면 `_adj_all` (UCIe 경유). `_MCPU_DMA_EXCLUDE` 가 PE pipeline 노드를 자동 배제하므로, M_CPU 가 PE 의 내부 stage 를 거쳐 routing 되는 잘못된 경로가 나오지 않는다. #### D3.4. `find_memory_path(src: str, dst: str) -> list[str]` `pcie_ep → io_noc → cube → router mesh → hbm_ctrl` 같은 직접 메모리 경로. `_adj_mcpu_dma` 를 사용하여 `pe_internal` 및 `pe_to_router` edge 를 제외 — host-issued read/write 가 PE pipeline 으로 새지 않게 보장. probe (ADR-0049 D1 의 H2D/D2H case) 에서 직접 호출. #### D3.5. `find_node_path(src: str, dst: str) -> list[str]` 임의의 두 node 사이의 path. **command edge 포함** (`_adj_all` 사용). M_CPU ↔ NOC 같은 command-kind link 를 거쳐야 하는 IoCpuComponent / MCpuComponent 등이 호출. #### D3.6. backward-compat shims - `_dijkstra(start, goal) -> list[str]` — `_run_dijkstra(self._adj, …)` 의 thin wrapper. - `_dijkstra_with_dist(start, goal) -> tuple[list[str], float]` — distance 포함 버전. 언더스코어 prefix 에서 보듯이 내부 API 인 척이지만 기존 테스트가 직접 호출. 새 코드는 D3.1–D3.5 를 사용하고, 이 두 shim 은 deprecation 후보. ### D4. Dijkstra 알고리즘 — single-source shortest path `_run_dijkstra_with_dist(adj, start, goal)`: - `heapq` priority queue. - `best: dict[node, distance]` — 노드별 최단 거리 캐시. - `prev: dict[node, predecessor]` — path reconstruction. - weight 는 `routing_weight_mm or distance_mm`. UCIe 처럼 routing_weight 가 명시되어 distance 와 다른 edge 가 있으므로 weight 분리가 의도된 것. `start == goal` 은 빠른 path `([start], 0.0)` 반환. 도달 불가는 `RoutingError(f"no path from {start} to {goal}")`. 이 알고리즘은 **deterministic** 하다 — 같은 graph + start/goal 이면 같은 경로. 이는 SPEC R1 의 "Routing MUST be deterministic" 요구와 정합. tie- break 는 `heapq` 의 push 순서를 따른다 (Python list 순서가 deterministic). ### D5. helper API 의 단일 소유자 원칙 다음 정보는 오직 router.py 안에서만 결정된다: - 명명 규칙: `sip{S}.cube{C}.`, `sip{S}.{io_id}.`, `sip{S}.cube{C}.hbm_ctrl.pe{pe_id}`. - adjacency 정책: 어떤 edge kind 가 어떤 그래프에 포함되는가. - HBM slice 크기로부터 PE id 복원 방법. - Dijkstra의 weight 결정 (`routing_weight_mm or distance_mm`). 이 단일 소유자 원칙이 깨지면 (예: 컴포넌트가 자체적으로 `f"sip{s}..."` 를 구성하기 시작하면) 명명 규칙 변경 시 영향 범위가 폭발한다. ADR-0015 D4 의 정신과 정렬. ### D6. helper API consumer 의 목록 본 helper 가 노출하는 메소드를 호출하는 곳을 명시 (현재 코퍼스 기준): - `probes/probe.py` (ADR-0049): `find_pcie_ep`, `find_io_cpu`, `find_m_cpu`, `find_node_path`, `find_mcpu_dma_path`, `find_memory_path`, `find_path`, `resolve`. - `runtime_api/distributed.py` (ADR-0047): 간접 (engine 내부 routing). - `ccl/install.py` (ADR-0023): `find_all_pcie_eps`, `resolve`. - `sim_engine/event_log.py`: probe 와 유사하게 `find_pcie_ep`, `find_memory_path`. - `components/builtin/m_cpu.py`, `components/builtin/io_cpu.py`: `find_node_path`, `find_mcpu_dma_path`. - 각종 tests (test_routing.py, test_cross_sip_routing.py 등): D3.1–D3.5 대부분. 새 consumer 가 추가될 때 본 ADR 의 D1/D3 가 그 의도에 맞는 메소드가 이미 있는지 / 새 메소드를 추가해야 하는지 1차 판단의 기준이 된다. ## Alternatives Considered ### A1. 단일 adjacency graph + edge-kind filter 동적 적용 기각. 매 `find_*()` 마다 graph filtering 을 다시 하면 Dijkstra 의 cache locality 와 성능이 떨어진다. 4 개 그래프 동시 구축 (D2) 은 메모리 비용 이 작고 (edge ≤ 수만 건 규모), 호출 시점에 정책 선택이 O(1) 로 결정. ### A2. adjacency 분리를 edge 의 `kind` 가 아닌 별도 metadata 로 기각. edge `kind` 는 이미 topology builder 가 부여하며 (ADR-0015 D4 + ADR-0017), 별도 metadata 를 도입하면 두 시스템이 동기화되어야 하는 중복이 생긴다. ### A3. Dijkstra 대신 BFS + uniform weight 기각. routing_weight_mm 이 edge 별로 다른 (mesh link / UCIe / IO-internal) 현실에서 BFS 는 hop 수 최소화일 뿐 latency / distance 최단을 보장하지 않는다. SPEC R1 + R2 의 결정적·정확한 routing 요구에 어긋남. ### A4. helper API 를 클래스 메서드가 아닌 모듈 함수로 기각. 두 클래스 (`AddressResolver`, `PathRouter`) 가 각각 cache 상태 (`_node_ids`, `_hbm_slice_bytes`, 4 adjacency graphs) 를 보유해야 하며, 같은 graph 인스턴스에 여러 routing 질의가 발생한다. 모듈 함수는 매 호출 시 state 를 다시 만들거나 global 로 두어야 해서 안전성/성능 저하. ## Consequences - 컴포넌트 / probe / IPCQ install / runtime API 가 모두 router.py 의 helper 만 호출하면 명명 규칙 변경 (예: `.io0.` → `.iochiplet0.`) 이 단 한 파일 수정으로 끝남 (D5). - D2 의 4 그래프 분리가 ADR 에 굳어져, 새 edge kind 가 추가될 때 (예: Inter-die UCIe link 의 새 kind) 어느 그래프에 포함시킬지 결정의 명확 한 기준 제공. - D3.1 의 cube-local vs cross-cube 분기 (ADR-0017 D7) 가 명시되어, 향후 routing 동작을 변경하려는 사람이 어느 adjacency 를 건드려야 할지 안다. - D6 의 consumer 목록이 명시되어, helper API 변경 시 PR review 범위가 분명. backward-compat shim (D3.6) 의 deprecation 후보가 식별됨.