Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / _op_profiles.py: 95%
133 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#
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"""Op profiles: the layer kinds of each known architecture family, as data.
17Every file under ``op_profiles/`` names a family and declares its layer kinds.
18A kind states how many times one layer runs each op the cost model prices,
19the numbers the ND arch hooks used to assign in code, and two flavours from a
20closed set that say how the layer is shaped:
22- ``attention``: ``full`` (the default) or ``linear``, a gated-DeltaNet layer
23 priced on the model's linear-attention dimensions;
24- ``ffn``: ``dense`` or ``moe``, or absent to keep the model's own
25 feed-forward, for families whose layers do not differ in it.
27A profile with more than one kind may name a ``default``, the kind a layer
28takes when nothing says otherwise; a profile with one kind defaults to it.
30A profile may also state what a model of its family has when its producer
31does not say: under ``run``, the run's byte widths, whether its gradients
32take memory without pipeline parallelism and whether tensor parallelism
33shards the activations between layers (:data:`DEFAULT_RUN` otherwise); under
34``model``, an MLA family's value-head width. The cost model's ``derive``
35reads them.
37A model spec names its profile with ``arch``, and may declare its own counts
38in ``ops`` instead. The cost model reads only what the spec declares. A
39producer that has nothing better than a free-text model name, such as a
40MindFormers ``trainer.model_name``, falls back on :func:`infer_arch`.
41"""
42import functools
43import logging
44import os
45from dataclasses import dataclass, field
46from types import MappingProxyType
47from typing import Any, Dict, Mapping, Optional, Tuple
49import yaml
51from hyper_parallel.auto_parallel._exec_spec import ExecSpec, ExecSpecError
52from hyper_parallel.auto_parallel._model_spec import ModelSpecError, OpCounts
54logger = logging.getLogger(__name__)
56PROFILE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "op_profiles")
57DEFAULT_ARCH = "default"
58# The profile every vision tower is priced with.
59VISION_ARCH = "vision"
61ATTENTION_FLAVOURS = ("full", "linear")
62FFN_FLAVOURS = ("dense", "moe")
64# The run of a family's model when its producer states none of it, and the
65# only execution-spec fields a profile's ``run`` may change: the byte widths
66# of gradients, optimizer states, norm activations and dropout masks, whether
67# gradients take memory without pipeline parallelism, whether tensor
68# parallelism shards the activations between layers, and whether the loss
69# runs on logits sharded over the vocabulary.
70DEFAULT_RUN = MappingProxyType({
71 "grad_bytes": 4,
72 "optimizer_state_bytes": 4,
73 "optimizer_states": 2,
74 "main_param_bytes": 0,
75 "norm_bytes": 4,
76 "dropout_bytes": 0,
77 "grad_accumulation": False,
78 "shard_activations": False,
79 "loss_parallel": True,
80 "reshard_params": False,
81 "deferred_grad_accumulation": False,
82 "overlapped_grad_reduce": False,
83})
85# The model facts a profile's ``model`` may give a model that states none.
86MODEL_DEFAULT_KEYS = ("v_head_dim",)
88# What a model name opens with, with the family it means. The longest that
89# a name opens with wins, so Qwen3.5, under both the Transformers spelling
90# and the release name, is not taken for the Qwen family it opens with too,
91# and the order they are listed in decides nothing.
92_NAME_PREFIXES = {
93 "llama2": "llama2", "mixtral": "mixtral", "t5": "t5", "pangualpha": "pangualpha",
94 "deepseek": "deepseek", "qwen3_5": "qwen3_5", "qwen3.5": "qwen3_5", "qwen": "qwen",
95}
96# The names the default profile was taken for, each said once a process.
97_DEFAULTED_NAMES = set()
100@dataclass(frozen=True)
101class LayerKind:
102 """One kind of layer: the ops it runs and how it is shaped.
104 Attributes:
105 name: The kind's name in its profile, which a layer stack refers to.
106 ops: How many times one layer of this kind runs each op.
107 attention: ``full``, or ``linear`` for a gated-DeltaNet layer.
108 ffn: ``dense``, ``moe``, or ``None`` to keep the model's own.
109 """
111 name: str
112 ops: OpCounts
113 attention: str = "full"
114 ffn: Optional[str] = None
116 def __deepcopy__(self, memo: Dict[int, Any]) -> "LayerKind":
117 """Frozen and made of immutable fields: a deep copy is the record itself."""
118 del memo
119 return self
122@dataclass(frozen=True)
123class OpProfile:
124 """The layer kinds of one architecture family.
126 Attributes:
127 arch: The family, the profile's file name.
128 layer_kinds: Every kind of the family, by name.
129 default: The kind a layer takes when nothing says otherwise, or
130 ``None`` for a family whose stack always states its kinds.
131 run: The run of the family's models where their producer states
132 none, every key of :data:`DEFAULT_RUN`.
133 model: The model facts the family gives a model that states none.
134 """
136 arch: str
137 layer_kinds: Dict[str, LayerKind]
138 default: Optional[str] = None
139 run: Mapping[str, Any] = field(default_factory=lambda: DEFAULT_RUN)
140 model: Mapping[str, int] = field(default_factory=lambda: MappingProxyType({}))
142 @property
143 def kinds(self) -> Dict[str, OpCounts]:
144 """Return each kind's op counts, by kind name."""
145 return {name: kind.ops for name, kind in self.layer_kinds.items()}
147 def counts(self, kind: str) -> OpCounts:
148 """Return one layer kind's counts, or raise naming the kinds there are."""
149 return self.kind(kind).ops
151 def kind(self, name: str) -> LayerKind:
152 """Return one layer kind, or raise naming the kinds there are."""
153 if name not in self.layer_kinds:
154 raise ModelSpecError(
155 f"op profile {self.arch!r} has no layer kind {name!r}; "
156 f"it has {sorted(self.layer_kinds)}"
157 )
158 return self.layer_kinds[name]
161@functools.lru_cache(maxsize=None)
162def known_archs() -> Tuple[str, ...]:
163 """Return the name of every family that has a profile file."""
164 return tuple(sorted(
165 name[:-len(".yaml")] for name in os.listdir(PROFILE_DIR) if name.endswith(".yaml")
166 ))
169def _flavour(value: Any, allowed: Tuple[str, ...], where: str, optional: bool) -> Optional[str]:
170 """Return a flavour value, refusing one outside *allowed*."""
171 if value is None and optional:
172 return None
173 if value not in allowed:
174 raise ModelSpecError(f"{where} must be one of {list(allowed)}, got {value!r}")
175 return str(value)
178def _layer_kind(arch: str, name: str, data: Any) -> LayerKind:
179 """Build one kind of a profile from its ``{ops, attention, ffn}`` mapping."""
180 where = f"{arch}.kinds.{name}"
181 if not isinstance(data, Mapping):
182 raise ModelSpecError(f"{where} must map ops and flavours, got {data!r}")
183 unknown = sorted(set(data) - {"ops", "attention", "ffn"})
184 if unknown:
185 raise ModelSpecError(f"{where} has unknown keys {unknown}; a kind has ops, attention and ffn")
186 return LayerKind(
187 name=str(name),
188 ops=OpCounts.from_dict(data.get("ops"), f"{where}.ops"),
189 attention=_flavour(data.get("attention", "full"), ATTENTION_FLAVOURS, f"{where}.attention", False),
190 ffn=_flavour(data.get("ffn"), FFN_FLAVOURS, f"{where}.ffn", True),
191 )
194def _run_defaults(arch: str, data: Any) -> Mapping[str, Any]:
195 """Return a family's run: :data:`DEFAULT_RUN`, with what its profile states."""
196 stated = {} if data is None else data
197 if not isinstance(stated, Mapping):
198 raise ModelSpecError(f"{arch}.run must map run facts to values, got {data!r}")
199 unknown = sorted(set(stated) - set(DEFAULT_RUN))
200 if unknown:
201 raise ModelSpecError(f"{arch}.run has unknown keys {unknown}; a family states {sorted(DEFAULT_RUN)}")
202 try:
203 spec = ExecSpec.from_dict(stated)
204 except ExecSpecError as exc:
205 raise ModelSpecError(f"{arch}.run: {exc}") from exc
206 return MappingProxyType({**DEFAULT_RUN, **spec.to_dict()})
209def _model_defaults(arch: str, data: Any) -> Mapping[str, int]:
210 """Return the model facts a family gives a model that states none."""
211 stated = {} if data is None else data
212 if not isinstance(stated, Mapping):
213 raise ModelSpecError(f"{arch}.model must map model facts to values, got {data!r}")
214 unknown = sorted(set(stated) - set(MODEL_DEFAULT_KEYS))
215 if unknown:
216 raise ModelSpecError(
217 f"{arch}.model has unknown keys {unknown}; a family states {list(MODEL_DEFAULT_KEYS)}"
218 )
219 for key, value in stated.items():
220 if isinstance(value, bool) or not isinstance(value, int) or value < 1:
221 raise ModelSpecError(f"{arch}.model.{key} must be a positive whole number, got {value!r}")
222 return MappingProxyType(dict(stated))
225@functools.lru_cache(maxsize=None)
226def load_op_profile(arch: str) -> OpProfile:
227 """Read and validate the profile of one family.
229 Raises:
230 ModelSpecError: If no profile has that name, or the file declares no
231 layer kind, an unknown key, an incomplete op vector, an unknown
232 flavour, a default that is not one of its kinds, or a run or
233 model default it cannot state.
234 """
235 if arch not in known_archs():
236 raise ModelSpecError(
237 f"unknown arch {arch!r}; the op profiles are {list(known_archs())}"
238 )
239 with open(os.path.join(PROFILE_DIR, f"{arch}.yaml"), encoding="utf-8") as handle:
240 data = yaml.safe_load(handle) or {}
241 unknown = sorted(set(data) - {"kinds", "default", "run", "model"})
242 if unknown:
243 raise ModelSpecError(f"op profile {arch!r} has unknown keys {unknown}")
244 kinds = data.get("kinds")
245 if not isinstance(kinds, Mapping) or not kinds:
246 raise ModelSpecError(f"op profile {arch!r} declares no layer kind")
247 layer_kinds = {str(name): _layer_kind(arch, name, kind) for name, kind in kinds.items()}
248 default = data.get("default")
249 if default is None and len(layer_kinds) == 1:
250 default = next(iter(layer_kinds))
251 if default is not None and default not in layer_kinds:
252 raise ModelSpecError(
253 f"op profile {arch!r} names default kind {default!r}, but its kinds are {sorted(layer_kinds)}"
254 )
255 return OpProfile(arch, layer_kinds, default, _run_defaults(arch, data.get("run")),
256 _model_defaults(arch, data.get("model")))
259def family_profile(arch: Optional[str]) -> OpProfile:
260 """Return the profile of *arch*, or the default family's when no profile has that name."""
261 return load_op_profile(arch if arch in known_archs() else DEFAULT_ARCH)
264def infer_arch(name: Any) -> str:
265 """Return the family a free-text model name belongs to.
267 The family the lower-cased name opens with, the longest first, else the
268 default profile, said once a name. Matched anywhere in the name, in a
269 fixed order, as the cost model matched names before the family became
270 data, a family listed earlier won over the one the name opens with.
271 Only a producer with no better source should use it; a spec that
272 declares ``arch`` never gets here.
273 """
274 lowered = str(name).lower()
275 for prefix in sorted(_NAME_PREFIXES, key=len, reverse=True):
276 if lowered.startswith(prefix):
277 return _NAME_PREFIXES[prefix]
278 if lowered not in _DEFAULTED_NAMES:
279 # Once: a search parses a config more than once.
280 _DEFAULTED_NAMES.add(lowered)
281 logger.warning(
282 "no op profile matches model %r, pricing it as %r; declare arch to choose one",
283 name, DEFAULT_ARCH,
284 )
285 return DEFAULT_ARCH
288# What a lower-cased model name contains when its attention normalizes each
289# head's queries and keys: the Qwen3 generation, Qwen3.5 and the Qwen3
290# vision-language models included.
291_QK_NORM_NAMES = ("qwen3",)
294def infer_qk_norm(name: Any) -> bool:
295 """Return whether a model named *name* normalizes each head's queries and keys.
297 For a producer whose config does not state it, as a Transformers config
298 does not: the Qwen3 generation does, and no other family the cost model
299 prices does.
300 """
301 lowered = str(name).lower()
302 return any(pattern in lowered for pattern in _QK_NORM_NAMES)
305# What a lower-cased model name contains when its attention projects queries,
306# keys and values with a bias and its output without, which its Transformers
307# config does not state: the Qwen2 generation.
308_QKV_BIAS_NAMES = ("qwen2",)
310# What a lower-cased model name contains when its MoE layer gates its shared
311# expert's output with a weight of its own: Qwen2-MoE, Qwen3.5 and Qwen3-Next.
312_SHARED_EXPERT_GATE_NAMES = ("qwen2_moe", "qwen3_5", "qwen3_next")
315def infer_qkv_bias(name: Any) -> bool:
316 """Return whether a model named *name* biases its query, key and value projections.
318 For a Transformers config that states no ``attention_bias``, as the
319 Qwen2 generation's, whose projections have one.
320 """
321 lowered = str(name).lower()
322 return any(pattern in lowered for pattern in _QKV_BIAS_NAMES)
325def infer_shared_expert_gate(name: Any) -> bool:
326 """Return whether a model named *name* gates its shared expert's output with a weight of its own."""
327 lowered = str(name).lower()
328 return any(pattern in lowered for pattern in _SHARED_EXPERT_GATE_NAMES)
331def resolve_ops(arch: str, ops: Optional[Mapping[str, OpCounts]] = None) -> Dict[str, OpCounts]:
332 """Return the op counts a model is priced with, per layer kind.
334 Declared *ops* replace the profile's, but must name the same layer kinds:
335 the family's hooks decide which layers are of which kind, so counts for a
336 kind they never assign would be dropped without a word.
338 Raises:
339 ModelSpecError: If *arch* has no profile, or *ops* names other kinds.
340 """
341 profile = load_op_profile(arch)
342 if ops is None:
343 return dict(profile.kinds)
344 if set(ops) != set(profile.kinds):
345 raise ModelSpecError(
346 f"ops declares layer kinds {sorted(ops)}, but arch {arch!r} "
347 f"prices {sorted(profile.kinds)}"
348 )
349 return dict(ops)