5.7 KiB
DAX L2 Adapter Design
This document describes the built-in dax L2 adapter for LMCache
multiprocess mode and how it shares implementation with the non-MP DAX
storage backend.
Goals
- Reuse one synchronous DAX core for MP and non-MP DAX storage.
- Keep the MP controller flow unchanged: adapters still use the normal submit, event-fd, and query-result contract.
- Keep the adapter facade stable while the runtime DAX device pool changes.
- Keep DAX volatile-only. Keys are indexed in process memory and are not recovered from device bytes after restart.
Components
lmcache/v1/storage_backend/dax/core.py defines DaxCore[KeyT]. The core owns
the mapped DAX arena, fixed-size slot allocation, in-memory index, LRU order,
in-flight writes, external lock refcounts, active read borrow counts, close
coordination, and direct ctypes.memmove copies.
lmcache/v1/storage_backend/plugins/dax_backend.py is the non-MP wrapper. It
keeps existing non-MP behavior such as the local CPU backend requirement, TP=1
validation, optional async put, and the staging-slab batched restore path.
lmcache/v1/distributed/l2_adapters/dax_l2_adapter.py is the MP adapter. It
self-registers adapter type dax, owns separate event notifiers and worker
pools for store, lookup, and load operations, and uses one or more
DaxCore[ObjectKey] instances behind a stable facade.
lmcache/v1/multiprocess/http_apis/reconfigure_api.py exposes runtime
reconfiguration endpoints:
GET /reconfigure/dax/statusPOST /reconfigure/dax/addPOST /reconfigure/dax/removePOST /reconfigure/dax/resize
The HTTP layer routes backend, operation, and adapter-specific JSON payloads
into the generic L2 adapter reconfiguration API on StorageManager.
StorageManager only routes operation plus payload to a reconfigurable
adapter; DAX path, mode, and migration semantics stay inside DaxL2Adapter.
The same interface is intended for future adapters such as P2P, so the HTTP
layer does not inspect private adapter lists or DAX core state directly.
Slot State
Each committed key points to one fixed-size slot. A slot is reusable only when:
- The key has been removed from the index.
- No external lock is held for the key.
- No store is in flight for the key.
- No active read has borrowed the slot.
Delete operations remove unlocked keys from the index immediately. If a read has borrowed the slot, the slot is marked pending-free and recycled when the borrow count reaches zero.
MP Flow
Store:
StoreControllercallssubmit_store_task(keys, objects).- The adapter chooses an active DAX device. Existing keys prefer their current mapped device; new keys use the active device with the lowest slot usage.
- A store worker copies each object into a DAX slot through
DaxCore.put_many. - The adapter records task-level success as
all(per_key_results). - The store event fd is signaled and store listeners are notified for the keys that were actually accepted by the core.
Lookup and load:
PrefetchControllercallssubmit_lookup_and_lock_task(keys).- The adapter checks
key -> devicemappings first, then scans readable devices if needed. - The adapter calls
DaxCore.exists_many(keys, lock=True)and returns a full bitmap, including holes. - Load workers call
DaxCore.load_many_into(keys, objects)on the device that currently owns each key. submit_unlock(keys)releases the external lock refcounts on every DAX core. This is deliberate because migration can updatekey -> devicemappings between lookup and unlock.
Runtime Hotplug
The DAX facade keeps the event fds and worker pools stable. Runtime hotplug only
mutates the device pool behind the facade, so StoreController,
PrefetchController, and the vLLM MP connector do not need ZMQ protocol changes
or poll-set re-registration.
Add:
- Validate
hotplug_enabled, path, and size. - Map a new
DaxCore[ObjectKey]. - Append a
DaxDeviceEntry(state="active"). - Return per-device status. Existing KV entries stay on their current devices.
Remove with migration:
- Mark the source device
drainingso new stores do not choose it. - Reject the operation if externally locked or borrowed slots would be deleted.
- Snapshot source keys and reserve source reads.
- Copy each reserved payload from the source DAX pointer into another active
DAX core with
put_reserved_from_ptr. - Update
key -> devicemappings, delete the source entries, then close the source core.
Resize:
- Grow remaps the same core to a larger size after active reads and writes drain. No KV payload movement is needed.
- Shrink first proves that every live slot fits below the new slot count. If not, the out-of-range keys must migrate to another active device or the request fails. Shrink never silently evicts data.
Restart Behavior
The adapter stores keys and metadata only in memory. Closing the adapter and opening a new adapter against the same DAX device starts with an empty index. Old bytes may remain on the device, but they are unreachable because PR1 does not define any on-device metadata, scan, checkpoint, or recovery format.
Capacity And Eviction
Usage is slot-based, not payload-byte-based. get_usage() reports occupied
slot capacity because the DAX arena is exhausted by slot count. The eviction
controller calls delete(keys), which skips externally locked keys and
reclaims slots after active read borrows drain.
Runtime capacity is the sum of active, draining, migrating, resizing, and removing device capacities. Closed, removed, and failed devices are excluded.
Current Limits
- Runtime hotplug does not perform kernel-level CXL or DAX reconfiguration.
- No per-TP partitioning.
- No restart recovery.
- Only single-buffer objects are supported.