Skip to content

Add KvikIO to Improve I/O Throughput - #2257

Open
julianmi wants to merge 56 commits into
NVIDIA:mainfrom
julianmi:ace-kvikio
Open

Add KvikIO to Improve I/O Throughput#2257
julianmi wants to merge 56 commits into
NVIDIA:mainfrom
julianmi:ace-kvikio

Conversation

@julianmi

@julianmi julianmi commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Motivation:

  • Performance: CAGRA-ACE uses intermediate files when the dataset, graph, and intermediate objects do not fit into memory. Using direct file I/O and GPUDirect Storage (GDS) improves I/O throughput. I observed a 1.17x end-to-end speedup for BIGANN-1B on L40S.
  • Reduce memory pressure: IVF-PQ uses pinned memory in k-means through raft::sample_rows in each partition. ACE uses large files leading to high page-cache pressure and memory fragmentation. Direct file I/O skips the page cache and resolves the potentially failing pinned memory allocations.
  • Disk-heavy serialize and deserialize routines are affected similarly.

Changes:

  • Add KvikIO-backed bulk I/O for ACE/HNSW disk paths.
  • Use KvikIO futures for concurrent reads and explicit async result handling.
  • Align ACE .npy data regions for direct I/O and GDS-friendly transfers.
  • Harden file I/O helpers with better validation, ownership, and short-read/write checks.
  • Support for GDS for CAGRA and IVF-PQ serialize and deserialize routines. The std::istream/std::ostream APIs are unchanged.

Package impact:

  • This PR adds one direct runtime dependency: KvikIO (libkvikio for conda and libkvikio-cu12/libkvikio-cu13 for wheels).
  • KvikIO is approximately 0.5 MiB compressed on conda and 2.6-2.8 MiB as a wheel. Wheel installations add cuda-pathfinder and cuFile, totaling approximately 3.7-4.2 MiB of additional downloads.
  • A clean x86_64 conda solve adds 14 packages totaling approximately 16.1 MiB compressed, primarily ICU (12.1 MiB) through KvikIO’s libcurl dependency chain.
  • Existing RAFT/RMM and rapids-logger packages are reused.

@coderabbitai

coderabbitai Bot commented Jun 23, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

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: 58839c96-47ac-458f-8930-fe78ab0acb37

📥 Commits

Reviewing files that changed from the base of the PR and between 66f1fe1 and 46b5cf2.

📒 Files selected for processing (1)
  • c/CMakeLists.txt
🚧 Files skipped from review as they are similar to previous changes (1)
  • c/CMakeLists.txt

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added KvikIO-backed large-file I/O with improved aligned NumPy on-disk layout and disk-oriented reads/writes.
    • Updated disk-mode CAGRA/HNSW loading to use KvikIO file handles for bulk transfers, with direct-to-device paths when possible.
  • Bug Fixes
    • Improved validation for file/NumPy header integrity, alignment, and full read/write completion.
  • Tests
    • Added end-to-end tests covering large-file round-trips and KvikIO output correctness, including failure cases for replaced/removed paths.
  • Chores
    • Updated Conda/Python/CI build dependencies and release tooling for KvikIO (including wheel auditwheel exclusions and static build wiring).

Walkthrough

Integrates KvikIO into cuVS build, runtime, file I/O, neighbor disk workflows, tests, and packaging. Large-file I/O now uses kvikio handles, CAGRA and HNSW disk paths use kvikio-based reads and writes, and dependency declarations are updated across CMake, conda, Python, and release tooling.

Changes

KvikIO GPU Direct Storage Integration

