Fragments of verbose memory

冗長な記憶の断片 - Web技術のメモをほぼ毎日更新

Jun 8, 2026 - 日記

DGX SparkでQwen3.6-27Bを動かす: ASUS Ascent GX10 + vLLMセットアップ

DGX SparkでQwen3.6-27Bを動かす: ASUS Ascent GX10 + vLLMセットアップ

ASUS Ascent GX10 を購入しました。NVIDIA GB10 Grace Blackwell Superchip を載せた、いわゆる NVIDIA DGX Spark 系の小型AIワークステーションです。

まずは難しいことをせず、「この箱でローカルLLMサーバを立てて、手元の端末からOpenAI互換APIとして叩ける」状態を目標にしました。今回は vLLMQwen/Qwen3.6-27B-FP8 を起動し、通常のチャットAPIと簡単なベンチマークまで確認した記録です。

なぜGX10を買ったのか

ここ数年、ローカルLLMはかなり現実的になってきました。小さめのモデルならノートPCでも動きますし、量子化モデルを使えば手元で試せる範囲も広がっています。以前書いた CanIRun.aiの記事 でも、「このGPUでどのLLMが動くか」を見積もるツールを紹介しました。

ただ、実際に日常的に使おうとすると、単に「動く」だけでは足りません。

  • ある程度大きいモデルを載せたい
  • 長めのコンテキストを扱いたい
  • 推論サーバとして常時起動したい
  • 手元のツールや自作アプリからOpenAI互換APIで呼びたい
  • GPUメモリやランタイムの挙動を自分で見ながら調整したい

このあたりを自宅・手元環境で試すための機械として、GX10はかなり面白い立ち位置にあります。Mac Studioクラスタや exo の方向も魅力的ですが、単体のNVIDIA系ワークステーションとしてvLLMを素直に動かせる環境も欲しかった、というのが購入理由です。

複数台Macで分散する方向については、以前 macOS 26.2のRDMA over Thunderbolt記事exo 1.0の記事 で書きました。今回はその対極として、単体のDGX Spark機をローカル推論APIサーバにする話です。

今回できたこと

今回の到達点は次の通りです。

  • GX10側でGPUとCUDAの基本確認
  • spark-vllm-docker を使ったvLLM環境の準備
  • Qwen/Qwen3.6-27B-FP8 の起動
  • http://gx10-3cd9:8000/v1 でOpenAI互換APIを公開
  • /v1/models/v1/chat/completions の疎通確認
  • vllm bench serve による簡単な性能確認

確認した範囲では、Qwen/Qwen3.6-27B-FP8 はGX10上でかなり素直に扱えました。35B MoE系も試していますが、まず日常的に立てておくローカルAPIとしては、27B Denseのほうが見通しがよさそうです。

構成

今回の構成は次の通りです。

  • ホスト: gx10-3cd9
  • 機種扱い: DGX Spark / NVIDIA GB10
  • API: http://gx10-3cd9:8000/v1
  • 起動方式: spark-vllm-dockerlaunch-cluster.sh
  • モデル: Qwen/Qwen3.6-27B-FP8

構成図にすると、こんな感じです。

flowchart LR
    client[手元の端末 / アプリ] -->|OpenAI互換API| api[http://gx10-3cd9:8000/v1]
    api --> vllm[vLLM]
    vllm --> qwen[Qwen3.6-27B-FP8]
    qwen --> gb10[NVIDIA GB10 / DGX Spark]

ポイントは、GX10を「LLMが入った作業端末」としてではなく、LAN内の推論APIサーバとして扱うことです。こうしておくと、手元のMacや別マシンから curl でもアプリでも同じAPIを叩けます。

事前確認

ここからのコマンドは、基本的に GX10上のターミナルで直接実行する 前提です。別マシンから遠隔操作してもよいのですが、記事としてはローカル作業の形で書いたほうが見通しがよいので、以降はGX10上でそのまま打てるコマンドとして載せます。

まず、GPUとPython環境が見えるか確認します。

1
2
3
4
hostname
nvidia-smi --query-gpu=name,memory.total,memory.free --format=csv,noheader
python3 --version
command -v vllm || true

GB10環境では、nvidia-smi --query-gpu=memory.total[N/A] になることがあります。通常のディスクリートGPUのようなVRAM表示とは少し違うので、ここだけで失敗と判断しないほうがよさそうです。

PyTorchからCUDAが見えているかも確認します。

1
2
3
4
5
python3 - <<"PY"
import torch
print(torch.cuda.is_available())
print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else None)
PY

