255 lines
9.7 KiB
ReStructuredText
255 lines
9.7 KiB
ReStructuredText
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
Device-DAX (/dev/dax)
|
|
=====================
|
|
|
|
Overview
|
|
--------
|
|
|
|
The DAX storage plugin maps a ``/dev/dax`` device using ``mmap(MAP_SHARED)``
|
|
and uses the mapped region as a fixed-size arena for KV cache chunks.
|
|
Typical ``/dev/dax`` devices include persistent memory,
|
|
CXL-attached memory, and other byte-addressable memory devices.
|
|
|
|
Data stored on the DAX device may survive process restarts,
|
|
but is not guaranteed to be durable.
|
|
|
|
KV cache data is stored in the DAX region as part of the backend's storage flow.
|
|
Reads copy data back into CPU-backed memory objects.
|
|
|
|
|
|
Configuration
|
|
-------------
|
|
|
|
.. code-block:: yaml
|
|
|
|
local_cpu: true
|
|
max_local_cpu_size: 80
|
|
|
|
storage_plugins: ["dax"]
|
|
extra_config:
|
|
storage_plugin.dax.module_path: lmcache.v1.storage_backend.plugins.dax_backend
|
|
storage_plugin.dax.class_name: DaxBackend
|
|
|
|
dax.device_path: "/dev/dax1.0"
|
|
dax.max_dax_size: 100
|
|
dax.restore_workers: 8
|
|
dax.restore_max_regions: 8
|
|
dax.retrieve_staging_slab_bytes: 268435456
|
|
|
|
|
|
Multiprocess Mode
|
|
-----------------
|
|
|
|
In LMCache multiprocess mode, Device-DAX is configured as a built-in L2
|
|
adapter named ``dax``. The MP adapter uses the normal L2 adapter
|
|
``submit -> event fd -> query`` contract; no vLLM connector protocol changes
|
|
are required.
|
|
|
|
.. code-block:: bash
|
|
|
|
lmcache server \
|
|
--l1-size-gb 80 \
|
|
--eviction-policy LRU \
|
|
--l2-adapter '{
|
|
"type": "dax",
|
|
"device_path": "/dev/dax1.0",
|
|
"max_dax_size_gb": 100,
|
|
"slot_bytes": 268435456,
|
|
"num_store_workers": 1,
|
|
"num_lookup_workers": 1,
|
|
"num_load_workers": 4
|
|
}'
|
|
|
|
The legacy single-device ``--l2-adapter`` JSON accepts these fields:
|
|
|
|
- ``device_path``: required path to a readable and writable DAX device.
|
|
- ``max_dax_size_gb``: required mapped size in GiB. The value must fit within
|
|
the device capacity when capacity can be determined with ``fstat``.
|
|
- ``slot_bytes``: required fixed slot size in bytes. It must be large enough
|
|
for one full LMCache chunk.
|
|
- ``num_store_workers``: optional store worker count, default ``1``.
|
|
- ``num_lookup_workers``: optional lookup worker count, default ``1``.
|
|
- ``num_load_workers``: optional load worker count, default
|
|
``min(4, os.cpu_count())``.
|
|
- ``persist_enabled``: accepted by common MP L2 parsing but ignored by
|
|
``dax`` in this release.
|
|
|
|
Runtime hotplug uses the multi-device form. The ``devices`` list may also be
|
|
empty when ``hotplug_enabled`` is ``true``.
|
|
|
|
.. code-block:: bash
|
|
|
|
lmcache server \
|
|
--l1-size-gb 80 \
|
|
--eviction-policy LRU \
|
|
--l2-adapter '{
|
|
"type": "dax",
|
|
"devices": [
|
|
{"device_path": "/dev/daxX.X", "max_dax_size_gb": 100},
|
|
{"device_path": "/dev/daxY.Y", "max_dax_size_gb": 100}
|
|
],
|
|
"slot_bytes": 268435456,
|
|
"hotplug_enabled": true,
|
|
"num_store_workers": 1,
|
|
"num_lookup_workers": 1,
|
|
"num_load_workers": 4
|
|
}'
|
|
|
|
MP DAX stores opaque ``ObjectKey`` values in memory and is volatile-only in
|
|
this release. Closing and reopening the server on the same DAX path starts
|
|
with an empty index, so previously written bytes are not discoverable after
|
|
restart.
|
|
|
|
MP DAX uses one stable adapter facade per LMCache server. The facade owns
|
|
stable event fds and worker pools, and runtime add/remove/resize only changes
|
|
the mapped DAX cores behind that facade. It does not add kernel-level CXL or
|
|
DAX reconfiguration, per-TP DAX partitions, on-device metadata, or restart
|
|
recovery. Capacity accounting and eviction are slot-based: a stored object
|
|
occupies one slot even if its payload is smaller than ``slot_bytes``.
|
|
|
|
|
|
Runtime Hotplug API
|
|
-------------------
|
|
|
|
Runtime hotplug is disabled unless ``hotplug_enabled`` is ``true``. The API
|
|
changes only LMCache runtime mappings and metadata; the ``/dev/dax*`` device
|
|
must already exist and be readable and writable by the LMCache server process.
|
|
The runtime endpoints are implemented through StorageManager's generic L2
|
|
adapter reconfiguration interface, which routes backend, operation name, and
|
|
adapter-specific payload to the selected adapter. DAX owns the path, mode,
|
|
migration, and resize semantics; the generic interface is reusable by other
|
|
adapters such as P2P.
|
|
Use JSON bodies because DAX paths contain slashes:
|
|
|
|
.. code-block:: bash
|
|
|
|
curl http://127.0.0.1:9000/reconfigure/dax/status
|
|
curl -X POST http://127.0.0.1:9000/reconfigure/dax/add \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"device_path": "/dev/daxX.X", "size": "100GiB"}'
|
|
curl -X POST http://127.0.0.1:9000/reconfigure/dax/remove \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"device_path": "/dev/daxX.X", "mode": "migrate"}'
|
|
curl -X POST http://127.0.0.1:9000/reconfigure/dax/resize \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"device_path": "/dev/daxX.X", "size": "200GiB"}'
|
|
|
|
``size`` is required for add and resize. Use an integer byte count or a string
|
|
such as ``"100GiB"``. ``remove`` supports these modes:
|
|
|
|
- ``migrate``: move DAX-resident KV to other active DAX devices before closing
|
|
the source device.
|
|
- ``evict``: delete DAX-resident KV on the source device. This is destructive
|
|
for the DAX tier.
|
|
- ``drain``: stop new writes to the source device and leave existing KV
|
|
readable until it is evicted or the server closes.
|
|
|
|
``resize`` supports ``migrate`` and ``evict`` modes. It does not support
|
|
``drain`` because resize completes synchronously.
|
|
|
|
Hotplug operations are lock-safe by default. A remove or shrink that would
|
|
delete externally locked or borrowed slots returns ``409 Conflict`` unless
|
|
``force`` is set. A migration that has no active destination capacity returns
|
|
``507 Insufficient Storage``. Resize grow preserves the in-memory key index and
|
|
does not move KV payloads. Resize shrink never silently drops keys; entries
|
|
outside the new slot range must migrate first, or the request fails.
|
|
|
|
|
|
Hardware Validation Flow
|
|
------------------------
|
|
|
|
Use the same Qwen 8B or 14B long-context workload before and after a runtime
|
|
capacity change. Without hotplug support, ``/reconfigure/dax/status`` and
|
|
``/reconfigure/dax/add`` are not available; changing the DAX device set
|
|
requires restarting LMCache with a new ``--l2-adapter`` value, which drops the
|
|
volatile DAX key index.
|
|
|
|
.. code-block:: bash
|
|
|
|
export MODEL=Qwen/Qwen3-8B # or a local Qwen 8B/14B checkpoint
|
|
curl http://127.0.0.1:9000/reconfigure/dax/status
|
|
python benchmarks/long_doc_qa/long_doc_qa.py \
|
|
--model "$MODEL" --num-documents 1 --document-length 1024 \
|
|
--output-len 16 --repeat-count 2 --repeat-mode tile \
|
|
--completions --host 127.0.0.1 --port 8000 --json-output
|
|
curl -X POST http://127.0.0.1:9000/reconfigure/dax/add \
|
|
-H 'Content-Type: application/json' \
|
|
-d '{"device_path": "/dev/daxX.X", "size": "100GiB"}'
|
|
curl http://127.0.0.1:9000/reconfigure/dax/status
|
|
|
|
Record these fields for the comparison:
|
|
|
|
- ``total_capacity_bytes`` before and after ``/reconfigure/dax/add``.
|
|
- ``total_used_bytes`` while the Qwen workload is running.
|
|
- Whether an LMCache restart was required.
|
|
- Whether the same cached prompt remains retrievable after the capacity change.
|
|
|
|
|
|
Using The Batched Restore Path
|
|
------------------------------
|
|
|
|
The current DAX optimization is a staged batched restore path for retrieval.
|
|
It is enabled automatically whenever the DAX backend is configured. No extra
|
|
feature flag is required.
|
|
|
|
The retrieve flow is:
|
|
|
|
1. Reserve a batched set of readable DAX chunks.
|
|
2. Allocate CPU restore buffers from ``LocalCPUBackend``.
|
|
3. Copy DAX data into a backend-owned pinned staging slab in coalesced regions.
|
|
4. Copy from the staging slab into the final CPU ``MemoryObj`` outputs.
|
|
5. Upload those CPU outputs through the normal GPU connector path.
|
|
|
|
The store flow is unchanged: KV data is still staged through CPU memory before
|
|
being written into the DAX arena.
|
|
|
|
The new DAX tuning knobs control the batched restore path:
|
|
|
|
- ``dax.restore_workers``: number of persistent worker threads used to execute
|
|
restore regions in parallel.
|
|
- ``dax.restore_max_regions``: maximum number of restore regions in one wave.
|
|
Larger values increase parallelism but also increase slab space requirements.
|
|
- ``dax.retrieve_staging_slab_bytes``: total size in bytes of the reusable
|
|
pinned retrieve slab. This must be large enough to hold one full chunk per
|
|
configured restore region.
|
|
|
|
For a first pass, start with:
|
|
|
|
- ``dax.restore_workers`` equal to the number of CPU workers you want devoted
|
|
to DAX restores
|
|
- ``dax.restore_max_regions`` equal to ``dax.restore_workers``
|
|
- ``dax.retrieve_staging_slab_bytes`` at least
|
|
``dax.restore_max_regions * full_chunk_size``, then scale upward if larger
|
|
batched restores are common
|
|
|
|
If retrieve throughput is low, increase the slab size first, then increase
|
|
worker and region counts together. If CPU pressure is high, reduce
|
|
``dax.restore_workers`` and ``dax.restore_max_regions``.
|
|
|
|
|
|
Runtime Requirements
|
|
--------------------
|
|
|
|
- ``extra_config['dax.device_path']`` is required and must point to a readable
|
|
and writable DAX device.
|
|
- The process must have read-write access to the DAX device
|
|
(e.g., via appropriate permissions or group membership).
|
|
- ``LocalCPUBackend`` must be enabled because DAX reads return CPU-backed
|
|
memory objects.
|
|
|
|
|
|
Validation and Current Limits
|
|
-----------------------------
|
|
|
|
- Tensor parallelism is currently limited to TP=1
|
|
(``metadata.world_size == 1``).
|
|
- Only single-tensor chunk layouts are supported. Multi-tensor put
|
|
requests are rejected.
|
|
- Batched restore uses a backend-owned retrieve staging slab and persistent
|
|
restore executors. The slab and region count can be tuned with
|
|
``dax.restore_workers``, ``dax.restore_max_regions``, and
|
|
``dax.retrieve_staging_slab_bytes``.
|
|
- Blocking batched restore preserves positional output semantics, while
|
|
asynchronous batched restore returns only the consecutive hit prefix.
|