@@ -337,6 +337,228 @@ TEST(CagraC, BuildExtendSearch)
337337 cuvsResourcesDestroy (res);
338338}
339339
340+ TEST (CagraC, BuildNoDatasetThenUpdateAndSearch)
341+ {
342+ // Test the attach_dataset_on_build = false workflow:
343+ // 1. Build index without attaching dataset (saves a full dataset copy)
344+ // 2. Attach dataset via cuvsCagraUpdateDataset
345+ // 3. Search and verify correctness
346+
347+ // create cuvsResources_t
348+ cuvsResources_t res;
349+ cuvsResourcesCreate (&res);
350+ cudaStream_t stream;
351+ cuvsStreamGet (res, &stream);
352+
353+ // create dataset DLTensor
354+ DLManagedTensor dataset_tensor;
355+ dataset_tensor.dl_tensor .data = dataset;
356+ dataset_tensor.dl_tensor .device .device_type = kDLCPU ;
357+ dataset_tensor.dl_tensor .ndim = 2 ;
358+ dataset_tensor.dl_tensor .dtype .code = kDLFloat ;
359+ dataset_tensor.dl_tensor .dtype .bits = 32 ;
360+ dataset_tensor.dl_tensor .dtype .lanes = 1 ;
361+ int64_t dataset_shape[2 ] = {4 , 2 };
362+ dataset_tensor.dl_tensor .shape = dataset_shape;
363+ dataset_tensor.dl_tensor .strides = nullptr ;
364+
365+ // create index
366+ cuvsCagraIndex_t index;
367+ cuvsCagraIndexCreate (&index);
368+
369+ // build index with attach_dataset_on_build = false
370+ cuvsCagraIndexParams_t build_params;
371+ cuvsCagraIndexParamsCreate (&build_params);
372+ build_params->attach_dataset_on_build = false ;
373+ ASSERT_EQ (cuvsCagraBuild (res, build_params, &dataset_tensor, index), CUVS_SUCCESS );
374+
375+ // now attach the dataset
376+ ASSERT_EQ (cuvsCagraUpdateDataset (res, &dataset_tensor, index), CUVS_SUCCESS );
377+
378+ // create queries DLTensor
379+ rmm::device_uvector<float > queries_d (4 * 2 , stream);
380+ raft::copy (queries_d.data (), (float *)queries, 4 * 2 , stream);
381+
382+ DLManagedTensor queries_tensor;
383+ queries_tensor.dl_tensor .data = queries_d.data ();
384+ queries_tensor.dl_tensor .device .device_type = kDLCUDA ;
385+ queries_tensor.dl_tensor .ndim = 2 ;
386+ queries_tensor.dl_tensor .dtype .code = kDLFloat ;
387+ queries_tensor.dl_tensor .dtype .bits = 32 ;
388+ queries_tensor.dl_tensor .dtype .lanes = 1 ;
389+ int64_t queries_shape[2 ] = {4 , 2 };
390+ queries_tensor.dl_tensor .shape = queries_shape;
391+ queries_tensor.dl_tensor .strides = nullptr ;
392+
393+ // create neighbors DLTensor
394+ rmm::device_uvector<uint32_t > neighbors_d (4 , stream);
395+
396+ DLManagedTensor neighbors_tensor;
397+ neighbors_tensor.dl_tensor .data = neighbors_d.data ();
398+ neighbors_tensor.dl_tensor .device .device_type = kDLCUDA ;
399+ neighbors_tensor.dl_tensor .ndim = 2 ;
400+ neighbors_tensor.dl_tensor .dtype .code = kDLUInt ;
401+ neighbors_tensor.dl_tensor .dtype .bits = 32 ;
402+ neighbors_tensor.dl_tensor .dtype .lanes = 1 ;
403+ int64_t neighbors_shape[2 ] = {4 , 1 };
404+ neighbors_tensor.dl_tensor .shape = neighbors_shape;
405+ neighbors_tensor.dl_tensor .strides = nullptr ;
406+
407+ // create distances DLTensor
408+ rmm::device_uvector<float > distances_d (4 , stream);
409+
410+ DLManagedTensor distances_tensor;
411+ distances_tensor.dl_tensor .data = distances_d.data ();
412+ distances_tensor.dl_tensor .device .device_type = kDLCUDA ;
413+ distances_tensor.dl_tensor .ndim = 2 ;
414+ distances_tensor.dl_tensor .dtype .code = kDLFloat ;
415+ distances_tensor.dl_tensor .dtype .bits = 32 ;
416+ distances_tensor.dl_tensor .dtype .lanes = 1 ;
417+ int64_t distances_shape[2 ] = {4 , 1 };
418+ distances_tensor.dl_tensor .shape = distances_shape;
419+ distances_tensor.dl_tensor .strides = nullptr ;
420+
421+ cuvsFilter filter;
422+ filter.type = NO_FILTER ;
423+ filter.addr = (uintptr_t )NULL ;
424+
425+ // search index
426+ cuvsCagraSearchParams_t search_params;
427+ cuvsCagraSearchParamsCreate (&search_params);
428+ cuvsCagraSearch (
429+ res, search_params, index, &queries_tensor, &neighbors_tensor, &distances_tensor, filter);
430+
431+ // verify output — should match the standard BuildSearch test results
432+ ASSERT_TRUE (
433+ cuvs::devArrMatchHost (neighbors_exp, neighbors_d.data (), 4 , cuvs::Compare<uint32_t >()));
434+ ASSERT_TRUE (cuvs::devArrMatchHost (
435+ distances_exp, distances_d.data (), 4 , cuvs::CompareApprox<float >(0 .001f )));
436+
437+ // de-allocate index and res
438+ cuvsCagraSearchParamsDestroy (search_params);
439+ cuvsCagraIndexParamsDestroy (build_params);
440+ cuvsCagraIndexDestroy (index);
441+ cuvsResourcesDestroy (res);
442+ }
443+
444+ TEST (CagraC, BuildNoDatasetThenUpdateDeviceAndSearch)
445+ {
446+ // Test the motivating scenario: dataset already on device (kDLCUDA).
447+ // Using attach_dataset_on_build = false avoids duplicating the device dataset,
448+ // then cuvsCagraUpdateDataset attaches it (zero-copy when properly aligned).
449+
450+ // create cuvsResources_t
451+ cuvsResources_t res;
452+ cuvsResourcesCreate (&res);
453+ cudaStream_t stream;
454+ cuvsStreamGet (res, &stream);
455+
456+ // copy dataset to device memory (simulating a dataset that is already on GPU)
457+ rmm::device_uvector<float > dataset_d (4 * 2 , stream);
458+ raft::copy (dataset_d.data (), (float *)dataset, 4 * 2 , stream);
459+
460+ // create dataset DLTensor on CPU for building the graph
461+ DLManagedTensor dataset_tensor;
462+ dataset_tensor.dl_tensor .data = dataset;
463+ dataset_tensor.dl_tensor .device .device_type = kDLCPU ;
464+ dataset_tensor.dl_tensor .ndim = 2 ;
465+ dataset_tensor.dl_tensor .dtype .code = kDLFloat ;
466+ dataset_tensor.dl_tensor .dtype .bits = 32 ;
467+ dataset_tensor.dl_tensor .dtype .lanes = 1 ;
468+ int64_t dataset_shape[2 ] = {4 , 2 };
469+ dataset_tensor.dl_tensor .shape = dataset_shape;
470+ dataset_tensor.dl_tensor .strides = nullptr ;
471+
472+ // create index
473+ cuvsCagraIndex_t index;
474+ cuvsCagraIndexCreate (&index);
475+
476+ // build index with attach_dataset_on_build = false
477+ cuvsCagraIndexParams_t build_params;
478+ cuvsCagraIndexParamsCreate (&build_params);
479+ build_params->attach_dataset_on_build = false ;
480+ ASSERT_EQ (cuvsCagraBuild (res, build_params, &dataset_tensor, index), CUVS_SUCCESS );
481+
482+ // attach the device-resident dataset via cuvsCagraUpdateDataset (kDLCUDA path)
483+ DLManagedTensor device_dataset_tensor;
484+ device_dataset_tensor.dl_tensor .data = dataset_d.data ();
485+ device_dataset_tensor.dl_tensor .device .device_type = kDLCUDA ;
486+ device_dataset_tensor.dl_tensor .device .device_id = 0 ;
487+ device_dataset_tensor.dl_tensor .ndim = 2 ;
488+ device_dataset_tensor.dl_tensor .dtype .code = kDLFloat ;
489+ device_dataset_tensor.dl_tensor .dtype .bits = 32 ;
490+ device_dataset_tensor.dl_tensor .dtype .lanes = 1 ;
491+ device_dataset_tensor.dl_tensor .shape = dataset_shape;
492+ device_dataset_tensor.dl_tensor .strides = nullptr ;
493+
494+ ASSERT_EQ (cuvsCagraUpdateDataset (res, &device_dataset_tensor, index), CUVS_SUCCESS );
495+
496+ // create queries DLTensor
497+ rmm::device_uvector<float > queries_d (4 * 2 , stream);
498+ raft::copy (queries_d.data (), (float *)queries, 4 * 2 , stream);
499+
500+ DLManagedTensor queries_tensor;
501+ queries_tensor.dl_tensor .data = queries_d.data ();
502+ queries_tensor.dl_tensor .device .device_type = kDLCUDA ;
503+ queries_tensor.dl_tensor .ndim = 2 ;
504+ queries_tensor.dl_tensor .dtype .code = kDLFloat ;
505+ queries_tensor.dl_tensor .dtype .bits = 32 ;
506+ queries_tensor.dl_tensor .dtype .lanes = 1 ;
507+ int64_t queries_shape[2 ] = {4 , 2 };
508+ queries_tensor.dl_tensor .shape = queries_shape;
509+ queries_tensor.dl_tensor .strides = nullptr ;
510+
511+ // create neighbors DLTensor
512+ rmm::device_uvector<uint32_t > neighbors_d (4 , stream);
513+
514+ DLManagedTensor neighbors_tensor;
515+ neighbors_tensor.dl_tensor .data = neighbors_d.data ();
516+ neighbors_tensor.dl_tensor .device .device_type = kDLCUDA ;
517+ neighbors_tensor.dl_tensor .ndim = 2 ;
518+ neighbors_tensor.dl_tensor .dtype .code = kDLUInt ;
519+ neighbors_tensor.dl_tensor .dtype .bits = 32 ;
520+ neighbors_tensor.dl_tensor .dtype .lanes = 1 ;
521+ int64_t neighbors_shape[2 ] = {4 , 1 };
522+ neighbors_tensor.dl_tensor .shape = neighbors_shape;
523+ neighbors_tensor.dl_tensor .strides = nullptr ;
524+
525+ // create distances DLTensor
526+ rmm::device_uvector<float > distances_d (4 , stream);
527+
528+ DLManagedTensor distances_tensor;
529+ distances_tensor.dl_tensor .data = distances_d.data ();
530+ distances_tensor.dl_tensor .device .device_type = kDLCUDA ;
531+ distances_tensor.dl_tensor .ndim = 2 ;
532+ distances_tensor.dl_tensor .dtype .code = kDLFloat ;
533+ distances_tensor.dl_tensor .dtype .bits = 32 ;
534+ distances_tensor.dl_tensor .dtype .lanes = 1 ;
535+ int64_t distances_shape[2 ] = {4 , 1 };
536+ distances_tensor.dl_tensor .shape = distances_shape;
537+ distances_tensor.dl_tensor .strides = nullptr ;
538+
539+ cuvsFilter filter;
540+ filter.type = NO_FILTER ;
541+ filter.addr = (uintptr_t )NULL ;
542+
543+ // search index
544+ cuvsCagraSearchParams_t search_params;
545+ cuvsCagraSearchParamsCreate (&search_params);
546+ cuvsCagraSearch (
547+ res, search_params, index, &queries_tensor, &neighbors_tensor, &distances_tensor, filter);
548+
549+ // verify output — should match the standard BuildSearch test results
550+ ASSERT_TRUE (
551+ cuvs::devArrMatchHost (neighbors_exp, neighbors_d.data (), 4 , cuvs::Compare<uint32_t >()));
552+ ASSERT_TRUE (cuvs::devArrMatchHost (
553+ distances_exp, distances_d.data (), 4 , cuvs::CompareApprox<float >(0 .001f )));
554+
555+ // de-allocate index and res
556+ cuvsCagraSearchParamsDestroy (search_params);
557+ cuvsCagraIndexParamsDestroy (build_params);
558+ cuvsCagraIndexDestroy (index);
559+ cuvsResourcesDestroy (res);
560+ }
561+
340562TEST (CagraC, BuildSearchFiltered)
341563{
342564 // create cuvsResources_t
0 commit comments