Skip to content

Commit 4f54494

Browse files
committed
refactor(vibe): integrate OOP scalar quantizer and streamline preset grid
- Update DegANN in module.py to calibrate and reuse ScalarQuantizerInt8 instance for base vectors and queries - Support automatic query feature quantization via base-calibrated quantizer - Clean up unused preset parameters in presets.py - Normalize query_dtype validation and logging across main.py and module.py"
1 parent f0e3315 commit 4f54494

3 files changed

Lines changed: 52 additions & 38 deletions

File tree

examples/vibe/main.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ def main():
194194
print(f"\n--- System & Distance Config ---")
195195
print(f"Metric: {metric_str} ({metric_enum.name}), Dimensions: {dims}")
196196
print(f"Vector Space Type: FloatSpace ({instruction_set})")
197-
print(f"Query Dtype: {args.query_dtype.upper()}")
197+
print(f"Query Dtype: {args.query_dtype.upper() if args.query_dtype else 'FROM CONFIG.YML'}")
198198
print(f"Hardware AVX: {avx_usable()}, AVX-512: {avx512_usable()}")
199199

200200
# Linear scan baseline
@@ -225,15 +225,13 @@ def main():
225225
threads=args.build_threads,
226226
query_dtype=query_dtype,
227227
)
228-
print(
229-
f"\n--- [{cfg_idx + 1}/{len(configs_to_run)}] Fitting / Loading Index: K={k}, Opt={opt_target}, Threads={args.build_threads}, QueryType={query_dtype}, PruneNonRNG={is_pruned} ---"
230-
)
228+
print(f"\n--- [{cfg_idx + 1}/{len(configs_to_run)}] Processing Graph Configuration ---")
231229
adapter.fit(base_vecs)
232230

233231
deglib.analysis.analyze_graph(adapter.graph)
234232

235-
# Determine rerank factors to evaluate
236-
if query_dtype == "int8":
233+
# Determine rerank factors to evaluate (for all quantized formats)
234+
if query_dtype != "float32":
237235
factors_to_eval = cfg.get("rerank_size_factors", [1.0, 1.2, 1.5, 2.0])
238236
else:
239237
factors_to_eval = [1.0]

examples/vibe/module.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ class BaseANN:
2929
"streamingdata": deglib.builder.OptimizationTarget.StreamingData,
3030
}
3131

32+
_VALID_QUERY_DTYPES = ("float32", "int8")
33+
3234

3335
class DegANN(BaseANN):
3436
"""
@@ -38,7 +40,7 @@ class DegANN(BaseANN):
3840
- k: Degree / edges per vertex (e.g. 16, 24, 30, 40, 48)
3941
- opt_target: 'HighLID' (for Cosine/IP/Normalized), 'LowLID' (for Euclidean), or 'StreamingData'
4042
- threads: CPU threads during graph build
41-
- query_dtype: Dtype used to search graph ('float32', 'float16', 'int8')
43+
- query_dtype: Dtype used to search graph ('float32', 'int8')
4244
- prune_non_rng: Optional MRNG edge pruning
4345
"""
4446

@@ -58,14 +60,14 @@ def __init__(
5860
self.needs_normalization: bool = (self.metric == "cosine")
5961

6062
self.k = int(k)
61-
self.opt_target = str(opt_target)
6263
self.extend_k = 60
6364
self.extend_eps = 0.1
65+
self.opt_target = str(opt_target)
6466
self.prune_non_rng = bool(prune_non_rng)
6567
self.threads = int(threads)
6668
self.query_dtype = query_dtype.lower().strip()
67-
if self.query_dtype not in ("float32", "int8"):
68-
raise ValueError(f"Unsupported dtype '{self.query_dtype}'. Choose from: 'float32', 'int8'")
69+
if self.query_dtype not in _VALID_QUERY_DTYPES:
70+
raise ValueError(f"Unsupported dtype '{self.query_dtype}'. Choose from: {_VALID_QUERY_DTYPES}")
6971
self.search_eps: float = 0.1
7072
self.rerank_size_factor: float = 1.0
7173

@@ -75,6 +77,7 @@ def __init__(
7577
self.metric_enum = self.query_metric_enum
7678
self.opt_enum = self._map_opt_target(self.opt_target)
7779

80+
self.quantizer = None
7881
self.graph: deglib.ReadOnlyGraph | None = None
7982
self.original_features_fp16: np.ndarray | None = None
8083
self.rerank_space_fp16: deglib.FloatSpace | None = None
@@ -163,25 +166,46 @@ def _resolve_graph_path(self, n_vectors: int, dims: int) -> Path:
163166
filename = f"{dims}D_{build_metric_str}_K{self.k}_AddK{self.extend_k}Eps{self.extend_eps:.1f}_{self.opt_target}_FLAS.deg"
164167
return deg_dir / filename
165168

169+
def _init_and_calibrate_quantizer(self, X_f32: np.ndarray) -> np.ndarray | None:
170+
"""Calibrates the appropriate quantizer on base vectors and returns the quantized features."""
171+
if self.query_dtype == "float32":
172+
self.quantizer = None
173+
return None
174+
elif self.query_dtype == "int8":
175+
self.quantizer = deglib.optimization.make_scalar_quantizer_int8(X_f32)
176+
return self.quantizer.quantize(X_f32, num_threads=self.threads)
177+
else:
178+
raise ValueError(f"Unknown query_dtype: {self.query_dtype}")
179+
166180
def fit(self, X: np.ndarray):
167181
"""Builds or loads the DEG graph for corpus X, applies optional pruning and quantizes for search."""
168182
X_f32 = self._prepare_input(X)
169183
n_vectors, dims = X_f32.shape
170184

171-
# Store original features in FP16 for fast and memory-efficient reranking
172-
if self.query_dtype == "int8":
173-
self.original_features_fp16 = deglib.distances.floats_to_fp16(X_f32)
174-
self.rerank_space_fp16 = deglib.FloatSpace.create(dim=dims, metric=_METRIC_MAP[(self.is_l2, "float16")])
175-
176185
# Automatically resolve graph save/load path
177186
graph_path = self._resolve_graph_path(n_vectors, dims)
178187

188+
print(
189+
f"Index Configuration: K={self.k}, ExtendK={self.extend_k}, ExtendEps={self.extend_eps:.2f}, "
190+
f"Opt={self.opt_target}, Threads={self.threads}, QueryType={self.query_dtype}, "
191+
f"PruneNonRNG={self.prune_non_rng}"
192+
)
193+
194+
# Always calibrate quantizer on base dataset (even when loading cached graph)
195+
quantized_features = self._init_and_calibrate_quantizer(X_f32)
196+
197+
# Store original features in FP16 for fast and memory-efficient reranking if quantized
198+
if self.query_dtype != "float32":
199+
self.original_features_fp16 = deglib.distances.floats_to_fp16(X_f32)
200+
self.rerank_space_fp16 = deglib.FloatSpace.create(dim=dims, metric=_METRIC_MAP[(self.is_l2, "float16")])
201+
179202
# 1. Obtain base graph in FP32 (either by loading cached file or by building)
180203
if graph_path and graph_path.is_file():
181204
load_fn = deglib.load_mutable_graph if self.prune_non_rng else deglib.load_readonly_graph
182205
print(f"Loading cached DEG graph from: {graph_path}")
183206
graph = load_fn(str(graph_path))
184207
else:
208+
print(f"No cached graph found at {graph_path}. Building from scratch...")
185209
graph = self._build_graph(X_f32, graph_path=graph_path)
186210

187211
# 2. Optional MRNG edge pruning on SizeBoundedGraph (MutableGraph)
@@ -193,11 +217,10 @@ def fit(self, X: np.ndarray):
193217
graph = mut_graph
194218

195219
# 3. Finalize ReadOnlyGraph with query_dtype features
196-
if self.query_dtype == "int8":
197-
print(f"Finalizing ReadOnlyGraph with INT8 (Metric: {self.query_metric_enum.name})...")
198-
int8_features = deglib.optimization.quantize_int8(X_f32, num_threads=self.threads)
220+
if quantized_features is not None:
221+
print(f"Finalizing ReadOnlyGraph with {self.query_dtype.upper()} (Metric: {self.query_metric_enum.name})...")
199222
target_space = deglib.FloatSpace.create(dim=dims, metric=self.query_metric_enum)
200-
self.graph = graph.to_readonly(target_space, int8_features)
223+
self.graph = graph.to_readonly(target_space, quantized_features)
201224
else:
202225
self.graph = graph if not graph.is_mutable() else graph.to_readonly()
203226

@@ -206,14 +229,23 @@ def set_query_arguments(self, search_eps: float, rerank_size_factor: float = 1.0
206229
self.search_eps = float(search_eps)
207230
self.rerank_size_factor = float(rerank_size_factor)
208231

232+
def _prepare_query_vectors(self, q_f32: np.ndarray, threads: int = 1) -> np.ndarray:
233+
"""Transforms query vectors using the base-calibrated quantizer."""
234+
if self.query_dtype == "float32":
235+
return q_f32
236+
elif self.quantizer is not None:
237+
return self.quantizer.quantize(q_f32, num_threads=threads)
238+
else:
239+
return q_f32
240+
209241
def _search_and_rerank(self, queries: np.ndarray, n: int, threads: int = 1) -> np.ndarray:
210242
"""Unified search and optional FP16 reranking engine."""
211243
if self.graph is None:
212244
raise RuntimeError("Index not fitted. Call fit(X) first.")
213245

214246
fetch_k = max(n, int(round(n * self.rerank_size_factor)))
215247
q_f32 = self._prepare_input(queries)
216-
query_mat = deglib.optimization.quantize_int8(q_f32, num_threads=threads) if self.query_dtype == "int8" else q_f32
248+
query_mat = self._prepare_query_vectors(q_f32, threads=threads)
217249
res = self.graph.search(query_mat, eps=self.search_eps, k=fetch_k, threads=threads, return_distances=False, unsorted=True)
218250
indices = res[0] if isinstance(res, tuple) else res
219251

examples/vibe/presets.py

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,7 @@ def load_vibe_config(config_path: Path | None = None) -> Dict[str, Any]:
1919

2020
return {
2121
"k_list": args.get("k", [30]),
22-
"extend_k_list": args.get("extend_k", [60]),
23-
"extend_eps_list": args.get("extend_eps", args.get("build_eps", [0.1])),
2422
"opt_target_list": args.get("opt_target", ["HighLID"]),
25-
"improve_k_list": args.get("improve_k", [0]),
26-
"improve_eps_list": args.get("improve_eps", [0.0]),
2723
"prune_non_rng_list": args.get("prune_non_rng", [False]),
2824
"threads_list": args.get("threads", [1]),
2925
"query_dtype_list": args.get("query_dtype", ["float32"]),
@@ -53,25 +49,17 @@ def get_config_grid_presets(dataset_key: str) -> List[Dict[str, Any]]:
5349
resolved_opt_targets.append(target_name)
5450

5551
grid = []
56-
# Cartesian product: iterate over opt_target first, then all k, improvement, pruning and query_dtype variants
57-
for opt_target, k, extend_k, extend_eps, imp_k, imp_eps, prune_non_rng, q_dtype in itertools.product(
52+
# Cartesian product: iterate over opt_target first, then all k, pruning and query_dtype variants
53+
for opt_target, k, prune_non_rng, q_dtype in itertools.product(
5854
resolved_opt_targets,
5955
cfg["k_list"],
60-
cfg["extend_k_list"],
61-
cfg["extend_eps_list"],
62-
cfg["improve_k_list"],
63-
cfg["improve_eps_list"],
6456
cfg["prune_non_rng_list"],
6557
cfg["query_dtype_list"],
6658
):
6759
grid.append(
6860
{
6961
"k": k,
70-
"extend_k": extend_k,
71-
"extend_eps": extend_eps,
7262
"optimization_target": opt_target,
73-
"improve_k": imp_k,
74-
"improve_eps": imp_eps,
7563
"prune_non_rng": prune_non_rng,
7664
"query_dtype": q_dtype,
7765
"anns_k": 100,
@@ -90,13 +78,9 @@ def get_default_config_preset(dataset_key: str) -> Dict[str, Any]:
9078
if presets
9179
else {
9280
"k": 30,
93-
"extend_k": 60,
94-
"extend_eps": 0.1,
9581
"optimization_target": "LowLID"
9682
if ("euclidean" in dataset_key.lower() or "agnews" in dataset_key.lower())
9783
else "HighLID",
98-
"improve_k": 0,
99-
"improve_eps": 0.0,
10084
"prune_non_rng": False,
10185
"query_dtype": "float32",
10286
"anns_k": 100,

0 commit comments

Comments
 (0)