第 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 像一万个小学生

一颗 CPU 像几位博士:能解难题,但人少。一块 GPU(如英伟达 H100)有上万个计算核心 (CUDA 核),像一万个小学生:每个只会算简单的加减乘除,但一万道口算题同时开做, 瞬间就完。大模型的矩阵运算恰好能拆成海量互不依赖的小乘加,天生适合这种“人海战术”。

怎么把一个大矩阵乘法均匀地拆给上万个核、让它们别打架又别闲着?这件苦活由英伟达的 CUDA 平台包办。开发者只管调用,底层调度交给它——这也是英伟达护城河极深的原因之一。 (人们口中的“买卡”,买的就是这种 GPU 卡。)

3. 显存:第一堵墙

比“算得慢”更早撞上的,往往是“放不下”。GPU 自带的高速内存叫显存(VRAM), 训练时它要同时装下好几样东西:

粗略一算:光是“参数 + 梯度 + Adam 状态”,每个参数就要存好几份。1750 亿参数用普通精度存, 轻松需要上 TB 的显存——而一张顶级 GPU 也就几十 GB。结论很硬:大模型根本塞不进单卡,必须切开。 怎么切,是第 5 节的主题。先看两招“把每份都变小”的省显存术。

4. 省显存两招:混合精度与量化

数字存得越“精细”,占的空间越大。一个小数默认用 32 位(float32)存;但训练/推理其实用不了那么精确。

量化就像把无损音乐压成 MP3

无损音乐(float32)音质完美但文件巨大;压成 MP3(int8/int4)体积骤减,大多数人几乎听不出差别。 量化同理:用一点点几乎察觉不到的精度损失,换来好几倍的显存和速度收益,让大模型能“瘦身”下沉到消费级设备。

“模型降智”最常发生在这里(推理压缩)

网上说的“降智”,很多发生在推理部署环节,而不是预训练把参数改坏了:

  • 低比特量化(INT8/INT4/GGUF 等):权重精度变粗,难题、长尾知识、细腻推理最先掉链子。
  • 蒸馏后的小模型(第 20 章):容量上限更低,不是同一档聪明。
  • 上下文被截断:KV cache 或产品限制只保留短窗口,模型“看不见”完整材料。
  • 对齐过猛(对齐税):更安全、更拒答,体感像“不敢想、不会答”。
  • MoE 激活过少(第 8 节):为省算力把 top-k、专家规模压得太狠,每步实际用的“脑子”变小。

同一套权重,用 FP16 云端跑和用 4-bit 本地跑,不是同一个体验——降智往往是工程权衡(速度/成本/安全)的副产品,先查部署配置再怀疑“模型本身”。

5. 把模型切到多卡:三种并行

既然单卡放不下、也算不快,就得把活儿拆给很多卡。主流有三种拆法,常常混着用。

① 数据并行:人手一份模型,各看一部分数据

最直观的一种。每张卡都放一份完整的模型,但各自只吃一部分训练数据,各自跑前向反向、 算出自己的梯度。问题来了:每张卡只看了局部数据,梯度各不相同,怎么保证大家的模型还是“同一个”?

办法是:每一步之后,把所有卡的梯度求和再取平均,然后广播回每张卡,大家用同一个平均梯度更新。 这个“各自算 → 汇总平均 → 同步回去”的集体动作,就是大名鼎鼎的 AllReduce

从“参数服务器”到 AllReduce

早期做法是设一台中心“参数服务器”收集、平均、再下发梯度,但它很快成为通信瓶颈(所有卡都挤它)。 现代训练改用环形/树形等去中心结构,让每张卡都能拿到全局平均梯度——这类“大家一起求和平均”的通信, 统称 AllReduce。

② 张量并行:一层太大,横切开分给几张卡

如果模型大到单层的权重矩阵一张卡都放不下,就得把这个矩阵本身切块,分给多张卡各算一部分, 再把结果拼接起来。因为权重是用张量表示的,这种切法叫 张量并行(也叫模型并行)

像把一张大拼图分给几个人

想象你在一张大拼图上画了整个神经网络,然后把拼图掰成几块,每块交给一张 GPU—— 每张卡只负责网络的一部分计算。算完再把碎片拼回去凑成完整结果。这种“只搬运、拼接,不改变数值”的通信, 有个专门的名字叫 AllGather(区别于 AllReduce 的“求和平均”)。

③ 流水线并行:不同层放不同卡,像流水线接力

还有一种:把网络的不同层放到不同卡上——第 1–10 层在卡 A,第 11–20 层在卡 B…… 数据像工厂流水线一样,算完 A 的部分传给 B 接着算。这叫流水线并行。 真实的超大模型训练,往往三种并行叠加使用,才能把一个庞然大物铺到上千张卡上同时开动。

