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(&centroids_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(&centroids_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, &centroids).unwrap();
206
207        // fit the centroids, make sure that inertia has gone down
208        let (inertia, n_iter) = fit(&res, &params, &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            &params,
220            &centroids,
221            &None,
222            &centroids,
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}