Source code for nemo_rl.distributed.actor_environments

# Copyright (c) 2026, NVIDIA CORPORATION.  All rights reserved.
#
# 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.
"""Which uv extras each Ray actor needs. The single source of truth.

Two readers:

* ``nemo_rl.distributed.ray_actor_environment_registry`` imports
  ``ACTOR_ENVIRONMENTS`` and turns it into ``py_executable`` strings at runtime.
* ``docker/Dockerfile`` runs this file **as a script** to list the venvs it must
  pre-build, so the image ships one venv per actor.

DO NOT IMPORT ANYTHING FROM ``nemo_rl`` HERE, AND KEEP IT STDLIB-ONLY.
The Dockerfile runs this from the dependency layer, where ``pyproject.toml``,
``uv.lock`` and a handful of standalone files exist, but the rest of the
``nemo_rl`` package does not (see the ``COPY`` directives in
``docker/Dockerfile``). Running it as a script rather than importing it is what
keeps ``nemo_rl/__init__.py`` -- which is copied in, and does real work at import
-- from executing there. An import added here breaks the image build in its most
expensive layer.
``tests/unit/distributed/test_actor_environments.py`` enforces this.
"""

# Keeps the annotations below from being evaluated at runtime, so this module also
# runs under interpreters older than the one the image pins.
from __future__ import annotations

import sys

# Actor fully-qualified name -> the uv extras its virtual environment needs.
# ``None`` means the actor runs on the driver's interpreter and gets no venv.
# Every extra must exist in ``[project.optional-dependencies]`` of pyproject.toml.
ACTOR_ENVIRONMENTS: dict[str, list[str] | None] = {
    # vLLM workers always get vllm + nemo_gym. Token capture (token_capture.enabled)
    # imports nemo_gym inside the worker, and worker venvs are cached by actor class
    # name -- a venv prebuilt with plain "vllm" is reused as-is, so the extras have to
    # be fixed here rather than swapped in at runtime.
    "nemo_rl.models.generation.vllm.vllm_worker.VllmGenerationWorker": [
        "vllm",
        "nemo_gym",
    ],
    "nemo_rl.models.generation.vllm.vllm_worker_async.VllmAsyncGenerationWorker": [
        "vllm",
        "nemo_gym",
    ],
    "nemo_rl.models.generation.sglang.sglang_worker.SGLangGenerationWorker": ["sglang"],
    "nemo_rl.models.generation.dynamo.dynamo_worker.DynamoVllmWorker": None,
    "nemo_rl.models.policy.workers.dtensor_policy_worker.DTensorPolicyWorker": ["fsdp"],
    "nemo_rl.models.policy.workers.dtensor_policy_worker_v2.DTensorPolicyWorkerV2": [
        "automodel"
    ],
    "nemo_rl.models.value.workers.dtensor_value_worker_v2.DTensorValueWorkerV2": [
        "automodel"
    ],
    "nemo_rl.models.policy.workers.megatron_policy_worker.MegatronPolicyWorker": [
        "mcore"
    ],
    "nemo_rl.models.value.workers.megatron_value_worker.MegatronValueWorker": ["mcore"],
    "nemo_rl.data.energon.sft_worker.SFTMegatronPolicyWorker": ["mcore"],
    "nemo_rl.models.generation.trtllm.trtllm_worker_async.TrtllmAsyncGenerationWorker": [
        "trtllm"
    ],
    "nemo_rl.environments.math_environment.MathEnvironment": None,
    "nemo_rl.environments.math_environment.MathMultiRewardEnvironment": None,
    "nemo_rl.environments.vlm_environment.VLMEnvironment": None,
    "nemo_rl.environments.code_environment.CodeEnvironment": None,
    "nemo_rl.environments.reward_model_environment.RewardModelEnvironment": None,
    "nemo_rl.environments.code_jaccard_environment.CodeJaccardEnvironment": None,
    "nemo_rl.environments.games.sliding_puzzle.SlidingPuzzleEnv": None,
    # AsyncTrajectoryCollector needs the vLLM environment to handle exceptions
    # from VllmGenerationWorker.
    "nemo_rl.algorithms.async_utils.AsyncTrajectoryCollector": ["vllm"],
    # ReplayBuffer needs the vLLM environment to handle trajectory data from
    # VllmGenerationWorker.
    "nemo_rl.algorithms.async_utils.ReplayBuffer": ["vllm"],
    # SyncRolloutActor doesn't import vllm directly -- policy_generation is a Ray
    # actor handle. The vLLM env is needed because (1) transfer_queue is bundled
    # into the vLLM venv (and the policy training venvs), and the actor writes
    # flattened tensors to TQ via dp_client.put_samples; (2) same-node colocation
    # with VllmGenerationWorker avoids duplicate venv caches.
    "nemo_rl.experience.sync_rollout_actor.SyncRolloutActor": ["vllm"],
    # Captured rollout finalization imports Gym's staging/rebuild package.
    "nemo_rl.experience.rollout_reassembler_actor.RolloutReassemblerActor": [
        "nemo_gym"
    ],
    "nemo_rl.environments.tools.retriever.RAGEnvironment": None,
    "nemo_rl.environments.nemo_gym.NemoGym": ["nemo_gym"],
    # ModelOpt quantization-aware workers
    "nemo_rl.modelopt.models.generation.vllm_quant_worker.VllmQuantGenerationWorker": [
        "modelopt",
        "vllm",
    ],
    "nemo_rl.modelopt.models.generation.vllm_quant_worker.VllmQuantAsyncGenerationWorker": [
        "modelopt",
        "vllm",
    ],
    "nemo_rl.modelopt.models.policy.workers.dtensor_quant_policy_worker.DTensorQuantPolicyWorker": [
        "modelopt",
        "automodel",
    ],
    "nemo_rl.modelopt.models.policy.workers.dtensor_quant_policy_worker_v2.DTensorQuantPolicyWorkerV2": [
        "modelopt",
        "automodel",
    ],
    "nemo_rl.modelopt.models.policy.workers.megatron_quant_policy_worker.MegatronQuantPolicyWorker": [
        "modelopt",
        "mcore",
    ],
}


