本文档为算法工程团队在强化学习训练场景中,使用分布式强化学习框架(veRL+Ray)提供基于火山引擎 VKE 集群的多节点训练环境部署方案,详细指导集群配置、存储卷创建、KubeRay 与 RayCluster 部署等步骤,并以 PPO 训练任务(Quickstart: PPO training on GSM8K dataset)为例提供数据集处理、任务提交及监控的全流程操作。
本文使用的训练框架是 veRL(Volcano Engine Reinforcement Learning for LLMs),是云平台针对大语言模型(LLMs)的强化学习框架。它不仅具备灵活、高效且可扩展的强化学习训练,还提供了名为“HybridEngine”的框架,该框架将控制流(在单进程中运行)与计算流(在分布式多进程中运行)分离,并且可以支持多种计算后端、训练与推理并行策略和性能优化技术。
准备好数据集和模型文件,具体可参考 Quickstart: PPO training on GSM8K dataset 中的说明。
下载 Qwen2.5-0.5B-Instruct 模型到本地。
# 需要先安装好git-lfs,如:apt install git-lfs(Ubuntu), brew install git-lfs(MacOS) git lfs install git clone https://www.modelscope.cn/Qwen/Qwen2.5-0.5B-Instruct.git
在本地下载并预处理 gsm8k 数据集文件,需在配置好 verl 的环境下进行,请参考 Installation。
git clone https://github.com/volcengine/verl cd verl python3 examples/data_preprocess/gsm8k.py --local_dir ~/data/gsm8k
在容器服务中创建集群,需要注意以下列举的参数配置。其余参数说明和详细的操作步骤,请参见 创建集群。
配置项 | 说明 |
---|---|
集群配置 | |
Kubernetes 版本 | 选择 v1.28 及以上版本。 |
容器网络模型 | 选择 VPC-CNI。 |
节点池配置 | |
托管节点池 | 开启 托管节点池。 |
计算规格 | 根据任务需要选择节点池 GPU 实例规格,本文以 ecs.gni3cl.11xlarge(44vCPU 240GiB NVIDIA L20*2) 为例。 |
节点数量 | 2 个。 |
数据盘 | 数据盘规格:选择 1T 极速型 SSD。 |
组件配置 | |
平台功能组件 | 在集群中安装如下组件:
|
配置项 | 说明 |
---|---|
存储类型 | 选择 对象存储。 |
访问密钥 | 单击 创建秘钥,完成秘钥配置。其中 AccessKey ID 和 AccessKey Secret 为您火山引擎账号的 AK/SK。获取方法,请参见 API 访问密钥管理。 |
存储桶 | 选择已创建的 TOS 桶。 |
子目录 | 默认/ 即可。 |
参数 | 说明 |
---|---|
基本信息 | |
名称 | 设置应用名称,同一个命名空间里名称必须唯一。 |
项目 | 选择需要部署应用的项目。默认选择 default(默认项目)。更多项目相关操作和说明,请参见 项目管理。 |
集群 | 选择需要部署应用的目标集群。 |
命名空间 | 选择目标集群下的命名空间。 |
编排模板 | |
Chart 来源 | 选择应用的 Chart 来源。默认来源为 应用模板,不可配置。 |
Chart | 选择应用的 Chart 版本,建议选择最新版本。 |
配置方式 | 选择应用的配置方法。每个应用模板的 values.yaml 文件配置方法不同,详细说明,请参见 values.yaml 文件中的注释说明。单击 ![]() |
nvidia.com/gpu
为节点 GPU 数量,cpu
和memory
建议在预留部分资源给其他系统组件的情况下,尽量多分配,比如分配节点资源的 80%。requests
和limits
的值修改为一样即可。persistentVolumeClaim.claimName
配置为已创建的 PVC 名称。rayStartParams
中tracing-startup-hook
为启动深度观测需要,启动脚本已提供在/workspace/verl/tracing/setup_tracing.py
中,只需要执行一下几步就可完成深度观测的启动。(样例配置已包含以下配置,可直接使用)。
tracing-startup-hook: "tracing.setup_tracing:setup_tracing"
PYTHONPATH=/workspace/verl
就能够完成深度观测的启动,数据会自动上报到 APMPlus。instrumentation.apmplus.volcengine.com/inject-python: "true"
。OTEL_PYTHON_DISABLED_INSTRUMENTATIONS=system_metrics
。apiVersion: ray.io/v1 kind: RayCluster metadata: name: raycluster-kuberay namespace: default spec: headGroupSpec: serviceType: ClusterIP rayStartParams: dashboard-host: "0.0.0.0" tracing-startup-hook: "tracing.setup_tracing:setup_tracing" template: metadata: annotations: instrumentation.apmplus.volcengine.com/inject-python: "true" spec: containers: - name: ray-head image: ai-containers-cn-beijing.cr.volces.com/deeplearning/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0-custom-v1 imagePullPolicy: IfNotPresent resources: limits: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 requests: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 volumeMounts: - name: log-volume mountPath: /tmp/ray - name: tos-pv mountPath: /data env: - name: PYTHONPATH value: /workspace/verl - name: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS value: system_metrics volumes: - name: log-volume emptyDir: {} - name: tos-pv persistentVolumeClaim: claimName: verl-pvc workerGroupSpecs: - groupName: workergroup replicas: 1 minReplicas: 1 maxReplicas: 3 rayStartParams: {} template: metadata: annotations: instrumentation.apmplus.volcengine.com/inject-python: "true" spec: containers: - name: ray-worker image: ai-containers-cn-beijing.cr.volces.com/deeplearning/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0-custom-v1 imagePullPolicy: IfNotPresent resources: limits: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 requests: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 volumeMounts: - name: log-volume mountPath: /tmp/ray - name: tos-pv mountPath: /data env: - name: PYTHONPATH value: /workspace/verl - name: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS value: system_metrics volumes: - name: log-volume emptyDir: {} - name: tos-pv persistentVolumeClaim: claimName: verl-pvc
在左侧导航栏中选择 工作负载 > 容器组,查看 head 和 worker 的状态(容器组名称分别为raycluster-kuberay-head-xxxx
和raycluster-kuberay-workergroup-worker-xxxx
),显示Running
即完成启动,这里花费时间相对较长。
cd /workspace/verl ray job submit --address="http://127.0.0.1:8265" \ --runtime-env=verl/trainer/runtime_env.yaml \ --no-wait \ -- \ python3 -m verl.trainer.main_ppo \ data.train_files=/data/data/gsm8k/train.parquet \ data.val_files=/data/data/gsm8k/test.parquet \ data.train_batch_size=256 \ data.max_prompt_length=512 \ data.max_response_length=256 \ actor_rollout_ref.model.path=/data/Qwen2.5-0.5B-Instruct \ actor_rollout_ref.actor.optim.lr=1e-6 \ actor_rollout_ref.actor.ppo_mini_batch_size=64 \ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \ actor_rollout_ref.rollout.tensor_model_parallel_size=1 \ actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \ critic.optim.lr=1e-5 \ critic.model.path=/data/Qwen2.5-0.5B-Instruct \ critic.ppo_micro_batch_size_per_gpu=4 \ algorithm.kl_ctrl.kl_coef=0.001 \ trainer.logger=['console'] \ trainer.default_hdfs_dir=null \ trainer.n_gpus_per_node=2 \ trainer.nnodes=2 \ trainer.save_freq=10 \ trainer.test_freq=10 \ trainer.total_epochs=15
训练任务提交成功后,预期输出如下图所示。
创建路由规则,对外暴露 ray dashboard 服务。
在左侧导航栏中选择 服务与路由 > 路由规则,单击 创建路由规则,配置路由规则。
需要注意以下列举的参数配置。其余参数说明和详细的操作步骤,请参见 通过控制台创建 ALB Ingress。
配置项 | 说明 |
---|---|
Ingress类型 | 选择 应用型负载均衡 ALB |
协议 | 选择 HTTP。 |
路径 | 配置为/ 。 |
服务 | 配置为raycluster-kuberay-head-xxxxxx:8265 。 |
创建完成后,在浏览器地址栏直接输入路由规则的 VIP,即可进入 dashboard,查看训练任务日志和集群状态。
默认情况下,训练出的 Checkpoint 保存在 raycluster-kuberay-head-xxxx 容器组的/tmp/ray/session_xxxxx/runtime_resources/working_dir_files/_ray_pkg_xxxxx/checkpoints/verl_examples/gsm8k
目录中。
登录 容器服务控制台。
在左侧导航栏中选择 云原生观测 > 概览,按照 接入指引,开启云原生观测和容器监控。包括:
开启云原生观测。
绑定托管 Prometheus 工作区。
安装Prometheus-agent 组件。
说明
更多配置详情,请参考 开启观测。
创建采集配置。
整体采集架构如下图所示,主要采集 Ray head node 和 worker node 的指标。
首先请确认集群里是否已经创建 ray 的 PodMonitor,如果没有需要创建。
确认方式:在容器服务控制台左侧导航栏中,选择 工作负载 > 对象浏览器,搜索 podmonitor
,点击进入后,搜索是否有 ray 相关的采集配置。
说明
即使已经创建了 PodMonitor,仍然需要确认上述两个 PodMonitor 资源上 必须 包含 label volcengine.vmp: "true"
,如果没有该 label,不会被 Prometheus-agent 识别,无法采集指标。
如果集群中未创建 ray 的 PodMonitor,请按照以下步骤创建 PodMonitor。
Head node 的采集配置 PodMonitor。
apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: labels: volcengine.vmp: "true" name: ray-head-monitor namespace: default spec: jobLabel: ray-head # Only select Kubernetes Pods in the "default" namespace. namespaceSelector: matchNames: - default # Only select Kubernetes Pods with "matchLabels". selector: matchLabels: ray.io/node-type: head # A list of endpoints allowed as part of this PodMonitor. podMetricsEndpoints: - port: metrics relabelings: - action: replace sourceLabels: - __meta_kubernetes_pod_label_ray_io_cluster targetLabel: ray_io_cluster - port: as-metrics # autoscaler metrics relabelings: - action: replace sourceLabels: - __meta_kubernetes_pod_label_ray_io_cluster targetLabel: ray_io_cluster - port: dash-metrics # dashboard metrics relabelings: - action: replace sourceLabels: - __meta_kubernetes_pod_label_ray_io_cluster targetLabel: ray_io_cluster
Worker Node 的采集配置 PodMonitor。
apiVersion: monitoring.coreos.com/v1 kind: PodMonitor metadata: name: ray-workers-monitor namespace: default labels: volcengine.vmp: "true" spec: jobLabel: ray-workers # Only select Kubernetes Pods in the "default" namespace. namespaceSelector: matchNames: - default # Only select Kubernetes Pods with "matchLabels". selector: matchLabels: ray.io/node-type: worker # A list of endpoints allowed as part of this PodMonitor. podMetricsEndpoints: - port: metrics relabelings: - sourceLabels: [__meta_kubernetes_pod_label_ray_io_cluster] targetLabel: ray_io_cluster
说明
请关注和修改以下配置:
namespace
:PodMonitor 所在的 namespace,理论上可创建在任意的 namespace 里。namespaceSelector
:选择匹配哪些 namespace 下的 ray cluster pod。请修改为 ray 所在的 namespace。采集规则配置完成后,ray 的指标就被采集到托管 Prometheus 工作区了,您可以使用托管 Prometheus 的 Explore 功能查询对应的指标。
您可以使用托管 Prometheus 预置面板查看 ray 监控(ray 监控查看需要开白)。
您也可以使用自建 Grafana 查看 ray 的监控图表。
部署 APMPlus 观测组件。
在 VKE 集群控制台界面,进入组件管理,在栏目选择监控,找到 apmplus-opentelemetry-collector,apmplus-server-agent 两个组件并进行安装。安装好的页面如下图,确认两个组件都是已安装状态且无异常提示。
开启添加深度观测。给 ray 相关实例增加以下注解。
key | value |
---|---|
instrumentation.apmplus.volcengine.com/inject-python | true |
在 RayCluster 部署 yaml 中,添加以下修改(上文 部署 RayCluster 中的样例 yaml 中已包含以下配置,可直接使用)。
apiVersion: ray.io/v1 kind: RayCluster metadata: creationTimestamp: "2025-05-23T05:23:37Z" generation: 2 name: raycluster-kuberay namespace: default resourceVersion: "52584" uid: cdb81f16-e53d-481f-9426-5db7f93155b5 spec: headGroupSpec: rayStartParams: dashboard-host: 0.0.0.0 tracing-startup-hook: ray.util.tracing.setup_local_tmp_tracing:setup_tracing serviceType: ClusterIP template: metadata: annotations: instrumentation.apmplus.volcengine.com/inject-python: "true" spec: containers: - env: - name: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS value: system_metrics - image: cr-helm-test-cn-beijing.cr.volces.com/cr-helm-test/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0-custom-v1 imagePullPolicy: IfNotPresent name: ray-head resources: limits: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 requests: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 volumeMounts: - mountPath: /tmp/ray name: log-volume - mountPath: /data name: tos-pv volumes: - emptyDir: {} name: log-volume - name: tos-pv persistentVolumeClaim: claimName: verl-pvc workerGroupSpecs: - groupName: workergroup maxReplicas: 3 minReplicas: 1 numOfHosts: 1 rayStartParams: {} replicas: 1 template: metadata: annotations: instrumentation.apmplus.volcengine.com/inject-python: "true" spec: containers: - env: - name: OTEL_PYTHON_DISABLED_INSTRUMENTATIONS value: system_metrics - image: cr-helm-test-cn-beijing.cr.volces.com/cr-helm-test/verl:ngc-th2.6.0-cu126-vllm0.8.4-flashinfer0.2.2-cxx11abi0-custom-v1 imagePullPolicy: IfNotPresent name: ray-worker resources: limits: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 requests: cpu: "30" memory: 100Gi nvidia.com/gpu: 2 volumeMounts: - mountPath: /tmp/ray name: log-volume - mountPath: /data name: tos-pv volumes: - emptyDir: {} name: log-volume - name: tos-pv persistentVolumeClaim: claimName: verl-pvc status: availableWorkerReplicas: 1 conditions: - lastTransitionTime: "2025-05-23T07:23:08Z" message: "" reason: HeadPodRunningAndReady status: "True" type: HeadPodReady - lastTransitionTime: "2025-05-23T05:29:39Z" message: All Ray Pods are ready for the first time reason: AllPodRunningAndReadyFirstTime status: "True" type: RayClusterProvisioned - lastTransitionTime: "2025-05-23T05:23:37Z" message: "" reason: RayClusterSuspended status: "False" type: RayClusterSuspended - lastTransitionTime: "2025-05-23T05:23:37Z" message: "" reason: RayClusterSuspending status: "False" type: RayClusterSuspending desiredCPU: "60" desiredGPU: "4" desiredMemory: 200Gi desiredTPU: "0" desiredWorkerReplicas: 1 endpoints: client: "10001" dashboard: "8265" gcs-server: "6379" metrics: "8080" serve: "8000" head: podIP: 192.168.5.38 podName: raycluster-kuberay-head-wcrb7 serviceIP: 192.168.5.38 serviceName: raycluster-kuberay-head-svc lastUpdateTime: "2025-05-23T07:23:24Z" maxWorkerReplicas: 3 minWorkerReplicas: 1 observedGeneration: 2 readyWorkerReplicas: 1 state: ready stateTransitionTimes: ready: "2025-05-23T05:29:39Z"
import ray import logging from opentelemetry import trace from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, ) from opentelemetry.exporter.otlp.proto.http.trace_exporter import ( OTLPSpanExporter as HTTPExporter, ) def setup_tracing() -> None: logging.warning("setup_tracing") trace.set_tracer_provider(TracerProvider()) trace.get_tracer_provider().add_span_processor( BatchSpanProcessor( HTTPExporter() ) )
RayCluster yaml 中 rayStartParams 添加如下参数。
spec: headGroupSpec: serviceType: ClusterIP rayStartParams: dashboard-host: "0.0.0.0" tracing-startup-hook: "tracing.setup_tracing:setup_tracing" template:
开启观测配置后,产生的 tracing 数据会发送到 APMPlus 中,在应用性能监控全链路版云产品服务页面,选择 服务端监控,即可看到 ray-worker 和 ray-header 两个服务。
点击服务名,进入观测看板页面。
Weights & Biases(简称 Weights & Biases,缩写为 wandb) 是一个用于机器学习实验跟踪与管理的工具平台,旨在帮助数据科学家和机器学习工程师更高效地开发、优化和部署模型。它提供了一套简洁的工具,可自动记录实验过程中的关键指标、可视化结果、管理代码版本和数据集,并支持团队协作。
本节介绍如何在上述集群中部署本地版 wandb,并将 veRL 的训练指标推送到 wandb 中。
准备 wandb local 镜像,由于中国大陆地区可能出现无法访问 Docker Hub 的情况,有两种解决方法:
部署 wandb local
apiVersion: apps/v1 kind: Deployment metadata: name: wandb labels: app: wandb spec: strategy: type: RollingUpdate replicas: 1 selector: matchLabels: app: wandb template: metadata: labels: app: wandb spec: initContainers: - name: init-permissions image: cr-helm-test-cn-beijing.cr.volces.com/cr-helm-test/wandb-local:0.68.2 command: ["sh", "-c", "chmod 777 /vol"] securityContext: runAsUser: 0 volumeMounts: - name: wandb-pv mountPath: /vol containers: - name: wandb imagePullPolicy: IfNotPresent image: cr-helm-test-cn-beijing.cr.volces.com/cr-helm-test/wandb-local:0.68.2 ports: - name: http containerPort: 8080 protocol: TCP volumeMounts: - name: wandb-pv mountPath: /vol livenessProbe: httpGet: path: /healthz port: http readinessProbe: httpGet: path: /ready port: http resources: requests: cpu: "2000m" memory: 4G limits: cpu: "4000m" memory: 8G volumes: - name: wandb-pv persistentVolumeClaim: claimName: wandb-pvc --- apiVersion: v1 kind: Service metadata: name: wandb-service spec: type: ClusterIP selector: app: wandb ports: - protocol: TCP port: 8080 targetPort: 8080
创建路由规则,对外暴露 wandb 服务。
配置项 | 说明 |
---|---|
Ingress类型 | 选择 应用型负载均衡 ALB。 |
协议 | 选择 HTTP。 |
路径 | 输入 / 。 |
服务 | 选择 wandb-service:8080。 |
创建完成后,在浏览器地址栏输入路由规则的 VIP,即可进入 wandb 管理页面。
verl/trainer/runtime_env.yaml
文件内容(可以使用 vim 或者 nano 编辑),添加WANDB_BASE_URL
和WANDB_API_KEY
配置,其中WANDB_API_KEY
填写为您的 API KEY。working_dir: ./ excludes: ["/.git/"] env_vars: TORCH_NCCL_AVOID_RECORD_STREAMS: "1" # If you are using vllm<=0.6.3, you might need to set the following environment variable to avoid bugs: # VLLM_ATTENTION_BACKEND: "XFORMERS" WANDB_BASE_URL: "http://wandb-service:8080" WANDB_API_KEY: "local-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
trainer.logger
添加'wandb'
。rainer.project_name
填写之前 wandb 中创建好的 project name。trainer.experiment_name
填写一个实验名称。ray job submit --address="http://127.0.0.1:8265" \ --runtime-env=verl/trainer/runtime_env.yaml \ --no-wait \ -- \ python3 -m verl.trainer.main_ppo \ data.train_files=/data/data/gsm8k/train.parquet \ data.val_files=/data/data/gsm8k/test.parquet \ data.train_batch_size=256 \ data.max_prompt_length=512 \ data.max_response_length=256 \ actor_rollout_ref.model.path=/data/Qwen2.5-0.5B-Instruct \ actor_rollout_ref.actor.optim.lr=1e-6 \ actor_rollout_ref.actor.ppo_mini_batch_size=64 \ actor_rollout_ref.actor.ppo_micro_batch_size_per_gpu=4 \ actor_rollout_ref.rollout.log_prob_micro_batch_size_per_gpu=8 \ actor_rollout_ref.rollout.tensor_model_parallel_size=1 \ actor_rollout_ref.rollout.gpu_memory_utilization=0.4 \ actor_rollout_ref.ref.log_prob_micro_batch_size_per_gpu=4 \ critic.optim.lr=1e-5 \ critic.model.path=/data/Qwen2.5-0.5B-Instruct \ critic.ppo_micro_batch_size_per_gpu=4 \ algorithm.kl_ctrl.kl_coef=0.001 \ trainer.default_hdfs_dir=null \ trainer.n_gpus_per_node=2 \ trainer.nnodes=2 \ trainer.save_freq=10 \ trainer.test_freq=10 \ trainer.total_epochs=15 \ trainer.logger=['console','wandb'] \ trainer.project_name='verl_example_gsm8k' \ trainer.experiment_name='experiment1'