Coverage for transformer_lens/tools/model_registry/api.py: 60%

144 statements  

« prev     ^ index     » next       coverage.py v7.10.1, created at 2026-09-21 19:27 +0000

1"""Public API for the TransformerLens model registry. 

2 

3This module provides a clean, programmatic interface for accessing model registry 

4data. It supports lazy loading with in-memory caching to avoid repeated file reads. 

5 

6Example usage: 

7 >>> from transformer_lens.tools.model_registry import api # doctest: +SKIP 

8 >>> api.is_model_supported("openai-community/gpt2") # doctest: +SKIP 

9 True 

10 >>> models = api.get_supported_models() # doctest: +SKIP 

11 >>> gpt2_models = api.get_architecture_models("GPT2LMHeadModel") # doctest: +SKIP 

12 >>> gaps = api.get_unsupported_architectures(min_models=100, top_n=10) # doctest: +SKIP 

13""" 

14 

15import json 

16import logging 

17from pathlib import Path 

18from threading import Lock 

19from typing import Optional 

20 

21from . import ARCHITECTURE_ALIASES 

22from .exceptions import DataNotLoadedError, ModelNotFoundError 

23from .schemas import ( 

24 ArchitectureGap, 

25 ArchitectureGapsReport, 

26 ArchitectureStats, 

27 ModelEntry, 

28 SupportedModelsReport, 

29) 

30from .verification import VerificationHistory 

31 

32logger = logging.getLogger(__name__) 

33 

34# Module-level cache for lazy loading 

35_cache: dict[str, object] = {} 

36_cache_lock = Lock() 

37 

38# Default data directory (relative to this module) 

39_DATA_DIR = Path(__file__).parent / "data" 

40 

41 

42def _load_json(filename: str) -> dict: 

43 """Load a JSON file from the data directory. 

44 

45 Args: 

46 filename: Name of the JSON file 

47 

48 Returns: 

49 Parsed JSON data as a dictionary 

50 

51 Raises: 

52 DataNotLoadedError: If the file doesn't exist or can't be read 

53 """ 

54 path = _DATA_DIR / filename 

55 if not path.exists(): 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

56 raise DataNotLoadedError(filename, str(path)) 

57 try: 

58 with open(path) as f: 

59 return json.load(f) 

60 except json.JSONDecodeError as e: 

61 raise DataNotLoadedError(filename, str(path)) from e 

62 

63 

64def _get_supported_models_report() -> SupportedModelsReport: 

65 """Get the cached supported models report, loading if necessary. 

66 

67 Returns: 

68 The SupportedModelsReport instance 

69 

70 Raises: 

71 DataNotLoadedError: If the data files are not available 

72 """ 

73 cache_key = "supported_models" 

74 with _cache_lock: 

75 if cache_key not in _cache: 

76 data = _load_json("supported_models.json") 

77 _cache[cache_key] = SupportedModelsReport.from_dict(data) 

78 result = _cache[cache_key] 

79 assert isinstance(result, SupportedModelsReport) 

80 return result 

81 

82 

83def _get_architecture_gaps_report() -> ArchitectureGapsReport: 

84 """Get the cached architecture gaps report, loading if necessary. 

85 

86 Returns: 

87 The ArchitectureGapsReport instance 

88 

89 Raises: 

90 DataNotLoadedError: If the data file is not available 

91 """ 

92 cache_key = "architecture_gaps" 

93 with _cache_lock: 

94 if cache_key not in _cache: 94 ↛ 97line 94 didn't jump to line 97 because the condition on line 94 was always true

95 data = _load_json("architecture_gaps.json") 

96 _cache[cache_key] = ArchitectureGapsReport.from_dict(data) 

97 result = _cache[cache_key] 

98 assert isinstance(result, ArchitectureGapsReport) 

99 return result 

100 

101 

102def _get_verification_history() -> VerificationHistory: 

103 """Get the cached verification history, loading if necessary. 

104 

105 Returns: 

106 The VerificationHistory instance 

107 

108 Raises: 

109 DataNotLoadedError: If the data files are not available 

110 """ 

111 cache_key = "verification_history" 

112 with _cache_lock: 

113 if cache_key not in _cache: 

114 data = _load_json("verification_history.json") 

115 _cache[cache_key] = VerificationHistory.from_dict(data) 

116 result = _cache[cache_key] 

117 assert isinstance(result, VerificationHistory) 

118 return result 

119 

120 

121def clear_cache() -> None: 

122 """Clear all cached data. 

123 

124 This forces data to be reloaded from disk on the next access. 

125 Useful after updating data files or for testing. 

126 """ 

