大型语言模型(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, 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 模型的量化。
修改代码:新增 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
以适应 Ovisgenerate
方法的入参结构。 -
_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)
完整的量化方案
采用 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,为开源社区贡献性能更高、使用更便捷的多模态大模型。
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。