Ray
Distributed Python for training, batch inference, and serving — Ray Train, Data, and Serve patterns for LLM platforms.
What Ray is
Ray is a Python-native distributed runtime for scaling AI workloads across a cluster: tasks/actors, data-parallel maps, multi-worker training, hyperparam search, and model serving.
When one machine is not enough — fine-tunes, embedding backfills, offline evals, multi-model serving — platforms reach for Ray (Anyscale, Databricks-adjacent stacks, many internal ML platforms).
flowchart TB
Job[Driver / job] --> Train[Ray Train]
Job --> Data[Ray Data]
Job --> Serve[Ray Serve]
Job --> Tune[Ray Tune]
Train --> Workers[GPU workers]
Data --> CPU[CPU/GPU map batches]
Serve --> Replicas[HTTP replicas]
Interview cue: Ray is orchestration + distribution, not a decode engine. Often Ray schedules work; vLLM or TensorRT-LLM burns tokens.
The engineering problem
| Pain | Ray-shaped fix |
|---|---|
| Embed 50M chunks overnight | Ray Data map_batches with actor-pool GPUs |
| Multi-GPU LoRA / SFT | Ray Train workers + shared config |
| Serve embed + rank + generate | Ray Serve deployment graph |
| Sweep decoding / prompt params | Ray Tune / jobs |
| Shard golden-set evals | Remote tasks over partitions |
Without a cluster runtime you either: (1) hand-roll Kubernetes Jobs and pray for retries, or (2) keep everything on one box until the backlog is measured in days. Ray’s bet: same Python code, declared resources, object-store shuffle.
Architecture: pieces you will touch
| Library | Job |
|---|---|
| Ray Core | remote tasks/actors, resource hints (num_gpus, num_cpus) |
| Ray Data | Parallel map over parquet / images / rows |
| Ray Train | Multi-worker PyTorch / Lightning |
| Ray Tune | Hyperparam / trial search |
| Ray Serve | Deploy models behind HTTP with autoscaling |
flowchart LR
subgraph cluster [Ray cluster]
Head[Head node]
W1[Worker]
W2[Worker]
W3[Worker]
end
Client[Driver script / CI] --> Head
Head --> W1
Head --> W2
Head --> W3
Mental model: the driver submits work; workers execute with declared CPU/GPU resources; the object store shuffles intermediate results without forcing everything through the driver’s memory.
Tasks vs actors
| Primitive | Lifetime | Best for |
|---|---|---|
Task (@ray.remote fn) |
One call | Stateless map, eval shard |
Actor (@ray.remote class) |
Sticky process | Loaded models, GPU warm state |
Actors win for embedding / LLM wrappers: load weights once, call many times. Tasks win for pure CPU transforms where startup is cheap.
How it fits LLM apps
flowchart TB
API[Product API] --> Serve[Ray Serve]
Serve --> Emb[Embedding deployment]
Serve --> Gen[vLLM deployment]
Batch[Nightly job] --> Data[Ray Data embed backfill]
Data --> VDB[(Vector DB)]
TrainJob[Fine-tune job] --> Train[Ray Train]
Train --> Registry[Artifact store / MLflow]
| Pattern | Notes |
|---|---|
| Batch inference | Best first Ray win for RAG platforms |
| Serve graphs | Route small vs large models; A/B |
| Train | When you outgrow one box |
| Evals | Shard golden sets across workers; log run ids |
Pair batch paths with Kafka for evented AI when you need durable fan-out and replay; Ray Data shines when the corpus is already in object storage and you want an in-cluster map.
How to use — batch embed sketch
# Shape — embed a corpus with an actor pool
import ray
@ray.remote(num_gpus=0.25)
class Embedder:
def __init__(self):
self.model = load_model() # load once per actor
def __call__(self, batch: dict) -> dict:
return {
"vector": self.model.encode(batch["text"]),
"id": batch["id"],
"pipeline_version": "embedder@v3",
}
# ds = ray.data.read_parquet("s3://bucket/chunks/")
# ds = ds.map_batches(Embedder, concurrency=8, batch_size=64)
# ds.write_parquet("s3://bucket/embedded/")Write vectors to pgvector/S3; same job shape as Kafka workers, but orchestration stays in-process to the Ray cluster. Always stamp pipeline_version so re-embeds are idempotent upserts downstream (chunking and metadata).
Serve deployment graph (shape)
# Pseudocode — Serve graph in front of engines
# from ray import serve
#
# @serve.deployment(num_replicas=2, ray_actor_options={"num_gpus": 0.25})
# class Embed: ...
#
# @serve.deployment(num_replicas=1, ray_actor_options={"num_gpus": 1})
# class Generate:
# def __init__(self, embed):
# self.embed = embed
# self.engine = connect_vllm() # or HTTP to vLLM pool
#
# async def __call__(self, request):
# ...
#
# graph = Generate.bind(Embed.bind())
# serve.run(graph)Keep auth, quotas, and OpenAI-shaped routing in a gateway; use Serve for the multi-model product graph.
Operational knobs that matter
| Knob | Effect |
|---|---|
num_gpus / fractional GPUs |
Pack small embedders; never guess |
Actor concurrency / replicas |
Throughput vs VRAM |
| Object store memory | Huge intermediates OOM the cluster |
Batch size in map_batches |
GPU util vs latency of stragglers |
| Autoscaling on Serve | Interactive vs batch isolation |
| Image / Ray version pin | Version skew across workers kills jobs |
flowchart LR
Interactive[Chat / low latency] --> PoolA[Serve pool A]
Batch[Nightly backfill] --> PoolB[Job / Data pool B]
PoolA --> GPU1[GPU quota interactive]
PoolB --> GPU2[GPU quota batch]
Ship rule: interactive chat and nightly embed backfills must not share one unconstrained GPU pool — isolate with separate Serve apps, queues, or cluster resource groups (cost/latency routing).
Serve vs vLLM (and friends)
| Need | Prefer |
|---|---|
| Pure LLM decode throughput | vLLM / TensorRT-LLM / SGLang alone |
| Multi-model product graph + batch jobs | Ray Serve + engines |
| Event-driven ingest fan-out | Kafka |
| One GPU cron | Skip Ray until pain is real |
| K8s-only ops culture | K8s Jobs/CronJobs + queue — less Python magic |
Ray Serve can front vLLM replicas, do routing (small vs large), and A/B. For decode math, tune the engine — not Ray.
Failure modes
- Head node SPOF unless you design HA / managed Ray
- Version skew between workers — pin container images
- Driver as bottleneck —
ray.geton huge result lists - Object store blowups — materialize huge tables eagerly
- GPU fractional scheduling surprises — 0.25 × 5 ≠ free packing if fragmentation
- Silent actor OOMs — one bad batch kills a warm replica
- Missing trial/run ids — cannot join to OpenTelemetry or MLflow
Production checklist
- Pin Ray + CUDA + model image digests per job.
- Declare
num_gpus/num_cpusexplicitly — no “best effort.” - Separate interactive Serve from batch Data/Train quotas.
- Stamp
pipeline_version+git_shaon every written vector/artifact. - Propagate
trace_id/run_idinto logs and OTel. - Alert on job failure rate, object-store pressure, and GPU idle during backlog.
- Rollback Serve = previous deployment graph revision, not “restart and hope.”
Alternatives
| Need | Prefer |
|---|---|
| Only chat decode at scale | vLLM / TRT-LLM / TGI alone |
| Durable event fan-out | Kafka / Pulsar / Kinesis |
| Managed serverless GPUs | Cloud batch / endpoints |
| Complex durable workflows + HITL | Temporal (+ Ray optional for compute) |
| Simple single-node maps | multiprocessing / local scripts |
Micro-project
- Sketch a Serve graph: embed → generate with resource annotations.
- Run a tiny local
map_batchesembed on a parquet sample. - Compare the same backfill designed as Kafka consumers — write three bullets on when you’d pick each.
- Guided Build & serve your SLM and Deploy, cost, latency, observability when leaving a single box.
Related
vLLM · Kafka for evented AI · Triton Inference Server · MLflow for LLMOps