Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / codegen / meta.py: 42%

98 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"""``codegen_meta.json`` read/write and schema validation. 

16 

17The meta file is the single source of truth for an artifact bundle's 

18identity: it records the signature, YAML path + hash, source file the 

19modeling output was generated from, parallel dims, and the resolved 

20replacements/injections/param plan so a later preflight pass can detect 

21drift without re-running the whole generation. 

22""" 

23 

24from __future__ import annotations 

25 

26import json 

27import os 

28from dataclasses import asdict, dataclass, field 

29from typing import Any, Optional 

30 

31from hyper_parallel.codegen.artifact import ArtifactLayout 

32from hyper_parallel.codegen.hash import file_line_count, sha256_file 

33 

34 

35@dataclass 

36class CodegenMeta: 

37 """Serializable artifact metadata for one generated modeling bundle.""" 

38 

39 codegen_version: str 

40 signature: str 

41 yaml_path: str 

42 yaml_sha256: str 

43 source: dict[str, Any] 

44 parallel_dims: dict[str, Any] 

45 model_name: Optional[str] = None 

46 outputs: dict[str, Any] = field(default_factory=dict) 

47 injections: list[dict[str, Any]] = field(default_factory=list) 

48 skipped_overrides: list[dict[str, Any]] = field(default_factory=list) 

49 param_plan: dict[str, Any] = field(default_factory=dict) 

50 frozen_sharded_params: list[str] = field(default_factory=list) 

51 tied_pairs: list[list[str]] = field(default_factory=list) 

52 special_handlers: dict[str, str] = field(default_factory=dict) 

53 #: FQN -> owning class name for every boundary. The emitter 

54 #: groups boundary FQNs by this to rewrite one ``class.forward`` per class. 

55 boundary_classes: dict[str, str] = field(default_factory=dict) 

56 entrypoints: dict[str, str] = field(default_factory=dict) 

57 #: Sunk ``replace_module`` overrides: one JSON-safe record per 

58 #: matched target, ``{match, fqn, fqns, module_type, factory, exact_type}``. 

59 #: The inline pipeline lowers them into the generated file, and the record 

60 #: is what lets the trainer skip its own HF replacement step 

61 #: (``covered["module_overrides"]``). 

62 module_overrides: list[dict[str, Any]] = field(default_factory=list) 

63 #: Entry model class name (the architecture name, e.g. ``AnthropicV3ForCausalLM``). 

64 #: The inline pipeline resolves the model's adapter from it. 

65 model_class: Optional[str] = None 

66 #: Generated class names whose ``forward`` an inline strategy body replaces 

67 #: (the EP MoE shell), so it orchestrates through instance attributes bound 

68 #: at install time (``hp_bind_inline_state``) instead of a compiled 

69 #: boundary. Derived from structure at generation time and copied here so 

70 #: the runtime reads the artifact's own record rather than a second copy 

71 #: that has to be kept in sync by hand. A *rendered* component class keeps 

72 #: its own forward plus the emitted boundary form, so it is not listed. 

73 external_state_classes: list[str] = field(default_factory=list) 

74 #: ``covered`` must always carry a bool ``sharding_plan``, 

75 #: so the default mirrors ``COVERED_DEFAULTS`` rather than ``{}`` — a bare 

76 #: ``CodegenMeta`` round-trips through ``load_codegen_meta`` instead of 

77 #: failing schema validation. The manager overwrites this explicitly. 

78 covered: dict[str, Any] = field(default_factory=lambda: dict(COVERED_DEFAULTS)) 

79 not_covered: list[str] = field(default_factory=list) 

80 #: Plan-coordinate active axes (``FrozenPlan.mesh_dim_names``), JSON form. 

81 #: The runtime slices the live mesh down to these axes so placements never 

82 #: land on dp axes the plan never shards on. ``None`` means the runtime 

83 #: uses the full mesh for compatibility with older artifacts. 

84 mesh_dim_names: Optional[list[str]] = None 

85 #: ``<remote>`` source sibling modules copied into the bundle: 

86 #: ``["configuration_deepseek.py", ...]`` 

87 #: listing every package sibling the generated file's relative imports 

88 #: resolve against. Empty for installed transformers sources (they use 

89 #: absolute imports) and for artifacts generated before this field existed. 

90 remote_siblings: list[str] = field(default_factory=list) 

91 

92 

93#: Stages the generated artifact declares it covers. ``sharding_plan`` starts 

94#: as the default here (False, kept for metas that predate the field), but 

95#: ``_fill_plan_fields`` (manager.py) sets it True once the plan is frozen: 

96#: the artifact carries the parallel logic and the runtime shards from 

97#: ``codegen_meta.json``. When True, the trainer short-circuits the 

98#: planner+applier and runs the artifact's own parallelize step. 

99#: ``module_overrides`` turns True once generation sunk ``replace_module``, so 

100#: the trainer's HF replacement step is skipped. 

101COVERED_DEFAULTS = { 

102 "sharding_plan": False, 

103 "module_overrides": False, 

104 "axes": [], 

105} 

106 

107#: Stages that remain trainer-owned. 

108NOT_COVERED_DEFAULT = [ 

109 "mesh", 

110 "fsdp2", 

111 "pp", 

112 "activation_checkpoint", 

113 "compile", 

114] 

115 

116 

117def meta_to_dict(meta: CodegenMeta) -> dict[str, Any]: 

118 """Serialize a CodegenMeta to a JSON-friendly dict.""" 

119 return asdict(meta) 

120 

121 

122def meta_from_dict(data: dict[str, Any]) -> CodegenMeta: 

123 """Rebuild a CodegenMeta from a JSON dict.""" 

124 validate_meta_schema(data) 

125 return CodegenMeta( 

126 **{k: v for k, v in data.items() if k in CodegenMeta.__dataclass_fields__} 

127 ) 

128 

129 

130def load_codegen_meta(meta_path: str) -> Optional[CodegenMeta]: 

131 """Read the meta file; ``None`` if absent or malformed.""" 

132 if not os.path.isfile(meta_path): 

133 return None 

134 try: 

135 with open(meta_path, "r", encoding="utf-8") as handle: 

136 return meta_from_dict(json.load(handle)) 

137 except (json.JSONDecodeError, TypeError, ValueError, KeyError): 

138 return None 

139 

140 

141def write_codegen_meta(meta: CodegenMeta, path: str) -> None: 

142 """Write the meta as pretty-printed JSON.""" 

143 with open(path, "w", encoding="utf-8") as handle: 

144 json.dump(meta_to_dict(meta), handle, indent=2, sort_keys=True) 

145 

146 

147def validate_meta_schema(data: dict[str, Any]) -> None: 

148 """Fail fast if required meta fields are missing or mistyped.""" 

149 required = ("codegen_version", "signature", "yaml_path", "yaml_sha256") 

150 for key in required: 

151 if key not in data: 

152 raise ValueError(f"codegen_meta missing required field {key!r}") 

153 if not isinstance(data[key], str): 

154 raise TypeError(f"codegen_meta field {key!r} must be a string") 

155 for key in ("source", "parallel_dims", "outputs"): 

156 if key in data and not isinstance(data[key], dict): 

157 raise TypeError(f"codegen_meta field {key!r} must be a dict") 

158 if "entrypoints" in data: 

159 entrypoints = data["entrypoints"] 

160 if not isinstance(entrypoints, dict) or not all( 

161 isinstance(k, str) and isinstance(v, str) for k, v in entrypoints.items() 

162 ): 

163 raise TypeError("codegen_meta field 'entrypoints' must be a dict[str, str]") 

164 if "covered" in data: 

165 covered = data["covered"] 

166 if not isinstance(covered, dict): 

167 raise TypeError("codegen_meta field 'covered' must be a dict") 

168 if not isinstance(covered.get("sharding_plan"), bool): 

169 raise TypeError("codegen_meta field 'covered.sharding_plan' must be a bool") 

170 if "not_covered" in data: 

171 not_covered = data["not_covered"] 

172 if not isinstance(not_covered, list) or not all( 

173 isinstance(item, str) for item in not_covered 

174 ): 

175 raise TypeError("codegen_meta field 'not_covered' must be a list[str]") 

176 if "module_overrides" in data: 

177 overrides = data["module_overrides"] 

178 if not isinstance(overrides, list) or not all( 

179 isinstance(item, dict) for item in overrides 

180 ): 

181 raise TypeError( 

182 "codegen_meta field 'module_overrides' must be a list[dict]" 

183 ) 

184 if "remote_siblings" in data: 

185 siblings = data["remote_siblings"] 

186 if not isinstance(siblings, list) or not all( 

187 isinstance(item, str) for item in siblings 

188 ): 

189 raise TypeError("codegen_meta field 'remote_siblings' must be a list[str]") 

190 if "external_state_classes" in data: 

191 class_names = data["external_state_classes"] 

192 if not isinstance(class_names, list) or not all( 

193 isinstance(item, str) for item in class_names 

194 ): 

195 raise TypeError( 

196 "codegen_meta field 'external_state_classes' must be a list[str]" 

197 ) 

198 

199 

200def record_output_hashes( 

201 meta: CodegenMeta, 

202 layout: ArtifactLayout, 

203 *, 

204 base_dir: str = "", 

205) -> CodegenMeta: 

206 """Populate ``meta.outputs`` with per-file hashes and line counts. 

207 

208 When ``base_dir`` is given every regular file is hashed from *there* (the 

209 staging directory); otherwise the artifact directory is used. Hashing must 

210 happen where the bytes actually live at write time: the atomic bundle 

211 writes ``codegen_meta.json`` into the staging dir, so ``base_dir`` is the 

212 staging dir and the layout paths still point at the old bundle. 

213 

214 The meta file itself is deliberately NOT recorded: its sha256 cannot be 

215 stored in its own bytes (the value changes the file, so the hash could 

216 never be a fixed point), and the previous record-then-write order hashed 

217 the *old* meta and made preflight report drift on every second 

218 generation. The meta file's integrity is covered by the signature check, 

219 the schema validation, and the hashes of every other bundle file — it does 

220 not need a self-hash. 

221 """ 

222 output_dir = base_dir or layout.artifact_dir 

223 role_by_name = { 

224 os.path.basename(layout.modeling_path): "modeling", 

225 os.path.basename(layout.diff_path): "diff", 

226 os.path.basename(layout.init_path): "init", 

227 } 

228 meta.outputs = {} 

229 for name in sorted(os.listdir(output_dir)): 

230 target = os.path.join(output_dir, name) 

231 if name == os.path.basename(layout.meta_path) or not os.path.isfile(target): 

232 continue 

233 key = role_by_name.get(name, name) 

234 meta.outputs[key] = { 

235 "sha256": sha256_file(target), 

236 "lines": file_line_count(target), 

237 } 

238 return meta 

239 

240 

241__all__ = [ 

242 "CodegenMeta", 

243 "load_codegen_meta", 

244 "meta_from_dict", 

245 "meta_to_dict", 

246 "record_output_hashes", 

247 "validate_meta_schema", 

248 "write_codegen_meta", 

249]