yams/tests/benchmarks/topology_build_bench.cpp
trvon 745818e67d
perf(topology): streamline artifact construction
Reuse admitted relations and dense splitter state while preserving deterministic identities, centroids, and the 64-document safety cap.

Add corpus-shaped benchmarks so construction regressions stay measurable.
2026-08-03 17:52:58 -04:00

561 lines
24 KiB
C++

// Focused benchmark for ConnectedComponentTopologyEngine::buildArtifacts() construction cost.
//
// This deliberately excludes database extraction, artifact persistence, topology routing, and
// retrieval quality. Profile this executable to attribute construction work without daemon noise.
//
// Production-safe construction controls are encoded explicitly rather than read from product
// environment variables: minEdgeScore=0.25, maxComponentDocs=64, reciprocalOnly=true.
// Harness-only controls:
// YAMS_BENCH_TOPO_BUILD_DOCS largest document scale (default 4096)
// YAMS_BENCH_TOPO_BUILD_REPEATS measured builds per point (default 30)
// YAMS_BENCH_TOPO_BUILD_DIMENSIONS embedding dimensions (default 1024)
// YAMS_BENCH_TOPO_BUILD_NEIGHBORS neighbors per document (default 32)
// YAMS_BENCH_TOPO_BUILD_SHAPE optional fragmented_singletons,
// clustered_low_score_noise, giant_component, or
// many_oversized_components filter
// YAMS_BENCH_TOPO_BUILD_CONFIG optional safe_defaults/unbounded_control filter
// YAMS_BENCH_TOPO_BUILD_ONLY_MAX run only the largest scale when set to 1
// YAMS_BENCH_OUTPUT optional JSONL output path
// YAMS_BENCH_RUN_ID optional run identifier
#include <yams/topology/topology_baseline.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <bit>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iostream>
#include <numeric>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace fs = std::filesystem;
using json = nlohmann::json;
using yams::topology::ConnectedComponentTopologyEngine;
using yams::topology::TopologyArtifactBatch;
using yams::topology::TopologyBuildConfig;
using yams::topology::TopologyBuildMode;
using yams::topology::TopologyDocumentInput;
using yams::topology::TopologyInputKind;
using yams::topology::TopologyNeighbor;
namespace {
constexpr std::size_t kComponentSize = 32;
constexpr std::size_t kOversizedComponentSize = 65;
constexpr std::size_t kFragmentedSingletonsPerCycle = 22;
constexpr std::size_t kFragmentedConnectedDocumentsPerCycle = 19;
constexpr std::size_t kFragmentedCycleDocuments =
kFragmentedSingletonsPerCycle + kFragmentedConnectedDocumentsPerCycle;
constexpr std::size_t kDefaultNeighborsPerDocument = 32;
constexpr std::size_t kDefaultEmbeddingDimensions = 1024;
constexpr std::size_t kSafeMaxComponentDocuments = 64;
constexpr double kSafeMinimumEdgeScore = 0.25;
std::size_t envSize(std::string_view name, std::size_t fallback) {
const std::string key{name};
const char* raw = std::getenv(key.c_str());
if (raw == nullptr || *raw == '\0') {
return fallback;
}
char* end = nullptr;
const auto parsed = std::strtoull(raw, &end, 10);
if (end == raw || *end != '\0') {
return fallback;
}
return static_cast<std::size_t>(parsed);
}
std::string envString(std::string_view name, std::string_view fallback = {}) {
const std::string key{name};
const char* raw = std::getenv(key.c_str());
return raw == nullptr ? std::string{fallback} : std::string{raw};
}
std::string makeHash(std::size_t component, std::size_t member) {
return "doc-" + std::to_string(component) + '-' + std::to_string(member);
}
enum class GraphShape : std::uint8_t {
FragmentedSingletons,
ClusteredLowScoreNoise,
GiantComponent,
ManyOversizedComponents
};
std::string_view shapeName(GraphShape shape) {
switch (shape) {
case GraphShape::FragmentedSingletons:
return "fragmented_singletons";
case GraphShape::ClusteredLowScoreNoise:
return "clustered_low_score_noise";
case GraphShape::GiantComponent:
return "giant_component";
case GraphShape::ManyOversizedComponents:
return "many_oversized_components";
}
return "unknown";
}
struct WorkloadConfig {
std::size_t embeddingDimensions{kDefaultEmbeddingDimensions};
std::size_t neighborsPerDocument{kDefaultNeighborsPerDocument};
};
std::vector<TopologyDocumentInput> synthesizeDocuments(std::size_t documentCount, GraphShape shape,
const WorkloadConfig& workload) {
std::vector<TopologyDocumentInput> documents(documentCount);
for (std::size_t i = 0; i < documentCount; ++i) {
const std::size_t componentSize =
shape == GraphShape::ManyOversizedComponents ? kOversizedComponentSize : kComponentSize;
const std::size_t component = i / componentSize;
const std::size_t member = i % componentSize;
const std::size_t componentStart = component * componentSize;
const std::size_t componentEnd = std::min(componentStart + componentSize, documentCount);
const std::size_t componentDocumentCount = componentEnd - componentStart;
auto& document = documents[i];
document.documentHash = makeHash(i / kComponentSize, i % kComponentSize);
document.filePath = "/synth/topology-build/" + document.documentHash;
document.embedding.assign(workload.embeddingDimensions, 0.0F);
document.embedding[component % workload.embeddingDimensions] = 1.0F;
document.embedding[i % workload.embeddingDimensions] += 0.05F;
document.neighbors.reserve(workload.neighborsPerDocument + 1);
// Twenty-two singleton clusters plus one 19-document cluster per cycle models the
// persisted corpus's 95.7% singleton-cluster ratio and roughly 55% singleton documents.
std::size_t neighborGroupStart = componentStart;
std::size_t neighborGroupMember = member;
std::size_t neighborGroupDocumentCount = componentDocumentCount;
bool connectedComponent = true;
if (shape == GraphShape::FragmentedSingletons) {
const std::size_t cycleStart =
(i / kFragmentedCycleDocuments) * kFragmentedCycleDocuments;
const std::size_t cycleMember = i - cycleStart;
connectedComponent = cycleMember >= kFragmentedSingletonsPerCycle;
if (connectedComponent) {
neighborGroupStart = cycleStart + kFragmentedSingletonsPerCycle;
neighborGroupMember = i - neighborGroupStart;
neighborGroupDocumentCount =
std::min(cycleStart + kFragmentedCycleDocuments, documentCount) -
neighborGroupStart;
}
}
const std::size_t localNeighborCount =
connectedComponent
? std::min(workload.neighborsPerDocument, neighborGroupDocumentCount > 0
? neighborGroupDocumentCount - 1
: std::size_t{0})
: 0;
for (std::size_t offset = 1; offset <= localNeighborCount; ++offset) {
const std::size_t neighbor =
neighborGroupStart + ((neighborGroupMember + offset) % neighborGroupDocumentCount);
document.neighbors.push_back(TopologyNeighbor{
.documentHash = makeHash(neighbor / kComponentSize, neighbor % kComponentSize),
.score = 0.90F - (static_cast<float>(offset) * 0.01F),
.reciprocal = true,
});
}
const bool componentBoundary = (i + 1 == componentEnd) && componentEnd < documentCount;
const bool connectComponents =
shape == GraphShape::ClusteredLowScoreNoise || shape == GraphShape::GiantComponent;
if (componentBoundary && connectComponents) {
document.neighbors.push_back(TopologyNeighbor{
.documentHash =
makeHash(componentEnd / kComponentSize, componentEnd % kComponentSize),
.score = shape == GraphShape::GiantComponent ? 0.90F : 0.10F,
.reciprocal = true,
});
}
}
return documents;
}
class ArtifactFingerprintBuilder {
public:
void appendInteger(std::uint64_t value) noexcept {
for (std::size_t byte = 0; byte < sizeof(value); ++byte) {
fingerprint_ ^= static_cast<unsigned char>(value >> (byte * 8U));
fingerprint_ *= kFnvPrime;
}
}
void appendString(std::string_view value) noexcept {
appendInteger(value.size());
for (const unsigned char byte : value) {
fingerprint_ ^= byte;
fingerprint_ *= kFnvPrime;
}
}
void appendOptionalString(const std::optional<std::string>& value) noexcept {
appendInteger(value.has_value() ? 1U : 0U);
if (value.has_value()) {
appendString(*value);
}
}
void appendFloat(float value) noexcept { appendInteger(std::bit_cast<std::uint32_t>(value)); }
void appendDouble(double value) noexcept { appendInteger(std::bit_cast<std::uint64_t>(value)); }
[[nodiscard]] std::uint64_t value() const noexcept { return fingerprint_; }
private:
static constexpr std::uint64_t kFnvOffset = 1469598103934665603ULL;
static constexpr std::uint64_t kFnvPrime = 1099511628211ULL;
std::uint64_t fingerprint_{kFnvOffset};
};
std::uint64_t artifactFingerprint(const TopologyArtifactBatch& batch) {
ArtifactFingerprintBuilder fingerprint;
// Snapshot ids, wall-clock timestamps, and publication epochs are intentionally excluded.
fingerprint.appendString(batch.algorithm);
fingerprint.appendInteger(static_cast<std::uint8_t>(batch.inputKind));
fingerprint.appendString(batch.embeddingSpaceIdentity);
fingerprint.appendString(batch.protectedRelationIdentity);
fingerprint.appendInteger(batch.clusters.size());
for (const auto& cluster : batch.clusters) {
fingerprint.appendString(cluster.clusterId);
fingerprint.appendOptionalString(cluster.parentClusterId);
fingerprint.appendInteger(cluster.level);
fingerprint.appendInteger(cluster.memberCount);
fingerprint.appendDouble(cluster.persistenceScore);
fingerprint.appendDouble(cluster.cohesionScore);
fingerprint.appendDouble(cluster.densityScore);
fingerprint.appendDouble(cluster.bridgeMass);
fingerprint.appendInteger(cluster.protectedPairCount);
fingerprint.appendInteger(cluster.preservedProtectedPairCount);
fingerprint.appendInteger(cluster.medoid.has_value() ? 1U : 0U);
if (cluster.medoid.has_value()) {
fingerprint.appendString(cluster.medoid->clusterId);
fingerprint.appendString(cluster.medoid->documentHash);
fingerprint.appendString(cluster.medoid->filePath);
fingerprint.appendDouble(cluster.medoid->representativeScore);
}
fingerprint.appendInteger(cluster.memberDocumentHashes.size());
for (const auto& hash : cluster.memberDocumentHashes) {
fingerprint.appendString(hash);
}
fingerprint.appendInteger(cluster.overlapClusterIds.size());
for (const auto& clusterId : cluster.overlapClusterIds) {
fingerprint.appendString(clusterId);
}
fingerprint.appendInteger(cluster.centroidEmbedding.size());
for (const float value : cluster.centroidEmbedding) {
fingerprint.appendFloat(value);
}
fingerprint.appendInteger(cluster.routingRepresentatives.size());
for (const auto& representative : cluster.routingRepresentatives) {
fingerprint.appendString(representative.documentHash);
fingerprint.appendInteger(representative.embedding.size());
for (const float value : representative.embedding) {
fingerprint.appendFloat(value);
}
}
}
fingerprint.appendInteger(batch.memberships.size());
for (const auto& membership : batch.memberships) {
fingerprint.appendString(membership.documentHash);
fingerprint.appendString(membership.clusterId);
fingerprint.appendOptionalString(membership.parentClusterId);
fingerprint.appendInteger(membership.clusterLevel);
fingerprint.appendDouble(membership.persistenceScore);
fingerprint.appendDouble(membership.cohesionScore);
fingerprint.appendDouble(membership.bridgeScore);
fingerprint.appendInteger(static_cast<std::uint8_t>(membership.role));
fingerprint.appendInteger(membership.overlapClusterIds.size());
for (const auto& clusterId : membership.overlapClusterIds) {
fingerprint.appendString(clusterId);
}
}
return fingerprint.value();
}
struct LatencySummary {
double p50Us{0.0};
double p95Us{0.0};
double p99Us{0.0};
double meanUs{0.0};
std::size_t samples{0};
};
LatencySummary summarizeLatency(std::vector<double>& samples) {
LatencySummary summary;
summary.samples = samples.size();
if (samples.empty()) {
return summary;
}
std::ranges::sort(samples);
const auto percentile = [&](double quantile) {
const double position = quantile * static_cast<double>(samples.size() - 1);
const auto lower = static_cast<std::size_t>(position);
const auto upper = std::min(lower + 1, samples.size() - 1);
const double fraction = position - static_cast<double>(lower);
return samples[lower] + ((samples[upper] - samples[lower]) * fraction);
};
summary.p50Us = percentile(0.50);
summary.p95Us = percentile(0.95);
summary.p99Us = percentile(0.99);
summary.meanUs =
std::accumulate(samples.begin(), samples.end(), 0.0) / static_cast<double>(samples.size());
return summary;
}
struct BuildGeometry {
std::size_t clusters{0};
std::size_t memberships{0};
std::size_t maxClusterSize{0};
std::size_t singletonClusters{0};
std::size_t protectedPairs{0};
std::size_t preservedProtectedPairs{0};
double giantClusterRatio{0.0};
};
BuildGeometry summarizeGeometry(const TopologyArtifactBatch& batch) {
BuildGeometry geometry;
geometry.clusters = batch.clusters.size();
geometry.memberships = batch.memberships.size();
std::size_t totalMembers = 0;
for (const auto& cluster : batch.clusters) {
geometry.maxClusterSize = std::max(geometry.maxClusterSize, cluster.memberCount);
geometry.singletonClusters += cluster.memberCount == 1 ? 1 : 0;
geometry.protectedPairs += cluster.protectedPairCount;
geometry.preservedProtectedPairs += cluster.preservedProtectedPairCount;
totalMembers += cluster.memberCount;
}
if (totalMembers > 0) {
geometry.giantClusterRatio =
static_cast<double>(geometry.maxClusterSize) / static_cast<double>(totalMembers);
}
return geometry;
}
struct ConfigPoint {
std::string_view name;
double minEdgeScore;
std::size_t maxComponentDocuments;
bool safeDefaults;
};
std::vector<GraphShape> selectShapes(std::string_view filter) {
if (filter.empty() || filter == "all") {
return {GraphShape::FragmentedSingletons, GraphShape::ClusteredLowScoreNoise,
GraphShape::GiantComponent, GraphShape::ManyOversizedComponents};
}
if (filter == shapeName(GraphShape::FragmentedSingletons)) {
return {GraphShape::FragmentedSingletons};
}
if (filter == shapeName(GraphShape::ClusteredLowScoreNoise)) {
return {GraphShape::ClusteredLowScoreNoise};
}
if (filter == shapeName(GraphShape::GiantComponent)) {
return {GraphShape::GiantComponent};
}
if (filter == shapeName(GraphShape::ManyOversizedComponents)) {
return {GraphShape::ManyOversizedComponents};
}
throw std::runtime_error{"unknown topology build shape filter: " + std::string{filter}};
}
std::vector<ConfigPoint> selectConfigurations(std::string_view filter) {
const ConfigPoint safe{
.name = "safe_defaults",
.minEdgeScore = kSafeMinimumEdgeScore,
.maxComponentDocuments = kSafeMaxComponentDocuments,
.safeDefaults = true,
};
const ConfigPoint control{
.name = "unbounded_control",
.minEdgeScore = 0.0,
.maxComponentDocuments = 0,
.safeDefaults = false,
};
if (filter.empty() || filter == "all") {
return {safe, control};
}
if (filter == safe.name) {
return {safe};
}
if (filter == control.name) {
return {control};
}
throw std::runtime_error{"unknown topology build configuration filter: " + std::string{filter}};
}
json runPoint(std::span<const TopologyDocumentInput> documents, GraphShape shape,
const ConfigPoint& point, const WorkloadConfig& workload, std::size_t repetitions,
std::string_view runId) {
TopologyBuildConfig config;
config.mode = TopologyBuildMode::Approximate;
config.inputKind = TopologyInputKind::Hybrid;
config.minEdgeScore = point.minEdgeScore;
config.maxComponentDocs = point.maxComponentDocuments;
config.maxNeighborsPerDocument = workload.neighborsPerDocument;
config.reciprocalOnly = true;
config.allowOverlap = false;
config.routingRepresentativeCount = 1;
ConnectedComponentTopologyEngine engine;
auto warmup = engine.buildArtifacts(documents, config);
if (!warmup) {
throw std::runtime_error{"topology build warmup failed"};
}
std::vector<double> latencyUs;
latencyUs.reserve(repetitions);
std::optional<std::uint64_t> expectedFingerprint;
BuildGeometry geometry;
for (std::size_t iteration = 0; iteration < repetitions; ++iteration) {
const auto start = std::chrono::steady_clock::now();
auto result = engine.buildArtifacts(documents, config);
const auto end = std::chrono::steady_clock::now();
if (!result) {
throw std::runtime_error{"topology build failed"};
}
latencyUs.push_back(std::chrono::duration<double, std::micro>(end - start).count());
const auto fingerprint = artifactFingerprint(result.value());
if (!expectedFingerprint.has_value()) {
expectedFingerprint = fingerprint;
geometry = summarizeGeometry(result.value());
} else if (fingerprint != *expectedFingerprint) {
throw std::runtime_error{"topology artifact content changed between repetitions"};
}
}
if (geometry.memberships != documents.size()) {
throw std::runtime_error{"topology build did not emit one primary membership per document"};
}
if (point.safeDefaults && geometry.maxClusterSize > kSafeMaxComponentDocuments) {
throw std::runtime_error{"safe construction cap was not preserved"};
}
if (!point.safeDefaults &&
(shape == GraphShape::ClusteredLowScoreNoise || shape == GraphShape::GiantComponent) &&
documents.size() > kComponentSize && geometry.clusters != 1) {
throw std::runtime_error{"control graph no longer forms the expected connected component"};
}
const auto directedNeighborEntries = std::accumulate(
documents.begin(), documents.end(), std::size_t{0},
[](std::size_t total, const auto& document) { return total + document.neighbors.size(); });
const auto summary = summarizeLatency(latencyUs);
const double documentsPerSecond =
summary.meanUs > 0.0 ? static_cast<double>(documents.size()) * 1'000'000.0 / summary.meanUs
: 0.0;
return json{
{"schema", "yams_topology_build_v1"},
{"run_id", runId},
{"configuration", point.name},
{"safe_defaults", point.safeDefaults},
{"shape", shapeName(shape)},
{"documents", documents.size()},
{"directed_neighbor_entries", directedNeighborEntries},
{"repetitions", repetitions},
{"construction",
{{"mode", "approximate"},
{"input_kind", "hybrid"},
{"min_edge_score", point.minEdgeScore},
{"max_component_docs", point.maxComponentDocuments},
{"max_neighbors_per_document", workload.neighborsPerDocument},
{"embedding_dimensions", workload.embeddingDimensions},
{"reciprocal_only", true},
{"allow_overlap", false},
{"routing_representative_count", 1}}},
{"build_latency_us",
{{"p50", summary.p50Us},
{"p95", summary.p95Us},
{"p99", summary.p99Us},
{"mean", summary.meanUs},
{"samples", summary.samples}}},
{"documents_per_second", documentsPerSecond},
{"geometry",
{{"clusters", geometry.clusters},
{"memberships", geometry.memberships},
{"max_cluster_size", geometry.maxClusterSize},
{"singleton_clusters", geometry.singletonClusters},
{"giant_cluster_ratio", geometry.giantClusterRatio},
{"protected_pairs", geometry.protectedPairs},
{"preserved_protected_pairs", geometry.preservedProtectedPairs}}},
{"artifact_fingerprint", expectedFingerprint.value_or(0)},
};
}
std::vector<std::size_t> documentScales(std::size_t maximum) {
std::vector<std::size_t> scales{256, 1024};
std::erase_if(scales, [maximum](std::size_t value) { return value > maximum; });
if (scales.empty() || scales.back() != maximum) {
scales.push_back(maximum);
}
return scales;
}
void emitRecord(const std::optional<fs::path>& outputPath, const json& record) {
if (!outputPath.has_value()) {
std::cout << record.dump() << '\n';
return;
}
if (!outputPath->parent_path().empty()) {
fs::create_directories(outputPath->parent_path());
}
std::ofstream output(*outputPath, std::ios::app);
if (!output) {
throw std::runtime_error{"failed to open benchmark output: " + outputPath->string()};
}
output << record.dump() << '\n';
}
} // namespace
int main() {
try {
const auto maximumDocuments =
std::max<std::size_t>(1, envSize("YAMS_BENCH_TOPO_BUILD_DOCS", 4096));
const auto repetitions =
std::max<std::size_t>(1, envSize("YAMS_BENCH_TOPO_BUILD_REPEATS", 30));
const auto runId = envString("YAMS_BENCH_RUN_ID", "local");
const WorkloadConfig workload{
.embeddingDimensions = std::max<std::size_t>(
1, envSize("YAMS_BENCH_TOPO_BUILD_DIMENSIONS", kDefaultEmbeddingDimensions)),
.neighborsPerDocument =
envSize("YAMS_BENCH_TOPO_BUILD_NEIGHBORS", kDefaultNeighborsPerDocument),
};
const auto outputValue = envString("YAMS_BENCH_OUTPUT");
const std::optional<fs::path> outputPath =
outputValue.empty() ? std::nullopt : std::optional<fs::path>{outputValue};
const auto configurations = selectConfigurations(envString("YAMS_BENCH_TOPO_BUILD_CONFIG"));
const auto shapes = selectShapes(envString("YAMS_BENCH_TOPO_BUILD_SHAPE"));
auto scales = documentScales(maximumDocuments);
if (envString("YAMS_BENCH_TOPO_BUILD_ONLY_MAX") == "1") {
scales = {maximumDocuments};
}
for (const auto documentCount : scales) {
for (const auto shape : shapes) {
const auto documents = synthesizeDocuments(documentCount, shape, workload);
for (const auto& configuration : configurations) {
const auto record =
runPoint(documents, shape, configuration, workload, repetitions, runId);
emitRecord(outputPath, record);
std::cerr << configuration.name << ' ' << shapeName(shape)
<< " docs=" << documentCount
<< " p50_us=" << record["build_latency_us"]["p50"] << '\n';
}
}
}
return 0;
} catch (const std::exception& error) {
std::cerr << "topology_build_bench: " << error.what() << '\n';
return 1;
}
}