A4 VM で Dynamo と SGLang を使用して、マルチホスト PD 分離により GKE で Qwen3.8-2.4T を提供する

このチュートリアルでは、Google Cloudの Google Kubernetes Engine(GKE)に Qwen3.8-2.4T(nvidia/Qwen3.8-2.4T-A95B-NVFP4)をデプロイして提供する方法について説明します。これは、NVFP4 に量子化された 950 億のアクティブ パラメータを持つ 2.4 兆パラメータの Mixture-of-Experts モデルです。このデプロイでは、Cluster Toolkit、合計 32 個の NVIDIA B200 GPU を搭載した 4 つのマルチホスト A4(a4-highgpu-8g)仮想マシン(VM)インスタンス、NVIDIA Dynamo、プリフィル デコード(PD)分離を備えた SGLang サービング フレームワークを使用します。

このチュートリアルで説明するデプロイ構成では、Cluster Toolkit を使用して標準モードで GKE クラスタをプロビジョニングします。クラスタには、GPUDirect リモート ダイレクト メモリ アクセス(RDMA)ネットワーキングを介して接続された 4 つの a4-highgpu-8g ノードが含まれています。次に、Grove ギャング スケジューリングを使用して KAI Scheduler と NVIDIA Dynamo プラットフォームをインストールし、モデルの重みを Cloud Storage にステージングします。次に、Cloud Storage FUSE を使用してバケットをマウントし、テンソル並列処理、エキスパート並列処理、FP8 KV キャッシュを使用して、2 つの 16 GPU マルチホスト ワーカープールにプリフィルとデコードを分離する DynamoGraphDeployment カスタム リソースをデプロイします。

目標

  • Cluster Toolkit をインストールし、標準モードでマルチホスト A4 GKE クラスタをプロビジョニングします。
  • クラスタに KAI Scheduler と NVIDIA Dynamo プラットフォームを Grove ギャング スケジューリングとともにインストールします。
  • Qwen3.8-2.4T モデルの重みを Cloud Storage バケットにステージングします。
  • 合計 32 個の NVIDIA B200 GPU を搭載した 4 つの A4 ノードに、マルチホストの PD 分離 SGLang 推論サーバーをデプロイします。
  • Dynamo フロントエンド ルーターにチャット完了リクエストを送信します。
  • サービング ベンチマーク ワークロードを実行します。
  • デプロイしたリソースをクリーンアップします。

費用

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

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

始める前に

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

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

    API の有効化

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

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

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

必要なロール

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

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

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

環境変数を設定する

このチュートリアル全体で使用する環境変数をエクスポートします。

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 GCS_BUCKET="YOUR_BUCKET_NAME"
export HF_TOKEN="YOUR_HF_TOKEN"
export CLUSTER_TOOLKIT_VERSION="v1.104.0"
export DYNAMO_RELEASE_VERSION="1.5.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_BUCKET_NAME: Cluster Toolkit の Terraform 状態とステージングされた Qwen3.8-2.4T モデルの重みを保存する Cloud Storage バケットのグローバルに一意の名前。
  • YOUR_HF_TOKEN: Hugging Face アクセス トークン。

GKE クラスタを作成する

Cluster Toolkit をダウンロードし、4 つの a4-highgpu-8g ノード、GPUDirect RDMA ネットワーキング、DRA サポートを備えた標準モードの GKE クラスタをプロビジョニングします。

  1. Google Cloud CLI にログインし、Terraform のアプリケーションのデフォルト認証情報を生成します。

    gcloud auth login
    gcloud auth application-default login
  2. CLUSTER_TOOLKIT_VERSION(v1.104.0)で指定されたバージョンのビルド済み Cluster Toolkit バンドルをダウンロードして展開します。

    TAG="${CLUSTER_TOOLKIT_VERSION:-v1.104.0}"
    OS="linux"
    ARCH="amd64"
    mkdir -p cluster-toolkit && curl -L "https://github.com/GoogleCloudPlatform/cluster-toolkit/releases/download/${TAG}/gcluster_bundle_${OS}_${ARCH}.tgz" | tar -xz -C cluster-toolkit

    macOS または arm64 ワークステーションでこのコマンドを実行する場合は、プラットフォームに合わせて OS="mac" または ARCH="arm64" を設定します。

  3. Cluster Toolkit のデプロイとステージングされたモデルの重みの Terraform 状態を保存するには、均一なバケットレベルのアクセスとオブジェクト バージョニングを有効にして、A4 予約と同じリージョンに Cloud Storage バケットを作成します。

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

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

    デフォルトでは、デプロイ ファイルは特定の予約を使用するように構成されています。特定の予約がない場合は、クラスタをデプロイする前に、cluster-toolkit/examples/gke-a4/gke-a4-deployment.yaml を更新して、Spot VM や Flex Start VM などの代替の使用オプションを使用できます。構成手順については、GKE の使用オプションをご覧ください。

  5. cluster-toolkit ディレクトリに移動し、gcluster deploy コマンドを使用して GKE クラスタをデプロイしてから、作業ディレクトリに戻ります。

    cd cluster-toolkit
    ./gcluster deploy \
        -d examples/gke-a4/gke-a4-deployment.yaml \
        examples/gke-a4/gke-a4.yaml \
        --auto-approve -w
    cd ..

    クラスタの作成には約 20 ~ 30 分かかります。--auto-approve フラグを指定せずに ./gcluster deploy を実行する場合は、提案された変更を適用するために、プロンプトが表示されたら「a」と入力します。

  6. クラスタと通信するように kubectl 認証情報を構成します。

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

KAI Scheduler と NVIDIA Dynamo をインストールする

Hugging Face Secret を作成し、Grove ギャング スケジューリングを使用して KAI Scheduler と NVIDIA Dynamo プラットフォームをインストールして、クラスタを準備します。

  1. 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 -
  2. NVIDIA Dynamo プラットフォームの Helm チャート(v1.5.0)をダウンロードし、KAI Scheduler をインストールして、Grove を有効にした Dynamo プラットフォームをインストールします。

    RELEASE_VERSION="${DYNAMO_RELEASE_VERSION:-1.5.0}"
    helm pull "https://helm.ngc.nvidia.com/nvidia/ai-dynamo/charts/dynamo-platform-${RELEASE_VERSION}.tgz"
    tar -zxf "dynamo-platform-${RELEASE_VERSION}.tgz"
    
    helm upgrade --install kai-scheduler ./dynamo-platform/charts/kai-scheduler \
        --set scheduler.args.default-staleness-grace-period="-1s" \
        --set topologyMigration.enabled=false
    
    kubectl wait --for=condition=Available deployment/kai-operator --timeout=120s
    until kubectl get deployment/queue-controller >/dev/null 2>&1; do sleep 2; done
    kubectl wait --for=condition=Available deployment/queue-controller --timeout=120s
    
    helm upgrade --install dynamo-platform ./dynamo-platform \
        --set global.grove.install=true \
        --set global.grove.enabled=true \
        --set global.kai-scheduler.enabled=true

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

推論ワーカー Pod がコールド スタートのたびに Hugging Face からダウンロードするのではなく、Cloud Storage FUSE を使用して重みをマウントできるように、リージョン内の Cloud Storage バケット(gs://${GCS_BUCKET})に nvidia/Qwen3.8-2.4T-A95B-NVFP4 チェックポイントをステージングします。

  1. GKE クラスタを作成するで作成した gs://${GCS_BUCKET} バケットに対する Storage オブジェクト ユーザーロール(roles/storage.objectUser)を、workload-identity-k8s-sa Kubernetes サービス アカウントにバインドされている Google サービス アカウントに付与します。

    GSA=$(kubectl get sa workload-identity-k8s-sa -o jsonpath='{.metadata.annotations.iam\.gke\.io/gcp-service-account}')
    gcloud storage buckets add-iam-policy-binding "gs://${GCS_BUCKET}" \
        --member="serviceAccount:${GSA}" \
        --role="roles/storage.objectUser"
  2. qwen3.8-2.4t-nvfp4-stage-weights-job.yaml マニフェストと qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml マニフェストをダウンロードします。

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/gpu/inference/qwen38-2t/qwen3.8-2.4t-nvfp4-stage-weights-job.yaml
    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/gpu/inference/qwen38-2t/qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml

    または、次の内容で qwen3.8-2.4t-nvfp4-stage-weights-job.yaml という名前のファイルを作成します。このインデックス付き Job は、4 つの A4 ノードで 4 つの Pod を並列に実行します。各 Pod は、nvidia/Qwen3.8-2.4T-A95B-NVFP4 チェックポイントのサイズがバランスされたスライスをノードのローカル NVMe SSD にダウンロードし、gcloud storage rsync を使用して gs://${GCS_BUCKET}/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4 にアップロードします。

    # ==============================================================================
    # Stage nvidia/Qwen3.8-2.4T-A95B-NVFP4 (~1.44 TB, 210 files) from Hugging Face Hub
    # into an in-region Cloud Storage bucket for qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml
    #
    # Each Indexed Job pod downloads a size-balanced slice of the repo onto the node's
    # local NVMe SSD, then uploads that slice with `gcloud storage rsync`.
    # ==============================================================================
    apiVersion: batch/v1
    kind: Job
    metadata:
      name: qwen3-8-2-4t-stage-weights
    spec:
      completionMode: Indexed
      completions: 4
      parallelism: 4
      backoffLimitPerIndex: 2
      template:
        metadata:
          labels:
            app: qwen3-8-2-4t-stage-weights
        spec:
          restartPolicy: Never
          serviceAccountName: workload-identity-k8s-sa
          nodeSelector:
            cloud.google.com/gke-accelerator: nvidia-b200
          tolerations:
            - key: "nvidia.com/gpu"
              operator: "Exists"
              effect: "NoSchedule"
          topologySpreadConstraints:
            - maxSkew: 1
              topologyKey: kubernetes.io/hostname
              whenUnsatisfiable: DoNotSchedule
              labelSelector:
                matchLabels:
                  app: qwen3-8-2-4t-stage-weights
          initContainers:
            - name: download
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              command: ["python3", "-c"]
              args:
                - |
                  import os, time
                  from huggingface_hub import HfApi, snapshot_download
                  repo = os.environ["REPO_ID"]
                  n = int(os.environ["NUM_SHARDS"])
                  i = int(os.environ["JOB_COMPLETION_INDEX"])
                  info = HfApi().model_info(repo, files_metadata=True)
                  files = sorted(((s.size or 0, s.rfilename) for s in info.siblings), reverse=True)
                  bins = [[0, []] for _ in range(n)]
                  for size, name in files:
                      b = min(bins, key=lambda x: x[0])
                      b[0] += size
                      b[1].append(name)
                  size, mine = bins[i]
                  print(f"shard {i}/{n}: {len(mine)} files, {size / 1e9:.1f} GB", flush=True)
                  for attempt in range(1, 7):
                      try:
                          snapshot_download(repo, local_dir="/ssd/model", allow_patterns=mine, max_workers=16)
                          break
                      except Exception as e:
                          print(f"shard {i}/{n}: attempt {attempt} failed: {e!r}", flush=True)
                          if attempt == 6:
                              raise
                          time.sleep(30)
                  print(f"shard {i}/{n}: download complete", flush=True)
              env:
                - name: REPO_ID
                  value: "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
                - name: NUM_SHARDS
                  value: "4"
                - name: HF_HOME
                  value: "/ssd/hf"
                - name: HF_HUB_DISABLE_XET
                  value: "1"
                - name: HF_TOKEN
                  valueFrom:
                    secretKeyRef:
                      name: hf-secret
                      key: hf_api_token
                      optional: true
              resources:
                requests:
                  cpu: "16"
                  memory: "64Gi"
                  ephemeral-storage: "450Gi"
                limits:
                  cpu: "16"
                  memory: "64Gi"
                  ephemeral-storage: "700Gi"
              volumeMounts:
                - name: ssd
                  mountPath: /ssd
          containers:
            - name: upload
              image: gcr.io/google.com/cloudsdktool/google-cloud-cli:slim
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -euo pipefail
                  gcloud storage rsync --recursive --exclude='^\.cache/.*' /ssd/model "${DEST}"
                  echo "shard ${JOB_COMPLETION_INDEX}: upload complete"
              env:
                - name: DEST
                  value: "gs://${GCS_BUCKET}/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
              resources:
                requests:
                  cpu: "16"
                  memory: "32Gi"
                limits:
                  cpu: "16"
                  memory: "32Gi"
              volumeMounts:
                - name: ssd
                  mountPath: /ssd
          volumes:
            - name: ssd
              emptyDir: {}
  3. Cloud Storage バケット名を qwen3.8-2.4t-nvfp4-stage-weights-job.yaml に置き換え、ステージング ジョブを開始します。4 つの Pod すべてが重みのアップロードを完了するまで待ってから、完了したジョブを削除します。

    envsubst '${GCS_BUCKET}' < qwen3.8-2.4t-nvfp4-stage-weights-job.yaml | kubectl apply -f -
    
    kubectl wait --for=condition=complete job/qwen3-8-2-4t-stage-weights --timeout=2h
    
    kubectl delete job qwen3-8-2-4t-stage-weights

    モデルの重みをローカル NVMe SSD にダウンロードして Cloud Storage にアップロードするには、約 25 分かかります。

  4. 省略可: ステージング ジョブの実行中に、2 番目のターミナル セッションでステージング Pod のログをストリーミングして、ダウンロードの進行状況をモニタリングします。

    POD_NAME="YOUR_POD_NAME"
    kubectl logs -f "${POD_NAME}"

    YOUR_POD_NAME は、kubectl get pods の qwen3-8-2-4t-stage-weights Pod の名前に置き換えます。

マルチホストの分離された SGLang サーバーをデプロイする

ワークロード マニフェスト(qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml)は、DRA を介してノードのマルチ NIC RDMA デバイス(mrdma.google.com)を割り当てる Kubernetes ResourceClaimTemplate(all-mrdma)、workload-identity-k8s-sa サービス アカウントの Role リソースと RoleBinding リソース、Grove ギャング スケジューリングが有効になっている NVIDIA Dynamo DynamoGraphDeployment(qwen3-8-2-4t-a4-disagg)を定義します。デプロイメントは次のものから構成されます。

  • フロントエンド(Frontend): クライアント リクエストを受け入れ、登録済みのプリフィル ワーカーとデコード ワーカーを検出し、分離されたリクエスト ルーティングを調整する 1 つのルーター レプリカを実行します。

  • プリフィル ワーカー(Prefill): 2 つの a4-highgpu-8g ノードにわたって 2 ノードのワーカープールを実行します。合計 16 個の NVIDIA B200 GPU が搭載されています。各 Pod は、Cloud Storage FUSE CSI ドライバ(gcsfuse.csi.storage.gke.io)とローカル NVMe SSD ファイル キャッシュを使用して、Cloud Storage バケットからステージングされた nvidia/Qwen3.8-2.4T-A95B-NVFP4 重みを /model-weights にマウントし、NCCL/gIB 環境をソースとして、次のフラグを使用して --disaggregation-mode prefill で python3 -m dynamo.sglang を起動します。

    • --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
    • --tensor-parallel-size 16
    • --ep-size 16
    • --nnodes 2
    • --moe-runner-backend flashinfer_trtllm
    • --attention-backend triton
    • --kv-cache-dtype fp8_e4m3
    • --disable-flashinfer-autotune
  • デコード ワーカー(Decode): 残りの 2 つの a4-highgpu-8g ノードで 2 ノードのワーカープールを実行します。合計 16 個の NVIDIA B200 GPU が使用されます。各 Pod は、Cloud Storage FUSE CSI ドライバを介して /model-weights の Cloud Storage からステージングされたモデルの重みをマウントし、次のフラグを使用して --disaggregation-mode decode で python3 -m dynamo.sglang を起動します。

    • --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4"
    • --tensor-parallel-size 16
    • --ep-size 16
    • --nnodes 2
    • --moe-runner-backend flashinfer_trtllm
    • --attention-backend triton
    • --kv-cache-dtype fp8_e4m3

分離された推論サーバーをデプロイしてモニタリングします。

  1. 前にダウンロードした qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml ワークロード マニフェストを確認するか、次の内容で qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml という名前のファイルを作成します。

    # ==============================================================================
    # Multihost Disaggregated Serving of Qwen3.8-2.4T-A95B (NVFP4 MoE) on GKE
    # using NVIDIA Dynamo Platform (v1.5.0) and SGLang on GPU A4 (4 x a4-highgpu-8g)
    # ==============================================================================
    apiVersion: resource.k8s.io/v1
    kind: ResourceClaimTemplate
    metadata:
      name: all-mrdma
    spec:
      spec:
        devices:
          requests:
            - name: req-mrdma
              exactly:
                deviceClassName: mrdma.google.com
                allocationMode: All
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: Role
    metadata:
      name: dynamo-workload-identity-discovery-role
      namespace: default
    rules:
      - apiGroups: [""]
        resources: ["endpoints", "pods"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["discovery.k8s.io"]
        resources: ["endpointslices"]
        verbs: ["get", "list", "watch"]
      - apiGroups: ["nvidia.com"]
        resources: ["dynamoworkermetadatas"]
        verbs: ["create", "get", "list", "watch", "update", "patch", "delete"]
    ---
    apiVersion: rbac.authorization.k8s.io/v1
    kind: RoleBinding
    metadata:
      name: dynamo-workload-identity-discovery-binding
      namespace: default
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: Role
      name: dynamo-workload-identity-discovery-role
    subjects:
      - kind: ServiceAccount
        name: workload-identity-k8s-sa
        namespace: default
    ---
    apiVersion: nvidia.com/v1alpha1
    kind: DynamoGraphDeployment
    metadata:
      name: qwen3-8-2-4t-a4-disagg
      annotations:
        nvidia.com/enable-grove: "true"
    spec:
      backendFramework: sglang
      services:
        # --------------------------------------------------------------------------
        # FRONTEND: Dynamo OpenAI-Compatible Router (`python3 -m dynamo.frontend`)
        # --------------------------------------------------------------------------
        Frontend:
          componentType: frontend
          replicas: 1
          resources:
            requests:
              cpu: "8"
              memory: "32Gi"
            limits:
              cpu: "8"
              memory: "32Gi"
          extraPodSpec:
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  exec python3 -m dynamo.frontend --http-port 8000
    
        # --------------------------------------------------------------------------
        # PREFILL WORKER: 2 x GPU A4 (`a4-highgpu-8g`, 16 x B200 GPUs, TP=16, EP=16)
        # --------------------------------------------------------------------------
        Prefill:
          componentType: worker
          subComponentType: prefill
          replicas: 1
          multinode:
            nodeCount: 2
          extraPodMetadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          livenessProbe:
            httpGet:
              path: /live
              port: system
            periodSeconds: 60
            timeoutSeconds: 30
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /health
              port: system
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 30
            failureThreshold: 60
          sharedMemory:
            size: 128Gi
          resources:
            requests:
              gpu: "8"
              cpu: "128"
              memory: "1500Gi"
            limits:
              gpu: "8"
              cpu: "128"
              memory: "2000Gi"
          envs:
            - name: MODEL_PATH
              value: "/model-weights/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
            - name: DYN_SYSTEM_PORT
              value: "9090"
            - name: LD_LIBRARY_PATH
              value: "/usr/local/gib/lib64:/usr/local/nvidia/lib64"
            - name: GLOO_SOCKET_IFNAME
              value: "eth0"
            - name: NCCL_ALGO
              value: "Ring,Tree"
            - name: WITH_NVIDIA_PEERMEM
              value: "0"
          extraPodSpec:
            serviceAccountName: workload-identity-k8s-sa
            automountServiceAccountToken: true
            resourceClaims:
              - name: rdma
                resourceClaimTemplateName: all-mrdma
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              resources:
                claims:
                  - name: rdma
              startupProbe:
                httpGet:
                  path: /live
                  port: system
                initialDelaySeconds: 120
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 120
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  if [ -f /usr/local/gib/scripts/set_nccl_env.sh ]; then
                    . /usr/local/gib/scripts/set_nccl_env.sh
                  fi
    
                  echo "=== [Dynamo Prefill Worker] Launching dynamo.sglang ==="
                  exec python3 -m dynamo.sglang \
                    --dist-init-addr "${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-prefill-ldr-0.${GROVE_HEADLESS_SERVICE}.:29500" \
                    --model-path "${MODEL_PATH}" \
                    --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4" \
                    --host 0.0.0.0 \
                    --disaggregation-mode prefill \
                    --disaggregation-bootstrap-port 30001 \
                    --tensor-parallel-size 16 \
                    --ep-size 16 \
                    --moe-runner-backend flashinfer_trtllm \
                    --attention-backend triton \
                    --disable-flashinfer-autotune \
                    --nnodes 2 \
                    --kv-cache-dtype fp8_e4m3 \
                    --context-length 16384 \
                    --mem-fraction-static 0.85 \
                    --enable-metrics
              volumeMounts:
                - name: library-dir-host
                  mountPath: /usr/local/nvidia
                - name: gib
                  mountPath: /usr/local/gib
                - name: gcs-model-dir
                  mountPath: /model-weights
                  readOnly: true
            volumes:
              - name: library-dir-host
                hostPath:
                  path: /home/kubernetes/bin/nvidia
              - name: gib
                hostPath:
                  path: /home/kubernetes/bin/gib
              - name: gcs-model-dir
                csi:
                  driver: gcsfuse.csi.storage.gke.io
                  readOnly: true
                  volumeAttributes:
                    bucketName: ${GCS_BUCKET}
                    mountOptions: "implicit-dirs,file-cache:max-size-mb:-1"
    
        # --------------------------------------------------------------------------
        # DECODE WORKER: 2 x GPU A4 (`a4-highgpu-8g`, 16 x B200 GPUs, TP=16, EP=16)
        # --------------------------------------------------------------------------
        Decode:
          componentType: worker
          subComponentType: decode
          replicas: 1
          multinode:
            nodeCount: 2
          extraPodMetadata:
            annotations:
              gke-gcsfuse/volumes: "true"
          livenessProbe:
            httpGet:
              path: /live
              port: system
            periodSeconds: 60
            timeoutSeconds: 30
            failureThreshold: 60
          readinessProbe:
            httpGet:
              path: /health
              port: system
            initialDelaySeconds: 120
            periodSeconds: 30
            timeoutSeconds: 30
            failureThreshold: 60
          sharedMemory:
            size: 128Gi
          resources:
            requests:
              gpu: "8"
              cpu: "128"
              memory: "1500Gi"
            limits:
              gpu: "8"
              cpu: "128"
              memory: "2000Gi"
          envs:
            - name: MODEL_PATH
              value: "/model-weights/hf_models/models--nvidia--qwen3_8-2_4t-a95b-nvfp4"
            - name: DYN_SYSTEM_PORT
              value: "9090"
            - name: LD_LIBRARY_PATH
              value: "/usr/local/gib/lib64:/usr/local/nvidia/lib64"
            - name: GLOO_SOCKET_IFNAME
              value: "eth0"
            - name: NCCL_ALGO
              value: "Ring,Tree"
            - name: WITH_NVIDIA_PEERMEM
              value: "0"
          extraPodSpec:
            serviceAccountName: workload-identity-k8s-sa
            automountServiceAccountToken: true
            resourceClaims:
              - name: rdma
                resourceClaimTemplateName: all-mrdma
            nodeSelector:
              cloud.google.com/gke-accelerator: nvidia-b200
            tolerations:
              - key: "nvidia.com/gpu"
                operator: "Exists"
                effect: "NoSchedule"
            mainContainer:
              image: nvcr.io/nvidia/ai-dynamo/sglang-runtime:1.5.0
              resources:
                claims:
                  - name: rdma
              startupProbe:
                httpGet:
                  path: /live
                  port: system
                initialDelaySeconds: 120
                periodSeconds: 30
                timeoutSeconds: 10
                failureThreshold: 120
              command: ["/bin/bash", "-c"]
              args:
                - |
                  set -ex
                  if [ -f /usr/local/gib/scripts/set_nccl_env.sh ]; then
                    . /usr/local/gib/scripts/set_nccl_env.sh
                  fi
    
                  echo "=== [Dynamo Decode Worker] Launching dynamo.sglang ==="
                  exec python3 -m dynamo.sglang \
                    --dist-init-addr "${GROVE_PCSG_NAME}-${GROVE_PCSG_INDEX}-decode-ldr-0.${GROVE_HEADLESS_SERVICE}.:29500" \
                    --model-path "${MODEL_PATH}" \
                    --served-model-name "nvidia/Qwen3.8-2.4T-A95B-NVFP4" \
                    --host 0.0.0.0 \
                    --disaggregation-mode decode \
                    --disaggregation-bootstrap-port 30001 \
                    --tensor-parallel-size 16 \
                    --ep-size 16 \
                    --moe-runner-backend flashinfer_trtllm \
                    --attention-backend triton \
                    --nnodes 2 \
                    --kv-cache-dtype fp8_e4m3 \
                    --context-length 16384 \
                    --mem-fraction-static 0.85 \
                    --enable-metrics
              volumeMounts:
                - name: library-dir-host
                  mountPath: /usr/local/nvidia
                - name: gib
                  mountPath: /usr/local/gib
                - name: gcs-model-dir
                  mountPath: /model-weights
                  readOnly: true
            volumes:
              - name: library-dir-host
                hostPath:
                  path: /home/kubernetes/bin/nvidia
              - name: gib
                hostPath:
                  path: /home/kubernetes/bin/gib
              - name: gcs-model-dir
                csi:
                  driver: gcsfuse.csi.storage.gke.io
                  readOnly: true
                  volumeAttributes:
                    bucketName: ${GCS_BUCKET}
                    mountOptions: "implicit-dirs,file-cache:max-size-mb:-1"
  2. qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml を Cloud Storage バケット名に置き換え、ワークロード マニフェストをクラスタに適用します。

    envsubst '${GCS_BUCKET}' < qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml | kubectl apply -f -
  3. 重みの読み込みとサーバーの初期化をモニタリングするには、クラスタ内の Pod を一覧表示し、プリフィル リーダー Pod とデコード リーダー Pod のログをストリーミングします。

    kubectl get pods
    POD_NAME="YOUR_POD_NAME"
    kubectl logs -f "${POD_NAME}"

    YOUR_POD_NAME は、kubectl get pods 出力のプリフィル リーダー Pod またはデコード リーダー Pod の名前に置き換えます。

    コールド スタートの場合、Cloud Storage FUSE を介して Cloud Storage から 2,400,000,000,000 個の NVFP4 モデルの重み(200 シャードで約 1.44 TB)をストリーミングし、マルチホスト SGLang サーバーを初期化するのに約 4 ~ 5 分かかります。リクエストを送信する前に、kubectl get pods の READY 列で、フロントエンド Pod に 1/1 が表示され、プリフィル Pod とデコード Pod に 2/2 が表示されるまで待ちます。

    NAME                                                     READY   STATUS    RESTARTS   AGE
    qwen3-8-2-4t-a4-disagg-0-decode-0-decode-ldr-wz92h       2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-decode-0-decode-wkr-fb8g6       2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-frontend-jrx4s                  1/1     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-prefill-0-prefill-ldr-7nz25     2/2     Running   0          5m
    qwen3-8-2-4t-a4-disagg-0-prefill-0-prefill-wkr-7d5cw     2/2     Running   0          5m
    

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

デプロイされた Qwen3.8-2.4T サービスにチャット完了リクエストを送信します。

  1. ローカルポート 8000 を qwen3-8-2-4t-a4-disagg-frontend サービスに転送します。

    kubectl port-forward svc/qwen3-8-2-4t-a4-disagg-frontend 8000:8000

    このターミナル セッションでポート転送を実行したままにするか、バックグラウンドでコマンドを実行します。

  2. 2 番目のターミナル セッションで、/v1/chat/completions エンドポイントにチャット完了リクエストを送信します。

    curl -s http://127.0.0.1:8000/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "nvidia/Qwen3.8-2.4T-A95B-NVFP4",
        "messages": [{"role": "user", "content": "Explain KV-cache disaggregation in one sentence."}],
        "max_tokens": 64
      }' | jq .

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

1,024 個の入力トークン、1,024 個の出力トークン(1k/1k)、256 個のプロンプト、128 個の最大同時実行数の構成で sglang.bench_serving を使用して、Dynamo フロントエンド Pod からデプロイされた Qwen3.8-2.4T サーバーのベンチマークを行います。

ROUTER_POD=$(kubectl get pod -l 'nvidia.com/dynamo-component-type=frontend' -o jsonpath='{.items[0].metadata.name}')

kubectl exec "${ROUTER_POD}" -- python3 -m sglang.bench_serving \
  --backend sglang-oai-chat \
  --base-url http://localhost:8000 \
  --model nvidia/Qwen3.8-2.4T-A95B-NVFP4 \
  --dataset-name random \
  --num-prompts 256 \
  --random-input-len 1024 \
  --random-output-len 1024 \
  --random-range-ratio 1.0 \
  --max-concurrency 128 | tee benchmark.log

ベンチマークが完了したら、次のような指標を含む出力を探します。

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

クリーンアップ

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

  1. DynamoGraphDeployment リソースと ResourceClaimTemplate リソースを削除します。

    kubectl delete -f qwen3.8-2.4t-nvfp4-multihost-a4-gcsfuse.yaml --ignore-not-found || true
  2. NVIDIA Dynamo プラットフォームと KAI Scheduler Helm リリースをアンインストールします。

    helm uninstall dynamo-platform kai-scheduler || true
  3. Hugging Face Kubernetes Secret を削除します。

    kubectl delete secret hf-secret --ignore-not-found || true
  4. cluster-toolkit ディレクトリから、gcluster destroy コマンドを使用して、GKE クラスタとその関連インフラストラクチャを破棄します。

    cd cluster-toolkit
    ./gcluster destroy "${CLUSTER_NAME}" --auto-approve || true
    cd ..
  5. ステージングされたモデルの重みと Terraform の状態を保存する Cloud Storage バケットを削除します。

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

次のステップ