Skip to content

feat: add AutoTune V1 orchestration - #2569

Open
jac0626 wants to merge 10 commits into
antgroup:mainfrom
jac0626:codex/autotune-v1-minimal
Open

feat: add AutoTune V1 orchestration#2569
jac0626 wants to merge 10 commits into
antgroup:mainfrom
jac0626:codex/autotune-v1-minimal

Conversation

@jac0626

@jac0626 jac0626 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Change Type

  • Bug fix
  • New feature
  • Improvement/Refactor
  • Documentation
  • CI/Build/Infra

Linked Issue

What Changed

  • Add an experimental AutoTune V1 CLI and build-tree-only typed C++ API.
  • Generate HGraph and IVF build/search candidates, reuse equivalent builds, evaluate real VSAG indexes, apply constraints, and select recommendation or best-effort evidence.
  • Support search-only tuning for existing HGraph, IVF, and Pyramid indexes.
  • Extract reusable in-memory build/search evaluation entry points from eval_performance while preserving its existing file-based flow.
  • Add structured reports, artifact lifecycle handling, examples, focused tests, and synchronized English/Chinese documentation.

V1 intentionally supports one dense float32 KNN workload. SINDI, Multi, sparse vectors, cross-request caching, model-based proposals, and a public Factory::CreateIndexWithConstraints API remain out of scope.

Test Evidence

  • make fmt
  • make lint
  • make test
  • make cov, run tests, and collect coverage
  • Other (describe below)

Test details:

clang-format-15 --dry-run --Werror <24 changed C++ files>
  passed

git diff --check
  passed

./build/tools/autotune/autotune_test --rng-seed 12345
  All tests passed (242 assertions in 23 test cases)

./build/tools/eval/eval_dataset_test --rng-seed 12345
  All tests passed (119 assertions in 14 test cases)

cmake --build build-release --target autotune \
  326_feature_create_index_with_constraints \
  327_feature_autotune_existing_index \
  328_feature_autotune_existing_pyramid -j$(nproc)
  passed

All three Release examples completed successfully.

Full `make test` and ASan were not run for this review cycle.

Compatibility Impact

  • API/ABI compatibility: No installed SDK API or ABI change. The typed AutoTune API is experimental and build-tree-only.
  • Behavior changes: Adds an opt-in tool and reusable eval entry points; existing eval_performance commands remain supported.

Performance and Concurrency Impact

  • Performance impact: Production query paths are unchanged. AutoTune itself performs real build/search trials and can be expensive by design.
  • Concurrency/thread-safety impact: AutoTune requests are serialized around non-thread-safe HDF5 loading and shared evaluation state.

Documentation Impact

  • No docs update needed
  • Updated docs:
    • README.md
    • DEVELOPMENT.md
    • CONTRIBUTING.md
    • Other: docs/docs/{en,zh}/src/resources/autotune*.md and tools/autotune/README*.md

Risk and Rollback

  • Risk level: medium
  • Rollback plan: Revert this PR; the feature is isolated behind the tools build and does not alter installed SDK APIs.

Checklist

  • I have linked the relevant issue (required for kind/bug and kind/feature; see "Linked Issue" above)
  • I have added/updated tests for new behavior or bug fixes
  • I have considered API compatibility impact
  • I have updated docs if behavior/workflow changed
  • My commit messages follow project conventions (Conventional Commits, optional [skip ci] prefix)

Copilot AI lite review requested due to automatic review settings July 29, 2026 09:38
@vsag-bot

vsag-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

/label status/waiting-for-review
/waiting-on reviewer
/request-review @jiaweizone
/request-review @wxyucs
/request-review @inabao

@vsag-bot

vsag-bot commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Automated pull request review completed.

Review effort: high (6937 changed lines across 43 files).

No actionable inline findings were found.
Review: #2569 (review)

@jac0626 jac0626 added kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 version/1.1 module/tools module/testing module/docs module/example labels Jul 29, 2026
@mergify

mergify Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Merge Protections

🟢 All 3 merge protections satisfied — ready to merge.

Show 3 satisfied protections

🟢 Require kind label

  • label~=^kind/

🟢 Require version label

  • label~=^version/

🟢 Require linked issue for feature/bug PRs

  • body~=(?im)(?:^|[\s\-\*])(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?\s+(?:#\d+|[\w.\-]+/[\w.\-]+#\d+|https?://github\.com/[\w.\-]+/[\w.\-]+/issues/\d+)

@jac0626
jac0626 marked this pull request as ready for review July 29, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (6342 changed lines across 41 files).
Submitted 1 inline comment.

Reviewed commit 998445f.

Comment thread tools/eval/case/build_eval_case.cpp
Comment thread tools/autotune/autotune_evaluation.cpp Outdated
Comment thread tools/eval/case/search_eval_case.cpp
Comment thread tools/autotune/autotune.cpp
Comment thread tools/autotune/autotune_candidate.cpp
Comment thread tools/autotune/autotune.cpp
Comment thread tools/eval/eval_dataset.cpp
Comment thread tools/autotune/autotune.cpp
Comment thread tools/autotune/autotune_evaluation.cpp
Comment thread tools/eval/eval_dataset.cpp
Signed-off-by: jc543239 <jc543239@antgroup.com>
Assisted-by: Codex:gpt-5
Copilot AI review requested due to automatic review settings August 4, 2026 07:28
@jac0626
jac0626 force-pushed the codex/autotune-v1-minimal branch from 817a97d to 8f16932 Compare August 4, 2026 07:28
@vsag-bot
vsag-bot self-requested a review August 4, 2026 07:28
Signed-off-by: jc543239 <jc543239@antgroup.com>
Assisted-by: Codex:gpt-5

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

Suppressed comments (4)

tools/eval/case/search_eval_case.cpp:1

  • SearchFailure::Record() sets failed_ to true before writing message_. Another thread can observe Failed()==true and reach ThrowIfFailed() before message_ is assigned, which can throw with an empty message. Make the state update atomic w.r.t. the message (e.g., take the mutex first and set both message_ and the failed flag under the same critical section, or store the message first and then publish failed_ with a release store).
    tools/eval/case/search_eval_case.cpp:1
  • These throws occur inside an OpenMP parallel for region in do_knn_filter_search() (and likely similar flows). Throwing C++ exceptions across OpenMP boundaries is undefined and commonly terminates the process. Use the same pattern as do_knn_search() (record the first failure in a thread-safe shared object, have other threads early-exit, then throw once after the parallel region), rather than throwing from worker threads.
    tools/eval/case/search_eval_case.cpp:1
  • These throws occur inside an OpenMP parallel for region in do_knn_filter_search() (and likely similar flows). Throwing C++ exceptions across OpenMP boundaries is undefined and commonly terminates the process. Use the same pattern as do_knn_search() (record the first failure in a thread-safe shared object, have other threads early-exit, then throw once after the parallel region), rather than throwing from worker threads.
    tools/eval/monitor/recall_monitor.cpp:1
  • get_id_recall() allocates and populates an unordered_set per query without reserving, which can rehash for larger top_k and adds significant overhead over many queries/trials. Consider reserving top_k capacity (or using a lower-overhead approach like sorting + two-pointer intersection when top_k is moderate) to reduce allocations and rehash cost in the hot path.

Copilot AI review requested due to automatic review settings August 4, 2026 07:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 43 out of 43 changed files in this pull request and generated no new comments.

Suppressed comments (4)

tools/eval/eval_dataset.cpp:1

  • neighbors is an int64 dataset, but the code constructs an H5::FloatType with NATIVE_INT64. This is the wrong HDF5 type class and can lead to incorrect reads or runtime errors depending on the HDF5 implementation. Use H5::IntType (or pass H5::PredType::NATIVE_INT64 directly) for reading the neighbors dataset.
    tools/eval/case/search_eval_case.cpp:1
  • The final “statistics-only” pass now runs whenever statistics_collected is false, even if there are no monitors enabled (previously it was gated by !this->monitors_.empty()). This can add an extra full min_query search pass that produces no externally requested metrics, which is expensive for large workloads. Consider restoring a guard (e.g., only run this pass if statistics are actually needed for the output schema) or documenting why statistics must always be collected.
    tools/eval/eval_dataset.cpp:1
  • The cosine/“angular” distance lambda can divide by zero when either vector has zero norm, producing inf/nan distances and making recall evaluation unreliable. Add an explicit check for a zero (or near-zero) denominator and return a well-defined distance (e.g., 1.0f for maximally dissimilar) in that case.
    tools/autotune/examples/sift_hgraph_autotune_request.json:9
  • The example uses sq8, while the AutoTune docs and built-in proposal logic reference sq8_uniform. If sq8 is not a supported value in the underlying index config, this example will fail; even if it is supported as an alias, it’s inconsistent with the documented contract. Align the example (and/or docs) to use the canonical value to avoid confusion.
          "base_quantization_type": ["fp32","sq8"],

@vsag-bot vsag-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated inline review completed.

Review effort: high (6937 changed lines across 43 files).
No actionable inline findings were found.

Reviewed commit 317ca2f.

} // namespace

void
EvaluateEfSearchRange(const HGraphEfSearchRange& range,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] EvaluateEfSearchRange relies entirely on side effects — the winning ef_search value is not returned

The function evaluates ef_search candidates via the evaluate callback and narrows the range through exponential probing + binary search, but it returns void. The caller (EvaluateCandidates) has no programmatic way to determine which ef_search value was the minimal passing one — it must scan all recorded trials to find it. This works because SelectResult does exactly that scan, but the contract between EvaluateEfSearchRange and its caller is implicit.

Consider returning the optimal ef_search value (or std::nullopt if none found) to make the contract explicit and allow the caller to tag the winning trial directly.

}
double previous = 0.0;
bool emitted = false;
for (uint64_t i = 0;; ++i) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] expand_range floating-point loop uses uint64_t counter that can overflow before reaching stop

