外观
常规 ML 推理 vs. LLM 推理
Regular ML Inference vs. LLM Inference
相比常规 ML 推理,LLM 推理面临一系列独特的挑战,这也是为什么会出现 vLLM、LMCache、SGLang、TensorRT LLM 这类专门的高性能 LLM 推理引擎。接下来我们就来了解这些挑战,以及它们是如何被解决的!
连续批处理(Continuous Batching)
传统模型,比如 CNN,通常有固定尺寸的图像输入和固定长度的输出(比如一个标签),这让批处理变得很简单。

然而,LLM 处理的输入(也就是提示词,prompt)长度是可变的,生成的输出长度也同样可变。

因此,如果把多个请求打包成一批,它们完成的时间各不相同,GPU 必须等到最长的那个请求处理完,才能开始处理新的请求。这就导致了 GPU 出现空闲时间:

连续批处理解决了这个问题。系统不再等待整批请求全部完成,而是持续监控所有序列,一旦某个序列完成(出现 <EOS> token),就立刻把它替换成新的查询:

这样可以让 GPU 的处理流水线始终保持饱和,从而最大化利用率。
预填充-解码分离(Prefill-decode Disaggregation)
LLM 推理是一个两阶段的过程,这两个阶段对资源的需求存在本质差异。
- 预填充(prefill)阶段会一次性处理输入提示词的所有 token,因此是计算密集型的。
- 解码(decode)阶段以自回归方式逐个生成输出 token,因此对低延迟的要求很高。
- 如果把这两个阶段放在同一批 GPU 上运行,计算密集型的预填充请求就会干扰对延迟敏感的解码请求。预填充-解码分离通过为预填充阶段和解码阶段分别分配一组专用 GPU,来解决这个问题。


相比之下,标准 ML 模型通常只有一个统一的计算阶段。
GPU 内存管理与 KV 缓存
生成一个新 token 需要用到之前所有 token 的 key 向量和 value 向量。为了避免对所有 token 反复重新计算这些向量,我们会把它们缓存起来。

这个 KV 缓存(KV cache)会随着对话历史的总长度线性增长。不过在很多工作流中,像系统提示词(system prompt)这样的输入会被许多请求共享,因此我们可以在所有对话中复用这些 KV 向量,从而避免重复计算:

话虽如此,由于 KV 缓存是以连续内存块的形式存储的,它会占用相当可观的内存,这既浪费 GPU 内存,也会导致内存碎片化:

Paged Attention 通过把 KV 缓存存储在不连续的内存块中、再用一张查找表来追踪这些块,解决了这个问题。这样一来,LLM 只需要加载它实际需要的那些块,而不必一次性加载全部内容。我们会在另一篇文章中详细介绍 Paged Attention。
前缀感知路由(Prefix-aware Routing)
要扩展标准 ML 模型,你只需把模型复制到多台服务器/GPU 上,再配合简单的负载均衡策略,比如轮询(Round Robin),或者把请求路由到最空闲的服务器即可。
由于每个请求都是相互独立的,这种做法完全没问题。但 LLM 严重依赖缓存(比如前面提到的共享 KV 前缀),这意味着请求之间不再是相互独立的了。如果一个新查询的共享前缀已经缓存在副本 A 上,但路由器却把它发到了更空闲的副本 B,副本 B 就不得不重新计算整个前缀的 KV 缓存。前缀感知路由正是为了解决这个问题。

不同的开源框架对前缀感知路由各有各的实现方式。一般来说,前缀感知路由需要路由器维护一张映射表(或者使用某种预测算法),来追踪哪些 KV 前缀当前缓存在哪些 GPU 副本上。
当新查询到来时,路由器会把它发送给已经缓存了相关前缀的那个副本。
模型分片策略(Model Sharding Strategies)
扩展一个稠密(dense)ML 模型有几种常见策略:

而像混合专家(MoE)这样的 LLM 架构,情况要复杂得多。

MoE 模型使用一种专门的并行策略,叫做专家并行(expert parallelism):把各个专家(experts)分散到不同的设备上,而注意力层则会在所有 GPU 上进行复制:

也就是说,每个 GPU 只保存部分专家的完整权重,而不是全部专家。这意味着每个 GPU 只处理分配给它所存储的那些专家的 token。当一个查询到来时,MoE 层中的门控网络(gating network)会根据被激活的是哪些专家,动态决定这个查询应该被送到哪个 GPU 上。这是一个复杂的内部路由问题,不能简单地用「模型复制」的思路来处理,而是需要一个足够精细的推理引擎,去管理跨分片专家池的动态计算流程。