NTTドコモR&Dの技術ブログです。

AWS T4インスタンスでプライベートLLMはどこまで通じる?Locust負荷試験で見えた「性能の限界」

はじめに

はじめまして。サービスイノベーション部の藤本 啓輔と申します。

普段の業務では、グラフ理論やLLM(大規模言語モデル)を活用した研究・開発を行っています。

最近ではシステム開発のフェーズにも携わっており、本番稼働を見据えた「負荷試験」を行ったり、リリースに向けたコスト削減の観点から、OpenAI等のAPIだけでなく「OSS(オープンソース)のLLMモデル」の自社ホスティングにも着目したりしています。

今回は、それら2つの関心事を組み合わせ、「AWS上の安価なGPUインスタンスで、OSSモデルは実用的な速度で動くのか?」を検証した記録を共有します。

負荷試験とは

概要

システムに対して擬似的に高い負荷をかけ、性能の限界やボトルネック、エラー発生時の挙動を確認するテストのことです。特にLLMを用いたシステムは、通常のWebシステムと比較してレスポンス生成に時間がかかる(GPU計算リソースを大量に消費する)ため、事前の検証が不可欠です。

今回使ったツール:Locust

今回は、Python製のオープンソース負荷試験ツール Locust1 を採用しました。

選定理由:

  • Pythonでシナリオが書ける: 普段の開発言語と同じPythonで記述できるため、柔軟なテストシナリオが作成可能です。
  • LLM特有の挙動を再現しやすい: 今回は「短い質問」「小説の執筆」「コード生成」といった負荷の異なるリクエストをランダムに投げ分けたり、ユーザーの思考時間(Wait time)を設けたりすることで、実際のチャット利用に近い負荷を再現しました。

オープンソースLLMとは

概要

ChatGPT (OpenAI) や Claude (Anthropic) のようなプロプライエタリなモデルに対し、モデルの重みデータが公開され、誰でも自由に(ライセンスの範囲内で)利用できるLLMのことです。 OpenAIが公開した「gpt-oss-20b2や、Alibabaの「Qwen 2.53など、近年急速に性能が向上しており、特定のタスクであれば商用APIに匹敵する性能を出せるようになっています。 自社環境で動かすことで、「データプライバシーの確保」や「推論コストの固定化(削減)」が期待できます。

(補足)Amazon Bedrock との比較

AWSには、インフラ管理不要でOSSモデル利用できるマネージドサービス Amazon Bedrock4 も存在します。 通常、実運用では運用負荷の低いBedrockが第一候補となりますが、今回は以下の理由からあえてEC2でのセルフホスティングを選択しました。

  • 最新・特定モデルの利用: Bedrockでサポートされる前の最新モデルや、特定の量子化バージョン(今回のAWQなど)を柔軟に試すため。
  • コスト構造の違い: Bedrockは「トークン従量課金」ですが、EC2(特にスポットインスタンス)は「時間課金」です。高負荷で回し続ける検証や、定額で運用したいケースでのコストメリットを検証するため。

今回使ったモデル:Qwen2.5-7B-Instruct-AWQ

今回は、日本語性能に定評のある Qwen2.5-7B-Instruct5 を採用しました。 ただし、検証環境の制約(後述のT4 GPU / VRAM 16GB)に合わせるため、AWQ (4bit量子化)6 された軽量モデルを使用しています。

なぜ量子化モデルか?:

  • 通常のFP16(半精度)モデルだと、7Bクラスでもモデル展開だけで約15GBのVRAMを消費し、推論用のメモリが足りずにエラー(OOM)となります。
  • AWQ形式であれば約6GB程度まで圧縮できるため、T4 GPUでも余裕を持って動作します。

実験

アーキテクチャ

コストパフォーマンスを重視し、AWS上で最も安価に利用できるGPUインスタンスの一つであるg4dn.xlargeを使用しました(EC2インスタンスA)。

アーキテクチャ図

実験は上図の通り、大きく3つのフェーズで行いました。

  • ① モデルロード Private SubnetにあるGPUインスタンスから、NAT Gatewayを経由してHugging Face7(インターネット)上のモデルデータをダウンロード・ロードします。

  • ② サーバ起動 GPUインスタンス上で推論エンジン(vLLM8)を立ち上げ、APIサーバとして待機させます。

  • ③ 負荷実行 同じVPC内に立てた負荷試験用インスタンス(Locust)から、Private IP経由で大量のリクエストを送信し、性能を計測します。

開発環境 (VS Code + Session Manager)

今回はセキュリティを考慮してインスタンスをPrivate Subnetに配置したため、直接のSSH接続(ポート22)は許可していません。 代わりに、AWS Systems Manager (Session Manager)9を利用して接続しました。

開発効率を上げるため、ローカルPCのVS CodeRemote - SSH拡張機能を導入し、SSM経由でインスタンスに直接リモート接続してコーディングやコマンド実行を行いました。これにより、セキュアな環境下でも快適な開発体験が得られました。

推論エンジンの設定 (vLLM)

推論には高速なライブラリである vLLM を使用しましたが、古いT4 GPUで動かすためにチューニングが必要でした。 最新のvLLMは新しいGPU向けに最適化されており、そのままではT4で動作しませんでした。最終的に以下のコマンドで、バックエンドを TRITON_ATTN に指定し、安定性重視の Eager Mode で起動することで動作に成功しました。

VLLM_ATTENTION_BACKEND=TRITON_ATTN \
python3 -m vllm.entrypoints.openai.api_server \
  --model Qwen/Qwen2.5-7B-Instruct-AWQ \
  --quantization awq --gpu-memory-utilization 0.90 \ 
  --max-model-len 4096 --port 8000 --host 0.0.0.0 \
  --enforce-eager

テストシナリオ

実際の利用シーンでは、軽い質問もあれば、重い生成タスクもあります。 今回はより現実に近い負荷を再現するため、以下の3種類のプロンプト(Short / Creative / Coding)をランダムに送信する mixed シナリオを作成しました。

パターン 役割 プロンプト例 負荷 (Max Tokens)
Short アシスタント ・「10 + 5 はいくつですか?」
・「日本の首都は?」
・「HTTPプロトコルを一言で説明してください。」
低 (60 tokens)
Creative 小説家 ・「宇宙を旅する猫の冒険物語を、200文字程度で書いてください。」
・「静かな海辺の夕暮れについて、感動的な詩を書いてください。」
・「100年後の未来の東京の様子を、SF風に詳しく描写してください。」
高 (300 tokens)
Coding プログラマー ・「フィボナッチ数列を計算するPython関数を書いてください。」
・「Dockerコンテナの仕組みを、初心者にわかるように説明してください。」
・「ユーザーテーブルから、20歳以上の会員を抽出するSQLクエリを書いて。」
中 (250 tokens)

コード抜粋

    # mixedの場合、ランダムに選択
    if pattern == "mixed":
        pattern = random.choice(["short", "creative", "coding"])

    messages = []
    max_tokens = 100
    temperature = 0.7

    # --- シナリオ定義 (日本語プロンプト) ---
    
    if pattern == "short":
        # 負荷: 低 (短い入力、短い出力)
        # 簡潔な回答を求めることで、素早いレスポンスをテストします
        system_content = "あなたは簡潔に答えるAIアシスタントです。"
        user_content = random.choice([
            "10 + 5 はいくつですか?",
            "日本の首都はどこですか?",
            "HTTPプロトコルを一言で説明してください。"
        ])
        max_tokens = 60
        temperature = 0.1

    elif pattern == "creative":
        # 負荷: 高 (中程度の入力、長い出力)
        # 創作的な文章生成を求め、GPUに高い計算負荷をかけます
        system_content = "あなたは創造力豊かな小説家です。"
        user_content = random.choice([
            "宇宙を旅する猫の冒険物語を、200文字程度で書いてください。",
            "静かな海辺の夕暮れについて、感動的な詩を書いてください。",
            "100年後の未来の東京の様子を、SF風に詳しく描写してください。"
        ])
        max_tokens = 300
        temperature = 0.9

    elif pattern == "coding":
        # 負荷: 中 (論理的思考)
        # コード生成や技術解説を求めます
        system_content = "あなたは優秀なプログラマーです。"
        user_content = random.choice([
            "フィボナッチ数列を計算するPython関数を書いてください。",
            "Dockerコンテナの仕組みを、初心者にわかるように説明してください。",
            "ユーザーテーブルから、20歳以上の会員を抽出するSQLクエリを書いて。"
        ])
        max_tokens = 250
        temperature = 0.2

ユーザー挙動のシミュレーション (思考時間)

実際のチャットサービス利用時、ユーザーはAIからの回答を受け取った後、それを読んだり次の質問を考えたりする時間(Think Time)を挟みます。 今回はマシンガンのようにリクエストを連射するのではなく、より現実に即した負荷を再現するため、各リクエストの間に 1秒〜5秒のランダムな待機時間 を設定しました。

負荷試験の設定

T4 GPU 1枚の限界を探るため、以下のパラメータで実施しました。 一気に負荷をかけるのではなく、1秒ごとにユーザーを増やしていくことで、「どの時点で遅延が発生し始めるか」を観測します。

  • Number of users (最大ユーザー数): 50
  • Ramp up (Spawn rate): 1 user / sec (1秒に1人増員)
  • Run time: 3分間

結果

実験1

  • スループット: 秒間 約2.5リクエスト (RPS) で頭打ちとなりました。これがT4 GPU 1枚の物理的な限界です。
  • レイテンシ: 同時接続数が 10〜15ユーザー を超えたあたりから、応答待ち時間が直線的に増加しました。50ユーザー時点では30秒以上の待ちが発生しました。
  • 安定性: ただし、高負荷状態でもサーバーエラー(500系)は一度も発生せず、リクエストを順番に処理し続けました。

レスポンス内容の確認(定性的評価)

負荷試験中、単にステータスコード(200 OK)を確認するだけでなく、実際にAIが適切な日本語で応答しているかもログとして記録しました。 以下は、負荷試験中に取得された実際のレスポンスの抜粋です。

1. Short パターン(短い質問) 単純な知識や計算問題に対し、的確かつ高速に応答しています。

Request: 10 + 5 はいくつですか?
Response: 10 + 5 は 15 です。 (0.42s)

Request: 日本の首都はどこですか?
Response: 日本の首都は東京です。 (0.32s)

2. Creative パターン(創作) 最も負荷が高いシナリオですが、文脈の通った長い物語を生成しています。

Request: 宇宙を旅する猫の冒険物語を、200文字程度で書いてください。
Response:
星々が織りなす無限のマトリックスの中を、チアシさんが軽快にジャンプを繰り広げた。彼女の毛並みは夜空と同調し、まるで星が落ちてくるかのようだ。小さな宇宙船「フリーダム」はチアシの冒険をサポートし、彼女が新しい惑星を発見したり、謎の宇宙信号に挑戦したりする度に、驚きと喜びを運んでいく。チアシの冒険はまだ始まったばかりで、無数の未知の世界へと続く可能性は広大な星空そのものだ。
(5.21s)

3. Coding パターン(コード生成) 論理的な思考が必要なタスクでも、SQLやPythonのコードを正しく生成できています。

Request: ユーザーテーブルから、20歳以上の会員を抽出するSQLクエリを書いて。
Response:
当然、以下は一般的なSQLクエリで、20歳以上の会員を抽出するものです...
SELECT * FROM ユーザーテーブル WHERE 年齢 >= 20;
(7.61s)

考察: ログを見ると、Short パターンは 0.3〜0.8秒 と非常に高速に返答している一方、CreativeCoding パターンでは 5秒〜30秒 近くかかっているケースが見受けられます。

特に負荷が高まった試験後半(ログの下部)では、生成時間の長さに加えてキューイング(待ち行列)による待機時間も加わり、トータルの応答時間が伸びていたことが読み取れました。それでも、生成されたテキスト自体は破綻しておらず、高負荷下でもモデルが正常に動作していたことが確認できます。

追加実験1:実用的な限界点の検証

前述の実験では「待てばエラーにならない」ことが分かりましたが、実際のチャットボット運用では「回答まで30秒も待てない(タイムアウトさせたい)」というケースが一般的です。 そこで、「15秒以内に応答がなければエラー(失敗)」 という厳しい条件を設定し、実用的な同時接続数の限界を探ってみました。

  • 変更点:
    • Locustのタイムアウト設定: 120秒15秒

追加実験1

結果と考察:

  • レイテンシの天井: グラフ中段の紫線(95th percentile)を確認すると、応答時間が 15,000ms (15秒) で完全に頭打ち になっており、システム側のタイムアウト設定による強制切断が示唆されます。
  • 実用限界: ユーザー数が 15〜20人 を超えたあたりから応答時間が急激に悪化(数秒〜10秒以上)し始めました。その後、ユーザー数が 35人 前後に達した時点で応答時間が上限の15秒に張り付き、上段の赤線(Failures)が発生しています。
  • 結論: エラーが発生するのは35人以降ですが、それ以前から大幅な遅延が生じています。この構成(T4 GPU x 1)において、ユーザーにストレスを与えない応答速度を維持できる実質的な定員は、約15名 であると判断できます。

追加実験2:過負荷耐性の検証(50人 vs 100人)

システムの限界挙動をより深く理解するため、ユーザー数を50人から100人へ、Ramp-up(増員速度)も5倍に高めた「ストレステスト」を行いました。

  • 変更点:
    • ユーザー数: 50人100人
    • Ramp-up (Spawn rate): 1 user/sec5 user/sec (一気に負荷をかける)

追加実験2(左図:50人、右図:100人)

  • スループットの頭打ち (Compute Bound): ユーザー数を倍にしても、処理数(RPS)は 2.5 req/s から変化しませんでした。これにより、ボトルネックはネットワークやメモリ帯域ではなく、T4 GPUの計算能力そのもの にあることが確定しました。
  • 待ち行列の法則: ユーザー数が2倍(50→100)になると、待ち時間もきれいに約2倍(36秒→75秒)になりました。vLLMが溢れたリクエストを完全なFIFO(先入れ先出し)キューとして処理していることが分かります。
  • システムの堅牢性: 100人から一気にアクセスが来ても、システムはクラッシュせず、エラーゼロで稼働し続けました。応答速度は実用的ではありませんが、「決して落ちない」 というバックエンドとしての信頼性は確認できました。

まとめ

今回の検証を通じて、「月額数万円程度の安価なインスタンスでも、工夫次第で実用的なLLM APIが構築できる」 ことが確認できました。 特に Qwen2.5 のAWQモデルは非常に軽量かつ高性能で、T4 GPUでもサクサクと日本語を生成してくれました。

一方で、1台構成では「同時接続10人」程度が快適に使える限界であることも分かりました。より大規模なサービスに適用する場合は、上位のGPU(A10Gなど)を検討するか、Auto Scalingによる並列化が必要になります。

環境構築では、CUDAのバージョンやライブラリの依存関係(PyTorchとvLLMの相性など)に苦戦しましたが、その分、OSSモデルを動かすための低レイヤーな知識が身についた良い機会となりました。今後はRAG(検索拡張生成)10と組み合わせた場合の負荷についても検証していきたいと思います。