Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / codegen / source / resolver.py: 0%
234 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-10-25 05:45 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2# Licensed under the Apache License, Version 2.0
3# ============================================================================
4"""Locate the real modeling source file for a model id.
6The generated modeling file is produced from the original ``modeling_*.py``
7that the checkpoint actually loads — for native HF models that is the file in
8the installed transformers package, for remote-code models it is the file
9shipped inside the checkpoint. Getting the right file matters: the generated
10bundle must stay importable in the exact environment the model runs in.
12The resolution chain mirrors what HF actually loads (``from_pretrained``):
14 pretrained_model_name_or_path -> AutoConfig -> model_type / architectures
15 -> modeling source file
17and prefers the *same* source HF would load. The deciding input is
18``trust_remote_code`` (forwarded from the YAML ``model:`` block): when it is
19set and the checkpoint declares ``auto_map``, HF loads the checkpoint's own
20modeling file — remote code wins even over an installed transformers module.
21Without it, HF uses the installed module (if any), and remote code is only
22reached as a fallback. ``None`` of the resolution helpers import
23``transformers`` at module scope: the codegen manager runs before the model is
24built, and source resolution must stay functional (degrading) in environments
25without torch/transformers.
26"""
27from __future__ import annotations
29import importlib.util
30import json
31import logging
32import os
33import re
34from dataclasses import dataclass
35from types import SimpleNamespace
36from typing import Any, Optional
38from hyper_parallel.codegen.hash import file_line_count, sha256_file
40logger = logging.getLogger(__name__)
43@dataclass
44class ResolvedSource:
45 """A located modeling source file, ready to be copied into a bundle."""
47 module_name: str
48 file_path: str
49 architecture: Optional[str] = None
50 model_type: Optional[str] = None
51 sha256: Optional[str] = None
52 line_count: Optional[int] = None
54 def to_dict(self) -> dict[str, Any]:
55 """JSON-friendly form for ``CodegenMeta.source``."""
56 return {
57 "module_name": self.module_name,
58 "file_path": self.file_path,
59 "architecture": self.architecture,
60 "model_type": self.model_type,
61 "sha256": self.sha256,
62 "line_count": self.line_count,
63 }
66def resolve_model_source(
67 config: Any, model_id: str, *, hf_config: Any = None,
68) -> Optional[ResolvedSource]:
69 """Resolve the modeling source for a model id, degrading gracefully.
71 Resolves ``model_id -> AutoConfig -> architectures/model_type -> modeling
72 file``, with fallbacks when the environment cannot do the real lookup:
74 1. ``hf_config`` already resolved by the caller -> resolve against it.
75 2. ``AutoConfig.from_pretrained(model_id)`` (real HF config, lazy import;
76 None on any failure — no transformers / no network / bad id).
77 3. ``resolve_source_identity`` — installed module guessed from the
78 basename (covers locally-installed model types when AutoConfig is
79 unavailable).
80 4. Local checkpoint ``config.json`` -> remote code (``SimpleNamespace``
81 mirrors ``AutoConfig`` attribute access).
83 Returns ``None`` when nothing resolved, so the caller decides whether to
84 fail (codegen ``gen`` backend must not run without a source) or degrade
85 (``hf`` backend records the unresolved ``SourceSpec`` projection).
86 """
87 if hf_config is not None:
88 return resolve_hf_source(config, hf_config)
89 cfg = _load_auto_config(model_id, config)
90 if cfg is not None:
91 try:
92 return resolve_hf_source(config, cfg)
93 except FileNotFoundError:
94 # AutoConfig resolved but no modeling file exists anywhere (not
95 # even remote code in the checkpoint) — keep degrading.
96 pass
97 source = resolve_source_identity(model_id)
98 if source is not None:
99 return source
100 return _resolve_source_from_yaml(config, model_id)
103def resolve_hf_source(config: Any, hf_config: Any) -> ResolvedSource:
104 """Resolve the modeling source for a resolved HF config.
106 Priority mirrors what the hyper trainer actually loads, which is plain
107 transformers ``from_pretrained`` semantics (see auto_model._init_model ->
108 _from_pretrained_parent_class, with ``trust_remote_code`` forwarded from the
109 YAML ``model:`` block). The deciding input is ``trust_remote_code``, NOT
110 the mere presence of ``auto_map``:
112 1. ``trust_remote_code`` AND the checkpoint ships remote code (``auto_map``)
113 -> remote code wins, even when an installed module also exists (this is
114 what HF loads). Must resolve to a file, else raise.
115 2. Otherwise an installed transformers module for ``model_type`` -> use it
116 (this is what HF loads when remote code is not trusted).
117 3. Otherwise remote code, if any (a checkpoint whose model_type has no
118 installed module; HF would require trust_remote_code to load it, so we
119 only reach here to still produce a source for generation).
120 """
121 arch = _first_architecture(hf_config)
122 model_type = getattr(hf_config, "model_type", None)
123 has_remote_code = bool(getattr(hf_config, "auto_map", None))
124 trust_remote_code = _trust_remote_code(config)
125 model_path = _model_path_of(config)
127 module_name = _module_for_model_type(model_type)
128 installed_path = import_module_file(module_name) if module_name else None
130 # 1. Trusted remote code is authoritative — HF loads the checkpoint's own
131 # modeling file over any installed module.
132 if has_remote_code and trust_remote_code:
133 remote_path = resolve_remote_code_file(model_path, hf_config)
134 if remote_path is not None:
135 logger.debug("source: resolved to trusted remote code %s", remote_path)
136 return _build_resolved_source(remote_path, "<remote>", arch, model_type)
137 raise FileNotFoundError(
138 f"codegen: trust_remote_code is set and the config for "
139 f"{model_path or model_type!r} declares auto_map "
140 f"({list(getattr(hf_config, 'auto_map', {}) or {})}), but no "
141 "modeling_*.py was found in the checkpoint dir or the loaded "
142 "config's module — remote code is the source HF would load"
143 )
145 # 2. Custom-registered model — what hyper_parallel's CUSTOM/GEN backends
146 # load for a registered architecture. The registry entry is
147 # authoritative over an installed transformers module of the same
148 # model_type: backend resolution prefers the registered class.
149 custom_module = _custom_registered_module(hf_config)
150 if custom_module is not None:
151 custom_path = import_module_file(custom_module)
152 if custom_path is None:
153 raise FileNotFoundError(
154 f"codegen: architecture {arch!r} is registered to module "
155 f"{custom_module!r} but its file could not be located"
156 )
157 logger.debug(
158 "source: resolved architecture=%r to registered module %s",
159 arch, custom_path,
160 )
161 return _build_resolved_source(custom_path, custom_module, arch, model_type)
163 # 3. Installed transformers module — what HF loads when remote code is not
164 # trusted (or not present).
165 if installed_path is not None:
166 logger.debug(
167 "source: resolved model_type=%r to installed module %s",
168 model_type, installed_path,
169 )
170 return _build_resolved_source(installed_path, module_name, arch, model_type)
172 # 4. Remote code without an installed module (HF would need
173 # trust_remote_code, but we can still locate the file for generation).
174 if has_remote_code:
175 remote_path = resolve_remote_code_file(model_path, hf_config)
176 if remote_path is not None:
177 logger.debug(
178 "source: resolved to remote code %s (no installed module)",
179 remote_path,
180 )
181 return _build_resolved_source(remote_path, "<remote>", arch, model_type)
183 raise FileNotFoundError(
184 f"codegen: cannot resolve modeling source for model_type={model_type!r}, "
185 f"architecture={arch!r} (no installed transformers module, no remote code)"
186 )
189def _custom_registered_module(hf_config: Any) -> Optional[str]:
190 """Consult hyper_parallel's custom-model registry for the modeling module.
192 ``MODEL_ARCH_MAPPING`` is filled lazily: a family's ``registration.py``
193 self-registers on first import (directory convention, see
194 :mod:`hyper_parallel.models.registry`). Call ``get_model_adapter`` first
195 so the lookup cannot miss an undiscovered family.
196 """
197 arch = _first_architecture(hf_config)
198 if not arch:
199 return None
200 try:
201 from hyper_parallel.models.registry import ( # pylint: disable=C0415
202 MODEL_ARCH_MAPPING,
203 get_model_adapter,
204 )
205 except ImportError:
206 return None
207 if MODEL_ARCH_MAPPING.get(arch) is None:
208 # Either spelling may be the key that discovers the family (mirrors
209 # get_is_hf_model's identity loop); stop once the architecture is
210 # registered. The architecture goes first: it is the registry key.
211 for identity in (arch, getattr(hf_config, "model_type", "")):
212 if not identity:
213 continue
214 get_model_adapter(identity)
215 if MODEL_ARCH_MAPPING.get(arch) is not None:
216 break
217 entry = MODEL_ARCH_MAPPING.get(arch)
218 return entry[0] if entry is not None else None
221def resolve_source_identity(model_id: str) -> Optional[ResolvedSource]:
222 """Best-effort source resolution keyed only on a model id.
224 Used as a degradation when no ``hf_config`` / ``AutoConfig`` is available.
225 Derives the module name from the model id's basename: model ids usually
226 carry the repo name the transformers module is keyed on (``DeepSeek-V3``
227 -> ``deepseek_v3``). ``None`` when nothing resolves so the caller falls
228 back further (or records the unresolved ``SourceSpec``).
229 """
230 if not model_id:
231 return None
232 name = os.path.basename(str(model_id).rstrip("/\\"))
233 safe = "".join(c if c.isalnum() or c == "_" else "_" for c in name).lower()
234 module_name = _module_for_model_type(safe)
235 if module_name is None:
236 return None
237 file_path = import_module_file(module_name)
238 if file_path is None:
239 return None
240 return ResolvedSource(
241 module_name=module_name,
242 file_path=file_path,
243 architecture=None,
244 model_type=safe,
245 sha256=sha256_file(file_path),
246 line_count=file_line_count(file_path),
247 )
250def resolve_remote_code_file(model_path: str, hf_config: Any) -> Optional[str]:
251 """Locate the remote-code modeling file for a checkpoint.
253 Remote-code checkpoints carry ``modeling_*.py`` + ``configuration_*.py``
254 next to the weights. Two layouts are handled:
256 * A local checkpoint directory — scan it.
257 * A Hub id — the files live in the HF cache, not under ``model_path``. A
258 config loaded with ``trust_remote_code`` was constructed from the cached
259 ``configuration_*.py``, so its class's ``__module__`` file locates the
260 cache dir to scan.
262 Within a directory, resolve in order of increasing specificity:
264 1. ``auto_map["AutoModel..."]`` value, e.g. ``"modeling_foo.FooForCausalLM"``
265 -> ``<dir>/modeling_foo.py``.
266 2. The architecture class name, e.g. ``FooForCausalLM`` -> the file whose
267 basename contains it.
268 3. The single ``modeling_*.py`` when there is exactly one.
270 Returns None when no remote code is reachable.
271 """
272 for directory in _remote_code_dirs(model_path, hf_config):
273 found = _scan_remote_code_dir(directory, hf_config)
274 if found is not None:
275 return found
276 return None
279def _remote_code_dirs(model_path: str, hf_config: Any) -> list[str]:
280 """Directories that may hold this checkpoint's remote code, best first."""
281 dirs: list[str] = []
282 if model_path and os.path.isdir(model_path):
283 dirs.append(model_path)
284 # A remote-code config instance is defined in the cached configuration
285 # module (``.../modules/transformers_modules/<repo>/configuration_x.py``),
286 # which sits next to the cached modeling file. This is how a Hub id
287 # resolves without downloading anything extra.
288 module_name = getattr(type(hf_config), "__module__", "") or ""
289 if module_name and module_name not in ("builtins", "types"):
290 origin = import_module_file(module_name)
291 if origin is None:
292 module = __import__("sys").modules.get(module_name)
293 origin = getattr(module, "__file__", None) if module else None
294 if origin:
295 cached_dir = os.path.dirname(origin)
296 if cached_dir and cached_dir not in dirs and os.path.isdir(cached_dir):
297 dirs.append(cached_dir)
298 return dirs
301def _scan_remote_code_dir(model_path: str, hf_config: Any) -> Optional[str]:
302 """Pick this checkpoint's modeling file out of one directory."""
303 try:
304 candidates = sorted(
305 name for name in os.listdir(model_path)
306 if name.startswith("modeling_") and name.endswith(".py")
307 )
308 except OSError:
309 return None
310 if not candidates:
311 return None
313 auto_map = getattr(hf_config, "auto_map", None) or {}
314 for value in auto_map.values():
315 if not isinstance(value, str) or "." not in value:
316 continue
317 # "modeling_foo.FooForCausalLM" -> "modeling_foo.py". Hub auto_map
318 # values can be "<repo>--modeling_foo.FooForCausalLM"; keep the tail.
319 module_ref = value.split("--")[-1]
320 module_file = module_ref.split(".", 1)[0] + ".py"
321 if module_file in candidates:
322 return os.path.join(model_path, module_file)
324 arch = _first_architecture(hf_config)
325 if arch:
326 for name in candidates:
327 if arch in name:
328 return os.path.join(model_path, name)
329 if len(candidates) == 1:
330 return os.path.join(model_path, candidates[0])
331 return None
334def import_module_file(module_name: str) -> Optional[str]:
335 """Resolve a Python module's ``__file__`` without importing it."""
336 try:
337 spec = importlib.util.find_spec(module_name)
338 except (ImportError, ModuleNotFoundError, ValueError, AttributeError):
339 return None
340 if spec is None or spec.origin is None:
341 return None
342 if not spec.origin.endswith(".py"):
343 return None
344 return spec.origin
347def _load_auto_config(model_id: str, config: Any) -> Optional[Any]:
348 """``AutoConfig.from_pretrained(model_id)`` with lazy import; None on failure.
350 ``trust_remote_code`` is forwarded so remote-code checkpoints can load
351 their own config classes — mirroring what ``HyperAutoModel.from_pretrained``
352 will do at build time. Any failure (no transformers installed, no network,
353 unknown id, remote config raising) returns ``None``; the caller degrades.
354 """
355 if not model_id:
356 return None
357 try:
358 from transformers import AutoConfig
359 except ImportError:
360 return None
361 kwargs: dict[str, Any] = {}
362 if _trust_remote_code(config):
363 kwargs["trust_remote_code"] = True
364 try:
365 return AutoConfig.from_pretrained(model_id, **kwargs)
366 except Exception:
367 return None
370def _resolve_source_from_yaml(config: Any, model_id: str) -> Optional[ResolvedSource]:
371 """Best-effort source resolution from a local checkpoint's ``config.json``.
373 Used when ``AutoConfig`` and the basename guess both came up empty: for a
374 local checkpoint directory, read its ``config.json`` and resolve against
375 it. ``SimpleNamespace`` attribute access mirrors what
376 ``AutoConfig.from_pretrained`` produces, without importing transformers.
377 Returns ``None`` on any failure so the caller degrades.
378 """
379 if not model_id or not os.path.isdir(model_id):
380 return None
381 config_path = os.path.join(model_id, "config.json")
382 if not os.path.isfile(config_path):
383 return None
384 try:
385 with open(config_path, "r", encoding="utf-8") as handle:
386 raw = json.load(handle)
387 hf_config = SimpleNamespace(**raw)
388 return resolve_hf_source(config, hf_config)
389 except (OSError, ValueError, FileNotFoundError, ImportError):
390 return None
393def _module_for_model_type(model_type: Optional[str]) -> Optional[str]:
394 """Map ``config.model_type`` to a transformers ``modeling_*`` module name."""
395 if not model_type:
396 return None
397 safe = "".join(c if c.isalnum() or c == "_" else "_" for c in model_type)
398 return f"transformers.models.{safe}.modeling_{safe}"
401def _first_architecture(hf_config: Any) -> Optional[str]:
402 architectures = getattr(hf_config, "architectures", None) or []
403 return architectures[0] if architectures else None
406def _model_path_of(config: Any) -> str:
407 """Extract the checkpoint dir / HF id from the model target.
409 Same key order as ``project_source_spec``: most YAMLs and
410 ``HyperAutoModel.from_pretrained`` configure the id under
411 ``pretrained_model_name_or_path``, with ``model_name_or_path`` as fallback.
412 """
413 model = getattr(config, "model", None)
414 if model is None:
415 return ""
416 path = getattr(model, "pretrained_model_name_or_path", None)
417 if path is None:
418 path = getattr(model, "model_name_or_path", None)
419 if path is None:
420 path = getattr(model, "_kwargs", {}).get("pretrained_model_name_or_path", "")
421 return path or ""
424def _trust_remote_code(config: Any) -> bool:
425 """Whether this model is loaded with trusted remote code.
427 The truth is the YAML's ``model.trust_remote_code`` — it is what the
428 trainer forwards into ``AutoConfig.from_pretrained`` / the HF
429 ``from_pretrained`` kwargs, and therefore what decides whether HF loads
430 the checkpoint's own modeling file or an installed transformers module.
431 The ``TrainerConfig`` object itself has no such field, so this must read
432 through the model target.
433 """
434 model = getattr(config, "model", None)
435 if model is None:
436 return False
437 value = getattr(model, "trust_remote_code", None)
438 if value is None:
439 value = getattr(model, "_kwargs", {}).get("trust_remote_code", False)
440 return bool(value)
443def _relative_imports_of(path: str) -> list[str]:
444 """Names a file relatively imports from its own package (one level).
446 Mirrors ``transformers.dynamic_module_utils.get_relative_imports`` without
447 importing transformers (source resolution must stay dependency-free). Only
448 single-dot relatives are considered: a sibling ``from .name import x`` /
449 ``import .name`` resolves within the same package directory, which is what
450 a ``<remote>`` modeling source ships. Multi-dot relatives (``..x``) do not
451 resolve against a flat remote code dir and are ignored.
452 """
453 try:
454 with open(path, "r", encoding="utf-8", errors="replace") as handle:
455 content = handle.read()
456 except OSError:
457 return []
458 names = re.findall(r"(?m)^\s*import\s+\.([A-Za-z_][A-Za-z0-9_]*)\s*$", content)
459 names += re.findall(r"(?m)^\s*from\s+\.([A-Za-z_][A-Za-z0-9_]*)\s+import", content)
460 return sorted(set(names))
463def resolve_remote_siblings(source: ResolvedSource) -> dict[str, str]:
464 """Transitive closure of a ``<remote>`` source's package siblings.
466 A trust-remote-code modeling file is compiled as the top-level module of a
467 synthetic package (loader.import_generated_module), so its single-dot
468 relative imports must resolve against *siblings in the same directory*.
469 ``source.module_name == "<remote>"`` marks exactly this case; for an
470 installed transformers module the source uses absolute imports and this
471 returns empty.
473 Returns ``{basename: absolute_path}`` for every sibling reachable through
474 the source's relative-import closure, e.g. a ``configuration_deepseek.py``
475 that ``modeling_deepseek.py`` imports (and any module *it* relatively
476 imports). The caller copies these into the bundle so the artifact is
477 self-contained; the loader registers them under the synthetic package.
478 """
479 if isinstance(source, dict):
480 module_name = source.get("module_name")
481 file_path = source.get("file_path")
482 else:
483 module_name = getattr(source, "module_name", None)
484 file_path = getattr(source, "file_path", None)
485 if module_name != "<remote>" or not file_path:
486 return {}
487 base_dir = os.path.dirname(os.path.abspath(file_path))
488 seen: dict[str, str] = {}
489 queue: list[str] = [os.path.abspath(file_path)]
490 while queue:
491 module_file = queue.pop(0)
492 for name in _relative_imports_of(module_file):
493 sibling = os.path.join(base_dir, f"{name}.py")
494 if not os.path.isfile(sibling):
495 continue
496 if sibling in seen:
497 continue
498 seen[sibling] = sibling
499 queue.append(sibling)
500 return seen
503def _build_resolved_source(
504 file_path: str, module_name: str, arch: Optional[str], model_type: Optional[str],
505) -> ResolvedSource:
506 return ResolvedSource(
507 module_name=module_name,
508 file_path=file_path,
509 architecture=arch,
510 model_type=model_type,
511 sha256=sha256_file(file_path),
512 line_count=file_line_count(file_path),
513 )
516__all__ = [
517 "ResolvedSource",
518 "import_module_file",
519 "resolve_hf_source",
520 "resolve_model_source",
521 "resolve_remote_code_file",
522 "resolve_remote_siblings",
523 "resolve_source_identity",
524]