본문으로 건너뛰기

P0 — put_many 내부 병렬 I/O (batched_write N-SQE)

한 줄 요약: put_many 내부 N-key 순차 루프를 단일 batched_write(2N SQE) 호출로 교체하여 NVMe NCQ 병렬성 활성화. 구 L2 correctness fix(①②) 흡수 — #3274 머지 후 단독 PR.

plugin dispatch batching(if io_engine=="io_uring" and len(pending)>1: _submit_put_many)은 PR #3274에 이미 존재 — 이번 PR이 신설한 게 아니다. 이번 PR의 plugin 변경은 dedup 비용 절감(exists_many 일괄 + 단일 _put_lock 윈도우) + ref 누수 정리 (try/except)뿐(§3.0 정정 참조).


1. 배경

1.1 현재 put_many 구조 (PR #3274 이후에도)

PR #3274가 io_uring 인프라(_write_buffers/_read_buffers dispatcher)를 도입하지만, put_many 내부 루프는 여전히 키 1개씩 순차 처리:

# core.py put_many 내부 (단순화)
for spec, obj in zip(specs, objs):
slot = self._allocate(spec) # _put_lock 획득/해제
self._write_one(slot, spec, obj) # io_uring: write_uring × 2 (header + payload)
self._commit(slot, spec) # _put_lock 획득/해제

_write_onecan_batch=False(header=32B, O_DIRECT 요구=4096B 불일치) → 순차. 체크포인트 write만 batched_write로 진짜 배치 처리됨 (io_uring_post_pr3274.md §2-2).

1.2 correctness fix(구 L2) 흡수

구 L2(batched_submit_put_task correctness: ① batched dedup + ② ref 누수 롤백)는 2026-06-10 P0로 흡수, 단독 PR 폐기(브랜치 fix/rawblock-batched-put-toctou 삭제). P0의 batched_submit_put_task가 ①②를 이미 포함하므로 별도 이전 작업 없음 (P0 ⊃ L2). 폐기 이유: ①은 멱등 core 위 마이크로 최적화(진짜 TOCTOU 아님 — core.put_many_lock 안에서 _index/_inflight 재검사), ②는 셧다운 경로 좁은 하드닝 → 둘 다 배치 dispatch 맥락(=P0)에서만 값이 산다. 상세 L2 노트.

P0가 내부 NCQ 병렬화(core _put_many_batch_io) + 구 L2 correctness(plugin ①②)를 함께 소유한다. plugin dispatch batching 분기 자체는 PR #3274 기여로 이미 존재 (§3.0 정정 참조).

dispatch batching alone regression (벤치마크 근거, 분리 이유): plugin이 N개 키를 1 task로 묶되 core 내부가 순차이면 fanout의 thread pool 병렬성이 dispatch 절약을 압도 → 실 스토리지에서 regression.

벤치마크 결과 (2026-06-08, bench_dispatch_patterns.py):

latN=10N=100N=1000
0µs (순수 CPU)batch 85% 빠름 ✓batch 85% 빠름 ✓batch 93% 빠름 ✓
50µs (NVMe 현실)fanout 110% 빠름 !fanout 60% 빠름 !~equal
200µs (느린 NVMe)fanout 395% 빠름 !fanout 44% 빠름 !fanout 126% 빠름 !

근본 원인: batch = 1 to_thread에서 N key 순차 write. fanout = N to_thread가 thread pool 동시 실행.

P0 구현 후 예상:

  • L2: dispatch 1회 (overhead N→1)
  • P0: N SQE 한 번에 submit → NCQ가 N개 동시 처리 → lat=50µs 구간에서도 batch가 fanout 대비 이득

2. 설계 방향

2.1 목표 구조

# 목표 core.py put_many (단순화)

# Phase 1: 전체 N 슬롯 할당 (단일 lock 구간)
slots = self._allocate_many(specs) # _put_lock 한 번, N 슬롯 예약

# Phase 2: N-SQE 배치 제출 (NVMe NCQ 활용)
offsets = [self._slot_offset(s) for s in slots]
buffers = [self._prepare_combined_buffer(spec, obj) for spec, obj in zip(specs, objs)]
lens = [len(b) for b in buffers]
batch_id = self._raw_dev.batched_write(offsets, buffers, lens)
self._raw_dev.wait_iouring(batch_id) # 모든 CQE 수거

# Phase 3: 일괄 commit
self._commit_many(slots, specs) # _put_lock 한 번

2.2 현재 대비 변경 요약

항목현재 (PR #3274 이후)목표
슬롯 할당key별 _put_lock 획득 N회_allocate_many — lock 1회
I/O 제출_write_one N회 (2 write_uring per key)batched_write([N]) — SQE 2N개를 한 번에 submit
NVMe NCQ활용 안 됨 (순차)N개 동시 전송 가능
commitkey별 _put_lock N회_commit_many — lock 1회

2.3 header 정렬 문제 (can_batch=False 원인)

현재 _write_onecan_batch=False인 이유:

  • header = 32B, payload는 블록 정렬됨
  • O_DIRECT는 모든 버퍼가 block_align(≥512B, 실제 4096B) 배수여야 함
  • header 단독 SQE는 정렬 불만족 → batched_write 거부 (Rust에서 ValueError)

해결 방향 비교:

방안설명하위호환비고
(b) 합산 버퍼header + payload를 정렬된 단일 버퍼에 합산 → SQE 1개✅ 슬롯 레이아웃 불변zero-copy 뷰 재설계 필요
(a) header 패딩header 슬롯을 4096B로 확장❌ 포맷 변경마이그레이션 필요
(c) buffered 모드header만 O_DIRECT 없이 write중간io_uring buffered write 지원 여부 확인 필요

권장: (b) 합산 버퍼 — 슬롯 레이아웃 변경 없음, Rust batched_write 재사용 가능. _build_direct_odirect_view의 zero-copy 경로와 정합성 별도 검토 필요.

2.4 부분 실패 처리

batched_write 중 일부 SQE 실패 시 CQE에 per-SQE error code 반환. → 결과 비트맵(list[bool])으로 변환, 현재 put_many.results 인터페이스 유지.


3. 구현 내용 (2026-06-08 완료, 2026-06-10 브랜치 분리)

브랜치: perf/rawblock-put-many-batch-io (ankit-sam/raw_iouring_cmd = PR #3274 기반)

3.0 Plugin 변경 (rust_raw_block_backend.py) — correctness 보강만

정정 (2026-06-11): 초기 서술이 "P0가 dispatch batching을 추가"한 것처럼 읽혔으나, dispatch batching 분기는 PR #3274 시점(bf7d2ff1)에 이미 존재. 확인 명령: git diff bf7d2ff1 aad5e37e -- lmcache/v1/storage_backend/plugins/rust_raw_block_backend.py

해당 diff에서 if self._core.io_engine == "io_uring" and len(pending) > 1:로 시작하는 분기와 _submit_put_many async 함수는 양쪽 커밋 모두에 동일하게 존재. 이번 PR(aad5e37e)이 plugin에 추가한 것은 아래 두 항목뿐:

  1. dedup 비용 절감 (구 L2 ①): 키마다 contains_key(Rust FFI) + _put_lock 2~3회씩 호출하던 것을 exists_many 배치 1회 + 단일 _put_lock 윈도우 안으로 묶음. 진짜 TOCTOU 수정 아님 — core.put_many가 이미 _lock 안에서 _index/_inflight 재검사하여 멱등이므로, plugin dedup이 stale해도 중복 write는 발생 안 함. 코루틴 dispatch 낭비를 줄이는 best-effort 최적화.
  2. ref/inflight 누수 정리 (구 L2 ②): obj.ref_count_up()run_coroutine_threadsafe 사이 예외(주로 셧다운 레이스)에서 ref가 올라간 채 _put_tasks에 키가 영구히 남던 문제를 try/except로 감싸 롤백.

기존 dispatch 분기(이미 #3274에 존재):

if self._core.io_engine == "io_uring" and len(pending) > 1:
fut = asyncio.run_coroutine_threadsafe(
self._submit_put_many(pending, on_complete_callback), loop)
return [fut]

_submit_put_many async 함수(N개 (key, spec, obj) → core.put_many(N_keys) 1회 호출)도 PR #3274 기여이며 이번 PR에서 본문 변경 없음.

3.1 put_many 분기 (core.py)

def put_many(self, keys, objs):
# 검증 ...
if self.io_engine == "io_uring" and len(keys) > 1:
return self._put_many_batch_io(keys, objs) # ← 신설 batch 경로
# 기존 순차 루프 (posix 또는 단일 key) — 한 줄도 변경 안 함
for i, (key, obj) in enumerate(...):
...

분기 조건 = io_engine=="io_uring" AND len(keys)>1. posix는 thread pool 병렬성이 이미 잘 작동하므로 batch로 빼지 않음 (벤치마크가 posix batch는 regression임을 보여줌).

3.2 _put_many_batch_io 4단계 (신설)

  • Phase A (단일 _lock): N개 키에 대해 index/inflight dedup → _allocate_slot_locked 로 N슬롯 할당 → _Inflight N개 등록. write_plan 구성.
  • Phase B+C: write_plan의 각 키마다 header + payload 버퍼를 모아 2N 길이 리스트 구성 → _write_buffers(all_offsets, all_bufs, ...) 1회 호출. 내부에서 can_batch=Truebatched_write([2N]) → 1 io_uring_submit → NCQ.
  • Phase D (단일 _lock): write_plan 전체를 _index에 일괄 commit, _inflight 정리.
  • 예외 처리: _write_buffers 실패 시 except 블록에서 write_plan 전체를 _inflight.pop + _append_free_slot_locked 롤백 (all-or-nothing).

3.3 테스트 (tests/v1/storage_backend/test_raw_block_core.py)

posix core에서 _put_many_batch_io를 직접 호출하여 Phase A/D 정합성 검증 (io_uring 실장 비의존). 3개 모두 통과:

  • test_put_many_batch_io_round_trip — N=10 write→load round-trip
  • test_put_many_batch_io_skips_already_indexed — 중복 5 + 신규 5, stored_keys 신규만
  • test_put_many_batch_io_write_failure_rolls_back_write_buffers mock raise → 슬롯 전량 반환, _inflight 비었는지 확인

기존 회귀: test_raw_block_core.py + test_rust_raw_block_backend.py 34개 통과. (GDS·uring_cmd_get_nvme_info 2건 실패는 실 NVMe 권한 문제로 무관)


4. 성능 검증 — 시뮬레이션 (2026-06-08)

스크립트: benchmarks/storage_backend_io/bench_dispatch_patterns.py (4-way 확장) 환경: fake in-memory device, Python 3.12, warmup=3, iters=10

4.1 결과

4-way dispatch benchmark obj=64B
N lat µs fanout batch/posix batch+P0 P0 vs fanout
10 0 0.750 ms 0.276 ms 0.330 ms -55.9% ✓
10 50 1.791 ms 3.566 ms 2.303 ms +28.6%
10 200 6.112 ms 6.476 ms 4.027 ms -34.1% ✓
100 0 6.957 ms 0.919 ms 0.915 ms -86.8% ✓
100 50 22.672 ms 27.038 ms 16.240 ms -28.4% ✓
100 200 43.198 ms 57.953 ms 31.163 ms -27.9% ✓
1000 0 115.275 ms 7.859 ms 7.993 ms -93.1% ✓
1000 50 321.555 ms 265.765 ms 164.061 ms -49.0% ✓
1000 200 232.541 ms 579.574 ms 309.628 ms +33.2%

4.2 해석

  • N=100, lat=50µs (NVMe 현실): batch/posix는 fanout 대비 +19% regression (L2 단독 한계 재확인), batch+P0는 fanout 대비 −28% → P0가 L2 regression을 뒤집고 fanout보다 빠름.
  • N=100, lat=200µs: batch+P0 −28%, 일관되게 우위.
  • N=1000, lat=200µs: batch+P0가 fanout보다 +33% 느림 — Phase A 단일 lock 구간에서 1000개 슬롯 순차 할당이 직렬화 병목. 큰 배치에서 Phase A 최적화 여지 있음 (후속).

4.3 ⚠️ 시뮬레이션의 결정적 한계 (반드시 인지)

이 벤치마크는 NCQ 병렬성을 모델 가정으로 박아넣었다:

def batched_write(self, offsets, buffers, total_lens):
if self._latency_s > 0:
time.sleep(self._latency_s) # ← N개를 latency 1회만 대기
# = "NVMe가 N개를 완벽 병렬 처리한다"를 전제

즉 "P0가 NCQ로 빨라진다"는 결론을 전제로 깔고 측정한 것이라 논리적으로 순환이다. 이 벤치마크가 증명하는 것은:

  • ✅ P0 구조가 N-SQE batch를 한 번에 submit할 수 있는 형태임
  • ✅ dispatch/lock overhead 측면에서 batch가 fanout 대비 유리할 여지
  • ❌ 실제 NVMe에서 NCQ가 정말 병렬화하는지 — 증명 못 함

검증 안 된 실제 변수: io_uring submit 오버헤드, Rust 2N 버퍼 준비 비용, NVMe 큐 깊이·칩 수에 따른 실 병렬도, wait_iouring 완료 수거 비용.


5. 실측 검증 계획 (미실행)

5.1 필요 환경

io_engine="io_uring"을 실제로 태우려면 실 NVMe block device + io_uring 지원 커널(5.19+). fake device로는 검증 불가 (ci_test_coverage.md §4-4: io_uring은 CI best-effort, 실장 필수).

5.2 측정 스크립트 (작성 필요)

benchmarks/storage_backend_io/bench_put_many_real_nvme.py (신규):

  • 실 device path 인자 (/dev/nvmeXnY 또는 테스트 파일 on NVMe FS)
  • io_engine="io_uring", use_odirect=True로 RawBlockCore 생성
  • 비교: (a) 기존 순차 put_many (분기 우회 플래그) vs (b) _put_many_batch_io
  • 그리드: N=[10, 100, 1000], payload=[4KB, 64KB, 256KB] (실 KV 청크 크기)
  • 측정: wall-clock, p50/p95/p99, iostat로 device QD(큐 깊이) 동시 관찰

5.3 판정 기준

  • N=100, 실 NVMe에서 batch+P0가 순차 대비 >10% 빠르면 의미 있음 → PR 진행
  • iostat에서 batch 시 평균 QD가 순차 대비 상승 확인 (NCQ 활용 증거)
  • 차이 <5%면: 구조는 맞으나 실효 없음 → L2 correctness fix만 살리고 P0 보류

5.4 실행 체크리스트

  • 실 NVMe device 확보 (Samsung SSD 평가 장비?)
  • bench_put_many_real_nvme.py 작성
  • 순차/batch 분기 우회 플래그 또는 두 코어 인스턴스
  • 5.3 기준으로 판정 → 노트 §6 갱신

6. 실측 결과 (2026-06-11)

6.1 측정 환경

  • 디바이스: /dev/nvme10n1 (Samsung PM1753, PCI BDF 0000:24:00.0, 27.9TB)
    • setfacl -m u:ny:rw /dev/nvme10n1 로 사용자 권한 부여 (disk 그룹 추가 아님)
    • 파일시스템 없음, 마운트 없음 → raw block 직접 접근
  • io_engine: io_uring (use_odirect=True, iouring_queue_depth=256)
  • block_align: 4096B / header_bytes: 4096B / meta: 4MB
  • 반복: warmup 3회 + 측정 10회 (cell당)
  • 비교 방식: 같은 bench_put_many_real_nvme.py 스크립트로
    • baseline = git checkout bf7d2ff1 (= aad5e37e^, P0 직전)
    • p0_batch = git checkout aad5e37e (P0 적용)
  • 스크립트: /tmp/bench_put_many_real_nvme.py (repo 외부)

6.2 결과

N payload baseline_p50 p0_batch_p50 delta% baseline_p95 p0_batch_p95 delta%
10 4KB 0.771ms 0.381ms -50.6% 0.791ms 0.407ms -48.6%
10 64KB 0.961ms 0.454ms -52.7% 0.984ms 0.607ms -38.3%
10 256KB 1.583ms 1.310ms -17.2% 1.816ms 2.271ms +25.0%
100 4KB 9.160ms 5.526ms -39.7% 9.245ms 5.615ms -39.3%
100 64KB 11.901ms 4.755ms -60.1% 12.037ms 5.108ms -57.6%
100 256KB 18.789ms 10.857ms -42.2% 19.203ms 16.402ms -14.6%
1000 4KB 91.954ms 33.326ms -63.8% 191.424ms 34.874ms -81.8%
1000 64KB 98.191ms 50.321ms -48.8% 228.303ms 51.594ms -77.4%
1000 256KB 163.241ms 94.188ms -42.3% 186.740ms 95.289ms -49.0%

6.3 해석

판정 기준 (5.3) 통과:

  • N=100, payload=4~64KB에서 p0_batch가 baseline 대비 −40% ~ −60% (≫ +10% 기준)
  • 모든 셀(9/9)에서 p50 단조 감소, 차이 ≥17%
  • 최대 효과: N=1000, 4KB에서 p95 −81.8% — baseline의 큰 batch 꼬리 지연이 p0_batch에서 거의 사라짐

구조적 이득 확인:

  • N=10, 256KB에서도 p50 −17% — 작은 배치/큰 페이로드에서도 SQE 병합·락 1회화 효과 존재
  • N=1000 구간에서도 단조 개선 — 시뮬레이션이 우려한 Phase A 단일 락 직렬화 병목이 실 NVMe에서는 device latency가 압도하여 가시화되지 않음
  • baseline의 N=1000 p95가 p50의 ~2배(꼬리 지연 큼) → p0_batch는 p95가 p50에 근접 (NCQ가 N건을 평탄하게 흘려보냄)

시뮬레이션 한계 해소: §4.3에서 우려한 "NCQ 병렬성을 모델 가정으로 박은 순환 측정"은 실 디바이스에서 유의미한 lat 감소(전 셀 −17% ~ −82%)로 reproducible. io_uring submit 오버헤드, 2N 버퍼 준비 비용, wait_iouring 수거 비용 모두 NCQ 이득에 의해 상쇄됨.

미관찰 항목:

  • iostat avgqu-sz 동시 관찰은 수행하지 않음 (lat 차이가 명확해 추가 검증 불요). 필요 시 후속으로 iostat -x nvme10n1 1 병행 실행으로 큐 깊이 직접 확인 가능.

6.4 결론

판정 기준의 모든 조건(N=100에서 >10% 빠름, 차이 <5%인 셀 없음) 통과. P0 PR 진행 권고.


6.5 정리본 측정 — PR 후보 브랜치 (860d0598, 2026-06-11)

§6.1~6.4는 aad5e37e(plugin correctness fix 포함) 기준. 실제 PR로 올릴 브랜치는 forkdev/perf/rawblock-put-many-iouring-batch (HEAD: 860d0598, "[Perf][RawBlock] Batch io_uring put_many writes into a single submission")로, plugin 변경 없는 core-only 정리본이다 (구 L2 ①② 제외).

부모 동일성: 두 브랜치 모두 bf7d2ff1이 부모 → baseline은 §6.2와 동일 데이터 재사용.

측정 환경: §6.1과 동일 (/dev/nvme10n1, io_uring/O_DIRECT, warmup 3 / iters 10).

6.5.1 baseline vs p0_clean — 핵심 결과

실제 KV 청크 쓰기 워크로드를 대표하는 N=100 / N=1000 구간, 4KB / 64KB payload만 정리한 표 (그리드 전체는 §6.5.5 raw 데이터 참조):

Npayloadbaseline p50p0_clean p50Δ p50baseline p95p0_clean p95Δ p95
1004 KB9.16 ms3.56 ms−61.2 %9.25 ms3.68 ms−60.2 %
10064 KB11.90 ms4.04 ms−66.0 %12.04 ms5.27 ms−56.2 %
10004 KB91.95 ms43.40 ms−52.8 %191.42 ms44.37 ms−76.8 %
100064 KB98.19 ms50.19 ms−48.9 %228.30 ms54.67 ms−76.1 %

→ 핵심 워크로드 4셀 모두 p50 −49 % ~ −66 %, p95에서는 baseline의 큰 꼬리 지연(p95/p50 ≈ 2x)이 p0_clean에서 거의 사라짐. 판정 기준(>10 % 빠름) 압도적 통과.

6.5.2 정리 전후 비교 — p0_batch (aad5e37e) vs p0_clean (860d0598)

N payload p0_batch_p50 p0_clean_p50 delta%
10 4KB 0.381ms 0.342ms -10.2%
10 64KB 0.454ms 0.485ms +6.8%
10 262144 1.310ms 1.574ms +20.1% (*)
100 4KB 5.526ms 3.555ms -35.7%
100 64KB 4.755ms 4.041ms -15.0%
100 256KB 10.857ms 10.383ms -4.4%
1000 4KB 33.326ms 43.399ms +30.2% (**)
1000 64KB 50.321ms 50.186ms -0.3%
1000 256KB 94.188ms 93.969ms -0.2%

(*) N=10/256KB +20% — 절대 lat 1.3 → 1.6ms, 작은 절대값에 변동성이 큼. (**) N=1000/4KB +30% — p0_clean이 p0_batch 대비 약 10ms 느림. baseline 대비로는 여전히 −53%. 가능성: ① 측정 노이즈(N=1000은 baseline p95/p50 비율도 ~2배로 변동 큼), ② 정리 과정에서 hot path 마이크로 변경 (예: payload_lens 계산이 io_uring 전용 조건문 제거, inflight_io_count 증감을 try 안쪽으로 이동). 코드 차이는 §6.5.3 참조.

대부분 셀(7/9)에서 p0_clean이 p0_batch와 ±15% 이내 또는 더 빠름. 두 outlier도 baseline 대비로는 큰 폭 단조 개선이라 PR 진행 자체는 정당화됨. 다만 N=1000/4KB 구간은 후속 측정으로 노이즈 vs 진짜 회귀를 가려볼 가치가 있음.

6.5.3 코드 차이 (요약)

git diff aad5e37e 860d0598 -- lmcache/v1/storage_backend/raw_block/core.py:

  • 메소드 위치 이동 (put_many 직후 → 파일 후반)
  • docstring 정비
  • hdr_payload_len 계산: if io_engine == "io_uring" else len(header) 분기 제거, _requires_transfer_alignment 결과를 그대로 사용 (정리)
  • 예외 처리 구조: try ... except 외부 블록 → try/finally로 inflight 카운터만 보호하고 commit/롤백을 단일 Phase D 락 안으로 통합
  • 의미상 동일하나 hot path 한 줄 분기가 사라지면서 cache miss/branch pred 양상이 미세하게 달라질 수 있음 — N=1000/4KB의 ±10ms 노이즈와 일관됨

6.5.4 결론

  • 판정 기준 (1차 동일): p0_clean이 baseline 대비 N=100/4~64KB에서 −61% ~ −66%

    10% 기준 압도적 통과. 9/9 셀이 단조 개선 (p50 기준; p95는 N=10/256KB 1셀 outlier).

  • 회귀 검증: N=1000/4KB만 정리 전(p0_batch) 대비 +30% 느림. 절대 lat은 여전히 baseline의 절반 이하. 측정 노이즈 가능성 큼. 후속 재측정으로 확인 권고.
  • PR 진행 OK: forkdev/perf/rawblock-put-many-iouring-batch(860d0598)로 PR 제출 가능. plugin은 변경하지 않는 깔끔한 core-only 변경. 기대 lat 감소를 실 NVMe에서 재현.

6.5.5 전체 그리드 raw 데이터

요약 표(§6.5.1)에서 가린 N=10 셀과 N=*/256KB 셀까지 포함한 전체 9개 셀:

N payload baseline_p50 p0_clean_p50 delta% baseline_p95 p0_clean_p95 delta%
10 4KB 0.771ms 0.342ms -55.7% 0.791ms 0.356ms -55.0%
10 64KB 0.961ms 0.485ms -49.5% 0.984ms 0.504ms -48.7%
10 256KB 1.583ms 1.574ms -0.6% 1.816ms 10.742ms +491.5% (*)
100 4KB 9.160ms 3.555ms -61.2% 9.245ms 3.682ms -60.2%
100 64KB 11.901ms 4.041ms -66.0% 12.037ms 5.273ms -56.2%
100 256KB 18.789ms 10.383ms -44.7% 19.203ms 10.799ms -43.8%
1000 4KB 91.954ms 43.399ms -52.8% 191.424ms 44.372ms -76.8%
1000 64KB 98.191ms 50.186ms -48.9% 228.303ms 54.670ms -76.1%
1000 256KB 163.241ms 93.969ms -42.4% 186.740ms 95.576ms -48.8%

(*) N=10/256KB p95: 단일 jitter 샘플 (절대 lat ≤2.3ms 구간, p50은 거의 동일 −0.6%). N=10은 절대 lat이 1ms 미만이라 큐에 쌓을 명령 자체가 적어 batch 이득이 작고, 이 구간 p95는 OS 스케줄링/페이지 캐시 워밍업 변동에 민감.


7. PR 구성 계획

단독 PR (구 L2 흡수). #3274 머지 + 실측 검증 완료 시 제출. core(_put_many_batch_io)

  • plugin(구 L2 correctness ①② — dedup 비용 절감 + ref/inflight 정리)을 한 PR로. plugin dispatch batching 분기는 PR #3274 기여라 본 PR에는 변경 없음.
변경파일내용상태
P0 (core)core.py_put_many_batch_io N-SQE batch (본 PR의 본질 변경)구현 완료, 실측 통과 (2026-06-11)
P0 (plugin)rust_raw_block_backend.py구 L2 ①②: dedup 일괄(exists_many+단일 _put_lock 윈도우), try/except ref 롤백구현 완료
P0 테스트test_raw_block_core.pycorrectness 3개통과
시뮬 벤치bench_dispatch_patterns.py4-way 시뮬레이션완료 (NCQ 가정 순환)
실측 벤치/tmp/bench_put_many_real_nvme.py/dev/nvme10n1 실측 (§6)완료, 9/9 셀 단조 개선

제출 조건 (충족): #3274 머지 → 실 NVMe 실측 통과(§6) → P0 PR 제출 가능.


8. 변경 로그

일자작성자내용
2026-06-08ny초안 작성. L2 벤치마크 결과(NVMe 구간 regression) + io_uring 분석 후 방향 결정. L2와 묶어 단일 PR 계획 수립.
2026-06-08nyP0 구현 완료 (_put_many_batch_io, branch perf/l2-p0-batch-put). correctness 테스트 3개 통과. 4-way 시뮬레이션: N=100/lat=50µs에서 batch+P0가 fanout −28%. 단 NCQ 병렬성을 모델 가정으로 깐 순환 측정 — 실 NVMe 검증 필요. §5 실측 계획 수립.
2026-06-10nyL2/P0 브랜치 분리 재정비. dispatch batching을 L2에서 제거 → P0로 이전. P0 커밋에 plugin.py 변경 명시적 포함 (TOCTOU fix + dispatch batching). 브랜치 perf/rawblock-put-many-batch-io로 갱신. 36개 테스트 통과.
2026-06-10ny구 L2 흡수 확정. P0의 batched_submit_put_task가 ①batched dedup·②ref 롤백을 이미 포함(P0 ⊃ L2) → L2 단독 PR 폐기, 브랜치 fix/rawblock-batched-put-toctou(8860f132) 삭제. 리뷰 결론: ①은 멱등 core 위 마이크로 최적화(진짜 TOCTOU 아님), ②는 셧다운 경로 하드닝. P0 = plugin(dispatch+①②)+core 단일 PR. 코드 이동 없음(이미 superset).
2026-06-11ny실측 검증 완료. /dev/nvme10n1(Samsung PM1753) + io_uring/O_DIRECT, aad5e37e^ vs aad5e37e 직접 비교. 9개 (N, payload) 셀 전부 단조 개선 (p50 −17% ~ −64%, 최대 p95 −82% @ N=1000/4KB). N=100에서 −40% ~ −60% — 판정 기준(>10%) 압도적 통과. 시뮬레이션이 우려한 NCQ 가정 순환 측정 한계 해소. §6 작성. P0 PR 진행 가능.
2026-06-11ny문서 정정. 이전 서술이 "P0가 plugin dispatch batching을 추가"한 것처럼 읽혔으나, git diff bf7d2ff1 aad5e37e -- .../rust_raw_block_backend.py 확인 결과 dispatch batching 분기(if io_engine=="io_uring" and len>1: _submit_put_many)와 _submit_put_many 함수는 PR #3274 시점에 이미 존재. 본 PR이 plugin에 추가한 것은 구 L2 ①②(dedup 일괄+ref 롤백)뿐이며, 본질 변경은 core _put_many_batch_io 단독. §3.0/§7/한 줄 요약 정정. wiki §3 동일 정정.
2026-06-11nyPR 후보 정리본 재측정. forkdev/perf/rawblock-put-many-iouring-batch(860d0598, plugin 변경 없는 core-only) 측정. baseline 재사용(부모 동일 bf7d2ff1). 9/9 셀 p50 단조 개선 (N=100/4~64KB에서 −61% ~ −66%). 정리 전(aad5e37e) 대비 7/9 셀이 ±15% 이내; N=1000/4KB만 +30%(절대 lat은 여전히 baseline 절반 이하 — 측정 노이즈 가능성). N=10/256KB p95 outlier 1건. §6.5 추가. PR 제출 가능.
2026-06-15nyplugin-dedup 분리 PR 준비 완료. 구 L2 ①②(batched dedup + ref 롤백)만 포함하는 plugin-only 브랜치 perf/rawblock-put-many-plugin-dedup을 upstream/dev 기반으로 리베이스 후 4커밋 squash → 242d5ca9. pre-commit 전체 통과. PR 본문 작성 완료, 제출 예정.
2026-06-15nyplugin-dedup → 순수 ref-rollback fix로 축소. 검토 결과 ①dedup 배치화(exists_many+단일 _put_lock)는 구 L2 폐기 판단과 동일 계열(<1% gain·실측 미입증)이고 _put_lock hold 증가 트레이드오프(치명적 회귀는 없으나 미입증)를 동반 → dev 원본 dedup으로 되돌리고 ②ref 롤백만 유지. dev 대비 diff = dispatch try/except 롤백 블록만 추가(upcount 제거로 현 브랜치보다 단순). empty-keys early-return·테스트 제거(dev가 이미 None 반환). 커밋 8046269e, 제목 [Fix][RawBlock].... test 31 pass + pre-commit pass. 브랜치 perf/rawblock-put-many-plugin-dedupfix/rawblock-put-dispatch-ref-rollback로 rename, origin push 완료 + 옛 origin 브랜치 삭제.