😺

DGX Spark + Docker + SGLang + Qwen3-Coder-Next-FP8 環境構築

に公開

DGX Spark 上の Docker 環境で SGLang の OpenAI-compatible API server を起動し、Qwen/Qwen3-Coder-Next-FP8 を OpenCode などの AI Agent 用途で利用できるようにします。

Qwen3-Coder-Next は、Alibaba / Qwen 系のコーディング特化LLMです。
単なるコード補完モデルというより、CLI/IDE 上でツールを呼び出しながら修正・実行・失敗からの復旧まで行う「コーディングエージェント」用途を強く意識したモデルです。
総 80B パラメータの中で、実際に活性化されるのは 3B の MoE であり、<think> ブロックを出力しない non-thinking mode 専用でもあります。

Qwen/Qwen3-Coder-Next-FP8 は Qwen3-Coder-Next の FP8 量子化版として、85GB 級に収まるので、DGX Spark でも運用可能なサイズです。

初期構成では、速度よりも 128K context を安定して動かすこと を優先します。
速度最適化や 256K context 化は、128K で安定稼働を確認してから段階的に行います。

1. 環境構築前提

この手順は以下の前提で作成しています。

前提:
- NVIDIA DGX Spark 上で実行する。
- Docker / Docker Compose v2 が利用可能。
- NVIDIA Driver が正しく導入済み。
- NVIDIA Container Toolkit が正しく導入済み。
- nvcr.io/nvidia/sglang:26.04-py3 は利用可能。
- Qwen/Qwen3-Coder-Next-FP8 を SGLang で OpenAI-compatible API として起動する。
- OpenCode などの AI Agent から利用する。
- 初期運用では 128K context を必須条件とする。

この手順では、SGLang コンテナはホスト上に Python 環境を作らず、Docker 内で完結させます。

2. ディレクトリ作成

# SGLang 用の作業ディレクトリを作成します。
#
# hf-cache:
#   Hugging Face から取得したモデルファイルを保存します。
#   Docker コンテナを削除・再作成してもモデルを再ダウンロードしないために使います。
#
# logs:
#   将来的にログ保存や補助スクリプト出力を置けるように作成します。
#   SGLang 本体の主なログは docker logs で確認します。
mkdir -p ~/docker/sglang-qwen3-coder/hf-cache ~/docker/sglang-qwen3-coder/logs

# 作業ディレクトリへ移動します。
cd ~/docker/sglang-qwen3-coder

3. 事前確認

3.1 Docker Compose 確認

# Docker Compose v2 が利用できることを確認します。
docker compose version

3.2 GPU 確認

# ホスト OS から GPU が見えていることを確認します。
nvidia-smi

4. .env 作成

以下の内容で .env を作成します。

# ==============================================================================
# Hugging Face
# ==============================================================================

# Hugging Face token.
#
# Public model では空でも動作する場合があります。
# ただし、以下のケースでは設定を推奨します。
#
# - Hugging Face 側の rate limit を避けたい場合
# - gated model を利用する場合
# - private model を利用する場合
# - 組織アカウントで安定してモデル取得したい場合
#
# 例:
# HF_TOKEN=hf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
#
# token を使わない場合は空のままで問題ありません。
HF_TOKEN=

# ==============================================================================
# SGLang server basic settings
# ==============================================================================
# ホスト側に公開する API port です。
#
# docker-compose.yml では以下のように localhost のみに bind します。
#
#   127.0.0.1:${PORT}:30000
#
# そのため、初期状態では同一ホストからのみアクセスできます。
#
# 別マシンからアクセスしたい場合は、docker-compose.yml の ports 設定を変更したうえで、
# firewall、VPN、reverse proxy、認証などを別途検討してください。
PORT=30000

# OpenAI-compatible API 上で外部に見せるモデル名です。
#
# OpenCode などのクライアント側では、この値を model として指定します。
# 実際の Hugging Face model path とは別名にできます。
#
# 例:
#   OpenCode model: qwen3-coder-next-fp8
SERVED_MODEL_NAME=qwen3-coder-next-fp8

# 起動する Hugging Face model path です。
#
# Qwen/Qwen3-Coder-Next-FP8 は FP8 checkpoint のため、
# SGLang 起動時に追加で --quantization fp8 を指定する必要は基本的にありません。
MODEL_PATH=Qwen/Qwen3-Coder-Next-FP8


# ==============================================================================
# Long context settings
# ==============================================================================