The loop for (uint64_t i = 0;; ++i) computes current = start + static_cast<double>(i) * step. For very small steps (e.g. step=1e-9) over a moderate range, i can overflow uint64_t before current reaches stop. The max_trials check in generate_candidates provides indirect protection, but the overflow would happen inside expand_range before generate_candidates ever sees the emitted values.

Consider adding an explicit iteration cap inside expand_range (e.g. if (i > some_reasonable_limit) throw ...) or using the max_trials value as a direct bound passed into the expand functions.

JsonType
SelectResult(const RequestContext& request, const Evaluation& evaluation) {
JsonType trials = JsonType::array();
int64_t best = -1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] SelectResult uses int64_t for vector indices — type mismatch with size_t

best and best_effort are declared as int64_t (lines 736-737) but are used to index into trials (a JsonType array backed by std::vector). While max_trials is capped at 100000, the signed/unsigned mismatch could produce compiler warnings and is semantically incorrect — a negative index would be a bug but is silently representable.

Consider using size_t or JsonType::size_type for these index variables, with std::numeric_limits<size_t>::max() as the sentinel instead of -1.

std::string run_path;
std::string report_path;
bool keep_intermediate = false;
const auto failure = [&](const std::string& message) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] run_tuning_locked failure lambda captures keep_intermediate by reference before it is assigned

The failure lambda at line 863 captures keep_intermediate by reference, but keep_intermediate is not assigned until line 889 (from context.keep_intermediate). If the parser at line 887 throws, keep_intermediate is still false (its default), which is the correct cleanup behavior. However, the temporal coupling between the lambda definition and the assignment is fragile — reordering the code could silently break cleanup.

Consider moving the keep_intermediate assignment before the lambda definition, or capturing it by value after assignment.

throw std::invalid_argument("ground_truth contains too many ids");
}
const auto ground_truth_count = ground_truth->GetNumElements() * ground_truth->GetDim();
for (int64_t i = 0; i < ground_truth_count; ++i) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] FromDatasets validates every ground truth ID against the base dataset (O(N*K) lookup)

