量化检查点常缺少缩放规则、零点约定、打包顺序和布局等关键语义,生产者与消费者不一致时可能静默产生错误数值。文章通过为这些格式约束建立显式类型,让不兼容配置在加载阶段暴露。
有一个事实至今仍让我耿耿于怀:一个四位量化模型文件会告诉你它包含多少个元素,以及这些元素带有什么 dtype 标签——除此之外,那些真正重要的信息几乎一概没有。它不会告诉你共享缩放因子是按什么规则推导出来的,不会告诉你零点约定,不会告诉你亚字节的打包顺序,也不会告诉你缩放平面写入时采用的布局。文件之所以能用,仅仅是因为生产者和消费者碰巧在所有这些方面达成了默契,而这些约定只是静默地存在于代码中。
仅在 2026 年,vLLM 和 SGLang 就记录了六起事故,恰恰是这些无声的约定导致模型输出在不知不觉中出错。不是崩溃——而是数值错误。缩放张量在加载时被丢弃。带符号缩放因子被当作无符号值读取——导致反量化误差达到约四个数量级。采用 swizzled 布局的生产者连接到了采用线性布局的消费者。每一个文件都顺利完成了加载。
2026 年,vLLM + SGLang:六起有记录的事故、五类故障——每一个都顺利完成了加载
静默出错的内容 | 让错误变得明显的字段
带符号缩放因子被当作无符号值读取 | 约四个数量级的反量化误差
精确的格式标识(s8 ≠ u8)
Swizzled 生产者 → 线性消费者 | 布局不一致,双方都“正确”
placement id 不匹配
缩放张量在加载时被丢弃 | 平面直接缺失,却没有任何报错
§7.3 平面大小方程
静默的 E8M0 截断 | 缩放格式在毫无提示的情况下被缩窄
GPTQ“零点减一”(迟至 2026 年 1 月仍存在)| 数万个零点 → NaN 困惑度
不是崩溃——而是数值错误。每一种情况,都是本可以由边界处的某个机制回答的问题。
这些是 2026 年 vLLM 和 SGLang 中记录的故障类别,以及 GRIT 中能够把每一种故障从“静默产生错误数值”转变为边界处显式失败的字段。这些故障没有一个导致崩溃;所有文件都顺利完成了加载,然后生成了错误输出。
因此,我花了今年的一段时间构建 GRIT——Grouped Reduced-precision Interchange Type(分组低精度交换类型)。这是我对一个简单问题的回答:检查点至少需要携带什么信息,才能让这些故障显式暴露出来?论文已发布在 Zenodo 上,规范和全部五种实现都放在 GitHub 上,下面的所有内容都能从一次干净的检出中复现。
GRIT 数组是一个四元组:(Grade, Placement, Planes, Shape)。Grade 是完整的数值契约——元素格式、缩放因子推导规则、零点约定、舍入、稀疏性等全部内容——它具有规范化的字符串形式和一个 64 位 id。Placement 是物理布局,它作为值携带,而不是硬编码进类型参数中,因此“采用哪种布局”和“采用哪种数值契约”不再被融合成类似 marlin_24 这样的单个枚举名称。Planes 是最多四个字节缓冲区——数据、两个层级的缩放因子以及辅助数据。Shape 由调用方提供。
GRIT 数组是一个四元组——整个四元组都会与字节一起传递
Grade
数值契约:格式 · 缩放规则 · zp 舍入 · 稀疏性
Placement
物理布局,作为值携带——新增布局只需新增一个常量
Planes
最多 4 个字节缓冲区:data · scale0 · scale1 · aux
Shape
秩与各维度大小,携带在视图中
64 字节 POD 描述符
grade + 哈希帧 + shape ⇒ 128 位 gid
每个边界都执行 grit_check——O(1)、完备、无未定义行为
任意 64 字节 → 恰好一个状态 · 相同 gid + 相同 planes ⇒ 位级完全一致的 decode()
这就是四元组。Grade 携带数值契约,Placement 以哈希值的形式携带布局,Planes 携带字节,Shape 来自视图——所有这些信息都会序列化为一个 64 字节描述符,并在每次从生产者跨越到消费者的边界处,以 O(1) 复杂度检查该描述符与字节是否一致。
所有这些信息都能容纳在一个 64 字节的普通旧数据(POD)描述符中——每个字段都位于固定偏移处,采用小端序,不包含任何变长内容。描述符与 shape 共同决定每个平面的精确字节长度,因此,“这个描述符是否与这些字节匹配?”可以在跨越边界时以 O(1) 复杂度判定。而且该检查是完备的:无论向它传入哪 64 个字节,包括恶意构造的垃圾数据,它都会终止并返回且仅返回一个状态,同时不会读取缓冲区之外的数据。
完整契约:64 字节,每个字段位于固定偏移处,采用小端序
magic · ver
levels · flags
elem · scale0
scale1 · zp
axis
k0 · k1
sparse · container
note
placement × 3
data · scale · metadata
grade_id
FNV-1a-64
标志位承载过去依赖惯例传递的信息:零点是否存在及其约定、稀疏性、位顺序、交错方式。note(第 28–32 字节)记录假设的来源——它是唯一一个被排除在所有指纹之外的字段,因此为文件添加注释绝不会改变其标识。描述符 + shape ⇒ 每个平面的精确字节长度 ⇒ “这个描述符是否与这些字节匹配?”的检查复杂度为 O(1)。
这是精确到字节的 64 字节描述符。首先是标识和标志,随后是来自封闭格式阶梯的格式、分组几何、稀疏性与容器打包方式、被排除在指纹之外的 note 字段、三个 64 位 placement id,以及 FNV-1a-64 grade id。grade、哈希帧和 shape 会共同生成一个 128 位 gid:两个工具如果持有相同的 gid,就表示它们持有相同的契约。
不过,我最愿意坚定捍卫的部分并不是描述符,而是附着在 decode() 上的规则集合:NaN 污染、稀疏性优先于污染、切片诚实性、将分组轴转置视为重量化而不是视图、固定为最外层优先的求值顺序、禁止用 FMA 替换、placement 不变性。格式会告诉你字节意味着什么;但几乎没有人明确写出解码器允许做什么。静默分歧正是藏在这里——而且现有语义恰好在这个边缘止步:StableHLO 的 0 < scales 约束明确排除了其中最棘手的情况,因此,这两套语义恰恰在实际实现会产生分歧的地方彼此不相交。
整个项目建立在两项保证之上,而且二者都可以测试:
相同的 gid 和相同的平面字节 ⇒ 在每一种符合规范的实现上,decode() 的结果都位级完全一致;
即使面对恶意构造的 64 字节描述符,grit_check 也绝不会出现未定义行为。
没有人会因为一篇博客文章客气地提了要求,就重写自己的检查点格式。因此,切入点是 grit scan:这是一个只读审计器;存在描述符时,它会对照字节检查声明的契约;不存在描述符时,它会根据容器中的证据推断契约——目前这意味着可以检查你磁盘上已有的 GGUF 和 safetensors 文件。
我用它扫描了四个真实的 Hugging Face 检查点。我最喜欢的发现是:一个 GPTQ 文件和一个 AWQ 文件,其零点平面在字节数、shape、dtype 和张量名称上完全相同——但采用的约定却截然相反。一个按原值存储零点;另一个存储零点减一后的值。把其中一种当作另一种加载,每个权重都会偏移整整一个量化步长。两个文件中都没有任何地方记录内部采用的是哪种约定。
你现在就能运行的实验:两个文件,零点字节完全相同,数值却不同
GPTQ-Int4 · qzeros
0x77 0x77 0x77 0x77 …
相同 shape · 相同 dtype · 相同名称
AWQ-Int4 · qzeros
0x77 0x77 0x77 0x77 …
相同 shape · 相同 dtype · 相同名称
zpc = minus1 : (w − (z+1)) · s
GPTQ 的惯例约定
zpc = asis : (w − z) · s
AWQ 约定
把其中一种当作另一种加载 ⇒ 每个权重都会偏移整整一个量化步长——而且文件会顺利完成加载。GRIT 的解决方案是一个声明式标志位:zpc ∈ { asis, minus1 }——把惯例提升为可检查字段。
这是一个你现在就能下载并观察到的歧义:GPTQ 和 AWQ 的零点平面在字节层面完全相同——shape、dtype、张量名称全都一致——解码后却得到不同数值,因为“减一”约定存在于工具源代码中,而不是文件里。GRIT 的 zpc 标志只需一个声明式比特位,就能终结这种猜测。
这个发现确实让我感到意外。我原本预计扫描器会发现草率的元数据;但没想到,两种部署最广泛的量化家族在字节层面竟然无法形式化地区分,却表示不同的数值。这几乎是整个问题最纯粹的标本:含义并不在文件中。它存在于某个评论讨论串里,也存在于你碰巧使用的那个加载器的源代码里。
grit scan——无需任何采用成本的切入点
发现描述符 → 验证它
没有描述符(目前)→ 推断
大小方程 · gid 差异 · 约定歧义 · grade 漂移
推断得到的契约会被标记为 inferred,绝不会标记为 declared;语法无法表达的内容会报告为 inferred_inexpressible,绝不会进行近似。分级、可用作 CI 门禁的退出码:
0 clean
1 violation
2 disagreement
3 warn
4 parse
pip install grit-datatype && python3 -m grit.scan --deep --json PATH...
grit scan 会在存在已声明描述符时验证它们;在不存在描述符时,则根据 ggml 块结构、llama.cpp 文件类型以及 GPTQ、AWQ、compressed-tensors 家族推断契约——每个推断出的契约都会被标记为 inferred,绝不会标记为 declared;v1 语法无法表达的内容会报告为 inferred_inexpressible,而不是对其进行近似。分级退出码让它可以通过一行命令成为 CI 门禁。
关于扫描器的说法很容易夸大其词,因此这项实地研究同时报告了两轮运行结果,而且先把不尽如人意的部分摆出来。我们下载了四个真实且流行的检查点——GGUF Q4_K_M、GPTQ-Int4、AWQ-Int4、compressed-tensors W4A16——并逐字节读取。第 1 轮采用人工分析,加上最初发布版本的扫描器:四个文件中有三个确认存在真实不匹配,但工具仅间接自动捕获了其中一个。坦率地说,这个成绩不够好。我们恰好补上了三个推断缺口,然后对字节完全相同的文件重新运行。第 2 轮:发现项从 12 个增加到 349 个,现在每一类不匹配都能自动触发检测。让这个数字真正有意义的是:对两个已知无问题文件进行的误报对照测试仍为零个发现项,退出码为 0。完整的实验日志(包括两轮运行)位于仓库的 audit/scan-experiment.md。
四个真实检查点,两轮运行,相同的字节
GGUF Q4_K_M · GPTQ-Int4 · AWQ-Int4 · compressed-tensors W4A16——逐字节读取
第 1 轮——扫描器最初发布时的版本
12 个发现项(人工分析确认 4 个文件中有 3 个存在不匹配;工具间接自动捕获了 1 个)
第 2 轮——补上三个推断缺口,对字节完全相同的文件重新运行
349 个发现项——每一类别均能触发检测
误报对照——两个已知无问题的文件
柱长 ∝ 发现项数量
两轮运行的结构如实报告如下:最初发布版本的扫描器发现 12 项;补上三个推断缺口并重新扫描相同字节后发现 349 项——已知无问题的对照文件则为零个发现项。n=4 个检查点:这证明这些不匹配类别确实存在于实际环境中,而不能说明它们有多普遍。
这个项目的目标是让人检查,而不是让人相信。它包含一份规范性规约(2,164 行)、一个可执行的 Python 参考实现,以及五个零依赖实现——C11、C++20、Rust、仅使用标准库的 Python、严格模式 TypeScript。它们都能复现一套由 SHA-256 固定的 68 向量一致性测试套件,并且在 96/96 个跨语言描述符指纹上逐位一致。这一结果由已提交到仓库的测试工具和一个 CI 作业验证;CI 会在每次推送时重新证明,而不是依靠一次性脚本。Rust 实现经过差分模糊测试;C++ 实现则在 AddressSanitizer 和严格构建模式下运行。
五种实现,每种都零依赖——一份逐位一致的契约,由 CI 证明
C11 910 项检查 · 0 次失败
C++20 801 项检查 · 0 次失败 · ASan+strict
Rust 37 项测试 + 完整一致性测试套件
Python 124 项测试 · 仅使用标准库,从不导入 numpy
TypeScript 91 项测试 · 严格模式
柱长 ∝ 测试套件检查项数量(并非覆盖率)——每个套件均为 0 次失败
跨语言 gid:五种实现与参考实现的 96/96 个 gid 全部一致——共 96 个不同的 gid
共享测试套件:68 个由 SHA-256 固定的向量
34 个正向
15 个负向
16 个指纹
3 个 supersedes
验证面包括:五种各自零依赖的实现、所有测试套件均为零失败、五种实现加参考实现对 96/96 个跨语言 gid 达成一致,以及按类型细分的 68 向量共享一致性测试套件。只需一条命令即可复现跨语言证明:bash spec/crosslang/run.sh。
而且,这项检查足够便宜,可以一直开启。一级结构检查在 C 中耗时 296 ns(887 个周期),在 Rust 中耗时 687 ns;对于一个合成的、包含 8 个分片且大小为 1.07 GB 的 MXFP4 检查点,在加载时检查每个张量只会增加 2.7 ms、33.4 KB 的头部数据,以及用于 192 次检查的 108 KiB 常驻内存。与耗时数秒的检查点加载相比,这个安全保障几乎没有成本。
边界检查的成本——一次调用,一级(结构)
对数刻度(100 ns → 10 µs)· 在一台 i9-13900HK 上测得,仅供参考、不作保证 · 测试工具位于 bench/
C 296 ns(887 个周期)
Rust 687 ns
Python 9.8 µs
完整检查点:1.07 GB · 8 个分片 · 加载时执行 192 次检查
+2.7 ms 墙钟时间 · +33.4 KB 头部数据 · +108 KiB RSS
图中以对数刻度展示一级结构检查的单次调用成本,以及完整检查点的整体情况:对一组总计 1.07 GB 的分片执行 192 次检查,墙钟时间成本为 2.7 ms。这些数据来自一台机器,仅供参考——生成这些结果的测试工具随项目放在 bench/ 中,方法说明见论文 §8.4。
GRIT 不声称什么
之所以有这一节,是因为项目的座右铭也必须适用于它自己的宣传。规约中包含一张逐组件的声明表,其中列出了每个组件最接近的先前工作,并在旁边给出诚实的适用范围说明。参数化量化类型、嵌套的两级缩放,以及在同一种格式中组合缩放与稀疏性,都不是 GRIT 的发明——可参见 MLIR 的子通道量化类型、compressed-tensors 和 Qualcomm 的 LPBQ。将算术契约作为值,是 StableHLO 和 JAX 的 DotAlgorithm 中已经部署的技术。先规范化再生成指纹,逐个步骤都遵循 Apache Avro 的方法——GRIT 改变的是被散列的对象。可移植 POD 描述符模式来自 Khronos Data Format Specification 和 DLPack,连低于一字节数据采用 LSB-first 的打包规则也相同。FNV-1a 指纹用于防止漂移和错误标注,而不是抵御攻击者——这里不声称具备抗碰撞能力。
v1 语法也确实存在缺口,而且这些缺口都在规约中明确列出,没有被掩饰:AMD 的 FNUZ FP8 变体无法表达;没有 codebook/LUT 元素类别,因此 NF4 和 llama.cpp 的 IQ* 系列不在支持范围内;不支持 GPTQ act-order 的 g_idx 分组,因为如果把 act-order 隐藏在 Placement 中,就会导致放置不变性定律不成立;密集半字节零点平面——GPTQ/AWQ 的 qzeros,每字节两个值——在 v1 填充规则下无法按字节表示。扫描器会将这些情况全部报告为 inferred_inexpressible,而不是假装能够支持。此外,实地研究的样本量为 n=4:它证明不匹配类别确实存在于实际环境中,而不能说明这些问题有多普遍——要研究普遍程度,需要对数百个检查点进行分层扫描,这是未来的工作。
GRIT 最初源于一次挑战,而不是一个产品创意:寻找真正缺失的东西,并且只有在这个空白经受住试图证明它早已存在的对抗性检验后,才动手构建。在编写任何代码之前,我们全面梳理并用最接近的先前工作攻击这个想法——MLIR 子通道类型、StableHLO DotAlgorithm、compressed-tensors、Avro、Khronos DFS、DLPack、torchao、TOSA 块缩放类型、OCP MX、IEEE P3109——所有未能经受检验的新颖性声明都在发表前撤回。最终保留下来的不是一种格式,而是一份缺失的契约;这决定了后续的一切:要构建的是一种可检查的类型,而不是又一个容器。
我会坦率说明所采用的方法,因为它也是整个故事的一部分:我与 Claude(Anthropic)密切协作构建了 GRIT——包括规约起草、协调五种语言的实现,尤其是对抗性验证:使用精确算术预言机对每个编码器进行全面扫描;在发布前,让一个敌对的三人评审小组审查论文;针对从 StableHLO、OCP MX 到 P3109 的所有相关工作进行新颖性检索;以及进行引用审计,对照公开记录获取并核验每一项参考资料。工作规则很简单:每一个数字都必须能从干净检出中重新生成;每一条评审意见要么得到修复,要么被记录为限制——绝不弱化措辞。论文中的每个数字都能追溯到仓库中的制品。整个项目的座右铭同样适用于它自己的构建过程:这里没有任何内容要求你相信;一切都要求你检查。
在你已有的检查点上试一试
# the scanner — zero adoption required
pip install grit-datatype
python3 -m grit.scan --deep --json path/to/checkpoints/
# the type, in your language of choice
cargo add grit-datatype
npm install grit-datatype
# reproduce every claim in this post from a clean checkout
bash spec/crosslang/run.sh # 96/96 on every implementation
GitHub 上的 GRIT——规范性规约、论文、全部五种实现、基准测试和实地研究日志
项目网站 · 交互式工作台——在浏览器中构建并破坏描述符字节,观察检查如何捕获问题;它运行的是字节完全相同的 npm 构建版本
项目网站 · 交互式工作台——在浏览器中构建并破坏描述符字节,观察检查如何捕获问题;它运行的是字节完全相同的 npm 构建版本
论文——DOI 10.5281/zenodo.21817716
论文——DOI 10.5281/zenodo.21817716
PyPI · crates.io · npm——仓库标签与三个注册表始终同步更新
PyPI · crates.io · npm——仓库标签与三个注册表始终同步更新
在你已有的检查点上试试这个扫描器。如果它发现了我没有预测到的问题,那将是你所能告诉我的最有价值的信息。
GRIT 采用 Apache-2.0 许可证开源,由人类–AI 团队以开放方式构建——作者负责方向、约束和证据标准,Claude(Anthropic)担任仓库共同作者。成本数据是在一台机器上测得的,仅供参考;生成这些数据的测试工具位于仓库中。实地研究样本量为 n=4,它声称的是这些不匹配类别确实存在,而不是其普遍程度。
如需采取进一步措施,你可以考虑屏蔽此人和/或举报滥用行为。