Fireworks AI 入門 — 初心者のためのハンズオンガイド|2026-09 版
第 8 章

Batch API — 大量処理を安く回す

急がないリクエストをまとめて投げて割引を受ける

この章で分かること

これは何か

Batch API(バッチ推論、Batch Inference)とは、急がない大量のリクエストをまとめて投げ、非同期(asynchronous、送信してから結果が返るまでの間に他の処理を進められる方式)に処理してもらう仕組みである。第 5 章で見た Serverless 推論が「リクエストを送ったらすぐ応答が返る」ことを前提にしていたのに対し、Batch API は「結果は後でまとめて受け取ればよい」という前提に立つ。その見返りとして、標準の Serverless 単価より安い料金で処理してもらえる。

投入経路は 3 つある。コンソール(app.fireworks.ai)にはBatch Inference タブがあり、公式ガイドは「Batch Inference タブに移動し、Create Batch Inference Job をクリックする」という手順を示している。ここでモデルとデータセットをドロップダウンで選んでジョブを作成できる。CLI からは firectl の batch-inference-job 系サブコマンドを使う。プログラムから制御したいときは REST API(https://api.fireworks.ai/v1/accounts/{account_id}/batchInferenceJobs)を直接叩く。どの経路でも、投入するデータはJSONL(JSON Lines、1 行が 1 つの完結した JSON オブジェクトになっているテキスト形式)で用意したデータセットという単位にまとめる必要がある。データセットの作成自体はfirectl dataset createや dataset タブなど、Batch とは別の仕組みで行う。

いつ使うのか

Batch API が向くユースケースとして、公式ガイドは次の 4 つを挙げている。

いずれも「今すぐ 1 件だけ答えが欲しい」のではなく「まとまった量を、多少待ってもよいから安く処理したい」という場面である。隣接する第 5 章の Serverless 推論、第 9 章の On-demand Deployments との使い分けは次のとおり。

観点Serverless(第 5 章)Batch API(本章)On-demand Deployments(第 9 章)
結果が返るまで即時(対話的に使える)最大 72 時間(既定 24 時間)の実行ウィンドウ内でまとめて即時(専有 GPU なので安定)
課金標準のトークン単価入力・出力とも標準単価から 50% オフ、さらにキャッシュ分に追加割引GPU 時間課金(専有)
適したワークロード対話的・即時応答が必要な用途急がない大量リクエストのまとめ処理予測可能な高スループット・独自のレイテンシ要件
投入前の確認事項Serverless タグ付きモデルかBatch 対応モデルか(非対応は pending のまま止まる)対応モデルを On-demand でデプロイ済みか

特に、Batch 対応かどうかは投入前に必ず確認する必要がある。対応モデルは「Model Library 上で On-demand Deployments に対応しているベースモデル、またはそのベースモデル上に構築したカスタムモデル」であり、新しく追加されたモデルは対応が遅れることもある、と公式ドキュメントは注意している。

仕組み(1)— ジョブのライフサイクルと有効期限

Batch ジョブを投入すると、ガイドページが示す主要な状態を次の順にたどる。

ジョブの状態遷移(主要経路) 検証 確保 完走 異常終了 時間切れ 時間の経過 → 検証中 (VALIDATING) JSONL形式チェック 待機中 (PENDING) リソース確保待ち 実行中 (RUNNING) 推論処理を実行 完了 (COMPLETED) 出力データセットへ保存 失敗 (FAILED) 回復不能なエラー 期限切れ (EXPIRED) 完了済み分は保存
図 8-1 主要経路(検証中→待機中→実行中→完了)に失敗・期限切れへの分岐を加えた 6 状態を図示している。API が返す state(gatewayJobState)はこの 6 状態を含む全 20 種類あり、実行中の一時停止(容量再取得待ち)は state とは別の waitingOnCapacity という boolean フィールドで表される。

まず検証中(VALIDATING)でデータセットの形式をチェックし、待機中(PENDING)でリソースが確保されるのを待ち、実行中(RUNNING)で実際に推論が行われ、すべて処理し終えると完了(COMPLETED)になる。回復不能なエラーが起きれば失敗(FAILED)、後述の有効期限に達すれば期限切れ(EXPIRED)という扱いになるが、期限切れでも完了済みのリクエストの結果は失われず出力データセットに保存されている。

実行中はさらに、容量の再取得のために一時的に止まることがある(waitingOnCapacity)。これはジョブが一度 RUNNING になった後、プリエンプション(他の優先度の高い処理に一時的に GPU を明け渡すこと)や GPU 不足で内部的な待機フェーズへ戻り、再開を待っている状態を指す。公式 API の説明では、ジョブの state 自体は RUNNING のまま変わらず、このあいだ進捗は保存されていて自動的に再開する、とされている。コンソール上では「Briefly paused — waiting on capacity」という表示になる。

また、ここで示した 6 つの状態はガイドページが解説用に挙げている主要な状態にすぎない。API(gatewayJobState)が実際に返しうる値は JOB_STATE_WRITING_RESULTS や JOB_STATE_RE_QUEUEING なども含む合計 20 種類(2026-09 時点)あり、ジョブの状態をポーリング(一定間隔で状態を問い合わせて変化を待つこと)する実装を書くときはこの主要 6 状態だけを想定してはいけない。

ジョブの有効期限(実行ウィンドウ)は、コンソールやガイドの説明では 12・24・48・72 時間から選べるとされ、既定値は 24 時間である。API 側の maxJobDuration フィールドで見ると、サーバがこの値を 12 時間〜72 時間の範囲にクランプ(切り詰め)し、未設定なら既定 24 時間になる、というのが実装上の正確な挙動である。期限を過ぎるとジョブは EXPIRED になるが、それまでに完了した行の結果は課金対象になり出力データセットに保存される。なお、レスポンスに含まれる expireTime(実際の期限日時)はサーバが create_time + 実行ウィンドウ から計算して返す値で、クライアント側から指定しても無視される。期限を変えたいときは maxJobDuration の方を設定する。

期限切れなどで未完了のリクエストが残った場合は、--continue-from で元のジョブを指定すると、未完了・失敗した行だけを対象に新しいジョブとして再実行できる。継続チェーン全体の出力をまとめてダウンロードしたいときは firectl dataset download <dataset-id> --download-lineage を使う。

仕組み(2)— JSONL フォーマットとジョブレベル system prompt

Batch ジョブへの入力は、1 行が 1 リクエストに対応する JSONL ファイルで用意する。各行には一意な custom_id と、リクエストパラメータをまとめた body という 2 つのフィールドが必須で、入力データセットの上限は 80 GiB である。

{"custom_id": "request-1", "body": {"messages": [{"role": "user", "content": "日本の首都はどこですか?"}], "max_tokens": 50}}
{"custom_id": "request-2", "body": {"messages": [{"role": "user", "content": "量子コンピュータを一言で説明してください"}], "max_tokens": 50}}
{"custom_id": "request-3", "body": {"messages": [{"role": "user", "content": "冗談を一つ言ってください"}], "max_tokens": 50}}

body の中身は、標準の messages 形式か、事前にトークン化済みの prompt_token_ids 形式のどちらかを使う。この 2 つは排他で、同じ行に両方を入れることはできない。prompt_token_ids は独自のトークナイザ(文字列をトークン ID 列に変換する処理)やレンダラ(トークン列をモデル入力の形式に整形する処理)で作った入力をそのままモデルに渡したいときに使う上級者向けの経路で、通常は messages 形式で十分である。

各行にまったく同じ system メッセージを繰り返し書く代わりに、ジョブ全体に対して 1 つだけジョブレベルの system promptを設定できる。

入力JSONL(3行) 結果ファイル エラーファイル custom_id: request-1 body.messages custom_id: request-2 body.messages custom_id: request-3 body.messages Batch Inference Job モデル推論を実行 成功/失敗で振り分けて出力 1行目の結果 モデルの応答 2行目の結果 モデルの応答 3行目のエラー エラー行
図 8-2 入力 JSONL の各行はジョブを通過し、成功した行は結果ファイル、失敗した行はエラーファイルに振り分けられて出力データセットに入る(図は振り分けの模式で、3 行目が必ず失敗するという意味ではない)。

公式ドキュメントの説明では、ジョブレベルの system prompt は「まだ system メッセージで始まっていない行」だけに、先頭の system メッセージとして注入される。行自身が先頭に system メッセージを持っていれば、その行の指定が常に優先される。すべての行が同じ大きな system prompt を共有している場合、これを行ごとに書く代わりにジョブレベルへ切り出すことで、アップロードするデータセットを小さくでき、しかも注入されるプレフィックスは全行でバイト同一になるので、後述するプロンプトキャッシュも効き続ける。

ジョブが終わると、出力データセットには結果ファイル(成功したレスポンスの JSONL)とエラーファイル(失敗したリクエストのデバッグ情報)の 2 つが入る。どちらも入力行と対応付けて読む前提のファイルだが、出力行の具体的なスキーマは公式ドキュメントに記載がない。公式は追跡用途に意味のある custom_id を付けることを推奨している。REST API からダウンロードする場合は、出力データセットに対して署名付き URL(一定時間だけ有効な、認証情報なしでダウンロードできる URL)のマップを取得してから、個別にファイルを取りに行くという 2 段構えになる(後述のハンズオンで使う firectl dataset download は、この 2 段階を 1 コマンドにまとめている)。

サイズの上限は入力と出力で単位そのものが違う。入力データセットは最大 80 GiB、出力データセットは最大 8 GB(いずれも 2026-09 時点)で、公式ドキュメントの原文でもこの 2 つの単位は揃えられていない。出力側は上限に達するとジョブが早期に expire することがあるので、1 行あたりの max_tokens を無闇に大きくしすぎないことが、有効期限だけでなく出力サイズの面でも効いてくる。

仕組み(3)— 割引とリアルタイム推論の対比

Batch API の割引は 2 段階で重なる。まず、標準の Serverless 単価(第 5 章)に対して入力・出力とも 50% オフになる(Serverless Pricing の原文は「Batch inference is billed at 50% of serverless pricing on both input and output」と明記している)。さらに、Batch ジョブは自動的にプロンプトキャッシュ(同じ内容のプロンプト冒頭をキャッシュし、再利用分の課金を抑える仕組み。詳しくは第 7 章)を使うため、キャッシュにヒットしたトークン分にはさらに追加で 50% の節約が乗る、と公式ガイドは説明している。キャッシュのヒット率を上げるコツは、プロンプトの中で変わらない部分(指示文やスキーマなど)を先頭にまとめて置くことである。ジョブの進捗(jobProgress)には cachedInputTokenCount というフィールドがあり、実際にどれだけのトークンがキャッシュにヒットしたかを実測できる。

Serverless(リアルタイム) Batch API 標準単価 投入後はまとめて処理 50%オフ 時間の経過 →
図 8-3 Serverless は個々のリクエストがすぐ終わる代わりに標準単価、Batch はまとめて投入し結果を待てる代わりに半額になる。Batch のレーン全体(灰色の帯)の長さは処理時間そのものではなく、ジョブの締切(実行ウィンドウ、既定 24 時間・最大 72 時間)内であればいつ結果が届いてもよいことを表す。「待てるかどうか」が最初の判断軸になる。

この図が示すとおり、Serverless は個々のリクエストがすぐに終わる代わりに標準単価がかかり、Batch は投入後しばらく待つ代わりに安くなる。「待てるかどうか」が両者を選ぶ最初の判断軸になる。

ハンズオン — Batch ジョブを実際に動かす

ここでは公式ガイドの最小構成に沿って、まず firectl でジョブを一通り動かし、次に REST API でジョブレベル system prompt を試す。

ハンズオン 8-1 firectl で JSONL を投入し結果をダウンロードする

  1. ターミナルで API キーを環境変数にセットする(export FIREWORKS_API_KEY=...)。firectl は第 2 章で発行した API キーで既にサインイン済みである前提とする。
  2. 次の 3 行を batch_input_data.jsonl として保存する。
{"custom_id": "request-1", "body": {"messages": [{"role": "user", "content": "日本の首都はどこですか?"}], "max_tokens": 50}}
{"custom_id": "request-2", "body": {"messages": [{"role": "user", "content": "量子コンピュータを一言で説明してください"}], "max_tokens": 50}}
{"custom_id": "request-3", "body": {"messages": [{"role": "user", "content": "冗談を一つ言ってください"}], "max_tokens": 50}}
  1. データセットとして登録する。
firectl dataset create batch-input-dataset ./batch_input_data.jsonl

データセット ID が表示されれば成功。

ここから先は Batch 推論を実際に呼び出すため、少額のトークン課金が発生する(Batch API は標準単価の 50% オフで課金される。gpt-oss-120b の標準単価は 2026-09 時点で入力 $0.15 / キャッシュ入力 $0.015 / 出力 $0.60(いずれも 1M トークンあたり USD、Serverless Pricing)なので、Batch では入力 $0.075 / 出力 $0.30 になる)。3 行程度の短いリクエストなのでごく少額だが、繰り返し実行する際は行数に注意する。内容を実行前に確認したいときは、次のコマンドに --dry-run を付けるとリクエストの内容だけを表示して実際には実行しない。

  1. ジョブを作成する。--system-prompt は firectl のコマンドリファレンスのフラグ一覧に載っておらず実機での挙動が確認できないため、ここでは使わない。
firectl batch-inference-job create \
  --job-id ch08-handson-job \
  --model accounts/fireworks/models/gpt-oss-120b \
  --input-dataset-id batch-input-dataset \
  --output-dataset-id batch-output-dataset \
  --max-tokens 50 \
  --temperature 0.7
  1. ジョブの状態をポーリングする。数十秒〜数分おきに実行し、state が JOB_STATE_COMPLETED になるまで待つ。API が返す状態は本章で説明した主要 6 状態だけでなく合計 20 種類あるので、想定外の値が出ても異常とは限らない。
firectl batch-inference-job get ch08-handson-job

既定のテキスト出力に現在の状態が表示される。スクリプトから状態だけを取り出したいときは -o json で JSON 出力に切り替えられる(jq 等の JSON 処理ツールがあれば | jq -r .state のように絞り込める)。

  1. 完了したら結果をダウンロードする。
firectl dataset download batch-output-dataset

ローカルに結果ファイルとエラーファイルの 2 つが保存される。結果ファイルを開き、3 行分の応答が入っていれば成功(各行には入力と同じ custom_id が引き継がれているはずだが、出力ファイルの詳細なスキーマは公式ドキュメントに記載がないため、ここでは行数と応答の有無で確認する)。

ハンズオン 8-2 REST API でジョブレベル system prompt を使う

  1. 自分のアカウント ID を環境変数にセットする。値は ~/.fireworks/auth.ini の account_id(firectl login 時に保存されたもの)で確認できる。第 1 章で見たモデル ID accounts/fireworks/models/... に出てくる fireworks は Fireworks 社自身のアカウント ID であって、自分のものではない点に注意する。
export ACCOUNT_ID=<your-account-id>

こちらも Batch 推論を 1 ジョブ分呼び出すため、少額のトークン課金が発生する(同じ gpt-oss-120b の Batch 単価が適用される)。

  1. ハンズオン 8-1 でアップロード済みの batch-input-dataset を再利用し、行ごとに system メッセージを書く代わりにジョブレベルの systemPrompt を指定してジョブを作る。
curl -X POST "https://api.fireworks.ai/v1/accounts/${ACCOUNT_ID}/batchInferenceJobs?batchInferenceJobId=ch08-handson-job-2" \
  -H "Authorization: Bearer ${FIREWORKS_API_KEY}" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "accounts/fireworks/models/gpt-oss-120b",
    "inputDatasetId": "accounts/'${ACCOUNT_ID}'/datasets/batch-input-dataset",
    "outputDatasetId": "accounts/'${ACCOUNT_ID}'/datasets/batch-output-dataset-2",
    "systemPrompt": "あなたは日本語で簡潔に答えるアシスタントです。",
    "inferenceParameters": { "maxTokens": 50 }
  }'

レスポンスに name と初期状態の state が含まれていれば成功。

  1. 状態を確認し、完了後に jobProgress を見てキャッシュヒットを確認する。(整形に python3 -m json.tool を使っている。無ければ生の JSON のまま読んでもよい)
curl -s -X GET "https://api.fireworks.ai/v1/accounts/${ACCOUNT_ID}/batchInferenceJobs/ch08-handson-job-2" \
  -H "Authorization: Bearer ${FIREWORKS_API_KEY}" \
  | python3 -m json.tool

整形された JSON の中で state が JOB_STATE_COMPLETED になっていることを確認する。jobProgress.cachedInputTokenCount が返っていれば、その値がキャッシュにヒットした入力トークン数を表す。ただし今回は 3 行という行数の少なさから値が 0 になったり、フィールド自体が出力に含まれなかったりすることがある。

本章のハンズオンで作成した batch-input-dataset / batch-output-dataset / batch-output-dataset-2 は、不要になったら firectl dataset delete <dataset-id> で削除できる。

つまずきポイント

非対応モデルへの投入はエラーにならない。 Batch に対応していないモデルへジョブを投げても、即座にはエラーが出ない。ジョブは pending のまま永久にスケジュールされないことがある。投入前に、対象モデルが Model Library 上で On-demand Deployments に対応しているか確認する。

