# Running SGLang Multi-Node Cluster on DGX Spark + Jetson Thor ## Cluster Layout | Machine | Hostname | IP | GPU | Role | |---------|----------|-----|-----|------| | DGX Spark 1 | spark-ea8e | 10.0.0.25 | NVIDIA GB10 (sm_120, 128GB) | HEAD | | DGX Spark 2 | spark-4080 | 10.0.0.22 | NVIDIA GB10 (sm_120, 128GB) | Worker | | Jetson Thor 1 | jetson-thor | 10.0.0.27 | NVIDIA Thor (sm_110, 128GB) | Worker | | Jetson Thor 2 | thorx | 10.0.0.26 | NVIDIA Thor (sm_110, 128GB) | Worker | **Total: 4 GPUs, ~461 GiB unified memory** --- ## Prerequisites ### Create the Virtual Environment Each machine needs the SGLang virtualenv at `~/Projects/sglang/.sglang/`: ```bash cd ~/Projects/sglang uv venv .sglang --python 3.12 source .sglang/bin/activate ``` ### Set Architecture & CUDA Paths Choose the correct `TORCH_CUDA_ARCH_LIST` for your hardware: **Jetson Thor:** ```bash export TORCH_CUDA_ARCH_LIST="11.0a" ``` **DGX Spark:** ```bash export TORCH_CUDA_ARCH_LIST="12.1a" ``` **Common (all machines):** ```bash export CUDA_HOME=/usr/local/cuda-13 export TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas export PATH="${CUDA_HOME}/bin:$PATH" ``` ### Install Dependencies ```bash uv pip install sglang uv pip install --force-reinstall torch==2.9.1 torchvision==0.24.1 torchaudio==2.9.1 --index-url https://download.pytorch.org/whl/cu130 uv pip install --force-reinstall sgl-kernel --index-url https://docs.sglang.ai/whl/cu130/ ``` ### Install Ray (required for multi-node) ```bash uv pip install -U "ray[all]" ``` ### Verify the Installation ```bash python -c "import sglang; print(f'SGLang version: {sglang.__version__}')" python -c "import torch; print(f'CUDA available: {torch.cuda.is_available()}, arch: {torch.cuda.get_device_capability()}')" ``` --- ## Step 1: Start the Ray Cluster Use `run_cluster_bare.sh` on each machine. The script auto-detects the network interface and sets NCCL/GLOO env vars. ### Spark 1 (HEAD): ```bash bash run_cluster_bare.sh 10.0.0.25 --head ``` ### Spark 2 (Worker): ```bash bash run_cluster_bare.sh 10.0.0.25 --worker --node-ip 10.0.0.22 ``` ### Thor 1 (Worker): ```bash bash run_cluster_bare.sh 10.0.0.25 --worker --node-ip 10.0.0.27 ``` ### Thor 2 (Worker): ```bash bash run_cluster_bare.sh 10.0.0.25 --worker --node-ip 10.0.0.26 ``` ### Verify the cluster: ```bash source .sglang/bin/activate ray status ``` You should see 4 active nodes, 4 GPUs, ~461 GiB memory. --- ## Step 2: Serve a Model Open another terminal on the HEAD (Spark 1) and set the environment: ```bash source .sglang/bin/activate export NCCL_SOCKET_IFNAME=^lo,docker,tailscale,veth export GLOO_SOCKET_IFNAME=enp1s0f0np0 export NCCL_IB_DISABLE=1 export NCCL_P2P_DISABLE=1 export NCCL_SHM_DISABLE=1 export NCCL_PROTO=Simple ``` ### NCCL Environment Variables Explained | Variable | Value | Why | |----------|-------|-----| | `NCCL_SOCKET_IFNAME` | `^lo,docker,tailscale,veth` | Exclude loopback/docker/vpn interfaces; NCCL auto-selects the right NIC on each machine | | `NCCL_IB_DISABLE=1` | Disable InfiniBand | Sparks have RoCE, Thors don't — forces all nodes to use TCP Socket | | `NCCL_P2P_DISABLE=1` | Disable peer-to-peer | No NVLink between nodes | | `NCCL_SHM_DISABLE=1` | Disable shared memory | Forces TCP for all communication | | `NCCL_PROTO=Simple` | Simple protocol | Most compatible across heterogeneous nodes | | `GLOO_SOCKET_IFNAME` | `enp1s0f0np0` | For PyTorch Gloo backend (used during init) | --- ## Example: Nemotron Super 120B NVFP4 (4 nodes) ```bash python -m sglang.launch_server \ --model-path nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \ --served-model-name nvidia/nemotron-3-super \ --dtype auto \ --kv-cache-dtype fp8 \ --tp 1 \ --pp 4 \ --nnodes 4 \ --use-ray \ --trust-remote-code \ --mem-fraction-static 0.85 \ --chunked-prefill-size 8192 \ --max-running-requests 512 \ --host 0.0.0.0 \ --port 5000 \ --disable-cuda-graph ``` --- ## Example: Qwen3.5-122B-A10B-FP8 (4 nodes) ```bash python -m sglang.launch_server \ --model-path Qwen/Qwen3.5-122B-A10B-FP8 \ --served-model-name qwen3.5-122b \ --tp 1 \ --pp 4 \ --nnodes 4 \ --use-ray \ --trust-remote-code \ --mem-fraction-static 0.85 \ --context-length 32768 \ --max-running-requests 256 \ --host 0.0.0.0 \ --port 5000 \ --disable-cuda-graph ``` --- ## Example: Nemotron Super NVFP4 (2 Sparks only) If Thor nodes aren't available, use only the 2 Sparks: ```bash python -m sglang.launch_server \ --model-path nvidia/NVIDIA-Nemotron-3-Super-120B-A12B-NVFP4 \ --served-model-name nvidia/nemotron-3-super \ --dtype auto \ --kv-cache-dtype fp8 \ --tp 1 \ --pp 2 \ --nnodes 2 \ --use-ray \ --trust-remote-code \ --mem-fraction-static 0.85 \ --chunked-prefill-size 8192 \ --max-running-requests 512 \ --host 0.0.0.0 \ --port 5000 \ --disable-cuda-graph ``` --- ## Step 3: Test the API SGLang exposes an OpenAI-compatible API. Once the server is ready, test from any machine: ```bash curl http://10.0.0.25:5000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "nvidia/nemotron-3-super", "messages": [{"role": "user", "content": "Hello! Write a haiku about GPUs."}], "max_tokens": 200, "temperature": 1.0, "top_p": 0.95 }' ``` --- ## Key Flags Reference | Flag | Purpose | |------|---------| | `--model-path` | HuggingFace model ID or local path | | `--pp N` | Split model across N nodes (pipeline parallelism) | | `--tp 1` | No tensor parallelism (1 GPU per node) | | `--nnodes N` | Number of nodes in the cluster | | `--use-ray` | Use Ray actors for multi-node process management ([PR #17684](https://github.com/sgl-project/sglang/pull/17684)) | | `--disable-cuda-graph` | Disable CUDA graphs (avoids Triton ptxas sm_110a errors and OOM from graph capture) | | `--mem-fraction-static 0.85` | Use 85% of GPU memory for static allocation (leave room for OS) | | `--kv-cache-dtype fp8` | FP8 KV cache (saves memory, NVFP4 models only) | | `--chunked-prefill-size N` | Enable chunked prefill with chunk size N tokens | | `--max-running-requests N` | Maximum concurrent requests | | `--context-length N` | Override model's max context length | | `--trust-remote-code` | Allow running model code from HuggingFace | ## Troubleshooting ### NCCL "wrong type 3 != 4" **Cause**: Sparks use RoCE/IB, Thors use Socket — different NCCL transports. **Fix**: `export NCCL_IB_DISABLE=1` forces all nodes to Socket. ### Gloo "Connection refused 127.0.0.1" **Cause**: Gloo defaults to localhost instead of the real IP. **Fix**: `export GLOO_SOCKET_IFNAME=enp1s0f0np0` (set to your NIC name). ### "no kernel image is available for execution on the device" (Thor only) **Cause**: Pre-built wheels don't include `sm_110` CUDA kernels. **Fix**: Reinstall `sgl-kernel` from the cu130 index with `TORCH_CUDA_ARCH_LIST="11.0a"` set before install. ### "ptxas fatal: Value 'sm_110a' is not defined" (Thor only) **Cause**: Triton's bundled ptxas doesn't support sm_110a. **Fix**: Set `export TRITON_PTXAS_PATH=/usr/local/cuda/bin/ptxas` and use `--disable-cuda-graph`. ### OOM during CUDA graph capture **Cause**: CUDA graph capture consumes extra GPU memory. **Fix**: Use `--disable-cuda-graph` to skip graph capture entirely. ### "Free memory less than desired" **Cause**: Other processes using GPU memory. **Fix**: Lower `--mem-fraction-static` (e.g., 0.80 or 0.75). ### NCCL "Network is unreachable" on fe80:: addresses **Cause**: NCCL tries IPv6 link-local interfaces that aren't connected. **Fix**: Use `NCCL_SOCKET_IFNAME=enp1s0f0np0,enP2p1s0f0np0` to whitelist only working interfaces. ### Ray actor placement fails **Cause**: Not enough GPUs visible in the Ray cluster. **Fix**: Verify `ray status` shows all expected nodes and GPUs before launching. --- ## Architecture Notes - **Pipeline Parallelism (PP)**: Splits model layers across nodes. Each node processes a subset of layers. Low inter-node communication — ideal for Ethernet. - **Tensor Parallelism (TP)**: Splits each layer across GPUs. High inter-node communication — requires NVLink. NOT suitable for this cluster over Ethernet. - SGLang uses ZMQ for inter-process data transfer. With `--use-ray`, Ray manages process lifecycle (control plane) while ZMQ handles the data plane — zero throughput overhead. - The `--use-ray` flag is opt-in ([PR #17684](https://github.com/sgl-project/sglang/pull/17684)). Without it, SGLang defaults to Python multiprocessing. - DGX Spark (GB10) = compute capability 12.1, `sm_120` - Jetson Thor = compute capability 11.0, `sm_110` / `sm_110a` Extra code ```bash #!/bin/bash # # Launch a Ray cluster (without Docker) for SGLang multi-node inference. # # Uses the uv virtual environment at .sglang/ relative to this script. # All machines must have the same environment replicated at the same path, # and must be reachable at the supplied IP addresses (port 6379 open). # # Cluster layout (4 nodes): # Spark 1 (HEAD) – IP_SPARK_1 # Spark 2 (worker) – IP_SPARK_2 # Thor 1 (worker) – IP_THOR_1 # Thor 2 (worker) – IP_THOR_2 # # Usage – run each command on the corresponding machine: # # 1. Spark 1 (HEAD): # bash run_cluster_bare.sh IP_SPARK_1 --head # ^^^^^^^^^ # su propia IP # # 2. Spark 2 (worker): # bash run_cluster_bare.sh IP_SPARK_1 --worker --node-ip IP_SPARK_2 # ^^^^^^^^^ ^^^^^^^^^^ # IP del HEAD su propia IP # # 3. Thor 1 (worker): # bash run_cluster_bare.sh IP_SPARK_1 --worker --node-ip IP_THOR_1 # ^^^^^^^^^ ^^^^^^^^^ # IP del HEAD su propia IP # # 4. Thor 2 (worker): # bash run_cluster_bare.sh IP_SPARK_1 --worker --node-ip IP_THOR_2 # ^^^^^^^^^ ^^^^^^^^^ # IP del HEAD su propia IP # # 5. Once all workers have joined, open another terminal on Spark 1 (HEAD): # source .sglang/bin/activate # python -m sglang.launch_server --model-path <model> --tp 1 --pp <N> --nnodes <N> --use-ray # # Keep each terminal session open. Ctrl-C stops the Ray node. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" VENV_DIR="${SCRIPT_DIR}/.sglang" if [ ! -f "${VENV_DIR}/bin/activate" ]; then echo "Error: virtual environment not found at ${VENV_DIR}" echo "Create it with: uv venv .sglang --python 3.12 && uv pip install sglang ray" exit 1 fi # shellcheck disable=SC1091 source "${VENV_DIR}/bin/activate" echo "Activated virtualenv: ${VIRTUAL_ENV}" if [ $# -lt 2 ]; then echo "Usage: $0 <head_node_ip> --head|--worker [--node-ip <this_node_ip>]" exit 1 fi HEAD_NODE_ADDRESS="$1" NODE_TYPE="$2" shift 2 NODE_IP="" while [[ $# -gt 0 ]]; do case "$1" in --node-ip) NODE_IP="$2" shift 2 ;; *) echo "Unknown argument: $1" exit 1 ;; esac done if [ "${NODE_TYPE}" != "--head" ] && [ "${NODE_TYPE}" != "--worker" ]; then echo "Error: Node type must be --head or --worker" exit 1 fi # Auto-detect the network interface for the given IP so that # NCCL and Gloo use the correct NIC instead of defaulting to loopback. detect_interface() { local target_ip="$1" ip -4 addr show | awk -v ip="${target_ip}" '$0 ~ "inet " ip "/" {print $NF; exit}' } MY_IP="" if [ "${NODE_TYPE}" == "--head" ]; then MY_IP="${HEAD_NODE_ADDRESS}" else MY_IP="${NODE_IP:-}" fi if [ -n "${MY_IP}" ]; then IFNAME=$(detect_interface "${MY_IP}") if [ -n "${IFNAME}" ]; then export NCCL_SOCKET_IFNAME="${IFNAME}" export GLOO_SOCKET_IFNAME="${IFNAME}" export TP_SOCKET_IFNAME="${IFNAME}" echo "Detected interface ${IFNAME} for IP ${MY_IP}" else echo "Warning: could not detect interface for ${MY_IP}, NCCL/Gloo may fail" fi fi cleanup() { echo "Stopping Ray node..." ray stop } trap cleanup EXIT if [ "${NODE_TYPE}" == "--head" ]; then echo "Starting Ray HEAD node on ${HEAD_NODE_ADDRESS}:6379 ..." ray start --block --head \ --node-ip-address="${HEAD_NODE_ADDRESS}" \ --port=6379 \ --dashboard-host=0.0.0.0 else WORKER_IP="${NODE_IP:-}" echo "Starting Ray WORKER node, connecting to head at ${HEAD_NODE_ADDRESS}:6379 ..." RAY_ARGS=(--block --address="${HEAD_NODE_ADDRESS}:6379") if [ -n "${WORKER_IP}" ]; then RAY_ARGS+=(--node-ip-address="${WORKER_IP}") fi ray start "${RAY_ARGS[@]}" fi ```