TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Empirical measurement of round-robin vs. KV-aware routing when sending the same requests to Dynamo

Status: Draft (2026-11-14), empirically measured. The numbers in this post were measured directly by the author on 2026-11-13. The environment and requests are the same as in Part 1. Kubernetes v1.34, three GPU servers (one 24GB-class GPU, two 8GB-class laptop GPUs), 1GbE between nodes, the vllm-runtime:1.6.2 image for Dynamo v1.6.2 (vLLM 0.31.0), and the Qwen3-0.6B model. All router arguments are at their default values. Writing convention: Empirical data is the baseline; unmeasured items are marked as "Unmeasured", and unverified items are marked as "Unverified".

Round-Robin and KV-Aware Routing

Table of Contents

1. Questions Answered in This Part

The conclusion of Part 1 was as follows: When multiple vLLM instances are placed behind a Service, the prefix cache hit rate is determined by "(number of prefixes held by a single worker) ÷ (total number of prefix types)" and does not increase even when more workers are added. This part sends the same requests to Dynamo to check three things.

  1. Is there any difference just by routing through Dynamo (round-robin)?
  2. How much do the hit rate and time to first token change when KV-aware routing is enabled?
  3. What happens when the two things the documentation warns about—namely, hash seeds and KV events—are removed?

2. What Changes in the Manifest

The baseline used a Deployment and a Service. In Dynamo, those two are replaced by a single DynamoGraphDeployment(DGD). The operator reads this and creates the pods and the Service. Keeping the engine arguments, image, model, and node placement identical, only the following four items were changed.

Changed Item Baseline Dynamo
Resource Deployment + Service A single DGD (Components: 1 frontend, multiple workers)
Worker startup command python3 -m vllm.entrypoints.openai.api_server python3 -m dynamo.vllm
Endpoint opening the OpenAI API Port 8000 on every worker Port 8000 on frontend only; workers do not open HTTP
Routing Service randomizes per connection Frontend's DYN_ROUTER_MODE

Two settings are required for KV-aware routing.

# 프런트엔드
env:
  - {name: DYN_ROUTER_MODE, value: kv}

# 워커 인자
- --kv-events-config
- '{"publisher":"zmq","topic":"kv-events","endpoint":"tcp://*:20080","enable_kv_cache_events":true}'

Whenever KV blocks are created and destroyed, workers emit events to this address, and the router inside the frontend receives them to build a map of "which worker holds which blocks." When a request arrives, the router selects the worker whose prefix overlaps the most with the request, while also taking worker load into account.

Here are some facts confirmed during the migration process.

3. Five Variations

Name Configuration Objective
Baseline vLLM behind Service (Part 1) Comparison Value
Round-Robin DGD, DYN_ROUTER_MODE=round-robin Effect of merely passing through Dynamo
KV Routing DGD, DYN_ROUTER_MODE=kv, KV events, PYTHONHASHSEED=0 Primary feature
No Seed KV routing with PYTHONHASHSEED removed Is a hash seed really necessary?
Approximate Mode DYN_ROUTER_USE_KV_EVENTS=false, worker events also disabled How well does it perform with guesswork instead of events?

Requests are the same as in Part 1. A shared prefix of about 2,000 tokens was combined with a unique suffix of about 300 tokens per request, and the KV capacity of a single worker was capped at 1GiB (equivalent to about 3.95 requests). Prefix types were 2, 7, 12, and 24; concurrent requests were 4, 8, and 16; and each data point was run 3 times to use the median. Because the seed was identical, all variations received the exact same requests in the exact same order. There were 0 errors across all data points of the five variations.

For one data point in the round-robin variation (2 prefix types, concurrency 4), the first iteration was excluded because the prefix was already in the cache due to a test request sent immediately prior with the same seed. That data point uses the median of the remaining 2 runs.

4. Results: Round-Robin Matches Baseline, KV Routing Differs

같은 요청, 세 가지 길 워커 6대 · 동시 8 · 3회 중앙값 · 요청 단위 적중 비율 0 25% 50% 75% 100% 2 7 12 24 서로 다른 접두의 수 KV 인지 라우팅 Service(동그라미), 라운드로빈(네모)
Figure: Request-Level Cache Hit Ratio Across Three Paths

Ratio of requests that hit the cache (6 workers, concurrency 8).

Prefix Types vLLM behind Service Round-Robin KV Routing
2 95.0% 95.0% 97.5%
7 55.5% 53.5% 83.0%
12 32.1% 29.2% 67.9%
24 17.5% 17.5% 43.3%

The results are similar when viewed as the ratio of input tokens coming from the cache. In this request set, 2,032 out of 2,371 tokens are prefixes, so 0.857 is the maximum possible value.

Prefix Types Concurrency vLLM behind Service Round-Robin KV Routing
2 4 0.814 0.814 0.857
7 4 0.431 0.385 0.788
12 4 0.241 0.262 0.664
24 4 0.135 0.126 0.429
2 16 0.814 0.814 0.835
7 16 0.425 0.442 0.779
12 16 0.268 0.237 0.695
24 16 0.141 0.148 0.431

Round-robin is indistinguishable from the baseline. Simply placing the Dynamo frontend in front does not revive the cache. Distributing requests evenly across workers versus scattering them randomly are equivalent from the perspective of the prefix cache.

KV routing doubled to tripled hit rates in the range where prefixes exceed the capacity of a single worker. The difference is small for 2 prefix types. As noted in Part 1, when prefixes fit entirely within a single worker, routing provides virtually no benefit.

We verified whether the router actually consolidated requests by checking "how many workers a single prefix was sent to." Including nvext.extra_fields in the request's worker_id makes the response include the identifier of the worker that processed it. We counted using that.

Prefix Types Round-Robin KV Routing
2 6.00 workers 1.00 worker
7 6.00 workers 2.86 workers
12 6.00 workers 4.00 workers
24 5.92 workers 5.17 workers

(6 workers, concurrency 4, median of 3 runs.) Round-robin sent every prefix to all workers. KV routing sent each prefix to only one worker when there were 2 prefix types, spreading them out as the number of prefixes increased.

5. Time to First Token and Throughput

첫 토큰까지의 시간, 꼬리(p99) 워커 6대 · 동시 16 · 3회 중앙값 0 0.4초 0.8초 1.2초 1.6초 2 7 12 24 서로 다른 접두의 수 Service 뒤 vLLM Dynamo 라운드로빈 Dynamo KV 인지 라우팅
Figure: Time to First Token p99

Time to First Token (TTFT). Values are p50 / p99 per cell (6 workers).

Prefix Types Concurrency vLLM behind Service Round-Robin KV Routing
2 4 37 / 122ms 41 / 125ms 34 / 73ms
7 4 63 / 184ms 89 / 142ms 40 / 129ms
12 4 94 / 193ms 106 / 140ms 43 / 138ms
24 4 101 / 254ms 113 / 143ms 64 / 149ms
2 16 64 / 325ms 74 / 332ms 67 / 206ms
7 16 142 / 1,353ms 111 / 576ms 72 / 240ms
12 16 193 / 1,441ms 190 / 646ms 77 / 193ms
24 16 205 / 1,508ms 199 / 774ms 109 / 242ms

There are three takeaways here.

Throughput (requests per second, 6 workers, concurrency 16) also increased accordingly.

Prefix Types vLLM behind Service Round-Robin KV Routing
2 18.9 19.1 21.1
7 14.1 16.0 20.1
12 13.9 14.7 19.2
24 13.3 14.3 17.6

Even with the same GPUs, 32% more requests were processed with 24 prefix types. This corresponds to the reduction in time spent recalculating prefixes.

6. Are Results the Same with 3 Workers?

This deployment places one worker per GPU (concurrency 8).