The validation loop at lines 311-315 calls GetOneTrainById() for every ground truth ID, which does an unordered_map::find(). For large datasets this is O(N*K) expected time. This runs once per TuneIndex/TuneSearch call. The author has acknowledged this and deferred cross-request caching until profiling justifies it — which is reasonable for V1.

No action needed now, but worth keeping in mind if tuning performance on million-scale datasets becomes a bottleneck.

}

auto high = low;
while (low < range.stop) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[note] EvaluateEfSearchRange binary search correctly avoids evaluating stop unnecessarily

The exponential probing doubles high from low until recall >= target or high == stop. When high reaches stop and still fails, the function returns early (line 169-171). When high passes before reaching stop, the binary search narrows between low (last failing) and high (first passing). The stop value is only evaluated if all earlier probes fail — which is the correct behavior for proving the range infeasible. The implementation matches the documented design.

}
metrics["search_seconds"] = seconds;
return metrics;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] search_seconds metric uses wall-clock time

}
metrics["search_seconds"] = seconds;
return metrics;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] search_seconds metric uses wall-clock time which conflates parallelism with per-query cost

In autotune_evaluation.cpp, search_metrics(raw, elapsed(measured_start)) records search_seconds as the wall-clock duration of the entire search evaluation batch. When concurrency > 1, this wall-clock time shrinks with more threads even though the total CPU work is the same. This makes search_seconds and the derived build_and_search_seconds incomparable across different concurrency settings.

Consider dividing by concurrency to get an amortized per-query cost, or use the latency_avg_ms metric (which already measures per-query time) as the primary search-cost signal.

set_metric(metrics, "build_seconds", number(raw, "duration(s)"));
const auto memory = number(raw, "index_memory(B)");
if (memory.has_value() && *memory > 0.0) {
metrics["index_memory_mb"] = *memory / BYTES_PER_MEBIBYTE;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] build_metrics silently ignores missing duration(s) — consider logging a warning

build_metrics() at line 60 calls number(raw, "duration(s)") which returns std::nullopt when the field is absent or non-numeric. This silently produces a MetricMap without build_seconds. While the current BuildEvalCase always emits duration(s) via the DurationMonitor, a future change to the monitor pipeline could silently break the metric without any diagnostic.

Consider logging a warning when duration(s) is missing from the raw build result, so that regressions in the eval pipeline are immediately visible in AutoTune output.

continue;
}

double score = 0.0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] SelectResult best-effort ranking uses unscaled violation score that favors large-magnitude metrics

In SelectResult() at line 770-778, the violation score is computed as abs(actual - expected) / max(expected, 1e-12). When multiple constraints are violated, the total score is the sum of per-metric ratios. This means a metric with large absolute values (e.g. qps at 10000 vs expected 5000 → ratio 1.0) dominates over a metric with small values (e.g. recall_at_k at 0.8 vs expected 0.9 → ratio 0.11). The best-effort selection may prefer a candidate that barely misses a high-magnitude constraint over one that severely violates a [0,1]-range constraint.

Consider normalizing each violation ratio to [0,1] (e.g. via ratio / (1 + ratio)) or weighting constraints equally so that the ranking is not magnitude-dependent.

