aleph65 commited on
Commit
be1c0b3
·
verified ·
1 Parent(s): 4f50176

download_missing_models: fix mid-download deadlock (disable HF_XET_HIGH_PERFORMANCE/hf_transfer, add stall watchdog + resumable curl fallback)

Browse files
Files changed (1) hide show
  1. 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
- export HF_HUB_ENABLE_HF_TRANSFER=1
 
 
 
 
 
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 (huggingface_hub >= 1.x ships fast Xet downloads built in and no
51
- # longer provides the hf-transfer extra, so hf_transfer is best-effort only)
52
  if ! python3 -c "import huggingface_hub" >/dev/null 2>&1; then
53
- echo "Installing huggingface_hub[hf_transfer]..."
54
- pip install -q -U "huggingface_hub[hf_transfer]" || pip install -q -U huggingface_hub
55
  fi
56
- python3 -c "import hf_transfer" >/dev/null 2>&1 || pip install -q hf_transfer >/dev/null 2>&1 || true
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 = 0
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
- # hf_transfer parallelizes chunks within a file; snapshot_download parallelizes across files.
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
- snapshot_download(
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