본문으로 건너뛰기

L2 어댑터 (L2AdapterInterface)

[!tldr] 업무 관점 takeaway L2 어댑터는 LMCache가 NVMe SSD에 KV 캐시를 읽고 쓰는 실제 I/O 손발이다. FDP Backend를 만들려면 L2AdapterInterface를 구현하면 된다 — LMCache 코드 수정 없이 plugin 타입으로 등록 가능. submit_store_task의 진입 직전이 FDP placement_id를 결정하는 자연스러운 훅 포인트다.


계층 관계

StorageManager ← 서빙 엔진(vLLM)이 부르는 진입점

├─ StoreController ← L1 쓰기 완료 → L2에 복제 (L1→L2)
└─ PrefetchController ← 외부 prefetch 요청 → L2→L1 로드


L2AdapterInterface ← 실제 I/O 손발 (raw_block, dax, nixl, s3 ...)

두 컨트롤러는 같은 어댑터 인스턴스를 공유 → 어댑터는 thread-safe 필수.

컨트롤러어댑터에 요청하는 것방향
StoreControllersubmit_store_taskL1 → L2 (쓰기)
PrefetchControllersubmit_lookup_and_lock_tasksubmit_load_taskL2 → L1 (읽기)

인터페이스 — 메서드 3종 4쌍

모든 작업은 submit → eventfd 신호 대기 → 결과 조회 3단 패턴.

동작submit결과 조회호출자
Storesubmit_store_task(keys, objects) → L2TaskIdpop_completed_store_tasks() → {id: bool}StoreController
Lookup+Locksubmit_lookup_and_lock_task(keys) → L2TaskIdquery_lookup_and_lock_result(id) → Bitmap|NonePrefetchController
Loadsubmit_load_task(keys, objects) → L2TaskIdquery_load_result(id) → Bitmap|NonePrefetchController
Unlocksubmit_unlock(keys) → None (fire-and-forget)PrefetchController

EventFD 3종

각 어댑터는 3개의 독립된 eventfd를 노출:

get_store_event_fd() → store 완료 시 시그널
get_lookup_and_lock_event_fd() → lookup 완료 시 시그널
get_load_event_fd() → load 완료 시 시그널

핵심 invariant: 모든 어댑터 × 모든 op 에 걸쳐 fd가 전역 고유해야 한다. 중복 시 fd → adapter_index 맵이 깨져 silent misroute 발생.


Store 흐름 (L1 → L2)

L1.finish_write → StoreListener (eventfd 신호, L1 lock 내부 — 논블로킹 필수)
→ _process_new_keys
→ shape별 그룹화 (model_name, kv_rank)
→ StorePolicy.select_store_targets ← 어떤 어댑터에 보낼지
→ L1Manager.reserve_read ← read lock (eviction 방지)
→ adapter.submit_store_task
── store_efd 시그널 ──
→ pop_completed_store_tasks
→ L1Manager.finish_read ← lock 해제
→ StorePolicy.select_l1_deletions ← L1 evict 여부 결정

Lock 상태: unlocked → (store 중) read-locked → unlocked


Prefetch 흐름 (L2 → L1)

submit_prefetch_request(keys)
→ _start_lookup_phase
→ 모든 어댑터에 lookup_and_lock_task 제출
→ _transition_to_load_phase
→ PrefetchPolicy.select_load_plan ← key를 어댑터에 비중복 할당
→ trim_load_plan_to_prefix ← 연속된 prefix만 남김 ★
→ L1Manager.reserve_write (is_temporary=True)
→ Phase 1 unlock (plan 외 key)
→ adapter.submit_load_task
→ _finalize_load
→ Phase 2 unlock (plan 내 모든 key)
→ L1Manager.finish_write_and_reserve_read ← write→read 원자적 전환

Prefix-only 이유: vLLM은 KV 캐시를 연속 prefix로만 사용 가능. 중간 gap이 있으면 뒤는 의미 없음. key {0,1,2,4,5} 있고 3이 없으면 {0,1,2}만 로드.

Lock 상태: 없음 → (lookup) L2 locked → (load 중) L1 write-locked + L2 locked → (완료) L1 read-locked + L2 unlocked


어댑터 종류

카테고리어댑터비고
로컬 블록/메모리raw_block_l2_adapter.py, dax_l2_adapter.py연구 타깃
로컬 FSfs_l2_adapter.py, fs_native_l2_adapter.py
원격 KV/오브젝트s3_l2_adapter.py, resp_l2_adapter.py (Redis), mooncake_store_l2_adapter.py
NIXLnixl_store_l2_adapter.py, nixl_store_dynamic_l2_adapter.py
Plugin 브리지plugin_l2_adapter.py, native_plugin_l2_adapter.pyFDP 진입점
Native 브리지native_connector_l2_adapter.pyC++/Rust IStorageConnector
인프라base.py, factory.py, config.py
테스트mock_l2_adapter.py인메모리 레퍼런스

자동 등록 메커니즘

l2_adapters/__init__.pypkgutil.iter_modules()*_l2_adapter.py 자동 발견.
각 모듈은 register_l2_adapter_type(name, ConfigCls) + register_l2_adapter_factory(name, factory_fn)으로 self-register → 새 어댑터 추가 시 기존 파일 수정 불필요.


핵심 invariant 10개

  1. EventFD 전역 고유
  2. L2TaskId는 per-adapter (전역 키는 (adapter_index, task_id))
  3. Query는 one-shot (다시 호출하면 None)
  4. submit_unlock은 어댑터가 반드시 결국 성공 (caller는 절대 retry 안 함) → 실패 시 스토리지 누수
  5. Prefix-only loading — gap 발생 시 뒤 key 버림
  6. StoreListener callback은 L1Manager lock 내부 → 논블로킹 필수, L1Manager 호출 금지 (deadlock)
  7. prefetch L1 write 버퍼는 is_temporary=True (eviction 허용)
  8. finish_write_and_reserve_read()로 write→read 전환은 원자적 (eviction window 없음)
  9. stop()은 항상 in-flight lock 정리
  10. 어댑터는 thread-safe (Store/Prefetch 두 스레드 동시 호출 전제)

FDP 삽입 관점

submit_store_task 진입 직전이 FDP placement_id를 결정할 수 있는 자연스러운 지점:

# StoreController._process_new_keys 내부
adapter.submit_store_task(keys, objs)

# 여기서 keys → FDP placement_id 결정 로직 삽입 가능
# io_uring_cmd의 dspec 필드로 전달

plugin 타입을 쓰면 LMCache 코드 수정 없이 FDP 로직을 외부에서 주입 가능.


Open Questions

  • select_store_targets은 한 key를 여러 어댑터에 보낼 수 있는데, select_load_plan은 non-overlapping 강제. 다중 어댑터 환경에서 L2 간 일관성은 누가 보장?
  • mock_l2_adapter가 thread-safety와 invariant를 충족하는지 — 새 어댑터 작성 시 기준점으로 사용 가능?

관련 페이지

  • [[LMCache-MP-NonMP-모드]] — MP 모드에서만 L2AdapterInterface 사용, Non-MP는 StoragePluginInterface
  • [[Plugin-Pipeline]] — 외부 어댑터 등록 메커니즘 (LMCACHE_STORAGE_BACKEND=plugin)
  • [[raw_block-io_uring-cmd]] — Rust 레이어까지 내려가는 FDP dspec 전달 경로
  • [[raw_block-종단-분석]] — L4(Rust) 호출 체인 전계층 분석
  • [[NVMe-FDP]] — submit_store_task에 삽입할 placement_id 개념