[cos θ, sin θ, θ̇],输出作用于摆杆的扭矩动作 a ∈ [-2, 2],目标是把摆杆从任意初始角度摆起并稳定在竖直向上位置(θ=0)。每回合最多 200 步。MlpPolicy(Actor-Critic)。策略网络 3→64→64→1(tanh 激活),值函数网络 3→64→64→1,并使用 StateDependentNoise(SDE) 状态依赖噪声(use_sde=True,log_std 形状 (64,1))。policy.pth 共 13 个权重张量,load_state_dict 严格匹配)。mean_reward = -189.25 ± 66.36关键适配点: 该模型为强化学习策略(非 LLM),无法使用 vLLM-Ascend / sglang 等大模型推理引擎, 适配采用 torch_npu(PyTorch for Ascend)作为昇腾推理引擎。模型以 SB3 zip 格式保存, 使用
stable_baselines3(2.9.0)的PPO.load()直接加载(policy.pth13/13 张量严格匹配, 0 缺失 / 0 冗余),权重与策略结构一一对应,推理所需算子(线性层 / tanh / 正态分布采样) 全部在昇腾 NPU 上原生执行。确定性推理在 CPU 与 NPU 上逐位一致(余弦相似度 1.0)。
| 组件 | 版本 |
|---|---|
| OS | Linux 5.10 (aarch64) |
| Python | 3.11.14 |
| torch | 2.9.0 |
| torch-npu | 2.9.0.post1 |
| stable-baselines3 | 2.9.0 |
| gymnasium | 1.3.0 |
| numpy | 1.26.4 |
| cloudpickle | 3.1.2 |
| CANN | 8.5.1 |
| NPU | Ascend 910B (64GB HBM) |
完整依赖清单见 requirements.txt。
# 1) 创建并激活虚拟环境(--system-site-packages 复用系统预装的 torch_npu/CANN)
cd ppo-Pendulum-v1-NPU
python3 -m venv --system-site-packages venv
source venv/bin/activate
# 2) 安装依赖(国内镜像源示例)
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
# 或阿里云: pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple/
# 3) 确认昇腾环境
npu-smi info # 应看到 Ascend910 设备
python -c "import torch, torch_npu; print(torch.npu.is_available())" # True说明: 若机器尚未安装 torch / torch_npu,请从 pytorch-ascend 获取与 CANN 8.5.1 匹配的 aarch64 wheel 安装(详见
requirements.txt顶部注释),再执行pip install -r requirements.txt。 运行时若看到一行[LOG_WARNING] can not create directory ... /ascend/log,为容器内 CANN 日志目录 权限的一次性良性提示,不影响推理正确性,可忽略。
模型权重位于 /data/models/HumanCompatibleAI/ppo-Pendulum-v1/ppo-Pendulum-v1.zip(SB3 zip 格式),
推理脚本 inference.py 使用 torch_npu 在 npu:0 上运行。以下为四种操作模式:
cd ppo-Pendulum-v1-NPU
# ① 单步动作预测:给定一个观测 [cos, sin, thetadot],输出动作 + 值函数 + log_std
python3 inference.py --mode single --obs "0.0,1.0,0.0" --device npu:0
# ② 完整 episode 推理:从指定 seed 跑一整条轨迹(默认 200 步截断),打印抽样轨迹与回报
python3 inference.py --mode episode --seed 1 --deterministic --device npu:0 --detail-every 10
# ③ 批量评测:连续评测 N 个 seed 的 episode(默认 43),统计回报分布
python3 inference.py --mode evaluate --num-episodes 43 --deterministic --device npu:0 --save
# ④ 延迟 / 吞吐基准:统计单次策略前向耗时
python3 inference.py --mode benchmark --iterations 300 --device npu:0参数说明:
--device:推理设备,npu:0(默认)/ npu:1 / cpu(精度对拍用)。--deterministic:使用确定性动作(均值策略);不指定时使用 SDE 随机采样。--seed:同时固定 numpy / torch / npu 随机种子,保证随机推理可复现。--save:把结果保存到 outputs/*.json(默认目录 outputs/)。测试均在 npu:0 上完成,共 72 组测试用例,覆盖单步预测、完整轨迹、批量 episode 评测、
随机 SDE 推理、CPU/NPU 精度对拍与性能基准,全部输出为真实运行记录。
命令示例:
python3 inference.py --mode single --obs "0.0,1.0,0.0" --device npu:0 --deterministic实际终端输出(npu:0):
[INFO] 昇腾推理引擎: torch_npu 2.9.0.post1+gitee7ba04 | torch 2.9.0+cpu | NPU available=True | device=npu:0
[INFO] 模型加载成功: /data/models/HumanCompatibleAI/ppo-Pendulum-v1/ppo-Pendulum-v1.zip
[INFO] 策略: ActorCriticPolicy | use_sde=True | 网络=MlpExtractor | log_std 形状=(64, 1)
[INFO] 动作空间: Box(-2.0, 2.0, (1,), float32) | 观测空间: Box([-1. -1. -8.], [1. 1. 8.], (3,), float32)
[RESULT] 单步动作预测 (确定性):
obs = [0.0, 1.0, 0.0]
action = [-2.0]
value = -47.041252
log_std(前5维) = [-2.514487, -3.085547, -2.124068, -1.180484, -2.217466]10 组不同观测的完整输出汇总:
| 用例 | 观测 obs [cos, sin, θ̇] | 动作 action | 值函数 value |
|---|---|---|---|
| 1 | [0.0, 1.0, 0.0](θ=90°,静止) | -2.000000 | -47.041252 |
| 2 | [1.0, 0.0, 0.0](θ=0°,水平右) | -0.267384 | -0.065614 |
| 3 | [-1.0, 0.0, 0.0](θ=180°,水平左) | -1.908298 | -78.816574 |
| 4 | [0.0, -1.0, 0.0](θ=270°,倒挂) | +2.000000 | -45.921501 |
| 5 | [0.97, 0.24, 0.0](近竖直) | -2.000000 | -0.116712 |
| 6 | [0.997, -0.07, 0.0](平衡点附近) | +0.349201 | -0.146035 |
| 7 | [0.0, 1.0, 8.0](向下快速摆) | -2.000000 | -63.259060 |
| 8 | [0.0, 1.0, -8.0](向上快速摆) | +2.000000 | -7.578959 |
| 9 | [0.5, 0.866, 2.0](θ=60°,下摆) | -2.000000 | -47.365841 |
| 10 | [-0.5, 0.866, -3.0](θ=120°,上摆) | -2.000000 | -43.409199 |
解读: 策略输出与物理直觉一致——远离竖直位置时输出满扭矩
±2摆起;接近平衡点 (用例 6, obs≈[0.997,-0.07,0])输出+0.3492的小扭矩对抗重力维持平衡;值函数 在竖直附近最小(≈-0.07)、远离平衡位置时绝对值增大。
随机模式基于 StateDependentNoise 采样,--seed 42 保证可复现。相同 10 组观测:
| 用例 | 观测 obs [cos, sin, θ̇] | 动作 action | 值函数 value |
|---|---|---|---|
| 11 | [0.0, 1.0, 0.0] | -2.000000 | -47.041252 |
| 12 | [1.0, 0.0, 0.0] | -0.546874 | -0.065614 |
| 13 | [-1.0, 0.0, 0.0] | -1.287496 | -78.816574 |
| 14 | [0.0, -1.0, 0.0] | +2.000000 | -45.921501 |
| 15 | [0.97, 0.24, 0.0] | -2.000000 | -0.116712 |
| 16 | [0.997, -0.07, 0.0] | +0.096820 | -0.146035 |
| 17 | [0.0, 1.0, 8.0] | -2.000000 | -63.259060 |
| 18 | [0.0, 1.0, -8.0] | +2.000000 | -7.578959 |
| 19 | [0.5, 0.866, 2.0] | -2.000000 | -47.365841 |
| 20 | [-0.5, 0.866, -3.0] | -2.000000 | -43.409199 |
解读: 确定性 vs 随机仅在“非饱和”动作处有差异(用例 12: -0.2674 → -0.5469;用例 16: +0.3492 → +0.0968),说明 SDE 噪声的方差随状态变化(状态依赖噪声),且饱和动作(±2)不受噪声影响。
python3 inference.py --mode episode --seed 1 --deterministic --device npu:0 --detail-every 10实际输出(npu:0,return = -1.397779,200 步,每 10 步抽样):
[RESULT] Episode 轨迹 (seed=1, 确定性, 200 步, return=-1.397779):
step 0 obs=[0.997243, 0.074209, 0.900927] action=[-2.0]
step 10 obs=[0.998094, 0.061709, -0.390563] action=[-0.281659]
step 20 obs=[0.997618, -0.068984, -0.112428] action=[0.535947]
step 30 obs=[0.996923, -0.078381, 0.01186] action=[0.397249]
step 40 obs=[0.997331, -0.07301, 0.007543] action=[0.360065]
step 50 obs=[0.997484, -0.070889, 0.002289] action=[0.352549]
step 60 obs=[0.997525, -0.070307, 0.00057] action=[0.351021]
step 70 obs=[0.997535, -0.070169, 0.000129] action=[0.350723]
step 80 obs=[0.997537, -0.070139, 2.7e-05] action=[0.350667]
step 90 obs=[0.997538, -0.070132, 6e-06] action=[0.350656]
step 100 obs=[0.997538, -0.070131, 1e-06] action=[0.350654]
step 110 obs=[0.997538, -0.070131, 0.0] action=[0.350654]
step 120 obs=[0.997538, -0.070131, 0.0] action=[0.350654]
step 130 obs=[0.997538, -0.070131, -0.0] action=[0.350654]
step 140 obs=[0.997538, -0.070131, -0.0] action=[0.350654]
step 150 obs=[0.997538, -0.070131, 0.0] action=[0.350654]
step 160 obs=[0.997538, -0.070131, 0.0] action=[0.350654]
step 170 obs=[0.997538, -0.070131, -0.0] action=[0.350654]
step 180 obs=[0.997538, -0.070131, -0.0] action=[0.350654]
step 190 obs=[0.997538, -0.070131, -0.0] action=[0.350654]
[INFO] 总回报 = -1.397779, 平均动作 = 0.213688, 耗时 = 0.2041 s解读: 摆杆从
[0.997, 0.074, 0.9](接近竖直)出发,策略前几步摆动调整后在第 ~60 步收敛到[0.997538, -0.070131, ~0]的稳定平衡点(θ ≈ -4.0°,重力矩与施加扭矩0.3506平衡), 之后观测完全静止,角速度收敛到 0——典型的倒立摆“摆起并稳定”行为,回报仅 -1.40。
python3 inference.py --mode evaluate --num-episodes 43 --deterministic --device npu:0 --save实际输出(npu:0):
[RESULT] 批量评测 43 个 episode (确定性):
回报: mean=-216.1244, std=234.7515, min=-1490.9736, max=-0.9525, 平均步数=200.0| 用例 | seed | return | 用例 | seed | return | 用例 | seed | return |
|---|---|---|---|---|---|---|---|---|
| 22 | 0 | -137.4297 | 37 | 15 | -133.6376 | 52 | 30 | -271.6895 |
| 23 | 1 | -1.3978 | 38 | 16 | -3.4188 | 53 | 31 | -448.6918 |
| 24 | 2 | -271.1363 | 39 | 17 | -277.0116 | 54 | 32 | -270.7735 |
| 25 | 3 | -253.8230 | 40 | 18 | -137.1632 | 55 | 33 | -2.4535 |
| 26 | 4 | -1490.9736 | 41 | 19 | -3.5677 | 56 | 34 | -375.4601 |
| 27 | 5 | -130.1256 | 42 | 20 | -138.0503 | 57 | 35 | -135.3818 |
| 28 | 6 | -1.4899 | 43 | 21 | -133.5972 | 58 | 36 | -267.5669 |
| 29 | 7 | -134.4659 | 44 | 22 | -133.8522 | 59 | 37 | -134.8307 |
| 30 | 8 | -139.1168 | 45 | 23 | -134.6587 | 60 | 38 | -1.3173 |
| 31 | 9 | -282.0901 | 46 | 24 | -135.0872 | 61 | 39 | -0.9525 |
| 32 | 10 | -516.4454 | 47 | 25 | -257.8601 | 62 | 40 | -134.0916 |
| 33 | 11 | -268.5712 | 48 | 26 | -1.2446 | 63 | 41 | -396.1085 |
| 34 | 12 | -277.7774 | 49 | 27 | -137.3269 | 64 | 42 | -135.9141 |
| 35 | 13 | -256.4185 | 50 | 28 | -256.3600 | |||
| 36 | 14 | -270.3456 | 51 | 29 | -403.6752 |
解读: 43 组回报均落在
[-1490.97, -0.95],均值 -216.12 与模型卡片-189.25 ± 66.36同量级。 其中 8 组(seed 1/6/16/19/26/33/38/39)回报 > -5,说明摆杆初始即接近竖直可直接稳定; seed 4 等少数种子从底部随机初始角度无法在 200 步内完成摆起(回报最差 -1490.97)。 所有 episode 均跑满 200 步截断,动作有限、无 NaN,NPU 推理完整正常。
python3 inference.py --mode evaluate --num-episodes 5 --device npu:0 # 不指定 --deterministic| 用例 | seed | return(随机 SDE) |
|---|---|---|
| 65 | 0 | -810.841378 |
| 66 | 1 | -3.485036 |
| 67 | 2 | -830.852260 |
| 68 | 3 | -915.429787 |
| 69 | 4 | -1490.973616 |
解读: 随机模式下策略在动作中加入状态依赖噪声,轨迹变长/回报略差于确定性模式,属 SDE 探索的 正常行为;
--seed固定下多次运行逐位一致(见 5.7 可复现性说明)。
以 CPU(fp32) torch 参考实现为基准,对相同输入在 NPU(fp32) 上对拍(确定性推理):
用例 70 — 逐观测对拍(200 个随机观测,obs ~ U([-1,-1,-8],[1,1,8])):
| 指标 | 数值 |
|---|---|
| action 最大绝对误差 | 7.749e-07 |
| action 余弦相似度 | 1.000000000 |
| value 最大绝对误差 | 1.526e-05 |
| value 余弦相似度 | 1.000000000 |
用例 71 — 43-seed 轨迹回报对拍(seeds 0–42,确定性 rollout):
| 指标 | 数值 |
|---|---|
| 回报最大绝对差 | 0.0005 |
| 回报平均绝对差 | 0.0000 |
| NPU 平均回报 | -216.1244 |
| CPU 平均回报 | -216.1244 |
说明: 单步 action 在 CPU 与 NPU 上逐位一致(误差 < 8e-7,余弦相似度 1.0);value 差异 (< 1.6e-5)来自 CPU/NPU 浮点累加顺序不同,属 fp32 正常误差范围。43 组完整 rollout 的回报 平均绝对差为 0.0000,最大仅 0.0005——NPU 推理精度与 CPU 完全一致,误差远低于 1% 阈值。
python3 inference.py --mode benchmark --iterations 300 --device npu:0实际输出(npu:0):
[RESULT] 单次前向延迟基准 (300 次, deterministic):
平均延迟=0.788 ms | P50=0.7848 ms | P95=0.8743 ms | 最大=1.1674 ms
吞吐 = 1269.09 calls/s说明: 模型仅 9,026 参数,单次前向为 3 层 MLP,延迟主要受 NPU 任务下发开销限制,P95 稳定在 0.9 ms 以内;连续前向吞吐约 1,269 次/秒,完全满足 200 步实时控制的实时性需求。
--seed 同时固定 numpy / torch / npu 随机数生成器,相同 seed 下多次运行
逐位一致(已验证 seed=0 两次随机 rollout 回报均为 -836.507821)。运行 --save 后,结果保存于 outputs/ 目录:
| 文件 | 内容 |
|---|---|
outputs/evaluate_det_seed0.json | 43 组确定性 episode 的逐 seed 回报 |
outputs/evaluate_stoch_seed0.json | 5 组随机 SDE episode 的逐 seed 回报 |
outputs/episode_det_seed0.json | seed=0 完整轨迹(全部 200 步,未抽样) |
outputs/episode_det_seed1.json | seed=1 完整轨迹(平衡案例,抽样保存) |
outputs/single_step_det.json | 10 组单步确定性预测 (obs/action/value) |
outputs/single_step_stoch.json | 10 组单步随机 SDE 预测 (obs/action/value) |
| 指标 | 数值 |
|---|---|
| NPU 单次前向延迟(npu:0, fp32) | ~0.79 ms(P95 < 0.9 ms) |
| NPU 前向吞吐 | ~1,269 calls/s |
| 单条 episode(200 步闭环)耗时 | ~0.21 s |
| 权重大小 | 9,026 参数 ≈ 36 KB(fp32) |
| HBM 占用 | < 1 GB |
说明: 模型权重极小,HBM 占用可忽略;单条 episode 的 0.21 s 主要来自 200 步仿真环境 (gymnasium)推进与 200 次设备下发,纯策略计算占比很小。
贡献者: z_studio | 赛道: 模型适配赛道