Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / config_adapter / _strategy_output.py: 92%

104 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-07-06 05:41 +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"""Strategy output writer for auto parallel strategy search. 

16 

17Serializes :class:`NormalizedConfig` instances to JSON or YAML files, 

18generates human-readable summaries, and provides a PPB (PR524) 

19configuration stub. 

20""" 

21 

22import json 

23import logging 

24import os 

25from typing import Any, Dict 

26 

27import yaml # type: ignore[import-untyped] 

28 

29from hyper_parallel.auto_parallel.config_adapter._normalized_config import NormalizedConfig 

30 

31 

32logger = logging.getLogger(__name__) 

33 

34 

35def _resolve_pp_degree(pp_cfg: Dict) -> int: 

36 """Resolve pp_degree to a scalar from pp_config.""" 

37 pp_degree_val = pp_cfg.get("pp_degree", 1) 

38 if isinstance(pp_degree_val, list): 

39 return pp_degree_val[0] if pp_degree_val else 1 

40 return pp_degree_val 

41 

42 

43def _write_json(data: Dict[str, Any], output_path: str) -> None: 

44 """Write a dict as a pretty-printed JSON file.""" 

45 with open(output_path, "w", encoding="utf-8") as fh: 

46 json.dump(data, fh, indent=2, default=str) 

47 fh.write("\n") 

48 

49 

50# Mapping from resolved strategy keys to HP YAML accelerator field names. 

51_YAML_KEY_MAP: Dict[str, str] = { 

52 "dp_shard": "dp_shard", 

53 "dp_replicate": "dp_replicate", 

54 "data_parallel_shard_degree": "dp_shard", 

55 "data_parallel_replicate_degree": "dp_replicate", 

56 "data_parallel_degree": "dp_replicate", 

57 "dp": "dp_replicate", 

58 "tensor_parallel_degree": "tp_degree", 

59 "tp_degree": "tp_degree", 

60 "tp": "tp_degree", 

61 "pipeline_parallel_degree": "pipeline_parallel_degree", 

62 "context_parallel_degree": "context_parallel_degree", 

63 "expert_parallel_degree": "expert_parallel_degree", 

64 "expert_tensor_parallel_degree": "expert_tensor_parallel_degree", 

65 "pp_degree": "pipeline_parallel_degree", 

66 "pp": "pipeline_parallel_degree", 

67 "cp_degree": "context_parallel_degree", 

68 "cp": "context_parallel_degree", 

69 "ep_degree": "expert_parallel_degree", 

70 "ep": "expert_parallel_degree", 

71 "etp_degree": "expert_tensor_parallel_degree", 

72 "etp": "expert_tensor_parallel_degree", 

73} 

74 

75 

76def _validate_strategy_and_yaml( 

77 config: NormalizedConfig, 

78 original_yaml_path: str, 

79) -> None: 

80 """Validate that resolved_strategy is set and the original YAML exists.""" 

81 if config.resolved_strategy is None: 

82 raise ValueError( 

83 "config.resolved_strategy is None — no strategy to write. " 

84 "Set config.resolved_strategy first." 

85 ) 

86 if not os.path.isfile(original_yaml_path): 

87 raise FileNotFoundError( 

88 f"Original YAML not found: {original_yaml_path}" 

89 ) 

90 

91 

92def _load_yaml_to_inject(original_yaml_path: str) -> Dict[str, Any]: 

93 """Load and validate the original YAML, ensuring train/accelerator exist.""" 

94 with open(original_yaml_path, "r", encoding="utf-8") as fh: 

95 data = yaml.safe_load(fh) 

96 if data is None or not isinstance(data, dict): 

97 raise ValueError( 

98 f"Original YAML {original_yaml_path} must contain a top-level mapping." 

99 ) 

100 if "train" not in data or not isinstance(data["train"], dict): 

101 data["train"] = {} 

102 if "accelerator" not in data["train"] or not isinstance(data["train"]["accelerator"], dict): 

103 data["train"]["accelerator"] = {} 

104 return data 

105 

106 

107def _inject_resolved_strategy(data: Dict[str, Any], resolved: Dict[str, Any]) -> None: 

108 """Inject resolved strategy values into the YAML data dict.""" 

109 train = data["train"] 

110 accel = train["accelerator"] 

111 

112 for src_key, dst_key in _YAML_KEY_MAP.items(): 

113 if src_key in resolved: 

114 accel[dst_key] = int(resolved[src_key]) 

115 

116 if "global_batch_size" in resolved: 

117 train["global_batch_size"] = int(resolved["global_batch_size"]) 

118 

119 if "micro_batch_num" in resolved: 

120 train["micro_batch_num"] = int(resolved["micro_batch_num"]) 

121 

122 

123def _write_output_yaml( 

124 data: Dict[str, Any], 

125 output_path: str, 

126 overwrite: bool, 

127 original_yaml_path: str, 

128) -> None: 

129 """Write the data dict to the output YAML file.""" 

130 write_path = original_yaml_path if overwrite else output_path 

131 

132 parent_dir = os.path.dirname(os.path.abspath(write_path)) 

133 if parent_dir and not os.path.isdir(parent_dir): 

134 os.makedirs(parent_dir, exist_ok=True) 

135 

136 with open(write_path, "w", encoding="utf-8") as fh: 

137 yaml.dump(data, fh, default_flow_style=False, sort_keys=False) 

138 

139 logger.info( 

140 "Resolved YAML written to %s (overwrite=%s)", 

141 write_path, overwrite, 

142 ) 

143 

144 

145def write_strategy_config( 

146 config: NormalizedConfig, 

147 output_path: str, 

148 fmt: str = "json", 

149) -> None: 

150 """Write a normalized configuration to a file. 

151 

152 Args: 

153 config: The normalized configuration to serialize. 

154 output_path: Destination file path. 

155 fmt: Output format, ``"json"`` (default) only. 

156 

157 Raises: 

158 ValueError: If the output format is unsupported. 

159 OSError: If the file cannot be written. 

160 """ 

161 data = config.to_dict() 

162 

163 parent_dir = os.path.dirname(os.path.abspath(output_path)) 

164 if parent_dir and not os.path.isdir(parent_dir): 

165 os.makedirs(parent_dir, exist_ok=True) 

166 

167 try: 

168 if fmt == "json": 

169 _write_json(data, output_path) 

170 else: 

171 raise ValueError( 

172 f"Unsupported output format: {fmt!r}. " 

173 "Supported format: 'json'" 

174 ) 

175 except OSError as exc: 

176 raise OSError(f"Failed to write config to {output_path}: {exc}") from exc 

177 

178 logger.info("Strategy config written to %s", output_path) 

179 

180 

181def write_resolved_strategy( 

182 config: NormalizedConfig, 

183 output_path: str, 

184 fmt: str = "json", 

185) -> None: 

186 """Write only the resolved strategy to a file. 

187 

188 This produces a compact config snippet suitable for consumption 

189 by training scripts. 

190 

191 Args: 

192 config: The normalized configuration (must have ``resolved_strategy`` set). 

193 output_path: Destination file path. 

194 fmt: Output format, ``"json"`` (default) only. 

195 

196 Raises: 

197 ValueError: If ``resolved_strategy`` is ``None``. 

198 """ 

199 if config.resolved_strategy is None: 

200 raise ValueError("config.resolved_strategy is None — no strategy to write") 

201 

202 parent_dir = os.path.dirname(os.path.abspath(output_path)) 

203 if parent_dir and not os.path.isdir(parent_dir): 

204 os.makedirs(parent_dir, exist_ok=True) 

205 

206 data = {"resolved_strategy": dict(config.resolved_strategy)} 

207 

208 try: 

209 if fmt == "json": 

210 _write_json(data, output_path) 

211 else: 

212 raise ValueError( 

213 f"Unsupported output format: {fmt!r}. " 

214 "Supported format: 'json'" 

215 ) 

216 except OSError as exc: 

217 raise OSError(f"Failed to write resolved strategy to {output_path}: {exc}") from exc 

218 

219 logger.info("Resolved strategy written to %s", output_path) 

220 

221 

222def write_ppb_config( 

223 config: NormalizedConfig, 

224 output_path: str, 

225) -> None: 

226 """Write a PPB (PR524 pipeline balancer) input JSON file. 

227 

228 Writes the PP-relevant fields from ``NormalizedConfig`` 

229 in a structure compatible with ``args_for_pipeline_parallel.json``. 

230 

231 Args: 

232 config: The normalized configuration. 

233 output_path: Destination JSON file path. 

234 

235 Raises: 

236 OSError: If the file cannot be written. 

237 """ 

238 model = config.model_spec 

239 pp_cfg = config.pp_config 

240 constraint = config.constraint 

241 

242 ppb_data = { 

243 "llm_class": "0", 

244 "train_yaml": "", 

245 "mindformers_dir": "", 

246 "layer_ratio": 0.33, 

247 "backward_ratio": 2.0, 

248 "head_loss": 1.5, 

249 "recompute_ratio": 1, 

250 "time_limit": 2147483647, 

251 "dryrun": True, 

252 "check": True, 

253 "fit_level": 0, 

254 "extract": False, 

255 "env_json": "./config/env_config.json", 

256 "dryrun_lim": 16, 

257 "_hyper_model": { 

258 "n_layers": model.get("n_layers", 0), 

259 "dim": model.get("dim", 0), 

260 "n_heads": model.get("n_heads", 0), 

261 "vocab_size": model.get("vocab_size", 0), 

262 "seq_len": model.get("seq_len", 4096), 

263 "moe_enabled": model.get("moe_enabled", False), 

264 "num_experts": model.get("num_experts", 1), 

265 }, 

266 "_hyper_pp": { 

267 "pp_degree": _resolve_pp_degree(pp_cfg), 

268 "stage_partition_mode": pp_cfg.get("stage_partition_mode", "uniform"), 

269 "micro_batch_num": pp_cfg.get("micro_batch_num", 1), 

270 "layer_offset_range": list(pp_cfg.get("layer_offset_range", (0, 0))), 

271 "layer_recompute_layers": pp_cfg.get("layer_recompute_layers", []), 

272 }, 

273 "_hyper_constraint": { 

274 "global_batch_size": constraint.get("global_batch_size", 0), 

275 "memory_limit_gb": constraint.get("memory_limit_gb", 0.0), 

276 }, 

277 } 