6. 卡与卡怎么通信

一旦切到多卡,数据就得在卡之间飞来飞去,通信快慢直接决定训练效率。这里有几层:

一个有意思的类比

训练大模型,本质是“不断调参数,让输出去目标”;而挖矿是“不断试,让 hash 去目标值”。 两者都是海量并行的“碰撞”游戏——这也是为什么显卡在这两股浪潮里都成了硬通货。

7. 推理提速:让它“回答得快”

训练是一次性的苦工,推理(你每次提问)却要天天做、追求又快又省。几个关键招数:

这几招是“单点提速”。把它们组织成一套能同时服务海量用户的在线系统,还需要连续批处理、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)进来,分诊台只把你分给最对口的一两个科,用不着惊动所有医生。 于是“医院很大、科室很全”(总参数多),但“你这一次只看一两个科”(每次算得少)。

稠密 Block(第 18 章) Self-Attention · 共用 1 套 1 个 FFN 每个 token 都过同一套 W₁/W₂ MoE Block(只换 FFN 段) Self-Attention · 仍是 1 套 Router FFN① FFN② N 个并列 FFN,每次只算 top-k 个 换这里

MoE 只替换 Block 里的 FFN 段;Attention 不动。左边「一个 FFN」→ 右边「Router + N 个并列专家」。

② 结构怎么定:并列专家、共享 Attention,Nk 从哪来?

很多人接着会问:专家个数 N 是训出来的吗?FFN 前面要不要也复制 N 套 Attention? 答案很干脆:Nk(每次点亮几个)都是设计模型时写死的超参数,和 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
输入 [n×d] · 每层 token 一行 Masked Self-Attention 共用 1 套 · 全句 token 互相看 LayerNorm MoE 层 · N 个并列专家(示意 N=4, k=2) Router W_gate · d×N FFN① FFN②✓ FFN③ FFN④✓ 加权求和 → 每个 token 一个输出向量 LayerNorm → 输出 [n×d] 残差

一块 MoE Block:Attention 仍共用 1 套;FFN 段换成 Router + N 个并列专家(✓=本 token 被点亮的 top-k)。进出形状仍是 [n×d]

❌ 不是「串行过 N 个专家」 token FFN① FFN② 输出① 再喂给 ② … 深度串联 · MoE 不是这样 ✓ MoE:同一层里并列, Router 选 k 个 token h Router FFN② FFN④ ①③ 同一输入 h · 只算被点亮的 k 条支路 · 再加权合并 对比:堆 N 层 Block = 流水线一站接一站 · MoE = 一站里多个科室并排

左:专家若串行叠在一起,算力随 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 Router 路由器 打分 → 选 top-2 FFN 专家 ① FFN 专家 ② FFN 专家 ③ FFN 专家 ④ 加权求和 按门控值 输出

同一个 token 进来,路由器只点亮 top-2 个专家(紫色实线),其余专家(灰色虚线)这次一次都不跑。

h ① softmax h·W_gate → N 分 ② top-k 只留最高 k 个 ③ 加权 FFN Σ score·FFN(h) out 例: [0.05, 0.41, 0.08, 0.32, …] → 选 ②④ → 0.41·FFN₂ + 0.32·FFN₄

Router 不是只吐「专家编号」:先可微打分,再稀疏选 k 个,最后用门控分加权专家输出——训练时梯度从加权这一步回传。

MoE 路由 · 示意伪代码(帮助理解,非本仓库代码)
// 每个 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
  1. 门控网络就是一个小小的线性层 W_gate,把 token 映射成“对每个专家的偏好分”。
  2. 取 top-k(这里 k=2)——“稀疏”正来自这一步:N 个专家每次只点亮 k 个。
  3. 每个被选中的专家是一个完整 FFN(第 18 章),各自输出再按门控分加权相加。
  4. 关键:算力只花在 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 个输出, 不是硬选一个
❌ 误以为:硬开关(难训) h if 选 ②④ else 不算 离散开关 · 梯度难回 Router ✓ 实际:软权重 × 稀疏计算 h softmax + top-k 0.41·FFN₂ +0.32·FFN₄ 门控分可导 · loss 能改 W_gate 和 FFN 离散的是「谁进 top-k」· 进了之后仍用 softmax 权重参与加权求和

工程实现靠门控加权让 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 ↑ (预测错) MoE 输出 out 专家 ② · W₁/W₂ 本轮有梯度 · 更新 专家 ④ · W₁/W₂ 本轮有梯度 · 更新 W_gate 路由器 调门控分 · 下次分给谁 专家 ①③ 本轮无梯度 + 主 loss: CrossEntropy(下一 token) · 同 MNIST 更新公式

