リクエスト数ではLLMのコストを制御できない
一言でいうと
一般的なAPIは、リクエスト1つのコストがおおむね同じですが、LLMは、リクエスト1つが100トークンのこともあれば、100,000トークンのこともあります。そのため、ゲートウェイは、リクエストではなく、トークンを数える必要があります。
なぜ必要なのか
毎分60リクエストで、レートリミットをかけたとします。あるテナントが、毎回のリクエストに200,000トークンのコンテキストを入れると、リクエスト数は制限の範囲内ですが、コストとGPUの占有は、他のテナントの数百倍です。そして、そのリクエストがKVキャッシュを独占して、他のリクエストがプリエンプションされます。
リクエスト数の制限では、この状況をまったく防げません。必要なのは、トークン単位の会計です。
どう動くのか
ゲートウェイがすべきことは、5つあります。
第1に、認証とテナントの識別です。APIキーをテナントにマッピングします。
第2に、トークン会計です。リクエストの入力トークンと、レスポンスの出力トークンを、それぞれ数えて記録します。ストリーミングなら、出力トークンはストリームが終わって初めて確定するので、途中で切れた場合も、そこまでのトークンを計上する必要があります。
第3に、2つの層での制限です。RPM(毎分のリクエスト)とTPM(毎分のトークン)を、いっしょにかけます。RPMは乱用を防ぎ、TPMはコストとリソースを守ります。TPMには、トークンバケットがよく合います。普段は静かなテナントの瞬間的なバーストを許容しながら、長期の平均は守れます。
第4に、429とRetry-Afterです。拒否するときに、いつ再び来ればよいかを知らせます。これがないと、クライアントたちがそれぞれリトライして、波を作ります。
第5に、コストの算出です。モデルごとに入力・出力の単価が違うので、トークン数に単価を掛けて累積します。出力トークンのほうが入力より高いのが普通なので、2つを分けて数える必要があります。
ここに、バジェットが加わります。テナントごとに日次または月次の上限を置き、超えたら遮断するか、より安いモデルにダウングレードします。後者をフォールバックと呼び、サービスが完全に止まるよりよい場合が多くあります。
何を基準に制限するのか
RPM(毎分のリクエスト)だけをかけても、LLMサービスでは、ほとんど役に立ちません。リクエスト1つが、100トークンのこともあれば、10万トークンのこともあるからです。そのため、2つの軸をいっしょにかけます。
| 軸 | 何を防ぐか | 単位 |
|---|---|---|
| RPM | リクエストの殺到、ボット | 毎分のリクエスト数 |
| TPM | GPU時間の消耗 | 毎分のトークン数(入力+出力) |
| 同時実行数 | キューの爆発、メモリ | 進行中のリクエスト数 |
3つのうち、同時実行数がGPUを最も正確に反映します。vLLMは、バッチで処理するので、同時リクエスト数が、そのままKVキャッシュの使用量であり、それがあふれると、OOMかプリエンプション(preemption)です。RPMとTPMはコストの会計に、同時実行数は安定性に使います。
トークンバケットで実装すれば、バーストを許容しながら、平均を守れます。
용량 = 60,000 토큰, 채우는 속도 = 1,000 토큰/초 (= 60k TPM)
요청이 오면 max_tokens 만큼 버킷에서 뺀다(예약)
완료되면 실제 사용량과의 차이를 돌려준다(정산)
버킷이 비면 429 + Retry-After: <채워질 때까지의 초>
このコードブロックの韓国語は、容量が60,000トークン、補充速度が毎秒1,000トークン(=60k TPM)で、リクエストが来たらmax_tokens分をバケットから引き(予約)、完了したら実際の使用量との差を返し(精算)、バケットが空なら429とRetry-Afterを返す、という意味です。
Retry-Afterを必ず送ります。ないと、クライアントがすぐにリトライして、状況がさらに悪くなります。
429をうまく返すことも設計
拒否するときに何を知らせるかが、クライアントのコードの品質を決めます。
HTTP/1.1 429 Too Many Requests
Retry-After: 12
X-RateLimit-Limit-Tokens: 60000
X-RateLimit-Remaining-Tokens: 0
X-RateLimit-Reset-Tokens: 12s
そして、どの上限に引っかかったかを、本文に書きます。RPMなのか、TPMなのか、バジェットなのかによって、クライアントがすることが違います。前の2つは待てばよいですが、バジェットの超過は、待ってもだめです。
キャッシュが最大の節約
制限より効果が大きいのが、キャッシュです。3つの層があります。
- 完全一致キャッシュ: 同じプロンプト・同じパラメーターなら、保存された答えを返します。FAQ的なトラフィックで、ヒット率が30%を超えることもあります。温度が0でなければ、同じ入力に同じ答えを返すのが正しいのかを、製品が決める必要があります。
- セマンティックキャッシュ: エンベディングで似た質問を探して再利用します。ヒット率は高いですが、間違った再利用のリスクがあるので、しきい値を保守的に設定します。
- プレフィックスキャッシュ: システムプロンプトのように、前の部分が同じリクエストたちのKVキャッシュを再利用します。vLLMの
enable_prefix_cachingがこれで、精度に影響がなく、TTFTを大きく減らします。最初にオンにするものです。
現場での姿
トークン数があらかじめわからないという点が、この設計を難しくします。入力トークンは、リクエストの時点で数えられますが、出力トークンは、生成が終わってから初めてわかります。そのため、実務では、max_tokensを上限として予約しておいて、完了後に実際の使用量で精算する方式を使います。予約をしないと、同時に入ってきたリクエストが、すべて予算の範囲内だと判断して通過したあと、いっしょに超過します。
コスト暴走の事故は、たいていリトライのループで起きます。クライアントがタイムアウトのたびにリトライするのに、サーバーは生成し続けていると、ユーザー1人に同じ答えを5回作らせることになります。ゲートウェイで冪等キーを受け取って、進行中の同一リクエストをまとめれば、この無駄がなくなります。
次のラボですること
前に作ったモックサーバーの前に、ゲートウェイを立てます。APIキー認証、RPM制限、トークン会計、TPMのトークンバケット、Retry-After、モデルごとのコスト算出、そしてテナントのバジェット超過の遮断まで付けます。