TT Lab
开始
学习 学习路径 课程

Transformer — 手算一遍注意力

亲手算一遍 int8 的算术

在 TT Lab 中继续学习

目标

只用标准库亲手做整数量化的算术。从钉死四舍五入规则开始,做出对称和非对称量化,测量折叠后又展开的值的误差,数出一个离群值把缩放因子毁到什么程度,把按张量与按行并排对比,让码值之间只用整数相乘,最后测量这个误差经过 softmax 之后在概率上是多少。

为什么重要

量化工具只有一行。如果不知道里面发生了什么算术,精度下降时就只能不断改选项重新跑。缩放因子由什么决定、四舍五入往哪一侧走、收窄单位会有什么变化——在含有八个值的列表上看过一次之后,在大模型里也能指出同样的地方。 本实验不使用工具。这个 Pod 的系统 Python 里没有 numpy(numpy 只在 /opt/onnx-lab/bin/python 里有),也不调用模型。所以“某个模型在 int8 下精度掉了百分之几”这样的话,这里不说。只使用在你做出的列表和矩阵上测得的数字。 难的不是式子,而是细节。0.5 往哪边送、超出范围的要不要截断、缩放因子取什么的最大值,如果这些没有规定,即使输入相同,码值也会一档一档地错开。 评分器不会相信你写下的说明。它会真正导入你的模块,每次用不同的输入调用函数,并与评分器另行计算的值对照。对照大部分是整数数组之间的,所以不会有晃动。

步骤

  1. 在 /root/work/tf-quant/quant.py 中创建 QMAX = 127、UMAX = 255 以及 round_half_even(x)、round_half_away(x)、rounding_gap(values)。亲眼确认两种四舍五入规则在哪里产生分歧。
  2. 增加 sym_scale(values)、quantize_sym(values, scale)、dequantize_sym(codes, scale),做出对称量化。缩放因子是 max(|x|) / 127。
  3. 增加 affine_params(values)、quantize_affine(values, scale, zero_point)、dequantize_affine(codes, scale, zero_point),做出非对称量化。缩放因子是 (max - min) / 255。
  4. 增加 levels_used(codes) 和 error_stats(original, restored),做出测量误差的尺子。
  5. 增加 outlier_effect(values, outlier),测量一个大值对其余的值做了什么。
  6. 增加 quantize_tensor(matrix)、quantize_rows(matrix)、granularity_gap(matrix),对比按张量与按行。
  7. 增加 transpose(matrix)、int_matmul(left_codes, right_codes)、float_matmul(left, right)、quant_matmul(left, right),只用整数来做矩阵乘法。
  8. 做出 WEIGHTS、OUTLIER、QUERIES、KEYS 以及 softmax(scores)、attention_shift(queries, keys),并把结果记录到 /root/work/tf-quant/quant_report.json 和 /root/work/tf-quant/quant_report.md 中。

参考

先把四舍五入钉死

在 /root/work/tf-quant/quant.py 中创建 QMAX = 127、UMAX = 255 以及 round_half_even(x)、round_half_away(x)、rounding_gap(values)。前者与 Python 默认的 round 一样,把恰好是 0.5 的位置送往偶数一侧,后者送往远离 0 的一侧。rounding_gap 只把两种规则产生分歧的值按收到的顺序收集起来返回。

先运行 python3 -c "print(round(0.5), round(1.5), round(2.5))"。会得到 0 2 2。如果把 round_half_away 写成 math.floor(x + 0.5) 一行,负数会出错——-1.5 应该变成 -2,而那个式子给出的是 -1。请用 math.floor(x) 求出下取整,再单独看小数部分,只对恰好是 0.5 的位置按符号分开处理。两个函数都返回整数。

用对称量化折叠再展开

增加 sym_scale(values)、quantize_sym(values, scale)、dequantize_sym(codes, scale)。缩放因子是 max(|x|) / QMAX,如果值全是 0 则是 1.0。折叠时用缩放因子相除,用 round_half_even 四舍五入,然后截断到 -QMAX 至 QMAX 的范围。

缩放因子要取最大绝对值而不是最大值,负数一侧才会落在范围内。截断一行 max(-QMAX, min(QMAX, code)) 就行。展开的函数只是乘以缩放因子,一行就够——展开后的值与原值不同才是正常的。损失的量落在缩放因子的一半之内。

用非对称量化把 256 档全用上

增加 affine_params(values)、quantize_affine(values, scale, zero_point)、dequantize_affine(codes, scale, zero_point)。缩放因子是 (max - min) / UMAX,零点是 round_half_even(-min / scale) 截断到 0 至 UMAX 的范围后的值。如果最大值与最小值相同,则是 (1.0, 0)。折叠时把 round_half_even(x / scale) + zero_point 截断到 0 至 UMAX 的范围。

漏掉截断,会在这里真正出问题——两端在四舍五入时各挪了一档,round(x/scale) + zero_point 就会变成 256,超出 uint8 范围。展开的式子是 (code - zero_point) * scale。多亏这个式子,实数 0 才能毫无误差地还原——这就是需要零点的原因。

测量偏离多远的尺子

增加 levels_used(codes) 和 error_stats(original, restored)。前者是不同码值的个数,后者是带有 max_abs、mean_abs、max_rel 三个键的字典。max_rel 是最大绝对误差除以原值的最大绝对值得到的值,如果那个最大绝对值是 0,则是 0.0。

