Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / codegen / plan / spec_fields.py: 100%

21 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"""Single source of truth: which ``ModuleShardingSpec`` fields cross the meta boundary. 

16 

17``freeze_param_plan`` / ``freeze_injections`` (plan -> meta, in 

18``codegen/plan/freeze.py``) and ``_rebuild_live_plan_from_meta`` 

19(meta -> plan, in ``codegen/runtime.py``) both walk this table, so a spec 

20field can no longer be frozen-but-never-rebuilt (or the reverse): adding a 

21field to ``ModuleShardingSpec`` without classifying it here fails the 

22reflection test in ``tests/codegen/test_spec_field_table.py``. 

23 

24The table is pure data (no torch, no imports from freeze/runtime — both 

25consume it, so an import here would be circular). Each entry names the 

26spec attribute, the meta key it serializes to, the meta section it belongs 

27to, and the serializer/deserializer *kind* the two sides dispatch on. The 

28kind also fixes the write condition — ``is not None`` (an explicit empty 

29dict/list crosses the boundary; this is the params={} case). 

30 

31Divergences this table replaces (found 2026-09-22, all closed here): 

32- ``tp_divide_attrs`` was never frozen, so a rebuilt spec silently lost the 

33 user's YAML declaration and ``maybe_update_head_counts`` fell back to the 

34 head-sharded heuristic only; 

35- freeze wrote ``params`` under a truthy check, dropping an explicit empty 

36 dict and leaving the rebuilt spec with ``params=None`` where Phase A 

37 (``_shard_planned_parameters``) expects a mapping; 

38- freeze read ``getattr(spec, "needs_cp_attn")`` — the real field is 

39 ``_needs_cp_attn``, so the lookup was dead code; the field is now 

40 explicitly EXEMPT (fixing the name would add a key to every existing 

41 artifact's meta and shift bytes; see the table comment). 

42""" 

43from __future__ import annotations 

44 

45from dataclasses import dataclass 

46 

47#: Frozen into ``meta.param_plan[fqn]`` entries (freeze_param_plan). 

48GROUP_PARAM_PLAN = "param_plan" 

49#: Frozen into ``meta.injections`` rules (freeze_injections). 

50GROUP_INJECTIONS = "injections" 

51#: Never crosses the meta boundary (planner-internal / apply-time only). 

52GROUP_EXEMPT = "exempt" 

53 

54 

55@dataclass(frozen=True) 

56class SpecField: 

57 """One ``ModuleShardingSpec`` field's freeze/rebuild contract. 

58 

59 Attributes: 

60 name: Attribute on ``ModuleShardingSpec`` (``"_ep_stack"``). 

61 key: Key in the frozen meta (``"ep_stack"``); ``""`` for exempt. 

62 group: Meta section — one of the ``GROUP_*`` constants. 

63 freeze: Freeze-side serializer kind; ``""`` for exempt fields. 

64 rebuild: Rebuild-side deserializer kind; ``""`` for exempt fields. 

65 post_init: The dataclass field is ``init=False`` (planner-internal 

66 output like ``_deferred_bias_params``) — the rebuild sets it 

67 via ``setattr`` after construction instead of a constructor 

68 kwarg. 

69 

70 Serializer kinds (freeze side, dispatched in freeze.py): 

71 

72 - ``placement_map``: per-param -> per-axis placements, each via 

73 ``placement_to_string``. Written when the value ``is not None`` — 

74 an explicit ``params={}`` ("shards nothing") survives the boundary. 

75 - ``named_placements``: ``named_placement_to_dict``. Written when the 

76 value ``is not None``. 

77 - ``name_list`` / ``attr_list``: ``list(value)`` copy. ``name_list`` 

78 is written only when truthy (legacy out_names behavior); 

79 ``attr_list`` when ``is not None`` (an explicit ``[]`` clears an 

80 inherited glob declaration and must not be lost). 

81 - ``scalar``: value passes through unchanged. Written when the value 

82 ``is not None``. 

83 - ``value_to_dict``: ``_value_to_dict`` (Target/Placement/callable -> 

84 JSON-safe). Written when the value ``is not None``. 

85 - ``plain``: value passes through unchanged (string fields). 

86 - ``dict_copy``: ``dict(value)``. Written when truthy. 

87 - ``int_truthy``: value passes through. Written when truthy (0 is the 

88 "inactive" default). 

89 

90 Deserializer kinds (rebuild side, dispatched in runtime.py): 

91 ``placement_map`` / ``named_placements`` / ``name_list`` / ``attr_list`` 

92 mirror their freeze kinds; ``bool_default_true`` restores ``is_boundary`` 

93 with a ``True`` default for pre-flag metas; ``opt_scalar`` reads 

94 ``entry.get(key)``; ``injection_target`` re-resolves a serialized 

95 Target through ``_injection_target``. 

96 """ 

97 

98 name: str 

99 key: str 

100 group: str 

101 freeze: str = "" 

102 rebuild: str = "" 

103 post_init: bool = False 

104 

105 

106#: Every ``ModuleShardingSpec`` field, classified. Order is load-bearing: 

107#: it is the insertion order of keys in frozen entries, so it must match 

108#: the legacy hand-written freeze order (params -> in/out placements -> 

109#: out_names -> tp_divide_attrs -> flags; injections keep the legacy 

110#: inner_* -> ep_* order) to keep regenerated artifacts byte-stable. 

111SPEC_FIELDS: tuple[SpecField, ...] = ( 

112 # ── Frozen into meta.param_plan[fqn] ── 

113 SpecField("params", "params", GROUP_PARAM_PLAN, 

114 freeze="placement_map", rebuild="placement_map"), 

115 SpecField("in_src", "in_src", GROUP_PARAM_PLAN, 

116 freeze="named_placements", rebuild="named_placements"), 

117 SpecField("in_dst", "in_dst", GROUP_PARAM_PLAN, 

118 freeze="named_placements", rebuild="named_placements"), 

119 SpecField("out_src", "out_src", GROUP_PARAM_PLAN, 

120 freeze="named_placements", rebuild="named_placements"), 

121 SpecField("out_dst", "out_dst", GROUP_PARAM_PLAN, 

122 freeze="named_placements", rebuild="named_placements"), 

123 # out_names: the declared output *order* is separate from the out_src 

124 # dict's serialized key order — the runtime resolves tuple indices 

125 # against the spec's declared order (declared_out_names prefers 

126 # entry["out_names"]), not a sorted order that could swap e.g. 

127 # hidden/aux and redistribute the wrong output. 

128 SpecField("out_names", "out_names", GROUP_PARAM_PLAN, 

129 freeze="name_list", rebuild="name_list"), 

130 SpecField("tp_divide_attrs", "tp_divide_attrs", GROUP_PARAM_PLAN, 

131 freeze="attr_list", rebuild="attr_list"), 

132 SpecField("is_boundary", "is_boundary", GROUP_PARAM_PLAN, 

133 freeze="scalar", rebuild="bool_default_true"), 

134 SpecField("region_dispatch", "region_dispatch", GROUP_PARAM_PLAN, 

135 freeze="scalar", rebuild="opt_scalar"), 

136 # _deferred_bias_params: D-22 (rowwise bias defer) — the planner-computed 

137 # bias param paths (e.g. ("o_proj.bias",)) whose addition is deferred 

138 # until AFTER the boundary exit TP reduction (Megatron 

139 # RowParallelLinear semantics). Frozen so the emitted forward can 

140 # call ``self._hp_deferred_bias(outputs)`` at the exit and the 

141 # install path can run the native suppression/restore pair 

142 # (``_install_bias_suppression`` / ``_maybe_add_deferred_biases``). 

143 # ``init=False`` on the dataclass — rebuilt via setattr (post_init). 

144 SpecField("_deferred_bias_params", "deferred_bias_params", GROUP_PARAM_PLAN, 

145 freeze="name_list", rebuild="tuple_empty", post_init=True), 

146 # skip_source_lowering: codegen opt-out of the class-level source 

147 # forward rewrite — set by meta normalization after freeze 

148 # (inline/meta_plan._mark_shared_class_boundaries) when the class is 

149 # shared between boundary and non-boundary instances. The 

150 # int_truthy freeze kind keeps unmarked artifacts byte-stable (only 

151 # an explicit True is written); opt_scalar rebuild restores it onto 

152 # the spec so freeze(rebuild(meta)) stays symmetric. 

153 SpecField("skip_source_lowering", "skip_source_lowering", GROUP_PARAM_PLAN, 

154 freeze="int_truthy", rebuild="opt_scalar"), 

155 # ── Frozen into meta.injections rules ── 

156 SpecField("inner_wrapper", "inner_wrapper", GROUP_INJECTIONS, 

157 freeze="value_to_dict", rebuild="injection_target"), 

158 SpecField("inner_target", "inner_target", GROUP_INJECTIONS, 

159 freeze="plain", rebuild="opt_scalar"), 

160 SpecField("inner_out_src", "inner_out_src", GROUP_INJECTIONS, 

161 freeze="value_to_dict", rebuild="injection_target"), 

162 SpecField("local_compute_fn", "local_compute_fn", GROUP_INJECTIONS, 

163 freeze="value_to_dict", rebuild="injection_target"), 

164 SpecField("_ep_stack", "ep_stack", GROUP_INJECTIONS, 

165 freeze="dict_copy", rebuild="dict_copy"), 

166 SpecField("_ep_size", "ep_size", GROUP_INJECTIONS, 

167 freeze="int_truthy", rebuild="int_truthy"), 

168 # ── Exempt: never crosses the meta boundary ── 

169 # _tp_local_attr_plan: planner-derived from tp_divide_attrs + module 

170 # structure (TpLocalAttrPlan, not JSON-safe). A rebuilt spec leaves 

171 # it None and maybe_update_head_counts takes its backward-compatible 

172 # path: auto attrs via the _is_head_sharded heuristic, user attrs via 

173 # the rebuilt tp_divide_attrs — both sources survive. 

174 SpecField("_tp_local_attr_plan", "", GROUP_EXEMPT), 

175 # _is_terminal: validate-mode propagation marker, not production state. 

176 SpecField("_is_terminal", "", GROUP_EXEMPT), 

177 # _needs_cp_attn: apply-time preflight metadata (attention boundary 

178 # under cp>1 without inner_wrapper fails fast in apply_sharding_plan 

179 # — a path codegen's rebuild does not take). The legacy freeze loop 

180 # read "needs_cp_attn" (no underscore), which never exists, so no 

181 # artifact meta carries the key; fixing the name would shift bytes 

182 # for every existing artifact, so the field stays exempt. 

183 SpecField("_needs_cp_attn", "", GROUP_EXEMPT), 

184 # _resolved_inner_wrapper / _resolved_inner_target: written back by the 

185 # applier for introspection; meaningless at generation time. 

186 SpecField("_resolved_inner_wrapper", "", GROUP_EXEMPT), 

187 SpecField("_resolved_inner_target", "", GROUP_EXEMPT), 

188) 

