qinghuan
← 回到知识探索

AI 直觉 / 动手实验

一个特别大的数,为什么让其余量化值全变成零?

给同一张小矩阵换三种刻度,再用两种输入计算输出,分清保护大值、保留小值和任务误差之间的取舍。

展开本文目录
  1. 下载完整练习包
  2. 先动手看看
  3. 先把坐标和尺子的规则写清楚
  4. 整张共用刻度,最大的数决定了格子宽度
  5. 每行一把尺子,能缩小影响范围
  6. 把大值裁掉,小值能保住,但代价没有消失
  7. 换一种输入,误差的后果也跟着变
  8. 运行账本,逐个位置追到最终输出
  9. 把小实验接到真实量化方法时,要保留范围区别
  10. 下一步该交付的是对照记录
  11. 记住这些要点
  12. 别把例子用过头
  13. 自己试一小步
  14. 查原始资料
开始前,会这些就够了
  • 会做分数乘加
  • 知道矩阵的一行怎样与输入相乘
  • 能运行Python标准库脚本

你拿一把只能标出有限格子的尺子,同时量零点一和十二。为了把十二放进去,每格就得变宽;零点一可能连半格都不到,最后被记成零。这种问题发生在同一组数共用量化刻度时,不能只看它们都用了相同位数。

我们用六个虚构权重做精确分数实验,比较整张共用刻度、每行单独定刻度,以及主动裁掉大值。然后让两种输入经过同一张矩阵。你会得到逐项误差与输出误差账本,也会看到为何一个更小的权重误差不能直接代替任务评价。

把例子带到自己的电脑上

下载完整练习包

需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。

下载 ZIP · 5 个文件
包里有什么
  • quantization_scale.py:完整示例
  • README.md:环境、范围与独立练习
  • observed.stdout.txt:制作时的实际输出
  • verification.json:源码与产物验证记录
  • SHA256SUMS.txt:文件内容摘要

在这个文件夹打开 PowerShell,逐行运行

python --version
python -B quantization_scale.py

解压到独立练习文件夹,在含源文件的目录打开终端运行。程序会在当前目录写入教学报告;先预测结果,再查看当次输出。固定 2×3 矩阵与精确分数运算;不打包 int4、不运行语言模型、不测速度或真实任务质量。

找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。

一个大权重,怎样改变其他数的刻度?

矩阵第一行是 [0.1, 0.2, 0.3],第二行是 [0.1, 0.2, 可调数字]。每行对应一个输出,互动范围为 0.3–20.0,正文固定为 12。先把最后一个数调大,再切换刻度选择方式。

权重均方误差:0.0317

当前输入的输出均方误差:0.0900

第一行刻度 1.7143;第二行刻度 1.7143。

整数范围 −7 到 7;零点为 0,恰好一半时取偶数
位置原权重整数恢复的权重
第 1 行,第 1 列0.100000.0000
第 1 行,第 2 列0.200000.0000
第 1 行,第 3 列0.300000.0000
第 2 行,第 1 列0.100000.0000
第 2 行,第 2 列0.200000.0000
第 2 行,第 3 列12.0000712.0000
输入 [1, 1, 0] 与各行相乘相加
输出使用原权重使用恢复的权重
第 1 个0.30000.0000
第 2 个0.30000.0000

保持权重与刻度方式不变,只切换输入。权重误差没变,输出误差还会一样吗?

本面板计算固定的 2×3 教学矩阵,输入未量化。刻度选择与整数舍入用十分之一单位的整数计算,显示值和误差使用浏览器 Number;正文脚本另用精确分数核对。这里没有打包真实 int4、加载模型或测量速度,也不能凭两组输入推断真实模型质量。

先把坐标和尺子的规则写清楚

矩阵W有两行三列,行代表输出通道,列对应输入的三个特征。第一行为[0.1,0.2,0.3],第二行为[0.1,0.2,12]。对输入x,每一行分别与x逐项相乘再相加,得到两个输出。这里没有偏置、激活函数或训练,输入本身也不量化。

我们自定义整数刻度从−7到7,零点固定为0。原数除以正步长s,四舍五入到最近整数,正好在中间时选偶数,再限制到刻度范围;还原值是整数乘s。这个对称范围能放进四位有符号编码,但故意不用−8,不能把它说成所有int4格式的唯一规则。

整张共用刻度,最大的数决定了格子宽度

第一种方案取整张矩阵的最大绝对值12,令s=12/7。这样12正好对应整数7,但0.1、0.2、0.3除以s都不到半格,舍入后全部变成0。还原矩阵因此是[[0,0,0],[0,0,12]]:最大的数保住了,其余五个非零数却全部丢失。

这不是程序漏算小数,而是有限格子覆盖大范围的结果。把一组数放在同一把尺子上,它们就共享分辨能力。本文称12为离群大值,只是相对于这六个数的大小而言;没有对真实模型做异常检测,也没有据此判定这个权重无用。

每行一把尺子,能缩小影响范围

第二种方案按输出行分别取最大绝对值。第一行步长为0.3/7=3/70,第二行仍为12/7。第一行得到整数[2,5,7],还原为[3/35,3/14,3/10]。前两项误差分别为−1/70、1/70,第三项没有误差;第二行的两个小数仍然变成零。

这说明分组可以隔开不同尺度,却不能消除同组内的大值影响。说“逐通道”时必须同时说明矩阵方向和哪个轴是通道;本例就是逐输出行。它需要两份scale,整张方案只需一份。真实格式还要保存这些辅助信息,所以参数位数不是完整存储账单。

试着手算第一项:0.1除以3/70得到7/3,离整数2更近,还原为6/70。这个值不是直接把原小数保留一位,而是先按当前组的尺子转换。把同一个整数2与第二行的scale配在一起,会还原成24/7;因此整数值离开所属组和scale,不能单独解释成权重。

把大值裁掉,小值能保住,但代价没有消失

第三种方案主动把可还原范围设为[−0.7,0.7],步长于是为0.1。五个小值都能精确表示,12却只能还原成0.7,误差为−11.3。这相当于承认尺子量不了这么大,而不是把大数偷偷变成合法的小数。导出的表会标记原值是否超出格子范围。

按六个位置等权求误差平方的平均,三种方案依次得到19/600、247/29400、12769/600。裁剪方案的权重均方误差最大,因为12这一个位置损失很大。但这张分数表还没有告诉我们,实际输入会不会用到第三个特征。

代码把误差定义为还原值减原值,保留正负号;均方误差则先平方再平均,不会让不同位置的正负误差抵消。本文所有权重方案的分母都是六,所有单次输出误差的分母都是二。若先把正负误差平均再平方,算到的将是另一件事,也会隐藏成对出现的误差。

换一种输入,误差的后果也跟着变

先输入[1,1,0]。原矩阵输出[0.3,0.3],第三列因为乘零没有贡献。整张方案输出[0,0];分行方案输出[0.3,0],第一行的两个小误差正好抵消;裁剪方案完全恢复原输出。按两个输出等权算均方误差,分别为0.09、0.045、0。

再输入[0,0,1],只读取第三列。原输出[0.3,12];整张方案输出[0,12],分行方案完全正确,裁剪方案输出[0.3,0.7]。对应输出误差变成0.045、0、63.845。裁剪的好坏因此依赖输入;不能只挑第一种输入,就宣布它比其他方案更好。

计算关系是输出误差等于量化误差矩阵乘输入,即Δy=ΔW×x。输入会放大、忽略或抵消某些误差。权重均方误差描述了参数改动的一种大小,任务质量还要看真实输入、后续运算和最终评价标准。这里两个输入都是人为挑选的机制反例,不是代表性测试集。

分行方案第一行的误差在[1,1,0]下抵消,也不是以后一直抵消。若两个输入系数不同,正负误差会乘上不同份量,原来的抵消关系就可能消失。这提醒我们,输出偶然一致只能作为该输入下的观测;要说明矩阵本身完全相同,必须逐项检查,而不能只喂一个向量。

运行账本,逐个位置追到最终输出

下面脚本使用Fraction保存分数,避免把0.1的二进制近似混入这次比较。它生成quantization-report.json、quantization-values.csv和quantization-outputs.csv。前一张CSV有18行,记录三种方案的每个权重;后一张有12行,记录两个输入、两个输出和三种方案。

先核对原值、scale、整数和还原值,再从输出CSV手算均方误差。零点与舍入规则写在报告里;全零组约定scale为1,所有整数仍为0,避免由最大值为零造成除零。脚本还检查正负半整数的偶数舍入,防止不同语言的默认规则悄悄改变结果。

展开收起代码 · 小实验python · 112 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

小实验 · python
"""Exact uniform-grid experiment, not packed int4 storage or a model benchmark."""
import csv
import json
from fractions import Fraction as F
from pathlib import Path

QMAX = 7  # Custom symmetric grid [-7, 7]; the int4 code -8 is deliberately unused.
WEIGHTS = ((F(1, 10), F(1, 5), F(3, 10)),
           (F(1, 10), F(1, 5), F(12)))  # [output_channel][input_feature]
