[Bugfix][Router] loadaware: score bursts against live load, not the arrival snapshot - #1108
ibrahimnd2000 wants to merge 3 commits into
Conversation
…rrival snapshot route_general_request snapshots request_stats before awaiting route_request, and LoadAwareRouter.route_request awaits the controller lookup (and the instance-map refresh). A request only counts as in flight once process_request calls on_new_request. So every request of a burst was scored against the same pre-burst load, and a small shared-prefix match (e.g. a common system prompt cached on one endpoint) won every tie: a 128-request burst over three endpoints was placed 128/0/0. Re-read request_stats from the RequestStatsMonitor after the awaits, on the select path and the no-cache fallback path. Nothing awaits between the placement decision and on_new_request, so each decision now sees every earlier one; the same burst is placed 43/43/42. Without a reachable monitor the caller's snapshot is used as before. Signed-off-by: Muhammad Ibrahim <ibrahimnd2000@gmail.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a live_request_stats method to fetch up-to-date request statistics from the monitor during routing, rather than relying on stale snapshots taken at the request's arrival. This prevents concurrent request bursts from being routed to the same endpoint due to outdated load information. Corresponding unit tests have been added to verify burst routing behavior and live stats retrieval. There are no review comments, so no further feedback is provided.
chrikrah
left a comment
There was a problem hiding this comment.
@ibrahimnd2000 I would merge this, and the no-await argument checks out. At 7085a91, deleting both live_request_stats call sites fails test_a_burst_is_spread_by_live_load, where your branch passes all 47.
non-blocking: the live read on the no-cache path at routing_logic.py:775 is pinned by nothing. Deleting only that line leaves all 47 passing, and that path carries the opening burst of a cold fleet.
$ PYTHONPATH=src python -m pytest src/tests/test_loadaware_router.py src/tests/test_kvaware_router.py src/tests/test_request_stats.py -q
47 passed # at 7085a91
4 failed, 43 passed # routing_logic.py reverted to 691dffa, tests kept
1 failed, 46 passed # only the two call sites deleted
47 passed # only line 775 deleted
$ PYTHONPATH=src python -m pytest src/tests/test_loadaware_router.py -q -s -k cold_burst # with the case below
PLACEMENT [43, 43, 42] # at 7085a91: 1 passed
PLACEMENT [128] # line 775 deleted: 1 failed
# not run: the rest of src/tests, and no real controller or engines
The cold-burst case
-async def route_burst(router, monitor):
+async def route_burst(router, monitor, layout_info=None):
@@ query_manager
+ if layout_info is not None:
+ return LookupRet(layout_info)
return LookupRet({INST_A: (LOCAL, SHARED_PREFIX_TOKENS)})
@@ end of file
+@pytest.mark.asyncio
+async def test_a_cold_burst_is_spread_by_live_load(stats_monitor):
+ """Nothing cached anywhere: the fallback must also see live load."""
+ from uhashring import HashRing
+
+ router = burst_router()
+ router.session_key = "x-user-id"
+ router.hash_ring = HashRing()
+ placement = await route_burst(router, stats_monitor, layout_info={})
+ print("PLACEMENT", sorted(placement.values(), reverse=True))
+ assert max(placement.values()) <= BURST // 2non-blocking: #1107 adds a third fallback_url call, on the lookup-failure path, still with the arrival snapshot. Reading live_request_stats inside fallback_url would cover all three in either merge order.
@ruizhang0101, you are on #1107 too, so whichever lands second needs this. @ibrahimnd2000, would you add the cold-burst case above as a second test?
Move the live request_stats read from the no-cache branch of route_request into fallback_url, so every fallback scores against the live load: no cache info, no endpoint selected, and the lookup-failure fallback added in vllm-project#1107, in either merge order. Add a cold-burst test (nothing cached anywhere) that fails when the fallback uses the arrival snapshot: 128/0/0 instead of 43/43/42. Signed-off-by: Muhammad Ibrahim <muhammad.ibrahim@multiversecomputing.com>
|
@chrikrah thanks for the careful review and the counter-checks. Both points are addressed in 431d783:
The rest of |
Problem
Under a burst,
loadawareplaces almost every request on one endpoint.route_general_requestsnapshotsrequest_statsbefore it awaitsroute_request.LoadAwareRouter.route_requestthen awaits the controller lookup (and the instance-map refresh). A request only counts as in flight onceprocess_requestcallson_new_request. So when many requests arrive together, every one of them is scored against the same pre-burst load. The load term is equal for all endpoints, and whichever endpoint holds even a short shared prefix (for example a common system prompt) wins every placement.Impact: a GLM-5.x deployment with 3 vLLM replicas ran a benchmark cell of 128 concurrent ~32k-token prompts.
Fix
route_request, re-readrequest_statsfromrequest.app.state.request_stats_monitor(new helperLoadAwareRouter.live_request_stats). This applies to the select path and to the no-cache fallback path.on_new_request. Afterroute_requestreturns,route_general_requestruns only synchronous code untilawait anext(stream_generator), which runsprocess_requestsynchronously up toon_new_request. So each decision sees every request placed before it.Tests
New tests in
src/tests/test_loadaware_router.pyroute 128 concurrent requests throughroute_requestwith a realRequestStatsMonitor. The controller round-trip is mocked with 1–20 ms of latency, and a 512-of-32,000-token prefix is cached on one of three endpoints. As in the real request path, each placement is followed byon_new_request.Also added: unit tests for
live_request_stats, covering the live read and the fallback to the snapshot without a monitor.Four of the new tests fail on
main.pre-commit(black, isort, ruff, codespell) passes.Validation
In the deployment above, with the fix, a live 30-request burst with a shared cached system prompt was placed 11 / 10 / 9. The router's per-endpoint in-flight counts matched the engines' running requests.
Notes
route_request. Once both are merged, that fallback should also uselive_request_stats. Both PRs add a test section at the same place intest_loadaware_router.py, so whichever lands second needs a trivial rebase.