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

124 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-08-04 05: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"""Configuration loader for auto parallel strategy search. 

16 

17Reads Search Config (``search.yaml``) and HyperParallel training config 

18(``train.yaml``) files, producing :class:`NormalizedConfig` instances. 

19""" 

20 

21import logging 

22import os 

23from typing import Any, Dict, List, Tuple, Optional 

24 

25try: 

26 import yaml # type: ignore[import-untyped] # pylint: disable=C0415 

27except ImportError: 

28 yaml = None # pragma: no cover 

29 

30from hyper_parallel.auto_parallel.config_adapter._normalized_config import NormalizedConfig 

31 

32logger = logging.getLogger(__name__) 

33 

34# Mapping from HuggingFace-style config_overrides keys to internal names. 

35# After field-name alignment, most HP YAML keys already match the internal 

36# model_spec names. Only ``seq_length`` (MF variant) needs remapping. 

37_HP_TO_INTERNAL: Dict[str, str] = { 

38 "seq_length": "max_position_embeddings", 

39} 

40 

41 

42def _normalize_model_spec(model_spec: Dict[str, Any]) -> Dict[str, Any]: 

43 """Rename non-standard config overrides keys to canonical names. 

44 

45 Only maps a key if the target name is not already present, so 

46 explicit canonical names in the YAML take precedence. 

47 """ 

48 for hf_key, internal_key in _HP_TO_INTERNAL.items(): 

49 if hf_key in model_spec and internal_key not in model_spec: 

50 model_spec[internal_key] = model_spec.pop(hf_key) 

51 return model_spec 

52 

53 

54# Mapping from short dimension names (used in search config YAML parallelism section) 

55# to canonical NormalizedConfig search_space keys. 

56_UNIFIED_DIM_MAP: Dict[str, str] = { 

57 "dp": "data_parallel_replicate_degree", 

58 "fsdp": "data_parallel_shard_degree", 

59 "tp": "tensor_parallel_degree", 

60 "pp": "pipeline_parallel_degree", 

61 "cp": "context_parallel_degree", 

62 "ep": "expert_parallel_degree", 

63 "etp": "expert_tensor_parallel_degree", 

64 "micro_batch_num": "micro_batch_num", 

65} 

66 

67# Mapping from short dimension names to constraint fixed_*_degree keys. 

68_FIXED_DIM_MAP: Dict[str, str] = { 

69 "dp": "fixed_dp_degree", 

70 "fsdp": "fixed_fsdp_degree", 

71 "tp": "fixed_tp_degree", 

72 "pp": "fixed_pp_degree", 

73 "cp": "fixed_cp_degree", 

74 "ep": "fixed_ep_degree", 

75 "etp": "fixed_etp_degree", 

76 "micro_batch_num": "fixed_micro_batch_num", 

77} 

78 

79 

80def _get_dict(raw: Dict[str, Any], key: str) -> Dict[str, Any]: 

81 """Return the value of a key if it is a dict, otherwise an empty dict.""" 

82 val = raw.get(key, {}) 

83 return val if isinstance(val, dict) else {} 

84 

85 

86def _load_yaml(path: str) -> Dict[str, Any]: 

87 """Read and parse a YAML file, returning the raw dict.""" 

88 if yaml is None: 

89 raise ImportError( 

90 "PyYAML is required to read HyperParallel YAML configs. " 

91 "Install it with: pip install pyyaml" 

92 ) 

93 if not os.path.isfile(path): 

94 raise FileNotFoundError(f"Config file not found: {path}") 

95 

96 ext = os.path.splitext(path)[1].lower() 

97 if ext not in (".yaml", ".yml"): 

98 raise ValueError( 

99 f"Unsupported config file format: {ext!r}. " 

100 "Supported formats: .yaml, .yml" 

101 ) 

102 

103 try: 

104 with open(path, "r", encoding="utf-8") as fh: 

105 raw = yaml.safe_load(fh) 

106 except yaml.YAMLError as exc: 

107 raise ValueError(f"Failed to parse YAML file {path}: {exc}") from exc 

108 

109 if raw is None: 

110 raw = {} 

111 if not isinstance(raw, dict): 

112 raise ValueError( 

113 f"Config file {path} must contain a YAML mapping at the top level, " 

114 f"got {type(raw).__name__}" 

115 ) 