const auto should_retain = keep_all || (selected.has_value() && path == *selected);
if (exists && !should_retain && removed.emplace(path).second) {
std::error_code remove_error;
if (std::filesystem::remove(path, remove_error)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] finalize_artifacts removes parent directories unconditionally, which may collide with concurrent runs

In finalize_artifacts()update_artifact() at line 978-982, when an artifact file is removed, the code also attempts to remove its parent directory and grandparent directory via std::filesystem::remove(). If two concurrent TuneIndex calls (serialized by run_mutex()) share overlapping workspace paths, the second call may find its artifact directories already removed by the first call cleanup. While the global mutex prevents true concurrency, if the mutex is ever relaxed (as suggested in another review), this cleanup logic would be unsafe.

Consider using a unique per-run subdirectory (already done via new_run_name()) and only removing the run-specific directory, not walking up the tree.

Comment thread tools/eval/evaluator.cpp
EvaluateSearch(const IndexPtr& index, const EvalDatasetPtr& dataset, const EvalConfig& config) {
validate(index, dataset);
validate_search(dataset, config);
ScopedOpenMpThreads openmp_threads;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] ScopedOpenMpThreads in evaluator.cpp snapshots but does not set thread count — differs from test-fixture version

The ScopedOpenMpThreads in evaluator.cpp:29-36 only saves omp_get_max_threads() in its constructor without calling omp_set_num_threads(). It relies on SearchEvalCase::do_knn_search() to set the thread count later. This is different from the ScopedOpenMpThreads in autotune_test.cpp:86-93 which accepts a thread count and sets it immediately.

If EvaluateSearch is ever extended to support non-knn search modes (range search, filter search), those code paths may not call omp_set_num_threads(), leaving the thread count at whatever the caller left it at. Consider either: (a) having the evaluator version also accept and set the thread count, or (b) documenting that the evaluator version is intentionally passive and the search case is responsible for thread configuration.

if (not statistics_collected and not this->monitors_.empty()) {
if (not statistics_collected) {
omp_set_num_threads(config_.num_threads_searching);
SearchFailure search_failure;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] do_knn_search fallback statistics pass re-executes all queries without SearchFailure protection

In do_knn_search() at line 298-314, when statistics_collected is false after the monitor loop (i.e. no non-latency monitor was configured), a fallback pass executes queries solely to collect statistics. This fallback pass uses SearchFailure for error propagation, which is good. However, it does not check search_failure.Failed() inside the parallel loop body before calling prepare_query() and KnnSearch(), unlike the main monitor loop at line 248. If a prior query in the same batch failed, subsequent iterations still execute — they will also likely fail, wasting work.

Consider adding the if (search_failure.Failed()) { continue; } guard to the fallback pass for consistency with the main loop.

}
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] expand_range floating-point loop may produce excessive iterations for tiny step values

The floating-point range expansion at line 117-136 uses for (uint64_t i = 0;; ++i) with a current == previous duplicate check as the only non-convergence guard. For very small step values (e.g. step=1e-15 over [0, 1]), this could produce up to 1e15 iterations before the duplicate check triggers. While max_trials in the caller provides a safety net, the check happens after candidate generation completes.

Consider adding an explicit iteration cap inside expand_range itself (e.g. if (i > 10000000) throw ...) to fail fast rather than relying on the downstream max_trials check which only triggers after all ranges have been fully expanded.

}
metrics["search_seconds"] = seconds;
return metrics;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] search_seconds records wall-clock time, which conflates parallelism with per-query cost

At line 85, search_seconds = elapsed(measured_start) captures the total wall-clock duration of the entire search evaluation loop. When OpenMP parallelism is active, this measures real time (e.g. 0.5s for 8-thread execution) rather than aggregate CPU time. The metric is then used as an objective for candidate comparison, meaning a candidate evaluated with more threads will appear faster even if its per-query latency is identical.

If the intent is to compare per-query algorithmic cost independent of parallelism, consider dividing by the number of queries or using omp_get_wtime() accumulation per query. If wall-clock throughput is the intended metric, document this clearly in the metric name (e.g. wall_clock_seconds) and the API doc.

set_metric(metrics, "build_seconds", number(raw, "duration(s)"));
const auto memory = number(raw, "index_memory(B)");
if (memory.has_value() && *memory > 0.0) {
metrics["index_memory_mb"] = *memory / BYTES_PER_MEBIBYTE;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] build_metrics silently ignores missing duration(s), producing a zero-filled record

At line 60, build_metrics reads duration(s) from the eval result JSON. If the key is absent (e.g. due to a future eval config change or a monitor that skips duration), json.value(key, 0.0) silently defaults to 0.0. The caller then treats this as a valid build time of zero seconds, which could rank a broken or incomplete build above properly measured candidates.

Consider either: (a) making duration(s) required and throwing if absent, or (b) using NaN / std::optional as the default so downstream scoring can detect and penalize missing data.

continue;
}

