A small, self-contained distributed key-value cache that pools GPU-node NVMe SSDs into a shared, large-capacity KV pool for LLM inference (e.g. GLM-5.1 / MLA, DeepSeek-V4), without any DingoFS / brpc / S3-RADOS dependency — it runs on its own (only its built-in MDS + etcd for dynamic membership). It plugs into three engines through thin adapters over one portable core:
- SGLang HiCache as an L3 external KV store (
--hicache-storage-backend dynamic). - LMCache as a
RemoteConnector. - vLLM directly as a
KVConnectorBase_V1(GPUDirect RDMA, no LMCache).
Origin: extracted from the DingoFS branch
feat/kvcache-sglang(src/cache/kvclient). The portable core has zero coupling to DingoFS, so it lives here as an independent repo. To instead fuse these semantics into the productiondingo-cache(brpc + MDS), seedocs/INTEGRATION.md.
dfkv_server— a cache-node daemon. Disk + bounded eviction, cache-only (a miss is a clean NotFound; no object-store fallback), synchronous durable-visible writes. Supports multiple NVMe SSDs per node (--dir d1,d2,d3, intra-node Ketama). With--mds,--group,--id,--advertise,--weightit registers into the MDS tier. With neither a--store-engineoption norDFKV_STORE_ENGINE, every server/store path selects the restart-safe slab backend.fileremains an explicit diagnostic/rollback choice; invalid slab capacity/geometry refuses startup rather than falling back. An optional write-through RAM hot tier (DFKV_RAM_TIER=1) serves registered-memory hits without disk I/O — see docs/ARCHITECTURE.md.dfkv_mds— stateless Membership Directory Service daemon. Flags:--listen <port>and--etcd <host:port>(default127.0.0.1:2379). The only etcd client in the system; holds each node's etcd lease on its behalf. Deploy as N replicas — no load-balancer needed; nodes and clients each pick any reachable MDS and fail over automatically.libdfkv.so— C ABI client (canonical namespace + object-key hashing, Ketama routing, opaque raw-value Put/Get/Exist).integration/hicache/dfkv_hicache.py— SGLangHiCacheStorageplugin loaded via--hicache-storage-backend dynamic(no SGLang fork). MLA: one packed-latent object per page, no tp_rank suffix,backup_skip(only tp_rank 0 writes).
SGLang HiCache (zero-copy interface_v1) → dfkv_hicache.py (ctypes) →
libdfkv v2 client (length-framed namespace/object identity + Ketama route) →
TCP/RDMA → dfkv_server → optional RAM hot tier → DiskCacheGroup over N NVMe
(production slab extents; explicit file fallback).
Distributed = client-side consistent hashing; no replication (regenerable KV →
node loss = miss → recompute). Full architecture: docs/ARCHITECTURE.md.
Membership is managed by the MDS tier (dfkv_mds + etcd). Nodes register
with the MDS on startup and send periodic heartbeats; etcd leases (TTL 30 s)
are the liveness signal. Native clients use the single immutable
dfkv_client_options_v2 constructor: set either static members or
mds_endpoints + mds_group. MDS mode polls the directory and rebuilds the
weighted consistent-hash ring whenever its epoch advances. Two-layer offline
detection: layer-2 — etcd lease expiry → MDS view changes → client epoch →
ring rebuild (authoritative removal, ≤ 30 s); layer-1 — PeerHealth fast
avoidance short-circuits transport failures to misses during a cooldown. There
is no post-open membership mutation API in v2.
Client registration (who is using dfkv): cache consumers (inference
connector instances — vLLM / LMCache / SGLang HiCache) register themselves with
the MDS under a disjoint etcd prefix (/dfkv/v1/groups/<g>/clients/<id>) so
they never enter the placement ring. The same lease/heartbeat contract as nodes
applies — a dead connector's key expires out of etcd within the TTL, no explicit
deregister, no stale keys. Connectors set
DFKV_CLIENT_OPT_REGISTER_WITH_MDS, client_id, client_info, and heartbeat
fields in dfkv_client_options_v2 when MDS discovery is used (opt out with
DFKV_CLIENT_REGISTER=0).
Observe with dfkvctl clients --mds <ep,...> --group <g> or the
dfkv_mds_group_clients gauge. Only upgraded clients register, so an empty list
means "none of the current consumers are registered," not "no one is using dfkv."
cmake -S . -B build # add -DDFKV_STATIC_LIBSTDCXX=ON for portable binaries
cmake --build build -j
ctest --test-dir build --output-on-failure # C++ gtests + the Python plugin testArtifacts: build/dfkv_server, build/dfkv_mds, build/libdfkv.so.
# 1. Start etcd (one or three nodes, external)
# 2. Start MDS replicas (stateless, any number)
dfkv_mds --listen 9400 --etcd 127.0.0.1:2379
# 3. On each cache node (--mds requires --id and --advertise)
dfkv_server --dir /mnt/disk1/dfkv,/mnt/disk2/dfkv,/mnt/disk3/dfkv \
--port 12000 --cap 6597069766656 \
--mds 10.0.0.1:9400,10.0.0.2:9400 \
--group default --id n1 --advertise 10.0.0.10:12000
# 4. Client: one immutable v2 construction descriptor (recommended MDS mode)
# dfkv_client_options_v2 o = { ... .mds_endpoints = "10.0.0.1:9400,10.0.0.2:9400",
# .mds_group = "default" };
# dfkv_client_t c = dfkv_open_v2(&o);
# Static single-node mode sets o.members instead; no mutable follow-up calls.dfkvctl ring --mds 10.0.0.1:9400 --group default # cache nodes + ring share
dfkvctl clients --mds 10.0.0.1:9400 --group default # inference consumers
dfkvctl stats --mds 10.0.0.1:9400 --group default # ring stats + clients=N
dfkvctl stat --mds 10.0.0.1:9400 --group default --all # per-node deep-diveFull dfkv CLUSTER deploy runbook (etcd + MDS + systemd units): docs/DEPLOY.md.
Per-engine connect/config + client env/config reference (all connectors): docs/CONNECTORS.md.
src/ portable C++ core: common/ (shared types) · utils/ (generic helpers) ·
transport/ (TCP/RDMA + wire protocol) · cache/ (StoreEngine: file
KVStore | slab SlabAllocator+DiskSlabStore · RamTier · dfkv_server) ·
client/ (KV client + C ABI) · mds/ (membership service + dfkv_mds) · tools/ (CLIs)
integration/hicache/ dfkv_hicache.py (SGLang dynamic backend plugin) + dfkv_telemetry/
(canonical shared telemetry pkg, vendored by the other connectors)
integration/common/ dfkv_common shared identity and C ABI schema package
integration/lmcache/ dfkv_connector (LMCache RemoteConnector, ctypes over libdfkv.so)
integration/vllm/ dfkv_vllm (vLLM KVConnectorBase_V1, GPUDirect RDMA, bypass LMCache)
test/ gtest suites + test/python (unittest + no-torch sglang shim)
docs/ ARCHITECTURE.md (layers · storage engines · RAM hot tier · wire protocol) ·
CONNECTORS.md (engine connectors: HiCache · vLLM · LMCache + client env/config reference) ·
DEPLOY.md (dfkv CLUSTER deploy: etcd + MDS + server + systemd) · INTEGRATION.md (fuse into dingo-cache)
CACHELIB_EVALUATION.md (F25 decision: no CacheLib/Navy backend in v2.0.0)
- SGLang HiCache:
integration/hicache/dfkv_hicache.py— seedocs/CONNECTORS.md§2 (connect/config/use; cluster deploy isdocs/DEPLOY.md). - LMCache:
integration/lmcache/(dfkv_connector) — seedocs/CONNECTORS.md§4 (deploy + design + implementation). - vLLM (direct):
integration/vllm/(dfkv_vllm) — aKVConnectorBase_V1connector occupying the same--kv-transfer-configslot asMooncakeStoreConnector, storing/loading KV directly over GPUDirect RDMA (no LMCache, no host bounce). Pure-Python ctypes overlibdfkv.so; uses the scatter-gather batch API to coalesce per-chunk keys. Validated on H100 + IB with DeepSeek-V4 (multi kv_cache_group / MLA + SWA), full cross-restart and cross-DP prefix hit. Seedocs/CONNECTORS.md§3 (config reference + recommended settings) andintegration/vllm/README.md.
- Slab-first storage (no flag/env required;
--store-engine=slabmay pin it explicitly): a fixed pool of pre-allocated extent files carved into deterministic size classes. Sparseslots.tblmetadata is rebuilt in bulk after restart, preserving cache warmth without directory walks, per-object opens, or file-per-block inode pressure. CRC-protected records, dirty-epoch cold reset, deferred removal, O_DIRECT payload I/O, and byte-aware eviction keep failures fail-closed.--store-engine=fileis an explicit diagnostic fallback, not a silent recovery path or the production default. - RAM hot tier (
DFKV_RAM_TIER=1, off by default): a pre-registered arena fronting the disk — PUT is write-through (synchronously visible, async-flushed to disk) and a warm GET is served straight from the arena over RDMA (zero-copy scatter-send from the arena MR, no open/pread/disk), removing the disk-bound COLD load bottleneck. Send-in-flight slot pinning + flush backpressure keep it correct;dfkv_ram_*metrics expose hit-rate + backpressure. See docs/ARCHITECTURE.md §5–6. - 128-bit native block identity: SHA-256 of length-framed canonical namespace + object-key bytes, truncated to 128 bits for wire/storage identity. Connector object keys are self-delimiting binary bytes encoding pool, full chunk hash, DP/TP/PCP/DCP/PP coordinates, cache-group/component, and optional scatter geometry; namespace remains separate binary identity. C ABI v2 passes every key as pointer + exact length (parallel arrays for batch/SG), so embedded NUL and non-UTF-8 bytes are preserved. Values remain opaque raw bytes with no hidden header or compatibility unwrap.
- Connection pooling + keep-alive (TCP_NODELAY): ~250× lower latency vs dial-per-call.
- Batch APIs with concurrent fan-out across nodes and local NVMe disks.
- Connect/IO timeouts + stale-connection retry: a hung node fails fast, never hangs.
- Observability (docs/METRICS.md): opt-in embedded Prometheus
/metricsondfkv_serveranddfkv_mds(--metrics-port); sampled op-latency histogram, eviction/error/per-disk/RDMA counters server-side; client-side counters (peer health, IO errors) viadfkv_stats_snapshot+ a plugin poller. Opt-in and off the datapath — no--metrics-port⇒ no listener, behavior unchanged. The three connectors (vLLM / LMCache / SGLang HiCache) can also push fleet metrics (ops/keys/bytes, op latency, per-peer latency) over OTLP to a central Collector → Grafana — opt-in viaDFKV_METRICS_ENABLED=1, zero-dependency stdlib exporter by default; see deploy/observability/CONNECTOR-USAGE.md and docs/METRICS.md §3.4. - Dynamic membership: the immutable
dfkv_client_options_v2descriptor selects MDS discovery at construction; the client polls MDS and atomically rebuilds its weighted Ketama ring on etcd-epoch changes. No mutable membership or post-open discovery API is exported. - CLI tools:
dfkv_smoke(roundtrip check),dfkvctl— per-node ops (put/get/exist/stat) plus cluster views:dfkvctl ring(membership + ring vnode share + each node's self-reported version/config — engine, capacity, RAM tier, RDMA dev — carried on register/heartbeat, so fleet-wide version/config audit is one command, no per-node ssh) anddfkvctl stat --all(per-node metrics + aggregate) via MDS. - RDMA transport v2 (gated
-DDFKV_WITH_RDMA=ON, native libibverbs RC): with no device override each host chooses its firstACTIVElocal HCA and sends an empty device selector to its peer. An explicit comma whitelist opts into multi-rail round-robin and therefore must name the intended fabric on both hosts. QPs bootstrap over a tiny TCP channel, so the data fabric needs no IP. A v2 capability probe is mandatory; protocol, QP, receive-segment, or registration mismatch rejects the connection. UnsetDFKV_RDMAselects TCP; once RDMA is requested it never switches transports. - One-sided zero-copy data plane: control descriptors/status use small 4-KiB SEND/RECV buffers. PUT payloads RDMA-WRITE into leases from one process-wide, pre-registered receive segment; GET payloads RDMA-WRITE directly into the caller's registered buffer. No connection-sized block buffers or payload copy are used.
- Optional pipelining (
DFKV_RDMA_DEPTH=K): K requests in flight per connection. A network-latency hider, not a throughput knob — GET and PUT are both depth-flat (the per-connection serve loop is in-order; benchmarked GET ~1.24 GB/s at depth 1 == 32). The throughput levers are multi-connection fan-out (batch_concurrency) and fewer/larger keys. Seedocs/datapath-perf-notes.md. - NUMA-aware rail selection (
DFKV_RDMA_NUMA=1): with an explicit multi-rail whitelist, prefers anACTIVErail local to the calling thread, then round-robins the listed healthy rails. Server threads follow their QP's rail NUMA node; the single shared receive segment is registered on each selected rail but is not separately NUMA-allocated per rail. Off by default; vendor-neutral (sysfs +sched_getcpu, no libnuma/CUDA). - HiCache v2 (PoolTransfer) for multi-pool models (Mamba/SWA/DeepSeek-V4).
- Packaging: CPack (deb/rpm/tgz) + Dockerfile; graceful shutdown; leveled logging.
Validated on a 5-node ring (8×B200 hosts, 6× Gen4 NVMe + 8×400G IB per node, 128 GiB RAM arena): cold read 97 → 156 GB/s, hot read 48 → 97 GB/s single-pair / 147 GB/s mesh, cold-read p99 0.9–2.2 s → ~50 ms. Defaults are safe for a single-fabric node; production overrides below make fabric selection and capacity explicit.
Server (per cache node):
| Knob | Recommended | Why |
|---|---|---|
--rdma-dev |
leave unset for one local HCA; list the fabric explicitly for multi-rail | Unset selects the first ACTIVE local HCA (peer names may differ). A comma list opts into multi-rail, anchors only listed active devices, and requires compatible names/fabric on both hosts; inactive entries are rejected. |
DFKV_DISK_HASH_WEIGHT |
10 |
Flattens the intra-server disk ring share from ±20 % to ±3 % so the hottest disk stops gating the whole node (+5–6 % cold read, ~2× lower p99). Re-routes existing keys (cache miss + refill) — flip together with a restart/upgrade window. |
--rdma-depth |
4 (keep) |
Deeper is not itself a throughput knob. It consumes more slots from the fixed shared receive segment. |
--ram-tier / --ram-tier-bytes / --ram-tier-shards |
on / sized to the node / 16 for ≥100 GiB arenas |
Large arenas contend on the shard locks under mixed load (+40 % mixed R/W at 16 shards on a 128 GiB arena); small (≤16 GiB) arenas are fine at the default 8. |
--store-engine |
slab |
Index rebuilds on restart; removes file-per-block hazards. |
Production client (inference connectors, one process per TP rank):
| Knob | Recommended | Why |
|---|---|---|
DFKV_RDMA_DEV |
best: rail affinity per rank — inject the matching local/server HCA name per process; simpler on symmetric hosts: the full comma list + DFKV_RDMA_NUMA=1 |
Explicit values are sent to the peer, so verify those names exist on both hosts/containers. Leave unset to let each host select its own first ACTIVE HCA when names differ. |
DFKV_RDMA_DEPTH |
4 |
Pairs with the server's posted depth (window = min of both, negotiated). |
DFKV_FANOUT_THREADS |
unset (default 32) | Only wide single-process clients (benchmarks, many concurrent Batch* callers) need more. |
Read-side convoy collapse (v2.0, opt-in) — for MLA + TP-N inference rings, where every rank is a separate process fetching the SAME page and the NVMe otherwise eats N identical reads per page (measured 1:1 disk:wire on a TP8 replay; with the knobs below: ~2.4 device reads per duplicated page, repeat reads of promoted pages hit zero disk):
| Knob | Recommended | Why |
|---|---|---|
DFKV_READ_COALESCE |
1 |
Master switch. Concurrent identical GETs share one disk read (sync + io_uring paths), and a whole-value read with convoy evidence is promoted into the RAM arena as a born-durable resident (zero flush cost, evictable at once). Unset = exact pre-1.35 data path. |
DFKV_READ_COALESCE_RECUR_MS |
1000 (default) |
Recurrence window: a lone whole read leaves a 64-byte key fingerprint; an identical read inside the window promotes on completion even when rank drift missed the in-flight overlap. Fingerprints, not payloads — widening it costs ~nothing (64 Ki-entry bound). 0 restores the overlap-only gate. |
DFKV_READ_COALESCE_TIMEOUT_MS |
500 (default) |
Follower wait bound; a waiter whose leader connection dies falls back to its own read instead of hanging. |
Watch dfkv_read_coalesce_{leaders,coalesced,recur,timeouts}_total and
dfkv_ram_promoted_total; timeouts staying at 0 and promoted tracking
recur are the healthy signature. Requires the RAM tier (--ram-tier) for
promotion; coalescing alone works without it. Note for env-file deployments:
the file is sourced, so new knobs must be explicitly exported by the
start script or a systemd drop-in to reach the server process.
Order matters when rolling this out: upgrade servers to v1.34+ first, then widen clients to multi-rail — pre-1.34 servers have no anchor on non-default rails and will exhibit the idle re-registration storm described above.
Benchmark reproduction (dfkv_bench): DFKV_RDMA=1 (explicit, or it
silently measures TCP), 8-rail DFKV_RDMA_DEV, DFKV_RDMA_DEPTH=4,
DFKV_FANOUT_THREADS=256; cold-read sweet spot --threads 16 --batch 8 --size 4194304 per client node. Keep --count ≤ the seed's written key count,
and let the drives settle ~10 min after bulk writes before cold-read A/Bs (FTL
GC depresses cold reads ~25 %). The command prints every requested phase report,
then exits 1 if any PUT/GET failed, 2 for invalid CLI usage, and 0 only
when every requested operation succeeded.
TDD; 264 C++ ctest entries (default) / 288 (RDMA+io_uring) + Python plugin &
connector tests green, 0 warnings, ThreadSanitizer-clean.
CI: gcc/clang build+test, TSan, RDMA datapath (Soft-RoCE loopback), RDMA compile-check, static-artifact build. License: Apache-2.0.
Architecture & design: docs/ARCHITECTURE.md. Rollout: docs/DEPLOY.md.