Add I/O statistics: totals of what a run read and wrote - #1036
Merged
Conversation
madsbk
force-pushed
the
summary-monitor
branch
3 times, most recently
from
August 19, 2026 06:42
a28c446 to
2a27152
Compare
madsbk
commented
Aug 19, 2026
|
|
||
| using namespace std; | ||
|
|
||
| class Timer { |
Member
Author
There was a problem hiding this comment.
We can now use the new Monitor instead of this homemade Timer
madsbk
commented
Aug 19, 2026
| @@ -0,0 +1,78 @@ | |||
| /* | |||
Member
Author
There was a problem hiding this comment.
This file and its .cpp is mostly a copy-paste from https://github.com/rapidsai/rapidsmpf/blob/main/cpp/include/rapidsmpf/utils/string.hpp#L73
madsbk
force-pushed
the
summary-monitor
branch
5 times, most recently
from
August 19, 2026 11:08
da11e39 to
b0a5e4e
Compare
madsbk
force-pushed
the
summary-monitor
branch
from
August 19, 2026 12:12
b0a5e4e to
dd42b89
Compare
madsbk
marked this pull request as ready for review
August 19, 2026 12:41
bdice
approved these changes
Aug 21, 2026
kingcrimsontianyu
approved these changes
Aug 21, 2026
kingcrimsontianyu
left a comment
Contributor
There was a problem hiding this comment.
Great framework. This opens doors to many statistics we want to collect such as p50/p90/ p99 latency and probably TTFB (time to first byte).
Member
Author
|
/merge |
rapids-bot Bot
pushed a commit
to NVIDIA/cudf
that referenced
this pull request
Aug 25, 2026
This PR enables KvikIO statistics on every rank and gathers them on the client. Depends on [rapidsai/kvikio#1036](rapidsai/kvikio#1036), which adds the monitor that does the counting. Each rank turns on counting when the engine is configured with `statistics=True`, and `StreamingEngine.gather_io_summary()` brings back one `kvikio.Summary` per rank, keyed by rank index. ```python options = StreamingOptions(statistics=True) with SPMDEngine( rapidsmpf_options=options.to_rapidsmpf_options(), executor_options=options.to_executor_options(), engine_options=options.to_engine_options(), ) as engine: pl.scan_parquet(path).select(pl.col("a").sum()).collect(engine=engine) for rank, summary in engine.gather_io_summary().items(): print(f"--- rank {rank} ---") print(summary) ``` ### What a rank reports KvikIO renders the report, so this PR formats nothing. A single-rank run of a parquet scan: ``` KvikIO I/O summary wall time 122.55 ms busy time 18.40 ms (15.02 % of the wall time) busy bandwidth 66.44 MB/s operations 12 (12 read, 0 write) mean duration 3.90 ms bytes 1.17 MiB of 1.17 MiB requested (1.17 MiB read, 0 B written) errors 0 backend POSIX 1.17 MiB in 12 ops, 46.83 ms, 26.11 MB/s backend GDS unused backend MMAP unused backend REMOTE_HTTP unused backend REMOTE_HDFS unused ``` **Busy time** counts only the stretches with a read in flight, so **busy bandwidth** measures the storage rather than the query: this scan spent 15 % of its wall time reading, and dividing by the whole span would have reported it at a tenth of the rate the disk was really giving. ### In the benchmarks The PDS runners record the per-rank summaries on each iteration's record when `--rapidsmpf-statistics` is passed, so I/O stays queryable across a whole sweep rather than being printed once and lost. `print_results_file.py` (new file) reads a results file back and prints it, since nothing existed that could. Timings and I/O side by side, one row per rank per iteration: ``` $ python -m cudf_polars.streaming.benchmarks.print_results_file pdsh-output.json ============================================================================== run : b6274527-a5d4-4791-9aa0-2a7a4ee3d307 (2026-08-20T08:35:11+00:00) engine : cudf-polars frontend=ray dataset : /datasets/tpch-rs/scale-10-duckdb/ scale=10 workers : 2 iterations=3 ============================================================================== Timings query iters min max mean 1 3 0.1364s 0.3168s 0.2166s 3 3 0.1616s 0.2154s 0.1824s total 0.3990s I/O per rank query iter rank ops read busy busy% bandwidth backends 1 0 0 796 312.09 MiB 64.0ms 9.8% 5.11GB/s POSIX 1 0 1 793 310.21 MiB 75.4ms 11.5% 4.31GB/s POSIX 1 1 0 788 306.54 MiB 40.1ms 29.1% 8.01GB/s POSIX 1 1 1 793 310.21 MiB 54.3ms 39.5% 5.99GB/s POSIX 1 2 0 788 306.54 MiB 37.3ms 18.9% 8.61GB/s POSIX 1 2 1 793 310.21 MiB 49.7ms 25.1% 6.54GB/s POSIX 3 0 0 1485 553.21 MiB 91.2ms 42.0% 6.36GB/s POSIX 3 0 1 793 339.17 MiB 82.1ms 37.8% 4.33GB/s POSIX 3 1 0 1474 549.74 MiB 68.6ms 40.0% 8.41GB/s POSIX 3 1 1 793 339.17 MiB 59.5ms 34.7% 5.98GB/s POSIX 3 2 0 1474 549.74 MiB 67.1ms 41.1% 8.59GB/s POSIX 3 2 1 793 339.17 MiB 61.3ms 37.6% 5.80GB/s POSIX widest read skew: 1.63x (query 3, iteration 0) ``` ### Caveats worth knowing - **Counting is per process.** With Ray and Dask each rank has a process to itself, so a summary covers only cudf-polars. With SPMD cudf-polars shares your script's process, so KvikIO operations your own code performs are counted too. - **Not all I/O is observed.** Per KvikIO's `Monitor` docs the cuFile async API on a working GDS system and the batch API report nothing, and anything cudf-polars reads outside KvikIO is invisible. - **Timestamps are per rank.** Each monitor takes its own clock anchor, so across hosts the start and end times carry whatever NTP skew exists. Ratios within a rank, `busy_fraction` and the bandwidths, are unaffected. Authors: - Mads R. B. Kristensen (https://github.com/madsbk) Approvers: - Peter Andreas Entschev (https://github.com/pentschev) - Tom Augspurger (https://github.com/TomAugspurger) URL: #23738
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Builds on the observation facility from #1033. That PR gives a callback per user-facing I/O call, and this one gives the answer most people actually want from it, which is what a run did in total.
SummaryMonitorregisters itself on construction and accumulates while it exists.Summarycarries the operations, the bytes, the errors, and the time at least one operation was in flight, which is what distinguishes an I/O-bound run from a compute-bound one.Using it
In C++ the constructor takes an optional callback, which runs on destruction, so a program can report its own I/O without touching the code that performs it.
An interval is the difference of two readings, so periodic reporting takes one reading per tick and differences it against the last. Taking the reading and the interval separately would leave a gap that an operation could fall into twice.
The report
Printing a summary, or
report(), gives a report meant for a person, always the same shape so two runs can be compared line by line. This ispython/kvikio/examples/hello_world.py:Five operations for one write and four reads, however many reads KvikIO issued underneath, since the observations are logical.
Busy time is the union of the operations' spans, so overlapping work counts once and the gaps between calls count as idle. Busy bandwidth divides by that rather than by the wall time, so a program that reads for 10 ms and then computes for 90 ms is not reported as ten times slower than its storage really is. The backend rows are the only place the report says whether a read reached cuFile or fell back to POSIX, which compatibility mode decides per call.
Overhead
On my local workstation, 32 cores, medians over two million observed operations, so an order of magnitude rather than a specification.
SummaryMonitorThis includes everything, both the observation facility from #1033 stamping and dispatching each operation, and the accumulation this PR adds on top. So a monitor costs about 2 % of a 4 KiB
pread()and 0.25 % of a 1 MiB one. Registering none is the 4.8 ns row, one relaxed atomic load per operation.Eight threads doing nothing but emitting observations cost 320 ns each, every operation taking the monitor's lock to add itself to the totals. Real work in between makes it disappear.
Follow-ups
Summary.ObservationKind::PHYSICAL. Everything here is logical, one user-facing call being one operation. When physical operations arrive,Summarystays one type rather than splitting in two, since the fields mean the same thing at either level.