ggml介绍 (2) 量化 (Quantization)

在上一章 张量 (ggml_tensor) 中,我们了解了 ggml 是如何使用张量来表示数据的。我们知道,一个张量可以存储标准的 32 位浮点数(F32)。对于一个拥有 70 亿参数的大型语言模型(LLM),如果每个参数都是一个 F32 数字,那将需要 70亿 * 4字节/参数 ≈ 28GB 的内存!这对于大多数笔记本电脑甚至一些台式机来说都是一个巨大的挑战。

那么,我们有没有办法在不严重影响模型性能的情况下,让模型"瘦身"呢?答案是肯定的,而这正是本章的主角------量化 (Quantization)

什么是量化?

核心思想:量化是一种信息压缩技术,它通过降低数字的精度来减小模型大小和计算量。

让我们用一个生动的比喻来理解它。想象一下:

  • 32位浮点数 (F32) 就像一张专业相机拍摄的 RAW 格式高清照片。它保留了所有细节,色彩丰富,但文件巨大。
  • 4位整数 (Q4) 就像一张经过精心压缩的 JPEG 图片。虽然丢失了一些肉眼难以察觉的细节,但它的画质依然非常清晰,并且文件大小只有原来的几分之一。

量化做的就是类似的事情:它将模型中表示权重的高精度 F32 数字(占用32位)转换为低精度的整数,比如 4 位或 8 位。这个过程会带来三大好处:

  1. 更小的模型体积 :模型文件在硬盘上占用的空间大大减小。一个 28 GB 的 F32 模型,在 4 位量化后,大约只需要 28 / (32/4) = 3.5 GB
  2. 更低的内存占用:加载和运行模型所需的 RAM 或 VRAM 大幅降低,使得在消费级硬件(如你的笔记本电脑或手机)上运行大型模型成为可能。
  3. 更快的计算速度:许多现代 CPU 对整数运算的优化比浮点数运算更好,尤其是在没有高端 GPU 的情况下。使用整数进行计算可以显著提速。

ggml 的核心魅力之一就在于其强大而灵活的量化支持,这也是 llama.cpp 等项目能够在各种设备上高效运行的秘密武器。

ggml 如何实现量化?

你可能会问:"把一个像 3.1415926 这样的高精度浮点数变成一个只有 16 个可能取值(-8 到 7)的 4 位整数,难道不会丢失太多信息导致模型变傻吗?"

这是一个非常好的问题!ggml 采用了一种聪明的策略,叫做块量化 (Block Quantization) 。它不是孤立地转换每一个数字,而是将张量中的数字分成小组(称为"块",例如每块 32 个数字),然后为每个块单独进行量化。

这个过程有点像给每个小组配备一个"翻译工具包",这个工具包里主要有两样东西:

  1. 缩放因子 (Scale) :一个高精度的浮点数(通常是 F16)。它像一个放大镜,负责将块内原始 F32 数值的范围调整到一个适合低精度整数表示的范围。
  2. 量化后的整数 :块内每个 F32 数值在经过缩放后,四舍五入到最近的低位整数(例如 4 位整数)。

让我们通过一个简化的图示来看看这个过程:

graph TD subgraph original["原始F32数据块"] direction LR A[3.14] --> B[-1.59] --> C[0.8] --> D[...] end subgraph quantize["量化过程"] direction TB P1[" 找到块内最大绝对值, 计算缩放因子 (Scale)
例如: d = max(abs(x)) / 7"] P2[" 对每个数字应用缩放并四舍五入
例如: q = round(x / d)"] end subgraph result["量化后数据块"] S["缩放因子 (F16)
例如: 0.45"] Q["4位整数
[7, -4, 2, ...]"] end A -- "量化" --> P1 B -- "量化" --> P1 C -- "量化" --> P1 P1 --> P2 P2 -- "产出" --> S P2 -- "产出" --> Q

反量化(Dequantization) 则是这个过程的逆操作。当需要进行计算时(比如矩阵乘法),ggml 会使用缩放因子将这些 4 位整数"复原"成近似的浮点数(通常是 F16),然后再进行计算。

原始值 ≈ 量化整数 × 缩放因子 3.14 ≈ 7 × 0.45

通过为每个小块存储一个高精度的缩放因子,ggml 极大地保留了原始数值的动态范围,从而将精度损失控制在可接受的范围内。

ggml 的量化方案"大家族"

随着技术的发展,ggml 社区发明了多种量化方案,以在模型性能和资源消耗之间取得不同的平衡。你会在 GGUF 模型文件中看到类似 Q4_K_M, Q8_0, IQ2_XXS 这样的名字。作为初学者,你不需要了解它们的全部细节,但知道一些基本命名规则会很有帮助:

  • Q:代表"Quantized"(已量化)。
  • 数字 (2, 3, 4, 5, 6, 8):通常表示每个权重所占用的平均比特数。数字越小,压缩率越高,但潜在的精度损失也越大。
  • 后缀 (_0, _1, _K, _S, _M) :表示具体的量化策略。
    • _0_1 是早期的方案。
    • _K 代表一种更现代、更复杂的"超级块"(Super-block)结构。它通常能在相同的比特率下提供比旧方案更好的性能,是目前非常流行的选择。
    • IQ 代表"Incredibly Quantized",是一些追求极致压缩率的先进方案。

对于大多数用途,Q4_K_MQ5_K_M 在性能和大小之间取得了很好的平衡。

深入幕后:量化相关的函数

ggml 提供了一套函数来执行量化操作。通常,这些操作是在转换模型格式时(例如,从 PyTorch 格式转换为 GGUF 格式)一次性完成的,而不是在每次模型推理时都进行。

让我们看看 ggml 是如何将一个 F32 张量转换为量化张量的。

c 复制代码
#include "ggml.h"
#include <stdio.h>
#include <stdlib.h> // for malloc/free

// ... 假设我们有一些浮点数据 ...
#define N_ELEMENTS 256
float * my_f32_data = (float *) malloc(sizeof(float) * N_ELEMENTS);
// ... 用一些随机数据填充 my_f32_data ...

// 1. 选择量化类型
enum ggml_type q_type = GGML_TYPE_Q8_0; // 我们选择 8-bit 量化

// 2. 确定量化后需要多少内存
size_t q_data_size = ggml_row_size(q_type, N_ELEMENTS);
void * my_q8_data = malloc(q_data_size);

// 3. 执行量化
// 这个函数会处理所有块和缩放因子的计算
ggml_quantize_chunk(
    q_type,          // 目标量化类型
    my_f32_data,     // 源 F32 数据
    my_q8_data,      // 目标内存地址
    0,               // 起始元素索引
    1,               // 行数 (我们这里是扁平数组)
    N_ELEMENTS,      // 每行的元素数量
    NULL             // 重要性矩阵(imatrix),高级用法,通常为NULL
);

printf("原始 F32 数据大小: %zu 字节\n", sizeof(float) * N_ELEMENTS);
printf("量化后 Q8_0 数据大小: %zu 字节\n", q_data_size);

// ... 使用 my_q8_data ...

free(my_f32_data);
free(my_q8_data);

代码解释: 上面这段代码模拟了模型转换工具中的核心步骤。

  • 我们从一堆 F32 数据开始。
  • 我们调用 ggml_quantize_chunk,告诉它我们想要转换成 GGML_TYPE_Q8_0
  • ggml 内部会根据 Q8_0 的规则(比如每 32 个元素一个块)来处理 my_f32_data,计算每个块的缩放因子,并将量化后的数据写入 my_q8_data

内部调用流程

当你调用 ggml_quantize_chunk 时,ggml 内部会为你选择正确的量化函数。

sequenceDiagram participant UserApp as 用户应用 (如转换脚本) participant ggml as ggml API (ggml.c) participant Quants as 量化核心 (ggml-quants.c) UserApp->>ggml: 调用 ggml_quantize_chunk(GGML_TYPE_Q4_K, ...) ggml->>ggml: 检查类型 ggml_is_quantized(GGML_TYPE_Q4_K) ggml->>ggml: 获取该类型的特性 ggml_get_type_traits(GGML_TYPE_Q4_K) Note over ggml: 查找到处理 Q4_K 的
专用函数指针 from_float ggml->>Quants: 调用 quantize_row_q4_K(...) Quants->>Quants: 遍历数据, 按块处理 Quants->>Quants: 计算缩放因子和量化值 Quants-->>ggml: 返回量化后的数据 ggml-->>UserApp: 量化完成

正如你在 ggml-quants.hggml-cpu/quants.c 等文件中看到的,ggml 为每种量化类型都提供了专门的实现:

  • quantize_row_q4_0_ref()
  • quantize_row_q8_0_ref()
  • quantize_row_q4_K_ref()
  • ... 等等

同样,当 ggml 需要对量化张量进行计算时(例如,两个量化矩阵相乘),它也会调用高度优化的、特定于类型的计算函数(例如 ggml_vec_dot_q4_K_q8_K),这些函数知道如何高效地处理量化数据。

总结

在本章中,我们揭开了 ggml 高效运行大型模型的关键魔法------量化

  • 量化是一种压缩技术,通过降低数值精度来减小模型尺寸和内存占用,并加速计算。
  • ggml 使用块量化 策略,为每组数据存储一个缩放因子,以最小化精度损失。
  • ggml 支持多种量化方案(如 Q4_K_M, Q8_0),提供了在模型性能和资源消耗之间的多种选择。
  • 我们了解了 ggml_quantize_chunk 是将 F32 数据转换为量化格式的核心 API 函数。

现在我们已经掌握了 ggml 中数据的两种形态:高精度的张量 (ggml_tensor)和经过压缩的量化张量。但是,所有这些张量都需要在内存中进行创建和管理。我们如何才能有序地分配和组织这些张量的内存呢?

相关推荐
roman_日积跬步-终至千里2 分钟前
【深度学习】深度学习的四个核心步骤:从房价预测看机器学习本质
人工智能·深度学习·机器学习
wwww.bo5 分钟前
机器学习(1)
人工智能·机器学习
CV实验室11 分钟前
CVPR 2025 | 北大团队SLAM3R:单目RGB长视频实时重建,精度效率双杀!
人工智能·计算机视觉·论文·音视频
MARS_AI_24 分钟前
云蝠智能 VoiceAgent:重构物流售后场景的智能化引擎
人工智能·自然语言处理·重构·交互·信息与通信
SugarPPig1 小时前
TensorFlow 和 Transformer 的关系
人工智能·tensorflow·transformer
khystal1 小时前
ISTA为什么要加上软阈值激活函数?r若没有L1 正则化也要加其他激活函数吗?
神经网络·信号处理
极造数字1 小时前
深度剖析MES/MOM系统架构:功能模块与核心优势解析
大数据·人工智能·物联网·系统架构·制造
AI 嗯啦1 小时前
计算机视觉--opencv(代码详细教程)(二)
人工智能·opencv·计算机视觉
Moshow郑锴2 小时前
什么是主成分分析(PCA)和数据降维
人工智能·主成分分析·数据降维