subscriber

package
v0.0.0-...-1ec8fb3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

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

Constants

This section is empty.

Variables

This section is empty.

Functions

func ParseAdapterNames

func ParseAdapterNames(s string) (map[int64]string, error)

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.

const (
	CacheTierAuto CacheTier = "auto"
	CacheTierKV   CacheTier = "kv"  // vLLM 0.21+: vllm:kv_cache_usage_perc
	CacheTierGPU  CacheTier = "gpu" // legacy: vllm:gpu_cache_usage_perc
	CacheTierCPU  CacheTier = "cpu" // legacy: vllm:cpu_cache_usage_perc
)

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.

func (CacheTier) IsValid

func (t CacheTier) IsValid() bool

IsValid reports whether t is one of the documented tiers.

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

func (c Config) AdapterID(loraID *int64) (string, bool)

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.

func (Config) Validate

func (c Config) Validate() error

Validate checks the required identity fields are set.

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

type EvictedPrefix struct {
	PrefixHash []byte
	AdapterID  string
	TokenCount int32
}

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.

func (*Reporter) Run

func (r *Reporter) Run(ctx context.Context, in <-chan *EventBatch) error

Run consumes decoded event batches until ctx is cancelled or in is closed. On input close it drains the final buffered adds before returning.

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.

func (*StatsReporter) Run

func (r *StatsReporter) Run(ctx context.Context) error

Run ticks until ctx is cancelled, scraping and emitting on each tick. It returns ctx.Err() on shutdown so the caller can distinguish clean exit from a misconfiguration that triggers an early return.

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()).

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL