Skip to content

在 AIStudio 训练任务中使用 RLinf 复现 ManiSkill PPO MLP

本教程使用一个三层 MLP,根据机械臂关节、末端执行器和方块位置等低维状态控制 Panda 机械臂完成 PickCube-v1 任务。MLP 从随机权重开始训练,轨迹由仿真环境在线生成,因此不需要下载预训练模型或静态数据集。

EmbodiChain CartPole 适合用来熟悉 RLinf 的基本任务流程;这个场景进一步把 GPU 机器人仿真加入训练链路。相比 ManiSkill PPO OpenVLA 场景,它不需要准备大模型,也不涉及视觉输入和多 GPU 放置,因此更容易单独判断 SAPIEN、Vulkan/EGL、在线采样和 PPO 更新是否正常。

实战目标

您将创建一个单 Worker、单 GPU 的训练任务。任务使用两个并行环境采集短轨迹,完成一次 PPO 更新,再运行一次短时评估。

本教程聚焦 AIStudio 的 GPU 机器人仿真运行时,是引入大模型和多 GPU 前的基础检查。

以下结果全部出现时,说明本次短时验证通过:

  1. PickCube-v1 在 GPU 上创建并完成一次预检 step,状态向量和动作向量的末维分别为 428
  2. RLinf 在一个 Worker 内启动本地 Ray,并把 environment、rollout 和 actor 放到 GPU 0
  3. 两个训练环境各运行最多 8 个时间步,日志中的 num_trajectories 不小于 2
  4. PPO 完成一个 global step,actor/grad_norm 为有限值。
  5. 两个评估环境完成短时评估,并在共享存储中生成评估视频。
  6. 任务正常退出,Hydra 配置、训练日志、TensorBoard 文件、评估视频和验收结果都可以从共享存储回读。

本次短时验证只检查仿真和训练流程。随机初始化的 MLP 可能取得 0 成功率;策略收敛需要更长时间的训练和独立评估。

场景信息

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

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

  • RLinf

    • commit:6f46f36ad790fec0de1efa86aaa8f9cd3e10d3f4
    • 配置:examples/embodiment/config/maniskill_ppo_mlp.yaml
    • 入口:examples/embodiment/train_embodied_agent.py
  • 镜像:将 Docker Hub 镜像 rlinf/rlinf:agentic-rlinf0.4-maniskill_libero 导入自己的租户镜像仓库。

  • 模型和数据:MLP 从头训练,不需要预训练模型或静态训练数据。轨迹由 PickCube-v1 在任务运行期间生成。

  • 仿真资产:使用镜像内置的 ManiSkill 和 SAPIEN 资产;任务启动时只建立资产链接,不下载文件。

  • 资源来源:Spot 资源。

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

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

  • 共享存储:挂载到 /mnt/rlinf-reproduction;代码和工具作为固定输入,运行结果写入 runs/maniskill-ppo-mlp/

  • 运行范围:2 个训练环境和 2 个评估环境,每个环境最多运行 8 个时间步,完成 1 个 PPO global step 和 2 个 update epoch,不保存 checkpoint。本文先用缩减后的配置检查仿真和训练流程,再决定是否扩大规模。

本场景在 AIStudio 中怎样运行

本场景选择“单机”,因为一个 Worker 和一张 GPU 已能容纳全部组件。AIStudio 创建 Worker、注入 GPU 驱动并挂载共享存储;RLinf 在 Worker 中启动本地 Ray,再运行 ManiSkill 环境、MLP rollout 和 PPO actor。

language-text
AIStudio 单机训练任务
└── Worker 0 / GPU 0
    └── RLinf 本地 Ray
        ├── EnvGroup / ManiSkill PickCube-v1
        ├── RolloutGroup / MLP policy
        └── ActorGroup / PPO actor-critic

状态、动作、奖励和轨迹都在任务运行期间产生,不需要从共享存储读取训练数据。Worker 从共享存储读取固定版本的 RLinf 代码,并把日志、Hydra 配置、TensorBoard 文件和评估视频写入本次 RUN_ID 的输出目录。

开始前准备

创建 GPU 任务前,使用挂载同一共享存储的 AICoder 或开发机准备镜像、RLinf checkout 和辅助脚本。本文使用宁夏 B;选择其他可用区时,也需要把这些内容准备到任务所在可用区。

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

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

