LMCache ValkeyConnector 基准测试深度解析:单键存储、GLIDE 同步客户端与 TLS 集群模式的性能实测
2026/9/16 20:15:54 网站建设 项目流程

LMCache ValkeyConnector 基准测试深度解析:单键存储、GLIDE 同步客户端与 TLS 集群模式的性能实测

【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache

导读

本文基于 examples/kv_cache_reuse/remote_backends/valkey/VALKEY_CONNECTOR_BENCHMARKING.md,完整还原 LMCache 新一代ValkeyConnector(基于 valkey-glide 同步客户端)相对旧版RedisClusterConnector的性能基准测试:包含 70B/8B 模型、8k–64k 上下文、Provisioned 与 Serverless(TLS)两种集群形态的完整 TTFT 对比矩阵,以及一套可复现的 "零前缀重叠 flood 提示词 + keyspace_hits 校验" 的 L2 基准测试方法论。读者将掌握:ValkeyConnector 的三大设计改动(零拷贝大值处理、TLS、单配置切换集群/单机模式)、如何科学测量 L2 缓存命中时的 TTFT、以及配套的 LMCache 配置与 vLLM 启动参数。


一、背景:ValkeyConnector 的三项关键改动

ValkeyConnector是 LMCache 面向 Valkey(Redis 的 BSD 开源分支)的远程 KV Cache 存储连接器,其底层客户端为valkey-glide。该连接器在加入集群模式、TLS 支持与优化的大值处理能力后,与RedisClusterConnector在同一批 Valkey 集群上完成了对照基准测试。驱动本次测试的三个关键改动如下:

  1. GLIDE 同步客户端 + 优化的大值处理——利用两个上游 GLIDE 贡献点,减少大块 KV Cache 传输时的内存拷贝:一是通过bytearray/memoryview参数实现零拷贝 SET;二是通过 buffer GET 将数据直接读入预分配内存。这两项改动消除了多 MB 级 chunk 传输时的中间分配开销。
  2. TLS 支持——允许连接启用了 TLS 的集群(包括强制要求 TLS 的 ElastiCache Serverless)。这是RedisClusterConnector无法做到的,且实测在 64k 上下文下 TLS 仅带来约 7–8% 的额外开销。
  3. 集群与单机双模式——通过一个valkey_mode配置项即可在GlideClusterClient(从种子节点自动发现拓扑)与GlideClient(单节点,可选database_id)之间切换。

关于 "零拷贝" 需要准确理解:这是 glide 官方语境下的术语,实际是copy-reduced(减拷贝)而非完全免拷贝——GET 时值仍会在 glide-core 内部物化后再写入调用方缓冲区,SET 时载荷仍会进入命令缓冲区。但相比 async 客户端因 Protobuf IPC 层导致的每次bytes复制,同步路径已经消除了中间分配。glide 上游明确文档化:zero-copy is sync-only

源码层面的印证

新连接器的实现在 valkey_connector.py 中,其模块文档说明了几个核心设计决策:

  • glide 客户端生命周期与工作线程池统一收敛在共享的ValkeyWorkerPool类(worker_pool.py),非 MP 模式的ValkeyConnector与 MP 模式的ValkeyL2Adapter复用同一套零拷贝 GET/SET/EXISTS/DELETE 实现;
  • 通过memoryview直接访问 pinned CPU 内存——线程共享父进程地址空间,无需共享内存 arena 或跨进程拷贝;
  • 单键存储(类似RESPConnector),相比旧版 2 键(metadata + kv_bytes)拆分,将每次 chunk 的 Valkey 往返次数减半;
  • 通过AsyncPQExecutor实现优先级调度(PEEK > PREFETCH > GET > PUT,见 valkey_connector.py),保证延迟敏感的查询不被批量写阻塞,与RESPConnector的优先级方案一致。

依赖方面,该连接器需要valkey-glide-sync包(>= 2.3.0提供的glide_sync模块;普通的valkey-glide包仅含 async 客户端、不满足要求。安装命令为:

pip install 'valkey-glide-sync>=2.3.0'

若未安装,会在 worker 启动时报出明确的引导错误:

Valkey support requires the glide_sync module. Install: pip install 'valkey-glide-sync>=2.3.0' (note: the plain 'valkey-glide' package is async-only)

二、硬件与软件测试环境

运行环境

组件详情
实例p4de.24xlarge— 8× A100-SXM4-80GB,96 vCPUs,1.1 TB RAM
模型meta-llama/Llama-3.1-70B-Instruct(bf16,TP=8)与Llama-3.1-8B-Instruct(bf16,TP=1)
vLLM0.17.0,配合LMCacheConnectorV1
LMCache0.1.dev1240
valkey-glidevalkey-io/valkey-glidemain 分支构建
哈希算法sha256_cbor_64bit(TP>1 时必须——Python 的hash()在 vLLM 的 subprocess 边界上不可确定)

集群后端

两组测试使用完全相同的 Valkey 集群后端——两个连接器面向同一套基础设施:

类型节点TLS实例类型
ElastiCache Provisioned10 个 primarycache.r7g.16xlarge
ElastiCache Serverless自动扩缩容托管

被测连接器

连接器客户端库存储格式每 chunk 的 GET 数
ValkeyConnectorGLIDE 同步(Rust FFI)单键(原始字节)1
RedisClusterConnectorredis-py(async)2 键(metadata + kv_bytes)2

这里 "每 chunk 的 GET 数" 的差异,正是后面keyspace_hits校验与性能差异的核心:单键存储意味着一个 chunk 只需要一次 GET 往返,而 2 键存储需要两次。

Chunk 大小

KV Cache 的 chunk 字节数取决于模型架构。chunk_size 默认按 token 计(如 256 tokens):

模型层数chunk_size (tokens)Chunk 字节计算公式
70B (TP=8)80256~10 MB2 × 80 × 256 × 1 head × 128 dim × 2 bytes (bf16)
8B (TP=1)32256~4 MB2 × 32 × 256 × 8 heads × 128 dim × 2 bytes (bf16)

注意:两个 chunk 字节数公式中的 head 数不同,是因为 TP=8 时每 rank 只持有 1 个 head 的 KV,而 TP=1 时单进程持有全部 8 个 head。这正是 KV Cache 张量并行切分的结果。


三、TTFT 测量方法

TTFT(Time To First Token)从客户端侧端到端测量,通过curl请求 vLLM 的/v1/completions接口:

START=$(date +%s%N) curl -s -X POST http://localhost:8000/v1/completions \ -H "Content-Type: application/json" \ -d @prompt_64k_70b.json > /dev/null END=$(date +%s%N) echo "TTFT: $(( (END-START)/1000000 ))ms"

该测量捕获了完整请求延迟,包括网络、tokenization、KV Cache 检索(或计算)以及首个 token 生成。测试报告三种 TTFT 变体:

  • Cold TTFT— 任何地方都没有缓存数据。vLLM 从零计算 KV Cache,然后存入 L1(CPU)+ L2(Valkey)。这是基线。
  • L1 TTFT— 数据在 CPU pinned 内存中,约 5 GB/s per rank。这不是本次测试的目标,仅作参考。
  • L2 TTFT— 数据已从 L1 逐出,从 Valkey 检索。这是目标指标。Speedup = Cold TTFT / L2 TTFT。

每 rank 吞吐量

vLLM 会为每个请求记录 per-rank 检索统计:

Retrieved 65024 out of 65024 required tokens (from 65024 total tokens). size: 2.4805 gb, cost 2558.0752 ms, throughput: 0.9697 GB/s

这是 LMCache 内部测量——从batched_get开始到所有 chunk 接收完毕的时间,按 TP rank 统计。聚合吞吐量 = per-rank × rank 数。

从源码看,batched_get的实现位于 valkey_connector.py:它会先通过local_cpu_backend.allocate为每个 key 预分配固定大小的MemoryObj,再并行向线程池提交submit_get_into,最后用asyncio.gather汇总结果。这正是 "直接读入预分配内存" 的代码路径——每个memoryview缓冲区在请求到达前就已就位。


四、为什么 L2 基准测试很难做

LMCache 有 L1(CPU)与 L2(远端)两级缓存。命中时优先查 L1。要测量 L2 的性能,必须先强制 L1 miss——即在检索请求发出前把测试数据从 L1 逐出。

前缀哈希问题

LMCache 使用滚动前缀哈希生成 chunk 键:

chunk_hash[i] = hash(chunk_hash[i-1], tokens[i*256 : (i+1)*256])

如果两个 prompt 共享 token 前缀,那么该前缀内的所有 chunk 会产生完全相同的哈希。LRU 会把这些条目视为同一项而永远不会逐出。也就是说,发送那些恰好与测试 prompt 共享前缀的 "不同" prompt,根本无法逐出测试数据——这是 L2 基准测试中最隐蔽的坑。

解法:零重叠 Flood Prompts

仓库提供了 benchmark_l2.py,该脚本使用不同随机种子生成完全不相交的随机文本作为 flood prompts,从第一个 chunk 起就保证每个 chunk 哈希唯一:

python3 benchmark_l2.py generate \ --model meta-llama/Llama-3.1-70B-Instruct \ --context-tokens 65024 \ --num-floods 3 \ --output-dir /home/ubuntu/bench_prompts

脚本会验证零重叠:

Flood 1: 65024 tokens Token prefix overlap with test: 0 (< chunk_size=256 → OK, different chunk hashes)

从源码看,benchmark_l2.py 的_random_text为每个种子使用独立的词汇空间(3–10 个随机小写字母组成的单词 + 空格),并通过seed=42(测试)与seed=1000 + i*1000(flood)拉开距离;生成后还会逐 token 计算与测试 prompt 的前缀重叠长度,若第一个 chunk(前 256 tokens)完全一致则打印 WARNING 提示更换种子。

L1 容量设置

max_local_cpu_size必须足够大以容纳检索缓冲区(否则报No eviction candidates found in local cpu backend),但又必须足够小,使 floods 能逐出测试数据:

模型上下文每 rank 数据量推荐 L1需要的 floods
70B TP=88k320 MB1 GiB3
70B TP=864k2.48 GB5 GiB3
8B TP=18k128 MB1 GiB3
8B TP=164k1.02 GB10 GiB5

注意 8B 64k 场景推荐 10 GiB L1 与 5 个 floods,是因为该场景下每 rank 数据量(1.02 GB)较大、而单进程 L1 又需要留足检索缓冲区空间,因此需要更多 flood 才能完成逐出。


五、基准测试工作流

每次基准测试严格遵循以下序列:

  1. 重启 vLLM— 清空 L1(CPU pinned 内存是进程级的)
  2. FLUSHALL— 清空所有 Valkey 集群节点
  3. Cold 请求— 发送测试 prompt;vLLM 从头计算 KV Cache,存入 L1 + L2
  4. Flood L1— 发送 3–5 个不相交的 prompts,通过 LRU 填满 L1 并逐出测试数据
  5. 记录keyspace_hits(所有集群 primary,L2 请求之前)
  6. L2 请求— 再次发送同一测试 prompt;L1 miss 强制从 Valkey 进行 L2 检索
  7. 记录keyspace_hits(所有集群 primary,L2 请求之后)
  8. 验证keyspace_hits差值确认发生了真实的集群 GET

L2 验证(双重校验)

每条 L2 结果都通过两种独立方法验证:

方法一:keyspace_hits差值(Provisioned 集群):

for node in $NODES; do redis-cli -h $node INFO stats | grep keyspace_hits done
  • ValkeyConnector:期望差值 = chunks × ranks(每 chunk 1 次 GET)
  • RedisClusterConnector:期望差值 = chunks × ranks × 1.5(每 chunk 2 次 GET)
  • 差值为 0 意味着 L1 命中——测试无效

方法二:vLLM 日志

Retrieved 65024 out of 65024 required tokens ... throughput: 0.95 GB/s
  • "Retrieved" + 亚 1 GB/s 吞吐量 = L2 命中
  • "Retrieved" + 约 5 GB/s 吞吐量 = L1 命中(不是 L2 测试)
  • 只有 "Stored" = 未发生检索

benchmark_l2.pyrun子命令(benchmark_l2.py)将上述步骤自动化:依次执行 cold 请求、发送所有 flood、记录前后keyspace_hits并计算差值,最后输出明确的裁决——差值 > 0 则输出 "✓ L2 RETRIEVAL CONFIRMED" 及 Speedup;否则输出 "✗ L2 RETRIEVAL NOT DETECTED",并列出可能原因(max_local_cpu_size过大、flood 数量不足、flood 与测试 prompt 共享前缀)。


六、测试结果

70B 模型完整对比矩阵(TP=8,10 MB chunks)

连接器后端TLS上下文Cold TTFTL2 TTFTSpeedupPer-rank聚合
ValkeyConnector(32 workers)Provisioned64k15,555ms3,216ms4.8×0.89–0.98 GB/s~7.5 GB/s
ValkeyConnector(32 workers)Serverless64k15,987ms3,425ms4.7×0.85–0.89 GB/s~6.9 GB/s
ValkeyConnector(32 workers)Provisioned8k2,224ms505ms4.4×
ValkeyConnector(32 workers)Serverless8k2,274ms656ms3.5×
RedisClusterConnectorProvisioned64k15,612ms5,794ms2.7×0.47–0.52 GB/s~4.0 GB/s
RedisClusterConnectorProvisioned8k2,361ms796ms3.0×

70B 模型 — 4 MB chunks(chunk_size=96)

连接器后端TLS上下文L2 TTFTSpeedupPer-rank聚合
ValkeyConnector(32 workers)Provisioned64k3,644ms4.5×0.87–0.93 GB/s~7.1 GB/s
ValkeyConnector(32 workers)Serverless64k3,884ms4.3×0.87–0.93 GB/s~6.9 GB/s
ValkeyConnector(64 workers)Provisioned64k4,134ms3.9×0.74–0.92 GB/s~6.6 GB/s
RedisClusterConnectorProvisioned64k6,392ms2.3×0.46–0.58 GB/s~4.1 GB/s

8B 模型(TP=1,Provisioned,4 MB chunks)

连接器上下文Cold TTFTL2 TTFTSpeedupkeyspace_hits Δ
ValkeyConnector(32 workers)8k803ms421ms1.9×+64
ValkeyConnector(32 workers)64k11,487ms2,527ms4.5×+508
RedisClusterConnector8k1,789ms1,859ms1.0×+96
RedisClusterConnector64k13,189ms15,600ms0.8×+762

汇总:在所有配置下,ValkeyConnector的 L2 检索速度相比RedisClusterConnector1.6–1.8×;在 64k 上下文下相对冷计算最多获得4.8× 加速(3.2s vs 15.6s)。


七、性能差异分析

为什么 ValkeyConnector 比 RedisClusterConnector 快

  1. 每个 chunk 1 次 GET vs 2 次 GET。ValkeyConnector 将每个 chunk 存为单键;RedisClusterConnector 拆成metadata+kv_bytes两个键,需要两次往返。keyspace_hits证实了这一点:8B 64k 场景下 ValkeyConnector 为 +508、RedisClusterConnector 为 +762——恰好是 1.5× 的 GET 数差。

  2. 32 个并行 worker 线程 + 独立客户端。每个 worker 线程拥有自己的 GLIDE 客户端和连接池。Rust FFI 调用期间 GIL 被释放,可实现真正的并行 I/O。RedisClusterConnector 使用redis-py的 async 客户端并受 asyncio semaphore 约束。

  3. 零拷贝 buffer GET。GLIDE 通过buffer=memoryview直接写入 pinned CPU 内存,避免中间分配 + 拷贝。RedisClusterConnector 从redis-py收到 bytes 后再拷贝进 memory 对象。

  4. 集群原生的槽位路由。GLIDE 的 cluster 客户端对所有集群节点维持持久连接,内部按槽位路由命令,无需客户端自行计算哈希槽或处理重定向。

源码佐证:worker 线程池的并发模型见 worker_pool.py——ValkeyWorkerPool维护一个ThreadPoolExecutor,每个 worker 通过threading.local()懒构建并缓存自己的GlideClient(standalone)或GlideClusterClient(cluster);池只讲str键与字节缓冲区,不感知任何 LMCache 类型,因此连接器与 L2 adapter 可以共享。零拷贝 GET 的具体实现是_do_get_into(worker_pool.py):它把目标缓冲区 cast 成无符号字节"B"格式的 memoryview(glide 的 buffer 协议拒绝非字节格式视图),写入线程私有 scratch 缓冲区后在同一 GIL 保护下拷入目标内存,并严格校验读取字节数必须等于缓冲区长度——任何短读、截断或超长值都被当作 miss 拒绝(GET_MISS = -1),杜绝脏尾字节流入上层。buffer GET 能力通过"buffer" in inspect.signature(client.get).parameters探测一次并缓存。

TLS 开销

上下文Provisioned(无 TLS)Serverless(TLS)开销
64k3,216ms3,425ms+6.5%
8k505ms656ms+30%

64k 时 TLS 开销可忽略,因为数据传输占主导;8k 时固定的 TLS 握手/加密成本在较小的传输中占比更大。综合两组数据,文档将其总结为 TLS 在 64k 上下文下约 7–8% 的开销。

Chunk 大小的影响

Chunk 大小L2 TTFT(Provisioned,64k)Speedup
10 MB(chunk_size=256)3,216ms4.8×
4 MB(chunk_size=96)3,644ms4.5×

10 MB chunks 只比 4 MB 快13%。更少的 chunk 意味着更少的往返,但在单请求开销本来就低的情况下,差异有限。

Worker 数量

WorkersL2 TTFT(4 MB chunks,Provisioned,64k)Per-rank
323,644ms0.87–0.93 GB/s
644,134ms0.74–0.92 GB/s

32 workers 是 70B TP=8 的甜点值。64 workers 反而因线程竞争降低了吞吐量。这也与源码一致:并发上限被num_workers卡住(默认 8,valkey_adapter.py 从extra_config读取,同时兼容旧的valkey_sync_num_workers键),在大型集群上线程池而非集群本身可能成为瓶颈——提高num_workers才能饱和更多节点。

8B 模型:RedisClusterConnector 的开销反转

在 8B 64k 场景下,RedisClusterConnector 的 L2 TTFT(15,600ms)超过了冷计算时间(13,189ms),意味着该模型规模下 2 键存储的额外开销完全抵消了缓存收益。keyspace_hits差值证实了原因:同一份数据 762 次 GET(RedisClusterConnector)vs 508 次 GET(ValkeyConnector)——1.5× 的往返数。ValkeyConnector 的单键存储避免了这一点,在相同负载下实现了 4.5× 加速。


八、关键结论

  1. ValkeyConnector 比 RedisClusterConnector 快 1.6–1.8×——在所有模型与上下文长度下成立,源于单键存储与并行 worker 线程。
  2. 64k 下 TLS 开销为 7–8%——Serverless ElastiCache 可以用于生产。
  3. Chunk 大小的影响低于预期——10 MB 只比 4 MB 快 13%。
  4. 70B TP=8 下 32 workers 最优——更多线程只会增加竞争。
  5. RedisClusterConnector 的 2 键存储在较小模型上成为瓶颈——每 chunk 多出的往返数可以完全抵消缓存收益。

九、使用的 LMCache 配置与启动命令

ValkeyConnector — Provisioned 集群

# ValkeyConnector — provisioned cluster chunk_size: 256 local_cpu: true max_local_cpu_size: 5.0 remote_url: "valkey://<cluster-endpoint>:6379" remote_serde: "naive" blocking_timeout_secs: 120 pre_caching_hash_algorithm: sha256_cbor_64bit extra_config: valkey_num_workers: 32 valkey_mode: "cluster"

ValkeyConnector — Serverless TLS

# ValkeyConnector — serverless TLS chunk_size: 256 local_cpu: true max_local_cpu_size: 5.0 remote_url: "valkey://<serverless-endpoint>:6379" remote_serde: "naive" blocking_timeout_secs: 120 pre_caching_hash_algorithm: sha256_cbor_64bit extra_config: valkey_num_workers: 32 valkey_mode: "cluster" tls_enable: true

vLLM 启动命令

export LMCACHE_CONFIG_FILE=/home/ubuntu/valkey_cluster.yaml vllm serve meta-llama/Llama-3.1-70B-Instruct \ --tensor-parallel-size 8 \ --kv-transfer-config '{"kv_connector":"LMCacheConnectorV1","kv_role":"kv_both"}' \ --no-enable-log-requests \ --no-enable-prefix-caching \ --gpu-memory-utilization 0.90 \ --max-model-len 65536

配置项参考(extra_config

仓库的 Valkey 后端文档 给出了完整的extra_config键表,与 valkey_adapter.py 的参数解析一一对应:

Key默认值说明
valkey_num_workers8工作线程数,每线程一个独立的 GLIDE 客户端连接
valkey_mode"standalone""standalone""cluster"。集群模式从种子节点自动发现拓扑
tls_enablefalse启用 TLS。ElastiCache Serverless 必须
valkey_username""认证用户名
valkey_password""认证密码
valkey_databaseNone数据库 ID(仅 standalone 模式,集群模式忽略并告警)
valkey_enable_ttlfalse功能开关。为true时每个键写入过期时间(见valkey_ttl_sec),使 Valkey/Redis 的volatile-*逐出策略能在节点达到maxmemory后回收 L2 缓存键;false(默认)则键无 TTL 永久保留
valkey_ttl_sec86400键 TTL 秒数,仅当valkey_enable_ttltrue时生效。必须是正整数
request_timeout5.0GLIDE 请求超时(秒),同时用作 Python 侧 Future 超时
connection_timeout10.0GLIDE 初始连接超时(秒)

其他值得注意的约束(均可在 valkey_adapter.py 中看到强制校验):ValkeyConnector 采用单键、定长存储,不携带逐 chunk 元数据,因此save_chunk_meta必须为falsesave_unfull_chunk必须为false,否则连接器创建时直接抛ValueError。此外,TLS 目前是 glideuse_tls的开关式支持,仅适用于证书可由系统 OS 信任库验证的场景(如 ElastiCache Serverless、Let's Encrypt 公网证书);自签名证书、私有/内部 CA 与 mTLS 属于后续规划,详见 Valkey L2 Adapter 设计文档。


十、复现与进一步探索

完整基准测试命令

仓库在 benchmark_l2.py 中提供了端到端 L2 基准脚本(依赖 vLLM +LMCacheConnectorV1transformers分词器、redis-py用于keyspace_hits校验):

# 生成测试 + flood 提示词(一次性) python3 benchmark_l2.py generate \ --model meta-llama/Llama-3.1-70B-Instruct \ --context-tokens 65536 \ --num-floods 3 \ --output-dir /home/ubuntu/bench_prompts # 运行完整基准(cold → flood → L2) python3 benchmark_l2.py run \ --prompt-dir /home/ubuntu/bench_prompts \ --vllm-url http://localhost:8000 \ --valkey-nodes <node1>,<node2>,<node3> \ --valkey-port 6379

相关仓库资源

  • 连接器实现:valkey_connector.py、valkey_adapter.py
  • 共享 worker 线程池(零拷贝 GET/SET 核心):worker_pool.py
  • MP 模式 L2 adapter:valkey_l2_adapter.py
  • 配置示例:valkey.yaml
  • 非 MP 模式配置文档:docs/source/kv_cache/storage_backends/valkey.rst
  • MP 模式 L2 存储文档:docs/source/mp/l2_storage/valkey.rst
  • 设计文档(线程模型、槽位路由、MOVED/ASK 重定向、容量与逐出策略):docs/design/v1/distributed/l2_adapters/valkey.md
  • 测试用例:tests/v1/storage_backend/test_valkey_connector.py、tests/v1/distributed/test_valkey_l2_adapter.py

适用前提提醒:文中所有性能数字均来自仓库基准文档所记录的特定环境(p4de.24xlarge、Llama 3.1 70B/8B、特定 chunk_size 与 worker 数),不同硬件、模型与集群配置下的绝对数值会变化;但单键 vs 2 键的往返数差异、32 workers 的甜点值、TLS 在长上下文下开销趋近于零等结构性结论,在复测中具有较高的可迁移参考价值。

【免费下载链接】LMCacheLMCache: Supercharge Your LLM with the Fastest KV Cache Layer项目地址: https://gitcode.com/GitHub_Trending/lm/LMCache

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询