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

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

为什么差别这么大?根子在“晶体管预算花在哪”

“一个少而强、一个多而弱”只是现象。真正的原因只有一句话: 一块芯片面积有限,能塞的晶体管数量是固定的;CPU 和 GPU 拿到同样一笔“预算”,却花在了相反的地方。

CPU:少数几个强核 ALU ALU ALU ALU 控制:分支预测 / 乱序执行 大容量缓存 Cache GPU:几千个弱核 控制 + 缓存(压到很薄)

同样大的一块芯片:CPU 把大片面积让给控制电路和缓存(蓝紫),真正算数的 ALU(金)只占一角;GPU 几乎整块铺满 ALU,控制和缓存被压到极薄。“花钱买聪明”还是“花钱买数量”,一眼可见。

CPU 为什么舍得把大半面积“浪费”在不做计算的电路上?因为它要解决三个 GPU 干脆放弃的难题。这三笔取舍,就是差距的全部来源:

取舍一:为“一条路走到黑”提速,还是为“人多”让路

CPU 面对的典型程序是:if 条件成立走 A,否则走 B,而且下一步算什么,取决于上一步的结果。这种强依赖的串行代码没法靠“人多”加速——你都不知道该往哪走,加再多核也只能干等。 于是 CPU 把晶体管砸在让这一条指令流尽量快上:

这些电路一点计算都不做,纯粹是“调度和加速”的管理层,却吃掉了 CPU 大半面积——这就是“博士很聪明”背后那套昂贵的辅助机构。 GPU 直接把这套全砍了:它赌的是另一种活——成千上万笔计算彼此独立、没有分支、算什么早已定死(矩阵乘法正是如此)。不用猜路、不用调度,那些电路就是浪费,省下的面积全换成 ALU

取舍二:内存很慢,是“想办法别等”还是“人海盖过去”

访问内存很慢:CPU 算一步只要 1 个时钟周期,去内存取一次数据却要等几百个周期。两家的应对天差地别:

所以 GPU 的每个核心其实又慢又笨,单看一个线程比 CPU 慢得多;可几千个一起转,总吞吐反而碾压 CPU。它赢在总量,从不赢在单点。

取舍三:几千个核心“合用一个大脑”(SIMT)

GPU 还有个省面积的狠招:不给每个核心都配一套指挥系统。它把核心分组,一组几十个核心共享同一个控制单元、齐步走同一条指令,只是各自处理不同的数据。这叫 SIMT(单指令多线程)

而神经网络的矩阵乘法没有分支,每个数据走的路一模一样,完美适配 SIMT——这不是巧合,是深度学习恰好落在了 GPU 最擅长的那类计算上。

一句话记住差距的根源

CPU 假设“任务复杂、彼此依赖、充满判断”,所以花钱买聪明;GPU 假设“任务简单、彼此独立、没有判断”,所以花钱买数量 神经网络恰好是后一种,所以同一件事 GPU 能比 CPU 快几十上百倍——快的不是“单个核”,是“核多、且全用在了刀刃上”。

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

维度CPUGPU
核心数量 少(几个 ~ 几十个) 极多(上千 ~ 上万个)
单核能力 强:擅长复杂逻辑、分支判断 弱:只擅长简单算术
设计目标 低延迟——把单件事做到最快 高吞吐——单位时间处理的总量最大
擅长的活 串行、分支多、逻辑复杂的任务 大规模并行、同一种运算铺开的任务
在深度学习里 数据预处理、任务调度、跑不并行的零碎逻辑 矩阵乘法主力(训练与推理的绝大部分算力)

一句话:CPU 追求“把一件事做到极快”,GPU 追求“同时做海量简单的事”。神经网络的核心计算就是大量互不依赖的乘加,天生落在 GPU 这一栏。

还有更专一的芯片:TPU / NPU

既然神经网络说到底就是矩阵乘法,有人干脆造了把电路直接做成“矩阵乘法机器”的专用芯片:谷歌的 TPU、手机 / 端侧里的 NPU 都属这类。 它们比通用 GPU 更“偏科”——在“算矩阵”这一件事上更快、更省电,但灵活性不如 GPU。你可以把它们理解成:GPU 是“什么并行活都能接的万能车间”,TPU/NPU 是“只造一种零件、但造得飞快的专用产线”。

3. 显存:第一堵墙

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

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

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

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

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

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

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

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

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

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

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

从“参数服务器”到 AllReduce

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

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

如果模型大到单层的权重矩阵一张卡都放不下,就得把这个矩阵本身切块,分给多张卡各算一部分, 再把各卡的结果合并成完整答案。因为权重是用张量表示的,这种切法叫 张量并行(也叫模型并行)。这里“怎么合并”是关键,下面这个类比就专门讲它。

像把一道大乘法拆给几个人算

回到第 6 章那件事:一层前向就是一次矩阵乘。 矩阵能竖着切也能横着切,而这两种切法算完之后要做的事完全不同:

  • 竖着切(按输出维):每张卡算出的是完整结果的不同几列——各人负责的部分互不重叠,横向拼起来就是答案。
  • 横着切(按输入维):每张卡只拿到输入的一部分,算出来的是完整结果的一个部分和——形状虽然对了,数值却都不完整,必须把几张卡的结果逐元素相加才是答案。

真实的 Transformer 层(Megatron 那套做法)是先竖切、再横切配成一对,所以每层最后总要落到“逐元素相加”这一步—— 也就是说张量并行的主通信仍然是 AllReduce,不是单纯的拼接。 这也解释了它为什么最“娇贵”:每一层前向都要 AllReduce 一次(反向再来一次),通信频繁得多,所以张量并行一般只在同一台机器内用下面第 6 节的 NVLink 来做。 (纯拼接式的通信也确实有个名字叫 AllGather,但它主要出现在 ZeRO/FSDP 那类“把参数分片存、用时再收集”的省显存方案里,和这里切矩阵不是一回事。)

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

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

6. 卡与卡怎么通信

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

一个有意思的类比

训练大模型,本质是“不断调参数,让输出去目标”;而挖矿是“不断试,让 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|xt),loss 也只盯着 xt+1。MTP 会额外加几个“未来预测”目标,让同一个状态也去预测 xt+2xt+3……实现上可以是多个辅助 head, 也可以是几层轻量 MTP 模块,但训练信号的本质一样:一个位置不只问“下一步是什么”,还问“后面几步大概要往哪走”。

NTP(t) = CE( p(1)t, xt+1 ) 普通 next-token:只用当前位置预测下一个 token
MTP(t) = Σr=1R λr · CE( p(r)t, xt+r ) MTP:同一个位置同时监督后面 R 个 token;通常令 λ1=1,后面的 λ 控制辅助目标权重

这会带来三层训练收益:第一,监督更密,每个位置能贡献多份未来 token 的训练信号; 第二,表示更前瞻,因为 ht 不能只服务于眼前一个 token,还要对后续几步有一点规划; 第三,MTP head / MTP 模块本身学会了“便宜地猜一小段”,这正好可以拿到推理阶段当草稿器。 它对代码、数学推导、格式化输出这类“后面几步结构比较强”的任务尤其有价值。

推理时怎么加速? 自回归生成慢,是因为正常路径必须一格一格走: 先生成 xt+1,它变成上下文后,才能再生成 xt+2。 MTP 的草稿器先给出一串候选,比如 y1 y2 y3 y4;主模型不盲信它,而是把这串候选接到上下文后, 用一次带 causal mask 的前向计算同时算出多个位置的主模型分布。

如果当前上下文是 C,草稿器提出 y1,y2,y3,y4, 主模型一次验证时看的就是下面这些分布:

Pi = P( xt+i | C, y1, …, yi-1 ),   i = 1,2,3,4 第 i 个草稿 token 是否接受,看主模型在“前面草稿已当作临时上下文”时给出的分布

主模型怎么“验证”? 直觉版可以理解成:从左到右检查草稿 token 是否符合主模型在对应位置的分布。 贪心解码时,可以近似理解为“草稿 token 是否等于主模型此处最想选的 token”;采样解码时,实际系统会用概率接受 / 拒绝规则, 尽量保持主模型原本的采样分布不被草稿器改变。连续通过验证的 token 会被一次性接受,并且这次验证前向已经顺手建好了它们的 KV cache; 一旦某个位置没通过,后面的草稿就丢掉,从主模型在这个位置给出的分布继续生成。

所以 MTP 的原则是:草稿器只负责提案,主模型负责裁决。 它加速的是 decode 轮数,不是降低主模型标准。若平均每次能接受 3 个 token,原来 3 轮 decode 的进度可能 1 轮就推进完; 若草稿经常猜错,加速就会变小,甚至被额外草稿计算抵消。越是确定性强、格式稳定的输出,越容易吃到这类收益。

MTP 的代价也很实在。 它不是白送的加速,主要有三类成本:

案例:快模型不是只靠“变小”

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)进来,分诊台只把你分给最对口的一两个科,用不着惊动所有医生。 于是“医院很大、科室很全”(总参数多),但“你这一次只看一两个科”(每次算得少)。

稠密 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 一样,走同一条反传链,并不是不可训的黑箱。

这个结论后面还会反复出现:挑选本身可以是离散的,但挑选前的打分器是可微的,挑选后的加权计算也能从主 loss 回传梯度。 MoE 是“先给专家打分,再挑 FFN”;稀疏注意力是“先给历史 token / 压缩块打分,再挑该看的位置”。 真正麻烦的是没被选中的分支本轮通常拿不到直接梯度,所以系统还要靠负载均衡、预训练、蒸馏或逐步切换等办法,避免一开始就只宠少数路径。

反传分两路(直觉版)

某个 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」 路由 FFN② 反复接到「诗词类」token(床 / 明…) → 越训越专 FFN⑦ 反复接到「数字类」token(3…) → 走出另一条路 随机 init 不同 · 稀疏更新 · 路由共进化 · 无人工贴标签

不同 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 上的总参数量。

⑦ 稠密模型 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:

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

一张顶级 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 万位还不乱:

拉长手段到底在拉什么一句话直觉
位置插值(PI) 把要处理的位置整体等比压缩回训练见过的范围(如把 0–32K 缩放到 0–4K 再喂进去) “把尺子刻度整体缩小”,位置再远也落回熟悉区间
NTK-aware 不均匀地缩放 RoPE 各频率:高频少动、低频多拉,兼顾近处精度和远处覆盖 “该细的地方保持细,该拉远的地方才拉”
YaRN 在 NTK 思路上进一步分频段调整,是目前把上下文拉到几十上百 K 的常用配方 NTK 的加强版,少量微调即可撑很远

这几招都在 RoPE 的“旋转角度”上做文章:让训练时只见过短序列的模型,推理时也能把位置编到远处而不乱——这就是模型能“事后加长”的关键(第 20 章提过它们的名字)。

KDA 是一个例外:顺序写在“状态怎么更新”里

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:原始历史越长,要保留的 K/V 越多 K₁,V₁token 1 K₂,V₂token 2 K₃,V₃token 3 … Kₙ,Vₙ一路增长 当前 Q 逐个查历史 输出 序列越长,计算对象和 KV cache 越大 KDA:每来一个 token,就纠正同一块状态 S 旧状态 Sₜ₋₁ 压缩后的历史 新 token 产生 q / k / v 遗忘 + Delta 修正 旧答案哪里错了,只改差值 得到新状态 Sₜ 用 q 查询 Sₜ 不逐条翻历史 输出 状态大小固定

标准 Attention 像保留全部原始记录再检索;KDA 像维护一块不断修订的记忆板。这里的“压缩历史”是帮助理解的类比,状态里存的是学习出来的矩阵表示,不是一段可读文字摘要。

Delta Rule:不是把新内容硬塞进去,而是“发现错多少就改多少”

对当前 token,KDA 先得到 Query qt、Key kt、Value vt, 还会算出每个记忆通道的保留率 αt 和写入强度 βt。把原论文公式拆成更容易读的四步:

t = Diag(αt)St−1
t = tTkt
et = vtt, St = t + βtktetT
ot = StTqt 先选择性遗忘 → 用 key 问记忆当前会答什么 → 只写入“新 value − 旧答案”的差 → 再用 query 读取

这些遗忘和写入规则,也是训练出来的

KDA 并没有人工规定“遇到人名记久一点、遇到标点马上忘”。产生 q/k/v/α/β 的投影参数 都是从初始化值开始学习;预测下一个 token 的 loss 会穿过读取公式和每一步状态更新反向传播,教它什么该保留、什么该覆盖、用什么 key 写、用什么 query 读。 学习机制仍是本书反复讲的“前向 → loss → 反向传播 → 优化器更新”,变化的是前向计算图。

训练时不会真的一个 token 一个 token 慢慢算

递归公式看起来必须串行。实际训练会把长序列切成 chunk:chunk 内改写成矩阵乘法并行算,chunk 之间只传状态。 Kimi K3 还给单步衰减设下界,把累计缩放限制在 BF16 能表示的范围内,让原本难算的块也能走 Tensor Core 密集矩阵乘法。 因此它既保留递归记忆,又能适配 GPU 的并行训练方式 技术报告

为什么还要周期性插入全局 MLA?

固定状态的容量不会随历史增长,效率高,但也意味着许多 token 的细节要挤进同一块记忆,可能互相覆盖。 标准全局 attention 则能直接回看某个原始位置,精确但昂贵。Kimi K3 没有二选一,而是采用混合结构:

一组 Hybrid Attention:3 层 KDA + 1 层 Gated MLA KDAKDAKDAGated MLA 更新固定状态更新固定状态更新固定状态 重新做全局 token 交互 重复整组 Kimi K3 共 93 层 Attention 69 层 KDA + 23 层组内 MLA + 主干末尾再 1 层 MLA = 24 层 MLA

KDA 负责用固定状态低成本传递长历史,MLA 定期恢复直接的全局 token-to-token 交互。可以类比为“平时维护动态笔记,隔几层重新翻一次原始资料”。

机制历史怎么留优势代价
KDA压进固定大小状态 S随序列近线性计算,状态不随长度增长固定容量可能覆盖精确细节
Gated MLA保存压缩后的逐 token KV能直接做全局 token-to-token 交互计算更贵,KV cache 仍随长度增长

KDA 不是“完全等价但更便宜的 softmax attention”。Kimi K3 的关键取舍正是让两者互补,而不是押注单一路线。

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

把“局部窗口 + 少数全局”拼起来,就是 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”上。

这里的“动态”先别理解成“每个 query 自己临时决定 k 多大”。工程上常见的是先给模型或服务档位定一个预算, 比如长上下文档位用更大的 k;真正动态的是这次到底选中哪些历史位置。换句话说, k 多半是配置,被选进来的 token / 压缩块才是每个 query 现场决定的。

稀疏路线③ 闪电索引器:top-k 到底怎么选出来?

不是人工写规则,也不是先把完整注意力算完再裁剪。论文里的做法是:索引器给“当前 query token”和每个“历史 token” 算一个很便宜的索引分 I(t,s),再按这个分数排序取前 k 个。

It,s = Σj wIt,j · ReLU( qIt,j · kIs ) 索引分 = 多个索引头的“正相关点积”加权求和;分数越高,这个历史 token 越可能被选进 top-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:四层共用一次“目录” 只在第 1 层找 top-k,第 2-4 层复用同一份索引结果 朴素做法 第 1 层:找 top-k 第 2 层:再找 top-k 第 3 层:再找 top-k 第 4 层:再找 top-k IndexShare 第 1 层:找 top-k 第 2 层:复用目录 第 3 层:复用目录 第 4 层:复用目录 少问 3 次 4 层问 4 次路 4 层只问 1 次路

GLM-5.2 的 IndexShare 省的是“反复找该看谁”的成本:相邻几层通常关注的历史 token 差不多,没必要每层都重新算一遍索引。

DeepSeek-V4:一份长历史,按远近分流 近处保细节 · 中间压缩后挑重点 · 远处只保留轮廓 1M 历史 KV 不能全部原样摊开 按距离 分流 SWA · 近处 原样保留,细节不丢 CSA · 中距离 摘要 摘要 HCA · 远处 压得更狠,只留轮廓 三条线严格对齐:近处细看,中间摘要后挑重点,远处只看轮廓

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 预填充把整段 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。

为什么长上下文常常按档收费?

不是因为 attention “吐出来的 token”变了,而是每生成一个新 token,背后要维护、读取、搬运的历史信息变多了。 即使用稀疏 / 压缩注意力,服务侧也可能按上下文档位给更大的历史预算。下面只是一个简化示意,不是某家厂商的公开公式:

context < 32K   → k = 512
context < 128K  → k = 2048
context >= 1M   → k = 4096

上下文越长,prefill 要读的 prompt 越长,decode 每步要访问的 KV cache / 压缩历史 / 索引结果也越多; 同时超长请求更占显存、更难批处理。分段收费本质上是在给这些额外计算、显存占用和调度难度买单。

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

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

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

两层扛法:副本之间靠负载均衡摊开总流量,每个副本内部靠连续批处理 + 分页 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 用通道保留率 α 选择性遗忘,再按 vSTk 的 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……