GPU名が返り、PyTorchでCUDAが使えるなら、まずは次に進めます。

spark-vllm-dockerを準備する

今回は eugr/spark-vllm-docker を使いました。DGX Spark / GB10向けにvLLMをDocker経由で立ち上げるための構成です。

1
2
3
4
5
mkdir -p ~/services
cd ~/services
test -d spark-vllm-docker || git clone https://github.com/eugr/spark-vllm-docker.git
cd ~/services/spark-vllm-docker
sudo HOME=/home/tumf ./build-and-copy.sh

ビルド後、vllm-node イメージができているか確認します。

1
sudo docker images --format "{{.Repository}}:{{.Tag}} {{.Size}}" | grep "^vllm-node:"

Qwen3.6-27B-FP8を起動する

起動スクリプトは gx10 側に置きました。

モデルはHugging Faceから取得するため、vLLMを起動するコンテナにもHugging Faceのアクセストークンを渡します。ここでは、あらかじめGX10上で huggingface-cli login などを済ませておき、/home/tumf/.cache/huggingface/token に保存されているトークンを読む形にしています。

トークンの値そのものは表示しないようにします。スクリプトでもファイルから読み込んで HF_TOKEN 環境変数としてコンテナに渡すだけです。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!/usr/bin/env bash
set -euo pipefail

cd /home/tumf/services/spark-vllm-docker
HF_TOKEN_VALUE="$(tr -d "\n" < /home/tumf/.cache/huggingface/token)"

sudo HOME=/home/tumf ./launch-cluster.sh \
  --solo \
  --name vllm_qwen36 \
  -p 8000:8000 \
  -e "HF_TOKEN=${HF_TOKEN_VALUE}" \
  exec vllm serve Qwen/Qwen3.6-27B-FP8 \
    --host 0.0.0.0 \
    --port 8000 \
    --dtype bfloat16 \
    --max-model-len 262144 \
    --gpu-memory-utilization 0.65 \
    --kv-cache-dtype fp8 \
    --served-model-name qwen3.6-27b-fp8 \
    --reasoning-parser qwen3 \
    --enable-chunked-prefill \
    --enable-prefix-caching \
    --trust-remote-code \
    --default-chat-template-kwargs '{"enable_thinking": false}'

ポイントはこのあたりです。

  • --max-model-len 262144: Qwen3.6-27Bの長いコンテキストを活かすため
  • --gpu-memory-utilization 0.65: まずは余裕を持たせた設定から開始
  • --kv-cache-dtype fp8: 長いcontextを確保するために使用
  • --default-chat-template-kwargs '{"enable_thinking": false}': まずは通常応答を安定させるためthinkingを切る

Qwen3.6 系は thinking mode が有効になり得ます。最初のセットアップでは、thinkingを切って「普通に聞いたら普通に返る」状態を作るほうが切り分けしやすいです。

なお、起動にはそれなりに時間がかかります。私の環境では、safetensors 66 shard の読み込み、torch.compile、CUDA graph capture を含めて約6〜7分ほど待ちました。ready前の /health は connection reset になることがあります。

疎通確認

モデル一覧を確認します。

1
curl -sS http://127.0.0.1:8000/v1/models

通常チャットも確認します。

1
2
3
4
5
6
7
8
curl -sS http://127.0.0.1:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3.6-27b-fp8",
    "messages": [
      {"role": "user", "content": "日本語で短くpongと返して"}
    ]
  }'

疎通確認だけなら、max_tokenstemperature は指定しなくても十分です。短く・決定的に返したい場合だけ、リクエスト側で追加します。

ready待ちは、次のようなループにしておくと楽です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
for i in $(seq 1 96); do
  if curl -fsS http://127.0.0.1:8000/v1/models >/tmp/vllm_models.json 2>/tmp/vllm_curl.err; then
    echo READY
    cat /tmp/vllm_models.json
    exit 0
  fi
  sleep 5
done

echo NOT_READY
ss -ltnp | grep :8000 || true
cat /tmp/vllm_curl.err 2>/dev/null || true

LAN内の別マシンから使う場合は、base URL を次のように見ます。

1
http://gx10-3cd9:8000/v1

OpenAI互換APIなので、自作アプリ、CLIツール、各種LLMクライアントから流用しやすいのがvLLMの便利なところです。

ベンチマーク

稼働中の qwen3.6-27b-fp8 に対して、vllm bench serve も軽く実行しました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
sudo docker exec vllm_qwen36 vllm bench serve \
  --backend openai-chat \
  --model qwen3.6-27b-fp8 \
  --base-url http://127.0.0.1:8000 \
  --endpoint /v1/chat/completions \
  --dataset-name random \
  --random-input-len 1024 \
  --random-output-len 256 \
  --num-prompts 50 \
  --request-rate inf \
  --max-concurrency 8 \
  --tokenizer Qwen/Qwen3.6-27B-FP8

結果は次の通りです。

条件 Output tok/s Peak output tok/s Total tok/s TTFT mean TPOT mean 成功/失敗
16 prompts / max concurrency 4 25.57 34.00 129.07 1911.70 ms 149.38 ms 16 / 0
50 prompts / max concurrency 8 43.85 64.00 221.37 2849.90 ms 156.66 ms 50 / 0

この数字だけを見ると、クラウド上の大型GPUサーバのような爆速環境ではありません。ただ、手元の小型機で27Bクラスのモデルを長めのcontext付きで常時立てられる、という意味ではかなり実用的です。

注意点として、--model qwen3.6-27b-fp8 だけだと、ベンチ側が Hugging Face repo として qwen3.6-27b-fp8 を探して404になることがありました。--tokenizer Qwen/Qwen3.6-27B-FP8 を明示すると通りました。

また、vllm bench serve はデフォルトでtemperature 0を送らないため、再現性を重視してgreedy固定したい場合は --temperature=0 を付けます。単なる疎通確認では不要です。

27B Denseを先に選んだ理由

最初は、より大きい Qwen3.6-35B-A3B-FP8 や DFlash 構成も試しました。通常チャットや速度実験としては、こちらも面白いです。

ここでいう Dense は、MoE(Mixture of Experts)のように一部のexpertを選んで使う構成ではなく、基本的に一枚岩のモデルとして動く、という意味です。ざっくり言えば、Denseは普通のモデル、MoEは複数の専門家を持つモデルです。

ただ、初期セットアップで重要なのは「一番大きいモデルを載せること」より、安定して起動し、APIとして素直に返ることです。モデル重みだけでなく、KV cache、chat template、thinking mode、ランタイム側の対応状況が絡みます。

その意味で、今回のGX10では Qwen/Qwen3.6-27B-FP8 が最初の常用候補として扱いやすいと感じました。27B Denseなので挙動が読みやすく、262k context構成も試しやすいです。

停止方法と運用メモ

停止は launch-cluster.sh から行えます。

1
2
cd ~/services/spark-vllm-docker
sudo HOME=/home/tumf ./launch-cluster.sh --solo --name vllm_qwen36 stop

雑に pkill -f "vllm serve" すると、実行中のシェルや関連プロセスまで巻き込むことがあります。PIDを確認するなら、まずLISTENしているプロセスを見るほうが安全です。

1
ss -ltnp | grep :8000

おまけ:GPUモニター用にnvtopをビルドする

vLLMを立ち上げると、モデル読み込み中やCUDA graph capture中にGPU側で何が起きているか見たくなります。通常なら nvtop を入れて見るところですが、2026年6月時点では、配布版の nvtop がGX10のunified memory表示にうまく対応していませんでした。

そのため、私は nvtop をGitHubから取得してビルドしました。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
mkdir -p ~/work
cd ~/work
git clone https://github.com/Syllo/nvtop.git

sudo apt update
sudo apt install -y \
    libudev-dev \
    libsystemd-dev \
    libdrm-dev \
    libncurses-dev \
    libncursesw5-dev

cd ~/work/nvtop
rm -rf build
mkdir build
cd build
cmake ..
make -j$(nproc)
cp ./src/nvtop ~/.local/bin

これで ~/.local/bin/nvtop として使えます。~/.local/binPATH に入っていない場合は、フルパスで実行します。

1
~/.local/bin/nvtop

まとめ

ASUS Ascent GX10 / DGX Spark 相当機で、Qwen/Qwen3.6-27B-FP8 を vLLM から起動し、LAN内のOpenAI互換APIとして使えるところまで確認しました。

今回の実感としては、GX10は「LLMを試すための高価な箱」というより、自宅や開発環境に置けるローカル推論サーバとして見ると魅力が出ます。クラウドGPUを借りるほどではない日常的な検証、長めのcontextを使った実験、自作ツールからのAPI呼び出しなどに使いやすいです。

まずは27B Denseで安定した土台を作り、そこから35B MoE、DFlash、SGLang、複数モデル切り替えへ広げていくのがよさそうです。

参考リンク