vllm-project/router — High-performance Rust+PyO3 request router for vLLM deployments

code vllm-project-router
load-balancingrustpyo3vllmaxumradix-tree

vllm-project/router — L2 Distillation #

§1 TL;DR #

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.


§2 Project Identity & Motivation #

FieldValue
Repovllm-project/router
Versionv0.1.14
Primary languageRust (1.64M LOC) + Python (307K LOC)
LicenseApache-2.0
Stars233
MaintainerByron Hsu / vllm-project org
Fork originSGLang 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.


§3 Architecture & Module Map #

flowchart TB subgraph Entry["Entry Points"] CLI["main.rs
clap CLI"] Py["py_src/ vllm_router
PyO3 wrapper"] end subgraph Server["HTTP Server (axum + tokio)"] SRV["server.rs
bootstrap, lifecycle"] MW["middleware.rs
auth · rate-limit · request-id"] RT["routes/
routing_tree_builder"] end subgraph Routers["Router Implementations"] RF["RouterFactory"] R1["http/router.rs
Regular DP"] R2["http/pd_router.rs
Generic PD"] R3["http/vllm_pd_router.rs
vLLM PD (NIXL/Mooncake/MoRI)"] R4["http/openai_router.rs
OpenAI passthrough"] end subgraph Policies["Load Balancing Policies"] PF["PolicyFactory"] CA["CacheAwarePolicy
radix-tree affinity"] P2["PowerOfTwo
P2C least-loaded"] CH["ConsistentHash
virtual nodes"] RH["RendezvousHash
HRW"] RR["RoundRobin"] RD["Random"] end subgraph Core["Core Primitives"] W["Worker trait
BasicWorker · DPAwareWorker"] WR["WorkerRegistry
DashMap-backed"] CB["CircuitBreaker
Closed→Open→HalfOpen"] RY["Retry
exp-backoff + jitter"] TB["TokenBucket
rate limiter"] TR["Tree
concurrent radix tree"] end subgraph Infra["Infrastructure"] SD["service_discovery.rs
K8s pod watcher"] ZMQ["vllm_service_discovery.rs
ZMQ worker registry"] TOK["tokenizer/
HF + tiktoken"] MET["metrics.rs
Prometheus"] OT["otel_trace.rs
OpenTelemetry"] end CLI --> SRV Py --> SRV SRV --> MW --> RT RT --> RF RF --> R1 & R2 & R3 & R4 R1 & R2 & R3 --> PF PF --> CA & P2 & CH & RH & RR & RD CA --> TR R1 & R2 & R3 --> W W --> WR W --> CB R1 & R2 & R3 --> RY SRV --> SD & ZMQ SRV --> MET & OT CA -.-> TOK

Module responsibilities:


§4 Entry Points & API Surface #

Public API (Python) #


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.

CLI entry points #

EntrySourceDescription
vllm-router (Python)py_src/vllm_router/launch_router.py:main()Parses args → Router.start()
vllm-router (Rust)src/main.rsclap → ServerConfigserver::startup()
cargo benchbenches/Request routing, tokenizer, OTel benchmarks

Top 10 configuration flags #

FlagTypeDefaultPurpose
--worker-urlsVecrequiredBackend worker addresses
--policyenumround_robinLoad balancing algorithm
--vllm-pd-disaggregationboolfalseEnable prefill-decode split
--prefill / --decodeVecPD worker pools
--kv-connectorenumnixlKV transfer mechanism for PD
--intra-node-data-parallel-sizeusize1DP replicas per physical node
--service-discoveryboolfalseEnable K8s pod watcher
--api-key-validation-urlsVecExternal auth endpoints
--retry-max-retriesu323Max retry attempts
--cache-thresholdf32Min prefix match ratio for cache hit

Extension points #


§5 Core Data Structures #

Tree — 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
}

Worker implementations (src/core/worker.rs) #

