Skip to content

Commit e101626

Browse files
committed
[test] perf: reduce slow unit test overhead
Shrink real Qwen3VL fixtures while strengthening tensor routing and vision payload assertions. Avoid redundant compilation, MLflow initialization and full-heap collection in focused tests, and reuse immutable command inputs with scoped parser caching. Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent 5f1250a commit e101626

5 files changed

Lines changed: 86 additions & 35 deletions

File tree

‎tests/unit_tests/doc_consistency/test_verification_card_published_commands.py‎

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import shlex
2828
import sys
2929
from collections.abc import Iterator
30+
from functools import cache
3031
from pathlib import Path
3132
from typing import overload
3233

@@ -41,9 +42,11 @@
4142
CONVERSION_ARGUMENTS = REPO_ROOT / "scripts" / "conversion" / "arguments.py"
4243
SETUP_CONVERSION = REPO_ROOT / "scripts" / "conversion" / "setup_conversion.py"
4344
SETUP_INFERENCE = REPO_ROOT / "scripts" / "inference" / "setup_inference.py"
45+
PublishedCommands = tuple[tuple[str, tuple[str, ...]], ...]
4446

4547

46-
def _declared_options(source_path: Path, *, append_only: bool = False, function: str | None = None) -> set[str]:
48+
@cache
49+
def _declared_options(source_path: Path, *, append_only: bool = False, function: str | None = None) -> frozenset[str]:
4750
"""Read parser options, including called argument helpers in Bridge source."""
4851
options: set[str] = set()
4952
tree = ast.parse(source_path.read_text(encoding="utf-8"))
@@ -77,7 +80,15 @@ def _declared_options(source_path: Path, *, append_only: bool = False, function:
7780
if isinstance(argument, ast.Constant) and isinstance(argument.value, str):
7881
if argument.value.startswith("-"):
7982
options.add(argument.value)
80-
return options
83+
return frozenset(options)
84+
85+
86+
@pytest.fixture(autouse=True)
87+
def _clear_declared_options_cache() -> Iterator[None]:
88+
# Reuse parser results within a check, but never across tests that edit source.
89+
_declared_options.cache_clear()
90+
yield
91+
_declared_options.cache_clear()
8192

8293

8394
def _inference_tasks() -> dict[str, Path]:
@@ -128,15 +139,21 @@ def _published_commands() -> Iterator[tuple[str, list[str]]]:
128139
yield f"{card.parent.name}{location}", tokens
129140

130141

142+
@pytest.fixture(scope="module")
143+
def published_commands() -> PublishedCommands:
144+
"""Parse every shipped command once, with immutable argv shared by the checks."""
145+
return tuple((leaf, tuple(tokens)) for leaf, tokens in _published_commands())
146+
147+
131148
@overload
132-
def _flag_value(tokens: list[str], flag: str, default: int) -> int: ...
149+
def _flag_value(tokens: tuple[str, ...], flag: str, default: int) -> int: ...
133150

134151

135152
@overload
136-
def _flag_value(tokens: list[str], flag: str, default: None) -> int | None: ...
153+
def _flag_value(tokens: tuple[str, ...], flag: str, default: None) -> int | None: ...
137154

138155

139-
def _flag_value(tokens: list[str], flag: str, default: int | None) -> int | None:
156+
def _flag_value(tokens: tuple[str, ...], flag: str, default: int | None) -> int | None:
140157
"""Return the last integer value passed to a flag, or the launcher default."""
141158
value = default
142159
for index, token in enumerate(tokens[:-1]):
@@ -145,17 +162,19 @@ def _flag_value(tokens: list[str], flag: str, default: int | None) -> int | None
145162
return value
146163

147164

148-
def _selected_task(tokens: list[str]) -> str:
165+
def _selected_task(tokens: tuple[str, ...]) -> str:
149166
task = "text-generation"
150167
for index, token in enumerate(tokens[:-1]):
151168
if token == "--task":
152169
task = tokens[index + 1]
153170
return task
154171

155172

156-
def test_published_gpu_conversion_commands_decompose_over_the_published_world_size() -> None:
173+
def test_published_gpu_conversion_commands_decompose_over_the_published_world_size(
174+
published_commands: PublishedCommands,
175+
) -> None:
157176
offenders = []
158-
for leaf, tokens in _published_commands():
177+
for leaf, tokens in published_commands:
159178
if "convert.sh" not in tokens[0]:
160179
continue
161180
device = "gpu"
@@ -178,12 +197,12 @@ def test_published_gpu_conversion_commands_decompose_over_the_published_world_si
178197
assert not offenders, "setup_conversion.py refuses these published commands: " + "; ".join(offenders)
179198

180199

181-
def test_published_inference_commands_carry_no_conversion_launcher_flag() -> None:
200+
def test_published_inference_commands_carry_no_conversion_launcher_flag(published_commands: PublishedCommands) -> None:
182201
launcher_options = _declared_options(SETUP_INFERENCE)
183202
conversion_options = _declared_options(CONVERSION_ARGUMENTS) | _declared_options(SETUP_CONVERSION)
184203
tasks = _inference_tasks()
185204
offenders = []
186-
for leaf, tokens in _published_commands():
205+
for leaf, tokens in published_commands:
187206
if "infer.sh" not in tokens[0]:
188207
continue
189208
accepted = launcher_options | _declared_options(tasks[_selected_task(tokens)])
@@ -196,12 +215,12 @@ def test_published_inference_commands_carry_no_conversion_launcher_flag() -> Non
196215
assert not offenders, "these published inference commands die in the container: " + "; ".join(offenders)
197216

198217

199-
def test_published_card_commands_do_not_repeat_a_flag() -> None:
218+
def test_published_card_commands_do_not_repeat_a_flag(published_commands: PublishedCommands) -> None:
200219
repeatable = _declared_options(CONVERSION_ARGUMENTS, append_only=True) | _declared_options(
201220
SETUP_INFERENCE, append_only=True
202221
)
203222
offenders = []
204-
for leaf, tokens in _published_commands():
223+
for leaf, tokens in published_commands:
205224
flags = [token.split("=", 1)[0] for token in tokens[1:] if token.startswith("--")]
206225
repeated = sorted({flag for flag in flags if flags.count(flag) > 1 and flag not in repeatable})
207226
if repeated:
@@ -252,10 +271,7 @@ def test_declared_options_follow_only_called_helpers(tmp_path: Path, monkeypatch
252271
),
253272
],
254273
)
255-
def test_published_command_checks_reject_original_errors(
256-
command: str, check_name: str, message: str, monkeypatch: pytest.MonkeyPatch
257-
) -> None:
274+
def test_published_command_checks_reject_original_errors(command: str, check_name: str, message: str) -> None:
258275
module = sys.modules[__name__]
259-
monkeypatch.setattr(module, "_published_commands", lambda: iter([("regression", shlex.split(command))]))
260276
with pytest.raises(AssertionError, match=message):
261-
getattr(module, check_name)()
277+
getattr(module, check_name)((("regression", tuple(shlex.split(command))),))

‎tests/unit_tests/models/nemotron_omni/test_nemotron_omni_conversion.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,9 @@ def test_nemotron_omni_vision_projection_uses_squared_relu():
581581
values = torch.tensor([-2.0, 0.0, 3.0])
582582

583583
assert vision_projection_config.activation_func is squared_relu
584-
assert torch.equal(vision_projection_config.activation_func(values), torch.tensor([0.0, 0.0, 9.0]))
584+
# This checks configuration and arithmetic; the model test covers compiled execution.
585+
activation = torch.compiler.disable(vision_projection_config.activation_func)
586+
assert torch.equal(activation(values), torch.tensor([0.0, 0.0, 9.0]))
585587

586588

587589
def test_nemotron_omni_providers_use_contract_specific_cpe_defaults():

‎tests/unit_tests/models/qwen_vl/modelling_qwen3_vl/test_model.py‎

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,32 @@ def fake_all_gather(outputs, _local_counts, group):
152152

153153
@pytest.fixture(scope="module")
154154
def hf_config():
155-
"""Create a local HuggingFace config once for all tests."""
156-
config = Qwen3VLMoeConfig()
157-
config.vision_config.out_hidden_size = config.text_config.hidden_size
158-
return config
155+
"""Keep real vision, deepstack and language paths with small, compatible shapes."""
156+
return Qwen3VLMoeConfig(
157+
text_config={
158+
"num_hidden_layers": 2,
159+
"hidden_size": 128,
160+
"intermediate_size": 256,
161+
"num_attention_heads": 4,
162+
"num_key_value_heads": 2,
163+
"head_dim": 32,
164+
"vocab_size": 256,
165+
"max_position_embeddings": 256,
166+
"rope_parameters": {"rope_type": "default", "rope_theta": 5000000.0, "mrope_section": [4, 6, 6]},
167+
},
168+
vision_config={
169+
"depth": 2,
170+
"hidden_size": 128,
171+
"intermediate_size": 256,
172+
"num_heads": 4,
173+
"out_hidden_size": 128,
174+
"deepstack_visual_indexes": [0, 1],
175+
},
176+
image_token_id=240,
177+
video_token_id=241,
178+
vision_start_token_id=242,
179+
vision_end_token_id=243,
180+
)
159181

