# ADR-0034: HBM 컨트롤러 내부 설계 ## Status Accepted ## Context `HbmCtrlComponent`는 큐브 NOC의 말단(leaf)에 위치하는 PE별 HBM 파티션 엔드포인트이다. 토폴로지 노드 `sip{S}.cube{C}.hbm_ctrl.pe{idx}` 아래에 PE마다 하나의 인스턴스가 생성되며 해당 PE의 라우터에 연결된다 (ADR-0017 D4). 본 컴포넌트는 의사 채널(PC, pseudo-channel)별 스케줄링, 버스트 단위 커밋 타이밍, 주소 기반 PC 선택, 그리고 응답을 요청자에게 되돌리는 라우팅을 모델링한다. 본 ADR은 현재 구현된 컴포넌트를 문서화한다. ADR-0017 D4/D8은 HBM CTRL이 *어디에* 부착되는지와 *어떤* 집계 대역폭을 제공해야 하는지를 정의한다. ADR-0033 D1/D2는 HBM 모델링의 *어떤 정밀도(fidelity)*가 범위에 포함되는지를 정의한다. 본 ADR은 그 둘 사이의 공백 — 인스턴스별 내부 스케줄링 모델을 채운다. ## Decision ### D1. 역할 `HbmCtrlComponent`는 PE별 HBM 파티션 엔드포인트이다. PE당 하나의 인스턴스(큐브당 기본 8개, `cube.memory_map.hbm_slices_per_cube`로 설정)가 `cube_mesh.yaml`의 `peX.hbm` 부착 목록을 통해 해당 PE의 라우터에 연결된다 (ADR-0017 D4). 기본 n:1 채널 매핑(ADR-0017 D8)에서는 인스턴스가 `channels_per_pe`개의 의사 채널을 하나의 엔드포인트로 집계한다. 본 컴포넌트는 다음을 모델링한다: - PC별 스케줄링(D2) 및 R/W 명령 버스 공유. - 주소 기반 PC 선택(D3). - 버스트 단위 커밋 타이밍(D4). - Flit 인지 per-flit PC 커밋 및 비동기 finalize(D5, D6). - 읽기 데이터 드레인(drain)을 위한 명령 전용 Transaction 처리(D7). - 요청자에게 되돌리는 응답 라우팅(D8). 다음은 모델링하지 않는다: - Bank 수준의 row-buffer 충돌, refresh, ECC, 열 스로틀링 (ADR-0033 D3). - 자신의 라우터 엣지를 넘어가는 PE 간 HBM 경합(라우터 메시가 처리 — ADR-0017 D3). - 1:1 채널 모드(ADR-0017 D8 향후 작업). ### D2. PC별 스케줄링 모델 `start()`에서 초기화되는 인스턴스별 상태: - `_pc_avail: list[float]` — 각 PC가 다음에 자유로워지는 가장 빠른 시뮬레이션 시각; 길이 `num_pcs`, 초기값 0.0. - `_pc_last_dir: list["R"|"W"|None]` — 각 PC의 마지막 커밋 방향, 스위치 페널티 감지에 사용(D4); 초기값 `None`. `num_pcs`와 `burst_bytes`는 각각 양의 2의 거듭제곱이어야 주소 기반 PC 선택(D3)이 시프트와 마스크로 축약된다. 읽기와 쓰기 요청은 PC별로 동일한 `_pc_avail` 슬롯을 공유한다 — 실제 HW에서 PC별 명령 버스는 읽기와 쓰기 트래픽이 공유하므로, PC k에 쓰기를 발행하면 PC k에 대한 후속 읽기가 정확히 버스트 시간만큼 블록된다. 요청의 방향 `dir`은 요청 타입으로부터 추론된다: - `MemoryWriteMsg` → `"W"`. - `is_write=True`인 `PeDmaMsg` → `"W"`. - 그 외 전부(`MemoryReadMsg`, 읽기 `PeDmaMsg`) → `"R"`. ### D3. 주소 기반 PC 선택 접근에 대한 PC 인덱스는 접근 주소로부터 시프트와 마스크로 도출된다: ```text pc_shift = log2(burst_bytes) # 기본값 8 (burst=256B) pc_mask = num_pcs - 1 # 기본값 7 (8 PCs) pc = (address >> pc_shift) & pc_mask ``` 대안적인 `(burst_bytes, num_pcs)` 쌍과의 정합성을 유지하기 위해 `start()`에서 토폴로지 설정으로부터 한 번 계산된다. 정규 기본값 `(256, 8)`에서는 PC 선택 필드가 HBM 바이트 오프셋의 비트 `[10:8]`에 배치된다: 비트 `[7:0]`은 버스트 내부(같은 PC), 비트 `[10:8]`은 3비트 PC 인덱스, 비트 `[36:11]`은 PC 슬라이스 내부의 row/bank/column이다 (`phyaddr.py` 주석 참조). 주소 기반 스트라이핑은 — 주소를 보지 않는 전역 라운드로빈과 달리 — 오프셋이 분리된 동시 전송들에 대해 PC 병렬성을 보존한다: 각 전송의 버스트는 자신의 바이트 주소가 함의하는 PC 집합 위에 결정론적으로 떨어지므로, 분리된 영역에 접근하는 멀티 PE 워크로드가 단일 PC에서 충돌하지 않는다. ### D4. 버스트 단위 시간 및 PC 커밋 타이밍 단일 PC 커밋에 걸리는 시간: ```text chunk_time = burst_bytes / pc_bw_gbs # ns ``` - `burst_bytes`(기본 256)는 flit 크기와 일치하는 버스트 단위이다 (ADR-0033 D1). - `pc_bw_gbs`는 **빌더에서 도출**된다: `hbm_to_router_bw_gbs / num_pcs` (`topology/builder.py`). 이는 PE당 집계 대역폭이 라우터-HBM 링크 대역폭과 같아야 한다는 ADR-0017 D8의 불변식을 강제한다. 방향 `dir`로 PC `pc`에 도착한 접근에 대한 PC별 커밋 스케줄링: ```text switch_cost = switch_penalty_ns if pc_last_dir[pc] not in (None, dir) else 0 start = max(env.now, pc_avail[pc]) + switch_cost finish = start + chunk_time pc_avail[pc] = finish pc_last_dir[pc] = dir ``` 기본 `switch_penalty_ns = 0` — 이상적인 HBM 스케줄러가 R/W 스위칭 비용을 분할 상환한다는 Tier 0 가정(ADR-0033 D2). 0이 아닌 값은 교차마다 발생하는 비관적 비용을 모델링한다. ### D5. Flit 인지 per-flit PC 커밋 (주 경로) `_handle_flit`이 주 워커 경로이다. 각 도착 `Flit`에 대해: 1. 트랜잭션의 **첫 번째** flit인 경우(`tid = id(txn)`가 `_txn_state`에 없는 경우): - `run(env, nbytes)`를 통해 `overhead_ns`를 한 번 적용 — 헤더 디코드 모델, first-flit overhead 패턴(ADR-0033 D1). - `_txn_state[tid] = {"last_finish": env.now}`로 초기화. 2. `pc = _pc_for_address(flit.address)`를 계산(D3). 3. 요청 방향(D2)을 사용하여 PC별 스케줄(D4)을 적용. 4. `state["last_finish"] = max(state["last_finish"], finish)`로 갱신. 5. `flit.is_last`이면: `_txn_state[tid]`를 pop하고 `_finalize_txn`을 spawn(D6). per-flit 주소 인지 커밋이 분리된 HBM 오프셋으로 향하는 동시 멀티 PE 트래픽이 서로 다른 PC를 통해 병렬로 파이프라인되도록 하는 메커니즘이다. ### D6. 트랜잭션별 비동기 finalize 트랜잭션의 마지막 flit이 스케줄링되고 나면, finalize는 별도로 spawn된 프로세스에서 실행된다: ```python def _finalize_txn(env, txn, last_finish): wait = last_finish - env.now if wait > 0: yield env.timeout(wait) yield from _send_response(env, txn) ``` `_handle_flit`은 이를 `env.process(...)`로 spawn한 뒤 즉시 반환하므로, 마지막 PC 커밋이 드레인되는 동안에도 워커는 다음 inbox 메시지를 집어들 수 있다. 이 분리가 없다면 — 즉 워커 자신이 `yield env.timeout(wait)`를 한다면 — 서로 다른 PC에 떨어지는 주소를 가진 동시 단일 flit 트랜잭션들도 결국 워커 내부에서 각각 `chunk_time`만큼 직렬화되어, D3와 D5가 노출하려고 설계한 PC 병렬성을 숨겨버린다. ### D7. 명령 전용 트랜잭션을 위한 non-flit 폴백 `_handle_txn`은 inbox가 `Flit`이 아닌 `Transaction`을 전달할 때 실행된다. 이는 와이어가 flit으로 분할하지 않는 명령 전용 요청에 대한 경로로 — 대표적으로 명령 트랜잭션이 `nbytes=0`을 운반하는 `MemoryReadMsg`가 해당한다(데이터 드레인은 HBM CTRL 후처리에서 모델링되며, 인바운드 flit으로 모델링되지 않는다). 절차: 1. `work_bytes = txn.nbytes if txn.nbytes > 0 else int(request.nbytes or 0)` — 읽기 명령의 경우 작업량은 요청으로 결정된다. 2. `work_bytes > 0`이면 `n_chunks = ceil(work_bytes / burst_bytes)`, 아니면 0. 3. 둘 다 > 0일 때 `chunk_interval = drain_ns / n_chunks` — 청크는 `drain/n_chunks` ns 간격으로 시간상에 스케줄링되어 병목 링크의 데이터 도착 속도를 모델링한다(ADR-0033 D1 청크 루프 드레인). 4. `overhead_ns`를 위해 `run(env, txn.nbytes)`를 한 번 적용. 5. 각 청크 `i`에 대해 `chunk_interval` ns만큼 진행한 뒤 `pc = _pc_for_address(base_address + i * burst_bytes)`로 D4 스케줄을 적용. 6. 모든 청크 스케줄링 후 `last_finish - env.now`만큼 대기한 다음 `_send_response`를 호출. `_handle_txn`은 `_handle_flit`과 동일한 `_pc_avail` / `_pc_last_dir` 상태를 공유한다 — 두 경로에 걸쳐 PC 스케줄링의 단일 진실 원천이 정확히 하나만 존재한다. ### D8. 응답 라우팅 `_send_response`는 요청 타입과 경로 형상에 따라 디스패치한다: | 경우 | 트리거 | 응답 | | --- | --- | --- | | PE_DMA | `isinstance(txn.request, PeDmaMsg)` | 신규 역방향 경로 Transaction(`is_response=True`, `nbytes=0`), 동일한 `done` | | Bypass — Memory Read | `"m_cpu" not in any(txn.path)` AND `MemoryReadMsg` | `nbytes=request.nbytes`(데이터 반환)인 역방향 경로 Transaction | | Bypass — Memory Write | `"m_cpu" not in any(txn.path)` AND not Memory Read | `txn.done.succeed()` (쓰기는 로컬에서 완료) | | 기본 | 그 외 | 역방향 경로상의 신규 `ResponseMsg(correlation_id, request_id, src_cube, src_pe, success=True)` | "bypass" 분류는 ADR-0015 D4에서 정의된 Memory R/W 패브릭 경로(PCIE_EP → io_noc → ucie → 큐브 라우터 → hbm_ctrl, M_CPU 미경유)와 일치한다. PE_DMA 케이스는 내부 루프 DMA를 빠르게 유지하기 위한 전용 역방향 경로이다 (PE_DMA 읽기/쓰기는 ResponseMsg 봉투를 합성하지 않는다). 모든 역방향 경로 케이스에서, 응답 Transaction은 `out_ports[reverse_path[1]]` — 기록된 정방향 경로를 따라 되돌아가는 첫 홉 — 에 put된다. `reverse_path`의 엔트리가 2개 미만이면(축퇴된 경로), 원래의 `txn.done`이 직접 시그널된다. ### D9. 설정 가능한 속성 | 속성 | 기본값 | 출처 | 비고 | | --- | --- | --- | --- | | `num_pcs` | 8 | 토폴로지 큐브 `hbm_ctrl.attrs` | 2의 거듭제곱이어야 함 | | `pc_bw_gbs` | 32.0 | 빌더 도출: `hbm_to_router_bw_gbs / num_pcs` | ADR-0017 D8 불변식 강제 | | `burst_bytes` | 256 | 토폴로지 attrs | 2의 거듭제곱이어야 함; `flit_bytes`와 동일(ADR-0033 D1) | | `switch_penalty_ns` | 0.0 | 토폴로지 attrs | Tier 0 기본값; 0이 아니면 비관적 R/W 스위칭 모델링 | | `efficiency` | 1.0 | 토폴로지 attrs | 빌더 시점에 `hbm_to_router_bw_gbs`에 적용(라우터 엣지 BW 스케일링만) | | `overhead_ns` | 0.0 | 토폴로지 attrs | First-flit 디코드 오버헤드(D5) | `pc_bw_gbs`는 yaml 측 중복 없이 PE당 집계 대역폭을 라우터-HBM 링크 대역폭과 일치시키기 위해 직접 설정되지 않고 `topology/builder.py`에서 도출된다. ## Consequences ### Positive - 주소 기반 PC 선택은 주소를 보지 않는 라운드로빈이 무너뜨릴 멀티 스트림 HBM 병렬성을 보존한다 — 분리된 HBM 영역을 갖는 멀티 PE 워크로드에서 중요하다. - Flit 인지 경로(D5) + 비동기 finalize(D6)는 웜홀 파이프라이닝을 보존하며, 연속적인 단일 flit 트랜잭션에 대해 PC 병렬성을 노출한다. - PC 스케줄링의 단일 진실 원천(D4 메커니즘이 D5 flit 경로와 D7 청크 루프 경로 모두에서 사용됨). - 빌더 도출 `pc_bw_gbs`가 yaml 규율이 아닌 코드에서 ADR-0017 D8을 강제한다. ### Negative - PC 내부의 bank 수준 충돌 모델링이 없음; bank/row-buffer 재사용에 주소-무관(ADR-0033 D3). - HBM 스케줄러 없음(FR-FCFS / write-buffer / watermark drain); PC당 고정 FIFO. 버스티한 혼합 R/W는 `switch_penalty_ns`로 근사화된다 (ADR-0033 D2). - `_txn_state`는 `id(txn)`로 키를 잡는 일반 dict이다; 동시 트랜잭션마다 in-flight 상태가 누적되며 `is_last` 시에만 제거된다. 현재 워크로드에는 충분하다. ## Links - ADR-0001 (물리 주소 레이아웃 — PC 비트 필드 주석) - ADR-0015 D4 (Memory R/W 패브릭 경로 — bypass 응답 케이스) - ADR-0017 D4 (PE별 HBM 파티셔닝 — PE 라우터로의 부착) - ADR-0017 D8 (HBM 채널 매핑 모드 — 본 ADR이 구현하는 n:1 집계) - ADR-0017 D9 (AddressResolver — `hbm_ctrl.pe{pe_id}` 엔드포인트 해석) - ADR-0033 D1 (정확한 모델링 — PC별 병렬성, 스위치 페널티, flit 인지 PC 커밋, first-flit 오버헤드, 청크 루프 드레인) - ADR-0033 D2 (스위치 페널티 기본값 0 — 이상적 스케줄러의 분할 상환)