DeepSeek-V4-Flash 的混合精度压缩

按 tensor 家族分配精度,可以把 DeepSeek-V4-Flash-0731 的 284B 权重压到 80.76 GiB,全部 256 个路由专家一个不删,在一台 128 GB 的机器上以 262,144 的物理上下文运行。
这个结果的关键不在压缩率,而在精度分配的不对称。这个模型每 token 只激活 13B 参数,但字节几乎全部堆在路由专家上:43 层、每层 256 个专家。2-bit 只发给这一个家族,其余一切——注意力投影、共享专家、router、embedding、输出头、norm 与 scale——都保持 Q8 或更高。位宽最省的地方恰好是冗余最高的地方,而每个 token 都必经的窄路径一律不降精度。
压缩把模型放进了机器,但没有把它放进目标速度。剩下的差距只能靠投机解码补。本文后半部分是那一半的结果:在这套连续投机的实现里,无损保证不成立——同一个 prompt、温度 0,三个验证深度给出三组互不相同、但各自稳定的输出。
配方
精度按 tensor 家族分配,不按层分配:
路由专家的 gate/up 走 IQ2_XXS,down 走 Q2_K;注意力投影、共享专家和输出头保持 Q8;embedding、compressor 与 indexer 保持 F16;router 保持 F16;norm、scale 与 critical scalar 留 F32。

同时钉死四条不许动的:保留 256/256 路由专家,保持 top-6,不做层跳过,不改稀疏注意力的 mask。这四条与量化不是一类操作——量化改变的是权重的表示,这四条改变的是模型函数本身。一旦动了,任何与官方基线的质量对照都失去意义,而剪专家、降 top-k 恰恰是压缩时最容易被顺手拿走的两个额度。
同源提取的 DSpark drafter 单独转成 5.64 GiB 的 sidecar。它必须来自同一个 revision:结构一致不等于 draft 分布一致,混版会改变接受率与残差校正的输入,而且无法审计。我们做过一次 0731 与预览版的 tensor 清单对照,差异全部落在 mtp.* 命名空间——预览版是旧的 MTP 结构,0731 是重新设计过的 DSpark,连 tensor 名都不同。混版禁令因此有结构级证据,不只是纪律。
转换审计是逐 tensor 的:每个源 tensor 必须恰好落入一个目标 tensor,或者有明确的未转换理由;shape、stride、scale、expert id、layer id 全部校验;再按量化类型分层抽样做反量化误差比对——sidecar 侧抽了 660 个 IQ2_XXS 与 316 个 Q2_K,target 侧覆盖 BF16、F8_E4M3、F32 到各目标类型的全部组合。
引擎推翻了纸面配方
第一次全量转换是照着架构文档字面执行的,把 embedding、compressor、indexer 都显式覆盖成 q8_0。smoke 加载直接被拒:tensor token_embd.weight has type q8_0, expected f16。引擎对这几个 tensor 硬编码了期望的布局类型,配方里写什么都不算数。
改回模板原生 recipe 之后加载通过。与纸面配方的偏差只有一处:compressor、indexer 和 embedding 用 F16 而不是 Q8——精度只高不低,尺寸 80.78 GiB 仍在 81.5 GiB 的上限内。这类冲突值得单独记一笔:量化配方的可行域是引擎定义的,不是文档定义的,而这件事只有在真的加载一次之后才会知道。
第二处是构建标志。make cuda-spark 在 CUDA_ARCH 为空时构建出的 D2R IQ2_XXS/Q2_K kernel 是 arch-gated 的,运行时直接返回错误码,CUDA regression 双 FAILED。显式用 sm_121a 原生构建后 regression 全绿,融合 D2R 的 parity 是 0.00000000。架构文档里写的 sm_121/sm_121a 要求不是建议。
imatrix 的先有鸡还是先有蛋
imatrix 校准需要用运行时跑一遍校准语料,而运行时需要一个已经转好的工件才能起来。我们的顺序是先用合成 importance(工具的 fallback,取权重平方和)转一版 bootstrap 工件,把引擎跑起来,再采集这一版权重专属的 imatrix,最后重转正式 target。
采集环节又撞了一次工具边界:fork 自带的 --imatrix-dataset 是 Metal-only 的,CUDA 构建下直接报错要求 --metal。改用 llama.cpp 采集(GGML_CUDA,ARCHS=1210),能正常读 ds4 格式的 GGUF,输出标准 .dat。校准数据集的 hash 落进 manifest,与 artifact id 绑定。
一个值得记录的观测:合成 sidecar 的投机加速为零。同样 32K 上下文、温度 1.0、300 个 token,关闭投机 12.38 tok/s,K=1 是 11.87,K=5 是 11.91——接受率太低,验证开销吃掉了全部收益。这不是投机解码不管用,是 drafter 没校准。target 的 imatrix 是这里的关键变量。
容量账
标称 128 GB 是十进制。本机实测 MemTotal 127.6 GB,落到 GiB 约 119;纯读带宽实测 250.3 GB/s。
最终 target 工件 86,720,111,552 字节,即 80.76 GiB。加上 5.64 GiB 的 sidecar、12 GiB 热缓存、262K 上下文的 KV 与索引约 6 GiB、CUDA graph 与 workspace 约 2.5 GiB、系统与常驻守护进程约 4.5 GiB,设计点略高于 115 GiB,给 119 GiB 留下三到四 GiB 余量。
实测:暖页缓存下 15.6 秒映射完 80.76 GiB;一次性前填 245,769 个 token,前填 704 tok/s,续写 65 个 token 后正常退出;全程最低 MemAvailable 2.24 GiB。

这次运行不算正式的容量门。冻结的 prompt 是 245,760 个 token,引擎报了 245,769,因为 HTTP 层给原始 prompt 加了 9 个协议 token。这 9 个 token 让它不能作为正式证据使用。4 GiB 的内存地板要在 220K 暖态那一档判,那一档还没跑。
压缩没有解决的问题
单流 decode 的瓶颈是带宽,不是算力。每生成一个 token 都要把活跃权重完整读一遍——这个工件量级下约 11 GB——按 215–250 GB/s 的有效读带宽算,物理上界落在 19–22 tok/s,真实实现更低。我们实测的 plain decode 是短上下文 15.9 tok/s、32K 上下文 12.4 tok/s。
压缩已经把这个数推到了它能推到的位置。再压下去伤的是质量,而不是带宽账——2-bit 已经给了字节最多的家族,剩下的家族加起来也省不出一个量级。
所以剩下的差距只有一条路:一次权重扫描提交多个 token。小模型先猜 K 个,大模型一次性验证。0731 的官方 checkpoint 自带这个小模型,挂在 mtp.0..2 上,对应 target 的第 40、41、42 层。看起来是白送的杠杆。
投机解码号称无损,保证写在一行规则里:接受概率取 min(1, p(x)/q(x)),拒绝后从归一化的 max(p − q, 0) 采样,输出分布严格等于 target 自己逐 token 解码的分布。前提是 p 确实是 target 的分布。
验证宽度漂移
固定一个 greedy 请求——temperature=0、top_p=1,采样随机性被完全排除——只改验证深度,在隔离环境的公共 sidecar 上扫一遍:关闭投机时模型写 71 个 token 停下,K=1 和 K=2 写 84 个,K=3 和 K=4 写 128 个。三组输出各自跨独立重启逐字节稳定,互相不同,finish reason 都是 stop,响应体 SHA-256 都已记录。两次关闭投机的独立重启结果完全一致,所以这不是环境噪声。

