@@ -29,6 +29,8 @@ class BaseANN:
2929 "streamingdata" : deglib .builder .OptimizationTarget .StreamingData ,
3030}
3131
32+ _VALID_QUERY_DTYPES = ("float32" , "int8" )
33+
3234
3335class 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
0 commit comments