Bedrock の TPM を前提にした並行処理の設計
はじめに
こんにちは、Acsim 開発チームの @satococoa です。
Acsim では LLM として Amazon Bedrock を使っています。Bedrock を本番環境で運用していく上で避けて通れないのが、TPM (Tokens per Minute) 制限との向き合い方です。
やりたいことはシンプルで、「LLM 呼び出しをできるだけ速く処理したい」。ただし、並行数を上げすぎると TPM 制限にすぐ達してしまい、処理全体が止まってしまいます。
この記事では、私たちが TPM 制限を前提にどのように並行処理を設計してきたか、実装を通じて学んだことを紹介します。
Bedrock の rate limit を理解する
Bedrock にはいくつかの制限がありますが、今回特に重要なのは TPM (Tokens per Minute) です。これは 1 分間に消費できるトークン数の上限で、並行処理の設計に直結します。
TPM の値はモデルごとに異なり、実際の値は Service Quotas から確認できます。
トークン消費の仕組み
実装を始める前に、トークンがどう消費されるのかを知っておく必要があります。
まず、Bedrock でリクエストする際には、生成される出力トークンの最大数(maxTokens)を指定します。これにより、LLM がどれだけ長い応答を返せるかを制御します。
公式ドキュメントによると、トークン消費は次の 3 段階で処理されます。
1. リクエスト開始時
Total input tokens + maxTokens
リクエストを投げた瞬間、入力トークン数と maxTokens の合計が TPM から控除されます。入力トークン数には、プロンプトキャッシュを使用する場合は CacheReadInputTokens と CacheWriteInputTokens も含まれます。この時点で TPM を超えていると、リクエストはスロットルされます。
2. 処理中
実際の出力トークン数に応じて、消費量が定期的に調整されます(詳細な計算式は公開されていません)。
3. リクエスト完了時
InputTokenCount + CacheWriteInputTokens + (OutputTokenCount × burndown rate)
実際の消費量が確定し、未使用分が TPM に返還されます。注意点として、CacheReadInputTokens はこの計算に含まれません。
全体の流れを図にすると、次のようになります(Claude 系モデルでは出力トークンが burndown rate により 5 倍換算されるため、ここでは burndown rate = 5 の例を示しています。詳細は後述)。
burndown rate とは
出力トークンは burndown rate という倍率で TPM を消費します。Claude の最新モデル(Opus 4、Sonnet 4 など)は 5 倍換算 です。つまり、出力トークン 100 個 = TPM 500 個分の消費になります。
ここでの 5 倍換算はあくまで TPM 制限の消費量 の話です。課金は実際の入出力トークン数に基づいて計算されるので、TPM の消費が 5 倍だからといって課金も 5 倍になるわけではありません。
- Claude Opus 4 / 4.1: 5 倍
- Claude Sonnet 4 / 4.5: 5 倍
- Claude 3.7 Sonnet: 5 倍
- Claude Haiku 4.5: 5 倍
- その他のモデル: 1 倍
maxTokens が並行数を左右する
この仕組みで重要なのは、maxTokens が並行数に直接影響する点です。
maxTokens を大きく設定すると、リクエスト開始時の控除額が増えるため、同時に実行できるリクエスト数が減ります。逆に、実際の出力に近い値に抑えると、同じ TPM でも並行数を増やせます。
並行処理のパフォーマンスを上げるには、maxTokens の最適化が鍵になります。
並行処理の設計
並行数をどう決めるか
並行処理を設計する上で最初に考えるのは、「同時に何個のリクエストを実行できるか」です。理論的には次の式で概算できます。
同時実行数 ≒ TPM ÷ (入力トークン数 + maxTokens)
ただし、これは簡易的な計算です。実運用では、過去の出力トークンの履歴から maxTokens を推定し、workflow 内の並列フェーズごとに合算して見積もっています(詳細は後述)。
実際の workflow では複数エージェントが同時に動く
私たちのプロジェクトでは Mastra を使って複数のエージェントを組み合わせた workflow を構築しています。workflow の中には、複数のエージェントが並列で動くステップがあります。
例えば、次のような構成があったとします。
この場合、Step 2 と Step 3 は並列実行されます。つまり、この区間では Agent B と Agent C の maxTokens を合算した値が、実質的な最大消費量になります。
並行実行の「フェーズ」を洗い出す
並行数を正しく見積もるには、workflow 内のどこで並列実行が発生するかを把握する必要があります。私たちは次のようにしています。
- workflow の構造から並列実行が発生する区間(フェーズ)を特定する
- 各フェーズ内で同時に動くエージェントの
maxTokensを合算する - 全フェーズの中で最大の消費量を基準にする
この「最大ケース」を基準にすることで、どんな状況でも TPM を超えないようにしています。
maxTokens を動的に調整する
固定値で maxTokens を設定すると、安全を見て大きめの値にしがちです。そうすると並行数が抑えられ、パフォーマンスが落ちます。
私たちは過去の実行履歴を使って、maxTokens を動的に調整しています。
- 過去の出力トークン数を記録
- 外れ値を除外する
- 残りの最大値 × 1.5(安全マージン)を次回の
maxTokensとする
例えば、過去 10 回の実行で出力トークン数が [800, 850, 900, 820, 3000, 870, 810, 890, 840, 860] だった場合:
- 外れ値(3000)を除外
- 残りの最大値は 900
- 900 × 1.5 = 1350 を次回の
maxTokensとする
この仕組みで、安全性を保ちながら並行数を最大化できています。
出力が途中で切れた場合のリトライ
maxTokens を抑えすぎると、出力が途中で切れてしまう問題があります。Bedrock のレスポンスには stopReason というフィールドがあり、出力が途中で切れた場合は "max_tokens" として返されます。
この場合、私たちは次のように対応しています。
stopReasonが"max_tokens"なら、maxTokensを 2 倍にしてリトライ- モデルの最大出力トークン数に達するまで繰り返す
- 最大値に達しても切れる場合は、エラーとして扱う
これにより、最初は控えめな maxTokens で並行数を確保しつつ、必要に応じて自動的に増やすことができます。
全体の流れを図にすると次のようになります。
SQS を使った並行処理のスケーリング
私たちの構成では、LLM 呼び出しを SQS のキュー処理でさばいています。
SQS でキュー処理を行う場合、ReceiveMessage API の取得件数は 10 件が上限です。
そのため、並行数を増やすには Consumer 自体を増やす必要があります。
実効並行数 = Consumer 数 × 取得件数
私たちは TPM から算出した並行数に応じて、Consumer を動的に増減させています。
まとめ
Bedrock を本番環境で運用する上で、TPM 制限を前提にした並行処理の設計は避けて通れません。私たちが学んだポイントをまとめます。
トークン消費の仕組み
- リクエスト開始時に
入力トークン数 + maxTokensが TPM から控除される(入力トークン数にはキャッシュ分も含む) - リクエスト完了時に実際の消費量が確定し、未使用分が返還される
maxTokensの最適化が並行数に直結する
並行処理の設計
- 並行数は TPM と最大トークン消費量から逆算する
- workflow の並列実行区間(フェーズ)を特定し、最大ケースを基準にする
maxTokensを過去の履歴から動的に調整することで、並行数を最大化できる- 出力が途中で切れた場合は
maxTokensを 2 倍にしてリトライする - SQS の場合は Consumer 数を増やしてスケールする
これらを押さえておくと、TPM 制限に悩まされることなく、安定したパフォーマンスを維持できます。
謝辞
今回この記事で説明した件に関し、調査・設計・開発は Acsim 開発チームの @technote-space さんに担当していただきました。 ありがとうございました。
