跳到主要内容

推理服务商向你隐瞒了什么:KV 缓存、批处理与延迟底线

· 阅读需 14 分钟
Tian Pan
Software Engineer

你正在运行一个由 LLM 驱动的应用,你的 p99 延迟为 4 秒。你已经优化了提示词,减少了输出长度,并切换到了流式传输。但这个数字几乎没变。问题不在于你的代码——而是在你无法控制的黑盒内部运作的物理学和排队论。

每个推理服务商在你的第一次 API 调用之前,就已经通过数十项架构决策决定了你应用的性能上限。KV 缓存淘汰策略、连续批处理(continuous batching)调度、分块预填充(chunked prefill)块大小——文档中没有提到这些,你也无法配置,但它们决定了你不得不面对的延迟和成本曲线。

这篇文章将解释推理基础设施内部究竟发生了什么,为什么它会产生不可避免的延迟底线,以及你真正能做的少数几件事。

什么是 KV 缓存,以及为什么它在消耗你的预算

当 Transformer 模型生成下一个 token 时,它会对上下文中的每个先前 token 计算注意力(attention)。如果没有缓存,在 10,000 个 token 的提示词基础上生成 2,000 个 token 的回复,将需要为这 2,000 个解码步骤中的每一个重新计算这 10,000 个 token 的注意力——这种二次方级的计算膨胀会使 LLM 实际上无法使用。

KV 缓存通过在处理每个 token 后存储注意力层的键(key)和值(value)张量来解决这个问题。在随后的解码步骤中,模型从缓存中读取,而不是重新计算。这就是为什么生成一旦开始就会很快:每个新 token 只需要计算自身与缓存序列之间的注意力。

难点在于内存。一个具有 128K 上下文的 70B 参数模型的单次缓存序列大约消耗 40GB 的高带宽显存(HBM)。HBM 是 GPU 上最快且最有限的内存——它不仅稀缺,而且由所有并发请求共享。当缓存填满时,必须淘汰(evict)一些内容。淘汰什么以及何时淘汰,决定了你的下一个请求是从热缓存还是冷缓存开始。

服务商如何决定淘汰什么是事情变得有趣的地方。最基础的是 LRU(最近最少使用)算法——淘汰最近最少访问的内容。但生产系统会在其上叠加优先级分数、估计的重用概率和尾部延迟优化。关于尾部优化淘汰策略的研究表明,与标准 LRU 相比,SLO 违规减少了高达 38.9%。你服务商使用的算法直接影响你的 p95 和 p99 指标。

前缀缓存:你可能没有得到的 90% 成本削减

KV 缓存重用的产品化版本被称为前缀缓存(prefix caching)或提示词缓存(prompt caching)。服务商不再仅限于在单个请求内进行缓存,而是可以跨多个请求缓存共享前缀的 KV 状态。如果你的应用在每次 API 调用时都发送相同的 5,000 token 系统提示词,智能服务商只需处理一次这些 token——后续共享该前缀的请求将重用已缓存的状态。

经济效益是巨大的。在 Anthropic 的 API 上,缓存读取的成本为每百万 token 0.30 美元,而全新的输入处理成本为每百万 token 3.00 美元——相当于 90% 的折扣。OpenAI 为超过 1,024 token 的提示词提供自动缓存,可节省 50% 的成本。一个具有稳定系统提示词和每个会话文档上下文的聊天应用可以缓存 70% 以上的输入 token,从而在一夜之间将成本降低一半以上。

生产数据证实了这一点。Google Vertex AI 报告称,通过将缓存感知作为首要约束的调度优化,首 token 时间(TTFT)减少了 40%,p50 延迟提高了 43%。Anthropic 的客户报告称,在发生缓存命中时,长提示词的延迟减少了 85%。

但前缀缓存有一个大多数开发者在不知情的情况下会违反的结构要求:前缀必须从提示词的开头开始,字节级完全一致。 序列早期的任何变化都会破坏后续所有内容的缓存。这就是为什么在系统提示词的开头放置时间戳、用户 ID 或会话令牌,可能会让你在无声无息中比结构良好的提示词多付出 10 到 50 倍的成本。

三种批处理模式及其对你请求的影响

你的推理请求并非单独执行。服务商会将多个请求打包在一起,以使 GPU 计算达到饱和——LLM 推理的经济效益只有在高利用率下才成立。批处理的实现方式对你的延迟有直接影响。

