Export runner labels via env var (#13018)

This commit is contained in:
Kangyan-Zhou
2025-11-11 13:08:54 -08:00
committed by GitHub
parent 36d147121f
commit 9b247f7374
4 changed files with 122 additions and 37 deletions
+2
View File
@@ -37,6 +37,8 @@ jobs:
if: github.repository == 'sgl-project/sglang' if: github.repository == 'sgl-project/sglang'
runs-on: 8-gpu-b200 runs-on: 8-gpu-b200
continue-on-error: true continue-on-error: true
env:
RUNNER_LABELS: 8-gpu-b200
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
+2
View File
@@ -151,6 +151,8 @@ jobs:
if: github.repository == 'sgl-project/sglang' if: github.repository == 'sgl-project/sglang'
runs-on: 8-gpu-h200 runs-on: 8-gpu-h200
continue-on-error: true continue-on-error: true
env:
RUNNER_LABELS: 8-gpu-h200
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
+6
View File
@@ -467,6 +467,8 @@ jobs:
if: always() && !failure() && !cancelled() && if: always() && !failure() && !cancelled() &&
((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true'))
runs-on: 8-gpu-h200 runs-on: 8-gpu-h200
env:
RUNNER_LABELS: 8-gpu-h200
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
@@ -818,6 +820,8 @@ jobs:
if: always() && !failure() && !cancelled() && if: always() && !failure() && !cancelled() &&
((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true'))
runs-on: 8-gpu-h200 runs-on: 8-gpu-h200
env:
RUNNER_LABELS: 8-gpu-h200
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -845,6 +849,8 @@ jobs:
if: always() && !failure() && !cancelled() && if: always() && !failure() && !cancelled() &&
((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true')) ((needs.check-changes.outputs.main_package == 'true') || (needs.check-changes.outputs.sgl_kernel == 'true'))
runs-on: 8-gpu-h200 runs-on: 8-gpu-h200
env:
RUNNER_LABELS: 8-gpu-h200
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
+112 -37
View File
@@ -3,12 +3,14 @@
Validate model integrity for CI runners and download if needed. Validate model integrity for CI runners and download if needed.
This script checks HuggingFace cache for model completeness and downloads This script checks HuggingFace cache for model completeness and downloads
missing models. It exits with code 1 if download was required (indicating missing models. It exits with code 0 if models are present or successfully
cache corruption), which causes the CI job to fail and surface cache issues. downloaded (emitting a warning annotation if repairs were needed), and exits
with code 1 only if download attempts fail.
""" """
import os import os
import re import re
import shutil
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Dict, List, Optional, Tuple 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}" 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. 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 model_path: Path to model's snapshot directory
Returns: 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 # Pattern for sharded files: model-00001-of-00009.safetensors or pytorch_model-00001-of-00009.bin
shard_pattern = re.compile( 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": if single_files[0].suffix == ".safetensors":
is_valid, error_msg = validate_safetensors_file(single_files[0]) is_valid, error_msg = validate_safetensors_file(single_files[0])
if not is_valid: if not is_valid:
return False, f"Corrupted file {single_files[0].name}: {error_msg}" return (
return True, None False,
return False, "No model files found (safetensors or bin)" 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 # Extract total shard count from any shard filename
total_shards = None total_shards = None
@@ -189,7 +196,7 @@ def validate_model_shards(model_path: Path) -> Tuple[bool, Optional[str]]:
break break
if total_shards is None: 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 # Check that all shards exist
expected_shards = set(range(1, total_shards + 1)) 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: if missing_shards:
missing_list = sorted(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 # Check for index file
index_file = model_path / "model.safetensors.index.json" index_file = model_path / "model.safetensors.index.json"
if not index_file.exists(): 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 # Validate each safetensors shard file for corruption
print(f" Validating {len(shard_files)} shard file(s) for corruption...") print(f" Validating {len(shard_files)} shard file(s) for corruption...")
corrupted_files = []
for shard_file in shard_files: for shard_file in shard_files:
if shard_file.suffix == ".safetensors": if shard_file.suffix == ".safetensors":
is_valid, error_msg = validate_safetensors_file(shard_file) is_valid, error_msg = validate_safetensors_file(shard_file)
if not is_valid: 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. 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 cache_dir: HuggingFace cache directory
Returns: 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}") print(f"Validating model: {model_id}")
# Find model in cache # Find model in cache
model_path = get_model_cache_path(model_id, cache_dir) model_path = get_model_cache_path(model_id, cache_dir)
if model_path is None: 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}") print(f" Found in cache: {model_path}")
# Check for incomplete files # Check for incomplete files
incomplete_files = check_incomplete_files(model_path, cache_dir) incomplete_files = check_incomplete_files(model_path, cache_dir)
if incomplete_files: 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 # 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: if not is_valid:
return False, error_msg return False, error_msg, corrupted_files
print(f" ✓ Model validated successfully") 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. Download a model from HuggingFace.
Completely removes the model cache directory before downloading to ensure a clean download.
Args: Args:
model_id: Model identifier model_id: Model identifier
cache_dir: HuggingFace cache directory
corrupted_files: List of specific file paths that are corrupted (unused, kept for compatibility)
Returns: Returns:
True if download succeeded, False otherwise True if download succeeded, False otherwise
@@ -272,7 +304,23 @@ def download_model(model_id: str) -> bool:
return False return False
print(f"Downloading model: {model_id}") 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: try:
snapshot_download( snapshot_download(
@@ -362,8 +410,8 @@ def main() -> int:
Main validation logic. Main validation logic.
Returns: Returns:
0 if all models are valid or runner doesn't need validation 0 if all models are valid, successfully downloaded, or runner doesn't need validation
1 if models needed to be downloaded or validation failed 1 only if download attempts fail
""" """
print("=" * 70) print("=" * 70)
print("Model Validation for CI Runners") print("Model Validation for CI Runners")
@@ -397,17 +445,16 @@ def main() -> int:
print("-" * 70) print("-" * 70)
# Track validation results # Track validation results
models_needing_download = [] # Maps model_id -> (error_msg, corrupted_files)
validation_errors = [] models_needing_download: Dict[str, Tuple[str, List[Path]]] = {}
# Validate each required model # Validate each required model
for model_id in required_models: 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: if not is_valid:
print(f" ✗ Validation failed: {error_msg}") print(f" ✗ Validation failed: {error_msg}")
models_needing_download.append(model_id) models_needing_download[model_id] = (error_msg, corrupted_files)
validation_errors.append(f"{model_id}: {error_msg}")
print("-" * 70) print("-" * 70)
@@ -418,16 +465,16 @@ def main() -> int:
# Models need to be downloaded # Models need to be downloaded
print(f"⚠ Cache validation failed for {len(models_needing_download)} model(s)") print(f"⚠ Cache validation failed for {len(models_needing_download)} model(s)")
for error in validation_errors: for model_id, (error_msg, _) in models_needing_download.items():
print(f" - {error}") print(f" - {model_id}: {error_msg}")
print("-" * 70) print("-" * 70)
print("Attempting to download missing/corrupted models...") print("Attempting to download missing/corrupted models...")
print("-" * 70) print("-" * 70)
download_failed = False download_failed = False
for model_id in models_needing_download: for model_id, (error_msg, corrupted_files) in models_needing_download.items():
if not download_model(model_id): if not download_model(model_id, cache_dir, corrupted_files):
download_failed = True download_failed = True
print("-" * 70) print("-" * 70)
@@ -436,11 +483,39 @@ def main() -> int:
print("✗ FAILED: Some models could not be downloaded") print("✗ FAILED: Some models could not be downloaded")
return 1 return 1
# All downloads succeeded, but we still exit with error to flag cache issues # All downloads succeeded - now validate them again
print("✗ FAILED: Models were downloaded due to cache corruption/missing files") print("✓ All models downloaded successfully!")
print("This indicates the cache was invalid and needed to be repaired.") print("-" * 70)
print("Failing the job to surface this issue for investigation.") print("Validating downloaded models...")
return 1 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__": if __name__ == "__main__":