# 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 동작
  과의 상호작용을 빠르게 가늠 가능.
