先说一个我遇到过的真实场景:带新人做横向联邦的原型,第一版代码跑完,一位同学盯着聚合函数看了半天,问我——客户端本地明明算的是梯度,为什么上传给服务器的是更新完的模型参数?服务器更奇怪,直接把一堆模型参数求个加权平均就当成新一轮全局模型,这凭什么是对的?这个问题问得特别好,因为它正好卡在联邦学习里最容易被跳过的一步推导上:模型参数和模型梯度这两样东西,在什么条件下是同一份信息的两种写法,又在什么条件下会分道扬镳。
很多人对这块的理解停留在“参数平均听起来比较合理”这种直觉层面。但只要动笔把公式写开,你会发现结论比想象中干净:在本地只走一步、且学习率统一的条件下,传参数和传梯度是严格的代数等价,连近似都算不上;而工业界之所以普遍选择传参数,真正的原因不在等价性,而在等价性失效之后依然能换来通信轮数的大幅下降。这篇文章我打算把这条线从头到尾推一遍,包括泰勒展开下的偏差项、二次目标上的闭式解、以及我在实际调参时踩过的几个坑。适合已经能看懂 FedAvg 伪代码、但对其中的数学动机还有疑问的读者,也适合想给自己的联邦项目做方案选型的同学。
1. 把问题钉死在公式上:传参数和传梯度到底差在哪
争论“传参数还是传梯度”,如果只停留在文字层面,很容易变成各说各话。最有效的办法是把两份伪代码并排放在一起,看它们唯一的分歧点落在哪一行。
1.1 两份伪代码的唯一分歧
先定义符号。假设有 K 个客户端,第 k 个客户端持有 n_k 条样本,总样本数 n = Σ n_k,聚合权重 p_k = n_k / n。全局目标是
F(θ) = Σ_k p_k F_k(θ)
其中 F_k 是第 k 个客户端上的经验风险。梯度版(FedSGD)的每一轮是这样:
# FedSGD:服务器收梯度 for t in range(T): selected = sample_clients(K) grads = [] for k in selected: g_k = compute_gradient(model, data_k) # 本地全量算一次梯度 grads.append((p_k, g_k)) model = model - lr * weighted_sum(grads) # 服务器做一次梯度下降参数版(FedAvg)的每一轮是这样:
# FedAvg:服务器收参数 for t in range(T): selected = sample_clients(K) models = [] for k in selected: local = copy(model) for epoch in range(E): for batch in data_k: local = local - lr * grad(local, batch) models.append((p_k, local)) model = weighted_sum(models) # 服务器对参数做加权平均差异就落在那两行:一边把g_k加权求和后乘lr再减,另一边把local加权求和直接赋给model。前者是在梯度空间做线性组合,后者是在参数空间做线性组合。看上去完全不同的两件事,为什么在很多场合结果一样?
1.2 本地只走一步时是严格等价,不是近似
把 FedAvg 的本地更新推到极端:设本地只跑一个 epoch,且这个 epoch 只有一个 batch,正好覆盖该客户端的全部数据。这时候本地更新退化成一步完整梯度下降:
θ_k = θ^t - η ∇F_k(θ^t)
服务器聚合:
θ^{t+1} = Σ_k p_k θ_k = Σ_k p_k (θ^t - η ∇F_k(θ^t)) = θ^t - η Σ_k p_k ∇F_k(θ^t)
注意到 Σ_k p_k ∇F_k(θ^t) 恰好就是全局梯度 ∇F(θ^t),于是
θ^{t+1} = θ^t - η ∇F(θ^t)
这跟 FedSGD 服务器收到梯度后做的那一步,逐字逐项完全一致。也就是说,在 E=1 的退化情形下,“参数加权平均”和“梯度加权平均”是同一个式子的两种写法。
关键在于这是一个仿射映射。服务器手里已经有 θ^t,收到 θ_k 之后完全可以反算出 g_k = (θ^t − θ_k) / η;反过来,客户端把 g_k 传上去,服务器也能算出 θ_k = θ^t − η g_k。两边携带的信息量一模一样,维度也都是 d。所以在这个条件下讨论“传哪个更好”根本没有意义,它们就是同一件事。
真正值得讨论的是:既然 E=1 时两者等价,那为什么大家还要费劲在本地多跑几个 epoch?答案藏在下面这一小节。
1.3 等价性在哪儿断掉:本地步数 E 和学习率 η
当 E > 1,客户端本地会连续走 E 步:
θ_k^{(0)} = θ^t,θ_k^{(j+1)} = θ_k^{(j)} - η ∇F_k(θ_k^{(j)})
展开写就是
θ_k^{(E)} = θ^t - η Σ_{j=0}^{E-1} ∇F_k(θ_k^{(j)})
定义伪梯度(pseudo-gradient)
Δ̃_k = (θ^t - θ_k^{(E)}) / η = Σ_{j=0}^{E-1} ∇F_k(θ_k^{(j)})
这个式子是整个联邦学习里我最喜欢的一个恒等式,因为它一次性解释了两件事。
第一件事:上传参数本质上是一种压缩。客户端本来可以在本地算 E 个梯度,把 E×d 个浮点数全部发上去,通信量是 E·d;但它选择把这 E 个梯度“累加”成一个 d 维的位移向量,通信量降到 d。压缩率正好是 E。这也解释了为什么本地步数越多、通信越省——代价是压缩过程不可逆,服务器没法还原出每一步的梯度。
第二件事:伪梯度不等于真实梯度。Δ̃_k 里的每一项 ∇F_k(θ_k^{(j)}) 是在不同的点上求的,而服务器真正想要的 Σ p_k ∇F_k(θ^t) 是在同一个基准点 θ^t 上求的。E 越大、η 越大、各客户端数据分布差异越大,这两者的偏离就越大。这个偏离就是文献里说的client drift,是 FedAvg 收敛分析里绕不开的核心量。
所以我一般会跟团队里这么解释:传参数字面上是“少传了 E−1 份梯度”,实质上是“用位移的一次累加,替换掉了梯度的逐步上报”。省下来的通信量,就是等价性被打破的那部分。
2. 泰勒展开视角:参数平均到底在优化什么
理解了“等价性在 E>1 处断掉”,下一个问题自然是:断得有多严重?偏差是零阶、一阶还是二阶的?这一步不推导清楚,调参时就没法判断该动 η 还是该动 E。
2.1 把偏差项展开到二阶
设服务器这一轮产生的总位移为 δ̄ = θ^{t+1} − θ^t = Σ_k p_k (θ_k^{(E)} − θ^t)。代入伪梯度:
δ̄ = -η Σ_k p_k Σ_{j=0}^{E-1} ∇F_k(θ_k^{(j)})
把 ∇F_k(θ_k^{(j)}) 在基准点 θ^t 处做一阶泰勒展开,记 H_k = ∇²F_k(θ^t):
∇F_k(θ_k^{(j)}) ≈ ∇F_k(θ^t) + H_k (θ_k^{(j)} − θ^t)
而中间那次位移又可以近似为 θ_k^{(j)} − θ^t ≈ −jη ∇F_k(θ^t)。代回去:
δ̄ ≈ -η Σ_k p_k Σ_j [∇F_k(θ^t) − jη H_k ∇F_k(θ^t)]
把两段分开,第一段是理想项,第二段是偏差项:
δ̄ ≈ −Eη ∇F(θ^t) + η² · E(E−1)/2 · Σ_k p_k H_k ∇F_k(θ^t)
这个式子的信息量很大,我逐条拆:
- E = 1 时偏差项系数为 0。E(E−1)/2 = 0,偏差自动消失,这和上一节的严格等价推导对上了。
- 偏差项与 η² 成正比,所以它是二阶小量。这也解释了为什么把学习率砍一半,偏差就掉到四分之一左右,比线性下降快得多。
- 偏差项与 E(E−1)/2 成正比,E 从 2 涨到 4,系数从 1 涨到 6,涨了六倍。这是本地步数不能无限加大的根本原因。
- 偏差项的形式是 Σ p_k H_k ∇F_k(θ^t),注意它既不是 Σ p_k H_k 乘 Σ p_k ∇F_k,也不是任何单个量的函数,而是一个“每个客户端的 Hessian 乘自己的梯度,再按权重求和”的混合项。只有当所有客户端完全同构(H_k 相同、∇F_k 相同)时,它才退化成 H∇F,此时偏差只改变有效步长、不改变方向。
最后这一条特别值得说。同构情形下,偏差项正比于 H∇F,和理想项 −Eη∇F 方向一致,所以相当于把学习率做了一个修正——从 Eη 变成 Eη − η²E(E−1)/2 乘以某个量。这种情况下参数平均不会把模型带到错误的地方,只是等效学习率变了。这是很多人第一次做同构实验时觉得“FedAvg 好像和 FedSGD 差不多”的原因,因为同构数据把方向性偏差屏蔽掉了。
2.2 异构数据下的不动点偏移
为了让上面的结论落地,我拿一个一维二次目标做闭式计算。这种例子在论文里常见,但很少有人把 E 的影响算出具体数字来。
设第 k 个客户端的本地目标为
F_k(θ) = h_k (θ − a_k)² / 2
这里 h_k 是曲率(对应特征分布的分散程度),a_k 是该客户端的最优解(对应标签分布的中心)。全局目标 F = Σ p_k F_k,全局最优解是
θ* = (Σ p_k h_k a_k) / (Σ p_k h_k)
本地做 E 步梯度下降,学习率 η,可以精确解出
θ_k^{(E)} = a_k + (1 − η h_k)^E (θ − a_k)
服务器聚合 θ' = Σ p_k θ_k^{(E)},整理成不动点方程 θ = Aθ + B 的形式:
A = Σ_k p_k (1 − η h_k)^E,B = Σ_k p_k [1 − (1 − η h_k)^E] a_k
收敛后的不动点为 θ_∞ = B / (1 − A)。
现在代入一组具体数字:p_1 = p_2 = 0.5,a_1 = 0,a_2 = 10,h_1 = 1,h_2 = 4,η = 0.1。
- E = 1:A = 0.5×0.9 + 0.5×0.6 = 0.75,B = 0.5×0.1×0 + 0.5×0.4×10 = 2,θ_∞ = 2 / 0.25 =8.00,正好等于 θ* = (0.5×1×0 + 0.5×4×10) / (0.5×1 + 0.5×4) = 20 / 2.5 = 8。
- E = 2:A = 0.5×0.81 + 0.5×0.36 = 0.585,B = 0.5×0.19×0 + 0.5×0.64×10 = 3.2,θ_∞ = 3.2 / 0.415 ≈7.71,偏移约 0.29。
- E = 4:A = 0.5×0.6561 + 0.5×0.1296 ≈ 0.393,B = 0.5×0.8704×10 ≈ 4.35,θ_∞ = 4.35 / 0.607 ≈7.17,偏移扩大到约 0.83。
再把学习率降到 η = 0.05、E 保持 2:A = 0.5×0.9025 + 0.5×0.64 ≈ 0.771,B = 0.5×0.36×10 = 1.8,θ_∞ = 1.8 / 0.229 ≈7.87,偏移缩到 0.13 左右。
这组数字非常直观地验证了前面的二阶结论:E 翻四倍,偏移放大约三倍;η 减半,偏移缩到原来的一半以下。同时也说明一个反直觉的点——真正制造偏差的是 h_k 的差异(曲率异构),而不是 a_k 的差异(最优解异构)。在这个例子里,即便 a_1 和 a_2 差出 10,只要 h_1 = h_2,E 取多大都不会偏移。所以“各客户端标签分布不均衡”本身并不一定是漂移的主因,特征分布差异导致本地更新方向上的“步长不一致”才更致命。这个区分在做数据切分实验时特别有用。
2.3 收敛性还剩下什么保证
有人看到偏差项会担心:既然参数平均是有偏的,那 FedAvg 还能收敛吗?答案是能,但收敛目标要改口。在 μ-强凸、L-光滑、本地梯度方差有界的标准假设下,FedAvg 的收敛结论通常是这样的形式:经过 T 轮,梯度范数的平方被一个与 1/T 成正比的项,加上一个与 η、E 相关的邻域半径所界定。也就是说,它收敛到一个最优解附近的小球里,球的半径随 η 和 E 增大而增大。
这一点在实践中的含义很明确:FedAvg 不是“收敛到全局最优”的算法,而是“收敛到全局最优的邻域”的算法。你要控制的是这个邻域的半径,手段就是 η、E 和本地 batch 大小。如果业务上要求高精度,标准的补救方式有几种——把 E 调小、把 η 调小并配合学习率衰减、加近端项(本地目标里附加 μ/2·‖θ − θ^t‖² 把本地模型往起点拉)、或者用控制变量法对伪梯度做修正。最后一种的代价是通信量翻倍,一般只在异构特别严重时才值得。
3. 通信与工程视角:工业界为什么选参数
到这里数学账基本算清了。但如果只从数学出发,会得出一个奇怪的结论:既然 E=1 时两者等价、E>1 时传参数还有偏差,那为什么不干脆传梯度,让服务器精确地做全局聚合?要回答这个,得把视角从“一轮通信里传了什么”切换到“整个训练过程一共通信了多少次”。
3.1 真正的瓶颈是通信轮数,不是单次通信量
这是个特别容易混淆的地方。传梯度和传参数,单次通信的数据量都是 d 维浮点数,完全一样。所以“传参数更省带宽”这句话,如果指单次,那是错的。它省的是轮数。
比较一下总通信量。设每个客户端每轮本地计算的时间成本可以忽略(实际上远小于一次网络往返),设要在全局收敛需要 T 轮,每轮参与 K 个客户端,上行加下行共 2K d 个数值:
- 梯度方案(E=1):每轮通信 2Kd,一轮推进一次全局梯度下降。
- 参数方案(E=E₀):每轮通信 2Kd,一轮推进 E₀ 次本地梯度下降的等效进度。
在理想情况(无偏差)下,达到同样的精度,参数方案需要的轮数大约是梯度方案的 1/E₀。也就是说,每单位通信量所覆盖的本地计算量提高了 E₀ 倍。这是 FedAvg 在 2016 年被提出来时最核心的卖点,当时的实验里 E 取到 20 甚至更多,通信轮数直接掉了一个数量级。
当然这不是免费的。E 越大,前面推的偏差项越大,实际需要的轮数不会真的降到 1/E₀,而且会收敛到更高的邻域。所以中间有个最优点。我的经验是:在中等异构的数据切分下,E 取 5 到 20 之间通常能吃到大部分通信收益,再往上增加本地步数,收敛曲线就开始明显变慢,甚至震荡。
3.2 无状态聚合带来的工程简化
数学之外的第二个理由,是参数聚合在工程上几乎不带来状态负担。
服务器在做梯度聚合时,本质上是在用“某个基准点”上的方向。如果所有客户端都同步、每一轮都准时返回,这没问题。但现实里总有慢节点、掉线、重连。一旦某次梯度是基于三轮之前的 θ 算出来的,这个梯度就是过期的,直接聚合进来会把模型往回拽。你要么等它(拖慢整轮),要么丢弃(浪费它的本地计算),要么用带权方式软化(复杂化聚合逻辑)。
参数聚合规避了这个问题的一半。服务器收到客户端上传的 θ_k,可以直接比较它和当前 θ^t 的距离,甚至可以直接对“落后的模型”做一个局部重加权;也可以设计成半异步,服务器维护一个全局模型,每收到一个客户端就做一次带衰减的凸组合。这些策略都不需要客户端维护任何跨轮的额外状态。
客户端侧也是同理。传梯度意味着客户端要保证自己算梯度的那个模型和服务器当前的模型完全一致,需要在协议里显式携带版本号并且做校验;传参数则是“你传你算出来的东西,我来决定怎么融合”,耦合度低得多。这也是为什么很多落地实现里,客户端上传的其实既不是纯梯度也不是纯参数,而是两者之差Δ_k = θ_k − θ^t,也就是前面定义的那个带符号的伪梯度。它在语义上偏向“梯度”,但不需要服务器提供基准点就能算出来,所以在工程上比裸梯度更好管理。
3.3 压缩、量化、安全聚合的兼容性
第三个理由和压缩有关。模型参数量动辄千万级,通信优化是绕不开的一环,常见手段有量化、稀疏化、低秩分解。这些手段在参数和梯度上都可以用,但行为不同。
对梯度做无偏压缩时,压缩误差会在每一轮独立出现,累积起来影响收敛;而参数方案因为本地压缩率是 E,本身就相当于已经做了一次“时间维度”的结构化压缩,再叠加空间维度压缩的收益递增效果会更好。另外,参数方案支持一种梯度方案很难做的优化——增量更新。客户端上传的是它相对于上一轮全局模型的差量,服务器可以用保存的上一轮参数做差量还原,从而实现稀疏化后只传非零位置的坐标和值。
安全聚合方面,两者都是同维向量,本质上都支持多方加密后求和,技术上没有明显优劣。差分隐私方面,噪声加在参数上还是梯度上,因为二者只差一个仿射变换,敏感度会随学习率缩放,实现时需要相应调整噪声尺度。这一点我在第一版代码里就搞错过,把梯度版本标定好的噪声参数直接搬到参数版本,结果精度掉得莫名其妙。
4. 实操:把等价性做成能跑的对照实验
纸上推完,我一般会要求在组里做一个最小对照实验,把“E=1 时严格等价”和“E>1 时出现偏差”这两条结论在代码上复现出来。这个实验成本极低,但对理解算法非常有用。
4.1 实验设计与代码骨架
用最朴素的做法:造一个二维的合成数据集,故意做成两个客户端的特征分布不一样(比如一个客户端的数据点集中在左半边,另一个在右半边),用一维线性回归拟合。这种数据下梯度、Hessian 都可以手算,误差曲线一眼能看出好坏。
核心是把两种聚合写成两个可以互相切换的函数:
import torch def aggregate_params(glob, locals_, weights): """参数空间加权平均""" new = {} for k in glob.keys(): new[k] = sum(w * loc[k] for w, loc in zip(weights, locals_)) return new def aggregate_grads(glob, locals_, weights, lr): """梯度空间加权平均后做一步下降""" new = {} for k in glob.keys(): g = sum(w * (glob[k] - loc[k]) / lr for w, loc in zip(weights, locals_)) new[k] = glob[k] - lr * g return new注意aggregate_grads里我是用(glob - loc) / lr反算出伪梯度的,因为本地也是用 SGD 更新的,这样两种聚合在数学上就是同一个东西的两条代码路径。跑完你会看到 E=1 时两条曲线逐位重合,浮点误差都在 1e-7 量级以内。这一步非常值得做,它能让团队里所有人对“严格等价”这件事有肌肉记忆。
然后把 E 调到 5、10、20,两条曲线就开始分叉了。分叉的速度和数据异构程度正相关——这也是我建议在实验里做三组数据切分(同构、轻度异构、重度异构)的原因。
4.2 参数选择:学习率、本地步数、批大小
这三者的耦合关系是我踩坑最多的地方。展开说几个点。
学习率和 E 不能分开调。根据前面推的偏差项,偏差 ∝ η²·E(E−1)/2,而每轮的进度 ∝ Eη。所以如果你把 E 翻倍,理想情况下 ξ = Eη 不变能让进度持平,但实际上偏差会变成 4 倍(因为 η² E² 大致不变,但 E(E−1) 增长更快)。我的做法是固定一个“每轮总步长” ξ = Eη,然后在这个约束下扫 E,看收敛曲线哪个最陡。
本地批大小主要影响两件事:计算效率和梯度噪声。批太小,本地 SGD 方向抖动大,E 步累积下来的漂移会更大;批太大,客户端本地计算开销上来但方向更接近真实本地梯度。在小样本客户端上(比如只有几百条数据),我见过有人把 batch 设得比样本数还大,实际每个 epoch 只走一步,这时候 E 就等于有效步数,跟直接设 E 没区别,但代码里看不出来,很容易让人误判。
还有一个细节:很多框架把“本地 epoch”和“本地步数”混着用。E 的定义在不同论文里也不一致——有的指 epoch,有的指 SGD 步数。做对照实验时一定要在自己代码里统一口径,否则看到“E=5 反而比 E=20 好”这种结论时,你都不知道是算法问题还是计数问题。
4.3 聚合权重的坑
权重 p_k 怎么取,看似是个细节,其实对结果影响很大。理论推导里常用 p_k = n_k / n,也就是按样本数加权,这对应“全局目标是各客户端风险的样本加权平均”。但实际业务里,各客户端的数据量可能差几个数量级,这时候按样本数加权会让大数据客户端几乎独占模型。
如果业务目标是“对所有客户端公平”,就得换成均匀权重 p_k = 1/K;如果是“对某个重点客户端更在意”,那就是自定义权重。这三者的聚合结果和收敛性是不一样的,而且和前面推的偏差项直接相关——因为偏差项本身就是 Σ p_k H_k ∇F_k 这种按权重求和的混合量,换权重等于换偏差的结构。
还有一个隐蔽的坑:客户端的样本数可能在轮与轮之间变化(比如数据是流式的)。如果服务器缓存的 p_k 没更新,权重就会和实际情况错位。我见过一次线上效果异常,最后定位就是权重用的是第一轮快照,而某些客户端的数据量在几轮内涨了十倍。
5. 常见问题与排查实录
这块我把实际遇到过的问题整理成一张表,再补几条不太容易在文档里看到的经验。
5.1 常见问题速查表
| 现象 | 最可能的原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 换成 E>1 后精度明显下降 | 数据异构 + 步长过大导致 client drift | 固定 E,把 η 减半看精度是否回升 | 降 η、加近端项、或改用控制变量修正 |
| E=1 时两种聚合结果不一致 | 学习率在聚合和本地更新时用了不同值 | 打印本地实际用的 lr 与聚合时除的 lr | 统一 lr,或改用模型差量做聚合 |
| 训练前期正常,后期震荡 | 固定 η 配大 E,偏差项累积 | 观察 loss 曲线包络 | 加学习率衰减,或随轮数递减 E |
| 部分客户端上传后模型反而变差 | 聚合权重与样本量不匹配 | 核对每个客户端的 p_k 与其实际样本数 | 重新计算权重,必要时改为均匀权重 |
| 完全同构数据也收敛不到最优点 | 有效学习率被偏差项修正 | 手算二次目标的理论不动点进行比对 | 重新标定 η,按 1−(1−ηh)^E 反推 |
| 通信轮数不降反升 | E 增大但 η 同比例放大 | 计算每轮总步长 ξ = Eη 是否失控 | 保持 ξ 不变,单独扫 E |
这张表里最值得展开的是第二行。我见过不止一次,本地更新用的学习率是 0.01,聚合时反算伪梯度却默认除以 0.1,结果两种聚合方式“怎么都对不上”。这类错误在代码里极其隐蔽,因为两处都读的是同一个配置项,但一处做了缩放一处没做。
5.2 几条踩坑心得
第一条:先验证 E=1,再谈别的。任何联邦训练的调参,我都建议先把 E 设成 1、batch 设成全量,跑通“参数聚合 = 梯度聚合”这一步。这一步过了,说明你的本地更新逻辑、权重计算、聚合顺序都没问题,后面看到的所有异常都可以归因到 E 和异构上。这一步没过,那问题一定在工程实现里,别急着往算法上找原因。
第二条:用二次目标做单元测试,比用真实数据集划算得多。前面 2.2 节那个闭式解,我后来写成了一个小脚本,每次改聚合逻辑就跑一遍,拿实测不动点和理论值对比。真实数据集上你只能看到 loss 曲线,而闭式解能直接告诉你“偏移了多少、方向对不对”。用一维二次目标做测试,几分钟就能定位到是权重算错了还是学习率没对齐。
第三条:别把伪梯度当梯度用。有些实现会把客户端上传的 (θ^t − θ_k)/η 当作梯度,喂进服务器的优化器(比如 Adam)里。这在数学上会出问题,因为伪梯度的尺度依赖 η 的选择,而 Adam 这类优化器对输入尺度是敏感的,你换个 η 就会得到完全不同的自适应步长行为。如果非要用服务器端优化器,更稳妥的做法是把它当作“模型差量”来处理,并重新标定优化器的超参。
第四条:通信节省的收益要在真实网络条件下评估。我最初在本地模拟时,把每轮通信耗时设成一个常数,结论是 E 越大越好。换到真实的跨机房环境后,发现单次往返的延迟波动很大,而且上行带宽往往是下行的几分之一,这导致最优 E 比模拟值小不少。如果你的场景里上行是瓶颈,那么“客户端少传、服务器多算”的方案会比理论最优更偏保守。
第五条:异构评估要分维度。前面推导已经指出,曲率异构(特征分布差异)和最优解异构(标签分布差异)对 client drift 的贡献完全不同。做数据切分实验时,建议至少做三组:只切标签(按类别分)、只切特征(按特征区间分)、两者都切。我自己的经验是第二组最难受,因为特征偏移直接改变了本地函数的形状,E 稍微大一点就能看到明显漂移。
最后分享一个我觉得挺有用的小习惯:每次调 E 和 η,我都会顺手把“这一轮实际消耗的通信量”和“这一轮等效的全局梯度下降步数”记下来,算一个比值。这个比值能直接告诉你,当前的参数组合离“通信高效”还有多远。很多时候,把 E 从 20 降到 10、同时把 η 调大一倍,比值几乎不变,但收敛稳定性会好很多,这种交换往往比死磕大 E 更划算。