LMCache RESP 存储后端实战:基于原生 C++ 连接器把 Redis/Valkey 打造成高性能 KV Cache 层
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
导读
本文聚焦 LMCache 项目中的 RESP(Native Redis/Valkey)存储后端,讲解如何把 Redis 8.2+ / Valkey 通过一个多线程、零拷贝的原生 C++ 连接器变成 LLM KV Cache 的高吞吐远端存储层。文章涵盖服务器与客户端两侧的完整配置(IO threads、chunk size、worker 数量、认证、L2 驱逐),并给出单进程(non-MP)与多进程(MP)两种部署模式的逐步命令;读完你可以在自己的 GPU 机器上复现 6+ GB/s 级别的 KV 读写吞吐,并学会用仓库自带的基准脚本对硬件做调参扫描。
一、RESP 后端是什么:原生 C++ 的 Redis/Valkey 连接器
RESP(REdis Serialization Protocol)后端是 LMCache 为 Redis 与 Valkey 服务器提供的高性能原生 C++ 存储连接器,通过 TCP 上的 RESP2 线协议通信,针对 KV cache 的存取场景做了极致的吞吐优化。在文档记录的测试条件下(Redis 8.2 + 8 个 worker),读取吞吐可以达到6+ GB/s。
与标准 Redis 连接器相比,RESP 后端的关键优势体现在四个方面:
- 多线程 C++ I/O:每个 worker 线程持有独立的 TCP 会话并行收发,缓冲区零拷贝传递,并且完整释放 GIL,Python 侧不会被阻塞;
- 批量分片(Batched tiling):大批量操作会被自动切分到多个 worker 线程,最大化并行度;
- eventfd 完成通知:内核通过 eventfd 直接唤醒 Python 侧的 asyncio 事件循环,无轮询开销;
- 双模式复用:同一个 C++ 连接器既能以
ConnectorClientBase子类形式工作在非 MP 模式,也能作为NativeConnectorL2Adapter形式的 L2 adapter 工作在 MP 模式。
原生 C++ 源码位于 csrc/storage_backends/redis/,包含connector.cpp、connector.h、pybind.cpp三个文件。其中 connector.h 的注释直接点明了三条核心优化手段:
- 预置
batch_chunk_num_bytes(避免逐字节解析\r\n); - 使用预分配缓冲区的 scatter/gather 发送;
- 零拷贝(无 bounce buffer)。
从源码结构还可以看到,每个 worker 线程对应一个WorkerConn(一个实现 RESP2 的 TCP 会话),它会预计算GET/SET/EXISTS/DEL的协议前缀、预分配 key 头部与尺寸头部缓冲区(connector.h),避免重复动态分配——这些都是吞吐优化的底层细节。pybind.cpp通过PYBIND11_MODULE(lmcache_redis, m)把RedisConnector暴露为LMCacheRedisClient(pybind.cpp),其 Python 包装是RESPClient。
二、前置条件
使用 RESP 后端需要满足:
- LMCache从源码安装(
pip install -e .),以编译 C++ 扩展。这一点在代码中也有体现:若未编译扩展,RESPClient的构造函数会直接抛出RuntimeError("RESPClient requires the C++ Redis extension. Build with: pip install -e .")(见 resp_client.py); - 一台Redis 8.2+ 或 Valkey服务器(推荐 Redis 8.2,因其支持 IO threads);
- 至少一块 GPU 用于 vLLM 推理。
三、Redis 服务器设置:版本与 IO threads 决定吞吐量级
重要:Redis 版本和服务器配置对吞吐有决定性影响。文档实测数据是:Redis 8.2 开启 IO threads 后读吞吐约6 GB/s,而 Redis 6.0 默认配置下仅约1.5 GB/s。
推荐从源码构建 Redis 8.2:
git clone https://github.com/redis/redis.git cd redis git checkout 8.2 make -j开启 IO threads 启动服务器:
./src/redis-server \ --protected-mode no \ --save '' \ --appendonly no \ --io-threads 4 \ --port 6379各标志的作用如下表:
| 标志 | 作用 |
|---|---|
--protected-mode no | 允许来自其他主机的连接(生产环境请配合认证使用) |
--save '' --appendonly no | 关闭持久化——KV cache 是易失数据,持久化只会浪费带宽 |
--io-threads 4 | 开启多线程 I/O,并行处理读写 |
--port 6379 | 默认端口(多实例时自行调整) |
提示:
--io-threads的数量应大致匹配 Redis 进程可用的物理核心数。4 是一个不错的起点,建议结合你的硬件基准测试找到最优值。
四、块大小选择与吞吐调优:4 MB 是最佳甜蜜点
块大小(chunk size,以 token 计)决定每个 Redis key-value 对承载多少字节,是影响吞吐量的最重要参数。
文档给出的结论非常明确:每个 chunk 约 4 MB 是最佳点,更小或更大都会导致吞吐下降。以下是文档记录的实测数据(Redis 8.2、8 个 worker):
| Chunk 大小 | 总数据量 | SET 吞吐 | GET 吞吐 |
|---|---|---|---|
| 1 MB(500 keys) | 500 MB | ~3.5 GB/s | ~5.2 GB/s |
| 4 MB(500 keys) | 2 GB | ~4.4 GB/s | ~5.9 GB/s |
| 8 MB(200 keys) | 1.6 GB | ~4.2 GB/s | ~1.4 GB/s |
为什么是 4 MB?
- 低于约 2 MB 时,单 key 开销(RESP 帧头、TCP 往返)占主导;
- 高于约 4 MB 时,Redis 服务端内存分配和 TCP 窗口大小成为瓶颈;
- 4 MB 正好在摊销开销与内存压力之间取得最佳平衡。
chunk size 的 token 数如何计算?
chunk 的字节数取决于模型的隐藏维度、KV head 数量、层数和 dtype:
bytes_per_token = 2 * num_kv_heads * head_dim * num_layers * dtype_bytes以meta-llama/Llama-3.1-8B-Instruct+ BFloat16 为例:
bytes_per_token = 2 * 8 * 128 * 32 * 2 = 131,072 bytes (~128 KB) chunk_size_tokens = 4 MB / 128 KB = 32 tokens # 但配置里 chunk_size 通常直接填 token 数: chunk_size: 16 # ~2 MB per chunk(保守) chunk_size: 32 # ~4 MB per chunk(吞吐最优)注意:每 token 字节数因模型架构而异。更大规模的模型(如 70B)层数更多、隐藏维度更大,达到 4 MB 甜蜜点所需的 token 数更少。
仓库自带的 resp.yaml 也印证了这一调优结论,其注释明确写着:Redis 高吞吐传输的"黄金点"约 4 MB(更高或更低都会造成性能退化,对于meta-llama/Llama-3.1-8B-Instruct这类模型约为 16 tokens),因此示例配置取chunk_size: 16。
4.1 吞吐扫描(Throughput Sweep)
要为自己的硬件找到最优配置,可以直接使用仓库内置的基准脚本 benchmark_resp_client.py。该脚本通过RESPClient依次执行批量 SET、批量 GET 和批量 EXISTS,并校验数据一致性(assert all(read_bufs[i] == buffers[i] ...),见 benchmark_resp_client.py)。
cd examples/kv_cache_reuse/remote_backends/resp # 扫描 chunk 大小 for mb in 0.5 1 2 4 8; do echo "=== Chunk: ${mb} MB ===" python benchmark_resp_client.py \ --host 127.0.0.1 --port 6379 \ --chunk-mb $mb --num-workers 8 --num-keys 500 done # 扫描 worker 数量 for w in 1 2 4 8 16; do echo "=== Workers: $w ===" python benchmark_resp_client.py \ --host 127.0.0.1 --port 6379 \ --chunk-mb 4 --num-workers $w --num-keys 500 done脚本支持的全部参数(见 benchmark_resp_client.py):--host(默认127.0.0.1)、--port(默认6379)、--chunk-mb(默认4.0)、--num-workers(默认8)、--num-keys(默认500),以及可选的--username/--password用于认证场景。仅做冒烟验证时可直接运行python benchmark_resp_client.py使用全部默认值。
预期输出示例:
Redis RESP Client Benchmark Server: 127.0.0.1:6379, Workers: 8 Chunk size: 4096KB, Keys: 500 ------------------------------------------------------------ Batch SET: 4.36 GB/s (1.95 GB written) Batch GET: 5.91 GB/s (1.95 GB read) Batch EXISTS: 143528 ops/s (500/500 hits) ------------------------------------------------------------ All tests passed五、环境变量配置:让凭据远离日志
敏感凭据(以及可选的 host/port)可以通过环境变量提供,避免它们出现在启动时的日志配置中。支持的环境变量如下:
| 变量 | 说明 |
|---|---|
LMCACHE_RESP_USERNAME | Redis ACL 用户名。当 config/JSON 中未设置username时作为默认值 |
LMCACHE_RESP_PASSWORD | Redis AUTH 密码。当 config/JSON 中未设置password时作为默认值 |
LMCACHE_RESP_HOST | Redis 主机名或 IP。当 config/JSON/URL 中未设置host时作为默认值 |
LMCACHE_RESP_PORT | Redis 端口。当 config/JSON/URL 中未设置port时作为默认值 |
优先级规则:配置文件(非 MP)和--l2-adapterJSON(MP)优先于环境变量。环境变量只是默认值——仅在对应配置值为空或未设置时生效。它们在 adapter 内部创建时被读取,因此永远不会存入 config 对象,也永远不会打印到启动日志。
MP 模式使用环境变量示例:
export LMCACHE_RESP_USERNAME="default" export LMCACHE_RESP_PASSWORD="secret" lmcache server \ --l1-size-gb 10 \ --eviction-policy LRU \ --chunk-size 16 \ --l2-adapter '{"type": "resp", "host": "localhost", "port": 6379, "num_workers": 8}' \ --port 6555非 MP 模式使用环境变量示例:
export LMCACHE_RESP_USERNAME="default" export LMCACHE_RESP_PASSWORD="secret" LMCACHE_CONFIG_FILE=resp-config.yaml \ vllm serve meta-llama/Llama-3.1-8B-Instruct \ --kv-transfer-config '{"kv_connector":"LMCacheConnectorV1", "kv_role":"kv_both"}' \ --no-enable-prefix-caching \ --load-format dummy提示:生产环境部署时,始终优先用环境变量承载凭据,而不是把它们写进配置文件或命令行参数。这一建议在示例配置 resp.yaml 中也有体现:配置内直接标注了prefer environment variables to keep secrets out of logs,并提醒若写入配置则会出现在日志中。
六、非 MP 模式(单进程):直接作为远端存储后端
在非 MP 模式下,RESP 连接器通过RESPClientasyncio 包装直接作为远端存储后端使用。RESPClient继承自ConnectorClientBase,其 asyncio 集成方式很值得一提:构造函数里取出原生客户端的 eventfd 并注册到事件循环(self.loop.add_reader(self._fd, self._on_ready),见 connector_client_base.py),当 C++ worker 线程完成操作后,_on_ready回调通过drain_completions()一次性取出所有完成项并结算对应的 asyncio Future——这就是"内核唤醒、无轮询"机制在 Python 侧的具体实现。同时,_pending字典中的 keepalive 引用保证传给原生代码的缓冲区在 C++ 线程仍持有裸指针期间不会被垃圾回收(connector_client_base.py)。
配置文件(resp-config.yaml):
chunk_size: 16 remote_url: "resp://localhost:6379" remote_serde: "naive"凭据可以通过环境变量(推荐)或配置文件中的extra_config设置。
启动 vLLM:
LMCACHE_CONFIG_FILE=resp-config.yaml \ vllm serve meta-llama/Llama-3.1-8B-Instruct \ --kv-transfer-config '{"kv_connector":"LMCacheConnectorV1", "kv_role":"kv_both"}' \ --no-enable-prefix-caching \ --load-format dummy注意:使用原生 RESP 连接器获得最优吞吐时,
save_unfull_chunk必须保持关闭(默认即关闭),并且必须关闭 chunk 元数据保存。这一点在 resp.yaml 中有对应体现:save_chunk_meta: False # make sure we have fixed size payloads——关闭元数据保存是为了保证 payload 尺寸固定,便于 C++ 层按预置字节数快速解析。
完整的单进程示例配置可以直接参考仓库自带的 resp.yaml,它额外展示了local_cpu: false、max_local_cpu_size: 20.0、async_loading: false(RESP 后端也支持异步加载)、blocking_timeout_secs: 120以及extra_config.resp_num_threads: 8(默认即 8)等可调项。
七、MP 模式(多进程):RESP 作为 L2 Adapter
在 MP 模式下,LMCache 作为独立服务器进程运行,通过 ZMQ 与 vLLM 通信。此时 RESP 连接器以 L2 adapter 形态工作,并支持可变尺寸 chunk。
Step 1:启动 Redis(参见上文"Redis 服务器设置")
Step 2:启动 LMCache MP Server:
lmcache server \ --l1-size-gb 10 \ --eviction-policy LRU \ --chunk-size 16 \ --l2-adapter '{"type": "resp", "host": "localhost", "port": 6379, "num_workers": 8}' \ --port 6555Step 3:用 LMCache MP Connector 启动 vLLM:
PORT=8000 vllm serve meta-llama/Llama-3.1-8B-Instruct \ --kv-transfer-config '{ "kv_connector": "LMCacheMPConnector", "kv_role": "kv_both", "kv_connector_extra_config": { "lmcache.mp.host": "tcp://localhost", "lmcache.mp.port": 6555 } }' \ --no-enable-prefix-caching \ --port $PORT \ --load-format dummy7.1 L2 Adapter 配置字段
--l2-adapterJSON 接受以下字段:
| 字段 | 类型 | 默认值 | 说明 |
|---|---|---|---|
type | str | (必填) | 必须为"resp" |
host | str | (必填) | Redis/Valkey 主机名或 IP |
port | int | (必填) | Redis/Valkey 端口 |
num_workers | int | 8 | 并行 I/O 的 C++ worker 线程数 |
username | str | "" | Redis ACL 用户名(留空表示不认证)。为空时回退到LMCACHE_RESP_USERNAME环境变量 |
password | str | "" | Redis AUTH 密码(留空表示不认证)。为空时回退到LMCACHE_RESP_PASSWORD环境变量 |
max_capacity_gb | float | 0 | 客户端侧用量跟踪的 L2 最大容量(GB)。启用 L2 驱逐必需。设为 0(默认)则禁用用量跟踪 |
7.2 L2 驱逐(Eviction)
当 Redis 后端存满时,若希望自动驱逐最久未使用(LRU)的 key,需要设置max_capacity_gb并追加"eviction"块:
lmcache server \ --l1-size-gb 10 \ --eviction-policy LRU \ --chunk-size 16 \ --l2-adapter '{ "type": "resp", "host": "localhost", "port": 6379, "num_workers": 8, "max_capacity_gb": 10, "eviction": { "eviction_policy": "LRU", "trigger_watermark": 0.8, "eviction_ratio": 0.2 } }' \ --port 6555该配置声明了 10 GB 容量上限。当用量超过 80%(trigger_watermark)时,驱逐控制器会通过 Redis 的DEL命令删除约 20%(eviction_ratio)最久未使用的 key。
注意:
max_capacity_gb启用的是客户端侧的容量跟踪,它并不会配置 Redis 服务器的maxmemory。你应该把max_capacity_gb设置为等于或略低于 Redis 服务器的可用内存。
八、验证整套部署:两次相同请求,一次命中缓存
向 vLLM 发送两次完全相同的 prompt。第一次请求会把 KV cache 写入 Redis,第二次请求则直接从 Redis 读取。
PORT=8000 PROMPT="$(printf 'Elaborate the significance of KV cache in language models. %.0s' {1..1000})" # 第一次请求:写入 curl -s -X POST http://localhost:${PORT}/v1/completions \ -H "Content-Type: application/json" \ -d '{"model":"meta-llama/Llama-3.1-8B-Instruct","prompt":"'"$PROMPT"'","max_tokens":10}' # 第二次相同前缀请求:从 Redis 读取 curl -s -X POST http://localhost:${PORT}/v1/completions \ -H "Content-Type: application/json" \ -d '{"model":"meta-llama/Llama-3.1-8B-Instruct","prompt":"'"$PROMPT"'","max_tokens":10}'验证数据确实写入:
redis-cli -p 6379 DBSIZE两次运行之间清空状态:
redis-cli -p 6379 FLUSHALL九、最佳实践
服务器部署:
- 使用 Redis 8.2+,并开启
--io-threads 4(或更多,匹配可用核心数); - 对 KV cache 工作负载关闭持久化(
--save '' --appendonly no); - 多路 socket 系统上,把 Redis 固定到独立的 NUMA 节点;
- 生产环境用
--requirepass开启认证,并通过LMCACHE_RESP_USERNAME/LMCACHE_RESP_PASSWORD环境变量提供凭据,避免泄露到日志。
客户端调优:
- 从
num_workers: 8起步;若服务器还有空闲 CPU 且未打满网络,可继续增加; - chunk 越小时越需要更多 worker(每个 batch 的 key 更多,需要的并行度更高);
- NUMA 系统上,确保 LMCache 进程与 NIC 运行在同一 NUMA 节点。
Chunk size:
- 以每 chunk 约 4 MB 为目标获得最大吞吐;
- 用上文公式按模型的每 token 字节数换算成 token 数;
- 拿不准时就跑一遍基准扫描,为你的具体硬件找最优值。
网络:
- 单机部署使用 localhost/loopback;
- 跨机部署要保证低延迟网络(理想 RTT < 100 us);
- RESP 连接器走 TCP,暂不支持 RDMA;RDMA 场景请参考 Mooncake 后端文档。
十、深入阅读
- 基准脚本:benchmark_resp_client.py,配套完整示例见 resp 示例目录 README 与 resp.yaml 配置示例;
- C++ 源码:csrc/storage_backends/redis/(含
connector.cpp、connector.h、pybind.cpp); - Python 客户端封装:resp_client.py 与 asyncio 基类 connector_client_base.py;
- 原生连接器架构总览:csrc/storage_backends/README.md;
- 新增原生连接器的开发指南:Adding Native Connectors。
【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考