[Router][Feat] loadaware routing: KV-cache-aware placement weighted by live load - #1035
Conversation
…ed by live load
kvaware maximizes cache-hit benefit alone: it routes to the first instance
reported to hold a request's prefix, however busy that instance is. Under
workloads with popular shared prefixes this concentrates load - every
request for a hot prefix lands on the one engine holding it, which queues
while its peers idle.
loadaware scores every endpoint
score(i) = matched_tokens(i) / prompt_tokens - beta * relative_load(i)
relative_load(i) = (load(i) - mean_load) / max(1, mean_load)
and routes to the argmax, so a warm-but-saturated instance can lose to a
cold-but-idle one. Both terms are dimensionless (a fraction of this prompt,
a fraction of this fleet's mean load), so the single tunable beta carries
no unit from the deployment and ships a meaningful default (1.0).
Measured on a 2xA10 OpenShift cluster serving Qwen2.5-3B through this
stack (Zipf-distributed shared prefixes, 16 req/s open-loop, 20 paired
seeds per arm): load imbalance (busiest/idlest vllm:num_requests_running)
drops 48.1% vs kvaware (exact Wilcoxon p < 0.0001), reproduced at 49.4%
by an independent re-run; a beta = 0 ablation is statistically
indistinguishable from kvaware, so the load term is the entire mechanism.
vLLM prefix-cache hit rate cost at beta = 0.5 is ~0.5 pp (91.2% -> 90.7%),
and router CPU/memory are unchanged.
KvawareRouter is untouched: LoadAwareRouter subclasses it and overrides
only the selection step, plus a new enum member, factory branch, CLI flag
(--loadaware-beta, with LOADAWARE_BETA env fallback) and the parser choice.
Works against stock LMCache lookups (it scores whatever holders
layout_info reports); a multi-holder lookup on the LMCache side makes the
benefit term fleet-wide.
28 offline unit tests included; no new dependencies.
Signed-off-by: Eliad Bazak <bazakeliad@gmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Eliad Bazak <bazakeliad@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new loadaware routing logic (LoadAwareRouter) that balances cache-hit benefits against relative engine loads using a tunable beta parameter. The feedback focuses on performance and robustness improvements in the new router: making tokenize_prompt asynchronous to avoid blocking the event loop with synchronous HTTP requests, optimizing refresh_instance_map to query endpoints concurrently and parse IPs more robustly, and adding a guard in route_request to handle empty endpoints gracefully.
…esh, 503 on no endpoints Three review findings, all inherited verbatim from the KvawareRouter code this class extends; fixed here without touching KvawareRouter: - tokenize_prompt: the remote /tokenize fallback was a synchronous requests.post on the event loop; it now runs in an executor and the method is async. - refresh_instance_map: controller round-trips for the instance_id -> URL bridge now run concurrently via asyncio.gather, and the endpoint IP is parsed with urllib.parse.urlparse instead of string splitting. - route_request: an empty endpoint list now raises HTTPException 503 (matching the disaggregated router's convention) instead of IndexError. Two tests added: the 503 guard, and an in-flight counter proving the refresh queries endpoints concurrently. Signed-off-by: Eliad Bazak <bazakeliad@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Eliad Bazak <bazakeliad@gmail.com>
ruizhang0101
left a comment
There was a problem hiding this comment.
This is great! Could you make the docstring concise? Also could you add a user-facing documentation(i.e. rst files) for this as well?
Per review on vllm-project#1035: trim the LoadAwareRouter docstrings to the essentials and move the design rationale (score normalization, beta semantics, tuning guidance) into a new user-facing tutorial at docs/source/use_cases/loadaware-routing.rst, linked from the Use Cases toctree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per review on vllm-project#1035: trim the LoadAwareRouter docstrings to the essentials and move the design rationale (score normalization, beta semantics, tuning guidance) into a new user-facing tutorial at docs/source/use_cases/loadaware-routing.rst, linked from the Use Cases toctree. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Eliad Bazak <bazakeliad@gmail.com>
3bc6953 to
bbb610e
Compare
|
Thanks for the review, @ruizhang0101! Both points addressed in bbb610e:
|
Motivation
kvawaremaximizes cache-hit benefit alone: it routes to the first instance reported to hold a request's prefix, however busy that instance is. Under workloads with popular shared prefixes this concentrates load — every request for a hot prefix lands on the one engine holding it, which queues while its peers idle. Cache affinity and load balance pull in opposite directions, and the stock router only pulls one way.What this PR adds
A
loadawarerouting logic that scores every endpointand routes to the argmax, so a warm-but-saturated instance can lose to a cold-but-idle one.
Design properties:
request_stats. The single tunablebetais therefore a pure exchange rate between cache locality and load balance and carries no unit from the deployment, which is what lets it ship a meaningful default (1.0: an endpoint 100% above fleet-average load is docked one full cache hit's worth of preference).KvawareRouteris untouched.LoadAwareRoutersubclasses it and overrides only the selection step. The rest is a new enum member, a factory branch,--loadaware-beta(withLOADAWARE_BETAenv fallback), and the parser choice.layout_inforeports; a multi-holder lookup on the LMCache side (their acknowledgedlookup()TODO) widens the benefit term to the whole fleet, and I plan to submit that separately to LMCache.request_statswas already handed to everyroute_requestcall).Measured results
2×NVIDIA A10 OpenShift cluster serving
Qwen/Qwen2.5-3B-Instructthrough this stack (chart 0.1.11), Zipf-distributed shared prefixes (128 × 2048 tokens, s = 0.9), 16 req/s open-loop Poisson, 500 requests × 20 paired seeds per arm, exact Wilcoxon signed-rank on per-seed differences:kvawareloadawareβ=0.5vllm:num_requests_running)Client-observed TTFT p95 was a pre-registered co-primary and did not reach significance at this operating point (−2.7%, p = 0.12): the fleet never queued (
vllm:num_requests_waiting= 0 in all scrapes), so there was no queueing delay for placement to remove. The imbalance reduction is the claim; the latency null is its boundary. Full methodology, per-seed data, and reproduction scripts: https://github.com/BenEpstein/caching-in-llms.Tests
28 offline unit tests in
src/tests/test_loadaware_router.pycovering the score arithmetic (scale invariance, fleet-relative load, benefit normalization and cap, the near-idle clamp), the instance-id → URL bridge (engine-restart staleness, phantom credit from dead instance ids), tie-breaking determinism, fallback behavior, and the beta configuration chain (flag > env > default). They run without lmcache installed.-swhen doinggit commit[Bugfix],[Feat], and[CI].