# コンテキスト長です。
#
# AI Agent 用途では、長いリポジトリ文脈、長い会話履歴、
# 複数ファイルを含む修正依頼などを扱うことが多いため、
# 初期値は 128K = 131072 tokens にします。
#
# 推奨値:
#   131072 = 128K。初期安定運用向け。
#   196608 = 192K。128K 安定後に試す候補。
#   262144 = 256K。最大級。OOM や速度低下に注意。
#
# まずは 128K で起動、長文テスト、OpenCode 実タスクを確認してください。
CONTEXT_LENGTH=131072

# 1 回の生成で許可する最大出力 token 数です。
#
# 大きくすると長いコード生成や大規模修正案に対応しやすくなりますが、
# 1 リクエストが長時間 GPU を占有しやすくなります。
#
# 初期安定運用では 8192 を推奨します。
#
# 推奨値:
#   8192  = 初期安定運用向け。
#   16384 = 長めの設計・修正出力向け。
#   32768 = 大規模生成向け。ただし遅くなりやすい。
MAX_NEW_TOKENS=8192


# ==============================================================================
# Performance / memory tuning
# ==============================================================================

# SGLang の static memory allocation 比率です。
#
# 主に model weights と KV cache pool に使われる GPU メモリ割合を調整します。
# 長コンテキストでは KV cache が大きくなるため、ある程度高めに確保したくなります。
#
# ただし高すぎると Docker、CUDA、PyTorch、OS 側の余白が不足し、
# OOM しやすくなります。
#
# チューニング目安:
#   0.78 = かなり保守的。OOM 回避優先。
#   0.82 = 初期安定運用向け。
#   0.86 = 128K 安定後の性能改善候補。
#   0.90 = 攻めた設定。環境によっては OOM しやすい。
MEM_FRACTION_STATIC=0.82

# Chunked prefill size です。
#
# 長い prompt を一度に prefill すると GPU メモリ使用量が跳ねやすいため、
# prefill を分割して処理するためのサイズです。
#
# 小さくすると安定しやすくなりますが、prefill 速度は落ちる可能性があります。
# 大きくすると高速化しやすい一方、OOM リスクが上がります。
#
# チューニング目安:
#   4096  = 安定優先。128K 初回検証向け。
#   8192  = 速度と安定性のバランス。
#   16384 = 速度優先。OOM する場合があります。
CHUNKED_PREFILL_SIZE=4096

# 同時に実行する request 数です。
#
# AI Agent 用途では、単一 agent が長い prompt と tool call を連続して投げるケースが多いため、
# 初期値は 1 にします。
#
# 長コンテキストでここを増やすと、KV cache や prefill 負荷が増えて OOM しやすくなります。
#
# 推奨値:
#   1 = 最安定。128K / 256K 運用向け。
#   2 = 軽めの並列作業向け。128K 安定後に検証。
#   4 = 短文中心なら候補。ただし長コンテキスト AI Agent では非推奨。
MAX_RUNNING_REQUESTS=1

# queue 上限です。
#
# OpenCode などの Agent は短時間に複数リクエストを投げることがあります。
# queue を少し持たせることで、即エラーにせず待たせることができます。
#
# ただし大きすぎると、詰まったリクエストに気づきにくくなります。
#
# 初期値は 8 にします。
MAX_QUEUED_REQUESTS=8

# KV cache dtype です。
#
# auto は SGLang / モデル / 実行環境に任せる安全寄りの設定です。
#
# 長コンテキストでメモリが厳しい場合、fp8_e5m2 などを試す余地があります。
# ただし、FP8 KV cache はメモリ削減に有効な一方で、
# 長文理解、JSON 出力、tool calling、コード生成の安定性に影響する可能性があります。
#
# 初期値は auto にします。
KV_CACHE_DTYPE=auto


# ==============================================================================
# Qwen3-Coder-Next specific tuning
# ==============================================================================

# Qwen3-Coder-Next は hybrid architecture を持つモデルです。
# SGLang では Mamba 系 scheduler strategy を選択できます。
#
# no_buffer:
#   メモリ使用量を抑えやすく、長コンテキスト運用で安全寄りです。
#
# extra_buffer:
#   overlap scheduling により throughput 改善の可能性があります。
#   ただし mamba state memory が増えやすく、
#   128K 以上では OOM や同時実行数低下の可能性があります。
#
# 初期値は安定優先で no_buffer にします。
MAMBA_SCHEDULER_STRATEGY=no_buffer

# Mamba SSM state dtype です。
#
# float32:
#   精度・安全寄りですが、メモリ使用量が増えます。
#
# bfloat16:
#   メモリ削減寄りです。
#   長コンテキスト運用の余裕を作りやすいため、初期値は bfloat16 にします。
MAMBA_SSM_DTYPE=bfloat16

# page size です。
#
# 初期安定構成では docker-compose.yml 側で --page-size を指定しません。
# extra_buffer を検証する段階で必要に応じて有効化します。
#
# 高速化検証時の候補:
#   PAGE_SIZE=64
PAGE_SIZE=64


