@@ -37,6 +37,8 @@ class BM_VecSimCommon : public BM_VecSimIndex<index_type_t> {
3737 static void TopK_HNSW_DISK (benchmark::State &st);
3838 static void TopK_HNSW_DISK_MarkDeleted (benchmark::State &st);
3939 static void TopK_HNSW_DISK_DeleteLabel (benchmark::State &st);
40+ // Same as DeleteLabel but excludes ground truth vectors from deletion to keep recall stable
41+ static void TopK_HNSW_DISK_DeleteLabel_ProtectGT (benchmark::State &st);
4042 static void TopK_Tiered (benchmark::State &st, unsigned short index_offset = 0 );
4143
4244 // Does nothing but returning the index memory.
@@ -327,6 +329,113 @@ void BM_VecSimCommon<index_type_t>::TopK_HNSW_DISK_DeleteLabel(benchmark::State
327329 }
328330}
329331
332+ // Same as TopK_HNSW_DISK_DeleteLabel but excludes ground truth vectors from deletion.
333+ // This keeps the ground truth stable across different deletion counts for fair recall comparison.
334+ // st.range(0) = ef_runtime
335+ // st.range(1) = k
336+ // st.range(2) = number of vectors to delete
337+ template <typename index_type_t >
338+ void BM_VecSimCommon<index_type_t >::TopK_HNSW_DISK_DeleteLabel_ProtectGT(benchmark::State &st) {
339+ using data_t = typename index_type_t ::data_t ;
340+ using dist_t = typename index_type_t ::dist_t ;
341+
342+ size_t iter = 0 ;
343+
344+ // Build a set of all ground truth vector IDs (to protect from deletion)
345+ std::unordered_set<labelType> gt_labels_set;
346+ for (size_t q = 0 ; q < N_QUERIES; q++) {
347+ auto gt_results = BM_VecSimIndex<fp32_index_t >::TopKGroundTruth (q, 100 );
348+ for (const auto &res : gt_results->results ) {
349+ gt_labels_set.insert (res.id );
350+ }
351+ VecSimQueryReply_Free (gt_results);
352+ }
353+
354+ // Reload the index to get a fresh copy without any deleted vectors
355+ std::string folder_path = BM_VecSimGeneral::AttachRootPath (BM_VecSimGeneral::hnsw_index_file);
356+ INDICES[INDEX_HNSW_DISK] = IndexPtr (HNSWDiskFactory::NewIndex (folder_path));
357+ auto hnsw_index = GET_INDEX (INDEX_HNSW_DISK);
358+ auto *disk_index = dynamic_cast <HNSWDiskIndex<data_t , dist_t > *>(hnsw_index);
359+
360+ // Delete vectors using deleteVector, but skip ground truth vectors
361+ std::vector<labelType> deleted_labels;
362+ const size_t num_to_delete = st.range (2 );
363+
364+ // Get pseudo-random unique labels, but the same ones for all runs of the benchmark
365+ // Divide N_VECTORS into num_to_delete equal strata and pick one from each
366+ // Skip any labels that are in ground truth
367+ std::mt19937 rng (42 ); // Fixed seed for determinism
368+ size_t skipped_gt = 0 ;
369+ for (size_t i = 0 ; i < num_to_delete && i < N_VECTORS; i++) {
370+ size_t stratum_start = (i * N_VECTORS) / num_to_delete;
371+ size_t stratum_end = ((i + 1 ) * N_VECTORS) / num_to_delete;
372+ size_t stratum_size = stratum_end - stratum_start;
373+
374+ std::uniform_int_distribution<size_t > dist (0 , stratum_size - 1 );
375+ labelType label = stratum_start + dist (rng);
376+
377+ // Skip if this label is in ground truth
378+ if (gt_labels_set.find (label) != gt_labels_set.end ()) {
379+ skipped_gt++;
380+ continue ;
381+ }
382+ deleted_labels.push_back (label);
383+ }
384+
385+ // Measure the time spent on deleteVector calls (includes batch merge every 10 vectors)
386+ auto delete_start = std::chrono::high_resolution_clock::now ();
387+ for (const auto &label : deleted_labels) {
388+ disk_index->deleteVector (label);
389+ }
390+ // Force flush any pending deletes to ensure graph is fully repaired
391+ disk_index->flushDeleteBatch ();
392+ auto delete_end = std::chrono::high_resolution_clock::now ();
393+ double delete_time_ms = std::chrono::duration<double , std::milli>(delete_end - delete_start).count ();
394+
395+ size_t total_deleted = deleted_labels.size ();
396+ st.counters [" num_deleted" ] = total_deleted;
397+ st.counters [" num_gt_protected" ] = skipped_gt;
398+ st.counters [" delete_time_ms" ] = delete_time_ms;
399+ if (total_deleted > 0 ) {
400+ st.counters [" delete_time_per_vector_ms" ] = delete_time_ms / total_deleted;
401+ }
402+
403+ // Get DB statistics before benchmark
404+ auto stats = disk_index->getDBStatistics ();
405+ size_t io_bytes_before = 0 ;
406+ if (stats) {
407+ io_bytes_before = stats->getTickerCount (rocksdb::Tickers::BYTES_COMPRESSED_TO);
408+ }
409+
410+ std::atomic_int correct = 0 ;
411+ size_t ef = st.range (0 );
412+ size_t k = st.range (1 );
413+
414+ for (auto _ : st) {
415+ HNSWRuntimeParams hnswRuntimeParams = {.efRuntime = ef};
416+ auto query_params = BM_VecSimGeneral::CreateQueryParams (hnswRuntimeParams);
417+ auto &q = QUERIES[iter % N_QUERIES];
418+
419+ auto hnsw_results = VecSimIndex_TopKQuery (hnsw_index, q.data (), k, &query_params, BY_SCORE);
420+ st.PauseTiming ();
421+
422+ // Ground truth is unchanged since we protected all GT vectors from deletion
423+ auto gt_results = BM_VecSimIndex<fp32_index_t >::TopKGroundTruth (iter % N_QUERIES, k);
424+
425+ BM_VecSimGeneral::MeasureRecall (hnsw_results, gt_results, correct);
426+
427+ VecSimQueryReply_Free (hnsw_results);
428+ VecSimQueryReply_Free (gt_results);
429+ st.ResumeTiming ();
430+ iter++;
431+ }
432+ st.counters [" Recall" ] = (float )correct / (float )(k * iter);
433+ if (stats) {
434+ size_t io_bytes_after = stats->getTickerCount (rocksdb::Tickers::BYTES_COMPRESSED_TO);
435+ st.counters [" io_bytes_per_query" ] = static_cast <double >(io_bytes_after - io_bytes_before) / iter;
436+ }
437+ }
438+
330439template <typename index_type_t >
331440
332441void BM_VecSimCommon<index_type_t >::TopK_BF(benchmark::State &st, unsigned short index_offset) {
@@ -438,10 +547,10 @@ void BM_VecSimCommon<index_type_t>::TopK_Tiered(benchmark::State &st, unsigned s
438547 BENCHMARK_REGISTER_F (BM_CLASS, BM_FUNC) \
439548 ->Args({10 , 10 , 1000 }) \
440549 ->Args({10 , 10 , 10000 }) \
441- ->Args({10 , 10 , 50000 }) \
550+ ->Args({10 , 10 , 25000 }) \
442551 ->Args({200 , 50 , 1000 }) \
443552 ->Args({200 , 50 , 10000 }) \
444- ->Args({200 , 50 , 50000 }) \
553+ ->Args({200 , 50 , 25000 }) \
445554 ->ArgNames({" ef_runtime" , " k" , " num_marked_deleted" }) \
446555 ->Iterations(10 ) \
447556 ->Unit(benchmark::kMillisecond )
@@ -452,10 +561,24 @@ void BM_VecSimCommon<index_type_t>::TopK_Tiered(benchmark::State &st, unsigned s
452561 BENCHMARK_REGISTER_F (BM_CLASS, BM_FUNC) \
453562 ->Args({10 , 10 , 1000 }) \
454563 ->Args({10 , 10 , 10000 }) \
455- ->Args({10 , 10 , 50000 }) \
564+ ->Args({10 , 10 , 25000 }) \
565+ ->Args({200 , 50 , 1000 }) \
566+ ->Args({200 , 50 , 10000 }) \
567+ ->Args({200 , 50 , 25000 }) \
568+ ->ArgNames({" ef_runtime" , " k" , " num_deleted" }) \
569+ ->Iterations(10 ) \
570+ ->Unit(benchmark::kMillisecond )
571+
572+ // {ef_runtime, k, num_deleted}
573+ // Same as DeleteLabel but protects ground truth vectors from deletion for stable recall comparison
574+ #define REGISTER_TopK_HNSW_DISK_DeleteLabel_ProtectGT (BM_CLASS, BM_FUNC ) \
575+ BENCHMARK_REGISTER_F (BM_CLASS, BM_FUNC) \
576+ ->Args({10 , 10 , 1000 }) \
577+ ->Args({10 , 10 , 10000 }) \
578+ ->Args({10 , 10 , 25000 }) \
456579 ->Args({200 , 50 , 1000 }) \
457580 ->Args({200 , 50 , 10000 }) \
458- ->Args({200 , 50 , 50000 }) \
581+ ->Args({200 , 50 , 25000 }) \
459582 ->ArgNames({" ef_runtime" , " k" , " num_deleted" }) \
460583 ->Iterations(10 ) \
461584 ->Unit(benchmark::kMillisecond )
0 commit comments