第 21 章 · 通往大模型
大模型的工程与基础设施
上一章我们讲清了大模型“是什么、怎么训”。但“上千 GPU 训数周”这句话背后, 藏着一整个工程世界。这一章我们钻进机房,用大白话讲清楚:为什么非用 GPU 不可、 显存为什么是真正的瓶颈、一个单卡放不下的模型怎么切开分到成百上千张卡上、 这些卡又怎么高速通信,以及推理时那些让它“快起来”的关键技巧。
上一站(第 20 章)讲了大模型“怎么炼”;本站钻进机房,看它靠什么才跑得起来——GPU / 显存 / 并行 / KV cache / MoE / 稀疏注意力;下一站(第 22 章)讲你作为使用者怎么把它用好。
读完这一章,你会明白
- GPU 凭什么比 CPU 适合深度学习(上万个“小工人”并行);
- 为什么显存是训练大模型最先撞上的墙,以及混合精度/量化怎么省显存;
- 数据并行、张量并行、流水线并行分别把什么“切开”了;
- AllReduce、NCCL、NVLink 这些通信黑话在干嘛;
- 推理提速的关键:KV cache、批处理、量化,以及量化与“降智”体感的关系;
- MoE(混合专家)把什么换成并列专家、
N/k怎么定、Attention 是否共用、路由器和专家怎么一起训、负载均衡,以及压 k/缩专家会不会降智; - 稀疏注意力的几种套路(滑动窗口 / 全局 / 动态 top-k),以及它和 FlashAttention 的区别;
- 1M 上下文到底怎么做到:RoPE 外推 + 位置插值/NTK/YaRN,再叠加少算 / 缓存 / 分块 / 并行 / 长训练的组合拳;
- 推理服务怎么扛住海量用户:prefill/decode 两阶段、连续批处理、PagedAttention 分页复用 KV cache、多副本负载均衡,以及吞吐与延迟的取舍。
1. 规模带来的三座大山
回忆一下:大模型的训练和推理,归根结底就是海量的矩阵乘法(第 3、17 章), 说白了是天量的加法和乘法。规模一上来,立刻撞上三座大山:
大模型工程的三大挑战。本章逐个击破。
2. GPU:为“并行”而生
一颗 CPU 很聪明,但核心不多——家用的 4 核、16 核,服务器上百核也就到头了。它擅长“一件复杂的事,一步步做好”。 可矩阵乘法不需要“聪明”,它需要的是同时做成千上万次简单的乘加。这正是 GPU(图形处理器)的主场。
一颗 CPU 像几位博士:能解难题,但人少。一块 GPU(如英伟达 H100)有上万个计算核心 (CUDA 核),像一万个小学生:每个只会算简单的加减乘除,但一万道口算题同时开做, 瞬间就完。大模型的矩阵运算恰好能拆成海量互不依赖的小乘加,天生适合这种“人海战术”。
怎么把一个大矩阵乘法均匀地拆给上万个核、让它们别打架又别闲着?这件苦活由英伟达的 CUDA 平台包办。开发者只管调用,底层调度交给它——这也是英伟达护城河极深的原因之一。 (人们口中的“买卡”,买的就是这种 GPU 卡。)
3. 显存:第一堵墙
比“算得慢”更早撞上的,往往是“放不下”。GPU 自带的高速内存叫显存(VRAM), 训练时它要同时装下好几样东西:
- 参数本身(那 1750 亿个权重);
- 每个参数的梯度(反向传播算出的更新量,和参数一样多);
- 优化器的状态(比如 Adam 要为每个参数多存两个量,第 8 章);
- 前向时的激活值(留着反向传播用,第 3、6 章)。
粗略一算:光是“参数 + 梯度 + Adam 状态”,每个参数就要存好几份。1750 亿参数用普通精度存, 轻松需要上 TB 的显存——而一张顶级 GPU 也就几十 GB。结论很硬:大模型根本塞不进单卡,必须切开。 怎么切,是第 5 节的主题。先看两招“把每份都变小”的省显存术。
4. 省显存两招:混合精度与量化
数字存得越“精细”,占的空间越大。一个小数默认用 32 位(float32)存;但训练/推理其实用不了那么精确。
- 混合精度训练训练时把大部分数值改用 16 位(float16/bf16)存和算,显存直接减半、还更快; 少数对精度敏感的地方仍用 32 位兜底,所以叫“混合”。
- 量化(quantization)推理时更狠,把权重压到 8 位甚至 4 位整数。 这就是你能在个人电脑上跑“量化版”开源模型的原因。
无损音乐(float32)音质完美但文件巨大;压成 MP3(int8/int4)体积骤减,大多数人几乎听不出差别。 量化同理:用一点点几乎察觉不到的精度损失,换来好几倍的显存和速度收益,让大模型能“瘦身”下沉到消费级设备。
网上说的“降智”,很多发生在推理部署环节,而不是预训练把参数改坏了:
- 低比特量化(INT8/INT4/GGUF 等):权重精度变粗,难题、长尾知识、细腻推理最先掉链子。
- 蒸馏后的小模型(第 20 章):容量上限更低,不是同一档聪明。
- 上下文被截断:KV cache 或产品限制只保留短窗口,模型“看不见”完整材料。
- 对齐过猛(对齐税):更安全、更拒答,体感像“不敢想、不会答”。
- MoE 激活过少(第 8 节):为省算力把 top-k、专家规模压得太狠,每步实际用的“脑子”变小。
同一套权重,用 FP16 云端跑和用 4-bit 本地跑,不是同一个体验——降智往往是工程权衡(速度/成本/安全)的副产品,先查部署配置再怀疑“模型本身”。
5. 把模型切到多卡:三种并行
既然单卡放不下、也算不快,就得把活儿拆给很多卡。主流有三种拆法,常常混着用。
① 数据并行:人手一份模型,各看一部分数据
最直观的一种。每张卡都放一份完整的模型,但各自只吃一部分训练数据,各自跑前向反向、 算出自己的梯度。问题来了:每张卡只看了局部数据,梯度各不相同,怎么保证大家的模型还是“同一个”?
办法是:每一步之后,把所有卡的梯度求和再取平均,然后广播回每张卡,大家用同一个平均梯度更新。 这个“各自算 → 汇总平均 → 同步回去”的集体动作,就是大名鼎鼎的 AllReduce。
早期做法是设一台中心“参数服务器”收集、平均、再下发梯度,但它很快成为通信瓶颈(所有卡都挤它)。 现代训练改用环形/树形等去中心结构,让每张卡都能拿到全局平均梯度——这类“大家一起求和平均”的通信, 统称 AllReduce。
② 张量并行:一层太大,横切开分给几张卡
如果模型大到单层的权重矩阵一张卡都放不下,就得把这个矩阵本身切块,分给多张卡各算一部分, 再把结果拼接起来。因为权重是用张量表示的,这种切法叫 张量并行(也叫模型并行)。
想象你在一张大拼图上画了整个神经网络,然后把拼图掰成几块,每块交给一张 GPU—— 每张卡只负责网络的一部分计算。算完再把碎片拼回去凑成完整结果。这种“只搬运、拼接,不改变数值”的通信, 有个专门的名字叫 AllGather(区别于 AllReduce 的“求和平均”)。
③ 流水线并行:不同层放不同卡,像流水线接力
还有一种:把网络的不同层放到不同卡上——第 1–10 层在卡 A,第 11–20 层在卡 B…… 数据像工厂流水线一样,算完 A 的部分传给 B 接着算。这叫流水线并行。 真实的超大模型训练,往往三种并行叠加使用,才能把一个庞然大物铺到上千张卡上同时开动。
6. 卡与卡怎么通信
一旦切到多卡,数据就得在卡之间飞来飞去,通信快慢直接决定训练效率。这里有几层:
- NCCL英伟达的集合通信库。开发者只要调
allreduce()、allgather()这些函数, 底层怎么高效传由它搞定,不用操心。 - NVLink / NVSwitch同一台服务器内,GPU 之间的专用高速公路(带宽远超普通网络), 让 8 张卡像一张大卡一样紧密协作。
- 跨服务器成千上万张卡分布在很多台机器上,靠高速网卡 + 交换机(接入层 leaf、汇聚层 spine)连起来—— 这套“分层组网”思想,和普通数据中心网络一脉相承。
训练大模型,本质是“不断调参数,让输出去碰目标”;而挖矿是“不断试,让 hash 去碰目标值”。 两者都是海量并行的“碰撞”游戏——这也是为什么显卡在这两股浪潮里都成了硬通货。
7. 推理提速:让它“回答得快”
训练是一次性的苦工,推理(你每次提问)却要天天做、追求又快又省。几个关键招数:
- KV cache最重要的一招。自回归生成时(第 19 章),每多写一个词,前面那些词的 Key、Value 并不会变 (第 17 章)。把它们缓存下来,新词只算自己的部分,省掉海量重复计算,生成因此快很多。它直接建立在你学过的 Q/K/V 之上。
- 批处理(batching)把很多用户的请求攒成一批一起算,充分喂饱 GPU 的上万个核,提高吞吐。
- 量化推理用低精度权重(第 4 节),又快又省显存——也最容易带来“降智”体感。
- 推测解码(speculative decoding)用小模型先“猜”一串 token,大模型再批量验证,猜对就白赚速度;猜错再回退。不降低能力,是用额外算力换延迟的推理技巧。
这几招是“单点提速”。把它们组织成一套能同时服务海量用户的在线系统,还需要连续批处理、KV cache 分页复用、多副本调度——这是第 10 节的主题。
8. MoE:不是每次都动用全部参数
还记得 DeepSeek-V3 “6710 亿参数,但每次只激活 370 亿”吗?秘密就是 MoE(Mixture of Experts,混合专家)。一句话:参数堆得很多,但每个 token 只用得上其中一小撮。 下面把它到底怎么运作,一步步拆开。
① 到底把什么换成了“专家”?
回忆第 18 章:一块 Transformer Block = 注意力 + 前馈网络(FFN)。MoE 动的不是注意力,而是那层 FFN—— 把原来一个 FFN,换成并排的许多个结构相同的 FFN,每一个就叫一个专家(expert)。 注意力照旧由所有 token 共享;真正“分家”的只是 FFN 这一层。
原来是一位“全科 FFN 医生”给每个 token 看病;MoE 把他拆成几十个专科医生(专家)。 你挂号(一个 token)进来,分诊台只把你分给最对口的一两个科,用不着惊动所有医生。 于是“医院很大、科室很全”(总参数多),但“你这一次只看一两个科”(每次算得少)。
MoE 只替换 Block 里的 FFN 段;Attention 不动。左边「一个 FFN」→ 右边「Router + N 个并列专家」。
② 结构怎么定:并列专家、共享 Attention,N 和 k 从哪来?
很多人接着会问:专家个数 N 是训出来的吗?FFN 前面要不要也复制 N 套 Attention?
答案很干脆:N 和 k(每次点亮几个)都是设计模型时写死的超参数,和
d_model、层数、head_num 一样——定好结构再开训,中途不能随便改(改 N 等于换网络,权重对不上)。
也不是每一层都必须是 MoE:常见做法是若干层用 MoE FFN,其余层仍是普通的一个 FFN。
| 模块 | 几个? | 和别的模块关系 |
|---|---|---|
| Masked Self-Attention | 1 套 WQ/WK/WV/WO | 整层共用;所有 token 先在这里混上下文(第 18 章) |
| Router | 1 个小线性层 Wgate (d×N) |
每个 token 各自打分、各自选 top-k |
| FFN 专家 | N 套并列,各含 W1/W2 | 同一输入 h,每次只算其中 k 套,输出加权相加 |
MoE 只把 Block 里的「一个 FFN」换成「N 个并列 FFN + 路由器」;Attention 不会变成 N 份——否则参数量和算力会再乘 N,和「省算力」目标相反。
输入 X [n × d]
→ Masked Self-Attention + Add&Norm ← 共用 1 套, token 之间互相看
→ 对每个 token 的向量 h:
Router(h) → top-k 专家 FFN → 加权求和
→ Add&Norm
→ 输出 [n × d] → 进下一层 Block
一块 MoE Block:Attention 仍共用 1 套;FFN 段换成 Router + N 个并列专家(✓=本 token 被点亮的 top-k)。进出形状仍是 [n×d]。
左:专家若串行叠在一起,算力随 N 线性涨——不是 MoE。右:并列 + top-k,算力只跟 k 走。
N 个专家是同一层里并排摆着的多条 FFN 支路,不是「先过专家 1 再过专家 2」那种深度串联。 和「Transformer 堆 N 层 Block」不同:堆层是流水线一站接一站;MoE 是一个站里多个科室,分诊只去其中 k 个。
③ 路由器:每个 token 该找哪几个专家?
决定“分诊”的是一个路由器 / 门控网络(router / gating)——其实就是一个很小的线性层。 它给每个 token 对 N 个专家各打一个分,softmax 之后挑出分最高的 top-k 个(常见 k=2), 只让这几个专家干活,它们的输出再按门控分加权求和。
同一个 token 进来,路由器只点亮 top-2 个专家(紫色实线),其余专家(灰色虚线)这次一次都不跑。
Router 不是只吐「专家编号」:先可微打分,再稀疏选 k 个,最后用门控分加权专家输出——训练时梯度从加权这一步回传。
// 每个 token 各走一遍:
scores = softmax(token · W_gate); // 小线性层给 N 个专家打分 1
top = top_k(scores, k = 2); // 只留分最高的 2 个专家 2
out = 0;
for (e : top) // 只计算被选中的专家
out += scores[e] * Expert[e](token); // 各自算 FFN, 再按门控分加权 3
// 其余 N-2 个专家: 这一步一次都不跑 4
- 门控网络就是一个小小的线性层
W_gate,把 token 映射成“对每个专家的偏好分”。 - 取 top-k(这里 k=2)——“稀疏”正来自这一步:N 个专家每次只点亮 k 个。
- 每个被选中的专家是一个完整 FFN(第 18 章),各自输出再按门控分加权相加。
- 关键:算力只花在 k 个专家上,和专家总数 N 无关——这就是“参数多、算得少”。
举个数:N=8 个专家、每次只选 k=2,那每个 token 实际只算了 2/8 = 25% 的 FFN 参数。 算力只跟“激活的专家数 k”走,和“专家总数 N”无关——这就是“参数能堆到很大、计算量却不跟着爆”的根源。 DeepSeek-V3 更进一步:用了几百个“细粒度”小专家 + 1 个始终激活的共享专家(存放通用知识), 每个 token 激活其中 8 个 + 那个共享专家,于是 6710 亿总参数里,每步只算了约 370 亿。
④ 路由器和专家怎么一起训出来?
听到「top-k 选专家」,很容易以为路由器输出的是硬开关——像 if-else 直接指定「激活 FFN ② 和 ④」, 那梯度似乎传不回去,路由层会很难训。实际工程里并不是这样裸奔的: 先 softmax 打分,再对选中的 k 个专家做门控加权求和,和主任务端到端一起反传。
| 要训的参数 | 是什么 | 和稠密 Transformer 比 |
|---|---|---|
W_gate |
路由器,把 h 映射成对 N 个专家的偏好分 | 多出来的 |
Expert_1 … Expert_N 各自的 W1/W2 |
N 套完整 FFN | 原来 1 套 FFN → N 套 |
| Attention、Embedding 等 | 与稠密模型相同 | 不变 |
没有单独的「路由训练算法」——仍是预测下一个 token 的交叉熵 + 优化器(第 5 章),和 MNIST 同一套更新公式。
Router 到底输出什么? 分两步,不要和「只吐专家编号」混在一起:
scores = softmax(h · W_gate) // ① 可微: N 个门控分, 和为 1
top = top_k(scores, k) // ② 稀疏: 只计算分最高的 k 个专家
out = Σ scores[e] · FFN_e(h) // ③ 用门控分加权 k 个输出, 不是硬选一个
工程实现靠门控加权让 Router 和专家走同一条反传链,不是裸 if-else 硬选。
离散的是「谁进 top-k」,但进了之后,scores[e] 仍是 softmax 的软权重,会乘在专家输出上——
loss 对 out 求导时,梯度既能改被选中的 FFN,也能改 W_gate(「这类 token 该不该多分点给专家 ②」)。
所以路由层虽小,却和 MNIST 里一层全连接 W 一样,走同一条反传链,并不是不可训的黑箱。
某个 token 过完 MoE 后 loss 偏大,梯度大致兵分两路:
→ 专家 ②、④ 的 W1/W2:「你接到这类活,输出得改。」
→ W_gate:「下次遇到类似的 h,门控分要不要调整?该不该少宠 ②、多给 ⑦ 一点?」
这一步只有被选中的 k 个专家有梯度;没点名的专家本轮不更新,别的 token 可能会点到它们。
loss 反传:被选中的 k 个专家各收一份梯度;Router 的 W_gate 通过门控权重同时更新;未点亮专家本轮不动。
专家怎么「训成不同」? 并不是事先贴标签「专家 1=语法、专家 2=数学」:
- 随机初始化:N 套 FFN 起点本就不同;
- 稀疏更新:每个 token 只更新 top-k 个专家,长期下来相似样本会反复走同一小撮专家;
- 路由共进化:哪条「专家组合」能让 loss 降,路由就更爱往那儿送;
- 分工是涌现的:数据 + 路由 + 稀疏梯度自然分出来的,不是人工指定专科。
不同 token 被 Router 送到不同专家;相似 token 长期走同一路 → 专家涌现分工(示意,真模型里专科未必人类可读)。
也不是「先训专家、再 frozen 训路由」——常见做法是从头联合预训练,路由和专家同步更新。 真正多出来的训练难点在下一节:怎么防止路由只宠少数专家,让冷门专家也收到足够样本。
⑤ 最难的一环:负载均衡
MoE 有个“不训不知道”的坑:路由器会“偏心”。它一旦尝到某几个专家好用的甜头,就老往那儿送 token, 结果热门专家被挤爆、冷门专家饿死(收不到数据就永远练不好)——这叫专家坍缩(routing collapse), 等于白白浪费了大半参数。所以 MoE 训练里,怎么把 token 摊匀比路由本身还关键:
- 负载均衡损失:在主损失之外加一个惩罚项,谁分配得越不均衡就罚得越狠,逼路由器把 token 尽量摊平到所有专家。
- 专家容量:给每个专家设一个“每批最多接多少 token”的上限,超出的“溢出”给别人或跳过,防止个别专家过载。
- DeepSeek 的改进:提出“无辅助损失”的均衡——给每个专家加一个会自动调整的偏置分来纠偏,既均衡,又不让惩罚项来干扰主目标。
左:路由坍缩 → 大半专家名存实亡。右:负载均衡逼 Router 把 token摊开,各专家都能练到。
一个常见误会:“每次只激活 370 亿,那放得下 370 亿不就行了?”不行。 你事先并不知道每个 token 会用到哪些专家,所以全部 6710 亿参数都得常驻显存待命。 也就是:显存开销 ≈ 总参数,算力开销 ≈ 激活参数。MoE 的本质是“用显存换算力”—— 这也是为什么 MoE 大模型依旧是“显存大户”,只是训练/推理更省算力罢了。
⑥ 为省算力压 k、缩专家:会不会“降智”?
这是很多人问 MoE 时真正纠结的点:宣传的是几百 B 总参数,但推理时每个 token 只点亮少数几个专家—— 如果产品侧为了再省一点算力,把 top-k 从 8 降到 1、把专家做得更小、或可路由的专家池变少, 算不算另一种“降智”?
理论上:会。 但机制和量化不同——不是权重变糙,而是这一步前向实际调用的容量变小了。 每个 token 在 MoE 层真正吃到的算力,大致正比于:
| 为了省算力常做的收紧 | 会发生什么 | 谁最先吃亏 |
|---|---|---|
| k 变小(如 8→2→1) | 每步只问更少的“专科医生”,综合意见变窄 | 复杂推理、需要多专长混搭的任务 |
| 专家 FFN 变窄/变浅 | 单个专家容量下降,专科也变成“小诊所” | 长尾知识、细腻变换 |
| 专家总数 N 变少 | 可分工的“科室”变少,路由选择空间变小 | 多领域、多风格并存的任务 |
| 路由出错 / 负载失衡 | token 被分给不合适的专家,或冷门专家没训好 | 表面像“随机变笨”,实为专家坍缩 |
广告牌写“671B 参数”,像说医院有几百个科室;但你这次挂号若只允许看 1 个小科室, 体验取决于当次激活的那一小撮,不是墙上挂的总编制。 简单问答可能差不多;难题、代码、多步规划更容易露出差距——体感就是“同一个 GPT,有时神有时呆”。
量化:同一套网络,数字精度变粗(4-bit/8-bit)。
MoE 收紧:网络还在,但这一步只走了更窄的子路径(更少的专家 × 更小的专家)。
两者都可能掉能力,也都可以是线上为成本/延迟做的工程取舍——外面很少写清楚,用户只能凭体感猜。
设计良好的 MoE 本意是:用更少的激活算力,逼近更大稠密模型的效果;若收紧过头,就从“省算力不损太多质”变成“真降智”。
训练阶段也有同类风险:负载均衡没做好 → 部分专家饿死 → 相当于大半个参数名存实亡(见上一节)。 所以 MoE 既要会“路由”,也要会“摊活”;推理阶段则要分清:你体验到的能力,看的是激活算力,不是 PPT 上的总参数量。
9. 稀疏注意力:不必“人人都聊”
第 17 章的注意力是“稠密”的:每个 token 都要和其他所有 token 两两打分。 长度为 n,计算量就是 n×n——长度翻倍,计算和显存翻四倍。 n=1000 时是百万对还好,可长上下文动辄 n=十万,那就是百亿量级的两两打分 (还得每层、每个头都来一遍),这正是第 20 章“长上下文难”的根子。
先算笔账:这个矩阵到底有多大?
很多人第一次意识到问题的严重,是在脑子里把 n=1M 代进去的那一刻。注意力打分矩阵 Q·KT 的形状是 n×n:
一张顶级 GPU 的显存也就 80 GB 上下,而这一个矩阵就要约 2 TB,再乘上几十层、几十个头,更是天文数字。 结论很硬:n=1M 的稠密注意力矩阵,根本不可能被整个存下来、更别说一次算出来。 这就是长上下文一切麻烦的总根源。
这里最容易误解的一点是:虽然它在数学上“存在”,但真实系统从不把完整的 n×n 矩阵物化(materialize)到显存里。 对付它有两条互补的路:
· 不一次算、也不整个存(分块 + FlashAttention):
把序列切块,注意力一次只出现一小条,算完 softmax 就丢,只留最终输出。FlashAttention 就是这么干的——
它一个都不少算(精确),但全程不写出完整大矩阵,所以省下海量显存。
· 干脆不全算(稀疏注意力):那一万亿个格子里绝大多数其实不相关,只保留窗口 / 全局 / top-k 的连接,
把 n² 降到接近线性——这是近似,下文细讲。
所以要分清两件事:显存可以靠“不物化”省掉;但稠密注意力的计算量本身仍是 O(n²), 只有稀疏近似才能把复杂度真正降下来。
稀疏注意力(sparse attention)的想法很朴素:没必要人人都聊。 大多数 token 只跟少数几个真正相关的 token 有关系,那就只算这部分连接,其余干脆不算。 至于“算哪些”,分成两大流派。
先拨开一个误会:1M token 不是把普通 attention 硬拉到 1,000,000
真要做百万级上下文,通常靠的是一整套组合拳,不是只把窗口数字改大。 算法上要少算(滑动窗口、全局 token、分块稀疏、动态 top-k……总之不再让所有 token 两两全聊); 系统上要更会存和搬(长前缀的 KV cache 得分页管理、压缩、复用,不然光缓存就先把显存吃爆); 推理上往往得分块 prefill,再把计算摊到多卡上一起扛;训练时也要专门补长上下文,让位置编码和模型能力都真的撑到更远。
所以今天很多“1M context”更准确的含义是:系统能处理百万级输入,但背后依赖的是 “少算 + 缓存 + 分块 + 并行 + 长上下文训练”这一整套工程,而不是朴素的全量稠密注意力。
还有一环:位置编码怎么“编”得到第 100 万个 token?
上面都在说“怎么少算、怎么存得下”,但有个更底层的问题:注意力本身对位置无感, 全靠位置编码告诉它谁在前谁在后。要支持 1M,就得让位置编码一路编到第 100 万位还不乱。今天主流靠的是 RoPE(旋转位置编码):
- RoPE 为什么天生能外推一点:它不给每个位置发“绝对号码牌”(那种一旦超出训练长度就没牌可发),而是按位置把 Q/K 向量旋转一个角度,角度和位置成正比。注意力打分只依赖两个 token 的相对角度差——换个绝对位置,只要相对距离没变,规律仍成立,所以它比可学习绝对位置更容易“往后多编一点”。
- 但硬外推会崩:训练时只见过 4K 以内的旋转角度,直接拿去编到 128K,那些转得最快的维度会转出“没见过的角度”,注意力就乱套。于是需要下面这些“拉伸”手段,再配少量长文本微调。
| 拉长手段 | 到底在拉什么 | 一句话直觉 |
|---|---|---|
| 位置插值(PI) | 把要处理的位置整体等比压缩回训练见过的范围(如把 0–32K 缩放到 0–4K 再喂进去) | “把尺子刻度整体缩小”,位置再远也落回熟悉区间 |
| NTK-aware | 不均匀地缩放 RoPE 各频率:高频少动、低频多拉,兼顾近处精度和远处覆盖 | “该细的地方保持细,该拉远的地方才拉” |
| YaRN | 在 NTK 思路上进一步分频段调整,是目前把上下文拉到几十上百 K 的常用配方 | NTK 的加强版,少量微调即可撑很远 |
这几招都在 RoPE 的“旋转角度”上做文章:让训练时只见过短序列的模型,推理时也能把位置编到远处而不乱——这就是模型能“事后加长”的关键(第 20 章提过它们的名字)。
把“1M 怎么做到”一次串起来
综合前面所有内容,一个百万级上下文的模型,其实是这五层同时做到位的结果——缺哪一层都撑不到 1M:
| 层面 | 要解决的问题 | 典型手段 |
|---|---|---|
| ① 位置编码 | 位置能不能“编”到第 100 万位还不乱 | RoPE + 位置插值 / NTK / YaRN 拉伸(本节上文) |
| ② 少算(算法) | 稠密注意力 O(n²),1M 时是天文数字 | 滑动窗口 / 全局 token / 分块稀疏 / 动态 top-k(本节下文) |
| ③ 存得下(系统) | 百万长前缀的 KV cache 会先把显存吃爆 | 量化、PagedAttention 分页复用(第 10 节) |
| ④ 算得动(工程) | 一次性吃 1M 输入,单卡扛不住 | 分块 prefill、多卡并行(第 5、6、10 节) |
| ⑤ 真学过(训练) | 没在长序列上训过,远处信息用不起来 | 长上下文续训 + 上面的位置拉伸再微调(第 20 章) |
所以“1M 上下文”是一套组合拳:位置编得到 + 少算 + 存得下 + 算得动 + 真训过,而不是把 full attention 的窗口数字改成 1000000。
① 固定套路:按位置预先规定谁跟谁算
- 滑动窗口(局部注意力):每个 token 只跟最近的 w 个邻居算(如某些模型 w=4096)。计算量从 n² 降到 n×w(线性)。别担心看不到远处:多层叠起来,感受野会像 CNN 一样层层扩大,远处信息仍能“一跳一跳”传过来。
- 全局 token:留几个“公共联络员”token,它们对所有人可见、也能看所有人,专门负责跨全局传信息(比如句首的特殊标记)。
- 跳跃 / 膨胀:每隔几个 token 才看一个,用很小的代价覆盖很远的距离。
把“局部窗口 + 少数全局”拼起来,就是 Longformer、BigBird 这类长文本模型的经典配方。看图最直观:
稠密(全算)
每个词和它之前所有词都算:n² 量级
滑动窗口
只跟最近几个邻居算:n×w 线性
窗口 + 全局
再留一个“全局列”负责跨远传信息
同一段 6 词序列(行 = 当前词,列 = 被看的词;只画下三角,因为 decoder 只能看前文)。稀疏,就是把大片格子“留白不算”。
② 动态挑选:让模型自己找“该聊的人”
固定套路有个软肋:万一“该关注的词”恰好落在窗口之外呢?于是有了内容自适应的稀疏—— 不按位置死板划定,而是当场挑出最相关的那几个。做法通常是:先用一个廉价的打分器快速估一下 每个历史 token 跟当前 query 有多相关,只留下 top-k 个,再对这 top-k 做精确注意力。
它用一个叫“闪电索引器(lightning indexer)”的小模块,为每个 query 飞快选出 top-k 个最该看的历史 token, 只对这 top-k 算注意力。说白了,就是把第 19 章的 top-k 思想从“挑下一个字”搬到了“挑该关注的 token”上, 让长上下文推理成本大幅下降。
让注意力更快有两条不同的路,常被搞混:
· 稀疏注意力:少算一些(近似)——用极小的精度损失换大幅提速。
· FlashAttention:一个都不少算(精确),但把计算重新编排、大幅减少显存读写,让同样的稠密注意力跑得更快更省显存。
前者是“少做题”,后者是“做题更利索”,两者还能叠加使用。
MoE 稀疏的是参数 / 计算路径——每个 token 只走部分专家 FFN(第 8 节)。
稀疏注意力稀疏的是token 两两连接——每个 token 只和部分 token 打分。
一个省在“哪些参数参与”,一个省在“哪些 token 互动”,超大模型里常常两个一起上。
10. 推理服务:几亿用户同时问,怎么扛得住
前面(第 5、6 节)讲的“切多卡、AllReduce”主要是训练的场景——训练是一次性的苦工。 但大模型天天要面对的是另一个战场:成千上万甚至上亿用户同时提问,还都要求几秒内回答。 第 7 节的 KV cache、批处理只是单点技巧,这一节把它们拼成一套在线推理服务,看它到底怎么把海量请求扛下来。
① 先分清:推理其实是“一快一慢”两个阶段
你发一句 prompt、模型回一段话,内部其实分成脾气完全不同的两段:
Prefill 是“读题”:能像训练那样整段并行算,吃得饱、很快;Decode 是“答题”:天生自回归、只能一步一个字。
- Prefill(预填充)把你的 prompt 整段一次性喂进去,一遍前向就算完所有位置、建好 KV cache。它计算密集,能把 GPU 喂得很饱。
- Decode(解码)之后逐字生成,每步只算一个新词(靠 KV cache 复用历史)。它计算量小、却要反复来很多次,GPU 经常在“等”,利用率天然偏低。
矛盾就在这:Decode 阶段单个请求根本喂不饱 GPU。一个用户逐字生成时,那上万个 CUDA 核大半闲着——这正是“怎么扛住海量用户”的突破口。
② 连续批处理:把很多人的“下一个字”拼成一次大计算
既然一个请求喂不饱 GPU,那就把很多请求凑一起算。可普通批处理要求“一批一起开始、一起结束”,而每个用户问题长短不一、生成长度也不同,硬等最慢的那个,GPU 又闲下来了。
于是有了连续批处理(continuous batching):调度器每生成一步就动态调整这一批的成员——谁生成完了(吐出结束 token)就立刻踢出、腾出位置,新到的请求随时插进来一起算。GPU 几乎不空转。
普通批处理是“包车”:满一车人一起出发、必须等所有人都到终点才算完,先到的白等。 连续批处理是“拼车”:到站的随时下车,路边新乘客随时上车,车座始终坐满。 同一批里,有人还在 prefill“读题”、有人在 decode“逐字答”,调度器把他们编排到一起——吞吐能翻好几倍。
③ KV cache 是这里最难搬的“大件行李”
一批里塞得下多少用户,往往不取决于算力,而取决于显存里能放下多少份 KV cache——每个用户的对话历史都得各留一份,越聊越长。传统做法给每个请求预留一整块连续显存,可对话长度事先并不知道:留多了浪费,留少了不够,显存被切得七零八落(碎片化),明明总量够却塞不下新请求。
vLLM 提出的 PagedAttention 借用了操作系统虚拟内存分页的老智慧: 把 KV cache 切成一块块固定大小的“页”,用到多少分多少、不必连续。于是碎片几乎消失,同样的显存能多塞很多并发请求。 更妙的是,多个请求共享的前缀(比如同一条超长 system prompt)可以共用同一批页,不必各存一份——这就是前缀复用,既省显存又省掉重复的 prefill。
④ 再不够,就多副本 + 负载均衡
单张(或单组)GPU 的吞吐终有上限。真到“亿级用户”的体量,靠的是最朴素也最有效的一招:把整个模型复制成很多份副本,前面架一个负载均衡器,把请求分发到最空的那个副本——这和普通网站用多台服务器扛流量是同一个道理。
两层扛法:副本之间靠负载均衡摊开总流量,每个副本内部靠连续批处理 + 分页 KV cache 榨干 GPU。
⑤ 一对甩不掉的矛盾:吞吐 vs 延迟
最后点破一个贯穿始终的权衡。批处理把很多请求攒一起算,吞吐(每秒服务多少人)高了;但攒批、排队本身会让每个人的延迟(尤其是“等第一个字”的时间)变长。
- 想省钱、服务更多人 → 批攒大一点,吞吐优先(适合离线批量任务);
- 想聊天跟手、首字快 → 批小一点、优先响应,延迟优先(适合在线对话)。
真实系统会盯着两个指标调:TTFT(Time To First Token,多久蹦出第一个字,主要受 prefill 影响)和 TPOT(Time Per Output Token,之后每个字的间隔,主要受 decode 批大小影响)。你感觉某个模型“想一下才开口、然后飞快”或“立刻开口、但吐字慢”,背后就是这套调度在做取舍。
小结
- 大模型工程的三座大山:算力、显存、通信。
- GPU 靠上万个 CUDA 核并行,天然适合矩阵乘法;CUDA 平台负责调度。
- 显存是第一堵墙(要放参数+梯度+优化器状态+激活);混合精度、量化能大幅省显存。
- 三种并行:数据并行(人手一份模型,AllReduce 同步梯度)、张量并行(切开单层矩阵,AllGather 拼接)、流水线并行(不同层放不同卡)。
- NCCL 管通信,NVLink/NVSwitch 是卡间高速公路;推理靠 KV cache、批处理、量化提速。
- MoE:FFN 换成 N 个并列专家(
N/k设计时定死),Attention 共用 1 套;路由器 softmax 打分 + top-k 门控加权,与专家端到端同训;还得靠负载均衡防专家坍缩。参数多但算得少;全部专家仍要常驻显存。压 k/缩专家会掉能力,是另一类「降智」来源。 - 稀疏注意力:只算部分 token 对——固定套路(滑动窗口 + 全局)或动态挑 top-k,把 O(n²) 压下来;它是“少算”(近似),区别于 FlashAttention 的“精确加速”。
- n×n 注意力矩阵在 n=1M 时高达一万亿个数(单头单层就 ~2TB),但它从不被整个物化:FlashAttention 分块流式算(精确、不写出大矩阵、省显存),稀疏注意力干脆少算(近似、降复杂度)。
- 百万级长上下文是五层组合拳:位置编码(RoPE + PI/NTK/YaRN 拉伸)编得到远处、稀疏注意力少算、量化/分页缓存存得下、分块 prefill+多卡算得动、长上下文续训真学过——不是把 full attention 的窗口数字改成 1M。
- 推理服务扛海量用户:分 prefill(并行、算得饱)/ decode(逐字、喂不饱)两阶段;用连续批处理把多请求拼一起、PagedAttention 分页复用 KV cache、多副本 + 负载均衡横向扩展;吞吐与延迟(TTFT/TPOT)之间要按场景取舍。
动手与思考
问题 1:为什么深度学习用 GPU 而不是 CPU?
因为训练/推理的核心是海量互不依赖的简单乘加(矩阵运算)。CPU 核心少但强,擅长复杂串行任务;GPU 有上万个简单核心,能把这些乘加同时并行做完,吞吐高得多。
问题 2:数据并行和张量并行,切开的分别是什么?
数据并行切的是数据:每卡放完整模型、各吃一部分数据,再用 AllReduce 平均梯度同步。张量并行切的是模型本身(单层权重矩阵太大放不下),把矩阵分块到多卡再拼接。
问题 3:KV cache 为什么能加速生成?它依赖哪一章的知识?
自回归生成时,已经出现的词的 Key/Value 不会改变,缓存起来就不必每步重算,新词只算自己的部分。它直接建立在第 17 章注意力的 Q/K/V 机制之上。
问题 4:MoE“6710 亿参数只激活 370 亿”是什么意思?好处是什么?
模型里有很多“专家”子网络(其实是并排的多个 FFN),每个 token 只被路由到最相关的少数几个专家计算,其余不参与。于是总参数(知识容量)可以很大,但每次实际算的量不大——用较小的算力代价换更大更强的模型。
问题 5:MoE 每次只激活一小部分参数,那是不是显存也跟着省了?
不省。因为事先不知道每个 token 会用到哪些专家,全部专家都得常驻显存待命。所以 MoE 是“显存开销 ≈ 总参数,算力开销 ≈ 激活参数”——它用显存换算力,省的是计算量,不是显存。
问题 6:为省算力把 MoE 的 top-k 压得很小,会不会“降智”?和量化降智一样吗?
会掉能力,但机制不同。量化是同一网络权重精度变粗;压 k/缩专家是每步只走更窄的子网络(更少专家 × 更小专家),激活算力变小。简单题可能差不多,难题、代码、多步推理更容易露怯。总参数量大不代表每一步都动用那么多能力——看的是当次激活算力。
问题 7:MoE 的路由器像硬开关,梯度怎么训?Attention 也要复制 N 份吗?
路由器先 softmax 得到门控分,再 top-k 选出专家,用门控分加权 k 个 FFN 输出——软权重让梯度能回传到 W_gate 和被选中的专家,和主 loss 端到端同训,没有单独的路由算法。Attention 仍是 1 套共用,不会随专家变成 N 份;并列的是 FFN。N 和 k 是设计时定死的超参数。难点在负载均衡,防止只宠少数专家。
问题 8:稀疏注意力和 FlashAttention 都能让注意力更快,区别在哪?
稀疏注意力是近似:干脆少算一些 token 对(只保留窗口/全局/top-k 的连接),用极小精度损失换速度。FlashAttention 是精确:一个都不少算,只是把计算重排、减少显存读写,让同样的稠密注意力更快更省显存。前者“少做题”,后者“做题更利索”,可叠加。
问题 9:一个模型号称支持 1M 上下文,靠的是把 RoPE 的位置直接编到 100 万吗?
不是,是五层同时到位的组合拳:①位置编码——RoPE 只依赖相对角度差、天生能外推一点,但硬编到百万会崩,得靠位置插值/NTK/YaRN把旋转角度“拉伸”回可用范围,再少量长文本微调;②少算——稀疏注意力(窗口/全局/top-k)把 O(n²) 压下来;③存得下——量化 + PagedAttention 分页复用 KV cache;④算得动——分块 prefill + 多卡并行;⑤真训过——在长序列上续训。缺哪层都撑不到 1M。
问题 10:一个用户逐字生成时明明用不满 GPU,为什么服务上亿用户反而扛得住?
正因为单个请求(尤其 decode 阶段)喂不饱 GPU,才有空间把很多请求拼一起算。连续批处理让不同用户的“下一个字”凑成一次大计算、谁完成谁下车、新请求随时插入,把 GPU 榨满;PagedAttention 把 KV cache 分页管理、消除碎片还能复用公共前缀,让一批塞下更多并发;真到亿级再用多副本 + 负载均衡横向扩展。代价是攒批会增加单人延迟,所以要在吞吐(TPOT)和首字延迟(TTFT)之间按场景权衡。
硬件和并行搞定,模型终于能跑了。可是对绝大多数人来说,我们既不训练也不部署,而是直接使用别人的大模型。 最后一章,我们讲讲怎么把它用好:提示词、RAG、工具调用、Agent、MCP……