A4 VM でマルチホスト vLLM を使用して GKE で DeepSeek-R1 671B を提供する

このチュートリアルでは、Google Cloudの Google Kubernetes Engine(GKE)に DeepSeek-R1(deepseek-ai/DeepSeek-R1)をデプロイして提供する方法について説明します。DeepSeek-R1 は、370 億個のアクティブ パラメータを持つ 6,710 億個のパラメータの Mixture-of-Experts モデルです。このデプロイでは、Cluster Toolkit、合計 16 個の NVIDIA B200 GPU を搭載した 2 つのマルチホスト A4(a4-highgpu-8g)仮想マシン(VM)インスタンス、vLLM サービング フレームワークを使用します。

このチュートリアルで説明するデプロイ構成では、Cluster Toolkit を使用して標準モードで GKE クラスタをプロビジョニングします。クラスタには、GPUDirect リモート ダイレクト メモリ アクセス(RDMA)ネットワーキングを介して接続された 2 つの a4-highgpu-8g ノードがあります。次に、モデルの重みを Cloud Storage にステージングし、Kubernetes JobSet API と Ray を使用して、テンソル並列処理、エキスパート並列処理、FP8 KV キャッシュを使用して 16 個の NVIDIA B200 GPU すべてでモデルを提供します。

目標

  • Cluster Toolkit をインストールし、標準モードでマルチホスト A4 GKE クラスタをプロビジョニングします。
  • DeepSeek-R1 モデルの重みを Cloud Storage バケットにステージングします。
  • JobSet と Ray を使用して、2 つの A4 ノードに合計 16 個の NVIDIA B200 GPU を搭載したマルチホスト vLLM 推論サーバーをデプロイします。
  • チャット完了リクエストを vLLM サーバーに送信します。
  • サービング ベンチマーク ワークロードを実行します。
  • デプロイしたリソースをクリーンアップします。

費用

このドキュメントでは、課金対象である次のGoogle Cloudコンポーネントを使用します。

料金計算ツールを使うと、予想使用量に基づいて費用の見積もりを生成できます。

始める前に

チュートリアルの環境を設定します。

  1. Google Cloud CLI をインストールして初期化し、kubectl をインストールします。Cloud Shell を使用する場合、どちらのツールもプリインストールされています。
  2. Cloud Shell ではなく Linux または macOS ワークステーションからこのチュートリアルを実行する場合は、必要な Cluster Toolkit の依存関係(Terraform や Git など)をインストールします。
  3. Google Cloud プロジェクトで Kubernetes Engine API、Compute Engine API、Cloud Storage API、Artifact Registry API、Service Usage API を有効にします。

    API の有効化

Hugging Face アクセス トークンを取得する

このチュートリアルを使用するには、Hugging Face アクセス トークンが必要です。Hugging Face で無料アカウントに登録できます。アカウントを作成したら、アクセス トークンを生成します。

  1. [Welcome to Hugging Face] ページで、アカウントのアバターをクリックし、[アクセス トークン] を選択します。
  2. [アクセス トークン] ページで、[Create new token] をクリックします。
  3. [Read] トークン タイプを選択し、トークンの名前を入力します。
  4. アクセス トークンが表示されます。トークンを安全な場所に保存します。

必要なロール

このチュートリアルを完了するために必要な権限を取得するには、プロジェクトに対する次の IAM ロールを付与するよう管理者に依頼してください。

ロールの付与については、プロジェクト、フォルダ、組織へのアクセスを管理するをご覧ください。

必要な権限は、カスタムロールや他の事前定義ロールから取得することもできます。

環境変数を設定する