INPUTS = {"small_features": (F(1), F(1), F(0)),
          "outlier_feature": (F(0), F(0), F(1))}


def quantize(value, scale):
    if scale <= 0:
        raise ValueError("scale must be positive")
    rounded = round(value / scale)  # Fraction implements nearest, ties to even.
    integer = max(-QMAX, min(QMAX, rounded))
    return integer, integer * scale


def choose_scale(values, cap=None):
    if not values or (cap is not None and cap <= 0):
        raise ValueError("nonempty values and positive cap required")
    peak = max(abs(v) for v in values) if cap is None else cap
    return peak / QMAX if peak else F(1)  # All-zero group has no division by zero.


def matvec(matrix, vector):
    if any(len(row) != len(vector) for row in matrix):
        raise ValueError("shape mismatch")
    return [sum((w * x for w, x in zip(row, vector)), F(0)) for row in matrix]


def mse(first, second):
    if not first or len(first) != len(second):
        raise ValueError("equal nonempty vectors required")
    return sum(((a - b) ** 2 for a, b in zip(first, second)), F(0)) / len(first)


def main():
    flat = [v for row in WEIGHTS for v in row]
    policies = {"whole_tensor": [choose_scale(flat)] * 2,
                "per_output_row": [choose_scale(row) for row in WEIGHTS],
                "clip_at_7_over_10": [choose_scale(flat, F(7, 10))] * 2}
    reports, weight_rows, output_rows = {}, [], []
    for name, scales in policies.items():
        restored = []
        for r, (row, scale) in enumerate(zip(WEIGHTS, scales)):
            restored_row = []
            for c, value in enumerate(row):
                integer, recovered = quantize(value, scale)
                restored_row.append(recovered)
                weight_rows.append({"policy": name, "row": r, "column": c,
                                    "original": str(value), "scale": str(scale),
                                    "integer": integer, "restored": str(recovered),
                                    "error": str(recovered - value),
                                    "outside_grid_range": abs(value) > QMAX * scale})
            restored.append(restored_row)
        outputs = {}
        for input_name, vector in INPUTS.items():
            reference, actual = matvec(WEIGHTS, vector), matvec(restored, vector)
            outputs[input_name] = {"reference": list(map(str, reference)),
                                   "quantized": list(map(str, actual)),
                                   "output_mse": str(mse(reference, actual))}
            for r, (original, recovered) in enumerate(zip(reference, actual)):
                output_rows.append({"policy": name, "input": input_name, "row": r,
                                    "reference": str(original), "actual": str(recovered),
                                    "error": str(recovered - original)})
        reports[name] = {"scales": list(map(str, scales)),
                         "scale_count": 2 if name == "per_output_row" else 1,
                         "restored": [[str(v) for v in row] for row in restored],
                         "weight_mse": str(mse(flat, [v for row in restored for v in row])),
                         "outputs": outputs}
    assert reports["whole_tensor"]["restored"] == [["0", "0", "0"], ["0", "0", "12"]]
    assert reports["per_output_row"]["scales"] == ["3/70", "12/7"]
    assert reports["clip_at_7_over_10"]["restored"] == [["1/10", "1/5", "3/10"], ["1/10", "1/5", "7/10"]]
    expected_weight_mse = {"whole_tensor": "19/600", "per_output_row": "247/29400", "clip_at_7_over_10": "12769/600"}
    expected_outputs = {"whole_tensor": ("9/100", "9/200"),
                        "per_output_row": ("9/200", "0"),
                        "clip_at_7_over_10": ("0", "12769/200")}
    for name in policies:
        assert reports[name]["weight_mse"] == expected_weight_mse[name]
        assert tuple(reports[name]["outputs"][i]["output_mse"] for i in INPUTS) == expected_outputs[name]
    assert choose_scale([F(0), F(0)]) == 1
    assert quantize(F(0), choose_scale([F(0)])) == (0, 0)
    assert [quantize(F(x, 2), F(1))[0] for x in (1, 3, -1, -3)] == [0, 2, 0, -2]
    assert quantize(F(100), F(1)) == (7, 7)
    try:
        quantize(F(1), F(0))
    except ValueError:
        pass
    else:
        raise AssertionError("zero scale accepted")
    report = {"scope": "Exact rational arithmetic on one 2-by-3 matrix; no model inference.",
              "convention": {"shape": "[output_channel,input_feature]", "qmin": -7,
                             "qmax": 7, "zero_point": 0, "rounding": "nearest_ties_even",
                             "all_zero_scale": "1", "inputs_unquantized": True},
              "weights": [[str(v) for v in row] for row in WEIGHTS],
              "inputs": {k: list(map(str, v)) for k, v in INPUTS.items()}, "policies": reports}
    Path("quantization-report.json").write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
    for filename, rows in (("quantization-values.csv", weight_rows), ("quantization-outputs.csv", output_rows)):
        with Path(filename).open("w", newline="", encoding="utf-8") as handle:
            writer = csv.DictWriter(handle, fieldnames=list(rows[0]))
            writer.writeheader()
            writer.writerows(rows)
    print(json.dumps({"status": "PASS", "policies": 3, "weight_rows": len(weight_rows),
                      "output_rows": len(output_rows), "weight_mse": expected_weight_mse}))


