L2 Adapters — Contract & 등록 흐름
원문: lmcache/v1/distributed/l2_adapters/base.py, factory.py, config.py
L1 — Contract
추상 메서드 목록 (L2AdapterInterface, base.py:78)
| 메서드 | 필수 여부 | 설명 |
|---|---|---|
get_store_event_fd() | 필수 | store 완료 신호용 fd |
get_lookup_and_lock_event_fd() | 필수 | lookup 완료 신호용 fd |
get_load_event_fd() | 필수 | load 완료 신호용 fd |
submit_store_task(keys, objects) | 필수 | 배치 store 제출 → L2TaskId |
pop_completed_store_tasks() | 필수 | 완료된 store 드레인 → dict[id, bool] |
submit_lookup_and_lock_task(keys) | 필수 | lookup + lock 제출 → L2TaskId |
query_lookup_and_lock_result(id) | 필수 | lookup 결과 one-shot 조회 → Bitmap|None |
submit_unlock(keys) | 필수 | fire-and-forget unlock |
submit_load_task(keys, objects) | 필수 | 배치 load 제출 → L2TaskId |
query_load_result(id) | 필수 | load 결과 one-shot 조회 → Bitmap|None |
close() | 필수 | 리소스 해제 |
pop_completed_store_task_bytes() | 선택 | 실제 전송된 bytes 수 (기본 {}) |
delete(keys) | 선택 | eviction 시 삭제 (기본 no-op) |
get_usage() | 선택 | 사용량 스냅샷 반환 (기본 base class 구현) |
report_status() | 선택 | 헬스 체크 (기본 is_healthy: True) |
base class 가 대신 처리해주는 것
새 어댑터를 만들 때 직접 구현하지 않아도 되는 것들:
- byte 회계 (
_usage_lock+_total_bytes_used+_bytes_by_cache_salt):_notify_keys_stored(keys, sizes)/_notify_keys_deleted(keys, sizes)를 호출하면 base class 가 자동 집계 get_usage():_notify_*만 제대로 호출하면 base class 구현이 그대로 작동- Listener 관리:
register_listener()+_notify_keys_accessed()포함해서 base class 가 관리
→ 새 어댑터는 추상 메서드 11개만 구현하고, _notify_keys_stored / _notify_keys_deleted 를 올바른 시점에 호출하면 됨.
AdapterUsage (base.py:35)
AdapterUsage (frozen dataclass)
├─ total_bytes_used: int ← 전체 사용 bytes
├─ total_capacity_bytes: int ← 최대 용량 (0 = unknown)
├─ bytes_by_cache_salt: Mapping ← salt 별 사용 bytes (read-only)
└─ usage_fraction: float ← 사용률 [0, 1], 용량 미지 → -1.0
bytes_by_cache_salt는MappingProxyType(불변 스냅샷) — 호출자가 수정 불가usage_fraction == -1.0→ eviction 신호 없음 (레거시 sentinel 유지)cache_salt= user/vLLM deployment/격리 단위 어떤 것이든 (어댑터는 무관심)
supports_global_eviction (base.py:457)
@property
def supports_global_eviction(self) -> bool:
return self._max_capacity_bytes > 0
max_capacity_bytes=0으로 생성하면 전역 eviction 비활성화- per-cache_salt quota eviction 은 이 플래그와 무관하게 작동 가능
L2 — 등록 흐름과 생성 경로
두 레지스트리
_L2_ADAPTER_CONFIG_REGISTRY (config.py)
name → config class (예: "raw_block" → RawBlockConfig)
_L2_ADAPTER_FACTORY_REGISTRY (factory.py)
name → factory callable (예: "raw_block" → _create_raw_block_adapter)
두 레지스트리는 분리되어 있고 모두 self-register 패턴으로 채워짐:
- 각
*_l2_adapter.py모듈이 import 되는 순간 모듈 하단의register_l2_adapter_type(...)+register_l2_adapter_factory(...)가 실행됨
Lazy import 흐름
__init__.py (pkgutil 스캔)
→ add_pending_module("lmcache.v1.distributed.l2_adapters.raw_block_l2_adapter")
→ (실제 import 는 하지 않음, _PENDING_MODULES 에만 추가)
-- 사용자가 --l2-adapter '{"type":"raw_block",...}' 지정 --
parse_args_to_l2_adapters_config()
→ type_name = "raw_block"
→ _ensure_config_loaded("raw_block")
→ ensure_adapter_loaded("raw_block") (factory.py)
→ _PENDING_MODULES 에서 하나씩 pop → importlib.import_module(mod_path)
→ raw_block_l2_adapter.py import 됨
→ register_l2_adapter_type("raw_block", RawBlockConfig)
→ register_l2_adapter_factory("raw_block", _create_raw_block)
→ 레지스트리에 등록 확인 → 완료
→ config_cls = _L2_ADAPTER_CONFIG_REGISTRY["raw_block"] → RawBlockConfig
→ adapter_cfg = RawBlockConfig.from_dict(d)
→ adapter_cfg.eviction_config = _parse_eviction_config(d) ← JSON "eviction" 키
→ adapter_cfg.persist_config = _parse_persist_config(d) ← JSON "persist_enabled" 키
→ adapter_cfg.serde_config = _parse_serde_config(d) ← JSON "serde" 키
-- 실제 어댑터 생성 --
create_l2_adapter_from_registry(config, l1_memory_desc)
→ name = get_type_name_for_config(config) ← 역방향 lookup (config 클래스 → 이름)
→ ensure_adapter_loaded(name) ← 이미 로드됐으면 no-op
→ factory = _L2_ADAPTER_FACTORY_REGISTRY[name]
→ return factory(config, l1_memory_desc) ← 어댑터 인스턴스 생성
Serde 끼어드는 지점 (config.py:173)
adapter_cfg.serde_config 가 None 이 아니면
StorageManager 가 어댑터를 SerdeL2AdapterWrapper 로 감쌈
컨트롤러 입장에서는 평범한 L2AdapterInterface 로 보임
serde (예: fp8 quantization) 가 store/load 전후에 투명하게 실행됨
즉 컨트롤러는 serde 를 모르고, 어댑터도 serde 를 모름. 중간에 wrapper 가 끼어드는 구조.
Config JSON 구조 예시
{
"type": "raw_block",
"path": "/dev/nvme0n1",
"eviction": {
"eviction_policy": "LRU",
"trigger_watermark": 0.8,
"eviction_ratio": 0.2
},
"persist_enabled": true,
"serde": {
"type": "fp8",
"fp8_dtype": "float8_e4m3fn"
}
}
eviction_policy 가능 값: "LRU", "IsolatedLRU", "noop"
L3 — FDP / HC-SSD 삽입 지점 예비 후보
_notify_keys_stored(keys, sizes)호출 시점 직전: key 의cache_salt기반으로 FDP placement ID(RUH) 를 결정할 수 있음serde_config+SerdeL2AdapterWrapper의 store/load 훅: HC-SSD 측 compression offload 의 자연스러운 끼워넣기 지점
L4 — io_uring (해당 없음)
base.py / factory.py / config.py 수준에서는 io_uring 관련 없음. raw_block/core.py 에서 분석 (TODO 4).
Open Questions
get_type_name_for_config(config)가 레지스트리를 선형 탐색 (type(config) is cls비교) 함. 어댑터가 수십 개일 때 성능 영향은 무시 가능한 수준인지? (생성 시 1회라 무시 가능할 듯)pop_completed_store_task_bytes()가 선택 메서드인데, 구현 안 하면 L2 throughput 히스토그램이 "제출된 bytes" 기준으로 계산됨. raw_block 은 fast-path (중복 key skip) 가 있으므로 구현 필요 여부 확인 필요 (TODO 4에서 확인).