Skip to content

perf(kv-router): run advisory selection on the caller's task - #17954

Draft
nnshah1 wants to merge 5 commits into
nnshah1/gr-core-c2b-transportfrom
nnshah1/gr-core-k1-advisory-inline
Draft

nnshah1 wants to merge 5 commits into
nnshah1/gr-core-c2b-transportfrom
nnshah1/gr-core-k1-advisory-inline

Conversation

@nnshah1

@nnshah1 nnshah1 commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

Overview

An advisory selection (SchedulerQueue::select_without_admission) no longer goes through the per-model admission actor. It runs on the caller's task, with the same selection code and the same state.

Stack (DEP #17638): #17597 → #17598 → #17665 → #17669 → this PR. This is the only PR in the stack that changes lib/kv-router.

Why: the fleet benchmark of the Global Frontend's core path (inbox/global-view-relay/gf-max-2026-11-03) showed that the remaining cost is the hop through the admission actor. Round-robin cost about 15 µs more per request than the current path. At saturation, selection took milliseconds, because each advisory pick waited behind the actor's other commands. Any advisory caller gets this fix, including the local router.

Key Changes

  • SelectionView holds what one selection reads: worker configurations, the selector, the availability and overload providers, the selection trace and observer, the block size and the full-rank counts. The actor and the inline path both build one, so there is one copy of the selection logic.
  • Shared helpers: project_worker_loads (load projection and reject thresholds) and advisory_response.
  • Selector lock: the selector moves behind Arc<parking_lot::Mutex<_>>, shared by the actor and the queue. The lock keeps picks and runtime policy updates serialized, as on the actor. No trait bound changes: pickers stay Send and do not need Sync.
  • Inline path: each call uses its own scratch buffers for loads and full ranks.
  • Semantics: an advisory selection reads the same shared state as before. It no longer waits behind actor commands that the caller did not wait for (for example, an unacknowledged cleanup). Acknowledged bookings and releases are already applied, so the caller sees them. Cleanup still runs: it has its own wake-up, and every booking is an actor command.
  • One yield before the pick: select_without_admission yields once (tokio::task::yield_now) before the inline pick, as the round trip to the actor did. Without it the caller's task runs on without a break, and the runtime loses the chance to batch other connections' I/O. Nothing is picked before the yield, so a caller dropped there leaves nothing behind.
  • Test reference: the actor path stays, test-only, as select_without_admission_on_actor.
  • Global Router: the abandoned-select test now accepts a select that answers on its first poll. A new test runs 64 least-in-flight picks on 4 threads: they split evenly and leave nothing pending.

Validation

  • cargo test --release -p dynamo-kv-router -p dynamo-global-router -p dynamo-global-frontend -p dynamo-global-common: all pass (kv-router 1427, global-router 79, global-frontend 12 + 9 + 16, discovery peers 2, global-common 10).
  • New kv-router tests: the inline path picks what the actor picks from the same state; 256 concurrent advisory selections next to bookings all succeed.
  • The parallel least-in-flight test passed 20 times in a row.
  • cargo fmt, cargo clippy --all-targets -D warnings: clean. cargo check -p dynamo-llm: builds.
  • Fleet (4 regions, 8 Global Frontends at 0.5 CPU, stub pools), first pass, before the yield: least-in-flight on the core went from 43,173 req/s at 0.093 ms CPU per request (R2) to 37,634 at 0.106 ms with this change, while selection time fell from 1.9 ms to 31 µs. A local profile showed the cause: no lock contention (the lock's slow path is 0.01% of samples); the request no longer yields, so tokio batches less I/O and the whole forwarding path costs more. Adding the yield brought the local cost back to R2's level (least-in-flight, 64 connections: R2 50.9, without yield 66.2, with yield 54.8 µs CPU per request). A fleet rerun with the yield is next.
  • Local benchmark (advisory_selection_cost, #[ignore]; 4 workers, 4 runtime threads, 20,000 selections):
Callers Actor p50 / p99 Inline p50 / p99 Calls/s actor → inline
1 0.7 / 2.8 µs 0.3 / 0.3 µs 0.75M → 2.3M
16 17.3 / 29.0 µs 0.7 / 4.8 µs 0.89M → 3.0M
256 287 / 352 µs 0.6 / 4.1 µs 0.88M → 3.2M

Issues and Ongoing Work

Pull Request Checklist
  • I used Dynamo's /pr-checklist skill.

🤖 Generated with Claude Code

nnshah1 and others added 4 commits November 3, 2026 07:33
…r core

Pools become the router core's workers. With --use-selection-core every
pick is an advisory select on the core, which books nothing, and the core
runs the policy's WorkerPicker plugin, picking as RoundRobinSelector and
LeastInFlightSelector do. A least-in-flight pick counts as pending on its
pool until the request takes it over as its in-flight count, so
concurrent picks see each other; a failed or abandoned select gives the
pick back. PoolSelector becomes the one selection trait: async, returning
a Selection that may hold state for the request. Adds FrontendSignals and
fixes the redirect path. Affinity is rejected on the core path until the
next PR.

Least-in-flight now places requests by affinity's rule, on both paths:
nearest region first, then requests in flight over the pool's capacity
(from its Relay), then fewer in flight, ties rotating; it leaves the
nearest pool only for one with more than --headroom-factor times its
spare room. The rule lives once, in placement.rs, shared by affinity and
both least-in-flight pickers. --region is now required for
least-in-flight, and --pool-relay is read by it. This intentionally
differs from main's least-in-flight, which had no notion of region.

Part of #17638

Signed-off-by: nnshah1 <neelays@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds HeadroomPicker, AffinityConfig, the soft session table and bounded
PinRefresh. Affinity books locally only to set, move or refresh a pin,
and now runs on the core path.

Part of #17638

Signed-off-by: nnshah1 <neelays@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds proto/replica_sync.proto (package dynamo.globalrouter.replica.v1)
behind the grpc feature, its build.rs and CONTRACT_MARKER, and the wire
conversions for affinity bindings in dynamo-global-router. The stream
carries session bindings and per-pool InflightCount only.

Part of #17639

Signed-off-by: nnshah1 <neelays@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds the gRPC hub, server and dialer, --replica-sync and --peer, peer
discovery through Global Discovery, InflightCount publishing, binding
replication and the replica-sync metrics, and documents them in the
README's "Replica sync" section. Bookings are never replicated.

Part of #17639

Signed-off-by: nnshah1 <neelays@nvidia.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@devin-ai-integration

devin-ai-integration Bot commented Nov 3, 2026 •

Copy link
Copy Markdown
Contributor

✅ Dynamo PR CI passed — run 41711101971 (attempt 1) on 3f1a6e1194

Gate checks: ✅ backend-status-check · ✅ deploy-status-check · ✅ dynamo-status-check

Posted automatically by Devin for run 41711101971. Updated on every full-CI run of this PR.

`SchedulerQueue::select_without_admission` sent each advisory selection to
the per-model admission actor and waited for the answer. The selection only
reads state the actor shares, so it now runs on the caller's task:

- `SelectionView` holds what one selection reads; the actor and the inline
  path both build one, so both run the same selection code.
- `project_worker_loads` and `advisory_response` are shared helpers.
- The selector moves behind `Arc<parking_lot::Mutex<_>>`, shared by the
  actor and the queue. The lock keeps picks and runtime policy updates
  serialized, as on the actor, so no trait bound changes (pickers stay
  `Send`, not `Sync`).
- The actor path stays as a test-only reference.

Global Router: the abandoned-select test now accepts a select that answers
on its first poll, and a new test runs least-in-flight picks on parallel
threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: nnshah1 <neelays@nvidia.com>
@nnshah1
nnshah1 force-pushed the nnshah1/gr-core-k1-advisory-inline branch from 040df3d to 3f1a6e1 Compare November 3, 2026 20:16
@nnshah1
nnshah1 force-pushed the nnshah1/gr-core-c2b-transport branch 4 times, most recently from 49b912d to 4861c1d Compare November 4, 2026 16:41

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant