在 GKE 上使用 A4 虚拟机上的多主机 vLLM 部署 DeepSeek-R1 671B

本教程介绍了如何在Google Cloud上的 Google Kubernetes Engine (GKE) 上部署和应用 DeepSeek-R1 (deepseek-ai/DeepSeek-R1),这是一个拥有 6,710 亿个参数的混合专家模型,其中包含 370 亿个活跃参数。该部署使用 Cluster Toolkit、两个多主机 A4 (a4-highgpu-8g) 虚拟机 (VM) 实例(总共 16 个 NVIDIA B200 GPU)以及 vLLM 部署框架。

在本教程中介绍的部署配置中,您将使用 Cluster Toolkit 在标准模式下预配 GKE 集群。该集群有两个通过 GPUDirect 远程直接内存访问 (RDMA) 网络连接的 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 在两个 A4 节点上部署多主机 vLLM 推理服务器,总共使用 16 个 NVIDIA B200 GPU。
  • 向 vLLM 服务器发送对话补全请求。
  • 运行服务基准工作负载。
  • 清理已部署的资源。

费用

在本文档中,您将使用Google Cloud的以下收费组件:

如需根据您的预计使用量来估算费用,请使用价格计算器。

准备工作

为本教程设置环境:

  1. 安装并初始化 Google Cloud CLI,然后安装 kubectl。如果您使用 Cloud Shell,则这两个工具均已预安装。
  2. 如果您是在 Linux 或 macOS 工作站(而非 Cloud Shell)中运行本教程,请安装所需的 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 部署的名称。请使用不超过 20 个字符的名称,以便生成的节点池服务账号 ID (YOUR_CLUSTER_NAME-gke-np-sa) 保持在 30 个字符的 Identity and Access Management (IAM) 限制范围内。
    • 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,并在 Standard 模式下预配一个 GKE 集群,该集群具有两个 a4-highgpu-8g 节点、GPUDirect RDMA 网络和 JobSet 控制器:

  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. 创建 Cloud Storage 存储桶,用于存储 Cluster Toolkit 部署的 Terraform 状态:

    gcloud storage buckets create "gs://${TERRAFORM_BUCKET}" \
        --default-storage-class=STANDARD \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --uniform-bucket-level-access
  4. 为两个 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 --- 部分。
    • 灵活启动型虚拟机:确保您的项目有足够的抢占式配额来满足 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 推理服务器和基准 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. 向默认 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 权限包括 Storage Legacy Bucket Reader 角色 (roles/storage.legacyBucketReader),这是 --load-format runai_streamer 所必需的。

  4. 创建一个名为 hf-secret 的 Kubernetes Secret,用于存储您的 Hugging Face 访问令牌:

    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 启动,然后流式传输作业日志:

    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. 作业完成后,验证 Cloud Storage 存储桶中是否存在所有 163 个 .safetensors 分片:

    gcloud storage ls "gs://${MODEL_BUCKET}/DeepSeek-R1/*.safetensors" | wc -l
  9. 删除已完成的暂存作业:

    kubectl delete job download-deepseek-r1

部署多主机 vLLM 服务器

工作负载清单 (deepseek_r1_server.yaml) 定义了一个 ClusterIP Service (vllm-r1-leader) 和一个 Kubernetes JobSet (vllm-r1),用于协调两个 a4-highgpu-8g 节点上的多主机服务:

  • Leader (leader):在一个具有 8 个 NVIDIA B200 GPU 的 A4 节点上运行一个副本,获取 NCCL/gIB 环境 (/usr/local/gib/scripts/set_nccl_env.sh),在端口 6379 上启动 Ray 头节点(针对双节点集群),并在 HTTP 端口 8080 上启动 vllm serve。服务器使用 --load-format runai_streamer 直接从 gs://$MODEL_BUCKET/DeepSeek-R1 流式传输权重,并从装载的 Cloud Storage FUSE 卷(位于 /models/DeepSeek-R1)加载分词器。--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):在第二个 A4 节点上运行一个无头副本,该节点配备 8 个 NVIDIA B200 GPU,配置 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 中的参数。例如,如需扩缩到 4 个 A4 节点(总共 32 个 NVIDIA B200 GPU),请在 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. 监控 Leader Pod 和 Worker 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,请在单独的终端会话中运行工作器日志命令。如需随时停止流式传输日志,请按 Control+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
        }'

运行基准工作负载

使用 vllm bench serve 对已部署的 DeepSeek-R1 服务器进行基准测试,配置为 8,192 个输入词元和 1,024 个输出词元 (8k/1k):

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

后续步骤