画像生成AIをサービスに組み込むとき、モデルを動かすこと自体は難しくありません。
難しいのは GPU を遊ばせずに、かつ待たせすぎない運用です。
GPU は「待機時間も課金される」
Web アプリの感覚で GPU インスタンスを常時起動しておくと、
リクエストが無い時間もそのまま費用になります。
かといって都度起動すると、モデルのロードに数分かかって使い物になりません。
現実的な落としどころはこうでした。
[Web(CPU)] --- ジョブ投入 ---> [キュー] <--- ポーリング --- [GPUワーカー]
↓
生成 → ストレージ → 完了通知
Web と GPU を完全に分離し、GPU 側はキューを見るだけにします。
キューの状態を必ず持つ
class GenerationJob(models.Model):
class Status(models.TextChoices):
QUEUED = "queued"
RUNNING = "running"
DONE = "done"
FAILED = "failed"
status = models.CharField(max_length=10, choices=Status.choices)
started_at = models.DateTimeField(null=True)
finished_at = models.DateTimeField(null=True)
error = models.TextField(blank=True)
started_at を持つのが重要です。
RUNNING のまま一定時間を超えたジョブを検出して復旧できます。
stale = GenerationJob.objects.filter(
status="running", started_at__lt=now() - timedelta(minutes=15)
)
stale.update(status="queued", started_at=None) # 拾い直す
GPU ワーカーが落ちると、ジョブが RUNNING のまま永遠に残ります。
この回収処理が無いと、ユーザーの画面は「生成中」のまま止まります。
同時実行数は GPU メモリで決まる
CPU のワーカー数の感覚で増やすと、すぐ VRAM が溢れて落ちます。
モデルのVRAM使用量から逆算して、1GPUあたりの同時実行数を固定します。
MAX_CONCURRENT = 2 # 実測して決める。増やすと OOM
推測ではなく、実際に生成しながら nvidia-smi で見て決めました。
コストを抑える3つの運用
1. アイドル時間で自動停止
一定時間ジョブが無ければインスタンスを停止し、
投入時に起動する構成にすると費用が大きく変わります。
起動待ちが発生するので、待ち時間が許容される機能に限って適用します。
2. 生成物のキャッシュ
同じパラメータの生成は結果を使い回します。
プロンプトとシードのハッシュをキーにするだけで、体感が変わります。
key = hashlib.sha256(f"{prompt}|{seed}|{model}".encode()).hexdigest()
3. 解像度を段階的に
まず低解像度でプレビューを返し、ユーザーが選んだものだけ高解像度で生成します。
GPU時間の大半は使われない候補の生成に消えていたので、これが一番効きました。
生成物の扱い
生成画像をアプリのサーバーに置くとディスクが溢れます。
オブジェクトストレージに逃がし、署名付きURLで期限を切って配信します。
直リンクで無制限に配れる状態にすると、帯域費用が読めなくなります。
まとめ
- Web と GPU を分離し、GPU はキューを見るだけにする
started_atを持ち、放置された RUNNING を回収する- 同時実行数は VRAM から実測で決める
- プレビュー段階の解像度を下げるのが最大のコスト削減
- 生成物は署名付きURLで期限を切る