system prompt とコンテキスト超過は completion ゼロという形で失敗する。 ジョブレベルの system prompt もモデルのコンテキストウィンドウを消費する。system prompt が大きく、各行のコンテンツ(画像などのメディアを含む場合)も長いと、合計がモデルのコンテキストウィンドウを超えることがある。この場合ジョブ自体は完走するが、completion が 1 件も出ないという静かな失敗になる。system prompt と行ごとのコンテンツの合計をモデルの上限内に収める。

prompt_token_ids を使うときは user か prompt_cache_key を忘れない。 メッセージ本文が無いと、キャッシュのアフィニティ(同じレプリカに寄せる仕組み)を決めるためのハッシュ元が無くなり、行ごとにバラバラのレプリカへ流れてキャッシュヒット率が下がる。これもエラーにはならず、静かに割引が減るだけなので気づきにくい。

ジョブ状態のポーリング中に HTTP 500 が返ることがある。 Batch ジョブの失敗理由は status.code(gRPC の gatewayCode)と status.message で返る、推論リクエストのエラーコード表とは別系統の仕組みである。gatewayCode の UNKNOWN(HTTP 500 に対応)は一時的な失敗であることもあるため、少数回のリトライは妥当な対応である。ただし同じ呼び出しで 500 が続く場合は自然回復を期待せずサポートへ連絡したほうがよい。これとは別に公式ガイドは、ジョブが「creating」のまま 30 分を超えて動かない場合もジョブ ID を添えてサポートへ連絡するよう案内している。

