はじめに
LLM勉強用にDGX spark 2台購入したものの、接続に手間取ったので、その時の内容をメモとしてchatGPTに書いてもらった内容を残しておきます。
(chatGPT出力で、だいぶ冗長な記事になっています。)
接続にはこちらの記事だいぶ参考にしています。
2台のDGX Sparkを相互接続して無事にvLLMを動作させるまでのメモ
最初に用語メモ
| 用語 | 役割 |
|---|---|
| RoCE | 2台間の高速ネットワーク通信 |
| NCCL | GPU間の集合通信ライブラリ |
| Ray | 複数ノードを1つのクラスタとして管理 |
| vLLM | LLM推論APIサーバー |
用語詳細
本記事で出てくる RoCE、NCCL、Ray、vLLM について、先に簡単に整理しておきます。
RoCE
RoCE は RDMA over Converged Ethernet の略です。
ざっくり言うと、Ethernet上でRDMAを使うための仕組みです。
通常、ネットワーク越しにデータを送る場合は、CPUやOSのネットワークスタックを経由します。一方、RDMAでは、NICが相手側メモリに近い形でデータ転送できるため、CPU負荷を抑えつつ高速・低遅延な通信ができます。
今回のDGX Spark 2台直結では、QSFPポートを使って2台間をRoCEで接続しています。
確認に使ったコマンドは主に以下です。
ibdev2netdev
ib_write_bw
ib_write_bw で約13GB/s程度の帯域が出ていれば、RoCE/RDMA通信自体はかなり良好と判断しました。
NCCL
NCCL は NVIDIA Collective Communications Library の略です。
複数GPU間で高速に通信するためのNVIDIA製ライブラリです。特に、以下のような分散処理で使われます。
- 複数GPUでのLLM推論
- 複数GPUでの学習
- Tensor Parallel
- Pipeline Parallel
- Distributed Data Parallel
GPU同士で以下のような集合通信を行うときに使われます。
- AllReduce
- AllGather
- Broadcast
- ReduceScatter
今回の検証では、2台のDGX Spark間でNCCL通信が成立しているかを nccl-tests の all_gather_perf で確認しました。
./build/all_gather_perf
ログに以下のように出ていれば、NCCLがRoCE経由で通信しています。
Using network IB
Using [0]rocep1s0f0:1/RoCE
via NET/IB/0
ここで IB と表示されていますが、InfiniBandそのものではなく、NCCL/verbs側の表現としてRoCEもこの経路で扱われます。
GDR / GPUDirect RDMA
GDR は GPUDirect RDMA のことです。
これは、NICがGPUメモリに直接アクセスして通信できる仕組みです。これが使えると、CPUメモリを経由するより効率よくGPU間通信できる場合があります。
NCCLログには以下のように出ることがあります。
GPU Direct RDMA Disabled
GDR 0
今回のDGX Spark環境では GDR 0 でした。ただし、RoCE/NCCL自体は正常に使えており、all_gather_perf でも約13GB/s程度の帯域が出ました。
つまり、今回の構成では、
GPUDirect RDMAは無効
しかしRoCE/NCCL通信は有効
という状態です。
Ray
Ray は、複数台のマシンをまとめて1つの分散実行環境として扱うためのフレームワークです。
今回の構成では、以下のように使っています。
spark-9879: Ray head
spark-439b: Ray worker
Ray headはクラスタの管理役です。Ray workerは計算リソースを提供するノードです。
Rayクラスタが正しく組めると、head側から以下のように2台分のGPUが見えるようになります。
import ray
ray.init(address="auto")
print(ray.cluster_resources())
期待する出力は以下のようなものです。
'GPU': 2.0
'node:172.16.100.1': 1.0
'node:172.16.100.2': 1.0
重要なのは、Rayを使ってもJupyter Notebook上で cuda:1 として別マシンのGPUが見えるわけではない、という点です。
Rayは、別マシン上のGPUを「クラスタのリソース」として管理します。
vLLM
vLLM は、LLM推論を高速・効率的に実行するための推論サーバーです。
Transformersで直接 model.generate() するのではなく、vLLMをAPIサーバーとして立てて、HTTP経由で推論を呼び出せます。
vLLMはOpenAI互換APIを提供できます。
たとえば、以下のように呼び出せます。
from openai import OpenAI
client = OpenAI(
base_url="http://172.16.100.1:8000/v1",
api_key="EMPTY",
)
response = client.chat.completions.create(
model="Qwen/Qwen2.5-0.5B-Instruct",
messages=[
{"role": "user", "content": "日本語で短く自己紹介してください。"}
],
)
print(response.choices[0].message.content)
今回の構成では、vLLMをRay上で起動し、2台のDGX SparkのGPUを使ってTensor Parallelで推論しました。
vllm serve Qwen/Qwen2.5-0.5B-Instruct \
--host 0.0.0.0 \
--port 8000 \
--distributed-executor-backend ray \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.8 \
--trust-remote-code
Tensor Parallel
Tensor Parallel は、1つのモデルの計算を複数GPUに分割する並列化手法です。
今回のように、
--tensor-parallel-size 2
を指定すると、モデルの一部の計算を2GPUに分割して実行します。
今回の構成では、各DGX SparkにGPUが1枚ずつあるため、Tensor Parallelはノードをまたいで行われます。
Qwen model
├── TP rank 0: spark-9879 GPU
└── TP rank 1: spark-439b GPU
このとき、GPU間通信にNCCLが使われます。
そのため、事前にNCCL/RoCEの帯域確認をしておくことが重要です。
全体の関係
今回の構成をまとめると、以下のようになります。
Jupyter Notebook / curl
↓ OpenAI互換API
vLLM API Server
↓
Ray cluster
↓
Tensor Parallel
↓
NCCL
↓
RoCE
↓
spark-9879 GPU + spark-439b GPU
それぞれの役割は以下です。
| 用語 | 役割 |
|---|---|
| RoCE | 2台間の高速ネットワーク通信 |
| NCCL | GPU間の集合通信ライブラリ |
| Ray | 複数ノードを1つのクラスタとして管理 |
| vLLM | LLM推論APIサーバー |
| Tensor Parallel | モデル計算を複数GPUに分割 |
| OpenAI互換API | Notebookやcurlから推論を呼ぶ入口 |
今回やったこと→DGX Sparkを2台を専用のケーブルで連結し、以下を確認。
- 2台間のRoCE接続
-
ib_write_bwによるRDMA帯域確認 -
nccl-testsによるNCCL疎通・帯域確認 - 固定IP化
- Rayクラスタ構築
- vLLMをRay上で2GPU Tensor Parallel起動
- OpenAI互換API経由で推論
- Jupyter NotebookからvLLM APIを叩くための準備
最終的には、2台のDGX SparkそれぞれのGPUをRay/vLLMクラスタとして使い、NotebookやcurlからAPIとして呼び出せる状態にします。
構成
今回の構成は以下です。ホスト名やリンクローカルIPは記事用に一部置き換えています。
| ホスト | 役割 | 固定IP | IFACE | RDMA device |
|---|---|---|---|---|
spark-a |
Ray head / vLLM API server | 172.16.100.1/30 |
enp1s0f0np0 |
rocep1s0f0 |
spark-b |
Ray worker | 172.16.100.2/30 |
enp1s0f0np0 |
rocep1s0f0 |
172.16.100.0/30 は2台直結用のプライベートネットワークとして使っています。実環境では、自分のネットワーク構成に合わせて読み替えてください。
最終的なvLLM APIエンドポイントは以下です。
http://172.16.100.1:8000/v1
注意点
Jupyter Notebook上で普通に torch.cuda.device_count() を実行しても、別マシンのGPUが cuda:1 として見えるわけではありません。
今回の構成では、NotebookはあくまでAPIクライアントです。
Jupyter Notebook
↓ HTTP / OpenAI互換API
vLLM API Server on Ray
↓
spark-a GPU + spark-b GPU
つまり、Notebookから2台目GPUを直接触るのではなく、Ray/vLLMクラスタとして2台のGPUを束ね、そのAPIをNotebookから呼び出します。
1. 接続されているRoCEインターフェースを確認
まず各ノードで ibdev2netdev を確認します。
ibdev2netdev
最終的に、今回の環境では以下のように f0 側がUpになりました。
roceP2p1s0f0 port 1 ==> enP2p1s0f0np0 (Up)
roceP2p1s0f1 port 1 ==> enP2p1s0f1np1 (Down)
rocep1s0f0 port 1 ==> enp1s0f0np0 (Up)
rocep1s0f1 port 1 ==> enp1s0f1np1 (Down)
IPアドレスも確認します。
ip -brief addr
ip -brief link show
最終的に、使うべきインターフェースは以下になりました。
IFACE=enp1s0f0np0
RDMA_DEV=rocep1s0f0
ポイントは、enP... 側ではなく、IPv4アドレスが付いている enp... 側を使うことです。
2. リンクローカルIPで疎通確認
固定IP化する前に、まず自動で付与されていたリンクローカルIPで疎通を確認しました。
例:
| ホスト | IFACE | IP |
|---|---|---|
spark-a |
enp1s0f0np0 |
169.254.xxx.xxx |
spark-b |
enp1s0f0np0 |
169.254.yyy.yyy |
spark-a 側で以下を設定します。
export IFACE=enp1s0f0np0
export RDMA_DEV=rocep1s0f0
export SELF_IP=169.254.xxx.xxx
export PEER_IP=169.254.yyy.yyy
疎通確認します。
ping -c 3 $PEER_IP
ssh -o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
$PEER_IP hostname
期待値は以下です。
spark-b
3. RDMA帯域を確認する
まず ib_write_bw でRDMA単体の帯域を確認します。
spark-b → spark-a
spark-a 側で待ち受けます。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608
spark-b 側から接続します。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608 169.254.xxx.xxx
結果は以下のようになりました。
#bytes #iterations BW peak[MB/sec] BW average[MB/sec] MsgRate[Mpps]
8388608 5000 12992.54 12992.52 0.001624
spark-a → spark-b
spark-b 側で待ち受けます。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608
spark-a 側から接続します。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608 169.254.yyy.yyy
結果は以下のようになりました。
#bytes #iterations BW peak[MB/sec] BW average[MB/sec] MsgRate[Mpps]
8388608 5000 13015.76 13015.72 0.001627
両方向とも約13GB/s出ています。
途中で遭遇した問題
最初は片方向だけ約1.5GB/sしか出ていませんでした。
spark-b → spark-a: 約1.5GB/s
spark-a → spark-b: 約13GB/s
この状態だと、NCCLの all_gather_perf も遅い方向に引っ張られて性能が出ませんでした。
今回は、QSFPケーブルを差し直して再起動したところ、両方向とも約13GB/s出るようになりました。
4. NCCLテストを実行する
nccl-tests を使って2ノード間NCCLを確認します。
spark-a 側から実行します。
cd ~/nccl-tests
export NCCL_HOME=$HOME/nccl/build
export LD_LIBRARY_PATH=$NCCL_HOME/lib:$LD_LIBRARY_PATH
export IFACE=enp1s0f0np0
export RDMA_DEV=rocep1s0f0
export SELF_IP=169.254.xxx.xxx
export PEER_IP=169.254.yyy.yyy
export UCX_NET_DEVICES=$IFACE
export NCCL_SOCKET_IFNAME=$IFACE
export OMPI_MCA_btl_tcp_if_include=$IFACE
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=INIT,NET
export NCCL_IB_DISABLE=0
export NCCL_IB_HCA=$RDMA_DEV
export NCCL_NET_GDR_LEVEL=0
mpirun -np 2 -H ${SELF_IP}:1,${PEER_IP}:1 \
--mca plm_rsh_agent "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \
-x LD_LIBRARY_PATH \
-x UCX_NET_DEVICES \
-x NCCL_SOCKET_IFNAME \
-x OMPI_MCA_btl_tcp_if_include \
-x NCCL_DEBUG \
-x NCCL_DEBUG_SUBSYS \
-x NCCL_IB_DISABLE \
-x NCCL_IB_HCA \
-x NCCL_NET_GDR_LEVEL \
./build/all_gather_perf -b 32M -e 2G -f 2 -g 1
結果例です。
33554432 ... busbw 11.28
67108864 ... busbw 11.53
134217728 ... busbw 12.76
268435456 ... busbw 13.06
536870912 ... busbw 13.21
1073741824 ... busbw 13.34
2147483648 ... busbw 13.51
# Avg bus bandwidth : 12.7458
# Out of bounds values : 0 OK
ログ上も、NCCLがRoCEを使っていることを確認できます。
Using network IB
Using [0]rocep1s0f0:1/RoCE
via NET/IB/0
GDR 0について
ログには以下のような行が出ます。
GPU Direct RDMA Disabled for HCA 0 'rocep1s0f0'
Connected all rings, use ring PXN 0 GDR 0
DGX SparkではGPUDirect RDMAが使えないため、GDR 0 は想定内として扱いました。
重要なのは、今回の環境では GDR 0 でもNCCL bus bandwidthが約13GB/s出ていることです。
5. 固定IP化する
リンクローカルIPは再起動などで変わる可能性があるため、2台直結用に固定IPを設定しました。
| ホスト | IFACE | 固定IP |
|---|---|---|
spark-a |
enp1s0f0np0 |
172.16.100.1/30 |
spark-b |
enp1s0f0np0 |
172.16.100.2/30 |
spark-a側
sudo cp -a /etc/netplan /etc/netplan.backup.$(date +%Y%m%d-%H%M%S)
sudo tee /etc/netplan/90-dgx-spark-roce.yaml > /dev/null <<'EOF'
network:
version: 2
ethernets:
enp1s0f0np0:
dhcp4: no
addresses:
- 172.16.100.1/30
EOF
sudo netplan generate
sudo netplan apply
確認します。
ip -brief addr show enp1s0f0np0
期待値です。
enp1s0f0np0 UP 172.16.100.1/30 ...
spark-b側
sudo cp -a /etc/netplan /etc/netplan.backup.$(date +%Y%m%d-%H%M%S)
sudo tee /etc/netplan/90-dgx-spark-roce.yaml > /dev/null <<'EOF'
network:
version: 2
ethernets:
enp1s0f0np0:
dhcp4: no
addresses:
- 172.16.100.2/30
EOF
sudo netplan generate
sudo netplan apply
確認します。
ip -brief addr show enp1s0f0np0
期待値です。
enp1s0f0np0 UP 172.16.100.2/30 ...
6. 固定IPで疎通確認
spark-a から確認します。
ping -c 3 172.16.100.2
ssh -o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
172.16.100.2 hostname
期待値です。
spark-b
spark-b からも確認します。
ping -c 3 172.16.100.1
ssh -o UserKnownHostsFile=/dev/null \
-o StrictHostKeyChecking=no \
172.16.100.1 hostname
期待値です。
spark-a
7. 固定IPでRDMA帯域確認
固定IP化後も ib_write_bw を確認します。
spark-a 側で待ち受けます。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608
spark-b 側から接続します。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608 172.16.100.1
結果例です。
BW average[MB/sec] 12992.35
逆方向も確認します。
spark-b 側で待ち受けます。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608
spark-a 側から接続します。
ib_write_bw -d rocep1s0f0 -i 1 -F -s 8388608 172.16.100.2
結果例です。
BW average[MB/sec] 13015.30
固定IP化後も両方向で約13GB/s出ました。
8. 固定IPでNCCL再確認
spark-a 側から実行します。
cd ~/nccl-tests
export NCCL_HOME=$HOME/nccl/build
export LD_LIBRARY_PATH=$NCCL_HOME/lib:$LD_LIBRARY_PATH
export IFACE=enp1s0f0np0
export RDMA_DEV=rocep1s0f0
export SELF_IP=172.16.100.1
export PEER_IP=172.16.100.2
export UCX_NET_DEVICES=$IFACE
export NCCL_SOCKET_IFNAME=$IFACE
export OMPI_MCA_btl_tcp_if_include=$IFACE
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=INIT,NET
export NCCL_IB_DISABLE=0
export NCCL_IB_HCA=$RDMA_DEV
export NCCL_NET_GDR_LEVEL=0
mpirun -np 2 -H ${SELF_IP}:1,${PEER_IP}:1 \
--mca plm_rsh_agent "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \
-x LD_LIBRARY_PATH \
-x UCX_NET_DEVICES \
-x NCCL_SOCKET_IFNAME \
-x OMPI_MCA_btl_tcp_if_include \
-x NCCL_DEBUG \
-x NCCL_DEBUG_SUBSYS \
-x NCCL_IB_DISABLE \
-x NCCL_IB_HCA \
-x NCCL_NET_GDR_LEVEL \
./build/all_gather_perf -b 32M -e 2G -f 2 -g 1
結果例です。
33554432 ... busbw 11.08
67108864 ... busbw 12.29
134217728 ... busbw 13.20
268435456 ... busbw 13.28
536870912 ... busbw 13.49
1073741824 ... busbw 13.58
2147483648 ... busbw 13.68
# Out of bounds values : 0 OK
# Avg bus bandwidth : 13.0856
これで固定IP化後もNCCL over RoCEが正常に動作していることを確認できました。
9. Docker / vLLMイメージ確認
vLLMはDockerコンテナで動かします。
今回使ったイメージです。
export VLLM_IMAGE=nvcr.io/nvidia/vllm:25.11-py3
両ノードでpullします。
docker pull $VLLM_IMAGE
Docker権限がない場合は sudo を付けます。
sudo docker pull $VLLM_IMAGE
コンテナ内からGPUが見えるか確認します。
docker run --rm --gpus all --network host $VLLM_IMAGE nvidia-smi
権限が必要なら以下です。
sudo docker run --rm --gpus all --network host $VLLM_IMAGE nvidia-smi
Python側でも確認します。
docker run --rm -i --gpus all --network host --ipc=host \
--ulimit memlock=-1 --ulimit stack=67108864 \
$VLLM_IMAGE python3 - <<'PY'
import torch
import ray
import vllm
print("torch:", torch.__version__, flush=True)
print("ray:", ray.__version__, flush=True)
print("vllm:", vllm.__version__, flush=True)
print("cuda available:", torch.cuda.is_available(), flush=True)
print("cuda device count:", torch.cuda.device_count(), flush=True)
PY
各ノード単体では以下でOKです。
cuda available: True
cuda device count: 1
10. run_cluster.shを用意する
Ray head / workerの起動にはvLLM公式の run_cluster.sh を使います。
両ノードで実行します。
cd ~
wget -O run_cluster.sh https://raw.githubusercontent.com/vllm-project/vllm/main/examples/online_serving/run_cluster.sh
chmod +x run_cluster.sh
確認します。
grep -n "node-ip-address" ~/run_cluster.sh
11. Ray headを起動する
run_cluster.sh で起動したターミナルを閉じるとコンテナも止まるため、tmux を使うのがおすすめです。
spark-a で実行します。
tmux new -s ray-head
tmux内で以下を実行します。
cd ~
export VLLM_IMAGE=nvcr.io/nvidia/vllm:25.11-py3
export HEAD_IP=172.16.100.1
export MN_IF_NAME=enp1s0f0np0
sudo bash run_cluster.sh $VLLM_IMAGE $HEAD_IP --head ~/.cache/huggingface \
-e VLLM_HOST_IP=172.16.100.1 \
-e MASTER_ADDR=172.16.100.1 \
-e UCX_NET_DEVICES=$MN_IF_NAME \
-e NCCL_SOCKET_IFNAME=$MN_IF_NAME \
-e GLOO_SOCKET_IFNAME=$MN_IF_NAME \
-e TP_SOCKET_IFNAME=$MN_IF_NAME \
-e NCCL_IB_HCA=rocep1s0f0 \
-e NCCL_IB_DISABLE=0 \
-e NCCL_NET_GDR_LEVEL=0 \
-e RAY_memory_monitor_refresh_ms=0
tmuxから抜けるだけなら以下です。
Ctrl-b → d
12. Ray workerを起動する
spark-b で実行します。
tmux new -s ray-worker
tmux内で以下を実行します。
cd ~
export VLLM_IMAGE=nvcr.io/nvidia/vllm:25.11-py3
export HEAD_IP=172.16.100.1
export MN_IF_NAME=enp1s0f0np0
sudo bash run_cluster.sh $VLLM_IMAGE $HEAD_IP --worker ~/.cache/huggingface \
-e VLLM_HOST_IP=172.16.100.2 \
-e MASTER_ADDR=172.16.100.1 \
-e UCX_NET_DEVICES=$MN_IF_NAME \
-e NCCL_SOCKET_IFNAME=$MN_IF_NAME \
-e GLOO_SOCKET_IFNAME=$MN_IF_NAME \
-e TP_SOCKET_IFNAME=$MN_IF_NAME \
-e NCCL_IB_HCA=rocep1s0f0 \
-e NCCL_IB_DISABLE=0 \
-e NCCL_NET_GDR_LEVEL=0 \
-e RAY_memory_monitor_refresh_ms=0
13. RayクラスタでGPUが2個見えるか確認する
spark-a の別ターミナルで確認します。
sudo docker ps
headコンテナ名を取得します。
export HEAD_CONTAINER=$(sudo docker ps --format '{{.Names}}' | grep '^node-' | head -n 1)
echo $HEAD_CONTAINER
コンテナに入ります。
sudo docker exec -it $HEAD_CONTAINER /bin/bash
コンテナ内で確認します。
ray status
結果例です。
Resources
---------------------------------------------------------------
Total Usage:
0.0/40.0 CPU
0.0/2.0 GPU
Pythonからも確認します。
python3 - <<'PY'
import ray
ray.init(address="auto")
print(ray.cluster_resources())
PY
結果例です。
{
'accelerator_type:GB10': 2.0,
'CPU': 40.0,
'GPU': 2.0,
'node:172.16.100.1': 1.0,
'node:172.16.100.2': 1.0,
...
}
これでRayクラスタとして2ノード/2GPUが見えています。
14. vLLM APIサーバーを1GPUで起動して疎通確認
最初は1GPUで小さいモデルを起動し、API疎通だけ確認します。
headコンテナ内で実行します。
export MODEL_NAME=Qwen/Qwen2.5-0.5B-Instruct
vllm serve $MODEL_NAME \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.8 \
--trust-remote-code
gpu-memory-utilizationについて
最初、デフォルトの 0.9 では以下のエラーが出ました。
ValueError: Free memory on device (106.21/119.7 GiB) on startup is less than desired GPU memory utilization (0.9, 107.73 GiB).
Decrease GPU memory utilization or reduce GPU memory used by other processes.
そのため、今回は --gpu-memory-utilization 0.8 を指定しました。
別ターミナルから確認します。
curl http://172.16.100.1:8000/v1/models
生成も確認します。
curl http://172.16.100.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-0.5B-Instruct",
"messages": [
{"role": "user", "content": "日本語で短く自己紹介してください。"}
],
"temperature": 0.7,
"max_tokens": 128
}'
レスポンス例です。
{
"object": "chat.completion",
"model": "Qwen/Qwen2.5-0.5B-Instruct",
"choices": [
{
"message": {
"role": "assistant",
"content": "..."
}
}
]
}
15. vLLM APIサーバーを2GPU Tensor Parallelで起動する
1GPU版が動いたら、Ctrl-C で止めてから、2GPU版を起動します。
headコンテナ内で実行します。
export MODEL_NAME=Qwen/Qwen2.5-0.5B-Instruct
vllm serve $MODEL_NAME \
--host 0.0.0.0 \
--port 8000 \
--distributed-executor-backend ray \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.8 \
--trust-remote-code
ログでは以下のように、worker側 172.16.100.2 が参加していることを確認できます。
tensor_parallel_size=2
RayWorkerWrapper pid=..., ip=172.16.100.2
rank 1 in world size 2 is assigned as ... TP rank 1
Found nccl from library libnccl.so.2
vLLM is using nccl==2.28.8
この警告も出ます。
tensor_parallel_size=2 is bigger than a reserved number of GPUs (1 GPUs) in a node ...
Tensor parallel workers can be spread out to 2+ nodes which can degrade the performance unless you have fast interconnect across nodes
各ノードにGPUが1枚しかないため、TP=2がノード跨ぎになるという警告です。今回はRoCE/NCCLで約13GB/s出ているため、この構成で進めました。
16. 2GPU版vLLM APIを確認する
spark-a のホスト側から確認します。
curl http://172.16.100.1:8000/v1/models
レスポンス例です。
{
"object": "list",
"data": [
{
"id": "Qwen/Qwen2.5-0.5B-Instruct",
"object": "model",
"owned_by": "vllm",
"root": "Qwen/Qwen2.5-0.5B-Instruct"
}
]
}
生成確認です。
curl http://172.16.100.1:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-0.5B-Instruct",
"messages": [
{"role": "user", "content": "日本語で短く自己紹介してください。"}
],
"temperature": 0.7,
"max_tokens": 128
}'
レスポンスが返れば成功です。
17. 両GPUを使っているか確認する
spark-a 側で確認します。
nvidia-smi
結果例です。
Processes:
GPU PID Type Process name GPU Memory
0 xxxxx C ...RayWorkerWrapper.__ray_call__ xxxxxMiB
spark-b 側でも確認します。
nvidia-smi
同様に RayWorkerWrapper.__ray_call__ が出ていれば、両ノードのGPUを使っています。
18. NotebookからvLLM APIを叩く
Jupyter NotebookからはOpenAI互換APIとして呼び出します。
from openai import OpenAI
client = OpenAI(
base_url="http://172.16.100.1:8000/v1",
api_key="EMPTY",
)
response = client.chat.completions.create(
model="Qwen/Qwen2.5-0.5B-Instruct",
messages=[
{"role": "user", "content": "日本語で短く自己紹介してください。"}
],
temperature=0.7,
max_tokens=128,
)
print(response.choices[0].message.content)
Notebook側で torch.cuda.device_count() が 1 のままでも問題ありません。
2GPUを使っているのはNotebookではなく、裏側のvLLM/Rayクラスタです。
19. 停止方法
vLLM APIサーバーを止める
vllm serve を起動しているターミナルで以下を押します。
Ctrl-C
Ray head / workerを止める
tmuxに戻ります。
spark-a:
tmux attach -t ray-head
spark-b:
tmux attach -t ray-worker
それぞれ Ctrl-C で止めます。
コンテナが残っている場合は、各ノードで以下を実行します。
sudo docker ps
sudo docker stop $(sudo docker ps -q --filter "name=node-")
まとめ
DGX Spark 2台を直結し、最終的に以下まで確認できました。
-
enp1s0f0np0/rocep1s0f0を使用 - 固定IP
172.16.100.1/30,172.16.100.2/30 -
ib_write_bwで両方向約13GB/s -
nccl-testsでAvg bus bandwidth約13GB/s - Rayクラスタで2ノード/2GPU認識
- vLLMをRay上で
--tensor-parallel-size 2起動 - OpenAI互換APIで推論成功
- NotebookからAPI経由で利用可能
最終的な構成は以下です。
Jupyter Notebook
↓ OpenAI互換API
http://172.16.100.1:8000/v1
↓
vLLM API Server on Ray
↓
spark-a GPU + spark-b GPU
これで、Notebookから2台のDGX Sparkを束ねたvLLM推論環境を利用できるようになりました。