687c98086d
Filename + lifecycle:
- ADR rename to ADR-NNNN-<cat>-title.md with 8 3-letter category prefixes
(dev / mem / lat / prog / algo / par / api / ver). Numbers stay immutable.
- ADR Lifecycle split into 3 folders, documented in CLAUDE.md Part 2:
docs/adr/ (Accepted), docs/adr-proposed/ (Proposed/Stub/Draft),
docs/adr-history/ (Superseded/Merged). Status field gains "Draft" for
retroactive docs pending verification.
Merges (one ADR per topic, no change-history annotations):
- ADR-0017 absorbs ADR-0019 (Cube NOC + per-PE HBM connectivity, 10 D-items)
- ADR-0014 absorbs ADR-0021 (PE pipeline execution model, 8 D-items incl.
TileToken self-routing and multi-op composite epilogue scope)
- ADR-0023 absorbs docs/ipcq-dma-codesign-hw.md as new "HW Realization
Notes (Informative)" section (D16-D23 + Open HW Questions). codesign-hw.md
deleted; ADR-0019/0021 moved to adr-history with one-line stub status
Retroactive documentation (G4 closures, code-verified):
- ADR-0037 forwarding component (TransitComponent: first-flit overhead,
serial worker, path-based routing, single impl/multiple names)
- ADR-0036 IO_CPU component (target_start_ns global barrier stamping,
per-cube fan-out, response aggregation)
- ADR-0035 M_CPU & M_CPU.DMA component (3 fan-out paths, DMA Resources,
target_start_ns passthrough)
- ADR-0034 HBM controller internal design (per-PC state, address-based
selection, flit-aware per-flit commit, async finalize, command-only
fallback path)
Content updates:
- ADR-0010 expanded to full CLI surface (run/probe/web), retitled
"Command Line Interface and Execution Semantics"
- ADR-0007 D2 rewritten to current state; ADR-0015 supersession notes pruned
- ADR-0005 wrapped in Decision header with D1-D5; ADR-0022 metadata
block replaced with standard Status header
- ADR-0024 trimmed to rank=SIP launcher essentials (D1-D4);
ADR-0027 cleaned of supersession history
- ADR-0033 D6 cleanup: address-based PC selection moved out of future-work
(now documented in ADR-0034 D3); related D1/D3 wording realigned
- Cross-references back-filled in 5 ADRs (G3 gaps closed)
Onboarding docs split:
- docs/onboarding/ created
- moved: hw-architecture-overview.md, latency-model.md, di-presentation.md,
ccl-author-guide{,.en}.md
- references updated in README, ADR-0023{,.en}, src/kernbench/ccl/__init__.py
Source / test / yaml: ADR-NNNN cross-references in docstrings and YAML
comments updated after the merges (ADR-0021->0014 D6, ADR-0019->0017 D8).
No behavior change.
Tooling:
- tools/verify_adr_lang_pairs.py + tests/test_verify_adr_lang_pairs.py
(ADR EN/KO pair invariant checker)
- .claude/commands/report.md tracked (/report slash command)
- .gitignore: allow .claude/commands/*.md while keeping settings files ignored
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1649 lines
70 KiB
Markdown
1649 lines
70 KiB
Markdown
# ADR-0023: PE-level IPCQ — Inter-PE Collective Communication
|
||
|
||
## Status
|
||
|
||
Accepted
|
||
|
||
## Context
|
||
|
||
### 목표
|
||
|
||
CCL (Collective Communication Library) 커널을 PE 안에서 실행할 수 있도록
|
||
PE 간 데이터 교환 인프라를 추가한다. 호스트는 그저 각 SIP에 커널을 launch만 하고,
|
||
실제 동기화와 데이터 이동은 **PE 커널 안에서 IPCQ(Inter-Process Communication
|
||
Queue)를 통해** 일어난다.
|
||
|
||
이는 NCCL이 GPU 커널 안에서 NVLink 통신을 수행하는 모델, 또는 Cerebras/Tenstorrent의
|
||
core-local 통신 큐와 유사하다. 호스트 레벨 collective(`dist.all_reduce`)는
|
||
**미래 작업**으로 미루고, 본 ADR은 커널 collective 인프라에만 집중한다.
|
||
|
||
### 풀어야 할 문제
|
||
|
||
1. PE 간 직접 데이터 이동 (peer's memory에 write)
|
||
2. 동기화 — 송신 측이 수신 측 buffer 공간을 확인해야 함 (backpressure)
|
||
3. compute traffic과 communication traffic의 자원 경쟁 (Head-of-Line blocking)
|
||
4. 호스트가 알고리즘에 따라 (ring/mesh/tree) 논리적 neighbor 토폴로지를 구성할 수 있어야 함
|
||
|
||
---
|
||
|
||
## Decision
|
||
|
||
### D1. PE_IPCQ 컴포넌트 신규 추가
|
||
|
||
PE 안에 새 컴포넌트 `PE_IPCQ`를 추가한다. PE_GEMM/PE_MATH가 PE_CPU의
|
||
sub-block을 별도 컴포넌트로 모델링하는 것과 동일한 패턴이다.
|
||
|
||
```
|
||
PE
|
||
├── PE_CPU
|
||
├── PE_SCHEDULER
|
||
├── PE_DMA
|
||
├── PE_IPCQ ← 신규
|
||
├── PE_FETCH_STORE
|
||
├── PE_GEMM
|
||
├── PE_MATH
|
||
├── PE_TCM
|
||
├── PE_MMU
|
||
```
|
||
|
||
**역할 분리** (control plane vs data plane):
|
||
- **PE_IPCQ (control plane)**: ring buffer 주소 계산, head/tail pointer 관리,
|
||
peer pointer 캐시, backpressure 결정, 4-방향 neighbor 매핑
|
||
- **PE_DMA (data plane)**: 실제 데이터를 cube_noc/UCIe/PCIE 경유로 peer 메모리에 전송
|
||
|
||
PE_IPCQ는 데이터 이동을 직접 수행하지 않고 PE_DMA에 위임한다.
|
||
|
||
### D2. Ring Buffer 모델
|
||
|
||
각 PE는 4-방향(N/S/E/W) × {tx, rx} = 총 8개의 ring buffer를 가진다.
|
||
|
||
```python
|
||
@dataclass
|
||
class IpcqQueuePair:
|
||
direction: Direction # N/S/E/W
|
||
peer: IpcqEndpoint # init 시 호스트가 설정 (D2.5)
|
||
tx_buffer_base: int # 내가 보낼 데이터의 base addr (자기 메모리)
|
||
rx_buffer_base: int # 내가 받을 데이터의 base addr (자기 메모리)
|
||
slot_size: int # tile 단위
|
||
n_slots: int # ring depth
|
||
my_head: int # 내 send 위치 (다음에 쓸 tx/peer slot)
|
||
my_tail: int # 내 recv 위치 (다음에 읽을 rx slot)
|
||
peer_head_cache: int # 캐시: peer가 마지막으로 보낸 head 위치 (D9 piggyback으로 갱신)
|
||
peer_tail_cache: int # 캐시: peer가 마지막으로 소비한 tail 위치 (D9 fast path credit으로 갱신)
|
||
```
|
||
|
||
**필드명 규약 (canonical)**: 본 ADR 전체에서 다음 4개 이름을 일관되게 사용한다.
|
||
|
||
| 필드 | 소유자 | 갱신 시점 |
|
||
|------|--------|----------|
|
||
| `my_head` | 자기 PE_IPCQ | tl.send 호출 후 즉시 (송신 추적용) |
|
||
| `my_tail` | 자기 PE_IPCQ | tl.recv 호출 후 즉시 (수신 추적용) |
|
||
| `peer_head_cache` | 자기 PE_IPCQ | IpcqMetaArrival 도착 시 (D9 piggyback) |
|
||
| `peer_tail_cache` | 자기 PE_IPCQ | IpcqCreditMetadata 도착 시 (D9 fast path) |
|
||
|
||
다른 표현(`peer_head_local`, `peer_head`, `peer_tail` 등)은 사용하지 않는다.
|
||
|
||
**Slot 단위**: fixed-size, 한 slot이 한 tile 데이터를 통째로 담는다.
|
||
descriptor 모델이 아니라 **full data embedding** 모델 (D5에서 상세).
|
||
|
||
### D2.5. PeAddress / IpcqEndpoint 스키마
|
||
|
||
`IpcqQueuePair.peer`가 가져야 할 정보를 명시한다. 송신 측 PE_IPCQ가
|
||
peer rx slot에 직접 DMA write하려면 다음을 모두 알아야 한다.
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class IpcqEndpoint:
|
||
"""송신 측이 peer's rx_buffer 주소를 계산하기 위해 필요한 모든 정보."""
|
||
sip: int # 목적지 SIP
|
||
cube: int # 목적지 cube
|
||
pe: int # 목적지 PE (cube 내 local index)
|
||
buffer_kind: str # "tcm" | "hbm" | "sram" — 어느 메모리 공간
|
||
rx_base_pa: int # peer rx_buffer base의 PA (PhysAddr.encode())
|
||
rx_base_va: int # peer rx_buffer base의 VA (선택, MMU 사용 시)
|
||
n_slots: int # peer ring depth (경계 wrap-around 계산용)
|
||
slot_size: int # peer slot 크기 (offset 계산용)
|
||
```
|
||
|
||
`IpcqQueuePair`의 `peer` 필드는 이 `IpcqEndpoint` 객체를 들고 있다.
|
||
주소 계산은:
|
||
|
||
```python
|
||
slot_idx = self.my_head % peer.n_slots
|
||
dst_pa = peer.rx_base_pa + slot_idx * peer.slot_size
|
||
```
|
||
|
||
PE_IPCQ는 이 dst_pa를 `IpcqDmaToken`의 dst_addr로 PE_DMA에 전달한다.
|
||
PE_DMA(vc_comm)는 fabric 라우팅(cube_noc/UCIe/PCIE)을 통해 dst_pa로 데이터를 전송한다.
|
||
|
||
**Endpoint 생성 시점**: backend init (D10)에서 모든 PE의 IPCQ buffer를
|
||
allocator로 할당받고, 각 rank의 neighbor table을 만들 때 peer rank의
|
||
endpoint 정보를 install한다. 즉 install 순서는:
|
||
|
||
1. **모든 rank의 IPCQ buffer 할당** (각 PE의 buffer_kind 메모리 공간에서)
|
||
2. **rank별 endpoint table 구성** (자신의 4-방향 peer가 어느 sip/cube/pe/pa를 갖는지)
|
||
3. **PE_IPCQ에 install** (`IpcqInitMsg` via fabric or sideband)
|
||
|
||
이 순서는 모든 rank가 서로의 PA를 알아야 하므로, 단계 1을 모든 rank에 대해
|
||
먼저 끝낸 후 단계 2-3을 진행한다.
|
||
|
||
### D3. 4-방향 매핑 = 논리적 ProcessGroup
|
||
|
||
PE는 4방향(N/S/E/W)을 logical port로 본다. 실제 peer 주소는 호스트 CCL init이
|
||
알고리즘에 따라 설정한다. PE 커널은 토폴로지를 알지 못하고 방향만 사용한다.
|
||
|
||
```python
|
||
# 호스트 init 예시 — 1D ring
|
||
for rank in range(world_size):
|
||
ipcq_set_neighbor(rank, "E", peer=ranks[(rank + 1) % world_size])
|
||
ipcq_set_neighbor(rank, "W", peer=ranks[(rank - 1) % world_size])
|
||
|
||
# 호스트 init 예시 — 2D mesh
|
||
for r in range(R):
|
||
for c in range(C):
|
||
ipcq_set_neighbor((r, c), "N", peer=((r - 1) % R, c))
|
||
ipcq_set_neighbor((r, c), "S", peer=((r + 1) % R, c))
|
||
ipcq_set_neighbor((r, c), "E", peer=(r, (c + 1) % C))
|
||
ipcq_set_neighbor((r, c), "W", peer=(r, (c - 1) % C))
|
||
```
|
||
|
||
PE 코드 입장에서 `tl.send(dir="E", ...)`가 어디로 가는지는 알 필요가 없다.
|
||
|
||
### D4. PE 커널 API
|
||
|
||
```python
|
||
# Send (blocking, backpressure 발생 가능)
|
||
tl.send(dir: str, src_addr: int, nbytes: int) -> None
|
||
|
||
# Recv (blocking)
|
||
data = tl.recv(dir: str) # 특정 방향에서 수신
|
||
data = tl.recv() # 4방향 round-robin, 도착한 첫 tile 반환
|
||
|
||
# Recv (non-blocking)
|
||
handle = tl.recv_async(dir: str)
|
||
data = tl.wait(handle)
|
||
```
|
||
|
||
`tl.recv()` (방향 미지정)는 IPCQ가 last_polled_dir 인덱스를 들고 있다가
|
||
다음 호출 시 그 다음 방향부터 검사하면서 데이터 있는 첫 슬롯을 반환한다.
|
||
4방향 모두 비어있으면 wait.
|
||
|
||
**Fairness는 weak fairness**: polling 시작 방향을 회전시켜 단순 편향을
|
||
완화하지만, 한 방향에 데이터가 항상 먼저 도착하면 다른 방향이 starvation될
|
||
수 있다. strict fairness가 필요한 알고리즘은 `tl.recv(dir=...)`로 방향을
|
||
명시해야 한다. (Open Questions 참조)
|
||
|
||
### D5. Single-hop DMA Write + Full-data Slot 모델
|
||
|
||
데이터는 송신 측 메모리에서 수신 측 ring slot으로 **단일 DMA 전송**으로
|
||
이동한다. 핵심 속성:
|
||
|
||
- **Single-hop**: 송신 측 IPCQ가 peer rx slot 주소를 직접 알고 있어 한 번의
|
||
fabric DMA로 데이터가 도착한다.
|
||
- **No CPU memcpy**: CPU가 데이터를 복사하지 않는다.
|
||
- **No intermediate staging**: 송신/수신 어느 쪽에도 별도 staging buffer가
|
||
없다 (송신은 자기 source 주소에서 직접, 수신은 자기 ring slot으로 직접).
|
||
|
||
(엄밀히 말하면 fabric DMA write 자체는 발생하므로 "data movement가 전혀 없다"는
|
||
의미는 아니다. NCCL의 "zero-copy"가 가리키는 것 — CPU memcpy / staging copy
|
||
부재 — 과 동일한 속성이다.)
|
||
|
||
데이터 이동 모델:
|
||
|
||
```
|
||
PE A: tl.send(E, src_addr, nbytes)
|
||
1. IPCQ가 peer rx slot 주소 계산
|
||
dst_addr = peer.rx_base_pa + (my_head % peer.n_slots) * peer.slot_size
|
||
2. backpressure: my_head - peer_tail_cache < peer.n_slots ?
|
||
(꽉 찼으면 sleep/poll)
|
||
3. PE_DMA(vc_comm)에 DMA 요청 → src_addr에서 peer의 dst_addr로 nbytes 전송
|
||
4. my_head += 1
|
||
|
||
PE B: data = tl.recv(W)
|
||
1. 내 rx_buffer[my_tail % n_slots] 위치 확인
|
||
2. 데이터 도착 대기 (D7 backpressure 모드)
|
||
3. 그 주소를 PE 커널에 반환 (또는 fetch unit으로 register file에 로드)
|
||
4. my_tail += 1
|
||
5. credit return fast path 발행 (D9) — bottleneck-BW latency 후
|
||
peer A의 peer_tail_cache 갱신
|
||
```
|
||
|
||
**핵심**: Slot에 데이터가 통째로 들어간다. PE B의 recv는 자기 rx_buffer만
|
||
읽으면 되고, A의 메모리를 read하지 않는다. 송신 측 IPCQ가 peer rx slot
|
||
주소를 알고 있으므로 직접 그 주소로 DMA write한다 (single-hop).
|
||
|
||
본인의 PE_TCM read/write는 DMA를 거치지 않는다 (PE에 직접 붙어있음).
|
||
slot이 본인 TCM에 있으면 직접 접근, 아니면 PE_DMA 경유.
|
||
|
||
### D6. Buffer 위치 — 3-way benchmark
|
||
|
||
호스트 CCL init이 IPCQ ring buffer의 메모리 위치를 결정한다:
|
||
|
||
```python
|
||
ipcq_init(
|
||
backend="ahbm",
|
||
buffer_kind="tcm" | "hbm" | "sram",
|
||
n_slots=8,
|
||
slot_size=4096,
|
||
)
|
||
```
|
||
|
||
| 위치 | 특징 | trade-off |
|
||
|------|------|-----------|
|
||
| **PE_TCM** | PE에 직접 붙음, 빠름 | 작음, PE 내부 자원과 경쟁 |
|
||
| **PE-local HBM** | 큼, DMA 경유 | latency 큼 |
|
||
| **Cube SRAM** | 중간 크기, cube-shared | cube 내 PE 간 contention |
|
||
|
||
세 위치 모두 동일 코드로 동작하며 init만 다르다. 벤치마크로 비교 가능.
|
||
|
||
**규칙**: peer가 read/write할 때는 DMA 경유. 본인이 자기 PE_TCM 읽기/쓰기는
|
||
DMA 없음.
|
||
|
||
### D7. Backpressure — 2-mode benchmark
|
||
|
||
송신 측이 peer slot full을 감지했을 때, 또는 수신 측이 데이터 미도착을
|
||
감지했을 때 어떻게 대기하는가:
|
||
|
||
| 모드 | 동작 | 모델 |
|
||
|------|------|------|
|
||
| **poll** | 캐시된 peer pointer를 주기적으로 재확인. cache update event를 폴링 | spin loop |
|
||
| **sleep** | SimPy event를 yield하고 sleep, peer가 update event를 trigger하면 wake | interrupt-like |
|
||
|
||
```python
|
||
ipcq_init(backpressure="poll" | "sleep", ...)
|
||
```
|
||
|
||
두 모드 모두 구현하여 latency/throughput trade-off를 벤치마크할 수 있다.
|
||
|
||
### D8. PE_DMA Virtual Channel
|
||
|
||
PE_DMA를 단일 큐에서 **2-channel virtual channel** 모델로 확장한다.
|
||
|
||
```
|
||
PE_DMA
|
||
├── vc_compute: GEMM/MATH의 tile load/store/writeback
|
||
└── vc_comm: IPCQ의 send 데이터
|
||
```
|
||
|
||
각 VC는 독립적인 state machine을 가진다:
|
||
- 한 채널이 stall되어도 다른 채널은 진행
|
||
- 동일 link(cube_noc, UCIe 등)는 공유하지만, link BW는 두 채널이 분할 사용
|
||
|
||
**Chunk 단위 인터리브**:
|
||
- 큰 GEMM tile DMA가 한 번에 link를 점유하지 않음
|
||
- chunk_size 단위로 진행 (예: 256B), 매 chunk마다 다른 VC와 link BW 공유
|
||
- chunk_size는 init 파라미터 (작을수록 fair, 클수록 효율)
|
||
|
||
이로써:
|
||
- HoL blocking 해소 (compute DMA 진행 중에도 IPCQ send 끼어들 수 있음)
|
||
- compute/comm overlap 자연스러움 (NVIDIA copy engine + compute SM 패턴)
|
||
- HW 모델 정합 (NoC virtual channel은 실제 HW 기법)
|
||
|
||
**첫 구현의 정확도 한계 (intentional)**:
|
||
|
||
본 ADR의 첫 구현은 **deterministic chunk-level interleave + weighted
|
||
round-robin arbitration** (default 50/50, ccl.yaml에 노출)을 채택한다.
|
||
이는 first-order approximation이며, 실제 HW의 dynamic contention/credit-based
|
||
arbitration보다는 단순화된 모델이다.
|
||
|
||
| 모델링 항목 | 첫 구현 | 향후 확장 가능 |
|
||
|------------|---------|---------------|
|
||
| VC 간 BW 분할 | 정적 weight | dynamic contention 기반 |
|
||
| Chunk 단위 인터리브 | deterministic round-robin | priority/QoS 기반 |
|
||
| Cross-VC dependency | 없음 (독립) | NoC arbiter component 추가 |
|
||
|
||
이 단순화는 functional correctness에는 영향이 없으며, latency 측정에서
|
||
heavy contention 시나리오는 실제보다 약간 optimistic한 결과를 낼 수 있다.
|
||
정밀화가 필요하면 별도 ADR로 NoC arbiter를 도입한다.
|
||
|
||
#### Token routing
|
||
|
||
- compute용 token (TileToken): 기존 PE_FETCH_STORE → PE_DMA 체이닝 그대로
|
||
- comm용 token (IpcqDmaToken, 신규): PE_IPCQ → PE_DMA로 self-routing
|
||
- PE_DMA가 token 종류로 채널 결정
|
||
|
||
```python
|
||
class PeDmaComponent:
|
||
def _process(self, env, token):
|
||
if isinstance(token, IpcqDmaToken):
|
||
yield from self._vc_comm_process(env, token)
|
||
else:
|
||
yield from self._vc_compute_process(env, token)
|
||
```
|
||
|
||
### D9. Pointer 동기화 — DMA payload piggyback
|
||
|
||
실제 하드웨어(NVLink, UCIe 등)는 DMA 메시지의 payload에 메타데이터를
|
||
piggyback하여 송수신과 함께 pointer를 갱신한다. 본 시뮬레이션도 같은 모델을
|
||
채택하여 **별도의 control 채널 없이** 메타데이터가 data와 함께 도착하도록 한다.
|
||
|
||
이 모델의 핵심 이점:
|
||
|
||
- **자동 ordering**: 메타데이터가 data와 동일 token으로 이동하므로 data가
|
||
먼저 visible해진 다음에야 head_cache가 갱신된다. 별도 ordering invariant
|
||
없이 race condition이 원천 차단된다.
|
||
- **HW 정합**: 실제 NVLink/UCIe의 piggybacked header 모델과 일치
|
||
- **컴포넌트 단순화**: 별도 IpcqPtrUpdate event 종류가 필요 없음
|
||
|
||
#### Send 흐름 (head 측 piggyback)
|
||
|
||
```
|
||
PE A: tl.send(E, src_addr, nbytes)
|
||
1. PE_IPCQ가 backpressure 체크 (peer_tail_cache 기준)
|
||
2. PE_IPCQ가 IpcqDmaToken 생성:
|
||
- data 본체 (src_addr → peer dst_addr)
|
||
- piggyback metadata: (sender_seq, src_sip/cube/pe, src_direction)
|
||
3. PE_DMA(vc_comm)에 token put
|
||
4. PE A는 자기 my_head++ (송신 추적용)
|
||
|
||
[fabric DMA: latency 만큼 진행]
|
||
|
||
PE B의 PE_DMA가 token 수신
|
||
5. data를 dst_addr (B의 rx slot)에 MemoryStore.write
|
||
6. token의 metadata를 PE B의 PE_IPCQ로 forward (PE 내부 wire, ~1 cycle)
|
||
|
||
PE B의 PE_IPCQ가 metadata 수신
|
||
7. peer_head_cache 갱신 (= A의 head 위치)
|
||
8. 대기 중인 recv (해당 direction)가 있으면 wake
|
||
```
|
||
|
||
여기서 핵심은 **5와 6은 같은 SimPy step**이라는 것이다 — DMA 완료와 동시에
|
||
data와 metadata가 atomic하게 visible해진다.
|
||
|
||
#### Recv 흐름 (credit return — fast path with bottleneck-BW latency)
|
||
|
||
수신측이 slot을 비우면 송신측은 그 사실을 알아야 한다 (backpressure 해제).
|
||
data 경로의 piggyback 모델과 달리, credit return은 일반 vc_comm fabric을
|
||
거치지 않고 **별도 fast path**로 처리한다. 이는 실제 HW의 NVLink/UCIe
|
||
credit return fast path를 추상화한 것이다.
|
||
|
||
**Latency 계산**: magic constant가 아니라 **라우팅 경로의 full path
|
||
latency** (per-node overhead + edge propagation + drain) 기준으로
|
||
산출한다.
|
||
|
||
```
|
||
credit_size_bytes = 16 (ccl.yaml: ipcq_credit_size_bytes)
|
||
path = router.find_path(self_pe, peer_pe.pe_dma)
|
||
latency = compute_path_latency_ns(path, credit_size_bytes)
|
||
= sum(edge.distance_mm * ns_per_mm)
|
||
+ sum(node_overhead_ns[n] for n in path)
|
||
+ credit_size_bytes / bottleneck_bw_on_path
|
||
```
|
||
|
||
router는 source에만 `.pe_dma`를 자동 부여하므로 destination에는 반드시
|
||
`.pe_dma` suffix를 명시해야 한다. 그렇지 않으면 `find_path`가 raise하고
|
||
credit이 0 cost로 silently teleport되는 latent bug가 발생한다 (이번
|
||
업데이트에서 수정됨).
|
||
|
||
`tl.recv`는 credit-emit 완료를 yield-from으로 기다린다 (이전에는
|
||
`env.process`로 fork). 이로써 credit-return cost가 receiver의
|
||
`pe_exec_ns`에 반영되어, IPCQ control-plane이 consume-acknowledgement를
|
||
완료한 뒤에야 recv가 kernel에 반환된다 — RAW DMA의 non-posted `tl.store`가
|
||
HBM ack-trip을 기다리는 것의 protocol-level 등가물이다.
|
||
|
||
이로써:
|
||
- **토폴로지 비례 approximation**: cube 내 credit return과 cross-SIP credit이
|
||
자동으로 다른 latency를 가짐
|
||
- **Magic constant 없음**: 모든 ns 값이 데이터 트래픽과 동일한 edge_map
|
||
및 `node_overhead_ns`에서 산출되는 `compute_path_latency_ns`로부터 옴
|
||
- **Deadlock 위험 없음**: `peer_credit_store.put`은 unbounded, B가 A에게
|
||
보낼 데이터가 없어도 credit이 자동 발행됨
|
||
- **`IPCQ ≥ raw DMA`** 보장: matched physical move에 대해 credit-emit이
|
||
RAW의 ack-trip cost와 균형을 이룸
|
||
|
||
```
|
||
PE B: tl.recv(W) → 데이터 가져감 → my_tail++
|
||
|
||
PE B의 PE_IPCQ:
|
||
1. router로 PE A까지 path 계산
|
||
2. compute_drain_ns(path, credit_size_bytes) = latency_ns
|
||
3. env.process(self._delayed_credit_send(latency_ns, peer_credit_store, my_tail))
|
||
|
||
[fast path: latency_ns 만큼 timeout, fabric vc 미사용]
|
||
|
||
PE A의 PE_IPCQ가 자기 credit_store에서 IpcqCreditMetadata 수신:
|
||
4. peer_tail_cache 갱신
|
||
5. 대기 중인 send (해당 direction)가 있으면 wake
|
||
```
|
||
|
||
#### Component 결합도 — SimPy Store 채널
|
||
|
||
PE B의 PE_IPCQ가 PE A의 PE_IPCQ를 직접 호출하지 않는다. 대신 **init 시점에
|
||
양쪽 PE_IPCQ 사이에 SimPy Store를 한 번 wire**해두고 (양방향 fast path 채널),
|
||
credit metadata는 그 store로 put한다.
|
||
|
||
```python
|
||
class PeIpcqComponent:
|
||
def _delayed_credit_send(self, env, peer_credit_store, my_tail, latency_ns):
|
||
yield env.timeout(latency_ns)
|
||
yield peer_credit_store.put(IpcqCreditMetadata(seq=my_tail, ...))
|
||
```
|
||
|
||
backend init에서 IpcqInitMsg fan-out 시 양방향 fast path channel을 함께
|
||
설치한다 (D12 IpcqInitMsg에 명시).
|
||
|
||
#### Credit return fast path의 한계
|
||
|
||
- `credit_size_bytes`는 estimate. 보통 16-64 bytes로 충분하며, 실제 HW의
|
||
credit return wire 크기를 모방한 값.
|
||
- fast path는 일반 vc_comm BW contention 모델에서 **제외**된다 (별도 채널).
|
||
실제 HW의 credit return wire는 매우 lightweight이므로 1차 근사로 합리적.
|
||
- 정밀화가 필요하면 후속 ADR에서:
|
||
- credit fast path를 별도 link로 모델링 (BW limit + contention)
|
||
- 또는 piggyback 모드로 변경 가능 (`credit_return_mode: piggyback`)
|
||
|
||
#### PE_DMA의 책임 추가
|
||
|
||
PE_DMA(vc_comm)는 token 수신 시 다음 시퀀스로 처리한다: Transaction
|
||
terminal의 BW drain을 먼저 지불하고, 이어서 atomic하게 data write +
|
||
metadata forward 수행. **data write와 metadata forward 사이에는 SimPy
|
||
yield를 두어서는 안 된다** (I6 MUST 규칙 참조). drain yield는 atomic
|
||
구간 안이 아니라 그 앞에 위치해야 한다:
|
||
|
||
```python
|
||
def _on_vc_comm_recv(self, env, txn):
|
||
# Sender PE_DMA가 찍어 둔 drain_ns (= nbytes / bottleneck_bw) 를
|
||
# 여기서 지불. atomic 구간보다 앞이어야 한다 — recv는 bytes가
|
||
# "도착"한 이후에만 깨어나야 하므로.
|
||
drain = getattr(txn, "drain_ns", 0.0)
|
||
if drain > 0:
|
||
yield env.timeout(drain)
|
||
|
||
token = txn.request
|
||
# ── ATOMIC: 두 동작 사이에 yield 금지 ──
|
||
# 1. data를 dst_addr에 write (dst의 메모리 공간은 token.dst_endpoint.buffer_kind)
|
||
data = self._memory_store.read(token.src_space, token.src_addr,
|
||
shape=..., dtype=...)
|
||
self._memory_store.write(token.dst_endpoint.buffer_kind, token.dst_addr, data)
|
||
# 2. token의 metadata를 자기 PE의 IPCQ로 forward
|
||
yield self.out_ports[self._ipcq_id].put(IpcqMetaArrival(token=token))
|
||
# ─────────────────────────────────────
|
||
```
|
||
|
||
`out_ports[ipcq_id].put`은 SimPy Store의 yield-able 호출이지만, PE 내부
|
||
wire로 capacity가 unbounded인 store를 사용하므로 즉시 완료된다 (실질적으로
|
||
single-step). 이 최종 put이 atomic 구간의 끝이며, 그 이전에 다른 yield가
|
||
삽입되면 안 된다.
|
||
|
||
#### Drain-at-inbound semantics (D9 timing model)
|
||
|
||
Transaction은 sender PE_DMA가 `drain_ns = nbytes / bottleneck_bw_on_path`
|
||
를 찍어 둔 상태로 fabric에 들어간다. 이 simulator에서 per-hop `overhead_ns`
|
||
는 각 forwarding component의 `run()` 에서 지불되고, 남은 BW drain은
|
||
Transaction의 terminal node에서 한 번 지불된다. IPCQ가 아닌 모든
|
||
Transaction (raw DMA, kernel-launch fanout 등) 은
|
||
`ComponentBase._forward_txn` 이 terminal에서 이 drain을 지불한다. IPCQ의
|
||
경우 목적지 PE_DMA가 `_handle_ipcq_inbound` 핸들러로 Transaction을
|
||
가로채서 (IPCQ 전용 data write + metadata forward를 해야 하므로)
|
||
**이 핸들러 최상단에서 drain을 명시적으로 지불해야 한다** — 그래야 IPCQ의
|
||
timing model이 다른 모든 fabric Transaction과 동일선상에 놓인다.
|
||
|
||
여기서 drain을 지불할 때의 side-effect:
|
||
|
||
- **SRC `tl.send`**: 동작 불변. sender PE_DMA가 `sub_done` 을 `yield`
|
||
하지 않으므로 fire-and-forget 의미가 보존된다. metadata forward 이후
|
||
호출되는 `sub_done.succeed()` 는 sender 입장에서 listener가 없는 이벤트.
|
||
- **DST `tl.recv`**: `drain_ns` 만큼 늦게 깨어난다. recv는 local PE_IPCQ
|
||
의 `IpcqMetaArrival` 수신 시에만 wake되며, metadata forward가 drain
|
||
이후로 이동했으므로 recv는 bandwidth까지 포함한 전체 fabric transfer
|
||
시간을 관측하게 된다.
|
||
|
||
물리적 그림과 일치: send는 dispatch하고 바로 반환; recv는 bytes가 실제로
|
||
자신의 inbox로 drain될 때까지 대기.
|
||
|
||
#### Backpressure latency 정확도
|
||
|
||
backpressure 해제까지 걸리는 시간:
|
||
|
||
- **데이터 send 측 latency** = full fabric DMA (data + piggyback metadata 함께)
|
||
- **Credit return 측 latency** = fast path with bottleneck-BW
|
||
(`credit_size_bytes / bottleneck_bw_on_path`)
|
||
|
||
| 시나리오 | 모델링된 latency | 실제 HW와의 관계 |
|
||
|---------|----------------|----------------|
|
||
| Cube 내 (fast link) | 작음 (bottleneck = cube_noc BW) | topology-aware approximation |
|
||
| Cross-cube (UCIe) | 중간 (bottleneck = UCIe BW) | topology-aware approximation |
|
||
| Cross-SIP (PCIE) | 큼 (bottleneck = PCIE BW) | topology-aware approximation |
|
||
|
||
별도 magic latency 파라미터 없이 토폴로지에 비례한 first-order
|
||
approximation이 자동으로 반영된다. 실제 HW와 정확히 일치하지는 않지만
|
||
(credit fast path는 contention 모델에서 제외, credit_size_bytes는 estimate),
|
||
magic constant 모델보다 훨씬 의미 있는 비교 가능. 정밀화는 후속 ADR로
|
||
넘긴다.
|
||
|
||
### D9.5. ADR-0020 (2-Pass) 통합
|
||
|
||
`tl.send/recv`는 ADR-0020의 2-pass 모델과 통합되어야 한다. Phase 1은
|
||
타이밍과 실제 데이터 이동(MemoryStore) 모두 모델링하고, Phase 2는 op_log
|
||
기반 정합성 검증을 가능케 한다.
|
||
|
||
#### Phase 1 (타이밍 + 데이터 이동)
|
||
|
||
D9는 head 갱신과 tail 갱신을 다른 메커니즘으로 모델링한다:
|
||
|
||
- **Send-side (head update)** — DMA payload piggyback. data write와 metadata
|
||
forward가 동일 SimPy step에 일어나므로 자동으로 atomic visibility 보장.
|
||
- **Recv-side (tail credit return)** — fast path SimPy Store 채널.
|
||
bottleneck-BW 기반 latency 후 peer_tail_cache 갱신.
|
||
|
||
두 메커니즘을 합쳐서 전체 ring buffer pointer 일관성을 유지한다.
|
||
|
||
**send 시**:
|
||
|
||
1. PE_IPCQ가 backpressure 체크 (peer_tail_cache 기준)
|
||
2. PE_IPCQ가 IpcqDmaToken 생성 (data + piggyback metadata) → PE_DMA(vc_comm)에 put
|
||
3. PE_DMA가 fabric DMA 시뮬레이션 (latency 진행)
|
||
4. **DMA 완료와 동일한 SimPy step에 atomic 시퀀스**:
|
||
- **MemoryStore.write(buffer_kind, dst_pa, data)** — single-hop DMA write
|
||
- 수신측 PE_IPCQ에 metadata forward → peer_head_cache 갱신 → 대기 recv wake
|
||
5. **op_log 기록**: `OpRecord(op_kind="ipcq", op_name="send", params={src_space, src_addr, dst_space, dst_addr, nbytes, dir, dtype, shape, sender_seq})`
|
||
- `dst_space`는 `token.dst_endpoint.buffer_kind`에서 derive된 값이다
|
||
(별도 token 필드가 아니다). dst_addr은 `token.dst_addr`.
|
||
|
||
**recv 시**:
|
||
|
||
1. PE_IPCQ가 (peer_head_cache > my_tail) AND (MemoryStore.has(slot_addr)) 조건 대기
|
||
(D9 piggyback 모델에서는 두 조건이 같은 step에 truthy가 되지만, defensive check)
|
||
2. 조건 만족 시: `slot_addr = my_rx_base + slot_idx * slot_size`
|
||
3. **두 가지 모드** (`recv_mode`로 op_log에 기록):
|
||
- **`return_slot`** (default): slot_addr을 그대로 PE 커널에 반환.
|
||
데이터 복사 없음. 커널이 slot 메모리를 직접 사용한다.
|
||
- **`copy_to_dst`**: 호출 시 dst_addr이 지정된 경우. slot 데이터를 읽어서
|
||
dst_addr에 write. `data = memory_store.read(...)`; `memory_store.write(dst_space, dst_addr, data)`
|
||
4. PE_IPCQ가 my_tail++, fast path credit return을 발행 (D9 — vc_comm
|
||
fabric을 거치지 않고 별도 SimPy Store 채널로 bottleneck-BW latency 후
|
||
peer 측 peer_tail_cache 갱신)
|
||
5. **op_log 기록**: `OpRecord(op_kind="ipcq", op_name="recv", params={recv_mode, src_space, src_addr, dst_space, dst_addr, nbytes, dir, dtype, shape, consumer_seq})`
|
||
- `recv_mode="return_slot"`: src_space/src_addr가 slot 위치, dst_addr=None
|
||
- `recv_mode="copy_to_dst"`: src_space/src_addr가 slot 위치, dst_space/dst_addr가 사용자 지정 위치
|
||
|
||
#### Phase 2 (op_log replay)
|
||
|
||
DataExecutor가 `op_kind="ipcq"` 레코드를 만나면:
|
||
|
||
- **send**: src → dst (peer rx slot)로 ndarray를 idempotent하게 write
|
||
- **recv (`recv_mode="return_slot"`)**: no-op. slot 데이터는 Phase 1에서
|
||
이미 적절한 위치에 있으며, 커널이 해당 slot 메모리를 직접 사용함.
|
||
- **recv (`recv_mode="copy_to_dst"`)**: slot → dst_addr로 ndarray를 idempotent
|
||
하게 copy
|
||
|
||
본질적으로 IPCQ는 **데이터 이동**만 하므로 Phase 2가 추가로 계산할 것은 없다.
|
||
DataExecutor의 GEMM/Math가 그 데이터를 사용하면 자동으로 정합성이 검증된다.
|
||
|
||
```python
|
||
class DataExecutor:
|
||
def _execute_op(self, op):
|
||
if op.op_kind == "ipcq":
|
||
self._execute_ipcq(op)
|
||
elif op.op_kind == "memory":
|
||
...
|
||
elif op.op_kind == "gemm":
|
||
...
|
||
|
||
def _execute_ipcq(self, op):
|
||
"""IPCQ ops are data movement; Phase 1 already wrote to MemoryStore."""
|
||
p = op.params
|
||
if op.op_name == "send":
|
||
data = self.store.read(p["src_space"], p["src_addr"],
|
||
shape=p["shape"], dtype=p["dtype"])
|
||
self.store.write(p["dst_space"], p["dst_addr"], data)
|
||
elif op.op_name == "recv":
|
||
if p.get("recv_mode") == "copy_to_dst":
|
||
data = self.store.read(p["src_space"], p["src_addr"],
|
||
shape=p["shape"], dtype=p["dtype"])
|
||
self.store.write(p["dst_space"], p["dst_addr"], data)
|
||
# recv_mode == "return_slot": no-op (data already in slot)
|
||
```
|
||
|
||
#### `--verify-data` 흐름 (CCL 커널)
|
||
|
||
```
|
||
1. kernbench run --bench ccl_allreduce --verify-data
|
||
2. backend init → IPCQ buffers 할당, neighbor table install
|
||
3. 모든 rank greenlet 동시 실행
|
||
4. 각 PE 커널이 tl.send/recv → MemoryStore에 데이터 누적
|
||
5. 시뮬레이션 완료 후 DataExecutor.run() → ipcq op 멱등 replay (no-op)
|
||
6. 벤치마크가 print(out) 또는 out.data 비교 → 정합성 확인
|
||
```
|
||
|
||
벤치 작성자는 `out.data`로 결과를 읽고 expected와 비교하면 된다 (ADR-0020 D7
|
||
Tensor.data 패턴).
|
||
|
||
### D10. 호스트 CCL Init은 PyTorch 패턴 그대로
|
||
|
||
호스트 코드는 실제 PyTorch distributed 코드와 동일하게 유지한다.
|
||
`init_process_group`은 backend 객체만 만들고, IPCQ 설정 (neighbor topology,
|
||
buffer_kind, backpressure 등)은 받지 않는다.
|
||
|
||
```python
|
||
# benches/ccl_allreduce.py — 실제 PyTorch와 동일한 호스트 코드
|
||
def run_rank(rank, world_size, torch):
|
||
dist = torch.distributed
|
||
dist.init_process_group(backend="ahbm", world_size=world_size, rank=rank)
|
||
|
||
tensor = torch.zeros((M, K), dtype="f16", dp=...)
|
||
|
||
from kernbench.ccl.algorithms import ring_allreduce
|
||
torch.launch("ring_allreduce", ring_allreduce.kernel, tensor, rank, world_size)
|
||
```
|
||
|
||
IPCQ 설정은 backend가 **init_process_group 시점에** `ccl.yaml`을 읽고 즉시
|
||
PE_IPCQ neighbor table을 install한다. 호스트 코드는 IPCQ를 인지할 필요가 없다.
|
||
|
||
벤치마크 하나는 하나의 알고리즘을 사용하는 것을 가정하며, 사용할 알고리즘은
|
||
`ccl.yaml`의 `defaults.algorithm` 으로 지정한다 (D11). 호스트 코드 변경 없이
|
||
ccl.yaml만 수정하여 다른 알고리즘으로 교체할 수 있다.
|
||
|
||
#### Init 흐름 (eager)
|
||
|
||
1. `init_process_group(backend="ahbm")` 호출
|
||
2. backend가 `ccl.yaml` 로드 → `defaults.algorithm` 결정
|
||
3. `algorithms[<algo>]`에서 topology + buffer_kind + backpressure + slot/size 결정
|
||
4. **즉시** 모든 PE의 PE_IPCQ에 neighbor table을 install (sideband 또는 fabric `IpcqInitMsg`)
|
||
5. 이후 `torch.launch(kernel_name, ...)`는 일반 launch와 동일하게 처리
|
||
(CCL kernel이든 아니든 PE_IPCQ는 이미 준비됨)
|
||
|
||
### D11. CCL 설정 파일 (`ccl.yaml`)
|
||
|
||
IPCQ 설정과 알고리즘 metadata는 별도 YAML 파일에 둔다.
|
||
`components.yaml`/`topology.yaml`과 같은 패턴을 유지하며, 변경 이력이 코드처럼
|
||
추적 가능하다.
|
||
|
||
벤치마크 한 번 실행은 한 알고리즘만 사용한다 (`defaults.algorithm`).
|
||
다른 알고리즘으로 교체하려면 `ccl.yaml`의 `defaults.algorithm` 만 바꾸면 된다.
|
||
|
||
```yaml
|
||
# ccl.yaml — CCL backend (ahbm) configuration
|
||
#
|
||
# 이 파일은 init_process_group(backend="ahbm") 시점에 로드되며,
|
||
# defaults.algorithm 으로 지정된 알고리즘에 따라 PE_IPCQ neighbor table을
|
||
# install한다. 호스트 코드는 IPCQ 설정을 인지하지 않는다.
|
||
|
||
defaults:
|
||
# 이번 벤치 실행에서 사용할 알고리즘. algorithms 섹션에 정의된 것 중 하나.
|
||
algorithm: ring_allreduce
|
||
|
||
# IPCQ ring buffer가 위치할 메모리.
|
||
# tcm — PE-local TCM (작지만 빠름, PE 내부 자원과 경쟁)
|
||
# hbm — PE-local HBM (큼, DMA latency 큼)
|
||
# sram — Cube-shared SRAM (중간 크기, cube 내 PE 간 contention)
|
||
buffer_kind: tcm
|
||
|
||
# send/recv가 peer slot full / data 미도착을 만났을 때의 대기 방식.
|
||
# poll — peer pointer 캐시를 spin loop로 재확인
|
||
# sleep — SimPy event yield 후 wakeup 대기 (interrupt-like)
|
||
backpressure: sleep
|
||
|
||
# Ring buffer depth (한 방향당 slot 개수). 클수록 in-flight 가능, 메모리 ↑
|
||
n_slots: 8
|
||
|
||
# Slot 하나의 크기 (bytes). 한 tile을 통째로 담을 수 있는 크기여야 함.
|
||
slot_size: 4096
|
||
|
||
# PE_DMA virtual channel chunk 크기 (bytes). 작을수록 fair, 클수록 효율.
|
||
# IPCQ traffic과 compute traffic 사이의 인터리브 granularity (D8 참조).
|
||
vc_chunk_size: 256
|
||
|
||
# Credit return fast path 메시지 크기 (bytes). 실제 HW의 credit return wire
|
||
# 크기를 모방. backend가 라우팅 경로의 bottleneck BW를 보고 latency를
|
||
# 계산한다 (D9 참조). 보통 16-64로 충분.
|
||
ipcq_credit_size_bytes: 16
|
||
|
||
algorithms:
|
||
# ── 알고리즘 정의 ─────────────────────────────────────────────────
|
||
# 각 entry는 알고리즘 모듈과 그 알고리즘이 요구하는 topology를 명시한다.
|
||
# 알고리즘별 default override 가능 (buffer_kind, backpressure 등).
|
||
|
||
ring_allreduce:
|
||
# PE 커널이 정의된 모듈. `kernel(t_ptr, rank, world_size, tl)` 함수를 export.
|
||
module: kernbench.ccl.algorithms.ring_allreduce
|
||
|
||
# 이 알고리즘이 요구하는 neighbor topology. builtin 이름 또는 "custom".
|
||
# ring_1d — 1D 양방향 ring (E/W)
|
||
# ring_1d_unidir — 1D 단방향 ring (E only)
|
||
# mesh_2d — 2D mesh (N/S/E/W)
|
||
# tree_binary — binary tree (parent/children direction)
|
||
# custom — 모듈의 neighbors(rank, world_size) 함수 사용
|
||
topology: ring_1d
|
||
|
||
tree_allreduce:
|
||
module: kernbench.ccl.algorithms.tree_allreduce
|
||
topology: tree_binary
|
||
# 알고리즘별 override (이 알고리즘만 hbm 사용)
|
||
buffer_kind: hbm
|
||
|
||
custom_mesh:
|
||
module: kernbench.ccl.algorithms.custom_mesh
|
||
topology: custom # 모듈이 직접 neighbors() 함수 제공
|
||
```
|
||
|
||
#### 알고리즘 모듈 구조
|
||
|
||
알고리즘 모듈은 두 개의 hook을 export한다 — `kernel`은 필수, `neighbors`는 선택.
|
||
|
||
```python
|
||
# src/kernbench/ccl/algorithms/ring_allreduce.py
|
||
|
||
def kernel(t_ptr, rank, world_size, tl):
|
||
"""필수 — PE 커널.
|
||
|
||
IPCQ 설정은 backend가 ccl.yaml + neighbors() 결과로 install한 상태이다.
|
||
커널은 그저 4-방향 send/recv API만 사용하면 된다.
|
||
"""
|
||
for step in range(world_size - 1):
|
||
...
|
||
tl.send(dir="E", ...)
|
||
data = tl.recv(dir="W")
|
||
|
||
|
||
def neighbors(rank, world_size, neighbor_map):
|
||
"""선택 — neighbor table override hook.
|
||
|
||
backend는 ccl.yaml의 topology 필드에 따라 builtin neighbor_map을 생성한 뒤,
|
||
이 함수가 정의되어 있으면 호출하여 결과를 override 한다.
|
||
|
||
Args:
|
||
rank: 이 rank의 인덱스
|
||
world_size: 전체 rank 수
|
||
neighbor_map: ccl.yaml의 topology 필드가 만든 builtin 매핑
|
||
예: ring_1d → {"E": (rank+1)%ws, "W": (rank-1)%ws}
|
||
mutable dict — 직접 수정 가능
|
||
|
||
Returns:
|
||
dict | None:
|
||
dict — neighbor_map을 override한 결과
|
||
None — override 안 함, neighbor_map 그대로 사용
|
||
"""
|
||
return None # 또는 수정 후 반환
|
||
```
|
||
|
||
#### `neighbors` override 패턴
|
||
|
||
대부분의 알고리즘은 builtin topology만으로 충분하므로 `neighbors` 정의가 필요 없다.
|
||
정의가 필요한 경우의 패턴:
|
||
|
||
**Pattern A — builtin을 base로 일부만 수정**:
|
||
```python
|
||
def neighbors(rank, world_size, neighbor_map):
|
||
# 짝수 rank만 W 사용
|
||
if rank % 2 == 1:
|
||
neighbor_map.pop("W", None)
|
||
return neighbor_map
|
||
```
|
||
|
||
**Pattern B — 완전히 새로 만들기 (skip-connection ring 등)**:
|
||
```python
|
||
def neighbors(rank, world_size, neighbor_map):
|
||
# neighbor_map은 무시하고 새로 작성
|
||
return {"E": (rank + 2) % world_size}
|
||
```
|
||
|
||
#### Builtin topology generators
|
||
|
||
`ccl.yaml`의 `topology` 필드가 다음 builtin 이름이면 backend가 알아서 처리:
|
||
|
||
| topology | 설명 | direction set |
|
||
|----------|------|---------------|
|
||
| `ring_1d` | 1D 양방향 ring | E, W |
|
||
| `ring_1d_unidir` | 1D 단방향 ring | E only |
|
||
| `mesh_2d` | 2D mesh | N, S, E, W |
|
||
| `tree_binary` | binary tree (root = rank 0) | parent, child_left, child_right |
|
||
| `none` | 빈 매핑 — 알고리즘이 `neighbors()`로 처음부터 작성 | (없음) |
|
||
|
||
`topology: none`은 builtin이 빈 dict를 반환하므로 알고리즘의 `neighbors()`가
|
||
처음부터 매핑을 만들어야 한다.
|
||
|
||
#### 알고리즘 추가 절차
|
||
|
||
1. `src/kernbench/ccl/algorithms/<algo>.py`에 `kernel` 함수 작성
|
||
2. `ccl.yaml`의 `algorithms` 섹션에 entry 추가 (`module`, `topology`)
|
||
3. (선택) 같은 모듈에 `neighbors()` 함수 추가하여 builtin override
|
||
4. `defaults.algorithm`을 새 알고리즘으로 설정하면 적용
|
||
|
||
호스트 코드는 손대지 않는다.
|
||
|
||
### D12. 메시지 / 토큰 스키마
|
||
|
||
본 ADR이 추가하는 모든 메시지/토큰의 필드를 명시한다. 구현 시 이 정의를
|
||
`src/kernbench/common/pe_commands.py`와 `src/kernbench/runtime_api/kernel.py`에
|
||
그대로 추가한다.
|
||
|
||
#### `IpcqInitMsg` (sideband, init 시 fan-out)
|
||
|
||
backend가 모든 PE의 PE_IPCQ에 neighbor table을 install하기 위해 사용한다.
|
||
구조는 `MmuMapMsg`와 유사 (target_sips, target_cubes, target_pe + entries).
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class IpcqInitEntry:
|
||
direction: str # "N" | "S" | "E" | "W"
|
||
peer: IpcqEndpoint # D2.5 참조
|
||
my_rx_base_pa: int # 자신의 rx_buffer base
|
||
my_rx_base_va: int # 선택
|
||
n_slots: int
|
||
slot_size: int
|
||
# Credit fast path 채널 (D9).
|
||
# 계약: 이 필드는 반드시 simpy.Store 인스턴스이며, IpcqCreditMetadata
|
||
# 객체만을 받는 receive endpoint이다 (peer's PE_IPCQ가 자기 입력 큐로
|
||
# 사용). 송신 측 PE_IPCQ는 _delayed_credit_send에서 이 store에 직접
|
||
# IpcqCreditMetadata를 put한다. 다른 객체 type을 put해서는 안 된다.
|
||
# backend init 시 양방향 SimPy Store가 한 번 wire되며 이후 변경 불가.
|
||
peer_credit_store: "simpy.Store[IpcqCreditMetadata]"
|
||
|
||
@dataclass(frozen=True)
|
||
class IpcqInitMsg:
|
||
correlation_id: str
|
||
request_id: str
|
||
target_sips: tuple[int, ...]
|
||
target_cubes: tuple[int, ...]
|
||
target_pe: int | tuple[int, ...] | str
|
||
entries: tuple[IpcqInitEntry, ...] # 이 PE의 4-방향 entry
|
||
backpressure_mode: str # "poll" | "sleep"
|
||
buffer_kind: str # "tcm" | "hbm" | "sram"
|
||
credit_size_bytes: int # D9 fast path latency 계산용 (default 16)
|
||
```
|
||
|
||
**Credit fast path channel wiring**: backend init이 모든 PE의 PE_IPCQ에
|
||
양방향 fast path 채널을 한 번 설치한다. PE A의 IpcqInitEntry(direction=E)에
|
||
PE B의 credit-receive Store reference를 넣어 송신 측이 직접 put할 수 있게
|
||
한다 (별도 fabric routing 없음).
|
||
|
||
#### `IpcqSendCmd` (PE_CPU → PE_IPCQ)
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class IpcqSendCmd:
|
||
direction: str # 어느 방향으로 보낼지
|
||
src_addr: int # 보낼 데이터의 원본 주소 (TCM/HBM)
|
||
src_space: str # "tcm" | "hbm" | "sram"
|
||
nbytes: int
|
||
shape: tuple[int, ...] # data shape (op_log/MemoryStore용)
|
||
dtype: str
|
||
handle_id: str # completion 추적용
|
||
data_op: bool = True # ADR-0020 op_log 기록 대상
|
||
```
|
||
|
||
#### `IpcqRecvCmd` (PE_CPU → PE_IPCQ)
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class IpcqRecvCmd:
|
||
direction: str | None # None이면 round-robin (weak fairness, D4)
|
||
# recv_mode: 두 가지 동작 모드
|
||
# "return_slot" — slot 주소를 그대로 PE 커널에 반환 (default, zero-copy)
|
||
# "copy_to_dst" — slot 데이터를 dst_addr에 copy 후 반환
|
||
recv_mode: str = "return_slot"
|
||
# dst_addr / dst_space는 recv_mode="copy_to_dst"일 때만 사용됨
|
||
dst_addr: int = 0
|
||
dst_space: str = ""
|
||
shape: tuple[int, ...] = () # data shape (op_log/MemoryStore용)
|
||
dtype: str = ""
|
||
handle_id: str = ""
|
||
blocking: bool = True # blocking vs non-blocking
|
||
data_op: bool = True
|
||
```
|
||
|
||
#### `IpcqDmaToken` (PE_IPCQ → PE_DMA, vc_comm 채널)
|
||
|
||
D9의 piggyback 모델에 따라 token이 data + head metadata를 함께 담아
|
||
fabric을 따라 이동한다. 수신 측 PE_DMA가 도착 시점에 data를 dst_addr에
|
||
write하고 metadata를 PE_IPCQ로 forward한다 (atomic).
|
||
|
||
```python
|
||
@dataclass
|
||
class IpcqDmaToken:
|
||
# ── Data movement (single-hop DMA write) ──
|
||
src_addr: int # 자기 메모리 주소
|
||
src_space: str
|
||
dst_addr: int # peer rx slot 주소 (이미 계산됨)
|
||
dst_endpoint: IpcqEndpoint # 라우팅용 (sip/cube/pe)
|
||
nbytes: int # data 크기
|
||
handle_id: str # 완료 시 송신 측 PE_IPCQ로 알림
|
||
|
||
# ── Piggyback metadata (수신측 PE_IPCQ가 자동 갱신할 정보) ──
|
||
sender_seq: int # 단조 증가 sequence number
|
||
# peer가 자기 head_cache로 사용
|
||
src_sip: int # 송신 측 (수신측이 어느 peer인지 식별)
|
||
src_cube: int
|
||
src_pe: int
|
||
src_direction: str # 송신측 기준 방향 (수신측은 reverse 매핑으로 자기 direction 결정)
|
||
|
||
data_op: bool = True # ADR-0020 op_log 기록 대상
|
||
```
|
||
|
||
PE_DMA는 token type으로 채널 결정 (D8): TileToken → vc_compute, IpcqDmaToken → vc_comm.
|
||
|
||
**수신 측 PE_DMA의 처리** (vc_comm 도착 시):
|
||
|
||
```python
|
||
def _vc_comm_arrival(self, env, token: IpcqDmaToken):
|
||
# 1. data를 dst_addr에 write (data와 metadata atomic visibility)
|
||
if self._memory_store is not None:
|
||
data = self._memory_store.read(token.src_space, token.src_addr,
|
||
shape=..., dtype=...)
|
||
self._memory_store.write(token.dst_endpoint.buffer_kind, token.dst_addr, data)
|
||
# 2. metadata를 자기 PE의 IPCQ로 forward (PE 내부 wire, 같은 step)
|
||
yield self.out_ports[self._ipcq_id].put(IpcqMetaArrival(token=token))
|
||
```
|
||
|
||
PE_IPCQ는 `IpcqMetaArrival`을 받아 sender_seq를 보고 peer_head_cache를 갱신한다.
|
||
|
||
#### `IpcqCreditMetadata` (PE_IPCQ → peer PE_IPCQ, fast path 채널)
|
||
|
||
Credit return은 D9의 fast path 모델에 따라 vc_comm fabric을 거치지 않고
|
||
**별도의 SimPy Store 채널**로 전달된다. backend init 시 양방향 channel이
|
||
미리 wire되며, latency는 bottleneck-BW 기반으로 계산된다.
|
||
|
||
```python
|
||
@dataclass(frozen=True)
|
||
class IpcqCreditMetadata:
|
||
"""Credit return — recv 측 → send 측 fast path."""
|
||
consumer_seq: int # my_tail (recv 측의 새 tail)
|
||
src_sip: int # 누가 보냈는지 (수신 측이 어느 peer credit인지 식별)
|
||
src_cube: int
|
||
src_pe: int
|
||
src_direction: str # 송신 측 기준 방향 (수신 측은 reverse 매핑)
|
||
```
|
||
|
||
**전송 흐름**:
|
||
|
||
```python
|
||
class PeIpcqComponent:
|
||
def _delayed_credit_send(self, env, peer_credit_store, my_tail, latency_ns):
|
||
yield env.timeout(latency_ns)
|
||
yield peer_credit_store.put(IpcqCreditMetadata(
|
||
consumer_seq=my_tail, src_sip=..., src_cube=..., src_pe=...,
|
||
src_direction=...,
|
||
))
|
||
```
|
||
|
||
`latency_ns`는 D9에 정의된 대로:
|
||
|
||
```python
|
||
path = self.ctx.router.find_path(self_pe_prefix, peer_pe_prefix)
|
||
latency_ns = self.ctx.compute_drain_ns(path, credit_size_bytes)
|
||
```
|
||
|
||
**별도의 IpcqPtrUpdate 이벤트는 없다** — head 갱신은 D9 piggyback 모델로,
|
||
tail 갱신은 D9 fast path SimPy Store 채널로 처리된다.
|
||
|
||
### D13. 테스트 전략
|
||
|
||
단위/통합/regression 테스트를 명시한다.
|
||
|
||
#### T1. 단위 테스트 (component-level)
|
||
|
||
- **PE_IPCQ 단위** (`tests/test_pe_ipcq.py`):
|
||
- send: backpressure 미발생 시 즉시 PE_DMA로 token forward
|
||
- send: peer slot full → backpressure (poll/sleep 모드별)
|
||
- send: peer credit return (IpcqCreditMetadata) 도착 후 backpressure 해제
|
||
- recv: 데이터 도착 시 즉시 반환
|
||
- recv: 데이터 미도착 → wait → IpcqMetaArrival (D9 piggyback) 수신 시 wake
|
||
- recv (round-robin): 4-방향 중 도착한 첫 데이터 반환 (weak fairness)
|
||
- 잘못된 방향 → IpcqInvalidDirection 예외
|
||
|
||
- **PE_DMA virtual channel** (`tests/test_pe_dma_vc.py`):
|
||
- vc_compute / vc_comm 독립 진행 (한 채널 stall 시 다른 채널 진행)
|
||
- chunk-level 인터리브 verification
|
||
- link BW 분할 (50/50 또는 weighted)
|
||
|
||
- **builtin topology** (`tests/test_ccl_topologies.py`):
|
||
- ring_1d/mesh_2d/tree_binary 각각 (rank, world_size) → neighbor dict 정합성
|
||
- mesh_2d non-square → ValueError
|
||
- resolve_topology(custom, module) → module.neighbors 반환
|
||
|
||
#### T2. 통합 테스트 (E2E send/recv)
|
||
|
||
- **`tests/test_ipcq_e2e.py`**:
|
||
- 2-rank ring: rank 0 send(E) → rank 1 recv(W) → 데이터 정합성
|
||
- 4-rank ring: 양방향 send/recv 동시 진행, deadlock 없음
|
||
- mesh_2d 4×4: N/S/E/W 4방향 동시 send/recv
|
||
|
||
- **CCL kernel + 2-pass** (`tests/test_ipcq_2pass.py`):
|
||
- greenlet 모드 + IPCQ → op_log에 ipcq 레코드 생성 검증
|
||
- DataExecutor가 ipcq op 처리 후 결과 정합성 (`out.data` 확인)
|
||
|
||
#### T3. Backend init 테스트 (`tests/test_ccl_backend_ipcq.py`)
|
||
|
||
- ccl.yaml 로드 → `defaults.algorithm` 추출
|
||
- builtin topology → IpcqInitMsg fan-out
|
||
- IpcqEndpoint의 PA가 모든 PE에서 일관 (rank A의 peer E의 rx_base_pa = rank A+1의 자기 rx_base_pa)
|
||
- buffer_kind 별 메모리 할당 (tcm/hbm/sram)
|
||
|
||
#### T4. Regression
|
||
|
||
- 기존 401 tests 전부 PASS
|
||
- ADR-0020 통합으로 인한 op_log/DataExecutor 영향 없음 (CCL 미사용 벤치)
|
||
|
||
#### T5. 성능 / overhead
|
||
|
||
- 단일 send/recv pair latency = (DMA latency) + (IPCQ overhead)
|
||
- 비교: 같은 nbytes의 일반 PE_DMA write와 거의 동일해야 함 (IPCQ overhead < 100 ns)
|
||
|
||
### D14. Invariants & Failure Modes
|
||
|
||
CCL 인프라에서 흔히 발생하는 hang/오류 상황을 명시하고, 대응 방식을 정의한다.
|
||
|
||
#### Invariants (시뮬레이션이 보장해야 하는 것)
|
||
|
||
I1. **Slot lifecycle exactly-once**: 한 send → 정확히 한 recv. 중복 send나
|
||
중복 recv는 sequence 오류로 간주.
|
||
|
||
I2. **Pointer monotonicity**: my_head, my_tail은 단조 증가 (감소 없음).
|
||
sender_seq는 송신 측에서 단조 증가, 수신 측 cache 갱신도 단조 증가.
|
||
|
||
I3. **Endpoint consistency**: rank A의 IpcqEndpoint(direction=E)의 peer가
|
||
rank B라면, rank B의 IpcqEndpoint(reverse(E))의 peer는 rank A여야 함.
|
||
backend init 시 검증.
|
||
|
||
I4. **buffer_kind consistency**: 한 ProcessGroup 내 모든 PE의 buffer_kind는
|
||
동일 (mixed kind는 supported 안 함, 첫 구현). 검증 실패 시 init 에러.
|
||
|
||
I5. **op_log ordering**: send → DMA 완료 → recv 가능. op_log의 t_start
|
||
순서가 이 인과관계를 위배하지 않음.
|
||
|
||
I6. **Atomic data + metadata visibility (MUST)**: 본 ADR의 correctness 핵심
|
||
조건이다. 수신 측에서 data write (MemoryStore.write)와 metadata forward
|
||
(peer_head_cache 갱신)는 동일한 SimPy step에 일어나야 한다. control이
|
||
data를 앞지를 수 없다.
|
||
|
||
**구현 규칙 (MUST)**:
|
||
- PE_DMA의 vc_comm token 도착 처리(`_vc_comm_arrival`)는 다음 두 동작
|
||
사이에 **어떤 SimPy yield도 두어서는 안 된다**:
|
||
1. `MemoryStore.write(token.dst_endpoint.buffer_kind, token.dst_addr, data)`
|
||
2. PE_IPCQ에 `IpcqMetaArrival` forward
|
||
- 두 동작은 동일 SimPy event callback 내에서 연속 실행되어야 한다.
|
||
- 코드 리뷰에서 이 사이에 `yield` (또는 `yield from`)을 추가하는 것은
|
||
correctness 위반으로 reject한다.
|
||
|
||
이 규칙을 위반하면 다른 SimPy process가 끼어들어 head_cache가 data
|
||
visibility보다 먼저 또는 늦게 보이는 race condition이 발생한다.
|
||
|
||
I7. **MemoryStore slot existence ↔ pointer**: I6의 결과로,
|
||
`peer_head_cache > my_tail`이 truthy가 되는 step과 `MemoryStore.has(slot_addr)`
|
||
이 truthy가 되는 step이 동일하다. recv는 두 조건을 모두 체크하지만 (defensive),
|
||
단일 조건만 체크해도 정확하다.
|
||
|
||
#### Failure Modes (런타임 에러)
|
||
|
||
F1. **잘못된 direction**:
|
||
- PE 커널이 `tl.send(dir="X")` 호출 → install 안 된 direction
|
||
- PE_IPCQ가 즉시 `IpcqInvalidDirection` 예외 raise
|
||
- SimPy 시뮬레이션 즉시 abort, 사용자에게 명확한 에러
|
||
|
||
F2. **타입 mismatch**:
|
||
- send와 recv의 dtype/shape/nbytes가 일치하지 않음
|
||
- 첫 구현은 검증 안 함 (dtype/shape는 hint), 향후 strict mode로 추가
|
||
|
||
F3. **Deadlock detection (timeout 기반)**:
|
||
- send: peer_tail_cache가 갱신 안 되고 영원히 wait
|
||
- recv: peer_head_cache 갱신 안 되고 영원히 wait
|
||
- 시뮬레이션 timeout (default 10ms simulated time) 초과 시 abort
|
||
- 디버그를 위해 각 PE의 last send/recv 위치, blocking 상태 dump
|
||
|
||
F4. **Backend init 실패**:
|
||
- ccl.yaml에 `defaults.algorithm` 누락
|
||
- `algorithms[name]` 정의 누락
|
||
- 알고리즘 모듈 import 실패
|
||
- topology 검증 실패 (I3, I4)
|
||
→ 모두 `init_process_group` 시점에 즉시 에러
|
||
|
||
F5. **Slot full + 무한 backpressure**:
|
||
- peer가 영원히 안 받음
|
||
- F3과 같이 timeout으로 처리
|
||
- 디버그: 막힌 PE의 my_head, peer_tail_cache 출력
|
||
|
||
#### 진단 도구 (구현 단계에서 추가)
|
||
|
||
- **CCL trace**: 각 send/recv를 (rank, t, dir, nbytes) 형태로 로깅
|
||
- **Pointer dump**: 시뮬레이션 종료 시 또는 hang 시 모든 PE의 IPCQ pointer 상태 출력
|
||
- **Deadlock graph**: hang 발생 시 wait-for 그래프 출력 (어느 PE가 어떤 PE를 기다리는지)
|
||
|
||
### D15. 알고리즘 작성자 가이드 (요약)
|
||
|
||
본 섹션은 알고리즘 작성자가 한 화면으로 시작점을 잡을 수 있도록 한다.
|
||
자세한 step-by-step 가이드는 [docs/onboarding/ccl-author-guide.md](../onboarding/ccl-author-guide.md) 참조.
|
||
|
||
#### 만지는 것 / 만지지 않는 것
|
||
|
||
| 만지는 것 | 만지지 않는 것 |
|
||
|----------|---------------|
|
||
| `src/kernbench/ccl/algorithms/<your_algo>.py` (kernel + 선택적 neighbors) | `benches/ccl_allreduce.py` 호스트 코드 |
|
||
| `ccl.yaml` 의 한 entry 추가 + `defaults.algorithm` | `src/kernbench/ccl/` 프레임워크 |
|
||
| (선택) `tests/test_<your_algo>.py` 단위 테스트 | `src/kernbench/components/builtin/pe_ipcq.py` 컴포넌트 |
|
||
| | `src/kernbench/runtime_api/distributed.py` backend |
|
||
|
||
#### 알고리즘 모듈 인터페이스 contract
|
||
|
||
```python
|
||
# src/kernbench/ccl/algorithms/<your_algo>.py
|
||
|
||
def kernel(*args, tl) -> None:
|
||
"""필수. PE 커널.
|
||
|
||
Args (positional): tensor pointers, rank, world_size, 알고리즘 파라미터
|
||
Args (keyword): tl — TLContext (자동 주입)
|
||
|
||
사용 가능한 IPCQ API:
|
||
tl.send(dir, src_addr, nbytes) # blocking, backpressure 시 wait
|
||
tl.recv(dir) # 특정 방향에서 blocking recv
|
||
tl.recv() # 4방향 round-robin
|
||
tl.recv_async(dir) → handle # non-blocking
|
||
tl.wait(handle) # non-blocking 완료 대기
|
||
|
||
기존 API도 그대로 사용:
|
||
tl.load / tl.store / tl.composite / tl.program_id 등
|
||
"""
|
||
...
|
||
|
||
def neighbors(rank, world_size, neighbor_map) -> dict | None:
|
||
"""선택. ccl.yaml의 builtin topology가 만든 neighbor_map을 override.
|
||
|
||
None 반환 → builtin 그대로 사용
|
||
dict 반환 → 그 dict로 override (builtin을 base로 수정 가능)
|
||
"""
|
||
return None
|
||
```
|
||
|
||
#### 5-step 흐름
|
||
|
||
1. **kernel 함수 작성** — `src/kernbench/ccl/algorithms/<algo>.py` 신규 파일
|
||
2. **ccl.yaml 등록** — `algorithms.<name>` entry + `defaults.algorithm` 변경
|
||
3. **(선택) neighbors override** — builtin topology를 base로 수정이 필요할 때
|
||
4. **단위 테스트** — `kernbench.ccl.testing.run_kernel_in_mock` (SimPy 없이 빠름)
|
||
5. **시뮬 검증** — `kernbench run --bench ccl_allreduce --verify-data`
|
||
|
||
호스트 코드 (`benches/ccl_allreduce.py`)는 손대지 않는다.
|
||
|
||
#### 사용 가능한 헬퍼 (`kernbench.ccl.helpers`)
|
||
|
||
| Helper | 설명 |
|
||
|--------|------|
|
||
| `chunked(addr, n_chunks, ...)` | 텐서를 n개 chunk view로 슬라이싱 |
|
||
| `ring_step(rank, step, ws)` | ring algorithm의 step별 (send_idx, recv_idx) |
|
||
| `tree_step(rank, level)` | binary tree의 level별 parent/child 인덱스 |
|
||
|
||
#### 디버깅 도구
|
||
|
||
- `KERNBENCH_CCL_TRACE=1` — send/recv trace 출력
|
||
- 시뮬 종료 시 자동 IPCQ pointer dump
|
||
- Deadlock 시 (10ms 시뮬 시간 초과) wait-for graph dump
|
||
|
||
#### 흔한 실수
|
||
|
||
1. **install 안 된 direction 사용** — ccl.yaml의 topology가 ring_1d면 N/S 사용 불가
|
||
2. **send/recv 짝 맞지 않음** — peer 측 recv 없으면 hang (slot full backpressure)
|
||
3. **dtype/shape 불일치** — 첫 구현은 검증 안 함, 작성자 책임
|
||
|
||
자세한 step-by-step과 hello-world 예제는 `docs/onboarding/ccl-author-guide.md` 참조.
|
||
|
||
---
|
||
|
||
## HW Realization Notes (Informative)
|
||
|
||
**Status of this section**: Forward-looking. Describes how the simulator
|
||
contract (D1–D15) would map to silicon. Not currently implemented;
|
||
subject to revision before tapeout. The simulator implements the
|
||
contract via Python/SimPy equivalents in
|
||
[pe_ipcq.py](../../src/kernbench/components/builtin/pe_ipcq.py) and
|
||
[pe_dma.py](../../src/kernbench/components/builtin/pe_dma.py).
|
||
|
||
### D16. Proposed HW Block Diagram and End-to-End Dataflow
|
||
|
||

|
||
|
||
> Source: [`../diagrams/pe_baseline.d2`](../diagrams/pe_baseline.d2) — `d2 --layout=elk --scale 1.5`.
|
||
|
||

|
||
|
||
> Source: [`../diagrams/pe_proposed.d2`](../diagrams/pe_proposed.d2) — `d2 --layout=elk`.
|
||
|
||
**Baseline → Proposed 핵심 변경**:
|
||
|
||
- 단일 FIFO inbox → **compute port / IPCQ port 분리 + WRR Arbiter** (NEW)
|
||
- PE_IPCQ (SimPy component) → **IPCQ Controller** (HW register + combinational logic)
|
||
- TCM 내 **IPCQ Slot Region 예약 영역** 명시
|
||
- Credit Injector / Receiver가 Fabric Port를 통해 NoC에 직접 연결
|
||
|
||
#### End-to-End Sequence (HW view)
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant CPU_A as PE_A: PE_CPU
|
||
participant IPCQ_A as PE_A: IPCQ Ctrl
|
||
participant DMA_A as PE_A: DMA
|
||
participant NOC as NoC Fabric
|
||
participant DMA_B as PE_B: DMA
|
||
participant IPCQ_B as PE_B: IPCQ Ctrl
|
||
participant TCM_B as PE_B: TCM
|
||
participant CPU_B as PE_B: PE_CPU
|
||
|
||
Note over CPU_A: tl.send(dir="E", src=0x1000)
|
||
|
||
CPU_A->>IPCQ_A: MMIO: send request
|
||
Note over IPCQ_A: Backpressure check:<br/>(head - peer_tail_cache) < n_slots → PASS<br/>Slot addr gen:<br/>dst = peer_rx_base + (head%n) × slot_size
|
||
IPCQ_A->>DMA_A: IpcqDmaToken {src, dst, sender_seq=head}
|
||
Note over IPCQ_A: my_head++
|
||
IPCQ_A-->>CPU_A: send returns (fire-and-forget)
|
||
|
||
Note over DMA_A: TCM read → snapshot in read buffer<br/>Flit pack: data + {sender_seq, dst_addr}
|
||
DMA_A->>NOC: IPCQ data flit(s)
|
||
|
||
Note over NOC: hop latency + BW drain
|
||
|
||
NOC->>DMA_B: IPCQ data flit(s)
|
||
Note over DMA_B: Terminal BW drain<br/>Slot write latency
|
||
|
||
rect rgb(255, 240, 220)
|
||
Note over DMA_B,IPCQ_B: ATOMIC (I6): same cycle, no stall
|
||
DMA_B->>TCM_B: write data → slot address
|
||
DMA_B->>IPCQ_B: Meta Extractor: {sender_seq, dst_addr}
|
||
end
|
||
|
||
Note over IPCQ_B: Range match dst_addr → direction "W"<br/>peer_head_cache["W"] = sender_seq + 1
|
||
IPCQ_B-->>CPU_B: recv_wake signal
|
||
|
||
Note over CPU_B: tl.recv(dir="W") wakes up
|
||
CPU_B->>IPCQ_B: recv request
|
||
Note over IPCQ_B: peer_head_cache > my_tail → YES<br/>slot_addr = rx_base + (tail%n) × slot_size
|
||
IPCQ_B-->>CPU_B: return slot_addr
|
||
CPU_B->>TCM_B: read data from slot
|
||
Note over IPCQ_B: my_tail++
|
||
|
||
IPCQ_B->>NOC: Credit (16B): {consumer_seq, dst_rx_base_pa}
|
||
Note over NOC: credit traversal (NoC latency)
|
||
NOC->>IPCQ_A: Credit arrival
|
||
|
||
Note over IPCQ_A: Match dst_rx_base_pa → direction "E"<br/>peer_tail_cache["E"] = consumer_seq<br/>Backpressure deassert (if stalled)
|
||
```
|
||
|
||
### D17. IPCQ Controller HW Module (신규)
|
||
|
||
PE_CPU와 DMA Engine 사이에 위치하는 하드웨어 제어 블록. 시뮬레이터의
|
||
`PeIpcqComponent`에 대응한다.
|
||
|
||
#### QPair Register File
|
||
|
||
방향별 queue pair 상태를 flip-flop으로 유지. PE_CPU가 MMIO(CSR)로 읽기/쓰기
|
||
가능하며, init 시점에 소프트웨어가 채워넣는다.
|
||
|
||
```
|
||
Per-direction registers (each 64-bit):
|
||
my_head — sender write position (monotonic)
|
||
my_tail — receiver read position (monotonic)
|
||
peer_head_cache — last known peer head (updated by Meta Extractor)
|
||
peer_tail_cache — last known peer tail (updated by Credit Receiver)
|
||
rx_base_pa — this PE's rx buffer base physical address
|
||
peer_rx_base_pa — peer's rx buffer base physical address
|
||
n_slots — ring depth (power-of-2 제약, D21 참조)
|
||
slot_size — bytes per slot
|
||
peer_credit_tgt — peer PE의 credit receive 주소
|
||
|
||
Directions: 최대 8 (N/S/E/W/parent/child_left/child_right + spare)
|
||
Total: 8 dirs × 9 regs × 8B = 576B flip-flops
|
||
```
|
||
|
||
#### Slot Address Generator (combinational)
|
||
|
||
```
|
||
Input: pointer (my_head or my_tail), n_slots, slot_size, base_pa
|
||
Output: slot_addr = base_pa + (pointer % n_slots) * slot_size
|
||
|
||
Implementation:
|
||
n_slots power-of-2 → pointer & (n_slots - 1) (AND mask, 1 gate)
|
||
slot_size power-of-2 → barrel shift (1 cycle)
|
||
64-bit add → ripple/kogge-stone adder (1 cycle)
|
||
|
||
Latency: 1-2 cycles combinational
|
||
```
|
||
|
||
#### Backpressure Comparator (combinational)
|
||
|
||
```
|
||
full = (my_head - peer_tail_cache) >= n_slots
|
||
|
||
Implementation: 64-bit subtract + unsigned compare
|
||
Output: stall signal → PE_CPU (IPCQ send blocked) or DMA issue hold
|
||
Latency: 1 cycle
|
||
```
|
||
|
||
#### Meta Extractor (inbound datapath sideband)
|
||
|
||
DMA Engine의 inbound vc_comm path에 wired. 도착하는 IPCQ flit의 header에서
|
||
metadata를 추출하여 queue pair 상태를 갱신한다.
|
||
|
||
```
|
||
Trigger: DMA inbound write completion (same cycle)
|
||
Extract: {sender_seq, dst_addr} from flit header
|
||
|
||
Direction matching (ADR-0025 D2):
|
||
for each dir:
|
||
match = (base_pa[dir] <= dst_addr) && (dst_addr < base_pa[dir] + n_slots[dir] * slot_size[dir])
|
||
8× parallel range comparators + priority encoder
|
||
|
||
Update: peer_head_cache[matched_dir] = max(peer_head_cache, sender_seq + 1)
|
||
Output: recv_wake signal → PE_CPU interrupt/flag
|
||
Latency: 1 cycle (pipelined with DMA write — I6 atomicity 자연 보장)
|
||
```
|
||
|
||
#### Credit Injector (outbound)
|
||
|
||
```
|
||
Trigger: recv completion (my_tail 증가 후)
|
||
Action: pack 16B credit packet → DMA vc_comm (또는 dedicated credit VC)
|
||
|
||
Packet: {consumer_seq = my_tail, dst_rx_base_pa = my_rx_base_pa}
|
||
Latency: 1 cycle to generate, then NoC traversal
|
||
```
|
||
|
||
#### Credit Receiver (inbound sideband)
|
||
|
||
```
|
||
Trigger: 16B credit packet arrival (from NoC)
|
||
Extract: {consumer_seq, dst_rx_base_pa}
|
||
|
||
Direction matching (ADR-0025 D3):
|
||
for each dir:
|
||
match = (peer_rx_base_pa[dir] == credit.dst_rx_base_pa)
|
||
|
||
Update: peer_tail_cache[matched_dir] = max(peer_tail_cache, consumer_seq)
|
||
Output: send_wake signal → deassert backpressure stall
|
||
Latency: 1 cycle
|
||
```
|
||
|
||
### D18. DMA Engine vc_comm IPCQ-aware Mode
|
||
|
||
기존 vc_comm 채널(D8)에 IPCQ flit 처리 모드를 추가한다.
|
||
|
||
**Outbound**:
|
||
|
||
1. IPCQ Controller로부터 command 수신: `{src_addr, dst_addr, nbytes, sender_seq}`
|
||
2. TCM에서 src_addr read → DMA read buffer에 snapshot (standard DMA behavior)
|
||
3. Flit pack: data + piggyback metadata (sender_seq, dst_addr)
|
||
4. NoC fabric port에 inject
|
||
5. Fire-and-forget (completion 미대기)
|
||
|
||
**Inbound**:
|
||
|
||
1. NoC로부터 IPCQ flit 수신
|
||
2. Terminal BW drain charge (`drain_ns = nbytes / bottleneck_bw`)
|
||
3. Slot write latency charge (backing memory tier)
|
||
4. **ATOMIC** (same pipeline stage, no stall insertion):
|
||
- TCM write: data → slot address
|
||
- Meta Extractor trigger: sender_seq + dst_addr → IPCQ Controller
|
||
5. Done
|
||
|
||
**I6 atomicity 하드웨어 보장**: TCM write completion과 Meta Extractor trigger가
|
||
동일 pipeline stage에서 발생하므로 별도 synchronization이 불필요. 시뮬레이터의
|
||
"no SimPy yield between MemoryStore.write and IpcqMetaArrival put" (D9, I6)이
|
||
자연스럽게 보장된다.
|
||
|
||
#### Data Snapshot Semantics
|
||
|
||
DMA read buffer에 latch된 데이터는 src memory의 이후 수정에 영향받지 않는다.
|
||
이는 DMA standard read-then-write behavior이므로 추가 HW 불필요.
|
||
|
||
#### Credit Virtual Channel (선택적)
|
||
|
||
- **옵션 A**: vc_comm에 credit을 multiplexing (16B header-only flit으로 구분).
|
||
- **옵션 B**: 3rd dedicated credit VC 추가 (strict priority > data).
|
||
|
||
옵션 B가 deadlock prevention에 유리하나, 16B credit의 BW 영향이 무시 가능하므로
|
||
옵션 A로도 충분.
|
||
|
||
### D19. Fabric Flit Format Extension
|
||
|
||
```
|
||
일반 data flit (예: 512-bit):
|
||
┌──────────────────────────────────────────┐
|
||
│ [511:480] routing header (32b) │
|
||
│ [479:0] payload (480b = 60B) │
|
||
└──────────────────────────────────────────┘
|
||
|
||
IPCQ data flit (첫 flit에만 metadata 포함):
|
||
┌──────────────────────────────────────────┐
|
||
│ [511:480] routing header (32b) │
|
||
│ [511] ipcq_flag (1b) │ ← IPCQ vs normal DMA 식별
|
||
│ [510:509] vc_id (2b) │
|
||
│ [508:480] route + hop count │
|
||
│ [479:416] ipcq_metadata (64b) │ ← piggyback
|
||
│ [479:448] sender_seq (32b) │
|
||
│ [447:416] dst_addr[31:0] (32b) │ ← direction matching용
|
||
│ [415:0] payload (416b = 52B) │
|
||
└──────────────────────────────────────────┘
|
||
후속 flits: full 60B payload (metadata 없음)
|
||
|
||
Credit-only flit (128-bit, header-only):
|
||
┌──────────────────────────────────────────┐
|
||
│ [127:96] routing header (32b) │
|
||
│ [127] credit_flag (1b) │
|
||
│ [95:64] consumer_seq (32b) │
|
||
│ [63:0] dst_rx_base_pa (64b) │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
첫 flit의 payload가 60B → 52B로 감소 (13% overhead). Multi-flit transfer에서는
|
||
후속 flit이 full payload이므로 대형 전송에서 overhead < 1%.
|
||
|
||
### D20. TCM IPCQ Slot Region Layout
|
||
|
||
```
|
||
TCM Memory Map (16MB):
|
||
┌─────────────────────────────┐ 0x000000
|
||
│ Kernel Working Memory │
|
||
│ (compute tensors) │
|
||
│ ~14MB │
|
||
├─────────────────────────────┤ 0xE00000
|
||
│ IPCQ RX Buffers │
|
||
│ Dir N: slots × slot_size │
|
||
│ Dir S: slots × slot_size │
|
||
│ Dir E: slots × slot_size │
|
||
│ Dir W: slots × slot_size │
|
||
│ ~1MB │
|
||
├─────────────────────────────┤ 0xF00000
|
||
│ IPCQ Metadata / Scratch │
|
||
│ ~1MB │
|
||
└─────────────────────────────┘ 0xFFFFFF
|
||
```
|
||
|
||
IPCQ region을 TCM의 상위 bank에 배치하여 compute access와의 bank conflict를
|
||
최소화한다 (Risk D22 참조).
|
||
|
||
### D21. 2nm Implementation Analysis
|
||
|
||
#### Area Estimate
|
||
|
||
| Module | Gate Count | Area (2nm est.) | Notes |
|
||
|---|---|---|---|
|
||
| QPair Register File | ~4.6K FF | 0.002 mm² | 576B flip-flops |
|
||
| Slot Addr Gen + Backpressure | ~5K gates | 0.001 mm² | Combinational |
|
||
| Meta Extractor + Credit Logic | ~3K gates | 0.001 mm² | 8× parallel comparators |
|
||
| **IPCQ Controller subtotal** | **~12.6K** | **~0.004 mm²** | **PE 전체 대비 < 0.1%** |
|
||
| DMA vc_comm 확장 | ~2K gates | 0.002 mm² | Flit pack/unpack |
|
||
| **Total 변경분** | **~14.6K** | **~0.006 mm²** | |
|
||
|
||
#### Timing
|
||
|
||
| Path | Delay (2nm est.) | Target Clock | Margin |
|
||
|---|---|---|---|
|
||
| Backpressure (sub + cmp) | ~0.3 ns | 1 GHz (1 ns) | 3× |
|
||
| Slot Addr Gen (mask + shift + add) | ~0.5 ns | 1 GHz | 2× |
|
||
| Meta Extractor (8× range match) | ~0.4 ns | 1 GHz | 2.5× |
|
||
| Credit Receiver (8× equality) | ~0.3 ns | 1 GHz | 3× |
|
||
|
||
모든 critical path가 1 cycle 이내. Timing closure 문제 없음.
|
||
|
||
#### Power
|
||
|
||
- Active: ~1 mW (register R/W + comparators, send/recv 동작 시)
|
||
- Idle: leakage only
|
||
- PE 전체 전력 대비 무시 가능
|
||
|
||
#### Constraints
|
||
|
||
| 항목 | 제약 | 근거 |
|
||
|---|---|---|
|
||
| `n_slots` | **반드시 power-of-2** | mod → AND mask (1 gate). 임의 값은 divider 필요 (~10 cycles) |
|
||
| `slot_size` | **power-of-2 권장** | mul → barrel shift. 임의 값은 multiplier 필요 |
|
||
| TCM IPCQ region | **전용 bank 배치** | Compute access와 bank conflict 방지 |
|
||
|
||
### D22. Risk Assessment
|
||
|
||
#### TCM Bank Conflict
|
||
|
||
- **Risk**: IPCQ slot write와 compute read가 동일 bank 접근 시 stall
|
||
- **Mitigation**: IPCQ region을 TCM 상위 address의 전용 bank에 배치 (D20)
|
||
- **Cost**: TCM banking flexibility 소폭 감소
|
||
- **Severity**: Medium (성능 영향), Low (correctness 문제 아님)
|
||
|
||
#### Credit Return Latency under Congestion
|
||
|
||
- **Risk**: NoC 혼잡 시 credit return 지연 → sender backpressure stall
|
||
- **Mitigation**:
|
||
- Credit을 별도 VC로 분리 + strict priority (16B로 BW impact 미미)
|
||
- 또는 n_slots를 넉넉히(8+) 설정하여 credit 지연을 buffer로 흡수
|
||
- **Severity**: Low (credit 16B는 congestion에 거의 기여하지 않음)
|
||
|
||
#### Inter-Direction Ordering
|
||
|
||
- **Risk**: 같은 PE에서 여러 방향으로 동시 send 시 순서
|
||
- **Mitigation**: Per-direction monotonic seq으로 충분. Inter-direction ordering은
|
||
kernel(소프트웨어) 책임 — 현재 시뮬레이터 모델과 동일 (D2 + D4)
|
||
- **Severity**: Low (아키텍처 설계에 의해 해소)
|
||
|
||
### D23. HW Alternatives Considered
|
||
|
||
#### Doorbell + Polling (전통적 방식)
|
||
|
||
```
|
||
Send: DMA write data → DMA write doorbell register at peer → peer polls doorbell
|
||
Recv: Polling loop on doorbell, or interrupt-driven
|
||
```
|
||
|
||
| 장점 | 단점 |
|
||
|---|---|
|
||
| 단순한 HW (IPCQ controller 불필요) | 2번의 DMA transaction (data + doorbell) |
|
||
| 기존 DMA 재사용 | Data/doorbell 사이 ordering 보장 필요 (fence) |
|
||
| | Polling은 전력 낭비, interrupt는 latency overhead |
|
||
|
||
**평가**: Piggyback 대비 latency 2-3× 증가. **불채택.**
|
||
|
||
#### Hardware Message Queue (NVIDIA NVLink 스타일)
|
||
|
||
```
|
||
Send: CPU → HMQ에 descriptor push → HW가 peer HMQ로 자동 전달
|
||
Recv: HMQ에서 descriptor pop → data pointer 확인
|
||
```
|
||
|
||
| 장점 | 단점 |
|
||
|---|---|
|
||
| CPU는 descriptor만 작성 | 별도 HMQ engine 필요 (~0.05 mm²) |
|
||
| Descriptor/data 분리 → 유연 | DMA와 별개 datapath → area/power 중복 |
|
||
| | Large tensor에는 결국 DMA 필요 |
|
||
|
||
**평가**: CCL의 large tensor 패턴에서 DMA 필수이므로 HMQ + DMA 이중 구조는
|
||
면적 낭비. **불채택.**
|
||
|
||
#### RDMA-style Completion Queue (CQ)
|
||
|
||
```
|
||
Send: DMA write → peer에 CQE 자동 생성
|
||
Recv: CQ poll/interrupt → data 위치 확인
|
||
```
|
||
|
||
| 장점 | 단점 |
|
||
|---|---|
|
||
| InfiniBand/RoCE 성숙 모델 | CQ 관리 logic + CQE memory overhead |
|
||
| Multi-tenant/isolation 용이 | CQE/data ordering 보장 추가 필요 |
|
||
| | PE-to-PE CCL에는 over-engineered |
|
||
|
||
**평가**: RDMA CQ는 host-facing NIC의 multi-tenant 격리에 적합.
|
||
PE 간 단일 owner 환경에서는 불필요한 복잡성. **불채택.**
|
||
|
||
#### Credit-in-Data Piggyback (v2 최적화 후보)
|
||
|
||
현재 설계에서 credit return은 별도 16B packet이다. Bidirectional 통신
|
||
패턴에서는 **reverse 방향 data flit에 credit을 합칠 수 있다.**
|
||
|
||
```
|
||
PE_A →E→ PE_B: data + sender_seq=3
|
||
PE_B →W→ PE_A: data + sender_seq=5 + credit_ack=4 ← credit이 data에 합쳐짐
|
||
```
|
||
|
||
| 장점 | 단점 |
|
||
|---|---|
|
||
| Credit 전용 packet 제거 → NoC BW 절약 | Unidirectional 패턴에서는 fallback 필요 |
|
||
| Bidirectional allreduce에서 credit latency → 0 | Flit header에 8B 추가 (overhead 미미) |
|
||
| | Logic 복잡도 소폭 증가 |
|
||
|
||
**평가**: 현재 설계의 우수한 최적화. Bidirectional allreduce에서 credit packet을
|
||
완전 제거 가능. Standalone credit fallback도 유지. **v2로 채택 권고.**
|
||
|
||
### Open HW Questions
|
||
|
||
- IPCQ slot region size를 TCM의 몇 %까지 허용할 것인가? (현재 가정: ~1MB / 16MB = 6.25%)
|
||
- Credit VC를 별도로 둘 것인가, vc_comm에 multiplexing할 것인가? (D18 참조)
|
||
- Inter-SIP link에서의 flit format 호환성 검증 필요
|
||
- n_slots 최대값 제한? (8 directions × 8 slots × 64KB = 4MB → TCM의 25%)
|
||
|
||
---
|
||
|
||
## Non-goals
|
||
|
||
- **호스트 collective**: `dist.all_reduce`가 데이터 이동을 직접 수행하는 모델은
|
||
본 ADR 범위 외. 본 ADR은 PE 커널 안에서 일어나는 통신만 다룬다.
|
||
- **All-reduce 알고리즘**: ring/tree 등 알고리즘 자체는 별도 ADR (또는 커널
|
||
코드)에서 다룬다. 본 ADR은 인프라(IPCQ + VC)만 정의.
|
||
- **Reliability/error handling**: send/recv 실패, link 장애 등은 다루지 않음.
|
||
- **NoC arbiter 정밀 모델**: VC 간 dynamic contention은 첫 구현 범위 외 (D8).
|
||
|
||
---
|
||
|
||
## Open Questions
|
||
|
||
- **VC arbitration 정확도**: 첫 구현은 deterministic chunk interleave +
|
||
weighted round-robin. heavy contention 시나리오에서 실제보다 optimistic한
|
||
결과가 나올 수 있음. 정밀화 필요 시 별도 NoC arbiter component 도입을 검토.
|
||
- **Credit return fast path BW 모델**: 첫 구현은 fast path가 fabric BW
|
||
contention 모델에서 제외 (별도 lightweight wire 가정). 정밀화 필요 시
|
||
credit fast path를 별도 link로 모델링하거나, `credit_return_mode: piggyback`
|
||
옵션 추가.
|
||
- **Ring buffer slot의 메모리 할당**: TCM/HBM/SRAM 어디에 두든 IPCQ가 알아야
|
||
할 metadata (base addr, slot_size, n_slots). init 시 호스트가 사이드밴드로
|
||
넣을지, fabric MmuMapMsg와 유사한 메시지로 넣을지 결정 필요.
|
||
- **VC 간 BW 분할 default**: 균등 분할(50/50)인지, weighted(예: 80% compute,
|
||
20% comm)인지. ccl.yaml에 노출하되 default 값 결정 필요.
|
||
- **Direction 개수**: 4방향(N/S/E/W) 고정인지, 6방향(+ Up/Down for 3D),
|
||
또는 가변 N개로 확장할지. 첫 구현은 4방향 고정.
|
||
- **다중 channel 데이터 구조 (multi-tile aggregation)**: 한 collective에서
|
||
여러 tile을 fan-out 받는 경우 기존 round-robin recv로 충분한지, 별도
|
||
primitive(`tl.recv_all`)가 필요한지.
|
||
- **Round-robin recv fairness**: 첫 구현은 last_polled_dir 인덱스 기반 weak
|
||
fairness. 한 방향에 데이터가 항상 먼저 도착하면 starvation 가능. strict
|
||
fairness가 필요하면 별도 fairness counter 추가.
|
||
- **Deadlock detection 정밀화**: 첫 구현은 timeout 기반. 향후 wait-for graph
|
||
실시간 추적으로 deterministic deadlock detection 가능.
|
||
|
||
---
|
||
|
||
## Consequences
|
||
|
||
### 긍정적
|
||
|
||
- PE 간 직접 통신 가능 → CCL 커널 작성 가능
|
||
- 호스트는 launch만, 동기화는 PE 안에서 → 단순한 호스트 코드, 강한
|
||
compute/comm overlap
|
||
- VC를 통해 HoL blocking 제거 → collective latency가 compute traffic에
|
||
block되지 않음
|
||
- Buffer 위치/backpressure 모드를 init 파라미터로 선택 가능 → 벤치마크 가능
|
||
- 4-방향 logical neighbor → 호스트가 ring/mesh/tree 등 알고리즘 자유롭게
|
||
매핑
|
||
|
||
### 부정적
|
||
|
||
- 컴포넌트 1개 신규 추가 (PE_IPCQ), PE_DMA 재설계 (VC 추가)
|
||
- IPCQ 메모리 (8 ring × slot_size × n_slots) 만큼 PE-local 메모리 사용
|
||
- VC arbitration 모델이 first-order approximation이므로 heavy contention
|
||
시나리오에서 실제 HW보다 약간 optimistic한 latency 결과 가능 (D8 한계)
|
||
- VC chunk-level 인터리브로 PE_DMA 구현이 더 복잡해짐
|