【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 反序列化时必须位解包无符号和有符号要走同一套机制、只差符号位。

相关新闻

从零开始搭建个人服务器 网站建设:一份写给独立开发者的真心话

从零开始搭建个人服务器 网站建设:一份写给独立开发者的真心话

在这个互联网巨头垄断流量、平台算法决定生死的大环境下,越来越多的技术人和内容创作者开始转向一种更底层、更本质,却也更具掌控力的生活方式——那就是通过个人服务器 网站建设 来实现数字空间的绝对自主。这不仅仅是一个技术项目的执行,更像是一场关于自由、隐私以及自我…

2026/10/30 15:30:21 阅读更多 →
从Softmax与交叉熵损失原理到NumPy实现:掌握分类任务核心梯度推导

从Softmax与交叉熵损失原理到NumPy实现:掌握分类任务核心梯度推导

1. 项目概述:从公式到代码的深度穿越 如果你正在入门深度学习,尤其是分类任务,那么“Softmax 交叉熵损失”这个组合对你来说,就像学开车必须先学会踩油门和刹车一样基础。但很多人可能只是调用了 torch.nn.CrossEntropyLoss() …

2026/10/31 14:06:29 阅读更多 →
搜索引擎爬虫触发LLM安全模糊测试:从Applebot现象看新型应用防御

搜索引擎爬虫触发LLM安全模糊测试:从Applebot现象看新型应用防御

最近在安全圈和AI圈的交界处,一个看似不起眼的变化正在发生:苹果的搜索引擎爬虫 Applebot,开始频繁地出现在一些大型语言模型(LLM)的访问日志里。这听起来可能只是技术栈里又多了一个用户代理字符串,但如果…

2026/10/31 23:59:06 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/1 0:24:43 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/1 10:36:53 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/1 10:36:51 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/1 1:06:58 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/1 10:35:11 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/1 9:35:41 阅读更多 →

日新闻

周新闻

月新闻