重要

请使用自己的镜像地址

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

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

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

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

在挂载目标共享存储的 AICoder 或开发机中执行:

language-shell
set -euo pipefail

export WORK_ROOT=/mnt/rlinf-reproduction
export RLINF_COMMIT=6f46f36ad790fec0de1efa86aaa8f9cd3e10d3f4
export RLINF_ROOT="$WORK_ROOT/code/$RLINF_COMMIT"

findmnt -T "$WORK_ROOT"
test -w "$WORK_ROOT"
mkdir -p "$WORK_ROOT/code"

if [ ! -e "$RLINF_ROOT" ]; then
  git clone https://github.com/RLinf/RLinf.git "$RLINF_ROOT"
  git -C "$RLINF_ROOT" checkout --detach "$RLINF_COMMIT"
fi

test "$(git -C "$RLINF_ROOT" rev-parse HEAD)" = "$RLINF_COMMIT"
test -z "$(git -C "$RLINF_ROOT" status --porcelain)"
test -r "$RLINF_ROOT/examples/embodiment/config/maniskill_ppo_mlp.yaml"
test -r "$RLINF_ROOT/examples/embodiment/train_embodied_agent.py"

训练任务只读取并校验这个 checkout,不执行 git clonegit fetch 或网络补齐。目录已经存在时,以上命令会直接检查 commit、工作树和 recipe。

使用镜像内置的仿真资产

这个场景从随机初始化的 MLP 开始训练,PickCube-v1 会在线生成状态、动作、奖励和轨迹。因此,无需准备模型目录或数据集目录。

镜像已经包含 ManiSkill、SAPIEN PhysX 和运行 PickCube-v1 所需的基础资产。启动脚本调用 link_assets 后,以离线模式创建 PickCube-v1 仿真环境;资产缺失时,预检会直接失败,不会在 GPU Worker 中下载文件。

在共享存储中创建预检和启动脚本

本教程使用三个独立脚本:GPU 仿真预检脚本、训练结果校验脚本和训练启动脚本。先创建目录:

language-shell
mkdir -p /mnt/rlinf-reproduction/tools/maniskill-ppo-mlp

然后使用 AICoder 编辑器创建以下文件。AIStudio 的启动命令只执行这些文件,不在 GPU Worker 中临时生成 Python 或 Shell 脚本。

创建 GPU 仿真预检脚本

创建 /mnt/rlinf-reproduction/tools/maniskill-ppo-mlp/preflight-v1.py,写入:

language-python
#!/usr/bin/env python3
"""Create and step the bounded PickCube environment on one GPU."""

import os
import sys

import gymnasium as gym
import torch

import rlinf.envs.maniskill  # noqa: F401


def main() -> None:
    env = gym.make(
        "PickCube-v1",
        num_envs=2,
        obs_mode="state",
        sim_backend="gpu",
        render_mode="all",
        max_episode_steps=8,
        sensor_configs={"shader_pack": "default"},
    )
    try:
        observation, _ = env.reset(seed=0)
        device = str(env.unwrapped.device)
        observation_shape = tuple(observation.shape)
        action_shape = tuple(env.action_space.shape)

        if not device.startswith("cuda"):
            raise RuntimeError(f"ManiSkill is not using CUDA: {device}")
        if observation_shape[-1] != 42:
            raise RuntimeError(
                f"Expected observation width 42, found {observation_shape}"
            )
        if action_shape[-1] != 8:
            raise RuntimeError(f"Expected action width 8, found {action_shape}")

        next_observation, reward, _, _, _ = env.step(env.action_space.sample())
        if tuple(next_observation.shape)[-1] != 42:
            raise RuntimeError("Unexpected observation shape after env.step")
        if not torch.isfinite(reward).all():
            raise RuntimeError("ManiSkill returned a non-finite reward")
        if env.render() is None:
            raise RuntimeError("ManiSkill did not return a rendered frame")

        print(f"MANISKILL_DEVICE={device}")
        print(f"OBSERVATION_SHAPE={observation_shape}")
        print(f"ACTION_SHAPE={action_shape}")
        print("MANISKILL_PREFLIGHT=passed")
    finally:
        env.close()

    # End the isolated preflight after closing the native simulator and
    # flushing its result.
    sys.stdout.flush()
    sys.stderr.flush()
    os._exit(0)


if __name__ == "__main__":
    main()

创建训练结果校验脚本

创建 /mnt/rlinf-reproduction/tools/maniskill-ppo-mlp/validate-run-v1.py,写入:

language-python
#!/usr/bin/env python3
"""Require the bounded ManiSkill PPO MLP completion signals."""

import argparse
import json
import math
import re
from pathlib import Path


NUMBER = r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?"
FATAL_MARKERS = (
    "Traceback (most recent call last):",
    "RayTaskError(",
    "Exception in thread",
    "Segmentation fault (core dumped)",
    "terminate called after throwing an instance of",
    "INTERNAL ASSERT FAILED",
)


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("training_log", type=Path)
    parser.add_argument("video_root", type=Path)
    parser.add_argument("acceptance_json", type=Path)
    args = parser.parse_args()

    text = args.training_log.read_text(encoding="utf-8", errors="replace")
    clean = re.sub(r"\x1b\[[0-9;]*[A-Za-z]", "", text)

    if any(marker in clean for marker in FATAL_MARKERS):
        raise RuntimeError("The training log contains a fatal exception marker")
    if not re.search(r"Global Step:\s*1/1", clean):
        raise RuntimeError("Global Step 1/1 was not found")

    trajectories = [
        float(value)
        for value in re.findall(rf"num_trajectories=({NUMBER})", clean)
    ]
    if not trajectories or max(trajectories) < 2:
        raise RuntimeError("At least 2 trajectories were not observed")

    grad_norms = [
        float(value)
        for value in re.findall(rf"actor/grad_norm=({NUMBER})", clean)
    ]
    if not grad_norms or not math.isfinite(grad_norms[-1]):
        raise RuntimeError("A finite actor/grad_norm was not observed")

    if " Evaluation " not in clean:
        raise RuntimeError("The bounded evaluation section was not found")
    success_values = [
        float(value) for value in re.findall(rf"success_once=({NUMBER})", clean)
    ]
    if not success_values or not all(math.isfinite(value) for value in success_values):
        raise RuntimeError("A finite success_once metric was not observed")

    videos = sorted(
        path for path in args.video_root.rglob("*.mp4") if path.stat().st_size > 0
    )
    if not videos:
        raise RuntimeError("No non-empty evaluation video was found")

    result = {
        "global_step": 1,
        "num_trajectories": max(trajectories),
        "actor_grad_norm": grad_norms[-1],
        "success_once_values": success_values,
        "evaluation_videos": [str(path) for path in videos],
        "accepted": True,
    }
    args.acceptance_json.write_text(
        json.dumps(result, indent=2, sort_keys=True) + "\n",
        encoding="utf-8",
    )
    print("MANISKILL_PPO_MLP_ACCEPTANCE=passed")


if __name__ == "__main__":
    main()

创建训练启动脚本

创建 /mnt/rlinf-reproduction/tools/maniskill-ppo-mlp/run-maniskill-ppo-mlp-v1.sh,写入:

language-bash
#!/usr/bin/env bash
set -euo pipefail

: "${RUN_ID:?Set RUN_ID in the task environment}"
: "${IMAGE_TAG:?Set IMAGE_TAG to the selected task image}"
[[ "$RUN_ID" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]] || {
  echo "RUN_ID must use 1-64 letters, numbers, dots, underscores, or hyphens" >&2
  exit 1
}

readonly WORK_ROOT=/mnt/rlinf-reproduction
readonly RLINF_COMMIT=6f46f36ad790fec0de1efa86aaa8f9cd3e10d3f4
readonly RLINF_ROOT="$WORK_ROOT/code/$RLINF_COMMIT"
readonly TOOL_ROOT="$WORK_ROOT/tools/maniskill-ppo-mlp"
readonly OUTPUT_DIR="$WORK_ROOT/runs/maniskill-ppo-mlp/$RUN_ID"
readonly HYDRA_DIR="$OUTPUT_DIR/hydra"
readonly RLINF_OUTPUT_DIR="$OUTPUT_DIR/rlinf"
readonly TRAINING_LOG="$OUTPUT_DIR/training.log"
readonly PYTHON=/opt/venv/openvla/bin/python
readonly RAY_CLI=/opt/venv/openvla/bin/ray

test ! -e "$OUTPUT_DIR" || {
  echo "Refusing to reuse OUTPUT_DIR: $OUTPUT_DIR" >&2
  exit 1
}
mkdir -p "$OUTPUT_DIR"
exec > >(tee "$OUTPUT_DIR/startup.log") 2>&1

printf 'IMAGE_TAG=%s\n' "$IMAGE_TAG"
printf 'RUN_ID=%s\n' "$RUN_ID"
printf 'RLINF_COMMIT=%s\n' "$RLINF_COMMIT"

test -d "$RLINF_ROOT"
test -s "$RLINF_ROOT/examples/embodiment/config/maniskill_ppo_mlp.yaml"
test -s "$RLINF_ROOT/examples/embodiment/train_embodied_agent.py"
test -s "$TOOL_ROOT/preflight-v1.py"
test -s "$TOOL_ROOT/validate-run-v1.py"
test -x "$PYTHON"
test -x "$RAY_CLI"

test "$(git -C "$RLINF_ROOT" rev-parse HEAD)" = "$RLINF_COMMIT"
test -z "$(git -C "$RLINF_ROOT" status --porcelain)"

mapfile -t DRIVER_VERSIONS < <(
  nvidia-smi --query-gpu=driver_version --format=csv,noheader,nounits \
    | sed '/^[[:space:]]*$/d' \
    | sort -u
)
test "${#DRIVER_VERSIONS[@]}" -eq 1
printf 'NVIDIA_DRIVER_VERSION=%s\n' "${DRIVER_VERSIONS[0]}"

nvidia-smi -L | tee "$OUTPUT_DIR/gpus.txt"
test "$(wc -l < "$OUTPUT_DIR/gpus.txt")" -eq 1
printf 'GPU_COUNT=1\n'

SHM_KIB="$(df --output=size -k /dev/shm | tail -n 1 | tr -d ' ')"
test "$SHM_KIB" -ge 33554432
printf 'SHM_KIB=%s\n' "$SHM_KIB"

test "${NVIDIA_DRIVER_CAPABILITIES:-}" = all
test -r /etc/vulkan/icd.d/nvidia_icd.json
test -r /usr/share/glvnd/egl_vendor.d/10_nvidia.json
ldconfig -p > "$OUTPUT_DIR/ldconfig.txt"
grep -q 'libEGL_nvidia.so' "$OUTPUT_DIR/ldconfig.txt"
grep -q 'libGLX_nvidia.so' "$OUTPUT_DIR/ldconfig.txt"

command -v link_assets >/dev/null
link_assets
test -d "$HOME/.maniskill"
test -d "$HOME/.sapien"

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

export VIRTUAL_ENV=/opt/venv/openvla
export PATH="$VIRTUAL_ENV/bin:$PATH"
export EMBODIED_PATH="$RLINF_ROOT/examples/embodiment"
export PYTHONPATH="$RLINF_ROOT${PYTHONPATH:+:$PYTHONPATH}"
export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export MUJOCO_GL=egl
export PYOPENGL_PLATFORM=egl
export VK_DRIVER_FILES=/etc/vulkan/icd.d/nvidia_icd.json
export VK_ICD_FILENAMES=/etc/vulkan/icd.d/nvidia_icd.json
export RAY_DEDUP_LOGS=0

"$PYTHON" -c \
  'import torch; assert torch.cuda.device_count() == 1; print(torch.__version__, torch.cuda.get_device_name(0))'

{
  printf 'IMAGE_TAG=%s\n' "$IMAGE_TAG"
  printf 'RUN_ID=%s\n' "$RUN_ID"
  printf 'RLINF_COMMIT=%s\n' "$RLINF_COMMIT"
  printf 'NVIDIA_DRIVER_VERSION=%s\n' "${DRIVER_VERSIONS[0]}"
  printf 'GPU_COUNT=1\n'
  printf 'SHM_KIB=%s\n' "$SHM_KIB"
} > "$OUTPUT_DIR/run-manifest.env"

{
  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' "$RLINF_OUTPUT_DIR"
  printf 'runner.max_steps=1\n'
  printf 'runner.val_check_interval=1\n'
  printf 'env.train.total_num_envs=2\n'
  printf 'env.eval.total_num_envs=2\n'
  printf 'env.train.max_steps_per_rollout_epoch=8\n'
  printf 'env.eval.max_steps_per_rollout_epoch=8\n'
  printf 'algorithm.update_epoch=2\n'
  printf 'actor.global_batch_size=2\n'
  printf 'actor.micro_batch_size=2\n'
} > "$OUTPUT_DIR/overrides.txt"

