Important
這項功能目前處於 公開預覽版。
此範例使用 Ray Train 的 TorchTrainer,在單一節點上的 8 顆 H100 GPU 上執行分散式資料平行微調。 一個引導腳本會在節點上啟動 Ray 叢集,然後 Ray Train 驅動程式會為每個 GPU 啟動一個工作者,然後將模型包裝成 DDP,並自動將資料集分片到各個工作者之間。
它對一個公開模型(Qwen2.5-3B)進行微調,因此無需 Hugging Face 權杖即可直接執行。
工作量會做出以下效果:
- 使用
code_source: snapshot上傳本地專案。 - 啟動一台 Ray 主機,連同 8 顆 GPU,然後執行 Ray Train 驅動程式。
- 使用
ray.train.torch.prepare_model及prepare_data_loader處理 DDP 包裝、裝置放置及分散式取樣。 - 將指標記錄到 MLflow。
先決條件
-
airCLI 已安裝並完成驗證。 請參閱 安裝 AI 執行階段 CLI。
專案版面配置
建立一個包含以下檔案的目錄。
ray_train_distributed/
├── train.yaml # air workload config (inline dependencies + Ray bootstrap)
└── train_ray.py # Ray Train TorchTrainer driver + per-worker training
步驟 1:撰寫 YAML 工作負載
train.yaml 請求單一 GPU_8xH100 節點。 相依關係會在執行environmentversion時內聯宣告,然後在command節點上啟動 Ray 叢集,然後執行驅動程式,因此工作負載不需要獨立的相依檔案或啟動腳本:
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
內嵌的 command 會使用該節點上的所有 GPU 啟動一個 Ray head 節點,接著使用 python train_ray.py 執行驅動程式,然後停止叢集。 它還包含一個工作分支,可以連接頭部,所以即使你將工作擴展到多個節點,同一指令仍能持續運作。
步驟 2:定義 Ray Train 驅動程式
train_ray.py 定義了一個會在每個工作節點上執行的 train_func,以及一個用來設定 main 以使用叢集中所有 GPU 的 TorchTrainer。
prepare_model 將模型包裹在 DDP 中,並移至工作者的 GPU。
prepare_data_loader 新增一個分散式取樣器:
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()
完整腳本列在本頁末尾的 完整訓練腳本 中。
步驟三:提交跑量
air run -f train.yaml --dry-run
air run -f train.yaml --watch
步驟 4:檢查執行結果
air get run <run-id>
air logs <run-id>
Ray 頭和驅動程式都跑在節點 0,所以日誌串流來自單一節點。
結果落在哪裡
用 ray.train.report MLflow 報告並記錄的指標會出現在命名為 experiment_name的 MLflow 實驗中,該實驗可在工作區的 MLflow UI 中查看。
完整訓練腳本
完整的 train_ray.py,供複製貼上:
#!/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={"lr": 2e-5, "batch_size": 4, "max_steps": 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()