VariantKey fieldsWhen used
BasicWorkerurl, healthy: AtomicBool, load: AtomicUsize, circuit_breakerStandard routing
DPAwareWorkerwraps BasicWorker + base_url, dp_rank, dp_sizeintra_node_data_parallel_size > 1

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.


§6 Critical Path Analysis & Implementation Highlights #

Hot path: HTTP request → routing decision → proxy #


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)

The cache-aware decision — the hardest-to-replicate insight #

CacheAwarePolicy::select_worker_with_headers (file: src/policies/cache_aware.rs) implements a two-regime algorithm:

  1. Load-balanced regime (triggered when $\text{max\_load} - \text{min\_load} > \text{abs\_threshold}$ AND $\text{max\_load} > \text{min\_load} \times \text{rel\_threshold}$): Falls back to shortest-queue routing, sacrificing cache affinity to prevent worker starvation.
    1. Cache-affinity regime (otherwise): Performs a character-level prefix match on the request text against a per-model radix tree. If $\text{match\_rate} = \frac{\text{matched\_chars}}{\text{input\_chars}} > \text{cache\_threshold}$, routes to the owning worker. Otherwise routes to the least-loaded worker and inserts the new prefix into the tree for future matches.
    2. This is novel because:

      • It operates on raw text characters, not token IDs, deliberately avoiding tokenization latency in the routing hot path. The approximation is acceptable because prefix sharing is character-aligned in practice (system prompts, few-shot examples).
      • The radix tree is multi-tenant (keyed by worker URL), allowing a single tree per model to track which worker "owns" which prefix region.
      • The two-regime fallback prevents the classic stickiness problem where cache-affinity routing creates load hotspots.

      Prefix match inner loop (src/tree.rs) #

      Tree::prefix_match_with_counts walks the trie character-by-character:

      1. Look up first_char in current node's DashMap children — O(1) via CharHasher
      2. Compare shared prefix via shared_prefix_count(remaining, node_text) — character scan
      3. If full match: advance remaining pointer, descend; if partial: stop
      4. Return PrefixMatchResult { tenant, matched_char_count, input_char_count }
      5. 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).

        Prefill-decode disaggregation #

        Three KV connector modes affect routing semantics:

        ConnectorModeRouting behavior
        NIXLPull-based (default)Router selects prefill worker, prefill returns result; decode worker pulls KV
        MooncakePush-basedRouter injects transfer_id + remote_engine_id into request body; prefill pushes KV to decode
        MoRI-IOSeparate 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.

        Key implementation details #

        1. Probabilistic LRU (1-in-8): The radix tree's epoch-based eviction only updates a node's last-access timestamp with probability 1/8 on reads. This dramatically reduces write contention on AtomicU64 timestamps under high-concurrency reads, at the cost of slightly stale eviction ordering — an acceptable trade-off for a routing cache.
          1. DP rank injection: When 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.

          2. §7 Concurrency & Memory #

            Concurrency model #

            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 hierarchy #

            LockTypeScopeContention
            Tree node childrenDashMap (sharded)Per-nodeLow (32 shards at root)
            Tree node textRwLockPer-nodeLow (writes only on split)
            Tree tenant_char_countDashMapPer-treeLow
            CircuitBreaker stateRwLockPer-workerVery low
            Worker health/loadAtomicBool / AtomicUsizePer-workerLock-free
            WorkerRegistryDashMapGlobalLow (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.

            Memory management #

            • Rust ownership model: no GC. Arc for shared ownership of workers, tree nodes, and policy instances. DashMap for concurrent hash maps.
            • Interned tenant IDs: Arc avoids per-lookup string allocation.
            • CompactString: inline storage for short node texts (≤24 bytes on stack).
            • Bounded radix tree: evict_tenant_by_size(max_tree_size) prevents unbounded memory growth; periodic eviction runs on a background tokio task with configurable eviction_interval_secs.

            §8 Performance Characteristics #

            Benchmark suite #

            The repo includes three Criterion benchmarks in benches/:

            BenchmarkWhat it measures
            request_processing.rsEnd-to-end request routing throughput (policy selection, no network)
            tokenizer_benchmark.rsTokenizer encode/decode speed (HF vs tiktoken)
            otel_disabled_path.rsOverhead 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).

            Scaling properties #

            • Worker count: All policies are O(N) or better in worker count. CacheAware is O(L) in request text length for the radix tree walk, plus O(N) for the load comparison fallback.
            • Concurrent requests: Bounded by max_concurrent_requests config and tokio thread pool. Lock-free atomics for load tracking avoid serialization.
            • Multi-model: PolicyRegistry maintains independent policy instances per model, so cross-model interference is zero.
            • Radix tree size: Bounded by max_tree_size per model. Eviction is O(tenants) to find the largest tenant, then O(tree depth) to remove its entries.

            Expected bottlenecks #

            1. Tokenization (if enabled): HuggingFace tokenizer initialization is slow (~100ms per model); runtime encode is ~1ms/request but adds to P99 latency.
            2. HTTP proxying: The actual forwarding via hyper is the dominant latency component — routing decisions are sub-microsecond for stateless policies, low-microsecond for CacheAware.
            3. Tree contention under write-heavy workloads: If many unique prompts arrive simultaneously (cold cache), the tree sees many inserts, each requiring DashMap write locks. The 32-shard root mitigates this.

            4. §9 Tech Debt & Code Quality #

              Test coverage #

              Three-tier test suite:

              • Unit tests (py_test/unit/): arg parsing, config validation
              • Integration tests (py_test/integration/): all 6 LB policies, circuit breaker state transitions, retry logic, PD routing
              • E2E tests (py_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.

              CI #

              .buildkite/ pipeline config present. No GitHub Actions visible. Buildkite likely runs cargo test, cargo clippy, Python tests, and Docker builds.

              Linter discipline #

              • Rust: Cargo.toml workspace with standard edition settings; clippy likely enforced in CI
              • Python: pyproject.toml present; no visible ruff or mypy config

              Known warts #

              1. No CHANGELOG.md — version history only from git tags
              2. handler.rs and server.rs were inaccessible during L1 fetch (timeout), suggesting large files that may benefit from splitting
              3. mini_lb.py is a "debug only" pure-Python mini load balancer — tech debt artifact
              4. Rendezvous hashing policy exists but is not documented in the README's policy table
              5. Dependency health #

                Key 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.


                §10 Community Health #

                MetricObservation
                Stars233 (growing — project is young, v0.1.x)
                LicenseApache-2.0 (permissive)
                Primary authorByron Hsu
                Orgvllm-project (corporate-backed by the vLLM team)
                Bus factorLow — appears primarily single-maintainer with the vLLM org as backstop
                GovernancePart of the vllm-project GitHub org; effectively BDFL + org review
                Fork originSGLang 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.


                §11 Comparison with Alternatives #

                Dimensionvllm-project/routerSGLang Model GatewayNginx/HAProxyCustom Python router
                LanguageRust + Python (PyO3)PythonCPython
                Cache-aware routingYes (radix tree)Yes (radix tree, original impl)NoPossible but slow
                PD disaggregationYes (NIXL/Mooncake/MoRI)PartialNoManual
                LB policies6 (including P2C, HRW)FewerMany (generic)Custom
                Circuit breakerBuilt-inNoExternalManual
                K8s service discoveryNative (kube-rs)NoIngress controllerManual
                Latency overheadSub-µs (Rust hot path)~ms (Python)Sub-µs~ms
                ObservabilityPrometheus + OTelBasicExtensiveCustom
                OpenAI API compatFull (chat, completion, embeddings, rerank, responses)PartialPassthroughCustom
                Ecosystem fitvLLM-nativeSGLang-nativeGenericAny
                Maturityv0.1.x (early)Integrated in SGLangBattle-testedVaries

                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.


                §12 Verdict & Recommendations #

                When to adopt (the "yes" regime) #

                • You run ≥2 vLLM workers behind a single endpoint and need cache-aware or PD-disaggregated routing
                • You need vLLM-specific PD disaggregation (NIXL/Mooncake/MoRI-IO connectors)
                • You want Kubernetes-native service discovery for dynamically scaling vLLM pods
                • You need production-grade resilience (circuit breakers, retries) with minimal latency overhead
                • Your deployment uses data parallelism with intra_node_data_parallel_size > 1 and needs transparent rank injection

                When NOT to adopt (the "no" regime) #

                • You use SGLang, TensorRT-LLM, or other non-vLLM backends — the PD disaggregation is vLLM-specific
                • You only have a single worker — no routing needed
                • You need a general-purpose API gateway with auth, rate-limiting, transformation — use a dedicated gateway (Kong, Envoy) with this router behind it
                • You need stable, battle-tested production software — v0.1.x with low bus factor carries risk for mission-critical deployments
                • You require custom routing logic beyond the 6 built-in policies and don't want to write Rust

                Usage gotchas #

                1. Cache-aware threshold tuning: The cache_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.
                2. DP size must match backend: 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).
                3. ZMQ discovery is NCCL-only: The --vllm-discovery-address flag only works with the NCCL connector, not NIXL or Mooncake.
                4. No graceful worker drain: Removing a worker (via K8s scaling or health failure) drops in-flight requests to that worker. The retry mechanism catches some, but streaming responses are lost.
                5. Token bucket is per-router, not per-user: The rate_limit_tokens_per_second applies globally, not per API key. Multi-tenant fairness requires an external gateway.
                6. Suggested contributions #

                  • Add integration benchmarks with published numbers (router overhead per policy under load)
                  • Document the Rendezvous hashing policy in the README
                  • Implement graceful worker drain (finish in-flight, stop new) before removal
                  • Add per-user rate limiting for multi-tenant deployments

                  §13 Implementation Cross-Reference #

                  ConceptFileKey function/struct
                  Cache-aware routing decisionsrc/policies/cache_aware.rsCacheAwarePolicy::select_worker_with_headers
                  Radix tree prefix matchsrc/tree.rsTree::prefix_match_with_counts
                  Radix tree insert/evictionsrc/tree.rsTree::insert, Tree::evict_tenant_by_size
                  Worker abstractionsrc/core/worker.rsWorker trait, BasicWorker, DPAwareWorker
                  Circuit breaker FSMsrc/core/circuit_breaker.rsCircuitBreaker
                  Retry with backoffsrc/core/retry.rsexponential backoff + jitter
                  PD routing (vLLM)src/routers/http/vllm_pd_router.rsVllmPrefillDecodeRouter
                  PD routing (generic)src/routers/http/pd_router.rsgeneric PD router
                  Policy factorysrc/policies/factory.rsPolicyFactory
                  Router factorysrc/routers/factory.rsRouterFactory
                  Config typessrc/config/types.rsRouterConfig, RoutingMode, PolicyConfig
                  K8s service discoverysrc/service_discovery.rsK8s pod watcher
                  ZMQ service discoverysrc/routers/http/vllm_service_discovery.rsZMQ worker registration
                  PyO3 bindingsrc/lib.rsRouter, PolicyType, vllm_router_rs module
                  Python CLIpy_src/vllm_router/launch_router.pymain(), launch_router()
                  DP rank injectionsrc/routers/http/dp_utils.rsX-data-parallel-rank header
                  Logprobs mergingsrc/routers/http/logprobs_merge.rsstreaming logprobs merge
                  Prometheus metricssrc/metrics.rsRouterMetrics
                  OpenTelemetrysrc/otel_trace.rstracing 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.