Acsim Logo
エンジニアリング

Bedrock の TPM を前提にした並行処理の設計

Bedrock の TPM を前提にした並行処理の設計
#LLM#AWS#Bedrock#SQS

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 から控除されます。入力トークン数には、プロンプトキャッシュを使用する場合は CacheReadInputTokensCacheWriteInputTokens も含まれます。この時点で 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 内のどこで並列実行が発生するかを把握する必要があります。私たちは次のようにしています。

  1. workflow の構造から並列実行が発生する区間(フェーズ)を特定する
  2. 各フェーズ内で同時に動くエージェントの maxTokens を合算する
  3. 全フェーズの中で最大の消費量を基準にする

この「最大ケース」を基準にすることで、どんな状況でも TPM を超えないようにしています。

maxTokens を動的に調整する

固定値で maxTokens を設定すると、安全を見て大きめの値にしがちです。そうすると並行数が抑えられ、パフォーマンスが落ちます。

私たちは過去の実行履歴を使って、maxTokens を動的に調整しています。

  • 過去の出力トークン数を記録
  • 外れ値を除外する
  • 残りの最大値 × 1.5(安全マージン)を次回の maxTokens とする

例えば、過去 10 回の実行で出力トークン数が [800, 850, 900, 820, 3000, 870, 810, 890, 840, 860] だった場合:

  1. 外れ値(3000)を除外
  2. 残りの最大値は 900
  3. 900 × 1.5 = 1350 を次回の maxTokens とする

この仕組みで、安全性を保ちながら並行数を最大化できています。

出力が途中で切れた場合のリトライ

maxTokens を抑えすぎると、出力が途中で切れてしまう問題があります。Bedrock のレスポンスには stopReason というフィールドがあり、出力が途中で切れた場合は "max_tokens" として返されます。

この場合、私たちは次のように対応しています。

  1. stopReason"max_tokens" なら、maxTokens を 2 倍にしてリトライ
  2. モデルの最大出力トークン数に達するまで繰り返す
  3. 最大値に達しても切れる場合は、エラーとして扱う

これにより、最初は控えめな 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 さんに担当していただきました。 ありがとうございました。

この記事をシェア: