Agentic OS 技術スタックを下から読む 第19回:コールドスタート ―― 推論を「本当にサーバーレス」にする四段の早回し
第1回から第3回では、推論コストの中心にある単純な事実を見た。GPU は高い。だから、確保した GPU を遊ばせると、そのまま損になる。
GPU を遊ばせない、の裏側
第1回から第3回では、推論コストの中心にある単純な事実を見た。GPU は高い。だから、確保した GPU を遊ばせると、そのまま損になる。
しかし、推論の需要は、訓練のように自分で計画しやすいものではない。ある時間にまとめて実行する、と決められる仕事ではない。市場の動き、SNS の流れ、プロダクト内の導線、外部イベント、ユーザーの生活時間によって、外から急に山が来る。
この山に備えて、ピークに合わせた GPU を常に固定で持つと、ほとんどの時間は余る。実際、確保した GPU 秒のうち、アプリケーションのコードが本当に動いている割合、つまり割当利用率は、多くの組織でピーク時でも七割を切る。ふだんは一割から二割ほどまで落ちる。
自然な解は、必要なときだけ立ち上げ、終わったら畳むことだ。いわゆるサーバーレスである。
ただし、ここに大きな条件がある。需要は秒単位で動く。ならば、新しい推論サーバーの複製も、秒単位で立ち上がらなければならない。
ふつうに大きなモデルの推論サーバーを一台立ち上げると、準備に数十分かかる。具体的には、およそ二千秒かかることがある。これを約五十秒まで縮める。およそ四十倍である。
魔法の一手ではない。立ち上げを四つの段に分け、それぞれを別の仕組みで早回しする。
一段目は、機械を確保して健康かを確かめる段である。数分から数十分かかる。
二段目は、コンテナの画像とファイルを読み込む段である。数分かかる。
三段目は、ホスト、つまり CPU 側でアプリケーションを起動し、最初の要求を受けられる状態にする段である。数十秒かかる。
四段目は、デバイス、つまり GPU 側で推論エンジンを起動し、要求を受けられる状態にする段である。ここは数分から数十分かかる。
この四段を、それぞれ別の方法で潰す。
一段目:緩衝池で、確保をホットパスから外す
最初の遅さは、GPU を確保するところにある。
要求が来た。新しい複製が必要になった。そこから GPU を探す。空いている機械を割り当てる。健康かどうかを確認する。必要なら初期化する。この順で進めると、もう遅い。数分から数十分が、ユーザーの待ち時間に乗ってしまう。
だから、この仕事を要求の前に済ませておく。
健康で、空いている GPU を、少しだけ余分に立てておく。この余分な待機分を、ここでは緩衝池と呼ぶ。緩衝池とは、急な需要や故障を吸収するために、あらかじめ用意しておく余白のことである。
新しい推論サーバーの複製が必要になったら、まずこの緩衝池から空き GPU を取る。要求が来てから確保するのではない。すでに確保済みで、健康確認も済んだ GPU に、すぐ割り当てる。
池の水位が下がったら、裏で非同期に補充する。つまり、ユーザーの要求を待たせない経路から、確保と健康チェックを外す。
ここで重要なのは、この緩衝池を一つのアプリケーション専用にしないことだ。多数のアプリケーションで共有する。あるアプリケーションでは需要が落ち、別のアプリケーションでは需要が跳ねる。個別に余白を持つと無駄が大きいが、共有すれば、必要な余白は小さくなる。
池の大きさは勘で決めない。線形計画で決める。線形計画とは、最小化したい費用と、満たすべき制約を、数式の形にして解く最適化の方法である。
入力になるのは、各地域や各供給元の価格だけではない。実際に観測した供給も入れる。広告されている価格が安くても、その地域に在庫がなければ意味がない。安いが取れない GPU は、計画上は安く見えても、実行時には山を吸収できない。
緩衝池は、ピーク利用率を百パーセント未満に抑える。これは一見すると損に見える。高い GPU をあえて少し空けておくからである。
しかし、百パーセント利用は幻想である。余裕がゼロなら、小さな故障がそのまま障害になる。ある一台が落ちる。ある地域の供給が遅れる。健康チェックで異常が出る。そのたびに、すぐユーザーの待ち時間へ跳ね返る。
CPU やディスクでも同じである。利用率が高くなりすぎたら複製を増やす。八割、九割を超えた状態を常態化させない。GPU だけが例外ではない。
むしろ GPU は、故障を前提にしたほうがよい部品である。高負荷で長く動き、メモリも広く、ドライバや電源や冷却の影響も受ける。だから、起動時には短い健康チェックをかける。起動後も継続して監視する。重い診断は、毎回の起動に入れない。週に一度のような遅い周期に回す。
一段目でやっていることは単純である。遅い確保を、要求が来る前に済ませる。故障しそうなものを、要求の前で弾く。需要の山に対して、少しだけ水を張った池を置く。
これだけで、数分から数十分の不確実な待ち時間を、ホットパスから外せる。
二段目:怠惰な、内容アドレス指定のファイル提供
次の遅さは、コンテナ画像である。
推論サーバーのコンテナ画像は大きい。数万のファイルがあり、全体では数ギガバイトになる。言語処理系、計算ライブラリ、補助ツール、地域情報、設定ファイル、共有ライブラリが入っている。
しかし、起動直後にその全部が必要になるわけではない。世界中のタイムゾーン情報や地域情報のように、画像の中には入っているが、その起動では一度も読まれないファイルが大量にある。
ならば、起動時に全部を読む必要はない。
ここで使うのが、怠惰なファイル提供である。怠惰とは、必要になるまで実行しないという意味である。起動を止めて読むのは、目次だけにする。目次とは、ファイル名、サイズ、権限、配置といったメタデータである。これは数メガバイト程度で済む。読み込みは百ミリ秒以下で終わる。
中身は、起動と並行して読む。あるいは、本当にアクセスされたときだけ読む。
さらに、ファイルを置き場所ではなく中身で参照する。これを内容アドレス指定と呼ぶ。内容アドレス指定とは、ファイルのパスや保管場所ではなく、その中身から計算したハッシュ値でファイルを参照する方法である。
なぜ効くのか。
多数のコンテナは、同じ部品を持っている。同じ言語処理系。同じ計算ライブラリ。同じ補助バイナリ。同じ共有ライブラリ。パスは違っても、中身は同じであることが多い。
置き場所で参照すると、同じ中身でも別物として扱われる。内容で参照すると、同じ中身は同じものとして扱える。一度キャッシュした部品を、別のコンテナでも使える。重複して保存しない。重複して読まない。
このキャッシュは階層になっている。
一番速いのは、メモリ上のページキャッシュである。ページキャッシュとは、ディスクから読んだファイルの内容を、次にまた使うためにメモリ上へ残しておく仕組みである。待ち時間はおよそ百万分の一秒から十万分の一秒で、帯域は毎秒十から四十ギガバイトほど出る。
次がローカルの SSD である。待ち時間はおよそ十万分の一秒、帯域は毎秒四ギガバイトほどである。
その次に、同じ地域の中継サーバーがある。待ち時間はおよそ千分の一秒、帯域は毎秒十ギガバイトほどである。
さらに遠いところに、地域をまたぐ配信網がある。待ち時間はおよそ十分の一秒になる。
最後に、巨大な塊置き場がある。これは容量が大きいが、待ち時間はおよそ五分の一秒になる。
容量が大きくなるほど遅い。だから、よく使う部品を、できるだけ上の階層に置く。上の階層に当たれば、起動は速い。外しても、必要な分だけ下の階層から持ってくる。
細部も効く。
たとえば先読みである。先読みとは、要求された場所だけでなく、その先の連続した範囲もまとめて読んでおく仕組みである。コンテナ画像の読み込みでは、大きな連続読みが多い。初期値の百二十八キロバイトだけ読むと、何度も小さく取りに行く。これを三十二メガバイトに上げると、一回の読み込みで先まで取れる。画像読み込みのような連続アクセスでは効く。
ただし、先読みを大きくすればよいわけではない。数ギガバイトまで上げると、読まれないデータまで大量に取る。無駄な帯域を食い、キャッシュを汚し、逆に遅くなる。三十二メガバイト程度は、連続読みをまとめるには大きく、全体を無駄に読むほどではない、という釣り合いである。
もう一つの細部は、圧縮展開を飛ばすことだ。
よく使われる圧縮方式は、本質的に一本道である。つまり、複数の CPU コアで好きなだけ並列に伸ばせるわけではない。展開速度は毎秒百メガバイト程度に落ちることがある。
これは遅い。ページキャッシュより遅い。ローカル SSD より遅い。同じ地域の中継サーバーより遅い。せっかくキャッシュ階層を速くしても、最後に圧縮展開が毎秒百メガバイトで詰まれば、そこがボトルネックになる。
だから、起動時に毎回圧縮展開しない形にする。展開済みの中身を、内容アドレス指定で扱う。重複はハッシュで消えるので、展開済みだからといって無制限に膨らむわけではない。
この二段目で、コンテナ起動から約一分を削れる。
三段目:CPU 側の状態を、スナップショットで早送りする
コンテナが見えるようになっても、アプリケーションはまだすぐ働けない。
プロセスを起動する。言語処理系を立ち上げる。計算ライブラリを読み込む。設定を読む。共有ライブラリを結びつける。初期化コードを走らせる。最初の要求を受けられる形にする。
ここで数十秒かかる。
見た目は一行でも、中では大量の仕事が走っている。よくある計算ライブラリを読み込む一行が、内部では数千行のコードを実行し、数万回のシステムコールを呼ぶことがある。システムコールとは、プロセスがファイル、メモリ、スレッド、ネットワークなどについて、基本ソフトウェア側へ依頼する呼び出しである。
この段を速くする鍵は、同じ初期化を毎回やり直さないことだ。
動いているプロセスを突き詰めると、いくつかの状態に分けられる。ヒープ、つまりプロセスが使っているメモリ。実行中または待機中のスレッド。開いているファイル記述子の表。ファイル記述子とは、プロセスが開いているファイルやソケットを識別する小さな番号である。
これらをまるごと保存する。これをチェックポイントと呼ぶ。チェックポイントとは、ある時点の実行状態を保存し、あとでその地点から再開できるようにする仕組みである。
復元は、ゼロからの実行とは違う。数千行の初期化コードをもう一度走らせない。数万回のシステムコールをもう一度呼ばない。保存しておいたメモリを戻し、スレッドの状態を戻し、ファイル記述子の表を戻す。
実装上は、ページの生データを一つのファイルに固める。ページとは、メモリを一定の大きさに区切った単位である。保存ファイルの大きさは、数百メガバイトから数ギガバイトになる。ただし、システムメモリを超えることはない。実際にプロセスが持っているメモリ状態を保存しているだけだからである。
この方法で、ホスト側の準備はおよそ十倍速くなる。
ただし、注意がある。
チェックポイントは、ホストの環境に敏感である。ある機種で作った保存を、別の機種でそのまま復元できるとは限らない。たとえば、保存を作った機種には特殊な命令があり、復元先の機種にはその命令がない場合がある。プロセスは、復元後にその命令を実行しようとして落ちる。
クラウドでは、同じように見える機械でも、内部の世代や命令の対応が混じっていることがある。だから、一つの推論サーバーに対して、一つの保存だけを持つのは危ない。複数の機種に合わせて、複数のチェックポイントを用意する必要がある。
三段目でやっていることは、CPU 側の時間を飛ばすことである。毎回初期化するのではない。初期化済みのプロセスを保存しておき、そこから戻す。
四段目:GPU 側の状態も、スナップショットで早送りする
一番重いのは、GPU 側である。
ここには遅い仕事が二つある。
一つは、モデルの重みを GPU のメモリへ読み込むことだ。重みとは、モデルが学習で得た大量の数値である。サイズは、数ギガバイトから数テラバイトになる。これを置き場所から読み出し、GPU のメモリに載せる。
毎秒数ギガバイトで読めたとしても、数ギガバイトなら数秒、数百ギガバイトなら数十秒、数テラバイトなら数百秒かかる。これは、保存と復元では根本的には消せない。飛ばせる初期化ではなく、大きなデータを運ぶ帯域そのものが律速だからである。
もう一つは、推論エンジンの準備である。
推論では、ただ重みを置くだけでは足りない。計算の手順を、実行時に速く回せる形へ固める必要がある。たとえば、計算の手順をひとまとめに記録したものを作る。これは、毎回その場で組み立てると遅いので、あらかじめ捕まえておく。また、実行するコードを、その機械で速く動くように最適化する。
この準備に、数十秒から数分かかる。
やっかいなのは、この GPU 側の状態が、普通のファイルのように保存しやすい形ではないことだ。計算手順の記録は、メモリ上のテンソルやカーネルへのポインタの塊である。テンソルとは、多次元の数値配列である。カーネルとは、GPU 上で実行される小さな計算単位である。ポインタとは、メモリ上の場所を指す値である。
ポインタは、その時点のそのメモリ配置に依存している。だから、そのままディスクに書いて、別の起動で戻せばよい、とはならない。
ここで使うのが、GPU 側のチェックポイントである。
新しい世代のドライバは、デバイス、つまり GPU のメモリを、いったんホスト、つまり CPU 側のメモリに退避できる。退避とは、GPU 側にある状態を、CPU 側から扱える場所へ一時的に移すことである。
そうすると、三段目で使ったホスト側の保存の仕組みが、その退避された GPU メモリも含めてディスクに保存できる。
復元時は、まずホスト側のプロセスを戻す。次に、ドライバが退避してあったデバイス側の状態を GPU メモリへ書き戻す。これで、推論エンジンの準備の多くを飛ばせる。
効果は大きい。四倍から十倍ほど速くなる。数分かかっていた GPU 側の準備が、数十秒に縮む。
ただし、ここにも注意が多い。
まず、複数 GPU は難しい。複数の GPU で一つの推論を動かすと、GPU 間で集団通信が走る。集団通信とは、複数の GPU が同期しながらデータを交換する通信である。この通信は、一時停止と復元を前提に作られていないことが多い。一台が黙ると、他の GPU が待ち続けて行き詰まる。
次に、重みは保存の前にいったんホストへ戻しておくとよい。重みは巨大で、GPU メモリ上に置いたまま保存すると扱いづらい。どのみち重みは大きなデータであり、帯域が支配する。初期化として飛ばせる部分と、転送として避けられない部分を分ける必要がある。
さらに、過去文脈の置き場は、保存から戻さないほうが速いことがある。過去文脈の置き場とは、前に読んだ入力に対応する中間状態のキャッシュである。これは推論を続けるときには重要だが、新しい複製の起動時には空でよい。保存から戻すより、空のまま作り直すほうが速い。
四段目の本質は、GPU 側にも「初期化済みの状態」を持たせることだ。ただし、巨大な重みの転送そのものは消えない。飛ばせるのは、推論エンジンの準備、計算手順の記録、最適化済みコード、GPU メモリ上の実行状態である。
ここを切り分けないと、「全部保存すれば速くなる」という雑な理解になる。実際には、帯域で決まる部分と、初期化で決まる部分を分け、後者をスナップショットで飛ばしている。
積み重なって、四十倍になる
この四段は、独立した小技の寄せ集めではない。下から積み重なる。
GPU 側のチェックポイントは、ホスト側のチェックポイントの上に乗る。ホスト側のチェックポイントは、怠惰なファイル提供の上に乗る。怠惰なファイル提供は、緩衝池で確保済みの健康な機械があることを前提にする。
一段目で、機械の確保と健康チェックを要求の前に出す。
二段目で、コンテナ画像の全読みをやめる。目次だけで起動を進め、必要なファイルだけを、内容アドレス指定のキャッシュ階層から読む。
三段目で、CPU 側の初期化をやり直さない。初期化済みプロセスをチェックポイントから戻す。
四段目で、GPU 側の推論エンジン準備をやり直さない。デバイスメモリを退避し、復元で戻す。
この積み重ねで、数十分が数十秒になる。
小さなモデルでも効果は見える。一ギガバイトほどのモデルで、立ち上げの平均が約九十五秒から約十四秒へ縮む例がある。別の構成では、約八十四秒から約十七秒へ縮む。ある文書処理の現場では、約七十秒が約十二秒になった。およそ六倍である。
大きなモデルでは、差はさらに効く。ふつうに立ち上げると約二千秒かかる準備を、約五十秒に近づける。およそ四十倍である。
ここで大切なのは、数字の見方である。
約五十秒は、ゼロではない。完全な瞬間起動ではない。重みの転送のように、どうしても帯域に縛られる部分が残るからである。
しかし、約二千秒と約五十秒では、運用の意味がまったく違う。二千秒は三十分を超える。需要の山が来てから増やしても、山が去ったあとに到着する。五十秒なら、短い遅れを許せる設計、緩衝池、事前予測、少量の待機複製と組み合わせて、実際の増減制御に使える。
サーバーレスが机上の言葉から、推論の実装になる境目はここにある。
Agentic OS への含意
エージェントの実行は、需要が読みにくい。
人間の操作だけではない。別のエージェントから呼ばれる。スケジュールで走る。外部イベントで起動する。検索、要約、計画、コード実行、文書処理、画像処理のように、呼ぶ道具も多い。一回ごとに重い推論を呼ぶこともある。
だから、常に最大需要ぶんの GPU を固定で抱えると、すぐに経済性が壊れる。必要なときだけ立ち上げ、終わったら畳むほうが自然である。
しかし、「秒で立ち上がる」は願いでは手に入らない。
確保を前出しする緩衝池がいる。コンテナ画像を全部読まない、怠惰な内容アドレス指定のファイル提供がいる。CPU 側の初期化を飛ばすチェックポイントがいる。GPU 側の推論エンジン準備を飛ばすチェックポイントがいる。
この四段が積み重なって、初めて推論サーバーは、需要に合わせて増減できる。
これは、第1回から第3回で見た推論コストの、もう一つの顔である。
確保した GPU を遊ばせない。そのためには、単にスケジューラを書くだけでは足りない。機械の確保、ファイルの読み方、プロセスの保存、デバイスメモリの復元まで、土台の細部を詰める必要がある。
上の層では、エージェントが自然に道具を呼んでいるように見える。だが、その下では、数メガバイトの目次、三十二メガバイトの先読み、数百メガバイトから数ギガバイトのチェックポイント、数ギガバイトから数テラバイトの重み、約五十秒の立ち上げ時間が、静かに積み上がっている。
Agentic OS の経済性は、この地味な層で決まる。
← 一覧へ