Skip to content

在 AIStudio 训练任务中使用 RLinf 复现 EmbodiChain CartPole

如果您第一次在 AIStudio 上运行 RLinf,可以从 EmbodiChain CartPole 开始。这个场景使用低维状态输入和 MLP actor-critic,不需要提前准备预训练模型或静态训练数据,适合先熟悉镜像、共享存储、启动命令和训练结果之间的关系。

本文先准备镜像、固定版本的 RLinf 代码和 CartPole 资源,再创建一个单 Worker、单 GPU 的 PPO 训练任务。任务结束后,通过完整轨迹、PPO 更新和共享存储中的输出判断训练是否正常完成。

实战目标

本文会在 1 张 GPU 上运行一个简短的 CartPole PPO 训练任务。运行结束后,检查是否完成至少 2 条轨迹、一次 PPO 更新,并确认输出已写入共享存储。

以下结果全部出现时,说明任务运行成功:

  1. AIStudio 任务和启动日志记录相同的完整镜像 tag,RLinf 提交和资源 SHA-256 与准备记录一致。
  2. 启动命令从 Worker 检测驱动并输出且只输出一条 NVIDIA_DRIVER_VERSION=<version>;Worker 只看到 1 张 GPU,/dev/shm 不小于 32 GiB。
  3. CartPoleSimResources 从固定共享存储目录加载,任务运行期间不下载代码或资源。
  4. RLinf 在一个 Worker 内启动本地 Ray,actor、rollout 和 environment 组件均放置到逻辑 GPU 0
  5. num_trajectories 的最大值不小于 2,并且至少完成一次 PPO 优化器更新;TensorBoard 中的 env/rollout/train/ 标量为有限值。
  6. 任务退出码为 0,任务结束后仍可从共享存储读取运行清单、启动日志、Hydra 配置和 TensorBoard 文件。

场景信息

创建任务时使用以下设置。

  • 示例可用区:宁夏 B。您可以选择其他可用区,但镜像、GPU 资源和共享存储必须位于同一可用区。

  • RLinf

    • commit:3cb54c2eb2e355378ba049d50f7ec0cb8acc2da0
    • 配置:tests/e2e_tests/embodied/embodichain_ppo_cart_pole.yaml
    • 入口:examples/embodiment/train_embodied_agent.py
    • 要求:检出指定 commit,并保持代码不变。
  • 上游镜像rlinf/rlinf:agentic-rlinf0.4-embodichain,来自 Docker Hub。

  • AIStudio 任务镜像cr.infini-ai.com/<tenant-id>/rlinf:agentic-rlinf0.4-embodichain。先把上游镜像上传或导入自己的租户镜像仓库。

  • 仿真资产CartPoleSimResources,以及快照元数据和 SHA-256 清单。每次任务校验同一固定目录,不在 Worker 中下载。

  • 资源来源:Spot 资源。中断后使用新 RUN_ID 完整重跑,不恢复进度。

  • 分布式框架:单机;RLinf 在 Worker 内启动本地 Ray,不使用 AIStudio 托管 Ray。

  • Worker 拓扑:1 个 Worker、1 个节点、1 张 A100-SXM4-80GB。其他 GPU SKU 需要单独验证。

  • 共享存储:目标可用区内的共享高性能存储;在实际挂载点下使用 rlinf-reproduction/ 工作目录。固定输入只读使用,runs/ 写入持久化输出。

  • 训练范围:2 个训练环境、2 个评估环境、每轮最多 8 步、2 个 PPO 更新轮次。此范围用于快速检查运行链路,不评估训练收敛。

本场景在 AIStudio 中怎样运行

AIStudio 创建 1 个 Worker、挂载共享存储并执行启动命令。RLinf 在这个 Worker 内启动本地 Ray,并运行 actor、rollout 和 environment 组件。

  • 单机(本场景选择):AIStudio 创建 1 个 Worker,不创建托管 Ray 集群。RLinf 在这个 Worker 内启动本地 Ray。Worker 数量为 1 时,创建页只提供“单机”选项。
  • Ray(本场景不选择):AIStudio 创建托管 Ray Head 和 Worker,启动命令在 Head 中运行。该模式用于多 Worker 场景,并会改变启动入口、网络检查和日志位置。

RLinf 使用本地 Ray 不要求 AIStudio 选择 Ray。本场景的实际拓扑如下:

language-text
AIStudio 单机训练任务
└── Worker 0 / GPU 0
    └── RLinf 本地 Ray
        ├── Driver
        ├── ActorGroup
        ├── RolloutGroup
        └── EnvGroup / EmbodiChain CartPole

Worker 从共享存储读取固定版本的代码和资源,并把日志、Hydra 配置和 TensorBoard 文件写入本次 RUN_ID 的输出目录。AIStudio 分布式框架的选择方法见使用 Spot 资源提交训练任务

开始前准备

创建 GPU 任务前,使用 AICoder 或开发机准备镜像、RLinf 代码、EmbodiChain 资源和启动脚本。本文使用宁夏 B;您也可以选择其他可用区,但镜像、任务和共享存储需要位于同一可用区。

在目标可用区准备容器镜像

本场景使用 Docker Hub 镜像 rlinf/rlinf:agentic-rlinf0.4-embodichain

重要

请先上传到自己的镜像仓库

下面的地址属于本示例验证时使用的私有租户,不是公共镜像地址,不能直接用于其他租户。请先把上游镜像上传或导入自己所选可用区的镜像仓库,再将地址中的 te-b905754427352261 替换为自己的 <tenant-id>

language-text
cr.infini-ai.com/te-b905754427352261/rlinf:agentic-rlinf0.4-embodichain

在镜像中心确认自己的 tag 位于任务所在可用区,并且“可用服务”包含任务。本文不展开镜像下载、导入或上传步骤;具体操作请查看准备并验证容器镜像

在共享存储中准备固定版本的 RLinf 代码

在已挂载共享存储的 AICoder 或开发机中执行以下命令。先把 STORAGE_MOUNT 替换为页面显示的容器内路径,例如 /mnt/user_dir/your-user-name。命令不会覆盖已有的代码目录。

language-shell
set -euo pipefail

export STORAGE_MOUNT=/mnt/user_dir/your-user-name
export WORK_ROOT="$STORAGE_MOUNT/rlinf-reproduction"
export RLINF_COMMIT=3cb54c2eb2e355378ba049d50f7ec0cb8acc2da0
export RLINF_ROOT="$WORK_ROOT/code/$RLINF_COMMIT"

mkdir -p "$WORK_ROOT"
findmnt -T "$WORK_ROOT"
df -hT "$WORK_ROOT"
test -w "$WORK_ROOT"

mkdir -p "$WORK_ROOT/code"
test ! -e "$RLINF_ROOT" || {
  echo "RLinf target already exists: $RLINF_ROOT" >&2
  exit 1
}

git clone https://github.com/RLinf/RLinf.git "$RLINF_ROOT"
git -C "$RLINF_ROOT" checkout --detach "$RLINF_COMMIT"

test "$(git -C "$RLINF_ROOT" rev-parse HEAD)" = "$RLINF_COMMIT"
test -z "$(git -C "$RLINF_ROOT" status --porcelain)"
test -r "$RLINF_ROOT/tests/e2e_tests/embodied/embodichain_ppo_cart_pole.yaml"

训练任务不会自动同步代码。后续 GPU 启动命令只读取并校验这个固定版本,不执行 git clonegit fetch 或网络下载。代码、数据和输出目录的一般准备方法见准备代码、数据与输出目录

固定 CartPole 和 SimResources

使用同一镜像创建一个临时开发机,挂载同一共享存储,然后下载 EmbodiChain 资源并生成校验清单。

language-shell
set -euo pipefail

export STORAGE_MOUNT=/mnt/user_dir/your-user-name
export WORK_ROOT="$STORAGE_MOUNT/rlinf-reproduction"
export ASSET_ROOT="$WORK_ROOT/assets/embodichain/cartpole-v1"
export EMBODICHAIN_DATA_ROOT="$ASSET_ROOT"

set +u
source /opt/venv/embodichain/bin/activate
set -u
test ! -e "$ASSET_ROOT" || {
  echo "Asset target already exists: $ASSET_ROOT" >&2
  exit 1
}
mkdir -p "$ASSET_ROOT"

python -m embodichain.data download --name CartPole
python -m embodichain.data download --name SimResources

