大模型LLM.int8()量化技术原理与代码实现

大语言模型LLM因其庞大的参数规模,往往难以在消费级硬件上直接运行。这些模型的参数量可能达到数十亿级别(主要是权重),这些参数不仅存储成本高,推理阶段的计算量也很大。通常需要显存较大的GPU来加速推理过程。

因此,越来越多的研究开始关注如何缩小模型,比如改进训练方法或引入适配器模块。其中一项关键技术便是量化(quantization)。

本文将深入探讨量化的基本原理,介绍LLM.int8()大模型量化方法,并通过具体的代码实战来展示如何实现模型的量化,以便在各种设备上高效运行这些模型。

一、基础知识

1、数值表示

模型推理过程中,激活值是输入和权重之积,因此权重数量越多,激活值也会越大。

因此,我们需要尽可能高效表示数十亿个值,从而尽可能减少存储参数所需的空间。

大语言模型中参数数值,通常被表示为浮点数。浮点数(floating-point numbers,简称 floats)是一种用于表示带有小数点的正数或负数的数据类型。

在计算机科学中,浮点数通常遵循IEEE-754标准进行存储。这一标准定义了浮点数的结构,包括符号位、指数位和尾数位三个组成部分。其中,

  • 符号位(Sign Bit):决定了浮点数的正负。

  • 指数位(Exponent Bits):表示浮点数的指数部分。

  • 尾数位(Fraction Bits):表示小数点后的数值部分。

三部分结合起来,即可根据一组bit值计算出所表示的数值。使用的位数越多,表示的数值值通常越精确。

  • 半精度浮点数(Half-Precision Floats, FP16):使用16位来表示一个浮点数,其中包括1位符号位、5位指数位和10位尾数位。

  • 单精度浮点数(Single-Precision Floats, FP32):使用32位来表示一个浮点数,其中包括1位符号位、8位指数位和23位尾数位。

2、动态范围与精度

动态范围(Dynamic Range):指的是可表示的最大值与最小值之间的范围。可用的位数越多,动态范围也就越广。

精度(Precision):指两个相邻可表示值之间的差距。可用的位数越多,精度也就越高。

对于给定的浮点数表示形式,我们可以计算出存储特定数值所需的内存大小。例如,对于32位浮点数(FP32),每个数值占用4字节(8位/字节),而对于16位浮点数(FP16),每个数值占用2字节。

假设模型有N个参数,每个参数使用B位表示,则模型的内存需求(以字节为单位)可以用以下公式计算:

在实际应用中,除了模型本身的参数外,推理过程中的内存/显存需求还受到其他因素的影响,例如:

  • 上下文大小:对于序列模型而言,处理的序列长度会影响内存需求。

  • 模型架构:不同的模型架构可能会有不同的内存使用模式。

对于一个包含700亿参数的模型,如果使用32位浮点数表示,所需的内存为:

如果改为使用16位浮点数表示,所需的内存将减少一半:

由此可见,将模型参数的表示位数最小化,即量化(不仅是推理,还有训练过程)能够显著减少内存需求,但这也意味着精度的降低,可能会对模型的准确性产生负面影响。

「因此,量化技术的目标是在保持模型准确性的同时尽可能减少表示数值所需的位数。」


二、什么是模型量化?

所谓模型量化,其实就是将模型参数的精度从较高位宽(如FP16、FP32、BF16,一般是浮点运算)转换为较低位宽(如Int8、Int4,一般是整数运算),从而降低模型存储大小及显存占用、提升推理性能。

三、量化分类

模型量化可从以下几方面分类:

根据量化时机

  • 训练时量化(Quantization-Aware Training, QAT),需要模型重新训练。
  • 训练后量化(Post Training Quantization,PTQ),可以量化预训练好的模型。不需要重新训练。

根据映射函数是否为线性

  • 线性量化
  • 非线性量化

根据量化的粒度(共享量化参数的范围)

  • Tensor粒度(per-tensor):整个矩阵一起量化。
  • Token粒度(per-token)和Channel粒度(per-channel):每行/每列单独量化,X的每一行代表一个Token,W的每一列代表一个Channel。
  • Group粒度(per-group):两者的折衷,多行/多列分为一组,每组分别量化。

根据量化范围

  • 只量化权重(Weight Only):只量化模型权重,推理时是INT乘FLOAT。

  • 权重与激活同时量化(Weight and Activation):这里的激活实际是就是每一层的输入,对于矩阵乘法Y = WX,同时量化W和X,推理时是INT乘INT。

    目前Weight and Activation可以做到Int8(或者叫W8A8,Weight 8bit Activition 8bit)与FP16水平相当,而Weight Only方向INT4(W4A16)已经可以做到与FP16相差无几,INT3(W3A16)也很接近了。实际上,这两个方向并不是互斥的,我们完全可以同时应用两种方式,只是工程比较复杂,暂时还没有成熟的框架。

根据存储一个权重元素所需的位数

  • 8bit量化
  • 4bit量化
  • 2bit量化
  • 1bit量化

四、量化方案

1、LLM.int8()

LLM.int8()量化算法

在这里插入图片描述

如下图所示为通过该方式实现量化-反量化的示例。假设使用absmax quantization技术对向量[1.2, -0.5, -4.3, 1.2, -3.1, 0.8, 2.4, 5.4]进行量化。首先找到绝对值最大值5.4。Int8的取值范围是[-127, 127],所以量化因子为127/5.4=23.5。因此向量会被量化成[28, -12, -101, 28, -73, 19, 56, 127]。

为了还原原始值,可以使用全精度的int8数值除以量化因子23.5。但是它们在量化过程中是四舍五入取整过的,会损失一些精度。

为了不区分int8格式的正负符号,我们需要减去最小值,然后再使用最大值作为量化因子。具体实现如下:
在这里插入图片描述

这类似于zero-point量化,但不同之处在于,zero-point量化会确保全精度数值0仍然转换为整数0,从而在数值0处保证不会有量化损失。

LLM.int8()算法的具体步骤:

  1. 从矩阵隐藏层中,以列为单位,抽取值大于确定阈值的异常值(outliers)。
  2. 分别通过FP16精度对outliers的部分做矩阵乘法,通过量化int8精度对其他的做矩阵乘法。
  3. 将量化的部分恢复成FP16,然后将两部分合在一起。

在这里插入图片描述

「为什么要单独抽出异常值(outliers)?」

在大规模模型中,数值超出全局阈值范围的被称为outliers。8位精度的数据是压缩的,因此量化一个含有几个大数值的向量会导致大量错误的结果。例如,如果一个向量中有几个数值远大于其他数值,那么量化这些数值会导致其他数值被压缩到零,从而产生较大的误差。

Transformer 架构的模型会将所有的内置特征连接组合在一起。因此,这些量化错误会在多层网络的传播中逐步混合在一起,导致整体性能的下降。

为了解决这些问题,混合精度量化技术应运而生。这种技术将大数值的特征拆分出来,进行更有效的量化和混合精度计算。

LLM.int8()量化实现

如下是在Transformers库中集成nuances库,利用bitsandbytes库提供的8位量化功能,将模型转换为int8精度。

第一步:导入库

import torch
import torch.nn as nn
import bitsandbytes as bnb
from bnb.nn import Linear8bitLt

在自己的数据集和任务上训练模型了,最后保存模型定义自己的模型。可以从任何精度(FP16,BF16,FP32)转换至int8。但模型的输入需要是FP16精度。所以下面为FP16精度的模型。

fp16_model = nn.Sequential(
    nn.Linear(64, 64),
    nn.Linear(64, 64)
)

第三步:在自己的数据集和任务上训练模型,最后保存模型

# 训练模型
[... train the model ...]

# 保存模型
torch.save(fp16_model.state_dict(), "model.pt")

第四步:定义一个int8精度的模型。

int8_model = nn.Sequential(
    Linear8bitLt(64, 64, has_fp16_weights=False),
    Linear8bitLt(64, 64, has_fp16_weights=False)
)

这里加入has_fp16_weights的参数是很重要的。因为它默认会被设置为True,这意味着它会被作为Int8/FP16混合精度训练。然而,我们关心的是use has_fp16_weights=False时的计算内存占用。

第五步:加载模型并量化至int8精度。

int8_model.load_state_dict(torch.load("model.pt"))
int8_model = int8_model.to(0)  # 量化

int8_model = int8_model.to(0) 将模型存入显卡,会执行量化。

如果在其之前打印int8_model[0]的权重,可得到FP16的精度值。

print("Before quantization:")
print(int8_model[0].weight)


Parameter containing:
tensor([[ 0.0031, -0.0438, 0.0494, ..., -0.0046, -0.0410, 0.0436],
        [-0.1013, 0.0394, 0.0787, ..., 0.0986, 0.0595, 0.0162],
        [-0.0859, -0.1227, -0.1209, ..., 0.1158, 0.0186, -0.0530],
        ...,
        [ 0.0804, 0.0725, 0.0638, ..., -0.0487, -0.0524, -0.1076],
        [-0.0200, -0.0406, 0.0663, ..., 0.0123, 0.0551, -0.0121],
        [-0.0041, 0.0865, -0.0013, ..., -0.0427, -0.0764, 0.1189]],
 dtype=torch.float16)

如果在其之后打印int8_model[0]的权重,可得到INT8的精度值。

print("After quantization:")
print(int8_model[0].weight)
Parameter containing:
tensor([[ 3, -47, 54, ..., -5, -44, 47],
        [-104, 40, 81, ..., 101, 61, 17],
        [ -89, -127, -125, ..., 120, 19, -55],
        ...,
        [ 82, 74, 65, ..., -49, -53, -109],
        [ -21, -42, 68, ..., 13, 57, -12],
        [ -4, 88, -1, ..., -43, -78, 121]],
 device='cuda:0', dtype=torch.int8, requires_grad=True)

