1pub use nvidia_impl::{try_nvidia_kmeans, try_nvidia_search_batch};
12pub use rocm_impl::{try_rocm_kmeans, try_rocm_search_batch};
13
14mod nvidia_impl {
25 use std::ffi::c_void;
26
27 use ailake_core::{RowId, VectorMetric};
28 use libloading::{Library, Symbol};
29 use tracing::warn;
30
31 const H2D: i32 = 1; const D2H: i32 = 2; const OP_T: i32 = 1; const OP_N: i32 = 0; type CudaMallocFn = unsafe extern "C" fn(*mut *mut c_void, usize) -> i32;
41 type CudaFreeFn = unsafe extern "C" fn(*mut c_void) -> i32;
42 type CudaMemcpyFn = unsafe extern "C" fn(*mut c_void, *const c_void, usize, i32) -> i32;
43 type CudaSyncFn = unsafe extern "C" fn() -> i32;
44
45 #[cfg(target_os = "linux")]
46 const RT_LIBS: &[&str] = &["libcudart.so", "libcudart.so.12", "libcudart.so.11"];
47 #[cfg(windows)]
48 const RT_LIBS: &[&str] = &["cudart64_12.dll", "cudart64_11.dll"];
49 #[cfg(not(any(target_os = "linux", windows)))]
50 const RT_LIBS: &[&str] = &[];
51
52 #[cfg(target_os = "linux")]
53 const BLAS_LIBS: &[&str] = &["libcublas.so", "libcublas.so.12", "libcublas.so.11"];
54 #[cfg(windows)]
55 const BLAS_LIBS: &[&str] = &["cublas64_12.dll", "cublas64_11.dll"];
56 #[cfg(not(any(target_os = "linux", windows)))]
57 const BLAS_LIBS: &[&str] = &[];
58
59 type SgemmFn = unsafe extern "C" fn(
61 *mut c_void, i32, i32, i32, i32, i32, *const f32, *const c_void,
69 i32, *const c_void,
71 i32, *const f32, *mut c_void,
74 i32, ) -> i32;
76
77 struct DevBuf {
79 ptr: *mut c_void,
80 free_fn: CudaFreeFn,
81 }
82
83 impl Drop for DevBuf {
84 fn drop(&mut self) {
85 if !self.ptr.is_null() {
86 unsafe { (self.free_fn)(self.ptr) };
87 }
88 }
89 }
90
91 struct BlasHandle {
93 handle: *mut c_void,
94 destroy_fn: unsafe extern "C" fn(*mut c_void) -> i32,
95 }
96
97 impl Drop for BlasHandle {
98 fn drop(&mut self) {
99 if !self.handle.is_null() {
100 unsafe { (self.destroy_fn)(self.handle) };
101 }
102 }
103 }
104
105 fn try_open(names: &[&str]) -> Option<Library> {
106 names
107 .iter()
108 .find_map(|name| unsafe { Library::new(name) }.ok())
109 }
110
111 pub fn try_nvidia_search_batch(
119 queries: &[&[f32]],
120 row_ids: &[u64],
121 flat_vecs: &[f32],
122 dim: usize,
123 metric: VectorMetric,
124 top_k: usize,
125 ) -> Option<Vec<Vec<(RowId, f32)>>> {
126 if !crate::hardware::detect_cuda() {
127 return None;
128 }
129 if RT_LIBS.is_empty() || BLAS_LIBS.is_empty() {
130 return None;
131 }
132 let q = queries.len();
133 if row_ids.is_empty() || q == 0 {
134 return Some(vec![vec![]; q]);
135 }
136 let result = batch_inner(queries, row_ids, flat_vecs, dim, metric, top_k);
137 if result.is_none() {
138 warn!(
139 "ailake: NVIDIA GPU search failed at runtime (cuBLAS error or allocation failure); \
140 falling back to CPU SIMD — check CUDA runtime libraries and available GPU memory"
141 );
142 }
143 result
144 }
145
146 pub fn try_nvidia_kmeans(
151 vectors: &[Vec<f32>],
152 k: usize,
153 max_iter: usize,
154 ) -> Option<Vec<Vec<f32>>> {
155 if !crate::hardware::detect_cuda() {
156 return None;
157 }
158 if RT_LIBS.is_empty() || BLAS_LIBS.is_empty() {
159 return None;
160 }
161 if vectors.is_empty() {
162 return Some(vec![]);
163 }
164 let n = vectors.len();
165 let dim = vectors[0].len();
166 let k = k.min(n);
167 let result = kmeans_inner(vectors, k, max_iter, n, dim);
168 if result.is_none() {
169 warn!(
170 "ailake: NVIDIA GPU k-means failed at runtime (cuBLAS error or allocation failure); \
171 falling back to CPU k-means — check CUDA runtime libraries and available GPU memory"
172 );
173 }
174 result
175 }
176
177 unsafe fn load_cuda_fns(
180 rt: &Library,
181 ) -> Option<(CudaMallocFn, CudaFreeFn, CudaMemcpyFn, CudaSyncFn)> {
182 let malloc_sym: Symbol<CudaMallocFn> = rt.get(b"cudaMalloc\0").ok()?;
183 let free_sym: Symbol<CudaFreeFn> = rt.get(b"cudaFree\0").ok()?;
184 let memcpy_sym: Symbol<CudaMemcpyFn> = rt.get(b"cudaMemcpy\0").ok()?;
185 let sync_sym: Symbol<CudaSyncFn> = rt.get(b"cudaDeviceSynchronize\0").ok()?;
186 Some((*malloc_sym, *free_sym, *memcpy_sym, *sync_sym))
187 }
188
189 unsafe fn upload(
190 data: &[f32],
191 malloc_fn: CudaMallocFn,
192 free_fn: CudaFreeFn,
193 memcpy_fn: CudaMemcpyFn,
194 ) -> Option<DevBuf> {
195 let bytes = std::mem::size_of_val(data);
196 let mut ptr: *mut c_void = std::ptr::null_mut();
197 if malloc_fn(&mut ptr, bytes) != 0 {
198 return None;
199 }
200 let buf = DevBuf { ptr, free_fn };
201 if memcpy_fn(ptr, data.as_ptr() as *const c_void, bytes, H2D) != 0 {
202 return None;
203 }
204 Some(buf)
205 }
206
207 unsafe fn alloc_dev(
208 len: usize,
209 malloc_fn: CudaMallocFn,
210 free_fn: CudaFreeFn,
211 ) -> Option<DevBuf> {
212 let bytes = len * std::mem::size_of::<f32>();
213 let mut ptr: *mut c_void = std::ptr::null_mut();
214 if malloc_fn(&mut ptr, bytes) != 0 {
215 return None;
216 }
217 Some(DevBuf { ptr, free_fn })
218 }
219
220 fn normalize_rows(mut data: Vec<f32>, dim: usize) -> Vec<f32> {
221 for row in data.chunks_mut(dim) {
222 let norm: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
223 if norm > 1e-8 {
224 row.iter_mut().for_each(|x| *x /= norm);
225 }
226 }
227 data
228 }
229
230 fn batch_inner(
231 queries: &[&[f32]],
232 row_ids: &[u64],
233 flat_vecs: &[f32],
234 dim: usize,
235 metric: VectorMetric,
236 top_k: usize,
237 ) -> Option<Vec<Vec<(RowId, f32)>>> {
238 let n = row_ids.len();
239 let q = queries.len();
240
241 let rt = try_open(RT_LIBS)?;
242 let blas_lib = try_open(BLAS_LIBS)?;
243
244 let (cuda_malloc, cuda_free, cuda_memcpy, cuda_sync) = unsafe { load_cuda_fns(&rt) }?;
245
246 let blas_create: Symbol<unsafe extern "C" fn(*mut *mut c_void) -> i32> =
247 unsafe { blas_lib.get(b"cublasCreate_v2\0") }.ok()?;
248 let blas_destroy: unsafe extern "C" fn(*mut c_void) -> i32 = *unsafe {
249 blas_lib.get::<unsafe extern "C" fn(*mut c_void) -> i32>(b"cublasDestroy_v2\0")
250 }
251 .ok()?;
252 let sgemm: Symbol<SgemmFn> = unsafe { blas_lib.get(b"cublasSgemm_v2\0") }.ok()?;
253
254 let mut raw_handle: *mut c_void = std::ptr::null_mut();
255 if unsafe { blas_create(&mut raw_handle) } != 0 {
256 return None;
257 }
258 let _blas = BlasHandle {
259 handle: raw_handle,
260 destroy_fn: blas_destroy,
261 };
262
263 let q_flat: Vec<f32>;
264 let db_data: &[f32];
265 let q_data: &[f32];
266 let q_owned;
267 let db_owned;
268
269 match metric {
270 VectorMetric::Cosine => {
271 q_owned = normalize_rows(
272 queries.iter().flat_map(|q| q.iter().copied()).collect(),
273 dim,
274 );
275 db_owned = normalize_rows(flat_vecs.to_vec(), dim);
276 q_data = &q_owned;
277 db_data = &db_owned;
278 }
279 _ => {
280 q_flat = queries.iter().flat_map(|q| q.iter().copied()).collect();
281 q_data = &q_flat;
282 db_data = flat_vecs;
283 }
284 }
285
286 let db_dev = unsafe { upload(db_data, cuda_malloc, cuda_free, cuda_memcpy) }?;
287 let q_dev = unsafe { upload(q_data, cuda_malloc, cuda_free, cuda_memcpy) }?;
288 let c_dev = unsafe { alloc_dev(n * q, cuda_malloc, cuda_free) }?;
289
290 let (alpha, beta) = match metric {
293 VectorMetric::DotProduct | VectorMetric::NormalizedCosine => (-1.0f32, 0.0f32),
294 VectorMetric::Cosine => (-1.0f32, 0.0f32),
295 VectorMetric::Euclidean => (-2.0f32, 0.0f32),
296 };
297
298 let rc = unsafe {
299 sgemm(
300 raw_handle,
301 OP_T,
302 OP_N,
303 n as i32,
304 q as i32,
305 dim as i32,
306 &alpha,
307 db_dev.ptr as *const c_void,
308 dim as i32,
309 q_dev.ptr as *const c_void,
310 dim as i32,
311 &beta,
312 c_dev.ptr,
313 n as i32,
314 )
315 };
316 if rc != 0 {
317 return None;
318 }
319 if unsafe { cuda_sync() } != 0 {
320 return None;
321 }
322
323 let mut c_host = vec![0.0f32; n * q];
324 if unsafe {
325 cuda_memcpy(
326 c_host.as_mut_ptr() as *mut c_void,
327 c_dev.ptr as *const c_void,
328 n * q * std::mem::size_of::<f32>(),
329 D2H,
330 )
331 } != 0
332 {
333 return None;
334 }
335
336 let db_sq: Option<Vec<f32>> = if matches!(metric, VectorMetric::Euclidean) {
337 Some(
338 (0..n)
339 .map(|ni| {
340 flat_vecs[ni * dim..(ni + 1) * dim]
341 .iter()
342 .map(|x| x * x)
343 .sum()
344 })
345 .collect(),
346 )
347 } else {
348 None
349 };
350
351 let results = (0..q)
352 .map(|qi| {
353 let dists: Vec<f32> = (0..n)
354 .map(|ni| {
355 let raw = c_host[ni + qi * n];
356 match metric {
357 VectorMetric::DotProduct => raw,
358 VectorMetric::Cosine | VectorMetric::NormalizedCosine => 1.0 + raw,
359 VectorMetric::Euclidean => {
360 let q_sq: f32 = queries[qi].iter().map(|x| x * x).sum();
361 (q_sq + db_sq.as_ref().unwrap()[ni] + raw).max(0.0).sqrt()
362 }
363 }
364 })
365 .collect();
366
367 let mut indexed: Vec<(usize, f32)> = dists.into_iter().enumerate().collect();
368 indexed.sort_unstable_by(|a, b| {
369 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
370 });
371 indexed.truncate(top_k);
372 indexed
373 .into_iter()
374 .map(|(i, d)| (RowId::new(row_ids[i]), d))
375 .collect()
376 })
377 .collect();
378
379 Some(results)
380 }
381
382 fn kmeans_inner(
383 vectors: &[Vec<f32>],
384 k: usize,
385 max_iter: usize,
386 n: usize,
387 dim: usize,
388 ) -> Option<Vec<Vec<f32>>> {
389 let rt = try_open(RT_LIBS)?;
390 let blas_lib = try_open(BLAS_LIBS)?;
391
392 let (cuda_malloc, cuda_free, cuda_memcpy, cuda_sync) = unsafe { load_cuda_fns(&rt) }?;
393
394 let blas_create: Symbol<unsafe extern "C" fn(*mut *mut c_void) -> i32> =
395 unsafe { blas_lib.get(b"cublasCreate_v2\0") }.ok()?;
396 let blas_destroy: unsafe extern "C" fn(*mut c_void) -> i32 = *unsafe {
397 blas_lib.get::<unsafe extern "C" fn(*mut c_void) -> i32>(b"cublasDestroy_v2\0")
398 }
399 .ok()?;
400 let sgemm: Symbol<SgemmFn> = unsafe { blas_lib.get(b"cublasSgemm_v2\0") }.ok()?;
401
402 let mut raw_handle: *mut c_void = std::ptr::null_mut();
403 if unsafe { blas_create(&mut raw_handle) } != 0 {
404 return None;
405 }
406 let _blas = BlasHandle {
407 handle: raw_handle,
408 destroy_fn: blas_destroy,
409 };
410
411 let flat: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
412 let x_dev = unsafe { upload(&flat, cuda_malloc, cuda_free, cuda_memcpy) }?;
413
414 let x_sq: Vec<f32> = vectors
415 .iter()
416 .map(|v| v.iter().map(|x| x * x).sum())
417 .collect();
418
419 let step = n / k;
420 let mut centroids_flat: Vec<f32> = (0..k)
421 .flat_map(|i| vectors[(i * step) % n].iter().copied())
422 .collect();
423
424 let mut prev_asgn: Vec<u32> = vec![];
425
426 for _ in 0..max_iter {
427 let c_dev = unsafe { upload(¢roids_flat, cuda_malloc, cuda_free, cuda_memcpy) }?;
428 let cross_dev = unsafe { alloc_dev(k * n, cuda_malloc, cuda_free) }?;
429
430 let alpha = -2.0f32;
432 let beta = 0.0f32;
433 let rc = unsafe {
434 sgemm(
435 raw_handle,
436 OP_T,
437 OP_N,
438 k as i32,
439 n as i32,
440 dim as i32,
441 &alpha,
442 c_dev.ptr as *const c_void,
443 dim as i32,
444 x_dev.ptr as *const c_void,
445 dim as i32,
446 &beta,
447 cross_dev.ptr,
448 k as i32,
449 )
450 };
451 if rc != 0 {
452 return None;
453 }
454 if unsafe { cuda_sync() } != 0 {
455 return None;
456 }
457
458 let mut cross_host = vec![0.0f32; k * n];
459 if unsafe {
460 cuda_memcpy(
461 cross_host.as_mut_ptr() as *mut c_void,
462 cross_dev.ptr as *const c_void,
463 k * n * std::mem::size_of::<f32>(),
464 D2H,
465 )
466 } != 0
467 {
468 return None;
469 }
470
471 let c_sq: Vec<f32> = centroids_flat
472 .chunks(dim)
473 .map(|c| c.iter().map(|x| x * x).sum())
474 .collect();
475
476 let asgn: Vec<u32> = (0..n)
477 .map(|ni| {
478 let base = &cross_host[ni * k..(ni + 1) * k];
479 let best = (0..k)
480 .min_by(|&a, &b| {
481 let da = x_sq[ni] + c_sq[a] + base[a];
482 let db = x_sq[ni] + c_sq[b] + base[b];
483 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
484 })
485 .unwrap_or(0);
486 best as u32
487 })
488 .collect();
489
490 if asgn == prev_asgn {
491 break;
492 }
493
494 let mut new_flat = vec![0.0f32; k * dim];
495 let mut counts = vec![0usize; k];
496 for (i, &ci) in asgn.iter().enumerate() {
497 let ci = ci as usize;
498 for (d, &v) in vectors[i].iter().enumerate() {
499 new_flat[ci * dim + d] += v;
500 }
501 counts[ci] += 1;
502 }
503 for j in 0..k {
504 if counts[j] > 0 {
505 let inv = 1.0 / counts[j] as f32;
506 new_flat[j * dim..(j + 1) * dim]
507 .iter_mut()
508 .for_each(|x| *x *= inv);
509 } else {
510 new_flat[j * dim..(j + 1) * dim]
511 .copy_from_slice(¢roids_flat[j * dim..(j + 1) * dim]);
512 }
513 }
514
515 centroids_flat = new_flat;
516 prev_asgn = asgn;
517 }
518
519 Some(centroids_flat.chunks(dim).map(|c| c.to_vec()).collect())
520 }
521}
522
523#[cfg(test)]
526mod tests {
527 use ailake_core::{RowId, VectorMetric};
528
529 fn gpu_backend() -> String {
530 std::env::var("AILAKE_GPU_BACKEND").unwrap_or_else(|_| "none".into())
531 }
532
533 fn make_vecs(n: usize, dim: usize) -> Vec<Vec<f32>> {
535 (0..n)
536 .map(|i| (0..dim).map(|d| ((i * dim + d + 1) as f32).sin()).collect())
537 .collect()
538 }
539
540 #[test]
543 fn gpu_search_batch_cosine_top1_exact() {
544 let backend = gpu_backend();
545 if backend == "none" {
546 println!("AILAKE_GPU_BACKEND=none — skipping gpu_search_batch_cosine_top1_exact");
547 return;
548 }
549 let dim = 16;
550 let vecs = make_vecs(64, dim);
551 let flat: Vec<f32> = vecs.iter().flat_map(|v| v.iter().copied()).collect();
552 let row_ids: Vec<u64> = (0..64).collect();
553 let q = vecs[0].clone();
554 let queries: &[&[f32]] = &[q.as_slice()];
555
556 let got = match backend.as_str() {
557 "cuda" => super::try_nvidia_search_batch(
558 queries,
559 &row_ids,
560 &flat,
561 dim,
562 VectorMetric::Cosine,
563 5,
564 ),
565 "rocm" => {
566 super::try_rocm_search_batch(queries, &row_ids, &flat, dim, VectorMetric::Cosine, 5)
567 }
568 other => panic!("unknown AILAKE_GPU_BACKEND={other}"),
569 };
570
571 let got = got.expect("GPU cosine search returned None — check driver/library installation");
572 assert_eq!(got.len(), 1);
573 let (top_row, top_dist) = got[0][0];
574 assert_eq!(top_row, RowId::new(0), "top-1 must be the query itself");
575 assert!(
576 top_dist < 1e-3,
577 "cosine dist to self must be ≈0, got {top_dist}"
578 );
579 }
580
581 #[test]
583 fn gpu_search_batch_euclidean_top1_exact() {
584 let backend = gpu_backend();
585 if backend == "none" {
586 println!("AILAKE_GPU_BACKEND=none — skipping gpu_search_batch_euclidean_top1_exact");
587 return;
588 }
589 let dim = 8;
590 let vecs = make_vecs(32, dim);
591 let flat: Vec<f32> = vecs.iter().flat_map(|v| v.iter().copied()).collect();
592 let row_ids: Vec<u64> = (0..32).collect();
593 let q = vecs[7].clone();
594 let queries: &[&[f32]] = &[q.as_slice()];
595
596 let got = match backend.as_str() {
597 "cuda" => super::try_nvidia_search_batch(
598 queries,
599 &row_ids,
600 &flat,
601 dim,
602 VectorMetric::Euclidean,
603 3,
604 ),
605 "rocm" => super::try_rocm_search_batch(
606 queries,
607 &row_ids,
608 &flat,
609 dim,
610 VectorMetric::Euclidean,
611 3,
612 ),
613 other => panic!("unknown AILAKE_GPU_BACKEND={other}"),
614 };
615
616 let got = got.expect("GPU euclidean search returned None");
617 let (top_row, top_dist) = got[0][0];
618 assert_eq!(top_row, RowId::new(7), "top-1 must be the query itself");
619 assert!(
620 top_dist < 1e-4,
621 "euclidean dist to self must be 0, got {top_dist}"
622 );
623 }
624
625 #[test]
627 fn gpu_kmeans_returns_k_centroids() {
628 let backend = gpu_backend();
629 if backend == "none" {
630 println!("AILAKE_GPU_BACKEND=none — skipping gpu_kmeans_returns_k_centroids");
631 return;
632 }
633 let dim = 8;
634 let k = 4usize;
635 let vecs: Vec<Vec<f32>> = (0..k)
637 .flat_map(|c| {
638 (0..10).map(move |_| {
639 (0..dim)
640 .map(|d| c as f32 * 20.0 + d as f32 * 0.01)
641 .collect()
642 })
643 })
644 .collect();
645
646 let centroids = match backend.as_str() {
647 "cuda" => super::try_nvidia_kmeans(&vecs, k, 20),
648 "rocm" => super::try_rocm_kmeans(&vecs, k, 20),
649 other => panic!("unknown AILAKE_GPU_BACKEND={other}"),
650 };
651
652 let centroids =
653 centroids.expect("GPU k-means returned None — check driver/library installation");
654 assert_eq!(
655 centroids.len(),
656 k,
657 "expected {k} centroids, got {}",
658 centroids.len()
659 );
660 for c in ¢roids {
661 assert_eq!(
662 c.len(),
663 dim,
664 "centroid dim mismatch: expected {dim}, got {}",
665 c.len()
666 );
667 }
668 }
669}
670
671mod rocm_impl {
682 use std::ffi::c_void;
683
684 use ailake_core::{RowId, VectorMetric};
685 use libloading::{Library, Symbol};
686 use tracing::warn;
687
688 const H2D: i32 = 1; const D2H: i32 = 2; const OP_T: i32 = 112; const OP_N: i32 = 111; type HipMallocFn = unsafe extern "C" fn(*mut *mut c_void, usize) -> i32;
698 type HipFreeFn = unsafe extern "C" fn(*mut c_void) -> i32;
699 type HipMemcpyFn = unsafe extern "C" fn(*mut c_void, *const c_void, usize, i32) -> i32;
700 type HipSyncFn = unsafe extern "C" fn() -> i32;
701
702 #[cfg(target_os = "linux")]
703 const HIP_LIB: &str = "libamdhip64.so";
704 #[cfg(windows)]
705 const HIP_LIB: &str = "amdhip64.dll";
706 #[cfg(not(any(target_os = "linux", windows)))]
707 const HIP_LIB: &str = "";
708
709 #[cfg(target_os = "linux")]
710 const BLAS_LIB: &str = "libhipblas.so";
711 #[cfg(windows)]
712 const BLAS_LIB: &str = "hipblas.dll";
713 #[cfg(not(any(target_os = "linux", windows)))]
714 const BLAS_LIB: &str = "";
715
716 type SgemmFn = unsafe extern "C" fn(
718 *mut c_void, i32, i32, i32, i32, i32, *const f32, *const c_void,
726 i32, *const c_void,
728 i32, *const f32, *mut c_void,
731 i32, ) -> i32;
733
734 struct DevBuf {
736 ptr: *mut c_void,
737 free_fn: unsafe extern "C" fn(*mut c_void) -> i32,
738 }
739
740 impl Drop for DevBuf {
741 fn drop(&mut self) {
742 if !self.ptr.is_null() {
743 unsafe { (self.free_fn)(self.ptr) };
744 }
745 }
746 }
747
748 struct BlasHandle {
750 handle: *mut c_void,
751 destroy_fn: unsafe extern "C" fn(*mut c_void) -> i32,
752 }
753
754 impl Drop for BlasHandle {
755 fn drop(&mut self) {
756 if !self.handle.is_null() {
757 unsafe { (self.destroy_fn)(self.handle) };
758 }
759 }
760 }
761
762 pub fn try_rocm_search_batch(
770 queries: &[&[f32]],
771 row_ids: &[u64],
772 flat_vecs: &[f32],
773 dim: usize,
774 metric: VectorMetric,
775 top_k: usize,
776 ) -> Option<Vec<Vec<(RowId, f32)>>> {
777 if !crate::hardware::detect_rocm() {
778 return None;
779 }
780 if HIP_LIB.is_empty() || BLAS_LIB.is_empty() {
781 return None;
782 }
783 let n = row_ids.len();
784 let q = queries.len();
785 if n == 0 || q == 0 {
786 return Some(vec![vec![]; q]);
787 }
788 let result = batch_inner(queries, row_ids, flat_vecs, dim, metric, top_k);
789 if result.is_none() {
790 warn!(
791 "ailake: AMD ROCm GPU search failed at runtime (hipBLAS error or allocation failure); \
792 falling back to CPU SIMD — check ROCm runtime libraries and available GPU memory"
793 );
794 }
795 result
796 }
797
798 pub fn try_rocm_kmeans(
803 vectors: &[Vec<f32>],
804 k: usize,
805 max_iter: usize,
806 ) -> Option<Vec<Vec<f32>>> {
807 if !crate::hardware::detect_rocm() {
808 return None;
809 }
810 if HIP_LIB.is_empty() || BLAS_LIB.is_empty() {
811 return None;
812 }
813 let n = vectors.len();
814 if n == 0 {
815 return Some(vec![]);
816 }
817 let dim = vectors[0].len();
818 let k = k.min(n);
819 let result = kmeans_inner(vectors, k, max_iter, n, dim);
820 if result.is_none() {
821 warn!(
822 "ailake: AMD ROCm GPU k-means failed at runtime (hipBLAS error or allocation failure); \
823 falling back to CPU k-means — check ROCm runtime libraries and available GPU memory"
824 );
825 }
826 result
827 }
828
829 unsafe fn load_hip_fns(
835 lib: &Library,
836 ) -> Option<(HipMallocFn, HipFreeFn, HipMemcpyFn, HipSyncFn)> {
837 let malloc_sym: Symbol<HipMallocFn> = lib.get(b"hipMalloc\0").ok()?;
838 let free_sym: Symbol<HipFreeFn> = lib.get(b"hipFree\0").ok()?;
839 let memcpy_sym: Symbol<HipMemcpyFn> = lib.get(b"hipMemcpy\0").ok()?;
840 let sync_sym: Symbol<HipSyncFn> = lib.get(b"hipDeviceSynchronize\0").ok()?;
841 Some((*malloc_sym, *free_sym, *memcpy_sym, *sync_sym))
842 }
843
844 unsafe fn upload(
846 data: &[f32],
847 malloc_fn: HipMallocFn,
848 free_fn: HipFreeFn,
849 memcpy_fn: HipMemcpyFn,
850 ) -> Option<DevBuf> {
851 let bytes = std::mem::size_of_val(data);
852 let mut ptr: *mut c_void = std::ptr::null_mut();
853 if malloc_fn(&mut ptr, bytes) != 0 {
854 return None;
855 }
856 let buf = DevBuf { ptr, free_fn };
857 if memcpy_fn(ptr, data.as_ptr() as *const c_void, bytes, H2D) != 0 {
858 return None;
859 }
860 Some(buf)
861 }
862
863 unsafe fn alloc_dev(len: usize, malloc_fn: HipMallocFn, free_fn: HipFreeFn) -> Option<DevBuf> {
865 let bytes = len * std::mem::size_of::<f32>();
866 let mut ptr: *mut c_void = std::ptr::null_mut();
867 if malloc_fn(&mut ptr, bytes) != 0 {
868 return None;
869 }
870 Some(DevBuf { ptr, free_fn })
871 }
872
873 fn normalize_rows(mut data: Vec<f32>, dim: usize) -> Vec<f32> {
875 for row in data.chunks_mut(dim) {
876 let norm: f32 = row.iter().map(|x| x * x).sum::<f32>().sqrt();
877 if norm > 1e-8 {
878 row.iter_mut().for_each(|x| *x /= norm);
879 }
880 }
881 data
882 }
883
884 fn batch_inner(
885 queries: &[&[f32]],
886 row_ids: &[u64],
887 flat_vecs: &[f32],
888 dim: usize,
889 metric: VectorMetric,
890 top_k: usize,
891 ) -> Option<Vec<Vec<(RowId, f32)>>> {
892 let n = row_ids.len();
893 let q = queries.len();
894 let hip = unsafe { Library::new(HIP_LIB) }.ok()?;
896 let blas_lib = unsafe { Library::new(BLAS_LIB) }.ok()?;
897
898 let (hip_malloc, hip_free, hip_memcpy, hip_sync) = unsafe { load_hip_fns(&hip) }?;
899
900 let blas_create: Symbol<unsafe extern "C" fn(*mut *mut c_void) -> i32> =
901 unsafe { blas_lib.get(b"hipblasCreate\0") }.ok()?;
902 let blas_destroy: unsafe extern "C" fn(*mut c_void) -> i32 = *unsafe {
903 blas_lib.get::<unsafe extern "C" fn(*mut c_void) -> i32>(b"hipblasDestroy\0")
904 }
905 .ok()?;
906 let sgemm: Symbol<SgemmFn> = unsafe { blas_lib.get(b"hipblasSgemm\0") }.ok()?;
907
908 let mut raw_handle: *mut c_void = std::ptr::null_mut();
910 if unsafe { blas_create(&mut raw_handle) } != 0 {
911 return None;
912 }
913 let _blas = BlasHandle {
914 handle: raw_handle,
915 destroy_fn: blas_destroy,
916 };
917
918 let q_flat: Vec<f32>;
920 let db_data: &[f32];
921 let q_data: &[f32];
922 let q_owned;
923 let db_owned;
924
925 match metric {
926 VectorMetric::Cosine => {
927 q_owned = normalize_rows(
928 queries.iter().flat_map(|q| q.iter().copied()).collect(),
929 dim,
930 );
931 db_owned = normalize_rows(flat_vecs.to_vec(), dim);
932 q_data = &q_owned;
933 db_data = &db_owned;
934 }
935 _ => {
936 q_flat = queries.iter().flat_map(|q| q.iter().copied()).collect();
937 q_data = &q_flat;
938 db_data = flat_vecs;
939 }
940 }
941
942 let db_dev = unsafe { upload(db_data, hip_malloc, hip_free, hip_memcpy) }?;
944 let q_dev = unsafe { upload(q_data, hip_malloc, hip_free, hip_memcpy) }?;
945 let c_dev = unsafe { alloc_dev(n * q, hip_malloc, hip_free) }?;
946
947 let (alpha, beta) = match metric {
956 VectorMetric::DotProduct | VectorMetric::NormalizedCosine => (-1.0f32, 0.0f32), VectorMetric::Cosine => (-1.0f32, 0.0f32), VectorMetric::Euclidean => (-2.0f32, 0.0f32), };
960
961 let rc = unsafe {
962 sgemm(
963 raw_handle,
964 OP_T,
965 OP_N,
966 n as i32,
967 q as i32,
968 dim as i32,
969 &alpha,
970 db_dev.ptr as *const c_void,
971 dim as i32,
972 q_dev.ptr as *const c_void,
973 dim as i32,
974 &beta,
975 c_dev.ptr,
976 n as i32,
977 )
978 };
979 if rc != 0 {
980 return None;
981 }
982 if unsafe { hip_sync() } != 0 {
983 return None;
984 }
985
986 let mut c_host = vec![0.0f32; n * q];
988 if unsafe {
989 hip_memcpy(
990 c_host.as_mut_ptr() as *mut c_void,
991 c_dev.ptr as *const c_void,
992 n * q * std::mem::size_of::<f32>(),
993 D2H,
994 )
995 } != 0
996 {
997 return None;
998 }
999
1000 let db_sq: Option<Vec<f32>> = if matches!(metric, VectorMetric::Euclidean) {
1002 Some(
1003 (0..n)
1004 .map(|ni| {
1005 flat_vecs[ni * dim..(ni + 1) * dim]
1006 .iter()
1007 .map(|x| x * x)
1008 .sum()
1009 })
1010 .collect(),
1011 )
1012 } else {
1013 None
1014 };
1015
1016 let results = (0..q)
1017 .map(|qi| {
1018 let dists: Vec<f32> = (0..n)
1019 .map(|ni| {
1020 let raw = c_host[ni + qi * n];
1021 match metric {
1022 VectorMetric::DotProduct => raw,
1024 VectorMetric::Cosine | VectorMetric::NormalizedCosine => 1.0 + raw,
1026 VectorMetric::Euclidean => {
1028 let q_sq: f32 = queries[qi].iter().map(|x| x * x).sum();
1029 (q_sq + db_sq.as_ref().unwrap()[ni] + raw).max(0.0).sqrt()
1030 }
1031 }
1032 })
1033 .collect();
1034
1035 let mut indexed: Vec<(usize, f32)> = dists.into_iter().enumerate().collect();
1036 indexed.sort_unstable_by(|a, b| {
1037 a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal)
1038 });
1039 indexed.truncate(top_k);
1040 indexed
1041 .into_iter()
1042 .map(|(i, d)| (RowId::new(row_ids[i]), d))
1043 .collect()
1044 })
1045 .collect();
1046
1047 Some(results)
1048 }
1049
1050 fn kmeans_inner(
1051 vectors: &[Vec<f32>],
1052 k: usize,
1053 max_iter: usize,
1054 n: usize,
1055 dim: usize,
1056 ) -> Option<Vec<Vec<f32>>> {
1057 let hip = unsafe { Library::new(HIP_LIB) }.ok()?;
1058 let blas_lib = unsafe { Library::new(BLAS_LIB) }.ok()?;
1059
1060 let (hip_malloc, hip_free, hip_memcpy, hip_sync) = unsafe { load_hip_fns(&hip) }?;
1061
1062 let blas_create: Symbol<unsafe extern "C" fn(*mut *mut c_void) -> i32> =
1063 unsafe { blas_lib.get(b"hipblasCreate\0") }.ok()?;
1064 let blas_destroy: unsafe extern "C" fn(*mut c_void) -> i32 = *unsafe {
1065 blas_lib.get::<unsafe extern "C" fn(*mut c_void) -> i32>(b"hipblasDestroy\0")
1066 }
1067 .ok()?;
1068 let sgemm: Symbol<SgemmFn> = unsafe { blas_lib.get(b"hipblasSgemm\0") }.ok()?;
1069
1070 let mut raw_handle: *mut c_void = std::ptr::null_mut();
1071 if unsafe { blas_create(&mut raw_handle) } != 0 {
1072 return None;
1073 }
1074 let _blas = BlasHandle {
1075 handle: raw_handle,
1076 destroy_fn: blas_destroy,
1077 };
1078
1079 let flat: Vec<f32> = vectors.iter().flat_map(|v| v.iter().copied()).collect();
1081 let x_dev = unsafe { upload(&flat, hip_malloc, hip_free, hip_memcpy) }?;
1082
1083 let x_sq: Vec<f32> = vectors
1085 .iter()
1086 .map(|v| v.iter().map(|x| x * x).sum())
1087 .collect();
1088
1089 let step = n / k;
1091 let mut centroids_flat: Vec<f32> = (0..k)
1092 .flat_map(|i| vectors[(i * step) % n].iter().copied())
1093 .collect();
1094
1095 let mut prev_asgn: Vec<u32> = vec![];
1096
1097 for _ in 0..max_iter {
1098 let c_dev = unsafe { upload(¢roids_flat, hip_malloc, hip_free, hip_memcpy) }?;
1100
1101 let cross_dev = unsafe { alloc_dev(k * n, hip_malloc, hip_free) }?;
1104
1105 let alpha = -2.0f32;
1110 let beta = 0.0f32;
1111 let rc = unsafe {
1112 sgemm(
1113 raw_handle,
1114 OP_T,
1115 OP_N,
1116 k as i32,
1117 n as i32,
1118 dim as i32,
1119 &alpha,
1120 c_dev.ptr as *const c_void,
1121 dim as i32,
1122 x_dev.ptr as *const c_void,
1123 dim as i32,
1124 &beta,
1125 cross_dev.ptr,
1126 k as i32,
1127 )
1128 };
1129 if rc != 0 {
1130 return None;
1131 }
1132 if unsafe { hip_sync() } != 0 {
1133 return None;
1134 }
1135
1136 let mut cross_host = vec![0.0f32; k * n];
1137 if unsafe {
1138 hip_memcpy(
1139 cross_host.as_mut_ptr() as *mut c_void,
1140 cross_dev.ptr as *const c_void,
1141 k * n * std::mem::size_of::<f32>(),
1142 D2H,
1143 )
1144 } != 0
1145 {
1146 return None;
1147 }
1148
1149 let c_sq: Vec<f32> = centroids_flat
1151 .chunks(dim)
1152 .map(|c| c.iter().map(|x| x * x).sum())
1153 .collect();
1154
1155 let asgn: Vec<u32> = (0..n)
1157 .map(|ni| {
1158 let base = &cross_host[ni * k..(ni + 1) * k];
1159 let best = (0..k)
1160 .min_by(|&a, &b| {
1161 let da = x_sq[ni] + c_sq[a] + base[a];
1162 let db = x_sq[ni] + c_sq[b] + base[b];
1163 da.partial_cmp(&db).unwrap_or(std::cmp::Ordering::Equal)
1164 })
1165 .unwrap_or(0);
1166 best as u32
1167 })
1168 .collect();
1169
1170 if asgn == prev_asgn {
1171 break;
1172 }
1173
1174 let mut new_flat = vec![0.0f32; k * dim];
1176 let mut counts = vec![0usize; k];
1177 for (i, &ci) in asgn.iter().enumerate() {
1178 let ci = ci as usize;
1179 for (d, &v) in vectors[i].iter().enumerate() {
1180 new_flat[ci * dim + d] += v;
1181 }
1182 counts[ci] += 1;
1183 }
1184 for j in 0..k {
1185 if counts[j] > 0 {
1186 let inv = 1.0 / counts[j] as f32;
1187 new_flat[j * dim..(j + 1) * dim]
1188 .iter_mut()
1189 .for_each(|x| *x *= inv);
1190 } else {
1191 new_flat[j * dim..(j + 1) * dim]
1193 .copy_from_slice(¢roids_flat[j * dim..(j + 1) * dim]);
1194 }
1195 }
1196
1197 centroids_flat = new_flat;
1198 prev_asgn = asgn;
1199 }
1200
1201 Some(centroids_flat.chunks(dim).map(|c| c.to_vec()).collect())
1202 }
1203}