1500のAIエージェントを同時実行した結果得られた6つの数字
本稿では、単一のAWS c6i.metalインスタンス上で1500の完全KVM分離された仮想マシンを同時に実行し、AIエージェントタスクを行った際の主要パフォーマンス指標(起動時間、メモリ密度、DNSキャッシュ、ネットワークレイテンシ、TLSセッション再利用、スループット)を紹介し、軽量分離アーキテクチャの実際のコストと利点を示す。
本稿は連載の第4回目です。最初の3回で基礎を築きました:第1回では、ロード時にバイナリのすべてのシステムコールを書き換え、ゲストがランタイムをバイパスできないようにする方法を示しました。第2回では、ゲストカーネルを一切使わず、エージェントが実際に必要とする約30のシステムコールを処理するわずか36KBのシムで動作する理由を説明しました。第3回では、プールデーモンについて説明しました:VMの事前準備、接続の事前確立、ランタイムが各エージェントの経路に配置される仕組みです。
第4回は予定より遅れました。空白期間はライターズブロックではなく、実際の作業のためです。1500のVMを安定して動作させ、プールデーモンが実負荷下でTLSセッションを維持し、開発機からAWSの異なるCPUファミリに移したときだけ現れるスナップショットの移植性バグを追跡すること。この投稿が存在するのは、システムが今や信頼性高く測定できるほど安定したからです。
この投稿では、それらすべてを使って何ができるか、大規模実行の実際の姿を紹介します。
ここにあるすべての数字は、単一のAWS c6i.metal上で1500の完全KVM分離された仮想マシンを同時に実行した結果です。コンテナでも、カーネル共有プロセスでもありません。各エージェントは独自のVM内にあり、独自のハードウェア分離境界を持ちます。
私が答えようとした質問は、これに実際どれだけのコストがかかるのか、ということです。メモリ、レイテンシ、スループット——分離モデルによりこれらの一部が測定不能になるのか、それともどこでもコストを支払うことになるのか。
以下が運用ダッシュボードが示したデータです。
- ウォームVMの起動時間——0.42秒
何よりも先にプールが準備できていなければなりません。
1500のKVM VMを単純に起動する方法——fork、exec、boot、import——では、エージェントの実行ごとに約200msかかり、最初のジョブが実行されるまでに5分かかります。
私たちが行う方法:Pythonを完全なインポートシーケンスで一度実行し、その時点でVMをフリーズし、結果をスナップショットとして保存します。99MBの非ゼロページ——インタプリタ状態、ロード済みモジュール、事前解決されたインポート。プール起動時に、全1500のVMがそのスナップショットから並列にリストアします。物理ページはエージェントが書き込むまでVM間で共有されます。各エージェントはインポートコストを支払いません。
明確にしておくべきこと:このスナップショットにはPython標準ライブラリのみが含まれます。ユーザーレベルのインポート——anthropic、openai、requests、そしてエージェントが使うその他——は依然としてエージェント起動時に行われます。シーケンスの後半、それらのインポート後にスナップショットを撮り、ディスパッチ時間をさらに短縮することも可能です。そのトレードオフは、エージェントタイプごとに異なるスナップショットになり、フリート全体で共有される単一のユニバーサルPythonイメージではなくなることです。私たちはユニバーサルイメージを選択しました——0.42秒は十分に速く、1つのイメージは運用上よりシンプルです。
棒グラフは、1500のVMが100msの時間バケットにどのように分布したかを示しています。ほとんどは最初の2つのバケットに収まっています。合計壁時計時間:0.42秒。
ゲストカーネルはありません。シムは約36KB。他には何もありません。
- 実行密度——10.8エージェント/GB
これは最もよく聞かれる数字です。
1500のKVM分離VM——それぞれがLLM APIに対してアクティブなTLSコールを行っている——は、139GBの物理RAMに収まります。つまり、1GBあたり10.8エージェント、アイドル時ではなく、実負荷下でです。この数字には、エージェントが実行時に蓄積するすべて——Pythonヒープ、オープンTLSセッション、インフライトリクエストバッファ、スタックフレーム、システムコール状態が含まれます。
ps RSSはより高く読み取れます——全VMMプロセスを合計して約290GB。この数字は誤解を招きます:99MBのPythonスナップショットは全1500ワーカー間で共有されており、RSSアカウンティングは物理ページごとではなくプロセスごとにカウントします。ダッシュボードの139GBは物理メモリで、ホストから直接測定されています。
10.8エージェント/GBは実負荷下で測定されています。ウォームアイドルVMは3MB(上記)です。あなたの数字は、エージェントの動作や実行時に蓄積するヒープの量に応じて、これら2つの間のどこかに収まるでしょう。
- DNSキャッシュ——100%ヒット率、260,700ヒット
ネイティブLinuxプロセスはOSレベルのDNSキャッシュを取得します。そのプロセスを独自のカーネルを持つVMに移すと、通常はそれを失います——ゲストは独自のリゾルバを実行し、独自のアップストリームクエリを発行し、独自のレイテンシを支払います。
私たちのゲストにはカーネルがありません。DNSクエリはプールデーモンを通じて抜け出します。プールデーモンは、UDPパケットがホストを離れる前の経路上にあります。プールは全1500エージェント間で共有キャッシュを維持します——そのため、各ゲストはネイティブプロセスが得るものとまったく同じもの、そしてそれ以上を得ます:1回のアップストリームクエリがフリート全体にサービスを提供します。
50エージェントが同時に同じ名前をコールドミスした場合、1つのクエリが発行され、50すべてが応答を受け取ります。アップストリームリゾルバは、フリートサイズに関係なく、TTLウィンドウごとに名前ごとに1つのクエリしか見ません。
この連載を追ってきた方にはおなじみでしょう:プールが解析または処理できないもの——特殊なレコードタイプ、DNSSEC、非標準のもの——については、クエリはネイティブLinuxスタックにフォールスルーします。システムコール設計と同じ原則です。一般的なケースには高速パス、本当のものはバックストップとして。
100%ヒット率、260,700キャッシュヒット。この実行では、全1500エージェントが同じ2つのエンドポイント——api.anthropic.comとローカルのPostmanモック——にアクセスしていたため、キャッシュはすぐに飽和します。より多様なフリートではヒット率は低下しますが、結合の利点はエージェント数に応じて拡大します。分離はエージェントからDNSキャッシュを奪いませんでした——彼らはより良いものを得ました。
- 最初のネットワークコールまでの時間——p50 44ms、p99 105ms
エージェントコードは決定論的です。同じタスクは毎回同じ時点でネットワークにアクセスします。したがって、ディスパッチから最初のネットワークコールまでの時間はほぼ完全にインフラストラクチャ——エージェントが準備できるまで待機した時間であり、作業ではありません。
ディスパッチから最初のシステムコール: p50: 44ms p95: 86ms p99: 105ms n: 1500
接続から最初の送信(ウォームTLS): p50: 70ms p99: 1.6s n: 1500
ディスパッチの数字は純粋なインフラストラクチャオーバーヘッド——VM再開から最初のシステムコールまでです。接続の数字は、事前確立されたTLSセッションを介して最初のバイトがワイヤに到達するまでの時間です。接続のp99テールは、最初の送信前にAnthropicのレート制限に当たったエージェントを反映しています。
1つのジョブでは44msはノイズです。しかし1500エージェントが継続的に実行されている場合、これはインフラが測定可能かどうかの違いになります。
- TLSキャッシュ——338セッション保持
すべてのHTTPSコールは通常、TCPハンドシェイク(10-50ms)とTLSハンドシェイク(50-150ms)から始まります。1500エージェントすべてが同じエンドポイントにアクセスする場合、すべてのコールド接続で大きなレイテンシを支払うことになります。
プールデーモンは各エージェントと各アップストリーム宛先の間のネットワーク経路上にあります——つまり、TLSレイヤーも所有できます。
プール起動時に、デーモンは設定されたアップストリームエンドポイントへの永続的なTLSセッションを確立します。エージェントがHTTPSリクエストを行うと、リングバッファに平文を書き込みます。デーモンはエージェントに代わって、既に確立されたセッションでTLSを処理します。ハンドシェイクなし、ラウンドトリップなし。
フリート全体で338セッションを保持。1500エージェントが338のアップストリーム接続を共有。APIには338の接続が見えます。エージェントにはゼロのハンドシェイクレイテンシが見えます。
一つ率直に述べておくべきこと:プールデーモンはすべてのエージェントリクエストの平文を読み取ります。これは意図的です——同じ継ぎ目により、ポリシー実施、資格情報注入、応答検査が可能になります。これがあなたの求めるところかどうかは、脅威モデル次第です。設計上の選択であり、副作用ではありません。
- ネットワークスループット——受信33.2 MB/s、送信37.0 MB/s
このスナップショットの時点で、累計138,972回のLLMコールが完了——プール開始からの累積で、1500エージェントがそれぞれ約1秒あたり1コールを実行しています。
受信33.2 MB/s、送信37.0 MB/s。これらの数字はフリートとともに変動します——これは実行中のスナップショットであり、上限ではありません。1500エージェントでは、ランタイムはc6i.metalのネットワーク制限に遠く及びません。ここでの制約はLLM APIです——私たちの場合、Anthropicのレート制限。エージェントはほとんどの時間を待機に費やしており、バイトを移動しているわけではありません。
これらの数字が意味するもの
それらを合わせると、実行中のKVMフリートでのジョブのライフサイクルが得られます:
プールのウォームアップ:0.42秒で1500 VM、密度は10.8エージェント/GB
ジョブ到着:最初のネットワークコールまで44ms
エージェントがエンドポイントを解決:<1µs DNSキャッシュヒット、0ms TCP(事前確立)
エージェントがHTTPSコールを発行:0ms TLS——338セッション保持、エージェントは平文を書き込む
応答が戻る:フリート全体で持続33.2 MB/s
各層は同じように機能します:ランタイムはすでに経路上にあるため、DNS応答をキャッシュし、TLSセッションを保持し、スナップショットページを共有します。別個のシステムは必要ありませんでした。分離を実施するのと同じメカニズムが最適化を行っています。
KVM分離は重くなければならないわけではありません。これらの数字がその証拠です。
1500エージェントでは節約は測定可能です。10000になると、それらは構造的になります——より多くのマシンが必要かどうかの違いです。
次の投稿では、ランタイムがホスト上のすべてのエージェントにわたって、すべてのシステムコール、すべてのDNSクエリ、すべてのTLSセッション、すべてのモデルコールを同時に見ることができるようになると、何が可能になるかを扱います。
もしあなたが本番環境でエージェントを実行しており、これらのいずれかが実際の問題であるなら、LinkedInで連絡をください。