1. 基础
从一次模型请求理解大语言模型的输入、计算与系统代价。
1 概览:一次模型请求的生命周期
当用户向一个 AI 助手发送一条消息时,系统是如何把原始文本一步步变成下一个生成 token 的?
把场景再具体一点。假设一个用户在周一早上给内部 AI 助手发来一条消息:
“昨晚支付服务为什么超时?请根据下面的日志和工单,给我一个 5 句以内的摘要,并标出最可能的根因。”
对用户来说,这只是一次普通问答;对系统来说,这是一条会立刻触发完整推理流水线的请求。原始字符串先被 tokenizer 切成 token,长日志在 prefill 阶段消耗算力,历史上下文开始占用 KV cache,模型在最后一个位置输出 logits,解码器再把概率分布变成第一个可见 token。首个字出现之前,延迟、显存和成本其实已经被 prompt 长度、模型架构和系统实现共同决定了。
这是本章唯一真正需要回答的问题。
如果你能把这条链路讲清楚,那么分词、嵌入、注意力、解码、长上下文和排障就不再是彼此割裂的概念,而会变成同一个系统的不同层面。
本章会沿着一条固定流水线往下走:
text → tokens → embeddings → transformer → logits → decoding → text
这条流水线既是理解 LLM 的最小心智模型,也是工程排障时最有用的坐标系。
很多看起来像“模型变笨了”的问题,最后都不是出在参数本身,而是出在这条链路的某个环节:tokenizer 不一致、上下文太长、KV cache 膨胀、解码参数失衡,或者服务系统把正确的分布变成了错误的行为。
2 输入表示
本节先回答几个关键问题:
- 为什么现代 LLM 使用子词分词,而不是词级或字符级分词?
- 词表大小如何在质量、序列长度和计算成本之间做权衡?
- 模型为什么不能直接使用 token ID 作为数字输入?
- embedding lookup 为什么等价于 one-hot 向量乘以嵌入矩阵?
- 为什么 Transformer 还需要额外的位置表示?
输入表示的核心结论很简单:模型先看到的不是“文字”,而是一串被编码过的离散符号。
对 LLM 来说,原始字符串必须先被切分成 token,再被映射成向量,最后才进入后续网络计算。也正因此,输入表示不是预处理细节,而是整个模型接口的一部分。
2.1 分词:把字符串变成符号序列
分词的作用,是把原始文本映射成 token 序列。它看起来像预处理,实际上却决定了模型能否稳定覆盖专业术语、多语言文本、数字、代码和噪声输入,也直接决定了序列长度和推理成本。
主流 LLM 之所以不采用词级分词,是因为词级分词会迅速遇到 OOV(未登录词)问题。新词、形态变化、拼写噪声和专业术语会迫使词表无限膨胀。字符级分词则走向另一个极端:几乎不会 OOV,但序列会变得很长,而长序列意味着更昂贵的注意力计算。
因此,现代 LLM 普遍采用子词分词(subword tokenization)。它的工程意义不是“语义更高级”,而是让系统在两件事之间取得平衡:
- 高频模式尽量短,减少 token 数;
- 稀有模式仍然可表示,避免 OOV。
最常见的三类方法分别是:
BPE(Byte-Pair Encoding)
从较小单元出发,不断合并高频相邻片段,逐步长出词表。它简单、成熟、兼容性好。
WordPiece
也做合并,但更偏向“哪些合并更有助于语言模型概率”。可以把它理解成带语言模型目标的 BPE 变体。
Unigram LM
从一个较大的候选词表开始,再不断删除对整体似然帮助最小的词元。给定字符串 \(s\),它会在所有合法切分中选择概率最大的那一种:
\[ \hat{t}_{1:m} = \arg\max_{t_{1:m}:\,\mathrm{concat}(t_{1:m})=s} \prod_{i=1}^{m} p(t_i) \]
这三类方法的差别,本质上是如何学习符号边界。
如果你在做多语言、代码或脏数据较多的语料,byte-level BPE 往往更稳,因为它先在字节层面保证可表示性,再在字节上继续合并。它不是简单的字符级分词,而是一种更稳健的底层编码方案。
一个具体例子能更直观看到分词粒度如何改写系统成本。考虑一段很常见的工程文本:
microservice_payment_v2 failed at 03:14
下面的切分只是示意,不代表某个具体 tokenizer 的真实输出,但足够说明问题:
| 粒度 | 示意切分 | 对词表大小 \(V\) 的压力 | 对序列长度 \(L\) 的压力 | 工程含义 |
|---|---|---|---|---|
| 词级 | microservice_payment_v2 failed at 03:14 |
高 | 低 | 标识符和罕见字符串容易 OOV |
| 字符级 | m i c r o … |
很低 | 很高 | 几乎无 OOV,但序列很长 |
| 子词级 | micro service _payment _v 2 failed at 03 : 14 |
中 | 中 | 主流折中方案 |
| byte-level | 与子词类似,但未知片段可回退到字节 | 中 | 中到偏高 | 对代码、脏数据、多语言更稳健 |
这个例子说明 tokenizer 为什么不是小细节。
同一条报错日志,如果被切得更碎,后面的 token 数就会上升;token 数一旦上升,API 成本、上下文占用和注意力计算都会一起上升。代码标识符、混合数字字符串、URL、文件路径和多语言文本,往往正是把碎片率拉高的主要来源。
2.2 词表大小、碎片率与成本
分词不是只影响“看起来怎么切”,它还会改变系统成本。
一段文本被切得越碎,token 数就越多;而 token 数越多,API 成本越高,上下文越容易被塞满,后续注意力计算也越贵。这就是很多工程团队会关注的碎片率(fragmentation rate)。
词表大小的权衡可以用一条很直观的关系理解:
\[ \text{embedding / softmax 开销} \propto Vd, \qquad \text{attention 开销} \propto L^2 \]
其中:
- \(V\) 是词表大小;
- \(d\) 是表示维度;
- \(L\) 是序列长度。
词表更大时,embedding 层和输出投影更重;词表更小时,文本会被切得更碎,序列更长,注意力成本上升。没有永远最优的词表大小,只有和语料分布、语言覆盖、部署预算一起确定的折中点。
这也是为什么不能直接比较使用不同 tokenizer 的模型困惑度。困惑度是按 token 序列定义的,而不同 tokenizer 会把同一文本切成不同的 token 边界和不同数量的 token。分母变了,数字就不再处于同一坐标系。
2.3 嵌入:把离散符号映射到向量空间
分词之后得到的是 token ID,但 ID 只是标签,不具有连续数值的几何意义。
ID=100 并不比 ID=3 “更接近某个概念”。如果把它直接当连续数值输入,模型会学到本来不存在的顺序关系。
embedding 的作用,就是把离散 token 映射到连续向量空间。设词表大小为 \(V\),嵌入维度为 \(d\),嵌入矩阵为
\[ E \in \mathbb{R}^{V \times d} \]
对于第 \(t\) 个 token,它的 embedding 可以写成
\[ e_t = E[t] \]
如果把它写成 one-hot 向量 \(x_t \in \mathbb{R}^{V}\),那么同样的操作也可以写成
\[ e_t = x_t^\top E \]
这说明 embedding lookup 表面是查表,本质上是稀疏线性映射。
也正因为如此,embedding 不只是“查个向量出来”,而是离散符号世界和神经网络计算之间的接口层。
很多 decoder-only LLM 还会使用权重绑定(weight tying),即让输入 embedding 和输出投影共享参数:
\[ W_{\text{out}} = E^\top \]
这样做的原因很直接:模型读入 token 和预测 token,本质上是在使用同一个符号空间。共享参数既减少参数量,也常能提高样本效率。
2.4 位置表示:让模型知道顺序
仅有 token embedding 还不够,因为注意力本身并不天然携带顺序感。
如果不给模型任何位置信息,“人咬狗”和“狗咬人”会使用几乎相同的一组 token,顺序差异很难被稳定表达。
最经典的绝对位置编码是正弦位置编码:
\[ PE(t,2i)=\sin\left(\frac{t}{10000^{2i/d}}\right), \qquad PE(t,2i+1)=\cos\left(\frac{t}{10000^{2i/d}}\right) \]
它给每个位置分配一个固定向量,再和 token embedding 相加。
为什么要用多频率的 \(\sin/\cos\)?因为单一频率只能表达一种尺度的位移变化,多频率组合才能让模型同时感知短距离和长距离的顺序模式。
现代 LLM 更常见的是:
- 相对位置偏置:更强调“距离”而不是“索引”;
- RoPE:直接对 \(Q/K\) 做位置相关旋转,让相对位移体现在点积几何中;
- ALiBi:在注意力分数里加入和距离成线性关系的偏置。
这里要记住的不是方法名,而是工程含义:位置方案会影响模型的长度外推能力。
如果模型只在 8K 长度上训练,你却希望它稳定处理 128K,上下文失败往往不只是“模型不够大”,还可能是位置表示开始失真。
2.5 小结
输入表示层最值得记住的三点是:
- tokenizer 决定模型看见什么符号,也决定 token 数和成本;
- embedding 把离散符号映射到可学习的向量空间;
- 位置信息决定模型能否稳定理解顺序和长度。
工程上,token 数本身就是成本变量。
同一段用户输入,如果被切成更多 token,就会同时推高:
- API 计费;
- 上下文占用;
- prefill 计算量;
- 长上下文下的整体延迟。
因此,输入表示从来不是“模型前面的准备工作”,而是系统成本的第一层。
3 Transformer Blocks
本节先回答几个关键问题:
- 缩放点积注意力是怎样工作的?为什么要除以 \(\sqrt{d_k}\)?
- 什么是因果掩码?为什么 decoder-only 模型必须有它?
- 多头注意力为什么不是“一个大头就够了”?
- 如果注意力已经在 token 之间混合信息,为什么还需要 FFN/MLP?
- 为什么残差连接和归一化对深层 Transformer 如此关键?
Transformer 的核心不是“很多层线性代数”,而是一种特殊的计算分工。
在一层典型的 decoder block 中,系统会先通过注意力在 token 之间路由信息,再通过 FFN 在单个 token 内部做非线性变换,最后依靠残差和归一化让整个深层网络仍然可训练。
用 pre-norm 写法表示,一个基本块可以写成:
\[ h^{(l+\frac{1}{2})} = h^{(l)} + \mathrm{Attention}(\mathrm{Norm}(h^{(l)})) \]
\[ h^{(l+1)} = h^{(l+\frac{1}{2})} + \mathrm{FFN}(\mathrm{Norm}(h^{(l+\frac{1}{2})})) \]
这两行式子基本上已经概括了现代 LLM 的主计算模式。
3.1 注意力:内容驱动的检索系统
对输入隐藏态 \(X \in \mathbb{R}^{L \times d}\),模型会先投影出三组向量:
\[ Q = XW_Q, \qquad K = XW_K, \qquad V = XW_V \]
其中:
- Q(query):我正在寻找什么信息;
- K(key):我这里能提供什么匹配线索;
- V(value):如果我被关注,我真正贡献什么内容。
标准缩放点积注意力写成:
\[ \mathrm{Attention}(Q,K,V) = \mathrm{softmax}\left(\frac{QK^\top + M}{\sqrt{d_k}}\right)V \]
其中 \(M\) 是掩码矩阵。
除以 \(\sqrt{d_k}\) 的原因,是避免维度变大时点积分数方差过高,导致 softmax 过早饱和。
最稳的直觉是:注意力本质上是一个内容驱动的检索系统。
每个位置拿自己的 query 去匹配全序列里的 key,决定该从哪些位置读取 value。它不是按固定窗口读取,而是根据当前内容动态路由信息。
例如在句子“服务器重启后,它恢复了服务”里,“它”更可能去读“服务器”的表示,而不是“重启”这个动作本身。注意力做的就是这种依赖于上下文内容的选择性读取。
3.2 因果掩码与多头注意力
对于 decoder-only 模型,自回归目标要求位置 \(i\) 在预测下一个 token 时不能看到未来位置 \(j>i\)。
这就是因果掩码的作用:把未来位置的分数置为极小值,让 softmax 后概率变成 0。没有因果掩码,训练时模型就会偷看答案,训练目标和推理过程不一致。
多头注意力(MHA)的意义则在于分工。
如果只用一个大头,模型必须用同一套表示同时学习局部语法、长程依赖、格式边界和其他关系模式。多个头让系统可以在不同子空间里并行学习不同的关注模式,因此表达力和稳定性都更好。
当 query 来自一个序列,而 key/value 来自另一个序列时,就得到交叉注意力(cross-attention)。这在 encoder–decoder 和多模态模型中非常常见。
3.3 FFN:在单个位置内部制造特征
注意力负责“去哪里读”,但它不负责“读到之后怎么加工”。
FFN(Feed-Forward Network)正是承担这件事的子层。最常见形式是:
\[ \mathrm{FFN}(x)=W_2 \,\sigma(W_1x+b_1)+b_2 \]
如果没有这个非线性子层,多层线性变换可以折叠成一层,模型表达力会明显受限。
因此,注意力和 FFN 不是替代关系,而是分工关系:
- 注意力:token 之间的信息交换;
- FFN:单个 token 内部的特征变换。
在现代 LLM 里,FFN 常常是参数量和计算量的大头之一。很多模型会先把维度扩张到更高,再投回原维度;带门控的 SwiGLU 结构则进一步提高了表达效率。你可以把它理解为:注意力负责“路由信息”,FFN 负责“制造特征”。
3.4 残差连接与归一化:为什么深层网络还能训得动
残差连接的形式很简单:
\[ x_{l+1} = x_l + f(x_l) \]
但它的意义非常大。它给网络提供了一条显式恒等路径,使每一层不必推倒重来,而更像是在旧表示上学习增量修正。深层网络中的梯度也因此更容易传播。
归一化则负责控制表示尺度。Transformer 里最常见的是:
- LayerNorm:归一化均值和方差;
- RMSNorm:只按均方根缩放,更简洁,也更符合很多现代 decoder-only 配方。
另一个关键选择是 pre-norm 和 post-norm。现代大多数深层 LLM 更偏向 pre-norm,因为在大规模训练中它通常更稳定。
3.5 小结
Transformer block 最值得记住的一句话是:
注意力负责跨 token 的信息路由,FFN 负责单 token 的非线性变换,残差与归一化负责让深层网络可训练。
如果你从更老的神经网络架构看它,它和 RNN 最大的差别在于:
Transformer 在 prefill 阶段可以并行处理整个序列,不依赖递归状态沿时间步传递;它用注意力来做可学习的信息选择,而不是靠固定卷积窗口或单向递归隐状态传播。这既是它表达力强的原因,也是它在长上下文下成本高的原因。
5 解码
本节先回答几个关键问题:
- 贪心搜索、温度采样、top-k 和 top-p 分别在做什么?
- 温度在数学上改变了什么?极低和极高温度各会带来什么问题?
- 为什么束搜索常常让开放式生成变得乏味?
- 为什么同一个模型只改解码参数,就可能出现重复、幻觉或不稳定?
- 结构化输出和 tool calling 为什么高度依赖 stop 设计与约束解码?
模型只负责给出概率分布,解码(decoding)负责把这个分布变成真实文本。
因此,解码不是“最后一步小技巧”,而是系统行为的直接控制杆。
5.1 贪心、温度、top-k、top-p
最简单的方式是贪心解码(greedy decoding):
每一步都选概率最大的 token。它速度快、确定性强,但很容易模板化和陷入重复。
如果想保留多样性,就需要先对 logits 做温度缩放:
\[ p_i = \frac{\exp(z_i/T)}{\sum_j \exp(z_j/T)} \]
其中:
- \(T < 1\):分布更尖锐,更保守,更接近确定性;
- \(T > 1\):分布更平坦,更随机,也更容易失真。
然后常见的采样策略有:
top-k
只保留概率最高的前 \(k\) 个 token,再从中采样。优点是简单;缺点是候选边界固定,不同 prompt 下不一定稳。
top-p(nucleus sampling)
不固定候选个数,而是取累计概率达到 \(p\) 的最小候选集。它更自适应,因此在开放生成里通常更稳。
beam search
并行保留多条高分路径,更偏向高似然输出。但也正因为它偏高似然,在开放式对话或创作任务里经常显得保守、同质化。
5.2 解码为什么会制造问题
很多“模型问题”其实是解码问题。
幻觉
如果模型本身对答案并不确定,但你又使用了偏高温度、较大的 top-p,而且没有 grounding 或引用约束,尾部分布中的低质量 token 就更容易被采样出来,最终表现为高置信幻觉。
重复
如果模型进入高概率自循环,而你又使用 greedy 或极低温度,它就可能不断沿着同一条局部高分路径滑下去。重复惩罚可以缓解,但过强的重复惩罚又可能误伤代码、JSON、表格和固定模板中的合法重复。
不稳定
结构化输出场景里,温度稍高一点、stop sequence 稍差一点、JSON 边界稍模糊一点,就可能让输出突然从“完全可解析”变成“几乎不能消费”。这不是小幅质量波动,而是产品级故障。
5.3 工程权衡:质量、随机性与稳定性
从工程角度看,解码是在三个目标之间做权衡:
- 质量:答案是否准确、有帮助;
- 随机性:输出是否足够多样,不显得机械;
- 稳定性:格式、结构和工具调用是否可依赖。
因此,不同任务应当使用不同策略:
- 工具调用、JSON、结构化输出:低温度,必要时约束解码;
- 聊天问答:适度温度和 top-p,兼顾自然性与稳定性;
- 头脑风暴、候选生成:可以提高随机性,但后面必须接排序器或验证器。
stop sequence 在这里尤其关键。
对于 tool calling、代码补全、JSON 输出这类场景,stop 不是“顺手加一个结束符”,而是边界控制机制。如果 stop 设计不合理,模型就会越界生成、截断错误,或者把结构化结果和普通文本粘在一起。
一句话本章小结:权重决定概率分布,解码决定系统行为。
6 架构变体
本节先回答几个关键问题:
- encoder-only、decoder-only 和 encoder–decoder 分别适合什么任务?
- 为什么大多数现代聊天 LLM 都是 decoder-only?
- 什么场景下你会优先选择 BERT 风格模型,而不是 GPT 风格模型?
- MoE 在这里扮演的是什么角色?它为什么不是“另一种任务架构”?
- 哪一类架构选择最容易主导解码阶段的推理内存?
“架构”回答的问题,不只是“模型有多少层”,而是模型如何读取上下文、如何组织计算、以及系统把成本压在哪一侧。
在工程里,架构选择通常先问五个问题,而不是先背三种名字:
- 任务是表示学习,还是开放式生成?
- 输入和输出是否天然分成两段?
- 服务时最贵的是 prefill 还是 decode?
- 你是否愿意为了更强的输入建模接受更复杂的推理路径?
- 你是在扩大总参数量,还是在降低每步激活成本?
可以用下面这张决策表先抓住主线:
| 主要需求 | 更自然的架构选择 | 原因 | 典型代价 |
|---|---|---|---|
| 分类、检索、排序、reranking | encoder-only | 双向建模更利于表示学习 | 不适合长文本自回归生成 |
| 聊天、补全、代码生成、tool use | decoder-only | 训练目标与推理接口统一 | decode 路径和 KV cache 成本突出 |
| 翻译、摘要、改写、严格 seq2seq | encoder–decoder | 输入输出分工明确 | 系统路径更复杂,部署更重 |
| 在固定 FLOPs 下放大总参数 | MoE(叠加在主架构上) | 稀疏激活提高参数效率 | 路由、负载均衡、通信变复杂 |
换句话说,架构首先决定的是产品接口、推理路径和成本曲线,其次才是论文里的类别名。
6.1 Encoder-only
encoder-only 模型使用双向注意力,当前位置可以同时看到左边和右边上下文。
它们非常适合表示学习,因此常用于分类、匹配、排序、检索、reranking 等任务。BERT、RoBERTa、DeBERTa 都属于这一类。
如果你的任务本质上是“把输入编码成一个高质量表示”,而不是“继续生成一个长输出”,encoder-only 往往是更自然的选择。
6.2 Decoder-only
decoder-only 模型使用因果掩码,只能看到当前位置之前的内容。
它的训练目标与推理方式天然一致:
\[ P(x_t \mid x_{<t}) \]
这正是今天主流聊天模型的范式。GPT、LLaMA、Mistral、Qwen 等都属于这一类。
为什么现代聊天 LLM 几乎都选 decoder-only?因为它把三件事统一成了一条接口:
- 训练目标是 next-token prediction;
- 推理过程是逐 token 自回归生成;
- 产品输入可以统一表达为“继续补全当前上下文”。
这让系统接口极其简单:系统提示、用户消息、检索片段、工具返回值,最终都可以被拼接成上下文,然后让模型继续写下去。
6.3 Encoder–decoder
encoder–decoder 架构把输入和输出分开处理。encoder 先双向编码输入,decoder 再在 encoder 输出的条件下自回归生成。
它天然适合翻译、摘要、改写等“输入序列 → 输出序列”的任务,T5 和 BART 是典型代表。
相对 decoder-only,它的优势是对输入建模更完整;代价是架构和推理路径更复杂。
6.4 PrefixLM 与 MoE:两个常见补充概念
PrefixLM
可以把它理解为一种混合掩码:前缀部分允许双向读取,后缀部分因果生成。它展示的是“掩码本身就是任务设计”。
MoE(Mixture of Experts)
MoE 不是 encoder/decoder 这种任务架构,而是一种扩展参数规模的方式。它通常发生在 FFN 上:token 先经过路由器,再只激活少数几个专家。
\[ \mathrm{MoE}(x)=\sum_{k \in \mathrm{TopK}(g(x))} g_k(x)\,E_k(x) \]
MoE 的收益是:总参数量可以很大,但每一步实际激活的参数只是一小部分。
它的代价也很明确:路由质量、负载均衡和分布式通信都可能变成新瓶颈。
6.5 小结
如果只从推理系统看,decoder-only 的一个关键事实是:
解码阶段的推理内存通常不是先被参数主导,而是先被 KV cache 主导。
也就是说,真正让 70B 模型在长上下文和高并发下难以服务的,往往不是“权重本身太大”,而是历史上下文的缓存成本。
这也是为什么很多现代 decoder 配方会逐渐收敛到类似组合:RoPE、RMSNorm、SwiGLU、GQA。它们不是偶然流行,而是在质量、训练稳定性和推理成本之间形成了很好的工程均衡。
7 长上下文是一个系统问题
本节先回答几个关键问题:
- 为什么长上下文会带来二次方级别的注意力成本?
- 为什么现代推理系统会把 prefill 和 decode 分成两个不同阶段?
- KV cache 为什么存在?它的内存开销如何增长?
- 当上下文窗口从 4K 拉到 128K 时,什么会先变贵,什么会先出问题?
- 为什么长上下文既是建模问题,也是系统问题?
长上下文最容易被误解成“把窗口调大一点”。
但对真实系统来说,它同时是一个建模问题和一个系统问题:模型要能理解更长的依赖关系,系统则要承担更高的计算、显存和带宽开销。
7.1 Prefill 与 decode:两个不同的物理阶段
一次自回归推理通常分成两个阶段:
Prefill
处理已有上下文。模型要对整段 prompt 做完整前向,计算量大,通常更偏向算力受限。
Decode
逐 token 生成新内容。每一步只新增一个位置,但系统需要反复读取历史缓存,因此更容易受显存和带宽约束。
这就是为什么很多现代推理系统会显式区分 prefill 和 decode。
它们的瓶颈不同,优化手段也不同。prefill 更关心 attention kernel、批处理和上下文长度;decode 更关心 KV cache、内存布局和带宽效率。
7.2 二次方注意力与线性增长的缓存
标准注意力在 prefill 阶段的核心成本来自 \(L \times L\) 的分数矩阵,因此序列长度翻倍,计算和显存压力都会明显放大。这也是长 prompt 常常拉高首 token 延迟(TTFT)的根源。
而 decode 阶段的关键成本则来自 KV cache。
为了避免每生成一个新 token 都把历史上下文全部重算,系统会缓存过去所有位置的 key/value。单层近似开销可写为:
\[ \text{KV bytes} = B \times L \times H_{kv} \times d_h \times 2 \times s \]
其中:
- \(B\):batch size
- \(L\):序列长度
- \(H_{kv}\):KV 头数
- \(d_h\):每个头的维度
- \(2\):K 和 V 各一份
- \(s\):每个数值的字节数
如果模型有 \(N\) 层,总开销还要再乘以层数 \(N\)。
这条公式非常重要,因为它解释了为什么 decode 阶段常常先被缓存而不是参数拖垮。
一个数量级示例会更直观。假设一个 decoder-only 模型有 80 层,\(H_{kv}=8\),\(d_h=128\),使用 FP16/BF16(\(s=2\) 字节),单请求 batch 为 \(B=1\),上下文长度 \(L=32768\)。那么单层 KV cache 大约是:
\[ 32768 \times 8 \times 128 \times 2 \times 2 \approx 128\text{ MB} \]
乘上 80 层后,整模型的 KV cache 约为 10 GB。
如果 batch size 变成 4,仅 KV cache 就会逼近 40 GB,还没有计算模型权重、激活、allocator 碎片和其他运行时开销。
这就是为什么长上下文一旦进入高并发服务,就会立刻从“模型能不能读”变成“系统还能不能装下、还能不能跑快”的问题。GQA、MQA、分页式 KV 管理和更激进的请求调度,都是围绕这个现实约束展开的。
7.3 为什么长上下文会同时拖慢延迟和吞吐
把窗口从 4K 提到 128K,不会只发生一件事,而是多层压力叠加:
- prefill 计算显著上涨;
- KV cache 线性增长;
- batch size 往往被显存限制压小;
- decode 阶段带宽压力上升;
- 位置编码外推、检索精度和长文一致性也可能退化。
因此,长上下文不只是“模型能不能读进去”的问题,还包括“系统能不能服务得动”。
7.4 常见缓解手段
工程上常见的缓解方式包括:
- KV cache:避免 decode 时重算历史;
- MQA / GQA:通过共享或分组共享 K/V,直接减小缓存;
- FlashAttention:优化实现,减少 HBM 访存;
- 更好的请求调度和内存管理:例如分页式 KV 管理;
- RAG / chunking / summaries:不把所有信息硬塞进上下文,而是把一部分问题转移到系统层。
也正因为如此,长上下文能力经常不应该被理解成“窗口越大越好”。
很多业务问题,用检索、分块、摘要和结构化状态来组织信息,会比暴力堆大窗口更可靠、更便宜。
一句话本章小结:长上下文不是单一模型能力,而是注意力复杂度、缓存管理、位置外推和系统调度共同作用后的结果。
8 AI 工程师排障手册
本节先回答几个关键问题:
- 如果模型输出突然变得混乱,首先应该查哪一层?
- 如果延迟突然上升,应该先看 prefill 还是 decode?
- 如果显存使用异常膨胀,为什么要先怀疑 KV cache?
- 哪些现象看起来像“模型能力问题”,其实是 tokenizer、模板或解码问题?
基础知识真正有价值的地方,不在于面试时能背出多少公式,而在于出了问题时知道先看哪里。下面这张表可以当成一个 AI 工程师的最小排障笔记。
| 症状 | 第一检查点 | 常见根因 | 下一步动作 |
|---|---|---|---|
| 输出乱码、角色错乱、工具参数边界错 | tokenizer / special tokens / chat template | 训练与推理使用了不同协议 | 对齐 tokenizer、special tokens 和模板 |
| 首 token 很慢 | prefill / prompt 长度 / attention kernel | 上下文过长,prefill 被注意力成本主导 | 缩短上下文、做 chunking、检查 kernel 与批处理 |
| 生成越往后越慢,显存不断上涨 | KV cache / context length / batch | decode 阶段缓存增长、带宽受限 | 检查上下文长度、KV 管理、GQA/MQA 是否启用 |
| 输出重复、模板化、进入循环 | decoding | greedy 或低温度自循环;重复惩罚失衡 | 调整 temperature、top-p、repetition penalty |
| JSON / tool call 不稳定 | stop sequence / constrained decoding / template | 边界条件不清,结构约束太弱 | 加 stop,必要时用约束解码 |
| 长文档问答前后矛盾 | retrieval / chunking / positional scheme | 关键证据没进上下文,或长度外推失败 | 先排检索与分块,再看位置表示与窗口策略 |
| 专业术语、代码标识符理解差 | tokenizer / training data | 过度碎片化或领域覆盖不足 | 检查碎片率、术语归一化、继续预训练 |
| 同一模型离线正常、线上变差 | serving pipeline | 预处理、模板、stop、解码设置不一致 | 对齐线上线下配置,做端到端回放 |
这里最重要的排障原则是:
不要一上来就怪模型权重。
先沿着这条链路检查:输入表示对不对?上下文组织对不对?推理系统的阶段瓶颈在哪里?解码策略有没有把一个正常分布变成异常行为?
如果你有这条链路作为心智模型,很多问题都会变得更可定位。
9 本章小结
本章沿着一条最小但完整的推理流水线,重新解释了大语言模型的工作方式。
- 文本先被 tokenizer 切成 token;
- token 被映射到 embedding 空间,并补上位置信息;
- 多层 Transformer block 通过注意力和 FFN 不断重写表示;
- 最后一个位置的 hidden state 被投影成 logits;
- softmax 得到概率分布;
- 解码策略把概率分布变成真实文本;
- 长上下文和推理系统则决定这条链路在现实里能否以可接受的成本运行。
如果只记住一句话,那就是:
LLM 不是“会说话的程序”,而是一条把符号映射到分布、再把分布映射到行为的工程链路。
9.1 问题小结
1. 什么是 token?为什么现代 LLM 更偏好子词分词?
token 是模型处理文本的基本离散符号。子词分词在词级 OOV 和字符级长序列之间取得了最实用的折中。
2. 为什么不能直接把 token ID 当成输入?
因为 ID 只是标签,不具有连续空间里的几何意义。embedding 负责把离散标签映射到可学习的向量空间。
3. 为什么 Transformer 需要位置信息?
因为注意力本身不自带顺序感。没有位置表示,模型很难稳定区分词序。
4. 模型在真正生成文本前输出的是什么?
先输出 hidden state,再经过线性投影得到 logits,softmax 后才得到概率分布。
5. 为什么语言模型生成的是概率分布,而不是直接输出词?
因为训练需要可微分目标,推理也需要保留运行时选择空间。logits 让模型分布和解码策略可以解耦。
6. 缩放点积注意力为什么要除以 \(\sqrt{d_k}\)?
为了控制点积分数的尺度,避免维度增大后 softmax 过早饱和。
7. 因果掩码为什么必要?
它阻止模型偷看未来 token,保证训练目标和自回归推理过程一致。
8. 为什么注意力是 \(O(L^2)\)?
因为每个位置都要与所有位置做交互,形成 \(L \times L\) 的分数矩阵。
9. logits 和 probabilities 有什么区别?
logits 是未归一化分数;probabilities 是经 softmax 归一化后的分布。
10. 温度、top-k 和 top-p 各改变什么?
温度改变分布尖锐程度;top-k 限制固定数量候选;top-p 限制累计概率候选集。它们共同决定随机性与稳定性的平衡。
11. 为什么长上下文会显著增加推理成本?
prefill 的注意力计算随长度急剧上升,decode 的 KV cache 也会线性增长并压迫显存与带宽。
12. prompt 到达后,推理流水线内部会发生什么?
文本先被分词,再被映射到 embedding;模型通过多层 Transformer 计算 hidden states;最后输出 logits,经解码后变成新 token,并循环执行。
13. 为什么系统要区分 prefill 和 decode?
因为两阶段瓶颈不同:prefill 更偏算力受限,decode 更偏缓存和带宽受限。
14. KV cache 为什么存在?
为了避免自回归生成时每一步都重算历史上下文的 key/value。它用显存换取解码效率。
15. 解码策略为什么是系统设计问题?
因为它直接影响输出质量、随机性、格式稳定性和推理成本,而不只是“文风”。
16. 输出混乱、延迟激增或显存爆炸时先看哪里?
先沿流水线排查:tokenizer 和模板、上下文长度与 prefill、KV cache 与 decode、解码参数与 stop 设计。
10 参考资料
- Vaswani et al. Attention Is All You Need. 2017.
- Sennrich, Haddow, Birch. Neural Machine Translation of Rare Words with Subword Units. 2016.
- Kudo, Richardson. SentencePiece: A simple and language independent subword tokenizer and detokenizer for Neural Text Processing. 2018.
- Mikolov et al. Efficient Estimation of Word Representations in Vector Space. 2013.
- Pennington, Socher, Manning. GloVe: Global Vectors for Word Representation. 2014.
- Press, Wolf. Using the Output Embedding to Improve Language Models. 2017.
- Devlin et al. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. 2019.
- Su et al. RoFormer: Enhanced Transformer with Rotary Position Embedding. 2021.
- Press et al. Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. 2022.
- Dao et al. FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness. 2022.
- Ainslie et al. GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints. 2023.
- Gu, Dao. Mamba: Linear-Time Sequence Modeling with Selective State Spaces. 2023.
- Holtzman et al. The Curious Case of Neural Text Degeneration. 2020.
- Ba, Kiros, Hinton. Layer Normalization. 2016.
- Zhang, Sennrich. Root Mean Square Layer Normalization. 2019.