Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / trainer / runtime / model_integration.py: 25%

250 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"""Trainer-owned model-integration validation lifecycle.""" 

16 

17from __future__ import annotations 

18 

19import os 

20import time 

21from pathlib import Path 

22from typing import Any, Mapping, Optional, Protocol 

23 

24from hyper_parallel.tools.model_integration.checkpoint_coverage import build_checkpoint_coverage 

25from hyper_parallel.tools.model_integration.data_contract import ( 

26 modality_gradient_parameters, 

27 validate_data_contract, 

28 validate_observed_forward_fields, 

29) 

30from hyper_parallel.tools.model_integration.evidence_store import EvidenceStore 

31from hyper_parallel.tools.model_integration.optimizer_layout import ( 

32 flatten_tensor_layouts, 

33 snapshot_training_layout, 

34 validate_optimizer_layout, 

35) 

36from hyper_parallel.tools.model_integration.tensor_probes import ( 

37 InputIdentityRecorder, 

38 ParameterProbeRecorder, 

39) 

40from hyper_parallel.tools.model_integration.schemas import IntegrationState 

41from hyper_parallel.tools.model_integration.schemas import ModelIntegrationReport 

42from hyper_parallel.tools.model_integration.integration_validator import ( 

43 collect_activation_checkpoint_wrappers, 

44 inventory_final_model, 

45 resolve_model_validation_spec, 

46 validate_final_model, 

47 validate_state_invariants, 

48) 

49from hyper_parallel.trainer.runtime.memory import peak_device_memory, reset_peak_device_memory 

50 

51 

52_OUTPUT_DIR_ENV = "HYPER_PARALLEL_MODEL_INTEGRATION_OUTPUT_DIR" 

53_SMALL_PARAMETER_NUMEL = 8192 

54 

55 

56def _checkpoint_event_name(event: str, checkpoint_path: str) -> str: 

57 """Build a filesystem-safe event name that preserves checkpoint identity.""" 

58 checkpoint_name = Path(checkpoint_path).name 

59 safe_name = "".join( 

60 character if character.isalnum() or character in "._-" else "_" 

61 for character in checkpoint_name 

62 ) 

63 return f"{event}_{safe_name}" if safe_name else event 

64 

65 

66class ModelIntegrationSession(Protocol): 

67 """Trainer-facing lifecycle shared by disabled and enabled sessions.""" 

68 

69 runtime_enabled: bool 

70 

71 def attach_optimizer(self, optimizer: Any) -> None: 

72 """Attach the final optimizer.""" 

73 

74 def attach_data_pipeline(self, **pipeline: Any) -> None: 

75 """Attach resolved data-pipeline objects.""" 

76 

77 def begin_step(self, step: int) -> None: 

78 """Begin one optimizer step.""" 

79 

80 def record_batch( 

81 self, 

82 model_inputs: Mapping[str, Any], 

83 loss_inputs: Mapping[str, Any], 

84 ) -> None: 

85 """Observe one micro-batch.""" 

86 

87 def after_backward_before_clip(self) -> None: 

88 """Observe gradients before clipping.""" 

89 

90 def after_clip(self, grad_norm: Any) -> None: 

91 """Observe gradients and their known norm after clipping.""" 

92 

93 def after_optimizer(self) -> None: 

94 """Observe parameters after the optimizer step.""" 

95 

96 def end_step(self, metrics: Mapping[str, Any]) -> None: 

97 """Finish one optimizer step.""" 

98 

99 def record_fsdp_trace(self, trace: Mapping[str, Any]) -> None: 

100 """Record optional FSDP runtime evidence.""" 

101 

102 def capture_checkpoint_payload( 

103 self, 

104 event: str, 

105 checkpoint_path: str, 

106 payload: Mapping[str, Any], 

107 ) -> None: 

108 """Record optional checkpoint layout evidence.""" 

109 

110 

111def _analyze_shared_state_events( 

112 events: tuple[dict[str, Any], ...], 

113 producer_keys: tuple[str, ...], 

114 consumer_keys: tuple[str, ...], 

115) -> dict[str, Any]: 

116 """Normalize trace identities and classify shared-state contract failures.""" 

117 state_indices: dict[Any, int] = {} 

118 normalized_events = [] 

119 for event in events: 

120 state_index = state_indices.setdefault(event["state_id"], len(state_indices)) 

121 normalized_events.append({**event, "state_id": state_index}) 

122 

123 published = set() 

124 duplicate_publications = [] 

125 missing_sources = [] 

126 for event in normalized_events: 

127 identity = (event["state_id"], event["key"], event["source_layer"]) 

128 if event["action"] == "publish": 

129 if identity in published: 

130 duplicate_publications.append(identity) 

131 published.add(identity) 

132 elif identity not in published: 

133 missing_sources.append(event) 

134 

135 produced_keys = { 

136 event["key"] for event in normalized_events if event["action"] == "publish" 

137 } 

138 consumed_keys = { 

139 event["key"] for event in normalized_events if event["action"] == "consume" 

140 } 

141 return { 

142 "events": normalized_events, 

143 "missing_producer_keys": sorted(set(producer_keys) - produced_keys), 

144 "missing_consumer_keys": sorted(set(consumer_keys) - consumed_keys), 

145 "missing_sources": missing_sources, 

146 "duplicate_publications": duplicate_publications, 

147 } 

148 

149 

150class DisabledModelIntegrationSession: 

151 """No-op session used to keep validation branches out of training code.""" 

152 

153 runtime_enabled = False 

154 

155 def attach_optimizer(self, optimizer: Any) -> None: 

156 """Ignore optimizer construction when validation is disabled.""" 

157 del optimizer 

158 

159 def attach_data_pipeline(self, **pipeline: Any) -> None: 

160 """Ignore data-pipeline construction when validation is disabled.""" 

161 del pipeline 

162 

163 def begin_step(self, step: int) -> None: 

164 """Ignore the optimizer-step boundary when validation is disabled.""" 

165 del step 

166 

167 def record_batch( 

168 self, 

169 model_inputs: Mapping[str, Any], 

170 loss_inputs: Mapping[str, Any], 

171 ) -> None: 

172 """Ignore model inputs when validation is disabled.""" 

173 del model_inputs, loss_inputs 

174 

175 def after_backward_before_clip(self) -> None: 

176 """Ignore the pre-clip boundary when validation is disabled.""" 

177 

178 def after_clip(self, grad_norm: Any) -> None: 

179 """Ignore the post-clip boundary when validation is disabled.""" 

180 del grad_norm 

181 

182 def after_optimizer(self) -> None: 

183 """Ignore the optimizer boundary when validation is disabled.""" 

184 

185 def end_step(self, metrics: Mapping[str, Any]) -> None: 

186 """Ignore step metrics when validation is disabled.""" 

187 del metrics 

188 

189 def record_fsdp_trace(self, trace: Mapping[str, Any]) -> None: 

190 """Ignore FSDP traces when validation is disabled.""" 

191 del trace 

192 

193 def capture_checkpoint_payload( 

194 self, 

195 event: str, 

196 checkpoint_path: str, 

197 payload: Mapping[str, Any], 

198 ) -> None: 

199 """Ignore checkpoint payloads when validation is disabled.""" 

200 del event, checkpoint_path, payload 

201 

202 

203class TrainerModelIntegrationSession: 

204 """Coordinate build checks and runtime evidence without changing execution.""" 

205 

206 def __init__( 

207 self, 

208 *, 

209 model: Any, 

210 config: Any, 

211 global_rank: int, 

212 mesh_context: Any = None, 

213 ) -> None: 

214 """Validate the final model and persist integration evidence.""" 

215 self.model = model 

216 self.config = config 

217 self.global_rank = global_rank 

218 self.mode = config.model_integration.mode 

219 configured_output = os.environ.get(_OUTPUT_DIR_ENV) 

220 if not configured_output: 

221 configured_output = str( 

222 Path(config.checkpoint.checkpoint_dir) / "model_integration" 

223 ) 

224 self.evidence_store = EvidenceStore(configured_output) 

225 self.parameter_probes: Optional[ParameterProbeRecorder] = None 

226 self.optimizer: Any = None 

227 self.validation_spec = resolve_model_validation_spec(self.model) 

228 data_spec = self.validation_spec.data if self.validation_spec is not None else None 

229 self.input_identity = InputIdentityRecorder( 

230 mesh_context, 

231 cp_replicated_forward_fields=( 

232 () if data_spec is None else data_spec.cp_replicated_forward_fields 

233 ), 

234 ) 

235 self._current_step = 0 

236 self._step_start_time = 0.0 

237 self._grad_norm_post_clip: Optional[float] = None 

238 self._shared_trace_token: Any = None 

239 self._observed_forward_fields = False 

240 self._modality_fields_observed = False 

241 self._field_ownership: dict[str, str] = {} 

242 self._validate_final_model() 

243 

244 @property 

245 def runtime_enabled(self) -> bool: 

246 """Whether step-level precision and execution probes are enabled.""" 

247 return self.mode == "runtime" 

248 

249 def _validate_final_model(self) -> None: 

250 """Run final-tree validation and record the structured report.""" 

251 report, ownership, fsdp_units = validate_final_model( 

252 self.model, 

253 context={"trainer_config": self.config.to_dict()}, 

254 ) 

255 self.parameter_ownership = ownership 

256 if self.global_rank == 0: 

257 self.evidence_store.write_json("check/findings.json", report.to_dict()) 

258 self.evidence_store.write_json("check/fsdp_units.json", fsdp_units) 

259 self.evidence_store.write_csv( 

260 "check/parameter_ownership.csv", 

261 (entry.to_dict() for entry in ownership), 

262 fieldnames=( 

263 "parameter_fqn", 

264 "owner_fqn", 

265 "gradient_domain", 

266 "source_mesh", 

267 "source_placements", 

268 "global_shape", 

269 "local_shape", 

270 ), 

271 ) 

272 self.evidence_store.write_json("trainer_config.resolved.json", self.config.to_dict()) 

273 self.evidence_store.capture_environment() 

274 self.evidence_store.advance_if_before( 

275 IntegrationState.DISCOVERED, 

276 ("environment.json",), 

277 ) 

278 if not report.errors: 

279 self.evidence_store.advance_if_before( 

280 IntegrationState.STRUCTURE_VALIDATED, 

281 ( 

282 "check/findings.json", 

283 "check/parameter_ownership.csv", 

284 "check/fsdp_units.json", 

285 ), 

286 ) 

287 self.evidence_store.write_json( 

288 "inventory/candidate.json", 

289 inventory_final_model(self.model), 

290 ) 

291 checkpoint_spec = ( 

292 self.validation_spec.checkpoint 

293 if self.validation_spec is not None 

294 else None 

295 ) 

296 checkpoint_coverage, _ = build_checkpoint_coverage( 

297 self.model, 

298 checkpoint_spec, 

299 ) 

300 self.evidence_store.write_json( 

301 "checkpoint/coverage.json", 

302 checkpoint_coverage, 

303 ) 

304 self.evidence_store.write_json( 

305 "check/activation_checkpoint.json", 

306 collect_activation_checkpoint_wrappers(self.model), 

307 ) 

308 report.raise_for_errors() 

309 

310 def attach_optimizer(self, optimizer: Any) -> None: 

311 """Resolve the semantic parameter watchlist after optimizer construction.""" 

312 self.optimizer = optimizer 

313 findings = validate_optimizer_layout(self.model, optimizer) 

314 if self.global_rank == 0: 

315 self.evidence_store.write_json( 

316 "check/optimizer_layout_initial.json", 

317 { 

318 "status": "FAIL" if findings else "PASS", 

319 "findings": [finding.to_dict() for finding in findings], 

320 "layout": snapshot_training_layout(self.model, optimizer), 

321 }, 

322 ) 

323 if findings: 

324 ModelIntegrationReport(findings=findings).raise_for_errors() 

325 if not self.runtime_enabled: 

326 return 

327 self.parameter_probes = ParameterProbeRecorder( 

328 self.model, 

329 optimizer, 

330 self.evidence_store, 

331 rank=self.global_rank, 

332 validation_spec=self.validation_spec, 

333 include_small_parameters_numel_le=_SMALL_PARAMETER_NUMEL, 

334 representative_parameter_names=self._representative_parameter_names(), 

335 ) 

336 if self.global_rank == 0: 

337 self.evidence_store.write_json( 

338 "check/parameter_watchlist.json", 

339 {"parameters": list(self.parameter_probes.watched_parameter_names)}, 

340 ) 

341 

342 def _representative_parameter_names(self) -> tuple[str, ...]: 

343 """Select at least one parameter from every owner/domain/placement group.""" 

344 representatives = {} 

345 for entry in self.parameter_ownership: 

346 key = ( 

347 entry.owner_fqn, 

348 entry.gradient_domain, 

349 entry.source_mesh, 

350 entry.source_placements, 

351 ) 

352 representatives.setdefault(key, entry.parameter_fqn) 

353 return tuple(sorted(representatives.values())) 

354 

355 def attach_data_pipeline( 

356 self, 

357 *, 

358 runtime_adapter: Any, 

359 get_batch: Any, 

360 model_assets: tuple[Any, ...], 

361 ) -> None: 

362 """Validate and record the resolved data/forward-input ownership.""" 

363 ownership, findings = validate_data_contract( 

364 self.validation_spec, 

365 runtime_adapter, 

366 labels_are_shifted=getattr(get_batch, "labels_are_shifted", None), 

367 source_type=getattr(get_batch, "source_type", None), 

368 assets=model_assets, 

369 ) 

370 self._field_ownership = dict(ownership["fields"]) 

371 if self.global_rank == 0: 

372 self.evidence_store.write_json("data/field_ownership.json", ownership) 

373 self.evidence_store.write_json( 

374 "data/findings.json", 

375 { 

376 "status": "FAIL" if findings else "PASS", 

377 "findings": [finding.to_dict() for finding in findings], 

378 }, 

379 ) 

380 if findings: 

381 ModelIntegrationReport(findings=findings).raise_for_errors() 

382 

383 def begin_step(self, step: int) -> None: 

384 """Begin input and parameter evidence for one optimizer step.""" 

385 if not self.runtime_enabled: 

386 return 

387 self._current_step = step 

388 self._step_start_time = time.perf_counter() 

389 self._grad_norm_post_clip = None 

390 reset_peak_device_memory() 

391 shared_state_spec = ( 

392 self.validation_spec.shared_state 

393 if self.validation_spec is not None 

394 else None 

395 ) 

396 if shared_state_spec is not None and shared_state_spec.begin_trace is not None: 

397 self._shared_trace_token = shared_state_spec.begin_trace() 

398 self.input_identity.begin_step() 

399 if self.parameter_probes is not None: 

400 self.parameter_probes.begin_step(step) 

401 

402 def record_batch( 

403 self, 

404 model_inputs: Mapping[str, Any], 

405 loss_inputs: Mapping[str, Any], 

406 ) -> None: 

407 """Record exact tensor fields reaching the model/loss boundary. 

408 

409 Args: 

410 model_inputs: Keyword inputs passed to the model forward call. 

411 loss_inputs: Labels and masks consumed by the loss path. 

412 """ 

413 if not self.runtime_enabled: 

414 return 

415 if not self._observed_forward_fields: 

416 findings = validate_observed_forward_fields( 

417 self.validation_spec, 

418 model_inputs, 

419 ) 

420 data_spec = self.validation_spec.data if self.validation_spec is not None else None 

421 if data_spec is not None: 

422 self._modality_fields_observed = bool( 

423 set(data_spec.modality_fields).intersection(model_inputs) 

424 ) 

425 if self.global_rank == 0: 

426 actual_ownership = { 

427 field_name: self._field_ownership.get( 

428 field_name, 

429 "source_or_get_batch", 

430 ) 

431 for field_name in sorted(model_inputs) 

432 } 

433 self.evidence_store.write_json( 

434 "data/observed_forward_fields.json", 

435 { 

436 "status": "FAIL" if findings else "PASS", 

437 "model_fields": sorted(model_inputs), 

438 "loss_fields": sorted(loss_inputs), 

439 "field_ownership": actual_ownership, 

440 "findings": [finding.to_dict() for finding in findings], 

441 }, 

442 ) 

