一步一步理解大模型:模型量化技术2-源代码

量化的过程可以根据其发生的时机,被划分为训练时量化和训练后量化,也被称为后训练量化(Post Training Quantization)。在模型的训练阶段,我们通常会寻求优质的硬件条件和更高的计算精度,以便训练出性能更优秀的模型,从而获得更高的评估分数。然而,在模型部署阶段,我们需要考虑到用户硬件的实际限制。为了让更多的用户能够使用我们的模型,我们通常会在训练后的推理阶段进行量化处理。这样,我们可以在保持模型性能的同时,降低模型的硬件需求和运行成本,使模型能够在更广泛的硬件环境中运行。因此,后训练量化是实际商业应用中的常见做法。

所以,这里我们只关心训练后量化,将训练好的权重拿出来量化,这些压缩后的模型仅用于推理。

在模型量化技术(1)中,我们提到一个重要的步骤是找到权重附近的“落脚点”,在实际运算过程中,我们是找到浮点数附近的整数,就是做四舍五入运算来间接处理的。代码如下:

static inline int nearest_int(float fval) {
    assert(fval <= 4194303.f);
    float val = fval + 12582912.f;
    int i; memcpy(&i, &val, sizeof(int));
    return (i & 0x007fffff) - 0x00400000;
}

对了,是不是和你课本上 ( int )( x + 0.5f ) 不一样?

这段代码实现了一个产品化的,高性能的,精准的将浮点数四舍五入到最接近的整数的函数。这个函数的工作原理基于IEEE 754浮点数的表示方式和浮点数到整数的转换规则。

IEEE 754单精度浮点数由一个符号位、8位指数和23位尾数组成。浮点数的值由这三部分共同决定。在这个函数中,通过加上一个特定的浮点数(12582912.f,这是2的23次方),可以将浮点数的小数部分移到尾数的位置。

然后,通过memcpy函数,将浮点数的内存表示复制到整数变量i中。这样,i的低23位就是浮点数的小数部分的二进制表示。

最后,通过位运算,将i的低23位取出,并减去2的22次方(0x00400000)。这样,如果浮点数的小数部分大于0.5,那么减去2的22次方后,i的值会增加1,实现了四舍五入的效果。如果浮点数的小数部分小于0.5,那么减去2的22次方后,i的值不变,也实现了四舍五入的效果。

这个函数的输入参数是一个浮点数fval,返回值是fval四舍五入后的整数值。注意,这个函数假设fval的值不大于4194303.f,这是因为如果fval的值过大,那么在加上2的23次方后,可能会导致浮点数的精度丢失。

下面这个函数是一个量化到8比特的函数:

void quantize_row_q8_K_reference(const float * restrict x, block_q8_K * restrict y, int k) {
    assert(k % QK_K == 0);
    const int nb = k / QK_K;

    for (int i = 0; i < nb; i++) {

        float max = 0;
        float amax = 0;
        for (int j = 0; j < QK_K; ++j) {
            float ax = fabsf(x[j]);
            if (ax > amax) {
                amax = ax; max = x[j];
            }
        }
        if (!amax) {
            y[i].d = 0;
            memset(y[i].qs, 0, QK_K);
            x += QK_K;
            continue;
        }
        const float iscale = -128.f/max;
        for (int j = 0; j < QK_K; ++j) {
            int v = nearest_int(iscale*x[j]);
            y[i].qs[j] = MIN(127, v);
        }
        for (int j = 0; j < QK_K/16; ++j) {
            int sum = 0;
            for (int ii = 0; ii < 16; ++ii) {
                sum += y[i].qs[j*16 + ii];
            }
            y[i].bsums[j] = sum;
        }
        y[i].d = 1/iscale;
        x += QK_K;
    }
}

这段函数将浮点数数组x量化为8位整数数组y,并计算每个块的和。

对于每个块,执行以下操作:

  • 找到块中的最大值max和绝对最大值amax

  • 如果amax为0(即块中所有的值都为0),那么将y[i].d设置为0,将y[i].qs设置为全0数组,然后跳过这个块,处理下一个块。

  • 计算量化的比例因子iscale,等于-128除以max

  • 对块中的每个值,使用nearest_int函数进行量化,然后将量化的结果存储到y[i].qs[j]中。注意,量化的结果被限制在127以内,这是因为y[i].qs[j]是一个8位整数数组,其最大值为127。

  • 对块中的每个子块(大小为16),计算子块中所有值的和,然后将和存储到y[i].bsums[j]中。

  • 1/iscale存储到y[i].d中,这是量化的比例因子的倒数,可以用于后续的重构。

  • x的指针向前移动QK_K个位置,准备处理下一个块。

关键代码是这一句:

 int v = nearest_int(iscale*x[j]);

你可能感兴趣的:(算法,数据结构,人工智能)