Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / compile.py: 25%
120 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"""Compile Transformer decoder layers as independent graph segments."""
17import logging
18from collections.abc import Iterable, Mapping
19from typing import Any, Optional, Union
21import torch
22from torch import nn
24from hyper_parallel.models.build_options import CompileConfig
25from hyper_parallel.distributed._builder.fsdp_adapter import FSDP2Manager
27logger = logging.getLogger(__name__)
30_MAPPING_GET_POLYFILL_INSTALLED = False
32# No per-model layer-path table: the model-owned ``get_compile_layers()``
33# contract comes first, and the generic HF-convention fallback paths below
34# cover every supported family. A family whose container lives elsewhere
35# declares the contract on its model class instead of registering here.
36_GENERIC_LAYER_PATHS = ("layers", "model.layers")
39def _get_attribute(root: Any, path: str) -> Any:
40 """Read a dotted module path, including numeric ModuleList indexes."""
41 value = root
42 for part in path.split("."):
43 value = value[int(part)] if part.isdigit() else getattr(value, part)
44 return value
47def _is_named_layer(item: Any) -> bool:
48 """Return whether an item is a valid ``(fqn, module)`` pair."""
49 if not isinstance(item, tuple) or len(item) != 2:
50 return False
51 name, layer = item
52 return isinstance(name, str) and isinstance(layer, nn.Module)
55def _normalize_declared_layers(declared: Any) -> list[tuple[str, nn.Module]]:
56 """Normalize the model-owned decoder-layer contract."""
57 if isinstance(declared, nn.ModuleList):
58 return [(str(index), layer) for index, layer in enumerate(declared)]
59 if not isinstance(declared, Iterable) or isinstance(declared, (str, bytes)):
60 raise TypeError(
61 "get_compile_layers() must return an iterable of modules or "
62 "(fqn, module) pairs"
63 )
65 layers = []
66 for index, item in enumerate(declared):
67 if isinstance(item, nn.Module):
68 layers.append((str(index), item))
69 continue
70 if _is_named_layer(item):
71 layers.append(item)
72 continue
73 raise TypeError(
74 "get_compile_layers() entries must be modules or (fqn, module) pairs, "
75 f"but entry {index} is {type(item).__name__}"
76 )
77 return layers
80def _qualify_declared_layers(
81 model: nn.Module,
82 layers: list[tuple[str, nn.Module]],
83) -> list[tuple[str, nn.Module]]:
84 """Replace generated numeric names with the modules' model-owned FQNs."""
85 module_names = {id(module): name for name, module in model.named_modules()}
86 return [
87 (module_names.get(id(layer), name) if name.isdigit() else name, layer)
88 for name, layer in layers
89 ]
92def _layers_from_path(model: nn.Module, path: str) -> list[tuple[str, nn.Module]]:
93 """Return indexed layers from one declared container path."""
94 try:
95 container = _get_attribute(model, path)
96 except (AttributeError, IndexError, KeyError, TypeError):
97 return []
98 if not isinstance(container, (nn.ModuleList, nn.Sequential)):
99 return []
100 return [(f"{path}.{index}", layer) for index, layer in enumerate(container)]
103def get_compile_layers(model: nn.Module) -> list[tuple[str, nn.Module]]:
104 """Return stable decoder-layer segments declared by a supported model.
106 Resolution order: the model-owned ``get_compile_layers()`` contract
107 first, then the generic HF-convention container paths
108 (``_GENERIC_LAYER_PATHS``). A family whose container follows neither
109 declares ``get_compile_layers()`` on its model class.
110 """
111 declared_getter = getattr(model, "get_compile_layers", None)
112 if callable(declared_getter):
113 layers = _qualify_declared_layers(
114 model,
115 _normalize_declared_layers(declared_getter()),
116 )
117 if layers:
118 return layers
120 seen_containers = set()
121 for path in _GENERIC_LAYER_PATHS:
122 layers = _layers_from_path(model, path)
123 if not layers:
124 continue
125 container_id = id(_get_attribute(model, path))
126 if container_id in seen_containers:
127 continue
128 seen_containers.add(container_id)
129 return layers
131 raise ValueError(
132 "compile is enabled, but the model exposes no decoder-layer compile contract; "
133 "define get_compile_layers() or use a supported model layer container"
134 )
137def _install_dynamo_mapping_get_polyfill() -> None:
138 """Make ``Mapping.get`` traceable without changing Transformers source.
140 Transformers' attention registry calls ``Mapping.get`` from every decoder
141 layer. TorchDynamo treats the standard-library implementation as a
142 skipfile, so the call creates a graph break. ``substitute_in_graph`` only
143 replaces the implementation while Dynamo inlines it; eager execution
144 continues to use the original method.
145 """
146 # Module-level idempotency flag: the polyfill must be installed at most
147 # once per process, so a global statement is required here.
148 global _MAPPING_GET_POLYFILL_INSTALLED # pylint: disable=global-statement
149 if _MAPPING_GET_POLYFILL_INSTALLED:
150 return
152 substitute_in_graph = getattr(torch.compiler, "substitute_in_graph", None)
153 if substitute_in_graph is None:
154 logger.warning(
155 "Torch does not provide compiler.substitute_in_graph; "
156 "Mapping.get graph breaks cannot be removed"
157 )
158 _MAPPING_GET_POLYFILL_INSTALLED = True
159 return
161 def _mapping_get(self, key, default=None):
162 try:
163 return self[key]
164 except KeyError:
165 return default
167 try:
168 substitute_in_graph(Mapping.get)(_mapping_get)
169 except ValueError as exc:
170 if "Duplicate dispatch rule" not in str(exc):
171 raise
172 logger.debug("Mapping.get already has a TorchDynamo substitution")
173 _MAPPING_GET_POLYFILL_INSTALLED = True
176def resolve_compile_kwargs(config: CompileConfig) -> dict[str, Any]:
177 """Convert ``CompileConfig`` into keyword arguments for ``Module.compile``."""
178 kwargs: dict[str, Any] = {
179 "fullgraph": config.fullgraph,
180 "dynamic": config.dynamic,
181 }
182 if config.backend is not None:
183 kwargs["backend"] = config.backend
184 if config.options:
185 kwargs["options"] = dict(config.options)
186 else:
187 kwargs["mode"] = config.mode
188 return kwargs
191def apply_compile(model: nn.Module, config: CompileConfig) -> nn.Module:
192 """Compile each decoder layer in place while preserving module identity."""
193 if not config.enabled:
194 return model
196 _install_dynamo_mapping_get_polyfill()
197 torch._dynamo.config.cache_size_limit = config.dynamo_cache_size_limit # pylint: disable=W0212
198 compile_kwargs = resolve_compile_kwargs(config)
199 layers = get_compile_layers(model)
200 for layer_fqn, layer in layers:
201 try:
202 layer.compile(**compile_kwargs)
203 except Exception as exc:
204 raise RuntimeError(f"failed to compile segment {layer_fqn}") from exc
206 logger.info(
207 "Compiled %d decoder layers (backend=%s, mode=%s, dynamic=%s, fullgraph=%s)",
208 len(layers),
209 config.backend,
210 config.mode,
211 config.dynamic,
212 config.fullgraph,
213 )
214 return model
217__all__ = [
218 "apply_compile",
219 "get_compile_layers",
220 "resolve_compile_kwargs",
221]
224def _resolve_compile_config(
225 compile_config: Optional[Union[CompileConfig, dict]],
226 validate_placement: bool,
227 fsdp2_manager: Optional[FSDP2Manager],
228) -> tuple[Optional[CompileConfig], bool]:
229 """Normalize compile configuration and validate its FSDP interaction.
231 Moved from ``_transformers/infrastructure.py`` (05 §15.2.1): compile
232 option normalization and the fullgraph/FSDP conflict check follow the
233 compile feature.
234 """
235 if isinstance(compile_config, dict):
236 compile_config = CompileConfig(enabled=True, **compile_config)
237 if compile_config is not None and not isinstance(compile_config, CompileConfig):
238 raise TypeError("compile_config must be a CompileConfig, mapping, or None")
239 graph_mode_requested = bool(
240 compile_config is not None and compile_config.selects_graph_compiler()
241 )
242 compile_for_execution = bool(
243 not validate_placement
244 and compile_config is not None
245 and compile_config.enabled
246 and not graph_mode_requested
247 )
248 if graph_mode_requested:
249 logger.info(
250 "compile.use_joint_graph=True selects graph-mode trainer; "
251 "skipping decoder-layer compile",
252 )
253 if validate_placement and compile_config is not None and compile_config.enabled:
254 logger.info("Skipping decoder-layer compile during placement validation")
255 if compile_for_execution and compile_config.fullgraph and isinstance(fsdp2_manager, FSDP2Manager):
256 raise ValueError(
257 "compile.fullgraph=True is incompatible with FSDP hooks kept eager "
258 "by _dynamo_disable; set compile.fullgraph=False"
259 )
260 return compile_config, compile_for_execution