この記事についてClaude(Anthropic)との共同編集により作成されました。
要約
- VRAM に空きがあれば、複数の Python プログラムからも複数の Docker コンテナからも、同じ GPU を設定なしで使える
- ただし VRAM は分け合えても、計算は分け合えない。別々のプロセスのカーネルは同時には走らず、GPU を交代で使う(タイムスライス)
- VRAM の「空き」も見かけより小さい。プロセスごとに CUDA コンテキストが数百 MB 乗り、TensorFlow はデフォルトで VRAM をほぼ全部確保する
- 同時に計算させたいなら NVIDIA MPS を入れる。コード変更なしで、複数プロセスのカーネルが同時に走るようになる
- 実際に MPS を入れると基本は速くなったが、改善が小さいケースもあった。CPU 処理を挟むプログラムは GPU を使い続けないので、交代制でもそもそも待たされていない
きっかけの疑問
GPU インスタンスを 1 台借りて推論サーバーを動かしていると、同じ GPU で別のスクリプトも回したくなる。nvidia-smi を見ると VRAM は 24GB 中 10GB しか使っていない。残り 14GB に収まるなら、もう 1 本乗せてもいいのではないか。
VRAM が足りなければ OOM で落ちる。これは前提として、気になったのはその手前で、VRAM に余裕さえあれば、複数のプログラムから同じ GPU を使えるのか。使えるとして、何か損をしていないか。
結論から書くと、使える。設定も要らない。ただし VRAM は分け合えても、計算は交代制になる。
何もしなくても、複数のプロセスは同じ GPU に乗る
CUDA は、複数のプロセスが同じ GPU を使うことをデフォルトで許している。Python のプログラムを 2 本起動して、どちらも cuda:0 を指定すれば、両方とも GPU に乗る。nvidia-smi のプロセス一覧にも 2 本並ぶ。
これを決めているのは GPU のコンピュートモードで、次のコマンドで確認できる。
nvidia-smi --query-gpu=compute_mode --format=csvDefault なら複数プロセスで共有できる。Exclusive_Process になっていると 1 プロセスしか GPU を掴めず、2 本目は CUDA-capable device(s) is/are busy or unavailable で起動に失敗する。クラウドの GPU インスタンスは通常 Default なので、特に何もしなくていい。
Docker コンテナでも同じ
Docker コンテナから GPU を使う場合も同じで、ホストに NVIDIA Container Toolkit を入れたうえで、同じ GPU を指定してコンテナを複数起動すればいい。
# コンテナ1(GPU 0 を使用)docker run --gpus '"device=0"' --rm my-image python train.py
# コンテナ2(同じ GPU 0 を使用)docker run --gpus '"device=0"' --rm my-image python infer.pyGPU のドライバから見ると、コンテナの中のプロセスもホスト上の普通のプロセスと変わらない。なので以下の話は、プログラムでもコンテナでも同じように当てはまる。
VRAM は分け合えても、計算は交代制
使えることは分かった。問題は速さである。
GPU は、1 つのプロセスの中から投げられた複数の処理(ストリーム)なら同時に実行できる。一方、別々のプロセスから投げられた処理は同時に実行できない。プロセスごとに CUDA コンテキストという実行環境が別々に作られ、GPU は異なるコンテキストの処理をタイムスライスで切り替えながら交代で実行する1。
時間 →
プロセスA |■■■■| |■■■■| |■■■■|プロセスB |■■■■| |■■■■| |■■■■| ←── GPU を交代で使う。同時には走らない ──→つまり、2 本のプログラムがどちらも GPU を休みなく使い続けるなら、それぞれ単独で動かしたときより遅くなる。2 本の合計の処理量が増えるわけではなく、むしろ切り替えのぶん少し損をする。
もったいないのは、1 本あたりが GPU を使い切れていないケースである。小さいバッチの推論のように、1 回のカーネルが GPU の演算ユニットの一部しか使わない処理でも、自分の番のあいだは GPU を丸ごと占有する。空いている演算ユニットがあっても、他のプロセスの処理はそこに入れない。
VRAM の「空き」も見かけより小さい
もう 1 つ、きっかけの疑問の前提にある「VRAM の余裕」にも落とし穴がある。
- CUDA コンテキストのぶん: プロセスごとにコンテキストが作られ、それだけで VRAM を数百 MB 使う(CUDA やライブラリのバージョンで変わる)。プロセスを増やすほど、モデルに使える VRAM が減る
- TensorFlow は全部取りにいく: デフォルトで、見えている GPU の VRAM をほぼすべて確保する2。TensorFlow のプログラムを先に起動すると、VRAM の中身はスカスカでも 2 本目が OOM になる。
tf.config.experimental.set_memory_growthで、必要なぶんだけ確保するように変えられる2 - PyTorch はキャッシュを抱える: 一度確保して使い終わったメモリをキャッシュとして持ち続け、
nvidia-smi上は使用中に見える3。torch.cuda.empty_cache()を呼ぶと、未使用のキャッシュを他のプログラムに返せる3
しかも、VRAM はプロセスごとに予約されるわけではない。片方のプロセスがあとから確保量を増やせば、もう片方が OOM で落ちる。起動したときに余裕があっても、その余裕は誰も守ってくれない。
同時に計算させたいなら NVIDIA MPS
交代制をやめて、複数プロセスの処理を同時に走らせる仕組みが NVIDIA MPS(Multi-Process Service)である。各プロセスの処理を MPS サーバーがまとめて GPU に投げることで、別々のプロセスのカーネルが同時に実行されるようになる4。CUDA API と互換なので、Python 側のコードは変えなくていい。
効くのは、先ほどの「1 本あたりが GPU を使い切れていない」ケースで、NVIDIA も 1 回のカーネルが少ないブロック数・スレッド数で GPU の占有率が低いアプリケーションを MPS の対象に挙げている4。
セットアップ
MPS の制御デーモンを起動するだけで、各プログラムは CUDA の初期化時に自動で MPS に接続する。
# デーモン側export CUDA_VISIBLE_DEVICES=0export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mpsexport CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log
# GPU を排他プロセスモードにする(root 権限が必要)sudo nvidia-smi -i 0 -c EXCLUSIVE_PROCESS
# MPS の制御デーモンを起動nvidia-cuda-mps-control -d# 各プログラム側の環境変数(CUDA_VISIBLE_DEVICES は設定しない)export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mpsexport CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-logpython app.py# 停止echo quit | nvidia-cuda-mps-controlEXCLUSIVE_PROCESS にしておくのは、GPU を掴むのが MPS サーバー 1 つだけになるようにするためで、NVIDIA が推奨している4。裏を返すと、MPS デーモンが止まっている状態でプログラムを 2 本起動すると、2 本目は排他モードに弾かれる。
Docker コンテナから使う場合は、ホストで MPS デーモンを起動しておき、パイプ用のディレクトリをコンテナにマウントする。
docker run --gpus '"device=0"' \ -v /tmp/nvidia-mps:/tmp/nvidia-mps \ -v /tmp/nvidia-log:/tmp/nvidia-log \ -e CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps \ -e CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log \ --rm my-image python app.pyMPS の制約
- Linux(と QNX)でしか使えない4
- 1 GPU あたりにつなげるプロセス数に上限がある。現行のドキュメントでは、デフォルト設定で 60 まで4
- 障害の分離が弱い。Volta 世代以降は、あるプロセスの致命的な GPU エラーが、同じ GPU を共有している他のプロセスにも波及する4。互いに信頼できるプログラム同士で使う前提の仕組みである
MPS を入れても、そこまで速くならないこともある
実際に MPS を入れて同じ GPU で複数のプロセスを回してみると、基本的には速くなった。ただ、思ったほど改善しないケースもあった。
理由は、処理が GPU だけで完結していないことにある。データの読み込みや前処理、後処理、結果の書き出しなど、1 本のプログラムの中には CPU で動く時間がかなりある。その間、そのプロセスは GPU を使っていない。
時間 →
プロセスA |CPU|■GPU■|CPU|■GPU■|CPU|プロセスB |CPU| |■GPU■|CPU| |■GPU■|こうなると、交代制でも GPU の取り合いはあまり起きない。片方が CPU で前処理をしている間に、もう片方が GPU を使う。GPU 処理の時間帯がたまたま重なったときだけ待たされる。もともと待たされていないのだから、MPS で同時実行できるようにしても縮む時間は少ない。
逆に言えば、MPS が大きく効くのは、複数のプロセスが GPU を休みなく使い続けていて、しかも 1 本ずつでは GPU を使い切れていないときである。MPS を入れる前に、nvidia-smi dmon -s u などで GPU の使用率がどう推移しているかを見ておくと、効きそうかどうか見当がつく。
その他の共有方式
GPU を共有する方式は、MPS のほかにも 2 つある。どちらも目的が少し違う。
| 方式 | 何をするか | 計算 | 分離 |
|---|---|---|---|
| 何もしない | 複数プロセスが同じ GPU に乗る | 交代制 | なし |
| NVIDIA MPS | 複数プロセスの処理をまとめて GPU に投げる | 同時に走る | 弱い |
| タイムスライシング(Kubernetes) | 1 枚の GPU を複数の Pod に割り当てられるようにする | 交代制 | なし |
| NVIDIA MIG | GPU をハードウェアで分割する | 分割したぶんずつ独立 | 完全 |
タイムスライシングは、NVIDIA GPU Operator の機能で、1 枚の GPU を「4 枚あるもの」のように Kubernetes に見せて、複数の Pod に割り当てられるようにする。名前から速くなりそうに見えるが、実行のされ方は何もしないときと同じ交代制で、GPU はすべてのプロセスに均等に時間を配るだけである5。メモリや障害の分離もない5。Kubernetes の上で、GPU 1 枚を要求する Pod を複数スケジュールするための仕組みと考えたほうがいい。
MIG(Multi-Instance GPU)は、GPU をハードウェアのレベルで最大 7 つに分割し、メモリも演算ユニットも別々に割り当てる。分割したものどうしは完全に分離されるので、片方の OOM やエラーがもう片方に及ばない。ただし対応しているのは A100、A30、H100、H200、B200 などのデータセンター向け GPU に限られる6。AWS なら p4d(A100)や p5(H100)のクラスになり、推論 1 本を載せるような手軽なインスタンスでは使えない。
まとめ
VRAM に余裕さえあれば、複数のプログラムやコンテナから同じ GPU は使える。設定も要らない。ただし VRAM は分け合えても、計算は交代制になる。
- 遅くなるのは、複数のプロセスが GPU を同時に使い続けるときで、1 本あたりが GPU を使い切れていないほど無駄が大きい
- VRAM の余裕は、コンテキストのぶんとフレームワークの確保のぶんだけ見かけより小さく、しかも守られない
- 同時に計算させたいなら MPS を入れる。コードは変えなくていい
- ただし CPU 処理を挟むプログラムは、交代制でもそこまで待たされていないので、MPS の効きも小さい
参考文献
- NVIDIA Multi-Process Service — Architecture https://docs.nvidia.com/deploy/mps/latest/architecture.html
- TensorFlow — Use a GPU https://www.tensorflow.org/guide/gpu
- PyTorch — CUDA semantics https://docs.pytorch.org/docs/stable/notes/cuda.html
- NVIDIA Multi-Process Service — When to Use MPS https://docs.nvidia.com/deploy/mps/when-to-use-mps.html
- NVIDIA GPU Operator — Time-Slicing GPUs in Kubernetes https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-sharing.html
- NVIDIA Multi-Instance GPU User Guide — Supported GPUs https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-gpus.html