Repository navigation
Tenant Isolation
Keep one customer's memory unreachable from another's, and see who is crowding whom.
| Status | ✅ Isolation works. ⚙ Capacity protection is configured, not automatic: see What is manual rather than automatic |
| Verified | cross tenant reads return 404, not 403, on a real server |
If two companies use the same server, neither may see the other's data. This keeps them apart: one customer cannot find, or even ask for, another's memory.
It also counts what each tenant is doing: how often they find something in memory, and how often their data gets pushed out to make room for someone else. So when one customer complains that things got slow, you can see whether another customer caused it.
⚙ Measuring and enforcing are two separate settings. The counters are on by default; the limits are not. Set a reservation for each tenant you owe capacity to, and the counters tell you which tenants those are.
Two banks share a server. Bank A cannot read Bank B's memory: a request for it simply finds nothing. If Bank A runs a huge job and pushes Bank B's data out, the counters show it, and a reservation set for Bank B in advance keeps the capacity it is owed.
How it works
- Every request says which customer (tenant) it belongs to.
- Galahad keeps each tenant's memory separate. A tenant can only find its own memory.
- Galahad counts, per tenant, what was found, what was missed and what was pushed out.
How to use it
- Choose the mode:
singlefor one organisation,multiwhen customers must be kept apart. - Pass the customer's
tenant_idwith every request. - Read the counters to see who uses the memory. Set a reservation if a customer needs guaranteed room.
cfg.tenancy = MERLIN_TENANCY_MULTI; /* separate trust domains */
merlin_init(&cfg);With vLLM or SGLang, set GALAHAD_TENANCY=multi.
Every call that reads or writes memory takes a tenant_id. Pass the ID of the
customer the request belongs to.
⚠ MERLIN_TENANCY_SINGLE is for one organisation and gives maximum reuse.
Use MULTI only when the tenants genuinely do not trust each other. It reduces
sharing, which is the point.
⚙ There is no default, and a server that does not state its mode refuses to start:
merlin_init(tenancy=UNSET) -> tenancy mode not configured
A wrong guess would not fail loudly: it would miss every lookup, or serve one tenant's memory to another.
⚠ Choose the mode once. A store written in one mode is refused in the other
mode (MERLIN_ERR_TENANCY_MISMATCH).
merlin_set_tenant_salt(tenant_id, salt); /* salt: a 64-bit value you choose and keep */This gives the tenant its own keyspace. Two tenants that send the same prompt then share nothing.
- It reduces reuse. A system prompt used by 50 salted tenants is stored 50 times instead of once. Use it where tenants are separate organisations, not for teams inside one company.
- Set it after
merlin_initand before the first request for that tenant. - Never change it for a tenant that already has data. Its saved memory would no longer be found. Keep the salt in your own configuration.
merlin_get_tenant_salt(tenant_id, &salt) tells you which salt is active
(0 = none).
size_t n = 0;
merlin_get_tenant_stats(NULL, 0, &n); /* how many tenants */
merlin_tenant_stats rows[64];
for (size_t i = 0; i < 64; i++) rows[i].struct_size = sizeof rows[i];
merlin_get_tenant_stats(rows, 64, &n); /* n can be larger than 64: then the list was cut */| Counter | Meaning |
|---|---|
tenant_id |
which tenant this row is about |
hits · misses
|
how well this tenant's memory is working |
evictions_caused |
this tenant pushed someone else's data out |
evictions_suffered |
this tenant's data was pushed out |
blocks_materialised |
how much they brought in |
⭐ caused - suffered is net pressure on other tenants. When a tenant
pushes out its own data, both counters go up, so the subtraction shows pressure
on others rather than total churn.
Reading the counters is cheap enough to do on a busy path.
merlin_reset_tenant_stats() sets them back to zero, for example to measure one
time window. Saved memory is not touched.
merlin_reserve_tenant_vram(tenant_id, bytes); /* guaranteed capacity */⚙ The default reservation is zero, which means capacity is shared freely until you say otherwise. A multi tenant deployment that owes anyone guaranteed capacity should set a reservation per tenant at startup.
- If all reservations together are larger than the memory Galahad may use, the
call returns
MERLIN_ERR_CAPACITY. -
bytes = 0removes the reservation.
⚙ Protection is something you configure. There is no automatic defence against a noisy neighbour: an operator who sets reservations is protected, one who does not is not.
| ✅ built in | tenant separation, per tenant counters, reservations, tenant erasure |
| ⚙ configure it yourself | set reservations in advance for tenants you owe capacity |
| ⚠ not available | per tenant memory use in bytes: merlin_tenant_stats has no bytes_resident field. Use blocks_materialised as a stand-in |
⚠ The counters do not change what gets evicted. They are measurement.
⭐ In one line: reservations protect capacity; the counters tell you who needs one.
size_t removed = 0;
merlin_forget_tenant(tenant_id, 1, &removed);This is the GDPR call. It removes the tenant's saved entries. With
erase_payloads = 1 it also deletes the tenant's saved files. With 0, the
files stay on disk.
With Encryption at Rest, you can also destroy the tenant's key in your key manager. The tenant's data can then no longer be read.
Through a real server, with the Memory Inspector:
| tenant 1 reads its own entry | 200 |
| tenant 0 asks for the same entry | ⭐ 404, not 403 |
| admin reads it | 200 |
every sub resource (/tensors, /lineage) |
404 cross tenant |
| a misspelled permission | startup failure, exit 2 |
⭐ Why 404 and not 403. A 403 would confirm the entry exists. The tenant check is made on the server, never taken from the request.
| Symptom | Cause | Fix |
|---|---|---|
tenancy mode not configured |
cfg.tenancy left at zero, or GALAHAD_TENANCY not set |
set SINGLE or MULTI
|
MERLIN_ERR_TENANCY_MISMATCH at startup |
the store was written in the other mode | use the original mode, or a new store directory |
| a tenant's lookups all miss | its salt changed | restore the original salt |
| one tenant starves another | no reservation set |
merlin_reserve_tenant_vram; there is no automatic defence |
evictions_caused high for one tenant |
they are crowding others | set reservations for the tenants you owe capacity |
| you want per tenant memory use in bytes |
merlin_tenant_stats has no bytes_resident field |
use blocks_materialised as a stand-in |
- Encryption at Rest: per tenant keys and erasure
- Memory Inspector
- Install · API Reference