127 with _cache_lock: 

128 _cache.clear() 

129 logger.debug("Model registry cache cleared") 

130 

131 

132def get_supported_models( 

133 architecture: Optional[str] = None, 

134 verified_only: bool = False, 

135) -> list[ModelEntry]: 

136 """Get a list of supported models. 

137 

138 Args: 

139 architecture: Filter by architecture ID (e.g., "GPT2LMHeadModel"). 

140 If None, returns all supported models. 

141 verified_only: If True, only return models that have been verified 

142 to work with TransformerLens. 

143 

144 Returns: 

145 List of ModelEntry objects matching the filters 

146 

147 Raises: 

148 DataNotLoadedError: If the supported models data is not available 

149 

150 Example: 

151 >>> models = get_supported_models(architecture="GPT2LMHeadModel") # doctest: +SKIP 

152 >>> verified = get_supported_models(verified_only=True) # doctest: +SKIP 

153 """ 

154 report = _get_supported_models_report() 

155 models = report.models 

156 

157 if architecture: 

158 models = [m for m in models if m.architecture_id == architecture] 

159 

160 if verified_only: 

161 models = [m for m in models if m.status == 1] 

162 

163 return models 

164 

165 

166def get_unsupported_architectures( 

167 min_models: int = 0, 

168 top_n: Optional[int] = None, 

169) -> list[ArchitectureGap]: 

170 """Get a list of unsupported architectures sorted by model count. 

171 

172 Args: 

173 min_models: Minimum number of models for an architecture to be included. 

174 Useful for filtering out rare architectures. 

175 top_n: Return only the top N architectures by model count. 

176 If None, returns all matching architectures. 

177 

178 Returns: 

179 List of ArchitectureGap objects sorted by total_models (descending) 

180 

181 Raises: 

182 DataNotLoadedError: If the architecture gaps data is not available 

183 

184 Example: 

185 >>> gaps = get_unsupported_architectures(min_models=100, top_n=10) # doctest: +SKIP 

186 >>> for gap in gaps: # doctest: +SKIP 

187 ... print(f"{gap.architecture_id}: {gap.total_models} models") 

188 """ 

189 report = _get_architecture_gaps_report() 

190 gaps = report.gaps 

191 

192 if min_models > 0: 

193 gaps = [g for g in gaps if g.total_models >= min_models] 

194 

195 # Already sorted by total_models descending in the report 

196 if top_n is not None: 

197 gaps = gaps[:top_n] 

198 

199 return gaps 

200 

201 

202def is_model_supported(model_id: str) -> bool: 

203 """Check if a model is supported by TransformerLens. 

204 

205 Args: 

206 model_id: The HuggingFace model ID to check (e.g., "gpt2", "meta-llama/Llama-2-7b-hf") 

207 

208 Returns: 

209 True if the model is in the supported models list, False otherwise 

210 

211 Raises: 

212 DataNotLoadedError: If the supported models data is not available 

213 

214 Example: 

215 >>> is_model_supported("openai-community/gpt2") # doctest: +SKIP 

216 True 

217 >>> is_model_supported("some-unsupported-model") # doctest: +SKIP 

218 False 

219 """ 

220 report = _get_supported_models_report() 

221 return any(m.model_id == model_id for m in report.models) 

222 

223 

224def get_model_architecture(model_id: str) -> Optional[str]: 

225 """Get the architecture ID for a given model. 

226 

227 Args: 

228 model_id: The HuggingFace model ID to look up 

229 

230 Returns: 

231 The architecture ID (e.g., "GPT2LMHeadModel"), or None if not found 

232 

233 Raises: 

234 DataNotLoadedError: If the supported models data is not available 

235 

236 Example: 

237 >>> get_model_architecture("openai-community/gpt2") # doctest: +SKIP 

238 'GPT2LMHeadModel' 

239 >>> get_model_architecture("unknown-model") # doctest: +SKIP 

240 """ 

241 report = _get_supported_models_report() 

242 for model in report.models: 

243 if model.model_id == model_id: 

244 return model.architecture_id 

245 return None 

246 

247 

248def get_architecture_models(architecture_id: str) -> list[str]: 

249 """Get all model IDs for a given architecture. 

250 

251 Args: 

252 architecture_id: The architecture to get models for (e.g., "GPT2LMHeadModel") 

253 

254 Returns: 

255 List of model IDs that use this architecture 

256 

257 Raises: 

258 DataNotLoadedError: If the supported models data is not available 

259 

260 Example: 

261 >>> models = get_architecture_models("GPT2LMHeadModel") # doctest: +SKIP 

262 >>> "openai-community/gpt2" in models # doctest: +SKIP 

263 True 

264 """ 

265 report = _get_supported_models_report() 

266 return [m.model_id for m in report.models if m.architecture_id == architecture_id] 

267 

268 

269def suggest_similar_model(model_id: str) -> Optional[str]: 

270 """Suggest a similar supported model for an unsupported model ID. 

271 

272 This function attempts to find a supported model that is similar to the 

273 requested model based on naming patterns. Useful for providing helpful 

274 suggestions when a user tries to use an unsupported model. 

275 

276 Args: 

277 model_id: The model ID that is not supported 

278 

279 Returns: 

280 A suggested model ID, or None if no similar model is found 

281 

282 Raises: 

283 DataNotLoadedError: If the supported models data is not available 

284 

285 Example: 

286 >>> suggest_similar_model("bigscience/bloom-560m") # doctest: +SKIP 

287 'bigscience/bloom-1b1' 

288 """ 

289 report = _get_supported_models_report() 

290 

291 # If the model is already supported, return None (no suggestion needed) 

292 if any(m.model_id == model_id for m in report.models): 292 ↛ 293line 292 didn't jump to line 293 because the condition on line 292 was never true

293 return None 

294 

295 # Extract potential matching criteria from the model ID 

296 model_id_lower = model_id.lower() 

297 parts = model_id.replace("/", "-").replace("_", "-").lower().split("-") 

298 

299 # Build a scoring function for similarity 

300 def score_model(candidate: ModelEntry) -> int: 

301 candidate_lower = candidate.model_id.lower() 

302 score = 0 

303 

304 # Same organization prefix 

305 if "/" in model_id and "/" in candidate.model_id: 305 ↛ 310line 305 didn't jump to line 310 because the condition on line 305 was always true

306 if model_id.split("/")[0].lower() == candidate.model_id.split("/")[0].lower(): 306 ↛ 307line 306 didn't jump to line 307 because the condition on line 306 was never true

307 score += 10 

308 

309 # Matching parts 

310 for part in parts: 

311 if len(part) > 2 and part in candidate_lower: 311 ↛ 312line 311 didn't jump to line 312 because the condition on line 311 was never true

312 score += 5 

313 

314 # Architecture name hints 

315 arch_hints = ["gpt", "llama", "bloom", "opt", "mistral", "gemma", "phi", "qwen"] 

316 for hint in arch_hints: 

317 if hint in model_id_lower and hint in candidate_lower: 317 ↛ 318line 317 didn't jump to line 318 because the condition on line 317 was never true

318 score += 8 

319 

320 return score 

321 

322 # Score all models and find the best match 

323 scored = [(m, score_model(m)) for m in report.models] 

324 scored = [(m, s) for m, s in scored if s > 0] # Only consider matches with some score 

325 scored.sort(key=lambda x: x[1], reverse=True) 

326 

327 if scored: 327 ↛ 328line 327 didn't jump to line 328 because the condition on line 327 was never true

328 return scored[0][0].model_id 

329 return None 

330 

331 

332def get_model_info(model_id: str) -> ModelEntry: 

333 """Get full information about a specific model. 

334 

335 Args: 

336 model_id: The HuggingFace model ID to look up 

337 

338 Returns: 

339 The ModelEntry for this model 

340 

341 Raises: 

342 ModelNotFoundError: If the model is not in the registry 

343 DataNotLoadedError: If the supported models data is not available 

344 

345 Example: 

346 >>> info = get_model_info("openai-community/gpt2") # doctest: +SKIP 

347 >>> info.architecture_id # doctest: +SKIP 

348 'GPT2LMHeadModel' 

349 """ 

350 report = _get_supported_models_report() 

351 for model in report.models: 

352 if model.model_id == model_id: 352 ↛ 353line 352 didn't jump to line 353 because the condition on line 352 was never true

353 return model 

354 

355 # Model not found - try to suggest an alternative 

356 suggestion = suggest_similar_model(model_id) 

357 raise ModelNotFoundError(model_id, suggestion) 

358 

359 

360def get_supported_architectures() -> list[str]: 

361 """Get a list of all supported architecture IDs. 

362 

363 Returns: 

364 List of unique architecture IDs that TransformerLens supports 

365 

366 Raises: 

367 DataNotLoadedError: If the supported models data is not available 

368 

369 Example: 

370 >>> archs = get_supported_architectures() # doctest: +SKIP 

371 >>> "GPT2LMHeadModel" in archs # doctest: +SKIP 

372 True 

373 """ 

