Skip to content

feat: add a Rust LMCache L2 connector - #5

Draft
neo-th wants to merge 10 commits into
mainfrom
feat/lmcache-rust-connector
Draft

neo-th wants to merge 10 commits into
mainfrom
feat/lmcache-rust-connector

Conversation

@neo-th

@neo-th neo-th commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Draft. Not ready to merge: see Before merging. Related to #4.

What this adds

An LMCache L2 connector in Rust, loaded through LMCache's native_plugin type, that stores KV cache through the HyperCache daemon and stripes it across NVMe drives.

  • lmcache-connector/: the connector. Below LMCache's own wrapper it runs without Python or the interpreter lock, and every load is checked with CRC32. Any doubt about a block is a miss.
  • control-plane/src/protocol.rs, server.rs: commit_write accepts local_only and chunk_checksums, and locate reports sync_status on hits. All optional on the wire, so older clients aren't affected.
  • client-py/: O_DIRECT writes to raw block devices, off by default.
  • Reconnects after a daemon restart. A request is resent only if the write itself found a dead kept connection, never once the daemon may have received it.
  • Batched lookups and 4 lookup workers. No daemon or protocol change.

Reviewing

Of about 835 changed files, 798 are raw benchmark data under benchmarks/results/lmcache_l2_raw/, about 73,000 of the 78,000 added lines. Start with lmcache-connector/src/, the two control-plane files, then benchmarks/results/lmcache_rust_connector_nvme.md. Connector usage is in GETTING_STARTED.md section 9 and ARCHITECTURE.md section 7.5.

Results

Storage only, on bare metal with 4 Micron 7450 drives and no GPU, against LMCache raw_block on md RAID 0. 14 MiB objects, median of 3. Full results: benchmarks/results/lmcache_rust_connector_nvme.md.

Rust connector raw_block on RAID 0
Load, 4 drives 24.9 GB/s, 90% of the drives' 27.6 23.0 GB/s, range 15.2-24.7
Store, 4 drives 18.5 GB/s 16.9 GB/s
Load, 2 drives 12.1 GB/s 11.5 GB/s at its best worker count
Load, 1 drive (raw_block without RAID) 6.5 GB/s 6.2 GB/s
  • raw_block got stuck near 15.2 GB/s in 4 of 6 RAID 0 runs on 4 drives. The cause isn't known and could be our array setup, so don't read this as a general fault of raw_block.
  • A 30-minute soak made 2,585,477 loads while writes replaced the same objects. 323,029 were checked byte for byte, and none were wrong.
  • Lookups through LMCache, µs per key at 64 in flight, measured on a Ryzen 5 5600: hits went from 11.95 to 3.69 and misses from 9.77 to 2.36, against raw_block's 4.34 and 3.91. This hasn't been repeated on the bare-metal node.

Testing

  • Connector tests: 39 of 39, and part of CI now.
  • Safety script (scripts/e2e/lmcache_l2_e2e.py): 30 of 30 on LMCache 0.5.5 and 0.5.6, on 1 and 2 devices, on the final code.
  • Failure tests: 19 of 19 on the final code: kill -9 during loads, stores and lookups, restarts, a frozen daemon, reused blocks, corruption, a capacity limit and a full device. A load was always a miss or the right bytes.

The final-code safety and failure runs used loop devices on a RAM disk, with lower volumes to fit. Earlier rounds of the safety and failure tests ran on the real Micron drives, before the batched lookup change. The upgrade test also ran there, and the daemon hasn't changed since.

Before merging

  • Real vLLM on the 4060 Ti with new Gen4 drives: first-token time and concurrent traffic. So far, these storage results say nothing about first-token time.
  • Safety and failure tests on those real drives, on the final code.
  • Open issue: temperature-0 output can differ after a cache hit, including with LMCache L1 alone and no HyperCache. The cause isn't known, and neither is whether answers get worse. See benchmarks/results/lmcache_rust_connector_xeon.md.
  • Publish wheels to PyPI and add them to release.yml.
  • Connector build, lint and tests in CI (ci.yml).
  • Run the safety script in CI. It needs LMCache and a running daemon, so a green check doesn't cover it.
  • Confirm the lookup speed on the bare-metal node.

Known limits

  • Full-speed writes cut reads hard, from 25.4 to 6.5 GB/s. Most of that comes from the drives (fio drops from 27.7 to 10.7), but the connector leans further towards writes than fio did.
  • We haven't yet compared against fs_native on multiple drives, or run the failure tests on raw_block or fs_native, so these results don't show that HyperCache is safer than either.
  • The RAM tier isn't covered here.