**静态批处理(Static batching)**是初级的方法:收集一批请求,并行运行,等待批次中的每个请求完成后再开始下一批。这简单但浪费。如果你的批次包含一个 50 token 的请求和一个 2,000 token 的请求,GPU 在处理那 1,950 token 的差额时会处于部分闲置状态。

连续批处理(Continuous batching)(也称为运行中批处理,in-flight batching)通过在 token 级别运行来解决这个问题。当批次中的任何请求生成其最后一个 token 时,调度程序会立即将其移出,并为下一个解码步骤插入一个等待中的请求。无论输出长度如何波动,GPU 利用率都能保持在高位。vLLM 证明了这种方法比静态批处理的吞吐量提高了 10-20 倍;Anyscale 在其基准测试中报告了 23 倍的提升。

**分块预填充(Chunked prefill)**解决了另一个问题。处理长提示词(“预填充”阶段)是计算密集型的,并且会阻塞其他请求的所有解码操作,直到其完成。一个 100K token 的预填充可能会阻塞较短请求数百毫秒。分块预填充将长提示词拆分为固定大小的块,并将其与其他请求的解码步骤交替进行。你的长提示词请求开始生成的时间会稍长,但队列中的短请求将不再被阻塞。

权衡利弊:分块预填充提高了整体公平性和 p99 延迟,代价是分块请求本身的首 token 时间(TTFT)增加。天下没有免费的午餐。如果你的应用既需要低于 500 毫秒的 TTFT,又需要在长上下文中实现高吞吐量,唯一的实际解决方案是预填充-解码解耦(prefill-decode disaggregation)——在独立的硬件池上运行预填充和解码。这就是像 Together AI 这样的服务商和大规模部署所采用的方案,但作为 API 调用者,你对此是无感的。

无法通过代码优化的延迟底线

根本约束在于内存带宽,而非计算能力。在解码阶段——即逐个生成 token 的过程中——GPU 必须针对生成的每一个 token 从 HBM 中读取整个 KV 缓存。这是内存受限(memory-bound)而非计算受限(compute-bound)的。增加更多的 GPU 核心并没有帮助,因为瓶颈在于从内存搬运数据的速度,而不是矩阵乘法的速度。

这造成了几个难以逾越的硬性底线:

  • 序列长度与内存呈平方级关系。 上下文长度翻倍需要 4 倍的内存和计算。这就是为什么长上下文请求明显更贵,以及为什么供应商会针对这些请求收取更高费用的原因。
  • 量化无法拯救 KV 缓存。 你可以量化模型权重以减小内存占用,但 KV 缓存存储的是注意力激活值(attention activations),而不是权重。INT4 量化模型面临与 FP16 模型相同的 KV 缓存扩展问题。
  • Batch size 是在用延迟换取吞吐量。 更大的 Batch size 可以提高 GPU 利用率并降低每个 token 的成本,但它们会增加单次请求的延迟,因为每个请求在自己的槽位(slot)上等待的时间更长。供应商对 Batch size 的决策反映了他们的优化目标——而这可能与你的目标不匹配。
  • 队列深度驱动尾部延迟。 当供应商处于负载下时,请求会排队。排在你之前的一个长请求可能会让你的 TTFT 增加数秒。这在 API 层面是无法预测或绕过的。

2025 年基准测试中的典型生产环境 TTFT 数据:GPT-4o mini 平均为 0.7–1.4 秒,Gemini 2.5 Flash 约为 0.3 秒,优化架构上的 Llama 70B 为 180–350ms。对于 Llama 405B,供应商通常将 p99 TTFT 目标设定在 6 秒左右。这些数字反映了硬件、模型大小和负载,而不是你的应用程序代码。

悄无声息破坏缓存命中率的错误

对生产环境 LLM 应用的研究发现,破坏前缀缓存(prefix cache)是最常见且代价最高的部署错误之一——在最坏的情况下,它会悄悄地将成本增加 10-50 倍。

提示词开头的动态内容。 缓存是按从左到右的顺序工作的。任何请求之间的变化都会使随后的所有缓存失效。在系统提示词的第一位放置当前时间戳意味着缓存命中率永远为零。修复方法:结构化提示词,使所有静态内容位于所有动态内容之前。先放系统提示词,然后是稳定的上下文,最后是动态的用户内容。

针对每个用户的系统提示词个性化。 在系统提示词中包含用户名、偏好或个性化指令会阻止跨用户的缓存共享。每个用户都会得到自己未缓存的前缀。对于拥有数百万用户的应用程序,这会累积成巨大的不必要计算开销。

