多模态联邦微调这事儿,做过的朋友应该都有感觉:理想很丰满,现实全是破绽。你手里可能有一批边缘客户端,有的只有图像数据,有的只有文本数据,有的倒是图文都有,但显存只够跑个 batch 16,参数稍微大一点就直接 OOM。把全局多模态模型拉到每个客户端全参微调根本不现实,通信开销、算力天花板、隐私约束一条条卡死你。最近我一直在这个方向上折腾 FediLoRA 这类思路——说白了就是用异构 LoRA 加联邦聚合,专门硬刚“模态缺失 + 异构资源”这两个真实约束。这篇文章想把它背后的设计逻辑、聚合细节和一堆踩坑经验聊透,给准备往这个方向试水的同学一份可以照着走的路线图。
先说清楚这篇文章适合谁。如果你已经跑通过基础联邦学习框架,对 LoRA 有一定了解,想搞清楚“客户端模态不齐时 LoRA 到底该怎么发、怎么收、怎么聚合”,那你来对地方了。如果你只是听说过联邦微调,看到“多模态 + 缺失模态 + 异构秩”这几个词有点懵,也不用慌,我会把前置概念都过一遍。下文的叙述都基于我在本地复现类似架构时的经验,不吹数据,只讲逻辑和坑。
1. 问题场景拆解:多模态联邦微调里的三座大山
1.1 模态缺失:数据层最隐蔽的毒药
多模态数据在真实业务里从来都不是整整齐齐的“图文对”。医疗场景中,影像设备采集到图像,但病历文本可能只有一句话甚至没有;电商场景中,商品图齐全,但评论、描述文本稀疏;工业质检中,传感器信号和现场照片往往有时间差,对不上就是缺失。在联邦环境下,情况更极端:某个客户端可能从部署第一天起就只有摄像头输入,另一个客户端只有历史日志文本。模态缺失不是偶发情况,而是常态。
很多人第一反应是“缺什么就丢掉什么”。但在多模态微调里,这么做很亏。一则单模态样本往往数量庞大,全丢掉等于把冷启动数据全浪费掉;二则任务本身就是多模态语义理解,丢掉一个模态后,模型对另一个模态的表征能力会严重偏离全局目标。更隐蔽的问题是梯度漂移——如果客户端在本地前向时用零向量或占位符去补缺失模态,text encoder 和 vision encoder 会被同时污染,那些“假的输入”会产生稳定的错误梯度,让共享层越训越歪。模态缺失不是“少了一部分数据”,而是“一部分客户端一直在用错误信号训练”,这是第一座大山。
1.2 异构算力与异构数据分布:客户端不是同一个模子刻出来的
联邦学习教科书里默认客户端是 i.i.d. 的同构设备,但现实不是。有的客户端是手机、有的客户端是服务器、有的只是嵌入式盒子。把同一个模型推给它们,本身就是不公平的:大设备嫌模型太轻,小设备直接跑不动。多模态模型更是重灾区,双塔编码器加融合模块,参数量和中间特征图的尺寸都相当可观。
算力差异会直接影响你能用多大的微调方案。同一套 LoRA 配置,显存 24G 的客户端可以设秩 16,显存 8G 的客户端设秩 4 都勉强。如果强行统一用低秩,能力强的客户端表达力受限;强行统一用高秩,弱客户端根本参与不了训练。这就是“异构”二字的核心动因:让每个客户端按照自己的资源边界,选择适合自己的参数规模,而不是反过来被全局配置绑架。数据分布也一样,不同客户端的模态覆盖范围是不同的,有些是双模态齐全,有些是单模态,这种分布差异比传统联邦里的 label skew 更难处理,因为它改变的是模型结构层面的输入通路。
1.3 通信与隐私约束:微调大模型不能太任性
联邦微调还有两把隐形的锁:通信和隐私。全参微调一个几十亿参数的多模态模型,单轮上传的梯度可能上百 MB,在真实网络环境下是灾难。LoRA 的优势就在这:它只训练低秩适配器,更新量极小。但多模态场景里,不同客户端的 LoRA 结构不一致、模态分支不齐全,服务端不能简单地“收了梯度求平均”。
隐私约束要求原始数据不能离开客户端,甚至中间层特征、梯度也最好做最小化暴露。这意味着服务端无法看到“缺失模态对应的原始输入长什么样”,只能依赖客户端上报的更新状态和元信息。设计联邦协议时,你必须把“哪些分支被更新了、这个客户端缺失了什么、用了什么秩”这类信息显式上报,否则服务端聚合时只能瞎猜。这三座大山叠在一起,逼迫你在“模型表达能力、系统异构容忍度、通信效率”之间做平衡,单纯的 FedAvg + 标准 LoRA 根本走不通。
2. 核心思路解析:异构 LoRA 到底在“异”什么
2.1 LoRA 快速回顾:为什么它天生适合联邦微调
LoRA 的思想一句话就能说完:对大模型的权重矩阵 W,不直接更新它,而是学习两个低秩矩阵 B 和 A,用它们的乘积 BA 近似一个增量 ΔW,前向时走 W + BA。B 的尺寸是 d×r,A 的尺寸是 r×k,秩 r 远小于 d 和 k。原始权重保持冻结,推理时可以把 BA 合并回 W,也可以单独保留适配器。
这个特性放到联邦里就是天作之合。第一,客户端不需要给服务端传大模型权重,只要传小适配器,通信量降两个数量级;第二,每个客户端可以自由选择自己的秩 r,只要本地能算得动就行,服务端聚合时再想办法对齐;第三,LoRA 是“可插拔”的,可以把不同任务的适配器叠加到同一个底座上,这对多模态任务非常友好——视觉模态用一套分支、文本模态用另一套分支。用生活类比的话,LoRA 就像给同一个大脑装不同型号的“外挂肌肉”,联邦环境里大家共享大脑,但外挂肌肉的大小和发力方式可以个人定制。
2.2 异构设计的三个维度:秩、分支、结构
“异构 LoRA”不是某一个点特殊,而是三个维度上都做了差异化,需要分开理解:
- 秩异构:不同客户端使用不同的 r 值。资源强的客户端用 r=16,弱的用 r=4,中间档用 r=8。这是最常见、也最容易实现的异构方式,主要难点在聚合时的对齐。
- 分支异构:多模态模型通常包含视觉编码器分支、文本编码器分支,以及跨模态融合分支。客户端缺图像时,视觉分支的 LoRA 就不在本地产生任何更新;缺文本时同理。服务端需要维护一个“分支注册表”,记录每个分支有哪些客户端参与更新。分支异构导致每个通信轮次里,各分支收到的更新数量不一致,聚合时如果还用统一系数,会把参与度低的分支拉向退化方向。
- 结构异构:客户端可以决定 LoRA 插在哪些层。常见做法是插在双塔编码器的 attention q、v 矩阵,以及跨模态交互层。但缺失模态的客户端无法计算跨模态模块,它可能跳过这部分 LoRA,只更新单塔分支;而资源充足的客户端可以额外插入跨模态融合模块。结构异构最难处理,因为它意味着不同客户端上传的 LoRA 参数张量形状可能完全不同。
这三个维度不是互斥的,实际框架里往往同时存在。设计时我建议先固定一个维度做差异化,另外两个维度与全局保持一致,跑通后再逐步放开,否则你排查问题时会疯掉。
2.3 为什么不能直接用 FedAvg 合 LoRA
很多人会想:既然 LoRA 参数量小,那直接用 FedAvg 平均不就行了?我一开始也是这么干的,结果很快就发现不对劲,原因有三。
第一,异构秩的直接平均没有数学意义。r=4 的适配器权重形状是 d×4 和 4×k,r=16 的是 d×16 和 16×k,它们的矩阵在形状层面对不上,强行用 zero-padding 补齐后,小秩客户端的更新会被大秩客户端稀释,而且 padding 出来的零空间会在聚合过程中引入不必要的方向偏差。
第二,分支缺失下的“零更新”问题。某个客户端只有图像数据,没有文本数据,它本地只更新视觉分支的 LoRA。如果服务端在做全局平均时把“文本分支 LoRA”的所有参与客户端更新都加起来,其中那个只有图像数据的客户端贡献的就是零矩阵。零矩阵会导致平均值往零方向收缩。一个没有该模态分支更新的客户端,理论上不应该影响这个分支,但 FedAvg 的朴素实现做不到区分。
第三,LoRA 的低秩子空间在全局平均后可能被破坏。每个客户端学到的 ΔW 都是一个低秩矩阵,低秩矩阵的平均一般不再是同样秩的低秩矩阵,这会削弱适配器的表达力。FedAvg 把多个客户端参数直接平均时,这种几何效应被忽略了,可能在视觉分类这类简单任务上感觉不明显,但多模态任务里特征对齐要求高,很快就会出现“单看每个客户端都学得不错,聚合完一坨稀碎”的情况。
3. 聚合策略与训练流程:缺失模态下的协调细节
3.1 服务端与客户端的职责划分
要解决上面这些问题,首先得分清楚服务端和客户端各自该干什么。服务端不再是一个简单的“求平均”角色,而是一个调度中枢,它至少要维护四类元信息:全局骨干模型的地址、各模态分支的全局 LoRA 版本、每个客户端的模态覆盖报告、每个客户端下一轮可用的秩档位。客户端则需要在上报适配器时,附带一份“更新清单”,说清楚自己更新了哪些分支、用了什么秩、本地样本量是多少、以及哪些分支因为模态缺失没有参与更新。
这套协议设计非常关键,因为它决定了聚合算法能拿到什么样的信息。我在复现过程中最大的体会是:不要节省元信息字段。宁可上报内容冗余一点,也不要等出了偏差再去猜原因。比如“这个客户端文本分支的 LoRA 为什么是零矩阵”,若是没有显式标记缺失模态而是靠零矩阵去推断,你永远不会知道它是真的没数据,还是只是本地训练时梯度恰好为零。
3.2 缺失模态的本地前向处理
客户端本地训练时,缺失模态怎么过前向网络,是整个方案中最容易“草率开局”的地方。我试过几种处理方式,各自代价完全不同。
第一种是零向量补位。把缺失模态的输入张量置零,让双塔照样前向。听起来简单,实际效果非常差。文本编码器会对零向量产生有偏的 token embedding 更新,视觉编码器也会因为全零图像 patch 产生大量垃圾梯度。如果你同时让缺失模态分支的 LoRA 参与训练,这几轮之后那分支就废了。零向量补位可以用,但前提是你必须把缺失模态对应的 LoRA 冻结,不让它的梯度反传。
第二种是模态掩码嵌入。设计一个可学习的“缺失标记”向量,作为缺失模态的输入占位符。这比零向量好一些,因为它给了模型一个“当前这个模态不存在”的显式信号,而不是让模型错误地以为这就是正常的零值输入。但可学习占位符也需要更新,而更新它所需要的梯度来自于其他非缺失模态的损失,这相当于用单模态信号去反向训练一个多模态占位符,容易过拟合本地任务。
第三种是我更推荐的方式:缺失模态时完全跳过该分支的上半段计算,只更新可见模态分支和对应的单模态辅助头。举个例子,某客户端只有图片,前向时只跑 vision encoder,文本塔不参与,损失函数改为“视觉单模态分类损失 + 特征空间一致性损失”。这样梯度来源是干净的,不会污染缺失模态分支。代价是跨模态交互层在这个客户端上没有更新,需要在服务端聚合时依靠其他双模态客户端来贡献。
实操上,我建议的默认配置是:缺失模态分支的 LoRA 一律不参与本地更新,缺失模态输入用零向量补位但不更新相应分支,同时给本地损失加一个小权重的单模态辅助损失。辅助损失的作用是保证可见模态的表征不会因为缺乏跨模态对齐信号而退化。
3.3 异构秩的对齐聚合策略
服务端聚合是 FediLoRA 这类方案真正的硬骨头。异构秩怎么合?我踩过几种方案,值得摆出来对比。
最简单的方案是 Pad & Average。把所有客户端适配器矩阵沿秩维度零填充到全局最大值,然后加权平均。这种方法代码最容易写,但问题也很明显:大秩客户端的更新量天然占据更多有效维度,小秩客户端的更新被稀释;零填充的空间参与平均后,会逐渐产生一个“虚假骨架”,抑制后续训练中的低秩方向。我在实验中观察到,这种方案下小秩客户端的收敛速度明显变慢,而且聚合出来的模型在迁移到新任务时表达能力打折。
更合理的方式是按照全局秩先做投影对齐。每个客户端上传适配器后,服务端先把它还原成完整增量矩阵 ΔW = B·A 的低秩表现,再将其投影到一个公共子空间。公共子空间的维度可以取所有客户端秩的中间值,或者取资源最丰富客户端的秩。投影过程可以使用随机正交基,也可以做一次 SVD 后取主成分。SVD 的开销会随参数量增加,以我实测经验,在 r=8~16 这个区间,双塔模型的服务端 SVD 耗时还是可以接受的,但如果你有几千个客户端,就必须做近似处理。
更工程化的做法是分组聚合。把客户端按秩档位分成几个组,组内做普通加权平均,组间再做二次聚合,二次聚合时用“秩贡献系数”加权——秩更高的客户端在最终模型中占更大贡献,但小秩组的稳定性权重也保留一部分。这个做法不会破坏低秩子空间,计算量可控,而且实现难度不高。如果你打算后续做增量客户端扩容,分组聚合还有个好处:新客户端加进来时只需要匹配一个秩档位,不用重算所有聚合。
无论用哪种对齐方式,加权系数都必须认真设计。传统 FedAvg 按样本量加权,在这里不够用。因为模态缺失导致各分支的参与客户端集合不一样,应该按“有效更新量”加权:某个分支只有 3 个客户端更新了,那这 3 个客户端在该分支上的权重就应按它们本地的数据量重新归一化,而不是沿用全局样本量权重。否则,一个客户端样本量很高但没更新某个分支,它的零矩阵会以很高权重参与该分支平均,直接把该分支拉向零。
3.4 损失函数与跨模态一致性约束
缺少模态时的训练信号往往不足,单靠下游任务损失容易让可见模态分支产生退化。我建议在本地损失上加一个对齐正则项。基本形式是:
L = L_task + λ·L_align
其中 L_task 是本地数据对应的下游任务损失,L_align 是跨模态一致性损失。对于双模态齐全的客户端,L_align 可以直接用两个模态的特征相似度对比损失;对于单模态客户端,可以用一个预计算好的模态锚点做蒸馏,比如让本地可见模态的输出逼近服务端下发时的全局多模态特征分布。这个锚点特征可以是服务端在每轮聚合后从某个代理数据上算好并随模型一同下发的。这样单模态客户端也能获得跨模态对齐的“影子信号”,不至于完全和双模态客户端的特征空间脱节。
λ 的取值我建议初期设成 0.1 到 0.3,然后按验证集表现调。λ 太大会压过下游任务的真实监督信号,导致任务指标下降;太小又等于没有对齐,特征空间在客户端之间各自为政。这个参数对多模态结构比较敏感,换一个骨干网络后最好重新验证,不要照搬别人的配置。
4. 从零到跑通:参数设置、数据构造与避坑清单
4.1 如何构造一个带模态缺失的联邦实验环境
复现类 FediLoRA 的思路时,第一步不是改模型代码,而是先把缺失模态的联邦数据环境搭出来。我推荐的做法是:选一个公开图文多模态数据集,然后按三种缺失模式切分客户端,避免只用一种缺失方式导致结论失真。
第一种是“完全单模态”模式,直接把客户端按模态切分,部分客户端只有图像,部分只有文本。第二种是“随机缺失”模式,每个客户端按比例随机丢掉图像或文本,模拟现实中偶发采集丢失。第三种是“模态偏移”模式,不同客户端的主导模态不同,比如 A 类客户端 90% 有图缺文,B 类客户端 90% 有文缺图。这三种模式分别考验框架对模态缺失率、缺失均衡性、参与度不均的容忍度。我都建议跑通之后再考虑更复杂的部分缺失情况。
模态缺失比例建议从 20% 起步,最高到 70%。低于 20% 时,普通方案可能也不会崩,差异不明显;高于 70%,缺失模态分支的聚合几乎退化为极少数客户端的单点更新,没太大分析价值。我测试时最常用的配置是 30% 与 50% 两档,足够暴露问题。
4.2 关键参数设置:默认配置与调整思路
参数配置直接决定你能不能在可接受的时间内看到收敛。以下是我在复现类似架构时认为比较稳妥的默认起点:
| 配置项 | 建议范围 | 备注 |
|---|---|---|
| 全局秩 r_global | 8~16 | 需要兼顾小秩客户端的投影空间 |
| 客户端秩 r_client | 4~16 分档 | 资源弱用 4,强用 16,中间用 8 |
| LoRA alpha | r 的 1~2 倍 | 一般取 alpha=2r,后续微调缩放 |
| LoRA 插入层 | 双塔编码器的 attention q、v | 先不插融合层,稳定后再加 |
| 本地 epoch | 1~2 | 联邦环境避免本地过拟合 |
| 客户端数量 | 20~50 | 太少会导致分支参与度统计不稳 |
| 每轮采样客户端比例 | 30%~50% | 保证每个分支每轮都有更新 |
| 服务端聚合对齐方式 | 分组聚合或子空间投影 | 不要用纯 Pad & Average |
| 优化器与学习率 | AdamW,1e-4~5e-4 | LoRA 适配器的学习率可以稍高 |
这个表不是金科玉律,但它是踩过坑之后总结出来的安全区。等你发现自己对默认配置的偏差很有把握了,再放开调不迟。
4.3 复现时最容易踩的五个坑
第一个坑:小秩客户端的适配器在聚合后“消失”。原因是零填充平均让它们的更新权重等价于零,最后聚合结果被大秩客户端主导。解决思路是分组聚合,或者给子空间投影时做一个最小秩保护——小于全局秩的客户端贡献一个缩放因子,保证它不会完全被淹没。
第二个坑:缺失模态分支被零稀释。某个分支只有两三个客户端有更新,服务端却用全局总样本量做分母,导致这个分支每轮的有效更新都被摊薄到几乎为零。解决思路是按有效更新量重新归一化权重,缺失模态分支的聚合只看参与客户端的样本量。
第三个坑:本地验证指标好看,全局测试指标崩盘。原因多半是客户端本地 epoch 过多,加上单模态辅助头过拟合了本地模态分布。联邦微调的本地 epoch 不是越高越好,多模态场景尤其敏感,我建议把本地 epoch 控制在 1,最多 2,然后靠增大参与率来提升全局效果。
第四个坑:只评估完整模态测试集。如果测试集只有成对的图文数据,你根本无法评估单模态客户端的表现是否退化。很多方案的提升在全文对测试集上看着不错,单看缺失模态客户端却已经废掉了。评估指标至少拆成三块:完整模态测试集、仅图像测试集、仅文本测试集。
第五个坑:LayerNorm 或 BatchNorm 的统计量没处理。冻结骨干模型时,BatchNorm 里的 running mean 和 running variance 通常也应冻结,或者对缺失模态客户端使用统一的统计量。否则本地前向时这些统计量会持续更新,全局聚合时引发统计量偏移,多模态模型里的视觉和文本两个 tower 各自统计量漂移方向还不一致,问题更难排查。
4.4 评估指标怎么设才不被带偏
多模态联邦微调很容易被一个漂亮的平均数字骗到。我强烈建议把评估表拆成下面几个维度:完整模态下游任务准确率(或检索指标)、单模态图像任务指标、单模态文本任务指标、通信量统计(平均每轮上传的 LoRA 参数量)、以及各客户端秩利用率。通信量统计能帮你论证异构 LoRA 相对全参微调的优势到底有多大,这个数据在汇报和方案评审时很关键。
如果你是在做对比实验,建议基线至少设三组:一是全局同秩 LoRA + FedAvg,二是全参微调 + 模态补零(现实世界最常见的土办法),三是异构 LoRA 但缺失分支也参与聚合的版本。通过这三个基线,你能分离出“异构秩”和“分支级聚合”各自带来的收益。在我自己的对照中,最明显的提升往往不是来自总指标,而是来自单模态客户端指标和训练稳定性的改善。
5. 基于个人经验的一些补充和扩展方向
如果让我给后来人一个深入尝试的方向,我觉得最值得探索的是“共享基石 + 模态专家”的分层结构,而不是把所有 LoRA 都放在同一个骨架里。具体来说,用一部分 LoRA 参数作为所有模态公用的底层表示,另一部分参数作为模态专属专家。缺失模态时,公共层照样更新,模态专家选择性激活。这个结构比单纯的分支异构更容易做聚合,因为公共层天然具有全参与度。
还有一个我比较看好的方向是客户端动态档案的引入。服务端根据客户端历史更新参与度、模态覆盖比例、算力评级,自动决定给它下发哪一档秩、要不要多给一个跨模态辅助分支。这种动态调度能降低人工调参成本,但也意味着你需要先建立一个可靠的状态追踪系统,不能每次分发都拍脑袋。
我自己从零把类似架构跑通之后,最真实的感受有两个:一是这个方案的收益不是体现在某个单一指标暴增上,而是体现在“所有客户端都能以可接受的方式参与多模态训练”这个系统层面,二是日志系统设计得越细,后面省的时间越多。每个客户端上报的分支更新列表、秩、有效更新量这些字段,不要怕多,等你排查问题时就知道多值钱了。
如果你现在正准备在这个方向上起一个新实验,我的建议是:先别碰大模型,用小规模的视觉-语言双塔骨干把分支级聚合逻辑验证完,再迁移到真正的大模型底座上百试不厌。还有一点,本地训练时缺失模态分支的 LoRA 一定不要偷偷更新,哪怕你用零向量补位,这一步错了,后面所有结论都会失真。祝实验顺利。