Standalone KV Indexer
Overview
The standalone KV indexer (dynamo-kv-indexer) is a lightweight HTTP binary that subscribes to ZMQ KV event streams from workers, maintains a radix tree of cached blocks, and exposes HTTP endpoints for querying and managing workers.
This is distinct from the Standalone Router, which is a full routing service. The standalone indexer provides only the indexing and query layer without routing logic.
The HTTP API follows the Mooncake KV Indexer RFC conventions.
Multi-Model and Multi-Tenant Support
The indexer maintains one radix tree per (model_name, tenant_id) pair. Workers registered with different model names or tenant IDs are isolated into separate indexers — queries against one model/tenant never return scores from another.
model_name(required on/registerand/query): Identifies the model. Workers serving different models get separate radix trees.tenant_id(optional, defaults to"default"): Enables multi-tenant isolation within the same model. Omit for single-tenant deployments.block_sizeis per-indexer: the first/registercall for a given(model_name, tenant_id)sets the block size. Subsequent registrations for the same pair must use the same block size or the request will fail.
Compatibility
The standalone indexer works with any engine that publishes KV cache events over ZMQ in the expected msgpack format. This includes bare vLLM and SGLang engines, which emit ZMQ KV events natively — no Dynamo-specific wrapper is required.
Use Cases
- Debugging: Inspect the radix tree state to verify which blocks are cached on which workers.
- State verification: Confirm that the indexer’s view of KV cache state matches the router’s internal state (used in integration tests).
- Custom routing: Build external routing logic that queries the indexer for overlap scores and makes its own worker selection decisions.
- Monitoring: Observe KV cache distribution across workers without running a full router.
P2P Recovery
Multiple indexer replicas can subscribe to the same ZMQ worker endpoints for fault tolerance. When a replica starts (or restarts after a crash), it bootstraps its radix tree state from a healthy peer before processing live events.
How It Works
- Workers are registered via
--workersCLI, which connects ZMQ SUB sockets immediately. - A 1-second delay ensures the peer’s tree state has advanced past the ZMQ connection point, so the dump covers any events that would otherwise be lost to the slow-joiner window.
- The indexer fetches a
/dumpfrom the first reachable peer in--peers. - Dump events are applied to populate the radix tree.
- ZMQ listeners are unblocked and begin draining any events that buffered during recovery.
If no peers are reachable, the indexer starts with an empty state.
Example: Two-Replica Setup
Both replicas subscribe to the same workers. Replica B recovers A’s tree state on startup, then both independently process live ZMQ events going forward.
Consistency
The dump is a weakly consistent BFS snapshot of the radix tree — concurrent writes may race with the traversal. This is acceptable because:
- Stale blocks (partially removed branches): live
Removeevents will clean them up. - Missing blocks (partially added branches): live
Storedevents will add them. - The tree converges to the correct state after live events catch up.
Peer Management
Peers can be registered at startup via --peers or dynamically via the HTTP API. The peer list is used for recovery only — peers do not synchronize state in real time.
Building
The binary is a feature-gated target in the dynamo-kv-router crate:
CLI
HTTP API
GET /health — Liveness check
Returns 200 OK unconditionally.
GET /metrics — Prometheus metrics
Returns metrics in Prometheus text exposition format. Available when the binary is built with the metrics feature (enabled by default via standalone-indexer).
POST /register — Register an endpoint
Register a ZMQ endpoint for an instance. Each call creates or reuses the indexer for the given (model_name, tenant_id) pair.
POST /unregister — Deregister an instance
Remove an instance. Omitting tenant_id removes the instance from all tenants for the given model; providing it targets only that tenant’s indexer.
GET /workers — List registered instances
Returns:
POST /query — Query overlap for token IDs
Given raw token IDs, compute block hashes and return per-instance overlap scores (in matched tokens):
Returns:
Scores are in matched tokens (block overlap count × block size). Nested by instance_id then dp_rank.
POST /query_by_hash — Query overlap for pre-computed hashes
Same response format as /query. Scores are in matched tokens.
GET /dump — Dump all radix tree events
Returns the full radix tree state as a JSON object keyed by model_name:tenant_id:
Returns:
Each indexer is dumped concurrently. The block_size field lets recovering peers create indexers with the correct block size without requiring --block-size on every replica.
POST /register_peer — Register a peer indexer
POST /deregister_peer — Remove a peer indexer
GET /peers — List registered peers
Returns:
DP Rank Handling
When a worker registers with the standalone KV indexer (/register), it provides an instance_id, a ZMQ endpoint, and an optional dp_rank (defaults to 0). The service spawns one ZMQ listener per registration.
Each incoming KvEventBatch may carry an optional data_parallel_rank field. If present, it overrides the statically-registered dp_rank for that batch. This allows a single ZMQ port to multiplex events from multiple DP ranks.
Caveat: the registry only tracks dp_ranks from explicit /register calls. If an engine dynamically emits batches with a dp_rank that was never registered, the indexer will store those blocks correctly (under the dynamic WorkerWithDpRank key), but per-dp_rank deregistration (/unregister with dp_rank) will not find them. Full-instance deregistration (/unregister without dp_rank) still cleans up all dp_ranks for a given worker_id in the tree via remove_worker.
Gap Detection and Replay
ZMQ PUB/SUB is lossy — messages can be dropped under backpressure or brief disconnects. The indexer detects gaps by tracking the sequence number of each batch: if seq > last_seq + 1, a gap is detected.
When a replay_endpoint is provided during /register, the indexer connects a DEALER socket to the engine’s ROUTER socket and requests the missing batches by sequence number. The engine streams back buffered (seq, payload) pairs from its ring buffer until an empty-payload sentinel.
If no replay_endpoint is configured, gaps are logged as warnings but not recovered.
The sequence counter (last_seq) persists across unregister/register cycles, so re-registering a worker after a gap will trigger replay on the first batch received by the new listener.
Limitations
- ZMQ only: Workers must publish KV events via ZMQ PUB sockets. The standalone indexer does not subscribe to NATS event streams.
- No routing logic: The indexer only maintains the radix tree and answers queries. It does not track active blocks, manage request lifecycle, or perform worker selection.
Architecture
P2P Recovery Flow
See Also
- Mooncake KV Indexer RFC: Community API standardization for KV cache indexers
- Router Guide: Full KV router configuration and tuning
- Router Design: Architecture and event transport modes
- Standalone Router: Full routing service (routes requests to workers)