1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DGX Sparkを2台直結して、NCCL/RoCE確認からRay + vLLM 2GPU推論まで動かす

1
Posted at

はじめに

LLM勉強用にDGX spark 2台購入したものの、接続に手間取ったので、その時の内容をメモとしてchatGPTに書いてもらった内容を残しておきます。
(chatGPT出力で、だいぶ冗長な記事になっています。)
接続にはこちらの記事だいぶ参考にしています。
2台のDGX Sparkを相互接続して無事にvLLMを動作させるまでのメモ


最初に用語メモ

用語 役割
RoCE 2台間の高速ネットワーク通信
NCCL GPU間の集合通信ライブラリ
Ray 複数ノードを1つのクラスタとして管理
vLLM LLM推論APIサーバー
用語詳細

本記事で出てくる RoCENCCLRayvLLM について、先に簡単に整理しておきます。

RoCE

RoCERDMA 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

NCCLNVIDIA 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-testsall_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

GDRGPUDirect 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-testsAvg 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推論環境を利用できるようになりました。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?