1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

邪悪なプロキシ環境下でローカルLLMを構築した話<Dual DGX Spark編>

Last updated at Posted at 2025-11-13

はじめに

DGX Sparkを2台入手したので,Dual DGX Spark環境を構築して,
巨大なローカルLLMモデル(Qwen3-235B-A22B-FP4)を動かしてみた備忘録です。
プロキシ環境ではない場合は公式ドキュメントの通りで動くと思います。

公式ドキュメント

このあたりを参照のこと。

前提

基本的な初期設定とインターネットのプロキシ設定は完了していること。
Dockerのプロキシ設定(proxy_setting.conf)も完了済み。
前回記事を参照:

定義

それぞれの手順で操作するマシンを以下のように表します。
① ... 1台目のDGX Spark。メインで操作する方.公式ドキュメントではNode 1とかprimary nodeと呼ばれる。
② ... 2台目のDGX Spark。公式ドキュメントではNode 2とかworker nodeと呼ばれる。

どちらのマシンも同じ操作する必要がある場合は「Step X(①②)」のように記述していきます。

1. 2台のDGX Sparkを繋げる

Step 1(①②):アカウントを作る

①②で同じユーザー名のアカウントを用意します。
アカウントは管理者権限が必要です。

Step 2(①②):ケーブルで物理的に接続する

2台のDGX SparkをQSFPケーブルで接続します。
ポートが2つありますが,in/outが決まっているわけではないのでどちらでも可。
接続後にターミナルで ibdev2netdevを実行して次のように表示されればOKです。

roceP2p1s0f0 port 1 ==> enP2p1s0f0np0 (Down)
roceP2p1s0f1 port 1 ==> enP2p1s0f1np1 (Up)
rocep1s0f0 port 1 ==> enp1s0f0np0 (Down)
rocep1s0f1 port 1 ==> enp1s0f1np1 (Up)

それぞれのポートは名前を2つもっており,enP2p1s0f0np0 enp1s0f0np0は同じポートを指します。
この例では1番のポートにケーブルが接続されているので,enP2p1s0f1np1 enp1s0f1np1Upの表示となっています。

Step 3(①②):ネットワーク設定

netplanを使用してネットワークインターフェースを設定します(公式ドキュメントの推奨方法)。

# Create the netplan configuration file
sudo tee /etc/netplan/40-cx7.yaml > /dev/null <<EOF
network:
  version: 2
  ethernets:
    enp1s0f0np0:
      link-local: [ ipv4 ]
    enp1s0f1np1:
      link-local: [ ipv4 ]
EOF

# Set appropriate permissions
sudo chmod 600 /etc/netplan/40-cx7.yaml

# Apply the configuration
sudo netplan apply

sudo netplan applyを実行した段階で,UbuntuのGUIのネットワーク設定画面が更新されます。
これ以降,2つあるポートの内のケーブルを接続していない方について,通信を試みて失敗するメッセージが無限に表示されるようになります。鬱陶しいので自動接続をオフにしておくことを推奨。

Step 4(①):SSHでDGX Sparkを接続する

Githubからdiscover-sparks.shをダウンロードし,ホームディレクトリに置いて実行します。

bash ./discover-sparks

実行すると以下のようにローカルなIPアドレスが表示されます。

Found: 169.254.XXX.XXX (dgx-spark-1.local)
Found: 169.254.YYY.YYY (dgx-spark-2.local)

Setting up bidirectional SSH access (local <-> remote nodes)...
You may be prompted for your password for each node.

その後,それぞれのDGX Sparkのアカウントのパスワードを入力すると,以下のように表示されます。

SSH setup complete! Both local and remote nodes can now SSH to each other without passwords.

これ以降,①のIPアドレスを169.254.XXX.XXX,②のIPアドレスを169.254.YYY.YYYとします。

Step 5(①②):認識できているかテスト

ssh 169.254.XXX.XXX hostname
ssh 169.254.YYY.YYY hostname

相手側のIPアドレスを入力して,マシンの名前(アカウント名ではない)が出てきたらOKです.
ここまで完了したら,以降は②は①のターミナルからSSHで操作できます。

①のターミナル
ssh 169.254.YYY.YYY

これでつながります。

2. NCCLを使ったGPU通信のテスト

Step 1(①②):NCCLを構築する

NCCL(NVIDIA Collective Communication Library)をビルドします。

# Install dependencies and build NCCL
sudo apt-get update && sudo apt-get install -y libopenmpi-dev
git clone -b v2.28.3-1 https://github.com/NVIDIA/nccl.git ~/nccl/
cd ~/nccl/
make -j src.build NVCC_GENCODE="-gencode=arch=compute_121,code=sm_121"

# Set environment variables
export CUDA_HOME="/usr/local/cuda"
export MPI_HOME="/usr/lib/aarch64-linux-gnu/openmpi"
export NCCL_HOME="$HOME/nccl/build/"
export LD_LIBRARY_PATH="$NCCL_HOME/lib:$CUDA_HOME/lib64/:$MPI_HOME/lib:$LD_LIBRARY_PATH"

NCLL test suitもコンパイルしておきます。

git clone https://github.com/NVIDIA/nccl-tests.git ~/nccl-tests/
cd ~/nccl-tests/
make MPI=1

Step 2(①):テストを実行

環境変数を設定します。

export UCX_NET_DEVICES=enp1s0f1np1
export NCCL_SOCKET_IFNAME=enp1s0f1np1
export OMPI_MCA_btl_tcp_if_include=enp1s0f1np1

この例は enp1s0f1np1Upになっている(つまり1番のポートにケーブルが接続されている)場合です。
その後,以下を実行します。

mpirun -np 2 -H 169.254.XXX.XXX:1,169.254.YYY.YYY:1 \
  --mca plm_rsh_agent "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no" \
  -x LD_LIBRARY_PATH=$LD_LIBRARY_PATH \
  $HOME/nccl-tests/build/all_gather_perf

こんな感じの結果が表れるはず。

# nccl-tests version 2.17.6 nccl-headers=22803 nccl-library=22803
# Collective test starting: all_gather_perf
# nThread 1 nGpus 1 minBytes 33554432 maxBytes 33554432 step: 1048576(bytes) warmup iters: 1 iters: 20 agg iters: 1 validation: 1 graph: 0
#
# Using devices
#  Rank  0 Group  0 Pid  70632 on spark-8028 device  0 [000f:01:00] NVIDIA GB10
#  Rank  1 Group  0 Pid  45867 on spark-7c14 device  0 [000f:01:00] NVIDIA GB10
#
#                                                              out-of-place                       in-place    
#       size         count      type   redop    root     time   algbw   busbw  #wrong     time   algbw   busbw  #wrong 
#        (B)    (elements)                               (us)  (GB/s)  (GB/s)             (us)  (GB/s)  (GB/s)   
    33554432       4194304     float    none      -1   987.95   33.96   16.98       0   938.19   35.77   17.88       0
# Out of bounds values : 0 OK
# Avg bus bandwidth    : 17.4322 
#

なお,②から操作しても同じようにテストできます。

3. TRT-LLMをデュアルDGX Sparkで動かす

Step 1(①②):Dockerの事前設定

Dockerコマンドを実行した際に権限拒否でエラーが出ないように,権限を付与しておきます。

sudo usermod -aG docker $USER
newgrp docker

次に,NVIDIA Container Toolkitをインストールします。

sudo apt-get update && sudo apt-get install -y --no-install-recommends \
   curl \
   gnupg2

リポジトリを設定します。

curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

パッケージリストを更新します。

sudo apt-get update

NVIDIA Container Toolkit パッケージをインストールします。

export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.0-1
sudo apt-get install -y \
      nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}

また,以下のコマンドを実行しておきます。

docker run --rm --gpus all nvcr.io/nvidia/tensorrt-llm/release:spark-single-gpu-dev nvidia-smi

ある程度時間がかかるのでのんびり待ちましょう。

Step 2(①②):NVIDIA Container Runtime を修正

次のコマンドを実行してGPUのUUIDを確認します。

nvidia-smi -a | grep UUID

次に、GPUをSwarmに通知するように /etc/docker/daemon.json内の記述を管理者権限で以下のように変更します。

