LLM大模型:推理优化-PTQ int8量化

    前面介绍的推理优化方法都有缺陷:

  • knowledge distillation:需要样本数据训练student,同时要大量算力更新student参数
  • vLLM:通过page attention,减少显存浪费,充分利用碎片化的显存,并未减少算力

  以上两种推理优化的方式都有缺陷,为了弥补上述缺陷,需要新的推理优化方式!

  transformer架构中,不论是embedding,还是FFN,核心都是矩阵乘法,所以这里选个layer看看weight的分布,如下:

         

   很明显的(-1,1)之间的正太分布,而且全是浮点数,除了0就没整数了,这就是weight的现状!面对大量的浮点数,怎么提速计算速度,并且减少显存占用了?既然存的都是浮点数,自然要想办法对浮点数进行优化了!

  • 浮点数存储:fp32需要4byte、fp16需要2byte

  • 浮点数计算:对阶、尾数运算、规格化、舍入处理、溢出判断

  浮点数存储和计算怎么看都觉得”费事“!既然浮点数这么难缠,能不能用整数替代浮点数了?还有,如果不能用整体替代浮点数做运算,也找不到其他合适的数了,现在只能硬着头皮想办法换算成整数了!

  • 整数存储:空间明显比浮点数少,普通整数只需要8bit
  • 整数计算:整数计算直接XOR异或,根本不需要像浮点数那样经历5个阶段,速度快很多

  又该怎么把浮点数映射转换成整数了?浮点数和整数完全是两种不同的数啊!先回顾一下:机器学习很重要的一个步骤就是矩阵乘法,为啥要用矩阵乘法了?用几何解释就是把向量通过矩阵乘法映射到新的空间,这个空间就是矩阵的列向量组成的!在新空间可以根据业务需求进一步做各种操作,最常见的就是分类和回归了!按照这个思路,就需要把浮点数映射到整数这边来计算了!使用整数的初衷是节约显存,所以整数的bit不能过多,否则就没意义了,不如直接使用fp16;所以整数最多只能用8bit,简称int8;int8还要表示正负数,所以整个范围就在-2^7~2^7之间,也就是-127~127之间!最核心的一步就是想办法把浮点数映射到这个范围内了,这类似的功能是不是很熟悉了?以前做传统机器学习,对于输入的特征数据,为了防止过拟合,提升泛化能力,大部分模型都要求做normalization,把输入的每个特征都压缩在某个特定的范围内(一般是-1到1之间),和这里的浮点数转到整数特定范围的应用场景刚好一样啊!

  1、量化方式

   (1)对称量化:找到一组float数据中最大的数,除以127,得到缩放比例scale,然后每个数都除以scale后四舍五入取整(这里是反量化后误差的根因),就得到整数的隐射了;反量化还原就乘以scale即可,举例如下:

        

    用对称量化做矩阵乘法:输入x和权重w都做量化相乘,如下:可以直观感受到原始结果和量化结果之间的误差

        

  (2)对称量化的精度不高,还有提升的需求。使用同样的scale思路,映射到0~255之间了?如下:反量化后误差明显比对称量化小

        

   非对称量化矩阵乘法:量化结果相比对称量化,确实精确一些

        

   2、量化的方式有了,那什么时候量化了?量化本质就是把浮点数映射成整数后做计算,所以只要涉及到浮点数运算的地方都可以量化后再计算,完成后通过反量化还原即可!从量化的环节看,分以下两种方式:

  (1)训练后动态量化:

  • 将训练好的模型权重量化为int8,并保存量化参数,
  • 在模型推理时,对每一层输入的fp32激活值,动态进行进行量化为int8:
  • 在每一层对量化后的int8权重和int8激活值进行计算。
  • 在每一层输出时将结果反量化为fp32。
  • 将fp32激活值传入到下一层。

  流程示意如下:

      

   这种方式有明显缺陷:

  • 每一次推理每一层都要对输入统计量化参数,比较耗时;
  • 每一层计算完都转化为fp32,存入显存,占用显存带宽

 (2)训练后静态量化:针对训练后动态量化的缺陷,改进的思路如下:

  • 每一次推理每一层都要对输入统计量化参数,比较耗时:用有代表性的输入数据跑一遍整个网络,通过统计得到每层大概得量化参数
  • 每一层计算完都转化为fp32,存入显存,占用显存带宽:这一层的输出是下一层的输入,下一层还是要量化,不如在这层直接量化好再传给下一层,计算量是一样的,但是传输的数据量少了很多,节约带宽

    具体操作方法:

  • 将训练好的模型权重量化为int8,并保存量化参数。
  • 校准(calibration): 选一些样本模拟推理,求出每层激活函数各自的scale、zero_point,后续每次推理时输入数据x都用这次得到的scale、zero_point,不用每次都重复计算scale、zero_point了,减少计算量
  • 在每一层对量化后的int8权重和int8激活值进行十算。
  • 在每一层输出时将结果反量化为fp32,同时根据校准产生的激活值量化参数,把激活值量化为int8,把量化参数放入量化后的激活值中;这里先反量化后再次量化,不是多此一举么?
    • 神经网络或者说深度学习有多层,每层处理的语义是不同的,所以每层都要单独量化,不能直接用上一层的量化传递给下一层
    • 每层的量化范围不同,直接把上层int8的结果输出给下一层,可能导致数值范围不匹配
    • 所以每层输出先反量化,让层与层之间统一量纲(以浮点数为准),然后再次量化后传入下一层
  • 将int8的激活值和它的量化参数传入到下一层。

  流程示意如下:

      

   3、huggingface的transformer库中也有可以直接使用量化框架:LLM.int8() 混合精度量化;在不同参数量的模型上,使用不同的量化位数,其准确率如下(原论文:https://arxiv.org/pdf/2208.07339):

       

   参数超过6.7B时,LLM.int8()的准确率和原模型惊人地保持一致,并未降低,这个量化方法到底是怎么做的了?中国人有句古话:林子大了什么样的鸟都有!这话同样适用于LLM:一旦模型参数量达到数十亿级别,参数中就会出现一些离群点,也就是值比较大的维度;这些维度的值较大,应该是模型学到的比较重要的特征维度,俗称Emergent feathers。先看一个例子:原始特征有些权重比较大,经过量化后再反量化时,会让其他部分维度清零,信息严重丢失!如果强行忽略这些维度值较大的特征,其他维度倒是保持不变了,但重要的特征信息也没了,会严重影响下游的任务准确性

  

   现在面临的情况是:值较大的维度去也不是,不去也不是,怎么办?既然放在一起量化和反量化不行,要不然分而治之?把较小值的维度和较大值的维度分开处理试一试?比如下面的矩阵:

       

   左边是weight,明显有outlier feature(原论文设置的阈值>=6),右边是input;按照以前的放在一起、胡子眉毛一把抓的量化思路,权重小的部分维度量化后可能就没了。新的处理方法如下:

  • 从输入的隐含状态中,按列提取异常值 (即大于某个阈值的值)。
  • 对 FP16 离群值矩阵和 Int8 非离群值矩阵分别作矩阵乘法。
  • 反量化非离群值的矩阵乘结果并其与离群值矩阵乘结果相加,获得最终的 FP16 结果。

   上述的weight中,第2列和第4列权重明显大,把这两列单独踢出来;input对应的2、4两行也单独踢出来,组成新矩阵,分别相乘,如下:

      

   weight小的权重才量化,weight大的权重直接浮点数相乘就行了!我个人认为weight大的不用量化:

  • weight大的特征肯定是少数,看看文章开头那张weight分布图呗!
  • weight小的数量多,量化效果明显

   注意:LLM.int8()虽然精度没降低,但因为要把矩阵拆开,分别计算,所以过程比较复杂,推理的效率和原模型比甚至是降低的!

       

  使用的方式也简单,transformer包里面已经集成好了:事先安装好BitsAndBytesConfig就行:

 quantization_config = BitsAndBytesConfig(load_in_8bit=True)
    model = AutoModelForCausalLM.from_pretrained(
        base_model,
        device_map=device,
        trust_remote_code=True,
        quantization_config=quantization_config,
        # torch_dtype=torch.float16
    )

   tips:经常做各种LLM的尝试,需要依赖各种第三方包。为了避免包或版本冲突,还是用虚拟环境吧,然后就是一路上各种缺包,各种补齐!

       

  4、注意:上述quantization只是针对model本身优化的方法, 事实上,还有其他很多优化的方法,比如用torch.profiler查看整个流程中的耗时情况,比如下面这种:gpu的利用率只有5%,这种情况下做量化是没意义的!

  

   还有这种:cpu往gpu拷贝数据时间很长,这时就要考虑让模型和数据从一开始就放在显存,而不是在内存和显存之间来回倒腾!

  

   比如这样写:

  

import torch
import torch.nn as nn
import torch.optim as optim

# 确定设备
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# 定义一个简单的模型
class SimpleModel(nn.Module):
    def __init__(self):
        super(SimpleModel, self).__init__()
        self.fc = nn.Linear(10, 5)

    def forward(self, x):
        return self.fc(x)

# 实例化模型并移到设备
model = SimpleModel().to(device)

# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)

# 创建一些假数据和目标
inputs = torch.randn(20, 10)  # 假设有20个样本,每个样本有10个特征
targets = torch.randn(20, 5)  # 假设目标有5个特征

# 将数据移到设备
inputs = inputs.to(device)
targets = targets.to(device)

# 前向传播
outputs = model(inputs)

# 计算损失
loss = criterion(outputs, targets)

# 反向传播和优化
optimizer.zero_grad()
loss.backward()
optimizer.step()

# 如果需要将结果移回 CPU,使用 .to('cpu') 或 .cpu()
outputs_cpu = outputs.cpu()

  首先确定了设备,然后将模型和数据都移动到了 GPU 上。在训练过程中,没有将数据从 GPU 移回 CPU,除非有必要(比如打印输出或进一步处理),这样可以避免不必要的数据倒腾,提高效率。

 

参考:

1、https://www.bilibili.com/video/BV1EE42157Ms/?spm_id_from=333.337.search-card.all.click&vd_source=241a5bcb1c13e6828e519dd1f78f35b2  大模型部署推理优化

2、https://www.bilibili.com/video/BV1FH4y1c73W/?spm_id_from=333.788.recommend_more_video.4&vd_source=241a5bcb1c13e6828e519dd1f78f35b2  bitsandbytes、GPTQ、GGUF、AWQ

   https://github.com/chunhuizhang/llm_inference_serving/blob/main/tutorials/quantization/qlora_gptq_gguf_awq.ipynb

   https://github.com/chunhuizhang/llm_inference_serving/blob/main/tutorials/quantization/basics.ipynb

3、https://www.cnblogs.com/yilang/p/11277201.html 浮点数计算

4、https://huggingface.co/blog/zh/hf-bitsandbytes-integration  大规模 Transformer 模型 8 比特矩阵乘简介 - 基于 Hugging Face Transformers、Accelerate 以及 bitsandbytes

posted @ 2024-08-03 21:13  第七子007  阅读(428)  评论(0编辑  收藏  举报