Repository navigation
[serve][llm] Session pinning for KVAwareRouter - #67224
jeffreywang88 wants to merge 4 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces session pinning to the KVAwareRouter and KVTokenTracker using an x-session-id header to improve KV cache reuse, along with a configurable idle timeout session_affinity_ttl_s. Feedback on the changes highlights potential runtime issues, including an AttributeError if request_router_kwargs is None in build_kv_token_tracker, and a TypeError if session_affinity_ttl_s is passed as None in KVAwareRouter.initialize_state. Additionally, it is recommended to validate that session_affinity_ttl_s is non-negative within the KVTokenTracker constructor.
0734676 to
0837da0
Compare
Signed-off-by: Jeffrey Wang <jeffreywang@anyscale.com>
0837da0 to
40fc1b3
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 40fc1b3. Configure here.
| # serves exactly one model, so a single fixed key scopes all of its workers | ||
| # together. | ||
| _MODEL_NAME = "default" | ||
| _TENANT_ID = "default" | ||
| _ROUTING_GROUP = "default" |
There was a problem hiding this comment.
Note for reviewers
This is only a rename to match dynamo's current API (it renamed tenancy to routing groups). Behavior doesn't change.
In dynamo, routing_group and model_name together pick a routing partition. Each partition has its own workers, KV index, load tracking, and session-affinity table. Ray doesn't use routing groups: every call passes "default", so each SelectionService has one partition.
LoRA doesn't use routing groups either. Serve multiplexing narrows the candidate replicas to those that have the adapter, and each request's lora_name salts its KV block hashes. That keeps each adapter's cache separate inside the same partition.
Signed-off-by: Jeffrey Wang <jeffreywang@anyscale.com>
|
I benchmarked this path with ai-dynamo/dynamo#16969 (details on that PR: 4 ingress processes, 1,024 mock vLLM workers, AgentX sessions, 1,000 ongoing selections per ingress).
The cache hit ratio at 4 replicas was 0.64, against 0.93 for one replica, and KV churn was 11× higher. Awaiting each round together ( Without session ids the same backlog appears (13–18 s of lag, 72% of reservations skipped), so this is independent of session pinning. Minor: |
…nd, cache worker ids Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Jeffrey Wang <jeffreywang@anyscale.com>
|
Thanks @jthomson04! I was able to repro the issue by using ray serve LLM with 4 ingress replicas, 64 mocker engines (
|
| await _complete(router, *skew) | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_failover_rebind_reaches_every_ingress(self): |
There was a problem hiding this comment.
Note for reviewers: This validate the SOFT affinity mode -- when the allowlist excludes the original bound worker, selection service rebinds the session to another eligible worker.
|
Need to build a wheel with the upstream changes for KV release tests. |

Description
Add session pinning to
KVAwareRouterso conversation turns reuse the same replica and its KV cache.The first request selects a replica using estimated token load and KV-cache overlap. Later requests with the same
x-session-idstay on that replica, even when another has lower load or greater overlap. If the replica becomes ineligible or the session expires, the router selects again.Session affinity flow
A. Ray calls its local selection service'sselect_and_reserve(...)with the session ID and allowed replicas. With no binding yet, the service selects by token load and KV overlap, books the request's load, and binds the session to that replica.Abroadcasts load and binding separately. The reservation goes out as aload_reservationupdate. The binding comes from thesession_binding_updates()stream, which emits each new binding and each TTL refresh, and goes out as asession_affinityupdate.Bapplies both.apply_session_binding(...)reconciles the binding by version.create_reservation(...)mirrors the load on the original worker, unless the request already completed. A completion that arrives mid-booking frees the booking.Balways learns the binding, even when it skips the load.B.Bfollows the replicated session binding even if another worker has lower load or more overlap. If the bound worker is unavailable or excluded from the allowlist, it rebinds the session with a newer version, and every peer receives it through the stream. Each turn books and broadcasts its own load, and each completed turn refreshes the session's TTL on every peer.API shape
Session pinning defaults to a 30-minute idle timeout. Configure
session_affinity_ttl_sthroughrequest_router_kwargs, or set it to0to disable pinning.Benchmark
For long-context, long-horizon workloads like AgentX, routing a request to a different replica later in a session can trigger substantial redundant prefill, significantly hurting throughput and interactivity.
b6f2a69:

Related issues
Dynamo PR: ai-dynamo/dynamo#16969.