Repository navigation
Description
HyperCache has no real LMCache integration, and some docs claim more than the code does. The fixes are on branch feat/lmcache-l2-adapter, awaiting review. They are not merged.
Problems found
- KV blocks get uploaded to the backend.
handle_stage_writeignoresauto_flush_s3=False, and every commit starts write-back unless it is write-through.HyperCacheKVStoretherefore tries to upload prompt-derived KV data. If the upload fails, the block staysSYNC_FAILED, dirty and impossible to evict. locatereturns HIT for staged objects. The layout is published before the data is written, soHyperCacheKVStorecould return garbage during a write or after a crash.- LMCache and Dynamo KVBM were described as ready.
kv_store.pyand the README said "plug-and-play" and "LMCache / Dynamo KVBM Ready", but neither integration existed. - The pipelined loader claim is wrong. The README says Layer 0 loads in milliseconds so compute "starts immediately", and
pipeline.pysays "sub-100ms perceived cold starts". The first token needs every layer, so the saving is at mostmin(T_io, T_compute). The loader also returns bytes only and runs no model. - The README's pipelined loader example doesn't match the API.
prefetch_depthandstream_layers(layer_uris)do not exist. DirectBlockWriterignores short writes.
Measurements
Small test server: RTX 4060 Ti 16 GB, Ryzen 5 5600GT, Intel Gen3 x2 NVMe (about 1.4 GB/s direct read). Treat these as behaviour and relative cost, not production throughput.
Pipelined loader, Qwen2.5-7B-Instruct, 15.2 GB, median of 3 runs:
| Prompt tokens | First layer | Eager TTFT | Pipelined TTFT | Change |
|---|---|---|---|---|
| 5 | 880 ms | 15.14 s | 15.66 s | 3.4% slower |
| 2048 | 878 ms | 15.88 s | 16.07 s | 1.2% slower |
| 4096 | 881 ms | 16.67 s | 16.49 s | 1.1% faster |
The cause of the slowdown on short prompts was not profiled. Much faster storage could change these percentages, and that was not tested.
vLLM 0.31.0, LMCache 0.5.5 and the new adapter, prompt of about 9,500 tokens:
| Request | TTFT | Served from |
|---|---|---|
| Cold | 3.55 s | Computed |
| Repeat | 0.20 s | L1 |
| After L1 was filled | 0.68 s | NVMe (37 of 37 chunks in 459 ms, one run) |
Output text was identical across all three. A raw-device speed comparison against LMCache's adapters is in progress and will be added as a comment.
What the branch changes
- Adapter: adds
hypercache.integrations.lmcache_l2as a plugin L2 adapter, with an optionallmcacheextra. - Daemon: adds a
LOCAL_ONLYstatus (never flushed, stays evictable), optionallocal_onlyandchunk_checksumsoncommit_write, andsync_statusonlocateHITs. All are backward compatible on the wire. - Safety: the adapter treats any doubt as a miss, and verifies a CRC32 for every chunk on load. Its
namespacesetting is required, because LMCache's key has no model revision. It refuses to store against an older daemon before writing any data, so nothing can be uploaded by accident. - Client:
HyperCacheKVStorecommits local-only unlesswrite_throughis set, and treats staged blocks as misses.DirectBlockWriterretries short writes. - Docs: corrects the claims above and the broken example.
Verification
- 69 Python tests pass with 1 skipped on macOS, including 35 new ones. All 79 Rust library tests pass locally, including 2 new ones.
scripts/e2e/lmcache_l2_e2e.pypasses 18 of 18 on LMCache 0.5.5 and 0.5.6 with the real daemon on a raw device, including a flipped byte on disk.verify_release.sh --quickpasses on macOS, with YAML skipped because PyYAML is not installed.verify_release.sh --stages linuxpasses in a clean container. The image stage also passed; the k8s stage was skipped.
Still open
- Flaky test:
test_concurrent_locates_trigger_single_ingestfailed 11 of 60 runs under CPU load, and once in the harness. The final check onengine.inflightappears to race with the ingest leader's cleanup. The branch does not touch that file. - Pressure eviction frees extents with no grace period. A reader holding an old layout could read reused bytes. The adapter's CRC check turns that into a miss, but the other readers need review.
- Crashed writers:
STAGEDcounts as dirty, so an object from a crashed writer can't be evicted until something rewrites or removes it. -
socket_pathin the config file: the daemon ignored it in testing and used the default path unlessHYPERCACHE_SOCKET_PATHwas set. - Not supported by the adapter:
list_l2_keysand IsolatedLRU quotas. - Later LMCache versions:
get_memory_tagsbecomes abstract andObjectKeychanges on thedevbranch, so the adapter pins<0.6. - LMCache adapter docs: document how to install, configure and use the adapter, and what it does not support. The README has a short section only. Cover
GETTING_STARTED.md,client-py/README.md,ARCHITECTURE.mdand thehypercache-webdocs. - Website docs:
hypercache-web(python-sdk.md,architecture.md) repeats the same claims.
Activity
neo-th commentedon Nov 2, 2026
Raw-device comparison
All rows use the same drive (Intel SSDPEKNU512GZ, Gen3 x2, about 1.6 GB/s read), wiped before every run. 14 MiB objects, 8 keys per submit, 4 in flight, 20 rounds. Median of 3 runs. This shows relative cost on one small drive, not production throughput.
raw_blockraw_blockfs_native, O_DIRECTThe arena row and its
fs_nativecomparison used LMCache 0.5.5 (fs_nativeat 8 workers: 1343 and 1514). The other rows used 0.5.6.raw_blockwith io_uring could not start in this container, so it was not measured.raw_block. HyperCache does verify a CRC32 per chunk, costing about 3% of read throughput in a direct probe.raw_block's source shows CRC checks only on metadata checkpoints.raw_blockwrites in parallel and still loads at 1534 MB/s, so this looks specific to HyperCache's raw write path. Writing chunks through the page cache from a shared pool may scatter them inside the drive, but that is unverified.The adapter's
num_workersdefault is now 4 because of this.neo-th commentedon Nov 2, 2026
Update: raw write path fixed on the branch
My earlier comment guessed that out-of-order chunks were the cause of slow raw reads. That guess was wrong. Testing each write method separately showed that writing through the page cache is what lowers later read speed. Writing chunks in order made no improvement.
At 8 objects in parallel, on a freshly wiped drive, with no LMCache involved:
Why page-cache writeback has this effect on this drive was not investigated, so other drives may behave differently.
The change:
DirectBlockWritergains an opt-indirect_on_block_devicesmode, used only by the LMCache adapter and only on raw block devices. Partial last blocks, refused writes and devices without O_DIRECT go through the page cache. Unaligned buffers go through an aligned copy. The default path, the disk format and the protocol are unchanged.Results through the adapter, LMCache 0.5.6, same drive, median of 3 runs in shuffled order:
raw_blockraw_blockand stores about 4% slower, while verifying a CRC32 per chunk. It does not beatraw_block, and these loads are near this drive's limit of about 1.6 GB/s.raw_blockwith io_uring could not start in this container and was not measured.Verification: the adapter's 18 end-to-end checks pass on LMCache 0.5.5 and 0.5.6 on a raw device, including rewriting the same extents alternately through the page cache and O_DIRECT.
verify_release.sh --stages linuxpasses in a clean container.The earlier to-do on writing chunks in order is closed by these results. Still open: daemon model ingest and other
DirectBlockWritercallers still write through the page cache, and were not tested with O_DIRECT.neo-th commentedon Nov 4, 2026
Closing in favour of #4, which tracks the Rust connector that replaced the Python adapter described here.
Where each part stands:
feat/lmcache-rust-connector, not on main yet. Get the Rust LMCache connector to main #4 tracks merging them.feat/lmcache-l2-adapterat5108dcc.lmcache_rust_connector_nvme.mdon the branch.Add LMCache L2 adapter and correct KV and pipelined loader claims