loss 反传:被选中的 k 个专家各收一份梯度;Router 的 W_gate 通过门控权重同时更新;未点亮专家本轮不动。

专家怎么「训成不同」? 并不是事先贴标签「专家 1=语法、专家 2=数学」:

同一批句子里的 token · 路由逐渐「分专科」(示意) token「床」 token「明」 token「3」 专家 ② 反复接到「诗词类」token → 越训越专 专家 ⑦ 反复接到「数字类」token → 另一条路 随机 init 不同 · 稀疏更新 · 路由共进化 · 无人工贴标签 FFN② FFN⑦

不同 token 被 Router 送到不同专家;相似 token 长期走同一路 → 专家涌现分工(示意,真模型里专科未必人类可读)。

也不是「先训专家、再 frozen 训路由」——常见做法是从头联合预训练,路由和专家同步更新。 真正多出来的训练难点在下一节:怎么防止路由只宠少数专家,让冷门专家也收到足够样本。

⑤ 最难的一环:负载均衡

MoE 有个“不训不知道”的坑:路由器会“偏心”。它一旦尝到某几个专家好用的甜头,就老往那儿送 token, 结果热门专家被挤爆、冷门专家饿死(收不到数据就永远练不好)——这叫专家坍缩(routing collapse), 等于白白浪费了大半参数。所以 MoE 训练里,怎么把 token 摊匀比路由本身还关键:

❌ 专家坍缩 · 路由偏心 一批 token 全挤向 专家①② ①② 爆满 · 梯度爆炸式更新 ③④⑤… 饿死 · 几乎不更新 ✓ 负载均衡 · token 摊开 辅助 loss / 容量上限 / 偏置纠偏 各专家都收到足够样本 · 参数不白堆 总 loss = CrossEntropy(主任务) + λ · LoadBalance(摊匀惩罚) 主 loss 训专家和路由 · 均衡项专管「别偏心」

左:路由坍缩 → 大半专家名存实亡。右:负载均衡逼 Router 把 token摊开,各专家都能练到。

MoE 省的是算力,不是显存

一个常见误会:“每次只激活 370 亿,那放得下 370 亿不就行了?”不行。 你事先并不知道每个 token 会用到哪些专家,所以全部 6710 亿参数都得常驻显存待命。 也就是:显存开销 ≈ 总参数,算力开销 ≈ 激活参数。MoE 的本质是“用显存换算力”—— 这也是为什么 MoE 大模型依旧是“显存大户”,只是训练/推理更省算力罢了。

⑥ 为省算力压 k、缩专家:会不会“降智”?

这是很多人问 MoE 时真正纠结的点:宣传的是几百 B 总参数,但推理时每个 token 只点亮少数几个专家—— 如果产品侧为了再省一点算力,把 top-k 从 8 降到 1、把专家做得更小、或可路由的专家池变少, 算不算另一种“降智”?

理论上:会。 但机制和量化不同——不是权重变糙,而是这一步前向实际调用的容量变小了。 每个 token 在 MoE 层真正吃到的算力,大致正比于:

激活算力 ∝ k × 单个专家的 FFN 规模(宽度/深度) k = 路由器点亮的专家个数;和专家总数 N 不是一回事
为了省算力常做的收紧会发生什么谁最先吃亏
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:

n = 1,000,000 → 矩阵元素数 = 106 × 106 = 1012(一万亿个数) 按半精度每个数 2 字节 ≈ 2 TB —— 而这还只是「一个头、一层」的一个中间矩阵

一张顶级 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(旋转位置编码):

