Export runner labels via env var (#13018)
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
Validate model integrity for CI runners and download if needed.
|
||||
|
||||
This script checks HuggingFace cache for model completeness and downloads
|
||||
missing models. It exits with code 1 if download was required (indicating
|
||||
cache corruption), which causes the CI job to fail and surface cache issues.
|
||||
missing models. It exits with code 0 if models are present or successfully
|
||||
downloaded (emitting a warning annotation if repairs were needed), and exits
|
||||
with code 1 only if download attempts fail.
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
@@ -144,7 +146,7 @@ def validate_safetensors_file(file_path: Path) -> Tuple[bool, Optional[str]]:
|
||||
return False, f"{error_type}: {error_msg}"
|
||||
|
||||
|
||||
def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str]]:
|
||||
def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str], List[Path]]:
|
||||
"""
|
||||
Validate that all model shards are present and complete.
|
||||
|
||||
@@ -152,7 +154,8 @@ def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str]]:
|
||||
model_path: Path to model's snapshot directory
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
Tuple of (is_valid, error_message, corrupted_files)
|
||||
- corrupted_files: List of paths to corrupted shard files that should be removed
|
||||
"""
|
||||
# Pattern for sharded files: model-00001-of-00009.safetensors or pytorch_model-00001-of-00009.bin
|
||||
shard_pattern = re.compile(
|
||||
@@ -176,9 +179,13 @@ def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str]]:
|
||||
if single_files[0].suffix == ".safetensors":
|
||||
is_valid, error_msg = validate_safetensors_file(single_files[0])
|
||||
if not is_valid:
|
||||
return False, f"Corrupted file {single_files[0].name}: {error_msg}"
|
||||
return True, None
|
||||
return False, "No model files found (safetensors or bin)"
|
||||
return (
|
||||
False,
|
||||
f"Corrupted file {single_files[0].name}: {error_msg}",
|
||||
[single_files[0]],
|
||||
)
|
||||
return True, None, []
|
||||
return False, "No model files found (safetensors or bin)", []
|
||||
|
||||
# Extract total shard count from any shard filename
|
||||
total_shards = None
|
||||
@@ -189,7 +196,7 @@ def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str]]:
|
||||
break
|
||||
|
||||
if total_shards is None:
|
||||
return False, "Could not determine total shard count from filenames"
|
||||
return False, "Could not determine total shard count from filenames", []
|
||||
|
||||
# Check that all shards exist
|
||||
expected_shards = set(range(1, total_shards + 1))
|
||||
@@ -205,25 +212,41 @@ def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str]]:
|
||||
|
||||
if missing_shards:
|
||||
missing_list = sorted(missing_shards)
|
||||
return False, f"Missing shards: {missing_list} (expected {total_shards} total)"
|
||||
# Missing shards - nothing to remove, let download handle it
|
||||
return (
|
||||
False,
|
||||
f"Missing shards: {missing_list} (expected {total_shards} total)",
|
||||
[],
|
||||
)
|
||||
|
||||
# Check for index file
|
||||
index_file = model_path / "model.safetensors.index.json"
|
||||
if not index_file.exists():
|
||||
return False, "Missing model.safetensors.index.json"
|
||||
return False, "Missing model.safetensors.index.json", []
|
||||
|
||||
# Validate each safetensors shard file for corruption
|
||||
print(f" Validating {len(shard_files)} shard file(s) for corruption...")
|
||||
corrupted_files = []
|
||||
for shard_file in shard_files:
|
||||
if shard_file.suffix == ".safetensors":
|
||||
is_valid, error_msg = validate_safetensors_file(shard_file)
|
||||
if not is_valid:
|
||||
return False, f"Corrupted shard {shard_file.name}: {error_msg}"
|
||||
corrupted_files.append(shard_file)
|
||||
print(f" ✗ Corrupted: {shard_file.name} - {error_msg}")
|
||||
|
||||
return True, None
|
||||
if corrupted_files:
|
||||
return (
|
||||
False,
|
||||
f"Corrupted shards: {[f.name for f in corrupted_files]}",
|
||||
corrupted_files,
|
||||
)
|
||||
|
||||
return True, None, []
|
||||
|
||||
|
||||
def validate_model(model_id: str, cache_dir: str) -> Tuple[bool, Optional[str]]:
|
||||
def validate_model(
|
||||
model_id: str, cache_dir: str
|
||||
) -> Tuple[bool, Optional[str], List[Path]]:
|
||||
"""
|
||||
Validate a model's cache integrity.
|
||||
|
||||
@@ -232,37 +255,46 @@ def validate_model(model_id: str, cache_dir: str) -> Tuple[bool, Optional[str]]:
|
||||
cache_dir: HuggingFace cache directory
|
||||
|
||||
Returns:
|
||||
Tuple of (is_valid, error_message)
|
||||
Tuple of (is_valid, error_message, corrupted_files)
|
||||
- corrupted_files: List of paths to corrupted files that should be removed
|
||||
"""
|
||||
print(f"Validating model: {model_id}")
|
||||
|
||||
# Find model in cache
|
||||
model_path = get_model_cache_path(model_id, cache_dir)
|
||||
if model_path is None:
|
||||
return False, "Model not found in cache"
|
||||
return False, "Model not found in cache", []
|
||||
|
||||
print(f" Found in cache: {model_path}")
|
||||
|
||||
# Check for incomplete files
|
||||
incomplete_files = check_incomplete_files(model_path, cache_dir)
|
||||
if incomplete_files:
|
||||
return False, f"Found incomplete download files: {len(incomplete_files)} files"
|
||||
return (
|
||||
False,
|
||||
f"Found incomplete download files: {len(incomplete_files)} files",
|
||||
[],
|
||||
)
|
||||
|
||||
# Validate shards
|
||||
is_valid, error_msg = validate_model_shards(model_path)
|
||||
is_valid, error_msg, corrupted_files = validate_model_shards(model_path)
|
||||
if not is_valid:
|
||||
return False, error_msg
|
||||
return False, error_msg, corrupted_files
|
||||
|
||||
print(f" ✓ Model validated successfully")
|
||||
return True, None
|
||||
return True, None, []
|
||||
|
||||
|
||||
def download_model(model_id: str) -> bool:
|
||||
def download_model(model_id: str, cache_dir: str, corrupted_files: List[Path]) -> bool:
|
||||
"""
|
||||
Download a model from HuggingFace.
|
||||
|
||||
Completely removes the model cache directory before downloading to ensure a clean download.
|
||||
|
||||
Args:
|
||||
model_id: Model identifier
|
||||
cache_dir: HuggingFace cache directory
|
||||
corrupted_files: List of specific file paths that are corrupted (unused, kept for compatibility)
|
||||
|
||||
Returns:
|
||||
True if download succeeded, False otherwise
|
||||
@@ -272,7 +304,23 @@ def download_model(model_id: str) -> bool:
|
||||
return False
|
||||
|
||||
print(f"Downloading model: {model_id}")
|
||||
print(f" This may take a while for large models...")
|
||||
|
||||
# Completely remove the model directory from cache
|
||||
cache_model_name = "models--" + model_id.replace("/", "--")
|
||||
model_cache_path = Path(cache_dir) / cache_model_name
|
||||
|
||||
if model_cache_path.exists():
|
||||
print(f" Removing entire model directory: {model_cache_path}")
|
||||
try:
|
||||
shutil.rmtree(model_cache_path)
|
||||
print(f" ✓ Successfully removed model directory")
|
||||
except Exception as e:
|
||||
print(f" ✗ Failed to remove model directory: {e}")
|
||||
print(f" Attempting download anyway...")
|
||||
else:
|
||||
print(f" Model directory not found in cache (will download fresh)")
|
||||
|
||||
print(f" Downloading from HuggingFace (this may take a while for large models)...")
|
||||
|
||||
try:
|
||||
snapshot_download(
|
||||
@@ -362,8 +410,8 @@ def main() -> int:
|
||||
Main validation logic.
|
||||
|
||||
Returns:
|
||||
0 if all models are valid or runner doesn't need validation
|
||||
1 if models needed to be downloaded or validation failed
|
||||
0 if all models are valid, successfully downloaded, or runner doesn't need validation
|
||||
1 only if download attempts fail
|
||||
"""
|
||||
print("=" * 70)
|
||||
print("Model Validation for CI Runners")
|
||||
@@ -397,17 +445,16 @@ def main() -> int:
|
||||
print("-" * 70)
|
||||
|
||||
# Track validation results
|
||||
models_needing_download = []
|
||||
validation_errors = []
|
||||
# Maps model_id -> (error_msg, corrupted_files)
|
||||
models_needing_download: Dict[str, Tuple[str, List[Path]]] = {}
|
||||
|
||||
# Validate each required model
|
||||
for model_id in required_models:
|
||||
is_valid, error_msg = validate_model(model_id, cache_dir)
|
||||
is_valid, error_msg, corrupted_files = validate_model(model_id, cache_dir)
|
||||
|
||||
if not is_valid:
|
||||
print(f" ✗ Validation failed: {error_msg}")
|
||||
models_needing_download.append(model_id)
|
||||
validation_errors.append(f"{model_id}: {error_msg}")
|
||||
models_needing_download[model_id] = (error_msg, corrupted_files)
|
||||
|
||||
print("-" * 70)
|
||||
|
||||
@@ -418,16 +465,16 @@ def main() -> int:
|
||||
|
||||
# Models need to be downloaded
|
||||
print(f"⚠ Cache validation failed for {len(models_needing_download)} model(s)")
|
||||
for error in validation_errors:
|
||||
print(f" - {error}")
|
||||
for model_id, (error_msg, _) in models_needing_download.items():
|
||||
print(f" - {model_id}: {error_msg}")
|
||||
|
||||
print("-" * 70)
|
||||
print("Attempting to download missing/corrupted models...")
|
||||
print("-" * 70)
|
||||
|
||||
download_failed = False
|
||||
for model_id in models_needing_download:
|
||||
if not download_model(model_id):
|
||||
for model_id, (error_msg, corrupted_files) in models_needing_download.items():
|
||||
if not download_model(model_id, cache_dir, corrupted_files):
|
||||
download_failed = True
|
||||
|
||||
print("-" * 70)
|
||||
@@ -436,11 +483,39 @@ def main() -> int:
|
||||
print("✗ FAILED: Some models could not be downloaded")
|
||||
return 1
|
||||
|
||||
# All downloads succeeded, but we still exit with error to flag cache issues
|
||||
print("✗ FAILED: Models were downloaded due to cache corruption/missing files")
|
||||
print("This indicates the cache was invalid and needed to be repaired.")
|
||||
print("Failing the job to surface this issue for investigation.")
|
||||
return 1
|
||||
# All downloads succeeded - now validate them again
|
||||
print("✓ All models downloaded successfully!")
|
||||
print("-" * 70)
|
||||
print("Validating downloaded models...")
|
||||
print("-" * 70)
|
||||
|
||||
validation_failed = False
|
||||
for model_id in models_needing_download.keys():
|
||||
is_valid, error_msg, _ = validate_model(model_id, cache_dir)
|
||||
if not is_valid:
|
||||
print(f" ✗ Post-download validation failed for {model_id}: {error_msg}")
|
||||
validation_failed = True
|
||||
|
||||
print("-" * 70)
|
||||
|
||||
if validation_failed:
|
||||
print("✗ FAILED: Some models failed validation after download")
|
||||
return 1
|
||||
|
||||
# All validations passed - emit warning but exit successfully
|
||||
print("✓ All downloaded models validated successfully!")
|
||||
print("⚠ WARNING: Models were missing/corrupted in cache and have been repaired.")
|
||||
print(f" Repaired models: {', '.join(models_needing_download.keys())}")
|
||||
|
||||
# Emit GitHub Actions warning annotation for visibility
|
||||
print(
|
||||
f"::warning file=scripts/ci/validate_and_download_models.py::"
|
||||
f"Cache validation failed for {len(models_needing_download)} model(s). "
|
||||
f"Models were re-downloaded and validated successfully. "
|
||||
f"This may indicate cache corruption or infrastructure issues."
|
||||
)
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user