一个 RL step 里发生了什么#
学习目标
读完本页你能:
按顺序说出一次
env.step(action)内部发生的事,以及每一步由哪个类负责区分物理步长、控制周期、渲染间隔三个时间尺度,知道改哪个参数影响什么
解释为什么一次 step 同时推进几千个环境,而不是逐个循环
指出 Direct 环境在这张图上替换了哪些环节
前置知识
0.2 分层依赖图:本页每一步都会标注它落在哪一层
本页的时序图是全站的锚点图。之后讲仿真循环(3.1)、各个 Manager(4.9)、源码导读(9.1)时都会回到这里。本页以 Isaac Lab v2.3.2 的 Manager-based 环境 ManagerBasedRLEnv 为准。
一圈时序图#
这一节回答:策略给出一个动作,到它拿回下一帧观测,中间经过了谁?
下图按源码中 ManagerBasedRLEnv.step() 的实际执行顺序画出[1]。虚线 loop 框是 decimation 循环:一个 RL step 内执行多次物理步。
%%{init: {"sequence": {"mirrorActors": false, "width": 96, "actorMargin": 16, "height": 40, "messageMargin": 12, "boxMargin": 4, "noteMargin": 4, "boxTextMargin": 2, "diagramMarginY": 4, "wrap": true}}}%%
sequenceDiagram
participant P as 策略<br/>RL 库
participant W as Wrapper
participant E as Env.step
participant A as Action<br/>Manager
participant S as Scene / 资产
participant X as Simulation<br/>Context
participant M as 其他 Manager
P->>W: step(actions)
W->>E: step(actions)
E->>A: process_action
loop decimation 次
E->>A: apply_action
A->>S: 设关节目标
E->>S: write_data_to_sim
E->>X: sim.step(PhysX 前进 dt)
E->>S: scene.update(dt)
end
E->>M: 终止 → 奖励 → _reset_idx → 指令 → 事件 → 观测
E-->>W: obs, rew, 终止标志
W-->>P: obs, rew, dones
Note over A,M: Direct 环境:Action Manager 与其他 Manager 换成子类方法
图 1:一次 env.step() 的调用时序(Isaac Lab 2.3.2,Manager-based)。实线是调用,虚线是返回;loop 框内的步骤每个 RL step 执行 decimation 次。渲染没有画进图里,它只在满足条件时才发生,见下文"三个时间尺度"。
读这张图时,可以用 loop 框把一个 step 切成三段:
loop 之前:只有
process_action一步,纯 Isaac Lab 的张量运算,不碰仿真器。loop 之内:一个 step 里唯一碰到仿真器的地方。
write_data_to_sim把数据交给 PhysX,sim.step让 Isaac Sim 驱动 PhysX 前进,scene.update再把时间戳推进到新状态。loop 之后:又回到 Isaac Lab 内部,终止、奖励、重置、指令、观测依次在 GPU 张量上计算,最后原路返回给 RL 库。
所以排查性能或行为问题时,先判断它发生在哪一段:物理不稳定看 loop 之内,奖励或观测算错看 loop 之后。
逐步说明#
这一节回答:图上每一步具体是哪个类的哪个方法,落在哪一层?
"层"一列对应 0.2 分层依赖图 的层次。行号均指向 v2.3.2 源码。
# |
发生什么 |
层 |
类与方法 |
源码 |
|---|---|---|---|---|
1 |
RL 库把一批动作交给适配层(wrapper);wrapper 按需裁剪后调用环境 |
RL 库 / Isaac Lab |
|
|
2 |
动作按 Term 切分,每个 Term 做缩放、偏移等处理,结果缓存起来 |
Isaac Lab |
|
|
3 |
进入 decimation 循环;每一轮把处理好的动作写成关节目标(例如 |
Isaac Lab |
|
|
4 |
资产经执行器模型(actuator)算出实际下发的目标或力矩,写入 PhysX 缓冲区 |
Isaac Lab → PhysX |
|
|
5 |
只推进物理,不渲染;Isaac Sim 的 |
Isaac Lab → Isaac Sim → PhysX |
|
|
6 |
资产数据的时间戳前进一个 |
Isaac Lab |
|
|
7 |
循环结束,回合计数加一,计算终止条件(分为 terminated 与 time_outs) |
Isaac Lab |
|
[1] L204 |
8 |
计算奖励,各项按权重乘以 |
Isaac Lab |
|
[1] L208 |
9 |
对已结束的环境执行课程更新、场景重置、reset 事件,并清零各 Manager 的状态 |
Isaac Lab |
|
[1] L221、L349–L394 |
10 |
更新目标指令,执行 interval 类事件(例如周期性推一下机器人) |
Isaac Lab |
|
[1] L232、L235 |
11 |
计算观测,此时读取的是步 6 之后的最新物理状态;对刚重置的环境,则是步 9 写入的新初始状态 |
Isaac Lab |
|
[1] L238 |
12 |
返回观测、奖励、terminated、truncated、extras;rsl_rl wrapper 把后两者合并成 dones |
Isaac Lab / RL 库 |
|
注意两个顺序上的细节:
观测在重置之后计算(步 9 在步 11 之前)。已结束环境返回的观测是新回合的第一帧,源码注释也说明了这一点[1]。
奖励在重置之前计算(步 8 在步 9 之前)。所以结束那一步的奖励仍按旧回合的状态计算。
三个时间尺度#
这一节回答:dt、decimation、render_interval 分别管什么?
名称 |
定义 |
默认值(源码) |
官方 reach 任务的取值 |
改它影响什么 |
|---|---|---|---|---|
物理步长 |
|
1/60 s[11] |
1/60 s(60 Hz)[12] |
仿真精度与稳定性;越小越准,但同样的仿真时长要算更多步 |
控制周期 |
|
|
1/60 × 2 = 1/30 s(30 Hz) |
策略多久出一次动作;决定每个 RL step 内执行几次物理步 |
渲染间隔 |
每 |
1[11] |
设为等于 decimation,即每个 RL step 渲染一次 |
只在有 GUI 或 RTX 传感器时生效;影响画面刷新率与相机数据的新鲜度 |
physics_dt 和 step_dt 在环境基类里定义为两个属性[14]。回合长度按控制周期换算:reach 任务 episode_length_s = 12.0[12],即 12 ÷ (1/30) = 360 个 RL step。
渲染的条件写在循环里:物理步计数是 render_interval 的整数倍,并且 has_gui() 或 has_rtx_sensors() 为真,才调用 sim.render()[1](L179、L194–L195)。headless 训练、场景里又没有相机时,整个训练过程不渲染。
调参时的对应关系:
机器人抖动、穿模:先看
sim.dt和 PhysX 求解参数,属于物理层,见 3.3。想让策略控制更频繁:减小 decimation;训练会变慢,因为同样的仿真时长要做更多次策略推理。
改了 decimation 之后,记得同步检查
render_interval和episode_length_s的含义是否仍符合预期。
数据在哪里#
这一节回答:几千个环境是怎么同时推进的?
step() 的输入和所有输出都是张量,第一维是环境数:动作的形状是 (num_envs, action_dim)[1],奖励、终止标志是 (num_envs,),观测按组是 (num_envs, …)。它们默认在 GPU 上(SimulationCfg.device = "cuda:0"[11])。
所以 step() 里没有"对每个环境循环"的代码。decimation 循环是时间上的循环,每一轮都一次性推进全部环境。PhysX 的读写也是批量的:Articulation 通过 root_physx_view 一次性设置或读取所有环境的关节数据[6][10]。
为什么要这样设计,以及它和强化学习样本效率的关系,见 5.4 为什么需要几千个并行环境。
Direct 环境的差异#
这一节回答:不用 Manager 的写法,这张图哪里会变?
DirectRLEnv.step() 保留了相同的骨架:decimation 循环、write_data_to_sim、sim.step(render=False)、scene.update 都还在[15]。变化是图 1 中 ActionManager 和"其他 Manager"那两列(图末的 Note 标出了这个替换点),换成了你在子类里实现的方法:
Manager-based |
Direct(用户实现) |
源码 |
|---|---|---|
|
|
[15] L363 |
|
|
[15] L373 |
|
|
[15] L391 |
|
|
[15] L393 |
|
|
[15] L410 |
若配置了 events,interval 事件在 Direct 环境中仍由 EventManager 处理[15](L405–L407)。两种写法怎么选,见 4.14 Direct 环境。
常见误解#
误解一:一个 step 就是一次物理步
一个 RL step 包含 decimation 次物理步[1]。reach 任务中是 2 次[12]。日志里的"步数"通常指 RL step;要换算成仿真时间,得乘以 step_dt,而不是 sim.dt。
误解二:reset 会重新加载场景
不会。_reset_idx 只对结束的那部分环境(env_ids)调用 scene.reset,再用 reset 事件写入新的初始状态,并清零各 Manager 的缓冲[1](L349–L394)。USD 场景和其他环境都不受影响,也不会打断同一批次里还在运行的环境。
误解三:渲染每步都发生
sim.step 调用时固定传 render=False。渲染只在满足 render_interval 且有 GUI 或 RTX 传感器时才发生[1]。反过来,如果你加了相机,却把 render_interval 设得比 decimation 大,相机数据就会比控制周期旧(推断:依据源码注释,渲染间隔被假定为最短可接受的渲染间隔,需要更高频率渲染的相机会出现意外行为[1],L191–L192)。
误解四:资产的 data.* 每个物理步都会全部从 PhysX 拷一遍
大部分 data.* 不会每步拷贝:joint_pos、body 位姿等在被读取、且时间戳过期时才从 PhysX 视图取数据。例外是 Articulation 的关节速度:为了用差分算关节加速度,ArticulationData.update 每个物理步都会读一次[10]。
延伸阅读#
源码:
官方文档:
Isaac Lab 2.3.2:Task Design Workflows(Manager-based 与 Direct 两种工作流)
版本说明#
Isaac Lab 2.x 其他小版本:本页只对照了 v2.3.2 源码,未逐一核对其他 2.x 版本。图中省略了
recorder_manager的调用(用于录制示教数据,默认没有 Term 时不影响流程)以及重置后按num_rerenders_on_reset补渲染的分支[1]。Isaac Lab 3.0.0-EA:资产与传感器的
.data.*返回ProxyArray而非torch.Tensor;写入方法拆分为_index与_mask两种;物理后端可选 Newton,此时步 5 不再经过 Isaac Sim 与 PhysX[16]。详见 8.1 3.0 改了什么、为什么改。