本文为 EfficientML Lab 2 实验笔记,包含 K-Means 量化、K-Means QAT、线性量化等内容,难度不大,内容丰富。

Part 1: K-Means Quantization

Qustion 1

第一个问题是实现 K-means 量化的核心算法,其中 K-means 本身是调库实现的。

第一小问求簇数,n bit 可以表示 2^n 个簇。第二小问是根据已有的 Codebook 表示量化后的张量,使用 Tensor 的索引表示即可。需要注意的是,codebook 可能是调用者传入的而非一定是由我们计算得到的,因此在表示的时候要使用 codebook 的成员来获取 centroidslabels,否则在后面会报错。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    if codebook is None:
        ############### YOUR CODE STARTS HERE ###############
        # get number of clusters based on the quantization precision
        # hint: one line of code
        n_clusters = 2 ** bitwidth
        ############### YOUR CODE ENDS HERE #################
        # use k-means to get the quantization centroids
        kmeans = KMeans(n_clusters=n_clusters, mode='euclidean', verbose=0)
        labels = kmeans.fit_predict(fp32_tensor.view(-1, 1)).to(torch.long)
        centroids = kmeans.centroids.to(torch.float).view(-1)
        codebook = Codebook(centroids, labels)
    ############### YOUR CODE STARTS HERE ###############
    # decode the codebook into k-means quantized tensor for inference
    # hint: one line of code
    quantized_tensor = codebook.centroids[codebook.labels].view(fp32_tensor.shape)
    ############### YOUR CODE ENDS HERE #################

Question 2

Part 2: Trained K-Means Quantization

Question 3

在低比特量化后模型掉点很厉害,因此要进行 QAT。量化后的权重的梯度推导为:

$$ \frac{\partial \mathcal{L} }{\partial C_k} = \sum_{j} \frac{\partial \mathcal{L} }{\partial W_{j}} \frac{\partial W_{j} }{\partial C_k} = \sum_{j} \frac{\partial \mathcal{L} }{\partial W_{j}} \mathbf{1}(I_{j}=k) $$

但在本实验中,简单起见,我们使用相同簇的原始权重的均值作为量化后的该簇更新后的值。代码实现就一行:

1
codebook.centroids[k] = fp32_tensor[codebook.labels == k].mean()

最终不同量化位数得到的性能指标如下表所示,2 bits 掉点很夸张,QAT 后也难以恢复到原始性能。

量化位数 掉点率 微调后掉点率 微调轮数
8 bits 0.17% 0.17% 0
4 bits 13.87% 0.49% 1
2 bits 82.95% 1.75% 5

Part 3: Linear Quantization

Question 4

本问实现的是线性量化的核心函数,即给定张量、量化位宽、缩放系数、零点,计算量化后的张量。根据线性量化公式:

$$ q = r/S + Z $$

计算即可:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
############### YOUR CODE STARTS HERE ###############
    # Step 1: scale the fp_tensor
    scaled_tensor = fp_tensor / scale
    # Step 2: round the floating value to integer value
    rounded_tensor = scaled_tensor.round()
    ############### YOUR CODE ENDS HERE #################

    rounded_tensor = rounded_tensor.to(dtype)

    ############### YOUR CODE STARTS HERE ###############
    # Step 3: shift the rounded_tensor to make zero_point 0
    shifted_tensor = rounded_tensor + zero_point
    ############### YOUR CODE ENDS HERE #################

值得一提的是,在 step 4 中,执行了一步将溢出的结果压缩到范围之内的操作。

Question 5

计算缩放系数和零点的公式分别为:

$$ \begin{align*} S&=(r_{\mathrm{max}} - r_{\mathrm{min}}) / (q_{\mathrm{max}} - q_{\mathrm{min}})\\ Z &= \mathrm{int}(\mathrm{round}(q_{\mathrm{min}} - r_{\mathrm{min}} / S)) \end{align*} $$

代码照抄即可。

权重有一个特殊的性质:分布通常都是关于 0 点对称的,因此权重量化的零点可以直接设置为 0。

此外,经验表明,对卷积核进行量化时,按照输出通道逐通道量化能够取得更好的表现

Question 6-8

在此之前,实验文档首先推导了考虑线性量化的全连接层和卷积层的表达式,推导过程进行了一系列代入、假设和化简,主要包括:

$$ \begin{align*} Z_{\mathrm{weight}}&=0\\ r_{\mathrm{weight}} &= S_{\mathrm{weight}}q_{\mathrm{weight}}\\ Z_{\mathrm{bias}} &= 0\\ S_{\mathrm{bias}} &= S_{\mathrm{input}} \cdot S_{\mathrm{weight}} \end{align*} $$

最终得到的结论为:

$$ \begin{align*} q_{\mathrm{output}} &= (\mathrm{CONV}[q_{\mathrm{input}}, q_{\mathrm{weight}}] + Q_{\mathrm{bias}}) \cdot (S_{\mathrm{input}}S_{\mathrm{weight}} / S_{\mathrm{output}}) + Z_{\mathrm{output}}\\ q_{\mathrm{output}} &= (\mathrm{Linear}[q_{\mathrm{input}}, q_{\mathrm{weight}}] + Q_{\mathrm{bias}})\cdot (S_{\mathrm{input}} \cdot S_{\mathrm{weight}} / S_{\mathrm{output}}) + Z_{\mathrm{output}}\\ \text{其中,}Q_{\mathrm{bias}} &= q_{\mathrm{bias}} - \mathrm{Linear}[Z_{\mathrm{input}}, q_{\mathrm{weight}}] \end{align*} $$

Q7 和 Q8 的代码实现相同,需要注意的是在对 output 进行缩放时,由于是逐通道量化的,因此权重的缩放系数是个张量,需要处理好形状以便进行广播。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    ############### YOUR CODE STARTS HERE ###############
    # Step 2: scale the output
    #         hint: 1. scales are floating numbers, we need to convert output to float as well
    #               2. the shape of weight scale is [oc, 1, 1, 1] while the shape of output is [batch_size, oc]
    output = output * (input_scale * weight_scale / output_scale).swapaxes(0, 1)

    # Step 3: shift output by output_zero_point
    #         hint: one line of code
    output = output + output_zero_point
    ############### YOUR CODE ENDS HERE #################

Question 9

要干的活文档基本都干好了,只剩下一个对输入进行量化的活需要我们完成。照猫画虎,使用 get_quantization_scale_and_zero_point 计算缩放系数和零点,使用 linear_quantize 进行线性量化即可。

1
2
3
4
############### YOUR CODE STARTS HERE ###############
x_scale, x_zero_point = get_quantization_scale_and_zero_point(x, 8)
return linear_quantize(x, 8, x_scale, x_zero_point)
############### YOUR CODE ENDS HERE #################

量化后的模型精度为 92.21% 几乎没掉点。

为什么在线性量化中没有 ReLU 层:ReLU 层被融合到前一层中网络中,可以减少数据的搬运次数。

Question 10

回答来自 deepseek:

量化方法 核心优势 核心劣势 适用场景
K-means 量化 高精度(非均匀数据) 计算复杂、硬件支持差 数据分布复杂、对精度敏感、专用硬件场景
线性量化 低延迟、硬件友好、易部署 对非均匀数据精度低 实时推理、通用硬件、动态范围稳定场景

小结

实验文档本身体量很大,知识点也很丰富,但是大多数代码都已经给出了,每个回答只需要写一行或者两行代码,并且周围也给出了充足的提示。这使得实验本身缺乏挑战性,有点鸡肋。