はじめに
この記事は、松尾研LLMコンペ2025 への参加を通じて得た知見をまとめたものです。DeepSeek-R1 671Bのような大規模言語モデルの推論を高速かつスケーラブルに実行するために、vLLM と Ray を組み合わせたシステム構成を検証しました。本記事では、その過程で得られた経験や、遭遇した課題に対する解決策を整理して共有します。同様のシステムを構築する方々の参考になればと思います。
背景
vLLMとは
vLLMは、大規模言語モデルの推論を高速化するために設計されたオープンソースライブラリです。PagedAttentionやContinuous batchingなどの最適化技術を通じて、従来のライブラリ(Transformersなど)より、数十倍のスループットの向上を実現できたことが特徴です。
主な特徴:
-
PagedAttention: vLLMが採用するメモリ最適化技術です。KVキャッシュを固定サイズのブロック単位に分割し管理することで、メモリ上に非連続的に配置できるようにします。これにより、メモリ断片化を防ぎ、GPUメモリを効率的に活用できます
-
Continuous batching: vLLMの計算効率を高めるための技術です。従来の静的バッチ処理では、バッチ内のすべてのリクエストが終わるまで次のリクエストを処理できません。継続的バッチ処理では、各リクエストの生成が完了次第、新しいリクエストをバッチに追加できます。これによりGPUの待機時間を削減し使用率を向上させます
Rayとは
Rayは、Pythonで開発された分散コンピューティングフレームワークです。複数のノードからなるクラスタを管理し、タスクを自動的に分散・実行します。クラスタでは Head Node が全体の制御を行い、Worker Node が実際の計算処理を担当します。これにより、開発者は分散処理の仕組みを意識することなく、スケーラブルで高性能なアプリケーションを容易に構築できます。
vLLMとの組み合わせのメリット:
-
水平スケーリング: 単一ノードのGPUメモリや処理能力の制約を超え、推論負荷を複数ノードに分散できます
-
負荷分散: Rayがリクエストを自動的に各vLLMワーカーへ振り分け、GPUリソースを最大限に活用します
-
柔軟な構成: ノード数やGPU数に応じて、動的にスケールアップ・ダウンできます
前提条件
本記事で紹介する構成は、以下の環境を前提としています:
実行環境
- クラスタ管理システム: Slurm(Simple Linux Utility for Resource Management)
- OS: Linux(Ubuntu 24.04等)
- GPU: Nvidia H100 80GB × 8基/ノード
- ストレージ: 高速NVMe SSD
Slurmについて
Slurmは、Linuxクラスタ向けのオープンソースジョブスケジューラです。複数の計算ノードを効率的に管理し、ユーザーからのジョブリクエストに応じてリソースを割り当てます。本記事では、Slurmのsbatchコマンドでバッチジョブを投入し、srunコマンドで各ノード上のプロセスを起動する方法を採用しています。
Slurmの主要コマンド:
-
sbatch: バッチジョブをスケジューラに投入 -
srun: ジョブ内で並列タスクを実行 -
scontrol: ノード情報の取得や制御 -
squeue: ジョブの状態確認
vLLMによるシングルノード推論
1. シングルノード推論のサンプルコード
事前準備: 環境構築
vLLMとRayをインストールしたConda環境を事前に構築しておく必要があります。以下のrequirements.txtを用意し、手順で環境を構築します:
# requirements.txt
datasets>=3.6.0
hf-transfer>=0.1.9
hydra-core>=1.3.2
jupyterlab>=4.4.2
matplotlib>=3.10.3
ollama>=0.4.8
openai>=1.79.0
環境構築スクリプトの実行:
# モジュールのロード
module purge
module load cuda/12.6 miniconda/24.7.1-py312
module load cudnn/9.6.0
module load nccl/2.24.3
# Conda環境の作成
conda create -n llmbench python=3.12 -y
source "$(conda info --base)/etc/profile.d/conda.sh"
conda activate llmbench
# 計算ノードでインタラクティブセッションを起動
srun --partition=<partition_name> \ # 使用するpartitionを指定
--nodelist=<node_list> \ # 使用するノードを指定(例:node[01-03])
--nodes=1 \
--ntasks=1 \
--cpus-per-task=8 \
--gpus-per-node=8 \
--time=00:30:00 \
--pty bash -l
# パッケージのインストール
conda install -c conda-forge --file requirements.txt
pip install \
--index-url https://download.pytorch.org/whl/cu126 \
torch==2.7.1+cu126 torchvision==0.22.1+cu126 torchaudio==2.7.1+cu126 \
vllm>=0.4.2 \
--extra-index-url https://pypi.org/simple
vLLMサーバーの起動
vllm serve Qwen/Qwen3-32B \
--tensor-parallel-size 8 \
--enable-reasoning \
--reasoning-parser qwen3 \
--rope-scaling '{"rope_type":"yarn","factor":4.0,"original_max_position_embeddings":32768}' \
--max-model-len 131072 \
--gpu-memory-utilization 0.95
主要パラメータの説明:
-
--tensor-parallel-size 8: モデルを8つのGPUに分割して並列実行 -
--enable-reasoning: 推論モードを有効化 -
--max-model-len 131072: 最大コンテキスト長を約13万トークンに設定 -
--gpu-memory-utilization 0.95: GPUメモリの95%まで使用
推論の実行
vLLMサーバーが起動したら、OpenAI互換APIを使って推論を実行します。
from openai import AsyncOpenAI
# vLLMサーバーに接続
client = AsyncOpenAI(
base_url="http://<head_node_ip>:8000/v1",
timeout=86400,
max_retries=3,
api_key="fakeapikey", # vLLMではAPI keyは不要
)
# 単一の推論リクエスト
response = await client.chat.completions.create(
model="Qwen/Qwen3-32B",
max_completion_tokens=4096,
messages=[
{"role": system_role, "content": system_prompt},
{"role": "user", "content": content}
],
stream=False,
)
content = response.choices[0].message.content
print(content)
2. シングルノード推論の問題点
シングルノード構成では、以下のような制約や課題があります:
GPUメモリの制約: DeepSeek-R1 671Bのような大規模モデルでは、モデルの重みやKVキャッシュが膨大なGPUメモリを消費します。シングルノード構成では OOM(Out of Memory) が発生する可能性があり、安定した推論が難しくなる場合があります。
スループットの限界: シングルノードでは同時に処理できるリクエスト数に上限があり、リクエストが集中するとレスポンスが遅延し、ピーク時の性能がボトルネックになります。また、ピーク時に合わせた高スペック構成では、平常時にリソースが余ってしまい、効率的とは言えません。
同時リクエスト処理のボトルネック: 長文生成リクエストが混在すると、他のリクエストが待たされることが発生します。さらに、シングルノード構成では障害が発生した場合、サービス全体が停止するリスクもあります。
これらの課題を解決するために、マルチノードでの分散構成を検討する必要があります。
vLLMとRayによるマルチノード推論
1. マルチノード構成の概要
マルチノード構成では、Rayクラスタを構築し、その上でvLLMサーバーを起動します。本構成では以下のような構造を採用しました:
- Head Node: Rayクラスタ全体を管理し、vLLMサーバーも起動
- Worker Node: 追加の計算リソースを提供
- Pipeline Parallel + Tensor Parallel: 大規模モデル(DeepSeek-R1 671Bなど)を複数ノード・GPU間で分散
アーキテクチャ図
┌─────────────────────────────────────────┐
│ Head Node │
│ ┌───────────────────────────────────┐ │
│ │ Ray Head (port 37173) │ │
│ │ - Cluster Management │ │
│ │ - Job Scheduling │ │
│ └───────────────────────────────────┘ │
│ ┌───────────────────────────────────┐ │
│ │ vLLM Server (port 8000) │ │
│ │ - Pipeline Parallel Size: 3 │ │
│ │ - Tensor Parallel Size: 8 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
│
│ Ray Cluster Network
│
┌─────────────────────────────────────────┐
│ Worker Node │
│ ┌───────────────────────────────────┐ │
│ │ Ray Worker │ │
│ │ - Connected to Head │ │
│ │ - GPU × 8 │ │
│ └───────────────────────────────────┘ │
└─────────────────────────────────────────┘
2. マルチノード推論のサンプルコード
本セクションでは、RayとvLLMを使用した3ノード推論の構築手順を説明します。
環境構成
| 項目 | 詳細 |
|---|---|
| ノード数 | 3ノード(Head + Worker) |
| GPU | Nvidia H100 80GB × 8基/ノード |
| vLLM | 0.10.0 |
| Ray | 2.48.0 |
| openai (Python Client) | 1.101.0 |
ステップ1: Rayクラスタの起動
ray_cluster.shスクリプトを使用してRayクラスタを起動します。事前にスクリプト内のpartition、nodelist、ログファイルのパスを環境に合わせて設定します。
#!/bin/bash
# ray_cluster.sh
#SBATCH --job-name=vllm-multinode
#SBATCH -p <partition_name> # 使用するpartitionを指定
#SBATCH --nodelist=<node_list> # 使用するノードを指定(例:node[01-03])
#SBATCH --nodes=3
#SBATCH --ntasks-per-node=1
#SBATCH --gpus-per-node=8
#SBATCH --cpus-per-task=64
#SBATCH --output=logs/slurm-%j.out # ログファイルのパスを指定
#SBATCH --error=logs/slurm-%j.err
# モジュールとConda環境の読み込み
module load cuda/12.6 miniconda/24.7.1-py312
source "$(conda info --base)/etc/profile.d/conda.sh"
conda activate llmbench
# ノード情報の取得
nodes_array=($(scontrol show hostnames "$SLURM_JOB_NODELIST" | tr '\n' ' '))
head_node=${nodes_array[0]}
port=37173
dashboard_port=$((port + 1))
# Head NodeのIPアドレスを取得
head_node_ip=$(srun --nodes=1 --ntasks=1 -w "$head_node" hostname --ip-address | awk '{print $1}')
ip_head=$head_node_ip:$port
export ip_head
######## Ray Head Nodeの起動 ########
srun --nodes=1 --ntasks=1 -w "$head_node" \
bash -c "source \$(conda info --base)/etc/profile.d/conda.sh && conda activate llmbench && \
ray start --head --node-ip-address=$head_node_ip --port=$port \
--dashboard-port=$dashboard_port --dashboard-host=0.0.0.0 \
--num-cpus=\$SLURM_CPUS_PER_TASK --num-gpus=\$SLURM_GPUS_PER_NODE --block" &
sleep 25
######## Ray Worker Nodeの起動 ########
worker_num=$((SLURM_JOB_NUM_NODES - 1))
for ((i = 1; i <= worker_num; i++)); do
node_i=${nodes_array[$i]}
echo "[INFO] Launching worker on $node_i ..."
srun --nodes=1 --ntasks=1 -w "$node_i" \
bash -c "source \$(conda info --base)/etc/profile.d/conda.sh && conda activate llmbench && \
ray start --address $ip_head --node-ip-address=\$(hostname --ip-address) \
--num-cpus=\$SLURM_CPUS_PER_TASK --num-gpus=\$SLURM_GPUS_PER_NODE --block" &
sleep 5
done
スクリプトを実行してRayクラスタを起動:
sbatch ray_cluster.sh
重要なポイント:
- slurmが自動的にノードリストを取得し、最初のノードをHead Nodeとして設定
-
sbatchを使ったバッチジョブ により、各ノードでRayプロセスを起動 - Worker間で
sleepを入れることで起動タイミングをずらして安定性を確保
ステップ2: Head Nodeに接続してvLLMサーバーを起動
Rayクラスタが起動したら、SSHでHead Nodeに接続し、vLLMサーバーを起動します。
# Head NodeにSSH接続
ssh <head_node_name>
# モジュールとConda環境の読み込み
module load cuda/12.6 miniconda/24.7.1-py312
source "$(conda info --base)/etc/profile.d/conda.sh"
conda activate llmbench
# vLLMサーバーの起動
vllm serve deepseek-ai/DeepSeek-R1-0528 \
--pipeline-parallel-size 3 \
--tensor-parallel-size 8 \
--max-model-len 65536 \
--gpu-memory-utilization 0.9 \
--reasoning-parser deepseek_r1 \
--dtype bfloat16 \
--trust-remote-code
主要パラメータの説明:
-
--pipeline-parallel-size 3: モデルを3つのステージに分割(複数ノード間での分散) -
--tensor-parallel-size 8: 各ステージを8つのGPUに分割 -
--gpu-memory-utilization 0.9: GPUメモリの90%まで使用
ステップ3: 推論の実行
vLLMサーバーが起動したら、Head Nodeまたは任意のWorker Nodeから推論を実行します。
from openai import AsyncOpenAI
# vLLMサーバーに接続(Head NodeのIPアドレスを指定)
client = AsyncOpenAI(
base_url="http://<head_node_ip>:8000/v1",
timeout=86400,
max_retries=3,
api_key="fakeapikey",
)
# 推論リクエスト
response = await client.chat.completions.create(
model="deepseek-ai/DeepSeek-R1-0528",
max_completion_tokens=65536,
messages=[
{"role": system_role, "content": system_prompt},
{"role": "user", "content": content}
],
stream=False,
)
content = response.choices[0].message.content
print(content)
3. vLLM と Ray構成でハマったポイントと対策
問題1: Rayクラスタ起動時の権限エラー
現象: Rayクラスタを起動しようとすると、以下のエラーが発生しました:
PermissionError: [Errno 13] Permission denied: '/tmp/ray/ray_current_cluster'
原因: 複数ユーザーが同じ計算ノードでRayを使用する環境では、デフォルトの一時ディレクトリ(/tmp/ray)で権限の競合が発生します。別のユーザーが作成したファイルやディレクトリが残っている場合、新しいユーザーがアクセスできず、エラーになりました。
対策: ユーザーごとに専用の一時ディレクトリを作成し、--temp-dirで指定します:
# ユーザー専用ディレクトリを作成
mkdir -p /nvme12/<username>
# Ray起動時に--temp-dirを指定
srun --nodes=1 --ntasks=1 -w "$head_node" \
bash -c "source \$(conda info --base)/etc/profile.d/conda.sh && conda activate llmbench && \
ray start --head --temp-dir=/nvme12/<username> --node-ip-address=$head_node_ip --port=$port \
--dashboard-port=$dashboard_port --dashboard-host=0.0.0.0 \
--num-cpus=\$SLURM_CPUS_PER_TASK --num-gpus=\$SLURM_GPUS_PER_NODE --block" &
ポイント:
-
/nvme12などの高速ストレージを使用することで、I/Oパフォーマンスも向上 - Worker NodeはHead Nodeの設定を自動的に継承するため、
--temp-dirの指定は不要
問題2: Rayクラスタでのノード認識エラー
現象: RayクラスタでvLLMを起動すると、以下のエラーが発生しました:
(autoscaler +5s) Error: No available node types can fulfill resource request {'GPU': 1.0, 'node:10.255.255.70': 0.001}.
Add suitable node types to this cluster to resolve this issue.
INFO 08-22 23:05:23 [ray_utils.py:234] Waiting for creating a placement group of specs for 10 seconds.
specs=[{'node:10.255.255.70': 0.001, 'GPU': 1.0}, {'GPU': 1.0}, ...].
Check `ray status` and `ray list nodes` to see if you have enough resources,
and make sure the IP addresses used by ray cluster are the same as VLLM_HOST_IP environment variable
specified in each node if you are running on a multi-node.
原因: RayクラスタとvLLMの間でノードのIPアドレスの認識に不整合が生じていました。
対策: Head NodeとWorker NodeのIPアドレスを明示的に指定し、vLLM起動時のRAY_ADDRESSも同じIPアドレスを使用します。
参考:
VLLM_HOST_IP環境変数を使用する解決方法がGitHub Issueや公式サイトで提案されていますが、本環境では効果が得られなかったため、IPアドレスの指定する方式を採用しました。
ray_cluster.shの修正:
# Head NodeのIPアドレスを明示的に指定
head_node_ip="10.255.255.70" # ← ハードコード
######## Ray Head Nodeの起動 ########
srun --nodes=1 --ntasks=1 -w "$head_node" \
bash -c "source \$(conda info --base)/etc/profile.d/conda.sh && conda activate llmbench && \
ray start --head --node-ip-address=$head_node_ip --port=$port \
--dashboard-port=$dashboard_port --dashboard-host=0.0.0.0 \
--num-cpus=\$SLURM_CPUS_PER_TASK --num-gpus=\$SLURM_GPUS_PER_NODE --block" &
sleep 25
######## Ray Worker Nodeの起動 ########
# Worker NodeのIPアドレスも明示的に指定
worker_num=$((SLURM_JOB_NUM_NODES - 1))
for ((i = 1; i <= worker_num; i++)); do
node_i=${nodes_array[$i]}
# Hardcode IP mapping based on node name
if [[ "$node_i" == "osk-gpu71" ]]; then
node_i_ip="10.255.255.71"
elif [[ "$node_i" == "osk-gpu72" ]]; then
node_i_ip="10.255.255.72"
else
echo "[ERROR] Unknown node: $node_i"
exit 1
fi
echo "[INFO] Launching worker on $node_i ..."
srun --nodes=1 --ntasks=1 -w "$node_i" \
bash -c "source \$(conda info --base)/etc/profile.d/conda.sh && conda activate llmbench && \
ray start --address $ip_head --node-ip-address=$node_i_ip \
--num-cpus=\$SLURM_CPUS_PER_TASK --num-gpus=\$SLURM_GPUS_PER_NODE --block" &
sleep 5
done
run_prediction.shの修正:
# Rayクラスタのアドレスを明示的に指定(Head NodeのIPと一致させる)
export RAY_ADDRESS=10.255.255.70:37173
# vLLMサーバーの起動
vllm serve ${MODEL_NAME} \
--pipeline-parallel-size 3 \
--tensor-parallel-size 8 \
--max-model-len 65536 \
--gpu-memory-utilization 0.9 \
--reasoning-parser deepseek_r1 \
--dtype bfloat16 \
--trust-remote-code
ポイント:
-
head_node_ipとRAY_ADDRESSのIPアドレスを完全に一致させる - Worker Nodeも各ノード名に対応する正しいIPアドレスを指定
- 自動取得に頼らず、環境に応じたIPアドレスを事前に確認して指定する
-
ray statusコマンドでノードが正しく認識されているか確認できる
まとめ
vLLMとRayで実現できたこと
本記事では、vLLMとRayを組み合わせたマルチノード分散推論システムを検証しました。DeepSeek-R1 671Bのような大規模モデルを複数ノードに分散することで、単一ノードの限界を超えたスケーラブルな推論基盤を実現しました。構築過程ではノード認識エラーなどの課題に直面しましたが、適切な設定により解決できました。
今後の展望や改善ポイント
今回は、時間やリソースの制約により、シングルノード推論とマルチノード推論の性能や効率を全面的に比較・分析することはできませんでした。将来的には、これらを詳細に比較し、各構成の利点やボトルネックを明確にした上で、最適な分散推論環境の設計につなげていきたいと考えています。
謝辞
本記事の作成にあたり、松尾研LLMコンペ2025 に参加する機会をいただき、誠にありがとうございました。また、PontNeufチーム をはじめ、共に参加した他のチームの皆様にも感謝申し上げます。今回の経験を通じて得た知見を今後の開発に活かし、引き続き大規模言語モデル(LLM)の発展に貢献していきたいと考えています。
本プロジェクトは、国立研究開発法人新エネルギー・産業技術開発機構(NEDO)の「日本語版医療特化型LLMの社会実装に向けた安全性検証・実証」における基盤モデル開発プロジェクトの一環として行われます。