执行摘要
- 一句话:修复完全异步FSDP2训练在AMD ROCm平台(MI300X系列)的兼容性问题。
- 推荐动作:该PR值得精读,特别是ZMQ IPC句柄重构的设计决策,它展示了如何通过平台无关的rank算术解决硬件特定的通信不匹配问题。建议关注
_get_zmq_handle方法的实现和讨论中关于错误处理与Actor命名的权衡。
功能与动机
根据PR body描述,完全异步FSDP2训练在AMD ROCm平台(MI300X系列)上无法正常工作,具体表现为FSDP初始化失败(ncclSystemError)、ZMQ通信不匹配、JSON序列化错误和IPC套接字残留导致重启失败。作者在AMD Instinct MI3xx(8× GPU, 192 GB HBM each)和NVIDIA H20上进行了交叉验证,旨在使训练流程在AMD平台上稳定运行。
实现拆解
- 添加AMD RCCL必需的环境变量:在
verl/trainer/constants_ppo.py的PPO_RAY_RUNTIME_ENV中添加"HSA_NO_SCRATCH_RECLAIM": "1",以解决FSDP初始化时的ncclSystemError。该变量在非AMD平台上会被忽略。
- 修复JSON序列化错误:在
verl/trainer/ppo/ray_trainer.py的_dump_generations方法中,将json.dumps调用改为json.dumps(..., default=str),以处理NumPy 2.x中numpy.bool_不再是Python bool子类的问题。
- 重构ZMQ IPC句柄生成逻辑:在
verl/workers/rollout/vllm_rollout/vllm_rollout.py、verl/workers/rollout/vllm_rollout/utils.py和verl/workers/rollout/vllm_rollout/vllm_async_server.py中,将句柄从基于GPU UUID改为基于(replica_rank, local_rank)。这解决了ROCm平台上因CUDA_VISIBLE_DEVICES/HIP_VISIBLE_DEVICES设置不同导致的GPU UUID不匹配问题,并避免了多副本共享节点时的套接字冲突。
- 清理残留的ZMQ IPC套接字文件:在
verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py的_init_socket和_cleanup方法中,添加逻辑在绑定前和清理后删除IPC套接字文件,使用except OSError捕获权限错误等异常,防止重启时出现Address already in use错误。
- 修复Hydra配置路径:将
verl/experimental/fully_async_policy/config/fully_async_ppo_trainer.yaml中的搜索路径从file://verl/trainer/config改为pkg://verl.trainer.config,以支持可编辑安装。
- 防止Ray Actor重复创建:在
verl/tools/sandbox_fusion_tools.py的init_execution_pool函数中,为ExecutionWorker添加name="sandbox-execution-pool"和get_if_exists=True选项,确保在同一训练会话中重用Actor。
关键文件:
verl/workers/rollout/vllm_rollout/utils.py(模块 Rollout通信;类别 source;类型 core-logic;符号 _get_zmq_handle): 修改了vLLMColocateWorkerExtension和vLLMOmniColocateWorkerExtension的_get_zmq_handle方法,将句柄从基于GPU UUID改为基于(replica_rank, local_rank),这是解决ROCm通信不匹配的核心改动之一。
verl/workers/rollout/vllm_rollout/vllm_rollout.py(模块 Rollout通信;类别 source;类型 core-logic;符号 init): 修改了VLLMRollout类的__init__方法,重构了ZMQ句柄生成逻辑,使用replica_rank和node-local rank替代GPU UUID,并添加了详细注释解释原因。
verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py(模块 权重传输;类别 source;类型 core-logic;符号 _init_socket, _cleanup): 在BucketedWeightSender的_init_socket和_cleanup方法中添加了IPC套接字文件清理逻辑,防止重启时出现'Address already in use'错误,并根据review反馈改进了错误处理。
verl/trainer/constants_ppo.py(模块 训练配置;类别 source;类型 configuration;符号 PPO_RAY_RUNTIME_ENV): 在PPO_RAY_RUNTIME_ENV中添加了AMD RCCL必需的环境变量HSA_NO_SCRATCH_RECLAIM,解决了FSDP初始化失败问题。
verl/trainer/ppo/ray_trainer.py(模块 训练器核心;类别 source;类型 core-logic;符号 _dump_generations): 修复了JSON序列化错误,为json.dumps添加default=str参数以处理numpy.bool_类型。
verl/tools/sandbox_fusion_tools.py(模块 工具脚本;类别 source;类型 core-logic;符号 init_execution_pool): 为ExecutionWorker添加name和get_if_exists选项,防止同一训练会话中重复创建Actor。
verl/workers/rollout/vllm_rollout/vllm_async_server.py(模块 异步服务器;类别 source;类型 core-logic): 设置了VERL_REPLICA_RANK环境变量,确保vLLM工作进程能继承正确的副本rank。
verl/experimental/fully_async_policy/config/fully_async_ppo_trainer.yaml(模块 实验配置;类别 config;类型 configuration): 修复Hydra配置搜索路径,支持可编辑安装。
关键符号:_get_zmq_handle, init, _init_socket, _cleanup, _dump_generations, init_execution_pool
关键源码片段
verl/workers/rollout/vllm_rollout/utils.py
修改了vLLMColocateWorkerExtension和vLLMOmniColocateWorkerExtension的_get_zmq_handle方法,将句柄从基于GPU UUID改为基于(replica_rank, local_rank),这是解决ROCm通信不匹配的核心改动之一。
def _get_zmq_handle(self) -> str:
"""Get ZMQ handle for communication.
Uses replica_rank + local_rank to form handle so it matches the sender side
regardless of CUDA_VISIBLE_DEVICES differences, and avoids collisions
when multiple replicas share the same node.
"""
replica_rank = os.environ.get("VERL_REPLICA_RANK", "0") # 从环境变量获取副本 rank,默认为 "0"
return f"ipc:///tmp/rl-colocate-zmq-replica-{replica_rank}-rank-{self.local_rank}.sock" # 基于副本 rank 和本地 rank 构建句柄
verl/workers/rollout/vllm_rollout/vllm_rollout.py
修改了VLLMRollout类的__init__方法,重构了ZMQ句柄生成逻辑,使用replica_rank和node-local rank替代GPU UUID,并添加了详细注释解释原因。
def __init__(self, config, replica_rank=-1):
# ... 省略 rank 计算等初始化代码 ...
# Use replica_rank + node-local rank to form ZMQ handle instead of GPU UUID,
# because CheckpointEngineWorker and vLLM worker may see different GPU UUIDs
# when CUDA_VISIBLE_DEVICES differs between processes (common on ROCm/AMD).
# Must use node-local rank (not rollout_rank) so it matches vLLM worker's
# local_rank on every node. Include replica_rank to avoid collisions when
# multiple replicas share a node.
local_rank = self.rollout_rank % local_world_size # 计算节点本地 rank
self.zmq_handle = f"ipc:///tmp/rl-colocate-zmq-replica-{self.replica_rank}-rank-{local_rank}.sock" # 构建新句柄
verl/workers/rollout/vllm_rollout/bucketed_weight_transfer.py
在BucketedWeightSender的_init_socket和_cleanup方法中添加了IPC套接字文件清理逻辑,防止重启时出现'Address already in use'错误,并根据review反馈改进了错误处理。
def _init_socket(self):
"""Initialize ZMQ REQ socket and bind."""
if self.zmq_handle.startswith("ipc://"):
ipc_path = self.zmq_handle[len("ipc://") :] # 提取 IPC 文件路径
try:
os.remove(ipc_path) # 尝试删除残留文件
except OSError: # 捕获所有文件系统错误(包括 PermissionError)
pass # 忽略错误,避免进程崩溃
self.socket = self.zmq_context.socket(zmq.REQ)
self.socket.bind(self.zmq_handle)
评论区精华
- IPC套接字清理的错误处理:
gemini-code-assist[bot]指出,仅捕获FileNotFoundError在多用户环境中存在风险,残留套接字文件可能因权限问题导致PermissionError,进而使训练进程终止。建议改为捕获更宽泛的OSError。作者在第二次提交中采纳了此建议,将except FileNotFoundError改为except OSError。
- Ray Actor全局名称的潜在冲突:
gemini-code-assist[bot]警告,硬编码的全局名称"sandbox-execution-pool"在多租户Ray集群中可能导致碰撞,使新作业意外连接到现有池。作者xiaohong42解释,Verl训练作业通过ray.init()创建独立的Ray集群,因此跨作业的命名空间冲突不会发生;硬编码名称旨在同一训练会话内重用Actor,与同文件中TokenBucketWorker的模式一致。
- AMD环境变量的作用:
wuxibin89询问HSA_NO_SCRATCH_RECLAIM环境变量的用途。xiaohong42详细解释,该变量是AMD RCCL在MI300X GPU上的必需设置,用于控制GPU暂存内存回收,避免FSDP初始化失败。
- IPC套接字清理的错误处理 (correctness): 作者在第二次提交中采纳建议,将except FileNotFoundError改为except OSError,以更优雅地处理文件系统错误。
- Ray Actor全局名称的潜在冲突 (design): 作者认为当前设计是合理的,未作修改,因为Verl的作业隔离机制降低了风险。
- AMD环境变量的作用 (question): 通过解释澄清了该变量的必要性和平台特异性。
风险与影响
- 风险:
- 平台兼容性风险:新增的
HSA_NO_SCRATCH_RECLAIM环境变量是AMD特有的,在NVIDIA或Ascend平台上会被忽略,但需确保不会意外干扰其他硬件。
- ZMQ句柄逻辑变更风险:ZMQ IPC句柄从基于GPU UUID改为基于
(replica_rank, local_rank),虽然解决了ROCm不匹配问题,但需确保在所有部署场景(如单节点、多节点、多副本)下都能正确匹配,避免通信失败。
- IPC套接字清理风险:清理残留套接字文件时捕获
OSError可能掩盖其他文件系统问题,但权衡后认为防止重启失败更重要。
- Ray Actor重用风险:硬编码的Actor名称在同一Ray集群内可能导致意外重用,但作者解释Verl的作业隔离机制降低了此风险。
- 测试覆盖不足:由于改动针对ROCm特定运行时行为,无法在CI中无AMD硬件的情况下充分测试,可能隐藏平台特定bug。
- 影响:
- 用户影响:使完全异步FSDP2训练能够在AMD ROCm平台(如MI300X)上稳定运行,扩展了硬件支持范围。用户无需修改代码,所有改动为内部实现细节。
- 系统影响:提升了跨平台兼容性,ZMQ句柄逻辑的改进也使多节点、多副本部署更健壮。环境变量和JSON序列化的修复是通用改进,对所有平台有益。
- 团队影响:为后续在AMD硬件上开展训练任务铺平道路,减少了平台特定的调试开销。
- 风险标记:跨平台兼容性, 核心通信路径变更, 缺少硬件特定测试
关联脉络
- PR #5967 [fully_async] fix: Add Mindspeed Patch for Async Training on Ascend NPUs: 类似地,该PR修复了在特定硬件(Ascend NPU)上完全异步训练的问题,涉及环境变量和补丁应用,与本PR的AMD平台修复有相似之处。
- PR #6052 [fully_async] fix: avoid blocking ray.get inside async actor methods: 同属完全异步训练模块的bugfix,关注异步通信和事件循环问题,与本PR的ZMQ通信改进相关。
参与讨论