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 与 NIXL
LMCache 对接 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/api,Key 填刚生成的,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: "test"chunk_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 设 1GiB,nixl_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_EXPERIMENTAL=True \ PYTHONHASHSEED=67 \ AWS_PROFILE='lmcache' \ 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 必须是 LMCacheConnectorV1,kv_role 用 kv_both 表示既读又写,kv_parallel_size 16 控制并行度。
如果你在 Gaudi3 上跑,额外加这几个环境变量:
PT_HPU_GPU_MIGRATION=1 VLLM_USE_V1=1 VLLM_SKIP_WARMUP=True VLLM_EXPONENTIAL_BUCKETING=False3.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.py:
python3 ~/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 生成 Key,Base URL 用 https://taotoken.net/api,Model 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/api,Key 在 https://taotoken.net/api-keys 生成,模型 ID 按实际填。这样你在压测缓存行为时,可以快速切换模型验证一致性,不用维护多套凭证。长期跑编码或 Agent 负载,Coding Plan 页面有更固定的方案。
最后提醒一句:缓存命中率是手段,TTFT 和 TPOT 才是目标。别为了刷命中率把 chunk_size 调得过大,导致单次传输延迟上升。压测时两个指标一起看,找到你工作负载的最优平衡点。