チュートリアルの認証と環境変数を設定します。

  1. 認証を設定します。

    gcloud auth login
    gcloud auth application-default login
  2. このチュートリアル全体で使用する環境変数をエクスポートします。

    export PROJECT_ID="YOUR_PROJECT_ID"
    export REGION="YOUR_REGION"
    export ZONE="YOUR_ZONE"
    export CLUSTER_NAME="YOUR_CLUSTER_NAME"
    export RESERVATION_NAME="YOUR_RESERVATION_NAME"
    export HF_TOKEN="YOUR_HF_TOKEN"
    export MODEL_BUCKET="YOUR_GCS_BUCKET"
    export TERRAFORM_BUCKET="${MODEL_BUCKET}-tf-state"
    export CLUSTER_TOOLKIT_VERSION="v1.104.0"
    
    gcloud config set project "${PROJECT_ID}"
    gcloud config set billing/quota_project "${PROJECT_ID}"

    次のように置き換えます。

    • YOUR_PROJECT_ID: 実際のGoogle Cloud プロジェクト ID。
    • YOUR_REGION: A4 容量が配置され、クラスタをデプロイするコンピューティング リージョン(us-central1 など)。
    • YOUR_ZONE: A4 容量が配置されているコンピューティング ゾーン(us-central1-b など)。
    • YOUR_CLUSTER_NAME: GKE クラスタと Cluster Toolkit デプロイの名前。生成されたノードプール サービス アカウント ID(YOUR_CLUSTER_NAME-gke-np-sa)が Identity and Access Management(IAM)の 30 文字の制限内に収まるように、20 文字以下の名前を使用します。
    • YOUR_RESERVATION_NAME: A4 予約の名前、または拡張予約ブロックのパス(RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME 形式)。
    • YOUR_HF_TOKEN: Hugging Face アクセス トークン。
    • YOUR_GCS_BUCKET: モデルの重みをステージングする Cloud Storage バケットのグローバルに一意の名前。このスクリプトは、TERRAFORM_BUCKET を YOUR_GCS_BUCKET-tf-state に設定して、Cluster Toolkit の Terraform 状態を保存します。

    このスクリプトは、CLUSTER_TOOLKIT_VERSION を v1.104.0 に設定します。これにより、インストールする Cluster Toolkit のバージョンが指定されます。Cluster Toolkit バージョン v1.104.0 以降には、examples/gke-a4/gke-a4-deployment.yaml の容量消費オプションが組み込まれています。v1.104.0 より前のバージョンでは、これらの容量オプションを手動で追加する必要があります。

GKE クラスタを作成する

Cluster Toolkit をダウンロードし、2 つの a4-highgpu-8g ノード、GPUDirect RDMA ネットワーキング、JobSet コントローラを使用して、Standard モードで GKE クラスタをプロビジョニングします。

  1. CLUSTER_TOOLKIT_VERSION(v1.104.0)で指定されたバージョンのビルド済み Cluster Toolkit バンドルをダウンロードして展開し、作業ディレクトリを cluster-toolkit ディレクトリに変更して、バイナリ バージョンを確認します。

    case "$(uname -s)-$(uname -m)" in
      Linux-x86_64)              BUNDLE="gcluster_bundle_linux_amd64.tgz" ;;
      Linux-aarch64|Linux-arm64) BUNDLE="gcluster_bundle_linux_arm64.tgz" ;;
      Darwin-x86_64)             BUNDLE="gcluster_bundle_mac_amd64.tgz" ;;
      Darwin-arm64)              BUNDLE="gcluster_bundle_mac_arm64.tgz" ;;
      *) BUNDLE=""; echo "Unsupported platform: $(uname -s)-$(uname -m)" >&2 ;;
    esac
    mkdir -p cluster-toolkit
    curl -fsSL "https://github.com/GoogleCloudPlatform/cluster-toolkit/releases/download/${CLUSTER_TOOLKIT_VERSION}/${BUNDLE:?}" \
      | tar -xz -C cluster-toolkit
    cd cluster-toolkit
    ./gcluster --version
  2. 必要に応じて、Cluster Toolkit で使用可能なコマンドとフラグを表示します。

    ./gcluster --help
  3. Cluster Toolkit デプロイの Terraform 状態を保存する Cloud Storage バケットを作成します。

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. 2 つの a4-highgpu-8g ノード用の examples/gke-a4/gke-a4-deployment.yaml デプロイ構成ファイルを作成します。

    cat <<EOF > examples/gke-a4/gke-a4-deployment.yaml
    terraform_backend_defaults:
      type: gcs
      configuration:
        bucket: ${TERRAFORM_BUCKET}
    
    vars:
      project_id: ${PROJECT_ID}
      deployment_name: ${CLUSTER_NAME}
      region: ${REGION}
      zone: ${ZONE}
      static_node_count: 2
      authorized_cidr: 0.0.0.0/0
      reservation_affinity:
        consume_reservation_type: SPECIFIC_RESERVATION
        specific_reservations:
        - name: ${RESERVATION_NAME}
    EOF

    デフォルトでは、examples/gke-a4/gke-a4.yaml ブループリントは特定の予約を使用するように構成されています。特定の予約がない場合は、クラスタをデプロイする前に examples/gke-a4/gke-a4.yaml を編集して、次のいずれかの使用オプションを使用できます。

    • Spot 容量: プロジェクトに GPU、ディスク、CPU のプリエンプティブル割り当てが十分にあることを確認します。examples/gke-a4/gke-a4.yaml の下部で、--- Option 1: Specific Reservation --- セクションをコメントアウトし、--- Option 4: Spot --- セクションのコメント化を解除します。
    • Flex Start VM: プロジェクトに GPU、ディスク、CPU の十分なプリエンプティブル割り当てがあることを確認します。examples/gke-a4/gke-a4.yaml の下部で、--- Option 1: Specific Reservation --- セクションをコメントアウトし、--- Option 2: DWS Flex Start --- セクションのコメント化を解除します。
  5. gcluster deploy コマンドを使用して GKE クラスタをデプロイします。

    ./gcluster deploy -d examples/gke-a4/gke-a4-deployment.yaml examples/gke-a4/gke-a4.yaml \
        --download-dependencies \
        --auto-approve -w

    クラスタの作成には約 30 ~ 45 分かかります。--auto-approve フラグを指定せずに ./gcluster deploy を実行する場合は、提案された変更を適用するために、プロンプトが表示されたら a と入力します。デプロイが完了すると、出力は次のようなメッセージで終了します。

    Apply complete! Resources: 80 added, 0 changed, 0 destroyed.
    
  6. クラスタと通信するように kubectl 認証情報を構成します。

    gcloud container clusters get-credentials "${CLUSTER_NAME}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}"

