
都叫INT8,两个输入也不一定同为i8。看VNNI内核前,我先查具体指令的乘数类型;把负数缓冲原样当无符号读取,会改变它代表的数。
Rust _mm512_dpbusd_epi32文档说明:a中相邻四个u8与b中对应i8相乘,再与i32累加器合并;接口要求AVX-512 VNNI目标特性,文档标记自Rust 1.89.0稳定。本机aarch64没有执行该指令。

把 i8 变成 u8 后,多出来什么
若有符号输入x映射为u=x+128,则sum(uw)=sum(xw)+128sum(w)。因此需要减回128sum(w)。这里的+128是在更宽类型里算完再表示为u8,不是对i8做溢出加法,也不是简单按位重解释。
fn signed_dot(x:&[i8],w:&[i8])->i64{
assert_eq!(x.len(),w.len());
x.iter().zip(w).map(|(&a,&b)|i64::from(a)*i64::from(b)).sum()
}
fn shifted_dot(x:&[i8],w:&[i8])->i64{
assert_eq!(x.len(),w.len());
let raw:i64=x.iter().zip(w).map(|(&a,&b)|i64::from(i16::from(a)+128)*i64::from(b)).sum();
let sum_w:i64=w.iter().map(|&v|i64::from(v)).sum();
raw-128*sum_w
}
fn main(){
let x=[-128,127,-1,0];let w=[1,-1,2,3];
assert_eq!(signed_dot(&x,&w),-257);
assert_eq!(shifted_dot(&x,&w),-257);
for n in 0..=17{
let x:Vec<i8>=(0..n).map(|i|((i*13)%256) as u8 as i8).collect();
let w:Vec<i8>=(0..n).map(|i|((i*7)%256) as u8 as i8).collect();
assert_eq!(signed_dot(&x,&w),shifted_dot(&x,&w));
}
println!("signed/unsigned compensation: boundary case + 18 lengths passed");
}
rustc 1.98.0执行通过,边界构造例为-257,0–17长度也保持代数等价。代码全部使用标量i64参考;它没有模拟每个i32 lane的溢出或VNNI硬件执行时序。
真实量化还有其他零点
如果激活和权重各自带zero point,展开(x-zx)(w-zw)还会出现其他修正项。不能把本例只针对有符号偏移的补偿公式当成所有非对称量化公式。
指令每四对归约的布局,也需要与矩阵打包方式匹配。尾块如何补零要考虑量化零点,不能随意把内存字节0当成数值0。相邻指令还可能有不同饱和语义,应按完整名称查文档,不因名字接近就互换。
累加宽度先给界
小例使用i64避免在参考计算中混进i32回绕,但真实内核使用的i32累加器有范围。先按最大乘积与归约长度给出上界,再决定分块、更宽中间值或溢出策略。之后才逐lane比较硬件与标量结果。
本轮没有测吞吐、GEMM或模型效果,不能从几条断言推导INT8端到端性能。对我而言,先把有符号和无符号这件小事算清楚,比给一个其实算错的内核加“算力狂飙”更实在。
本文深入解析AVX-512 VNNI指令中u8×i8矩阵乘法的实现机制,重点阐述有符号INT8权重与无符号INT8激活混合运算时的数值映射关系;指出将i8直接 reinterpret 为u8会引入+128∑w偏差,需显式补偿;进一步说明非对称量化中零点(zero point)带来的额外修正项,以及累加器位宽(i32)对溢出边界的影响。强调硬件指令语义、打包布局与量化数学的一致性。
1049

被折叠的 条评论
为什么被折叠?