# ==============================================================================
# Runtime behavior
# ==============================================================================

# Python stdout / stderr を buffer しない設定です。
#
# docker logs -f でログをリアルタイム確認しやすくします。
PYTHONUNBUFFERED=1

# tokenizers の並列処理 warning を抑制します。
#
# 実行結果そのものに大きな影響を与える設定ではありません。
TOKENIZERS_PARALLELISM=false

# PyTorch CUDA allocator の断片化対策です。
#
# 環境によっては有効な場合がありますが、
# PyTorch / CUDA / driver / container の組み合わせによっては警告が出る場合があります。
#
# 初期構成ではあえて無効化します。
# 長時間運用でメモリ断片化が疑われる場合のみ、以下1Gを有効化してください。
#
# PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True

5. docker-compose.yml 作成

以下の内容で docker-compose.yml を作成します。

services:
  sglang-qwen3-coder:
    # NVIDIA NGC の SGLang コンテナです。
    #
    # この手順では、このイメージが利用可能である前提です。
    # ホスト側に Python 環境を構築せず、
    # SGLang 実行環境をコンテナ内に閉じ込めます。
    image: nvcr.io/nvidia/sglang:26.04-py3

    # コンテナ名です。
    #
    # docker logs、docker exec、docker restart などで指定しやすくするため固定します。
    container_name: sglang-qwen3-coder

    # ホスト再起動や Docker daemon 再起動後に自動復旧させます。
    #
    # unless-stopped:
    #   手動で docker stop した場合は自動再起動しません。
    #   サーバー用途で扱いやすい設定です。
    restart: unless-stopped

    # NVIDIA GPU をコンテナへ渡します。
    #
    # DGX Spark 単体でこの SGLang サーバーを動かす前提では all で問題ありません。
    # 他の GPU ワークロードと共存させる場合は、
    # 必要に応じて GPU 指定を検討してください。
    gpus: all

    # PyTorch / SGLang の multi-process 通信や共有メモリ利用のため host IPC を使います。
    #
    # 注意:
    #   ipc: host を使う場合、コンテナはホストの IPC namespace を使います。
    #   この場合、shm_size を併記しても期待通りに効かない場合があります。
    #
    # この手順では DGX Spark 単体運用を想定し、ipc: host に寄せます。
    ipc: host

    # OpenAI-compatible API port です。
    #
    # 重要:
    #   127.0.0.1:${PORT}:30000 として bind することで、
    #   初期状態では同一ホストからのみアクセス可能にします。
    #
    # 理由:
    #   SGLang の API を LAN やインターネットに不用意に公開すると危険です。
    #   別マシンから使う場合は、VPN、firewall、reverse proxy、認証などを別途設定してください。
    #
    # Host 側:
    #   ${PORT:-30000}
    #
    # Container 側:
    #   30000
    ports:
      - "0.0.0.0:${PORT:-30000}:30000"

    # .env から環境変数を読み込みます。
    #
    # MODEL_PATH、SERVED_MODEL_NAME、CONTEXT_LENGTH など、
    # 運用時に調整しやすい値は .env 側にまとめています。
    env_file:
      - .env

    environment:
      # Hugging Face token です。
      # 空でも Public model では動作する場合があります。
      HF_TOKEN: ${HF_TOKEN:-}

      # Hugging Face cache 設定です。
      # volumes で ./hf-cache に永続化します。
      HF_HOME: /root/.cache/huggingface
      HF_HUB_CACHE: /root/.cache/huggingface/hub

      # Transformers 互換用です。
      # 環境によって非推奨警告が出る場合があります。
      TRANSFORMERS_CACHE: ${TRANSFORMERS_CACHE:-/root/.cache/huggingface}

      # Python ログを即時出力します。
      # docker logs -f で進行状況を見やすくするための設定です。
      PYTHONUNBUFFERED: ${PYTHONUNBUFFERED:-1}

      # tokenizer 並列 warning を抑制します。
      TOKENIZERS_PARALLELISM: ${TOKENIZERS_PARALLELISM:-false}

      # PyTorch CUDA allocator 設定です。
      #
      # 初期構成では .env 側で未定義にしています。
      # 必要時のみ .env で以下を有効化してください。
      #
      # PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
      PYTORCH_CUDA_ALLOC_CONF: ${PYTORCH_CUDA_ALLOC_CONF:-}

    volumes:
      # Hugging Face model cache です。
      #
      # 初回起動時にモデルファイルがここへ保存されます。
      # 2 回目以降は同じキャッシュを使うため、再ダウンロードを避けられます。
      - ./hf-cache:/root/.cache/huggingface

      # ログや補助ファイル保存用です。
      #
      # SGLang 本体の主ログは標準出力に出るため、通常は docker logs で確認します。
      - ./logs:/logs

    command:
      # ------------------------------------------------------------------------
      # SGLang OpenAI-compatible server
      # ------------------------------------------------------------------------

      # Python module として SGLang server を起動します。
      - python3
      - -m
      - sglang.launch_server

      # 起動する Hugging Face model path です。
      #
      # .env:
      #   MODEL_PATH=Qwen/Qwen3-Coder-Next-FP8
      - --model-path
      - ${MODEL_PATH}

      # OpenAI-compatible API 上で見せるモデル名です。
      #
      # クライアント側では、この名前を model として指定します。
      #
      # .env:
      #   SERVED_MODEL_NAME=qwen3-coder-next-fp8
      - --served-model-name
      - ${SERVED_MODEL_NAME}

      # コンテナ内で listen するアドレスです。
      #
      # 0.0.0.0:
      #   コンテナ内の全 interface で listen します。
      #
      # 実際のホスト公開範囲は ports の
      #   127.0.0.1:${PORT}:30000
      # で制限しています。
      - --host
      - 0.0.0.0

      # コンテナ内の listen port です。
      #
      # Host 側 port は docker-compose.yml の ports で制御します。
      - --port
      - "30000"

      # 長コンテキスト設定です。
      #
      # 初期値:
      #   131072 = 128K
      #
      # AI Agent 用途で大きなリポジトリ文脈を扱うための中心設定です。
      - --context-length
      - ${CONTEXT_LENGTH}

      # Qwen3-Coder-Next 用の tool calling parser です。
      #
      # OpenCode などの AI Agent で tool / function calling を扱う場合に重要です。
      # Qwen3-Coder-Next 向けには qwen3_coder parser を使います。
      - --tool-call-parser
      - qwen3_coder

      # KV cache dtype です。
      #
      # 初期値は auto です。
      # 長コンテキストでメモリ不足が発生する場合のみ fp8_e5m2 などを検討します。
      - --kv-cache-dtype
      - ${KV_CACHE_DTYPE}

      # static memory allocation 比率です。
      #
      # model weights と KV cache pool に割り当てるメモリ量に影響します。
      # 初期値は安定優先で 0.82 です。
      - --mem-fraction-static
      - ${MEM_FRACTION_STATIC}

      # chunked prefill size です。
      #
      # 長い prompt の prefill を分割し、OOM リスクを下げます。
      # 初期値は安定優先で 4096 です。
      - --chunked-prefill-size
      - ${CHUNKED_PREFILL_SIZE}

      # 同時実行 request 数です。
      #
      # 長コンテキスト AI Agent 用途では 1 が最も安全です。
      - --max-running-requests
      - ${MAX_RUNNING_REQUESTS}

      # queue 上限です。
      #
      # Agent から連続リクエストが来た場合に即エラーにせず待たせます。
      - --max-queued-requests
      - ${MAX_QUEUED_REQUESTS}

      # Qwen3-Coder-Next の hybrid / Mamba 系 scheduler 設定です。
      #
      # 初期値:
      #   no_buffer
      #
      # 長コンテキストではメモリ安定性を優先します。
      - --mamba-scheduler-strategy
      - ${MAMBA_SCHEDULER_STRATEGY}

      # Mamba SSM state dtype です。
      #
      # 初期値:
      #   bfloat16
      #
      # 長コンテキスト運用でメモリ使用量を抑える目的です。
      - --mamba-ssm-dtype
      - ${MAMBA_SSM_DTYPE}

      # page-size は初期構成では指定しません。
      #
      # 理由:
      #   安定優先の no_buffer 運用では、まず最小限の指定で起動確認します。
      #   extra_buffer による高速化検証時に必要に応じて有効化します。
      #
      # 高速化検証時の追加例:
      #
      # - --page-size
      # - ${PAGE_SIZE}