Layer / File(s) Summary
KvikIO CMake dependency and target linking
cpp/cmake/thirdparty/get_kvikio.cmake, cpp/CMakeLists.txt, c/CMakeLists.txt
Adds KvikIO fetch/configuration logic and links kvikio::kvikio into cuVS CMake targets, including the C library target when the local KvikIO target exists.
file_io.hpp alignment and stream declarations
cpp/include/cuvs/util/file_io.hpp
Adds NumPy alignment helpers, refactors fd_streambuf lifecycle handling, updates create_numpy_file layout logic, and expands the file I/O stream declarations and documentation.
kvikio-backed bulk file I/O
cpp/src/util/file_io.cpp
Replaces chunked POSIX large-file reads and writes with kvikio FileHandle calls, adds file-identity validation helpers, and implements kvikio_ofstream buffering and flush behavior.
CAGRA disk partition loading
cpp/src/neighbors/detail/cagra/cagra_build.cuh
Changes ACE partition loading to use pre-opened file descriptors and kvikio pread futures, then updates disk-mode sub-index construction to use a direct-device read path with host fallback and timing logs.
HNSW disk serialization and reads
cpp/src/neighbors/detail/hnsw.hpp
Opens kvikio FileHandle objects for HNSW disk inputs, replaces batch reads with concurrent kvikio pread calls and validation, and writes spill output through kvikio_ofstream.
file_io test coverage
cpp/tests/util/file_io_test.cpp, cpp/tests/CMakeLists.txt
Adds tests for aligned NumPy file creation, host and device round-trips, kvikio_ofstream behavior, invalid arguments, replaced-path handling, and registers the new test target in CMake.
Packaging and runtime dependency wiring
conda/environments/all_cuda-*, conda/recipes/libcuvs/recipe.yaml, dependencies.yaml, python/libcuvs/pyproject.toml, ci/release/update-version.sh, python/libcuvs/libcuvs/load.py, ci/build_wheel.sh
Adds libkvikio to conda environments, libcuvs recipe requirements, dependency matrix groups, Python build/runtime dependencies, version update scripts, wheel repair exclusions, and Python load order.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested labels

C++

Suggested reviewers

  • cjnolet
  • robertmaynard
  • jameslamb
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 22.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly matches the main change: adding KvikIO support for CAGRA-ACE disk I/O paths.
Description check ✅ Passed The description is directly related to the changeset and summarizes the KvikIO, GDS, and I/O hardening work.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cpp/src/neighbors/detail/hnsw.hpp`:
- Around line 840-846: The code calls get() on graph_future, dataset_future, and
label_future sequentially, but if graph_future.get() throws an exception, the
subsequent calls to dataset_future.get() and label_future.get() are never
executed, leaving those async operations unresolved in the background. To fix
this, ensure all three futures are awaited before any exception can propagate by
either using try-catch blocks to drain all futures before re-throwing, or by
restructuring the code to call all three get() methods within a context that
guarantees all are executed (such as a scoped guard or utility function that
ensures all futures are consumed).

In `@cpp/src/util/file_io.cpp`:
- Around line 26-38: The bulk I/O operations are reopening files by path using
kvikio::FileHandle instead of preserving the already-open file descriptor, which
creates a TOCTOU race condition where the path could be replaced between open
and I/O. Fix this by either using the existing file descriptor directly instead
of reopening via path, or by verifying that the inode and device have not
changed before proceeding with the I/O operation. This change breaks the API
contract for descriptor-only usage (where get_path() is unavailable), so add
proper deprecation warnings at the affected call sites (around line 26-38 and
line 49-61 in file_io.cpp) and update the migration guide to document the
required changes for users relying on descriptor-only semantics.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 300f4901-414b-474c-924a-8a674e657be5

📥 Commits

Reviewing files that changed from the base of the PR and between 71306ec and 18d59e5.

📒 Files selected for processing (24)
  • conda/environments/all_cuda-129_arch-aarch64.yaml
  • conda/environments/all_cuda-129_arch-x86_64.yaml
  • conda/environments/all_cuda-133_arch-aarch64.yaml
  • conda/environments/all_cuda-133_arch-x86_64.yaml
  • conda/environments/bench_ann_cuda-129_arch-x86_64.yaml
  • conda/environments/bench_ann_cuda-133_arch-aarch64.yaml
  • conda/environments/bench_ann_cuda-133_arch-x86_64.yaml
  • conda/environments/go_cuda-129_arch-x86_64.yaml
  • conda/environments/go_cuda-133_arch-aarch64.yaml
  • conda/environments/go_cuda-133_arch-x86_64.yaml
  • conda/environments/rust_cuda-129_arch-x86_64.yaml
  • conda/environments/rust_cuda-133_arch-aarch64.yaml
  • conda/environments/rust_cuda-133_arch-x86_64.yaml
  • conda/recipes/libcuvs/recipe.yaml
  • cpp/CMakeLists.txt
  • cpp/cmake/thirdparty/get_kvikio.cmake
  • cpp/include/cuvs/util/file_io.hpp
  • cpp/src/neighbors/detail/cagra/cagra_build.cuh
  • cpp/src/neighbors/detail/hnsw.hpp
  • cpp/src/util/file_io.cpp
  • cpp/tests/CMakeLists.txt
  • cpp/tests/util/file_io_test.cpp
  • dependencies.yaml
  • python/libcuvs/pyproject.toml

Comment thread cpp/src/neighbors/detail/hnsw.hpp Outdated
Comment thread cpp/src/util/file_io.cpp

@jameslamb jameslamb left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left a few small initial comments from a packaging-codeowners perspective. In general, not opposed to having kvikio be a dependency of cuvs if cuVS maintainers are ok with it.

I'd be happy to review again once cuVS maintainers have more thoroughly gone through this and given their opinions.

Comment thread conda/recipes/libcuvs/recipe.yaml Outdated
Comment thread python/libcuvs/pyproject.toml Outdated
Comment thread python/libcuvs/pyproject.toml Outdated
@cjnolet cjnolet added improvement Improves an existing functionality non-breaking Introduces a non-breaking change labels Jun 23, 2026
@cjnolet cjnolet moved this to In Progress in Unstructured Data Processing Jun 23, 2026
@julianmi
julianmi requested review from a team as code owners June 24, 2026 08:30
@julianmi
julianmi requested a review from bdice June 24, 2026 08:30
@copy-pr-bot

copy-pr-bot Bot commented Jun 25, 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.

@julianmi
julianmi requested review from a team as code owners June 29, 2026 06:43
@julianmi
julianmi requested a review from a team as a code owner July 22, 2026 07:09
@julianmi
julianmi requested a review from huuanhhuyn July 23, 2026 07:09

@dantegd dantegd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The integration looks well designed, the pr looks great overall.

Comment thread cpp/CMakeLists.txt Outdated
$<$<BOOL:${CUVS_NVTX}>:CUDA::nvtx3>
$<COMPILE_ONLY:nvidia::cutlass::cutlass>
$<COMPILE_ONLY:cuco::cuco>
$<BUILD_LOCAL_INTERFACE:kvikio::kvikio>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we preserve KvikIO in the installed static target’s transitive link interface?

cuvs_static now contains references to compiled kvikio::* symbols, but $<BUILD_LOCAL_INTERFACE:...> evaluates to empty when the target is consumed from another build system. Installing libkvikio supplies the DSO, but it does not add it to a downstream executable’s final link line, so consumers of the installed cuvs::cuvs_static target can get unresolved symbols when an affected archive member is selected, right?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, thanks. cuvs_static now retains KvikIO as a link-only dependency, and KvikIO is registered in the static build/install export sets. The shared target continues to keep it private.

Comment thread cpp/src/util/file_io.cpp Outdated
RAFT_EXPECTS(dest_ptr != nullptr, "Destination pointer must not be nullptr");
RAFT_EXPECTS(fd.is_valid(), "File descriptor must be valid");
const std::string path = fd.get_path();
RAFT_EXPECTS(!path.empty(), "File descriptor must have an associated path for kvikio I/O");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inode/device validation looks good for the path replacement race, but should we also retain the previous POSIX pread path for pathless descriptors, and likewise retain pwrite in the write helper?

file_descriptor(int) and these bulk helpers are installed public APIs if I'm not mistaken, and descriptor only callers worked before this PR and the new test at file_io_test.cpp now checks that both operations throw. That breaks raw external FDs and memfd-style descriptors in a PR labeled non-breaking, or we can make it breaking of course. What do you think?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that might be helpful in the future. I've added a POSIX pread/pwrite path for pathless descriptors. I've also extended the tests.

Comment thread cpp/include/cuvs/util/file_io.hpp Outdated
buffer_(std::move(other.buffer_)),
buffer_size_(std::exchange(other.buffer_size_, 0))
{
setg(buffer_.get(), buffer_.get(), buffer_.get());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to preserve the unread portion of the buffer when moving the stream. After a buffered read, fd_ has already advanced past the full 8 KiB buffer, so resetting the get area here causes any unread bytes to be skipped. It looks like move assignment has the same issue. Would it make sense to add a test that reads a prefix, moves the stream, and then verifies the remaining contents?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. Move construction and assignment now transfer the underlying std::streambuf get area, preserving unread buffered bytes. Both operations have regression coverage.

"ACE: partition %lu did not fit in device memory for a direct (GDS) read; "
"falling back to a host read",
partition_id);
} catch (const raft::logic_error&) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we narrow this catch, or at least log the original exception?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback now catches only std::bad_alloc, which includes RMM allocation failures, and logs the original exception.

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

Labels

improvement Improves an existing functionality non-breaking Introduces a non-breaking change

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

6 participants