double score = 0.0;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] SelectResult best-effort violation scoring favors large-magnitude metrics

At line 770, when no candidate satisfies all constraints, the fallback scoring computes sum += (metric_value - threshold) / threshold. This normalizes each violation by its threshold, which is correct. However, the score is an unweighted sum across all metrics. If one metric (e.g. search_seconds) has a threshold of 0.01 and another (e.g. memory_usage) has a threshold of 1e9, a 10% violation of memory adds 0.1 to the score while a 10x violation of search_seconds adds only 9.0 — making the sum dominated by the metric with the smallest relative violation range.

Consider using a weighted sum or normalizing to a common scale, or documenting that constraints with small absolute thresholds inherently carry more weight in the best-effort selection.

const auto should_retain = keep_all || (selected.has_value() && path == *selected);
if (exists && !should_retain && removed.emplace(path).second) {
std::error_code remove_error;
if (std::filesystem::remove(path, remove_error)) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] finalize_artifacts removes parent directories unconditionally, risking unintended deletion

At line 978, std::filesystem::remove_all(workspace) deletes the entire workspace directory tree. If the workspace path is misconfigured (e.g. set to "." or a shared directory), this could delete files outside the intended tuning artifacts. The current validation in make_context checks that the workspace is absolute and non-empty, but does not verify it is a dedicated tuning directory.

Consider adding a sentinel file check (e.g. require a .autotune_workspace marker) before deletion, or restricting cleanup to only the known subdirectories created during tuning.

Comment thread tools/eval/evaluator.cpp
EvaluateSearch(const IndexPtr& index, const EvalDatasetPtr& dataset, const EvalConfig& config) {
validate(index, dataset);
validate_search(dataset, config);
ScopedOpenMpThreads openmp_threads;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] ScopedOpenMpThreads in evaluator.cpp diverges from the version in autotune_test.cpp

evaluator.cpp:29-36 defines ScopedOpenMpThreads with a default constructor that snapshots omp_get_max_threads() but does NOT call omp_set_num_threads(). In contrast, autotune_test.cpp:86-93 defines a version whose constructor accepts a thread count and calls omp_set_num_threads(value).

The evaluator version is used in EvaluateSearch() at line 97, which means the search evaluation inherits whatever thread count was set by the caller rather than explicitly controlling it. The test version is used to verify that omp_set_num_threads(3) is properly restored.

If both classes are meant to represent the same RAII guard, consider consolidating them into a single definition (e.g. in a shared header) to avoid the two implementations drifting further apart.

@LHT129 LHT129 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] parse_metric and metric_name maintain parallel but independent string-to-enum mappings

In tools/autotune/autotune.cpp, parse_metric (line 123) maps strings to Metric enum values, while metric_name (line 96) maps Metric enum values back to strings. The two functions are maintained independently — if a new metric is added to one but not the other, they silently diverge, potentially causing metric_name to throw "AutoTune metric must be specified" at runtime for a valid metric that parse_metric accepted.

Consider using a static table (e.g., an array of std::pair<std::string_view, Metric>) that both functions iterate over, or a code-generated switch, so that adding a metric requires only one change point.

if (not statistics_collected and not this->monitors_.empty()) {
if (not statistics_collected) {
omp_set_num_threads(config_.num_threads_searching);
SearchFailure search_failure;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] do_knn_search fallback statistics pass missing SearchFailure guard

At line 300, the fallback statistics loop iterates over all queries a second time to collect per-query metrics. This pass runs after the main search loop but does not check SearchFailure::Failed() before accessing result data. If a query in the main loop recorded a failure via SearchFailure::Record(), the corresponding result entry may be incomplete or invalid, and the statistics pass could read uninitialized or partial data.

Consider either: (a) checking SearchFailure::Failed() before the statistics pass and skipping it entirely on failure, or (b) guarding each per-query statistic access with a per-query validity check.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kind/feature Brand-new functionality or capabilities 引入全新的功能、新特性或新能力 module/docs module/example module/testing module/tools size/XXL version/1.1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat](other): add constraint-driven AutoTune V1

4 participants