#模块二: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 r→er
第 2 轮:
w er出现 2+6 = 8 次(注意 lower 中w和er相邻)- 合并
w er→wer
第 3 轮:
low出现 5+2+1 = 8 次- 合并
l o w→low
最终词表包含: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 相反——从一个大词表开始,不断删除使损失增加最少的子词。
- 初始词表:包含所有可能的字符、常见词、高频子串
- 对训练语料中的每个词,用 Viterbi 算法找到最优切分
- 计算每个子词的概率
- 删除那些概率低、对整体损失影响小的子词
- 重复直到达到目标词表大小
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 常见的 RoPE、ALiBi 也都可以理解为在更适合长上下文和自回归的场景里编码位置信息。
绝对位置编码回答的是”我在第几个位置”,相对位置编码回答的是”我和你相隔多远”。后者在长文本里往往更自然,因为很多语言关系并不关心绝对编号,而更关心相对距离,所以在长度扩展场景下通常更稳。
#深度解析
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-j,distance=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。
- 答:Sinusoidal 编码虽然连续、有确定性,但它编码的是绝对位置。训练时最长 512,测试时 1024 就外推失败了(因为
- ”如果不用任何位置编码,模型能学会位置信息吗?”
- 答:纯自注意力是位置无关的(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 = 3,base = 10000,d = 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 的区分能力下降。
- 答:base 变大意味着
#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 在长上下文下会遇到什么问题?
#标准答案
长度外推难,本质上是模型在训练时只见过某个长度分布,比如 4K 或 8K,一旦推理时突然拉到 32K、128K,模型并不是只多看一点内容,而是整个位置分布、依赖距离、注意力使用方式都超出了训练经验。
对 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)
每一步都需要:
- 调整位置编码(如 NTK-aware scaling)
- 准备对应长度的训练数据
- 调整学习率(通常比预训练小一个数量级)
- 评估外推效果
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):
- 扩展词表:在原始词表基础上增加中文常用字词
- 继续预训练:用中文语料训练新加入的 embedding
- 注意:不能只换 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 大小。