系列目录 · 数值与量化 · Read in English
“两个整数相乘,结果和计算器一致,模型还是不对。”这时候盯着乘号往往没有用。计算器只替你检查了一张账单:整数乘积。模型还在看另一张账单:这些整数分别代表多大的实数。
量化乘法最有迷惑性的地方,是底层每一步都可能“合法”,最后拼起来却不是原来的运算。
一、先把三个量化域写在纸上
定义输入与输出:
1 | a = s_a * (q_a - z_a) |
由 y=a*b 得:
1 | q_y ≈ round[(s_a*s_b/s_y)*(q_a-z_a)*(q_b-z_b)] + z_y |
乘法的比例是两个输入尺度的乘积再除以输出尺度。加法则是两路输入分别换算到输出域后相加,不能把为加法准备的公共参数规则不加审视地拿来用。
教学例子取 s_a=0.02, s_b=0.05, s_y=0.01,零点均为零,输入整数为 10 和 6。实数分别是 0.2、0.3,乘积为 0.06,对应输出整数 6。整数乘积却是 60,还要乘 0.1。如果错误地只用第一路尺度比 2,结果会完全偏离。
历史补丁为乘法增加了独立参数生成,显式计算两输入尺度乘积与输出尺度之比,并携带三个零点。这是数学账单和寄存器账单对齐的关键一步。
二、整数 multiplier 和 shift 只是系数的近似表示
目标执行单元通常不直接存一个任意实数系数,而是使用类似 M / 2^r 的形式。设真实系数为 α,近似为 α̂,则在未饱和、舍入规则固定的简化条件下:
1 | 输出误差的一部分 ≈ (α̂-α) * (q_a-z_a) * (q_b-z_b) |
所以同一系数误差,在小输入上不明显,在极端输入上会被乘积放大。随机小数值测试很容易给出过度乐观的印象。
系数表示还要回答:M 是有符号还是无符号?r 能否为负?左移前的乘积是否有足够位宽?右移负数采用什么规则?“先加舍入偏置再移位”对负数是否符合目标定义?这里不存在一个脱离执行单元契约的万能模板。
历史差异能证明系数计算和参数位置改变了,不能证明所有极端组合都已满足误差上界。适合公开写作的说法是“修正了公式对应关系”,后面再列出需要验证的位宽与舍入条件。
三、标量不仅是 shape 小,它还有来源
图里一个标量常量,到了底层可能来自立即数、参数寄存器、局部存储中的一个元素,或某个可寻址缓冲区。指令需要知道如何读取它。
历史补丁从内核到发射接口再到底层编码,贯穿增加了标量来源控制,并让乘法识别左右两侧的标量形状。只在最上层加一个布尔值,却没传到编码器,行为不会改变;只在编码器加字段而调用者沿用默认值,也可能继续读错位置。
这种问题的症状常常不像量化错误。若标量实际上读到了旧寄存器值,结果可能随着前一个算子变化;若误按向量读取一个元素的缓冲区,首元素正确、后续元素异常。它们也可能被全零初始化遮住。
可以把契约写成两个独立判断:
1 | operand_kind = scalar 或 vector |
两个维度不要合成“是不是常量”。常量可以是一整块向量;运行时值也可以是标量。来源和形状是不同属性。
四、交换乘法两边,为什么仍然需要测试?
在实数数学里 ab=ba。但底层接口可能对两路输入有不同寻址、广播或类型限制。历史修改显式补充了乘法左输入的标量识别,说明“只支持右边标量”的默认假设值得单独审查。
测试应当比较 tensor * scalar 与 scalar * tensor,同时让两路尺度和零点不同。否则交换后即使系数绑定错误,也可能因为参数相同而不显现。
对于减法,情况更严格:scalar - tensor 与 tensor - scalar本就不等价。复用二元内核接口时,不应因为乘法可交换就把交换策略推广到所有运算。历史相关改动还触及加减的标量来源传递,但这不等于它们所有左右标量组合都已经完整支持。
五、平方是一个适合暴露“复用错了”的小算子
历史中增加过仅支持指数 2 的幂操作,底层复用乘法思路,并对其他指数报错。这样做避免为了 x²引入完整幂函数,范围明确,也容易检查。
但把同一个输入接到乘法两端,不代表数值参数可以忽略:
1 | q_square ≈ round[(s_x^2 / s_out)*(q_x-z_x)^2] + z_out |
尺度要平方,零点要先扣除。若只平方 q_x,当 z_x非零时,会额外出现交叉项和常量项。教学输入 q_x=z_x 时表示实数零,平方输出就应该表示实数零,这是特别便宜却有效的断言。
同样,不应因为后续修正了普通乘法的尺度构造,就自动宣布所有复用乘法的平方路径也已修好。必须追踪实际调用的参数生成路径。
六、怎样排查:先判读错,再判算错
一个高效的诊断顺序是:先确认标量来源与地址正确,再确认整数运算,再确认量化尺度,最后分析舍入与饱和。否则你可能用修改 shift 的方式“修好”某个错误读取的标量,换一组数据又坏掉。
建议为调试输出准备一份语义记录:两输入的形状、存储类型、scale、zero point、广播方式、输出范围、M 和 r。记录不必暴露内部地址给最终产品用户,但对编译器开发者,它比一长串机器字更有解释力。
| 维度 | 待执行组合 | 目的 |
|---|---|---|
| 形状 | 向量×向量、左右标量、单元素 | 覆盖来源与广播 |
| 量化域 | 同尺度、异尺度、非零零点 | 覆盖完整公式 |
| 数据 | 实数零、正负、极值、舍入边缘 | 分离偏移、溢出与误差 |
| 操作 | mul、square、加减对照 | 防止错误复用参数逻辑 |
| 发射 | 文本字段、二进制字段 | 保证控制位传递到底层 |
性能方面,标量广播可减少参数流量;但若为了广播增加大量短循环,配置和分支成本也会上升。平方复用乘法可能复用已有基础设施,不代表所有目标都比专用平方指令快。应观察的量包括:有效元素吞吐、参数加载次数、广播物化大小和中间缓冲区。
乘号很简单,围绕乘号的约定并不简单。把“值从哪里来”和“这个值代表什么”分开记录,很多看似玄学的数值问题就会变成两张可以逐项核对的账单。
七、用三组输入把不同错误分开
第一组只测试实数零:让每路输入整数等于自己的零点。正确乘积应仍为实数零,输出整数应等于输出零点。若失败,优先检查零点位置与来源,而不是系数精度。这样可以避免拿一堆随机误差去猜是偏移错还是乘子错。
第二组选择中心化整数为一的小值,令量化比例可以精确表示。此时输出主要检验尺度公式和参数绑定。左右输入故意使用不同尺度,交换操作数后同时交换各自类型与数值,数学结果应保持;若结果变化,要查看左标量支持、两路类型绑定和寄存器选择。不要只交换数据指针而保留类型不动,那会制造另一个本来就不等价的实验。
第三组再进入极值与舍入边界。它检验中间乘积位宽、固定点系数误差及饱和规则。三组分开运行,可以让一次失败有较明确的解释,而不是只给出一个总平均误差。特别是饱和值,误差可能看起来变小,因为两个不同错误都被夹到同一个端点,所以报告还应单列饱和样本。
标量来源问题可以用连续两次调用暴露。先执行一个使用标量甲的操作,再执行相同形状但标量乙不同的操作;然后交换顺序。如果第二次结果依赖第一次的参数,很可能存在未加载、错误复用或来源字段不一致。这里描述的是通用诊断方法,不是从历史差异确认的运行故障。
指令接口扩展还应审查默认参数。新增控制位若默认关闭,旧调用点是否确实都应该关闭?一个调用点没有编译错误,只说明函数签名兼容,不说明新语义已经正确传入。针对标量左输入、标量右输入和两个向量,列出每个调用点最终应该选择的来源,是一种简单有效的迁移检查。
最后注意错误处理:获取量化信息失败时,应让失败沿调用链清楚传播。若辅助函数返回一个全零参数对象,而调用者继续发射,编译可能完成,结果却难以解释。
If you like this blog or find it useful for you, you are welcome to comment on it. You are also welcome to share this blog, so that more people can participate in it. All the images used in the blog are my original works or AI works, if you want to take it,don't hesitate. Thank you !