본문으로 건너뛰기

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 error
    no 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=falseread_uring이 non-bounce로 비정렬 ptr을 그대로 NVMe DMA addr로 전달. passthrough는 multi-page 전송 시 PRP 페이지 정렬을 요구 → 2MiB(512페이지) 비정렬 시작이 PRP 위반 → EINVAL. header(단일 페이지=PRP 1개)는 통과해서 그동안 안 드러남.
  • 정적 코드 분석상 커맨드 구성(nvme_uring_cmd_prep slba/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의 RawBlockCoreConfigmax_data_transfer_size 를 2 MiB로 강제 주입해 BM1743 조건을 재현했다.

#bench chunk(max_data_transfer_size)내부 chunk(_read_uring_cmd_buffers)_load_meta_payload bufindexedEINVAL
베이스 (auto 256 KiB)256 KiB256 KiB비정렬 bytearray511,744
2 MiB 강제2 MiB2 MiB비정렬 bytearray0✅ @offset 4096
실험 A2 MiB4 KiB (강제)비정렬 bytearray511,744
실험 B2 MiB2 MiBpage-aligned memoryview0✅ @offset 4096
실험 C (A+B)2 MiB4 KiBaligned511,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 지점:

  1. Python: _resolve_max_data_transfer_size에서 uring_cmd일 때 chunk 상한을 컨트롤러 MDTS(또는 PRP 안전치)로 clamp. NVMe ioctl id-ctrl로 MDTS 조회 가능. chunk를 단일 페이지(4 KiB)까지 줄이면 정렬 무관해지지만 submit 횟수↑로 성능 저하 — 실용적으로는 MDTS 안에서 multi-page 쓰되 정렬 보장이 정답.
  2. 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 포함 → 비정렬 버퍼를 페이지 정렬 AlignedBuf bounce로 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_bufferspadding 경로(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 = True
    else:
    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) 위 "수정 방향"대로 Rust read_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 아님.
    1. uring_cmd read를 serial read_uring → 단일 batched_read로 전환.
    2. Rust wait_iouring: first-error Err성공 비트맵 Vec<bool> 반환.
    3. per-IO 성공 bool + completion bitmap 길이 검증(all([]) 방지) + prefix 부분 로드 보존.
  • 버퍼는 여전히 비정렬:
    • 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 반환타입만 변경).
  • → EINVAL 여전히 발생. 단 실패 동작이 "예외 propagate" → "False 비트맵 → _load_meta_payload None"으로 바뀌어 크래시 없이 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 검증 후)

  1. core dispatch _read_slot_headers에서 not self.use_uring_cmd 제거(1줄) → uring_cmd도 _read_slot_headers_batched로.
  2. 테스트 test_validate_loaded_entries_uring_cmd_uses_sequential_uring_cmd_uses_batched_read.
  3. 벤치 --io-engine io_uring_cmd + --cmd-device-path(measure char) + payload device_path omit 재도입(block prepare/char measure path 불일치 우회). README io_uring_cmd 블록 복원.
  4. 실 NVMe char device에서 indexed=N 확인.

위 1~3의 제거 시점 구현은 git 이력(이 커밋 직전)과 plan shimmying-squishing-bumblebee.md F4에 보존됨.