忘记在 Anthropic 上标记缓存断点。 与 OpenAI 的自动缓存不同,Anthropic 需要在你想设置缓存断点的每个位置显式标注 cache_control。如果没有这些标记,根本不会发生前缀缓存。每个检查点至少 1,024 个 token,每个请求最多四个检查点。

提示词模板版本偏移。 即使对系统提示词进行微小的措辞更改,也会破坏所有尚未完成其 5 分钟缓存窗口的在途请求的缓存。应像对待版本化代码产物一样对待提示词模板,并避免在流量高峰期进行更改。

62% 问题。 对生产部署的研究发现,大多数团队没有意识到他们每天都在重复处理数千次相同的系统提示词。这种成本表现为正常的 token 使用量——除非你显式跟踪缓存命中率,否则供应商仪表盘中不会出现“你做错了”的信号。

你真正能掌控的调节杠杆

坦率地说,清单比你希望的要短:

提示词结构。 安排内容以最大化前缀稳定性——静态在动态之前,稳定在变量之前。这是在没有 API 成本的情况下,你可以进行的最高杠杆率的改变。

显式缓存标记。 在需要它们的供应商上,在你拥有稳定、长期运行的前缀处标记缓存断点。对于任何每天发送超过几百个请求的应用来说,这都是基本要求。

流式与非流式。 流式传输不会改变推理延迟——token 生成的速度是一样的。但感知延迟会大幅下降,因为用户在 TTFT 窗口内就能看到输出,而不是等待完整响应。对于交互式应用,开启流式传输是投资回报率最高的改动之一。“看着它思考”和“看着它打字”的区别,就是 4 秒等待感与可接受体验之间的区别。

输出长度约束。 更短的输出意味着更少的解码步骤、更短的 KV 缓存增长以及更快的完成。max_tokens 是上限,而不是目标——如果你经常触及这个上限,说明你生成的 token 可能超出了实际需求。

供应商选择。 不同的供应商运行不同的硬件、不同的调度算法和不同的默认 Batch size。对于延迟敏感的工作负载,请进行基准测试而不要靠推测。同一模型在不同基础设施上的负载下,p99 TTFT 可能会有 5-10 倍的差异。

请求超时和重试策略。 卡在长队列中的请求最终会超时。采取激进的超时策略并配合“超时重试”(而非“全量重试”),可以将队列深度问题暴露为延迟峰值,而不是无声的挂起。一些供应商在响应头中公开了队列深度或预计等待时间——请善用它们。

这对架构意味着什么

实际的影响在于,你的推理提供商是你应用程序性能配置文件的共同设计者。了解他们的基础设施能让你顺应,而不是违背他们的调度模型进行设计。

以下是几个由此得出的原则:

将你的系统提示词视为基础设施,而非配置。 它应该是稳定的、有版本的,并且通过结构化设计来最大化缓存命中率。变更的部署应像代码部署一样谨慎。

根据延迟特征分离请求类型。 一个 100K 上下文的批量摘要任务和一个亚秒级的用户交互补全请求,应该发送到不同的端点、不同的提供商,或者至少具备不同的队列优先级。大多数提供商的 API 都提供具有不同 SLA 目标和定价的批量端点。

将缓存命中率作为一级指标进行监控。 你的成本和延迟都取决于它。如果你的提供商公开了缓存命中统计数据,请在数据下降时设置告警。如果没有公开,你可以从每 token 成本趋势中推断出来。

在负载下测量延迟,而非在零并发时。 大多数开发者是在孤立环境下对应用进行基准测试。但在生产环境中,会增加队列深度、批处理差异和缓存驱逐压力。在 10 倍负载下的 p99 延迟可能比你的基准测试数据差 5 倍 —— 在用户发现之前先查明这一点。

延迟底线是客观存在的,且不随你的意愿而降低。但上限 —— 即你的实际表现比底线差多少 —— 很大程度上取决于你的应用程序与所运行的基础设施配合得有多好。


这里描述的底层机制 —— KV 缓存(KV cache)、持续批处理(continuous batching)、分块预填充(chunked prefill) —— 都在 vLLM 和 TensorRT-LLM 等开源系统中得到了实现。运行你自己的推理服务可以让你完全控制这些参数,但代价是增加运维复杂度。对于大多数团队来说,正确的做法是理解这些限制,围绕它们进行设计,并选择优化目标与你的延迟要求相匹配的提供商。

References:Let's stay in touch and Follow me for more thoughts and updates