TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

Hands-On with NVIDIA Dynamo: KV-Aware Routing Got Almost Everything Right, but Disaggregated Serving Was 5x Slower on a Single GPU and Over 20x Slower Across Two Servers

Status: Draft (2026-10-07). All metrics in this post were measured directly by the author. The environment consisted of a single 8GB-class GPU, a 0.6B model, and Dynamo 1.6.0.dev20261006 (development build). The documentation reviewed in Part 6 was v1.5.0, so flags and default values may differ. The test between two servers with different GPUs (1GbE-class network) was measured in Section 4.5, while NVLink and RDMA connections are unmeasured . Writing rule: Empirical data takes priority, and unmeasured items are marked as "unmeasured".

Table of Contents

Part 6 was an overview and summary of Dynamo based on its documentation. To verify whether the documentation's claims hold true in practice, we installed it directly in a single-GPU environment. This post answers four questions:

  1. How much latency does the Dynamo frontend add per request?
  2. How effectively does KV-aware routing direct requests with repeated prefixes to the same worker compared to round-robin?
  3. What happens when the router fails to receive KV events?
  4. How does separating prefill and decode into two processes affect the time-to-first-token (TTFT) on a single GPU?
  5. How does splitting across two servers with different GPUs (1GbE-class network) affect performance?

Short Answer. Frontend overhead was approximately 13 ms when the server was idle and negligible under load. KV-aware routing directed repeated prefixes to the same worker 53 to 60 times out of 60 trials, compared to 35 to 43 times for round-robin. When the router was configured in event mode but workers failed to emit events, cache hit rates dropped to random levels without generating errors; only a single warning appeared in the logs. On a single GPU, disaggregated serving resulted in TTFT being 4.7 to 7.2 times slower across all conditions. This occurred because both workers shared the same GPU while incurring additional KV transfer overhead, meaning this did not measure the benefits of disaggregation. When splitting across two servers with different GPUs, the TTFT for a 1,500-word input jumped to 1.7 seconds, and for a 3,000-word input to 3.4–3.5 seconds—over 20 times slower than standalone serving (44–172 ms). This increased time matched the KV bytes divided by the link throughput (104 MB/s).

1. Environment and Methodology

2. Frontend Overhead

We compared running the same model using vLLM standalone (solo) versus behind the Dynamo frontend (dyn) with 3 runs each, using an input of 512 tokens and an output of 128 tokens. Values represent the median Time-to-First-Token (TTFT).

Concurrent Requests vLLM Solo Behind Dynamo
1 29–39 ms 42–43 ms
8 129–130 ms 129–141 ms
32 272–274 ms 272–274 ms

At concurrency 1, the median value increased by approximately 13 ms, from 30 ms to 43 ms. At concurrency levels 8 and 32, the difference was indistinguishable, and throughput was identical (1,507 vs. 1,509 tokens/sec at concurrency 32). The overhead of the frontend (HTTP, tokenization, routing decisions) is a fixed cost visible only when the server is idle, becoming masked under heavy load. Because this model is extremely fast, the 13 ms overhead is noticeable; for larger models, its relative impact is expected to be even smaller, though this remains unmeasured .

3. KV-Aware Routing

3.1 Configuration

We launched two workers with the same model and varied only the frontend's --router-mode .

Name Frontend Option Worker Side
Round Robin --router-mode round-robin KV Events Disabled
Random --router-mode random KV Events Disabled
KV-Aware, Approximate --router-mode kv --no-router-kv-events KV Events Disabled
KV-Aware, Event Mode But No Events --router-mode kv KV Events Disabled
KV-Aware, Event Mode --router-mode kv KV Events Enabled (Published via ZMQ)

The workload consisted of 12 types of prefixes (approx. 1,500 words, synthetic) sent in shuffled batches of 6, with the trailing 24 words changing each time. This resulted in 72 total requests, with 60 being re-requests where the prefix was sent for the second time or later. A re-request was counted as a cache hit if its TTFT was less than 60% of the initial request (~80 ms). Hits averaged ~15 ms, while misses averaged ~80 ms.

3.2 Results

Routing Method Hits out of 60 Re-requests (3 runs) Average Re-request TTFT
Round Robin 35, 40, 43 34–42 ms
Random 44, 31, 45 32–46 ms
KV-Aware, Approximate 60, 53, 60 15–26 ms
KV-Aware, Event Mode But No Events 39, 44, 41 32–38 ms
KV-Aware, Event Mode 60, 60, 60 15 ms

