视频加载失败

1.1 LLM 推理基础

4943 字
25 分钟
1.1 LLM 推理基础

把一个训练好的大模型”搬上线”提供服务,和把它训练出来,是两件目标完全不同的事。训练追求的是把海量数据喂进去、让 Loss 收敛;而推理追求的是在延迟和成本的约束下,尽可能快、尽可能多地生成 Token。这一节不谈任何优化技巧,只做一件事:把 LLM 推理的底层逻辑彻底讲清楚——它分成哪两个阶段、为什么要缓存 KV、显存花在了哪里、用什么指标衡量快慢,以及一个绕不开的灵魂拷问:为什么砸了几万块钱的 GPU,生成文字时算力却大部分在闲置?

读完这一节,你就有了一套”坐标系”。后续每一项推理优化技术(PagedAttention、Continuous Batching、量化、投机解码……)本质上都是在这套坐标系里的某个位置动刀,理解了地基,才能看懂它们各自在解决什么问题。

1. 自回归生成:Prefill 与 Decode 两阶段#

1.1 从”续写”理解自回归#

主流的大语言模型(GPT、Llama、Qwen 等)都是 Decoder-only 的自回归模型。“自回归”听起来抽象,其实就是一次只预测下一个词,然后把预测出来的词接到输入末尾,再预测下一个——像玩接龙一样,把刚说出口的字重新当成线索,猜下一个字。

用一句话概括生成过程:给定已有的 Token 序列 x1,x2,…,xtx_1, x_2, \dots, x_t,模型输出下一个 Token 的概率分布,采样得到 xt+1x_{t+1},再把它拼回去继续。

xt+1∼P(x∣x1,x2,…,xt)x_{t+1} \sim P(x \mid x_1, x_2, \dots, x_t)

这个”接龙”会一直进行,直到模型生成了结束符(EOS),或者达到了设定的最大长度。

📌 关键点:自回归的本质决定了生成是串行的——第 t+1t+1 个 Token 必须等第 tt 个 Token 算完才能开始。这个串行依赖,是后面所有延迟问题的总根源。

1.2 两个阶段:Prefill 和 Decode#

虽然都是”跑一遍模型”,但推理过程被清晰地切成了两个计算特性截然不同的阶段。

Prefill(预填充)阶段:处理你输入的整段 Prompt。假设你输入了 500 个 Token 的问题,模型会把这 500 个 Token 一次性、并行地送进网络,计算出每一层的中间结果,并生成第一个输出 Token。因为这一步要同时处理成百上千个 Token,矩阵乘法又大又密集,GPU 的算力被喂得很饱。

Decode(解码)阶段:从第二个输出 Token 开始,进入逐个生成的循环。每一步只输入上一步刚生成的那 1 个 Token,跑一遍完整的网络,吐出下一个 Token。生成 200 个 Token,就要串行地跑 200 次 Decode。

用一个类比:Prefill 像是读题——一口气把整道题读完,理解题意;Decode 像是答题——一个字一个字往外写,每写一个字都要回顾前面写过的所有内容。读题可以快速扫过,但答题只能一笔一画。

输入 Prompt

500 tokens

Prefill

并行处理全部 token

生成第 1 个 token

Decode step 1

输入 1 个 token

生成第 2 个 token

Decode step 2...

... 直到 EOS

输入 Prompt

500 tokens

Prefill

并行处理全部 token

生成第 1 个 token

Decode step 1

输入 1 个 token

生成第 2 个 token

Decode step 2...

... 直到 EOS

1.3 两阶段的计算特性对比#

📊 维度Prefill 阶段Decode 阶段
输入 Token 数整段 Prompt(几十到几千)每步仅 1 个
并行度高,Token 间可并行低,步与步之间串行
计算瓶颈Compute Bound(算力受限)Memory Bound(访存受限)
决定的指标首 Token 延迟(TTFT)每 Token 延迟(TPOT)
执行次数1 次输出长度 - 1 次

这张表里最反直觉的一行是计算瓶颈:为什么每步只算 1 个 Token 的 Decode,反而是”访存受限”、效率更低?这个问题我们留到第 4 节用 Roofline 模型正面回答,先记住这个结论——它是理解推理优化的分水岭。


2. KV Cache:用显存换计算#

2.1 不缓存会怎样#

回到自回归的串行特性。Decode 第 tt 步要计算 Attention,需要用当前 Token 的 Query 去和前面所有 Token 的 Key、Value 做注意力。问题来了:前面那些 Token 的 Key 和 Value,在上一步不是已经算过了吗?

如果什么都不缓存,每生成一个新 Token,都要把前面所有 Token 重新过一遍网络算出它们的 K、V——生成第 100 个 Token 时要重算 99 个,生成第 200 个时要重算 199 个,计算量随序列长度平方级膨胀。这显然是巨大的浪费。

💡 提示:KV Cache 的核心思想就是——把已经算过的 Key 和 Value 存起来,下一步直接复用,绝不重算。 这是一个典型的”用空间换时间”:牺牲显存,换掉大量重复计算。

注意一个不对称:Query 不需要缓存,因为每一步只用当前 Token 的 Query;而 Key、Value 是历史累积的、每步都要全量用到,所以缓存的是 K 和 V,这也是”KV Cache”名字的由来。

2.2 KV Cache 的生命周期#

一个请求的 KV Cache 会经历四个阶段,理解这个生命周期,才能明白后续 PagedAttention 到底在优化哪一环:

📥 分配 → ✍️ 填充 → 🔁 使用 → 🗑️ 释放

  • 分配:请求进来,推理引擎为它预留一块显存放 KV Cache
  • 填充:Prefill 阶段一次性把整段 Prompt 的 K、V 算出来写进缓存;Decode 每步再追加 1 个 Token 的 K、V
  • 使用:每一步 Decode 都要读取全部历史 KV 来算 Attention
  • 释放:请求生成结束(EOS 或达到长度上限),显存归还

2.3 显存账本:KV Cache 到底占多少#

这是必须会手算的一笔账。单个 Token 在 KV Cache 中占用的字节数为:

per_token_bytes=2×L×Hkv×dhead×bytes\text{per\_token\_bytes} = 2 \times L \times H_{kv} \times d_{head} \times \text{bytes}

各项含义:

  • 22:分别存 Key 和 Value 两份
  • LL:Transformer 层数(每一层都有独立的 KV Cache)
  • HkvH_{kv}:KV 头的数量(注意是 KV 头,不是 Query 头)
  • dheadd_{head}:每个头的维度
  • bytes\text{bytes}:数据类型字节数,FP16/BF16 为 2,FP8/INT8 为 1

一个请求的总占用,再乘以序列长度和并发请求数:

total_bytes=per_token_bytes×seq_len×batch_size\text{total\_bytes} = \text{per\_token\_bytes} \times \text{seq\_len} \times \text{batch\_size}

举个具体的例子(以一个 32 层、采用标准多头注意力即 32 个 KV 头、dhead=128d_{head}=128、FP16 的模型为例):

per_token_bytes=2×32×32×128×2=524288 bytes≈0.5 MB\text{per\_token\_bytes} = 2 \times 32 \times 32 \times 128 \times 2 = 524288 \text{ bytes} \approx 0.5\ \text{MB}

也就是说,每生成 1 个 Token,就要多吃约 0.5 MB 显存。当一个请求的上下文长到 4096 Token 时,光这一个请求的 KV Cache 就要吃掉约 2 GB。如果要同时服务 50 个这样的并发请求,仅 KV Cache 就是 100 GB——单张 80GB 的 GPU 直接爆掉。这就是为什么 KV Cache 的显存管理是推理优化的头号战场。

📌 关键点:HkvH_{kv} 在公式里是线性因子,这正是 GQA(分组查询注意力)/ MQA(多查询注意力) 能大幅省显存的原因——让多个 Query 头共享一组 KV 头。比如 32 个 Query 头共享 8 个 KV 头(GQA),KV Cache 直接砍到原来的 1/41/4,而模型表达能力几乎不受影响。现代大模型几乎清一色采用 GQA,就是这笔账算出来的。

2.4 碎片化问题#

KV Cache 还有一个隐蔽的坑:你事先不知道一个请求会生成多长。传统做法是按”最大可能长度”预留一整块连续显存,就像餐厅不管来几个人,都先给你留一张最大的桌子。结果是:请求实际只生成了 100 个 Token,却按 2048 预留,剩下的座位全部空占——这叫内部碎片;不同请求预留块之间的空隙又凑不成一整块可用空间——这叫外部碎片。

⚠️ 注意:碎片化会让 KV Cache 的实际显存利用率大打折扣,大量显存被无效预留白白浪费。这正是 vLLM 的 PagedAttention 要解决的核心问题——它把连续大块换成了操作系统式的分页管理(下一章详解)。


3. 关键性能指标:如何衡量”快”#

推理服务的”快”不是一个数字能说清的。用户体验、集群成本、SLA 承诺,分别对应不同的指标。搞混它们,优化就会跑偏。

3.1 延迟类指标:用户等多久#

TTFT(Time To First Token,首 Token 延迟):从用户提交请求到收到第一个 Token 的时间。它主要由排队时间 + Prefill 时间 + 网络延迟构成,Prompt 越长,Prefill 越久,TTFT 越大。对于聊天、流式输出这类场景,TTFT 直接决定”响应快不快”的第一印象。

TPOT(Time Per Output Token,每 Token 延迟),又称 ITL(Inter-Token Latency,Token 间延迟):生成阶段平均每个输出 Token 花的时间。它决定了文字”往外蹦”的速度——TPOT 是 50ms,用户看到的就是每秒约 20 个字的吐字速度。

💡 提示:TPOT 和 ITL 在多数语境下是同义词,都指 Decode 阶段的平均出字间隔。细微差别只在于计算时是否把首 Token 那一段算进去——NVIDIA GenAI-Perf 的做法是排除首 Token,即 ITL=(e2e_latency−TTFT)/(输出 Token 数−1)\text{ITL} = (\text{e2e\_latency} - \text{TTFT}) / (\text{输出 Token 数} - 1)。

端到端延迟(E2E Latency):从提交请求到收到完整响应的总时间。它可以拆成:

E2E=TTFT+TPOT×(输出 Token 数−1)\text{E2E} = \text{TTFT} + \text{TPOT} \times (\text{输出 Token 数} - 1)

这个公式非常实用——它告诉你:短输出场景 TTFT 主导,长输出场景 TPOT 主导。优化时要看清自己的业务落在哪一侧。

代入两个场景对比(设 TTFT =500 ms=500\ \text{ms}、TPOT =40 ms=40\ \text{ms}):

  • 短输出(生成 20 Token):E2E=500+40×19=1260 ms\text{E2E} = 500 + 40 \times 19 = 1260\ \text{ms},其中 TTFT 占约 40%40\%——首字慢用户就先炸了。
  • 长输出(生成 800 Token):E2E=500+40×799≈32460 ms\text{E2E} = 500 + 40 \times 799 \approx 32460\ \text{ms},TTFT 只占约 1.5%1.5\%——这时候优化 TPOT 才是正事。

同一套系统,业务形态不同,优化重心天差地别。这也是为什么”先测清楚自己的输入输出长度分布,再谈优化”是铁律。

3.2 吞吐类指标:机器能扛多少#

Throughput(吞吐量) 通常有两种口径:

📊 指标📝 含义适用视角
Token/s(系统)全系统每秒生成的总 Token 数衡量集群整体产能与成本
Token/s(单用户)单个用户感受到的出字速度约等于 1/TPOT1 / \text{TPOT}
RPS(Requests/s)每秒成功完成的请求数衡量并发承载能力

系统吞吐会随并发上升而增长,直到 GPU 资源饱和后趋于平稳甚至回落。吞吐和延迟通常是一对矛盾:把更多请求塞进一个 Batch 能提高系统吞吐,但每个请求的排队和计算时间变长,单用户延迟变差。

3.3 尾延迟与 Goodput:别被平均值骗了#

平均延迟是最容易骗人的指标。真实流量下,少数请求可能慢得离谱,而平均值把它们藏了起来。所以生产环境更看尾延迟:

  • P50:中位数,一半请求比它快
  • P95 / P99:95% / 99% 的请求比它快——这才是用户投诉的来源

📌 关键点:Goodput(有效吞吐) 是比 Raw QPS 更贴近真实价值的指标。它只统计满足 SLO(比如 TTFT < 1s 且 TPOT < 50ms)的那部分请求的吞吐。一个系统 QPS 很高但大部分请求都超时,Goodput 就很低——用户根本用不爽。后面第 7 章的 P/D 解耦、SLO 感知调度,优化目标正是 Goodput。


4. 瓶颈分析:为什么 Decode 打不满算力#

现在回答第 1 节埋下的问题。这是整个推理优化最重要的第一性原理,值得单独一节讲透。

4.1 算术强度:计算与访存的比值#

GPU 干活要同时做两件事:从显存(HBM)搬数据,和用计算单元算数据。谁慢谁就是瓶颈。衡量一个任务”偏计算还是偏访存”,用算术强度(Arithmetic Intensity):

算术强度=浮点运算次数 (FLOPs)访存字节数 (Bytes)\text{算术强度} = \frac{\text{浮点运算次数 (FLOPs)}}{\text{访存字节数 (Bytes)}}

直白理解:每从显存搬 1 个字节的数据,能顺带做多少次计算。这个比值高,说明数据搬来后被充分利用,算力吃得饱;比值低,说明算力在干等数据,大部分时间浪费在搬运上。

4.2 Roofline 模型:一张图看懂瓶颈#

Roofline 模型把这件事画成一张图:横轴是算术强度,纵轴是实际能达到的算力。图上有一条”屋顶线”——左半段是被显存带宽压住的斜坡(Memory Bound 区),右半段是被峰值算力压住的水平线(Compute Bound 区)。两段的交点,就是这块 GPU 的”平衡点”算术强度。

低算术强度

Memory Bound

(Decode 在这)

平衡点

高算术强度

Compute Bound

(Prefill 在这)

低算术强度

Memory Bound

(Decode 在这)

平衡点

高算术强度

Compute Bound

(Prefill 在这)

一个任务落在斜坡上(算术强度低于平衡点),瓶颈就是显存带宽,无论 GPU 算力多强都用不上;落在水平线上,才是算力受限、真正把 GPU 榨干了。

平衡点怎么算?就是峰值算力除以显存带宽。以 NVIDIA H100 SXM 为例,其 BF16 稠密算力约 990 TFLOPS990\ \text{TFLOPS}(官方标称的 1979 TFLOPS1979\ \text{TFLOPS} 是开启稀疏后的数值,稠密约为一半),HBM3 带宽为 3.35 TB/s3.35\ \text{TB/s}:

平衡点=990×1012 FLOP/s3.35×1012 Byte/s≈295 FLOP/Byte\text{平衡点} = \frac{990 \times 10^{12}\ \text{FLOP/s}}{3.35 \times 10^{12}\ \text{Byte/s}} \approx 295\ \text{FLOP/Byte}

也就是说,在 H100 上,一个任务每从显存搬 1 字节数据,得配上约 295 次浮点运算,才刚好把算力和带宽同时喂饱。低于这个值就落在斜坡上——这个数字请记住,下一节我们会拿它当标尺。

💡 提示:不同 GPU 的平衡点不同,但都在同一量级(几百 FLOP/Byte)。带宽升级往往比算力升级更”值钱”,正是因为绝大多数 Decode 负载都死死卡在斜坡左侧的带宽墙上。

4.3 把 Prefill 和 Decode 放上去#

  • Prefill:一次并行处理成百上千个 Token,权重从显存搬一次(batch 内所有 Token 共享),却要做海量矩阵乘法——算术强度高,落在 Compute Bound 区,GPU 算力吃得饱。
  • Decode:每步只处理 1 个 Token,但为了算这 1 个 Token,得把整个模型的权重从显存完整搬一遍。搬了几十上百 GB 的权重,只做了一个 Token 的计算——算术强度极低,死死落在 Memory Bound 区。

用数字感受一下这个悬殊。Decode 每步的核心是”权重矩阵 × 单 Token 向量”。对一个权重量为 WW(字节数)的模型,FP16 下每步:搬运约 WW 字节权重,对应 W/2W/2 个权重,每个权重参与约一次乘加,即做约 W/2W/2 次乘加(按一次乘加计 2 FLOPs,即约 2×W/2=W2 \times W/2 = W FLOP)。于是单请求 Decode 的算术强度约为:

算术强度decode≈W FLOPW Byte≈1 FLOP/Byte\text{算术强度}_{\text{decode}} \approx \frac{W\ \text{FLOP}}{W\ \text{Byte}} \approx 1\ \text{FLOP/Byte}

对比上一节算出的 H100 平衡点约 295 FLOP/Byte295\ \text{FLOP/Byte},单请求 Decode 的算术强度低了整整两个数量级——GPU 算力利用率理论上还不到 1%1\%。这就是”Decode 打不满算力”最赤裸的数字证据。而把 BB 个请求拼成一个 Batch,权重仍只搬一次却服务 BB 个 Token,算术强度直接放大 BB 倍,逼近平衡点——这就是 Batching 的威力从何而来。

🔑 核心概念:Decode 慢,不是因为算不过来,而是因为”搬不过来”。GPU 的计算单元大部分时间在空等权重从显存加载。这就解释了两个关键现象:

  1. 为什么 Batch 能大幅提升吞吐却几乎不增加延迟:把多个请求的 Decode 拼成一个 Batch,权重只搬一次就能服务整个 Batch 的所有 Token,算术强度成倍提升,把闲置的算力利用起来——这正是 Continuous Batching 的理论依据。
  2. 为什么量化(尤其是 Weight-only)对 Decode 加速明显:Decode 瓶颈在搬权重,把权重从 FP16 压到 INT4,要搬的字节数直接减少到 1/41/4,延迟随之大幅下降。

