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

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"""Strict local-only manifest loading for integration and validation runs.""" 

16 

17from __future__ import annotations 

18 

19import copy 

20import hashlib 

21import importlib 

22import json 

23from collections.abc import Mapping 

24from dataclasses import dataclass 

25from pathlib import Path 

26from typing import Any, Callable, Optional 

27 

28import yaml 

29 

30from hyper_parallel.tools.model_integration.schemas import SCHEMA_VERSION 

31 

32 

33class ManifestError(ValueError): 

34 """Raised when a model-integration manifest violates its public schema.""" 

35 

36 

37def _mapping(value: Any, path: str, required: bool = True) -> dict[str, Any]: 

38 if value is None and not required: 

39 return {} 

40 if not isinstance(value, Mapping): 

41 raise ManifestError(f"{path} must be a mapping") 

42 return dict(value) 

43 

44 

45def _local_path(value: Any, base_dir: Path, field_path: str) -> Optional[Path]: 

46 if value is None: 

47 return None 

48 if not isinstance(value, str) or not value: 

49 raise ManifestError(f"{field_path} must be a non-empty local path or null") 

50 if value.startswith(("http://", "https://", "hf://")): 

51 raise ManifestError( 

52 f"{field_path} must be local; model-integration tools never download implicitly" 

53 ) 

54 path = Path(value).expanduser() 

55 return (base_dir / path).resolve() if not path.is_absolute() else path.resolve() 

56 

57 

58@dataclass(frozen=True) 

59class ValidationManifest: 

60 """Resolved manifest with path normalization and immutable raw content.""" 

61 

62 path: Path 

63 raw: dict[str, Any] 

64 model: dict[str, Any] 

65 reference: dict[str, Any] 

66 matrix: dict[str, Any] 

67 acceptance: dict[str, Any] 

68 launcher: dict[str, Any] 

69 output_dir: Path 

70 

71 @property 

72 def family(self) -> str: 

73 """Return the adapter/model family lookup key.""" 

74 family = self.model.get("adapter") or self.model.get("model_type") 

75 if not isinstance(family, str) or not family: 

76 raise ManifestError("model.adapter or model.model_type must be a non-empty string") 

77 return family 

78 

79 @property 

80 def model_path(self) -> Optional[Path]: 

81 """Return the local authoritative model asset path when declared.""" 

82 return _local_path(self.model.get("id_or_path"), self.path.parent, "model.id_or_path") 

83 

84 @property 

85 def reference_path(self) -> Optional[Path]: 

86 """Return the local reference repository path when declared.""" 

87 return _local_path(self.reference.get("source_path"), self.path.parent, "reference.source_path") 

88 

89 @property 

90 def integration_handoff_path(self) -> Optional[Path]: 

91 """Return the prior integration handoff required by precision validation.""" 

92 return _local_path( 

93 self.raw.get("integration_handoff"), 

94 self.path.parent, 

95 "integration_handoff", 

96 ) 

97 

98 @property 

99 def launcher_config_path(self) -> Optional[Path]: 

100 """Return the Trainer recipe path relative to the validation manifest.""" 

101 return _local_path( 

102 self.launcher.get("config"), 

103 self.path.parent, 

104 "launcher.config", 

105 ) 

106 

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

108 """Return a deep copy safe for evidence serialization.""" 

109 payload = copy.deepcopy(self.raw) 

110 payload["output_dir"] = str(self.output_dir) 

111 return payload 

112 

113 def import_callable(self, target: str) -> Callable[..., Any]: 

114 """Import an explicit dotted callable without mutating ``sys.path``.""" 

115 if not isinstance(target, str) or "." not in target: 

116 raise ManifestError(f"callable target must be a dotted path, got {target!r}") 

117 module_name, attribute_name = target.rsplit(".", 1) 

118 try: 

119 value = getattr(importlib.import_module(module_name), attribute_name) 

120 except (ImportError, AttributeError) as exc: 

121 raise ManifestError(f"could not import callable {target!r}: {exc}") from exc 

122 if not callable(value): 

123 raise ManifestError(f"manifest target {target!r} is not callable") 

124 return value 

125 

126 def fingerprint_paths(self) -> dict[str, Any]: 

127 """Hash bounded metadata from declared model and reference sources.""" 

128 fingerprints = {} 

129 path_fields = { 

130 "model.id_or_path": self.model.get("id_or_path"), 

131 "reference.source_path": self.reference.get("source_path"), 

132 } 

133 for field_path, value in path_fields.items(): 

134 path = _local_path(value, self.path.parent, field_path) 

135 if path is None: 

136 continue 

137 if not path.exists(): 

138 fingerprints[field_path] = {"path": str(path), "exists": False} 

