ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多模态大模型底层架构拆解:从Transformer到Token融合实践指南

多模态大模型底层架构拆解:从Transformer到Token融合实践指南

前阵子有个做应用开发的同学来问我,多模态大模型到底是怎么做到同时看懂图片和文字的。他说网上资料刷了一堆,要么堆名词,要么是产品宣传稿,看完还是不知道底层是怎么转起来的。我给他讲完这套架构之后,他最大的感慨是:原来那层“多”的背后,是把所有输入都变成同一体系里的token再做统一处理,而底层承载这一切的,仍然是那套大家绕不开的Transformer体系。这篇文章我就按自己的理解,把大模型与多模态的底层架构拆开讲讲,包括不同融合路线的取舍、训练和推理层面的细节,以及我在实际项目中踩过的坑。适合两类人看:一类是准备选型做多模态应用的开发者,另一类是刚入行、想搞懂大模型原理但没时间啃论文的读者。

1. 底层基座:为什么大模型都绕不开Transformer

1.1 自注意力机制:让模型自己决定“看哪里”

如果你只看懂一个概念,那就先看自注意力机制。它的核心作用,简单说就是让序列里的每一个元素,都能直接和其他所有元素建立关联。想象一群人在开会,每句话都需要参考整场所有发言内容才能确定自己的意思,这就是自注意力在做的事。所谓的“关联”,是通过查询(Query)、键(Key)、值(Value)三个向量之间的点积计算出来的,计算到哪个位置的关联分数高,模型在本次输出时就会更偏向吸收哪个位置的信息。

这套机制带来的能力提升非常关键。在它出现之前,处理序列数据主要靠循环神经网络,信息是顺着时间步一步一步传的,长距离的依赖关系容易被层层稀释。而自注意力打破了顺序限制,无论两个token隔着多远,都能在一步之内建立联系。这也是大语言模型能够处理几千上万个上下文窗口的基础能力。

实际代码实现里,自注意力一般会写成类似下面这个过程:输入经过三个矩阵映射出Q、K、V,再按多头拆分,然后做缩放点积计算。下面这个伪代码描述的就是核心流程,很多教材里都能看到类似的实现思路。

import torch import torch.nn.functional as F def self_attention(x, W_q, W_k, W_v, W_o, num_heads): batch_size, seq_len, hidden_dim = x.shape head_dim = hidden_dim // num_heads Q = x @ W_q K = x @ W_k V = x @ W_v # 拆多头 Q = Q.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) K = K.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) V = V.view(batch_size, seq_len, num_heads, head_dim).transpose(1, 2) # 缩放点积注意力 scores = Q @ K.transpose(-2, -1) / (head_dim ** 0.5) attn = F.softmax(scores, dim=-1) out = attn @ V # 合并多头输出 out = out.transpose(1, 2).contiguous().view(batch_size, seq_len, hidden_dim) return out @ W_o

这里有个容易忽略的细节:注意力分数要除以根号下的头维度。头维度越大,点积结果方差越大,如果不缩放,softmax之后分布会过于尖锐,梯度很容易变成很小的数甚至消失。这个缩放系数不是拍脑袋决定的,而是有理论依据的。

1.2 多头注意力与整体结构

多头注意力就是把上面这个过程并行做多份。每一头有自己独立的Q、K、V投影矩阵,这相当于让模型从多个不同子空间去看同一个输入序列。有的头可能更关注语法关系,有的头可能更关注指代关系,有的头对位置敏感,最后再把所有头的结果拼起来做一次线性变换。多头的设置初衷,就是不想让模型只用一种“立场”去理解输入。

在Transformer完整结构里,正常情况下由两部分组成:编码器部分负责把输入序列编码成一系列带上下文的表征,解码器部分负责根据已有输出序列逐步生成下一个token。大语言模型通常只保留解码器的自回归结构,因为生成式任务本质上就是“预测下一个token”。多模态大模型则往往要重新引入编码器,或者用统一序列的方式处理图像输入,这一点后面详细讲。

