Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed / recipe_spec.py: 95%

132 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +0800

1# Copyright 2025-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"""recipe_spec: recipe input contract and placement DSL for the dual-mode DTensor strategy. 

16 

17Contains the user-visible plan/spec data model (05 §3.1/§3.2 canonical): 

18- ``MeshAxisName``: mesh dimension name enum (canonical definition, imported and reused by later docs such as 06); 

19- ``NamedPlacement``: alias for ``dict[MeshAxisName, Placement]``; 

20- ``ModuleShardingSpec``: per-module I/O contract; 

21- ``PlacementMismatchError``: error for placement declarations inconsistent with DTensor propagation; 

22- ``resolve_placements`` / ``parse_placement`` / ``parse_named_placement`` / ``_normalize_out_fields``: 

23 placement utilities; 

24- the injection DSL (``forward_replacement`` / ``local_compute`` / 

25 ``inner_wrapper`` decorators, 

26 ``InjectionMeta``, ``require_injection_meta``): the recipe-side input 

27 contract of the explicit injection mechanism. 

28 

29The model-level :class:`ShardingPlan` lives in ``distributed/plan.py``; the 

30semantic-role templates live in ``distributed/_builder/default_templates.py``. 

31""" 

32 

33import inspect 

34import logging 

35import re 

36from dataclasses import dataclass, field 

37from enum import Enum 

38from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Tuple, Union 

39 

40from hyper_parallel.core.dtensor.placement_types import ( 

41 Partial, 

42 Placement, 

43 Replicate, 

44 Shard, 

45) 

46 

47if TYPE_CHECKING: 

48 from hyper_parallel.distributed.tensor_parallel.head_count import TpLocalAttrPlan 

49 

50logger = logging.getLogger(__name__) 

51 

52 

53class MeshAxisName(str, Enum): 

54 """Canonical enum of mesh dimension names. 

55 

56 A str enum: directly comparable to plain strings like "tp" and usable as 

57 a dict key. 

58 """ 

59 TP = "tp" 

60 CP = "cp" 

61 EP = "ep" 

62 PP = "pp" 

63 DP = "dp" 

64 DP_REPLICATE = "dp_replicate" 

65 DP_SHARD = "dp_shard" 

66 DP_CP = "dp_cp" 

67 EP_SHARD = "ep_shard" 

68 

69 

70# Shorthand aliases for the {TP: ..., CP: ..., EP: ...} literals in templates and examples. 

71# str-enum keys interoperate with plain string keys like "tp" (hash/eq are identical). 

72TP = MeshAxisName.TP 

73CP = MeshAxisName.CP 

74EP = MeshAxisName.EP 

75DP = MeshAxisName.DP 

76 

77# NamedPlacement = {MeshAxisName: Placement}. 

78# The key is a mesh dimension name; the N in Shard(N) of a value is a tensor dimension index (05 §3.2.1). 

79NamedPlacement = Dict[MeshAxisName, Placement] 

80 

81 

82class PlacementMismatchError(ValueError): 

83 """DTensor propagation result is inconsistent with the ModuleShardingSpec declaration (05 §5.3).""" 

84 

85 def __init__(self, module_name: str, expected: Any, actual: Any, stage: str) -> None: 

86 """Initialize the mismatch error. 

87 

88 Args: 

89 module_name: FQN of the module whose placement mismatched. 

90 expected: Placement declared by the ShardingConfig. 

91 actual: Placement produced by DTensor propagation. 

92 stage: Which contract stage mismatched (e.g. "in_dst"/"out_src"). 

93 """ 

94 self.module_name = module_name 

95 self.expected = expected 

96 self.actual = actual 

97 self.stage = stage 

98 super().__init__( 

99 f"[{module_name}] {stage} placement mismatch:\n" 

100 f" Expected (from ShardingConfig.{stage}): {expected}\n" 

101 f" Actual (from DTensor propagation): {actual}\n" 

102 f" → Check the ShardingConfig for this module." 

103 ) 

104 

105 

106@dataclass 

107class ModuleShardingSpec: 

108 """Complete DTensor contract for a single module (05 §3.2). 

109 

110 The four placement fields form the complete I/O contract — the runtime does no 

111 inference and executes exactly as declared: 

112 

113 in_src: placement of the input when it arrives at the module boundary 

114 (from the output of an upstream module or the dataloader) 

115 in_dst: placement required by the module's internal computation 