116 return raw 

117 

118 

119# ── Search Config YAML reader (primary) ────────────────────────────── 

120 

121 

122def _parse_unified_parallelism( 

123 para_raw: Dict[str, Any], 

124) -> Tuple[Dict[str, List[int]], Dict[str, Any]]: 

125 """Convert the unified parallelism declaration into search_space + constraint. 

126 

127 Rules: 

128 * Scalar integer value → fixed dimension (both ``constraint.fixed_*`` 

129 and ``search_space`` with a single-element list). 

130 * List value → search candidates (placed into ``search_space`` only). 

131 * String ``"auto"`` → dimension is left to the searcher's ``bound_space`` 

132 (neither fixed nor explicitly enumerated). 

133 

134 Returns: 

135 A ``(search_space, constraint)`` tuple. 

136 """ 

137 search_space: Dict[str, List[int]] = {} 

138 constraint: Dict[str, Any] = {} 

139 

140 for short_key, canonical_key in _UNIFIED_DIM_MAP.items(): 

141 if short_key not in para_raw: 

142 continue 

143 value = para_raw[short_key] 

144 

145 if isinstance(value, int): 

146 constraint[_FIXED_DIM_MAP[short_key]] = value 

147 search_space[canonical_key] = [value] 

148 elif isinstance(value, list): 

149 search_space[canonical_key] = [int(v) for v in value] 

150 elif isinstance(value, str) and value.strip().lower() == "auto": 

151 continue 

152 

153 return search_space, constraint 

154 

155 

156def _build_config_from_search_yaml(raw: Dict[str, Any]) -> NormalizedConfig: 

157 """Construct a NormalizedConfig from a parsed Search Config YAML dict. 

158 

159 Supports two modes: 

160 

161 * **Standalone** (no ``train_yaml``) — ``model`` section and all other 

162 info must be present in the search config file. 

163 * **With train_yaml** — loads the specified ``train.yaml`` for model 

164 parameters and current parallelism values, then overlays the search 

165 config's ``cluster``, ``parallelism``, and ``constraint`` sections. 

166 

167 Undeclared parallelism dimensions are inherited from ``train.yaml`` 

168 as fixed (single-element) entries. 

169 """ 

170 train_yaml_path = raw.get("train_yaml") 

171 base_config: Optional[NormalizedConfig] = None 

172 

173 if train_yaml_path: 

174 if not isinstance(train_yaml_path, str): 

175 raise ValueError("'train_yaml' must be a file path string") 

176 base_raw = _load_yaml(train_yaml_path) 

177 base_config = _build_config_from_hp_yaml(base_raw) 

178 

179 model_spec: Dict[str, Any] 

180 if base_config: 

181 model_spec = dict(base_config.model_spec) 

182 else: 

183 model_spec = {} 

184 

185 # Override or supply model section from search.yaml 

186 search_model = _get_dict(raw, "model") 

187 if search_model: 

188 model_spec.update(search_model) 

189 

190 model_spec.setdefault("max_position_embeddings", 4096) 

191 model_spec.setdefault("local_batch_size", 1) 

192 

193 model_spec = _normalize_model_spec(model_spec) 

194 

195 cluster_spec = _get_dict(raw, "cluster") 

196 

197 pp_raw = _get_dict(raw, "pp_config") 

198 parallelism_raw = _get_dict(raw, "parallelism") 

199 constraint_raw = _get_dict(raw, "constraint") 

200 

201 search_space, parallelism_constraint = _parse_unified_parallelism(parallelism_raw) 

202 

203 # Inherit undeclared dimensions from train.yaml as fixed values. 

204 if base_config: 

205 for space_key, candidates in base_config.search_space.items(): 

206 if space_key not in search_space: 

207 search_space[space_key] = candidates 

208 

209 if constraint_raw.get("global_batch_size", 0) is None or constraint_raw.get("global_batch_size", 0) == 0: 

210 constraint_raw.setdefault( 

211 "global_batch_size", base_config.constraint.get("global_batch_size", 0) 

212 ) 

213 

