Skip to content

03. 物理引擎工程实践:选型、调参与跨引擎 Debug

前置自测

📋 答不出 ≥ 2 题 → 先回 Ch01-02 复习对应内容

  1. MuJoCo 使用的接触模型(凸优化 vs LCP)和 PhysX 使用的接触模型(TGS 迭代)有什么本质区别?(Ch01 §1.3 预览)
  2. solrefsolimp 分别控制 MuJoCo 接触行为的什么方面?(Ch01 §1.3 接触入门)
  3. Newton 1.0 GA 包含哪 7 个求解器?Kamino 专门解决什么问题?(Ch01 §1.3 Newton)
  4. timestepdecimation 的关系是什么?policy frequency 怎么计算?(Ch01 §1.2 action repeat)
  5. CUDA Graph 的作用和限制是什么?什么操作会导致 graph 失效?(Ch01 §1.2)

本章目标

学完本章后,你应该能够:

  1. 解释三大引擎(MuJoCo Warp / PhysX / Newton)的物理建模差异,并根据任务需求做出选型判断
  2. 调参 MuJoCo 的 solref/solimp 和 PhysX 的 contact_offset/friction,消除穿透、弹跳和滑移
  3. 理解 MJCF 和 USD 两种模型格式的结构差异,知道互转时的常见坑
  4. 画出 MuJoCo Warp 从 MJCF 到 GPU batched worlds 的完整数据流
  5. 区分 CPU MuJoCo 与 MuJoCo Warp 的 solver 支持差异,避免盲目迁移配置
  6. 使用 sim2sim 验证策略的跨引擎鲁棒性
  7. 定位 物理层的稳定性和吞吐量瓶颈

前置知识桥接

回顾 Ch01 §1.3:我们介绍了物理引擎生态的六阶段演进——MuJoCo CPU → Isaac Gym → Isaac Lab → MJX → Newton → mjlab。你已经知道 MuJoCo 用凸优化接触、PhysX 用 TGS 迭代、Newton 统一了 7 个求解器。但你还不知道这些差异在工程层面意味着什么:什么时候选哪个引擎?参数怎么调?出了问题怎么查?

回顾 Ch02 §2.6:你已经完成了第一次训练并观察了 reward 曲线。如果训练中出现"机器人穿透地面"、"脚底打滑"或"关节抖动"——这些都是物理引擎层的问题,本章给你答案。

如果跳过本章会怎样

你可能在 Ch06(Reward 设计)中发现"机器人总是摔倒",花一周调 reward 权重——最后发现问题是 solref 参数太软导致脚底微滑。或者你在 Ch08(DR)中加入 friction 随机化后训练崩溃——因为你把摩擦系数随机到了 PhysX 不支持的范围。物理引擎是整个训练管线的地基——地基出问题,上层所有调试都是浪费时间。

预计阅读时间

阅读方式 时间 适合谁
精读(含练习) 4-5 小时 需要深入理解物理引擎选型和调参的读者
速读(跳过公式推导) 2-3 小时 有物理仿真经验,重点看双框架对比和调参表
速查(只看调参表和故障排查) 45 分钟 遇到具体物理问题时回来查

3.1 三大引擎的物理建模思想 ⭐⭐

这一节解决什么问题:理解 MuJoCo、PhysX 和 Newton 在物理建模上的本质差异——这决定了它们各自的强项和限制。

动机

Ch01 从"选型"角度介绍了三大引擎,但没有深入它们的物理建模思想。为什么同一个机器人在 MuJoCo 和 PhysX 中"走得不一样"?为什么 MuJoCo 擅长接触密集任务而 PhysX 擅长大规模场景?答案在于它们解决接触力求解问题的根本方法不同。

如果不理解建模差异会怎样

你在 MuJoCo 中训练了一个四足 locomotion 策略,效果很好。然后你想用 Isaac Lab(PhysX)做 sim2sim 验证——发现机器人行为完全不同。你以为是 bug,花两天 debug——最后发现是两个引擎的摩擦模型不同导致的。这不是 bug,是物理建模差异的正常表现(ASAP 论文 RSS'25 系统量化了这种差异)。理解建模差异,才能区分"配置错误"和"引擎特性差异"。

MuJoCo:广义坐标 + 凸优化接触

MuJoCo(Multi-Joint dynamics with Contact)由 Emo Todorov 在 2012 年设计(论文:MuJoCo: A physics engine for model-based control,IROS 2012),核心设计决策是将机器人动力学表述为广义坐标下的凸优化问题

广义坐标意味着 MuJoCo 用最少的变量描述系统状态。一个旋转关节只需要 1 个角度变量(而非 3×3 旋转矩阵或 4 元素四元数),自由基座用 7 个变量(3 平移 + 4 四元数)。这导致一个重要的工程特征:nq(位置维度)不等于 nv(速度维度)——因为四元数有 4 个分量但角速度只有 3 个分量。对于一个有 free joint 基座 + 23 个旋转关节的人形机器人:nq = 7 + 23 = 30,但 nv = 6 + 23 = 29

凸优化接触是 MuJoCo 与其他引擎最大的区别。传统引擎(如 ODE、Bullet)用 LCP(线性互补问题)求解接触力——这在数学上是 NP-hard 的,求解器只能做近似。MuJoCo 的做法不同:它把刚性接触松弛为软接触(允许微小穿透),然后把接触力求解表述为一个凸优化问题。凸优化有全局最优解且总是可解——这意味着 MuJoCo 的求解器不会发散

核心动力学方程:

\[M(q)\dot{v} + c(q,v) = \tau + J(q)^T f\]
符号 含义 mjlab 中的工程对应
\(M(q)\) 广义质量矩阵 MjData.qM(稀疏下三角)
\(\dot{v}\) 广义加速度 MjData.qacc
\(c(q,v)\) 科氏力 + 重力项 MjData.qfrc_bias
\(\tau\) 执行器力矩 MjData.qfrc_actuator(经 actuator 模型变换后)
\(J(q)\) 接触 Jacobian MjData.efc_J(稀疏矩阵,连接接触力与关节力)
\(f\) 接触力 MjData.efc_force(由 solver 通过凸优化求解)

本质洞察:MuJoCo 的每一步仿真都在解一个优化问题——给定当前状态和执行器力矩,找到使穿透最小化的接触力。这就是为什么 MuJoCo 的接触行为比游戏引擎更"物理正确",但计算代价也更高。

从方程到调试:理解方程的工程价值不在于推导,而在于它给你一张"现象→方程项→检查参数"的映射表:

现象 方程中的对应 第一检查项
机器人飞走 \(\tau\) 太大 action scale、actuator gains
机器人穿透地面 \(f\) 接触解异常 contact 参数、solver iterations
高频关节抖动 \(M\)-damping-timestep 不稳 timestep、integrator、kd
动作没有效果 \(\tau\) 链路断裂 ctrl 地址、actuator target
脚底打滑 \(f\) 中摩擦力不足 friction、condim、cone
关节到限位卡死 \(f\) 的 joint limit 约束 joint range、solref
奇怪的旋转 \(c\) 中科氏/离心力 惯量参数是否正确

如果你在训练中遇到"机器人做了物理上不合理的事",第一反应不应该是调 reward——而是回到这张表,检查物理层是否正确。Reward 只能引导 policy 优化方向,但如果物理本身就不稳定,再好的 reward 也无法让 policy 学到合理行为。

关键物理事实:位置控制不是改位置。 MuJoCo 中"设置 joint position target = 0.5 rad"不会直接把关节移到 0.5 rad——actuator 只产生一个朝向 0.5 rad 的力矩,关节是否到达取决于惯量、摩擦、接触和 timestep。这意味着:kp 过大 + timestep 过粗 → 力矩 overshoot → 振荡。

Integrator 选型:MuJoCo 共支持四种积分器(EulerimplicitimplicitfastRK4);训练中最常用的是下面两种,对训练稳定性有直接影响(注意 MuJoCo 不实现"显式 Euler",其 Euler 是半隐式、并对 joint damping 做隐式处理):

Integrator 核心思想 对 actuator 的处理 mjlab 默认
euler 半隐式 Euler(用新速度更新位置,隐式处理 joint damping) 视 actuator 力矩为"外部常量"
implicitfast 快速隐式(implicit-in-velocity) 对 builtin actuator 隐式处理速度依赖 ✅ 默认

为什么 implicitfast + builtin actuator 最稳? Builtin actuator 创建 MuJoCo 原生 <position>/<velocity> 元素——integrator 能看到这些速度相关力,implicitfast 可以隐式处理已知的速度依赖(就像把弹簧阻尼器装进物理引擎内部——引擎知道弹簧公式,积分时提前考虑阻尼)。而 Explicit actuator 在 PyTorch 中算 torque,通过 <motor> passthrough——integrator 看不到控制律对速度的导数,高增益 PD 更容易不稳。

mjlab 的 MujocoCfgsrc/mjlab/sim/sim.py):

@dataclass
class MujocoCfg:
    # 积分器
    timestep: float = 0.002          # physics dt(不是 policy dt!)
    integrator: Literal["euler", "implicitfast"] = "implicitfast"

    # 接触模型
    impratio: float = 1.0            # 增大可减少微滑
    cone: Literal["pyramidal", "elliptic"] = "pyramidal"

    # 求解器
    solver: Literal["newton", "cg", "pgs"] = "newton"
    iterations: int = 100            # solver 最大迭代数
    tolerance: float = 1e-8          # solver 收敛阈值

    # 环境
    gravity: tuple[float, float, float] = (0.0, 0.0, -9.81)

这个配置通过 MujocoCfg.apply(model) 写入 mujoco.MjModel.opt。每个字段直接对应 MuJoCo 的 mjOption 结构。

MuJoCo 的三种 solver

Solver 方法 适用场景 mjlab 默认
Newton 二阶方法(Hessian + Cholesky) 小-中规模,精度要求高 ✅ 默认
CG 共轭梯度(无 Cholesky) 较大系统,精度可接受 可选
PGS 投影 Gauss-Seidel 简单系统,速度优先 ⚠️ MuJoCo Warp 不完整支持

MuJoCo Warp 的关键限制:MuJoCo Warp 是 MuJoCo 的 GPU 版本(Google DeepMind + NVIDIA 共同维护),但不是 CPU 版本的完全复制。以下是 CPU vs GPU 的差异表:

特性 CPU MuJoCo MuJoCo Warp 影响
Newton solver ✅(默认)
CG solver
PGS solver ⚠️ 不完整支持 不要用 PGS
noslip 高摩擦场景需调 impratio
Sparse Jacobian ❌(总是 dense) 大系统显存更高
Override contact ❌(o_margin/o_solref/o_solimp 不可用) DR 需用 expand_model_fields
自动微分 需可微分→用 MJX
float64 ❌(仅 float32) 极端精度需求不满足
>60 DOF ⚠️ 性能下降 超大系统需注意吞吐

反事实推理:如果 MuJoCo 用硬接触(像 PhysX 一样),它在灵巧手操作(几十个接触点同时活跃)中会经常求解失败——因为 LCP 在接触点多且互相约束时容易不收敛。这就是 Todorov 选择凸优化路线的核心理由。

PhysX:笛卡尔坐标 + TGS 迭代求解

PhysX 是 NVIDIA 的物理引擎,从游戏引擎演进而来,现在是 Isaac Lab 的默认后端。它的设计哲学与 MuJoCo 截然不同。

笛卡尔坐标意味着 PhysX 用 position + orientation 来表示每个刚体的状态,关节通过约束(而非最小坐标)来实现。这在建模上更直观(每个物体的位置/旋转都是显式的),但代价是需要在每一步求解约束来保持关节连接。

TGS(Temporal Gauss-Seidel)求解器是 PhysX 5 的核心创新。传统 PGS 求解器对整个时间步做 N 次迭代,而 TGS 把时间步细分为 N 个等大的子步,每个子步做一次约束求解后立即更新位置——这等效于"小 dt × 单次迭代",对接触和 articulation 的数值稳定性显著更好。

PhysX 的关键参数:

参数 含义 默认值 调参方向
solver_position_iteration_count TGS 位置求解迭代数(per-body,设在 RigidBodyPropertiesCfg/ArticulationRootPropertiesCfgPhysxCfgmin/max_position_iteration_count 4 增加→更精确但更慢
solver_velocity_iteration_count TGS 速度求解迭代数(同上) 1 通常不需要改
contact_offset 两个形状距离小于此值时生成接触(属 CollisionPropertiesCfg,非 PhysxCfg 0.02 m 过小→穿透,过大→虚假接触
rest_offset 两个形状静止时的间距(属 CollisionPropertiesCfg 0.01 m 通常与 contact_offset 配合
bounce_threshold_velocity 触发弹跳的最小相对速度(PhysxCfg,scene 级) 0.5 m/s 降低→更自然的弹跳
friction_correlation_distance 接触点合并为单个 friction anchor 的距离 0.025 m 小物体需降低
solver_type PGS (0) 或 TGS (1) 1 (TGS) 始终推荐 TGS

PhysX 的 patch friction 模型:PhysX 不是对每个接触点独立计算摩擦力,而是把相邻的接触点合并成一个"接触片"(patch),然后在 patch 上计算摩擦。这在大多数情况下足够好,但在需要精确逐点摩擦的场景(如灵巧手操作、足端抓地力分析)中,PhysX 的摩擦行为可能不如 MuJoCo 精确。

Isaac Lab 中 PhysX 的完整配置SimulationCfg):

from isaaclab.sim import SimulationCfg, PhysxCfg

sim_cfg = SimulationCfg(
    dt=0.005,                          # physics timestep
    render_interval=2,                  # 渲染间隔(不影响物理)
    gravity=(0.0, 0.0, -9.81),
    physx=PhysxCfg(
        # 求解器
        solver_type=1,                  # 0=PGS, 1=TGS(始终推荐 TGS)
        num_position_iterations=4,      # TGS 位置迭代(= 子步数)
        num_velocity_iterations=1,      # TGS 速度迭代
        enable_stabilization=True,      # 启用 solver 稳定化

        # 接触检测
        contact_offset=0.02,            # 接触检测距离 (m)
        rest_offset=0.01,               # 静止间距 (m)
        bounce_threshold_velocity=0.2,  # 弹跳阈值 (m/s)

        # 摩擦
        friction_offset_threshold=0.04, # 摩擦开始计算的距离
        friction_correlation_distance=0.025,  # 接触点合并距离

        # GPU 参数
        gpu_found_lost_pairs_capacity=2**21,
        gpu_max_rigid_contact_count=2**20,
        gpu_max_rigid_patch_count=2**17,
    ),
)

API 提示(重要):上面的代码块是"概念示意",字段名按 Isaac Lab v2.3.0 实际 API 需要拆分:PhysxCfg(scene 级)用 min_position_iteration_count/max_position_iteration_count/min_velocity_iteration_count/max_velocity_iteration_count,没有 num_position_iterations 这个名字;per-body 的 solver 迭代数设在资产的 RigidBodyPropertiesCfg/ArticulationRootPropertiesCfgsolver_position_iteration_count 等);contact_offset/rest_offset 属于 CollisionPropertiesCfg。照抄会因字段不存在报错。

双重解读:position iteration count 在 PhysX TGS 中有两重含义:

角度 1(求解器视角):它是"迭代次数"——更多迭代 = 更精确的约束求解 角度 2(积分器视角):它是"子步数"——4 次迭代等效于把 dt 分成 4 个 dt/4 的子步,每个子步解一次约束后立即更新位置

这就是为什么 TGS 的收敛行为比 PGS 好——PGS 用 4 次迭代解同一个大 dt 的问题,TGS 用 4 个小 dt 逐步逼近。

Isaac Lab 中 decimation 的设置

# 在 ManagerBasedRLEnvCfg 中
class VelocityEnvCfg(ManagerBasedRLEnvCfg):
    decimation = 4   # env.step() 中调用 sim.step() 的次数
    # policy_dt = sim_cfg.dt * decimation = 0.005 * 4 = 0.02s
    # policy_freq = 1 / policy_dt = 50 Hz

mjlab 中的对应

# velocity_env_cfg.py
class VelocityEnvCfg(ManagerBasedRLEnvCfg):
    decimation = 4
    sim = SimulationCfg(
        mujoco=MujocoCfg(timestep=0.005),
    )
    # policy_freq = 1 / (0.005 * 4) = 50 Hz

两个框架的 decimation 语义完全相同——这是 Manager-Based 架构趋同的又一个体现。

一个跨领域类比:MuJoCo 的凸优化接触像"精确数学求解"——慢但总能给出最优解。PhysX 的 TGS 迭代像"渐进式逼近"——快且通常足够好,但在极端情况下可能不收敛。这类似于求解线性方程组时,直接法(LU 分解)vs 迭代法(Jacobi/Gauss-Seidel)的权衡。

Newton 1.0:多求解器统一 API

回顾 Ch01 §1.3:Newton(NVIDIA × Google DeepMind × Disney Research 联合开发,构建于 NVIDIA Warp + OpenUSD,Apache 2.0,Linux Foundation 托管)官方定位是一个开源、GPU 加速、可扩展的物理仿真引擎。它区别于传统引擎的关键特征是模块化的多求解器架构——用统一的数据模型和 API 组织多个 solver,便于按任务接入不同求解器。下表是其当前文档示例化的若干 solver(具体列表随版本变化,以目标 Newton release 文档为准):

求解器 来源 接触模型 适用场景
MuJoCo Warp DeepMind 凸优化 通用刚体,足式 locomotion
Kamino Disney Research Proximal-ADMM 闭环机构(平行连杆腿)
XPBD 社区 位置约束投影 软体、小规模接触
VBD Style3D Vertex Block Descent 布料、线缆
Featherstone 标准算法 机械臂运动链
Implicit MPM 研究 连续介质 颗粒、沙地、雪地
SemiImplicit 基础 半隐式积分 通用 baseline

Newton 在 Isaac Lab 3.0 Beta 中通过 isaaclab_newton extension 集成。当你在 Isaac Lab 中选择 Newton 后端时,实际使用的是 MuJoCo Warp 求解器(也可以切换到其他求解器)。

Kamino 求解器深入:Kamino(Disney Research,arXiv:2603.16536)是 Newton 中最值得关注的新求解器——它是首个能在 GPU 上高效仿真闭环机构的求解器。

什么是闭环机构?大多数机器人关节链是开链(open chain)——从基座到末端只有一条路径。但一些高性能腿式机器人(如 Digit 的平行连杆腿、BRUCE 的四连杆膝关节)使用闭环机构(closed-loop / parallel linkage)——从基座到末端有多条路径形成环。

拓扑 标准 MuJoCo 方法 Kamino 方法 精度
开链 广义坐标(原生支持) 也支持 相同
闭链 equality constraint 近似 maximal-coordinate + Proximal-ADMM Kamino 更精确

MuJoCo 处理闭环的传统方法是用 <equality type="connect"> 约束"焊接"两个关节——这在物理上是近似的(允许微小违反)。Kamino 用 maximal-coordinate + Proximal-ADMM 直接在约束满足的空间内求解——闭环约束被精确满足。

实用建议:如果你的机器人没有闭环机构(大多数四足和人形),不需要 Kamino——MuJoCo Warp 就够了。如果你的机器人有平行连杆腿,目前用 MuJoCo 的 equality constraint 可以工作,但考虑在 Newton/Kamino 成熟后迁移。

Newton 的 SDF collision library:Newton 还提供有符号距离场(SDF)碰撞检测——这对非凸形状(如人手的复杂几何)的碰撞精度比传统凸分解更好。

Newton 的价值不在于"又多了一个选择",而在于它让跨引擎验证变得简单——同一套 Isaac Lab 代码,只需改一行配置就能切换 PhysX 和 MuJoCo Warp,观察策略在不同物理模型下的行为差异。

在 Isaac Lab 3.0 中使用 Newton

# Isaac Lab 3.0 kit-less 模式 + Newton 后端
# 训练命令
./isaaclab.sh -p scripts/reinforcement_learning/rsl_rl/train.py \
    --task Isaac-Velocity-Flat-Anymal-C-v0 \
    --num_envs 4096 \
    presets=newton  # 一行切换到 Newton/MuJoCo Warp 后端

注意:Newton 后端在 Isaac Lab 3.0 Beta 中不支持 deformable objects、surface grippers 和 material randomization——这些功能仍需要 PhysX。

Newton 的价值不在于"又多了一个选择",而在于它让跨引擎验证变得简单——同一套 Isaac Lab 代码,只需改一行配置就能切换 PhysX 和 MuJoCo Warp,观察策略在不同物理模型下的行为差异。

三引擎核心对比表

维度 MuJoCo (Warp) PhysX 5 Newton
坐标系统 广义坐标(最小表示) 笛卡尔 + 约束 取决于子求解器
接触模型 凸优化(软接触) TGS 迭代(硬接触近似) MuJoCo Warp 或其他
摩擦模型 逐接触点(elliptic/pyramidal cone) Patch friction(合并相邻接触点) 取决于子求解器
求解保证 总是收敛(凸优化) 可能不收敛(迭代近似) 取决于子求解器
接触精度 高(密集接触场景优势明显) 中-高(大多数场景足够) 高(通过 MuJoCo Warp)
大规模场景 中(>60 DOF 性能下降) 高(游戏引擎基因)
模型格式 MJCF URDF/USD 两者都支持
渲染 Viser(Web-based) Isaac Sim(RTX 光追) 继承 Isaac Lab
使用框架 mjlab Isaac Lab(默认) Isaac Lab 3.0 Beta

⚠️ 常见陷阱

  1. 认为"接触精度高 = 更好"。 对于很多任务(如桌面推物体、导航避障),PhysX 的接触精度完全足够。MuJoCo 的优势主要体现在足端抓地力需要精确控制的任务(如四足粗糙地形、灵巧手操作)。
  2. 把 CPU MuJoCo 的 solver 配置直接迁移到 MuJoCo Warp。 PGS solver 在 MuJoCo Warp 上不完整支持。MujocoCfg.solver 的类型提示包含 "pgs" 字符串——但这不代表 GPU 后端能正确运行它。
  3. 忽略 float32 限制。 MuJoCo Warp 仅支持 float32,而 CPU MuJoCo 支持 float64。在需要极端数值精度的场景(如微小力矩的精确控制),GPU 结果可能与 CPU 有微小差异。
  4. 认为 Newton = 新引擎。 Newton 不是一个新的物理求解器——它是已有求解器(MuJoCo Warp、Kamino 等)的统一封装。使用 Newton 不会获得比直接使用 MuJoCo Warp 更高的精度或速度。
  5. 在 PhysX 中用 PGS 替代 TGS。 PhysX 文档明确推荐"Whenever possible, using TGS instead of PGS is highly recommended"。TGS 在数值稳定性和收敛质量上全面优于 PGS。

练习

  1. 解释 MuJoCo 动力学方程 \(M(q)\dot{v} + c(q,v) = \tau + J(q)^T f\) 中每一项的物理含义。如果 \(f = 0\)(无接触),方程退化成什么?这在什么物理场景下成立?(提示:机械臂在空中运动时。)
  2. 为什么 MuJoCo 的 nq(位置维度)不等于 nv(速度维度)?对于一个有 free joint 基座 + 12 个旋转关节的四足机器人,计算 nqnv。对于有 free joint + 29 旋转关节 + 2 个 ball joint 的人形呢?
  3. PhysX 的 TGS 求解器为什么比 PGS 更好?从"子步 + 即时位置更新"的角度解释。如果 num_position_iterations=4,TGS 等效于多大的有效 timestep?
  4. 解释 builtin actuator 和 explicit actuator 的区别。为什么 implicitfast integrator 对 builtin actuator 更稳定?从"integrator 能看到什么"的角度回答。
  5. (跨章综合题)回顾 Ch01 §1.3 的 Newton benchmark 数据(locomotion 252× MJX,manipulation 475× MJX)。这些加速比是物理求解本身的加速,还是包含了其他因素(如 CUDA Graph、内存优化、batch 并行)?本章的哪些内容帮你回答这个问题?
  6. (源码阅读题)打开 mjlab 的 velocity_env_cfg.py,找到 MujocoCfg 的配置。记录 solver、integrator、timestep、iterations 的值。解释为什么选择这些默认值(提示:结合本节的 solver 对比表和 integrator 选型分析)。

上一节建立了三大引擎的物理建模直觉。接下来的实际问题是:面对一个新任务,我该选哪个引擎? 这不是一个学术问题,而是一个有明确工程答案的选型决策。

3.2 引擎选型决策树 ⭐⭐⭐

这一节解决什么问题:给出从任务需求到引擎选择的完整决策流程,避免"凭直觉选型"导致的返工。

动机

回顾 Ch01 §1.4:我们给出了框架级选型(mjlab vs Isaac Lab)的决策树。但框架选型和引擎选型是两个独立的决策——特别是 Isaac Lab 3.0 开始支持多后端后,在 Isaac Lab 中也需要选择 PhysX 还是 Newton/MuJoCo Warp。

如果选型错误会怎样

真实案例:某同学要做灵巧手操作(Allegro hand + 物体抓取),选了 Isaac Lab + PhysX 默认后端。训练了两周,策略学会了"用手掌按住物体"而不是"用手指灵巧抓取"——因为 PhysX 的 patch friction 模型把多个指尖接触合并成了一个大 patch,手指间的独立摩擦力丢失了。如果他一开始选择 mjlab(MuJoCo Warp 的逐接触点摩擦),可能一周内就训出更好的策略。

选型决策流程

新任务开始
    │
    ├─ 任务需要 RGB/Depth 视觉输入?
    │     ├─ 是 → Isaac Lab(RTX 渲染)
    │     │     └─ 接触精度重要?
    │     │           ├─ 是 → Isaac Lab 3.0 + Newton (MuJoCo Warp)
    │     │           └─ 否 → Isaac Lab + PhysX(默认)
    │     └─ 否 → 继续判断
    │
    ├─ 任务是否接触密集?
    │     ├─ 足式 locomotion(足端频繁建立/断开接触)→ mjlab(MuJoCo Warp)
    │     ├─ 灵巧手操作(多指接触耦合)→ mjlab(MuJoCo Warp)
    │     ├─ 桌面推/抓取(少量接触点)→ PhysX 足够
    │     └─ 物流/码垛(大规模刚体堆叠)→ PhysX 更快
    │
    ├─ 需要闭环机构仿真?
    │     ├─ 是(如平行连杆腿)→ Newton + Kamino
    │     └─ 否 → 继续判断
    │
    ├─ 需要软体/布料仿真?
    │     ├─ 是 → Newton + VBD/XPBD
    │     └─ 否 → 继续判断
    │
    ├─ 需要对比多种 RL 算法?
    │     ├─ 是 → Isaac Lab(多 RL 后端)
    │     └─ 否 → mjlab(RSL-RL 足够)
    │
    └─ 需要最快启动和最简安装?
          └─ 是 → mjlab(`pip install mjlab`,2 秒启动)

选型速查表

任务类型 推荐引擎 推荐框架 理由
四足 flat/rough terrain MuJoCo Warp mjlab 接触精度 + 安装简单
人形 locomotion MuJoCo Warp mjlab 或 Isaac Lab 两者都有强支持
灵巧手操作 MuJoCo Warp mjlab 或 Isaac Lab 3.0 逐接触点摩擦
机械臂 pick-and-place PhysX Isaac Lab 接触要求不高 + RTX 渲染
视觉策略(RGB 输入) PhysX Isaac Lab RTX 渲染是刚需
多算法对比 PhysX Isaac Lab 多 RL 后端
快速原型验证 MuJoCo Warp mjlab 安装快、启动快
闭环机构(平行连杆) Kamino Isaac Lab 3.0 + Newton 唯一支持闭环的 GPU 求解器
布料/线缆操作 VBD Isaac Lab 3.0 + Newton VBD + MuJoCo Warp 耦合
颗粒/沙地 Implicit MPM Isaac Lab 3.0 + Newton 连续介质仿真

双框架选型的核心权衡

维度 选 mjlab 的理由 选 Isaac Lab 的理由
安装 pip install,2 分钟 需要 Isaac Sim,30-60 分钟
启动 ~2 秒 ~15-30 秒
接触精度 MuJoCo Warp 原生 PhysX 默认,Newton 可选
视觉 Viser(无 RTX) RTX 光追
RL 后端 RSL-RL only RSL-RL / RL Games / SKRL / SB3
社区 2.2k Stars,增速快 6.5k Stars,社区最大
内置任务 ~10 ~30
迭代速度 极快(适合 reward 调参) 中等(适合最终训练)

本质洞察:引擎选型不是"哪个更好"的问题,而是"哪个更适合你当前任务"的问题。很多研究者在同一个项目中使用两个框架——在 mjlab 中快速迭代 reward 设计,在 Isaac Lab 中做最终训练和视觉策略。

实际研究场景的选型分析

场景 1:博士一年级,复现 humanoid-gym(RSS 2024)

humanoid-gym 基于 Isaac Gym(已停止维护)。你需要选择在哪个现代框架中重实现。

分析: - humanoid-gym 的核心贡献是 reward 设计和 sim2sim 验证,不依赖特定的物理引擎特性 - G1/H1 是人形机器人,接触密集(双脚频繁建立/断开接触) - 不需要视觉输入 - 你只需要 PPO(humanoid-gym 只用 PPO)

结论:mjlab。理由:(1) MuJoCo Warp 的接触精度更适合双足接触,(2) mjlab 已内置 G1 velocity task,(3) pip install 2 分钟安装。

场景 2:做视觉引导的桌面操作(RGB 相机 + Franka 机械臂)

分析: - 需要 RGB 渲染 → 必须有 RTX 渲染支持 - 桌面操作的接触点较少(机械臂 + 物体 ≤ 10 个接触点) - 可能需要对比 PPO 和 SAC 的表现

结论:Isaac Lab + PhysX。理由:(1) RTX 渲染是刚需,(2) 接触精度要求不高——PhysX 足够,(3) 多 RL 后端支持。

场景 3:四足 + 机械臂的移动操作(Go2 + Z1)

分析: - 四足部分接触密集 → 倾向 MuJoCo - 操作部分可能需要视觉 → 倾向 Isaac Lab - 这是一个典型的"两者都需要"的场景

结论:开发阶段用 mjlab(快速迭代 locomotion reward),最终训练用 Isaac Lab(如果需要视觉)或继续 mjlab(如果不需要视觉)

场景 4:闭环机构的双足机器人(如 Digit 的平行连杆腿)

分析: - 平行连杆是闭环机构 → 标准 MuJoCo 和 PhysX 都不原生支持 - Newton 的 Kamino 求解器专门处理闭环机构 - 但 Newton 在 Isaac Lab 中的集成仍是 Beta

结论:短期:mjlab + MuJoCo 的 equality constraint 近似闭环。长期:等 Newton/Kamino 在 Isaac Lab 3.0 中成熟后迁移

反事实推理:如果你在场景 4 中选了 PhysX 并试图用刚性约束模拟平行连杆——PhysX 的 TGS 迭代求解器在互相竞争的硬约束上容易数值不稳定(NVIDIA 稳定性指南明确提到这个问题)。Kamino 的 Proximal-ADMM 算法专门为这类拓扑设计,是正确的长期选择。

⚠️ 常见陷阱

  1. 因为"大家都用 Isaac Lab"而选择 Isaac Lab。 如果你的任务是纯 locomotion 且不需要视觉,mjlab 的安装简单和启动速度优势非常显著。
  2. 认为 Newton 已经可以无缝替代 PhysX。 Newton 在 Isaac Lab 3.0 Beta 中的集成仍在开发中,缺少 deformable objects、surface grippers 和 material randomization 支持。
  3. 在选型时过度关注 benchmark 数字。 Newton vs PhysX 的 65% 速度优势(灵巧手操作)在大多数研究场景中不是瓶颈——reward 设计和 DR 策略对最终性能的影响远大于引擎选择。

练习

  1. 为以下三个研究课题选择引擎和框架,并说明理由:(a) ANYmal D 在楼梯上行走,(b) Franka 从 RGB 图像抓取桌面物体,(c) 平行连杆腿双足机器人的 locomotion。每个课题给出明确的引擎(MuJoCo Warp/PhysX/Newton+Kamino)和框架(mjlab/Isaac Lab)选择。
  2. 你当前的研究课题是什么?用上面的决策流程做一次选型。写出你的选择和理由(至少 3 句话)。
  3. (思考题)如果 Isaac Lab 3.0 的 Newton 集成完全成熟(所有 PhysX-only 功能都有对应),mjlab 的独特价值还有什么?列出至少 3 个 mjlab 在 Newton 时代仍然有价值的理由。
  4. 某同学要做"人形全身操作"(上半身抓物体 + 下半身行走)。分析这个任务的接触特征——上半身(操作)接触要求如何?下半身(locomotion)接触要求如何?你会推荐什么引擎?为什么这个任务比纯 locomotion 或纯操作更难选型?
  5. (跨章综合题)回顾 Ch01 §1.4 的双框架教学核心理由。本章的引擎选型分析为"双框架教学的收益 > 成本"提供了什么证据?提示:如果只会一个框架,你在场景 3(四足+操作)中会怎样?

引擎选好了,但引擎的"出厂设置"不一定适合你的任务——接触参数需要根据任务需求调优。这是物理引擎工程实践中最需要动手经验的部分。

3.3 接触参数调优实战 ⭐⭐⭐

这一节解决什么问题:掌握 MuJoCo 和 PhysX 的核心接触参数,建立"从症状到参数"的调优直觉。

动机

回顾 Ch01 §1.3 的接触模型入门:我们介绍了 MuJoCo 的 solref/solimp 和一个材质推荐表。本节深入这些参数的工程含义,并补充 PhysX 的对应参数——让你在两个框架中都能调优接触行为。

如果不调接触参数会怎样

MuJoCo 和 PhysX 的默认接触参数为"通用场景"设计。但"通用"不等于"你的任务":足式机器人需要高摩擦的地面接触(否则脚底打滑),灵巧手需要适中的物体摩擦(太高则放不下,太低则抓不住),工业装配需要精确的接触刚度(否则零件不对齐)。

MuJoCo 接触参数:solref 和 solimp

MuJoCo 的软接触模型由两个参数组控制:

solref = [timeconst, dampratio](标准格式):控制接触的"弹簧-阻尼"行为。

参数 含义 效果
timeconst 接触恢复的时间常数 值越小→接触越硬(恢复越快)
dampratio 阻尼比 1.0 = 临界阻尼,<1 = 欠阻尼(有振荡),>1 = 过阻尼

solimp = [d0, d1, width, midpoint, power]:控制约束阻抗函数的形状。对大多数任务,默认值 [0.9, 0.95, 0.001, 0.5, 2] 足够好。只有在模拟特殊材质时需要修改。

材质推荐表(来源:ROBOLAWEB solref/solimp Cheat Sheet + 社区验证):

材质 solref solimp 适用场景 典型表现
金属 [0.005, 1.0] 默认 关节轴承、金属工具 极硬,几乎无变形
硬塑料 [0.01, 1.0] 默认 机器人外壳、地面 硬且稳定
橡胶 [0.02, 1.5] [0.95, 0.99, 0.001, 0.5, 2] 足底垫、轮胎 有弹性,接触面积大
海绵 [0.05, 2.0] [0.9, 0.95, 0.005, 0.5, 2] 缓冲垫、软体操作 明显变形
locomotion 地面 [0.02, 1.0] 默认 四足/人形行走 mjlab 默认值附近

MuJoCo 接触调优的三步流程

  1. 先用默认值训练——大多数情况下默认值就够好
  2. 观察问题症状——穿透?弹跳?微滑?
  3. 按症状调参
症状 原因 调参方向
脚底穿透地面 solref 太软(timeconst 太大) 减小 timeconst(如 0.02→0.005)
物体落地后反复弹跳 dampratio 太低 增加 dampratio(如 1.0→1.5)
静止物体在斜面上缓慢滑动 MuJoCo 软接触的固有微滑 增加 model.opt.impratio(如 1→10)
高速碰撞后物体"粘在一起" solref 太软 + solimp d0 太低 减小 timeconst + 增加 d0
关节在接触时高频振荡 timestep 太大 减小 timestep(如 0.005→0.002)

mjlab 中修改接触参数的代码示例

# 方法 1:在 MujocoCfg 中全局设置
from mjlab.sim import MujocoCfg

sim_cfg = MujocoCfg(
    timestep=0.005,
    solver="newton",
    iterations=10,
    impratio=5.0,      # 增大以减少微滑(默认 1.0)
    cone="pyramidal",  # pyramidal(快)或 elliptic(精确)
)

# 方法 2:在 MJCF 模型中为不同 geom 设置不同参数
# Go2 的 MJCF 中,足端 geom 的 friction 比身体 geom 更高
"""
<default class="foot">
    <geom friction="1.0 0.005 0.001" solref="0.02 1.0"/>
</default>
<default class="body">
    <geom friction="0.4 0.005 0.001"/>
</default>
"""

# 方法 3:运行时通过 expand_model_fields 修改(DR 使用)
# 这会触发 CUDA Graph 重建——只在 reset/startup 时做

Isaac Lab 中修改接触参数的代码示例

# 在 SimulationCfg 中设置 PhysX 参数
from isaaclab.sim import SimulationCfg, PhysxCfg

sim_cfg = SimulationCfg(
    dt=0.005,          # physics timestep
    physx=PhysxCfg(
        solver_type=1,                # 0=PGS, 1=TGS(始终推荐 TGS)
        num_position_iterations=8,    # 默认 4,增加提高精度
        num_velocity_iterations=1,    # 通常不需要改
        contact_offset=0.02,          # 接触检测距离
        rest_offset=0.01,             # 静止间距
        bounce_threshold_velocity=0.2,
    ),
)

# 在 MaterialPropertiesCfg 中设置摩擦
from isaaclab.sim import MaterialPropertiesCfg

ground_material = MaterialPropertiesCfg(
    static_friction=1.0,
    dynamic_friction=1.0,
    restitution=0.0,    # 完全非弹性碰撞
)

PhysX TGS 求解器深入

PhysX 的 TGS 值得深入理解,因为它是 Isaac Lab 默认后端的核心。

TGS 的关键创新是把 solver iterations 等价于 substeps:每个 position iteration 不是"再解一遍约束",而是"用更小的 dt 推进一步"。具体来说:

  1. 将 timestep dt 分成 N 个子步(N = num_position_iterations
  2. 每个子步:解一次约束 → 立即更新位置 → 进入下一子步
  3. 这等效于 dt/N 的小 timestep 做 N 次——但不需要用户手动设小 timestep

这就是为什么 NVIDIA 稳定性指南建议:如果不稳定,先减小 timestep 而不是增加 iterations——因为减小 timestep 同时减小了每个 TGS 子步的大小,效果比增加 iterations 更好。

PhysX 5.4+ 的新特性EnableExternalForcesEveryIteration 选项允许在每个 TGS 子步中都重新计算外力(包括重力和关节力矩),而不是只在第一步计算一次。这对高速运动的机器人(如快速行走、跳跃)的稳定性有显著改善。

PhysX 的 residual reporting(Isaac Sim 4.0+):可以查询 solver 每步的残差值,用于判断求解质量:

# Isaac Lab 中查询 solver 残差
# 通过 PhysxSceneAPI 获取 residual 数据
# 高残差 → solver 没有充分收敛 → 需要更多 iterations 或更小 timestep

双框架接触参数对照表

行为 MuJoCo 参数 PhysX 参数 说明
接触硬度 solref[0](时间常数) 隐式(由 iterations 决定) MuJoCo 可精确控制
接触阻尼 solref[1](阻尼比) 隐式 MuJoCo 可精确控制
摩擦系数 geom.friction[0] static_friction / dynamic_friction MuJoCo 不区分动静摩擦
扭转摩擦 geom.friction[1](torsional) 无直接对应 MuJoCo 支持扭转和滚动摩擦
滚动摩擦 geom.friction[2](rolling) 无直接对应 MuJoCo 支持
弹性恢复 geom.solref 间接影响 restitution PhysX 更直接
接触距离 geom.margin + geom.gap contact_offset / rest_offset 不同机制
求解精度 model.opt.iterations num_position_iterations 增加都能提高精度
微滑控制 model.opt.impratio 无直接对应 MuJoCo 特有
摩擦锥 model.opt.cone(pyramidal/elliptic) patch friction 完全不同的模型

反事实推理:如果你不调 impratio 就直接使用 MuJoCo 做 locomotion,可能发现机器人的脚在地面上有微小的滑动——这不是 bug,而是 MuJoCo 软接触模型的固有特性。MuJoCo 文档明确指出:"gradual contact slip cannot be avoided ... Increasing impratio can be particularly effective."

接触调参案例集

以下是三个典型任务的接触参数调优案例,展示"从症状到诊断到解决"的完整流程:

案例 1:四足 locomotion 脚底打滑

某同学在 mjlab 中训练 Go2 flat terrain velocity task,发现策略学会了走路但转向时脚底明显打滑,导致转弯半径比预期大得多。

诊断过程: 1. 检查 reward 曲线 → 正常上升,说明 RL 层没问题 2. 可视化检查 → 确认脚底有明显的切向滑移 3. 回到方程:\(f\) 中摩擦力不足 → 检查 friction 参数 4. 发现 impratio=1.0(默认值)→ 这对 locomotion 来说偏低

解决方案:

# velocity_env_cfg.py 中
sim = SimulationCfg(
    mujoco=MujocoCfg(
        impratio=10.0,  # 从默认 1.0 增大到 10.0
        # 或者调整 cone: "elliptic" 比 "pyramidal" 摩擦更精确
    ),
)

结果:转向时脚底滑移显著减少,转弯半径更接近命令值。

案例 2:灵巧手操作物体掉落

某同学在 Isaac Lab 中训练 Allegro Hand 抓取立方体,策略学会了接近物体但总是抓不住——物体从手指间滑出。

诊断过程: 1. 可视化检查 → 手指和物体之间有接触但摩擦力不够 2. 检查 PhysX 参数 → static_friction=0.5,对精密抓取偏低 3. 检查 contact_offset=0.02 → 对小物体可能太大,导致虚假接触

解决方案:

# 增大手指和物体的摩擦系数
finger_material = MaterialPropertiesCfg(
    static_friction=1.5,   # 从 0.5 增大到 1.5
    dynamic_friction=1.2,
    restitution=0.0,
)

# 减小 contact_offset(避免虚假接触干扰摩擦计算)
physx=PhysxCfg(
    contact_offset=0.005,  # 从 0.02 减小到 0.005
    rest_offset=0.001,
    num_position_iterations=8,  # 从 4 增大到 8,提高接触求解精度
)

结果:抓取成功率从 30% 提升到 75%。

案例 3:人形 locomotion 关节振荡

某同学训练 G1 人形在平地行走,发现膝关节有高频振荡(约 100 Hz),可视化时表现为"腿在抖"。

诊断过程: 1. 回到方程:\(M\)-damping-timestep 组合不稳 2. 检查 actuator 类型 → 使用了 explicit actuator(在 PyTorch 中计算力矩) 3. 检查 kp/kd → kp=100, kd=5(增益较高) 4. 检查 timestep → 0.005(200 Hz physics)

解决方案(三选一):

# 方案 A:切换到 builtin actuator(推荐)
actuator = BuiltinPositionActuatorCfg(kp=100, kd=5)
# integrator 能看到 builtin actuator 的速度依赖 → 更稳定

# 方案 B:减小 timestep
mujoco=MujocoCfg(timestep=0.002)  # 从 0.005 减到 0.002

# 方案 C:降低增益
actuator = ExplicitActuatorCfg(kp=50, kd=2)  # 降低增益

结果:方案 A 最有效——切到 builtin 后振荡消失,且不降低吞吐。

接触参数的"不要调"清单

有些参数在 99% 的情况下不需要调——它们的默认值已经是广泛验证过的:

参数 默认值 为什么不要动
MuJoCo solimp [0.9, 0.95, 0.001, 0.5, 2] 约束阻抗函数的形状,默认值对绝大多数材质足够
MuJoCo condim 3(滑动摩擦 + 扭转摩擦) 改为 1(无摩擦)或 6(滚动摩擦)很少需要
PhysX bounce_threshold_velocity 0.2 m/s 只有模拟弹跳球等特殊场景才需要调
PhysX friction_correlation_distance 0.025 m 只有极小物体才需要减小

PhysX TGS 调优的关键经验

来自 NVIDIA Isaac Sim 稳定性指南和社区实践:

经验 1:先减 timestep,再减 iterations

如果仿真不稳定,新手倾向于增加 num_position_iterations(如从 4 增到 16)。但更有效的做法是减小 timestep(如从 0.005 减到 0.002)。原因:TGS 的每个 position iteration 相当于一个子步——减小 timestep 同时减小了每个子步的大小,效果比增加迭代数更好。

减小 timestep 后如果稳定了,尝试减少 iterations——在更小的 timestep 下,更少的 iterations 通常就够了。这可以恢复部分吞吐量。

经验 2:RL 训练早期最不稳定

RL 训练初期,策略是随机的——随机 action 可能产生极端的关节加速度和速度,这是 PhysX 不稳定的主要来源。两种缓解方法:

# 方法 A:训练开始时限制 action scale
# 前 200 iteration 用 scale=0.25,之后逐步增大到 1.0
# 需要在 ActionManager 中实现 curriculum

# 方法 B:增加 position iterations(只在前 200 iter)
# 前 200 iter: num_position_iterations=8
# 之后: num_position_iterations=4

经验 3:mimic joints 需要 compliance

如果你的机器人有 mimic joints(如 Franka 的 gripper 的两个手指联动),PhysX 中"硬 mimic 约束 + 硬接触 + 硬关节驱动"可能互相竞争导致数值不稳定。添加 mimic compliance(弹性)可以缓解:

# USD 属性
physxJoint:mimicJointNaturalFrequency = 50.0  # Hz
physxJoint:mimicJointDampingRatio = 0.5

经验 4:使用 residual reporting 诊断收敛质量

PhysX 5.4+ 支持 solver residual reporting——你可以查询每步求解后的残差值。高残差意味着 solver 没有充分收敛:

residual 级别 含义 建议
< 1e-4 良好收敛 可以尝试减少 iterations
1e-4 ~ 1e-2 可接受 当前配置合理
> 1e-2 收敛不足 增加 iterations 或减小 timestep

不同物理配置的吞吐量参考(Go2 flat terrain,RTX 4090,4096 envs):

配置 timestep decimation iterations 预计 FPS
mjlab 默认 0.005 4 10 (Newton) ~60,000
mjlab 高精度 0.002 10 20 (Newton) ~25,000
Isaac Lab 默认 0.005 4 4 (TGS) ~55,000
Isaac Lab 高精度 0.002 10 8 (TGS) ~22,000

以上数据为参考量级,实际取决于任务复杂度、sensor 配置和 GPU 型号。关键信息是:高精度配置(小 timestep + 多 iterations)的吞吐量约为默认配置的 40%——这是值得的权衡还是浪费取决于你的任务是否真的需要更高精度。

本质洞察:MuJoCo 的接触参数是"物理层"的——你在控制接触的力学行为(刚度、阻尼)。PhysX 的接触参数是"求解器层"的——你在控制求解器的精度和性能权衡(迭代次数、接触距离)。这个本质差异意味着跨引擎调参不能简单地"翻译参数值"——你需要理解两套参数背后的不同机制。

⚠️ 常见陷阱

  1. 把 MuJoCo 的 friction 直接映射到 PhysX 的 static_friction MuJoCo 的摩擦锥模型(elliptic 或 pyramidal)和 PhysX 的 patch friction 模型有本质差异。相同的数值可能产生不同的行为。
  2. 在 MuJoCo 中把 solref 设得太硬(timeconst < 0.001)。 虽然物理上更"真实",但会导致 solver 需要更多 iterations 才能收敛——训练速度大幅下降。
  3. 忽略 PhysX 的 contact_offset 默认的 0.02m 对于小物体(如骰子、螺丝)来说太大了,会产生虚假接触力。
  4. 在 DR 中过度随机化接触参数。 如果 friction 被随机到非常小的值(<0.1),机器人会"站不住"——这不是有价值的 DR,而是让训练浪费时间在不可能的配置上。

练习

  1. 在 mjlab 中修改 Go2 velocity task 的地面 friction(分别设为 0.3、1.0 和 3.0 各训练 500 iteration),观察步态差异。用 WandB 对比训练曲线和可视化行为——哪个 friction 值下策略学得最快?为什么?
  2. 解释为什么"减小 timestep"比"增加 solver iterations"更能改善 PhysX 的稳定性。用 TGS 的子步机制画一个示意图。
  3. 一个灵巧手操作任务中,物体总是从手指间滑落。在 MuJoCo 中应该调什么参数?在 PhysX 中呢?为什么两个框架的调参方向不同?
  4. (实验题)在 mjlab 中,分别用 impratio=1impratio=10 训练 Go2 在平地上的速度跟踪。可视化转弯行为——impratio 对转弯半径有什么影响?用本章的接触模型知识解释这个现象。
  5. (设计题)你需要为一个搬运任务设置接触参数——机器人(Go2)脚底需要高摩擦(不打滑),但搬运的纸箱需要低摩擦(容易推动)。在 MuJoCo 中如何为不同 geom 设置不同的 friction?(提示:使用 <default class>)在 PhysX 中呢?
  6. (跨章综合题)回顾 Ch02 §2.6 的"训练后 10 问"。其中"步态对称吗"和"足端有严重滑移吗"两个问题对应本章的哪些物理参数?如果答案是"不对称"或"有滑移",你应该按什么顺序调参?

接触参数调优依赖于正确的物理模型。而物理模型的载体是模型文件格式——MuJoCo 用 MJCF,Isaac Lab 用 USD。理解两种格式的结构和互转方式,是双框架工作流的基础。

3.4 MJCF vs USD 模型格式 ⭐⭐

这一节解决什么问题:理解两种模型格式的结构差异,掌握互转工具和常见坑。

动机

mjlab 使用 MJCF 格式,Isaac Lab 使用 USD 格式。如果你要在两个框架中都跑你的机器人,就需要同时维护两种格式的模型文件——或者知道如何从一种格式转换到另一种。

如果不理解模型格式会怎样

你从 MuJoCo Menagerie 下载了 Go2 的 MJCF 文件,想在 Isaac Lab 中使用。你尝试用 MJCF Importer 转换为 USD,转换成功了——但 zero agent 可视化时发现机器人的默认姿态和 mjlab 中不一样。你花了半天排查,最后发现是 MJCF 的 joint.ref(关节零位参考角度)在转换时丢失了。如果你事先知道这个坑,5 分钟就能修复。

MJCF 格式精讲

MJCF(MuJoCo XML Format)是 MuJoCo 的原生模型格式。它的设计哲学是声明式 + 继承式——用 XML 描述世界的层级结构,通过 <default> 机制实现属性继承。

MJCF 的核心层级结构

<mujoco model="robot">
  <!-- 全局仿真参数 -->
  <option timestep="0.005" solver="Newton" iterations="10"
          gravity="0 0 -9.81" impratio="1"/>

  <!-- 默认属性继承(减少重复)-->
  <default>
    <default class="leg">
      <geom type="capsule" friction="1.0 0.005 0.001"
            solref="0.02 1" condim="3"/>
      <joint damping="0.5" armature="0.01"/>
    </default>
    <default class="body">
      <geom type="box" friction="0.4 0.005 0.001"/>
    </default>
  </default>

  <!-- 运动学树 -->
  <worldbody>
    <body name="base" pos="0 0 0.35">
      <freejoint name="root"/>
      <geom class="body" size="0.3 0.1 0.05" mass="5"/>

      <!-- 前左腿 -->
      <body name="FL_thigh" pos="0.2 0.1 0">
        <joint name="FL_hip" type="hinge" axis="0 1 0"
               range="-0.5 1.5" class="leg"/>
        <geom class="leg" fromto="0 0 0 0 0 -0.25"
              size="0.03" mass="0.5"/>
        <body name="FL_calf" pos="0 0 -0.25">
          <joint name="FL_knee" type="hinge" axis="0 1 0"
                 range="-2.5 -0.5" class="leg"/>
          <geom class="leg" fromto="0 0 0 0 0 -0.25"
                size="0.02" mass="0.3"/>
        </body>
      </body>
      <!-- 其他三条腿类似结构 -->
    </body>

    <!-- 地面 -->
    <body name="ground">
      <geom type="plane" size="10 10 0.1" friction="1 0.005 0.001"/>
    </body>
  </worldbody>

  <!-- 执行器 -->
  <actuator>
    <position joint="FL_hip" kp="25" kv="0.5"/>
    <position joint="FL_knee" kp="25" kv="0.5"/>
    <!-- ... 其他关节 -->
  </actuator>

  <!-- 传感器 -->
  <sensor>
    <gyro name="imu_gyro" site="imu_site"/>
    <accelerometer name="imu_acc" site="imu_site"/>
    <touch name="FL_foot_touch" site="FL_foot_site"/>
  </sensor>
</mujoco>

MJCF 的核心元素详解

元素 说明 mjlab 中的对应 常用属性
<option> 全局仿真参数 MujocoCfg timestep, solver, iterations, gravity
<default> 属性继承 class-based defaults class, geom, joint
<body> 运动学树节点 Entity spec pos, quat, name
<joint> 关节 EntityCfg.joint_names_regex type, axis, range, damping
<geom> 碰撞/视觉几何 collision geometry type, size, mass, friction
<actuator> 执行器 ActuatorCfg joint, kp, kv, ctrlrange
<sensor> 传感器 SensorCfg type, site, name
<tendon> MuJoCo 特有 耦合多关节
<equality> 约束 MuJoCo 特有 mimic joint、weld
<site> 参考点 sensor/actuator 的挂载点 pos, quat

class-based defaults 的威力:MJCF 的 <default> 机制允许你定义属性模板,然后在任意元素上引用。这对于对称机器人(如四足的四条腿、人形的左右臂)极为方便——只需定义一次 class="leg",所有引用该 class 的 geom/joint 自动继承其属性。

MjSpec API:用 Python 程序化构建模型

除了 XML,mjlab 还支持通过 Python 的 MjSpec API 程序化构建模型。这在需要动态组合多个模型时(如机器人 + 桌子 + 物体)非常有用:

import mujoco

# 从 XML 加载
spec = mujoco.MjSpec.from_file("go2.xml")

# 或者用 Python 构建
spec = mujoco.MjSpec()
spec.option.timestep = 0.005
spec.option.solver = mujoco.mjtSolver.mjSOL_NEWTON

# 添加 body
base = spec.worldbody.add_body()
base.name = "base"
base.pos = [0, 0, 0.35]

# 添加 free joint
fj = base.add_freejoint()
fj.name = "root"

# 编译为 MjModel
model = spec.compile()

mjlab 的 EntityCfg.spec_fn 就是一个返回 MjSpec 的函数——你可以用 XML 或 Python 来定义。

USD 格式精讲

USD(Universal Scene Description)由 Pixar 开发,是 NVIDIA Omniverse 生态的核心格式。与 MJCF 的"专为物理仿真设计"不同,USD 是一种通用场景描述语言——它最初为电影制作设计,后来被 NVIDIA 扩展用于物理仿真。

Isaac Lab 中机器人的 USD 文件通常由 URDF 或 MJCF 导入生成,而非手动编写。

USD vs MJCF 的核心差异

维度 MJCF USD
设计目的 物理仿真 通用场景描述(电影→仿真)
物理属性 原生支持(friction/solref/solimp) 通过 Schema 扩展(PhysX/Newton)
表达力 tendon/equality/site 等高级特性 不支持 MuJoCo 特有特性
视觉 基础(rgba、texture) 丰富(PBR 材质、RTX 渲染)
组合 <include> / <attach> Layer composition(更强大)
工具链 Python + XML Omniverse + Python
社区模型库 MuJoCo Menagerie(50+ MJCF) Isaac Lab Assets(16+ USD)

MJCF ↔ URDF ↔ USD 互转

方向 工具 说明 信息丢失
MJCF → URDF 无 MuJoCo 原生导出;用第三方工具(如 mjcf-urdf-simple-converter MuJoCo 官方只提供保存 MJCF(mujoco.mj_saveLastXML()),不提供 MJCF→URDF 无损导出 tendon/equality/site/class defaults(第三方只覆盖有限子集,需人工复核)
URDF → MJCF mujoco.MjSpec.from_file("robot.urdf")(或 MjModel.from_xml_path + mj_saveLastXML MuJoCo 3.x MjSpec 需复核 inertial、mesh path、joint limits、transmission/actuator、collision/visual、mimic 等(URDF 不是 MJCF 的语法子集)
URDF → USD Isaac Sim URDF Importer isaacsim.asset.importer.urdf minimal
MJCF → USD Isaac Sim MJCF Importer isaacsim.asset.importer.mjcf tendon/equality
USD → MJCF ❌ 无官方工具 需手动重建
USD → URDF ❌ 无官方工具 需第三方工具

互转时的 5 个常见坑

问题 原因 修复
默认关节角度不同 MJCF USD joint.ref vs USD default pos 手动对齐初始关节角
摩擦参数丢失 MJCF URDF URDF 不支持 solref/solimp 在目标框架中重新设置
接触行为不同 MJCF USD+PhysX 物理引擎不同 这是预期行为,不是 bug
tendon/equality 丢失 MJCF URDF/USD 目标格式不支持 用其他机制实现(如 mimic joint)
视觉网格路径断裂 URDF MJCF mesh 路径引用方式不同 手动调整 meshdir

本质洞察:模型格式转换不是"无损压缩"——每种格式有自己的表达力边界。MJCF 在物理建模方面更强(tendon/equality/site),USD 在视觉渲染方面更强(PBR 材质/RTX)。跨框架工作时,接受一些信息丢失是正常的——关键是知道丢了什么,以及如何在目标框架中补回来。

MjSpec 的高级用法:多 Entity 组合

mjlab 的 Scene 在 MjSpec 阶段通过 attach 组合多个 Entity。这个操作的本质是"把一个 MjSpec 的 worldbody 子树挂到另一个 MjSpec 的某个 body 上":

import mujoco

# 加载机器人和桌子
robot_spec = mujoco.MjSpec.from_file("go2.xml")
table_spec = mujoco.MjSpec.from_file("table.xml")

# 把桌子挂到 worldbody
robot_spec.worldbody.attach(table_spec, prefix="table_")
# 现在 robot_spec 包含了机器人和桌子

# 还可以挂载物体到桌面上
cube_spec = mujoco.MjSpec.from_file("cube.xml")
# 找到桌面 body
table_top = robot_spec.worldbody.find_body("table_top")
table_top.attach(cube_spec, prefix="cube_")

# 编译
model = robot_spec.compile()

mjlab 的 EntityCfg.spec_fn 返回的就是一个 MjSpec。在 Scene._add_entities() 中,所有 Entity 的 MjSpec 通过 attach 组合成一个统一的场景。

与 Isaac Lab 的对比:Isaac Lab 的场景组合是通过 USD 的 layer composition 实现的——概念上相似(多个模型组合成一个场景),但 API 完全不同。

⚠️ 常见陷阱

  1. 假设 URDF → USD 转换是无损的。 URDF 是"最小公分母"格式——不支持 tendon/equality/class defaults/site。
  2. 忽略 collision geometry 和 visual geometry 的区别。 MJCF 默认碰撞和视觉使用同一 geom,USD 默认分离。转换后可能不一致。
  3. 认为"模型相同"就代表"行为相同"。 即使几何和质量完全一致,MuJoCo 和 PhysX 的接触行为仍然不同。
  4. 手动编辑 USD 文件。 USD 是二进制格式(.usd)或文本格式(.usda),不像 MJCF 的 XML 那样容易手动编辑。通过 Isaac Lab 的 Python API 修改。
  5. 在 MjSpec attach 时忘记加 prefix。 如果两个 Entity 有同名的 body/joint(如都叫 "base"),attach 时不加 prefix 会导致命名冲突。

练习

  1. 下载 MuJoCo Menagerie 中的 Go2 MJCF 文件。列出它使用了多少个 body、joint、geom 和 actuator。找出所有使用了 class 属性的元素——class 机制为代码减少了多少重复?
  2. 尝试用第三方工具(如 mjcf-urdf-simple-converter)将 Go2 的 MJCF 转换为 URDF(MuJoCo 官方不提供 MJCF→URDF 导出)。比较文件大小和内容差异。列出丢失的信息(至少 3 项)。
  3. 使用 MjSpec API 从 Python 构建一个最简单的机器人:一个 box base + 一个 hinge joint + 一个 capsule leg。验证 spec.compile() 成功。
  4. (跨章综合题)回顾 Ch01 §1.4 的双框架对比表。在模型格式层面,mjlab 的 MJCF 和 Isaac Lab 的 USD 各有什么优势?如果你的机器人有闭环机构(如平行连杆腿),哪种格式能更自然地描述它?为什么?
  5. (设计题)你有一个 URDF 格式的机械臂模型,需要同时在 mjlab 和 Isaac Lab 中使用。画出你的工作流程:URDF → ?(转换)→ 两个框架各自使用。标注每一步可能遇到的问题。

模型格式定义了"世界长什么样"。接下来我们深入 mjlab 的核心——MuJoCo Warp 如何把这个世界搬到 GPU 上并行运行。

3.5 MuJoCo Warp 的 GPU 数据流 ⭐⭐⭐

这一节解决什么问题:理解从 MJCF 到 GPU batched worlds 的完整数据流——这是调试 mjlab 性能和稳定性问题的基础。

动机

Ch02 的第一次训练中,你只需要 uv run train 一条命令就能启动训练。但当你需要调试"为什么训练变慢了"、"为什么 DR 不生效"、"为什么出了 NaN"时,你需要知道数据在 GPU 上是怎么组织的。

如果不理解数据流会怎样

反面案例 A:把 MjModel 当成运行时可随意改变的对象。 新手想法是"MuJoCo model 里有 body_massgeom_friction,那我每一步直接改这些字段就能做 DR"。在 MuJoCo Warp + CUDA Graph 下这很危险——GPU 字段是 wp.array,CUDA Graph 记录的是 kernel 序列和数组地址。如果你替换了数组对象,旧图读的是旧地址。mjlab 在 expand_model_fields() 里明确处理了这个问题。

反面案例 B:把 nconmax 当成全局 contact 数。 SimulationCfg.nconmax 是 per-world 的 contact allocation,不是整个 batch 的全局上限。如果误解为全局数,你会把 nconmax 设得过小(导致接触截断)或在显存估算时出错。

反面案例 C:把 CPU solver 支持表照搬到 MuJoCo Warp。 已在 §3.1 详述——PGS 在 GPU 上不完整支持。

完整数据流

MJCF/XML 或 Python MjSpec
  ┃
  ▼ spec.compile()
mujoco.MjModel (CPU)           ← 编译层:常量结构,不可改树
  ┃                               nq, nv, nbody, njnt 等固定
  ▼ Simulation._init_with_model()
mjwarp.put_model(mj_model)      ← GPU 上传模型常量
mjwarp.put_data(nworld=N,       ← GPU 创建 N 个并行世界
    nconmax=..., njmax=...)        每个 world 有独立的 qpos/qvel/ctrl
  ┃
  ▼
mjwarp.Model + mjwarp.Data      ← GPU batched worlds
  ┃
  ▼ WarpBridge
torch.Tensor 视图               ← 零拷贝桥接(wp.to_torch)
  ┃
  ▼ Simulation.create_graph()
CUDA Graph 捕获                 ← 记录 kernel 序列和 GPU 地址
  ┃
  ▼ mjwarp.step() replay
训练循环                         ← 每步 replay 预录制的 graph

三个关键分界点

分界点 之前可以做什么 之后不能做什么 类比
MjSpec → MjModel 添加/删除 body、修改树结构 不能改树结构(body 数、joint 数固定) 数据库 ALTER TABLE → 锁定 schema
MjModel → mjwarp.Model 修改 model.opt、调整参数 修改必须通过 WarpBridge 原地写入 本地文件 → 上传到云存储
CUDA Graph capture GPU 数组可以分配/替换 不能替换数组对象(地址必须稳定) 录音 → 播放录音

nqnv 的关系

MuJoCo 使用广义坐标,nq(位置维度)和 nv(速度维度)不相等。这是因为四元数有 4 个分量但角速度只有 3 个分量:

关节类型 nq 贡献 nv 贡献 说明
free(自由基座) 7(3 平移 + 4 四元数) 6(3 线速度 + 3 角速度) nq - nv = 1
hinge(旋转关节) 1 1 相等
slide(平移关节) 1 1 相等
ball(球关节) 4(四元数) 3(角速度) nq - nv = 1

计算示例:Go2(四足)= free joint + 12 hinge joints → nq = 7 + 12 = 19nv = 6 + 12 = 18。G1(人形)= free joint + 29 hinge joints → nq = 7 + 29 = 36nv = 6 + 29 = 35

这个差异影响 mjlab 的 Entity 初始化——initial_joint_pos 的维度是 nq(包含基座的四元数),而 initial_joint_vel 的维度是 nv

WarpBridge:零拷贝桥接的细节

mjlab 的 obs/reward/action 都是 PyTorch tensor。但底层物理状态存储在 MuJoCo Warp 的 wp.array 中。WarpBridgesrc/mjlab/sim/sim_data.py)通过 wp.to_torch() 暴露 GPU 缓冲区为 PyTorch tensor 视图——不拷贝数据

# WarpBridge 内部简化示意
class WarpBridge:
    def __init__(self, warp_data):
        # wp.to_torch 返回一个 PyTorch tensor,
        # 与 warp_data 共享同一块 GPU 内存
        self.qpos = wp.to_torch(warp_data.qpos)  # shape: (nworld, nq)
        self.qvel = wp.to_torch(warp_data.qvel)  # shape: (nworld, nv)
        self.ctrl = wp.to_torch(warp_data.ctrl)   # shape: (nworld, nu)

    def __setattr__(self, name, value):
        # 禁止替换 tensor 对象!只允许原地写入
        if name in ('qpos', 'qvel', 'ctrl') and hasattr(self, name):
            raise RuntimeError(
                f"Cannot replace {name}. Use tensor[:] = value for in-place write."
            )
        super().__setattr__(name, value)

关键约束

操作 是否安全 原因
bridge.qpos[:] = new_values ✅ 安全 原地写入,地址不变
bridge.qpos = new_tensor ❌ 危险 Python 对象替换,旧地址上的 CUDA Graph 失效
bridge.qpos[env_ids] = values ✅ 安全 索引写入,地址不变
torch.where(mask, new, bridge.qpos) ⚠️ 视情况 如果结果写回 bridge.qpos[:] 则安全

nconmaxnjmax:接触容量预分配

MuJoCo Warp 的 GPU 并行要求所有 world 有相同的内存布局——包括接触缓冲区。nconmax(per-world 最大接触数)和 njmax(per-world 最大约束行数)在创建 batched worlds 时就固定。

显存估算(对后续 num_envs 选择有直接影响):

参数 默认值 每条记录大小 4096 envs 下的显存
nconmax 35 ~500 bytes 35 × 500 × 4096 ≈ 69 MB
njmax 1500 ~200 bytes 1500 × 200 × 4096 ≈ 1.2 GB

njmax 是显存消耗的大户。如果你的任务接触点很少(如机械臂抓取),可以大幅降低 njmax(如从 1500 降到 200),释放显存给更多 num_envs。

nconmax 过小的症状:训练日志中出现 contact buffer overflow warning,可视化中物体穿透,reward 曲线异常。

nconmax 的设置经验

机器人类型 推荐 nconmax 推荐 njmax 说明
四足(flat) 20-30 500-1000 4 只脚 × 几个接触点
四足(rough) 30-50 1000-2000 地形接触更多
人形 40-60 1500-3000 更多身体部分可能接触
灵巧手 80-120 3000-5000 多指多接触
机械臂+物体 15-25 300-800 接触较少

GPU 显存的实测方法

# 方法 1:训练时监控(最常用)
watch -n 1 nvidia-smi

# 方法 2:训练前精确查询
python -c "
import torch
torch.cuda.empty_cache()
# 创建环境后
print(torch.cuda.memory_summary())
# 关注 'Allocated' 和 'Reserved' 行
"

# 方法 3:从小到大找最大 num_envs
for N in 256 512 1024 2048 4096 8192; do
    echo "Testing num_envs=$N"
    timeout 30 uv run train <TASK> --env.scene.num-envs $N 2>&1 | head -5
    if [ $? -ne 0 ]; then
        echo "OOM at num_envs=$N"
        break
    fi
done

显存优化的优先级(当 OOM 时):

优先级 操作 预期效果 代价
1 减少 num_envs 显存线性下降 吞吐下降
2 减少 njmax 显存显著下降(约束是大户) 约束截断风险
3 减少 nconmax 显存中等下降 接触截断风险
4 移除不必要的 sensor 释放 sensor buffer 丢失 obs 信息
5 减小网络 hidden_dims 释放网络参数显存 策略容量下降
6 使用 torch.cuda.empty_cache() 释放 PyTorch 缓存 可能引入碎片化

经验法则:在 RTX 4090(24 GB)上,4096 envs 的四足 locomotion(无相机)通常占用约 10-12 GB 显存,留有充足余量。如果 OOM,第一反应应该检查是否有遗留的其他 GPU 进程——nvidia-smi 会显示所有 GPU 进程。

CUDA Graph Capture 深入

CUDA Graph 是 mjlab 性能的关键——它把一系列 GPU kernel 的启动序列"录制"下来,之后每步只需一次 CPU 调用就能 replay 整个序列。

capture 时机:mjlab 在 Simulation.__init__ 完成后立即 capture。capture 记录了: 1. 所有 kernel 的执行顺序 2. 每个 kernel 读写的 GPU 地址 3. kernel 之间的依赖关系

graph 失效条件

条件 是否导致失效 处理方式
expand_model_fields() ✅ 失效 自动重新 capture
替换 GPU 数组对象 ✅ 失效 禁止——用原地写入
改变 num_envs ✅ 失效 需要重新创建 Simulation
修改 qpos/qvel 数值 ❌ 不失效 正常操作(原地写入)
添加新 sensor ✅ 失效 需要重新 compile + capture

expand_model_fields 的工作流(Domain Randomization 触发时):

1. DR 事件触发(如 randomize_mass)
2. mjlab 检查该 model field 是否已展开
3. 如果未展开:
   a. 将共享的 scalar/array 展开为 per-world array
   b. 清理 WarpBridge cache
   c. 重建 sensor context
   d. 调用 create_graph() 重新 capture
4. 后续的 DR 修改直接写入 per-world array(无需重新 capture)

第一次 DR 触发后的 step 会比平时慢(因为 graph 重建)——这是正常行为。后续不再需要重建。

mjlab 中的对应源码

阶段 mjlab 入口 关键操作
描述 EntityCfg.spec_fn 返回 MjSpec(从 XML 或 Python 构造)
组合 Scene._add_entities() self._spec.attach(ent.spec, prefix=..., frame=...)
编译 Scene.compile() spec.compile()MjModel
配置 MujocoCfg.apply(model) 写入 model.opt(solver/timestep/gravity 等)
上传 Simulation._init_with_model() mjwarp.put_model(mj_model)
数据创建 Simulation._init_with_model() mjwarp.put_data(nworld=num_envs, ...)
桥接 WarpBridge.__init__() wp.to_torch() 创建零拷贝视图
图捕获 Simulation.create_graph() CUDA Graph capture + warmup

Isaac Lab 3.0 的对应(Newton 后端)

Isaac Lab 3.0 通过 Newton 使用 MuJoCo Warp 时,数据流类似但入口不同:

USD/URDF → Isaac Lab Asset Importer → ArticulationCfg
  → Newton MuJoCo Warp solver
  → wp.array (.data.* 属性)
  → wp.to_torch() 转换

关键差异:Isaac Lab 3.0 的 .data.* 属性默认返回 wp.array 而非 torch.Tensor——需要手动 wp.to_torch() 包装。Isaac Lab 提供了自动化迁移工具 scripts/tools/wrap_warp_to_torch.py

⚠️ 常见陷阱

  1. 在 CUDA Graph capture 之后替换 GPU 数组对象。 Graph 记录的是地址,不是变量名。替换对象 = 改变地址 = graph 读错数据。
  2. nconmax 理解为全局 contact 数。 它是 per-world 的。显存按 num_envs × nconmax × ~500 bytes 增长。
  3. 认为 expand_model_fields 是免费的。 它需要重建 CUDA Graph,有一次性开销。
  4. 忽略 nq ≠ nv free joint 的四元数导致 nq > nv,初始化时维度必须匹配。
  5. 在 Isaac Lab 3.0 中忘记 wp.to_torch() .data.* 属性现在返回 wp.array,直接传给 PyTorch 会报错。

练习

  1. 画出从 EntityCfg.spec_fnSimulation.create_graph() 的完整数据流图。标注每个分界点。
  2. (计算题)nconmax=35njmax=1500num_envs=4096。估算 contact buffer 和 constraint buffer 的 GPU 显存占用。如果把 nconmax 翻倍到 70,显存增加多少?
  3. 为什么 MuJoCo Warp 要求所有 world 有相同的 nconmax?从 GPU SIMT 并行的角度解释。
  4. 一个机器人有 free joint(基座)+ 23 个 revolute joint。计算 nqnv。如果场景中添加第二个相同机器人(同一 world),nqnv 变成多少?
  5. (源码阅读题)打开 velocity_env_cfg.py,记录 timestep、iterations、nconmax、njmax 的值。计算 policy frequency(= 1 / (timestep × decimation))。

源码阅读路线

如果你想深入理解 mjlab 的物理层实现,按以下顺序阅读源码:

步骤 文件 关注点 预计时间
1 velocity_env_cfg.py SimulationCfg 和 MujocoCfg 的默认配置 15 分钟
2 src/mjlab/sim/sim.py MujocoCfg → model.opt 的映射、_SOLVER_MAP 30 分钟
3 src/mjlab/sim/sim_data.py WarpBridge 的 __setattr__wp.to_torch() 调用 30 分钟
4 src/mjlab/scene/scene.py _add_entities() 的 MjSpec attach 流程 20 分钟
5 src/mjlab/entity/entity.py EntityCfg → spec_fn → 运行时数据 20 分钟

这 5 个文件覆盖了本章 §3.5 讲述的整条数据流。如果你只有 30 分钟,只读步骤 1 和 2——它们回答了"默认配置是什么"和"配置如何变成物理引擎参数"。

对应的 Isaac Lab 源码阅读路线:

步骤 文件 关注点
1 source/isaaclab/sim/simulation_cfg.py SimulationCfg 和 PhysxCfg
2 source/isaaclab/sim/simulation_context.py PhysX scene 创建和参数设置
3 source/isaaclab/assets/articulation/articulation.py ArticulationCfg → USD 加载

本章使用的主要数据来源

数据 来源 可信度
MuJoCo 动力学方程 Todorov, IROS 2012 原始论文 ✅
MuJoCo Warp CPU/GPU 差异表 mjlab 论文 + MuJoCo Warp docs 官方文档 ✅
PhysX TGS 机制 PhysX 5.4 文档 官方文档 ✅
Newton 求解器列表 NVIDIA Technical Blog 2026-03 官方 blog ✅
Newton benchmark 数据 NVIDIA Technical Blog 2026-03 官方 blog ✅(峰值数据)
solref/solimp 推荐值 ROBOLAWEB Cheat Sheet 社区验证 ⚠️
ASAP 跨引擎差异 He et al., RSS 2025 顶会论文 ✅
Kamino 闭环机构 Disney Research, arXiv:2603.16536 预印本 ⚠️
接触调参案例 教学经验 + 社区 Issues 经验值 ⚠️

3.6 跨引擎 sim2sim 验证 ⭐⭐⭐

这一节解决什么问题:建立从"单引擎训练"到"跨引擎验证"的工程流程——这是 sim-to-real 前的关键一步。

动机

你在 mjlab 中训练了一个策略,效果很好。但你怎么知道这个策略的行为不是"MuJoCo 特有的"?如果策略依赖了 MuJoCo 的某个数值特性(如软接触的微小穿透),在真机上(接触行为不同)可能会失败。

sim2sim 验证的核心思想:在另一个引擎上运行你的策略,检查行为是否大致一致。如果两个引擎上的行为相似,说明策略学到了"真正的运动技能"而非"仿真器特定的 exploit"。

ASAP 的跨引擎发现

ASAP(Aligning Simulation and Real-World Physics,He et al.,arXiv:2502.01143,RSS 2025,CMU LeCAR-Lab)系统量化了跨引擎差异。其核心发现:

  1. 同一个策略在 Isaac Gym、Isaac Sim 和 Genesis 上训练后,真机表现差异很大
  2. 差异的主要来源不是 reward 设计或 DR 策略,而是物理引擎的接触模型差异
  3. ASAP 提出的 delta-action model 通过真机数据弥补 sim-sim gap

ASAP 的训练管线分为 5 个阶段:

阶段 操作 引擎 输出
A. Pretrain 在仿真器中训练 motion tracker Isaac Gym/Sim/Genesis 基础策略
B. Real Rollout 部署到真机,MoCap 采集数据 真机 真实轨迹 (s, a, s')
C. Train δ 最小化 sim-real 状态差异 仿真器 delta action model δ(s,a)
D. Finetune 冻结 δ,用 a' = a + δ(s,a) 继续 RL 仿真器 改进策略
E. Deploy 直接部署(不需要 δ) 真机 最终策略

ASAP 的关键洞察是:delta action model 本质上在学习"仿真器哪里不准"。它不需要你知道具体哪个物理参数不对——只需要真机数据就能自动弥补差异。这比手动调接触参数高效得多,但代价是需要真机 MoCap setup。

最小 sim2sim 流程(不需要真机)

在没有真机的情况下,你仍然可以做有价值的 sim2sim 验证:

# Step 1: 在 mjlab(MuJoCo Warp)中训练
uv run train Mjlab-Velocity-Flat-Unitree-Go2 \
    --env.scene.num-envs 4096

# Step 2: 导出 ONNX
uv run play Mjlab-Velocity-Flat-Unitree-Go2 \
    --wandb-run-path your-org/project/<run-id> \
    --export-onnx

# Step 3: 在 CPU MuJoCo 中加载 ONNX(sim2sim 第一层)
# 这验证 GPU → CPU 的数值一致性
python sim2sim_cpu.py \
    --model go2.xml \
    --policy logs/policy.onnx \
    --num-episodes 10

# Step 4(可选): 在 Isaac Lab(PhysX)中加载 ONNX(sim2sim 第二层)
# 这验证跨引擎的行为一致性
python scripts/reinforcement_learning/rsl_rl/play.py \
    --task Isaac-Velocity-Flat-Unitree-Go2-v0 \
    --policy logs/policy.onnx \
    --num_envs 4

sim2sim 的三层验证

层级 对比 验证什么 预期差异
第一层 MuJoCo Warp → CPU MuJoCo GPU/CPU 数值一致性 极小(同一引擎)
第二层 MuJoCo Warp → PhysX 跨引擎行为鲁棒性 中等(不同接触模型)
第三层 仿真 → 真机 sim-to-real 可行性 较大(域差距)

如果第一层就有显著差异,说明策略 overfit 了 GPU 的数值特性——需要加更多 DR 或减小 action scale 后重新训练。如果第一层一致但第二层差异大,说明策略对接触模型敏感——可能需要 ASAP 风格的 delta-action model。

CPU MuJoCo sim2sim 的最小代码

以下是在 CPU MuJoCo 中加载 ONNX 策略并运行的简化代码:

import mujoco
import numpy as np
import onnxruntime as ort

# 加载 MuJoCo 模型和 ONNX 策略
model = mujoco.MjModel.from_xml_path("go2.xml")
data = mujoco.MjData(model)
session = ort.InferenceSession("policy.onnx")

# 模拟一个 episode
obs_list = []
for step in range(1000):
    # 构建 obs(需要和训练时完全一致!)
    obs = np.concatenate([
        data.qpos[7:],                # joint positions(跳过 free joint 的 7 维)
        data.qvel[6:],                # joint velocities(跳过 free joint 的 6 维)
        data.qvel[:3],                # base linear velocity
        data.qvel[3:6],               # base angular velocity
        # ... 其他 obs 项
    ]).astype(np.float32).reshape(1, -1)

    # 推理
    action = session.run(None, {"obs": obs})[0][0]

    # 执行
    data.ctrl[:] = action * action_scale + default_joint_pos
    for _ in range(decimation):
        mujoco.mj_step(model, data)

    # 检查 termination
    if data.qpos[2] < 0.15:  # 基座高度过低
        print(f"Terminated at step {step}")
        break

注意:obs 的构建必须和训练时完全一致——同样的特征、同样的顺序、同样的 normalization。这是 sim2sim 最容易出错的地方。建议在训练代码中导出一个 obs_config.json,sim2sim 时严格按照它构建 obs。

MuJoCo Warp vs CPU MuJoCo 的已知差异来源

差异来源 影响程度 是否需要处理 缓解方法
float32 vs float64 微小(<0.1%) 通常不需要 大多数情况可忽略
并行归约顺序 微小 通常不需要 增加 solver iterations
Warmstart 策略 微小 通常不需要 用更多 iterations 补偿
solref/solimp 在 GPU 上 可能显著 可能需要 在 GPU 上调参而非从 CPU 复制
RNG 序列不同 不影响确定性策略 不需要
sensor 计算路径 微小 通常不需要 对比 CPU/GPU sensor 输出

经验法则:如果 GPU 训练后在 CPU 上 sim2sim 的行为差异 < 10%(以 tracking error 或 episode length 衡量),通常不需要额外处理。如果差异 > 20%,需要检查是否策略依赖了仿真器特定行为——最常见的原因是 obs 构建不一致或 action scale 不匹配。

MuJoCo Warp vs CPU MuJoCo 的数值差异

即使是同一个 MuJoCo 引擎,GPU 和 CPU 版本的数值行为也不完全相同。GitHub issue google-deepmind/mujoco#2548 详细分析了这个问题:

差异来源 影响程度 缓解方法
float32 vs float64 微小 大多数情况可忽略
并行归约顺序不同 微小 增加 solver iterations
Warmstart 策略不同 微小 用更多 iterations 补偿
solref/solimp 在 GPU 上的行为 可能显著 在 GPU 上调参而非从 CPU 复制

经验法则:如果 GPU 训练后在 CPU 上 sim2sim 的行为差异 < 10%(以 tracking error 衡量),通常不需要额外处理。如果差异 > 20%,需要检查是否策略依赖了仿真器特定行为。

⚠️ 常见陷阱

  1. 跳过 sim2sim 直接上真机。 如果策略在 sim2sim 都失败了(同一引擎的 CPU 版本),在真机上几乎不可能成功。
  2. 期望跨引擎行为完全一致。 微小差异是正常的。关注定性行为一致——步态类型、速度范围、转向方式应该相似。
  3. 用 reward 数值跨引擎比较。 Reward 数值取决于配置(权重、sigma),不同引擎不可比。比较行为质量。
  4. 认为 ASAP 是唯一的 sim-to-real 方案。 ASAP 的 delta-action model 需要真机 MoCap 数据。对于大多数研究者,充分的 DR + sim2sim 验证是更实用的路径。

练习

  1. 在 mjlab 中训练 Go2 velocity task 后,导出 ONNX 并在 CPU MuJoCo 中运行。比较 GPU 训练时和 CPU 推理时的行为差异(用 tracking error 量化)。
  2. (思考题)ASAP 的 delta-action model 是在训练后"修补" sim-real gap。有没有可能在训练前就消除这个 gap?什么条件下这是可行的?
  3. (跨章综合题)结合 Ch02 的 sim2sim 预验证和本章的跨引擎验证,设计一个完整的"训练→验证→部署"流程。标注每一步使用什么框架/引擎,以及每一步可能遇到的问题。

3.7 稳定性与吞吐的排查优先级 ⭐⭐

这一节解决什么问题:当训练出现爆炸、NaN、变慢等物理层问题时,按什么顺序排查——避免在错误方向上浪费时间。

稳定性排查优先级

遇到 NaN、爆炸、穿透时,每次只改一个变量,按以下优先级逐步排查:

优先级 检查项 典型安全值 调整方向 诊断命令
1 action scale 0.25-0.5 降低到 0.25 看是否消除 NaN 检查 action_manager.action_scale
2 actuator 类型 builtin (IdealPD) 从 explicit 切到 builtin 检查 EntityCfg.actuator
3 actuator gains (kp/kd) kp=25, kd=0.5 降低 kp/kd 检查 MJCF <actuator>
4 timestep 0.002-0.005 减小 timestep sim_cfg.timestep
5 solver iterations 10-20 增加 iterations sim_cfg.iterations
6 接触参数 默认 solref/solimp 更软 → 更稳 检查 <geom> friction/solref
7 nconmax / njmax 35 / 1500 增大(确认非截断) 检查训练 warning
8 reward terms 去掉鼓励高速接触的 reward 打印 reward 各分项
9 DR 范围 缩小 DR 范围 检查 event terms
10 obs noise/delay 减小 noise 检查 obs terms

排查的黄金法则:如果 NaN 在第一个 iteration 就出现 → 问题在物理层(优先级 1-6)。如果 NaN 在训练中途出现 → 可能是 RL 层(优先级 7-10)。如果 NaN 只在 DR 触发后出现 → 问题在 DR 配置(优先级 9)。

吞吐排查优先级

遇到 steps/s 太低时:

优先级 检查项 诊断方法 预期影响
1 viewer 是否开启 --headless 对比 2-10× 提升
2 视频录制是否开启 关闭 video recording 对比 1.5-3× 提升
3 WandB 是否阻塞 WANDB_MODE=disabled 对比 通常 <5%
4 sensor 数量/分辨率 禁用 sensor 对比 视 sensor 类型
5 num_envs 是否过大 从 256 开始逐步加倍 找到吞吐峰值
6 nconmax / njmax 降低到实际需要值 释放显存
7 solver iterations 减少到可接受精度 线性提升
8 heightfield 分辨率 降低地形精度 减少接触计算
9 CUDA Graph 是否启用 检查 warning 1.5-3× 提升
10 Python manager 热点 torch.profiler 定位 Python 瓶颈

timestep × decimation 的联合调参

timestepdecimation 共同决定 policy frequency 和物理精度:

timestep decimation physics freq policy freq 特点
0.005 4 200 Hz 50 Hz mjlab 默认,平衡精度和速度
0.002 10 500 Hz 50 Hz 更精确,速度慢 ~2.5×
0.01 2 100 Hz 50 Hz 更快但精度低,可能不稳定
0.005 2 200 Hz 100 Hz policy 决策更频繁

反事实推理:如果你把 timestep 改小(0.005→0.002)但忘记调整 decimation,policy frequency 也会变高(125 Hz vs 50 Hz)——这可能改善控制精度但也改变了 PPO 的 batch 结构。调 timestep 时始终同步调整 decimation 以保持 policy frequency 不变。

性能不是只看 steps/s

steps/s 是有用指标但不是唯一指标。一个健康的训练应该同时满足:

条件 怎么检查 不满足时怎么办
吞吐足够高 fps 接近 GPU 理论峰值 用吞吐排查表逐项检查
物理足够稳定 不频繁 NaN 或 reset 用稳定性排查表逐项检查
行为足够合理 可视化检查策略动作 如果利用 solver artifact → 加更多 DR

思维陷阱:性能不好就先换算法。 训练 wall-clock 慢时,新手的第一反应往往是"PPO 太慢,换 SAC"。更可能的原因是 env.step() 本身慢——先测 env 吞吐量,再考虑算法。正确排查顺序:先确认 viewer/video/sensor 关闭 → 测 env 吞吐 → 调 num_envs → 调 solver → 最后才考虑算法。

⚠️ 常见陷阱

  1. 同时改多个参数。 如果同时改 timestep 和 solver iterations,你不知道改善来自哪个。每次只改一个。
  2. 把物理不稳定当成训练不稳定。 如果 NaN 在第一个 iteration 就出现,问题是物理层,不是 RL 层。
  3. 忽略 contact buffer overflow warning。 这个 warning 意味着有些接触被丢弃了——物理行为不再正确。必须增大 nconmax
  4. 在 MuJoCo 中追求极高精度。 训练目的不是精确仿真,而是训出鲁棒策略。过度精确(极小 timestep + 极多 iterations)会大幅降低吞吐,而 DR 可以弥补适度的物理不精确。

练习

  1. 在 mjlab 中分别用 timestep=0.005(decimation=4)和 timestep=0.002(decimation=10)训练 Go2——两者 policy frequency 都是 50 Hz。比较:(a) steps/s 差异,(b) reward 收敛速度,(c) 可视化行为是否更平滑。
  2. nconmax 从默认值 35 减到 15,训练 500 iteration。观察是否出现 warning 或行为异常(如脚底穿透地面)。
  3. (计算题)timestep=0.005decimation=4。计算 physics frequency(200 Hz)和 policy frequency(50 Hz)。如果想把 physics frequency 提高到 500 Hz 但保持 policy 在 50 Hz,应该设 timestep=0.002, decimation=10。验证:500 Hz × 0.002s = 1 步,50 Hz 需要 10 个物理步。
  4. 在训练中分别用 --headless 和不用 --headless 测 FPS。差异有多大?这帮你理解 viewer 对吞吐的影响。
  5. (跨章综合题)结合 Ch02 §2.6 的 GPU 训练时间对照表和本章的吞吐排查优先级。如果你在 RTX 4090 上训练 Go2 flat terrain 只得到 20,000 steps/s(预期 ~60,000),按什么顺序排查?列出前 5 个检查步骤。

本章建立的心智模型

读完本章后,你脑中应该有以下心智模型:

"物理层的问题"
      │
      ├── 引擎选型(§3.1-3.2)
      │     ├── 接触密集 → MuJoCo Warp(凸优化,总能收敛)
      │     ├── 大规模场景 → PhysX(TGS 迭代,快)
      │     └── 闭环/软体 → Newton(Kamino/VBD/XPBD)
      │
      ├── 接触调参(§3.3)
      │     ├── MuJoCo: solref/solimp/impratio
      │     ├── PhysX: contact_offset/friction/iterations
      │     └── 症状 → 参数 映射表
      │
      ├── 模型格式(§3.4)
      │     ├── MJCF(物理表达力强:tendon/equality/site)
      │     └── USD(视觉表达力强:PBR/RTX)
      │
      ├── GPU 数据流(§3.5)
      │     └── MjSpec → MjModel → mjwarp → WarpBridge → CUDA Graph
      │           ↑描述层    ↑编译层    ↑GPU 层   ↑桥接层    ↑图层
      │
      └── 验证与排查(§3.6-3.7)
            ├── sim2sim 三层验证(GPU→CPU→跨引擎)
            └── 排查优先级(先物理层,后 RL 层)

如果你能在不看笔记的情况下画出这个模型,说明本章的核心知识已经内化了。

自检:本章你应该能回答的 10 个问题

  1. MuJoCo 的凸优化接触和 PhysX 的 TGS 迭代有什么本质区别?
  2. 什么任务该选 MuJoCo Warp?什么任务该选 PhysX?
  3. solref 的两个参数分别控制什么?脚底穿透时应该怎么调?
  4. PhysX 的 num_position_iterations 在 TGS 中等效于什么?
  5. MJCF 的 <default class="..."> 机制有什么好处?
  6. 从 MJCF 转换到 URDF 时,哪些信息会丢失?
  7. MuJoCo Warp 的 nconmax 是 per-world 还是全局的?为什么?
  8. CUDA Graph 在什么情况下会失效?expand_model_fields 为什么需要重建 graph?
  9. sim2sim 验证的三层是什么?每层验证什么?
  10. 训练第一个 iteration 就出 NaN,应该从哪里开始排查?

MjSpec → MjModel → MjData 的三阶段生命周期 ⭐⭐

这一节解决什么问题:深入理解三个核心数据结构的生命周期边界——这决定了哪些修改在哪个阶段合法。

描述阶段:MjSpec

MjSpec 是 MuJoCo 3.x 引入的可编辑蓝图。在这个阶段,你可以:

操作 合法 示例
添加/删除 body spec.worldbody.add_body()
修改 geom 参数 geom.friction = [1.0, 0.005, 0.001]
attach 另一个 Entity spec.attach(other_spec, prefix="arm_")
修改关节范围 joint.range = [-1.5, 1.5]
添加 sensor spec.add_sensor(...)

mjlab 的 Scene._add_entities() 就在这个阶段工作——它把多个 Entity 的 MjSpec 组合成一个统一的场景描述。

编译阶段:MjModel

spec.compile() 把描述转化为高效的数值数据结构。编译后:

操作 合法 说明
修改 body_mass ✅(数值修改) 但需要调用 mj_setConst() 重新计算派生量
修改 geom_friction ✅(数值修改) 直接生效
修改 model.opt timestep、solver、gravity 等
添加新 body 树结构已固定
删除 joint 数组尺寸已固定
修改关节类型 影响 nq/nv,不可改

MjModel 包含: - 常量信息:运动学树结构、惯性参数、几何形状、地址映射 - 可调数值:body_mass、geom_friction、dof_damping 等(不改结构只改值) - 配置选项model.opt.*(solver、timestep、gravity)

运行阶段:MjData

MjData状态容器——每一步仿真都在更新它的内容:

字段 含义 更新时机
qpos 广义位置 每步积分后
qvel 广义速度 每步积分后
ctrl 控制输入 用户写入,每步前
qacc 广义加速度 每步计算
qfrc_actuator 执行器力 每步计算
efc_force 约束力 每步 solver 求解
contact 接触列表 每步碰撞检测
sensordata 传感器读数 每步计算

关键概念:一个 MjModel 可以配多个 MjData。这就是 MuJoCo Warp batched worlds 的基础——同一个 MjModel 的 N 个独立 MjData 实例在 GPU 上并行运行。

三阶段的不可逆性

MjSpec(可编辑蓝图)
   │  spec.compile() ← 不可逆:编译后不能回到 MjSpec
   ▼
MjModel(常量结构)
   │  mjwarp.put_model() ← 不可逆:上传后在 GPU 上
   ▼
mjwarp.Model + mjwarp.Data(GPU batched worlds)
   │  create_graph() ← 可重建:expand_model_fields 后重新 capture
   ▼
CUDA Graph(预录制的 kernel 序列)

一个跨领域类比:这三阶段类似于 C++ 的编译流程。MjSpec 是源代码(可以随意编辑),MjModel 是编译后的目标文件(结构固定,但可以链接时替换符号),MjData 是运行时内存(每次执行都不同)。你不能在运行时给程序"加一个新类"——同样,你不能在 MjModel 阶段给机器人"加一个新关节"。

在 mjlab 中的实践意义

你想做什么 应该在哪个阶段做 mjlab 的入口
给场景加一个桌子 描述阶段(MjSpec) EntityCfg.spec_fnspec.attach()
修改地面摩擦(全局) 编译阶段(MjModel) MujocoCfg 或直接修改 model.geom_friction
修改某个 world 的质量(DR) 运行阶段(mjwarp.Data via WarpBridge) expand_model_fields("body_mass")
重置某个 world 的状态 运行阶段 bridge.qpos[env_ids] = default_pos
修改 solver iterations 编译阶段 MujocoCfg.iterations

当前限制与边界清单

MuJoCo Warp 在功能上不完全等于 CPU MuJoCo。以下是完整的边界清单(截至 2026 年 5 月):

类别 限制 工程影响 workaround
数值精度 仅 float32 极端精度需求不满足 接受微小误差或用 CPU 验证
Solver PGS 不完整支持 不要用 PGS 用 Newton(默认)
Solver noslip 不支持 高摩擦场景微滑 增大 impratio
Jacobian 总是 dense 大系统显存更高 接受或减少接触数
Override contact o_margin/o_solref/o_solimp 不可用 DR 不能覆盖接触参数 用 expand_model_fields
DOF >60 DOF 性能下降 人形手指级系统变慢 简化模型或用 CPU
可微分 不支持自动微分 不能 grad through sim 用 MJX(JAX)
Island 不支持 island 求解 多独立子系统不能分别求解 接受
Tendon 支持但有限制 复杂 tendon 网络需验证 CPU 上先验证

如何使用这张表:当你在 mjlab 中遇到"为什么 CPU 上能跑但 GPU 上不行"时,先来这张表查——可能是 GPU 后端的已知限制。


本章常见误解汇总

误解 正确理解
"MuJoCo 比 PhysX 更好" 各有强项——MuJoCo 接触精度高,PhysX 大规模场景强
"GPU 仿真 = 更精确" GPU 解决吞吐问题,不解决精度问题
"Newton 只是个新求解器" Newton 是开源、GPU 加速、可扩展的物理引擎,特点是模块化多求解器架构(统一 API 组织 MuJoCo Warp/Kamino/XPBD/VBD 等)
"solref 越硬越好" 太硬 → solver 不收敛 → 训练崩溃
"PGS 在 MuJoCo Warp 上能用" 不完整支持,避免使用
"MJCF → USD 无损转换" 多种特性会丢失(tendon/equality/class defaults)
"sim2sim 行为应该完全一致" 微小差异正常,关注定性行为
"跨引擎调参 = 翻译参数值" 两套参数机制不同,需理解底层差异

本章小结

术语速查表

术语 含义 首次出现
广义坐标 用最少变量描述系统状态(hinge joint = 1 变量) §3.1
凸优化接触 MuJoCo 的接触力求解方式——把约束松弛为凸问题,总是可解 §3.1
TGS Temporal Gauss-Seidel,PhysX 的迭代求解器,把 dt 分成子步 §3.1
Patch friction PhysX 把相邻接触点合并为 patch 再计算摩擦 §3.1
solref MuJoCo 接触刚度/阻尼参数 [timeconst, dampratio] §3.3
solimp MuJoCo 约束阻抗函数参数(通常用默认值) §3.3
impratio MuJoCo 的摩擦阻抗比,增大可减少微滑 §3.3
contact_offset PhysX 的接触检测距离 §3.3
num_position_iterations PhysX TGS 的位置迭代数(= 子步数) §3.3
MJCF MuJoCo XML Format,MuJoCo 的原生模型格式 §3.4
USD Universal Scene Description,NVIDIA Omniverse 的核心格式 §3.4
MjSpec MuJoCo 3.x 的可编辑模型蓝图 §3.4/§3.5
MjModel 编译后的 MuJoCo 模型(常量结构) §3.5
MjData 运行时状态容器(每步更新) §3.5
WarpBridge mjlab 中 Warp→PyTorch 的零拷贝桥接 §3.5
nconmax per-world 最大接触数(影响显存) §3.5
njmax per-world 最大约束行数(显存大户) §3.5
CUDA Graph 预录制的 GPU kernel 执行序列 §3.5
expand_model_fields mjlab 把共享参数展开为 per-world 的操作(触发 graph 重建) §3.5
nq / nv 广义位置维度 / 广义速度维度(因四元数而不相等) §3.5
sim2sim 在不同引擎间交叉验证策略行为 §3.6
delta-action model ASAP 提出的修补 sim-real gap 的方法 §3.6
builtin actuator MuJoCo 原生 actuator,integrator 可隐式处理 §3.1
explicit actuator 在 PyTorch 中计算力矩,integrator 视为外力 §3.1

知识点总表

编号 知识点 核心要点 对应节 难度
1 MuJoCo 物理模型 广义坐标 + 凸优化接触,solver 总能收敛 3.1 ⭐⭐
2 PhysX 物理模型 笛卡尔坐标 + TGS 迭代,patch friction 3.1 ⭐⭐
3 Newton 多求解器 7 个求解器统一 API,Isaac Lab 3.0 集成 3.1 ⭐⭐
4 CPU vs GPU MuJoCo 差异 PGS 不支持,float32,noslip 不支持 3.1 ⭐⭐⭐
5 引擎选型决策树 从任务需求出发,不从流行度出发 3.2 ⭐⭐⭐
6 MuJoCo solref/solimp 接触刚度/阻尼/阻抗函数,材质推荐表 3.3 ⭐⭐⭐
7 PhysX 接触参数 contact_offset/friction/iterations 3.3 ⭐⭐⭐
8 跨引擎参数不可直接翻译 不同机制,需理解底层差异 3.3 ⭐⭐⭐
9 MJCF 格式 MuJoCo 原生,表达力强(tendon/equality) 3.4 ⭐⭐
10 USD 格式 Pixar/NVIDIA,通用场景描述 3.4 ⭐⭐
11 模型互转坑 默认关节角/摩擦/tendon 丢失 3.4 ⭐⭐
12 GPU 数据流 MjSpec → MjModel → mjwarp → WarpBridge → CUDA Graph 3.5 ⭐⭐⭐
13 expand_model_fields DR 修改共享参数 → 展开 + 重建 graph 3.5 ⭐⭐⭐
14 sim2sim 验证 跨引擎验证策略鲁棒性,ASAP 参考 3.6 ⭐⭐⭐
15 排查优先级 先物理层(action/timestep/contact),后 RL 层 3.7 ⭐⭐

累积项目:本章新增模块

项目 本章贡献 验证标准
A 四足速度跟踪 理解 Go2 的物理参数配置(friction/timestep/solver) 能解释 velocity_env_cfg.py 中每个物理参数的含义和取值理由
B 人形 locomotion 理解 MuJoCo vs PhysX 的接触行为差异 能预测同一策略在两个引擎上的行为差异方向

项目 A 的具体检查清单

读完本章后,打开 mjlab 的 velocity_env_cfg.py,你应该能回答以下问题(下表为该 velocity 任务的配置取值,注意它会覆盖 MujocoCfg 的类默认 timestep=0.002/iterations=100):

参数 velocity 任务取值 你应该能解释
mujoco.timestep 0.005 为什么选 200 Hz 而不是 100 Hz 或 500 Hz?
mujoco.solver "newton" 为什么不用 CG 或 PGS?Newton 在接触密集任务中有什么优势?
mujoco.iterations 10 增大到 50 会怎样?减小到 2 会怎样?
mujoco.integrator "implicitfast" 为什么不用 "euler"?和 builtin actuator 有什么关系?
mujoco.impratio 1.0 如果脚底打滑,应该怎么调?
decimation 4 policy frequency 是多少?如果改成 2 呢?
nconmax 35 对四足机器人够不够?如果加上桌子和物体呢?
njmax 1500 主要的显存消耗者。如果 OOM 应该先降这个还是 nconmax?

如果有 3 个以上回答不出来,建议重读对应小节。

项目 B 的下一步(Ch04 预览):在 Isaac Lab 中查看 ANYmal 的 SimulationCfg,对比 PhysX 参数和 mjlab 的 MuJoCo 参数。注意 PhysX 没有 solref/solimp——接触行为由 num_position_iterationscontact_offset 间接控制。


版本信息速查

本章涉及的版本锚定(所有代码示例和参数以此为准):

组件 版本 相关内容
mjlab 1.2.0 MujocoCfg、SimulationCfg、expand_model_fields
MuJoCo ≥ 3.8.0 MjSpec API、solver/integrator 选项
MuJoCo Warp 随 mjlab 1.2.0 GPU solver 支持表、nconmax/njmax
Isaac Lab 2.3.0(主线) SimulationCfg、PhysxCfg、MaterialPropertiesCfg
Isaac Lab 3.0 Beta(注释标注) Newton 集成、wp.array 数据管线
Newton 1.0 GA 7 个求解器、Kamino 论文
PhysX 5.4+ TGS 子步机制、EnableExternalForcesEveryIteration

延伸阅读

物理引擎理论

资源 难度 说明
Todorov, MuJoCo: A physics engine for model-based control, IROS 2012 ⭐⭐⭐ MuJoCo 的设计哲学、凸优化接触模型的数学推导
Todorov, Convex and analytically-invertible dynamics with contacts and constraints, ICRA 2014 ⭐⭐⭐ MuJoCo 接触模型的完整理论基础
MuJoCo 官方文档:Computation ⭐⭐ 动力学方程、solver 算法、integrator 的详细描述
MuJoCo 官方文档:Modeling ⭐⭐ MJCF 格式的完整参考
PhysX 5.4 文档:Rigid Body Dynamics ⭐⭐ TGS 求解器、patch friction、constraint stabilization
PhysX 5.4 文档:Simulation ⭐⭐ solver residual reporting、performance tuning

接触参数与调优

资源 难度 说明
ROBOLAWEB solref/solimp Cheat Sheet MuJoCo 接触参数的实用推荐值
MuJoCo 官方文档:Contact ⭐⭐ solref/solimp 的完整数学描述和参数空间
NVIDIA Isaac Sim Articulation Stability Guide ⭐⭐ PhysX 接触调优的工程建议,含 TGS 子步机制
NVIDIA Supercharge Robotics Workflows with Isaac Sim 4.0, Technical Blog 2024 ⭐⭐ TGS 新特性:external forces every iteration + residual reporting
MuJoCo GitHub issue #2548 ⭐⭐ MJX vs CPU MuJoCo 的 sim2sim 差异详细分析

跨引擎验证

资源 难度 说明
He et al., ASAP: Aligning Simulation and Real-World Physics, RSS 2025, arXiv:2502.01143 ⭐⭐⭐ 跨仿真器 delta-action model,系统量化了 sim-sim gap
Borse et al., ComFree-Sim, arXiv:2603.12185 ⭐⭐⭐ 密集接触替代求解器,比 MuJoCo Warp 在密集接触场景快 2-3×
Kamino: GPU-based Massively Parallel Simulation of Multi-Body Systems with Challenging Topologies, arXiv:2603.16536 ⭐⭐⭐ Newton 闭环机构求解器的原始论文

Newton 与多引擎生态

资源 难度 说明
NVIDIA, Newton Adds Contact-Rich Manipulation and Locomotion, Technical Blog 2026 ⭐⭐ Newton 1.0 benchmark 和 7 个求解器介绍
Newton GitHub README 安装指南和快速入门
Isaac Lab 3.0 Release Notes ⭐⭐ Newton 集成状态、kit-less 模式、quaternion 变更

GPU 仿真工程

资源 难度 说明
NVIDIA Warp 文档 ⭐⭐⭐ 理解 MuJoCo Warp 的 GPU kernel 实现
Zakka et al., mjlab: A Lightweight Framework, arXiv 2601.22074 ⭐⭐ mjlab 的 WarpBridge、expand_model_fields、CUDA Graph 设计
NVIDIA CUDA Programming Guide: CUDA Graphs ⭐⭐⭐ CUDA Graph 的底层原理和约束

本章与后续章节的关系

本章是"物理地基"——后续所有涉及仿真行为的调试都应回到本章的框架:

后续章节 与本章的关系 本章哪个知识点为其铺垫
Ch04 Manager 架构 Manager 的 sim.step() 调用的就是本章讲的物理引擎 GPU 数据流(§3.5)、env.step 时序
Ch05 Obs/Action obs 来自物理状态(qpos/qvel/sensordata),action 写入 ctrl nq/nv 区别(§3.5)、actuator 类型
Ch06 Reward 设计 reward 读取物理状态,接触力异常会导致 reward 异常 现象→方程项映射表(§3.1)
Ch07 训练管线 timestep/decimation 影响 PPO 的 batch 结构 timestep×decimation 联合调参(§3.7)
Ch08 DR DR 修改物理参数,通过 expand_model_fields 实现 expand_model_fields 工作流(§3.5)
Ch11 URDF/MJCF 建模 深入模型文件的创建和编辑 MJCF 结构精讲(§3.4)、MjSpec API
Ch12 Actuator 建模 深入 actuator 模型的选择和调参 integrator + actuator 关系(§3.1)
Ch13 四足实战 四足任务的物理参数调优 接触调参案例 1(§3.3)
Ch17 操作实战 灵巧手任务的接触参数调优 接触调参案例 2(§3.3)
Ch23 Sim2Real 完整的 sim-to-real 部署管线 sim2sim 验证(§3.6)、ASAP 参考
Ch24 大规模训练 nconmax/njmax 的显存优化 显存估算(§3.5)、吞吐排查(§3.7)

阅读建议:如果你当前没有物理层问题,可以先跳到 Ch04(Manager 架构)。本章的内容在你遇到具体物理问题时(穿透/滑移/振荡/OOM)再回来查阅更高效。


🔧 故障排查手册

物理稳定性问题

症状 可能原因 排查步骤 相关节
机器人穿透地面 solref 太软 / contact_offset 太小 1. 检查接触参数 → 2. 减小 timestep → 3. 增加 solver iterations 3.3
脚底打滑 MuJoCo 软接触微滑 / PhysX friction 太低 1. 增加 impratio(MuJoCo) → 2. 增加 friction(PhysX) → 3. 添加 foot slip penalty 3.3
关节高频振荡 timestep 太大 / explicit actuator + 大 kd 1. 减小 timestep → 2. 改用 builtin actuator → 3. 降低 kd 3.7
训练第一步就 NaN action scale 太大 / 初始状态不合法 1. 降低 action scale → 2. 检查初始关节角度 → 3. 启用 NaN guard 3.7
DR 后性能骤降 expand_model_fields 重建 CUDA Graph 正常行为——第一次 DR 后慢一步,后续恢复 3.5

吞吐量问题

症状 可能原因 排查步骤 相关节
steps/s 远低于预期 viewer/video 开启 / sensor 过重 1. 加 --headless → 2. 关闭 video → 3. 减少 sensor 3.7
增加 num_envs 后 OOM nconmax 过大 降低 nconmax 或 num_envs 3.5
CUDA Graph 失败 warning 动态分支 / 数组替换 检查是否有条件分支或数组对象替换 3.5

跨引擎问题

症状 可能原因 排查步骤 相关节
同一机器人两引擎行为不同 接触模型差异 正常现象。比较定性行为(步态类型),不比较数值 3.6
sim2sim 差异很大 策略 overfit 仿真器特性 加更多 DR → 重新训练 → 重新 sim2sim 3.6
MJCF → USD 后姿态不对 默认关节角定义不同 对比两个格式的 default joint pos 3.4
MJCF 的 tendon 在 USD 中丢失 USD 不支持 MuJoCo tendon 用其他约束机制替代(如 mimic joint) 3.4
Isaac Lab 3.0 的 .data.* 返回 wp.array 而非 tensor 3.0 的 warp-native 变更 添加 wp.to_torch() 包装 3.5

常见"以为是物理问题但其实不是"的情况

症状 看起来像 实际原因 解决方法
机器人"飘"起来 接触不工作 gravity 被设为 (0,0,0) 检查 MujocoCfg.gravity
所有环境行为完全相同 物理引擎 bug seed 没有正确设置 检查 env.seed
reward 突然变成 NaN 物理爆炸 obs 中有 Inf(如除以零的速度) 在 obs 中加 clip
训练很慢但 GPU 利用率低 物理引擎瓶颈 CPU 端的 Python Manager 是瓶颈 torch.profiler 定位
策略"学会了"但可视化很丑 物理参数错 reward hacking(利用了不合理的 reward 项) 审查 reward terms

研究实践建议

给博士新生的物理引擎建议

  1. 从默认参数开始:mjlab 和 Isaac Lab 的默认物理参数已经过社区验证。除非你有明确的症状(穿透/滑移/振荡),否则不要改。
  2. 先排除物理问题再调 reward:如果 zero agent 可视化时机器人姿态就不对,reward 再怎么调也没用。
  3. 记录你的参数修改:每次修改物理参数时,在 WandB notes 中记录修改了什么、为什么改、改后行为如何变化。一个月后你会感谢自己。
  4. 不要同时改多个参数:这是所有调参的黄金法则——同时改两个参数,你永远不知道效果来自哪个。
  5. 用 CPU MuJoCo 做 sanity check:如果 GPU 训练行为奇怪,在 CPU MuJoCo 中跑同一个场景。CPU 行为是你的参考基准。

给有经验研究者的跨引擎建议

  1. 接受引擎间差异:ASAP 论文已经系统量化了跨引擎差异——这是物理建模差异的正常表现,不是 bug。
  2. 利用 sim2sim 筛选策略:如果策略在 sim2sim 时行为差异很大,增加 DR 比调物理参数更有效。
  3. 关注 Newton 的发展:Newton 1.0 GA 已经发布,但 Isaac Lab 的集成仍在 Beta。等集成成熟后,跨引擎验证将变得极其简单(一行配置切换后端)。
  4. ComFree-Sim 值得关注:对于密集接触场景(如灵巧手),ComFree-Sim(2026)比 MuJoCo Warp 快 2-3×——但它尚未集成到主流框架中。

物理参数版本管理

当你为一个新任务调好了物理参数后,建议用以下格式记录:

# physics_params_go2_rough_v2.py
# 最后验证日期: 2026-05-21
# 验证条件: mjlab 1.2.0, RTX 4090, 4096 envs
# 训练结果: mean_reward=15.2, mean_ep_len=480
PHYSICS_CONFIG = {
    "timestep": 0.005,
    "solver": "newton",
    "iterations": 10,
    "impratio": 5.0,
    "cone": "pyramidal",
    "decimation": 4,
    "nconmax": 40,
    "njmax": 2000,
    # 修改日志
    # v1: impratio=1.0 → 脚底打滑
    # v2: impratio=5.0 → 打滑消除,reward +15%
}

这种格式让你在半年后回来时能快速理解"为什么这个参数是这个值"。


Ch03 完结。 本章从物理建模思想出发,系统对比了 MuJoCo、PhysX 和 Newton 三大引擎,建立了选型决策树,深入了接触参数调优,讲解了模型格式差异,并给出了 GPU 数据流和跨引擎验证的工程实践。

你的收获: - 理解了 MuJoCo 凸优化 vs PhysX TGS vs Newton 多求解器的本质差异 - 拥有了一张从"症状→参数"的接触调优映射表 - 能够为新任务做出引擎选型决策 - 理解了 MuJoCo Warp 的 GPU 数据流和 CUDA Graph 约束 - 掌握了 sim2sim 验证和物理层排查的优先级

下一步行动: 1. 如果你的训练有物理问题(穿透/滑移/振荡)→ 用本章的排查优先级定位问题 2. 如果一切正常 → 进入 Ch04,深入 Manager-Based 架构的源码级精读 3. 如果你想做跨引擎验证 → 按 §3.6 的 sim2sim 流程操作 4. 如果你需要理解某个物理参数的深层含义 → 回到本章的术语速查表和延伸阅读

下一章(Ch04)将深入框架的 Manager-Based 架构——你已经知道"物理引擎怎么工作",接下来要理解"框架怎么把物理引擎包装成可训练的环境"。这是从"理解物理"到"理解框架"的关键过渡。

向 Ch08 的预告:本章提到的 expand_model_fields 在 Ch08(Domain Randomization)中会被密集使用——它是 mjlab 实现 mass/friction/damping 随机化的核心机制。Ch08 将详细讲解如何配置 DR,本章只需要理解其底层原理。

版本更新提醒:MuJoCo Warp 的功能持续扩展中——本章列出的 CPU/GPU 差异表和限制清单以 mjlab 1.2.0 + MuJoCo Warp 3.8.0 为准。新版本可能已经修复了部分限制(如 PGS 支持、sparse Jacobian)。查阅 MuJoCo Warp 的 CHANGELOG 获取最新信息。


给不同读者的阅读建议

如果你只做四足 locomotion:重点读 §3.1(MuJoCo 部分)、§3.3(MuJoCo solref/solimp 调优 + 案例 1)、§3.5(GPU 数据流)、§3.7(稳定性排查)。PhysX 和 USD 部分可以快速浏览。

如果你只做 Isaac Lab + PhysX:重点读 §3.1(PhysX 部分)、§3.3(PhysX 调优 + TGS 经验)、§3.7(吞吐排查)。MuJoCo Warp 数据流部分可以跳过。

如果你需要做跨引擎验证:全章精读,特别关注 §3.1(引擎差异)、§3.3(双框架参数对照表)、§3.6(sim2sim 三层验证)。

如果你遇到具体物理问题:直接跳到 §3.7(排查优先级表)和 🔧 故障排查手册——它们按症状组织,可以快速定位。

本教材版本锚定:本章所有代码示例和参数值以 mjlab 1.2.0 + Isaac Lab 2.3.0 为准。MuJoCo Warp 的 CPU/GPU 差异表以 MuJoCo Warp 3.8.0 为准。Newton 相关内容以 1.0 GA(GTC 2026)为准。PhysX 相关内容以 5.4+ 为准。

关于精度:本章多次提到"MuJoCo 接触精度高于 PhysX"——这个判断基于接触密集场景(多接触点、复杂摩擦耦合)的定性比较,不是所有场景的定量结论。在简单接触场景(单点碰撞、少量摩擦)中,PhysX 的精度完全足够。引擎选型应基于你的具体任务需求,而非"精度高 = 更好"的简单逻辑。

致谢:本章的接触参数推荐值部分参考了 ROBOLAWEB solref/solimp Cheat Sheet 和 MuJoCo Discord 社区的讨论。ASAP 跨引擎验证部分参考了 CMU LeCAR-Lab 的工作。PhysX TGS 调优建议参考了 NVIDIA Isaac Sim 稳定性指南和社区最佳实践。

提醒:本章的物理引擎知识是整本书的"地基"——当你在后续章节(Ch06 reward 设计、Ch08 DR、Ch13-17 实战)中遇到物理行为异常时,第一反应应该是回到本章的排查框架,而不是在 RL 层面盲目调参。

一个实用的自检方法:在开始任何新任务的训练之前,用 zero agent 和 random agent 可视化检查物理行为是否合理(Ch02 §2.5 的可视化调试工作流)。如果 zero agent 时机器人就穿透地面或关节扭曲——这是物理层问题,本章的排查优先级表可以帮你快速定位。

如果你在安装过程中遇到了本章未覆盖的物理引擎问题并找到了解决方案,欢迎在对应框架的 GitHub Issues 或 Discord 中分享——这是开源社区进步的基础。


物理引擎选型总结一句话:如果只记住一件事——接触密集选 MuJoCo Warp(mjlab),视觉密集选 PhysX(Isaac Lab),闭环机构选 Kamino(Newton)。其他所有复杂的选型分析都是在这个基础上的细化。


附:物理引擎快速参考卡

以下是本章最重要的参数和推荐值,适合打印或保存为 cheat sheet:

MuJoCo Warp(mjlab)默认配置

timestep = 0.005       # 200 Hz physics
decimation = 4         # 50 Hz policy
solver = "newton"      # 总是用 Newton
integrator = "implicitfast"  # 对 builtin actuator 最稳
iterations = 10        # solver 迭代数
impratio = 1.0         # 增大可减少微滑(locomotion 推荐 5-10)
cone = "pyramidal"     # pyramidal 快,elliptic 精确
nconmax = 35           # per-world 接触数(四足够用)
njmax = 1500           # per-world 约束行数(显存大户)

PhysX(Isaac Lab)默认配置

dt = 0.005                      # 200 Hz physics
decimation = 4                  # 50 Hz policy
solver_type = 1                 # TGS(始终推荐)
num_position_iterations = 4     # TGS 子步数
num_velocity_iterations = 1     # 通常不改
contact_offset = 0.02           # 接触检测距离
rest_offset = 0.01              # 静止间距
static_friction = 1.0           # 静摩擦(足式需 0.8-1.5)
dynamic_friction = 1.0          # 动摩擦
restitution = 0.0               # 弹性(0=非弹性)

以上参考值适用于 mjlab 1.2.0 + Isaac Lab 2.3.0。新版本可能有不同的默认值——查阅框架文档获取最新信息。

本章完。