Skip to content

perf(policies): drop per-request locks and string probes from routing scoring - #1712

Closed
slin1237 wants to merge 2 commits into
mainfrom
perf/routing-hot-path
Closed

slin1237 wants to merge 2 commits into
mainfrom
perf/routing-hot-path

Conversation

@slin1237

Copy link
Copy Markdown
Member

Summary

Final PR of the wave-1 dynamo-review batch (#1693/#1685): remove the two verified per-request serialization points in routing scoring. Stacked on #1706 (base branch is fix/kv-index-worker-cap; this diff shows only the routing changes).

K4 — LeastLoad: no more write-lock-across-scan

The old policy held a RwLock write guard across its entire O(workers) scan on every request — a global serialization point (a prime suspect for the incident's 1.2-core ceiling) — plus a url().to_string() allocation per request. What the lock actually protected: atomicity of read-scan-then-credit on inflight_tokens.

Now: one read-mostly map HashMap<String, WorkerLoadState> where the in-flight token credit is an AtomicU64 inside the state. Selection takes a read guard (concurrent selections run fully parallel), resolves each worker once (1 string probe instead of 3 — the old code probed two maps plus a throughput re-probe), credits via fetch_add — no exclusive lock, no allocation, no lost updates (proven by an 8-thread × 250-selection test asserting the exact credit total). Merging the two maps also closes a pre-existing torn-update window between their separate locks. Deliberate relaxation: two truly simultaneous selections may pick the same worker before either credit lands — bounded by concurrency, dynamo-style; sequential semantics unchanged.

K3 — cache_aware: de-stringed scoring + memoized hashing

  • The scoring loop did 2–3 URL-keyed DashMap probes per worker per request (filter + max_by_key + debug log). Now one relaxed atomic load per worker: each worker caches its interned kv_index id packed with the indexer's process-unique epoch in one AtomicU64 on the shared WorkerRuntime — same-URL replace inherits it, indexer replacement self-invalidates (regression-tested with a deliberate cross-instance raw-id collision). Tie-breaking preserved exactly, guarded by an oracle test that keeps the previous implementation as the reference.
  • compute_request_content_hashes (full-prompt XXH3) ran once per select call — twice per PD request. Now memoized on SelectWorkerInfo via OnceLock, block-size-keyed.
  • Honest correction to the research report: the suspected "second full-sequence hash" at the old :1012 is on the mutually-exclusive approximate-tree path, not a per-request duplicate — left untouched.

Herding fix (deliberate selection change)

Both dead/unmatched-tenant fallbacks routed to healthy_indices.first() — herding traffic onto one worker exactly when a tenant dies. Now: least-loaded healthy worker, consistent with the file's other fallbacks. Covered by tests for both tree types.

Tests

8 new: lost-update concurrency (exact credit accounting), concurrent update_loads safety, the score_overlap reference-oracle equivalence (cold/warm/ties/unknown workers), cross-instance id invalidation, hash memoization (per-policy and per-block-size), and both dead-tenant fallbacks.

Gates: workspace clippy -D warnings clean, 3565 tests passed / 0 failed (115 policy + 197 kv-index among them).

Refs #1693 #1685. After #1706 squash-merges, this branch gets a trivial rebase.

slin1237 added 2 commits June 12, 2026 15:55
… failures

PositionalIndexer::intern_worker asserted id < MAX_WORKERS(2048). At
fleets past 2048 workers — or any long-lived gateway with enough worker
churn, since interned ids are monotonic and never recycled — the assert
panicked inside the per-worker KV event subscription task, and the
panic was discarded at await time, so cache-aware routing silently
stopped seeing those workers' KV events.

- Replace the fixed 2048-slot tree_sizes Vec with segmented, doubling,
  lazily-allocated storage (OnceLock<Box<[AtomicUsize]>> per segment):
  reads stay a lock-free array index on the query hot path, worker
  count is unbounded (full u32 id space), memory is 8 bytes per
  interned id and at most 2x the high-water id.
- intern_worker returns Result instead of asserting; the only error is
  u32 id-space exhaustion, detected via a u64 counter instead of
  wrapping. Interning can no longer kill a subscription task.
- Surface subscription failures instead of swallowing them: panics are
  caught inside the task (catch_unwind) and logged with worker context
  when they happen, JoinErrors are logged on the stop paths, and all
  failure modes increment the new
  smg_kv_event_subscription_failures_total counter (reasons: panic,
  join_error, intern_failed).
- Tests: segment math across the full u32 space, interning past the
  old cap with matches probed on both sides of the 2048 boundary,
  id-lifecycle (no recycling) semantics, no-alloc reset for never
  written segments, exhaustion edge, and concurrent interning racing
  across a segment boundary.

Refs #1685.

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
… scoring

At incident scale (#1693/#1685: 4000 workers, 3k req/s) two routing hot
paths do O(workers) serialized or string-keyed work on every request.

LeastLoad held a std RwLock WRITE guard across the whole scoring scan,
serializing all routing for a model on one lock, plus a per-request
url().to_string() allocation. The guard only protected the in-flight
estimate's read-scan-credit atomicity, so the load report and the
since-poll counter merge into one read-mostly map entry: selection now
takes a shared read guard, resolves each worker's entry once (one
URL probe per worker instead of three), and credits the chosen worker
with a relaxed fetch_add, so concurrent credits are never lost. Credits
to workers without a load entry are dropped: they were never observable,
because scores only read the estimate when a report exists and the poll
that installs one resets it.

cache_aware's event-driven scoring probed the indexer's URL-keyed
DashMap 2-3 times per worker per request (filter + max_by_key + debug
log). PositionalIndexer now exposes a process-unique non-zero instance
id (epoch), and workers cache their interned u32 id per epoch in one
packed AtomicU64 in the shared WorkerRuntime (same-URL replace keeps
it). The scoring loop is integer-keyed: one relaxed atomic load per
worker, URL interning at most once per (worker, indexer instance) via
intern_worker, and the epoch makes stale ids self-invalidating when a
model's indexer is dropped and recreated.

The request's per-block content hashes are memoized on SelectWorkerInfo
keyed by block size: PD mode selects prefill and decode with the same
info, so the full-prompt XXH3 pass runs once per request instead of
once per select_worker call. Verified while in there: the second
full-sequence hash flagged at cache_aware.rs:1012 (hash_token_path) is
on the approximate-tree path, mutually exclusive with the event-driven
hash within a request and gated behind mesh hash-index population, so
it is not a duplicate and stays.

One deliberate routing change: the approximate-tree fallback when a
matched tenant is gone or unhealthy used to herd every such request
onto healthy_indices.first(); it now picks the least-loaded healthy
worker (cache_aware.rs:1020,1101).

Selection is otherwise unchanged: same worker for the same state,
including tie-breaks (fully equal score/load/tree-size keeps the later
worker), guarded by an equivalence test that keeps the previous
implementation as an oracle.

Tests: LeastLoad lost-update concurrency test and selection-vs-writers
safety test; score_overlap equivalence and epoch-invalidation tests;
request-hash memoization tests; min-load fallback tests for both tree
paths.

Stacked on #1706 (intern_worker returning Result<WorkerId,
WorkerIdExhausted>).

Refs #1693 #1685

Signed-off-by: Simo Lin <25425177+slin1237@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 8871d097-0840-41f4-8934-32b9fd81f248

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch perf/routing-hot-path

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added grpc gRPC client and router changes model-gateway Model gateway crate changes kv-index KV index crate changes labels Jun 13, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces significant performance optimizations and concurrency improvements to the routing policies in the model gateway. Key changes include: 1) Cache-Aware Policy Optimizations: Added process-unique epoch IDs (instance_id) to PositionalIndexer to safely cache interned worker IDs directly on worker objects, avoiding redundant URL-keyed map lookups during scoring. Additionally, full-prompt content hashing is now memoized within SelectWorkerInfo to prevent redundant hashing passes across prefill and decode selections. 2) Least-Load Policy Concurrency: Refactored LeastLoadPolicy to store in-flight token counts in atomics (AtomicU64) within a single read-mostly map, allowing concurrent worker selections to execute in parallel under a shared read lock and eliminating serialization bottlenecks. 3) Anti-Herding Fallbacks: Updated fallback logic in token-tree and string-tree routing paths to select the least-loaded healthy worker instead of herding all fallback requests onto the first healthy worker. Comprehensive unit and integration tests have been added to validate the correctness, concurrency safety, and equivalence of these optimizations. No review comments were provided, so there is no additional feedback to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@claude

