第 21 章 · 通往大模型
大模型的工程与基础设施
上一章我们讲清了大模型“是什么、怎么训”。但“上千 GPU 训数周”这句话背后, 藏着一整个工程世界。这一章我们钻进机房,用大白话讲清楚:为什么非用 GPU 不可、 显存为什么是真正的瓶颈、一个单卡放不下的模型怎么切开分到成百上千张卡上、 这些卡又怎么高速通信,以及推理时那些让它“快起来”的关键技巧。
上一站(第 20 章)讲了大模型“怎么炼”;本站钻进机房,看它靠什么才跑得起来——GPU / 显存 / 并行 / KV cache / MoE / 长上下文注意力;下一站(第 22 章)讲你作为使用者怎么把它用好。
读完这一章,你会明白
- GPU 凭什么比 CPU 适合深度学习(上万个“小工人”并行);
- 为什么显存是训练大模型最先撞上的墙,以及混合精度/量化怎么省显存;
- 数据并行、张量并行、流水线并行分别把什么“切开”了;
- AllReduce、NCCL、NVLink 这些通信黑话在干嘛;
- 推理提速的关键:KV cache、批处理、量化,以及为什么服务链路一压缩就会造成“降智”体感;
- MoE(混合专家)把什么换成并列专家、
N/k怎么定、Attention 是否共用、路由器和专家怎么一起训、负载均衡,以及压 k/缩专家会不会降智; - 长上下文注意力的三条路线:FlashAttention 分块精算、稀疏注意力少算连接、KDA 把历史压进固定状态;
- 1M 上下文到底怎么做到:位置表示 + 少算或压缩状态,再叠加缓存 / 分块 / 并行 / 长训练的组合拳;并用 Kimi K3 / GLM-5.2 / DeepSeek-V4 看不同路线如何落地;
- 推理服务怎么扛住海量用户:prefill/decode 两阶段、连续批处理、PagedAttention 分页复用 KV cache、多副本负载均衡,以及吞吐与延迟的取舍。
1. 规模带来的三座大山
回忆一下:大模型的训练和推理,归根结底就是海量的矩阵乘法(第 3、17 章), 说白了是天量的加法和乘法。规模一上来,立刻撞上三座大山:
大模型工程的三大挑战。本章逐个击破。
2. GPU:为“并行”而生
一颗 CPU 很聪明,但核心不多——家用的 4 核、16 核,服务器上百核也就到头了。它擅长“一件复杂的事,一步步做好”。 可矩阵乘法不需要“聪明”,它需要的是同时做成千上万次简单的乘加。这正是 GPU(图形处理器)的主场。
一颗 CPU 像几位博士:能解难题,但人少。一块 GPU(如英伟达 H100)有上万个计算核心 (CUDA 核),像一万个小学生:每个只会算简单的加减乘除,但一万道口算题同时开做, 瞬间就完。大模型的矩阵运算恰好能拆成海量互不依赖的小乘加,天生适合这种“人海战术”。
为什么差别这么大?根子在“晶体管预算花在哪”
“一个少而强、一个多而弱”只是现象。真正的原因只有一句话: 一块芯片面积有限,能塞的晶体管数量是固定的;CPU 和 GPU 拿到同样一笔“预算”,却花在了相反的地方。
同样大的一块芯片:CPU 把大片面积让给控制电路和缓存(蓝紫),真正算数的 ALU(金)只占一角;GPU 几乎整块铺满 ALU,控制和缓存被压到极薄。“花钱买聪明”还是“花钱买数量”,一眼可见。
CPU 为什么舍得把大半面积“浪费”在不做计算的电路上?因为它要解决三个 GPU 干脆放弃的难题。这三笔取舍,就是差距的全部来源:
取舍一:为“一条路走到黑”提速,还是为“人多”让路
CPU 面对的典型程序是:if 条件成立走 A,否则走 B,而且下一步算什么,取决于上一步的结果。这种强依赖的串行代码没法靠“人多”加速——你都不知道该往哪走,加再多核也只能干等。
于是 CPU 把晶体管砸在让这一条指令流尽量快上:
- 分支预测:
if结果还没算出来,就先赌一个方向提前开跑,赌对了白赚时间; - 乱序执行:某条指令在等数据,就先把后面不依赖它的指令拎上来先算;
- 大容量缓存:把常用数据留在芯片上,别每次都去慢吞吞的内存取。
这些电路一点计算都不做,纯粹是“调度和加速”的管理层,却吃掉了 CPU 大半面积——这就是“博士很聪明”背后那套昂贵的辅助机构。 GPU 直接把这套全砍了:它赌的是另一种活——成千上万笔计算彼此独立、没有分支、算什么早已定死(矩阵乘法正是如此)。不用猜路、不用调度,那些电路就是浪费,省下的面积全换成 ALU。
取舍二:内存很慢,是“想办法别等”还是“人海盖过去”
访问内存很慢:CPU 算一步只要 1 个时钟周期,去内存取一次数据却要等几百个周期。两家的应对天差地别:
- CPU:想办法别去等——靠大缓存把数据留在身边,靠乱序执行在等待的空档找别的活干。花的还是“辅助电路”的钱。
- GPU:大方承认就是慢,但我人多——一批线程卡在等内存时,调度器瞬间切去算另一批早已就绪的线程。几千个线程轮着上,单个线程的等待被彻底“盖住”。这叫用吞吐掩盖延迟。
所以 GPU 的每个核心其实又慢又笨,单看一个线程比 CPU 慢得多;可几千个一起转,总吞吐反而碾压 CPU。它赢在总量,从不赢在单点。
取舍三:几千个核心“合用一个大脑”(SIMT)
GPU 还有个省面积的狠招:不给每个核心都配一套指挥系统。它把核心分组,一组几十个核心共享同一个控制单元、齐步走同一条指令,只是各自处理不同的数据。这叫 SIMT(单指令多线程)。
- 好处:省下的控制电路面积,又能多塞一大堆 ALU;
- 代价:一旦这组核心里出现分支(有的该走 A、有的该走 B),它们没法各走各的,只能先全体走 A(该走 B 的干等)、再全体走 B,效率直接对半砍。这正是 GPU 最怕
if-else分支的原因,也是它“笨”的代价。
而神经网络的矩阵乘法没有分支,每个数据走的路一模一样,完美适配 SIMT——这不是巧合,是深度学习恰好落在了 GPU 最擅长的那类计算上。
CPU 假设“任务复杂、彼此依赖、充满判断”,所以花钱买聪明;GPU 假设“任务简单、彼此独立、没有判断”,所以花钱买数量。 神经网络恰好是后一种,所以同一件事 GPU 能比 CPU 快几十上百倍——快的不是“单个核”,是“核多、且全用在了刀刃上”。
怎么把一个大矩阵乘法均匀地拆给上万个核、让它们别打架又别闲着?这件苦活由英伟达的 CUDA 平台包办。开发者只管调用,底层调度交给它——这也是英伟达护城河极深的原因之一。 (人们口中的“买卡”,买的就是这种 GPU 卡。)
| 维度 | CPU | GPU |
|---|---|---|
| 核心数量 | 少(几个 ~ 几十个) | 极多(上千 ~ 上万个) |
| 单核能力 | 强:擅长复杂逻辑、分支判断 | 弱:只擅长简单算术 |
| 设计目标 | 低延迟——把单件事做到最快 | 高吞吐——单位时间处理的总量最大 |
| 擅长的活 | 串行、分支多、逻辑复杂的任务 | 大规模并行、同一种运算铺开的任务 |
| 在深度学习里 | 数据预处理、任务调度、跑不并行的零碎逻辑 | 矩阵乘法主力(训练与推理的绝大部分算力) |
一句话:CPU 追求“把一件事做到极快”,GPU 追求“同时做海量简单的事”。神经网络的核心计算就是大量互不依赖的乘加,天生落在 GPU 这一栏。
既然神经网络说到底就是矩阵乘法,有人干脆造了把电路直接做成“矩阵乘法机器”的专用芯片:谷歌的 TPU、手机 / 端侧里的 NPU 都属这类。 它们比通用 GPU 更“偏科”——在“算矩阵”这一件事上更快、更省电,但灵活性不如 GPU。你可以把它们理解成:GPU 是“什么并行活都能接的万能车间”,TPU/NPU 是“只造一种零件、但造得飞快的专用产线”。
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)体积骤减,大多数人几乎听不出差别。 量化同理:用一点点几乎察觉不到的精度损失,换来好几倍的显存和速度收益,让大模型能“瘦身”下沉到消费级设备。
5. 把模型切到多卡:三种并行
既然单卡放不下、也算不快,就得把活儿拆给很多卡。主流有三种拆法,常常混着用。
① 数据并行:人手一份模型,各看一部分数据
最直观的一种。每张卡都放一份完整的模型,但各自只吃一部分训练数据,各自跑前向反向、 算出自己的梯度。问题来了:每张卡只看了局部数据,梯度各不相同,怎么保证大家的模型还是“同一个”?
办法是:每一步之后,把所有卡的梯度求和再取平均,然后广播回每张卡,大家用同一个平均梯度更新。 这个“各自算 → 汇总平均 → 同步回去”的集体动作,就是大名鼎鼎的 AllReduce。
早期做法是设一台中心“参数服务器”收集、平均、再下发梯度,但它很快成为通信瓶颈(所有卡都挤它)。 现代训练改用环形/树形等去中心结构,让每张卡都能拿到全局平均梯度——这类“大家一起求和平均”的通信, 统称 AllReduce。
② 张量并行:一层太大,横切开分给几张卡
如果模型大到单层的权重矩阵一张卡都放不下,就得把这个矩阵本身切块,分给多张卡各算一部分, 再把各卡的结果合并成完整答案。因为权重是用张量表示的,这种切法叫 张量并行(也叫模型并行)。这里“怎么合并”是关键,下面这个类比就专门讲它。
回到第 6 章那件事:一层前向就是一次矩阵乘。 矩阵能竖着切也能横着切,而这两种切法算完之后要做的事完全不同:
- 竖着切(按输出维):每张卡算出的是完整结果的不同几列——各人负责的部分互不重叠,横向拼起来就是答案。
- 横着切(按输入维):每张卡只拿到输入的一部分,算出来的是完整结果的一个部分和——形状虽然对了,数值却都不完整,必须把几张卡的结果逐元素相加才是答案。
真实的 Transformer 层(Megatron 那套做法)是先竖切、再横切配成一对,所以每层最后总要落到“逐元素相加”这一步—— 也就是说张量并行的主通信仍然是 AllReduce,不是单纯的拼接。 这也解释了它为什么最“娇贵”:每一层前向都要 AllReduce 一次(反向再来一次),通信频繁得多,所以张量并行一般只在同一台机器内用下面第 6 节的 NVLink 来做。 (纯拼接式的通信也确实有个名字叫 AllGather,但它主要出现在 ZeRO/FSDP 那类“把参数分片存、用时再收集”的省显存方案里,和这里切矩阵不是一回事。)
③ 流水线并行:不同层放不同卡,像流水线接力
还有一种:把网络的不同层放到不同卡上——第 1–10 层在卡 A,第 11–20 层在卡 B…… 数据像工厂流水线一样,算完 A 的部分传给 B 接着算。这叫流水线并行。 真实的超大模型训练,往往三种并行叠加使用,才能把一个庞然大物铺到上千张卡上同时开动。
6. 卡与卡怎么通信
一旦切到多卡,数据就得在卡之间飞来飞去,通信快慢直接决定训练效率。这里有几层:
- NCCL英伟达的集合通信库。开发者只要调
allreduce()、allgather()这些函数, 底层怎么高效传由它搞定,不用操心。 - NVLink / NVSwitch同一台服务器内,GPU 之间的专用高速公路(带宽远超普通网络), 让 8 张卡像一张大卡一样紧密协作。上面张量并行之所以只敢关在机内,就是因为它每层都要 AllReduce 一次, 只有这条高速公路才喂得起这么频繁的通信。
- 跨服务器成千上万张卡分布在很多台机器上,靠高速网卡 + 交换机(接入层 leaf、汇聚层 spine)连起来—— 这套“分层组网”思想,和普通数据中心网络一脉相承。
训练大模型,本质是“不断调参数,让输出去碰目标”;而挖矿是“不断试,让 hash 去碰目标值”。 两者都是海量并行的“碰撞”游戏——这也是为什么显卡在这两股浪潮里都成了硬通货。
7. 推理提速:让它“回答得快”
训练是一次性的苦工,推理(你每次提问)却要天天做、追求又快又省。 推理提速不是单靠一个魔法开关,而是几类技巧叠起来:少重算、攒一起算、低精度算、先猜再验。
① KV cache:旧上下文的 K/V 不要反复算
KV cache 是生成提速里最基础的一招。自回归生成时(第 19 章), 每多写一个 token,前面那些 token 的 Key、Value 并不会变(第 17 章)。 所以第一步算出的历史 K/V 可以存下来,下一步只算新 token 的 Q/K/V,再拿新 Q 去看缓存里的历史 K/V。
不缓存:
每生成 1 个 token → 重新算整段 prompt + 已生成内容
KV cache:
历史 K/V 存起来
每生成 1 个 token → 只算新 token,再读历史 K/V
它省掉的是重复前向计算,代价是显存里要长期放着每个请求的历史 K/V。 所以后面第 10 节会继续讲:真正服务大量用户时,KV cache 反而会变成最难搬的“大件行李”。
② 批处理:把很多人的“下一步”拼成一次计算
单个用户 decode 时每次只生成一个 token,矩阵很小,GPU 往往吃不饱。 批处理(batching) 的想法就是把很多用户的请求攒在一起: 多个 prompt 可以一起 prefill,多个用户的“下一个 token”也可以凑成一个大 batch 去 decode。 GPU 一次做更大的矩阵乘法,吞吐就上来了。
批处理省的是单位请求摊到的 GPU 空转,但会带来调度权衡: 攒批越大,总体吞吐越高;等批越久,单个用户越容易觉得“首字慢”。 这一点在在线服务里会进一步演化成连续批处理,也放到第 10 节细讲。
③ 量化:用更低精度换速度和显存
推理阶段通常不需要像训练那样保留高精度梯度,所以可以把权重、激活,甚至 KV cache 用更低精度表示。 这就是第 4 节讲过的量化:原来用 FP16/BF16 存的数,可以压到 INT8、FP8、INT4 等格式。 好处很直接:显存少占一点,显存带宽少搬一点,某些硬件上矩阵乘也更快。
但量化不是免费午餐。精度压得太狠,小误差会在层间传递,尤其容易影响代码、数学、多步推理这些对细节敏感的任务。 所以产品里你感到的“降智”,很多时候不是模型忽然变笨, 而是推理服务为了成本和速度启用了更激进的低精度路径。
④ 推测解码:草稿模型先猜,主模型再验
推测解码(speculative decoding) 解决的是 decode 天生串行的问题。 正常生成必须等主模型一步一步吐 token;推测解码则让一个更便宜的草稿器先猜一小串, 再让主模型一次性验证这串草稿。猜对的 token 直接收下,猜错的位置再回到主模型正常生成。
草稿器可以是外部小模型,也可以是模型内部的 MTP 层。关键原则是: 草稿器只负责提案,最终接受什么仍由主模型决定。 因此它的目标不是降低能力,而是用一点额外草稿计算换更少的等待轮数。
⑤ MTP:训练时多预测,推理时少等几步
MTP(Multi-Token Prediction,多 token 预测) 横跨训练和推理两端: 训练时让模型同时预测后面多个 token,推理时则可以把这些预测能力变成一段候选草稿。 它不是让模型“跳过中间 token 直接乱吐一串”,而是把生成拆成两件事:先猜一小段草稿,再让主路径批量验收。
训练时怎么助力? 普通语言模型在位置 t 只预测下一个 token: hidden state ht 经过 LM head 得到 P(xt+1|x≤t),loss 也只盯着 xt+1。MTP 会额外加几个“未来预测”目标,让同一个状态也去预测 xt+2、xt+3……实现上可以是多个辅助 head, 也可以是几层轻量 MTP 模块,但训练信号的本质一样:一个位置不只问“下一步是什么”,还问“后面几步大概要往哪走”。
这会带来三层训练收益:第一,监督更密,每个位置能贡献多份未来 token 的训练信号; 第二,表示更前瞻,因为 ht 不能只服务于眼前一个 token,还要对后续几步有一点规划; 第三,MTP head / MTP 模块本身学会了“便宜地猜一小段”,这正好可以拿到推理阶段当草稿器。 它对代码、数学推导、格式化输出这类“后面几步结构比较强”的任务尤其有价值。
推理时怎么加速? 自回归生成慢,是因为正常路径必须一格一格走:
先生成 xt+1,它变成上下文后,才能再生成 xt+2。
MTP 的草稿器先给出一串候选,比如 y1 y2 y3 y4;主模型不盲信它,而是把这串候选接到上下文后,
用一次带 causal mask 的前向计算同时算出多个位置的主模型分布。
如果当前上下文是 C,草稿器提出 y1,y2,y3,y4, 主模型一次验证时看的就是下面这些分布:
主模型怎么“验证”? 直觉版可以理解成:从左到右检查草稿 token 是否符合主模型在对应位置的分布。 贪心解码时,可以近似理解为“草稿 token 是否等于主模型此处最想选的 token”;采样解码时,实际系统会用概率接受 / 拒绝规则, 尽量保持主模型原本的采样分布不被草稿器改变。连续通过验证的 token 会被一次性接受,并且这次验证前向已经顺手建好了它们的 KV cache; 一旦某个位置没通过,后面的草稿就丢掉,从主模型在这个位置给出的分布继续生成。
所以 MTP 的原则是:草稿器只负责提案,主模型负责裁决。 它加速的是 decode 轮数,不是降低主模型标准。若平均每次能接受 3 个 token,原来 3 轮 decode 的进度可能 1 轮就推进完; 若草稿经常猜错,加速就会变小,甚至被额外草稿计算抵消。越是确定性强、格式稳定的输出,越容易吃到这类收益。
MTP 的代价也很实在。 它不是白送的加速,主要有三类成本:
- 训练更贵:多出来的 MTP head / MTP 模块要参与前向和反向,loss 里还要多算几个未来 token 的交叉熵;预测越远,目标越不确定,辅助 loss 权重 λ 还得调,否则可能干扰主 next-token 目标。
- 推理不一定稳赚:草稿器先猜也要算力,主模型验证草稿也要算力。只有“平均接受长度”足够大时,省下的 decode 轮数才抵得过这些额外计算;如果开放式创作、随机采样很强、草稿经常错,收益会明显下降。
- 服务实现更复杂:系统要同时管理草稿 token、验证位置、被接受 token 的 KV cache、被拒绝后的回退;采样模式下还要用更严谨的概率接受规则,不能为了速度破坏主模型原本的输出分布。
MiMo-V2-Flash 这类新模型给了一个很好的工程样板:总参数很多,但 MoE 每步只激活一小部分专家; 注意力层不是层层全局看,而是滑动窗口 + 间隔全局注意力;训练时加 MTP(多 token 预测), 推理时又把 MTP 层当草稿模型做推测解码。结果是:模型并不只是“缩小”,而是把少激活、少看、少等三件事叠在一起。
这几招是“单点提速”。把它们组织成一套能同时服务海量用户的在线系统,还需要连续批处理、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 一样,走同一条反传链,并不是不可训的黑箱。
这个结论后面还会反复出现:挑选本身可以是离散的,但挑选前的打分器是可微的,挑选后的加权计算也能从主 loss 回传梯度。 MoE 是“先给专家打分,再挑 FFN”;稀疏注意力是“先给历史 token / 压缩块打分,再挑该看的位置”。 真正麻烦的是没被选中的分支本轮通常拿不到直接梯度,所以系统还要靠负载均衡、预训练、蒸馏或逐步切换等办法,避免一开始就只宠少数路径。
某个 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 上的总参数量。
⑦ 稠密模型 vs 稀疏模型:同样叫“大模型”,两种活法
讲完 MoE,正好回答一个常被问到的分类:大模型按“每个 token 是不是都要过全部参数”分成两大类—— 稠密模型(Dense)和稀疏模型(Sparse,通常就是 MoE)。 它们的差别只在前馈那一段:稠密模型每个 token 都老老实实过那一个 FFN; 稀疏模型把 FFN 换成 N 个并列专家,每个 token 只走 Router 点亮的 k 个。
| 稠密模型 Dense | 稀疏模型 MoE(Sparse) | |
|---|---|---|
| FFN 段 | 每层一个 FFN,所有 token 共用 | 每层 N 个并列专家 + 路由器 |
| 每个 token 用到多少参数 | 全部参数都参与计算 | 只激活 k 个专家(总参数的一小撮) |
| 总参数 vs 激活参数 | 几乎相等(总参 ≈ 激活参) | 总参数 ≫ 激活参数(如 671B 总 / 37B 激活) |
| 算力开销 | 随总参数线性涨,想更强就得全体变大 | 只跟 激活的 k 走,堆专家不显著加算力 |
| 显存开销 | ≈ 总参数 | 依然 ≈ 总参数(全部专家都得常驻待命) |
| 典型代表 | GPT-2/3、LLaMA、Qwen Dense、本项目的 Mini Transformer | Mixtral、DeepSeek-V3、多数新一代旗舰 |
| 一句话 | 结构简单、训练稳、好部署;想更强只能整体变大 | 用显存换算力:参数堆很大、每步算得少,但要处理路由和负载均衡 |
核心一句:稠密 = 每个 token 过全部参数;稀疏 = 每个 token 只过被选中的一部分。稀疏靠“总参数大、激活参数小”兼顾容量与算力,代价是多了路由/均衡这套机关,而且显存并不省。
本章出现了两处“稀疏”,针对的不是同一段:
· 稀疏模型 / MoE(本节):稀疏的是 FFN 那一段——N 个专家里每次只激活 k 个。
· 稀疏注意力(下一节):稀疏的是 Attention 那一段——不再让每个 token 和所有 token 两两打分,只保留一部分连接。
两者可以同时用在一个模型里:一个模型既是 MoE(FFN 稀疏),又用稀疏注意力(Attention 稀疏),互不冲突。
9. 长上下文注意力:少算连接,还是压缩历史?
第 17 章的注意力是“稠密”的:每个 token 都要和其他所有 token 两两打分。 长度为 n,计算量就是 n×n——长度翻倍,计算和显存翻四倍。 n=1000 时是百万对还好,可长上下文动辄 n=十万,那就是百亿量级的两两打分 (还得每层、每个头都来一遍),这正是第 20 章“长上下文难”的根子。
先算笔账:这个矩阵到底有多大?
很多人第一次意识到问题的严重,是在脑子里把 n=1M 代进去的那一刻。注意力打分矩阵 Q·KT 的形状是 n×n:
一张顶级 GPU 的显存也就一两百 GB(H100 是 80 GB、H200 是 141 GB、B200 是 192 GB),而这一个矩阵就要约 2 TB,再乘上几十层、几十个头,更是天文数字。 结论很硬:n=1M 的稠密注意力矩阵,根本不可能被整个存下来、更别说一次算出来。 这就是长上下文一切麻烦的总根源。
这里最容易误解的一点是:虽然它在数学上“存在”,但真实系统从不把完整的 n×n 矩阵物化(materialize)到显存里。 对付它有三条不同的路:
· 不一次算、也不整个存(分块 + FlashAttention):
把序列切块,注意力一次只出现一小条,算完 softmax 就丢,只留最终输出。FlashAttention 就是这么干的——
它一个都不少算(精确),但全程不写出完整大矩阵,所以省下海量显存。
· 干脆不全算(稀疏注意力):那一万亿个格子里绝大多数其实不相关,只保留窗口 / 全局 / top-k 的连接,
把 n² 降到接近线性——这是少算连接。
· 不再保存逐 token 历史(KDA 等递归注意力):把读过的内容不断压进固定大小的状态,新 token 直接读写这块“记忆板”,
从计算图上绕开 n×n——这是改变注意力形式,本节也会细讲。
所以要分清两件事:显存可以靠“不物化”省掉;但标准稠密 softmax attention 的计算量本身仍是 O(n²)。 想真正降复杂度,可以保留 softmax 但稀疏掉连接,也可以像 KDA 那样换成固定状态的递归计算。
稀疏注意力(sparse attention)的想法很朴素:没必要人人都聊。 大多数 token 只跟少数几个真正相关的 token 有关系,那就只算这部分连接,其余干脆不算。 至于“算哪些”,又分固定连接和动态挑选两大流派。下面先把 1M 上下文的整套约束串起来、讲 KDA 这条固定状态路线, 再回来展开这两类稀疏连接。
先拨开一个误会:1M token 不是把普通 attention 硬拉到 1,000,000
真要做百万级上下文,通常靠的是一整套组合拳,不是只把窗口数字改大。 算法上要少算或换表示(滑动窗口、动态 top-k,或像 KDA 那样把历史压进固定状态,总之不再让所有 token 两两全聊); 系统上要更会存和搬(长前缀的 KV cache 得分页管理、压缩、复用,不然光缓存就先把显存吃爆); 推理上往往得分块 prefill,再把计算摊到多卡上一起扛;训练时也要专门补长上下文,让位置编码和模型能力都真的撑到更远。
所以今天很多“1M context”更准确的含义是:系统能处理百万级输入,但背后依赖的是 “少算或压缩状态 + 缓存 + 分块 + 并行 + 长上下文训练”这一整套工程,而不是朴素的全量稠密注意力。
还有一环:模型怎么知道第 100 万个 token 的位置?
上面都在说“怎么少算、怎么存得下”,但有个更底层的问题:注意力本身对位置无感, 必须有机制告诉它谁在前谁在后。对标准 softmax attention,今天主流靠的是 RoPE(旋转位置编码), 要支持 1M,就得让旋转角度一路编到第 100 万位还不乱:
- RoPE 为什么天生能外推一点:它不给每个位置发“绝对号码牌”(那种一旦超出训练长度就没牌可发),而是按位置把 Q/K 向量旋转一个角度,角度和位置成正比。注意力打分只依赖两个 token 的相对角度差——换个绝对位置,只要相对距离没变,规律仍成立,所以它比可学习绝对位置更容易“往后多编一点”。
- 但硬外推会崩:训练时只见过 4K 以内的旋转角度,直接拿去编到 128K,那些转得最快的维度会转出“没见过的角度”,注意力就乱套。于是需要下面这些“拉伸”手段,再配少量长文本微调。
| 拉长手段 | 到底在拉什么 | 一句话直觉 |
|---|---|---|
| 位置插值(PI) | 把要处理的位置整体等比压缩回训练见过的范围(如把 0–32K 缩放到 0–4K 再喂进去) | “把尺子刻度整体缩小”,位置再远也落回熟悉区间 |
| NTK-aware | 不均匀地缩放 RoPE 各频率:高频少动、低频多拉,兼顾近处精度和远处覆盖 | “该细的地方保持细,该拉远的地方才拉” |
| YaRN | 在 NTK 思路上进一步分频段调整,是目前把上下文拉到几十上百 K 的常用配方 | NTK 的加强版,少量微调即可撑很远 |
这几招都在 RoPE 的“旋转角度”上做文章:让训练时只见过短序列的模型,推理时也能把位置编到远处而不乱——这就是模型能“事后加长”的关键(第 20 章提过它们的名字)。
Kimi K3 的 KDA 不给 Q/K 额外加 RoPE。它按 token 1 → token 2 → ... 的顺序不断更新循环状态,
先读 A 再读 B 与先读 B 再读 A 会得到不同状态,顺序因此已经隐含在递归过程里。K3 周期性插入的 MLA 层也采用 NoPE。
这不表示“位置编码没用”,而是位置信息换了一种进入计算图的方式。
把“1M 怎么做到”一次串起来
综合前面所有内容,一个百万级上下文的模型,其实是这五层同时做到位的结果——缺哪一层都撑不到 1M:
| 层面 | 要解决的问题 | 典型手段 |
|---|---|---|
| ① 位置编码 | 位置能不能“编”到第 100 万位还不乱 | 标准 attention 用 RoPE + PI/NTK/YaRN;KDA 可用递归状态隐式携带顺序 |
| ② 少算(算法) | 稠密注意力 O(n²),1M 时是天文数字 | 滑动窗口 / 动态 top-k 少算连接,或 KDA 把历史压进固定状态 |
| ③ 存得下(系统) | 百万长前缀的 KV cache 会先把显存吃爆 | 量化、PagedAttention 分页复用;KDA 状态固定大小 |
| ④ 算得动(工程) | 一次性吃 1M 输入,单卡扛不住 | 分块 prefill、多卡并行(第 5、6、10 节) |
| ⑤ 真学过(训练) | 没在长序列上训过,远处信息用不起来 | 长上下文续训 + 上面的位置拉伸再微调(第 20 章) |
所以“1M 上下文”是一套组合拳:顺序表示得了 + 少算或压缩状态 + 存得下 + 算得动 + 真训过,而不是把 full attention 的窗口数字改成 1000000。
KDA:把历史压进一块固定大小的“记忆板”
稀疏注意力仍然保留“当前 token 去找一部分历史 token”的框架,只是少找一些。 KDA(Kimi Delta Attention)走得更远:不再把所有历史 token 的 K/V 原样留给这一层查, 而是边读边把历史写进一块固定大小的状态矩阵 S。新 token 到来时先修改 S,再用自己的 Query 从中读取结果。 状态大小取决于 head 维度,不随已经读过的 token 数增长。
标准 Attention 像保留全部原始记录再检索;KDA 像维护一块不断修订的记忆板。这里的“压缩历史”是帮助理解的类比,状态里存的是学习出来的矩阵表示,不是一段可读文字摘要。
Delta Rule:不是把新内容硬塞进去,而是“发现错多少就改多少”
对当前 token,KDA 先得到 Query qt、Key kt、Value vt, 还会算出每个记忆通道的保留率 αt 和写入强度 βt。把原论文公式拆成更容易读的四步:
v̂t = S̃tTkt
et = vt − v̂t, St = S̃t + βtktetT
ot = StTqt 先选择性遗忘 → 用 key 问记忆当前会答什么 → 只写入“新 value − 旧答案”的差 → 再用 query 读取
- α 管“记多久”:每个 key 通道都有自己的保留率,有的快速更新,有的长期保留;
- β 管“这次改多狠”:差值很重要时可以多写,不确定时少动;
- Delta 是“纠错”:若状态已经能用 kt 读出接近 vt 的答案,et 就很小,无需重复写一遍。
这些遗忘和写入规则,也是训练出来的
KDA 并没有人工规定“遇到人名记久一点、遇到标点马上忘”。产生 q/k/v/α/β 的投影参数 都是从初始化值开始学习;预测下一个 token 的 loss 会穿过读取公式和每一步状态更新反向传播,教它什么该保留、什么该覆盖、用什么 key 写、用什么 query 读。 学习机制仍是本书反复讲的“前向 → loss → 反向传播 → 优化器更新”,变化的是前向计算图。
递归公式看起来必须串行。实际训练会把长序列切成 chunk:chunk 内改写成矩阵乘法并行算,chunk 之间只传状态。 Kimi K3 还给单步衰减设下界,把累计缩放限制在 BF16 能表示的范围内,让原本难算的块也能走 Tensor Core 密集矩阵乘法。 因此它既保留递归记忆,又能适配 GPU 的并行训练方式 技术报告。
为什么还要周期性插入全局 MLA?
固定状态的容量不会随历史增长,效率高,但也意味着许多 token 的细节要挤进同一块记忆,可能互相覆盖。 标准全局 attention 则能直接回看某个原始位置,精确但昂贵。Kimi K3 没有二选一,而是采用混合结构:
KDA 负责用固定状态低成本传递长历史,MLA 定期恢复直接的全局 token-to-token 交互。可以类比为“平时维护动态笔记,隔几层重新翻一次原始资料”。
| 机制 | 历史怎么留 | 优势 | 代价 |
|---|---|---|---|
| KDA | 压进固定大小状态 S | 随序列近线性计算,状态不随长度增长 | 固定容量可能覆盖精确细节 |
| Gated MLA | 保存压缩后的逐 token KV | 能直接做全局 token-to-token 交互 | 计算更贵,KV cache 仍随长度增长 |
KDA 不是“完全等价但更便宜的 softmax attention”。Kimi K3 的关键取舍正是让两者互补,而不是押注单一路线。
稀疏路线① 固定套路:按位置预先规定谁跟谁算
- 滑动窗口(局部注意力):每个 token 只跟最近的 w 个邻居算(如某些模型 w=4096)。计算量从 n² 降到 n×w(线性)。别担心看不到远处:多层叠起来,感受野会像 CNN 一样层层扩大,远处信息仍能“一跳一跳”传过来。
- 全局 token:留几个“公共联络员”token,它们对所有人可见、也能看所有人,专门负责跨全局传信息(比如句首的特殊标记)。
- 跳跃 / 膨胀:每隔几个 token 才看一个,用很小的代价覆盖很远的距离。
把“局部窗口 + 少数全局”拼起来,就是 Longformer、BigBird 这类长文本模型的经典配方。看图最直观:
稠密(全算)
每个词和它之前所有词都算:n² 量级
滑动窗口
只跟最近几个邻居算:n×w 线性
窗口 + 全局
再留一个“全局列”负责跨远传信息
同一段 6 词序列(行 = 当前词,列 = 被看的词;只画下三角,因为 decoder 只能看前文)。稀疏,就是把大片格子“留白不算”。
稀疏路线② 动态挑选:让模型自己找“该聊的人”
固定套路有个软肋:万一“该关注的词”恰好落在窗口之外呢?于是有了内容自适应的稀疏—— 不按位置死板划定,而是当场挑出最相关的那几个。做法通常是:先用一个廉价的打分器快速估一下 每个历史 token 跟当前 query 有多相关,只留下 top-k 个,再对这 top-k 做精确注意力。
DeepSeek-V3.2 的稀疏注意力(DSA)就是这个思路:它用一个叫“闪电索引器(lightning indexer)”的小模块, 为每个 query 飞快选出 top-k 个最该看的历史 token,只对这 top-k 算注意力。说白了,就是把第 19 章的 top-k 思想从“挑下一个字”搬到了“挑该关注的 token”上。
这里的“动态”先别理解成“每个 query 自己临时决定 k 多大”。工程上常见的是先给模型或服务档位定一个预算, 比如长上下文档位用更大的 k;真正动态的是这次到底选中哪些历史位置。换句话说, k 多半是配置,被选进来的 token / 压缩块才是每个 query 现场决定的。
稀疏路线③ 闪电索引器:top-k 到底怎么选出来?
不是人工写规则,也不是先把完整注意力算完再裁剪。论文里的做法是:索引器给“当前 query token”和每个“历史 token” 算一个很便宜的索引分 I(t,s),再按这个分数排序取前 k 个。
这里 qIt,j 是当前 token 投影出的“小 query”,
kIs 是历史 token 投影出的“小 key”,
wIt,j 是当前 token 给各个索引头的权重。ReLU 只保留正相关的点积分数;
多个索引头加权求和后,就得到“这个历史 token 值不值得看”的粗评分。
取 top-k 这一步本身像“排序截断”,确实不是光滑可微的。但索引器前面的打分函数是可微的: 训练时可以先在完整注意力还开着的时候学习模仿原本的注意力分布,完整注意力本来会重看的位置,索引器就要给高分; 进了 top-k 的位置之后仍会进入真正的 softmax attention 和最终 loss,梯度也会回到产生这些分数的参数。 没进 top-k 的位置这一步拿不到直接梯度,所以这类系统通常还要靠预训练、蒸馏、逐步切换到稀疏路径等办法, 避免索引器一开始就学歪。推理时才用它先粗筛,把非 top-k 位置遮掉,再让真正的 Q/K/V 注意力只在留下来的 token 上精算。
稀疏路线④ 百万上下文怎么继续省?
先别记名词。把 1M 上下文想成一本一百万页的书:模型每生成一个新 token,都要决定“该回头看哪些页”。 真正的优化,不是假装能把整本书逐页重读,而是想办法少翻页、复用目录、把远处内容压成摘要。
GLM-5.2 的 IndexShare 省的是“反复找该看谁”的成本:相邻几层通常关注的历史 token 差不多,没必要每层都重新算一遍索引。
DeepSeek-V4 的思路是“分层看资料”:近处别压太狠,远处不必原样保存,中间先压缩再挑重点。
GLM-5.2 的 IndexShare 很像几层模型共用一份“目录”:第一层先找出最相关的 top-k 历史 token, 后面几层先复用这份结果,不用每层都重新找。文章给的数字是:在 1M context 下,这能把 per-token FLOPs 降低约 2.9×来源。
DeepSeek-V4 的 CSA/HCA 更像给超长历史做“摘要”: CSA 把每 m 个 token 的 KV cache 压成一个条目,再在压缩后的历史上挑重点; HCA 把更远的历史压得更狠,保留全局远景;滑动窗口注意力则负责近处细节。 论文给出的 1M context 设置下,DeepSeek-V4-Pro 相比 DeepSeek-V3.2 只需要约 27% 的单 token 推理 FLOPs 和 10% 的 KV cache来源。
所以 V4 不是“从 1M 个原始 token 里直接挑几千个”这么粗暴。更准确地说,它先把历史变成不同粒度的资料: 近处原样保留,中距离压缩成小摘要后再 top-k,远处压得更狠只留全局轮廓。这样一来,top-k 选的是压缩后的历史块, 而不是整条 1M 序列里的每个原始 token。
这两条路线说到底都在回答同一个问题: 面对一百万个 token,模型不能每层都重新翻完整资料,也不能把所有历史原样存在显存里。 它必须学会建目录、做摘要、复用缓存,再配合真正的长上下文训练。
最后把三种容易混淆的省法分清:MoE 稀疏的是参数 / 计算路径——每个 token 只走部分专家 FFN(第 8 节); 稀疏注意力省的是token 两两连接——只和部分历史位置打分; KDA则不是 top-k 稀疏,而是把逐 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。
不是因为 attention “吐出来的 token”变了,而是每生成一个新 token,背后要维护、读取、搬运的历史信息变多了。 即使用稀疏 / 压缩注意力,服务侧也可能按上下文档位给更大的历史预算。下面只是一个简化示意,不是某家厂商的公开公式:
context < 32K → k = 512
context < 128K → k = 2048
context >= 1M → k = 4096
上下文越长,prefill 要读的 prompt 越长,decode 每步要访问的 KV cache / 压缩历史 / 索引结果也越多; 同时超长请求更占显存、更难批处理。分段收费本质上是在给这些额外计算、显存占用和调度难度买单。
④ 再不够,就多副本 + 负载均衡
单张(或单组)GPU 的吞吐终有上限。真到“亿级用户”的体量,靠的是最朴素也最有效的一招:把整个模型复制成很多份副本,前面架一个负载均衡器,把请求分发到最空的那个副本——这和普通网站用多台服务器扛流量是同一个道理。
两层扛法:副本之间靠负载均衡摊开总流量,每个副本内部靠连续批处理 + 分页 KV cache 榨干 GPU。
⑤ 一对甩不掉的矛盾:吞吐 vs 延迟
最后点破一个贯穿始终的权衡。批处理把很多请求攒一起算,吞吐(每秒服务多少人)高了;但攒批、排队本身会让每个人的延迟(尤其是“等第一个字”的时间)变长。
- 想省钱、服务更多人 → 批攒大一点,吞吐优先(适合离线批量任务);
- 想聊天跟手、首字快 → 批小一点、优先响应,延迟优先(适合在线对话)。
真实系统会盯着两个指标调:TTFT(Time To First Token,多久蹦出第一个字,主要受 prefill 影响)和 TPOT(Time Per Output Token,之后每个字的间隔,主要受 decode 批大小影响)。你感觉某个模型“想一下才开口、然后飞快”或“立刻开口、但吐字慢”,背后就是这套调度在做取舍。
⑥ 为什么会觉得模型降智了?
网上说的“降智”,很多发生在推理服务环节,不是预训练把参数改坏了。 真正要抓住的是:一次模型调用链路被压缩了。真实产品里,你点到的往往只是一个模型名;服务端还有一层 serving policy(推理服务策略),会根据请求复杂度、套餐、负载、延迟目标和安全规则, 决定这次到底给模型多少上下文、多少推理预算、多少工具能力,是否走低比特量化 / 蒸馏小模型 / fallback 后端,以及安全壳要收多紧。
用户请求
↓
Router / Policy Engine
↓
模型变体 + 推理预算 + 上下文策略 + 工具策略 + 批处理/超时/安全策略
↓
真正执行
UI 里看到的仍然可能是“老模型 A”,但后面这几处一变,用户就会觉得它“变笨”:
| 降智原因 | 服务端怎么省 | 为什么像变笨 |
|---|---|---|
| 路由到便宜后端 | 同名模型走 fast / quantized / distilled / fallback backend | 简单问答差别小,复杂任务突然不稳 |
| 权重或 KV 低精度 | INT8/INT4 权重量化,或 FP8 KV cache 降显存 | 多数问题还行,难题、长尾知识和细腻推理先掉 |
| 推理预算变短 | 降低 reasoning effort、内部步数、输出长度、自检和重试次数 | 代码漏边界、数学少推几步、回答更短更浅 |
| 上下文给少了 | 截断历史、提前摘要、检索 top-k 变小、工具轨迹压缩 | 不是不会,而是没看到关键材料 |
| 工具链被剪短 | 少读文件、少跑代码、少 verifier / reviewer、少子 Agent | Agent 少试错、少验证,表现像“懒了” |
| 吞吐优先与超时 | 更激进 batching、排队、early stop、超时 fallback | 首字慢、输出短、中途草草收尾 |
| 安全壳更保守 | 输入/输出 classifier、policy rewrite、拒答模板更强,也就是对齐税变高 | 更少细节、更爱免责声明,像“不敢答” |
| MoE 激活过少 | 压 top-k 或缩专家规模,每步实际激活的子网络变小 | 总参数看着很大,但当次动用的能力变少 |
所以“降智”的真实原因,往往不是单独某项优化技术有问题,而是几项策略叠在一起: 少看上下文 + 少想几步 + 少用工具 + 更快超时 + 更保守安全壳。MoE 少开专家当然可能掉能力, 但它通常不是线上最先怀疑的开关;更常见、更好控的是 router、预算、上下文、工具链和调度策略。
小结
- 大模型工程的三座大山:算力、显存、通信。
- GPU 靠上万个 CUDA 核并行,天然适合矩阵乘法;CUDA 平台负责调度。
- 显存是第一堵墙(要放参数+梯度+优化器状态+激活);混合精度、量化能大幅省显存。
- 很多“降智”体感来自推理调用链路被压缩:serving policy 可能把请求路由到便宜后端,并收紧推理预算、上下文、工具链、超时和安全策略。
- 三种并行:数据并行(人手一份模型,AllReduce 同步梯度)、张量并行(切开单层矩阵,各卡算出部分和后 AllReduce 相加,通信最频繁、通常只在机内做)、流水线并行(不同层放不同卡)。
- NCCL 管通信,NVLink/NVSwitch 是卡间高速公路;推理靠 KV cache、批处理、量化提速。
- MoE:FFN 换成 N 个并列专家(
N/k设计时定死),Attention 共用 1 套;路由器 softmax 打分 + top-k 门控加权,与专家端到端同训;还得靠负载均衡防专家坍缩。参数多但算得少;全部专家仍要常驻显存。压 k/缩专家会掉能力,是另一类「降智」来源。 - 离散 top-k 能训练的关键:不可微的是“谁入围”,可微的是入围前的打分器和入围后的加权路径;没入围的专家或 token 本轮通常没有直接梯度。
- 长上下文注意力有不同路线:FlashAttention 分块流式精算、不写出完整矩阵;稀疏注意力只算部分 token 对;KDA 把历史压进固定状态,用递归更新替代逐 token 全局检索。
- KDA 用通道保留率 α 选择性遗忘,再按 v−STk 的 Delta 误差纠正状态;这些读写规则由 next-token loss 端到端训练。Kimi K3 用 69 层 KDA + 24 层 Gated MLA,让低成本长记忆与精确全局交互互补。
- n×n 注意力矩阵在 n=1M 时高达一万亿个数(单头单层就 ~2TB),但它从不被整个物化:可以分块精算、稀疏少算,也可以改成 KDA 这类固定状态架构。
- 动态 top-k 的“动态”通常是选中哪些历史位置,不是每个 query 自己决定 k 多大;k 多半是模型或服务档位里的预算。
- 百万级长上下文是五层组合拳:顺序表示得到远处、注意力少算连接或压缩状态、缓存存得下、分块 prefill+多卡算得动、长上下文续训真学过——不是把 full attention 的窗口数字改成 1M。
- MTP 训练时让同一隐藏状态预测多个未来 token,提供更密训练信号并逼表示更前瞻;推理时当内置草稿器先猜一串,主模型批量验证,连续通过的 token 一次接受。代价是训练/验证额外计算、接受率不稳和服务实现复杂度。
- 快模型也是组合拳:MoE 少激活专家、混合滑窗/全局注意力少看 token、MTP 既辅助训练又能做推测解码,一起把成本压下来。
- 推理服务扛海量用户:分 prefill(并行、算得饱)/ decode(逐字、喂不饱)两阶段;用连续批处理把多请求拼一起、PagedAttention 分页复用 KV cache、多副本 + 负载均衡横向扩展;长上下文更占计算、显存和调度资源,所以常会按上下文档位收费。
动手与思考
问题 1:为什么深度学习用 GPU 而不是 CPU?显存为什么又会先撞墙?
因为训练/推理的核心是海量互不依赖的简单乘加(矩阵运算)。CPU 核心少但强,擅长复杂串行任务;GPU 有上万个简单核心,能把这些乘加同时并行做完,吞吐高得多。但模型一大,参数、梯度、优化器状态和激活值都要进显存,容量很快不够,所以显存常常比算力更早成为第一堵墙。
问题 2:混合精度和量化都在省显存,区别是什么?
混合精度主要发生在训练中:大部分数值用 FP16/BF16 存和算,少数敏感位置用 FP32 兜底。量化主要发生在推理中:把权重、激活甚至 KV cache 压到 INT8、FP8、INT4 等更低精度。前者是训练时的折中,后者更像把模型部署得更小、更便宜,压得太狠就可能影响细节任务。
问题 3:数据并行、张量并行、流水线并行分别切开的是什么?
数据并行切的是数据:每卡放完整模型、各吃一部分样本,再用 AllReduce 同步梯度。张量并行切的是单层大矩阵:一层太大,就把权重矩阵分块放到多卡,各卡算出的往往只是完整结果的部分和,所以要用 AllReduce 逐元素相加才能合并。流水线并行切的是层:前几层放一组卡、后几层放另一组卡,像流水线一样接力。
问题 4:KV cache 和连续批处理分别解决推理里的什么浪费?
KV cache 解决的是重复计算:自回归生成时,历史 token 的 Key/Value 不变,缓存起来后每步只算新 token。连续批处理解决的是GPU 空转:单个用户 decode 一次只算一个 token,喂不饱 GPU;把很多用户的“下一个字”动态拼成一批,吞吐就上来了。
问题 5:MoE“总参数很大、每次只激活一小部分”到底是什么意思?
MoE 把每层 FFN 换成很多并列专家。每个 token 只被路由到少数几个专家计算,其余专家本步不参与,所以总参数可以很大,但激活参数较小。它省的是计算量,不是显存:因为事先不知道 token 会找哪些专家,全部专家仍要常驻显存待命。
问题 6:MoE 的路由器怎么训练?Attention 也要复制 N 份吗?
路由器先 softmax 得到门控分,再 top-k 选出专家,用门控分加权被选中的 FFN 输出。top-k 的“谁入围”是离散的,但入围前的打分器可微,入围后的加权路径也能从主 loss 回传梯度,所以路由器和被选中的专家能端到端训练。Attention 通常仍是共用 1 套,不会随专家复制 N 份;并列的是 FFN。
问题 7:MTP 对训练和推理分别有什么帮助?它是不是一步直接吐多个 token?
不是无条件“一步吐多个 token”。训练时,MTP 让同一个 hidden state 预测多个未来 token,监督更密,也逼表示携带后面几步的规划信息。推理时,MTP 只当草稿器,先猜一串候选;主模型再批量验证。连续通过的 token 可以一次接受,不通过就回到主模型正常生成。
问题 8:一个模型号称支持 1M 上下文,只靠把 RoPE 编到 100 万就够了吗?KDA 路线又有何不同?
不够。标准 attention 路线还要靠 PI/NTK/YaRN 等方法拉伸 RoPE、用稀疏连接少算或用 FlashAttention 分块精算、管理 KV cache、分块多卡 prefill,并真的做长序列训练。KDA 则把顺序隐含在递归状态更新里,把历史压进固定状态,不依赖 RoPE 和不断增长的逐 token KDA cache;但固定状态可能丢精确细节,所以 Kimi K3 又周期性插入全局 MLA。两条路线都还需要系统并行和长上下文训练。
问题 9:为什么同一个模型名下,用户有时会觉得模型“降智”?
因为前端显示的是模型名,后端执行的是一条服务链路。serving policy 可能把请求路由到更便宜的 backend,收紧 reasoning budget、上下文长度、工具调用、自检重试、输出长度和超时策略,再叠加量化、小模型 fallback 或更保守的安全壳。模型不一定是权重坏了,而是这次调用少看、少想、少用工具、结束更早。
硬件和并行搞定,模型终于能跑了。可是对绝大多数人来说,我们既不训练也不部署,而是直接使用别人的大模型。 最后一章,我们讲讲怎么把它用好:提示词、RAG、工具调用、Agent、MCP……