|
| 1 | +# Train a robot in a simulated environment with Isaac Lab and Modal. |
| 2 | +# |
| 3 | +# In this example, we'll use Modal to train a robot model in a simluated environment using an L40S GPU. |
| 4 | +# Specifically, we'll run a headless instance of Isaac Lab to train a policy that teaches Anymal-C, |
| 5 | +# a quadruped robot, to obey a velocity command and walk over rough terrain. |
| 6 | +# |
| 7 | +# Isaac Lab is NVIDIA's open source python framework for robot learning with GPUs. It's built on top of Isaac Sim, |
| 8 | +# NVIDIA's open source robotics simulation platform. Isaac Sim utilizes Omniverse (simulation and rendering) |
| 9 | +# and PhysX (physics engine), which both take advantage of GPUs for acceleration. |
| 10 | +# |
| 11 | +# Isaac Lab integrates with a variety of RL frameworks. Today, we'll use rl-games, an open source |
| 12 | +# reinforcement learning library for robotics training, with PPO as the training algorithm. All of |
| 13 | +# these details are transparent to our use, as Isaac Lab ships a pre-made `task` for training a quadruped |
| 14 | +# to follow a velocity command. |
| 15 | +# see: https://github.com/isaac-sim/IsaacLab/blob/main/source/isaaclab_tasks/isaaclab_tasks/manager_based/locomotion/velocity |
| 16 | +# |
| 17 | +# modal run isaac_lab_rl.py |
| 18 | +# Output mp4 is written to the `isaac-demo-output` Volume (and is downloadable). |
| 19 | + |
| 20 | +import shutil |
| 21 | +import subprocess |
| 22 | +from pathlib import Path |
| 23 | + |
| 24 | +import modal |
| 25 | + |
| 26 | +# NVIDIA's official container bundles Isaac Lab and all the necessary dependencies for this example. |
| 27 | +image = modal.Image.from_registry( |
| 28 | + "nvcr.io/nvidia/isaac-lab:3.0.0-beta2-post1", add_python="3.11" |
| 29 | +) |
| 30 | +# IMPORTANT: the isaac-lab image's ENTRYPOINT runs `/isaac-sim/runheadless.sh`, and never |
| 31 | +# execs the arguments passed to it, which is a requirement for Modal Images, so we must override it. |
| 32 | +# see https://modal.com/docs/guide/existing-images#entrypoint |
| 33 | +image = ( |
| 34 | + image.entrypoint([]) |
| 35 | + .env({"ACCEPT_EULA": "Y", "HYDRA_FULL_ERROR": "1"}) |
| 36 | + .run_commands("/workspace/isaaclab/isaaclab.sh -i rl_games") |
| 37 | +) |
| 38 | + |
| 39 | +app = modal.App("example-isaac-lab-rl") |
| 40 | + |
| 41 | +# Persisted shader cache at /root/.cache/ov, should reduce cold start times after first run. |
| 42 | +ov_cache = modal.Volume.from_name("isaac-ov-cache", create_if_missing=True) |
| 43 | +# Outputs (rendered mp4s) so you can grab them after the run. |
| 44 | +output_vol = modal.Volume.from_name("isaac-demo-output", create_if_missing=True) |
| 45 | + |
| 46 | +OUTPUT_PATH = "/output" |
| 47 | + |
| 48 | + |
| 49 | +# Training here is PPO via rl-games: thousands of robots run in parallel at each |
| 50 | +# iteration, each gets a random commanded velocity, |
| 51 | +# and a reward (track velocity, keep back horizontal, etc) shapes the |
| 52 | +# policy. Rough terrain adds a curriculum that ramps up difficulty. |
| 53 | +# We train once, then render the final checkpoint and record it as a video. |
| 54 | +@app.function( |
| 55 | + image=image, |
| 56 | + gpu="L40S:1", |
| 57 | + volumes={ |
| 58 | + "/root/.cache/ov": ov_cache, |
| 59 | + OUTPUT_PATH: output_vol, |
| 60 | + }, |
| 61 | + timeout=60 * 60, |
| 62 | +) |
| 63 | +def train_and_render_demo( |
| 64 | + train_task: str = "Isaac-Velocity-Rough-Anymal-C-v0", |
| 65 | + play_task: str = "Isaac-Velocity-Rough-Anymal-C-Play-v0", |
| 66 | + video_length: int = 200, |
| 67 | + num_envs: int = 4096, |
| 68 | + iterations: int = 125, |
| 69 | + play_seed: int = 3, |
| 70 | +): |
| 71 | + import time |
| 72 | + |
| 73 | + start_time = time.time() |
| 74 | + |
| 75 | + # First we train the policy using the rl-games training script baked into the image, implemented here: |
| 76 | + # https://github.com/isaac-sim/IsaacLab/blob/b0542fe2d45bf91c4e1d9ef6952b9c709c80b4e8/scripts/reinforcement_learning/rl_games/train.py |
| 77 | + # this is a thin script that is mainly responsible for instantiating a Gymnasium environment based |
| 78 | + # on the provided `task` and mediating the data exchange between rl-games' training runner and the environment. |
| 79 | + # `--viz none` disables all visualizers, so the simulation runs headless. |
| 80 | + run_name = "training_run" |
| 81 | + subprocess.run( |
| 82 | + [ |
| 83 | + "/workspace/isaaclab/isaaclab.sh", |
| 84 | + "-p", |
| 85 | + "scripts/reinforcement_learning/train.py", |
| 86 | + "--rl_library", |
| 87 | + "rl_games", |
| 88 | + "--task", |
| 89 | + train_task, |
| 90 | + "--viz", |
| 91 | + "none", |
| 92 | + "--num_envs", |
| 93 | + str(num_envs), |
| 94 | + "--max_iterations", |
| 95 | + str(iterations), |
| 96 | + "--kit_args", |
| 97 | + "--/log/level=error --/log/fileLogLevel=error --/log/outputStreamLevel=error --/omni.kit.plugin/usdMuteDiagnosticMessage=true", |
| 98 | + f"agent.params.config.full_experiment_name={run_name}", |
| 99 | + "agent.params.config.save_frequency=25", |
| 100 | + ], |
| 101 | + check=True, |
| 102 | + cwd="/workspace/isaaclab", |
| 103 | + ) |
| 104 | + print("Training completed") |
| 105 | + |
| 106 | + # Once we have trained the model, we grab the latest checkpoint and play a demo simulation at it. |
| 107 | + # We'll copy the video to the Volume-mounted output path so that the results are persisted |
| 108 | + # and can be downloaded later. |
| 109 | + |
| 110 | + checkpoint = _latest_checkpoint(run_name) |
| 111 | + |
| 112 | + clip = _render( |
| 113 | + checkpoint=checkpoint, |
| 114 | + play_task=play_task, |
| 115 | + video_length=video_length, |
| 116 | + play_seed=play_seed, |
| 117 | + ) |
| 118 | + print(f"Download with: `modal volume get isaac-demo-output {clip}`") |
| 119 | + |
| 120 | + end_time = time.time() |
| 121 | + print(f"Time taken: {end_time - start_time} seconds") |
| 122 | + |
| 123 | + |
| 124 | +@app.local_entrypoint() |
| 125 | +def main( |
| 126 | + train_task: str = "Isaac-Velocity-Rough-Anymal-C-v0", |
| 127 | + play_task: str = "Isaac-Velocity-Rough-Anymal-C-Play-v0", |
| 128 | + video_length: int = 200, |
| 129 | + num_envs: int = 4096, |
| 130 | + iterations: int = 80, |
| 131 | + play_seed: int = 3, |
| 132 | +): |
| 133 | + train_and_render_demo.remote( |
| 134 | + train_task=train_task, |
| 135 | + play_task=play_task, |
| 136 | + video_length=video_length, |
| 137 | + num_envs=num_envs, |
| 138 | + iterations=iterations, |
| 139 | + play_seed=play_seed, |
| 140 | + ) |
| 141 | + |
| 142 | + |
| 143 | +# Demo rendering utility |
| 144 | +def _render( |
| 145 | + checkpoint, |
| 146 | + play_task: str, |
| 147 | + video_length: int, |
| 148 | + play_seed: int, |
| 149 | +): |
| 150 | + x_vel, y_vel, yaw_vel = 1.0, 0.0, 0.0 |
| 151 | + cmd = [ |
| 152 | + "/workspace/isaaclab/isaaclab.sh", |
| 153 | + "-p", |
| 154 | + "scripts/reinforcement_learning/play.py", |
| 155 | + "--rl_library", |
| 156 | + "rl_games", |
| 157 | + "--task", |
| 158 | + play_task, |
| 159 | + "--viz", |
| 160 | + "none", |
| 161 | + "--enable_cameras", |
| 162 | + "--device", |
| 163 | + "cuda:0", |
| 164 | + "--num_envs", |
| 165 | + "1", |
| 166 | + "--video", |
| 167 | + "--video_length", |
| 168 | + str(video_length), |
| 169 | + "--seed", |
| 170 | + str(play_seed), |
| 171 | + "--kit_args", |
| 172 | + "--/log/level=error --/log/fileLogLevel=error --/log/outputStreamLevel=error --/omni.kit.plugin/usdMuteDiagnosticMessage=true", |
| 173 | + "--checkpoint", |
| 174 | + checkpoint, |
| 175 | + "env.viewer.origin_type=world", |
| 176 | + "env.viewer.eye=[5.2,5.2,2.7]", |
| 177 | + "env.viewer.lookat=[0.0,0.0,0.55]", |
| 178 | + "env.scene.terrain.terrain_generator.num_rows=1", |
| 179 | + "env.scene.terrain.terrain_generator.num_cols=1", |
| 180 | + "env.scene.terrain.max_init_terrain_level=0", |
| 181 | + "env.scene.terrain.terrain_generator.sub_terrains.pyramid_stairs.proportion=1.0", |
| 182 | + "env.scene.terrain.terrain_generator.sub_terrains.pyramid_stairs_inv.proportion=0.0", |
| 183 | + "env.scene.terrain.terrain_generator.sub_terrains.boxes.proportion=0.0", |
| 184 | + "env.scene.terrain.terrain_generator.sub_terrains.random_rough.proportion=0.0", |
| 185 | + "env.scene.terrain.terrain_generator.sub_terrains.hf_pyramid_slope.proportion=0.0", |
| 186 | + "env.scene.terrain.terrain_generator.sub_terrains.hf_pyramid_slope_inv.proportion=0.0", |
| 187 | + "env.commands.base_velocity.debug_vis=false", |
| 188 | + f"env.commands.base_velocity.ranges.lin_vel_x=[{x_vel},{x_vel}]", |
| 189 | + f"env.commands.base_velocity.ranges.lin_vel_y=[{y_vel},{y_vel}]", |
| 190 | + f"env.commands.base_velocity.ranges.ang_vel_z=[{yaw_vel},{yaw_vel}]", |
| 191 | + "env.commands.base_velocity.heading_command=false", |
| 192 | + "env.commands.base_velocity.ranges.heading=[0.0,0.0]", |
| 193 | + ] |
| 194 | + subprocess.run(cmd, check=True, cwd="/workspace/isaaclab") |
| 195 | + |
| 196 | + recording_dir = Path(checkpoint).parent.parent / "videos" / "play" |
| 197 | + recordings = sorted(recording_dir.glob("*.mp4")) |
| 198 | + if not recordings: |
| 199 | + raise FileNotFoundError(f"No recorded video in {recording_dir}.") |
| 200 | + |
| 201 | + # Copy the video to the Volume-mounted output path |
| 202 | + video_name = f"{play_task}.mp4" |
| 203 | + shutil.copyfile(recordings[0], Path(OUTPUT_PATH) / video_name) |
| 204 | + return video_name |
| 205 | + |
| 206 | + |
| 207 | +def _latest_checkpoint(run_name: str) -> str: |
| 208 | + import glob |
| 209 | + import re |
| 210 | + |
| 211 | + ckpts = glob.glob(f"/workspace/isaaclab/logs/rl_games/*/*{run_name}/nn/*.pth") |
| 212 | + if not ckpts: |
| 213 | + raise FileNotFoundError(f"No .pth checkpoints for run matching '{run_name}'.") |
| 214 | + |
| 215 | + def iteration(path: str) -> int: |
| 216 | + match = re.search(r"_ep_(\d+)", Path(path).name) |
| 217 | + return int(match.group(1)) if match else 0 |
| 218 | + |
| 219 | + return max(ckpts, key=iteration) |
0 commit comments