Skip to content

Commit 35d9858

Browse files
authored
Add more customization to neighbors (#381)
* add algorithm_kwds to neighbors * fix docs * add tests * add release note
1 parent 7656d31 commit 35d9858

3 files changed

Lines changed: 103 additions & 13 deletions

File tree

docs/release-notes/0.12.7.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
```{rubric} Features
44
```
5+
* Adds `algorithm_kwds` to `pp.neighbors` & `pp.bbknn` to fine-tune `ivfflat`, `ivfpq` & `nn_descent` {pr}`381` {smaller}`S Dicks`
56

67
```{rubric} Performance
78
```

src/rapids_singlecell/preprocessing/_neighbors.py

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,8 +73,10 @@ def _brute_knn(
7373
X: cp_sparse.spmatrix | cp.ndarray,
7474
Y: cp_sparse.spmatrix | cp.ndarray,
7575
k: int,
76+
*,
7677
metric: _Metrics,
7778
metric_kwds: Mapping,
79+
algorithm_kwds: Mapping,
7880
) -> tuple[cp.ndarray, cp.ndarray]:
7981
from cuml.neighbors import NearestNeighbors
8082

@@ -91,7 +93,13 @@ def _brute_knn(
9193

9294

9395
def _cagra_knn(
94-
X: cp.ndarray, Y: cp.ndarray, k: int, metric: _Metrics, metric_kwds: Mapping
96+
X: cp.ndarray,
97+
Y: cp.ndarray,
98+
k: int,
99+
*,
100+
metric: _Metrics,
101+
metric_kwds: Mapping,
102+
algorithm_kwds: Mapping,
95103
) -> tuple[cp.ndarray, cp.ndarray]:
96104
if not _cuvs_switch():
97105
try:
@@ -135,8 +143,20 @@ def _cagra_knn(
135143
return neighbors, distances
136144

137145

146+
def _compute_nlist(N):
147+
base = math.sqrt(N)
148+
next_pow2 = 2 ** math.ceil(math.log2(base))
149+
return int(next_pow2 * 2)
150+
151+
138152
def _ivf_flat_knn(
139-
X: cp.ndarray, Y: cp.ndarray, k: int, metric: _Metrics, metric_kwds: Mapping
153+
X: cp.ndarray,
154+
Y: cp.ndarray,
155+
k: int,
156+
*,
157+
metric: _Metrics,
158+
metric_kwds: Mapping,
159+
algorithm_kwds: Mapping,
140160
) -> tuple[cp.ndarray, cp.ndarray]:
141161
if not _cuvs_switch():
142162
from pylibraft.neighbors import ivf_flat
@@ -151,12 +171,16 @@ def _ivf_flat_knn(
151171
build_kwargs = {} # cuvs does not need handle/resources
152172
search_kwargs = {}
153173

154-
n_lists = int(math.sqrt(X.shape[0]))
174+
# Extract n_lists and nprobes from algorithm_kwds, with defaults
175+
n_lists = algorithm_kwds.get("n_lists", _compute_nlist(X.shape[0]))
176+
n_probes = algorithm_kwds.get("n_probes", 20)
177+
print(f"n_lists: {n_lists}, n_probes: {n_probes}")
155178
index_params = ivf_flat.IndexParams(n_lists=n_lists, metric=metric)
156179
index = ivf_flat.build(index_params, X, **build_kwargs)
157-
distances, neighbors = ivf_flat.search(
158-
ivf_flat.SearchParams(), index, Y, k, **search_kwargs
159-
)
180+
181+
# Create SearchParams with nprobes if provided
182+
search_params = ivf_flat.SearchParams(n_probes=n_probes)
183+
distances, neighbors = ivf_flat.search(search_params, index, Y, k, **search_kwargs)
160184

161185
if resources is not None:
162186
resources.sync()
@@ -168,7 +192,13 @@ def _ivf_flat_knn(
168192

169193

170194
def _ivf_pq_knn(
171-
X: cp.ndarray, Y: cp.ndarray, k: int, metric: _Metrics, metric_kwds: Mapping
195+
X: cp.ndarray,
196+
Y: cp.ndarray,
197+
k: int,
198+
*,
199+
metric: _Metrics,
200+
metric_kwds: Mapping,
201+
algorithm_kwds: Mapping,
172202
) -> tuple[cp.ndarray, cp.ndarray]:
173203
if not _cuvs_switch():
174204
from pylibraft.neighbors import ivf_pq
@@ -183,12 +213,16 @@ def _ivf_pq_knn(
183213
build_kwargs = {}
184214
search_kwargs = {}
185215

186-
n_lists = int(math.sqrt(X.shape[0]))
216+
# Extract n_lists and nprobes from algorithm_kwds, with defaults
217+
n_lists = algorithm_kwds.get("n_lists", _compute_nlist(X.shape[0]))
218+
n_probes = algorithm_kwds.get("n_probes", 20)
219+
187220
index_params = ivf_pq.IndexParams(n_lists=n_lists, metric=metric)
188221
index = ivf_pq.build(index_params, X, **build_kwargs)
189-
distances, neighbors = ivf_pq.search(
190-
ivf_pq.SearchParams(), index, Y, k, **search_kwargs
191-
)
222+
print(f"n_lists: {n_lists}, n_probes: {n_probes}")
223+
# Create SearchParams with nprobes if provided
224+
search_params = ivf_pq.SearchParams(n_probes=n_probes)
225+
distances, neighbors = ivf_pq.search(search_params, index, Y, k, **search_kwargs)
192226
if resources is not None:
193227
resources.sync()
194228

@@ -199,7 +233,13 @@ def _ivf_pq_knn(
199233

200234

201235
def _nn_descent_knn(
202-
X: cp.ndarray, Y: cp.ndarray, k: int, metric: _Metrics, metric_kwds: Mapping
236+
X: cp.ndarray,
237+
Y: cp.ndarray,
238+
k: int,
239+
*,
240+
metric: _Metrics,
241+
metric_kwds: Mapping,
242+
algorithm_kwds: Mapping,
203243
) -> tuple[cp.ndarray, cp.ndarray]:
204244
from cuvs import __version__ as cuvs_version
205245

@@ -210,8 +250,13 @@ def _nn_descent_knn(
210250
)
211251
from cuvs.neighbors import nn_descent
212252

253+
# Extract intermediate_graph_degree from algorithm_kwds, with default
254+
intermediate_graph_degree = algorithm_kwds.get("intermediate_graph_degree", None)
255+
213256
idxparams = nn_descent.IndexParams(
214-
graph_degree=k, metric="sqeuclidean" if metric == "euclidean" else metric
257+
graph_degree=k,
258+
intermediate_graph_degree=intermediate_graph_degree,
259+
metric="sqeuclidean" if metric == "euclidean" else metric,
215260
)
216261
idx = nn_descent.build(
217262
idxparams,
@@ -392,6 +437,7 @@ def neighbors(
392437
algorithm: _Algorithms = "brute",
393438
metric: _Metrics = "euclidean",
394439
metric_kwds: Mapping[str, Any] = MappingProxyType({}),
440+
algorithm_kwds: Mapping[str, Any] = MappingProxyType({}),
395441
key_added: str | None = None,
396442
copy: bool = False,
397443
) -> AnnData | None:
@@ -437,6 +483,16 @@ def neighbors(
437483
A known metric's name or a callable that returns a distance.
438484
metric_kwds
439485
Options for the metric.
486+
algorithm_kwds
487+
Options for the algorithm. For 'ivfflat' and 'ivfpq' algorithms, the following
488+
parameters can be specified:
489+
* 'n_lists': Number of inverted lists for IVF indexing. Default is 2 * next_power_of_2(sqrt(n_samples)).
490+
* 'n_probes': Number of lists to probe during search. Default is 20. Higher values
491+
increase accuracy but reduce speed.
492+
For 'nn_descent' algorithm, the following parameters can be specified:
493+
* 'intermediate_graph_degree': The degree of the intermediate graph. Default is None.
494+
It is recommended to set it to `>= 1.5 * n_neighbors`.
495+
440496
key_added
441497
If not specified, the neighbors data is stored in .uns['neighbors'],
442498
distances and connectivities are stored in .obsp['distances'] and
@@ -484,6 +540,7 @@ def neighbors(
484540
k=n_neighbors,
485541
metric=metric,
486542
metric_kwds=metric_kwds,
543+
algorithm_kwds=algorithm_kwds,
487544
)
488545

489546
n_nonzero = n_obs * n_neighbors
@@ -516,6 +573,7 @@ def neighbors(
516573
random_state=random_state,
517574
metric=metric,
518575
**({"metric_kwds": metric_kwds} if metric_kwds else {}),
576+
**({"algorithm_kwds": algorithm_kwds} if algorithm_kwds else {}),
519577
**({"use_rep": use_rep} if use_rep is not None else {}),
520578
**({"n_pcs": n_pcs} if n_pcs is not None else {}),
521579
)
@@ -543,6 +601,7 @@ def bbknn(
543601
algorithm: _Algorithms_bbknn = "brute",
544602
metric: _Metrics = "euclidean",
545603
metric_kwds: Mapping[str, Any] = MappingProxyType({}),
604+
algorithm_kwds: Mapping[str, Any] = MappingProxyType({}),
546605
trim: int | None = None,
547606
key_added: str | None = None,
548607
copy: bool = False,
@@ -588,6 +647,13 @@ def bbknn(
588647
A known metric's name or a callable that returns a distance.
589648
metric_kwds
590649
Options for the metric.
650+
algorithm_kwds
651+
Options for the algorithm. For 'ivfflat' and 'ivfpq' algorithms, the following
652+
parameters can be specified:
653+
654+
* 'n_lists': Number of inverted lists for IVF indexing. Default is 2 * next_power_of_2(sqrt(n_samples)).
655+
* 'nprobes': Number of lists to probe during search. Default is 1. Higher values
656+
increase accuracy but reduce speed.
591657
trim
592658
Trim the neighbours of each cell to these many top connectivities.
593659
May help with population independence and improve the tidiness of clustering.
@@ -660,6 +726,7 @@ def bbknn(
660726
k=neighbors_within_batch,
661727
metric=metric,
662728
metric_kwds=metric_kwds,
729+
algorithm_kwds=algorithm_kwds,
663730
)
664731

665732
col_range = cp.arange(
@@ -705,6 +772,7 @@ def bbknn(
705772
metric=metric,
706773
trim=trim,
707774
**({"metric_kwds": metric_kwds} if metric_kwds else {}),
775+
**({"algorithm_kwds": algorithm_kwds} if algorithm_kwds else {}),
708776
**({"use_rep": use_rep} if use_rep is not None else {}),
709777
**({"n_pcs": n_pcs} if n_pcs is not None else {}),
710778
)

tests/test_neighbors.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ def test_algo(algo):
4646
neighbors(adata, n_neighbors=5, algorithm=algo)
4747

4848

49+
def test_nn_descent_intermediate_graph_degree():
50+
adata = pbmc68k_reduced()
51+
neighbors(
52+
adata,
53+
n_neighbors=5,
54+
algorithm="nn_descent",
55+
algorithm_kwds={"intermediate_graph_degree": 10},
56+
)
57+
58+
59+
@pytest.mark.parametrize("algo", ["ivfflat", "ivfpq"])
60+
def test_ivf_algorithm_kwds(algo):
61+
adata = pbmc68k_reduced()
62+
neighbors(
63+
adata,
64+
n_neighbors=5,
65+
algorithm=algo,
66+
algorithm_kwds={"n_lists": 10, "n_probes": 10},
67+
)
68+
69+
4970
@pytest.mark.parametrize("algo", ["nn_descent", "ivfpq"])
5071
def test_indices_approx_nn(algo):
5172
adata = pbmc68k_reduced()

0 commit comments

Comments
 (0)