uring_cmd recovery batched — future work
perf/iouring-recovery-batched-read(커밋 87638cb)에서 uring_cmd batched는 제거하고
io_uring(block)+POSIX만 남겼다. uring_cmd recovery 활성화는 아래 선행 버그 수정이 필요하다.
호칭 정정: uring_cmd(passthrough)는 #3274로 dev에 이미 머지됨(
7021790). 따라서 이 버그는 "#3274 영역"이 아니라 현재 dev 코드의 passthrough read 경로(read_uring/_read_uring_cmd_buffers) 문제다.
증상 (실 NVMe, 2026-06-23 · 524,272 entries · /dev/nvme6n1 prepare ↔ /dev/ng6n1 measure)
posix/io_uring(block): 정상 indexed=524272 (io_uring 11.6s, serial 대비 6.08×).io_uring_cmd(char): indexed=0. 로그:_load_meta_payload read failed at offset 4096: [Errno 22] io_uring I/O error_load_meta_payload read failed at offset 134221824: [Errno 22] io_uring I/O errorno valid on-device metadata checkpoint found
원인 (확정 단서 + 유력 가설)
- meta header(offset 0, 4096B=1페이지) read는 성공(magic 읽힘). meta payload(offset
4096+, ~75MiB를
max_data_transfer_size=2MiB chunk로 read)에서 EINVAL. → device_path 검증(P1)이 아니라 payload read 자체 실패. - 유력 원인:
_read_uring_cmd_buffers의 버퍼는 비정렬bytearray인데, uring_cmd는use_odirect=false라read_uring이 non-bounce로 비정렬 ptr을 그대로 NVMe DMA addr로 전달. passthrough는 multi-page 전송 시 PRP 페이지 정렬을 요구 → 2MiB(512페이지) 비정렬 시작이 PRP 위반 → EINVAL. header(단일 페이지=PRP 1개)는 통과해서 그동안 안 드러남. - 정적 코드 분석상 커맨드 구성(
nvme_uring_cmd_prepslba/nlb/opcode/addr/nsid)·NvmeUringCmd레이아웃·lba_shift 출처는 모두 정상.
확정 검증 (다른 PC, 실 NVMe)
- chunk를 4096(단일 페이지)으로 축소 시 EINVAL 사라지면 = multi-page 문제.
- 또는 버퍼를 페이지 정렬 시 사라지면 = 정렬 문제. (둘 다 뿌리는 "정렬 bounce 없음")
확정 검증 결과 (PM1753, 2026-06-25)
장비: SAMSUNG PM1753 / /dev/nvme10n1 (block, prepare) / /dev/ng10n1 (char, measure) / BDF 0000:24:00.0.
규모: 500 GiB / 1 MiB slot ≈ 511,744 entries (payload ~75 MiB). 평가 베이스: cfda5823
(NY 본인 커밋, SY a340ab7 미포함). 워킹트리 패치만 적용·실험 후 전부 revert (commit 없음).
PM1753은 max_hw_sectors_kb=256 → auto chunk = 256 KiB. BM1743(2 MiB chunk)과 달리 auto
세팅에서는 EINVAL이 나지 않음. 그래서 bench의 RawBlockCoreConfig에 max_data_transfer_size
를 2 MiB로 강제 주입해 BM1743 조건을 재현했다.
| # | bench chunk(max_data_transfer_size) | 내부 chunk(_read_uring_cmd_buffers) | _load_meta_payload buf | indexed | EINVAL |
|---|---|---|---|---|---|
| 베이스 (auto 256 KiB) | 256 KiB | 256 KiB | 비정렬 bytearray | 511,744 | ❌ |
| 2 MiB 강제 | 2 MiB | 2 MiB | 비정렬 bytearray | 0 | ✅ @offset 4096 |
| 실험 A | 2 MiB | 4 KiB (강제) | 비정렬 bytearray | 511,744 | ❌ |
| 실험 B | 2 MiB | 2 MiB | page-aligned memoryview | 0 | ✅ @offset 4096 |
| 실험 C (A+B) | 2 MiB | 4 KiB | aligned | 511,744 | ❌ |
해석:
- multi-page chunk가 dominant factor: 4 KiB(단일 페이지)면 정렬 무관 항상 통과.
- 시작 ptr 정렬 단독으로는 부족(실험 B):
_load_meta_payload의 buf 시작을 page-aligned로 만들어도 2 MiB chunk를 통째로read_uring에 넘기면 EINVAL 그대로. 시작만 정렬해도 multi-page PRP의 중간 페이지/페이지 수 제약을 만족시키지 못함(혹은_read_uring_cmd_buffers내부에서dst[:total_len]slice가 다른 backing 정렬에 의존). - 가설 일부 수정: 기존 followup은 "비정렬 ptr → DMA addr 정렬 위반"으로 단일 원인 잡았으나, 실제로는 chunk 크기(=PRP entry 수, MDTS) 가 직접 트리거. 정렬은 필요조건일 수는 있어도 단독 fix로는 작동하지 않음. Rust 정렬 bounce를 넣더라도 chunk가 컨트롤러 MDTS를 넘으면 동일 문제 재현 가능 → fix는 정렬 + chunk 상한 둘 다 보장해야 안전.
왜 chunk 크기가 dominant인지 (NVMe PRP 규칙):
- 단일 페이지 전송(실험 A, chunk=4 KiB): PRP entry 1개(PRP1)만 사용. PRP1은 페이지 내 byte offset을 허용하므로 비정렬 ptr도 OK → 실험 A에서 정렬 안 했는데 통과한 이유.
- multi-page 전송(베이스 2 MiB, 실험 B): PRP1 + PRP_list. PRP_list의 모든 entry는
page-aligned 시작이어야 함. 시작 ptr만 정렬해도 chunk를 통째로
read_uring에 넘기면 컨트롤러 MDTS 초과 또는 list 페이지 정렬 위반으로 EINVAL. - 즉 "정렬 필요"는 multi-page chunk를 쓰겠다는 전제에서만 성립. chunk≤4 KiB면 정렬 무관.
→ 원인 카테고리: multi-page chunk 자체 (BM1743 EINVAL의 직접 원인이며, BM1743의 큰
max_hw_sectors_kb(2 MiB)에서 auto resolve 결과가 PRP 위반 chunk를 만들어 터졌음).
→ 다음 단계 fix 지점:
- Python:
_resolve_max_data_transfer_size에서 uring_cmd일 때 chunk 상한을 컨트롤러 MDTS(또는 PRP 안전치)로 clamp. NVMe ioctlid-ctrl로 MDTS 조회 가능. chunk를 단일 페이지(4 KiB)까지 줄이면 정렬 무관해지지만 submit 횟수↑로 성능 저하 — 실용적으로는 MDTS 안에서 multi-page 쓰되 정렬 보장이 정답. - Rust(별도 PR, @DongDongJu):
batched_read공유 submit 경로 bounce 조건을use_odirect || use_uring_cmd로 확장. multi-page chunk를 유지하면서 비정렬 ptr을 자동 bounce → PRP entry 정렬 보장.
결과 로그:
/tmp/pm1753_baseline.log,/tmp/pm1753_diag.log,/tmp/pm1753_2mib.log,/tmp/pm1753_expA.log,/tmp/pm1753_expB.log,/tmp/pm1753_expC.log(모두 detached 임시).
수정 방향 (Rust, 정렬 bounce)
- Rust
read_uring/batched_read의 정렬 판정에use_uring_cmd포함 → 비정렬 버퍼를 페이지 정렬AlignedBufbounce로 read + copy-back(기존 bounce 경로 재사용). 정렬 bounce면 2MiB multi-page PRP도 정상 → chunk 축소 불필요.let needs_align = self.use_odirect || self.use_uring_cmd;let ptr_aligned = if needs_align { (ptr as usize).is_multiple_of(align) } else { true };let use_bounce = !ptr_aligned || cap < total_len; - 영향 범위:
load_many_into/_read_buffers등 모든 uring_cmd read의 multi-page payload가 동일 버그. recovery만의 문제 아님 → passthrough read 전반 수정 + 별도 이슈/Rust PR(CODEOWNER @DongDongJu 영역)로 격상.
기존 fix 브랜치 검토 — priv/sy/fix/raw-block-uring-cmd-aligned-buffers (커밋 a340ab7)
결론: 이 브랜치만으로는 recovery 버그 해결 안 됨 (부분 fix).
- 브랜치 내용:
_allocate_aligned_buffer()추가 후,_read_uring_cmd_buffers/_write_uring_cmd_buffers의 padding 경로(len(dst) < total_len)에서 만드는 임시 버퍼만block_align정렬로 교체.# _read_uring_cmd_buffers (core.py:1227~)if len(dst) < total_len: # padding 필요할 때만target = self._allocate_aligned_buffer(total_len) # 정렬 ✅ (fix가 바꾼 곳)copy_back = Trueelse:target = dst[:total_len] # 버퍼 충분하면 호출자 버퍼 그대로 (정렬 안 함 ❌)copy_back = False - recovery는 이 분기를 안 탐:
_load_meta_payload(core.py:1539)가buf = bytearray(total_len)로 정확히 total_len 크기 버퍼를 할당 →len(dst) < total_len이 거짓 →else분기 → 정렬 안 된bytearray를 그대로 DMA 대상으로 전달 → 비정렬 multi-page PRP 위반 → EINVAL 그대로. - 즉 a340ab7은 "호출자 버퍼 < total_len(=padding 필요)" 케이스만 커버. recovery처럼 정확한 크기의 비정렬 버퍼를 직접 넘기는 경로는 미커버.
- → 보강 옵션: (a) Python
else분기도 "ptr 비정렬이면 aligned target bounce + copy_back"으로 확장, 또는 (b) 위 "수정 방향"대로 Rustread_uring에서 비정렬 ptr 무조건 bounce. 호출자별 중복을 피하려면 (b)가 깔끔(모든 호출자 일괄 해결). a340ab7은 (b)와 별개로 padding 경로 정렬을 보장하므로 병행 가치는 있으나 recovery 재활성화의 선행조건은 (b).
기존 PR 검토 — #3812 core/rawblock-load-many-iouring-batch (3xdevv)
결론: 이 PR로도 recovery 버그 해결 안 됨. 정렬을 전혀 안 건드림 — 실패 방식만 fail-closed로 바뀜.
- PR 목적("Batch load_many reads with per-IO results"): 에러 처리/배칭 의미론 변경이지 정렬 fix 아님.
- uring_cmd read를 serial
read_uring→ 단일batched_read로 전환. - Rust
wait_iouring: first-errorErr→ 성공 비트맵Vec<bool>반환. - per-IO 성공 bool + completion bitmap 길이 검증(
all([])방지) + prefix 부분 로드 보존.
- uring_cmd read를 serial
- 버퍼는 여전히 비정렬:
- Python
_read_uring_cmd_buffers: 배칭으로 리팩터됐으나target = memoryview(bytearray(total_len))(비정렬, a340ab7의 정렬판도 아님) /dst[:total_len](호출자 비정렬) 그대로. → a340ab7의 Python 정렬 보강을 오히려 plain bytearray로 되돌려 무력화. - Rust
batched_read공유 submit 경로(lib.rs:2092):if use_odirect { …bounce… } else { (ptr,None,…) }→ uring_cmd(use_odirect=false)는 여전히 비정렬 ptr 그대로 DMA. PR은 이 블록 미수정 (Rust diff는wait_iouring반환타입만 변경).
- Python
- → EINVAL 여전히 발생. 단 실패 동작이 "예외 propagate" → "
False비트맵 →_load_meta_payloadNone"으로 바뀌어 크래시 없이 indexed=0(관측 결과는 동일). - 수정 지점 이동(중요): #3812 머지 후 uring_cmd read가
read_uring이 아니라batched_read로 가므로, 위 (b)의 정렬 bounce를 넣을 1순위 지점이 lib.rs:2092의batched_read공유 submit 경로로 이동. 거기 조건을use_odirect || use_uring_cmd로 → recovery·load_many일괄 해결. (Python 보강은 #3812이 되돌려놨으므로 Rust 단일 지점 수정이 정답.)
재활성화 (Rust 수정·실 NVMe 검증 후)
- core dispatch
_read_slot_headers에서not self.use_uring_cmd제거(1줄) → uring_cmd도_read_slot_headers_batched로. - 테스트
test_validate_loaded_entries_uring_cmd_uses_sequential→_uring_cmd_uses_batched_read. - 벤치
--io-engine io_uring_cmd+--cmd-device-path(measure char) + payloaddevice_pathomit 재도입(block prepare/char measure path 불일치 우회). README io_uring_cmd 블록 복원. - 실 NVMe char device에서
indexed=N확인.
위 1~3의 제거 시점 구현은 git 이력(이 커밋 직전)과 plan
shimmying-squishing-bumblebee.mdF4에 보존됨.