LMCache RESP 存储后端实战:基于原生 C++ 连接器把 Redis/Valkey 打造成高性能 KV Cache 层
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 项目中的 RESPNative 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 连接器RESPREdis 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 会话并行收发缓冲区零拷贝传递并且完整释放 GILPython 侧不会被阻塞批量分片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暴露为LMCacheRedisClientpybind.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.2git 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 个 workerChunk 大小总数据量SET 吞吐GET 吞吐1 MB500 keys500 MB~3.5 GB/s~5.2 GB/s4 MB500 keys2 GB~4.4 GB/s~5.9 GB/s8 MB200 keys1.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 数量、层数和 dtypebytes_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-adapterJSONMP优先于环境变量。环境变量只是默认值——仅在对应配置值为空或未设置时生效。它们在 adapter 内部创建时被读取因此永远不会存入 config 对象也永远不会打印到启动日志。MP 模式使用环境变量示例export LMCACHE_RESP_USERNAMEdefault export LMCACHE_RESP_PASSWORDsecret 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_USERNAMEdefault export LMCACHE_RESP_PASSWORDsecret LMCACHE_CONFIG_FILEresp-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.yamlchunk_size: 16 remote_url: resp://localhost:6379 remote_serde: naive凭据可以通过环境变量推荐或配置文件中的extra_config设置。启动 vLLMLMCACHE_CONFIG_FILEresp-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: falseRESP 后端也支持异步加载、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 Serverlmcache 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 启动 vLLMPORT8000 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 接受以下字段字段类型默认值说明typestr必填必须为resphoststr必填Redis/Valkey 主机名或 IPportint必填Redis/Valkey 端口num_workersint8并行 I/O 的 C worker 线程数usernamestrRedis ACL 用户名留空表示不认证。为空时回退到LMCACHE_RESP_USERNAME环境变量passwordstrRedis 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 读取。PORT8000 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 usRESP 连接器走 TCP暂不支持 RDMARDMA 场景请参考 Mooncake 后端文档。十、深入阅读基准脚本benchmark_resp_client.py配套完整示例见 resp 示例目录 README 与 resp.yaml 配置示例C 源码csrc/storage_backends/redis/含connector.cpp、connector.h、pybind.cppPython 客户端封装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),仅供参考