Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / tools / model_integration / integration_validator.py: 74%

194 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"""Build/runtime model integration validators backed by final model and FSDP state facts.""" 

16 

17from __future__ import annotations 

18 

19import fnmatch 

20from collections import defaultdict 

21from dataclasses import dataclass 

22from typing import Any, Iterable, Mapping, Optional 

23 

24from hyper_parallel.tools.model_integration.structure_inventory import build_model_inventory 

25from hyper_parallel.tools.model_integration.checkpoint_coverage import build_checkpoint_coverage 

26from hyper_parallel.tools.model_integration.schemas import ( 

27 ModelIntegrationReport, 

28 FindingSeverity, 

29 ModelIntegrationFinding, 

30) 

31from hyper_parallel.models.registry import get_model_adapter 

32from hyper_parallel.models.validation_spec import ModelValidationSpec 

33 

34 

35@dataclass(frozen=True) 

36class ParameterOwnership: 

37 """One final parameter's inferred FSDP owner and gradient domain.""" 

38 

39 parameter_fqn: str 

40 owner_fqn: str 

41 gradient_domain: str 

42 source_mesh: tuple[str, ...] 

43 source_placements: tuple[str, ...] 

44 global_shape: tuple[int, ...] 

45 local_shape: tuple[int, ...] 

46 parameter_identity: int = 0 

47 

48 def to_dict(self) -> dict[str, Any]: 

49 """Return a flat representation suitable for CSV or JSON.""" 

50 return { 

51 "parameter_fqn": self.parameter_fqn, 

52 "owner_fqn": self.owner_fqn, 

53 "gradient_domain": self.gradient_domain, 

54 "source_mesh": ",".join(self.source_mesh), 

55 "source_placements": ",".join(self.source_placements), 

56 "global_shape": repr(self.global_shape), 

57 "local_shape": repr(self.local_shape), 

58 } 

59 

60 

61def resolve_model_validation_spec(model: Any) -> Optional[ModelValidationSpec]: 

62 """Resolve and type-check the final model family's validation provider.""" 

63 config = getattr(model, "config", None) 

64 identities = [getattr(config, "model_type", None)] 

65 identities.extend(getattr(config, "architectures", None) or ()) 

66 adapter_spec = None 

67 for identity in identities: 

68 if not identity: 

69 continue 

70 adapter_spec = get_model_adapter(identity) 

71 if adapter_spec is not None: 

72 break 

73 provider = getattr(adapter_spec, "validation", None) 

74 if provider is None: 

75 return None 

76 if not callable(provider): 

77 raise TypeError("ModelAdapterSpec.validation must be callable") 

78 validation_spec = provider() # pylint: disable=not-callable 

79 if not isinstance(validation_spec, ModelValidationSpec): 

80 raise TypeError( 

81 "ModelAdapterSpec.validation must return ModelValidationSpec, " 

82 f"got {type(validation_spec).__name__}" 

83 ) 

84 return validation_spec 

85 

86 

87def _tensor_local_shape(tensor: Any) -> tuple[int, ...]: 

88 to_local = getattr(tensor, "to_local", None) 

89 local = to_local() if callable(to_local) else tensor 

90 return tuple(int(size) for size in getattr(local, "shape", ())) 

91 

92 

93def _source_layout(hsdp_param: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: 

94 source_info = getattr(hsdp_param, "source_shard_info", None) 

95 source_mesh = getattr(source_info, "mesh", None) 

96 names = tuple(getattr(source_mesh, "mesh_dim_names", None) or ()) 

97 placements = tuple(repr(value) for value in getattr(source_info, "placements", ()) or ()) 

98 return names, placements 

99 

100 

101def _gradient_domain(hsdp_param: Any) -> str: 

102 names, placements = _source_layout(hsdp_param) 

103 expert_axis = any(name in ("ep", "expert") or name.startswith("ep_") for name in names) 

104 expert_shard = any("Shard" in placement for placement in placements) 

105 if expert_axis and expert_shard: 

106 return "EXPERT" 

107 return "DENSE" 

108 

109 

110def collect_parameter_ownership(model: Any) -> tuple[list[ParameterOwnership], dict[str, Any]]: 

111 """Collect parameter-to-FSDP-unit ownership from final runtime state.""" 

112 ownership = [] 

113 units = {} 

114 seen_parameters = set() 

115 for module_fqn, module in model.named_modules(): 

116 scheduler = getattr(module, "hsdp_scheduler", None) 

117 state = getattr(scheduler, "hsdp_state", None) 

118 hsdp_params = tuple(getattr(state, "hsdp_params", ()) or ()) 

119 if scheduler is None: 

120 continue 

121 owner_fqn = module_fqn or "<root>" 

122 unit_domains = defaultdict(list) 

123 for parameter_index, hsdp_param in enumerate(hsdp_params): 

124 parameter = getattr(hsdp_param, "sharded_param", None) 

125 parameter_fqn = getattr(hsdp_param, "_param_fqn", None) or ( 

126 f"{owner_fqn}.param[{parameter_index}]" 

127 ) 

128 names, placements = _source_layout(hsdp_param) 

129 domain = _gradient_domain(hsdp_param) 

130 unit_domains[domain].append(parameter_fqn) 

131 global_shape = tuple( 

132 int(size) for size in getattr(hsdp_param, "_orig_size", getattr(parameter, "shape", ())) 

133 ) 

134 ownership.append( 

135 ParameterOwnership( 

136 parameter_fqn=parameter_fqn, 

137 owner_fqn=owner_fqn, 

138 gradient_domain=domain, 

139 source_mesh=names, 

140 source_placements=placements, 

141 global_shape=global_shape, 

142 local_shape=_tensor_local_shape(parameter), 

143 parameter_identity=id(parameter) if parameter is not None else 0, 

144 ) 

145 ) 

146 if parameter is not None: 

147 seen_parameters.add(id(parameter)) 

148 units[owner_fqn] = { 

149 "domains": {name: sorted(fqns) for name, fqns in sorted(unit_domains.items())}, 

150 "parameter_count": len(hsdp_params), 

151 "forward_prefetch_targets": [ 

152 _module_fqn(model, target) 

153 for target in getattr(scheduler, "forward_prefetch_cells", ()) 

154 ], 

155 "backward_prefetch_targets": [ 

156 _module_fqn(model, target) 

157 for target in getattr(scheduler, "backward_prefetch_cells", ()) 

158 ], 

159 } 

160 for parameter_fqn, parameter in model.named_parameters(remove_duplicate=False): 

161 if id(parameter) in seen_parameters: 

162 continue 

163 ownership.append( 

164 ParameterOwnership( 

165 parameter_fqn=parameter_fqn, 

166 owner_fqn="<unmanaged>", 

167 gradient_domain="LOCAL" if not parameter.requires_grad else "DENSE", 

168 source_mesh=(), 

169 source_placements=(), 

170 global_shape=tuple(int(size) for size in parameter.shape), 

171 local_shape=_tensor_local_shape(parameter), 

172 parameter_identity=id(parameter), 

173 ) 

174 ) 

175 return ownership, units 

176 

177 

178def _module_fqn(model: Any, target: Any) -> str: 

179 for module_fqn, module in model.named_modules(): 

180 if module is target: 

181 return module_fqn or "<root>" 

182 return f"<untracked:{type(target).__name__}>" 

183 

184 

185def _validate_gradient_domains( 

186 ownership: Iterable[ParameterOwnership], 

187) -> list[ModelIntegrationFinding]: 

188 """Emit findings for conflicting ownership or gradient domains.""" 

189 grouped = defaultdict(lambda: defaultdict(list)) 

190 for entry in ownership: 

191 if entry.owner_fqn == "<unmanaged>" or entry.gradient_domain == "LOCAL": 

192 continue 

193 grouped[entry.owner_fqn][entry.gradient_domain].append(entry) 

194 findings = [] 

195 aliases = defaultdict(list) 

196 for entry in ownership: 

197 if entry.parameter_identity: 

198 aliases[entry.parameter_identity].append(entry) 

199 for entries in aliases.values(): 

200 contracts = { 

201 ( 

202 entry.owner_fqn, 

203 entry.gradient_domain, 

204 entry.source_mesh, 

205 entry.source_placements, 

206 ) 

207 for entry in entries 

208 } 

209 if len(contracts) <= 1: 

210 continue 

211 findings.append( 

212 ModelIntegrationFinding( 

213 phase="D3", 

214 owner_fqn=entries[0].parameter_fqn, 

215 severity=FindingSeverity.ERROR, 

216 message="aliased parameter has conflicting ownership or gradient domains", 

217 facts={ 

218 "aliases": [entry.parameter_fqn for entry in entries], 

219 "contracts": [repr(contract) for contract in sorted(contracts, key=repr)], 

220 }, 

221 why_unsafe="one storage cannot be reduced or sharded under two incompatible contracts", 

222 remediation="assign every alias of the tied parameter to one owner and source layout", 

223 related_config=("plan_overrides", "model adapter fsdp_wrap_modules"), 

224 ) 

225 ) 

226 for owner_fqn, domain_entries in sorted(grouped.items()): 

227 domains = set(domain_entries) 

228 if len(domains) > 1: 

229 facts = { 

230 domain: [entry.parameter_fqn for entry in entries] 

231 for domain, entries in sorted(domain_entries.items()) 

232 } 

233 findings.append( 

234 ModelIntegrationFinding( 

235 phase="D3", 

236 owner_fqn=owner_fqn, 

237 severity=FindingSeverity.ERROR, 

238 message="FSDP unit has incompatible gradient domains", 

239 facts=facts, 

240 why_unsafe=( 

241 "one FSDP unit selects one collective mesh, so dense and expert " 

242 "parameters cannot both receive their required gradient reduction" 

243 ), 

244 remediation=( 

245 "declare homogeneous child FSDP units and keep dense gates/norms " 

246 "in a dense owner" 

247 ), 

248 related_config=("model adapter fsdp_wrap_modules", "plan_overrides"), 

249 ) 

250 ) 

251 if owner_fqn == "<root>" and "EXPERT" in domains: 

252 findings.append( 

253 ModelIntegrationFinding( 

254 phase="D3", 

255 owner_fqn=owner_fqn, 

256 severity=FindingSeverity.ERROR, 

257 message="expert-domain parameter remains in the dense root FSDP unit", 

258 facts={ 

259 "expert_parameters": [ 

260 entry.parameter_fqn for entry in domain_entries["EXPERT"] 

261 ] 

262 }, 

263 why_unsafe="the dense root does not own the expert reduction domain", 

264 remediation="declare a homogeneous nested expert FSDP unit", 

265 related_config=("model adapter fsdp_wrap_modules",), 

266 ) 

267 ) 

268 return findings 

269 

270 

271def _validate_materialization(model: Any) -> list[ModelIntegrationFinding]: 

272 """Emit findings for meta tensors and incomplete buffer rebuilds.""" 

273 meta_state = [] 

274 for name, value in ( 

275 *model.named_parameters(remove_duplicate=False), 

276 *model.named_buffers(remove_duplicate=False), 

277 ): 

278 if bool(getattr(value, "is_meta", False)): 

279 meta_state.append(name) 

280 findings = [] 

281 if meta_state: 

282 findings.append( 

283 ModelIntegrationFinding( 

284 phase="D4", 

285 owner_fqn="<root>", 

286 severity=FindingSeverity.ERROR, 

287 message="materialized model still contains meta tensors", 

288 facts={"state_fqns": sorted(meta_state)}, 

289 why_unsafe="forward or checkpoint access would read storage that does not exist", 

290 remediation="materialize storage and run the adapter rebuild lifecycle before validation", 

291 related_config=("training.init_device", "model adapter materialization"), 

292 ) 

293 ) 

294 invalid_rebuildable_buffers = [] 

295 for module_fqn, module in model.named_modules(remove_duplicate=False): 

296 specs = module.__dict__.get("_hp_rebuildable_buffer_specs", {}) 

297 for buffer_name, spec in specs.items(): 

298 buffer = module._buffers.get(buffer_name) # pylint: disable=protected-access 

299 reason = None 

300 if buffer is None: 

301 reason = "buffer is missing" 

302 elif buffer_name not in module._non_persistent_buffers_set: # pylint: disable=protected-access 

303 reason = "buffer became persistent" 

304 elif bool(getattr(buffer, "is_meta", False)): 

305 reason = "buffer still has meta storage" 

306 elif getattr(spec, "source", None) is None and getattr(spec, "factory", None) is None: 

307 reason = "rebuild recipe has neither source nor factory" 

308 if reason is not None: 

309 invalid_rebuildable_buffers.append( 

310 { 

311 "fqn": f"{module_fqn}.{buffer_name}" if module_fqn else buffer_name, 

312 "reason": reason, 

313 } 

314 ) 

315 if invalid_rebuildable_buffers: 

316 findings.append( 

317 ModelIntegrationFinding( 

318 phase="D4", 

319 owner_fqn="<root>", 

320 severity=FindingSeverity.ERROR, 

321 message="rebuildable derived-buffer lifecycle is incomplete", 

322 facts={"buffers": invalid_rebuildable_buffers}, 

323 why_unsafe="to_empty() may leave model-derived non-persistent state undefined", 

324 remediation="register and execute a deterministic rebuild recipe after materialization", 

325 related_config=("training.init_device", "model adapter materialization"), 

326 ) 

327 ) 

328 return findings 

329 

330 

331def _validate_parameter_probes( 

332 model: Any, 

333 validation_spec: Optional[ModelValidationSpec], 

334) -> list[ModelIntegrationFinding]: 

335 """Emit findings for required parameter probes that miss.""" 

336 if validation_spec is None: 

337 return [] 

338 parameter_names = tuple(name for name, _ in model.named_parameters(remove_duplicate=False)) 

339 findings = [] 

340 for probe in validation_spec.parameter_probes: 

341 matches = [name for name in parameter_names if fnmatch.fnmatchcase(name, probe.match)] 

342 if probe.required and not matches: 

343 findings.append( 

344 ModelIntegrationFinding( 

345 phase="V0", 

346 owner_fqn=probe.match, 

347 severity=FindingSeverity.ERROR, 

348 message="required parameter probe matched no final parameter", 

349 facts={"pattern": probe.match}, 

350 why_unsafe="a renamed or missing critical parameter would be silently unobserved", 

351 remediation="update the model validation provider to match final parameter FQNs", 

352 related_config=("ModelAdapterSpec.validation.parameter_probes",), 

353 ) 

354 ) 

355 return findings 

356 

357 

358def validate_state_invariants( 

359 model: Any, 

360 validation_spec: Optional[ModelValidationSpec], 

361 context: Any, 

362 phases: tuple[str, ...] = ("structure", "materialization", "checkpoint"), 

363) -> list[ModelIntegrationFinding]: 

364 """Evaluate model-owned invariants at their declared lifecycle phase.""" 

365 findings = [] 

366 if validation_spec is None: 

367 return findings 

368 phase_by_name = { 

369 "structure": "D2", 

370 "materialization": "D4", 

371 "checkpoint": "D5", 

372 "runtime": "D7", 

373 } 

374 for invariant in validation_spec.state_invariants: 

375 if invariant.phase not in phase_by_name: 

376 findings.append( 

377 ModelIntegrationFinding( 

378 phase="D1", 

379 owner_fqn=invariant.name, 

380 severity=FindingSeverity.ERROR, 

381 message="model state invariant declares an unsupported lifecycle phase", 

382 facts={"phase": invariant.phase, "supported": tuple(phase_by_name)}, 

383 why_unsafe="an invariant with an unknown phase would never be evaluated", 

384 remediation="use structure, materialization, checkpoint, or runtime", 

385 related_config=("ModelAdapterSpec.validation.state_invariants",), 

386 ) 

387 ) 

388 continue 

389 if invariant.phase not in phases: 

390 continue 

391 result = invariant.checker(model, context) 

392 if result is True or result is None: 

393 continue 

394 facts = result if isinstance(result, Mapping) else {"result": result} 

395 findings.append( 

396 ModelIntegrationFinding( 

397 phase=phase_by_name[invariant.phase], 

398 owner_fqn=invariant.name, 

399 severity=FindingSeverity.ERROR, 

400 message=f"model state invariant failed: {invariant.name}", 

401 facts=dict(facts), 

402 why_unsafe="the final model state does not satisfy its authoritative lifecycle contract", 

403 remediation="fix model initialization/checkpoint mapping or the adapter invariant declaration", 

404 related_config=("model adapter validation",), 

405 ) 

406 ) 

407 return findings 

408 

409 

410def validate_final_model( 

411 model: Any, 

412 *, 

413 context: Any = None, 

414) -> tuple[ModelIntegrationReport, list[ParameterOwnership], dict[str, Any]]: 

415 """Run model/FSDP-state model integration on the final built model. 

416 

417 This function is observational. It does not change module structure, 

418 parameters, communication state, or optimizer state. 

419 """ 

420 validation_spec = resolve_model_validation_spec(model) 

421 ownership, fsdp_units = collect_parameter_ownership(model) 

422 report = ModelIntegrationReport( 

423 metadata={ 

424 "model_type": getattr(getattr(model, "config", None), "model_type", None), 

425 "inventory_counts": { 

426 "modules": len(list(model.modules())), 

427 "parameters": len(list(model.parameters())), 

428 "buffers": len(list(model.buffers())), 

429 }, 

430 } 

431 ) 

432 report.extend(_validate_materialization(model)) 

433 report.extend(_validate_gradient_domains(ownership)) 

434 checkpoint_spec = validation_spec.checkpoint if validation_spec is not None else None 

435 checkpoint_coverage, checkpoint_findings = build_checkpoint_coverage(model, checkpoint_spec) 

436 report.metadata["checkpoint_coverage"] = checkpoint_coverage 

437 report.extend(checkpoint_findings) 

438 report.extend(_validate_parameter_probes(model, validation_spec)) 

439 report.extend(validate_state_invariants(model, validation_spec, context)) 

440 return report, ownership, fsdp_units 

441 

442 

443def inventory_final_model(model: Any, source: Optional[dict[str, Any]] = None) -> dict[str, Any]: 

444 """Build a serializable final-tree inventory for integration evidence.""" 

445 return build_model_inventory(model, source=source).to_dict() 

446 

447 

448def collect_activation_checkpoint_wrappers(model: Any) -> dict[str, Any]: 

449 """List final activation-checkpoint wrappers without backend imports.""" 

450 wrappers = [] 

451 for module_fqn, module in model.named_modules(remove_duplicate=False): 

452 if type(module).__name__ != "CheckpointWrapper": 

453 continue 

454 wrapped = getattr(module, "_wrapped_module", None) 

455 wrappers.append( 

456 { 

457 "fqn": module_fqn or "<root>", 

458 "wrapped_type": ( 

459 None 

460 if wrapped is None 

461 else f"{type(wrapped).__module__}.{type(wrapped).__qualname__}" 

462 ), 

463 } 

464 ) 

465 return {"count": len(wrappers), "wrappers": wrappers} 

466 

467 

468__all__ = [ 

469 "ParameterOwnership", 

470 "collect_activation_checkpoint_wrappers", 

471 "collect_parameter_ownership", 

472 "inventory_final_model", 

473 "resolve_model_validation_spec", 

474 "validate_final_model", 

475 "validate_state_invariants", 

476]