量化
ggml中的张量为ggml_tensor,
ggml_tensor有几个值得注意的属性:
-
enum ggml_op op
表示这个tensor是从哪个操作得到。 -
enum ggml_type type
,为tensor的格式,
ggml_type的定义如下:
enum ggml_type {
GGML_TYPE_F32 = 0,
GGML_TYPE_F16 = 1,
GGML_TYPE_Q4_0 = 2,
GGML_TYPE_Q4_1 = 3,
// GGML_TYPE_Q4_2 = 4, support has been removed
// GGML_TYPE_Q4_3 (5) support has been removed
GGML_TYPE_Q5_0 = 6,
GGML_TYPE_Q5_1 = 7,
GGML_TYPE_Q8_0 = 8,
GGML_TYPE_Q8_1 = 9,
// k-quantizations
GGML_TYPE_Q2_K = 10,
GGML_TYPE_Q3_K = 11,
GGML_TYPE_Q4_K = 12,
GGML_TYPE_Q5_K = 13,
GGML_TYPE_Q6_K = 14,
GGML_TYPE_Q8_K = 15,
GGML_TYPE_I8,
GGML_TYPE_I16,
GGML_TYPE_I32,
GGML_TYPE_COUNT,
};
其中,包含Q的表示经过量化得到的数据类型。
ggml在处理神经网络正向传播时,首先构建计算图,记录了每个节点的operator,然后进行统一处理。
在进行计算时,针对是否是量化tensor,分情况进行处理。
比如说,针对add操作,对应的ggml_op为GGML_OP_ADD,就包括了ggml_compute_forward_add_f32
和ggml_compute_forward_add_q_f32
两个处理操作,分别处理非量化和量化的数据。
查看量化情况下的add操作
static void ggml_compute_forward_add_q_f32(
const struct ggml_compute_params * params,
const struct ggml_tensor * src0,
const struct ggml_tensor * src1,
struct ggml_tensor * dst)
{
...
ggml_to_float_t const dequantize_row_q = type_traits[type].to_float;
ggml_from_float_t const quantize_row_q = type_traits[dtype].from_float;
...
// unquantize row from src0 to temp buffer
dequantize_row_q(src0_row, wdata, ne00);
// add src1
ggml_vec_acc_f32(ne00, wdata, src1_row);
// quantize row to dst
if (quantize_row_q != NULL) {
quantize_row_q(wdata, dst_row, ne00);
} else {
memcpy(dst_row, wdata, ne0*nb0);
}
...
}
省略了部分代码 ,可以看到有两个神奇函数dequantize_row_q
和quantize_row_q
,在进行实际add操作是需要将量化数据转化成float进行add,再转换回量化
这两个函数来自
ggml_to_float_t const dequantize_row_q = type_traits[type].to_float;
ggml_from_float_t const quantize_row_q = type_traits[dtype].from_float;
从type_traits截取部分,
[GGML_TYPE_Q4_0] = {
.type_name = "q4_0",
.blck_size = QK4_0,
.type_size = sizeof(block_q4_0),
.is_quantized = true,
.to_float = (ggml_to_float_t) dequantize_row_q4_0,
.from_float = quantize_row_q4_0,
.from_float_reference = (ggml_from_float_t) quantize_row_q4_0_reference,
.vec_dot = ggml_vec_dot_q4_0_q8_0,
.vec_dot_type = GGML_TYPE_Q8_0,
},
type_traits中,以ggml_type为GGML_TYPE_Q4_0为例,实际上每个量化type都有对应的quantize和unquantize,为和float之间的转换。
于是关注下to_float也就是dequantize_row_q4_0
static void dequantize_row_q4_0(const block_q4_0 * restrict x, float * restrict y, int k) {
// x是需要dequantize的tensor, 以block_q4_0格式存储信息
// y是得到的tensor
// k是元素个数
static const int qk = QK4_0; // 32
assert(k % qk == 0); // 总元素个数需能被32整除
const int nb = k / qk; // k/32, dim0
for (int i = 0; i < nb; i++) {
const float d = GGML_FP16_TO_FP32(x[i].d); // 提取出这一组的scale
for (int j = 0; j < qk/2; ++j) {
const int x0 = (x[i].qs[j] & 0x0F) - 8; // 后4位处理
const int x1 = (x[i].qs[j] >> 4) - 8; // 前4位处理
y[i*qk + j + 0 ] = x0*d; // scale,将tensor拆开
y[i*qk + j + qk/2] = x1*d;
}
}
}
其中,QK4_0是32,注意到
y[i*qk + j + 0 ] = x0*d;
y[i*qk + j + qk/2] = x1*d;
这两行都乘了个d,为作者对delta的缩写,也就是scale,来自于x[i].d
x[i]
的类型为block_q4_0:
typedef struct {
ggml_fp16_t d; // delta
uint8_t qs[QK4_0 / 2]; // nibbles / quants
} block_q4_0;
block_q4_0的d就是一个fp16。
实参为void * src0_row = (void *) ((char *) src0->data + (i01*nb01 + i02*nb02 + i03*nb03));
也就是进行计算的的tensor,进行了类型转化。
至此,实现了ggml中的tensor的unquantize流程,quantize同理。
附上quantize代码
static void quantize_row_q4_0_reference(const float * restrict x, block_q4_0 * restrict y, int k) {
// x是需要quantize的tensor
// y是得到的tensor, 用block_q4_0格式存储信息
// k是元素个数
static const int qk = QK4_0; // 32
assert(k % qk == 0); // 总元素个数需能被32整除
const int nb = k / qk; // k/32, dim0, 划分的组数
for (int i = 0; i < nb; i++) {
float amax = 0.0f; // absolute max
float max = 0.0f;
for (int j = 0; j < qk; j++) {
const float v = x[i*qk + j];
if (amax < fabsf(v)) {
amax = fabsf(v);
max = v;
}
}
const float d = max / -8; // scale
const float id = d ? 1.0f/d : 0.0f; // 原本需要的除scale转化成乘
y[i].d = GGML_FP32_TO_FP16(d); // 保存这一组的scale
for (int j = 0; j < qk/2; ++j) {
const float x0 = x[i*qk + 0 + j]*id; // 量化
const float x1 = x[i*qk + qk/2 + j]*id;
const uint8_t xi0 = MIN(15, (int8_t)(x0 + 8.5f));
const uint8_t xi1 = MIN(15, (int8_t)(x1 + 8.5f));
y[i].qs[j] = xi0;
y[i].qs[j] |= xi1 << 4; // 两个int4组装成一个int8放在一起
}
}
}