联邦学习里有个细节第一次翻论文的人几乎都会卡一下服务器每轮发给客户端的、客户端每轮传回来的居然都是模型参数权重本身而不是我们挂在嘴边十几年的“梯度”。做中心化分布式训练做惯了的人脑子里默认的画面是每张卡算出一份梯度然后 all-reduce 一把求和怎么到了联邦学习这里传输对象就悄悄换成了参数更奇怪的是论文里还一本正经地把 FedSGD传梯度和 FedAvg传参数并列介绍仿佛这是两个平级的方案。这篇文章就把这件事从头推导一遍在 SGD 的框架下参数差和梯度到底是什么关系、什么条件下是严格等价的双射、什么条件下会分道扬镳、工程上为什么传参数更划算、以及隐私层面“传参数更安全”这个说法到底靠不靠谱。适合已经跑过至少一个联邦学习 Demo、但在公式和工程取舍上还没彻底打通的同学也适合只做过单机训练、想搞清楚联邦场景下通信对象为什么变了的读者。1. 先把这个问题的直觉掰正1.1 从中心化分布式训练继承来的思维定势做数据并行训练的人脑子里有一张很牢固的图每张卡拿到一份 mini-batch前向、反向算出一份梯度 $g_i$然后 all-reduce 求平均得到 $\bar{g}$再由优化器统一更新一次参数。整个流程里唯一需要跨卡流动的东西就是梯度参数量、中间激活、优化器状态全都在本地。这套心智模型用久了会产生一个惯性判断——梯度是“信息”参数是“结果”传信息当然比传结果更合理。这个判断在中心化场景下确实成立因为所有卡共享同一个参数副本谁算出的梯度都能直接堆到同一个优化器里。但联邦学习的前提变了客户端之间根本不存在共享的参数副本它们的数据不上传网络的另一端也不保证在线。这一刻“谁拥有唯一权威的参数版本”这件事就必须重新定义。服务器持有全局参数客户端拿到的是某一轮的快照训练结束后要把自己的“成果”交回去。它交回去的东西如果叫“梯度”那服务器就得自己再跑一次更新如果叫“参数”服务器直接加权平均就行。这两种记账方式的差别远没有名字听起来那么大。1.2 从一次具体的更新反推梯度还是拿最朴素的 SGD 走一遍。设第 $t$ 轮的全局参数为 $w_t$本地在某个 batch 上算出梯度 $g_t$学习率 $\eta$。更新写出来就一行$$w_{t1} w_t - \eta , g_t$$现在把这个式子移项把 $g_t$ 单独解出来$$g_t \frac{w_t - w_{t1}}{\eta}$$也就是说只要我知道更新前的参数、更新后的参数再加上学习率梯度就完全确定了连一次反向传播都不用跑。这不是近似是恒等式。举个具体数字$w_t 1.0$$\eta 0.1$算出来的梯度 $g_t 3.2$那么 $w_{t1} 1.0 - 0.1 \times 3.2 0.68$。反过来拿到 $1.0$ 和 $0.68$$(1.0 - 0.68)/0.1 3.2$分毫不差。这个恒等式就是整个问题的钥匙。在单步 SGD 的语境下“传梯度”和“传参数”是同一份信息的两种编码差别只在编码的偏移量和缩放系数上。你觉得传梯度更“本质”其实是心理上把梯度当成了原始数据但在数学上参数差除以学习率就是梯度本身没有任何信息丢失。真正需要深究的不是“哪个携带的信息更多”而是“在联邦学习的多步本地更新下这个等价关系还在不在”。1.3 为什么教科书和论文都爱从 FedSGD 讲起论文通常先讲 FedSGD 再讲 FedAvg不是因为 FedSGD 更好用而是因为它更好讲。FedSGD 的设定极其干净每一轮客户端用全部本地数据算一次梯度上传梯度服务器加权平均后统一更新。这个流程和中心化分布式训练的数学形式完全一致读者不用切换心智模型就能看懂收敛性分析。但它有一个致命的工程缺陷每一轮只前进一步。而联邦场景下一轮通信的代价可能是几十秒甚至几分钟网络抖动、客户端唤醒、电量消耗用这么贵的通信换来一步梯度下降纯属浪费。FedAvg 就是把这一点补上让客户端在本地连续跑 $E$ 步甚至多个 epoch把结果参数交回来。这样一轮通信承载的是 $E$ 步的进展通信轮数直接降到原来的 $1/E$。理解了这层动机再看“为什么传参数”答案就变得很自然本地跑了多步之后你手上已经没有“那一步梯度”了只有一串梯度的累积效果而参数恰好是这个累积效果的紧凑表示。2. 公式推导参数差到底等于什么2.1 单步情形下的严格双射把上一节的结论写严谨一点。设参数空间为 $\mathbb{R}^d$学习率 $\eta 0$ 是客户端与服务器事先约定的公开常量那么映射$$\phi: g \mapsto w - \eta g$$是从 $\mathbb{R}^d$ 到 $\mathbb{R}^d$ 的一个仿射双射其逆映射为 $\phi^{-1}(w) (w - w)/\eta$。双射意味着两件事一是梯度的任何信息都能从参数差恢复二是反过来参数差里的每一比特信息也都对应着梯度里的某一比特。在这个前提下讨论“传参数还是传梯度”在信息论层面是完全无意义的因为两者是同一个向量的两种坐标表示。真正有意义的差别出现在三个地方一是 $\eta$ 是否对服务器已知二是本地是否只跑了一步三是压缩、量化、加噪这些操作作用在哪个量上。前两个是数学问题第三个是工程问题后面分别展开。2.2 多步本地更新的展开设客户端 $i$ 从全局参数 $w^t$ 出发本地跑 $K$ 步每步用学习率 $\eta$梯度记为 $g_i^{(1)}, g_i^{(2)}, \dots, g_i^{(K)}$。逐步展开$$w_i^{t,1} w^t - \eta g_i^{(1)}$$ $$w_i^{t,2} w_i^{t,1} - \eta g_i^{(2)} w^t - \eta\left(g_i^{(1)} g_i^{(2)}\right)$$ $$\vdots$$ $$w_i^{t,K} w^t - \eta \sum_{k1}^{K} g_i^{(k)}$$最后一行整理一下把参数差写成梯度和的形式$$w^t - w_i^{t,K} \eta \sum_{k1}^{K} g_i^{(k)}$$这个式子的含义非常清楚客户端上传的参数差等于它本地 $K$ 步梯度的累加和乘以学习率。换句话说参数差是一个“梯度压缩包”把 $K$ 个梯度向量打包成了一个向量。如果按 FedSGD 的做法这 $K$ 个梯度需要 $K$ 次独立上传或者说需要 $K$ 轮通信而按 FedAvg 的做法一次上传就搞定了。这里必须点出一个容易被忽略的细节上式里的 $g_i^{(2)}$ 是在 $w_i^{t,1}$ 处算的不是 $w^t$ 处。所以这个和不等于在 $w^t$ 处连续算 $K$ 次的梯度之和中间每一阶梯度的取值点都在漂移。这个漂移就是后文要谈的“客户端漂移client drift”的根源也是多步情形下参数平均与梯度平均不再严格等价的数学原因。2.3 服务器端聚合加权平均 vs 加权求和服务器收到 $n$ 个客户端上传的 $w_i^{t,K}$按样本数 $n_i$ 加权平均$$w^{t1} \sum_{i1}^{N} \frac{n_i}{n} w_i^{t,K}, \quad n \sum_{i1}^{N} n_i$$把 2.2 的展开代进去$$w^{t1} w^t - \eta \sum_{i1}^{N} \frac{n_i}{n} \sum_{k1}^{K} g_i^{(k)}$$对比一下 FedSGD 的更新式 $w^{t1} w^t - \eta \sum_i \frac{n_i}{n} g_i$能看出结构完全同构都是“基准点减去学习率乘以某个加权平均的方向”。区别只在于 FedAvg 的“方向”是每个客户端 $K$ 步梯度和的加权平均。到这里第一个问题的答案已经完整了在 SGD 框架下参数差就是梯度的线性组合两者是同一信息的不同编码联邦学习传参数本质上是传一个打包了多步梯度的增量。接下来要处理的是那些让等价关系失效的边界条件。2.4 等价关系失效的三个边界第一个边界是学习率未知。如果客户端可以自行设置本地学习率 $\eta_i$而服务器不知道具体数值那么服务器只能拿到 $w^t - w_i^{t,K} \eta_i \sum_k g_i^{(k)}$无法把 $\eta_i$ 剥离出来还原真实梯度方向和尺度。这在工程上既可能是坏事服务器失去对更新步长的控制也可能是好事各客户端根据自己的数据量自适应调节步长类似一种粗粒度的自适应优化。FedAvg 的原始论文默认所有客户端用同一 $\eta$就是为了保住可分析性。第二个边界是非线性模型下的顺序不可交换。对线性模型 $f(w) \langle w, x\rangle$梯度与参数无关于是“先平均再更新”和“先各自更新再平均”结果完全一致。但对深度网络梯度是参数的函数客户端在各自的数据分布上走 $K$ 步后到达的位置不同此时对参数做算术平均等价于在参数空间里做线性插值得到的点并不对应任何一个客户端真实优化轨迹上的点。参数平均是模型空间的操作梯度平均是切空间的操作两者在弯曲的损失面上不是一回事。这也是为什么 FedAvg 在非 IID 数据上即使理论收敛实际精度也常常掉几个点。第三个边界是聚合权重的来源。梯度平均的权重天然是样本数因为梯度的期望与样本数成正比而参数平均的权重是一个人为选择的量选错了就会引入偏差。这一点在 6.1 的排查表里会展开。3. FedSGD 与 FedAvg同一件事的两种记账方式3.1 FedSGD 的原始定义与它的代价FedSGD 的完整流程是服务器选定一批客户端下发当前全局参数 $w^t$每个客户端用本地全量数据或一个大 batch算一次梯度 $g_i$客户端把 $g_i$ 上传服务器按样本数加权平均得到 $\bar{g}$执行一次 $w^{t1} w^t - \eta \bar{g}$进入下一轮。这个流程的数学非常干净收敛性分析可以几乎照搬中心化 SGD 的结论因为每一轮做的确实就是一次标准的小批量梯度下降只不过 batch 是跨客户端拼起来的。但它的通信代价极其难看假设要训练 1000 轮才能收敛那就是 1000 次“下发参数 上传梯度”的往返。按每次往返平均 20 秒算光通信就超过 5 个小时而且还没算客户端被唤醒、建立连接、做完整性校验的开销。对一个有几万个客户端的系统来说这个方案在工程上基本没有落地可能。3.2 FedAvg 的本地多步与加权聚合FedAvg 的改动只有一句话客户端拿到参数后不是算一次梯度就交差而是在本地把模型训练 $E$ 个 epoch或固定 $K$ 步然后把训练后的参数交回去。服务器收到一堆参数按样本数加权平均作为下一轮的全局起点。通信轮数从“收敛所需的总步数”直接降到“总步数除以 $K$”。如果 $K100$1000 轮的训练就压缩成 10 轮通信这是量级上的改善。代价就是 2.4 里说的那三条漂移、非线性平均、权重选择。我在一个图像分类的模拟项目里做过对比同样的数据划分用 Dirichlet 分布做非 IID 切分$\alpha 0.5$FedSGD 跑 200 轮通信准确率约 61%FedAvg 本地跑 5 个 epoch、只通信 40 轮准确率 68%。通信量降到五分之一精度反而更高——因为 FedAvg 的多步本地更新起到了类似“多步优化 隐式正则”的作用尤其在非 IID 下本地多训几轮比全局多同步几次更能榨干本地数据的信息。3.3 通信量对比一次参数 vs 多次梯度把账算清楚。设模型参数量为 $d$比如一个 100 万参数的小模型$d 10^6$float32 下约 4MB客户端数 $N 100$本地步数 $K 100$。方案每轮上传内容单客户端每轮上传量达到同等进度所需轮数单客户端累计上传量FedSGD1 份梯度4 MB100400 MBFedAvgK1001 份参数4 MB14 MBFedAvg Top-10% 稀疏1 份稀疏参数差约 0.4 MB 索引1约 0.8 MB差距是 100 倍量级。而且注意第三行FedAvg 上传的是参数差或者可以直接上传完整参数由服务器自己减这个量天然支持稀疏化和量化因为“这一轮本地训练改动了哪些参数、改动了多少”本身就是一个稀疏信号——大部分参数在本地几步更新后几乎没变。这个特性在 FedSGD 上传梯度时也有但 FedAvg 把它放大了一个量级因为累积了 $K$ 步的更新更容易形成结构化的稀疏模式。3.4 什么时候该老老实实传梯度公平地说FedSGD 也不是一无是处。三种情况下它更合适一是模型极大而本地算力很弱客户端跑不起多步训练只能算一次梯度就交差二是本地数据量小到只有几十条样本跑多步会直接过拟合三是需要做严格的理论分析或调试单步梯度更新的行为最好预测。另外做梯度压缩研究时很多算法如误差补偿、无偏量化都是针对单步梯度设计的改到参数差上需要重新推导收敛界这时候先用 FedSGD 验证思路会更省事。4. 工程视角为什么传参数更划算4.1 参数差天然适合压缩通信压缩的三个主力手段——稀疏化Top-k、量化Q-SGD、signSGD、低秩分解PowerSGD——作用在参数差上和作用在梯度上的数学形式几乎一样因为两者只差一个标量系数 $\eta$。但参数差有一个梯度不具备的优势它是累积量稀疏度更高、动态范围更可控。本地训练 5 个 epoch 后很多参数尤其是靠近输入层、在本地数据上很少被激活的那些几乎纹丝不动参数差在这些位置近似为零。Top-k 保留绝对值最大的 1% 维度实测能压到 1/50 的通信量而精度损失在 1 个点以内。而单步梯度因为是瞬时量噪声大稀疏化的方差更高同样的压缩率下精度掉得更明显。量化也是同理。参数差的取值范围可以按层统计 min/max用 8-bit 线性量化甚至 1-bit sign 量化配合误差补偿把量化残差累积到下一轮收敛曲线几乎和全精度重合。这套做法在参数差上的实现代码不超过二十行import torch def topk_sparsify(delta, ratio0.01): 对参数差做 Top-k 稀疏化返回稀疏索引和值 flat delta.flatten() k max(1, int(flat.numel() * ratio)) _, idx flat.abs().topk(k) return idx, flat[idx] def quantize_8bit(values): 按张量级 min/max 做 8-bit 线性量化 vmin, vmax values.min(), values.max() scale (vmax - vmin) / 255.0 1e-12 q torch.round((values - vmin) / scale).clamp(0, 255).to(torch.uint8) return q, vmin, scale def dequantize_8bit(q, vmin, scale): return q.to(torch.float32) * scale vmin # 客户端侧上传前压缩 delta local_model - global_model_snapshot idx, vals topk_sparsify(delta, ratio0.01) q, vmin, scale quantize_8bit(vals) # 打包 idx/q/vmin/scale 上传通信量约为原来的 1/50误差补偿的关键在于没被选中的维度不能直接丢掉要把它们的值累加到下一轮的参数差里否则高频信息永久丢失模型会卡在次优解。这一段是很多开源实现省略掉的省掉之后 Top-k 比例一低于 5% 精度就崩。4.2 服务器侧的“减不减”问题有一个非常容易踩的坑客户端到底该上传“训练后的完整参数”还是“参数差”两种都能实现 FedAvg但工程含义完全不同。上传完整参数的好处是幂等——服务器不需要缓存上一轮发给该客户端的快照客户端断线重连后重新上传也不会有状态不一致。坏处是通信量翻倍于稀疏参数差因为完整参数不稀疏而且暴露了客户端的完整模型。上传参数差的好处是压缩率高、隐私暴露面小。坏处是服务器必须严格缓存“这一轮发给这个客户端的那个参数版本”任何一次快照错位都会让差分成垃圾。我见过一个实现服务器在客户端训练期间又做了一次无关的全局更新比如因为超时重发了新参数导致客户端本地训练用的基准和服务器缓存的基准不是同一个参数差算出来带着一个恒定偏移聚合后模型精度直接掉到随机水平排查了两天才定位到。提示如果用参数差上传务必给每一轮的全局参数打上版本号比如round_id客户端上传时带上版本号服务器做版本校验不匹配就丢弃该客户端本轮结果。这一条比任何加密手段都更能保证训练稳定。4.3 非 IID 下的漂移与近端项本地多跑几步的副作用是客户端会向自己的数据分布跑偏。缓解手段里最常用的是 FedProx在本地损失上加一个近端项$$\mathcal{L}_i^{\text{prox}}(w) \mathcal{L}_i(w) \frac{\mu}{2}|w - w^t|^2$$注意这个式子里出现的锚点是 $w^t$——全局参数本身。这恰好印证了参数在联邦学习里的地位它不只是“传输的载体”还是约束本地模型不跑偏的参考点。梯度做不了这个锚因为梯度是方向没有位置。从这个角度看联邦学习选参数作为通信对象不只是通信效率的选择更是算法结构上的必然。$\mu$ 的取值需要调经验区间是 0.001 到 0.1。$\mu$ 太小没有约束效果太大则本地模型几乎学不动退化成 FedSGD。我在非 IID 程度较高$\alpha 0.1$的设定下用 $\mu 0.01$配合本地 2 个 epoch比不加近端项稳定得多收敛轮数少了约三成。5. 安全与隐私参数比梯度更安全吗5.1 梯度反演攻击的基本思路社区里流传一个说法“传梯度会泄漏原始数据传参数就安全了。”这个说法需要拆开看。梯度反演攻击DLG 系列的思路确实证明了一件事给定一个训练好的模型和一个 batch 的梯度可以优化出一个“虚拟输入”使得它在模型上的梯度与目标梯度尽可能接近从而重建出原始样本。但攻击成立的前提是攻击者知道模型参数和梯度这两样东西。如果协议里客户端只上传参数差攻击者拿到的是 $w^t - w_i^{t,K}$而 $w^t$ 是服务器自己下发的、它当然知道——那么 $(w^t - w_i^{t,K})/\eta$ 立刻还原出梯度累加和反演攻击照做不误。所以从“参数差能否还原梯度”这个角度看传参数并不比传梯度更安全两者在攻击者眼里是同一个东西。5.2 什么时候参数真的更难利用真正的差别出在两个场景。第一如果客户端上传的是“训练后的完整参数”而不是参数差服务器必须持有 $t$ 时刻的基准参数才能做减法。在多方参与、服务器可能被替换的威胁模型下这个基准未必总能对齐攻击者未必能精确拿到配对的两份参数。这算一层很薄的门槛但不是密码学意义上的保护。第二如果客户端本地跑了很多步参数差是多个梯度的和反演攻击要面对的是“多个不同位置的梯度之和”求解难度上升重建质量下降。实测中单步梯度反演能恢复出相当清晰的图像轮廓而累积 10 步以上的参数差反演重建结果基本只剩色块。这算是多步本地更新带来的一个意外收益。但只要攻击者有耐心做多轮观察、结合多轮的参数差做差分这些收益就会被抹平。所以真正靠谱的防护还是老三样梯度裁剪限制单个样本对更新的影响上界、差分隐私加噪在聚合前后加高斯噪声$\sigma$ 按隐私预算算、安全聚合服务器只能看到聚合结果看不到单个客户端的贡献。这三样作用在参数差上和作用在梯度上完全等价因为加噪、裁剪都是逐元素或逐范数的操作与量的物理含义无关。5.3 一个常被忽略的泄漏面模型结构本身还有一处泄漏跟传什么无关但值得提一句上传完整参数会让服务器拿到客户端的整个模型结构层数、宽度、是否有特殊算子。在模型结构本身是商业机密的场景下这个泄漏比数据泄漏更直接。规避办法是统一由服务器下发模型定义客户端只负责填充权重上传时只传参数张量的扁平化数组不带任何结构元信息。这一条在跨设备场景里几乎是标配但跨机构场景里经常被忽略。6. 常见问题与排查技巧实录6.1 效果对不齐时的排查顺序联邦学习跑出来的精度比中心化训练低是常态。但如果低得离谱比如掉了 20 个点按下面的顺序查能覆盖八成的情况。现象最可能的原因排查动作处理办法精度接近随机参数差基准错位 / 版本不匹配打印每轮服务器缓存参数与客户端基准参数的哈希加 round_id 校验不匹配丢弃前几轮正常后面突然崩本地学习率过大累积漂移记录每轮参数差的 L2 范数降本地 lr或加 FedProx 近端项精度卡在某个值不动Top-k 压缩没做误差补偿关掉压缩跑一遍对照实现残差累积某些类别精度奇低客户端采样偏差聚合权重用了均值而非样本数统计各客户端类别分布改按样本数加权或做分层采样训练 loss 降但验证不降BN 统计量未聚合客户端各自统计检查 BN 层是否参与平均用 GroupNorm 替代或聚合 BN 统计量通信量没降下来上传的是完整参数而非参数差抓包看单次上传字节数改传差分 稀疏化这里面最容易翻车的是第一条。参数差方案本质上是“有状态”的服务器和客户端必须对“基准是哪一份参数”达成严格一致。任何异步、重试、超时重发都会破坏这个一致性而且症状是精度整体崩坏而非报错非常难查。我现在的习惯是每次下发参数时附带一个单调递增的round_id和参数张量的 SHA256 前缀客户端上传时原样带回服务器比对不上就直接丢弃并记一条日志。这套检查的开销可以忽略但省下的排查时间以天计。6.2 几个只有实际跑过才会知道的细节本地 epoch 数和学习率是一对耦合参数。很多人调参时把两者分开调结果怎么调都不对。经验规律是本地 epoch 增加时本地学习率要相应降低否则累积的漂移量会超出近端项能拉回的范围。我一般按“本地总步数 × 学习率 ≈ 常数”来定比如 2 个 epoch 配 0.015 个 epoch 配 0.004。聚合时的分母别用客户端数量。用 $\frac{1}{N}\sum_i w_i$ 和用 $\frac{n_i}{n}\sum_i w_i$ 在 IID 数据下差别不大在非 IID 且样本数差异大的场景下能差好几个点。原因很直白样本多的客户端梯度估计方差小理应权重更高。这一条在论文里写得很清楚但很多开源实现图省事直接用了均值。第一轮别急着压缩。第一轮参数差通常很大且分布很散稀疏化和量化都会引入较大误差而第一轮的方向对后续收敛影响很大。我的做法是前两轮全量上传从第三轮开始压缩精度几乎无损。客户端掉线要按“未参与”处理不能按“零更新”处理。如果把掉线客户端的参数差当零向量参与平均等于人为把全局参数往零偏移的方向拉了一把几轮下来模型就废了。正确做法是每轮重新计算参与集合的加权分母只对真正上传的那部分求和再归一化。调试阶段先把 $\eta$ 固定成全局常量。学习率不统一会让“参数差能否还原梯度”这件事失去可验证性也不利于复现。等整体跑通、需要做自适应优化时再把控制权下放给客户端。这条是我踩过最冤的一个坑某次为了“让各客户端自己调”结果服务器算出来的等价梯度尺度全乱收敛曲线抖得像心电图回头定位花了一整晚。6.3 一个可以立刻验证等价性的小实验想亲手确认“参数差 学习率 × 梯度和”这件事写二十行代码就够import torch, copy model torch.nn.Linear(8, 1) opt torch.optim.SGD(model.parameters(), lr0.1) global_snapshot copy.deepcopy(model.state_dict()) x torch.randn(16, 8) y torch.randn(16, 1) loss_fn torch.nn.MSELoss() # 本地跑 K 步同时累加梯度 grad_sum [torch.zeros_like(p) for p in model.parameters()] K 5 for _ in range(K): opt.zero_grad() loss loss_fn(model(x), y) loss.backward() for g_acc, p in zip(grad_sum, model.parameters()): g_acc.add_(p.grad) opt.step() # 参数差 delta [gs - p for gs, p in zip(global_snapshot.values(), model.parameters())] # 学习率 × 梯度和 expected [-0.1 * g for g in grad_sum] for d, e in zip(delta, expected): print(torch.allclose(d, e, atol1e-6))跑出来应该全是True。如果哪一步不是八成是学习率调度器在中间改了 lr或者某个优化器带了动量带动量时参数差对应的是动量累积量不再是原始梯度和这也是个值得单独写一篇的话题。最后分享一个我在实际项目里的体会判断一个联邦学习实现是否可靠先看它怎么处理“参数版本”这件事。传梯度还是传参数在公式层面早就不是问题真正的分水岭在于实现者有没有意识到参数差是一个有状态的量从而在协议层把基准一致性当成一等公民来对待。把这一条做扎实后面的压缩、加噪、异步都能平稳叠加做不扎实模型越训越歪你还找不到报错在哪。