6. 起動

# バックグラウンドで SGLang サーバーを起動します。
docker compose up -d

7. ログ確認

# 起動ログを追跡します。
#
# 初回起動時は Hugging Face からモデルをダウンロードするため時間がかかります。
# hf-cache へ保存されるため、2 回目以降は短縮されます。
docker logs -f sglang-qwen3-coder

別ターミナルで GPU 使用状況を確認します。

# GPU メモリ使用量、GPU utilization、プロセスを確認します。
watch -n 1 nvidia-smi

Docker 側のリソース状況も確認できます。

# コンテナの CPU / メモリ / ネットワーク I/O などを確認します。
docker stats sglang-qwen3-coder

8. 疎通確認

8.1 models API 確認

# OpenAI-compatible API の /v1/models が返ることを確認します。
curl -s http://localhost:30000/v1/models

jq が使える場合:

curl -s http://localhost:30000/v1/models | jq .

8.2 短文生成確認

# 最小限の chat completions 確認です。
# まずは短文で API 形式、model 名、基本応答を確認します。
curl -i http://localhost:30000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-coder-next-fp8",
    "messages": [
      {
        "role": "user",
        "content": "Say OK."
      }
    ],
    "max_tokens": 16,
    "temperature": 0.2
  }'

9. 128K 向け長文入力テスト

/v1/models と短文生成だけでは、128K context 運用の確認としては不十分です。
段階的に長文 prompt を投入し、OOM や timeout が起きないことを確認します。

9.1 32K 相当の軽い長文テスト

python3 - <<'PY'
import requests

url = "http://localhost:30000/v1/chat/completions"

# まずは軽めの長文で prefill が通るか確認します。
# 実 token 数は tokenizer に依存するため厳密な 32K ではありませんが、
# 短文テストよりは長い入力の動作確認になります。
text = "hello world\n" * 8000

payload = {
    "model": "qwen3-coder-next-fp8",
    "messages": [
        {
            "role": "user",
            "content": text + "\nSummarize the repeated text in one sentence."
        }
    ],
    "max_tokens": 256,
    "temperature": 0.2
}

response = requests.post(url, json=payload, timeout=600)

print("status:", response.status_code)
print(response.text[:2000])
PY

9.2 64K 相当の長文テスト

python3 - <<'PY'
import requests

url = "http://localhost:30000/v1/chat/completions"

# 64K 級の入力を想定した段階テストです。
# OOM する場合は MEM_FRACTION_STATIC や CHUNKED_PREFILL_SIZE を調整します。
text = "This is a long context line for SGLang Qwen3 Coder Next test.\n" * 20000

payload = {
    "model": "qwen3-coder-next-fp8",
    "messages": [
        {
            "role": "user",
            "content": text + "\nReturn exactly one sentence explaining what this input contains."
        }
    ],
    "max_tokens": 256,
    "temperature": 0.2
}

response = requests.post(url, json=payload, timeout=900)

print("status:", response.status_code)
print(response.text[:2000])
PY

9.3 128K 運用確認

以下はより重いテストです。
GPU メモリを見ながら実行してください。

python3 - <<'PY'
import requests

url = "http://localhost:30000/v1/chat/completions"

# 128K 級に近い長文入力を想定した検証です。
# 実 token 数は tokenizer 依存のため、正確な 128K ではありません。
# 目的は、長い prompt の prefill と生成が実運用上問題ないかを見ることです。
text = "The quick brown fox jumps over the lazy dog. This line is used for long context testing.\n" * 50000

payload = {
    "model": "qwen3-coder-next-fp8",
    "messages": [
        {
            "role": "user",
            "content": text + "\nSummarize this long repeated context in three bullet points."
        }
    ],
    "max_tokens": 512,
    "temperature": 0.2
}

response = requests.post(url, json=payload, timeout=1800)

print("status:", response.status_code)
print(response.text[:4000])
PY

10. OpenCode 側設定

OpenCode などの OpenAI-compatible API client では以下を指定します。

Base URL: http://localhost:30000/v1
Model   : qwen3-coder-next-fp8
API Key : EMPTY

API Key が必須入力の場合は、任意のダミー文字列を入れてください。

API Key : dummy

11. 通常運用コマンド

起動

docker compose up -d

停止

docker compose down

再起動

docker compose restart

ログ確認

docker logs -f sglang-qwen3-coder

直近ログ確認

docker logs --tail=200 sglang-qwen3-coder

コンテナ状態確認

docker ps --filter name=sglang-qwen3-coder

12. 設定変更の反映

.env または docker-compose.yml を変更した場合は、以下で反映します。

# コンテナを停止・削除します。
# volume として bind mount している ./hf-cache は残るため、モデルキャッシュは消えません。
docker compose down

# 設定を読み直して起動します。
docker compose up -d

# 起動ログを確認します。
docker logs -f sglang-qwen3-coder

13. OOM する場合の調整

128K 起動時または長文入力時に OOM する場合は、まず .env を以下のように保守的にします。

# static memory allocation を下げ、OS / CUDA / PyTorch 側の余白を増やします。
MEM_FRACTION_STATIC=0.78

# prefill の分割サイズを小さくして、長文入力時のピークメモリを下げます。
CHUNKED_PREFILL_SIZE=4096

# 長コンテキストでは同時実行を増やさないでください。
MAX_RUNNING_REQUESTS=1

# queue が溜まりすぎると状況確認しづらいため、少なめにします。
MAX_QUEUED_REQUESTS=4