daemon.json
{
  "runtimes": {
    "nvidia": {
      "path": "nvidia-container-runtime",
      "runtimeArgs": []
    }
  },
  "default-runtime": "nvidia",
  "node-generic-resources": [
    "NVIDIA_GPU=GPU-*************************************************" # この部分に調べたUUIDを入力
    ]
}

また,/etc/nvidia-container-runtime/config.toml内の swarm-resourceの行のコメントアウトを外します。
その後,Dockerデーモンを再起動します。

sudo systemctl restart docker

Step 3-1(①):Docker Swarm を初期化

以下の初期化コマンドを実行します。

docker swarm init --advertise-addr $(ip -o -4 addr show enp1s0f0np0 | awk '{print $4}' | cut -d/ -f1) $(ip -o -4 addr show enp1s0f1np1 | awk '{print $4}' | cut -d/ -f1)

実行時の出力には次のような記述が含まれます。

Swarm initialized: current node (node-id) is now a manager.

To add a worker to this swarm, run the following command:

    docker swarm join --token <worker-token> <advertise-addr>:<port>

To add a manager to this swarm, run 'docker swarm join-token manager' and follow the instructions.

Step 3-2(②):Docker Swarm に参加

Step 3で出力されたコマンド(3行目)を実行します。

docker swarm join --token <worker-token> <advertise-addr>:<port>

Step 3-3(①②):実行スクリプトの用意

Githubからtrtllm-mn-entrypoint.sh をダウンロードし,ホームディレクトリに置きます。
その後,実行権限を付与します。

chmod +x $HOME/trtllm-mn-entrypoint.sh

Step 4(①):Dockerコンテナの起動(デプロイ)

Githubからdocker-compose.ymlファイルをホームディレクトリにダウンロードします。
ダウンロードしたファイル内の environmentにプロキシの設定を加えます。

docker-compose.yml
      - http_proxy=http://your-proxy-address:port
      - https_proxy=http://your-proxy-address:port
      - HTTP_PROXY=http://your-proxy-address:port
      - HTTPS_PROXY=http://your-proxy-address:port
      - no_proxy=localhost,127.0.0.1,.example.com
      - NO_PROXY=localhost,127.0.0.1,.example.com
      - REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt 

この設定が無いとプロキシ環境下ではコンテナの起動に失敗します
その後,以下のコマンドでコンテナを起動します。

docker stack deploy -c $HOME/docker-compose.yml trtllm-multinode

docker psで起動を確認すると,他に何か動かしていなければ以下のように1つのコンテナが表示されます。

CONTAINER ID   IMAGE                                          COMMAND                   CREATED       STATUS                 PORTS     NAMES
f4d68dbca080   nvcr.io/nvidia/tensorrt-llm/release:1.0.0rc3   "/opt/trtllm-mn-entr…"   5 hours ago   Up 5 hours (healthy)             trtllm-multinode_trtllm.2.to1dkm2rz2ry038quveaqgox9

それぞれのマシンが動いていることは docker stack ps trtllm-multinodeで確認できます。出力結果は以下のような感じ。

ID             NAME                            IMAGE                                          NODE         DESIRED STATE   CURRENT STATE             ERROR     PORTS
oe9k5o6w41le   trtllm-multinode_trtllm.1       nvcr.io/nvidia/tensorrt-llm/release:1.0.0rc3   spark-1d84   Running         Running 2 minutes ago
phszqzk97p83   trtllm-multinode_trtllm.2       nvcr.io/nvidia/tensorrt-llm/release:1.0.0rc3   spark-1b3b   Running         Running 2 minutes ago

または docker node lsでも確認できます。

ID                            HOSTNAME     STATUS    AVAILABILITY   MANAGER STATUS   ENGINE VERSION
hza2b7yisatqiezo33zx4in4i *   spark-1b3b   Ready     Active         Leader           28.3.3
m1k22g3ktgnx36qz4jg5fzhr4     spark-1d84   Ready     Active                          28.3.3

Step 5(①②):コンテナ内でのプロキシの追加設定

コンテナのIDを取得しておきます。

export TRTLLM_MN_CONTAINER=$(docker ps -q -f name=trtllm-multinode)

コンテナの中の適当な場所にプロキシ証明書をコピーします。

docker cp proxy-ca.crt $TRTLLM_MN_CONTAINER:/

