196 lines
6.4 KiB
Python
196 lines
6.4 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
from huggingface_hub import HfApi, hf_hub_url
|
|
|
|
from common import load_model_card
|
|
|
|
|
|
def selected_files(repo_id: str, allow_files: list[str] | None) -> list[tuple[str, int]]:
|
|
api = HfApi()
|
|
info = api.model_info(repo_id=repo_id, files_metadata=True)
|
|
selected = []
|
|
allowed = set(allow_files or [])
|
|
for sibling in info.siblings:
|
|
name = sibling.rfilename
|
|
if name == ".gitattributes":
|
|
continue
|
|
if allowed and name not in allowed:
|
|
continue
|
|
if sibling.size is None:
|
|
raise ValueError(f"Unknown remote size for {name}")
|
|
selected.append((name, int(sibling.size)))
|
|
return selected
|
|
|
|
|
|
def request_range(url: str, start: int, end: int, token: str | None) -> urllib.request.Request:
|
|
headers = {
|
|
"Range": f"bytes={start}-{end}",
|
|
"User-Agent": "LLM-local-range-downloader/1.0",
|
|
}
|
|
if token:
|
|
headers["Authorization"] = f"Bearer {token}"
|
|
return urllib.request.Request(url, headers=headers, method="GET")
|
|
|
|
|
|
def local_size(path: Path) -> int:
|
|
return path.stat().st_size if path.exists() else 0
|
|
|
|
|
|
def download_file(
|
|
repo_id: str,
|
|
filename: str,
|
|
output: Path,
|
|
expected_size: int,
|
|
*,
|
|
chunk_size: int,
|
|
retries: int,
|
|
retry_delay: float,
|
|
timeout: int,
|
|
token: str | None,
|
|
) -> bool:
|
|
output.parent.mkdir(parents=True, exist_ok=True)
|
|
url = hf_hub_url(repo_id, filename)
|
|
|
|
while True:
|
|
current_size = local_size(output)
|
|
if current_size == expected_size:
|
|
print(f"OK {filename}: {current_size}/{expected_size}")
|
|
return True
|
|
if current_size > expected_size:
|
|
print(
|
|
f"ERROR {filename}: local file is larger than expected: {current_size}/{expected_size}",
|
|
file=sys.stderr,
|
|
)
|
|
return False
|
|
|
|
start = current_size
|
|
end = min(start + chunk_size - 1, expected_size - 1)
|
|
print(f"GET {filename}: bytes {start}-{end} of {expected_size}")
|
|
|
|
recompute_range = False
|
|
for attempt in range(1, retries + 1):
|
|
try:
|
|
request = request_range(url, start, end, token)
|
|
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
status = response.status
|
|
if status == 200:
|
|
raise RuntimeError("server ignored Range request")
|
|
if status not in (200, 206):
|
|
raise RuntimeError(f"unexpected HTTP status {status}")
|
|
|
|
mode = "ab" if start else "wb"
|
|
written = 0
|
|
with output.open(mode + "") as handle:
|
|
while True:
|
|
block = response.read(1024 * 1024)
|
|
if not block:
|
|
break
|
|
handle.write(block)
|
|
written += len(block)
|
|
|
|
if written == 0:
|
|
raise RuntimeError("downloaded zero bytes")
|
|
|
|
new_size = local_size(output)
|
|
if new_size <= current_size:
|
|
raise RuntimeError(f"file did not grow: {new_size}")
|
|
if new_size > expected_size:
|
|
raise RuntimeError(f"file grew past expected size: {new_size}/{expected_size}")
|
|
break
|
|
except (OSError, RuntimeError, urllib.error.URLError, urllib.error.HTTPError) as exc:
|
|
new_size = local_size(output)
|
|
if new_size > current_size:
|
|
print(
|
|
f"WARN {filename}: partial chunk saved, size is now {new_size}/{expected_size}",
|
|
file=sys.stderr,
|
|
)
|
|
recompute_range = True
|
|
break
|
|
print(f"WARN {filename}: attempt {attempt}/{retries} failed: {exc}", file=sys.stderr)
|
|
if attempt == retries:
|
|
return False
|
|
time.sleep(retry_delay)
|
|
if recompute_range:
|
|
continue
|
|
|
|
|
|
def parse_size(value: str) -> int:
|
|
text = value.strip().lower()
|
|
multipliers = {
|
|
"k": 1024,
|
|
"kb": 1024,
|
|
"m": 1024**2,
|
|
"mb": 1024**2,
|
|
"g": 1024**3,
|
|
"gb": 1024**3,
|
|
}
|
|
for suffix, multiplier in multipliers.items():
|
|
if text.endswith(suffix):
|
|
return int(float(text[: -len(suffix)]) * multiplier)
|
|
return int(text)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Download Hugging Face files using explicit HTTP ranges.")
|
|
parser.add_argument("card_id")
|
|
parser.add_argument("--local-dir", required=True)
|
|
parser.add_argument("--allow-file", action="append", dest="allow_files")
|
|
parser.add_argument("--chunk-size", default="64mb")
|
|
parser.add_argument("--retries", type=int, default=20)
|
|
parser.add_argument("--retry-delay", type=float, default=2.0)
|
|
parser.add_argument("--timeout", type=int, default=120)
|
|
args = parser.parse_args()
|
|
|
|
card = load_model_card(args.card_id)
|
|
repo_id = card.get("upstream_id")
|
|
if not repo_id:
|
|
print(f"{args.card_id}: upstream_id is required", file=sys.stderr)
|
|
return 1
|
|
|
|
files = selected_files(repo_id, args.allow_files)
|
|
if not files:
|
|
print("No files selected.", file=sys.stderr)
|
|
return 1
|
|
|
|
token = os.environ.get("HF_TOKEN") or None
|
|
local_dir = Path(args.local_dir)
|
|
chunk_size = parse_size(args.chunk_size)
|
|
failed = []
|
|
|
|
for filename, expected_size in files:
|
|
ok = download_file(
|
|
repo_id,
|
|
filename,
|
|
local_dir / filename,
|
|
expected_size,
|
|
chunk_size=chunk_size,
|
|
retries=args.retries,
|
|
retry_delay=args.retry_delay,
|
|
timeout=args.timeout,
|
|
token=token,
|
|
)
|
|
if not ok:
|
|
current_size = local_size(local_dir / filename)
|
|
failed.append((filename, current_size, expected_size))
|
|
|
|
if failed:
|
|
print("Incomplete downloads:", file=sys.stderr)
|
|
for filename, current_size, expected_size in failed:
|
|
print(f"- {filename}: {current_size}/{expected_size}", file=sys.stderr)
|
|
return 1
|
|
|
|
print(f"Downloaded {len(files)} file(s) for {args.card_id}.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|