หมายเหตุ
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลอง ลงชื่อเข้าใช้หรือเปลี่ยนไดเรกทอรีได้
การเข้าถึงหน้านี้ต้องได้รับการอนุญาต คุณสามารถลองเปลี่ยนไดเรกทอรีได้
Important
This feature is in Public Preview.
This example runs offline LLM batch inference with
Ray Data and
vLLM across 4 A10 nodes. A bootstrap script starts a
Ray cluster across the nodes, then the driver uses Ray Data's LLM API (ray.data.llm) to
launch one vLLM replica per node and stream a dataset of prompts through them, writing the
generated text to a Unity Catalog volume as Parquet.
It uses a public model (Qwen2.5-7B-Instruct), so it runs as-is without a Hugging Face token.
The workload does the following:
- Uploads the local project with
code_source: snapshot. - Starts a Ray head on node 0, joins 3 worker nodes, then runs the batch inference driver.
- Uses
ray.data.llmto run one vLLM replica per node and process prompts in parallel. - Writes the prompts and generated outputs to a Unity Catalog volume as Parquet.
Prerequisites
- The
airCLI installed and authenticated. See Install the AI Runtime CLI. - A Unity Catalog volume you can write to. You set its path in the workload YAML below.
Project layout
Create a directory with the following files.
ray_batch_inference/
├── train.yaml # air workload config (inline dependencies + Ray bootstrap)
└── batch_inference.py # Ray Data + vLLM batch inference driver
Step 1: Write the workload YAML
train.yaml requests 4 GPU_1xA10 nodes. Dependencies are declared inline under
environment (with the client image version), and the command starts a Ray cluster
across the nodes then runs the driver, so the workload doesn't need a separate dependency
file or launcher script.
vLLM isn't in the base image, so it's installed inline along with three pins the GPU nodes
need: hf_transfer (the base image enables fast Hugging Face downloads and expects this
package), a newer fsspec (the base image ships an old one that breaks downloads), and a
pinned opencv-python-headless (vLLM pulls in OpenCV, whose default wheel crashes the
OpenSSL FIPS self-test on the GPU nodes).
Set OUTPUT_PATH to a Unity Catalog volume you can write to. Set NUM_GPUS to the same value as num_accelerators.
experiment_name: air-ray-batch-inference
environment:
version: '5'
dependencies:
- ray[data]==2.56.1
- vllm
- datasets>=3.0
- huggingface_hub>=0.34
# The base image sets HF_HUB_ENABLE_HF_TRANSFER=1; install the package it expects
# so model and dataset downloads don't error out.
- hf_transfer
# The base image ships fsspec 2023.5.0, which is too old for modern
# huggingface_hub and breaks dataset/model downloads. Pin a newer fsspec.
- fsspec>=2024.6.1
# vLLM pulls in opencv; its default wheel crashes the OpenSSL FIPS self-test
# on the GPU nodes. This pinned headless build avoids the crash.
- opencv-python-headless==4.12.0.88
# 4 A10 nodes, one GPU each. Ray Data runs one vLLM replica per node.
compute:
num_accelerators: 4
accelerator_type: GPU_1xA10
code_source:
type: snapshot
snapshot:
root_path: .
command: |
set -e
cd $CODE_SOURCE_PATH
RAY_HEAD_PORT=6379
GPUS_PER_NODE=${LOCAL_WORLD_SIZE:-1}
if [ "${NODE_RANK:-0}" = "0" ]; then
echo "NODE_RANK=0: starting Ray head with $GPUS_PER_NODE GPU(s)..."
ray start --head --port=$RAY_HEAD_PORT --num-gpus="$GPUS_PER_NODE" --dashboard-host=0.0.0.0
trap 'ray stop || true' EXIT
python batch_inference.py
else
echo "NODE_RANK=$NODE_RANK: connecting to Ray head at $MASTER_ADDR:$RAY_HEAD_PORT..."
joined=""
for i in $(seq 1 12); do
if ray start --address="$MASTER_ADDR:$RAY_HEAD_PORT" --num-gpus="$GPUS_PER_NODE" 2>/dev/null; then
joined=1
break
fi
echo "Attempt $i failed, retrying in 5s..."
sleep 5
done
if [ -z "$joined" ]; then
echo "Worker failed to join the Ray head after all retries." >&2
exit 1
fi
echo "Worker joined. Waiting for the head to finish..."
consecutive_failures=0
for _ in $(seq 1 720); do
if timeout 5 ray health-check --address "$MASTER_ADDR:$RAY_HEAD_PORT" 2>/dev/null; then
consecutive_failures=0
else
consecutive_failures=$((consecutive_failures + 1))
if [ "$consecutive_failures" -ge 3 ]; then
echo "Head is no longer healthy. Stopping local Ray processes..."
ray stop || true
exit 0
fi
echo "Head health check failed ($consecutive_failures/3). Retrying..."
fi
sleep 5
done
echo "Timed out waiting for the Ray head to finish." >&2
ray stop || true
exit 1
fi
max_retries: 0
timeout_minutes: 60
env_variables:
NCCL_SOCKET_IFNAME: eth0
# Unity Catalog volume where results land as Parquet. Replace with your volume.
OUTPUT_PATH: /Volumes/main/default/air_examples/ray_batch_inference
NUM_GPUS: '4' # must match num_accelerators
The inline command starts a Ray head with the node's GPU on node 0, then runs the driver
with python batch_inference.py. Worker nodes join the head using MASTER_ADDR and
NODE_RANK, which the platform sets automatically. Each worker monitors the head and stops
its local Ray processes after three consecutive health check failures.
Step 2: Define the batch inference driver
batch_inference.py builds a Ray Dataset of prompts, configures a vLLM processor with
ray.data.llm, and writes the results. The driver waits for all nodes to join before
reading the GPU count. AIR provisions a fixed accelerator pool, so the driver sets
concurrency to a fixed (minimum, maximum) tuple that requests one replica per GPU.
Because this example uses a short, fixed workload, the driver waits up to 300 seconds for
all replicas to initialize before dispatching work. Each actor processes up to two batches
concurrently and has at most two submitted Ray Data tasks, including running and queued
tasks. This prevents the first actor to initialize from reserving most of the workload. The
2,000 prompts are split into 32 input blocks, with eight blocks available per replica. For
longer workloads, tune these settings based on startup time and throughput requirements:
import os
import time
import ray
from ray.data import DataContext
from ray.data.llm import build_processor, vLLMEngineProcessorConfig
ray.init(address="auto")
data_context = DataContext.get_current()
data_context.wait_for_min_actors_s = 300
num_gpus = int(os.environ["NUM_GPUS"])
for _ in range(60):
if int(ray.cluster_resources().get("GPU", 0)) >= num_gpus:
break
time.sleep(5)
total_gpus = int(ray.cluster_resources().get("GPU", 0))
if total_gpus < num_gpus:
raise SystemExit(f"Expected {num_gpus} GPU(s) but Ray only sees {total_gpus}.")
ds = build_prompts().repartition(total_gpus * 8)
config = vLLMEngineProcessorConfig(
model_source="Qwen/Qwen2.5-7B-Instruct",
engine_kwargs={"max_model_len": 4096, "tensor_parallel_size": 1},
concurrency=(total_gpus, total_gpus),
batch_size=64,
max_concurrent_batches=2,
max_tasks_in_flight_per_actor=2,
)
processor = build_processor(
config,
preprocess=lambda row: dict(
messages=[{"role": "user", "content": row["instruction"]}],
sampling_params=dict(max_tokens=256, temperature=0.7),
),
postprocess=lambda row: dict(instruction=row["instruction"], output=row["generated_text"]),
)
out = processor(ds) # ds is a Ray Dataset with an "instruction" column
out.write_parquet(OUTPUT_PATH)
preprocess turns each input row into a chat request, and postprocess keeps the columns
to persist. Ray Data adds a generated_text column with the model's output. The complete
script is in Full driver script at the end of this page.
tensor_parallel_size=1 keeps each vLLM replica on one A10 GPU.
Step 3: Submit the run
air run -f train.yaml --dry-run
air run -f train.yaml --watch
Step 4: Inspect the run
air get run <run-id>
air logs <run-id>
The logs show the vLLM engine's prompt and generation throughput as the batch runs, then a
Wrote <n> rows line when the output is written.
Where results land
The driver writes one Parquet dataset to the OUTPUT_PATH volume, with an instruction
column and an output column. Read it back with Spark or pandas, for example
spark.read.parquet(OUTPUT_PATH).
Full driver script
The complete batch_inference.py for copy-paste:
#!/usr/bin/env python3
"""Offline batch inference with Ray Data + vLLM across 4 A10 nodes.
The workload `command` starts a Ray head on node 0 and joins 3 worker nodes, each
contributing 1 GPU. Ray Data's LLM API (`ray.data.llm`) launches one vLLM replica
per GPU and streams a dataset of prompts through them, then writes the generated text
to a Unity Catalog volume as Parquet.
Uses a public model (no Hugging Face token required) so the example runs as-is.
"""
import os
import time
import ray
from datasets import load_dataset
from ray.data import DataContext
from ray.data.llm import build_processor, vLLMEngineProcessorConfig
MODEL_SOURCE = "Qwen/Qwen2.5-7B-Instruct"
NUM_PROMPTS = 2000
BATCH_SIZE = 64
BLOCKS_PER_REPLICA = 8
# Unity Catalog volume path where results land as Parquet. Set this in train.yaml.
OUTPUT_PATH = os.environ.get("OUTPUT_PATH", "/Volumes/main/default/air_examples/ray_batch_inference")
def build_prompts():
"""Build a Ray Dataset of prompts from a public instruction dataset."""
raw = load_dataset("tatsu-lab/alpaca", split=f"train[:{NUM_PROMPTS}]")
items = []
for row in raw:
instruction = row["instruction"]
if row.get("input"):
instruction = f"{instruction}\n\n{row['input']}"
items.append({"instruction": instruction})
return ray.data.from_items(items)
def main():
ray.init(address="auto")
data_context = DataContext.get_current()
data_context.wait_for_min_actors_s = 300
num_gpus = int(os.environ["NUM_GPUS"])
for _ in range(60):
if int(ray.cluster_resources().get("GPU", 0)) >= num_gpus:
break
time.sleep(5)
total_gpus = int(ray.cluster_resources().get("GPU", 0))
if total_gpus < num_gpus:
raise SystemExit(
f"Expected {num_gpus} GPU(s) but Ray only sees {total_gpus}; "
"check GPU discovery / node join on all nodes."
)
print(f"Ray cluster ready: {total_gpus} GPU(s)", flush=True)
ds = build_prompts().repartition(total_gpus * BLOCKS_PER_REPLICA)
# AIR provisions a fixed accelerator pool. Bound prefetching so the first ready
# actor cannot reserve the small workload before the other actors initialize.
config = vLLMEngineProcessorConfig(
model_source=MODEL_SOURCE,
engine_kwargs={
"max_model_len": 4096,
"tensor_parallel_size": 1,
"enable_chunked_prefill": True,
},
concurrency=(total_gpus, total_gpus),
batch_size=BATCH_SIZE,
max_concurrent_batches=2,
max_tasks_in_flight_per_actor=2,
)
# preprocess maps each input row to a chat request; postprocess keeps the columns
# we want to persist. ray.data.llm adds a `generated_text` column.
processor = build_processor(
config,
preprocess=lambda row: dict(
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": row["instruction"]},
],
sampling_params=dict(max_tokens=256, temperature=0.7),
),
postprocess=lambda row: dict(
instruction=row["instruction"],
output=row["generated_text"],
),
)
# materialize once so the write and the sample print don't re-run inference.
out = processor(ds).materialize()
out.write_parquet(OUTPUT_PATH)
print(f"Wrote {out.count()} rows to {OUTPUT_PATH}", flush=True)
for row in out.take(2):
print("INSTRUCTION:", row["instruction"][:120], flush=True)
print("OUTPUT:", row["output"][:200], flush=True)
ray.shutdown()
if __name__ == "__main__":
main()