python -m pip show embodichain > "$ASSET_ROOT/embodichain-package.txt"
(
  cd "$ASSET_ROOT"
  find . -type f \
    ! -name SHA256SUMS \
    ! -name SHA256SUMS.sha256 \
    ! -name SNAPSHOT.env \
    -print0 \
    | sort -z \
    | xargs -0 sha256sum > SHA256SUMS

  ASSET_MANIFEST_SHA256="$(sha256sum SHA256SUMS | awk '{ print $1 }')"
  EMBODICHAIN_VERSION="$(
    python -c 'import importlib.metadata; print(importlib.metadata.version("embodichain"))'
  )"

  {
    printf 'ASSET_SNAPSHOT_ID=cartpole-v1\n'
    printf 'ASSET_NAMES=CartPole,SimResources\n'
    printf 'EMBODICHAIN_VERSION=%s\n' "$EMBODICHAIN_VERSION"
    printf 'ASSET_MANIFEST_SHA256=%s\n' "$ASSET_MANIFEST_SHA256"
  } > SNAPSHOT.env
  printf '%s  SHA256SUMS\n' "$ASSET_MANIFEST_SHA256" > SHA256SUMS.sha256
)

test -s "$ASSET_ROOT/SHA256SUMS"
test -s "$ASSET_ROOT/SHA256SUMS.sha256"
test -s "$ASSET_ROOT/SNAPSHOT.env"
(
  cd "$ASSET_ROOT"
  sha256sum -c SHA256SUMS.sha256
  sha256sum -c SHA256SUMS
  cat SNAPSHOT.env
)

SHA256SUMS 中的每个文件都返回 OK 后,资源便已保存到 ASSET_ROOT。后续任务会读取同一目录并再次校验清单。

准备短时训练配置、启动脚本和输出目录

本文读取固定版本中的 tests/e2e_tests/embodied/embodichain_ppo_cart_pole.yaml,并在运行时使用以下参数:

  • train 和 eval 各使用 2 个环境;
  • train 和 eval 的 max_steps_per_rollout_epoch 均为 8,与 max_episode_steps: 8 对齐;
  • PPO 使用 2 次 update epoch;
  • rollout.enable_cuda_graph=false
  • 输出写入 $WORK_ROOT/runs/cartpole/<RUN_ID>/
  • 启动脚本保存为 $WORK_ROOT/fixtures/cartpole/bounded-v1/launch.sh

RUN_ID 必须由字母、数字、点、下划线或连字符组成,并且每次运行使用新值。启动脚本会拒绝复用已有输出目录。

固定 Hydra 工作目录和持久化输出

Hydra 配置和 RLinf 训练输出使用不同目录,但都保存在本次 RUN_ID 对应的共享存储路径中。

设置本场景值结果
hydra.job.chdirfalse保持 RLinf 进程工作目录稳定
hydra.run.dir$OUTPUT_DIR/hydra保存 Hydra 日志和本次运行信息
hydra.output_subdir.hydra保留 config.yamlhydra.yamloverrides.yaml
runner.logger.log_path$OUTPUT_DIR把 TensorBoard 和 RLinf 解析后配置写入持久化目录

确认运行条件

本文使用宁夏 B 的 A100-SXM4-80GB。请在自己的账号中选择可用且符合以下要求的资源:

  • 任务可以挂载已准备的共享存储,并能通过实际 WORK_ROOT 读取代码和资源、写入 runs/
  • Worker 可以提供至少 32 GiB /dev/shm
  • 镜像中存在 /opt/venv/embodichain
  • 镜像、任务和共享存储位于同一可用区。

运行这个场景

按以下步骤校验输入、创建任务并检查结果。

Step 1 使用 AICoder 校验共享存储中的固定输入

分配 GPU 前,在挂载同一共享存储的 AICoder 或开发机中执行以下命令:

language-shell
set -euo pipefail

export STORAGE_MOUNT=/mnt/user_dir/your-user-name
export WORK_ROOT="$STORAGE_MOUNT/rlinf-reproduction"
export RLINF_COMMIT=3cb54c2eb2e355378ba049d50f7ec0cb8acc2da0
export RLINF_ROOT="$WORK_ROOT/code/$RLINF_COMMIT"
export ASSET_ROOT="$WORK_ROOT/assets/embodichain/cartpole-v1"

