Skip to content

fix(bench): fair Redis-vs-Vertex measurement + single-shard/high-QPF readiness (supersedes #145/#146/#147) - #148

Merged
fcostaoliveira merged 3 commits into
masterfrom
fix/vertex-benchmark-fairness
Jul 19, 2026
Merged

fcostaoliveira merged 3 commits into
masterfrom
fix/vertex-benchmark-fairness

Conversation

@fcostaoliveira

Copy link
Copy Markdown
Collaborator

Consolidates the stacked Vertex PRs #145 (approx-neighbor tuning), #146 (open-loop fixed-rate mode), #147 (Vertex gRPC transports + duration-bounded closed loop) and applies the fixes from a 24-agent adversarial review of that stack. Merging this lands all three PRs' functionality with the fairness defects corrected — it supersedes them.

Why

The open-loop / duration search harness had measurement asymmetries that all ran one direction — understating Redis — and there was no clean operating point to drive a fast single-shard / high-QPF Redis to its true throughput. This mattered because the Redis-vs-Vertex GloVe report is produced by this code, and a single-shard/highest-QPF Redis rerun is planned.

Changes

Fairness (blocker/major)

  • Redis prime + two-barrier clock start (open-loop and closed-loop-duration), mirroring the Vertex engine: every worker connects and issues a discarded warm-up FT.SEARCH, then the measurement clock is stamped and released only once all workers are warm. Connection setup and the cold first query now fall outside total_time and the latency percentiles. The barrier is deadlock-safe — a worker that fails to connect still waits both barriers.
  • Hoist Redis request construction out of the timed window: configured_index_name() resolved once (was a per-query env::var inside the hot loop), and FT.SEARCH assembly split into build_ft_search_cmd() (pre-timer) + exec_ft_search(). The timed window now wraps only the RPC round-trip + reply parse, matching Vertex's boundary.
  • client_saturated decided only by measured signals (client/system CPU); the pure parallel > cores "oversubscribed" term is demoted to an advisory field so a legitimately high-concurrency run against a fast shard is not auto-flagged saturated.

Correctness / robustness

  • num_candidates clamps to >= top (was max(1)) — Vertex requires approximateNeighborCount >= neighborCount.
  • Redis recycled-index length guard (clean Err instead of a worker panic that aborts the run).
  • Redis search connections get 120 s read/write socket timeouts (avoid multi-minute kernel TCP-retransmit blocking).
  • Overload-shed runs report rps=0 with drop/schedule-delay accounting instead of Err("No searches completed") — preserves the strongest overload signal.
  • Dropped requests no longer double-count as late or pollute the schedule_delay percentiles.
  • --warmup-seconds allowed in closed-loop-duration mode (discarded warm-up for both engines); open-loop rep selection no longer keys on rps (which is pinned to the offered rate).
  • Reject non-finite / absurd --search-duration / --max-lateness-ms / --warmup-seconds instead of panicking in Duration::from_secs_f64.
  • Documented the CPU-sample-window caveat (brackets the whole search()).

Deliberately NOT changed (adversarially rejected)

Vertex per-iteration body build, grpc.ready() per call, and query-recycling recall — verified to either favor Redis or be non-issues.

Rerun runbook (run-config, not code)

For the single-shard/highest-QPF Redis rerun to be fair, the operator must still: sweep client concurrency and report the QPS/latency knee (never a single over-saturated point); use --repetitions 1 with a real --warmup-seconds; match recall (tune Redis ef and Vertex num_candidates/leaf-fraction to the same measured recall, print both); pin Vertex num_candidates=300 (else it silently falls back to the index default of 150 — cheaper/lower-recall, flattering Vertex); assert failed_queries == 0 in every published cell; and size the loadgen so required concurrency and parallel stay ≤ cores.

Testing

cargo build clean · cargo clippy 0 warnings · cargo fmt applied · 477 unit tests pass. integration_dragonfly needs a live Docker service (green in CI).

🤖 Generated with Claude Code

kaizer113 and others added 3 commits July 17, 2026 17:07
…readiness

Applies the fixes from an adversarial review of the stacked Vertex PRs
(#145/#146/#147). Every measurement asymmetry in the open-loop / duration
search harness ran one direction — it understated Redis — and there was no
clean operating point to drive a fast single-shard/high-QPF Redis. Changes:

Fairness (blocker/major):
- Redis prime + two-barrier clock start (open-loop AND closed-loop-duration),
  mirroring Vertex: every worker connects + issues a discarded warm-up
  FT.SEARCH, then the measurement clock is stamped and released only once all
  workers are warm. Connection setup and the cold first query now fall OUTSIDE
  total_time and the latency percentiles. Barrier is deadlock-safe: a worker
  that fails to connect still waits both barriers.
- Hoist all Redis client-side request construction out of the timed window:
  configured_index_name() resolved once (was a per-query env::var), and
  FT.SEARCH assembly split into build_ft_search_cmd() (pre-timer) + exec, so
  the timed window wraps only the RPC round-trip + reply parse, matching the
  Vertex boundary.
- client_saturated is now decided ONLY by measured signals (client/system CPU);
  the pure parallel>cores "oversubscribed" term is demoted to an advisory field
  so a legitimately high-concurrency run against a fast shard is not auto-flagged.

Correctness / robustness:
- num_candidates clamps to >= top (was max(1)); Vertex requires
  approximateNeighborCount >= neighborCount.
- Redis recycled-index length guard (clean Err instead of a worker panic).
- Redis search connections get 120s read/write socket timeouts.
- Overload-shed runs report rps=0 with drop/schedule-delay accounting instead
  of Err("No searches completed").
- Dropped requests no longer double-count as late / pollute schedule_delay.
- --warmup-seconds allowed in closed-loop-duration mode (discarded warm-up
  phase for both engines); open-loop rep selection no longer keys on rps.
- Reject non-finite/absurd --search-duration/--max-lateness-ms/--warmup-seconds.
- CPU-sample window caveat documented (brackets whole search()).

Verified: cargo build clean, cargo clippy 0 warnings, cargo fmt applied,
477 unit tests pass. (integration_dragonfly needs a live Docker service.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Docker Build Validation

Platforms tested:

  • linux/amd64 (built and tested)
  • linux/arm64 (build validated)

Tests performed:

  • --help command
  • --describe datasets command
  • Integration test (random-100 dataset + Redis)
  • Multi-platform build validation (amd64 + arm64)

@fcostaoliveira
fcostaoliveira merged commit 8303f1d into master Jul 19, 2026
16 checks passed
@fcostaoliveira
fcostaoliveira deleted the fix/vertex-benchmark-fairness branch July 19, 2026 17:43
fcostaoliveira added a commit that referenced this pull request Jul 22, 2026
… 0 default (#200) (#201)

In the Vertex search paths an UNSET num_candidates was sent as `0` (and an unset
fraction_leaf as `0.0`) — Vertex's "use the deployed index default" sentinel. So
a sweep point that forgot to thread num_candidates ran at the index default
(~150) while still being LABELLED with the config's claimed value: fewer
candidates = less work = flattering QPS/latency at lower recall, breaking
recall-matching against the Redis `ef` sweep (fairness gate #4 of the #148
review).

Fix (resolve_approx_neighbor_count + resolve_search_knobs):
- Honor the config's num_candidates when set (clamped to `top`, Vertex's floor).
- When unset, fall back to the index's OWN configured approximateNeighborsCount
  (self.approx_neighbors) EXPLICITLY — the value the index was built with, sent
  as a real number, never the hidden 0 sentinel. approximate_neighbor_count is
  now always Some(...), so request()/build_find_neighbors_body never emit 0.
- Log the effective knobs per config ("Vertex effective search knobs:
  approximateNeighborCount=… (config|index-default), fraction_leaf_nodes_to_search=…")
  so a sweep point is honestly labeled instead of silently measured at default.
- Applies to REST + public/private gRPC (single resolution before the transport
  branch).
- Pure resolver unit-tested: config honored, unset→index-default, clamped to top,
  never 0.

README: document num_candidates/approximateNeighborCount resolution + logging.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants