Skip to content
Blog

Batch Inference at Scale with Ray

Offline LLM work — scoring, summarization, backfills — needs throughput and reliability, not latency. Distribute thousands of calls with Ray tasks and actors, retries, and structured cost tracking.

Published on • August 3, 2026

AI Assistant

Batch inference is the cost and throughput play: run thousands of LLM calls reliably, cheaply, and observably — instead of paying for per-interactive-request spikes.

When you need batch inference

Interactive chat needs per-request latency. But a lot of LLM work is offline: scoring a corpus, summarizing last week’s logs, classifying a million tickets, regenerating evals, or backfilling a vector store. For those, latency per call isn’t the concern — total time, cost, and reliability are. A crash ten minutes into a marathon should not restart from scratch.

Ray: the distributed compute substrate

Ray is an open-source framework for distributing Python. Its core abstractions are tasks (an arbitrary Python function executed on a cluster) and actors (long-lived objects that keep state between calls). It is the right tool when you want to map over a large number of LLM calls with retries, checkpointing, and a rich graph.

Install: pip install "ray[default]" then ray start --head for a local cluster.

The batch worker: an actor

A @ray.remote actor wraps the model client. Each remote call runs on the cluster and keeps a single client, avoiding re-initialization overhead per request.

import ray
from google import genai

@ray.remote
class Generator:
    def __init__(self, model="gemini-2.5-pro"):
        self.client = genai.Client()
        self.model = model

    def generate(self, prompt: str) -> str:
        resp = self.client.models.generate_content(model=self.model, contents=prompt)
        return resp.text

Submitting a big batch

Fan out a list of tasks to the actor pool and gather them:

actors = [Generator.remote() for _ in range(NUM_WORKERS)]
futures = [actors[i % NUM_WORKERS].generate.remote(p) for i, p in enumerate(prompts)]
results = ray.get(futures)   # blocks until all done

for doc, text in zip(documents, results):
    doc.generated = text

ray.get waits for all, and because each task is separate, Ray resubmits a failed task without rerunning the whole batch.

Reliability: retries and rate limits

LLM APIs apply rate limits (HTTP 429) and transient errors. Wrap the call in retry logic with exponential backoff:

from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type

class RateLimitError(Exception):
    pass

@retry(reraise=True, stop=stop_after_attempt(5),
       wait=wait_exponential(max=60),
       retry=retry_if_exception_type(RateLimitError))
def generate_with_retry(actor, prompt):
    return actor.generate.remote(prompt)

This is the ref’s “make cost visible and operations professional”: a named job handled in backoff on rate-limit and resumable on failure.

Observability and tracking

Wrap each generation with a trace_id and log a structured row (model, tokens, prompt ID, latency). Ray ships a dashboard (default :8265). The ref’s theme — “You cannot manage what you cannot measure” — collect throughput, failure rate, and a token-to-cost tally per run and store them for post-hoc audits.

Putting It All Together

import ray
from my_executor import Generator, build_prompts

ray.init()
gen = Generator.remote()

prompts = build_prompts()                     # e.g. 5,000 tagged sentences
futures = [gen.generate.remote(p) for p in prompts]
outputs = ray.get(futures)

for rec, text in zip(records, outputs):
    log_row({"model": model, "tokens": count(text), "cost_usd": estimate_cost(text)})
    store(rec, text)

Conclusion & Next Steps

Batch inference with Ray turns heavy offline LLM work into a resumable, retried, observable pipeline. Next: add checkpoint progress so a failed run continues where it left off, add back-pressure for very large sets, and attach the trace/cost pipeline from the observability draft. — make cost visible and operations professional.

References / Sources