Aller au contenu principal

Formation distribuée avec Ray Train.

info

Aperçu

Cette fonctionnalité est en aperçu public.

Cet exemple exécute l'affinement distribué de données parallèles avec TorchTrainer de Ray Train sur 8 GPU H100 sur un seul nœud. Un script de démarrage start un cluster Ray sur le nœud, puis le Driver Ray Train lance un Worker par GPU, enveloppe le modèle dans DDP et fragmente le dataset automatiquement entre les Workers.

Il affine un modèle public (Qwen2.5-3B), afin qu’il s’exécute tel quel sans jeton Hugging Face.

La charge de travail réalise les étapes suivantes :

  • Importe le projet local avec code_source: snapshot.
  • start un Ray head avec les 8 GPU, puis exécute le Driver Ray Train.
  • Utilise ray.train.torch.prepare_model et prepare_data_loader pour gérer l'encapsulation DDP, le placement des appareils et l'échantillonnage distribué.
  • Logs des métriques vers MLflow.

Prérequis

Project Layout

Créez un répertoire avec les fichiers suivants.

Text
ray_train_distributed/
├── train.yaml # air workload config (inline dependencies + Ray bootstrap)
└── train_ray.py # Ray Train TorchTrainer driver + per-worker training

Étape 1 : Rédiger le YAML de la charge de travail

train.yaml demande un seul nœud GPU_8xH100. Les dépendances sont déclarées en ligne sous environment (avec le runtime version), et le command démarre un cluster Ray sur le nœud puis exécute le Driver, de sorte que le workload n'a pas besoin de fichier de dépendance ou de script de lancement séparé :

YAML
experiment_name: air-ray-train-distributed

environment:
version: 'AI5'
dependencies:
# AI5 (the databricks-ai runtime) already ships ray, transformers, datasets, and
# huggingface_hub, so they no longer need to be listed here. It does ship fsspec
# 2023.5.0, which is too old for modern huggingface_hub and breaks dataset/model
# downloads, so pin a newer fsspec.
- fsspec>=2024.6.1

# 8 H100 on a single node. Ray Train launches one worker 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 train_ray.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: 90
env_variables:
NCCL_SOCKET_IFNAME: eth0

Le command en ligne démarre une tête Ray avec tous les GPU sur le nœud, exécute le Driver avec python train_ray.py, puis arrête le cluster. Il inclut également une Branch Worker qui rejoint la tête, de sorte que la même commande continue de fonctionner si vous Monter en charge le Job vers plusieurs nœuds.

Étape 2 : Définir le Driver Ray Train

train_ray.py définit un train_func qui s’exécute sur chaque Worker et un main qui configure le TorchTrainer pour utiliser tous les GPU dans le cluster. prepare_model encapsule le modèle dans DDP et le déplace vers le GPU du Worker. prepare_data_loader ajoute un échantillonneur distribué :

Python
def train_func(config: dict):
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, torch_dtype=torch.bfloat16)
model.config.use_cache = False
model = prepare_model(model) # DDP wrap + device placement

loader = DataLoader(dataset, batch_size=config["batch_size"], shuffle=True, drop_last=True)
loader = prepare_data_loader(loader) # distributed sampler + GPU transfer
optimizer = torch.optim.AdamW(model.parameters(), lr=config["lr"])
...
ray.train.report({"loss": out.loss.item(), "step": step})


def main():
ray.init(address="auto")
total_gpus = int(ray.cluster_resources().get("GPU", 0))
trainer = TorchTrainer(
train_func,
train_loop_config={"lr": 2e-5, "batch_size": 4, "max_steps": 100},
scaling_config=ScalingConfig(num_workers=total_gpus, use_gpu=True),
)
trainer.fit()

Le script complet est répertorié dans Script d'entraînement complet à la fin de cette page.

Étape 3 : Soumettre l'exécution

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

Étape 4 : Inspecter l'exécution

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

Le Ray head et le Driver s'exécutent tous deux sur le nœud 0, de sorte que les Logs sont Stream à partir d'un seul nœud.

Où les résultats aboutissent

Les métriques signalées avec ray.train.report et consignées avec MLflow apparaissent dans l'expérience MLflow nommée dans experiment_name, visible dans l'interface utilisateur MLflow du workspace.

Script de formation complet

Le train_ray.py complet pour le copier-coller :

Python
#!/usr/bin/env python3
"""Distributed data-parallel fine-tuning with Ray Train on a single 8x H100 node.

The workload `command` starts a Ray head with 8 GPUs and runs this script. Ray Train's
TorchTrainer launches one worker per GPU (8 total), wraps the model in DDP, shards
the dataset across workers, and aggregates metrics. Each worker runs `train_func`.

Uses a public model (no Hugging Face token required) so the example runs as-is.
"""

import os

import mlflow
import ray
import ray.train
import torch
from datasets import load_dataset
from ray.train import RunConfig, ScalingConfig
from ray.train.torch import TorchTrainer, prepare_data_loader, prepare_model
from torch.utils.data import DataLoader
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL_NAME = "Qwen/Qwen2.5-3B"
DATASET_NAME = "tatsu-lab/alpaca"
MAX_SEQ_LEN = 1024


def build_dataset(tokenizer):
raw = load_dataset(DATASET_NAME, split="train[:8000]")

def format_example(row):
prompt = f"### Instruction:\n{row['instruction']}\n\n"
if row.get("input"):
prompt += f"### Input:\n{row['input']}\n\n"
text = f"{prompt}### Response:\n{row['output']}{tokenizer.eos_token}"
out = tokenizer(text, truncation=True, max_length=MAX_SEQ_LEN, padding="max_length")
out["labels"] = out["input_ids"].copy()
return out

return raw.map(format_example, remove_columns=raw.column_names)


def train_func(config: dict):
"""Runs on every Ray Train worker (one per GPU)."""
rank = ray.train.get_context().get_world_rank()

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, torch_dtype=torch.bfloat16)
model.config.use_cache = False
# prepare_model moves the model to this worker's GPU and wraps it in DDP.
model = prepare_model(model)

dataset = build_dataset(tokenizer).with_format("torch")
loader = DataLoader(dataset, batch_size=config["batch_size"], shuffle=True, drop_last=True)
# prepare_data_loader injects a DistributedSampler and moves batches to the GPU.
loader = prepare_data_loader(loader)

optimizer = torch.optim.AdamW(model.parameters(), lr=config["lr"])

# AI Runtime injects MLFLOW_RUN_ID and configures the databricks tracking URI on
# the node, so logging works without DATABRICKS_HOST/TOKEN. Gate on MLFLOW_RUN_ID
# so the script also runs cleanly off-platform (e.g. locally) where it is unset.
use_mlflow = rank == 0 and bool(os.environ.get("MLFLOW_RUN_ID"))
if use_mlflow:
mlflow.start_run(run_id=os.environ.get("MLFLOW_RUN_ID"))
mlflow.log_params({"model": MODEL_NAME, "lr": config["lr"], "batch_size": config["batch_size"]})

model.train()
step = 0
for batch in loader:
out = model(
input_ids=batch["input_ids"],
attention_mask=batch["attention_mask"],
labels=batch["labels"],
)
out.loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
optimizer.zero_grad()
step += 1

ray.train.report({"loss": out.loss.item(), "step": step})
if use_mlflow:
mlflow.log_metric("train_loss", out.loss.item(), step=step)
if step >= config["max_steps"]:
break

if use_mlflow:
mlflow.end_run()


def main():
ray.init(address="auto")
total_gpus = int(ray.cluster_resources().get("GPU", 0))
print(f"Ray cluster ready: {total_gpus} GPU(s)", flush=True)

trainer = TorchTrainer(
train_func,
train_loop_config={&quot;lr&quot;: 2e-5, &quot;batch_size&quot;: 4, &quot;max_steps&quot;: 100},
scaling_config=ScalingConfig(num_workers=total_gpus, use_gpu=True),
run_config=RunConfig(storage_path="/tmp/ray_results", name="qwen-sft"),
)
result = trainer.fit()
print(f"Training finished. Final metrics: {result.metrics}", flush=True)

ray.shutdown()


if __name__ == "__main__":
main()

Ressources supplémentaires