做机器人控制策略训练时,我第一版 PPO 是在单机环境里跑的。结果非常扎心:环境仿真一步动辄几十毫秒,8 核 CPU 开满并行数,采样吞吐撑死到几千步每秒,而 PPO 想收敛到像样的控制效果,往往要几十万上百万步。于是我把训练改成了典型的异步分布式架构,把采样拆到几十个 actor 进程上,吞吐一下冲上去,但新的问题立刻冒出来——因为 actor 拿到的参数永远是 learner 更新之前的老版本,我用这批“滞后的样本”去更新最新策略,训练曲线开始剧烈抖动,loss 一度直接飞掉。
这个课题本质上不是“怎么把 PPO 改分布式”这么简单,而是要在采样吞吐、策略滞后、学习效率这三者之间找平衡。它适合正在做强化学习工程化、想把单机 PPO 扩展到多机多进程、或者已经在分布式 PPO 上踩过坑的读者。下面我把自己的设计思路、实现细节、调参心得和一些真金白银踩出来的坑写下来。
1. 这个课题要解决什么问题
1.1 单机 PPO 卡在哪
先看单个样本的生产成本。一次交互包含:环境 step 计算、通过神经网络前向得到 action、把 transition 存进经验池。环境仿真往往是绝对值最高的开销项,比如 MuJoCo 类的接触仿真,一步可能就要 10~50ms;哪怕是 Gym 里的一些轻量任务,Python 侧环境封装加上 ray 或 gym.vector 的开销也不小。更麻烦的是,很多环境本身是 CPU 密集型的,一个进程内开多线程并行环境,收益会迅速饱和。
在单机同步训练里,agent 采样一步,learn 更新一步,整个系统完全被最慢的环节卡死。就算我用gym.vector一次跑 16 个环境,吞吐提升也是有限度的,毕竟机器就那么多核,而且 PPO 一轮更新要先攒 4096 甚至更多条 transition,采集时间和学习时间直接串行叠加。单机跑 PPO,90% 的时间都在等数据,GPU 时不时空转,这是最典型的工程浪费。
1.2 异步分布式拆掉了什么
异步分布式把“采样者(actor)”和“学习者(learner)”彻底拆开,中间用经验队列连接。Actor 只管拉取最新参数、跑环境、产生 transition,往队列里塞;Learner 只管从队列里取数据、组 batch、做 PPO 更新,更新完再广播新参数。整个过程可变长,actor 和 learner 不必严格对齐节奏。
好处非常直接:吞吐量不再受单机仿真速度限制,我可以开 20 个 actor 进程,吞吐量直接乘 20。GPU 也能被持续喂饱。坏处也很直接:每次 actor 从参数缓存区拿到的theta_old,和 learner 当前正在更新的theta之间,永远存在时间差,这就是策略滞后。队列越长、actor 越多、参数广播越不频繁,滞后越严重。
1.3 三要素的权衡关系
这里有一组很难同时满足的目标:想让吞吐量高,就得堆 actor 数量、把队列开大、数据尽量攒成大批次再学;想让策略滞后小,就得减少队列积压、频繁同步参数,甚至限制 actor 数;想保持学习效率高,就得让样本尽量贴近当前策略分布、PPO 的 importance sampling 权重不至于偏离 1 太多,同时样本复用次数又不能太贪。
我的经验是:这三个指标构成一个不可能三角。任何只追求单项指标的做法,都会在另外两项上付出代价。比如把采样吞吐从 5000 拉到 20000,策略滞后从几百步膨胀到几千步,学习效率反而下滑,最终 wall-clock 收敛速度可能原地踏步。整个系统的关键,就是找到这个三角的稳定工作点。
2. 框架架构与组件设计
2.1 Actor 集群:环境仿真的并发化
Actor 集群的核心设计非常朴素:每个 actor 进程负责若干个并行环境,循环执行“拉参数、跑环境、推样本”。
我在实现时给 actor 定了几个职责边界。第一,actor 必须维护本地参数副本,不直接访问 learner 正在训练的模型;第二,actor 内部用一个固定长度的环形缓冲区接收 rollout 数据,积攒到一定数量再打包推给 learner,避免每条 transition 做一次进程间通信;第三,actor 定期去参数缓存区拉取最新权重,而不是每步都拉。
一个容易忽略的细节是批量推理。单个环境单步推理时,模型前向是串行的,吞吐极低。正确做法是 actor 内部维护多个环境实例,把多份观察拼成一个 batch 一起前向,这样推理吞吐几乎线性增长。比如我开了 16 个环境实例,GPU 推理 batch 从 1 变成 16,耗时可能只增加 30%,吞吐却翻了接近 10 倍。如果模型很小而环境仿真也很轻量,推理甚至可以放到 CPU 上做,避免 GPU 上频繁小 batch 拷贝反而拖慢整体速度。
2.2 Learner 端:训练循环与参数版本管理
Learner 是系统里唯一负责梯度更新的节点。它从经验队列中批量取样本,计算 PPO 的 clipped 损失,做若干轮 minibatch 更新,然后发布新参数。
这里最容易忽略的是“参数版本管理”。我建议给每次发布的参数快照打上一个递增的版本号,actor 采样时记录自己用的是哪个版本号。这样在任何时刻回看数据,都能算出这批样本离当前策略有多远。很多分布式 RL 工程最后调不动,就是因为组件间没有统一的版本语言,问题出现时连“样本有多旧”都答不上来。
参数发布策略建议用“定期 + 定时”双触发:每次 learner 完成 N 次更新就发布一次快照,如果长时间没有完成 N 次更新但已经超过了最长时间阈值,也强制发布一次。这么做的目的是防止 actor 长时间拿不到新参数,导致策略滞后无限增长。
2.3 数据通道与共享内存设计
数据通道设计直接决定吞吐上限。同一台机器上,我强烈建议用共享内存队列,而不是 TCP、UDP 或者 gRPC。RL 的 transition 是小对象,高频小消息走网络协议栈,每秒上万条时 CPU 开销极其惊人,而且带宽利用率很低。
我在本项目中使用的是“生产者消费者 + 共享内存环形队列”模型。典型流程如下:
- Actor 将一组 transition 序列化到共享内存的连续 buffer 中;
- 在 buffer 头部写入 batch 长度、策略版本号、样本 meta 信息;
- 通过一个轻量的原子计数器或信号量通知 learner 有新数据;
- Learner 轮询或阻塞等待新数据,直接读取该段 buffer,反序列化后进入训练。
跨机器部署时,经验传输改成批量 gRPC 是可行的,但务必把几百条 transition 打包成一个 request 再发,否则网络握手会直接把训练拖垮。
注意:共享内存不是无限大的,必须要设计背压机制。队列满时 actor 应主动阻塞暂停,而不是不断覆盖还没有被 learner 消费的数据。覆盖老数据对 PPO 是一个深坑——你会把一段“高价值的新鲜样本”悄悄丢掉,学习效率会明显劣化。
3. 采样吞吐优化
3.1 第一步先定位瓶颈:环境仿真、模型推理还是传输
优化吞吐前,先搞清楚瓶颈在哪个环节。我常用一个很土的办法:分别压测三段的极限吞吐。
- 只跑环境仿真,不接策略,统计每秒环境 step 数;
- 只跑模型推理,不跑环境,统计每秒前向次数;
- 只做共享内存读写,统计每秒 transition 传输条数。
压测结果通常会落在以下三种情况里:
| 瓶颈环节 | 特征表现 | 有效手段 |
|---|---|---|
| 环境仿真 | CPU 多核打满,actor 数增加吞吐不增 | 扩大 actor 进程数、降低环境 step 频率、换更高效的仿真后端 |
| 模型推理 | GPU 利用率接近 100%,但单 batch 过小 | 增大并行环境数、拼大 batch、减少模型层数或使用更轻量 backnone |
| 数据传输 | CPU 耗费在序列化/拷贝/锁竞争 | 改用共享内存、减少拷贝次数、大块聚合多次再写 |
我遇到过最典型的情况:环境本身超轻量,反而是 Python 共享内存写入端频繁加锁,导致多 actor 在高频写入时互相等待。改成“每个 actor 攒满一个 chunk 再写入”,吞吐直接翻倍,代价是队列里数据的实时性稍微下降,这部分滞后可以在后续权衡里弥补。
3.2 Actor 数量与批量推理的合理配比
Actor 数量不是越多越好,每个 actor 也不是越大越好。我通常把“并行环境总数 = actor 数量 × 每个 actor 的环境数”作为一个整体调节旋钮。
假设每个环境单步耗时T_env,每个 actor 每次前向处理B条环境并行数据,那么单个 actor 的单步吞吐大致是B / T_env步每秒。想要 10000 步每秒,如果B=16、T_env=0.01s,一个 actor 能产生 1600 步每秒,那么 7 个 actor 左右就够了。实际还要考虑参数同步、通信、学习端消费速度,留出 1.5~2 倍余量比较稳妥。
GPU 推理批量大小也值得调。模型很小的时候,batch 从 1 加到 32,推理耗时可能只增加 30%;但如果再往上加到 256,延迟就会线性增长,反而让单条轨迹的等待时间变长。经验上,控制在“刚好把一个 actor 内所有并行环境的一次观测塞完”的水平,再叠 2~4 个 actor 作为余量,通常是比较好的起点。
3.3 经验缓存与传输层避坑
传输层有几个细节非常影响吞吐:
第一,尽量避免逐条推数据。我见过有人每条 transition 都往消息队列发一次,actor 一多,消息队列直接成为瓶颈。正确做法是:actor 端攒满 256 条或 512 条再发布一次。对于 PPO 这种一次更新需要几千条样本的算法,这么做的滞后代价其实可接受。
第二,序列化格式要选好。Python pickle 简单但慢;numpy.save到 bytes 更快;如果数据字段固定,直接定义结构体np.ndarray批量打包,是共享内存方案里最优解之一。实际测下来,批量 numpy 打包比逐字段 pickle 快 5 倍以上。
第三,拷贝次数要压到最低。共享内存里写一次,learner 读的时候尽量直接在同块内存上解析,避免“共享内存 → 中间 bytes → tensor”二次拷贝。PyTorch 的 tensor 可以直接通过from_numpy和共享内存 buffer 共享底层内存,但要小心 buffer 生命周期,learner 还没读完,actor 就复用同一片区域是典型的使用越界。
4. 策略滞后问题的本质与缓解
4.1 什么是策略滞后,怎么度量它
策略滞后指 actor 采样时使用的参数版本θ_old和 learner 当前更新用的θ_now之间的差异。分布式场景下,这个差异不可避免,因为参数从 learner 传到 actor 需要时间,样本从 actor 传回 learner 也需要时间。
度量滞后,最直观的方法是“样本年龄”:一条样本被 learner 取出计算时,距离它被 actor 产生时经历过的全局参数更新次数。如果 learner 已经做了 50 次梯度更新,这条样本的年龄就是 50。队列越长、参数广播越慢,年龄越大。
更严格地,可以算参数空间距离,比如‖θ_now − θ_old‖₂,或用一层网络输出分布的 KL 散度近似。对 PPO 来说,滞后对训练的影响本质上不是“时间旧”,而是“策略分布变了”。如果 learner 已经大幅更新,老样本对应的重要性采样比率ρ_t = π_θ / π_θ_old就会显著偏离 1,PPO 的 clip 机制会反复被触发。
4.2 滞后如何破坏 PPO 训练
PPO 的目标函数在设计上是 near-on-policy 的,它用 clip 限制单次更新步长,前提是采样分布和更新分布差别不大。在异步框架里,如果滞后严重,ρ_t可能变得非常大或非常小。
这里特别要注意负优势一侧。当优势 A < 0 时,标准 PPO 只会限制ρ的下界(即clip(ρ, 1−ε, 1+ε) * A),如果ρ远大于 1,那么该项是(1+ε)*A,负号被限制住了;但如果ρ远小于 1,clip 后是(1−ε)*A,更新幅度也有限。然而工程实现上如果对ρ没有下限处理,或者滞后过大导致 clip 不再生效,负优势样本会产生一个非常大的负 loss 贡献,策略更新会被“惩罚性梯度”推飞。这也是分布式 PPO 训练里 loss 突然暴涨的最常见原因。
经验上,当经验队列里的平均样本年龄超过 30~50 次全局更新时,训练曲线就开始明显抖动;超过 100 次,基本要出事故。
4.3 Dual-Clip PPO:负优势侧的守护策略
针对异步滞后带来的负优势侧稳定性问题,业界的常见改法是 dual-clip PPO。它在标准 PPO 的 clipped 目标基础上,对负优势增加一个额外的下界,让负优势对应的 loss 不会无上限地膨胀。
dual-clip 形式可以写作:
L = min( ρ_t * A_t, clip(ρ_t, 1−ε, 1+ε) * A_t, c * A_t )当 A_t < 0 时,第三项c * A_t作为绝对下限;当 A_t > 0 时,第三项通常不起作用。超参数 c 一般取 0.5~0.8,滞后越大、噪声越强,c 越要调小,但不能太小,否则负优势方向几乎没有学习信号,策略会失去“避免坏动作”的能力。
我做过对比实验:同样的异步分布式配置,标准 PPO 在样本年龄超过 50 时 loss 发散,加上 dual-clip 后稳定运行,收敛速度反而更快。原因很简单——稳定性保住了,学习效率才谈得上。
4.4 工程侧的滞后缓解手段
除了算法层面的 dual-clip,工程侧也有一些有效手段把滞后压下来。
- 限制经验队列长度:队列太长,样本年龄必然增大。把队列长度控制在 learner 两三个 batch 的量级,能显著降低平均年龄。
- 提高参数广播频率:learner 每更新 50~200 次就广播一次新参数,actor 拉取频率同步提高。
- 新样本优先采样:如果一条样本在队列里滞留过久,可以设置最大年龄,超龄样本直接丢弃。丢弃会损失一部分吞吐,但能保住样本质量。
- 合理限制 PPO epoch 数:每批数据不要重复学太多轮,否则旧样本被反复使用,策略分布进一步漂移,等价于人为放大了滞后效应。
5. 学习效率的权衡与调参
5.1 样本利用率与吞吐量的矛盾
PPO 在同步模式下,同一批数据通常做 3~10 个 epoch 的 minibatch 更新,这是它样本利用率的核心来源。但在异步模式下,样本在队列里已经变旧,如果还贪心地学很多 epoch,等于在“远离当前策略的数据”上反复过拟合,损失函数不断把策略拉向一个不再准确的目标。
我在异步架构里通常把 epoch 数降到 1~2,minibatch 数量也相应减少。这样单 batch 的学习效率会损失,但换来的是策略更新轨迹更贴近真实分布,长期收敛速度反而好。
另一个矛盾点是 batch size。训练端往往想把 batch 组得尽可能大,以提高 GPU 利用率和更新稳定性;但这会延长“攒 batch 所需时间”,从而抬高样本平均年龄。我的经验是:batch size 和队列深度要联动调节,保证一次批次组完,队列中样本平均年龄不超过 10 次全局更新。
5.2 关键超参数的经验值
给出一个我在多种连续控制任务上验证过的参考配置:
| 参数 | 同步单机基线 | 异步分布式建议 | 备注 |
|---|---|---|---|
| 队列深度 | 无(立即更新) | 2~4 个 batch 量 | 过长则滞后陡增 |
| PPO epoch | 5~10 | 1~2 | 异步下贪多必炸 |
| batch size | 256~4096 | 4096~16384 | 按吞吐量和 cache 容量调整 |
| clip ε | 0.2 | 0.1~0.2 | 滞后大时调小 |
| dual-clip c | 不需要 | 0.5 起调 | 负优势稳定性关键 |
| 学习率 | 3e-4 | 1e-4~3e-4 | 异步噪声大,偏保守 |
学习率这里尤其要小心,同步模式下的 lr 直接搬过来用,经常会导致更新步长相对噪声过大。异步系统的梯度本身更像“统计平均值”,噪声更大,lr 需要适度降低,必要时加学习率 warmup,前几千步用小步长,让策略在靠近初始化分布的区域先稳定走一段。
5.3 一套早期验证的消融思路
任何配置如果直接拿去做完整实验,成本太高。我的方式是先做一个“模拟滞后实验”:
在单机同步环境里,人为给样本注入延迟——每批训练时,把训练数据替换成 10 步、50 步、100 步之前的旧数据。这样可以快速判断你的任务对策略滞后的敏感度,并预先验证 dual-clip 是否必要。
实测结果通常是:滞后 10 步时,PPO 几乎无感;滞后 50 步时,标准差明显变大;滞后 100 步时,不稳定的任务开始发散。这个实验帮我省下了大量分布式调参时间,推荐你也先跑一轮。
6. 连续动作空间的 PPO 实现要点(附代码)
6.1 高斯策略的构造与 Log-Prob 计算
连续动作空间里,最常用的策略是“独立高斯分布”:对每个动作维度,网络输出均值 μ 和标准差 σ,采样时从N(μ, σ²)采一个动作。实现时,网络通常输出log_std而不是直接输出 σ,再通过exp得到标准差,避免 σ 出现负数。
一个简洁的 PyTorch 实现:
import torch import torch.nn as nn import torch.distributions as td class GaussianActor(nn.Module): def __init__(self, obs_dim, act_dim, hidden=[256, 256]): super().__init__() self.trunk = nn.Sequential( nn.Linear(obs_dim, hidden[0]), nn.Tanh(), nn.Linear(hidden[0], hidden[1]), nn.Tanh(), ) self.mean_head = nn.Linear(hidden[1], act_dim) self.log_std = nn.Parameter(torch.zeros(act_dim)) # 初始 std=1 def get_dist(self, obs): h = self.trunk(obs) mean = self.mean_head(h) log_std = self.log_std.clamp(-2.0, 1.0) # 限制 std 范围,防止除零 return td.Independent(td.Normal(mean, log_std.exp()), 1) def sample_with_logp(self, obs): dist = self.get_dist(obs) action = dist.sample() logp = dist.log_prob(action) # Independent 自动对各维度求和 return action, logp这里有一个关键点:td.Independent里的维度设置。Normal产生的分布默认会对每个维度分别计算log_prob,返回形状是[batch, act_dim];用Independent(..., 1)包装后,返回的是整个动作向量的联合 log-prob,形状变成[batch]。不包装的话,后面 PPO 损失计算要把动作维度再 sum 一次,很容易出错。
如果动作空间有边界,比如机器人关节角度限制在 -1~1,需要在采样后做tanh压缩,同时做 tanh 变换的 log-prob 校正。公式是logp_after = logp_before - sum(2 * (log 2 - x - softplus(-2x))),其中x是 tanh 前的值。很多开源实现会漏掉这一项,导致 PPO 收敛到一个看起来能跑但不正确的策略。
6.2 往异步流水线里塞样本时的格式设计
异步训练时,样本格式必须方便打包和序列化。我把一条 transition 组织成固定字段的 numpy 数组:
# 一条样本的字段顺序 # obs: [obs_dim] # action: [act_dim] # logp: [1] # reward: [1] # done: [1] # value: [1](可选项,如果要用 GAE 预计算 return)建议把整批 transition 拼接成 shape 为[time_steps, dim]的 numpy 矩阵再传输。这样做的好处是:写入共享内存时是连续的一整块,learner 读取后直接torch.from_numpy转 tensor,几乎零拷贝。
一个重要细节是 value 估计。如果在 actor 端预测 value 并一起传输,learner 就可以直接算 GAE;如果在 learner 端算,则 learner 必须额外做一次前向,拉长训练循环。我倾向于把 value head 放在 actor 端一起输出,牺牲一点采样侧算力,换取 learner 端更短的更新周期。
6.3 与 dual-clip 结合的 PPO 损失函数
PPO 的 loss 可以在训练循环里这样组织:
def ppo_loss(logp_now, logp_old, adv, clip_eps=0.2, dual_clip_c=None): # logp_old 是 actor 采样时记录的 logp ratio = (logp_now - logp_old).exp() # shape [batch] if dual_clip_c is not None: # 标准 clipped loss surr1 = ratio * adv surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * adv # 负优势侧的绝对下限 surr3 = dual_clip_c * adv pg_loss = -torch.min(surr1, surr2, surr3).mean() else: surr1 = ratio * adv surr2 = torch.clamp(ratio, 1 - clip_eps, 1 + clip_eps) * adv pg_loss = -torch.min(surr1, surr2).mean() return pg_loss注意,torch.min要传入多个张量,并且ratio * adv、clamp(...) * adv、c * adv三者在负优势时确实形成下界,正优势时第三项只会更大,不会参与最小化。这里我用dual_clip_c作为开关,单机同步模式直接关闭,异步模式默认开启,方便做对照实验。
在实际训练循环里,logp_old必须和样本打包一起传输,不能训练时再重新计算。如果重新计算,就相当于用当前参数算旧动作的 logp,importance ratio 会错误地被拉回 1,整个 PPO 的修正机制都被破坏了。这一点是很多初学分布式 PPO 的人最容易写错的地方。
7. 常见问题与排查实录
7.1 训练 loss 突然暴涨
现象:训练平稳跑了一段时间,某个节点 loss 曲线突然跳高,严重时直接 NaN。
排查路径:先看队列里的样本平均年龄。做法是在样本 meta 里记录“样本产生时的 learner 全局步数”,learner 取样本时对比当前步数。若平均年龄 > 50,基本能断定是策略滞后过大的问题。再看 importance ratio 的 log 值分布,如果大量样本的|log ρ|超过 1~2,说明新旧策略分布已经显著漂移。
解决手段:缩短队列长度、提高参数广播频率、打开 dual-clip、降低 PPO epoch 到 1。如果已经 NaN,降低学习率 3~5 倍重跑是更快的恢复路径。
7.2 采样吞吐提不上去
现象:actor 数量已经涨了一倍,整体吞吐几乎没有变化。
先查瓶颈。直接用top看 CPU 占用,如果多核都很闲但吞吐上不去,大概率卡在数据传输或锁竞争。共享内存方案里,优先检查写端是否有逐条序列化、释放锁是否过于频繁。把 actor 的推送改成攒批推送后,再量一次吞吐。
如果 CPU 全部打满,瓶颈便是环境本身,那就需要增加 actor 进程数或减少单 actor 环境数;如果 GPU 利用率接近 100%,瓶颈在策略推理,先增大每个 actor 的并行环境数,把 GPU batch 加大。
7.3 策略更新慢、GPU 利用率忽高忽低
现象:GPU 平均利用率只有 20%,但偶尔冲到 100%。
这是典型的学习端“等待样本”的表现。原因是队列深度太小,learner 攒不完一个 batch。解决方向有两个:把队列调大一点(但要接受滞后增加),或者调低 batch size,让 learner 更容易攒满。
如果是吞吐明显过剩、队列经常清空,更合理的做法是裁减 actor 数量,而不是继续调队列。保留太多采样能力只会浪费资源并推高滞后。
7.4 曲线表现不错,但最终效果不如同步版
这个现象比较隐蔽,我踩过不少次:分布式训练的 reward 曲线看起来比同步版还平滑,但最终测试效果差一截。
原因通常有两个。一是负优势更新被dual-clip c压得太保守,策略只会沿着正优势方向优化,缺乏“远离坏动作”的推力,测试时泛化不足;二是 epoch=1 导致样本利用率太低,很多有价值的状态未被充分学习。
我的处理方式是把dual_clip_c从 0.5 往上调,或者只在滞后超标时开启 dual-clip,平时用标准 PPO。另一个经验是用“最终测试 reward”而不是训练曲线来评估并行版本质量——训练曲线平滑很可能只是因为优势估计被 clip 后数值稳定,不代表策略真的更好。
7.5 黄金监控指标速查
分布式 PPO 调试时,我保留以下监控指标:
| 指标 | 含义 | 健康范围 |
|---|---|---|
| queue_depth | 队列当前长度 | 不超过 2~4 个 batch |
| sample_age | 样本产生到学习的全局更新步差均值 | 小于 20~30 |
| clip_fraction | 被 clip 掉的比例 | 0.1~0.3 之间 |
| log_rho | importance ratio 的对数分布 | 波动范围尽量小于 ±1 |
| grad_norm | 梯度范数 | 不持续大于 10 |
| gpu_util | 学习端 GPU 利用率 | 维持 60% 以上 |
我一般在 learner 端每 100 次全局更新打一条日志,把上面这些指标一起输出。分布式系统一切都在动态变化,只看 loss 和 reward 很难定位问题,这些指标才是真正的“体检表”。
一些最后的体会
我自己反复调过很多轮之后,最大的体会是:不要试图让吞吐量最大化,也不要让滞后清零,而是要在“实时性”和“吞吐量”之间找一个让学习过程最稳定的工作点。对这个模型小、环境快的任务,我最终选定的配置是 16 个 actor、队列长度 4 个 batch、PPO epoch 为 1、dual-clip c=0.5。吞吐从单机 3000 步每秒涨到了接近 15000 步每秒,策略滞后控制在 20 次全局更新以内,学习效率和训练稳定度都令人满意。
最后再分享一个容易被忽略的小技巧:无论架构怎么调,一定要保留一套单机同步 PPO 作为基准。每改动一个分布式参数,先用同步版测一下任务本身有没有发生变化。很多时候你调了半天,最后发现是环境版本或 reward 放缩换了,而不是分布式的问题。有了同步基准,你才能分清哪些是算法噪声、哪些是异步带来的真正影响。