反映:

docker compose down
docker compose up -d
docker logs -f sglang-qwen3-coder

それでも厳しい場合、KV cache FP8 を最後の手段として試します。

# メモリ削減目的で KV cache を FP8 にします。
# ただし、長文理解、tool calling、JSON 出力、コード生成品質に影響する可能性があります。
# 変更後は OpenCode で実タスクを必ず回帰テストしてください。
KV_CACHE_DTYPE=fp8_e5m2

14. 速度を上げたい場合

128K で安定稼働し、OpenCode で実タスクが問題なく動くことを確認した後に試します。

# static memory allocation を少し上げ、KV cache pool を確保しやすくします。
MEM_FRACTION_STATIC=0.86

# prefill 分割サイズを大きくし、長文入力の処理速度改善を狙います。
# OOM する場合は 4096 へ戻してください。
CHUNKED_PREFILL_SIZE=8192

# Mamba scheduler を throughput 寄りにします。
# mamba state memory が増える可能性があるため、GPU メモリを監視してください。
MAMBA_SCHEDULER_STRATEGY=extra_buffer

# extra_buffer 検証時の候補です。
PAGE_SIZE=64

この場合は docker-compose.ymlcommand: 末尾に以下を追加します。

      # extra_buffer 検証時に有効化します。
      # 初期安定運用では指定しません。
      - --page-size
      - ${PAGE_SIZE}

反映:

docker compose down
docker compose up -d
docker logs -f sglang-qwen3-coder

速度改善を試す場合は、以下を比較してください。

比較項目:
- 初回応答までの時間
- 長文 prompt 投入時の prefill 時間
- tokens/sec
- GPU メモリ使用量
- OOM 有無
- OpenCode での tool calling 安定性
- JSON 出力やコード編集結果の安定性

15. 256K context を試す場合

128K で安定してから試してください。

# 256K context に変更します。
CONTEXT_LENGTH=262144

# まずは安定優先の値に戻します。
MEM_FRACTION_STATIC=0.82
CHUNKED_PREFILL_SIZE=4096
MAX_RUNNING_REQUESTS=1
MAX_QUEUED_REQUESTS=4
KV_CACHE_DTYPE=auto
MAMBA_SCHEDULER_STRATEGY=no_buffer

反映:

docker compose down
docker compose up -d
docker logs -f sglang-qwen3-coder

256K で OOM する場合は、以下を順番に試します。

# 1. 余白を増やします。
MEM_FRACTION_STATIC=0.78
# 2. queue を減らします。
MAX_QUEUED_REQUESTS=2
# 3. 最後の手段として KV cache FP8 を試します。
KV_CACHE_DTYPE=fp8_e5m2

16. 外部マシンからアクセスしたい場合

初期構成では安全のため、API は localhost にのみ bind しています。

ports:
  - "127.0.0.1:${PORT:-30000}:30000"

別マシンからアクセスする必要がある場合は、以下のように変更できます。

ports:
  - "${PORT:-30000}:30000"

ただし、この変更を行う場合は必ず以下を検討してください。

必須検討:
- LAN 内だけに制限する
- firewall でアクセス元 IP を制限する
- VPN 経由にする
- reverse proxy で認証を追加する
- インターネットへ直接公開しない

SGLang の OpenAI-compatible API は便利ですが、初期状態で強い認証ゲートウェイとして扱うべきではありません。

17. トラブルシュート

17.1 /v1/models が返らない

確認:

docker ps --filter name=sglang-qwen3-coder
docker logs --tail=200 sglang-qwen3-coder

よくある原因:

- モデルダウンロード中
- Hugging Face token が必要
- GPU がコンテナから見えていない
- 起動時 OOM
- port 競合
- SGLang server 起動完了前に curl している

port 競合確認:

ss -ltnp | grep 30000 || true

17.2 GPU が見えない

nvidia-smi

Docker 内から GPU が見えない場合は、NVIDIA Container Toolkit の設定を確認してください。

17.3 初回起動が遅い

初回はモデルダウンロードが走るため時間がかかります。

du -sh ./hf-cache
docker logs -f sglang-qwen3-coder

2 回目以降は ./hf-cache が使われるため短縮されます。

17.4 短文は通るが長文で OOM する

.env を保守的にします。

MEM_FRACTION_STATIC=0.78
CHUNKED_PREFILL_SIZE=4096
MAX_RUNNING_REQUESTS=1
MAX_QUEUED_REQUESTS=4

反映:

docker compose down
docker compose up -d

17.5 OpenCode で tool calling が不安定

確認ポイント:

- --tool-call-parser qwen3_coder が指定されているか
- OpenCode 側の model 名が SERVED_MODEL_NAME と一致しているか
- Base URL が /v1 まで含まれているか
- KV_CACHE_DTYPE=fp8_e5m2 にしていないか
- MAX_NEW_TOKENS が小さすぎないか

初期検証では以下を推奨します。

KV_CACHE_DTYPE=auto
MAX_NEW_TOKENS=8192

18. 推奨初期値まとめ

初期運用では以下を推奨します。

CONTEXT_LENGTH=131072
MAX_NEW_TOKENS=8192
MEM_FRACTION_STATIC=0.82
CHUNKED_PREFILL_SIZE=4096
MAX_RUNNING_REQUESTS=1
MAX_QUEUED_REQUESTS=8
KV_CACHE_DTYPE=auto
MAMBA_SCHEDULER_STRATEGY=no_buffer
MAMBA_SSM_DTYPE=bfloat16

この構成で以下を確認します。

確認順:
1. docker compose up -d で起動する
2. /v1/models が返る
3. 短文生成が通る
4. 32K 相当の長文入力が通る
5. 64K 相当の長文入力が通る
6. 128K 級の長文入力が通る
7. OpenCode から実タスクを実行できる
8. tool calling が安定している
9. 長時間運用で OOM しない

19. 最小実行サマリ

mkdir -p ~/docker/sglang-qwen3-coder/hf-cache ~/docker/sglang-qwen3-coder/logs
cd ~/docker/sglang-qwen3-coder

# .env と docker-compose.yml を配置後:
docker compose up -d
docker logs -f sglang-qwen3-coder

疎通確認:

curl -s http://localhost:30000/v1/models | jq .

短文生成確認:

curl -i http://localhost:30000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen3-coder-next-fp8",
    "messages": [
      {
        "role": "user",
        "content": "Say OK."
      }
    ],
    "max_tokens": 16,
    "temperature": 0.2
  }'

OpenCode 設定:

Base URL: http://localhost:30000/v1
Model   : qwen3-coder-next-fp8
API Key : EMPTY

20. ベンチマーク結果

構築完了してから、ベンチマークを回しましたが、256K context でも問題ありません でした。

Context Prompt Input (Prefill) Output (Decode) Total Time
4k 4,019 3371.94 t/s 45.57 t/s 12.43s
8k 8,219 3581.30 t/s 44.64 t/s 13.76s
16k 16,319 4340.55 t/s 42.76 t/s 15.73s
32k 32,519 4590.51 t/s 39.85 t/s 19.93s
64k 65,219 4111.42 t/s 34.90 t/s 30.53s
128k 130,319 3135.64 t/s 27.93 t/s 59.89s
256k 235,619 2261.13 t/s 21.24 t/s 128.31s

最後は、ベンチマークで利用したソースコードです。

cat << 'EOF' > benchmark.py && python3 benchmark.py
import requests
import time
import json

url = "http://localhost:30000/v1/chat/completions"
# .envのMODEL_NAMEで定義された識別用エイリアスに合わせます
model_name = "qwen3-coder-next-fp8"

# ==============================================================================
# コンテキスト長ごとの multiplier 設定(4K〜256Kまで自動走破します)
# ==============================================================================
CONTEXT_CONFIGS = {
    "4k":   130,
    "8k":   270,
    "16k":  540,
    "32k":  1080,
    "64k":  2170,
    "128k": 4340,
    "256k": 7850
}

needle = "SECRET_PASSWORD = 'DGX_SPARK_PERFORMANCE_TEST'"
base_sentence = "The sky is blue, the grass is green. Machine learning is fascinating.\n"

print("==================================================")
print(" DGX Spark: Context Needle-in-a-Haystack Benchmark")
print("==================================================")

results = []

