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