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

143 statements  

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

16 

17from __future__ import annotations 

18 

19import ast 

20from dataclasses import replace 

21import importlib 

22import textwrap 

23from typing import Any 

24 

25from hyper_parallel.codegen.inline.ir import ( 

26 ImportPatch, 

27 InlinePatchSet, 

28 ModuleSnippetPatch, 

29) 

30from hyper_parallel.codegen.inline.meta_plan import normalize_inline_meta 

31from hyper_parallel.codegen.inline.definition_edits import drop_shadowed_definitions 

32from hyper_parallel.codegen.inline.patch_engine import apply_patch_set 

33from hyper_parallel.codegen.inline.framework_spec import ( 

34 replacement_spec_for as replacement_spec, 

35 strategy_spec_for as strategy_spec, 

36) 

37from hyper_parallel.codegen.inline.strategy_pass import build_strategy_patches 

38from hyper_parallel.codegen.inline.observations import collect_source_observations 

39from hyper_parallel.codegen.inline.source_plan import build_source_patch_plan 

40 

41 

42def render_inline_modeling( 

43 source_text: str, 

44 meta: Any, 

45 model_type: str | None = None, 

46 *, 

47 patch_plan: Any = None, 

48) -> str: 

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

50 

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

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

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

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

55 

56 Args: 

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

58 meta: Frozen plan metadata recorded by the generator. 

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

60 

61 Returns: 

62 The rendered modeling source. 

63 """ 

64 

65 rendered, _ = render_inline_modeling_with_plan( 

66 source_text, 

67 meta, 

68 model_type, 

69 patch_plan=patch_plan, 

70 ) 

71 return rendered 

72 

73 

74def render_inline_modeling_with_plan( 

75 source_text: str, 

76 meta: Any, 

77 model_type: str | None = None, 

78 *, 

79 patch_plan: Any = None, 

80) -> tuple[str, Any]: 

81 """Render source and return the independently normalized emission plan.""" 

82 rules = collect_source_observations(meta) 

83 if not rules: 

84 return source_text, meta 

85 patches = patch_plan 

86 recorded_local_compute = bool( 

87 getattr(patches, "local_computes", ()) if patches is not None else () 

88 ) 

89 strategy_rules = ( 

90 tuple(replace(rule, local_compute_target=None) for rule in rules) 

91 if recorded_local_compute 

92 else rules 

93 ) 

94 _require_inline_coverage(strategy_rules, model_type) 

95 patch_set = build_source_patch_plan( 

96 rules, 

97 strategy_rules, 

98 model_type, 

99 _observed_boundary_classes(meta, patch_plan), 

100 getattr(meta, "external_state_classes", None), 

101 ) 

102 _add_recorded_forward_host_imports(source_text, patch_set, patch_plan) 

103 _materialize_imported_forward_hosts(source_text, patch_set) 

104 rendered = apply_patch_set(source_text, patch_set) 

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

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

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

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

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

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

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

112 # on purpose) keep their definitions. 

113 rendered = drop_shadowed_definitions( 

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

115 ) 

116 normalized_meta = normalize_inline_meta(meta, strategy_rules, model_type) 

117 return rendered, normalized_meta 

118 

119 

120def _add_recorded_forward_host_imports( 

121 source_text: str, 

122 patch_set: InlinePatchSet, 

123 patch_plan: Any, 

124) -> None: 

125 """Expose observed replacement hosts needed by a forward rewrite. 

126 

127 A replacement application can be observed after the root model has built, 

128 while its constructor belongs to an imported decoder rather than the copied 

129 top-level source. In that case the frozen replacement record is the source 

130 of truth for the concrete host type. Add its real import so the generic 

131 imported-host materializer can emit the CP dispatch class. 

