Skip to content

Tutorial 14: Batch Inference

Batch inference submits multiple LLM calls as a single asynchronous job rather than making them one by one. Most providers offer ~50% cost reduction for batch mode. KeGAL exposes batch at two levels: intra-node (one node processes N user messages) and inter-node (N separate nodes submitted as one job).

Both are activated from YAML alone. The Python API (compiler.compile()) is unchanged.


1. Level 1: one node, many messages

The simplest batch pattern sends N user messages through the same node in a single batch job, then collects the N results for a downstream synthesiser.

flowchart TD
    M0["message 0"] --> AN["analyst\nbatch_use_messages: [0,1,2]"]
    M1["message 1"] --> AN
    M2["message 2"] --> AN
    AN -->|"<message_0> ... <message_2>"| SY["synthesiser\nmessage_passing.input"]
    SY --> OUT([output])

What changes in YAML

Replace user_message at the graph level with batch_user_messages. On the node that runs in batch, set prompt.batch_use_messages to the list of indices to process. Use message_passing.output: true to forward the tagged results downstream.

batch_user_messages:
  - "Analyse the financial performance of company A."
  - "Analyse the financial performance of company B."
  - "Analyse the financial performance of company C."

models:
  - llm: "anthropic"
    model: "claude-haiku-4-5-20251001"
    api_key: "${ANTHROPIC_API_KEY}"

prompts:
  # 0 — analyst
  - template:
      system_template:
        role: |
          You are a financial analyst.
          Summarise this company's financial performance covering revenue, margin, and key risks.
      prompt_template:
        task: "{user_message}"

  # 1 — synthesiser
  - template:
      system_template:
        role: |
          You are a portfolio strategist.
          Given the individual summaries below, produce a comparative overview
          and a single investment recommendation.
      prompt_template:
        analyses: |
          {message_passing}

nodes:
  - id: "analyst"
    model: 0
    temperature: 0.3
    max_tokens: 512
    show: true
    prompt:
      template: 0
      batch_use_messages: [0, 1, 2]   # 3 calls → 1 batch job
    message_passing:
      output: true                     # tagged output forwarded to synthesiser

  - id: "synthesiser"
    model: 0
    temperature: 0.5
    max_tokens: 1024
    show: true
    message_passing:
      input: true                      # receives <message_0>, <message_1>, <message_2>
    prompt:
      template: 1

edges:
  - node: "analyst"
  - node: "synthesiser"

What the synthesiser receives

The {message_passing} placeholder in the synthesiser's prompt is filled with:

<message_0>Company A summary …</message_0>
<message_1>Company B summary …</message_1>
<message_2>Company C summary …</message_2>

The synthesiser sees all three analyses in a single LLM call.


2. Level 1 with structured output

If the analyst produces structured JSON, each object is serialised with json.dumps inside its tag:

<message_0>{"revenue": "€120M", "margin": "18%", "risk": "regulatory"}</message_0>
<message_1>{"revenue": "€95M",  "margin": "22%", "risk": "supply chain"}</message_1>
<message_2>{"revenue": "€210M", "margin": "14%", "risk": "competition"}</message_2>

The downstream node receives valid JSON strings. Its prompt template can instruct the LLM to parse them.


3. Level 1 with per-item pass-through (batch_message_passing)

When you want the downstream node to be called once per batch item (instead of once with all items tagged), use batch_message_passing instead of message_passing on both nodes:

flowchart TD
    AN["analyst\nbatch_message_passing.output"] -->|item 0| EN["enricher\nbatch_message_passing.input"]
    AN -->|item 1| EN
    AN -->|item 2| EN
nodes:
  - id: "analyst"
    model: 0
    temperature: 0.3
    max_tokens: 512
    show: true
    prompt:
      template: 0
      batch_use_messages: [0, 1, 2]
    batch_message_passing:
      output: true     # each result passed individually downstream

  - id: "enricher"
    model: 0
    temperature: 0.3
    max_tokens: 512
    show: true
    batch_message_passing:
      input: true      # receives each item as a separate call
    prompt:
      template: 1

The enricher is called 3 times, once per analyst output. From the enricher's perspective each call looks identical to a regular non-batch call.


4. Level 2: batch fan-out / fan-in

When you already have separate nodes (each with its own prompt) and want to submit them as a single batch job, use batch_children and batch_fan_in on the edges. This is functionally identical to children / fan_in but uses the provider's batch API.

flowchart TD
    D[dispatcher] -->|"batch job"| B1[branch_economic]
    D -->|"batch job"| B2[branch_environmental]
    D -->|"batch job"| B3[branch_social]
    B1 --> S[synthesiser]
    B2 --> S
    B3 --> S
models:
  - llm: "anthropic"
    model: "claude-haiku-4-5-20251001"
    api_key: "${ANTHROPIC_API_KEY}"

prompts:
  - template:   # 0 — dispatcher
      system_template:
        role: Decompose the research question into three independent sub-questions.
      prompt_template:
        question: "{user_message}"

  - template:   # 1 — economic branch
      system_template:
        role: Analyse only the economic aspects of the topic.
      prompt_template:
        topic: "{message_passing}"

  - template:   # 2 — environmental branch
      system_template:
        role: Analyse only the environmental aspects of the topic.
      prompt_template:
        topic: "{message_passing}"

  - template:   # 3 — social branch
      system_template:
        role: Analyse only the social aspects of the topic.
      prompt_template:
        topic: "{message_passing}"

  - template:   # 4 — synthesiser
      system_template:
        role: Combine the three analyses into a single coherent report.
      prompt_template:
        analyses: "{message_passing}"

nodes:
  - id: "dispatcher"
    model: 0
    temperature: 0.2
    max_tokens: 256
    show: false
    prompt: { template: 0, user_message: true }
    message_passing: { output: true }

  - id: "branch_economic"
    model: 0
    temperature: 0.5
    max_tokens: 512
    show: true
    prompt: { template: 1 }
    message_passing: { input: true, output: true }

  - id: "branch_environmental"
    model: 0
    temperature: 0.5
    max_tokens: 512
    show: true
    prompt: { template: 2 }
    message_passing: { input: true, output: true }

  - id: "branch_social"
    model: 0
    temperature: 0.5
    max_tokens: 512
    show: true
    prompt: { template: 3 }
    message_passing: { input: true, output: true }

  - id: "synthesiser"
    model: 0
    temperature: 0.5
    max_tokens: 1024
    show: true
    prompt: { template: 4 }
    message_passing: { input: true }

edges:
  - node: "dispatcher"
    batch_children:          # one batch job for all three branches
      - node: "branch_economic"
      - node: "branch_environmental"
      - node: "branch_social"

  - node: "synthesiser"
    batch_fan_in:            # wait for all three batch results
      - node: "branch_economic"
      - node: "branch_environmental"
      - node: "branch_social"

All three branches are submitted in a single batch job. The synthesiser waits for all three before running.


5. Provider notes

Provider Activation Extra config needed
anthropic Automatic when batch mode is declared None
anthropic_aws Automatic None
openai Automatic None
gemini Automatic None
bedrock Automatic batch_role_arn, batch_s3_input_uri, batch_s3_output_uri on the model
ollama Thread pool fallback None

For the full provider reference and Bedrock S3 configuration details, see Batch Inference Reference.