S2: raw_block checkpoint payload overflow
상태 | 분석 완료 — PR 준비 중
대상 코드 | lmcache/v1/storage_backend/raw_block/core.py
영향 범위 | MP / non-MP 공통 (RawBlockCore 공유)
심각도 | 잠복형 스케일 버그 — 대용량 SSD 프로덕션 배포 전 수정 필요
작성일 | 2026-05-29
요약
raw_block 백엔드는 재시작 복구를 위해 키→슬롯 인덱스를 JSON으로 직렬화해 디바이스에 백업한다. 이 JSON에 64 MB 상한이 있고, 엔트리가 ~32만 개(≈ 82M distinct 토큰)를 초과하면 백업이 경고 한 줄만 남기고 조용히 실패한다. 다음 재시작 시 캐시 전량이 유실된다.
3.84 TB 이상 디바이스와 Llama-70B / Qwen3-480B 같은 대형 모델 조합에서 디바이스가 가득 차면 반드시 도달하는 조건이다.
버그 메커니즘
디바이스 레이아웃
raw_block은 디바이스를 두 영역으로 나눈다.
| 영역 | 범위 | 역할 |
|---|---|---|
| 메타데이터 | offset 0 ~ 128 MB | key→offset 인덱스 (checkpoint JSON 저장) |
| 데이터 | 128 MB ~ 디바이스 끝 | 고정 크기 slot_bytes 슬롯 배열 (실제 KV 캐시) |
런타임 인덱스는 RAM에만 존재
self._index: dict[str, _Entry] = {} # 키 → _Entry(offset, size, meta)
휘발성이다. 재시작하면 증발하고, 데이터 영역 KV가 물리적으로 멀쩡해도 슬롯↔키 매핑을 잃는다.
Checkpoint 직렬화 흐름
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 계산
메타 영역은 2벌 미러링(_meta_copy_count=2). 한 벌당 가용 용량:
_meta_container_bytes = (meta_total_bytes // 2 // block_align) × block_align
payload_cap = _meta_container_bytes - block_align
기본값 meta_total_bytes=128 MB, block_align=4096 → payload_cap ≈ 64 MB
천장 초과 시 처리 — silent skip
if len(payload) > payload_cap:
logger.warning(
"RawBlockCore metadata payload too large (%d > %d), skipping checkpoint",
len(payload), payload_cap,
)
return False # 예외 없음, WARNING 한 줄, 메타 영역 미기록
복구 경로는 checkpoint가 유일
checkpoint JSON이 없으면 복구 방법이 없다. 두 가지 이유:
_validate_loaded_entries는 로드된_index를 검증만 한다. 슬롯을 스캔해 인덱스를 재구축하는 경로가 없다.- 슬롯 헤더에는
slot_identity(8 B 해시) +payload_len만 있고 원본 키 문자열이 없다 → 헤더만으로 키 복원 불가.
전체 실패 시나리오
인덱스 누적 → JSON > 64 MB → checkpoint 매번 silent skip
→ 메타 영역은 이전 상태 (또는 meta_seq=0 빈 상태)
→ 운영자 인지 불가 (WARNING 한 줄, 알림/예외 없음)
→ 재시작/크래시 → RAM의 _index 증발
→ 메타 영역에서 복구 시도 → 비었거나 구버전 → KV 전부 미아
→ 캐시 전량 유실 (물리 데이터는 디스크에 있으나 키를 모름)
Overflow 임계점
임계 엔트리 수: 64 MB ÷ 209 B ≈ 321,000
임계 토큰 수: ≈ 82 M distinct KV (chunk_size=256 기준)
slot_bytes와 TP의 관계: non-layerwise 기본에서
slot_bytes는num_layers × chunk_tokens × hidden_dim × dtype_bytes에서 파생된다. TP가 높을수록 GPU당 KV head 수가 줄어 slot이 작아지고, 같은 디바이스 용량에 엔트리가 더 많이 쌓여 overflow에 취약해진다.
모델별 overflow 임계 디바이스 크기
| 모델 / 설정 | slot_bytes | overflow 임계 (디바이스 가득 찰 때) |
|---|---|---|
| Llama-3-8B TP=1 (non-layerwise) | 32 MB | 9.8 TB |
| Llama-3-70B TP=8 (non-layerwise) | 10 MB | 3.1 TB |
| Qwen3-480B TP=8 (non-layerwise) | 7.75 MB | 2.4 TB |
| layerwise=True | ~1 MB | 314 GB |
| sparse (cached_positions 있음) | 10 MB | 0.5 TB |
HC SSD 전용 문제가 아니다. 큰 모델(높은 TP로 slot 작아짐) + 3.84 TB 이상 표준 SSD 조합에서도 발생한다.
디바이스 크기 × 모드별 overflow 여부 (Qwen3-480B TP=8)
non-MP와 MP는 meta_total_bytes 기본값이 다르다.
| 모드 | meta_total_bytes 기본값 | payload_cap | overflow 임계 엔트리 |
|---|---|---|---|
| non-MP | 128 MB | 64 MB | ~321 K |
| MP (L2 adapter) | 256 MB | 128 MB | ~642 K |
| 디바이스 | max 엔트리 | JSON 크기 | MP overflow | non-MP 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 | YES |
| 15 TB | 2.08 M | 414 MB | YES | YES |
| 30 TB | 4.16 M | 828 MB | YES | YES |
재현
실제 디바이스 없이 _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
버그는 디바이스 물리 크기가 아닌 엔트리 개수로 트리거된다. 8 KB 가짜 디바이스로 15 TB SSD 상황을 동일하게 재현할 수 있다.
영향 범위
MP / non-MP 공통
S2는 공유 RawBlockCore에 있어 두 모드 모두 영향받는다.
| 모드 | 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 등만 있음), MP L2 어댑터 통합도 2026-05-04 머지(#3119)로 매우 신생이다. 현 프로덕션 배포들은 파일시스템 기반 백엔드를 사용하고 inode/디렉토리가 인덱스를 대신 관리해 S2가 구조적으로 발생하지 않는다.
"아무도 안 밟음 ≠ 버그 없음" — "이 기능이 아직 그 규모로 배포 안 됨"
수정 방향
단계별 커버리지 요약
| 단계 | meta_total_bytes | Std SSD 커버 | HC SSD 커버 | layerwise |
|---|---|---|---|---|
| 0 — 현재 | 128 MB (non-MP) | ❌ | ❌ | ❌ |
| 1 — 권장 (즉시) | 512 MB | ✅ 전부 | ❌ | ❌ |
| 2a — HC bump | 2 GB | ✅ | ✅ (~30 TB) | ❌ |
| 2b — 바이너리 포맷 | 128 MB 유지 | ✅ | ✅ | ✅ |
단계 1 — 즉시, 머지 가능성 높음
변경 대상: plugins/rust_raw_block_backend.py 기본값 한 줄
# 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 |
부가 작업
meta_versionbump 필요: 기존 128 MB 메타 영역으로 기록된 디바이스는_meta_container_bytes오프셋이 달라져 재오픈 시 체크포인트를 못 읽음 → 인덱스 재구축 1회 발생- 회귀 테스트:
tests/v1/storage_backend/test_rust_raw_block_backend.py의_FakeRawBlockDevice헬퍼로 overflow + 복구 경로 커버
머지 명분: 이미 영향받는 메인스트림 사용자(3.84/7.68 TB Std SSD + 70B/480B 모델), 보수적인 4× bump, 디바이스 용량 대비 메타 비율 0.025% 미만.
단계 2 — 단계 1 머지 후 결정
옵션 2a — 한 번 더 bump (512 MB → 2 GB, 총 16×)
- 임계 디바이스 ≈ 38 TB, HC SSD 30 TB까지 커버 (layerwise 제외)
- 메타 영역 비율 0.007% 이하
옵션 2b — 바이너리 직렬화 (entry 209 B → ~30 B, 약 7×)
- 천장 크기 그대로 헤드룸 7배 확보, layerwise 포함 전 케이스 해결
_snapshot_state/_apply_loaded_state전면 수정 필요, RFC 소요 3–6개월
단계 1 머지 반응 보고 2a(빠르게) vs 2b(장기 본질 해결) 결정.
관련 이슈 / PR
| 번호 | 내용 |
|---|---|
| #1175, #2840 | 사용자 영속성 요구 이슈 — S2가 근본 원인 중 하나 |
| #2482 | raw_block 최초 도입 (2026-02-04) |
| #3119 | MP L2 어댑터 통합 머지 (2026-05-04) |