nemo_rl.models.generation.vllm.patches#
Module Contents#
Functions#
Return absolute path to a vLLM file or raise if it cannot be found. |
|
Yield (content, writer) under an exclusive file lock. |
|
Patch vLLM’s Ray executor env propagation and worker runtime_env. |
|
Patch LlamaEagle3 to keep truncated draft lm_head ownership. |
|
Guard vLLM’s NamespaceTool import for openai < 2.25. |
|
Keep RayExecutorV2’s TCPStore port out of the MessageQueue’s scan range. |
|
Make MessageQueue’s remote socket survive losing a port race. |
|
Load explicit RADIO LayerScale weights and initialize folded weights. |
|
Restore the vLLM 0.24 decoder boundary for GLM DSA models. |
|
Apply interpreter-independent vLLM source-compat patches. |
|
API#
- nemo_rl.models.generation.vllm.patches._get_vllm_file(relative_path: str) str#
Return absolute path to a vLLM file or raise if it cannot be found.
The relative_path should be a POSIX-style path under the vllm package root, e.g. “v1/executor/ray_executor.py” or “attention/layer.py”.
- nemo_rl.models.generation.vllm.patches._locked_file_patch(file_path: str)#
Yield (content, writer) under an exclusive file lock.
- nemo_rl.models.generation.vllm.patches._patch_vllm_init_workers_ray(
- py_executable: str,
- extra_env_vars: list[str] | None,
Patch vLLM’s Ray executor env propagation and worker runtime_env.
Pass custom runtime_env in _init_workers_ray call (file patch).
This allows passing custom py_executable to worker initialization.
Forward extra env vars to the Ray workers via vLLM’s additive VLLM_RAY_EXTRA_ENV_VARS_TO_COPY hook (vLLM >= 0.25). NCCL_, HF_, and HUGGING_FACE_* vars are already copied by vLLM’s default prefix list (this includes the NCCL_CUMEM_ENABLE/NCCL_NVLS_ENABLE workaround from https://github.com/NVIDIA-NeMo/RL/pull/898).
.. note:: Step 1 patches the v1 Ray executor, which vLLM 0.25 no longer selects by default:
VLLM_USE_RAY_V2_EXECUTOR_BACKENDflipped from"0"(0.20) to"1"(0.25), soExecutor.get_classreturnsRayExecutorV2for ray-backed engines.RayExecutorV2has no_init_workers_rayat all – it creates workers inline, and its_build_runtime_envnever setspy_executable.The patch is kept because it is still load-bearing when ``VLLM_USE_RAY_V2_EXECUTOR_BACKEND=0`` selects the v1 executor. Under the 0.25 default it is inert, and workers get the right interpreter from Ray's per-field ``runtime_env`` inheritance instead: the parent NeMo-RL actor sets ``py_executable``, and a child created with a ``runtime_env`` that omits it inherits the parent's value. So a ``True`` return means "the anchor is in place", not "this is what put the workers on the right interpreter". The caller logs accordingly.
- Returns:
Whether the v1 runtime_env source patch is in place. The env-var merge in step 2 cannot fail, but step 1 is anchored on a call-site string; if that moves upstream the py_executable injection silently stops happening, so the caller must not report success unconditionally.
- nemo_rl.models.generation.vllm.patches._patch_vllm_llama_eagle3_own_lm_head(logger) None#
Patch LlamaEagle3 to keep truncated draft lm_head ownership.
- nemo_rl.models.generation.vllm.patches._patch_vllm_tool_parser_namespace_tool(logger) None#
Guard vLLM’s NamespaceTool import for openai < 2.25.
vLLM 0.25 imports
openai.types.responses.NamespaceTool(added in openai 2.25.0) at the top oftool_parsers/utils.py, but nemo-gym pinsopenai<=2.7.2and its child server venvs must match the parent’s openai version exactly. NamespaceTool is only used in isinstance checks for Responses-API namespace tools, which cannot be constructed by an openai client that predates the feature, so a never-matching stub is a faithful fallback.
- nemo_rl.models.generation.vllm.patches._patch_vllm_ray_executor_v2_tcpstore_port(logger) None#
Keep RayExecutorV2’s TCPStore port out of the MessageQueue’s scan range.
vLLM 0.25’s
RayExecutorV2._init_executorpicks the torch.distributed TCPStore port with a bind-probe (Step 3) but only binds it much later, in the rank-0 worker’sinit_process_group. In between, Step 4 builds the broadcastMessageQueue; when the engine spans nodes that queue needs a real TCP socket, so it callsget_open_port()and binds and holds the result (shm_broadcast.py:remote_subscribe_port = get_open_port()thenremote_socket.bind(...)). Both searches start atVLLM_PORT, so the queue deterministically takes the very port the probe just released and engine startup dies withEADDRINUSE(DeepSeek-V3 generation TP=32, observed on port 7000). Engines that fit on one node use a shm/ipc socket instead and never allocate a TCP port here, which is why only node-spanning engines are affected.Offsetting the TCPStore search past the queue’s scan range removes the collision while keeping both ports inside the engine’s 100-port window, and therefore below the OS ephemeral floor. That band is deliberate: leaving
VLLM_PORTunset would send vLLM to kernel-assigned ephemeral ports and reintroduce the TOCTOU contention this layout exists to prevent (#2380, #3103).The offset must be applied before the
local_dp_rank is Nonetest, not inside it. vLLM’s own disjoint-window branch below reads as if it only applies to DP engines, butParallelConfig.__post_init__takes the “offline SPMD” path for every engine NeMo-RL builds and assignsdata_parallel_rank_local = envs.VLLM_DP_RANK_LOCAL(0 by default) anddata_parallel_master_port = envs.VLLM_DP_MASTER_PORT(0 by default). So a plain non-DP engine arrives here withlocal_dp_rank=0, notNone: theNonebranch is dead, and the DP branch searches from0 + 100 + 0 * 32 = 100, fails all 32 attempts on the privileged range, and falls through toget_open_port()— straight back toVLLM_PORT. That is exactly the port the MessageQueue takes. See RL-1104.Returns without raising when the snippet is missing, but logs at warning level so a silent no-op is visible in worker logs.
- nemo_rl.models.generation.vllm.patches._patch_vllm_shm_broadcast_bind_retry(logger) None#
Make MessageQueue’s remote socket survive losing a port race.
MessageQueue.__init__picks the port for its remote (TCP) socket withremote_subscribe_port = get_open_port(), which probes a port and releases it, and only binds it with ZMQ several statements later (shm_broadcast.py:self.remote_socket.bind(socket_addr)). The window between the probe and the bind is a TOCTOU race.On vLLM 0.25 that race is lost reliably, not occasionally. Every
RayWorkerProcon a non-driver node takesn_local_reader=0(ray_executor_v2.py::_init_message_queues), so every one of them needs a real TCP port, and they all scan from the sameVLLM_PORT– 7000 for a node-spanning engine._init_message_queuesruns immediately afterinit_device(), whose process-group setup is a collective barrier, so all workers on the node arrive at the probe within microseconds of each other, all see the same port free, and all but one die with::zmq.error.ZMQError: Address already in use (addr='tcp://10.65.1.9:7000')
Workers on the driver node take
n_local_reader=1and use anipc://socket instead, which is why only node-spanning engines are affected – and why no nightly test catches it (none runs an engine whosetensor_parallel_size * pipeline_parallel_sizeexceedscluster.gpus_per_node). See RL-1111.Fix the race at the bind rather than the probe: retry, advancing past the port that was lost. This is safe and terminating because a port a peer already holds with ZMQ is visible to the next
_get_open_portprobe (a plainbind(("", port))on it fails withEADDRINUSE), so each retry makes forward progress.Deliberately keeps the search anchored at
VLLM_PORTinstead of letting vLLM fall back tobind(("", 0)): kernel-assigned ephemeral ports are exactly the TOCTOU contention the reserved sub-ephemeral band exists to prevent (#2380, #3103).Patching the bind (rather than handing each worker a private start port) also covers every other
MessageQueuewith a remote reader – notably the executor’s ownrpc_broadcast_mq– instead of the one call site that happens to be failing today.Returns without raising when the snippet is missing, but logs at warning level so a silent no-op is visible in worker logs.
- nemo_rl.models.generation.vllm.patches._patch_vllm_radio_layerscale_loader(logger) None#
Load explicit RADIO LayerScale weights and initialize folded weights.
vLLM 0.25.1 uses
ls1andls2inRadioVisionEncoderLayerbut skips them inRadioModel.load_weights. Explicit checkpoint values are therefore ignored, while folded checkpoints leave the parameters at dummy initialization. Patch the loader so explicit values are loaded and absent values are initialized to RADIO’s configured identity factor.
- nemo_rl.models.generation.vllm.patches._patch_vllm_glm_decoder_sequence_parallel_moe(logger) None#
Restore the vLLM 0.24 decoder boundary for GLM DSA models.
vLLM 0.25.1 keeps hidden states sequence-parallel across attention and MoE decoder layers when TP, DP, and EP are all enabled. GLM-5.1/5.2 decode diverges on that new path: the first generated token is correct, while subsequent decode-token logprobs collapse. Keep vLLM’s existing MoE-local sequence parallelism, but disable the new decoder-level optimization for
glm_moe_dsaso the MoE gathers its output as it did in vLLM 0.24.The upstream bug and proposed fix are tracked at https://github.com/vllm-project/vllm/issues/50154 and https://github.com/vllm-project/vllm/pull/50155. Remove this patch after upgrading to a vLLM release containing the fix and validating iterative GLM-5.1/5.2 decode with TP, DP, and EP all enabled.
- nemo_rl.models.generation.vllm.patches.ensure_vllm_source_compat() None#
Apply interpreter-independent vLLM source-compat patches.
Safe to call from any process that imports vLLM directly (e.g. the tools/model_diagnostics scripts, which construct
vllm.LLMwithout going through a NeMo-RL generation worker). Must be called BEFORE the firstimport vllmsubmodule that pulls invllm.tool_parsers. Worker processes get this via_apply_vllm_patchesat init.
- nemo_rl.models.generation.vllm.patches._apply_vllm_patches(
- py_executable: str,
- *,
- extra_env_vars: list[str] | None = None,