SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

Immichの機械学習を別のPCで動かす:VPSのメモリ不足を解消

VPSのImmichでメモリを多く使う機械学習コンテナを、自宅のPCへ移す手順です。ポート3003を公開せずTailscaleでつなぐ理由、バージョンの揃え方、日本語検索に強い多言語CLIPモデルの選び方まで説明します。

Immichの機械学習を別のマシンで動かすとは

Immichの機械学習を別のマシンで動かす手順は2つです。まず、そのマシンで immich-machine-learning コンテナだけを起動します。次に、VPS側のImmichの管理画面にそのURLを登録します。VPSに残るのは写真の保存とWeb画面だけです。メモリを多く使うモデルの読み込みは、自宅のPCが受け持ちます。ポート3003はインターネットに公開しません。TailscaleかWireGuardのトンネルの中だけで通信させることが前提です。

小さなVPSでImmichを動かすと、最初に足りなくなるのはたいていRAMです。原因のほとんどは機械学習コンテナです。このコンテナは、写真の内容で検索するスマート検索と、顔検出のためにモデルをメモリに読み込みます。日本語で検索するために多言語モデルへ変えると、必要なメモリはさらに増えます。VPS全体のメモリの目安はImmichのRAMとストレージの必要量にまとめています。ここでは、VPSのプランを上げる代わりに、手元にあるPCへ重い処理を任せる方法を説明します。

前提として、VPSではすでにImmichが動いているものとします。まだの場合は、先にVPSにImmichを立ててGoogleフォトから移行する手順を済ませてください。機械学習を受け持つ側は、Dockerが動くLinuxのPCを想定しています。

別のマシンに移る処理と、VPSに残る処理

公式ドキュメントによると、リモートの機械学習コンテナを使うのはスマート検索(Smart Search)と顔検出(Face Detection)です。顔認識(Facial Recognition)は移りません。顔認識は、データベースに保存済みの顔検出の結果を使って、人物ごとにまとめる処理です。そのため、Immichのサーバーとデータベースの間で完結します。

スマート検索が機械学習コンテナを呼ぶ場面は2つあります。1つ目は写真を取り込んだときです。プレビュー画像を送り、ベクトル(画像の特徴を表す数値の列)を作ります。2つ目は利用者が検索したときです。検索の文章をベクトルに変換します。つまり、検索するたびに機械学習コンテナへのリクエストが発生します。自宅のPCが電源オフやスリープの間は、ローカルの予備(後で説明します)がなければスマート検索は使えません。

ドキュメントには、機械学習コンテナは受け取ったデータを保存せず、どのユーザーのものかも記録しない、と書かれています。ただし、プレビュー画像そのものは送られます。この点が次の節につながります。

なぜポート3003を公開してはいけないのか

公式ドキュメントは、機械学習コンテナは内部向けのサービスであり、セキュリティ対策がまったくない(no security measures whatsoever)と明記しています。認証もアクセス制限もありません。ポート3003に届く人なら、誰でも推論を実行させることができます。

公開してはいけない理由は2つあります。1つ目は、知らない人があなたのPCのCPUやGPUを自由に使えてしまうことです。2つ目は、VPSからPCへ送られるプレビュー画像が、暗号化されていないHTTPで流れることです。インターネット上の経路のどこかで、家族の写真のプレビューがそのまま読める状態になります。

自宅のIPアドレスだけをファイアウォールで許可する方法も考えられます。しかし、この方法はおすすめしません。通信は暗号化されないままだからです。また、日本の家庭向け光回線ではIPv4 over IPv6(MAP-EやDS-Lite)方式が多く使われています。この方式では、外からの接続を受けるためのポート開放ができない場合がよくあります。

そこで、VPSと自宅PCをTailscaleかWireGuardでつなぎます。どちらも通信を暗号化します。そして、ポート3003はトンネルの中のアドレスにだけ見せます。Tailscaleは両方の端末から外向きに接続するので、自宅側のポート開放が要りません。2つの違いはWireGuardとTailscaleの比較で詳しく説明しています。

注意点がひとつあります。Tailscale Funnelは使わないでください。Funnelは、tailnet(Tailscaleでつながった端末のネットワーク)の中のサービスをインターネットに公開する機能です。ポート3003に使うと、公開するのと同じ結果になります。仕組みはTailscale Funnelの制限と使えるポートを参照してください。

自宅PCとVPSをTailscaleでつなぐ

VPSと自宅PCの両方にTailscaleを入れます。公式のインストールスクリプトを使います。

