TPU v6e에서 vLLM TorchTPU를 사용하여 GKE에서 Qwen3-30B-A3B 서빙

이 튜토리얼에서는 단일 호스트 TPU v6e(Trillium) 가속기와 TorchTPU(torchtpu)가 포함된 vLLM TPU 서빙 프레임워크를 사용하여 Google Kubernetes Engine(GKE)에서 전문가 혼합 모델 Qwen3-30B-A3B(Qwen/Qwen3-30B-A3B-FP8)를 배포하고 서빙하는 방법을 보여줍니다. 데이터 동시 로드 및 전문가 동시 로드가 사용 설정된 2x2 토폴로지에 TPU v6e 칩 4개가 포함된 단일 ct6e-standard-4t 노드에서 워크로드를 실행합니다.

더 큰 Qwen3.5-397B 모델을 사용하여 TPU7x(Ironwood) 가속기에서 유사한 단일 호스트 vLLM TorchTPU 서빙 워크플로를 실행하려면 TPU7x 가속기에서 vLLM TorchTPU를 사용하여 GKE에서 Qwen3.5-397B 서빙을 참조하세요.

목표

  • GKE 클러스터와 단일 호스트 TPU v6e 노드 풀을 설정합니다.
  • GKE에 단일 호스트 vLLM TorchTPU 서버를 배포합니다.
  • vLLM TorchTPU 서버에 추론 요청을 보냅니다.
  • 서빙 벤치마크 워크로드를 실행합니다.
  • 삭제

비용

이 문서에서는 비용이 청구될 수 있는Google Cloud구성요소를 사용합니다.

프로젝트 사용량을 기준으로 예상 비용을 산출하려면 가격 계산기를 사용하세요.

이 문서에 설명된 태스크를 완료했으면 만든 리소스를 삭제하여 청구가 계속되는 것을 방지할 수 있습니다. 자세한 내용은 삭제를 참조하세요.

시작하기 전에

튜토리얼 환경을 설정합니다.

  • Google Cloud 프로젝트에서 Kubernetes Engine API를 사용 설정합니다.

    Kubernetes Engine API 사용 설정

  • Google Cloud 프로젝트에서 Artifact Registry API를 사용 설정합니다.

    Artifact Registry API 사용 설정

  • Google Cloud CLI를 설치하고 초기화하고 kubectl를 설치합니다. Cloud Shell을 사용하는 경우 두 도구 모두 사전 설치되어 있습니다.

  • TPU 용량이 있는 리전에 프로젝트의 SSD 할당량 및 외부 IP 주소 할당량이 충분한지 확인합니다.

    1. Google Cloud 콘솔에서 할당량 및 시스템 한도 페이지로 이동합니다.

      할당량 및 시스템 한도로 이동

    2. 영구 디스크 SSD(GB)로 필터링하고 모델 캐시 볼륨에 사용 가능한 할당량이 500GB 이상인지 확인합니다.

    3. 사용 중인 리전 외부 IPv4 주소로 필터링하고 대상 리전에 IP 주소 할당량이 충분한지 확인합니다.

Hugging Face 액세스 토큰 가져오기

이 튜토리얼을 사용하려면 Hugging Face 액세스 토큰이 필요합니다. Hugging Face에서 무료 계정에 가입할 수 있습니다. 계정이 있으면 액세스 토큰을 생성합니다.

  1. Hugging Face에 오신 것을 환영합니다 페이지에서 계정 아바타를 클릭하고 액세스 토큰을 선택합니다.
  2. 액세스 토큰 페이지에서 새 토큰 만들기를 클릭합니다.
  3. 읽기 토큰 유형을 선택하고 토큰 이름을 입력합니다.
  4. 액세스 토큰이 표시됩니다. 토큰을 안전한 장소에 저장합니다.

필요한 역할

튜토리얼을 완료하는 데 필요한 권한을 얻으려면 관리자에게 프로젝트에 대한 다음 IAM 역할을 부여해 달라고 요청하세요.

역할 부여에 대한 자세한 내용은 프로젝트, 폴더, 조직에 대한 액세스 관리를 참조하세요.

커스텀 역할이나 다른 사전 정의된 역할을 통해 필요한 권한을 얻을 수도 있습니다.

