ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

使用 vLLM、LMCache 和 Ceph 进行 KV 缓存:把缓存命中率与首 token 延迟一起压下来

使用 vLLM、LMCache 和 Ceph 进行 KV 缓存:把缓存命中率与首 token 延迟一起压下来 1. 为什么单机 KV 缓存救不了长上下文推理长上下文推理的成本结构很多人第一次算都会算错。预填充阶段是计算密集型的解码阶段是内存带宽密集型的这两个阶段的瓶颈完全不同。当你的服务开始承接 32K、64K 甚至 128K 上下文的请求时真正拖慢首 token 延迟TTFT的往往不是 GPU 算力不够而是同一段前缀被反复重新计算。我见过一个典型场景一套 RAG 服务系统提示词加检索文档前缀稳定在 20K token 左右用户问题只有几十个 token。每个请求进来vLLM 都要把这 20K 前缀重新跑一遍预填充。GPU 利用率看着很高但大部分算力都花在了重复劳动上。这时候你加卡、加张量并行TTFT 确实会降但降得不成比例——因为你在用最贵的资源做最廉价的重复计算。KV 缓存要解决的就是这件事。把已经算过的 key/value 权重存下来下次遇到相同前缀直接取回跳过预填充。vLLM 本身有分层缓存先查 GPU 显存里的块未命中查 CPU 内存再未命中才走 KV 连接器去外部存储找。问题在于单机的 GPU 显存和 CPU 内存都是有限的而且进程一重启缓存全丢。多副本部署时A 副本算过的前缀B 副本完全不知道各自重复计算。所以工程上真正要做的是把 KV 缓存从单机内存扩展到跨节点共享存储。LMCache 在这里扮演缓存层通过 vLLM 的 KV 连接器接口对接Ceph 作为共享存储后端提供对象存储语义。这套组合的关键收益有两个一是缓存命中率上去之后TTFT 直接下降二是缓存块的内容可寻址设计让多个 vLLM 实例之间不需要协调就能共享同一份缓存。适合谁看这篇正在跑 vLLM 推理服务、上下文长度超过 8K、有多副本或频繁重启需求的团队。如果你只是本地跑个 7B 模型玩玩这套东西的复杂度不值得。但只要你的服务开始吃长上下文或者你发现 GPU 账单里预填充占比过高那就该认真考虑跨节点 KV 缓存了。下面我会按实际落地顺序拆先讲清楚 LMCache 和 Ceph 的对接路径再给可复制的配置片段然后是压测验证方法最后是几个我踩过的报错。中间会穿插怎么用 TaoToken 统一 API 通道做端到端联调避免你在多套 Key 之间来回切换。2. LMCache 与 Ceph 对接缓存写入读取路径拆解2.1 内容可寻址设计为什么重要LMCache 和 vLLM 都用 token 序列的哈希值作为缓存块标识符。vLLM 通过 KV 连接器把感兴趣的块哈希传过去LMCache 返回一个位掩码告诉它哪些块能提供。这个设计最妙的地方在于不需要持久化缓存块映射表多个 vLLM 加 LMCache 实例跑在不同主机上也完全不需要协调。对 Ceph 来说每个块标识符就是一次对象查询。块存在就命中不存在就未命中。存储系统可以用生命周期配置做基于时间的过期删掉的块自动变成未命中。你得到的是一个完全弹性的、内容可寻址的 KV 缓存块存储。熟悉 Ceph 的人会立刻意识到这本质上是在计算数据位置而不是执行查找。块大小需要特别注意。vLLM 默认 16 个 token 一块LMCache 默认 256 个 token 一块。为什么 LMCache 用更大的块为了减少管理大量块引用的开销同时更好地分摊每个块的传输开销。换算成字节每个 token 的字节数取决于模型的隐藏大小、键值头数量、隐藏层数量、头维度和数据类型大小。以 Qwen3-32B 为例256 token 的块大约是 62.5 MiB。这个数字直接决定了你在 Ceph 侧要按多大的对象来规划存储池和网络带宽。2.2 两条连接器路径原生 S3 与 NIXLLMCache 对接 Ceph 有两条路。第一条是原生 S3 连接器用 AWS 通用运行时库CRT客户端连接池里的连接会多路复用到对象存储 FQDN 的 DNS 响应返回的端点。这条路的优点是接入门槛低任何有 S3 兼容接口的 Ceph 集群都能用。但 CRT 在 Python 里只支持 recv_filepath 和 send_filepath这限制了 LMCache 把 GetObject 响应体直接流式传输到 LocalCPUBackend 分配的页锁定内存缓冲区。连接器的解法是在挂载到 /dev/shm 的 tmpfs 上预分配并 mmap 文件每个并发请求一个让 CRT 客户端传内存映射文件的文件描述符再从对应缓冲区 memcpy 到用于 DMA 传输到 GPU 的页锁定缓冲区。这是个巧妙的绕行方案但要做到真正零拷贝还得改绑定层。第二条路是 NIXL 路径。LMCache PR#1939 引入了直接把 S3 数据读到页锁定 NIXL 缓冲区的能力绕过了 /dev/shm 上的文件和相关的内存复制。它还引入了一个存在缓存消除用于判断给定序列是否有缓存块的冗余 GetObjectInfo 请求。NIXL obj 插件本身需要预分配的对象键池还需要 LMCache 控制器或 Dynamo KVBM 维护每个缓存块的设备 ID、偏移量和长度信息。PR1939 的解法是保留内容可寻址方法不用对象键池也不跟踪缓存块元数据。NIXL 剩下的缺点是用了 S3Client 而不是 S3CrtClient后者支持跨 S3 端点的多路径。选哪条如果你刚开始做先用原生 S3 连接器跑通验证命中率和 TTFT 收益。等确认这套架构对你的工作负载有效再考虑切 NIXL 路径压榨最后那点拷贝开销。2.3 Ceph 侧的关键配置Ceph 这边存储池要在初始化 RGW 服务之前预创建。数据池、索引池、非 EC 池分开建副本数按你的可靠性要求设。RGW 服务用集中器模式每台主机跑多个实例一个集中器绑定到主机 IP 的 80 端口。流量管理是容易被忽略的一环。LMCache 期望一个单一的 S3 端点但你要最大化到存储集群的带宽就得让这个 FQDN 解析出多个记录。用 Hashicorp Consul 加 CoreDNS 返回多条 DNS 记录正好和 LMCache 原生 S3 连接器用的 AWS CRT 库配合。CRT 会把连接多路复用到 DNS 响应返回的所有端点等于自动做了负载均衡。验证方法很简单dig 一下你的对象 FQDN看到返回 4 条 A 记录就对了。这一步不做你的 S3 流量全压在一个 RGW 实例上带宽上不去缓存命中率再高也白搭。2.4 用 TaoToken 统一接入通道做联调端到端联调时模型服务这边你可能同时要对接多个模型、多套 Key。TaoToken 在这里的价值是提供一个统一的 Key 和 API 通道把模型对话、Coding Plan、控制台、API Keys 管理都收在一个入口下。你可以在 https://taotoken.net/api 拿到兼容的 API 地址然后在控制台生成 Key用同一个 Key 去调不同的模型做对比测试。具体操作访问 https://taotoken.net/api-keys 生成你的 API Key模型 ID 按你实际要联调的填比如 Qwen 系列或 Claude 系列。Base URL 用 https://taotoken.net/apiKey 填刚生成的Model ID 填你要验证的模型。这样你在压测 LMCache 命中率的同时可以快速切换不同模型验证缓存行为是否一致不用为每个模型单独配一套凭证。如果你要长期跑编码类或 Agent 类负载可以看下 Coding Plan 页面把常用模型的调用通道固定下来。联调阶段建议先用模型对话页面确认通道通不通再进压测。3. 可复制的 LMCache 与 vLLM 配置片段3.1 凭证文件先配 S3 凭证。路径按你实际环境放内容如下[lmcache] region default endpoint_url http://s3.cephlab.com:80 aws_access_key_id xxx aws_secret_access_key yyy response_checksum_validation when_required preferred_transfer_client crt注意 endpoint_url 指向你的 Ceph RGW 集中器地址preferred_transfer_client 设成 crt 才会走多路复用。3.2 原生 S3 连接器配置chunk_size: 256 local_cpu: False max_local_cpu_size: 100 remote_url: s3://lmcache.s3.cephlab.com save_unfull_chunk: False enable_async_loading: True remote_serde: naive blocking_timeout_secs: 100 extra_config: s3_max_io_concurrency: 1024 s3_max_inflight_reqs: 1024 s3_prefer_http2: False s3_region: default s3_enable_s3express: False save_chunk_meta: False s3_file_prefix: testchunk_size 256 对应 256 个 token 一块。local_cpu 设 False 表示不启用本地 CPU 缓存全部走远程。s3_max_io_concurrency 和 s3_max_inflight_reqs 都拉到 1024是为了压满网络带宽。save_chunk_meta 设 False 减少元数据开销。3.3 NIXL 路径配置chunk_size: 512 local_cpu: false max_local_cpu_size: 50 remote_serde: naive nixl_buffer_size: 1073741824 nixl_buffer_device: cpu extra_config: enable_nixl_storage: true nixl_backend: OBJ nixl_pool_size: 512 nixl_backend_params: endpoint_override: http://s3.cephlab.com access_key: CR98FOT054QZJ60NR7E3 secret_key: 15CTFkiAdwPkkiSh4gOlQ5zF14KZ0uCnZloYVo3w scheme: http region: default req_checksum: required bucket: lmcachenixl_buffer_size 设 1GiBnixl_buffer_device 设 cpu。nixl_pool_size 512 是对象键池大小。注意这里的 access_key 和 secret_key 要换成你自己的别直接用示例值。3.4 纯 DRAM 对照配置chunk_size: 256 local_cpu: True max_local_cpu_size: 50 save_unfull_chunk: False enable_async_loading: True remote_serde: naive blocking_timeout_secs: 100这个配置只走本地 CPU 内存用来做对照实验验证远程存储到底带来了多少额外收益。3.5 vLLM 启动命令LMCACHE_CONFIG_FILE/root/lmcache-nixl-s3.yaml \ LMCACHE_USE_EXPERIMENTALTrue \ PYTHONHASHSEED67 \ AWS_PROFILElmcache \ vllm serve Qwen/Qwen3-32B \ --gpu-memory-utilization 0.55 \ --rope-scaling {rope_type:yarn,factor:4.0,original_max_position_embeddings:32768} \ --max-model-len 131072 \ --kv-transfer-config {kv_connector:LMCacheConnectorV1,kv_role:kv_both,kv_parallel_size:16} \ --tensor-parallel-size 2几个参数要解释。gpu-memory-utilization 0.55 是给 KV 缓存留出显存空间具体值按你的卡调整。max-model-len 131072 配合 rope-scaling 的 yarn 因子 4.0把上下文拉到 128K。kv-transfer-config 里 kv_connector 必须是 LMCacheConnectorV1kv_role 用 kv_both 表示既读又写kv_parallel_size 16 控制并行度。如果你在 Gaudi3 上跑额外加这几个环境变量PT_HPU_GPU_MIGRATION1 VLLM_USE_V11 VLLM_SKIP_WARMUPTrue VLLM_EXPONENTIAL_BUCKETINGFalse3.6 Ceph 存储池与 RGW 配置存储池预创建ceph osd pool set noautoscale ceph osd pool create default.rgw.buckets.data 2048 2048 replicated ceph osd pool create default.rgw.buckets.index 64 64 replicated ceph osd pool create default.rgw.buckets.non-ec 64 64 replicated ceph osd pool set default.rgw.buckets.data size 2 ceph osd pool set default.rgw.buckets.data min_size 1 ceph osd pool application enable default.rgw.buckets.data ceph osd pool application enable default.rgw.buckets.index ceph osd pool application enable default.rgw.buckets.non-ecRGW 服务配置service_type: rgw service_id: standard service_name: rgw.standard placement: count_per_host: 4 label: rgw networks: - 10.67.67.0/24 spec: rgw_exit_timeout_secs: 120 rgw_frontend_port: 8080 concentrator: haproxy: concentrator_frontend_port: 80 concentrator_monitor_port: 1967 concentrator_monitor_user: admin每台主机 4 个 RGW 实例集中器绑 80 端口。count_per_host 按你的 CPU 核数调别把机器压垮。3.7 DNS 负载均衡配置Consul 服务注册datacenter smci data_dir /opt/consul bind_addr 172.19.65.41 client_addr 0.0.0.0 retry_join [172.19.65.41,172.19.65.42,172.19.65.43,172.19.65.44] server true bootstrap_expect 3 services [ { name s3 port 8080 check { id tcp-check name S3 TCP tcp localhost:8080 interval 10s timeout 2s } } ]CoreDNS 配置.:53 { log errors forward . 8.8.8.8 } s3.cephlab.com { rewrite stop { name exact s3.cephlab.com s3.service.consul. answer name s3.service.consul. s3.cephlab.com. } forward . 172.19.65.41:8600 172.19.65.42:8600 172.19.65.43:8600 172.19.65.44:8600 log errors debug }配完 dig 一下验证dig s3.cephlab.com看到 ANSWER SECTION 返回 4 条 A 记录就说明负载均衡生效了。4. 验证请求与压测命中率与 TTFT 一起看4.1 基线存储性能在引入 vLLM 和 LMCache 之前先用 elbencho 建立存储集群的基线。从 GPU 主机生成负载打到 Ceph S3 端点块大小用 62MB 匹配 LMCache 持久化的 KV 缓存块预期大小。这一步能告诉你存储侧的天花板在哪后面 TTFT 降不下去时你能快速判断是缓存没命中还是存储带宽不够。4.2 压测脚本用 LMCache 的 long_doc_qa.pypython3 ~/LMCache/benchmarks/long_doc_qa/long_doc_qa.py \ --model Qwen/Qwen3-32B \ --port 8000 \ --num-documents 1 \ --document-length ${len} \ --output-len 100 \ --repeat-count 1 \ --repeat-mode interleave \ --max-inflight-requests 1 \ --output results/ttft_${L}.outdocument-length 按你要测的上下文长度填从 4K 到 128K 逐档测。4.3 数据收集方法要干净地对比计算预填充和远程缓存命中的 TTFT按这个流程走第一步启动 vLLM跑 long_doc_qa.py记录热身轮的 TTFT这是计算预填充的结果。第二步重启 vLLM再跑一次记录热身轮 TTFT这是远程存储 KV 缓存命中的结果。第三步停止 vLLM从远程存储移除缓存块。重启 vLLM 是为了确保结果不被 GPU HBM 或 CPU 内存里的 KV 缓存污染。停止 vLLM 并清远程缓存是为了确保每个后续上下文长度不会从先前长度的远程缓存中受益。这样每次测试开始时除了你要测的远程缓存所有 KV 缓存都是冷的。4.4 结果解读实测下来Gaudi3 和 MI300X 上都能看到 TTFT 显著下降最大测量提升到 23 倍。这个数字说明 KV 缓存比单纯用张量并行分散预填充更有效两者结合能拿到最低 TTFT。除了 TTFT还要看 TPOT每个输出 token 的时间。前缀缓存省下的 GPU 周期可以用于解码可能顺带降低 TPOT。压测时把这两个指标一起记录。4.5 用 TaoToken 做端到端联调压测跑通后把模型服务接到 TaoToken 通道上做端到端验证。在 https://taotoken.net/api-keys 生成 KeyBase URL 用 https://taotoken.net/apiModel ID 填你压测用的模型。这样你可以从客户端侧发起真实请求观察经过完整链路后的 TTFT 和缓存命中行为。如果要做多模型对比直接在模型对话页面切换模型 ID 即可不用改代码。长期跑编码或 Agent 负载的话Coding Plan 页面有更固定的通道方案。5. 常见报错排查5.1 401 Unauthorized最常见的是 S3 凭证不对。检查 .aws/credentials 里的 aws_access_key_id 和 aws_secret_access_key以及 lmcache 配置里 nixl_backend_params 的 access_key 和 secret_key 是否一致。Ceph RGW 的凭证和 Ceph 集群的 admin key 是两回事别搞混。如果用的是 TaoToken 通道401 通常是 API Key 没配对或过期。去 https://taotoken.net/api-keys 重新生成一个确认 Base URL 是 https://taotoken.net/api。5.2 local proxy failed这个报错通常出现在网络层。检查 endpoint_url 能不能通Ceph RGW 集中器的 80 端口是否监听。如果用了 DNS 负载均衡确认 dig 能返回多条记录。CoreDNS 的 forward 配置里Consul 的 8600 端口要能通。另一个可能是 /dev/shm 空间不够。原生 S3 连接器会在 tmpfs 上预分配 mmap 文件每个并发请求一个。df -h /dev/shm 看一下不够就调大或减少并发。5.3 reading choices 相关报错这个多半是 vLLM 和 LMCache 版本不匹配。LMCache 开发版和 vLLM 生产栈的接口可能对不上需要定制容器镜像。确认你用的 vllm-gaudi 或对应加速器版本包含了匹配的 LMCache 开发版。kv_connector 名字必须是 LMCacheConnectorV1写错了会直接报连接器找不到。5.4 OAuth 或认证类报错如果走 TaoToken 通道出现认证问题检查 Key 的权限范围。模型对话、Coding Plan、API Keys 是不同的入口确认你用的 Key 有对应模型的调用权限。接入文档页面有详细的认证说明。5.5 缓存命中率上不去先确认 chunk_size 设置。vLLM 默认 16 token 一块LMCache 默认 256。如果两边对不上哈希值算出来不一致永远命中不了。再检查 save_unfull_chunk设 False 时不满一块的前缀不会被保存短前缀场景命中率会低。还有一点内容可寻址意味着前缀必须完全一致才能命中。如果你的系统提示词里带了时间戳或随机 ID每次哈希都不同缓存永远冷。把可变部分挪到前缀之后。5.6 TTFT 没降反升检查是不是走了远程存储但网络带宽不够。用 elbencho 复测存储基线确认 S3 端点能跑满带宽。如果 DNS 只返回一条记录所有流量压一个 RGW 实例带宽上不去远程读取比本地计算还慢。另外确认压测方法正确。如果没重启 vLLM 就测GPU HBM 里的缓存会污染结果看起来 TTFT 很低但那是本地缓存的效果不是远程存储的。6. 把缓存命中率和 TTFT 一起压下来的实操路径整套东西跑下来我的经验是分三步走最稳。第一步先用纯 DRAM 配置跑通 vLLM 加 LMCache确认 KV 连接器接口没问题本地缓存命中正常。这一步不碰 Ceph排除存储变量。第二步接原生 S3 连接器配好 Ceph 存储池和 RGW用 elbencho 确认存储带宽达标。然后按 4.3 的方法做对照压测拿到远程缓存命中的 TTFT 数据。如果提升不明显先查 DNS 负载均衡和 chunk_size 对齐。第三步确认收益后再考虑切 NIXL 路径压榨拷贝开销。NIXL 配置更复杂需要预分配缓冲区但能省掉 /dev/shm 的文件和 memcpy。联调阶段用 TaoToken 统一通道Base URL 用 https://taotoken.net/apiKey 在 https://taotoken.net/api-keys 生成模型 ID 按实际填。这样你在压测缓存行为时可以快速切换模型验证一致性不用维护多套凭证。长期跑编码或 Agent 负载Coding Plan 页面有更固定的方案。最后提醒一句缓存命中率是手段TTFT 和 TPOT 才是目标。别为了刷命中率把 chunk_size 调得过大导致单次传输延迟上升。压测时两个指标一起看找到你工作负载的最优平衡点。
返回列表