443 self._observed_forward_fields = True 

444 if findings: 

445 ModelIntegrationReport(findings=findings).raise_for_errors() 

446 self.input_identity.record(model_inputs, loss_inputs) 

447 

448 def after_backward_before_clip(self) -> None: 

449 """Capture un-clipped main gradients.""" 

450 if not self.runtime_enabled: 

451 return 

452 runtime_findings = validate_state_invariants( 

453 self.model, 

454 self.validation_spec, 

455 {"trainer": self, "step": self._current_step}, 

456 phases=("runtime",), 

457 ) 

458 if runtime_findings: 

459 ModelIntegrationReport(findings=runtime_findings).raise_for_errors() 

460 if self.parameter_probes is not None: 

461 self.parameter_probes.capture("main_grad_before_clip") 

462 if self.optimizer is not None: 

463 findings = validate_optimizer_layout(self.model, self.optimizer) 

464 if self.global_rank == 0: 

465 self.evidence_store.write_json( 

466 f"cases/runtime/optimizer_layout_step{self._current_step}.json", 

467 { 

468 "status": "FAIL" if findings else "PASS", 

469 "findings": [finding.to_dict() for finding in findings], 

470 "layout": snapshot_training_layout(self.model, self.optimizer), 

471 }, 

472 ) 

473 if findings: 

474 ModelIntegrationReport(findings=findings).raise_for_errors() 

475 if self._modality_fields_observed: 

476 modality_gradients = modality_gradient_parameters( 

477 self.validation_spec, 

478 self.model, 

479 ) 

480 if self.global_rank == 0: 

481 self.evidence_store.write_json( 

482 f"cases/runtime/modality_gradients_step{self._current_step}.json", 

483 modality_gradients, 

484 ) 

485 if modality_gradients and not all(modality_gradients.values()): 

486 raise RuntimeError( 

487 "multimodal inputs reached the model but one or more " 

488 "declared visual parameters have missing or non-finite gradients" 

489 ) 

490 

491 def record_fsdp_trace(self, trace: Mapping[str, Any]) -> None: 

492 """Persist rank-local runtime order before the optimizer-boundary gate.""" 

493 if not self.runtime_enabled: 

494 return 

495 self.evidence_store.write_json( 

496 f"check/fsdp_runtime_rank{self.global_rank}.json", 

497 dict(trace), 

498 ) 

499 

500 def after_clip(self, grad_norm: Any) -> None: 

501 """Capture clipped main gradients.""" 

502 if not self.runtime_enabled: 

503 return 

504 if grad_norm is None: 

505 raise ValueError("runtime model integration requires the post-clip gradient norm") 

506 if self.parameter_probes is not None: 

507 self.parameter_probes.capture("main_grad_after_clip") 

508 self._grad_norm_post_clip = float(grad_norm) 

509 

510 def after_optimizer(self) -> None: 

511 """Capture main/model updates and optimizer moment layouts.""" 

512 if not self.runtime_enabled: 

513 return 

514 if self.parameter_probes is None: 

515 return 

516 self.parameter_probes.capture("main_after_optimizer") 

517 self.parameter_probes.capture("model_after_copy_back") 

518 self.parameter_probes.capture("optimizer_state") 

519 

520 def end_step(self, metrics: Mapping[str, Any]) -> None: 

521 """Persist structured scalar and global input identity evidence.""" 

522 if not self.runtime_enabled: 

523 return 

524 self._finish_shared_state_trace() 

525 input_identity = self.input_identity.finish_step() 

526 elapsed = time.perf_counter() - self._step_start_time 

527 tokens = metrics.get("tokens") 

528 samples = metrics.get("samples") 

529 memory = peak_device_memory() 

530 row = { 

531 "step": self._current_step, 

532 "loss": metrics.get("loss"), 

533 "grad_norm_pre_clip": metrics.get("grad_norm"), 

534 "grad_norm_post_clip": metrics.get( 

535 "grad_norm_post_clip", 

536 self._grad_norm_post_clip, 

537 ), 

538 "lr": metrics.get("lr"), 

539 "global_input_sha256": input_identity["global_sha256"], 

540 "input_identity": input_identity, 

541 "step_time_seconds": elapsed, 

542 "tokens_per_second": None if tokens is None else float(tokens) / elapsed, 

543 "samples_per_second": None if samples is None else float(samples) / elapsed, 

544 **memory, 

545 } 

546 performance_row = { 

547 "step": self._current_step, 

548 "rank": self.global_rank, 

549 "step_time_seconds": elapsed, 

550 "tokens_per_second": row["tokens_per_second"], 

551 "samples_per_second": row["samples_per_second"], 

552 **memory, 

553 } 

554 self.evidence_store.append_jsonl( 

555 f"cases/runtime/performance_rank{self.global_rank}.jsonl", 

556 performance_row, 

557 ) 

558 if self.global_rank == 0: 

559 self.evidence_store.append_jsonl("cases/runtime/metrics.jsonl", row) 

560 self.evidence_store.append_jsonl( 

561 "cases/runtime/performance.jsonl", 

562 performance_row, 

563 ) 

564 

565 def _finish_shared_state_trace(self) -> None: 

566 """Validate model-declared shared-state publication and consumption.""" 

567 if self._shared_trace_token is None or self.validation_spec is None: 

568 return 

569 shared_state_spec = self.validation_spec.shared_state 

570 if shared_state_spec is None or shared_state_spec.finish_trace is None: 

571 return 

572 events = shared_state_spec.finish_trace(self._shared_trace_token) 

573 self._shared_trace_token = None 

574 analysis = _analyze_shared_state_events( 

575 events, 

576 shared_state_spec.producer_keys, 

577 shared_state_spec.consumer_keys, 

578 ) 

579 if self.global_rank == 0: 

580 self.evidence_store.write_json( 

581 f"cases/runtime/shared_state_step{self._current_step}.json", 

582 analysis, 

583 ) 

584 if ( 

585 analysis["missing_sources"] 

586 or analysis["missing_producer_keys"] 

587 or analysis["missing_consumer_keys"] 

588 ): 

589 raise RuntimeError( 

590 "shared-state producer/consumer contract failed: " 

591 f"missing_producers={analysis['missing_producer_keys']}, " 

592 f"missing_consumers={analysis['missing_consumer_keys']}, " 

593 f"missing_sources={analysis['missing_sources']}" 

594 ) 

595 if analysis["duplicate_publications"]: 

596 raise RuntimeError( 

597 "shared-state producer replayed inside one forward: " 

598 f"{analysis['duplicate_publications']}" 

599 ) 

600 

601 def capture_checkpoint_payload( 

602 self, 

603 event: str, 

604 checkpoint_path: str, 

605 payload: Mapping[str, Any], 

606 ) -> None: 

607 """Record versioned DCP layouts without rebuilding checkpoint chunks.""" 

608 if not self.runtime_enabled: 

609 return 

610 event_name = _checkpoint_event_name(event, checkpoint_path) 

611 self.evidence_store.write_json( 

612 f"checkpoint/{event_name}_rank{self.global_rank}.json", 

613 { 

614 "checkpoint_path": checkpoint_path, 

615 "tensor_layouts": flatten_tensor_layouts(payload), 

616 }, 

617 ) 

618 

619 

620def build_model_integration_session( 

621 model: Any, 

622 config: Any, 

623 global_rank: int, 

624 mesh_context: Any = None, 

625) -> TrainerModelIntegrationSession | DisabledModelIntegrationSession: 

626 """Build the configured validation session or a branch-free no-op session.""" 

627 if getattr(getattr(config, "model_integration", None), "mode", "off") == "off": 

628 return DisabledModelIntegrationSession() 

629 return TrainerModelIntegrationSession( 

630 model=model, 

631 config=config, 

632 global_rank=global_rank, 

633 mesh_context=mesh_context, 

634 ) 

635 

636 

637__all__ = [ 

638 "DisabledModelIntegrationSession", 

639 "ModelIntegrationSession", 

640 "TrainerModelIntegrationSession", 

641 "build_model_integration_session", 

642]