Slotmap mode: directory-free deterministic KV placement¶
PeerCache's default p2p mode keeps a distributed directory (consistent-hash
shard of key -> {node, addr, rkey, len}). Every read therefore needs a
directory lookup RPC before the one-sided RDMA READ.
mode="slotmap" removes the directory entirely. A key maps to its owner by
the ring and to a fixed physical slot by an independent hash, so a reader
computes the address locally and issues one one-sided RDMA READ of the whole
N-way bucket — no dir_get round-trip, no metadata at all.
p2p: read = dir_get(key) ──RPC──> directory shard ──> {node,addr,rkey,len}
└── one-sided RDMA READ
slotmap: read = owner = ring(key); bucket = hash(key); slot = way(bucket)
└── one-sided RDMA READ of the bucket
(header-validated locally, no lookup)
Why¶
- Read latency: one RTT saved per read (no directory RPC). On the hot path this is the dominant saving for small pages.
- No metadata SPOF / resharding: with no directory there is nothing to reshard when nodes join/leave; placement is a pure function of the ring + key hash. Ring membership still changes on join/leave (owner reassignment), but there is no per-entry metadata to migrate.
- Deterministic addressing: any node can compute any key's location from
the ring and the geometry — a property
p2pgets only after a directory lookup.
How it works¶
Geometry (python/peercache/slotmap.py)¶
Each node owns a fixed slot region — one registered MR of
where slot_stride = HEADER + max_payload (one size class for now). Peers
advertise (base_addr, geometry, rkeys) via a _on_slot_layout RPC; a reader
caches that layout and never asks again.
key -> owner: existing consistent-hash ring (data_owner_all).key -> bucket: independent hash (bucket_hash) over the key.key -> way: N-way associativity.pick_wayprefers an empty slot, then the same key (overwrite), then the oldest sequence (evict).
Header / seqlock (slot_matches)¶
Every slot starts with a versioned header carrying key_hash128(key) and
payload_len. slot_matches(header, key, expected_len) is the single
gate that guarantees a reader never returns:
- another key's page (hash mismatch → clean miss),
- a torn page (odd seq → write in progress → clean miss),
- a wrong-length page (payload mismatch → clean miss).
A "miss" is therefore always clean: the caller simply treats the slot as
empty. exists probes the header only (no payload copy).
Write / read paths (store.py)¶
| path | behavior |
|---|---|
batch_set_v1 |
_publish_slotmap: own keys → local write_local (memmove + seqlock); remote keys → one batched READ of the target buckets, pick a way, one batched WRITE (RMW, no reservation RPC). An intra-batch reservation map stops same-bucket keys from evicting each other. |
batch_get_v1 |
_fetch_slotmap: per key, one READ of its bucket, slot_matches locally, memmove payload on a hit. |
batch_exists |
_exists_slotmap: header-only bucket READs, no payload copy. |
Both RDMA and TCP transports implement batch_read_multi / batch_write_multi,
so slotmap works over TCP too (functional validation without RDMA hardware)
and over one-sided RDMA (the intended production path).
Configuration¶
{
"backend_name": "peercache",
"module_path": "peercache.store",
"class_name": "PeerCacheStore",
"discovery_addr": "NODE0_IP:31998",
"protocol": "rdma",
"mode": "slotmap",
"slot_max_page_bytes": 262144,
"slot_ways": 4,
"slot_num_buckets": 0
}
| key | default | meaning |
|---|---|---|
mode |
"p2p" |
"slotmap" enables directory-free placement |
slot_max_page_bytes |
262144 (256 KiB) |
max payload of one KV page; must be ≥ the KV page size sglang emits (one size class for now). Tune to your page size; pages larger than this are cleanly skipped. |
slot_ways |
4 |
N-way associativity per bucket. More ways → higher hit rate under collisions, at the cost of a larger bucket READ. |
slot_num_buckets |
0 |
bucket count per node; 0 derives from global_segment_size / (ways × slot_stride). |
Run with SGLang¶
python -m sglang.launch_server --enable-hierarchical-cache \
--hicache-write-policy write_through \
--hicache-storage-backend dynamic \
--hicache-storage-backend-extra-config \
'{"backend_name":"peercache","module_path":"peercache.store","class_name":"PeerCacheStore",
"discovery_addr":"NODE0_IP:31998","protocol":"tcp","mode":"slotmap",
"slot_max_page_bytes":262144,"slot_ways":4,"slot_num_buckets":0}'
Startup log confirms the slot region:
PeerCacheStore slot region ready: 1023 buckets x 4 ways x 262208 B (=1072955136 bytes) across 1 rail(s)
Metrics: write_requests / bytes_written count published pages. There is no
pool_keys in slotmap mode (no published pool, no directory) — that is by
design, not a bug.
Verified (L20, sglang 0.5.9 + Qwen2.5-0.5B, TCP)¶
- Store-level byte-identical cross-node roundtrip:
tests/test_slotmap_e2e_tcp.py(write on A, read on B, content verified; clean miss on missing keys; no dirty hit on length mismatch). - SGLang e2e, single node:
tests/sglang/test_sglang_e2e.py --mode slotmaplaunches a real server, drives shared-prefix requests, assertswrite_requests/bytes_written(PASS). - Cross-node read against a live server:
tests/sglang/test_sglang_2node_slotmap_read.pyjoins the cluster as a second node, resolves the producer's slot layout (no directory), and reads the published pages back with one-shot bucket READs — 7 pages × 12 KiB all recovered, 256 bucket reads OK.
Limitations / roadmap¶
- One size class:
slot_max_page_bytesis global; a page larger than it is skipped (clean miss). Multi-size classes would raise slot utilization on mixed workloads. - Deterministic, not load-balanced: keys land on their hash bucket
regardless of hot/cold;
p2p's LRU published pool absorbs hotspots better. slotmap trades that for zero-lookup reads. - Collisions evict: with
wayssmall and high collision, older pages are evicted silently (readers see a clean miss, never stale data). Sizingslot_num_bucketshigh enough matters for hit rate. - bench tooling:
peercache-benchdoes not yet drive slotmap mode (p2p only); the read path is covered by the tests above. - RDMA: the design targets one-sided RDMA READ; TCP exercises the same
code path functionally. The C++ data plane compiles against real
libibverbs (verified:
HAS_RDMA=True, full suite passes) and degrades to TCP cleanly on NIC-less machines. A cross-host RoCE measurement for slotmap is the remaining production validation.
When to use which¶
p2p |
slotmap |
|
|---|---|---|
| read path | directory RPC + RDMA READ | one RDMA READ |
| metadata | distributed DHT, reshard on membership change | none (pure hash) |
| hotspots | LRU published pool absorbs | fixed buckets, collisions evict |
| page sizes | any | one size class |
| best for | mixed workloads, hot pages, small clusters | uniform pages, latency-sensitive reads, large clusters |