132 """ 

133 source_classes = { 

134 node.name 

135 for node in ast.parse(source_text).body 

136 if isinstance(node, ast.ClassDef) 

137 } 

138 imported_names = { 

139 name 

140 for item in patch_set.imports 

141 if not item.raw 

142 for name in item.names 

143 } 

144 replacements = ( 

145 getattr(patch_plan, "module_replacements", ()) 

146 if patch_plan is not None 

147 else () 

148 ) 

149 replacement_types = { 

150 record.replacement_type.__name__: record.replacement_type 

151 for record in replacements 

152 } 

153 for patch in patch_set.forward_extracts: 

154 if patch.class_name in source_classes or patch.class_name in imported_names: 

155 continue 

156 replacement_type = replacement_types.get(patch.class_name) 

157 if replacement_type is None: 

158 continue 

159 patch_set.imports.append( 

160 ImportPatch(replacement_type.__module__, (replacement_type.__name__,)) 

161 ) 

162 imported_names.add(patch.class_name) 

163 

164 

165def _observed_boundary_classes(meta: Any, patch_plan: Any) -> dict[str, str]: 

166 """Return boundary owners after native inner-wrapper target resolution.""" 

167 classes = dict(getattr(meta, "boundary_classes", None) or {}) 

168 wrappers = ( 

169 getattr(patch_plan, "inner_wrappers", ()) 

170 if patch_plan is not None 

171 else () 

172 ) 

173 for record in wrappers: 

174 target_type = getattr(record, "target_type", None) 

175 if target_type is None: 

176 continue 

177 target_fqn = ( 

178 record.module_fqn 

179 if record.target_fqn == "self" 

180 else f"{record.module_fqn}.{record.target_fqn}" 

181 ) 

182 classes[target_fqn] = target_type.__name__ 

183 return classes 

184 

185 

186def _materialize_imported_forward_hosts( 

187 source_text: str, patch_set: InlinePatchSet 

188) -> None: 

189 """Render same-name subclasses when a strategy host is an imported component.""" 

190 source_classes = { 

191 node.name 

192 for node in ast.parse(source_text).body 

193 if isinstance(node, ast.ClassDef) 

194 } 

195 remaining = [] 

196 rendered: set[str] = set() 

197 for patch in patch_set.forward_extracts: 

198 if patch.class_name in source_classes: 

199 remaining.append(patch) 

200 continue 

201 import_index = next( 

202 ( 

203 index 

204 for index, item in enumerate(patch_set.imports) 

205 if not item.raw and patch.class_name in item.names 

206 ), 

207 None, 

208 ) 

209 if import_index is None: 

210 remaining.append(patch) 

211 continue 

212 if patch.class_name in rendered: 

213 continue 

214 rendered.add(patch.class_name) 

215 imported = patch_set.imports[import_index] 

216 other_names = tuple(name for name in imported.names if name != patch.class_name) 

217 alias = f"_{patch.class_name}Base" 

218 replacement_imports = [] 

219 if other_names: 

220 replacement_imports.append(ImportPatch(imported.module, other_names)) 

221 replacement_imports.append(ImportPatch( 

222 module="", 

223 names=(), 

224 raw=f"from {imported.module} import {patch.class_name} as {alias}", 

225 )) 

226 patch_set.imports[import_index:import_index + 1] = replacement_imports 

227 body = patch.body.replace( 

228 "return self._forward_impl(__hp_forward_args__)", 

229 "return super().forward(__hp_forward_args__)", 

230 ).replace("__hp_forward_args__", "*args, **kwargs") 

231 snippet = ( 

232 f"class {patch.class_name}({alias}):\n" 

233 " \"\"\"Generated source-visible parallel form.\"\"\"\n\n" 

234 " cp_enable = False\n\n" 

235 " def forward(self, *args, **kwargs):\n" 

236 f"{textwrap.indent(body.strip(), ' ')}\n" 

237 ) 

238 patch_set.module_snippets.append(ModuleSnippetPatch(snippet)) 

239 patch_set.forward_extracts = remaining 

240 

241 

242def render_inline_strategies_for_source_unit( 

243 source_text: str, 

244 meta: Any, 

245 model_type: str | None = None, 

246 *, 

247 patch_plan: Any = None, 

248) -> str: 

249 """Render only strategy forwards owned by one sibling source unit.""" 

250 rules = collect_source_observations(meta) 

251 if not rules: 

252 return source_text 

253 recorded_local_compute = bool( 

254 getattr(patch_plan, "local_computes", ()) 

255 if patch_plan is not None 

256 else () 

257 ) 

258 strategy_rules = ( 

259 tuple(replace(rule, local_compute_target=None) for rule in rules) 

260 if recorded_local_compute 

261 else rules 

262 ) 

263 _require_inline_coverage(strategy_rules, model_type) 

264 patches = build_strategy_patches( 

265 strategy_rules, 

266 model_type, 

267 _observed_boundary_classes(meta, patch_plan), 

268 getattr(meta, "external_state_classes", None), 

269 ) 

270 source_classes = { 

271 node.name 

272 for node in ast.parse(source_text).body 

273 if isinstance(node, ast.ClassDef) 

274 } 

275 patches.forward_extracts = [ 

276 patch for patch in patches.forward_extracts 

277 if patch.class_name in source_classes 

278 ] 

279 if not patches.forward_extracts: 

280 return source_text 

281 patches.module_snippets.clear() 

282 patches.constructor_replaces.clear() 

283 patches.class_removals.clear() 

284 return apply_patch_set(source_text, patches) 

285 

286 

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

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

289 

290 Args: 

291 text: Rendered modeling source. 

292 

293 Returns: 

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

295 """ 

296 

297 names: set[str] = set() 

298 try: 

299 tree = ast.parse(text) 

300 except SyntaxError: 

301 return names 

302 for node in ast.walk(tree): 

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

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

305 return names 

306 

307 

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

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

310 

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

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

313 only those definitions may be dropped. 

314 

315 Args: 

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

317 

318 Returns: 

319 The set of decorated top-level definition names. 

320 """ 

321 

322 try: 

323 tree = ast.parse(text) 

324 except SyntaxError: 

325 return set() 

326 return { 

327 node.name 

328 for node in tree.body 

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

330 and node.decorator_list 

331 } 

332 

333 

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

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

336 

337 Structural injection discipline: the decorator stamps the factory with 

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

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

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

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

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

343 """ 

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

345 

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

347 if not module_name or not attr: 

348 return False 

349 try: 

350 module = importlib.import_module(module_name) 

351 except ImportError: 

352 return False 

353 factory = getattr(module, attr, None) 

354 if not callable(factory): 

355 return False 

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

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

358 

359 

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

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

362 

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

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

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

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

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

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

369 from hiding the next gap. 

370 """ 

371 

372 uncovered: list[str] = [] 

373 for rule in rules: 

374 if ( 

375 rule.replace_target is not None 

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

377 ): 

378 uncovered.append(rule.replace_target) 

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

380 if target is None: 

381 continue 

382 if strategy_spec( 

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

384 ) is not None: 

385 continue 

386 # An inner_wrapper target has no runtime channel and stays 

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

388 # @local_compute decoration check. 

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

390 continue 

391 uncovered.append(target) 

392 if uncovered: 

393 raise RuntimeError( 

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

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

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

397 "names no @local_compute factory" 

398 )