1ステップ19分の地獄。RTX 5090を自動化パイプライン3本で取り合うと何が起きるのか|診断から恒久対策まで
ローカルLLMと画像生成が同じGPUを取り合うと、生成は1100倍遅くなり7時間の学習が全損する。実際に起きた事故の記録と、原因プロセスの特定手順、ゲートウェイ方式による恒久対策をまとめた。
最終更新:
1ステップ19分の地獄。RTX 5090を自動化パイプライン3本で取り合うと何が起きるのか
📖 30秒でわかるこの記事のまとめ
むずかしい言葉を使わずに言うと、こういう話です。
私のパソコンの中では、「絵を描くAI」「文章を書くAI」「調べ物をするAI」の3人が働いています。ところが作業に必要な「机」(GPUという部品)は1つしかありません。
ある日、3人が同時に机を使おうとして大混乱に。誰もエラー(悲鳴)を上げないまま、全員の仕事がほとんど止まってしまいました。絵を1枚描くのに5時間半。7時間かけた作業が消えたこともありました。
この記事は、「誰が机を占領しているのか」を突き止める方法と、「順番待ちのルール」を作って二度と混乱しないようにした方法の記録です。
「AIを2つ以上、同じパソコンで動かしている(動かしたい)人」に、いつか必ず役立つ話です。
📚 前提知識:この記事に出てくるもの
事前知識がなくても読めるように、登場するものを先に説明します。知っている方は読み飛ばしてください。
| 用語 | ざっくり説明 |
|---|---|
| GPU | AIの計算を高速にこなす部品。この記事では「作業机」に例えています。1台のPCに1枚が普通 |
| VRAM | GPUに直結した専用メモリ(=机の広さ)。私のGPU(RTX 5090)は32GB。AIモデルはここに載らないと本来の速度が出ません |
| ローカルLLM | ChatGPTのような文章AIを、クラウドではなく自分のPCで動かすもの。1つで19〜21GBものVRAMを使います |
| Ollama(オラマ) | ローカルLLMを動かすための定番ソフト。PCの中で「窓口(ポート11434)」を開いて、いろんなプログラムからの依頼を受け付けます |
| ComfyUI | 画像生成AIを動かすための定番ソフト。こちらも大量のVRAMを使います |
| LoRA学習 | 画像AIに特定の絵柄や人物を覚えさせる追加学習。数時間かかる重い処理です |
| パイプライン | 「自動で動き続ける一連の処理」のこと。この記事では、記事の自動生成・データ収集・画像生成という3つの自動処理が登場します |
| Docker | プログラムを「コンテナ」という箱に入れて動かす仕組み。箱の中からもOllamaの窓口に依頼が飛んできます |
| ゲートウェイ(プロキシ) | 窓口の前に立つ「受付係」。依頼を中継しつつ、必要なら「今は順番待ちです」と待たせることができます。今回の解決策の主役です |
押さえてほしいポイントは1つだけ: VRAM(机の広さ)は32GBしかないのに、文章AI(21GB)と画像AI(20GB超)を同時に載せようとすると溢れる、ということです。溢れたときに何が起きるかが、この記事の本題です。
🗺️ この記事の構成
- 何が起きたか — 実際の事故の記録(数字つき)
- どう考えて解決したか — 対策の発想と設計
- 具体的な実装 — コードと手順(コピペ可能)
- 明日から使えるTips — 調査コマンド集と応急処置
🎯 この記事で得られること
この記事を読むと、ローカルGPUを複数の自動化処理で安全に共有できるようになります。
- ✅ GPU競合が起きたときの具体的な症状と見分け方(実測値つき)
- ✅ 「どのプロセスがGPUを使っているのか」を特定する調査手順
- ✅ 応急処置から恒久対策(ゲートウェイ調停)までの段階的な対処法
😰 あなたもこんな悩みを抱えていませんか?
- 「ローカルLLMと画像生成を同じマシンで動かしたら、どっちも遅くなった…」
- 「夜中に仕込んだバッチが朝になっても終わっていない。エラーも出ていないのに…」
- 「
nvidia-smiを見るとVRAMが満杯。でも誰が使っているのか分からない…」
私も先週末、まったく同じ状況に陥りました。 しかも自動化パイプラインを3本も同じマシンで飼っていたせいで、事態は想像よりずっと悪化しました。
📖 私のストーリー:静かに死んでいくパイプライン
Before:1台のRTX 5090に同居する3つの自動化
私のメインマシンにはRTX 5090(VRAM 32GB)が1枚。ここに、いつの間にか3系統の自動化が同居していました。
- 記事自動生成パイプライン(Docker常駐、ローカルLLM 21GBを長時間占有)
- データ収集パイプライン(スケジューラが定期起動、Vision系LLM 19GBを使用)
- 画像生成・LoRA学習パイプライン(ComfyUI、20Bクラスの画像モデル)
それぞれ単体では問題なく動いていました。「32GBもあるし、なんとかなるだろう」——この楽観が、すべての始まりでした。
事故1:画像生成が「1ステップ19分」になる
ある日、画像生成バッチの進捗が止まりました。エラーはゼロ。プロセスは生きている。でも12時間で生成枚数が1枚も増えていない。
ComfyUIのログを見て目を疑いました。
90%|████████▉ | 18/20 [5:30:56<37:46, 1133.31s/it]
1ステップ1133秒(約19分)。通常は1秒前後なので、およそ1100倍の遅さです。1枚の画像に5時間半かかっていました。
原因はVRAMの取り合いでした。LLMが19〜21GBを占有した状態で20Bクラスの画像モデルを動かすと、モデルの一部がVRAMから追い出され、システムメモリとの間でスワップが発生します。クラッシュせず、エラーも出さず、ただ絶望的に遅くなる。これがGPU競合の最も厄介な性質です。
事故2:7時間20分の学習が全損する
さらに手痛かったのはLoRA学習です。VRAM 31.9/32.6GBという限界張り付き状態で7時間20分学習を続けた末、CUDAエラーで突然死。途中保存を設定していなかったため、7時間分の計算が完全に消えました。
学習速度の劣化も顕著でした。競合なしで1.2it/s出ていた学習が、競合下では13.6s/itまで低下。16倍遅いうえに、いつクラッシュするか分からない爆弾を抱えて走り続けることになります。
転機:「犯人探し」の手順を確立する
「またLLMが載ってる。でも、誰が呼んでいるんだ?」
ollama ps を見ると、モデルのkeep-alive(使い終わったモデルをメモリに残しておく猶予時間)が「4分後に解放」と表示されたまま、何時間経っても4分後のまま更新され続けている。猶予時間が延長され続けている=誰かが継続的にリクエストを投げている、という証拠です。しかしOllamaのログには「誰が」の情報(クライアントのプロセス名)は残りません。
ここで確立したのが、後述する「netstatでOllamaへの接続元PIDを特定する」手順です。調べてみると、犯人は1つではありませんでした。
- 深夜〜昼: Dockerコンテナ(記事生成パイプライン)
- 昼〜夜: ホストのPython(データ収集ジョブ。しかもスケジューラが次々に新しいジョブを起動)
個別のプロセスを止めても、次のジョブがまた起動してGPUを取る。 もぐら叩きでは解決しない構造だと分かりました。
After:ゲートウェイ調停で「無改修の一括管理」へ
最終的にたどり着いたのは、Ollamaの前段にゲートウェイ(プロキシ)を置く構成です。
| 項目 | Before | After |
|---|---|---|
| 画像生成速度(競合時) | 1133秒/step | 競合自体が発生しない |
| LoRA学習 | 7時間全損の事故 | 専有ウィンドウで安全に完走 |
| パイプライン追加時の対応 | 各リポジトリを改修 | 改修ゼロ(自動的に統治下) |
| 犯人探しの時間 | 数時間 | 不要(ゲートウェイのログで一目瞭然) |
導入後、画像生成側が「専有宣言」をすると、全パイプラインのLLM呼び出しがゲートウェイで自動的に待機します。解放されれば何事もなかったように再開。どのパイプラインのコードも一切変更していません。
💭 なぜこのアプローチを選んだのか
最初に考えた方法とその限界
最初は「各パイプラインにロック確認コードを入れる」方式を実装しました。ロックファイルを見て、あれば待つ——素朴で確実です。
しかし、すぐに限界が見えました。
- 問題点1: パイプラインが増えるたびに同じ改修が必要になる
- 問題点2: 改修し忘れた(or 改修できない)クライアントが1つでもあると、そこから競合が再発する
- 問題点3: 実行中の旧コードのジョブには効かない
「今後も自動化は増える。そのたびに全リポジトリを直すのか?」
発想の転換:「全員がすでに通っている場所」で止める
行き詰まって気づいたのは、全クライアントがすでに同じ場所を通っているという事実です。Ollamaのポート11434(=LLMへの依頼が必ず通る唯一の窓口)。ここを通らないLLM呼び出しは存在しません。
つまり、Ollama本体を別の窓口(ポート11435)にこっそり引っ越しさせ、元の窓口(11434)に「受付係」=ゲートウェイを座らせれば、既存・将来の全クライアントを1行も書き換えずに調停できるのです。依頼する側は今まで通り11434に話しかけるだけで、受付係が裏で交通整理をしてくれます。
決め手になった3つのポイント
- 無改修: クライアントは今まで通り11434を叩くだけ。新パイプラインも自動的に対象になる
- 選択的: 生成系エンドポイントだけ保留し、
ollama ps等の軽量APIは素通しにできる - 可観測: 誰が何を要求し、何が保留されているかがゲートウェイのログ1箇所に集まる
「N個のクライアントを直すより、1個のチョークポイントを作る」 —— これがこの経験で最も大きな学びでした。
🔧 具体的な実装方法
全体アーキテクチャ
heavy-worker / collector / chat UI / 今後の全パイプライン
│ (全て :11434 のまま無改修)
▼
┌──────────────────────────────┐
│ Ollamaゲートウェイ (:11434) │ ← 生成系のみロック中は保留
│ - /api/chat, /api/generate │ /api/tags 等は素通し
│ - ロック: 期限付きJSON │
└──────────────┬───────────────┘
▼
Ollama本体 (:11435)
画像生成/学習側は開始前にロックを acquire、終了後に release
Step 1: 期限付きロックファイル
デッドロック対策としてロックには必ず有効期限を入れます。取得側がクラッシュしても、期限が切れれば自動的に無視されます。
# gpu_lock.py (抜粋)
LOCK_PATH = Path(r"C:\Users\you\gpu_lock\exclusive.lock")
def is_locked() -> dict | None:
if not LOCK_PATH.exists():
return None
d = json.loads(LOCK_PATH.read_text(encoding="utf-8"))
if datetime.fromisoformat(d["expires_at"]) < datetime.now():
return None # 期限切れは無視 → デッドロックしない
return d
def acquire(owner: str, reason: str, hours: float = 3.0) -> bool:
LOCK_PATH.write_text(json.dumps({
"owner": owner, "reason": reason,
"expires_at": (datetime.now() + timedelta(hours=hours)).isoformat(),
}), encoding="utf-8")
ポイントは単一ファイルではなくディレクトリごと運用すること。Dockerコンテナに見せる場合、ファイル単体のbindマウントは削除・再作成でマウントが壊れるため、ディレクトリをread-onlyマウントします。
Step 2: ゲートウェイ本体(aiohttp)
# ollama_gateway.py (核心部の抜粋)
GOVERNED_PREFIXES = (
"/api/generate", "/api/chat", "/api/embed", "/api/embeddings",
"/v1/chat/completions", "/v1/completions", "/v1/embeddings",
)
async def handle(request: web.Request) -> web.StreamResponse:
# 生成系だけロック中は保留(30秒ポーリング)
if any(request.path.startswith(p) for p in GOVERNED_PREFIXES):
while (d := lock_active()) is not None:
await asyncio.sleep(30)
# あとは素直にストリーミング転送
body = await request.read()
async with session.request(
request.method, UPSTREAM + request.path_qs, data=body or None,
headers=filtered_headers(request),
) as up:
out = web.StreamResponse(status=up.status, headers=filtered_headers(up))
await out.prepare(request)
async for chunk in up.content.iter_chunked(8192):
await out.write(chunk)
await out.write_eof()
return out
注意点は2つ。タイムアウトを外すこと(LLM生成は分単位、ClientTimeout(total=None))と、ストリーミングをそのまま中継すること(チャットUIの逐次表示が壊れないように)。
Step 3: Ollama本体の退避とカットオーバー
# Ollama本体を11435へ (ユーザー環境変数)
setx OLLAMA_HOST "127.0.0.1:11435"
# Ollamaを再起動 → ゲートウェイを11434で起動
これで全クライアントは無改修のままゲートウェイ経由になります。切り戻したい場合は環境変数を戻すだけです。
Step 4: 運用
# 画像生成・学習の前に専有宣言(所要時間+余裕で期限を切る)
python gpu_lock.py acquire image-pipeline "LoRA学習" 3
# 終わったら解放
python gpu_lock.py release image-pipeline
# 状態確認
curl http://127.0.0.1:11434/gateway/status
🧱 壁にぶつかった瞬間と乗り越え方
「止めても止めても、また誰かがGPUを使う」
一番心が折れそうになったのは、原因プロセスを特定して止めた1時間後、別のジョブがまたGPUを埋めていた時です。
スケジューラ駆動の自動化は、個々のプロセスを止めても意味がありません。「プロセスを止める」という発想自体が間違いで、「リソースの入り口で流量を制御する」に頭を切り替える必要がありました。
「サイレント劣化」は監視をすり抜ける
もう1つの学びは、GPU競合はエラーを出さないことです。プロセス生存監視も、エラーログ監視も、すべて正常のまま。ただスループットだけが1/1000になる。
これに対しては「進捗ファイル数を定期的に数え、一定時間増えていなければ異常とみなす」という進捗ベースの監視が有効でした。沈黙は成功ではない。バッチ処理の監視は「生きているか」ではなく「進んでいるか」を見るべきでした。
🎓 この経験から得た3つの教訓
教訓1:クライアントを直すな、チョークポイントを作れ
N個の呼び出し側を修正する対策は、N+1個目が現れた瞬間に破綻します。全員が必ず通る場所(今回はOllamaのポート)を押さえれば、対策は1箇所で済み、将来の追加にも自動で効きます。
教訓2:ロックには必ず「期限」を入れる
期限のないロックは、取得側が死んだ瞬間にシステム全体を道連れにします。「期限切れは無視してよい」というルールをロックの仕様に組み込むだけで、デッドロックの心配が消え、安心して自動化に組み込めます。
教訓3:長時間処理は「途中保存」がない時点で設計ミス
7時間の学習が一瞬で消えた経験から言えます。GPU競合のような外因はいつでも起こり得るので、チェックポイントなしの長時間処理はそれ自体がリスクです。学習なら save_every_n_steps、バッチ生成なら1件ごとの逐次保存+冪等リトライを最初から入れておくべきでした。
💡 実践Tips・よくあるエラーと解決法
症状: GPUを使う処理がエラーなしで極端に遅くなる
VRAM競合のサイレント劣化を疑ってください。確認手順:
# 1. VRAM使用量と使用率
nvidia-smi --query-gpu=memory.used,memory.total,utilization.gpu --format=csv
# 2. Ollamaのモデル常駐状況(keep-aliveが更新され続けていないか)
ollama ps
# UNTIL が「4 minutes from now」のまま長時間変わらない
# → 誰かが継続的にリクエストしている証拠
Tips: Ollama(11434)を使っているプロセスを特定する
Ollamaのログにはクライアント情報が残らないため、TCP接続から逆引きします。
# 11434への接続元PIDを列挙
netstat -ano | findstr :11434 | findstr ESTABLISHED
# PIDからプロセスとコマンドラインを特定
Get-CimInstance Win32_Process -Filter "ProcessId = <PID>" |
Select-Object ProcessId, Name, CommandLine
- 接続元が
com.docker.backend.exe→ Dockerコンテナ内のクライアント。docker psで稼働コンテナを確認 - 接続元が
python.exe→ CommandLine でどの自動化ジョブか判別
Tips: モデルのロード履歴を確認する
# Ollamaサーバーログからランナー起動時刻を抽出
Select-String -Path "$env:LOCALAPPDATA\Ollama\server.log" -Pattern "starting llama-server" |
Select-Object -Last 5
いつからモデルが常駐しているか(=占有時間)が分かります。
Tips: 応急処置コマンド
# LLMを即アンロード(VRAM解放)
ollama stop <model名>
# Dockerコンテナを一時停止/再開(プロセスは保持される)
docker pause <container>
docker unpause <container>
docker pause はプロセス状態を保ったまま凍結するので、キューやジョブの状態を壊さずにGPUを空けられます。
Tips: 32GB VRAMで20Bクラスの学習を安定させる(musubi-tuner)
--fp8_base --fp8_scaled --blocks_to_swap 16
--save_every_n_steps 100 --save_last_n_steps 300
fp8_scaled+blocks_to_swapでVRAM約30GB→24GBに(限界張り付きを回避)- 途中保存がないと外因クラッシュで全損する(実体験: 7時間20分が消えました)
恒久対策: Ollamaゲートウェイ(全クライアント無改修)
クライアント群(:11434のまま) → ゲートウェイ(:11434) → Ollama本体(:11435)
setx OLLAMA_HOST "127.0.0.1:11435"でOllama本体を退避- aiohttpの薄いプロキシを11434に常駐(生成系パスのみ、ロックファイル存在中は保留)
- GPU専有したい処理は期限付きロックを書く(期限切れ自動無視でデッドロックなし)
ポイント:
- プロキシは
ClientTimeout(total=None)(LLM生成は分単位) - ストリーミングは
iter_chunkedでそのまま中継 /api/tags/api/psは素通しにして運用コマンドを生かす- Dockerに見せるロックはファイル単体でなくディレクトリをread-onlyマウント
❓ よくある質問(FAQ)
Q1: ゲートウェイが単一障害点になりませんか?
A: なります。対策として (1) 異常終了時に自動再起動するランナー経由で常駐、(2) 各パイプライン側にもロック確認をフォールバックとして残す、(3) 切り戻し手順(環境変数を戻すだけ)を文書化、の3点を入れています。
Q2: vLLMやLM Studioでも使えますか?
A: 考え方はそのまま使えます。要は「全クライアントが通る唯一のポートの前段に、保留機能つきプロキシを置く」だけなので、OpenAI互換APIを話すサーバーなら同じ構成が組めます。
Q3: Kubernetesやジョブスケジューラを入れるべきでは?
A: 正攻法はそうです。ただ個人の1台マシンに対しては過剰で、「今日動いている自動化を無改修で調停できる」ことを優先しました。パイプラインがさらに増えたら本格的なスケジューラへの移行を検討します。
📝 まとめ:今日からできるアクションプラン
- まず現状把握:
nvidia-smiとollama psで、誰がVRAMを使っているか確認する - 犯人特定の手順を手元に:
netstat -ano | findstr :11434→ PID逆引きをブックマーク - 長時間処理に途中保存を入れる: 学習は
save_every_n_steps、バッチは逐次保存+冪等化 - 競合が2回起きたら恒久対策: ゲートウェイ+期限付きロックの構成を検討する
📌 まずは
ollama psを実行して、UNTILの表示が更新され続けていないか見てみてください。所要時間は10秒です。
🙏 おわりに:伝えたかったこと
最後まで読んでいただき、ありがとうございました。
ローカルLLMも画像生成も、1台のGPUでここまでできる時代になりました。だからこそ「同居」の問題は、これから多くの人が踏む地雷だと思います。
エラーが出ない障害は、本当に心が削られます。12時間進まないバッチを前に呆然とした週末の私が、この記事を必要としていました。
同じ地雷を踏んだ誰かの調査時間が、この記事で数時間短くなれば嬉しいです。
あなたの自動化ライフを、心から応援しています。
📚 参考リンク
この記事が役に立ったら、ぜひシェアをお願いします!
あなたのシェアが、同じ悩みを持つ誰かの助けになります。
tinou
情報処理安全確保支援士とPMの資格を使ってITコンサルタントとして働く傍ら、自宅で自動化とセキュリティを研究しているエンジニア