adr: add ADR-0050-0053 — close /report's second-pass G4 candidates

Documents four cross-cutting surfaces one layer deeper than the prior
G4 batch:

- 0050 par-ccl-algorithm-module-contract: how to author a new CCL
  algorithm in src/kernbench/ccl/algorithms/. Pairs with ADR-0045's
  bench-module contract. Pins the four required public symbols
  (kernel, kernel_args, TOPO_NAME_TO_KIND constants, kernel alias),
  the 9 + tl standardized kernel signature, the kernel_args tuple
  format, sip_topo_kind dispatch, and the ccl.yaml entry workflow.

- 0051 lat-routing-helper-api: every public method of AddressResolver
  (resolve, find_m_cpu, find_pcie_ep, find_io_cpu, find_all_pcie_eps)
  and PathRouter (find_path, find_path_with_distance,
  find_mcpu_dma_path, find_memory_path, find_node_path + 2 shims).
  Pins the four adjacency graphs (_adj_all / _adj / _adj_mcpu_dma /
  _adj_local) and the edge-kind exclusion sets they use, plus the
  single-owner naming convention.

- 0052 dev-oplog-memory-store-schemas: OpRecord's 7 fields, the
  per-op_name params matrix (dma_read, dma_write, gemm_*, math, math
  reduction, composite_gemm, ipcq_copy, unknown), snapshot timing
  rules (math = all inputs, dma_write = HBM-only — ADR-0027 race
  avoidance), TileToken stage_type capture, and MemoryStore's
  (space, addr) two-level dict with reference-store semantics.

- 0053 dev-topology-builder-algorithms: the 6-stage compile pipeline,
  cube_mesh.yaml's source_hash cache and its 5 input fields, the
  cube NoC auto-layout algorithm (row/col placement, HBM exclusion
  zone, PE/M_CPU/SRAM attachment via nearest-router, UCIe N/S/E/W
  distribution), the node naming convention (single-owner with
  router.py), the edge-kind catalog, the 4 view projections, and a
  table of spec-field changes vs mesh regeneration.

