hipvs/cluster/kmeans/mod.rs
1/*
2 * Copyright (c) 2025, NVIDIA CORPORATION.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17// MIT License
18//
19// Modifications Copyright (C) 2026 Advanced Micro Devices, Inc. All rights reserved.
20//
21// Permission is hereby granted, free of charge, to any person obtaining a copy
22// of this software and associated documentation files (the "Software"), to deal
23// in the Software without restriction, including without limitation the rights
24// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
25// copies of the Software, and to permit persons to whom the Software is
26// furnished to do so, subject to the following conditions:
27//
28// The above copyright notice and this permission notice shall be included in all
29// copies or substantial portions of the Software.
30//
31// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
32// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
33// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
34// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
35// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
36// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
37// SOFTWARE.
38
39//! Kmeans clustering API's
40//!
41//! Example:
42//! ```
43//!
44//! use hipvs::cluster::kmeans;
45//! use hipvs::{ManagedTensor, Resources, Result};
46//!
47//! use ndarray_rand::rand_distr::Uniform;
48//! use ndarray_rand::RandomExt;
49//!
50//! fn kmeans_example() -> Result<()> {
51//! let res = Resources::new()?;
52//!
53//! // Create a new random dataset to index
54//! let n_datapoints = 65536;
55//! let n_features = 512;
56//! let n_clusters = 8;
57//! let dataset =
58//! ndarray::Array::<f32, _>::random((n_datapoints, n_features), Uniform::new(0., 1.0));
59//! let dataset = ManagedTensor::from(&dataset).to_device(&res)?;
60//!
61//! let centroids_host = ndarray::Array::<f32, _>::zeros((n_clusters, n_features));
62//! let mut centroids = ManagedTensor::from(¢roids_host).to_device(&res)?;
63//!
64//! // find the centroids with the kmeans index
65//! let kmeans_params = kmeans::Params::new()?.set_n_clusters(n_clusters as i32);
66//! let (inertia, n_iter) = kmeans::fit(&res, &kmeans_params, &dataset, &None, &mut centroids)?;
67//! Ok(())
68//! }
69//! ```
70
71mod params;
72
73pub use params::Params;
74
75use crate::dlpack::ManagedTensor;
76use crate::error::{check_cuvs, Result};
77use crate::resources::Resources;
78
79/// Find clusters with the k-means algorithm
80///
81/// # Arguments
82///
83/// * `res` - Resources to use
84/// * `params` - Parameters to use to fit KMeans model
85/// * `x` - A matrix in device memory - shape (m, k)
86/// * `sample_weight` - Optional device matrix shape (n_clusters, 1)
87/// * `centroids` - Output device matrix, that has the centroids for each cluster
88/// shape (n_clusters, k)
89pub fn fit(
90 res: &Resources,
91 params: &Params,
92 x: &ManagedTensor,
93 sample_weight: &Option<ManagedTensor>,
94 centroids: &mut ManagedTensor,
95) -> Result<(f64, i32)> {
96 let mut inertia: f64 = 0.0;
97 let mut niter: i32 = 0;
98
99 unsafe {
100 let sample_weight_dlpack = match sample_weight {
101 Some(tensor) => tensor.as_ptr(),
102 None => std::ptr::null_mut(),
103 };
104 check_cuvs(ffi::cuvsKMeansFit(
105 res.0,
106 params.0,
107 x.as_ptr(),
108 sample_weight_dlpack,
109 centroids.as_ptr(),
110 &mut inertia as *mut f64,
111 &mut niter as *mut i32,
112 ))?;
113 }
114 Ok((inertia, niter))
115}
116
117/// Predict clusters with the k-means algorithm
118///
119/// # Arguments
120///
121/// * `res` - Resources to use
122/// * `params` - Parameters to use to fit KMeans model
123/// * `x` - Input matrix in device memory - shape (m, k)
124/// * `sample_weight` - Optional device matrix shape (n_clusters, 1)
125/// * `centroids` - Centroids calculated by fit in device memory, shape (n_clusters, k)
126/// * `labels` - preallocated CUDA array interface matrix shape (m, 1) to hold the output labels
127/// * `normalize_weight` - whether or not to normalize the weights
128pub fn predict(
129 res: &Resources,
130 params: &Params,
131 x: &ManagedTensor,
132 sample_weight: &Option<ManagedTensor>,
133 centroids: &ManagedTensor,
134 labels: &mut ManagedTensor,
135 normalize_weight: bool,
136) -> Result<f64> {
137 let mut inertia: f64 = 0.0;
138
139 unsafe {
140 let sample_weight_dlpack = match sample_weight {
141 Some(tensor) => tensor.as_ptr(),
142 None => std::ptr::null_mut(),
143 };
144 check_cuvs(ffi::cuvsKMeansPredict(
145 res.0,
146 params.0,
147 x.as_ptr(),
148 sample_weight_dlpack,
149 centroids.as_ptr(),
150 labels.as_ptr(),
151 normalize_weight,
152 &mut inertia as *mut f64,
153 ))?;
154 }
155 Ok(inertia)
156}
157
158/// Compute cluster cost given an input matrix and existing centroids
159/// # Arguments
160///
161/// * `res` - Resources to use
162/// * `x` - Input matrix in device memory - shape (m, k)
163/// * `centroids` - Centroids calculated by fit in device memory, shape (n_clusters, k)
164pub fn cluster_cost(res: &Resources, x: &ManagedTensor, centroids: &ManagedTensor) -> Result<f64> {
165 let mut inertia: f64 = 0.0;
166
167 unsafe {
168 check_cuvs(ffi::cuvsKMeansClusterCost(
169 res.0,
170 x.as_ptr(),
171 centroids.as_ptr(),
172 &mut inertia as *mut f64,
173 ))?;
174 }
175 Ok(inertia)
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use ndarray_rand::rand_distr::Uniform;
182 use ndarray_rand::RandomExt;
183
184 #[test]
185 fn test_kmeans() {
186 let res = Resources::new().unwrap();
187
188 let n_clusters = 4;
189
190 // Create a new random dataset to index
191 let n_datapoints = 256;
192 let n_features = 16;
193 let dataset =
194 ndarray::Array::<f32, _>::random((n_datapoints, n_features), Uniform::new(0., 1.0));
195 let dataset = ManagedTensor::from(&dataset).to_device(&res).unwrap();
196
197 let centroids_host = ndarray::Array::<f32, _>::zeros((n_clusters, n_features));
198 let mut centroids = ManagedTensor::from(¢roids_host)
199 .to_device(&res)
200 .unwrap();
201
202 let params = Params::new().unwrap().set_n_clusters(n_clusters as i32);
203
204 // compute the inertia, before fitting centroids
205 let original_inertia = cluster_cost(&res, &dataset, ¢roids).unwrap();
206
207 // fit the centroids, make sure that inertia has gone down
208 let (inertia, n_iter) = fit(&res, ¶ms, &dataset, &None, &mut centroids).unwrap();
209
210 assert!(inertia < original_inertia);
211 assert!(n_iter >= 1);
212
213 let mut labels_host = ndarray::Array::<i32, _>::zeros((n_clusters,));
214 let mut labels = ManagedTensor::from(&labels_host).to_device(&res).unwrap();
215
216 // make sure the prediction for each centroid is the centroid itself
217 predict(
218 &res,
219 ¶ms,
220 ¢roids,
221 &None,
222 ¢roids,
223 &mut labels,
224 false,
225 )
226 .unwrap();
227
228 labels.to_host(&res, &mut labels_host).unwrap();
229 assert_eq!(labels_host[[0,]], 0);
230 assert_eq!(labels_host[[1,]], 1);
231 assert_eq!(labels_host[[2,]], 2);
232 assert_eq!(labels_host[[3,]], 3);
233 }
234}