|
17 | 17 | from __future__ import annotations |
18 | 18 |
|
19 | 19 | import importlib |
| 20 | +import inspect |
| 21 | +import pkgutil |
| 22 | +import socket |
| 23 | +import sys |
20 | 24 | from collections.abc import Callable, Iterator |
21 | | -from types import ModuleType |
| 25 | +from types import ModuleType, SimpleNamespace |
22 | 26 |
|
23 | 27 | import pytest |
24 | 28 |
|
@@ -59,3 +63,170 @@ def patch_recipe_module_global( |
59 | 63 | if hasattr(owner, name) and id(owner) not in patched_ids: |
60 | 64 | monkeypatch.setattr(owner, name, value) |
61 | 65 | 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) |
0 commit comments