본문으로 건너뛰기

Raw Block Storage Stack 분석 메모

분석 범위: core.py + raw_block_l2_adapter.py (MP path) + rust_raw_block_backend.py (legacy path)

변경 검증 가이드 (다음 fetch 후):

git log 29bbd553..HEAD -- lmcache/v1/storage_backend/raw_block/core.py \
lmcache/v1/storage_backend/raw_block/key_codec.py
  • _write_checkpoint / _snapshot_state / _free_slots 구현이 변경되면 B1·P1·B2 진단이 stale.
  • _checkpoint_once idle 로직이 바뀌면 D2 정책 이슈가 해소됐을 수 있음.

역할 요약

RawBlockCore는 legacy non-MP와 MP L2 path 양쪽에서 공유하는 raw-block I/O 엔진. 슬롯 할당/해제, 체크포인트/복구, lock refcount, Rust lmcache_rust_raw_block_io 바인딩을 소유.


발견 이슈

B — 버그 / 논리 오류

B1. _meta_seq 레이스 컨디션 (medium / latent)

위치: core.py:1173

_write_checkpointself._lock 밖에서 next_seq = self._meta_seq + 1을 읽는다.

현재는 안전하다. 작성자가 이를 인지하고 설계한 것으로 보임:

  • close()_meta_thread.join() 완료 후 checkpoint를 쓰므로 background thread와 close() 사이에 레이스 없음
  • 프로덕션 코드에서 checkpoint_now()의 호출자는 현재 테스트 1곳뿐 (tests/v1/storage_backend/test_raw_block_core.py:114)

암묵적 가정: _write_checkpoint는 "한 번에 한 스레드만 호출한다"는 전제 위에 설계됐다. 이 가정이 유지되는 한 next_seq를 락 밖에서 읽어도 문제없다.

레이스가 실재하는 조건 (현재는 해당 없음):

  • adapter/plugin이 checkpoint_now()를 외부에서 주기적으로 호출하는 기능 추가 시
  • meta_enable_periodic=True 상태에서 checkpoint_now() 동시 호출 시

결론: 현재 아키텍처에서는 버그가 아니다. 단, checkpoint_now()가 public API인 이상 호출 제약을 docstring에 명시하거나 _checkpoint_lock으로 직렬화를 enforce하지 않으면 향후 확장 시 조용히 터질 수 있는 잠재 취약점.

B2. _append_free_slot_locked O(n) 중복 검사 (low / cosmetic)

위치: core.py:1019

_free_slotslist이므로 slot in self._free_slots 중복 검사는 O(n). 단, caller 3곳 모두 slot lifecycle 구조상 duplicate가 실제로 발생하지 않는다:

callerslot 출처중복 가능성
delete_many L685self._index.pop() — 동일 key는 index에 1번만없음
put_many 실패 L502self._inflight.pop() — 역시 1번만없음
_validate_loaded_entries L1441to_drop list, index에서 각 key 1번없음

즉 guard는 순수 방어 코드이며 정상 동작에서는 항상 통과된다.

O(n) 비용이 실질적으로 문제가 되는 조건:

  • _free_slots가 F개 쌓인 상태에서 D개 bulk delete 시 비교 횟수 = D×F + D(D-1)/2
  • 하지만 캐시가 꽉 찬 직후 eviction이 시작될 때 _free_slots ≈ 0이므로 F ≈ 0 → O(D)에 수렴
  • _free_slots가 크게 쌓이는 시나리오("많이 지우고 잠깐 put 안 하는" 경우)에서만 O(D×F) 발생

단독 수정 가치는 낮다. 의미 있는 이유는 P2(FDP FIFO)를 구현할 때 자연스럽게 함께 처리할 수 있다는 점이다. deque + set 교체 하나로 O(1) dedup + FIFO 동시 해결:

from collections import deque

# __init__
self._free_slots: deque[int] = deque()
self._free_slots_set: set[int] = set()

def _append_free_slot_locked(self, slot: int) -> None:
if slot < 0 or slot >= self._max_slots:
return
if slot in self._free_slots_set: # O(1)
return
self._free_slots.append(slot)
self._free_slots_set.add(slot)

def _allocate_slot_locked(self) -> int:
if self._free_slots:
slot = self._free_slots.popleft() # FIFO (P2 동시 해결)
self._free_slots_set.discard(slot)
return self._slot_to_offset(slot)
...

주의: _apply_loaded_state core.py:1354에서 self._free_slots = [...]로 직접 덮어쓰는 부분도 함께 수정 필요.


P — 성능 이슈

P1. _snapshot_state lock 내 직렬화 작업 (medium / conditional)

위치: core.py:1102-1145

with self._lock: 안에서 전체 index dict comprehension이 실행된다. entry마다 list(shape), hasattr() 2회, .tolist() 조건부 실행 포함.

실질 영향은 조건부다:

  • _checkpoint_onceidle_ok(inflight_io_count == 0) 조건에서만 실행 → 정상 운용 중에는 I/O와 checkpoint가 겹치지 않음
  • 따라서 lock 경합이 실제로 발생하려면 checkpoint가 도는 순간 별도 스레드가 put_many/delete_many를 호출해야 함
  • entry < ~1,000이면 lock 보유 시간 수 ms 미만 → 무시 가능
  • entry > ~10,000이고 checkpoint 빈도가 높은 경우에만 체감 영향

개선 가치: 대용량 NVMe(수만 entry 목표 설계)에서는 선제적으로 수정할 이유가 있다. shallow copy 분리로 lock 보유 구간을 크게 줄일 수 있다:

# 락 안에서는 shallow copy만
with self._lock:
dirty_total = self._meta_dirty_total
index_snapshot = dict(self._index) # O(n) but no per-entry work
free_slots_copy = list(self._free_slots)
next_slot = self._next_slot
# 락 밖에서 직렬화 (per-entry Python work 여기서)
snapshot["entries"] = {k: _entry_to_dict(v) for k, v in index_snapshot.items()}

P2. _free_slots LIFO → FDP/HC-SSD wear에 불리 (FDP 전용)

위치: core.py:1007

_free_slots.pop() (LIFO)는 최근 해제된 슬롯을 즉시 재사용한다.

SSD 타입별 영향:

SSD 타입LIFO 영향
일반 NVMe (TLC/QLC)중립~약간 유리 — FTL이 자체 wear leveling, LIFO로 SLC cache 재활용 가능
DRAM/ramdisk무관
FDP-capable NVMe불리 — host가 placement 직접 제어, LIFO로 특정 RU에 집중 기록
HC-SSD불리 — 동일 이유

측정 방법:

  • 단기 latency/throughput 측정으로는 차이 없음 (I/O 속도 자체는 동일)
  • _allocate_slot_locked에 slot histogram 계측 → LIFO/FIFO 분포 즉시 확인 가능
  • WAF 측정: nvme smart-logdata_units_written 비교 → 수일 단위 sustained load 필요
  • FDP RU 분포: nvme fdp stats → FDP 장치에서 수 시간 부하 후 확인

일반 SSD 사용자에게 FIFO 강제는 regression 가능free_slot_order: "lifo"|"fifo" config로 선택하거나 FDP 활성화 여부로 분기하는 설계가 바람직. B2와 통합 수정 시 자연스럽게 해결 가능 (deque.popleft FIFO).

P3. put_many 순차 write, io_uring batch 미활용 (medium)

위치: core.py:434-521

put_many 루프가 각 key에 대해 _write_one을 동기적으로 호출하므로 io_uring SQ에 여러 SQE를 한 번에 제출하는 batch I/O를 활용하지 못한다. Rust 바인딩에 pwrite_batch 인터페이스가 추가되면 여기서 활용할 수 있다.


Q — 코드 품질 (coding_standards.md 기준)

#위치문제
Q1core.py:9Optional import + 사용 (standards: prefer X | None)
Q2core.py:58-62Any 타입 힌트 — io_engine: str | None, use_iouring: bool | None으로 명시 가능
Q3core.py:463zip(strict=False) — 이미 길이 검증 완료 후이므로 strict=True가 방어적으로 더 적절
Q4core.py:602except Exception: pass — silently drop cast failure, 최소 logger.debug 권장

D — 설계 주의사항

D1. 체크포인트 JSON 포맷 확장성

위치: core.py:1161-1171

대형 NVMe에 KV 텐서 수만 개가 올라오면 JSON 페이로드가 수 MB가 된다. _meta_payload_capacity 초과 시 checkpoint를 통째로 skip하는데, 이 경우 crash 후 데이터는 device에 있지만 index가 날아간다. 용량 초과 warning 외 별도 알람이 없다.

D2. 지속 I/O 하에서 checkpoint 미발생 가능성

위치: core.py:1200-1211

지속적인 I/O load 하에서 idle_ok = False이므로 periodic checkpoint가 전혀 발생하지 않는다. close()force=True checkpoint가 최후 보루인데, SIGKILL 등 비정상 종료 시 최신 index를 잃는다. max_dirty_threshold 추가로 일정 수 이상 변경 시 강제 checkpoint하는 방어 로직 필요.

D3. 단일 글로벌 락

index 조회, 슬롯 할당, lock refcount, inflight 추적 모두 self._lock 하나를 사용. 실제 I/O는 락 밖에서 하므로 당장은 큰 문제 없으나, 다수 vLLM worker가 동시에 exists_many/load_many_into를 호출할 때 read 경합 발생 가능.


우선순위 요약 (core.py 범위)

우선순위항목실질 영향수정 난이도
향후 확장 전 정리B1 (checkpoint_now 호출 제약 docstring 명시)현재는 안전, latent only낮음
FDP 구현 시 통합 수정B2 + P2 세트 (deque + set 교체, FIFO)B2 단독은 낮음; P2는 FDP 전용 효과낮음
대용량 NVMe 설계 시 선제 수정P1 (lock 내 shallow copy 분리)entry < 1,000 이면 거의 무관; 수만 entry 목표라면 의미 있음낮음
코드 정리Q1~Q4 (타입 힌트, zip strict)기능 영향 없음낮음
중기 검토D1 (JSON 포맷 한계 대응)수만 entry 시 checkpoint skip 위험중간
중기 검토D2 (dirty threshold checkpoint)지속 부하 하 비정상 종료 시 index 손실낮음
장기 / Rust 연동P3 (io_uring batch write)io_uring 효과 미활용높음 — Rust 바인딩 확장 필요

Adapter layer 분석 (T1/T2/L1~L4) 은 별도 문서 참조: [[raw_block_stack_analysis]] — raw_block_l2_adapter.py + rust_raw_block_backend.py + core.py stack 전체 I/O lifecycle 분석