1use diskann::{error::IntoANNResult, utils::VectorRepr, ANNError, ANNResult};
6use diskann_providers::storage::{StorageReadProvider, StorageWriteProvider};
7use diskann_providers::utils::{gen_random_slice, RayonThreadPoolRef, READ_WRITE_BLOCK_SIZE};
8
9use crate::utils::{compute_closest_centers, k_meanspp_selecting_pivots, run_lloyds};
10use rand::Rng;
11use tracing::info;
12
13use crate::{
14 disk_index_build_parameter::BYTES_IN_GB,
15 storage::{CachedReader, CachedWriter, DiskIndexWriter},
16};
17
18const BLOCK_SIZE_LARGE_FILE: u32 = 10_000;
20
21#[allow(clippy::too_many_arguments)]
22pub fn partition_with_ram_budget<T, StorageProvider, F>(
23 dataset_file: &str,
24 dim: usize,
25 sampling_rate: f64,
26 ram_budget_in_bytes: f64,
27 k_base: usize,
28 merged_index_prefix: &str,
29 storage_provider: &StorageProvider,
30 rng: &mut impl Rng,
31 pool: RayonThreadPoolRef<'_>,
32 ram_estimator: F,
33) -> ANNResult<usize>
34where
35 T: VectorRepr,
36 StorageProvider: StorageReadProvider + StorageWriteProvider,
37 F: Fn(u64, u64) -> f64,
38{
39 let (num_parts, pivot_data, train_dim) = find_partition_size::<T, StorageProvider, F>(
41 dataset_file,
42 sampling_rate,
43 ram_budget_in_bytes,
44 k_base,
45 storage_provider,
46 rng,
47 pool,
48 &ram_estimator,
49 )?;
50
51 info!("Saving shard data into clusters, with only ids");
52
53 shard_data_into_clusters_only_ids::<T, StorageProvider>(
54 dataset_file,
55 &pivot_data,
56 num_parts,
57 dim,
58 train_dim,
59 k_base,
60 merged_index_prefix,
61 storage_provider,
62 pool,
63 )?;
64
65 Ok(num_parts)
66}
67
68#[allow(clippy::too_many_arguments)]
69fn find_partition_size<T, StorageProvider, F>(
70 dataset_file: &str,
71 sampling_rate: f64,
72 ram_budget_in_bytes: f64,
73 k_base: usize,
74 storage_provider: &StorageProvider,
75 rng: &mut impl Rng,
76 pool: RayonThreadPoolRef<'_>,
77 ram_estimator: &F,
78) -> ANNResult<(usize, Vec<f32>, usize)>
79where
80 T: VectorRepr,
81 StorageProvider: StorageReadProvider + StorageWriteProvider,
82 F: Fn(u64, u64) -> f64,
83{
84 const MAX_K_MEANS_REPS: usize = 10;
85
86 let (train_data_float, num_train, train_dim) =
87 gen_random_slice::<T, StorageProvider>(dataset_file, sampling_rate, storage_provider, rng)?;
88 info!("Loaded {} points for train, dim: {}", num_train, train_dim);
89
90 let (test_data_float, num_test, test_dim) =
91 gen_random_slice::<T, StorageProvider>(dataset_file, sampling_rate, storage_provider, rng)?;
92 info!("Loaded {} points for test, dim: {}", num_test, test_dim);
93
94 let total_points = (num_train as f64 / sampling_rate) as u64;
96 let initial_num_parts = estimate_initial_partition_count::<F>(
98 total_points,
99 train_dim as u64,
100 k_base,
101 ram_budget_in_bytes,
102 ram_estimator,
103 );
104
105 let mut num_parts = initial_num_parts;
106 let mut fit_in_ram = false;
107 let mut pivot_data = Vec::new();
108 while !fit_in_ram {
110 fit_in_ram = true;
111
112 let mut max_ram_usage_in_bytes = 0.0;
113
114 pivot_data = vec![0.0; num_parts * train_dim];
115
116 info!("Processing global k-means (kmeans_partitioning Step)");
118 k_meanspp_selecting_pivots(
119 &train_data_float,
120 num_train,
121 train_dim,
122 &mut pivot_data,
123 num_parts,
124 rng,
125 &mut (false),
126 pool,
127 )?;
128
129 run_lloyds(
130 &train_data_float,
131 num_train,
132 train_dim,
133 &mut pivot_data,
134 num_parts,
135 MAX_K_MEANS_REPS,
136 &mut (false),
137 pool,
138 )?;
139
140 let mut cluster_sizes = Vec::new();
143 estimate_cluster_sizes(
144 &test_data_float,
145 num_test,
146 &pivot_data,
147 num_parts,
148 test_dim,
149 k_base,
150 &mut cluster_sizes,
151 pool,
152 )?;
153
154 let mut partition_stats = Vec::with_capacity(num_parts);
155 for p in &cluster_sizes {
156 let p = (*p as f64 / sampling_rate) as u64;
158 let cur_shard_ram_estimate_in_bytes = ram_estimator(p, train_dim as u64);
159 partition_stats.push((p, cur_shard_ram_estimate_in_bytes));
160
161 if cur_shard_ram_estimate_in_bytes > max_ram_usage_in_bytes {
162 max_ram_usage_in_bytes = cur_shard_ram_estimate_in_bytes;
163 }
164 }
165
166 info!(
167 "Partition RAM estimates (GB): {}",
168 partition_stats
169 .iter()
170 .map(|(size, ram)| format!("#{}: {:.2}", size, ram / BYTES_IN_GB))
171 .collect::<Vec<_>>()
172 .join(", ")
173 );
174
175 info!(
176 "With {} parts, max estimated RAM usage: {:.2} GB, budget given is {:.2} GB",
177 num_parts,
178 max_ram_usage_in_bytes / BYTES_IN_GB,
179 ram_budget_in_bytes / BYTES_IN_GB
180 );
181 if max_ram_usage_in_bytes > ram_budget_in_bytes {
182 fit_in_ram = false;
183 num_parts += 2;
184 } else {
185 info!(
186 "Found optimal partition count: [parts={}, initial={}, max_ram={:.2}GB, budget={:.2}GB]",
187 num_parts,
188 initial_num_parts,
189 max_ram_usage_in_bytes / BYTES_IN_GB,
190 ram_budget_in_bytes / BYTES_IN_GB
191 );
192 }
193 }
194
195 Ok((num_parts, pivot_data, train_dim))
196}
197
198fn estimate_initial_partition_count<F>(
200 total_points: u64,
201 dimension: u64,
202 k_base: usize,
203 ram_budget_in_bytes: f64,
204 ram_estimator: &F,
205) -> usize
206where
207 F: Fn(u64, u64) -> f64,
208{
209 let total_ram_estimate = ram_estimator(total_points * k_base as u64, dimension);
211
212 let mut partition_count = (total_ram_estimate / ram_budget_in_bytes).ceil() as usize;
213
214 partition_count = std::cmp::max(3, partition_count);
216 if partition_count.is_multiple_of(2) {
217 partition_count += 1;
218 }
219
220 info!(
221 "Estimated initial partition count: {} (total points: {}, dimension: {}, k_base: {}, total_ram_estimate: {:.2} GB, ram_budget: {:.2} GB)",
222 partition_count,
223 total_points,
224 dimension,
225 k_base,
226 total_ram_estimate / BYTES_IN_GB,
227 ram_budget_in_bytes / BYTES_IN_GB
228 );
229
230 partition_count
231}
232
233#[allow(clippy::too_many_arguments)]
234fn shard_data_into_clusters_only_ids<T, StorageProvider>(
235 dataset_file: &str,
236 pivot_data: &[f32],
237 num_parts: usize,
238 dim: usize,
239 full_dim: usize,
240 k_base: usize,
241 merged_index_prefix: &str,
242 storage_provider: &StorageProvider,
243 pool: RayonThreadPoolRef<'_>,
244) -> ANNResult<()>
245where
246 T: VectorRepr,
247 StorageProvider: StorageReadProvider + StorageWriteProvider,
248{
249 let mut dataset_reader = CachedReader::<StorageProvider>::new(
250 dataset_file,
251 READ_WRITE_BLOCK_SIZE,
252 storage_provider,
253 )?;
254 let num_points = dataset_reader.read_u32()?;
255 let base_dim = dataset_reader.read_u32()?;
256 if base_dim != dim as u32 {
257 return Err(ANNError::log_index_error(
258 "dimensions dont match for train set and base set",
259 ));
260 }
261
262 let mut shard_counts = vec![0; num_parts];
263 let shard_idmaps_names = (0..num_parts)
264 .map(|shard| {
265 DiskIndexWriter::get_merged_index_subshard_id_map_file(merged_index_prefix, shard)
266 })
267 .collect::<Vec<String>>();
268
269 const WRITE_ID_CACHE_SIZE: u64 = 8 * 1024;
271 let mut shard_idmap_cached_writers = Vec::new();
272 for name in &shard_idmaps_names {
273 let writer = storage_provider.create_for_write(name)?;
274 let cached_writer =
275 CachedWriter::<StorageProvider>::new(name, WRITE_ID_CACHE_SIZE, writer)?;
276 shard_idmap_cached_writers.push(cached_writer);
277 }
278
279 let dummy_size: u32 = 0;
280 let const_one: u32 = 1;
281 for writer in shard_idmap_cached_writers.iter_mut() {
282 writer.write(&dummy_size.to_le_bytes())?;
283 writer.write(&const_one.to_le_bytes())?;
284 }
285
286 let block_size = if num_points <= BLOCK_SIZE_LARGE_FILE {
287 num_points
288 } else {
289 BLOCK_SIZE_LARGE_FILE
290 };
291
292 let num_blocks = num_points.div_ceil(block_size);
293
294 let mut block_closest_centers = vec![0u32; block_size as usize * k_base];
295 let mut block_data_t: Vec<u8> = vec![0; block_size as usize * dim * std::mem::size_of::<T>()];
296 let mut block_data_float: Vec<f32> = vec![0.0; full_dim * block_size as usize];
297
298 for block in 0..num_blocks {
299 let start_id = (block * block_size) as usize;
300 let end_id = std::cmp::min((block + 1) * block_size, num_points) as usize;
301 let cur_blk_size = end_id - start_id;
302
303 dataset_reader.read(&mut block_data_t[..cur_blk_size * dim * std::mem::size_of::<T>()])?;
304
305 let cur_vector_t: &[T] =
307 bytemuck::cast_slice(&block_data_t[..cur_blk_size * dim * std::mem::size_of::<T>()]);
308
309 for (v, dst) in cur_vector_t
310 .chunks_exact(dim)
311 .zip(block_data_float.chunks_exact_mut(full_dim))
312 {
313 T::as_f32_into(v, dst).into_ann_result()?;
314 }
315
316 compute_closest_centers(
317 &block_data_float[..full_dim * cur_blk_size],
318 cur_blk_size,
319 full_dim,
320 pivot_data,
321 num_parts,
322 k_base,
323 &mut block_closest_centers[..cur_blk_size * k_base],
324 None,
325 None,
326 pool,
327 )?;
328
329 for p in 0..cur_blk_size {
330 for p1 in 0..k_base {
331 let shard_id = block_closest_centers[p * k_base + p1] as usize;
332 let original_point_map_id = (start_id + p) as u32;
333 shard_idmap_cached_writers[shard_id].write(&original_point_map_id.to_le_bytes())?;
334 shard_counts[shard_id] += 1;
335 }
336 }
337 }
338
339 let mut total_count = 0;
340
341 for i in 0..num_parts {
342 let cur_shard_count = shard_counts[i] as u32;
343 info!(" shard_{} with npts : {} ", i, cur_shard_count);
344 total_count += cur_shard_count;
345 shard_idmap_cached_writers[i].reset()?;
346 shard_idmap_cached_writers[i].write(&cur_shard_count.to_le_bytes())?;
347 shard_idmap_cached_writers[i].flush()?;
348 }
349
350 info!(
351 "Partitioned {} with replication factor {} to get {} points across {} shards",
352 num_points, k_base, total_count, num_parts
353 );
354
355 Ok(())
356}
357
358#[allow(clippy::too_many_arguments)]
359fn estimate_cluster_sizes(
360 data_float: &[f32],
361 num_pts: usize,
362 pivot_data: &[f32],
363 num_centers: usize,
364 dim: usize,
365 k_base: usize,
366 cluster_sizes: &mut Vec<u32>,
367 pool: RayonThreadPoolRef<'_>,
368) -> ANNResult<()> {
369 cluster_sizes.clear();
370 let mut shard_counts = vec![0; num_centers];
371
372 let block_size = if num_pts <= BLOCK_SIZE_LARGE_FILE as usize {
373 num_pts
374 } else {
375 BLOCK_SIZE_LARGE_FILE as usize
376 };
377
378 let mut block_closest_centers = vec![0; block_size * k_base];
379
380 let num_blocks = num_pts.div_ceil(block_size);
381
382 for block in 0..num_blocks {
383 let start_id = block * block_size;
384 let end_id = std::cmp::min((block + 1) * block_size, num_pts);
385 let cur_blk_size = end_id - start_id;
386
387 let block_data_float = &data_float[start_id * dim..(start_id + cur_blk_size) * dim];
388
389 compute_closest_centers(
390 block_data_float,
391 cur_blk_size,
392 dim,
393 pivot_data,
394 num_centers,
395 k_base,
396 &mut block_closest_centers[..cur_blk_size * k_base],
397 None,
398 None,
399 pool,
400 )?;
401
402 for p in 0..cur_blk_size {
403 for p1 in 0..k_base {
404 let shard_id = block_closest_centers[p * k_base + p1] as usize;
405 shard_counts[shard_id] += 1;
406 }
407 }
408 }
409
410 (0..num_centers).for_each(|i| {
411 let cur_shard_count = shard_counts[i] as u32;
412 cluster_sizes.push(cur_shard_count);
413 });
414 info!("Estimated cluster sizes: {:?}", cluster_sizes);
415 Ok(())
416}
417
418#[cfg(test)]
419mod partition_test {
420 use std::io::Read;
421
422 use diskann_providers::storage::VirtualStorageProvider;
423 use diskann_providers::utils::create_thread_pool_for_test;
424 use diskann_utils::test_data_root;
425 use vfs::{MemoryFS, OverlayFS};
426
427 use super::*;
428
429 #[test]
430 fn test_estimate_cluster_sizes() {
431 let data_float = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
432 let num_pts = 3;
433 let pivot_data = &[1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
434 let num_centers = 3;
435 let dim = 2;
436 let k_base = 2;
437 let mut cluster_sizes = vec![];
438 let pool = create_thread_pool_for_test();
439
440 estimate_cluster_sizes(
441 &data_float,
442 num_pts,
443 pivot_data,
444 num_centers,
445 dim,
446 k_base,
447 &mut cluster_sizes,
448 pool.as_ref(),
449 )
450 .unwrap();
451
452 assert_eq!(cluster_sizes.len(), num_centers);
453 assert_eq!(cluster_sizes, &[2, 3, 1]);
454 }
455
456 #[test]
457 fn test_shard_data_into_clusters_only_ids() {
458 let dataset_path = "/dataset_file";
460 let mut data_float = Vec::new();
462 let num_points: u32 = 100;
463 let dim: usize = 10;
464
465 let storage_provider = VirtualStorageProvider::new_overlay(test_data_root());
466 {
467 let writer = storage_provider.create_for_write(dataset_path).unwrap();
468 let mut dataset_writer = CachedWriter::<VirtualStorageProvider<MemoryFS>>::new(
469 dataset_path,
470 READ_WRITE_BLOCK_SIZE,
471 writer,
472 )
473 .unwrap();
474 dataset_writer.write(&num_points.to_le_bytes()).unwrap();
475 dataset_writer.write(&dim.to_le_bytes()).unwrap();
476 for i in 0..num_points {
477 for j in 0..dim {
478 let val = (i * dim as u32 + j as u32) as f32;
479 data_float.push(val);
480 dataset_writer.write(&val.to_le_bytes()).unwrap();
481 }
482 }
483 }
484
485 let k_base: usize = 2;
487 let num_parts = 3;
488
489 let pivot_data: [f32; 30] = [
491 820.0, 821.0, 822.0, 823.0, 824.0, 825.0, 826.0, 827.0, 828.0, 829.0, 155.0, 156.0,
492 157.0, 158.0, 159.0, 160.0, 161.0, 162.0, 163.0, 164.0, 480.0, 481.0, 482.0, 483.0,
493 484.0, 485.0, 486.0, 487.0, 488.0, 489.0,
494 ];
495
496 let merged_index_prefix = "/merged_index";
498 let pool = create_thread_pool_for_test();
499 shard_data_into_clusters_only_ids::<f32, VirtualStorageProvider<OverlayFS>>(
501 dataset_path,
502 &pivot_data,
503 num_parts,
504 dim,
505 dim,
506 k_base,
507 merged_index_prefix,
508 &storage_provider,
509 pool.as_ref(),
510 )
511 .unwrap();
512
513 let expected_prefix = "/partition/id_maps/merged_index_expected";
515 for shard in 0..num_parts {
516 let path1 =
517 DiskIndexWriter::get_merged_index_subshard_id_map_file(merged_index_prefix, shard);
518 let path2 =
519 DiskIndexWriter::get_merged_index_subshard_id_map_file(expected_prefix, shard);
520 let file1 =
521 load_file_to_vec::<VirtualStorageProvider<OverlayFS>>(&path1, &storage_provider);
522 let file2 =
523 load_file_to_vec::<VirtualStorageProvider<OverlayFS>>(&path2, &storage_provider);
524
525 assert_eq!(file1.len(), file2.len());
526 assert_eq!(file1[..], file2[..]);
527
528 storage_provider.delete(&path1).unwrap();
530 }
531
532 storage_provider.delete(dataset_path).unwrap();
533 }
534
535 fn load_file_to_vec<StorageProvider>(
536 file_path: &str,
537 storage_provider: &StorageProvider,
538 ) -> Vec<u8>
539 where
540 StorageProvider: StorageReadProvider,
541 {
542 let mut file = storage_provider.open_reader(file_path).unwrap();
543 let mut buffer = vec![];
544 file.read_to_end(&mut buffer).unwrap();
545 buffer
546 }
547
548 #[test]
549 fn test_estimate_initial_partition_count_minimum_clamp() {
550 let count = estimate_initial_partition_count(
552 100, 10, 1, 1_000_000.0, &|n, _d| n as f64 * 100.0, );
558 assert_eq!(count, 3);
559 }
560
561 #[test]
562 fn test_estimate_initial_partition_count_odd_rounding() {
563 let count = estimate_initial_partition_count(
565 1000,
566 128,
567 1,
568 1000.0, &|n, _d| n as f64 * 4.0, );
571 assert_eq!(count, 5);
572 }
573
574 #[test]
575 fn test_estimate_initial_partition_count_large_ratio() {
576 let count = estimate_initial_partition_count(
578 1000,
579 128,
580 1,
581 1000.0, &|n, _d| n as f64 * 7.0, );
584 assert_eq!(count, 7);
585 }
586
587 #[test]
588 fn test_estimate_initial_partition_count_k_base_multiplier() {
589 let count = estimate_initial_partition_count(
591 100,
592 10,
593 3, 100.0, &|n, _d| n as f64 * 1.0, );
597 assert_eq!(count, 3);
598 }
599
600 #[test]
601 fn test_partition_with_ram_budget() -> ANNResult<()> {
602 let storage_provider = VirtualStorageProvider::new_overlay(test_data_root());
603 let dataset_file = "/sift/siftsmall_learn.bin";
604 let mut file = storage_provider.open_reader(dataset_file).unwrap();
605 let mut data = vec![];
606 file.read_to_end(&mut data).unwrap();
607
608 let sampling_rate = 1.0;
609 let ram_budget_in_bytes = 15_000_000.0;
610 let max_degree = 64;
611 let k_base = 2;
612 let merged_index_prefix = "/test_merged_index_prefix";
613 let pool = create_thread_pool_for_test();
614
615 let num_parts = partition_with_ram_budget::<f32, _, _>(
616 dataset_file,
617 128, sampling_rate,
619 ram_budget_in_bytes,
620 k_base,
621 merged_index_prefix,
622 &storage_provider,
623 &mut diskann_providers::utils::create_rnd_in_tests(),
624 pool.as_ref(),
625 |num_points, dim| {
626 use diskann_providers::model::GRAPH_SLACK_FACTOR;
628
629 let datasize = std::mem::size_of::<f32>() as u64;
630 let graph_degree = max_degree as u64;
631 let dataset_size = (num_points * dim.next_multiple_of(8u64) * datasize) as f64;
632 let graph_size = (num_points * graph_degree * 4) as f64 * GRAPH_SLACK_FACTOR;
633 1.1 * (dataset_size + graph_size)
634 },
635 )?;
636
637 assert!(num_parts >= 3);
638
639 for i in 0..num_parts {
640 let idmap_filename =
641 DiskIndexWriter::get_merged_index_subshard_id_map_file(merged_index_prefix, i);
642 storage_provider.delete(&idmap_filename)?;
643 }
644
645 Ok(())
646 }
647}