四舍五入输在哪里:用小矩阵复现 GPTQ 的误差补偿h

📅 2026/8/2 12:10:25
四舍五入输在哪里:用小矩阵复现 GPTQ 的误差补偿h
四舍五入输在哪里用小矩阵复现 GPTQ 的误差补偿摘要模型量化文章在 CSDN 算法频道近期有较高讨论度但很多讲法会直接跳到公式让人误以为 GPTQ 只是“更聪明的 round”。本文用一个 3×4 小矩阵复现实验说明逐列量化真正有价值的地方是把当前列造成的输出误差补偿到后续列。如果只看量化后的权重GPTQ 很容易被误解成“把浮点数四舍五入到最近刻度”。这种理解只能解释权重为什么变小解释不了为什么同样是低比特表示有的量化方法对推理输出伤害更小。关键差异在于量化不是只关心每个权重本身还要关心这些权重会怎样和输入激活相乘。这篇文章不推完整论文公式而是做一个小实验。我们把权重矩阵按列处理每一列被量化后产生误差再用一个近似的逆 Hessian 矩阵把误差分摊到还没处理的列。这个过程很像整理账本当前列已经“结账”后面的列要根据相关性调整余额尽量让最终输出别偏太远。实验目标同一组样本比较两种量化第一种方法是朴素量化每个权重都直接四舍五入到 0.2 的刻度。第二种方法是简化版 GPTQ仍然逐个权重量化但在处理第 col 列后把该列误差按 H_inv[col][nxt] 传给后续列。H_inv 在真实场景里来自校准数据的二阶信息本文只用一个手写小矩阵目的是让你看清补偿机制而不是复现工业级量化库。代码中用若干输入样本计算量化前后的输出均方误差输出越小说明量化对这些样本的影响越小。defquantize(x,step0.2):returnround(x/step)*stepdefmatmul_vec(matrix,vec):return[sum(row[j]*vec[j]forjinrange(len(vec)))forrowinmatrix]defmse(a,b):returnsum((x-y)**2forx,yinzip(a,b))/len(a)defoutput_mse(W,Q,samples):returnsum(mse(matmul_vec(W,x),matmul_vec(Q,x))forxinsamples)/len(samples)defnaive_quantize(W,step0.2):return[[quantize(v,step)forvinrow]forrowinW]defgptq_like_quantize(W,H_inv,step0.2):work[row[:]forrowinW]rows,colslen(work),len(work[0])out[[0.0]*colsfor_inrange(rows)]forcolinrange(cols):diagH_inv[col][col]ifabs(diag)1e-12:raiseValueError(H_inv diagonal must be non-zero)forrinrange(rows):qquantize(work[r][col],step)out[r][col]q err(work[r][col]-q)/diagfornxtinrange(col1,cols):work[r][nxt]-err*H_inv[col][nxt]returnoutdefrun_tests():W[[0.02,0.78,0.06,-0.30],[-0.03,-1.32,-1.28,0.57],[1.35,0.26,-0.30,-0.92],]H_inv[[1.05,0.58,0.32,0.05],[0.58,1.23,-0.32,0.02],[0.32,-0.32,1.28,0.09],[0.05,0.02,0.09,1.03],]samples[[1.0,0.2,-0.4,0.7],[-0.3,1.1,0.5,-0.2],[0.8,-0.6,0.9,0.1],[0.0,0.4,-1.2,1.0],]naivenaive_quantize(W,0.2)compensatedgptq_like_quantize(W,H_inv,0.2)naive_erroutput_mse(W,naive,samples)compensated_erroutput_mse(W,compensated,samples)assertcompensated!naiveassertcompensated_errnaive_errprint(naive_mse ,round(naive_err,6))print(gptq_like_mse ,round(compensated_err,6))print(better ,compensated_errnaive_err)if__name____main__:run_tests()本地运行结果naive_mse 0.013173 gptq_like_mse 0.010579 better True观察记录一权重误差不等于输出误差朴素量化追求的是每个权重离最近刻度最短。例如 0.78 会变成 0.8-1.32 会变成 -1.4 或 -1.2取决于刻度位置。这个操作看上去很合理但它没有考虑输入样本。如果某一列权重经常和较大的激活值相乘那么这列的一点误差就会被放大如果两列在输出中具有相关性当前列的误差也可以由后续列部分抵消。简化版 GPTQ 的核心就在这里处理当前列后不假装误差消失而是把误差沿着列相关性传到未来。代码中的 work 矩阵就是“还没结账的余额”。out 保存已经定下来的量化权重work 保存经过补偿后的后续权重。列顺序为什么会影响结果逐列算法有一个天然特点先处理的列不能再改后处理的列可以吸收前面传来的误差。因此列顺序会影响最终结果。教学代码按自然顺序从第 0 列处理到最后一列方便观察真实实现往往会根据列的重要性、分块策略或数值稳定性调整顺序。这个细节解释了一个常见现象同样是 GPTQ不同库、不同参数、不同 group size 的结果可能不完全一致。如果把量化想成“每个元素独立压缩”列顺序似乎不该重要但一旦承认误差会在列之间传递顺序就成了算法的一部分。前面的列越重要越应该谨慎量化后面的列越相关越能承担补偿。小矩阵里这种差别只是几个小数点放到大模型层里就可能体现为困惑度、准确率或生成稳定性的变化。校准集不是装饰品很多同学复现实验时会直接拿随机输入当校准样本然后发现指标很好看。问题是随机输入并不一定像真实请求。语言模型的激活分布会受 tokenizer、提示词长度、上下文主题和层位置影响视觉模型的激活又会受图像亮度、分辨率和预处理影响。如果校准集只覆盖了很窄的输入量化方法会学到一套对测试样本好看、对真实流量未必可靠的补偿。实际项目里校准集可以不大但要代表真实分布。比如文本模型至少覆盖短问答、长上下文、代码片段和多轮对话分类模型要覆盖主要类别和容易混淆的边界样本。量化完成后也不要只看平均误差还要观察最差样本。平均值下降但少数样本严重劣化在线体验仍然可能变差。观察记录二补偿不是一定赢但要用校准样本衡量上面的测试中朴素量化的均方误差是 0.013173补偿后降到 0.010579。这个差距来自我们手写的 H_inv 和样本集合。换一批样本、换一种刻度、换一个列顺序结果可能不同。真实工程不会只拿一个小矩阵下结论而会准备校准集、统计各层误差并关注最终任务指标。当你把量化实验接入模型推理原型、自动评测脚本或多模型对比面板时可以把 https://haerapi.com 作为一个可自行评估的 API 接入选择但量化误差、校准集和回归测试应留在自己的工程链路里用可复现数据说话。复杂度分析设权重矩阵有 R 行 C 列。朴素量化只遍历每个权重一次时间复杂度 O(RC)额外空间 O(RC) 或原地 O(1)。本文的补偿版在每一列量化后会更新所有后续列时间复杂度 O(RC²)需要保存工作矩阵和输出矩阵空间复杂度 O(RC)。真实 GPTQ 会用分块、列重排和矩阵运算优化避免在大模型上按教学代码的方式运行。边界条件H_inv 对角线不能为 0否则误差缩放会除零。量化步长必须大于 0步长越大压缩越狠误差通常也越大。样本维度必须等于权重列数否则矩阵乘法无意义。补偿只更新尚未量化的列已经确定的列不能再被改动。校准样本要覆盖真实输入分布否则测试误差低不代表上线效果好。常见错误第一只比较权重矩阵的绝对误差不比较模型输出误差。第二把 H_inv 当成随便填的调参表忽略它来自校准数据。第三量化后只跑单个样例没看边界输入。第四把教学代码直接搬到大模型权重上结果时间复杂度爆炸。第五误以为 GPTQ 会让所有层都变好实际还要看层敏感度和任务指标。可复制测试用例直接运行上面的脚本应该看到补偿版误差小于朴素版。你可以把 step 改成 0.4观察误差差距如何变化也可以把 samples 中第二个样本删掉看补偿优势是否稳定。这个练习能帮助你建立一个判断量化方法要放在样本输出上评估而不是只看权重文件变小。总结GPTQ 的直觉不是“更会四舍五入”而是“当前列量化后把对输出有影响的误差补到未来列”。这正是它比朴素 round 更像算法的地方它利用校准数据中的二阶信息让低比特权重尽量保留原模型在常见输入上的行为。理解了这个小矩阵实验再去看分块 GPTQ、列排序或工程库实现会清楚很多。