# ADR-0047: AHBM CCL Backend — `torch.distributed`-compat shim

## Status

Accepted (2026-05-22).

`runtime_api/distributed.py` 의 `AhbmCCLBackend` + `DistributedContext` —
즉 `torch.distributed.init_process_group(backend="ahbm")` 진입점이 실제로
무엇을 설치하고 어떤 의미로 `all_reduce`/`barrier`/`get_rank` 등을
구현하는지를 명시한다. ADR-0023 D11 이 "torch.distributed compatibility"
의도를 언급하나, **backend 자체의 동작 모델**은 ADR-level 에 없었다.

## First action (제일 처음에 하는 일)

`RuntimeContext.__post_init__` 가 자동으로 `DistributedContext()` 인스턴스를
만들어 `self.distributed` 에 attach 한다. 그 시점의 첫 일은:

1. `self._backend: AhbmCCLBackend | None = None` 으로 초기화 (아직 init
   되지 않은 상태).
2. `self._rank_by_greenlet: dict = {}` 로 greenlet-local rank 레지스트리
   초기화 (ADR-0024 D2).
3. 호출자(RuntimeContext) 측에서 `dc._ctx_ref = self` 로 back-reference 를
   심어, 이후 `init_process_group` 가 `ctx.engine` / `ctx.spec` / `ctx.launch`
   에 도달할 수 있게 한다.

즉, **DistributedContext 의 첫 일은 "RuntimeContext 에 자기 자신을
back-reference 와 함께 부착하고 backend 슬롯을 비워두는 것"**. 실제 backend
설치(IPCQ install, world_size 산출, 알고리즘 모듈 로드)는 사용자 코드의
`torch.distributed.init_process_group(backend="ahbm")` 호출 시점에 비로소
일어난다.

해당 시점의 `init_process_group` 의 첫 일은:

1. `backend != "ahbm"` 이면 즉시 `ValueError("Unsupported backend ...")`.
2. `getattr(self, "_ctx_ref", None)` 가 None 이면
   `RuntimeError("DistributedContext not bound to a RuntimeContext")`.
3. `self._backend = AhbmCCLBackend(torch_ctx=ctx)` — 이 생성자 안에서
   ccl.yaml load + 알고리즘 모듈 import + world_size 산출 + SFR 설정 +
   IPCQ install 이 모두 일어난다.
4. `self._backend._dist_ctx = self` — backend 가 거꾸로
   `_rank_by_greenlet` 에 접근할 수 있게 함.

## Context

PyTorch DDP 의 collective 호출 (`init_process_group`, `all_reduce` 등) 을
그대로 사용할 수 있게 만들어, bench 코드가 "진짜 DDP training script" 와
동일한 모습이 되도록 하는 것이 `AhbmCCLBackend` 의 목적이다 (ADR-0024 +
ADR-0027 의 launcher 모델과 정렬).

이 backend 가 책임지는 것:

- `init_process_group` 시점에 **IPCQ neighbor table 을 한 번 설치** (real
  NCCL communicator creation 과 유사).
- `all_reduce(tensor, op="sum")` 호출 시 **설정된 algorithm 의 kernel 함수
  를 `ctx.launch(...)` 로 발사**.
- `get_world_size` / `get_rank` 를 greenlet-local rank 레지스트리와
  ccl.yaml/topology 로부터 일관되게 답함.

ADR-0023 D10 (IPCQ install plan), ADR-0024 (SIP launcher) 가 부분적으로
이를 다루나, **`AhbmCCLBackend` 자체의 책임 범위와 의사결정 순서**는
어디에도 명시되어 있지 않다. 본 ADR 이 채운다.

## Decision

### D1. backend 는 `init_process_group(backend="ahbm")` 시점에만 생성된다

`DistributedContext` 는 `__init__` 시점에 `_backend = None` 으로 시작한다.
backend 객체는 사용자가 `dist.init_process_group(backend="ahbm")` 를
호출하기 전까지 존재하지 않으며, 그 외 API (`is_initialized`,
`get_world_size`, `all_reduce`, `barrier`) 가 backend 가 None 인 채로
호출되면 `RuntimeError("Default process group has not been initialized...")`
를 던진다 (`_ensure_initialized` 헬퍼).

`backend != "ahbm"` 은 즉시 `ValueError`. 다른 backend 명 (nccl, gloo
등) 은 인식하지 않는다.

### D2. world_size 산출 우선순위 — algorithm > defaults > topology

`AhbmCCLBackend._resolve_world_size` (ADR-0024 D1) 의 결정 순서:

1. `ccl.yaml` 의 algorithm entry 에 `world_size` 가 있으면 그 값.
2. `defaults.world_size` 가 있으면 그 값.
3. 둘 다 없으면 `spec.system.sips.count` (=topology 의 SIP 개수).

