Documentation
¶
Overview ¶
Package subscriber implements KV-event ingestion for the kvevent-subscriber sidecar. It runs next to a vLLM engine replica, subscribes to the engine's KV cache events over ZMQ, decodes them, and reports cache state to the inferencecache-server over gRPC.
Two independent paths share one gRPC client:
- Event path: ZMQ → EventBatch → ReportCacheState (prefix adds) + PublishEvent (removals/clears), debounced on a short window.
- Stats path: HTTP GET against the engine's Prometheus /metrics → MetricsScraper → StatsReporter → ReportCacheState (stats-only CacheStateUpdate populating cacheMemoryBytes / hitRate / pressure on its own cadence, default ~10s).
Metadata only — never KV tensors or prompt text. Fail-soft on both paths: neither a ZMQ drop nor a scrape failure can stall the engine. The package is private to the kvevent-subscriber binary (cmd/kvevent-subscriber).
Index ¶
- func ParseAdapterNames(s string) (map[int64]string, error)
- type AllBlocksCleared
- type BlockRemoved
- type BlockStored
- type CacheTier
- type Config
- func (c Config) AdapterID(loraID *int64) (string, bool)
- func (c Config) ClearedEvent(tsSeconds float64) *icpb.CacheEvent
- func (c Config) EvictedEvent(prefixHash []byte, adapterID string, tsSeconds float64) *icpb.CacheEvent
- func (c Config) StatsUpdate(tsUs int64, stats *icpb.ReplicaStats) *icpb.CacheStateUpdate
- func (c Config) Update(tsUs int64, prefixes []*icpb.PrefixEntry) *icpb.CacheStateUpdate
- func (c Config) Validate() error
- type Event
- type EventBatch
- type EvictedPrefix
- type MetricsScraper
- type Reporter
- type ReporterOption
- type ScraperConfig
- type StatsReporter
- type StatsReporterOption
- type Subscriber
- type SubscriberOption
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func ParseAdapterNames ¶
ParseAdapterNames parses a comma-separated "id=name" list into the Config.AdapterNames map (e.g. "1=sql-lora,2=chat-lora"). Empty input yields a nil map — no mapping, so every non-nil lora_id is fail-closed (dropped, not cached) at ingest until it is mapped. Blank list entries are skipped so a trailing comma is tolerated; a malformed pair, a non-integer id, an empty name, or a duplicate id is an error, because silently dropping one would leave that adapter unmapped — uncached where the operator asked for caching.
Types ¶
type AllBlocksCleared ¶
type AllBlocksCleared struct{}
AllBlocksCleared reports that the engine flushed its entire KV cache.
type BlockRemoved ¶
type BlockRemoved struct {
BlockHashes [][]byte
}
BlockRemoved reports KV blocks that were evicted. BlockHashes are opaque bytes (see BlockStored).
type BlockStored ¶
type BlockStored struct {
BlockHashes [][]byte
ParentBlockHash []byte
TokenIDs []uint32
BlockSize int32
LoRAID *int64
}
BlockStored reports KV blocks that became resident. BlockSize is the number of tokens per block; a block covers BlockSize tokens of the prefix.
BlockHashes are the engine's opaque per-block hashes (int variants normalized to 8-byte big-endian). They serve only as a stable per-block identity for the reverse map; the index key itself is our own content fingerprint derived from TokenIDs (see positional.go).
ParentBlockHash is the engine hash of the block preceding this event's first block — nil for a sequence root or when absent — normalized like BlockHashes. It lets the subscriber chain its rolling prefix hash across events.
TokenIDs are the flat token IDs of this event's blocks (BlockSize tokens per block, in block order). They are hashed in-pod to derive the fingerprint and never leave the pod.
LoRAID is the engine's LoRA adapter id for these blocks — nil when the event carries no adapter (msgpack nil, a truncated 5-field tuple, or a base-model request). It is the engine's INTERNAL integer id, assigned in adapter load order, so Config.AdapterID maps it to the stable adapter identity that becomes the index partition; it is never mixed into the content fingerprint.
type CacheTier ¶
type CacheTier string
CacheTier selects which vLLM cache-usage gauge feeds cache_memory_bytes. vLLM 0.21+ exposes a single unified `vllm:kv_cache_usage_perc`; older vLLM exposed `vllm:gpu_cache_usage_perc` and (on some builds) `vllm:cpu_cache_usage_perc`. "auto" probes that fallback chain — kv → gpu → cpu — so the scraper degrades across vLLM releases without operator action.
func ValidCacheTierNames ¶
func ValidCacheTierNames() []CacheTier
ValidCacheTierNames returns the accepted --cache-tier values in fallback order. Returns a fresh slice each call so callers cannot mutate the canonical set.
type Config ¶
type Config struct {
// ReplicaID identifies the engine replica these events come from. Required;
// it is the index key the server attributes prefixes/stats to.
ReplicaID string
// ModelID is the served model identifier. Required.
ModelID string
// TenantID is the tenant namespace. Optional (empty = shared/default).
TenantID string
// HashScheme names the engine's prefix-hash domain (e.g. "vllm"). Required
// and non-empty: an empty scheme fails open server-side (dropped on ingest),
// so reporting with one would silently lose the data.
HashScheme string
// AdapterNames maps the engine's INTERNAL LoRA id (BlockStored.lora_id) to
// the stable adapter identity used as the index partition — the same string
// a gateway puts in LookupRouteRequest.adapter_id.
//
// The mapping is needed because the engine's id is a load-order integer, not
// a name: vLLM assigns lora_int_id from the ordered --lora-modules list (and
// increments it for adapters loaded at runtime), so the SAME integer can mean
// different adapters on two replicas whose load order differs. The index is
// shared across replicas, so partitioning on the raw integer would re-create
// the aliasing this partition exists to prevent — across replicas instead of
// within one.
//
// Only a NIL lora_id (base model / no LoRA) uses the default ("") partition,
// and that path needs no configuration. A non-nil lora_id with NO mapping is
// FAIL-CLOSED at ingest — AdapterID reports ok=false and the subscriber drops
// the event rather than partitioning it under a replica-local "lora:<id>",
// which could place different adapters in one partition across replicas whose
// --lora-modules order differs (the very cross-replica alias this partition
// exists to prevent).
//
// So LoRA caching REQUIRES this map: supply it from the same --lora-modules
// ordering the engine gets, and send the matching adapter_id from the gateway
// (whatever identity is ingested here MUST match the gateway's query id — the
// same producer/consumer agreement HashScheme requires). An unmapped adapter
// is neither aliased nor cached — its prefixes get no hint (a miss, never a
// wrong replica) until it is mapped. Nil/empty AdapterNames is correct only
// for base-model / non-LoRA traffic.
AdapterNames map[int64]string
}
Config is the per-engine-replica identity the subscriber stamps onto every report. vLLM's KV-cache events carry none of this, so the subscriber must be told which replica/model/tenant it watches and which hash scheme the engine uses (so the server keeps different engines' hashes in disjoint domains).
func (Config) AdapterID ¶
AdapterID resolves an engine LoRA id to the stable adapter identity that partitions the index, and reports whether that id is safe to ingest:
- a nil id (base model / no adapter) → ("", true): the default partition, the behavior for every non-LoRA deployment;
- a mapped id → (name, true): its configured stable identity;
- an unmapped non-nil id → ("", false): FAIL CLOSED. The engine's id is a replica-local load-order integer with no globally stable meaning, so indexing it under "lora:<id>" could place different adapters in the same partition on replicas whose --lora-modules order differs — the very cross-replica alias this partition exists to prevent. The caller drops the ingest; the adapter is cached only once --lora-adapter-names maps its id.
func (Config) ClearedEvent ¶
func (c Config) ClearedEvent(tsSeconds float64) *icpb.CacheEvent
ClearedEvent builds the ALL_CLEARED CacheEvent for an AllBlocksCleared event.
func (Config) EvictedEvent ¶
func (c Config) EvictedEvent(prefixHash []byte, adapterID string, tsSeconds float64) *icpb.CacheEvent
EvictedEvent builds one PREFIX_EVICTED CacheEvent for an already-derived prefix hash (our content fingerprint, the index key to drop) in a specific adapter partition. The subscriber maps an evicted engine block hash to both via positionalIndex.Removed. An empty adapterID is the default partition; the server then falls back to its cross-partition (legacy) removal.
func (Config) StatsUpdate ¶
func (c Config) StatsUpdate(tsUs int64, stats *icpb.ReplicaStats) *icpb.CacheStateUpdate
StatsUpdate stamps the replica/model/tenant/hash_scheme identity onto a scraped ReplicaStats and produces a stats-only CacheStateUpdate (empty prefixes). The contract treats a CSU as an additive delta, so a stats-only update refreshes liveness + the per-replica stats without touching prefixes. Returns nil if stats is nil.
The nested stats are rebuilt by-field rather than copied — proto messages embed a sync.Mutex via MessageState, so go vet rejects value copies.
func (Config) Update ¶
func (c Config) Update(tsUs int64, prefixes []*icpb.PrefixEntry) *icpb.CacheStateUpdate
Update stamps the replica/model/tenant/hash_scheme identity onto a set of prefixes. Returns nil for an empty prefix set (nothing to report).
The update-level adapter_id is deliberately left empty: one replica can hold KV for several adapters at once and the Reporter batches events across them into a single update, so adapter identity is stamped PER ENTRY in positionalIndex.Stored. The server treats the update-level field only as a default for entries that set none, so leaving it empty is correct here.
type Event ¶
type Event interface {
// contains filtered or unexported methods
}
Event is one decoded KV-cache event. The concrete types are BlockStored, BlockRemoved, and AllBlocksCleared.
type EventBatch ¶
type EventBatch struct {
// TimestampSeconds is the engine's batch timestamp (Unix seconds, float).
TimestampSeconds float64
Events []Event
}
EventBatch is one decoded vLLM event batch.
func DecodeEventBatch ¶
func DecodeEventBatch(payload []byte) (*EventBatch, error)
DecodeEventBatch decodes one msgpack EventBatch payload (the last ZMQ frame). Unknown event tags are skipped (forward-compatible); a malformed batch is an error so the caller can drop it without corrupting state.
type EvictedPrefix ¶
EvictedPrefix is one index key the engine evicted, resolved back to the entry we derived for it: our content-fingerprint prefix hash, the adapter partition it lives in, and its cumulative token count. The adapter scopes the eviction to the right partition (the fingerprint is token-only, so one prefix hash can be live under several adapters at once); the token count lets the caller re-report the prefix at a colder tier (T2) when a paired L2 store still holds it, instead of only being able to delete it.
type MetricsScraper ¶
type MetricsScraper struct {
// contains filtered or unexported fields
}
MetricsScraper polls an engine's Prometheus /metrics endpoint and projects the payload into a ReplicaStats, using the per-engine metricProfile selected by ScraperConfig.Scheme. It is fail-soft: any HTTP/parse error returns a zero ReplicaStats + the error so the caller logs and retries on the next tick.
For vLLM, hit rate is a sliding signal — the per-scrape delta of (prefix_cache_hits_total / prefix_cache_queries_total) — so the very first scrape returns hit_rate=0 (no delta available) and the previous values are cached on the scraper for the next tick. A counter reset (engine restart) resets the delta state too. SGLang reads a direct gauge instead (see hitRate).
func NewMetricsScraper ¶
func NewMetricsScraper(httpClient *http.Client, cfg ScraperConfig, logger *slog.Logger) *MetricsScraper
NewMetricsScraper builds a scraper. If httpClient is nil, a fresh *http.Client with the configured Timeout is used.
func (*MetricsScraper) Scrape ¶
func (s *MetricsScraper) Scrape(ctx context.Context) (*icpb.ReplicaStats, error)
Scrape performs one GET against the engine /metrics endpoint and returns the derived ReplicaStats. On error a zero-valued *icpb.ReplicaStats is returned alongside the error — the caller logs and skips the tick.
A 200 OK whose body contains NONE of the recognized engine metric families is treated as an error, not a healthy all-zero sample: it means a metric-name / port / engine mismatch (e.g. an SGLang endpoint exposing sglang:* names, or a wrong --engine-metrics-url), and emitting fabricated zeros would be marked delivered and silently disable load-aware routing. Likewise, a scrape that recognizes the engine but is missing either load gauge — including a model_name filter that excludes them — fails loud rather than report a fabricated pressure=0. Cache-side metrics may still be absent and report 0: usage (cache_memory_bytes) is not consumed by the ranker, and an absent hit_rate reports 0, which conservatively disables the replica's TENANT_HOT fallback (a safe, hint-only degradation — see hitRate) rather than fail the whole scrape and also drop the load signal.
type Reporter ¶
type Reporter struct {
// contains filtered or unexported fields
}
Reporter forwards decoded KV-cache events to the policy server over gRPC.
Adds (BlockStored, tagged tier T1) are accumulated and flushed on a short window — this debounces high engine event rates. Each flush sends one CacheStateUpdate on a fresh, time-bounded ReportCacheState stream. A BlockRemoved is handled one of two ways depending on whether the engine has an L2 offload tier (WithIgnoreBlockRemoved): with one, the block moved tiers, so it is re-reported at T2 through the same ReportCacheState add path; without one it is gone, forwarded as PREFIX_EVICTED via a time-bounded unary PublishEvent. AllBlocksCleared → ALL_CLEARED, also via PublishEvent. Adds, T2 downgrades, and evictions all carry the entry's resolved adapter partition (see positionalIndex).
Every RPC uses its own bounded context, so a stalled or unreachable server can never block the loop for longer than rpcTimeout — the cache is an optimization and must never stall the engine. Errors are logged and dropped (soft state); Run only returns on context cancellation or input close.
func NewReporter ¶
func NewReporter(client icpb.InferenceCacheClient, cfg Config, opts ...ReporterOption) *Reporter
NewReporter builds a Reporter for one engine replica.
type ReporterOption ¶
type ReporterOption func(*Reporter)
ReporterOption configures a Reporter.
func WithIgnoreBlockRemoved ¶
func WithIgnoreBlockRemoved(b bool) ReporterOption
WithIgnoreBlockRemoved declares that this replica's engine is paired with an L2 offload tier (e.g. LMCache) that retains a block after the engine evicts it from HBM. vLLM emits BlockRemoved on every HBM eviction even when the block is still resident at L2, so a plain PREFIX_EVICTED would drop a routing hint the replica can still cheaply serve from the offload tier. With this set, a BlockRemoved is instead re-reported as a T2 entry (reload-able from L2) rather than deleted — the hint stays, but honestly tagged colder than HBM, and ages out on the freshness TTL like any other entry. Without it (single-tier), a BlockRemoved forwards PREFIX_EVICTED (the block is gone). A wrong T2 tag is soft state (a cache miss at worst, never a wrong answer); a missing hint routes the request away from its warm replica and wastes the L2 hit — the opposite risk, hence the split.
The name (and the operator flag --ignore-block-removed) predates the T2 downgrade — earlier this path simply suppressed the eviction and left the entry stale at T1. It is kept for backward compatibility; the signal it carries ("this replica has an L2 tier") is unchanged.
func WithLogger ¶
func WithLogger(l *slog.Logger) ReporterOption
WithLogger sets the logger (default slog.Default()).
func WithRPCTimeout ¶
func WithRPCTimeout(d time.Duration) ReporterOption
WithRPCTimeout bounds each gRPC call/flush (default 5s).
func WithWindow ¶
func WithWindow(d time.Duration) ReporterOption
WithWindow sets the add-batching/debounce flush window (default 100ms).
type ScraperConfig ¶
type ScraperConfig struct {
// URL is the engine's Prometheus /metrics endpoint.
URL string
// Scheme selects the per-engine metric profile ("vllm" | "sglang"). Empty
// defaults to vLLM. It MUST match the engine the URL points at: a mismatch
// makes every metric family unrecognized and Scrape fails loud rather than
// fabricate zeros. Wired from the subscriber's --hash-scheme.
Scheme string
// Tier selects which cache-usage gauge feeds cache_memory_bytes.
// Defaults to CacheTierAuto.
Tier CacheTier
// ModelLabel filters every series by the `model_name` Prometheus label
// (the one vLLM stamps on its metrics). When non-empty, only series whose
// label matches participate in the scrape — so a /metrics endpoint that
// happens to expose multiple models cannot attribute another model's
// usage/load/hit-rate to the replica this scraper is configured for. When
// empty, every series is included (the legacy aggregate behaviour).
ModelLabel string
// CacheSizeBytes is the engine's total KV-cache capacity, used to map a
// usage_perc gauge (0..1) to bytes. When zero, cache_memory_bytes is
// emitted as 0 — the ranker doesn't consume this field, so an honest
// "unknown" is preferred over a fabricated number.
CacheSizeBytes int64
// MaxConcurrencyCeiling is the denominator for the pressure proxy:
// pressure = clamp01((num_requests_running + num_requests_waiting) / ceiling).
// 0 disables pressure (it stays 0).
MaxConcurrencyCeiling int
// Timeout bounds each scrape; defaults to defaultScrapeTimeout when <= 0.
Timeout time.Duration
}
ScraperConfig tunes the metrics scraper. URL is required.
type StatsReporter ¶
type StatsReporter struct {
// contains filtered or unexported fields
}
StatsReporter periodically scrapes engine /metrics and emits a stats-only CacheStateUpdate via ReportCacheState. It runs alongside the event Reporter (different cadence, different data source) and shares the same gRPC client.
Failure independence is load-bearing: a scrape failure (engine /metrics down, HTTP timeout, parse error) logs and skips the tick — it never blocks the event path or kills the subscriber. The two paths are independent failure domains.
func NewStatsReporter ¶
func NewStatsReporter(client icpb.InferenceCacheClient, scraper statsScraper, cfg Config, opts ...StatsReporterOption) *StatsReporter
NewStatsReporter builds a StatsReporter that ticks against scraper and publishes onto client.
type StatsReporterOption ¶
type StatsReporterOption func(*StatsReporter)
StatsReporterOption configures a StatsReporter.
func WithStatsInterval ¶
func WithStatsInterval(d time.Duration) StatsReporterOption
WithStatsInterval sets the scrape tick interval (default 10s).
func WithStatsLogger ¶
func WithStatsLogger(l *slog.Logger) StatsReporterOption
WithStatsLogger sets the logger (default slog.Default()).
func WithStatsRPCTimeout ¶
func WithStatsRPCTimeout(d time.Duration) StatsReporterOption
WithStatsRPCTimeout bounds each ReportCacheState call (default 5s).
type Subscriber ¶
type Subscriber struct {
// contains filtered or unexported fields
}
Subscriber reads a vLLM KV-cache event stream from a ZMQ PUB endpoint, decodes each batch, and emits it. It reconnects with backoff and never returns except on context cancellation (fail-soft — the engine is unaffected by our outages).
func NewSubscriber ¶
func NewSubscriber(endpoint, topic string, opts ...SubscriberOption) *Subscriber
NewSubscriber builds a Subscriber for one engine's ZMQ event endpoint (e.g. "tcp://127.0.0.1:5557") and topic (e.g. "kv-events"; "" = all topics).
func (*Subscriber) Run ¶
func (s *Subscriber) Run(ctx context.Context, out chan<- *EventBatch) error
Run connects, decodes batches, and sends them on out until ctx is cancelled. out is not closed (the caller owns it).
type SubscriberOption ¶
type SubscriberOption func(*Subscriber)
SubscriberOption configures a Subscriber.
func WithSubscriberBackoff ¶
func WithSubscriberBackoff(d time.Duration) SubscriberOption
WithSubscriberBackoff sets the reconnect backoff (default 1s).
func WithSubscriberLogger ¶
func WithSubscriberLogger(l *slog.Logger) SubscriberOption
WithSubscriberLogger sets the logger (default slog.Default()).