第二次取证换了工件和内存路径:用最终的 imatrix target 和生产的 device-cache 路径,每个深度跑 3 个不同的 coding prompt、各 100 个 token。K=1 三个全部与关闭投机逐字节相同,K=2 到 K=5 全部发散。
两次放在一起才是完整结论。K=1 在生产路径上通过了有限对照,很容易被读成「K=1 是安全的」;但在公共 sidecar 上,K=1 同样偏离了 plain target。通过一次有限对照不等于精确。 我们据此把生产配置锁在 K=0,而不是锁在 K=1。
失效点在接受判定之前,不在回滚里。这套实现把「当前 token + K 个草稿」打包成一次宽度 1+K 的 target 前向,而这次前向与宽度 1 的普通解码不是同一条数值路径:MoE 的跨行归约顺序不同,浮点加法不满足结合律;CUDA 在宽度 2 到 8 上另有一层向量化派发。投机的第一个 token 直接取自这次宽前向,所以偏差在接受、回滚、EOS 处理和 usage 记账有任何机会介入之前就已经发生。宽度 `1+K` 的 target,和宽度 1 的 target,不是同一个 target。
最直接的证据来自部署的源码本身。它在门禁注释里写明:宽度 1+D 的批量验证与宽度 1 的解码不是逐位相同,原因是跨行 MoE 归约的浮点差异,因此 accept(A≠E) 只作 informational。它随后用一个更弱的不变量替换了「与普通解码一致」——只要求在同一验证宽度下、两个强制拒绝值之间的 token 流一致。那证明的是被拒绝尾部的因果性,不是与宽度 1 解码的等价性。回滚处的注释同样直白:已提交 cache 里的值本身就带着宽前向的差异,那里出现数值差属于预期。
回滚能删掉被拒绝的行,但删不掉一件事——被接受的那一行,本来就是另一条数值路径算出来的。 深度越大越容易观测到,但这不是深度 3 以上才有的 bug,是深度 1 以上就已经存在的属性。
我们把它归类为引擎缺陷,而不是可接受的量化噪声。投机解码只应该影响速度;如果关掉它输出就变了,说明实现不精确,要修的是引擎,不能把 drafter 当成质量旋钮。
为什么这不是一个宿主端补丁
现成的精确修法只有一种:顺序验证——每个草稿都走一遍已确立的单 token 路径。它是对的,而且是刻意慢的;另一个 fork 的作者在文档里把这个开关直接标注为「正确,且刻意地慢」。它放弃的恰好是权重扫描的摊薄,而摊薄是投机解码全部收益的来源。在当前实现里,精确和加速是同一份预算里对立的两项。
真正的修法是写新 kernel:每层权重只读一次,同时逐位复现宽度 1 的算术、路由决策、cache 写入顺序和输出头。这不是在宿主端改接受或回滚逻辑能解决的问题。
即使做到精确,收益也仍待证明。我们先修好了 sidecar 的加载兼容性——旧的命名 schema 绑不上新 loader,而且一个容量守卫会让深度 1 静默产出零草稿,日志里写着 drafts=0,不报错。修完之后 K=1 拿到 96.77% 的接受率:31 个草稿命中 30 个,输出字节、finish reason 和 usage 与关闭投机完全一致。速度是开 14.3 tok/s、关 21.1 tok/s。在另一组用最终工件做的扫描里,K=1 是 15.5 tok/s、关闭是 15.4 tok/s。到目前为止,我们没有观测到投机解码带来过一次净收益。

