Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions docs/concepts/cachetenant-identity-and-quota.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ These are the only spec fields — do not expect others.
| `isolationMode` | enum (`Fairness`) | `Fairness` | Only value implemented today. |
| `crypto` | object | empty | Reserved for future cryptographic isolation; carries no fields today. |

The quota unit is the **distinct key** `(tenant, model, hash_scheme, prefix_hash)`.
The quota unit is the **distinct key** `(tenant, model, hash_scheme, adapter, prefix_hash)`.
A prefix held warm on several replicas counts **once**, not once per replica.
Because `adapter` is part of the key (`""` for base-model / non-LoRA traffic),
the *same* prompt content served under N LoRA adapters counts as N distinct keys
— serving many adapters multiplies quota consumption accordingly.

## Status and printer columns

Expand All @@ -43,7 +46,7 @@ Columns: **Tenant** (`spec.tenantID`), **Entries** (`status.indexEntries`),

## Identity isolation is structural

The index keys every entry by `(tenant, model, hash_scheme, prefix_hash)`, so
The index keys every entry by `(tenant, model, hash_scheme, adapter, prefix_hash)`, so
tenant A's hint records can never collide with tenant B's, and `LookupRoute` is
tenant-scoped: a lookup carrying `tenant_id = A` only ever sees A's prefixes.
This isolation is a property of **our** data structure, not of the engine — it
Expand Down
2 changes: 1 addition & 1 deletion docs/design/cachebackend-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ Deliberately **not** injected for SGLang (a real engine difference, not an omiss

The two override surfaces are **separate, and neither reaches the other**: the MP tunables above are read from `spec.backendConfig` and shape the **worker** sidecar, while `spec.integration.engineOverrides` edits the **engine** container's args/env only. So `engineOverrides` cannot retune `l1SizeGB` or `mpPort` (set them in `backendConfig`), and it cannot reach the worker's command line at all; what it *can* do is add or suppress engine args/env outside the reserved set above.

**KV-event source & `hash_scheme: "sglang"`.** SGLang adopted vLLM's KV-event wire wholesale — `--kv-events-config` drives a ZMQ `ZmqEventPublisher` emitting the same msgspec array-like `BlockStored` / `BlockRemoved` / `AllBlocksCleared` tuples (`BlockStored` carries `token_ids`, so the subscriber derives the same in-pod content fingerprint it does for vLLM). The shipped `kvevent-subscriber` binary therefore decodes SGLang's stream **unchanged**; the only difference is the adapter pins the sidecar's `--hash-scheme=sglang`. The index keys on `(tenant, model, hash_scheme, prefix_hash)`, so SGLang prefixes occupy a **domain disjoint** from vLLM's: a request hashed under one scheme never false-hits a bytewise-identical entry recorded under the other, even when both engines tokenize the same text to the same token ids and the content fingerprints collide. As with vLLM, the operator must launch the engine with `--kv-events-config '{"publisher":"zmq","endpoint":"tcp://*:5557","topic":"kv-events"}'` for the publisher to be active — the adapter wires the cache offload, not the event publisher.
**KV-event source & `hash_scheme: "sglang"`.** SGLang adopted vLLM's KV-event wire wholesale — `--kv-events-config` drives a ZMQ `ZmqEventPublisher` emitting the same msgspec array-like `BlockStored` / `BlockRemoved` / `AllBlocksCleared` tuples (`BlockStored` carries `token_ids`, so the subscriber derives the same in-pod content fingerprint it does for vLLM). The shipped `kvevent-subscriber` binary therefore decodes SGLang's stream **unchanged**; the only difference is the adapter pins the sidecar's `--hash-scheme=sglang`. The index keys on `(tenant, model, hash_scheme, adapter, prefix_hash)`, so SGLang prefixes occupy a **domain disjoint** from vLLM's: a request hashed under one scheme never false-hits a bytewise-identical entry recorded under the other, even when both engines tokenize the same text to the same token ids and the content fingerprints collide. As with vLLM, the operator must launch the engine with `--kv-events-config '{"publisher":"zmq","endpoint":"tcp://*:5557","topic":"kv-events"}'` for the publisher to be active — the adapter wires the cache offload, not the event publisher.

> **Block-size alignment for server-derived lookups (operational note).** The subscriber-ingested path is block-size-safe by construction: the subscriber derives each prefix fingerprint in-pod using the `block_size` carried on the engine's own `BlockStored` event (SGLang's `--page-size`, often 64), so SGLang entries land in the index at SGLang's block size with no server involvement. The **server-derived** lookup path is the catch: when a gateway calls `LookupRoute` with raw `token_ids` / `prompt_text` (rather than a pre-computed `prefix_hash` / `block_hashes` chain), the server fingerprints them with its single global `--engine-block-size` (default 16, vLLM's). For SGLang those server-side hashes only line up with SGLang-ingested entries when `--engine-block-size` is set to SGLang's page size. Because the flag is **one global value**, a single server cannot serve raw-`token_ids` lookups for both a vLLM (16) and an SGLang (64) deployment at once — in a mixed-engine cluster, have gateways send the pre-computed `prefix_hash` / `block_hashes` (the subscriber/fingerprint path, which is block-size-correct per engine) for the raw-token path, or run a server per block size. Making `--engine-block-size` per-`hash_scheme` is a server change tracked as a follow-up; it is the first concrete latently-vLLM-centric assumption this second engine surfaced.