CUDA_VISIBLE_DEVICES=0 "$PYTHON" "$TOOL_ROOT/preflight-v1.py"

set +e
"$PYTHON" -u \
  "$RLINF_ROOT/examples/embodiment/train_embodied_agent.py" \
  --config-path "$RLINF_ROOT/examples/embodiment/config" \
  --config-name maniskill_ppo_mlp \
  "hydra.job.chdir=false" \
  "hydra.run.dir=$HYDRA_DIR" \
  "hydra.output_subdir=.hydra" \
  "runner.logger.log_path=$RLINF_OUTPUT_DIR" \
  runner.max_steps=1 \
  runner.max_epochs=1 \
  runner.val_check_interval=1 \
  runner.save_interval=-1 \
  algorithm.update_epoch=2 \
  env.train.total_num_envs=2 \
  env.train.rollout_epoch=1 \
  env.train.max_episode_steps=8 \
  env.train.max_steps_per_rollout_epoch=8 \
  env.train.seed=0 \
  env.train.video_cfg.save_video=false \
  env.eval.total_num_envs=2 \
  env.eval.rollout_epoch=1 \
  env.eval.max_episode_steps=8 \
  env.eval.max_steps_per_rollout_epoch=8 \
  env.eval.seed=1 \
  env.eval.video_cfg.save_video=true \
  actor.global_batch_size=2 \
  actor.micro_batch_size=2 \
  actor.seed=1234 \
  rollout.enable_torch_compile=false \
  rollout.enable_cuda_graph=false \
  2>&1 | tee "$TRAINING_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" "$TOOL_ROOT/validate-run-v1.py" \
  "$TRAINING_LOG" \
  "$RLINF_OUTPUT_DIR/video/eval" \
  "$OUTPUT_DIR/acceptance.json"

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

printf 'RLINF_TRAINING_COMPLETE=1\n'

固定 Hydra 和训练输出目录

启动脚本为每个 RUN_ID 创建一个独立输出目录,并分别保存 Hydra 元数据和 RLinf 训练结果。

设置本场景值结果
hydra.job.chdirfalse保持训练进程的工作目录稳定
hydra.run.dir$OUTPUT_DIR/hydra保存 Hydra 日志和本次运行信息
hydra.output_subdir.hydra保留解析后的配置和 overrides
runner.logger.log_path$OUTPUT_DIR/rlinf保存 TensorBoard 和评估视频

确认 GPU 仿真运行条件

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

  • 1 张与镜像兼容的 NVIDIA GPU;
  • 任务可以把准备好的共享存储挂载到 /mnt/rlinf-reproduction
  • Worker 可以提供至少 32 GiB /dev/shm
  • 任务环境变量可以设置 NVIDIA_DRIVER_CAPABILITIES=all

Worker 启动后,脚本会自动检查 Python 环境、仿真资产、NVIDIA 驱动以及 Vulkan/EGL 图形库。

运行这个场景

以下步骤先检查固定输入,再分配 GPU 并判断仿真、训练和持久化结果是否全部通过。

Step 1 使用 AICoder 校验共享存储中的代码和脚本

创建 GPU 任务前,在挂载同一共享存储的 AICoder 或开发机中执行:

language-shell
set -euo pipefail

export WORK_ROOT=/mnt/rlinf-reproduction
export RLINF_COMMIT=6f46f36ad790fec0de1efa86aaa8f9cd3e10d3f4
export RLINF_ROOT="$WORK_ROOT/code/$RLINF_COMMIT"
export TOOL_ROOT="$WORK_ROOT/tools/maniskill-ppo-mlp"

findmnt -T "$WORK_ROOT"
test -w "$WORK_ROOT"
test "$(git -C "$RLINF_ROOT" rev-parse HEAD)" = "$RLINF_COMMIT"
test -z "$(git -C "$RLINF_ROOT" status --porcelain)"
test -s "$RLINF_ROOT/examples/embodiment/config/maniskill_ppo_mlp.yaml"
python3 -m py_compile \
  "$TOOL_ROOT/preflight-v1.py" \
  "$TOOL_ROOT/validate-run-v1.py"
bash -n "$TOOL_ROOT/run-maniskill-ppo-mlp-v1.sh"
chmod 755 \
  "$TOOL_ROOT/preflight-v1.py" \
  "$TOOL_ROOT/validate-run-v1.py" \
  "$TOOL_ROOT/run-maniskill-ppo-mlp-v1.sh"

所有命令退出码为 0 后再创建任务。检查失败时,在 AICoder 或开发机中修复固定输入;GPU 启动脚本不会克隆代码、下载资产或修改 checkout。

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

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

AIStudio 字段本场景设置
任务类型训练任务
资源类型Spot 资源
可用区和 GPU本文使用宁夏 B、1 张 A100-SXM4-80GB;请在自己的账号中选择符合要求的资源
分布式框架单机
Worker 数量1
RDMA 配置不开启
镜像自己租户中的 agentic-rlinf0.4-maniskill_libero 完整 tag
共享内存至少 32 GiB
任务可视化开启;日志路径指向本次运行的 RLinf 输出目录

一个 Worker 已能运行本场景的本地 Ray 和三个 RLinf 组件,因此不需要选择 AIStudio 的 Ray 分布式框架或开启 RDMA。

Step 3 挂载共享存储并设置唯一运行参数

把包含 rlinf-reproduction 内容的共享存储挂载到容器内的 /mnt/rlinf-reproduction。系统挂载路径必须和脚本中的 WORK_ROOT 完全一致。

在任务环境变量中设置:

language-text
RUN_ID=maniskill-ppo-mlp-step1-<date>-01
IMAGE_TAG=cr.infini-ai.com/<tenant-id>/rlinf:agentic-rlinf0.4-maniskill_libero
NVIDIA_DRIVER_CAPABILITIES=all

开启任务可视化,并把日志路径设置为:

language-text
/mnt/rlinf-reproduction/runs/maniskill-ppo-mlp/${RUN_ID}/rlinf

${RUN_ID} 来自上面添加的任务环境变量,平台会在启动 TensorBoard 前替换它。具体用法见使用环境变量同步 TensorBoard 日志路径。每次运行只需设置新的 RUN_ID,无需再修改任务可视化路径。启动脚本会拒绝复用已有输出目录,防止旧日志和本次结果混在一起。

Step 4 设置执行版本化脚本的启动命令

在“启动命令”中填写:

language-shell
exec /bin/bash /mnt/rlinf-reproduction/tools/maniskill-ppo-mlp/run-maniskill-ppo-mlp-v1.sh

启动命令保持简短,具体预检、Hydra overrides、训练和验收逻辑保存在共享存储中的版本化脚本里,便于运行前检查和失败后定位。

Step 5 核对任务配置后确认创建

确认 1 个 Worker、1 张 GPU、单机框架、自己的租户镜像、/mnt/rlinf-reproduction 系统挂载路径、至少 32 GiB 共享内存和三个环境变量均已设置。挂载路径必须以页面最终显示的值为准,不能根据存储卷名称推断。

第一次验证时关闭自动重启。自动重启会复用同一个 RUN_ID,而本教程的脚本会保留首次失败输出并拒绝覆盖。配置一致后,点击 确认创建

Step 6 确认 PickCube GPU 预检和本地 Ray 启动

任务开始运行后,在日志中依次确认:

language-text
NVIDIA_DRIVER_VERSION=<version>
GPU_COUNT=1
MANISKILL_DEVICE=cuda
OBSERVATION_SHAPE=(2, 42)
ACTION_SHAPE=(2, 8)
MANISKILL_PREFLIGHT=passed

随后确认 RLinf 创建 EnvGroup、RolloutGroup 和 ActorGroup,并在一个本地 Ray 节点上使用 GPU 0。以 MANISKILL_PREFLIGHT=passed 作为 GPU 仿真预检通过的标志;任务进入“运行中”只表示 Worker 已启动。

Step 7 确认两条轨迹、一次 PPO 更新和短时评估

训练完成后,日志应包含:

language-text
Global Step:    1/1
num_trajectories=<不小于 2 的数值>
actor/grad_norm=<有限数值>
MANISKILL_PPO_MLP_ACCEPTANCE=passed
RLINF_TRAINING_COMPLETE=1

指标表还应包含 Evaluation 部分,且 success_once 为有限值。本次任务只需确认评估流程完成;随机初始化策略的成功率可以为 0

Step 8 Worker 结束后回读训练结果和评估视频

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

language-shell
set -euo pipefail

export RUN_ID=maniskill-ppo-mlp-step1-<date>-01
export WORK_ROOT=/mnt/rlinf-reproduction
export OUTPUT_DIR="$WORK_ROOT/runs/maniskill-ppo-mlp/$RUN_ID"

test -s "$OUTPUT_DIR/startup.log"
test -s "$OUTPUT_DIR/training.log"
test -s "$OUTPUT_DIR/acceptance.json"
test -s "$OUTPUT_DIR/hydra/.hydra/config.yaml"
find "$OUTPUT_DIR/rlinf/tensorboard" \
  -type f -name 'events.out.tfevents.*' -size +0 -print -quit | grep -q .
find "$OUTPUT_DIR/rlinf/video/eval" \
  -type f -name '*.mp4' -size +0 -print -quit | grep -q .

grep -aFx 'MANISKILL_PPO_MLP_ACCEPTANCE=passed' "$OUTPUT_DIR/startup.log"
grep -aFx 'RLINF_TRAINING_COMPLETE=1' "$OUTPUT_DIR/startup.log"

cat "$OUTPUT_DIR/acceptance.json"

所有命令退出码为 0 时,说明验收结果、Hydra 配置、TensorBoard 文件和评估视频在 Worker 回收后仍然可读。

延长训练并保存 checkpoint

短时验证通过后,可以复制启动脚本并使用新的版本名逐步增加 runner.max_steps。先增加 global step 数,再根据显存和仿真速度调整并行环境数。每次只调整一类参数,便于在出现问题时定位原因。

上游 recipe 默认不限制训练 step,并设置 env.train.total_num_envs=1024env.eval.total_num_envs=32actor.global_batch_size=6400algorithm.update_epoch=8。这些值可作为扩大规模时的参考,但仍需根据所选 GPU 和目标运行时间逐项调整。

需要保存策略时,把 runner.save_interval 设置为正整数。checkpoint 会写入 runner.logger.log_path 下的实验目录。恢复训练前,先确认 checkpoint 文件完整,再通过 runner.resume_dir=<checkpoint-directory> 启动新的任务和新的 RUN_ID

延长运行时,请根据目标 step 重新规划存储空间、Spot 中断恢复和 checkpoint 周期,并单独评估吞吐、收敛和超参数。

停止或重新运行任务

预检、训练或结果校验失败时,脚本会返回非 0。保留本次输出,先检查 startup.logtraining.log 和任务事件;修复问题后使用新的 RUN_ID 重跑。

通用问题请按训练故障处理检查任务事件、日志和共享存储挂载。

处理本场景特有故障

找不到 ManiSkill 或 SAPIEN 资产

如果 link_assets 之后仍找不到 $HOME/.maniskill$HOME/.sapien,先确认任务选择了本文指定的镜像 tag,并检查 /opt/assets 是否存在。仿真资产应随镜像提供;缺失时请修正镜像或挂载。

Vulkan、EGL 或 GPU 仿真预检失败

如果缺少 NVIDIA Vulkan ICD、EGL 库,或者 MANISKILL_DEVICE 不是 CUDA 设备,说明当前 Worker 无法运行 GPU 仿真。确认任务使用预期镜像、NVIDIA_DRIVER_CAPABILITIES=all 已设置,并重新选择符合要求的 GPU 资源。

已有 Ray 进程阻止任务启动

本场景由 RLinf 在单个 Worker 内启动本地 Ray。启动前检测到 RAY_ADDRESS 或已有 Ray 运行时时,保留日志并使用干净 Worker 重新运行。

任务显示运行成功但缺少 PPO 完成信号

如果任务页面显示运行成功,但没有 MANISKILL_PPO_MLP_ACCEPTANCE=passedRLINF_TRAINING_COMPLETE=1,检查 training.log 中的异常标记、轨迹数和 actor/grad_norm。只有结果校验脚本通过并且持久化文件完整时,才把本次运行记为通过。

评估完成但没有生成视频

检查解析后的 Hydra 配置中 env.eval.video_cfg.save_video 是否为 true,并确认 runner.logger.log_path 指向共享存储。随后检查 Vulkan/EGL 预检和评估日志。评估视频是本教程的验收项;训练更新完成后仍需确认视频已写入共享存储。