116 (a mismatch triggers communication) 

117 out_src: placement naturally produced by the module's internal computation 

118 (used by validate mode) 

119 out_dst: placement expected by downstream modules (a mismatch triggers 

120 communication) 

121 

122 plan_overrides input side only: the contract fields (params/in_src/in_dst/ 

123 out_src/out_dst/out_names) follow one rule — **"unset inherits, set wins"** 

124 (2026-08-05): ``None`` (unset) or the sentinel ``"auto"`` means 

125 inherit-from-template (merge only); an explicit value IS the final value, 

126 including the empty dict ``{}`` (explicit "no sharding / no contract" 

127 — the ViT ``params={}`` pattern); the sentinel ``"none"`` is a readable 

128 alias for clearing (``{}`` for params/in_*, ``None`` for out_*). The 

129 planner resolves sentinels and normalizes ``None`` at merge/insert time; 

130 a spec inside a finished ShardingPlan always carries concrete dicts 

131 (params/in_src/in_dst) or Optional dicts (out_*). 

132 """ 

133 # ── Parameter sharding: submodule path → NamedPlacement ── 

134 # Default None = "not declared" (merge: inherit); an explicit {} means 

135 # "this boundary shards nothing" (I/O-stitch-only citizen). 

136 params: Optional[Dict[str, NamedPlacement]] = None 

137 

138 # ── Input contract ── 

139 in_src: Optional[Dict[str, NamedPlacement]] = None 

140 in_dst: Optional[Dict[str, NamedPlacement]] = None 

141 

142 # ── Output contract ── 

143 # out_src=None: no src validation; out_dst=None: output needs no redistribution. 

144 # Single-output modules use {"output": NamedPlacement}; the scalar shorthand 

145 # {TP: ...} is wrapped into {"output": ...} during the normalization phase 

146 # (_normalize_out_fields). 

147 out_src: Optional[Dict[str, NamedPlacement]] = None 

148 out_dst: Optional[Dict[str, NamedPlacement]] = None 

149 # out_names: output-name ordering for multi-output modules (returning a tuple), 

150 # used to map the keys of out_src/out_dst to tuple positions 

151 # (RedistOp.arg_index). Defaults to the key order of out_src. 

152 out_names: Optional[List[str]] = None 

153 

154 # TP-local module-instance attributes explicitly declared by the user. 

155 # None means "not declared" during override merge; [] explicitly clears 

156 # an inherited glob declaration. The planner normalizes this field into 

157 # _tp_local_attr_plan after all overrides have been merged. 

158 tp_divide_attrs: Optional[List[str]] = None 

159 

160 # Internal planner output. init=False keeps it out of both the public 

161 # programmatic constructor and the YAML transport interface. 

162 # Annotated as a string: TpLocalAttrPlan lives in 

163 # tensor_parallel/head_count.py, which imports resolve_placements from 

164 # this module — a runtime import here would be circular. 

165 _tp_local_attr_plan: Optional["TpLocalAttrPlan"] = field( 

166 default=None, init=False, repr=False, compare=False, 

167 ) 

168 

169 # D-22 (rowwise bias defer): planner-computed tuple of bias param paths 

170 # (relative to the boundary module, e.g. ("o_proj.bias",)) whose addition 

171 # is deferred until AFTER the boundary exit TP reduction (Megatron 

172 # RowParallelLinear semantics: the bias never enters F.linear inside the 

173 # region, so the Partial reduce counts it exactly once). Internal — built 

174 # by ShardingPlanner._finalize_deferred_biases after all overrides are 

175 # merged (detection is anchored on the FINAL spec declarations, so 

176 # derived / merge / insert / derive=False specs share one code path); 

177 # user spec objects never carry this field. 

178 _deferred_bias_params: Tuple[str, ...] = field( 

179 default=(), init=False, repr=False, compare=False, 

180 ) 

181 

182 # ── Boundary flag ── 

183 is_boundary: bool = True 

184 

185 # ── Structural flags (user-configurable) ── 

186 # ┌─────────────────────────────────────────────────────────────────┐ 

187 # │ The user extension-point interfaces: region_dispatch / │ 

188 # │ local_compute_fn / inner_target / inner_wrapper │ 

189 # │ │ 

190 # │ One axiom governs every boundary: region computation is │ 

191 # │ dispatch-through by default (under validate: DTensors enter │ 

192 # │ directly, strategy propagation, real out_src validation); what │ 

193 # │ cannot dispatch explicitly declares region_dispatch=False │ 

194 # │ (skeleton black-box: to_local -> local execution -> │ 

195 # │ declarative re-wrap). │ 

196 # │ │ 

197 # │ [local-region family] (module level: skeleton unchanged, │ 

198 # │ content swapped) │ 

199 # │ region_dispatch / local_compute_fn │ 

200 # │ The skeleton = _wrap_local_region_forward: boundary │ 

201 # │ entry/exit stitching + local compute + validate dual-mode │ 

202 # │ fault tolerance (to_local/_temp_local_params/from_local │ 

203 # │ re-wrapping), shared by both modes. Whether a module runs │ 

204 # │ through the skeleton is **derived from a single resolution │ 

205 # │ chain** (not a stored bool) by _resolve_local_compute_fn: │ 

206 # │ chain link 1 local_compute_fn (user-defined computation: │ 

207 # │ callable, or a factory Target built at apply │ 

208 # │ time — e.g. a shipped EP archetype factory │ 

209 # │ (recipes.qwen2moe_ep_compute_fn)) │ 

210 # │ chain link 2 region_dispatch=False (the module's own │ 

211 # │ forward can't dispatch — it IS the compute) │ 

212 # │ none present → None (skeleton not used) │ 

213 # │ │ 

214 # │ ★ EXPLICIT INJECTION (since the rework): the shipped EP │ 

215 # │ compute is NEVER auto-injected. The planner only shards the │ 

216 # │ expert params ({EP: Shard(0)} + _ep_size/_ep_stack metadata); │ 

217 # │ the compute must be declared via plan_overrides / direct │ 

218 # │ spec assignment, WITH an explicit region_dispatch (no │ 

219 # │ default for injections — the tutorial teaches False for │ 

220 # │ CP/EP-style comm-carrying injections and why). An EP-sharded │ 

221 # │ boundary whose chain resolves to None fails fast at apply │ 

222 # │ time (_preflight_compute_injection). HF-native MoE layouts │ 

223 # │ (per-expert / batched) get the template's region_dispatch │ 

224 # │ CLEARED by the planner (their forward is not EP-aware); │ 

225 # │ custom-named pre-stacked modules (w1/w2/w3) keep False │ 

226 # │ (EP-aware by construction). │ 

227 # │ │ 

228 # │ [inner-wrap family] (submodule level: content and wrapping both │ 

229 # │ replaced) │ 

230 # │ inner_target / inner_wrapper │ 

231 # │ The mechanism is a generic "locate an inner submodule + │ 

232 # │ replace its forward": inner_target answers "replace whom", │ 

233 # │ inner_wrapper answers "replace with what". It is NOT CP-gated │ 

234 # │ (declaration == application); the shipped reference domain is │ 

235 # │ CP (K/V all-gather), and the four INNER_WRAPPER_REGISTRY │ 

236 # │ entries still require an active cp axis (fail-fast otherwise) │ 

237 # │ — custom wrappers receive cp_mesh=None and own their │ 

238 # │ semantics. │ 

239 # │ │ 

240 # │ ★ EXPLICIT INJECTION (since the rework): NO heuristic │ 

241 # │ dispatch. inner_wrapper must be declared explicitly: a │ 

242 # │ INNER_WRAPPER_REGISTRY name ("sdpa_qkv"/"sdpa_hf"/"flex_qkv"/ │ 

243 # │ "flex_hf"), a callable fn(target_module, cp_mesh), or a │ 

244 # │ Target pointing at a wrapper fn (e.g. │ 

245 # │ wrappers.sdpa_hf_cp_wrapper). _needs_cp_attn is now pure │ 

246 # │ METADATA (template recognition) — it no longer triggers any │ 

247 # │ injection; an attention boundary under an active cp mesh │ 

248 # │ without inner_wrapper fails fast at apply time. │ 

249 # │ │ 

250 # │ The two families are orthogonal and composable: the same module │ 

251 # │ may declare both inner_* (wrapping of an inner attention) and │ 

252 # │ local_* (module-level skeleton). │ 

253 # └─────────────────────────────────────────────────────────────────┘ 

254 # 

255 # region_dispatch: **the validate execution-mode declaration for the 

256 # region computation (whatever its source: the module's own forward or 

257 # an injected function)** — one axiom across the whole framework: region 

258 # computation is dispatch-through by default; what cannot dispatch 

259 # explicitly declares False. 

260 # - None (no injection): an ordinary boundary — under validate, DTensors 

261 # go straight into the original forward, dispatch-through + real 

262 # out_src validation (the axiom's default, not this field's "default 

263 # value"); 

264 # - False (no injection): the module's own forward cannot dispatch 

265 # (data-dependent logic inside the forward, e.g. the a2a of an 

266 # in-house EP-aware MoE) → takes the local-region skeleton, compute = 

267 # the module's own forward (derived and filled by the planner when the 

268 # template recognizes such a module, visible in the logs); 

269 # - With injection (local_compute_fn / inner_wrapper non-None): **must be 

270 # declared explicitly** (no default) — True = the injected code is pure 

271 # standard ops, dispatchable (validate dispatch-through + real 

272 # out_src/inner_out_src validation); False = the injected code contains 

273 # communication / custom kernels (validate black-box local execution + 

274 # declarative re-wrap). Missing → fail-fast at apply time; 

275 # - True (no injection): a redundant declaration → fail-fast (an 

276 # ordinary boundary is inherently dispatch-through). 

277 # production is not affected by this field (region is always local 

278 # passthrough). 

279 region_dispatch: Optional[bool] = None 

280 

281 # ── inner-wrap custom entry points (user-configurable, 05 §4.4.2/§8.6) ── 

282 # inner_target: **pure location** — names the attribute of the inner 

283 # submodule whose forward is to be replaced ("self" means the module 

284 # itself). Automatic locating (_resolve_inner_target) fails fast, in which 

285 # case the user must specify it via plan_overrides/injections. Declaring 

286 # inner_target WITHOUT inner_wrapper is an error since the rework 

287 # (location alone cannot pick a scheme — no heuristic dispatch). 

288 # inner_wrapper: **pure behavior** — selects which scheme wraps the target. 

289 # NOT CP-gated: declaration == application, whatever the parallel mode. 

290 # - str: a name in the INNER_WRAPPER_REGISTRY registry ("sdpa_qkv"/"sdpa_hf"/ 

291 # "flex_qkv"/"flex_hf", or a user-registered name); explicitly pins a 

292 # built-in scheme; an unknown name fails fast; the four SHIPPED names 

293 # are CP schemes and require an active cp axis (fail-fast otherwise); 

294 # - Callable: a fully custom wrapper, which MUST be decorated 

295 # ``@inner_wrapper`` (injection discipline, see injection.py — 

296 # undecorated callables fail fast). Context contract: 

297 # fn(target_module, mesh, tp_mesh, cp_mesh, ep_mesh) — the anchor plus 

298 # the mesh family are MANDATORY context params, ALL filled by the 

299 # framework at apply time (None for inactive axes; the user just uses 

300 # them); ``spec`` is the only optional context. The wrapper replaces 

301 # target.forward in place (use collectives.flex_cp_allgather for K/V 

302 # all-gather). The replaced forward must accept the original 

303 # forward's params (validated at apply time). region_dispatch must be 

304 # declared explicitly (see above): the injected code either targets 

305 # local tensors only (False, the adapter manages the DTensor 

306 # conversion) or is pure standard ops, dispatchable (True, validate 

307 # dispatch-through with real validation); 

308 # - Target (hyper_parallel.trainer.config.Target): a delayed wrapper 

309 # reference resolved from YAML; the referenced fn must likewise be 

310 # @inner_wrapper decorated (the shipped built-ins 

311 # wrappers.sdpa_hf_cp_wrapper etc. satisfy this contract 

312 # directly). A None return = in-place replacement done; a callable 

313 # return (also decorated) = applied as a custom wrapper. 

314 inner_target: Optional[str] = None 

315 inner_wrapper: Optional[Union[str, Callable]] = None # or Target 

316 # inner_out_src: the **explicit declaration** of the output placement for 

317 # inner-wrap case B (inner_target points at a submodule rather than 

318 # self) — the framework does zero derivation and zero guessing about the 

319 # inner output layout: 

320 # - sentinel "first_input": a layout-preserving declaration (the output 

321 # layout == the runtime layout of the first DTensor input argument; 

322 # used by attention-type wrappers; illegal for multiple outputs); 

323 # - NamedPlacement ({axis: Placement}): an explicit single-output 

324 # declaration; 

325 # - {name: NamedPlacement}: a per-name declaration for multiple outputs 

326 # (tuple positions follow the declaration key order). 

327 # Case B without a declaration → fail-fast at apply time (case A 

328 # target=self uses the boundary out_src and does not need this field). 

329 # validate does no propagation check on the inner region: declaration 

330 # errors are backstopped by post-rewrap global-shape consistency / the 

331 # boundary out_src check / numerical comparison. 

332 inner_out_src: Optional[Union[str, Dict]] = None 

333 

334 # ── local-region custom computation (user-configurable, 05 §4.4.3/§8.6) ── 

335 # local_compute_fn: the custom compute FACTORY of the local region 

336 # (chain link 1; single form since 2026-08-10 — the direct compute-fn 

337 # form was retired, mesh family is always filled, use-it-or-not): 

338 # - Callable: a ``@local_compute``-decorated factory 

339 # fn(mesh, tp_mesh, cp_mesh, ep_mesh, [module], <config keys...>) 

340 # -> compute_fn (programmatic direct pass; injection discipline: 

341 # mesh family mandatory, no defaults, no *args/**kwargs); 

342 # - Target: the same factory as a delayed reference resolved from 

343 # YAML (config keys bound by name; typo fail-fast). 

344 # The factory is built ONCE at apply time with the framework context 

345 # filled by name (mesh / tp_mesh / cp_mesh / ep_mesh mandatory — 

346 # ep_mesh is the same object the expert params were sharded on; 

347 # module optional anchor) and must RETURN the region compute fn 

348 # fn(module, *local_args) -> Tensor, whose params MUST match the 

349 # module's forward params (validated at apply time) — executed on 

350 # local tensors inside the _wrap_local_region_forward skeleton (the 

351 # skeleton handles boundary entry/exit stitching + validate dual-mode 

352 # to_local/_temp_local_params/from_local re-wrapping). Config keys 

353 # carry DATA only (function-typed config is rejected by discipline — 

354 # custom behavior = your own injected function, e.g. a MoE factory 

355 # with its router written inline). 

356 # Suitable for custom modules that want to reuse the skeleton with their 

357 # own data-dependent logic: typically a custom MoE (router not in 

358 # MOE_ROUTER_ADAPTERS / expert layout not using HF-standard naming / 

359 # hooked to a DeepEP fused dispatcher). 

360 # Priority: local_compute_fn > region_dispatch=False gate (the module's own 

361 # forward) — a single resolution chain (_resolve_local_compute_fn); 

362 # declaring it takes effect immediately: **there is no need — and it is 

363 # wrong — to also set region_dispatch=False** unless the injected fn is 

364 # itself non-dispatchable; the skeleton gate is derived from the 

365 # resolution chain (non-None means the skeleton is used). 

366 # Dual-mode convention: with region_dispatch=False the inputs are always 

367 # local tensors and the return value is a local tensor; validate's 

368 # DTensor unwrap/re-wrapping is done by the skeleton, so the compute_fn 

369 # need not be aware of the mode. With region_dispatch=True the compute 

370 # fn must be pure dispatchable ops (validate feeds DTensors straight 

371 # through and truly validates out_src). 

372 local_compute_fn: Optional[Callable] = None # or factory Target 

373 

374 # ── Internal flags (set automatically by ShardingPlanner / applier) ── 

375 _is_terminal: bool = False # marked automatically during chained propagation 

376 # _needs_cp_attn: template recognition METADATA (attention module: the 

377 # inner attention needs a CP-aware forward under an active cp mesh). Since 

378 # the explicit-injection rework it does NOT trigger any injection — it is 

379 # used only by the apply-time preflight (an attention boundary under cp>1 

380 # without inner_wrapper fails fast) and for introspection. 

381 _needs_cp_attn: bool = False 

382 # _resolved_inner_wrapper: written back by the applier after resolution (for 

383 # introspection) — the inner wrapper actually injected: 

384 # "sdpa_qkv"/"sdpa_hf"/"flex_qkv"/"flex_hf"/"custom"/<target_path>/None. 

385 _resolved_inner_wrapper: Optional[str] = None 

386 # _resolved_inner_target: written back by the applier after resolution — 

387 # the inner-wrap target actually located: the child attribute name, or 

388 # "self" when the boundary module itself is wrapped. Nothing is located 

389 # silently: the resolved target is always visible here and in the INFO log. 

390 _resolved_inner_target: Optional[str] = None 

391 # D-09 (05 §6.4.7): EP pass-through for HF-native MoE. A non-empty _ep_stack 

392 # means per-expert parameters must be pre-stacked into [E, ...] in Phase A. 

393 # Since the explicit-injection rework the compute side is NOT auto-injected: 

394 # declare local_compute_fn explicitly (e.g. Target → an EP archetype 

395 # factory such as recipes.qwen2moe_ep_compute_fn). 

396 _ep_stack: Dict[str, List[str]] = field(default_factory=dict) 

397 # D-10 (05 §6.4.8): virtual/TP-extended EP parameter-sharding marker. When 

398 # >0 this is the extended EP group size (= ep_size; the a2a communication 

399 # domain includes TP/CP ranks) and a derived expert mesh (edp, ep) owns 

400 # every parameter whose placement explicitly contains ``EP: Shard(...)``. 

401 # Routed MoE experts and sparse lookup tables share this generic source- 

402 # layout path. It drives ONLY parameter sharding/expert-mesh derivation — 

403 # matching all-to-all compute must be injected explicitly. 

404 _ep_size: int = 0 

405 

406 

407def resolve_placements( 

408 named: NamedPlacement, 

409 mesh_dim_names: Tuple[str, ...], 

410) -> List[Placement]: 

411 """Arrange placements in mesh_dim_names order, fill missing axes with Replicate().""" 

412 return [named.get(axis, Replicate()) for axis in mesh_dim_names] 

413 

414 

415_PLACEMENT_DSL_DOC = ( 

416 '"replicate" / "partial" / "shard(N)" (N is the tensor dim index, e.g. "shard(0)")') 

417 

418 

419def parse_placement(text: Any, *, path: str = "placement") -> Placement: 

420 """Parse the YAML placement DSL string into a Placement object. 