374 report = _get_supported_models_report() 

375 return list(sorted(set(m.architecture_id for m in report.models))) 

376 

377 

378def get_all_architectures_with_stats() -> list[ArchitectureStats]: 

379 """Get statistics for all architectures (both supported and unsupported). 

380 

381 Returns: 

382 List of ArchitectureStats objects for all known architectures, 

383 sorted by model count (descending) 

384 

385 Raises: 

386 DataNotLoadedError: If the registry data is not available 

387 

388 Example: 

389 >>> stats = get_all_architectures_with_stats() # doctest: +SKIP 

390 >>> for s in stats[:5]: # doctest: +SKIP 

391 ... status = "supported" if s.is_supported else "unsupported" 

392 ... print(f"{s.architecture_id}: {s.model_count} models ({status})") 

393 """ 

394 gaps_report = _get_architecture_gaps_report() 

395 supported_report = _get_supported_models_report() 

396 

397 # Build stats for supported architectures 

398 arch_stats: dict[str, ArchitectureStats] = {} 

399 for model in supported_report.models: 

400 arch_id = model.architecture_id 

401 if arch_id not in arch_stats: 

402 arch_stats[arch_id] = ArchitectureStats( 

403 architecture_id=arch_id, 

404 is_supported=True, 

405 model_count=0, 

406 verified_count=0, 

407 example_models=[], 

408 ) 

409 stats_obj = arch_stats[arch_id] 

410 stats_obj.model_count += 1 

411 if model.status == 1: 

412 stats_obj.verified_count += 1 

413 if len(stats_obj.example_models) < 5: 

414 stats_obj.example_models.append(model.model_id) 

415 

416 # Add stats for unsupported architectures 

417 for gap in gaps_report.gaps: 

418 if gap.architecture_id not in arch_stats: 

419 arch_stats[gap.architecture_id] = ArchitectureStats( 

420 architecture_id=gap.architecture_id, 

421 is_supported=False, 

422 model_count=gap.total_models, 

423 verified_count=0, 

424 example_models=[], 

425 ) 

426 

427 result = sorted(arch_stats.values(), key=lambda x: x.model_count, reverse=True) 

428 return result 

429 

430 

431def is_architecture_supported(architecture_id: str) -> bool: 

432 """Check if an architecture is supported by TransformerLens. 

433 

434 Args: 

435 architecture_id: The architecture ID to check 

436 

437 Returns: 

438 True if the architecture is supported, False otherwise 

439 

440 Raises: 

441 DataNotLoadedError: If the supported models data is not available 

442 

443 Example: 

444 >>> is_architecture_supported("GPT2LMHeadModel") # doctest: +SKIP 

445 True 

446 >>> is_architecture_supported("SomeUnknownModel") # doctest: +SKIP 

447 False 

448 """ 

449 # Hub configs may report an alias casing (e.g. JetMoEForCausalLM) while rows 

450 # are keyed by the transformers class name; normalize before scanning. 

451 architecture_id = ARCHITECTURE_ALIASES.get(architecture_id, architecture_id) 

452 report = _get_supported_models_report() 

453 return any(m.architecture_id == architecture_id for m in report.models) 

454 

455 

456def get_registry_stats() -> dict: 

457 """Get summary statistics about the model registry. 

458 

459 Returns: 

460 Dictionary with registry statistics including: 

461 - total_supported_models: Number of supported models 

462 - total_supported_architectures: Number of supported architectures 

463 - total_verified: Number of verified models 

464 - total_provisional: Number of provisional (structural-only) models 

465 - total_unsupported_architectures: Number of unsupported architectures 

466 - generated_at: When the data was generated 

467 

468 Raises: 

469 DataNotLoadedError: If the registry data is not available 

470 

471 Example: 

472 >>> stats = get_registry_stats() # doctest: +SKIP 

473 >>> print(f"Supported: {stats['total_supported_models']} models") # doctest: +SKIP 

474 """ 

475 supported = _get_supported_models_report() 

476 gaps = _get_architecture_gaps_report() 

477 

478 return { 

479 "total_supported_models": supported.total_models, 

480 "total_supported_architectures": supported.total_architectures, 

481 "total_verified": supported.total_verified, 

482 "total_provisional": supported.total_provisional, 

483 "total_unsupported_architectures": gaps.total_unsupported_architectures, 

484 "total_unsupported_models": gaps.total_unsupported_models, 

485 "supported_generated_at": supported.generated_at.isoformat(), 

486 "gaps_generated_at": gaps.generated_at.isoformat(), 

487 }