# ADR-0025: IPCQ Direction Addressing — address-based matching ## Status Proposed (Revision 2 — Address-based matching; peer_direction field dropped) ## Context ### 목표 ADR-0023의 IPCQ protocol에서 **"어느 direction pair를 통한 전송인가"의 식별**을 topology / dict-order에 의존하지 않고 **주소 기반**으로 일관되게 한다. 2-rank bidirectional ring (또는 여러 direction이 동일 peer를 가리키는 topology 일반)에서 정확히 동작하도록 한다. ### 현재 상태 (ADR-0023 D9 구현) `src/kernbench/components/builtin/pe_ipcq.py` — `_handle_meta_arrival`: ```python def _handle_meta_arrival(self, msg: IpcqMetaArrival) -> None: token = msg.token sender_key = (token.src_sip, token.src_cube, token.src_pe) for d, qp in self._queue_pairs.items(): p = qp["peer"] if (p.sip, p.cube, p.pe) == sender_key: qp["peer_head_cache"] = max(qp["peer_head_cache"], token.sender_seq + 1) # ... wake recv waiters ... return ``` `_credit_worker`도 동일한 "sender-coord-first-match" 패턴. `src/kernbench/ccl/install.py` — `reverse_direction`: ```python def reverse_direction(my_rank: int, peer_rank: int) -> str | None: for d, target in neighbor_table[peer_rank].items(): if target == my_rank: return d return None ``` ### 드러난 버그 — 2-rank bidirectional ring `ring_1d(rank, world_size=2)` → `{"E": 1, "W": 1}` (rank 0). 양쪽 방향이 같은 peer. **버그 1 (install)**: - `reverse_direction(0, 1)` → dict order로 "E" 반환 (틀림, "W"가 맞음 — opposite direction convention) - rank 0의 E entry가 `peer.rx_base_pa = rx_base(sip1, cube0, pe0, d="E")`로 설정 - tl.send(E) → data가 sip1의 E-rx buffer로 landing (should be W-rx) **버그 2 (runtime)**: - 설령 install이 올바른 주소로 설정해도, receiver의 `_handle_meta_arrival`이 sender 좌표만으로 direction 매칭 → 첫 direction (E) 승 - peer_head_cache[E] 증가, peer_head_cache[W]는 불변 - Kernel의 tl.recv(W)는 peer_head_cache[W] 대기 → 영원히 블록 → IpcqDeadlock ### 근본 원인 두 축에서 동일 문제: 1. **Install-time pairing**: "내 direction과 peer의 어느 direction이 짝인가" 결정이 dict-iteration-order에 의존 → 여러 direction이 같은 peer를 가리킬 때 fragile 2. **Runtime identification**: "어느 qp를 업데이트해야 하는가" 결정이 sender 좌표만으로 이루어짐 → direction 중복 시 ambiguous ### 해결 방향 — address-based matching 각 PE의 rx buffer는 **direction별로 고유한 주소 range**에 위치 (rx_base_pa + direction_idx × bytes_per_direction). 따라서: - **Runtime**: sender coord 대신 **dst_addr 범위**로 매칭 → unambiguous - **Install**: opposite-direction 우선 선택 heuristic (ring / mesh의 자연스러운 대칭성) - `peer_direction` 같은 이중 메타데이터 불필요 — **주소가 single source of truth** 이 설계는 **PhysAddr 전환 (ADR-0030)과 독립적**으로 작동. 현재 synthetic 주소든 PhysAddr든 direction별 range 유일성만 지켜지면 동일하게 적용 가능. --- ## Decision ### D1. Install — `reverse_direction` opposite-preference `src/kernbench/ccl/install.py`: ```python _OPPOSITE_DIR = {"E": "W", "W": "E", "N": "S", "S": "N"} def reverse_direction(my_rank: int, peer_rank: int, my_dir: str) -> str | None: """Find peer's direction that reciprocates my_dir→peer_rank. Prefer the OPPOSITE direction (E↔W, N↔S) when the peer has it pointing back to us. This matters in 2-rank bidirectional rings where both E and W on one side point to the same peer — without the preference, the first-match-wins iteration would route data into the wrong rx slot. Falls back to any direction pointing back for topologies without an opposite convention (tree_binary's parent/child). """ nt = neighbor_table[peer_rank] opp = _OPPOSITE_DIR.get(my_dir) if opp is not None and nt.get(opp) == my_rank: return opp for d, target in nt.items(): if target == my_rank: return d return None ``` 호출부: ```python for d, peer_rank in nbrs.items(): peer_dir = reverse_direction(r, peer_rank, d) # my_dir 전달 if peer_dir is None: continue ... ``` ### D2. Runtime — `_handle_meta_arrival` dst_addr 매칭 `src/kernbench/components/builtin/pe_ipcq.py`: ```python def _handle_meta_arrival(self, msg: IpcqMetaArrival) -> None: """Match incoming token to the receiver-side direction by dst_addr range. Each direction has a unique rx buffer address range (my_rx_base_pa + n_slots * slot_size). The token's dst_addr (set by the sender's IPCQ when computing peer's slot address) falls within exactly one such range. This address-based matching is unambiguous even when multiple directions have the same peer (2-rank ring). """ token = msg.token dst_addr = token.dst_addr for d, qp in self._queue_pairs.items(): base = qp["my_rx_base_pa"] size = qp["n_slots"] * qp["slot_size"] if base <= dst_addr < base + size: qp["peer_head_cache"] = max(qp["peer_head_cache"], token.sender_seq + 1) self._arrived_tokens.setdefault(d, []).append(token) waiters = self._recv_waiters.get(d, []) self._recv_waiters[d] = [] for ev in waiters: if not ev.triggered: ev.succeed() any_waiters = self._any_recv_waiters self._any_recv_waiters = [] for ev in any_waiters: if not ev.triggered: ev.succeed() return # Unknown dst_addr — diagnostic log (should not happen under correct install) ``` Sender 좌표 검사는 **제거**. `dst_addr`가 이미 direction을 결정. ### D3. Credit — `dst_rx_base_pa` 필드 추가 `src/kernbench/common/ipcq_types.py`: ```python @dataclass(frozen=True) class IpcqCreditMetadata: consumer_seq: int dst_rx_base_pa: int # NEW: 원 sender의 peer.rx_base_pa와 매칭용 # 기존 필드 (diagnostic / log 용도로 유지) src_sip: int src_cube: int src_pe: int src_direction: str ``` Credit 생성 시 (`_delayed_credit_send`): 자기 direction의 `my_rx_base_pa`를 `dst_rx_base_pa`로 실어 보냄 (이게 상대방이 sender 당시 썼던 `peer.rx_base_pa`). 수신 측 (`_credit_worker`): ```python def _credit_worker(self, env): while True: credit = yield self._credit_inbox.get() for d, qp in self._queue_pairs.items(): # peer의 rx_base_pa와 credit의 dst_rx_base_pa가 일치하는 qp 찾기 if qp["peer"].rx_base_pa == credit.dst_rx_base_pa: qp["peer_tail_cache"] = max(qp["peer_tail_cache"], credit.consumer_seq) waiters = self._send_waiters.get(d, []) self._send_waiters[d] = [] for ev in waiters: if not ev.triggered: ev.succeed() break ``` Sender 좌표 검사 제거. `dst_rx_base_pa` 매칭으로 unambiguous. ### D4. `IpcqInitEntry`에 `peer_direction` 필드를 **추가하지 않음** ADR-0025 rev 1에서 제안했던 `IpcqInitEntry.peer_direction`은 **불필요**. 이유: - Meta arrival은 dst_addr로 매칭 (D2) - Credit은 dst_rx_base_pa로 매칭 (D3) - qp에 peer_direction 저장 필요 없음 - Install은 rx_base_pa 계산 시 내부적으로만 peer_dir 사용 (`reverse_direction`) IpcqInitEntry schema 변경 없음. Rev 1 대비 **단순화**. ### D5. `IpcqDmaToken.src_direction` 유지 (diagnostic only) 기존 `src_direction` 필드는 제거하지 않는다. 다음 용도로 유지: - Logging / trace: `KERNBENCH_CCL_TRACE=1` 출력의 `(rank, t, dir, nbytes)` - Diagnostics: pointer_dump 등에서 direction 표시 - 미래 확장 여지 Runtime matching은 `dst_addr`만 사용. ### D6. Invariants (ADR-0023 I3 강화) **I3 (엄격)**: 각 방향 pair `(my_direction, peer_direction)`에 대해 my rx_base와 peer rx_base는 **별개의 direction slot**을 가리켜야 함. Install은 이를 보장해야 한다 (reverse_direction opposite-preference). **I3.1 (신규)**: 모든 qp에 대해 `qp["my_rx_base_pa"]`와 `qp["peer"].rx_base_pa`는 서로 disjoint한 주소 range를 점유한다 (다른 direction의 buffer는 절대 겹치지 않음). 이것이 D2/D3의 주소-기반 매칭의 전제. Install time에 검증 가능: ```python # ccl/install_plan.py: build_install_plans 끝에 assertion all_rx_ranges = set() for plan in plans: for pe_install in plan.pe_installs: for entry in pe_install.neighbors: r = (entry.my_rx_base_pa, entry.my_rx_base_pa + plan.n_slots * plan.slot_size) overlap = any(_ranges_overlap(r, e) for e in all_rx_ranges) assert not overlap all_rx_ranges.add(r) ``` --- ## Dependencies - **ADR-0023** (IPCQ protocol): 본 ADR은 ADR-0023의 runtime 매칭 로직 수정 (D2, D3) + install heuristic 개선 (D1). IPCQ 프로토콜의 semantic layer 변경은 없음. - **ADR-0024** (launcher): 2-rank bidirectional ring이 실제 쓰이는 경우가 ADR-0024의 ws=SIP_count 모델. 본 ADR이 그 케이스를 작동시킴. - **ADR-0030** (PhysAddr transition, stub): **독립적** — ADR-0025의 주소-기반 매칭은 현재 synthetic 주소든 PhysAddr이든 동일하게 작동. --- ## Non-goals - **IPCQ 주소 체계를 PhysAddr로 전환**: ADR-0030 scope. 본 ADR은 주소가 어떻게 인코딩되는가와 무관. - **Multi-hop routing**: ADR-0023 D5의 single-hop DMA write 전제 유지. - **Unidir ring 특수화**: `ring_1d_unidir`는 direction 하나만 있으므로 본 버그 무관. --- ## Open questions - **주소 매칭 성능**: `_handle_meta_arrival`과 `_credit_worker`가 qp를 선형 순회 (max 4 direction). 성능 영향 무시 가능 수준. 문제 시 dict lookup으로 전환 가능 (`_qp_by_rx_base`). - **`IpcqDmaToken.src_direction` 필요성 재평가**: diagnostic 용도로만 남긴 필드를 계속 유지할지, 또는 logging 외부로 분리할지. 현재는 유지. - **Install-time invariant 검증 cost**: D6의 I3.1 검증은 O(N_PE × N_direction)^2. 대형 topology에서 느려질 수 있음 → interval tree 등 자료구조로 개선 가능. 단순 구현 먼저. --- ## Test strategy ### T1. Unit — `reverse_direction` opposite-preference `tests/test_ccl_install.py` (확장): - Ring ws=2: `reverse_direction(0, 1, "E")` → "W", `reverse_direction(0, 1, "W")` → "E" - Ring ws=4: `reverse_direction(0, 1, "E")` → "W" (자연스러운 opposite) - Mesh 2×2: `reverse_direction(r, peer, "N")` → "S", "E" ↔ "W" - Tree binary: opposite 없는 direction (parent) → fallback 경로 - Non-symmetric topology: opposite가 peer에 없고 다른 direction만 있는 경우 ### T2. Runtime — `_handle_meta_arrival` dst_addr 매칭 `tests/test_pe_ipcq.py` (확장): - 2-rank pair install 후, E direction dst_addr로 meta arrival → E의 `peer_head_cache` 증가 (W는 불변) - W direction dst_addr로 meta arrival → W의 `peer_head_cache` 증가 - 잘못된 dst_addr (어느 rx range에도 속하지 않음) → 에러 또는 silent drop (결정 후 명시) ### T3. Credit — `dst_rx_base_pa` 매칭 `tests/test_pe_ipcq.py` (확장): - E direction send 후 peer가 consume → credit에 자기 W의 `my_rx_base_pa` 담아 송신 → sender의 E direction `peer_tail_cache` 증가 - W direction도 동일 ### T4. E2E — 2-rank bidirectional ring `tests/test_ipcq_e2e.py`: - 2-rank ring_1d로 tl.send(E) + tl.recv(W) pattern이 양방향으로 작동 - ADR-0024의 `test_ccl_allreduce_matrix.py`에서 ring at ws=2가 통과 ### T5. Install invariant — rx_base range disjointness `tests/test_ccl_install_plan.py` (확장): - I3.1 검증: `build_install_plans` 결과에서 모든 qp의 rx_base range가 disjoint ### T6. 회귀 - 기존 ws≥3 ring / mesh / tree 테스트 그대로 통과 - `test_pe_ipcq`, `test_ipcq_e2e` 기존 케이스 회귀 --- ## Consequences ### Positive - **단순함**: `peer_direction` 이중 메타데이터 제거. 주소가 single source of truth. - **Unambiguous matching**: 모든 topology (direction 중복 포함)에서 동작. - **Schema 변경 최소**: `IpcqInitEntry` 불변, `IpcqCreditMetadata`에 1 필드 추가. - **PhysAddr 전환 (ADR-0030) 독립**: 주소-기반 매칭은 주소 인코딩 방식과 무관. - **Diagnostic 유지**: `IpcqDmaToken.src_direction`은 로깅 용도로 존치. ### Negative - Runtime 매칭이 주소 비교로 바뀌어서 디버깅 시 "왜 peer_head_cache[E]가 아닌 W가 업데이트됐나" 같은 질문에 address range를 추적해야 함 (기존엔 direction 이름으로 충분). 해결: pointer_dump에 "direction ↔ rx_base_pa" 매핑 포함. ### Neutral - IPCQ protocol의 semantic layer (sender가 dst_addr 계산, receiver가 수신)는 불변. --- ## Affected files | File | Change | |------|--------| | `src/kernbench/ccl/install.py` | D1: `reverse_direction`에 `my_dir` 인자 추가, opposite-preference | | `src/kernbench/components/builtin/pe_ipcq.py` | D2: `_handle_meta_arrival` dst_addr 매칭 / D3: `_credit_worker` dst_rx_base_pa 매칭 / `_delayed_credit_send`가 `dst_rx_base_pa` 필드 채움 | | `src/kernbench/common/ipcq_types.py` | D3: `IpcqCreditMetadata`에 `dst_rx_base_pa` 필드 추가 | | `src/kernbench/ccl/install_plan.py` (ADR-0024 신규) | D6: I3.1 invariant 검증 (optional) | | `docs/adr/ADR-0023-ipcq-pe-collective.md` | Reference note: runtime 매칭 방식이 ADR-0025에서 바뀜 | | `tests/test_ccl_install.py` | T1 | | `tests/test_pe_ipcq.py` | T2, T3 | | `tests/test_ipcq_e2e.py` | T4 | | `tests/test_ccl_install_plan.py` | T5 |