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>
70 KiB
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 인프라에만 집중한다.
풀어야 할 문제
- PE 간 직접 데이터 이동 (peer's memory에 write)
- 동기화 — 송신 측이 수신 측 buffer 공간을 확인해야 함 (backpressure)
- compute traffic과 communication traffic의 자원 경쟁 (Head-of-Line blocking)
- 호스트가 알고리즘에 따라 (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를 가진다.
@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하려면 다음을 모두 알아야 한다.
@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 객체를 들고 있다.
주소 계산은:
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 순서는:
- 모든 rank의 IPCQ buffer 할당 (각 PE의 buffer_kind 메모리 공간에서)
- rank별 endpoint table 구성 (자신의 4-방향 peer가 어느 sip/cube/pe/pa를 갖는지)
- PE_IPCQ에 install (
IpcqInitMsgvia fabric or sideband)
이 순서는 모든 rank가 서로의 PA를 알아야 하므로, 단계 1을 모든 rank에 대해 먼저 끝낸 후 단계 2-3을 진행한다.
D3. 4-방향 매핑 = 논리적 ProcessGroup
PE는 4방향(N/S/E/W)을 logical port로 본다. 실제 peer 주소는 호스트 CCL init이 알고리즘에 따라 설정한다. PE 커널은 토폴로지를 알지 못하고 방향만 사용한다.
# 호스트 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
# 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의 메모리 위치를 결정한다:
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 |
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 종류로 채널 결정
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한다.
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 구간 안이 아니라 그 앞에 위치해야 한다:
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 시:
- PE_IPCQ가 backpressure 체크 (peer_tail_cache 기준)
- PE_IPCQ가 IpcqDmaToken 생성 (data + piggyback metadata) → PE_DMA(vc_comm)에 put
- PE_DMA가 fabric DMA 시뮬레이션 (latency 진행)
- DMA 완료와 동일한 SimPy step에 atomic 시퀀스:
- MemoryStore.write(buffer_kind, dst_pa, data) — single-hop DMA write
- 수신측 PE_IPCQ에 metadata forward → peer_head_cache 갱신 → 대기 recv wake
- 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 시:
- PE_IPCQ가 (peer_head_cache > my_tail) AND (MemoryStore.has(slot_addr)) 조건 대기 (D9 piggyback 모델에서는 두 조건이 같은 step에 truthy가 되지만, defensive check)
- 조건 만족 시:
slot_addr = my_rx_base + slot_idx * slot_size - 두 가지 모드 (
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)
- PE_IPCQ가 my_tail++, fast path credit return을 발행 (D9 — vc_comm fabric을 거치지 않고 별도 SimPy Store 채널로 bottleneck-BW latency 후 peer 측 peer_tail_cache 갱신)
- 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=Nonerecv_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가 그 데이터를 사용하면 자동으로 정합성이 검증된다.
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 등)은 받지 않는다.
# 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)
init_process_group(backend="ahbm")호출- backend가
ccl.yaml로드 →defaults.algorithm결정 algorithms[<algo>]에서 topology + buffer_kind + backpressure + slot/size 결정- 즉시 모든 PE의 PE_IPCQ에 neighbor table을 install (sideband 또는 fabric
IpcqInitMsg) - 이후
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 만 바꾸면 된다.
# 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는 선택.
# 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로 일부만 수정:
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 등):
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()가
처음부터 매핑을 만들어야 한다.
알고리즘 추가 절차
src/kernbench/ccl/algorithms/<algo>.py에kernel함수 작성ccl.yaml의algorithms섹션에 entry 추가 (module,topology)- (선택) 같은 모듈에
neighbors()함수 추가하여 builtin override 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).
@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)
@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)
@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).
@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 도착 시):
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 기반으로 계산된다.
@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 매핑)
전송 흐름:
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에 정의된 대로:
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도 두어서는 안 된다:MemoryStore.write(token.dst_endpoint.buffer_kind, token.dst_addr, data)- PE_IPCQ에
IpcqMetaArrivalforward
- 두 동작은 동일 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 참조.
만지는 것 / 만지지 않는 것
| 만지는 것 | 만지지 않는 것 |
|---|---|
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
# 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 흐름
- kernel 함수 작성 —
src/kernbench/ccl/algorithms/<algo>.py신규 파일 - ccl.yaml 등록 —
algorithms.<name>entry +defaults.algorithm변경 - (선택) neighbors override — builtin topology를 base로 수정이 필요할 때
- 단위 테스트 —
kernbench.ccl.testing.run_kernel_in_mock(SimPy 없이 빠름) - 시뮬 검증 —
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
흔한 실수
- install 안 된 direction 사용 — ccl.yaml의 topology가 ring_1d면 N/S 사용 불가
- send/recv 짝 맞지 않음 — peer 측 recv 없으면 hang (slot full backpressure)
- 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 and pe_dma.py.
D16. Proposed HW Block Diagram and End-to-End Dataflow
Source:
../diagrams/pe_baseline.d2—d2 --layout=elk --scale 1.5.
Source:
../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)
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:
- IPCQ Controller로부터 command 수신:
{src_addr, dst_addr, nbytes, sender_seq} - TCM에서 src_addr read → DMA read buffer에 snapshot (standard DMA behavior)
- Flit pack: data + piggyback metadata (sender_seq, dst_addr)
- NoC fabric port에 inject
- Fire-and-forget (completion 미대기)
Inbound:
- NoC로부터 IPCQ flit 수신
- Terminal BW drain charge (
drain_ns = nbytes / bottleneck_bw) - Slot write latency charge (backing memory tier)
- ATOMIC (same pipeline stage, no stall insertion):
- TCM write: data → slot address
- Meta Extractor trigger: sender_seq + dst_addr → IPCQ Controller
- 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 구현이 더 복잡해짐

