最近 GLM 5.3 Flash、Qwen 3.8 Flash、Hy4 Preview 都扎堆发布,一些术语介绍看的很多人都说什么玩意,那就干脆简单聊聊这些东西是什么,主要是能至少理解这些是什么东西,知道有啥用。
比如以前段时间发布的 Hy4 preiview 的内容里,有 MoE、Top-8、共享专家、GQA、MLA、DSA、Indexer、IndexCache、残差连接、iHC、MTP、推测解码····
首先最基础的而且最常见的一些简单术语就有:
| 术语 | 是什么 | 作用 | 例子 |
|---|---|---|---|
| Parameter 参数 | 模型训练出来的数字,可以理解为它存储知识和计算规则的地方 | 参数越多,潜在容量通常越大,但显存、通信和部署成本也更高 | 770B 表示约 7700 亿个参数 |
| Activated Parameters 激活参数 | 处理一个 Token 时真正参与本次计算的参数 | MoE 可以保留巨大总容量,同时避免每个 Token 都跑完整模型 | Hy4 总计 770B,但每个 Token 只激活约 49B |
| MoE | Mixture of Experts,混合专家,把 FFN 拆成许多并行专家,每次只调用其中一部分 | 增加模型容量,同时控制每个 Token 的计算量 | Hy4 每个 MoE 层有 256 个路由专家 |
| Router 路由器 | 一个很小的网络,给所有专家打分 | 决定当前 Token 送给哪些专家 | 某个 Token 获得专家 17、43、91 等最高分,然后就被送给这些专家 |
| Top-8 | 从 256 个路由专家中选得分最高的 8 个 | 让每个 Token 只计算少数专家 | 比如“Flutter”这个 Token 在某一层选择 8 个专家,下一层可能选择另一组 |
| Shared Expert 共享专家 | 每个 Token 都必须经过的专家 | 处理各类输入都需要的通用知识,减少路由专家重复学习基础能力 | 比如语法、基础语言理解等通用处理可以进入共享专家 |
| Dense FFN | 所有 Token 都经过同一套完整 FFN 参数 | 计算稳定,没有专家路由问题,但参数越大计算越贵 | Hy4 第一层使用 Dense FFN,后面 77 层使用 MoE |
另外,“专家”只是结构名称,不能直接理解就成 “代码专家” 、“金融专家” 之类,训练后可能出现一定分工,但不一定就给这 256 个专家贴上人类可读的标签。
最后就是权重大小,例如腾讯 Hy4 的 770B/49B,这里 770B 权重都要放进 GPU 显存或者内存,粗略计算,比如 770B 的 BF16 权重大概需 1.54 TB(770B × 2 Byte ≈ 1.54 TB),如果是 FP8 就是接近 770GB,当然还没算 MTP、KV Cache 和运行时缓冲区,另外 Hy4 也是 Top-8+1 个共享专家的结构。
其次是一些参数的术语,比如:
| 术语 | 是什么 | 作用 | 例子 |
|---|---|---|---|
| Layer 层 | 模型连续进行一次注意力和 FFN 处理的计算单元 | 层数体现处理深度 | Hy4 有 78 层,一个 Token 的状态要连续经过 78 次更新 |
| Hidden Size | 每个 Token 在模型内部用多长的向量表示 | 向量越宽,可以同时编码更多特征,计算和显存开销也更高 | Hy4 的一个 Token 一般由 6144 个数表示 |
| Context Length | 一次请求最多可以放入多少 Token | 决定模型能同时处理多少代码、文档和对话历史 | 1M 可以容纳大型代码库、日志和多轮工具结果 |
接着就是一些让人更陌生的关键词,比如 GQA、MLA、DSA 和 IndexCache,这里就需要先简单理解普通注意力,一般就是说:
模型生成当前 Token 时,要拿当前 Token 的 Query,去和历史 Token 的 Key 比较,再取回相应的 Value。
可以把它剪掉理解成:
- Query:我现在想找什么
- Key:历史信息的索引
- Value:历史信息真正携带的内容
- KV Cache:已经计算好的历史 Key 和 Value
注意力术语
然后就是这里几个术语的简单理解:
GQA
GQA 是 Grouped Query Attention,多个 Query 注意力头共享同一组 Key、Value。
比如有 64 个 Query 头,普通多头注意力可能要保存 64 组 K/V,而 GQA 可以让每 8 个 Query 头共享一组,只保存 8 组 K/V,它主要降低 KV Cache 和内存带宽,比较适合长上下文推理。
Hy3 的时候用的就是这类相对常规的结构。
比如普通 MHA 相当于给 64 个调查员各复制一整套历史档案,然后 GQA 的做法是每 8 个调查员共用一套档案,所以最终只需要保存 8 套。
不过调查员想查的问题还是可能不同,所以 Query 还是 64 份,只是他们查阅的 Key/Value 档案可以共享,也就是 64 个问题还是 64 个,只是少保存很多份历史 K/V。
MLA
MLA 是 Multi-head Latent Attention,它进一步把 K/V 压缩成更短的潜在向量,需要使用时再参与注意力计算。
Hy4 的 KV 压缩维度是 512,简单说就是可以把每个历史 Token 原本庞大的多头 K/V 信息压成一个 512 维“压缩包”,它主要降低 1M 上下文时的 KV Cache ,占用和读取带宽也会更低。
当然严格来说,MLA 更接近于模型的注意力结构本身就学习一个低维 latent representation,需要做注意力计算时再从这个 latent representation 构造/参与对应计算。
实际上直觉上可以把它理解成:普通 Attention 给每个历史 Token 保存多组完整 K/V,而 MLA 不直接长期保存这么宽的一整套信息,它选择学习一个更紧凑的 latent state(潜在状态)。
比如 Hy4 的 KV 压缩维度是 512,可以粗暴理解成每个历史 Token 留下一个 512 维“压缩档案”,以后需要时再拿这个档案参与注意力计算。
所以 GQA 和 MLA 都在减少 K/V 成本,只是 MLA 的压缩力度和结构更激进, 类似于:
- MHA:很多份完整 K/V
- GQA:少保存几份完整 K/V
- MLA:连 K/V 本身的保存方式都进一步压缩
DSA
DSA 是 DeepSeek Sparse Attention,它主要解决的是另一个问题:
就算 K/V 已经压缩,如果当前 Token 还在逐一关注前面 100 万个 Token,计算量还是很大。
所以 DSA 加入一个轻量 Indexer:
- Indexer 给所有历史 Token 粗略打分
- 选出相关性最高的 2048 个位置
- 主注意力只对这 2048 个位置进行精确计算
比如上下文里有 100 万 Token 的 Flutter 仓库、构建日志和 Issue,然后当前问题是:
为什么 firebase_auth 在 Kotlin 2.4 下编译失败?
Indexer 可能从一百万个位置里挑出:
- Kotlin 编译错误
UnknownInitializationIdTokenChannelStreamHandlerFirebaseAuth相关代码checker-qual依赖信息
主注意力随后集中处理这些位置,DSA 把核心注意力复杂度从约 O(L^2) 降到 O(Lk),这里的 k 就是 2048。
不过需要注意的是,Indexer 自己还是要扫描历史信息,所以还是有成本在,只是它比主注意力轻得多。
Gated DSA
Gate 就是“阀门”,从 Hy4 配置里的 gated_mla=true、gating_type=elementwise 和各层的 linear_gate 看,它会对注意力输出进行逐元素控制,然后决定这次检索出来的信息有多少应该进入后续状态。
简单来说就是,Indexer 更偏向决定“看哪里”,然后 Gate 更偏向调节 「Attention 已经产生的这些特征,多少写回后续状态」。
所以 Gate 也可以理解成注意力输出后面的很多个“小音量旋钮”,Indexer 已经决定这次重点看哪些历史位置,Attention 也已经把这些信息汇总回来,而 Gate 接下来可以逐元素控制这些结果进入后续状态的强弱。
比如 Attention 输出里同时形成了“这是 Kotlin 编译错误”、“涉及 FirebaseAuth”、“可能和依赖版本有关” 等不同内部特征,Gate 可以把当前步骤不重要的部分压低,把更有用的特征保留下来。
IndexCache
因为 DSA 的每一层理论上都要重新运行 Indexer,重新挑选 2048 个位置,但相邻层选出来的位置一般高度相似,所以 IndexCache 把 78 层分成:
- Full Layer:自己运行 Indexer
- Shared Layer:直接复用附近 Full Layer 的 Top-k 索引
Hy4 的模式是:
0、1 层独立计算
2、3、4 层复用
5 层重新计算
6、7、8 层复用
9 层重新计算
……
这里总计 21 层运行完整 Indexer,57 层复用,所以大概 73.1% 的层省掉了独立 Indexer,但是这不是复用注意力结果,只复用“应该看哪些历史位置”的名单。
IndexCache 之前的论文就提到过,在 30B DSA 模型上移除了 75% 的 Indexer 计算,报告最高 1.82 倍 Prefill 加速和1.48 倍 Decode 加速。
可以简单理解成,第 20 层刚刚从 100 万页资料里列出了一张“最值得看的 2048 页清单”,然后第 21、22、23 层发现自己的关注重点通常和第 20 层差不多,所以直接借这张书单用来 Query 。
残差连接、残差流
一般普通 Transformer 的每一层大致是 xl+1=xl+F(xl) ,其中 xl 直接保留下来,F(xl) 就是是这一层计算出的新信息,这条直接保留原状态的道路,就是残差连接或 Identity Path。
它的作用是防止模型越深,原始信息和梯度越容易丢失。
也就是,比如第 20 层拿到的 Token 状态里已经包含“这是一个 Flutter 编译问题”,然后第 20 层又分析出一条新信息 “问题可能与 Kotlin 类型推断有关”,如果没有残差连接,可以粗暴理解成第 20 层拿新结果直接覆盖旧状态,但是如果有残差连接就类似于:
旧笔记 + 本层新增批注 = 交给下一层的新笔记。
所以模型一层一层往上走时,就不需要每层都重新从零保存前面的信息。
Residual Stream 残差流
残差流可以理解为贯穿所有层的内部状态通道,普通 Transformer 只有一条,注意力和 FFN 都从这里读取,再把结果写回这里,例如当前 Token 的 6144 维状态经过第 20 层更新,再交给第 21 层继续处理。
Residual Stream 就是这本从第 1 层一路传到第 78 层、不断被追加和修改的“共享笔记本”。
Hyper-Connections
Hyper-Connections 就是把一条残差流扩展成多条,然后让每个子层动态决定:
- 从哪些流读取
- 读取多少
- 计算结果写入哪些流
- 每条流写入多少
这样可以增加层与层之间的信息传输带宽。
iHC
iHC 是 identity Hyper-Connections,Hy4 给每个 Token 保留四条并行残差流,每条流都是 6144 维,一次子层计算大致是:
- 根据当前 Token 的内容,给四条流分别计算读取权重
- 把四条流加权合并,送进注意力或 FFN
- 得到一份新的计算结果
- 再给四条流计算不同的写入权重
- 把结果按不同强度写回四条流
比如读取权重可能是:
[0.5, 0.2, 0.2, 0.1]
写回权重可能是:
[1.0, 0.1, 0.7, 0.4]
四条流会逐渐形成不同状态,虽然可以简单粗暴理解成 “一条保存语义、一条保存任务状态、一条保存工具结果”,但其实这只是帮助理解的例子,实际上模型没有这样的固定分工。
iHC 中的 Identity 表示四条旧流继续各走各的,不用一个
4×4矩阵把它们相互混合,实际上就是输入时复制成四条流,子层动态 Read/Write。
也就是说,普通 Transformer 像全公司只有一本公共笔记本,每一层都从这一本里读,然后把自己的结果继续写进去,模型越来越深以后,早期的重要信息可能不断被后面的内容搅在一起。
然后 iHC 相当于同时维护 4 本并行笔记本,某一层工作时,可以说“这次主要参考第 1 本和第 3 本”,算完以后又决定“结果主要写回第 2、3 本,第 1 本只写一点”,这样一个 Token 穿过 78 层时,也就不再只有一条信息高速公路,可以同时拥有四条通路。
mHC
mHC 同样有多条残差流,但它会额外学习一个流间混合矩阵,旧的第一条流可以被混进第二、第三条流里面,然后通过双随机矩阵和 Sinkhorn 投影约束它,避免训练失控。
所以它的表达能力更强,计算、显存访问和训练复杂度也更高。
如果说 iHC 是“4 本笔记本各自保留自己的旧内容,只允许当前子层从几本里取信息、再分别写回”,那么 mHC 就是额允许在进入下一层以前,直接把 1 号笔记本的一部分旧内容搬进 2、3 号笔记本。
这样四条流之间交流能力更强,但也容易出现某一本越来越强、其他几本被冲淡的问题,所以 mHC 需要额外约束这个跨流混合矩阵,比如 Sinkhorn 的作用就是“别让信息全挤到其中一条高速公路”。
Qwen GR
这里还可以额外介绍下 Qwen3.8 Flash Next 的 GR ,也就是 Gated Residual,同样把残差流扩展成四路,同时动态控制读取和写回,Qwen 官方描述的是逐元素 Read Gate 和按分支 Write Gate。
所以 GR 和 iHC 都属于同一轮“多路残差流”探索,但实现细节不同,不过结构思想相近。
这里 Qwen 官方还提供了一个非常适合科普的观察:
四路残差流训练之后,其中一路会自然形成一条更明显的 long-range pathway,把较早层的信息带到深层。
这就像给模型修了四条路,工程师没有规定哪条是高速公路,但训练最后自己跑出了一条更适合长距离运输的主干道。
AttnRes
一般 Attention 不只可以看前面的 Token,也可以“看前面的层” ,前面介绍 iHC、mHC 和 Qwen GR 的时候,主要都在解决一个问题:
模型越来越深以后,信息怎么从第 1 层稳定地传到第 80、90 层?
Kimi K3 用的 Attention Residuals,也就是 AttnRes,思路又有点不一样。
普通 Residual 可以粗暴理解成:每一层都把自己的新结果加进同一本笔记,所以模型走到第 80 层时,手里拿的是前面几十层一路累积出来的一大本笔记,问题是每一层都是固定加进去 x = x + F(x) ,也就是:
越往后,这本笔记里的内容越来越多,早期某一层非常有价值的信息可能逐渐被大量后续更新稀释。
AttnRes 干脆选择让当前层自己判断:我现在到底更想参考前面哪几个阶段留下来的表示?
实现上,它会对前面的层或者 Block 表示做 Attention,所以它不是所有历史层永远都按照固定的 +1 往 Residual 里累加,AttnRes 会根据当前内容动态分配权重,比如模型已经处理到第 80 层,正在最终判断:
firebase_auth编译失败到底是什么原因?
它可能发现:
- 第 20 层形成的“Checker Framework 注解缺失”特别重要;
- 第 40 层形成的“Kotlin 2.4 SAM 推断”也很重要;
- 第 70 层刚刚产生的一些其他猜测反而没那么重要。
AttnRes 就可以动态地:
多拿一点第 20、40 层的信息,少拿一点其他阶段的信息。
所以这里其实也有一个很有意思的区别:
- 普通 Attention 主要是在:Sequence 维度找信息,前面哪些 Token 值得看?
- AttnRes 是在:Depth 维度找信息,前面哪些层留下来的表示值得看?
Kimi K3 实际为了避免把所有 93 层的输出全保存下来,还采用了 Block AttnRes,把多层合成一个 Block,再在 Block 级别做这种跨深度检索,所以现在把几家的 Residual 放在一起看就很有意思:
| 架构 | 最简单的理解 |
|---|---|
| 普通 Residual | 一本笔记从头写到尾 |
| iHC / GR | 一本笔记扩展成多本并行笔记 |
| mHC | 多本笔记之间还能动态交换内容 |
| AttnRes | 当前层可以主动翻以前几个阶段的笔记 |
线性注意力和混合架构
前面讲的 GQA、MLA、DSA,本质上都其实都在围绕一个问题:模型怎么更便宜地从很长的历史 Token 里找东西?然后如果你看最近的 GLM 5.3 Flash、Qwen3.8 Flash Next、Kimi K3 又有一些其他术语:Linear Attention、KDA、GDN、QSA……
这是因为现在很多模型已经不打算让每一层都保存和查阅完整历史了,普通 Attention 可以理解成,每读到一个新的 Token,都把以前的资料继续留在档案柜里,需要的时候再通过 Query / Key 去查。
如果上下文只有以前几千 Token,这么干确实没什么问题,但是到了 100 万 Token,哪怕已经用 MLA 压缩 KV,再用 DSA 少看一些位置,模型还是要维护一套很大的历史信息。
所以另一条路线就是:能不能一边读,一边把历史不断压进一个固定大小的“工作记忆”里?这就是 Linear Attention 这条路线最直观的理解。
KDA 和 GDN
比如现在模型正在读一个有 100 万 Token 的 Flutter 仓库,普通 Attention 会把前面 100 万 Token 的资料尽量都留着,以后需要什么再回头查。
但是如果是 Linear Attention ,就会类似:“我每读一页,就不断更新手里的一本“工作笔记”,后面主要带着这本笔记继续读”。
所以 Linear Attention 不需要随着上下文越来越长,让 KV Cache 一直线性膨胀,然后现在比较常见的两个名字就是:
- GDN:Gated DeltaNet
- KDA:Kimi Delta Attention。
KDA 本身就是沿着 Gated DeltaNet 继续发展出来的 Linear Attention,核心是维护一个固定大小的 recurrent state,也就是随着 Token 到来不断更新内部状态,然后 KDA 又把记忆衰减的控制做得更加细粒度,让不同内部通道可以有不同的遗忘速度。
可以粗暴理解成,有些信息 “这是一个 Flutter 项目” 可能需要记很久,然后有些信息比如 “刚才这个变量当前值是 17” 可能过几步就没用了。
所以 KDA 不只是“不断往笔记本里加东西”,还需要学习 「什么该留下、什么该覆盖、什么该慢慢忘掉」。
不过 Linear Attention 也有代价,因为你把几十万 Token 不断塞进一个固定大小的状态里,本质上就在不断做信息压缩,一些非常精确的历史细节可能越来越难恢复,所以现在真正流行的其实是:
Linear Attention + 普通/稀疏 Attention 的混合架构。
比如 Qwen3.8 Flash Next 就是一个非常典型的例子,它就是 3 层 GDN + 1 层 QSA 循环使用:
- 前三层主要负责便宜地“记”
- 第四层再真正回到全局上下文里精准地“找”
同样 Kimi K3 也是类似思路,3 层 KDA + 1 层 Gated MLA 不断循环:
- 93 层里一共是 69 层 KDA
- 24 层 Gated MLA
KDA 负责低成本维护长序列状态,周期性的 MLA 负责真正做全局 Token-to-Token Attention。
GLM 5.3 Flash 也走到了类似路线,它把 Linear Attention 和 Sparse Attention 混在一起使用:
- 大部分层承担便宜的状态建模
- 间隔插入 Sparse Attention 去精确检索全局信息
所以简单来说,现在这类混合架构可以理解成:
平时写摘要,需要细节的时候再翻原始资料。
QSA、IndexPool:现在连 Indexer 自己都开始压缩了
前面讲 DSA 的时候说过,整个过程类似于:
100 万 Token - Indexer 海选 - 找出最相关的 2048 个 - 主 Attention 精算。
但是做到这里又会出现一个问题,主 Attention 是省下来了,但是 Indexer 自己还要扫描 100 万个位置。
所以最近几家的一个共同方向就是,连“海选简历”这一步也要开始压缩,比如 Qwen3.8 Flash Next 的 QSA,也就是 Qwen Sparse Attention 就是这么做:
它不会让 Indexer 直接在每一个 Token 上都做完整索引,它反而先把连续 Token 聚合成更粗的 micro-block,Indexer 先判断哪几个区域最值得看?然后再把这些区域交给后面的 Sparse Attention。
如果继续用找简历的例子,DSA 就类似于:
100 万份简历全部快速扫一遍,然后挑 2048 个人。
而 QSA 就变成了:
先把简历按照部门、项目或者小组整理成很多小册子,先判断哪些册子值得看,再进去找具体的人。
所以 QSA 省掉的不只是 Attention,它连 Indexer 的搜索空间也缩小了。
GLM 5.3 Flash 的 IndexPool 也是在解决类似问题,只是实现方法不同,GLM 会把 4 个 Indexer Key 通过加权 Pooling 压成 1 个,所以原本 Indexer 要维护、读取的索引信息直接缩小大概 4 倍,然后才去执行后面的 Top-k。
简单书就是:
- IndexPool:一层里面,先把搜索目录压小
- IndexCache:不同层之间,已经找过的目录直接复用
一个是在压缩“这次要搜多少东西”,另一个是在减少“同一件事搜多少遍”。
DeepSeek V4 的 CSA / HCA:先把历史压缩,再决定怎么找
然后在这个问题上, DeepSeek V4 感觉又更进一步,它现在主要交替使用两种 Attention:
- CSA:Compressed Sparse Attention
- HCA:Heavily Compressed Attention。
CSA 可以简单拆成两,首先它会沿着 Token 序列压缩 KV:
大约每 4 个历史 Token → 1 个压缩 KV Entry。
比如原来有 100 万个历史位置,先压成大约 25 万个压缩位置,然后 Lightning Indexer 再从这些已经压缩过的位置里进行 Top-k,最后 Sparse Attention 只处理选出来的部分,同时另外留一小段 Sliding Window 保存最近 Token 的细粒度信息。
所以和最早的 DSA 对比:
- DSA:100 万个 Token 里找重点
- CSA:先把 100 万压成 25 万,再从 25 万里找重点
这种情况下 Indexer 自己自然也便宜很多。
然后 HCA 就更加激进了,它大概会:
每 128 个 Token → 压成一个历史 Entry。
这样 100 万 Token 最后只剩下七八千个压缩块,压到这么少以后,DeepSeek 干脆连 Indexer 都不要了,直接对这些高度压缩后的历史块全部做 Attention。
也就是 CSA 是「压一点 → 再 Top-k」,然后 HCA 「压得特别狠 → 全都看」。
DeepSeek V4 就把两种层交替使用,一个更擅长从历史中精确选重点,一个更像低成本地获得整个历史的大范围概览。
所以再回头看这些架构图,其实就没那么乱了
现在这些新模型,主要都在同时改几条“信息高速公路”:
| 问题 | 现在常见的办法 |
|---|---|
| 100 万 Token 怎么记得便宜 | KDA / GDN 这种 Linear Attention |
| 需要精确历史信息怎么办 | MLA / Sparse Attention / QSA / CSA |
| 连 Indexer 都太贵怎么办 | IndexPool / compressed indexer |
| 历史 KV 本身太大怎么办 | MLA / CSA / HCA |
| 模型太深,早期信息容易淡掉 | iHC / mHC / GR / AttnRes |
最后,如果回到之前发的图看,大概就可以简单理解现在国内所有开源大模型的实现的常见术语:
顺便用大佬的图解释下 KV Cache,为什么 KV 缓存会存储 K 和 V 向量,但不会存储前面我们一直提到的 Query 的 Q 向量?
简单来说,虽然每个 token 都会产生自己的 Q、K、V 向量,但是 Q 术语用完就扔的东西,缓存它完全是浪费内存,新 token 只需要自己的 Query 去“查询”历史就行,而历史的 Key 和 Value 是固定的、可复用的,所以 KV Cache 只存 K 和 V,每次只计算当前新 token 的 Q(以及它自己的 K/V,然后追加进缓存) :
推测解码和草稿
这部分其实之前讲过,详细可以看 《vLLM 测试不同 AI 推测解码架构》,简单来说就是,就算每一步只新增一个 token,模型权重都需要参与一遍完整前向计算,而推测解码会先让一个成本更低的草稿组件猜出未来的一些 token,比如一次猜 4 个,然后让目标模型在一次验证过程中检查这 4 个位置,如果前 2 个都符合目标模型的分布,第 3 个被拒绝,那这一轮就可以一次提交 2 个草稿 token,这样速度就可以快很多。
然后不同推测编码框架目的一样,但是实现方式各有千秋,比如 MTP 就是腾讯 Hy4 默认的推测编码,Hy4 内置一层 MTP,总参数 10B、激活 0.7B,和主模型一起训练并打包在模型里。
其他常见的有 EAGLE-3、DFlash 和 DSpark 之类。
量化
这一块应该是大模型里,普通人看着最容易看乱的地方之一,因为你经常会同时看到:
BF16、FP8、MXFP8、INT4、W4A16、GPTQ、AWQ、Q4_K_M、IQ2_XXS……
看起来全是“量化”,但很难直观从文本上就判断出都是些什么,其实可以最简单可以先分成三层:
| 看见什么 | 它主要在讲什么 | 例子 |
|---|---|---|
| BF16 / FP8 / INT8 / INT4 | 一个数字本身怎么表示 | 每个权重到底花 16 bit、8 bit 还是 4 bit |
| MXFP8 / NVFP4 / W4A16 | 一堆低精度数字怎么组织和使用 | 每 32 个数字共用一个 Scale;权重 4-bit、激活 16-bit |
| GPTQ / AWQ / Q4_K / IQ | 具体怎么把模型压下去 | 哪些误差要保护、每一块权重怎么编码 |
所以量化最核心的目的其实很简单:
模型本来有几千亿个数字,能不能少花一点 bit 保存这些数字,同时尽量别把模型能力压坏。
比如 Hy4 有 770B 参数,如果权重全部用 BF16,一个参数 2 Byte,光权重理论上就是:
770B × 2 Byte ≈ 1.54 TB
如果平均能做到 8 bit,大约就是 770GB,平均做到 4 bit,理论上来到 385GB 左右。
当然真实模型文件还会有 Scale、Metadata、高精度层等额外内容,所以不能直接拿这个数字当最终文件大小,不过但大方向就是: bit 越少,模型越小,内存带宽压力也越低。
先从 BF16、FP16、FP8 说起
因为众所周知,计算机做不到无限精确地保存一个小数,所以浮点数会把有限的 bit 分给三部分:
符号位 + 指数 + 尾数
可以粗暴把它想成一把尺子:
- 指数位决定这把尺子能量多远
- 尾数位决定尺子的刻度有多细
所以同样都是 16 bit,FP16 和 BF16 的“性格”其实很不一样:
| 格式 | 位布局 | 最大特点 | 常见用途 |
|---|---|---|---|
| FP32 | 1 + 8 + 23 | 范围大、刻度也细,但是占空间 | 高精度计算、累加、部分优化器状态 |
| FP16 | 1 + 5 + 10 | 刻度比较细,但范围比较窄 | 推理、传统混合精度训练 |
| BF16 | 1 + 8 + 7 | 范围和 FP32 接近,但刻度粗一些 | 现代大模型训练和推理里非常常见 |
| FP8 E4M3 | 1 + 4 + 3 | 范围较小,精度相对高 | 权重、激活等 |
| FP8 E5M2 | 1 + 5 + 2 | 范围更大,精度更低 | 对动态范围要求更高的场景 |
那 BF16 为什么这几年特别常见?主要还是因为它把 8 bit 都留给指数,和 FP32 一样,所以可以表示非常大或者非常小的数字。
FP16 是把更多空间留给尾数,所以同一个范围附近的小数分得更细,但是它只有 5 位指数,能覆盖的范围明显更窄,比如训练过程中有些梯度可能特别小。
所以这时候 FP16 类似于:
我的尺子刻度挺细,但是这个数字已经小到跑出我的量程了。
然后 BF16 更像是:
我的刻度没你那么细,但至少这个数字我还能量得到。
所以大模型训练里,很多时候其实 “先别溢出或者下溢” 会比多保留几位小数更重要。
然后 FP8 就是继续往下压,一个数字只用 8 bit,这样理论上权重占用和内存读取量相对 BF16 都可以接近减半,不过 FP8 能表示的数字明显更有限,所以实际使用时就开始遇到一个很重要的问题 :Scale。
Scale 是干什么的?
比如我要把下面这些数字压进一个低精度格式的时候:
0.02、0.03、0.05、0.08、18.0
问题就在于大部分数字都在 0.0x 附近,偏偏混进来一个 18.0 ,如果所有数字都直接共用同一套有限范围,那为了容纳 18.0,整个范围必须拉得很大,结果就是 0.02 和 0.03 可能最后都被压成差不多的值。
所以很多低精度格式会给一组数字配一个 Scale,也就是缩放比例,简单理解就是:
我先看看这一组数字大概有多大,再决定给它配一把多大量程的尺子。
比如原始数字是18 ,低精度格式放不下,那就可以先除以一个 Scale 18 ÷ 8 = 2.25 ,然后先保存这个 2.25,以后计算的时候再乘回去。
所以实际低精度计算里,肯定不是不是简单的一个数字直接砍到 8 bit,实际上是 一组低精度数字 + 一个 Scale ,这也就是为什么所谓“4-bit 模型” 最终平均占用经常不是严格的 4.000 bit/weight,因为 Scale 自己也要占空间。
那 MXFP8 又是什么?
有了 Scale 以后还有一个问题:到底多少个数字共用一把尺子?
比如几十万个数字全共用一个 Scale,里面只要混进几个特别大的离群值,其他小数字还是会被挤得很难受,所以 MX 就是 OCP Microscaling,干脆把 Tensor 切稀碎。
比如 MXFP8 通常是每 32 个元素共用一个 Scale,可以把它想成原来一个体育场几十万人共用一把尺子,现在改成:
每 32 个人发一把自己的尺子。
- 这一组数字都是
0.01~0.1,就用适合小数的 Scale - 另一组数字是
10~50,自己换一个更大的 Scale
这样一来,就算每个元素本身只有 8 bit、6 bit 甚至 4 bit,也更容易保住原来的信息,OCP 的 MX 格式大概可以这样看:
| 名称 | 每个元素 | 每组大小 | Scale | 大约有效存储 |
|---|---|---|---|---|
| MXFP8 | FP8 E4M3 / E5M2 | 32 | E8M0 | ~8.25 bit |
| MXFP6 | FP6 E2M3 / E3M2 | 32 | E8M0 | ~6.25 bit |
| MXFP4 | FP4 E2M1 | 32 | E8M0 | ~4.25 bit |
| MXINT8 | INT8 | 32 | E8M0 | ~8.25 bit |
这里 E8M0 的意思可以简单理解成:
Scale 自己也是一个很紧凑的浮点数,而且只表示 2 的幂。
所以 MXFP4 虽然名字叫 4-bit,实际平均下来大约是 4.25 bit,因为每 32 个元素还需要额外保存一个 Scale。
那 MXFP4 和 NVIDIA 的 NVFP4 又有什么区别?
NVFP4 也是 4-bit 浮点,但 NVIDIA 在 Blackwell 上把 Scale 做得更细,两边块内的数字本身都是 E2M1:
1 位符号 + 2 位指数 + 1 位尾数
真正的差别主要在 Scale:
| MXFP4 | NVFP4 | |
|---|---|---|
| 块大小 | 32 个元素 | 16 个元素 |
| 块内数字 | E2M1 | E2M1 |
| 局部 Scale | E8M0,只能按 2 的幂缩放 | FP8 E4M3,缩放更细 |
| 额外 Scale | 无 | 通常还有 tensor-level FP32 Scale |
| 平均存储 | ~4.25 bit | ~4.5 bit |
也就 MXFP4 32 个人共用一把只能按固定档位调节的尺子,但是 NVFP4 是 16 个人共用一把调节更加细腻的尺子 ,所以虽然 NVFP4 平均会稍微多花一点空间,但通常更容易贴近原始数字,精度也更好。
那到这里其实 BF16、FP8、MXFP8、NVFP4 这一组就已经比较清楚了:
- BF16 / FP8 / FP4 主要解决“每个数字本身用多少 bit”
- MX 和 NVFP4 进一步解决“很多低精度数字应该怎么共享 Scale”
然后是 INT8、INT4、W4A16 这些词
前面的 BF16、FP8、FP4 都还是浮点数,另外一大类量化会直接使用整数,例如:
- INT8:通常把数字映射到大约
[-128, 127] - INT4:通常映射到大约
[-8, 7]
当然模型原来的权重肯定不只是这些整数,所以还是靠 Scale,比如原始权重 0.137 ,经过量化以后可能保存成 7 ,同时记录 Scale = 0.02 ,这样恢复时大噶就是 7 × 0.02 = 0.14,也就是原来的 0.137 变成了 0.14,这就是量化误差。
模型量化真正麻烦的地方也是这个:
怎么让几十亿、几百亿甚至几千亿个这种小误差,不要最后一起把模型能力搞坏。
然后你就会看到 W4A16、W8A8 这种名字,这里:
W= Weight,权重A= Activation,激活
所以 W4A16 就是权重 4-bit,运行过程中 Activation 还是 16-bit,它最大的好处是模型权重小很多,同时 Activation 还保留比较高的精度,所以也是很多本地模型常见的思路。
而 W8A8 就是权重和 Activation 都是 8-bit,W4A8 就是权重 4-bit、Activation 8-bit。
那 NF4、GPTQ、AWQ、SmoothQuant 又是什么?
到这里就又是另外一层东西了,前面的 INT4、FP4 主要是在说:
一个数字最后怎么表示。
然后 GPTQ、AWQ、SmoothQuant 的目的是:我要怎么把模型变成这些低精度数字,同时尽量少掉能力。
比如现在有一万个权重要压到 4-bit,最粗暴的方法当然是大家一起四舍五入,但模型里的权重不是同等重要,有些权重 0.81 → 0.80几乎没什么影响,但是有些敏感位置稍微改一点,最后一层层传递下去,输出可能就变很多,所以就有了各种量化算法:
| 名称 | 可以怎么理解 |
|---|---|
| NF4 | 专门给近似正态分布权重设计的一套 4-bit 数值表示,QLoRA / bitsandbytes 很常见 |
| GPTQ | PTQ 量化算法,会利用近似二阶信息尽量补偿权重量化带来的输出误差 |
| AWQ | 观察 Activation,重点保护对模型输出比较重要的权重通道 |
| SmoothQuant | 把 Activation 里特别难压的离群值部分“搬”给权重,让 W8A8 更容易做 |
| PTQ | Post-Training Quantization,模型训练完以后再量化 |
| QAT | Quantization-Aware Training,训练阶段就让模型适应未来的低精度环境 |
所以可以简单理解成:
4-bit 只是告诉你预算还有多少,GPTQ、AWQ 之类这些名字是在告诉你这 4 bit 怎么花。
GGUF 里的 Q4_K_M、IQ3_XXS 又是什么?
如果你平时用 llama.cpp、LM Studio、Ollama 或者下载 Hugging Face 上的 GGUF,看到的又会是另外一套名字:
Q4_0
Q4_K_M
IQ3_XXS
Q8_0
UD-Q4_K_XL
MIX-STQ1_0
这里首先要注意的时候,这些文件名更多是一套社区量化配方的名字,不能简单理解成某一种标准的 4-bit 数字格式,比如 Q4_K_M.gguf 不代表整个模型里所有 Tensor 全都是严格 4-bit,实际上它更多代表的是一种量化预设。
发布者可以让模型大部分权重使用低 bit,同时把某些比较敏感的 Tensor 留在更高精度,所以看这些名字,我觉得最简单的办法还是先看前面的数字:
Q2 → Q3 → Q4 → Q5 → Q6 → Q8 → F16/BF16
但这里的数字也只能理解成“大概处在几 bit 这一档”,因为除了权重本身,Scale、Min、码本以及某些高精度 Tensor 都会占空间。
比如底层 Q4_K Tensor 本身大概是 4.5 bpw,但一个完整的 Q4_K_M.gguf 因为里面可能混合其他精度,整个模型平均 bpw 可以更高,所以不要看到 Q4 就直接拿参数量 × 4 bit 当最终文件大小。
_0、_K、IQ 又是什么?
是不是快看晕了,实施这些主要是在说到底使用哪一套编码方案,比如:
| 名字 | 简单理解 |
|---|---|
| Q4_0 | llama.cpp 比较早的一套 4-bit block quant |
| Q4_1 | 在 Q4_0 思路上额外保存一些范围信息 |
| Q4_K | K-quant,用更大的 super-block,同时进一步压缩 Scale 等辅助数据 |
| IQ4_XS | I-quant,使用更复杂的码本和重要性处理 |
| TQ1_0 | 三值量化,权重大致只留下 -d / 0 / +d |
| STQ1_0 | Sparse Ternary,在三值基础上进一步加入固定稀疏结构 |
所以平时下载 GGUF,可以粗略这样看:
_0 / _1往往是比较早的 block quant_K是目前 llama.cpp 很常见的一套 K-quant 家族IQ通常会用更复杂的编码换取更好的超低 bit 表现TQ / STQ已经属于非常激进的三值、稀疏三值量化
所以实际上不需要纠结比如 _K 这个字母到底代表什么,知道它是一套 K-quant / super-block 量化家族就够了。
_S / _M / _L / _XL 怎么看?
然后这个又是一个让人困惑的东西,它们可以大致理解成:
同一个量化家族下面,不同大小和质量取向的配方。
比如有些方案会:
- 大部分 Tensor 使用 Q4_K;
- 部分比较敏感的 Tensor 升到 Q5_K;
- 更重要的位置可能继续留 Q6_K、Q8_0。
所以通常后缀越偏“大”,文件也会更大一些,质量更保守,但要注意的是:
S / M / L / XL 不是 GGUF 规定的一套所有量化方法都必须支持的统一等级。
比入 llama.cpp 自己有 Q3_K_S / Q3_K_M / Q3_K_L ,但是 Q4_K 常见的是 Q4_K_S / Q4_K_M,然后当前 llama.cpp 里的 Q4_K 单独出现时,实际上也是 Q4_K_M 的别名。
而至于 UD-Q4_K_XL 的这种 _XL,更多就是 Unsloth Dynamic 等量化方案自己增加的档位,所以你可以大致用 S < M < L < XL 判断“这个发布者大概越来越保质量”,但是具体到底哪些 Tensor 升了多少 bit,还是要看具体量化配方。
UD-、MIX- 这些前缀呢?
这个相对就简单了,它们通常是发布者用来标记自己的混合量化方案,比如:
| 前缀 | 大概什么意思 |
|---|---|
| 没特殊前缀 | 经常直接沿用 llama.cpp 的量化 preset 名称 |
| UD- | Unsloth Dynamic,根据模型不同 Tensor 的敏感程度动态选择量化类型 |
| MIX- | 发布者自己组合出来的一套 Mixed Quantization |
所以一个 UD-Q4_K_XL 表达的:这是一个整体体积大约位于 4-bit 档位、以质量为导向的 Unsloth Dynamic 混合量化方案。
比如 Hy4 的 200GiB 版本,Hy4 原始有 770B 参数,BF16 权重理论上大概就是 1.54 TB,但现在已经有 200GiB 出头的 MIX-STQ1_0 GGUF,官方做的就是一种非常激进的混合量化:
- 占参数量特别大的 routed expert 某些矩阵可以使用 STQ1_0、IQ2_XXS 这种极低 bit
- 一些更敏感的 expert down 会留到 IQ3、IQ4;
- Attention、MLA、DSA Indexer、Router 等关键部分继续使用 Q5、Q8,甚至部分保持 F32。
实际上最后整个模型平均下来才落到两三个 bit/weight 左右。
感兴趣的可以看:Hy4 将 1.5TB 模型压缩到 200GiB GGUF 的 MIX-STQ1_0 定制版本,性能几乎没什么损失。