5.6 KiB
KV Cache SDK
Status: out-of-process, CPU-only client for the LMCache multiprocess (MP) server.
Goal
A small Python surface for moving KV cache tensors in and out of a running LMCache MP server, addressed by token ids — so an ML engineer can retrieve a prefix's KV, edit it, and store it back (e.g. token-dropping inside a KV connector).
import lmcache.sdk.kvcache as lmc_sdk
ctx = lmc_sdk.connect(
url="tcp://localhost:5555", # ZMQ url
http_url="http://localhost:9000", # HTTP url for retrieving KV cache shape
model_name="Qwen/Qwen3-8B",
)
kv = lmc_sdk.retrieve(ctx, tokens=[1, 2, 3, ...]) # [2, L, hit_tokens, D] or None
# ... edit kv ...
ok = lmc_sdk.store(ctx, kv=kv, tokens=[4, 5, 6, ...])
lmc_sdk.close(ctx)
See the token-dropping example.
The model layout must already be registered in the server by a vLLM instance that called
REGISTER_KV_CACHE; the SDK reads that layout from /status and /config to configure itself.
Architecture
The SDK is a separate, CPU-only process from the LMCache server.
SDK process LMCache MP server
----------- -----------------
LMCacheKVCacheContext
├ ContiguousTransferWrapper
│ └ EngineDrivenContext{Shm,Pickle} ──MQ──▶ EngineDrivenTransferModule
│ └ StorageManager (L1 pool, locks, prefetch)
└ MessageQueueClient ──────────────────ZMQ──▶ LookupModule (LOOKUP / QUERY_PREFETCH_STATUS)
SharedMemory(name) ◀──────────────────────── L1 POSIX segment (SHM transport only)
- Control plane (ZMQ): lookup/prefetch, slot reservation, lock release, session end.
- Data plane: SHM when the server exposes an L1 pool, otherwise pickle over the
MQ — both driven through one
EngineDrivenContext, so the SDK never branches on transport. ContiguousTransferWrapper(wrapper/contiguous.py) bridges a contiguous[2, L, T, D]tensor to the per-chunkprepare/commitprotocol and masks the SHM-vs-pickle difference.
Registration handshake
connect() builds the context, then register_kv_caches() runs once:
- HTTP
/config→chunk_size. - HTTP
/status→ thecache_context_metaentry formodel_name:world_sizeand the GPUkv_cache_layout(num_layers,dtype,tokens_per_block,engine_kv_format,engine_kv_concrete_shape). - Decode geometry from the layout:
num_kv_heads/block_sizevia the format-awareget_num_heads/get_block_size(on ametaprobe tensor — no allocation),head_dimfrom the last dim; assertblock_size == tokens_per_block. - Allocate a dummy 1-block HND buffer
{layer.i: zeros([1, 2, NH, BS, HS], cpu)}— used only to register the layout; the data plane never touches it. create_transfer_context(buffer)→EngineDrivenTransferContext.register(...), which sendsREGISTER_KV_CACHE_ENGINE_DRIVEN_CONTEXT(server reserves the SHM pool / picks SHM vs pickle).- Wrap
transfer_ctx.engine_driven_contextin aContiguousTransferWrapper.
instance_id is the SDK process PID.
Public API
Module functions in lmcache.sdk.kvcache; LMCacheKVCacheContext / KVCacheSDKError are
exported from lmcache.sdk.
connect(url, http_url, model_name, timeout=60.0)— open the MQ client, fetch config, run the handshake.retrieve(ctx, tokens, cache_salt="")→ contiguous CPU[2, num_layers, hit_tokens, hidden_dim]for the cached prefix, orNone(empty/sub-chunk input, or nothing cached).store(ctx, kv, tokens, cache_salt="")→bool.kvis[2, L, T, D];len(tokens)must equalT; both are truncated to whole chunks before storing.close(ctx)— shut down the MQ client and ZMQ context.
Cache addressing
Both build an IPCCacheServerKey(model_name, world_size=1, worker_id=0, token_ids, start=0, end=<chunk-aligned>, request_id, cache_salt). The server resolves it to per-chunk ObjectKeys;
cache identity = token-chunk hashes + model_name + kv_rank(worker_id) + cache_salt.
request_id (store-/retrieve-<uuid>) only keys the per-request session, not cache identity.
worker_id=0is valid because the SDK isworld_size == 1(one shard per chunk).- LOOKUP uses the
worker_id=None(expand-to-all-workers) variant.
Flows
store — validate len(tokens) == kv.shape[2], truncate to whole chunks, then
transfer_ctx.store(key, instance_id, kv_cpu): prepare_store returns writable SHM slots
(filled in place) or None for pickle (gather all chunks); commit_store. Writes use mode
"new", so already-cached chunks are deduplicated.
retrieve — Phase 0: LOOKUP + poll QUERY_PREFETCH_STATUS until non-None; if 0 → None.
Phase 1: transfer_ctx.retrieve(key, instance_id): prepare_retrieve → torch.cat the chunk
slots into one contiguous tensor → commit_retrieve. end_session always runs in finally.
Copy summary
| Flow | SHM | Pickle |
|---|---|---|
| store | 1 (tensor → SHM slot) | ~2 (tensor → chunks, then serialize) |
| retrieve | 1 (slot → contiguous) | ~2 (deserialize, then → contiguous) |
Constraints & known gaps
world_size == 1only, and a single non-hybrid kernel group only.- Model must be pre-registered by a vLLM instance (
REGISTER_KV_CACHE); the SDK reads the GPU layout from/statusand cannot derive it frommodel_namealone.