278 

279 parent_dir = os.path.dirname(os.path.abspath(output_path)) 

280 if parent_dir and not os.path.isdir(parent_dir): 

281 os.makedirs(parent_dir, exist_ok=True) 

282 

283 try: 

284 with open(output_path, "w", encoding="utf-8") as fh: 

285 json.dump(ppb_data, fh, indent=4) 

286 fh.write("\n") 

287 except OSError as exc: 

288 raise OSError(f"Failed to write PPB config to {output_path}: {exc}") from exc 

289 

290 logger.info("PPB config stub written to %s", output_path) 

291 

292 

293def write_resolved_yaml( 

294 config: NormalizedConfig, 

295 original_yaml_path: str, 

296 output_path: str, 

297 overwrite: bool = False, 

298) -> None: 

299 """Write a resolved strategy into a HyperParallel YAML file. 

300 

301 Copies the original ``train.yaml`` and replaces the parallel 

302 dimension fields with the resolved strategy values. This produces 

303 a complete, immediately launchable training configuration. 

304 

305 The resolved strategy is read from ``config.resolved_strategy``. 

306 Supported keys: ``dp_shard``, ``dp_replicate``, ``tp_degree``, 

307 ``pipeline_parallel_degree``, ``context_parallel_degree``, 

308 ``expert_parallel_degree``, ``global_batch_size``. 

309 

310 Args: 

311 config: NormalizedConfig with ``resolved_strategy`` set. 

312 original_yaml_path: Path to the original ``train.yaml``. 

313 output_path: Destination file path for the resolved YAML. 

314 overwrite: If ``True``, write to ``original_yaml_path`` instead 

315 of ``output_path`` (default ``False``). 

316 

317 Raises: 

318 ValueError: If ``resolved_strategy`` is ``None``. 

319 FileNotFoundError: If ``original_yaml_path`` does not exist. 

320 """ 

321 _validate_strategy_and_yaml(config, original_yaml_path) 

322 data = _load_yaml_to_inject(original_yaml_path) 

323 _inject_resolved_strategy(data, config.resolved_strategy) 

324 _write_output_yaml(data, output_path, overwrite, original_yaml_path) 

325 

326 

327def normalized_to_summary(config: NormalizedConfig) -> Dict[str, Any]: 

328 """Generate a human-readable summary of the configuration. 

329 

330 Args: 

331 config: The normalized configuration to summarize. 

332 

333 Returns: 

334 A dictionary with summary fields suitable for logging or display. 

335 """ 

336 model = config.model_spec 

337 cluster = config.cluster_spec 

338 search = config.search_space 

339 constraint = config.constraint 

340 estimator = config.estimator 

341 pp_cfg = config.pp_config 

342 

343 return { 

344 "model": { 

345 "name": model.get("name", "unknown"), 

346 "n_layers": model.get("n_layers", 0), 

347 "dim": model.get("dim", 0), 

348 "inter_dim": model.get("inter_dim", 0), 

349 "n_heads": model.get("n_heads", 0), 

350 "n_kv_heads": model.get("n_kv_heads", 0), 

351 "vocab_size": model.get("vocab_size", 0), 

352 "seq_len": model.get("seq_len", 0), 

353 "moe_enabled": model.get("moe_enabled", False), 

354 "num_experts": model.get("num_experts", 0), 

355 }, 

356 "cluster": { 

357 "num_nodes": cluster.get("num_nodes", 0), 

358 "cards_per_node": cluster.get("cards_per_node", 0), 

359 "total_cards": ( 

360 cluster.get("num_nodes", 0) * cluster.get("cards_per_node", 0) 

361 ), 

362 "device_memory_gb": cluster.get("device_memory_gb", 0), 

363 "device_type": cluster.get("device_type", "unknown"), 

364 }, 

365 "search_space": dict(sorted(search.items())), 

366 "constraints": { 

367 "global_batch_size": constraint.get("global_batch_size", 0), 

368 "memory_limit_gb": constraint.get("memory_limit_gb", 0.0), 

369 "fixed_dimensions": { 

370 k: constraint.get(f"fixed_{k}_degree") 

371 for k in ("dp", "tp", "pp", "cp", "ep") 

372 if constraint.get(f"fixed_{k}_degree") is not None 

373 }, 

374 }, 

375 "estimator": { 

376 "type": estimator.get("type", "symbolic"), 

377 "recompute_strategy": estimator.get("recompute_strategy", "none"), 

378 }, 

379 "pipeline": { 

380 "pp_degree": _resolve_pp_degree(pp_cfg), 

381 "stage_partition_mode": pp_cfg.get("stage_partition_mode", "uniform"), 

382 "micro_batch_num": pp_cfg.get("micro_batch_num", 1), 

383 }, 

384 "resolved_strategy": config.resolved_strategy, 

385 }