由此可见,权重值被压缩了,分布在[-127,127]之间。

如需恢复FP16精度,则:

print("Restored FP16 precision:")
print((int8_model[0].weight.CB * int8_model[0].weight.SCB) / 127)

然后得到:

tensor([[ 0.0028, -0.0459, 0.0522, ..., -0.0049, -0.0428, 0.0462],
        [-0.0960, 0.0391, 0.0782, ..., 0.0994, 0.0593, 0.0167],
        [-0.0822, -0.1240, -0.1207, ..., 0.1181, 0.0185, -0.0541],
        ..., 
        [ 0.0757, 0.0723, 0.0628, ..., -0.0482, -0.0516, -0.1072],
        [-0.0194, -0.0410, 0.0657, ..., 0.0128, 0.0554, -0.0118], 
        [-0.0037, 0.0859, -0.0010, ..., -0.0423, -0.0759, 0.1190]],
 device='cuda:0')

这和原始的FP16精度权重非常接近。

第六步:在同一个显卡上用FP16精度计算模型。

input_ = torch.randn(64, dtype=torch.float16)
hidden_states = int8_model(input_.to(torch.device('cuda', 0)))

第七步:集成到Transformer库。

  • 使用accelerate库初始化模型。当处理大型模型时,accelerate库提供了很多便利,特别是在内存管理和模型初始化方面。init_empty_weights方法可以帮助任何模型在初始化时不占用任何内存。
import torch.nn as nn
from accelerate import init_empty_weights
with init_empty_weights():
    model = nn.Sequential([nn.Linear(100000, 100000) for _ in range(1000)]) # This will take ~0 RAM!
  • 修改.from_pretrained。当调用函数.from_pretrained时,会内置将所有参数调用torch.nn.Parameter,这不符合功能模块Linear8bitLt。因此,将Actor生成的
module._parameters[name] = nn.Parameter(module._parameters[name].to(torch.device("meta")))   

修改为:

param_cls = type(module._parameters[name])
kwargs = module._parameters[name].__dict__
module._parameters[name] = param_cls(module._parameters[name].to(torch.device("meta")), **kwargs)
  • 替换 nn.Linear 层为 Linear8bitLt 层
def replace_8bit_linear(model, threshold=6.0, module_to_not_convert="lm_head"):
    for name, module in model.named_children():
        if len(list(module.children())) > 0:
            replace_8bit_linear(module, threshold, module_to_not_convert)

        if isinstance(module, nn.Linear) and name != module_to_not_convert:
            with init_empty_weights():
                model._modules[name] = bnb.nn.Linear8bitLt(
                    module.in_features,
                    module.out_features,
                    module.bias is not None,
                    # 参数has_fp16_weights需要被设置为False,从而直接加载模型权重为int8精度。
                    has_fp16_weights=False,
                    threshold=threshold
                )
    return model
model = replace_8bit_linear(model, threshold=6.0)   

五、如何学习大模型?

学习AI大模型是一个系统的过程,需要从基础开始,逐步深入到更高级的技术。

这里给大家精心整理了一份全面的AI大模型学习资源,包括:AI大模型全套学习路线图(从入门到实战)、精品AI大模型学习书籍手册、视频教程、实战学习、面试题等,资料免费分享!

1. 成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

这里,我们为新手和想要进一步提升的专业人士准备了一份详细的学习成长路线图和规划。可以说是最科学最系统的学习成长路线。
在这里插入图片描述

2. 大模型经典PDF书籍

书籍和学习文档资料是学习大模型过程中必不可少的,我们精选了一系列深入探讨大模型技术的书籍和学习文档,它们由领域内的顶尖专家撰写,内容全面、深入、详尽,为你学习大模型提供坚实的理论基础(书籍含电子版PDF)

在这里插入图片描述

3. 大模型视频教程

对于很多自学或者没有基础的同学来说,书籍这些纯文字类的学习教材会觉得比较晦涩难以理解,因此,我们提供了丰富的大模型视频教程,以动态、形象的方式展示技术概念,帮助你更快、更轻松地掌握核心知识

在这里插入图片描述

4. 大模型项目实战

学以致用 ,当你的理论知识积累到一定程度,就需要通过项目实战,在实际操作中检验和巩固你所学到的知识,同时为你找工作和职业发展打下坚实的基础。

在这里插入图片描述

5. 大模型面试题

面试不仅是技术的较量,更需要充分的准备。

在你已经掌握了大模型技术之后,就需要开始准备面试,我们将提供精心整理的大模型面试题库,涵盖当前面试中可能遇到的各种技术问题,让你在面试中游刃有余。

在这里插入图片描述

全套的AI大模型学习资源已经整理打包,有需要的小伙伴可以微信扫描下方CSDN官方认证二维码,免费领取【保证100%免费

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值