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 필수.
| 컨트롤러 | 어댑터에 요청하는 것 | 방향 |
|---|---|---|
StoreController | submit_store_task | L1 → L2 (쓰기) |
PrefetchController | submit_lookup_and_lock_task → submit_load_task | L2 → L1 (읽기) |
인터페이스 — 메서드 3종 4쌍
모든 작업은 submit → eventfd 신호 대기 → 결과 조회 3단 패턴.
| 동작 | submit | 결과 조회 | 호출자 |
|---|---|---|---|
| Store | submit_store_task(keys, objects) → L2TaskId | pop_completed_store_tasks() → {id: bool} | StoreController |
| Lookup+Lock | submit_lookup_and_lock_task(keys) → L2TaskId | query_lookup_and_lock_result(id) → Bitmap|None | PrefetchController |
| Load | submit_load_task(keys, objects) → L2TaskId | query_load_result(id) → Bitmap|None | PrefetchController |
| Unlock | submit_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 | 연구 타깃 |
| 로컬 FS | fs_l2_adapter.py, fs_native_l2_adapter.py | |
| 원격 KV/오브젝트 | s3_l2_adapter.py, resp_l2_adapter.py (Redis), mooncake_store_l2_adapter.py | |
| NIXL | nixl_store_l2_adapter.py, nixl_store_dynamic_l2_adapter.py | |
| Plugin 브리지 | plugin_l2_adapter.py, native_plugin_l2_adapter.py | FDP 진입점 |
| Native 브리지 | native_connector_l2_adapter.py | C++/Rust IStorageConnector |
| 인프라 | base.py, factory.py, config.py | |
| 테스트 | mock_l2_adapter.py | 인메모리 레퍼런스 |
자동 등록 메커니즘
l2_adapters/__init__.py가 pkgutil.iter_modules()로 *_l2_adapter.py 자동 발견.
각 모듈은 register_l2_adapter_type(name, ConfigCls) + register_l2_adapter_factory(name, factory_fn)으로 self-register → 새 어댑터 추가 시 기존 파일 수정 불필요.
핵심 invariant 10개
- EventFD 전역 고유
L2TaskId는 per-adapter (전역 키는(adapter_index, task_id))- Query는 one-shot (다시 호출하면 None)
submit_unlock은 어댑터가 반드시 결국 성공 (caller는 절대 retry 안 함) → 실패 시 스토리지 누수- Prefix-only loading — gap 발생 시 뒤 key 버림
StoreListenercallback은 L1Manager lock 내부 → 논블로킹 필수, L1Manager 호출 금지 (deadlock)- prefetch L1 write 버퍼는
is_temporary=True(eviction 허용) finish_write_and_reserve_read()로 write→read 전환은 원자적 (eviction window 없음)stop()은 항상 in-flight lock 정리- 어댑터는 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 개념