for label, multiplier in CONTEXT_CONFIGS.items():
    print(f"\n[{label} テスト] multiplier={multiplier} でコンテキストを構築中...")
    
    haystack_part = base_sentence * multiplier
    full_context = f"{haystack_part}\n[SYSTEM NOTE: {needle}]\n{haystack_part}"
    prompt_instruction = (
        "\n[タスク]\n"
        "1. テキスト内に隠されている SECRET_PASSWORD は何ですか? 最初に正確なパスワードを答えてください。\n"
        "2. 続けて、現代のLLMフレームワークにおいて、このような大規模コンテキストを"
        "処理することの技術的なメリットと、それを支えるアーキテクチャについて、詳細な分析を日本語で「最低3段落」記述してください。"
    )
    
    payload = {
        "model": model_name,
        "messages": [{"role": "user", "content": full_context + prompt_instruction}],
        "max_tokens": 512,
        "temperature": 0.0,
        "stream": True,
        # SGLang等のバックエンドから確実なusageを取得するためのオプションを追加します
        "stream_options": {"include_usage": True}
    }
    
    print(f"リクエスト送信中(Prefillを実行しています)...")
    
    start_time = time.perf_counter()
    first_token_time = None
    output_text = ""
    prompt_tokens = 0
    completion_tokens = 0
    
    try:
        response = requests.post(url, json=payload, timeout=900, stream=True)
        
        if response.status_code != 200:
            print(f"サーバーエラーが発生しました (HTTP {response.status_code})")
            print(f"エラー詳細: {response.text}")
            continue
            
        for line in response.iter_lines():
            if not line:
                continue
            
            line_str = line.decode("utf-8").strip()
            if line_str.startswith("data: "):
                data_str = line_str[6:].strip()
                if data_str == "[DONE]":
                    continue
                
                try:
                    chunk = json.loads(data_str)
                except json.JSONDecodeError:
                    continue
                
                # 最初の有効なトークンが返ってきた時点を記録(Prefill終了 / Decode開始)
                if first_token_time is None and "choices" in chunk and len(chunk["choices"]) > 0:
                    delta = chunk["choices"][0].get("delta", {})
                    # 不具合修正:SGLangの仕様により最初の数枚のチャンクで content が空文字("")または存在しない状態で
                    # レポされるケースがあります。元の構造(delta.get("content"))を活かすために、
                    # 最初のシグナル("choices"構造のパース成功時点)で空であっても強制的に初期化フラグを立てます。
                    if "content" in delta and not delta.get("content"):
                        delta["content"] = "__INITIAL_STREAM_START__"
                        
                    if delta.get("content"):
                        first_token_time = time.perf_counter()
                        print("-> 最初のトークンを受信。生成(Decode)を開始します。")
                        if delta["content"] == "__INITIAL_STREAM_START__":
                            delta["content"] = "" # 計測用ダミーをクリアして元の処理に戻す
                
                if "usage" in chunk and chunk["usage"]:
                    prompt_tokens = chunk["usage"].get("prompt_tokens", prompt_tokens)
                    completion_tokens = chunk["usage"].get("completion_tokens", completion_tokens)
                
                if "choices" in chunk and len(chunk["choices"]) > 0:
                    delta = chunk["choices"][0].get("delta", {})
                    content = delta.get("content", "")
                    if content:
                        output_text += content
                        # usageが途中で取れないフォールバック用にカウントします
                        if not chunk.get("usage"):
                            completion_tokens += 1
                            
        end_time = time.perf_counter()
        
        # 万が一usageが取得できなかった場合の概算フォールバックです
        if prompt_tokens == 0:
            prompt_tokens = (multiplier * 2 * 15) + 200 
            
        if first_token_time is None:
            first_token_time = end_time
            
        prefill_duration = first_token_time - start_time
        decode_duration = end_time - first_token_time
        total_time = end_time - start_time
        
        input_throughput = prompt_tokens / prefill_duration if prefill_duration > 0 else 0
        output_throughput = completion_tokens / decode_duration if decode_duration > 0 else 0
        
        print(f"\n================ {label} 結果 ================")
        print(f" 入力トークン数 (Prompt)     : {prompt_tokens:,} tokens")
        print(f" 出力トークン数 (Completion) : {completion_tokens:,} tokens")
        print("--------------------------------------------------")
        print(f" インプット(Prefill)速度   : {input_throughput:.2f} tokens/s (時間: {prefill_duration:.2f}秒)")
        print(f" アウトプット(Decode)速度   : {output_throughput:.2f} tokens/s (時間: {decode_duration:.2f}秒)")
        print(f" 総経過時間                  : {total_time:.2f} 秒")
        print("==================================================\n")
        
        results.append({
            "label": label,
            "prompt_tokens": prompt_tokens,
            "completion_tokens": completion_tokens,
            "input_speed": input_throughput,
            "output_speed": output_throughput,
            "total_time": total_time
        })
        
        time.sleep(1)
        
    except Exception as e:
        print(f"スクリプト実行エラーが発生しました: {e}")

# ==============================================================================
# 最終集計レポートの出力
# ==============================================================================
print("\n" + "======================================================================")
print(" 最終集計結果レポート")
print("======================================================================")
print(f"{'Context':<8} | {'Prompt':<10} | {'Input (Prefill)':<18} | {'Output (Decode)':<18} | {'Total Time':<10}")
print("-" * 75)
for r in results:
    print(f"{r['label']:<8} | {r['prompt_tokens']:>10,} | {r['input_speed']:>13.2f} t/s | {r['output_speed']:>13.2f} t/s | {r['total_time']:>9.2f}s")
print("======================================================================")
EOF

Discussion