- KVCache 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.KVCache isolation with cache_salt
cache_salt is a per-request isolation key. It tells TMO which requests are allowed to
share cached KV and which ones must stay separate.
The behavior is simple:
- same model + same
cache_salt→ kv cache access is allowed - same model + different
cache_salt→ kv cache access is blocked
How to set it
cache_salt is a first-class field on OpenAI-compatible requests (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 TMO. 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
KVCache isolation keeps tenants from reading each other’s data. It does not stop one tenant from filling the cache capacity. For that, TMO 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.
Managing quotas at runtime
The engine exposes an HTTP cache-admin frontend onspec.httpPort (default 8080). Quotas are
created, inspected, and removed there:
_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.