1.3 位置编码:给序列补上“先后顺序”

自注意力机制本身是不感知顺序的,它把所有token当成一个集合来计算关系。但语言是有顺序的,“我爱你”和“你爱我”的token组成完全一样,语义却刚好反过来。所以必须在输入里加入位置信息。

早期常见做法是正弦位置编码,或者让模型自己去学习一组位置向量。现在业界用的比较多的则是旋转位置编码(RoPE),它的思路是把位置信息以旋转矩阵的方式作用在Q、K向量上,位置差会直接体现为两个向量之间的角度差。这种方式有几个好处:一是能处理比训练时更长的序列而不至于完全崩掉;二是内积结果只跟相对位置有关,比较符合语言模型对相邻token关系的建模需求。我在实际项目中测试过,把老式位置编码换成RoPE之后,长文本的检索准确率有明显提升,这在长上下文中尤其值得注意。

2. 多模态融合架构:文本、图像、音频如何在一个模型里共存

2.1 三条主流融合路线

多模态模型之间架构差异很大,如果只看产品层面的输出,很容易以为它们都差不多。实际拆开来看,目前业界主流大致可以归成三类思路。

第一类是双塔结构。文本一个塔,图像一个塔,各自独立编码,最后把两个表征拿到同一个向量空间里去算相似度或做对比学习。这种结构在图文检索任务上效率很高,因为图像和文本的特征都已经离线算好存起来了,线上只用做向量检索。缺点是只得到了“图文匹配”的能力,模型本身不擅长生成新的内容,很难直接拿来做对话式多模态助手。

第二类是单塔统一序列结构。把图像切片也转成token序列,和文本token拼在一起,直接送进统一的Transformer里进行处理。这样Cross-Attention就不再是两个独立模态之间的交互,而是把图文token混合在同一个自注意力序列里,让模型自己学习跨模态的关联。这种结构对生成和理解任务都更友好,是很多多模态对话模型和视觉生成模型的基础路线。

第三类是分层对齐加映射的方案。保留一个预训练好的文本大模型作为底座,另外训练一个视觉编码器,再通过一层或多层映射模块(Adapter/Q-Former类型的结构)把视觉特征转换到文本模型能理解的表征空间。训练的时候可以冻结文本底座,只更新视觉编码器和映射层,这样训练成本比重新训练一个大模型低很多,也能快速拿到不错的多模态能力。缺点是视觉信息和文本信息之间的交互深度有限,复杂推理任务的表现会差一些。

三类方案没有绝对的好坏,关键看你的目标。做检索项目选第一类会省心很多,做对话助手该选第二或第三类,而如果做的是垂直场景且训练预算有限,第三类往往是性价比最高的起点。

2.2 视觉token化:把图像变成大模型能读的序列

大模型只认token序列,图像也得转成token才能和文本统一处理。这里有一种经典做法是把图像切成一堆固定大小的patch,每个patch通过一个形状(比如16x16像素)压平成向量,再经过视觉编码器编码成视觉token。这个过程很像把一张大拼图拆成几百块小拼图,每一块都带着自己的像素信息进了大模型。

这里涉及到一个最实际的问题:token数量决定了算力成本和延迟。假设输入图像分辨率是512x512,切patch尺寸是16x16,那就是1024个patch,也就是1024个视觉token。如果分辨率翻倍到1024x1024,token数量就直接变成4096个。注意这里不是线性翻倍,而是平方级增长。这也是为什么很多多模态模型在实际做推理的时候,图像部分的耗时远高于一段短文本的耗时。

业界通常的做法是双阶段分辨率策略:预训练阶段用低分辨率输入,保证训练速度;微调阶段再提高分辨率,提升模型对细节的感知能力。或者采用动态分辨率方案,根据图像的宽高比自适应调整切块方式,在信息量和算力之间找平衡。我自己做项目时深有体会,如果不控制图像token数量,批量推理时显存占用会非常难看,后面章节我会给一个具体的估算方法。