curl -fsSL https://tailscale.com/install.sh | sh
sudo tailscale up

sudo tailscale up を実行すると、ログイン用のURLが表示されます。ブラウザで開き、2台とも同じアカウントでログインしてください。そのあと、どちらかの端末で次を実行します。

tailscale status

2台のマシン名と、100. で始まるアドレスが並んでいれば接続できています。この記事では、自宅PCのマシン名を gpu-pc とします。自分の環境のマシン名に読み替えてください。

Tailscaleの制御サーバーも自分で持ちたい場合は、Headscaleで自前のTailscale制御サーバーを動かす方法が使えます。すでにWireGuardで自宅とVPSをつないでいる場合は、そのトンネルのアドレスを、この後の手順の 100. のアドレスの代わりに使います。構成はWireGuardでVPSから自宅LANへ経路を通す方法を参照してください。

自宅PCに機械学習コンテナだけを立てる

自宅PCに作業用のディレクトリを作ります。

mkdir -p ~/immich-ml
cd ~/immich-ml

次の内容で docker-compose.yml を作ります。公式ドキュメントのファイルとの違いは、ports の1行だけです。

name: immich_remote_ml

services:
  immich-machine-learning:
    container_name: immich_machine_learning
    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
    volumes:
      - model-cache:/cache
    restart: always
    ports:
      - "${ML_BIND_IP:?ML_BIND_IP is not set}:3003:3003"

volumes:
  model-cache:

公式の例は 3003:3003 と書いています。この書き方では、DockerはPCのすべてのネットワークインターフェースでポートを待ち受けます。さらに、Dockerが公開したポートへの通信は、ufwのルールより先に処理されます。これはDockerの公式ドキュメントにも書かれている動作です。そのため、ufwで3003を閉じても効きません。そこで、待ち受けるアドレスをTailscaleのアドレスだけに絞ります。

${ML_BIND_IP:?...} という書き方には意味があります。変数が空のままだと、docker compose up は起動せずに止まり、エラーに ML_BIND_IP is not set と表示します。設定を忘れたときに、すべてのインターフェースで公開されてしまう事故を防げます。

model-cache ボリュームも大事です。モデルは初めて使うときにダウンロードされ、/cache に保存されます。ボリュームがないと、コンテナを作り直すたびに数GBのモデルを取り直すことになります。

ML_BIND_IP は .env ファイルに書きます。tailscale ip -4 は、そのマシン自身のTailscaleのIPv4アドレスを表示するコマンドです。

echo "ML_BIND_IP=$(tailscale ip -4)" >> .env
cat .env

cat .env の出力に ML_BIND_IP=100. で始まる行があれば、正しく書けています。

VPSとバージョンを揃える

起動する前に、バージョンを揃えます。公式ドキュメントは、2つのホストでバージョンが違うと不具合や不安定な動作の原因になる、と注意しています。VPS側のImmichのディレクトリ(公式手順どおりなら immich-app)で、使っているバージョンを確認します。

grep IMMICH_VERSION ~/immich-app/.env

表示された行を、そのまま自宅PCの ~/immich-ml/.env に追記してください。release のような、新しい版が出るたびに中身が変わるタグには注意が必要です。VPSと自宅PCで docker compose pull を実行した日が違うと、それだけで別のバージョンになるからです。v で始まる固定のバージョン番号を両方の .env に書いておけば、ずれることはありません。

準備ができたら起動します。

docker compose up -d
docker compose ps

docker compose ps の PORTS 列に、100. で始まるアドレスと 3003->3003/tcp が表示されれば成功です。Tailscaleのアドレスだけで待ち受けています。

自宅PCの上で動作を確認します。

curl http://$(tailscale ip -4):3003/ping

pong と返ってくれば、機械学習コンテナは動いています。

念のため、待ち受けアドレスも確認します。

sudo ss -tlnp | grep 3003

待ち受けアドレスが 100. で始まるものだけなら問題ありません。0.0.0.0:3003 や *:3003 があれば、別のプロセスか別のコンテナがすべてのインターフェースで3003を開いています。止めてからやり直してください。

VPSから届くかを確認する

次に、VPSから確認します。tailscale ip -4 にマシン名を渡すと、その相手のアドレスが表示されます。

curl http://$(tailscale ip -4 gpu-pc):3003/ping

ここでも pong が返れば、経路はできています。

応答がなく止まったままになる場合は、パケットが自宅PCに届いていません。自宅PCで tailscale status を実行し、Tailscaleが動いているかを確認してください。すぐに Connection refused が返る場合は、PCには届いていますが、そのアドレスの3003で待ち受けているプロセスがありません。自宅PCで docker compose ps を実行し、コンテナが動いているかと、ML_BIND_IP が今のTailscaleのアドレスと同じかを確認してください。

Immichの管理画面にURLを登録する

VPS側のImmichに管理者でログインし、管理画面の Machine Learning Settings(機械学習の設定)を開きます。URLの欄が1つあり、最初は http://immich-machine-learning:3003 が入っています。これはVPS上の機械学習コンテナを指すURLです。

URLを複数登録すると、Immichは上から順に試します。負荷を分散するわけではありません。先頭が応答しないときだけ、次のURLを使います。そのため、自宅PCに処理させたい場合は、自宅PCのURLを先頭に置く必要があります。手順は次のとおりです。

  1. 最初のURL欄の内容を、自宅PCのURLに書き換えます。形は http://100.x.y.z:3003 です。100.x.y.z には、VPSで tailscale ip -4 gpu-pc を実行して表示されたアドレスを入れます。
  2. Add URL をクリックし、増えた欄に http://immich-machine-learning:3003 を入力します。
  3. 設定を保存します。

URLには、Tailscaleのマシン名ではなくIPアドレスを書いてください。このURLを使うのはVPSのホストではなく、Immichのサーバーコンテナです。コンテナの中の名前解決は、ホストのMagicDNS(Tailscaleの名前解決)を通るとは限りません。IPアドレスなら、この問題が起きません。

コンテナから 100. のアドレスに届くのは、コンテナの通信がホストを経由して外に出るからです。ホストに tailscale0 インターフェースがあれば、100. 宛ての通信はそこを通ります。

本当に自宅PCで処理されているか確かめる

自宅PCで、ログを表示したままにします。

cd ~/immich-ml
docker compose logs -f immich-machine-learning

この状態で、VPS側のImmichに写真を1枚アップロードするか、スマート検索で何か検索します。自宅PCのログに Loading textual model や Loading visual model で始まり、モデル名を含む行が出れば、処理は自宅PCに来ています。初回は、その前にモデルをダウンロードする行が出るので、少し時間がかかります。

自宅PCのログに何も出ないのに検索結果が返る場合は、VPSのローカルコンテナが処理しています。URLの順番と、VPSから pong が返るかを、もう一度確認してください。

ローカルの予備を残すか、外すか

ローカルの予備を残すと、自宅PCが止まっている間もスマート検索と顔検出が動き続けます。待機中のメモリは大きくありません。機械学習コンテナは、一定時間使われなかったモデルをメモリから外すからです。この時間は環境変数 MACHINE_LEARNING_MODEL_TTL で決まり、初期値は300秒です。

問題は、自宅PCが止まった瞬間です。予備のコンテナは、VPSのRAMにモデルを読み込みます。後で説明する大きな多言語モデルを設定していると、VPSが数GBのモデルを読み込もうとします。RAMが足りなければ、カーネルのOOM killer(メモリ不足のときにプロセスを強制終了する仕組み)がプロセスを止めます。このとき dmesg には Out of memory: Killed process で始まる行が残ります。

判断の基準はひとつです。選んだモデルのメモリ使用量がVPSの空きRAMより大きいなら、予備は外してください。外す手順は次のとおりです。

  1. 管理画面のURLを自宅PCのものだけにして、保存します。
  2. VPSの docker-compose.yml から immich-machine-learning のサービスを削除します。
  3. VPSで docker compose up -d --remove-orphans を実行し、残ったコンテナを片付けます。

この構成には代償があります。公式ドキュメントによると、自宅PCが止まっている間、スマート検索と顔検出のジョブは失敗します。その結果、それらに依存する重複検出と顔認識も、該当する写真では実行されません。PCが戻ったら、管理画面の Job Status ページで、Smart Search と Face Detection の横にある Missing ボタンを押してください。失敗した写真がもう一度処理されます。

日本語で検索するなら多言語CLIPモデルを選ぶ

スマート検索は、CLIP(Contrastive Language-Image Pre-training、画像と文章を同じ数値の空間に置くモデル)を使います。初期設定のモデルは ViT-B-32__openai です。これは英語のモデルです。公式ドキュメントも、英語だけで検索するならCLIPの英語用モデルを、それ以外の言語なら多言語モデルを使うよう案内しています。日本語の検索文を正しく扱うには、多言語モデルへの変更が必要です。

公式ドキュメントには、言語ごとに各モデルを測った表があります。下のグラフは、その日本語の表から主なモデルを抜き出したものです。再現率は、テスト用の検索で正しい画像を上位に返せた割合です。数値はImmichが公表しているもので、筆者が測ったものではありません。

