9a02955770
Documents four cross-cutting surfaces that previously had no ADR backing, each surfaced as a G4 candidate by /report: - 0046 prog-tl-context-contract: the kernel-side tl.* API. Enumerates all primitives (ref/load/store/dot/composite/math/reduction/IPCQ/...), the two execution modes (command-list vs greenlet runner), scratch allocator semantics, dispatch-overhead model, and the kernel registry. - 0047 par-ahbm-ccl-backend: torch.distributed.init_process_group (backend="ahbm") install path. world_size priority (algorithm > defaults > topology), the 4-step init sequence (load ccl.yaml, import algorithm module, derive world_size, install SFR + IPCQ), greenlet- local rank registry, all_reduce dispatch via _defer_wait, barrier no-op rationale, and the explicit list of unsupported dist.* APIs. - 0048 mem-allocator-algorithms: VirtualAllocator + PEMemAllocator free-list semantics. Offset-keyed first-fit with coalescing, the no-validation trust model for free(), HBM/TCM channel separation, page-aligned VA allocation, the page_size dual-default (VirtualAllocator 2 MiB / _ensure_allocators 4 KiB fallback), and one-allocator-per-sub-unit rule. - 0049 ver-probe-subcommand: kernbench probe traffic-pattern catalog. H2D / D2H / PE DMA categories with their exact cube-index choices, the 32 KiB reference size, the 5-point utilization sweep, the formula vs actual column meanings, automatic invariant checks (monotonicity, D2H >= H2D, best < worst), per-case GraphEngine isolation, and the human-readable (not machine-parsable) output contract. Bilingual pair verifier passes for all four EN/KO pairs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
263 lines
12 KiB
Markdown
263 lines
12 KiB
Markdown
# ADR-0048: Memory Allocator Algorithms — VirtualAllocator + PEMemAllocator
|
||
|
||
## Status
|
||
|
||
Accepted (2026-05-22).
|
||
|
||
`policy/address/allocator.py` 의 `_FreeList` / `PEMemAllocator` 와
|
||
`va_allocator.py` 의 `VirtualAllocator` 가 사용하는 free-list 알고리즘,
|
||
페이지 정렬, coalescing 규칙을 명시한다. ADR-0001 (PhysAddr 레이아웃) 과
|
||
ADR-0011 (PA/VA/LA 모델) 이 주소 스킴을 정의하나, **할당 알고리즘**은 별도
|
||
ADR 이 없었다.
|
||
|
||
## First action (제일 처음에 하는 일)
|
||
|
||
### `_FreeList(capacity)`
|
||
|
||
생성 즉시 `self._capacity = capacity`, `self._used = 0`, `self._free =
|
||
[(0, capacity)]` 로 초기화. 첫 일은 **전 영역을 single free block 으로
|
||
세우는 것** — 즉 `(offset=0, size=capacity)` 한 튜플이 free list 의 유일한
|
||
원소다.
|
||
|
||
### `PEMemAllocator(sip_id, die_id, pe_id, cfg)`
|
||
|
||
생성 즉시 두 개의 `_FreeList` 를 만든다:
|
||
|
||
- `self._hbm = _FreeList(cfg.hbm_slice_bytes)` — 이 PE 가 소유한 HBM
|
||
slice 의 바이트 크기 (`hbm_bytes_per_cube // hbm_slices_per_cube`) 만큼.
|
||
- `self._tcm = _FreeList(cfg.tcm_allocatable_bytes)` — `tcm_bytes_per_pe -
|
||
tcm_scheduler_reserved_bytes` 만큼 (scheduler 예약분은 사전 분리).
|
||
|
||
따라서 PEMemAllocator 의 첫 일은 **이 PE 의 HBM slice 와 사용자
|
||
TCM 영역을 각각 단일 free block 으로 세우는 것**.
|
||
|
||
### `VirtualAllocator(va_base, va_size, page_size=2*1024*1024)`
|
||
|
||
생성 즉시 `self._va_base = va_base`, `self._va_size = va_size`,
|
||
`self._page_size = page_size`, `self._used = 0`, `self._free = [(va_base,
|
||
va_size)]`. 첫 일은 **VA base 부터 size 까지 single block 으로 세우고
|
||
page_size 를 회수**.
|
||
|
||
## Context
|
||
|
||
`runtime_api/context.py::_ensure_allocators` 는 다음 단계로 allocator 세트를
|
||
구성한다:
|
||
|
||
1. spec 으로부터 `hbm_total_gb_per_cube`, `hbm_slices_per_cube`,
|
||
`tcm_size_mb`, target_device 별 SIP 범위 등을 읽음.
|
||
2. `AddressConfig` 로 모든 파라미터를 frozen 하게 패킹.
|
||
3. target SIP 범위 × cube × PE 의 모든 조합에 대해
|
||
`PEMemAllocator(sip, cube, pe, cfg)` 인스턴스를 1개씩 생성.
|
||
4. `VirtualAllocator(va_base=0x1_0000_0000, va_size=64 GiB,
|
||
page_size=pe_mmu.page_size)` 를 1개 생성.
|
||
|
||
allocator 들의 책임:
|
||
|
||
- **PEMemAllocator**: PE-로컬 HBM slice / TCM 의 PA-공간 할당 (PhysAddr
|
||
encoding 까지 포함).
|
||
- **VirtualAllocator**: device-wide VA 공간을 페이지 정렬로 할당. 이후
|
||
`RuntimeContext._create_tensor` 가 VA → PA 매핑을 `MmuMapMsg` 로 fabric
|
||
에 push.
|
||
|
||
이 알고리즘들은:
|
||
|
||
- **first-fit** 으로 단순.
|
||
- 자유 블록 리스트는 **offset 정렬 (sorted by start)** 유지.
|
||
- `free()` 시 **양쪽 인접 블록과 coalesce**.
|
||
|
||
이런 결정의 근거가 어디에도 없으므로, 향후 누군가 "왜 best-fit 이 아닌가",
|
||
"왜 buddy allocator 가 아닌가", "왜 partial overlap free 가 silently
|
||
허용되는가" 라는 질문에 답할 기준이 필요. 본 ADR 이 그 기준을 마련한다.
|
||
|
||
## Decision
|
||
|
||
### D1. `_FreeList` — offset-기반 first-fit + coalescing
|
||
|
||
`policy/address/allocator.py::_FreeList`:
|
||
|
||
- 내부 표현: `list[tuple[int, int]]` = `[(start_offset, size), ...]` —
|
||
start offset 으로 정렬된 자유 블록의 sorted list.
|
||
- `alloc(nbytes)`:
|
||
1. free list 를 앞에서부터 순회 (first-fit).
|
||
2. 처음 만나는 `size >= nbytes` 인 블록에서 앞부분을 잘라 사용.
|
||
3. 정확히 일치하면 블록 통째로 제거; 아니면 `(start+nbytes, size-nbytes)`
|
||
로 축소.
|
||
4. `_used += nbytes`, 잘라낸 `start` 반환.
|
||
5. 맞는 블록이 없으면 `AllocationError("overflow ... largest free block
|
||
...")`.
|
||
- `free(offset, nbytes)`:
|
||
1. `_used -= nbytes`.
|
||
2. `bisect_left(self._free, (offset,))` 로 삽입 위치 결정.
|
||
3. 직전 블록과 인접 (`prev_start + prev_size == offset`) 하면 흡수.
|
||
4. 직후 블록과 인접 (`offset+nbytes == next_start`) 하면 흡수.
|
||
5. coalesced range 를 정렬 위치에 insert.
|
||
|
||
이 알고리즘은 fragmentation 에 약점이 있으나 (best-fit / buddy 대비), 본
|
||
시뮬레이터의 워크로드 특성상 (deploy/free 패턴이 거의 stack-like) 충분
|
||
하다는 것이 디자인 가정이다. 워크로드가 변하면 D1 supersede 후보.
|
||
|
||
### D2. partial overlap free 는 **검사하지 않는다**
|
||
|
||
`_FreeList.free(offset, nbytes)` 는 호출자가 정확한 (offset, nbytes) 를
|
||
넘긴다고 신뢰한다. 다음을 검증하지 않는다:
|
||
|
||
- 그 range 가 실제로 alloc 된 것인지.
|
||
- 그 range 가 다른 alloc 된 영역과 겹치지 않는지.
|
||
|
||
이유: 시뮬레이터 컨텍스트에서 호출자는 항상 `alloc()` 의 반환값을 그대로
|
||
저장했다가 `free()` 에 넘기는 패턴이며, 외부 사용자 입력이 아니다. 안전성
|
||
검사를 추가하면 매 free 마다 O(N) 비용이 들어 시뮬 wall-clock 에 영향.
|
||
|
||
이 신뢰 모델이 깨지면 (예: 두 텐서가 같은 PA 를 가리키는 코드 경로 도입)
|
||
즉시 ADR-level 으로 재검토.
|
||
|
||
### D3. `PEMemAllocator` — HBM/TCM 두 채널 분리
|
||
|
||
`PEMemAllocator(sip_id, die_id, pe_id, cfg)` 는 두 `_FreeList` 를 보유:
|
||
|
||
- `_hbm`: `cfg.hbm_slice_bytes` 크기.
|
||
- `_tcm`: `cfg.tcm_allocatable_bytes` (= `tcm_bytes_per_pe -
|
||
tcm_scheduler_reserved_bytes`) 크기.
|
||
|
||
`alloc_hbm(nbytes) -> PhysAddr`:
|
||
|
||
- `_hbm.alloc(nbytes)` 로 offset 획득.
|
||
- `PhysAddr.pe_hbm_addr(sip_id, die_id, pe_id, pe_local_hbm_offset=offset,
|
||
slice_size_bytes=cfg.hbm_slice_bytes)` 로 PA 인코딩.
|
||
- 실패 시 `AllocationError("HBM overflow ...")`.
|
||
|
||
`free_hbm(pa, nbytes)`:
|
||
|
||
- `pa.hbm_offset - pe_id * cfg.hbm_slice_bytes` 로 PE-local offset 복원.
|
||
- `_hbm.free(offset, nbytes)`.
|
||
|
||
`alloc_tcm(nbytes) -> PhysAddr`: 유사하게 `PhysAddr.pe_tcm_addr` 로 인코딩.
|
||
|
||
`free_tcm(pa, nbytes)`: `pa.sub_offset` 을 그대로 사용 (TCM 은 PE-local
|
||
offset 이 곧 sub_offset).
|
||
|
||
scheduler-reserved TCM 영역 (`cfg.tcm_scheduler_reserved_bytes`) 은
|
||
allocator 가 인지하지 않는다 (`_tcm` 의 capacity 에서 사전 차감되어 있음).
|
||
이는 ADR-0014 의 PE_SCHEDULER 내부 buffer 예약과 정합된다.
|
||
|
||
### D4. `VirtualAllocator` — 페이지 정렬 first-fit + coalescing
|
||
|
||
`policy/address/va_allocator.py::VirtualAllocator`:
|
||
|
||
- 내부 표현: `_FreeList` 와 동일한 sorted `list[tuple[int, int]]`.
|
||
최초: `[(va_base, va_size)]`.
|
||
- `_align_up(nbytes) = ceil(nbytes / page_size) * page_size`.
|
||
- `alloc(nbytes) -> int`:
|
||
1. `aligned = _align_up(nbytes)`.
|
||
2. first-fit 으로 `size >= aligned` 인 블록 탐색.
|
||
3. 블록 앞부분 `aligned` 만큼 잘라 사용. 정확히 일치하면 제거.
|
||
4. `_used += aligned`. 블록 `start` (= aligned 된 VA) 반환.
|
||
5. 실패 시 `VaAllocationError`.
|
||
- `free(va, nbytes)`: `_align_up(nbytes)` 단위로 free. _FreeList 와 동일한
|
||
coalesce 알고리즘.
|
||
|
||
`page_size` 의 실제 값은 두 곳에서 다른 기본을 갖는다:
|
||
|
||
- `VirtualAllocator.__init__` 의 매개변수 기본값: `2 MiB`. 직접 호출하는
|
||
테스트가 그대로 받는다.
|
||
- `RuntimeContext._ensure_allocators` 가 인스턴스화할 때:
|
||
`pe_mmu.attrs.get("page_size", 4096)` — `topology.yaml` 의
|
||
`pe_mmu.attrs.page_size` 가 있으면 그 값, 없으면 fallback 4 KiB.
|
||
|
||
두 기본이 다른 이유: VirtualAllocator 의 standalone 기본은 ADR-0039 의
|
||
PE_MMU stopgap 기본 (2 MiB) 과 정합되어 직접 테스트가 자연스럽고, context
|
||
fallback 의 4 KiB 는 topology 미설정 시 안전한 minimum page 다. 실제 사용
|
||
경로는 항상 후자이며 (`_ensure_allocators` 가 인스턴스화하므로),
|
||
`topology.yaml` 에서 `page_size` 가 명시되면 그 값이 양쪽 (MMU + VA
|
||
allocator) 으로 일관되게 흐른다.
|
||
|
||
만약 이 일치가 깨지면 (예: VirtualAllocator 의 page_size 를 PE_MMU 와
|
||
다르게 인스턴스화) MMU `map()` 가 서브-페이지 region 모드 (ADR-0039 D3) 로
|
||
흐른다.
|
||
|
||
VA 기본 범위: `va_base = 0x1_0000_0000` (= 4 GiB), `va_size = 64 GiB`. 이
|
||
값은 `_ensure_allocators` 에 하드코딩되어 있으며 ADR-0011 의 VA 모델에서
|
||
직접적인 의미를 갖지는 않는다 — 단지 host 코드와 충돌하지 않을 만큼 큰
|
||
주소 공간을 device-wide 로 잡아둔 것.
|
||
|
||
### D5. allocator 인스턴스의 lifecycle
|
||
|
||
- `RuntimeContext._ensure_allocators` 가 lazy 하게 호출됨 (`_create_tensor`
|
||
의 첫 호출 시점).
|
||
- 한 번 생성된 allocator dict (`self._allocators`) 는 RuntimeContext 의
|
||
lifetime 동안 재사용. 같은 process 안의 두 번째 deploy 는 새 객체를
|
||
만들지 않는다.
|
||
- `RuntimeContext.cleanup()` 이 모든 living tensor 의 `_free_tensor()` 를
|
||
호출 → MMU unmap + `va_allocator.free` + `pemem_allocator.free_hbm` 으로
|
||
free list 가 원상복구. 다음 RuntimeContext 가 다시 만들면 초기 상태부터.
|
||
|
||
allocator 상태가 RuntimeContext 간에 공유되지 않는 점이 단일 process 안의
|
||
연속 실행에서 deploy → cleanup → deploy 의 결정성을 보장한다.
|
||
|
||
### D6. Allocator 실패는 raise 한다 (silent OOM 금지)
|
||
|
||
`_FreeList.alloc` / `VirtualAllocator.alloc` 모두 충분한 free block 이
|
||
없으면 `AllocationError` / `VaAllocationError` 를 던진다. 메시지에는
|
||
"required size + largest available block" 가 포함되어, fragmentation
|
||
인지 진짜 OOM 인지 진단 가능.
|
||
|
||
silent fallback (예: 가장 큰 블록만큼만 alloc) 는 절대 금지 — 부분 할당된
|
||
텐서가 SimPy 단계에 들어가면 라우팅·DMA 가 잘못된 PA 를 인지하여 시뮬
|
||
정확도가 깨진다.
|
||
|
||
### D7. address space 와 allocator 의 1:1 대응
|
||
|
||
물리 주소 공간 분리는 PhysAddr 의 sub-unit (ADR-0001 D2.3) 으로 표현되며,
|
||
각 sub-unit 마다 별도 allocator 인스턴스를 둔다:
|
||
|
||
- HBM slice → `PEMemAllocator._hbm`.
|
||
- PE TCM → `PEMemAllocator._tcm`.
|
||
- (현재 미사용) M_CPU local memory, CUBE SRAM → 별도 allocator 필요. 현재
|
||
구현은 아직 IPCQ-only slot 으로 처리 (ADR-0023 D9.7) 하며 PA 공간을
|
||
share 하지 않으므로 별도 free-list 가 없음.
|
||
|
||
cube-level SRAM allocator 가 필요해지면 `_FreeList(cfg.sram_bytes_per_cube)`
|
||
인스턴스를 cube 단위로 추가한다 (`cfg.sram_bytes_per_cube` 는 이미
|
||
`AddressConfig` 에 정의되어 있어 데이터 모델은 준비됨).
|
||
|
||
## Alternatives Considered
|
||
|
||
### A1. best-fit / buddy allocator
|
||
|
||
기각 (현재). 워크로드의 alloc/free 패턴이 stack-like (deploy 순서 = free
|
||
순서) 라 first-fit + coalescing 으로 fragmentation 이 충분히 통제된다.
|
||
LLM kernel sweep 에서 long-running fragmentation 이 관찰되면 buddy 로
|
||
교체하는 ADR 을 별도로 만든다.
|
||
|
||
### A2. partial overlap free 검증 추가
|
||
|
||
기각. D2 의 신뢰 모델 + O(N) 검사 비용. 단, 디버그 모드 (`KERNBENCH_DEBUG`
|
||
env var 등) 에서 활성화하는 옵션은 후속 작업으로 가능.
|
||
|
||
### A3. VA 와 PA 의 통합 allocator
|
||
|
||
기각. VA 공간 (64 GiB device-wide) 과 PA 공간 (slice 별 ~6 GiB) 는 의미
|
||
차원이 다르다. VA 는 host kernel 의 view, PA 는 device sub-unit 의 view.
|
||
ADR-0011 의 VA 모델 정신 (MMU 가 둘 사이를 매핑) 과 정합하기 위해
|
||
allocator 도 분리.
|
||
|
||
### A4. page_size 의 multi-tier 지원 (large page + small page)
|
||
|
||
기각 (현재). 단일 page_size (현재 2 MiB) 가 LLM kernel 의 텐서 단위 (수
|
||
MiB~수 GiB) 에 맞고, ADR-0039 D3 의 서브-페이지 region 으로 작은 매핑이
|
||
필요할 때 흡수된다. multi-tier page 는 MMU 자체 모델을 확장해야 하므로
|
||
별도 ADR 후보.
|
||
|
||
## Consequences
|
||
|
||
- allocator 알고리즘이 ADR-level 에서 굳어져 (D1·D3·D4), 새로운 시뮬
|
||
시나리오에서 fragmentation 이슈가 발생할 때 "여기서 first-fit + coalesce
|
||
를 쓰고 있다" 가 명확.
|
||
- D2 의 신뢰 모델이 명시되어, 향후 사용자 입력으로부터 직접 alloc/free 를
|
||
받는 경로가 도입되면 본 ADR supersede 가 필요함을 일찍 인지 가능.
|
||
- D7 의 sub-unit별 allocator 1:1 대응이 명시되어, M_CPU/SRAM 별도 영역이
|
||
필요해질 때 어디에 free-list 를 추가해야 하는지 명확.
|
||
- `VirtualAllocator` 의 page_size 가 PE_MMU 설정과 일치해야 함이 D4 에
|
||
적혀 있어, 향후 topology.yaml 의 page_size 변경 시 ADR-0039 stopgap 동작
|
||
과의 상호작용을 빠르게 가늠 가능.
|