这是本系列的第二篇,讨论推理技术这一方向。本文作为一个初学者在学习过程中的记录,可能不一定能做到非常完整和系统。
性能模型与瓶颈分析:一切优化的判断基础
推理阶段拆解与特点
当前的生成式文本模型采用decoder only架构,先批量填充prompt token,再逐token做预测,分别称为prefill和decode。
- prefill:一次性输入并处理完整prompt,
个输入要计算 次注意力和 维的FFN - decode:用上一次输出的token作为输入预测下一个token,
个输入要计算 次注意力和 维的FFN
因此,prefill往往是算力受限(Compute-Bound),decode往往是访存受限(Memory-Bound)。
性能分析模型
一个计算设备想要能够计算,首先需要把数据从内存拉到ALU中。因此设备的实际计算性能,不仅受制于其浮点运算能力,还受制于其内存带宽。
Roofline Model是一个经典的性能分析模型,如下图1。
图为NVIDIA A6000在FP16计算下的Roofline Model。其中:
- 横坐标:算术强度(Arithmetic Intensity),表示一个计算任务(计算函数或算子)的浮点运算量与内存访存量(数据传输量)的比值
,或是浮点运算速度与内存带宽的比值 - 纵坐标:计算性能(Performance),表示该计算任务在设备上能达到的浮点运算量
因此,该图表示了在一个特定的设备上,不同算术强度的计算任务能达到的实际浮点性能,公式为:
- 上方横线为设备的峰值浮点性能,任何计算任务的实际性能都不会超过这条线,因为它代表硬件限制
- 左侧斜线表示在不同算数强度下的计算任务实际能够达到的最大浮点性能,斜率为峰值内存带宽
- 斜线与横线的交点,代表同时达到峰值浮点性能和峰值内存带宽的条件,这要求计算任务发生在特定的算术强度下,即峰值浮点性能与峰值内存带宽的比值
根据Roofline Model,我们可以判定一个计算任务是算力受限还是访存受限:
- 如果其算数强度较小,在斜线区间中,则为访存受限
- 如果其算数强度较大,在横线区间中,则为算力受限
在实际的优化工作中,我们往往需要做算子级拆解和分析。
性能指标体系
介绍一些常见的指标。
在用户视角,常用指标:
- TTFT(Time To First Token):从用户提交查询到系统返回第一个输出内容所需的时间
- TPOT(Time Per Output Token):在生成第一个token后,后续每个输出token所需时间
- Goodput:在满足SLO的条件下的有效吞吐量(区别于Throughput)
在硬件利用率视角,常用指标:
- MFU(Model FLOPs Utilization):系统实际观测的运算速度与系统理论峰值运算速度的比值,即实际有效FLOPS/理论峰值FLOPS,一般在计算受限场景中使用
- MBU(Model Bandwidth Utilization):系统实际达到的带宽与系统理论峰值带宽的比值,即实际有效MBW/理论峰值MBW,一般在访存受限场景中使用
更细粒度的性能分析
Roofline Model只是一个粗粒度的性能分析模型,实际性能优化还要更深入考虑通信开销、kernel开销、KV Cache容量等等细节问题。
例如NVIDIA的AIConfigurator,将推理过程拆解为底层的算子原语(GEMM矩阵乘法、Attention机制、通信、内存等),利用内置的底层性能数据库进行组合预测,在5到30秒内即可评估成千上万种配置组合。
推理技术:逐层分析优化方向
请求编排
批处理与调度
prefill与decode瓶颈相反,在大模型推理中,我们需要考虑在什么粒度上拆分prefill与decode,以在满足SLO的前提下实现资源利用率最大化。粒度从单卡内的token级切片到集群级的隔离划分都需要考虑。
常见的拆分手段有:
- Chunked Prefill(分块预处理)
- 粒度:token级别拆分
- 做法:把一个长prefill阶段拆成多个固定大小的token块(Chunk),穿插在decode的各个时间步之间计算
- 作用:用于prefill,在PD不分离的情况下,略微牺牲了最好情况下的TTFT和TPOT,但保障了最坏情况下的TTFT和TPOT稳定
- Continuous Batching(连续批处理)
- 粒度:请求级别拆分
- 做法:模型每完成一次计算任务(分块的prefill或一次decode),检查该请求是否完成,如果完成,调度器立刻将新请求添加进去
- 作用:通常用于decode,在每个时间步的迭代中,将多个活跃请求组成batch(已有请求生成结束时即时补入新请求),提升算术强度,大幅减少GPU空转
- PD分离(Prefill-Decode Disaggregation)
- 粒度:集群架构级别拆分
- 做法:将整个集群的GPU划分为完全独立的prefill池与decode池,请求先到prefill实例算完输入并生成KV Cache,再通过高速网络(NVLink/RDMA等)将KV Cache传输给decode实例集群接力生成
- 作用:彻底消除算力和显存带宽的资源争抢,保障了稳定性,但是不提升性能,代价是引入了KV Cache的跨机/跨卡网络传输开销
KV Cache的运行时管理与复用
先来回顾下KV Cache:KV Cache本质上缓存的是Transformer每一层用于自注意力计算的投影向量值。KV Cache并没有改变
可以说:KV Cache把算力问题变成了显存和带宽问题。正是这一特点,KV Cache逐步被改造成一个有分页器、有缓存命中率、有分层存储、有传输协议的独立被管理资源。
显存管理:从碎片化到分页
朴素的推理实现有三类典型的显存浪费2:
- reserved:为未来token产生的KV Cache预留的空间
- internal fragmentation:由于过度配置以应对潜在的最大序列长度而导致的内部碎片
- external fragmentation:来自内存分配器的外部碎片
因此,由vLLM提出的PagedAttention诞生,其核心思路是借鉴操作系统内存分页的思路,将KV Cache切成固定大小block(默认16个token),维护block table,物理块带引用计数,多序列共享时用 Copy-on-Write。
除非修改显卡的低级驱动,否则PagedAttention只能完全在用户态工作,无法像CPU内存一样利用MMU硬件处理,因此会有一定的性能损耗。很显然,这里后续还存在很多优化空间。
前缀复用(Prefix Caching)
在用户进行多轮对话,或者多Agent存在公共的System Prompt时,重复计算前缀非常耗时且是不必要的。
因此,我们可以引入前缀缓存机制,对于每个输入,检查其是否包含已经计算过的历史片段,如果缓存命中,就直接复用KV Cache。
典型的代表是SGLang的RadixAttention3,将请求序列用基数树(Radix Tree)来维护。它跟字典树(Trie Tree)非常类似,核心区别在于字典树的每个节点只存储单个字符(或固定大小的片段),而基数树会将只有一个子节点的连续链条进行压缩合并,从而节省大量空间。
通过基数树这种数据结构,我们可以很轻松地维护和查询最长公共前缀,从而实现KV Cache的前缀复用。
另外,vLLM也支持前缀缓存,不过是用哈希的方式实现的。但是vLLM的哈希前缀缓存也是递归的,所以从本质上讲也是个树形结构,所以两个方案可以说是殊途同归。
分层存储与卸载
随着上下文长度飙升,单个GPU存不下海量的KV Cache。除了并行化拆分,KV Cache也开始走向异构存储体系。
分层存储即为将KV Cache按需求分别放在HBM、CPU RAM、SSD、网络RDMA等,有点类似CPU多级缓存。
因此,不同层级的带宽和容量是核心需要考虑的问题。方案制定的核心原则是取回来比重算是否比重新prefill更快。
这是一份AI给的不同存储层级大致的特点:
| 存储层级 | 典型带宽范围 (单机/单卡) | 典型容量规模 | 访问延迟级别 | 在 KV Cache 中的角色与权衡 |
|---|---|---|---|---|
| HBM (显存) | 2 TB/s - 5 TB/s | 24 GB - 141 GB /卡 | 纳秒级 (ns) | 核心层。Prefill(首字)与 Decode(生成)的计算核心区。容量极小、成本极高、速度极快。 |
| CPU RAM (DDR) | 100 GB/s - 400 GB/s | 512 GB - 2 TB /机 | 百纳秒级 (ns) | 温数据层。作为 HBM 的延伸(Offloading)。带宽缩减10-20倍,但容量大一个数量级,成本较低。 |
| 网络 RDMA (集群) | 50 GB/s - 100 GB/s (400G/800G) | 视集群规模而定 | 微秒级 (µs) | 共享/协作层。用于多卡/多机之间的 KV Cache 交换(如 Disaggregated Attention 架构),受限于网络拓扑。 |
| SSD (NVMe) | 7 GB/s - 32 GB/s (PCIe 4.0/5.0) | 几TB - 数十TB /机 | 几十微秒级 (µs) | 冷数据层。用于长文本、多轮对话的历史缓存(Context Caching)。带宽太低,直接读取会严重拖慢 Decode 速度。 |
与此同时,业界也开始开发专门针对KV Cache的传输协议与通信框架,例如通过定制化的RDMA协议,实现KV Cache在集群内部的高速路由与传输,降低网络带宽带来的延迟。
运行时压缩:推理时稀疏化
这里主要是指淘汰与稀疏化系列方法。
核心基于两个观察:attention sink(首几个token恒定吸走大量权重)与heavy hitters(约20%的token占了80%权重)。
因此可以通过一些算法,计算出哪些token对结果的影响不大,让这些token不参与注意力的计算。相当于对结果做近似,但是省下了KV Cache的容量和读取量。
然而这类方法在生产中失效,核心原因是:
- FlashAttention不落盘注意力分数,导致部分依赖注意力分数的算法失效
- 反复的token淘汰在生产系统里可能根本释放不了显存:PagedAttention中一个物理block约16个token,只有整块全空才能回收
当然也有一些补救办法:使用不依赖注意力分数的算法,以及一些碎片整理方案或是忍受页内碎片。
投机解码
投机解码的思想与CPU分支预测非常像:使用一个外部小模型(或是模型自身的MTP)做多token预测,同时计算出多个输出token的概率分布并在大模型中批量decode并验证,如果有其中一个token验证失败,那这个token后的生成结果都要丢弃。
最朴素的方式是利用大模型输出的概率分布采样得到最终token后,与小模型预测的token逐一对比。但这个方法在top 1的token概率不高时非常容易验证失败,例如:
大模型分布
小模型分布
尽管两者预测的概率分布差异不大,经采样后验证成功的概率只有
因此,投机解码采用一种叫做拒绝采样的策略,通过比对概率而非比对结果,能在保证大模型原始输出概率分布完全不变(无损)的前提下,大幅提升小模型预测token的通过率。我们记大模型输出概率分布为
从左往右检查每个候选token
- 如果
,说明大模型比小模型更认可它,直接接受该候选token - 如果
,以的概率接受该候选token,一旦拒绝,立刻丢弃该位置及其后所有候选,并从修正分布 中重采一个token。其中,修正分布 这里的修正分布本质上是:大模型把所有被小模型“高估”的token概率压低,计算出一个新的分布(残差分布),然后做归一化(即
的分母部分),再采样。
下面证明一下这样的策略可以使得最终输出token的分布恰好等于直接从大模型
某个token
- 路径A:候选恰好是
且被接受 - 概率为:
- 解释:抽到
的概率是 ,接受的条件概率是 ,两者相乘即为候选恰好是 且被接受的概率
- 概率为:
- 路径B:候选被拒绝,重采样采到了
- 概率为:
- 解释:
- 等式第一段:由路径A可知,候选恰好是
且被接受的概率为 ,我们对候选边缘化,得到
接受率 ,
则拒绝率为 ,所以候选被拒绝且重采样采到了的概率就是 - 等式第二段:
- 利用恒等式
,我们可以得到
修正分布的分子部分 - 再次利用该恒等式,我们可以得到
修正分布的分母部分
相乘后分母抵消,只剩修正分布的分子部分
- 利用恒等式
- 等式第一段:由路径A可知,候选恰好是
- 概率为:
两个路径概率相加
模型表示
集群与部署
内核与硬件
新场景与新变化
负载特性变化
端侧推理
总结
先判断瓶颈,再选择手段
论文加速比不等于生产收益
优化是相互耦合的,不一定能独立叠加

欢迎留言