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

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

摘要:模型量化文章在 CSDN 算法频道近期有较高讨论度,但很多讲法会直接跳到公式,让人误以为 GPTQ 只是“更聪明的 round”。本文用一个 3×4 小矩阵复现实验说明:逐列量化真正有价值的地方,是把当前列造成的输出误差补偿到后续列。

如果只看量化后的权重,GPTQ 很容易被误解成“把浮点数四舍五入到最近刻度”。这种理解只能解释权重为什么变小,解释不了为什么同样是低比特表示,有的量化方法对推理输出伤害更小。关键差异在于:量化不是只关心每个权重本身,还要关心这些权重会怎样和输入激活相乘。

这篇文章不推完整论文公式,而是做一个小实验。我们把权重矩阵按列处理,每一列被量化后产生误差,再用一个近似的逆 Hessian 矩阵把误差分摊到还没处理的列。这个过程很像整理账本:当前列已经“结账”,后面的列要根据相关性调整余额,尽量让最终输出别偏太远。

实验目标:同一组样本,比较两种量化

第一种方法是朴素量化:每个权重都直接四舍五入到 0.2 的刻度。第二种方法是简化版 GPTQ:仍然逐个权重量化,但在处理第 col 列后,把该列误差按 H_inv[col][nxt] 传给后续列。

H_inv 在真实场景里来自校准数据的二阶信息;本文只用一个手写小矩阵,目的是让你看清补偿机制,而不是复现工业级量化库。代码中用若干输入样本计算量化前后的输出均方误差,输出越小,说明量化对这些样本的影响越小。

def quantize(x, step=0.2):
    return round(x / step) * step

def matmul_vec(matrix, vec):
    return [sum(row[j] * vec[j] for j in range(len(vec))) for row in matrix]

def mse(a, b):
    return sum((x - y) ** 2 for x, y in zip(a, b)) / len(a)

def output_mse(W, Q, samples):
    return sum(mse(matmul_vec(W, x), matmul_vec(Q, x)) for x in samples) / len(samples)

def naive_quantize(W, step=0.2):
    return [[quantize(v, step) for v in row] for row in W]

def gptq_like_quantize(W, H_inv, step=0.2):
    work = [row[:] for row in W]
    rows, cols = len(work), len(work[0])
    out = [[0.0] * cols for _ in range(rows)]

    for col in range(cols):
        diag = H_inv[col][col]
        if abs(diag) < 1e-12:
            raise ValueError("H_inv diagonal must be non-zero")
        for r in range(rows):
            q = quantize(work[r][col], step)
            out[r][col] = q
            err = (work[r][col] - q) / diag
            for nxt in range(col + 1, cols):
                work[r][nxt] -= err * H_inv[col][nxt]
    return out

def run_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],
    ]

    naive = naive_quantize(W, 0.2)
    compensated = gptq_like_quantize(W, H_inv, 0.2)
    naive_err = output_mse(W, naive, samples)
    compensated_err = output_mse(W, compensated, samples)

    assert compensated != naive
    assert compensated_err < naive_err

    print("naive_mse =", round(naive_err, 6))
    print("gptq_like_mse =", round(compensated_err, 6))
    print("better =", compensated_err < naive_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、列排序或工程库实现,会清楚很多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值