iouring-batch put_many — 리뷰 반영 패치 노트
한 줄 요약: PR #3636 리뷰 중 실제 correctness/CI 커버리지 문제인 importorskip, same-batch duplicate, oversize isolation, partial completion rollback, large-batch fairness cap 을 반영한다. O_DIRECT batch fallback 리뷰는 타당한 한계지만 Rust API 변경 또는 copy+pad 전략이 필요해 이 패치에서는 제외한다.
1. 스코프 판단
이번 패치는 RawBlockCore._put_many_batch_io 의 Python-side 상태 전이를 정리하는
후속 수정이다. PR 의 원래 목적은 io_uring 환경에서 put_many 의 N개 키를
header+payload 2N write entry 로 모아 단일 batched_write 제출을 만드는 것이다.
리뷰별 판단:
| 리뷰 | 판단 | 이유 |
|---|---|---|
1. 모듈 레벨 importorskip | 수정 필요 | fake device 기반 fast-path 테스트가 Rust 확장 없는 CI 에서 통째로 스킵된다. PR 핵심 계약 테스트가 CI 밖으로 빠지는 문제. |
| 2. 배치 내 중복키 | 수정 필요 | 순차 put_many([K,K]) 는 [True, True] 이지만 batch 경로는 두 번째 K 를 _inflight 충돌로 [True, False] 처리했다. adapter 의 all(results) 게이트를 잘못 실패시킨다. |
| 3. oversize 전체 롤백 | 수정 필요 | device write 전의 per-key eligibility 실패가 정상 이웃 키까지 실패시키면 순차 parity 가 깨진다. |
| 4. O_DIRECT batch fallback | 이번 패치 제외 | 문제 지적 자체는 맞다. 다만 현재 Rust buffer API 는 byte_array.get_size() 기준의 논리 크기만 노출하므로 "이미 padded buffer 니까 payload_len=total_len 으로 넘기자" 식 수정은 성립하지 않는다. 진짜 수정은 Rust batched_write API 를 (payload_len,total_len) 쌍으로 넓히거나 Python 에서 copy+pad buffer 를 새로 만드는 방향이며, PR #3636 의 Python-side 상태 정리 범위를 넘는다. |
| 5. partial completion failure path | 수정 필요 | raw-block 에서 device write 실패를 all-or-nothing 으로 취급하는 정책은 괜찮다. 대신 fake device 가 2N write list 중 일부 entry 를 쓴 뒤 실패하는 케이스에서 Python state(_index, _inflight, free slots)가 완전히 롤백되는지 테스트해야 한다. |
| 6. large batch lock fairness | 수정 필요 | batch 경로가 I/O 는 lock 밖에서 하지만 reserve/commit 을 O(N) lock 구간으로 묶으므로 큰 put_many 가 다른 caller 를 오래 기다리게 할 수 있다. Rust 기본 ring depth 256 SQE 와 key당 2 SQE(header+payload)를 기준으로 64-key chunk cap 을 둔다. |
2. 코드 변경 방향
2.1 importorskip 축소
기존 tests/v1/storage_backend/test_raw_block_core.py 는 파일 상단에서
pytest.importorskip("lmcache_rust_raw_block_io") 를 호출했다. 이 때문에 Rust 확장이
없는 환경에서는 fake _FakeRawDevice 로 검증 가능한 io_uring fast-path 테스트까지
전부 skip 됐다.
수정:
importlib.util.find_spec()기반requires_rust_raw_block_iomarker 추가- 실제 Rust extension 이 필요한 real raw device 테스트 4개에만 marker 부착
- fake raw device 기반
_put_many_batch_io테스트는 Rust 확장 없이 실행
검증:
- 일반 환경:
test_raw_block_core.py전체14 passed - Rust 확장 차단 simulation:
11 passed, 4 skipped(chunk cap 테스트 추가 후)
2.2 same-batch duplicate parity
기존 Phase A 는 같은 배치의 첫 번째 K 를 _inflight 에 등록한 뒤, 두 번째 K 를 외부
inflight 충돌처럼 보고 False 로 남겼다. 순차 경로와 adapter 계약 관점에서는 같은
배치 중복키가 첫 occurrence 의 최종 결과를 공유해야 한다.
수정:
- Phase A 에
planned_keys: set[str]추가 - 같은 배치에서 이미 계획된 key 는 새 슬롯을 할당하지 않고
batch_duplicates에 기록 - Phase D 이후 duplicate 결과를
encoded_key in self._index로 확정
효과:
- 첫 occurrence commit 성공: duplicate 도
True - 첫 occurrence write 실패 또는 buffer-prep 실패: duplicate 도
False - 실제 device write 는 첫 occurrence 의 header+payload 2개 entry 만 제출
stored_keys는 중복 없이 첫 occurrence 하나만 포함
2.3 oversize/pre-submit failure 격리
all-or-nothing 은 "이미 combined device write 를 제출한 뒤 실패하면 submitted new keys
전부 롤백"이라는 의미로 유지한다. 반대로 write 제출 전의 eligibility failure 는 순차
put_many 처럼 per-key 실패로 격리해야 한다.
수정:
_payload_fits_slot(payload_len)helper 추가- Phase A 에서 슬롯 할당 전에 payload 수용 가능 여부 검사
- oversize key 는
_inflight등록과 슬롯 할당 없이False - 정상 이웃 key 들은 하나의 batch write 로 계속 진행
O_DIRECT 일 때는 logical payload 가 round_up(payload_len, block_align) 이후에도
slot_bytes - header_bytes 안에 들어가는지 검사한다. 이 검사는 write-prep 예외를
줄이고, 실패한 key 가 슬롯/inflight 를 건드리지 않게 만드는 목적이다.
2.4 buffer-prep failure 격리
이전 finalize 보완에서는 Phase B buffer prep 을 broad try 로 감싸 batch 전체 rollback 대상에 넣었다. 리뷰 후 정책을 더 정확히 나누면 Phase B 는 아직 device write 전이므로 per-key 실패로 격리하는 편이 순차 parity 에 맞다.
수정:
write_plan과 별도로prepared_plan도입_encode_header,_prepare_write_payload실패 시 해당 key 의_inflight만 제거하고 슬롯만 free list 로 반환- buffer prep 에 성공한 key 들만
prepared_plan에 넣고_write_buffers에 제출 _inflight_io_count는 실제 제출되는prepared_plan길이만큼만 증감
2.5 partial completion rollback 테스트
리뷰어가 요청한 핵심 케이스는 "fake device 가 batch 시작 전에 실패"가 아니라
"2N write list 중 일부 entry 를 이미 받아 쓴 뒤 실패"하는 상황이다.
수정:
_FakeRawDevice.fail_after_write_entries추가batched_write()가 앞 N개 entry 를store에 기록한 뒤 예외를 던질 수 있게 함- 실패 후 raw device 내부에는 부분 write 흔적이 남지만, Python-side 상태는 all-or-nothing 으로 롤백되는지 검증
검증 항목:
put_manyresult 는 submitted key 전부Falsestored_keys == []- fake raw device
store에는 일부 entry 가 남아 partial completion 을 실제 재현 core.report_status()기준inflight_key_count == 0indexed_key_count == 0- available slots 는 호출 전과 동일
exists_many()는 모두False
2.6 large-batch fairness chunk cap
리뷰어가 지적한 lock fairness 문제는 "device I/O 중 lock 보유"가 아니라, batch 경로가
Phase A reserve 와 Phase D commit 을 각각 하나의 O(N) lock 구간으로 묶는 데서 온다.
작은 batch 에서는 lock 획득 횟수를 줄이는 장점이 크지만, N 이 커지면 한 caller 가
RawBlockCore 상태 lock 을 길게 점유할 수 있다.
수정:
_MAX_PUT_MANY_IO_URING_BATCH_KEYS = 64추가put_many의 io_uring fast path 는_put_many_batch_io()로 진입- 64-key 이하 입력은
_put_many_batch_io_chunk()로 단일 submit - 64-key 초과 입력은 unique first occurrence 들을 64-key chunk 로 나누어 여러 번 제출
- chunk 경계를 넘는 duplicate key 는 첫 occurrence 의 최종 결과를 공유하고 다시 쓰지 않음
64 기준 근거:
- Rust raw-block io_uring 기본 ring size 는
RING_SIZE = 256 - Python config 기본도
DEFAULT_IOURING_QUEUE_DEPTH = 256 - key 1개는 header+payload 로 2 write entry 를 만든다
- 64 keys = 128 SQE 이므로 기본 ring 의 절반만 한 Python batch 가 사용한다
- NVMe queue parallelism 은 살리면서 lock 점유와 ring 독점을 함께 제한한다
메서드 역할:
_put_many_batch_io: publicput_manyfast-path entry. chunking, result aggregation, cross-chunk duplicate handling 을 담당한다._put_many_batch_io_chunk: bounded chunk 하나를 header+payload2Nentries 로 flatten 하여 single_write_buffers/batched_write로 제출한다.
3. 추가 테스트 목록
tests/v1/storage_backend/test_raw_block_core.py 에 추가/수정된 주요 테스트:
| 테스트 | 목적 |
|---|---|
test_raw_block_core_io_uring_put_many_duplicate_keys_in_batch | [K,K] 결과가 [True, True] 이고 첫 payload 만 저장되는지 검증 |
test_raw_block_core_io_uring_put_many_oversize_key_isolated | oversize 1건이 정상 이웃 키의 batch commit 을 막지 않는지 검증 |
test_raw_block_core_io_uring_put_many_partial_completion_rolls_back | fake device partial completion 이후 Python-side state 가 전부 롤백되는지 검증 |
test_raw_block_core_io_uring_put_many_chunks_large_batches | 큰 put_many 가 bounded chunk 로 나뉘고 chunk 경계 duplicate 이 재작성되지 않는지 검증 |
| Rust 확장 차단 simulation | fake io_uring fast-path 테스트가 Rust extension 없이 CI 에서 실행 가능한지 검증 |
4. 검증 결과
작업 트리: /home/ny/workspace/LMCache
/home/ny/LMCache/.venv/bin/python -m pytest -q \
tests/v1/storage_backend/test_raw_block_core.py
# 15 passed, 54 warnings
PYTHONPATH=/tmp/lmcache_no_rust_pytest \
/home/ny/LMCache/.venv/bin/python -m pytest -q \
-p block_rust_plugin tests/v1/storage_backend/test_raw_block_core.py
# 11 passed, 4 skipped, 54 warnings
/home/ny/LMCache/.venv/bin/pre-commit run --files \
lmcache/v1/storage_backend/raw_block/core.py \
tests/v1/storage_backend/test_raw_block_core.py
# Passed
git -C /home/ny/workspace/LMCache diff --check
# clean
추가 참고:
- 2026-06-12 chunk cap 반영 후 targeted
pytest는15 passed. - 같은 시점 targeted
pre-commit run --files ...는 approval token refresh 문제로 실행 자체가 거절됨. 우회 실행하지 않음.
5. 리뷰 응답 초안
짧게 답하면:
importorskip: fixed by moving Rust-extension gating to only the tests that need the real extension. Fake io_uring fast-path tests now run withoutlmcache_rust_raw_block_io.- duplicate keys: fixed. Same-batch duplicates no longer allocate/write twice and inherit the first occurrence's final result.
- oversize / pre-submit failures: fixed as per-key failures. All-or-nothing is kept only after the combined device write is submitted.
- partial completion: added a fake-device test that writes part of the
2Nentry list and then raises. The raw device can contain partial bytes, but Python-side state rolls back completely. - fairness: large
put_manycalls are split into bounded 64-key io_uring batches. With the default Rust ring depth of 256 SQEs and two write entries per key, each submit is capped at 128 SQEs. - O_DIRECT padded-buffer batching: acknowledged as a real limitation, but left out of this patch because fixing it correctly requires either widening the Rust API to carry
(payload_len,total_len)pairs or introducing a copy+pad Python buffer strategy.
6. 남은 일
- O_DIRECT batch fallback 는 별도 패치에서 다룬다.
- 이번 review-fix patch 를 commit/amend 한 뒤 PR #3636 에 push.
- push 후 PR 코멘트에는 위 §5 요지와 검증 결과를 함께 남긴다.