Expand Down
14 changes: 9 additions & 5 deletions docs/design/lookuproute-ranking.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,15 @@ response is ever an error on the hot path. Fail open.

## 2. The baseline strategy — `PREFIX_MATCH`

The index keys cached prefixes by `(tenant, model, hash_scheme, prefix_hash)`
The index keys cached prefixes by `(tenant, model, hash_scheme, adapter, prefix_hash)`
→ the set of replicas that hold it, with per-replica `token_count` and a
`last_seen` timestamp. A baseline lookup:
`last_seen` timestamp. `adapter` is the resolved LoRA identity (`""` for
base-model / non-LoRA traffic); it partitions the content, never the fingerprint,
so identical tokens under different adapters share a hash but not an entry. The
`(tenant, model, hash_scheme)` *scope* used by the fallbacks below deliberately
stays adapter-blind. A baseline lookup:

1. Look up the request's `(tenant, model, hash_scheme, prefix_hash)`.
1. Look up the request's `(tenant, model, hash_scheme, adapter, prefix_hash)`.
2. For each replica that holds it, compute

```
Expand Down Expand Up @@ -71,7 +75,7 @@ collapses back to `matched_tokens × freshness`.

### Why the exact-full-hash path leaves hits on the table

§2 looks up the request's `(tenant, model, hash_scheme, prefix_hash)` as
§2 looks up the request's `(tenant, model, hash_scheme, adapter, prefix_hash)` as
**one opaque blob**. Two requests that share the first N KV blocks of a
prefix but diverge after — common with a shared system prompt plus
per-request RAG context — hash to different `prefix_hash` values and miss
Expand Down Expand Up @@ -116,7 +120,7 @@ slip into a non-additive change.
### How the lookup walks the chain

For each block `block_hashes[i]` the request carries, the index already
holds a `(tenant, model, hash_scheme, block_hash)` → `{replicaID →
holds a `(tenant, model, hash_scheme, adapter, block_hash)` → `{replicaID →
{tokenCount, lastSeen}}` entry — populated either from a single chain
`PrefixEntry` (the index *expands* the chain into N per-block entries)
or from N legacy single-blob `PrefixEntry` reports (one per block, the
Expand Down
12 changes: 9 additions & 3 deletions docs/operations/index-sizing.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
# Index sizing & memory tuning

The inferencecache-server holds a soft-state cache-state index in process memory:
distinct prefix entries per `(tenant, model, hash_scheme, prefix_hash)`, replica
distinct prefix entries per `(tenant, model, hash_scheme, adapter, prefix_hash)`, replica
stats per `(tenant, model, replica_id)`, plus a few secondary indices. This guide
gives operators the numbers and dials to size the server pod and the per-namespace /
per-tenant knobs that bound the index footprint.

> **LoRA note.** `adapter` (`""` for base-model / non-LoRA traffic) is part of the
> prefix key, so identical prompt content served under N adapters is N distinct
> entries. A multi-adapter workload multiplies key cardinality — and therefore
> memory and eviction pressure — by roughly the number of actively-cached
> adapters; size `maxIndexEntries` and pod memory for that fan-out.

**Audience.** Cluster operators picking pod resource limits, `CachePolicy.spec.evictionTTL`,
and `CacheTenant.spec.quota.maxIndexEntries` for their workload.

Expand Down Expand Up @@ -54,7 +60,7 @@ so reaching for a larger footprint needs a recompile today.
**Two units that get conflated.** The CRD field name `maxIndexEntries`, the snapshot
field `tenants[].indexEntries`, and the metric `inferencecache_index_entries` (with a
`model` label, e.g. `inferencecache_index_entries{model="meta-llama/Llama-3"}`) all
count **distinct prefix keys** — one per `(tenant, model, hash_scheme, prefix_hash)`,
count **distinct prefix keys** — one per `(tenant, model, hash_scheme, adapter, prefix_hash)`,
regardless of how many replicas hold it. The internal `pkg/index.DefaultMaxEntries` cap,
by contrast, counts **total storage entries** — one per `(prefix_key, replica)` tuple.
A single prefix held by R replicas is 1 "indexEntries" but R "storage entries". When
Expand Down Expand Up @@ -86,7 +92,7 @@ table — the rule of thumb is for back-of-envelope work, not a tight predictor.
A single entry is a chain of small Go allocations under two nested `map`s:

- outer `map[prefixKey]…` bucket header + the prefix-key strings (tenant, model,
hash_scheme, prefix_hash);
hash_scheme, adapter, prefix_hash);
- inner `map[replicaID]*replicaEntry` header + the replica-id string;
- the `replicaEntry` struct (token count + lastSeen `time.Time` + `atomic.Int64`
LFU counter).
Expand Down
2 changes: 1 addition & 1 deletion docs/reference/metrics.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ silently.
|---|---|---|---|
| `inferencecache_server_up` | *(none)* | `1` if the cache policy server is serving requests, `0` otherwise. | Server starts (→`1`) / shuts down (→`0`). Liveness signal. |
| `inferencecache_server_grpc_tls_enabled` | *(none)* | `1` if the gRPC server (`:9090`) is terminating TLS, `0` if serving plaintext. | Set once at startup from `--tls-cert-file`/`--tls-key-file` (both set → `1`, both empty → `0`). Confirms the prod wire posture from Prometheus. See `docs/design/grpc-tls.md`. |
| `inferencecache_index_entries` | `model` | **Distinct prefix entries** the in-memory `CacheIndex` currently holds for that model, **excluding reserved-tenant (`inferencecache.io/probe`) entries**. One entry = one unique `(tenant, model, hash_scheme, prefix_hash)` tuple, regardless of how many replicas hold it. | Rises on new `(scheme, hash)` from `ReportCacheState`; falls on `AllBlocksCleared` / TTL eviction / max-entries cap. A `BlockRemoved` only drops the entry in **single-tier (non-L2) mode**, where the subscriber forwards it as `PREFIX_EVICTED`; in **L2/Offload mode** a `BlockRemoved` **retains** the entry — the subscriber re-reports it at tier T2 rather than deleting it, so the count holds (the entry ages out only via TTL). Idempotent re-reports and T1↔T2 tier changes on an existing `(scheme, hash)` do **not** move it. The probe's synthetic state IS in the index during a Run but is excluded from this gauge so a scrape that races Stage C cannot transiently surface a probe-tenant count on a real model bucket — see `WithReservedTenants` in `pkg/index/index.go`. |
| `inferencecache_index_entries` | `model` | **Distinct prefix entries** the in-memory `CacheIndex` currently holds for that model, **excluding reserved-tenant (`inferencecache.io/probe`) entries**. One entry = one unique `(tenant, model, hash_scheme, adapter, prefix_hash)` tuple, regardless of how many replicas hold it. | Rises on new `(scheme, hash)` from `ReportCacheState`; falls on `AllBlocksCleared` / TTL eviction / max-entries cap. A `BlockRemoved` only drops the entry in **single-tier (non-L2) mode**, where the subscriber forwards it as `PREFIX_EVICTED`; in **L2/Offload mode** a `BlockRemoved` **retains** the entry — the subscriber re-reports it at tier T2 rather than deleting it, so the count holds (the entry ages out only via TTL). Idempotent re-reports and T1↔T2 tier changes on an existing `(scheme, hash)` do **not** move it. The probe's synthetic state IS in the index during a Run but is excluded from this gauge so a scrape that races Stage C cannot transiently surface a probe-tenant count on a real model bucket — see `WithReservedTenants` in `pkg/index/index.go`. |

### Counters

Expand Down
Loading
Loading