使用 LEAPP 导出 Direct 工作流策略#
本进阶教程介绍如何为 Direct 工作流策略添加 LEAPP 导出支持。如果策略采用基于管理器的工作流,请参阅 标准 LEAPP 部署工作流 。
LEAPP 概念、支持的节点模式、状态反馈及运行时验证等背景知识,见 LEAPP 文档 。
快速开始#
要通过 LEAPP 导出 Direct 工作流策略,需要在环境代码中添加 LEAPP 标注。导出时,LEAPP 会跟踪已标注的张量,构建完整策略流水线的中间表示。这些标注在环境正常运行时不启用,仅带来少量开销,直到导出阶段才被激活。运行导出流程时,由 isaaclab leapp export --rl_library rsl_rl 激活它们。
本教程以 scripts/tutorials/06_deploy/anymal_c_env.py 为例,说明如何在 Direct 工作流环境中添加 LEAPP 标注。您可以将同样的标注方法用于自己的 Direct RL 环境。
这一导出流程需要 leapp 。它是专用的可选额外依赖,不包含在 --extra all 中。额外依赖的选择方式与 isaaclab train 相同:每次 uv run 都添加 --extra leapp ,并添加与任务匹配的后端额外依赖( isaacsim 、 ovphysx ,不依赖 Kit 的 Newton 则无需添加)。见 安装并运行第一个任务 及 使用 LEAPP 导出策略 。
如果希望使用现有的 IsaacContrib-Velocity-Rough-AnymalC-Direct 任务注册来运行导出的示例,请将已标注的教程环境复制到任务包中:
cp scripts/tutorials/06_deploy/anymal_c_env.py \
source/isaaclab_tasks/isaaclab_tasks/contrib/anymal_c_direct/anymal_c_env.py
copy scripts\tutorials\06_deploy\anymal_c_env.py ^
source\isaaclab_tasks\isaaclab_tasks\contrib\anymal_c_direct\anymal_c_env.py
环境具备所需的 LEAPP 输入、输出及状态标注后,可按与训练相同的后端额外依赖设置导出已训练策略。例如,使用 Isaac Sim PhysX 时:
# Newton: uv run --extra leapp isaaclab leapp export --rl_library rsl_rl ... physics=newton_mjwarp
# OV PhysX: uv run --extra ovphysx --extra leapp isaaclab leapp export --rl_library rsl_rl ... physics=ovphysx
# Isaac Sim:
OMNI_KIT_ACCEPT_EULA=Y ACCEPT_EULA=Y uv run --extra isaacsim --extra leapp isaaclab leapp export --rl_library rsl_rl \
--task <TASK_NAME> \
--checkpoint <PATH_TO_CHECKPOINT> \
--export_save_path <EXPORT_PATH> \
physics=isaacsim_physx
:: Newton: uv run --extra leapp isaaclab leapp export --rl_library rsl_rl ... physics=newton_mjwarp
:: OV PhysX: uv run --extra ovphysx --extra leapp isaaclab leapp export --rl_library rsl_rl ... physics=ovphysx
:: Isaac Sim:
set OMNI_KIT_ACCEPT_EULA=Y
set ACCEPT_EULA=Y
uv run --extra isaacsim --extra leapp isaaclab leapp export --rl_library rsl_rl ^
--task <TASK_NAME> ^
--checkpoint <PATH_TO_CHECKPOINT> ^
--export_save_path <EXPORT_PATH> ^
physics=isaacsim_physx
--task 参数是已注册的任务名,如 IsaacContrib-Velocity-Rough-AnymalC-Direct 。 --checkpoint 指向要导出的已训练 RSL-RL 检查点。训练请使用 uv run isaaclab train --rl_library rsl_rl --task <TASK_NAME> (见 安装并运行第一个任务 ),不要使用已移除的各后端 train.py 脚本。可选的 --export_save_path 指定导出产物的输出目录;省略时,导出到检查点所在目录。
备注
与受管理的环境不同,这里的 export_method 不是命令行选项。请在输出标注中指定导出后端,例如 annotate.output_tensors(..., export_with="onnx-dynamo") 。具体用法见下文的输出标注示例。
警告
本教程仅介绍 Direct RL 策略的导出。 isaaclab leapp deploy 目前不支持 Direct RL 策略。
导出参数的更多信息,见 标准 LEAPP 部署工作流 。
完整示例脚本
1# Copyright (c) 2022-2026, The Isaac Lab Project Developers (https://github.com/isaac-sim/IsaacLab/blob/main/CONTRIBUTORS.md).
2# All rights reserved.
3#
4# SPDX-License-Identifier: BSD-3-Clause
5
6# ruff: noqa: I001
7
8from __future__ import annotations
9
10import gymnasium as gym
11import torch
12import warp as wp
13
14from isaaclab.envs import DirectRLEnv
15from isaaclab.utils import index_fill_
16
17from .anymal_c_env_cfg import AnymalCFlatEnvCfg, AnymalCRoughEnvCfg
18from leapp import annotate # isort: skip
19
20
21class AnymalCEnv(DirectRLEnv):
22 cfg: AnymalCFlatEnvCfg | AnymalCRoughEnvCfg
23
24 def __init__(self, cfg: AnymalCFlatEnvCfg | AnymalCRoughEnvCfg, render_mode: str | None = None, **kwargs):
25 super().__init__(cfg, render_mode, **kwargs)
26 self._robot, self._contact_sensor, self._terrain = [
27 self.scene[name] for name in ("robot", "contact_sensor", "terrain")
28 ]
29 if isinstance(self.cfg, AnymalCRoughEnvCfg):
30 self._height_scanner = self.scene["height_scanner"]
31
32 self._actions = torch.zeros(self.num_envs, gym.spaces.flatdim(self.single_action_space), device=self.device)
33 self._previous_actions = torch.zeros(
34 self.num_envs, gym.spaces.flatdim(self.single_action_space), device=self.device
35 )
36
37 self._commands = torch.zeros(self.num_envs, 3, device=self.device)
38
39 self._episode_sums = {
40 key: torch.zeros(self.num_envs, dtype=torch.float, device=self.device)
41 for key in [
42 "track_lin_vel_xy_exp",
43 "track_ang_vel_z_exp",
44 "lin_vel_z_l2",
45 "ang_vel_xy_l2",
46 "dof_torques_l2",
47 "dof_acc_l2",
48 "action_rate_l2",
49 "feet_air_time",
50 "undesired_contacts",
51 "flat_orientation_l2",
52 ]
53 }
54 self._base_id, _ = self._contact_sensor.find_sensors("base")
55 self._feet_ids, _ = self._contact_sensor.find_sensors(".*FOOT")
56 self._undesired_contact_body_ids, _ = self._contact_sensor.find_sensors(".*THIGH")
57
58 def _pre_physics_step(self, actions: torch.Tensor):
59 self._actions = actions.clone()
60 self._processed_actions = self.cfg.action_scale * self._actions + self._robot.data.default_joint_pos.torch
61 # start LEAPP annotations for outputs
62 annotate.update_state(self.spec.id, {"previous_actions": actions})
63 annotate.output_tensors(self.spec.id, {"processed_actions": self._processed_actions}, export_with="onnx-dynamo")
64 # end LEAPP annotations for outputs
65
66 def _apply_action(self):
67 self._robot.set_joint_position_target_index(target=self._processed_actions)
68
69 def _get_observations(self) -> dict:
70 self._previous_actions = self._actions.clone()
71 height_data = None
72 if isinstance(self.cfg, AnymalCRoughEnvCfg):
73 height_data = (
74 self._height_scanner.data.pos_w.torch[:, 2].unsqueeze(1)
75 - self._height_scanner.data.ray_hits_w.torch[..., 2]
76 - 0.5
77 ).clip(-1.0, 1.0)
78 # start LEAPP annotations for inputs
79 root_lin_vel_b = annotate.input_tensors(self.spec.id, {"root_lin_vel_b": self._robot.data.root_lin_vel_b.torch})
80 root_ang_vel_b = annotate.input_tensors(self.spec.id, {"root_ang_vel_b": self._robot.data.root_ang_vel_b.torch})
81 projected_gravity_b = annotate.input_tensors(
82 self.spec.id, {"projected_gravity_b": self._robot.data.projected_gravity_b.torch}
83 )
84 commands = annotate.input_tensors(self.spec.id, {"commands": self._commands})
85 joint_pos = annotate.input_tensors(self.spec.id, {"joint_pos": self._robot.data.joint_pos.torch})
86 default_joint_pos = annotate.input_tensors(
87 self.spec.id, {"default_joint_pos": self._robot.data.default_joint_pos.torch}
88 )
89 joint_vel = annotate.input_tensors(self.spec.id, {"joint_vel": self._robot.data.joint_vel.torch})
90 if height_data is not None:
91 height_data = annotate.input_tensors(self.spec.id, {"height_data": height_data})
92 previous_actions = annotate.state_tensors(self.spec.id, {"previous_actions": self._actions})
93 # end LEAPP annotations for inputs
94
95 obs = torch.cat(
96 [
97 tensor
98 for tensor in (
99 root_lin_vel_b,
100 root_ang_vel_b,
101 projected_gravity_b,
102 commands,
103 joint_pos - default_joint_pos,
104 joint_vel,
105 height_data,
106 previous_actions,
107 )
108 if tensor is not None
109 ],
110 dim=-1,
111 )
112 observations = {"policy": obs}
113 return observations
114
115 def _get_rewards(self) -> torch.Tensor:
116 lin_vel_error = torch.sum(
117 torch.square(self._commands[:, :2] - self._robot.data.root_lin_vel_b.torch[:, :2]), dim=1
118 )
119 lin_vel_error_mapped = torch.exp(-lin_vel_error / 0.25)
120 yaw_rate_error = torch.square(self._commands[:, 2] - self._robot.data.root_ang_vel_b.torch[:, 2])
121 yaw_rate_error_mapped = torch.exp(-yaw_rate_error / 0.25)
122 z_vel_error = torch.square(self._robot.data.root_lin_vel_b.torch[:, 2])
123 ang_vel_error = torch.sum(torch.square(self._robot.data.root_ang_vel_b.torch[:, :2]), dim=1)
124 joint_torques = torch.sum(torch.square(self._robot.actuators.applied_effort.torch), dim=1)
125 joint_accel = torch.sum(torch.square(self._robot.data.joint_acc.torch), dim=1)
126 action_rate = torch.sum(torch.square(self._actions - self._previous_actions), dim=1)
127 first_contact = self._contact_sensor.compute_first_contact(self.step_dt).torch[:, self._feet_ids]
128 last_air_time = self._contact_sensor.data.last_air_time.torch[:, self._feet_ids]
129 air_time = torch.sum((last_air_time - 0.5) * first_contact, dim=1) * (
130 torch.linalg.norm(self._commands[:, :2], dim=1) > 0.1
131 )
132 net_contact_forces = self._contact_sensor.data.net_normal_forces_w_history.torch
133 is_contact = (
134 torch.max(torch.linalg.norm(net_contact_forces[:, :, self._undesired_contact_body_ids], dim=-1), dim=1)[0]
135 > 1.0
136 )
137 contacts = torch.sum(is_contact, dim=1)
138 flat_orientation = torch.sum(torch.square(self._robot.data.projected_gravity_b.torch[:, :2]), dim=1)
139
140 rewards = {
141 "track_lin_vel_xy_exp": lin_vel_error_mapped * self.cfg.lin_vel_reward_scale * self.step_dt,
142 "track_ang_vel_z_exp": yaw_rate_error_mapped * self.cfg.yaw_rate_reward_scale * self.step_dt,
143 "lin_vel_z_l2": z_vel_error * self.cfg.z_vel_reward_scale * self.step_dt,
144 "ang_vel_xy_l2": ang_vel_error * self.cfg.ang_vel_reward_scale * self.step_dt,
145 "dof_torques_l2": joint_torques * self.cfg.joint_torque_reward_scale * self.step_dt,
146 "dof_acc_l2": joint_accel * self.cfg.joint_accel_reward_scale * self.step_dt,
147 "action_rate_l2": action_rate * self.cfg.action_rate_reward_scale * self.step_dt,
148 "feet_air_time": air_time * self.cfg.feet_air_time_reward_scale * self.step_dt,
149 "undesired_contacts": contacts * self.cfg.undesired_contact_reward_scale * self.step_dt,
150 "flat_orientation_l2": flat_orientation * self.cfg.flat_orientation_reward_scale * self.step_dt,
151 }
152 reward = torch.sum(torch.stack(list(rewards.values())), dim=0)
153 for key, value in rewards.items():
154 self._episode_sums[key] += value
155 return reward
156
157 def _get_dones(self) -> tuple[torch.Tensor, torch.Tensor]:
158 time_out = self.episode_length_buf >= self.max_episode_length - 1
159 net_contact_forces = self._contact_sensor.data.net_normal_forces_w_history.torch
160 died = torch.any(
161 torch.max(torch.linalg.norm(net_contact_forces[:, :, self._base_id], dim=-1), dim=1)[0] > 1.0, dim=1
162 )
163 return died, time_out
164
165 def _reset_idx(self, env_ids: torch.Tensor | None):
166 if env_ids is None or len(env_ids) == self.num_envs:
167 env_ids = wp.to_torch(self._robot._ALL_INDICES)
168 assert env_ids is not None
169 self._robot.reset(env_ids)
170 super()._reset_idx(env_ids)
171 if len(env_ids) == self.num_envs:
172 self.episode_length_buf[:] = torch.randint_like(self.episode_length_buf, high=int(self.max_episode_length))
173 index_fill_(self._actions, env_ids, 0.0)
174 index_fill_(self._previous_actions, env_ids, 0.0)
175 self._commands[env_ids] = torch.zeros_like(self._commands[env_ids]).uniform_(-1.0, 1.0)
176 joint_pos = self._robot.data.default_joint_pos.torch[env_ids]
177 joint_vel = self._robot.data.default_joint_vel.torch[env_ids]
178 default_root_pose = self._robot.data.default_root_pose.torch[env_ids]
179 default_root_vel = self._robot.data.default_root_vel.torch[env_ids]
180 default_root_pose[:, :3] += self._terrain.env_origins[env_ids]
181 self._robot.write_root_pose_to_sim_index(root_pose=default_root_pose, env_ids=env_ids)
182 self._robot.write_root_velocity_to_sim_index(root_velocity=default_root_vel, env_ids=env_ids)
183 self._robot.write_joint_position_to_sim_index(position=joint_pos, env_ids=env_ids)
184 self._robot.write_joint_velocity_to_sim_index(velocity=joint_vel, env_ids=env_ids)
185 extras = dict()
186 for key in self._episode_sums.keys():
187 episodic_sum_avg = torch.mean(self._episode_sums[key][env_ids])
188 extras["Episode_Reward/" + key] = episodic_sum_avg / self.max_episode_length_s
189 index_fill_(self._episode_sums[key], env_ids, 0.0)
190 self.extras["log"] = dict()
191 self.extras["log"].update(extras)
192 extras = dict()
193 extras["Episode_Termination/base_contact"] = torch.count_nonzero(self.reset_terminated[env_ids]).item()
194 extras["Episode_Termination/time_out"] = torch.count_nonzero(self.reset_time_outs[env_ids]).item()
195 self.extras["log"].update(extras)
标注的工作原理#
主要工作是识别环境中的输入、输出和持久状态,并向 LEAPP 注册。本示例使用四个标注辅助函数:
annotate.input_tensors()标记进入策略流水线的张量。annotate.output_tensors()标记离开流水线中环境侧部分的张量。annotate.state_tensors()标记作为持久状态的张量。annotate.update_state()在每步之后更新该持久状态。
输入标注#
输入标注通常放在 _get_observations() 中,因为该方法负责收集传给策略的张量。
# start LEAPP annotations for inputs
root_lin_vel_b = annotate.input_tensors(self.spec.id, {"root_lin_vel_b": self._robot.data.root_lin_vel_b.torch})
root_ang_vel_b = annotate.input_tensors(self.spec.id, {"root_ang_vel_b": self._robot.data.root_ang_vel_b.torch})
projected_gravity_b = annotate.input_tensors(
self.spec.id, {"projected_gravity_b": self._robot.data.projected_gravity_b.torch}
)
commands = annotate.input_tensors(self.spec.id, {"commands": self._commands})
joint_pos = annotate.input_tensors(self.spec.id, {"joint_pos": self._robot.data.joint_pos.torch})
default_joint_pos = annotate.input_tensors(
self.spec.id, {"default_joint_pos": self._robot.data.default_joint_pos.torch}
)
joint_vel = annotate.input_tensors(self.spec.id, {"joint_vel": self._robot.data.joint_vel.torch})
if height_data is not None:
height_data = annotate.input_tensors(self.spec.id, {"height_data": height_data})
previous_actions = annotate.state_tensors(self.spec.id, {"previous_actions": self._actions})
# end LEAPP annotations for inputs
annotate.input_tensors() 包装一个张量,使 LEAPP 能跟踪依赖该张量的所有下游操作。该函数有两个重要参数:
self.spec.id标识张量所属的节点。运行isaaclab leapp export时,该 ID 与导出的策略节点对应。第二个参数是字典,将唯一的张量名映射到张量本身。LEAPP 在导出元数据和调试中使用这些名称。
为便于阅读,本例逐个注册观测张量; annotate.input_tensors() 也支持一次注册多个张量。
备注
未显式标注的输入会自动作为常量内联。对于固定变换或默认值等数据,这可能正是所需行为。
输出标注#
输出标注应放在环境已完成机器人指令准备的位置。本例中,该位置在 _pre_physics_step() 中。
# start LEAPP annotations for outputs
annotate.update_state(self.spec.id, {"previous_actions": actions})
annotate.output_tensors(self.spec.id, {"processed_actions": self._processed_actions}, export_with="onnx-dynamo")
# end LEAPP annotations for outputs
annotate.output_tensors() 标记离开流水线中环境侧部分的张量。与输入标注一样,调用时使用 self.spec.id ,以及将张量名映射到张量的字典。
export_with 参数将输出标注限定为特定导出后端。支持的后端名有 onnx-dynamo 、 onnx-torchscript 、 jit-script 和 jit-trace 。必须提供该参数,才能根据跟踪结果实际生成中间表示(IR)。
与 annotate.input_tensors() 不同,输出标注应只对流水线阶段的最终输出执行一次。本例中,要导出的张量是 processed_actions 。调用 annotate.output_tensors() 后,无需使用返回值。
备注
传入 annotate.output_tensors() 的所有张量都必须是已跟踪的张量,即由输入或输入派生的张量创建。
警告
不要在 _apply_action() 中添加输出标注。根据控制降采样倍数设置,该方法可能在每个环境步内被调用多次,从而导致跟踪出的流水线不正确。
状态标注#
如果策略依赖内部状态或反馈环路,请用 annotate.state_tensors() 显式注册这些数据,并通过 annotate.update_state() 更新。
本例将前一步动作作为观测的一部分,因此 previous_actions 是反馈状态:
在
_get_observations()中调用annotate.state_tensors(),使状态参与被跟踪的观测流水线。在
_pre_physics_step()中调用annotate.update_state(),为下一步更新存储值。
两次调用的状态名必须一致。本例中,两者都使用 previous_actions ,使 LEAPP 能正确传递反馈张量。
语义标注#
本例介绍跟踪流水线所需的最少标注。更复杂的导出工作流还可以附加语义元数据,让下游运行时了解每个张量的含义。要部署到下游部署库,必须提供语义标注。
对于 Direct 环境,语义标注是可选的,需由用户显式编写。与基于管理器的导出方式不同,Isaac Lab 不会自动推断 Direct 环境的张量语义,用户需自行提供这些数据。LEAPP 通过 TensorSemantics 支持这一功能。可用它更精确地描述张量含义,使导出流水线更易于检查、验证和集成到部署系统。
备注
语义标注的编写方法,见 LEAPP 语义标注指南 及 LEAPP API 参考 。