文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-11-29 18:59

关注

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

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

基础知识

1.数值表示

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

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

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

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

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

2.动态范围与精度

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

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

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

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

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

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

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

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

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

二、什么是模型量化?

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

三、量化分类

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

(1) 根据量化时机

(2) 根据映射函数是否为线性

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

(4) 根据量化范围

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

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

四、量化方案

1.LLM.int8()

(1) LLM.int8()量化算法

INT8量化的基本思想是将浮点数通过缩放因子scale映射到范围在[-128, 127]内的8位整数表示。

量化公式如下:

其中:

缩放因子scale的计算公式:

其中,表示浮点数最大绝对值。

反量化的过程为:

如下图所示为通过该方式实现量化-反量化的示例。假设使用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格式的正负符号,我们需要减去最小值,然后再使用最大值作为量化因子。具体实现如下:

对于给定的向量或矩阵,首先找到最小值,并减去最小值:

找到X'的绝对最大值,使用绝对最大值作为量化因子:

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

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

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

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

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

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

(2) 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内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