探索 Ovis 多模态大模型量化实战指南
本文介绍了 Ovis 多模态大模型的量化实战。针对消费级设备部署需求,采用 GPTQ 算法对 Ovis1.6-Gemma2-9B 和 Ovis1.6-Llama3.2-3B 进行 4-bit 量化。通过定制 AutoGPTQ 库适配 Ovis 架构,修改模型加载与推理逻辑,并优化校准数据格式。实验表明,该方案在降低显存占用的同时保持了接近原版的性能。文章提供了完整的安装环境、代码示例及微调后自行量化的流程,帮助开发者在本地高效部署多模态大模型。

本文介绍了 Ovis 多模态大模型的量化实战。针对消费级设备部署需求,采用 GPTQ 算法对 Ovis1.6-Gemma2-9B 和 Ovis1.6-Llama3.2-3B 进行 4-bit 量化。通过定制 AutoGPTQ 库适配 Ovis 架构,修改模型加载与推理逻辑,并优化校准数据格式。实验表明,该方案在降低显存占用的同时保持了接近原版的性能。文章提供了完整的安装环境、代码示例及微调后自行量化的流程,帮助开发者在本地高效部署多模态大模型。

大型语言模型(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
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):
# 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 模型的量化。
在 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 environment
conda create -n <your_env_name> python=3.10
conda 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/cu121
pip 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.git
cd AutoGPTQ
pip install -vvv --no-build-isolation -e .
调用 Ovis 量化模型执行推理:代码大纲
from transformers import GenerationConfig
from auto_gptq.modeling import OvisGemma2GPTQForCausalLM
# load model
load_device = "cuda:0" # customize load device
model = 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 Model
model = 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 follows
data_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 formation
train_loader = ...
# Start quantizing
model.quantize(examples=train_loader, cache_examples_on_gpu=False)
# Save quantized model
quantize_save_path = ...
model.save_quantized(quantize_save_path, use_safetensors=True)
为了使 Ovis 模型能够在消费级设备上运行,让更多的开源社区用户可以在本地部署,我们使用 AutoGPTQ 对 Ovis1.6 的 9B 和 3B 版本进行了量化,并发布了 Ovis1.6-Gemma2-9B-GPTQ-Int4 和 Ovis1.6-Llama3.2-3B-GPTQ-Int4。
在未来,我们将持续迭代 Ovis,为开源社区贡献性能更高、使用更便捷的多模态大模型。

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online