零基础也能行:verl + ROCm训练全流程
1. 引言:为什么选择 verl 与 ROCm 组合?
大型语言模型(LLM)的后训练阶段,尤其是基于强化学习(Reinforcement Learning, RL)的对齐训练,正变得日益关键。然而,传统RLHF(Reinforcement Learning from Human Feedback)框架在扩展性、效率和工程复杂度上存在显著瓶颈。verl作为字节跳动火山引擎团队开源的生产级强化学习训练框架,正是为解决这些问题而生。
verl 是 HybridFlow 论文的官方实现,专为 LLM 后训练设计,具备高吞吐、低通信开销、模块化集成等核心优势。更重要的是,它支持与 vLLM、FSDP、Megatron-LM 等主流推理与训练框架无缝对接,极大降低了部署门槛。
与此同时,随着 AMD MI300 系列 GPU 在高性能计算领域的崛起,基于ROCm平台进行大规模模型训练成为极具性价比的选择。本文将带你从零开始,完整走通verl + ROCm的多节点分布式训练全流程,涵盖环境准备、容器构建、Ray集群搭建、数据预处理到最终启动PPO训练的每一个关键步骤。
无论你是刚接触强化学习的新手,还是希望在AMD硬件上落地高效训练的工程师,本文都能提供可直接复用的实践路径。
2. verl 核心特性解析
2.1 模块化架构与灵活扩展
verl 采用模块化设计,将 Actor、Critic、Rollout、Reference 模型解耦,允许用户独立配置每个组件的并行策略、设备映射和优化方式。这种设计使得:
- 可以使用不同并行策略运行 Rollout 和 Training;
- 支持 FSDP、TP(Tensor Parallelism)、vLLM 推理后端等多种组合;
- 易于接入 HuggingFace 模型,无需修改模型结构。
2.2 高效的 3D-HybridEngine
verl 的核心性能优势来自其3D-HybridEngine,该引擎实现了以下关键优化:
- Actor 模型重分片(Resharding):在 Rollout 和 Training 阶段之间自动进行模型参数的重新分布,避免冗余副本,减少通信量。
- 内存复用机制:通过精确控制计算图中的张量生命周期,最大化 GPU 内存利用率。
- 异步流水线调度:利用 Ray 的分布式任务调度能力,实现生成、打分、训练阶段的高效重叠。
2.3 多后端支持与生产就绪
verl 支持多种训练与推理后端: -训练后端:PyTorch FSDP、ZeRO -推理后端:vLLM(推荐)、HuggingFace Transformers
这使得 verl 能够灵活适应不同规模的集群和硬件配置,真正达到“生产可用”标准。
3. 环境准备与镜像构建
3.1 前置条件
确保你的集群满足以下要求: - 使用 Slurm 作业调度系统 - 节点间通过 InfiniBand 或高速以太网连接 - 安装 ROCm 6.2+(建议 rocm6.2_mi300_ubuntu20.04) - 已安装 Docker 或 Podman(支持 GPU 容器化)
3.2 构建 ROCm 兼容镜像
首先创建Dockerfile.rocm文件:
FROM rocm/vllm:rocm6.2_mi300_ubuntu20.04_py3.9_vllm_0.6.4 # 设置工作目录 WORKDIR /workspace # 安装 verl 依赖 RUN pip install --no-cache-dir torch==2.3.0+rocm6.1 torchvision==0.18.0+rocm6.1 torchaudio==2.3.0+rocm6.1 \ -f https://download.pytorch.org/whl/torch_stable.html # 安装 ray RUN pip install "ray[default]==2.40.0" debugpy>=1.8.0 # 克隆 verl 仓库 RUN git clone https://github.com/volcengine/verl.git . RUN pip install -e . # 设置缓存路径 ENV TRANSFORMERS_CACHE=/root/.cache/huggingface ENV HF_HOME=/root/.cache/huggingface构建镜像:
docker build -f Dockerfile.rocm -t verl.rocm .4. 多节点训练全流程实战
4.1 Slurm 作业脚本详解
以下是完整的slurm_script.sh脚本,已根据最佳实践优化:
#!/bin/bash #SBATCH --job-name=verl-ray-on-slurm #SBATCH --nodes=2 #SBATCH --ntasks-per-node=2 #SBATCH --mem=200G #SBATCH --time=30-00:00:00 #SBATCH --gpus-per-node=8 #SBATCH --cpus-per-task=28 #SBATCH --output=../verl_log/slurm-%j.out #SBATCH --error=../verl_log/slurm-%j.err #SBATCH --nodelist=gpu-[0,1] # 加载必要模块(根据实际环境调整) module load rocm/6.2.0 # 项目配置 CONTAINER_NAME="multinode_verl_training" IMG="verl.rocm" DOCKERFILE="Dockerfile.rocm" verl_workdir="${HOME}/projects/verl_upstream" export TRANSFORMERS_CACHE="${HOME}/.cache/huggingface" export HF_HOME=$TRANSFORMERS_CACHE # ROCm & NCCL 网络优化设置 export NCCL_DEBUG=INFO export GPU_MAX_HW_QUEUES=2 export TORCH_NCCL_HIGH_PRIORITY=1 export NCCL_CHECKS_DISABLE=1 export NCCL_IB_HCA=mlx5_0,mlx5_1,mlx5_2,mlx5_3,mlx5_4,mlx5_5,mlx5_8,mlx5_9 export NCCL_IB_GID_INDEX=3 export NCCL_CROSS_NIC=0 export CUDA_DEVICE_MAX_CONNECTIONS=1 export NCCL_PROTO=Simple export RCCL_MSCCL_ENABLE=0 export TOKENIZERS_PARALLELISM=false export HSA_NO_SCRATCH_RECLAIM=1 # GPU 可见性设置 export HIP_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 export ROCR_VISIBLE_DEVICES=$HIP_VISIBLE_DEVICES export CUDA_VISIBLE_DEVICES=$HIP_VISIBLE_DEVICES # 构建并启动容器 srun bash -c " set -e docker image prune -f || true if ! docker images --format '{{.Repository}}:{{.Tag}}' | grep -q '${IMG}'; then echo 'Building ${IMG} image...' docker build -f '${DOCKERFILE}' -t '${IMG}' . else echo '${IMG} image already exists.' fi docker rm '${CONTAINER_NAME}' 2>/dev/null || true ibdev2netdev docker run --rm -d \ -e HYDRA_FULL_ERROR=1 \ -e HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES} \ -e ROCR_VISIBLE_DEVICES=${ROCR_VISIBLE_DEVICES} \ -e CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES} \ -e NCCL_DEBUG=${NCCL_DEBUG} \ -e TRANSFORMERS_CACHE=${TRANSFORMERS_CACHE} \ -e HF_HOME=${HF_HOME} \ --network host \ --device /dev/dri \ --device /dev/kfd \ --device /dev/infiniband \ --group-add video \ --cap-add SYS_PTRACE \ --security-opt seccomp=unconfined \ --privileged \ -v \${HOME}:\${HOME} \ -v \${HOME}/.ssh:/root/.ssh \ -w \"${verl_workdir}\" \ --shm-size 128G \ --name \"${CONTAINER_NAME}\" \ \"${IMG}\" \ tail -f /dev/null echo 'Container setup completed' " # 获取节点信息 nodes_array=($(scontrol show hostnames "$SLURM_JOB_NODELIST")) head_node=${nodes_array[0]} head_node_ip=$(srun --nodes=1 --ntasks=1 -w "$head_node" hostname --ip-address) if [[ "$head_node_ip" == *" "* ]]; then IFS=' ' read -ra ADDR <<<"$head_node_ip" head_node_ip=${ADDR[1]:-${ADDR[0]}} fi port=6379 ip_head=$head_node_ip:$port export ip_head echo "Head Node IP: $ip_head" # 启动 Ray Head 节点 srun --nodes=1 --ntasks=1 -w "$head_node" \ docker exec "${CONTAINER_NAME}" \ ray start --head --node-ip-address="$head_node_ip" --port=$port \ --dashboard-port=8266 \ --num-cpus "${SLURM_CPUS_PER_TASK}" --num-gpus "${SLURM_GPUS_PER_NODE}" --block & sleep 10 # 启动 Worker 节点 worker_num=$((SLURM_JOB_NUM_NODES - 1)) for ((i = 1; i <= worker_num; i++)); do node_i=${nodes_array[$i]} echo "Starting worker on $node_i" srun --nodes=1 --ntasks=1 -w "$node_i" \ docker exec "${CONTAINER_NAME}" \ ray start --address "$ip_head" \ --num-cpus "${SLURM_CPUS_PER_TASK}" --num-gpus "${SLURM_GPUS_PER_NODE}" --block & sleep 5 done # 验证 Ray 集群状态 echo "Testing Ray cluster..." docker exec "${CONTAINER_NAME}" python3 -c ' import ray try: ray.init(address="auto") print("=== Ray Cluster Info ===") print(f"Total nodes: {len(ray.nodes())}") for node in ray.nodes(): print(f"Node: {node["NodeManagerHostname"]}, Alive: {node["Alive"]}") ray.shutdown() except Exception as e: print(f"Ray init failed: {e}") '4.2 数据预处理
在训练前需准备训练数据集(如 GSM8K、MATH):
docker exec "${CONTAINER_NAME}" \ python3 examples/data_preprocess/gsm8k.py --local_dir ../data/gsm8k docker exec "${CONTAINER_NAME}" \ python3 examples/data_preprocess/math_dataset.py --local_dir ../data/math4.3 模型加载验证
测试 HuggingFace 模型是否可正常加载:
docker exec "${CONTAINER_NAME}" \ python3 -c "from transformers import pipeline; pipeline('text-generation', model='Qwen/Qwen2-7B-Instruct')"4.4 启动 PPO 训练任务
最后提交 PPO 训练作业:
train_files="../data/gsm8k/train.parquet" val_files="../data/gsm8k/test.parquet" MODEL_PATH="Qwen/Qwen2-7B-Instruct" PYTHONUNBUFFERED=1 srun --overlap --nodes=${SLURM_NNODES} --ntasks=1 -w "$head_node" \ docker exec "${CONTAINER_NAME}" \ python3 -m verl.trainer.main_ppo \ data.train_files=$train_files \ data.val_files=$val_files \ data.train_batch_size=1024 \ data.max_prompt_length=1024 \ data.max_response_length=1024 \ actor_rollout_ref.model.path=$MODEL_PATH \ actor_rollout_ref.actor.optim.lr=1e-6 \ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=16 \ actor_rollout_ref.rollout.tensor_model_parallel_size=2 \ actor_rollout_ref.rollout.name=vllm \ actor_rollout_ref.rollout.gpu_memory_utilization=0.9 \ critic.optim.lr=1e-5 \ critic.model.path=$MODEL_PATH \ algorithm.kl_ctrl.kl_coef=0.0001 \ trainer.logger=['console','wandb'] \ trainer.project_name='verl_example' \ trainer.experiment_name='Qwen2-7B-Instruct_gsm8k' \ trainer.n_gpus_per_node=${SLURM_GPUS_PER_NODE} \ trainer.nnodes=${SLURM_NNODES} \ trainer.total_epochs=155. 常见问题与调试建议
5.1 Ray 集群无法连接
- 检查防火墙是否开放
6379(GCS)和8266(Dashboard)端口 - 确保所有节点时间同步(NTP)
- 使用
ibdev2netdev验证 InfiniBand 设备状态
5.2 ROCm 运行时错误
- 确认内核驱动版本与 ROCm 匹配
- 检查
/opt/rocm路径是否存在且权限正确 - 使用
rocminfo和rocm-smi验证 GPU 识别状态
5.3 性能调优建议
| 参数 | 推荐值 | 说明 |
|---|---|---|
rollout.gpu_memory_utilization | 0.9~0.95 | 提高 vLLM 推理显存利用率 |
ppo_micro_batch_size_per_gpu | 8~16 | 根据模型大小调整 |
tensor_model_parallel_size | 2 或 4 | 大模型建议开启 TP |
fsdp_config.param_offload | True(Ref/Critic) | 节省内存 |
获取更多AI镜像
想探索更多AI镜像和应用场景?访问 CSDN星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。