多模态大模型这五个字,现在基本等于工程团队的一道必答题。手上的业务要么在做图文问答,要么在做票据文档解析,要么在做视频片段理解,绕不开一件最前置的事:架构怎么选。选错了,后面数据配比、训练脚本、推理服务全要返工,时间成本按周算。这篇整理就是我踩过几轮坑之后的一份完整笔记,把多模态大模型的两条核心架构路线彻底拆开讲清楚,再把最新一批模型的对比维度摊开来,最后落到一份能直接跑起来的最小实操方案上。适合三类人看:刚接手多模态项目的工程师、要给团队做技术选型的负责人、以及想搞明白“为什么有的模型看图强、有的模型OCR强”的技术爱好者。不需要你有分布式训练经验,但至少要跑过单机微调,不然中间那些显存账和梯度账会看得比较吃力。
1. 两种核心架构到底差在哪:先把地图画出来
多模态大模型发展到现在,架构上其实收敛成了两条清晰的路线,中间地带的产品基本都是这两条的杂交或者过渡形态。先把这张地图画出来,后面所有细节才好挂上去。第一条叫拼接式架构,也叫模块化架构、松散耦合架构,业内常说的“视觉编码器 + 投影层 + 语言主干”三段式就是它。第二条叫原生统一架构,也有人叫早期融合架构,核心主张是别搞什么外挂视觉模块,所有模态在进入主干之前就已经变成同一串 token,一个 Transformer 从头吃到尾。
这两条路线的分歧点不在工程实现,而在一个很根本的哲学判断:视觉和语言到底该不该被当成两种不同的东西处理。拼接派认为该,因为视觉信号和语言信号的统计特性差别太大,用专门预训练好的视觉编码器去抽特征,再翻译成语言主干能懂的形式,是成本最低、最稳的做法。原生派认为不该,因为人类感知世界本来就不是分模块的,强行分开会在模态边界处丢信息,尤其是细粒度空间关系、时序动作这类东西,投影层一压缩就没了。
我个人的判断是:这两条路线在2024年之后其实已经不是“谁替代谁”的关系,而是成本结构和能力上限的取舍。拼接派的上限受限于投影层这个信息瓶颈,但它的下限极高,几乎不会训崩;原生派的上限更高,能保留更细的视觉细节,但训练稳定性、数据需求量、算力开销这三座山,劝退了绝大多数团队。所以你会看到一个很有意思的现象:开源社区里能跑起来的、能落地的,九成是拼接派;而各种“下一代架构”的论文,九成在讲原生派。
为了让后面的讨论不变成厂商广告,我把具体的模型名字全部隐去,用代称指代。拼接派的代表我称为模型族 A(7B 到 70B 参数区间,视觉侧用对比学习预训练的 ViT,中间接一个轻量投影层),原生派的代表称为模型族 B(从预训练第一天就混着图文 token 一起训的那种)。这个处理方式不影响技术讨论,反而能让注意力集中在架构本身。
1.1 拼接派:编码器加投影层加语言主干的三段式
拼接式架构的工作流可以拆成三个明确的阶段,每个阶段都有独立的输入输出,这种边界清晰恰恰是它最大的工程优势。第一阶段,一张图进来,先 resize 到固定分辨率,切成 patch,送进视觉编码器。视觉编码器一般是个 ViT,输出一批视觉特征向量,比如 336×336 的图、patch size 14,那就是 24×24=576 个 patch,每个 patch 出一个 1024 维或者 1152 维的向量。第二阶段,这 576 个向量送进投影层,维度对齐到语言主干的 hidden size,比如 4096,同时数量上可能被压缩。第三阶段,这 576 个(或者压缩后的 144 个)向量当成“视觉 token”,和文本 token 拼在一起,塞进语言模型做自回归生成。
这套流程的好处说白了就是每一段都可以单独换、单独测、单独调。视觉编码器不满意?换一个更强的,投影层重新训一下就行,语言主干完全不动。语言主干想升级?换个更大的底座,投影层重训,视觉编码器不动。这种可插拔性在真实项目里价值巨大,因为业务方今天要 OCR 强,明天要图表理解强,你不可能每次都重训整个模型。
代价也很明确:信息瓶颈。576 个 patch 向量经过投影层之后,通常被压到几百个甚至几十个 token。一张 A4 扫描件上的小字,在 336 分辨率下每个字可能只占几个像素,patch 化之后基本糊成一团,投影层再怎么努力也救不回来。这就是为什么早期拼接派模型在文档类任务上表现拉胯,后来大家拼命往上堆分辨率,从 336 堆到 448,再堆到动态分辨率切图,本质上都是在跟这个瓶颈搏斗。
还有一个容易被忽略的点:视觉编码器和语言主干的训练目标不一致。视觉编码器通常是对比学习训出来的,它的特征空间优化的是“图文匹配”这个目标,而不是“有利于生成”这个目标。这两者存在系统性偏差,投影层的作用之一就是做这个偏差的补偿。理解这一点很重要,因为当你发现换了更强的视觉编码器反而效果变差时,八成就是偏差没被补偿好,投影层的容量或者训练策略不够。
1.2 原生派:一个 Transformer 吃下所有 token
原生统一架构的思路是,图像不再是一个“外来物件”,而是从一开始就和文本平权。具体实现上有两条子路线,差别很大,很多人会混淆。
第一条子路线是离散 token 化。用一个图像 tokenizer(类似 VQ-VAE 那一套)把图像编码成一串离散 ID,比如一张图变成 1024 个 ID,每个 ID 落在 8192 或 16384 大小的码本里。这样一来,图像 token 和文本 token 在形式上完全一致,都是词表里的整数索引,一个标准语言模型就能直接处理,连 embedding 层都不用改。这条路线的优点是极致的统一,缺点是量化损失——离散化过程会丢掉大量细节,尤其是纹理、颜色渐变、小字号文本,重建出来就已经模糊了,模型再强也是在模糊的基础上推理。
第二条子路线是连续 embedding 早期融合。图像经过一个轻量 patch embed 层直接变成连续向量,和文本 embedding 在同一层就拼起来,不做量化。这条路保留了更多信息,但要求主干从预训练阶段就见过图文混合序列,否则位置编码和注意力模式会严重不适配。业内真正走通这条路的模型不多,因为预训练数据配比和训练稳定性两个问题极难解决。
原生派最大的诱惑在于模态扩展的边际成本极低。你想加音频?加一个音频 tokenizer,产出 token,丢进去继续训。想加视频?视频本来就是帧序列,天然是 token 流。拼接派要做同样的事,得为每个新模态设计一套编码器加投影层,还要解决多套投影层之间的对齐问题,工程量是数倍。
但原生派的坑也是真深。我在一个小规模实验里试过走离散 token 路线,1.3B 参数,纯图文混合预训练,结果在第七天左右 loss 开始震荡,查了很久才定位到是图文序列长度比例失衡导致注意力分布塌缩到文本侧。这类问题在拼接派里几乎不会出现,因为视觉 token 和文本 token 在进入主干前就已经被投影层“驯化”过了。
1.3 一张表看清两条路线的取舍
把上面的分析压缩成一张表,选型的时候可以直接对着看:
| 对比维度 | 拼接式架构 | 原生统一架构 |
|---|---|---|
| 视觉信息保真度 | 受投影层瓶颈限制,中等 | 高,尤其是连续 embedding 路线 |
| 训练稳定性 | 高,几乎不会训崩 | 低,需要仔细调数据配比 |
| 数据需求量 | 中等,百万级图文对可用 | 极高,需要十亿级混合序列 |
| 算力门槛 | 单机多卡可起步 | 多机多卡起步 |
| 模态扩展成本 | 每加一个模态要重设计一套模块 | 加个 tokenizer 继续训即可 |
| 可解释与调试 | 好,可以分段定位问题 | 差,问题往往出在全局 |
| 推理效率 | 视觉 token 可压缩,较快 | token 数多,KV cache 压力大 |
| 生态成熟度 | 高,工具链齐全 | 低,很多要自己造轮子 |
这张表最重要的一行其实是“训练稳定性”。很多团队看论文觉得原生派上限高就想上,但忽略了一个事实:上限高不代表你能摸到。拼接派的下限是“能跑出个能用的东西”,原生派的下限是“训了两个月发现白训”。对于交付周期紧的业务,这个差别是致命的。
2. 拼接式架构的工程细节:从视觉编码器到投影层
既然九成落地项目走的都是拼接派,这一章就把它的工程细节彻底拆开。我按数据流的顺序讲:视觉编码器怎么选、投影层怎么设计、token 预算怎么算、训练分几个阶段。每一节都会给出具体的参数建议和背后的计算过程。
2.1 视觉编码器怎么选:别只看参数量
视觉编码器的选型有个很反直觉的结论:参数量不是第一优先级,预训练方式才是。同样是 300M 参数的 ViT,用对比学习(图文配对)训出来的,和用纯图像分类训出来的,在下游多模态任务上差距能到十几个点。原因在于对比学习的过程中,视觉编码器已经被迫学会了“哪些视觉特征和语言语义相关”,这个先验对后续接语言主干极其宝贵。
具体到选型,我会看三件事。第一,训练目标是否涉及文本。涉及文本的(图文对比、图文匹配)优先,纯视觉自监督的次之。第二,patch size 和原生分辨率。patch size 14 比 16 好,因为切得更细,小目标保留更多;原生分辨率支持动态输入的(比如能接受任意长宽比)比固定分辨率的强,尤其是在文档和表格场景。第三,输出维度。1024 维和 1152 维都常见,维度本身不重要,重要的是和语言主干 hidden size 的比例关系,这个下面讲投影层时会说。
踩过的坑里最典型的一个是:换了更强的视觉编码器,整体效果反而掉了两个点。排查下来发现是新编码器的特征分布和旧的不一样,原来那套投影层的初始化策略失效了。解决办法是把投影层的学习率单独调高 3 到 5 倍,并且加一个短期的“只训投影层”预热阶段。这件事说明,视觉编码器和投影层是一个耦合体,不能单独评价。
另外一个实战建议:不要迷信大分辨率。把输入从 336 提到 448,视觉 token 数从 576 涨到 1024,算力开销接近翻倍,但在通用图文问答上收益可能只有 1 个点。真正需要高分辨率的场景是文档、表格、密集小字、遥感图像这几类,如果你的业务不在这里,把省下来的算力用在数据质量上回报更高。
2.2 投影层的三种做法:线性、MLP、查询式压缩
投影层看起来是个小零件,但它决定了视觉信息进入语言主干的带宽,是整个拼接架构里最值得反复调的地方。常见做法有三种,复杂度递增。
第一种是单层线性投影。就是一个矩阵乘法,把视觉特征维度映射到语言主干维度,token 数量不变。优点是极致简单、参数少、不容易过拟合;缺点是对齐能力弱,遇到分布差异大的视觉编码器就力不从心。这种方案适合“视觉编码器和语言主干已经在相近数据上预训练过”的情况。
第二种是两层 MLP 加激活函数。中间加一个非线性,通常是 GELU,隐藏维度取语言主干维度的 1 到 4 倍。这是目前最主流的做法,性价比最高。非线性带来的表达力提升,让它能处理更复杂的一对多映射关系。我实测过 2 层 MLP 和 4 层 MLP 的差别,在通用任务上几乎没差,所以别在这一层堆深度,浪费参数量。
第三种是查询式压缩,用一个固定数量的可学习查询向量,通过交叉注意力从视觉特征里“抽取”信息。典型配置是 32 个或 64 个查询向量,输出就是 32 或 64 个视觉 token。这种做法的核心价值不是对齐,而是压缩——把 576 个 token 压到 32 个,推理时 KV cache 直接省一个数量级。
这里有个重要的取舍要讲清楚:查询式压缩是有损的。32 个查询向量再怎么设计,也没法完整承载一张复杂图表的所有信息。所以在 OCR、图表理解这类任务上,压缩率高的方案普遍吃亏。我的经验是,如果你的业务里视觉信息密度高(文档、表格、密集界面),压缩率控制在 4 倍以内;如果是自然图像理解(“图里有什么”“这是什么动物”),8 到 16 倍压缩完全够用。
还有个小技巧值得单独说:投影层的初始化。用视觉特征的均值和方差去初始化投影权重,让初始输出和语言主干的 embedding 分布接近,能显著加快收敛。这一招看着不起眼,但在我做过的一次对比里,收敛到同等 loss 的步数少了将近 30%。
2.3 分辨率和 token 预算的计算过程
这一节是纯干货,把 token 预算这笔账算清楚,因为它是你决定分辨率和压缩策略的唯一依据。整个计算链条是这样的:
给定输入分辨率H×W和 patch sizeP,视觉编码器产出的 token 数是(H/P) × (W/P)。以 336×336、P=14 为例,24×24 = 576。如果要做 2×2 的 pixel shuffle 降采样(相邻 4 个 patch 合并成 1 个,维度扩 4 倍),token 数变成576/4 = 144。如果再接一层查询式压缩,32 个查询向量,最终进入语言主干的就是 32 个 token。
这个数字意味着什么?拿语言侧做个类比:一个中文汉字在主流 tokenizer 里大约占 1 到 1.5 个 token。所以144 个视觉 token 大约等于 100 到 140 个汉字的信息容量。一张 336 分辨率的图,你指望它能承载多少文字?如果图里有一页 A4 文档(约 800 字),144 个 token 根本装不下,必然丢信息。这就是为什么文档场景必须上高分辨率加切图策略——把 A4 切成 4 块,每块单独编码,总 token 数就上去了。
反过来算推理成本。语言主干的 KV cache 大小和总序列长度成正比。假设文本输入 500 token,视觉 token 576 个(不做压缩),总长度 1076;如果视觉压缩到 144,总长度 644,KV cache 直接降到 60%。在并发场景下,这个差别会放大成实实在在的显存和延迟差异。我在一个 QPS 要求比较高的服务里,把视觉 token 从 576 压到 144,单卡并发数从 8 提到 21,端到端延迟从 1.8 秒降到 0.7 秒,代价是文档类任务的准确率掉了 4 个点。最后按业务分流做了两套配置,高精度走全量 token,高并发走压缩。
提示:token 预算这件事没有全局最优解,一定要按业务的信息密度分档。别用一个配置打天下,那不是省事,是给自己埋雷。
2.4 训练三阶段:对齐、指令微调、偏好对齐
拼接派的标准训练流程是三个阶段,每个阶段的目的、数据、冻结策略都不一样,混淆了就会出问题。
第一阶段是对齐训练,也叫预训练对齐。这个阶段语言主干和视觉编码器全部冻结,只训投影层。数据是海量图文对,量级在百万到千万。目的是让投影层学会“把视觉特征翻译成语言主干能理解的语言”。这一步的学习率可以设得比较高,因为参数量小、风险低。我在这一步通常设2e-3,训 1 到 2 个 epoch。判断收敛的标准不是 loss 降到多低,而是生成 caption 的语法正确率——如果模型能说出通顺但内容错的句子,说明对齐基本到位了;如果连语法都是乱的,那就是投影层还没训好。
第二阶段是指令微调。这时候可以放开语言主干(全参或者 LoRA),数据换成指令格式的图文对,量级在十万到百万。这一步决定模型的“对话能力”和“任务泛化能力”。这里有个关键决策:视觉编码器要不要放开。我的建议是不放开,或者只放开最后两三层。原因是视觉编码器的参数在少量指令数据上极易过拟合,训完之后通用视觉能力会掉。如果确实需要适配特殊图像域(比如医学影像、工业检测图),那就单独做一次视觉侧的领域适配,别混在指令微调里。
第三阶段是偏好对齐,用偏好数据做 DPO 或者类似方法。多模态场景下的偏好对齐比纯文本难,因为“好”的定义更模糊。一张图配一段描述,什么叫“描述得好”?是更详细,还是更准确,还是更有用?实践中我倾向于只对幻觉做惩罚,也就是构造“正确描述 vs 编造了图中不存在内容”的偏好对,专门打压幻觉。这个策略比追求全面“更好”要稳得多,也更符合业务对可靠性的要求。
三个阶段的数据配比也有讲究。很多团队在第二阶段只用指令数据,结果模型丢掉了第一阶段的视觉对齐能力,出现“能聊天但看不懂图”的退化。解决办法是在第二阶段混入 10% 到 20% 的第一阶段数据,做一个回放,成本很低但效果明显。
3. 原生统一架构的实现要点
原生派虽然落地项目少,但理解它的实现细节有两个价值:一是判断某些宣称“原生多模态”的模型到底是真是假,二是当你真的要做模态扩展时知道坑在哪。这一章讲四个点:图像怎么变成 token、位置编码怎么排、训练为什么容易崩、模态扩展的真实成本。
3.1 图像变成 token 的两条路:离散与连续
离散 token 化的核心组件是图像 tokenizer。它的工作分两步:编码器把图像压成一个低维特征图,量化器把每个位置的特征映射到码本里最近的一个向量,输出就是整数 ID。码本大小通常取 8192 或 16384,特征图尺寸决定了 token 数,比如 32×32 的特征图就是 1024 个 token。
这里最关键的参数是压缩率和码本大小的平衡。压缩率越高(token 越少),训练和推理越省,但重建质量越差。码本越大,细节保留越好,但 embedding 层参数暴涨,而且码本利用率会下降(很多码字从来没被用到)。我做过一组对照,1024 token、16384 码本的配置,重建 PSNR 能到 30 以上,小字基本可辨;换成 256 token、8192 码本,PSNR 掉到 24,小字完全糊掉。所以如果你的业务有 OCR 需求,离散路线基本可以放弃了,除非你愿意接受 1024 甚至 4096 的 token 数,那时候推理成本又会失控。
连续 embedding 路线没有量化损失,但要解决一个隐蔽的问题:视觉 embedding 和文本 embedding 的尺度不一致。文本 embedding 是学出来的,范数分布相对集中;视觉 embedding 如果直接用 patch 线性映射,范数可能大一个数量级。两者拼在一起送进 LayerNorm,视觉部分会主导归一化,文本信息被压扁。解决办法是在视觉侧加一个独立的 LayerNorm 或者缩放系数,把它拉到和文本 embedding 同一量级。这个细节在论文里经常一笔带过,但漏了就是训不动。
3.2 位置编码和序列排布:二维结构不能被压平就完事
图像是二维的,语言是一维的,把图像 token 拍平成一维序列丢进去,位置信息就丢了。拼接派靠视觉编码器内部的二维位置编码解决了这个问题(视觉特征本身已经带了空间信息),但原生派是直接在主干里处理的,必须显式设计。
常见做法有三种。第一种是可学习的绝对位置编码,给每个图像位置分配一个 embedding,简单但泛化差,换个分辨率就要重训。第二种是二维 RoPE,把旋转位置编码拆成两个一维分别作用在行和列上,泛化性好,是目前比较主流的选择。第三种是不显式编码,靠 attention mask 或者特殊分隔符,效果最差,基本只有早期工作这么干。
序列排布也有讲究。图文交错的时候,图像 token 是放在提及它的文本之前、之后,还是单独成段?实践中效果最好的是放在提及位置之后紧邻,因为语言的指代关系需要上下文,图像紧跟在指代之后,注意力最容易建立关联。但这样会导致序列碎片化,训练效率下降。另一个常见做法是把所有图像 token 前置,然后靠位置编码区分,简单但关联性弱一些。
还有一个实际会遇到的问题:多图输入时的注意力混淆。一次输入三张图,模型经常把第二张图的内容说成第一张的。解决办法是给每张图加独立的可学习分隔 token,或者用二维位置编码的图索引维度。这个改动很小,但多图场景下效果提升明显。
3.3 训练稳定性:原生路线为什么更容易崩
我在小规模实验里遇到的稳定性问题,归纳下来有三类,每一类的成因和应对都不一样。
第一类是模态间的梯度尺度失衡。文本 token 的 loss 和图像 token 的 loss 量级不同,如果混在一起算总 loss,梯度会被大的那一侧主导。表现是训练初期 loss 下降很快,但某一侧的能力完全不涨。应对办法是给不同模态的 loss 加权重,或者更彻底一点,做梯度归一化——按模态分别算梯度范数,然后缩放到同一量级再更新。后者实现起来麻烦,但效果确实好。
第二类是序列长度分布不均导致的注意力塌缩。图文混合数据里,纯文本样本可能 200 token,图文样本可能 2000 token,混着训的时候,长序列的注意力会被大量短序列稀释。表现是模型对长序列的建模能力上不去,而且训练后期 loss 平台期特别长。应对办法是按长度分桶采样,并且控制一个 batch 内的长度方差。
第三类是分辨率变化带来的位置编码错配。如果你的训练数据里图像分辨率不统一,而位置编码是按固定尺寸设计的,那每次换分辨率都会引入系统性偏差。表现是模型在某些尺寸的图上表现正常,换个尺寸就崩。应对办法是位置编码用插值或者直接用支持动态尺寸的方案。
注意:原生路线的很多问题在训练日志上表现为“loss 正常但效果不对”,非常隐蔽。一定要在训练过程中定期跑小规模评测,别只看 loss。
3.4 模态扩展的真实成本
原生派最常被吹的一点是“加模态很容易”。这话对一半。技术路径上确实容易:设计一个 tokenizer,产出 token,混进训练数据,继续训。但真实成本在三个地方。
第一个是数据。加一个新模态,你需要这个模态和其他所有模态的配对数据。加音频,你需要音频-文本对、音频-图像对、音频-图像-文本三元组。这个数据量级不是几十万条能解决的,而且很多配对数据在公开渠道根本拿不到。我在一个视频理解项目里算过账,要训出可用的视频能力,光视频-文本对就需要千万级,标注成本是图文对的十倍以上。
第二个是训练配比。新模态加进来,和已有模态的数据比例怎么定?比例不合适,要么新模态学不会,要么旧模态能力退化。这是个反复试错的过程,每次试错都是几十张卡跑几天。
第三个是评测。新模态的评测基准往往不成熟,你得自己造。造评测集的工作量经常被低估,我见过一个团队花了三个月训模型,最后花四个月造评测集,因为公开基准和自己的业务场景差异太大。
所以我的结论是:模态扩展在架构层面是便宜的,在数据层面是昂贵的。如果一个团队连单模态的高质量数据都还没有,先别急着上多模态。
4. 最新模型横向对比:拿哪几个维度去量
标题里说“最新模型对比分析”,但我不想做成一个型号罗列表,那种表过三个月就过期,而且对选型没实质帮助。更有价值的是对比维度本身——知道该量什么,你就能自己去量任何新出来的模型。这一章讲维度怎么定、怎么填表、以及榜单分数和业务表现之间的落差。
4.1 对比维度的选取逻辑
维度选取的原则是:能区分架构路线、能预测业务表现、能实测。三个条件缺一个,这个维度就不该进表。
按这个原则筛下来,我会用下面这几组。第一组是架构属性,包括架构类型、视觉侧参数占比、视觉 token 数、是否支持动态分辨率。这组的作用是快速归类,看到一个新模型,先判断它是拼接派还是原生派,视觉带宽大概多少。第二组是训练属性,包括预训练数据规模、是否多阶段训练、指令微调数据量。这组能预测模型的泛化能力,数据量少但效果好的,通常是架构或者训练策略有巧思。第三组是能力实测,包括通用图文理解、文档 OCR、图表推理、幻觉率、多图关系理解。这组必须自己跑,别看榜单。第四组是工程属性,包括推理显存、首 token 延迟、并发能力、量化友好度。这组决定了能不能上线。
特别说一下幻觉率这个维度,很多对比表里没有,但它对业务影响最大。一个模型通用分数再高,如果它老是编造图里不存在的东西,在客服、医疗、工业质检这些场景就是不可用的。测幻觉的方法很简单:给模型看一张图,问图里没有的东西,比如图里是只猫,你问“这只狗什么品种”,看它是纠正你还是顺着答。顺答率超过 20% 的模型,我一般会直接排除。
4.2 一份可复用的对比表
下面这张表是我自己用的模板,里面的数值是我在几个项目里实测的量级范围,用区间表示,因为具体数字依赖硬件和配置,写死反而误导。模型用代称,A 系列是拼接派,B 系列是原生派。
| 对比项 | 拼接派 A-7B | 拼接派 A-13B | 原生派 B-7B | 备注 |
|---|---|---|---|---|
| 视觉 token 数 | 144 ~ 576 | 144 ~ 576 | 1024 左右 | A 系可配,B 系固定 |
| 动态分辨率 | 支持(切图) | 支持(切图) | 部分支持 | 影响 OCR 表现 |
| 单图推理显存 | 14 ~ 18 GB | 24 ~ 30 GB | 20 ~ 26 GB | 含 KV cache,序列 1K |
| 首 token 延迟 | 200 ~ 400 ms | 350 ~ 600 ms | 400 ~ 700 ms | 单卡,batch=1 |
| 通用图文理解 | 基线 | +3 ~ 5 点 | +1 ~ 3 点 | 相对 A-7B |
| 文档 OCR | 中等 | 中等偏上 | 偏弱 | 依赖分辨率策略 |
| 图表推理 | 中等 | 中等偏上 | 偏弱 | 依赖视觉保真度 |
| 多图关系 | 偏弱 | 中等 | 中等偏上 | 原生序列更自然 |
| 幻觉率 | 8% ~ 15% | 7% ~ 12% | 10% ~ 18% | 我的实测口径 |
| 微调成本 | 低 | 中 | 高 | LoRA 单卡可跑 vs 需多卡 |
| 生态工具 | 完善 | 完善 | 欠缺 | 量化、部署、评测 |
表格里最值得琢磨的两行是“幻觉率”和“微调成本”。原生派幻觉率偏高不是偶然,因为它的图像信息是原始 token 直接进来的,模型有更多“自由发挥”的空间;拼接派有投影层这个瓶颈,反而起到了某种约束作用。微调成本这一行更现实——拼接派用 LoRA 单卡 24G 就能跑起来,原生派稍微一动就是几十张卡,这不是技术优劣,是能不能做的问题。
4.3 榜单之外:真实业务里的落差
这一节说点不太好听但很重要的。公开榜单上的排名,和业务里的实际表现,落差经常大到让人怀疑人生。原因有三个。
第一个是榜单数据的分布和业务数据差太远。榜单里的图通常是清晰的、构图规整的、主体明确的,业务里的图是手机随手拍的、模糊的、带水印的、角度歪的。一个模型在榜单上 85 分,在你的业务数据上可能只有 60 分,而且不同模型的排序会完全反过来。
第二个是榜单任务形式和业务任务形式不同。榜单多是选择题或者短答案,业务多是开放式生成。模型做选择题的能力和做开放式生成的能力,相关性没有想象中高。我见过一个模型选择题分数很高,但让它自由描述一张图,就开始胡编。
第三个是榜单不测工程指标。延迟、显存、并发,这些在榜单上一个字都没有,但它们决定项目能不能上线。
所以我的做法是:永远先在自己的业务数据上抽 200 到 500 条做小评测,再决定要不要深入。这个评测集不需要标注得很完美,但一定要覆盖业务里的真实分布,包括那些“脏”样本。这 200 条样本的价值,超过任何公开榜单。
5. 实操:从零搭一个最小可用的多模态模型
讲完架构和对比,落到动手。这一章给一套完整的最小可行方案,7B 级别,拼接式架构,单机 8 卡能跑起来。我不想给一个“理论上最优”的方案,那种方案在真实环境里跑不通;我给的是一个能在两周内出可用结果的方案。
5.1 环境与数据准备
环境这块没什么花活,主流的深度学习框架加分布式训练库就够。关键的版本约束是:视觉编码器和语言主干要用同一套精度策略,别一个 fp16 一个 bf16,会出莫名其妙的数值问题。我统一用 bf16,虽然老卡不支持,但现在租的卡基本都支持了。
数据准备是整个流程里最耗时的部分,占掉 60% 以上的时间。格式上我建议统一成一种简单的结构:每条样本一个 JSON,包含图像路径、对话轮次列表。别一开始就搞复杂的多图、交错格式,先把单图单轮跑通。
数据质量的检查清单,我列几条实战中最容易忽略的:
- 图像可读性。听起来很傻,但真的有大量损坏文件混在数据集里,训练时随机报错,非常难查。开训前跑一遍全量校验。
- 文本长度分布。统计一下回答长度的分布,如果有个别样本回答上万字,会把序列长度拉爆。截断或者剔除。
- 图文相关性。抽样人工看 100 条,如果相关性差的比例超过 5%,说明数据源有问题,别急着训。
- 重复样本。图文对数据集里重复非常常见,去重能省不少算力,还能降低过拟合。
数据配比上,我的经验是:通用图文问答、文档类、细粒度描述,三类按 5:3:2 混。文档类占比不能太低,否则模型在结构化图像上会明显弱。
5.2 训练脚本的关键片段
下面这段是投影层对齐阶段的核心逻辑,省略了数据加载和日志部分。重点看三个地方:冻结策略、学习率分组、loss 计算。
import torch import torch.nn as nn # 冻结视觉编码器和语言主干,只训投影层 for p in vision_encoder.parameters(): p.requires_grad = False for p in language_model.parameters(): p.requires_grad = False for p in projector.parameters(): p.requires_grad = True # 学习率分组:投影层用大学习率,其余为 0 optimizer = torch.optim.AdamW( [ {"params": projector.parameters(), "lr": 2e-3}, {"params": vision_encoder.parameters(), "lr": 0.0}, {"params": language_model.parameters(), "lr": 0.0}, ], betas=(0.9, 0.98), weight_decay=0.0, ) # 前 3% 步数做线性 warmup,避免初期梯度爆炸 total_steps = len(dataloader) * epochs warmup_steps = int(total_steps * 0.03) scheduler = torch.optim.lr_scheduler.LambdaLR( optimizer, lambda s: min(1.0, s / max(1, warmup_steps)), ) for step, batch in enumerate(dataloader): images = batch["images"].to(device, dtype=torch.bfloat16) input_ids = batch["input_ids"].to(device) labels = batch["labels"].to(device) # 视觉侧:编码 + 投影,得到视觉 token with torch.no_grad(): vis_feat = vision_encoder(images) # [B, N, D_v] vis_tokens = projector(vis_feat) # [B, N', D_l] # 文本侧:embedding 后与视觉 token 拼接 text_emb = language_model.get_input_embeddings()(input_ids) inputs_embeds = torch.cat([vis_tokens, text_emb], dim=1) # 视觉位置不计 loss,label 用 -100 屏蔽 vis_labels = torch.full( (vis_tokens.size(0), vis_tokens.size(1)), -100, device=device ) full_labels = torch.cat([vis_labels, labels], dim=1) outputs = language_model(inputs_embeds=inputs_embeds, labels=full_labels) loss = outputs.loss loss.backward() torch.nn.utils.clip_grad_norm_(projector.parameters(), max_norm=1.0) optimizer.step() scheduler.step() optimizer.zero_grad(set_to_none=True)几个容易写错的地方,单独强调。第一,视觉 token 对应的 label 必须是 -100,不然模型会去学着“预测图像 token”,那是另一套训练目标,混进来会把语言能力带偏。第二,梯度裁剪只作用于可训练参数,别对全局裁剪,否则冻结参数的梯度是 0,裁剪阈值形同虚设。第三,get_input_embeddings拿到的 embedding 层不要带梯度,在对齐阶段它应该和语言主干一起冻结,有些实现里忘了这层,导致 embedding 被悄悄更新了。
指令微调阶段把冻结打开,用 LoRA 的话配置大概是:rank 128、alpha 256,作用在注意力的 q/k/v/o 和 MLP 的 gate/up/down 上。rank 别再小了,视觉对齐需要比较大的容量,rank 16 那种配置在多模态任务上明显不够。
5.3 显存和吞吐的账怎么算
这笔账必须算清楚,否则你会在训练第二天收到 OOM。7B 参数模型,全参训练的话:参数本身 bf16 是 14GB,梯度 bf16 是 14GB,AdamW 的优化器状态(一阶动量加二阶动量,fp32)是 56GB,加起来 84GB,还没算激活值。单卡 80G 直接爆。所以全参必须上 ZeRO-2 或者 ZeRO-3,8 卡分摊之后每卡 10GB 出头,能跑。
LoRA 就轻松多了。7B 模型冻结参数 bf16 占 14GB,LoRA 参数 rank 128 大概几百 MB,优化器状态也就 GB 级,激活值取决于 batch 和序列长度。序列长度 2048、batch size 4 的情况下,单卡 24G 基本够。这也是我为什么强烈建议先从 LoRA 起步——它让你在一张卡上就能验证整条链路通不通。
对齐阶段更省,因为视觉编码器和语言主干都冻结,只有投影层可训。但要注意,前向传播的激活值还是要存的,如果视觉编码器比较大,激活值本身就可能占十几 GB。这时候可以用梯度检查点,代价是慢 20% 到 30%,但能省一半以上的激活显存。
吞吐方面给个参考量级:8 卡 A100 80G,7B 模型,LoRA 微调,序列长度 2048,全局 batch 128,大概每秒能处理 3 到 5 个 step。如果是全参加 ZeRO-3,同样的配置掉到每秒 1 到 2 个 step。数据量 50 万条的话,3 个 epoch 大概需要 3 到 5 天。这个数字可以用来倒推项目排期。
提示:算显存账的时候一定要留 20% 余量。真实训练里的峰值显存经常出现在你没想到的地方,比如某个 batch 里有张超大图,或者某个样本序列特别长。
5.4 评测:别只盯着 loss 曲线
训练跑起来之后,最容易犯的错是只看 loss。多模态任务的 loss 和实际效果的相关性,比纯文本任务弱得多。因为 loss 是 token 级的平均,而用户体验是样本级的。一个模型可能 99% 的 token 都预测对了,但那 1% 恰好是最关键的数字,用户就直接判定“这模型不行”。
我自己的评测分三层。第一层是自动化指标,包括固定测试集上的准确率、BLEU 或者 CIDEr 这类生成指标、以及幻觉率。第二层是分类抽样人工看,按任务类型分层各抽 50 条,看失败模式集中在哪。这一步最有价值,因为它能告诉你“模型错得有没有规律”。第三层是端到端体验,把自己当成真实用户去用,看哪里别扭。很多问题只有亲自用才能发现,比如输出格式不稳定、多轮对话里丢失前文信息、对模糊指令的理解偏差。
有个具体建议:建一个“回归集”,把每次上线后发现的问题样本都存进去,做成一个固定的小集合,每次模型迭代都跑一遍。这个集合会随着项目推进越来越有价值,最后成为你最核心的资产。我在一个项目里积累到 400 多条回归样本,后来换模型、换架构,全靠它来兜底。
6. 常见问题与排查实录
这一章是我自己踩过的坑的合集,按症状归类。多模态的问题排查比纯文本难,因为故障点跨了两个模态,症状和原因之间经常隔着一层。
6.1 训练阶段的典型症状
症状一:loss 正常下降,但模型完全不看图,只按语言先验答题。这是最常见的。判断方法很简单:把图像换成纯噪声再测一遍,如果输出基本不变,就是这个问题。原因通常是视觉 token 在注意力里权重太低,或者投影层没训好。排查顺序是先看视觉 token 的范数,和文本 embedding 比一下量级,差一个数量级就加 LayerNorm;再看对齐阶段训了多久,少于 1 个 epoch 基本不够。
症状二:训到一半 loss 突然飙升然后不降。八成是学习率或者数据问题。先查数据,有没有超长样本、损坏图像、空文本;再查学习率,投影层的学习率 2e-3 已经偏高了,如果 batch 小,降到 5e-4。还有一个隐蔽原因:混合精度下的数值溢出,加梯度缩放一般能解决。
症状三:模型输出重复循环。典型的表现是“图中有一只猫,猫在桌子上,桌子上有猫……”。这是生成阶段的退化,原因可能是训练数据里有大量重复模式,或者解码策略的温度设置太低。先调解码参数验证,如果调完就好,说明是推理配置问题;如果调完还这样,那就是训练数据要清洗。
症状四:换了新视觉编码器之后,OCR 能力反而下降。这个我遇到过,原因是新编码器的输入分辨率归一化方式和旧的不一样,预处理代码没同步改。多模态项目里,预处理的一致性比模型本身更容易出问题,因为图像预处理和后处理分散在不同代码文件里。
6.2 推理部署阶段的坑
坑一:batch 推理时不同样本的视觉 token 数不一样。动态分辨率一开,每张图切出来的 patch 数不同,padding 处理不好就会串味。解决办法是把视觉 token 补齐到固定长度,并且用 attention mask 严格屏蔽 padding 部分。这一条不做,多图并发的输出会完全错乱。
坑二:KV cache 显存预估错误。视觉 token 也要占 KV cache,很多人只算了文本部分。一张高分辨率图 576 个视觉 token,在 32 层 32 头的模型里,KV cache 大概要多占几百 MB。并发一上来,这个数字会直接顶爆显存。
坑三:量化之后效果断崖式下跌。4bit 量化对语言能力影响不大,但对视觉对齐的破坏可能很严重,因为投影层的输出分布范围比较宽,量化误差被放大。如果必须量化,优先量化语言主干,投影层保持高精度。
坑四:多轮对话里图像被重复编码。每轮都把历史图像重新过一遍视觉编码器,延迟直接翻几倍。正确做法是缓存视觉 token,后续轮次直接复用。这个优化实现起来不难,但收益很大。
6.3 一份速查表
| 症状 | 最可能的原因 | 快速验证方法 | 处理方向 |
|---|---|---|---|
| 不看图只靠语言先验 | 视觉 token 权重过低 | 换成噪声图看输出是否变化 | 加 LayerNorm、延长对齐训练 |
| loss 中途飙升 | 学习率过高或数据异常 | 检查超长样本与损坏图 | 降学习率、清洗数据 |
| 输出重复循环 | 训练数据重复或解码配置 | 调温度与重复惩罚 | 清数据、调解码参数 |
| 换编码器后能力下降 | 预处理不一致 | 对比新旧预处理输出 | 统一归一化与尺寸处理 |
| 多图输出串味 | padding 未屏蔽 | 单图与多图结果对比 | 加 attention mask |
| 量化后大幅掉点 | 投影层被量化 | 投影层单独保持高精度 | 分层量化策略 |
| 多轮延迟高 | 图像重复编码 | 打日志看编码调用次数 | 缓存视觉 token |
| 幻觉严重 | 指令数据里编造样本多 | 问图中不存在的物体 | 构造偏好对做 DPO |
最后分享一个我个人在多次项目里验证过的判断标准:如果一个多模态模型在你的业务数据上,前 50 条抽样里有一条是“完全编造”的,那这个模型还不能上生产。架构选型、参数调优、榜单对比,所有这些工作最后都要落到“用户能不能信它”这一件事上。我宁愿要一个能力中等但输出可靠的模型,也不要一个能力很强但偶尔胡说八道的模型,因为在真实业务里,一次胡说的修复成本,远高于十次“答得不够好”。