2121)
2222from genelab .cli ._interactive import pick_task_interactively
2323from genelab .cli ._prof import prof_app
24+ from genelab .cli ._progress import fetch_progress
2425from genelab .cli ._render import (
2526 render_cache ,
2627 render_entry_info ,
@@ -86,13 +87,16 @@ class _RegistryKindArg(str, Enum):
8687Runner flags (used when an RL runner is engaged):
8788
8889\b
89- --num_envs N Parallel environments.
90- --agent KIND one of: zero, random, trained (play only).
91- --checkpoint PATH Resume from a checkpoint.
92- --seed N RNG seed.
93- --log_dir PATH Override the log directory.
94- --max_iterations N Cap training iterations (train only).
95- --gpus N Distributed training across N GPUs (train only).
90+ --num_envs N Total parallel environments across all ranks.
91+ Must divide evenly by --gpus when both are set.
92+ --num_envs_per_gpu N Per-rank parallel environments. Mutually exclusive
93+ with --num_envs.
94+ --agent KIND one of: zero, random, trained (play only).
95+ --checkpoint PATH Resume from a checkpoint.
96+ --seed N RNG seed.
97+ --log_dir PATH Override the log directory.
98+ --max_iterations N Cap training iterations (train only).
99+ --gpus N Distributed training across N GPUs (train only).
96100
97101Profiling flags (forwarded to torch.profiler; rank-0 only):
98102
@@ -226,7 +230,8 @@ def info_cmd(
226230 ],
227231) -> None :
228232 _load_extensions (_state (ctx ))
229- render_entry_info (name )
233+ with fetch_progress ():
234+ render_entry_info (name )
230235
231236
232237@app .command (
@@ -331,7 +336,8 @@ def _configured_task(
331336 task_id , overrides = parse_run_args ([* tokens , picked ])
332337
333338 try :
334- task = cast (_RunnableTask , TASKS .get (task_id ))
339+ with fetch_progress ():
340+ task = cast (_RunnableTask , TASKS .get (task_id ))
335341 except KeyError as exc :
336342 raise SystemExit (str (exc )) from exc
337343
@@ -387,13 +393,15 @@ def _dispatch_play(
387393 task_cfg = getattr (task , "cfg" , None )
388394 agent_cfg = getattr (task_cfg , "agent" , None ) if task_cfg is not None else None
389395 checkpoint_raw = runner_args .get ("checkpoint" )
390- num_envs_raw = runner_args .get ("num_envs" )
391396 agent_raw = runner_args .get ("agent" )
392397 if agent_raw is not None and agent_raw not in _AGENT_KINDS :
393398 raise SystemExit (f"--agent must be one of {{zero, random, trained}}; got { agent_raw !r} " )
399+ # play is always single-process; either flag is accepted but mapped through the same
400+ # resolver so the mutual-exclusion guard fires on misuse.
401+ num_envs_per_rank = _resolve_per_rank_num_envs (runner_args , gpus = 1 )
394402 if (
395403 checkpoint_raw is None
396- and num_envs_raw is None
404+ and num_envs_per_rank is None
397405 and agent_raw is None
398406 and agent_cfg is None
399407 and not prof_args
@@ -408,7 +416,7 @@ def _dispatch_play(
408416 play_task (
409417 task_id ,
410418 checkpoint = Path (checkpoint_raw ) if checkpoint_raw is not None else None ,
411- num_envs = int ( num_envs_raw ) if num_envs_raw is not None else None ,
419+ num_envs = num_envs_per_rank ,
412420 agent = cast ("AgentKind | None" , agent_raw ),
413421 ** _coerce_prof_kwargs (prof_args ),
414422 )
@@ -435,44 +443,79 @@ def _dispatch_train(
435443
436444 gpus_raw = runner_args .pop ("gpus" , None )
437445 gpus = int (gpus_raw ) if gpus_raw is not None else 1
446+ num_envs_per_rank = _resolve_per_rank_num_envs (runner_args , gpus = gpus )
438447 if gpus > 1 and "TORCHELASTIC_RUN_ID" not in os .environ :
439- _relaunch_under_torchrun (gpus , agent_cfg , runner_args )
448+ _relaunch_under_torchrun (gpus , agent_cfg , runner_args , num_envs_per_rank )
440449 return
441450
442- num_envs_raw = runner_args .get ("num_envs" )
443451 max_iter_raw = runner_args .get ("max_iterations" )
444452 seed_raw = runner_args .get ("seed" )
445453 log_dir_raw = runner_args .get ("log_dir" )
446454 train_task (
447455 task_id ,
448456 agent_cfg ,
449- num_envs = int ( num_envs_raw ) if num_envs_raw is not None else None ,
457+ num_envs = num_envs_per_rank ,
450458 max_iterations = int (max_iter_raw ) if max_iter_raw is not None else None ,
451459 seed = int (seed_raw ) if seed_raw is not None else None ,
452460 log_dir = Path (log_dir_raw ) if log_dir_raw is not None else None ,
453461 ** _coerce_prof_kwargs (prof_args ),
454462 )
455463
456464
465+ def _resolve_per_rank_num_envs (runner_args : dict [str , str ], * , gpus : int ) -> int | None :
466+ """Pop ``num_envs`` / ``num_envs_per_gpu`` from ``runner_args`` and return per-rank N.
467+
468+ ``--num-envs N`` is interpreted as the **total** across all ranks: ``N // gpus``
469+ becomes the per-rank count and ``N`` must be divisible by ``gpus``.
470+ ``--num-envs-per-gpu M`` is verbatim per-rank. Passing both is a hard error so users
471+ don't quietly get one or the other's semantics. Returns ``None`` when neither flag
472+ was set so callers can defer to the cfg default.
473+ """
474+ total_raw = runner_args .pop ("num_envs" , None )
475+ per_gpu_raw = runner_args .pop ("num_envs_per_gpu" , None )
476+ if total_raw is not None and per_gpu_raw is not None :
477+ raise SystemExit (
478+ "--num-envs and --num-envs-per-gpu are mutually exclusive; pass exactly "
479+ "one (or neither, to use the task's cfg default)."
480+ )
481+ if per_gpu_raw is not None :
482+ return int (per_gpu_raw )
483+ if total_raw is None :
484+ return None
485+ total = int (total_raw )
486+ if gpus <= 0 :
487+ raise SystemExit (f"--gpus must be a positive integer (got { gpus } )" )
488+ if total % gpus != 0 :
489+ raise SystemExit (
490+ f"--num-envs { total } is not divisible by --gpus { gpus } ; pick a multiple "
491+ f"of { gpus } or use --num-envs-per-gpu instead."
492+ )
493+ return total // gpus
494+
495+
457496def _relaunch_under_torchrun (
458497 gpus : int ,
459498 agent_cfg : Any ,
460499 runner_args : dict [str , str ],
500+ num_envs_per_rank : int | None ,
461501) -> None :
462502 """Re-exec the current ``train`` invocation under torchrun.
463503
464504 The parent precomputes the log directory and forwards it via ``--log-dir`` so all
465505 ranks land in the same directory (avoiding per-rank timestamp drift). The original
466- argv is forwarded verbatim minus the ``--gpus N`` tokens so env overrides such as
467- ``--env.simulation.steps 5`` survive the relaunch.
506+ argv is forwarded verbatim minus the ``--gpus`` / ``--num-envs`` / ``--num-envs-per-gpu``
507+ tokens; if either env-count flag was set, an authoritative ``--num-envs-per-gpu N``
508+ is injected so each worker sees the parent-resolved per-rank value.
468509 """
469510 from genelab .rl .runner import resolve_log_dir
470511
471512 log_root_raw = runner_args .get ("log_dir" )
472513 log_root = Path (log_root_raw ) if log_root_raw else Path ("logs" ) / "rsl_rl"
473514 log_dir = resolve_log_dir (log_root , agent_cfg .experiment_name , agent_cfg .run_name )
474515
475- inner = _strip_gpus_flag (sys .argv [1 :])
516+ inner = _strip_distributed_flags (sys .argv [1 :])
517+ if num_envs_per_rank is not None :
518+ inner += ["--num-envs-per-gpu" , str (num_envs_per_rank )]
476519 if not _has_log_dir_flag (inner ):
477520 inner += ["--log-dir" , str (log_dir )]
478521
@@ -489,18 +532,24 @@ def _relaunch_under_torchrun(
489532 os .execvp (cmd [0 ], cmd )
490533
491534
492- def _strip_gpus_flag (tokens : list [str ]) -> list [str ]:
493- """Drop ``--gpus N`` (two-token form) and ``--gpus=N`` (single-token) from tokens."""
535+ _STRIPPABLE_DISTRIBUTED_FLAGS : Final [frozenset [str ]] = frozenset (
536+ {"--gpus" , "--num-envs" , "--num_envs" , "--num-envs-per-gpu" , "--num_envs_per_gpu" }
537+ )
538+
539+
540+ def _strip_distributed_flags (tokens : list [str ]) -> list [str ]:
541+ """Drop ``--gpus`` / ``--num-envs`` / ``--num-envs-per-gpu`` (and the ``_``-spelt forms),
542+ in both ``--flag value`` and ``--flag=value`` shapes, from a forwarded argv slice."""
494543 out : list [str ] = []
495544 skip_next = False
496545 for tok in tokens :
497546 if skip_next :
498547 skip_next = False
499548 continue
500- if tok == "--gpus" :
549+ if tok in _STRIPPABLE_DISTRIBUTED_FLAGS :
501550 skip_next = True
502551 continue
503- if tok .startswith ("--gpus=" ):
552+ if any ( tok .startswith (f" { flag } =" ) for flag in _STRIPPABLE_DISTRIBUTED_FLAGS ):
504553 continue
505554 out .append (tok )
506555 return out
0 commit comments