환경 변수 설정

인증을 설정합니다.

gcloud auth login
gcloud auth application-default login

환경 변수 설정:

export PROJECT_ID="YOUR_PROJECT_ID"
export REGION="YOUR_REGION"
export ZONE="YOUR_ZONE"
export CLUSTER_NAME="YOUR_CLUSTER_NAME"
export MACHINE_TYPE="tpu7x-standard-4t"
export NODE_POOL_NAME="np-0"
export RESERVATION_NAME="YOUR_RESERVATION_NAME"
export HF_TOKEN="YOUR_HF_TOKEN"
export NETWORK="YOUR_NETWORK_NAME"
export SUBNETWORK="YOUR_SUBNETWORK_NAME"
export VLLM_IMAGE="YOUR_VLLM_TPU_IMAGE"

gcloud config set project "${PROJECT_ID}"
gcloud config set billing/quota_project "${PROJECT_ID}"

다음을 바꿉니다.

  • YOUR_PROJECT_ID: Google Cloud프로젝트의 ID
  • YOUR_REGION: 클러스터를 배포하려는 리전입니다(예: us-central1).
  • YOUR_ZONE: TPU v6e 노드 풀을 배포할 영역입니다(예: us-central1-c).
  • YOUR_CLUSTER_NAME: GKE 클러스터의 이름입니다(예: tpu-v6e-tutorial-cluster).
  • YOUR_RESERVATION_NAME: 예약을 사용하는 경우 TPU v6e 예약의 이름입니다.
  • YOUR_HF_TOKEN: Hugging Face 액세스 토큰입니다.
  • YOUR_NETWORK_NAME:Google Cloud 네트워크의 이름입니다. 커스텀 네트워크를 만든 경우 해당 네트워크를 지정합니다. 그렇지 않은 경우 default를 지정합니다.
  • YOUR_SUBNETWORK_NAME:Google Cloud 서브네트워크의 이름입니다. 커스텀 서브네트워크를 만든 경우 해당 서브네트워크를 지정합니다. 그렇지 않은 경우 default를 지정합니다.
  • YOUR_VLLM_TPU_IMAGE: 사용할 vLLM TorchTPU 이미지입니다. 공개 이미지는 vLLM TorchTPU 저장소를 참조하세요.

GKE 클러스터 만들기

일반 모드에서 GKE 클러스터를 만들고 단일 호스트 TPU v6e 노드 풀을 연결하려면 다음 단계를 완료합니다.

  1. 리전 GKE 클러스터를 만듭니다.

    gcloud container clusters create "${CLUSTER_NAME}" \
        --project="${PROJECT_ID}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --network="${NETWORK}" \
        --subnetwork="${SUBNETWORK}"

    GKE 클러스터의 기본 머신 유형(e2-medium)에는 워크로드 컨트롤러에 충분한 CPU 용량이 없으므로 이 명령어는 e2-standard-4을 지정합니다.

    출력은 다음과 비슷합니다.

    NAME                      LOCATION     MASTER_VERSION      MASTER_IP      MACHINE_TYPE   NODE_VERSION        NUM_NODES  STATUS
    tpu-v6e-tutorial-cluster  us-central1  1.35.7-gke.1027000  34.63.222.246  e2-standard-4  1.35.7-gke.1027000  3          RUNNING
    
  2. 예약을 사용하여 ct6e-standard-4t 노드 1개(2x2 토폴로지의 TPU v6e 칩 4개)가 있는 단일 호스트 TPU v6e 노드 풀을 만듭니다.

    gcloud container node-pools create "${NODE_POOL_NAME}" \
        --cluster="${CLUSTER_NAME}" \
        --machine-type="${MACHINE_TYPE}" \
        --location="${REGION}" \
        --node-locations="${ZONE}" \
        --project="${PROJECT_ID}" \
        --num-nodes=1 \
        --reservation-affinity=specific \
        --reservation="${RESERVATION_NAME}"

    특정 예약을 사용하는 대신 다음 플래그를 전달하여 스팟 용량 또는 Flex-start VM을 사용하여 노드 풀을 만들 수도 있습니다.

    • 스팟 VM: --reservation-affinity 및 --reservation 플래그를 --spot로 바꿔 스팟 용량을 사용합니다. 스팟 용량을 사용하는 인스턴스는 노드 풀이 생성된 후 생성되는 데 시간이 걸릴 수 있으며 워크로드 실행 중에 선점될 수 있습니다. 포드를 만드는 데 시간이 걸리거나 실행 중에 워크로드가 중지되는 경우 이것이 원인일 수 있습니다.
    • Flex-start VM: Flex-start 소비 옵션을 사용하여 노드 풀을 프로비저닝하려면 --flex-start 플래그를 전달합니다. 자세한 내용은 TPU 및 Flex-start VM으로 소규모 일괄 워크로드 실행을 참조하세요.

    출력은 다음과 비슷합니다.

    NAME  MACHINE_TYPE      DISK_SIZE_GB  NODE_VERSION
    np-0  ct6e-standard-4t  100           1.35.7-gke.1027000
    
  3. 클러스터와 통신하도록 kubectl 사용자 인증 정보를 구성합니다.

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

사용자 인증 정보 구성

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 -

vLLM TorchTPU 서버 배포

워크로드 매니페스트(qwen3_5_server.yaml)는 다음 리소스를 정의합니다.

  • 다운로드한 모델 가중치, XLA 컴파일 캐시, PyTorch 컴파일 캐시를 저장하기 위해 Google Cloud Hyperdisk Balanced 스토리지에서 지원하는 StorageClass(hyperdisk-balanced-qwen3) 및 500GiB PersistentVolumeClaim(hd-claim-qwen3)

  • 2x2 토폴로지(google.com/tpu: '4')가 있는 단일 TPU v6e 노드에 예약된 배포(vllm-qwen3). 컨테이너는 다음 플래그를 사용하여 Qwen/Qwen3-30B-A3B-FP8에 대해 vllm serve를 실행합니다.

    • --data-parallel-size=4
    • --tensor-parallel-size=1
    • --enable-expert-parallel
    • --kv-cache-dtype=fp8
    • --max-model-len=9236
    • --max-num-seqs=64개
  • 포트 8000를 노출하는 ClusterIP 서비스(vllm-qwen3-service)

워크로드를 배포하고 모니터링하려면 다음 단계를 완료합니다.

  1. 서버 매니페스트를 가져옵니다.

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/tpu/inference/qwen3-5-397b/qwen3_5_server.yaml

    또는 다음 콘텐츠가 포함된 qwen3_5_server.yaml 파일을 만들 수 있습니다.

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: hyperdisk-balanced-qwen3-5
    provisioner: pd.csi.storage.gke.io
    parameters:
      type: hyperdisk-balanced
    reclaimPolicy: Delete
    volumeBindingMode: WaitForFirstConsumer
    allowVolumeExpansion: true
    ---
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: hd-claim-qwen3-5
      namespace: default
    spec:
      storageClassName: hyperdisk-balanced-qwen3-5
      accessModes:
        - ReadWriteOnce
      resources:
        requests:
          storage: 1500Gi
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: vllm-qwen3-5
      namespace: default
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: vllm-qwen3-5
      template:
        metadata:
          labels:
            app: vllm-qwen3-5
        spec:
          nodeSelector:
            cloud.google.com/gke-tpu-accelerator: tpu7x
            cloud.google.com/gke-tpu-topology: 2x2x1
          tolerations:
          - key: "cloud.google.com/gke-spot"
            operator: "Equal"
            value: "true"
            effect: "NoSchedule"
          containers:
          - name: vllm-tpu
            image: $VLLM_IMAGE
            command: ["vllm", "serve"]
            args:
            - --host=0.0.0.0
            - --port=8000
            - --model=Qwen/Qwen3.5-397B-A17B-FP8
            - --tokenizer=Qwen/Qwen3.5-397B-A17B-FP8
            - --max-model-len=9236
            - --max-num-batched-tokens=1024
            - --max-num-seqs=64
            - --no-enable-prefix-caching
            - --gpu-memory-utilization=0.9
            - --data-parallel-size=8
            - --tensor-parallel-size=1
            - --async-scheduling
            - --language-model-only
            - --enable-auto-tool-choice
            - --tool-call-parser=qwen3_coder
            - --reasoning-parser=qwen3
            - '--limit-mm-per-prompt={"image": 0, "video": 0}'
            - --kv-cache-dtype=fp8
            - --enable-expert-parallel
            - --download-dir=/data
            - --block-size=128
            env:
            - name: MODEL_IMPL_TYPE
              value: "vllm"
            - name: DP_SCHED_ENABLED
              value: "1"
            - name: ONEHOT_MOE_PERMUTE_THRESHOLD
              value: "32768"
            - name: VLLM_ENGINE_READY_TIMEOUT_S
              value: "9600"
            - name: VLLM_ENGINE_ITERATION_TIMEOUT_S
              value: "1800"
            - name: USE_MOE_SPARSE_CORE
              value: "1"
            - name: TPU_MOE_SKIP_PADDED_TOKENS
              value: "1"
            - name: VLLM_CACHE_ROOT
              value: "/data/vllm_cache"
            - name: VLLM_TORCH_COMPILE_CACHE_DIR
              value: "/data/torch_compile_cache"
            - name: XLA_CACHE_DIR
              value: "/data/xla_cache"
            - name: HF_HOME
              value: /data
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            ports:
            - containerPort: 8000
            resources:
              limits:
                google.com/tpu: '4'
              requests:
                google.com/tpu: '4'
            readinessProbe:
              tcpSocket:
                port: 8000
              initialDelaySeconds: 15
              periodSeconds: 10
            volumeMounts:
            - mountPath: "/data"
              name: data-volume
            - mountPath: /dev/shm
              name: dshm
          volumes:
          - emptyDir:
              medium: Memory
            name: dshm
          - name: data-volume
            persistentVolumeClaim:
              claimName: hd-claim-qwen3-5
    ---
    apiVersion: v1
    kind: Service
    metadata:
      name: vllm-qwen3-5-service
      namespace: default
    spec:
      selector:
        app: vllm-qwen3-5
      type: ClusterIP
      ports:
      - name: http
        protocol: TCP
        port: 8000
        targetPort: 8000
  2. vLLM TorchTPU 서버 매니페스트를 클러스터에 적용합니다.

    envsubst '$VLLM_IMAGE' < "qwen3_5_server.yaml" | kubectl apply -f -
  3. 애플리케이션 시작이 완료될 때까지 서버 배포 로그를 모니터링합니다.

    kubectl wait --for=jsonpath='{.status.phase}'=Running pod -l app=vllm-qwen3-5 --timeout=900s
    kubectl logs deployment/vllm-qwen3-5 -c vllm-tpu -f

    포드가 모델 가중치를 다운로드하고 모델을 컴파일하는 동안 초기 서비스 시작에 약 15~30분이 걸릴 수 있습니다.

    스팟 또는 Flex-start와 같은 선점형 용량을 사용하는 경우 노드 풀 생성 후 TPU 슬라이스 노드가 생성되는 데 시간이 더 오래 걸릴 수 있으며 워크로드 실행 중에 선점될 수 있습니다. 포드가 시작하는 데 예상보다 오래 걸리거나 워크로드가 중간에 중지되는 경우 이것이 원인일 수 있습니다. 자세한 내용은 스팟 VM 선점을 참조하세요. TPU 슬라이스 노드가 생성되었는지 확인하려면 노드 상태 및 조건 확인을 참조하세요.

    트래픽을 전송하기 전에 로그에서 Application startup complete를 보고할 때까지 기다립니다.

    (APIServer pid=1) INFO:     Started server process [1]
    (APIServer pid=1) INFO:     Waiting for application startup.
    (APIServer pid=1) INFO:     Application startup complete.
    
  4. 두 번째 터미널 세션을 엽니다.

  5. 두 번째 터미널 세션에서 로컬 포트 8000을 vllm-qwen3-service에 전달합니다.

    kubectl port-forward service/vllm-qwen3-5-service 8000:8000

    이 터미널 세션에서 포트 전달을 실행 중인 상태로 둡니다.

