Skip to content

Commit 6fe7e49

Browse files
committed
test(recipes): validate all recipe factories
Signed-off-by: yaoyu-33 <yaoyu.094@gmail.com>
1 parent 4c0c54c commit 6fe7e49

3 files changed

Lines changed: 355 additions & 1 deletion

File tree

tests/unit_tests/recipes/recipe_test_utils.py

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717
from __future__ import annotations
1818

1919
import importlib
20+
import inspect
21+
import pkgutil
22+
import socket
23+
import sys
2024
from collections.abc import Callable, Iterator
21-
from types import ModuleType
25+
from types import ModuleType, SimpleNamespace
2226

2327
import pytest
2428

@@ -59,3 +63,170 @@ def patch_recipe_module_global(
5963
if hasattr(owner, name) and id(owner) not in patched_ids:
6064
monkeypatch.setattr(owner, name, value)
6165
patched_ids.add(id(owner))
66+
67+
68+
def discover_recipe_factories(
69+
package: ModuleType,
70+
*,
71+
exclude_module_prefixes: tuple[str, ...] = (),
72+
) -> tuple[Callable[..., object], ...]:
73+
"""Import and return every public recipe factory defined below ``package``.
74+
75+
Compatibility modules often re-export the same callable under a second
76+
name. Restricting discovery to functions owned by each leaf module keeps
77+
the construction matrix focused on unique implementations.
78+
"""
79+
package_path = getattr(package, "__path__", None)
80+
if package_path is None:
81+
raise ValueError(f"{package.__name__} is not a package")
82+
83+
factories: dict[tuple[str, str], Callable[..., object]] = {}
84+
prefix = f"{package.__name__}."
85+
for module_info in pkgutil.walk_packages(package_path, prefix=prefix):
86+
if module_info.name.startswith(exclude_module_prefixes):
87+
continue
88+
89+
module = importlib.import_module(module_info.name)
90+
for name, value in vars(module).items():
91+
if name.startswith("_") or not name.endswith("_config") or not inspect.isfunction(value):
92+
continue
93+
if value.__module__ != module.__name__:
94+
continue
95+
factories[(value.__module__, value.__qualname__)] = value
96+
97+
return tuple(factories[key] for key in sorted(factories))
98+
99+
100+
def recipe_factory_key(factory: Callable[..., object]) -> tuple[str, str]:
101+
"""Return a stable identity for a recipe factory or compatibility alias."""
102+
return factory.__module__, factory.__qualname__
103+
104+
105+
def recipe_factory_id(factory: Callable[..., object]) -> str:
106+
"""Return a readable pytest parameter ID for a recipe factory."""
107+
module, name = recipe_factory_key(factory)
108+
return f"{module}:{name}"
109+
110+
111+
def exported_recipe_factory_keys(module: ModuleType) -> set[tuple[str, str]]:
112+
"""Return identities of public ``*_config`` callables exported by a module."""
113+
return {
114+
recipe_factory_key(value)
115+
for name, value in vars(module).items()
116+
if not name.startswith("_") and name.endswith("_config") and callable(value)
117+
}
118+
119+
120+
class _OfflineModelProvider:
121+
"""Mutable provider with the fields HF-backed recipes read before writing."""
122+
123+
def __init__(self) -> None:
124+
self.apply_rope_fusion = False
125+
self.context_parallel_size = 1
126+
self.cross_entropy_fusion_impl = "native"
127+
self.csa_compress_ratios = [0] * 33
128+
self.dsa_indexer_skip_topk_offset = 0
129+
self.dsa_indexer_topk_freq = 1
130+
self.experimental_attention_variant = "dsa"
131+
self.make_vocab_size_divisible_by = 128
132+
self.moe_flex_dispatcher_backend = None
133+
self.mtp_num_layers = 1
134+
self.num_layers = 32
135+
self.num_moe_experts = 8
136+
self.rotary_base = 10000.0
137+
self.rotary_scaling_factor = 1
138+
self.seq_length = 4096
139+
self.tensor_model_parallel_size = 1
140+
self.use_te_rng_tracker = False
141+
self.use_transformer_engine_op_fuser = False
142+
self.vocab_size = 256000
143+
self.yarn_original_max_position_embeddings = 32768
144+
145+
def finalize(self) -> None:
146+
"""Match the model-provider interface without initializing a model."""
147+
148+
149+
class _OfflineAutoBridge:
150+
"""Build a local provider without reading a Hugging Face configuration."""
151+
152+
@classmethod
153+
def from_hf_pretrained(cls, *args: object, **kwargs: object) -> "_OfflineAutoBridge":
154+
del args, kwargs
155+
return cls()
156+
157+
def to_megatron_provider(self, *args: object, **kwargs: object) -> _OfflineModelProvider:
158+
del args, kwargs
159+
return _OfflineModelProvider()
160+
161+
162+
class _OfflineTokenizer:
163+
"""Small tokenizer stand-in used by Gemma vocabulary adjustment helpers."""
164+
165+
def __len__(self) -> int:
166+
return 256000
167+
168+
169+
class _OfflinePreTrainedFlux:
170+
"""Expose the local config surface consumed by ``FluxBridge``."""
171+
172+
def __init__(self, model_name_or_path: object, **kwargs: object) -> None:
173+
del kwargs
174+
self.model_name_or_path = str(model_name_or_path)
175+
self.config = SimpleNamespace(
176+
num_attention_heads=24,
177+
attention_head_dim=128,
178+
in_channels=64,
179+
patch_size=1,
180+
num_layers=19,
181+
num_single_layers=38,
182+
joint_attention_dim=4096,
183+
pooled_projection_dim=768,
184+
guidance_embeds=True,
185+
axes_dims_rope=[16, 56, 56],
186+
ffn_dim=12288,
187+
)
188+
189+
190+
def patch_recipe_construction_dependencies(monkeypatch: pytest.MonkeyPatch) -> None:
191+
"""Keep exhaustive recipe construction deterministic, CPU-only, and offline."""
192+
monkeypatch.setenv("HF_DATASETS_OFFLINE", "1")
193+
monkeypatch.setenv("HF_HUB_OFFLINE", "1")
194+
monkeypatch.setenv("TRANSFORMERS_OFFLINE", "1")
195+
196+
recipe_prefixes = ("megatron.bridge.recipes.", "megatron.bridge.perf_recipes.")
197+
198+
def skip_flex_dispatcher_hardware_probe(*args: object, **kwargs: object) -> None:
199+
del args, kwargs
200+
201+
for module_name, module in tuple(sys.modules.items()):
202+
if module is None or not module_name.startswith(recipe_prefixes):
203+
continue
204+
if hasattr(module, "AutoBridge"):
205+
monkeypatch.setattr(module, "AutoBridge", _OfflineAutoBridge)
206+
if hasattr(module, "apply_flex_dispatcher_backend"):
207+
monkeypatch.setattr(module, "apply_flex_dispatcher_backend", skip_flex_dispatcher_hardware_probe)
208+
209+
flux_recipe_module = importlib.import_module("megatron.bridge.recipes.flux.h100.flux")
210+
monkeypatch.setattr(flux_recipe_module, "PreTrainedFlux", _OfflinePreTrainedFlux)
211+
212+
deepseek_v4_recipe_module = importlib.import_module("megatron.bridge.recipes.deepseek.h100.deepseek_v4")
213+
monkeypatch.setattr(deepseek_v4_recipe_module, "deepseek_v4_supports_blackwell_fused_kernels", lambda: False)
214+
215+
from transformers import AutoTokenizer
216+
217+
def load_offline_tokenizer(*args: object, **kwargs: object) -> _OfflineTokenizer:
218+
del args, kwargs
219+
return _OfflineTokenizer()
220+
221+
monkeypatch.setattr(
222+
AutoTokenizer,
223+
"from_pretrained",
224+
staticmethod(load_offline_tokenizer),
225+
)
226+
227+
def reject_network(*args: object, **kwargs: object) -> None:
228+
del args, kwargs
229+
raise AssertionError("recipe construction attempted network access")
230+
231+
monkeypatch.setattr(socket, "create_connection", reject_network)
232+
monkeypatch.setattr(socket.socket, "connect", reject_network)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Offline construction tests for every public performance recipe factory."""
16+
17+
import importlib
18+
import inspect
19+
import pkgutil
20+
from collections.abc import Callable
21+
22+
import pytest
23+
24+
from megatron.bridge.training.config import ConfigContainer
25+
from tests.unit_tests.recipes.recipe_test_utils import (
26+
discover_recipe_factories,
27+
exported_recipe_factory_keys,
28+
patch_recipe_construction_dependencies,
29+
recipe_factory_id,
30+
recipe_factory_key,
31+
)
32+
33+
34+
pytestmark = pytest.mark.unit
35+
36+
_PERF_RECIPES_PACKAGE = importlib.import_module("megatron.bridge.perf_recipes")
37+
_PERF_RECIPE_FACTORIES = discover_recipe_factories(_PERF_RECIPES_PACKAGE)
38+
_PERF_RECIPE_FAMILY_PACKAGES = tuple(
39+
importlib.import_module(module_info.name)
40+
for module_info in pkgutil.iter_modules(
41+
_PERF_RECIPES_PACKAGE.__path__,
42+
prefix=f"{_PERF_RECIPES_PACKAGE.__name__}.",
43+
)
44+
if module_info.ispkg
45+
)
46+
_PERF_RECIPE_FAMILY_PACKAGES_BY_NAME = {package.__name__: package for package in _PERF_RECIPE_FAMILY_PACKAGES}
47+
48+
49+
@pytest.fixture(autouse=True)
50+
def _keep_recipe_construction_offline(monkeypatch: pytest.MonkeyPatch) -> None:
51+
patch_recipe_construction_dependencies(monkeypatch)
52+
53+
54+
def test_all_perf_recipe_factories_are_exported() -> None:
55+
"""Every canonical factory remains available from its public family package."""
56+
canonical = {recipe_factory_key(factory) for factory in _PERF_RECIPE_FACTORIES}
57+
exported = set().union(*(exported_recipe_factory_keys(package) for package in _PERF_RECIPE_FAMILY_PACKAGES))
58+
assert exported == canonical
59+
for factory in _PERF_RECIPE_FACTORIES:
60+
family_name = factory.__module__.split(".", maxsplit=4)[3]
61+
family_package = _PERF_RECIPE_FAMILY_PACKAGES_BY_NAME[f"{_PERF_RECIPES_PACKAGE.__name__}.{family_name}"]
62+
assert getattr(family_package, factory.__name__) is factory
63+
64+
65+
@pytest.mark.parametrize("recipe_factory", _PERF_RECIPE_FACTORIES, ids=recipe_factory_id)
66+
def test_perf_recipe_factory_builds_config(recipe_factory: Callable[..., object]) -> None:
67+
"""Every performance recipe can be built without GPU or network access."""
68+
inspect.signature(recipe_factory).bind()
69+
70+
cfg = recipe_factory()
71+
72+
assert isinstance(cfg, ConfigContainer)
73+
for section in (
74+
"model",
75+
"train",
76+
"optimizer",
77+
"scheduler",
78+
"dataset",
79+
"logger",
80+
"tokenizer",
81+
"checkpoint",
82+
"rng",
83+
"ddp",
84+
"dist",
85+
):
86+
assert getattr(cfg, section) is not None
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
# Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Offline construction tests for every public library recipe factory."""
16+
17+
import importlib
18+
import inspect
19+
from collections.abc import Callable
20+
21+
import pytest
22+
23+
from megatron.bridge.training.config import ConfigContainer
24+
from tests.unit_tests.recipes.recipe_test_utils import (
25+
discover_recipe_factories,
26+
exported_recipe_factory_keys,
27+
patch_recipe_construction_dependencies,
28+
recipe_factory_id,
29+
recipe_factory_key,
30+
)
31+
32+
33+
pytestmark = pytest.mark.unit
34+
35+
_RECIPES_PACKAGE = importlib.import_module("megatron.bridge.recipes")
36+
_RECIPE_FACTORIES = discover_recipe_factories(
37+
_RECIPES_PACKAGE,
38+
exclude_module_prefixes=("megatron.bridge.recipes.utils",),
39+
)
40+
_UNSUPPORTED_FACTORY_KEYS = {
41+
(
42+
"megatron.bridge.recipes.qwen.h100.qwen3_next",
43+
"qwen3_next_80b_a3b_peft_1gpu_h100_bf16_config",
44+
),
45+
}
46+
_RUNNABLE_RECIPE_FACTORIES = tuple(
47+
factory for factory in _RECIPE_FACTORIES if recipe_factory_key(factory) not in _UNSUPPORTED_FACTORY_KEYS
48+
)
49+
_UNSUPPORTED_RECIPE_FACTORIES = tuple(
50+
factory for factory in _RECIPE_FACTORIES if recipe_factory_key(factory) in _UNSUPPORTED_FACTORY_KEYS
51+
)
52+
53+
54+
@pytest.fixture(autouse=True)
55+
def _keep_recipe_construction_offline(monkeypatch: pytest.MonkeyPatch) -> None:
56+
patch_recipe_construction_dependencies(monkeypatch)
57+
58+
59+
def test_all_recipe_factories_are_exported() -> None:
60+
"""Every canonical factory remains available from the public recipe package."""
61+
canonical = {recipe_factory_key(factory) for factory in _RECIPE_FACTORIES}
62+
assert exported_recipe_factory_keys(_RECIPES_PACKAGE) == canonical
63+
for factory in _RECIPE_FACTORIES:
64+
assert getattr(_RECIPES_PACKAGE, factory.__name__) is factory
65+
66+
67+
@pytest.mark.parametrize("recipe_factory", _RUNNABLE_RECIPE_FACTORIES, ids=recipe_factory_id)
68+
def test_recipe_factory_builds_config(recipe_factory: Callable[..., object]) -> None:
69+
"""Every supported recipe can be called with defaults without GPU or network access."""
70+
inspect.signature(recipe_factory).bind()
71+
72+
cfg = recipe_factory()
73+
74+
assert isinstance(cfg, ConfigContainer)
75+
for section in (
76+
"model",
77+
"train",
78+
"optimizer",
79+
"scheduler",
80+
"dataset",
81+
"logger",
82+
"tokenizer",
83+
"checkpoint",
84+
"rng",
85+
"ddp",
86+
"dist",
87+
):
88+
assert getattr(cfg, section) is not None
89+
90+
91+
@pytest.mark.parametrize("recipe_factory", _UNSUPPORTED_RECIPE_FACTORIES, ids=recipe_factory_id)
92+
def test_intentionally_unsupported_recipe_has_clear_error(recipe_factory: Callable[..., object]) -> None:
93+
"""Keep the documented Qwen3-Next PEFT limitation explicit in the exhaustive matrix."""
94+
inspect.signature(recipe_factory).bind()
95+
96+
with pytest.raises(NotImplementedError, match="PEFT is not currently supported for Qwen3-Next"):
97+
recipe_factory()

0 commit comments

Comments
 (0)