Repository navigation
Conversation
Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
…-main-20261015 Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com> # Conflicts: # nemo/collections/speechlm2/models/salm_automodel.py # nemo/collections/speechlm2/parts/hf_hub.py
Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
|
/ok to test 1a996c5 |
| from nemo.utils import logging, logging_mode | ||
|
|
||
| if TYPE_CHECKING: | ||
| from nemo_automodel.components.utils.parameter_config import ParameterConfig |
Set parameter_config on the bare SALMAutomodel used by the MTP tests, and compare checkpoint round-trips against CPU copies of the source tensors, since other speechlm2 modules make CUDA the default device on GPU runners. Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
|
/ok to test a8165e7 |
…ed link Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
|
[🤖]: Hi @vmendelev 👋, We wanted to let you know that a CICD pipeline for this PR just finished successfully. So it might be time to merge this PR or get some approvals. |
|
/ok to test 8a1b217 |
|
[🤖]: Hi @vmendelev 👋, We wanted to let you know that a CICD pipeline for this PR just finished successfully. So it might be time to merge this PR or get some approvals. |
| 2. **W&B Keys**: Update Weights & Biases API keys in configuration files | ||
| 3. **Batch Size Tuning**: Adjust batch size based on your GPU memory and model size | ||
|
|
||
| Refit destination diagnostics |
There was a problem hiding this comment.
This section is too much of an implementation detail IMO, I'd skip it
There was a problem hiding this comment.
Removed the section. The opt-in check is now described in the refit_verification.py module docstring instead.
Done in 4fd4c20.
| parameter surface, training/resume commands, and canonical HF export. It supports | ||
| compatible full SpeechLM checkpoints with or without a bundled CTC timestamp head. | ||
|
|
||
| For CTC-bearing checkpoints, Speech registers the head as frozen state and retains |
There was a problem hiding this comment.
This paragraph seems unnecessary
| shared speech encoder can still affect timestamp quality even though the head is | ||
| frozen; timestamp accuracy requires a separate evaluation. | ||
|
|
||
| Use ``examples/speechlm2/prepare_rl_export_config.py --seed <full-hf-seed> --output <config.json>`` |
There was a problem hiding this comment.
It meant the Hugging Face model directory the GSPO run started from. The command now uses --seed <hf-model-dir> and says that directory contains config.json, llm_backbone/ and the tokenizer.
Done in 4fd4c20.
| dtype: str = "bfloat16" | ||
|
|
||
| # Native NeMo-RL uses flat DCP tensors under weights/model. | ||
| checkpoint_format: str = "lightning" |
There was a problem hiding this comment.
The default format's name should be "safetensors", Lightning's DCP is the input not the output
There was a problem hiding this comment.
checkpoint_format is the format of the input ckpt_path: lightning, or the nemo_rl DCP directory that NeMo RL saves. The output is always HF safetensors. I kept the name and rewrote the comment to say this.
Done in 4fd4c20.
| save_llm_backbone_config(model, cfg.output_dir) | ||
| load_checkpoint(model, cfg.ckpt_path, cfg.checkpoint_format) | ||
| if cfg.checkpoint_format == "nemo_rl": | ||
| # The canonical saver preserves intrinsically-FP32 Mamba/router |
| # prevent_freeze_params pattern is set for configure_optimizers. | ||
| ensure_lora_trainable(self) | ||
|
|
||
| if device_mesh is None: |
There was a problem hiding this comment.
are you sure that skipping device_mesh is None condition here is not breaking anything?
There was a problem hiding this comment.
Yes. Without parameter_config, the no-mesh path behaves as before:
- Perception is still not cast: the cast now runs only with a device mesh or a
parameter_config. - Perception activation checkpointing is still applied. It moved after the LoRA setup, which only touches the LLM, so the order doesn't matter.
- The same early return (
maybe_load_pretrained_modelsand the MTP training mode) follows.
The early return moved below the parameter_config block so that single-process construction, such as to_hf.py export, also applies the selected parameter dtypes before activation-checkpoint wrapping. The unchanged path is exercised by the shared fixture in tests/collections/speechlm2/test_salm_automodel.py, which calls configure_model() without a mesh.
| *, | ||
| require_complete: bool, | ||
| ) -> set[str]: | ||
| """Validate all supplied state before mutation and optionally require full coverage. |
There was a problem hiding this comment.
I don't understand this docstring
There was a problem hiding this comment.
Rewrote it: the loader checks every name and shape before copying anything. The first load must cover every perception tensor; later RL weight updates may send only some of them. The function now lives in weight_loading.py (see the merge thread).
Done in 4fd4c20.
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """Opt-in, bounded verification of received refit weights after normal loading. |
There was a problem hiding this comment.
Can this docstring explain what do you mean by "Refit"
There was a problem hiding this comment.
The docstring now starts with the definition: in NeMo RL, a refit copies the trainer's updated weights into the vLLM generation engine after each training step. It also describes how to enable the check, which used to be in the docs.
Done in 4fd4c20.
| @@ -0,0 +1,114 @@ | |||
| # SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |||
There was a problem hiding this comment.
Can we merge perception_weight_loading.py into this file?
There was a problem hiding this comment.
Merged: load_perception_state is now in weight_loading.py, and perception_weight_loading.py is deleted.
Done in 4fd4c20.
| from pkgutil import extend_path | ||
|
|
||
| # Speech and telemetry distributions install distinct modules under nemo.*. | ||
| __path__ = extend_path(__path__, __name__) |
There was a problem hiding this comment.
NeMo RL depends on nemo-lens for telemetry, which installs nemo.lens into the same nemo package. The GSPO guide installs Speech in editable mode, so without extend_path the nemo package resolves only to the Speech checkout and import nemo.lens fails. extend_path lets Python find the other installed nemo.* modules and changes nothing otherwise. tests/test_namespace_package.py covers it. I rewrote the comment to say this.
Done in 4fd4c20.
|
/review |
svcnvidia-nemo-ci
left a comment
There was a problem hiding this comment.
Reviewed all 22 changed files and traced the affected checkpoint, precision, export, and serving-loader paths. Five correctness or validation issues need changes; details and proposed acceptance checks are inline.
The added tests cover useful isolated round trips, but do not exercise the new explicit parameter-policy path or the identified native-export cases. Documentation was also checked against the implementation.
This was a static review: no tests, GPU jobs, or distributed export/refit runs were executed. Suggested patches require author verification.
Note: formal approval is not available for this request, so this review is posted as a comment only.
Review generated by codex. This was a full review. Request a full review with /review reset=true.
| # The LLM is already in the target dtype (loaded via torch_dtype=dtype). | ||
| # FSDP2 requires uniform parameter dtype, so we cast all parameters. | ||
| if dtype != torch.float32: | ||
| if dtype != torch.float32 and (device_mesh is not None or self.parameter_config is not None): |
There was a problem hiding this comment.
[P2] Apply the explicit parameter dtype policy before any blanket cast
Failure: With torch_dtype=bfloat16 and a ParameterConfig selecting FP32 trainable perception parameters, this casts those parameters to BF16 before apply_parameter_config casts them back to FP32. For example, a pristine weight of 0.1234567 becomes approximately 0.12353515625, stored as FP32. This affects initialization from pretrained ASR weights, which setup_speech_encoder has already loaded; without a subsequent full-model checkpoint load, the precision loss is permanent for that model instance.
Correction: Let the explicit policy cast parameters directly from their pristine values. Preserve the existing buffer conversion separately. A suggested replacement for this cast block is:
if dtype != torch.float32:
if self.parameter_config is None:
if device_mesh is not None:
self.perception.to(dtype=dtype)
else:
for module in self.perception.modules():
for name, buffer in module._buffers.items():
if buffer is not None and (
buffer.is_floating_point() or buffer.is_complex()
):
module._buffers[name] = buffer.to(dtype=dtype)Keep the following apply_parameter_config call before activation-checkpoint wrapping. The invariant is that explicit FP32 parameters must never pass through BF16 storage first.
Preservation and acceptance: Add a proposed configure_model unit test with a tiny mocked perception stack, a selected FP32 weight initialized to 0.1234567, a frozen BF16-selected parameter, and a floating frontend buffer. Assert exact equality for the selected FP32 value, the declared frozen dtype/trainability, and the existing frontend-buffer cast. Cover mesh and no-mesh construction. Without ParameterConfig, retain the current distributed BF16 cast and the no-mesh behavior that leaves perception storage unchanged. These checks are proposed, not executed.
| # tensors. A blanket model.to(BF16) would round them before saving. | ||
| save_hf_checkpoint(model, consolidate_state_dict(model), cfg) | ||
| else: | ||
| model = model.to(str_to_dtype(cfg.dtype)) |
There was a problem hiding this comment.
[P2] Preserve CTC storage in the single-process Lightning export path
Failure: A Lightning checkpoint containing the newly registered CTC artifact takes this branch unless checkpoint_format=nemo_rl. The whole-model .to(bfloat16) rounds FP32 CTC decoder weights before HFHubMixin._save_pretrained saves them. Thus a CTC weight such as 1.000123 is exported as BF16 1.0, despite the preservation contract. The canonical saver exempts CTC tensors, but this path bypasses it.
Correction: Exclude the artifact from this dtype-only cast while retaining the existing save_pretrained serialization, including its shared-tensor handling. One suggested replacement for model.to(...) is:
artifact = getattr(model, "ctc_timestamp", None)
protected = set() if artifact is None else {
id(t) for t in (*artifact.parameters(), *artifact.buffers())
}
target_dtype = str_to_dtype(cfg.dtype)
def cast_for_export(tensor):
if id(tensor) in protected or not (
tensor.is_floating_point() or tensor.is_complex()
):
return tensor
return tensor.to(dtype=target_dtype)
model = model._apply(cast_for_export)The invariant is that CTC parameters and tokenizer bytes retain their loaded storage and values, while ordinary floating weights still use the requested export dtype.
Acceptance: Add a proposed test through the non-native single-process branch using the existing CTC bundle fixture and dtype=bfloat16. Reload the output and assert exact FP32 CTC values, unchanged uint8 tokenizer bytes, identical CTC metadata, and BF16 ordinary weights. Retain the existing native/distributed saver preservation assertions and no-CTC Lightning behavior. This patch and these checks have not been executed.
| def prepare_native_export_model_config(model_cfg: dict) -> dict: | ||
| """Keep native MTP topology and enable the canonical dtype adapter.""" | ||
| model_cfg = deepcopy(model_cfg) | ||
| model_cfg["retain_native_mtp"] = True |
There was a problem hiding this comment.
[P2] Do not invent an MTP head when exporting a headless native checkpoint
Failure: Native export unconditionally sets retain_native_mtp=True. For a valid SpeechLM checkpoint whose backbone has MTP depth zero and whose SpeechLM MTP block is disabled, configure_model consequently supplies positive-depth MTP overrides. The LLM loader treats the absent head as a request to initialize a fresh one. Strict native loading then rejects the checkpoint for missing llm.mtp.* tensors. Export should retain the actual topology, including the absence of a head.
Correction: Resolve whether a head is expected before overriding the training switch. For example, in the native-format block in main, preserve the original enabled/retention switches and resolve the backbone config before calling this helper:
from transformers import AutoConfig
from nemo.collections.speechlm2.parts.pretrained import _automodel_config_mtp_depth
mtp_requested = bool((model_cfg.get("mtp") or {}).get("enabled", False))
retention_requested = bool(model_cfg.get("retain_native_mtp", False))
backbone_cfg = AutoConfig.from_pretrained(
model_cfg["pretrained_llm"],
trust_remote_code=model_cfg.get("trust_remote_code", False),
)
model_cfg = prepare_native_export_model_config(model_cfg)
model_cfg["retain_native_mtp"] = (
mtp_requested or retention_requested or _automodel_config_mtp_depth(backbone_cfg) > 0
)Do not infer optionality solely from missing checkpoint tensors: that would weaken strict coverage for a genuinely expected head.
Preservation and acceptance: Proposed tests should export a depth-zero/disabled native model successfully with no MTP tensors and disabled output metadata; retain a native positive-depth head; retain repeated-head physical depth one plus its logical depth; and retain a recipe-created head when the original SpeechLM MTP switch was enabled even though the base backbone had depth zero. Keep the existing assertion that an explicitly requested retained head which was not constructed fails export. No tests were executed.
| model_cfg = deepcopy(model_cfg) | ||
| model_cfg["retain_native_mtp"] = True | ||
| model_cfg["preserve_parameter_dtypes"] = True | ||
| model_cfg["mtp"] = {**model_cfg.get("mtp", {}), "enabled": False} |
There was a problem hiding this comment.
[P2] Normalize nullable optional configuration blocks before merging
Failure: prepare_native_export_model_config({"mtp": None}) raises TypeError at **None; the same happens with automodel_backend=None. Both are valid disabled/default representations in the model constructor: MTP is normalized with or {}, and a null backend is explicitly skipped. A seed/config with either null block therefore fails before native checkpoint loading.
Correction: Use these replacements in this helper:
model_cfg["mtp"] = {**(model_cfg.get("mtp") or {}), "enabled": False}
model_cfg["automodel_backend"] = {
**(model_cfg.get("automodel_backend") or {}),
"enable_hf_state_dict_adapter": True,
}Apply the same nullable-MTP normalization to the retained-head merge in _hf_export_config, since direct model configurations can retain a head while leaving the optional training block null.
Preservation and acceptance: Parameterize proposed tests over absent, null, and dictionary-valued blocks. Absent/null inputs must prepare successfully; dictionary inputs must retain backend settings and repeated-head options while changing only the intended switches. Preserve the existing assertion that preparation does not mutate the input. With an attached positive-depth retained head and mtp=None, config export must produce enabled MTP metadata rather than a TypeError. These are static deductions and proposed checks, not executed results.
| loaded = load_perception_state( | ||
| self.perception, | ||
| perception_weights, | ||
| require_complete=requires_exact_architecture and not self._perception_weights_initialized, |
There was a problem hiding this comment.
[P2] Initialize the new loader state in the existing FP8 test fixture
Failure: This new unconditional attribute access breaks the existing test_plugin_perception_loader_loads_fp8_checkpoint. That test constructs the model through object.__new__, initializes nn.Module, and sets _uses_pe_encoder=True, but never runs the constructor that initializes _perception_weights_initialized. In a vLLM-enabled test environment, a valid FP8 checkpoint now raises AttributeError before loading instead of satisfying the existing regression assertions.
Correction: Add this line to that fixture before _load_perception_weights(state):
model._perception_weights_initialized = FalseProduction construction already initializes the flag correctly; this does not require weakening the first-load coverage check.
Preservation and acceptance: Run that test in a vLLM-enabled environment and retain its assertions that every perception name is reported loaded, FP8 weight bytes match exactly, and the diarization weights are BF16. Also retain the neighboring invalid-quantization test's ValueError expectation and the updated PE loader test's initial-complete/subsequent-partial behavior. The failure is established by source inspection; tests were not executed here.
…ight loading - Drop the refit diagnostics section and the CTC paragraph from the training guide; explain the --seed directory for the export config. - Clarify that to_hf.py checkpoint_format describes ckpt_path, and drop an unnecessary comment. - Simplify the train_gate comment in SALMAutomodel. - Merge load_perception_state into parts/weight_loading.py with a clearer docstring. - Explain refits and how to enable the check in refit_verification.py. - Explain why nemo/__init__.py extends the package path. Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
…ccept null config blocks Cast perception parameters once through parameter_config instead of rounding them through the training dtype first, keep the CTC timestamp artifact's storage in Lightning export, accept null mtp/automodel_backend blocks in native export, and initialize the perception loader flag in the FP8 vLLM test. Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
|
/ok to test 758f9c6 |
|
[🤖]: Hi @vmendelev 👋, We wanted to let you know that a CICD pipeline for this PR just finished successfully. So it might be time to merge this PR or get some approvals. |
Keep main's strict init_from_checkpoint parameter coverage and add exact coverage for strict from_pretrained. Use main's NeMoSpeechLMConfig.validate. Signed-off-by: Valentin Mendelev <vmendelev@nvidia.com>
|
/ok to test 8896150 |

What does this PR do?
Enable native SpeechLM RL loading, selected refits and canonical HF export while preserving optional frozen CTC timestamp artifacts and the exact configured parameter precision.
Collection: SpeechLM2.
Changelog
checkpoint_format=nemo_rl, intrinsic-dtype preservation and atomic publication. Add a helper that derives the export configuration from the original HF seed.Usage
Prepare the export configuration from the original full HF seed:
Then run the canonical converter in an eight-GPU allocation with the prepared environment:
Validation and GitHub Actions CI
Latest focused CPU run: 77 passed across
test_ctc_timestamp_checkpoint.py,test_weight_loading.py,test_native_export.py,test_to_hf.pyandtest_hf_hub.pyundertests/collections/speechlm2. Run these withpytest --cpu. Branch pre-commit and diff checks passed.Earlier real training/resume/export exercised the CTC preservation implementation on separately pinned image-compatible sources. A fresh current-main distributed EP/vLLM run has not been completed. Frozen CTC preservation does not establish unchanged timestamp accuracy after encoder updates.
Before your PR is "Ready for review"
PR Type: New feature, bugfix, documentation.
Additional Information
Depends on the companion Automodel precision-selection API. The companion NeMo-RL recipe consumes these loading/refit/export APIs. No existing issue is closed.
Companion PRs