专栏名称: Hugging Face
The AI community building the future.
目录
相关文章推荐
唐史主任司马迁  ·  昨天两市成交2.11万亿,盘中还是比较强韧的 ... ·  9 小时前  
爱股君2020  ·  又搞事情了。。 ·  2 天前  
淘股吧  ·  1天亏20个点!明天谁敢接飞刀? ·  23 小时前  
诸海滨科新先声  ·  【PPT】万达轴承:叉车轴承国内第一全球第三 ... ·  4 天前  
51好读  ›  专栏  ›  Hugging Face

社区供稿 | 探索 Ovis: 多模态大模型量化的实战指南

Hugging Face  · 公众号  ·  · 2024-11-20 10:30

正文

大型语言模型(LLM)近年来取得了长足进步,为通用人工智能(AGI)带来了曙光。 这些模型展现出强大的文本理解和生成能力,但要真正接近人类智能的复杂性和多面性,LLM必须突破纯文本的限制,具备理解视觉信息的能力。 为此,研究者们将目光投向了多模态大型语言模型(MLLM),旨在赋予模型感知和理解视觉信息的能力。


当前开源MLLM大多并非从头训练整个模型,而是借助预训练的LLM和视觉Transformer来构建文本和视觉模块。这两个模块采用不同的嵌入策略:文本嵌入是从LLM的嵌入查找表中索引得到的,其中文本词表的每个“单词”通过独热文本token映射到一个嵌入向量。相比之下,视觉嵌入通常由视觉编码器经MLP连接器投影后以非结构化方式直接生成。虽然基于MLP连接器的MLLM在许多任务上取得了不错的成绩,但由于模态间嵌入策略的结构性差异,这种架构存在潜在的局限性。


一个自然而然的问题是:如果像文本嵌入那样,以结构化的方式生成视觉嵌入,是否能进一步提升MLLM的性能? 为了探究这个问题,我们提出了一种名为Ovis (Open VISion)的新型MLLM架构。Ovis借鉴了LLM中的文本嵌入策略,引入了可学习的视觉嵌入表,将连续的视觉特征先转换为概率化的视觉token,再经由视觉嵌入表多次索引加权得到结构化的视觉嵌入。


经过半年多的迭代,Ovis系列已经推出1.0、1.5、1.6三个大版本。其中,9月发布的Ovis1.6-Gemma2-9B在多模态领域权威评测榜单OpenCompass上取得68.8的综合得分,位列30B以下开源模型榜首,超过了Qwen2-VL-7B、InternVL2-26B、MiniCPM-V-2.6等知名开源多模态大模型。在10月,我们发布了Ovis1.6-Llama3.2-3B,取得了61.7的OpenCompass均分,在4B以下模型中位列第一,超过了InternVL2-4B、Qwen2-VL-2B等4B以下开源模型,甚至超过规模更大的Llama-3.2-11B-Vision-Instruct。


Ovis的强劲性能在 Hugging Face、X 等社区上收获了热烈反响,获得了累计10万余次下载量以及众多海外技术大V的转发和点赞。 为进一步扩大Ovis的社区影响力,让更多人用消费级设备来体验、部署Ovis,我们在11月推出了Ovis的量化版本:Ovis1.6-Gemma2-9B-GPTQ-Int4和Ovis1.6-Llama3.2-3B-GPTQ-Int4。 本文将介绍Ovis模型的量化过程,为开发者量化自己的Ovis模型提供参考。



量化方案的选取


我们希望量化后的 Ovis 模型能被社区用户广泛使用,让用户可以无需繁琐的流程而轻松部署到自己的设备上。同时,我们希望给出一套完整量化的方案,能让用户根据自己的需求,量化经自己微调后的 Ovis 模型。最后,在满足以上条件的基础上,我们希望这套量化方案能让模型维持较好的性能。因此我们需要采用通用、易用且高性能的量化方案。经调研,目前主流的多模态大模型采用的高性能量化算法有 AWQ 和 GPTQ,两者均有成熟的开源库支撑,且已被 Qwen2-VL,InternVL-Chat-V1-5 等多模态大模型采用。其中,Qwen2-VL 公布了相对完整的量化流程,对不同规模的模型均提供了 GPTQ 和 AWQ 两个量化版本。


