#模块二:Tokenizer、Embedding、位置编码与上下文窗口

#13. BPE、WordPiece、SentencePiece 的主要区别是什么?

#标准答案

三者本质上都在做子词切分,但训练方式和工程假设不一样。BPE 可以理解成不断把高频相邻片段合并,所以更偏频次驱动;WordPiece 更像是在问“合并后能不能让语言模型更划算”,因此带一点概率视角;SentencePiece 的优势则是它不依赖人工先分词,直接在原始文本流上学习切分单元,所以对中文、多语言、带特殊符号的文本更友好。

面试里不要只说“它们都是 tokenizer 方法”,最好补一句:它们的差别不仅影响词表构建,还会影响 token 长度分布、训练成本以及不同语言场景下的适配性。这样答案会更像工程判断。


#深度解析

1. 为什么需要子词切分?从一个例子说起

假设词表只有 10000 个英文单词。遇到 "unhappiness" 怎么办?

  • 如果按"词"切分:"unhappiness" 不在词表里 → OOV(未登录词)
  • 如果按"字"切分:u-n-h-a-p-p-i-n-e-s-s → 10 个 token,语义完全丢失
  • 子词切分:un + happiness → 2 个 token,既覆盖了未登录词,又保留了语义

子词切分的核心思想:高频词保持完整,低频词拆成有意义的片段


2. BPE(Byte Pair Encoding)

训练过程:从字符级别开始,不断合并最高频的相邻片段。

具体例子

假设训练语料中有这些词和频次:

low: 5, lower: 2, lowest: 1, newer: 6, wider: 3

初始时每个词拆成字符 + </w>(词尾标记):

l o w </w>: 5
l o w e r </w>: 2
l o w e s t </w>: 1
n e w e r </w>: 6
w i d e r </w>: 3

第 1 轮:统计所有相邻字符对的出现次数:

  • e r 出现 2+6+3 = 11 次(最高频)
  • 合并 e rer

第 2 轮

  • w er 出现 2+6 = 8 次(注意 lower 中 wer 相邻)
  • 合并 w erwer

第 3 轮

  • low 出现 5+2+1 = 8 次
  • 合并 l o wlow

最终词表包含:low, wer, er, new, wide, est 等。

编码过程:对新词 "lowering":

lowering → low + er + ing

BPE 的特点

  • 贪婪合并:每次选最高频的 pair
  • 合并次数是超参数(决定词表大小)
  • 对英语效果很好,对中文需要预处理(因为中文没有天然字符分隔)

3. WordPiece

WordPiece 和 BPE 很像,但合并标准不同:

| | BPE | WordPiece | |--|-----|-----------| | 合并标准 | 频率最高 | 使语言模型损失下降最多 | | 合并方式 | 基于频次 | 基于概率 |

WordPiece 的合并规则

对于候选 pair (a, b),计算合并后的得分:

score = count(a, b) / (count(a) * count(b))

这个得分衡量的是"a 和 b 一起出现的频率"相对于"它们各自独立出现的频率"的比值。比值越大,说明 a 和 b 越应该合并。

例子

  • th 经常一起出现,count(th)count(t) * count(h) 的比值很大 → 合并
  • qx 很少一起出现,比值很小 → 不合并

WordPiece 的优势

  • 更"语义化"——不只是看频率,还看了组合的信息增益
  • Google 的 BERT 使用 WordPiece

4. SentencePiece

SentencePiece 最大的区别:不假设输入有分词边界

BPE 和 WordPiece 都需要先把文本按空格分成"词",然后再做子词切分。这对英语没问题,但对中文、日文(没有空格)、泰文(没有空格)就有问题了。

SentencePiece 的做法

直接把原始文本当成字符流,用 (特殊符号)标记空格:

原始文本:Hello world
SentencePiece 输入:Hello▁world

然后在这个字符流上跑 BPE 或 Unigram Language Model。

Unigram Language Model(SentencePiece 默认算法):

和 BPE 相反——从一个大词表开始,不断删除使损失增加最少的子词。

  1. 初始词表:包含所有可能的字符、常见词、高频子串
  2. 对训练语料中的每个词,用 Viterbi 算法找到最优切分
  3. 计算每个子词的概率
  4. 删除那些概率低、对整体损失影响小的子词
  5. 重复直到达到目标词表大小

SentencePiece 的优势

  • 语言无关:中文、日文、代码、数字混用都没问题
  • 可逆性:token 序列可以 100% 还原成原始文本(因为保留了空格信息)
  • T5、ALBERT 等模型使用 SentencePiece

5. 三种方法的核心对比

维度 BPE WordPiece SentencePiece
合并策略 频率最高 信息增益最大 反向删除(Unigram)
预处理 需先按空格分词 需先按空格分词 直接在原始文本上学习
语言适应性 英语好 英语好 任何语言(含中文/日文)
代表模型 GPT-2, RoBERTa BERT T5, ALBERT
空格处理 空格作为分隔符 空格作为分隔符 显式编码空格

6. 面试官常见深挖追问

  • "BPE 的词表大小怎么选?大了好还是小了好?"
    • 答:词表大(如 100K)→ token 数少,序列短,计算快,但词表占内存大,罕见词可能被切成单字。词表小(如 16K)→ token 数多,序列长,计算慢,但词表紧凑。常见选择:英文 32K-50K,中文 50K-100K,多语言 100K-250K。
  • "为什么中文 tokenizer 经常把词切成单字?"
    • 答:因为中文训练语料里单字频率极高(的、了、是),BPE/WordPiece 会优先合并高频 pair,而中文高频 pair 大多是单字组合。解决方法:1)增大词表;2)用预分词(如 jieba)做初始切分;3)用 SentencePiece 的 Unigram 算法,它更容易学到多字词。
  • "tokenizer 对模型效果影响大吗?"
    • 答:非常大。tokenizer 决定了:1)序列长度(影响 attention 计算量);2)词表大小(影响 embedding 层参数量);3)OOV 率(影响未登录词的处理);4)不同语言的切分质量(影响多语言模型效果)。有研究表明,换 tokenizer 可以让同规模模型效果波动几个百分点。

#14. 什么是 tokenization,为什么不能简单按“词”来切?

#标准答案

tokenization 就是把原始文本切成模型能处理的离散单元,也就是 token。之所以不能简单按“词”切,是因为真实世界文本远比“单词列表”复杂:有新词、错拼、缩写、代码片段、表情、数字串、多语言混写,还有中文这种天然没有空格分词的场景。

如果直接按词建词表,会遇到两个大问题:第一,词表会非常大,参数和训练成本都高;第二,大量未登录词会变成 OOV,模型泛化能力差。子词切分的价值,就是在”粒度不要太粗”和”不要切得太碎”之间找平衡。


#深度解析

1. 为什么”按词切”不行?数字说话

