1use std::{
7 io::{Seek, SeekFrom, Write},
8 marker::PhantomData,
9 time::Instant,
10};
11
12use diskann::{error::IntoANNResult, utils::VectorRepr, ANNError, ANNResult};
13use diskann_providers::{
14 storage::{StorageReadProvider, StorageWriteProvider},
15 utils::{load_metadata_from_file, BridgeErr, ParallelIteratorInPool, RayonThreadPoolRef},
16};
17use diskann_utils::{io::Metadata, views};
18use rayon::iter::IndexedParallelIterator;
19use tracing::info;
20
21use crate::{
22 build::chunking::{
23 checkpoint::Progress,
24 continuation::{process_while_resource_is_available, ChunkingConfig},
25 },
26 storage::quant::compressor::{CompressionStage, QuantCompressor},
27};
28
29#[derive(Clone, Debug)]
35pub struct GeneratorContext {
36 pub offset: usize,
38 pub compressed_data_path: String,
40}
41
42impl GeneratorContext {
43 pub fn new(offset: usize, compressed_data_path: String) -> Self {
44 Self {
45 offset,
46 compressed_data_path,
47 }
48 }
49}
50
51pub struct QuantDataGenerator<T, Q>
55where
56 T: Copy + VectorRepr,
57 Q: QuantCompressor<T>,
58{
59 pub quantizer: Q,
60 pub data_path: String, pub context: GeneratorContext, phantom: PhantomData<T>,
63}
64
65impl<T, Q> QuantDataGenerator<T, Q>
66where
67 T: Copy + VectorRepr,
68 Q: QuantCompressor<T>,
69{
70 pub fn new(
71 data_path: String,
72 context: GeneratorContext,
73 quantizer_context: &Q::CompressorContext,
74 ) -> ANNResult<Self> {
75 let stage = match context.offset {
76 0 => CompressionStage::Start,
77 _ => CompressionStage::Resume,
78 };
79 let quantizer = Q::new_at_stage(stage, quantizer_context)?;
80 Ok(Self {
81 data_path,
82 context,
83 quantizer,
84 phantom: PhantomData,
85 })
86 }
87
88 pub fn generate_data<Storage>(
103 &self,
104 storage_provider: &Storage, pool: RayonThreadPoolRef<'_>, chunking_config: &ChunkingConfig, ) -> ANNResult<Progress>
108 where
109 Storage: StorageReadProvider + StorageWriteProvider,
110 {
111 let timer = Instant::now();
112
113 let metadata = load_metadata_from_file(storage_provider, &self.data_path)?;
114 let (num_points, dim) = metadata.into_dims();
115
116 self.validate_params(num_points, storage_provider)?;
117
118 let offset = self.context.offset;
119 let compressed_path = self.context.compressed_data_path.as_str();
120
121 if offset == 0 && storage_provider.exists(compressed_path) {
122 storage_provider.delete(compressed_path)?;
123 }
124
125 info!("Generating quantized data for {}", compressed_path);
126
127 let data_reader = &mut storage_provider.open_reader(&self.data_path)?;
128
129 let mut compressed_data_writer = if offset > 0 {
131 storage_provider.open_writer(compressed_path)?
132 } else {
133 let mut sp = storage_provider.create_for_write(compressed_path)?;
134 Metadata::new(num_points, self.quantizer.compressed_bytes())?.write(&mut sp)?;
136 sp
137 };
138
139 data_reader.seek(SeekFrom::Start(
141 (size_of::<i32>() * 2 + offset * dim * size_of::<T>()) as u64,
142 ))?;
143
144 let compressed_size = self.quantizer.compressed_bytes();
145 let max_block_size = chunking_config.data_compression_chunk_vector_count;
146 let num_remaining = num_points - offset;
147
148 let block_size = std::cmp::min(num_points, max_block_size);
149 let num_blocks =
150 num_remaining / block_size + !num_remaining.is_multiple_of(block_size) as usize;
151
152 info!(
153 "Compressing with block size {}, num_remaining {}, num_blocks {}, offset {}, num_points {}",
154 block_size, num_remaining, num_blocks, offset, num_points
155 );
156
157 let mut compressed_buffer = vec![0_u8; block_size * compressed_size];
158
159 let action = |block_index| -> ANNResult<()> {
161 let start_index: usize = offset + block_index * block_size;
162 let end_index: usize = std::cmp::min(start_index + block_size, num_points);
163 let cur_block_size: usize = end_index - start_index;
164
165 let block_compressed_base = &mut compressed_buffer[..cur_block_size * compressed_size];
166
167 let raw_block: Vec<T> =
168 diskann::utils::read_exact_into(data_reader, cur_block_size * dim)?;
169
170 let full_dim = T::full_dimension(&raw_block[..dim]).into_ann_result()?; let mut block_data: Vec<f32> = vec![f32::default(); cur_block_size * full_dim];
173 for (v, dst) in raw_block
174 .chunks_exact(dim)
175 .zip(block_data.chunks_exact_mut(full_dim))
176 {
177 T::as_f32_into(v, dst).into_ann_result()?;
178 }
179
180 const BATCH_SIZE: usize = 128;
189
190 let mut compressed_block = views::MutMatrixView::try_from(
193 block_compressed_base,
194 cur_block_size,
195 compressed_size,
196 )
197 .bridge_err()?;
198 let base_block =
199 views::MatrixView::try_from(&block_data, cur_block_size, full_dim).bridge_err()?;
200 base_block
201 .par_window_iter(BATCH_SIZE)
202 .zip_eq(compressed_block.par_window_iter_mut(BATCH_SIZE))
203 .try_for_each_in_pool(pool, |(src, dst)| self.quantizer.compress(src, dst))?;
204
205 let write_offset = start_index * compressed_size + std::mem::size_of::<i32>() * 2;
206 compressed_data_writer.seek(SeekFrom::Start(write_offset as u64))?;
207 compressed_data_writer.write_all(block_compressed_base)?;
208 compressed_data_writer.flush()?;
209 Ok(())
210 };
211
212 let progress = process_while_resource_is_available(
213 action,
214 0..num_blocks,
215 chunking_config.continuation_checker.clone_box(),
216 )?
217 .map(|processed| processed * block_size + offset);
218
219 info!(
220 "Quant data generation took {} seconds",
221 timer.elapsed().as_secs_f64()
222 );
223
224 Ok(progress)
225 }
226
227 fn validate_params<Storage: StorageReadProvider + StorageWriteProvider>(
228 &self,
229 num_points: usize,
230 storage_provider: &Storage,
231 ) -> ANNResult<()> {
232 if self.context.offset > num_points {
233 return Err(ANNError::log_pq_error(
235 "Error: offset for compression is more than number of points",
236 ));
237 }
238
239 let compressed_path = &self.context.compressed_data_path;
240
241 if self.context.offset > 0 {
242 if !storage_provider.exists(compressed_path) {
243 return Err(ANNError::log_file_not_found_error(format!(
244 "Error: Generator expected compressed file {compressed_path} but did not find it."
245 )));
246 }
247 let expected_length = self.quantizer.compressed_bytes() * self.context.offset
248 + std::mem::size_of::<i32>() * 2;
249 let existing_length =
250 storage_provider.get_length(&self.context.compressed_data_path)?;
251
252 if existing_length != expected_length as u64 {
253 return Err(ANNError::log_pq_error(format_args!(
255 "Error: compressed data file length {existing_length} does not match expected length {expected_length}."
256 )));
257 }
258 }
259
260 Ok(())
261 }
262}
263
264#[cfg(test)]
269mod generator_tests {
270 use std::{
271 io::BufReader,
272 sync::{Arc, RwLock},
273 };
274
275 use diskann::utils::read_exact_into;
276 use diskann_providers::storage::VirtualStorageProvider;
277 use diskann_providers::utils::{create_thread_pool_for_test, save_bytes};
278 use diskann_utils::{
279 io::{write_bin, Metadata},
280 views::MatrixView,
281 };
282 use rstest::rstest;
283 use vfs::{FileSystem, MemoryFS};
284
285 use super::*;
286 use crate::build::chunking::continuation::{
287 ContinuationGrant, ContinuationTrackerTrait, NaiveContinuationTracker,
288 };
289
290 pub struct DummyCompressor {
291 pub output_dim: u32,
292 pub code: Vec<u8>,
293 }
294 impl DummyCompressor {
295 pub fn new(output_dim: u32) -> Self {
296 Self {
297 output_dim,
298 code: (0..output_dim).map(|x| (x % 256) as u8).collect(),
299 }
300 }
301 }
302 impl QuantCompressor<f32> for DummyCompressor {
303 type CompressorContext = u32;
304
305 fn new_at_stage(
306 _stage: CompressionStage,
307 context: &Self::CompressorContext,
308 ) -> ANNResult<Self> {
309 Ok(Self::new(*context))
310 }
311
312 fn compress(
313 &self,
314 _vector: views::MatrixView<f32>,
315 mut output: views::MutMatrixView<u8>,
316 ) -> ANNResult<()> {
317 output
318 .row_iter_mut()
319 .for_each(|r| r.copy_from_slice(&self.code));
320 Ok(())
321 }
322
323 fn compressed_bytes(&self) -> usize {
324 self.output_dim as usize
325 }
326 }
327
328 fn create_test_data(num_points: usize, dim: usize) -> Vec<f32> {
329 let mut data = Vec::new();
330
331 for i in 0..num_points {
333 for j in 0..dim {
334 data.push((i * dim + j) as f32);
335 }
336 }
337
338 data
339 }
340
341 struct MockStopContinuationChecker {
343 count: Arc<RwLock<usize>>,
344 stop_count: usize,
345 }
346
347 impl Clone for MockStopContinuationChecker {
348 fn clone(&self) -> Self {
349 MockStopContinuationChecker {
350 count: self.count.clone(),
351 stop_count: self.stop_count,
352 }
353 }
354 }
355
356 impl ContinuationTrackerTrait for MockStopContinuationChecker {
357 fn get_continuation_grant(&self) -> ContinuationGrant {
358 let mut count = self.count.write().unwrap();
359 *count += 1;
360 if !(*count).is_multiple_of(self.stop_count) {
361 ContinuationGrant::Continue
362 } else {
363 ContinuationGrant::Stop
364 }
365 }
366 }
367
368 fn generate_data_and_compressed(
369 num_points: usize,
370 dim: usize,
371 offset: usize,
372 output_dim: u32,
373 ) -> ANNResult<(VirtualStorageProvider<MemoryFS>, String, String)> {
374 let storage_provider = VirtualStorageProvider::new_memory();
375 storage_provider
376 .filesystem()
377 .create_dir("/test_data")
378 .expect("Could not create test directory");
379
380 let data_path = "/test_data/test_data.bin".to_string();
381 let compressed_path = "/test_data/test_compressed.bin".to_string();
382
383 let data = create_test_data(num_points, dim);
385 let view = MatrixView::try_from(data.as_slice(), num_points, dim).unwrap();
386 write_bin(
387 view,
388 &mut storage_provider.create_for_write(data_path.as_str())?,
389 )?;
390
391 if offset > 0 {
392 let code = (0..output_dim).map(|x| (x % 256) as u8).collect::<Vec<_>>(); let mut buffer = vec![0_u8; offset * output_dim as usize];
396 buffer
397 .chunks_exact_mut(output_dim as usize)
398 .for_each(|bf| bf.copy_from_slice(code.as_slice()));
399 let _ = save_bytes(
400 &mut storage_provider.create_for_write(compressed_path.as_str())?,
401 buffer.as_slice(),
402 num_points,
403 output_dim as usize,
404 0,
405 )?;
406 }
407
408 Ok((storage_provider, data_path, compressed_path))
409 }
410
411 fn create_and_call_generator<F: vfs::FileSystem>(
412 offset: usize,
413 compressed_path: String,
414 storage_provider: &VirtualStorageProvider<F>,
415 data_path: String,
416 output_dim: u32,
417 chunking_config: &ChunkingConfig,
418 ) -> (
419 QuantDataGenerator<f32, DummyCompressor>,
420 Result<Progress, ANNError>,
421 ) {
422 let pool: diskann_providers::utils::RayonThreadPool = create_thread_pool_for_test();
423 let context = GeneratorContext::new(offset, compressed_path.clone());
425 let generator = QuantDataGenerator::<f32, DummyCompressor>::new(
426 data_path.clone(),
427 context,
428 &output_dim,
429 )
430 .unwrap();
431 let result = generator.generate_data(storage_provider, pool.as_ref(), chunking_config);
433 (generator, result)
434 }
435
436 #[rstest]
437 #[case(100, 8, 4, 0, 10, 100 * 4)] #[case(100, 8, 4, 50, 10, 100 * 4)] #[case(257, 4, 8, 0, 10, 257 * 8)] #[case(60_000, 384, 192, 5_000, 10, 60_000 * 192)] #[case(60_000, 384, 192, 0, 10, 60_000 * 192)] #[case(60_000, 384, 192, 0, 2, 10_000 * 192)] #[case(60_000, 384, 192, 1000, 2, 11_000 * 192)] fn test_generate_data_from_offset(
445 #[case] num_points: usize,
446 #[case] dim: usize,
447 #[case] output_dim: u32,
448 #[case] offset: usize,
449 #[case] config_stop_count: usize,
450 #[case] expected_size: usize,
451 ) -> ANNResult<()> {
452 let (storage_provider, data_path, compressed_path) =
453 generate_data_and_compressed(num_points, dim, offset, output_dim)?;
454
455 let chunking_config = ChunkingConfig {
456 continuation_checker: Box::new(MockStopContinuationChecker {
457 count: Arc::new(RwLock::new(0)),
458 stop_count: config_stop_count,
459 }),
460 data_compression_chunk_vector_count: 10_000,
461 inmemory_build_chunk_vector_count: 10_000,
462 };
463
464 let (generator, result) = create_and_call_generator(
465 offset,
466 compressed_path.clone(),
467 &storage_provider,
468 data_path,
469 output_dim,
470 &chunking_config,
471 );
472
473 assert!(result.is_ok(), "Result is not ok, got {:?}", result); assert!(storage_provider.exists(&compressed_path)); let file_len = storage_provider.get_length(&compressed_path)? as usize;
478 assert_eq!(file_len, expected_size + 2 * std::mem::size_of::<i32>());
479
480 let mut r = storage_provider.open_reader(compressed_path.as_str())?;
481 let mut reader = BufReader::new(&mut r);
482 let metadata = Metadata::read(&mut reader)?;
483
484 let data: Vec<u8> = read_exact_into(&mut reader, expected_size)?;
485
486 assert_eq!(metadata.ndims_u32(), output_dim);
488 assert_eq!(metadata.npoints(), num_points);
489
490 data.chunks_exact(output_dim as usize)
492 .for_each(|chunk| assert_eq!(chunk, generator.quantizer.code.as_slice()));
493
494 Ok(())
495 }
496
497 #[test]
498 fn test_stop_and_continue_chunking_config() -> ANNResult<()> {
499 let (num_points, dim, output_dim) = (256, 128, 128);
500 let chunking_config = ChunkingConfig {
501 continuation_checker: Box::<NaiveContinuationTracker>::default(),
502 data_compression_chunk_vector_count: 10,
503 inmemory_build_chunk_vector_count: 10,
504 };
505 let (storage_provider, data_path, compressed_path) =
506 generate_data_and_compressed(num_points, dim, 0, output_dim)?;
507 let (mut generator, mut result) = create_and_call_generator(
508 0,
509 compressed_path.clone(),
510 &storage_provider,
511 data_path.clone(),
512 output_dim,
513 &chunking_config,
514 );
515 loop {
516 match result.as_ref().unwrap() {
517 Progress::Completed => break,
518 Progress::Processed(num_points) => {
519 (generator, result) = create_and_call_generator(
520 *num_points,
521 compressed_path.clone(),
522 &storage_provider,
523 data_path.clone(),
524 output_dim,
525 &chunking_config,
526 );
527 }
528 }
529 }
530
531 assert!(result.is_ok(), "Result is not ok, got {:?}", result); assert!(storage_provider.exists(&compressed_path)); let file_len = storage_provider.get_length(&compressed_path)? as usize;
536 let expected_size = (num_points * output_dim as usize) + 2 * std::mem::size_of::<i32>();
537 assert_eq!(file_len, expected_size,);
538
539 let mut r = storage_provider.open_reader(compressed_path.as_str())?;
540 let mut reader = BufReader::new(&mut r);
541 let metadata = Metadata::read(&mut reader)?;
542
543 let data: Vec<u8> =
544 read_exact_into(&mut reader, expected_size - 2 * std::mem::size_of::<i32>())?;
545
546 assert_eq!(metadata.ndims_u32(), output_dim);
548 assert_eq!(metadata.npoints(), num_points);
549
550 data.chunks_exact(output_dim as usize)
552 .for_each(|chunk| assert_eq!(chunk, generator.quantizer.code.as_slice()));
553 Ok(())
554 }
555
556 #[rstest]
557 #[case(
558 1_024,
559 384,
560 192,
561 1_025,
562 0,
563 "offset for compression is more than number of points"
564 )]
565 #[case(
566 1_1024,
567 384,
568 192,
569 5,
570 15,
571 "compressed data file length 2888 does not match expected length 968."
572 )]
573 fn test_offset_error_case(
574 #[case] num_points: usize,
575 #[case] dim: usize,
576 #[case] output_dim: u32,
577 #[case] offset: usize,
578 #[case] error_offset: usize,
579 #[case] msg: String,
580 ) -> ANNResult<()> {
581 assert!(offset > 0);
582 let (storage_provider, data_path, compressed_path) =
583 generate_data_and_compressed(num_points, dim, error_offset, output_dim)?;
584
585 let (_, result) = create_and_call_generator(
586 offset,
587 compressed_path,
588 &storage_provider,
589 data_path,
590 output_dim,
591 &ChunkingConfig::default(),
592 );
593
594 assert!(result.is_err());
595 if let Err(e) = result {
596 let error_msg = format!("{:?}", e);
597 assert!(error_msg.contains(&msg), "{}", &error_msg);
598 }
599
600 Ok(())
601 }
602
603 #[test]
604 fn test_validate_params_missing_compressed_file() -> ANNResult<()> {
605 let storage_provider = VirtualStorageProvider::new_memory();
606 storage_provider
607 .filesystem()
608 .create_dir("/test_data")
609 .expect("Could not create test directory");
610
611 let data_path = "/test_data/data.bin";
612 let compressed_path = "/test_data/compressed.bin";
613 let num_points = 100;
614 let dim = 8;
615 let output_dim = 4u32;
616
617 let data = create_test_data(num_points, dim);
619 let view = MatrixView::try_from(data.as_slice(), num_points, dim).unwrap();
620 write_bin(view, &mut storage_provider.create_for_write(data_path)?)?;
621
622 let context = GeneratorContext::new(10, compressed_path.to_string());
624 let generator = QuantDataGenerator::<f32, DummyCompressor>::new(
625 data_path.to_string(),
626 context,
627 &output_dim,
628 )
629 .unwrap();
630
631 let err = generator
632 .validate_params(num_points, &storage_provider)
633 .unwrap_err();
634 assert_eq!(err.kind(), diskann::ANNErrorKind::FileNotFoundError);
635 assert!(err.to_string().contains("expected compressed file"));
636 Ok(())
637 }
638}