はじめに

Hugging Face 形式のモデルを保管用の cold 側から実運用側の hot 側へ移すとき、単純にディレクトリごと丸ごとコピーすると扱いにくい。特に blobssnapshots では中身の性質が違うので、同じコマンドで一気に流すよりも役割ごとに道具を分けた方が安全だった。

今回メモしたのは、GLM-5-GGUF を例にして、blobsrclone で並列コピーし、snapshotsrefsrsync で symlink のまま持っていく手順だ。ストレージ戦略では、アクティブに使うものと保管寄りのものを分けて置く前提を取っていて、この手順はその cold/hot 分離をモデル配置に落としたものになっている。

背景・動機

Hugging Face のローカルキャッシュや hub 形式のディレクトリは、見た目より構造がある。容量の大半を占めるのは blobs 側の実体ファイルだが、実際に revision を解決するときは snapshotsrefs の参照関係も必要になる。

ここを全部同じ道具で処理すると、転送性能は出ても symlink の扱いが曖昧になったり、逆に構造保持はできても実体ファイルの並列性が足りなかったりする。なので今回は、実体ファイル群と参照構造を分けて考えることにした。

前提

まずベースとなるパスを固定する。

  BASE="models--unsloth--GLM-5-GGUF"
SRC_BASE="/srv/archive/cold/hf/hub/$BASE"
DST_BASE="/srv/archive/hot/hf/hub/$BASE"
mkdir -p "$DST_BASE"
  

この前提で、/srv/archive/cold/hf/hub/ を保管側、/srv/archive/hot/hf/hub/ を利用側として扱う。モデルごとに変わるのは基本的に BASE だけなので、別モデルへ横展開するときも差し替え箇所が明確だ。

rcloneでblobsを並列複製

blobs は symlink ではなく実体ファイルの集合なので、まずここを rclone copy で流す。

  rclone copy "$SRC_BASE/blobs" "$DST_BASE/blobs" \
  --exclude "*.incomplete" \
  --transfers 16 --checkers 32 \
  --local-no-check-updated \
  -P
  

この分け方にした理由は単純で、blobs は容量が大きく、並列転送の恩恵を受けやすいからだ。--transfers 16 --checkers 32 を開始点にしていて、10GbE + SSD なら 32/64 まで攻める余地がある。

もう一つ重要なのが .incomplete の除外で、未完了ファイルを hot 側へ持ち込まないようにしている。転送対象を実体だけに絞るなら、この条件は最初から入れておいた方が後で悩まない。

次に snapshots をコピーする。

  mkdir -p "$DST_BASE/snapshots"
rsync -aH --info=progress2 \
  --exclude="*.incomplete" \
  "$SRC_BASE/snapshots/" "$DST_BASE/snapshots/"
  

ここでは rsync -aH を使って、symlink を辿らず構造ごと維持するのがポイントになる。Hugging Face の hub ディレクトリでは、snapshots 側が blobs を参照する形になっているので、ここを実体化してしまうよりリンク構造のまま複製した方が repository の意味を壊しにくい。

--info=progress2 を付けているので、長い転送でも進捗を追いやすい。blobs 側と同じく .incomplete を除外しているのも、未完了状態の混入を避けるためだ。

rsyncでrefsも複製

refs はサイズ的には軽いが、参照解決のために一緒に持っていく。

  mkdir -p "$DST_BASE/refs"
rsync -aH --info=progress2 \
  "$SRC_BASE/refs/" "$DST_BASE/refs/"
  

ここを省くと、ファイル本体と revision ディレクトリが揃っていても、参照名ベースの解決で齟齬が出る可能性がある。軽いからこそ省略せず、整合性の一部としてコピーしておく方がわかりやすい。

最低限の整合チェック

転送後は最低限の確認を入れる。

  find "$DST_BASE/snapshots" -type l ! -exec test -e {} \; -print | head
  

何も出なければ OK、というルールにしている。これは厳密な全件検証ではないが、snapshots 内の symlink 切れがないかを最短で確認するには十分に実用的だった。

自分の中では、この一行があるだけで「コピーしただけで終わり」にならず、構造の成立まで見た手順になる。blobssnapshots を分けている以上、参照先の欠落は最初に見つけたい。

特定revisionの部分コピー

全部ではなく、特定 revision の一部量子化だけを持ってきたいケースもある。その場合は snapshots 配下を絞る。

  REV="acc91597d28b7ebd3a8c20fd5331ceaf07a4ece1"
mkdir -p "$DST_BASE/snapshots/$REV"
rsync -aH --info=progress2 \
  "$SRC_BASE/snapshots/$REV/IQ4_NL/" "$DST_BASE/snapshots/$REV/IQ4_NL/"
  

この形なら、たとえば IQ4_NL だけを切り出して試したいときに、全体を hot 側へ展開せずに済む。モデルや revision が大きいときほど、この絞り込みは効く。

ただし、部分コピーを使うなら「どの revision と量子化を hot 側へ置くか」を運用として固定した方がよい。配置先ごとの役割分担を先に決める考え方が必要になる。

別モデルへ横展開するとき

DeepSeek-V3.2-Speciale 側へ流用したい場合は BASE= だけ変えれば同じ型で動く。

つまり手順の本質はモデル名ではなく、次の分担にある。

  • 実体ファイルの blobsrclone
  • symlink と参照構造を持つ snapshots / refsrsync

この分け方を守れば、hub ディレクトリ構造を崩さずに cold/hot 間で転送しやすい。

結果

今回の手順で整理できたのは次の点だ。

  • blobs は並列転送しやすいので rclone copy
  • snapshotsrefs は構造保持のため rsync -aH
  • .incomplete は除外して未完了データを持ち込まない
  • 転送後は find ... -type l ! -exec test -e {} \; で最低限の整合チェックを行う
  • 必要なら revision や量子化単位で部分コピーできる

この手順にしておくと、速度と構造保持の両方を無理なく両立しやすい。全部を単一コマンドで済ませるより、何をどの道具で扱うかがはっきりする。

今後の改善

次に詰めたいのは、転送後の検証と運用ルールの固定だ。

  • blobs の件数やサイズ差分確認を追加して、転送完了判定をもう一段明確にする
  • モデルサイズごとに --transfers / --checkers の目安を残す
  • hot 側へ置く revision と量子化の選び方を別メモで固定する

cold と hot の役割を分けているなら、転送コマンドだけでなく「何を移すか」の判断基準まで一緒に残しておく方が運用しやすい。今回のメモは、そのための最小テンプレートとして使い回せる形になった。