본문으로 건너뛰기

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 동작설명
POSIXbatch 아님pwrite_from_buffer()를 entry별로 호출
regular io_uring, non-paddedbatchpayload_len == total_len이면 batched_write() 사용
regular io_uring, padded O_DIRECTbatch 아님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_len alignment를 검증한다.
  • 기존 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 regression
    • bytearray(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 regression
    • payload_lens 길이 mismatch
    • payload_len > total_len
    • cap < 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()과 batched batched_write()는 같은 helper를 공유한다.
  • padding tail은 zero로 기록된다.
  • tail이 이미 zero면 direct/fixed-buffer fast path를 유지한다.
  • POSIX와 io_uring_cmd의 기존 구조는 건드리지 않는다.