neo-th added 10 commits November 2, 2026 01:21
Adapter:
- Add hypercache.integrations.lmcache_l2, a plugin L2 adapter for LMCache
  multiprocess mode, tested on LMCache 0.5.5 and 0.5.6 and vLLM 0.31.0.
- Any doubt about a block is a miss. A required namespace covers the model
  revision that LMCache's key lacks.

Daemon, backward compatible on the wire:
- Add a LOCAL_ONLY status: never flushed to a backend, stays evictable.
- Add optional local_only and chunk_checksums to commit_write.
- Report sync_status on locate HITs.

Client:
- HyperCacheKVStore commits local-only unless write_through is set, and
  treats staged blocks as misses.
- DirectBlockWriter retries short writes and gains an opt-in O_DIRECT mode
  for raw block devices. Only the LMCache adapter turns it on.

Docs:
- Correct the pipelined loader claims using measured time to first token.
- Remove the LMCache and Dynamo KVBM readiness claims.
- Fix the pipelined loader README example.

Results and scripts are in benchmarks/ and scripts/e2e/.

Refs #2
Add lmcache-connector/, an L2 connector loaded through LMCache's
native_plugin type, and remove hypercache.integrations.lmcache_l2 with
its unit tests and the hypercache[lmcache] extra. The Python adapter
never shipped in a release and remains at 5108dcc.

The connector does its I/O in worker threads without the interpreter
lock, and defaults to 32 workers per direction. On a Xeon VM with 4
network disks it loaded at 6.4 GB/s against 2.8 GB/s for Python, and at
39.8 GB/s against 9.4 GB/s from the RAM tier. On one drive the two tie,
because the drive sets the limit.

The end-to-end script now tests only this connector. Checks that shared
blocks with the Python adapter become checks on the URI format.

Known open issue: temperature-0 output sometimes differs after a cache
hit, including with LMCache L1 only and no HyperCache. See
benchmarks/results/lmcache_rust_connector_xeon.md.
Each worker keeps a connection to the daemon. After a restart that connection
is dead, so the worker's next request failed with a broken pipe: one failed
store or load per worker, with 32 failures at the default worker count.

A request that fails while being written can't have reached the daemon, so it
now reconnects and sends it again, once. A failure after that point is never
resent, because the daemon may already have acted on the request, so a commit
can't run twice.

Repeated on 4 NVMe drives: no failed first stores after a daemon restart, in
all 10 crash and restart tests.
Adds install, configuration and limits to GETTING_STARTED.md, ARCHITECTURE.md,
client-py/README.md and the connector's README, and the results from 4 Micron
7450 drives: striping, worker counts, raw_block comparison, mixed reads and
writes, failure tests, upgrades and a 30-minute soak.
The page cache check read every chunk through a descriptor for the object's
first drive, so it only held for one drive. It failed on 4 drives once objects
started on different drives. It had passed before most likely by accident: the
test data repeats every 32 bytes, and the old allocator kept drives' offsets in
step.

Passes 30 of 30 on LMCache 0.5.5 and 0.5.6, on 1 and 4 drives.
Each lookup made one socket round trip per key on 2 workers, which took
most of the lookup time. A batch's locate requests now go out in one
write, up to 64 keys or 64 KiB, and the replies are read in order. The
daemon and its protocol don't change, so this works with existing
daemons.

At 64 in flight through LMCache on a Ryzen 5 5600, hits went from about
12.0 to 3.7 us per key and misses from about 9.8 to 2.4. raw_block is
about 4.3 and 3.9 on the same CPU.

If a batch fails part-way, no key in it is reported as found. A timeout
or reset drops the connection, so a late reply can't be read as the
answer to a later lookup.
…kups

Measured on a Ryzen 5 5600: lookups through LMCache, hits and misses,
against raw_block and fs_native, plus their effect on loads. The safety
script and failure tests passed on the final code, on loop devices on a
RAM disk. The speed hasn't been repeated on the EPYC node.
The history rewrite that removed attribution lines changed this hash from 5108dcc to 5be16e3.
ci.yml never touched lmcache-connector/, so a green check said nothing about it. The steps are in the existing job, so the check that main requires covers them. The safety script still isn't in CI, because it needs LMCache and a running daemon.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant