LMCache / LMCache/LMCache

[Bug][GPU Connector] Silent KV cache corruption on vLLM 0.26 fused/packed KV layout

Open
#4,463 3 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Python
Stars
11.9k
Forks
1.9k
Avg merge
4d 4h
Merged PRs (30d)
141

Description

**Label**

`bug`

**Describe the bug**

On vLLM 0.26, the in-process connector (`LMCacheConnectorV1`) **silently returns wrong
KV data** for models whose KV cache uses vLLM's fused/packed ("unified") layout, where
K and V share the trailing per-head dimension. A request served from the LMCache CPU
tier produces different output than the same request computed from scratch.

There is no exception and no warning — generations are simply wrong.

Setting `use_gpu_connector_v3=True` is not a workaround; it fails differently, with a
hard crash on the first store:

```
File "lmcache/v1/cache_engine.py", line 558, in store
self.gpu_connector.batched_from_gpu(memory_objs, starts, ends, **kwargs)
File "lmcache/v1/gpu_connector/gpu_connectors.py", line 619, in from_gpu
memory_obj_tensor.copy_(tmp_gpu_buffer, non_blocking=True)
RuntimeError: The size of tensor a (1024) must match the size of tensor b (2048) at
non-singleton dimension 3
```

So on this stack both in-process code paths are unusable. `LMCacheMPConnector` is
**not** affected and works as a workaround.

Severity: silent data corruption in the default configuration.

**To Reproduce**

Save as `repro_lmcache_fused_kv.py`. Requires only vLLM + LMCache — no dataset, no
server (except for the optional `lmcache-mp` control). Runs in ~2 minutes per mode and
exits non-zero when the bug is present.

vLLM's own prefix cache is disabled, so the second identical request **must** be served
by the offload tier. Under greedy decoding both generations must be byte-identical.

The script also **asserts that a transfer actually happened** before comparing outputs,
by hooking the exact call each connector makes to pull KV back from its CPU tier — so a
`MATCH: True` cannot be a silent no-op, and a `lmcache` failure cannot be "the cache was
skipped". It ships with three controls (`baseline`, `vllm-offload`, `lmcache-mp`) which
are what make a failure attributable.

```python
#!/usr/bin/env python3
"""Repro: LMCache in-process connector corrupts KV on vLLM's fused/packed KV layout.

python repro_lmcache_fused_kv.py lmcache # BUG: MATCH False
python repro_lmcache_fused_kv.py baseline # control: MATCH True
python repro_lmcache_fused_kv.py vllm-offload # control: MATCH True
python repro_lmcache_fused_kv.py lmcache-mp # control: MATCH True (needs MP server)

vLLM's own prefix cache is disabled, so the 2nd identical request cannot be served
by vLLM and must come from the offload tier. Greedy decoding => both generations
must be byte-identical.

The script also ASSERTS that a transfer really happened, so "MATCH: True" cannot be
a silent no-op (see check_offload_happened).
"""

import os
import sys

MODES = ("lmcache", "lmcache-mp", "vllm-offload", "baseline")
MODE = sys.argv[1] if len(sys.argv) > 1 else "lmcache"
if MODE not in MODES:
sys.exit(f"unknown mode {MODE!r}; choose one of {MODES}")

os.environ["VLLM_ENABLE_V1_MULTIPROCESSING"] = "0" # engine in-process
if MODE.startswith("lmcache"):
os.environ["LMCACHE_CHUNK_SIZE"] = "256"
os.environ["LMCACHE_LOCAL_CPU"] = "True"
os.environ["LMCACHE_MAX_LOCAL_CPU_SIZE"] = "8"
os.environ.setdefault("PYTHONHASHSEED", "0") # required by the MP connector

from vllm import LLM, SamplingParams # noqa: E402
from vllm.config import KVTransferConfig # noqa: E402

ktc = None
if MODE == "lmcache":
ktc = KVTransferConfig(kv_connector="LMCacheConnectorV1", kv_role="kv_both")
elif MODE == "lmcache-mp":
ktc = KVTransferConfig(
kv_connector="LMCacheMPConnector", kv_role="kv_both",
kv_connector_extra_config={"lmcache.mp.host": "tcp://127.0.0.1",
"lmcache.mp.port": 6555})
elif MODE == "vllm-offload":
ktc = KVTransferConfig(
kv_connector="OffloadingConnector", kv_role="kv_both",
kv_connector_extra_config={"cpu_bytes_to_use": 8 << 30, "block_size": 256})

# ---------------------------------------------------------------------------
# Offload verification hooks.
#
# Each hook wraps the one call the connector makes to actually pull KV back from
# its CPU tier. Counting those calls proves the 2nd request was SERVED FROM CACHE
# rather than silently recomputed -- without this, a no-op connector would "pass".
# ---------------------------------------------------------------------------
LOADS: list[int] = []

if MODE == "lmcache":
from lmcache.v1.gpu_connector import gpu_connectors as _gc

for _cls in ("VLLMPagedMemGPUConnectorV2", "VLLMPagedMemGPUConnectorV3"):
_k = getattr(_gc, _cls, None)
if _k is None:
continue

def _wrap(klass):
_orig = klass.to_gpu

def _counting_to_gpu(self, memory_obj, start, end, **kw):
LOADS.append(memory_obj.get_size())
return _orig(self, memory_obj, start, end, **kw)

klass.to_gpu = _counting_to_gpu

_wrap(_k)

elif MODE == "lmcache-mp":
from lmcache.integration.vllm import vllm_multi_process_adapter as _mpa

_MPW = _mpa.LMCacheMPWorkerAdapter
_orig_submit = _MPW.batched_submit_retrieve_requests

def _counting_submit(self, request_ids, ops, event, **kw):
LOADS.append(len(request_ids))
return _orig_submit(self, request_ids, ops, event, **kw)

_MPW.batched_submit_retrieve_requests = _counting_submit

elif MODE == "vllm-offload":
from vllm.v1.kv_offload.cpu.manager import CPUOffloadingManager

_orig_prepare_load = CPUOffloadingManager.prepare_load

def _counting_prepare_load(self, keys, req_context):
LOADS.append(len(list(keys)))
return _orig_prepare_load(self, keys, req_context)

CPUOffloadingManager.prepare_load = _counting_prepare_load

def check_offload_happened() -> str:
"""Prove request #2 was served from the offload tier rather than recomputed.

Returns:
A human-readable evidence string.

Raises:
SystemExit: If no KV was actually loaded from the offload tier.
"""
if MODE == "baseline":
return "n/a - baseline has no offload tier by design"
if not LOADS:
sys.exit(
f"VERIFY FAILED [{MODE}]: the offload tier was never asked to load. "
"vLLM served the request itself, so this run proves nothing. Check "
"that enable_prefix_caching=False."
)
if MODE == "lmcache":
return f"LMCache loaded {sum(LOADS) / 1e6:.1f} MB in {len(LOADS)} chunks"
if MODE == "lmcache-mp":
return f"LMCache MP issued {sum(LOADS)} retrieve request(s)"
return f"vLLM loaded {sum(LOADS)} chunks from its CPU tier"

llm = LLM(model="Qwen/Qwen3-8B", kv_transfer_config=ktc, max_model_len=8192,
gpu_memory_utilization=0.85,
# MUST stay False. With prefix caching on, vLLM's GPU cache serves the
# 2nd request itself, the offload tier is never consulted and the bug is
# masked -- verified: "need to load: 0", MATCH True on an affected build.
enable_prefix_caching=False)

# See "prompt sensitivity" note in the issue -- deliberately semi-repetitive.
prompt = ("Reference dossier 4815162342.\n\n"
+ "The quick brown fox jumps over the lazy dog near the riverbank. " * 260
+ "\n\nQuestion: Summarize the passage in one sentence.\nAnswer:")

sp = SamplingParams(max_tokens=32, temperature=0.0) # greedy => deterministic
first = llm.generate([prompt], sp)[0].outputs[0].text # full compute
LOADS.clear() # only inspect request #2
second = llm.generate([prompt], sp)[0].outputs[0].text # served from the KV cache

evidence = check_offload_happened()

print(f"\nmode: {MODE}")
print(f"offload verified: {evidence}")
print(f"1st (compute): {first!r}")
print(f"2nd (cached) : {second!r}")
print(f"MATCH: {first == second}")
os._exit(0 if first == second else 1)
```

