大型语言模型(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.quantizemodel.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 中导入OvisGemma2GPTQForCausalLMOvisLlamaGPTQForCausalLM,以方便使用。


校准数据的变量格式


按上文修改后,我们便可大致按下列代码实现 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 <your_env_name> python=3.10conda activate <your_env_name>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( "AIDC-AI/Ovis1.6-Gemma2-9B-GPTQ-Int4", device=load_device, trust_remote_code=True)model.model.generation_config = GenerationConfig.from_pretrained("AIDC-AI/Ovis1.6-Gemma2-9B-GPTQ-Int4")text_tokenizer = model.get_text_tokenizer()visual_tokenizer = model.get_visual_tokenizer()
# THE REST IS THE SAME AS UNQUANTIZED VERSIONS OF OVIS...


微调原版 Ovis 模型后,自行量化:代码大纲


from auto_gptq.modeling import OvisGemma2GPTQForCausalLM
# Load Modelmodel = OvisGemma2GPTQForCausalLM.from_pretrained( model_path, quantize_config, torch_dtype=torch.bfloat16, multimodal_max_length=8192, trust_remote_code=True).cuda()model.model.llm.model.config.use_cache = False
# Prepare your own calibration samples here and format them as followsdata_list = [ { "image": "path/to/image/of/this/sample", "conversations": [ { "from": "human", "value": "<image>\n[Your sample prompt]" }, { "from": "gpt", "value": "[Anything]" } ] }, ...]
# See the Hugging Face Model Cards for details on dataloader formationtrain_loader = ...
# Start quantizingmodel.quantize(examples=train_loader, cache_examples_on_gpu=False)
# Save quantized modelquantize_save_path = ...model.save_quantized(quantize_save_path, use_safetensors=True)



完整的量化方案


采用 GPTQ 算法来量化模型时,校准步骤的目的是计算出合适的量化比例因子,使得量化后模型的使用表现尽可能与量化前接近,因此校准数据的分布应尽可能与模型被实际使用时接触的数据一致。这要求我们从训练数据集合中挑选合适的子集用于校准。

Ovis 的训练过程分为 4 个阶段。其中,前两个阶段优化视觉模块,旨在实现视觉嵌入到文本信息的激发,后两个阶段优化整个模型,旨在让模型能够遵循多模态指令。显然,后两个阶段的多模态指令数据最契合模型实际应用场景,最适合用作校准数据。


(图 3/表 3: 采用不同数量、不同质量的多模态指令数据校准后的模型性能)


我们通过实验分析了多模态指令数据的数量和质量对模型量化校准后性能的影响。由图 3 可知,更多的校准数据并没有带来更好的量化性能;使用质量更高的校准数据可以有效提升量化模型的性能。



上图展示了 Ovis1.6-Gemma2-9B 和 Ovis1.6-Llama3.2-3B 的量化性能以及与原版的对比。可见,基于 AutoGPTQ 的 4-bit 量化方案可取得与原版相差无几的性能。



我们使用 500 条 ChartQA 高分辨率数据来测试模型量化前后的总推理时长及显存占用峰值(batch_size=1)。如上图所示,我们的量化方案有效降低了显存占用峰值。



总结


为了使 Ovis 模型能够在消费级设备上运行,让更多的开源社区用户可以在本地部署,我们使用 AutoGPTQ 对 Ovis1.6 的 9B 和 3B 版本进行了量化,并发布了 Ovis1.6-Gemma2-9B-GPTQ-Int4 和 Ovis1.6-Llama3.2-3B-GPTQ-Int4。


在未来,我们将持续迭代 Ovis,为开源社区贡献性能更高、使用更便捷的多模态大模型。


相关链接:


论文arXiv

https://arxiv.org/abs/2405.20797


Github

https://github.com/AIDC-AI/Ovis


Hugging Face

Ovis1.6-Gemma2-9B-GPTQ-Int4

https://hf.co/AIDC-AI/Ovis1.6-Gemma2-9B-GPTQ-Int4

Ovis1.6-Llama3.2-3B-GPTQ-Int4

https://hf.co/AIDC-AI/Ovis1.6-Llama3.2-3B-GPTQ-Int4




本文由 Hugging Face 中文社区内容共建项目提供,稿件由社区成员投稿,经授权发布于 Hugging Face 公众号。文章内容不代表官方立场,文中介绍的产品和服务等均不构成投资建议。了解更多请关注公众号: 
如果你有与开源 AI、Hugging Face 相关的技术和实践分享内容,以及最新的开源 AI 项目发布,希望通过我们分享给更多 AI 从业者和开发者们,请通过下面的链接投稿与我们取得联系:
https://hf.link/tougao

内容中包含的图片若涉及版权问题,请及时与我们联系删除