KV Events Cache-Aware Routing¶
This guide wires gRPC workers that publish KV cache events to SMG's cache_aware policy, so the gateway routes each request to the worker whose KV cache actually holds the longest prefix of it. SGLang is the main example; vLLM and TokenSpeed workers are set up the same way.
Why event-driven?¶
Without events, cache_aware predicts cache contents from its own routing history. With events, it routes against the blocks each engine reports it has stored and evicted.
| Mode | Index | Input | Used when |
|---|---|---|---|
| Event-driven | PositionalIndexer, built from engine events |
Token IDs | The model's event index has data |
| Approximate token tree | TokenTree, built from routing decisions |
Token IDs | The request has token IDs and the model has no event data |
| Approximate string tree | Tree, built from routing decisions |
Raw text | HTTP requests without token IDs |
The choice is made per model, not per worker. Once any worker of a model has streamed events into the index, every token-bearing request for that model is scored against the event index, and workers that do not stream events can then receive that model's requests only through the load-based fallback. Enable events on every worker of a model.
Event-driven routing also needs the default --cache-index tree: with --cache-index hash, the gateway never consults the event index.
How the pieces fit together¶
client
│ OpenAI-compatible HTTP
▼
SMG gateway: cache_aware policy ◀── PositionalIndexer (one per model)
│ ▲
│ gRPC requests │ KvEventMonitor
▼ │ (SubscribeKvEvents stream)
worker: smg-grpc-servicer ─────────────────┘
▲
│ ZMQ PUB → SUB, on the worker host (--kv-events-config)
engine scheduler (SGLang, vLLM or TokenSpeed)- The engine's scheduler publishes block-stored, block-removed and all-blocks-cleared events on a ZMQ
PUBsocket configured by--kv-events-config. smg-grpc-servicer, which serves the worker's gRPC API, subscribes to the engine's publishers locally and re-publishes the events as the server-streaming RPCSubscribeKvEvents.- SMG's
KvEventMonitoropens one subscription per gRPC worker and feeds the events into a per-modelPositionalIndexer;cache_awarescores token-bearing requests against that index. If a stream breaks, the monitor reconnects and either resumes or rebuilds the worker's entries (see Recovery after gaps).
Step 1 — Launch the SGLang worker¶
Install the SGLang extra of the servicer, then launch the SGLang server with both --smg-grpc-mode and --kv-events-config:
pip install "smg-grpc-servicer[sglang]"
python -m sglang.launch_server \
--model-path meta-llama/Llama-3.1-8B-Instruct \
--host 0.0.0.0 \
--port 50051 \
--smg-grpc-mode \
--page-size 16 \
--kv-events-config '{"publisher":"zmq","endpoint":"tcp://*:5557","replay_endpoint":"tcp://*:5558","topic":"kv-events"}'What each flag does:
| Flag | Why |
|---|---|
--smg-grpc-mode |
Serves SMG's gRPC SglangScheduler service from smg-grpc-servicer instead of SGLang's HTTP server. Required for SMG to talk to this worker over gRPC. --grpc-mode is a deprecated alias. SGLang also opens an HTTP sidecar on --port + 1 (--smg-http-sidecar-port moves it). |
--page-size 16 |
The KV cache page size, in tokens. Use the same value for SMG's --block-size (see Block size alignment). |
--kv-events-config |
A JSON object parsed by SGLang's KVEventsConfig.from_cli. "publisher":"zmq" turns publishing on (the default "null" publishes nothing), and replay_endpoint lets the servicer fill gaps in the stream from SGLang's replay buffer. |
--kv-events-config field reference¶
All fields and defaults match SGLang's KVEventsConfig (see python/sglang/srt/disaggregation/kv_events.py upstream):
| Field | Default | Notes |
|---|---|---|
publisher |
"null" |
Set to "zmq" to publish. With any other value the servicer answers SubscribeKvEvents with UNIMPLEMENTED, and SMG stops subscribing to that worker. |
endpoint |
"tcp://*:5557" |
ZMQ PUB address. The publisher binds wildcard endpoints such as tcp://*:PORT (and ipc:// or inproc:// ones) and connects to any other address, which leaves nothing listening, so keep the wildcard form. The servicer connects to it locally, rewriting * and 0.0.0.0 to 127.0.0.1. |
topic |
"" |
ZMQ topic. The servicer reads the same config and subscribes to this topic, so any value works. |
replay_endpoint |
null |
ZMQ ROUTER address that serves recent batches for replay. When set, the servicer fills gaps in each rank's events from it before forwarding later batches; without it, a missed batch costs the worker's index entries (see Recovery after gaps). |
buffer_steps |
10000 |
Number of recent batches kept for replay. |
hwm |
100000 |
ZMQ high-water mark: once this many messages are queued for a subscriber that is not keeping up, new events are dropped. The servicer uses the same value for its receive queue. |
max_queue_size |
100000 |
Queue between the scheduler and SGLang's publisher thread. |
With data parallelism, each rank publishes on endpoint port + dp_rank and serves replay on replay_endpoint port + dp_rank; rank 0 keeps the configured ports. Pick port ranges that do not overlap (5557 and 5558 collide as soon as there are two ranks). The servicer subscribes to every rank's publisher and relays them as one stream, each batch tagged with its rank, so the index sees every rank's cache.
Alternative: launch a vLLM worker¶
vLLM publishes KV cache events on a ZMQ socket; enable them with --kv-events-config and run the worker in SMG gRPC mode:
pip install "smg-grpc-servicer[vllm]"
# --grpc serves vLLM through smg-grpc-servicer instead of the OpenAI HTTP server;
# --kv-events-config turns on KV-event publishing:
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--grpc \
--host 0.0.0.0 \
--port 50051 \
--kv-events-config '{"enable_kv_cache_events": true, "publisher": "zmq", "endpoint": "tcp://*:5557", "replay_endpoint": "tcp://*:5558", "topic": "kv-events"}'Event-driven routing needs the worker in SMG gRPC mode (--grpc; pip install "vllm[grpc]" pulls in the same servicer package), because KV events stream over the SubscribeKvEvents RPC. See gRPC Workers for other launch flags.
| Field | Why |
|---|---|
enable_kv_cache_events: true |
vLLM's master switch. Without it no events are published, and SMG stops subscribing to the worker. |
publisher: "zmq" |
Selects the ZMQ publisher the servicer reads. Recent vLLM versions default to it once events are enabled. |
endpoint / topic |
As for SGLang; keep the wildcard tcp://*:PORT form. With data parallelism the port is endpoint port + dp_rank, and the servicer subscribes to every rank. |
replay_endpoint |
As for SGLang: the servicer fills gaps in each rank's events from it before forwarding later batches; without it, a missed batch costs the worker's index entries (see Recovery after gaps). Each rank serves replay on replay_endpoint port + dp_rank. |
SMG learns the block size from the BlockStored events themselves; set SMG's --block-size to vLLM's --block-size so the approximate token tree uses the same pages.
Alternative: launch a TokenSpeed worker¶
TokenSpeed's scheduler publishes KV cache events on a ZMQ socket; enable them with --kv-events-config. The TokenSpeed gRPC server is the SMG gRPC entrypoint, so there is no separate --grpc flag:
# TokenSpeed is installed from source (engine + kernel + scheduler); see
# scripts/ci_install_tokenspeed.sh in the smg repository. Install the servicer's extra deps:
pip install "smg-grpc-servicer[tokenspeed]"
# --kv-events-config turns on KV-event publishing in the scheduler:
python -m smg_grpc_servicer.tokenspeed \
--model meta-llama/Llama-3.1-8B-Instruct \
--host 0.0.0.0 \
--port 50051 \
--kv-events-config '{"enable_kv_cache_events": true, "publisher": "zmq", "endpoint": "tcp://*:5557", "topic": "kv-events"}'| Field | Why |
|---|---|
enable_kv_cache_events: true |
TokenSpeed master switch. Without it the scheduler records no events even if a publisher is set. |
publisher: "zmq" |
Selects the ZMQ publisher the servicer reads. Unset defaults to "zmq" when events are enabled; "null" (or any other value) disables event streaming. |
endpoint / topic |
ZMQ PUB address and topic. Use a bind-style endpoint (tcp://*:PORT): TokenSpeed binds only when the endpoint contains * or :: or starts with ipc:// or inproc://, so a concrete address like tcp://127.0.0.1:PORT makes it connect instead, leaving nothing bound and the stream idle. With data parallelism the port is endpoint port + dp_rank, and the TokenSpeed servicer subscribes to rank 0 only. |
--kv-events-config is parsed by TokenSpeed's KVEventsConfig.from_cli. The TokenSpeed servicer does not replay missed events; a sequence gap costs that worker at most the missed batches, bounded by SMG (see Recovery after gaps).
SMG learns the block size from the BlockStored events; TokenSpeed's cache reuse granularity is --prefix-granularity (default 64 tokens; older builds call it --block-size, which remains an alias), so set SMG's --block-size to the same value.
Step 2 — Launch SMG¶
Point SMG at the gRPC workers and select cache_aware:
smg launch \
--worker-urls grpc://worker-1:50051 grpc://worker-2:50051 \
--model-path meta-llama/Llama-3.1-8B-Instruct \
--policy cache_aware \
--block-size 16 \
--host 0.0.0.0 \
--port 30000worker-2 is a second worker launched the same way, on another host. On a single host, give each worker its own --port and its own event ports, and leave room for each SGLang worker's sidecar on --port + 1.
The flags that matter for event-driven routing:
| Flag | Why |
|---|---|
grpc://... worker URLs |
SMG subscribes to KV events only on gRPC workers. HTTP workers are skipped, and ZMQ (ipc://) workers log a warning and use the approximate token tree. |
--policy cache_aware |
The only policy that reads the event index. It is also the default policy. |
--block-size 16 |
Page size of the approximate token tree, which routes until the event index has data, and the block size the event index assumes until it learns the engine's. Match the engine's page size. |
--model-path tells the gateway which tokenizer to use, as in any gRPC-worker deployment (gRPC Workers). For the other routing knobs, see Tuning.
Block size alignment¶
The event index cuts each request's token IDs into blocks and looks the blocks up by content hash. If the block size differs from the one the engine used, no block matches, and the model's token requests are routed by load alone.
SMG picks the block size per model, in this order:
- Learned from events (highest priority): each worker's subscription records the block size of the first stored block it receives; the most recent value wins.
--block-size(router-wide default).
The kv_block_size field of a worker spec registered through the admin API (POST /workers) is accepted but not applied in v1.11.0. In practice, keep the engine's page size (SGLang --page-size, vLLM --block-size, TokenSpeed --prefix-granularity) and SMG's --block-size equal, and let SMG correct itself once events arrive. The approximate token tree always uses --block-size. Serve each model with one page size: with mixed sizes, the model's block size is whichever a worker reported last.
Step 3 — Send a request¶
The API surface is unchanged:
curl http://localhost:30000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "meta-llama/Llama-3.1-8B-Instruct",
"messages": [
{"role": "user", "content": "Hello, who are you?"}
]
}'Send the same prompt twice. The first request has no cached prefix and goes to the lowest-expected-wait worker. Once that worker reports the blocks it stored, the second request is scored against them and lands on the same worker, as long as that worker is not far busier than the rest of the fleet.
Verifying event delivery¶
The gateway logs these lines when the path is live (timestamps, targets and source locations omitted).
1. Subscription started. When SMG registers a gRPC worker under cache_aware, KvEventMonitor logs:
INFO Starting KV event subscription worker_url=grpc://worker-1:50051 model_id=meta-llama/Llama-3.1-8B-Instruct
INFO KV event stream connected worker_url=grpc://worker-1:50051 start_seq=0If you do not see the first line for a worker, the worker is not a gRPC worker, the gateway's --policy is not cache_aware (a policy worker label alone does not start subscriptions), or the worker's model is routed by another policy. If events are disabled on the worker, the servicer rejects the stream and SMG stops trying:
WARN Backend does not implement SubscribeKvEvents, disabling KV event subscription for this worker worker_url=grpc://worker-1:500512. Backend block size learned. Once the first stored block arrives, SMG records the backend's block size:
INFO Learned block_size from KV event model_id=meta-llama/Llama-3.1-8B-Instruct block_size=163. Routing decision uses the index. With RUST_LOG=info,smg::policies::cache_aware=debug, each routed request logs Event-driven routing: overlap match with the chosen worker and a branch of event_hit (affinity held) or event_spill (the spill gate moved the request), or Event-driven routing: no overlap, expected-wait fallback.
While the model's event index is empty, token requests route on the approximate token tree, so cache affinity keeps working, only less accurately.
Recovery after gaps¶
Every batch on the stream carries a sequence number. KvEventMonitor applies batches in order and skips duplicates. When it sees a gap in the sequence, it reconnects immediately and asks the servicer to resume after the last batch it applied; after a stream error or a clean close, it reconnects with exponential backoff from 100 ms up to 5 s. A subscription call that is not answered within 2 s — a port that accepts but does not serve yet, such as an engine still starting — is abandoned and retried.
| Worker | Recovery |
|---|---|
| SGLang, vLLM | The servicer fills any gap in a rank's events from that rank's replay_endpoint before forwarding later batches, so SMG receives an unbroken stream and the index stays intact. A gap it cannot fill — no replay_endpoint, the missed batches already left the replay buffer, the replay failed, or the engine's publisher restarted — ends the stream with DATA_LOSS: SMG drops that worker's index entries and starts a new subscription. |
| TokenSpeed | No replay: after a gap, the stream resumes from the engine's current position. SMG asks once for the missed batches, and when the stream skips ahead anyway it settles the gap — up to 1,024 missed batches keep the worker's existing entries (the stores SMG missed stay invisible until those blocks are stored again), a larger gap drops them. Index updates continue either way. |
The servicer waits up to five seconds for each replay message before declaring a gap unfillable.
On SGLang and vLLM workers, the default Python servicer keeps nothing between subscriptions: the stream's numbering is per call, a resume cursor is answered with OUT_OF_RANGE, and a new subscription carries live events only. After any stream break between SMG and the servicer, SMG therefore drops the worker's index entries and rebuilds them from live events. The rebuilt index is not a snapshot of the engine's cache: affinity to that worker returns as it caches new prefixes.
Workers running a Rust servicer keep more. Its relay follows the engine's publisher for the servicer's whole lifetime: a reconnecting gateway is served the batches after its cursor from history, and a new subscription is answered with either the engine's complete event history or an in-band snapshot of the engine's live blocks, which SMG applies in place of whatever it held for that worker. Cache affinity then survives gateway restarts and long disconnects without waiting for new stores.
SMG logs Sequence gap detected, reconnecting for replay from seq N on a gap, and KV event replay cursor expired; clearing worker state and requesting a current snapshot or KV event subscriber fell behind; clearing worker state and requesting a current snapshot when it drops a worker's entries. A restart of the engine's own publisher (its sequence numbers start over) also clears the worker's entries — the engine's cache is empty again — and the stream continues from the new sequence.
Bounding the index¶
The event index drops a worker's entries when its engine reports evictions or the worker is removed. If an engine stops reporting evictions, entries accumulate. Two optional bounds, both off by default, are checked every 30 seconds:
| Flag | Effect |
|---|---|
--kv-indexer-ttl-secs |
Prune entries that were neither stored nor read by a routing query within this many seconds. Unset or 0 disables the TTL pass. |
--kv-indexer-max-entries |
Per-model ceiling; above it, the least recently touched entries are pruned down to 90% of the ceiling. Unset or 0 disables the ceiling. |
A pass that prunes entries logs Pruned positional indexer with the counts. Both flags exist only in the Rust smg binary: the Python launcher that pip install smg and the container image use does not accept them.
Tuning¶
| Knob | Where | Effect |
|---|---|---|
--block-size |
SMG | Keep equal to the engine's page size. |
--balance-abs-threshold / --balance-rel-threshold |
SMG | Spill gate: a holder whose in-flight request count exceeds both margins over the fleet mean sends the request to another worker. |
--overlap-decay |
SMG | De-ranks candidates with a waiting-prefill backlog. Needs workers that report waiting uncached tokens (SGLang does; vLLM and TokenSpeed do not). |
--selection-temperature |
SMG | Spreads picks across candidates with near-equal overlap instead of always taking the best. |
--balance-token-usage-threshold / --overload-token-usage-threshold |
SMG | Suspend cache affinity under KV pressure (off by default). |
--kv-indexer-ttl-secs / --kv-indexer-max-entries |
SMG | Bound the event index (see Bounding the index). |
replay_endpoint / buffer_steps |
SGLang and vLLM --kv-events-config |
Enable gap replay; raise buffer_steps if gaps go unfilled because the missed batches already left the buffer. |
hwm |
--kv-events-config |
Raise if events are dropped under bursty load. |
--cache-threshold does not apply to event-driven routing: any worker holding at least the request's first full block is a candidate. See Cache-Aware Routing for the full list of knobs.
Monitoring¶
| Metric | Type | Labels | Description |
|---|---|---|---|
smg_kv_event_subscription_failures_total |
counter | worker, reason |
A subscription task failed: panic, join_error or intern_failed. After panic or intern_failed, that worker's events no longer reach the index. |
smg_engine_cache_hit_rate |
gauge | worker, model, dp_rank |
Engine-reported prefix cache hit rate, to check that routing on events pays off. |
The smg_cache_aware_policy_branch_total and smg_cache_aware_match_ratio metrics count tree-mode decisions only; use the debug log above to inspect event-driven decisions.
Caveats¶
- gRPC only. Events stream over gRPC through
smg-grpc-servicer. HTTP workers use the approximate string tree, ZMQ (ipc://) workers use the approximate token tree, and MLX workers do not implementSubscribeKvEvents. - TokenSpeed: DP rank 0 only. The SGLang and vLLM servicers relay every data-parallel rank's publisher; the TokenSpeed servicer subscribes to rank 0's only, so for data-parallel TokenSpeed workers the index reflects rank 0's cache.
- One block size per model. If workers serving the same model use different page sizes, the policy uses whichever block size a worker reported last. Keep page sizes homogeneous within a model.
- Per-gateway index. Each gateway subscribes to every worker itself. With
--enable-mesh, gateways synchronize the approximate trees, not the event index. - No cache partitions. The event index ignores the
cache_salt,extra_keyand LoRA fields that partition the approximate trees. - Unaligned stores are skipped. The servicers drop any
BlockStoredevent whose token count is not its block count times its block size (vLLM can emit these for Mamba models); those blocks never reach the index. - Replay needs
replay_endpoint. The SGLang and vLLM servicers fill gaps only whenreplay_endpointis set; without it, a gap costs the worker's index entries. The TokenSpeed servicer never replays, and SMG bounds what a gap costs instead (see Recovery after gaps).
Reference¶
- Policy implementation:
model_gateway/src/policies/cache_aware.rs - Event subscription manager:
model_gateway/src/worker/kv_event_monitor.rs(gap settling and resyncs:model_gateway/src/worker/kv_event_recovery.rs) - KV event proto:
crates/grpc_client/proto/common.proto(messagesSubscribeKvEventsRequest,KvEventBatch,KvSnapshotChunk,KvCacheEvent,KvBlocksStored,KvBlocksRemoved,KvCacheCleared) - Engine-neutral relay (per-rank subscriptions, gap replay, normalization):
grpc_servicer/smg_grpc_servicer/kv_relay.py; the Rust servicer's relay with history and state snapshots:crates/engine_servicer/src/kv_events.rs - SGLang servicer:
grpc_servicer/smg_grpc_servicer/sglang/servicer.py(SubscribeKvEvents) + per-rank sourcesgrpc_servicer/smg_grpc_servicer/sglang/kv_events.py - vLLM servicer:
grpc_servicer/smg_grpc_servicer/vllm/servicer.py(SubscribeKvEvents) + config resolvergrpc_servicer/smg_grpc_servicer/vllm/kv_events.py - TokenSpeed servicer:
grpc_servicer/smg_grpc_servicer/tokenspeed/servicer.py(SubscribeKvEvents, a single-rank bridge overgrpc_servicer/smg_grpc_servicer/kv_events.py) + config resolvergrpc_servicer/smg_grpc_servicer/tokenspeed/kv_events.py - SGLang upstream config:
python/sglang/srt/disaggregation/kv_events.py(classKVEventsConfig) - vLLM upstream config:
vllm/config/kv_events.py(classKVEventsConfig) - TokenSpeed upstream config:
python/tokenspeed/runtime/pd/kv_events.py(classKVEventsConfig)