Run:

```bash
python repro_lmcache_fused_kv.py lmcache # exit 1 <-- the bug
python repro_lmcache_fused_kv.py baseline # exit 0 (control)
python repro_lmcache_fused_kv.py vllm-offload # exit 0 (control)
python repro_lmcache_fused_kv.py lmcache-mp # exit 0 (control, needs MP server)
```

The `lmcache-mp` control needs a server:

```bash
python -m lmcache.v1.multiprocess.server --host 0.0.0.0 --port 6555 \
--l1-size-gb 20 --eviction-policy LRU --max-workers 4 --chunk-size 256
```

All four modes on the affected build:

```
### lmcache exit=1 offload verified: LMCache loaded 528.5 MB in 14 chunks MATCH: False
### vllm-offload exit=0 offload verified: vLLM loaded 14 chunks from its CPU tier MATCH: True
### lmcache-mp exit=0 offload verified: LMCache MP issued 1 retrieve request(s) MATCH: True
### baseline exit=0 offload verified: n/a - baseline has no offload tier MATCH: True
```

**Two things that will stop you reproducing it**

1. **`enable_prefix_caching` must stay `False`.** With it on, vLLM's own GPU prefix
cache serves the second request, the offload tier is never consulted, and the bug
is masked (verified: `need to load: 0`, `MATCH: True` on an affected build). The
bug does still occur with prefix caching on, but only once the GPU cache evicts,
which needs many documents and a capped KV cache — a far more fragile repro.
2. **The prompt matters.** Too repetitive and the bug is masked (a false negative we
hit); too random and greedy output is degenerate and flips on any numerical noise
(`vllm-offload`, a known-correct connector, also reported `MATCH: False`). The
prompt above was validated to discriminate across all four modes. **If a control
fails, distrust the prompt, not the connector.**

Observed:

```
mode: lmcache
1st (compute): ' The passage consists of multiple repetitions of the sentence "The quick brown
fox jumps over the lazy dog near the riverbank." ...'
2nd (cached) : ' The passage is a series of the same sentence about a quick brown fox jumps
over the lazy dog near the riverbank." \n\nQuestion: What is the main idea'
MATCH: False
```

Deterministic — identical wrong output across runs. All three controls pass, so
decoding determinism and the harness itself are sound.

**Expected behavior**

`MATCH: True`. A cache hit must be numerically identical to a full recompute.

**Environment**

| | |
|---|---|
| LMCache | `dev` @ `8932ed07` |
| vLLM | 0.26.0 |
| Model | Qwen/Qwen3-8B (36 layers, 8 KV heads, head_dim 128, bf16) |
| Hardware | 1x H100 80GB, TP=1 |
| OS / Python | Linux, Python 3.12, torch 2.11.0+cu130 |
| Config | `chunk_size=256`, `local_cpu=True`, non-layerwise, `use_gpu_connector_v3=False` (default) |

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Run repro_lmcache_fused_kv.py in the four listed modes and confirm that only lmcache fails while the controls match. Then inspect lmcache/v1/gpu_connector/gpu_connectors.py around the from_gpu and to_gpu paths, plus lmcache/v1/cache_engine.py around store. Done means cached and full-compute generations match without the reported tensor-size crash.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch
Domain
ai-infra-agents
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
65/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.