421 

422 Grammar (closed set): ``replicate`` / ``partial`` / ``shard(N)`` — case 

423 insensitive, whitespace tolerant. Anything else fails fast listing the 

424 grammar (typo self-discovery, same philosophy as 

425 ``_check_target_config_keys``). 

426 """ 

427 if not isinstance(text, str): 

428 raise ValueError( 

429 f"{path}: placement must be a string ({_PLACEMENT_DSL_DOC}), " 

430 f"got {type(text).__name__} {text!r}") 

431 normalized = re.sub(r"\s+", "", text).lower() 

432 if normalized == "replicate": 

433 return Replicate() 

434 if normalized == "partial": 

435 return Partial() 

436 match = re.fullmatch(r"shard\((\d+)\)", normalized) 

437 if match: 

438 return Shard(int(match.group(1))) 

439 raise ValueError( 

440 f"{path}: cannot parse placement {text!r} — valid grammar: {_PLACEMENT_DSL_DOC}") 

441 

442 

443def parse_named_placement(raw: Any, *, path: str = "named_placement") -> NamedPlacement: 

444 """Parse the YAML form ``{axis: placement_str}`` into a NamedPlacement. 

445 

446 Axis names stay plain strings (str-enum interop; custom mesh dims such as 

447 "encoder_dp" are legal) — validity against the actual mesh is checked at 

448 plan-merge time (ShardingPlanner._validate_override_axes), because the 

449 mesh does not exist at config time. 

450 """ 

451 if not isinstance(raw, dict) or not raw: 

452 raise ValueError( 

453 f"{path}: expected a non-empty mapping {{axis name: placement string}}, got {raw!r}") 

454 named: NamedPlacement = {} 

455 for axis, placement_text in raw.items(): 

456 if not isinstance(axis, str) or not axis: 

457 raise ValueError( 

458 f"{path}: axis name must be a non-empty string, got {axis!r}") 

459 named[axis] = parse_placement( 

460 placement_text, path=f"{path}.{axis}") 

461 return named 

462 

463 

464def _normalize_out_fields(spec: ModuleShardingSpec) -> ModuleShardingSpec: 

465 """Normalize the scalar shorthand {TP: ...} into {'output': {TP: ...}} (05 §3.5). 

466 

467 Detection heuristic: if val is a non-None dict and any of its values is not a 

468 dict, it is judged to be a scalar NamedPlacement shorthand. Idempotent — a 

469 second call on an already-normalized dict contract changes nothing. 

470 """ 

471 for attr in ("out_src", "out_dst"): 

472 val = getattr(spec, attr, None) 

473 if val and not all(isinstance(v, dict) for v in val.values()): 

474 setattr(spec, attr, {"output": dict(val)}) 

475 return spec 

476 

477 

478# ──────────────────────────────────────────────────────────────────────────── 

479# Injection DSL: template decorators for injected functions (the entry-point 

480# discipline of the explicit injection mechanism). 

481# 

482# Design principle: **an injected function is called exactly once at apply 

483# time, must declare the mesh family (filled by the framework by name; axes 

484# that are not active are filled with None), and may use them or not**. Two 

485# decorators cover all injection channels, each with a single canonical form: 

486# 

487# - ``@local_compute``: a regional-compute **factory**, 

488# ``fn(mesh, tp_mesh, cp_mesh, ep_mesh, [module], <config keys...>) 

489# -> compute_fn`` -- built once at apply time; 

490# - ``@inner_wrapper``: an inner-forward wrapper, ``fn(target_module, mesh, 

491# tp_mesh, cp_mesh, ep_mesh)`` -- returns the replacement forward (in-repo 

492# discipline; the forward rewriter installs it) or replaces 

493# ``target.forward`` in place and returns None (the external contract). 

494# 

495# Hard rules enforced by the decorators (fail-fast at import time): required 

496# context must be declared in full; context parameters must not have default 

497# values; injected functions forbid *args/**kwargs. The full design rationale 

498# is documented in docs/design/ (05 §3.7) — the runtime-layer signature 

499# validators live in ``distributed/_builder/forward_rewriter.py``. 

500# ──────────────────────────────────────────────────────────────────────────── 

501 

502# Injection kinds (meta.kind) 

503LOCAL_COMPUTE = "local_compute" # regional-compute factory (the only form of local_compute_fn) 

504INNER_WRAPPER = "inner_wrapper" # inner-forward wrapper 

505 

506# Reserved framework-context names per kind (declaring one means the 

507# framework fills it by name; configuring a key with the same name errors) 

508_MESH_FAMILY = frozenset({"mesh", "tp_mesh", "cp_mesh", "ep_mesh"}) 

509FACTORY_CONTEXT = frozenset({"module"}) | _MESH_FAMILY 

510WRAPPER_CONTEXT = frozenset({"target_module"}) | _MESH_FAMILY 

511 

512# Required context per kind (the decorators enforce full declaration -- 

513# the mesh family is always passed by the framework and the user merely 

514# uses it; the framework fills None for axes that are not active) 

515FACTORY_REQUIRED = _MESH_FAMILY 

516WRAPPER_REQUIRED = frozenset({"target_module"}) | _MESH_FAMILY 

517 

518_ALLOWED_CONTEXT = { 

519 LOCAL_COMPUTE: FACTORY_CONTEXT, # optional module anchor + mesh family 

520 INNER_WRAPPER: WRAPPER_CONTEXT, 

521} 

522_REQUIRED_CONTEXT = { 

523 LOCAL_COMPUTE: FACTORY_REQUIRED, # mesh family is required 

524 INNER_WRAPPER: WRAPPER_REQUIRED, 

525} 

526 

527_DECORATOR_NAMES = { 

528 LOCAL_COMPUTE: "@local_compute", 

529 INNER_WRAPPER: "@inner_wrapper", 

530} 

531 

532 

533@dataclass(frozen=True) 

534class InjectionMeta: 

535 """Metadata written by the decorators onto the injected function object 