160182

161183
class TestQwen3VLModel:
@@ -229,8 +251,7 @@ def get_vision_transformer_config(hf_config):
229251
def get_language_transformer_config(hf_config):
230252
"""Create a language transformer config for testing.
231253
232-
Uses actual Qwen3-VL-30B-A3B model sizes to ensure compatibility
233-
with the vision model output (2048 hidden size).
254+
Match the tiny HuggingFace fixture's language and vision dimensions.
234255
235256
Args:
236257
hf_config: HuggingFace config object.
@@ -239,16 +260,18 @@ def get_language_transformer_config(hf_config):
239260
Qwen3VLTransformerConfig: Configuration for the language model.
240261
"""
241262
return Qwen3VLTransformerConfig(
242-
# Use actual model dimensions from HF config
243-
num_layers=4, # Reduced for testing (actual: hf_config.text_config.num_hidden_layers)
244-
hidden_size=hf_config.text_config.hidden_size, # Must match vision output: 2048
263+
num_layers=hf_config.text_config.num_hidden_layers,
264+
hidden_size=hf_config.text_config.hidden_size,
245265
num_attention_heads=hf_config.text_config.num_attention_heads,
246266
num_query_groups=hf_config.text_config.num_key_value_heads,
247267
kv_channels=hf_config.text_config.hidden_size // hf_config.text_config.num_attention_heads,
248268
ffn_hidden_size=hf_config.text_config.intermediate_size,
249269
# Qwen3-VL specific
250270
vocab_size=hf_config.text_config.vocab_size,
251271
language_max_sequence_length=hf_config.text_config.max_position_embeddings,
272+
image_token_id=hf_config.image_token_id,
273+
video_token_id=hf_config.video_token_id,
274+
vision_start_token_id=hf_config.vision_start_token_id,
252275
# Vision parameters
253276
patch_size=hf_config.vision_config.patch_size,
254277
temporal_patch_size=hf_config.vision_config.temporal_patch_size,
@@ -513,17 +536,17 @@ def test_set_input_tensor(self, hf_config):
513536

514537
if torch.cuda.is_available():
515538
model_pre.to("cuda")
516-
test_tensor = torch.randn(2, 4, 2048).cuda()
539+
test_tensor = torch.randn(2, 4, language_transformer_config.hidden_size).cuda()
517540
else:
518-
test_tensor = torch.randn(2, 4, 2048)
541+
test_tensor = torch.randn(2, 4, language_transformer_config.hidden_size)
519542

520543
# Test with single tensor (not a list)
521544
model_pre.set_input_tensor(test_tensor)
522-
assert model_pre.encoder_hidden_state is not None
545+
assert model_pre.encoder_hidden_state is test_tensor
523546

524547
# Test with list of tensors
525548
model_pre.set_input_tensor([test_tensor])
526-
assert model_pre.encoder_hidden_state is not None
549+
assert model_pre.encoder_hidden_state is test_tensor
527550

528551
# Test with pre_process=False
529552
model_no_pre = Qwen3VLModel(
@@ -543,7 +566,7 @@ def test_set_input_tensor(self, hf_config):
543566

544567
# This should set the input tensor on the language model instead
545568
model_no_pre.set_input_tensor([test_tensor])
546-
# No assertion here as it sets internal state
569+
assert model_no_pre.language_model.decoder.input_tensor is test_tensor
547570

548571
@staticmethod
549572
def _attach_dist_train(language_transformer_config, vision_to_llm_dp_ratio: int = 1) -> None:
@@ -651,6 +674,10 @@ def test_forward_dist_train_decoder_only(self, hf_config):
651674
)
652675
vision_payload = enc_out["vision_module"].detach()
653676
decoder.set_input_tensor([{"vision_module": vision_payload}])
677+
assert decoder.vision_embeds is not None
678+
assert len(decoder.deepstack_feature_lists) == len(vision_transformer_config.deepstack_visual_indexes)
679+
received_payload = torch.cat((*decoder.deepstack_feature_lists, decoder.vision_embeds), dim=0)
680+
assert torch.equal(received_payload, vision_payload.flatten(0, 1))
654681
out = decoder(
655682
input_ids=batch["input_ids"],
656683
attention_mask=batch["attention_mask"],

‎tests/unit_tests/peft/test_recompute.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
"""Unit tests for PEFT-specific recompute helpers."""
1616

17-
import gc
1817
import weakref
1918
from types import SimpleNamespace
2019

@@ -116,7 +115,7 @@ def test_recompute_patch_registry_tracks_model_lifetime(monkeypatch):
116115
maybe_enable_recompute_inputs_grad(first_model)
117116
first_model_ref = weakref.ref(first_model)
118117
del first_model
119-
gc.collect()
118+
# The model has no reference cycle; the registry must not keep it alive.
120119
assert first_model_ref() is None
121120

122121
second_model = DummyModel(DummyHybridStack)

‎tests/unit_tests/training/test_train.py‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1158,7 +1158,10 @@ def test_no_checkpoint_when_disabled(self, mock_check_nvrx, mock_barrier_log, mo
11581158
@patch("megatron.bridge.training.train.save_checkpoint_and_time")
11591159
@patch("megatron.bridge.training.train.barrier_and_log")
11601160
@patch("megatron.bridge.training.train.check_nvrx_straggler_detection")
1161-
def test_multiple_exit_conditions_signal_wins(self, mock_check_nvrx, mock_barrier_log, mock_save_checkpoint):
1161+
@patch("megatron.bridge.training.train.end_active_mlflow_run")
1162+
def test_multiple_exit_conditions_signal_wins(
1163+
self, mock_end_mlflow, mock_check_nvrx, mock_barrier_log, mock_save_checkpoint
1164+
):
11621165
"""Test that signal handler exit takes precedence over other exit conditions."""
11631166
mock_check_nvrx.return_value = True # Straggler detection would also trigger
11641167

@@ -1181,6 +1184,10 @@ def test_multiple_exit_conditions_signal_wins(self, mock_check_nvrx, mock_barrie
11811184
# Verify signal handler message was logged, not straggler message
11821185
mock_barrier_log.assert_called_once_with("exiting program after receiving SIGTERM.")
11831186

1187+
mock_end_mlflow.assert_called_once_with("KILLED")
1188+
mock_check_nvrx.assert_not_called()
1189+
mock_save_checkpoint.assert_called_once()
1190+
11841191
@patch("megatron.bridge.training.train.save_checkpoint_and_time")
11851192
@patch("megatron.bridge.training.train.barrier_and_log")
11861193
@patch("megatron.bridge.training.train.check_nvrx_straggler_detection")

0 commit comments

Comments
 (0)