Rust-core (axum+tokio) request router for vLLM with 6 LB policies — headlined by a concurrent radix-tree cache-affinity algorithm — plus prefill-decode disaggregation over three KV connectors (NIXL/Mooncake/MoRI-IO), circuit breakers, K8s service discovery, and Python CLI via PyO3. Forked from SGLang Model Gateway.
| Field | Value |
|---|---|
| Repo | vllm-project/router |
| Version | v0.1.14 |
| Primary language | Rust (1.64M LOC) + Python (307K LOC) |
| License | Apache-2.0 |
| Stars | 233 |
| Maintainer | Byron Hsu / vllm-project org |
| Fork origin | SGLang Model Gateway |
Motivation. Large-scale vLLM deployments need a router between clients and a pool of inference workers that is: (a) aware of KV cache locality to avoid redundant prefill, (b) able to route prefill and decode phases to separate worker pools (PD disaggregation), and (c) production-grade with circuit breakers, retries, observability. Generic HTTP load balancers (Nginx, HAProxy) satisfy none of these. A Python-only router would bottleneck under high QPS. The project solves this with a Rust data plane exposed to the vLLM Python ecosystem via PyO3.
Module responsibilities:
main.rs uses clap to parse ~50 CLI flags into ServerConfig; the Python path (launch_router.py) parses the same flags via argparse and calls the PyO3-bound Router.start().server.rs wires up an Axum router tree with middleware (API key validation against external URLs, token-bucket rate limiting, request-ID propagation from 4 default headers).RouterTrait. The split is by deployment topology: Regular (homogeneous DP workers), PD (generic prefill/decode split), vLLM PD (vLLM-specific KV connectors), and OpenAI (backend passthrough).LoadBalancingPolicy. Stateless (Random, RoundRobin) vs. load-aware (PowerOfTwo) vs. affinity (ConsistentHash, RendezvousHash, CacheAware). Policies are per-model via PolicyRegistry.Worker trait abstracts health, load tracking, circuit breaker, and DP-rank metadata. Tree provides the concurrent radix tree for cache-aware routing.
from vllm_router import Router, PolicyType
router = Router(
worker_urls=["http://w1:8000", "http://w2:8000"],
policy=PolicyType.CacheAware,
# ... ~50 config fields
)
router.start()
The Python package (vllm-router on PyPI) is a thin wrapper. PolicyType enum and Router class are the only public surface — everything else is internal Rust.
| Entry | Source | Description |
|---|---|---|
vllm-router (Python) | py_src/vllm_router/launch_router.py:main() | Parses args → Router.start() |
vllm-router (Rust) | src/main.rs | clap → ServerConfig → server::startup() |
cargo bench | benches/ | Request routing, tokenizer, OTel benchmarks |
| Flag | Type | Default | Purpose |
|---|---|---|---|
--worker-urls | Vec | required | Backend worker addresses |
--policy | enum | round_robin | Load balancing algorithm |
--vllm-pd-disaggregation | bool | false | Enable prefill-decode split |
--prefill / --decode | Vec | — | PD worker pools |
--kv-connector | enum | nixl | KV transfer mechanism for PD |
--intra-node-data-parallel-size | usize | 1 | DP replicas per physical node |
--service-discovery | bool | false | Enable K8s pod watcher |
--api-key-validation-urls | Vec | — | External auth endpoints |
--retry-max-retries | u32 | 3 | Max retry attempts |
--cache-threshold | f32 | — | Min prefix match ratio for cache hit |
LoadBalancingPolicy trait, register in PolicyFactoryRouterTrait + WorkerManagement, register in RouterFactoryWorker trait, use WorkerFactoryTokenizer trait in tokenizer/traits.rsTree — concurrent multi-tenant radix tree (src/tree.rs) #
pub struct Tree {
root: NodeRef, // Arc<Node>
pub tenant_char_count: DashMap<TenantId, usize>, // per-worker prefix volume
}
CacheAwarePolicy::init_workers. Lives for the router lifetime. Evicted periodically via evict_tenant_by_size(max_tree_size).DashMap for children (32 shards at root, 8 for inner nodes) and RwLock for text. Tenant IDs are interned Arc.CompactString for node text avoids heap allocation for short strings. Custom CharHasher for DashMap optimizes single-character key lookups at tree children.Worker implementations (src/core/worker.rs) #| Variant | Key fields | When used |
|---|---|---|
BasicWorker | url, healthy: AtomicBool, load: AtomicUsize, circuit_breaker | Standard routing |
DPAwareWorker | wraps BasicWorker + base_url, dp_rank, dp_size | intra_node_data_parallel_size > 1 |
WorkerFactory during router init or service-discovery update. Stored in WorkerRegistry (a DashMap> ). Removed on health-check failure or K8s pod deletion.CircuitBreaker uses internal RwLock for state transitions.DPAwareWorker uses url@rank composite keys. When forwarding, it injects X-data-parallel-rank header so the backend vLLM process selects the correct DP shard.CircuitBreaker (src/core/circuit_breaker.rs) #Three-state machine: Closed → Open → HalfOpen → Closed. Transitions gated by configurable failure_threshold, success_threshold, timeout_duration_secs, and window_duration_secs. State stored in RwLock.
RouterConfig (src/config/types.rs) #~30-field struct driving all runtime behavior. Three RoutingMode variants (Regular, OpenAI, VllmPrefillDecode) control which router implementation is instantiated. Six PolicyConfig variants parameterize the selected policy. ConfigValidator runs structural checks before server startup.
Client HTTP request
→ axum handler (routes/pool_route.rs or prefill_decode_route.rs)
→ body parsing: serde_json::from_slice → ChatCompletionRequest
→ policy dispatch: LoadBalancingPolicy::select_worker_with_headers()
→ [CacheAware] Tree::prefix_match_with_counts() ← HOT INNER LOOP
→ compare match_rate vs cache_threshold
→ if hit: route to tenant worker; if miss: shortest-queue fallback
→ proxy: hyper HTTP client → selected worker URL
→ response streaming back to client (SSE for chat)
CacheAwarePolicy::select_worker_with_headers (file: src/policies/cache_aware.rs) implements a two-regime algorithm:
This is novel because:
src/tree.rs) #Tree::prefix_match_with_counts walks the trie character-by-character:
first_char in current node's DashMap children — O(1) via CharHashershared_prefix_count(remaining, node_text) — character scanremaining pointer, descend; if partial: stopPrefixMatchResult { tenant, matched_char_count, input_char_count }The fast-path tenant lookup uses a cached last_tenant field (avoiding DashMap iteration) that is correct when the tree is read-heavy (which it is in production — inserts are infrequent relative to lookups).
Three KV connector modes affect routing semantics:
| Connector | Mode | Routing behavior |
|---|---|---|
| NIXL | Pull-based (default) | Router selects prefill worker, prefill returns result; decode worker pulls KV |
| Mooncake | Push-based | Router injects transfer_id + remote_engine_id into request body; prefill pushes KV to decode |
| MoRI-IO | — | Separate I/O path for KV transfer |
The VllmPrefillDecodeRouter (src/routers/http/vllm_pd_router.rs) coordinates this by: (a) selecting a prefill-decode worker pair via select_worker_pair, (b) modifying the request body with connector-specific metadata, (c) forwarding to prefill, then (d) for streaming, merging decode responses via logprobs_merge.rs.
AtomicU64 timestamps under high-concurrency reads, at the cost of slightly stale eviction ordering — an acceptable trade-off for a routing cache.intra_node_data_parallel_size > 1, DPAwareWorker transparently expands each physical worker URL into N virtual workers (url@0, url@1, ...). The router treats them as independent targets for load balancing, but dp_utils.rs injects X-data-parallel-rank into the forwarded request so the backend vLLM selects the right NCCL rank.Fully async via tokio (multi-threaded runtime). The axum server handles requests concurrently on the tokio thread pool. No blocking I/O on the hot path — all worker health checks and HTTP proxying are async.
| Lock | Type | Scope | Contention |
|---|---|---|---|
| Tree node children | DashMap (sharded) | Per-node | Low (32 shards at root) |
| Tree node text | RwLock | Per-node | Low (writes only on split) |
| Tree tenant_char_count | DashMap | Per-tree | Low |
| CircuitBreaker state | RwLock | Per-worker | Very low |
| Worker health/load | AtomicBool / AtomicUsize | Per-worker | Lock-free |
| WorkerRegistry | DashMap | Global | Low (writes only on add/remove) |
No known ordering dependencies between locks — the design avoids nested locking by using atomics for high-frequency mutations (load counters, health flags) and DashMap sharding for concurrent reads.
Arc for shared ownership of workers, tree nodes, and policy instances. DashMap for concurrent hash maps.Arc avoids per-lookup string allocation.evict_tenant_by_size(max_tree_size) prevents unbounded memory growth; periodic eviction runs on a background tokio task with configurable eviction_interval_secs.The repo includes three Criterion benchmarks in benches/:
| Benchmark | What it measures |
|---|---|
request_processing.rs | End-to-end request routing throughput (policy selection, no network) |
tokenizer_benchmark.rs | Tokenizer encode/decode speed (HF vs tiktoken) |
otel_disabled_path.rs | Overhead of OTel instrumentation when tracing is disabled |
No published benchmark numbers in README or docs. The benchmarks measure router-internal latency, not end-to-end serving performance (which is dominated by GPU inference time).
max_concurrent_requests config and tokio thread pool. Lock-free atomics for load tracking avoid serialization.PolicyRegistry maintains independent policy instances per model, so cross-model interference is zero.max_tree_size per model. Eviction is O(tenants) to find the largest tenant, then O(tree depth) to remove its entries.Three-tier test suite:
py_test/unit/): arg parsing, config validationpy_test/integration/): all 6 LB policies, circuit breaker state transitions, retry logic, PD routingpy_test/e2e/): full router with mock workers (regular, PD, embeddings)No pytest --cov badge or coverage numbers published. Rust-side unit tests likely exist in #[cfg(test)] modules but are not surfaced in the repo's CI config.
.buildkite/ pipeline config present. No GitHub Actions visible. Buildkite likely runs cargo test, cargo clippy, Python tests, and Docker builds.
Cargo.toml workspace with standard edition settings; clippy likely enforced in CIpyproject.toml present; no visible ruff or mypy configCHANGELOG.md — version history only from git tagshandler.rs and server.rs were inaccessible during L1 fetch (timeout), suggesting large files that may benefit from splittingmini_lb.py is a "debug only" pure-Python mini load balancer — tech debt artifactKey Rust dependencies: axum (web framework), tokio (async runtime), hyper (HTTP), dashmap (concurrent map), pyo3 (Python bindings), serde/serde_json, clap (CLI), kube (K8s client), zeromq (ZMQ), prometheus (metrics). All are actively maintained, mainstream crates.
Key Python dependencies: maturin/setuptools-rust for PyO3 build. The Python surface is thin enough to have minimal dependency risk.
| Metric | Observation |
|---|---|
| Stars | 233 (growing — project is young, v0.1.x) |
| License | Apache-2.0 (permissive) |
| Primary author | Byron Hsu |
| Org | vllm-project (corporate-backed by the vLLM team) |
| Bus factor | Low — appears primarily single-maintainer with the vLLM org as backstop |
| Governance | Part of the vllm-project GitHub org; effectively BDFL + org review |
| Fork origin | SGLang Model Gateway; has diverged significantly |
The project is early-stage (v0.1.14) and tightly coupled to vLLM's PD disaggregation roadmap. Being under the vllm-project org provides credibility and long-term maintenance assurance, but the bus factor for the router-specific code is currently low.
| Dimension | vllm-project/router | SGLang Model Gateway | Nginx/HAProxy | Custom Python router |
|---|---|---|---|---|
| Language | Rust + Python (PyO3) | Python | C | Python |
| Cache-aware routing | Yes (radix tree) | Yes (radix tree, original impl) | No | Possible but slow |
| PD disaggregation | Yes (NIXL/Mooncake/MoRI) | Partial | No | Manual |
| LB policies | 6 (including P2C, HRW) | Fewer | Many (generic) | Custom |
| Circuit breaker | Built-in | No | External | Manual |
| K8s service discovery | Native (kube-rs) | No | Ingress controller | Manual |
| Latency overhead | Sub-µs (Rust hot path) | ~ms (Python) | Sub-µs | ~ms |
| Observability | Prometheus + OTel | Basic | Extensive | Custom |
| OpenAI API compat | Full (chat, completion, embeddings, rerank, responses) | Partial | Passthrough | Custom |
| Ecosystem fit | vLLM-native | SGLang-native | Generic | Any |
| Maturity | v0.1.x (early) | Integrated in SGLang | Battle-tested | Varies |
Key differentiator: The router is the only option that combines sub-microsecond Rust routing decisions with vLLM-native PD disaggregation support across three KV connector backends. The SGLang gateway has the cache-aware radix tree (it's the original source), but lacks the Rust performance and vLLM PD integration.
intra_node_data_parallel_size > 1 and needs transparent rank injectioncache_threshold parameter critically affects routing quality. Too low → false cache hits cause imbalanced load; too high → no affinity benefit. Start with 0.5 and tune based on your prompt distribution.intra_node_data_parallel_size in the router MUST equal the DP size configured on the vLLM workers. Mismatch causes silent routing errors (requests to wrong ranks).--vllm-discovery-address flag only works with the NCCL connector, not NIXL or Mooncake.rate_limit_tokens_per_second applies globally, not per API key. Multi-tenant fairness requires an external gateway.| Concept | File | Key function/struct |
|---|---|---|
| Cache-aware routing decision | src/policies/cache_aware.rs | CacheAwarePolicy::select_worker_with_headers |
| Radix tree prefix match | src/tree.rs | Tree::prefix_match_with_counts |
| Radix tree insert/eviction | src/tree.rs | Tree::insert, Tree::evict_tenant_by_size |
| Worker abstraction | src/core/worker.rs | Worker trait, BasicWorker, DPAwareWorker |
| Circuit breaker FSM | src/core/circuit_breaker.rs | CircuitBreaker |
| Retry with backoff | src/core/retry.rs | exponential backoff + jitter |
| PD routing (vLLM) | src/routers/http/vllm_pd_router.rs | VllmPrefillDecodeRouter |
| PD routing (generic) | src/routers/http/pd_router.rs | generic PD router |
| Policy factory | src/policies/factory.rs | PolicyFactory |
| Router factory | src/routers/factory.rs | RouterFactory |
| Config types | src/config/types.rs | RouterConfig, RoutingMode, PolicyConfig |
| K8s service discovery | src/service_discovery.rs | K8s pod watcher |
| ZMQ service discovery | src/routers/http/vllm_service_discovery.rs | ZMQ worker registration |
| PyO3 binding | src/lib.rs | Router, PolicyType, vllm_router_rs module |
| Python CLI | py_src/vllm_router/launch_router.py | main(), launch_router() |
| DP rank injection | src/routers/http/dp_utils.rs | X-data-parallel-rank header |
| Logprobs merging | src/routers/http/logprobs_merge.rs | streaming logprobs merge |
| Prometheus metrics | src/metrics.rs | RouterMetrics |
| OpenTelemetry | src/otel_trace.rs | tracing setup |
核心技术壁垒: The concurrent multi-tenant radix tree (src/tree.rs) with character-level prefix matching, probabilistic LRU timestamps (1-in-8 update), custom CharHasher, interned tenant IDs, and the two-regime load-balance/cache-affinity switch in CacheAwarePolicy. Replicating this requires deep understanding of both concurrent data structure design and LLM serving cache dynamics.