A prior run with the same configuration yielded consistent trends (Round Robin: 35, 40, 43; Random: 37, 37, 41; Approximate: 60, 60, 60; No Events: 41, 44, 37; Event Mode: 60, 60, 60). The approximate method achieved 60/60 hits across all three runs initially, but scored 53 on a rerun. While this suggests that self-tracking might be slightly less stable than the event-based approach, we cannot confirm this conclusively. The event-based approach achieved 60/60 in all six runs.

3.3 Silent Failures

The fourth row of the table represents the most critical finding in this experiment. When event mode was enabled via --router-mode kv but workers failed to emit KV events, the router's index remained permanently empty. Consequently, all requests evaluated to 0 overlap, causing workers to be selected solely based on load. Hit rates dropped to random levels (39, 44, 41), yet the server responded normally without errors, leaving only a single warning line in the logs. This warning was absent when running with workers that successfully emitted events.

WARN ... KV-event publishing is disabled for a role that requires KV events
diagnostic_code="kv_event_publisher_disabled" ... requirement="cache_aware_routing"

To detect that the hit rate is equivalent to random selection, one must inspect log warnings or separately measure whether the TTFT decreases upon resending the same prefix. If a router is enabled without measuring hit rates, systems can easily run in this degraded state.

3.4 Simulator vs. Empirical Data

Implementing the router's logic in pure Python and running the same workload (selecting the worker with the largest overlapping block from the beginning, breaking ties with the least loaded worker) shows that KV-aware routing achieves 60/60 hits, while lacking event notification yields results identical to round-robin. The directional trends match empirical findings. However, the simulator's round-robin hit rate is 49 out of 60, which is higher than the empirical results (35–43). While the simulator's workers remember processed prefixes indefinitely, real workers have a KV slot capacity of only 14,816 tokens, preventing them from holding all 12 prefix types (~18,000 tokens) and causing older ones to be evicted. Because KV-aware routing distributes prefixes among workers so that each worker holds only 6 types (~9,000 tokens), eviction is avoided. This implies that the benefit of prefix routing includes not only "routing to the same place" but also "reducing the working set size for each worker."

3.5 Router in Front of Two Servers

Placing two workers across two servers with different GPUs (KV slot capacities of 14,816 and 27,088 tokens) and sending the same workload over the inter-server network yielded the following hits out of 60 re-requests: Round Robin (40, 47, 50), Random (47, 40, 52), KV-Aware Approximate (60, 60, 60), and KV-Aware Event Mode (43, 60, 60). KV-aware routing achieved correct routing behavior even in a heterogeneous GPU environment. The reason behind the lower hit count (43) on the first iteration of event mode was not investigated (it is likely that some requests arrived before the event subscription was fully established, but this was unverified). Another limitation is that because initial request speeds differed between servers (44ms vs. 80ms), the hit determination criteria of "less than 60% of the initial request" was less precise in this test.

4. Prefill/Decode Disaggregation on a Single GPU

4.1 Configuration

We deployed dedicated prefill and decode workers as two processes on the same GPU (each using 38% of GPU memory, with KV transfer handled via NIXL), and compared them against a monolithic configuration where a single process handles both phases under the same workload. The output was set to 64 tokens, and values represent the median TTFT across three repetitions.

4.2 Results

Input (Words) Concurrent Requests Single Process (ms) Disaggregated (ms) Multiplier
1,500 1 78 406 5.2
1,500 4 190 1,359 7.2
3,000 1 171 812 4.7
3,000 4 398 2,442 6.1

Disaggregation was slower across all conditions. Looking at worker KV transfer metrics, concurrency 1 with a 1,500-word input averaged 196 ms per request (119.3 MB per transfer, 608 MB/s throughput), while concurrency 4 with a 3,000-word input averaged 711 to 1,723 ms. For concurrency 1, where the TTFT was about 330 ms slower than the single-process setup, the 196 ms transfer time accounted for a major portion of it. The remaining ~130 ms was not broken down or measured (unmeasured).

).

This is not "disaggregated serving is slow". Since both workers shared a single GPU, the computing resources did not increase, and there was only a single piece of hardware to solve the problem that disaggregation aims to address (long prefills blocking the decodes of other requests). Only the cost of moving the KV cache was added. Cases divided into two servers with different GPUs were measured in Section 4.5. Whether gains occur in environments connected by NVLink or RDMA was not measured in this post (Unmeasured).

