66
77#include " ../../core/nvtx.hpp"
88#include " ../../neighbors/detail/ann_utils.cuh"
9+ #include " kmeans_batch_loader.cuh"
910#include " kmeans_common.cuh"
1011
1112#include < cuvs/cluster/kmeans.hpp>
2425#include < raft/core/pinned_mdarray.hpp>
2526#include < raft/core/pinned_mdspan.hpp>
2627#include < raft/core/resource/cuda_stream.hpp>
28+ #include < raft/core/resource/device_memory_resource.hpp>
2729#include < raft/core/resource/thrust_policy.hpp>
2830#include < raft/core/resources.hpp>
2931#include < raft/linalg/map.cuh>
@@ -56,6 +58,43 @@ namespace cuvs::cluster::kmeans::detail {
5658
5759static const std::string CUVS_NAME = " cuvs" ;
5860
61+ template <typename DataT, typename IndexT>
62+ void cluster_cost (
63+ raft::resources const & handle,
64+ raft::device_matrix_view<const DataT, IndexT> X,
65+ raft::device_matrix_view<const DataT, IndexT> centroids,
66+ raft::device_scalar_view<DataT> cost,
67+ raft::device_vector_view<DataT, IndexT> norms,
68+ raft::device_vector_view<DataT, IndexT> distances,
69+ rmm::device_uvector<DataT>& distance_buffer,
70+ rmm::device_uvector<char >& workspace,
71+ std::optional<raft::device_vector_view<const DataT, IndexT>> sample_weight = std::nullopt )
72+ {
73+ auto n_samples = static_cast <IndexT>(X.extent (0 ));
74+ norms = raft::make_device_vector_view<DataT, IndexT>(norms.data_handle (), n_samples);
75+ distances = raft::make_device_vector_view<DataT, IndexT>(distances.data_handle (), n_samples);
76+
77+ raft::linalg::norm<raft::linalg::L2Norm, raft::Apply::ALONG_ROWS >(handle, X, norms);
78+ minClusterDistanceCompute<DataT, IndexT>(
79+ handle,
80+ X,
81+ raft::make_device_matrix_view<DataT, IndexT>(
82+ const_cast <DataT*>(centroids.data_handle ()), centroids.extent (0 ), centroids.extent (1 )),
83+ distances,
84+ norms,
85+ distance_buffer,
86+ cuvs::distance::DistanceType::L2Expanded,
87+ n_samples,
88+ centroids.extent (0 ),
89+ workspace);
90+
91+ if (sample_weight.has_value ()) {
92+ raft::linalg::map (
93+ handle, distances, raft::mul_op{}, raft::make_const_mdspan (distances), sample_weight.value ());
94+ }
95+ computeClusterCost (handle, distances, workspace, cost, raft::identity_op{}, raft::add_op{});
96+ }
97+
5998// =========================================================
6099// Init functions
61100// =========================================================
@@ -686,25 +725,32 @@ void kmeans_fit(
686725
687726 auto minClusterAndDistance = raft::make_device_vector<raft::KeyValuePair<IndexT, DataT>, IndexT>(
688727 handle, device_buffer_samples);
689- auto L2NormBatch = raft::make_device_vector<DataT, IndexT>(handle, device_buffer_samples);
728+ auto minClusterDistance = raft::make_device_vector<DataT, IndexT>(handle, device_buffer_samples);
729+ const IndexT l2_norm_size = data_on_device ? n_samples : device_buffer_samples;
730+ auto L2NormBatch = raft::make_device_vector<DataT, IndexT>(handle, l2_norm_size);
690731 auto batch_weights_buf = raft::make_device_vector<DataT, IndexT>(handle, device_buffer_samples);
691732 rmm::device_uvector<DataT> L2NormBuf_OR_DistBuf (0 , stream);
692733
693734 auto centroid_sums = raft::make_device_matrix<DataT, IndexT>(handle, n_clusters, n_features);
694735 auto weight_per_cluster = raft::make_device_vector<DataT, IndexT>(handle, n_clusters);
695736 auto clustering_cost = raft::make_device_scalar<DataT>(handle, DataT{0 });
696-
737+ auto batch_cost = raft::make_device_scalar<DataT>(handle, DataT{ 0 });
697738 rmm::device_uvector<char > batch_workspace (device_buffer_samples, stream);
698739
699- auto data_batches = cuvs::spatial::knn::detail::utils::make_batch_load_iterator<DataT>(
700- handle, X.data_handle (), n_samples, n_features, device_buffer_samples, stream);
740+ auto batch_mr = raft::resource::get_large_workspace_resource_ref (handle);
741+ auto batch_copy_stream = cuvs::spatial::knn::detail::utils::get_prefetch_stream (handle).first ;
742+
743+ kmeans_batch_loader<DataT, IndexT, data_on_device> data_batches (
744+ handle, X, device_buffer_samples, batch_copy_stream, batch_mr);
701745 // Host-path weight batches: only materialized when weights are provided and
702746 // the data resides on host
703- std::optional<cuvs::spatial::knn::detail::utils::batch_load_iterator_dyn <DataT>> weight_batches;
747+ std::optional<kmeans_batch_loader <DataT, IndexT, false >> weight_batches;
704748 if constexpr (!data_on_device) {
705749 if (weight_ptr != nullptr ) {
706- weight_batches = cuvs::spatial::knn::detail::utils::make_batch_load_iterator<DataT>(
707- handle, weight_ptr, n_samples, IndexT{1 }, device_buffer_samples, stream);
750+ auto weight_view =
751+ raft::make_host_matrix_view<const DataT, IndexT>(weight_ptr, n_samples, IndexT{1 });
752+ weight_batches.emplace (
753+ handle, weight_view, device_buffer_samples, batch_copy_stream, batch_mr);
708754 } else {
709755 raft::matrix::fill (handle, batch_weights_buf.view (), DataT{1 });
710756 }
@@ -758,6 +804,11 @@ void kmeans_fit(
758804 }
759805 };
760806
807+ auto prefetch_batch = [&](std::size_t batch_pos) {
808+ (void )data_batches.prefetch (batch_pos);
809+ if (weight_batches.has_value ()) { (void )weight_batches->prefetch (batch_pos); }
810+ };
811+
761812 RAFT_LOG_DEBUG (
762813 " KMeans.fit: n_samples=%zu, n_features=%zu, n_clusters=%d, device_buffer_samples=%zu" ,
763814 static_cast <size_t >(n_samples),
@@ -767,10 +818,6 @@ void kmeans_fit(
767818
768819 bool need_compute_norms = metric == cuvs::distance::DistanceType::L2Expanded ||
769820 metric == cuvs::distance::DistanceType::L2SqrtExpanded;
770- auto h_norm_cache = raft::make_pinned_vector<DataT, IndexT>(
771- handle, (need_compute_norms && !data_on_device) ? n_samples : 0 );
772- bool norms_cached = false ;
773-
774821 auto compute_batch_norms = [&](const DataT* batch_ptr, IndexT batch_size) {
775822 auto batch_view =
776823 raft::make_device_matrix_view<const DataT, IndexT>(batch_ptr, batch_size, n_features);
@@ -830,53 +877,45 @@ void kmeans_fit(
830877 raft::matrix::fill (handle, weight_per_cluster.view (), DataT{0 });
831878 raft::matrix::fill (handle, clustering_cost.view (), DataT{0 });
832879
880+ // Complete iteration setup before starting the cold pipeline, so no potentially blocking
881+ // CUDA setup remains between the first transfer and its first consumer.
882+ data_batches.start ();
883+ if (weight_batches.has_value ()) { weight_batches->start (); }
884+
833885 auto centroids_const = raft::make_device_matrix_view<const DataT, IndexT>(
834886 cur_centroids_ptr, n_clusters, n_features);
835887 auto new_centroids_view =
836888 raft::make_device_matrix_view<DataT, IndexT>(new_centroids_ptr, n_clusters, n_features);
837889
838- data_batches.reset ();
839- using wt_iter_t = cuvs::spatial::knn::detail::utils::batch_load_iterator_dyn<DataT>;
840- std::optional<wt_iter_t > wt_it;
841- if (weight_batches.has_value ()) {
842- weight_batches->reset ();
843- wt_it = weight_batches->begin ();
844- }
845- for (const auto & data_batch : data_batches) {
846- IndexT cur_batch_size = static_cast <IndexT>(data_batch.size ());
847- const DataT* wt_data = nullptr ;
848- if (wt_it.has_value ()) {
849- wt_data = (**wt_it).data ();
850- ++(*wt_it);
890+ for (std::size_t batch_pos = 0 ; batch_pos < data_batches.num_batches (); ++batch_pos) {
891+ const auto data_batch = data_batches.acquire (batch_pos);
892+ std::optional<kmeans_batch<DataT>> weight_batch;
893+ if (weight_batches.has_value ()) {
894+ weight_batch.emplace (weight_batches->acquire (batch_pos));
851895 }
852896
897+ IndexT cur_batch_size = static_cast <IndexT>(data_batch.size ());
898+ const DataT* wt_data = weight_batch.has_value () ? weight_batch->data () : nullptr ;
899+
853900 auto batch_data_view = raft::make_device_matrix_view<const DataT, IndexT>(
854901 data_batch.data (), cur_batch_size, n_features);
855902 auto batch_weights_view =
856903 cur_batch_weights (static_cast <IndexT>(data_batch.offset ()), wt_data, cur_batch_size);
857-
858904 auto minCAD_view = raft::make_device_vector_view<raft::KeyValuePair<IndexT, DataT>, IndexT>(
859905 minClusterAndDistance.data_handle (), cur_batch_size);
860906
861907 if constexpr (!data_on_device) {
862- if (need_compute_norms) {
863- if (!norms_cached) {
864- compute_batch_norms (data_batch.data (), cur_batch_size);
865- raft::copy (h_norm_cache.data_handle () + data_batch.offset (),
866- L2NormBatch.data_handle (),
867- cur_batch_size,
868- stream);
869- } else {
870- raft::copy (L2NormBatch.data_handle (),
871- h_norm_cache.data_handle () + data_batch.offset (),
872- cur_batch_size,
873- stream);
874- }
875- }
908+ if (need_compute_norms) { compute_batch_norms (data_batch.data (), cur_batch_size); }
876909 }
877910
911+ // An already-full pipeline makes this a no-op. During cold fill, submit the first real
912+ // consumer before making the second H2D eligible, so CUDA can dispatch both at batch-ready.
913+ prefetch_batch ((batch_pos + 1 ) % data_batches.num_batches ());
914+
915+ const auto l2_norm_offset =
916+ data_on_device ? static_cast <IndexT>(data_batch.offset ()) : IndexT{0 };
878917 auto l2_const_view = raft::make_device_vector_view<const DataT, IndexT>(
879- L2NormBatch.data_handle (), cur_batch_size);
918+ L2NormBatch.data_handle () + l2_norm_offset , cur_batch_size);
880919
881920 process_batch<DataT, IndexT>(handle,
882921 batch_data_view,
@@ -892,9 +931,15 @@ void kmeans_fit(
892931 centroid_sums.view (),
893932 weight_per_cluster.view (),
894933 clustering_cost.view (),
895- batch_workspace);
934+ batch_workspace,
935+ batch_cost.view ());
936+
937+ // The slot is reusable only after every batch consumer above has been submitted. Refill it
938+ // with the batch two positions ahead; modulo arithmetic naturally crosses pass boundaries.
939+ const auto next_batch_pos = (batch_pos + 2 ) % data_batches.num_batches ();
940+ data_batches.recycle (data_batch, next_batch_pos);
941+ if (weight_batch.has_value ()) { weight_batches->recycle (*weight_batch, next_batch_pos); }
896942 }
897- if (need_compute_norms) { norms_cached = true ; }
898943
899944 finalize_centroids<DataT, IndexT>(handle,
900945 raft::make_const_mdspan (centroid_sums.view ()),
@@ -927,46 +972,68 @@ void kmeans_fit(
927972 raft::copy (handle,
928973 raft::make_pinned_scalar_view (h_done_flag.data_handle ()),
929974 raft::make_device_scalar_view<const int >(d_done_flag.data_handle ()));
975+ // The next pass's first two input batches are already in flight. The compute stream still
976+ // serializes centroid finalization and convergence before it can consume them.
930977 }
931978
932979 {
933980 auto centroids_const = raft::make_device_matrix_view<const DataT, IndexT>(
934981 cur_centroids_ptr, n_clusters, n_features);
935982
936983 iter_inertia = DataT{0 };
937- data_batches.reset ();
938- using wt_iter_t = cuvs::spatial::knn::detail::utils::batch_load_iterator_dyn<DataT>;
939- std::optional<wt_iter_t > wt_it;
940- if (weight_batches.has_value ()) {
941- weight_batches->reset ();
942- wt_it = weight_batches->begin ();
943- }
944- for (const auto & data_batch : data_batches) {
945- IndexT cur_batch_size = static_cast <IndexT>(data_batch.size ());
946- const DataT* wt_data = nullptr ;
947- if (wt_it.has_value ()) {
948- wt_data = (**wt_it).data ();
949- ++(*wt_it);
984+ raft::matrix::fill (handle, clustering_cost.view (), DataT{0 });
985+ data_batches.start ();
986+ if (weight_batches.has_value ()) { weight_batches->start (); }
987+ for (std::size_t batch_pos = 0 ; batch_pos < data_batches.num_batches (); ++batch_pos) {
988+ const auto data_batch = data_batches.acquire (batch_pos);
989+ std::optional<kmeans_batch<DataT>> weight_batch;
990+ if (weight_batches.has_value ()) {
991+ weight_batch.emplace (weight_batches->acquire (batch_pos));
950992 }
951993
994+ IndexT cur_batch_size = static_cast <IndexT>(data_batch.size ());
995+ const DataT* wt_data = weight_batch.has_value () ? weight_batch->data () : nullptr ;
996+
952997 auto batch_data_view = raft::make_device_matrix_view<const DataT, IndexT>(
953998 data_batch.data (), cur_batch_size, n_features);
954-
955999 std::optional<raft::device_vector_view<const DataT, IndexT>> batch_sw = std::nullopt ;
9561000 if (weight_ptr != nullptr ) {
9571001 batch_sw =
9581002 cur_batch_weights (static_cast <IndexT>(data_batch.offset ()), wt_data, cur_batch_size);
9591003 }
9601004
961- DataT batch_cost = DataT{0 };
962- cuvs::cluster::kmeans::cluster_cost (handle,
963- batch_data_view,
964- centroids_const,
965- raft::make_host_scalar_view (&batch_cost),
966- batch_sw);
1005+ if (batch_pos + 1 < data_batches.num_batches () || seed_iter + 1 < n_init) {
1006+ prefetch_batch ((batch_pos + 1 ) % data_batches.num_batches ());
1007+ }
9671008
968- iter_inertia += batch_cost;
1009+ cuvs::cluster::kmeans::detail::cluster_cost (handle,
1010+ batch_data_view,
1011+ centroids_const,
1012+ batch_cost.view (),
1013+ L2NormBatch.view (),
1014+ minClusterDistance.view (),
1015+ L2NormBuf_OR_DistBuf,
1016+ ws,
1017+ batch_sw);
1018+ raft::linalg::add (clustering_cost.data_handle (),
1019+ clustering_cost.data_handle (),
1020+ batch_cost.data_handle (),
1021+ 1 ,
1022+ stream);
1023+
1024+ const bool needs_future_batch =
1025+ batch_pos + 2 < data_batches.num_batches () || seed_iter + 1 < n_init;
1026+ if (needs_future_batch) {
1027+ const auto next_batch_pos = (batch_pos + 2 ) % data_batches.num_batches ();
1028+ data_batches.recycle (data_batch, next_batch_pos);
1029+ if (weight_batch.has_value ()) { weight_batches->recycle (*weight_batch, next_batch_pos); }
1030+ } else {
1031+ data_batches.release (data_batch);
1032+ if (weight_batch.has_value ()) { weight_batches->release (*weight_batch); }
1033+ }
9691034 }
1035+ raft::copy (&iter_inertia, clustering_cost.data_handle (), 1 , stream);
1036+ raft::resource::sync_stream (handle);
9701037 }
9711038
9721039 if (iter_inertia < inertia[0 ]) {
0 commit comments