HuggingFaceの巨大ファイルDLが途中で止まる — curl再開で確実に取る
音声変換アプリの開発で、44.1kHz の Seed-VC モデル一式(DiT の重み、rmvpe、BigVGAN のボコーダ)を落とそうとして、こんな壁にぶつかりました。
huggingface_hubでモデルを取得しようとすると、数百MBのファイルの途中で転送が止まったまま、いつまで経っても進まない。
小さい config.json は一瞬で落ちてくるのに、数百MBの .pth や .pt になると、ある地点で進捗が止まって固まる。タイムアウトで落ちてくれるわけでもなく、ただ黙って停滞する——。この記事は、その環境で curl の停滞検知+自動再開を使って確実に取得し、さらに HuggingFace キャッシュと互換の構造を自作して、以後は HF_HUB_OFFLINE=1 でもモデルを読めるようにした記録です。
症状:大きいファイルだけ、静かに止まる
このマシンでは hf_hub_download 系の大ファイル転送が途中で停滞しました。原因として疑わしいのは IPv6 経路の不通や、長時間コネクションのドロップです。厄介なのは、
- 例外で落ちるわけではない(リトライも走らない)
- 進捗バーが止まったまま、実質ハングする
- 小さいファイルでは再現しない
という点。「ネットワークが遅い」のではなく「大きい転送だけが特定の条件で死ぬ」ので、リトライ回数を増やすといった小手先では解決しません。
方針:転送は curl に任せ、配置は HF 互換で自前で作る
huggingface_hub の魅力は、単なるダウンローダーではなく キャッシュ管理にあります。load_custom_model_from_hf や from_pretrained は、決まったディレクトリ構造(blob 実体+シンボリックリンク+ref)を前提に動きます。ここを壊すと、せっかく落としても「キャッシュにない」と判断されて再ダウンロードが走ってしまう。
そこで採った方針が二段構えです。
- 転送だけを curl に肩代わりさせる(停滞検知と自動再開が効く)
- 落とした実体を、HF キャッシュとまったく同じ構造に自前で配置する(以後は hf_hub がそのまま参照できる)
メタデータ(etag・コミットハッシュ・サイズ・実体URL)は huggingface_hub の API から取れるので、それを使ってキャッシュ構造を組み立てます。
curl 側:停滞を検知して自動再開する
肝は curl のオプションの組み合わせです。
subprocess.run(
["curl", "-L", "--fail", "-C", "-",
"--retry", "50", "--retry-delay", "3", "--retry-all-errors",
"--speed-time", "20", "--speed-limit", "2000", # 20秒 2KB/s 未満で切断→再開
"-o", blob, loc],
check=True,
)
それぞれの役割はこうです。
--speed-time 20 --speed-limit 2000— 20秒間、2000バイト/秒を下回ったら転送を失敗扱いにする。これが「静かな停滞」を能動的に切る仕組みです。タイムアウトを待つのではなく、こちらから見切りをつけます。-C -— ダウンロードを途中から再開する(Range リクエスト)。切れた地点から続きを取るので、毎回ゼロからやり直しになりません。--retry 50 --retry-delay 3 --retry-all-errors— 切れたら3秒待って再試行、最大50回。--speed-timeで切った転送も再試行対象になります。-L(リダイレクト追従)と--fail(HTTPエラーで非ゼロ終了)。
この組み合わせで、「停滞 → 切断 → 再開」を自動でぐるぐる回し、最終的に完走させます。大ファイルが途中で固まる環境では、これがいちばん確実でした。
HFキャッシュの内部構造を自作する
ここが本題です。HuggingFace のキャッシュ(~/.cache/huggingface/hub など)は、リポジトリごとに次の構造を持っています。
models--{org}--{repo}/
├── blobs/
│ └── {etag} # ファイルの実体。名前は etag(コンテンツハッシュ)
├── snapshots/
│ └── {commit_hash}/
│ └── {filename} # blob への相対シンボリックリンク
└── refs/
└── main # ブランチ名 → commit_hash を書いたテキスト
ポイントは3つあります。
- 実体は
blobs/に etag 名で置く。同じ内容が複数リビジョンから参照されても実体は1つで済む、という設計です。 snapshots/{commit}/側はシンボリックリンクで、blob を指します。ユーザーコードやライブラリはこちらのパス(人間が読めるファイル名)を触ります。refs/mainに commit hash を書く。これでrevision="main"の解決ができ、HF_HUB_OFFLINE=1でもどのスナップショットを見ればいいか分かります。
メタデータは huggingface_hub から取得できます。
from huggingface_hub import hf_hub_url, get_hf_file_metadata
url = hf_hub_url(repo_id, filename, revision=revision)
m = get_hf_file_metadata(url)
etag = (m.etag or "").strip('"') # blob のファイル名になる
commit = m.commit_hash or revision # snapshots/ のディレクトリ名になる
size = m.size # DL完了判定に使う
loc = m.location or url # 実体の(リダイレクト済み)URL
あとは、curl で blobs/{etag} に実体を落とし、snapshots/{commit}/{filename} から相対パスのシンボリックリンクを張り、refs/main に commit を書けば完成です。相対リンクにしておくのがコツで、キャッシュディレクトリごと別マシンに持っていっても壊れません。
os.symlink(os.path.relpath(blob, os.path.dirname(snap)), snap)
with open(os.path.join(refsdir, "main"), "w") as f:
f.write(commit)
サイズが分かっている場合は、既存 blob のサイズと突き合わせて「完走済みならスキップ」も入れておくと、再実行が冪等になります。
落とし穴と学び
必要なファイルだけ取る
BigVGAN のようなリポジトリには、推論には不要な学習用の巨大ファイル(discriminator や optimizer の状態)が同居していることがあります。リポジトリを丸ごと取りにいくと、いちばん重いものを掴まされます。推論に要るのは生成器の重みと config だけ、というように、必要なファイルを明示的に列挙して取得対象を絞るのが正解でした。
オフラインで動かすなら refs まで作る
snapshots と blobs だけ作って refs/main を忘れると、HF_HUB_OFFLINE=1 のときに「どのコミットを見ればいいか」が解決できず、キャッシュはあるのに読めない、という状態になります。3点セット(blob・snapshot リンク・refs)を必ず揃えます。
HEAD 確認にオンラインが要ることがある
キャッシュを作っても、HF_HUB_OFFLINE=0 のままだと hf_hub は起動時に HEAD で更新チェックをしにいきます(大ファイルの再DLは走りませんが、通信自体は発生します)。完全にオフラインで確実に動かしたいなら HF_HUB_OFFLINE=1/TRANSFORMERS_OFFLINE=1 を明示します。逆に「更新は見たいがDLは止まる」環境では、オンラインのまま HEAD だけ通す、という中間の運用もあり得ます。
まとめ
- 大ファイルの hf_hub 転送が静かに停滞する環境では、curl の
--speed-time/--speed-limitで停滞を能動的に切って-C -で再開、--retryで回し続けるのが確実 - 落とした実体は HFキャッシュ互換の構造(
blobs/{etag}+snapshots/{commit}/の相対シンボリックリンク +refs/main)で配置する - メタデータ(etag・commit・size・location)は
get_hf_file_metadataから取れる - リンクは相対パスで張るとキャッシュを持ち運んでも壊れない
- 推論に不要な巨大ファイル(discriminator/optimizer 等)は取得対象から明示的に除外する
- 完全オフライン運用なら
HF_HUB_OFFLINE=1を効かせるためにrefs/mainまで必ず作る