본문으로 건너뛰기

④ O_DIRECT padded write — codex 방식 (fallback 명시화 + Rust 후속)

Context

PR #3636 리뷰의 ④: io_uring batch 경로에서 패딩이 필요한 O_DIRECT write가 진짜 batched로 처리되지 않는다. 근본 원인은 Rust batched_write(offsets, buffers, total_lens)payload_len을 받지 않고 각 버퍼에서 total_len을 읽으므로, payload가 미정렬이라 패딩되는 경우(payload_len < total_len)를 batched로 안전히 다룰 수 없다는 점.

현재 동작은 이미 안전하다: _write_buffers()can_batch = all(payload_len == total_len)일 때만 batched_write()를 호출하고, 패딩이 필요한 O_DIRECT write는 payload_len < total_len이라 per-entry write_uring()으로 폴백된다. write_uring()은 Rust에 payload_len/total_len을 모두 넘겨 패딩을 bounce 버퍼로 안전 처리한다.

전략(codex 방식, 사용자 확정): can_batch 동작은 바꾸지 않는다. 이번 PR(#3636)에서는 이 폴백이 의도된 안전 동작임을 코드 주석과 테스트로 명시한다. 진짜 batched padded 지원(Rust API 변경)은 별도 후속 PR로 분리한다.

기준 노트: private/work/raw_block/put-many-batch/iouring-batch-follow-up-codex.md.

Part A — 이번 PR(#3636)에 추가

대상 파일:

  • lmcache/v1/storage_backend/raw_block/core.py_write_buffers() 주석만 (동작 변경 없음)
  • tests/v1/storage_backend/test_raw_block_core.py — O_DIRECT padded fallback 테스트

A-1. 주석 추가 (_write_buffers can_batch 근처)

  • batched_write()는 현재 total_lens만 받는다는 점.
  • O_DIRECT padded write는 payload_len < total_len이 되어 batched로 다룰 수 없으므로, payload_len/total_len을 분리 전달하는 per-entry write_uring() 폴백을 의도적으로 쓴다는 점.
  • 동작 코드는 손대지 않음 (can_batch = all(payload_len == total_len) 유지).

A-2. 유닛 테스트 (TDD)

기존 헬퍼 재사용: _FakeRawDevice(이미 batched_write_calls/write_uring_count 보유), _make_core_with_fake. 단 _make_core_with_fakeuse_odirect 오버라이드 인자 추가 (현재 io_engine/load_checkpoint/capacity만 override → use_odirect=False 기본 파라미터 추가해 overrides에 반영).

테스트 test_raw_block_core_io_uring_put_many_odirect_padded_falls_back:

  • _make_core_with_fake(path, fake, io_engine="io_uring", use_odirect=True).
  • N>1 키, block_align(4096) 비배수 payload(예: 1000·1500 bytes) → _prepare_write_payloadtotal_len=round_up(payload_len) > payload_len 생성(enable_zero_copy=False라 direct view None).
  • put_many() 호출.
  • 기대:
    • result.results == [True]*N (성공)
    • fake.batched_write_calls == [] (batched 미사용)
    • fake.write_uring_count == 2*N (키마다 header+payload per-entry 폴백)
    • round-trip: load_many_into로 원본 payload 복원 확인
  • TDD 절차상 이 테스트는 현재 코드에서 이미 통과(폴백이 이미 동작) → 회귀/명시화 테스트 성격. 먼저 작성해 현 동작을 고정한 뒤 주석을 단다.

A-3. (선택) 리뷰어 응답

codex 노트 하단 초안 기반으로 ④ 스레드에 답글: "안전 폴백은 이미 존재 / 이 PR에 주석+테스트로 명시 / Rust batched_write API 확장은 후속 PR로" — 게시는 사용자 승인 후.

A-4. 반영/검증

  • 테스트 클론(/home/ny/LMCache, Rust 확장 OK)에서 detached checkout 후 pytest -xvs tests/v1/storage_backend/test_raw_block_core.py -k odirect_padded 통과 → 파일 전체 통과.
  • workspace(/home/ny/workspace/LMCache)에서 pre-commit run --files <두 파일>.
  • amend + force-push (단일 커밋 유지): 커밋 메시지에 "make O_DIRECT padded write_uring fallback explicit (comment + test)" 취지 추가, Signed-off-by 유지. push는 사용자 승인 후.

Part B — 후속 Rust PR (별도, 이번 PR 아님) — ✅ 로컬 구현 완료 (2026-06-24)

상태: 워크트리 ../LMCache-partB, 브랜치 perf/iouring-batched-padded-write(@aead23c=#3636 head)에 커밋 4개로 구현·검증 완료. #3636 머지 후 git rebase --onto dev aead23c044fa perf/iouring-batched-padded-write → PR open(본문 self-contained, "depends on #3636"). 머지 순서: #3636 먼저 (Rust OOB 수정이 Python 게이트 제거보다 먼저 들어가야 안전 — 커밋 순서로 보장).

커밋 순서 (bisect-safe):

  1. [RawBlock][Rust] batched_write: accept payload_lens; fix OOB bounce copy — 시그니처 (offsets, buffers, total_lens, payload_lens=None)(optional, None=total_lens). bounce에서 payload_len만 copy + [payload_len,total_len) 명시적 zero-fill(posix_memalign zero-init에 의존 안 함, write_uring보다 엄격). validation: payload_len<=total_len, cap>=payload_len, O_DIRECT offset/total_len 정렬. 비-O_DIRECT도 cap<total_len이면 bounce(latent OOB 하드닝).
  2. [RawBlock][test] real-device padded batched_write round-trip — io_uring + opt-in O_DIRECT 패딩 round-trip, 꼬리 zero 검증(O_DIRECT는 비-O_DIRECT device로 물리 read-back).
  3. [RawBlock] route padded O_DIRECT writes through batched_write_write_buffers의 can_batch 게이트 완전 제거(제거=완화, 동작 동일). per-entry write_uring 폴백 삭제(유일 호출처라 dead). aggregated put_many· metadata write의 padded 케이스가 전부 batched로.
  4. [RawBlock][test] padded O_DIRECT put_many uses batched_write, no fallback — fake에 payload_lens 기록 + read_uring 추가, padded put_many가 단일 batched_write(payload_lens 전달)·write_uring 0회·round-trip 검증.

검증: core 17 passed + device 6 passed(O_DIRECT 포함), pre-commit 전부 Passed, cargo clippy -D warnings·fmt clean. (test_uring_cmd_get_nvme_info는 실 NVMe char device 권한 문제로 fail — 본 변경과 무관한 환경 이슈.)

원안 계획 대비 조정: ① 시그니처는 required 4-arg가 아니라 optional(기존 3-arg 호출처=device 테스트· _write_uring_cmd_buffers 무수정). ② 게이트는 완화가 아니라 완전 제거(폴백이 dead라 정리). ③ codex 노트가 가정한 _FakeRawDevice 없음 → 실제로는 존재(이 브랜치), read_uring 메서드만 추가.


진짜 batched padded 지원. 대상: rust/raw_block/src/lib.rs + core.py call site + 테스트.

  1. batched_write 시그니처 확장: (offsets, buffers, total_lens)(offsets, buffers, payload_lens, total_lens).
  2. Rust validation: vector 길이 동일 / payload_len <= total_len / buffer cap >= payload_len / O_DIRECT에서 offset·total_len 정렬.
  3. Rust 내부 패딩 처리(write_uring 미러링): aligned & cap>=total_len이면 직접 submit, 아니면 aligned bounce(AlignedBuf)에 payload_len 복사 + total_len-payload_len zero-fill.
  4. Python call site: _write_buffers에서 payload_lens도 batched_write에 전달하고 can_batch 게이트를 완화 → padded O_DIRECT도 폴백 없이 batched.
  5. 테스트: Rust 단위 + RawBlockCore에서 padded O_DIRECT put_many가 batched_write 사용 검증 + round-trip + 패딩 영역 안전성.

순서 제약(안전): 3(Rust bounce/패딩)이 먼저 들어간 뒤에야 4(Python 게이트 완화)를 한다. 역순이면 batched_write가 short 버퍼에서 total_len을 읽어 OOB read. codex 노트는 단계 순서로만 암묵적 안전 — 이 의존성을 명시 커밋/PR 본문에 적는다.

조정 필요(별 후속과 충돌 가능): iouring-batch-coalesce-writev-plan.md(write coalescing Tier 1)도 같은 batched_write에 벡터드(iovec 그룹/opcode::Writev) 확장을 추가한다. 본 Part B(payload_lens 추가)와 목적이 다르나 시그니처가 겹치므로, 둘이 별 PR로 가더라도 통합 시그니처(batched_writev(offsets, buffer_groups, payload_lens, total_lens) 등)를 함께 설계할 것.

검증 요약 (Part A)

단계명령기대
A-2 후pytest -k odirect_padded통과 (batched 0, write_uring 2N, round-trip OK)
A-2 후pytest tests/v1/storage_backend/test_raw_block_core.py전부 통과
A-4pre-commit run --files ...통과

마무리

  • 테스트 클론 git checkout p0-verify 복원.
  • private/ 작업 후: CHANGELOG.md [mod] 기록 + tasks/index.md 동기화(codex 노트 행에 PR 진행 반영).

Out of scope

  • can_batch 동작 변경(= 이전에 검토한 Python Layer 1 완화안). codex 방식 채택으로 제외.
  • _read_buffers 게이트, #3636 orphan-write 항목(별도 follow-up 후보).