Chart日本語検索の再現率 %(Immich公式ドキュメントの公表値)
The data behind this chart
[
  {
    "label": "XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k",
    "recall_pct": 83.95
  },
  {
    "label": "nllb-clip-large-siglip__v1",
    "recall_pct": 82.21
  },
  {
    "label": "nllb-clip-base-siglip__v1",
    "recall_pct": 78.72
  },
  {
    "label": "XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k",
    "recall_pct": 75.93
  },
  {
    "label": "ViT-L-16-SigLIP2-256__webli",
    "recall_pct": 63.69
  },
  {
    "label": "ViT-B-16-SigLIP2__webli",
    "recall_pct": 56.38
  }
]

日本語の表で最も再現率が高いのは XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k で、83.95% です。次が nllb-clip-large-siglip__v1 で、82.21% です。軽いモデルでは XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k が 75.93% を出しています。SigLIP2系の ViT-B-16-SigLIP2__webli は 56.38% で、日本語ではXLM系とnllb系に届いていません。

Chartベンチマーク中のピークメモリ MiB(Immich公式ドキュメントの公表値)
The data behind this chart
[
  {
    "label": "XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k",
    "memory_mib": "4,014"
  },
  {
    "label": "nllb-clip-large-siglip__v1",
    "memory_mib": "4,226"
  },
  {
    "label": "nllb-clip-base-siglip__v1",
    "memory_mib": "4,675"
  },
  {
    "label": "XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k",
    "memory_mib": "3,030"
  },
  {
    "label": "ViT-L-16-SigLIP2-256__webli",
    "memory_mib": "2,830"
  },
  {
    "label": "ViT-B-16-SigLIP2__webli",
    "memory_mib": "3,038"
  }
]

ここが、機械学習を別のマシンへ移す一番の理由です。再現率が最も高いモデルのピークメモリは 4,014 MiB、nllb-clip-large-siglip__v1 は 4,226 MiB です。軽い XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k でも 3,030 MiB あります。データベースやImmichのサーバーと同じ小さなVPSに、これを載せる余裕はほとんどありません。自宅PCに16GBや32GBのメモリがあれば、問題なく収まります。

モデルの種類によって、検索文の扱い方が違います。ドキュメントによると、名前に nllb を含むモデルは、ユーザー設定で指定した言語で検索文が書かれていることを前提にします。nllb系を使う場合は、各ユーザーの言語設定を日本語にしてください。xlm や siglip2 を含むモデルは、言語設定に関係なく検索文を理解します。ドキュメントのおすすめは、主に母語だけで検索するならnllb系、言語を混ぜて検索するならxlm系かsiglip2系です。「東京 sunset」のように日本語と英語を混ぜて検索する家族がいるなら、xlm系が向いています。

モデルを変える手順は次のとおりです。

  1. 使いたいモデル名をそのままコピーします。たとえば XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k です。
  2. 管理画面の機械学習の設定で、Smart Search の項目を開きます。
  3. モデル名の欄に貼り付けて、保存します。
  4. Job Status ページで、Smart Search の横の All を押します。

最後の手順は省略できません。モデルが違うとベクトルの形式も変わるので、古いモデルで作ったベクトルは新しいモデルの検索文と比べられないからです。すべての写真を処理し直すことになります。写真が数万枚あるなら、ここでGPUのあるPCが役に立ちます。なお、ドキュメントには、まれに古いモデルのデータがデータベースに残り、スマート検索のジョブがエラーになることがある、とも書かれています。

GPUを使う場合(ハードウェアアクセラレーション)

GPUがなくても、別のマシンに移すだけでVPSのメモリ不足は解消します。GPUが効くのは、全写真の処理し直しのような大量の処理です。ここでは手順の要点だけを説明します。GPUドライバーの導入は扱いません。

自宅PCの ~/immich-ml に、公式の hwaccel.ml.yml をダウンロードします。

cd ~/immich-ml
wget https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml

次に docker-compose.yml の2か所を変えます。イメージのタグの最後に、GPUの種類に合わせた名前を付けます。NVIDIAなら -cuda、AMDなら -rocm、Intelなら -openvino です。そして extends で同じ名前のサービスを指定します。NVIDIAの場合は次のようになります。

    image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
    extends:
      file: hwaccel.ml.yml
      service: cuda

