-
Notifications
You must be signed in to change notification settings - Fork 260
feat: prefetch gym venvs #2000
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
feat: prefetch gym venvs #2000
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,134 @@ | ||||||
| # Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved. | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Copyright year should be 2026 for this new file As per coding guidelines, the copyright header for new files should carry the current year (2026). ✏️ Proposed fix-# Copyright (c) 2025, NVIDIA CORPORATION. All rights reserved.
+# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| # | ||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||
| # you may not use this file except in compliance with the License. | ||||||
| # You may obtain a copy of the License at | ||||||
| # | ||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||
| # | ||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||
| # See the License for the specific language governing permissions and | ||||||
| # limitations under the License. | ||||||
|
|
||||||
| """Prefetch NeMo Gym internal venvs by doing a dry run of NemoGym initialization. | ||||||
|
|
||||||
| This complements nemo_rl/utils/prefetch_venvs.py (which prefetches Ray actor venvs) | ||||||
| by also triggering NeMo Gym's own internal venv creation for its servers (code_gen, | ||||||
| math, etc.). It reuses the real code path (create_env -> NemoGym.__init__) with | ||||||
| dry_run=True so no actual policy model is needed. | ||||||
| """ | ||||||
|
|
||||||
| import argparse | ||||||
| import sys | ||||||
|
|
||||||
| import ray | ||||||
| from omegaconf import OmegaConf | ||||||
|
|
||||||
| from nemo_rl.distributed.virtual_cluster import init_ray | ||||||
| from nemo_rl.environments.nemo_gym import ( | ||||||
| NemoGymConfig, | ||||||
| get_nemo_gym_uv_cache_dir, | ||||||
| get_nemo_gym_venv_dir, | ||||||
| ) | ||||||
| from nemo_rl.environments.utils import create_env | ||||||
| from nemo_rl.utils.config import load_config, register_omegaconf_resolvers | ||||||
|
|
||||||
|
|
||||||
| def prefetch_nemo_gym_venvs(config_paths: list[str]) -> None: | ||||||
| """Prefetch NeMo Gym venvs for each config by doing a dry-run initialization. | ||||||
|
|
||||||
| Args: | ||||||
| config_paths: List of paths to NeMo RL config files that contain | ||||||
| an env.nemo_gym section. | ||||||
| """ | ||||||
| register_omegaconf_resolvers() | ||||||
| init_ray() | ||||||
|
|
||||||
| succeeded = [] | ||||||
| failed = [] | ||||||
|
|
||||||
| for config_path in config_paths: | ||||||
| print(f"\n{'=' * 60}") | ||||||
| print(f"Processing config: {config_path}") | ||||||
| print("=" * 60) | ||||||
|
|
||||||
| try: | ||||||
| config = load_config(config_path) | ||||||
| config = OmegaConf.to_container(config, resolve=True) | ||||||
|
|
||||||
| nemo_gym_dict = dict(config["env"]["nemo_gym"]) | ||||||
| nemo_gym_dict["dry_run"] = True | ||||||
| uv_cache_dir = get_nemo_gym_uv_cache_dir() | ||||||
| if uv_cache_dir is not None: | ||||||
| nemo_gym_dict.setdefault("uv_cache_dir", uv_cache_dir) | ||||||
| uv_venv_dir = get_nemo_gym_venv_dir() | ||||||
| if uv_venv_dir is not None: | ||||||
| nemo_gym_dict.setdefault("uv_venv_dir", uv_venv_dir) | ||||||
|
|
||||||
| nemo_gym_config = NemoGymConfig( | ||||||
| model_name="dummy-model", | ||||||
| base_urls=["http://localhost:8000"], | ||||||
| initial_global_config_dict=nemo_gym_dict, | ||||||
| ) | ||||||
|
|
||||||
| print("Creating NeMo Gym environment (dry_run=True)...") | ||||||
| nemo_gym = create_env(env_name="nemo_gym", env_config=nemo_gym_config) | ||||||
|
|
||||||
| print("Waiting for NeMo Gym to finish initialization...") | ||||||
| ray.get(nemo_gym.health_check.remote()) | ||||||
|
Comment on lines
+79
to
+80
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If 🛡️ Proposed fix — add a generous build-time timeout- ray.get(nemo_gym.health_check.remote())
+ ray.get(nemo_gym.health_check.remote(), timeout=1800) # 30-minute cap for Docker builds🤖 Prompt for AI Agents |
||||||
| print("NeMo Gym initialized successfully.") | ||||||
|
|
||||||
| # TODO: Hangs... (DONT MERGE UNTIL FIXED - but kill may be fine) | ||||||
| # print("Shutting down NeMo Gym environment...") | ||||||
| # ray.get(nemo_gym.shutdown.remote()) | ||||||
| print("Killing NeMo Gym actor...") | ||||||
| ray.kill(nemo_gym) | ||||||
|
|
||||||
| succeeded.append(config_path) | ||||||
| print(f"Done with config: {config_path}") | ||||||
|
|
||||||
| except Exception as e: | ||||||
| print(f"Error processing {config_path}: {e}") | ||||||
| failed.append((config_path, str(e))) | ||||||
|
|
||||||
| print(f"\n{'=' * 60}") | ||||||
| print("NeMo Gym venv prefetch summary") | ||||||
| print("=" * 60) | ||||||
| print(f" Succeeded: {len(succeeded)}") | ||||||
| for path in succeeded: | ||||||
| print(f" - {path}") | ||||||
| if failed: | ||||||
| print(f" Failed: {len(failed)}") | ||||||
| for path, err in failed: | ||||||
| print(f" - {path}: {err}") | ||||||
|
|
||||||
| if failed: | ||||||
| sys.exit(1) | ||||||
|
|
||||||
|
|
||||||
| if __name__ == "__main__": | ||||||
| parser = argparse.ArgumentParser( | ||||||
| description="Prefetch NeMo Gym internal venvs via dry-run initialization.", | ||||||
| formatter_class=argparse.RawDescriptionHelpFormatter, | ||||||
| epilog="""\ | ||||||
| Examples: | ||||||
| # Prefetch venvs for a single config | ||||||
| uv run python examples/nemo_gym/prefetch_venvs.py \\ | ||||||
| examples/nemo_gym/grpo_workplace_assistant_nemotron_nano_v2_9b.yaml | ||||||
|
|
||||||
| # Prefetch venvs for multiple configs sequentially | ||||||
| uv run python examples/nemo_gym/prefetch_venvs.py \\ | ||||||
| examples/nemo_gym/grpo_workplace_assistant_nemotron_nano_v2_9b.yaml \\ | ||||||
| examples/nemo_gym/grpo_qwen3_30ba3b_instruct.yaml | ||||||
| """, | ||||||
| ) | ||||||
| parser.add_argument( | ||||||
| "configs", | ||||||
| nargs="+", | ||||||
| help="One or more NeMo RL config file paths containing an env.nemo_gym section.", | ||||||
| ) | ||||||
| args = parser.parse_args() | ||||||
|
|
||||||
| prefetch_nemo_gym_venvs(args.configs) | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,8 +11,10 @@ | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| import os | ||
| import subprocess | ||
| from pathlib import Path | ||
| from typing import Any, Dict, List, TypedDict | ||
| from typing import Any, Dict, List, Optional, TypedDict | ||
|
|
||
| import ray | ||
| import torch | ||
|
|
@@ -23,6 +25,30 @@ | |
| from nemo_rl.utils.timer import Timer | ||
|
|
||
|
|
||
| def get_nemo_gym_uv_cache_dir() -> Optional[str]: | ||
| """Return the uv cache directory inside a container, or None outside one. | ||
|
|
||
| Inside a container (NRL_CONTAINER=1), returns the uv cache location so Gym | ||
| stores its caches in the expected shared path. Returns None outside a | ||
| container, meaning the caller should omit this arg and let Gym create the | ||
| cache locally (the default when you may not be able to write to /opt). | ||
| """ | ||
| if not os.environ.get("NRL_CONTAINER"): | ||
| return None | ||
| return subprocess.check_output(["uv", "cache", "dir"]).decode().strip() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Partial executable path for
🛡️ Proposed fix+import shutil
...
- return subprocess.check_output(["uv", "cache", "dir"]).decode().strip()
+ uv_bin = shutil.which("uv") or "uv"
+ return subprocess.check_output([uv_bin, "cache", "dir"], timeout=30).decode().strip()🧰 Tools🪛 Ruff (0.15.1)[error] 38-38: Starting a process with a partial executable path (S607) 🤖 Prompt for AI Agents |
||
|
|
||
|
|
||
| def get_nemo_gym_venv_dir() -> Optional[str]: | ||
| """Return the NeMo Gym venv directory from NEMO_GYM_VENV_DIR, or None. | ||
|
|
||
| Returns the value of NEMO_GYM_VENV_DIR if set, otherwise None. When None | ||
| the caller should omit this arg and let Gym create venvs locally (the | ||
| default when a container is not used since you may not be able to write | ||
| to /opt). | ||
| """ | ||
| return os.environ.get("NEMO_GYM_VENV_DIR") | ||
|
|
||
|
|
||
| class NemoGymConfig(TypedDict): | ||
| model_name: str | ||
| base_urls: List[str] | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Prefetch script and
nemo_rlsource are both absent in thehermeticstage — build breaks by defaultTwo problems combine to break the Docker build:
Missing file:
examples/nemo_gym/prefetch_venvs.pyis never copied in thehermeticstage (the COPY commands at lines 120–126 only bring inpyproject.toml,uv.lock, twonemo_rlentry files, andtools/). Python will exit with "No such file or directory", failing theRUNlayer.Incomplete
nemo_rlsource: Even if the file were present, the script importsnemo_rl.environments.nemo_gym,nemo_rl.distributed.virtual_cluster, etc. None of these sub-packages are present inhermetic(onlynemo_rl/__init__.pyandnemo_rl/package_info.pyare copied), so the imports would raiseModuleNotFoundError.Because once a build argument is declared in a stage it is automatically inherited by child stages,
NEMO_GYM_PREFETCH_CONFIGShas its non-empty default value when theifcondition is evaluated inhermetic, so this code path executes on every default build.Recommended fix: Move the prefetch block to the
releasestage (after line 194, where the full source tree is copied), and re-declare the ARG there — matching the pattern already used fornemo_rl/utils/prefetch_venvs.py.🐛 Proposed fix — move prefetch to `release` stage
Remove lines 155–162 from the
hermeticRUN block, then in thereleasestage add:🤖 Prompt for AI Agents