大型语言模型(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 库提供了用户友好的 API,用户可通过短短数行代码使用 GPTQ 算法量化自己的大语言模型。目前,AutoGPTQ 库已支持 Llama、Mistral、Gemma、Qwen、Yi 等主流 LLM。然而,该库无法直接支持 Qwen2-VL、Ovis 等多模态大模型。因此,我们需要定制其源码,使其适配 Ovis 的模型结构,从而完成量化。
代码分析
首先,我们需要了解使用 AutoGPTQ 库来量化模型的流程。AutoGPTQ 库的 README 给出的量化示例代码大致如下:
from transformers import AutoTokenizer, TextGenerationPipeline
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
import logging
pretrained_model_dir = "facebook/opt-125m"
quantized_model_dir = "opt-125m-4bit
tokenizer
= 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]))
也即,量化一个模型大致需要经历如下流程:
-
准备校准样本
:GPTQ、AWQ 等属于训练后量化算法,运行时
会让原模型在校准数据上推理,记录隐藏层的激活值等信息,以确定合适的比例因子来量化模型参数,以实现更好的量化性能。
-
设置量化参数
:如量化的位数等。这部分不需要过多调整。
-
加载模型;调用量化函数;保存量化模型。
而要加载一个量化模型并执行推理,只需用相应类的
from_quantized
方法加载模型,并将推理样本处理成该模型的
generate
方法所需入参格式,传入即可,与普通模型的推理过程一致。
可见,AutoGPTQ 的量化过程隐含了推理步骤在其中。这引发了一些问题:量化 Ovis 时,
quantize
方法内是否会按正确的传参格式来调用 Ovis 以推理?怎样的校准数据变量格式才符合
quantize
方法内的调用要求?上述推理代码中传入
model.generate
的参数排布与 Ovis
generate
方法的签名不符(详见 Ovis1.6 Hugging Face 仓库内的 modeling_ovis.py),如何解决该问题?仔细查看
model.quantize
和
model.generate
方法后,我们发现:
-
quantize
方法用
examples
参数接收校准数据,其类型提示为
examples: List[Dict[str, Union[List[int], torch.LongTensor]]]
,也即
examples
需要是一个 list,其内各元素为字典,字典内各个值为 torch Tensor 或整数列表。
-
quantize
方法内
共有三处使用了
examples
,依次为:
-
examples = self._prepare_examples_for_quantization(examples, batch_size)
:调用
_prepare_examples_for_quantization
方法对传入的
examples
参数做某种转换。
-
num_batches = len(examples)
:求转换后的
examples
的长度,设其值为 batch 的总数。由此可见,
_prepare_examples_for_quantization
方法的其中一个功能是将校准样本组成 batch, 使返回的
examples
列表内的元素从原先的表示单个样本变成表示一个 batch 的样本。
-
见代码:
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 字典,将其键-值对直接传给模型来推理。
-
上述
self.model(**example)
是
quantize
方法唯一调用模型进行推理的地方。
-
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):
layers_block_name = "model.decoder.layers"
outside_layer_modules = [
"model.decoder.embed_tokens", "model.decoder.embed_positions", "model.decoder.project_out",
"model.decoder.project_in", "model.decoder.final_layer_norm"
]
inside_layer_modules = [
["self_attn.k_proj", "self_attn.v_proj", "self_attn.q_proj"],
["self_attn.out_proj"],
["fc1"],
["fc2"]
]
可见,对于不在支持列表内的模型,只需继承
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。完整的方案如下。
安装运行环境:
conda create -n python=3.10
conda activate
pip install torch==2.2.1 torchvision==0.17.1 torchaudio==2.2.1
pip install numpy==1.24.3 transformers==4.44.2 pillow==10.3.0 gekko pandas
git clone https://github.com/AIDC-AI/AutoGPTQ.git
cd AutoGPTQ
pip install -vvv
调用 Ovis 量化模型执行推理:代码大纲
from transformers import GenerationConfig
from auto_gptq.modeling import OvisGemma2GPTQForCausalLM
load_device = "cuda:0"
model = OvisGemma2GPTQForCausalLM.from_quantized(