RESP (原生 Redis/Valkey)#
概述#
RESP 后端是一个高性能的原生 C++ 存储连接器,适用于 Redis 和 Valkey 服务器,使用 TCP 上的 RESP2 传输协议。它旨在实现 KV Cache 存储和检索操作的最大吞吐量,在最佳配置下读取速度可达到 6+ GB/s。
相较于标准 Redis 连接器的主要优势:
多线程 C++ I/O:工作线程并行操作,采用零拷贝缓冲区传递和完全释放全局解释器锁(GIL)
批处理切片:大型批处理操作会自动在工作线程之间拆分,以实现最大并行性
基于 eventfd 的完成:内核在完成时唤醒 Python——没有轮询开销
双模式支持:相同的 C++ 连接器在非 MP 模式(通过
ConnectorClientBase)和 MP 模式(通过NativeConnectorL2Adapter作为 L2 适配器)下均可工作。
本地 C++ 源代码位于 csrc/storage_backends/redis/。有关完整架构,请参见 添加本地连接器。
先决条件#
从源代码安装的 LMCache (
pip install -e .) 以编译 C++ 扩展支持 IO 线程的 Redis 8.2+ 或 Valkey 服务器(推荐使用 Redis 8.2)
一台至少配备一个 GPU 的机器用于 vLLM 推理
Redis 服务器设置#
重要
Redis 版本和服务器配置对吞吐量有 重大 影响。使用 IO 线程的 Redis 8.2 读取速度约为 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 线程启动服务器:
./src/redis-server \
--protected-mode no \
--save '' \
--appendonly no \
--io-threads 4 \
--port 6379
标志 |
为什么 |
|---|---|
|
允许来自其他主机的连接(在生产环境中使用认证) |
|
禁用持久性 -- KV Cache 是短暂的,持久性会浪费带宽 |
|
启用多线程 I/O 以实现并行读/写处理 |
|
默认端口(如果运行多个实例,请调整) |
小技巧
--io-threads 的数量应大致与 Redis 进程可用的物理核心数量相匹配。4 是一个不错的起点;请根据您的硬件进行基准测试以找到最佳值。
块大小选择与吞吐量调优#
块大小(以令牌为单位)决定了每个 Redis 键值对所占用的字节数。这是 吞吐量最重要的参数。
最佳块大小约为 4 MB。 较小或较大的块都会降低吞吐量:
块大小 |
总数据 |
设置吞吐量 |
获取吞吐量 |
|---|---|---|---|
1 MB (500 个键) |
500 MB |
约 3.5 GB/s |
~5.2 GB/s |
4 MB (500 个键) |
2 GB |
~4.4 GB/s |
~5.9 GB/s |
8 MB (200 个键) |
1.6 GB |
约 4.2 GB/s |
~1.4 GB/s |
为什么是 4 MB?
在 ~2 MB 以下,每个键的开销(RESP 帧、TCP 往返时间)占主导地位
超过 ~4 MB 时,Redis 服务器端内存分配和 TCP 窗口大小成为瓶颈。
在 4 MB 时,摊销开销与内存压力之间的平衡是最佳的。
计算块大小(以 token 为单位):
块大小(以字节为单位)取决于模型的隐藏维度、KV 头的数量、层数和数据类型:
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
# But typically chunk_size is set as token count in config:
chunk_size: 16 # ~2 MB per chunk (conservative)
chunk_size: 32 # ~4 MB per chunk (optimal for throughput)
备注
每个令牌的字节计算因模型架构而异。较大的模型(例如,70B)具有更多的层和更大的隐藏维度,因此每个块所需的令牌数量较少,以达到 4 MB 的最佳点。
吞吐量扫描#
要找到适合您硬件的最佳配置,请使用随附的基准测试:
cd examples/kv_cache_reuse/remote_backends/resp
# Sweep chunk sizes
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
# Sweep worker counts
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
预期输出:
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
环境变量配置#
敏感凭据(可选的主机/端口)可以通过环境变量提供,而不是配置文件或 CLI 参数。这可以防止在启动时将秘密记录到配置日志中。
变量 |
描述 |
|---|---|
|
Redis ACL 用户名。当 |
|
Redis AUTH 密码。当 config/JSON 中未设置 |
|
Redis 主机名或 IP。当配置/JSON/URL 中未设置 |
|
Redis 端口。当 |
配置文件(非多租户)和 --l2-adapter JSON(多租户)优先于环境变量。环境变量作为默认值使用——当相应的配置值为空或未设置时使用。它们在适配器创建时被读取,因此**从不存储在配置对象中**,并且**从不在启动日志中打印**。
示例 — 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
小技巧
在生产环境中,始终使用环境变量来存储凭据,而不是将其嵌入配置文件或命令行参数中。
非 MP 模式(单进程)#
在非多进程模式下,RESP 连接器通过 RESPClient asyncio 包装器直接用作远程存储后端。
配置文件 (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
备注
save_unfull_chunk 必须关闭(默认)并且必须禁用块元数据保存,以便在使用原生 RESP 连接器时实现最佳吞吐量。
MP 模式(多进程)#
在 MP 模式下,LMCache 作为一个独立的服务器进程运行,通过 ZMQ 与 vLLM 进行通信。RESP 连接器作为一个具有可变大小块支持的 L2 适配器。
**步骤 1:启动 Redis**(请参见上面的 Redis Server Setup)
步骤 2:启动 LMCache MP 服务器:
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
步骤 3:启动带有 LMCache MP 连接器的 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
L2 适配器配置#
--l2-adapter JSON 接受以下字段:
字段 |
类型 |
默认 |
描述 |
|---|---|---|---|
|
字符串 |
(必需) |
必须是 |
|
字符串 |
(必需) |
Redis/Valkey 主机名或 IP |
|
整数 |
(必需) |
Redis/Valkey 端口 |
|
整数 |
8 |
用于并行 I/O 的 C++ 工作线程 |
|
字符串 |
|
Redis ACL 用户名(留空以不进行身份验证)。如果为空,将回退到 |
|
字符串 |
|
Redis AUTH 密码(留空表示不进行身份验证)。如果为空,将回退到 |
|
浮点数 |
0 |
用于客户端使用跟踪的最大 L2 存储容量(以 GB 为单位)。这是 L2 逐出的必要条件。设置为 0(默认值)以禁用使用跟踪。 |
L2 逐出#
要启用在 Redis 后端填满时自动逐出最近最少使用的键,请设置 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)。
备注
max_capacity_gb 启用 客户端 大小跟踪。它并不配置 Redis 服务器的 maxmemory 设置。您应该将 max_capacity_gb 设置为与 Redis 服务器的可用内存相匹配或略低于该值。
测试设置#
两次发送相同的提示。第一次请求将 KV Cache 存储到 Redis;第二次请求检索它。
PORT=8000
PROMPT="$(printf 'Elaborate the significance of KV cache in language models. %.0s' {1..1000})"
# First request: store
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}'
# Second request with same prefix: retrieve from 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``(或更多,匹配可用核心)
禁用持久化(
--save '' --appendonly no)以支持 KV Cache 工作负载在多插槽系统上将 Redis 固定到其自己的 NUMA 节点
在生产环境中,使用
--requirepass启用身份验证,并通过LMCACHE_RESP_USERNAME/LMCACHE_RESP_PASSWORD环境变量提供凭据,以避免将其记录到日志中。
客户端调优:
从
num_workers: 8开始,如果服务器有多余的 CPU 并且网络没有饱和,则可以增加该值。当块大小较小时,更多的工作线程会有所帮助(每批次更多的键 = 需要更多的并行性)
在 NUMA 系统上,确保 LMCache 进程与 NIC 运行在同一个 NUMA 节点上。
块大小:
每个块目标约 4 MB,以实现最大吞吐量
使用模型的每个 token 字节大小计算 token 数量(请参见上面的公式)
如果不确定,请运行基准测试以找到适合您特定硬件的最佳值。
网络:
在单机部署中使用 localhost 或回环地址
对于跨机器设置,确保低延迟网络(理想情况下 <100 微秒 RTT)
RESP 连接器使用 TCP;目前不支持 RDMA(考虑使用 Mooncake 进行 RDMA)。
附加资源#
基准测试脚本:
examples/kv_cache_reuse/remote_backends/resp/benchmark_resp_client.pyC++ 源码:
csrc/storage_backends/redis/本地连接器架构:
csrc/storage_backends/README.md添加新原生连接器的开发者指南: 添加原生连接器