claude Bot commented Jun 13, 2026

Copy link
Copy Markdown

👋 The PR description doesn't fully follow PULL_REQUEST_TEMPLATE.md:

  • Missing header: ## Description
  • Missing header: ### Problem
  • Missing header: ### Solution
  • Missing header: ## Changes
  • Missing header: ## Test Plan

Please update the PR description so reviewers have the context they need.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Reviewed all 8 changed files (event_tree.rs, cache_aware.rs, least_load.rs, mod.rs, worker_selection.rs, pd_router.rs, router.rs, worker.rs). This is a clean, well-tested performance optimization.

Findings: 0 🔴 Important · 0 🟡 Nit · 0 🟣 Pre-existing

Key things verified:

  • LeastLoadPolicy lock downgrade: The read-guard + AtomicU64::fetch_add scheme is correct. Concurrent credits are never lost (fetch_add is atomic regardless of ordering), and the deliberate relaxation (two simultaneous selections may pick the same worker before either credit lands) is bounded by concurrency — the concurrent test with 8×250 selections proves exact credit accounting.

  • Epoch-based cache invalidation: The (epoch << 32) | worker_id packing in WorkerRuntime::kv_worker_id is correct. Epoch 0 is reserved (sentinel for "unset"), epochs start at 1, and the cached_kv_worker_id guard rejects epoch 0. The cross-instance invalidation test demonstrates correctness even under deliberate raw-id collision.

  • Content hash memoization: OnceLock on SelectWorkerInfo correctly memoizes per-block hashes. The block-size check prevents wrong-width reuse (Cow::Owned fallback for mismatched sizes). PD mode's dual-selection reuse is tested.

  • Herding fix: healthy_indices.first() → min_by_key(load) in both tree fallbacks is a correctness improvement, not just perf. Both paths are covered by dedicated tests.

  • resolve_worker_id side-effects: Interning via the scoring path (vs. the old read-only worker_id()) is safe — intern_worker is idempotent, the id space is shared with the KV-event path, and the worker_to_id map is bounded by fleet size.

  • score_overlap behavioral equivalence: The reference-oracle test validates that the integer-keyed loop matches the old URL-probing max_by_key on cold caches, warm caches, ties, and unknown workers.

@slin1237
slin1237 force-pushed the fix/kv-index-worker-cap branch from ff20ff8 to b901a2e Compare June 14, 2026 04:05
@mergify

mergify Bot commented Jun 14, 2026

Copy link
Copy Markdown
Contributor

Hi @slin1237, this PR has merge conflicts that must be resolved before it can be merged. Please rebase your branch:

git fetch origin main
git rebase origin/main
# resolve any conflicts, then:
git push --force-with-lease

@mergify mergify Bot added the needs-rebase PR has merge conflicts that need to be resolved label Jun 14, 2026
Base automatically changed from fix/kv-index-worker-cap to main June 14, 2026 06:36
@slin1237 slin1237 closed this Jun 16, 2026
@lightseek-bot
lightseek-bot deleted the perf/routing-hot-path branch June 16, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

grpc gRPC client and router changes kv-index KV index crate changes model-gateway Model gateway crate changes needs-rebase PR has merge conflicts that need to be resolved

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant