Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docker-bake.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ variable "NEMO_RL_REPO" {
# RL pins Gym as a git submodule (-> soluwalana/Gym over https), so Gym rides in with the RL git ADD
# - no separate Gym pin needed.
variable "NEMO_RL_REF" {
default = "eab835b111b78d90c3629b26af9b3f12dc0d0d29" # soluwalana/RL nmp/customizer
default = "9932dc8aa63a55fd431670d1b7c9d0bf3b2d2373" # soluwalana/RL nmp/customizer
}
variable "RL_BASE_CONTEXT" {
default = ""
Expand Down
14 changes: 14 additions & 0 deletions docker/rl/Dockerfile.nmp-rl-base
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ ENV RAY_USAGE_STATS_ENABLED=0 \
NEMO_RL_VENV_DIR=/opt/ray_venvs \
NEMO_GYM_VENV_DIR=/opt/gym_venvs

# ---- gym-wheel: an installable nemo-gym for the Gym per-server venvs ----
# Gym resolves `nemo-gym==<parent version>` from an index for every staged server, and this
# image's fork version is published nowhere. See scripts/build-gym-wheel.sh for the why.
FROM base AS gym-wheel
COPY --from=nemo-rl 3rdparty/Gym-workspace/Gym /build/Gym
RUN --mount=type=bind,source=docker/rl/scripts/build-gym-wheel.sh,target=/build/build-gym-wheel.sh \
bash /build/build-gym-wheel.sh /build/Gym /opt/gym-wheels

# ---- builder: uv sync RL + Gym with the extras we use ----
# fsdp (DPO training), automodel (GRPO training on DTensor V2, the only LoRA-capable DTensor
# backend), mcore (Megatron training + Megatron-native generation), vllm (generation), nemo_gym and
Expand Down Expand Up @@ -542,12 +550,18 @@ RUN apt-get purge -y ccache vim vim-common less >/dev/null 2>&1 || true; \
/opt/nvidia/nsight-systems-cli /opt/nvidia/nsight-compute \
/usr/local/bin/nsys /usr/local/bin/nsys-ui /usr/local/bin/ncu /usr/local/bin/ncu-ui \
|| true
# UV_FIND_LINKS below is additive: it adds nemo-gym as a local candidate, and everything else
# still resolves from the index. Copied here rather than in `base` so the build's own uv steps
# never see a find-links path that does not exist yet.
COPY --from=gym-wheel /opt/gym-wheels /opt/gym-wheels

# NO_VCS_VERSION=1: with no .git present, nemo_rl/package_info.py's `git rev-parse` would fail on every
# `import nemo_rl` (harmlessly, but it forks a subprocess each time, and this image starts many
# worker processes). The flag is package_info.py's own opt-out, so the lookup is skipped entirely.
ENV VIRTUAL_ENV=/opt/nemo_rl_venv \
HF_HUB_ENABLE_HF_TRANSFER=1 \
PATH="/opt/nemo_rl_venv/bin:/usr/local/bin:${PATH}" \
UV_FIND_LINKS=/opt/gym-wheels \
NO_VCS_VERSION=1

# The venv's python symlinks to CPython under /opt/cpython, nemo_rl is installed
Expand Down
67 changes: 67 additions & 0 deletions docker/rl/scripts/build-gym-wheel.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# Build an installable nemo-gym wheel for the Gym per-server venvs.
#
# Gym rewrites the install line for any server directory that is NOT inside a Gym checkout --
# which a platform environment FileSet staged at /job/environment never is. It strips the editable
# line and substitutes `nemo-gym==<the parent venv's version>`, resolved from a package INDEX
# (nemo_gym/cli/setup_command.py, _get_nemo_gym_version_spec). This image runs the soluwalana fork,
# whose version is published nowhere, so that resolution has no candidate and every native-v1 job
# dies at server spin-up with "No solution found when resolving: nemo-gym" (nvbug 6716627).
#
# The pin is exact, so a wheel built at any other version is invisible to it and the job fails
# exactly as before -- hence the version check at the end.
set -euo pipefail

GYM_SRC=${1:?usage: build-gym-wheel.sh <gym-source-dir> <out-dir>}
OUT_DIR=${2:?usage: build-gym-wheel.sh <gym-source-dir> <out-dir>}

# Upstream declares almost no package-data, relying on setuptools-scm's VCS file-finder to sweep
# the configs and READMEs into the wheel. That finder needs git, and there is none here: the RL
# source arrives by `ADD <repo>#<ref>`, which keeps no submodule .git (a submodule's .git is a
# gitdir pointer file, not a directory). Left alone the wheel builds and installs happily while
# quietly missing every YAML, including the sandbox provider configs. Declare the data explicitly
# rather than depend on VCS state this build cannot have.
PKG_DATA_LINE='nemo_gym = ["resources/*.py"]'
if ! grep -qxF "${PKG_DATA_LINE}" "${GYM_SRC}/pyproject.toml"; then
echo "build-gym-wheel: Gym's [tool.setuptools.package-data] entry moved;" \
"re-check this substitution against ${GYM_SRC}/pyproject.toml" >&2
exit 1
fi
# Fully single-quoted, and the brackets/dots/stars escaped: unescaped, `["resources/*.py"]` is a
# BRE character class, which matches nothing here and would edit the file silently not at all.
sed -i 's|^nemo_gym = \["resources/\*\.py"\]$|nemo_gym = ["resources/*.py", "**/*.yaml", "**/*.yml", "**/*.md"]\n"*" = ["**/*.yaml", "**/*.yml", "**/*.json", "**/*.md", "**/*.txt"]|' \
"${GYM_SRC}/pyproject.toml"

# --no-config: uv would otherwise discover the platform workspace's `required-version` pin
# (docker/rl/pyproject.workspace.toml, uv <0.10) and refuse to run as this image's uv 0.11.
uv build --no-config --wheel --out-dir "${OUT_DIR}" "${GYM_SRC}"

# Fail here rather than at spin-up on a GPU node: a wheel at the wrong version, or one missing the
# package data above, installs cleanly and only misbehaves later.
shopt -s nullglob
wheels=("${OUT_DIR}"/nemo_gym-*.whl)
if [[ ${#wheels[@]} -ne 1 ]]; then
echo "build-gym-wheel: expected exactly one wheel, got: ${wheels[*]:-none}" >&2
exit 1
fi
# package_info.py assembles __version__ from MAJOR/MINOR/PATCH/PRE_RELEASE and imports nothing,
# so it can be read without installing the package it describes.
expected="nemo_gym-$(python -c \
'import runpy, sys; print(runpy.run_path(sys.argv[1])["__version__"])' \
"${GYM_SRC}/nemo_gym/package_info.py")-py3-none-any.whl"
if [[ "$(basename "${wheels[0]}")" != "${expected}" ]]; then
echo "build-gym-wheel: built $(basename "${wheels[0]}"), expected ${expected}" >&2
exit 1
fi
probe="nemo_gym/sandbox/providers/opensandbox/configs/opensandbox.yaml"
# Listed into a variable rather than piped into grep: `grep -q` exits at the first match, unzip
# takes SIGPIPE, and `set -o pipefail` then reports the pipeline as failed -- so a file that IS
# present reads as missing.
entries="$(unzip -Z1 "${wheels[0]}")"
if ! grep -qxF "${probe}" <<<"${entries}"; then
echo "build-gym-wheel: ${probe} missing from the wheel; package-data did not take" >&2
exit 1
fi
echo "build-gym-wheel: built $(basename "${wheels[0]}")"
Loading