2.3 跨模态对齐:模型怎么知道“图里这只猫”等于“文本里的cat”

多模态融合最大的难题,不是把图像和文本的表征拼在一起,而是如何让两个完全不同来源的信息在同一个语义空间里对齐。图像里的猫是一个由像素组成的实体,文本里的“猫”是一个抽象符号,它们之间没有天生的一一对应关系,全靠训练数据里成对的“图片-描述”把两者拉近。

比较经典的方法是对比学习。训练的时候,同一组图文对要尽量靠近,不同组的图文则要尽量远离。损失函数里通常包含一个温度参数,用来控制相似度分布的平滑程度。温度越低,负样本对模型的压力越大,模型会更激进地区分样本,但也更容易训练不稳定,表现成loss震荡甚至直接发散。温度怎么调,跟数据集分布有关,没有万能公式,我的经验是先在验证集上做小范围搜索,而不是直接拍一个数。

除了对比损失,实际训练里还会加一些任务相关的损失,比如图文匹配的二分类损失、文本生成的交叉熵损失。多种损失需要设权重,最常见的问题是文本生成损失收敛得比较快,图像对比损失收敛得比较慢,如果不做调整,训练后期模型会越来越像“只会读题不会看图”的偏科生。我见过不少多模态模型训练完以后,你问它图里有什么,它能答上来,你再问它两个物体之间的位置关系,它就胡说。这种情况往往不是模型容量不够,而是多任务损失权重和训练策略没调好。

2.4 多模态训练要分阶段走

训练一个多模态大模型,如果妄想从一开始就让所有参数同时随机初始化并一起收敛,结果通常是指数级增长的算力消耗和难以控制的loss波动。实际工程里,多模态模型训练几乎都采用分阶段策略。

常见流程是:先在大量图文对数据上用对比学习训练视觉编码器和文本对齐模块,让模型建立起图文匹配的初步能力;然后接入文本大模型底座,用图文交错数据做指令微调,训练模型按用户指令理解图像并生成回应;最后再用一些高质量人工标注数据做对齐微调,优化回答的安全性和有用性。

每一个阶段的学习率、批量大小和数据配比都不一样。对比学习阶段一般用的学习率比较小,批量则尽量大,因为对比学习对batch size非常敏感,batch太小的话负样本不够,模型很容易陷入一种“自嗨”状态,以为所有图文对都匹配。指令微调阶段反而要控制batch size,因为生成任务的计算量跟序列长度强相关,batch太大容易显存溢出。

3. 工程落地:从模型架构到训练与部署调优

3.1 显存与算力估算:先算账再动手

很多入门的同学在跑大模型之前,根本不估一下显存和算力,结果代码一启动就OOM,然后开始怀疑人生。其实这件事在动手之前是可以粗略估算的。

以参数量为70B的模型为例,如果采用FP16精度存储,每个参数占2字节,那么光是权重文件就需要140GB显存。这还没算优化器状态、梯度、KV Cache和中间激活值。实际训练显存通常是权重的3到6倍,这取决于你用的优化器、并行策略和batch大小。推理阶段虽然不需要优化器状态和梯度,但依然需要承载权重的显存,再加上KV Cache会随序列长度和batch大小动态变化,所以必须预留余量。

KV Cache的显存估算我有过一个简化公式,基本是:显存占用约等于2乘以层数乘以头数乘以头维度乘以序列长度乘以batch大小再乘以字节数。假设某模型有32层,32头,头维度128,序列长度4096,batch为8,用FP16存储,计算下来约为2乘以32乘以32乘以128乘以4096乘以8乘以2字节,结果大约是17GB。这个数字在部署长上下文的对话系统时是非常值得关注的,它意味着就算权重很小,上下文拉长后KV Cache也可能会悄悄吃掉一大半显存。

3.2 训练并行策略:数据并行、张量并行、流水线并行

训练超大模型几乎不可能在单卡上完成,必须引入多种并行策略。数据并行是比较容易理解的方式:每张卡持有一份完整的模型副本,把不同的数据分到不同卡上,然后梯度做同步平均。这种方案通信开销较小,但问题是单卡显存装不下模型时就没法用了。

这时需要张量并行。它把每一层内部的计算切到多张卡上,比如把矩阵乘法按行或按列切开,多个卡协同完成同一个层的计算。这样单卡只需要存模型的一部分权重,但代价是卡间通信非常频繁,每层都要做一次all-reduce操作。张量并行对卡间的网络带宽要求极高,一般建议在机器内部使用高速互联的网络,跨机做张量并行容易变成“通信被拖垮”。

流水线并行则是把模型按层切段,切分成多个阶段。由不同设备串行处理后阶段的输入,近似一条流水线。这种方式适合层数非常深的模型,但会产生流水线气泡,让部分设备在一段时间内闲置。目前主流框架通常把这三者混合使用:张量并行的度数设为单机卡数,流水线并行的度数设为机器数量,数据并行则用于扩到更多节点。

3.3 推理阶段优化:KV Cache与量化

推理阶段有两个关键优化点,第一个就是KV Cache。自回归生成的特点是逐个token输出,每次生成新token时,模型都需要用新token的查询向量对所有历史token的Key和Value做注意力计算。如果不做缓存,就需要重新计算一遍所有历史token的K和V,浪费大量算力。KV Cache就是把这些历史计算结果存起来,新的token来了直接复用。它的代价是显存,但换来的收益是解码速度好几倍的提升。我的经验是,长对话场景中KV Cache的开销往往比模型权重更值得关注,很多推理框架的参数里都会有一个KV Cache的分配策略,必须优先规划。

第二个优化点是量化。把FP16的权重压缩成INT8或INT4,可以让模型占用显存大幅下降。量化听起来简单,但要小心多模态模型。实操中常见的问题是,视觉token激活值的分布和文本token的分布不一样,直接沿用文本模型量化校准集的话,图像模态的表现会掉得比较厉害,严重时甚至出现图文错配。如果模型里有较多视觉呈现的内容,建议单独用一批图文混合样本做激活值分布统计,再决定每层的量化精度。

另外我在这两年还发现一个很实用的方案:后面层或者某些头的激活值离群点特别多,整层用INT8量化会掉点,但前面一些层用INT8,最后几层保留FP16,效果比全模型统一量化要好,而且显存只多了一点点。这种混合精度量化的思路在推理部署时会是比较实用的工程技巧。

3.4 多模态推理的特殊工程问题

多模态推理还有一个与纯文本模型不一样的麻烦:图像输入通常作为前缀token,一次性参与注意力计算,图片生成完毕之后,这些图像token实际上就不再更新了。所以可以利用“编码一次”的特性做缓存:把图像特征提前编码好,存成视觉KV Cache,对话时只需要在文本部分做增量推理。这样可以有效减少重复计算。我踩过的坑是,如果推理框架没有把视觉token和文本token分开管理,每一次多轮对话都会重新编码图像,造成不必要的延迟。

另外,分辨率与token数量带来的延迟问题,也必须在真实的线上环境中测试,而不是只看单张图的效果。有一些系统在单图测试时延迟还能接受,一旦batch并发上来,图像数量和token数量相乘之后显存直接爆掉。这种情况需要在视觉模块和语言模型之间做流量控制和预编码队列,避免峰值请求打垮推理节点。

4. 多模态项目实战踩坑记录与排查方法

4.1 对齐效果差的排查方向

如果模型训练完之后,图文匹配能力明显不行,我首先会怀疑训练数据本身,而不是模型结构。很多公开的图文数据集里噪声极大,有些图片配的文字描述与实际内容只有很弱的关联。这类数据拿来做对比学习,会严重拖累对齐效果。排查方法是从训练集里随机抽几千对样本,人工看一遍匹配质量,如果匹配率低于某个阈值,就说明原始数据该做清洗了。