Since the KV cache per token is 114,688 bytes, 1,500 tokens amount to approximately 164 MiB. This points in the same direction as the warning in the documentation cited in Part 6 (that moving the KV cache over a slow path can increase TTFT up to about 98 seconds). However, that 98-second figure is for a different model, different input length, and different connection, so its magnitude cannot be directly compared to this experiment. Moving 1,500 tokens at 608 MB/s takes about 280 ms, and at 0.1 GB/s levels like server-to-server TCP, it exceeds 1.5 seconds.

4.4 Why was KV transfer slow?

The cause could not be definitively determined. We tried changing the transfer method, and the results are as follows.

UCX Configuration Result
Default (unlimited) Transfer throughput 608 MB/s, Time to First Token 379~384 ms
Default with debug logs enabled 615 MB/s, Time to First Token 387~389 ms
Restricted to TCP family (UCX_TLS=tcp,cuda_copy,sm,self) 621 MB/s, Time to First Token 381~384 ms
Allowing both TCP and intra-device IPC Time to First Token 383~384 ms (drop in throughput not recorded in logs)
Allowing only intra-device IPC (UCX_TLS=cuda_ipc,cuda_copy,sm,self) Engine failed to start

The errors when allowing only IPC were no active messages transport ... cuda_ipc/cuda - no am bcopy and Destination is unreachable . It reads as though NIXL's UCX backend requires a transport capable of sending active messages for initialization, but IPC transport does not provide that. Therefore, the setting to narrow UCX_TLS exclusively to IPC because we wanted to use intra-device transfers cannot be used in this combination. The interface created by UCX under the default configuration was tcp/eth0, tcp/lo, cuda_ipc/cuda .

Because changing the transfer method did not alter the speed, the bottleneck may not be UCX's transport selection. Exactly where the bottleneck lies (request-level serialization, 28 block-level transfer descriptors, copy paths, etc.) was not broken down and measured (Unmeasured).

4.5 Disaggregated Serving Split Across Two Servers (Different GPUs, 1GbE-class Network)

One server was the 8GB-class GPU mentioned above, the other was a 24GB-class consumer GPU (time-shared with another service), and the inter-pod TCP throughput was 113 MB/s. Worker discovery was handled via etcd within one of the pods, and KV transfer used the NIXL default (UCX). The workload was identical (output 64, median Time to First Token across 3 repetitions per slot).

Configuration 1,500 words concurrency 1 1,500 concurrency 4 3,000 concurrency 1 3,000 concurrency 4
8GB-class server standalone 78ms 190ms 172ms 421ms
24GB-class server standalone 44ms 92ms 83ms 181ms
8GB-class does prefill, 24GB-class does decode 1,752ms 5,818ms 3,533ms 12,476ms
24GB-class does prefill, 8GB-class does decode 1,721ms 5,637ms 3,407ms 12,222ms

5. Incident Report

6. Limitations and Unmeasured Aspects

7. Operational Checklist

  1. If KV-aware routing is enabled, first verify whether workers are emitting KV events . Check frontend logs to ensure there are no kv_event_publisher_disabled warnings.
  2. After changing routing configurations, directly measure whether "sending the same prefix again reduces the Time to First Token". It can drop to random-level baselines without error.
  3. If prefixes exhibit greater diversity than a single worker's KV cache capacity, it has the additional effect of splitting prefixes across workers. Compare the worker's KV cache capacity (from server logs, GPU KV cache size) against the total sum of prefixes.
  4. Evaluate disaggregated serving only after separately measuring the KV transfer path (transfer throughput). In this experiment, transfer times matched calculations closely when links were 1GbE-class (Section 4.5). Multiplying the KV size per token (layers × KV heads × head dimension × 2 × bytes) by the input length yields the bytes transferred per request, and dividing that by the actual transfer throughput gives the additional time added to the Time to First Token.
  5. Before each run, verify that no leftover processes (engine cores) or model download locks remain.
  6. If NVIDIA driver versions differ across servers, verify first whether the driver supports the CUDA builds of the installed vLLM and torch. If not, it will fail with a CUDA error during the engine startup phase rather than showing up in server logs.

References

Sign in to like

Comments

No comments yet.

Sign in to leave a comment