214 pp_config: Dict[str, Any] = { 

215 "pp_degree": pp_raw.get("pp_degree", 

216 parallelism_raw.get("pp", 1)), 

217 "stage_partition_mode": pp_raw.get("stage_partition_mode", "uniform"), 

218 "stage_partition": pp_raw.get("stage_partition", []), 

219 "layer_offset_range": tuple(pp_raw.get("layer_offset_range", [0, 0])), 

220 "layer_recompute_layers": pp_raw.get("layer_recompute_layers", []), 

221 "micro_batch_num": pp_raw.get("micro_batch_num", 1), 

222 "pp_interleave_num": pp_raw.get("pp_interleave_num", 1), 

223 "pipeline_parallel_schedule": pp_raw.get("pipeline_schedule", "1F1B"), 

224 } 

225 

226 estimator: Dict[str, Any] = { 

227 "type": "symbolic", 

228 "recompute_strategy": str(raw.get("recompute", "none")), 

229 "enable_profiling_calibration": False, 

230 } 

231 

232 constraint: Dict[str, Any] = { 

233 "global_batch_size": constraint_raw.get("global_batch_size", 0), 

234 "memory_limit_gb": constraint_raw.get("memory_limit_gb", 0.0), 

235 **parallelism_constraint, 

236 } 

237 

238 return NormalizedConfig( 

239 model_spec=model_spec, 

240 cluster_spec=cluster_spec, 

241 search_space=search_space, 

242 constraint=constraint, 

243 estimator=estimator, 

244 pp_config=pp_config, 

245 ) 

246 

247 

248def read_search_config(path: str) -> NormalizedConfig: 

249 """Read a Search Config YAML file and return a :class:`NormalizedConfig`. 

250 

251 The Search Config YAML format uses a unified ``parallelism`` section 

252 where each dimension is declared as:: 

253 

254 parallelism: 

255 tp: 4 # scalar → fixed input 

256 dp: [1, 2, 4] # list → search candidate 

257 pp: auto # string → let the searcher decide 

258 

259 To reuse model parameters from an existing ``train.yaml`` without 

260 duplicating them, set the ``train_yaml`` key:: 

261 

262 train_yaml: "./train.yaml" # load model params from here 

263 cluster: 

264 num_nodes: 4 

265 cards_per_node: 8 

266 parallelism: 

267 dp: [1, 2, 4] 

268 tp: [1, 2, 4, 8] 

269 

270 Dimensions absent from ``parallelism`` are inherited from 

271 ``train.yaml`` as fixed values. A ``model`` section in the search 

272 config overrides values read from ``train_yaml``. 

273 

274 See ``auto_parallel/examples/dense_llm_search.yaml`` for a complete 

275 standalone example. 

276 

277 Args: 

278 path: Path to the YAML config file (``.yaml`` or ``.yml``). 

279 

280 Returns: 

281 A :class:`NormalizedConfig` instance. 

282 

283 Raises: 

284 FileNotFoundError: If the file does not exist. 

285 ValueError: If the file cannot be parsed, or if ``cluster`` 

286 is missing when ``train_yaml`` is not used. 

287 ImportError: If PyYAML is not installed. 

288 """ 

289 raw = _load_yaml(path) 

290 return _build_config_from_search_yaml(raw) 

291 

292 

293# ── HyperParallel training YAML reader (secondary) ────────────────── 

294 

295_ACCEL_TO_SEARCH = { 

296 "dp_shard": "data_parallel_shard_degree", 

297 "dp_replicate": "data_parallel_replicate_degree", 

298 "tp_degree": "tensor_parallel_degree", 

299 "pipeline_parallel_degree": "pipeline_parallel_degree", 

300 "context_parallel_degree": "context_parallel_degree", 

301 "expert_parallel_degree": "expert_parallel_degree", 

302 "expert_tensor_parallel_degree": "expert_tensor_parallel_degree", 

303} 

304 

305 

306def _build_config_from_hp_yaml(raw: Dict[str, Any]) -> NormalizedConfig: 

307 """Construct a NormalizedConfig from a parsed HyperParallel YAML dict. 

308 

309 Extracts model identifiers from ``model.name`` / ``model.config_overrides``, 

310 parallelism from ``train.accelerator.*``, batch settings from ``train.*``, 

311 sequence length from ``data.max_seq_len``, and recompute mode from 

312 ``train.gradient_checkpointing.activation_checkpoint``. 

313 

314 Model hyperparameters are extracted from ``model.config_overrides``. 

315 """ 

316 model_raw = _get_dict(raw, "model") 

317 train_raw = _get_dict(raw, "train") 

318 data_raw = _get_dict(raw, "data") 

319 accel_raw = _get_dict(train_raw, "accelerator") 

320 gc_raw = _get_dict(train_raw, "gradient_checkpointing") 

321 

322 # --- model_spec --- 

323 model_spec: Dict[str, Any] = {} 

324 model_spec["name"] = model_raw.get("name", "unknown") 

325 overrides = model_raw.get("config_overrides", {}) 

326 if isinstance(overrides, dict): 

327 model_spec.update(overrides) 

328 model_spec["max_position_embeddings"] = data_raw.get("max_seq_len", 4096) 

329 model_spec["local_batch_size"] = train_raw.get("micro_batch_size", 1) 

330 

331 # dtype from train.mixed_precision 

332 mp_raw = _get_dict(train_raw, "mixed_precision") 

333 if mp_raw.get("enabled", True): 

334 model_spec["compute_dtype"] = mp_raw.get("param_dtype", "bfloat16") 

335 

336 # --- cluster_spec (users should set via search config or directly) --- 

337 cluster_spec: Dict[str, Any] = {} 

338 

339 # --- search_space from train.accelerator --- 

340 search_space: Dict[str, List[int]] = {} 

341 for hkey, skey in _ACCEL_TO_SEARCH.items(): 

342 val = accel_raw.get(hkey) 

343 if val is not None: 

344 search_space[skey] = [int(val)] 

345 

346 # --- constraint from train --- 

347 gbs = train_raw.get("global_batch_size", 0) 

348 constraint: Dict[str, Any] = { 

349 "global_batch_size": gbs or 0, 

350 "memory_limit_gb": 0.0, 

351 } 

352 mb_num = int(gbs) // int(model_spec["local_batch_size"]) if gbs and model_spec.get("local_batch_size") else 1 

353 

354 # --- pp_config --- 

355 pp_degree = accel_raw.get("pipeline_parallel_degree", 1) 

356 pp_degree = max(1, int(pp_degree) if pp_degree else 1) 

357 pp_config: Dict[str, Any] = { 

358 "pp_degree": pp_degree, 

359 "stage_partition_mode": "uniform", 

360 "micro_batch_num": max(1, mb_num // pp_degree), 

361 } 

362 

363 # --- estimator from gradient_checkpointing --- 

364 ac_mode = str(gc_raw.get("activation_checkpoint", "none")) 

365 recompute_map = {"none": "none", "full": "full", "selective": "selective"} 

366 estimator: Dict[str, Any] = { 

367 "type": "symbolic", 

368 "recompute_strategy": recompute_map.get(ac_mode, "none"), 

369 } 

370 

371 model_spec = _normalize_model_spec(model_spec) 

372 

373 return NormalizedConfig( 

374 model_spec=model_spec, 

375 cluster_spec=cluster_spec, 

376 search_space=search_space, 

377 constraint=constraint, 

378 estimator=estimator, 

379 pp_config=pp_config, 

380 ) 

381 

382 

383def read_hp_yaml_config(path: str) -> NormalizedConfig: 

384 """Read a HyperParallel YAML configuration file. 

385 

386 This is a convenience reader for the native HyperParallel ``train.yaml`` 

387 format. It extracts parallelism from ``train.accelerator`` and model 

388 fields from ``model.config_overrides``. 

389 

390 .. note:: 

391 Cluster configuration is **not** present in ``train.yaml``. 

392 To perform a full strategy search, use :func:`read_search_config` 

393 which accepts cluster and search-space parameters. 

394 

395 See :func:`_build_config_from_hp_yaml` for the full list of recognised 

396 YAML sections. 

397 

398 Args: 

399 path: Path to the YAML config file (``.yaml`` or ``.yml``). 

400 

401 Returns: 

402 A :class:`NormalizedConfig` instance. 

403 

404 Raises: 

405 FileNotFoundError: If the file does not exist. 

406 ValueError: If the file cannot be parsed. 

407 ImportError: If PyYAML is not installed. 

408 """ 

409 raw = _load_yaml(path) 

410 return _build_config_from_hp_yaml(raw)