Cloud Storage にモデルの重みをステージングする

DeepSeek-R1 の重みを A4 ノードの 1 つのローカル SSD にダウンロードし、ノード間で高速ストリーミングできるように Cloud Storage バケットに転送します。

  1. モデルのダウンロード Job、vLLM 推論サーバー、ベンチマーク Pod の Kubernetes マニフェスト ファイルをダウンロードします。

    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/gpu/inference/deepseek-r1-gcluster/download_model.yaml
    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/gpu/inference/deepseek-r1-gcluster/deepseek_r1_server.yaml
    curl -LO https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/main/gpu/inference/deepseek-r1-gcluster/deepseek_r1_8k_1k.yaml
  2. モデルの重みを保存するために、均一なバケットレベルのアクセス権を持つ Cloud Storage バケットを作成します。

    gcloud storage buckets create "gs://${MODEL_BUCKET}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  3. バケットに対する IAM 権限を、デフォルトの Kubernetes サービス アカウント、デフォルトの Compute Engine サービス アカウント、クラスタ ノードプール サービス アカウント(${CLUSTER_NAME}-gke-np-sa)に付与します。

    PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)")
    
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="principal://iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${PROJECT_ID}.svc.id.goog/subject/ns/default/sa/default" \
        --role="roles/storage.objectAdmin"
    
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
        --role="roles/storage.objectAdmin"
    
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="serviceAccount:${CLUSTER_NAME}-gke-np-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
        --role="roles/storage.objectAdmin"
    
    # legacyBucketReader is required to load the model with --load-format runai_streamer
    gcloud storage buckets add-iam-policy-binding "gs://${MODEL_BUCKET}" \
        --member="serviceAccount:${CLUSTER_NAME}-gke-np-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
        --role="roles/storage.legacyBucketReader"

    上記のスクリプトによって付与される IAM 権限には、--load-format runai_streamer に必要なストレージ レガシー バケット読み取りロール(roles/storage.legacyBucketReader)が含まれています。

  4. Hugging Face アクセス トークンを保存する hf-secret という名前の Kubernetes Secret を作成します。

    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token="${HF_TOKEN}" \
        --dry-run=client -o yaml | kubectl apply -f -
  5. download_model.yaml マニフェストを確認します。以前にダウンロードしていない場合は、ローカルにファイルを作成します。

    apiVersion: batch/v1
    kind: Job
    metadata:
      name: download-deepseek-r1
      namespace: default
    spec:
      backoffLimit: 1
      template:
        spec:
          restartPolicy: OnFailure
          # Schedule onto an A4 node with Local SSD
          nodeSelector:
            cloud.google.com/gke-accelerator: nvidia-b200
          tolerations:
          - operator: Exists
          containers:
          - name: model-downloader
            image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            # Enable high-performance concurrent chunked downloads in huggingface_hub
            - name: HF_XET_HIGH_PERFORMANCE
              value: "1"
            # Place Hugging Face cache on Local SSD so the boot disk never fills up
            - name: HF_HOME
              value: "/local-ssd/hf-cache"
            command: ["/bin/bash", "-c"]
            args:
            - |
              set -euo pipefail
    
              echo "=== 1. Installing huggingface_hub ==="
              pip3 install --break-system-packages -q -U "huggingface_hub"
    
              echo "=== 2. Downloading deepseek-ai/DeepSeek-R1 to Local NVMe SSD ==="
              hf download deepseek-ai/DeepSeek-R1 --local-dir /local-ssd/DeepSeek-R1
              rm -rf /local-ssd/DeepSeek-R1/.cache
    
              echo "=== 3. Uploading from Local SSD to GCS ==="
              gcloud config set storage/parallel_composite_upload_compatibility_check False
              gcloud storage cp -r /local-ssd/DeepSeek-R1 gs://$MODEL_BUCKET/
    
              echo "=== 4. Cleaning up Local SSD ==="
              rm -rf /local-ssd/DeepSeek-R1 /local-ssd/hf-cache
            resources:
              requests:
                cpu: "64"
                memory: "128Gi"
            volumeMounts:
            - name: local-ssd
              mountPath: /local-ssd
          volumes:
          - name: local-ssd
            emptyDir: {}
  6. Cloud Storage バケット名を download_model.yaml マニフェストに置き換え、モデルのダウンロード ジョブを開始します。

    envsubst '$MODEL_BUCKET' < download_model.yaml | kubectl apply -f -
  7. ダウンロード Pod が起動するまで待ってから、Job ログをストリーミングします。

    kubectl wait --for=condition=Ready pod -l job-name=download-deepseek-r1 --timeout=120s
    kubectl logs -f job/download-deepseek-r1

    モデルをノードのローカル NVMe SSD にダウンロードし、重みを Cloud Storage バケットにアップロードするまでに約 30 ~ 40 分かかります。

  8. ジョブが完了したら、163 個の .safetensors シャードがすべて Cloud Storage バケットに存在することを確認します。

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. 完了したステージング ジョブを削除します。

    kubectl delete job download-deepseek-r1

マルチホスト vLLM サーバーをデプロイする

ワークロード マニフェスト(deepseek_r1_server.yaml)は、2 つの a4-highgpu-8g ノード間でマルチホスト サービングを調整する ClusterIP Service(vllm-r1-leader)と Kubernetes JobSet(vllm-r1)を定義します。

  • リーダー(leader): 8 個の NVIDIA B200 GPU を搭載した A4 ノードで 1 つのレプリカを実行し、NCCL/gIB 環境(/usr/local/gib/scripts/set_nccl_env.sh)をソースとして、2 ノード クラスタのポート 6379 で Ray ヘッドノードを起動し、HTTP ポート 8080 で vllm serve を起動します。サーバーは、--load-format runai_streamer を使用して gs://$MODEL_BUCKET/DeepSeek-R1 から重みを直接ストリーミングし、/models/DeepSeek-R1 でマウントされた Cloud Storage FUSE ボリュームからトークナイザーを読み込みます。--tensor-parallel-size 16、--pipeline-parallel-size 1、--distributed-executor-backend ray、--enable-expert-parallel、--kv-cache-dtype fp8_e4m3、--async-scheduling、CUDA グラフ(FULL_AND_PIECEWISE)で実行されます。
  • ワーカー(worker): 8 個の NVIDIA B200 GPU を搭載した 2 番目の A4 ノードで 1 つのヘッドレス レプリカを実行し、NCCL/gIB を構成して、vllm-r1-leader:6379 でリーダーの Ray クラスタに参加します。

マルチホスト vLLM サーバーをデプロイしてモニタリングします。

  1. deepseek_r1_server.yaml マニフェストを確認します。以前にダウンロードしていない場合は、ローカルにファイルを作成します。

    apiVersion: v1
    kind: Service
    metadata:
      name: vllm-r1-leader
      namespace: default
      labels:
        app: vllm-r1
    spec:
      ports:
      - name: http
        port: 8080
        targetPort: 8080
      - name: ray-head
        port: 6379
        targetPort: 6379
      selector:
        app: vllm-r1
        role: leader
      type: ClusterIP
    ---
    apiVersion: jobset.x-k8s.io/v1alpha2
    kind: JobSet
    metadata:
      name: vllm-r1
      namespace: default
      labels:
        app: vllm-r1
    spec:
      failurePolicy:
        maxRestarts: 3
        restartStrategy: Recreate
      network:
        enableDNSHostnames: true
        publishNotReadyAddresses: true
      replicatedJobs:
      # -------------------- LEADER --------------------
      - name: leader
        replicas: 1
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 1
            template:
              metadata:
                labels:
                  app: vllm-r1
                  role: leader
                annotations:
                  gke-gcsfuse/volumes: "true"
              spec:
                hostNetwork: true
                dnsPolicy: ClusterFirstWithHostNet
                hostIPC: true
                nodeSelector:
                  cloud.google.com/gke-accelerator: nvidia-b200
                tolerations:
                - key: nvidia.com/gpu
                  operator: Exists
                - key: cloud.google.com/impending-node-termination
                  operator: Exists
                - effect: NoSchedule
                  key: cloud.google.com/gke-queued
                  value: "true"
                containers:
                - name: vllm-leader
                  image: docker.io/vllm/vllm-openai@sha256:61fc8a896b0a4fbbbdc063bc4b0dbc25ce98e02b5050c24aeb7830ac02039b14
                  imagePullPolicy: IfNotPresent
                  securityContext:
                    privileged: true
                  env:
                  - name: PYTHONUNBUFFERED
                    value: "1"
                  - name: NCCL_DEBUG
                    value: "INFO"
                  - name: VLLM_ENGINE_READY_TIMEOUT_S
                    value: "1800"
                  - name: VLLM_RPC_TIMEOUT
                    value: "1800000"
                  - name: HF_TOKEN
                    valueFrom:
                      secretKeyRef:
                        name: hf-secret
                        key: hf_api_token
                  command: ["/bin/bash", "-c"]
                  args:
                  - |
                    set -e
                    echo "precedence ::ffff:0:0/96  100" >> /etc/gai.conf 2>/dev/null || true
                    ulimit -l unlimited || true
                    export LD_LIBRARY_PATH=/usr/local/gib/lib64:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH:-}
                    source /usr/local/gib/scripts/set_nccl_env.sh
                    export GLOO_SOCKET_IFNAME=eth0
                    export NCCL_SOCKET_IFNAME=eth0
                    export VLLM_HOST_IP=$(hostname -I | awk '{print $1}')
                    echo "=== Starting Optimized vLLM Server with Ray (Leader, rank 0) ==="
                    echo "Host IP: ${VLLM_HOST_IP}"
                    # 0. Install Ray with Compiled Graph support (not pre-installed in vLLM 0.28.0 image)
                    uv pip install --system "ray[cgraph]"
                    # 1. Start Ray head on port 6379 and wait until all 2 nodes (16 GPUs) join
                    bash /vllm-workspace/examples/ray_serving/multi-node-serving.sh leader \
                      --ray_cluster_size=2 \
                      --ray_port=6379 \
                      --node-ip-address="${VLLM_HOST_IP}"
                    # 2. Launch vLLM on the Leader using the Ray cluster
                    exec vllm serve \
                      --host 0.0.0.0 \
                      --port 8080 \
                      --model gs://$MODEL_BUCKET/DeepSeek-R1 \
                      --tokenizer /models/DeepSeek-R1 \
                      --load-format runai_streamer \
                      --model-loader-extra-config '{"distributed": true, "concurrency": 16, "memory_limit": 34359738368}' \
                      --served-model-name deepseek-ai/DeepSeek-R1 \
                      --tensor-parallel-size 16 \
                      --pipeline-parallel-size 1 \
                      --distributed-executor-backend ray \
                      --async-scheduling \
                      --disable-custom-all-reduce \
                      --kv-cache-dtype fp8_e4m3 \
                      --block-size 128 \
                      --enable-expert-parallel \
                      --max-num-batched-tokens 8192 \
                      --max-num-seqs 160 \
                      --compilation-config '{"mode": 3, "cudagraph_mode":"FULL_AND_PIECEWISE", "custom_ops":["none"], "pass_config":{"fuse_allreduce_rms":false}}' \
                      --trust-remote-code \
                      --max-model-len 16384 \
                      --gpu-memory-utilization 0.90
                  resources:
                    requests:
                      nvidia.com/gpu: "8"
                    limits:
                      nvidia.com/gpu: "8"
                  ports:
                  - containerPort: 8080
                    name: http
                  - containerPort: 6379
                    name: ray-head
                  volumeMounts:
                  - mountPath: /dev/shm
                    name: dshm
                  - mountPath: /models
                    name: model-volume
                    readOnly: true
                  - mountPath: /usr/local/gib
                    name: gib
                  - mountPath: /usr/local/nvidia/lib64
                    name: nvidia-lib64
                volumes:
                - name: dshm
                  emptyDir:
                    medium: Memory
                    sizeLimit: 64Gi
                - name: model-volume
                  csi:
                    driver: gcsfuse.csi.storage.gke.io
                    volumeAttributes:
                      bucketName: $MODEL_BUCKET
                      mountOptions: implicit-dirs
                - name: gib
                  hostPath:
                    path: /home/kubernetes/bin/gib
                - name: nvidia-lib64
                  hostPath:
                    path: /home/kubernetes/bin/nvidia/lib64
      # -------------------- WORKER --------------------
      - name: worker
        replicas: 1
        template:
          spec:
            parallelism: 1
            completions: 1
            backoffLimit: 1
            template:
              metadata:
                labels:
                  app: vllm-r1
                  role: worker
                annotations:
                  gke-gcsfuse/volumes: "true"
              spec:
                hostNetwork: true
                dnsPolicy: ClusterFirstWithHostNet
                hostIPC: true
                nodeSelector:
                  cloud.google.com/gke-accelerator: nvidia-b200
                tolerations:
                - key: nvidia.com/gpu
                  operator: Exists
                - key: cloud.google.com/impending-node-termination
                  operator: Exists
                - effect: NoSchedule
                  key: cloud.google.com/gke-queued
                  value: "true"
                containers:
                - name: vllm-worker
                  image: docker.io/vllm/vllm-openai@sha256:61fc8a896b0a4fbbbdc063bc4b0dbc25ce98e02b5050c24aeb7830ac02039b14
                  imagePullPolicy: IfNotPresent
                  securityContext:
                    privileged: true
                  env:
                  - name: PYTHONUNBUFFERED
                    value: "1"
                  - name: NCCL_DEBUG
                    value: "INFO"
                  - name: VLLM_ENGINE_READY_TIMEOUT_S
                    value: "1800"
                  - name: VLLM_RPC_TIMEOUT
                    value: "1800000"
                  - name: HF_TOKEN
                    valueFrom:
                      secretKeyRef:
                        name: hf-secret
                        key: hf_api_token
                  command: ["/bin/bash", "-c"]
                  args:
                  - |
                    set -e
                    echo "precedence ::ffff:0:0/96  100" >> /etc/gai.conf 2>/dev/null || true
                    ulimit -l unlimited || true
                    export LD_LIBRARY_PATH=/usr/local/gib/lib64:/usr/local/nvidia/lib64:${LD_LIBRARY_PATH:-}
                    source /usr/local/gib/scripts/set_nccl_env.sh
                    export GLOO_SOCKET_IFNAME=eth0
                    export NCCL_SOCKET_IFNAME=eth0
                    export VLLM_HOST_IP=$(hostname -I | awk '{print $1}')
                    echo "=== Starting Optimized vLLM Ray Worker (rank 1) ==="
                    echo "Host IP: ${VLLM_HOST_IP}"
                    # 0. Install Ray with Compiled Graph support (not pre-installed in vLLM 0.28.0 image)
                    uv pip install --system "ray[cgraph]"
                    # Join the Leader's Ray cluster and block so the container stays alive
                    exec bash /vllm-workspace/examples/ray_serving/multi-node-serving.sh worker \
                      --ray_address=vllm-r1-leader \
                      --ray_port=6379 \
                      --node-ip-address="${VLLM_HOST_IP}"
                  resources:
                    requests:
                      nvidia.com/gpu: "8"
                    limits:
                      nvidia.com/gpu: "8"
                  ports:
                  - containerPort: 8080
                    name: http
                  volumeMounts:
                  - mountPath: /dev/shm
                    name: dshm
                  - mountPath: /models
                    name: model-volume
                    readOnly: true
                  - mountPath: /usr/local/gib
                    name: gib
                  - mountPath: /usr/local/nvidia/lib64
                    name: nvidia-lib64
                volumes:
                - name: dshm
                  emptyDir:
                    medium: Memory
                    sizeLimit: 64Gi
                - name: model-volume
                  csi:
                    driver: gcsfuse.csi.storage.gke.io
                    volumeAttributes:
                      bucketName: $MODEL_BUCKET
                      mountOptions: implicit-dirs
                - name: gib
                  hostPath:
                    path: /home/kubernetes/bin/gib
                - name: nvidia-lib64
                  hostPath:
                    path: /home/kubernetes/bin/nvidia/lib64

    このデプロイをより多くのホストにスケーリングするには、deepseek_r1_server.yaml のパラメータを更新します。たとえば、合計 32 個の NVIDIA B200 GPU を搭載した 4 つの A4 ノードにスケールするには、worker レプリケートされたジョブで replicas: 3 を設定し、リーダー引数を次のように更新します。

    • --ray_cluster_size=4
    • --tensor-parallel-size=16
    • --pipeline-parallel-size=2
  2. Cloud Storage バケット名を deepseek_r1_server.yaml に置き換え、マニフェストをクラスタに適用します。

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. サーバーの起動が完了するまで、リーダー Pod とワーカー Pod のログをモニタリングします。

    # monitor leader
    kubectl logs -l jobset.sigs.k8s.io/replicatedjob-name=leader -c vllm-leader -f --tail=-1
    # monitor worker
    kubectl logs -l jobset.sigs.k8s.io/replicatedjob-name=worker -c vllm-worker -f --tail=-1

    kubectl logs -f はフォアグラウンドでログをストリーミングするため、両方の Pod を同時にモニタリングする場合は、別のターミナル セッションでワーカーログ コマンドを実行します。ログのストリーミングをいつでも停止するには、Ctrl+C キーを押します。

    最初の重みストリーミング、モデルの読み込み、CUDA グラフのキャプチャには、約 15 ~ 20 分かかります。リクエストを送信する前に、リーダー Pod のログに次のメッセージが出力されるまで待ちます。

    (APIServer pid=1) INFO:     Started server process [1]
    (APIServer pid=1) INFO:     Waiting for application startup.
    (APIServer pid=1) INFO:     Application startup complete.
    

推論リクエストを送信する

デプロイされた vLLM サーバーにチャット完了リクエストを送信します。

  1. ローカルポート 8080 をバックグラウンドで vllm-r1-leader Service に転送します。

    kubectl port-forward service/vllm-r1-leader 8080:8080 &
  2. /v1/chat/completions エンドポイントにチャット完了リクエストを送信します。

    curl http://localhost:8080/v1/chat/completions \
        -H "Content-Type: application/json" \
        -d '{
            "model": "deepseek-ai/DeepSeek-R1",
            "messages": [
                {"role": "user", "content": "Explain tensor parallelism in one sentence."}
            ],
            "max_tokens": 100,
            "temperature": 0.6
        }'

ベンチマーク ワークロードを実行する

8,192 個の入力トークンと 1,024 個の出力トークン(8k/1k)の構成で vllm bench serve を使用して、デプロイされた DeepSeek-R1 サーバーのベンチマークを行います。

  1. deepseek_r1_8k_1k.yaml マニフェストを確認します。以前にダウンロードしていない場合は、ローカルにファイルを作成します。

    apiVersion: v1
    kind: Pod
    metadata:
      name: vllm-bench-8k-1k
      namespace: default
      labels:
        app: vllm-bench
      annotations:
        gke-gcsfuse/volumes: "true"
    spec:
      restartPolicy: Never
      tolerations:
      - operator: Exists
      containers:
      - name: benchmark
        image: docker.io/vllm/vllm-openai@sha256:61fc8a896b0a4fbbbdc063bc4b0dbc25ce98e02b5050c24aeb7830ac02039b14
        imagePullPolicy: IfNotPresent
        env:
        - name: PYTHONUNBUFFERED
          value: "1"
        - name: HF_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-secret
              key: hf_api_token
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-secret
              key: hf_api_token
        command: ["/bin/bash", "-c"]
        args:
        - |
          set -e
          echo "=== Benchmark: Input 8k (8192) / Output 1k (1024) [1280 Prompts, Concurrency 112] ==="
          vllm bench serve \
            --backend openai \
            --base-url http://vllm-r1-leader:8080 \
            --model deepseek-ai/DeepSeek-R1 \
            --tokenizer /models/DeepSeek-R1 \
            --dataset-name random \
            --random-input-len 8192 \
            --random-output-len 1024 \
            --random-range-ratio 0.0 \
            --num-prompts 1280 \
            --max-concurrency 112 \
            --request-rate inf \
            --ready-check-timeout-sec 0 \
            --trust-remote-code
        resources:
          requests:
            cpu: "8"
            memory: 16Gi
        volumeMounts:
        - mountPath: /models
          name: model-volume
          readOnly: true
      volumes:
      - name: model-volume
        csi:
          driver: gcsfuse.csi.storage.gke.io
          volumeAttributes:
            bucketName: $MODEL_BUCKET
            mountOptions: implicit-dirs

    マニフェストでは、8,192 個の入力トークン、1,024 個の出力トークン、1,280 個のプロンプト、112 個の最大同時実行数が指定されています。

  2. deepseek_r1_8k_1k.yaml マニフェストを適用します。

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl apply -f -
  3. vllm-bench-8k-1k Pod が起動するまで待ってから、ベンチマーク ログをストリーミングします。

    kubectl wait --for=condition=Ready pod/vllm-bench-8k-1k --timeout=120s
    kubectl logs -f vllm-bench-8k-1k -c benchmark

    ベンチマークが完了するまでに約 15 分かかります。ベンチマークが完了したら、次のような指標を含む出力を探します。

    • Output token throughput
    • Total token throughput
    • TTFT
    • TPOT

クリーンアップ

このチュートリアルで使用したリソースについて、 Google Cloud アカウントに継続的な追加料金が発生しないようにするには、Kubernetes ワークロード、GKE クラスタ、Cloud Storage バケットを削除します。

  1. ベンチマーク Pod、vLLM サーバーの JobSet と Service、モデルのダウンロード Job、Hugging Face Secret を削除します。

    envsubst '$MODEL_BUCKET' < deepseek_r1_8k_1k.yaml | kubectl delete -f - --ignore-not-found || true
    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl delete -f - --ignore-not-found || true
    kubectl delete job download-deepseek-r1 --ignore-not-found || true
    kubectl wait --for=delete pod -l app=vllm-r1 --timeout=120s || true
    kubectl delete secret hf-secret --ignore-not-found || true
  2. ステージングされたモデルの重みを保存する Cloud Storage バケットを削除します。

    gcloud storage rm --recursive "gs://${MODEL_BUCKET}"
  3. cluster-toolkit ディレクトリから、gcluster destroy コマンドを使用して、GKE クラスタとその関連インフラストラクチャを破棄します。

    ./gcluster destroy "${CLUSTER_NAME}" --robust --auto-approve
  4. Terraform の状態を保存する Cloud Storage バケットを削除します。

    gcloud storage rm --recursive "gs://${TERRAFORM_BUCKET}"

次のステップ