【LLM国产化】量化技术在MindIE中的应用

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!

目前,由于大模型参数量显著激增,从而导致模型变得越来越大。因此,需要一些大模型压缩技术来降低模型部署的成本,并提升模型的推理性能。而模型量化作为模型压缩领域的研究热点之一,获得了广泛的应用。目前主要有的推理框架vLLM、TensorRT-LLM、LMDeploy都提供了针对大模型的量化方案。而本文将介绍量化技术在MindIE推理框架中的应用。

之前也分享过一些MindIE相关的内容。具体如下:

另外,我撰写的大模型相关的博客及配套代码 均整理放置在Github:llm-action,有需要的朋友自取。

LLM场景下常见的量化技术原理

大模型量化系列 已经针对当前主流的一些大模型量化技术进行过讲解。

而来自MIT HAN Lab 的 SmoothQuant 和 AWQ 无疑是其中的佼佼者,同时, AWQ 还获得了 MLSys 2024年最佳论文奖。此外,像GPTQ也获得了广泛关注。目前,三者广泛应用于TensorRT-LLM、vLLM、LMDeploy等LLM推理框架中,MindIE 框架中一些量化方法也是基于其思想进行实现。下面对这三篇论文的核心思想进行简要概述。

SmoothQuant

SmoothQuant (论文:SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models)是一种同时确保准确率且推理高效的训练后量化 (PTQ) 方法,可实现 8 比特权重、8 比特激活 (W8A8) 量化。

针对大模型而言,权重很容易量化,对于激活值,由于异常值的存在,导致激活则较难量化。作者对比了 per-channel、per-token、per-tensor 激活量化方案。在这几种不同的激活量化方案中。per-tensor量化是最高效的实现方式。但只有逐通道量化(per-channel)保留了精度,但它与 INT8 GEMM Kernel不兼容。即per-channel量化不能很好地映射到硬件加速的GEMM内核(硬件不能高效执行,从而增加了计算时间)。

为了进行 vector-wise quantization 以有效利用 INT8 GEMM 内核,我们只能使用外部维度(即激活的维度 T 和 权重的维度 <math xmlns="http://www.w3.org/1998/Math/MathML"> C 0 C_0 </math>C0)的缩放因子,不能使用内部维度(即通道内维度 <math xmlns="http://www.w3.org/1998/Math/MathML"> C i C_i </math>Ci)。因此,先前的工作对激活都采用了per-token量化,但并不能降低激活的难度。

SmoothQuant 引入平滑因子s来平滑激活异常值,通过数学上等效的变换将量化难度从激活转移到权重上。

于是 SmoothQuant 提出了一种数学上等价的逐通道缩放变换(per-channel scaling transformation),引入平滑因子s来平滑激活异常值,通过数学上等效的变换将量化难度从激活转移到权重上,从而使模型易于量化,保持精度的同时,还能够保证推理提升推理速度。

根据量化方式不同,作者提出三种策略 O1、O2、O3,其计算延迟依次降低。SmoothQuant的O1和O2级成功地保持了浮点精度,而O3级(per-tensor static)使平均精度下降了0.8%,可能是因为静态收集的统计数据与真实评估样本的激活统计数据之间的差异。

更加详细的介绍可参考之前的文章:大模型量化技术原理:SmoothQuant

AWQ

AWQ(AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration)方法源于"权重对于LLM的性能并不同等重要"的观察,存在约(0.1%-1%)显著权重对大模型性能影响太大,通过跳过这1%的重要权重(salient weight)不进行量化,可以大大减少量化误差。

具体如下图所示,左边的(a)使用RTN量化误差较大,中间的(b)通过保留1%的重要权重不量化,量化误差明显降低。但是该方法采用混合精度计算并不是硬件高效的方式。右边则是使用AWQ进行激活感知的仅权重(即W4A16)量化方法,通过逐通道缩放以保护显著权重来减少量化误差,这种方法不存在硬件效率低下的问题。

尽管我们只做了权重量化,但要找到显著的权重通道,我们应该根据激活分布而不是权重分布,与较大激活幅度(activation magnitudes)相对应的权重通道更加突出,因为它们处理了更重要的特征。

对于原始的RTN量化, <math xmlns="http://www.w3.org/1998/Math/MathML"> y = Q ( w ) x = Δ ⋅ Round ( w Δ ) x y=Q(\mathbf{w})\mathbf{x} = \Delta \cdot \text{Round}(\frac{\mathbf{w}}{\Delta}) \mathbf{x} </math>y=Q(w)x=Δ⋅Round(Δw)x。

作者通过对显著权重引入一个s,进行公式的等价变化。即将 w 与 s (s>1)相乘, 然后,再用 x 除以 s。具体如下:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> Q ( w ⋅ s ) ⋅ x s = Δ ′ ⋅ Round ( w s Δ ′ ) ⋅ x ⋅ 1 s Q(w\cdot s)\cdot \frac{x}{s} = \Delta^{'} \cdot \text{Round}(\frac{ws}{\Delta^{'}}) \cdot x \cdot \frac{1}{s} </math>Q(w⋅s)⋅sx=Δ′⋅Round(Δ′ws)⋅x⋅s1

