您是否曾梦想在自己的笔记本电脑上直接运行一个属于您的 ChatGPT?
随着大型语言模型(LLM)的快速发展,将这些强大的模型带到消费级硬件上的可能性正逐步成为现实。
解锁这一潜力的关键在于量化, 这是一种将日益庞大的模型压缩到可在日常设备上运行、且性能损失极小的技术 ——前提是实现得当!
在本指南中,我们将深入讲解量化的概念、工作机制,以及对 LLM 进行量化的不同方法。最后,我们将使用 Hugging Face 的 Quanto 库,用两个简单步骤对模型进行量化。
让我们深入探索! 您可以在 DataCamp DataLab 中跟随操作。
大型语言模型的迅猛增长
随着 LLM 的演进,其复杂度呈指数级增长,参数数量显著增加。例如,2018 年发布的首个 GPT 模型仅有 1.1 亿参数。到 2019 年底,GPT-2 扩展到 15 亿参数,而 2020 年底发布的 GPT-3 则飙升至 1750 亿参数。
如今,GPT-4 的参数量已超过 1 万亿。这一巨大增长带来挑战: 模型越大,内存需求越高 ,往往超过诸如 GPU 等先进硬件加速器的容量。
不断增长的内存需求限制了模型的训练与推理部署,从而制约了 基于 LLM 的解决方案 的可及性与采用率。
因此,迫切需要通过缩小模型体积来提升其可用性。通过改变模型某些组件的数值精度,量化可以在保持相近性能的同时,减少模型的内存占用。
什么是量化?
量化是一种模型压缩技术,它将大型语言模型中的权重和激活从高精度数值转换为低精度数值。这意味着将数据从可承载更多信息的类型转换为承载更少信息的类型。典型示例是将 32 位浮点数转换为 8 位整数。
减少每个权重或激活所需的比特数,会显著降低模型的整体体积。因此,量化能使 LLM 占用更少内存、需要更少存储空间,并且更节能 。
一个理解量化的有效类比是图像压缩。高分辨率图像常用于网站时进行压缩,这会通过移除部分数据或信息位来减小图像大小。尽管这通常会在一定程度上降低图像质量,但也会减少图像尺寸和文件大小,使网页加载更快,同时仍提供令人满意的视觉效果。
用于 更快加载的图像压缩 示意图,适用于网页等应用。
类似地,对 LLM 进行量化可降低其计算需求,使其在性能较弱的硬件上也能运行,同时仍能提供足够的性能。正如压缩后的图像更易处理,量化后的模型也更易在各平台部署,尽管细节或精度会有轻微取舍。正如我们将看到的, 量化过程也会引入一定噪声。
量化理论
量化通常应用于大型语言模型的权重,但也可应用于激活。模型权重是 神经网络 中的参数,决定不同层间神经元连接的强度。 本质上,权重是将输入数据在网络中传递时进行变换的学习系数 。
权重最初被随机初始化为无意义的数值,并在训练过程中根据预测输出与真实目标之间的误差进行调整。该调整过程由诸如梯度下降等优化算法引导。
如果您想进一步了解 LLM 的内部机制,课程 Developing Large Language Models 适合您!
量化模型的一个方式是降低其权重的数值精度。为说明这一点,让我们聚焦下图左侧的矩阵,它是一个四位小数精度的 3x3 权重矩阵:
左:四位小数精度的随机权重矩阵;右:将元素四舍五入到一位小数后的量化形式。
右侧矩阵为原矩阵的量化版本。该“量化”矩阵通过将原矩阵元素四舍五入到一位小数得到。
可以看到上述两个矩阵并不完全相同,但非常接近。逐元素的差异称为 量化误差 ,我们也可以将其表示为矩阵形式:
矩阵形式的量化误差。颜色越深,误差越大。
当前量化研究的重点是尽可能减小这种差异,以避免性能下降。
在这个简单示例中,我们只是对矩阵元素进行了四舍五入。在实践中,量化是通过将数值转换为不同的数据类型来完成的,例如从高精度数据类型转换为低精度数据类型。比如,大多数模型的默认存储数据类型是 float32 。
在这种情况下,我们需要为每个参数分配 4 个字节(4×8 位)。因此,对于示例中的 3x3 矩阵,其总内存占用为 36 字节。
通过更改数据类型——也称为降型转换(downcasting)——为 int8 ,我们只需为每个参数分配 1 个字节。因此,矩阵的总内存占用变为 9 字节。
Brain floating point——BF16
为模型权重选择的数据类型决定了我们可以将模型缩小到何种程度。传统浮点类型如 float32 和 float16 ,一直是许多机器学习应用的标准,在精度与计算效率间取得平衡。具体而言, float32 具有更高精度与更宽动态范围,但需要更多内存与算力;相反, float16 的精度与范围更低,但可显著加速计算。
2018 年,Google 认识到需要一种在 float32 的宽动态范围与 float16 的高效之间取得中间点的浮点格式。
这催生了所谓的 Brain Floating Point( bfloat16 ),它保留了 float32 的动态范围,但降低了精度。
降型转换(Downcasting)
降型转换是将高精度数据类型转换为低精度数据类型的正式术语。 通过降型转换,我们减少内存占用并提升速度,因为使用低精度进行计算也需要更少内存。
本节我们将探讨如何从 float32 ——多数模型的默认存储数据类型——降型到 Google 的 bfloat16 ,并观察其通常会带来一定的数据丢失。
让我们先在 PyTorch 中定义一个元素类型为 float32 的随机张量,并显示前五个元素:
import torch
tensor_fp32 = torch. rand( 1000 , dtype = torch. float32)
print ( tensor_fp32[ : 5 ] )
>> tensor( [ 0.2257 , 0.0480 , 0.8520 , 0.3115 , 0.1373 ] ) 技术支持
现在我们可以使用 .to(dtype) 方法将该张量降型为 bfloat16 ,并查看前 5 个新元素:
tensor_fp32_to_bf16 = tensor_fp32. to( dtype = torch. bfloat16)
print ( tensor_fp32_to_bf16[ : 5 ] )
>> tensor( [ 0.2256 , 0.0481 , 0.8516 , 0.3105 , 0.1377 ] , dtype= torch. bfloat16) 技术支持
如您所见,这些值非常接近,但并不完全相同。当我们对这些值执行运算时,差异会更加明显。例如,将原始张量与自身相乘:
m_float32 = torch. dot( tensor_fp32, tensor_fp32)
print ( m_float32)
>> tensor( 322.1082 ) 技术支持
如果对量化后的张量执行相同运算,我们会看到差异更大:
m_bfloat16 = torch. dot( tensor_fp32_to_bf16, tensor_fp32_to_bf16)
print ( m_bfloat16)
>> tensor( 256. , dtype= torch. bfloat16) 技术支持
由于误差传播,我们可以观察到最终结果的明显差异。
在对 LLM 降型时,对量化张量进行运算同样会产生误差传播,从而导致信息丢失。正如我们所见,使用更少内存意味着计算可能不那么精确。 乘法的效应类似于真实的逐层误差传播,这些误差会累积并最终影响某些 token 的预测 。
使用 bfloat16 类型进行降型时,性能通常仍可接受,但对更小的数据类型则不成立。
由于这一数据类型限制,降型并不常被用作高效的量化技术。取而代之的是其他方法,它们在推理期间转换回 float32 ,从而更好地保持接近原模型的性能。
量化类型
量化有多种类型,我们在下文中逐一介绍:
线性量化
线性量化是 LLM 最常用的量化方案之一。简单来说,它将原始权重的浮点值范围均匀映射到定点值范围,同时在推理中使用高精度数据类型。
尽量简化起见,让我们回顾对一个模型应用线性量化所需的步骤。请注意,实际公式见下图:
计算最小值与最大值: 对每个张量,我们需要得到最小和最大值,以确定要量化的浮点值范围。目标数据类型将给出量化范围的最小与最大值。例如,对于无符号整数,8 位的范围为 0 到 255。
计算尺度( s )与零点( s )值: 尺度用于将浮点值范围缩放以适配整数范围。零点确保浮点范围中的 零 能够被某个整数准确表示,从而在数值上保持精度与稳定性,尤其是接近零的数值。
量化数值( q ): 此步骤将浮点值映射到较低精度的整数范围,使用上一步得到的尺度 s 与零点 s 。舍入操作保证最终结果为离散整数,便于在低精度格式中存储与计算。
反量化: 在推理过程中,使用反量化后的数值进行计算以获得更高精度,尽管内存中仅存储量化后的权重。此步骤也便于计算量化误差。
让我们将上述每个步骤与相应的公式对应起来:
用于将线性量化应用到任意权重矩阵的量化方程。
很难想象如何应用这些公式吗? 那就上手试试吧!
如果将这些公式应用于下图左侧的 3x3 权重张量,我们将得到右侧的量化矩阵。我建议您花几分钟计算量化范围的最大最小值以及部分量化数值:
左:两位小数精度的随机权重矩阵;右:量化为 int8 数据类型后的形式。
我们可以看到, int8 值的下界对应原始张量的最小值( -0.40 → 0 ),上界对应原始张量的最大值( 0.50 → 255 )。
如果我们现在使用公式(4)进行反量化,可以看到反量化后的数值接近原始数值(左侧矩阵)。我们可以通过逐点差值来计算量化误差(右侧矩阵):
使用量化权重以及尺度和零点值可进行反量化(左)。也可计算逐点差异或量化误差(右)。
线性量化通过仅在内存中存储量化权重以及尺度与零点值,并在推理时据此还原原始权重来维持性能,从而缩小模型体积。
分块量化(Blockwise quantization)
线性量化因其简单而受欢迎,但构建映射的方法不止一种。如今另一种颇受欢迎的方法是 分块量化 ,对于权重分布不均匀的模型,它比线性量化更准确。
分块量化是一种更复杂的方法,它在较小的块中对权重量化,而不是在整个范围上统一量化。该方法依赖两个关键概念:
按块量化: 将权重划分为更小的块,并分别对每个块进行量化。这有助于更好地处理模型不同部分的差异。
分布感知块: 量化过程会考虑每个块内权重的相对频率,从而创建对权重分布更敏感的块,实现更高效的数值映射。
权重量化 vs 激活量化
在此前的矩阵示例中,我们主要关注模型权重量化的过程。尽管权重量化是模型优化的关键步骤,但同样需要考虑模型的激活也可以被量化。
激活量化 是指降低网络中每一层中间输出的精度。与训练完成后静态(常数)的权重不同,激活是动态的。这意味着 激活会随每个输入而变化,使其范围更难预测 。
总体而言,激活量化比权重量化更难实施。它需要谨慎的校准,以确保良好捕捉激活的动态范围。
权重量化与激活量化是互补技术。通过同时应用这两种技术,我们可以在不 过多 牺牲性能的前提下,显著缩减模型体积。
训练后量化(PTQ)vs 量化感知训练(QAT)
量化也可以在不同时间点进行。如果我们对一个预训练模型在推理阶段对其参数进行量化,我们执行的是训练后量化(PTQ)。
这种方法不涉及对训练过程的任何更改。参数的动态范围会在运行时重新计算,类似于我们处理示例矩阵的方式。
另一方面,也可以进行量化感知训练(QAT)。这种方法需要修改训练流程,在训练期间模拟量化的影响。模型会学习对量化噪声更具鲁棒性,从而带来更好的准确率。
在 QAT 期间,训练的中间状态会同时保留量化后的权重与原始未量化权重(同样在内存中!)。因此,我们在推理中使用量化版模型,而在反向传播中更新未量化的模型权重。
不出所料, 尽管更复杂、更耗时,QAT 通常比 PTQ 获得更高的准确率 。
校准技术
一些量化方法需要校准步骤。例如,我们必须在量化前确定模型原始的激活范围。 通用校准通常需要在具有代表性的数据集上运行推理,以优化量化参数 并最小化量化误差。
在此校准过程中,量化算法会收集有关模型激活与权重的分布与范围的统计信息。这些统计量有助于确定最佳量化参数。在对权重量化时计算尺度与零点也是一种校准,但还有其他类型:
百分位校准: 聚焦于权重的特定百分位范围,忽略极端离群值,从而实现更稳健的量化。
均值与标准差校准: 基于权重的均值与标准差等统计量来定义量化范围。
然而,像 QLoRA 这样的量化方法可无需任何校准步骤。
这些方法通常将模型中的所有线性层替换为量化线性层( QLinear )。 QLinear 层被设计为在内部处理量化 ,从而无需额外的校准步骤。这使得量化过程更易实施,同时仍可保持模型性能。
量化工具
Python 中有多种工具与库支持量化,既提供 PTQ,也支持 QAT。例如, pytorch 与 tensorflow 提供了量化方法,但要在现有模型中无缝集成量化,需要对库与模型内部机制有深入理解。参见官方 PyTorch 文档中的 量化选项 。
如果您有兴趣学习这些强大的框架,我推荐课程 Deep Learning in Python 。
到目前为止,我最喜欢的简易量化实现方案是 Hugging Face 的 Quanto 库,它旨在简化 PyTorch 模型的量化过程。
模型量化的实操步骤
使用 Hugging Face 的 Quanto 库进行量化的典型流程包括以下步骤:
1. 选择并加载预训练模型及其对应的分词器。 这里我们将使用 EuletherAI 的 Pythia 410M 模型:
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "EleutherAI/pythia-410m"
model = AutoModelForCausalLM. from_pretrained( model_name,
low_cpu_mem_usage= True )
tokenizer = AutoTokenizer. from_pretrained( model_name) 技术支持
方法 model.gpt_neox 有助于可视化已加载模型的不同层:
print ( model. gpt_neox) 技术支持
原始模型的分层结构示意。模型由多种层构成,如线性层与归一化层。
我们可以通过运行一些测试推理来检查模型是否正常工作。例如,
text = "Once upon a time, there was a"
inputs = tokenizer( text, return_tensors= "pt" )
outputs = model. generate( ** inputs, max_new_tokens= 10 )
print ( tokenizer. decode( outputs[ 0 ] , skip_special_tokens= True ) ) 技术支持
此外,由于对模型进行量化将使其体积缩小,开始之前检查原模型大小也很有意义:
import torch
module_sizes = compute_module_sizes( model)
print ( f"The model size is { module_sizes[ '' ] * 1e-9 } GB" ) 技术支持
注意: 在 DataCamp DataLab 中可以找到已实现的 compute_module_sizes() 方法。
最后,我们还可以如下可视化原模型的稠密张量:
print ( model. gpt_neox. layers[ 0 ] . attention. dense. weight) 技术支持
可以看到,它与我们在本文中使用的未量化矩阵相似。
2. 量化。 使用 quantize() 方法可直接将默认的标准浮点模型转换为量化模型。
from quanto import quantize, freeze
quantize( model, weights= torch. int8, activations= None ) 技术支持
此处我们仅将模型权重量化为 int8 数据类型。该方法将模型转换为使用低精度算术,包括校准步骤。
如果现在打印模型的各层,将看到原始线性层( Linear )已被量化线性层( QLinear )替换:
print ( model) 技术支持
量化模型的分层结构示意。注意所有原始线性层都已变为量化线性层( QLinear )。
然而,如果打印权重矩阵,会发现它们尚未被转换:
print ( model. gpt_neox. layers[ 0 ] . attention. dense. weight) 技术支持
3. 冻结(Freeze)。 要将量化效果应用到权重,需要使用 freeze() 方法。
freeze( model) 技术支持
该方法将量化参数嵌入模型,有效地将权重转换为目标数据类型。现在让我们查看量化后的模型参数:
print ( model. gpt_neox. layers[ 0 ] . attention. dense. weight) 技术支持
如您所见,这些权重现在处于 Pytorch 的 int8 数据类型范围内。
4. 最终检查。 模型量化完成后,我们可以查看缩减后的新模型大小:
module_sizes = compute_module_sizes( model)
print ( f"The model size is { module_sizes[ '' ] * 1e-9 } GB" ) 技术支持
我们将模型体积缩减到了原来的 35%!
最后,用一次简单的推理来检查模型的新性能:
outputs = model. generate( ** inputs, max_new_tokens= 10 )
print ( tokenizer. decode( outputs[ 0 ] , skip_special_tokens= True ) ) 技术支持
当然,只通过一次推理来评估性能并不具有意义,因此制定定量评估方法是个好做法。
现在轮到您来尝试了!
结论
LLM 日益增长的内存需求限制了其训练与推理托管,从而制约了 基于 LLM 的应用 的可及性与采用率。
本文介绍了量化这一概念,即通过降低模型权重与激活的精度来减少其内存占用与计算需求。该过程有助于让模型更高效、更易用,尤其适合在资源受限设备上部署。
我们先通过示例,用对权重数值取整的方式来理解该技术;随后转向一种简单的量化类型——降型转换。对模型进行降型时,会将参数转换为更紧凑的数据类型,如 bfloat16,用于推理。尽管这使模型能够在更小的数据类型中进行计算与激活,但其性能通常会随数据类型的减小而下降,对 int8 等整型类型则效果不佳。
为解决这一问题,我们引入了 线性量化 ,它在推理时转换回 float32 ,从而使性能更接近原模型。这也使得可以使用更小的数据类型,如 int8。
我们逐步演示了应用线性量化的数值步骤,并使用 Hugging Face 的 Quanto 库实现了权重量化。 值得一提的是,借助 Quanto 库,我们可以量化任何 PyTorch 模型, 包括 Hugging Face 上可用的模型。
如果您有兴趣尝试更重的模型,如 LLaMa 2 或 Mistral ,不妨将您的 DataCamp 空间升级到高级版,以获得更多算力。
更大型的模型可能需要 DataCamp 高级计划 才能获取的更多资源。
掌握量化技术, 我们就能在更少资源的前提下释放大型语言模型的全部潜力 ,让先进的 AI 更高效、更易用、也更通用。
希望本文能帮助您上手 LLM 的量化!
您是否曾梦想在自己的笔记本电脑上直接运行一个属于您的 ChatGPT?
随着大型语言模型(LLM)的快速发展,将这些强大的模型带到消费级硬件上的可能性正逐步成为现实。
解锁这一潜力的关键在于量化,这是一种将日益庞大的模型压缩到可在日常设备上运行、且性能损失极小的技术——前提是实现得当!
在本指南中,我们将深入讲解量化的概念、工作机制,以及对 LLM 进行量化的不同方法。最后,我们将使用 Hugging Face 的Quanto 库,用两个简单步骤对模型进行量化。
让我们深入探索!您可以在 DataCamp DataLab 中跟随操作。
大型语言模型的迅猛增长
随着 LLM 的演进,其复杂度呈指数级增长,参数数量显著增加。例如,2018 年发布的首个 GPT 模型仅有 1.1 亿参数。到 2019 年底,GPT-2 扩展到 15 亿参数,而 2020 年底发布的 GPT-3 则飙升至 1750 亿参数。
如今,GPT-4 的参数量已超过 1 万亿。这一巨大增长带来挑战:模型越大,内存需求越高,往往超过诸如 GPU 等先进硬件加速器的容量。
不断增长的内存需求限制了模型的训练与推理部署,从而制约了基于 LLM 的解决方案的可及性与采用率。
因此,迫切需要通过缩小模型体积来提升其可用性。通过改变模型某些组件的数值精度,量化可以在保持相近性能的同时,减少模型的内存占用。
什么是量化?
量化是一种模型压缩技术,它将大型语言模型中的权重和激活从高精度数值转换为低精度数值。这意味着将数据从可承载更多信息的类型转换为承载更少信息的类型。典型示例是将 32 位浮点数转换为 8 位整数。
减少每个权重或激活所需的比特数,会显著降低模型的整体体积。因此,量化能使 LLM 占用更少内存、需要更少存储空间,并且更节能。
一个理解量化的有效类比是图像压缩。高分辨率图像常用于网站时进行压缩,这会通过移除部分数据或信息位来减小图像大小。尽管这通常会在一定程度上降低图像质量,但也会减少图像尺寸和文件大小,使网页加载更快,同时仍提供令人满意的视觉效果。
用于更快加载的图像压缩示意图,适用于网页等应用。
类似地,对 LLM 进行量化可降低其计算需求,使其在性能较弱的硬件上也能运行,同时仍能提供足够的性能。正如压缩后的图像更易处理,量化后的模型也更易在各平台部署,尽管细节或精度会有轻微取舍。正如我们将看到的,量化过程也会引入一定噪声。
量化理论
量化通常应用于大型语言模型的权重,但也可应用于激活。模型权重是神经网络中的参数,决定不同层间神经元连接的强度。本质上,权重是将输入数据在网络中传递时进行变换的学习系数。
权重最初被随机初始化为无意义的数值,并在训练过程中根据预测输出与真实目标之间的误差进行调整。该调整过程由诸如梯度下降等优化算法引导。
如果您想进一步了解 LLM 的内部机制,课程Developing Large Language Models 适合您!
量化模型的一个方式是降低其权重的数值精度。为说明这一点,让我们聚焦下图左侧的矩阵,它是一个四位小数精度的 3x3 权重矩阵:
左:四位小数精度的随机权重矩阵;右:将元素四舍五入到一位小数后的量化形式。
右侧矩阵为原矩阵的量化版本。该“量化”矩阵通过将原矩阵元素四舍五入到一位小数得到。
可以看到上述两个矩阵并不完全相同,但非常接近。逐元素的差异称为量化误差,我们也可以将其表示为矩阵形式:
矩阵形式的量化误差。颜色越深,误差越大。
当前量化研究的重点是尽可能减小这种差异,以避免性能下降。
在这个简单示例中,我们只是对矩阵元素进行了四舍五入。在实践中,量化是通过将数值转换为不同的数据类型来完成的,例如从高精度数据类型转换为低精度数据类型。比如,大多数模型的默认存储数据类型是
float32。在这种情况下,我们需要为每个参数分配 4 个字节(4×8 位)。因此,对于示例中的 3x3 矩阵,其总内存占用为 36 字节。
通过更改数据类型——也称为降型转换(downcasting)——为
int8,我们只需为每个参数分配 1 个字节。因此,矩阵的总内存占用变为 9 字节。Brain floating point——BF16
为模型权重选择的数据类型决定了我们可以将模型缩小到何种程度。传统浮点类型如
float32和float16,一直是许多机器学习应用的标准,在精度与计算效率间取得平衡。具体而言,float32具有更高精度与更宽动态范围,但需要更多内存与算力;相反,float16的精度与范围更低,但可显著加速计算。float32的宽动态范围与 float16 的高效之间取得中间点的浮点格式。这催生了所谓的 Brain Floating Point(
bfloat16),它保留了float32的动态范围,但降低了精度。降型转换(Downcasting)
降型转换是将高精度数据类型转换为低精度数据类型的正式术语。 通过降型转换,我们减少内存占用并提升速度,因为使用低精度进行计算也需要更少内存。
本节我们将探讨如何从
float32——多数模型的默认存储数据类型——降型到 Google 的bfloat16,并观察其通常会带来一定的数据丢失。让我们先在 PyTorch 中定义一个元素类型为
float32的随机张量,并显示前五个元素:现在我们可以使用
.to(dtype)方法将该张量降型为bfloat16,并查看前 5 个新元素:如您所见,这些值非常接近,但并不完全相同。当我们对这些值执行运算时,差异会更加明显。例如,将原始张量与自身相乘:
如果对量化后的张量执行相同运算,我们会看到差异更大:
由于误差传播,我们可以观察到最终结果的明显差异。
在对 LLM 降型时,对量化张量进行运算同样会产生误差传播,从而导致信息丢失。正如我们所见,使用更少内存意味着计算可能不那么精确。乘法的效应类似于真实的逐层误差传播,这些误差会累积并最终影响某些 token 的预测。
使用
bfloat16类型进行降型时,性能通常仍可接受,但对更小的数据类型则不成立。由于这一数据类型限制,降型并不常被用作高效的量化技术。取而代之的是其他方法,它们在推理期间转换回
float32,从而更好地保持接近原模型的性能。量化类型
量化有多种类型,我们在下文中逐一介绍:
线性量化
线性量化是 LLM 最常用的量化方案之一。简单来说,它将原始权重的浮点值范围均匀映射到定点值范围,同时在推理中使用高精度数据类型。
尽量简化起见,让我们回顾对一个模型应用线性量化所需的步骤。请注意,实际公式见下图:
s)与零点(s)值: 尺度用于将浮点值范围缩放以适配整数范围。零点确保浮点范围中的零能够被某个整数准确表示,从而在数值上保持精度与稳定性,尤其是接近零的数值。q): 此步骤将浮点值映射到较低精度的整数范围,使用上一步得到的尺度 s 与零点 s 。舍入操作保证最终结果为离散整数,便于在低精度格式中存储与计算。让我们将上述每个步骤与相应的公式对应起来:
用于将线性量化应用到任意权重矩阵的量化方程。
很难想象如何应用这些公式吗?那就上手试试吧!
如果将这些公式应用于下图左侧的 3x3 权重张量,我们将得到右侧的量化矩阵。我建议您花几分钟计算量化范围的最大最小值以及部分量化数值:
左:两位小数精度的随机权重矩阵;右:量化为int8 数据类型后的形式。
我们可以看到,
int8值的下界对应原始张量的最小值(-0.40 → 0),上界对应原始张量的最大值(0.50 → 255)。如果我们现在使用公式(4)进行反量化,可以看到反量化后的数值接近原始数值(左侧矩阵)。我们可以通过逐点差值来计算量化误差(右侧矩阵):
使用量化权重以及尺度和零点值可进行反量化(左)。也可计算逐点差异或量化误差(右)。
线性量化通过仅在内存中存储量化权重以及尺度与零点值,并在推理时据此还原原始权重来维持性能,从而缩小模型体积。
分块量化(Blockwise quantization)
线性量化因其简单而受欢迎,但构建映射的方法不止一种。如今另一种颇受欢迎的方法是分块量化,对于权重分布不均匀的模型,它比线性量化更准确。
分块量化是一种更复杂的方法,它在较小的块中对权重量化,而不是在整个范围上统一量化。该方法依赖两个关键概念:
权重量化 vs 激活量化
在此前的矩阵示例中,我们主要关注模型权重量化的过程。尽管权重量化是模型优化的关键步骤,但同样需要考虑模型的激活也可以被量化。
激活量化是指降低网络中每一层中间输出的精度。与训练完成后静态(常数)的权重不同,激活是动态的。这意味着激活会随每个输入而变化,使其范围更难预测。
总体而言,激活量化比权重量化更难实施。它需要谨慎的校准,以确保良好捕捉激活的动态范围。
权重量化与激活量化是互补技术。通过同时应用这两种技术,我们可以在不过多牺牲性能的前提下,显著缩减模型体积。
训练后量化(PTQ)vs 量化感知训练(QAT)
量化也可以在不同时间点进行。如果我们对一个预训练模型在推理阶段对其参数进行量化,我们执行的是训练后量化(PTQ)。
这种方法不涉及对训练过程的任何更改。参数的动态范围会在运行时重新计算,类似于我们处理示例矩阵的方式。
另一方面,也可以进行量化感知训练(QAT)。这种方法需要修改训练流程,在训练期间模拟量化的影响。模型会学习对量化噪声更具鲁棒性,从而带来更好的准确率。
在 QAT 期间,训练的中间状态会同时保留量化后的权重与原始未量化权重(同样在内存中!)。因此,我们在推理中使用量化版模型,而在反向传播中更新未量化的模型权重。
不出所料,尽管更复杂、更耗时,QAT 通常比 PTQ 获得更高的准确率。
校准技术
一些量化方法需要校准步骤。例如,我们必须在量化前确定模型原始的激活范围。通用校准通常需要在具有代表性的数据集上运行推理,以优化量化参数并最小化量化误差。
在此校准过程中,量化算法会收集有关模型激活与权重的分布与范围的统计信息。这些统计量有助于确定最佳量化参数。在对权重量化时计算尺度与零点也是一种校准,但还有其他类型:
然而,像QLoRA 这样的量化方法可无需任何校准步骤。
这些方法通常将模型中的所有线性层替换为量化线性层(
QLinear)。QLinear 层被设计为在内部处理量化,从而无需额外的校准步骤。这使得量化过程更易实施,同时仍可保持模型性能。量化工具
Python 中有多种工具与库支持量化,既提供 PTQ,也支持 QAT。例如,
pytorch与tensorflow提供了量化方法,但要在现有模型中无缝集成量化,需要对库与模型内部机制有深入理解。参见官方 PyTorch 文档中的量化选项。如果您有兴趣学习这些强大的框架,我推荐课程Deep Learning in Python。
到目前为止,我最喜欢的简易量化实现方案是 Hugging Face 的Quanto 库,它旨在简化 PyTorch 模型的量化过程。
模型量化的实操步骤
使用 Hugging Face 的 Quanto 库进行量化的典型流程包括以下步骤:
1. 选择并加载预训练模型及其对应的分词器。 这里我们将使用 EuletherAI 的Pythia 410M 模型:
方法
model.gpt_neox有助于可视化已加载模型的不同层:原始模型的分层结构示意。模型由多种层构成,如线性层与归一化层。
我们可以通过运行一些测试推理来检查模型是否正常工作。例如,
此外,由于对模型进行量化将使其体积缩小,开始之前检查原模型大小也很有意义:
注意: 在DataCamp DataLab 中可以找到已实现的
compute_module_sizes()方法。最后,我们还可以如下可视化原模型的稠密张量:
可以看到,它与我们在本文中使用的未量化矩阵相似。
2. 量化。 使用
quantize()方法可直接将默认的标准浮点模型转换为量化模型。此处我们仅将模型权重量化为
int8数据类型。该方法将模型转换为使用低精度算术,包括校准步骤。如果现在打印模型的各层,将看到原始线性层(
Linear)已被量化线性层(QLinear)替换:量化模型的分层结构示意。注意所有原始线性层都已变为量化线性层(QLinear)。
然而,如果打印权重矩阵,会发现它们尚未被转换:
3. 冻结(Freeze)。 要将量化效果应用到权重,需要使用
freeze()方法。该方法将量化参数嵌入模型,有效地将权重转换为目标数据类型。现在让我们查看量化后的模型参数:
如您所见,这些权重现在处于 Pytorch 的
int8数据类型范围内。4. 最终检查。 模型量化完成后,我们可以查看缩减后的新模型大小:
我们将模型体积缩减到了原来的 35%!
最后,用一次简单的推理来检查模型的新性能:
当然,只通过一次推理来评估性能并不具有意义,因此制定定量评估方法是个好做法。
现在轮到您来尝试了!
结论
LLM 日益增长的内存需求限制了其训练与推理托管,从而制约了基于 LLM 的应用的可及性与采用率。
本文介绍了量化这一概念,即通过降低模型权重与激活的精度来减少其内存占用与计算需求。该过程有助于让模型更高效、更易用,尤其适合在资源受限设备上部署。
我们先通过示例,用对权重数值取整的方式来理解该技术;随后转向一种简单的量化类型——降型转换。对模型进行降型时,会将参数转换为更紧凑的数据类型,如
bfloat16,用于推理。尽管这使模型能够在更小的数据类型中进行计算与激活,但其性能通常会随数据类型的减小而下降,对int8等整型类型则效果不佳。为解决这一问题,我们引入了线性量化,它在推理时转换回
float32,从而使性能更接近原模型。这也使得可以使用更小的数据类型,如int8。我们逐步演示了应用线性量化的数值步骤,并使用Hugging Face 的 Quanto 库实现了权重量化。值得一提的是,借助 Quanto 库,我们可以量化任何 PyTorch 模型,包括 Hugging Face 上可用的模型。
如果您有兴趣尝试更重的模型,如LLaMa 2 或 Mistral,不妨将您的 DataCamp 空间升级到高级版,以获得更多算力。
更大型的模型可能需要DataCamp 高级计划才能获取的更多资源。
掌握量化技术,我们就能在更少资源的前提下释放大型语言模型的全部潜力,让先进的 AI 更高效、更易用、也更通用。
希望本文能帮助您上手 LLM 的量化!