拉长手段到底在拉什么一句话直觉
位置插值(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。

① 固定套路:按位置预先规定谁跟谁算

把“局部窗口 + 少数全局”拼起来,就是 Longformer、BigBird 这类长文本模型的经典配方。看图最直观:

稠密(全算)

每个词和它之前所有词都算:n² 量级

滑动窗口

只跟最近几个邻居算:n×w 线性

窗口 + 全局

再留一个“全局列”负责跨远传信息

窗口内 · 要算 全局 token 跳过 · 不算

同一段 6 词序列(行 = 当前词,列 = 被看的词;只画下三角,因为 decoder 只能看前文)。稀疏,就是把大片格子“留白不算”。

② 动态挑选:让模型自己找“该聊的人”

固定套路有个软肋:万一“该关注的词”恰好落在窗口之外呢?于是有了内容自适应的稀疏—— 不按位置死板划定,而是当场挑出最相关的那几个。做法通常是:先用一个廉价的打分器快速估一下 每个历史 token 跟当前 query 有多相关,只留下 top-k 个,再对这 top-k 做精确注意力。

DeepSeek-V3.2 的稀疏注意力(DSA)

它用一个叫“闪电索引器(lightning indexer)”的小模块,为每个 query 飞快选出 top-k 个最该看的历史 token, 只对这 top-k 算注意力。说白了,就是把第 19 章top-k 思想从“挑下一个字”搬到了“挑该关注的 token”上, 让长上下文推理成本大幅下降。

别混:稀疏注意力 ≠ FlashAttention

让注意力更快有两条不同的路,常被搞混:
· 稀疏注意力:少算一些(近似)——用极小的精度损失换大幅提速。
· FlashAttention:一个都不少算(精确),但把计算重新编排、大幅减少显存读写,让同样的稠密注意力跑得更快更省显存。
前者是“少做题”,后者是“做题更利索”,两者还能叠加使用。

两种“稀疏”别搞混

MoE 稀疏的是参数 / 计算路径——每个 token 只走部分专家 FFN(第 8 节)。
稀疏注意力稀疏的是token 两两连接——每个 token 只和部分 token 打分。
一个省在“哪些参数参与”,一个省在“哪些 token 互动”,超大模型里常常两个一起上。

10. 推理服务:几亿用户同时问,怎么扛得住

前面(第 5、6 节)讲的“切多卡、AllReduce”主要是训练的场景——训练是一次性的苦工。 但大模型天天要面对的是另一个战场:成千上万甚至上亿用户同时提问,还都要求几秒内回答。 第 7 节的 KV cache、批处理只是单点技巧,这一节把它们拼成一套在线推理服务,看它到底怎么把海量请求扛下来。

① 先分清:推理其实是“一快一慢”两个阶段

你发一句 prompt、模型回一段话,内部其实分成脾气完全不同的两段:

Prefill 预填充把整段 prompt 一次并行吃进去,建好 KV cache
Decode 解码一个字一个字往外蹦,每步只算新词

Prefill 是“读题”:能像训练那样整段并行算,吃得饱、很快;Decode 是“答题”:天生自回归、只能一步一个字。

矛盾就在这:Decode 阶段单个请求根本喂不饱 GPU。一个用户逐字生成时,那上万个 CUDA 核大半闲着——这正是“怎么扛住海量用户”的突破口。

② 连续批处理:把很多人的“下一个字”拼成一次大计算

既然一个请求喂不饱 GPU,那就把很多请求凑一起算。可普通批处理要求“一批一起开始、一起结束”,而每个用户问题长短不一、生成长度也不同,硬等最慢的那个,GPU 又闲下来了。

于是有了连续批处理(continuous batching):调度器每生成一步就动态调整这一批的成员——谁生成完了(吐出结束 token)就立刻踢出、腾出位置,新到的请求随时插进来一起算。GPU 几乎不空转。

像拼车,不像包车

普通批处理是“包车”:满一车人一起出发、必须等所有人都到终点才算完,先到的白等。 连续批处理是“拼车”:到站的随时下车,路边新乘客随时上车,车座始终坐满。 同一批里,有人还在 prefill“读题”、有人在 decode“逐字答”,调度器把他们编排到一起——吞吐能翻好几倍。

③ KV cache 是这里最难搬的“大件行李”

一批里塞得下多少用户,往往不取决于算力,而取决于显存里能放下多少份 KV cache——每个用户的对话历史都得各留一份,越聊越长。传统做法给每个请求预留一整块连续显存,可对话长度事先并不知道:留多了浪费,留少了不够,显存被切得七零八落(碎片化),明明总量够却塞不下新请求。

PagedAttention:把显存当“操作系统的内存”管

vLLM 提出的 PagedAttention 借用了操作系统虚拟内存分页的老智慧: 把 KV cache 切成一块块固定大小的“页”,用到多少分多少、不必连续。于是碎片几乎消失,同样的显存能多塞很多并发请求。 更妙的是,多个请求共享的前缀(比如同一条超长 system prompt)可以共用同一批页,不必各存一份——这就是前缀复用,既省显存又省掉重复的 prefill。

④ 再不够,就多副本 + 负载均衡

单张(或单组)GPU 的吞吐终有上限。真到“亿级用户”的体量,靠的是最朴素也最有效的一招:把整个模型复制成很多份副本,前面架一个负载均衡器,把请求分发到最空的那个副本——这和普通网站用多台服务器扛流量是同一个道理。

海量请求用户从四面八方来
负载均衡器分发到最空的副本
模型副本 ×N每个副本内部再做连续批处理

两层扛法:副本之间靠负载均衡摊开总流量,每个副本内部靠连续批处理 + 分页 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。Nk 是设计时定死的超参数。难点在负载均衡,防止只宠少数专家。

问题 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……