Skip to content

Start async device writes and default host reads eagerly instead of deferring I/O to future wait#23231

Open
Emillock wants to merge 2 commits into
rapidsai:mainfrom
Emillock:main
Open

Start async device writes and default host reads eagerly instead of deferring I/O to future wait#23231
Emillock wants to merge 2 commits into
rapidsai:mainfrom
Emillock:main

Conversation

@Emillock

Copy link
Copy Markdown

Description

Fixes two instances of the same defect class in cpp/src/io/utilities: an "async" API wraps the initiation of I/O inside std::async(std::launch::deferred, ...), so the I/O does not start until the returned future is waited on. Since the writers/readers fan out these futures and then wait on them in order, the I/O executes strictly serially — one operation's full latency after another — instead of in parallel as the calling code intends.

1. file_sink::device_write_async (data_sink.cpp)

The kvikio pwrite() call was placed inside the deferred lambda, so the write began only at .get(). The Parquet writer fans out one future per column chunk per rowgroup (write_parquet_data_to_sink) and the ORC writer one per stripe stream (write_data_stream), then both wait in order — serializing every device write in every file written through kvikio. Introduced in #10593 as an opt-in backend (the then-default cufile path initiated eagerly via its thread pool), this became the default path in #12574 and the only path when cufile was removed in #17764.

The fix initiates the write in a lambda capture-initializer (evaluated at call time) and defers only the wait, preserving the original purpose of the wrapper — converting kvikio's future<size_t> to future<void>. Safety: the existing eager stream.synchronize() guarantees source data is ready before pwrite reads it, offsets are reserved at submission time, writes target disjoint ranges (POSIX pwrite semantics, which kvikio already relies on internally), and the device buffers outlive the futures, which are all waited in the same scope.

2. Default datasource::host_read_async (datasource.cpp)

The base-class default wrapped the synchronous host_read() in launch::deferred. The ORC reader fans out one host_read_async per stripe data range (reader_impl_chunking.cu) — so for any datasource that does not override the async API (user-provided datasources; built-in kvikio sources override it), the reads ran serially. The fix submits the read to cudf::detail::host_worker_pool(). Concurrent host_read() calls are already required of every datasource by the multi-threaded Parquet reader, which submits host_read() to this same pool from multiple threads (parquet_io_utils.cpp), and the hierarchical pool design precludes nested-wait deadlocks. The documented contract ("the read operation may be deferred") permits eager execution.

Benchmarks

Measured with isolated harnesses running the verbatim before/after implementations against real files (a 12M-row NYC-taxi table; write side replays the 228 real column-chunk sizes of its Parquet output; read side the 24 x 12 MB stripe layout of its ORC output), with kvikio-equivalent staging (4 MiB task splitting, pinned-bounce D2H) and cudf's hierarchical_thread_pool logic compiled in. Read results checksum-validated against the serial path. 12-core machine, RTX 5070, ext4.

Write sink phase (243 MB, 228 chunks):

kvikio threads before after speedup
4 0.096 s 0.067 s 1.44x
8 0.097 s 0.066 s 1.46x

Read fan-out phase (20 x 2 MiB, 8 pool workers, per-read latency modeling source type):

source latency before after speedup
0 (page cache) 8 ms 7 ms 1.05-1.14x
100 us 11 ms 7 ms 1.63x
500 us 20 ms 7 ms 2.88x
2 ms 52 ms 12 ms 4.42x

Both gains scale with per-write/read latency and operation count; page-cache-backed local I/O is the lower bound. A pattern-level test of the write fix (4 x 100 ms simulated writes) shows 0/4 writes in flight before the wait loop on the old code vs 4/4 on the new (460 ms -> 110 ms). I was unable to run the nvbench suites locally (no libcudf build environment on this machine) — a PARQUET_WRITER_NVBENCH / ORC reader run on RAPIDS hardware to confirm end-to-end numbers would be welcome.

Checklist

  • I am familiar with the Contributing Guidelines.
  • New or existing tests cover these changes (no behavior change intended; existing cpp/tests/io sink/source and Parquet/ORC writer-reader tests exercise both paths).
  • The documentation is up to date with these changes (the host_read_async doc contract already states reads "may be deferred"; eager execution remains within it).

Emillock added 2 commits July 12, 2026 10:10
The kvikio pwrite() call was placed inside a std::launch::deferred
lambda, so the write was not initiated until the returned future was
waited on. The Parquet and ORC writers fan out one future per column
chunk / stripe stream and then wait on the futures in order, which
serialized all device writes: each write began only after the previous
one had fully completed. Introduced in rapidsai#10593 as an opt-in backend,
this became the default path in rapidsai#12574 and the only path in rapidsai#17764.

Initiate the write at call time via a lambda capture-initializer and
defer only the wait, so the wrapper keeps converting kvikio's
future<size_t> to future<void> without delaying the I/O. The eager
stream.synchronize() above already guarantees the source buffer is
ready, offsets are reserved before submission, and the buffers outlive
the futures, so eager initiation is safe.

Benchmark (isolated sink phase, replaying the 228 column-chunk sizes
of a 12M-row NYC-taxi Parquet write, 243 MB total, RTX 5070, ext4):
- 4 kvikio threads: 0.098 s -> 0.081 s per write phase (1.21x)
- 8 kvikio threads: 0.102 s -> 0.080 s per write phase (1.28x)
A pattern-level test with 4 x 100 ms simulated writes shows 0/4 writes
in flight before the wait loop without the fix and 4/4 with it
(460 ms -> 110 ms). Gains scale with per-write latency and chunk
count; page-cache-backed local writes are the lower bound.
The base-class host_read_async implementations wrapped the synchronous
host_read() in std::async(std::launch::deferred), so the read only
executed when the returned future was waited on, in the waiting thread.
The ORC reader fans out one host_read_async per stripe data range and
then waits on the futures in order (reader_impl_chunking.cu), so for
any datasource that does not override host_read_async -- notably
user-provided datasources such as Python/fsspec-backed remote sources --
the reads ran strictly serially, one full read latency after another.

Submit the read to cudf::detail::host_worker_pool() instead, so
fanned-out reads execute in parallel. Concurrent host_read() calls are
already required of every datasource by the multi-threaded Parquet
reader, which submits host_read() to the same pool from multiple
threads (parquet_io_utils.cpp), and the hierarchical pool design rules
out nested-wait deadlocks. Built-in kvikio-backed sources override
host_read_async and keep their native async paths.

Benchmark (ORC-shaped fan-out of 20 x 2 MiB reads from a real Parquet
file through a datasource without a host_read_async override, with
cudf's hierarchical_thread_pool logic compiled into the harness,
8 workers, 12-core machine; per-read latency injected to model the
source type):
- local page cache (no latency):  8 ms -> 7 ms  (1.05-1.14x)
- 100 us/read (Python overhead):  11 ms -> 7 ms (1.63x)
- 500 us/read (NFS-like):         20 ms -> 7 ms (2.88x)
- 2 ms/read (remote-like):        52 ms -> 12 ms (4.42x)
Read results validated by checksum against the serial path.
@Emillock Emillock requested a review from a team as a code owner July 12, 2026 12:51
@copy-pr-bot

copy-pr-bot Bot commented Jul 12, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@github-actions github-actions Bot added the libcudf Affects libcudf (C++/CUDA) code. label Jul 12, 2026
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 2218ac23-26a8-4ea9-b8e4-8a4ce3faca29

📥 Commits

Reviewing files that changed from the base of the PR and between 3aa6568 and 38087d9.

📒 Files selected for processing (2)
  • cpp/src/io/utilities/data_sink.cpp
  • cpp/src/io/utilities/datasource.cpp

📝 Walkthrough

Summary by CodeRabbit

  • Performance
    • Improved asynchronous data reads so multiple host-based reads can execute in parallel.
    • I/O operations now begin immediately while result waiting remains deferred, improving responsiveness for asynchronous writes.

Walkthrough

Changes

The PR updates asynchronous I/O scheduling: file sink writes begin before deferred waiting, while datasource host reads run through the shared host worker pool.

Asynchronous I/O behavior

Layer / File(s) Summary
File sink write future capture
cpp/src/io/utilities/data_sink.cpp
file_sink::device_write_async captures the KvikIO pwrite() future immediately and defers only get().
Host read worker-pool scheduling
cpp/src/io/utilities/datasource.cpp
Both host_read_async overloads submit synchronous reads to the shared host worker pool and return the resulting futures.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: eager async I/O for device writes and default host reads.
Description check ✅ Passed The description directly explains the same deferred-I/O fixes and is clearly related to the changeset.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

libcudf Affects libcudf (C++/CUDA) code.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant