Skill Tracer
Skills › Scientific Agent Skills › optimize-for-gpu › cuvs.md

cuvs.md

697 lines · skills/optimize-for-gpu/references/cuvs.md at 5f2bc03

Open on GitHub
1# cuVS Reference
2
3> Review: 2026-10-01. Code below is illustrative unless explicitly described as CPU-tested.
4> GPU execution, performance, GDS, and multi-GPU behavior require validation on target hardware.
5
6cuVS is NVIDIA's GPU-accelerated library for exact and approximate nearest-neighbor search, part
7of the RAPIDS ecosystem. It provides CAGRA, IVF-Flat, IVF-PQ, brute-force, and CPU-serving
8interoperability. Compare equal metrics and exactness requirements, validate ANN recall, and
9benchmark build, search, transfer, and serialization costs separately.
11> **Full documentation:** https://docs.nvidia.com/cuvs/home
12> **Reviewed release source:** https://github.com/rapidsai/cuvs/tree/v26.08.00/python/cuvs/cuvs

cuVS Reference

Review: 2026-10-01. Code below is illustrative unless explicitly described as CPU-tested. GPU execution, performance, GDS, and multi-GPU behavior require validation on target hardware.

cuVS is NVIDIA's GPU-accelerated library for exact and approximate nearest-neighbor search, part of the RAPIDS ecosystem. It provides CAGRA, IVF-Flat, IVF-PQ, brute-force, and CPU-serving interoperability. Compare equal metrics and exactness requirements, validate ANN recall, and benchmark build, search, transfer, and serialization costs separately.

