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

261 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"""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 copy 

22import logging 

23import os 

24from typing import Any, Dict, List, Optional, Set, Tuple 

25 

26try: 

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

28except ImportError: 

29 yaml = None # pragma: no cover 

30 

31from hyper_parallel.auto_parallel._hf_model_spec import ( 

32 exec_overrides, 

33 is_auto_models_schema, 

34 resolve_hf_model_spec, 

35) 

36from hyper_parallel.auto_parallel._layer_census import replacement_specs 

37from hyper_parallel.auto_parallel.config_adapter._normalized_config import NormalizedConfig 

38from hyper_parallel.auto_parallel.sapp_nd.nd.recompute_dimension import read_activation_checkpoint_mode 

39 

40logger = logging.getLogger(__name__) 

41 

42 

43def _recompute_strategy(stated: Any, where: str) -> str: 

44 """The estimator's recompute strategy for an activation checkpoint mode a yaml states, which says off as none.""" 

45 mode = read_activation_checkpoint_mode(stated, where) 

46 return "none" if mode == "off" else mode 

47 

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

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

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

51_HP_TO_INTERNAL: Dict[str, str] = { 

52 "seq_length": "max_position_embeddings", 

53} 

54 

55 

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

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

58 

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

60 explicit canonical names in the YAML take precedence. 

61 """ 

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

63 if hf_key in model_spec and internal_key not in model_spec: 

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

65 # A resolved Transformers config states its expert count, never a 

66 # moe_enabled flag; the constraint checker and the emitted strategy 

67 # both read the flag, so derive it rather than reporting a MoE as dense. 

68 if "moe_enabled" not in model_spec: 

69 model_spec["moe_enabled"] = int(model_spec.get("num_experts") or 1) > 1 

70 return model_spec 

71 

72 

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

74# to canonical NormalizedConfig search_space keys. 

75_UNIFIED_DIM_MAP: Dict[str, str] = { 

76 "dp": "data_parallel_replicate_degree", 

77 "fsdp": "data_parallel_shard_degree", 

78 "tp": "tensor_parallel_degree", 

79 "pp": "pipeline_parallel_degree", 

80 "cp": "context_parallel_degree", 

81 "ep": "expert_parallel_degree", 

82 "etp": "expert_tensor_parallel_degree", 

83 "micro_batch_num": "micro_batch_num", 

84} 

85 

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

87_FIXED_DIM_MAP: Dict[str, str] = { 

88 "dp": "fixed_dp_degree", 

89 "fsdp": "fixed_fsdp_degree", 

90 "tp": "fixed_tp_degree", 

91 "pp": "fixed_pp_degree", 

92 "cp": "fixed_cp_degree", 

93 "ep": "fixed_ep_degree", 

94 "etp": "fixed_etp_degree", 

95 "micro_batch_num": "fixed_micro_batch_num", 

96} 

97 

98 

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

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

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

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

103 

104 

105# The model section's keys that state how the run builds, loads and computes 

106# the model, not the model; the cost model reads them there. 

107_MODEL_RUN_KEYS = ("_target_", "torch_dtype", "param_init_type", "compute_dtype", "softmax_compute_type") 

108 

109# The root keys of an AutoModels train.yaml that state the run. Its 

110# plan_overrides state the modules it builds, which a census measures. 

111_RUN_SECTIONS = ("model_init_dtype", "accelerator", "fsdp_config", "training", "optimizer", "plan_overrides") 

112 

113# The ``context`` keys of a train.yaml that state how the cost model prices 

114# the run: a census of the layers, the census's spec of the model, a vision 

115# tower's token count, a launcher that shards each strategy's experts over 

116# its whole expert group, and what a MoE layer's token dispatch costs on this 

117# cluster. The device count and the memory budget are a search's own. 

118_RUN_CONTEXT_KEYS = ("census", "census_spec", "visual_seq_len", "expert_shard", "moe_dispatch") 

119 

120# The keys of a legacy train.yaml's ``train`` section that are not its 

121# training settings: recompute, the search's, and precision, which the cost 

122# model reads from the model section. 

123_LEGACY_TRAIN_SECTIONS = ("accelerator", "optimizer", "gradient_checkpointing", "mixed_precision") 

124 

125 

126def _model_run(model_raw: Dict[str, Any]) -> Dict[str, Any]: 

127 """Return the dtypes a model section states for the run.""" 

128 return {key: model_raw[key] for key in _MODEL_RUN_KEYS if model_raw.get(key) is not None} 

129 

130 

131def _stated_run(raw: Dict[str, Any]) -> Dict[str, Any]: 

132 """Return the run a train.yaml states, in the AutoModels sections' names. 

133 

134 The model's dtypes, FSDP's precision and resharding, the optimizer, 

135 gradient clipping and the accelerator's settings are the run's, and the 

136 model spec carries none of them. The strategy the same sections state 

137 stays in: the search runner writes the one it searches over it. A legacy 

138 train.yaml states them under ``train``. The context's pricing options, 

139 :data:`_RUN_CONTEXT_KEYS`, ride along. 

140 """ 

141 run: Dict[str, Any] = {"model": _model_run(_get_dict(raw, "model"))} 

142 context = {key: value for key, value in _get_dict(raw, "context").items() if key in _RUN_CONTEXT_KEYS} 

143 if context: 

144 run["context"] = copy.deepcopy(context) 

145 if is_auto_models_schema(raw): 

146 for key in _RUN_SECTIONS: 

147 if raw.get(key) is not None: 

148 run[key] = copy.deepcopy(raw[key]) 

149 return run 

150 train_raw = _get_dict(raw, "train") 

151 run["training"] = { 

152 key: copy.deepcopy(value) for key, value in train_raw.items() if key not in _LEGACY_TRAIN_SECTIONS 

153 } 

154 for key in ("accelerator", "optimizer"): 

155 if isinstance(train_raw.get(key), dict): 

156 run[key] = copy.deepcopy(train_raw[key]) 

157 return run 

158 

159 

160def _load_auto_models_model_spec( 

161 model_raw: Dict[str, Any], 

162 visual_seq_len: Optional[int] = None, 

163 census_seq_len: int = 0, 

164 census_spec: bool = False, 

165 replacements: tuple = (), 

166) -> Dict[str, Any]: 

167 """Resolve model dimensions through the shared AutoModels path. 

168 

169 Delegates to :func:`resolve_hf_model_spec` so this reader and the 

170 SAPP-ND parser cannot disagree about field names or fallbacks. The run 

171 keys of ``config_overrides`` ride along, as the adapter hands them on. 

172 """ 

173 spec = resolve_hf_model_spec(model_raw, visual_seq_len, census_seq_len, census_spec, replacements) 

174 spec.update(exec_overrides(model_raw)) 

175 return _normalize_model_spec(spec) 

176 

177 

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

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

180 if yaml is None: 

181 raise ImportError( 

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

183 "Install it with: pip install pyyaml" 

184 ) 

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

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

187 

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

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

190 raise ValueError( 

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

192 "Supported formats: .yaml, .yml" 

193 ) 

194 

195 try: 

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

197 raw = yaml.safe_load(fh) 

198 except yaml.YAMLError as exc: 

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

200 

201 if raw is None: 

202 raw = {} 

203 if not isinstance(raw, dict): 

204 raise ValueError( 

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

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

207 ) 

208 return raw 

209 

210 

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

212 

213 

214def _parse_unified_parallelism( 

215 para_raw: Dict[str, Any], 

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

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

218 

219 Rules: 

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

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

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

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

224 (neither fixed nor explicitly enumerated). 

225 

226 Returns: 

227 A ``(search_space, constraint, auto)`` tuple, where *auto* names the 

228 dimensions the user handed to the searcher. They carry no candidates, 

229 so without naming them they are indistinguishable from dimensions the 

230 file never mentioned, and a ``train_yaml`` would pin them. 

231 """ 

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

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

234 auto: Set[str] = set() 

235 

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

237 if short_key not in para_raw: 

238 continue 

239 value = para_raw[short_key] 

240 

241 if isinstance(value, int): 

242 constraint[_FIXED_DIM_MAP[short_key]] = value 

243 search_space[canonical_key] = [value] 

244 elif isinstance(value, list): 

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

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

247 auto.add(canonical_key) 

248 

249 return search_space, constraint, auto 

250 

251 

252# Every unified dimension ND can actually search. ``etp`` is declarable but 

253# has no ND dimension behind it, so it is always a fixed input. 

254_NOT_SEARCHABLE = frozenset({"etp"}) 

255 

256 

257def _describe_parallelism( 

258 search_space: Dict[str, List[int]], 

259 auto_dims: Any, 

260 declared: Dict[str, Any], 

261 from_train_yaml: bool, 

262) -> str: 

263 """Report how every parallelism dimension was resolved. 

264 

265 A dimension left out of the file is not switched off: it is pinned to the 

266 train.yaml value when there is one and searched freely when there is not. 

267 Pin a dimension to 1 to take it out of the strategy. 

268 """ 

269 parts = [] 

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

271 candidates = search_space.get(canonical_key) 

272 if short_key in _NOT_SEARCHABLE: 

273 fixed = candidates[0] if candidates else "default" 

274 state = f"fixed {fixed} (no ND dimension)" 

275 elif canonical_key in auto_dims: 

276 state = "searched (auto)" 

277 elif candidates is None: 

278 state = "searched (not declared)" 

279 elif len(candidates) == 1: 

280 source = ( 

281 "declared" if short_key in declared 

282 else "train.yaml" if from_train_yaml else "default" 

283 ) 

284 state = f"fixed {candidates[0]} ({source})" 

285 else: 

286 state = f"searched over {candidates}" 

287 parts.append(f"{short_key}={state}") 

288 return "; ".join(parts) 

289 

290 

291def _stated_recompute_modes(raw: Dict[str, Any]) -> Dict[str, Tuple[str, ...]]: 

292 """Return the trainer modes ``recompute_modes`` lets a search choose among, as the estimator's entry. 

293 

294 Under ``recompute: auto`` or ``per_layer`` the search chooses among the 

295 trainer's activation checkpoint modes: off and full, and its selective 

296 policy where a census prices it. ``recompute_modes`` narrows them. YAML 

297 reads an unquoted ``off`` as False. 

298 

299 Returns: 

300 ``{"recompute_modes": modes}``, or an empty dict where the search 

301 config states none. 

302 

303 Raises: 

304 ValueError: For an empty list, or a mode the trainer does not run. 

305 """ 

306 stated = raw.get("recompute_modes") 

307 if stated is None: 

308 return {} 

309 listed = list(stated) if isinstance(stated, (list, tuple)) else [stated] 

310 modes = tuple("off" if mode is False else str(mode) for mode in listed) 

311 unknown = sorted(set(modes) - {"off", "selective", "full"}) 

312 if unknown or not modes: 

313 raise ValueError(f"recompute_modes {listed}: expected some of off, selective and full") 

314 return {"recompute_modes": modes} 

315 

316 

317def _stated_recompute(raw: Dict[str, Any], parallelism_raw: Dict[str, Any]) -> Dict[str, Any]: 

318 """Return the estimator's recompute entries, from ``parallelism.recompute`` where it is stated. 

319 

320 ``parallelism.recompute`` states recompute as a degree is stated: one 

321 mode, a list of modes, or ``auto`` for every mode the trainer runs that 

322 the search can price. The search then chooses among them for each 

323 strategy, as ``recompute: auto`` with ``recompute_modes`` does, or for each 

324 layer where the top-level ``recompute`` says ``per_layer``. A mode left 

325 out is never chosen. 

326 

327 Returns: 

328 ``recompute_strategy``, and ``recompute_modes`` where a list narrows 

329 the modes. 

330 

331 Raises: 

332 ValueError: For a mode the trainer does not run, or when recompute 

333 is stated twice: the dimension beside ``recompute_modes``, or 

334 beside a top-level ``recompute`` that fixes one mode. 

335 """ 

336 stated = parallelism_raw.get("recompute") 

337 strategy = str(raw.get("recompute", "none")) 

338 if stated is None: 

339 return {"recompute_strategy": strategy, **_stated_recompute_modes(raw)} 

340 if "recompute_modes" in raw: 

341 raise ValueError("recompute modes are stated twice, in parallelism.recompute and recompute_modes: " 

342 "keep parallelism.recompute") 

343 if strategy not in ("none", "auto", "per_layer"): 

344 raise ValueError("recompute is stated twice: parallelism.recompute searches it as a dimension and the " 

345 f"top-level recompute fixes {strategy}; keep parallelism.recompute, with recompute: " 

346 "per_layer for a mode per layer") 

347 chosen = "per_layer" if strategy == "per_layer" else "auto" 

348 if not isinstance(stated, (list, tuple)) and str(stated).strip().lower() == "auto": 

349 return {"recompute_strategy": chosen} 

350 return {"recompute_strategy": chosen, **_stated_recompute_modes({"recompute_modes": stated})} 

351 

352 

353def _describe_recompute(estimator: Dict[str, Any]) -> str: 

354 """Report how a recompute dimension was resolved, as :func:`_describe_parallelism` reports a degree.""" 

355 modes = estimator.get("recompute_modes") 

356 among = f"among {list(modes)}" if modes else "among every mode it can price" 

357 per_layer = " per layer" if estimator["recompute_strategy"] == "per_layer" else "" 

358 return f"recompute=chosen{per_layer} {among}" 

359 

360 

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

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

363 

364 Supports two modes: 

365 

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

367 info must be present in the search config file. 

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

369 parameters and current parallelism values, then overlays the search 

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

371 

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

373 as fixed (single-element) entries. 

374 """ 

375 train_yaml_path = raw.get("train_yaml") 

376 base_config: Optional[NormalizedConfig] = None 

377 

378 if train_yaml_path: 

379 if not isinstance(train_yaml_path, str): 

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

381 base_raw = _load_yaml(train_yaml_path) 

382 base_config = _build_config_from_hp_yaml(base_raw) 

383 

384 model_spec: Dict[str, Any] 

385 run: Dict[str, Any] 

386 if base_config: 

387 model_spec = dict(base_config.model_spec) 

388 run = copy.deepcopy(base_config.run) 

389 else: 

390 model_spec = {} 

391 run = {} 

392 

393 # Override or supply model section from search.yaml 

394 search_model = _get_dict(raw, "model") 

395 if search_model: 

396 model_spec.update(search_model) 

397 run.setdefault("model", {}).update(_model_run(search_model)) 

398 

399 model_spec.setdefault("max_position_embeddings", 4096) 

400 model_spec.setdefault("local_batch_size", 1) 

401 

402 model_spec = _normalize_model_spec(model_spec) 

403 

404 cluster_spec = _get_dict(raw, "cluster") 

405 

406 pp_raw = _get_dict(raw, "pp_config") 

407 parallelism_raw = _get_dict(raw, "parallelism") 

408 constraint_raw = _get_dict(raw, "constraint") 

409 

410 search_space, parallelism_constraint, auto_dims = _parse_unified_parallelism( 

411 parallelism_raw 

412 ) 

413 

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

415 if base_config: 

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

417 if space_key not in search_space and space_key not in auto_dims: 

418 search_space[space_key] = candidates 

419 

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

421 constraint_raw.setdefault( 

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

423 ) 

424 

425 pp_config: Dict[str, Any] = { 

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

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

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

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

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

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

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

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

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

435 } 

436 

437 estimator: Dict[str, Any] = { 

438 "type": "symbolic", 

439 **_stated_recompute(raw, parallelism_raw), 

440 "enable_profiling_calibration": False, 

441 } 

442 

443 constraint: Dict[str, Any] = { 

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

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

446 **parallelism_constraint, 

447 } 

448 

449 summary = _describe_parallelism( 

450 search_space, auto_dims, parallelism_raw, bool(base_config) 

451 ) 

452 if parallelism_raw.get("recompute") is not None: 

453 summary += "; " + _describe_recompute(estimator) 

454 logger.info("parallelism resolved: %s", summary) 

455 

456 return NormalizedConfig( 

457 model_spec=model_spec, 

458 cluster_spec=cluster_spec, 

459 search_space=search_space, 

460 constraint=constraint, 

461 parallelism_summary=summary, 

462 estimator=estimator, 

463 pp_config=pp_config, 

464 run=run, 

465 ) 

466 

467 

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

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

470 

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

472 where each dimension is declared as:: 

473 

474 parallelism: 

475 tp: 4 # scalar → fixed input 

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

477 pp: auto # string → let the searcher decide 

478 

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

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

481 

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

483 cluster: 

484 num_nodes: 4 

485 cards_per_node: 16 # optional: defaults to the -A device type 

486 parallelism: 

487 dp: [1, 2, 4] 

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

489 

490 Dimensions absent from ``parallelism`` are inherited from 

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

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

493 

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

495 standalone example. 

496 

497 Args: 

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

499 

500 Returns: 

501 A :class:`NormalizedConfig` instance. 

502 

503 Raises: 

504 FileNotFoundError: If the file does not exist. 

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

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

507 ImportError: If PyYAML is not installed. 

508 """ 

509 raw = _load_yaml(path) 

510 return _build_config_from_search_yaml(raw) 

511 

512 

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

514 

515_ACCEL_TO_SEARCH = { 

516 "dp_shard": "data_parallel_shard_degree", 

517 "dp_replicate": "data_parallel_replicate_degree", 

518 "tp_degree": "tensor_parallel_degree", 

519 "pipeline_parallel_degree": "pipeline_parallel_degree", 

520 "context_parallel_degree": "context_parallel_degree", 

521 "expert_parallel_degree": "expert_parallel_degree", 

522 "expert_tensor_parallel_degree": "expert_tensor_parallel_degree", 

523} 

524 

525_AUTO_MODELS_ACCEL_TO_SEARCH = { 

526 "tp_size": "tensor_parallel_degree", 

527 "pp_size": "pipeline_parallel_degree", 

528 "cp_size": "context_parallel_degree", 

529 "ep_size": "expert_parallel_degree", 

530} 

531 

532 

533def _census_replacements(raw: Dict[str, Any], census_seq_len: int) -> tuple: 

534 """The module replacements a census runs its layers with: the ones the train.yaml's plan_overrides install.""" 

535 if not census_seq_len: 

536 return () 

537 return replacement_specs(raw.get("plan_overrides") or ()) 

538 

539 

540def _training_seq_len(raw: Dict[str, Any]) -> Any: 

541 """Return the training sequence length an AutoModels YAML states, or None. 

542 

543 Reads the three spellings the SAPP-ND parser accepts, in its order, so the 

544 two halves of the cost model agree on where the length comes from: the 

545 Online path's, an Indexed Dataset's, the legacy one. 

546 """ 

547 dataset_raw = _get_dict(raw, "dataset") 

548 return (_get_dict(dataset_raw, "data_transform").get("max_seq_len") 

549 or _get_dict(dataset_raw, "data_config").get("seq_length") 

550 or _get_dict(raw, "data").get("max_seq_len")) 

551 

552 

553def _load_auto_models_model_spec_from_yaml(raw: Dict[str, Any]) -> Dict[str, Any]: 

554 """Load model dimensions and training attributes from AutoModels YAML.""" 

555 model_raw = _get_dict(raw, "model") 

556 training_raw = _get_dict(raw, "training") 

557 context_raw = _get_dict(raw, "context") 

558 seq_len = _training_seq_len(raw) 

559 # A census builds its layers from the checkpoint's config, which only 

560 # this reader resolves: the search hands ND the spec, its records in it. 

561 census_seq_len = int(seq_len or 4096) if context_raw.get("census") else 0 

562 model_spec = _load_auto_models_model_spec( 

563 model_raw, context_raw.get("visual_seq_len"), census_seq_len, bool(context_raw.get("census_spec")), 

564 _census_replacements(raw, census_seq_len), 

565 ) 

566 if seq_len: 

567 model_spec["max_position_embeddings"] = seq_len 

568 else: 

569 logger.warning( 

570 "no dataset.data_transform.max_seq_len, dataset.data_config.seq_length " 

571 "nor data.max_seq_len: costing the model's context limit of %s tokens, " 

572 "which for a long-context model puts every candidate out of memory", 

573 model_spec.get("max_position_embeddings", 4096), 

574 ) 

575 model_spec.setdefault("max_position_embeddings", 4096) 

576 if context_raw.get("device_num") is not None: 

577 model_spec["device_num"] = int(context_raw["device_num"]) 

578 model_spec["local_batch_size"] = training_raw.get("micro_batch_size", 1) 

579 model_spec["compute_dtype"] = model_raw.get("torch_dtype", "bfloat16") 

580 return model_spec 

581 

582 

