#GPU#クラウド#ライブ配信#SRE

GPU 1枚でAIアバターは何体配信できる? 実測4体・1体あたり月7,600円


3Dアバターが自動でライブ配信する無人システムを作っています。番組の開始時刻になるとクラウドGPUのPodを起動し、レンダラーが映像を組み立てて配信し、尺が終わったらPodを破棄する。人が張り付かない前提なので、「何体乗るか」「壊れたときにどう自力で立ち直るか」「どれだけ速く立ち上がるか」の3つが、そのまま事業の成否とサービス品質に直結します。

どれも見積もりでは当たらない種類の問いでした。数時間ぶんGPUを借りて測るコストは数百円です。この記事では、実際に測って設計を決めた3つの話を、つまずき→原因→解決の順にまとめます。

  • キャパシティ: GPU 1枚に何体乗るのか。答えは4体、1体あたり月7,600円。ただしボトルネックはGPUではなかった
  • 信頼性: 同じイメージなのにホストによって60秒ごとに落ちる。不良ホストを自動で引き直す仕組み
  • 起動速度: Pod起動から配信開始まで4分かかっていたのを95秒にした話

この3つは独立した工夫に見えて、実は連鎖しています。起動を速くしたからこそ不良ホストの引き直しが実用になり、キャパシティが読めたからこそ単価が出せる。順に見ていきます。

1. GPU 1枚に何体乗るのか — 実測は4体

いちばん最初に答えが欲しかった数字がこれでした。「GPU 1枚に何体乗るのか」。ここが決まらないと単価が出ず、単価が出ないと事業として成立するかを判断できません。

見積もりでは意味がないので測りました。結果です。

項目 実測値
GPU RTX 4000 Ada(時間 $0.28、コミュニティ型)
同時配信数 4体で720p30の実時間維持(録画の実尺 89秒 / 89秒)
そのときのGPU使用率 26%
ボトルネック CPU(16 vCPU側が先に飽和)
上限の推定 5〜6体

そして単価です。

稼働形態 1体あたりの月額
24時間常時配信 ¥7,600
1日8時間の番組編成 ¥2,500

合否は「fps」ではなく「成果物の実尺」で見る

同時実行の計測でありがちなのがfpsだけを見ることですが、配信では不十分です。録画された成果物の尺が、実時間と一致しているかを見る必要があります。

理由は単純で、描画が間に合わなくなったとき、このパイプラインは「カクつく」のではなく「時間が飛ぶ」からです。90秒動かして6秒分しか記録されない、という壊れ方を実際に経験しました。fpsのログは出ているのに成果物が短い。

なので合否条件はこう置きました。

N体を同時に89秒動かして、
全部の出力ファイルの実尺が89秒であること

4体で全ファイルが89秒 / 89秒。ここが確定した瞬間に「4体は乗る」と言えるようになりました。あわせて画面キャプチャのレートが33fps出ていることも確認しています。

意外だったのは、GPUが余っていたこと

4体を回してGPU使用率が26%。つまりGPU側には3倍以上の余裕がありました。先に飽和したのはCPU(16 vCPU)です。内訳を考えると納得できます。

処理 どこを使うか
3Dシーンの描画 GPU
フレームの取り出し CPU / 転送
H.264エンコード CPU(ソフトウェアエンコードの場合)
音声のミックスとmux CPU
RTMP送出 CPU / ネットワーク

GPUの仕事は「描く」ところだけで、配信パイプラインの残り全部はCPU側にあります。「GPUを借りている」と思っているとGPUのスペックばかり比較してしまいますが、実際に体数を決めていたのはvCPU数でした。

この観測から次に効く手も見えます。ハードウェアエンコーダ(NVENC)を使えばCPUが空くので、体数はさらに伸びる見込みです。GPUを選ぶときに「NVENCが載っているか」を条件に入れるべきだ、というのはここから来ています。

相乗りさせるための実装上の注意

複数体を1つのホストに詰めるとき、1つだけ実装を直しました。

レンダラーはページから送られてくる音声を名前付きパイプ(fifo)経由でffmpegに渡していたのですが、**このパスがプロセス間で共通だと相乗りできません。**2体目が同じパイプを掴んで音声が混線します。ポート番号ごとにパスを分けるだけの修正で解決しました。

/tmp/audio.fifo            → 相乗り不可
/tmp/audio-<port>.fifo     → 体ごとに独立

こういう「1台で1個」を前提にした共有資源(一時ファイル、固定ポート、ロックファイル、キャッシュディレクトリ)は、相乗りを始めた瞬間に全部出てきます。先に洗い出しておくと計測がスムーズです。

単価をどう出したか

計算は素直です。

時間 $0.28 × 720時間/月 = $201.6/月(GPU 1枚)
$201.6 ÷ 4体 = $50.4/体 ≒ ¥7,600/体(150円/USD)

ここで効いてくるのが稼働時間の考え方です。24時間動かす前提だと¥7,600ですが、我々のシステムは「決まった時間に配信を始めて、尺が終わったら自動で終了してPodを破棄する」番組編成型です。1日8時間なら240時間なので、1体あたり約¥2,500。3倍違います。

深夜に視聴者がいない配信を回し続けるのは、単に金を燃やしているだけです。**「常時稼働が必要か」ではなく「どの時間帯に配信枠を置くか」**という設計の問題として扱うのが正しい、というのが今回いちばん実務的な学びでした。

忘れがちなコスト

GPUの時間単価以外に乗るものも挙げておきます。

  • 上り帯域: 2.5Mbpsで送出すると1日あたり約27GB。egress課金のあるクラウド($0.09/GB前後)だと1日+¥360、月+1万円規模になります。GPU専業のクラウドは通信費込みのことが多く、ここで差がつきます
  • LLM / TTSの生成コスト: 24時間しゃべり続けるAIは、その裏で推論を回し続けます。外部APIのTTS/LLMを使う構成なら、レンダラーより高くつく可能性があります。我々は自社の推論基盤をそのまま使う判断をしました
  • スポット/プリエンプティブル: 半額以下になりますが、配信中に落ちるのは放送事故です。長時間の本番には向きません

2. 同じイメージなのに、ホストによって60秒ごとに落ちる

キャパシティが読めても、その1枚が安定して動くとは限りません。安価なコミュニティ型(個人や事業者が余剰GPUを貸し出す形態)を使っていたところ、こういう現象に出会いました。

実行 ホスト 挙動
run3 ホストA 10分間、切断ゼロ
run4 ホストB 60〜150秒周期でページがクラッシュ → 復帰 → またクラッシュ

**イメージも設定もコードも同じ。**違うのは割り当てられた物理ホストだけです。

最初は自分のコードを疑って復帰処理のログを読み込んでいましたが、途中で「これはこちらのバグではない」と切り替えました。結論として、引き直せばいいという方向で解決しています。

なぜ困るのか — 復帰処理が正しいほど不気味になる

ライブ配信では、この症状は単なる再起動では済みません。レンダラーが復帰するたびに、

  • ページが再読み込みされ、サーバへ再接続する
  • 再接続のたびにアバターが挨拶をやり直す
  • 番組の終わり際に流すはずの締めの挨拶を取りこぼす

視聴者から見ると「1分おきに自己紹介を始める配信」になります。復帰処理が正しく動いているほど、症状は不気味になるという厄介さがありました。

設計: 2層で引き直す

対策は2つのコンポーネントに分けて入れました。片方だけでは足りません。

層1: レンダラー自身が「自分はダメだ」と申告する(時間窓バースト検知)

レンダラーには元々、障害からの自己復帰機能がありました。ffmpegが死んだりページがクラッシュしたら、ページを読み込み直して再接続し、配信を継続する、というものです。この復帰回数に時間窓を入れました。

600秒の窓の中で復帰が4回を超えたら、
プロセスを exit 1 で終了する

累積回数で数えないのがポイントです。長時間配信では、正常なホストでも一晩に数回は復帰します。累積カウンタだと、健全な長時間配信ほど閾値に達してしまう。「短い時間に集中して起きているか」を見たいので、窓で数えます。

自分から死ぬのは直感に反しますが、上位に「この個体はダメ」を伝える一番確実な手段がプロセスの終了でした。

層2: スケジューラがPodの死活を見て、別ホストで取り直す

配信のライフサイクルを管理しているスケジューラ側に、Podの状態監視を足しました。

  • Podの状態を定期的に取得する
  • EXITED(層1の自滅を含む)や、APIが404を返す(Podごと消えた)を検知する
  • 検知したら、新しいホストでPodを取り直す(上限3回)
  • ただし、番組の締めに入ったあとは対象外にする

404を「エラー」ではなく「消滅」という1つの状態として扱うのが実装上のコツでした。ここを例外として扱うと、リトライループに入って復帰しません。なお、消えたPodに対する削除要求も404を返しますが、これは無害です(すでに存在しないので目的は達成されている)。ログにERRORが出て一瞬焦りますが、無視して構いません。

検証: 手で殺して、直るのを見る

実装したら、実際に壊して直ることを確かめます。配信中に手動でPodを削除しました。

Pod削除
  → 32秒で消滅を検知
  → 別ホストでPodを再取得
  → レンダラー起動、配信成立
  → 番組の尺どおりに自動終了、Podは自動破棄

障害注入をやらないと、この手の復帰コードは「書いてあるだけ」になります。壊し方は雑でいいので、実際に殺してみるのが確実です。今回はAPIでPodを消すだけで、本物の不良ホストとほぼ同じ状況が作れました。

残っている制約 — 引き直しは「速さ」に支えられている

正直に書いておくと、完全ではありません。**配信が始まったあとに引き直すと、配信枠が先に閉じることがあります。**YouTube側には「ストリームが途切れたら配信を自動終了する」設定があり、新しいPodが立ち上がるより先に枠が閉じてしまうケースです。枠を作り直すところまでは実装していないので、この場合は番組が終了扱いになります。

対策としては、枠が閉じる前に立ち上げ直せる速度が効きます。ここが次の話につながります。引き直しの仕組みは、立ち上げの速さがあって初めて実用になる。我々はレンダラーのイメージに必要なものを全部焼き込んで、Pod作成から配信開始まで95秒まで縮めました。それでも間に合わないケースは残ります。

選択の指針

コミュニティ型は安い(時間 $0.24〜0.28程度)代わりに、この当たり外れがあります。使い分けはこうしています。

用途 選択
開発中の実験、短時間の検証 コミュニティ型。安さが効く
本番配信、長時間の通し検証 セキュア型(事業者運用)。当たり外れが小さい

そのうえで、どちらを選んでも引き直しの仕組みは入れておく。セキュア型でもホストは死にます。頻度が違うだけです。

3. 起動4分を95秒にした — 「起動時に用意する」をやめる

前章の引き直しが効くかどうかは、立ち上げの速さで決まります。ところが最初の実装では、Pod起動から配信開始まで約4分かかっていました。Podを起動してから実際にライブが始まるまでの時間が、そのまま「番組が始まらない時間」であり、不良ホストを引き直したときの「無音の時間」でもあります。専用イメージに変えて95秒になりました。何をやめたかという話です。

最初の方式: 公式イメージ + 起動時セットアップ

素早く動かすために、こういう構成にしていました。

  1. Playwrightの公式イメージでPodを起動する
  2. コンテナ起動コマンドで、セットアップスクリプトを外部から取ってくる
  3. スクリプトがaptで不足パッケージを入れる(Vulkanツール、日本語フォント、ffmpegなど)
  4. GPUドライバのICD設定ファイルを配置する
  5. アプリケーションのアーカイブを取得して展開、npm install
  6. レンダラーを起動

開発中はこれで正解でした。イメージをビルドし直さずにコードを差し替えられるので、試行のサイクルが速い。GPU環境での試行錯誤が続いていた時期は、この機動力に助けられました。問題は、これを本番の運用に持ち込んだときに起きました。

起動時セットアップの3つの問題

1. 単純に遅い

aptのインデックス更新とパッケージ取得、npmの依存解決。毎回、全部やり直します。番組開始が4分遅れる、というのは配信サービスとして単純に品質が悪い。

2. 外部依存が増える

起動のたびにaptのミラー、npmのレジストリ、自前の資材配布サーバに接続します。**このどれか1つが遅い日は、配信の開始が遅れます。**配信システムの信頼性が、パッケージリポジトリの機嫌に連動してしまう。

実際、資材配布用に立てたnginxがworker_processes autoのまま196ワーカーを起動してメモリ上限に当たり、配信が始まらないという事故も起こしました(コンテナでautoを使うとホストのコア数を見てしまう、という別の罠です)。配信のために増やした部品が、配信を止める。

3. 起動直後の書き込みが、たまに失敗する

これが厄介でした。コンテナ起動直後にファイルシステム(overlayfs)へ書き込むと、稀に失敗するケースがありました。ICDの設定ファイルを置く処理がこれに当たり、失敗するとGPU描画が成立しないまま起動が続きます。

GPU描画に失敗したChromiumはエラーを出さずにCPU描画へ落ちます。つまりこの失敗は、「配信は始まるが、絵がカクカクの状態で始まる」という形で表面化しました。ちょうど第1章で触れた「時間が飛ぶ」破綻と同じ根です。リトライを入れて対処しましたが、リトライで隠すべき問題ではなかったというのが正直なところです。

やったこと: 全部焼き込む

専用イメージを作り、起動時にやっていたことを全部ビルド時に移しました。

FROM mcr.microsoft.com/playwright:v1.54.0-noble

RUN apt-get update && apt-get install -y \
      ffmpeg \
      fonts-noto-cjk \        # 日本語フォント(無いとテロップが豆腐になる)
      vulkan-tools mesa-utils \
      xvfb \
    && rm -rf /var/lib/apt/lists/*

# NVIDIA の EGL/Vulkan ICD 設定を焼き込む(起動時の書き込み失敗をなくす)
COPY icd/ /usr/share/

COPY app/ /app/
RUN cd /app && npm ci --omit=dev

これで起動時にやることは「イメージをpullして、プロセスを起動する」だけになりました。結果、Pod作成からYouTubeのライブ遷移まで95秒。約4分から2.5倍以上の短縮です。

焼き込みの代償と、その扱い

正直に書くと、焼き込みにも代償があります。初回のイメージpullは重いです。Playwrightの公式イメージは元から大きく、そこにさらに足しているので、キャッシュのないホストに当たるとpullだけで数分かかることがあります(旧方式でも公式イメージの初回pullに9分かかったケースを観測しています)。

つまり**95秒は「イメージがキャッシュされたホストでの値」**で、初回はもっとかかります。ここは正直に見積もっておく必要があります。

そのうえで焼き込みを選んだ理由は、時間のばらつきが小さくなるからです。起動時セットアップは、毎回いくつもの外部サービスに依存してばらつきます。イメージpullは1回だけの、かつキャッシュが効く依存です。「毎回遅い」より「たまに遅い」のほうが運用しやすい。

副次的に良かったこと

  • 配布用のサーバが要らなくなった(前述のnginx事故の再発余地が消えた)
  • 起動ログが短くなり、障害調査が速くなった。以前はaptとnpmの出力に本質的なエラーが埋もれていた
  • バージョンが固定される。「先週は動いたのに今日は動かない」がなくなる。起動時aptは暗黙のうちに最新版を引いてきます

最後のものが実は一番大きいかもしれません。起動時セットアップは、再現性を毎回サイコロに委ねているのと同じでした。速度の問題として現れましたが、中身は再現性と依存関係の問題でした。

まとめ — 測ってから設計する

無人配信システムを支える3つの数字を、それぞれ実測から決めました。

キャパシティ

  • 相乗りの合否はfpsではなく成果物の実尺で判定する(描画が破綻すると時間が飛ぶ)
  • RTX 4000 Ada($0.28/h)で4体同時に720p30。上限は5〜6体と推定
  • ボトルネックはGPUではなくCPU。GPU使用率は26%しか使っていなかった。NVENCの有無をGPU選定の条件に入れる
  • 単価は24時間で¥7,600/体、1日8時間なら¥2,500/体。稼働時間の設計がコストを3倍動かす

信頼性

  • コミュニティ型クラウドGPUは、Podが確保できてもホストが不良なことがある。自分のコードを疑い続けず、個体差を疑う選択肢を早めに持つ
  • 対策は2層: レンダラーが時間窓でバーストを検知して自滅、スケジューラが死活を見て別ホストで取り直す
  • APIの404は「エラー」ではなく「消滅」という状態として扱う。実際にPodを殺して復帰を確認する

起動速度

  • 起動時セットアップ(apt / npm / 資材取得)は開発中は強力だが、本番では遅い・外部依存が増える・起動直後の書き込みが稀に失敗するの3つを持ち込む
  • 全部イメージに焼き込んで約4分 → 95秒。代償は初回pullの重さだが、「毎回遅い」より「たまに遅い」ほうが運用しやすい
  • 焼き込みは速度以上に再現性の話。コンテナでworker_processes autoを使わない

3つは連鎖しています。起動を95秒まで縮めたから不良ホストの引き直しが実用になり、引き直せる設計だからコミュニティ型の安さを本番手前まで使え、その安いGPUで測ったからこそ単価が出せました。クラウドのリソースは均質ではない。個体を直すのではなく、個体を交換できる設計にする——そのうえで、体数も起動時間も単価も、見積もりではなく数百円払って測ってから決める。結局それがいちばん安上がりでした。

← 記事一覧へ