理解了这一点,你就掌握了看穿几乎所有推理优化技术的”透视镜”。


5. 推理全链路拆解#

最后把一个请求从进到出的完整链路走一遍,标出每个环节的计算特性,方便你日后定位瓶颈到底卡在哪。

📥 Tokenize

文本→token id

Prefill

Compute Bound

Decode Loop

Memory Bound

Sampling

选下一个 token

📤 Detokenize

token id→文本

📥 Tokenize

文本→token id

Prefill

Compute Bound

Decode Loop

Memory Bound

Sampling

选下一个 token

📤 Detokenize

token id→文本

环节做什么计算特性潜在瓶颈
Tokenize把输入文本切成 Token ID 序列CPU 任务长文本或高并发时 CPU 成瓶颈
Prefill并行处理全部 Prompt Token,产出首 Token 与初始 KV CacheGPU,Compute BoundPrompt 很长时拉高 TTFT
Decode Loop逐 Token 生成,每步读全量 KV、追加新 KVGPU,Memory Bound显存带宽,决定 TPOT
Sampling从输出分布按 Temperature/Top-p 等采样下一个 TokenGPU,轻量复杂采样或约束解码略增开销
Detokenize把 Token ID 拼回可读文本,流式返回CPU 任务与主循环争抢 CPU

💡 提示:Tokenize / Detokenize 是 CPU 任务,却容易被忽视。当 GPU 优化到位后,CPU 上的这些环节反而可能成为新瓶颈——这正是 vLLM V1 引擎用多进程把它们和 GPU 核心循环重叠起来的原因(第 3 章详解)。


📝 总结#

这一节我们不谈优化技巧,而是把 LLM 推理的地基铺清楚:

  • 两阶段:Prefill 并行读题(Compute Bound,决定 TTFT),Decode 串行答题(Memory Bound,决定 TPOT)。
  • KV Cache:用显存换计算,避免重算历史 K/V;显存开销 =2×L×Hkv×dhead×bytes×seq_len= 2 \times L \times H_{kv} \times d_{head} \times \text{bytes} \times \text{seq\_len},GQA 靠减小 HkvH_{kv} 省显存,碎片化催生了 PagedAttention。
  • 性能指标:TTFT / TPOT 分别刻画首字与出字速度,E2E == TTFT ++ TPOT ×\times (输出长度 −1-1);生产环境看 P95/P99 尾延迟和满足 SLO 的 Goodput。
  • 瓶颈本质:Decode 是 Memory Bound——GPU 在等权重搬运,而非算不过来。这解释了 Batching 提升吞吐、量化加速 Decode 的根本原因。

带着这套坐标系,后面每一章的优化技术都能对号入座:它在攻 TTFT 还是 TPOT?在省显存还是提吞吐?在斜坡上还是水平线上动刀?

🎯 自我检验清单#

  • 能说清 Prefill 和 Decode 两阶段在输入规模、并行度、计算瓶颈上的差异
  • 能解释为什么需要 KV Cache,以及为什么只缓存 K、V 而不缓存 Q
  • 能手算一个给定配置模型每 Token 的 KV Cache 显存开销
  • 能说明 GQA/MQA 为什么能省 KV Cache,以及大致能省多少
  • 能准确区分 TTFT、TPOT/ITL、Throughput、尾延迟和 Goodput 的含义与适用场景
  • 能用 E2E 延迟公式判断某业务场景是 TTFT 主导还是 TPOT 主导
  • 能用算术强度和 Roofline 解释为什么 Decode 是 Memory Bound
  • 能从”Decode 卡在搬权重”推导出 Batching 提吞吐、Weight-only 量化加速 Decode 的原因
  • 能画出推理全链路并指出每个环节的计算特性与潜在瓶颈

📚 参考资料#


内容来源

本文整理自开源项目 AIInfraGuide 的《LLM 推理基础》(原文),内容版权归原作者所有。

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
Hyflu
专注 AI Infra 与 Agent 技术分享
公告
博客搭建中,将分享 AI Infra 与 Agent 相关技术文章。
分类
标签
最新动态
站点统计
文章
12
分类
3
标签
27
总字数
45,635
运行时长
0 天
最后活动
0 天前
文章目录