[docs] def _build_stage(extras: list[str]) -> str: """Which image layer can finish this venv. The tensorrt_llm wheel is only built in the TRT-LLM layer, so those venvs get their third-party packages in two steps; everything else finishes in the dependency layer. """ return "trtllm" if "trtllm" in extras else "deps"
[docs] def main(argv: list[str]) -> int: r"""Print "<actor FQN>\t<stage>\t<uv sync extra flags>" for uv-managed actors. Usage: actor_environments.py [<stage>] [<skip extra> ...] ``<stage>`` is "deps" or "trtllm" (omit for all). Any extras listed after it are skipped, which is how the Dockerfile honors SKIP_VLLM_BUILD and friends. Filtering on the declared extras -- rather than on a substring of the actor name -- is what makes SKIP_VLLM_BUILD also skip actors like AsyncTrajectoryCollector, whose name contains no "vllm". """ stage = argv[1] if len(argv) > 1 and argv[1] != "all" else None if stage not in (None, "deps", "trtllm"): print(f"unknown stage: {stage!r}", file=sys.stderr) return 2 skip = set(argv[2:]) unknown = skip - {e for extras in ACTOR_ENVIRONMENTS.values() for e in extras or ()} if unknown: print(f"unknown extras: {sorted(unknown)}", file=sys.stderr) return 2 for actor_fqn, extras in sorted(ACTOR_ENVIRONMENTS.items()): if extras is None or skip & set(extras): continue actor_stage = _build_stage(extras) if stage is not None and actor_stage != stage: continue flags = " ".join(f"--extra {extra}" for extra in extras) print(actor_fqn, actor_stage, flags, sep="\t") return 0
if __name__ == "__main__": raise SystemExit(main(sys.argv))