理解度チェック

Q1. Batch API の基本の割引率と、それに加えて何が節約されますか?

標準 Serverless 単価に対して入力・出力とも 50% オフが基本。さらに Batch ジョブは自動的にプロンプトキャッシュを使うため、キャッシュにヒットしたトークン分にはさらに追加で 50% の節約が乗る、と公式ガイドは説明している。

Q2. ジョブの有効期限(実行ウィンドウ)の既定値と、選べる範囲はどうなっていますか?

既定は 24 時間。コンソールやガイドでは 12・24・48・72 時間から選択でき、API の maxJobDuration はサーバが 12 時間〜72 時間の範囲にクランプする。

Q3. Batch に対応していないモデルへジョブを投げるとどうなりますか?

即座にはエラーにならず、ジョブが pending のまま永久にスケジュールされないことがある。投入前に Model Library でモデルの互換性を確認する必要がある。

Q4. JSONL の必須フィールドと、messages と prompt_token_ids の関係を説明してください。

必須フィールドは一意な custom_id と body。body 内の messages(標準)と prompt_token_ids(事前トークン化)は排他で、同じ行に両方を入れることはできない。

Q5. ジョブレベルの system prompt は、入力データセットのどの行に注入されますか?

まだ system メッセージで始まっていない行だけに、先頭の system メッセージとして注入される。行自身が先頭に system メッセージを持つ場合はその行の指定が常に優先される。注入されるプレフィックスは全行でバイト同一になるため、プロンプトキャッシュも効き続ける。

まとめ

次章では、Batch と同じく JSONL や API を使う場面もあるが、専有 GPU 上でモデルを動かす On-demand Deployments を見ていく。

参考リンク