S2: raw_block checkpoint payload overflow
대상:
lmcache/v1/storage_backend/raw_block/core.py영향: MP / non-MP 공통 (RawBlockCore공유) 심각도: 잠복형 스케일 버그 — 대용량 SSD 프로덕션 배포 전 수정 필요
TL;DR
raw_block은 재시작 복구를 위해 키→슬롯 인덱스를 64 MB JSON으로 디바이스에 백업한다. 엔트리가 ~32만 개(≈ 82M distinct 토큰)를 초과하면 백업이 silent fail하고, 다음 재시작 시 캐시 전량이 유실된다. 3.84 TB 이상 디바이스 + 70B/480B 모델 조합에서 디바이스가 가득 차면 반드시 도달한다.
1. 버그 메커니즘
디바이스 레이아웃 (core.py:975)
┌────────────────────────┬──────────────────────────────────────┐
│ ① 메타데이터 영역 │ ② 데이터 영역 │
│ (offset 0 ~ 128 MB) │ (128 MB ~ 디바이스 끝) │
│ key→offset 인덱스 │ 고정 크기 slot_bytes 슬롯 배열 │
└────────────────────────┴──────────────────────────────────────┘
런타임 인덱스는 RAM에만 존재 (core.py:241)
self._index: dict[str, _Entry] = {} # 키 → _Entry(offset, size, meta)
휘발성. 재시작하면 증발 → 데이터 영역 KV가 물리적으로 멀쩡해도 슬롯↔키 매핑 유실.
Checkpoint = 인덱스를 디바이스에 직렬화 (core.py:1200, 1102)
snapshot, _ = self._snapshot_state() # _index → dict
payload = json.dumps(snapshot, ...).encode() # → JSON bytes
return self._write_checkpoint(payload, ...) # → 메타 영역에 기록
엔트리 1개 JSON ≈ 209 B (dense, cached_positions=None):
"model@0@<64자 해시>": {"offset":…,"size":…,"shape":[…],"dtype":"bfloat16","fmt":"…","cached_positions":null}
payload_cap: 64 MB 천장 (core.py:1032, 231)
메타 영역은 2벌 미러링(_meta_copy_count=2), 한 벌당 용량:
_meta_container_bytes = (meta_total_bytes // 2 // block_align) * block_align
payload_cap = _meta_container_bytes - block_align
# 기본값(128 MB, block_align=4096) → payload_cap ≈ 64 MB
천장 초과 시 silent skip (core.py:1164)
if len(payload) > payload_cap:
logger.warning(
"RawBlockCore metadata payload too large (%d > %d), skipping checkpoint",
len(payload), payload_cap,
)
return False # 예외 없음, WARNING 한 줄, 메타 영역 미기록
복구 경로는 checkpoint가 유일
_validate_loaded_entries— checkpoint에서 로드된_index를 검증만 함. 슬롯 스캔으로 인덱스를 재구축하는 경로 없음.- 슬롯 헤더(
_encode_header)에는slot_identity(8 B 해시) +payload_len만 있고 원본 키 문자열 없음 → 헤더만으로 키 복원 불가. - ∴ checkpoint JSON이 키→offset 매핑의 유일한 영구 저장소. fallback 없음.
실패 시나리오
인덱스 누적 → JSON > 64 MB → checkpoint 매번 silent skip
→ 메타 영역은 이전 상태(또는 meta_seq=0 빈 상태)
→ 운영자 인지 불가 (WARNING 한 줄, 알림/예외 없음)
→ 재시작/크래시 → RAM의 _index 증발
→ 메타 영역에서 복구 → 비었거나 구버전 → KV 전부 미아
→ 캐시 전량 유실 (물리 데이터는 디스크에 있으나 키를 모름)
2. Overflow 임계점
- 임계 엔트리 수:
64 MB / 209 B ≈ 321,000 - 임계 토큰 수: ≈ 82 M distinct KV (chunk_size=256 기준)
- sparse(
cached_positions있음) 엔트리는 1개 ≈ 1.2 KB → 훨씬 빨리 도달
slot_bytes 주의: non-layerwise 기본에서
slot_bytes = full_chunk_bytes이며 full_chunk_bytes는num_layers × chunk_tokens × hidden_dim × dtype_bytes에서 파생. TP가 높을수록(KV head 분산) slot 작아짐 → 같은 디바이스에 엔트리 더 많이 쌓임.
| 설정 | slot_bytes | overflow 임계 (디바이스 가득 찰 때) |
|---|---|---|
| non-layerwise — Llama-3-8B TP=1 | 32 MB | 9.8 TB |
| non-layerwise — Llama-3-70B TP=8 | 10 MB | 3.1 TB |
| non-layerwise — Qwen3-480B TP=8 | 7.75 MB | 2.4 TB |
| layerwise=True | ~1 MB | 314 GB |
| sparse (cached_positions 있음) | 10 MB | 0.5 TB |
디바이스 크기별 실제 overflow 여부 (Qwen3-480B TP=8 기준)
| 디바이스 | max 엔트리 | JSON | overflow |
|---|---|---|---|
| 1.92 TB | 260 K | 52 MB | 간당간당 |
| 3.84 TB | 520 K | 104 MB | YES |
| 7.68 TB | 1.04 M | 207 MB | YES |
| 15 TB | 2.08 M | 414 MB | YES |
| 30 TB | 4.16 M | 828 MB | YES |
핵심: HC SSD 전용 문제가 아님. 큰 모델(높은 TP로 slot 작아짐) + 3.84 TB 이상 표준 SSD에서도 발생.
3. 재현
_FakeRawBlockDevice(메모리 bytearray)로 실제 디바이스 없이 동일 메커니즘 재현.
payload_cap을 4 KB로 축소:
payload_cap = 4096 bytes
저장 성공: 60/60 entries, _index 크기 = 60 ← 데이터 정상 기록
checkpoint payload 크기 = 10808 bytes (cap 4096)
→ overflow? True
_checkpoint_once(force=True) 반환값 = False ← silent skip
meta_seq = 0 ← 한 번도 기록 안 됨
--- 재시작 (같은 디바이스 재오픈) ---
복구된 _index 크기 = 0 ← 60개 전부 유실
로그 (WARNING 1줄, 예외 없음):
RawBlockCore metadata payload too large (10808 > 4096), skipping checkpoint (core.py:1165)
버그는 디바이스 물리 크기가 아닌 엔트리 개수로 트리거됨 → 8 KB 가짜 디바이스로 15 TB SSD 상황을 동일 재현 가능.
4. 영향 범위
MP / non-MP 공통 — 단 기본값 다름
S2는 공유 RawBlockCore에 있어 두 모드 모두 영향받지만, meta_total_bytes 기본값이 다르다.
| 모드 | 기본값 위치 | meta_total_bytes | payload_cap | overflow 임계 엔트리 |
|---|---|---|---|---|
| non-MP | rust_raw_block_backend.py:234 | 128 MB | 64 MB | ~321K |
| MP (L2 adapter) | raw_block_l2_adapter.py:78 | 256 MB | 128 MB | ~642K |
MP가 2배 높아 임계점이 늦지만, 7.68 TB 이상 디바이스에서는 두 모드 모두 overflow 도달.
| 디바이스 | Qwen3-480B TP=8 엔트리 수 | MP (256 MB) | non-MP (128 MB) |
|---|---|---|---|
| 3.84 TB | 520 K | 안전 | overflow |
| 7.68 TB | 1.04 M | overflow | overflow |
| 15 TB | 2.08 M | overflow | overflow |
| 모드 | eviction 주체 | 인덱스 누적 |
|---|---|---|
| MP | 외부 SM이 L2 eviction 관장 | SM 예산 범위 내 |
| non-MP | 자체 eviction 없음 (→ S1 별도 이슈) | max_slots까지 자연 누적 |
non-MP는 S1(eviction 없음)과 맞물려 인덱스가 max_slots까지 자연 누적 → 대용량 디바이스에서 S2 필연적으로 도달.
왜 아직 안 터졌나
raw_block은 2026-02-04 최초 도입(#2482), 아직 user-facing 문서에 미등재(docs에 Redis/Weka/S3 등만 있음). 현 프로덕션 배포들은 파일시스템 기반 백엔드를 사용 — inode/디렉토리가 인덱스를 대신 관리해 S2가 구조적으로 없음.
"아무도 안 밟음 ≠ 버그 없음" — "이 기능이 아직 그 규모로 배포 안 됨"
5. 수정 방향
단계별 커버리지
| 단계 | meta_total_bytes | Std SSD 커버 | HC SSD 커버 | layerwise |
|---|---|---|---|---|
| 0 현재 | 128 MB | ❌ | ❌ | ❌ |
| 1 (권장, 즉시) | 512 MB | ✅ 전부 | ❌ | ❌ |
| 2a | 2 GB | ✅ | ✅ ~30 TB | ❌ |
| 2b (바이너리 포맷) | 128 MB 유지 | ✅ | ✅ | ✅ |
단계 1 — 지금, 머지 가능성 높음
변경 대상: plugins/rust_raw_block_backend.py:234 — 기본값 한 줄
# before
meta_total_bytes = int(extra.get("rust_raw_block.meta_total_bytes", 128 * 1024 * 1024))
# after
meta_total_bytes = int(extra.get("rust_raw_block.meta_total_bytes", 512 * 1024 * 1024))
payload_cap: 64 MB → 256 MB
임계 엔트리: 321 K → 1.28 M
임계 디바이스 (Qwen3-480B TP=8): 2.4 TB → 9.5 TB
- Std SSD (1.92/3.84/7.68 TB) 전부 안전 (모든 모델, layerwise 제외)
- 7.68 TB 디바이스에서 메타 영역 비율 0.025% 잠식 — 사실상 무료
- 기존 128 MB 메타 영역으로 기록된 디바이스는
_meta_container_bytes가 달라져 재오픈 시 체크포인트를 못 읽음 →meta_versionbump + 인덱스 재구축 1회 필요 - 회귀 테스트:
tests/v1/storage_backend/test_rust_raw_block_backend.py헬퍼(_FakeRawBlockDevice)로 overflow + 복구 경로 커버
단계 2 — 단계 1 머지 후 결정
옵션 2a — 512 MB → 2 GB (총 16x)
- 임계 디바이스 ≈ 38 TB, HC SSD 30 TB까지 커버 (layerwise 제외)
- 30 TB에서 메타 영역 비율 0.007% — 사실상 무료
옵션 2b — 바이너리 직렬화 (entry ≈ 209 B → ~30 B, 약 7x)
- 천장 크기 그대로 헤드룸 7배 확보, layerwise 포함 전 케이스 해결
_snapshot_state/_apply_loaded_state전면 수정 필요, RFC 소요 3-6개월
단계 1 머지 반응 보고 2a(빠르게) vs 2b(장기 본질 해결) 결정.
6. 관련 이슈
- #1175, #2840 — 사용자 영속성 요구 이슈 (S2가 근본 원인 중 하나)
- #2482 — raw_block 최초 도입 (2026-02-04)
- #3119 — MP L2 어댑터 통합 머지 (2026-05-04)