FEATURED · 精选文章

【Bug已解决】CPU EP mis-loads packed `UINT2` Constant initializer (treats `UInt2x4` as unpacked storage;…

发布时间 / 2026/8/13 22:56:29
来源 / 创域科博编辑部
栏目 / 资讯中心
【Bug已解决】CPU EP mis-loads packed `UINT2` Constant initializer (treats `UInt2x4` as unpacked storage;… 【Bug已解决】CPU EP mis-loads packedUINT2Constant initializer (treatsUInt2x4as unpacked storage;INT2unaffected) 解决方案一、现象长什么样模型里有一个被量化成2-bit的权重常量用 ONNX 的UINT2打包数据类型在每个字节里塞 4 个 2-bit 元素UInt2x4存成Constant初始化器。在 ONNX Runtime 的 CPU EP 上加载并推理结果全是垃圾值和预期差很远余弦相似度接近 0或分类输出全错 —— 但同一个模型在 GPU EP 上结果正确最小触发import onnxruntime as ort # 模型含一个 UINT2 打包的 Constant 初始化器 sess ort.InferenceSession(uint2_model.onnx, providers[CPUExecutionProvider]) out sess.run(None, feed)[0] # 结果错误换成 providers[CUDAExecutionProvider] 结果正确排查发现CPU EP 把UInt2x4当成了未打包的存储来读——也就是按“每字节一个元素”去解释那块 buffer而实际上每字节装了 4 个 2-bit 元素。偏偏INT22-bit 有符号是正确的只有UINT2错。这是典型的“打包数据类型反序列化漏了一种”。二、背景ONNX 从 opset 21 起支持 2-bit 和 4-bit 的“打包”数据类型UINT2/INT2每字节 4 个、UINT4/INT4每字节 2 个。打包后的常量在 proto 里表现为外层 tensor 的elem_type是UINT2dims描述的是打包后的形状比如原始 1024 个元素打包成 256 个字节dims[256]原始元素个数靠data_type 一个packed属性或约定推断。加载时运行时需要识别elem_type UINT2按“每字节 4 个元素”做位解包bit-unpack还原出 1024 个 0~3 的值把解包后的张量交给后续算子。CPU EP 的常量加载器tensor deserializer对INT2写了正确的解包路径但对UINT2走错了分支——直接把dims[256]的字节 buffer 当 256 个完整uint8元素用没解包。于是 256 个字节被当成 256 个值范围 0255而不是 1024 个 03 的值形状也对不上后续算子拿到完全错误的数据。三、根因根因是CPU EP 的常量加载器对UINT2和INT2的处理不对称解包分支漏了UINT2加载器里有一张elem_type - 加载函数的映射INT2指向了“位解包到 int8”的实现UINT2却错误地指向了“原样按 uint8 读”的默认实现或根本没在映射里fallback 到逐字节读。形状未还原因为没解包dims停留在打包后的[256]而下游算子期望[1024]读出来的元素个数和语义全错。INT2 正常对照INT2因为有正确的解包实现所以无影响——这进一步说明不是“打包机制整体坏了”而是UINT2这一支被漏掉。所以这不是模型算错而是CPU EP 反序列化 2-bit 数据时漏了对无符号UINT2的解包把它当成了未打包的 uint8。四、最小可运行复现下面用 Python NumPy 模拟“UINT2 打包数据被错误地当 uint8 读取”的偏差并给出正确的位解包import numpy as np def wrong_load(packed: np.ndarray) - np.ndarray: CPU EP 当前的错误做法把每字节当 1 个 uint8 元素。 return packed.astype(np.uint8) # 256 个值值域 0~255且未解包 def correct_unpack_uint2(packed: np.ndarray) - np.ndarray: 正确做法每字节拆出 4 个 2-bit 无符号元素低位在前。 packed packed.astype(np.uint8) out np.zeros(packed.shape[0] * 4, dtypenp.uint8) for i in range(4): out[i::4] (packed (2 * i)) 0x03 return out if __name__ __main__: # 打包前原始 4 个元素 [0,1,2,3] - 打包成 1 字节 0b11100100? 计算 raw np.array([0, 1, 2, 3], dtypenp.uint8) packed np.zeros(1, dtypenp.uint8) for i, v in enumerate(raw): packed[0] | (v 0x03) (2 * i) print(packed byte:, int(packed[0])) # 0b00001111 15 wrong wrong_load(packed) right correct_unpack_uint2(packed) print(错误加载:, wrong, 形状, wrong.shape) # [15] 错 print(正确解包:, right, 形状, right.shape) # [0 1 2 3] 对 assert np.array_equal(right, raw)跑出来错误加载得到[15]一个 uint8正确解包得到[0 1 2 3]四个 2-bit 元素。这正好复现了 CPU EP 把UInt2x4当未打包存储读错的现象。五、解决方案第一层最小直接修复最小修复在常量加载器里给UINT2补上和INT2对称的解包实现。对使用者来说临时规避是把模型里的UINT2常量在导出时改成UINT4或UINT8未打包绕开 CPU EP 的UINT2解包 bug或者干脆用 GPU EP 跑GPU EP 解包正确。对 ORT 仓库侧加载器映射应改成// 伪代码常量加载器的 elem_type 分支 switch (elem_type) { case ONNX_NAMESPACE::TensorProto::UINT2: return LoadPackeduint8_t, 2, /*signed*/false(tensor); // 补这一支 case ONNX_NAMESPACE::TensorProto::INT2: return LoadPackedint8_t, 2, /*signed*/true(tensor); case ONNX_NAMESPACE::TensorProto::UINT4: return LoadPackeduint8_t, 4, /*signed*/false(tensor); case ONNX_NAMESPACE::TensorProto::INT4: return LoadPackedint8_t, 4, /*signed*/true(tensor); default: return LoadPlain(tensor); }LoadPacked负责按 bits2 做位解包并把dims从打包形状还原成原始元素个数dim[0] * 4。这一层立刻让UINT2常量被正确解包。六、解决方案第二层结构性改进把“哪些打包类型需要解包、如何解包”收口成唯一的配置对象OrtCpuUint2ConstantPolicy加载器读它避免再漏类型from dataclasses import dataclass, field from typing import Dict, Tuple dataclass(frozenTrue) class OrtCpuUint2ConstantPolicy: CPU EP 打包常量加载的单一事实来源。 # 打包类型 - (每字节元素数, 是否有符号) packed_types: Tuple[str, ...] (UINT2, INT2, UINT4, INT4) bits_per_element: Dict[str, int] field(default_factorylambda: { UINT2: 2, INT2: 2, UINT4: 4, INT4: 4, }) signed: Dict[str, bool] field(default_factorylambda: { UINT2: False, INT2: True, UINT4: False, INT4: True, }) # 是否必须解包True 表示不能当 uint8 原样读 must_unpack: Tuple[str, ...] (UINT2, INT2, UINT4, INT4) # 字节内位序低位在前 lsb_first: bool True def needs_unpack(self, elem_type: str) - bool: return elem_type in self.must_unpack def unpack_shape_scale(self, elem_type: str) - int: return 8 // self.bits_per_element.get(elem_type, 8) def describe(self) - str: return UINT2/INT2/UINT4/INT4 全部走对称位解包无符号与有符号一致 POLICY OrtCpuUint2ConstantPolicy() def plan_load(elem_type: str, policy: OrtCpuUint2ConstantPolicy POLICY) - dict: return { unpack: policy.needs_unpack(elem_type), scale: policy.unpack_shape_scale(elem_type), signed: policy.signed.get(elem_type, False), }所有加载逻辑读同一份POLICY新增打包类型只要在packed_types里加一项就自动覆盖不会再出现“INT2 有、UINT2 漏”的不对称。七、解决方案第三层断言 / CI 守护把“UINT2 必须解包、结果与 GPU EP 一致”做成断言。下面用 pytest 风格守护复用第四节解包逻辑import numpy as np def test_uint2_must_unpack(policy): assert policy.needs_unpack(UINT2) is True assert policy.needs_unpack(INT2) is True def test_uint2_symmetrical_with_int2(policy): # UINT2 与 INT2 应走相同的解包机制仅符号不同 assert policy.bits_per_element[UINT2] policy.bits_per_element[INT2] assert policy.signed[UINT2] is False assert policy.signed[INT2] is True def test_unpack_recovers_values(): raw np.array([0, 1, 2, 3], dtypenp.uint8) packed np.zeros(1, dtypenp.uint8) for i, v in enumerate(raw): packed[0] | (v 0x03) (2 * i) assert np.array_equal(correct_unpack_uint2(packed), raw) def test_shape_restored_after_unpack(policy): scale policy.unpack_shape_scale(UINT2) assert scale 4 # 每字节 4 个元素这四组断言锁住(1)UINT2必须解包(2)UINT2/INT2对称仅符号差异(3) 解包还原正确值(4) 形状按 4 倍还原。CI 跑通即代表 CPU EP 的 2-bit 加载对称正确。八、排查清单遇到 CPU EP 上 2-bit 量化模型结果错、GPU EP 正常先换 EP 测GPU EP 结果对、CPU EP 错 → 多半是 CPU 反序列化问题。看常量 elem_type模型里是不是UINT2打包常量INT2正常可对照。检查加载器映射UINT2有没有指向位解包实现还是 fallback 到逐字节读。临时规避导出时把UINT2改UINT4/UINT8或暂用 GPU EP。根本修复给UINT2补对称解包并还原dims。统一策略对象用OrtCpuUint2ConstantPolicy固化新增打包类型自动覆盖。CI 守护断言UINT2解包正确、与INT2对称防止回归。九、小结CPU EP mis-loads packed UINT2 Constant initializer的根因是 CPU EP 的常量加载器对 2-bit 数据类型处理不对称INT2走了正确的位解包而UINT2漏了分支、被当成未打包的 uint8 逐字节读取导致值错误、形状未还原而 GPU EP 解包正确所以无影响。最小修复是给UINT2补上和INT2对称的解包实现并还原形状结构性改进是用唯一的OrtCpuUint2ConstantPolicy把打包类型的处理收口CI 用四组断言守护“UINT2 必须解包、与 INT2 对称、形状还原、值正确”。记住2-bit 打包数据在 CPU 反序列化时必须位解包无符号和有符号要走同一套机制、只差符号位。
RELATED — 相关阅读

相关资讯

LATEST — 最新资讯

最新发布

TODAY — 本日精选

新闻

WEEKLY — 本周精选

新闻

MONTHLY — 本月精选

新闻