583def _load_auto_models_parallelism( 

584 raw: Dict[str, Any], 

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

586 """Load fixed parallelism degrees from AutoModels YAML.""" 

587 accelerator_raw = _get_dict(raw, "accelerator") 

588 fsdp_raw = _get_dict(raw, "fsdp_config") 

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

590 

591 dp_shard_size = fsdp_raw.get("dp_shard_size") 

592 if dp_shard_size is not None: 

593 search_space["data_parallel_shard_degree"] = [int(dp_shard_size)] 

594 for field_name, search_name in _AUTO_MODELS_ACCEL_TO_SEARCH.items(): 

595 value = accelerator_raw.get(field_name) 

596 if value is not None: 

597 search_space[search_name] = [int(value)] 

598 

599 data_parallel_size = int(dp_shard_size or 1) 

600 pp_degree = max(1, int(accelerator_raw.get("pp_size", 1) or 1)) 

601 return search_space, data_parallel_size, pp_degree 

602 

603 

604def _build_config_from_auto_models_yaml(raw: Dict[str, Any]) -> NormalizedConfig: 

605 """Construct a normalized config from the current AutoModels schema.""" 

606 model_spec = _load_auto_models_model_spec_from_yaml(raw) 

607 search_space, data_parallel_size, pp_degree = _load_auto_models_parallelism(raw) 

608 training_raw = _get_dict(raw, "training") 

609 activation_raw = _get_dict(raw, "activation_checkpoint") 

610 

611 global_batch_size = int(training_raw.get("global_batch_size", 0) or 0) 

612 local_batch_size = int(model_spec["local_batch_size"] or 1) 

613 micro_batch_num = ( 

614 global_batch_size // (local_batch_size * data_parallel_size) 

615 if global_batch_size 

616 and global_batch_size % (local_batch_size * data_parallel_size) == 0 

617 else 1 

618 ) 

619 

620 recompute_strategy = _recompute_strategy(activation_raw.get("mode"), "activation_checkpoint.mode") 

621 return NormalizedConfig( 

622 model_spec=model_spec, 

623 cluster_spec={}, 

624 search_space=search_space, 

625 constraint={ 

626 "global_batch_size": global_batch_size, 

627 "memory_limit_gb": 0.0, 

628 }, 

629 estimator={ 

630 "type": "symbolic", 

631 "recompute_strategy": recompute_strategy, 

632 }, 

633 pp_config={ 

634 "pp_degree": pp_degree, 

635 "stage_partition_mode": "uniform", 

636 "micro_batch_num": max(1, micro_batch_num), 

637 }, 

638 run=_stated_run(raw), 

639 ) 

640 

641 

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

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

644 

645 Extracts current AutoModels fields when root-level ``training`` and 

646 ``accelerator`` sections are present. The legacy ``model.name`` / 

647 ``model.config_overrides`` schema remains supported for compatibility. 

648 

649 Legacy parsing extracts model identifiers from ``model.config_overrides``, 

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

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

652 ``train.gradient_checkpointing.activation_checkpoint``. 

653 

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

655 """ 

656 if is_auto_models_schema(raw): 

657 return _build_config_from_auto_models_yaml(raw) 

658 

659 model_raw = _get_dict(raw, "model") 

660 train_raw = _get_dict(raw, "train") 

661 data_raw = _get_dict(raw, "data") 

662 accel_raw = _get_dict(train_raw, "accelerator") 

663 gc_raw = _get_dict(train_raw, "gradient_checkpointing") 

664 

665 # --- model_spec --- 

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

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

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

669 if isinstance(overrides, dict): 

670 model_spec.update(overrides) 

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

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

673 

674 # dtype from train.mixed_precision 

675 mp_raw = _get_dict(train_raw, "mixed_precision") 

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

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

678 

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

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

681 

682 # --- search_space from train.accelerator --- 

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

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

685 val = accel_raw.get(hkey) 

686 if val is not None: 

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

688 

689 # --- constraint from train --- 

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

691 constraint: Dict[str, Any] = { 

692 "global_batch_size": gbs or 0, 

693 "memory_limit_gb": 0.0, 

694 } 

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

696 

697 # --- pp_config --- 

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

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

700 pp_config: Dict[str, Any] = { 

701 "pp_degree": pp_degree, 

702 "stage_partition_mode": "uniform", 

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

704 } 

705 

706 # --- estimator from gradient_checkpointing --- 

707 estimator: Dict[str, Any] = { 

708 "type": "symbolic", 

709 "recompute_strategy": _recompute_strategy( 

710 gc_raw.get("activation_checkpoint"), "train.gradient_checkpointing.activation_checkpoint" 

711 ), 

712 } 

713 

714 model_spec = _normalize_model_spec(model_spec) 

715 

716 return NormalizedConfig( 

717 model_spec=model_spec, 

718 cluster_spec=cluster_spec, 

719 search_space=search_space, 

720 constraint=constraint, 

721 estimator=estimator, 

722 pp_config=pp_config, 

723 run=_stated_run(raw), 

724 ) 

725 

726 

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

728 """Read a HyperParallel YAML configuration file. 

729 

730 For the current AutoModels Trainer schema, model dimensions are resolved 

731 from ``model.pretrained_model_name_or_path`` through the shared 

732 Transformers config path. Parallelism is read from root-level 

733 ``accelerator`` / ``fsdp_config`` sections. The legacy 

734 ``model.config_overrides`` / ``train.accelerator`` schema remains 

735 supported for standalone cost-model configurations. 

736 

737 .. note:: 

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

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

740 which accepts cluster and search-space parameters. 

741 

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

743 YAML sections. 

744 

745 Args: 

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

747 

748 Returns: 

749 A :class:`NormalizedConfig` instance. 

750 

751 Raises: 

752 FileNotFoundError: If the file does not exist. 

753 ValueError: If the file cannot be parsed. 

754 ImportError: If PyYAML is not installed. 

755 """ 

756 raw = _load_yaml(path) 

757 return _build_config_from_hp_yaml(raw)