その後,コンテナに入ります。

docker exec -it $TRTLLM_MN_CONTAINER /bin/bash

コンテナの中で,以下を順番に実行します。

mkdir /usr/share/ca-certificates/mylocal
cp /proxy-ca.crt /usr/share/ca-certificates/mylocal/
echo "mylocal/proxy-ca.crt" | tee -a /etc/ca-certificates.conf 
update-ca-certificates 

Step 6(①):設定ファイルを作成する

MPI操作用にすべてのDocker Swarmノードアドレスを含むファイルを生成してコンテナにコピーします。

docker node ls --format '{{.ID}}' | xargs -n1 docker node inspect --format '{{ .Status.Addr }}' > ~/openmpi-hostfile
docker cp ~/openmpi-hostfile $TRTLLM_MN_CONTAINER:/etc/openmpi-hostfile

また,次のコマンドを実行します。

docker exec $TRTLLM_MN_CONTAINER bash -c 'cat <<EOF > /tmp/extra-llm-api-config.yml
print_iter_log: false
kv_cache_config:
  dtype: "auto"
  free_gpu_memory_fraction: 0.9
cuda_graph_config:
  enable_padding: true
EOF'

Step 7(①) モデルのダウンロード

追記:現状の方法だとDockerコンテナを停止したらモデル本体まで消失するので,別の方法を検討中です

Huggingfaceのトークンを準備しておきます。

export HF_TOKEN=<your-huggingface-token>

公式ではモデルのダウンロードに huggingface-cliを使っているのですが,こちらの環境では何故か動作が安定しなかったため git lfsを使用しました。

git lfs install

以下のコマンドでダウンロードします。
①で実行すれば②にも同じようにダウンロードされます。

docker exec \
  -e HF_TOKEN=$HF_TOKEN \
  -it $TRTLLM_MN_CONTAINER bash -c 'mpirun -x HF_TOKEN -x HTTP_PROXY -x HTTPS_PROXY -x NO_PROXY -x REQUESTS_CA_BUNDLE bash -c "git clone https://huggingface.co/nvidia/Qwen3-235B-A22B-FP4"'

公式でサポートされている他のモデルはここを参照してください。

Step 8(①)モデルの実行

以下のコマンドを実行することでLLMサーバーが起動します。

docker exec \
  -e HF_TOKEN=$HF_TOKEN \
  -it $TRTLLM_MN_CONTAINER bash -c '
    mpirun -x HF_TOKEN -x HTTP_PROXY -x HTTPS_PROXY -x NO_PROXY -x REQUESTS_CA_BUNDLE trtllm-llmapi-launch trtllm-serve Qwen3-235B-A22B-FP4 \
      --tp_size 2 \
      --backend pytorch \
      --max_num_tokens 32768 \
      --max_batch_size 4 \
      --extra_llm_api_options /tmp/extra-llm-api-config.yml \
      --port 8355'

大きなモデルだとしばらく時間がかかります。
INFO: Started server process [*****]と表示されたら完了です。

Step 9(①)APIサーバーの検証

以下のコマンドでリクエストを投げることができます。

curl -s http://localhost:8355/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "Qwen3-235B-A22B-FP4",
    "messages": [{"role": "user", "content": "Paris is great because"}],
    "max_tokens": 64
  }'

4. Open WebUIで使う

Step 1(①):Open WebUIの起動

以下のコマンドでOpen WebUIをダウンロード&実行します。

docker run \
  -d \
  -e OPENAI_API_BASE_URL="http://localhost:8355/v1" \
  -v open-webui:/app/backend/data \
  --network host \
  --add-host=host.docker.internal:host-gateway \
  --name open-webui \
  --restart always \
  ghcr.io/open-webui/open-webui:main

完了したら docker psでコンテナの状態を確認します。
open-webuiが実行されていればOKです。

Step 2(①):WebUIの表示

localhost:8080にWebブラウザでアクセスすればWebUIが表示される。
アカウントを作成すれば,その他の設定は特に不要です。

おわりに

そこまで実行速度は速くないですが,ここまでの大規模モデルを机の上で動かせるのは利点だと思いました。
今後は余裕があればvLLMの設定なども行っていく予定です。

1
1
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
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?