
[InferLLM大模型推理框架项目](28)kern中ARM优化模块的quantize代码分析(src/kern/optimized/arm/quantize.h)
InferLLM 框架中 ARM 优化模块的 quantize.h 分析
quantize.h
文件是 InferLLM 框架中 ARM 优化模块的量化计算部分,它实现了浮点数与整数量化格式之间的转换函数。这些函数在大语言模型推理中起着关键作用,可以减少内存占用和计算量,提高推理性能。
1. 文件结构概述
#pragma once
#include <assert.h>
#include "arm_neon.h"
#include "core/tensor.h"
#include "kern/kernel_define.h"
#include "kern/naive/naive.h"
namespace inferllm {
namespace opt {
// 量化和反量化函数
} // namespace opt
} // namespace inferllm
该文件包含了必要的头文件,其中 arm_neon.h
是 ARM NEON 指令集的头文件,提供了 NEON 指令集的内联函数。所有函数都定义在 inferllm::opt
命名空间中。
2. 量化数据结构
在分析具体函数之前,需要了解量化数据的存储结构。根据代码中的使用,可以推断出以下数据结构:
// 4位整数量化块,每个块存储32个4位整数和一个缩放因子
struct BlockQ40 {
float d; // 缩放因子
uint8_t qs[16]; // 存储32个4位整数,每个字节存储2个4位整数
};
// 8位整数量化块,每个块存储32个8位整数和一个缩放因子
struct BlockQ80 {
float d; // 缩放因子
int8_t qs[32]; // 存储32个8位整数
};
这些数据结构定义了量化数据的存储格式,每个块包含一个缩放因子和多个量化整数。
3. 4位整数量化函数
inline void quantize_row_q4_0(const float* __restrict x, void* __restrict vy, int k) {
const int nb = k / QK40;
BlockQ40* __restrict y = static_cast<BlockQ40*>(vy);
for (int i = 0; i < nb; i++) {
float32x4_t srcv[8];
float32x4_t asrcv[8];
float32x4_t amaxv[8];
// 加载32个浮点数到8个向量寄存器
for (int l = 0; l < 8; l++)
srcv[l] = vld1q_f32(x + i * 32 + 4 * l);
// 计算绝对值
for (int l = 0; l < 8; l++)
asrcv[l] = vabsq_f32(srcv[l]);
// 找到最大绝对值
for (int l = 0; l < 4; l++)
amaxv[2 * l] = vmaxq_f32(asrcv[2 * l], asrcv[2 * l + 1]);
for (int l = 0; l < 2; l++)
amaxv[4 * l] = vmaxq_f32(amaxv[4 * l], amaxv[4 * l + 2]);
for (int l = 0; l < 1; l++)
amaxv[8 * l] = vmaxq_f32(amaxv[8 * l], amaxv[8 * l + 4]);
const float amax = vmaxvq_f32(amaxv[0]);
// 计算缩放因子
const float d = amax / ((1 << 3) - 1);
const float id = d ? 1.0f / d : 0.0f;
y[i].d = d;
// 量化32个浮点数为32个4位整数
for (int l = 0; l < 8; l++) {
const float32x4_t v = vmulq_n_f32(srcv[l], id);
const float32x4_t vf = vaddq_f32(v, vdupq_n_f32(8.5f));
const int32x4_t vi = vcvtq_s32_f32(vf);
// 将4个32位整数打包为2个8位整数,每个8位整数存储2个4位整数
y[i].qs[2 * l + 0] = vgetq_lane_s32(vi, 0) | (vgetq_lane_s32(vi, 1) << 4);
y[i].qs[2 * l + 1] = vgetq_lane_s32(vi, 2) | (vgetq_lane_s32(vi, 3) << 4);
}
}
}
这个函数将浮点数量化为4位整数。主要步骤包括:
- 将输入数据分成多个块,每个块包含32个浮点数
- 对于每个块,找到最大绝对值
- 计算缩放因子,使得最大绝对值映射到4位整数的最大值(7)
- 将每个浮点数除以缩放因子,加上偏移值(8.5),然后转换为整数
- 将32个整数打包为16个字节,每个字节存储2个4位整数
这个函数使用 NEON 指令集进行向量化计算,提高计算效率。
4. 4位整数反量化函数
inline void dequantize_row_q4_0(const void* __restrict vx, float* __restrict y, int k) {
assert(k % QK40 == 0);
const int nb = k / QK40;
const BlockQ40* __restrict x = static_cast<const BlockQ40*>(vx);
for (int i = 0; i < nb; i++) {
const float32x4_t vd = vdupq_n_f32(x[i].d);
const uint8_t* __restrict pp = x[i].qs;
for (int l = 0; l < QK40; l += 16) {
// 加载8个字节,每个字节包含2个4位整数
const uint8x8_t v8 = vld1_u8(pp + l / 2);
// 提取4位整数
const uint8x8_t v0 = vand_u8(v8, vdup_n_u8(0x0f));
const uint8x8_t v1 = vshr_n_u8(v8, 4);
// 转换为有符号8位整数
const int8x8_t vs_0 = vreinterpret_s8_u8(v0);
const int8x8_t vs_1 = vreinterpret_s8_u8(v1);
// 减去偏移值8
const int8x8_t vb_0 = vsub_s8(vs_0, vdup_n_s8(8));
const int8x8_t vb_1 = vsub_s8(vs_1, vdup_n_s8(8));
// 交错排列
const int8x8_t vx_0 = vzip1_s8(vb_0, vb_1);
const int8x8_t vx_1 = vzip2_s8(vb_0, vb_1);
const int8x16_t vq = vcombine_s8(vx_0, vx_1);
// 转换为16位整数
const int16x8_t vi_0 = vmovl_s8(vget_low_s8(vq));
const int16x8_t vi_1 = vmovl_s8(vget_high_s8(vq));
// 转换为32位浮点数
const float32x4_t vf_0 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(vi_0)));
const float32x4_t vf_1 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(vi_0)));
const float32x4_t vf_2 = vcvtq_f32_s32(vmovl_s16(vget_low_s16(vi_1)));
const float32x4_t vf_3 = vcvtq_f32_s32(vmovl_s16(vget_high_s16(vi_1)));
// 乘以缩放因子
const float32x4_t r0 = vmulq_f32(vf_0, vd);
const float32x4_t r1 = vmulq_f32(vf_1, vd);
const float32x4_t r2 = vmulq_f32(vf_2, vd);
const float32x4_t r3 = vmulq_f32(vf_3, vd);
// 存储结果
vst1q_f32(y + i * QK40 + l + 0, r0);
vst1q_f32(y + i * QK40 + l + 4, r1);
vst1q_f32(y + i * QK40 + l + 8, r2);
vst1q_f32(y + i * QK40 + l + 12, r3);
}
}
}
这个函数将4位整数反量化为浮点数。主要步骤包括:
- 将输入数据分成多个块,每个块包含32个4位整数
- 对于每个块,加载缩放因子
- 将16个字节解包为32个4位整数
- 减去偏移值(8),转换为有符号整数
- 将整数转换为浮点数,并乘以缩放因子
这个函数也使用 NEON 指令集进行向量化计算,提高计算效率。
5. 8位整数量化函数
inline void quantize_row_q8_0(const float* __restrict x, void* __restrict vy, int k) {
assert(k % QK80 == 0);
BlockQ80* y = static_cast<BlockQ80*>(vy);
naive::quantize_row_q8_0_reference(x, y, k);
}
这个函数将浮点数量化为8位整数。与4位整数量化函数不同,它直接调用了 naive 模块中的参考实现,没有使用 NEON 指令集优化。这可能是因为8位整数量化的计算相对简单,优化的收益不大,或者是因为这个函数还没有被优化。
6. NEON 指令集优化分析
quantize.h
文件中的函数大量使用了 NEON 指令集进行向量化计算,提高计算效率。主要使用的 NEON 指令包括:
- 数据加载:使用
vld1q_f32
、vld1_u8
等指令加载数据到向量寄存器 - 数据存储:使用
vst1q_f32
等指令将向量寄存器中的数据存储到内存 - 算术运算:使用
vmulq_n_f32
、vaddq_f32
等指令进行向量算术运算 - 位运算:使用
vand_u8
、vshr_n_u8
等指令进行位运算 - 类型转换:使用
vreinterpret_s8_u8
、vcvtq_s32_f32
等指令进行类型转换 - 数据重排:使用
vzip1_s8
、vzip2_s8
等指令进行数据重排
这些 NEON 指令使得量化和反量化函数可以同时处理多个数据元素,提高计算效率。
7. 量化策略分析
InferLLM 框架使用的量化策略是对称量化,即将浮点数映射到有符号整数范围内,使得0映射到0。具体来说:
- 4位整数量化:将浮点数映射到[-8, 7]范围内,使用4位存储,每个字节存储2个4位整数
- 8位整数量化:将浮点数映射到[-128, 127]范围内,使用8位存储
这种量化策略的优点是实现简单,计算效率高;缺点是对于非对称分布的数据,可能会浪费一些表示范围。
8. 与 naive 实现的比较
quantize.h
文件中的函数与 naive 模块中的函数相比,主要区别在于:
- 向量化实现:
quantize_row_q4_0
和dequantize_row_q4_0
使用 NEON 指令集进行向量化计算,naive 模块使用标量实现 - 实现复杂度:优化版本的实现更复杂,使用了更多的 NEON 指令和优化技巧
- 性能差异:优化版本的性能明显优于 naive 版本,特别是在支持 NEON 指令集的 ARM 处理器上
值得注意的是,quantize_row_q8_0
函数直接调用了 naive 模块中的参考实现,没有使用 NEON 指令集优化。这可能是因为8位整数量化的计算相对简单,优化的收益不大,或者是因为这个函数还没有被优化。
9. 未来优化方向
基于当前实现,可以考虑以下优化方向:
- 优化
quantize_row_q8_0
函数:使用 NEON 指令集优化8位整数量化函数 - 支持更多量化格式:如3位、2位甚至1位量化,以及非对称量化、组量化等
- 使用更高级的 NEON 指令:如 ARMv8.2-A 的 DotProd 指令,加速点积计算
- 使用混合精度计算:在不同的计算阶段使用不同的精度,可以平衡精度和性能。例如,在权重存储时使用4位整数量化,在激活值计算时使用8位整数量化,在累加时使用32位整数,在最终输出时使用浮点数。这种混合精度计算可以在保持精度的同时提高性能。
9.2 非对称量化
当前的量化策略是对称量化,即将浮点数映射到有符号整数范围内,使得0映射到0。对于分布不对称的数据,可以考虑使用非对称量化,即引入一个偏移值,使得量化范围更好地覆盖数据分布。例如:
// 非对称量化
const float min_val = /* 找到最小值 */;
const float max_val = /* 找到最大值 */;
const float scale = (max_val - min_val) / 255.0f;
const float zero_point = -min_val / scale;
// 量化
int8_t q = (int8_t)(x / scale + zero_point);
// 反量化
float x = (q - zero_point) * scale;
9.3 组量化
组量化是将权重分成多个组,每个组使用不同的缩放因子。这种方法可以提高量化精度,特别是对于分布不均匀的权重。例如:
// 组量化,每32个权重为一组
for (int i = 0; i < n; i += 32) {
float max_val = /* 找到组内最大绝对值 */;
float scale = max_val / 7.0f;
// 量化组内权重
for (int j = 0; j < 32; j++) {
q[i + j] = (int8_t)(x[i + j] / scale);
}
// 存储缩放因子
scales[i / 32] = scale;
}
10. 量化对模型精度的影响
量化会导致模型精度下降,但是通过合理的量化策略,可以将精度损失控制在可接受的范围内。InferLLM 框架中使用的4位整数量化和8位整数量化是一种比较激进的量化策略,可能会导致较大的精度损失,特别是对于一些对精度要求较高的任务。
为了减轻量化对模型精度的影响,可以考虑以下方法:
- 量化感知训练:在训练过程中模拟量化操作,使模型适应量化带来的精度损失
- 选择性量化:只量化对精度影响较小的层,保留对精度影响较大的层的浮点表示
- 混合精度量化:对不同的层使用不同的量化精度,根据层的重要性和敏感性选择合适的量化精度
11. 量化计算在 ARM 平台上的优势
在 ARM 平台上,量化计算有以下优势:
- 减少内存占用:4位整数量化可以将内存占用减少到浮点数的1/8,8位整数量化可以将内存占用减少到浮点数的1/4
- 减少内存带宽需求:量化数据占用更少的内存空间,可以减少内存带宽需求,提高缓存命中率
- 加速计算:整数计算通常比浮点计算更快,特别是在一些低端 ARM 处理器上
- 支持专用指令:ARMv8.2-A 引入了 SDOT 指令,专门用于加速整数点积计算,可以进一步提高量化计算的性能
这些优势使得量化计算在 ARM 平台上的大语言模型推理中发挥重要作用,特别是在移动设备和嵌入式设备等资源受限的环境中。
12. 与其他量化库的比较
InferLLM 框架中的量化实现与其他量化库(如 TensorFlow Lite、NCNN、MNN 等)相比,有以下特点:
- 专注于大语言模型:InferLLM 框架专注于大语言模型推理,量化实现针对大语言模型的特点进行了优化
- 使用 NEON 指令集优化:InferLLM 框架使用 NEON 指令集进行向量化计算,提高计算效率
- 支持4位整数量化:InferLLM 框架支持4位整数量化,可以进一步减少内存占用和计算量
- 简单高效:InferLLM 框架的量化实现相对简单,没有复杂的依赖,易于集成和使用
这些特点使得 InferLLM 框架在大语言模型推理方面具有一定的优势,特别是在资源受限的环境中。
总结
InferLLM 框架中的 ARM 优化模块通过 quantize.h 文件实现了浮点数与整数量化格式之间的转换函数。这些函数使用 NEON 指令集进行向量化计算,提高计算效率。量化计算可以减少内存占用和计算量,提高大语言模型推理的性能,特别是在资源受限的环境中。
未来可以考虑优化 quantize_row_q8_0
函数、支持更多量化格式、使用更高级的 NEON 指令和混合精度计算等方向进行优化,进一步提高大语言模型在 ARM 平台上的推理性能。同时,也需要关注量化对模型精度的影响,通过合理的量化策略将精度损失控制在可接受的范围内。
