Wnioskowanie wsadowe z użyciem Ray Data i vLLM

Ważna

Ta funkcja jest dostępna w publicznej wersji testowej.

Ten przykład uruchamia wsadowe wnioskowanie LLM w trybie offline z użyciem Ray Data i vLLM na 8 układach GPU H100 w jednym węźle. Skrypt rozruchowy uruchamia klaster Ray na węźle, a następnie proces sterownika używa interfejsu API LLM Ray Data (ray.data.llm), aby uruchomić jedną replikę vLLM na każdy procesor GPU i strumieniowo przesyłać przez nie zbiór danych promptów, zapisując wygenerowany tekst do woluminu Unity Catalog w formacie Parquet.

Korzysta z publicznego modelu (Qwen2.5-7B-Instruct), więc działa od razu bez tokenu Hugging Face.

Obciążenie robocze realizuje następujące działania:

  • Przesyła lokalny projekt za pomocą polecenia code_source: snapshot.
  • Uruchamia głowicę Ray ze wszystkimi 8 procesorami GPU, a następnie uruchamia sterownik wnioskowania wsadowego.
  • Używa ray.data.llm do uruchamiania jednej repliki vLLM na procesor GPU i monitów procesów równolegle.
  • Zapisuje prompty i wygenerowane wyniki w woluminie Unity Catalog w formacie Parquet.

Wymagania wstępne

Układ projektu

Utwórz katalog z następującymi plikami.

ray_batch_inference/
├── train.yaml            # air workload config (inline dependencies + Ray bootstrap)
└── batch_inference.py    # Ray Data + vLLM batch inference driver

Krok 1. Zapisywanie obciążenia YAML

train.yaml żąda jednego GPU_8xH100 węzła. Zależności są deklarowane bezpośrednio pod environment (wraz z obrazem klienta version), a command uruchamia klaster Ray na węźle, a następnie uruchamia proces sterownika, więc zadanie nie wymaga oddzielnego pliku zależności ani skryptu startowego.

vLLM nie znajduje się w obrazie bazowym, więc jest instalowany bezpośrednio w poleceniu wraz z trzema przypiętymi wersjami pakietów wymaganymi przez węzły GPU: hf_transfer (obraz bazowy umożliwia szybkie pobieranie z Hugging Face i oczekuje tego pakietu), nowszą wersją fsspec (obraz bazowy zawiera starszą, która psuje pobieranie) oraz przypiętą wersją opencv-python-headless (vLLM instaluje OpenCV, którego domyślny wheel powoduje awarię autotestu FIPS biblioteki OpenSSL na węzłach GPU).

Ustaw OUTPUT_PATH na wolumin Unity Catalog, do którego można zapisywać.

experiment_name: air-ray-batch-inference

environment:
  version: '4'
  dependencies:
    - ray[data]>=2.44
    - 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

# 8 H100 on a single node. Ray Data runs one vLLM replica per GPU.
compute:
  num_accelerators: 8
  accelerator_type: GPU_8xH100

code_source:
  type: snapshot
  snapshot:
    root_path: .

command: |
  cd $CODE_SOURCE_PATH
  RAY_HEAD_PORT=6379
  GPUS_PER_NODE=${LOCAL_WORLD_SIZE:-8}
  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
    python batch_inference.py
    ray stop
  else
    echo "NODE_RANK=$NODE_RANK: connecting to Ray head at $MASTER_ADDR:$RAY_HEAD_PORT..."
    for i in $(seq 1 12); do
      if ray start --address="$MASTER_ADDR:$RAY_HEAD_PORT" --num-gpus="$GPUS_PER_NODE" --block 2>/dev/null; then
        break
      fi
      echo "Attempt $i failed, retrying in 5s..."
      sleep 5
    done
  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

Dyrektywa inline command uruchamia głowicę Ray ze wszystkimi GPU w węźle, uruchamia sterownik za pomocą python batch_inference.py, a następnie zatrzymuje klaster. Zawiera również odgałęzienie robocze, które dołącza do węzła głównego, dzięki czemu to samo polecenie nadal działa po skalowaniu zadania do wielu węzłów.