如果把相对误差逐个值去除,在 0 附近的值上会跳到无穷大。所以要除以列表所包含的宽度——缩放因子就是由这个宽度决定的,因此比较的对象也是它。levels_used 是 len(set(codes)) 一行。明明有 256 档,却只用了几档,这是下一步的内容。

一个大值对其余的值做了什么

增加 outlier_effect(values, outlier)。分别测量只折叠 values 时和折叠 values + [outlier] 时,并只对比原来 values 的位置。返回的键有 clean_scale、dirty_scale、clean_levels、dirty_levels、clean_max_abs、dirty_max_abs 六个。

把附上离群值的列表整个折叠之后,只从前面截取 len(values) 个来对比。如果把离群值自身的误差也算进去,结论就会颠倒——被毁掉的不是离群值,而是它旁边的普通值。请亲眼看看 dirty_levels 会掉到几。明明有 256 档,现在用了几档?

整个张量一个缩放因子与每一行各一个缩放因子

增加 quantize_tensor(matrix)、quantize_rows(matrix)、granularity_gap(matrix)。前两个分别返回 (배율, 코드 행렬) 和 (배율 목록, 코드 행렬)(占位符依次为缩放因子、码值矩阵,以及缩放因子列表、码值矩阵)。granularity_gap 的键有 tensor_max_abs、row_max_abs、tensor_worst_row_rel、row_worst_row_rel、tensor_worst_row_levels、row_worst_row_levels 六个。

worst_row_rel 是对每一行用该行的 error_stats 求出 max_rel 后的最大值,worst_row_levels 是对每一行求出 levels_used 后的最小值。只看最大绝对误差,两种方式的差别几乎看不出来——因为那个值是由最大的那一行决定的。小的行是怎样被压扁的,要从相对误差和用到的档数上才看得出来。

只用整数来做矩阵乘法

增加 transpose(matrix)、int_matmul(left_codes, right_codes)、float_matmul(left, right)、quant_matmul(left, right)。int_matmul 的乘和加都必须是整数。quant_matmul 把左边按行、右边按列折叠后用整数相乘,再用 acc * left_scale * right_scale 展开,返回 (복원 행렬, 정수 누적 행렬, 왼쪽 배율 목록, 오른쪽 배율 목록)(占位符依次为还原矩阵、整数累加矩阵、左侧缩放因子列表、右侧缩放因子列表)。

要把右边按列折叠,就用 transpose 取出列,对每一列使用 quantize_sym,再用 transpose 转回来。在累加里面绝对不要乘缩放因子——只在最后乘一次。所以矩阵乘法的误差不是在累加中增大的,而是最初折叠时就已经产生的。评分器会原样对照整数累加矩阵,所以只要错开一档就会被抓到。

经过 softmax 之后剩下的

做出 WEIGHTS(6 行 8 列)、OUTLIER(绝对值 20.0 以上)、QUERIES(4 行 8 列)、KEYS(5 行 8 列)以及 softmax(scores)、attention_shift(queries, keys)。WEIGHTS 最大那一行的最大绝对值必须是最小那一行的 10 倍以上,三个矩阵的所有值的绝对值都在 2.0 以下。然后在 /root/work/tf-quant/quant_report.json 中写入 sym_scale、sym_max_abs、sym_mean_abs、sym_max_rel、sym_levels、affine_scale、affine_zero_point、affine_max_abs、affine_levels、outlier_clean_scale、outlier_dirty_scale、outlier_clean_levels、outlier_dirty_levels、outlier_clean_max_abs、outlier_dirty_max_abs、tensor_max_abs、row_max_abs、tensor_worst_row_rel、row_worst_row_rel、tensor_worst_row_levels、row_worst_row_levels、matmul_max_abs、matmul_max_rel、clean_score_max_abs、clean_prob_max_abs、clean_argmax_changed、dirty_score_max_abs、dirty_prob_max_abs、dirty_argmax_changed,并在 /root/work/tf-quant/quant_report.md 中用 ## 무엇을 쟀나(韩文,意为“测量了什么”)、## 대칭과 비대칭은 어디서 갈렸나(韩文,意为“对称与非对称在哪里产生了分歧”)、## 이상치 하나가 한 일(韩文,意为“一个离群值做了什么”)、## 행렬 곱과 소프트맥스를 지나면(韩文,意为“经过矩阵乘法和 softmax 之后”)四节来写。

数字不要手写,要用实际运行你的代码得到的值来填。sym_* 和 affine_* 是对把 WEIGHTS 展平成一行的列表得到的值,outlier_* 是用那个列表和 OUTLIER 调用 outlier_effect 的结果。tensor_*、row_* 是 granularity_gap(WEIGHTS) 的值,matmul_* 是把 quant_matmul(QUERIES, transpose(KEYS)) 的还原矩阵与 float_matmul(QUERIES, transpose(KEYS)) 对比的 error_stats 中的 max_abs、max_rel。clean_* 是 attention_shift(QUERIES, KEYS),dirty_* 是用复制 KEYS 后只把 [0][0] 位置换成 OUTLIER 的矩阵调用的结果。不要修改原始的 KEYS。看分数误差和概率误差朝哪个方向变动,并如实写下来——不要猜着写。