A4 VM에서 멀티 호스트 vLLM을 사용하여 GKE에서 DeepSeek-R1 671B 서빙

이 튜토리얼에서는Google Cloud의 Google Kubernetes Engine(GKE)에서 370억 개의 활성 파라미터가 있는 6,710억 개의 파라미터 전문가 혼합 모델인 DeepSeek-R1(deepseek-ai/DeepSeek-R1)을 배포하고 서빙하는 방법을 보여줍니다. 이 배포에서는 Cluster Toolkit, 총 16개의 NVIDIA B200 GPU가 있는 멀티 호스트 A4 (a4-highgpu-8g) 가상 머신 (VM) 인스턴스 2개, vLLM 서빙 프레임워크를 사용합니다.

이 튜토리얼에 설명된 배포 구성에서는 Cluster Toolkit을 사용하여 일반 모드에서 GKE 클러스터를 프로비저닝합니다. 클러스터에는 GPUDirect 원격 직접 메모리 액세스 (RDMA) 네트워킹을 통해 연결된 a4-highgpu-8g 노드 2개가 있습니다. 그런 다음 Cloud Storage에서 모델 가중치를 스테이징하고 Kubernetes JobSet API와 Ray를 사용하여 텐서 동시 로드, 전문가 동시 로드, FP8 KV 캐시를 통해 16개의 모든 NVIDIA B200 GPU에서 모델을 서빙합니다.

목표

  • Cluster Toolkit을 설치하고 일반 모드에서 멀티 호스트 A4 GKE 클러스터를 프로비저닝합니다.
  • Cloud Storage 버킷에 DeepSeek-R1 모델 가중치를 스테이징합니다.
  • 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. Hugging Face에 오신 것을 환영합니다 페이지에서 계정 아바타를 클릭하고 액세스 토큰을 선택합니다.
  2. 액세스 토큰 페이지에서 새 토큰 만들기를 클릭합니다.
  3. 읽기 토큰 유형을 선택하고 토큰 이름을 입력합니다.
  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)가 30자(영문 기준) Identity and Access Management(IAM) 제한 내에 있도록 20자(영문 기준) 이하의 이름을 사용합니다.
    • YOUR_RESERVATION_NAME: A4 예약의 이름 또는 확장된 예약 블록 경로(RESERVATION_NAME/reservationBlocks/RESERVATION_BLOCK_NAME 형식)입니다.
    • YOUR_HF_TOKEN: Hugging Face 액세스 토큰입니다.
    • YOUR_GCS_BUCKET: 모델 가중치를 스테이징하는 Cloud Storage 버킷의 전역 고유 이름입니다. 또한 이 스크립트는 Cluster Toolkit의 Terraform 상태를 저장하기 위해 TERRAFORM_BUCKET를 YOUR_GCS_BUCKET-tf-state로 설정합니다.

    또한 스크립트는 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을 다운로드하고 일반 모드에서 a4-highgpu-8g 노드 2개, GPUDirect RDMA 네트워킹, JobSet 컨트롤러를 사용하여 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. a4-highgpu-8g 노드 2개에 대한 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를 수정하여 다음 사용 옵션 중 하나를 사용할 수 있습니다.

    • 스팟 용량: 프로젝트에 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 노드 중 하나의 로컬 SSD에 다운로드하고 노드 간 빠른 스트리밍을 위해 Cloud Storage 버킷으로 전송합니다.

  1. 모델 다운로드 작업, vLLM 추론 서버, 벤치마크 포드의 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. 기본 Kubernetes 서비스 계정, 기본 Compute Engine 서비스 계정, 클러스터 노드 풀 서비스 계정 (${CLUSTER_NAME}-gke-np-sa)에 버킷에 대한 IAM 권한을 부여합니다.

    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 보안 비밀을 만듭니다.

    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. 다운로드 포드가 시작될 때까지 기다린 후 작업 로그를 스트리밍합니다.

    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)는 두 개의 a4-highgpu-8g 노드에서 멀티 호스트 서빙을 조정하는 ClusterIP 서비스(vllm-r1-leader)와 Kubernetes JobSet (vllm-r1)를 정의합니다.

  • 리더 (leader): 8개의 NVIDIA B200 GPU가 있는 A4 노드에서 복제본 하나를 실행하고, 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가 있는 두 번째 A4 노드에서 헤드리스 복제본 하나를 실행하고, 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. deepseek_r1_server.yaml을 Cloud Storage 버킷 이름으로 대체하고 매니페스트를 클러스터에 적용합니다.

    envsubst '$MODEL_BUCKET' < deepseek_r1_server.yaml | kubectl apply -f -
  3. 서버 시작이 완료될 때까지 리더 및 워커 포드 로그를 모니터링합니다.

    # 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는 포그라운드에서 로그를 스트리밍하므로 두 포드를 동시에 모니터링하려면 별도의 터미널 세션에서 워커 로그 명령어를 실행합니다. 언제든지 로그 스트리밍을 중지하려면 Control+C를 누릅니다.

    초기 가중치 스트리밍, 모델 로드, CUDA 그래프 캡처에는 약 15~20분이 걸립니다. 요청을 보내기 전에 리더 포드 로그에 다음 메시지가 출력될 때까지 기다립니다.

    (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 서비스로 전달합니다.

    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 포드가 시작될 때까지 기다린 후 벤치마크 로그를 스트리밍합니다.

    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. 벤치마크 포드, vLLM 서버 JobSet 및 서비스, 모델 다운로드 작업, Hugging Face 보안 비밀을 삭제합니다.

    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}"

다음 단계