Prefix Types Cache Token Ratio: Baseline / Round-Robin / KV TTFT p99: Baseline / Round-Robin / KV Requests/sec: Baseline / Round-Robin / KV
2 0.840 / 0.840 / 0.848 165 / 171 / 83ms 17.0 / 17.3 / 20.6
7 0.487 / 0.475 / 0.690 853 / 413 / 145ms 12.4 / 13.6 / 18.5
12 0.283 / 0.291 / 0.474 1,125 / 679 / 152ms 11.2 / 12.5 / 17.2
24 0.143 / 0.141 / 0.252 1,216 / 830 / 202ms 10.7 / 11.8 / 16.6

The trend is the same. Throughput increased by 55% with 24 prefix types. However, because there are only 3 workers, the total amount of prefixes that can be stored is halved, and the hit rate is correspondingly lower than the 6-worker deployment.

There was also one point where load exceeded capacity. At 3 workers, concurrency 16, and 24 prefix types, KV routing's TTFT p50 was 279ms and p99 was 962ms. While better than the baseline (563ms, 3,060ms), it did not drop below 0.2 seconds like the other points. Routing only reduces waste; it does not manufacture insufficient compute resources.

7. How Close Did It Get to the Upper Bound?

The theoretical upper bound calculated in Part 1 was "(number of workers) × (number of prefixes held by one) ÷ (total prefix types)." This value assumes prefixes are distributed across workers without overlap.

Deployment Prefix Types Upper Bound Actual Request-Level Hit (Concurrency 4 / 8 / 16)
6 Workers 7 100% 93 / 83 / 91%
6 Workers 12 100% 80 / 68 / 82%
6 Workers 24 99% 52 / 43 / 52%
3 Workers 7 100% 77 / 82 / 90%
3 Workers 12 99% 53 / 58 / 64%
3 Workers 24 49% 29 / 30 / 28%

Default KV routing achieved between half and two-thirds of the upper boundin regions where the cache was constrained. While two to three times better than random distribution, it falls short of fully utilizing capacity without waste.

The reason is shown in the table in Section 4. At 24 prefix types, a single prefix went to an average of 5 workers. Since the router considers worker load in addition to overlap, if the overlapping worker is busy, it sends the request to another worker. That worker computes and caches the prefix from scratch, which evicts other prefixes in the process. The tighter the cache, the more this eviction triggers a cascade.

The Dynamo documentation includes router arguments to adjust the weight between overlap and load. This article measured only the default values; whether tweaking those arguments can bring it closer to the upper bound remains unmeasured.

8. Hash Seeds and Approximate Mode

The documentation and examples state to give the workers a PYTHONHASHSEED=0 . The reason given is that because vLLM's block hash goes through Python's hash function, if the seed differs per process, the same prefix will result in different hashes across workers. I retested without the seed (6 workers, concurrency 8, cache token ratio).

Prefix types Round Robin KV Routing No Seed Approximate Mode
2 0.814 0.835 0.848 0.848
7 0.439 0.702 0.729 0.638
12 0.247 0.573 0.547 0.480
24 0.149 0.361 0.343 0.245

Removing the seed yielded the same results. The differences across the four points were within 0.03 and their directions varied. This magnitude is indistinguishable from the fluctuation between iterations. At least in this setup and configuration, routing did not break without the seed. The reason remains unverified. I could not confirm in the code whether the runtime sets the seed itself or if there is a way for the router not to rely on the worker's hash. Since this is a setting required by the documentation and carries no cost to include, I do not recommend omitting it just because the results were the same.

Approximate mode falls in the middle. In this mode, workers do not send events, and the router guesses that "that worker probably still has this prefix" based on the history of requests it has sent. While there was little difference with KV events for 7 prefixes (0.638 vs 0.702), for 24 prefixes, the score dropped to 0.245, cutting the difference with round robin (0.149) in half. This is likely because when the cache is tight and blocks are frequently evicted, the router's guesses diverge from reality. This should be understood as a fallback option to be used in environments where opening event ports is difficult.

9. Conclusion

The limitations of this measurement are the same as in Part 1. Prefixes were sampled evenly, request lengths were identical, and the model is small. With a larger model, the computation for a single missed request will take much longer, meaning the same hit rate difference will manifest as a much larger time difference, but that is not what this article measured. Whether routers see the same map when multiple frontends are deployed was also not measured.

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment