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

124 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"""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 short names. 

35_HP_TO_INTERNAL: Dict[str, str] = { 

36 "num_hidden_layers": "n_layers", 

37 "hidden_size": "dim", 

38 "num_attention_heads": "n_heads", 

39 "intermediate_size": "inter_dim", 

40 "num_key_value_heads": "n_kv_heads", 

41 "max_position_embeddings": "seq_len", 

42 "seq_length": "seq_len", 

43} 

44 

45 

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

47 """Rename HuggingFace-style config overrides keys to internal short names. 

48 

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

50 explicit short names in the YAML take precedence. 

51 """ 

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

53 if hf_key in model_spec and internal_key not in model_spec: 

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

55 return model_spec 

56 

57 

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

59# to canonical NormalizedConfig search_space keys. 

60_UNIFIED_DIM_MAP: Dict[str, str] = { 

61 "dp": "data_parallel_replicate_degree", 

62 "fsdp": "data_parallel_shard_degree", 

63 "tp": "tensor_parallel_degree", 

64 "pp": "pipeline_parallel_degree", 

65 "cp": "context_parallel_degree", 

66 "ep": "expert_parallel_degree", 

67 "etp": "expert_tensor_parallel_degree", 

68 "micro_batch_num": "micro_batch_num", 

69} 

70 

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

72_FIXED_DIM_MAP: Dict[str, str] = { 

73 "dp": "fixed_dp_degree", 

74 "fsdp": "fixed_fsdp_degree", 

75 "tp": "fixed_tp_degree", 

76 "pp": "fixed_pp_degree", 

77 "cp": "fixed_cp_degree", 

78 "ep": "fixed_ep_degree", 

79 "micro_batch_num": "fixed_micro_batch_num", 

80} 

81 

82 

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

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

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

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

87 

88 

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

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

91 if yaml is None: 

92 raise ImportError( 

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

94 "Install it with: pip install pyyaml" 

95 ) 

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

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

98 

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

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

101 raise ValueError( 

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

103 "Supported formats: .yaml, .yml" 

104 ) 

105 

106 try: 

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

108 raw = yaml.safe_load(fh) 

109 except yaml.YAMLError as exc: 

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

111 

112 if raw is None: 

113 raw = {} 

114 if not isinstance(raw, dict): 

115 raise ValueError( 

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

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

118 ) 

119 return raw 

120 

121 

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

123 

124 

125def _parse_unified_parallelism( 

126 para_raw: Dict[str, Any], 

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

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

129 

130 Rules: 

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

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

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

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

135 (neither fixed nor explicitly enumerated). 

136 

137 Returns: 

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

139 """ 

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

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

142 

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

144 if short_key not in para_raw: 

145 continue 

146 value = para_raw[short_key] 

147 

148 if isinstance(value, int): 

149 constraint[_FIXED_DIM_MAP[short_key]] = value 

150 search_space[canonical_key] = [value] 

151 elif isinstance(value, list): 

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

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

154 continue 

155 

156 return search_space, constraint 

157 

158 

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

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

161 

162 Supports two modes: 

163 

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

165 info must be present in the search config file. 

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

167 parameters and current parallelism values, then overlays the search 

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

169 

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

171 as fixed (single-element) entries. 

172 """ 

173 train_yaml_path = raw.get("train_yaml") 

174 base_config: Optional[NormalizedConfig] = None 

175 

176 if train_yaml_path: 

177 if not isinstance(train_yaml_path, str): 

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

179 base_raw = _load_yaml(train_yaml_path) 

180 base_config = _build_config_from_hp_yaml(base_raw) 

181 

182 model_spec: Dict[str, Any] 

183 if base_config: 

184 model_spec = dict(base_config.model_spec) 

185 else: 

186 model_spec = {} 

187 

188 # Override or supply model section from search.yaml 

189 search_model = _get_dict(raw, "model") 

190 if search_model: 

191 model_spec.update(search_model) 

192 

193 model_spec.setdefault("seq_len", 4096) 

194 model_spec.setdefault("local_batch_size", 1) 

195 

196 model_spec = _normalize_model_spec(model_spec) 

197 

198 cluster_spec = _get_dict(raw, "cluster") 

199 

200 pp_raw = _get_dict(raw, "pp_config") 

201 parallelism_raw = _get_dict(raw, "parallelism") 

202 constraint_raw = _get_dict(raw, "constraint") 

203 

204 search_space, parallelism_constraint = _parse_unified_parallelism(parallelism_raw) 

205 

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

207 if base_config: 

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

209 if space_key not in search_space: 

210 search_space[space_key] = candidates 

211 

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

213 constraint_raw.setdefault( 

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

215 ) 

216 

217 pp_config: Dict[str, Any] = { 

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

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

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

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

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

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

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

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

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

227 } 

228 

229 estimator: Dict[str, Any] = { 

230 "type": "symbolic", 

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

232 "enable_profiling_calibration": False, 

233 } 

234 

235 constraint: Dict[str, Any] = { 

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

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

238 **parallelism_constraint, 

239 } 

240 

241 return NormalizedConfig( 

242 model_spec=model_spec, 

243 cluster_spec=cluster_spec, 

244 search_space=search_space, 

245 constraint=constraint, 

246 estimator=estimator, 

247 pp_config=pp_config, 

248 ) 

249 

250 

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

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

253 

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

255 where each dimension is declared as:: 

256 

257 parallelism: 

258 tp: 4 # scalar → fixed input 

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

260 pp: auto # string → let the searcher decide 

261 

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

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

264 

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

266 cluster: 

267 num_nodes: 4 

268 cards_per_node: 8 

269 parallelism: 

270 dp: [1, 2, 4] 

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

272 

273 Dimensions absent from ``parallelism`` are inherited from 

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

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

276 

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

278 standalone example. 

279 

280 Args: 

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

282 

283 Returns: 

284 A :class:`NormalizedConfig` instance. 

285 

286 Raises: 

287 FileNotFoundError: If the file does not exist. 

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

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

290 ImportError: If PyYAML is not installed. 

291 """ 

292 raw = _load_yaml(path) 

293 return _build_config_from_search_yaml(raw) 

294 

295 

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

297 

298_ACCEL_TO_SEARCH = { 

299 "dp_shard": "data_parallel_shard_degree", 

300 "dp_replicate": "data_parallel_replicate_degree", 

301 "tp_degree": "tensor_parallel_degree", 

302 "pipeline_parallel_degree": "pipeline_parallel_degree", 

303 "context_parallel_degree": "context_parallel_degree", 

304 "expert_parallel_degree": "expert_parallel_degree", 

305 "expert_tensor_parallel_degree": "expert_tensor_parallel_degree", 

306} 

307 

308 

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

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

311 

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

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

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

315 ``train.gradient_checkpointing.activation_checkpoint``. 

316 

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

318 """ 

319 model_raw = _get_dict(raw, "model") 

320 train_raw = _get_dict(raw, "train") 

321 data_raw = _get_dict(raw, "data") 

322 accel_raw = _get_dict(train_raw, "accelerator") 

323 gc_raw = _get_dict(train_raw, "gradient_checkpointing") 

324 

325 # --- model_spec --- 

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

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

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

329 if isinstance(overrides, dict): 

330 model_spec.update(overrides) 

331 model_spec["seq_len"] = data_raw.get("max_seq_len", 4096) 

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

333 

334 # dtype from train.mixed_precision 

335 mp_raw = _get_dict(train_raw, "mixed_precision") 

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

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

338 

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

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

341 

342 # --- search_space from train.accelerator --- 

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

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

345 val = accel_raw.get(hkey) 

346 if val is not None: 

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

348 

349 # --- constraint from train --- 

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

351 constraint: Dict[str, Any] = { 

352 "global_batch_size": gbs or 0, 

353 "memory_limit_gb": 0.0, 

354 } 

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

356 

357 # --- pp_config --- 

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

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

360 pp_config: Dict[str, Any] = { 

361 "pp_degree": pp_degree, 

362 "stage_partition_mode": "uniform", 

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

364 } 

365 

366 # --- estimator from gradient_checkpointing --- 

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

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

369 estimator: Dict[str, Any] = { 

370 "type": "symbolic", 

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

372 } 

373 

374 model_spec = _normalize_model_spec(model_spec) 

375 

376 return NormalizedConfig( 

377 model_spec=model_spec, 

378 cluster_spec=cluster_spec, 

379 search_space=search_space, 

380 constraint=constraint, 

381 estimator=estimator, 

382 pp_config=pp_config, 

383 ) 

384 

385 

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

387 """Read a HyperParallel YAML configuration file. 

388 

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

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

391 fields from ``model.config_overrides``. 

392 

393 .. note:: 

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

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

396 which accepts cluster and search-space parameters. 

397 

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

399 YAML sections. 

400 

401 Args: 

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

403 

404 Returns: 

405 A :class:`NormalizedConfig` instance. 

406 

407 Raises: 

408 FileNotFoundError: If the file does not exist. 

409 ValueError: If the file cannot be parsed. 

410 ImportError: If PyYAML is not installed. 

411 """ 

412 raw = _load_yaml(path) 

413 return _build_config_from_hp_yaml(raw)