Skip to content

Commit bef7261

Browse files
authored
Merge branch 'release/26.10' into hnsw-layered-index
2 parents 7483b61 + d09b235 commit bef7261

6 files changed

Lines changed: 737 additions & 189 deletions

File tree

‎cpp/src/cluster/detail/kmeans.cuh‎

Lines changed: 130 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
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>
@@ -24,6 +25,7 @@
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

5759
static 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

Comments
 (0)