跳转至

大模型推理加速与显存黑科技 (Inference Acceleration & KV Cache)

在日常使用 ChatGPT 或开源大模型时,很多初学者都会注意到一个奇特的现象: - 发送问题后,大模型通常“瞬间”就能开始吐出第一个字; - 但随着对话历史越来越长、回答篇幅越来越宏大,大模型打字机吐字的速度却肉眼可见地越来越慢,甚至在本地部署时经常弹出让人抓狂的 OOM(Out Of Memory,显存爆炸) 报错。

为什么会这样?大模型在生成每一个词的背后,GPU 到底在经历怎样的计算风暴?

本文将为你通俗拆解大模型推理的底层瓶颈,以及让大模型在工业界得以高并发落地的四大“加速黑科技”:KV Cache、PagedAttention、FlashAttention 与投机采样(Speculative Decoding)。


1. 推理的两大阶段:Prefill vs Decode

要理解推理优化,首先必须分清大模型推理生命周期中截然不同的两个阶段:

flowchart TD
    subgraph Phase1 ["1. 预填充阶段 (Prefill / Prompt Processing)"]
        direction TB
        P_In["输入用户提示词 (例如 1000 个字)"] --> P_Calc["所有输入词并行投入 GPU"]
        P_Calc --> P_Type["【算力密集型 (Compute-Bound)】<br>GPU 数万核心满载浮点运算,生成首字 (TTFT 极快)"]
    end

    subgraph Phase2 ["2. 解码生成阶段 (Decode / Autoregressive Generation)"]
        direction TB
        D_In["逐个生成新词: 词 1 -> 词 2 -> 词 3 ..."] --> D_Calc["每吐一个词,都要将上百亿参数从显存完整搬进计算核心"]
        D_Calc --> D_Type["【访存带宽密集型 (Memory-Bound)】<br>计算单元经常'饿肚子等待数据搬运',打字机速度受限"]
    end

    Phase1 --> Phase2
  1. 预填充阶段(Prefill):处理用户输入的提示词。因为整个 Prompt 是已知且一次性给出的,GPU 可以将这上千个词完全并行计算。GPU 计算核心满负荷运转,衡量指标是 TTFT(Time To First Token,首字延迟)。
  2. 解码阶段(Decode):大模型开始逐字自回归接龙。因为第 \(N\) 个词必须等第 \(N-1\) 个词算出来才能确定,所以无法时间并行,只能一次吐一个词。每吐一个词,GPU 都要把整个模型的庞大权重在显存中读一遍,GPU 算力利用率往往不足 \(20\%\),瓶颈卡在显存带宽(Memory Bandwidth)。

2. KV Cache:空间换时间的经典范式

在 Decoder-Only Transformer 中,自注意力机制的计算公式为:

\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V\]

假设当前已经生成了 100 个词,正在准备生成第 101 个词。 按照朴素做法,模型需要拿第 101 个词的 \(Q\),去和前面所有 100 个词的 \(K\) 和 \(V\) 计算注意力。

flowchart LR
    subgraph WithoutCache ["没有 KV Cache (每次全量重算)"]
        W1["生成词 1: 算 1 次"]
        W2["生成词 2: 算 2 次"]
        W3["生成词 100: 算 100 次"]
        W_Sum["计算复杂度随着长度飙升至 O(N^2),系统彻底卡死!"]
        W1 --> W2 --> W3 --> W_Sum
    end

    subgraph WithCache ["引入 KV Cache (缓存历史状态)"]
        C_Save["将前 100 个词计算好的 Key 和 Value 保存在显存中"]
        C_New["第 101 个词只需计算自己的 Q, K, V"]
        C_Match["拿新的 Q 直接去和缓存里的历史 K, V 快速匹配"]
        C_Fast["单步生成计算复杂度骤降至 O(1)!"]
        C_Save --> C_New --> C_Match --> C_Fast
    end

2.1 带来显存巨额负担的“吞噬兽”

KV Cache 完美解决了计算重复的问题,但它带来了新的噩梦:显存消耗巨大。

KV Cache 占用的显存大小估算公式:

\[\text{KV Cache Size} \approx 2 \times 2 \times n_{\text{layers}} \times n_{\text{heads}} \times d_{\text{head}} \times \text{seq\_len} \times \text{batch\_size}\text{ Bytes}\]
  • 前面的两个 2:分别代表存 \(K\) 和 \(V\),以及 FP16 浮点数每个数字占 2 字节;
  • 随着对话序列长度(\(\text{seq\_len}\))和并发请求数(\(\text{batch\_size}\))线性增加;
  • 惊人事实:对于一个 13B 参数的模型,当并发量为 32、上下文达到 4096 时,光是存放 KV Cache 就需要 超过 20GB 显存,甚至超过了模型权重本身的体积!

3. PagedAttention 与 vLLM:终结显存碎片化

既然 KV Cache 如此庞大,早期的推理框架(如原生 Hugging Face)是如何管理显存的?

3.1 传统方式的悲剧:过度预留与碎片化

传统的内存管理方式必须分配连续的物理显存。 因为系统事先不知道用户的对话到底会写 100 个字还是 4000 个字,系统只能“按最坏情况预留(Over-allocation)”:为每个并发请求预先划出一块支持最大长度(如 4K)的连续显存。

请求 A 实际用量: [ 500 Token ] | [ 3500 Token 闲置浪费但被霸占 ]
请求 B 实际用量: [ 800 Token ] | [ 3200 Token 闲置浪费但被霸占 ]

这种做法导致 \(60\% \sim 80\%\) 的宝贵 GPU 显存处于闲置与碎片化状态,导致显卡明明还有空余,却再也装不下新的用户请求。

3.2 操作系统灵感:PagedAttention

UC 伯克利团队受到操作系统虚拟内存分页(Paging)的启发,提出了 PagedAttention(并孵化了如今工业界统治级的推理框架 vLLM)。

flowchart TD
    subgraph Logical ["逻辑连续的 KV Cache (对模型可见)"]
        L1["Block 0: [Token 0 ~ 15]"]
        L2["Block 1: [Token 16 ~ 31]"]
        L3["Block 2: [Token 32 ~ 47]"]
        L1 --> L2 --> L3
    end

    subgraph Table ["页表 (Block Table 动态路由映射)"]
        T1["逻辑块 0 -> 物理块 #7"]
        T2["逻辑块 1 -> 物理块 #2"]
        T3["逻辑块 2 -> 物理块 #11"]
    end

    subgraph Physical ["物理显存 (零碎分布在显存各处)"]
        P2["物理块 #2 (显存地址 0x2000)"]
        P7["物理块 #7 (显存地址 0x7000)"]
        P11["物理块 #11 (显存地址 0xB000)"]
    end

    Logical --> Table --> Physical
  • 核心思想:不再强制分配连续物理显存,而是将 KV Cache 拆成固定大小的小块(每个 Block 容纳 16 个 Token);
  • 物理显存按需分配,用多少申请多少,通过页表(Block Table)做逻辑与物理地址映射;
  • 惊人成效:显存浪费率从 \(70\%\) 骤降到接近 \(4\%\),单卡能够承载的并发请求吞吐量(Throughput)直接暴涨 2 到 4 倍!

4. FlashAttention:冲破 GPU 显存带宽墙

你可能听说过:GPU 拥有成千上万个计算流处理器(CUDA Cores),算力极其强悍。但在运行标准 Attention 时,GPU 的算力利用率往往只有可怜的 \(30\%\) 左右。

4.1 硬件真相:SRAM 与 HBM 的巨大速度鸿沟

在 GPU 芯片内部,有两种不同的存储硬件:

存储层级 硬件位置 容量大小 读写带宽速度
SRAM(片上超高速缓存) 紧贴计算核心内部 极小(通常仅十几 MB) 极快(约 19 TB/s)
HBM(外部高带宽显存) GPU 芯片旁边的主显存 极大(24GB ~ 80GB) 较慢(约 1.5 ~ 3 TB/s)

在传统的注意力计算中,程序会生成庞大的 \(N \times N\) 注意力矩阵,这个中间大矩阵放不下 SRAM,必须频繁写入 HBM 再读回。计算核心大部分时间都在干等数据搬运。

