Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / trainer / config / parallelism.py: 91%
394 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Parallel topology, activation-checkpoint and plan-override configuration.
17Split from ``auto_models/trainer/config.py`` in stage 7 (05 §15.2.5).
18``PlanOverride`` stays the YAML DTO that desugars to
19``auto_models.distributed.recipe_spec.ModuleShardingSpec``.
20"""
21# pylint: disable=unused-import
23import importlib
24import logging
25from collections.abc import Mapping
26from dataclasses import dataclass, field
27from types import MappingProxyType
28from typing import Any, List, Literal, Optional, Union
30from torch import nn # pylint: disable=forbidden-backend-import
32from hyper_parallel.components.quantization.config import (
33 LowPrecisionConfig,
34 LowPrecisionDtypeScheme,
35)
36from hyper_parallel.distributed.encoder_parallel.config import EncoderParallelConfig
37from hyper_parallel.models.replacement import (
38 MethodReplacementSpec,
39 ModuleReplacementFactory,
40 ModuleReplacementSpec,
41 ModuleReplacementRequest,
42 method_replacement,
43 module_replacement,
44)
46from hyper_parallel.components.modules.family import get_module_family
47from hyper_parallel.trainer.config.replacement import ReplacementTarget
48from hyper_parallel.trainer.config.target import Target, _serialize_config_value
50logger = logging.getLogger(__name__)
53@dataclass
54class AcceleratorConfig:
55 """Parallel topology and target-selected strategy configurations."""
57 tp_size: int = 1
58 cp_size: int = 1
59 ep_size: int = 1
60 pp_size: int = 1
61 sequence_parallel: bool = False
62 loss_parallel: bool = False
63 encoder_parallel: EncoderParallelConfig = field(default_factory=EncoderParallelConfig)
66@dataclass
67class ActivationCheckpointSelection:
68 """Select adapter-declared recompute regions and exact layer coverage.
70 ``layer_count`` selects the contiguous prefix ``[0, layer_count)`` from
71 the flattened layer-container order. ``layer_indices`` selects explicit
72 zero-based entries from that same order.
74 ``model_adapter_safe_regions`` wraps the outermost module matching the
75 adapter's ``region_patterns`` inside every selected layer and keeps every
76 ``exclude_patterns`` descendant outside the recompute region. Which shape that
77 produces is the model's declaration, not the Trainer's: leaf patterns give
78 one region per submodule, a layer pattern gives one region per layer.
79 """
81 source: Literal["default", "model_adapter_safe_regions"] = "default"
82 layer_count: Optional[int] = None
83 layer_indices: Optional[List[int]] = None
85 def __post_init__(self) -> None:
86 """Validate the mutually exclusive layer-count and layer-index selectors."""
87 self._validate_selector_choice()
88 self._validate_selector_values()
90 def _validate_selector_choice(self) -> None:
91 """Require one supported source and its matching selector form."""
92 if self.source not in ("default", "model_adapter_safe_regions"):
93 raise ValueError(
94 "activation_checkpoint.selection.source must be default or "
95 "model_adapter_safe_regions"
96 )
97 selectors = (self.layer_count is not None, self.layer_indices is not None)
98 if self.source == "default" and any(selectors):
99 raise ValueError(
100 "activation_checkpoint.selection.layer_count and layer_indices "
101 "are only valid when "
102 "selection.source=model_adapter_safe_regions"
103 )
104 if self.source == "model_adapter_safe_regions" and sum(selectors) != 1:
105 raise ValueError(
106 "activation_checkpoint.selection requires exactly one of "
107 "layer_count or layer_indices when "
108 "selection.source=model_adapter_safe_regions"
109 )
111 def _validate_selector_values(self) -> None:
112 """Require non-negative, distinct layer numbers."""
113 if self.layer_count is not None and (
114 isinstance(self.layer_count, bool)
115 or not isinstance(self.layer_count, int)
116 or self.layer_count < 0
117 ):
118 raise ValueError(
119 "activation_checkpoint.selection.layer_count must be a "
120 "non-negative integer"
121 )
122 if self.layer_indices is None:
123 return
124 if not isinstance(self.layer_indices, list) or not self.layer_indices:
125 raise ValueError(
126 "activation_checkpoint.selection.layer_indices must be a "
127 "non-empty list of non-negative integers"
128 )
129 if any(
130 isinstance(index, bool) or not isinstance(index, int) or index < 0
131 for index in self.layer_indices
132 ):
133 raise ValueError(
134 "activation_checkpoint.selection.layer_indices must contain "
135 "only non-negative integers"
136 )
137 if len(set(self.layer_indices)) != len(self.layer_indices):
138 raise ValueError(
139 "activation_checkpoint.selection.layer_indices must not "
140 "contain duplicates"
141 )
144@dataclass
145class ActivationCheckpointConfig:
146 """Activation-checkpoint options exposed by the initial YAML schema.
148 ``swap_inputs`` is consumed only when ``mode`` is ``"full"`` or
149 ``"selective"``.
150 """
152 mode: Optional[Literal["off", "full", "selective"]] = "off"
153 swap_inputs: bool = False
154 selection: ActivationCheckpointSelection = field(
155 default_factory=ActivationCheckpointSelection
156 )
157 # Escape hatch for SAC's cached-tensor mutation check: safe when
158 # every in-place rewrite of a cached operator output is
159 # deterministic, so the recompute replays the same values (the
160 # CSA indexer's masked_fill_ family).
161 allow_cache_entry_mutation: bool = False
163 def __post_init__(self) -> None:
164 """Reject ambiguous values for activation input swapping."""
165 if not isinstance(self.swap_inputs, bool):
166 raise TypeError("activation_checkpoint.swap_inputs must be a bool")
167 if not isinstance(self.allow_cache_entry_mutation, bool):
168 raise TypeError(
169 "activation_checkpoint.allow_cache_entry_mutation must be a bool")
172def _resolve_replace_implementation(value: Union[str, Target[Any]]) -> Target[Any]:
173 """Resolve a replacement implementation without invoking its callable."""
174 if isinstance(value, Target):
175 return value
176 if not isinstance(value, str) or not value.strip():
177 raise TypeError("replace.implementation must be a non-empty import path or Target")
178 parts = value.split(".")
179 for split_at in range(len(parts), 0, -1):
180 module_name = ".".join(parts[:split_at])
181 try:
182 symbol = importlib.import_module(module_name)
183 except ModuleNotFoundError as exc:
184 if exc.name == module_name or module_name.startswith(f"{exc.name}."):
185 continue
186 raise ValueError(f"replace.implementation {value!r} failed while importing {exc.name!r}") from exc
187 except ImportError as exc:
188 raise ValueError(f"replace.implementation {value!r} failed while importing: {exc}") from exc
189 for attribute in parts[split_at:]:
190 if not hasattr(symbol, attribute):
191 raise ValueError(f"replace.implementation {value!r} has no attribute {attribute!r}")
192 symbol = getattr(symbol, attribute)
193 if not callable(symbol):
194 raise TypeError(f"replace.implementation {value!r} must name a callable")
195 return Target(symbol, target_path=value)
196 raise ValueError(f"replace.implementation {value!r} could not be imported")
199@dataclass(frozen=True)
200class ReplaceAction:
201 """One public module or method replacement declaration."""
203 kind: Literal["module", "method"]
204 implementation: Union[str, Target[Any]]
205 source_type: Optional[str] = None
206 exact_type: bool = False
208 def __post_init__(self) -> None:
209 """Validate kind-specific fields and normalize the delayed target."""
210 if self.kind not in ("module", "method"):
211 raise ValueError("replace.kind must be 'module' or 'method'")
212 if not isinstance(self.exact_type, bool):
213 raise TypeError("replace.exact_type must be a bool")
214 if self.kind == "module":
215 if self.source_type is not None and (
216 not isinstance(self.source_type, str) or not self.source_type.strip()):
217 raise ValueError("replace.source_type must be a non-empty import path when provided")
218 if self.exact_type and self.source_type is None:
219 raise ValueError("replace.exact_type requires replace.source_type")
220 elif self.source_type is not None or self.exact_type:
221 raise ValueError("replace.source_type and replace.exact_type are only valid for kind='module'")
222 object.__setattr__(
223 self, "implementation", _resolve_replace_implementation(self.implementation)
224 )
227@dataclass
228class PlanOverride:
229 """One plan_overrides entry (YAML ``plan_overrides`` list item).
231 This is the **YAML transport form** of ``ShardingPlanner(plan_overrides=
232 {...})`` — the planner's single override interface. Placement objects are
233 not YAML-serializable, so contract fields use the string DSL
234 (``"replicate"`` / ``"partial"`` / ``"shard(N)"``) plus the sentinels
235 ``"auto"`` (explicit inherit) / ``"none"`` (explicit clear); they are
236 desugared to real objects in ``to_override()`` — the planner never sees
237 strings. The desugar happens trainer-side in
238 ``instantiate_infrastructure`` BEFORE planner construction.
240 Fields:
241 replace: public replacement action. ``kind='module'`` requires a
242 decorated module factory at ``implementation``; optional
243 ``source_type`` checks the expected source module type;
244 ``kind='method'`` selects a literal method suffix in ``match``;
245 ``.forward`` uses the existing boundary executor and defaults to
246 ``region_dispatch=False``, while other methods use a decorated
247 ``@method_replacement`` factory before sharding.
248 match: module FQN/glob, or module glob plus a literal method suffix for
249 ``kind='method'``. Boundary globs first merge matching planner boundaries;
250 a glob with a concrete params/I/O contract also expands against
251 the final model's ``named_modules`` and creates missing boundaries.
252 A merge-only glob fails if it matches a real module that has no
253 planner boundary, with a suggested contract skeleton.
254 ``*`` spans dots, so ``"*.self_attn"`` hits
255 ``model.layers.0.self_attn``.
256 when: optional activation condition. Sharding actions accept ``"cp"``
257 (active when cp_size>1), ``"ep"`` (ep_size>1),
258 ``"sequence_parallel"``, or ``"low_precision"`` (active when
259 online low precision is enabled). Module replacement accepts only
260 ``"low_precision"`` and cannot depend on the parallel topology.
261 low_precision_dtype_scheme: a named ``training.low_precision.
262 dtype_schemes`` entry bound to this match. Only valid on
263 ``when="low_precision"`` replacement entries; the resolved scheme
264 is injected into the replacement factory context so the strategy
265 factory never resolves a catalog at replacement time.
266 Forward replacements use ``@forward_replacement`` or ``@local_compute``
267 factories through ``replace.implementation``.
268 region_dispatch: validate execution-mode declaration; forward method
269 replacement defaults to ``false``. ``false`` = the region
270 computation (the module's own forward or the injected function)
271 cannot be dispatched: it contains communication primitives /
272 custom kernels (e.g. an in-house EP-aware MoE all-to-all inside
273 forward, or the in-repo CP/EP reference implementations), so it
274 runs as a local-region skeleton black box; ``true`` = the
275 injected code is pure standard operators (fused kernels /
276 scripted-style optimizations), so under validate the dispatch
277 passes through and out_src is genuinely checked. This field is
278 not needed without an injection (ordinary boundaries pass through
279 naturally).
280 params / in_src / in_dst / out_src / out_dst: contract fields in the
281 YAML form ``{name: {axis: placement_str}}`` (out_* also accept
282 the scalar shorthand ``{axis: placement_str}``), or the sentinels
283 ``"auto"`` / ``"none"``. Merge mode (match hits a derived
284 boundary): an omitted field inherits and an explicit ``{}``
285 clears. Insert mode (misses every boundary): nothing can inherit;
286 at least one concrete parameter/I/O field must declare the new
287 boundary, and all fields required by that module's contract must
288 be written explicitly.
289 tp_divide_attrs: optional module-instance integer attributes divided
290 exactly by the active TP size when the module forward runs on
291 local tensors. Omit for no user adjustment; an explicit empty
292 list clears an inherited glob declaration.
293 """
295 match: Union[str, List[str]]
296 replace: Optional[ReplaceAction] = None
297 when: Optional[Literal[
298 "cp", "ep", "sequence_parallel", "low_precision"
299 ]] = None
300 low_precision_dtype_scheme: Optional[str] = None
301 region_dispatch: Optional[bool] = None
302 params: Optional[Any] = None
303 in_src: Optional[Any] = None
304 in_dst: Optional[Any] = None
305 out_src: Optional[Any] = None
306 out_dst: Optional[Any] = None
307 tp_divide_attrs: Optional[List[str]] = None
309 # Contract fields (string sentinels are resolved at planner merge time;
310 # insert mode rejects them)
311 _CONTRACT_FIELDS = ("params", "in_src", "in_dst", "out_src", "out_dst")
312 _BOUNDARY_FIELDS = _CONTRACT_FIELDS + ("tp_divide_attrs",)
314 def __post_init__(self) -> None:
315 """Validate the public replacement action and its boundary contract."""
316 if self.replace is None:
317 return
318 if not isinstance(self.replace, ReplaceAction):
319 raise TypeError("plan_overrides.replace must be a ReplaceAction")
320 if self.replace.kind == "module":
321 self._validate_module_replacement()
322 return
323 self._validate_method_replacement()
325 def _validate_module_replacement(self) -> None:
326 """Validate fields exclusive to a module replacement rule."""
327 if self.when not in (None, "low_precision"):
328 raise ValueError("kind='module' only supports when='low_precision'")
329 if self.region_dispatch is not None:
330 raise ValueError("region_dispatch cannot appear with kind='module'")
331 if any(getattr(self, name) is not None for name in self._BOUNDARY_FIELDS):
332 raise ValueError("kind='module' must use its own plan_overrides rule")
334 def _validate_method_replacement(self) -> None:
335 """Validate a literal method match and dispatch to its protocol checks."""
336 if isinstance(self.match, list):
337 raise ValueError(f"kind={self.replace.kind!r} requires one string match per rule")
338 if self.low_precision_dtype_scheme is not None:
339 raise ValueError("low_precision_dtype_scheme is only valid for kind='module'")
340 if not isinstance(self.match, str):
341 raise TypeError("kind='method' match must be a string")
342 owner_pattern, separator, method_name = self.match.rpartition(".")
343 if (not separator or not owner_pattern or not method_name.isidentifier()
344 or (method_name.startswith("__") and method_name.endswith("__"))):
345 raise ValueError("kind='method' match must end in a literal method name")
346 if method_name != "forward":
347 self._validate_non_forward_method()
348 return
349 self._validate_forward_method()
351 def _validate_non_forward_method(self) -> None:
352 """Validate a decorated method factory without a planner contract."""
353 if self.region_dispatch is not None or any(
354 getattr(self, name) is not None for name in self._BOUNDARY_FIELDS):
355 raise ValueError("kind='method' on a non-forward method cannot declare a boundary contract")
356 if not getattr(self.replace.implementation.callable, "_hp_method_replacement", False):
357 raise TypeError("kind='method' implementation must use @method_replacement")
359 def _validate_forward_method(self) -> None:
360 """Validate a forward factory and its boundary dispatch declaration."""
361 self.region_dispatch = False if self.region_dispatch is None else self.region_dispatch
362 if not isinstance(self.region_dispatch, bool):
363 raise TypeError("region_dispatch must be a bool")
364 implementation = self.replace.implementation
365 meta = getattr(implementation.callable, "_injection_meta", None)
366 if meta is None:
367 raise TypeError(
368 "kind='method' on forward implementation must use @forward_replacement or @local_compute"
369 )
370 if meta.kind not in ("local_compute", "inner_wrapper"):
371 raise TypeError(f"unsupported forward target metadata kind {meta.kind!r}")
373 def _serialized_replacement(self):
374 """Serialize only the replacement action, independently of layout fields."""
375 implementation_config = _serialize_config_value(self.replace.implementation)
376 implementation_value = (
377 implementation_config["_target_"]
378 if isinstance(implementation_config, dict) and set(implementation_config) == {"_target_"}
379 else implementation_config
380 )
381 action = {"kind": self.replace.kind, "implementation": implementation_value}
382 if self.replace.kind == "module":
383 if self.replace.source_type is not None:
384 action["source_type"] = self.replace.source_type
385 if self.replace.exact_type:
386 action["exact_type"] = True
387 return action
389 def to_dict(self) -> dict[str, Any]:
390 """Serialize the public declaration."""
391 result: dict[str, Any] = {"match": self.match}
392 if self.when is not None:
393 result["when"] = self.when
394 if self.low_precision_dtype_scheme is not None:
395 result["low_precision_dtype_scheme"] = self.low_precision_dtype_scheme
396 if self.replace is not None:
397 result["replace"] = self._serialized_replacement()
398 for name in ("region_dispatch", "params", "in_src", "in_dst", "out_src", "out_dst", "tp_divide_attrs"):
399 value = getattr(self, name)
400 default_forward_dispatch = (
401 name == "region_dispatch" and value is False
402 and self.replace is not None and self.replace.kind == "method"
403 and self.match.rpartition(".")[2] == "forward"
404 )
405 if value is not None and not default_forward_dispatch:
406 result[name] = _serialize_config_value(value)
407 return result
409 def _validate_override_match(self):
410 """Reject method-only and empty declarations before creating a boundary."""
411 if self.replace is not None and self.replace.kind == "method" and (
412 self.match.rpartition(".")[2] != "forward"):
413 raise ValueError("non-forward method replacements do not create planner boundaries")
415 if not self.match:
416 raise ValueError(
417 "plan_overrides entry is missing 'match' (an fqn or "
418 "fqn glob such as '*.self_attn' or '*.mlp')")
420 def to_override(self) -> "tuple[str, Any]":
421 """Desugar to a ``(match, ModuleShardingSpec)`` plan_overrides entry.
423 Placement DSL strings become real Placement objects here; the
424 ``"auto"``/``"none"`` sentinels pass through AS STRINGS into the spec
425 (the planner's merge resolves them; insert rejects them). Imports are
426 lazy: trainer.config must not become an import-time dependency of
427 components.distributed (the zero-dependency boundary is guarded by
428 test_s5_zero_dep_lint).
429 """
430 # Keep trainer.config outside the distributed component's import-time dependency graph.
431 # pylint: disable-next=import-outside-toplevel
432 from hyper_parallel.distributed.recipe_spec import (
433 ModuleShardingSpec,
434 )
436 self._validate_override_match()
437 implementation = self.replace.implementation if self.replace is not None else None
438 meta = getattr(implementation.callable, "_injection_meta", None) if implementation is not None else None
439 spec = ModuleShardingSpec(
440 local_compute_fn=implementation if meta is not None and meta.kind == "local_compute" else None,
441 inner_target="self" if meta is not None and meta.kind == "inner_wrapper" else None,
442 inner_wrapper=implementation if meta is not None and meta.kind == "inner_wrapper" else None,
443 region_dispatch=self.region_dispatch,
444 tp_divide_attrs=self._validate_tp_divide_attrs(),
445 )
446 for attr in self._CONTRACT_FIELDS:
447 raw = getattr(self, attr)
448 if raw is None:
449 continue
450 setattr(spec, attr, self._parse_contract_field(attr, raw))
451 match = (
452 self.match.rpartition(".")[0]
453 if self.replace is not None and self.replace.kind == "method"
454 else self.match
455 )
456 return match, spec
458 def _validate_tp_divide_attrs(self) -> Optional[List[str]]:
459 """Validate the YAML transport shape for TP-local attributes."""
460 attrs = self.tp_divide_attrs
461 if attrs is None:
462 return None
463 if not isinstance(attrs, list):
464 raise ValueError(
465 f"plan_overrides match={self.match!r}: tp_divide_attrs "
466 f"must be a list of attribute names, got {type(attrs).__name__}")
467 seen = set()
468 for attr in attrs:
469 if not isinstance(attr, str) or not attr or not attr.isidentifier():
470 raise ValueError(
471 f"plan_overrides match={self.match!r}: "
472 f"tp_divide_attrs may only contain valid attribute names, got {attr!r}")
473 if attr in seen:
474 raise ValueError(
475 f"plan_overrides match={self.match!r}: "
476 f"tp_divide_attrs contains duplicate attribute {attr!r}")
477 seen.add(attr)
478 return list(attrs)
480 def _parse_contract_field(self, attr, raw):
481 """YAML form → spec field value (DSL parse / sentinel pass-through)."""
482 # Keep trainer.config outside the distributed component's import-time dependency graph.
483 # pylint: disable-next=import-outside-toplevel
484 from hyper_parallel.distributed.recipe_spec import (
485 parse_named_placement,
486 )
488 if isinstance(raw, str):
489 if raw in ("auto", "none"):
490 return raw
491 raise ValueError(
492 f"plan_overrides match={self.match!r}: string value of contract "
493 f"field {attr} only accepts the sentinels 'auto' (explicit "
494 f"inherit) / 'none' (explicit clear), got {raw!r}")
495 if not isinstance(raw, dict):
496 raise ValueError(
497 f"plan_overrides match={self.match!r}: contract field {attr} "
498 f"expects a mapping or a sentinel string, got {raw!r}")
499 if not raw:
500 return {} # explicit empty ("as written": clear / no sharding), unlike omitting the field
501 if attr in ("out_src", "out_dst") and all(
502 isinstance(v, str) for v in raw.values()):
503 # Scalar shorthand {axis: placement} — already a NamedPlacement
504 # (the output name is filled in uniformly by the planner's
505 # _normalize_out_fields)
506 return parse_named_placement(
507 raw, path=f"plan_overrides[{self.match!r}].{attr}")
508 return {
509 name: parse_named_placement(
510 named, path=f"plan_overrides[{self.match!r}].{attr}.{name}")
511 for name, named in raw.items()
512 }
515_WHEN_CONDITIONS = ("cp", "ep", "sequence_parallel", "low_precision")
518def _import_module_type(path: str) -> type[nn.Module]:
519 """Import the source module type named by a replacement YAML entry."""
521 if not isinstance(path, str) or not path.strip():
522 raise ValueError("plan_overrides.replace.source_type must be a non-empty symbol path")
523 parts = path.split(".")
524 for split_at in range(len(parts), 0, -1):
525 module_name = ".".join(parts[:split_at])
526 try:
527 symbol = importlib.import_module(module_name)
528 except ModuleNotFoundError as exc:
529 if exc.name == module_name or module_name.startswith(f"{exc.name}."):
530 continue
531 raise ValueError(
532 f"plan_overrides.replace.source_type {path!r} failed while importing {exc.name!r}"
533 ) from exc
534 except ImportError as exc:
535 raise ValueError(
536 f"plan_overrides.replace.source_type {path!r} failed while importing: {exc}"
537 ) from exc
538 for attribute in parts[split_at:]:
539 if not hasattr(symbol, attribute):
540 raise ValueError(
541 f"plan_overrides.replace.source_type {path!r} has no attribute {attribute!r}"
542 )
543 symbol = getattr(symbol, attribute)
544 if not isinstance(symbol, type) or not issubclass(symbol, nn.Module):
545 raise TypeError(f"plan_overrides.replace.source_type {path!r} must name an nn.Module type")
546 return symbol
547 raise ValueError(f"plan_overrides.replace.source_type {path!r} could not be imported")
550def _target_replacement_factory(
551 target: Target[Any],
552 low_precision_policy: Optional[LowPrecisionDtypeScheme] = None,
553) -> ModuleReplacementFactory:
554 """Bind a YAML Target's static args to the replacement factory protocol."""
556 if not isinstance(target, ReplacementTarget) and get_module_family(target.callable) is not None:
557 # Preserve configured Python objects; only Family baselines need selection.
558 # Target has no public accessors for its raw constructor values.
559 target = ReplacementTarget(
560 target.callable, target_path=target._target_path, **target._kwargs,
561 )
563 if isinstance(target, ReplacementTarget) and target.low_precision_dtype_scheme is not None:
564 target = target.bind_dtype_scheme(low_precision_policy)
566 if not getattr(target.callable, "_hp_module_replacement", False):
567 raise TypeError(
568 "plan_overrides replace.implementation for kind='module' must use @module_replacement"
569 )
571 @module_replacement
572 def factory(
573 *,
574 module: nn.Module,
575 module_fqn: str,
576 context: Mapping[str, Any],
577 ) -> nn.Module:
578 """Build one replacement module by delegating to the bound YAML Target."""
579 if low_precision_policy is not None:
580 factory_context = dict(context)
581 factory_context["low_precision"] = low_precision_policy
582 context = MappingProxyType(factory_context)
583 return target.build(module=module, module_fqn=module_fqn, context=context)
585 return factory
588def _target_method_factory(target: Target[Any]):
589 """Bind YAML static arguments to the method replacement factory protocol."""
590 if not getattr(target.callable, "_hp_method_replacement", False):
591 raise TypeError("plan_overrides replace.implementation for kind='method' must use @method_replacement")
593 @method_replacement
594 def factory(
595 *, module: nn.Module, method_fqn: str, original_method: Any, context: Mapping[str, Any],
596 ) -> Any:
597 """Build one replacement callable from the configured target."""
598 return target.build(
599 module=module, method_fqn=method_fqn, original_method=original_method, context=context,
600 )
602 return factory
605def entries_to_method_replacements(
606 entries: List[PlanOverride], *, cp_size: int = 1, ep_size: int = 1,
607 sequence_parallel: bool = False, low_precision_enabled: bool = False,
608) -> tuple[MethodReplacementSpec, ...]:
609 """Resolve active non-forward method actions into executable rules.
611 Args:
612 entries: Trainer plan override declarations.
613 cp_size: Active context parallel degree.
614 ep_size: Active expert parallel degree.
615 sequence_parallel: Whether sequence parallelism is active.
616 low_precision_enabled: Whether online low precision is enabled.
618 Returns:
619 Rules applied after module replacement and before sharding.
620 """
621 rules = []
622 for entry in entries:
623 if entry.replace is None or entry.replace.kind != "method":
624 continue
625 if entry.match.rpartition(".")[2] == "forward":
626 continue
627 _validate_when(entry)
628 active = {
629 None: True,
630 "cp": cp_size > 1,
631 "ep": ep_size > 1,
632 "sequence_parallel": sequence_parallel,
633 "low_precision": low_precision_enabled,
634 }[entry.when]
635 if active:
636 rules.append(MethodReplacementSpec(
637 match=entry.match,
638 factory=_target_method_factory(entry.replace.implementation),
639 ))
640 else:
641 logger.info("plan_overrides: match=%r skipped because condition %r is inactive",
642 entry.match, entry.when)
643 return tuple(rules)
646def entries_to_module_replacement_requests(
647 entries: List[PlanOverride], *, low_precision_enabled: bool = False,
648 low_precision_config: Optional[LowPrecisionConfig] = None,
649) -> tuple[ModuleReplacementRequest, ...]:
650 """Preserve public family identity and explicit parameters through desugaring.
652 Args:
653 entries: Value passed to this operation.
654 """
655 effective = low_precision_config if low_precision_enabled else None
656 result = []
657 for entry_index, entry in enumerate(entries):
658 if entry.replace is None or entry.replace.kind != "module":
659 continue
660 if entry.when == "low_precision" and not low_precision_enabled:
661 continue
662 _validate_replacement_entry(entry, effective)
663 target = entry.replace.implementation
664 baseline = target.requested_target if isinstance(target, ReplacementTarget) else target.callable
665 arguments = {name: value for name, value in target._kwargs.items() # pylint: disable=protected-access
666 if name not in {"module", "module_fqn", "context"}}
667 if isinstance(target, ReplacementTarget):
668 arguments.update(target.selectors)
669 if entry.when == "low_precision":
670 # Legacy policy-bearing closures remain on the explicit compatibility path.
671 continue
672 result.append(ModuleReplacementRequest(
673 match=(entry.match,) if isinstance(entry.match, str) else tuple(entry.match),
674 target=baseline, kwargs=arguments, origin="user",
675 declaration_id=f"plan_override:{entry_index}:replace",
676 module_type=_import_module_type(entry.replace.source_type) if entry.replace.source_type else None,
677 exact_type=entry.replace.exact_type,
678 ))
679 return tuple(result)
682def entries_to_module_replacements(
683 entries: List[PlanOverride],
684 *,
685 low_precision_enabled: bool = False,
686 low_precision_config: Optional[LowPrecisionConfig] = None,
687) -> tuple[ModuleReplacementSpec, ...]:
688 """Desugar replacement actions and bind their named dtype schemes.
690 Args:
691 entries: Raw planner override entries.
692 low_precision_enabled: Whether low-precision replacement is active.
693 low_precision_config: Named dtype schemes for active replacements.
694 """
696 effective_config = low_precision_config if low_precision_enabled else None
697 rules = []
698 for entry_index, entry in enumerate(entries):
699 if entry.replace is None or entry.replace.kind != "module":
700 continue
701 if entry.when == "low_precision" and not low_precision_enabled:
702 continue
703 _validate_replacement_entry(entry, effective_config)
704 patterns = (entry.match,) if isinstance(entry.match, str) else tuple(entry.match)
705 policy = None
706 scheme_name = getattr(entry.replace.implementation, "low_precision_dtype_scheme", None)
707 if scheme_name is not None:
708 if entry.low_precision_dtype_scheme is not None:
709 raise ValueError(
710 f"plan_overrides match={entry.match!r}: specify dtype scheme only inside replace.implementation")
711 if low_precision_config is None:
712 raise ValueError(f"plan_overrides match={entry.match!r}: dtype scheme requires training.low_precision")
713 policy = low_precision_config.resolve_dtype_scheme(scheme_name)
714 elif entry.when == "low_precision" and effective_config is not None:
715 policy = effective_config.resolve_dtype_scheme(entry.low_precision_dtype_scheme)
716 serialized_target = entry.replace.implementation.to_dict()
717 rules.append(
718 ModuleReplacementSpec(
719 match=patterns,
720 factory=_target_replacement_factory(entry.replace.implementation, policy),
721 module_type=(
722 _import_module_type(entry.replace.source_type)
723 if entry.replace.source_type is not None else None
724 ),
725 exact_type=entry.replace.exact_type,
726 declaration_id=f"plan_override:{entry_index}:replace",
727 replacement_target=serialized_target["_target_"],
728 static_arguments={
729 key: value
730 for key, value in serialized_target.items()
731 if key != "_target_"
732 },
733 )
734 )
735 return tuple(rules)
738def _validate_replacement_entry(entry: PlanOverride, effective_config: Optional[LowPrecisionConfig]) -> None:
739 """Validate one replacement before constructing its planner rule."""
740 if entry.when not in (None, "low_precision"):
741 raise ValueError(
742 f"plan_overrides match={entry.match!r} uses kind='module' and "
743 "does not support 'when' values other than 'low_precision' "
744 "(module replacements are not conditioned on parallel topology)"
745 )
746 if entry.low_precision_dtype_scheme is not None and (
747 not isinstance(entry.low_precision_dtype_scheme, str)
748 or not entry.low_precision_dtype_scheme.strip()
749 ):
750 raise ValueError(
751 f"plan_overrides match={entry.match!r}: "
752 "low_precision_dtype_scheme must be a non-empty string"
753 )
754 if entry.low_precision_dtype_scheme is not None and entry.when != "low_precision":
755 raise ValueError(
756 f"plan_overrides match={entry.match!r} sets "
757 "low_precision_dtype_scheme but is not a low-precision entry"
758 )
759 if entry.when == "low_precision" and effective_config is None:
760 raise ValueError(
761 f"plan_overrides match={entry.match!r} enables a low-precision "
762 "replacement but training.low_precision is missing"
763 )
764 if not isinstance(entry.replace.implementation, Target):
765 raise TypeError(
766 f"plan_overrides match={entry.match!r} replace.implementation must be a YAML Target"
767 )
770def _has_sharding_action(entry: PlanOverride) -> bool:
771 action_names = (
772 "region_dispatch", "params", "in_src", "in_dst", "out_src", "out_dst",
773 )
774 return any(getattr(entry, name) is not None for name in action_names)
777def _validate_when(entry: PlanOverride) -> None:
778 """Validate programmatic when values that bypass the YAML Literal check."""
780 if entry.when is not None and entry.when not in _WHEN_CONDITIONS:
781 raise ValueError(
782 f"plan_overrides match={entry.match!r} has invalid when={entry.when!r}; "
783 f"expected one of {list(_WHEN_CONDITIONS)}"
784 )
787def _merge_plan_override(overrides: dict[str, Any], match: str, spec: Any) -> None:
788 """Merge one desugared spec into an existing match, with later fields winning."""
789 if match not in overrides:
790 overrides[match] = spec
791 return
792 previous = overrides[match]
793 for name in ("local_compute_fn", "inner_target", "inner_wrapper",
794 "inner_out_src", "params", "in_src", "in_dst",
795 "out_src", "out_dst", "tp_divide_attrs"):
796 value = getattr(spec, name)
797 if value is not None:
798 setattr(previous, name, value)
799 if spec.region_dispatch is not None:
800 previous.region_dispatch = spec.region_dispatch
803def _validate_override_transport(entry):
804 """Validate YAML module-selection transport separately from placement lowering."""
805 if isinstance(entry.match, list) and (
806 entry.replace is None or entry.replace.kind != "module" or _has_sharding_action(entry)
807 ):
808 raise ValueError(
809 "plan_overrides match lists are supported only for replace.kind='module' "
810 "entries; use separate string matches for sharding actions"
811 )
812 if entry.replace is not None and entry.replace.kind == "module" and entry.when not in (
813 None,
814 "low_precision",
815 ):
816 raise ValueError(
817 f"plan_overrides match={entry.match!r} uses replace.kind='module' and "
818 "does not support 'when' values other than 'low_precision' "
819 "(module replacements are not conditioned on parallel topology)"
820 )
823def entries_to_plan_overrides(
824 entries: "List[PlanOverride]", *, cp_size: int = 1, ep_size: int = 1,
825 sequence_parallel: bool = False,
826 low_precision_enabled: bool = False,
827) -> "dict[str, Any]":
828 """Desugar PlanOverride entries into a ``plan_overrides`` dict.
830 Args:
831 entries: Raw YAML planner override entries.
832 cp_size: Context-parallel degree.
833 ep_size: Expert-parallel degree.
834 sequence_parallel: Whether decoder sequence parallelism is active.
835 low_precision_enabled: Whether low-precision rules are active.
837 - ``when`` filter: an entry whose declared condition is inactive
838 (``when="cp"`` with cp_size==1 etc.) is SKIPPED with an INFO log —
839 declared gating, never silent application. An unknown ``when`` value
840 fails fast listing the valid conditions (programmatic construction
841 bypasses the resolver's Literal check);
842 - two entries sharing the same ``match`` are merged field-wise
843 (non-None fields, later entry wins) — the planner's interface is
844 exactly this dict.
845 """
846 overrides: dict[str, Any] = {}
847 for entry in entries:
848 if entry.replace is not None and entry.replace.kind == "method" and (
849 entry.match.rpartition(".")[2] != "forward"):
850 _validate_when(entry)
851 continue
852 _validate_override_transport(entry)
853 _validate_when(entry)
854 if entry.when is not None:
855 active = {
856 "cp": cp_size > 1,
857 "ep": ep_size > 1,
858 "sequence_parallel": sequence_parallel,
859 "low_precision": low_precision_enabled,
860 }[entry.when]
861 if not active:
862 logger.info(
863 "plan_overrides: match=%r skipped because condition %r "
864 "is inactive",
865 entry.match, entry.when)
866 continue
867 if entry.replace is not None and entry.replace.kind == "module":
868 continue
869 match, spec = entry.to_override()
870 _merge_plan_override(overrides, match, spec)
871 return overrides
874def normalize_distributed_setup_overrides(
875 distributed_setup: Any,
876 config: "TrainerConfig",
877) -> None:
878 """Fill the normalized plan fields of a ``DistributedSetup`` (05 §15.2.6).
880 Args:
881 distributed_setup: Mutable setup receiving normalized planner rules.
882 config: Trainer configuration containing raw override entries.
884 This is the single Trainer-side desugar point: raw YAML ``PlanOverride``
885 entries (``TrainerConfig.plan_overrides``) are converted HERE —
886 when-filtered against the built mesh and compiled into replacement
887 rules — so the AutoModels build pipeline consumes only normalized
888 objects and never imports trainer config.
889 """
890 entries = list(getattr(config, "plan_overrides", None) or [])
891 mesh_context = getattr(distributed_setup, "mesh_context", None)
892 low_precision_enabled = bool(
893 getattr(
894 getattr(distributed_setup, "low_precision_config", None),
895 "enabled",
896 False,
897 )
898 )
899 distributed_setup._hp_legacy_low_precision_targets = any( # pylint: disable=protected-access
900 entry.replace is not None and entry.replace.kind == "module" and (
901 entry.when == "low_precision"
902 or getattr(entry.replace.implementation, "low_precision_dtype_scheme", None) is not None
903 )
904 for entry in entries
905 )
906 if not distributed_setup._hp_legacy_low_precision_targets: # pylint: disable=protected-access
907 distributed_setup.module_replacement_requests = entries_to_module_replacement_requests(
908 entries, low_precision_enabled=low_precision_enabled,
909 low_precision_config=getattr(distributed_setup, "low_precision_config", None),
910 )
911 else:
912 distributed_setup.module_replacement_requests = None
913 distributed_setup.module_replacements = entries_to_module_replacements(
914 entries,
915 low_precision_enabled=low_precision_enabled,
916 low_precision_config=getattr(distributed_setup, "low_precision_config", None),
917 )
918 distributed_setup.method_replacements = entries_to_method_replacements(
919 entries,
920 cp_size=getattr(mesh_context, "cp_size", 1),
921 ep_size=getattr(mesh_context, "ep_size", 1),
922 sequence_parallel=getattr(mesh_context, "sequence_parallel", False),
923 low_precision_enabled=low_precision_enabled,
924 )
925 distributed_setup.plan_overrides = (
926 entries_to_plan_overrides(
927 entries,
928 cp_size=getattr(mesh_context, "cp_size", 1),
929 ep_size=getattr(mesh_context, "ep_size", 1),
930 sequence_parallel=getattr(mesh_context, "sequence_parallel", False),
931 low_precision_enabled=low_precision_enabled,
932 )
933 if entries
934 else None
935 )