139 continue 

140 if path.is_dir(): 

141 metadata_names = { 

142 "config.json", 

143 "generation_config.json", 

144 "preprocessor_config.json", 

145 "processor_config.json", 

146 "special_tokens_map.json", 

147 "tokenizer.json", 

148 "tokenizer_config.json", 

149 } 

150 metadata_files = [ 

151 candidate 

152 for candidate in path.iterdir() 

153 if candidate.is_file() 

154 and (candidate.name in metadata_names or candidate.name.endswith(".index.json")) 

155 ] 

156 for relative_name in ( 

157 "inference/model.py", 

158 "inference/engram.py", 

159 "inference/vision.py", 

160 ): 

161 candidate = path / relative_name 

162 if candidate.is_file(): 

163 metadata_files.append(candidate) 

164 metadata_files.sort(key=lambda candidate: str(candidate.relative_to(path))) 

165 fingerprints[field_path] = { 

166 "path": str(path), 

167 "exists": True, 

168 "kind": "directory", 

169 "metadata": { 

170 str(candidate.relative_to(path)): self._fingerprint_file(candidate) 

171 for candidate in metadata_files 

172 }, 

173 } 

174 continue 

175 fingerprints[field_path] = { 

176 "path": str(path), 

177 "exists": True, 

178 "kind": "file", 

179 **self._fingerprint_file(path), 

180 } 

181 return fingerprints 

182 

183 @staticmethod 

184 def _fingerprint_file(path: Path) -> dict[str, Any]: 

185 """Return a stable SHA-256 and size for one explicit local file.""" 

186 digest = hashlib.sha256() 

187 with path.open("rb") as input_file: 

188 for chunk in iter(lambda: input_file.read(1024 * 1024), b""): 

189 digest.update(chunk) 

190 return {"sha256": digest.hexdigest(), "size": path.stat().st_size} 

191 

192 def _require_matrix_steps(self) -> None: 

193 """Require an explicit baseline and a positive step budget.""" 

194 if not self.matrix.get("baseline"): 

195 raise ManifestError("matrix.baseline is required for validate") 

196 steps = self.matrix.get("steps") 

197 if not isinstance(steps, int) or steps <= 0: 

198 raise ManifestError("matrix.steps must be a positive integer") 

199 

200 def _require_launcher_contract(self) -> None: 

201 """Require an existing local Trainer config and a module path.""" 

202 trainer_config = self.launcher.get("config") 

203 trainer_module = self.launcher.get("module") 

204 if not isinstance(trainer_config, str) or not trainer_config: 

205 raise ManifestError("launcher.config must be a non-empty path") 

206 trainer_config_path = self.launcher_config_path 

207 if trainer_config_path is None or not trainer_config_path.is_file(): 

208 raise ManifestError( 

209 f"launcher.config does not exist: {trainer_config_path}" 

210 ) 

211 if not isinstance(trainer_module, str) or not trainer_module: 

212 raise ManifestError("launcher.module must be a non-empty dotted module") 

213 

214 def _require_matrix_resources(self) -> None: 

215 """Require a positive device count and a bool warm-start flag.""" 

216 devices = self.matrix.get("devices") 

217 if not isinstance(devices, int) or devices <= 0: 

218 raise ManifestError("matrix.devices must be a positive integer") 

219 shared_initial_checkpoint = self.matrix.get("shared_initial_checkpoint", False) 

220 if not isinstance(shared_initial_checkpoint, bool): 

221 raise ManifestError("matrix.shared_initial_checkpoint must be a bool") 

222 

223 def _require_launcher_timeout(self) -> None: 

224 """Require any declared launcher timeout to be positive.""" 

225 timeout = self.launcher.get("timeout_seconds") 

226 if timeout is not None and ( 

227 isinstance(timeout, bool) 

228 or not isinstance(timeout, (int, float)) 

229 or timeout <= 0 

230 ): 

231 raise ManifestError("launcher.timeout_seconds must be positive") 

232 

233 def _require_resume_split_step(self) -> None: 

234 """Bound the resume split step whenever resume cases are declared.""" 

235 steps = self.matrix.get("steps") 

236 has_resume_case = bool( 

237 self.matrix.get("same_topology_resume") 

238 or self.matrix.get("cross_topology_resume") 

239 ) 

240 split_step = self.matrix.get("resume_split_step") 

241 if has_resume_case and ( 

242 isinstance(split_step, bool) 

243 or not isinstance(split_step, int) 

244 or split_step <= 0 

245 or split_step >= steps 

246 ): 

247 raise ManifestError( 

248 "resume cases require 0 < matrix.resume_split_step < matrix.steps" 

249 ) 

250 shared_initial_checkpoint = self.matrix.get("shared_initial_checkpoint", False) 

251 if has_resume_case and shared_initial_checkpoint and split_step <= 1: 

252 raise ManifestError( 

253 "resume cases with matrix.shared_initial_checkpoint=true require " 

254 "matrix.resume_split_step > 1 because the shared warm-start is global step 1" 

255 ) 

256 

257 def _require_integration_handoff(self) -> None: 

258 """Require a PASS handoff for the same adapter family when declared.""" 

259 handoff = self.integration_handoff_path 

260 if handoff is not None and not handoff.is_file(): 

261 raise ManifestError(f"integration_handoff does not exist: {handoff}") 

262 if handoff is None: 

263 return 

264 try: 

265 handoff_payload = yaml.safe_load(handoff.read_text(encoding="utf-8")) 

266 except (OSError, yaml.YAMLError) as exc: 

267 raise ManifestError(f"could not parse integration_handoff: {exc}") from exc 

268 if not isinstance(handoff_payload, Mapping): 

269 raise ManifestError("integration_handoff must contain a mapping") 

270 if handoff_payload.get("schema_version") != SCHEMA_VERSION: 

271 raise ManifestError( 

272 f"integration_handoff schema_version must be {SCHEMA_VERSION}" 

273 ) 

274 if handoff_payload.get("status") != "PASS": 

275 raise ManifestError("integration_handoff status must be PASS") 

276 if handoff_payload.get("family") != self.family: 

277 raise ManifestError( 

278 "integration_handoff family does not match model adapter" 

279 ) 

280 

281 def validate_for(self, command: str) -> None: 

282 """Validate command-specific fields without silently adding defaults.""" 

283 _ = self.model_path 

284 _ = self.reference_path 

285 _ = self.integration_handoff_path 

286 if command == "parity" and not self.reference: 

287 raise ManifestError(f"reference is required for {command}") 

288 if command != "validate": 

289 return 

290 self._require_matrix_steps() 

291 self._require_launcher_contract() 

292 self._require_matrix_resources() 

293 self._require_launcher_timeout() 

294 self._require_resume_split_step() 

295 self._require_integration_handoff() 

296 

297 

298def load_manifest(path: str | Path, output_dir: str | Path | None = None) -> ValidationManifest: 

299 """Load and validate a local YAML/JSON integration manifest. 

300 

301 Args: 

302 path: Manifest path. 

303 output_dir: Optional CLI-only evidence directory override. 

304 

305 Returns: 

306 Resolved immutable manifest. 

307 

308 Raises: 

309 ManifestError: If schema, types, or local-only constraints are invalid. 

310 """ 

311 manifest_path = Path(path).expanduser().resolve() 

312 if not manifest_path.is_file(): 

313 raise ManifestError(f"manifest does not exist: {manifest_path}") 

314 try: 

315 if manifest_path.suffix.lower() == ".json": 

316 loaded = json.loads(manifest_path.read_text(encoding="utf-8")) 

317 else: 

318 loaded = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) 

319 except (OSError, json.JSONDecodeError, yaml.YAMLError) as exc: 

320 raise ManifestError(f"could not parse manifest {manifest_path}: {exc}") from exc 

321 raw = _mapping(loaded, "$") 

322 if raw.get("schema_version") != SCHEMA_VERSION: 

323 raise ManifestError(f"schema_version must be {SCHEMA_VERSION}") 

324 model = _mapping(raw.get("model"), "model") 

325 if not (model.get("adapter") or model.get("model_type")): 

326 raise ManifestError("model.adapter or model.model_type is required") 

327 reference = _mapping(raw.get("reference"), "reference", required=False) 

328 matrix = _mapping(raw.get("matrix"), "matrix", required=False) 

329 acceptance = _mapping(raw.get("acceptance"), "acceptance", required=False) 

330 launcher = _mapping(raw.get("launcher"), "launcher", required=False) 

331 configured_output = output_dir or raw.get("output_dir") or ( 

332 manifest_path.parent / "output" / "model_integration" / str(model.get("adapter")) 

333 ) 

334 resolved_output = Path(configured_output).expanduser() 

335 if not resolved_output.is_absolute(): 

336 resolved_output = (manifest_path.parent / resolved_output).resolve() 

337 else: 

338 resolved_output = resolved_output.resolve() 

339 return ValidationManifest( 

340 path=manifest_path, 

341 raw=raw, 

342 model=model, 

343 reference=reference, 

344 matrix=matrix, 

345 acceptance=acceptance, 

346 launcher=launcher, 

347 output_dir=resolved_output, 

348 ) 

349 

350 

351__all__ = ["ManifestError", "ValidationManifest", "load_manifest"]