mirror of
https://github.com/kvcache-ai/ktransformers.git
synced 2026-10-19 15:22:49 +08:00
315 lines
10 KiB
Python
Executable File
315 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Standalone Model Integrity Verifier
|
|
|
|
Verifies safetensors files against HuggingFace repository SHA256 checksums.
|
|
|
|
Usage:
|
|
python standalone_verify.py <local_model_path> <hf_repo_id>
|
|
|
|
Environment Variables:
|
|
HF_TOKEN: HuggingFace API token (optional, for private repos)
|
|
HF_ENDPOINT: Custom HuggingFace endpoint (optional, e.g., https://hf-mirror.com)
|
|
|
|
Examples:
|
|
python standalone_verify.py /path/to/model deepseek-ai/DeepSeek-V3
|
|
HF_TOKEN=hf_xxx python standalone_verify.py /path/to/model org/private-model
|
|
"""
|
|
|
|
import hashlib
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Dict, Tuple
|
|
from concurrent.futures import ProcessPoolExecutor, as_completed
|
|
|
|
|
|
def _compute_file_sha256(file_path: Path) -> Tuple[str, str, float]:
|
|
"""
|
|
Compute SHA256 for a single file (worker function for multiprocessing).
|
|
|
|
Args:
|
|
file_path: Path to the file
|
|
|
|
Returns:
|
|
Tuple of (filename, sha256_hash, file_size_mb)
|
|
"""
|
|
sha256_hash = hashlib.sha256()
|
|
file_size_mb = file_path.stat().st_size / (1024 * 1024)
|
|
|
|
# Read file in chunks to handle large files
|
|
with open(file_path, "rb") as f:
|
|
for byte_block in iter(lambda: f.read(8192 * 1024), b""): # 8MB chunks
|
|
sha256_hash.update(byte_block)
|
|
|
|
return file_path.name, sha256_hash.hexdigest(), file_size_mb
|
|
|
|
|
|
def calculate_local_sha256(local_dir: Path, file_pattern: str = "*.safetensors") -> Dict[str, str]:
|
|
"""
|
|
Calculate SHA256 hashes for files in a directory using parallel processing.
|
|
|
|
Args:
|
|
local_dir: Directory to scan
|
|
file_pattern: Glob pattern for files to hash
|
|
|
|
Returns:
|
|
Dictionary mapping filename to SHA256 hash
|
|
"""
|
|
result = {}
|
|
|
|
if not local_dir.exists():
|
|
print(f"Error: Directory does not exist: {local_dir}")
|
|
return result
|
|
|
|
# Get all files
|
|
files_to_hash = [f for f in local_dir.glob(file_pattern) if f.is_file()]
|
|
total_files = len(files_to_hash)
|
|
|
|
if total_files == 0:
|
|
print(f"Warning: No {file_pattern} files found in {local_dir}")
|
|
return result
|
|
|
|
# Use min(16, total_files) workers to avoid over-spawning processes
|
|
max_workers = min(16, total_files)
|
|
print(f"Using {max_workers} parallel workers for SHA256 calculation")
|
|
|
|
# Use ProcessPoolExecutor for CPU-intensive SHA256 computation
|
|
completed_count = 0
|
|
with ProcessPoolExecutor(max_workers=max_workers) as executor:
|
|
# Submit all tasks
|
|
future_to_file = {executor.submit(_compute_file_sha256, file_path): file_path for file_path in files_to_hash}
|
|
|
|
# Process results as they complete
|
|
for future in as_completed(future_to_file):
|
|
completed_count += 1
|
|
try:
|
|
filename, sha256_hash, file_size_mb = future.result()
|
|
result[filename] = sha256_hash
|
|
print(f" [{completed_count}/{total_files}] ✓ {filename} ({file_size_mb:.1f} MB)")
|
|
except Exception as e:
|
|
file_path = future_to_file[future]
|
|
print(f" [{completed_count}/{total_files}] ✗ {file_path.name} - Error: {str(e)}")
|
|
|
|
return result
|
|
|
|
|
|
def fetch_huggingface_sha256(repo_id: str, revision: str = "main") -> Dict[str, str]:
|
|
"""
|
|
Fetch SHA256 hashes from HuggingFace repository.
|
|
|
|
Args:
|
|
repo_id: Repository ID (e.g., "deepseek-ai/DeepSeek-V3")
|
|
revision: Branch/revision (default: "main")
|
|
|
|
Returns:
|
|
Dictionary mapping filename to SHA256 hash
|
|
"""
|
|
try:
|
|
from huggingface_hub import HfApi, list_repo_files
|
|
except ImportError:
|
|
print("Error: huggingface_hub is not installed")
|
|
print("Install it with: pip install huggingface-hub")
|
|
sys.exit(1)
|
|
|
|
# Read HF_TOKEN from environment if available
|
|
token = os.environ.get("HF_TOKEN")
|
|
if token:
|
|
print(f"Using HF_TOKEN from environment")
|
|
|
|
try:
|
|
api = HfApi(token=token)
|
|
|
|
print(f"Fetching file list from {repo_id} (revision: {revision})...")
|
|
all_files = list_repo_files(repo_id=repo_id, revision=revision, token=token)
|
|
safetensors_files = [f for f in all_files if f.endswith(".safetensors")]
|
|
|
|
if not safetensors_files:
|
|
print(f"Warning: No .safetensors files found in repository")
|
|
return {}
|
|
|
|
print(f"Found {len(safetensors_files)} safetensors files")
|
|
print(f"Fetching SHA256 hashes from {repo_id}...")
|
|
|
|
paths_info = api.get_paths_info(
|
|
repo_id=repo_id,
|
|
paths=safetensors_files,
|
|
revision=revision,
|
|
token=token,
|
|
)
|
|
|
|
result = {}
|
|
for file_info in paths_info:
|
|
if hasattr(file_info, "lfs") and file_info.lfs is not None:
|
|
sha256 = file_info.lfs.sha256
|
|
else:
|
|
sha256 = getattr(file_info, "blob_id", None)
|
|
result[file_info.path] = sha256
|
|
|
|
print(f"✓ Fetched {len(result)} file hashes from remote")
|
|
return result
|
|
|
|
except Exception as e:
|
|
print(f"Error fetching from HuggingFace: {e}")
|
|
|
|
# Suggest mirror if it's a connection error
|
|
if "connection" in str(e).lower() or "timeout" in str(e).lower():
|
|
print("\nTry using HuggingFace mirror:")
|
|
print(" export HF_ENDPOINT=https://hf-mirror.com")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
def verify_model(local_dir: Path, repo_id: str, revision: str = "main", verbose: bool = False) -> bool:
|
|
"""
|
|
Verify local model integrity against HuggingFace repository.
|
|
|
|
Args:
|
|
local_dir: Local directory containing model files
|
|
repo_id: HuggingFace repository ID
|
|
revision: Branch/revision to verify against
|
|
verbose: Show detailed comparison for each file
|
|
|
|
Returns:
|
|
True if verification passed, False otherwise
|
|
"""
|
|
print("\n" + "=" * 60)
|
|
print(f"Model Integrity Verification")
|
|
print("=" * 60)
|
|
print(f"Local path: {local_dir}")
|
|
print(f"Repository: {repo_id}")
|
|
print(f"Revision: {revision}")
|
|
print("=" * 60 + "\n")
|
|
|
|
# 1. Fetch official SHA256 hashes from HuggingFace
|
|
official_hashes = fetch_huggingface_sha256(repo_id, revision)
|
|
|
|
if not official_hashes:
|
|
print("\nError: No hashes fetched from remote repository")
|
|
return False
|
|
|
|
print()
|
|
|
|
# 2. Calculate local SHA256 hashes
|
|
print("Calculating SHA256 for local files...")
|
|
local_hashes = calculate_local_sha256(local_dir, "*.safetensors")
|
|
|
|
if not local_hashes:
|
|
print("\nError: No local hashes calculated")
|
|
return False
|
|
|
|
print(f"✓ Calculated {len(local_hashes)} local file hashes\n")
|
|
|
|
# 3. Compare hashes
|
|
print(f"Comparing {len(official_hashes)} files...\n")
|
|
|
|
files_passed = 0
|
|
files_failed = []
|
|
files_missing = []
|
|
|
|
for idx, (filename, official_hash) in enumerate(official_hashes.items(), 1):
|
|
file_basename = Path(filename).name
|
|
|
|
# Find matching local file
|
|
local_hash = None
|
|
for local_file, local_hash_value in local_hashes.items():
|
|
if Path(local_file).name == file_basename:
|
|
local_hash = local_hash_value
|
|
break
|
|
|
|
if local_hash is None:
|
|
files_missing.append(filename)
|
|
print(f" [{idx}/{len(official_hashes)}] ✗ {file_basename} - MISSING")
|
|
if verbose:
|
|
print(f" Remote: {official_hash}")
|
|
print(f" Local: <missing>")
|
|
elif local_hash.lower() != official_hash.lower():
|
|
files_failed.append(filename)
|
|
print(f" [{idx}/{len(official_hashes)}] ✗ {file_basename} - HASH MISMATCH")
|
|
if verbose:
|
|
print(f" Remote: {official_hash}")
|
|
print(f" Local: {local_hash}")
|
|
else:
|
|
files_passed += 1
|
|
print(f" [{idx}/{len(official_hashes)}] ✓ {file_basename}")
|
|
if verbose:
|
|
print(f" SHA256: {official_hash}")
|
|
|
|
# 4. Print summary
|
|
print("\n" + "=" * 60)
|
|
print("Verification Summary")
|
|
print("=" * 60)
|
|
print(f"Total files: {len(official_hashes)}")
|
|
print(f"✓ Passed: {files_passed}")
|
|
print(f"✗ Failed: {len(files_failed)}")
|
|
print(f"✗ Missing: {len(files_missing)}")
|
|
print("=" * 60)
|
|
|
|
if files_failed:
|
|
print("\nFiles with hash mismatch:")
|
|
for f in files_failed:
|
|
print(f" - {f}")
|
|
|
|
if files_missing:
|
|
print("\nMissing files:")
|
|
for f in files_missing:
|
|
print(f" - {f}")
|
|
|
|
print()
|
|
|
|
# Return result
|
|
if files_failed or files_missing:
|
|
print("❌ VERIFICATION FAILED")
|
|
print("\nSome files are corrupted or missing. Consider re-downloading the model.")
|
|
return False
|
|
else:
|
|
print("✅ VERIFICATION PASSED")
|
|
print("\nAll files verified successfully!")
|
|
return True
|
|
|
|
|
|
def main():
|
|
"""Main entry point."""
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(
|
|
description="Verify model integrity against HuggingFace repository",
|
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
epilog="""
|
|
Examples:
|
|
python standalone_verify.py /path/to/model deepseek-ai/DeepSeek-V3
|
|
python standalone_verify.py /path/to/model org/model --revision main --verbose
|
|
|
|
Environment Variables:
|
|
HF_TOKEN HuggingFace API token (for private repos)
|
|
HF_ENDPOINT Custom HuggingFace endpoint (e.g., https://hf-mirror.com)
|
|
""",
|
|
)
|
|
|
|
parser.add_argument("local_path", type=str, help="Local model directory path")
|
|
parser.add_argument("repo_id", type=str, help="HuggingFace repository ID (e.g., deepseek-ai/DeepSeek-V3)")
|
|
parser.add_argument("--revision", "-r", type=str, default="main", help="Repository revision/branch (default: main)")
|
|
parser.add_argument("--verbose", "-v", action="store_true", help="Show detailed SHA256 comparison")
|
|
|
|
args = parser.parse_args()
|
|
|
|
# Validate local path
|
|
local_path = Path(args.local_path).expanduser().resolve()
|
|
if not local_path.exists():
|
|
print(f"Error: Local path does not exist: {local_path}")
|
|
sys.exit(1)
|
|
|
|
if not local_path.is_dir():
|
|
print(f"Error: Local path is not a directory: {local_path}")
|
|
sys.exit(1)
|
|
|
|
# Run verification
|
|
success = verify_model(local_path, args.repo_id, args.revision, args.verbose)
|
|
|
|
# Exit with appropriate code
|
|
sys.exit(0 if success else 1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|