open_llm_leaderboard / backend /app /utils /model_validation.py
alozowski's picture
alozowski HF staff
Fix read models lfs files
50c76cd
import json
import logging
import asyncio
import aiohttp
from typing import Tuple, Optional, Dict, Any
from datasets import load_dataset
from huggingface_hub import HfApi, ModelCard, hf_hub_download
from huggingface_hub import hf_api
from huggingface_hub.utils import build_hf_headers
from transformers import AutoConfig, AutoTokenizer
from app.config.base import HF_TOKEN
from app.config.hf_config import OFFICIAL_PROVIDERS_REPO
from app.core.formatting import LogFormatter
logger = logging.getLogger(__name__)
class ModelValidator:
def __init__(self):
self.token = HF_TOKEN
self.api = HfApi(token=self.token)
self.headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
async def check_model_card(self, model_id: str) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
"""Check if model has a valid model card"""
try:
logger.info(LogFormatter.info(f"Checking model card for {model_id}"))
# Get model card content using ModelCard.load
try:
model_card = await asyncio.to_thread(
ModelCard.load,
model_id
)
logger.info(LogFormatter.success("Model card found"))
except Exception as e:
error_msg = "Please add a model card to your model to explain how you trained/fine-tuned it."
logger.error(LogFormatter.error(error_msg, e))
return False, error_msg, None
# Check license in model card data
if model_card.data.license is None and not ("license_name" in model_card.data and "license_link" in model_card.data):
error_msg = "License not found. Please add a license to your model card using the `license` metadata or a `license_name`/`license_link` pair."
logger.warning(LogFormatter.warning(error_msg))
return False, error_msg, None
# Enforce card content length
if len(model_card.text) < 200:
error_msg = "Please add a description to your model card, it is too short."
logger.warning(LogFormatter.warning(error_msg))
return False, error_msg, None
logger.info(LogFormatter.success("Model card validation passed"))
return True, "", model_card
except Exception as e:
error_msg = "Failed to validate model card"
logger.error(LogFormatter.error(error_msg, e))
return False, str(e), None
async def get_safetensors_metadata(self, model_id: str, is_adapter: bool = False, revision: str = "main") -> Optional[Dict]:
"""Get metadata from a safetensors file"""
try:
if is_adapter:
metadata = await asyncio.to_thread(
hf_api.parse_safetensors_file_metadata,
model_id,
"adapter_model.safetensors",
token=self.token,
revision=revision,
)
else:
metadata = await asyncio.to_thread(
hf_api.get_safetensors_metadata,
repo_id=model_id,
token=self.token,
revision=revision,
)
return metadata
except Exception as e:
logger.error(f"Failed to get safetensors metadata: {str(e)}")
return None
async def get_model_size(
self,
model_info: Any,
precision: str,
base_model: str,
revision: str
) -> Tuple[Optional[float], Optional[str]]:
"""Get model size in billions of parameters"""
try:
logger.info(LogFormatter.info(f"Checking model size for {model_info.modelId}"))
# Check if model is adapter
is_adapter = any(s.rfilename == "adapter_config.json" for s in model_info.siblings if hasattr(s, 'rfilename'))
# Try to get size from safetensors first
model_size = None
if is_adapter and base_model:
# For adapters, we need both adapter and base model sizes
adapter_meta = await self.get_safetensors_metadata(model_info.id, is_adapter=True, revision=revision)
base_meta = await self.get_safetensors_metadata(base_model, revision="main")
if adapter_meta and base_meta:
adapter_size = sum(adapter_meta.parameter_count.values())
base_size = sum(base_meta.parameter_count.values())
model_size = adapter_size + base_size
else:
# For regular models, just get the model size
meta = await self.get_safetensors_metadata(model_info.id, revision=revision)
if meta:
model_size = sum(meta.parameter_count.values()) # total params
if model_size is None:
# If model size could not be determined, return an error
return None, "Model size could not be determined"
# Adjust size for GPTQ models
size_factor = 8 if (precision == "GPTQ" or "gptq" in model_info.id.lower()) else 1
model_size = model_size / 1e9 # Convert to billions, assuming float16
model_size = round(size_factor * model_size, 3)
logger.info(LogFormatter.success(f"Model size: {model_size}B parameters"))
return model_size, None
except Exception as e:
logger.error(LogFormatter.error(f"Error while determining model size: {e}"))
return None, str(e)
async def check_chat_template(
self,
model_id: str,
revision: str
) -> Tuple[bool, Optional[str]]:
"""Check if model has a valid chat template"""
try:
logger.info(LogFormatter.info(f"Checking chat template for {model_id}"))
try:
config_file = await asyncio.to_thread(
hf_hub_download,
repo_id=model_id,
filename="tokenizer_config.json",
revision=revision,
repo_type="model"
)
with open(config_file, 'r') as f:
tokenizer_config = json.load(f)
if 'chat_template' not in tokenizer_config:
error_msg = f"The model {model_id} doesn't have a chat_template in its tokenizer_config.json. Please add a chat_template before submitting or submit without it."
logger.error(LogFormatter.error(error_msg))
return False, error_msg
logger.info(LogFormatter.success("Valid chat template found"))
return True, None
except Exception as e:
error_msg = f"Error checking chat_template: {str(e)}"
logger.error(LogFormatter.error(error_msg))
return False, error_msg
except Exception as e:
error_msg = "Failed to check chat template"
logger.error(LogFormatter.error(error_msg, e))
return False, str(e)
async def is_model_on_hub(
self,
model_name: str,
revision: str,
test_tokenizer: bool = False,
trust_remote_code: bool = False
) -> Tuple[bool, Optional[str], Optional[Any]]:
"""Check if model exists and is properly configured on the Hub"""
try:
# First try regular API approach
try:
config = await asyncio.to_thread(
AutoConfig.from_pretrained,
model_name,
revision=revision,
trust_remote_code=trust_remote_code,
token=self.token,
force_download=True
)
# Flag to indicate this is a regular config
config.is_lfs = False
except Exception as e:
logger.info(f"Standard config loading failed, attempting LFS fallback: {str(e)}")
# Fallback to direct LFS file access
async with aiohttp.ClientSession() as session:
config_url = f"https://huggingface.co./{model_name}/raw/{revision}/config.json"
headers = build_hf_headers(token=self.token)
async with session.get(config_url, headers=headers) as response:
if response.status == 200:
config_json = await response.json()
logger.info("Successfully loaded config.json from LFS")
config = AutoConfig.from_dict(config_json)
# Flag to indicate this is from LFS and store original JSON
config.is_lfs = True
config.lfs_json = config_json
else:
error_msg = f"Failed to load config.json (Status {response.status})"
logger.error(error_msg)
raise Exception(error_msg)
# Test tokenizer if requested
if test_tokenizer:
try:
await asyncio.to_thread(
AutoTokenizer.from_pretrained,
model_name,
revision=revision,
trust_remote_code=trust_remote_code,
token=self.token
)
except ValueError as e:
return False, f"The tokenizer is not available in an official Transformers release: {e}", None
except Exception:
return False, "The tokenizer cannot be loaded. Ensure the tokenizer class is part of a stable Transformers release and correctly configured.", None
return True, None, config
except ValueError as e:
if "trust_remote_code" in str(e):
return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None
return False, str(e), None
except Exception as e:
if "You are trying to access a gated repo." in str(e):
return True, "The model is gated and requires special access permissions.", None
return False, f"The model was not found or is misconfigured on the Hub. Error: {e}", None
except ValueError as e:
if "trust_remote_code" in str(e):
return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None
return False, str(e), None
except Exception as e:
if "You are trying to access a gated repo." in str(e):
return True, "The model is gated and requires special access permissions.", None
return False, f"The model was not found or is misconfigured on the Hub. Error: {e}", None
except ValueError:
return False, "The model requires `trust_remote_code=True` to launch, and for safety reasons, we don't accept such models automatically.", None
except Exception as e:
if "You are trying to access a gated repo." in str(e):
return True, "The model is gated and requires special access permissions.", None
return False, f"The model was not found or is misconfigured on the Hub. Error: {e.args[0]}", None
async def check_official_provider_status(
self,
model_id: str,
existing_models: Dict[str, list]
) -> Tuple[bool, Optional[str]]:
"""
Check if model is from official provider and has finished submission.
Args:
model_id: The model identifier (org/model-name)
existing_models: Dictionary of models by status from get_models()
Returns:
Tuple[bool, Optional[str]]: (is_valid, error_message)
"""
try:
logger.info(LogFormatter.info(f"Checking official provider status for {model_id}"))
# Get model organization
model_org = model_id.split('/')[0] if '/' in model_id else None
if not model_org:
return True, None
# Load official providers dataset
dataset = load_dataset(OFFICIAL_PROVIDERS_REPO)
official_providers = dataset["train"][0]["CURATED_SET"]
# Check if model org is in official providers
is_official = model_org in official_providers
if is_official:
logger.info(LogFormatter.info(f"Model organization '{model_org}' is an official provider"))
# Check for finished submissions
if "finished" in existing_models:
for model in existing_models["finished"]:
if model["name"] == model_id:
error_msg = (
f"Model {model_id} is an official provider model "
f"with a completed evaluation. "
f"To re-evaluate, please open a discussion."
)
logger.error(LogFormatter.error("Validation failed", error_msg))
return False, error_msg
logger.info(LogFormatter.success("No finished submission found for this official provider model"))
else:
logger.info(LogFormatter.info(f"Model organization '{model_org}' is not an official provider"))
return True, None
except Exception as e:
error_msg = f"Failed to check official provider status: {str(e)}"
logger.error(LogFormatter.error(error_msg))
return False, error_msg