feat: enable KV cache events on vLLM inference pods by default - #1890
Conversation
There was a problem hiding this comment.
Pull request overview
Enables vLLM KV-cache lifecycle event publishing by default and updates the Kubernetes-generated pod/service manifests to expose the KV-events ZMQ port (5557) so clients can subscribe to those events.
Changes:
- Enable
kv_events_config.enable_kv_cache_events=Truein the vLLM preset default args. - Add a new
PortKVCacheEvents = 5557constant and wire it into generated container/service ports. - Name the existing inference port as
httpand add a newkv-eventsport to the container/service definitions.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
presets/workspace/inference/vllm/inference_api.py |
Enables KV cache events by default via vLLM default args (kv_events_config). |
pkg/workspace/manifests/manifests.go |
Exposes kv-events (5557/TCP) on the generated Workspace Service. |
pkg/workspace/inference/preset_inferences.go |
Adds http and kv-events container ports to the inference container spec. |
pkg/utils/consts/consts.go |
Introduces PortKVCacheEvents constant (5557). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Failed to generate code suggestions for PR |
cc97e1f to
334ac60
Compare
|
This pull request has been automatically marked as stale because it has not had activity in the last 90 days. It will be closed in 14 days if no further activity occurs. Thank you for your contributions. |
334ac60 to
52a5ffc
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
pkg/workspace/manifests/manifests_test.go:333
- The comment suggests this test can be made flaky by other packages’ tests mutating
featuregates.FeatureGatesunder parallelgo testruns. Go runs each package’s tests in a separate process, so other packages can’t affect this package’s globals; this rationale is misleading. Reword the comment to reflect that you’re pinning global mutable feature-gate state for determinism within this test/process.
// Deterministically pin the vLLM feature gate for this test. Other packages'
// tests mutate featuregates.FeatureGates (sometimes flipping FeatureFlagVLLM
// to false or replacing the whole map) without restoring it, which would
// otherwise make kaitov1beta1.GetWorkspaceRuntimeName here flaky under
// parallel `go test` runs.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (1)
pkg/workspace/manifests/manifests_test.go:333
- The comment explains flakiness as coming from “other packages' tests” mutating
featuregates.FeatureGates.go testruns each package’s tests in its own process, so cross-package mutations can’t affect this test; if there’s flakiness risk, it’s from other tests in the same package/test binary mutating the global feature-gate map. Rewording avoids misleading future maintainers about how/why this pinning is needed.
// Deterministically pin the vLLM feature gate for this test. Other packages'
// tests mutate featuregates.FeatureGates (sometimes flipping FeatureFlagVLLM
// to false or replacing the whole map) without restoring it, which would
// otherwise make kaitov1beta1.GetWorkspaceRuntimeName here flaky under
// parallel `go test` runs.
Enable vLLM's KV cache events (enable_kv_cache_events) by default when starting vLLM inference workloads. This allows tracking KV cache block storage and removal events, which are published via ZMQ for external consumers to subscribe to. The KV events feature is part of vLLM's KVEventsConfig and publishes BlockStored, BlockRemoved, and AllBlocksCleared events through a ZMQ endpoint (default: tcp://*:5557). This is useful for observability and external cache management. Reference: https://docs.vllm.ai/en/stable/api/vllm/config/kv_events/
Add container port 5557 and corresponding service port to expose the vLLM KV cache events ZMQ endpoint in Kubernetes deployments. This port is required for external consumers to subscribe to KV cache events (BlockStored, BlockRemoved, AllBlocksCleared) published by vLLM. Changes: - Add PortKVCacheEvents (5557) constant in consts.go - Add kv-events container port in preset_inferences.go - Add kv-events service port in manifests.go (GenerateServiceManifest) - Add named ports (http, kv-events) to container port definitions
- Move --kv-events-config injection from the vLLM preset (Python) into buildVLLMInferenceCommand (Go) so operator can flip it without an image rebuild, and users can still override via --kaito-config-file. Using JSON via argv also avoids the argparse set_defaults issue where a nested dict would bypass the --kv-events-config type= conversion. - Only expose the kv-events port on ClusterIP Services; skip when the workspace opts into a LoadBalancer Service so the unauthenticated ZMQ stream is not accidentally published to the internet. - Add unit tests: default injection + user override in interface_test; ClusterIP vs LoadBalancer port exposure in manifests_test.
Copilot flagged that the kv-events port (5557) was being added to every inference pod and every ClusterIP Service, regardless of the runtime. Only vLLM produces the KV cache events ZMQ stream; other runtimes leave that port idle, so advertising it is misleading and clutters the Service. Gate both sites on the workspace runtime: - preset_inferences.go: split the kv-events container port into a separate variable and only append it to the container's Ports when GetWorkspaceRuntimeName == RuntimeNameVLLM. - manifests.go (GenerateServiceManifest): only add the kv-events Service port when the workspace runtime is vLLM (and the Service is ClusterIP, as before). - manifests_test.go: extend TestGenerateServiceManifest_KVEventsPort to also cover the non-vLLM runtime and assert kv-events is NOT exposed in that case.
Update the doc comment on the kv-events container port to reference the actual variable name (vllmKVEventsContainerPort) so it stays grep-friendly.
…heEvents Update the code comment in buildVLLMInferenceCommand to say "in-cluster subscribers" instead of "external consumers" (matching the actual Service exposure policy) and reference consts.PortKVCacheEvents instead of hardcoding tcp://*:5557.
…VEventsPort Address review feedback: this test calls kaitov1beta1.GetWorkspaceRuntimeName, which reads the global featuregates.FeatureGates map. Other packages' tests mutate that map without always restoring it, making this test flaky when go test runs packages in parallel. Set FeatureFlagVLLM=true up-front and restore it with a defer to keep the test deterministic.
The only in-cluster consumer of vLLM KV cache events today is the GAIE /
llm-d-inference-scheduler EPP that KAITO wires up through the InferenceSet
(a.k.a. MultiRoleInference) controller. When enableMultiRoleInferenceController
is off, nothing subscribes to port 5557, so:
- injecting --kv-events-config into the vLLM command line spawns an idle
ZMQ publisher thread + holds an open socket for no reason;
- advertising containerPort/kv-events and Service port/kv-events would
point at nothing.
Gate all three at the operator side on the feature flag:
- pkg/model/interface.go: only inject --kv-events-config when MRI is on.
- pkg/workspace/inference/preset_inferences.go: only add the kv-events
container port when the runtime is vLLM AND MRI is on.
- pkg/workspace/manifests/manifests.go: only expose the kv-events
Service port for vLLM/ClusterIP AND when MRI is on (LoadBalancer is
still always excluded to avoid publishing the unauth ZMQ stream).
Tests:
- Add TestGetInferenceCommandVLLMKVCacheEventsDisabledWithoutMRI to
prove the flag is omitted when MRI is off.
- Pin MRI=true in TestGetInferenceCommandVLLMKVCacheEventsDefault,
TestGeneratePresetInference (which asserts kv-events-config in the
expected vLLM params), and TestGenerateServiceManifest_KVEventsPort.
- Extend TestGenerateServiceManifest_KVEventsPort with a
'vLLM+ClusterIP but MRI=false' case that asserts the Service port
is NOT added.
Signed-off-by: andyzhangx <xiazhang@microsoft.com>
Extend the P/D-disaggregation MRI test so that after the standard
InferenceSet + GWIE + chat-completions checks, we also assert that the
vLLM KV cache events producer side is correctly wired up on every child
workspace (both prefill and decode roles).
Under the new operator-side gate this must hold whenever
enableMultiRoleInferenceController is on, which is exactly the MRI test
path. Regressing any of these would silently starve the llm-d EPP
KVCache scorer, so we lock them down in e2e:
1. StatefulSet pod spec declares containerPort/kv-events =
consts.PortKVCacheEvents on the main container.
2. StatefulSet pod command carries
--kv-events-config='{"enable_kv_cache_events":true}'.
3. Child Workspace ClusterIP Service exposes a Service port named
kv-events on consts.PortKVCacheEvents.
4. vLLM logs contain 'Starting ZMQ publisher thread' (proves the
process actually opened the socket, not just that the flag was
passed).
Runs from validateMultiRoleInferenceKVEvents, invoked right after
validateMultiRoleInferenceChatCompletions and before
validateMultiRoleInferencePDDisaggregation.
Signed-off-by: andyzhangx <xiazhang@microsoft.com>
Previously the KV cache events plumbing (--kv-events-config on the vLLM
command line, the kv-events container port, and the kv-events Service
port) was gated on FeatureFlagEnableMultiRoleInferenceController on the
theory that the only in-cluster consumer today is the GAIE / llm-d
Router EPP wired up by the InferenceSet / MultiRoleInference controller.
Turn that gate off and enable KV events by default for all vLLM
workspaces:
- The publisher-side cost is negligible (one background ZMQ thread
per pod + a listening TCP socket on 5557) even when nothing is
subscribed. The socket is unauthenticated but only reachable
in-cluster (ClusterIP), never on LoadBalancer Services.
- Consumers other than the KAITO-managed EPP (custom llm-d Router
deployments, external cache coordinators, KV-cache observers)
also want to subscribe without having to flip the MRI feature
gate first.
- Users who want to opt out can still override --kv-events-config
via VLLMParam.ModelRunParams / --kaito-config-file.
Changes:
- pkg/model/interface.go: always inject
--kv-events-config='{"enable_kv_cache_events":true}' unless the
user has already set kv-events-config themselves.
- pkg/workspace/inference/preset_inferences.go: always add the
kv-events container port for vLLM runtimes.
- pkg/workspace/manifests/manifests.go: always add the kv-events
Service port for vLLM + ClusterIP; LoadBalancer is still
excluded to keep the unauthenticated ZMQ stream off the internet.
- Drop the FeatureFlagEnableMultiRoleInferenceController checks and
the corresponding featuregates imports where they became unused.
Tests:
- Drop TestGetInferenceCommandVLLMKVCacheEventsDisabledWithoutMRI
(the MRI-off no-op case is no longer a valid expectation).
- Drop the MRI feature-gate toggling from
TestGetInferenceCommandVLLMKVCacheEventsDefault,
TestGeneratePresetInference, and
TestGenerateServiceManifest_KVEventsPort; assertions are unchanged.
- TestGenerateServiceManifest_KVEventsPort still pins FeatureFlagVLLM
for deterministic runtime resolution and still asserts the
non-vLLM and LoadBalancer negative cases.
Signed-off-by: andyzhangx <xiazhang@microsoft.com>
Description
Enable vLLM's KV cache events by default and expose the ZMQ endpoint port (5557) on in-cluster Services so KV-cache-aware consumers running in the same cluster (in particular the llm-d Router / EPP that KAITO's GAIE integration wires up, but also any custom llm-d Router deployment or external cache coordinator) can subscribe.
The publisher-side cost is negligible even when nothing is subscribed (one background ZMQ thread per pod + a listening TCP socket on 5557), the socket is only reachable in-cluster (
ClusterIPonly, never onLoadBalancer), and users can still opt out by overriding--kv-events-configvia--kaito-config-file.What this PR does
Enable KV cache events on vLLM — The operator injects
--kv-events-config='{"enable_kv_cache_events":true}'into the vLLM run command (inbuildVLLMInferenceCommand) when the workspace runtime is vLLM and the user hasn't already set--kv-events-configthemselves. This routes through vLLM's normal--kv-events-configCLI parser, so users can still override it via--kaito-config-file. Doing it operator-side means no image rebuild is needed to toggle the feature.Advertise the ZMQ port on the vLLM pod and Service — Add port 5557 (
kv-events) as a container port on vLLM inference pods and expose it as a Service port so in-cluster subscribers can connect.Don't publish the ZMQ port externally — The KV events ZMQ stream is unauthenticated / unencrypted. The Service port is only added when the Service type is
ClusterIP; if the workspace opts into a LoadBalancer Service (kaito.sh/enablelb: "True"), thekv-eventsport is intentionally not added. Users who genuinely need external access should create their own Service + NetworkPolicy.What events are actually published
vLLM emits a single
KVEventBatchmessage stream over ZMQ (defined invllm/distributed/kv_events.py). Each batch contains one or more of the following per-block lifecycle events:BlockStoredblock_hashes[],parent_block_hash,token_ids[],block_size,medium(GPU/CPU/STORAGE),lora_name,extra_keys[](multimodal ids,cache_salt, prompt embedding hashes — required for prefix-aware routing)BlockRemovedblock_hashes[],medium,group_idxAllBlocksClearedEvery batch carries a top-level
ts(timestamp) and, in data-parallel deployments, adata_parallel_rankso consumers can attribute events to the correct DP rank. Consumers see them asmsgspec-encoded, tagged union messages over ZMQ PUB/SUB (topic prefix optional). vLLM also opens an optional ROUTER replay endpoint so subscribers can request missed batches by starting sequence number — that endpoint is not exposed by this PR.What llm-d Router / EPP config is needed to actually consume them
Just wiring the vLLM producer up is not enough — the llm-d Router (formerly llm-d-inference-scheduler) EPP has to be configured with a plugin that subscribes to
tcp://<pod>:5557and turns those events into routing signals. Concretely, the EPP'sEndpointPickerConfigneeds to enable a KV-events-consuming producer and a prefix-cache scorer that references it. The canonical setup (from the upstream architecture doc and theprecise-prefix-cache-producerplugin README) looks like:Key points:
precise-prefix-cache-produceris the plugin that actually spawns a per-pod ZMQ SUB and ingestsBlockStored/BlockRemoved/AllBlocksCleared. ItsEndpointExtractorhook manages the subscriber lifecycle on pod add/delete against theInferencePool.approx-prefix-cache-producer(which is what the EPP auto-injects when nothing is configured) is heuristic-only and does not consume KV events. You have to explicitly configureprecise-prefix-cache-producer(or another KV-events-consuming producer) for the KV events KAITO is now emitting to be used.prefix-cache-scorermust setprefixMatchInfoProducerNameto the producer's name; without it the scorer falls back to the approx producer and the KV events are effectively ignored.TokenizedPrompton each request, which is provided upstream by the defaulttoken-producer— no extra config needed for that.--config-file <path>or--config-text '<inline yaml>'.EndpointPickerConfigAPI (apiVersion: llm-d.ai/v1alpha1) and theprecise-prefix-cache-producer/prefix-cache-scorerplugins are provided by the llm-d-inference-scheduler EPP, not upstreamgateway-api-inference-extension. Upstream GAIE only ships the lightweight EPP (lwepp) which does not consume KV events. If you're bringing your own EPP, make sure it's the llm-d one (or another KV-events-aware fork).extra_keys(includingcache_salt) in its KV events, so salted requests are routed precisely. SGLang currently does not emitextra_keys, so salted requests fall back to precise-cache misses until SGLang catches up.On the KAITO side, the InferenceSet-managed
InferencePooland the KAITO GAIE integration already point the Router at theInferencePoolfronting the vLLM workspace pods, so once the EPP config above is applied the subscribers connect to thekv-eventsport this PR is exposing. Consumers deployed independently of the KAITO MRI controller can connect the same way — the producer side is on for every vLLM workspace, not gated on any feature flag.Changes
pkg/utils/consts/consts.goPortKVCacheEvents = 5557constantpkg/model/interface.gobuildVLLMInferenceCommand, inject--kv-events-config='{"enable_kv_cache_events":true}'when not already set by the userpkg/workspace/inference/preset_inferences.gohttp; add a separatekv-eventscontainer port (5557) for vLLM workspacespkg/workspace/manifests/manifests.goGenerateServiceManifest, add thekv-eventsService port for vLLM workspaces when the Service type isClusterIP(LoadBalancer excluded to avoid publishing the unauth ZMQ stream)Unit tests added / updated:
pkg/model/interface_test.goTestGetInferenceCommandVLLMKVCacheEventsDefault— asserts default injection and that a user override wins.pkg/workspace/manifests/manifests_test.goTestGenerateServiceManifest_KVEventsPort— vLLM/ClusterIP exposes the port; vLLM/LoadBalancer does not; non-vLLM/ClusterIP does not.pkg/workspace/inference/preset_inferences_test.goTestGeneratePresetInference— assertsexpectedParams["kv-events-config"]in the generated vLLM command.Why (feature value)
KV cache events provide observability into vLLM's KV cache lifecycle (
BlockStored,BlockRemoved,AllBlocksCleared), which powers:precise-prefix-cache-producer+prefix-cache-scorer)The events are published via ZMQ as part of vLLM's KVEventsConfig. Only
enable_kv_cache_eventsneeds to be explicitly set — all other parameters (publisher,endpoint,hwm, etc.) use sensible defaults.Testing
go test ./pkg/model/... ./pkg/workspace/manifests/... ./pkg/workspace/inference/...passes locally--kv-events-configis present in the generated vLLM command for vLLM workspaces and absent for HuggingFace-transformers workspaceskv-eventsport for vLLM workspaces and does not expose it for LoadBalancer Services or non-vLLM runtimescontainerPort/kv-events=5557, Service haskv-eventsport, vLLM log showsStarting ZMQ publisher thread, live ZMQ SUB receivesBlockStored/BlockRemovedevents — see demo/llm/kv-events.md for the full walkthrough and consumer-side analysis (GAIE + llm-d).