Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / _hf_model_spec.py: 99%

256 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"""Shared Transformers-config resolution for the Auto Parallel cost model. 

16 

17Both the SAPP-ND parser (``CostModelParserHyperV2``) and the config-adapter 

18loader need the same model dimensions out of an AutoModels ``model`` section. 

19This module owns that translation exactly once, so the two callers cannot 

20drift apart on field names, aliases, or fallback behaviour. 

21 

22It is also the single place that imports ``transformers``. The import is 

23function-local because ``transformers`` is not a hard dependency of 

24``hyper_parallel`` (``requirements.txt`` only pins numpy) and the non-Hyper 

25cost-model backends must keep working without it. 

26""" 

27import copy 

28import dataclasses 

29import hashlib 

30import importlib 

31import json 

32import logging 

33import os 

34import sys 

35from typing import Any, Dict, Mapping, Optional, Tuple 

36 

37from hyper_parallel.auto_parallel._layer_census import ( 

38 CensusUnavailable, 

39 census_activations, 

40 census_output_activations, 

41) 

42from hyper_parallel.auto_parallel._layer_stack import spec_layer_stack, tower_layer_stack 

43from hyper_parallel.auto_parallel._model_spec import ModelSpec 

44from hyper_parallel.auto_parallel._op_profiles import ( 

45 infer_arch, 

46 infer_qk_norm, 

47 infer_qkv_bias, 

48 infer_shared_expert_gate, 

49 resolve_ops, 

50) 

51from hyper_parallel.auto_parallel._spec_census import census_model_spec 

52 

53logger = logging.getLogger(__name__) 

54 

55# AutoModels root sections that identify the current Trainer schema. 

56_AUTO_MODELS_SECTIONS = ("training", "accelerator", "fsdp_config") 

57 

58# ``model`` keys forwarded verbatim to ``AutoConfig.from_pretrained``. 

59_CONFIG_KWARG_NAMES = ( 

60 "cache_dir", "local_files_only", "revision", "subfolder", 

61 "token", "trust_remote_code", 

62) 

63 

64# Where the model directory is named. A from_config recipe builds the model 

65# through a factory (``_target_`` plus ``config_path``) and never loads a 

66# checkpoint, so it carries no pretrained path. 

67_MODEL_PATH_ALIASES = ("pretrained_model_name_or_path", "config_path") 

68 

69# The config a recipe's own factory builds, for the factories whose model 

70# ``AutoConfig`` cannot resolve: the path beside ``_target_`` is then a 

71# directory Transformers refuses, and the shape the run really trains is the 

72# one this function returns. DeepSeek-V4.1 is the case that forced it: 

73# Transformers 5.14.1 has no ``deepseek_v41``, and the factory's arguments are 

74# a recipe for deriving the shape (a divisor and an expert count) rather than 

75# the canonical fields :func:`_explicit_overrides` can harvest, so nothing 

76# short of calling it knows what the run builds. A factory is consulted 

77# BEFORE ``AutoConfig``, because where both answer they answer differently and 

78# the factory is the one that says what the run trains: V4.1's crops the 

79# released model eightfold, and the released config would price a model nobody 

80# runs. One line a family, and the entry is the declaration that the two are 

81# a pair. 

82_CONFIG_FACTORIES = { 

83 "hyper_parallel.models.deepseek_v41.adapter.validation.cropped_model." 

84 "build_cropped_deepseek_v41": 

85 "hyper_parallel.models.deepseek_v41.adapter.validation.cropped_model." 

86 "build_deepseek_v41_validation_config", 

87} 

88 

89# Canonical text-tower fields, with the Transformers aliases that carry them. 

90# First populated alias wins, so Hyper-internal names keep priority over the 

91# Hugging Face spelling when a config happens to define both. 

