このガイドでは、LLM に関連する幅広いユースケースでレイテンシを改善するための基本原則を紹介します。ここで紹介する手法は、さまざまなお客様や開発者と本番環境のアプリケーションに取り組む中で得られたものです。個々の細かなワークフローからエンドツーエンドのチャットアプリケーションまで、構築するものを問わず活用できるはずです。
個別の手法は数多くありますが、このガイドではレイテンシ改善のアプローチを大きく分類し、 7 つの原則 にまとめています。
最後に、これらをどのように適用できるかを具体例で説明します。
7 つの原則
トークン処理の高速化
レイテンシの改善で最初に思い浮かぶのは、おそらく推論速度 でしょう。ただし、この後で説明するように、着目すべき点はほかにも数多くあります。推論速度とは、実際に LLM がトークンを処理する速さを指し、多くの場合、TPM(1 分あたりのトークン数)や TPS(1 秒あたりのトークン数)で測定します。
推論速度を左右する主な要因は モデルのサイズです。小さいモデルは通常、より高速かつ低コストで動作し、適切に使えば大きいモデルを上回る性能を発揮することもあります。小さいモデルでも高い品質を維持するために、次の方法を検討できます。
- より長く、詳しいプロンプトの使用
- フューショットの例の追加、または例の数の増加
- ファインチューニングや蒸留
OpenAI の予測出力機能など、推論を最適化する手法も利用できます。予測出力を使うと、コード編集タスクのように出力の大部分があらかじめわかっている場合に、生成時のレイテンシを大幅に削減できます。モデルに予測を与えることで、LLM は変わらない内容にかける処理を減らし、実際の変更箇所により多くの処理を集中できます。
推論速度には、利用可能な
計算リソースの量や、追加で適用する
推論最適化も影響します。
ほとんどの場合、これらの要因を直接変えることはできません。ただし、関心があり、
インフラをある程度調整できるなら、より高速なハードウェアの使用や、
エンジンの負荷を抑えた運用によって、
TPM を多少向上させられる可能性があります。さらに踏み込んで取り組むなら、ほかにも数多くの
推論最適化手法
がありますが、このガイドで扱う範囲を超えるため、ここでは説明を省きます。
生成トークン数の削減
LLM を使用する際、ほぼ常に最も時間がかかるのはトークンの生成です。一般的な目安として、 出力トークン数を 50% 削減すると、レイテンシも約 50% 削減できる可能性があります。出力サイズを減らす方法は、出力の種類によって異なります。
自然言語を生成する場合は、「20 語未満にしてください」や「簡潔にしてください」のように、 より簡潔に書くようモデルに依頼する と効果があるかもしれません。フューショットの例やファインチューニング、またはその両方を使って、より短い応答をモデルに学習させることもできます。
構造化出力を生成する場合は、できる限り 出力の構文を簡素化 してみてください。たとえば、関数名を短くする、名前付き引数を省略する、パラメーターをまとめるといった方法があります。
最後に、一般的な方法ではありませんが、max_tokens や stop_tokens を使って生成を早めに終了することもできます。
出力トークンを減らした分だけ、秒単位、ミリ秒単位の時間を節約できることを覚えておきましょう。
入力トークン数の削減
入力トークン数を減らすとレイテンシは下がりますが、通常、その効果はそれほど大きくありません。プロンプトを 50% 削減しても、レイテンシの改善は 1~5% にとどまる場合があります。文書や画像など、非常に大きなコンテキストを扱うのでなければ、ほかの改善に力を注ぐ方がよいでしょう。
とはいえ、 実際に 大きなコンテキストを扱っている場合、または性能を限界まで引き出したいと考えていて、 なおかつ ほかの選択肢をすべて試した場合は、次の手法で入力トークン数を減らせます。
- 長い指示や例を不要にするためのモデルのファインチューニング
- RAG の結果の絞り込みや HTML の不要部分の除去などによる、入力コンテキストのフィルタリング
- RAG の結果や履歴などの動的な部分をプロンプトの後半に置き、プロンプトの先頭にある共通部分を最大化します。これにより、多くの LLM プロバイダーが採用するKV キャッシュをリクエストで活用しやすくなり、リクエストごとに処理する入力トークン数を減らせます。
プロンプトキャッシュ の仕組みについて詳しくは、 ドキュメントをご覧ください。
リクエスト数の削減
リクエストのたびに通信の往復によるレイテンシが発生し、その時間は積み重なっていきます。
LLM に順番に実行させるステップがある場合、ステップごとにリクエストを送るのではなく、 1 つのプロンプトにまとめ、すべての結果を 1 回の応答で受け取ることを検討してください。通信の往復による追加のレイテンシを避けられ、複数の応答を処理する複雑さも軽減できる可能性があります。
具体的な方法として、各ステップを番号付きリストにして 1 つのプロンプトにまとめ、結果を JSON オブジェクトの名前付きフィールドに入れて返すようモデルに依頼できます。こうすれば、それぞれの結果をパースして参照できます。
並列化
LLM で複数のステップを実行する場合、並列処理が大きな効果を発揮することがあります。
各ステップを 厳密な順序で実行する必要が ない場合は、 個別の呼び出しに分けて並列に実行できます。シャツを 2 枚同時に干しても、乾くまでの時間は 1 枚のときと同じです。
一方、各ステップを 厳密な順序で実行する必要がある場合でも、 投機的実行を活用できる可能性があります。これは、モデレーションのように、特定の結果がほかの結果よりも高い確率で得られる分類ステップで特に有効です。
- ステップ 1 とステップ 2(たとえば、入力のモデレーションと物語の生成)を同時に開始
- ステップ 1 の結果を確認
- 予想した結果と異なる場合はステップ 2 をキャンセルし、必要に応じて再試行
ステップ 1 の予想が当たれば、実質的にレイテンシを増やさずにそのステップを実行できたことになります。
ユーザーの待ち時間の短縮
ただ待つ ことと、 処理が進む様子を見ることには大きな違いがあります。ユーザーが後者を体験できるようにしましょう。そのための手法をいくつか紹介します。
- ストリーミング: 待ち時間 を 1 秒以下に短縮できる、最も効果的なアプローチです。(ChatGPT で応答が完成するまで何も表示されなかったら、使い心地は大きく変わるはずです。)
- チャンク分割:出力をユーザーに表示する前にモデレーションや翻訳などの追加処理が必要な場合は、一度にすべてを処理するのではなく、 チャンク単位で処理 することを検討してください。バックエンドでストリーミングを受け取り、処理済みのチャンクをフロントエンドに送ることで実現できます。
- 処理ステップの表示:複数のステップを実行している場合やツールを使用している場合は、その状況をユーザーに示しましょう。実際の進捗を詳しく示せるほど効果的です。
- 読み込み状態の表示:スピナーやプログレスバーも大いに役立ちます。
処理ステップや読み込み状態の表示 は主に心理的な効果をもたらしますが、 ストリーミングやチャンク分割 は、アプリとユーザーを合わせたシステム全体で考えると、 実際にレイテンシを削減します。ユーザーが応答を読み終えるまでの時間が 短くなるためです。
LLM 以外の選択肢も検討
言語モデルは高性能で汎用性が高いため、 より高速な従来の手法 が適している場面でも使われることがあります。こうしたケースを見つけることで、レイテンシを大幅に削減できる可能性があります。次の例を参考にしてください。
- ハードコーディング: 出力 の内容が厳しく限定されている場合、その生成に LLM は不要かもしれません。操作の確認、拒否メッセージ、定型的な入力を求めるメッセージなどは、いずれもハードコーディングに適しています。(それぞれにいくつかの言い回しを用意するという、昔ながらの方法も使えます。)
- 事前計算: カテゴリの選択など、 入力 が限定されている場合は、複数の応答をあらかじめ生成しておき、同じユーザーに同じ応答を二度表示しないようにするだけで対応できます。
- UI の活用: 指標の要約、レポート、検索結果などは、LLM が生成するテキストよりも、用途に合わせて作成した従来の UI コンポーネントで示す方が伝わりやすい場合があります。
- 従来の最適化手法: LLM アプリケーションもアプリケーションであることに変わりはありません。二分探索、キャッシュ、ハッシュマップ、時間計算量といった手法や考え方は、言語モデルを使う世界でも 引き続き 役立ちます。
具体例
それでは、サンプルアプリケーションを見ながら、レイテンシを最適化できる箇所を洗い出し、改善策を考えてみましょう。
実際の本番アプリケーションを参考にした、架空のカスタマーサービスボットのアーキテクチャとプロンプトを分析します。まず「アーキテクチャとプロンプト」で前提を説明し、続く「分析と最適化」でレイテンシの最適化手順を解説します。
この例では、すべての原則を取り上げるわけではありません。実際のユースケースでも、すべての手法を適用する必要はないのと同じです。
アーキテクチャとプロンプト
以下は、架空の カスタマーサービスボットの 初期アーキテクチャ です。これを変更していきます。

図に示す処理の流れは、大まかに次のとおりです。
- ユーザーが進行中の会話でメッセージを送信します。
- 最新のメッセージを、 単独で意味が通じるクエリ に変換します(プロンプト内の例を参照)。
- そのクエリに回答するために、 追加の情報取得が必要かどうか を判断します。
- 取得 を実行し、検索結果を得ます。
- アシスタントがユーザーのクエリと検索結果に基づいて 推論 し、 回答を生成します。
- 回答をユーザーに返します。
以下に、図の各部分で使用するプロンプトを示します。架空の例として簡略化していますが、実際の本番アプリケーションと同じ構成や表現で記述しています。
「[user input here]」のようなプレースホルダーは、 実行時に実際のデータに置き換わる動的な部分を表します。
分析と最適化
パート 1:取得に関するプロンプトの検討
アーキテクチャを見て最初に目に留まるのは、 連続する GPT-4 の呼び出し です。ここには非効率な処理がある可能性があり、多くの場合、単一の呼び出しや並列の呼び出しに置き換えられます。

この場合、取得の要否を判断するには文脈を補ったクエリが必要です。そこで、両方の処理を 単一のプロンプトにまとめ 、リクエスト数を減らしましょう。

実際、文脈の補完と取得の要否の判断は、単純で明確に定義されたタスクです。そのため、代わりに ファインチューニングした小型モデル を使えそうです。GPT-3.5 に切り替えれば、トークンをより速く処理できます。

パート 2:アシスタントのプロンプトの分析
次に、アシスタントのプロンプトに注目しましょう。JSON のフィールドを埋める過程で、いくつもの個別のステップを実行しているようです。ここには並列化の余地があるかもしれません。

ただし、いくつかテストした結果、JSON 内の推論ステップを分割すると回答の質が下がることがわかったとしましょう。その場合は、別の解決策を探す必要があります。
GPT-4 の代わりに、ファインチューニングした GPT-3.5 を使えるでしょうか? その可能性はあります。ただし、アシスタントの自由形式の回答は、幅広いケースにより適切に対応できる GPT-4 に任せるのが一般的には最善です。一方、推論ステップ自体を見ると、すべてに GPT-4 レベルの推論が必要とは限りません。対象範囲が明確で限定されているため、 ファインチューニングの有力な候補と考えられます。
{
message_is_conversation_continuation: "True", // <-
number_of_messages_in_conversation_so_far: "1", // <-
user_sentiment: "Aggravated", // <-
query_type: "Hardware Issue", // <-
response_tone: "Validating and solution-oriented", // <-
response_requirements: "Propose options for repair or replacement.", // <-
user_requesting_to_talk_to_human: "False", // <-
enough_information_in_context: "True", // <-
response: "...", // X -- benefits from GPT-4
}ここで、トレードオフを検討できます。 単一のリクエストで GPT-4 にすべてを生成させる構成を維持するか、 順次実行する 2 つのリクエストに分割 して最終回答以外を GPT-3.5 に任せるか、という選択です。これは原則同士が競合するケースです。前者ならリクエスト数を減らせますが、後者ならトークンをより速く処理できる可能性があります。
最適化に伴う多くのトレードオフと同様に、答えは具体的な条件によって変わります。たとえば、次のような条件です。
responseとその他のフィールドのトークン数の比率- 大半のフィールドの処理が速くなることによる、レイテンシの平均短縮量
- リクエストを 1 回から 2 回に増やすことによる、レイテンシの平均 増加 量
結論はケースによって異なります。判断するには、本番環境の実例を使ってテストするのが最善です。ここでは、テストの結果、プロンプトを 2 つに分割してトークンをより速く処理する方が有利だとわかったとしましょう。

注: 取得したコンテキストを分割後の両方のプロンプトに渡さずに済むように、response と enough_information_in_context を 2 つ目のプロンプトにまとめます。
さらに、推論プロンプトは取得したコンテキストに依存しなくなったため、取得に関するプロンプトと並列化し、同時に実行できます。

パート 3:構造化出力の最適化
推論プロンプトをもう一度見てみましょう。

推論用の JSON を詳しく見ると、フィールド名自体がかなり長いことに気づくかもしれません。
{
message_is_conversation_continuation: "True", // <-
number_of_messages_in_conversation_so_far: "1", // <-
user_sentiment: "Aggravated", // <-
query_type: "Hardware Issue", // <-
response_tone: "Validating and solution-oriented", // <-
response_requirements: "Propose options for repair or replacement.", // <-
user_requesting_to_talk_to_human: "False", // <-
}フィールド名を短くして説明をコメントに移すことで、生成するトークン数を減らせます。
{
cont: "True", // whether last message is a continuation
n_msg: "1", // number of messages in the continued conversation
tone_in: "Aggravated", // sentiment of user query
type: "Hardware Issue", // type of the user query
tone_out: "Validating and solution-oriented", // desired tone for response
reqs: "Propose options for repair or replacement.", // response requirements
human: "False", // whether user is expressing want to talk to human
}
この小さな変更で、出力トークンを 19 個削減できました。GPT-3.5 では数ミリ秒の短縮にとどまるかもしれませんが、GPT-4 では最大 1 秒短縮できる可能性があります。

モデルの出力がもっと長ければ、この変更でかなり大きな効果が得られることも想像できるでしょう。
さらに、JSON のフィールド名を 1 文字にしたり、すべてを配列にまとめたりすることもできますが、そこまで進めると応答の品質が低下する可能性があります。ここでも、確かめるための最善の方法はテストです。
例のまとめ
カスタマーサービスボットの例で実施した最適化を振り返りましょう。

- クエリにコンテキストを補うステップと取得の要否を確認するステップを統合 し、リクエスト数を削減
- 新しいプロンプトでは、 より小型でファインチューニング済みの GPT-3.5 に切り替え 、トークン処理を高速化
- アシスタントのプロンプトを 2 つに分割し、推論には より小型でファインチューニング済みの GPT-3.5 を使うように切り替え 、こちらもトークン処理を高速化
- 取得の要否の確認と推論のステップを並列化
- 推論用のフィールド名を短縮 し、コメントをプロンプト内に移して、生成するトークン数を削減