Full documentation: https://docs.nvidia.com/cuvs/home Reviewed release source: https://github.com/rapidsai/cuvs/tree/v26.08.00/python/cuvs/cuvs
Raw lines 1 to 13
14## Table of Contents
161. [Installation and Setup](#installation-and-setup)
172. [When to Use cuVS](#when-to-use-cuvs)
183. [Index Selection Guide](#index-selection-guide)
194. [CAGRA — Graph-Based Index](#cagra)
205. [IVF-Flat — Inverted File Index](#ivf-flat)
216. [IVF-PQ — Compressed Inverted File Index](#ivf-pq)
227. [Brute Force — Exact Search](#brute-force)
238. [HNSW — CPU Search from GPU Index](#hnsw)
249. [Distance Metrics](#distance-metrics)
2510. [Filtering](#filtering)
2611. [Multi-GPU](#multi-gpu)
2712. [Memory and Performance](#memory-and-performance)
2813. [Interoperability](#interoperability)
2914. [Common Patterns](#common-patterns)
31---

Table of Contents

  1. Installation and Setup
  2. When to Use cuVS
  3. Index Selection Guide
  4. CAGRA — Graph-Based Index
  5. IVF-Flat — Inverted File Index
  6. IVF-PQ — Compressed Inverted File Index
  7. Brute Force — Exact Search
  8. HNSW — CPU Search from GPU Index
  9. Distance Metrics
  10. Filtering
  11. Multi-GPU
  12. Memory and Performance
  13. Interoperability
  14. Common Patterns

Raw lines 14 to 32
33## Installation and Setup
35Use `uv add` in standalone examples; follow the user's existing project package manager when one
36is already configured.
38```bash
39uv add --extra-index-url=https://pypi.nvidia.com "cuvs-cu12==26.8.*" # For CUDA 12.x
40uv add --extra-index-url=https://pypi.nvidia.com "cuvs-cu13==26.8.*" # For CUDA 13.x
41```
43cuVS wheels (including the companion `libcuvs` wheel) are also published directly to PyPI, so the extra index is optional — but the official cuVS docs still show it and it does no harm.
45**Platform:** Linux and WSL2 only (no native macOS or Windows).
46**Requires:** NVIDIA GPU with CUDA 12.x or 13.x support, Python 3.11+, CuPy recommended for GPU arrays.
48Verify:
49```python
50from cuvs.neighbors import cagra
51import cupy as cp
53dataset = cp.random.rand(1000, 128, dtype=cp.float32)
54index = cagra.build(cagra.IndexParams(), dataset)
55print("cuVS working — built CAGRA index")
56```
58---

Installation and Setup

Use uv add in standalone examples; follow the user's existing project package manager when one is already configured.

uv add --extra-index-url=https://pypi.nvidia.com "cuvs-cu12==26.8.*"   # For CUDA 12.x
uv add --extra-index-url=https://pypi.nvidia.com "cuvs-cu13==26.8.*"   # For CUDA 13.x

cuVS wheels (including the companion libcuvs wheel) are also published directly to PyPI, so the extra index is optional — but the official cuVS docs still show it and it does no harm.

Platform: Linux and WSL2 only (no native macOS or Windows). Requires: NVIDIA GPU with CUDA 12.x or 13.x support, Python 3.11+, CuPy recommended for GPU arrays.

Verify:

from cuvs.neighbors import cagra
import cupy as cp

dataset = cp.random.rand(1000, 128, dtype=cp.float32)
index = cagra.build(cagra.IndexParams(), dataset)
print("cuVS working — built CAGRA index")

Raw lines 33 to 59
60## When to Use cuVS
62cuVS is the right tool when the user needs:
63- **Nearest neighbor search** on high-dimensional vectors (embeddings)
64- **Similarity search** for RAG, recommender systems, image/text/audio retrieval
65- **k-NN graph construction** for clustering or visualization pipelines
66- **Vector database backends** — cuVS powers search in Milvus, Lucene, Kinetica, and Faiss GPU
67- **Replacing Faiss, Annoy, ScaNN, or sklearn NearestNeighbors** with GPU acceleration
69cuVS is NOT the right tool for:
70- General machine learning (use cuML instead)
71- Small or low-dimensional datasets where build, transfer, or launch overhead dominates in a
72 representative benchmark
73- CPU-only environments with no GPU available
75---

When to Use cuVS

cuVS is the right tool when the user needs:

  • Nearest neighbor search on high-dimensional vectors (embeddings)
  • Similarity search for RAG, recommender systems, image/text/audio retrieval
  • k-NN graph construction for clustering or visualization pipelines
  • Vector database backends — cuVS powers search in Milvus, Lucene, Kinetica, and Faiss GPU
  • Replacing Faiss, Annoy, ScaNN, or sklearn NearestNeighbors with GPU acceleration

cuVS is NOT the right tool for:

  • General machine learning (use cuML instead)
  • Small or low-dimensional datasets where build, transfer, or launch overhead dominates in a representative benchmark
  • CPU-only environments with no GPU available

Raw lines 60 to 76
77## Index Selection Guide
79| Index | Best For | Build Speed | Search Speed | Memory | Accuracy |
80|-------|----------|-------------|--------------|--------|----------|
81| **CAGRA** | Strong first ANN candidate | Fast | Fast | Medium | Tunable |
82| **IVF-Flat** | High-recall ANN without vector compression | Medium | Fast | High (stores full vectors) | Tunable |
83| **IVF-PQ** | Large datasets that don't fit in GPU memory | Medium | Fast | Low (compressed) | Good |
84| **Brute Force** | Small datasets or ground truth | N/A | Slow at scale | High | Exact |
85| **HNSW** | CPU-side serving from GPU-built index | Slow | Fast (CPU) | Medium | High |
87Benchmark CAGRA as the first ANN candidate, then compare IVF-PQ when memory is tight and IVF-Flat
88when avoiding vector compression matters. Use brute force for exact search and recall ground truth.
89Choose build and search parameters from measured latency, throughput, memory, and recall targets.
91---

Index Selection Guide

IndexBest ForBuild SpeedSearch SpeedMemoryAccuracy
CAGRAStrong first ANN candidateFastFastMediumTunable
IVF-FlatHigh-recall ANN without vector compressionMediumFastHigh (stores full vectors)Tunable
IVF-PQLarge datasets that don't fit in GPU memoryMediumFastLow (compressed)Good
Brute ForceSmall datasets or ground truthN/ASlow at scaleHighExact
HNSWCPU-side serving from GPU-built indexSlowFast (CPU)MediumHigh

Benchmark CAGRA as the first ANN candidate, then compare IVF-PQ when memory is tight and IVF-Flat when avoiding vector compression matters. Use brute force for exact search and recall ground truth. Choose build and search parameters from measured latency, throughput, memory, and recall targets.


Raw lines 77 to 92
93## CAGRA
95CAGRA (CUDA Accelerated Graph-based) is a graph-based ANN index optimized for GPU. Treat it as a strong ANN candidate and compare recall, memory, and end-to-end latency.

CAGRA

CAGRA (CUDA Accelerated Graph-based) is a graph-based ANN index optimized for GPU. Treat it as a strong ANN candidate and compare recall, memory, and end-to-end latency.

Raw lines 93 to 96
97### Build
99```python
100import cupy as cp
101from cuvs.neighbors import cagra
103n_samples = 1_000_000
104n_features = 128
105dataset = cp.random.rand(n_samples, n_features, dtype=cp.float32)
107# Default parameters work well for most cases
108index_params = cagra.IndexParams(
109 metric="sqeuclidean", # "sqeuclidean", "inner_product", "cosine"
110 intermediate_graph_degree=128, # Higher = better quality, slower build
111 graph_degree=64, # Final graph degree (lower = less memory)
112 build_algo="ivf_pq", # "ivf_pq", "nn_descent", "iterative_cagra_search", or "ace"
115index = cagra.build(index_params, dataset)
116```

Build

import cupy as cp
from cuvs.neighbors import cagra

n_samples = 1_000_000
n_features = 128
dataset = cp.random.rand(n_samples, n_features, dtype=cp.float32)

# Default parameters work well for most cases
index_params = cagra.IndexParams(
    metric="sqeuclidean",             # "sqeuclidean", "inner_product", "cosine"
    intermediate_graph_degree=128,     # Higher = better quality, slower build
    graph_degree=64,                   # Final graph degree (lower = less memory)
    build_algo="ivf_pq",              # "ivf_pq", "nn_descent", "iterative_cagra_search", or "ace"
)

index = cagra.build(index_params, dataset)
Raw lines 97 to 117
118### Search
120```python
121from cuvs.common import Resources
123queries = cp.random.rand(1000, n_features, dtype=cp.float32)
125search_params = cagra.SearchParams(
126 itopk_size=64, # Intermediate top-k (higher = more accurate, slower)
127 search_width=1, # Starting nodes per iteration
128 max_iterations=0, # 0 = auto
129 algo="auto", # "auto", "single_cta", "multi_cta", "multi_kernel"
132resources = Resources()
133distances, neighbors = cagra.search(
134 search_params, index, queries, k=10, resources=resources
136resources.sync()
138# distances: shape (1000, 10) — squared Euclidean distances
139# neighbors: shape (1000, 10) — indices into original dataset
140```

Search

from cuvs.common import Resources

queries = cp.random.rand(1000, n_features, dtype=cp.float32)

search_params = cagra.SearchParams(
    itopk_size=64,       # Intermediate top-k (higher = more accurate, slower)
    search_width=1,      # Starting nodes per iteration
    max_iterations=0,    # 0 = auto
    algo="auto",         # "auto", "single_cta", "multi_cta", "multi_kernel"
)

resources = Resources()
distances, neighbors = cagra.search(
    search_params, index, queries, k=10, resources=resources
)
resources.sync()

# distances: shape (1000, 10) — squared Euclidean distances
# neighbors: shape (1000, 10) — indices into original dataset
Raw lines 118 to 141
142### Save / Load
144Persist the exact cuVS release, dtype, metric and dataset identity alongside the index.
145Serialized formats are experimental and not guaranteed portable across releases.
147```python
148cagra.save("my_index.cagra", index)
149loaded_index = cagra.load("my_index.cagra")
150```

Save / Load

Persist the exact cuVS release, dtype, metric and dataset identity alongside the index. Serialized formats are experimental and not guaranteed portable across releases.

cagra.save("my_index.cagra", index)
loaded_index = cagra.load("my_index.cagra")
Raw lines 142 to 151
152### Extend
154```python
155new_data = cp.random.rand(10_000, n_features, dtype=cp.float32)
156extended_index = cagra.extend(cagra.ExtendParams(), index, new_data)
157```

Extend

new_data = cp.random.rand(10_000, n_features, dtype=cp.float32)
extended_index = cagra.extend(cagra.ExtendParams(), index, new_data)
Raw lines 152 to 158
159### With Compression (for large datasets)
161```python
162from cuvs.neighbors.cagra import CompressionParams
164index_params = cagra.IndexParams(
165 compression=CompressionParams(
166 pq_bits=8,
167 pq_dim=64,
168 )
170index = cagra.build(index_params, dataset)
171```
173---

With Compression (for large datasets)

from cuvs.neighbors.cagra import CompressionParams

index_params = cagra.IndexParams(
    compression=CompressionParams(
        pq_bits=8,
        pq_dim=64,
    )
)
index = cagra.build(index_params, dataset)

Raw lines 159 to 174
175## IVF-Flat
177IVF-Flat partitions the dataset into clusters (inverted file) and stores full vectors. Higher accuracy than IVF-PQ but uses more memory.

IVF-Flat

IVF-Flat partitions the dataset into clusters (inverted file) and stores full vectors. Higher accuracy than IVF-PQ but uses more memory.

Raw lines 175 to 178
179### Build
181```python
182from cuvs.neighbors import ivf_flat
184build_params = ivf_flat.IndexParams(
185 n_lists=1024, # Number of clusters (sqrt(n_samples) is a good start)
186 metric="sqeuclidean", # "sqeuclidean", "euclidean", "inner_product", "cosine"
187 kmeans_trainset_fraction=0.5, # Fraction of data used for k-means training
188 kmeans_n_iters=20, # K-means iterations
189 add_data_on_build=True, # Add vectors during build (vs. extend later)
192index = ivf_flat.build(build_params, dataset)
193```

Build

from cuvs.neighbors import ivf_flat

build_params = ivf_flat.IndexParams(
    n_lists=1024,                    # Number of clusters (sqrt(n_samples) is a good start)
    metric="sqeuclidean",            # "sqeuclidean", "euclidean", "inner_product", "cosine"
    kmeans_trainset_fraction=0.5,    # Fraction of data used for k-means training
    kmeans_n_iters=20,               # K-means iterations
    add_data_on_build=True,          # Add vectors during build (vs. extend later)
)

index = ivf_flat.build(build_params, dataset)
Raw lines 179 to 194
195### Search
197```python
198search_params = ivf_flat.SearchParams(
199 n_probes=50, # Clusters to search (higher = more accurate, slower)
202distances, neighbors = ivf_flat.search(
203 search_params, index, queries, k=10
205```

Search

search_params = ivf_flat.SearchParams(
    n_probes=50,    # Clusters to search (higher = more accurate, slower)
)

distances, neighbors = ivf_flat.search(
    search_params, index, queries, k=10
)
Raw lines 195 to 206
207### Save / Load / Extend
209```python
210ivf_flat.save("my_index.ivf_flat", index)
211loaded_index = ivf_flat.load("my_index.ivf_flat")
213# Extend with new data
214import numpy as np
215new_vectors = cp.random.rand(5000, n_features, dtype=cp.float32)
216new_indices = cp.arange(n_samples, n_samples + 5000, dtype=cp.int64)
217ivf_flat.extend(index, new_vectors, new_indices)
218```
220---

Save / Load / Extend

ivf_flat.save("my_index.ivf_flat", index)
loaded_index = ivf_flat.load("my_index.ivf_flat")

# Extend with new data
import numpy as np
new_vectors = cp.random.rand(5000, n_features, dtype=cp.float32)
new_indices = cp.arange(n_samples, n_samples + 5000, dtype=cp.int64)
ivf_flat.extend(index, new_vectors, new_indices)

Raw lines 207 to 221
222## IVF-PQ
224IVF-PQ compresses vectors using product quantization, dramatically reducing memory usage. Best for large datasets that don't fit in GPU memory with full vectors.

IVF-PQ

IVF-PQ compresses vectors using product quantization, dramatically reducing memory usage. Best for large datasets that don't fit in GPU memory with full vectors.

Raw lines 222 to 225
226### Build
228```python
229from cuvs.neighbors import ivf_pq
231build_params = ivf_pq.IndexParams(
232 n_lists=1024, # Number of clusters
233 metric="sqeuclidean", # "sqeuclidean", "inner_product"
234 pq_bits=8, # Bits per subquantizer (4 or 8)
235 pq_dim=0, # PQ dimensions (0 = auto, typically dim/4)
236 codebook_kind="subspace", # "subspace" or "cluster"
237 kmeans_n_iters=20,
238 add_data_on_build=True,
241index = ivf_pq.build(build_params, dataset)
242```

Build

from cuvs.neighbors import ivf_pq

build_params = ivf_pq.IndexParams(
    n_lists=1024,                # Number of clusters
    metric="sqeuclidean",        # "sqeuclidean", "inner_product"
    pq_bits=8,                   # Bits per subquantizer (4 or 8)
    pq_dim=0,                    # PQ dimensions (0 = auto, typically dim/4)
    codebook_kind="subspace",    # "subspace" or "cluster"
    kmeans_n_iters=20,
    add_data_on_build=True,
)

index = ivf_pq.build(build_params, dataset)
Raw lines 226 to 243
244### Search
246```python
247search_params = ivf_pq.SearchParams(
248 n_probes=50, # Clusters to search
249 lut_dtype="float32", # Look-up table precision
250 internal_distance_dtype="float32",
253distances, neighbors = ivf_pq.search(
254 search_params, index, queries, k=10
256```

Search

search_params = ivf_pq.SearchParams(
    n_probes=50,                     # Clusters to search
    lut_dtype="float32",             # Look-up table precision
    internal_distance_dtype="float32",
)

distances, neighbors = ivf_pq.search(
    search_params, index, queries, k=10
)
Raw lines 244 to 257
258### Save / Load / Extend
260```python
261ivf_pq.save("my_index.ivf_pq", index)
262loaded_index = ivf_pq.load("my_index.ivf_pq")
264# Extend
265new_vectors = cp.random.rand(5000, n_features, dtype=cp.float32)
266new_indices = cp.arange(n_samples, n_samples + 5000, dtype=cp.int64)
267ivf_pq.extend(index, new_vectors, new_indices)
268```
270---

Save / Load / Extend

ivf_pq.save("my_index.ivf_pq", index)
loaded_index = ivf_pq.load("my_index.ivf_pq")

# Extend
new_vectors = cp.random.rand(5000, n_features, dtype=cp.float32)
new_indices = cp.arange(n_samples, n_samples + 5000, dtype=cp.int64)
ivf_pq.extend(index, new_vectors, new_indices)

Raw lines 258 to 271
272## Brute Force
274Exact k-NN search — computes all distances. Use for small datasets (< 50K vectors) or generating ground truth to evaluate approximate indexes.
276```python
277from cuvs.neighbors import brute_force
279# Build (just stores the dataset)
280index = brute_force.build(dataset, metric="sqeuclidean")
282# Search
283distances, neighbors = brute_force.search(index, queries, k=10)
285# Save / Load
286brute_force.save("bf_index.bin", index)
287loaded = brute_force.load("bf_index.bin")
288```
290---

Brute Force

Exact k-NN search — computes all distances. Use for small datasets (< 50K vectors) or generating ground truth to evaluate approximate indexes.

from cuvs.neighbors import brute_force

# Build (just stores the dataset)
index = brute_force.build(dataset, metric="sqeuclidean")

# Search
distances, neighbors = brute_force.search(index, queries, k=10)

# Save / Load
brute_force.save("bf_index.bin", index)
loaded = brute_force.load("bf_index.bin")

Raw lines 272 to 291
292## HNSW
294cuVS provides an HNSW implementation for CPU-side search. The typical workflow is: build a CAGRA index on GPU, convert it to HNSW for CPU serving. This lets you leverage GPU speed for building while serving from CPU (useful when GPU isn't available at query time).
296```python
297from cuvs.neighbors import cagra, hnsw
298import numpy as np
300# Build CAGRA on GPU
301dataset_gpu = cp.random.rand(100_000, 128, dtype=cp.float32)
302cagra_index = cagra.build(cagra.IndexParams(), dataset_gpu)
304# Convert to HNSW for CPU search
305hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), cagra_index)
307# Search on CPU with numpy queries
308queries_cpu = np.random.rand(100, 128).astype(np.float32)
309search_params = hnsw.SearchParams(
310 ef=200, # Search depth (higher = more accurate, slower)
311 num_threads=0, # 0 = auto (uses all available threads)
313distances, neighbors = hnsw.search(search_params, hnsw_index, queries_cpu, k=10)
315# Save / Load
316hnsw.save("my_index.hnsw", hnsw_index)
317loaded = hnsw.load(hnsw.IndexParams(), "my_index.hnsw", dim=128,
318 dtype=np.float32, metric="sqeuclidean")
319```

HNSW

cuVS provides an HNSW implementation for CPU-side search. The typical workflow is: build a CAGRA index on GPU, convert it to HNSW for CPU serving. This lets you leverage GPU speed for building while serving from CPU (useful when GPU isn't available at query time).

from cuvs.neighbors import cagra, hnsw
import numpy as np

# Build CAGRA on GPU
dataset_gpu = cp.random.rand(100_000, 128, dtype=cp.float32)
cagra_index = cagra.build(cagra.IndexParams(), dataset_gpu)

# Convert to HNSW for CPU search
hnsw_index = hnsw.from_cagra(hnsw.IndexParams(), cagra_index)

# Search on CPU with numpy queries
queries_cpu = np.random.rand(100, 128).astype(np.float32)
search_params = hnsw.SearchParams(
    ef=200,           # Search depth (higher = more accurate, slower)
    num_threads=0,    # 0 = auto (uses all available threads)
)
distances, neighbors = hnsw.search(search_params, hnsw_index, queries_cpu, k=10)

# Save / Load
hnsw.save("my_index.hnsw", hnsw_index)
loaded = hnsw.load(hnsw.IndexParams(), "my_index.hnsw", dim=128,
                    dtype=np.float32, metric="sqeuclidean")
Raw lines 292 to 320
321### Extendable HNSW
323To add vectors after building, use `hierarchy="cpu"`:
325```python
326hnsw_index = hnsw.from_cagra(hnsw.IndexParams(hierarchy="cpu"), cagra_index)
328new_data = np.random.rand(5000, 128).astype(np.float32)
329hnsw.extend(hnsw.ExtendParams(), hnsw_index, new_data)
330```
332---

Extendable HNSW

To add vectors after building, use hierarchy="cpu":

hnsw_index = hnsw.from_cagra(hnsw.IndexParams(hierarchy="cpu"), cagra_index)

new_data = np.random.rand(5000, 128).astype(np.float32)
hnsw.extend(hnsw.ExtendParams(), hnsw_index, new_data)

Raw lines 321 to 333
334## Distance Metrics
336| Metric | String | Notes |
337|--------|--------|-------|
338| Squared Euclidean | `"sqeuclidean"` | Default. Fastest — avoids sqrt. |
339| Euclidean | `"euclidean"` | L2 distance |
340| Inner Product | `"inner_product"` | For normalized embeddings (cosine similarity via dot product) |
341| Cosine | `"cosine"` | Supported by CAGRA and IVF-Flat |
343For cosine similarity with IVF-PQ, normalize vectors to unit length and use `"inner_product"`.
345---

Distance Metrics

MetricStringNotes
Squared Euclidean"sqeuclidean"Default. Fastest — avoids sqrt.
Euclidean"euclidean"L2 distance
Inner Product"inner_product"For normalized embeddings (cosine similarity via dot product)
Cosine"cosine"Supported by CAGRA and IVF-Flat

For cosine similarity with IVF-PQ, normalize vectors to unit length and use "inner_product".


Raw lines 334 to 346
347## Filtering
349cuVS supports pre-filtering search results using bitmaps or bitsets to exclude specific vectors.
351```python
352import numpy as np
353import cupy as cp
354from cuvs.neighbors import brute_force, filters
356def pack_allowed_mask(allowed):
357 """Pack sample inclusion flags into little-endian uint32 words."""
358 allowed = np.asarray(allowed, dtype=np.bool_)
359 if allowed.ndim != 1:
360 raise ValueError("Expected one inclusion flag per indexed vector")
361 padded = np.pad(allowed, (0, (-allowed.size) % 32), constant_values=False)
362 return np.packbits(padded, bitorder="little").view("<u4").astype(np.uint32)
364# 1 = allowed, 0 = excluded. One packed bit per indexed vector, NOT one byte.
365allowed = np.ones(dataset.shape[0], dtype=np.bool_)
366allowed[:1000] = False
367bitset = cp.asarray(pack_allowed_mask(allowed))
368prefilter = filters.from_bitset(bitset)
369distances, neighbors = brute_force.search(index, queries, k=10, prefilter=prefilter)
370```
372CAGRA uses `filter=prefilter` rather than `prefilter=`. Keep the bitset/filter alive until
373search completes; validate sentinel results if fewer than k candidates survive.
374`pack_allowed_mask` is CPU-tested; the filtered GPU search remains illustrative.
376---

Filtering

cuVS supports pre-filtering search results using bitmaps or bitsets to exclude specific vectors.

import numpy as np
import cupy as cp
from cuvs.neighbors import brute_force, filters

def pack_allowed_mask(allowed):
    """Pack sample inclusion flags into little-endian uint32 words."""
    allowed = np.asarray(allowed, dtype=np.bool_)
    if allowed.ndim != 1:
        raise ValueError("Expected one inclusion flag per indexed vector")
    padded = np.pad(allowed, (0, (-allowed.size) % 32), constant_values=False)
    return np.packbits(padded, bitorder="little").view("<u4").astype(np.uint32)

# 1 = allowed, 0 = excluded. One packed bit per indexed vector, NOT one byte.
allowed = np.ones(dataset.shape[0], dtype=np.bool_)
allowed[:1000] = False
bitset = cp.asarray(pack_allowed_mask(allowed))
prefilter = filters.from_bitset(bitset)
distances, neighbors = brute_force.search(index, queries, k=10, prefilter=prefilter)

CAGRA uses filter=prefilter rather than prefilter=. Keep the bitset/filter alive until search completes; validate sentinel results if fewer than k candidates survive. pack_allowed_mask is CPU-tested; the filtered GPU search remains illustrative.


Raw lines 347 to 377
378## Multi-GPU
380For datasets too large for a single GPU, use the multi-GPU API:
382```python
383from cuvs.neighbors.mg import cagra as mg_cagra
385# Build across all available GPUs
386build_params = mg_cagra.IndexParams(
387 intermediate_graph_degree=64,
388 graph_degree=32,
390dataset_host = cp.asnumpy(dataset) # MG CAGRA requires host input
391queries_host = cp.asnumpy(queries)
392index = mg_cagra.build(build_params, dataset_host)
394# Search across GPUs
395search_params = mg_cagra.SearchParams()
396distances, neighbors = mg_cagra.search(search_params, index, queries_host, k=10)
397```
399Multi-GPU is also available for IVF-Flat and IVF-PQ via `cuvs.neighbors.mg`.
401---

Multi-GPU

For datasets too large for a single GPU, use the multi-GPU API:

from cuvs.neighbors.mg import cagra as mg_cagra

# Build across all available GPUs
build_params = mg_cagra.IndexParams(
    intermediate_graph_degree=64,
    graph_degree=32,
)
dataset_host = cp.asnumpy(dataset)  # MG CAGRA requires host input
queries_host = cp.asnumpy(queries)
index = mg_cagra.build(build_params, dataset_host)

# Search across GPUs
search_params = mg_cagra.SearchParams()
distances, neighbors = mg_cagra.search(search_params, index, queries_host, k=10)

Multi-GPU is also available for IVF-Flat and IVF-PQ via cuvs.neighbors.mg.


Raw lines 378 to 402
403## Memory and Performance

Memory and Performance

Raw lines 403 to 404
405### Supported Data Types
407Dtypes are index-specific. Brute force accepts float32/float16; do not assume the int8/uint8 support of CAGRA or IVF-Flat applies to every index or metric.
409Using `float16` halves memory and can speed up both build and search when full float32 precision isn't needed (common for embeddings).

Supported Data Types

Dtypes are index-specific. Brute force accepts float32/float16; do not assume the int8/uint8 support of CAGRA or IVF-Flat applies to every index or metric.

Using float16 halves memory and can speed up both build and search when full float32 precision isn't needed (common for embeddings).

Raw lines 405 to 410
411### Performance Tips
4131. **Use CuPy arrays as input.** NumPy arrays work but trigger a CPU-to-GPU transfer. If your vectors are already on GPU (from a model or pipeline), pass them directly.
4152. **Tune search parameters, not just build parameters.** The biggest accuracy/speed tradeoff is at search time:
416 - CAGRA: increase `itopk_size` (default 64)
417 - IVF-Flat/IVF-PQ: increase `n_probes` (default 20)
418 - HNSW: increase `ef` (default 200)
4203. **Evaluate reduced precision, do not assume it is free.** If the chosen index supports float16,
421compare recall/ranking metrics and end-to-end performance with float32 before changing storage.
4234. **n_lists tuning for IVF indexes.** A good starting point is `sqrt(n_samples)`. Too few lists = slow search, too many = poor recall.
4255. **Batch queries.** GPU throughput scales with batch size. Searching 1000 queries at once is far more efficient than 1000 individual searches.
4276. **Reuse the Resources handle.** Create one `Resources()` object and pass it to all build/search calls — it manages CUDA streams and memory.

Performance Tips

  1. Use CuPy arrays as input. NumPy arrays work but trigger a CPU-to-GPU transfer. If your vectors are already on GPU (from a model or pipeline), pass them directly.
  1. Tune search parameters, not just build parameters. The biggest accuracy/speed tradeoff is at search time:
  2. CAGRA: increase itopk_size (default 64)
  3. IVF-Flat/IVF-PQ: increase n_probes (default 20)
  4. HNSW: increase ef (default 200)
  1. Evaluate reduced precision, do not assume it is free. If the chosen index supports float16,

compare recall/ranking metrics and end-to-end performance with float32 before changing storage.

  1. n_lists tuning for IVF indexes. A good starting point is sqrt(n_samples). Too few lists = slow search, too many = poor recall.
  1. Batch queries. GPU throughput scales with batch size. Searching 1000 queries at once is far more efficient than 1000 individual searches.
  1. Reuse the Resources handle. Create one Resources() object and pass it to all build/search calls — it manages CUDA streams and memory.
Raw lines 411 to 428
429### Memory Estimates
431- **Brute force:** `n_samples * dim * dtype_size` (full dataset)
432- **IVF-Flat:** Similar to brute force + cluster overhead
433- **IVF-PQ:** `n_samples * pq_dim * pq_bits / 8` (heavily compressed)
434- **CAGRA:** `n_samples * (dim * dtype_size + graph_degree * 4)` (dataset + graph)
436---

Memory Estimates

  • Brute force: n_samples * dim * dtype_size (full dataset)
  • IVF-Flat: Similar to brute force + cluster overhead
  • IVF-PQ: n_samples * pq_dim * pq_bits / 8 (heavily compressed)
  • CAGRA: n_samples * (dim * dtype_size + graph_degree * 4) (dataset + graph)

Raw lines 429 to 437
438## Interoperability
440- **CuPy:** Native input — zero-copy via `__cuda_array_interface__`
441- **NumPy:** Accepted as input (auto-transferred to GPU for GPU indexes, used directly for HNSW)
442- **PyTorch / TensorFlow:** Use a supported array-interface or DLPack conversion (e.g. to CuPy), preserving device, layout, ownership and stream ordering.
443- **cuDF:** Convert columns to CuPy with `.values` before passing to cuVS
444- **Faiss:** cuVS powers Faiss GPU under the hood; for direct use, cuVS gives more control
445- **Vector databases:** cuVS is integrated into Milvus, Lucene, and Kinetica

Interoperability

  • CuPy: Native input — zero-copy via __cuda_array_interface__
  • NumPy: Accepted as input (auto-transferred to GPU for GPU indexes, used directly for HNSW)
  • PyTorch / TensorFlow: Use a supported array-interface or DLPack conversion (e.g. to CuPy), preserving device, layout, ownership and stream ordering.
  • cuDF: Convert columns to CuPy with .values before passing to cuVS
  • Faiss: cuVS powers Faiss GPU under the hood; for direct use, cuVS gives more control
  • Vector databases: cuVS is integrated into Milvus, Lucene, and Kinetica
Raw lines 438 to 446
447### End-to-End RAG Pipeline Example
449```python
450import cupy as cp
451from cuvs.neighbors import cagra
453# Assume embeddings come from a model (e.g., sentence-transformers on GPU)
454document_embeddings = cp.array(embeddings, dtype=cp.float32) # (n_docs, 768)
456# Build index
457index_params = cagra.IndexParams(metric="inner_product")
458index = cagra.build(index_params, document_embeddings)
459cagra.save("doc_index.cagra", index)
461# At query time
462query_embedding = cp.array(encode("user question"), dtype=cp.float32).reshape(1, -1)
463search_params = cagra.SearchParams(itopk_size=128)
464distances, neighbors = cagra.search(search_params, index, query_embedding, k=20)
466# neighbors[0] contains the indices of the top-20 most similar documents
467top_doc_ids = cp.asnumpy(cp.asarray(neighbors)[0]) # Transfer to CPU
468```
470---

End-to-End RAG Pipeline Example

import cupy as cp
from cuvs.neighbors import cagra

# Assume embeddings come from a model (e.g., sentence-transformers on GPU)
document_embeddings = cp.array(embeddings, dtype=cp.float32)  # (n_docs, 768)

# Build index
index_params = cagra.IndexParams(metric="inner_product")
index = cagra.build(index_params, document_embeddings)
cagra.save("doc_index.cagra", index)

# At query time
query_embedding = cp.array(encode("user question"), dtype=cp.float32).reshape(1, -1)
search_params = cagra.SearchParams(itopk_size=128)
distances, neighbors = cagra.search(search_params, index, query_embedding, k=20)

# neighbors[0] contains the indices of the top-20 most similar documents
top_doc_ids = cp.asnumpy(cp.asarray(neighbors)[0])  # Transfer to CPU

Raw lines 447 to 471
472## Common Patterns

Common Patterns

Raw lines 472 to 473
474### Pattern 1: Quick ANN Search (CAGRA)
476```python
477import cupy as cp
478from cuvs.neighbors import cagra
480dataset = cp.random.rand(500_000, 128, dtype=cp.float32)
481queries = cp.random.rand(1000, 128, dtype=cp.float32)
483index = cagra.build(cagra.IndexParams(), dataset)
484distances, neighbors = cagra.search(cagra.SearchParams(), index, queries, k=10)
485```

Pattern 1: Quick ANN Search (CAGRA)

import cupy as cp
from cuvs.neighbors import cagra

dataset = cp.random.rand(500_000, 128, dtype=cp.float32)
queries = cp.random.rand(1000, 128, dtype=cp.float32)

index = cagra.build(cagra.IndexParams(), dataset)
distances, neighbors = cagra.search(cagra.SearchParams(), index, queries, k=10)
Raw lines 474 to 486
487### Pattern 2: Memory-Efficient Search (IVF-PQ)
489```python
490import cupy as cp
491from cuvs.neighbors import ivf_pq
493dataset = cp.random.rand(10_000_000, 256, dtype=cp.float32)
495# PQ compresses vectors — uses ~32x less memory than brute force
496params = ivf_pq.IndexParams(n_lists=4096, pq_bits=8, pq_dim=64)
497index = ivf_pq.build(params, dataset)
499search_params = ivf_pq.SearchParams(n_probes=100)
500distances, neighbors = ivf_pq.search(search_params, index, queries, k=10)
501```

Pattern 2: Memory-Efficient Search (IVF-PQ)

import cupy as cp
from cuvs.neighbors import ivf_pq

dataset = cp.random.rand(10_000_000, 256, dtype=cp.float32)

# PQ compresses vectors — uses ~32x less memory than brute force
params = ivf_pq.IndexParams(n_lists=4096, pq_bits=8, pq_dim=64)
index = ivf_pq.build(params, dataset)

search_params = ivf_pq.SearchParams(n_probes=100)
distances, neighbors = ivf_pq.search(search_params, index, queries, k=10)
Raw lines 487 to 502
503### Pattern 3: GPU Build, CPU Serve (CAGRA → HNSW)
505```python
506import cupy as cp
507import numpy as np
508from cuvs.neighbors import cagra, hnsw
510# Build on GPU (fast)
511dataset = cp.random.rand(1_000_000, 128, dtype=cp.float32)
512gpu_index = cagra.build(cagra.IndexParams(), dataset)
514# Convert to HNSW for CPU serving
515cpu_index = hnsw.from_cagra(hnsw.IndexParams(), gpu_index)
516hnsw.save("serving_index.hnsw", cpu_index)
518# At serving time (no GPU needed)
519loaded = hnsw.load(hnsw.IndexParams(), "serving_index.hnsw",
520 dim=128, dtype=np.float32)
521queries = np.random.rand(100, 128).astype(np.float32)
522distances, neighbors = hnsw.search(
523 hnsw.SearchParams(ef=200), loaded, queries, k=10
525```

Pattern 3: GPU Build, CPU Serve (CAGRA → HNSW)

import cupy as cp
import numpy as np
from cuvs.neighbors import cagra, hnsw

# Build on GPU (fast)
dataset = cp.random.rand(1_000_000, 128, dtype=cp.float32)
gpu_index = cagra.build(cagra.IndexParams(), dataset)

# Convert to HNSW for CPU serving
cpu_index = hnsw.from_cagra(hnsw.IndexParams(), gpu_index)
hnsw.save("serving_index.hnsw", cpu_index)

# At serving time (no GPU needed)
loaded = hnsw.load(hnsw.IndexParams(), "serving_index.hnsw",
                    dim=128, dtype=np.float32)
queries = np.random.rand(100, 128).astype(np.float32)
distances, neighbors = hnsw.search(
    hnsw.SearchParams(ef=200), loaded, queries, k=10
)
Raw lines 503 to 526
527### Pattern 4: Validate with Brute Force
529```python
530from cuvs.neighbors import brute_force, cagra
532# Ground truth
533bf_index = brute_force.build(dataset)
534gt_distances, gt_neighbors = brute_force.search(bf_index, queries, k=10)
536# Approximate
537cagra_index = cagra.build(cagra.IndexParams(), dataset)
538approx_distances, approx_neighbors = cagra.search(
539 cagra.SearchParams(), cagra_index, queries, k=10
542# Materialize once; default cuVS output containers need not be CuPy arrays.
543gt_neighbors_cpu = cp.asnumpy(cp.asarray(gt_neighbors))
544approx_neighbors_cpu = cp.asnumpy(cp.asarray(approx_neighbors))
545# Ties can admit multiple exact top-k sets; decide a tie policy for duplicate vectors.
546recall = sum(
547 len(set(gt_neighbors_cpu[i]) & set(approx_neighbors_cpu[i])) / 10
548 for i in range(len(queries))
549) / len(queries)
550print(f"Recall@10: {recall:.4f}")
551```

Pattern 4: Validate with Brute Force

from cuvs.neighbors import brute_force, cagra

# Ground truth
bf_index = brute_force.build(dataset)
gt_distances, gt_neighbors = brute_force.search(bf_index, queries, k=10)

# Approximate
cagra_index = cagra.build(cagra.IndexParams(), dataset)
approx_distances, approx_neighbors = cagra.search(
    cagra.SearchParams(), cagra_index, queries, k=10
)

# Materialize once; default cuVS output containers need not be CuPy arrays.
gt_neighbors_cpu = cp.asnumpy(cp.asarray(gt_neighbors))
approx_neighbors_cpu = cp.asnumpy(cp.asarray(approx_neighbors))
# Ties can admit multiple exact top-k sets; decide a tie policy for duplicate vectors.
recall = sum(
    len(set(gt_neighbors_cpu[i]) & set(approx_neighbors_cpu[i])) / 10
    for i in range(len(queries))
) / len(queries)
print(f"Recall@10: {recall:.4f}")
Raw lines 527 to 552
553### Pattern 5: Cosine Similarity Search
555```python
556import cupy as cp
557from cuvs.neighbors import cagra
559# Normalize embeddings to unit length
560embeddings = cp.random.rand(100_000, 768, dtype=cp.float32)
561norms = cp.linalg.norm(embeddings, axis=1, keepdims=True)
562if bool(cp.any(norms == 0)):
563 raise ValueError("Cosine similarity is undefined for zero-norm embeddings")
564embeddings_normalized = embeddings / norms
566# Use inner_product on normalized vectors = cosine similarity
567index = cagra.build(
568 cagra.IndexParams(metric="inner_product"),
569 embeddings_normalized,
572query = cp.random.rand(1, 768, dtype=cp.float32)
573query_norm = cp.linalg.norm(query)
574if float(query_norm) == 0:
575 raise ValueError("Cosine similarity is undefined for a zero-norm query")
576query_normalized = query / query_norm
578distances, neighbors = cagra.search(
579 cagra.SearchParams(), index, query_normalized, k=10
581```
583---

Pattern 5: Cosine Similarity Search

import cupy as cp
from cuvs.neighbors import cagra

# Normalize embeddings to unit length
embeddings = cp.random.rand(100_000, 768, dtype=cp.float32)
norms = cp.linalg.norm(embeddings, axis=1, keepdims=True)
if bool(cp.any(norms == 0)):
    raise ValueError("Cosine similarity is undefined for zero-norm embeddings")
embeddings_normalized = embeddings / norms

# Use inner_product on normalized vectors = cosine similarity
index = cagra.build(
    cagra.IndexParams(metric="inner_product"),
    embeddings_normalized,
)

query = cp.random.rand(1, 768, dtype=cp.float32)
query_norm = cp.linalg.norm(query)
if float(query_norm) == 0:
    raise ValueError("Cosine similarity is undefined for a zero-norm query")
query_normalized = query / query_norm

distances, neighbors = cagra.search(
    cagra.SearchParams(), index, query_normalized, k=10
)

Raw lines 553 to 584
585## Beyond Neighbors: Clustering, Distance, and Preprocessing
587cuVS also provides GPU-accelerated clustering, pairwise distance, and quantization — useful building blocks in vector search pipelines.

Beyond Neighbors: Clustering, Distance, and Preprocessing

cuVS also provides GPU-accelerated clustering, pairwise distance, and quantization — useful building blocks in vector search pipelines.

Raw lines 585 to 588
589### K-Means Clustering
591```python
592import cupy as cp
593from cuvs.cluster.kmeans import fit, predict, KMeansParams
595X = cp.random.rand(100_000, 128, dtype=cp.float32)
597params = KMeansParams(
598 n_clusters=256,
599 init_method="KMeansPlusPlus", # or "Random", "Array"
600 max_iter=300,
601 tol=1e-4,
603centroids, inertia, n_iter = fit(params, X)
604labels, inertia = predict(params, X, centroids)
605```
607For host-resident datasets, `device_buffer_samples` bounds the device staging batch in 26.08.
608This still requires enough host RAM for the NumPy input:
610```python
611import numpy as np
612from cuvs.cluster.kmeans import fit, KMeansParams
614X_host = np.random.rand(10_000_000, 128).astype(np.float32)
615params = KMeansParams(n_clusters=1000, device_buffer_samples=1_000_000)
616centroids, inertia, n_iter = fit(params, X_host)
617```

K-Means Clustering

import cupy as cp
from cuvs.cluster.kmeans import fit, predict, KMeansParams

X = cp.random.rand(100_000, 128, dtype=cp.float32)

params = KMeansParams(
    n_clusters=256,
    init_method="KMeansPlusPlus",   # or "Random", "Array"
    max_iter=300,
    tol=1e-4,
)
centroids, inertia, n_iter = fit(params, X)
labels, inertia = predict(params, X, centroids)

For host-resident datasets, device_buffer_samples bounds the device staging batch in 26.08. This still requires enough host RAM for the NumPy input:

import numpy as np
from cuvs.cluster.kmeans import fit, KMeansParams

X_host = np.random.rand(10_000_000, 128).astype(np.float32)
params = KMeansParams(n_clusters=1000, device_buffer_samples=1_000_000)
centroids, inertia, n_iter = fit(params, X_host)
Raw lines 589 to 618
619### Pairwise Distance
621```python
622from cuvs.distance import pairwise_distance
624# Supports: euclidean, l2, l1, inner_product, cosine, chebyshev,
625# canberra, hellinger, jensenshannon, kl_divergence, correlation, minkowski
626output = pairwise_distance(X, Y, metric="euclidean")
627```

Pairwise Distance

from cuvs.distance import pairwise_distance

# Supports: euclidean, l2, l1, inner_product, cosine, chebyshev,
# canberra, hellinger, jensenshannon, kl_divergence, correlation, minkowski
output = pairwise_distance(X, Y, metric="euclidean")
Raw lines 619 to 628
629### Quantization (Preprocessing)
631Quantization compresses vectors before indexing, reducing memory and often improving search throughput.
633**Scalar quantization** (float32 → int8):
634```python
635from cuvs.preprocessing.quantize import scalar
637params = scalar.QuantizerParams(quantile=0.99)
638quantizer = scalar.train(params, dataset)
639transformed = scalar.transform(quantizer, dataset) # int8
640reconstructed = scalar.inverse_transform(quantizer, transformed)
641```
643**Binary quantization** (float32 → uint8 bitpacked):
644```python
645from cuvs.preprocessing.quantize import binary
647transformed = binary.transform(dataset) # uint8
648# Use with metric="bitwise_hamming"
649```
651**Product quantization**:
652```python
653from cuvs.preprocessing.quantize import pq
655params = pq.QuantizerParams(pq_bits=8, pq_dim=16)
656quantizer = pq.build(params, dataset)
657transformed, _ = pq.transform(quantizer, dataset) # uint8
658reconstructed = pq.inverse_transform(quantizer, transformed)
659```

Quantization (Preprocessing)

Quantization compresses vectors before indexing, reducing memory and often improving search throughput.

Scalar quantization (float32 → int8):

from cuvs.preprocessing.quantize import scalar

params = scalar.QuantizerParams(quantile=0.99)
quantizer = scalar.train(params, dataset)
transformed = scalar.transform(quantizer, dataset)       # int8
reconstructed = scalar.inverse_transform(quantizer, transformed)

Binary quantization (float32 → uint8 bitpacked):

from cuvs.preprocessing.quantize import binary

transformed = binary.transform(dataset)  # uint8
# Use with metric="bitwise_hamming"

Product quantization:

from cuvs.preprocessing.quantize import pq

params = pq.QuantizerParams(pq_bits=8, pq_dim=16)
quantizer = pq.build(params, dataset)
transformed, _ = pq.transform(quantizer, dataset)        # uint8
reconstructed = pq.inverse_transform(quantizer, transformed)
Raw lines 629 to 660
661### PCA (Preprocessing)
663GPU-accelerated PCA for dimensionality reduction before indexing (added in 26.06):
665```python
666import cupy as cp
667from cuvs.preprocessing import pca
669X = cp.random.random_sample((500, 32), dtype=cp.float32)
670params = pca.Params(n_components=8, copy=True)
671result = pca.fit(params, X)
672transformed = pca.transform(params, X, result.components,
673 result.singular_vals, result.mu)
674reconstructed = pca.inverse_transform(
675 params, transformed, result.components,
676 result.singular_vals, result.mu)
677```

PCA (Preprocessing)

GPU-accelerated PCA for dimensionality reduction before indexing (added in 26.06):

import cupy as cp
from cuvs.preprocessing import pca

X = cp.random.random_sample((500, 32), dtype=cp.float32)
params = pca.Params(n_components=8, copy=True)
result = pca.fit(params, X)
transformed = pca.transform(params, X, result.components,
                            result.singular_vals, result.mu)
reconstructed = pca.inverse_transform(
    params, transformed, result.components,
    result.singular_vals, result.mu)
Raw lines 661 to 678
679### NN-Descent (k-NN Graph Construction)
681Builds an all-neighbors k-NN graph — useful as input to UMAP, t-SNE, or graph-based clustering.
683```python
684import cupy as cp
685from cuvs.neighbors import nn_descent
687dataset = cp.random.rand(100_000, 128, dtype=cp.float32)
689build_params = nn_descent.IndexParams(
690 metric="sqeuclidean",
691 graph_degree=64,
692 intermediate_graph_degree=96, # >= 1.5 * graph_degree
693 max_iterations=20,
695index = nn_descent.build(build_params, dataset)
696graph = index.graph # (n_samples, graph_degree) — the k-NN graph
697```

NN-Descent (k-NN Graph Construction)

Builds an all-neighbors k-NN graph — useful as input to UMAP, t-SNE, or graph-based clustering.

import cupy as cp
from cuvs.neighbors import nn_descent

dataset = cp.random.rand(100_000, 128, dtype=cp.float32)

build_params = nn_descent.IndexParams(
    metric="sqeuclidean",
    graph_degree=64,
    intermediate_graph_degree=96,   # >= 1.5 * graph_degree
    max_iterations=20,
)
index = nn_descent.build(build_params, dataset)
graph = index.graph  # (n_samples, graph_degree) — the k-NN graph
Raw lines 679 to 697