保存したら docker compose up -d でコンテナを作り直します。各GPUに必要なドライバーやコンテナ用ツールは、Immichの公式ドキュメントのハードウェアアクセラレーションのページに書かれています。WindowsのWSL2で動かす場合は、openvino-wsl のように -wsl の付いたサービスが用意されているものがあります。

アップデートのときに気をつけること

Immichを更新するときは、VPSと自宅PCを同じ日に、同じバージョンへ上げます。両方の .env の IMMICH_VERSION を同じ値に書き換えてから、それぞれのディレクトリで次を実行します。

docker compose pull
docker compose up -d

VPSだけを更新して自宅PCを忘れることが、この構成で最も起きやすい失敗です。Immichのリリースノートを読むときに、機械学習コンテナの変更も確認してください。

うまく動かないとき

再起動のあとに自宅PCのコンテナが起動していない。 docker compose up -d を実行して bind: cannot assign requested address と表示される場合は、DockerがポートをTailscaleのアドレスに割り当てようとした時点で、そのアドレスがまだPCに付いていません。tailscale status で接続を確認してから、もう一度 docker compose up -d を実行してください。再起動のたびに docker compose ps で確認する習慣をつけると安全です。

Tailscaleのアドレスが変わった。 Tailscaleのアドレスは、端末をtailnetから削除して登録し直さない限り変わりません。登録し直した場合は、自宅PCの .env の ML_BIND_IP と、Immichの管理画面のURLの両方を新しいアドレスに直します。

VPSのImmichが突然止まる。 VPSで dmesg を実行し、Out of memory: Killed process の行を探します。あれば、自宅PCが止まっている間にローカルの予備が大きなモデルを読み込んだ可能性が高いです。前の節の手順で予備を外してください。

スマート検索だけがエラーになる。 VPSで docker compose logs immich-server を実行し、エラーを確認します。モデルを変えた直後なら、Job Status ページで Smart Search の All をもう一度実行してください。

2台のVPSで同じ構成を作る場合も、考え方は同じです。プライベートネットワークのアドレスで待ち受けて、そのアドレスをURLに書きます。その場合の準備は2台のVPSをプライベートネットワークでつなぐ方法を参照してください。ただし、ドキュメントが注意しているとおり、有料のクラウドで処理させると、写真のプレビューはその事業者のマシンを通ります。

FAQ

Immichの機械学習を別のPCに移すと、写真は外部に送られますか?

送られるのは写真の原本ではなく、プレビュー画像です。送り先は自分で指定した機械学習コンテナだけです。公式ドキュメントによると、機械学習コンテナはこのデータを保存せず、どのユーザーのものかも記録しません。ただし、プレビュー画像は通信の途中で読める状態にあるので、通信はTailscaleやWireGuardのような暗号化されたトンネルの中に限定してください。

自宅PCの電源を切っていると、スマート検索はどうなりますか?

スマート検索は検索のたびに機械学習コンテナを使うので、自宅PCが止まっているとリモートには届きません。管理画面に http://immich-machine-learning:3003 を2番目のURLとして残していれば、VPSのローカルコンテナが代わりに処理します。予備を外している場合は、検索とジョブが失敗します。PCが戻ったあとに、Job Status ページで Smart Search と Face Detection の Missing を押してください。

Immichで日本語の検索をするには、どのCLIPモデルを選べばよいですか?

初期設定の ViT-B-32__openai は英語のモデルなので、日本語の検索文には多言語モデルを選びます。公式ドキュメントの日本語の表で再現率が最も高いのは XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k で、nllb-clip-large-siglip__v1 が続きます。どちらもピークメモリが約4GBあります。nllb系を選ぶ場合は、ユーザー設定の言語を日本語にしてください。変更したあとは Job Status ページで Smart Search の All を押し、すべての写真を処理し直します。

ポート3003をファイアウォールで制限して公開するのではだめですか?

おすすめしません。理由は2つあります。機械学習コンテナには認証がまったくないことと、VPSとPCの間の通信が暗号化されないHTTPであることです。さらに、Dockerが公開したポートへの通信はufwのルールより先に処理されるので、ufwで閉じても効きません。待ち受けアドレスをTailscaleやWireGuardのアドレスに限定するのが確実です。

GPUのないPCに移しても意味はありますか?

あります。VPSのメモリ不足の原因はモデルの読み込みなので、CPUだけのPCに移してもVPSのRAMは空きます。GPUが効くのは、モデルを変えたあとの全写真の処理し直しのような大量の処理です。GPUを使う場合は、hwaccel.ml.yml を同じディレクトリに置き、イメージのタグに -cuda、-rocm、-openvino のいずれかを付けます。