Skip to content

Optimize transfer/compute overlap in out-of-core KMeans - #2538

Open
viclafargue wants to merge 7 commits into
NVIDIA:mainfrom
viclafargue:ooc-kmeans-overlap
Open

Optimize transfer/compute overlap in out-of-core KMeans#2538
viclafargue wants to merge 7 commits into
NVIDIA:mainfrom
viclafargue:ooc-kmeans-overlap

Conversation

@viclafargue

@viclafargue viclafargue commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR improves data-transfer and compute overlap for host-resident out-of-core KMeans using a cyclic two-buffer pipeline.

  • Batch 1 follows batch 0 on the copy stream while batch 0 compute starts as soon as its transfer completes.
  • Subsequent transfers overlap computation of the current batch.
  • Buffer recycling continues across iteration boundaries, allowing batch 0 of the next pass to be prefetched during the previous pass.
  • Device-resident inputs remain zero-copy.

Implementation

  • Adds a private KMeans batch loader with explicit staged, acquired, and reusable buffer states.
  • Uses CUDA events so compute waits for H2D completion and a buffer cannot be overwritten until all of its consumers have been submitted.
  • Applies the same dependency-driven scheduling to every batch, without special APIs or states for the first two batches.
  • Uses persistent device scratch to avoid per-batch deallocation and its potential device-wide synchronization.
  • Computes final inertia through the regular batched assignment and reduction pipeline, accumulates it on device, and copies only the final result to host.
  • Leaves the shared ANN batch iterator unchanged.

Benchmark under similar configuration

10 GiB pinned-host FP32 dataset (10,485,760 × 256), 2,560 clusters, three iterations, ten 1 GiB out-of-core batches, and 131,072-sample assignment tiles.

Metric main PR
Median runtime 1.7795 s 0.7962 s
Speedup 1.00× 2.23×
Effective bulk throughput 22.48 GiB/s 50.24 GiB/s
Profiled GPU span 1773.12 ms 785.65 ms
H2D time 752.29 ms 752.38 ms
Kernel time 1003.08 ms 760.97 ms
H2D/kernel overlap 0.00 ms 727.91 ms
H2D overlapped by kernels 0.0% 96.75%
Kernels overlapped by H2D 0.0% 95.66%

Profile

Main branch :
profile_main

This PR :
profile_pr

This PR (multi-GPU) :
multi_gpu_profile

@copy-pr-bot

copy-pr-bot Bot commented Sep 1, 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.

@viclafargue viclafargue added improvement Improves an existing functionality non-breaking Introduces a non-breaking change labels Sep 1, 2026
@viclafargue
viclafargue marked this pull request as ready for review September 1, 2026 16:37
@viclafargue
viclafargue requested a review from a team as a code owner September 1, 2026 16:37
@viclafargue

Copy link
Copy Markdown
Contributor Author

/ok to test 4b8a5c5

Comment thread cpp/src/cluster/detail/kmeans.cuh Outdated
Comment thread cpp/src/cluster/detail/kmeans_batch_loader.cuh
Comment thread cpp/src/cluster/detail/kmeans_batch_loader.cuh Outdated
@viclafargue

Copy link
Copy Markdown
Contributor Author

/ok to test 9dd96bf

* @param[in] num_streams Number of CUDA streams in each device's pool
* @return cuvsError_t
*/
CUVS_EXPORT cuvsError_t cuvsMultiGpuResourcesSetStreamPool(cuvsResources_t res,

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.

again -- I am not entirely opposed to this because I see how it makes things convenient. But why are we providing an API for this specifically? SNMG resources or raft resources with nccl comms are not special in the way stream pools should be set, right? In general I think such APIs to directly modify raft resources, should be in raft and not cuvs. We can add them here and then they have to stick around until we are allowed to break the ABI.

auto data_batches = cuvs::spatial::knn::detail::utils::make_batch_load_iterator<DataT>(
handle, X.data_handle(), n_samples, n_features, device_buffer_samples, stream);
auto batch_mr = data_on_device ? raft::resource::get_workspace_resource_ref(handle)
: raft::resource::get_large_workspace_resource_ref(handle);

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.

if the data is already device accessible, why are we even using the workspace?

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.

Furthermore, if the large workspace is managed, the transfer speed is slightly slower (I found 55 GB/s versus 48 GB/s). I agree that the batchsize is not bounded here (it can be as large as the dataset) but I would argue for falling back to the large workspace only at the breaking point where allocating from the regular workspace is not possible. That calculation can get complicated to account for whether or not weights are present, so I'll tag @achirkin for some ideas.

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.

Actually since this is unbounded (only bounded by dataset size) lets just stick to large_mr @viclafargue. Since compute will overlap, its not a big deal.

};

bool input_pipeline_started = false;
auto start_input_pipeline = [&] {

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.

Can we make this function part of the batch loader class, so starting the pipeline explicitly becomes part of the batch loader? It makes it more readable. Overall there has been an increase in these lambdas all over the codebase (not the most readable I think)

if (!batches_.empty()) {
RAFT_CUDA_TRY_NO_THROW(cudaStreamSynchronize(raft::resource::get_cuda_stream(*res_)));
}
RAFT_CUDA_TRY_NO_THROW(cudaStreamSynchronize(copy_stream_));

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.

Please use raft APIs for these. I tend to find these, but these should not need to be pointed out anymore. You can make rules for agents to use raft APIs wherever possible.

* Unlike minClusterAndDistanceCompute, this path does not calculate cluster labels.
*/
template <typename DataT, typename IndexT>
void cluster_cost(

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.

why do we have this new function? Its not used anywhere, right?

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

None yet

Development

Successfully merging this pull request may close these issues.

3 participants