# 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 — 이상적 스케줄러의 분할 상환)