除了数据,温度参数也是一个排查重点。如果对比学习的loss异常低,但下游效果并不好,说明温度调太小了,负样本压力过大,模型把所有样本都推得很远,导致同类样本之间也分得太开。反之如果loss迟迟降不下来,可能是温度偏大,模型不够“敏锐”。我习惯的做法是固定其他参数,单独在验证集上扫几个不同的温度值,比如0.05、0.07、0.1,观察检索指标的变化。

4.2 训练不稳定的原因

多模态大模型训练不稳定,常见现象是loss曲线突然飙升或者出现NaN。这里有几个查证方向:先看学习率是不是太大,尤其是微调阶段,视觉编码器的学习率一般要比文本底座降一个数量级;再看梯度裁剪阈值,多模态训练的梯度范数波动比纯文本更剧烈,阈值设得太紧会降低收敛速度,设得太松又容易震荡;最后看混合精度策略,FP16训练时的梯度下溢和上溢都比BF16严重很多,如果训练平台支持BF16,最好优先选BF16,能解决大量NaN问题。

还有一个容易被忽视的点是数据并行下的loss计算。多模态模型通常会同时计算图像和文本的若干损失项,分布在不同设备上时,如果同步和加权时机不对,每个逻辑批次算出来的loss差异会很大,导致优化器更新方向不稳定。

4.3 推理速度上不去的瓶颈

推理速度变慢,先不要急着怀疑算子优化,先去看序列长度。多模态模型的输入序列里,图像token通常占大头。一张高分辨率图可能产生上千个视觉token,而一段普通用户问题可能只有几十个token。这种情况下的计算耗时主要来自图像token参与注意力计算的开销,而不是模型本身多强。

优化思路包括:适当降低图像输入分辨率、使用更高效的视觉编码器、修剪后处理的冗余令牌,以及对图像token做密度化采样。另外,如果单张图的视觉token数量固定,可以考虑在预处理阶段就把图像特征提前算好,推理时只走文本解码路径。这样线上请求的响应延迟能下降很多,而且不会明显影响任务效果。

4.4 多模态量化掉点排查

量化掉点的原因前面提过,主要是激活值分布不同。排查时先分模态测一遍指标,文本问答没怎么掉,图像理解掉得多,基本就是校准集不匹配。解决方法是混合校准集里加入图像样本,重新统计各层激活的min/max值。

如果混合校准集之后仍然掉点,那就要看看是哪一种算子掉得多。早期视觉Transformer用的GELU激活函数在INT8量化下误差偏大,可以考虑把这几层单独保留FP16。还有多头注意力的softmax结果会受到量化误差影响,如果某个头的注意力分数整体偏小,量化后很容易变成0,出现注意力退化。这个问题的处理方式是尝试改用per-token精度的量化方式,或者在关键层引入额外的缩放因子。

4.5 我的一些额外经验参考

关于多模态的项目,我现在倾向于从一个小场景切入,而不是一上来就挑战视觉问答、视频理解这种大目标。一个文档理解类的Demo,或者一个小规模的图文检索服务,已经足以把架构上各种坑都趟一遍。与其纸上谈兵研究架构对比图表,不如把一个真实场景的端到端流程跑通,你能获得的信息密度远高于刷十篇解读文章。

我自己做过的某个模拟项目里,最难的其实不是模型选型,而是处理海量图像的预处理流程和数据质量清洗。图像分辨率参差不齐,有的对比度极低,有的主体位置严重偏移,这些噪声对模型的影响非常大。模型架构再怎么先进,喂进去一批脏数据,出来的也不可能是漂亮的线上表现。数据先行的观念,在局部多模态项目中可能比模型规模更值得重视。

多模态这条技术路径还在快速演进。今天我们讨论的这些方案,可能过两年就会有更统一的架构替代。但底层那套“把一切输入token化,再在统一的Transformer体系里做序列建模”的思考方式,在未来相当长一段时间里仍然会是主线。希望这篇基于项目实战整理的架构拆解,能帮你少走几步弯路。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进