LMCache RESP 存储后端实战:基于原生 C++ 连接器把 Redis/Valkey 打造成高性能 KV Cache 层
2026/9/15 17:33:07 网站建设 项目流程

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.cppconnector.hpybind.cpp三个文件。其中 connector.h 的注释直接点明了三条核心优化手段:

  1. 预置batch_chunk_num_bytes(避免逐字节解析\r\n);
  2. 使用预分配缓冲区的 scatter/gather 发送;
  3. 零拷贝(无 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_USERNAMERedis ACL 用户名。当 config/JSON 中未设置username时作为默认值
LMCACHE_RESP_PASSWORDRedis AUTH 密码。当 config/JSON 中未设置password时作为默认值
LMCACHE_RESP_HOSTRedis 主机名或 IP。当 config/JSON/URL 中未设置host时作为默认值
LMCACHE_RESP_PORTRedis 端口。当 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: falsemax_local_cpu_size: 20.0async_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 6555

Step 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 dummy

7.1 L2 Adapter 配置字段

--l2-adapterJSON 接受以下字段:

字段类型默认值说明
typestr(必填)必须为"resp"
hoststr(必填)Redis/Valkey 主机名或 IP
portint(必填)Redis/Valkey 端口
num_workersint8并行 I/O 的 C++ worker 线程数
usernamestr""Redis ACL 用户名(留空表示不认证)。为空时回退到LMCACHE_RESP_USERNAME环境变量
passwordstr""Redis AUTH 密码(留空表示不认证)。为空时回退到LMCACHE_RESP_PASSWORD环境变量
max_capacity_gbfloat0客户端侧用量跟踪的 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.cppconnector.hpybind.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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询