download_missing_models: fix mid-download deadlock (disable HF_XET_HIGH_PERFORMANCE/hf_transfer, add stall watchdog + resumable curl fallback)
Browse files- download_missing_models.sh +101 -28
download_missing_models.sh
CHANGED
|
@@ -30,7 +30,12 @@
|
|
| 30 |
set -euo pipefail
|
| 31 |
|
| 32 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 34 |
# never let git sit on an interactive credential/hostkey prompt
|
| 35 |
export GIT_TERMINAL_PROMPT=0
|
| 36 |
|
|
@@ -47,13 +52,13 @@ else
|
|
| 47 |
fi
|
| 48 |
export HF_TOKEN
|
| 49 |
|
| 50 |
-
# 2) Ensure deps
|
| 51 |
-
#
|
| 52 |
if ! python3 -c "import huggingface_hub" >/dev/null 2>&1; then
|
| 53 |
-
echo "Installing huggingface_hub
|
| 54 |
-
pip install -q -U
|
| 55 |
fi
|
| 56 |
-
python3 -c "import
|
| 57 |
|
| 58 |
# 3) Run the downloader.
|
| 59 |
# Load the Python code into a variable (NOT via stdin, which must stay attached
|
|
@@ -502,29 +507,102 @@ def cleanup_debris():
|
|
| 502 |
if count:
|
| 503 |
print(f"{C_YELLOW}Cleaned up {count} leftover partial-download file(s) ({human(freed)} freed).{C_RESET}")
|
| 504 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 505 |
def download_heartbeat(to_download):
|
| 506 |
"""Print bytes-on-disk every 20s so a big download never looks hung."""
|
| 507 |
total = sum(s for _, _, s, _ in to_download)
|
| 508 |
stop = threading.Event()
|
| 509 |
-
cache = os.path.join(COMFY_DIR, ".cache", "huggingface", "download")
|
| 510 |
def run():
|
| 511 |
while not stop.wait(20):
|
| 512 |
-
done =
|
| 513 |
-
for _, _, size, dest in to_download:
|
| 514 |
-
if os.path.exists(dest):
|
| 515 |
-
done += min(os.path.getsize(dest), size)
|
| 516 |
-
for root, _, names in os.walk(cache):
|
| 517 |
-
for n in names:
|
| 518 |
-
if n.endswith(".incomplete"):
|
| 519 |
-
try:
|
| 520 |
-
done += os.path.getsize(os.path.join(root, n))
|
| 521 |
-
except OSError:
|
| 522 |
-
pass
|
| 523 |
print(f" ... still downloading: ~{human(min(done, total))} of {human(total)} on disk", flush=True)
|
| 524 |
t = threading.Thread(target=run, daemon=True)
|
| 525 |
t.start()
|
| 526 |
return stop
|
| 527 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 528 |
def main():
|
| 529 |
argv = sys.argv[1:]
|
| 530 |
dry_run = "--dry-run" in argv
|
|
@@ -656,21 +734,16 @@ def main():
|
|
| 656 |
|
| 657 |
# -------- download models --------
|
| 658 |
if to_download:
|
| 659 |
-
|
| 660 |
-
print(f"\n{C_CYAN}Downloading with hf_transfer (parallel)...{C_RESET}\n")
|
| 661 |
-
from huggingface_hub import snapshot_download
|
| 662 |
patterns = [m for _, m, _, _ in to_download]
|
| 663 |
heartbeat = download_heartbeat(to_download)
|
| 664 |
try:
|
| 665 |
-
|
| 666 |
-
repo_id=REPO_ID,
|
| 667 |
-
allow_patterns=patterns,
|
| 668 |
-
local_dir=COMFY_DIR,
|
| 669 |
-
token=os.environ.get("HF_TOKEN"),
|
| 670 |
-
max_workers=8,
|
| 671 |
-
)
|
| 672 |
finally:
|
| 673 |
heartbeat.set()
|
|
|
|
|
|
|
|
|
|
| 674 |
|
| 675 |
print(f"\n{C_BOLD}Verifying:{C_RESET}")
|
| 676 |
ok = True
|
|
|
|
| 30 |
set -euo pipefail
|
| 31 |
|
| 32 |
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
| 33 |
+
# The pod image exports HF_HUB_ENABLE_HF_TRANSFER=1 and HF_XET_HIGH_PERFORMANCE=1.
|
| 34 |
+
# hf_transfer is deprecated in huggingface_hub 1.x, and the Xet high-performance
|
| 35 |
+
# mode has deadlocked mid-download on this pod (~115 parallel connections, all
|
| 36 |
+
# data buffered in RAM, zero bytes written, no error). Stock Xet concurrency is
|
| 37 |
+
# still several hundred MB/s here, which is what the network volume tops out at.
|
| 38 |
+
unset HF_HUB_ENABLE_HF_TRANSFER HF_XET_HIGH_PERFORMANCE
|
| 39 |
# never let git sit on an interactive credential/hostkey prompt
|
| 40 |
export GIT_TERMINAL_PROMPT=0
|
| 41 |
|
|
|
|
| 52 |
fi
|
| 53 |
export HF_TOKEN
|
| 54 |
|
| 55 |
+
# 2) Ensure deps. huggingface_hub 1.x downloads Xet-backed repos with hf_xet;
|
| 56 |
+
# without it the hub falls back to slower, less robust code paths.
|
| 57 |
if ! python3 -c "import huggingface_hub" >/dev/null 2>&1; then
|
| 58 |
+
echo "Installing huggingface_hub..."
|
| 59 |
+
pip install -q -U huggingface_hub
|
| 60 |
fi
|
| 61 |
+
python3 -c "import hf_xet" >/dev/null 2>&1 || pip install -q hf_xet >/dev/null 2>&1 || true
|
| 62 |
|
| 63 |
# 3) Run the downloader.
|
| 64 |
# Load the Python code into a variable (NOT via stdin, which must stay attached
|
|
|
|
| 507 |
if count:
|
| 508 |
print(f"{C_YELLOW}Cleaned up {count} leftover partial-download file(s) ({human(freed)} freed).{C_RESET}")
|
| 509 |
|
| 510 |
+
def bytes_on_disk(to_download):
|
| 511 |
+
"""Bytes actually landed for the planned downloads: finished files,
|
| 512 |
+
hub .incomplete fragments, and our curl .part files."""
|
| 513 |
+
done = 0
|
| 514 |
+
cache = os.path.join(COMFY_DIR, ".cache", "huggingface", "download")
|
| 515 |
+
for _, _, size, dest in to_download:
|
| 516 |
+
if os.path.exists(dest):
|
| 517 |
+
done += min(os.path.getsize(dest), size)
|
| 518 |
+
if os.path.exists(dest + ".part"):
|
| 519 |
+
done += os.path.getsize(dest + ".part")
|
| 520 |
+
for root, _, names in os.walk(cache):
|
| 521 |
+
for n in names:
|
| 522 |
+
if n.endswith(".incomplete"):
|
| 523 |
+
try:
|
| 524 |
+
done += os.path.getsize(os.path.join(root, n))
|
| 525 |
+
except OSError:
|
| 526 |
+
pass
|
| 527 |
+
return done
|
| 528 |
+
|
| 529 |
def download_heartbeat(to_download):
|
| 530 |
"""Print bytes-on-disk every 20s so a big download never looks hung."""
|
| 531 |
total = sum(s for _, _, s, _ in to_download)
|
| 532 |
stop = threading.Event()
|
|
|
|
| 533 |
def run():
|
| 534 |
while not stop.wait(20):
|
| 535 |
+
done = bytes_on_disk(to_download)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
print(f" ... still downloading: ~{human(min(done, total))} of {human(total)} on disk", flush=True)
|
| 537 |
t = threading.Thread(target=run, daemon=True)
|
| 538 |
t.start()
|
| 539 |
return stop
|
| 540 |
|
| 541 |
+
STALL_SECS = 180
|
| 542 |
+
|
| 543 |
+
def hf_download_with_watchdog(patterns, to_download):
|
| 544 |
+
"""Run snapshot_download in a child process and kill it if bytes-on-disk
|
| 545 |
+
stop growing for STALL_SECS. Both hf_transfer and the Xet backend have
|
| 546 |
+
wedged mid-file on this pod (data buffered in RAM, file frozen, no error
|
| 547 |
+
raised), so a blocking in-process call can hang forever. Returns True if
|
| 548 |
+
the hub download finished."""
|
| 549 |
+
import multiprocessing as mp
|
| 550 |
+
def child():
|
| 551 |
+
from huggingface_hub import snapshot_download
|
| 552 |
+
snapshot_download(
|
| 553 |
+
repo_id=REPO_ID,
|
| 554 |
+
allow_patterns=patterns,
|
| 555 |
+
local_dir=COMFY_DIR,
|
| 556 |
+
token=os.environ.get("HF_TOKEN"),
|
| 557 |
+
max_workers=8,
|
| 558 |
+
)
|
| 559 |
+
p = mp.Process(target=child)
|
| 560 |
+
p.start()
|
| 561 |
+
last, last_t = -1, time.time()
|
| 562 |
+
while p.is_alive():
|
| 563 |
+
p.join(timeout=15)
|
| 564 |
+
if not p.is_alive():
|
| 565 |
+
break
|
| 566 |
+
done = bytes_on_disk(to_download)
|
| 567 |
+
if done > last:
|
| 568 |
+
last, last_t = done, time.time()
|
| 569 |
+
elif time.time() - last_t > STALL_SECS:
|
| 570 |
+
print(f"\n {C_YELLOW}⚠ no bytes written for {STALL_SECS}s — hub download is wedged, killing it{C_RESET}", flush=True)
|
| 571 |
+
p.terminate()
|
| 572 |
+
p.join(10)
|
| 573 |
+
if p.is_alive():
|
| 574 |
+
p.kill()
|
| 575 |
+
p.join()
|
| 576 |
+
return False
|
| 577 |
+
return p.exitcode == 0
|
| 578 |
+
|
| 579 |
+
def curl_fallback(to_download):
|
| 580 |
+
"""Plain resumable HTTP for whatever is still missing. --speed-limit makes
|
| 581 |
+
curl abort any transfer that drops below 1 MB/s for 30s (instead of
|
| 582 |
+
hanging on a dead connection), and -C - resumes from the .part file."""
|
| 583 |
+
token = os.environ.get("HF_TOKEN", "")
|
| 584 |
+
for _, match, size, dest in to_download:
|
| 585 |
+
if os.path.exists(dest) and (size == 0 or os.path.getsize(dest) == size):
|
| 586 |
+
continue
|
| 587 |
+
url = f"https://huggingface.co/{REPO_ID}/resolve/main/{match}"
|
| 588 |
+
part = dest + ".part"
|
| 589 |
+
os.makedirs(os.path.dirname(dest), exist_ok=True)
|
| 590 |
+
print(f" {C_CYAN}fetching {match} with curl (resumable) ...{C_RESET}", flush=True)
|
| 591 |
+
for attempt in range(1, 11):
|
| 592 |
+
r = subprocess.run(
|
| 593 |
+
["curl", "-L", "--fail", "-C", "-",
|
| 594 |
+
"-H", f"Authorization: Bearer {token}",
|
| 595 |
+
"--speed-limit", "1000000", "--speed-time", "30",
|
| 596 |
+
"--progress-bar", "-o", part, url],
|
| 597 |
+
stdin=subprocess.DEVNULL)
|
| 598 |
+
have = os.path.getsize(part) if os.path.exists(part) else 0
|
| 599 |
+
if r.returncode == 0 and (size == 0 or have == size):
|
| 600 |
+
os.replace(part, dest)
|
| 601 |
+
break
|
| 602 |
+
print(f" {C_YELLOW}… attempt {attempt} stopped at {human(have)} of {human(size)} — resuming{C_RESET}", flush=True)
|
| 603 |
+
else:
|
| 604 |
+
print(f" {C_RED}✘ gave up on {match} after 10 attempts{C_RESET}")
|
| 605 |
+
|
| 606 |
def main():
|
| 607 |
argv = sys.argv[1:]
|
| 608 |
dry_run = "--dry-run" in argv
|
|
|
|
| 734 |
|
| 735 |
# -------- download models --------
|
| 736 |
if to_download:
|
| 737 |
+
print(f"\n{C_CYAN}Downloading with the Hugging Face hub (Xet backend, parallel)...{C_RESET}\n")
|
|
|
|
|
|
|
| 738 |
patterns = [m for _, m, _, _ in to_download]
|
| 739 |
heartbeat = download_heartbeat(to_download)
|
| 740 |
try:
|
| 741 |
+
finished = hf_download_with_watchdog(patterns, to_download)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 742 |
finally:
|
| 743 |
heartbeat.set()
|
| 744 |
+
if not finished:
|
| 745 |
+
print(f"\n{C_YELLOW}Hub download did not finish cleanly — switching to plain resumable HTTP.{C_RESET}")
|
| 746 |
+
curl_fallback(to_download)
|
| 747 |
|
| 748 |
print(f"\n{C_BOLD}Verifying:{C_RESET}")
|
| 749 |
ok = True
|