io_uring padded write semantics 통일 구현 기록
한 줄 요약
perf/iouring-batched-padded-write는 PR #3636에서 남아 있던 regular io_uring
padded O_DIRECT write의 per-entry write_uring() fallback을 없애고,
batched_write()가 payload_lens를 받아 padded write도 batch submit으로 처리하게
하는 후속 작업이다.
이번 구현에서는 여기서 한 단계 더 나아가 regular io_uring의 serial write_uring()과
batched batched_write()가 같은 padding semantics를 갖도록 정리했다.
최종 정책:
payload_len = logical payload bytes
total_len = physical I/O bytes
regular io_uring write는 payload_len..total_len padding 영역이 zero로 기록되도록 보장한다.
tail이 이미 zero이고 alignment/capacity 조건이 맞으면 direct/fixed-buffer path를 유지한다.
tail이 non-zero이거나 source가 짧거나 O_DIRECT alignment가 맞지 않으면 bounce + zero-fill한다.
배경: PR #3636에서 무엇이 batch였나
PR #3636에서 NVMe passthrough만 batch였던 것은 아니다. 기준 동작은 다음과 같다.
| 경로 | PR #3636 동작 | 설명 |
|---|---|---|
| POSIX | batch 아님 | pwrite_from_buffer()를 entry별로 호출 |
| regular io_uring, non-padded | batch | payload_len == total_len이면 batched_write() 사용 |
| regular io_uring, padded O_DIRECT | batch 아님 | payload_len < total_len이면 per-entry write_uring() fallback |
NVMe passthrough (io_uring_cmd) | batch | _write_uring_cmd_buffers()가 chunk를 만들고 batched_write() 호출 |
PR #3636의 빈틈은 regular io_uring 전체가 batch가 아니었다는 점이 아니라, regular io_uring에서 padded O_DIRECT write만 batch path를 못 탔다는 점이다.
구현 내용
Rust batched_write() API 확장
RawBlockDevice.batched_write()가 optional payload_lens를 받도록 확장됐다.
batched_write(offsets, buffers, total_lens, payload_lens=None)
payload_lens=None이면 기존 호환성을 위해payload_lens = total_lens로 처리한다.payload_len > total_len, vector length mismatch,cap < payload_len은 즉시 에러 처리한다.- O_DIRECT에서는 기존처럼 offset과
total_lenalignment를 검증한다. - 기존 3-argument caller는 계속 동작한다.
regular io_uring write preparation helper
Rust에 serial/batched regular io_uring write가 공유하는 helper를 추가했다.
입력:
ptr, cap, payload_len, total_len, use_odirect, alignment, fixed_buffer_idx
결정:
cap < payload_len -> 에러
payload_len > total_len -> 에러
use_odirect && ptr unaligned -> bounce
cap < total_len -> bounce
payload_len < total_len && tail non-zero -> bounce
payload_len < total_len && tail already zero -> direct/fixed-buffer 허용
payload_len == total_len -> direct/fixed-buffer 허용
bounce:
total_len 크기의 aligned buffer 생성
payload_len bytes만 copy
[payload_len, total_len) zero-fill
이 helper 덕분에 write_uring()과 batched_write()가 같은 padding semantics를 갖는다.
path별 최종 의미
| 경로 | 최종 처리 |
|---|---|
POSIX pwrite_from_buffer() | 기존 hybrid 유지. aligned prefix direct + tail bounce/zero-fill |
regular io_uring write_uring() | 공통 helper 사용. tail zero면 direct/fixed-buffer, 아니면 bounce/zero-fill |
regular io_uring batched_write() | 공통 helper 사용. padded write도 batch 유지 |
io_uring_cmd | 기존 Python chunking path 유지. NVMe max transfer size 제약 때문에 이번 변경 대상 아님 |
Regression 방어 테스트
batched_write()stale-tail regressionbytearray(total_len)tail을 non-zero로 채우고payload_lens=[payload_len]로 write한다.- readback tail이 zero인지 확인한다.
- serial
write_uring()stale-tail regression- 같은 조건을 serial API에 적용한다.
- serial과 batched의 padding semantics가 같아졌음을 고정한다.
batched_write()validation regressionpayload_lens길이 mismatchpayload_len > total_lencap < payload_len
Side effect와 대응
zero-copy/fixed-buffer 손실 최소화
단순하게 payload_len < total_len이면 항상 bounce하도록 만들 수도 있었지만, 그 경우
aligned buffer와 fixed-buffer의 이점을 불필요하게 잃는다.
최종 구현은 tail을 검사한다.
tail already zero -> direct/fixed-buffer 가능
tail non-zero -> bounce + zero-fill
RawBlockCore는 direct O_DIRECT view를 만들 때 tail을 zeroing할 수 있으므로, 이 경로는 fixed-buffer 이점을 유지할 수 있다. generic caller가 non-zero tail을 넘기면 Rust가 bounce해서 디바이스에 zero padding만 기록한다.
serial/batched io_uring semantics 통일
기존 serial write_uring()은 payload_len/total_len을 받을 수 있었지만,
cap >= total_len이면 caller tail을 그대로 쓰는 구조였다. 이번 변경으로 serial도
batched와 같은 helper를 사용한다.
이제 regular io_uring write 계열에서 padding 책임이 path별로 갈라지지 않는다.
io_uring_cmd는 유지
io_uring_cmd는 NVMe max transfer size와 command alignment 제약이 있어 Python
chunking path를 유지한다. 이번 변경은 regular io_uring write semantics 통일이
목표이며, passthrough path를 단순화하지 않는다.
extension rebuild 필요
Python RawBlockCore는 regular io_uring path에서 4번째 인자인 payload_lens를
넘긴다. 따라서 Python 코드와 Rust extension은 함께 rebuild되어야 한다. 구버전
extension과 새 Python 코드가 섞이면 runtime TypeError가 날 수 있다.
검증 내용
작업 중 TDD red 확인:
/home/ny/LMCache/.venv/bin/python -m pytest -q \
tests/v1/storage_backend/test_raw_block_device.py::test_raw_block_device_iouring_batched_write_zeroes_existing_tail \
tests/v1/storage_backend/test_raw_block_device.py::test_raw_block_device_iouring_write_uring_zeroes_existing_tail
초기 결과: 2개 모두 실패. non-zero tail이 그대로 기록되는 것을 확인했다.
최종 검증:
cargo check
cargo fmt --check
cargo clippy --all-targets -- -D warnings
결과: 모두 통과.
/home/ny/LMCache/.venv/bin/python -m pytest -q \
tests/v1/storage_backend/test_raw_block_core.py \
tests/v1/storage_backend/test_raw_block_device.py
결과:
25 passed, 2 skipped, 54 warnings in 6.59s
/home/ny/LMCache/.venv/bin/python -m pytest -q \
tests/v1/storage_backend/test_rust_raw_block_backend.py
결과:
29 passed, 55 warnings in 5.42s
skip된 2개는 LMCACHE_RUN_ODIRECT_SMOKE로 보호된 hardware-dependent O_DIRECT smoke
test다. 이번 세션에서는 opt-in O_DIRECT smoke는 실행하지 않았다.
직접 실행할 hardware-dependent 검증은 io_uring_padded_write_user_validation_guide.md에 정리했다.
결론
후속 PR의 방향은 유지하되, regular io_uring write semantics를 더 깔끔하게 정리했다.
batched_write()는payload_lens를 받아 padded write도 batch path로 처리한다.- serial
write_uring()과 batchedbatched_write()는 같은 helper를 공유한다. - padding tail은 zero로 기록된다.
- tail이 이미 zero면 direct/fixed-buffer fast path를 유지한다.
- POSIX와
io_uring_cmd의 기존 구조는 건드리지 않는다.