如果你也在本地跑投机解码
三件事可以在自己的部署上直接做。
用固定 greedy 请求扫验证深度,而不是只测一个深度。 temperature=0、top_p=1,同一个 prompt,把 K 从 0 扫到你打算用的最大值,逐字节比对输出。只测一个 K,会让你把「这个深度恰好一致」读成「实现是精确的」。
跨独立重启复现每一档。 不跨重启,你无法区分实现的确定性偏差和环境噪声。我们那组数据之所以能下结论,是因为关闭投机的两次独立重启逐字节相同。
别把接受率当正确性证据。 这两件事测的是完全不同的东西:接受率高说明草稿模型猜得准,与验证路径的数值是否等价无关。我们手上就有 96.77% 接受率与输出发散并存的样本。
我们不宣称什么
- 不宣称这个 2-bit 工件与官方权重逐 token 等价。混合精度压缩的质量必须由对照冻结官方基线的评测门来判,而那道门我们一次都还没跑过。在它跑过之前,上面所有内容都是工程观测,不是质量结论。
- 不宣称已经定位到第一处数值发散的层或算子。要做到那一步需要 logit 与 hidden state 的逐层 trace,我们还没做。当前结论不依赖它:部署源码已自己声明宽验证非逐位相同,端到端的 greedy 输出展示了后果。
- 不宣称宽度漂移适用于所有投机解码实现。它适用于把草稿打包成一次宽 target 前向、并且宽度会改变 kernel 派发或归约顺序的实现;逐草稿走单 token 路径的顺序验证不在此列。
- 不宣称 K=1 是安全的。它在生产路径的有限对照里精确,在公共 sidecar 上发散。
- 不宣称 4 GiB 的内存地板在 220K 暖态成立。那一档还没跑。
这台机器上的验收远没有走完:为它设的八道门,目前过了第一道,其余的缺完整部署运行时证据、排队、或者暂停。
压缩这件事本身,结论是清楚的:284B、全专家、262K 上下文,在 119 GiB 里装得下、跑得动。接受率 97% 说明草稿猜得准——它不说明验证是对的。
附录 A:内存现场
这类机器上的稳态是 117 GiB 用掉、3.8 GiB 空着,于是系统自带的 OOM 守护会把模型当成异常。
earlyoom 默认在 6%(约 7.3 GB)触发,而生产路径稳态就在 4–8 GB 之间。引擎源码里硬写了 oom_score_adj=1000,主动把自己设成第一顺位牺牲品。我们打补丁想改成 -900,两天后才在 stderr 里看到一行 Permission denied——内核规则是非特权进程只能调高、不能调低自己的 oom_score_adj,补丁的保护前提从一开始就不成立。
加载瞬态更凶:0.2 秒轮询看到 4.4 GB,journalctl 记录的实际最低点是 1.6 GB,earlyoom 沿 badness 顺序往下杀,误杀过一台 Gitea。预驱逐 86.4 GB 页缓存没用(凹陷点的文件页只有约 7.5 GB,凹陷是结构性的);献祭一个 10 GB 气囊进程没用;调低权重缓存上限直接报错退出,因为有一段 span 是强制驻留的,没有优雅降级这条路。
最终的干净修复是两条:把模型进程加入 earlyoom 的 avoid 名单,再挂 16 GB swap。模型是 CUDA pinned,根本不吃 swap,但 swap 存在这件事让 earlyoom 的双触发条件不再同时成立,稳态就安静了。
附录 B:KV 前缀命中取决于客户端回显的字节
按 850 tok/s 算,220K 的冷前填是 259 秒。所以在多轮工具会话里,KV 前缀复用不是优化项,是产品能不能用的前提。
服务端的磁盘 checkpoint 按 token 文本前缀匹配,命中多少完全取决于客户端回显历史时的字节保真度。同一段会话,三种回显方式:按官方默认丢掉 reasoning、用规范化 DSML 块,命中 363/363,停在第一轮 prompt 边界,之后全部重算;保留空的 think 标记、把模型原样采样出的 DSML 块放回去,命中 363 加增量;回显 reasoning_content 并原样重放采样出的 DSML 字节,命中 428/448,只需写入 20 个新 token,续写前填省掉约 95%。
原因很朴素:客户端习惯把模型输出的 tool call 归一化成 JSON 再发回来,服务端重新渲染出的字节只要差一个字符,前缀就断在那个字符。

因此我们给 gateway 定了两条客户端契约。第一,把响应里的 reasoning_content 原样放回下一轮的 assistant 消息——那是采样原始字节,parser 不做 strip。第二,回显我们分配的 tool_call id,我们据此取回原始采样的 DSML 块,在编码时按 canonical 块的唯一性替换;不能按序号对齐,因为 tools 模板本身含示例 DSML 块,按序号会替换错位置,这条有回归测试守着。
诚实边界:上面三档命中数来自服务器日志。gateway 侧的重放接线目前用 mock transport 验证,live 端到端还没有在同一次运行里闭合,因此不作为已通过的证据。
