Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / codegen / spec / project.py: 19%
86 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"""Project a resolved ``TrainerConfig`` into a ``CodegenSpec``.
6The projection is deliberately narrow: it only extracts the fields that can
7change the generated modeling artifact, so unrelated YAML edits do not
8invalidate a bundle signature. It is NOT a second user-facing config source —
9everything here derives from the training YAML.
10"""
11from __future__ import annotations
13import os
14from typing import Any, Optional
16from hyper_parallel.codegen.artifact import (
17 ArtifactLayout,
18 resolve_artifact_layout,
19)
20from hyper_parallel.codegen.spec.types import (
21 CodegenSpec,
22 ParallelDimsSpec,
23 SourceSpec,
24 TargetSpec,
25 TransformSpec,
26)
28# Parallel dims that the generated artifact depends on. Kept as one list so
29# the spec projection and the signature cannot drift apart.
30_PARALLEL_DIM_KEYS = (
31 "dp_size",
32 "tp_size",
33 "cp_size",
34 "ep_size",
35 "pp_size",
36 "sequence_parallel",
37 "loss_parallel",
38)
41def project_codegen_spec(
42 config: Any,
43 yaml_path: str | None = None,
44 *,
45 layout: ArtifactLayout | None = None,
46) -> CodegenSpec:
47 """Project a resolved TrainerConfig into the codegen input spec.
49 ``config`` may be a ``TrainerConfig`` (or any object exposing the same
50 attributes) so the manager can call this without importing trainer.
51 """
52 if layout is None:
53 if yaml_path is None:
54 raise ValueError("project_codegen_spec requires yaml_path or layout")
55 layout = resolve_artifact_layout(yaml_path, model_name=_model_name(config))
56 source_spec = project_source_spec(config)
57 return CodegenSpec(
58 source=source_spec,
59 parallel_dims=project_parallel_dims(config),
60 overrides=project_overrides(config),
61 transform_specs=project_transform_specs(config),
62 target=TargetSpec(
63 enabled=getattr(config, "codegen", False),
64 artifact_dir=layout.artifact_dir,
65 output_modeling_name=os.path.basename(layout.modeling_path),
66 ),
67 )
70def project_source_spec(config: Any) -> SourceSpec:
71 """Project the model identity from the model target / config.
73 The modeling source file is resolved later by ``source.resolver``; here
74 we only record the identity the source resolver will key on. The model
75 Target holds its id under the key the caller actually used — most YAMLs and
76 ``HyperAutoModel.from_pretrained`` use ``pretrained_model_name_or_path`` —
77 so read that first and fall back to ``model_name_or_path``.
78 """
79 model = getattr(config, "model", None)
80 model_name_or_path = getattr(model, "pretrained_model_name_or_path", None)
81 if model_name_or_path is None:
82 model_name_or_path = getattr(model, "model_name_or_path", None)
83 if model_name_or_path is None:
84 # Target configured without a positional model id — the HF path will
85 # derive it from from_pretrained's first arg at load time.
86 model_name_or_path = ""
87 return SourceSpec(
88 model_name_or_path=model_name_or_path,
89 architecture=None,
90 model_type=None,
91 sha256=None,
92 transformers_version=None,
93 )
96def project_parallel_dims(config: Any) -> ParallelDimsSpec:
97 """Project ``accelerator.tp_size/cp_size/ep_size/pp_size`` etc."""
98 accelerator = getattr(config, "accelerator", None)
99 if accelerator is None:
100 return ParallelDimsSpec()
101 kwargs: dict[str, Any] = {}
102 for key in _PARALLEL_DIM_KEYS:
103 if key == "dp_size":
104 kwargs[key] = _project_dp_size(accelerator)
105 else:
106 kwargs[key] = getattr(accelerator, key, ParallelDimsSpec().__dataclass_fields__[key].default)
107 return ParallelDimsSpec(**kwargs)
110def project_overrides(config: Any) -> list[dict[str, Any]]:
111 """Project ``plan_overrides`` to stable canonical dicts.
113 The canonicalization lives here: keep the declarative fields, resolve
114 ``Target`` values to their ``to_dict()`` form, and drop ``None`` fields so
115 the canonical form is stable under YAML default omissions.
116 """
117 entries = getattr(config, "plan_overrides", None) or []
118 return [_canonicalize_override(entry) for entry in entries]
121def project_transform_specs(config: Any) -> TransformSpec:
122 """Project codegen-affecting model-rewrite config.
124 TODO: Populate transform specs when operator, weight, and checkpoint
125 transforms are supported.
126 """
127 model = getattr(config, "model", None)
128 config_overrides = getattr(model, "config_overrides", None)
129 extra = {}
130 if config_overrides:
131 extra["config_overrides"] = _canonicalize_value(config_overrides)
132 return TransformSpec(extra_codegen_options=extra)
135def _model_name(config: Any) -> Optional[str]:
136 """Best-effort model name for artifact file naming."""
137 model = getattr(config, "model", None)
138 path = getattr(model, "_target_", None)
139 if isinstance(path, str):
140 name = path.rsplit(".", 1)[-1]
141 if name.startswith(("AutoModel", "Pretrained", "from_")):
142 return None
143 return name
144 return getattr(config, "model_name", None)
147def _project_dp_size(accelerator: Any) -> int:
148 """Derive runtime DP size from ``WORLD_SIZE`` and non-DP topology."""
149 explicit = getattr(accelerator, "dp_size", None)
150 if explicit:
151 return max(1, int(explicit))
152 world_size = int(os.environ.get("WORLD_SIZE") or os.environ.get("OMPI_COMM_WORLD_SIZE") or 1)
153 tp_size = max(1, int(getattr(accelerator, "tp_size", 1)))
154 cp_size = max(1, int(getattr(accelerator, "cp_size", 1)))
155 pp_size = max(1, int(getattr(accelerator, "pp_size", 1)))
156 non_dp_size = tp_size * cp_size * pp_size
157 if world_size % non_dp_size != 0:
158 raise ValueError(
159 f"world_size {world_size} is not divisible by non-DP size {non_dp_size}"
160 )
161 return max(1, world_size // non_dp_size)
164def _canonicalize_override(entry: Any) -> dict[str, Any]:
165 """Stable dict form of one PlanOverride entry.
167 Resolves Target values instead of stashing raw ``repr()``.
168 ``replace_module`` is projected so changes invalidate the signature.
169 """
170 result: dict[str, Any] = {"match": entry.match}
171 for attr in (
172 "when",
173 "module_type",
174 "exact_type",
175 "inner_target",
176 "region_dispatch",
177 "tp_divide_attrs",
178 ):
179 value = getattr(entry, attr, None)
180 if value is not None:
181 result[attr] = value
182 for attr in ("inner_wrapper", "replace_module", "local_compute_fn"):
183 value = getattr(entry, attr, None)
184 if value is not None:
185 result[attr] = _canonicalize_value(value)
186 for attr in ("params", "in_src", "in_dst", "out_src", "out_dst"):
187 value = getattr(entry, attr, None)
188 if value is not None:
189 result[attr] = _canonicalize_value(value)
190 return result
193def _canonicalize_value(value: Any) -> Any:
194 """Resolve Target objects to dict form; recurse into containers."""
195 if hasattr(value, "to_dict"):
196 return value.to_dict()
197 if isinstance(value, dict):
198 return {key: _canonicalize_value(item) for key, item in value.items()}
199 if isinstance(value, (list, tuple)):
200 return [_canonicalize_value(item) for item in value]
201 return value
204__all__ = [
205 "project_codegen_spec",
206 "project_overrides",
207 "project_parallel_dims",
208 "project_source_spec",
209 "project_transform_specs",
210]