主题
LLM 推理全链路剖析
概念文章告诉你"模型在预测下一个词",这一篇拆开给你看预测是怎么发生的:一次请求从进入服务到吐出第一个字,中间每一步在算什么、瓶颈在哪、工程优化优的是哪一段。读完它,《大模型缓存是什么》里的每个优化点你都能对应到具体环节。
1. 全链路地图
text
你的 prompt(文本)
→ 1. Tokenizer 切分成 token id 序列
→ 2. Embedding 查表得到向量,注入位置信息
→ 3. Prefill:整段序列并行过 N 层 Transformer,算出每个位置的 KV
→ 4. Decode:一个 token 一个 token 自回归生成
每步:取最后一个位置的表示 → 预测下一个 token → 采样 → 拼回输入
→ 5. 命中停止条件(EOS / stop 序列 / max_tokens),返回两条时间线对应两个阶段:首字延迟(TTFT)主要由 Prefill 决定,后续每个字的间隔(TPOT)由 Decode 决定。性能优化先分清你在优化哪一段。
2. Tokenizer:文本如何变成数字
2.1 BPE 的直觉
模型不认识字符,只认识词表里的整数 id。主流分词器用 BPE(Byte Pair Encoding)思想:从字节/字符开始,迭代把语料中最高频的相邻组合合并成新 token,直到达到预设词表规模(如 10 万)。
结果是一个统计学习出的切分器,不是字典:
text
"learning" 可能是 1 个 token
"learn" + "ing" 可能是 2 个
生僻字、 emoji、很长的数字串 → 退化为字节序列,一个字符吃掉多个 token2.2 工程推论
- 中英成本不对等:同一含义中文常常消耗更多 token,这就是"输入 token 比输出多 20 倍"账单问题的一部分。
- 数字与代码是重灾区:
3.14159可能被切成五六个 token,所以结构化数据 frequently 用字符串比较而不是让模型算。 - 计费与窗口都按它算:《模型、API 与客户端》里的成本模型,单位就是这里的 token。
3. Embedding 与位置:模型眼中的输入
每个 token id 通过一张 embedding 矩阵(形状 词表 × d_model)查出一个向量——这一步是纯查表,没有计算。之后通过 RoPE 一类的旋转位置编码把"这个 token 在第几位"的信息注入向量。
关键理解:进入 Transformer 后,每个 token 的向量在层与层之间不断被"改写"——它逐步吸收上下文,从"这个词是什么"变成"在这个语境里这个词意味着什么"。这个不断改写的主干通道,业界常称为 residual stream(残差流)。
4. Prefill 与 Decode:为什么两段性能特征完全不同
4.1 Prefill:并行、算力瓶颈
Prompt 的全部 token 一次性并行通过所有层:每个位置同时算出注意力输出,并产生对应的 K、V 缓存。GPU 擅长这种大矩阵乘法,prefill 是算力瓶颈(compute-bound)。
这直接解释了开头地图里的现象:
- prompt 越长,首字延迟越长——近乎线性;
- Prompt Cache 复用的就是 prefill 产物:公共前缀的 KV 算一次存下来,后续请求跳过这段计算。
4.2 Decode:串行、访存瓶颈
生成是自回归的:每一步只算一个新 token——取序列最后一个位置的表示,映射到词表打分,采样出一个 token,拼回输入,再算下一步。每生成一个字都要完整读一遍全部模型权重,所以 decode 是访存瓶颈(memory-bound),GPU 算力大量闲置。
工程上的应对正是围绕这一点:
- Continuous batching:把几十个请求的 decode 步骤拼在一个批次里跑,共享一次权重读取;
- 投机解码(Speculative Decoding):用小模型先猜几个 token,大模型一次并行验证,把串行 decode 部分并行化。
4.3 KV Cache:decode 能进行的全部前提
自回归生成时,前面 token 的 K、V 不能每次重算——那会变成 O(n²)。所以服务端为每个序列缓存:
text
KV Cache 大小 ≈ 2 × 层数 × KV头数 × 每头维度 × 序列长度 × 精度字节数这就是《大模型缓存是什么》说的三层缓存的底层:KV Cache 是显存里的必需品,Prompt Cache 是跨请求复用它,语义缓存是干脆跳过整个模型。三层依次更粗粒度,也依次更"外挂"。
由此还能理解两个现象:
- 长上下文服务的成本高:KV Cache 随序列长度线性吃显存,128K 窗口的账不止是算力;
- GQA / MQA(分组查询注意力):让多个 Q 头共享一组 KV 头,把 KV Cache 压缩几倍到几十倍——这是"上下文窗口变大"的显存账。
5. 采样:从概率分布到具体的那个字
Decode 每步的输出不是"一个字",而是词表上的完整概率分布(logits 经 softmax 归一化)。采样决定怎么从中挑一个:
text
logits / temperature # 温度:缩放分布。T→0 趋近贪心,T 越大越平
→ top-k:只保留概率最高的 k 个
→ top-p(核采样):保留累计概率达到 p 的最小集合,再归一化
→ 按概率随机抽出一个 token几个此前文章里"结论式"的说法,在这里能看到机制:
- temperature=0 是贪心解码:每步直接取最大概率 token,没有随机;但 GPU 并行浮点的非确定性意味着它仍不保证逐字复现。
- 幻觉的采样视角:分布只反映"语料里像不像",不反映"真不真"——《推理的边界》会展开这一点。
- 降温和限谱换稳定:top-k/top-p 收窄分布,输出更稳,同时牺牲多样性——写代码与写诗该用不同配置,原因就在这里。
6. 停止条件与返回
生成在以下任一条件下终止:采样到 EOS token、命中 stop 序列、达到 max_tokens、服务端超时或预算熔断。工程上还要叠加:流式输出(decode 出一个 token 就推一个)、用量统计(就是你在账单里看到的 prompt/completion token 数)、以及《生产级 AI 应用架构》里的超时与降级。
7. 一张表:现象 → 机制 → 对策
| 你观察到的现象 | 底层机制 | 对应文章的对策 |
|---|---|---|
| prompt 长则首字慢 | Prefill 算力瓶颈,随长度近线性 | 公共前缀瘦身、Prompt Cache |
| 长会话费用线性上涨 | 无状态 + 全量历史重放 | 历史裁剪/摘要化 |
| "记得"久远的对话突然失效 | KV Cache/窗口按 token 数截断 | 关键信息前置、外部记忆 |
| 输出格式偶发崩坏 | 采样是从分布里抽,不是查表 | 受限解码 + schema 校验 |
| 高并发时吞吐上不去 | Decode 访存瓶颈 | Continuous batching(服务端) |
下一步:想知道"注意力到底是什么、为什么它能表达语义",读《Transformer 与注意力机制》;想深入 RAG 检索的向量数学与索引结构,读《Embedding 与向量检索》。
本文以工程视角做机制剖析,省略了实现细节(如注意力核优化、量化、多卡切分)。数值与结构以各厂商官方技术报告为准。