最终发现与原始的量化误差比值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ ′ Δ ⋅ 1 s \frac{\Delta^{'}}{\Delta} \cdot \frac{1}{s} </math>ΔΔ′⋅s1。给定 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ ′ ≈ Δ \Delta^{'}\approx\Delta </math>Δ′≈Δ 和 <math xmlns="http://www.w3.org/1998/Math/MathML"> s > 1 s>1 </math>s>1, 显著权重 w 的相对误差较小。

为了同时考虑显著权重和非显著权重,作者选择自动搜索每个输入通道最佳缩放因子,使某一层量化后的输出差最小。从形式上看,希望优化以下目标:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> s ∗ = a r g s m i n L ( s ) , L ( s ) = ∥ Q ( W ⋅ d i a g ( s ) ) ( d i a g ( s ) − 1 ⋅ X ) − W X ∥ \mathbf{s}^* = arg_{\mathbf{s}}min \mathcal{L}(\mathbf{s}), \quad \mathcal{L}(\mathbf{s})=\lVert Q(\mathbf{W}\cdot diag(\mathbf{s})) (diag(\mathbf{s})^{-1} \cdot \mathbf{X}) - \mathbf{W}\mathbf{X} \lVert </math>s∗=argsminL(s),L(s)=∥Q(W⋅diag(s))(diag(s)−1⋅X)−WX∥

由于量化函数不可微,我们无法直接用梯度反向传播来优化问题。有一些技术依赖于近似梯度,但我们发现它仍然存在收敛不稳定的问题。

为了使这一过程更加稳定,我们通过分析影响缩放因子选择的因数,为最佳缩放比例定义了一个搜索空间。
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> s = s X α , α ∗ = arg ⁡ α m i n L ( s X α ) \mathbf{s}=\mathbf{s_X}^{\alpha}, \quad \alpha^*=\arg_{\alpha} min\mathcal{L}(\mathbf{s_X}^{\alpha}) </math>s=sXα,α∗=argαminL(sXα)

其中,s仅与激活 <math xmlns="http://www.w3.org/1998/Math/MathML"> s X s_X </math>sX的大小有关, <math xmlns="http://www.w3.org/1998/Math/MathML"> s X s_X </math>sX是激活的平均幅值(每个通道),这里仅使用单个超参数α来平衡显著通道和非显著通道的保护。我们可以通过在 [0, 1] 区间内进行快速网格搜索(grid search)来找到最佳的 α(0 表示我们不进行缩放;1 对应于最激进的缩放)。

此外,作者还通过应用权重剪裁来进一步最小化 MSE 误差,因为剪裁权重可以进一步帮助减少 <math xmlns="http://www.w3.org/1998/Math/MathML"> Q ( w ⋅ s ) ⋅ x s = Δ ′ ⋅ Round ( w s Δ ′ ) ⋅ x ⋅ 1 s Q(w\cdot s)\cdot \frac{x}{s} = \Delta^{'} \cdot \text{Round}(\frac{ws}{\Delta^{'}}) \cdot x \cdot \frac{1}{s} </math>Q(w⋅s)⋅sx=Δ′⋅Round(Δ′ws)⋅x⋅s1 中的 <math xmlns="http://www.w3.org/1998/Math/MathML"> Δ ′ \Delta^{'} </math>Δ′;从而减少量化误差。

更加详细的介绍可参考之前的文章:大模型量化技术原理:AWQ、AutoAWQ

GPTQ

GPTQ(论文:GPTQ: ACCURATE POST-TRAINING QUANTIZATION FOR GENERATIVE PRE-TRAINED TRANSFORMERS) 是一种仅权重量化方法,其中模型权重被量化为 int4 数值类型,而激活值则保留在 float16。在推理阶段,模型权重被动态地反量化回 float16 并在该数值类型下进行实际的运算;同 OBQ 一样,GPTQ还是从单层量化的角度考虑,希望找到一个量化过的权重,使的新的权重和老的权重之间输出的结果差别最小。

GPTQ 将权重分组(如:128列为一组)为多个子矩阵(block)。对某个 block 内的所有参数逐个量化,每个参数量化后,需要适当调整这个 block 内其他未量化的参数,以弥补量化造成的精度损失。因此,GPTQ 量化需要准备校准数据集。

GPTQ 量化过程如下图所示。首先,使用 Cholesky 分解求解 Hessian 矩阵的逆,然后在给定的步骤中对连续列的块(粗体)进行量化,并在该步骤结束时更新剩余的权重(蓝色)。量化过程在每个块内递归应用,白色中间列表示当前正在被量化。

GPTQ 的创新点如下:

  • 取消贪心算法:OBS 采用贪心策略,先量化对目标影响最小的参数;但 GPTQ 发现直接按顺序做参数量化,对精度影响也不大。这项改进使得参数矩阵每一行的量化可以做并行的矩阵计算(这意味着我们可以独立地对每一行执行量化。即所谓的 per-channel quantization)。对于大模型场景,这项改进使得量化速度快了一个数量级;
  • Lazy Batch-Updates:OBQ 对权重一个个进行单独更新,作者发现性能瓶颈实际在于GPU的内存带宽,而且同一个特征矩阵W不同列间的权重更新是不会互相影响的。因此作者提出了延迟批处理的方法,通过延迟一部分参数的更新,一次处理多个(如:128)列,来缓解带宽的压力,大幅提升了计算速度。
  • Cholesky(乔莱斯基) 分解:用 Cholesky 分解(一种分解矩阵的方法)求海森矩阵的逆,提前计算好所有需要的信息,在增强数值稳定性的同时,后续更新的过程中再计算,进一步减少了计算量。

GPTQ的伪代码如下所示。

更加详细的介绍可参考之前的文章:大模型量化技术原理:GPTQ、LLM.int8()

MindStudio ModelSlim 量化简介

MindIE 推理框架的量化依赖于昇腾模型压缩工具 MindStudio ModelSlim。它是一个以加速为目标、压缩为技术、昇腾为根本的亲和压缩工具。支持训练加速和推理加速,包括模型低秩分解、稀疏训练、训练后量化、量化感知训练等功能,昇腾AI模型开发用户可以灵活调用Python API接口,对模型进行性能调优,并支持导出不同格式模型,在昇腾AI处理器上运行。

MindStudio ModelSlim 支持的产品

当前的模型压缩支持在以下硬件产品中使用。

  • Atlas 推理系列产品(Ascend 310P处理器)。
  • Atlas 训练系列产品。
  • Atlas A2训练系列产品/Atlas 800I A2推理产品。

MindStudio ModelSlim 大模型量化工作流程

关键步骤说明如下:

  1. 用户准备原始模型和校准数据。
  2. 可选:使用离群值抑制功能对LLM模型进行离群值抑制,可参考精度保持策略选择是否启用。
  • 使用AntiOutlierConfig生成离群值抑制配置。
  • 调用AntiOutlier接口,将模型、校准数据等传入,生成抑制器。
  • 调用抑制器的process()方法对原始模型进行离群值抑制。
  1. 使用QuantConfig生成量化配置。
  2. 根据原始模型、量化配置和校准数据,调用Calibrator接口构建量化校准对象。
  3. 调用生成的量化校准对象的run()方法对原始模型进行量化。
  4. 通过调用生成的量化校准对象的save()接口保存量化后的模型,包括模型量化权重和模型相关参数,用于后续量化模型的部署任务。

MindStudio ModelSlim 中的量化类型

昇腾模型压缩工具(MindStudio ModelSlim)支持的量化类型如下:

  • W8A16: Linear权重int8量化,激活值不量化
  • W8A8: Linear权重int8量化,激活值int8量化
  • W8A8S: Linear权重int8稀疏量化,激活值int8量化
  • W4A16: Linear权重int4量化,激活值不量化
  • KV8: kvcache量化,将kvcache量化为8bit ...
python 复制代码
# https://gitee.com/ascend/msit/blob/master/msmodelslim/msmodelslim/pytorch/llm_ptq/llm_ptq_tools/llm_ptq_utils.py
class QuantType(str, Enum):
    UNKNOWN = "UNKNOWN"  # 未被识别的类型
    W8A16 = "W8A16"  # W8A16量化,Matmul的weight为8bit,activation为16bit
    W4A16 = "W4A16"  # W4A16量化,Matmul的weight为4bit,activation为16bit
    W8A8 = "W8A8"  # W8A8量化,Matmul的weight、activation均为8bit
    W8A8S = "W8A8S"  # 稀疏量化,Matmul的weight、activation均为8bit,且weight经过稀疏(权重数值分布范围可能小于8bit)
    W8A8SC = "W8A8SC"  # 稀疏量化压缩后的权重
    FLOAT = "FLOAT"  # 浮点
    KV8 = 'C8' # kvcache量化,kvcache为8bit
    W8A8_DYNAMIC = "W8A8_DYNAMIC"  # W8A8静态量化与per-token动态量化混合量化

msmodelslim 工具生成的量化权重均为signed场景,即int8数据分布范围为-128到127。如果开源的权重为unsigned场景,对于int8可以考虑将weight和offset权重减去128。

MindStudio ModelSlim 量化权重格式

昇腾模型压缩工具(MindStudio ModelSlim)针对LLM训练后量化生成的safetensors量化权重文件包含两个文件:权重文件(quant_model_weight.safetensors)和权重描述文件(quant_model_description.json)。

权重描述文件,保存为json格式。内部储存格式为python的字典 dict,字典的key值为权重名称,value为权重对应的量化类型。

  • "model_quant_type":描述整体的量化类型。
  • "kv_cache_type":表示kv_cache是否量化
  • 其余为各个权重的类型,"FLOAT"表示来自于原始浮点权重,"W8A8"表示来自W8A8量化,"W8A16"表示来自W8A16量化,"W8A8S"表示来自稀疏量化。

下面为 ChatGLM2-6B W8A16量化权重的描述文件,描述文件字典内容排序不影响实际使用。

json 复制代码
{
    "model_quant_type": "W8A16",  
    "kv_cache_type": "C8", # 使用kv cache量化后会生成该行  
    "transformer.embedding.word_embeddings.weight": "FLOAT",  
    "transformer.rotary_pos_emb.inv_freq": "FLOAT",
    "transformer.encoder.layers.0.input_layernorm.weight": "W8A16",  
    "transformer.encoder.layers.0.self_attention.query_key_value.weight": "W8A16",  
    # 使用kv cache量化后生成如下4行  
    "transformer.encoder.layers.0.self_attention.query_key_value.k_proj.kv_cache_scale": "W8A16",  
    "transformer.encoder.layers.0.self_attention.query_key_value.k_proj.kv_cache_offset": "W8A16",
    "transformer.encoder.layers.0.self_attention.query_key_value.v_proj.kv_cache_scale": "W8A16",
    "transformer.encoder.layers.0.self_attention.query_key_value.v_proj.kv_cache_offset": "W8A16",
    "transformer.encoder.layers.0.self_attention.query_key_value.weight_scale": "W8A16",  
    "transformer.encoder.layers.0.self_attention.query_key_value.weight_offset": "W8A16",
    "transformer.encoder.layers.0.post_attention_layernorm.weight": "FLOAT", 
    "transformer.encoder.layers.0.mlp.dense_4h_to_h.weight": "W8A16",  
    "transformer.encoder.layers.0.mlp.dense_4h_to_h.weight_scale": "W8A16",  
    "transformer.encoder.layers.0.mlp.dense_4h_to_h.weight_offset": "W8A16",  
    "transformer.encoder.layers.0.mlp.dense_4h_to_h.weight": "W8A16",  
    "transformer.encoder.layers.0.mlp.dense_4h_to_h.weight_scale": "W8A16",  
    "transformer.encoder.layers.0.mlp.dense_4h_to_h.weight_offset": "W8A16", 
    ...
    剩下的层以此类推
    ...
    "transformer.encoder.final_layernorm.weight": "FLOAT",
    "transformer.output_layer.weight": "FLOAT"
}

权重文件保存为safetensors格式,内部格式为python的字典 dict,包含量化权重和量化不修改的浮点权重,字典的key值为权重名称,value为具体权重的数值。以ChatGLM2-6B为例:

  • 'transformer.embedding.word_embeddings.weight'为浮点模型中word_embedding层的权重,名称和权重均未修改,对应描述文件量化类型为'FLOAT';
  • 'transformer.encoder.layers.0.self_attention.dense.weight'为原始模型第0层layer的dense层linear的权重,经过量化修改,数据类型为int8,对应描述文件量化类型为'W8A16';
  • 'transformer.encoder.layers.0.self_attention.dense.weight_scale'为原始模型第0层layer的dense层linear量化后新增的量化参数weight_scale,对应描述文件量化类型为'W8A16'

ChatGLM2-6B W8A16量化权重如下所示:

json 复制代码
{
    'transformer.embedding.word_embeddings.weight': tensor([...]),
    'transformer.encoder.final_layernorm.weight': tensor([...]),
    'transformer.encoder.layers.0.input_layernorm.weight': tensor([...]),
    'transformer.encoder.layers.0.mlp.dense_4h_to_h.weight': tensor([...]),
    'transformer.encoder.layers.0.mlp.dense_4h_to_h.weight_scale': tensor([...]),
    'transformer.encoder.layers.0.mlp.dense_4h_to_h.weight_offset': tensor([...]),
    'transformer.encoder.layers.0.mlp.dense_h_to_4h.weight': tensor([...]),
    'transformer.encoder.layers.0.mlp.dense_h_to_4h.weight_scale': tensor([...]),
    'transformer.encoder.layers.0.mlp.dense_h_to_4h.weight_offset': tensor([...]),
    'transformer.encoder.layers.0.post_attention_layernorm.weight': tensor([...]),
    'transformer.encoder.layers.0.self_attention.dense.weight': tensor([...]),
    'transformer.encoder.layers.0.self_attention.dense.weight_scale': tensor([...]),
    'transformer.encoder.layers.0.self_attention.dense.weight_offset': tensor([...]),
    'transformer.encoder.layers.0.self_attention.query_key_value.weight': tensor([...]),
    'transformer.encoder.layers.0.self_attention.query_key_value.weight_scale': tensor([...]),
    'transformer.encoder.layers.0.self_attention.query_key_value.weight_offset': tensor([...]),
    ...
    剩下几层以此类推
    ...
    'transformer.output_layer.weight': tensor([...]),
    'transformer.rotary_pos_emb.inv_freq': tensor([...])
}

MindStudio ModelSlim 量化类型具体实现说明

W8A16量化

对于W8A16量化(仅权重量化),量化工具对于每个量化的 Linear 生成3个参数,参数名称为:weight、weight_scale、weight_offset,在safetensors权重文件中,完整的权重名称为Lienar层的名称+参数名称。

例如,ChatGLM2-6B量化权重中的"transformer.encoder.layers.0.self_attention.query_key_value.weight_scale",其中,"transformer.encoder.layers.0.self_attention.query_key_value"为Linear层的名称,"weight_scale"为参数的名称。

权重说明:

  • weight 为量化后的int8的权重,数据类型为torch.Tensor,dtype为torch.int8,shape和原始浮点的shape一致,记为n, k = weight.shape,k为hidden_size
  • weight_scale为量化的缩放系数,数据类型为torch.Tensor,dtype为torch.float32,在per_channel场景下,shape为[n],在per_group场景下,shape为[n, k / group_size]
  • weight_offset为量化的偏移系数,数据类型为torch.Tensor,dtype和shape与weight_scale一致。对称量化场景下,需要构造全0的weight_offset。

反量化计算公式如下,对于per_channel 场景:

ini 复制代码
deq_weight = (weight - weight_offset) * weight_scale  

对于 per_group 场景:

ini 复制代码
weight = weight.reshape((-1, group_size))  
weight_offset = weight_offset.reshape((n * k / group_size, 1))  
weight_scale = weight_scale.reshape((n * k / group_size, 1))  
deq_weight = ((weight - weight_offset) * weight_scale).reshape((n, k))

注意:npu量化算子计算时实际的逻辑为(weight + weight_offset) * weight_scale,昇腾推理框架在加载量化权重时进行了取负操作。

W8A8, W8A8S 量化

对于 W8A8, W8A8S 量化,量化工具对于每个量化的 Linear 生成5个参数,参数名称为:weight, input_scale, input_offset, deq_scale, quant_bias。在safetensors权重文件中,完整的权重名称为Linear层的名称+参数名称,与W8A16类似。

权重说明:

  • weight 为量化后的int8的权重,数据类型为torch.Tensor,dtype为torch.int8,shape和原始浮点的shape一致,记为n, k = weight.shape,k为hidden_size。
  • input_scale 为激活值量化的缩放系数,数据类型为torch.Tensor,dtype为torch.float16或torch.bfloat16,shape为[1]
  • input_offset 为激活值量化的偏移系数,数据类型为torch.Tensor,dtype和shape与input_scale一致。
  • deq_scale 为反量化缩放系数,数据类型为torch.Tensor,dtype为torch.int64或torch.float32,shape为[n]注意:为了亲和昇腾量化算子,开源量化若基于fp16,则deq_scale的数据在传给量化算子前需要进行数据类型转换。若开源量化权重为bf16,则不需要数据类型转换。
  • quant_bias 为反量化的偏移系数,数据类型为torch.Tensor,dtype为torch.int32,shape为[n]

量化、反量化计算公式:

ini 复制代码
input_quant = input_fp / input_scale + input_offset  
output_quant = input_quant * weight + quant_bias  
output_dequant = output_quant * deq_scale  

smooth quant 量化

MindStudio ModelSlim 量化工具集成的smooth quant算法针对norm层后的Linear层进行smooth平滑操作,而不是所有Linear层。采取这种量化方案的优势在于可以将原本乘在激活值上的scale等价转移到原始浮点模型norm层的权重norm.weight上,从而避免额外引入算子带来的性能开销。

量化工具使用smooth quant后,对于每个norm层,生成2个参数,module.weight和module.bias。完整的权重名称为norm层的名称+参数名称,例如:ChatGLM2-6B量化权重中的"transformer.encoder.layers.0.input_layernorm.module.weight",其中,"transformer.encoder.layers.0.input_layernorm"为norm层的名称,"module.weight"为量化参数名称。

  • module.weight为scale后的norm.weight,数据类型、dtype、shape和norm.weight一致。
  • module.bias为引入module.weight后带来的偏移系数,数据类型、dtype、shape和norm.weight一致。

为了适配几种特殊的回退情况,msmodelslim生成的smooth quant权重中还包含原始浮点权重norm层的权重(norm.weight)。如果开源量化权重不涉及回退场景,设置为None即可。

KV Cache 量化

MindStudio ModelSlim 量化工具提供的KV Cache量化采用int8量化。对于每个attention层,生成4个参数,k_proj.kv_cache_scale, k_proj.kv_offset, v_proj.kv_cache_scale, v_proj.kv_cache_offset。

对于qkv合并或kv合并的场景,完整的四个参数的名称为合并的Linear名称+参数名;

对于qkv分离场景,k_proj的scale、offset完整的参数名称为k对应Linear名称+参数名称,v_proj的scale、offset完整的参数名称为v对应Linear名称+参数名称。

例如,对于 "transformer.encoder.layers.0.query_key_value.k_proj.kv_cache_scale"来说,"transformer.encoder.layers.0.query_key_value"为qkv合并的Linear层名称,"k_proj.kv_cache_scale"为参数名称。

权重说明:

  • kv_cache_scale:为kvcache量化scale的缩放系数,数据类型为torch.Tensor,dtype为torch.float32或torch.float16,shape为kv channel的size,如果是qkv分开场景,则为k或v层linear的n维。
  • kv_scale_offset:为kvcache量化scale的偏移系数,数据类型、dtype、shape和scale一致。

计算公式:

ini 复制代码
# 量化
cache_int = cache_fp / cache_scale + cache_offset

#反量化
cache_deq = (cache_int - cache_offset) * cache_scale

量化在 MindIE Service 中的实践

目前,在 MindIE 中已经支持多个大模型进行量化。下面以baichuan2和qwen1.5为例进行演示。不熟悉 MindIE Service 的朋友可以先看之前的文章:大模型国产化适配8-基于昇腾MindIE推理工具部署Qwen-72B实战(推理引擎、推理服务化)

baichuan2

baichuan 支持的特性矩阵如下所示:

模型及参数量 800I A2 Tensor Parallelism 300I DUO Tensor Parallelism FP16 BF16 Flash Attention Paged Attention W8A8量化 W8A16量化 W4A16量化 KV cache量化 稀疏量化 MOE量化 MindIE Service
Baichuan2-7B 支持world size 1,2,4,8 支持world size 2 × × × × ×
Baichuan2-13B 支持world size 2,4,8 支持world size 2,4 × × × ×
Baichuan-7B 支持world size 1,2,4,8 支持world size 2 × × × × × × ×
Baichuan-13B 支持world size 2,4,8 支持world size 2,4 × × × × × × ×

以下为针对baichuan2使用w8a8进行量化的示例代码:

ini 复制代码
import logging
from transformers import AutoTokenizer, AutoModelForCausalLM
from msmodelslim.pytorch.llm_ptq.anti_outlier import AntiOutlierConfig, AntiOutlier
from msmodelslim.pytorch.llm_ptq.llm_ptq_tools import Calibrator, QuantConfig
import json
import argparse

def init_tokenizer(input_model_path:str):
    tokenizer = AutoTokenizer.from_pretrained(
        pretrained_model_name_or_path=input_model_path,
        # use_fast=False,
        # padding_side='left',
        trust_remote_code=True)
    return  tokenizer

def init_model(input_model_path:str):
    model = AutoModelForCausalLM.from_pretrained(
        pretrained_model_name_or_path=input_model_path,
        trust_remote_code=True).float().cpu()
    model = model.half().npu() # 如果需要使用npu进行量化
    return model

# 获取校准数据函数定义
def get_calib_dataset(
        auto_tokenizer,
        calib_list,
        device="cpu"):  # 如果需要使用npu进行量化, device="npu:0"。使用cpu,device="cpu"
    calib_dataset = []
    for calib_data in calib_list:
        inputs = auto_tokenizer(calib_data, return_tensors='pt')
        calib_dataset.append([
            inputs.data['input_ids'].to(device),
            inputs.data['attention_mask'].to(device)
        ])
    return calib_dataset

def load_dataset(calib_set_path = "./calib_set.json"):
    calib_set = json.load(open(calib_set_path, "r"))
    return calib_set

def parse_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument('--input_model_path', type=str, help="input model and tokenizer path")
    parser.add_argument('--output_model_path', type=str, help="output model and tokenizer path")
    parser.add_argument('--calib_set_path', type=str, default="./calib_set.json", help="calib set path")
    
    return parser.parse_args()

def disable_quant_module(input_model_path:str):
    config = json.load(open(input_model_path+"/config.json", "r"))
    num_hidden_layers = config.get("num_hidden_layers", 0)
    disable_names = []

    disable_idx_lst = list(range(num_hidden_layers))
    for layer_index in disable_idx_lst:
        down_proj_name = "model.layers.{}.mlp.down_proj".format(layer_index)
        disable_names.append(down_proj_name)

    # 仅7B禁用
    # BAICHUAN_LAYERS = 32
    if int(num_hidden_layers) == 32:
        disable_last_linear = False
    else:
        # 13B
        disable_last_linear = True

    print("disable_last_linear: ", disable_last_linear, "disable_names: \n", disable_names)
    return disable_names, disable_last_linear

if __name__ == '__main__':
    args = parse_arguments()
    
    tokenizer = init_tokenizer(input_model_path=args.input_model_path)
    model = init_model(input_model_path=args.input_model_path)
    calib_set = load_dataset(calib_set_path=args.calib_set_path)
    
    dataset_calib = get_calib_dataset(tokenizer, calib_set, device="npu:0")

    # 对于linear算子中的激活值如果有表示范围过大,或者"尖刺"的异常值过多,
    # 需要使用anti outleir功能,使用方法如下

    logging.info("===============start AntiOutlier==============")
    
    anti_config = AntiOutlierConfig(
        w_bit=8, 
        a_bit=8, 
        anti_method="m2",
        #dev_type="cpu",
        dev_type="npu", dev_id=0
        )  # dev_type="npu", dev_id=0  如果需要使用npu进行量化。

    anti_outlier = AntiOutlier(model,
                            calib_data=dataset_calib,
                            cfg=anti_config,
                            norm_class_name="RMSNorm")
    anti_outlier.process()

    #下面是回退层的设置,因为w8a8的对激活值也进行了量化,会有部分网络层对激活值的表示
    #范围较为敏感所以需要回退这些网络层使用浮点权重进行计算。

    logging.info("===============end AntiOutlier==============")

    disable_names, disable_last_linear = disable_quant_module(args.input_model_path)

    quant_config = QuantConfig(
        a_bit=8,
        w_bit=8,
        disable_names=disable_names,
        disable_last_linear=disable_last_linear,
        #dev_type='cpu',  # dev_type="npu", dev_id=0,  如果需要使用npu进行量化
        dev_type="npu", dev_id=0,
        act_method=3,
        pr=1.0,
        w_sym=True,
        mm_tensor=False)

    logging.info("===============start Calibrator==============")
    calibrator = Calibrator(model,
                            quant_config,
                            calib_data=dataset_calib,
                            disable_level='L0')
    calibrator.run()  # 执行PTQ量化校准

    calibrator.save(args.output_model_path, 
        save_type=[ "safe_tensor"]
    )  # "safe_tensor"对应safetensors格式权重,"numpy"对应npy格式权重
    logging.info("===============end Calibrator==============")

量化完成之后,从原始模型权重目录拷贝分词器和模型元数据配置文件到量化权重目录。

diff 复制代码
-rw-r--r--  1 root root        779 Sep  8 13:37 config.json
-rw-r--r--  1 root root       2452 Sep  8 13:34 configuration_baichuan.py
-rw-r--r--  1 root root        215 Sep  8 13:34 configuration.json
-rw-r--r--  1 root root        285 Sep  8 13:34 generation_config.json
-rw-r--r--  1 root root       2966 Sep  8 13:34 generation_utils.py
-rw-r--r--  1 root root      33239 Sep  8 13:34 modeling_baichuan.py
-rw-r--r--  1 root root       3358 Sep  8 13:34 ms_wrapper.py
-rw-r--r--  1 root root       9070 Sep  8 13:34 quantizer.py
-rw-------  1 root root      48193 Sep  8 13:34 quant_model_description_w8a8.json
-r--------  1 root root 9995222648 Sep  8 13:34 quant_model_weight_w8a8.safetensors
-rw-r--r--  1 root root        548 Sep  8 13:34 special_tokens_map.json
-rw-r--r--  1 root root       9639 Sep  8 13:34 tokenization_baichuan.py
-rw-r--r--  1 root root        795 Sep  8 13:34 tokenizer_config.json
-rw-r--r--  1 root root    2001107 Sep  8 13:34 tokenizer.model

然后,在config.json中新增quantize为w8a8的配置:

json 复制代码
{
  "model_type": "qwen2",
  ...
  "torch_dtype": "float16",
  "quantize": "w8a8"
}

最后,启动mindie service即可。

qwen1.5

qwen 支持的特性矩阵如下所示:

模型及参数量 800I A2 Tensor Parallelism 300I DUO Tensor Parallelism FP16 BF16 Flash Attention Paged Attention W8A8量化 W8A16量化 W4A16量化 KV cache量化 稀疏量化 MOE量化 MindIE Service
Qwen-7B 支持world size 1,2,4,8 支持world size 1 × × × × × ×
Qwen-14B 支持world size 2,4,8 支持world size 1,2 × × × × × ×
QWen-72B 支持world size 8 × × × × × × ×
Qwen1.5-0.5B 支持world size 1,2,4,8 支持world size 1 × × × × × × ×
Qwen1.5-1.8B 支持world size 1,2,4,8 支持world size 1 × × × × × × ×
Qwen1.5-4B 支持world size 1,2,4 支持world size 1 × × × × × × ×
Qwen1.5-7B 支持world size 1,2,4,8 支持world size 1 × × × × × × ×
Qwen1.5-14B 支持world size 2,4,8 支持world size 1,2 × × × ×
Qwen1.5-32B 支持world size 4,8 支持world size 1,2 × × × × × ×
Qwen1.5-72B 支持world size 8 × × × × × ×
Qwen1.5-MoE-A2.7B 支持world size 4 × × × × × × ×

以下为针对Qwen1.5-72B使用w8a16进行量化的示例代码:

ini 复制代码
import sys
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from msmodelslim.pytorch.llm_ptq.llm_ptq_tools import Calibrator, QuantConfig
import argparse
import json

def load_tokenizer_and_model(fp16_path):
    tokenizer = AutoTokenizer.from_pretrained(
        pretrained_model_name_or_path=fp16_path,
        pad_token='<|extra_0|>',
        eos_token='<|endoftext|>',
        padding_side='left',
        trust_remote_code=True
    )
    model = AutoModelForCausalLM.from_pretrained(
        pretrained_model_name_or_path=fp16_path,
        torch_dtype=torch.float32, trust_remote_code=True
    ).cpu()
    return tokenizer, model


def main(fp16_path, quant_save_path, calib_set_path):
    tokenizer, model = load_tokenizer_and_model(fp16_path)

    disable_names = ['lm_head']

    quant_config = QuantConfig(
        w_bit=8,                        # 权重量化位数
        a_bit=16,                       # 激活值量化位数
        disable_names=disable_names,    # 不做量化的层
        dev_type='cpu',
        pr=1.0,                         # 量化正则百分比
        w_sym=True,                     # 对称/非对称量化,True为对称量化,False为非对称量化
        mm_tensor=False                 # 权重量化粒度,True为per-tensor量化,False为per-channel量化(大模型场景建议False)
    )

    calibrator = Calibrator(
        model,
        quant_config,
        calib_data=None,    # W8A16量化无需校准
        disable_level='L0'  # 自动回退等级,根据精度损失程度增加不量化的层(L0~L5,L0为不回退,精度损失明显时可适当提升等级)
    )

    calibrator.run()  # 执行PTQ量化校准

    calibrator.save(quant_save_path, save_type=["safe_tensor"])

def parse_arguments():
    parser = argparse.ArgumentParser()
    parser.add_argument('--input_model_path', type=str, help="input model and tokenizer path")
    parser.add_argument('--output_model_path', type=str, help="output model and tokenizer path")
    parser.add_argument('--calib_set_path', type=str, default="./calib_set_72b.json", help="calib set path")
    
    return parser.parse_args()

if __name__ == "__main__":
    args = parse_arguments()
    model_path = args.input_model_path
    quant_weight_save_path = args.output_model_path
    calib_set_path = args.calib_set_path
    main(model_path, quant_weight_save_path, calib_set_path)

量化完成之后,从原始模型权重目录拷贝分词器和模型元数据配置文件到量化权重目录。

diff 复制代码
-rw-r--r--  1 root root         681 Sep  7 14:49 config.json
-rw-------  1 root root      116914 Sep  7 04:17 quant_model_description_w8a16.json
-r--------  1 root root 79829941952 Sep  7 04:17 quant_model_weight_w8a16.safetensors
-rwxr-xr-x  1 root root        1402 Sep  7 04:17 tokenizer_config.json
-rwxr-xr-x  1 root root     7028015 Sep  7 04:17 tokenizer.json

然后,在config.json中新增quantize为w8a16的配置:

json 复制代码
{
  "model_type": "qwen2",
  ...
  "torch_dtype": "float16",
  "quantize": "w8a16"
}

最后,启动mindie service即可。

MindIE Service 量化后效果测评及推理性能测试

对 baichuan2 和 qwen1.5 进行量化和使用 MindIE Service 进行服务化之后。在 C-Eval 和 CMMLU 进行效果测评,具体如下表所示。

C-Eval

全面的中文基础模型评估套件,涵盖了52个不同学科的13948个多项选择题,分为四个难度级别。

CMMLU

一个综合性的中文评估基准,专门用于评估语言模型在中文语境下的知识和推理能力。CMMLU涵盖了从基础学科到高级专业水平的67个主题。它包括:需要计算和推理的自然科学,需要知识的人文科学和社会科学,以及需要生活常识的中国驾驶规则等。此外,CMMLU中的许多任务具有中国特定的答案,可能在其他地区或语言中并不普遍适用。因此是一个完全中国化的中文测试基准。

对 baichuan2 和 qwen1.5 进行量化和使用 MindIE Service 进行服务化之后。对其进行推理性能测试,具体如下表所示:

总结

本文介绍了目前被大模型推理框架集成最多的一些量化方法。同时介绍了昇腾模型压缩工具(MindStudio ModelSlim)的特性和量化方法,之后,讲述了MindStudio ModelSlim量化后的模型如何在MindIE Service中应用,最后,针对量化后的模型进行了效果测评和推理性能测试。

码字不易,如果觉得我的文章能够能够给您带来帮助,期待您的点赞收藏加关注~~

参考文档:

相关推荐
数据智能老司机17 小时前
LLM工程师手册——监督微调
深度学习·架构·llm
AI_小站20 小时前
LLM——10个大型语言模型(LLM)常见面试题以及答案解析
人工智能·程序人生·语言模型·自然语言处理·大模型·llm·大模型面试
waiting不是违停1 天前
LangChain Ollama实战文献检索助手(二)少样本提示FewShotPromptTemplate示例选择器
langchain·llm·ollama
我爱学Python!1 天前
AI Prompt如何帮你提升论文中的逻辑推理部分?
人工智能·程序人生·自然语言处理·chatgpt·llm·prompt·提示词
AI_小站2 天前
多模态大模型微调实践!PAI+LLaMA Factory搭建AI导游
人工智能·程序人生·语言模型·大模型·llm·产品经理·多模态大模型
AI_小站2 天前
【AI工作流】FastGPT - 深入解析FastGPT工作流编排:从基础到高级应用的全面指南
人工智能·程序人生·语言模型·大模型·llm·fastgpt·大模型应用
蚝油菜花3 天前
MeetingMind:AI 会议助手,支持自动转录音频并提取会议中的关键信息
人工智能·开源·llm
Agile.Zhou3 天前
给 Ollama 穿上 GPT 的外衣
llm·ollama
火山引擎边缘云4 天前
直播预告|火山引擎大模型网关 x 地瓜机器人教你玩转主流大模型
机器人·llm·边缘计算
Seal软件4 天前
在昇腾Ascend 910B上运行Qwen2.5推理
ai·大模型·llm·昇腾·genai