Bilingual pair verifier passes for all four EN/KO pairs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-22 10:52:42 -07:00
parent 9a02955770
commit bd49c93703
8 changed files with 2566 additions and 0 deletions
@@ -0,0 +1,308 @@
# ADR-0050: CCL Algorithm Module Contract — `ccl/algorithms/*.py`
## Status
Accepted (2026-05-22).
`src/kernbench/ccl/algorithms/` 디렉터리 안의 모듈이 AHBM CCL backend
(ADR-0047) 에서 collective algorithm 으로 사용되려면 갖춰야 할 인터페이스,
kernel 시그너처, 그리고 새 알고리즘 추가 절차를 명시한다. ADR-0047 D3 가
"algorithm 모듈은 `kernel`, `kernel_args`, optional `TOPO_NAME_TO_KIND`
expose 해야 한다" 라고만 한 줄로 언급하나, **algorithm 모듈 작성자가 따라야
할 contract** 는 ADR-level 에서 정리된 적이 없다. ADR-0045 가 bench 모듈
contract 를 다루는 것과 짝을 이룬다.
## First action (제일 처음에 하는 일)
알고리즘 모듈이 import 되는 시점은 두 가지다:
1. **AHBM backend 진입**: 사용자 코드가 `dist.init_process_group(backend="ahbm")`
를 호출하면, `AhbmCCLBackend.__init__` 안에서 `self._algo_module =
importlib.import_module(self._merged["module"])` 가 실행된다. 이때 모듈
레벨에서 가장 먼저 일어나는 일:
- `SIP_TOPO_RING/TORUS/MESH` 같은 정수 상수가 모듈 namespace 에 노출.
- `TOPO_NAME_TO_KIND` 사전이 모듈 namespace 에 노출 — backend 가
`topo_map = getattr(self._algo_module, "TOPO_NAME_TO_KIND", None)`
조회.
- `kernel_args` 함수 정의 — 호출 시 호출자가 사용.
- `allreduce_intercube_multidevice` 같은 알고리즘 함수 정의.
- 모듈 마지막 줄에서 `kernel = allreduce_intercube_multidevice`
alias 가 노출.
2. **ccl.yaml install 단계**: `kernbench.ccl.install.install_ipcq` 가 호출
되어 IPCQ neighbor table 을 푸시할 때 같은 알고리즘 모듈이 import 됨.
즉, **algorithm 모듈의 첫 일은 "topology-kind 상수, `TOPO_NAME_TO_KIND`
사전, `kernel_args` 함수, 그리고 `kernel` alias 를 모듈 namespace 에 노출
하는 것"** 이다. 모든 노출은 import-time 부수효과로 충분하며 별도 초기화
함수 호출이 필요하지 않다.
## Context
`AhbmCCLBackend` (ADR-0047) 는 process group 초기화 시점에 `ccl.yaml`
`defaults.algorithm` (또는 사용자가 지정한 알고리즘 이름) 으로부터 모듈
경로를 얻어 dynamic import 한다. backend 는 그 모듈로부터 다음 4 가지를
기대한다:
- `kernel`: collective 의 진입 함수.
- `kernel_args(world_size, n_elem, cube_w=, cube_h=) -> tuple`: kernel 에
넘길 위치 인자 묶음.
- `TOPO_NAME_TO_KIND` (optional): `topology.yaml``sips.topology`
문자열 (예: `"ring_1d"`, `"torus_2d"`, `"mesh_2d_no_wrap"`) 을 정수
상수로 매핑하는 dict.
- (간접) IPCQ neighbor table 설치: `configure_sfr_intercube_multisip`
알고리즘 모듈의 `TOPO_NAME_TO_KIND``cube_w/h` 를 보고 SFR 을 결정.
현재 코퍼스의 유일한 algorithm 모듈은 `lrab_hierarchical_allreduce.py`
(248 줄) 이다. 이름은 "**l**eft-**r**ight **a**lternating **b**roadcast
**hierarchical allreduce**". 향후 `ring_allreduce`, `tree_allreduce`,
`broadcast` 같은 모듈이 추가될 때마다 이 contract 를 따라야 일관된
디스패치가 가능하다.
이 contract 가 ADR-level 에 없으면:
- 새 algorithm 작성자가 ADR-0047 D3 의 한 줄 만으로 시그너처를 추론해야.
- kernel 함수 인자 순서 (특히 `t_ptr, n_elem, cube_w, cube_h, n_sips,
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h, tl`) 의 의미가 코드
grep 없이는 명확하지 않다.
- `kernel_args` 가 어떤 인자를 받고 어떤 tuple 을 돌려줘야 하는지 관례
로만 굳어진다.
## Decision
### D1. algorithm 모듈은 4 가지 public symbol 을 노출한다
```python
# src/kernbench/ccl/algorithms/<name>.py
from __future__ import annotations
# (필수) topology-kind 상수 — 알고리즘 내부에서 사용
SIP_TOPO_RING = 0
SIP_TOPO_TORUS = 1
SIP_TOPO_MESH = 2
# (선택) topology 이름 → kind 매핑. backend 가 ccl.yaml/topology 의
# 문자열 SIP topology 를 정수로 변환하는 데 사용.
TOPO_NAME_TO_KIND = {
"ring_1d": SIP_TOPO_RING,
"torus_2d": SIP_TOPO_TORUS,
"mesh_2d_no_wrap": SIP_TOPO_MESH,
}
# (필수) kernel 인자 빌더
def kernel_args(world_size: int, n_elem: int, *, cube_w: int = 4, cube_h: int = 4) -> tuple:
return (n_elem, cube_w, cube_h, world_size)
# (필수) kernel 함수 (`tl=...` 키워드를 통해 TLContext 가 주입됨)
def my_allreduce_kernel(t_ptr, n_elem, cube_w, cube_h, n_sips,
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h, *, tl):
...
# (필수) kernel alias — backend 가 `module.kernel` 로 접근
kernel = my_allreduce_kernel
```
- `kernel` alias 는 backend 가 직접 호출하는 entry point 다. 함수 이름이
무엇이든 (`allreduce_intercube_multidevice` 처럼) `module.kernel = fn`
으로 노출해야 한다.
- `kernel_args` 가 없으면 backend 가 알고리즘 인자를 만들 방법이 없다.
signature 는 D2 참고.
- `TOPO_NAME_TO_KIND` 가 없으면 backend 는 `sip_topo_kind = 0` 으로
fallback 한다. 단일 topology 만 지원하는 알고리즘이라면 생략 가능.
### D2. `kernel_args` 시그너처 — `(world_size, n_elem, *, cube_w, cube_h)`
```python
def kernel_args(world_size: int, n_elem: int, *,
cube_w: int = 4, cube_h: int = 4) -> tuple:
return (n_elem, cube_w, cube_h, world_size)
```
- **위치 인자**: `world_size` (= rank 수), `n_elem` (= 단일 shard 의
element 수, f16 기준).
- **키워드 인자**: `cube_w`, `cube_h` (= cube mesh 크기). default 는
4×4 — `topology.yaml` 의 `sip.cube_mesh` 기본값과 정합.
- **반환**: kernel 의 위치 인자 순서대로 묶은 tuple.
backend 의 `all_reduce` 가 호출 시:
```python
kernel_args_tuple = self._algo_module.kernel_args(
self._world_size, n_elem, cube_w=eff_cube_w, cube_h=eff_cube_h,
)
extra_args = (sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)
pending = self.ctx.launch(
self._merged["algorithm"], kernel_fn, tensor,
*kernel_args_tuple, *extra_args, _defer_wait=True,
)
```
즉 kernel 의 최종 위치 인자는: `(tensor_ptr, *kernel_args_tuple,
sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)` 이며, 거기에 `tl=...` 가
키워드로 자동 주입된다. `kernel_args` 가 돌려주는 tuple 의 길이/순서는
**kernel signature 와 1:1 일치** 해야 한다.
### D3. `kernel` 함수 시그너처 — 정형화된 9 + tl 인자
권장 시그너처:
```python
def my_kernel(
t_ptr: int, # VA base of the row-wise-sharded tensor on this SIP
n_elem: int, # element count per cube tile (or per shard)
cube_w: int, # cube mesh width (kernel_args 에서 옴)
cube_h: int, # cube mesh height (kernel_args 에서 옴)
n_sips: int, # world_size 와 동일 (rank = SIP, ADR-0024)
sip_rank: int, # 이 SIP 의 rank
sip_topo_kind: int, # TOPO_NAME_TO_KIND lookup 결과
sip_topo_w: int, # SIP mesh width (ring_1d 면 0)
sip_topo_h: int, # SIP mesh height (ring_1d 면 0)
*, tl, # TLContext (auto-injected)
) -> None:
```
`kernel_args` 가 다른 위치 인자 순서를 채택하더라도, kernel 의 **마지막
4 개 위치 인자는 항상 `(sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)`**
이며 backend 가 `extra_args` 로 append 한다 (ADR-0047 D5). 이 4 개 인자는
사용자 정의 algorithm 도 받아야 하지만, 알고리즘이 single-SIP 이라면
그냥 무시하면 된다.
`tl` 은 위치 인자가 아닌 키워드로 주입된다 — `RuntimeContext.launch` 가
kernel 호출 직전에 `tl=tl_ctx` 를 추가한다. 따라서 kernel signature 의
`tl` 은 keyword-only (`*, tl`) 또는 마지막 키워드 매개변수 형태여야
한다.
### D4. kernel body 의 자유도와 제약
kernel body 안에서 사용 가능한 표면: ADR-0046 D3 의 모든 `tl.*` primitive.
특히 자주 쓰이는 패턴:
- `cube_id = tl.program_id(axis=1)` — 이 PE 가 속한 cube 인덱스.
- `pe_addr = t_ptr + cube_id * nbytes` — cube-별 tile 의 VA 계산.
- `acc = tl.load(pe_addr, shape=(n_elem,), dtype="f16")` — local 데이터
로드.
- `tl.send(dir=...)` / `tl.recv(dir=..., shape=, dtype=)` — IPCQ
collective.
- `acc = acc + recv` — TensorHandle 산술 연산자 (ADR-0046 D4).
- `tl.store(pe_addr, acc)` — 결과 저장.
kernel body 는 일반 Python 함수이며, branching/looping 자유. 단:
- SimPy `yield` 또는 `async` 금지 (ADR-0046 D1).
- TensorHandle 의 `.data` 직접 접근 금지 — phase 1 timing 모델은
데이터 의존을 모른다 (ADR-0020 의 2-pass 분리).
- kernel 실행은 deterministic 해야 한다 — 같은 입력으로 두 번 실행하면
같은 op 시퀀스 발사. random / external IO 금지.
### D5. SIP topology semantics — `sip_topo_kind` 의 의미
backend 가 `topology.yaml` 의 `system.sips.topology` 문자열을 algorithm
모듈의 `TOPO_NAME_TO_KIND` 로 lookup 하여 `sip_topo_kind` 정수로 변환.
algorithm 은 이 정수를 보고 분기:
```python
if sip_topo_kind == SIP_TOPO_RING:
acc = _inter_sip_ring(...)
elif sip_topo_kind == SIP_TOPO_TORUS:
acc = _inter_sip_torus_2d(...)
elif sip_topo_kind == SIP_TOPO_MESH:
acc = _inter_sip_mesh_2d(...)
```
각 topology branch 는 IPCQ direction 이름 (예: `"global_E"`, `"W"`, `"S"`,
`"N"`) 을 통해 peer 와 통신. direction 의 의미는 ADR-0023/0025 가 정의
하며, `configure_sfr_intercube_multisip` 가 IPCQ neighbor table 을 그에
맞춰 설치한다.
algorithm 모듈은 자기가 지원하지 않는 topology kind 가 들어오면 silent
no-op 으로 두기보다 명시적으로 `raise ValueError(f"unsupported topology
kind {sip_topo_kind}")` 하는 것을 권장 — 실수로 backend 에 잘못 dispatch
된 경우 빠르게 fail.
### D6. ccl.yaml 의 algorithm entry 구조
algorithm 모듈은 `ccl.yaml` 의 entry 와 짝을 이룬다 (ADR-0023 D10 +
ADR-0047 D3):
```yaml
defaults:
algorithm: lrab_hierarchical_allreduce
n_elem: 8
algorithms:
lrab_hierarchical_allreduce:
module: kernbench.ccl.algorithms.lrab_hierarchical_allreduce
# optional: world_size override
# optional: per-algorithm parameters consumed by configure_sfr_intercube_multisip
```
- `module`: full Python module path. backend 의 `importlib.import_module`
가 이 문자열을 그대로 사용.
- `world_size` (optional): 명시되면 topology fallback 을 override
(ADR-0047 D2).
- algorithm-specific parameters 는 `configure_sfr_intercube_multisip` 가
소비.
새 algorithm 추가 시:
1. `src/kernbench/ccl/algorithms/<name>.py` 작성 (D1 컨벤션).
2. `ccl.yaml` 의 `algorithms` 섹션에 entry 추가.
3. (필요 시) `kernbench.ccl.sfr_config` 에 SFR 설치 분기 추가.
4. test 추가 (예: `tests/sccl/test_<name>.py`, ADR-0043 의 eval harness
확장).
### D7. legacy "rank = flat PE index" 모드
ADR-0047 D2 가 명시한 `ccl.yaml` 의 `world_size` override 경로는 legacy
"rank = flat PE index" 테스트가 사용한다. algorithm 모듈은 이 모드 에서도
`n_sips=world_size` 만큼의 rank 가 들어옴을 가정하면 된다 — backend 가
rank↔(SIP, cube, PE) 매핑을 사전에 분리해 두므로 algorithm 본체에서는
modal 분기가 필요 없다.
단, single-cube workload 에서는 `cube_w=cube_h=1` 이 들어와 mesh-기반
phase 들이 skip 되도록 작성해야 한다 (`lrab_hierarchical_allreduce.py`
의 `single_cube = (cube_w == 1 and cube_h == 1)` 패턴 참고).
## Alternatives Considered
### A1. algorithm 모듈을 class 로 구조화 (`class Allreduce: kernel(...)` 등)
기각. Python 모듈 namespace 자체가 algorithm 의 identity 로 사용 중이며
(ADR-0047 D3 의 `importlib.import_module`), class 한 겹은 추가 indirection
만 늘리고 dispatch 측 코드를 두텁게 만든다. 모듈-레벨 free function
+ `kernel` alias 패턴이 충분히 명확.
### A2. `kernel_args` 를 명시적 dataclass 로 typing
기각 (현재). algorithm 마다 인자 갯수가 다른 것이 정상이며, dataclass 한
종류를 강제하면 다양한 algorithm 간 호환이 어려워진다. tuple 반환은 simple
하고 backend 측 `*kernel_args_tuple` unpacking 과 깨끗이 맞물린다.
algorithm 별 자체 타입 강도가 필요해지면 그 algorithm 모듈 안에서 NamedTuple
사용은 자유.
### A3. SFR 설치를 algorithm 모듈 안으로
기각. SFR 설치 (`configure_sfr_intercube_multisip`) 는 topology + algorithm
모두를 보고 IPCQ neighbor table 을 설치하는 cross-module 결정이라, algorithm
모듈 내부보다 `kernbench.ccl.sfr_config` 같은 전용 위치가 자연스럽다. D6 의
"필요 시 sfr_config 분기 추가" 워크플로우가 책임 분리 측면에서 더 명확.
### A4. algorithm name 을 모듈 namespace 에 자동 등록 (decorator)
기각. ADR-0045 (bench) 와 달리 algorithm 은 ccl.yaml entry 와 직접 묶여
있어 추가 등록 레지스트리가 중복이다. `module` 문자열 매핑 하나면 충분.
## Consequences
- ADR-0047 D3 의 한 줄 contract 가 D1–D7 의 작성자-친화적 가이드로 확장
되어, 새 algorithm 추가 시 시그너처를 grep 으로 추론할 필요 없음.
- D3 의 9 + tl 인자 시그너처가 표준화되어, backend 의 `extra_args` append
(ADR-0047 D5) 와 자연스럽게 맞물림. 향후 single-SIP-only algorithm 도
4 개의 sip_* 인자를 받아야 함이 명시.
- D5 의 fail-loud 권장으로, ccl.yaml 의 topology 가 algorithm 미지원
topology 로 잘못 설정되면 backend 가 silent wrong-result 가 아닌
ValueError 로 fail.
- D6 의 단계별 추가 절차가 명시되어, 새 algorithm 추가가 sfr_config /
test / ccl.yaml 어디까지 손대야 하는지 분명.
@@ -0,0 +1,267 @@
# 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}.<comp>`,
`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}.<comp>`, `sip{S}.{io_id}.<comp>`) 의 단일
소유자가 이 클래스다 (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}.<comp>`, `sip{S}.{io_id}.<comp>`,
`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.1D3.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 후보가 식별됨.
@@ -0,0 +1,352 @@
# ADR-0052: OpLog + MemoryStore Schemas — sim_engine internals
## Status
Accepted (2026-05-22).
`sim_engine/op_log.py``OpRecord` 스키마와 `OpLogger` 의 record_start /
record_end / record_copy 동작, 그리고 `sim_engine/memory_store.py`
`MemoryStore` 가 사용하는 (space, addr) 주소공간 namespace 와 read/write
의미를 명시한다. ADR-0020 (2-pass data execution) 가 두 인프라의 존재를
선언하나, **레코드의 정확한 필드와 의미** 는 ADR-level 에서 정리되지
않았고 ADR-0046 D3.2 (`tl.store` visibility), ADR-0023 D9 (IPCQ copy
record) 등 여러 ADR 이 이들의 동작에 의존하고 있다.
## First action (제일 처음에 하는 일)
### `OpLogger(memory_store=None)`
생성 즉시 다음 3 가지 필드 초기화:
1. `self._records: list[OpRecord] = []` — 누적된 op record.
2. `self._pending: dict[int, dict] = {}``id(msg)` 키로 partial record
(record_start 시점에 만들어졌고 record_end 가 아직 안 온 것).
3. `self._memory_store = memory_store` — 옵션 MemoryStore reference.
math op 의 input 스냅샷 + dma_write 의 HBM source 스냅샷 캡처에 사용.
생성 시점에는 records / pending 모두 비어 있으며, `record_*` 호출이
순차적으로 데이터를 누적한다.
### `MemoryStore()`
생성 즉시 `self._storage: dict[str, dict[int, np.ndarray]] = {}` 단 하나
의 필드 초기화. 두 단계 dict (`space → addr → ndarray`) 이며 lazy 하게
필요한 space 가 생길 때마다 inner dict 가 채워진다.
즉, **두 인프라의 첫 일은 "비어 있는 누적 buffer + space-별 sparse dict
를 만들어 두는 것"** 이다. 첫 record / write 가 실제로 도착하면 그때
필드가 채워지기 시작한다.
## Context
ADR-0020 (2-pass data execution) 의 D2/D5/D7 가 다음을 선언:
- Phase 1 (timing) 동안 `ComponentBase._on_process_start/end` hook 이
`OpLogger.record_start/end` 를 호출하여 모든 data op 의 시간 + 메타
데이터를 기록.
- Phase 2 (data) 가 op_log 를 t_start 순으로 재생하여 실 데이터 결과를
계산.
- 데이터 페이로드 자체는 `MemoryStore` 에 (space, addr) 키로 보관.
ADR-0023 D9 (IPCQ atomic write), ADR-0027 (Megatron TP scratch
overwrite 회피), ADR-0046 D3.2 (`tl.store` visibility) 등 후속 ADR 들이
op_log 와 MemoryStore 의 동작에 의존하지만, **정확한 record 필드 / space
이름 / 스냅샷 시점** 은 코드 grep 으로만 확인 가능하다. 본 ADR 이 이를
정리한다.
## Decision
### D1. `OpRecord` 스키마 — 7 개 필드
```python
@dataclass
class OpRecord:
t_start: float
t_end: float
component_id: str
op_kind: str # "memory" | "gemm" | "math" | "unknown"
op_name: str # e.g. "dma_read", "gemm_f16", "exp",
# "TileToken/DMA_READ", "composite_gemm",
# "ipcq_copy"
params: dict[str, Any]
dependency_ids: list[int] = field(default_factory=list)
```
- **`t_start` / `t_end`**: SimPy 시간 (float ns). `t_start` 는 component
가 op 를 시작한 시점, `t_end` 는 완료 시점. duration = `t_end - t_start`.
- **`component_id`**: op 가 발생한 node id (예:
`"sip0.cube0.pe0.pe_dma"`).
- **`op_kind`**: 4 가지 중 하나. Phase 2 DataExecutor 가 이 값으로 분기.
- **`op_name`**: 디버깅 / 분석용 사람-친화 이름. TileToken 일 경우
`"TileToken/{stage_type}"` (예: `"TileToken/DMA_READ"`) 로 stage 를
구분.
- **`params`**: op-종속 메타데이터 dict (D3 참고).
- **`dependency_ids`**: 현재 사용되지 않음 (default `[]`). 향후 cross-op
dependency 추적이 필요해질 때를 위한 자리.
### D2. `OpLogger.records` — t_start 정렬 보장
```python
@property
def records(self) -> list[OpRecord]:
self._records.sort(key=lambda r: r.t_start)
return self._records
```
매 접근 시 `t_start` 로 stable sort. 즉 같은 t_start 인 record 들은 insertion
순서를 유지. ADR-0020 D5 의 "t_start stable ordering" 요구와 정합.
Phase 2 DataExecutor 는 항상 `records` property 를 통해 접근하므로,
record_end 호출이 t_start 와 다른 순서로 도착해도 (예: 짧은 op 가 긴
op 보다 늦게 시작했으나 먼저 끝남) 재정렬되어 일관된 시퀀스를 받는다.
### D3. op_name 별 `params` 스키마 (`_extract_op_info` 매핑)
#### D3.1. `op_kind="memory", op_name="dma_read"` (DmaReadCmd)
```python
{"src_addr": int, "nbytes": int, "handle_id": str}
```
#### D3.2. `op_kind="memory", op_name="dma_write"` (DmaWriteCmd)
```python
{
"src_space": str, # handle.space ("tcm"|"hbm"|"sram"), default "tcm"
"src_addr": int, # handle.addr
"shape": tuple, "dtype": str,
"dst_space": "hbm", # DmaWrite 는 항상 HBM 으로
"dst_addr": int,
"nbytes": int,
"handle_id": str,
# record_end 시점에 src_space == "hbm" 이면 snapshot 추가 (D4)
"snapshot": np.ndarray | None,
}
```
#### D3.3. `op_kind="gemm", op_name=f"gemm_{dtype_a}"` (GemmCmd)
```python
{
"src_a_addr": int, "src_b_addr": int, "dst_addr": int,
"shape_a": tuple, "shape_b": tuple, "shape_out": tuple,
"dtype_in": str, "dtype_out": str,
"m": int, "k": int, "n": int,
# ADR-0027: per-operand + output spaces 보존
"src_a_space": str, "src_b_space": str, "dst_space": str,
}
```
#### D3.4. `op_kind="math", op_name=msg.op` (MathCmd; op = "exp", "sum", "add", "where" 등)
```python
{
"input_addrs": list[int], # 입력 핸들들의 addr
"input_shapes": list[tuple],
"input_spaces": list[str],
"input_dtypes": list[str],
"dst_addr": int, "dst_space": str,
"shape_out": tuple, "dtype": str,
"axis": int | None, # reduction 인 경우만 의미 있음
# record_end 시점에 모든 input 의 스냅샷이 채워짐 (D4)
"input_snapshots": list[np.ndarray | None],
}
```
#### D3.5. `op_kind="gemm" or "math", op_name=f"composite_{op}"` (CompositeCmd)
```python
{
"op": str, # "gemm" | "math"
"out_addr": int, "out_nbytes": int,
# op == "gemm" 인 경우 GemmCmd 와 같은 필드 추가:
"src_a_addr": int, "src_b_addr": int,
"shape_a": tuple, "shape_b": tuple,
"dtype_in": str, "dtype_out": str,
"src_a_space": str, "src_b_space": str,
"dst_space": "hbm", "dst_addr": int, # = out_addr
}
```
`op == "gemm"` 이면 `op_kind = "gemm"`, 아니면 `"math"`. Phase 2 측에서
GemmCmd 와 동일 path 로 재생되도록 alias.
#### D3.6. `op_kind="memory", op_name="ipcq_copy"` (record_copy 전용 경로)
```python
{
"src_space": str, "src_addr": int,
"dst_space": str, "dst_addr": int,
"shape": tuple, "dtype": str, "nbytes": int,
"snapshot": np.ndarray | None, # 호출자가 전달, 없으면 record_copy 가 fresh read
}
```
`PE_DMA._handle_ipcq_inbound` (ADR-0023 D9) 가 이 record 를 발사하여 IPCQ
slot 의 inbound copy 를 Phase 2 가 재생 가능하게 한다. 이 record 는
`record_start` / `record_end` 를 거치지 않고 직접 `record_copy()` 로 push.
#### D3.7. `op_kind="unknown", op_name=type(msg).__name__`
`_extract_op_info` 가 인식 못 한 message 의 fallback. params = `{}`.
DataExecutor 가 이 op_kind 를 만나면 skip — Phase 2 replay 에 영향 없음.
### D4. snapshot 캡처 시점
`OpLogger._memory_store` 가 set 되어 있을 때 record_end 가 다음을 수행:
- **math op**: 모든 input addr/shape/space/dtype 으로
`self._memory_store.read(...)` 를 호출하여 `params["input_snapshots"]`
ndarray copy 첨부. read 실패 시 None.
- **dma_write op**: `src_space == "hbm"` 인 경우에만 source HBM 의
스냅샷을 `params["snapshot"]` 에 첨부. TCM source 는 **명시적으로
스킵** — TCM (PE scratch) 은 Phase 2 math/gemm 재생이 다시 채우므로,
Phase-1-time snapshot 을 잡으면 이전 kernel 의 stale 데이터를 잡을 위험
(ADR-0027 postmortem: TP gemm → all_reduce race).
- **ipcq_copy**: `record_copy` 호출자가 `snapshot=token.data` 같이 in-flight
스냅샷을 전달. 없으면 record_copy 가 fresh read 로 대체 시도.
스냅샷은 `.copy()` 가 호출되어 (`ndarray.copy()` 가 fresh allocation) 이후
storage mutation 으로부터 안전. ADR-0027 의 "cross-PE Phase 2 ordering"
race 회피의 근간.
`memory_store` 가 None 인 경우 (Phase 1 timing-only 모드) 스냅샷 단계는
전부 skip. record 의 timing 정보만 보존되며 데이터 replay 는 불가능.
### D5. TileToken 처리 — record_start 가 stage 정보를 캡처
ADR-0014 D6 의 self-routing tile token (pipeline 모드) 은 stage_idx 가
record_end 시점에 이미 advance 되어 있을 수 있다 (TileToken 이 다음
component 로 이동하면서 next stage 의 params 를 캐시). 따라서:
`record_start` 가 다음을 `pending[id(msg)]["snap"]` 에 미리 저장:
```python
snap["stage_type"] = stage.stage_type.name # "DMA_READ", "GEMM", 등
snap["stage_params"] = dict(stage.params) # 시점의 params 복사본
```
`record_end` 에서 이 snap 을 꺼내 params 에 merge:
- `params["stage_type"]` 가 final params 에 추가.
- `stage_params` 의 key 들이 (이미 있으면 보존) merge.
- `op_name == "TileToken"` 이면 `op_name = f"TileToken/{stage_type}"`
rewrite (예: `"TileToken/DMA_READ"`) — 같은 component 에서 발생한 서로
다른 stage 의 record 를 disambiguate.
이 메커니즘 덕분에 DMA_READ vs DMA_WRITE, FETCH vs STORE 가 같은 component
(예: pe_dma) 에서 발생하더라도 reporting 측에서 구분 가능.
### D6. `MemoryStore` — (space, addr) 두 단계 dict
```python
class MemoryStore:
def __init__(self) -> None:
self._storage: dict[str, dict[int, np.ndarray]] = {}
def write(self, space, addr, data): self._storage[space][addr] = data
def read(self, space, addr, shape=None, dtype=None) -> np.ndarray: ...
def has(self, space, addr) -> bool: ...
def snapshot(self) -> MemoryStore: ...
```
#### D6.1. space namespace
문자열 키. 표준 값:
- `"hbm"`: HBM 데이터 (deploy_tensor + Phase 2 dma_write 결과).
- `"tcm"`: PE-로컬 TCM (Phase 2 math/gemm 결과).
- `"sram"`: cube-level SRAM (ADR-0023 D9.7 IPCQ slot tier).
다른 space (예: `"reg"`) 도 자유롭게 허용 — `_storage` 가 lazy dict 라
새 space 가 write 호출과 함께 자동 생성.
#### D6.2. address keying
`addr` 는 정수. **physical address (PA) 또는 virtual address (VA)** 일 수
있다 — MemoryStore 자체는 address space 의 의미를 모르고 그저 키로 쓴다.
Phase 1 의 `MemoryWriteMsg` 는 PA + VA 둘 다 write (`_create_tensor` 에서
PA 로 zero-init, VA base 로도 zero-init), Phase 2 는 op_log 가 captured
한 address 로 read/write.
`addr` 의 의미는 호출자가 결정한다 — `MemoryStore` 는 lookup 만 제공.
#### D6.3. read/write 의미 — reference store (no copy)
`write(space, addr, data)`: `data` ndarray 의 reference 를 저장. **copy
하지 않음**. 호출자가 같은 ndarray 를 이후 mutate 하면 stored value 도
변경된다.
`read(space, addr, shape=None, dtype=None)`: 저장된 ndarray 의 reference
반환. `shape` 또는 `dtype` 이 제공되면:
- `dtype != stored.dtype`: `arr.view(np_dtype)` 로 reinterpret cast (no
copy).
- `shape != stored.shape`: `nbytes` 가 일치하면 `arr.reshape(shape)` (view).
- `nbytes` 불일치: `ValueError`.
데이터를 안전하게 분리하려면 호출자가 `arr.copy()` 호출. ADR-0027 의
race 회피가 op_log snapshot 단계에서 명시적 copy 를 강제하는 이유.
#### D6.4. `has(space, addr) -> bool`
해당 키의 존재 여부만 확인. 데이터 인스턴스화는 안 함.
#### D6.5. `snapshot() -> MemoryStore`
shallow copy. inner dict 의 새 인스턴스를 만들되 ndarray reference 는
공유. Phase 2 초기화 시점에 Phase 1 의 store 를 fork 하여 Phase 2 의
mutation 이 Phase 1 의 다른 사용처에 영향을 주지 않게 분리하는 데 사용.
### D7. op_log 가 SimPy 단일-스레드를 가정한다
`OpLogger``_records`, `_pending` 은 lock 없이 사용. SimPy 가 single-
threaded 라 `record_start``record_end` 사이에 다른 thread 가 끼어들
수 없다는 가정.
향후 multi-process kernbench (ADR-0047 D6) 가 도입되면 OpLogger 도 process
별로 분리되어야 함이 명시. 단일 OpLogger 인스턴스가 multiple process 의
record 를 받지 못한다.
## Alternatives Considered
### A1. op_log 를 SQLite / parquet 같은 외부 store 로
기각 (현재). in-memory list 가 Phase 1 → Phase 2 의 핸드오프 latency 를
최소화한다. 외부화는 long-running batch run 에서 의미가 있겠으나, 현재
single-run 워크로드 에서는 overhead 만 추가.
### A2. snapshot 을 record_start 시점에 캡처
기각. record_start 시점은 input 이 아직 채워지지 않은 상황 (예: math
op 의 input 이 직전 op 의 output 일 때) 이 흔하다. record_end 가 정확한
시점.
### A3. MemoryStore 를 component-별 store 로 분리
기각. (space, addr) 키가 이미 충분히 disambiguation 을 제공하며, component
별 분리는 cross-PE IPCQ copy (ADR-0023 D9) 가 source/destination 양쪽
store 를 접근해야 하는 케이스를 복잡하게 만든다.
### A4. op_log 에 cross-op dependency edge 명시
부분 채택. `dependency_ids` 필드가 OpRecord 에 자리 잡고 있지만 현재
사용되지 않음 (D1). Phase 2 DataExecutor 가 t_start 정렬 + secondary sort
(memory ops before math at same t_start) 로 ordering 을 결정하며, 명시적
dependency graph 가 필요해지면 이 필드가 채워질 자리. 현재는 ordering rule
이 충분하므로 미사용.
## Consequences
- ADR-0020 의 op_log / MemoryStore 선언이 D1D6 의 구체 schema 로 확장
되어, Phase 2 DataExecutor 작성/수정 시 정확한 필드 의미를 grep 없이
ADR 에서 확인 가능.
- D3 의 op_name 별 params 스키마가 명시되어, 새 op (예: 새 reduction
type) 추가 시 `_extract_op_info` 분기 어디에 끼울지 명확.
- D4 의 snapshot 시점 차이 (math = input snapshot, dma_write = HBM-only
snapshot) 가 ADR 에 굳어져, ADR-0027 의 cross-PE race 회피 결정이 향후
refactor 에서 silently 깨지지 않음.
- D6.3 의 reference-store 의미가 명시되어, 호출자가 mutation safety 책임
을 인지. ADR-0027 의 explicit `.copy()` 패턴이 정당화됨.
- D7 의 single-thread 가정이 명시되어, multi-process kernbench (ADR-0047
D6 supersession 후보) 도입 시 OpLogger 분리가 필요함이 분명.
@@ -0,0 +1,307 @@
# ADR-0053: Topology Builder + Visualizer Algorithms
## Status
Accepted (2026-05-22).
`topology/builder.py`, `topology/mesh_gen.py`, `topology/visualizer.py`
함께 수행하는 토폴로지 컴파일·시각화 파이프라인의 핵심 알고리즘 선택
(placement-driven router attachment, mesh auto-layout, source_hash 캐시,
view projection, SVG rendering) 을 명시한다. ADR-0006 가 topology
compilation 의 high-level intent (compiled topology, distance extraction,
automatic diagram generation) 를 정의하나, **builder 가 실제로 어떤
알고리즘을 사용하는지** 는 코드 grep 으로만 확인 가능했다.
## First action (제일 처음에 하는 일)
`resolve_topology(path_str)` 가 호출되면 다음 4 단계가 순서대로 일어난다:
1. **경로 검증** (`builder.py::resolve_topology`):
`Path(path_str).expanduser().resolve()`, 존재 확인, file 여부 확인.
실패 시 `FileNotFoundError` 또는 `ValueError`.
2. **YAML 파싱** (`_read_spec`): `yaml.safe_load`. parse error 면 line/
column 정보 포함한 `ValueError`. dict 가 아니면 reject.
3. **mesh 자동 생성** (`mesh_gen.ensure_mesh_file`): topology yaml 과
같은 디렉터리에 `cube_mesh.yaml` 을 만들거나 (캐시 invalid 시) 재사용
(캐시 hit 시). 이 단계가 cube NoC 의 라우터 grid 와 부착 정보를 결정.
4. **graph 컴파일** (`_compile_graph`): system → IO chiplets → cubes →
inter-cube edges → IO↔cube edges → system↔IO edges 순으로 nodes/edges
를 누적, 그 다음 4 개의 view projection (system, sip, cube, pe) 을
생성하여 `TopologyGraph` 로 묶음.
즉, **topology compile 의 첫 일은 "topology.yaml 을 dict 로 읽고, 동일
디렉터리에 cube_mesh.yaml 을 생성/검증한 뒤, system→sip→cube→pe 순으로
flat graph + 4-view projection 을 만드는 것"** 이다.
## Context
`topology/` 패키지의 책임:
- **builder.py** (1207 줄): topology.yaml 을 받아 `TopologyGraph` (nodes
+ edges + 4 view projections) 를 컴파일.
- **mesh_gen.py** (305 줄): cube NoC 의 라우터 grid 와 PE/UCIe/M_CPU/SRAM
부착 위치를 자동 결정하여 `cube_mesh.yaml` 로 캐시.
- **visualizer.py** (887 줄): `TopologyGraph` 로부터 SVG 다이어그램 4종
(system / sip / cube / pe) 을 생성.
ADR-0006 가 "topology compilation 의 결과는 distance metadata 와 diagram
generation 의 single source" 라는 high-level 결정을 정의하나, 구체 알고리즘
(예: placement-driven nearest-router attachment, HBM 제외 zone 산출,
source_hash 의 어떤 필드가 invalidation 을 트리거하는가) 은 ADR 에 없다.
특히 다음 결정들이 ADR-level 에 부재:
- 왜 mesh_gen 이 별도 파일 (`cube_mesh.yaml`) 로 캐시되는가?
- source_hash 가 어떤 필드를 포함하며, 어떤 변경이 재생성을 강제하는가?
- placement coordinate 가 cube 좌표가 아닌 mm 단위인 이유?
- HBM zone 제외와 UCIe N/S/E/W 분배가 mesh 안에서 어떻게 결정되는가?
- view projection 4 개 (system/sip/cube/pe) 의 추상화 레벨 차이?
이 ADR 이 이 결정들을 한 곳에 정리한다.
## Decision
### D1. compile 파이프라인 — 6 단계
`_compile_graph(spec)`:
1. **시스템 노드 생성** (`_instantiate_system`): `fabric.switch0`, host CPU
등 system-level 노드 추가.
2. **per-SIP loop** (`for sip_id in range(system.sips.count)`):
- **IO chiplets** (`_instantiate_io_chiplets`): pcie_ep / io_cpu /
io_noc / io_ucie PHY / conn 노드 + 내부 양방향 edge 생성.
- **cube instantiation** (`_instantiate_cube`): cube_mesh.yaml 의 router
grid 를 토대로 cube-별 라우터, PE sub-components (pe_cpu, pe_dma,
pe_fetch_store, pe_gemm, pe_math, pe_mmu, pe_tcm, pe_scheduler,
pe_ipcq), m_cpu, sram, hbm_ctrl 인스턴스화 + 내부 edge 깔기.
- **inter-cube edges** (`_add_inter_cube_edges`): UCIe N/S/E/W mesh
edge.
- **IO ↔ cube edges** (`_add_io_to_cube_edges`): io_noc 와 cube 의
edge UCIe phy 사이 연결.
3. **switch ↔ IO edges** (`_add_system_to_io_edges`): `fabric.switch0`
와 각 SIP 의 `pcie_ep` 사이 양방향 edge (ADR-0038 D3 + ADR-0010 의
cross-SIP IPCQ 경로).
4. **view projections** 4 종 build:
- `_build_system_view(spec)` — Tray 레벨, SIP 들과 system switch.
- `_build_sip_view(spec)` — SIP 안의 cube mesh + IO chiplet.
- `_build_cube_view(spec)` — 단일 cube 안의 router grid + PE/M_CPU/SRAM/
HBM_CTRL 부착.
- `_build_pe_view(spec)` — 단일 PE 안의 9 sub-components + 내부 edge.
5. **TopologyGraph 리턴**: `TopologyGraph(spec, nodes, edges, system_view,
sip_view, cube_view, pe_view)`.
이 6 단계는 **순서가 의미를 가진다**: cubes 가 만들어진 후에야 inter-cube
edges 가 valid 한 src/dst 를 갖고, IO chiplet 이 먼저 만들어져야 IO ↔ cube
edge 가 그를 참조할 수 있다. 새 노드 종류를 끼울 때는 의존 관계를 보고
적절한 위치에 삽입해야 한다.
### D2. `cube_mesh.yaml` — 별도 파일 + source_hash 캐시
`mesh_gen.ensure_mesh_file(cube_spec, mesh_path)`:
1. `source_hash = _compute_source_hash(cube_spec)` 산출. 입력 필드:
- `geometry` (cube_mm.w/h 등).
- `pe_layout` (corners, pe_per_corner).
- `ucie.n_connections`.
- `memory_map.hbm_mapping_mode`.
- `placement` (m_cpu/sram pos_mm).
2. `mesh_path` (= `topology.yaml` 와 같은 디렉터리의 `cube_mesh.yaml`) 이
존재하고 `existing.source_hash == source_hash` 면 재사용 (캐시 hit).
3. 아니면 `_generate_mesh(cube_spec, source_hash)` 로 새 mesh 생성 후
yaml 로 저장.
별도 파일로 캐시하는 이유:
- mesh 생성은 PE/UCIe/router 부착 계산이 들어가 매번 다시 하기 무거움.
- 같은 cube spec 으로 여러 번 실행 시 동일 mesh 가 보장되어야 함.
- 사람이 직접 mesh 를 inspect / debug 할 수 있는 artifact 가 됨.
`source_hash` 가 list 한 5 개 필드가 mesh 형상을 결정하는 핵심이며, 그
외 (예: bandwidth, overhead_ns) 변경은 mesh 재생성을 트리거하지 않는다.
### D3. cube NoC mesh auto-layout 알고리즘
`_generate_mesh(cube_spec)`:
#### D3.1. 행/열 결정
- `pe_positions = _corner_pe_positions(cube_w, cube_h)`: 4 corner (NW/NE/
SW/SE) 마다 PE center 좌표 (mm). hardcoded `(1.5, 1.5)` / `(cube_w-1.5,
cube_h-1.5)` 패턴 + `pe_per_corner=2` 면 각 corner 에 2 PE 위치.
- `col_xs = _compute_col_positions(...)`: PE 들의 x 좌표 union + `max_spacing
= 3.0 mm` 보다 큰 gap 에 relay 컬럼 삽입.
- `row_ys, rows_per_half = _compute_row_positions(cube_h, n_connections,
pe_positions)`:
- `n_conn = max(n_connections, 2)` (hot path minimum).
- `rows_per_half = ceil(n_conn / 2)`.
- top 절반 + HBM 두 row + bottom 절반. HBM 은 `(cube_h/2 - 1.5, cube_h/2
+ 1.5)` 에 위치. PE rows 와 HBM rows 사이 `hbm_gap = 1.5 mm`.
#### D3.2. HBM 제외 zone
`hbm_row_start = rows_per_half`, `hbm_row_end = rows_per_half + 1`.
`hbm_col_start = n_cols // 2 - 1`, `hbm_col_end = n_cols // 2`.
이 (row, col) 사각형 안의 router 슬롯은 `None` 으로 마킹 (라우터 없음).
실제 HBM 컨트롤러는 별도 `hbm_ctrl.pe{X}` 노드로 ADR-0017 D9 의 per-PE
파티션 패턴을 따라 부착.
#### D3.3. PE 부착
각 corner 의 PE 들은 다음 row 에 매핑:
- Top half: NW → row 0, NE → row 1 (top_corners 안의 index).
- Bottom half: SW → row `hbm_row_end + 1`, SE → row `hbm_row_end + 2`.
각 PE 의 x 좌표가 가장 가까운 col 의 router 에 부착 (`min(range(n_cols),
key=lambda c: abs(col_xs[c] - pe_x))`). 부착 항목은 `pe{pe_idx}.dma`,
`pe{pe_idx}.cpu`, `pe{pe_idx}.hbm` 세 가지 (router 별 attach list 에 push).
#### D3.4. M_CPU / SRAM 부착 — nearest router by Euclidean distance
`placement.m_cpu.pos_mm` (default `[1.5, 5.5]`) 와 `placement.sram.pos_mm`
(default `[1.5, 8.5]`) 의 좌표에서 가장 가까운 router 를 Euclidean
distance 로 찾아 attach list 에 `"m_cpu"` / `"sram"` 추가.
#### D3.5. UCIe N/S/E/W 분배
`ucie_pe_rows = top_pe_rows + bot_pe_rows` (총 `2 * rows_per_half` 개).
- UCIe-E: 매 PE row 마다 rightmost col 의 router 에 `ucie_e.c{i}`.
- UCIe-W: leftmost col 의 router 에 `ucie_w.c{i}` (E 의 mirror).
- UCIe-N/S: PE column 들 중 절반을 좌측, 절반을 우측으로 나눠 top row /
bottom row 의 해당 col 에 부착.
각 UCIe connection 은 `c{i}` index 가 붙어 ucie_n_connections 만큼의 PHY
가 분산된다 (ADR-0017 D5+).
### D4. node 명명 규칙 — 단일 소유자
builder.py 는 다음 명명 규칙으로 노드를 만든다 (ADR-0051 D5 의 단일
소유자 원칙):
- `fabric.switch0` — system-level switch.
- `sip{S}.{io_id}.{pcie_ep|io_cpu|io_noc|io_ucie.{dir}|conn.{id}}` — IO
chiplet.
- `sip{S}.cube{C}.{m_cpu|sram|hbm_ctrl.pe{X}|noc.r{R}c{C}|...}` — cube 내부.
- `sip{S}.cube{C}.pe{P}.{pe_cpu|pe_dma|pe_fetch_store|pe_gemm|pe_math|pe_mmu|pe_tcm|pe_scheduler|pe_ipcq}` — PE sub-components.
이 명명 규칙을 변경하려면 builder.py 와 router.py (ADR-0051) 의 helper
양쪽이 함께 갱신되어야 한다. 컴포넌트는 명명 규칙을 직접 알지 못하고
helper 만 호출한다.
### D5. edge `kind` 분류
각 edge 가 부여받는 `kind` 가 라우팅 정책 (ADR-0051 D2) 의 입력. 주요
kind 값:
- `"pe_internal"` — PE 내부 sub-component 간.
- `"pe_to_router"` — PE_DMA ↔ cube NoC router.
- `"router_mesh"` — cube NoC router 간.
- `"router_to_hbm"`, `"router_to_mcpu"`, `"router_to_sram"`,
`"sram_to_router"` 등 — cube-attached component 간.
- `"ucie_internal"`, `"ucie_conn_to_router"`, `"router_to_ucie_conn"`,
`"ucie_conn_to_noc"`, `"noc_to_ucie_conn"`, `"ucie_mesh"` — UCIe 관련.
- `"io_internal"` — IO chiplet 내부.
- `"io_to_cube"`, `"cube_to_io"` — IO ↔ cube 경계.
- `"pcie"` — switch ↔ pcie_ep.
- `"command"` — control-plane only edges (M_CPU ↔ NOC 등; PE DMA path 에서
제외).
새 edge kind 를 추가하면 router.py 의 4 adjacency graph (ADR-0051 D2) 의
어느 카테고리에 속할지 결정해야 한다 — 그렇지 않으면 default 로 `_adj_all`
에만 포함되어 의도와 다른 routing 발생 가능.
### D6. view projection — 4 추상화 레벨
`TopologyGraph` 는 flat (nodes + edges) 외에 4 개의 view projection 을
보유:
- **system_view** (`_build_system_view`): Tray 레벨. SIP 박스들 + `fabric.
switch0`. PCIE 링크 표시. 외부 발표용 high-level overview.
- **sip_view** (`_build_sip_view`): 한 SIP 안. cube mesh + IO chiplet
(pcie_ep + io_cpu + io_noc). UCIe N/S/E/W 가 cube 간 연결로 보임.
- **cube_view** (`_build_cube_view`): 한 cube 안. router grid + PE/M_CPU/
SRAM/HBM_CTRL 부착 + UCIe PHY edge 부분. cube 내부 라우팅 / placement
진단용.
- **pe_view** (`_build_pe_view`): 한 PE 안. 9 sub-components + 내부 edge
(pe_internal kind). 자세한 PE 내부 dataflow 검토용.
view 는 spec 에서 `visualization.emit_views: [system, sip, cube]` 같이
선택적으로 출력 (ADR-0006). pe view 는 기본 출력에서 빠져 있으나 코드는
유지 (자세한 디버그용).
### D7. visualizer.py — SVG 다이어그램 출력
`emit_diagrams(graph, out_dir)` 가 모든 view 를 SVG 로 렌더. 핵심 함수:
- `_render_view_svg(view)` — 일반적인 view 렌더 (router grid 가 없는
경우).
- `_render_cube_view_svg(view, spec)` — cube view 전용 (HBM block 그리기,
router grid layout, PE/M_CPU/SRAM/HBM positioning).
- `_draw_node`, `_draw_edge` — 노드 / edge 의 시각적 표현.
- `_pick_scale`, `_compute_node_sizes` — 자동 스케일링.
visualizer 는 **derived artifact** (ADR-0006) 로 분류되며, 코드 변경 시
production check 대상이 아니다. CLAUDE.md 의 "Derived Artifacts" 항목과
정합.
### D8. spec 변경의 영향 범위
| spec 필드 | 영향 | mesh 재생성 |
|---------------------------------------|-------------------|-------------|
| `system.sips.count` | SIP 갯수, node 수 | No |
| `sip.cube_mesh.w/h` | cube mesh 형상 | No |
| `cube.geometry.cube_mm.w/h` | cube 크기 (mm) | **Yes** |
| `cube.pe_layout.corners/pe_per_corner`| PE 부착 위치 | **Yes** |
| `cube.ucie.n_connections` | UCIe PHY 분배 | **Yes** |
| `cube.memory_map.hbm_mapping_mode` | HBM 분배 모드 | **Yes** |
| `cube.placement` | M_CPU/SRAM 위치 | **Yes** |
| `cube.memory_map.*` (위 제외) | HBM 용량 / BW | No |
| `*.links.*.bw_gbs` | edge bandwidth | No |
| `*.attrs.overhead_ns` | 컴포넌트 latency | No |
위 표가 D2 의 `_compute_source_hash` 입력과 일치. mesh 재생성이 필요한
변경은 `cube_mesh.yaml` 의 source_hash 가 자동 invalidate.
## Alternatives Considered
### A1. mesh 를 별도 캐시 파일 없이 매 compile 시 재생성
기각. 같은 spec 으로 여러 번 호출되는 케이스 (CLI run, probe, test) 마다
mesh 생성 비용을 다시 지불. 또한 사람이 mesh 를 inspect 할 수 있는 artifact
가 사라짐.
### A2. mesh 생성을 builder.py 에 합치기
기각 (현재). 305 줄 짜리 자체 알고리즘이며, mesh layout 의 결정 (placement-
driven router attachment, HBM exclusion zone) 이 builder 의 일반적인
node/edge 생성 책임과 다르다. 분리 유지가 단일 책임 원칙에 더 부합.
### A3. placement coordinate 를 cube 좌표 (col/row) 로 표현
기각. mm 단위 좌표가 시각화 측 (visualizer) 과 mesh layout 측 (nearest-
router 산출) 양쪽에서 일관되게 쓰인다. cube 좌표는 router grid 가 결정
되기 전까지는 정의되지 않으므로 placement 입력에 부적절.
### A4. view projection 을 lazy 하게 생성
기각 (현재). 4 개 view 의 생성 비용이 작고 (보통 < 100 ms), eager 생성이
`TopologyGraph` 를 통한 single source of truth 를 보장.
### A5. visualizer 출력 형식을 SVG 외 (PNG/PDF) 도
기각. SVG 가 vector + 텍스트 검색 가능 + 브라우저 직접 렌더가 가능한 가장
유연한 형식. PNG 변환이 필요하면 별도 도구 (rsvg-convert 등) 로 후처리.
## Consequences
- ADR-0006 의 high-level intent 가 D1D7 로 구체화되어, topology 변경
영향을 D8 표로 빠르게 가늠 가능.
- D3 의 mesh auto-layout 알고리즘이 ADR-level 에서 굳어져, 추후 새 PE
부착 패턴 (예: HBM 의 6-zone 분할) 도입 시 어느 단계가 영향받는지 명확.
- D5 의 edge kind 목록과 D7 의 view 구조가 명시되어, 새 component 종류
추가 시 (builder + router + visualizer) 어디까지 손대야 하는지 PR
reviewer 가 한눈에 파악 가능.
- D2 의 source_hash invalidation 규칙이 명시되어, cube_mesh.yaml 이 stale
하게 남는 경우 (예: bw 값만 바꿨을 때) 가 정상 동작임이 분명.