if __name__ == "__main__":
    main()

怎样运行:Python 3.10+标准库;保存为quantization_scale.py,在空练习目录运行 python quantization_scale.py。不要加-O,验收使用assert。 2026-10-15 已用 Python 3.14.0 执行这份代码并通过自带检查;下方是当次输出。固定 2×3 矩阵与精确分数运算;不打包 int4、不运行语言模型、不测速度或真实任务质量。

运行后对照结果
{"status": "PASS", "policies": 3, "weight_rows": 18, "output_rows": 12, "weight_mse": {"whole_tensor": "19/600", "per_output_row": "247/29400", "clip_at_7_over_10": "12769/600"}}

把小实验接到真实量化方法时,要保留范围区别

PyTorch官方量化文章解释了整张与逐通道的参数粒度;ONNX QuantizeLinear的第23版规则明确列出scale形状、饱和范围和最近偶数舍入。本文借这些资料检查术语与约定,实际执行的仍是自己的标准库公式,没有导出或运行ONNX模型。

SmoothQuant论文重点讨论大模型激活中的大值,并通过等价缩放在激活和权重之间转移量化困难。本文只量化虚构权重,没有实现这种变换,也不能把这里的权重矩阵说成论文观测的激活分布。它提供的是理解格子为何被拉宽的入口,不是该论文的复现。

下一步该交付的是对照记录

面对真实模型,先保存量化前后的数值约定、分组轴、scale和裁剪政策,再用事先确定的输入集比较输出与任务表现。看见某一组误差很大,应检查它经过哪些输入和后续层,而不是直接删除那个位置。裁剪阈值也要说明如何选择,不能根据测试答案反复调到最好看。

本实验的整数是Python对象,分数也是普通软件计算,没有进行位打包或调用低位矩阵内核。因此它只证明这些约定下的数值后果,不提供模型文件大小、显存或延迟数据。要回答速度问题,需要另做固定硬件和工作负载的实测。

读者可以先把三种还原矩阵抄在纸上,遮住程序的汇总结果,独立算两个输入的输出。若手算和CSV不同,先查行列对应、scale归属以及误差正负号;这些检查比盯着最后一个平均数更容易找到错误,也能作为以后审查真实量化日志的起点。

想接着往下学,可以阅读量化与低秩适配路线。先分清本篇均匀量化的误差,再接着比较不同量化格式与适配方法。

记住这些要点

  • 共用scale时,大值可能把小值挤到同一个整数格子。
  • 分组轴决定谁共享一把尺子,逐行并不会消除行内离群值。
  • 裁剪改变数值,必须记录被截到端点的位置。
  • 输出误差由ΔW与输入共同决定,权重MSE不是任务质量。

别把例子用过头

  • 把自定−7到7当成所有int4格式的默认范围。
  • 只说逐通道,却不说明矩阵方向与坐标轴。
  • 只用不经过大值的输入证明裁剪有效。
  • 把Python整数列表称作已经打包的四位权重。

自己试一小步

你能构造一份不会只挑有利输入的量化审查表吗?

  1. 自行设计一张三行四列矩阵,至少两行有不同数值范围,并事先固定四个输入。
  2. 分别实现整张、逐行和自选分组策略,记录零点、舍入、scale与零范围规则。
  3. 导出逐权重和逐输出CSV,另外写一个只读这些文件的核对程序。
  4. 提交各输入的误差与选择理由,保留至少一个所选方案较差的输入;不填写未测量的速度。

做到这些,就算完成

  • 矩阵轴和输入维度一致,所有输出都能沿CSV追查。
  • 方案选择不只依据一个权重平均分。
  • 练习只提交自己的数据与结论,不把标准库机制实验叫做真实模型评测。

查原始资料

想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。