test "$(git -C "$RLINF_ROOT" rev-parse HEAD)" = "$RLINF_COMMIT"
test -z "$(git -C "$RLINF_ROOT" status --porcelain)"
test -r "$RLINF_ROOT/tests/e2e_tests/embodied/embodichain_ppo_cart_pole.yaml"
test -s "$ASSET_ROOT/SHA256SUMS"
test -s "$ASSET_ROOT/SHA256SUMS.sha256"
test -s "$ASSET_ROOT/SNAPSHOT.env"
(
  cd "$ASSET_ROOT"
  sha256sum -c SHA256SUMS.sha256
  sha256sum -c SHA256SUMS
  cat SNAPSHOT.env
)

所有命令退出码为 0,并且资源清单中的每个文件都返回 OK 后,继续创建任务。检查失败时,在 AICoder 或开发机中重新准备资源。GPU 启动命令不会克隆代码或下载资源。

Step 2 创建 1 Worker 单 GPU Spot 训练任务

使用 Spot 资源提交训练任务打开任务创建流程,并填写以下设置。

AIStudio 字段本场景设置
任务类型训练任务
资源类型Spot 资源
可用区和 GPU本文使用宁夏 B、1 张 A100-SXM4-80GB;请在自己的账号中选择符合要求的资源
分布式框架单机
Worker 数量1
镜像上传或导入到自己租户后的 cr.infini-ai.com/<tenant-id>/rlinf:agentic-rlinf0.4-embodichain
自动排队和自动重启不启用

选择完成后,页面的资源摘要应显示所选可用区、Spot、1 个 Worker、1 张 GPU 和单机框架。

Step 3 挂载共享存储并设置唯一 RUN_ID

在任务创建页中完成以下配置:

  • 选择已经保存固定输入的共享高性能存储,并记录页面显示的系统挂载路径;
  • 添加环境变量 WORK_ROOT,值为该系统挂载路径下的 rlinf-reproduction 目录,例如 /mnt/user_dir/your-user-name/rlinf-reproduction
  • 添加环境变量 RUN_ID,例如 cartpole-20260820-01
  • 把 TensorBoard 路径设置为 ${WORK_ROOT}/runs/cartpole/${RUN_ID}/tensorboard

${WORK_ROOT}${RUN_ID} 来自上面添加的任务环境变量,平台会在启动 TensorBoard 前替换它们。具体用法见使用环境变量同步 TensorBoard 日志路径。同一存储卷在 AICoder 和训练任务中的挂载路径可能不同,但两个路径需要指向同一个 rlinf-reproduction/ 目录。每次运行只需设置新的 RUN_ID,无需再修改 TensorBoard 路径。

Step 4 设置包含输入、驱动和 Hydra 检查的启动命令

将以下脚本保存为 $WORK_ROOT/fixtures/cartpole/bounded-v1/launch.sh 并赋予可执行权限。脚本在启动 RLinf 前校验固定输入、实际 NVIDIA 驱动、CUDA、GPU 数量、/dev/shm 和本地 Ray 状态;RLinf 结束后再校验轨迹和持久化文件。

language-shell
#!/bin/bash
set -euo pipefail

: "${WORK_ROOT:?Set WORK_ROOT to the prepared shared-storage directory}"
: "${RUN_ID:?Set RUN_ID in the AIStudio task}"

