Skip to content

Commit a4b4757

Browse files
committed
fix(nemotron-vl): tighten model support scope and adapter placement
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent d80db46 commit a4b4757

11 files changed

Lines changed: 126 additions & 84 deletions

File tree

‎docs/parallelisms.md‎

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,12 +244,6 @@ path performs a host scalar synchronization that is not capture-safe. They must
244244
provide equal per-rank dispatch shapes. Disable CUDA graphs when packed THD token
245245
counts can differ so the recipe can enable safe padding.
246246

247-
Fixed-shape eager workloads may set
248-
`moe_hybridep_assume_equal_dispatch_inputs=true` to preserve an explicitly
249-
disabled padding setting and avoid the safety collective. This is a strict
250-
runtime contract: every rank in the HybridEP group must dispatch the same token
251-
count. Do not enable it for variable-length or dynamically packed batches.
252-
253247
**GPU Architecture Requirements:**
254248

255249
- **DeepEP**: Ampere (SM 8.x), Hopper (SM 9.x), B200, B300

‎examples/model_verification_cards/nemotron-3.5-super-vl-120b-a12b/card.yaml‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ summary: >
99
The recorded results below used one MTP prediction depth. Verification is
1010
being refreshed for the corrected two-depth shared-block configuration;
1111
these historical results do not verify that new training objective.
12+
They also predate the current FP32 optimizer-state and weight-decay defaults
13+
and language-only PEFT adapter targets. Each verified status below applies
14+
only to its recorded historical Bridge commit, not the current recipes.
1215
Real-data BF16 pretraining verification on 64 H100s and 64 GB200s
1316
uses DataComp image-caption samples, natural dropless expert routing, and a
1417
learned vision-pretrained checkpoint; both 120-step runs and their final
@@ -438,7 +441,9 @@ items:
438441
produced finite loss with zero skipped or NaN iterations. The
439442
step-100 adapter contains all 16 distributed shards and two Energon
440443
states and reloaded successfully. This dataset does not contain video
441-
samples.
444+
samples. This historical recipe used unqualified adapter targets that
445+
also matched vision and projector modules; it does not verify the
446+
current language-only adapter placement.
442447
443448
GB200:
444449
status: verified
@@ -478,7 +483,9 @@ items:
478483
produced finite loss with zero skipped or NaN iterations. The
479484
step-100 adapter contains all 16 distributed shards and eight Energon
480485
states and reloaded successfully. This dataset does not contain video
481-
samples.
486+
samples. This historical recipe used unqualified adapter targets that
487+
also matched vision and projector modules; it does not verify the
488+
current language-only adapter placement.
482489
483490
checkpoint_resume:
484491
H100:

‎examples/models/nemotron/nemotron_3/super_vl/README.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ These settings control whether the pretrained base parameters receive
6666
gradients. PEFT adapter placement is a separate concern: `peft.target_modules`
6767
determines which matching modules receive adapters. Freezing a base component
6868
does not, by itself, prevent an unscoped LoRA target name from adding a
69-
trainable adapter inside that component. Decoder-only PEFT recipes should use
70-
language-model-qualified target patterns and verify that no adapter is created
71-
under `vision_model` or `vision_projection`.
69+
trainable adapter inside that component. The H100 and GB200 PEFT recipes use
70+
language-model-qualified target patterns, covering the decoder and MTP while
71+
excluding `vision_model` and `vision_projection`. Custom PEFT configurations
72+
must preserve that qualification if language-only adapters are intended.
7273

7374
## Draft Completion Checklist
7475

7576
- Add verified HF-to-Megatron and Megatron-to-HF conversion commands.
7677
- Add deterministic image and video inference examples.
7778
- Add SFT and PEFT dataset requirements and launch examples.
7879
- Document dynamic-resolution image and temporal-video preprocessing.
79-
- Document and test decoder-only PEFT target patterns.
8080
- Link the completed page from the Nemotron 3 example index.

‎scripts/inference/setup_inference.py‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def _build_parser() -> argparse.ArgumentParser:
8080
execution.add_argument(
8181
"--tasks-per-node",
8282
type=int,
83-
help="Inference tasks per node; defaults to one task per requested GPU.",
83+
help="Inference tasks per node; defaults to one task per GPU.",
8484
)
8585
execution.add_argument("--cpus-per-task", type=int, help="CPUs allocated to each inference task.")
8686
execution.add_argument("--mem", help="Optional Slurm memory request, such as 64G.")
@@ -199,7 +199,6 @@ def _validate_task_args(task_name: str, inference_args: list[str]) -> None:
199199
def _build_executor(args: argparse.Namespace, env_names: list[str], mounts: list[str]) -> object:
200200
"""Build the srun-native NeMo-Run Slurm executor."""
201201
gpu_kwargs = {} if args.no_gpu_resource_request else {"gpus_per_node": args.gpus_per_node}
202-
tasks_per_node = args.tasks_per_node if args.tasks_per_node is not None else args.gpus_per_node
203202
# Slurm's --export=NIL removes the site PATH used to find scontrol and srun
204203
# on clusters where they are not installed in /usr/bin. Always inherit PATH
205204
# for the generated batch script, but expose it to the container only when
@@ -209,7 +208,7 @@ def _build_executor(args: argparse.Namespace, env_names: list[str], mounts: list
209208
account=args.account,
210209
partition=args.partition,
211210
nodes=args.nodes,
212-
ntasks_per_node=tasks_per_node,
211+
ntasks_per_node=args.tasks_per_node or args.gpus_per_node,
213212
cpus_per_task=args.cpus_per_task,
214213
mem=args.mem,
215214
exclusive=True if args.exclusive else None,

‎skills/create-model-verification-card/scripts/verify_hf_inference.py‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -210,10 +210,17 @@ def _load_runtime(args: argparse.Namespace) -> tuple[Any, Any, Any]:
210210

211211
dtype = getattr(torch, args.dtype)
212212
if args.image:
213-
from transformers import AutoModelForImageTextToText, AutoProcessor
213+
from transformers import AutoConfig, AutoModelForImageTextToText, AutoModelForMultimodalLM, AutoProcessor
214214

215+
config = AutoConfig.from_pretrained(args.hf_model, trust_remote_code=args.trust_remote_code)
215216
processor = AutoProcessor.from_pretrained(args.hf_model, trust_remote_code=args.trust_remote_code)
216-
model_cls = AutoModelForImageTextToText
217+
auto_map = getattr(config, "auto_map", None) or {}
218+
# Keep the broader multimodal loader for built-in models, including Omni.
219+
# Some remote-code VLMs register only the image-text auto class.
220+
if "AutoModelForImageTextToText" in auto_map and "AutoModelForMultimodalLM" not in auto_map:
221+
model_cls = AutoModelForImageTextToText
222+
else:
223+
model_cls = AutoModelForMultimodalLM
217224
else:
218225
from transformers import AutoModelForCausalLM, AutoTokenizer
219226

@@ -224,6 +231,8 @@ def _load_runtime(args: argparse.Namespace) -> tuple[Any, Any, Any]:
224231
"trust_remote_code": args.trust_remote_code,
225232
"output_loading_info": True,
226233
}
234+
if args.image:
235+
model_kwargs["config"] = config
227236
if args.device_map:
228237
model_kwargs["device_map"] = args.device_map
229238
model, loading_info = model_cls.from_pretrained(args.hf_model, **model_kwargs)

‎src/megatron/bridge/models/transformer_config.py‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ def _uses_legacy_full_iteration(value: object) -> bool:
123123
or getattr(config, "moe_token_dispatcher_type", None) != "flex"
124124
or getattr(config, "moe_flex_dispatcher_backend", None) != "hybridep"
125125
or cuda_graphs_enabled
126-
or getattr(config, "moe_hybridep_assume_equal_dispatch_inputs", False)
127126
):
128127
return
129128