기본 의미는 **rank = SIP** (ADR-0024). cube/PE-level parallelism 은 각
rank 안에서 DPPolicy 로 표현되며 world_size 에 영향을 주지 않는다. 명시적
`ccl.yaml` 의 world_size override 가 있으면 legacy "rank = flat PE 인덱스"
테스트 경로를 위해 그대로 존중된다.

`init_process_group(world_size=..., rank=...)` 의 사용자 인자는 **수신하나
무시**된다 (real PyTorch 의 `RANK` / `WORLD_SIZE` env var 와 같은 의미).

### D3. `init_process_group` 가 즉시 하는 4가지 설치 작업

`AhbmCCLBackend.__init__` 안에서 다음이 순차 실행된다:

1. **ccl.yaml 로딩**: `kernbench.ccl.install.load_ccl_config()` →
   `resolve_algorithm_config(_cfg_all)` 로 `defaults.algorithm` (또는
   사용자가 지정한 알고리즘) 의 merged config 산출.
2. **알고리즘 모듈 import**: `importlib.import_module(self._merged["module"])`.
   이 모듈은 `kernel` 함수, `kernel_args(world_size, n_elem, cube_w, cube_h)`
   helper, optional `TOPO_NAME_TO_KIND` 매핑을 노출해야 한다.
3. **world_size 산출** (D2).
4. **topology 메타 수집**: `spec` 으로부터 `n_sips`, `sip_topo` (`ring_1d`
   기본), `cube_w`/`cube_h`, `sips.w`/`sips.h`. SIP topology 가 ring_1d 가
   아니면 explicit `w`/`h` 또는 square root 로 (`w*h == n_sips` 보장)
   `_sip_topo_w/h` 산출. 불일치 시 `ValueError`.
5. **SFR + IPCQ 설치**: `kernbench.ccl.sfr_config.configure_sfr_intercube_multisip
   (engine, spec, self._merged)` 를 호출. 이 함수가 모든 SIP/cube 의 pe0 에
   IPCQ neighbor table 을 푸시 (real NCCL communicator 의 일회성 설정에
   해당).

이 순서가 변하면 (예: SFR 전에 algorithm 모듈 load 가 실패하면) 부분 초기화
상태가 발생할 수 있다. 따라서 D3 는 atomic 한 4-단계로 본다 — 실패 시
backend 는 미설치 상태로 남는다.

### D4. greenlet-local rank 등록 (ADR-0024 D2)

`DistributedContext._rank_by_greenlet: dict[greenlet, int]` 은 spawn 된
worker greenlet 각각에 rank 를 매핑한다. bench launcher (예:
`torch.multiprocessing.spawn`) 가 worker 를 띄울 때
`dc._bind_rank(g, rank)` 를 호출하여 등록한다.

`get_rank()` 는 `getcurrent()` 의 greenlet 을 lookup. 미등록 greenlet은
fallback 으로 0 을 반환 — single-driver / 테스트 호환성 유지.

backend 는 `_dist_ctx._rank_by_greenlet` 를 통해 `all_reduce` 시 현재
greenlet 의 rank 를 가져온다 (D5).

### D5. `all_reduce(tensor, op="sum")` 동작

검증 단계:

- `op != "sum"` → `NotImplementedError`. 현재 kernel 들은 add reduction만 구현.
- `tensor._handle is None` → `RuntimeError("not deployed")`.
- `tensor._handle.shards` 가 비면 `RuntimeError("no shards")`.

준비 단계:

- `n_elem = shards[0].nbytes // tensor.itemsize` — 단일 shard 의 element 수.
- `kernel_fn = self._algo_module.kernel` — D3 에서 import 된 알고리즘 모듈의
  진입 함수.
- effective cube dims 결정: 첫 번째 SIP 의 cube 갯수가 1 이면 (1,1) 으로
  scalar 처리, 아니면 토폴로지의 `cube_w`/`cube_h` 사용. TP 가 일부 cube
  만 쓰는 경우를 자연스럽게 흡수.
- `kernel_args = self._algo_module.kernel_args(world_size, n_elem, cube_w,
  cube_h)` — 알고리즘이 자기 kernel 에 넘길 인자 셋을 결정.

dispatch:

- 현재 greenlet 의 rank 를 `_rank_by_greenlet.get(g, 0)` 로 lookup.
- `extra_args = (sip_rank, sip_topo_kind, sip_topo_w, sip_topo_h)` 를 append.
- `pending = self.ctx.launch(algorithm_name, kernel_fn, tensor, *kernel_args,
  *extra_args, _defer_wait=True)` — `_defer_wait=True` 로 collective drain
  을 메인 scheduler 에 위임 (ADR-0027 D0.4).

drain:

- 부모 greenlet 이 살아있으면 (multi-greenlet 모드) `_pending_collective_handles`
  에 enqueue 한 뒤 부모로 switch. 메인 scheduler 가 모든 rank 의 launch 후
  일괄 drain.
- 단일-driver 모드면 inline 으로 `for h, _sip_id, meta in pending:
  self.ctx.wait(h, _meta=meta)` 즉시 drain.

### D6. `barrier()` 는 no-op 이다 (single-driver 모델)

kernbench 는 하나의 Python process 안에서 모든 rank 를 greenlet 으로 다룬다.
process 간 동기화가 필요한 상황이 없으므로 `barrier()` 는 호출 가능하지만
실제 어떤 동기화도 수행하지 않는다. real PyTorch DDP 와의 API 호환성을
위해 유지 (호출자가 NotImplementedError 를 받지 않도록).

장래에 multi-process kernbench (예: SimPy event loop 가 process 별로
독립) 가 도입되면 D6 를 supersede 하는 새 ADR 이 필요.

### D7. `get_rank` / `get_world_size` / `get_backend` 의 의미

- `get_rank()` (D4): 현재 greenlet 의 bound rank. 미등록은 0.
- `get_world_size()` (D2): backend 가 D3 에서 산출한 world_size.
- `get_backend()`: 항상 `"ahbm"` 문자열. backend 객체가 존재하지 않으면
  `_ensure_initialized` 에서 RuntimeError.

real PyTorch 와의 차이:

- real PyTorch `get_rank()` 는 process global 값이지만, kernbench 는
  greenlet-local. spawn 된 worker 안에서 호출하면 rank, main thread 에서
  호출하면 0. bench 작성자는 worker 함수 안에서만 의미 있는 rank 를 기대해야
  한다.

### D8. 지원하는 API 표면 (final)

`DistributedContext` 가 노출하는 API:

- `init_process_group(backend="ahbm", world_size=None, rank=None, **kwargs)`
- `is_initialized() -> bool`
- `get_world_size() -> int`
- `get_rank() -> int`
- `get_backend() -> str`
- `all_reduce(tensor, op="sum") -> None`
- `barrier() -> None`
- (internal) `_bind_rank(g, rank)`

이외의 PyTorch distributed API (broadcast, reduce, all_gather, gather,
scatter, send/recv 등) 는 **아직 구현되어 있지 않다**. kernel 레벨에서는
`tl.send`/`tl.recv` (ADR-0046 D3.10) 로 직접 표현 가능하나, dist.* surface
로는 노출되지 않는다. 추가 collective 가 필요해질 시 별도 알고리즘 모듈
+ `DistributedContext` 메소드 한 쌍을 추가하여 D8 를 확장한다.

## Alternatives Considered

### A1. backend 를 `RuntimeContext.__init__` 에서 즉시 생성

기각. ccl.yaml 이 없거나 알고리즘 모듈을 import 할 수 없는 경우, bench 가
distributed 기능을 안 쓰는데도 RuntimeContext 생성 자체가 실패하게 된다.
"호출 시점에 비로소 설치" (D1) 가 lazy 의미상 옳다.

### A2. world_size 를 항상 topology 로부터 자동 산출 (override 금지)

기각. ADR-0024 D1 의 "explicit override" 경로가 legacy 테스트에서 사용 중.
한 SIP 안에서 PE-level rank 를 따로 정의해야 하는 진단 시나리오를 위해
유지.

### A3. `op != "sum"` 을 silent fallback 으로 처리

기각. 사용자가 `op="prod"` / `"max"` / `"avg"` 를 의도했는데 silently sum
이 실행되면 결과 검증이 매우 어렵다. 명시적 `NotImplementedError` 가 안전.

### A4. `barrier` 를 SimPy event 로 구현

기각 (현재). single-driver 모델에서 cross-process 동기화 의미가 없으므로
no-op 가 의미적으로 정확. SimPy fake-barrier 는 의미 없이 코드 복잡도만
높임. multi-process kernbench 도입 시 재평가.

## Consequences

- `torch.distributed.init_process_group(backend="ahbm")` 의 4-단계 설치
  (D3) 가 ADR-level 에서 굳어져, 향후 새 collective 알고리즘이 어디에
  훅을 걸어야 하는지 명확.
- D2 의 우선순위 (algorithm > defaults > topology) 가 명시되어, ccl.yaml
  변경 시 영향 범위를 빠르게 가늠 가능.
- D6 의 barrier no-op 결정이 ADR-level 에 굳어져, multi-process kernbench
  도입 시 별도 ADR 로 supersede 해야 함이 분명.
- D8 의 미지원 API 목록이 명시되어, 사용자가 `dist.broadcast(...)` 를
  호출하려 할 때의 명확한 거절 근거 제공.