WORK_ROOT="${WORK_ROOT%/}"
[[ "$WORK_ROOT" = /* && "$WORK_ROOT" != "/" ]] || {
  echo "WORK_ROOT must be an absolute non-root path" >&2
  exit 1
}

[[ "$RUN_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || {
  echo "RUN_ID contains unsupported characters" >&2
  exit 1
}

export WORK_ROOT
# 导入镜像后,把以下 URL 中的租户 ID 替换为自己的 <tenant-id>。
export IMAGE_TAG=cr.infini-ai.com/te-b905754427352261/rlinf:agentic-rlinf0.4-embodichain
export RLINF_COMMIT=3cb54c2eb2e355378ba049d50f7ec0cb8acc2da0
export RLINF_ROOT="$WORK_ROOT/code/$RLINF_COMMIT"
export ASSET_ROOT="$WORK_ROOT/assets/embodichain/cartpole-v1"
export EMBODICHAIN_DATA_ROOT="$ASSET_ROOT"
export OUTPUT_DIR="$WORK_ROOT/runs/cartpole/$RUN_ID"
export HYDRA_DIR="$OUTPUT_DIR/hydra"
export RUN_MANIFEST="$OUTPUT_DIR/run-manifest.txt"
export REPO_PATH="$RLINF_ROOT"
export PYTHONPATH="$RLINF_ROOT:${PYTHONPATH:-}"
export RAY_DEDUP_LOGS=0
export TOKENIZERS_PARALLELISM=false
export PYTHONHASHSEED=1234
export ACTOR_SEED=1234
export TRAIN_ENV_SEED=0
export EVAL_ENV_SEED=10000
export MIN_NUM_TRAJECTORIES=2

test ! -e "$OUTPUT_DIR" || {
  echo "Refusing to reuse OUTPUT_DIR: $OUTPUT_DIR" >&2
  exit 1
}
mkdir -p "$OUTPUT_DIR"

if ! NVIDIA_DRIVER_VERSIONS="$(
  nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits |
    sed '/^[[:space:]]*$/d' |
    sort -u
)"; then
  echo "Unable to detect the NVIDIA driver version" >&2
  exit 1
fi
NVIDIA_DRIVER_VERSION_COUNT="$(
  printf '%s\n' "$NVIDIA_DRIVER_VERSIONS" | wc -l | tr -d '[:space:]'
)"
if [[ -z "$NVIDIA_DRIVER_VERSIONS" || "$NVIDIA_DRIVER_VERSION_COUNT" -ne 1 ]]; then
  printf 'Expected one NVIDIA driver version, detected: %s\n' \
    "${NVIDIA_DRIVER_VERSIONS:-<none>}" >&2
  exit 1
fi

{
  printf 'SCENARIO=embodichain-cartpole-bounded\n'
  printf 'PURPOSE=bounded-functional-validation\n'
  printf 'FIXTURE_REVISION=bounded-v1\n'
  printf 'RUN_SCOPE=bounded\n'
  printf 'NVIDIA_DRIVER_VERSION=%s\n' "$NVIDIA_DRIVER_VERSIONS"
  printf 'IMAGE_TAG=%s\n' "$IMAGE_TAG"
  printf 'RLINF_REMOTE=%s\n' 'https://github.com/RLinf/RLinf.git'
  printf 'RLINF_COMMIT=%s\n' "$RLINF_COMMIT"
  printf 'RLINF_CONFIG=%s\n' 'tests/e2e_tests/embodied/embodichain_ppo_cart_pole.yaml'
  printf 'RLINF_LAUNCHER=%s\n' 'examples/embodiment/train_embodied_agent.py'
  printf 'RUN_ID=%s\n' "$RUN_ID"
  printf 'WORKER_COUNT=1\n'
  printf 'NODE_COUNT=1\n'
  printf 'VISIBLE_GPU_COUNT=1\n'
  printf 'ROLE_PLACEMENT=actor,env,rollout:0\n'
  printf 'ACTOR_SEED=%s\n' "$ACTOR_SEED"
  printf 'TRAIN_ENV_SEED=%s\n' "$TRAIN_ENV_SEED"
  printf 'EVAL_ENV_SEED=%s\n' "$EVAL_ENV_SEED"
  printf 'OUTPUT_DIR=%s\n' "$OUTPUT_DIR"
  printf 'HYDRA_DIR=%s\n' "$HYDRA_DIR"
  printf 'TENSORBOARD_DIR=%s\n' "$OUTPUT_DIR/tensorboard"
  printf 'CHECKPOINT_POLICY=disabled\n'
} | tee "$OUTPUT_DIR/driver.log" "$RUN_MANIFEST"

test "$(git -C "$RLINF_ROOT" rev-parse HEAD)" = "$RLINF_COMMIT"
test -z "$(git -C "$RLINF_ROOT" status --porcelain)"
test -r "$RLINF_ROOT/tests/e2e_tests/embodied/embodichain_ppo_cart_pole.yaml"
test -s "$ASSET_ROOT/SHA256SUMS.sha256"
test -s "$ASSET_ROOT/SNAPSHOT.env"
(
  cd "$ASSET_ROOT"
  sha256sum -c SHA256SUMS.sha256
  sha256sum -c SHA256SUMS
)
cat "$ASSET_ROOT/SNAPSHOT.env" \
  | tee -ia "$OUTPUT_DIR/driver.log" "$RUN_MANIFEST"

set +u
source /opt/venv/embodichain/bin/activate
set -u
export LD_LIBRARY_PATH="$(python -c 'import sysconfig; print(sysconfig.get_config_var("LIBDIR"))'):${LD_LIBRARY_PATH:-}"

if [[ -n "${RAY_ADDRESS:-}" ]]; then
  echo "Unexpected RAY_ADDRESS in an AIStudio single-machine task" >&2
  exit 1
fi
if ray status >/dev/null 2>&1; then
  echo "Unexpected pre-existing Ray runtime" >&2
  exit 1
fi

python - <<'PY' | tee -ia "$OUTPUT_DIR/driver.log" "$RUN_MANIFEST"
import importlib.metadata
import math
import os
import torch

assert torch.cuda.is_available(), "CUDA is unavailable"
assert torch.cuda.device_count() == 1, torch.cuda.device_count()
value = torch.tensor([2.0], device="cuda").square().item()
assert math.isfinite(value) and value == 4.0

stats = os.statvfs("/dev/shm")
shm_bytes = stats.f_frsize * stats.f_blocks
assert shm_bytes >= 32 * 1024**3, shm_bytes

print("TORCH_VERSION=" + importlib.metadata.version("torch"))
print("RAY_VERSION=" + importlib.metadata.version("ray"))
print("EMBODICHAIN_RUNTIME_VERSION=" + importlib.metadata.version("embodichain"))
print("CUDA_VERSION=" + str(torch.version.cuda))
print("GPU_NAME=" + torch.cuda.get_device_name(0))
print("DEV_SHM_BYTES=" + str(shm_bytes))
PY

{
  printf 'hydra.job.chdir=false\n'
  printf 'hydra.run.dir=%s\n' "$HYDRA_DIR"
  printf 'hydra.output_subdir=.hydra\n'
  printf 'runner.logger.log_path=%s\n' "$OUTPUT_DIR"
  printf 'actor.seed=%s\n' "$ACTOR_SEED"
  printf 'env.train.seed=%s\n' "$TRAIN_ENV_SEED"
  printf 'env.eval.seed=%s\n' "$EVAL_ENV_SEED"
  printf 'env.train.max_steps_per_rollout_epoch=8\n'
  printf 'env.eval.max_steps_per_rollout_epoch=8\n'
  printf 'rollout.enable_cuda_graph=false\n'
} > "$OUTPUT_DIR/overrides.txt"

set +e
python "$RLINF_ROOT/examples/embodiment/train_embodied_agent.py" \
  --config-path "$RLINF_ROOT/tests/e2e_tests/embodied" \
  --config-name embodichain_ppo_cart_pole \
  "hydra.job.chdir=false" \
  "hydra.run.dir=$HYDRA_DIR" \
  "hydra.output_subdir=.hydra" \
  "runner.logger.log_path=$OUTPUT_DIR" \
  "actor.seed=$ACTOR_SEED" \
  "env.train.seed=$TRAIN_ENV_SEED" \
  "env.eval.seed=$EVAL_ENV_SEED" \
  "env.train.max_steps_per_rollout_epoch=8" \
  "env.eval.max_steps_per_rollout_epoch=8" \
  "rollout.enable_cuda_graph=false" \
  2>&1 | tee -ia "$OUTPUT_DIR/driver.log"
PIPE_STATUSES=("${PIPESTATUS[@]}")
set -e

if (( PIPE_STATUSES[0] != 0 || PIPE_STATUSES[1] != 0 )); then
  printf 'RLinf or log persistence failed: rlinf=%s tee=%s\n' \
    "${PIPE_STATUSES[0]}" "${PIPE_STATUSES[1]}" >&2
  exit 1
fi

python - "$OUTPUT_DIR/driver.log" "$MIN_NUM_TRAJECTORIES" <<'PY' \
  | tee -ia "$OUTPUT_DIR/driver.log" "$RUN_MANIFEST"
import re
import sys
from pathlib import Path

log_path = Path(sys.argv[1])
minimum = int(sys.argv[2])
values = [
    int(value)
    for value in re.findall(r"\bnum_trajectories=(\d+)\b", log_path.read_text())
]
if not values:
    raise SystemExit("No num_trajectories metric was logged")
observed = max(values)
print(f"NUM_TRAJECTORIES_MAX={observed}")
if observed < minimum:
    raise SystemExit(
        f"Expected num_trajectories >= {minimum}, observed maximum {observed}"
    )
PY

test -s "$OUTPUT_DIR/overrides.txt"
test -s "$RUN_MANIFEST"
test -s "$HYDRA_DIR/.hydra/config.yaml"
test -s "$HYDRA_DIR/.hydra/hydra.yaml"
test -s "$HYDRA_DIR/.hydra/overrides.yaml"
test -s "$OUTPUT_DIR/tensorboard/config.yaml"
find "$OUTPUT_DIR/tensorboard" -type f -name 'events.out.tfevents.*' -size +0 -print -quit \
  | grep -q .

{
  printf 'RLINF_EXIT_CODE=0\n'
  printf 'PERSISTED_OUTPUT_READY=1\n'
} | tee -ia "$OUTPUT_DIR/driver.log" "$RUN_MANIFEST"

在任务创建页的启动命令中填写:

language-shell
exec /bin/bash "$WORK_ROOT/fixtures/cartpole/bounded-v1/launch.sh"

RLinf 正常退出、num_trajectories >= 2 且输出文件写入成功时,脚本返回 0

Step 5 确认配置并创建任务

在最终摘要中确认 1 个 Worker、1 张 GPU、单机框架、自己的租户镜像、共享存储路径和 bounded-v1/launch.sh 启动命令,然后点击 确认创建

Step 6 确认 Worker 通过预检并启动 RLinf 组件

任务进入运行中后,在任务日志中依次确认:

  1. IMAGE_TAGRLINF_COMMITRUN_IDOUTPUT_DIRHYDRA_DIR 与任务记录一致。
  2. 日志中只有一条 NVIDIA_DRIVER_VERSION=<version>,并记录 1 张目标 GPU、CUDA 版本和至少 32 GiB /dev/shm
  3. 固定版本的代码和资源通过 SHA-256 检查,日志中没有克隆或下载行为。
  4. RLinf 启动的本地 Ray 显示 1 个节点和 1 张 GPU。
  5. ActorGroup、RolloutGroup 和 EnvGroup 均已创建,并使用 GPU 0

这些检查都通过后,等待训练结束。

Step 7 确认至少两条轨迹和一次 PPO 更新

任务结束前,在任务日志和 TensorBoard 中核对:

  • 日志出现 NUM_TRAJECTORIES_MAX=<number>,且该值不小于 2
  • train/actor/grad_norm 以及其他 train/ 损失或优化器标量存在且为有限值;
  • 日志中出现 PERSISTED_OUTPUT_READY=1
  • 训练进程退出码为 0,任务最终显示运行成功。

页面显示运行成功后,仍需确认轨迹数和 PPO 更新结果。

Step 8 Worker 结束后检查共享存储中的输出

任务结束后,在挂载同一共享存储的 AICoder 或开发机中执行:

language-shell
set -euo pipefail

export RUN_ID=cartpole-20260820-01
export WORK_ROOT=/mnt/user_dir/your-user-name/rlinf-reproduction
export EXPECTED_IMAGE_TAG=cr.infini-ai.com/your-tenant-id/rlinf:agentic-rlinf0.4-embodichain

: "${RUN_ID:?Set the completed run ID}"
: "${WORK_ROOT:?Set WORK_ROOT to the completed run directory on shared storage}"
: "${EXPECTED_IMAGE_TAG:?Set the complete image tag used by the task}"

OUTPUT_DIR="${WORK_ROOT%/}/runs/cartpole/$RUN_ID"
RUN_MANIFEST="$OUTPUT_DIR/run-manifest.txt"
EXPECTED_RLINF_COMMIT=3cb54c2eb2e355378ba049d50f7ec0cb8acc2da0

test -s "$OUTPUT_DIR/driver.log"
test -s "$RUN_MANIFEST"
test -s "$OUTPUT_DIR/overrides.txt"
test -s "$OUTPUT_DIR/hydra/.hydra/config.yaml"
test -s "$OUTPUT_DIR/hydra/.hydra/hydra.yaml"
test -s "$OUTPUT_DIR/hydra/.hydra/overrides.yaml"
test -s "$OUTPUT_DIR/tensorboard/config.yaml"
find "$OUTPUT_DIR/tensorboard" -type f -name 'events.out.tfevents.*' -size +0 -print -quit \
  | grep -q .

grep -aFx "IMAGE_TAG=$EXPECTED_IMAGE_TAG" "$OUTPUT_DIR/driver.log"
grep -aFx "RLINF_COMMIT=$EXPECTED_RLINF_COMMIT" "$OUTPUT_DIR/driver.log"
test "$(grep -aEc '^NVIDIA_DRIVER_VERSION=[^[:space:]]+$' "$OUTPUT_DIR/driver.log")" -eq 1
grep -aFx 'PERSISTED_OUTPUT_READY=1' "$OUTPUT_DIR/driver.log"
grep -aF 'NUM_TRAJECTORIES_MAX=' "$OUTPUT_DIR/driver.log" | tail -1 \
  | awk -F= '$2 >= 2 { found=1 } END { exit !found }'

find "$OUTPUT_DIR" -maxdepth 4 -type f -printf '%P\t%s\n' | sort

所有命令退出码为 0 时,说明训练日志、配置和 TensorBoard 文件在任务结束后仍可读取。

重新运行失败任务

启动脚本检测到输入、GPU、共享内存、Ray、轨迹或输出异常时会返回非 0。保留本次输出目录以便排查,然后使用新的 RUN_ID 创建任务。通用问题请按训练故障处理检查任务事件、日志和存储挂载。

处理本场景特有故障

以下问题与本场景的本地 Ray、短轨迹或固定资源直接相关。其他 AIStudio 任务问题使用通用故障处理文档。

num_trajectories 始终为 0

overrides.txt 和解析后配置中确认 train/eval 的 max_steps_per_rollout_epoch 均为 8。值为 1 时,环境可能无法在 max_episode_steps: 8 内完成一条轨迹。修正后使用新 RUN_ID 重跑;日志出现 NUM_TRAJECTORIES_MAX>=2 表示问题已解决。

启动脚本检测到已有 Ray 运行时

本场景需要 RLinf 自己启动本地 Ray。检查任务环境变量中是否设置了 RAY_ADDRESS,并移除启动命令前的 ray startray job submit。重新创建任务后,RLinf 应启动一个本地节点。

CartPole 资源无法离线读取

在 AICoder 或开发机中重新执行 sha256sum -c SHA256SUMS,并确认资源路径与 EMBODICHAIN_DATA_ROOT 一致。资源缺失或损坏时,重新下载到新目录并生成校验清单。

Hydra 或 TensorBoard 文件没有写入共享存储

检查启动日志中的 OUTPUT_DIRHYDRA_DIR,再核对 hydra.run.dirhydra.output_subdirrunner.logger.log_path 的有效值。Hydra 元数据应位于 $OUTPUT_DIR/hydra/.hydra/,TensorBoard 文件应位于 $OUTPUT_DIR/tensorboard/。路径指向 Worker 本地目录时,修正启动脚本并使用新 RUN_ID 重跑。

确认本示例的使用范围

本文使用宁夏 B、1 个 Worker 和 1 张 A100-SXM4-80GB。您也可以在自己的账号中选择其他符合要求的资源,但镜像、任务和共享存储需要位于同一可用区。

  • 任务中断后需要重新运行:本配置不生成 checkpoint。Spot 资源中断后,使用新的 RUN_ID 从头运行。需要恢复训练进度时,请改用支持 checkpoint 保存和加载的配置。
  • 训练指标可能不同:EmbodiChain 日志可能显示 Environment seed : None。比较多次运行时,以至少 2 条完整轨迹、PPO 更新和退出码为准,不要求每个指标完全相同。
  • 本示例不用于性能测试rollout.enable_cuda_graph=false 时可能出现 CUDA stream 警告。轨迹、PPO 更新和退出码正常即可完成本示例,但该结果不代表完整训练的性能、稳定性或收敛情况。