92_TEXT_FIELD_ALIASES: Dict[str, Tuple[str, ...]] = { 

93 "hidden_size": ("hidden_size",), 

94 "num_hidden_layers": ("num_hidden_layers",), 

95 "num_attention_heads": ("num_attention_heads",), 

96 "num_key_value_heads": ("num_key_value_heads",), 

97 "head_dim": ("head_dim",), 

98 "intermediate_size": ("intermediate_size",), 

99 "vocab_size": ("vocab_size",), 

100 "max_position_embeddings": ("max_position_embeddings",), 

101 "num_experts": ("num_experts", "n_routed_experts"), 

102 "num_experts_per_tok": ("num_experts_per_tok",), 

103 "num_shared_experts": ("n_shared_experts", "num_shared_experts"), 

104 "moe_intermediate_size": ("moe_intermediate_size",), 

105 "shared_expert_intermediate_size": ("shared_expert_intermediate_size",), 

106 "first_k_dense_replace": ("first_k_dense_replace",), 

107 "mtp_depth": ("mtp_depth", "num_nextn_predict_layers", "mtp_num_hidden_layers"), 

108 "multiple_of": ("multiple_of",), 

109 "ffn_dim_multiplier": ("ffn_dim_multiplier",), 

110 "kv_lora_rank": ("kv_lora_rank",), 

111 "q_lora_rank": ("q_lora_rank",), 

112 "qk_rope_head_dim": ("qk_rope_head_dim",), 

113 "qk_nope_head_dim": ("qk_nope_head_dim",), 

114 "v_head_dim": ("v_head_dim",), 

115 "attn_output_gate": ("attn_output_gate",), 

116 "tie_word_embeddings": ("tie_word_embeddings",), 

117 # Llama's attention_bias biases all four projections; Qwen2's configs 

118 # state none and bias the query, key and value projections alone. 

119 "qkv_bias": ("qkv_bias", "attention_bias"), 

120 "o_bias": ("o_bias", "attention_bias"), 

121 "mlp_bias": ("mlp_bias",), 

122 # Stated by a few families; Qwen3's config does not state its own. 

123 "qk_norm": ("qk_norm", "use_qk_norm", "qk_layernorm"), 

124 # A hybrid stack states its layers; without this every layer is costed 

125 # as full attention, which is quadratic in the sequence length. 

126 "layer_types": ("layer_types",), 

127 "linear_num_key_heads": ("linear_num_key_heads",), 

128 "linear_key_head_dim": ("linear_key_head_dim",), 

129 "linear_num_value_heads": ("linear_num_value_heads",), 

130 "linear_value_head_dim": ("linear_value_head_dim",), 

131 "linear_conv_kernel_dim": ("linear_conv_kernel_dim",), 

132 # The dtype the checkpoint's weights are saved in, which the runtime 

133 # loads them in under its default torch_dtype of auto; newer 

134 # Transformers name it dtype. 

135 "torch_dtype": ("dtype", "torch_dtype"), 

136} 

137 

138# Vision towers use their own spelling for the shared concepts. 

139_VISION_FIELD_ALIASES: Dict[str, Tuple[str, ...]] = { 

140 "hidden_size": ("hidden_size",), 

141 "num_hidden_layers": ("depth", "num_hidden_layers"), 

142 "num_attention_heads": ("num_heads", "num_attention_heads"), 

143 "intermediate_size": ("intermediate_size",), 

144 "out_hidden_size": ("out_hidden_size",), 

145 "patch_size": ("patch_size",), 

146 "spatial_merge_size": ("spatial_merge_size",), 

147 "num_position_embeddings": ("num_position_embeddings",), 

148} 

149 

150# Fallback when a vision tower declares no positional-embedding grid. 

151_DEFAULT_VISUAL_SEQ_LEN = 1024 

152 

153# Keys of ``model.config_overrides`` that state the run, not the model. The 

154# model spec never sees them; the Hyper parser reads them into its ExecSpec. 

155EXEC_OVERRIDE_KEYS = ( 

156 "offset", "full_rec", "sel_rec", "capacity_factor", "cap_fact", "use_gmm", "gmm", "seq_length", 

157) 

158 

159 

160def _declares_pretrained_path(mapping: Any) -> bool: 

161 """Return whether the ``model`` section names a Transformers checkpoint. 

162 

163 This is the one key only this schema has: the older HyperParallel yamls 

164 name a checkpoint as ``model.weights_path``. Recognising it means a 

165 config needs no empty ``training``/``accelerator``/``fsdp_config`` 

166 section just to be identified. 

167 """ 

168 if isinstance(mapping, Mapping): 

169 model = mapping.get("model") 

170 else: 

171 model = getattr(mapping, "model", None) 

172 if model is None: 

173 return False 

174 if isinstance(model, Mapping): 

175 return bool(model.get("pretrained_model_name_or_path")) 

176 return bool(getattr(model, "pretrained_model_name_or_path", None)) 

177 

178 

179def is_auto_models_schema(mapping: Any) -> bool: 

180 """Return whether *mapping* looks like an AutoModels Trainer config. 

181 

182 Accepts a plain mapping or any object exposing the root sections as 

183 attributes, so the SAPP-ND ``Config`` tree and a parsed YAML dict can be 

184 tested with the same call. 

185 """ 

186 if _declares_pretrained_path(mapping): 

187 return True 

188 if isinstance(mapping, Mapping): 

189 return any(name in mapping for name in _AUTO_MODELS_SECTIONS) 

190 holder = getattr(mapping, "__dict__", None) 

191 if isinstance(holder, dict): 

192 return any(name in holder for name in _AUTO_MODELS_SECTIONS) 

193 return False 

194 

195 

196def _first_attr(config: Any, names: Tuple[str, ...], default: Any = None) -> Any: 

197 """Return the first populated attribute of *config* among *names*.""" 

198 for name in names: 

199 value = getattr(config, name, None) 

200 if value is not None: 

201 return value 

202 return default 

203 

204 

205def _text_tower(model_config: Any) -> Any: 

206 """Return the language-model sub-config of a composite config. 

207 

208 Multimodal Transformers configs (``Qwen3VLMoeConfig`` and friends) keep 

209 the language model under ``text_config`` and expose none of its fields at 

210 the top level, so reading ``hidden_size`` directly raises AttributeError. 

211 """ 

212 return getattr(model_config, "text_config", None) or model_config 

213 

214 

215def _at_stated_depth(model_config: Any, explicit: Mapping[str, Any]) -> Any: 

216 """*model_config* at the depth the model section states, as a crop of the released model is built. 

217 

218 A ``num_hidden_layers`` stated beside the checkpoint crops it to its first layers. The formulas read that 

219 count as stated, but the census builds its layers from the config, so the config it builds from has to 

220 carry the crop too, its ``layer_types`` cut to the same layers; otherwise the census lists the release's 

221 layers against the crop's count. 

222 

223 Returns: 

224 *model_config* itself where the section states no depth or the release's own, else a copy at that depth. 

225 """ 

226 depth = explicit.get("num_hidden_layers") 

227 if depth is None or getattr(_text_tower(model_config), "num_hidden_layers", None) == int(depth): 

228 return model_config 

229 cropped = copy.deepcopy(model_config) 

230 text = _text_tower(cropped) 

231 text.num_hidden_layers = int(depth) 

232 layer_types = getattr(text, "layer_types", None) 

233 if isinstance(layer_types, (list, tuple)): 

234 text.layer_types = list(layer_types)[:int(depth)] 

235 return cropped 

236 

237 

238def _spec_from_aliases(config: Any, aliases: Dict[str, Tuple[str, ...]]) -> Dict[str, Any]: 

239 """Collect the canonical fields declared by *config*.""" 

240 spec = {} 

241 for canonical, names in aliases.items(): 

242 value = _first_attr(config, names) 

243 if value is not None: 

244 spec[canonical] = value 

245 return spec 

246 

247 

248def _derive_shared_experts(spec: Dict[str, Any]) -> None: 

249 """Infer the shared-expert count from its total feed-forward width. 

250 

251 Qwen2-MoE style configs declare one wide shared expert instead of a 

252 count. The cost model only ever uses ``n_shared_exp * hff_exp``, so 

253 encoding that width as several narrow experts is width-equivalent. 

254 """ 

255 if spec.get("num_shared_experts"): 

256 return 

257 moe_inter = int(spec.get("moe_intermediate_size", 0) or 0) 

258 shared_inter = int(spec.get("shared_expert_intermediate_size", 0) or 0) 

259 if moe_inter and shared_inter: 

260 spec["num_shared_experts"] = max(1, shared_inter // moe_inter) 

261 

262 

263def _derive_dense_ffn_width(spec: Dict[str, Any]) -> None: 

264 """Fill the dense feed-forward width for an all-MoE config. 

265 

266 A model whose every layer is MoE (Qwen3.5-MoE) declares no 

267 ``intermediate_size``, but the cost model prices the shared expert at 

268 that width, so leaving it absent drops the shared expert entirely. 

269 The shared expert's own width is the value meant there. 

270 """ 

271 if spec.get("intermediate_size"): 

272 return 

273 width = spec.get("shared_expert_intermediate_size") or spec.get("moe_intermediate_size") 

274 if width: 

275 spec["intermediate_size"] = int(width) 

276 

277 

278def _visual_seq_len(vision_spec: Dict[str, Any], override: Optional[int]) -> int: 

279 """Resolve the encoder sequence length in merged visual tokens. 

280 

281 The true count depends on the image resolution served by the dataset, 

282 which no configuration we read carries. The positional-embedding grid 

283 divided by the spatial merge is the tightest bound available from the 

284 model alone, so it is the default and *override* exists to correct it. 

285 """ 

286 if override: 

287 logger.info("visual sequence length taken from context.visual_seq_len: %d", override) 

288 return int(override) 

289 grid = int(vision_spec.get("num_position_embeddings", 0) or 0) 

290 merge = max(1, int(vision_spec.get("spatial_merge_size", 1) or 1)) 

291 if not grid: 

292 logger.warning( 

293 "vision tower declares no num_position_embeddings; assuming %d visual tokens. " 

294 "Set context.visual_seq_len to the value your dataset produces.", 

295 _DEFAULT_VISUAL_SEQ_LEN, 

296 ) 

297 return _DEFAULT_VISUAL_SEQ_LEN 

298 derived = max(1, grid // (merge * merge)) 

299 logger.info( 

300 "visual sequence length derived from the vision config: %d " 

301 "(num_position_embeddings=%d, spatial_merge_size=%d). " 

302 "Set context.visual_seq_len to override.", 

303 derived, grid, merge, 

304 ) 

305 return derived 

306 

307 

308def _get_hf_config(model_raw: Mapping[str, Any]) -> Any: 

309 """Call the AutoModels Transformers entry point for *model_raw*.""" 

310 # Transformers is an optional dependency; import only when a config 

311 # actually needs it so the other cost-model backends stay importable. 

312 from hyper_parallel.models._transformers.config_resolver import get_hf_config # pylint: disable=C0415 

313 

314 config_kwargs = { 

315 name: model_raw[name] 

316 for name in _CONFIG_KWARG_NAMES 

317 if model_raw.get(name) is not None 

318 } 

319 return get_hf_config( 

320 str(_model_path(model_raw)), 

321 str(model_raw.get("attn_implementation", "sdpa")), 

322 model_raw.get("torch_dtype", "auto"), 

323 **config_kwargs, 

324 ) 

325 

326 

327def checkpoint_configs(model_raw: Mapping[str, Any]) -> Tuple[Any, Any]: 

328 """The Transformers config of the checkpoint *model_raw* names, and its language model's.""" 

329 config = _get_hf_config(model_raw) 

330 return config, _text_tower(config) 

331 

332 

333def _factory_config(model_raw: Mapping[str, Any]) -> Any: 

334 """The config the recipe's own factory builds, or None where it names none. 

335 

336 The model section's keys are offered to the factory by name, filtered to 

337 the ones it accepts, so a recipe states its shape once and the cost model 

338 reads the same statement the trainer does. Anything the factory cannot 

339 take, such as a dtype or a placement switch, is left out rather than 

340 refused. 

341 

342 Returns: 

343 A Transformers config, or None where ``_target_`` names no factory 

344 this knows. 

345 

346 Raises: 

347 ValueError: If the factory is known but cannot build its config, since 

348 a recipe that names one and then fails is a broken recipe rather 

349 than a model to price by formula. 

350 """ 

351 import inspect # pylint: disable=C0415 

352 

353 target = str(model_raw.get("_target_") or "") 

354 path = _CONFIG_FACTORIES.get(target) 

355 if path is None: 

356 return None 

357 module_path, _, name = path.rpartition(".") 

358 try: 

359 factory = getattr(importlib.import_module(module_path), name) 

360 accepted = inspect.signature(factory).parameters 

361 offered = {key: value for key, value in model_raw.items() if key in accepted} 

362 logger.info("building the config %s states, through %s", target.rpartition(".")[2], name) 

363 return factory(**offered) 

364 except (ImportError, AttributeError, TypeError, OSError, KeyError, ValueError) as exc: 

365 raise ValueError( 

366 f"model._target_ names {target}, whose config factory {name} could not build its " 

367 f"config ({exc}); the cost model prices what that factory builds, so fix the recipe " 

368 f"rather than let the formulas price a different model" 

369 ) from exc 

370 

371 

372def _model_path(model_raw: Mapping[str, Any]) -> Optional[str]: 

373 """Return the model directory the section names, under any accepted key.""" 

374 for alias in _MODEL_PATH_ALIASES: 

375 value = model_raw.get(alias) 

376 if value: 

377 return str(value) 

378 return None 

379 

380 

381def _explicit_overrides(model_raw: Mapping[str, Any]) -> Dict[str, Any]: 

382 """Return the model fields the model section states itself, as a plain dict. 

383 

384 ``config_overrides`` is the declared form, less its run keys 

385 (:data:`EXEC_OVERRIDE_KEYS`). A from_config recipe instead passes the shape 

386 it builds as factory arguments beside ``_target_``, such as a 

387 ``num_hidden_layers`` that crops the released model; those name canonical 

388 fields, so they are overrides too, and the released config must not win 

389 over them. ``config_overrides`` keeps priority where both are present. A 

390 value of ``auto``, which ``torch_dtype`` takes to load the checkpoint's 

391 own, states nothing over the checkpoint. 

392 """ 

393 overrides = model_raw.get("config_overrides") 

394 explicit = {} 

395 if isinstance(overrides, Mapping): 

396 explicit = {key: value for key, value in overrides.items() if key not in EXEC_OVERRIDE_KEYS} 

397 for field in _TEXT_FIELD_ALIASES: 

398 value = model_raw.get(field) 

399 if value is not None and value != "auto": 

400 explicit.setdefault(field, value) 

401 return explicit 

402 

403 

404def exec_overrides(model_raw: Mapping[str, Any]) -> Dict[str, Any]: 

405 """Return the run keys of the ``config_overrides`` fallback. 

406 

407 Args: 

408 model_raw: The ``model`` section, as a plain mapping. 

409 

410 Returns: 

411 The entries whose keys are in :data:`EXEC_OVERRIDE_KEYS`. 

412 """ 

413 overrides = model_raw.get("config_overrides") 

414 if not isinstance(overrides, Mapping): 

415 return {} 

416 return {key: value for key, value in overrides.items() if key in EXEC_OVERRIDE_KEYS} 

417 

418 

419def _validated(spec: Dict[str, Any]) -> Dict[str, Any]: 

420 """Check *spec* against the model IR before any consumer reads it. 

421 

422 Parsing and re-serialising is what makes the schema load-bearing rather 

423 than advisory: a field the model declares but the IR does not know would 

424 be dropped here, and an incoherent one raises by name. The op profile 

425 and the layer stack are settled here too, so the serialised spec states 

426 what it will be priced with and which kind each layer is, even when the 

427 family was only inferred from the model name and the stack from 

428 ``layer_types`` or ``first_k_dense_replace``; and so is whether its 

429 attention normalizes queries and keys, which a Transformers config does 

430 not state, and the biases and norms its layers hold: a Transformers 

431 decoder layer holds two RMSNorms, no bias unless its config states one 

432 or its family has one, and a shared expert gated where its family 

433 gates it. ``layer_types`` is 

434 consumed: ``layers`` says the same, checked against the profile. A 

435 vision tower states its stack likewise. 

436 """ 

437 typed = ModelSpec.from_dict(spec) 

438 typed = dataclasses.replace( 

439 typed, arch=typed.arch or infer_arch(typed.name), 

440 qk_norm=typed.qk_norm if typed.qk_norm is not None else infer_qk_norm(typed.name), 

441 qkv_bias=typed.qkv_bias if typed.qkv_bias is not None else infer_qkv_bias(typed.name), 

442 o_bias=bool(typed.o_bias), mlp_bias=bool(typed.mlp_bias), norm_bias=bool(typed.norm_bias), 

443 layer_norms=typed.layer_norms or 2, 

444 shared_expert_gate=(typed.shared_expert_gate if typed.shared_expert_gate is not None 

445 else infer_shared_expert_gate(typed.name)), 

446 ) 

447 resolve_ops(typed.arch, typed.ops) 

448 layers = spec_layer_stack(typed).to_layers() 

449 vision = typed.vision 

450 if vision is not None: 

451 vision = dataclasses.replace(vision, layers=tower_layer_stack(vision).to_layers()) 

452 return dataclasses.replace(typed, layers=layers, vision=vision, layer_types=None).to_dict() 

453# Canonical fields that say how a model is held, not what it is: a section 

454# stating only these describes no model. 

455_HOLDING_FIELDS = frozenset({"torch_dtype"}) 

456 

457 

458def _describes_a_model(explicit: Mapping[str, Any]) -> bool: 

459 """Whether the fields a section states describe a model, beyond how it is held.""" 

460 return any(key not in _HOLDING_FIELDS for key in explicit) 

461 

462 

463def _dtype_name(value: Any) -> Any: 

464 """A dtype as its plain name, ``bfloat16`` for ``torch.bfloat16``; anything else as it is.""" 

465 return str(value).replace("torch.", "") if value is not None else None 

466 

467 

468# The censuses this process has run, by config, layer stack and length: a 

469# harness pricing several runs of one model runs one. 

470_CENSUSES: Dict[Tuple[Any, ...], Dict[str, Any]] = {} 

471 

472# Where a census is kept between runs, which it then costs nothing: 

473# ND_CENSUS_CACHE names another folder, or is "off" to keep none. 

474_CENSUS_FOLDER = os.path.join(os.path.expanduser("~"), ".cache", "hyper_parallel", "census") 

475# The package whose files a kept census is checked against. 

476_PACKAGE_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 

477# The search's own code, which runs after a census and hands it only what its 

478# key holds: a change there leaves the kept censuses valid. 

479_NOT_CENSUS_CODE = ( 

480 "hyper_parallel.auto_parallel.sapp_nd", 

481 "hyper_parallel.auto_parallel.sapp_ppb", 

482 "hyper_parallel.auto_parallel.config_adapter", 

483) 

484 

485 

486def _census_file(key: Tuple[Any, ...]) -> Optional[str]: 

487 """The file a census of *key* is kept in, named by the key and the torch and Transformers it runs on. 

488 

489 ``None`` where ``ND_CENSUS_CACHE`` is ``off``. 

490 """ 

491 folder = os.environ.get("ND_CENSUS_CACHE") or _CENSUS_FOLDER 

492 if folder.lower() == "off": 

493 return None 

494 versions = [str(getattr(sys.modules.get(name), "__version__", None)) for name in ("torch", "transformers")] 

495 name = hashlib.sha256(json.dumps([repr(key), *versions]).encode("utf-8")).hexdigest() 

496 return os.path.join(folder, name[:32] + ".json") 

497 

498 

499def _source_digest(path: str) -> str: 

500 """The SHA-256 of the file at *path*, its line endings read as Python reads them. 

501 

502 Two checkouts of one commit can hold a file with Windows line endings in 

503 one and Unix ones in the other, which is the same code. 

504 """ 

505 with open(path, "rb") as handle: 

506 return hashlib.sha256(handle.read().replace(b"\r\n", b"\n")).hexdigest() 

507 

508 

509def _census_sources() -> Dict[str, str]: 

510 """The digest of each file of the package this process has loaded, the search's left out, by its path. 

511 

512 A census runs on them: its own code, the kernels' contracts, the 

513 checkpoint policy and the modules a run's replacements build. 

514 """ 

515 sources = {} 

516 for name, module in list(sys.modules.items()): 

517 path = getattr(module, "__file__", None) 

518 if path and name.split(".")[0] == "hyper_parallel" and not name.startswith(_NOT_CENSUS_CODE): 

519 sources[os.path.relpath(path, _PACKAGE_ROOT).replace(os.sep, "/")] = _source_digest(path) 

520 return sources 

521 

522 

523def _kept_census(path: str) -> Optional[Dict[str, Any]]: 

524 """The census kept in *path*, or ``None`` where there is none or a file it ran on has changed since.""" 

525 try: 

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

527 kept = json.load(handle) 

528 if all(_source_digest(os.path.join(_PACKAGE_ROOT, source)) == digest 

529 for source, digest in kept["sources"].items()): 

530 return kept["census"] 

531 except (OSError, ValueError, KeyError, TypeError, AttributeError): 

532 pass 

533 return None 

534 

535 

536def _keep_census(path: str, census: Dict[str, Any]) -> None: 

537 """Keep *census* in *path* with the digest of each file it ran on; failing costs the next run a census.""" 

538 partial = f"{path}.{os.getpid()}" 

539 try: 

540 os.makedirs(os.path.dirname(path), exist_ok=True) 

541 with open(partial, "w", encoding="utf-8") as handle: 

542 json.dump({"sources": _census_sources(), "census": census}, handle) 

543 os.replace(partial, path) 

544 except (OSError, TypeError, ValueError) as exc: 

545 logger.warning("the census could not be kept in %s: %s", path, exc) 

546 

547 

548def _census(text_config: Any, layers: Any, seq_length: int, replacements: Tuple[Any, ...] = ()) -> Dict[str, Any]: 

549 """The spec's census of each layer kind of *layers* and of the output layer, once per config and length. 

550 

551 A census is kept on disk (:func:`_census_file`) and read back by a later 

552 run of the same model, length and replacements, on the same torch and 

553 Transformers, while no file of the package it ran on has changed. 

554 """ 

555 key = ( 

556 text_config.to_json_string(), 

557 tuple((group["kind"], int(group["count"]), bool(group.get("mtp"))) for group in layers), 

558 int(seq_length), 

559 tuple((f"{spec.factory.__module__}.{spec.factory.__qualname__}", 

560 f"{spec.module_type.__module__}.{spec.module_type.__qualname__}", 

561 tuple(spec.match), bool(spec.exact_type)) for spec in replacements), 

562 ) 

563 if key not in _CENSUSES: 

564 path = _census_file(key) 

565 kept = _kept_census(path) if path is not None else None 

566 if kept is not None: 

567 logger.info("census of each layer kind and of the output layer at %d tokens read from %s", 

568 seq_length, path) 

569 _CENSUSES[key] = kept 

570 return copy.deepcopy(kept) 

571 logger.info("census of each layer kind and of the output layer at %d tokens%s", seq_length, 

572 f", {len(replacements)} module replacements installed" if replacements else "") 

573 try: 

574 kinds = census_activations(text_config, layers, seq_length, replacements) 

575 _CENSUSES[key] = { 

576 "activations": {kind: record.to_dict() for kind, record in kinds.items()}, 

577 "output_activations": census_output_activations(text_config, seq_length, replacements).to_dict(), 

578 } 

579 except CensusUnavailable as exc: 

580 # The formulas price the layers, as they do without a census: a 

581 # census of Transformers' own modules would price a layer the run 

582 # does not build. 

583 logger.warning("%s", _NOT_A_BOUND % exc) 

584 _CENSUSES[key] = {} 

585 else: 

586 if path is not None: 

587 _keep_census(path, _CENSUSES[key]) 

588 return copy.deepcopy(_CENSUSES[key]) 

589 

590 

591# What a run is told when a census it asked for did not happen. Measured 

592# without one, the formulas read 37 to 55% below Qwen3.5's peak and 51.1% 

593# below the DeepSeek-V4.1 crop's, so a memory a search filters on is an 

594# estimate and not an upper bound, and a strategy it calls feasible may not 

595# be. Said once, in the run's own log, where the number is read. 

596_NOT_A_BOUND = ( 

597 "no census of the layers: %s. The formulas price them instead, and without a census they have " 

598 "measured 37 to 55%% below a real peak, so this run's memory is an estimate and NOT a bound: a " 

599 "strategy it reports as fitting may still run out of memory" 

600) 

601 

602 

603def _no_census(census_seq_len: int, explicit: Mapping[str, Any]) -> None: 

604 """Warn that a census asked for cannot run, unless the overrides state its records. 

605 

606 A census builds its layers from the checkpoint's config; a search hands 

607 ND a spec whose overrides state the records its reader measured. 

608 """ 

609 if census_seq_len and not explicit.get("activations"): 

610 logger.warning("%s", _NOT_A_BOUND % "it needs the checkpoint's Transformers config") 

611 

612 

613def _stated_spec(model_raw: Mapping[str, Any], explicit: Dict[str, Any], census_seq_len: int) -> Dict[str, Any]: 

614 """The spec of a model its section states by hand, derived and settled as a resolved one is. 

615 

616 A model no Transformers config describes, every model Transformers 

617 cannot build, is stated field by field, and it takes the derivations a 

618 resolved spec does: a shared expert stated by its width counts as that 

619 many experts, and an all-MoE model's dense width is its shared 

620 expert's. Without them a hand-described all-MoE model kept no 

621 ``intermediate_size``, the width its shared expert is priced at, and 

622 its shared expert was dropped. 

623 """ 

624 explicit.setdefault("name", model_raw.get("name", "custom")) 

625 _derive_shared_experts(explicit) 

626 _derive_dense_ffn_width(explicit) 

627 _no_census(census_seq_len, explicit) 

628 return _validated(explicit) 

629 

630 

631def resolve_hf_model_spec( 

632 model_raw: Mapping[str, Any], 

633 visual_seq_len: Optional[int] = None, 

634 census_seq_len: int = 0, 

635 census_spec: bool = False, 

636 replacements: Tuple[Any, ...] = (), 

637) -> Dict[str, Any]: 

638 """Return canonical cost-model fields for a Trainer ``model`` section. 

639 

640 Resolves ``model.pretrained_model_name_or_path`` through the same 

641 Transformers path AutoModels uses. A composite (vision-language) config 

642 contributes its language tower under the canonical keys and its vision 

643 tower under ``"vision"``; a plain config leaves ``"vision"`` absent. 

644 

645 ``model.config_overrides`` stays supported for standalone cost-model 

646 search files, and doubles as the fallback when the Transformers config 

647 cannot be reached (offline node, unreachable repository). Explicit 

648 overrides always win over resolved values. Its run keys, 

649 :data:`EXEC_OVERRIDE_KEYS`, are not the model's: :func:`exec_overrides` 

650 returns them. 

651 

652 Args: 

653 model_raw: The ``model`` section, as a plain mapping. 

654 visual_seq_len: Optional override for the encoder sequence length. 

655 census_seq_len: The tokens to run a census of each layer kind of 

656 the language model at, and of its output layer 

657 (:mod:`hyper_parallel.auto_parallel._layer_census`), which the 

658 spec states as ``"activations"`` and ``"output_activations"``; 

659 0 runs none. The 

660 census builds its layers from the checkpoint's config: a spec 

661 from ``config_overrides`` alone gets none, and an override of a 

662 model field does not reach it. 

663 census_spec: Whether the language model's fields, its layer stack 

664 and its op counts are the census's, measured on the layers 

665 Transformers builds of the checkpoint 

666 (:func:`~hyper_parallel.auto_parallel._spec_census.census_model_spec`), 

667 rather than read from its config's fields. A spec from 

668 ``config_overrides`` alone is the overrides' either way. 

669 replacements: The module replacements the run installs 

670 (:func:`~hyper_parallel.auto_parallel._layer_census.replacement_specs`), 

671 which the census runs its layers with, so it measures what the 

672 trainer saves. 

673 

674 Returns: 

675 A dict of canonical model fields, always carrying ``"name"``. 

676 

677 Raises: 

678 ValueError: If neither a pretrained path nor overrides can supply 

679 the model dimensions. 

680 """ 

681 explicit = _explicit_overrides(model_raw) 

682 model_path = _model_path(model_raw) 

683 

684 if not model_path: 

685 if _describes_a_model(explicit): 

686 return _stated_spec(model_raw, explicit, census_seq_len) 

687 raise ValueError( 

688 "AutoModels train.yaml requires model.pretrained_model_name_or_path, " 

689 "model.config_path or model.config_overrides for Auto Parallel search" 

690 ) 

691 

692 # A recipe that builds its model through a factory is asked first: the 

693 # path beside _target_ is a directory that factory reads, not a checkpoint 

694 # to price, and where the two disagree the factory is what the run trains. 

695 model_config = _factory_config(model_raw) 

696 try: 

697 model_config = model_config if model_config is not None else _get_hf_config(model_raw) 

698 except (ImportError, OSError, ValueError, TypeError, AttributeError, KeyError) as exc: 

699 if _describes_a_model(explicit): 

700 logger.warning( 

701 "Transformers config resolution failed (%s); " 

702 "falling back to model.config_overrides", exc, 

703 ) 

704 return _stated_spec(model_raw, explicit, census_seq_len) 

705 raise ValueError( 

706 f"cannot resolve model.pretrained_model_name_or_path '{model_path}'; " 

707 "install transformers, set model.config_overrides, or make the config " 

708 "available offline (warm the HF_HOME cache, or pass local_files_only)" 

709 ) from exc 

710 

711 name = str(getattr(model_config, "model_type", None) or model_path) 

712 if census_spec: 

713 spec = census_model_spec(_at_stated_depth(model_config, explicit), explicit.get("arch"), name) 

714 else: 

715 spec = _spec_from_aliases(_text_tower(model_config), _TEXT_FIELD_ALIASES) 

716 _derive_shared_experts(spec) 

717 _derive_dense_ffn_width(spec) 

718 spec["name"] = name 

719 # The dtype the checkpoint is saved in, by name, which the census's 

720 # fields do not state. 

721 dtype = _first_attr(_text_tower(model_config), _TEXT_FIELD_ALIASES["torch_dtype"]) 

722 if dtype is not None: 

723 spec["torch_dtype"] = _dtype_name(dtype) 

724 

725 vision_config = getattr(model_config, "vision_config", None) 

726 if vision_config is not None: 

727 vision_spec = _spec_from_aliases(vision_config, _VISION_FIELD_ALIASES) 

728 vision_spec["name"] = f"{spec['name']}_vision" 

729 vision_spec["max_position_embeddings"] = _visual_seq_len(vision_spec, visual_seq_len) 

730 spec["vision"] = vision_spec 

731 

732 spec.update(explicit) 

733 resolved = _validated(spec) 

734 if census_seq_len: 

735 resolved.update(_census(_text_tower(model_config), resolved["layers"], census_seq_len, replacements)) 

736 resolved = ModelSpec.from_dict(resolved).to_dict() 

737 return resolved 

738 

739 

740def resolve_model_spec( 

741 model_raw: Mapping[str, Any], 

742 visual_seq_len: Optional[int] = None, 

743 census_seq_len: int = 0, 

744 census_spec: bool = False, 

745 replacements: Tuple[Any, ...] = (), 

746) -> ModelSpec: 

747 """Return the typed :class:`ModelSpec` for a Trainer ``model`` section. 

748 

749 The typed entry point behind :func:`resolve_hf_model_spec`, which returns 

750 the same facts as a plain mapping for callers that still read one. 

751 

752 Args: 

753 model_raw: The ``model`` section, as a plain mapping. 

754 visual_seq_len: Optional override for the encoder sequence length. 

755 census_seq_len, census_spec, replacements: As 

756 :func:`resolve_hf_model_spec` takes them. 

757 

758 Returns: 

759 A validated spec. 

760 

761 Raises: 

762 ModelSpecError: If the resolved fields are incomplete or incoherent. 

763 ValueError: If neither a pretrained path nor overrides can supply 

764 the model dimensions. 

765 """ 

766 return ModelSpec.from_dict( 

767 resolve_hf_model_spec(model_raw, visual_seq_len, census_seq_len, census_spec, replacements))