Modal 上の vLLM(Gemma 4 26B-A4B)のコールドスタートを縮める
代表
当社が開発しているチャットサービスでは、チャットエージェントの返答を生成する LLM を、Modal の上の vLLM で動かしています。
このサービスでは GPU コストを削減するため、 一定時間リクエストがない場合、コンテナを止めるようにしています。
コストを削減できる一方、 コンテナが止まった後の最初のリクエストのレスポンスに約5分かかることが課題でした。
本記事では、起動時間の内訳の計測、ステージングで試した5つの策の結果、そして最終的に GPU メモリのスナップショットだけを採用した理由を順に書きます。
計測条件
1. 起動の内訳:時間がかかっていたのは「重みの読み込み」ではなかった
今回の取り組みの前に、FlashInfer の JIT ビルドの結果を Volume に残すようにして、起動を一度短くしていました。
Modal のダッシュボードの Startup(コンテナの作成から起動まで)は次のとおりです。
それでも5分近くかかるため、コンテナのログの時刻から起動の全区間を埋め、どこに時間がかかっているのかを調べました。
総起動時間は 4分21秒(261秒) です。
init engine の150秒の中身は次のとおりでした。
48 GiB の重みの読み込みは全体の 15% にすぎませんでした。
いちばん大きいのは、vLLM が応答を受け付ける前に行う初期化(init engine)です。
ログからは、次の設定もわかりました。
- CUDA graph を取る大きさが57通り(1〜512)
max_model_lenが 262144(256K)- 重みは Modal の Volume(9P でマウントされる)に置いている
また、vLLM 自身が「ファイルシステムが 9P で、NFS や Lustre として認識できないため、重みの先読み(prefetch)を無効にしている」という警告を出していました。
2. 考えた策
内訳の計測結果に合わせて、策を次のように並べました。
本命は策1でした。
init engine の150秒(プロファイリング、KV キャッシュの確保、ウォームアップ)は、毎回同じ結果になる初期化です。
1回済ませて保存し、次からは復元すれば、その作業ごとなくせます。Modal の Memory Snapshots のドキュメントでは、起動の大部分が重みの読み込みの場合は効果が薄いとされています。
今回は重みの読み込みが 15% しかないので、この点は有利でした。
3. 試した策と結果
策1:GPU メモリのスナップショット
Modal の公式の例(vllm_low_latency.py)と同じ方法です。
なお、GPU メモリのスナップショットは、Modal ではまだアルファ版の機能です(Memory Snapshots)。
流れは次のとおりです。
snap=Trueの enter で vLLM を起動する- 生成を1回通す
- スリープ(
/sleep?level=1)させた状態でスナップショットを取る - 復元したあとは、
snap=Falseの enter で/wake_upを呼んで起こす
公式の例は vLLM 0.11.2 です。vLLM 0.31.0・Gemma 4・投機的デコーディングの組み合わせで動くかは、試すまでわかりませんでした。
結果として、この組み合わせでスリープとスナップショットが動き、復元したコンテナで生成が正常に返ることを確認しました。
結果
約2分30秒(約6割)短くなりました。
init engine と重みの読み込みがなくなっています。
ただし、物差しは揃っていません。スナップショットなしの4分21秒はログの区間の合計です。
復元の1分51秒は、ログの Restoring Function from memory snapshot. から Ready までの時間です。リクエストの受付から Restoring までの時間(GPU の割り当てなど)はログに出ないので、1分51秒には含まれていません。
ほかにわかったことは次のとおりです。
- スナップショットを作る回は遅い。 作成する起動は全体で約5分41秒かかり、スナップショットなしより遅くなりました。Modal のドキュメントでは、GPU の場合は GPU の種類ごとに2〜3回作られるとされています。コードや設定を変えてデプロイし直すと作り直しになります
- 生成の速さは落ちない。 コンパイル結果と CUDA graph もスナップショットに含まれるためです
策2:--enforce-eager
compilation と CUDA graph の取得を丸ごと止める策です。変更は1行だけです。比べるために、同じ日に変更前の構成もデプロイし直し、同じ方法で計測しました。
init engine は約5秒まで縮みました。第1節で「プロファイリング・KV キャッシュの作成・ウォームアップ」と見積もった約102秒も、大半は eager でない場合にだけ行われる処理だったことになります。
なお、init engine 以外の区間(vLLM の起動、重みの読み込みなど)も、変更前のほうが倍ほど長くなっていました。策2とは関係のない区間なので、回によるばらつきと考えられます。策2の効果と言えるのは、init engine の約2分37秒です。
一方で、生成の速さに影響が出ました。
最初の文字が出るまでの時間はほぼ同じでした。しかし、**そのあとの生成は約3.5倍遅くなりました。
** 今回の返答は9〜20トークンと短いので、1通あたりの差は約0.4秒です。返答が長いほど差は開きます。
また、起動後1回目の返答に14秒かかりました。起動で省いた準備が最初のリクエストに回ったのだと考えていますが、確認はしていません。
策3:重みの先読み(prefetch)
vLLM の警告に従って、先読みを強制する策です。3回起動して計測しました。
先読み自体は効いていました。ところが3回目は、2つ目のファイルの先読みに2分49秒かかりました。
Volume からの読み出しがその回だけ遅かったと考えています。
3回目を除くと先読みありは19秒前後で、先読みなしの速い回(20.5〜21.8秒)との差は1〜3秒でした。
→ 策3で起動が速くなったとは言えません。 この区間では、策の効果よりも読み出しの速さのぶれ(20〜193秒)のほうがはるかに大きかったからです。
策4:CUDA graph を取る大きさを絞る
上限を64に絞りました(--max-cudagraph-capture-size 64)。これで、CUDA graph を取る大きさは57通りから11通りに減ります。3回起動して計測しました。
大きさは約1/5になりましたが、時間は比例して減りませんでした。
策4なしでも18秒で済んだ回があり、この区間も回によるばらつきが大きいようです。遅い回が出なくなった可能性はありますが、3回では言い切れません。
生成の速さは変更前と変わりませんでした。
→ 起動時間への効果は、ばらつきに隠れて確認できませんでした。
策5:重みをイメージに含める
イメージのビルド時に重み(本体 48.1 GiB と投機的デコーディング用 0.81 GiB)を取得してイメージに置き、Volume から読まないようにしました。vLLM のログでも、重みのファイルシステムが 9P(Volume)から OVERLAY(イメージ)に変わったことを確認しています。
速くはなりませんでした。 速い回どうしで比べると、Volume のほうが数秒速いくらいです。
Modal のドキュメント(Storing model weights)にある「Performance is similar for the two methods」という記述と一致します。3回とも24〜32秒に収まり、Volume で出た37秒以上の回は出ませんでした。
ただ、3回だけでは遅い回がなくなったとまでは言えません。
運用面では、デプロイに約5分(299秒)かかるようになり、イメージは約50GBを超えます。
結果のまとめ
4. 1回の計測では策の効果を見分けられない
策1〜4の計測を通して見ると、同じ設定でも起動時間は区間ごとに大きくばらついていました。
vLLM が見た空きメモリは、89 GiB の回と 1017 GiB の回がありました。コンテナが性能の異なるマシンに置かれている可能性がありますが、確認はしていません。
起動全体の振れ幅が1分49秒〜5分43秒もあると、数十秒を縮める策の効果は1回や2回の計測では見えません。 策3・4・5の判断が「確認できなかった」になったのはこのためです。
ただし、策2の init engine(4.97秒)と、策1の復元(init engine 自体がない)は、ばらつきの下限(31.65秒)よりも明らかに短くなっています。
→ 区間そのものをなくす策は、ばらつきがあっても効果を見分けられました。
5. 結論:策1だけを採用
策1を選んだ理由
- ばらつきの主な原因である2区間を両方なくせる。 ばらつきが大きかったのは、重みの読み込み(18.70〜193.34秒)と
init engine(31.65〜177.39秒)の2区間でした。復元時には重みはスナップショットの CPU メモリにあり、init engineはスナップショットを取る前に済んでいます。ほかの策は、どちらか一方を少し短くするだけでした - 短くなった幅がばらつきより大きい。 復元から応答できるまでが1分51秒で、変更前(1分49秒〜5分43秒、中央付近は4〜5分)と比べて、ばらつき以上に短くなりました
- 生成の速さが落ちない。 コンパイル結果と CUDA graph もスナップショットに含まれます
ほかの策を入れなかった理由
おわりに
「重いモデルだから起動が遅い」と思い込みがちですが、計測すると、重みの読み込みは全体の 15% にすぎませんでした。時間の大半は、vLLM が応答を受け付ける前の初期化にありました。そして、その初期化を毎回やり直さずに済ませる GPU メモリのスナップショットが、最も効果のある策でした。
もう一つの学びは、クラウドの GPU コンテナの起動時間は回ごとに大きくぶれるということです。数十秒を縮める策の効果は、1〜3回の計測ではばらつきに埋もれてしまいます。策を比べるときは、計測を重ねるか、ばらつきよりはっきり大きい差が出る策を選ぶ必要があります。
出典
公式情報
- Modal:Memory Snapshots / Storing model weights
- Modal 公式の例:vllm_low_latency.py(modal-labs/modal-examples リポジトリ)
本記事の数値は上記の計測条件での当社実測です。GPU の割り当て先のマシン、Volume の状態、vLLM や Modal のバージョンなどによって変動します。