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. |
|
Keep MessageQueue’s remote socket in the reserved band with bind retries. |
|
Load explicit RADIO LayerScale weights and initialize folded weights. |
|
Accept transformers-native RADIO weight names in vLLM’s RADIO loader. |
|
Port checkpoint-backed Super Omni final vision LayerNorm to vLLM 0.29. |
|
Restore the vLLM 0.24 decoder boundary for GLM DSA models. |
|
Fire the routed-experts capture hook on the monolithic fused-MoE path. |
|
Let monolithic MoE kernels without in-kernel capture use the router hook. |
|
Compute NemotronH logits with an fp32 LM head (MiniMax-M1-style). |
|
Temporarily expose nested ModelOpt MoE amax buffers to vLLM’s loader. |
|
Apply interpreter-independent vLLM source-compat patches. |
|
Data#
API#
- nemo_rl.models.generation.vllm.patches.VLLM_NEMOTRON_H_FP32_LM_HEAD_ENV_VAR#
‘NRL_VLLM_FP32_LM_HEAD’
- 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.vLLM 0.29 fixes the race upstream (vllm-project/vllm#53666, #50969): the rank-0 actor now binds the TCPStore itself, on a kernel-assigned port, and holds that socket (
self._dist_init_store = store) untilinit_process_groupreuses it, so there is no probe/bind window for the MessageQueue to land in. That is not the TOCTOU pattern the reserved band guards against (the port is never released between selection and use), and_select_tcpstore_portno longer exists to patch. When that upstream marker is present this function logs at info level and leaves the file alone.Returns without raising when neither form is found, 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#
Keep MessageQueue’s remote socket in the reserved band with bind retries.
vLLM 0.28 binds port zero directly, avoiding the old probe/bind race but ignoring
VLLM_PORT. Restore reserved-band selection with retries so engine sockets do not consume the ephemeral ports used by other services. A probe alone releases its socket before ZMQ binds, leaving 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.
RadioVisionEncoderLayerscales each residual branch byls1/ls2(... + self.attn(...) * self.ls1), butRadioModel.load_weightsdiscards those entries for legacyradio_model.*checkpoints, and folded checkpoints ship no LayerScale tensors at all. NeMo-RL forcesload_format=dummybecause weights arrive by refit, so anything the loader does not populate keeps its random initialization: the vision tower then multiplies every residual branch by noise. The failure is silent – unchanged step time, a collapsed reward, and a large train/token_mult_prob_error.Applied as two INDEPENDENT edits rather than one block replacement, each anchored on text that is stable across loader variants. Both anchors are verified against stock vLLM 0.25.1 and 0.29.0. Custom vLLM builds that reshape the surrounding loader body (for example by gating the legacy branch or adding a native-name mapping branch) still take the required edit, where matching the whole body as one snippet would silently no-op.
Refit calls
load_weightsonce per size-limited batch, so names loaded by any earlier call are remembered on the module and never re-filled.
- nemo_rl.models.generation.vllm.patches._patch_vllm_radio_native_loader(logger) None#
Accept transformers-native RADIO weight names in vLLM’s RADIO loader.
vLLM’s
RadioModel.load_weightsonly understands the legacy timm-style layout (radio_model.model.blocks.N.attn.qkvwith a fused qkv). Nemotron 3.5 Super VL ships transformers-native remote code, so the Automodel policy holdsencoder.layer.N.attention.attention.{query,key,value}andlayer_scale{1,2}.lambda1parameters. Its per-tensor refit path cannot fuse the three attention tensors back into oneattn.qkv, so those names reach vLLM unchanged (Automodel main) or under the legacy block prefix (Automodel r0.6.0:radio_model.model.blocks.N.attention.attention.query). Stock vLLM silently drops both forms and keeps generating with stale vision weights once the vision tower is trained.Port the native branch of the
super_vl_rl_v0.25.1fork loader (TomerBN-Nvidia/vllm commit 436aefb8): map native embedding and encoder names onto vLLM’s keys and write q/k/v as shards of the fusedQKVParallelLinear. Legacy names keep the stock code path, so the LayerScale loader patch and its anchors are unaffected.
- nemo_rl.models.generation.vllm.patches._patch_vllm_radio_final_layernorm(logger) None#
Port checkpoint-backed Super Omni final vision LayerNorm to vLLM 0.29.
The layer runs in FP32 before pixel shuffle/projection. Both affine parameters must load before activation, including dummy startup + refit. Supports stock vLLM 0.29 source or this patch’s complete output. Custom forks, including the already-fixed serving fork below, are not recognized; unexpected source layouts fail before writing rather than being skipped. References: RL #4036 and TomerBN-Nvidia/vllm commit 10908b9f.
- 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._patch_vllm_moe_routed_experts_capture(
- logger,
- *,
- required: bool = False,
Fire the routed-experts capture hook on the monolithic fused-MoE path.
RoutedExpertsCapturer(used by router replay / R3) is driven by thecapture_fnthat only fires insideBaseRouter._select_experts. ButMoERunner._apply_quant_methodcallsselect_expertsonly on the modular kernel branch; monolithic kernels (e.g. the FlashInfer TRT-LLM NVFP4-per-token fused MoE) compute top-k routing internally viaforward_monolithicand never call it. The capture buffer therefore stays zero, and the returnedrouted_expertsare all-zero -> Megatron’s router replay sees duplicate expert ids and dies with “Split sizes doesn’t match total dim 0” in the MoE all_to_all during get_logprobs.This inserts an explicit
select_expertscall on the monolithic branch, guarded bycapture_fn is not Noneso it only runs during rollout when routing capture is active (no cost otherwise).
- nemo_rl.models.generation.vllm.patches._patch_vllm_routed_experts_capture_router_fallback(
- logger,
- *,
- required: bool = False,
Let monolithic MoE kernels without in-kernel capture use the router hook.
vLLM 0.29 moved the routed-experts binding into
routed_experts_capturer.bind_routed_experts_capturer. For a monolithic kernel it requiresfused_experts.supports_routing_replay_capture()and binds the capture function to that experts object (the kernel then writesrouting_replay_outitself); any other monolithic kernel is rejected withValueError. Two things make the object binding unusable for NeMo-RL’s NVFP4 per-token method: the kernel is rebuilt on every refit, so the bound capture function is dropped after the first weight update and the returned routes go back to all-zero; and the per-token kernel is the one FlashInfer launch that has not been validated with a replay buffer attached. Kernels that report no in-kernel capture (seenvfp4_pertoken.host_captured_experts_cls) therefore fall back torouter.set_capture_fn— the hook that_patch_vllm_moe_routed_experts_capturefires on the monolithic branch and the path vLLM 0.26 used for every monolithic kernel.
- nemo_rl.models.generation.vllm.patches._patch_vllm_nemotron_h_fp32_lm_head(logger) bool#
Compute NemotronH logits with an fp32 LM head (MiniMax-M1-style).
bf16 rounding of the logits GEMM output is the dominant contributor to generation/training logprob mismatch (train/token_mult_prob_error). With this patch the sampled-token logprobs come from fp32 logits, matching a trainer that enables megatron_cfg.fp32_lm_head.
This must be a source patch (not a monkeypatch): the model executes in vLLM’s EngineCore worker subprocesses, which import vllm independently of this process. The patched code is opt-in at runtime via an internal NRL_VLLM_FP32_LM_HEAD=1 environment variable set from policy.generation.vllm_cfg.fp32_lm_head. When enabled, the live ParallelLMHead keeps its original parameter dtype and quantization config; only the projection path casts hidden states, weights, and optional bias to fp32 at runtime.
- nemo_rl.models.generation.vllm.patches.modelopt_moe_amax_aliases(
- model: torch.nn.Module,
Temporarily expose nested ModelOpt MoE amax buffers to vLLM’s loader.
Verified against vLLM 0.26.0:
RoutedExperts.load_weightsmaps expert amax keys to names such asw13_input_quantizer._amax, then resolves them with onegetattr. This refit path sends ModelOpt buffers through that loader, which offers no hook for resolving nested target names.Nemotron-H reached this loader after vLLM removed the inner
NemotronHModel.load_weightsin 0.26.0. Its old flat parameter lookup accepted dotted keys exposed by our buffer-to-parameter adapter: https://github.com/vllm-project/vllm/commit/c233d90aa826df072872df47b201450059be8e71Aliases reference the original tensors without registering additional buffers or state-dict entries. Existing attributes belong to the caller or an outer context and are preserved. Only aliases created here are removed, including when setup or weight loading raises.
- Parameters:
model – ModelOpt model whose MoE quantizer buffers will be refitted.
- 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,
- nemotron_h_fp32_lm_head: bool | None = None,
- require_moe_routed_experts_capture: bool = False,