Krok 2: Definiowanie sterownika do wnioskowania wsadowego

batch_inference.py tworzy zbiór danych Ray z promptami, konfiguruje procesor vLLM przy użyciu ray.data.llm i zapisuje wyniki. concurrency to liczba replik vLLM uruchamianych równolegle przez Ray Data. Ustawienie tej wartości na liczbę GPU w klastrze daje jedną replikę na każde GPU, więc prompty są przetwarzane równolegle na wszystkich GPU, a przykład skaluje się wraz z dodawaniem kolejnych węzłów:

from ray.data.llm import build_llm_processor, vLLMEngineProcessorConfig

# Read the GPU count from the live Ray cluster so concurrency scales with the cluster.
total_gpus = int(ray.cluster_resources().get("GPU", 0))

config = vLLMEngineProcessorConfig(
    model_source="Qwen/Qwen2.5-7B-Instruct",
    engine_kwargs={"max_model_len": 4096, "tensor_parallel_size": 1},
    concurrency=total_gpus,   # one vLLM replica per GPU in the cluster
    batch_size=64,
)

processor = build_llm_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 przekształca każdy wiersz danych wejściowych w żądanie czatu, a postprocess zachowuje kolumny, które mają zostać zapisane. Ray Data dodaje kolumnę generated_text z danymi wyjściowymi modelu. Pełny skrypt znajduje się w skrygcie pełnego sterownika na końcu tej strony.

W przypadku większych modeli ustaw tensor_parallel_size, aby podzielić jedną replikę między kilka procesorów GPU, i podziel total_gpus przez tę wartość, tak aby repliki nadal wypełniały klaster, na przykład concurrency=total_gpus // 2 z ustawieniem tensor_parallel_size=2.

Krok 3: Prześlij uruchomienie

air run -f train.yaml --dry-run
air run -f train.yaml --watch

Krok 4. Sprawdzanie przebiegu

air get run <run-id>
air logs <run-id>

Dzienniki pokazują przepustowość przetwarzania monitu i generowania silnika vLLM podczas wykonywania wsadu, a następnie wiersz Wrote <n> rows, gdy dane wyjściowe zostaną zapisane.

Gdzie wyniki lądują

Sterownik zapisuje jeden zestaw danych Parquet w woluminie OUTPUT_PATH z kolumną instruction i kolumną output . Odczytaj go z powrotem za pomocą platformy Spark lub biblioteki pandas, na przykład spark.read.parquet(OUTPUT_PATH).

Pełny skrypt sterownika

Kompletny batch_inference.py do kopiowania i wklejania:

#!/usr/bin/env python3
"""Offline batch inference with Ray Data + vLLM on a single 8x H100 node.

The workload `command` starts a Ray head with 8 GPUs and runs this script. 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 ray
from datasets import load_dataset
from ray.data.llm import build_llm_processor, vLLMEngineProcessorConfig

MODEL_SOURCE = "Qwen/Qwen2.5-7B-Instruct"
NUM_PROMPTS = 1000
# 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")
    # Derive replicas from the live cluster so the example scales when nodes are added.
    total_gpus = int(ray.cluster_resources().get("GPU", 0))
    print(f"Ray cluster ready: {total_gpus} GPU(s)", flush=True)

    ds = build_prompts()

    # vLLM engine config. concurrency = number of replicas Ray Data runs in parallel;
    # one per GPU in the cluster here. engine_kwargs are passed through to the vLLM engine.
    config = vLLMEngineProcessorConfig(
        model_source=MODEL_SOURCE,
        engine_kwargs={
            "max_model_len": 4096,
            "tensor_parallel_size": 1,
            "enable_chunked_prefill": True,
        },
        concurrency=total_gpus,
        batch_size=64,
    )

    # 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_llm_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()

Następne kroki