#!/usr/bin/env python3 """ Download all files from a HuggingFace model repository with retries, resume support, and integrity verification. Usage: python3 download_hf_model.py [repo_id ...] Examples: python3 download_hf_model.py ai-sage/GigaAM-Multilingual python3 download_hf_model.py meta-llama/Llama-3-8B-Instruct Each repo is downloaded into a directory named by replacing '/' with '.' in the repo_id (e.g. ai-sage.GigaAM-Multilingual). Adapted from download_lilith_q8.py. """ import argparse import hashlib import importlib import os import subprocess import sys import tempfile import time from pathlib import Path MAX_RETRIES = 10 RETRY_DELAY_BASE = 15 # seconds, doubles each retry BASE_DIR = Path(__file__).resolve().parent RUNTIME_DIR = BASE_DIR / ".hf_download_runtime" HF_HOME_DIR = RUNTIME_DIR / "hf-home" HF_HUB_CACHE_DIR = HF_HOME_DIR / "hub" HF_ASSETS_CACHE_DIR = HF_HOME_DIR / "assets" XDG_CACHE_HOME_DIR = RUNTIME_DIR / "xdg-cache" PIP_CACHE_DIR = RUNTIME_DIR / "pip-cache" PYTHON_VENDOR_DIR = RUNTIME_DIR / "python-vendor" TMP_DIR = RUNTIME_DIR / "tmp" def prepare_runtime_dirs() -> None: for path in ( RUNTIME_DIR, HF_HOME_DIR, HF_HUB_CACHE_DIR, HF_ASSETS_CACHE_DIR, XDG_CACHE_HOME_DIR, PIP_CACHE_DIR, PYTHON_VENDOR_DIR, TMP_DIR, ): path.mkdir(parents=True, exist_ok=True) os.environ["HF_HOME"] = str(HF_HOME_DIR) os.environ["HF_HUB_CACHE"] = str(HF_HUB_CACHE_DIR) os.environ["HF_ASSETS_CACHE"] = str(HF_ASSETS_CACHE_DIR) os.environ["XDG_CACHE_HOME"] = str(XDG_CACHE_HOME_DIR) os.environ["PIP_CACHE_DIR"] = str(PIP_CACHE_DIR) os.environ["HF_HUB_DISABLE_XET"] = "1" os.environ["HF_XET_RECONSTRUCT_WRITE_SEQUENTIALLY"] = "1" os.environ["TMPDIR"] = str(TMP_DIR) os.environ["TMP"] = str(TMP_DIR) os.environ["TEMP"] = str(TMP_DIR) tempfile.tempdir = str(TMP_DIR) if str(PYTHON_VENDOR_DIR) not in sys.path: sys.path.insert(0, str(PYTHON_VENDOR_DIR)) prepare_runtime_dirs() def parse_major_minor(version: str) -> tuple[int, int]: parts = [] for chunk in version.split("."): digits = "".join(ch for ch in chunk if ch.isdigit()) if digits: parts.append(int(digits)) if len(parts) == 2: break while len(parts) < 2: parts.append(0) return parts[0], parts[1] def install_huggingface_hub() -> None: print("Installing newer huggingface_hub into the local runtime dir...") subprocess.check_call( [ sys.executable, "-m", "pip", "install", "--upgrade", "--target", str(PYTHON_VENDOR_DIR), "--cache-dir", str(PIP_CACHE_DIR), "huggingface_hub>=1.0,<2", ] ) try: import huggingface_hub as _huggingface_hub if parse_major_minor(_huggingface_hub.__version__) < (1, 0): raise ImportError(f"Found old huggingface_hub {_huggingface_hub.__version__}") from huggingface_hub import HfApi, hf_hub_url from huggingface_hub.utils import RepositoryNotFoundError except Exception: install_huggingface_hub() importlib.invalidate_caches() for name in list(sys.modules): if name == "huggingface_hub" or name.startswith("huggingface_hub."): del sys.modules[name] from huggingface_hub import HfApi, hf_hub_url from huggingface_hub.utils import RepositoryNotFoundError def load_token() -> str | None: token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN") if token: return token token_files = [ HF_HOME_DIR / "token", Path.home() / ".cache" / "huggingface" / "token", ] for token_file in token_files: if token_file.exists(): return token_file.read_text().strip() return None TOKEN = load_token() if not TOKEN: print("HF token not found. Proceeding without authentication (repo may be public).") else: print(f"Using HF token: {TOKEN[:8]}...") def repo_to_dirname(repo_id: str) -> str: return repo_id.replace("/", ".") def get_dir_size(path: Path) -> int: return sum(f.stat().st_size for f in path.rglob("*") if f.is_file()) def expected_size_for(sibling) -> int | None: size = getattr(sibling, "size", None) if size is not None: return size if sibling.lfs and sibling.lfs.get("size") is not None: return sibling.lfs["size"] return None def format_size(n: int | None) -> str: if n is None: return "unknown size" if n >= 1e9: return f"{n / 1e9:.2f} GB" if n >= 1e6: return f"{n / 1e6:.2f} MB" return f"{n / 1e3:.2f} KB" def quarantine_corrupt_files(paths: list[Path], download_dir: Path) -> None: if not paths: return quarantine_dir = download_dir / ".corrupt" quarantine_dir.mkdir(exist_ok=True) stamp = int(time.time()) for path in paths: target = quarantine_dir / f"{path.name}.{stamp}.corrupt" suffix = 1 while target.exists(): target = quarantine_dir / f"{path.name}.{stamp}.{suffix}.corrupt" suffix += 1 path.replace(target) print(f"Moved corrupt file aside: {target}") def download_file(rel_path: str, dest: Path, repo_id: str) -> None: url = hf_hub_url(repo_id=repo_id, filename=rel_path) cmd = [ "curl", "-L", "--fail", "--show-error", "--connect-timeout", "30", "--progress-bar", "-C", "-", "-o", str(dest), url, ] if TOKEN: cmd[1:1] = ["-H", f"Authorization: Bearer {TOKEN}"] subprocess.check_call(cmd) def download_repo_with_retries(repo_id: str, download_dir: Path) -> Path: """Download all files from a repo with curl, resume, and exponential backoff.""" for attempt in range(1, MAX_RETRIES + 1): try: print(f"\n{'=' * 60}") print(f"Download attempt {attempt}/{MAX_RETRIES}") print(f"{'=' * 60}") api = HfApi(token=TOKEN) repo_info = api.repo_info(repo_id=repo_id, token=TOKEN, files_metadata=True) siblings = [s for s in (repo_info.siblings or []) if s.rfilename] if attempt == 1: # Disk space check needed = 0 for s in siblings: exp = expected_size_for(s) dest = download_dir / s.rfilename have = dest.stat().st_size if dest.exists() else 0 if exp is not None: needed += max(exp - have, 0) try: stat = os.statvfs(download_dir) free = stat.f_bavail * stat.f_frsize print(f"Disk check: need ~{format_size(needed)} more, free ~{format_size(free)} on destination.") if needed > free: print("WARNING: not enough free space for the remaining download.") except OSError: pass siblings = sorted( siblings, key=lambda s: expected_size_for(s) or 0, reverse=True, ) size_before = get_dir_size(download_dir) if download_dir.exists() else 0 t0 = time.time() for index, sibling in enumerate(siblings, start=1): rel_path = sibling.rfilename dest = download_dir / rel_path dest.parent.mkdir(parents=True, exist_ok=True) expected_size = expected_size_for(sibling) current_size = dest.stat().st_size if dest.exists() else 0 if expected_size is not None and dest.exists() and current_size == expected_size: print( f"[{index}/{len(siblings)}] Already present: {rel_path} " f"({format_size(expected_size)})" ) continue if expected_size is not None and dest.exists() and current_size > expected_size: quarantine_corrupt_files([dest], download_dir) current_size = 0 if current_size > 0: print( f"[{index}/{len(siblings)}] Resuming: {rel_path} " f"from {format_size(current_size)} / {format_size(expected_size)}" ) else: print(f"[{index}/{len(siblings)}] Downloading: {rel_path} ({format_size(expected_size)})") download_file(rel_path, dest, repo_id) if expected_size is not None: actual_size = dest.stat().st_size if actual_size != expected_size: raise ValueError( f"Size mismatch after download for {rel_path}: " f"expected {expected_size}, got {actual_size}" ) elapsed = time.time() - t0 size_after = get_dir_size(download_dir) downloaded = max(size_after - size_before, 0) speed = downloaded / elapsed if elapsed > 0 else 0 print(f"\nDownload complete: {download_dir}") print(f" Downloaded: {format_size(downloaded)} in {elapsed:.0f}s") print(f" Avg speed: {speed / 1e6:.1f} MB/s") return download_dir except KeyboardInterrupt: print("\nInterrupted by user.") sys.exit(1) except (RepositoryNotFoundError, FileNotFoundError) as exc: print(f"\nFatal error: {exc}") sys.exit(1) except Exception as exc: delay = RETRY_DELAY_BASE * (2 ** (attempt - 1)) print(f"\nError on attempt {attempt}: {exc}") if attempt < MAX_RETRIES: print(f"Retrying in {delay}s...") time.sleep(delay) else: print("All retries exhausted.") sys.exit(1) def verify_integrity(repo_id: str, download_dir: Path) -> tuple[bool, list[Path]]: """Verify all downloaded files against repo metadata.""" print(f"\n{'=' * 60}") print("Verifying file integrity...") print(f"{'=' * 60}") api = HfApi(token=TOKEN) try: repo_info = api.repo_info(repo_id=repo_id, token=TOKEN, files_metadata=True) except Exception as exc: print(f"Could not fetch repo metadata for verification: {exc}") return False, [] siblings = [s for s in (repo_info.siblings or []) if s.rfilename] errors = [] checked = 0 corrupt_files: list[Path] = [] for sibling in siblings: if not sibling.rfilename: continue file_path = download_dir / sibling.rfilename if not file_path.exists(): errors.append(f"MISSING: {sibling.rfilename}") continue expected_sha = None hash_type = None if sibling.lfs and sibling.lfs.get("sha256"): expected_sha = sibling.lfs["sha256"] hash_type = "sha256" elif hasattr(sibling, "blob_id") and sibling.blob_id: expected_sha = sibling.blob_id hash_type = "git-sha1" else: continue checked += 1 size = file_path.stat().st_size print(f" Checking {sibling.rfilename} ({format_size(size)})...", end=" ", flush=True) if hash_type == "sha256": digest = hashlib.sha256() with open(file_path, "rb") as handle: while chunk := handle.read(8 * 1024 * 1024): digest.update(chunk) actual = digest.hexdigest() else: digest = hashlib.sha1() digest.update(f"blob {size}\0".encode()) with open(file_path, "rb") as handle: while chunk := handle.read(8 * 1024 * 1024): digest.update(chunk) actual = digest.hexdigest() if actual == expected_sha: print("OK") else: print("MISMATCH") corrupt_files.append(file_path) errors.append( f"HASH MISMATCH: {sibling.rfilename}\n" f" expected: {expected_sha}\n" f" actual: {actual}" ) print(f"\nChecked {checked} file(s).") if errors: print("\nIntegrity errors:") for err in errors: print(f" - {err}") return False, corrupt_files print("All files verified successfully.") return True, [] def main() -> None: parser = argparse.ArgumentParser( description="Download files from HuggingFace model repositories with retry and verification." ) parser.add_argument( "repos", nargs="+", help="HuggingFace repo IDs to download (e.g. ai-sage/GigaAM-Multilingual)", ) args = parser.parse_args() for repo_id in args.repos: dirname = repo_to_dirname(repo_id) download_dir = BASE_DIR / dirname download_dir.mkdir(parents=True, exist_ok=True) print(f"\n{'#' * 60}") print(f"# Repo: {repo_id}") print(f"# Target: {download_dir}") print(f"{'#' * 60}") download_repo_with_retries(repo_id, download_dir) ok, corrupt_files = verify_integrity(repo_id, download_dir) if not ok: quarantine_corrupt_files(corrupt_files, download_dir) print(f"\nIntegrity check FAILED for {repo_id}. Re-run the script to resume.") sys.exit(1) print(f"\nDone. Model saved to: {download_dir}") if __name__ == "__main__": main()