You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
摘要
直接回答标题中的几个问题:
log_alpha都在各自 optimizer step 前做阻塞式all_reduce(SUM) / world_size。log_alpha广播给其他 rank。之后没有“每 N 步广播一次参数”;各 rank 依靠相同初值、相同平均梯度和相同 optimizer 状态保持参数一致。DistributedDataParallel包模型,而是手写 collective:每个网络把所有已有梯度拼成一个 flat tensor,一次 NCCL all-reduce 后再拷回各参数梯度。这是一份源码审计,不是多卡性能 benchmark。相关结论基于本地
6e146b0;同时核对了 2026-08-14 的远端main@6761e2b,两者之间的提交没有改动本文涉及的 FastSAC、训练入口或分布式代码。审计基线:
amazon-far/holosoma@6761e2b。1. 启动与进程拓扑
官方示例用
torchrun --nproc_per_node=4启动 4 个进程,并把training.num_envs定义为全局环境数:README.md#L211-L219。每个进程从
WORLD_SIZE / RANK / LOCAL_RANK读取拓扑,默认使用 NCCL 初始化 process group,然后绑定cuda:LOCAL_RANK:train_agent.py#L59-L97。这里有一个容易混淆的入口语义:普通
train_agent.py是否进入多卡路径由WORLD_SIZE > 1决定,也就是要由torchrun建立进程;training.multigpu本身不会在这个入口里自动 spawn。nightly wrapper 才会根据该配置重新用固定 4 卡torchrun拉起:nightly.py#L68-L88。配置的全局环境数
E会在每个 rank 上做整数除法:源码直接使用
num_envs // world_size,没有分配余数,也没有整除校验:train_agent.py#L245-L260。每个 rank 的 seed 还会加上 global rank,因此在相同模型下产生不同的环境随机性和动作采样:train_agent.py#L195-L199。2. 数据如何并行:本地 replay shard,不传 transition
每个 rank 都创建自己的
SimpleReplayBuffer,形状以本地env.num_envs为第一维,容量是每个环境buffer_size个时间槽:fast_sac_agent.py#L339-L348、fast_sac_utils.py#L15-L53。每个外层迭代中,各 rank 独立执行一个 vectorized env step,把本地 transition 写进本地 buffer;这里没有任何 distributed collective:
fast_sac_agent.py#L681-L731。采样也只访问本地 tensor,而且不是在扁平 replay 上一次抽样:它为每个本地环境分别抽取同样数量的随机时间索引,再 reshape 成本地 batch:fast_sac_utils.py#L80-L110。因此数据流是:
全局 batch 的实际计算
源码把
args.batch_size注释为 global batch,但先算“每个环境抽几条”:随后每个 rank 的单次 update batch 是
local_envs * k,所有 rank 合起来是:证据:
fast_sac_agent.py#L730-L752、fast_sac_agent.py#L537-L624。所以只有在环境数和 batch 配置满足整除关系时,实际全局 batch 才严格等于配置值。例如框架默认
E=4096、FastSAC 默认B=8192,在W=4时每卡1024个环境、每环境抽2条,每卡 batch2048、全局正好8192。如果B < actual_global_envs,由于下限为 1,实际 batch 会至少等于实际全局环境数。配置证据:experiment.py#L55-L73、config_values/algo.py#L64-L109。注册的 FastSAC 默认还开启 symmetry,准备 batch 时会把每次 update 的样本数乘 2。3. 梯度同步还是参数同步
答案是“两者都有,但阶段不同”:
broadcastbroadcastlog_alpha值broadcastload_state_dictall_reduce(SUM) / Wall_reduce(SUM) / Wlog_alpha梯度all_reduce(SUM) / Wuse_autotune=true时初始化广播见
fast_sac_agent.py#L354-L380。手写梯度同步会把一个网络的全部现有梯度 flatten 成单个 tensor,只发起一次阻塞式 all-reduce,然后除以 world size 并 scatter 回各梯度 tensor:fast_sac_agent.py#L382-L402。critic 与 alpha 的调用位置在
fast_sac_agent.py#L446-L478,actor 的调用位置在fast_sac_agent.py#L513-L529。all-reduce 发生在 AMPbackward()之后、unscale_和 gradient clipping 之前;也就是说通信的是 scaled gradient,随后每个 rank 本地 unscale、clip、step。这不是定期参数平均,也不是 parameter-server 模式。同步 all-reduce 默认
async_op=False,因此每次 optimizer update 都是 rank 间 rendezvous;慢 rank 会在该 collective 处拖住其他 rank。参数之所以能持续一致,是因为所有 rank:在各 rank 本地 batch 大小相同的前提下,这个“平均 rank-local mean gradient”等价于对各 rank minibatch 并集求全局 mean gradient。
4. 默认配置下的精确同步频率
学习开始条件是
global_step > learning_starts。进入稳态后,每个外层环境 step 会先一次性采一个大 batch,再拆成num_updates个 update batch:fast_sac_agent.py#L730-L752。注册配置默认:
因此每个外层环境 step:
i % policy_frequency == 1,所以i=1,5更新,共 2 个 actor flat-gradient all-reduce;若
num_updates == 1,actor 才改用global_step % policy_frequency == 0。这些条件和 target 更新位置见fast_sac_agent.py#L738-L775。因此“actor 每 4 次更新同步”只是默认配置下的直观描述,准确语义应以这两个分支为准。5. 模型参数之外还有哪些同步
Observation normalization
默认启用的
EmpiricalNormalization会对本地 sum 和 squared-sum 做 all-reduce,以全局 batch 更新 mean/variance/count:fast_sac_utils.py#L274-L329。训练循环在 rollout inference 时传
update=False,所以采集阶段不更新统计;在大 batch 拆分前依次 normalizeobs / next_obs / critic_obs / next_critic_obs,默认每个外层学习迭代共触发 4 次统计 all-reduce,而不是每个 inner update 都触发:fast_sac_agent.py#L537-L595、fast_sac_agent.py#L686-L689。Curriculum
如果启用 curriculum,每个外层 rollout 前都会调用同步;具体实现是把 rank 0 的 average-episode tracker 和 penalty scale 广播给其他 rank,并不是对各 rank curriculum 指标求平均:
fast_sac_agent.py#L681-L684、locomotion_manager.py#L223-L236。Checkpoint 与日志
只有 rank 0 定期保存 checkpoint/ONNX:
fast_sac_agent.py#L794-L811。恢复时 setup 的初始化广播先发生,随后所有 rank 各自加载同一 checkpoint;load()会恢复 actor、critic、target、normalizer、三个 optimizer、GradScaler 和 global step,但没有恢复/交换 replay buffer,也没有 load 后的再次广播:train_agent.py#L281-L299、fast_sac_agent.py#L626-L648。一致性依赖所有 rank 读取相同 checkpoint。另一个可观测性边界是:episode 统计、TensorBoard/W&B 写入只取 rank 0,本地 loss/reward 没有额外做全局 reduce;吞吐量才用
num_gpus乘回全局口径:logging_utils.py#L120-L149、logging_utils.py#L181-L203、logging_utils.py#L280-L342。所以 rank 0 日志不是全局数据分布的严格聚合指标。6. 一句话定性
Holosoma FastSAC 是 replicated model + rank-local environment/replay shard + synchronous gradient averaging on every optimizer step。它不是共享 replay、不是周期参数平均、不是异步 parameter server,也不是 DDP wrapper;只是训练语义与同步 data parallel 相同,collective 由算法代码手工完成。
仓库的多卡 E2E 测试确实会用
torchrun覆盖 FastSAC workflow,但主要验证训练/评估流程成功,不验证上述 collective 的数值等价性或通信性能:test_training.py#L77-L103、test_training.py#L126-L155。7. 对 UniLab 的边界性启示
这套方案成立的前提是每个 rank 同时拥有一套 simulator、replay 和 learner,并在每个 optimizer step 等待所有 rank。UniLab 当前 off-policy production 拓扑是单 collector + learner-owned inference,且 #956 已明确把多 learner/NCCL、跨 learner replay 和多 GPU support claim 列为 non-goal,因此本文不建议把 Holosoma 路径直接移植进当前 execution path。
如果未来另立多 GPU roadmap,这次审计至少说明需要同时做出以下 contract 决策,不能只回答“用 DDP 还是同步参数”:
num_envs和 global batch 的余数/最小 batch 如何定义;相关讨论:
All reactions