Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / codegen / inline / pipeline.py: 22%

77 statements  

« 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"""Two-stage inline modeling source pipeline.""" 

16 

17from __future__ import annotations 

18 

19import ast 

20import importlib 

21from typing import Any 

22 

23from hyper_parallel.codegen.inline.ir import InlinePatchSet 

24from hyper_parallel.codegen.inline.meta_plan import normalize_inline_meta 

25from hyper_parallel.codegen.inline.patch_engine import apply_patch_set, drop_shadowed_definitions 

26from hyper_parallel.codegen.inline.replacement_pass import build_replacement_patches 

27from hyper_parallel.codegen.inline.specs import replacement_spec, strategy_spec 

28from hyper_parallel.codegen.inline.strategy_pass import build_strategy_patches 

29from hyper_parallel.codegen.inline.yaml_rules import collect_inline_rules 

30 

31 

32def render_inline_modeling(source_text: str, meta: Any, model_type: str | None = None) -> str: 

33 """Render the inline-patched modeling file for a frozen plan. 

34 

35 This is the only artifact route. A YAML target with no adapter declaration 

36 is a hard error rather than a silent fallback, so an inline coverage gap 

37 surfaces at generation time instead of degrading the artifact. A plan with 

38 no inline rules needs no patch and the source is returned unchanged. 

39 

40 Args: 

41 source_text: Modeling source the inline patches are applied to. 

42 meta: Frozen plan metadata recorded by the generator. 

43 model_type: Model family key used to resolve framework declarations. 

44 

45 Returns: 

46 The rendered modeling source. 

47 """ 

48 

49 rules = collect_inline_rules(meta) 

50 if not rules: 

51 return source_text 

52 _require_inline_coverage(rules, model_type) 

53 replacement_patches = build_replacement_patches(rules, model_type) 

54 strategy_patches = build_strategy_patches(rules, model_type) 

55 patch_set = _merge_patch_sets(replacement_patches, strategy_patches) 

56 rendered = apply_patch_set(source_text, patch_set) 

57 # The copied modeling source brings its own decorated helpers that share 

58 # names with the library functions the artifact imports, and a local 

59 # definition wins over the import: the native path calls the library 

60 # implementation (whose kernel binding is resolved in its own module), so a 

61 # redefined copy makes the generated model run something else. Only names 

62 # the base source defines with a decorator are candidates, so the classes and 

63 # snippets the inline passes render (which share names with library imports 

64 # on purpose) keep their definitions. 

65 rendered = drop_shadowed_definitions( 

66 rendered, _library_import_names(rendered) & _decorated_names(source_text) 

67 ) 

68 normalize_inline_meta(meta, rules, model_type) 

69 return rendered 

70 

71 

72def _library_import_names(text: str) -> set[str]: 

73 """Names the artifact imports from the library, i.e. the winning bindings. 

74 

75 Args: 

76 text: Rendered modeling source. 

77 

78 Returns: 

79 The set of names bound by its ``hyper_parallel`` imports. 

80 """ 

81 

82 names: set[str] = set() 

83 try: 

84 tree = ast.parse(text) 

85 except SyntaxError: 

86 return names 

87 for node in ast.walk(tree): 

88 if isinstance(node, ast.ImportFrom) and (node.module or "").startswith("hyper_parallel"): 

89 names.update(alias.asname or alias.name for alias in node.names) 

90 return names 

91 

92 

93def _decorated_names(text: str) -> set[str]: 

94 """Names of the module-level definitions the base source decorates. 

95 

96 A decorator is what makes a copied helper behave differently from the library 

97 function of the same name (a per-module kernel binding, for instance), so 

98 only those definitions may be dropped. 

99 

100 Args: 

101 text: Base modeling source before the inline patches are applied. 

102 

103 Returns: 

104 The set of decorated top-level definition names. 

105 """ 

106 

107 try: 

108 tree = ast.parse(text) 

109 except SyntaxError: 

110 return set() 

111 return { 

112 node.name 

113 for node in tree.body 

114 if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) 

115 and node.decorator_list 

116 } 

117 

118 

119def _is_local_compute_factory(target: str) -> bool: 

120 """Whether a local_compute target is a ``@local_compute`` factory. 

121 

122 Structural injection discipline: the decorator stamps the factory with 

123 ``_injection_meta`` (kind ``LOCAL_COMPUTE``), so a decorated factory is 

124 recognizable without a table. Such a target carries no inline strategy 

125 body -- its region is bound at install time (the emitted region template 

126 for an in-source class, the runtime local-region skeleton for an imported 

127 class), which resolves the factory exactly as the native applier does. 

128 """ 

129 from hyper_parallel.distributed.recipe_spec import LOCAL_COMPUTE # pylint: disable=C0415 

130 

131 module_name, _, attr = target.rpartition(".") 

132 if not module_name or not attr: 

133 return False 

134 try: 

135 module = importlib.import_module(module_name) 

136 except ImportError: 

137 return False 

138 factory = getattr(module, attr, None) 

139 if not callable(factory): 

140 return False 

141 meta = getattr(factory, "_injection_meta", None) 

142 return getattr(meta, "kind", None) == LOCAL_COMPUTE 

143 

144 

145def _require_inline_coverage(rules: tuple[Any, ...], model_type: str | None = None) -> None: 

146 """Fail when a YAML rule has no structural resolution behind it. 

147 

148 Covers both rule kinds: a replacement target needs a ``ReplacementSpec``, 

149 and every strategy target needs a ``StrategySpec``. One exception: a 

150 ``local_compute_fn`` target that is itself a ``@local_compute`` factory 

151 needs no inline strategy -- its region is runtime-bound (see 

152 :func:`_is_local_compute_factory`), so the factory's injection discipline 

153 is the coverage proof. Reporting all gaps at once keeps one fix cycle 

154 from hiding the next gap. 

155 """ 

156 

157 uncovered: list[str] = [] 

158 for rule in rules: 

159 if ( 

160 rule.replace_target is not None 

161 and replacement_spec(rule.replace_target, model_type, module_type=rule.module_type) is None 

162 ): 

163 uncovered.append(rule.replace_target) 

164 for target in (rule.local_compute_target, rule.inner_wrapper_target): 

165 if target is None: 

166 continue 

167 if strategy_spec( 

168 target, model_type, inner_wrapper=target == rule.inner_wrapper_target 

169 ) is not None: 

170 continue 

171 # An inner_wrapper target has no runtime channel and stays 

172 # fail-closed; a local_compute target passes through the 

173 # @local_compute decoration check. 

174 if target == rule.local_compute_target and _is_local_compute_factory(target): 

175 continue 

176 uncovered.append(target) 

177 if uncovered: 

178 raise RuntimeError( 

179 f"codegen: no inline declaration for {sorted(set(uncovered))} " 

180 f"(model_type={model_type!r}); the target names no known generic " 

181 "component or framework strategy, and a local_compute target " 

182 "names no @local_compute factory" 

183 ) 

184 

185 

186def _merge_patch_sets(*sets: InlinePatchSet) -> InlinePatchSet: 

187 merged = InlinePatchSet() 

188 for patch_set in sets: 

189 merged.imports.extend(patch_set.imports) 

190 merged.module_snippets.extend(patch_set.module_snippets) 

191 merged.constructor_replaces.extend(patch_set.constructor_replaces) 

192 merged.forward_extracts.extend(patch_set.forward_extracts) 

193 merged.class_removals.extend(patch_set.class_removals) 

194 return merged