diff --git a/cpp/bench/ann/src/common/benchmark.hpp b/cpp/bench/ann/src/common/benchmark.hpp index a588b1e2a6..5bf39ac79c 100644 --- a/cpp/bench/ann/src/common/benchmark.hpp +++ b/cpp/bench/ann/src/common/benchmark.hpp @@ -116,10 +116,25 @@ void bench_build(::benchmark::State& state, cuvs::bench::benchmark_thread_id = state.thread_index(); cuvs::bench::benchmark_n_threads = state.threads(); dump_parameters(state, index.build_param); + + if (state.thread_index() == 0) { + log_info( + "bench_build start: algo=%s index=%s file=%s force_overwrite=%d threads=%d dim=%d " + "base_set_size=%zu", + index.algo.c_str(), + index.name.c_str(), + index.file.c_str(), + force_overwrite ? 1 : 0, + state.threads(), + dataset->dim(), + dataset->base_set_size()); + } + if (file_exists(index.file)) { if (force_overwrite) { - log_info("Overwriting file: %s", index.file.c_str()); + log_info("bench_build overwriting existing index file: %s", index.file.c_str()); } else { + log_info("bench_build skipped existing index file: %s", index.file.c_str()); return state.SkipWithMessage( "Index file already exists (use --force to overwrite the index)."); } @@ -127,15 +142,39 @@ void bench_build(::benchmark::State& state, std::unique_ptr> algo; try { + if (state.thread_index() == 0) { + log_info( + "bench_build create algo start: algo=%s index=%s", index.algo.c_str(), index.name.c_str()); + } algo = create_algo(index.algo, dataset->distance(), dataset->dim(), index.build_param); + if (state.thread_index() == 0) { + log_info( + "bench_build create algo finish: algo=%s index=%s", index.algo.c_str(), index.name.c_str()); + } } catch (const std::exception& e) { + log_warn("bench_build create algo failed: algo=%s index=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + e.what()); return state.SkipWithError("Failed to create an algo: " + std::string(e.what())); } const auto algo_property = parse_algo_property(algo->get_preference(), index.build_param); + if (state.thread_index() == 0) { + log_info("bench_build algo properties: algo=%s dataset_memory_type=%d query_memory_type=%d", + index.algo.c_str(), + static_cast(algo_property.dataset_memory_type), + static_cast(algo_property.query_memory_type)); + } const T* base_set = dataset->base_set(algo_property.dataset_memory_type); std::size_t index_size = dataset->base_set_size(); + if (state.thread_index() == 0) { + log_info("bench_build base_set ready: algo=%s pointer=%p index_size=%zu", + index.algo.c_str(), + static_cast(base_set), + index_size); + } cuda_timer gpu_timer{algo}; { @@ -155,11 +194,24 @@ void bench_build(::benchmark::State& state, */ [[maybe_unused]] auto gpu_all = gpu_timer.lap(no_lap_sync); for (auto _ : state) { + if (state.thread_index() == 0) { + log_info( + "bench_build iteration start: algo=%s index=%s", index.algo.c_str(), index.name.c_str()); + } [[maybe_unused]] auto ntx_lap = nvtx.lap(); [[maybe_unused]] auto gpu_lap = gpu_timer.lap(!no_lap_sync); try { algo->build(base_set, index_size); + if (state.thread_index() == 0) { + log_info("bench_build iteration finish: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); + } } catch (const std::exception& e) { + log_warn("bench_build iteration failed: algo=%s index=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + e.what()); state.SkipWithError(std::string(e.what())); } } @@ -171,7 +223,25 @@ void bench_build(::benchmark::State& state, if (state.skipped()) { return; } make_sure_parent_dir_exists(index.file); - algo->save(index.file); + try { + log_info("bench_build save start: algo=%s index=%s file=%s", + index.algo.c_str(), + index.name.c_str(), + index.file.c_str()); + algo->save(index.file); + log_info("bench_build save finish: algo=%s index=%s file=%s exists=%d", + index.algo.c_str(), + index.name.c_str(), + index.file.c_str(), + file_exists(index.file) ? 1 : 0); + } catch (const std::exception& e) { + log_warn("bench_build save failed: algo=%s index=%s file=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + index.file.c_str(), + e.what()); + state.SkipWithError("Failed to save an index: " + std::string(e.what())); + } } template @@ -197,10 +267,26 @@ void bench_search(::benchmark::State& state, // Round down the query data to a multiple of the batch size to loop over full batches of data const std::size_t query_set_size = (dataset->query_set_size() / n_queries) * n_queries; + if (state.thread_index() == 0) { + log_info( + "bench_search start: algo=%s index=%s file=%s search_param_ix=%zu threads=%d " + "raw_queries=%zu query_set_size=%zu k=%u n_queries=%zu", + index.algo.c_str(), + index.name.c_str(), + index.file.c_str(), + search_param_ix, + state.threads(), + dataset->query_set_size(), + query_set_size, + k, + n_queries); + } + if (dataset->query_set_size() < n_queries) { std::stringstream msg; msg << "Not enough queries in benchmark set. Expected " << n_queries << ", actual " << dataset->query_set_size(); + if (state.thread_index() == 0) { log_warn("bench_search skipped: %s", msg.str().c_str()); } state.SkipWithError(msg.str()); return; } @@ -215,9 +301,18 @@ void bench_search(::benchmark::State& state, const T* query_set = nullptr; if (!file_exists(index.file)) { + if (state.thread_index() == 0) { + log_warn("bench_search missing index file: algo=%s index=%s file=%s", + index.algo.c_str(), + index.name.c_str(), + index.file.c_str()); + } state.SkipWithError("Index file is missing. Run the benchmark in the build mode first."); return; } + if (state.thread_index() == 0) { + log_info("bench_search index file exists: file=%s", index.file.c_str()); + } /** * Make sure the first thread loads the algo and dataset @@ -227,6 +322,9 @@ void bench_search(::benchmark::State& state, // algo is static to cache it between close search runs to save time on index loading static std::string index_file = ""; if (index.file != index_file) { + log_info("bench_search cache reset: previous_file=%s new_file=%s", + index_file.c_str(), + index.file.c_str()); current_algo.reset(); index_file = index.file; } @@ -235,26 +333,63 @@ void bench_search(::benchmark::State& state, algo* a; try { if (!current_algo || (a = dynamic_cast*>(current_algo.get())) == nullptr) { + log_info("bench_search create algo start: algo=%s index=%s dim=%d", + index.algo.c_str(), + index.name.c_str(), + dataset->dim()); auto ualgo = create_algo(index.algo, dataset->distance(), dataset->dim(), index.build_param); a = ualgo.get(); + log_info( + "bench_search load algo start: algo=%s file=%s", index.algo.c_str(), index_file.c_str()); a->load(index_file); + log_info( + "bench_search load algo finish: algo=%s file=%s", index.algo.c_str(), index_file.c_str()); current_algo = std::move(ualgo); + } else { + log_info("bench_search using cached algo: algo=%s file=%s", + index.algo.c_str(), + index_file.c_str()); } + log_info("bench_search create search_param start: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); search_param = create_search_param(index.algo, sp_json); + log_info("bench_search create search_param finish: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); } catch (const std::exception& e) { + log_warn("bench_search load/create failed: algo=%s index=%s file=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + index_file.c_str(), + e.what()); state.SkipWithError("Failed to create an algo: " + std::string(e.what())); return; } current_algo_props = std::make_unique(std::move(parse_algo_property(a->get_preference(), sp_json))); + log_info("bench_search algo properties: algo=%s dataset_memory_type=%d query_memory_type=%d", + index.algo.c_str(), + static_cast(current_algo_props->dataset_memory_type), + static_cast(current_algo_props->query_memory_type)); if (search_param->needs_dataset()) { try { + log_info("bench_search set_search_dataset start: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); a->set_search_dataset(dataset->base_set(current_algo_props->dataset_memory_type), dataset->base_set_size()); + log_info("bench_search set_search_dataset finish: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); } catch (const std::exception& ex) { + log_warn("bench_search set_search_dataset failed: algo=%s index=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + ex.what()); state.SkipWithError("The algorithm '" + index.name + "' requires the base set, but it's not available. " + "Exception: " + std::string(ex.what())); @@ -262,14 +397,28 @@ void bench_search(::benchmark::State& state, } } try { + log_info("bench_search set_search_param start: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); a->set_search_param(*search_param, dataset->filter_bitset(current_algo_props->dataset_memory_type)); + log_info("bench_search set_search_param finish: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); } catch (const std::exception& ex) { + log_warn("bench_search set_search_param failed: algo=%s index=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + ex.what()); state.SkipWithError("An error occurred setting search parameters: " + std::string(ex.what())); return; } query_set = dataset->query_set(current_algo_props->query_memory_type); + log_info("bench_search query_set acquired: algo=%s pointer=%p query_memory_type=%d", + index.algo.c_str(), + static_cast(query_set), + static_cast(current_algo_props->query_memory_type)); load_barrier.arrive(state.threads()); } else { // All other threads will wait for the first thread to initialize the algo. @@ -293,6 +442,12 @@ void bench_search(::benchmark::State& state, auto* neighbors_ptr = reinterpret_cast(result_buf.data(current_algo_props->query_memory_type)); auto* distances_ptr = reinterpret_cast(neighbors_ptr + result_elem_count); + if (state.thread_index() == 0) { + log_info("bench_search result buffer ready: algo=%s result_elem_count=%zu memory_type=%d", + index.algo.c_str(), + result_elem_count, + static_cast(current_algo_props->query_memory_type)); + } { nvtx_stats nvtx_stats{state}; @@ -300,8 +455,21 @@ void bench_search(::benchmark::State& state, std::unique_ptr> a{nullptr}; try { + if (state.thread_index() == 0) { + log_info( + "bench_search algo copy start: algo=%s index=%s", index.algo.c_str(), index.name.c_str()); + } dynamic_cast*>(current_algo.get())->copy().swap(a); + if (state.thread_index() == 0) { + log_info("bench_search algo copy finish: algo=%s index=%s", + index.algo.c_str(), + index.name.c_str()); + } } catch (const std::exception& e) { + log_warn("bench_search algo copy failed: algo=%s index=%s error=%s", + index.algo.c_str(), + index.name.c_str(), + e.what()); state.SkipWithError("Algo::copy: " + std::string(e.what())); return; } @@ -321,6 +489,16 @@ void bench_search(::benchmark::State& state, neighbors_ptr + out_offset * k, distances_ptr + out_offset * k); } catch (const std::exception& e) { + log_warn( + "bench_search benchmark loop failed: algo=%s index=%s batch_offset=%td " + "out_offset=%td n_queries=%zu k=%u error=%s", + index.algo.c_str(), + index.name.c_str(), + batch_offset, + out_offset, + n_queries, + k, + e.what()); state.SkipWithError("Benchmark loop: " + std::string(e.what())); break; } @@ -347,11 +525,27 @@ void bench_search(::benchmark::State& state, // This will be the total number of queries across all threads state.counters.insert({{"total_queries", queries_processed}}); - if (state.skipped()) { return; } + if (state.skipped()) { + if (state.thread_index() == 0) { + log_warn("bench_search skipped before recall: algo=%s index=%s queries_processed=%zu", + index.algo.c_str(), + index.name.c_str(), + queries_processed); + } + return; + } // Each thread calculates recall on their partition of queries. // evaluate recall if (dataset->max_k() >= k && dataset->gt_maps().has_value()) { + if (state.thread_index() == 0) { + log_info("bench_search recall start: algo=%s index=%s max_k=%u k=%u queries_processed=%zu", + index.algo.c_str(), + index.name.c_str(), + dataset->max_k(), + k, + queries_processed); + } // gt_maps[i] is a hash map of {id, neighbor_rank} for query i const auto& gt_maps = dataset->gt_maps(); result_buf.transfer_data(MemoryType::kHost, current_algo_props->query_memory_type); @@ -410,6 +604,16 @@ void bench_search(::benchmark::State& state, batch_offset = (batch_offset + queries_stride) % query_set_size; } double actual_recall = static_cast(match_count) / static_cast(total_count); + if (state.thread_index() == 0) { + log_info( + "bench_search recall finish: algo=%s index=%s match_count=%zu total_count=%zu " + "recall=%f", + index.algo.c_str(), + index.name.c_str(), + match_count, + total_count, + actual_recall); + } /* NOTE: recall in the throughput mode & filtering When filtering is enabled, `total_count` may vary between individual threads, but we still take diff --git a/cpp/bench/ann/src/cuvs/cuvs_vamana_wrapper.h b/cpp/bench/ann/src/cuvs/cuvs_vamana_wrapper.h index 7d7292bd50..e8fdf502c2 100644 --- a/cpp/bench/ann/src/cuvs/cuvs_vamana_wrapper.h +++ b/cpp/bench/ann/src/cuvs/cuvs_vamana_wrapper.h @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION. + * SPDX-FileCopyrightText: Copyright (c) 2025-2026, NVIDIA CORPORATION. * SPDX-License-Identifier: Apache-2.0 */ #pragma once @@ -10,12 +10,31 @@ #include #include +#include +#include +#include #include #include #include +#include +#include +#include namespace cuvs::bench { +inline bool cuvs_vamana_artifact_exists(const std::string& file) +{ + std::error_code ec; + return std::filesystem::exists(file, ec); +} + +inline std::uintmax_t cuvs_vamana_artifact_size(const std::string& file) +{ + std::error_code ec; + auto size = std::filesystem::file_size(file, ec); + return ec ? 0 : size; +} + template class cuvs_vamana : public algo, public algo_gpu { public: @@ -78,10 +97,31 @@ void cuvs_vamana::build(const T* dataset, size_t nrow) dataset, raft::make_extents(nrow, this->dim_)); bool dataset_is_on_host = raft::get_device_for_address(dataset) == -1; + log_info( + "cuvs_vamana build start: nrow=%zu dim=%d graph_degree=%u visited_size=%u alpha=%f " + "dataset_memory=%s", + nrow, + this->dim_, + static_cast(vamana_index_params_.graph_degree), + static_cast(vamana_index_params_.visited_size), + static_cast(vamana_index_params_.alpha), + dataset_is_on_host ? "host" : "device"); + vamana_index_ = std::make_shared>(std::move( dataset_is_on_host ? cuvs::neighbors::vamana::build(handle_, vamana_index_params_, dataset_view_host) : cuvs::neighbors::vamana::build(handle_, vamana_index_params_, dataset_view_device))); + + log_info("cuvs_vamana build finish: index_size=%zu index_dim=%u graph_degree=%u", + static_cast(vamana_index_->size()), + static_cast(vamana_index_->dim()), + static_cast(vamana_index_->graph_degree())); + + if (vamana_index_->size() == 0 || vamana_index_->dim() == 0) { + throw std::runtime_error("cuvs_vamana build produced an empty index: size=" + + std::to_string(static_cast(vamana_index_->size())) + + ", dim=" + std::to_string(vamana_index_->dim())); + } } template @@ -95,19 +135,89 @@ void cuvs_vamana::set_search_param(const search_param_base& param, template void cuvs_vamana::save(const std::string& file) const { + if (!vamana_index_) { throw std::runtime_error("cuvs_vamana save called before build"); } + + log_info("cuvs_vamana save start: file=%s index_size=%zu index_dim=%u graph_degree=%u", + file.c_str(), + static_cast(vamana_index_->size()), + static_cast(vamana_index_->dim()), + static_cast(vamana_index_->graph_degree())); + cuvs::neighbors::vamana::serialize(handle_, file, *vamana_index_); + + const auto data_file = file + ".data"; + const auto index_bytes = cuvs_vamana_artifact_size(file); + const auto data_bytes = cuvs_vamana_artifact_size(data_file); + const auto index_exists = cuvs_vamana_artifact_exists(file); + const auto data_exists = cuvs_vamana_artifact_exists(data_file); + + log_info( + "cuvs_vamana save artifacts: index_file=%s exists=%d bytes=%zu data_file=%s " + "exists=%d bytes=%zu", + file.c_str(), + index_exists ? 1 : 0, + static_cast(index_bytes), + data_file.c_str(), + data_exists ? 1 : 0, + static_cast(data_bytes)); + + if (!index_exists) { + throw std::runtime_error("cuvs_vamana serialize did not write index file: " + file); + } + if (!data_exists) { + throw std::runtime_error("cuvs_vamana serialize did not write dataset sidecar: " + data_file); + } + if (index_bytes == 0 || data_bytes == 0) { + throw std::runtime_error("cuvs_vamana serialize wrote empty artifact(s): index_file=" + file + + ", data_file=" + data_file); + } } template void cuvs_vamana::load(const std::string& file) { - diskann_memory_search_->load(file); + const auto data_file = file + ".data"; + const auto index_bytes = cuvs_vamana_artifact_size(file); + const auto data_bytes = cuvs_vamana_artifact_size(data_file); + + log_info( + "cuvs_vamana load start: index_file=%s exists=%d bytes=%zu data_file=%s exists=%d " + "bytes=%zu", + file.c_str(), + cuvs_vamana_artifact_exists(file) ? 1 : 0, + static_cast(index_bytes), + data_file.c_str(), + cuvs_vamana_artifact_exists(data_file) ? 1 : 0, + static_cast(data_bytes)); + + try { + log_info("cuvs_vamana load delegating to diskann_memory: file=%s", file.c_str()); + diskann_memory_search_->load(file); + } catch (const std::exception& e) { + log_warn("cuvs_vamana load failed while loading diskann_memory: file=%s error=%s", + file.c_str(), + e.what()); + throw; + } catch (...) { + log_warn("cuvs_vamana load failed while loading diskann_memory: file=%s unknown error", + file.c_str()); + throw; + } + log_info("cuvs_vamana load finish: file=%s", file.c_str()); } template void cuvs_vamana::search( const T* queries, int batch_size, int k, algo_base::index_type* neighbors, float* distances) const { + static std::atomic search_logged{false}; + bool expected = false; + if (search_logged.compare_exchange_strong(expected, true)) { + log_info("cuvs_vamana search first call: batch_size=%d k=%d query_ptr=%p", + batch_size, + k, + static_cast(queries)); + } diskann_memory_search_->search(queries, batch_size, k, neighbors, distances); } diff --git a/cpp/bench/ann/src/diskann/diskann_benchmark.cpp b/cpp/bench/ann/src/diskann/diskann_benchmark.cpp index 4f6a382e26..aaa5133ce3 100644 --- a/cpp/bench/ann/src/diskann/diskann_benchmark.cpp +++ b/cpp/bench/ann/src/diskann/diskann_benchmark.cpp @@ -25,7 +25,7 @@ void parse_build_param(const nlohmann::json& conf, { param.R = conf.at("R"); param.L_build = conf.at("L_build"); - if (conf.contains("alpha")) { param.num_threads = conf.at("alpha"); } + if (conf.contains("alpha")) { param.alpha = conf.at("alpha"); } if (conf.contains("num_threads")) { param.num_threads = conf.at("num_threads"); } } diff --git a/cpp/bench/ann/src/diskann/diskann_wrapper.h b/cpp/bench/ann/src/diskann/diskann_wrapper.h index b99c3c6354..09edf9b6b7 100644 --- a/cpp/bench/ann/src/diskann/diskann_wrapper.h +++ b/cpp/bench/ann/src/diskann/diskann_wrapper.h @@ -16,14 +16,32 @@ #include #include +#include #include #include #include +#include #include +#include +#include +#include #include namespace cuvs::bench { +inline bool diskann_memory_artifact_exists(const std::string& file) +{ + std::error_code ec; + return std::filesystem::exists(file, ec); +} + +inline std::uintmax_t diskann_memory_artifact_size(const std::string& file) +{ + std::error_code ec; + auto size = std::filesystem::file_size(file, ec); + return ec ? 0 : size; +} + diskann::Metric parse_metric_to_diskann(cuvs::bench::Metric metric) { if (metric == cuvs::bench::Metric::kInnerProduct) { @@ -80,9 +98,13 @@ class diskann_memory : public algo { private: std::shared_ptr diskann_index_write_params_{nullptr}; uint32_t max_points_; - uint32_t build_pq_bytes_ = 0; - int num_threads_; - uint32_t L_search_; + uint32_t build_pq_bytes_ = 0; + uint32_t configured_build_pq_bytes_ = 0; + uint32_t graph_degree_ = 0; + uint32_t L_build_ = 0; + float alpha_ = 0.0f; + int num_threads_ = 0; + uint32_t L_search_ = 0; Mode bench_mode_; std::string index_path_prefix_; std::shared_ptr> mem_index_{nullptr}; @@ -94,6 +116,10 @@ diskann_memory::diskann_memory(Metric metric, int dim, const build_param& par : algo(metric, dim) { assert(this->dim_ > 0); + configured_build_pq_bytes_ = param.build_pq_bytes; + graph_degree_ = param.R; + L_build_ = param.L_build; + alpha_ = param.alpha; num_threads_ = param.num_threads; diskann_index_write_params_ = std::make_shared( diskann::IndexWriteParametersBuilder(param.L_build, param.R) @@ -124,8 +150,25 @@ void diskann_memory::initialize_index_(size_t max_points) template void diskann_memory::build(const T* dataset, size_t nrow) { + log_info( + "diskann_memory build start: nrow=%zu dim=%d graph_degree=%u L_build=%u alpha=%f " + "configured_build_pq_bytes=%u build_pq_bytes=%u threads=%d", + nrow, + this->dim_, + static_cast(graph_degree_), + static_cast(L_build_), + static_cast(alpha_), + static_cast(configured_build_pq_bytes_), + static_cast(build_pq_bytes_), + num_threads_); initialize_index_(nrow); - mem_index_->build(dataset, nrow, std::vector()); + try { + mem_index_->build(dataset, nrow, std::vector()); + } catch (const std::exception& e) { + log_warn("diskann_memory build failed: nrow=%zu dim=%d error=%s", nrow, this->dim_, e.what()); + throw; + } + log_info("diskann_memory build finish: nrow=%zu dim=%d", nrow, this->dim_); } template @@ -134,25 +177,66 @@ void diskann_memory::set_search_param(const search_param_base& param, const v if (filter_bitset != nullptr) { throw std::runtime_error("Filtering is not supported yet."); } auto sp = dynamic_cast(param); L_search_ = sp.L_search; + log_info("diskann_memory set_search_param: L_search=%u dim=%d", + static_cast(L_search_), + this->dim_); } template void diskann_memory::search( const T* queries, int batch_size, int k, algo_base::index_type* indices, float* distances) const { + static std::atomic search_logged{false}; + bool expected = false; + if (search_logged.compare_exchange_strong(expected, true)) { + log_info( + "diskann_memory search first call: prefix=%s batch_size=%d k=%d L_search=%u dim=%d " + "mode=%s benchmark_threads=%d", + index_path_prefix_.c_str(), + batch_size, + k, + static_cast(L_search_), + this->dim_, + bench_mode_ == Mode::kThroughput ? "throughput" : "latency", + cuvs::bench::benchmark_n_threads); + } + for (int i = 0; i < batch_size; i++) { - mem_index_->search(queries + i * this->dim_, - static_cast(k), - L_search_, - reinterpret_cast(indices + i * k), - distances + i * k); + try { + mem_index_->search(queries + i * this->dim_, + static_cast(k), + L_search_, + reinterpret_cast(indices + i * k), + distances + i * k); + } catch (const std::exception& e) { + log_warn( + "diskann_memory search failed: prefix=%s query_in_batch=%d batch_size=%d k=%d " + "L_search=%u error=%s", + index_path_prefix_.c_str(), + i, + batch_size, + k, + static_cast(L_search_), + e.what()); + throw; + } } } template void diskann_memory::save(const std::string& index_file) const { - this->mem_index_->save(index_file.c_str()); + log_info("diskann_memory save start: file=%s dim=%d", index_file.c_str(), this->dim_); + try { + this->mem_index_->save(index_file.c_str()); + } catch (const std::exception& e) { + log_warn("diskann_memory save failed: file=%s error=%s", index_file.c_str(), e.what()); + throw; + } + log_info("diskann_memory save finish: file=%s exists=%d bytes=%zu", + index_file.c_str(), + diskann_memory_artifact_exists(index_file) ? 1 : 0, + static_cast(diskann_memory_artifact_size(index_file))); } template @@ -162,10 +246,45 @@ void diskann_memory::load(const std::string& index_file) bench_mode_ = (cuvs::bench::benchmark_n_threads > 1) ? Mode::kThroughput : Mode::kLatency; - initialize_index_(0); - int load_threads = (bench_mode_ == Mode::kThroughput) ? cuvs::bench::benchmark_n_threads : 1; - this->mem_index_->load(index_path_prefix_.c_str(), load_threads, 2000); + log_info( + "diskann_memory load enter: prefix=%s dim=%d benchmark_threads=%d load_threads=%d " + "mode=%s build_threads=%d", + index_path_prefix_.c_str(), + this->dim_, + cuvs::bench::benchmark_n_threads, + load_threads, + bench_mode_ == Mode::kThroughput ? "throughput" : "latency", + num_threads_); + + try { + log_info("diskann_memory initialize_index start: prefix=%s max_points=0", + index_path_prefix_.c_str()); + initialize_index_(0); + log_info("diskann_memory initialize_index finish: prefix=%s", index_path_prefix_.c_str()); + } catch (const std::exception& e) { + log_warn("diskann_memory initialize_index failed: prefix=%s error=%s", + index_path_prefix_.c_str(), + e.what()); + throw; + } catch (...) { + log_warn("diskann_memory initialize_index failed: prefix=%s unknown error", + index_path_prefix_.c_str()); + throw; + } + + log_info("diskann_memory load start: prefix=%s", index_path_prefix_.c_str()); + try { + this->mem_index_->load(index_path_prefix_.c_str(), load_threads, 100); + } catch (const std::exception& e) { + log_warn( + "diskann_memory load failed: prefix=%s error=%s", index_path_prefix_.c_str(), e.what()); + throw; + } catch (...) { + log_warn("diskann_memory load failed: prefix=%s unknown error", index_path_prefix_.c_str()); + throw; + } + log_info("diskann_memory load finish: prefix=%s", index_path_prefix_.c_str()); } template diff --git a/python/cuvs_bench/cuvs_bench/backends/cpp_gbench.py b/python/cuvs_bench/cuvs_bench/backends/cpp_gbench.py index a888e957e5..e1e009be5c 100644 --- a/python/cuvs_bench/cuvs_bench/backends/cpp_gbench.py +++ b/python/cuvs_bench/cuvs_bench/backends/cpp_gbench.py @@ -67,6 +67,41 @@ def __init__(self, config: Dict[str, Any]): f"C++ benchmark executable not found: {self.executable_path}" ) + @staticmethod + def _gbench_error_message( + benchmarks: List[Dict[str, Any]], + phase: str, + output_path: Optional[Path] = None, + ) -> Optional[str]: + """Return a compact message if Google Benchmark reported row errors.""" + errors = [b for b in benchmarks if b.get("error_occurred")] + if not errors: + return None + + location = f" in {output_path}" if output_path else "" + details = [] + for bench in errors: + name = bench.get("name", "") + message = bench.get("error_message", "") + details.append(f"{name}: {message}") + return f"{phase} benchmark error(s){location}: " + "; ".join(details) + + @staticmethod + def _index_debug_summary( + indexes: List[IndexConfig], + ) -> List[Dict[str, Any]]: + """Return the C++ benchmark index payload fields useful in CI logs.""" + return [ + { + "name": idx.name, + "algo": idx.algo, + "file": idx.file, + "build_param": idx.build_param, + "search_params": idx.search_params, + } + for idx in indexes + ] + def build( self, dataset: Dataset, @@ -230,6 +265,17 @@ def build( start_time = time.perf_counter() try: + print("[cuvs-bench] C++ build launch", flush=True) + print(f" executable: {self.executable_path}", flush=True) + print(f" temp_config: {temp_config_path}", flush=True) + print(f" benchmark_out: {benchmark_out}", flush=True) + print(f" data_prefix: {self.data_prefix}", flush=True) + print( + " indexes: " + + json.dumps(self._index_debug_summary(indexes), default=str), + flush=True, + ) + print(f" command: {' '.join(cmd)}", flush=True) subprocess.run( cmd, check=True, @@ -253,6 +299,18 @@ def build( raise ValueError( "No benchmarks found in Google Benchmark output" ) + print( + f"[cuvs-bench] C++ build parsed {len(benchmarks)} " + f"benchmark row(s) from {benchmark_out}", + flush=True, + ) + + error_message = self._gbench_error_message( + benchmarks, "Build", benchmark_out + ) + if error_message: + print(f"[cuvs-bench] {error_message}", flush=True) + raise RuntimeError(error_message) # Return aggregated result total_build_time = sum(b.get("real_time", 0) for b in benchmarks) @@ -273,6 +331,11 @@ def build( ) except subprocess.CalledProcessError as e: + print( + f"[cuvs-bench] C++ build subprocess failed: " + f"returncode={e.returncode} stderr={e.stderr}", + flush=True, + ) return BuildResult( index_path=first_index.file, build_time_seconds=time.perf_counter() - start_time, @@ -284,6 +347,7 @@ def build( ) except Exception as e: + print(f"[cuvs-bench] C++ build exception: {e}", flush=True) return BuildResult( index_path=first_index.file, build_time_seconds=time.perf_counter() - start_time, @@ -490,6 +554,21 @@ def search( start_time = time.perf_counter() try: + print("[cuvs-bench] C++ search launch", flush=True) + print(f" executable: {self.executable_path}", flush=True) + print(f" temp_config: {temp_config_path}", flush=True) + print(f" temp_output_json: {temp_output_json}", flush=True) + print(f" output_json: {output_json}", flush=True) + print(f" data_prefix: {self.data_prefix}", flush=True) + print( + f" total_search_configs: {total_search_configs}", flush=True + ) + print( + " indexes: " + + json.dumps(self._index_debug_summary(indexes), default=str), + flush=True, + ) + print(f" command: {' '.join(cmd)}", flush=True) subprocess.run( cmd, check=True, @@ -527,6 +606,18 @@ def search( raise ValueError( "No benchmarks found in Google Benchmark output" ) + print( + f"[cuvs-bench] C++ search parsed {len(benchmarks)} " + f"benchmark row(s) from {output_json}", + flush=True, + ) + + error_message = self._gbench_error_message( + benchmarks, "Search", output_json + ) + if error_message: + print(f"[cuvs-bench] {error_message}", flush=True) + raise RuntimeError(error_message) # Aggregate metrics across all benchmarks total_search_time = sum(b.get("real_time", 0) for b in benchmarks) @@ -568,6 +659,11 @@ def search( ) except subprocess.CalledProcessError as e: + print( + f"[cuvs-bench] C++ search subprocess failed: " + f"returncode={e.returncode} stderr={e.stderr}", + flush=True, + ) return SearchResult( neighbors=np.array([]), distances=np.array([]), @@ -581,6 +677,7 @@ def search( ) except Exception as e: + print(f"[cuvs-bench] C++ search exception: {e}", flush=True) return SearchResult( neighbors=np.array([]), distances=np.array([]), diff --git a/python/cuvs_bench/cuvs_bench/config/algos/cuvs_brute_force.yaml b/python/cuvs_bench/cuvs_bench/config/algos/cuvs_brute_force.yaml index df6855fb42..d86d13fa79 100644 --- a/python/cuvs_bench/cuvs_bench/config/algos/cuvs_brute_force.yaml +++ b/python/cuvs_bench/cuvs_bench/config/algos/cuvs_brute_force.yaml @@ -3,3 +3,6 @@ groups: base: build: search: + test: + build: + search: diff --git a/python/cuvs_bench/cuvs_bench/config/algos/cuvs_cagra_diskann.yaml b/python/cuvs_bench/cuvs_bench/config/algos/cuvs_cagra_diskann.yaml index e086e8230b..50fe2acf6a 100644 --- a/python/cuvs_bench/cuvs_bench/config/algos/cuvs_cagra_diskann.yaml +++ b/python/cuvs_bench/cuvs_bench/config/algos/cuvs_cagra_diskann.yaml @@ -9,3 +9,10 @@ groups: graph_build_algo: ["NN_DESCENT"] search: L_search: [10, 20, 30, 40, 50, 100, 200, 300] + test: + build: + graph_degree: [32] + intermediate_graph_degree: [32] + graph_build_algo: ["NN_DESCENT"] + search: + L_search: [10] diff --git a/python/cuvs_bench/cuvs_bench/config/algos/cuvs_vamana.yaml b/python/cuvs_bench/cuvs_bench/config/algos/cuvs_vamana.yaml index b840962d92..cee6c9eaf8 100644 --- a/python/cuvs_bench/cuvs_bench/config/algos/cuvs_vamana.yaml +++ b/python/cuvs_bench/cuvs_bench/config/algos/cuvs_vamana.yaml @@ -8,3 +8,10 @@ groups: search: L_search: [10, 20, 30, 40, 50, 100, 200, 300] num_threads: [32] + test: + build: + graph_degree: [32] + visited_size: [128] + alpha: [1.2] + search: + L_search: [10] diff --git a/python/cuvs_bench/cuvs_bench/config/algos/diskann_memory.yaml b/python/cuvs_bench/cuvs_bench/config/algos/diskann_memory.yaml index b62f057084..1ec67cb044 100644 --- a/python/cuvs_bench/cuvs_bench/config/algos/diskann_memory.yaml +++ b/python/cuvs_bench/cuvs_bench/config/algos/diskann_memory.yaml @@ -10,3 +10,11 @@ groups: num_threads: [32] search: L_search: [10, 20, 30, 40, 50, 100, 200, 300] + test: + build: + R: [32] + L_build: [128] + alpha: [1.2] + num_threads: [1] + search: + L_search: [10] diff --git a/python/cuvs_bench/cuvs_bench/run/data_export.py b/python/cuvs_bench/cuvs_bench/run/data_export.py index 707677a083..b869bd57bf 100644 --- a/python/cuvs_bench/cuvs_bench/run/data_export.py +++ b/python/cuvs_bench/cuvs_bench/run/data_export.py @@ -1,5 +1,5 @@ # -# SPDX-FileCopyrightText: Copyright (c) 2024-2025, NVIDIA CORPORATION. +# SPDX-FileCopyrightText: Copyright (c) 2024-2026, NVIDIA CORPORATION. # SPDX-License-Identifier: Apache-2.0 # @@ -70,6 +70,12 @@ def read_json_files(dataset, dataset_path, method): DataFrame of JSON content. """ dir_path = os.path.join(dataset_path, dataset, "result", method) + if not os.path.isdir(dir_path): + print( + f"[cuvs-bench] Export skipped: result directory does not exist: " + f"{dir_path}" + ) + return for file in os.listdir(dir_path): if file.endswith(".json"): file_path = os.path.join(dir_path, file) @@ -103,6 +109,36 @@ def clean_algo_name(algo_name): return name.removesuffix(".json") +def log_missing_export_columns(file, df, required_columns, method): + """ + Print enough context to diagnose why a JSON result cannot become CSV. + """ + missing = [name for name in required_columns if name not in df.columns] + if not missing: + return False + + print( + f"[cuvs-bench] Cannot export {method} CSV for {file}: " + f"missing column(s) {missing}; available columns: {list(df.columns)}" + ) + + if "error_occurred" in df.columns: + error_rows = df[ + df["error_occurred"].map( + lambda value: value is True + or str(value).lower() in {"true", "1"} + ) + ] + for _, row in error_rows.iterrows(): + print( + "[cuvs-bench] Google Benchmark error row: " + f"name={row.get('name', '')} " + f"message={row.get('error_message', '')}" + ) + + return True + + def write_csv(file, algo_name, df, extra_columns=None, skip_cols=None): """ Write a DataFrame to CSV with specified columns skipped. @@ -120,6 +156,9 @@ def write_csv(file, algo_name, df, extra_columns=None, skip_cols=None): skip_cols : set, optional Set of columns to skip when writing to CSV (default is None). """ + if log_missing_export_columns(file, df, ["name", "real_time"], "build"): + return + df["name"] = df["name"].str.split("/").str[0] write_data = pd.DataFrame( { @@ -183,6 +222,14 @@ def convert_json_to_csv_search(dataset, dataset_path): f"{','.join(algo_name)}.csv", ) algo_name = clean_algo_name(algo_name) + if log_missing_export_columns( + file, + df, + ["name", "Recall", "items_per_second", "Latency"], + "search", + ): + continue + df["name"] = df["name"].str.split("/").str[0] write = pd.DataFrame( { @@ -213,9 +260,10 @@ def convert_json_to_csv_search(dataset, dataset_path): write["build GPU"] = None for col_idx in range(start_idx, len(build_df.columns)): col_name = build_df.columns[col_idx] - write[col_name] = None if col_name == "num_threads": write["build_num_threads"] = None + else: + write[col_name] = None for s_index, search_row in write.iterrows(): for b_index, build_row in build_df.iterrows(): if search_row["index_name"] == build_row["index_name"]: diff --git a/python/cuvs_bench/cuvs_bench/tests/test_cli.py b/python/cuvs_bench/cuvs_bench/tests/test_cli.py index fa2a63d041..15541dd29f 100644 --- a/python/cuvs_bench/cuvs_bench/tests/test_cli.py +++ b/python/cuvs_bench/cuvs_bench/tests/test_cli.py @@ -3,6 +3,7 @@ # SPDX-License-Identifier: Apache-2.0 # +import platform from pathlib import Path import pandas as pd @@ -11,6 +12,11 @@ from cuvs_bench.get_dataset.__main__ import main +def is_arm_cpu(): + """Check if running on ARM Linux architecture.""" + return platform.machine() in ("aarch64", "arm64") + + @pytest.fixture(scope="session") def temp_datasets_dir(tmp_path_factory): return tmp_path_factory.mktemp("datasets") @@ -51,7 +57,8 @@ def test_run_command_creates_results(temp_datasets_dir: Path): python -m cuvs_bench.run --dataset test-data --dataset-path datasets/ \ --algorithms faiss_gpu_ivf_flat,faiss_gpu_ivf_sq,cuvs_ivf_flat,\ - cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq \ + cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq,cuvs_vamana,\ + diskann_memory \ --batch-size 100 -k 10 --groups test -m latency --force It then verifies that the set of expected result files @@ -63,6 +70,11 @@ def test_run_command_creates_results(temp_datasets_dir: Path): from cuvs_bench.run.__main__ import main as run_main + skip_diskann = is_arm_cpu() + algorithms = "faiss_gpu_ivf_flat,faiss_gpu_ivf_sq,cuvs_ivf_flat,cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq" + if not skip_diskann: + algorithms += ",cuvs_vamana,diskann_memory" + runner = CliRunner() run_args = [ "--dataset", @@ -70,7 +82,7 @@ def test_run_command_creates_results(temp_datasets_dir: Path): "--dataset-path", dataset_path_arg, "--algorithms", - "faiss_gpu_ivf_flat,faiss_gpu_ivf_sq,cuvs_ivf_flat,cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq", # noqa: E501 + algorithms, "--batch-size", "100", "-k", @@ -105,6 +117,8 @@ def test_run_command_creates_results(temp_datasets_dir: Path): ] # --- Verify that the expected result files exist and are not empty --- + skip_diskann = is_arm_cpu() + expected_files = { # Build files: "test-data/result/build/cuvs_ivf_flat,test.csv": { @@ -423,6 +437,143 @@ def test_run_command_creates_results(temp_datasets_dir: Path): }, } + if not skip_diskann: + expected_files.update( + { + "test-data/result/build/cuvs_vamana,test.csv": { + "header": common_build_header + + [ + "GPU", + "alpha", + "graph_degree", + "visited_size", + ], + "rows": 1, + }, + "test-data/result/search/cuvs_vamana,test,k10,bs100,raw.csv": { + "header": common_search_header + + [ + "GPU", + "L_search", + "end_to_end", + "k", + "n_queries", + "total_queries", + "build time", + "build threads", + "build cpu_time", + "build GPU", + "alpha", + "graph_degree", + "visited_size", + ], + "rows": 1, + }, + "test-data/result/search/cuvs_vamana,test,k10,bs100,latency.csv": { + "header": common_search_header + + [ + "GPU", + "L_search", + "end_to_end", + "k", + "n_queries", + "total_queries", + "build time", + "build threads", + "build cpu_time", + "build GPU", + "alpha", + "graph_degree", + "visited_size", + ], + "rows": 1, + }, + "test-data/result/search/cuvs_vamana,test,k10,bs100,throughput.csv": { + "header": common_search_header + + [ + "GPU", + "L_search", + "end_to_end", + "k", + "n_queries", + "total_queries", + "build time", + "build threads", + "build cpu_time", + "build GPU", + "alpha", + "graph_degree", + "visited_size", + ], + "rows": 1, + }, + "test-data/result/build/diskann_memory,test.csv": { + "header": common_build_header + + [ + "L_build", + "R", + "alpha", + "num_threads", + ], + "rows": 1, + }, + "test-data/result/search/diskann_memory,test,k10,bs100,raw.csv": { + "header": common_search_header + + [ + "L_search", + "end_to_end", + "k", + "n_queries", + "total_queries", + "build time", + "build threads", + "build cpu_time", + "L_build", + "R", + "alpha", + "build_num_threads", + ], + "rows": 1, + }, + "test-data/result/search/diskann_memory,test,k10,bs100,latency.csv": { + "header": common_search_header + + [ + "L_search", + "end_to_end", + "k", + "n_queries", + "total_queries", + "build time", + "build threads", + "build cpu_time", + "L_build", + "R", + "alpha", + "build_num_threads", + ], + "rows": 1, + }, + "test-data/result/search/diskann_memory,test,k10,bs100,throughput.csv": { + "header": common_search_header + + [ + "L_search", + "end_to_end", + "k", + "n_queries", + "total_queries", + "build time", + "build threads", + "build cpu_time", + "L_build", + "R", + "alpha", + "build_num_threads", + ], + "rows": 1, + }, + } + ) + for rel_path, expectations in expected_files.items(): file_path = temp_datasets_dir / rel_path assert file_path.exists(), f"Expected file {file_path} does not exist." @@ -466,7 +617,8 @@ def test_plot_command_creates_png_files(temp_datasets_dir: Path): python -m cuvs_bench.plot --dataset test-data --dataset-path datasets/ \ --algorithms faiss_gpu_ivf_flat,faiss_gpu_ivf_sq, \ - cuvs_ivf_flat,cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq \ + cuvs_ivf_flat,cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq,\ + cuvs_vamana,diskann_memory \ --batch-size 100 -k 10 --groups test -m latency and then verifies that the following files are produced in the @@ -481,6 +633,11 @@ def test_plot_command_creates_png_files(temp_datasets_dir: Path): from cuvs_bench.plot.__main__ import main as plot_main + skip_diskann = is_arm_cpu() + algorithms = "faiss_gpu_ivf_flat,faiss_gpu_ivf_sq,cuvs_ivf_flat,cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq" + if not skip_diskann: + algorithms += ",cuvs_vamana,diskann_memory" + runner = CliRunner() args = [ "--dataset", @@ -490,7 +647,7 @@ def test_plot_command_creates_png_files(temp_datasets_dir: Path): "--output-filepath", dataset_path_arg, "--algorithms", - "faiss_gpu_ivf_flat,faiss_gpu_ivf_sq,cuvs_ivf_flat,cuvs_cagra,ggnn,cuvs_cagra_hnswlib,cuvs_ivf_pq", # noqa: E501 + algorithms, "--batch-size", "100", "-k",