@@ -156,9 +155,6 @@ class TransformerConfig(MCoreTransformerConfig):
156155
config.finalize()
157156
"""
158157

159-
moe_hybridep_assume_equal_dispatch_inputs: bool = False
160-
"""Keep HybridEP uneven-input padding disabled for fixed-shape eager workloads."""
161-
162158
_NO_COPY_KEYS = {"_pg_collection"}
163159

164160
# Generalized tensor-parallel metadata was added after the frozen MCore dev pin.

‎src/megatron/bridge/recipes/nemotron_omni/h100/nemotron_35_super_vl.py‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ def _nemotron_35_super_vl_base() -> ConfigContainer:
4040
cfg = _sft_common_vlm()
4141
cfg.model = AutoBridge.from_hf_pretrained(
4242
NEMOTRON_35_SUPER_VL_HF_MODEL_ID,
43+
revision=NEMOTRON_35_SUPER_VL_HF_REVISION,
4344
trust_remote_code=True,
4445
).to_megatron_provider(load_weights=False)
4546
cfg.model.seq_length = 4096
@@ -163,7 +164,6 @@ def nemotron_35_super_vl_pretrain_64gpu_h100_bf16_config() -> ConfigContainer:
163164
cfg.model.moe_expert_capacity_factor = None
164165
cfg.model.moe_pad_expert_input_to_capacity = False
165166
cfg.model.moe_hybridep_pad_uneven_dispatch_inputs = False
166-
cfg.model.moe_hybridep_assume_equal_dispatch_inputs = True
167167
cfg.model.moe_flex_dispatcher_num_sms = 32
168168
cfg.ddp.overlap_param_gather = False
169169
cfg.model.moe_hybridep_num_sms = None
@@ -253,7 +253,12 @@ def nemotron_35_super_vl_peft_16gpu_h100_bf16_config(
253253
The Super-VL PEFT configuration.
254254
"""
255255
cfg = _nemotron_35_super_vl_base()
256-
target_modules = ["linear_qkv", "linear_proj", "in_proj", "out_proj", "linear_fc1", "linear_fc2"]
256+
# Base freeze flags do not prevent PEFT from inserting new adapters. Keep
257+
# targets language-qualified so the vision tower and projector stay frozen.
258+
target_modules = [
259+
f"*language_model.*.{name}"
260+
for name in ("linear_qkv", "linear_proj", "in_proj", "out_proj", "linear_fc1", "linear_fc2")
261+
]
257262
cfg.peft = default_peft_config(
258263
peft_scheme,
259264
target_modules=target_modules,

‎tests/unit_tests/models/test_transformer_config.py‎

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,19 +92,6 @@ def test_cuda_graph_config_does_not_require_padding_field(self):
9292

9393
_enable_safe_hybridep_dispatch(cfg, uses_thd=True)
9494

95-
def test_equal_dispatch_input_contract_preserves_disabled_padding(self):
96-
cfg = SimpleNamespace(
97-
moe_token_dispatcher_type="flex",
98-
moe_flex_dispatcher_backend="hybridep",
99-
moe_hybridep_pad_variable_tokens=False,
100-
moe_hybridep_assume_equal_dispatch_inputs=True,
101-
cuda_graph_impl="none",
102-
)
103-
104-
_enable_safe_hybridep_dispatch(cfg)
105-
106-
assert cfg.moe_hybridep_pad_variable_tokens is False
107-
10895

10996
# ---------------------------------------------------------------------------
11097
# _resolve_string_fields

‎tests/unit_tests/recipes/nemotron_omni/test_nemotron_omni_recipes.py‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
)
3030
from megatron.bridge.data.collators.registry import resolve_model_collate
3131
from megatron.bridge.models.nemotron_omni.data.collate_fn import nemotron_omni_expanded_collate_fn
32+
from megatron.bridge.peft.lora_layers import LoRALinear
3233
from megatron.bridge.training.config import ConfigContainer
3334
from megatron.bridge.training.mixed_precision import get_mixed_precision_config
3435
from megatron.bridge.training.optim import _get_scheduler
@@ -526,7 +527,7 @@ def test_super_vl_sft_recipe_reuses_omni_data_and_super_training_stack(fake_proc
526527

527528
assert isinstance(cfg, ConfigContainer)
528529
assert _FakeAutoBridge.hf_path == _TEST_SUPER_VL_HF_ID
529-
assert _FakeAutoBridge.kwargs == {"trust_remote_code": True}
530+
assert _FakeAutoBridge.kwargs == {"revision": _SUPER_VL_HF_REVISION, "trust_remote_code": True}
530531
assert _FakeAutoBridge.load_weights is False
531532

532533
assert isinstance(cfg.dataset, EnergonDatasetConfig)
@@ -593,12 +594,12 @@ def test_super_vl_peft_recipe_uses_native_lora_targets_and_frozen_vision(fake_pr
593594
assert cfg.dataset.pad_to_max_length is True
594595
assert cfg.dataset.do_validation is False
595596
assert cfg.peft.target_modules == [
596-
"linear_qkv",
597-
"linear_proj",
598-
"in_proj",
599-
"out_proj",
600-
"linear_fc1",
601-
"linear_fc2",
597+
"*language_model.*.linear_qkv",
598+
"*language_model.*.linear_proj",
599+
"*language_model.*.in_proj",
600+
"*language_model.*.out_proj",
601+
"*language_model.*.linear_fc1",
602+
"*language_model.*.linear_fc2",
602603
]
603604
assert cfg.peft.dim == 32
604605
assert cfg.peft.alpha == 32
@@ -648,6 +649,46 @@ def test_super_vl_peft_recipe_uses_native_lora_targets_and_frozen_vision(fake_pr
648649
assert cfg.ddp.grad_reduce_in_fp32 is True
649650

650651

652+
@pytest.mark.unit
653+
@pytest.mark.parametrize(
654+
"recipe_func",
655+
[
656+
_super_vl_h100_recipe_module.nemotron_35_super_vl_peft_16gpu_h100_bf16_config,
657+
_super_vl_gb200_recipe_module.nemotron_35_super_vl_peft_16gpu_gb200_bf16_config,
658+
],
659+
ids=["h100", "gb200"],
660+
)
661+
@pytest.mark.parametrize("wrapped", [False, True], ids=["bare", "wrapped"])
662+
def test_super_vl_peft_adapters_only_modify_language_modules(recipe_func, wrapped, fake_processor):
663+
cfg = _build_config(recipe_func, fake_processor)
664+
target_names = ("linear_qkv", "linear_proj", "in_proj", "out_proj", "linear_fc1", "linear_fc2")
665+
666+
def linear_stack():
667+
return torch.nn.ModuleDict({name: torch.nn.Linear(4, 4) for name in target_names})
668+
669+
# Identical leaf names expose accidental unqualified matches in both media
670+
# components, while the language MTP path must still receive adapters.
671+
model = torch.nn.Module()
672+
model.language_model = torch.nn.ModuleDict({"decoder": linear_stack(), "mtp": linear_stack()})
673+
model.vision_model = torch.nn.ModuleDict({"decoder": linear_stack()})
674+
model.vision_projection = torch.nn.ModuleDict({"encoder": linear_stack()})
675+
if wrapped:
676+
wrapper = torch.nn.Module()
677+
wrapper.module = model
678+
model = wrapper
679+
base_parameters = list(model.parameters())
680+
model = cfg.peft(model)
681+
682+
prefix = "module." if wrapped else ""
683+
expected = {f"{prefix}language_model.{stack}.{name}" for stack in ("decoder", "mtp") for name in target_names}
684+
adapted = {name for name, module in model.named_modules() if isinstance(module, LoRALinear)}
685+
assert adapted == expected
686+
assert all(not parameter.requires_grad for parameter in base_parameters)
687+
trainable = {name for name, parameter in model.named_parameters() if parameter.requires_grad}
688+
assert trainable
689+
assert all(name.startswith(f"{prefix}language_model.") and ".adapter." in name for name in trainable)
690+
691+
651692
def test_super_vl_pretrain_recipe_uses_tuned_h100_training_policy(fake_processor):
652693
cfg = _build_config(
653694
_super_vl_h100_recipe_module.nemotron_35_super_vl_pretrain_64gpu_h100_bf16_config,
@@ -682,7 +723,6 @@ def test_super_vl_pretrain_recipe_uses_tuned_h100_training_policy(fake_processor
682723
assert cfg.model.moe_expert_capacity_factor is None
683724
assert cfg.model.moe_pad_expert_input_to_capacity is False
684725
assert cfg.model.moe_hybridep_pad_uneven_dispatch_inputs is False
685-
assert cfg.model.moe_hybridep_assume_equal_dispatch_inputs is True
686726
assert cfg.model.moe_flex_dispatcher_num_sms == 32
687727
assert cfg.model.moe_hybridep_num_sms is None
688728
assert cfg.model.moe_hybridep_num_sms_preprocessing == 108
@@ -774,7 +814,6 @@ def test_super_vl_pretrain_recipe_uses_gb200_nvl72_policy(fake_processor):
774814
assert cfg.model.moe_expert_capacity_factor is None
775815
assert cfg.model.moe_pad_expert_input_to_capacity is False
776816
assert cfg.model.moe_hybridep_pad_uneven_dispatch_inputs is False
777-
assert cfg.model.moe_hybridep_assume_equal_dispatch_inputs is True
778817
assert cfg.model.moe_flex_dispatcher_num_sms == 32
779818
assert cfg.model.moe_hybridep_num_sms == 32
780819
assert cfg.model.moe_permute_fusion_into_hybridep is False
@@ -926,12 +965,12 @@ def test_super_vl_peft_recipe_uses_gb200_support_topology(fake_processor):
926965
assert isinstance(cfg.dataset, EnergonDatasetConfig)
927966
assert cfg.dataset.task_encoder.hf_processor_path == _TEST_SUPER_VL_HF_ID
928967
assert cfg.peft.target_modules == [
929-
"linear_qkv",
930-
"linear_proj",
931-
"in_proj",
932-
"out_proj",
933-
"linear_fc1",
934-
"linear_fc2",
968+
"*language_model.*.linear_qkv",
969+
"*language_model.*.linear_proj",
970+
"*language_model.*.in_proj",
971+
"*language_model.*.out_proj",
972+
"*language_model.*.linear_fc1",
973+
"*language_model.*.linear_fc2",
935974
]
936975
assert cfg.peft.dim == 32
937976
assert cfg.peft.alpha == 32

‎tests/unit_tests/scripts/inference/test_setup_inference.py‎

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -332,35 +332,6 @@ def __init__(self, **kwargs):
332332
assert executor.kwargs["gpus_per_node"] == 4
333333

334334

335-
def test_slurm_executor_can_assign_multiple_gpus_to_one_task(tmp_path, monkeypatch):
336-
module = _load_setup_inference_module()
337-
338-
class _SlurmExecutor:
339-
def __init__(self, **kwargs):
340-
self.kwargs = kwargs
341-
342-
module.run.Packager = lambda: "packager"
343-
module.run.LocalTunnel = lambda **kwargs: types.SimpleNamespace(**kwargs)
344-
module.run.SlurmExecutor = _SlurmExecutor
345-
monkeypatch.setattr(module, "get_nemorun_home", lambda: str(tmp_path))
346-
args, inference_args = module.parse_args(
347-
_launcher_args(
348-
"--gpus-per-node",
349-
"4",
350-
"--tasks-per-node",
351-
"1",
352-
"--device-map",
353-
"balanced_low_0",
354-
)
355-
)
356-
357-
executor = module._build_executor(args, [], [])
358-
359-
assert executor.kwargs["ntasks_per_node"] == 1
360-
assert executor.kwargs["gpus_per_node"] == 4
361-
assert inference_args == ["--device-map", "balanced_low_0"]
362-
363-
364335
def test_slurm_executor_can_request_exclusive_nodes(tmp_path, monkeypatch):
365336
module = _load_setup_inference_module()
366337

0 commit comments

Comments
 (0)