(图 1:Qwen2-VL 7B 和 2B 量化模型的性能表现。数据取自 Qwen2-VL GitHub 页面)


由图 1 可见,采用 GPTQ 或 AWQ 算法量化后,Qwen2-VL 均可维持与原版相近的高性能。我们还实测了两种算法量化后的模型运行速度和显存占用峰值


(图 2:Qwen2-VL-7B-Instruct 的实测结果。我们使用 500 条 ChartQA 高分辨率数据来测试 Qwen2-VL-7B-Instruct 原版及其不同版本量化模型的总推理时长及显存占用峰值。实验环境参照Qwen2-VL GitHub 页面指示)


由图 2 可见,采用 AWQ 算法量化后,Qwen2-VL-7B-Instruct 推理时的显存占用量与 GPTQ 算法接近,而推理用时显著高于 GPTQ 。基于此数据,我们选择采用 GPTQ 算法来量化 Ovis1.6。



定制AutoGPTQ库


AutoGPTQ 库提供了用户友好的 API,用户可通过短短数行代码使用 GPTQ 算法量化自己的大语言模型。目前,AutoGPTQ 库已支持 Llama、Mistral、Gemma、Qwen、Yi 等主流 LLM。然而,该库无法直接支持 Qwen2-VL、Ovis 等多模态大模型。因此,我们需要定制其源码,使其适配 Ovis 的模型结构,从而完成量化。


代码分析

首先,我们需要了解使用 AutoGPTQ 库来量化模型的流程。AutoGPTQ 库的 README 给出的量化示例代码大致如下:

from transformers import AutoTokenizer, TextGenerationPipelinefrom auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfigimport logging
pretrained_model_dir = "facebook/opt-125m"quantized_model_dir = "opt-125m-4bit
# 加载 Tokenizertokenizer = AutoTokenizer.from_pretrained(pretrained_model_dir, use_fast=True)
# 准备校准数据样本examples = [ tokenizer( "auto-gptq is an easy-to-use model quantization library with user-friendly apis, based on GPTQ algorithm." )]
# 设置量化参数quantize_config = BaseQuantizeConfig( bits=4, # quantize model to 4-bit group_size=128, # it is recommended to set the value to 128 desc_act=False, # set to False can significantly speed up inference but the perplexity may slightly bad)
# 加载待量化模型model = AutoGPTQForCausalLM.from_pretrained(pretrained_model_dir, quantize_config)
# 开始量化model.quantize(examples)
# 保存量化模型model.save_quantized(quantized_model_dir, use_safetensors=True)

加载量化后模型并推理的代码如下:


# 加载量化模型model = AutoGPTQForCausalLM.from_quantized(quantized_model_dir, device="cuda:0")
# 设置推理样例sample = "auto_gptq is"
# 推理print(tokenizer.decode(model.generate(**tokenizer(sample, return_tensors="pt").to(model.device))[0]))


也即,量化一个模型大致需要经历如下流程:

  1. 准备校准样本 :GPTQ、AWQ 等属于训练后量化算法,运行时 会让原模型在校准数据上推理,记录隐藏层的激活值等信息,以确定合适的比例因子来量化模型参数,以实现更好的量化性能。

  2. 设置量化参数 :如量化的位数等。这部分不需要过多调整。

  3. 加载模型;调用量化函数;保存量化模型。


而要加载一个量化模型并执行推理,只需用相应类的 from_quantized 方法加载模型,并将推理样本处理成该模型的 generate 方法所需入参格式,传入即可,与普通模型的推理过程一致。


可见,AutoGPTQ 的量化过程隐含了推理步骤在其中。这引发了一些问题:量化 Ovis 时, quantize 方法内是否会按正确的传参格式来调用 Ovis 以推理?怎样的校准数据变量格式才符合 quantize 方法内的调用要求?上述推理代码中传入 model.generate 的参数排布与 Ovis generate 方法的签名不符(详见 Ovis1.6 Hugging Face 仓库内的 modeling_ovis.py),如何解决该问题?仔细查看 model.quantize model.generate 方法后,我们发现:


  1. quantize 方法用 examples 参数接收校准数据,其类型提示为 examples: List[Dict[str, Union[List[int], torch.LongTensor]]] ,也即 examples 需要是一个 list,其内各元素为字典,字典内各个值为 torch Tensor 或整数列表。


  2. quantize 方法内 共有三处使用了 examples ,依次为:

    1. examples = self._prepare_examples_for_quantization(examples, batch_size) :调用 _prepare_examples_for_quantization 方法对传入的 examples 参数做某种转换。

    2. num_batches = len(examples) :求转换后的 examples 的长度,设其值为 batch 的总数。由此可见, _prepare_examples_for_quantization 方法的其中一个功能是将校准样本组成 batch, 使返回的 examples 列表内的元素从原先的表示单个样本变成表示一个 batch 的样本。

    3. 见代码:

      for example in examples:    for k, v in example.items():        if len(v.shape) == 1:            v = v.unsqueeze(0)        example[k] = move_to_device(v, cur_layer_device)    try:        self.model(**example)    except ValueError:        pass


      也即, examples 列表内的元素经处理后仍是字典,但每个字典内包含一个 batch 的样本。 在该循环中,对每个 batch,先将其值转移到对应的设备上(如 GPU),再解包 batch 字典,将其键-值对直接传给模型来推理。


  1. 上述 self.model(**example) quantize 方法唯一调用模型进行推理的地方。


  2. model.generate 方法代码如下:


    def generate(self, **kwargs):    """shortcut for model.generate"""    with torch.inference_mode(), torch.amp.autocast(device_type=self.device.type):        return self.model.generate(**kwargs)


    可见该方法仅用作将传入参数转发给原模型的 generate 方法,并在 torch.inference_mode 下开启 AMP 来执行。


由这些观察,我们可以得出结论:只需在 num_batches = len(examples) 处,保证 examples 内各样本为已组好 batch 的字典,其中的各 key 值为调用 self.model() 推理时的入参名,且各 value 为对应值,即可保证 quantize 方法正常执行模型推理。同时,只需修改 model.generate 方法自身入参结构,或在一个继承了其所在基类的子类中覆写该方法,即可正常执行 Ovis 推理。


我们继续看 AutoGPTQ 官方的如何量化不在支持列表内模型的教程:

from auto_gptq.modeling import BaseGPTQForCausalLM

class OPTGPTQForCausalLM(BaseGPTQForCausalLM): # chained attribute name of transformer layer block layers_block_name = "model.decoder.layers" # chained attribute names of other nn modules that in the same level as the transformer layer block outside_layer_modules = [ "model.decoder.embed_tokens", "model.decoder.embed_positions", "model.decoder.project_out", "model.decoder.project_in", "model.decoder.final_layer_norm" ] # chained attribute names of linear layers in transformer layer module # normally, there are four sub lists, for each one the modules in it can be seen as one operation, # and the order should be the order when they are truly executed, in this case (and usually in most cases), # they are: attention q_k_v projection, attention output projection, MLP project input, MLP project output inside_layer_modules = [ ["self_attn.k_proj", "self_attn.v_proj", "self_attn.q_proj"], ["self_attn.out_proj"], ["fc1"], ["fc2"] ]
# After this, you can use OPTGPTQForCausalLM.from_pretrained and other methods as shown in Basic.

可见,对于不在支持列表内的模型,只需继承 BaseGPTQForCausalLM 类,并根据模型结构设置好相关变量的值,即可用新建类的 from_pretrained 方法来加载模型,并调用 quantize 方法来完成量化。

基于以上分析,我们按下列方式修改了 AutoGPTQ 库的代码,以实现 Ovis 模型的量化。


修改代码:新增 ovis.py


在 auto_gptq.modeling 目录中,AutoGPTQ 库支持的各模型均将其量化类实现在对应 py 文件中。例如,gemma2.py 的内容如下:


from logging import getLogger
from ._base import BaseGPTQForCausalLM

logger = getLogger(__name__)

class Gemma2GPTQForCausalLM(BaseGPTQForCausalLM): layer_type = "Gemma2DecoderLayer" layers_block_name = "model.layers" outside_layer_modules = ["model.embed_tokens", "model.norm"] inside_layer_modules = [ ["self_attn.k_proj", "self_attn.v_proj", "self_attn.q_proj"], ["self_attn.o_proj"], ["mlp.up_proj", "mlp.gate_proj"], ["mlp.down_proj"], ]

__all__ = ["Gemma2GPTQForCausalLM"]


其中:

  • OvisGPTQForCausalLM 类继承了 BaseGPTQForCausalLM ,并根据 Ovis1.6-Gemma2-9B 和 Ovis1.6-Llama3.2-3B 模型内子模块层级结构,定义了 outside_layer_modules 等量化所需变量值。注意,我们仿照 Qwen2-VL 的做法,只量化多模态模型中 LLM 部分的 Decoder 层,而保持 ViT 及模型其它部分不变。


  • generate 方法覆写了 BaseGPTQForCausalLM.generate 以适应 Ovis generate 方法的入参结构。


  • _prepare_examples_for_quantization 待下一节介绍。


  • 对于采用不同 LLM 基座的 Ovis 模型(Gemma2, Llama3.2),我们定义 OvisGPTQForCausalLM 的不同子类来分别实现其特化部分。其中与 fused_attn_module_type 等变量相关的代码摘自 auto_gptq.modeling 内的 llama.py。


最后,在 auto_gptq.modeling 目录内的 __init__.py 中导入 OvisGemma2GPTQForCausalLM OvisLlamaGPTQForCausalLM ,以方便使用。


校准数据的变量格式


按上文修改后,我们便可大致按下列代码实现 Ovis 模型的量化:


from auto_gptq.modeling import OvisGemma2GPTQForCausalLM
# 准备校准数据样本examples = ...
# 设置量化参数...
# 加载待量化模型model = OvisGemma2GPTQForCausalLM.from_pretrained(...)
# 开始量化model.quantize(examples=examples, ...)
# 保存model.save_quantized(...)


此时,我们需要准备好校准样本,并处理好其变量格式,以在作为 examples 参数传入 quantize 方法后正常运行。根据上节的分析,我们将传入 quantize 方法的 examples 设计为一个 torch Dataloader,其在被遍历时会返回已完成 padding 的 batch 样本字典,可直接解包作为各参数传入 Ovis 模型的 forward 函数以实现推理。此时,由于样本组 batch 等工作已被 Dataloader 实现,故应采用某种方式使得 _prepare_examples_for_quantization 方法在 quantize 方法内被调用时不对 examples 做任何改变。我们通过在自定义的 OvisGPTQForCausalLM 类中覆写 _prepare_examples_for_quantization 实现了这一点。详细的校准数据变量格式构造参见 Ovis1.6-Gemma2-9B-GPTQ-Int4 或 Ovis1.6-Llama3.2-3B-GPTQ-Int4 的 Hugging Face model card。




完整的量化方案


我们开源了经以上方式修改后的 AutoGPTQ 库,并在量化版 Ovis 模型的 Hugging Face Model Card 内给出了详细的使用指南,便于社区用户部署 Ovis 量化模型或量化经自己微调后的 Ovis。完整的方案如下。


安装运行环境:


# BE SURE TO RUN UNDER CUDA 12.1
# 1. Get a basic environmentconda create -n python=3.10conda activate pip install torch==2.2.1 torchvision==0.17.1 torchaudio==2.2.1 --index-url https://download.pytorch.org/whl/cu121pip install numpy==1.24.3 transformers==4.44.2 pillow==10.3.0 gekko pandas
# 2. Build AutoGPTQ: We customized AutoGPTQ to support Ovis model quantization. You need to build from source to install the customized version.git clone https://github.com/AIDC-AI/AutoGPTQ.gitcd AutoGPTQpip install -vvv --no-build-isolation -e .


调用 Ovis 量化模型执行推理:代码大纲


from transformers import GenerationConfigfrom auto_gptq.modeling import OvisGemma2GPTQForCausalLM
# load modelload_device = "cuda:0" # customize load devicemodel = OvisGemma2GPTQForCausalLM.from_quantized(






请到「今天看啥」查看全文