多模态大模型这两年从论文里的概念快速落到了工程实践中,但真正动手跑通一个视觉语言模型(VLM)的推理和微调,中间踩的坑远比想象中多。Yi-VL 系列开源之后,我第一时间在自己的环境里把它从权重加载、图片推理一路跑到 LoRA 微调,整个过程里有些环节文档写得比较简略,有些则是版本兼容的暗坑。这篇就把我实际跑下来的完整链路整理出来,包括环境怎么配、推理脚本怎么写、微调数据怎么组织、显存怎么省,以及几个我调了很久才定位到的问题。不管你是刚接触多模态、想先跑个 Demo 看看效果,还是已经准备拿自己的业务数据做微调,下面这些内容应该都能直接拿去用。
Yi-VL 是零一万物开源的视觉语言模型系列,包含 6B 和 34B 两个规格,架构上是视觉编码器加投影层再对接语言模型主干的经典三段式结构。它能做的事很直接:给一张图加一段文字提问,模型输出对图片的理解、描述、推理结果。适合的人群其实挺广——做文档理解、图表问答、商品图描述、工业质检辅助判断这类场景的开发者,都可以拿它当基座来试。我下面讲的所有操作,都是基于常见工程实践补全的细节,原始资料里没写透的地方我会明确说明这是我的经验补充。
1. 先把运行环境和权重加载这件事理清楚
很多人卡在第一步不是因为不会写代码,而是环境装完发现模型加载报错,或者显存直接爆掉。这一节把环境准备和权重加载的完整逻辑拆开讲,包括为什么这么选版本、显存大概要多少、加载时有哪些容易忽略的参数。
1.1 依赖版本的选择逻辑与常见冲突
Yi-VL 的推理依赖主要围绕 transformers、torch、accelerate 和 Pillow 这几个包。我这里给一套实测能跑通的组合,但更重要的是理解为什么版本要对齐。
pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118 pip install transformers==4.35.0 accelerate==0.24.1 pip install Pillow==10.1.0 sentencepiece==0.1.99为什么锁 transformers 的版本?因为 Yi-VL 的模型代码里用到了LlavaForConditionalGeneration类似的视觉语言模型接口约定,不同版本的 transformers 对多模态 processor 的字段命名和返回结构有过调整。版本太新,processor 返回的pixel_values维度顺序可能和模型预期不一致,表现为推理结果全是乱码或者直接 shape mismatch;版本太旧,又可能不认识模型 config 里的某些字段。4.35 这个区间是我试下来比较稳的。
torch 和 CUDA 的对应关系也要注意。cu118 对应的是 CUDA 11.8 驱动环境,如果你机器上是 CUDA 12.x 的驱动,向下兼容一般没问题,但反过来不行。装之前先nvidia-smi看一眼驱动支持的 CUDA 版本,别盲目装。
提示:不要在一个已经装了其他大模型依赖的全局环境里直接叠装,版本冲突排查起来非常痛苦。用 conda 或 venv 建独立环境,这是省时间而不是浪费时间。
1.2 权重下载与目录结构
权重从模型托管平台拉下来之后,目录结构要保持原样,不要自己重命名文件。典型的结构是这样:
Yi-VL-6B/ ├── config.json ├── generation_config.json ├── tokenizer.model ├── tokenizer_config.json ├── preprocessor_config.json ├── model-00001-of-00002.safetensors ├── model-00002-of-00002.safetensors └── model.safetensors.index.jsonpreprocessor_config.json这个文件很关键,里面定义了图像预处理的分辨率、归一化均值方差等参数。有些人为了省事只拷了权重和 config,结果图片进去之后模型输出完全不对,就是因为预处理参数丢了。加载时直接用目录路径:
from transformers import AutoModelForCausalLM, AutoTokenizer from PIL import Image import torch model_path = "./Yi-VL-6B" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype=torch.float16, device_map="auto", trust_remote_code=True )trust_remote_code=True这个参数不能省,Yi-VL 的模型定义里有自定义的前向逻辑,不加会直接报找不到模型类。device_map="auto"让 accelerate 自动分配显存,单卡够用就全放一张卡,多卡会自动切分。
1.3 显存占用的估算方法
6B 模型用 fp16 加载,光权重就大约 12GB,加上推理时的 KV cache 和图像编码的中间激活,单张图推理峰值大概在 16GB 到 18GB 之间。所以 24GB 显存的卡跑 6B 推理是比较舒服的,16GB 的卡会紧张,需要控制输入图片分辨率和生成长度。
34B 版本 fp16 权重约 68GB,单卡基本放不下,要么用多卡 device_map 切分,要么上量化。我实测 34B 用 4bit 量化后大约 20GB 出头能跑起来,但量化会带来一定的输出质量损失,尤其是细粒度文字识别类任务,损失比较明显。如果你的任务对精度要求高,优先考虑多卡 fp16 而不是单卡量化。
估算显存的粗略公式可以记一下:权重显存 ≈ 参数量 × 精度字节数,fp16 是 2 字节,4bit 是 0.5 字节,再额外留 20% 到 30% 给激活和 cache。这个公式不精确但足够你做容量规划。
2. 图片推理的完整调用链路与参数调优
环境通了之后,下一步是把推理跑对。这一节讲清楚从图片输入到文本输出的每一步在做什么,以及生成参数怎么调才能既快又准。
2.1 从图片到模型输入的预处理细节
模型不是直接吃原始图片的,中间要经过 resize、归一化、切 patch 这一套。Yi-VL 的图像编码器对输入分辨率有固定要求,预处理时会先把图片调整到指定尺寸。这里有个实操要点:图片的宽高比如果和模型预期差异太大,resize 会造成形变,影响识别效果。
我的处理习惯是先做一次等比缩放加 padding,而不是直接硬 resize。比如目标分辨率是 448×448,我会把原图按长边缩到 448,短边不足的部分用背景色填充,这样内容不变形。对于文档类图片,这个细节对文字识别准确率影响很大。
from PIL import Image def smart_resize(image, target=448): w, h = image.size scale = target / max(w, h) new_w, new_h = int(w * scale), int(h * scale) resized = image.resize((new_w, new_h), Image.LANCZOS) canvas = Image.new("RGB", (target, target), (255, 255, 255)) canvas.paste(resized, ((target - new_w) // 2, (target - new_h) // 2)) return canvas用 LANCZOS 重采样而不是默认的最近邻,是因为它在缩小图片时保留的细节更多,对文字和细线条更友好。这个函数不是模型强制的,是我自己加的预处理层,你可以按任务需要决定要不要用。
2.2 对话模板与 prompt 组织方式
Yi-VL 走的是对话式接口,图片通过特殊的占位符嵌入到对话里。调用时一般是这样组织:
query = "请描述这张图片里的主要内容,如果有文字请一并识别出来。" image = smart_resize(Image.open("test.jpg").convert("RGB")) response, history = model.chat( tokenizer=tokenizer, query=query, image=image, history=None, max_new_tokens=512, temperature=0.3 ) print(response)model.chat是模型自定义的方法,内部会把图片编码成视觉 token 拼接到文本 token 序列前面。history传 None 表示单轮对话,如果要连续追问同一张图,把上一轮的 history 传回去即可,但注意每轮都会重新编码图片,多轮场景下开销会累积。
prompt 的写法直接影响输出质量。我总结的经验是:任务描述要具体,输出格式要约束。比如你要做结构化抽取,与其说"分析这张图",不如说"提取图中表格的所有行,每行输出为 JSON,字段为名称和数值"。模型对明确格式指令的遵循度明显更高。
2.3 生成参数的取舍:temperature、top_p 与 max_new_tokens
这几个参数不是随便填的,每个都对应不同的行为倾向。
| 参数 | 作用 | 我的常用取值 | 说明 |
|---|---|---|---|
| temperature | 控制随机性 | 0.1~0.3 | 描述、抽取类任务要低,创意类可到 0.7 |
| top_p | 核采样阈值 | 0.9 | 配合 temperature 用,一般不动 |
| max_new_tokens | 最大生成长度 | 256~1024 | 按任务输出长度估,设太大浪费显存和时间 |
| repetition_penalty | 抑制重复 | 1.05~1.1 | 长输出时防止车轱辘话 |
temperature 设 0 理论上最确定,但部分实现里 0 会触发除零或退化成贪心,我一般设 0.1 而不是 0。max_new_tokens 设太大不只是慢的问题,KV cache 会随长度线性增长,长输出场景下显存峰值可能比你预期高不少。做批量推理时尤其要卡住这个值。
2.4 批量推理的显存与吞吐平衡
单张推理跑通之后,很多人想直接上 batch。这里要泼个冷水:多模态模型的 batch 推理不像纯文本那么线性,因为每张图的视觉 token 数量固定但文本长度不一,padding 会浪费算力。我的做法是把长度相近的请求分到一组,减少 padding 比例。
另外,视觉编码器那部分计算是每张图独立的,batch 化收益有限,真正能并行的是语言模型解码阶段。所以如果你的场景是"大量图片各问一个问题",提升吞吐更有效的办法是多进程各占一张卡,而不是单卡大 batch。这个结论是我对比过两种方案之后得出的,单卡 batch=4 和四卡各 batch=1 的总吞吐接近,但后者显存更从容。
3. LoRA 微调:数据组织、训练配置与显存优化
推理跑顺了,接下来就是拿自己的数据做微调。全参数微调 6B 模型对大多数团队来说成本偏高,LoRA 是更现实的选择。这一节讲数据怎么准备、LoRA 挂在哪、训练参数怎么定。
3.1 微调数据的格式设计与质量把控
多模态微调数据本质上是"图片路径 + 对话"的配对。我用的格式是 JSON Lines,每行一条样本:
{"image": "images/0001.jpg", "conversations": [{"from": "user", "value": "这张图里的设备指示灯是什么颜色?"}, {"from": "assistant", "value": "指示灯为绿色。"}]}几个数据质量上的经验点,都是踩过坑总结的:
- 图片路径要能稳定解析,训练脚本里做一次存在性校验,缺图样本直接跳过并记录,不要让训练中途崩。
- 回答要短而准,多模态微调最容易犯的错是答案写得太长太发散,模型学到的是一堆废话。目标任务的答案控制在 1 到 3 句,格式统一。
- 负样本和边界样本要覆盖,比如图片里没有用户问的物体时,答案应该是"图中未发现该物体",而不是硬编一个。这类样本不加,模型上线后会疯狂幻觉。
数据量上,LoRA 微调一般几百到几千条就能看到明显效果,但前提是分布要对齐你的目标场景。我试过用 500 条高质量垂类数据,效果比 5000 条泛泛的通用数据好得多。
3.2 LoRA 目标模块的选择
LoRA 的核心是在注意力层的线性变换上挂低秩矩阵。对 Yi-VL 这类模型,通常挂在语言模型主干的 q_proj、k_proj、v_proj、o_proj 上,视觉编码器一般冻结不动。
from peft import LoraConfig, get_peft_model lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM" ) model = get_peft_model(model, lora_config) model.print_trainable_parameters()为什么视觉编码器不挂 LoRA?因为视觉编码器参数量相对小,且预训练的视觉表征已经比较通用,微调它容易过拟合且收益不明显。真正需要适配业务的是"如何根据视觉特征组织语言回答",这部分在语言主干里。r=8 是个保守起点,任务复杂可以加到 16 或 32,但 r 越大显存和过拟合风险越高。
lora_alpha一般设成 r 的两倍,这是社区里比较通行的经验比例,控制的是 LoRA 更新量相对于原权重的缩放。
3.3 训练超参与显存优化组合拳
训练配置上,我常用的一套起点是这样的:
training_args = TrainingArguments( output_dir="./yi-vl-lora", per_device_train_batch_size=1, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=3, logging_steps=10, save_strategy="epoch", fp16=True, gradient_checkpointing=True, optim="adamw_torch", report_to="none" )显存不够时的优化顺序,我按效果排序:先开gradient_checkpointing(用时间换显存,能省 30% 到 40%),再降 batch 到 1 配合梯度累积,然后考虑 8bit 优化器,最后才是量化基座。gradient_accumulation_steps=8配合 batch=1,等效 batch 是 8,对小数据集够用。
学习率 2e-4 是 LoRA 的常见量级,比全参数微调高一个数量级,因为 LoRA 只更新少量参数。epoch 数不要贪多,多模态任务 3 到 5 轮就够,再多容易把模型带偏,表现为通用能力下降、回答变得模板化。
注意:训练时把验证集留出来,每个 epoch 存一次 checkpoint,最后按验证集表现选模型,而不是用最后一个 epoch。我吃过这个亏,最后一轮过拟合,验证指标反而掉了。
4. 那些文档里没写、我实际踩到的坑
这一节是整篇里最"私货"的部分,都是我在真实环境里撞出来的问题,排查过程也一并写出来,方便你对照复现。
4.1 图片通道数导致的诡异报错
有一次推理脚本在部分图片上稳定报错,错误信息指向张量维度不匹配,但换一张图就正常。排查了半天发现是 PNG 图片带了 alpha 通道(RGBA 四通道),而模型预处理预期三通道 RGB。修复很简单,加载时统一convert("RGB"):
image = Image.open(path).convert("RGB")这个坑的隐蔽性在于,大部分 JPG 是三通道所以不报错,只有部分 PNG 或截图类图片会触发。养成加载即转 RGB 的习惯,能省掉这类随机性故障。
4.2 多轮对话里图片被重复编码的性能问题
前面提过,多轮对话每轮都会重新编码图片。我一开始没在意,直到做一个连续追问的 Demo 时发现响应越来越慢。原因是每轮 chat 调用都把图片重新过了一遍视觉编码器,而视觉编码的计算量不小。
解决办法有两个方向:一是把图片编码结果缓存下来,后续轮次复用视觉特征;二是如果业务允许,把多轮问题合并成一轮提问。前者需要改模型调用逻辑,后者更简单。我最终选了合并提问,因为我的场景里追问之间没有强依赖,合并后一次输出反而更连贯。
4.3 微调后通用能力下降的定位过程
第一次 LoRA 微调完,垂类任务准确率上去了,但拿通用图片测试时发现模型描述能力明显变差,回答变得生硬。排查思路是这样的:
- 先确认不是推理参数问题——用相同 temperature 对比微调前后,排除采样差异。
- 检查训练数据分布——发现我的数据里 90% 是短答案,模型被带成了"惜字如金"的风格。
- 混入一定比例的通用描述数据做回放,比例大概 1:4(通用:垂类),再训一轮。
调整后垂类指标基本没掉,通用能力恢复了大半。这个经验说明:LoRA 微调不是只喂目标任务数据就完事,适当混入通用数据做正则,是保住基座能力的关键。这个比例没有标准答案,得按你的数据量和任务偏离程度试。
4.4 生成结果截断与 EOS 处理
有段时间发现模型输出经常在句子中间断掉。查下来是 max_new_tokens 设得太小,输出被硬截断。但把值调大之后又出现另一个问题:部分样本生成停不下来,一直输出到上限。根因是模型没有正常输出结束符。
处理办法是在生成后做一次后处理,按句号或换行截断,同时训练数据里确保每条回答都以正常结束符结尾。另外检查 tokenizer 的 eos_token 配置是否和模型一致,这个不一致会导致模型不知道该在哪停。我在一次换用不同来源的 tokenizer 文件后就遇到过这个问题,换回权重目录自带的 tokenizer 就好了。
5. 从 Demo 到可用:部署侧的几个务实建议
跑通推理和微调只是第一步,真要上线还得考虑服务化和稳定性。这一节聊几个部署层面的实际问题。
5.1 推理服务的并发与超时设计
多模态推理单次耗时通常在几百毫秒到几秒之间,取决于图片大小和输出长度。做服务时,请求队列要有上限,超时时间要按 P99 耗时来设,而不是平均值。我见过按平均耗时设超时导致高峰期大量请求被误杀的情况。
并发数不要按 GPU 显存除以单请求显存来算,因为峰值是波动的。保守做法是先压测出单卡稳定并发数,再留 30% 余量。图片预处理(解码、resize)其实也吃 CPU,高并发下 CPU 可能先成为瓶颈,这点容易被忽略。
5.2 模型版本与配置的固化
上线前把模型权重、tokenizer、preprocessor 配置、推理代码版本全部固化下来,打成一个不可变镜像或版本化目录。多模态模型对预处理参数敏感,任何一处配置漂移都可能导致线上输出和测试环境不一致。我习惯在服务启动时打印一份配置摘要到日志,出问题时第一时间能对照。
5.3 效果监控的落地方式
线上没法像离线那样人工看每条输出,我的做法是抽样加规则双管:按比例抽样人工复核,同时对输出做规则检查,比如空输出率、超长输出率、特定关键词命中率。这些指标异常往往先于用户投诉暴露问题。另外把输入图片的尺寸分布也监控起来,突然出现大量超大图,可能是上游传参出了问题,会拖垮推理耗时。
这套东西搭起来不复杂,但能让你在模型"看起来正常"的时候提前发现退化。多模态模型的效果退化往往很隐蔽,不像纯文本那样容易从字面看出来,所以监控要更主动一些。
最后分享一个我自己的使用习惯:每次换模型版本或改训练数据,都固定用同一批"回归测试图"跑一遍,人工扫一眼输出。这批图覆盖文档、图表、自然场景、带文字截图几类,数量不用多,二三十张就够。它能帮你快速判断这次改动是变好还是变坏,比看一堆指标数字直观得多。