536 (``fn._injection_meta``). 

537 

538 Attributes: 

539 kind: The injection kind (LOCAL_COMPUTE / INNER_WRAPPER). 

540 context: The declared framework-context keys. 

541 """ 

542 kind: str # LOCAL_COMPUTE / INNER_WRAPPER 

543 context: frozenset # declared framework-context keys 

544 

545 

546def _make_decorator(kind: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]: 

547 """Build the template decorator for one injection kind. 

548 

549 Args: 

550 kind: The injection kind (LOCAL_COMPUTE or INNER_WRAPPER). 

551 

552 Returns: 

553 A decorator that validates the injected function's signature at 

554 import time and stamps it with :class:`InjectionMeta`. 

555 """ 

556 allowed = _ALLOWED_CONTEXT[kind] 

557 required = _REQUIRED_CONTEXT[kind] 

558 

559 def decorator(fn: Callable[..., Any]) -> Callable[..., Any]: 

560 """Validate ``fn`` against the injection discipline and stamp its meta. 

561 

562 Args: 

563 fn: The injected function to decorate. 

564 

565 Returns: 

566 The same function, with ``_injection_meta`` attached. 

567 

568 Raises: 

569 TypeError: If the signature cannot be introspected, uses 

570 *args/**kwargs, gives a context parameter a default value, 

571 or omits required context parameters. 

572 """ 

573 fname = getattr(fn, "__name__", fn) 

574 try: 

575 sig = inspect.signature(fn) 

576 except (TypeError, ValueError) as exc: 

577 raise TypeError( 

578 f"{_DECORATOR_NAMES[kind]}: cannot introspect the signature of " 

579 f"{fn!r} (an injected function must be an introspectable " 

580 "plain callable)") from exc 

581 context = [] 

582 for name, p in sig.parameters.items(): 

583 if p.kind in (inspect.Parameter.VAR_POSITIONAL, 

584 inspect.Parameter.VAR_KEYWORD): 

585 raise TypeError( 

586 f"{_DECORATOR_NAMES[kind]} injected function {fname}" 

587 f" must not use *args/**kwargs (parameter {name!r}) -- " 

588 "the signature of an injected function must be an explicit " 

589 "parameter list: context is declared by name and filled by " 

590 "the framework, config keys are bound by name, and " 

591 "**kwargs would silently swallow typos (conflicting with " 

592 "the _check_target_config_keys policy)") 

593 if name in allowed: 

594 if p.default is not inspect.Parameter.empty: 

595 raise TypeError( 

596 f"{_DECORATOR_NAMES[kind]} injected function {fname}: " 

597 f"context parameter {name!r} must not have a default " 

598 "value -- context names are reserved by the framework " 

599 "and are always filled by name, so a default would " 

600 "never take effect") 

601 context.append(name) 

602 missing = sorted(required - set(context)) 

603 if missing: 

604 raise TypeError( 

605 f"{_DECORATOR_NAMES[kind]} injected function {fname} is " 

606 f"missing required context parameters {missing} -- the " 

607 "injection discipline requires explicitly receiving " 

608 f"{sorted(required)} (all filled by the framework by name at " 

609 "apply time; the user merely uses them)") 

610 fn._injection_meta = InjectionMeta(kind=kind, context=frozenset(context)) 

611 return fn 

612 

613 return decorator 

614 

615 

616local_compute = _make_decorator(LOCAL_COMPUTE) 

617inner_wrapper = _make_decorator(INNER_WRAPPER) 

618 

619 

620def forward_replacement(factory: Callable[..., Callable[..., Any]]) -> Callable[..., Callable[..., Any]]: 

621 """Declare a forward factory hosted by the local-region executor. 

622 

623 Args: 

624 factory: Called once at plan application with the mesh family; returns 

625 an unbound compute function whose first argument is the module. 

626 

627 Returns: 

628 The decorated factory with existing local-compute metadata. 

629 """ 

630 return local_compute(factory) 

631 

632 

633def require_injection_meta(fn: Callable[..., Any], kind: str, *, source: str) -> InjectionMeta: 

634 """Fetch an injected function's metadata; fail-fast (with an instructive 

635 error) if it is undecorated or of the wrong kind. 

636 

637 Args: 

638 fn: The function expected to carry injection metadata. 

639 kind: The expected injection kind. 

640 source: Call-site label prepended to error messages. 

641 

642 Returns: 

643 The :class:`InjectionMeta` attached to ``fn``. 

644 

645 Raises: 

646 TypeError: If ``fn`` lacks the template decorator or its decorator 

647 kind does not match ``kind``. 

648 """ 

649 meta = getattr(fn, "_injection_meta", None) 

650 name = getattr(fn, "__name__", fn) 

651 if meta is None: 

652 raise TypeError( 

653 f"{source}: injected function {name} is missing the " 

654 f"{_DECORATOR_NAMES[kind]} decorator -- the explicit-injection " 

655 "discipline requires every injected function to carry a template " 

656 "decorator (the decorator declares the framework context it needs " 

657 "and guarantees the signature is validatable): use @local_compute " 

658 "for the runtime fn of local_compute_fn (a regional-compute " 

659 "factory) and @inner_wrapper for inner_wrapper (exported by " 

660 "hyper_parallel.distributed)") 

661 if meta.kind != kind: 

662 raise TypeError( 

663 f"{source}: injected function {name} has the wrong decorator " 

664 f"kind -- got {_DECORATOR_NAMES[meta.kind]}, expected " 

665 f"{_DECORATOR_NAMES[kind]}") 

666 return meta