4.2 FlashAttention 的优化奇迹

Tri Dao 等人提出的 FlashAttention 彻底颠覆了这一现状:

graph LR
    HBM["慢速高容量显存 (HBM)<br>存放输入 Q, K, V"] -->|"分块切片调入 (Tiling)"| SRAM["高速片上缓存 (SRAM)<br>在极小空间内直接计算 Softmax"]
    SRAM -->|只写回最终加权结果,不存中间大矩阵| Out["输出结果"]
  • 分块计算(Tiling):将输入矩阵切分成小块,一次刚好塞进高速的 SRAM 中;
  • 在线 Softmax 技巧(Online Softmax):无需看到全局的所有数值,在局部流式读取时就能动态更新 Softmax 分母归一化项;
  • 零精度损失:FlashAttention 是数学上 100% 严格等价的精确注意力计算(并非近似算法),不仅节省了显存,还直接带来了 2 到 4 倍的端到端运算提速!

5. 投机采样 (Speculative Decoding):大小模型双剑合璧

在日常语言中,很多词汇的接龙是极其平庸且高度确定的:

例如:“床前明月光,疑是地__” 后面几乎 99.9% 确定是“上霜”;
写 Python 代码时:“def __init__(self” 后面几乎必定跟着 “, ):”。

让一个拥有 70B(700亿)参数的庞然大物,每次耗费巨大的访存代价去预测这种毫无悬念的简单词,是一种巨大的算力浪费。

5.1 核心机制:小模型草稿 + 大模型并行验证

投机采样(Speculative Decoding) 采用类似“导师与助理”的协作模式:

sequenceDiagram
    autonumber
    participant Draft as 小草稿模型 (例如 1B,极速轻量)
    participant Target as 大目标模型 (例如 70B,沉重睿智)

    Note over Draft: 步骤 1: 投机草拟<br>飞速连续预测 4 个候选词: [A, B, C, D]
    Draft->>Target: 将用户问题 + [A, B, C, D] 打包提交
    Note over Target: 步骤 2: 单步并行核验<br>大模型利用 Prefill 并行能力,<br>在 1 次前向传播中同时核验这 4 个词!
    Target-->>Draft: 结论: 接受前 3 个词 [A, B, C],修正第 4 个词为 [E]
    Note over Target: 最终成果: 大模型仅花费 1 次前向计算,<br>却成功输出了 4 个高品质 Token!
  1. 投机草拟:指派一个轻量的辅助小模型(Draft Model,如 1B 参数,速度比大模型快 10 倍),一口气往前推测出 \(K\) 个词(如 4~5 个词);
  2. 并行校验:大模型(Target Model)利用前向并行的能力,一次性同时计算这 \(K\) 个词的概率分布;
  3. 验证接收:大模型从前往后比对,只要小模型猜对的词就直接通过,一旦发现某处不符合大模型判断,就截断并修正。

5.2 带来的收益

  • 完全无损:通过特殊的采样接收算法,最终生成的文本概率分布与纯粹由大模型自己逐字生成的分布在数学上完全一致;
  • 显著提速:在代码生成和常见自然语言任务中,平均每次前向能接收 2~3 个 Token,端到端生成速度提升 1.5 到 2.5 倍!

6. 推理优化核心技术对比全景

优化技术 解决的核心瓶颈 核心底层原理 典型代表 / 工具
KV Cache 重复计算历史注意力导致的算力浪费 空间换时间,缓存历史键值向量,计算降为 \(O(1)\) 几乎所有现代大模型推理默认开启
PagedAttention KV Cache 显存碎片化与过度预留 借鉴虚拟内存分页,物理块离散存储与页表映射 vLLM 核心基石
FlashAttention GPU SRAM 与 HBM 之间的访存带宽墙 矩阵分块(Tiling)与在线流式 Softmax FlashAttention-2 / FlashAttention-3
投机采样 解码阶段逐字自回归的低吞吐低并发 小模型飞速草拟 + 大模型单步并行核验 vLLM、SGLang、Ollama 支持