切分方式 词表大小 OOV 率 平均序列长度 问题
按词 100万+ 词表爆炸、罕见词 embedding 学不好
按字符 256 0% 很长 序列太长、失去词级别语义
子词 (BPE, 32K) 32,000 <1% 中等 平衡:常见词完整保留,罕见词拆成子词

假设英文语料:

2. 子词切分如何处理未见词?

已知词(在词表中):
- “apple” → [apple]
- “playing” → [play, ing]

未见词(OOV):
- “tokenization” → [token, ization](如果训练时见过这两个子词)
- “GPT-4o” → [GPT, -, 4, o](混合切分)

子词切分的核心优势:任何文本都能被切分成已知子词的组合,不会出现完全无法处理的 OOV。

3. 中文为什么更需要子词切分?

中文没有天然空格,”按词切”需要先分词(如 jieba),但分词本身就有歧义:

“南京市长江大桥”
- 分词 A:[南京市] [长江大桥]
- 分词 B:[南京] [市长] [江大桥]

子词方案(如 SentencePiece)不需要预分词,直接在原始字符流上学习:

  • 高频组合 → 合并成一个 token(如 “人工智能”)
  • 低频组合 → 保持单字(如 “饕”)

避免了分词错误传播给模型的问题。

4. token 长度对模型能力的影响

现象 原因 影响
英文单词通常 1 个 token 英文子词和词边界接近 英文效率高
中文通常 1-2 个 token/字 中文字符数多,但常用组合会合并 中文略低效
代码空格/缩进大量消耗 token 每个缩进 level 可能是一个 token 代码上下文利用率低
数字常被拆成单个数字 “2024” → “2”,”0”,”2”,”4” 数字理解碎片化

5. 面试官常见深挖追问

  • ”为什么 GPT-4 的 tokenizer 对中文不友好?怎么解决?”
    • 答:GPT-4 的 cl100k_base 词表主要基于英文语料训练,中文覆盖不足。一个中文字符通常被切成 1.5-2 个 token,导致:1)同样内容中文成本高 50-100%;2)上下文窗口”缩水”。解决:1)换中文优化的 tokenizer(如 ChatGLM 的);2)扩展词表并继续预训练;3)用更短的表达方式。
  • ”BPE 和 SentencePiece 的本质区别是什么?”
    • 答:BPE 需要预先把文本按空格分词(对中文不友好),然后合并高频子词对。SentencePiece 直接在原始字符流上操作,不需要预分词,对中文、日文等多语言更友好。BPE 是”词级别合并”,SentencePiece 是”字符级别合并”。
  • ”tokenizer 训练时,词表大小怎么定?”
    • 答:常见范围 32K-100K。太小 → 序列过长、语义碎片化;太大 → 罕见词 embedding 学不充分、输出层 softmax 计算量大。经验:通用多语言模型 50K-100K,英文专用 32K-50K。最终通过验证集 PPL 来调优。

#15. 什么是 contextual embedding,它和 Word2Vec 这种静态 embedding 有什么区别?

#标准答案

contextual embedding 的关键在“同词不同义时,向量也会变”。比如 bank 在“river bank”和“bank loan”里含义不同,现代语言模型会根据上下文生成不同的表示;而 Word2Vec 这类静态 embedding 不管上下文怎么变,一个词只有一套固定向量,所以很难表达歧义、指代和细粒度语境差异。

这也是为什么 Transformer 时代的表示能力远强于传统词向量:模型不是把词查表后就结束了,而是在上下文里反复更新表示,最后得到的是“这个词在这句话里是什么意思”,而不是“这个词通常是什么意思”。


#深度解析

1. 为什么需要 Contextual Embedding?

语言中大量存在一词多义。同一个词在不同上下文里意思完全不同:

"bank" 在 "river bank" 里 = 河岸
"bank" 在 "bank loan" 里 = 银行
"bank" 在 "data bank" 里 = 数据库

Word2Vec 这类静态 embedding 的做法是:一个词 = 一个向量。不管上下文怎么变,"bank" 永远对应同一个向量。模型无法区分它在不同句子里的含义。

Contextual Embedding 的做法是:同一个词在不同上下文中,向量也不同。模型根据周围词来动态调整这个词的表示。


2. Contextual Embedding 是怎么工作的?

以 Transformer 为例。输入 "river bank" 时:

Step 1:初始表示

每个词先查 embedding 表得到初始向量:

"river" → [0.2, -0.3, 0.5, ...]
"bank"  → [0.1, 0.4, -0.2, ...]   # 注意:这是 "bank" 的"平均"表示

Step 2:通过 Attention 更新

Attention 机制让 "bank" 看到 "river",发现两者相关(因为 "river bank" 是一个常见搭配),于是 "bank" 的表示被更新为:

"bank"(更新后) ≈ 0.7 * 原始 "bank" + 0.3 * "river" 的信息
                = [0.15, 0.25, -0.05, ...]   # 更偏向"河岸"语义

如果输入是 "bank loan":

"bank"(更新后) ≈ 0.7 * 原始 "bank" + 0.3 * "loan" 的信息
                = [0.12, 0.35, -0.15, ...]   # 更偏向"银行"语义

关键洞察:Contextual Embedding 不是"换一个查表",而是通过 attention 把上下文信息"混合"进当前词的表示中。同一个词的最终向量会因为邻居不同而不同。


3. Word2Vec 的局限

Word2Vec(包括 Skip-gram 和 CBOW)的核心假设:词的语义独立于上下文

训练方式:

给定 "cat sat on the mat",Word2Vec 会生成训练样本:
(中心词="sat", 上下文=["cat", "on"]) → 学习 sat 的向量
(中心词="sat", 上下文=["on", "the"]) → 同一个 sat 的向量

无论 "sat" 前面是 "cat" 还是 "dog",它学到的向量都一样。

为什么这不够?

因为语言中大量存在:

  • 一词多义:"bank" = 河岸/银行
  • 指代消解:"它" 在不同句子里指代不同对象
  • 语义组合:"hot dog" ≠ "hot" + "dog"
  • 语法角色:"dog" 在 "dog bites man" 和 "man bites dog" 中角色不同

Contextual Embedding 通过 attention 让模型学到:词的意思取决于它跟谁在一起


4. 从 Word2Vec 到 BERT:表示能力的进化

模型 表示方式 上下文感知 典型应用
Word2Vec 静态向量 词相似度、基础分类
ELMo 双向 LSTM 层输出 有(LSTM) 语义角色标注
BERT Transformer 输出 有(双向 Attention) 分类、匹配、抽取
GPT Transformer 输出 有(单向 Attention) 生成、对话

ELMo 的洞察:不用 Transformer 的最后一层,而是用所有层的加权平均。因为不同层学到不同信息——底层学词法,中层学句法,高层学语义。

BERT 的突破:用双向 attention,让每个词同时看到左右上下文,表示能力远超单向模型。


5. 面试官常见深挖追问

  • "Contextual Embedding 的输出应该怎么用?取最后一层还是取多层平均?"
    • 答:取决于任务。简单分类 → 取 [CLS] token 的最后一层输出即可。需要细粒度语义 → 取多层加权平均(如 ELMo 的做法),因为不同层编码不同信息。有研究表明,取最后 4 层平均通常比单层更好。
  • "Word2Vec 和 Contextual Embedding 在计算成本上差多少?"
    • 答:Word2Vec 训练一次后,查表即可,O(1)。Contextual Embedding 需要跑一次完整的前向传播,O(n² * d)。但 Contextual Embedding 的表示质量远超 Word2Vec,所以在现代 NLP 中几乎完全替代了静态 embedding。
  • "如果只有 Word2Vec,能做指代消解吗?"
    • 答:很难。因为 "he" 的 Word2Vec 向量永远是同一个,模型无法根据上下文判断 "he" 指的是谁。Contextual Embedding 通过 attention 让 "he" 的向量自动融入所指代对象的信息,所以能做指代消解。

#16. 位置编码分为哪几类?绝对位置编码和相对位置编码的差别是什么?

#标准答案

位置编码常见可以分成几类:最早的是绝对位置编码,直接给每个位置一个固定编号或位置向量;后面发展出相对位置编码,更强调两个 token 之间相距多远;现代 LLM 常见的 RoPEALiBi 也都可以理解为在更适合长上下文和自回归的场景里编码位置信息。

绝对位置编码回答的是”我在第几个位置”,相对位置编码回答的是”我和你相隔多远”。后者在长文本里往往更自然,因为很多语言关系并不关心绝对编号,而更关心相对距离,所以在长度扩展场景下通常更稳。


#深度解析

1. 位置编码的完整分类树

位置编码
├─ 绝对位置编码
│   ├─ 可学习(Learned):BERT、GPT-1
│   └─ 固定函数(Sinusoidal):原始 Transformer
├─ 相对位置编码
│   ├─ 显式偏置:Transformer-XL、T5
│   ├─ 旋转嵌入:RoPE(LLaMA、GPT-4)
│   └─ 距离衰减:ALiBi(BLOOM、MPT)
└─ 无显式位置编码
    └─ 结构化稀疏:Mamba、RWKV(靠扫描/循环隐式编码位置)

2. 绝对 vs 相对:数学本质对比

维度 绝对位置编码 相对位置编码
编码对象 每个位置的”绝对坐标” 两个位置之间的”相对距离”
公式形式 x_i = embedding_i + pos_i attn_score = f(q_i, k_j, i-j)
长度外推 差(未见过的位置没有编码) 好(只依赖距离,不依赖绝对位置)
代表方法 Sinusoidal, Learned RoPE, ALiBi, T5 bias

3. 四种主流方法的详细对比

方法 核心机制 优点 缺点 代表模型
Sinusoidal PE(pos, 2i) = sin(pos/10000^(2i/d)) 连续、有确定性的位置关系 不直接编码相对位置 原始 Transformer
Learned 每个位置学一个向量 灵活、可训练 长度外推差、占参数量 BERT, GPT-1
RoPE Q/K 向量旋转,角度∝位置 内积天然含相对位置、外推友好 高频维度外推仍衰减 LLaMA, Qwen
ALiBi Attention score 减去距离偏置 训练快、长度外推极强 短文本可能略弱于 RoPE BLOOM, MPT

4. 为什么相对位置编码更适合长上下文?

核心直觉:语言关系通常是”局部的”和”相对的”。

  • “我爱你”中,”我”和”爱”的关系取决于它们相距 1 个位置,而不在于它们在文档的第 100 个还是第 10000 个位置。
  • 绝对编码需要为每个位置学一个独立向量,pos=10000 的向量在训练时可能从未见过(训练最长 4096)。
  • 相对编码只关心 distance = i-jdistance=1 在短文本和长文本中是一样的。

5. RoPE vs ALiBi 的工程选择

场景 推荐 原因
通用 LLM(追求最佳效果) RoPE 表达力更强,配合 NTK/YaRN 外推效果好
需要训练时极快收敛 ALiBi 不需要位置编码层,直接 distance bias
超长上下文(1M+) ALiBi 或 RoPE+YaRN ALiBi 外推”免费”;RoPE 需要额外微调
代码/数学(精确位置敏感) RoPE 旋转编码对精确位置关系建模更细

6. 面试官常见深挖追问

  • ”RoPE 和 ALiBi 的本质区别是什么?”
    • 答:RoPE 是”乘法型”相对编码——把位置信息乘到 Q/K 向量上(通过旋转矩阵),attention score 通过内积自然获得相对位置信息。ALiBi 是”加法型”相对编码——直接在 attention score 上减去一个与距离成正比的偏置项。RoPE 更细粒度(每维不同频率),ALiBi 更简单粗暴但外推更稳。
  • ”为什么原始 Transformer 用 Sinusoidal,而现代 LLM 不用了?”
    • 答:Sinusoidal 编码虽然连续、有确定性,但它编码的是绝对位置。训练时最长 512,测试时 1024 就外推失败了(因为 pos=1000 的编码训练时没见过)。现代 LLM 需要支持 128K+ 上下文,必须有良好的长度外推能力,所以 RoPE/ALiBi 取代了 Sinusoidal。
  • ”如果不用任何位置编码,模型能学会位置信息吗?”
    • 答:纯自注意力是位置无关的(permutation equivariant)——打乱 token 顺序,attention 输出只是对应位置互换。没有位置编码,模型完全不知道”第一个 token”和”第二个 token”的区别。但一些新方法(如 Mamba、RWKV)通过循环/扫描结构隐式编码位置,不依赖显式位置编码。

#17. RoPE 的核心思想是什么?

#标准答案

RoPE 的核心思想,不是像早期方法那样给 token 向量额外加一个位置向量,而是直接对 Q/K 的不同维度做成对旋转,让 attention 分数在做内积时天然带上相对位置信息。换句话说,它把“位置”嵌进了相关性计算本身,而不是只在输入端做一次标记。

这样做的好处是:模型在判断两个 token 是否相关时,不仅知道它们内容像不像,还能顺带感知它们相隔多远、方向如何。这也是为什么很多人会说 RoPE 更像是在改造 attention,而不只是改造 embedding。


#深度解析

1. 为什么 attention 需要位置信息?

Self-Attention 有一个根本性质:位置无关性(permutation equivariant)。如果你把输入 token 的顺序打乱,attention 的输出只是对应位置互换,数值本身完全不变。

举例:句子 "猫 喜欢 鱼" 和 "鱼 喜欢 猫",如果不加位置信息,模型认为这两个句子完全一样——因为 attention 只看 token 内容,不看顺序。

所以位置编码的使命是:在不破坏 attention 核心机制的前提下,让模型感知顺序

早期做法(如原始 Transformer)是给每个位置加一个固定向量 p(m)x + p(m)。但这样位置信息和内容信息是"相加"关系,可能互相干扰。

RoPE 的做法更巧妙:不添加额外向量,而是直接旋转 Q/K 向量本身,让位置信息通过"旋转角度"注入到内积计算中。


2. 旋转的数学形式:用一个具体例子走一遍

RoPE 对 Q/K 向量的每一对维度做二维旋转。对于位置 m 的向量,第 (2i, 2i+1) 维度的旋转如下:

[q_{2i}  ]   [cos(mθ_i)  -sin(mθ_i)] [q_{2i}  ]
[q_{2i+1}] = [sin(mθ_i)   cos(mθ_i)] [q_{2i+1}]

其中 θ_i = base^(-2i/d)base 通常取 10000。

具体数值例子:假设某个 token 在维度对 (0, 1) 上的原始值是 q = [1.0, 0.0],位置 m = 3base = 10000d = 4(所以 i = 0)。

先算 θ_0

θ_0 = 10000^(-2*0/4) = 10000^0 = 1.0

再算旋转矩阵:

cos(3 * 1.0) = cos(3) ≈ -0.990
sin(3 * 1.0) = sin(3) ≈  0.141

[q_0']   [-0.990  -0.141] [1.0]   [-0.990 * 1.0 + (-0.141) * 0.0]   [-0.990]
[q_1'] = [ 0.141  -0.990] [0.0] = [ 0.141 * 1.0 + (-0.990) * 0.0] = [ 0.141]

位置 3 的 token,其 Q 向量在第 (0,1) 维上从 [1.0, 0.0] 旋转成了 [-0.990, 0.141]

关键观察

  • 同一个 token 在不同位置上,旋转角度不同
  • 位置 m 越大,旋转角度 mθ_i 越大
  • 向量长度不变(旋转矩阵是正交矩阵),所以位置编码不改变 token 表示的"强度"

3. 为什么二维一组旋转?不同频率的作用

RoPE 不是对整个向量做一次高维旋转,而是按维度对分别旋转。每对维度有自己的旋转速度 θ_i

维度对索引 i θ_i = 10000^(-2i/d) 旋转速度 编码范围
i = 0 10000^0 = 1.0 最快 短程精细位置
i = d/4 10000^(-0.5) ≈ 0.01 中等 中程位置
i = d/2-1 10000^(-(d-2)/d) ≈ 0.0001 最慢 长程粗粒度位置

直观理解

  • 高频对(i 小,θ_i 大):转得快,相邻位置的区分度高 → 适合编码"这个词紧挨着那个词"
  • 低频对(i 大,θ_i 小):转得慢,很多位置共享相近角度 → 适合编码"这两个词相距很远"

这种"多频率"设计让 RoPE 同时兼顾短程精度和长程覆盖。


4. 为什么旋转后的内积只跟相对位置有关?

这是 RoPE 最核心的数学性质。设位置 m 的 query 经过旋转后为 R(m)q,位置 n 的 key 经过旋转后为 R(n)k,则:

<R(m)q, R(n)k> = <q, R(n-m)k>

也就是说,attention 分数只取决于 m-n(相对距离),跟绝对位置无关。

用具体数值验证

假设 q = [1, 0]k = [1, 0]θ = 1.0

  • 位置 m=2, n=0(相对距离 = 2):
  R(2)q = [cos(2), sin(2)] ≈ [-0.416, 0.909]
  R(0)k = [cos(0), sin(0)] = [1, 0]
  内积 = -0.416 * 1 + 0.909 * 0 = -0.416
  • 位置 m=5, n=3(相对距离也是 2):
  R(5)q = [cos(5), sin(5)] ≈ [0.284, -0.959]
  R(3)k = [cos(3), sin(3)] ≈ [-0.990, 0.141]
  内积 = 0.284 * (-0.990) + (-0.959) * 0.141 ≈ -0.281 + (-0.135) ≈ -0.416

(数值近似是因为 cos(2) ≈ cos(5-3),严格相等需要更精确的计算,但趋势一致)

这个性质为什么重要?因为语言关系本质上大多是相对的

  • "A 修饰 B" 关系不依赖于 A 和 B 在句子的第几个位置
  • 只依赖于"A 在 B 前面几个位置"

RoPE 把这个直觉变成了数学保证。


5. Base = 10000 的含义

base = 10000 决定了最慢维度对(i = d/2-1)的旋转周期:

周期 T = 2π / θ_{max} ≈ 2π * base^((d-2)/d) ≈ 2π * 10000 ≈ 62832

对于 4K、8K 的上下文,一个周期还没转完,所以位置编码是"单调"的——每个位置的角度都不一样。

但对于 128K 上下文,位置角度可能超过训练时见过的范围,这就是外推困难的来源:模型在训练时只见过 [0, 4Kθ_i] 范围内的角度,推理时突然要处理 [0, 128Kθ_i],注意力分数的数值行为完全陌生。


6. RoPE 和绝对位置编码的本质区别

维度 绝对位置编码 RoPE
注入方式 x + p(m)(相加) R(m)x(旋转)
位置信息 显式附加在输入端 嵌入到 attention 内积中
相对位置 需要额外设计 天然满足 <R(m)q, R(n)k> = <q, R(n-m)k>
外推性 较差,位置向量从未见过 较好,但超长仍有问题

7. 面试官常见深挖追问

  • "RoPE 和绝对位置编码在数学上最本质的区别是什么?"
    • 答:绝对位置编码是 f(x) + p(m)(内容+位置相加),RoPE 是 f(x) * R(m)(内容被位置旋转)。相加时位置和内容可能互相干扰;旋转时内积 <R(m)q, R(n)k> = <q, R(n-m)k>,只跟相对距离有关,这是质的区别。
  • "为什么 RoPE 按维度对旋转,而不是对整个向量做高维旋转?"
    • 答:对整个向量做单一旋转,所有维度用同一个频率,无法同时兼顾长短程。按对旋转让每个维度对承载不同频率的位置信号,短程精细变化由高频对编码,长程粗粒度变化由低频对编码。
  • "base 从 10000 改成 500000(如 CodeLlama 那样),会带来什么变化?"
    • 答:base 变大意味着 θ_i 整体变小,所有维度对旋转得更慢。好处是长程外推时不会那么快重叠,长距离区分更稳;代价是短程位置分辨率变粗,近距离 token 的区分能力下降。

#18. context window 指什么,它为什么重要?

#标准答案

context window 就是模型一次前向计算里能同时接收和处理的 token 上限。你可以把它理解成模型当前“工作记忆”的大小:窗口越大,它一次能看到的对话历史、文档证据、代码上下文和工具返回结果就越多。

它之所以重要,不只是因为”能塞更多字”,而是因为很多能力都依赖足够上下文,比如长文问答、RAG、多轮 Agent、复杂代码补全。如果窗口太小,模型不是不会做,而是压根看不全问题。


#深度解析

1. Context Window 的本质是”一次可见信息量”

Context window 不是存储容量,而是前向传播时的计算可见范围。模型在生成第 \(i\) 个 token 时,只能 attend 到 \([0, i-1]\) 范围内的 token。这决定了:

  • 信息完整性:RAG 召回 10 篇文档,但窗口只有 4K,可能只够放 2 篇
  • 连贯性:多轮对话第 20 轮时,前面 10 轮可能被挤出窗口,模型”忘记”用户偏好
  • 推理深度:复杂代码补全需要看到整个文件 + 依赖库定义,窗口不够只能局部猜测

2. 窗口大小的工程制约链

目标窗口 ↑
    → Attention 计算量 O(n²) ↑
    → KV Cache 显存占用 2*n*h*l*d ↑
    → 预填充时间 ↑ (TTFT 恶化)
    → 需要更长序列的预训练/微调成本 ↑
窗口大小 典型场景 KV Cache (70B, BF16) 预填充 4K 耗时
4K 短对话、单轮 QA ~2 GB ~0.5s
32K 长文档摘要、代码库 ~16 GB ~3s
128K 整本书、多轮 Agent ~64 GB ~15s
1M+ 视频序列、长期记忆 ~512 GB 不可行(需稀疏/循环架构)

3. “长窗口”不等于”会用长窗口”

很多模型声称支持 128K,但实际能力分三层:

层级 能力 测试方法
机械容纳 能接收 128K token 不报错 直接喂长文本
信息提取 能从 128K 中找回特定信息(Needle in Haystack) 在无关文本中插入关键句,提问
长程推理 能整合分散在 128K 中的多段信息做推理 多跳问答,证据间隔 >50K token

目前多数开源模型做到第二层,第三层仍是挑战。”Lost in the Middle”现象说明即使窗口够大,模型也可能忽略中间内容。

4. 面试官常见深挖追问

  • ”如果窗口从 4K 扩展到 128K,训练成本增加多少?”
    • 答:预训练阶段,若要保持相同 token 数,计算量与序列长度呈超线性增长(O(n²) attention + 更大的激活显存)。实际做法通常是:先在短序列(4K)预训练,再用长序列(32K/128K)做继续预训练(continue pretraining),成本远低于从头训练。继续预训练通常只需要原预训练 5-10% 的 token。
  • ”KV Cache 是长窗口的主要瓶颈吗?”
    • 答:是主要瓶颈之一。Attention 计算本身可通过 FlashAttention、稀疏 attention 缓解,但 KV Cache 的显存占用与序列长度线性相关,且需要在每个生成步骤中从 HBM 读取。解决方案包括:MQA/GQA 减少 KV 头数、KV Cache 量化(INT8/INT4)、分页管理(vLLM PagedAttention)、以及滑动窗口/循环机制(只保留最近 W 个 token 的 KV)。

#19. 为什么 RoPE 会成为主流?它相比绝对位置编码到底好在哪里?

#标准答案

RoPE 会成为主流,核心原因是它同时满足了三件事:第一,和 Decoder-only 自回归架构兼容得很好;第二,能把相对位置信息自然融进 attention 打分;第三,在长度扩展时通常比简单绝对位置编码更稳。

从工程角度看,它实现统一、生态成熟、很多开源基座都默认采用,所以后来逐渐形成了事实标准。严格说它也不是没有缺点,比如超长外推仍会出问题,但在“效果、实现复杂度、生态接受度”三者之间,它处在很舒服的位置。


#深度解析

1. 绝对位置编码是怎么工作的?

最早的位置编码是给每个位置学一个固定向量。比如词表大小 50000,最大长度 512,就额外学一个 (512, d_model) 的矩阵 P

位置 0 的编码 → P[0] = [0.1, -0.3, 0.5, ...]
位置 1 的编码 → P[1] = [0.2,  0.1, -0.2, ...]
位置 2 的编码 → P[2] = [-0.1, 0.4, 0.3, ...]
...

输入表示 = Embedding(token) + P[position]

原始 Transformer 用的是正弦/余弦函数来生成位置向量,不需要学习参数:

PE(pos, 2i)   = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

2. 绝对位置编码的三个核心问题

问题一:位置和内容互相干扰

绝对位置编码是 x + p(m)。相加意味着位置向量和内容向量在同一个空间里竞争。如果位置向量很大,它会"淹没"内容信息;如果很小,位置信号又不够强。模型需要额外学习如何把这两者区分开。

问题二:不擅长表达相对位置

绝对位置编码回答的是"我在第几个位置",但语言关系更多是相对的:

  • "A 修饰 B" 不依赖于 A 在第 3 位、B 在第 5 位
  • 只依赖于"A 在 B 前面 2 个位置"

绝对编码要让模型从 P[3]P[5] 推断出"相距 2",这需要额外学习。RoPE 直接让内积 <R(m)q, R(n)k> 只跟 m-n 有关,不需要额外学习。

问题三:外推性差

训练时只见过位置 0~4095,绝对位置编码的 P[4096] 从未见过。推理时要处理位置 10000,P[10000] 完全超出训练分布,模型不知道该怎么处理。

RoPE 虽然也面临外推问题,但因为它的角度是连续的旋转,相邻位置的角度变化是平滑的,比绝对编码的"跳变"更容易外推(通过 PI、NTK 等方法)。


3. RoPE 的优势:从机制上理解

维度 绝对位置编码 RoPE
注入方式 x + p(m)(相加) R(m)x(旋转)
位置与内容的关系 同一空间竞争 旋转不改变向量长度,互不干扰
相对位置 需要模型额外学习 内积天然只依赖 m-n
与 attention 的耦合 仅在输入端注入 嵌入到 attention 计算本身
外推性 差(从未见过的位置向量) 较好(连续旋转可插值/微调)

关键洞察:RoPE 不是"多了一个组件",而是重新设计了 attention 的数学形式。它让位置信息不是从外部"附加"进去,而是从内部"生长"出来。


4. 为什么工程上 RoPE 成为事实标准?

生态正反馈

  • LLaMA(Meta)用 RoPE → 大量下游工作基于 LLaMA → 大家默认兼容 RoPE
  • GPT-4 也用了 RoPE(根据公开分析)→ 进一步确认行业选择
  • 开源工具链(HuggingFace、vLLM、llama.cpp)都对 RoPE 有最成熟的优化

实现简洁

  • 不需要额外的位置嵌入参数(省内存)
  • 实现就是几行旋转矩阵代码
  • 和 FlashAttention 等优化兼容良好

可扩展性

  • 通过调整 base(如 10000 → 500000)可以适应不同长度
  • PI、NTK-aware、YaRN 等扩展方法都建立在 RoPE 基础上

5. RoPE 真的完美吗?

不是。它的问题是:

  • 超长外推仍有挑战:128K 以上位置的角度可能超出训练分布
  • 近距离分辨率有限:base 太大时短程区分变粗
  • 没有显式的周期性设计:不像正弦编码有明确的周期概念

但综合来看,它在"效果、实现复杂度、生态接受度"三者之间确实处于很舒服的位置。


6. 面试官常见深挖追问

  • "ALiBi 和 RoPE 有什么区别?"
    • 答:RoPE 通过旋转注入位置信息;ALiBi 直接在 attention score 上加一个基于距离的偏置项(距离越远,分数越低)。ALiBi 实现更简单,但位置信息是"硬性"的(不能学习),而 RoPE 的旋转角度可以通过训练自适应调整。
  • "如果没有 RoPE,模型能学会位置信息吗?"
    • 答:纯 attention 是位置无关的(打乱 token 顺序输出只是位置互换)。但一些替代方案如 Mamba、RWKV 通过循环/扫描结构隐式编码位置,不需要显式位置编码。所以"没有 RoPE"不等于"没有位置信息",只是机制不同。
  • "为什么 RoPE 适合 Decoder-only,不适合 Encoder-only 吗?"
    • 答:RoPE 主要配合 causal mask 使用(每个位置只看前面)。Encoder-only 用双向 attention 时,RoPE 也能工作,但效果不一定比绝对编码更好。RoPE 的优势在长序列自回归生成中更明显。

#20. 长度外推为什么难?RoPE 在长上下文下会遇到什么问题?

#标准答案

长度外推难,本质上是模型在训练时只见过某个长度分布,比如 4K8K,一旦推理时突然拉到 32K128K,模型并不是只多看一点内容,而是整个位置分布、依赖距离、注意力使用方式都超出了训练经验。

RoPE 来说,问题会表现为高频旋转外推失真、远距离 token 关系建模变差、注意力分数数值行为变化,以及整体”看到了但用不好”。所以 RoPE 是长上下文的重要基础,但绝不是只要用了 RoPE,长度外推就自动解决。


#深度解析

1. 外推困难的本质:训练分布与推理分布的失配

模型训练时见过的位置范围是 [0, L_train],比如 4K。RoPE 的旋转角度 mθ_i 随位置 m 线性增长。训练时,模型学到了”角度在 [0, 4K×θ_i] 范围内的位置关系”。

推理时拉到 128K,位置 m 可能达到 32×L_train。对于高频维度(大 iθ_i 很小),mθ_i 可能已经超过训练时见过的最大角度几十倍。模型从未见过这样的角度分布,注意力分数的数值行为完全陌生。

更本质地说:Transformer 不是”看位置编码的公式来理解长度”,而是”看训练数据中的位置模式来学习长度规律”。训练数据里没有 128K 长度的样本,模型就没有学到 128K 长度的注意力使用方式。

2. 三类长上下文扩展方法的本质

方法 核心思想 数学操作 是否需要训练
位置插值 (PI) 把长位置压缩到训练范围内 m' = m × L_train / L_target 少量微调
NTK-aware 高频少压缩、低频多压缩 修改 base 实现频率差异化缩放 通常无需训练
YaRN NTK-by-parts + attention temperature 区分高低频维度,对attention分数做温度缩放 少量微调

位置插值最直观:原本 0-128K 的位置,都除以 32 缩回 0-4K。但所有位置都均匀压缩,近距离区分变模糊。

NTK-aware 更聪明:它通过增大 base,让 θ_i 整体变小,相当于”把钟表的表针调慢”,这样 128K 位置的角度范围不会超出训练见过的太多。而且高频维度(本身 θ_i 就小)受影响更大,低频维度受影响小,实现了差异化处理。

3. 为什么”支持 128K”不代表”能用好 128K”

即使通过 PI/NTK 把窗口扩到 128K,模型仍面临几个挑战:

  • 注意力稀释:128K 个 token 竞争注意力权重,每个 token 平均只能分到约 1/128K 的注意力,关键信息容易被淹没。
  • Lost in the Middle:模型天然更关注开头和结尾,中间信息利用率低。
  • 长程依赖学习不足:预训练数据中长序列的依赖模式本来就少,模型没有充分学习”跳过 50K token 去找答案”的能力。

所以扩窗口是必要不充分条件——窗口大了只是”装得下”,不代表”找得到、用得好”。

4. 面试官常见深挖追问

  • ”位置插值和 NTK-aware 在直觉上有什么区别?”
    • 答:位置插值是”把地图压扁”——所有位置等比例压缩,简单但粗暴;NTK-aware 是”把钟调慢”——让同样的位置区间对应更小的角度变化,且不同频率维度调的程度不同,更精细。
  • ”为什么有些模型扩窗口后继续训练 1B token 就够了,有些需要几十 B?”
    • 答:取决于原模型基础能力、扩窗倍数和数据质量。如果原模型已经在 4K 上学到了很好的短程模式,扩到 128K 只需要补”长程模式”;但如果模型本身基础不好,或者扩窗倍数太大(如 4K→1M),就需要更多数据来学习新的位置分布和长程依赖。
  • ”YaRN 的 attention temperature scaling 是为了解决什么问题?”
    • 答:扩窗口后,attention 分数的数值范围会变(因为更多 token 参与 softmax 竞争),temperature scaling 通过调整 softmax 前的分数尺度,让 attention 分布保持与训练时相似的”尖锐度”,避免因为窗口变大导致 attention 过于平坦。

#21. 如果一个模型窗口从 8K 扩到 128K,会带来哪些新的工程和建模问题?

#标准答案

如果窗口从 8K 拉到 128K,首先爆炸的是工程成本。attention 计算量会明显上升,prefill 时间更长,KV cache 显存占用显著增加,服务系统的带宽和调度压力也会更重。很多团队以为只是“模型能吃更长输入”,但实际上是整条推理链都被重新定价。

建模层面也会出现新问题,比如长距离信息被稀释、关键证据掉在中间不被利用、训练时没见过这么长的样本导致分布失配、位置编码外推失效等。所以长窗口不是一个简单参数,而是一整套建模和系统问题。


#深度解析

1. 工程成本的量化

从 8K 扩到 128K(16 倍),以 LLaMA-2 7B 为例:

指标 8K 128K 增长倍数
Attention FLOPs ~4 GFLOPs/层 ~1024 GFLOPs/层 256x
KV cache (batch=1) ~4 GB ~64 GB 16x
Prefill 时间 (A100) ~0.5s ~8s 16x
Decode 时间/token ~10ms ~15ms 1.5x

KV cache 从 4GB 涨到 64GB,单张 A100 80GB 已经快满了。如果要支持 batch=4,就需要 256GB,远超单卡容量。

2. 建模层面的具体问题

  • 注意力稀释:128K 个 token 竞争注意力,每个 token 平均只能分到 1/128K 的权重,关键信息容易被淹没。
  • 训练数据不足:预训练语料中长文本(>32K)占比很少,模型没有充分学习长程依赖模式。
  • 位置编码外推:RoPE 在 128K 上的角度可能超出训练分布,需要 PI/NTK/YaRN 等扩展方法。
  • 优化器状态:训练时 Adam 的动量统计量也随序列长度增加,显存压力更大。

3. 从 8K 到 128K 的典型路径

实际工程中不会一步跳到 128K,而是渐进扩展:

8K (预训练) → 32K (继续预训练 100B token) → 128K (继续预训练 50B token)

每一步都需要:

  1. 调整位置编码(如 NTK-aware scaling)
  2. 准备对应长度的训练数据
  3. 调整学习率(通常比预训练小一个数量级)
  4. 评估外推效果

4. 面试官常见深挖追问

  • ”如果一个模型训练时最长只见过 4K,直接推理 128K 会怎样?”
    • 答:大概率效果很差。模型不仅在位置编码上外推失败,更重要的是注意力模式完全陌生。它不知道如何分配 128K token 的注意力权重,可能只关注前几千个 token,后面的内容虽然”进去了”但”没被用上”。
  • ”扩窗口时,数据从哪里来?”
    • 答:几种来源:1)现有长文档(如书籍、论文、法律文件);2)合成数据(把多个短文档拼接成长序列);3)领域特定长文本(如代码仓库、多轮对话记录)。关键是数据质量要高,且分布与目标场景匹配。
  • ”128K 模型服务时,怎么解决 KV cache 爆显存的问题?”
    • 答:1)GQA/MQA(减少 KV cache 头数);2)KV cache 量化(INT8/INT4);3)PagedAttention(提高显存利用率);4)多卡 TP(张量并行,把 KV cache 分散到多张卡);5)限制最大 batch size 和生成长度。

#22. “Lost in the Middle” 是什么现象?为什么它在 RAG 里特别致命?

#标准答案

“Lost in the Middle” 指的是模型在长上下文里对中间位置的信息利用最差,往往更容易关注开头和结尾。这不是说中间内容完全没输入进去,而是说它进入了上下文,却没有在生成决策里被有效使用。

它在 RAG 里特别致命,因为检索系统常常会把多段证据拼接到一个长 prompt 里。如果真正关键的证据刚好落在中段,模型就可能出现“证据在上下文里,但答案还是错”的现象。所以 RAG 不是检索到了就结束,还要考虑证据排序和上下文组织。


#深度解析

1. 现象的定量描述

Liu et al. (2023) 的实验发现:在 GPT-3.5/Claude 等模型上,当上下文包含多个文档时,模型对中间位置文档的问答准确率显著低于开头和结尾。

具体数据(多文档问答任务,每个文档约 100 token,共 20 个文档):

  • 开头位置(第 1-5 个文档):准确率 ~80%
  • 中间位置(第 8-13 个文档):准确率 ~50%
  • 结尾位置(第 16-20 个文档):准确率 ~75%

中间位置的准确率比开头和结尾低了约 25-30 个百分点。

2. 为什么模型会"丢失"中间信息?

根本原因不是位置编码失效(RoPE 对所有位置一视同仁),而是注意力分配的问题:

  • 开头文档通常包含 system prompt、问题定义,模型天然关注
  • 结尾文档离生成位置最近,局部注意力更强
  • 中间文档既不被问题直接关联,也不享受位置邻近优势

此外,softmax 归一化也会加剧这个问题:当上下文很长时,每个位置分到的注意力权重被稀释,中间位置更难获得显著权重。

3. RAG 场景下的缓解策略

策略 做法 效果
重排序 用更强的 reranker 把最相关证据排到最前面 让关键证据享受"开头优势"
压缩/摘要 对召回文档先做摘要,减少总长度 减少中间位置数量
分层检索 先粗读找相关文档,再细读相关段落 避免一次性塞入过多文档
证据标注 在 prompt 中显式标记"以下是关键证据" 引导模型关注
多轮查询 第一轮找文档,第二轮基于找到的答案追问 缩短每轮上下文

4. 面试官常见深挖追问

  • "Lost in the Middle 是因为位置编码的问题吗?"
    • 答:不完全是。位置编码负责"让模型知道位置",但 Lost in the Middle 是"模型知道了位置但选择不关注"。本质上是 attention 机制和任务结构导致的注意力偏置,不是编码失败。即使换成 ALiBi 或其他位置编码,中间位置仍然相对弱势。
  • "如果一个 RAG 系统召回了 20 段证据,你会怎么组织它们进 prompt?"
    • 答:不会简单拼接。我会:1)先用 reranker 精排,确保 top-3 最相关;2)按逻辑顺序而非相关度排序(如时间线、因果关系);3)每段证据前加标记(如"[证据1]"),让模型知道在引用;4)如果总长超过窗口限制,先做摘要或分层处理。
  • "Lost in the Middle 和 Long Context 的能力退化是一回事吗?"
    • 答:不是。Long Context 退化是指模型在超长序列上整体效果下降(如 attention 稀释、位置编码外推失效);Lost in the Middle 是特指"中间位置利用率低于两端",即使在模型能处理的上下文范围内也存在。前者是能力问题,后者是注意力分配问题。

#23. 一个中英文混合系统为什么不能随便更换 tokenizer?

#标准答案

不能随便更换 tokenizer,是因为 tokenizer 并不是一个独立小组件,而是和词表、embedding 矩阵、训练数据分布、推理成本全部绑在一起。尤其是中英文混合系统里,切分粒度稍微变化,就会影响中文是否被切碎、英文长词是否被拆得太细、代码和符号是否异常膨胀。

更现实的问题是:一旦 tokenizer 变了,原来的 embedding 对齐关系就变了,很多已训练好的参数不能直接复用,线上 prompt 长度分布和成本也会被改写。所以除非你准备连训练分布一起重新适配,否则不要把换 tokenizer 当成轻量操作。


#深度解析

1. tokenizer 和模型参数的绑定关系

tokenizer ↔ 词表大小(vocab_size) ↔ embedding 矩阵形状(vocab_size × d_model)

更换 tokenizer → 词表大小变化 → embedding 矩阵和输出头 (lm_head) 形状改变 → 必须重新训练这些层。

组件 参数量 换 tokenizer 影响
token embedding V × d 必须重新训练(维度 V 变了)
lm_head V × d 必须重新训练
中间层 不变 理论上可保留,但分布 mismatch

2. 中英文混合系统的 tokenizer 特殊挑战

问题 现象 影响
中文切分过细 "人工智能" → "人/工/智/能"(4个token) 序列变长、成本上升、语义碎片化
中文切分过粗 "人工智能" → 1个token 罕见词表膨胀、OOV 增加
英文子词不平衡 "unhappiness" → "un/happiness" vs "unhapp/iness" 不同切分导致语义不一致
代码/符号膨胀 空格和缩进产生大量重复token 上下文浪费、推理成本飙升

3. 具体案例:LLaMA tokenizer 的中文问题

LLaMA 原始词表以英文为主,中文 token 覆盖差:

  • 一个中文字符可能被切成 2-3 个 token
  • 同样长度的中文文本,token 数是英文的 1.5-2 倍
  • 直接后果:同样的 4096 上下文窗口,中文实际容纳的字符数只有英文的 60%

解决方案(如 Chinese-LLaMA):

  1. 扩展词表:在原始词表基础上增加中文常用字词
  2. 继续预训练:用中文语料训练新加入的 embedding
  3. 注意:不能只换 tokenizer 不训练,否则新 token 的 embedding 是随机的

4. tokenizer 对训练成本和推理成本的影响

tokenizer token 数 训练成本 推理成本
A(中文优化) 1000 tokens 基准 基准
B(英文为主) 1800 tokens +80% +80%

假设两个 tokenizer 对同一段中文文本的切分:

token 数直接影响:

  • 训练:每个 token 都要过一遍 forward + backward
  • 推理:KV cache 大小 ∝ seq_len
  • API 计费:按 token 数计费

5. 面试官常见深挖追问

  • "如果要给中文 LLM 扩展词表,具体怎么做?"
    • 答:1)收集大量中文语料,用 BPE/SentencePiece 重新训练或扩展 tokenizer;2)确定新词表大小(如从 32K 扩到 60K);3)扩展 embedding 矩阵和 lm_head,新行随机初始化;4)用中文语料做继续预训练,让新 token 的 embedding 收敛;5)评估:检查中文 PPL 是否下降、同样文本的 token 数是否减少。
  • "为什么 GPT-4 的 tokenizer 对代码特别友好?"
    • 答:GPT-4 的 tokenizer(cl100k_base)在训练时加入了大量代码语料。代码中的常见模式(如 "def ", "import ", "self.", 常见缩进)被识别为独立 token。这让代码序列更短、结构更清晰。如果 tokenizer 没有见过这些模式,代码会被拆成碎片(如 "d/e/f/ /f/o/o"),既浪费上下文又损害代码理解。
  • "tokenizer 的 vocab_size 越大越好吗?"
    • 答:不是。vocab_size 增大的代价:1)embedding 矩阵和 lm_head 参数量线性增长(V×d);2)输出层 softmax 计算量增大;3)稀有 token 的 embedding 学习不充分。经验平衡:多语言模型 50K-100K,纯英文 32K-50K。太大反而降低效率。

#24. 你怎么理解 token 数、上下文长度、KV cache 占用之间的关系?

#标准答案

token 数、上下文长度和 KV cache 占用之间是近似线性放大的关系。因为每多一个历史 token,模型在每一层都要额外保存对应的 K/V,所以缓存开销会随着 batch size * seq_len * num_layers * kv_heads * head_dim 一起增长。

面试里最好再补一句工程直觉:很多人只盯着模型权重大小,其实在线推理里显存爆掉,经常不是因为权重本身,而是因为长上下文和大 batch 把 KV cache 顶上去了。也正因为如此,GQA/MQA、cache 量化、PagedAttention 这些技术才会变得很关键。


#深度解析

1. KV cache 显存占用的精确公式

KV_cache_size = 2 × batch_size × seq_len × num_layers × kv_heads × head_dim × bytes_per_param

其中:
- 2:K 和 V 两个张量
- batch_size:同时处理的请求数
- seq_len:序列长度(输入 + 已生成)
- num_layers:模型层数
- kv_heads:KV 头的数量(MHA=num_heads, GQA=group_size, MQA=1)
- head_dim:每个头的维度
- bytes_per_param:2 (FP16) 或 1 (INT8) 或 0.5 (INT4)

2. 具体数值感受(FP16)

假设模型配置:layers=32, heads=32, head_dim=128, kv_heads=8 (GQA)

batch seq_len KV cache 大小 说明
1 1K 1 GB 短对话,无压力
1 4K 4 GB 中等长度
1 32K 32 GB 长文档,单请求就占很大显存
8 4K 32 GB batch=8 时,KV cache 超过模型权重
32 8K 256 GB 多卡 serving 才能承载

对比:7B 模型权重(FP16)约 14GB。当 batch×seq_len 足够大时,KV cache 远超权重。

3. 为什么 token 数增长会导致显存"爆炸"?

序列长度从 1K → 32K(32 倍):
- KV cache 从 1GB → 32GB(线性增长,32 倍)
- Attention 计算量从 O(1K²) → O(32K²) = O(1M)(平方增长,1024 倍)

长度翻倍 → KV cache 翻倍(线性),但 attention 计算量翻 4 倍(平方)。所以长上下文不只是"显存问题",也是"计算问题"。

4. 三种降低 KV cache 的策略对比

策略 原理 显存节省 效果损失 工程复杂度
GQA/MQA 多个 Q 头共享 K/V 头 4-8 倍 轻微 低(推理时自动生效)
Cache 量化 KV cache 存 INT8/INT4 2-4 倍 轻微-中等 中(需要量化/反量化)
PagedAttention 按 block 动态分配,用完即释放 20-40% 高(需改造推理引擎)
H2O/SnapKV 只保留重要的 KV,丢弃冗余 30-50% 中等 中高(需实现压缩策略)

5. 面试官常见深挖追问

  • "如果 batch_size=1, seq_len=128K,KV cache 多大?怎么优化?"
    • 答:以 32 层、GQA(8 头)、head_dim=128、FP16 计算:2 × 1 × 128K × 32 × 8 × 128 × 2 = 约 16GB。优化:1)GQA 已把 KV cache 降到 MHA 的 1/4;2)INT4 量化再省 4 倍 → 4GB;3)H2O 动态压缩 → 可能 2-3GB;4)PagedAttention 避免内存碎片;5)如果仍不够,考虑模型并行(TP)把 KV cache 分到多卡。
  • "为什么推理时显存占用会随生成过程不断增长?"
    • 答:Decode 阶段每生成一个新 token,就要在 KV cache 中追加该 token 的 K 和 V。所以 seq_len 从 prompt 长度逐步增长到 prompt + output 长度,KV cache 线性增长。当输出很长(如 4K+)时,KV cache 可能从生成初期的几 GB 增长到几十 GB。
  • "Prefill 阶段的 KV cache 和 Decode 阶段有什么区别?"
    • 答:Prefill 阶段一次性计算整个 prompt 的 KV cache,然后存储起来。Decode 阶段每步只计算新 token 的 K/V,然后追加到已有 cache。Prefill 是"批量写入",Decode 是"逐条追加"。所以 Prefill 阶段显存占用瞬间跳到 prompt 长度对应的 KV cache 大小。