|
3 | 3 | # |
4 | 4 | # SPDX-License-Identifier: BSD-3-Clause |
5 | 5 |
|
6 | | -"""Multi-GPU training entrypoint for Isaac Lab reinforcement learning workflows.""" |
| 6 | +"""Multi-GPU training executable for Isaac Lab reinforcement learning workflows.""" |
7 | 7 |
|
8 | | -from __future__ import annotations |
9 | | - |
10 | | -import argparse |
11 | | -import os |
12 | | -import shlex |
13 | | -import signal |
14 | | -import subprocess |
15 | | -import sys |
16 | | -from pathlib import Path |
17 | | -from types import FrameType |
18 | | - |
19 | | -SCRIPT_DIR = Path(__file__).resolve().parent |
20 | | -TRAIN_SCRIPT = SCRIPT_DIR / "train.py" |
21 | | - |
22 | | -DISTRIBUTED_LIBRARIES = ("rl_games", "rsl_rl", "skrl") |
23 | | -SKRL_JAX_TORCHRUN_ONLY_ARGS = ( |
24 | | - "master_addr", |
25 | | - "master_port", |
26 | | - "rdzv_backend", |
27 | | - "rdzv_endpoint", |
28 | | - "rdzv_id", |
29 | | - "max_restarts", |
30 | | - "monitor_interval", |
31 | | - "start_method", |
32 | | - "role", |
33 | | - "tee", |
34 | | - "redirects", |
35 | | - "local_ranks_filter", |
36 | | - "log_dir", |
37 | | -) |
38 | | - |
39 | | - |
40 | | -def _parse_args(argv: list[str]) -> tuple[argparse.Namespace, list[str]]: |
41 | | - """Parse multi-GPU launcher arguments and return forwarded training arguments.""" |
42 | | - parser = argparse.ArgumentParser( |
43 | | - description="Launch multi-GPU RL training with the selected distributed launcher.", |
44 | | - formatter_class=argparse.RawDescriptionHelpFormatter, |
45 | | - allow_abbrev=False, |
46 | | - epilog=( |
47 | | - "Examples:\n" |
48 | | - " train_multigpu --num_gpus 4 --task Isaac-Cartpole\n" |
49 | | - " train_multigpu --rl_library skrl --num_gpus 2 --task Isaac-Cartpole\n" |
50 | | - " train_multigpu --rl_library skrl --num_gpus 2 --ml_framework jax " |
51 | | - "--task Isaac-Cartpole\n" |
52 | | - "\n" |
53 | | - "All unrecognized arguments are forwarded to the selected training library." |
54 | | - ), |
55 | | - ) |
56 | | - parser.add_argument( |
57 | | - "--rl_library", |
58 | | - choices=DISTRIBUTED_LIBRARIES, |
59 | | - default="rsl_rl", |
60 | | - help="Distributed-capable training library to use. Defaults to rsl_rl.", |
61 | | - ) |
62 | | - parser.add_argument( |
63 | | - "--num_gpus", |
64 | | - "--nproc_per_node", |
65 | | - dest="nproc_per_node", |
66 | | - default="gpu", |
67 | | - help=( |
68 | | - "Number of trainer processes to launch on each node. Accepts an integer or torchrun values " |
69 | | - "'gpu', 'cpu', and 'auto'. skrl JAX training requires an integer. Defaults to 'gpu'." |
70 | | - ), |
71 | | - ) |
72 | | - parser.add_argument("--nnodes", default=None, help="Number of nodes to use for distributed training.") |
73 | | - parser.add_argument("--node_rank", default=None, help="Rank of this node in a multi-node job.") |
74 | | - parser.add_argument( |
75 | | - "--coordinator_address", |
76 | | - default=None, |
77 | | - help="IP address and port where skrl JAX process 0 starts the JAX coordinator service.", |
78 | | - ) |
79 | | - parser.add_argument("--master_addr", default=None, help="Master node address for static rendezvous.") |
80 | | - parser.add_argument("--master_port", default=None, help="Master node port for static rendezvous.") |
81 | | - parser.add_argument("--rdzv_backend", default=None, help="Rendezvous backend used by torchrun.") |
82 | | - parser.add_argument("--rdzv_endpoint", default=None, help="Rendezvous endpoint used by torchrun.") |
83 | | - parser.add_argument("--rdzv_id", default=None, help="User-defined rendezvous id used by torchrun.") |
84 | | - parser.add_argument("--max_restarts", default=None, help="Maximum worker group restarts before failing.") |
85 | | - parser.add_argument("--monitor_interval", default=None, help="Worker monitor interval [s].") |
86 | | - parser.add_argument( |
87 | | - "--start_method", |
88 | | - choices=("spawn", "fork", "forkserver"), |
89 | | - default=None, |
90 | | - help="Multiprocessing start method used by torchrun.", |
91 | | - ) |
92 | | - parser.add_argument("--role", default=None, help="User-defined worker role used by torchrun.") |
93 | | - parser.add_argument("--tee", default=None, help="Tee selected worker stdout/stderr streams.") |
94 | | - parser.add_argument("--redirects", default=None, help="Redirect selected worker stdout/stderr streams.") |
95 | | - parser.add_argument("--local_ranks_filter", default=None, help="Only show logs from the listed local ranks.") |
96 | | - parser.add_argument("--log_dir", default=None, help="Directory used by torchrun for worker logs.") |
97 | | - parser.add_argument( |
98 | | - "--log_all_ranks", |
99 | | - action="store_true", |
100 | | - help=( |
101 | | - "Show console output from every rank. By default only local rank 0 on each node is shown, because " |
102 | | - "each rank otherwise repeats the same startup, warning, and model-summary output. Tracebacks from " |
103 | | - "failing ranks are reported either way." |
104 | | - ), |
105 | | - ) |
106 | | - parser.add_argument( |
107 | | - "--dry_run", action="store_true", help="Print the distributed launcher command without running it." |
108 | | - ) |
109 | | - |
110 | | - args_cli, train_args = parser.parse_known_args(argv) |
111 | | - if train_args[:1] == ["--"]: |
112 | | - train_args = train_args[1:] |
113 | | - _validate_launcher_args(parser, args_cli, train_args) |
114 | | - return args_cli, train_args |
115 | | - |
116 | | - |
117 | | -def _append_optional_launcher_arg(command: list[str], args_cli: argparse.Namespace, name: str) -> None: |
118 | | - """Append a launcher argument when it was provided.""" |
119 | | - value = getattr(args_cli, name) |
120 | | - if value is not None: |
121 | | - command.extend([f"--{name}", str(value)]) |
122 | | - |
123 | | - |
124 | | -def _with_distributed_arg(train_args: list[str]) -> list[str]: |
125 | | - """Ensure the selected training library receives the distributed flag.""" |
126 | | - if "--distributed" in train_args: |
127 | | - return train_args |
128 | | - return [*train_args, "--distributed"] |
129 | | - |
130 | | - |
131 | | -def _get_forwarded_arg_value(args: list[str], name: str) -> str | None: |
132 | | - """Return the last value of a forwarded command-line option.""" |
133 | | - value = None |
134 | | - prefix = f"{name}=" |
135 | | - for index, arg in enumerate(args): |
136 | | - if arg == name and index + 1 < len(args): |
137 | | - value = args[index + 1] |
138 | | - elif arg.startswith(prefix): |
139 | | - value = arg[len(prefix) :] |
140 | | - return value |
141 | | - |
142 | | - |
143 | | -def _is_skrl_jax_launcher(args_cli: argparse.Namespace, train_args: list[str]) -> bool: |
144 | | - """Return whether the launch should use skrl's JAX distributed launcher.""" |
145 | | - ml_framework = _get_forwarded_arg_value(train_args, "--ml_framework") |
146 | | - return args_cli.rl_library == "skrl" and ml_framework == "jax" |
147 | | - |
148 | | - |
149 | | -def _get_visible_cuda_device_count() -> int | None: |
150 | | - """Return the number of visible CUDA devices on this node, or ``None`` if undetermined.""" |
151 | | - visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES") |
152 | | - if visible_devices is not None: |
153 | | - entries = [entry for entry in visible_devices.split(",") if entry.strip()] |
154 | | - return len(entries) |
155 | | - try: |
156 | | - import torch |
157 | | - except ImportError: |
158 | | - return None |
159 | | - try: |
160 | | - if not torch.cuda.is_available(): |
161 | | - return 0 |
162 | | - return torch.cuda.device_count() |
163 | | - except Exception: |
164 | | - return None |
165 | | - |
166 | | - |
167 | | -def _validate_num_gpus_against_visible_devices(parser: argparse.ArgumentParser, args_cli: argparse.Namespace) -> None: |
168 | | - """Error early when fewer CUDA devices are visible than --num_gpus requests.""" |
169 | | - try: |
170 | | - requested = int(str(args_cli.nproc_per_node)) |
171 | | - except (TypeError, ValueError): |
172 | | - return # torchrun keywords like "gpu"/"cpu"/"auto" are resolved by the launcher itself. |
173 | | - visible = _get_visible_cuda_device_count() |
174 | | - if visible is None: |
175 | | - return |
176 | | - if visible == 0: |
177 | | - parser.error( |
178 | | - f"--num_gpus/--nproc_per_node={requested} was requested but no CUDA devices are visible. " |
179 | | - "Verify the CUDA installation and CUDA_VISIBLE_DEVICES." |
180 | | - ) |
181 | | - if requested > visible: |
182 | | - parser.error( |
183 | | - f"--num_gpus/--nproc_per_node={requested} exceeds the {visible} CUDA device(s) visible to this " |
184 | | - "process. Lower --num_gpus or expose more devices via CUDA_VISIBLE_DEVICES." |
185 | | - ) |
186 | | - |
187 | | - |
188 | | -def _validate_launcher_args( |
189 | | - parser: argparse.ArgumentParser, args_cli: argparse.Namespace, train_args: list[str] |
190 | | -) -> None: |
191 | | - """Validate launcher-specific argument combinations.""" |
192 | | - if _is_skrl_jax_launcher(args_cli, train_args): |
193 | | - unsupported_args = [f"--{name}" for name in SKRL_JAX_TORCHRUN_ONLY_ARGS if getattr(args_cli, name) is not None] |
194 | | - if unsupported_args: |
195 | | - parser.error( |
196 | | - f"{', '.join(unsupported_args)} are torchrun-only options and cannot be used with skrl JAX " |
197 | | - "multi-GPU training. Use --coordinator_address <host:port> to configure the JAX coordinator." |
198 | | - ) |
199 | | - try: |
200 | | - nproc_per_node = int(str(args_cli.nproc_per_node)) |
201 | | - except ValueError: |
202 | | - parser.error( |
203 | | - "skrl JAX multi-GPU training requires an integer --num_gpus/--nproc_per_node value; " |
204 | | - "torchrun values 'gpu', 'cpu', and 'auto' are not supported by skrl.utils.distributed.jax." |
205 | | - ) |
206 | | - if nproc_per_node < 1: |
207 | | - parser.error("skrl JAX multi-GPU training requires --num_gpus/--nproc_per_node to be at least 1.") |
208 | | - elif args_cli.coordinator_address is not None: |
209 | | - parser.error("--coordinator_address is only supported with --rl_library skrl --ml_framework jax.") |
210 | | - |
211 | | - _validate_num_gpus_against_visible_devices(parser, args_cli) |
212 | | - |
213 | | - |
214 | | -def _run_distributed_command(command: list[str]) -> int: |
215 | | - """Run the distributed launcher and forward termination signals to the child process.""" |
216 | | - proc = subprocess.Popen(command) |
217 | | - |
218 | | - def _terminate_child(_signum: int, _frame: FrameType | None) -> None: |
219 | | - proc.terminate() |
220 | | - |
221 | | - previous_sigterm = signal.signal(signal.SIGTERM, _terminate_child) |
222 | | - previous_sigint = signal.signal(signal.SIGINT, _terminate_child) |
223 | | - try: |
224 | | - return proc.wait() |
225 | | - finally: |
226 | | - signal.signal(signal.SIGTERM, previous_sigterm) |
227 | | - signal.signal(signal.SIGINT, previous_sigint) |
228 | | - |
229 | | - |
230 | | -def _build_torchrun_command(args_cli: argparse.Namespace, train_args: list[str]) -> list[str]: |
231 | | - """Build the torchrun command for multi-GPU training.""" |
232 | | - command = [ |
233 | | - sys.executable, |
234 | | - "-m", |
235 | | - "torch.distributed.run", |
236 | | - "--nproc_per_node", |
237 | | - str(args_cli.nproc_per_node), |
238 | | - ] |
239 | | - for name in ( |
240 | | - "nnodes", |
241 | | - "node_rank", |
242 | | - "master_addr", |
243 | | - "master_port", |
244 | | - "rdzv_backend", |
245 | | - "rdzv_endpoint", |
246 | | - "rdzv_id", |
247 | | - "max_restarts", |
248 | | - "monitor_interval", |
249 | | - "start_method", |
250 | | - "role", |
251 | | - "tee", |
252 | | - "redirects", |
253 | | - "local_ranks_filter", |
254 | | - "log_dir", |
255 | | - ): |
256 | | - _append_optional_launcher_arg(command, args_cli, name) |
257 | | - |
258 | | - # Every rank writes the same startup, warning, and model-summary output, so an unfiltered console |
259 | | - # repeats it once per GPU. Restrict it to local rank 0 unless the caller opted out or set an |
260 | | - # explicit filter. Note this is torchrun's local-rank filter, so a multi-node job still prints one |
261 | | - # copy per node. Failures still surface: torchrun names the failing rank and reports the traceback |
262 | | - # that ``record`` captures in train.py. |
263 | | - if not args_cli.log_all_ranks and args_cli.local_ranks_filter is None: |
264 | | - command.extend(["--local_ranks_filter", "0"]) |
265 | | - |
266 | | - command.extend( |
267 | | - [ |
268 | | - str(TRAIN_SCRIPT), |
269 | | - "--rl_library", |
270 | | - args_cli.rl_library, |
271 | | - *_with_distributed_arg(train_args), |
272 | | - ] |
273 | | - ) |
274 | | - return command |
275 | | - |
276 | | - |
277 | | -def _build_skrl_jax_command(args_cli: argparse.Namespace, train_args: list[str]) -> list[str]: |
278 | | - """Build the skrl JAX distributed command for multi-GPU training.""" |
279 | | - command = [ |
280 | | - sys.executable, |
281 | | - "-m", |
282 | | - "skrl.utils.distributed.jax", |
283 | | - "--nproc_per_node", |
284 | | - str(args_cli.nproc_per_node), |
285 | | - ] |
286 | | - for name in ("nnodes", "node_rank", "coordinator_address"): |
287 | | - _append_optional_launcher_arg(command, args_cli, name) |
288 | | - |
289 | | - command.extend( |
290 | | - [ |
291 | | - str(TRAIN_SCRIPT), |
292 | | - "--rl_library", |
293 | | - args_cli.rl_library, |
294 | | - *_with_distributed_arg(train_args), |
295 | | - ] |
296 | | - ) |
297 | | - return command |
298 | | - |
299 | | - |
300 | | -def _build_distributed_command(args_cli: argparse.Namespace, train_args: list[str]) -> list[str]: |
301 | | - """Build the distributed launcher command for multi-GPU training.""" |
302 | | - if _is_skrl_jax_launcher(args_cli, train_args): |
303 | | - return _build_skrl_jax_command(args_cli, train_args) |
304 | | - return _build_torchrun_command(args_cli, train_args) |
| 8 | +from isaaclab_rl.entrypoints import run_train_multigpu_cli |
305 | 9 |
|
306 | 10 |
|
307 | 11 | def main(argv: list[str] | None = None) -> int: |
308 | 12 | """Launch multi-GPU training with the selected distributed launcher.""" |
309 | | - if argv is None: |
310 | | - argv = sys.argv[1:] |
311 | | - |
312 | | - args_cli, train_args = _parse_args(argv) |
313 | | - command = _build_distributed_command(args_cli, train_args) |
314 | | - |
315 | | - if args_cli.dry_run: |
316 | | - print(shlex.join(command)) |
317 | | - return 0 |
318 | | - |
319 | | - print(f"[INFO] Launching distributed training with: {shlex.join(command)}") |
320 | | - return _run_distributed_command(command) |
| 13 | + return run_train_multigpu_cli(argv) |
321 | 14 |
|
322 | 15 |
|
323 | 16 | if __name__ == "__main__": |
|
0 commit comments