- Reuse isolation — control who is allowed to share cached prefixes. A prompt cached for one
tenant must not be reused by another. This is what
cache_saltprovides. - Capacity isolation — control how much cache each tenant may consume, so one noisy tenant cannot evict everyone else’s entries. This is what per-tenant quotas and the IsolatedLRU eviction policy provide.
Who configures what
Multi-tenancy spans three layers. Putting a setting in the wrong layer is the most common mistake.| Concern | Owner | Where it is set |
|---|---|---|
Reuse boundary per request (cache_salt) | API caller (vLLM / your app) | The request body sent to vLLM — not an operator field |
| Engine deployment, cache size, eviction config | Operator (TMO) | LMCacheEngine CRD / Helm values |
| Per-tenant storage budgets (quotas) | Runtime admin | LMCache HTTP cache-admin API on the engine (or coordinator) |
| Fleet-wide per-tenant usage tracking | Operator (TMO) | LMCacheEngine.spec.coordinator + an LMCacheCoordinator |
cache_salt.
There is no cache_salt field in any CRD or Helm value. The salt is attached to each inference
request by the caller.
Reuse isolation with cache_salt
cache_salt is a per-request isolation key. It tells LMCache which requests are allowed to
share cached KV and which ones must stay separate.
The behavior is simple:
- same model + same prompt prefix + same
cache_salt→ cache reuse is allowed - same model + same prompt prefix + different
cache_salt→ cache reuse is blocked - no
cache_salt→ the request uses the default unsalted cache domain
cache_salt is part of the cache key identity (the ObjectKey), alongside the chunk
hash, model name, and worker rank. Two requests whose content hashes identically but whose salts
differ resolve to distinct keys, so they never collide in L1 or L2. Unsalted traffic uses the
same on-disk key format as before salts existed, so introducing a salt for some tenants does not
invalidate existing unsalted entries.
How to set it
cache_salt is a first-class field on OpenAI-compatible requests to vLLM. Include it directly in
the request body.
How to choose a value
Pick a value that is:- stable across requests that should share cache
- different across requests that must stay isolated
- low-cardinality enough that it does not explode your metrics labels
tenant-acme, customer-123, prod, workspace-42.
Bad choices: a random UUID per request, a timestamp, or any value that changes on every call. If
the value changes on every request, you destroy reuse and effectively turn every request into a
cache miss.
Constraints
cache_salt is validated by LMCache. It:
- must not contain
@ - must not contain
/ - must not contain
\ - must not contain a NUL byte
- must be at most 128 characters
ValueError. Keep it simple: short ASCII
identifiers such as tenant-acme or prod are the safest choice.
The empty string (
"") is the default, unsalted domain. In the cache-admin HTTP API described
below, the empty salt cannot appear in a URL path, so it is represented by the reserved sentinel
_default (for example GET /quota/_default).Capacity isolation: per-tenant quotas
Reuse isolation keeps tenants from reading each other’s data. It does not stop one tenant from filling the cache. For that, LMCache supports per-tenant quotas backed by the IsolatedLRU eviction policy. How it works:- IsolatedLRU keeps a separate LRU ordering per
cache_saltinstead of one global list, so eviction victims for one tenant are chosen only from that tenant’s entries. - A quota assigns each
cache_salta storage budget in bytes. LMCache tracks usage per salt and evicts a salt’s oldest entries once it crosses its budget. - Quotas use allowlist semantics: a
cache_saltwith no registered quota has an effective limit of 0 bytes. Its stores are accepted on the hot path but evicted on the next eviction cycle (~1s). Only salts with an explicit quota retain cached data. Plan to register a quota for every tenant you want cached.
| Field | Default | Meaning |
|---|---|---|
eviction_policy | LRU | LRU, IsolatedLRU, or noop |
trigger_watermark | 0.8 | Per-salt usage fraction (of quota) that triggers eviction |
eviction_ratio | 0.2 | Fraction of over-quota bytes evicted per cycle |
Managing quotas at runtime
The engine exposes an HTTP cache-admin frontend onspec.httpPort (default 8080). Quotas are
created, inspected, and removed there:
| Method | Path | Body | Purpose |
|---|---|---|---|
PUT | /quota/{cache_salt} | {"limit_gb": <float>} | Create or update a tenant’s budget |
GET | /quota/{cache_salt} | — | Read a tenant’s limit and current usage |
DELETE | /quota/{cache_salt} | — | Remove a budget (entries evicted next cycle) |
GET | /quota | — | List every registered quota and its usage |
_default sentinel to manage the unsalted domain: PUT /quota/_default.
Fleet-wide quotas via the coordinator
The HTTP API above is per-engine. To track and bound a tenant’s usage across an entire fleet of engines, register the engines with anLMCacheCoordinator and enable event reporting on the
engine’s coordinator block:
PUT/DELETE /l2/quota/{cache_salt}, GET /l2/status[/{cache_salt}])
and its own sweep controls (evictionCheckInterval, default 5s; evictionRatio, default 0.2).
Deployment-level isolation
cache_salt and quotas isolate tenants within one shared cache. For harder isolation, give each
tenant its own engine:
- Per-namespace engines — run a separate
LMCacheEngineCR per tenant namespace. The chart’s multi-tenant mode (operator.enabled=true,engine.enabled=false) installs only the operator; each tenant then ships their own engine CR. Caches are not shared across separate engines at all. - Node placement —
spec.nodeSelector,spec.affinity, andspec.tolerationspin a tenant’s engine to dedicated GPU nodes. - RBAC / network —
spec.serviceAccountName,spec.podLabels, andspec.podAnnotationslet you attach per-tenant RBAC and NetworkPolicy.
cache_salt + quotas when you want the efficiency of one shared pool with logical separation.
Observability
LMCache tags several metrics withcache_salt, which is useful for per-tenant hit-rate analysis:
lmcache_mp.lookup_requested— tokens submitted for lookup (labels:model_name,cache_salt)lmcache_mp.lookup_hit— tokens served from cache (labels:model_name,cache_salt)lmcache_mp.real_reuse_gap— seconds between a chunk’s last write and next read (label:cache_salt)lmcache_mp.real_reuse_gap_objects— the same gap measured in chunks (label:cache_salt)
lookup_hit by lookup_requested, grouped by cache_salt, for a per-tenant hit rate.
Verification
cache_salt reuse behavior is straightforward to check by hand:
- Send a long prompt with
cache_salt: "tenant-a". - Send the same prompt again with
cache_salt: "tenant-a"→ reuse is allowed. - Send the same prompt with
cache_salt: "tenant-b"→ it must not reusetenant-a’s entries, even though the prompt is identical.
The current end-to-end test suite exercises basic cache hit/miss and L1/L2 storage, but does not
yet include automated coverage for
cache_salt isolation, per-tenant quotas, or IsolatedLRU.
Validate these flows in a staging deployment before relying on them in production.Recommended practice
- Use no
cache_saltif all traffic is inside one trust boundary. - Use one stable salt per tenant or environment when traffic must be isolated.
- Avoid per-request salts unless you explicitly want to disable sharing.
- If you need capacity fairness, enable IsolatedLRU and register a quota for every tenant you want cached (remember: unregistered salts get 0 bytes).
- For strict isolation, give each tenant its own engine per namespace instead of a shared pool.