189 

190 

191def fields_for_group(group: str) -> tuple[SpecField, ...]: 

192 """Return the table entries for one ``GROUP_*`` section, in table order.""" 

193 return tuple(field for field in SPEC_FIELDS if field.group == group) 

194 

195 

196#: The param_plan section (``meta.param_plan[fqn]`` entry keys). 

197PARAM_PLAN_SPEC_FIELDS: tuple[SpecField, ...] = fields_for_group(GROUP_PARAM_PLAN) 

198 

199#: The injections section (``meta.injections`` rule keys, minus "match"). 

200INJECTION_SPEC_FIELDS: tuple[SpecField, ...] = fields_for_group(GROUP_INJECTIONS) 

201 

202#: ``init=False`` dataclass fields in the frozen groups — the rebuild sets 

203#: them via ``setattr`` after constructing the spec. 

204POST_INIT_SPEC_FIELDS: tuple[SpecField, ...] = tuple( 

205 field for field in SPEC_FIELDS if field.post_init 

206) 

207 

208#: Every meta key that may appear in a frozen entry/rule (both groups). 

209FROZEN_SPEC_KEYS: frozenset[str] = frozenset( 

210 field.key for field in SPEC_FIELDS if field.key 

211) 

212 

213 

214__all__ = [ 

215 "FROZEN_SPEC_KEYS", 

216 "GROUP_EXEMPT", 

217 "GROUP_INJECTIONS", 

218 "GROUP_PARAM_PLAN", 

219 "INJECTION_SPEC_FIELDS", 

220 "PARAM_PLAN_SPEC_FIELDS", 

221 "POST_INIT_SPEC_FIELDS", 

222 "SPEC_FIELDS", 

223 "SpecField", 

224 "fields_for_group", 

225]