본문으로 건너뛰기

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 MBkey→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=4096payload_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이 없으면 복구 방법이 없다. 두 가지 이유:

  1. _validate_loaded_entries는 로드된 _index검증만 한다. 슬롯을 스캔해 인덱스를 재구축하는 경로가 없다.
  2. 슬롯 헤더에는 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_bytesnum_layers × chunk_tokens × hidden_dim × dtype_bytes에서 파생된다. TP가 높을수록 GPU당 KV head 수가 줄어 slot이 작아지고, 같은 디바이스 용량에 엔트리가 더 많이 쌓여 overflow에 취약해진다.

모델별 overflow 임계 디바이스 크기

모델 / 설정slot_bytesoverflow 임계 (디바이스 가득 찰 때)
Llama-3-8B TP=1 (non-layerwise)32 MB9.8 TB
Llama-3-70B TP=8 (non-layerwise)10 MB3.1 TB
Qwen3-480B TP=8 (non-layerwise)7.75 MB2.4 TB
layerwise=True~1 MB314 GB
sparse (cached_positions 있음)10 MB0.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_capoverflow 임계 엔트리
non-MP128 MB64 MB~321 K
MP (L2 adapter)256 MB128 MB~642 K
디바이스max 엔트리JSON 크기MP overflownon-MP overflow
1.92 TB260 K52 MB안전간당간당
3.84 TB520 K104 MB안전YES
7.68 TB1.04 M207 MBYESYES
15 TB2.08 M414 MBYESYES
30 TB4.16 M828 MBYESYES

재현

실제 디바이스 없이 _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_bytesStd SSD 커버HC SSD 커버layerwise
0 — 현재128 MB (non-MP)
1 — 권장 (즉시)512 MB✅ 전부
2a — HC bump2 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_cap64 MB256 MB
임계 엔트리321 K1.28 M
임계 디바이스 (Qwen3-480B TP=8)2.4 TB9.5 TB

부가 작업

  • meta_version bump 필요: 기존 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가 근본 원인 중 하나
#2482raw_block 최초 도입 (2026-02-04)
#3119MP L2 어댑터 통합 머지 (2026-05-04)