Some checks failed
Fuzz Smoke / libFuzzer smoke (push) Has been cancelled
* chore(warnings): default-initialize aggregate members left out of brace-init
GCC -Wmissing-field-initializers fired about 1300 times where tests and
src build structs with a short positional brace list. Give the omitted
members an empty default member initializer. Value-initialization from
{} is what aggregate init already did for them, so behavior is
unchanged.
Signed-off-by: trvon <me@trevon.dev>
* chore(warnings): remove or annotate unused parameters, functions and variables
- init_command, s3_signer test, request_handler_lifetime test: comment out
the unused parameter name.
- video_content_handler: path is only read under YAMS_HAVE_FFPROBE.
- vector_memory_scaling_bench: main() takes no arguments.
- daemon_status_render: delete unused neutralText helper.
- DaemonMetrics: mallocStackLoggingNoCompactEnabled is only called in the
__APPLE__ block, so define it there.
- SocketServer: einval_streak only matters in the __APPLE__ EINVAL path.
- onnx_plugin_harness test, vector_backend_engine_compare bench,
work_coordinator test: mark platform-conditional or busy-loop
variables [[maybe_unused]].
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): fix sign-compare, range-loop, nodiscard and dangling-ref in tests
- search_admission_controller: count started workers in std::uint32_t so
the comparison with the uint32 cap is same-signed.
- p2p_manager: iterate the invalid-host list by const reference.
- integrity_verifier: discard the nodiscard results the tests do not
inspect with static_cast<void>.
- post_ingest_queue: findNode/findEdge take std::string_view so no
temporary std::string is created for the call (GCC
-Wdangling-reference false positive on the returned reference).
- topology_ablation_bench: the doc comment ended a line with a backslash,
which continues the comment (-Wcomment); show the env export on its own
line.
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): drop redundant std::move of a const captured json
The coroutine lambda is not mutable, so the captured optional<json> is
const and std::move(*progressToken) already selected the copy assignment.
Drop the std::move so the code states what actually happens.
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): mark __int128 use in db_integrity_stamp as an extension
-Wpedantic flags __int128. Use a __extension__ typedef, the GCC/Clang
idiom for an intentional extension, instead of the bare keyword.
Signed-off-by: trvon <git@trevon.dev>
* chore(warnings): remove always-true unsigned >= 0 comparisons
GCC -Wtype-limits flagged 51 test assertions of the form
CHECK(unsignedValue >= 0) that can never fail, plus a dead
`totalCount.value() < 0` clause in the graph listByType handler (the
count is std::size_t). In the tests keep the evaluation as a
static_cast<void> so the accessors are still exercised; in the handler
drop the dead clause and keep the range check.
Signed-off-by: trvon <me@trevon.dev>
---------
Signed-off-by: trvon <me@trevon.dev>
Signed-off-by: trvon <git@trevon.dev>
662 lines
26 KiB
C++
662 lines
26 KiB
C++
#include <algorithm>
|
|
#include <chrono>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <cstdlib>
|
|
#include <numeric>
|
|
#include <random>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <unordered_set>
|
|
#include <vector>
|
|
|
|
#if defined(__APPLE__)
|
|
#include <os/log.h>
|
|
#include <os/signpost.h>
|
|
#endif
|
|
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include "bench_utils.h"
|
|
#include <yams/vector/sqlite_vec_backend.h>
|
|
|
|
using namespace yams::vector;
|
|
|
|
namespace {
|
|
|
|
enum class ProfilePhase {
|
|
CorpusSetup,
|
|
GroundTruth,
|
|
SimeonPqIngest,
|
|
SimeonPqBuild,
|
|
SimeonPqReadiness,
|
|
SimeonPqSteadyQueries,
|
|
Vec0L2Ingest,
|
|
Vec0L2Build,
|
|
Vec0L2Readiness,
|
|
Vec0L2SteadyQueries,
|
|
ExactScanIngest,
|
|
ExactScanBuild,
|
|
ExactScanReadiness,
|
|
ExactScanSteadyQueries,
|
|
};
|
|
|
|
class ProfileInterval {
|
|
public:
|
|
explicit ProfileInterval(ProfilePhase phase) : phase_(phase) {
|
|
#if defined(__APPLE__)
|
|
id_ = os_signpost_id_generate(log());
|
|
begin();
|
|
#endif
|
|
}
|
|
|
|
~ProfileInterval() {
|
|
#if defined(__APPLE__)
|
|
end();
|
|
#endif
|
|
}
|
|
|
|
ProfileInterval(const ProfileInterval&) = delete;
|
|
ProfileInterval& operator=(const ProfileInterval&) = delete;
|
|
|
|
private:
|
|
#if defined(__APPLE__)
|
|
static os_log_t log() {
|
|
static os_log_t benchmarkLog =
|
|
os_log_create("org.yams.benchmarks", OS_LOG_CATEGORY_POINTS_OF_INTEREST);
|
|
return benchmarkLog;
|
|
}
|
|
|
|
void begin() const {
|
|
switch (phase_) {
|
|
case ProfilePhase::CorpusSetup:
|
|
os_signpost_interval_begin(log(), id_, "corpus_setup");
|
|
break;
|
|
case ProfilePhase::GroundTruth:
|
|
os_signpost_interval_begin(log(), id_, "ground_truth");
|
|
break;
|
|
case ProfilePhase::SimeonPqIngest:
|
|
os_signpost_interval_begin(log(), id_, "simeon_pq_ingest");
|
|
break;
|
|
case ProfilePhase::SimeonPqBuild:
|
|
os_signpost_interval_begin(log(), id_, "simeon_pq_build");
|
|
break;
|
|
case ProfilePhase::SimeonPqReadiness:
|
|
os_signpost_interval_begin(log(), id_, "simeon_pq_readiness");
|
|
break;
|
|
case ProfilePhase::SimeonPqSteadyQueries:
|
|
os_signpost_interval_begin(log(), id_, "simeon_pq_steady_queries");
|
|
break;
|
|
case ProfilePhase::Vec0L2Ingest:
|
|
os_signpost_interval_begin(log(), id_, "vec0_l2_ingest");
|
|
break;
|
|
case ProfilePhase::Vec0L2Build:
|
|
os_signpost_interval_begin(log(), id_, "vec0_l2_build");
|
|
break;
|
|
case ProfilePhase::Vec0L2Readiness:
|
|
os_signpost_interval_begin(log(), id_, "vec0_l2_readiness");
|
|
break;
|
|
case ProfilePhase::Vec0L2SteadyQueries:
|
|
os_signpost_interval_begin(log(), id_, "vec0_l2_steady_queries");
|
|
break;
|
|
case ProfilePhase::ExactScanIngest:
|
|
os_signpost_interval_begin(log(), id_, "exact_scan_ingest");
|
|
break;
|
|
case ProfilePhase::ExactScanBuild:
|
|
os_signpost_interval_begin(log(), id_, "exact_scan_build");
|
|
break;
|
|
case ProfilePhase::ExactScanReadiness:
|
|
os_signpost_interval_begin(log(), id_, "exact_scan_readiness");
|
|
break;
|
|
case ProfilePhase::ExactScanSteadyQueries:
|
|
os_signpost_interval_begin(log(), id_, "exact_scan_steady_queries");
|
|
break;
|
|
}
|
|
}
|
|
|
|
void end() const {
|
|
switch (phase_) {
|
|
case ProfilePhase::CorpusSetup:
|
|
os_signpost_interval_end(log(), id_, "corpus_setup");
|
|
break;
|
|
case ProfilePhase::GroundTruth:
|
|
os_signpost_interval_end(log(), id_, "ground_truth");
|
|
break;
|
|
case ProfilePhase::SimeonPqIngest:
|
|
os_signpost_interval_end(log(), id_, "simeon_pq_ingest");
|
|
break;
|
|
case ProfilePhase::SimeonPqBuild:
|
|
os_signpost_interval_end(log(), id_, "simeon_pq_build");
|
|
break;
|
|
case ProfilePhase::SimeonPqReadiness:
|
|
os_signpost_interval_end(log(), id_, "simeon_pq_readiness");
|
|
break;
|
|
case ProfilePhase::SimeonPqSteadyQueries:
|
|
os_signpost_interval_end(log(), id_, "simeon_pq_steady_queries");
|
|
break;
|
|
case ProfilePhase::Vec0L2Ingest:
|
|
os_signpost_interval_end(log(), id_, "vec0_l2_ingest");
|
|
break;
|
|
case ProfilePhase::Vec0L2Build:
|
|
os_signpost_interval_end(log(), id_, "vec0_l2_build");
|
|
break;
|
|
case ProfilePhase::Vec0L2Readiness:
|
|
os_signpost_interval_end(log(), id_, "vec0_l2_readiness");
|
|
break;
|
|
case ProfilePhase::Vec0L2SteadyQueries:
|
|
os_signpost_interval_end(log(), id_, "vec0_l2_steady_queries");
|
|
break;
|
|
case ProfilePhase::ExactScanIngest:
|
|
os_signpost_interval_end(log(), id_, "exact_scan_ingest");
|
|
break;
|
|
case ProfilePhase::ExactScanBuild:
|
|
os_signpost_interval_end(log(), id_, "exact_scan_build");
|
|
break;
|
|
case ProfilePhase::ExactScanReadiness:
|
|
os_signpost_interval_end(log(), id_, "exact_scan_readiness");
|
|
break;
|
|
case ProfilePhase::ExactScanSteadyQueries:
|
|
os_signpost_interval_end(log(), id_, "exact_scan_steady_queries");
|
|
break;
|
|
}
|
|
}
|
|
|
|
os_signpost_id_t id_ = OS_SIGNPOST_ID_INVALID;
|
|
#endif
|
|
[[maybe_unused]] ProfilePhase phase_;
|
|
};
|
|
|
|
ProfilePhase ingestPhase(VectorSearchEngine engine) {
|
|
switch (engine) {
|
|
case VectorSearchEngine::SimeonPqAdc:
|
|
return ProfilePhase::SimeonPqIngest;
|
|
case VectorSearchEngine::Vec0L2:
|
|
return ProfilePhase::Vec0L2Ingest;
|
|
case VectorSearchEngine::ExactScan:
|
|
return ProfilePhase::ExactScanIngest;
|
|
}
|
|
std::abort();
|
|
}
|
|
|
|
ProfilePhase buildPhase(VectorSearchEngine engine) {
|
|
switch (engine) {
|
|
case VectorSearchEngine::SimeonPqAdc:
|
|
return ProfilePhase::SimeonPqBuild;
|
|
case VectorSearchEngine::Vec0L2:
|
|
return ProfilePhase::Vec0L2Build;
|
|
case VectorSearchEngine::ExactScan:
|
|
return ProfilePhase::ExactScanBuild;
|
|
}
|
|
std::abort();
|
|
}
|
|
|
|
ProfilePhase readinessPhase(VectorSearchEngine engine) {
|
|
switch (engine) {
|
|
case VectorSearchEngine::SimeonPqAdc:
|
|
return ProfilePhase::SimeonPqReadiness;
|
|
case VectorSearchEngine::Vec0L2:
|
|
return ProfilePhase::Vec0L2Readiness;
|
|
case VectorSearchEngine::ExactScan:
|
|
return ProfilePhase::ExactScanReadiness;
|
|
}
|
|
std::abort();
|
|
}
|
|
|
|
ProfilePhase steadyQueriesPhase(VectorSearchEngine engine) {
|
|
switch (engine) {
|
|
case VectorSearchEngine::SimeonPqAdc:
|
|
return ProfilePhase::SimeonPqSteadyQueries;
|
|
case VectorSearchEngine::Vec0L2:
|
|
return ProfilePhase::Vec0L2SteadyQueries;
|
|
case VectorSearchEngine::ExactScan:
|
|
return ProfilePhase::ExactScanSteadyQueries;
|
|
}
|
|
std::abort();
|
|
}
|
|
|
|
enum class EngineSelection { Both, SimeonPq, Vec0L2 };
|
|
|
|
struct Config {
|
|
size_t corpus = 5000;
|
|
size_t queries = 100;
|
|
size_t dim = 128;
|
|
size_t k = 10;
|
|
unsigned int seed = 42;
|
|
EngineSelection engine = EngineSelection::Both;
|
|
bool spq_sweep = false;
|
|
bool filter_cell = false;
|
|
};
|
|
|
|
struct EngineResult {
|
|
const char* name = "";
|
|
double build_ms = 0.0;
|
|
double mean_us = 0.0;
|
|
double p95_us = 0.0;
|
|
double p99_us = 0.0;
|
|
double qps = 0.0;
|
|
double recall = 0.0;
|
|
};
|
|
|
|
Config parseArgs(int argc, char* argv[]) {
|
|
Config cfg;
|
|
for (int i = 1; i < argc; ++i) {
|
|
std::string_view arg(argv[i]);
|
|
auto parse = [&](std::string_view prefix, auto& out) {
|
|
if (arg.rfind(prefix, 0) == 0) {
|
|
out = static_cast<std::decay_t<decltype(out)>>(
|
|
std::stoull(std::string(arg.substr(prefix.size()))));
|
|
return true;
|
|
}
|
|
return false;
|
|
};
|
|
if (parse("--corpus=", cfg.corpus) || parse("--queries=", cfg.queries) ||
|
|
parse("--dim=", cfg.dim) || parse("--k=", cfg.k) || parse("--seed=", cfg.seed)) {
|
|
continue;
|
|
}
|
|
if (arg.rfind("--engine=", 0) == 0) {
|
|
const auto engine = arg.substr(std::string_view{"--engine="}.size());
|
|
if (engine == "both") {
|
|
cfg.engine = EngineSelection::Both;
|
|
} else if (engine == "simeon") {
|
|
cfg.engine = EngineSelection::SimeonPq;
|
|
} else if (engine == "vec0") {
|
|
cfg.engine = EngineSelection::Vec0L2;
|
|
} else {
|
|
throw std::invalid_argument("--engine must be both, simeon, or vec0");
|
|
}
|
|
continue;
|
|
}
|
|
if (arg == "--spq-sweep") {
|
|
cfg.spq_sweep = true;
|
|
continue;
|
|
}
|
|
if (arg == "--filter-cell") {
|
|
cfg.filter_cell = true;
|
|
continue;
|
|
}
|
|
if (arg == "--help") {
|
|
std::printf("Usage: %s [--corpus=N] [--queries=N] [--dim=N] [--k=N] [--seed=N] "
|
|
"[--engine=both|simeon|vec0] [--spq-sweep] [--filter-cell]\n",
|
|
argv[0]);
|
|
std::exit(0);
|
|
}
|
|
}
|
|
if (cfg.queries == 0) {
|
|
throw std::invalid_argument("--queries must be >= 1");
|
|
}
|
|
if (cfg.k == 0) {
|
|
throw std::invalid_argument("--k must be >= 1");
|
|
}
|
|
return cfg;
|
|
}
|
|
|
|
std::vector<float> generateNormalizedVector(size_t dim, std::mt19937& rng) {
|
|
std::uniform_real_distribution<float> dist(-1.0f, 1.0f);
|
|
std::vector<float> vec(dim);
|
|
float norm_sq = 0.0f;
|
|
for (auto& value : vec) {
|
|
value = dist(rng);
|
|
norm_sq += value * value;
|
|
}
|
|
float norm = std::sqrt(norm_sq);
|
|
if (norm > 0.0f) {
|
|
for (auto& value : vec) {
|
|
value /= norm;
|
|
}
|
|
}
|
|
return vec;
|
|
}
|
|
|
|
std::vector<std::vector<float>> generateCorpus(size_t count, size_t dim, std::mt19937& rng) {
|
|
std::vector<std::vector<float>> corpus;
|
|
corpus.reserve(count);
|
|
for (size_t i = 0; i < count; ++i) {
|
|
corpus.push_back(generateNormalizedVector(dim, rng));
|
|
}
|
|
return corpus;
|
|
}
|
|
|
|
std::vector<size_t> bruteForceTopK(const std::vector<float>& query,
|
|
const std::vector<std::vector<float>>& corpus, size_t k) {
|
|
std::vector<std::pair<size_t, float>> scored;
|
|
scored.reserve(corpus.size());
|
|
for (size_t i = 0; i < corpus.size(); ++i) {
|
|
float dot = 0.0f;
|
|
for (size_t j = 0; j < query.size(); ++j) {
|
|
dot += query[j] * corpus[i][j];
|
|
}
|
|
scored.emplace_back(i, dot);
|
|
}
|
|
|
|
const size_t top = std::min(k, scored.size());
|
|
std::partial_sort(scored.begin(), scored.begin() + top, scored.end(),
|
|
[](const auto& lhs, const auto& rhs) { return lhs.second > rhs.second; });
|
|
|
|
std::vector<size_t> ids;
|
|
ids.reserve(top);
|
|
for (size_t i = 0; i < top; ++i) {
|
|
ids.push_back(scored[i].first);
|
|
}
|
|
return ids;
|
|
}
|
|
|
|
double percentile(std::vector<double> values, double p) {
|
|
return yams::bench::percentileNearestRank(std::move(values), p);
|
|
}
|
|
|
|
EngineResult runEngine(VectorSearchEngine engine, const Config& cfg,
|
|
const std::vector<std::vector<float>>& corpus,
|
|
const std::vector<std::vector<float>>& queries,
|
|
const std::vector<std::vector<size_t>>& ground_truth,
|
|
size_t spq_subquantizers = 0, size_t spq_rerank_factor = 0) {
|
|
SqliteVecBackend::Config backend_cfg;
|
|
backend_cfg.embedding_dim = cfg.dim;
|
|
backend_cfg.search_engine = engine;
|
|
if (spq_subquantizers > 0) {
|
|
backend_cfg.simeon_pq_subquantizers = spq_subquantizers;
|
|
}
|
|
if (spq_rerank_factor > 0) {
|
|
backend_cfg.simeon_pq_rerank_factor = spq_rerank_factor;
|
|
}
|
|
|
|
SqliteVecBackend backend(backend_cfg);
|
|
auto init = backend.initialize(":memory:");
|
|
if (!init) {
|
|
throw std::runtime_error(init.error().message);
|
|
}
|
|
auto create = backend.createTables(cfg.dim);
|
|
if (!create) {
|
|
throw std::runtime_error(create.error().message);
|
|
}
|
|
|
|
std::vector<VectorRecord> records;
|
|
records.reserve(corpus.size());
|
|
for (size_t i = 0; i < corpus.size(); ++i) {
|
|
VectorRecord rec;
|
|
rec.chunk_id = "chunk_" + std::to_string(i);
|
|
rec.document_hash = "doc_" + std::to_string(i / 4);
|
|
rec.embedding = corpus[i];
|
|
rec.content = "benchmark content " + std::to_string(i);
|
|
records.push_back(std::move(rec));
|
|
}
|
|
{
|
|
ProfileInterval profile(ingestPhase(engine));
|
|
auto insert = backend.insertVectorsBatch(records);
|
|
if (!insert) {
|
|
throw std::runtime_error(insert.error().message);
|
|
}
|
|
}
|
|
|
|
const auto build_start = std::chrono::steady_clock::now();
|
|
{
|
|
ProfileInterval profile(buildPhase(engine));
|
|
auto build = backend.buildIndex();
|
|
if (!build) {
|
|
throw std::runtime_error(build.error().message);
|
|
}
|
|
}
|
|
const auto build_end = std::chrono::steady_clock::now();
|
|
|
|
// Exclude one-time lazy index initialization from steady-state query timing. Run the same
|
|
// unmeasured query for every engine so Vec0, Simeon PQ, and exact scan enter the measured loop
|
|
// from the same query-ready boundary. The readiness signpost keeps this excluded work visible
|
|
// to Instruments and prevents it from being mistaken for build or steady-query work.
|
|
{
|
|
ProfileInterval profile(readinessPhase(engine));
|
|
auto warmup = backend.searchSimilar(queries.front(), cfg.k, 0.0f, std::nullopt, {});
|
|
if (!warmup) {
|
|
throw std::runtime_error(warmup.error().message);
|
|
}
|
|
}
|
|
|
|
std::vector<double> latencies_us;
|
|
latencies_us.reserve(queries.size());
|
|
size_t total_hits = 0;
|
|
|
|
{
|
|
ProfileInterval profile(steadyQueriesPhase(engine));
|
|
for (size_t i = 0; i < queries.size(); ++i) {
|
|
const auto start = std::chrono::steady_clock::now();
|
|
auto result = backend.searchSimilar(queries[i], cfg.k, 0.0f, std::nullopt, {});
|
|
const auto end = std::chrono::steady_clock::now();
|
|
if (!result) {
|
|
throw std::runtime_error(result.error().message);
|
|
}
|
|
|
|
latencies_us.push_back(std::chrono::duration<double, std::micro>(end - start).count());
|
|
|
|
std::unordered_set<std::string> expected;
|
|
for (size_t idx : ground_truth[i]) {
|
|
expected.insert("chunk_" + std::to_string(idx));
|
|
}
|
|
for (const auto& rec : result.value()) {
|
|
total_hits += expected.contains(rec.chunk_id) ? 1 : 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
const double total_us = std::accumulate(latencies_us.begin(), latencies_us.end(), 0.0);
|
|
EngineResult out;
|
|
switch (engine) {
|
|
case VectorSearchEngine::SimeonPqAdc:
|
|
out.name = "simeon-pq";
|
|
break;
|
|
case VectorSearchEngine::Vec0L2:
|
|
out.name = "vec0-l2";
|
|
break;
|
|
case VectorSearchEngine::ExactScan:
|
|
out.name = "exact-scan";
|
|
break;
|
|
}
|
|
out.build_ms = std::chrono::duration<double, std::milli>(build_end - build_start).count();
|
|
out.mean_us = total_us / static_cast<double>(latencies_us.size());
|
|
out.p95_us = percentile(latencies_us, 0.95);
|
|
out.p99_us = percentile(latencies_us, 0.99);
|
|
out.qps = out.mean_us > 0.0 ? (1e6 / out.mean_us) : 0.0;
|
|
out.recall = static_cast<double>(total_hits) / static_cast<double>(queries.size() * cfg.k);
|
|
return out;
|
|
}
|
|
|
|
void printResult(const EngineResult& result, size_t k) {
|
|
std::printf("%-12s build=%8.1f ms mean=%8.1f us p95=%8.1f us p99=%8.1f us qps=%8.0f "
|
|
"recall@%zu=%5.1f%%\n",
|
|
result.name, result.build_ms, result.mean_us, result.p95_us, result.p99_us,
|
|
result.qps, k, result.recall * 100.0);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
int main(int argc, char* argv[]) {
|
|
spdlog::set_level(spdlog::level::warn);
|
|
|
|
try {
|
|
const Config cfg = parseArgs(argc, argv);
|
|
std::mt19937 rng(cfg.seed);
|
|
std::vector<std::vector<float>> corpus;
|
|
std::vector<std::vector<float>> queries;
|
|
{
|
|
ProfileInterval profile(ProfilePhase::CorpusSetup);
|
|
corpus = generateCorpus(cfg.corpus, cfg.dim, rng);
|
|
queries = generateCorpus(cfg.queries, cfg.dim, rng);
|
|
}
|
|
|
|
std::vector<std::vector<size_t>> ground_truth;
|
|
{
|
|
ProfileInterval profile(ProfilePhase::GroundTruth);
|
|
ground_truth.reserve(queries.size());
|
|
for (const auto& query : queries) {
|
|
ground_truth.push_back(bruteForceTopK(query, corpus, cfg.k));
|
|
}
|
|
}
|
|
|
|
std::printf("============================================================\n");
|
|
std::printf("YAMS Vector Backend Engine Compare\n");
|
|
std::printf("============================================================\n");
|
|
std::printf("corpus=%zu queries=%zu dim=%zu k=%zu seed=%u\n", cfg.corpus, cfg.queries,
|
|
cfg.dim, cfg.k, cfg.seed);
|
|
std::printf("note: corpus/query vectors are normalized, so cosine and L2 rankings are "
|
|
"comparable\n\n");
|
|
|
|
if (cfg.filter_cell) {
|
|
std::printf("filtered-search cell: routed Simeon PQ/ADC versus exact allowed-set "
|
|
"control\n");
|
|
SqliteVecBackend::Config backend_cfg;
|
|
backend_cfg.embedding_dim = cfg.dim;
|
|
backend_cfg.search_engine = VectorSearchEngine::SimeonPqAdc;
|
|
SqliteVecBackend backend(backend_cfg);
|
|
if (!backend.initialize(":memory:")) {
|
|
throw std::runtime_error("init failed");
|
|
}
|
|
if (!backend.createTables(cfg.dim)) {
|
|
throw std::runtime_error("createTables failed");
|
|
}
|
|
std::vector<VectorRecord> records;
|
|
records.reserve(corpus.size());
|
|
for (size_t i = 0; i < corpus.size(); ++i) {
|
|
VectorRecord rec;
|
|
rec.chunk_id = "chunk_" + std::to_string(i);
|
|
rec.document_hash = "doc_" + std::to_string(i / 4);
|
|
rec.embedding = corpus[i];
|
|
rec.content = "benchmark content " + std::to_string(i);
|
|
records.push_back(std::move(rec));
|
|
}
|
|
if (!backend.insertVectorsBatch(records)) {
|
|
throw std::runtime_error("insert failed");
|
|
}
|
|
const auto buildStart = std::chrono::steady_clock::now();
|
|
if (!backend.buildIndex()) {
|
|
throw std::runtime_error("buildIndex failed");
|
|
}
|
|
const auto buildEnd = std::chrono::steady_clock::now();
|
|
std::printf("index_build_ms=%.1f vectors=%zu documents=%zu chunks_per_document=4\n",
|
|
std::chrono::duration<double, std::milli>(buildEnd - buildStart).count(),
|
|
cfg.corpus, (cfg.corpus + 3) / 4);
|
|
|
|
const size_t documentCount = (cfg.corpus + 3) / 4;
|
|
const std::vector<size_t> candidateCounts = {2UL, 7UL, 16UL, 64UL, documentCount};
|
|
for (const size_t nCandidates : candidateCounts) {
|
|
const size_t candidateCount = std::min(nCandidates, documentCount);
|
|
std::unordered_set<std::string> candidates;
|
|
for (size_t i = 0; i < candidateCount; ++i) {
|
|
candidates.insert("doc_" + std::to_string(i));
|
|
}
|
|
|
|
VectorSearchDiagnostics warmupDiagnostics;
|
|
auto warmup = backend.searchSimilarWithDiagnostics(
|
|
queries.front(), cfg.k, -1.0F, std::nullopt, candidates, {}, warmupDiagnostics);
|
|
if (!warmup) {
|
|
throw std::runtime_error(warmup.error().message);
|
|
}
|
|
|
|
std::vector<double> annLatencies;
|
|
std::vector<double> exactLatencies;
|
|
annLatencies.reserve(queries.size());
|
|
exactLatencies.reserve(queries.size());
|
|
std::uint64_t lookupNs = 0;
|
|
std::uint64_t projectionNs = 0;
|
|
std::uint64_t lutNs = 0;
|
|
std::uint64_t adcNs = 0;
|
|
std::uint64_t selectionNs = 0;
|
|
std::uint64_t materializationNs = 0;
|
|
std::uint64_t rerankNs = 0;
|
|
size_t candidateIndexPayloadBytes = 0;
|
|
size_t recallHits = 0;
|
|
size_t recallDenominator = 0;
|
|
for (const auto& q : queries) {
|
|
VectorSearchDiagnostics annDiagnostics;
|
|
const auto annStart = std::chrono::steady_clock::now();
|
|
auto ann = backend.searchSimilarWithDiagnostics(q, cfg.k, -1.0F, std::nullopt,
|
|
candidates, {}, annDiagnostics);
|
|
const auto annEnd = std::chrono::steady_clock::now();
|
|
if (!ann) {
|
|
throw std::runtime_error(ann.error().message);
|
|
}
|
|
VectorSearchDiagnostics exactDiagnostics;
|
|
const auto exactStart = std::chrono::steady_clock::now();
|
|
auto exact = backend.searchExactCandidatesWithDiagnostics(
|
|
q, cfg.k, -1.0F, candidates, exactDiagnostics);
|
|
const auto exactEnd = std::chrono::steady_clock::now();
|
|
if (!exact) {
|
|
throw std::runtime_error(exact.error().message);
|
|
}
|
|
|
|
annLatencies.push_back(
|
|
std::chrono::duration<double, std::micro>(annEnd - annStart).count());
|
|
exactLatencies.push_back(
|
|
std::chrono::duration<double, std::micro>(exactEnd - exactStart).count());
|
|
lookupNs += annDiagnostics.candidateLookupNanoseconds;
|
|
projectionNs += annDiagnostics.candidateProjectionNanoseconds;
|
|
lutNs += annDiagnostics.pqLutNanoseconds;
|
|
adcNs += annDiagnostics.adcScoringNanoseconds;
|
|
selectionNs += annDiagnostics.topKSelectionNanoseconds;
|
|
materializationNs += annDiagnostics.resultMaterializationNanoseconds;
|
|
rerankNs += annDiagnostics.exactRerankNanoseconds;
|
|
candidateIndexPayloadBytes = std::max(
|
|
candidateIndexPayloadBytes, annDiagnostics.candidateIndexPayloadBytes);
|
|
|
|
std::unordered_set<std::string> exactIds;
|
|
for (const auto& record : exact.value()) {
|
|
exactIds.insert(record.chunk_id);
|
|
}
|
|
recallDenominator += exactIds.size();
|
|
for (const auto& record : ann.value()) {
|
|
recallHits += exactIds.contains(record.chunk_id) ? 1 : 0;
|
|
}
|
|
}
|
|
const double divisor = static_cast<double>(queries.size());
|
|
const double annMean =
|
|
std::accumulate(annLatencies.begin(), annLatencies.end(), 0.0) / divisor;
|
|
const double exactMean =
|
|
std::accumulate(exactLatencies.begin(), exactLatencies.end(), 0.0) / divisor;
|
|
const auto meanUs = [divisor](std::uint64_t nanoseconds) {
|
|
return static_cast<double>(nanoseconds) / (1000.0 * divisor);
|
|
};
|
|
const double recall =
|
|
recallDenominator == 0
|
|
? 1.0
|
|
: static_cast<double>(recallHits) / static_cast<double>(recallDenominator);
|
|
std::printf(
|
|
"allowed_docs=%2zu allowed_vectors<=%3zu ann_mean=%8.1f us ann_p95=%8.1f us "
|
|
"exact_mean=%8.1f us recall=%5.1f%% phases_us{lookup=%.1f projection=%.1f "
|
|
"lut=%.1f adc=%.1f select=%.1f materialize=%.1f rerank=%.1f} "
|
|
"candidate_index_payload_kib=%.1f\n",
|
|
candidateCount, candidateCount * 4, annMean, percentile(annLatencies, 0.95),
|
|
exactMean, recall * 100.0, meanUs(lookupNs), meanUs(projectionNs),
|
|
meanUs(lutNs), meanUs(adcNs), meanUs(selectionNs), meanUs(materializationNs),
|
|
meanUs(rerankNs), static_cast<double>(candidateIndexPayloadBytes) / 1024.0);
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (cfg.spq_sweep) {
|
|
std::printf("simeon-pq sweep: subquantizers x rerank_factor\n");
|
|
for (size_t m : {8UL, 16UL, 32UL, 64UL}) {
|
|
if (cfg.dim % m != 0) {
|
|
continue;
|
|
}
|
|
for (size_t rerank : {2UL, 4UL, 8UL}) {
|
|
auto result = runEngine(VectorSearchEngine::SimeonPqAdc, cfg, corpus, queries,
|
|
ground_truth, m, rerank);
|
|
std::printf("m=%-3zu rerank=%zu ", m, rerank);
|
|
printResult(result, cfg.k);
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
if (cfg.engine != EngineSelection::Vec0L2) {
|
|
const auto spq =
|
|
runEngine(VectorSearchEngine::SimeonPqAdc, cfg, corpus, queries, ground_truth);
|
|
printResult(spq, cfg.k);
|
|
}
|
|
if (cfg.engine != EngineSelection::SimeonPq) {
|
|
const auto vec0 =
|
|
runEngine(VectorSearchEngine::Vec0L2, cfg, corpus, queries, ground_truth);
|
|
printResult(vec0, cfg.k);
|
|
}
|
|
return 0;
|
|
} catch (const std::exception& e) {
|
|
std::fprintf(stderr, "Benchmark failed: %s\n", e.what());
|
|
return 1;
|
|
}
|
|
}
|