추론 요청 전송

첫 번째 터미널 세션으로 돌아가서 /v1/chat/completions 엔드포인트에 채팅 완성 요청을 보냅니다.

curl http://localhost:8000/v1/chat/completions \
    -H "Content-Type: application/json" \
    -d '{
        "model": "Qwen/Qwen3.5-397B-A17B-FP8",
        "messages": [
            {"role": "user", "content": "Hello! Tell me a joke."}
        ],
        "max_tokens": 512,
        "temperature": 0.7
    }' | jq .

벤치마크 워크로드 실행

InferenceX 벤치마크 도구 모음을 사용하여 8,192 토큰 입력 및 1,024 토큰 출력(8k/1k) 워크로드를 통해 배포된 Qwen3-30B-A3B 모델을 벤치마킹하려면 다음 단계를 완료하세요.

  1. 벤치마크 매니페스트를 가져옵니다.

    curl -O https://raw.githubusercontent.com/GoogleCloudSamples/AIHypercomputerSamples/refs/heads/main/tpu/inference/qwen3-5-397b/qwen3_5-benchmark-8k1k.yaml

    또는 다음 콘텐츠로 qwen3_5-benchmark-8k1k.yaml라는 파일을 만듭니다.

    apiVersion: v1
    kind: Pod
    metadata:
      name: qwen3-5-bench-8k1k
      namespace: default
    spec:
      restartPolicy: OnFailure
      terminationGracePeriodSeconds: 60
      containers:
      - name: vllm-bench
        image: $VLLM_IMAGE
        command: ["/bin/bash", "-c"]
        args:
        - |
          while ! curl http://vllm-qwen3-5-service:8000/ping; do sleep 30 && echo 'Waiting for server...'; done
          apt-get update && apt-get install -y git && \
          git clone https://github.com/SemiAnalysisAI/InferenceX.git /ubench/inferencex && \
          cd /ubench/inferencex && \
          git checkout 89ce6098ef2bc4576a735c43f39c7d972b091cfc && \
          python3 /ubench/inferencex/utils/bench_serving/benchmark_serving.py \
            --backend=vllm \
            --percentile-metrics='ttft,tpot,itl,e2el' \
            --host=vllm-qwen3-5-service \
            --port=8000 \
            --model=Qwen/Qwen3.5-397B-A17B-FP8 \
            --tokenizer=Qwen/Qwen3.5-397B-A17B-FP8 \
            --dataset-name=random \
            --random-input-len=8192 \
            --random-output-len=1024 \
            --random-range-ratio=1.0 \
            --num-prompts=2560 \
            --num-warmups=512 \
            --max-concurrency=256 \
            --ignore-eos
        env:
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              key: hf_api_token
              name: hf-secret
  2. 벤치마크 포드 매니페스트를 적용합니다.

    envsubst '$VLLM_IMAGE' < "qwen3_5-benchmark-8k1k.yaml" | kubectl apply -f -
  3. 벤치마크 로그를 모니터링합니다.

    kubectl wait --for=jsonpath='{.status.phase}'=Running pod --field-selector=metadata.name=qwen3-5-bench-8k1k --timeout=600s
    kubectl logs -f qwen3-5-bench-8k1k

    벤치마크를 완료하는 데 약 25~30분이 걸립니다.

  4. 벤치마크가 완료되면 다음과 같은 측정항목이 포함된 출력을 찾습니다.

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

삭제

이 튜토리얼에서 사용한 리소스에 대한 추가 비용이 Google Cloud 계정에 청구되지 않도록 하려면 Kubernetes 워크로드, TPU 노드 풀, GKE 클러스터를 삭제합니다.

  1. vLLM TorchTPU 서버 배포, 서비스, PersistentVolumeClaim, Hugging Face 보안 비밀을 삭제합니다.

    kubectl delete -f qwen3_5_server.yaml --ignore-not-found || true
    kubectl delete secret hf-secret --ignore-not-found || true
  2. TPU v6e 노드 풀과 GKE 클러스터를 삭제합니다.

    gcloud container node-pools delete "${NODE_POOL_NAME}" \
        --cluster="${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet || true
    
    gcloud container clusters delete "${CLUSTER_NAME}" \
        --location="${REGION}" \
        --project="${PROJECT_ID}" \
        --quiet || true

다음 단계