نشر استنتاج vLLM متعدد المضيفين على وحدات معالجة Tensor باستخدام Ray على GKE

1. مقدمة

في هذا الدرس التطبيقي حول الترميز، ستتعرّف على كيفية نشر خدمات استنتاج vLLM (نموذج لغوي كبير افتراضي) عالي الأداء ومتعدد المضيفين على Google Kubernetes Engine (GKE) باستخدام وحدات TPU من Google Cloud. ستضبط إعدادات الاستنتاج الموزّع باستخدام Ray وتدير عبء العمل بشكلٍ أصلي على GKE باستخدام LeaderWorkerSets.

يحاكي هذا الدليل الإرشادي عملية إعداد الإنتاج لعرض نماذج كبيرة مثل Qwen 30B.

الإجراءات التي ستنفذّها

  • أنشئ شبكة VPC مخصّصة لحركة بيانات أداة تسريع الألعاب.
  • توفير مجموعة GKE مع Ray Operator وبرنامج تشغيل GCS Fuse CSI.
  • ابدأ بإنشاء ذاكرة تخزين مؤقت سريعة في GCS لتحميل النماذج بشكل أسرع.
  • توفير مجموعة أجهزة ذات التخصيص نفسه لوحدة معالجة الموتّرات v6e متعددة المضيفين مع سعة محجوزة
  • إعداد Workload Identity للوصول الآمن إلى أوزان النماذج
  • تفعيل محرك vLLM واختباره لعرض نموذج مَعلمات يبلغ حجمه 30 مليارًا

المتطلبات

  • مشروع Google Cloud تم تفعيل الفوترة فيه
  • حجز على Google Cloud لموارد TPU v6e (32 شريحة، ct6e-standard-4t)
  • إذن الوصول إلى نُسخ أوزان النماذج من حزمة مصدر
  • Cloud Shell أو وحدة طرفية محلية مثبَّت عليها gcloud وkubectl وhelm
  • المدة المقدَّرة: 60 دقيقة
  • التكلفة المقدّرة: أقل من 60 دولارًا أمريكيًا (بافتراض أنّ عملية التفكيك ستتمّ على الفور).

2. قبل البدء

إنشاء مشروع Google Cloud أو اختياره

  1. في Google Cloud Console، اختَر مشروعًا على Google Cloud أو أنشِئ مشروعًا.
  2. تأكَّد من تفعيل الفوترة لمشروعك على Cloud.

بدء Cloud Shell

  1. انقر على تفعيل Cloud Shell في أعلى "وحدة تحكّم Google Cloud".
  2. إثبات صحة المصادقة:
gcloud auth list
  1. أكِّد مشروعك:
gcloud config get project
  1. اضبطه إذا لزم الأمر:
export PROJECT_ID=<YOUR_PROJECT_ID>
gcloud config set project $PROJECT_ID

ضبط متغيرات البيئة

لتسهيل تنفيذ الأوامر، حدِّد المتغيّرات التالية في shell. استبدِل <YOUR_ZONE> بمنطقة TPU المخصّصة لك و <YOUR_RESERVATION_NAME> بمعرّف الحجز. عليك إنشاء رمز دخول مستخدم Hugging Face لتنزيل أوزان النماذج المحظورة. بعد إنشائه، استبدِل <YOUR_HUGGING_FACE_TOKEN> بالرمز المميز الذي أنشأته حديثًا.

export PROJECT_ID=$(gcloud config get-value project)
export PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} --format="value(projectNumber)")
export ZONE="<YOUR_ZONE>" # e.g., us-east5-a
export REGION=${ZONE%-*}
export CLUSTER_NAME="qwen-serving-cluster"
export GVNIC_NETWORK_PREFIX="qwen-serving"
export BUCKET_NAME="inf-demo-model-storage-${PROJECT_NUMBER}"
export RESERVATION_NAME="<YOUR_RESERVATION_NAME>"
export NODE_POOL_NAME="tpu-v6e-32-resvd-pool"
export MULTIHOST_COLLECTION_NAME="tpu-6-collection"
export HF_TOKEN="<YOUR_HUGGING_FACE_TOKEN>" # Token with access to Qwen model if restricted

تفعيل واجهات برمجة التطبيقات

فعِّل خدمات Google Cloud المطلوبة:

gcloud services enable \
    container.googleapis.com \
    compute.googleapis.com \
    iam.googleapis.com \
    cloudresourcemanager.googleapis.com

3- إنشاء شبكة مخصّصة

تتطلّب أحمال عمل وحدات TPU المتعددة المضيفين إعدادات شبكة معيّنة، بما في ذلك أحجام MTU أكبر لتحقيق كفاءة في التواصل بين أدوات التسريع. أنشئ شبكة VPC مخصّصة لمجموعتك.

  1. أنشئ شبكة VPC باستخدام وحدة نقل قصوى (MTU) كبيرة (8896):
    gcloud compute --project=${PROJECT_ID} \
        networks create ${GVNIC_NETWORK_PREFIX}-main \
        --subnet-mode=custom \
        --mtu=8896
    
  2. أنشئ الشبكة الفرعية للمجموعة:
    gcloud compute --project=${PROJECT_ID} \
        networks subnets create ${GVNIC_NETWORK_PREFIX}-tpu \
        --network=${GVNIC_NETWORK_PREFIX}-main \
        --region=${REGION} \
        --range=192.168.100.0/24
    
  3. أنشئ قواعد جدار الحماية التي تسمح بالزيارات الداخلية لتمكين العاملين من التواصل:
    gcloud compute --project=${PROJECT_ID} firewall-rules create ${GVNIC_NETWORK_PREFIX}-allow-internal \
        --network=${GVNIC_NETWORK_PREFIX}-main \
        --allow=all \
        --source-ranges=172.16.0.0/12,192.168.0.0/16,10.0.0.0/8 \
        --description="Allow all internal traffic within the network."
    

4. توفير مجموعة GKE

أنشئ إعدادًا عاديًا لمجموعة GKE تم ضبطه ليتوافق مع عمليات ربط GCS Fuse وأحمال عمل Ray Operator.

  1. إنشاء المجموعة:
    gcloud container clusters create ${CLUSTER_NAME} \
        --project=${PROJECT_ID} \
        --location=${REGION} \
        --release-channel=rapid \
        --machine-type=e2-standard-4 \
        --network=${GVNIC_NETWORK_PREFIX}-main \
        --subnetwork=${GVNIC_NETWORK_PREFIX}-tpu \
        --num-nodes=1 \
        --gateway-api=standard \
        --enable-managed-prometheus \
        --enable-dataplane-v2 \
        --enable-dataplane-v2-metrics \
        --workload-pool=${PROJECT_ID}.svc.id.goog \
        --addons=GcsFuseCsiDriver,RayOperator \
        --enable-ip-alias
    
  2. استرداد بيانات اعتماد المجموعة:
    gcloud container clusters get-credentials ${CLUSTER_NAME} --region=${REGION}
    
  3. إنشاء رمز Hugging Face السرّي: احفظ الرمز المميز بشكل آمن لتنزيل الحاويات:
    kubectl create secret generic hf-secret \
        --from-literal=hf_api_token=${HF_TOKEN} \
        --dry-run=client -o yaml | kubectl apply -f -
    
  4. ثبِّت LeaderWorkerSet (LWS) باستخدام Helm. تتولّى خدمة LWS إدارة مجموعات من وحدات البود التي يجب جدولة عملياتها معًا:
    helm install lws oci://registry.k8s.io/lws/charts/lws \
        --version=0.7.0 \
        --namespace lws-system \
        --create-namespace \
        --wait
    

5- تفعيل ميزة "ذاكرة التخزين المؤقت السريع" في GCS

لتسريع قراءة عشرات غيغابايت من الأوزان من Cloud Storage أثناء العرض، أنشئ حزمة GCS وفعِّل GCS Rapid Cache في منطقتك.

  1. إنشاء الحزمة:
    gcloud storage buckets create gs://$BUCKET_NAME \
        --location=$REGION \
        --uniform-bucket-level-access
    
  2. إعداد Rapid Cache في منطقة TPU:
    gcloud storage buckets anywhere-caches create gs://$BUCKET_NAME $ZONE \
        --ttl=1d \
        --admission-policy=ADMIT_ON_FIRST_MISS
    

6. إعداد Workload Identity وأذونات التخزين

اضبط روابط التعريف لتركيب مجموعة البيانات الخاصة بالوزن بشكل آمن في وحدات GKE بدون تضمين مفاتيح طويلة الأمد.

  1. إنشاء حساب خدمة مخصّص في "إدارة الهوية وإمكانية الوصول":
    gcloud iam service-accounts create tpu-reader-sa
    
  2. منح أذونات قراءة الحزمة:
    gcloud storage buckets add-iam-policy-binding gs://${BUCKET_NAME} \
        --member="serviceAccount:tpu-reader-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
        --role="roles/storage.objectAdmin"
    
  3. أنشئ ربط Workload Identity لحساب خدمة Kubernetes في مساحة الاسم default:
    gcloud iam service-accounts add-iam-policy-binding tpu-reader-sa@${PROJECT_ID}.iam.gserviceaccount.com \
        --role="roles/iam.workloadIdentityUser" \
        --member="serviceAccount:${PROJECT_ID}.svc.id.goog[default/default]"
    
  4. إضافة تعليق توضيحي إلى حساب الخدمة في Kubernetes:
    kubectl annotate serviceaccount default iam.gke.io/gcp-service-account=tpu-reader-sa@${PROJECT_ID}.iam.gserviceaccount.com
    

7. إعداد أوزان النماذج

لعرض نموذج مَعلمات 30B، عليك تنزيل الأوزان من Hugging Face إلى حزمة GCS. لتجاوز الحد الأقصى المسموح به لحصة القرص في Cloud Shell (5 غيغابايت)، استخدِم مهمة Standard Kubernetes لتنزيل البيانات مباشرةً داخل المجموعة والكتابة في وحدة تخزين GCS Fuse المثبَّتة بشكل آمن.

  1. نشر مهمة "تنزيل النموذج": أنشئ البيان التالي وطبِّقه لبدء عملية التنزيل:
    cat <<EOF | kubectl apply -f -
    apiVersion: batch/v1
    kind: Job
    metadata:
      name: model-downloader
    spec:
      ttlSecondsAfterFinished: 60
      template:
        metadata:
          annotations:
            gke-gcsfuse/volumes: "true"
            gke-gcsfuse/memory-limit: "0"
        spec:
          serviceAccountName: default
          restartPolicy: OnFailure
          containers:
          - name: downloader
            image: python:3.10-slim
            command: ["/bin/sh", "-c"]
            args:
            - |
              pip install -U "huggingface_hub[hf_transfer]" filelock
              export HF_HUB_ENABLE_HF_TRANSFER=1
    
              python -c '
              import filelock
    
              class DummyLock:
                  def __init__(self, *args, **kwargs): pass
                  def __enter__(self): return self
                  def __exit__(self, *args): pass
                  def acquire(self, *args, **kwargs): pass
                  def release(self, *args, **kwargs): pass
    
              filelock.FileLock = DummyLock
    
              from huggingface_hub import snapshot_download
              snapshot_download(
                  repo_id="Qwen/Qwen3-30B-A3B", 
                  local_dir="/models/qwen3-30b-weights",
                  local_dir_use_symlinks=False
              )
              '
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-secret
                  key: hf_api_token
            volumeMounts:
            - name: model-weights
              mountPath: /models
          volumes:
          - name: model-weights
            csi:
              driver: gcsfuse.csi.storage.gke.io
              volumeAttributes:
                bucketName: ${BUCKET_NAME}
                mountOptions: "implicit-dirs"
    EOF
    
  2. مراقبة عملية التنزيل: راجِع سجلّات وحدة تنزيل البيانات (downloader pod) لتتبُّع مستوى التقدّم:
    kubectl logs -f job/model-downloader
    
    انتظِر إلى أن تكتمل المهمة بنجاح.

8. إنشاء مجموعة عقد لوحدة معالجة الموتّرات محجوزة

وفِّر شريحة TPU الفعلية التي تتضمّن عدة مضيفين باستخدام حجز السعة الحالي.

  1. نفِّذ أمر الإنشاء:
    gcloud beta container node-pools create ${NODE_POOL_NAME} \
        --project=${PROJECT_ID} \
        --cluster=${CLUSTER_NAME} \
        --region=${REGION} \
        --node-locations=${ZONE} \
        --machine-type=ct6e-standard-4t \
        --tpu-topology=4x8 \
        --num-nodes=8 \
        --scopes=https://www.googleapis.com/auth/cloud-platform \
        --reservation-affinity=specific \
        --reservation=${RESERVATION_NAME} \
        --accelerator-network-profile=auto \
        --node-labels=cloud.google.com/gke-nodepool-group-name=${MULTIHOST_COLLECTION_NAME} \
        --node-labels=cloud.google.com/gke-workload-type=HIGH_AVAILABILITY \
        --node-labels=cloud.google.com/gke-networking-dra-driver=true
    
  2. انتظار انضمام العُقد: يمكنك مراقبة توسيع نطاق تجميع العُقد مباشرةً. انتظِر إلى أن تنضم 8 عُقد تحتوي على ct6e إلى kubectl get nodes.

9- نشر خدمة vLLM

  1. إنشاء مطالبات الشبكة: عليك طلب بيئة الشبكة:
    cat <<EOF | kubectl apply -f -
    apiVersion: resource.k8s.io/v1
    kind: ResourceClaimTemplate
    metadata:
      name: all-netdev
    spec:
      spec:
        devices:
          requests:
          - name: req-netdev
            exactly:
              deviceClassName: netdev.google.com
              allocationMode: All
    EOF
    
  2. نشر نقطة نهاية واجهة برمجة التطبيقات الخاصة بجهاز موازنة الحمل:
    cat <<EOF | kubectl apply -f -
    apiVersion: v1
    kind: Service
    metadata:
      name: vllm-tpu-service
    spec:
      type: LoadBalancer
      selector:
        leaderworkerset.sigs.k8s.io/name: vllm-tpu-qwen
        leaderworkerset.sigs.k8s.io/worker-index: "0"
      ports:
      - protocol: TCP
        port: 8000
        targetPort: 8000
    EOF
    
  3. نشر عبء عمل LeaderWorkerSet: يبدأ ملف البيان هذا تجميع خادم Ray الرئيسي/العامل بشكل ديناميكي على مستوى 8 مضيفين للشرائح.
    cat <<EOF | kubectl apply -f -
    apiVersion: leaderworkerset.x-k8s.io/v1
    kind: LeaderWorkerSet
    metadata:
      name: vllm-tpu-qwen
    spec:
      replicas: 1
      leaderWorkerTemplate:
        size: 8
        restartPolicy: RecreateGroupOnPodRestart
        workerTemplate:
          metadata:
            annotations:
              gke-gcsfuse/volumes: "true"
              gke-gcsfuse/memory-limit: "0"
            labels:
              leaderworkerset.sigs.k8s.io/name: vllm-tpu-qwen
              gke-gcsfuse/volumes: "true"
          spec:
            hostname: vllm-tpu-qwen
            serviceAccountName: default
            containers:
            - name: vllm-tpu
              image: vllm/vllm-tpu:nightly
              command: ["sh", "-c"]
              args:
              - |
                MY_TPU_IP=\$(hostname -I | awk '{print \$1}')
                echo "My TPU Network IP is: \$MY_TPU_IP"
    
                LEADER_DNS="vllm-tpu-qwen-0.vllm-tpu-qwen"
                until getent hosts \$LEADER_DNS; do
                  echo "DNS not ready. Sleeping 5s..."
                  sleep 5
                  done
                LEADER_IP=\$(getent hosts \$LEADER_DNS | awk '{print \$1}')
    
                export JAX_PLATFORMS=''
                export SCAN_TPU_CHIPS=True
                export TPU_MULTIHOST_BACKEND=ray
                export JAX_DISTRIBUTED_INITIALIZATION_TIMEOUT=300
                export LD_LIBRARY_PATH=\$LD_LIBRARY_PATH:/usr/local/lib
                export VLLM_HOST_IP=\$MY_TPU_IP
    
                if [ "\$LWS_WORKER_INDEX" = "0" ]; then
                  echo "Starting Ray Head..."
                  ray start --head --port=6379 --node-ip-address=\$MY_TPU_IP --resources='{"TPU": 4}' --block &
                  sleep 20
                  until ray status; do sleep 5; done
    
                  echo "Starting vLLM API Server..."
                  python3 -m vllm.entrypoints.openai.api_server \
                    --model=/models/qwen3-30b-weights \
                    --tensor-parallel-size=32 \
                    --pipeline-parallel-size=1 \
                    --distributed-executor-backend=ray \
                    --host=0.0.0.0 --port=8000 \
                    --enforce-eager \
                    --gpu-memory-utilization=0.90
                else
                  ray start --address=\${LEADER_IP}:6379 --node-ip-address=\$MY_TPU_IP --resources='{"TPU": 4}' --block
                fi
              ports:
              - containerPort: 8000
              - containerPort: 6379
              volumeMounts:
              - name: model-weights
                mountPath: /models
                readOnly: true
              - name: dshm
                mountPath: /dev/shm
              resources:
                claims:
                - name: net-resources
                limits:
                  google.com/tpu: 4
                  memory: "100Gi"
                requests:
                  google.com/tpu: 4
                  memory: "100Gi"
            nodeSelector:
              cloud.google.com/gke-tpu-accelerator: tpu-v6e-slice
              cloud.google.com/gke-tpu-topology: 4x8
              gke.networks.io/accelerator-network-profile: auto
            resourceClaims:
            - name: net-resources
              resourceClaimTemplateName: all-netdev
            volumes:
            - name: model-weights
              csi:
                driver: gcsfuse.csi.storage.gke.io
                volumeAttributes:
                  bucketName: ${BUCKET_NAME}
                  mountOptions: "implicit-dirs"
            - name: dshm
              emptyDir:
                medium: Memory
    EOF
    

10. استجابة اختبار النشر

قد يستغرق الأمر من 5 إلى 10 دقائق حتى تسحب جميع الحاويات في LeaderWorkerSet صور الحاويات، وتهيئ Ray، وتصبح Ready بالكامل. يمكنك تتبُّع الحالة من خلال مراقبة عملية تهيئة الحزمة:

kubectl get pods -l leaderworkerset.sigs.k8s.io/name=vllm-tpu-qwen -w

انتظِر إلى أن تعرض جميع وحدات vllm-tpu-qwen- STATUS الثماني Running وREADY 2/2، وتأكَّد من أنّ موازن التحميل قد تلقّى عنوان IP خارجيًا قبل المتابعة. قد تستغرق هذه العملية من 7 إلى 10 دقائق.

  1. استرداد عنوان IP الخارجي:
    export EXTERNAL_IP=$(kubectl get svc vllm-tpu-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
    echo $EXTERNAL_IP
    

تنبيه: في خدمة إنتاجية، يجب تأمين نقطة النهاية هذه باستخدام خدمة مثل Identity Aware Proxy (IAP).

  1. إرسال طلب استنتاج باستخدام curl:
        curl -N -s http://$EXTERNAL_IP:8000/v1/chat/completions \
            -H "Content-Type: application/json" \
            -d '{
                "model": "/models/qwen3-30b-weights",
                "messages": [{"role": "user", "content": "Write a haiku about high-performance computing on TPUs."}],
                "temperature": 0.7,
                "max_tokens": 100,
                "stream": true
            }' | sed 's/^data: //' | grep -v '\[DONE\]' | grep -v '^$' | jq -rj '.choices[0].delta.content // empty' ; echo ""
    
    من المفترض أن يظهر لك ناتج يشبه استجابة JSON تحتوي على النص الذي يمثّل الاستنتاج الذي تم إنشاؤه.

11. تَنظيم

لتجنُّب الرسوم المستمرة على حسابك على Google Cloud، احذف الموارد التي تم إنشاؤها أثناء هذا الدرس العملي.

  1. حذف مجموعة العُقد:
    gcloud container node-pools delete "${NODE_POOL_NAME}" \
        --cluster="${CLUSTER_NAME}" \
        --region="${REGION}" \
        --project="${PROJECT_ID}" --quiet
    
  2. حذف المجموعة:
    gcloud container clusters delete "${CLUSTER_NAME}" \
        --region="${REGION}" \
        --project="${PROJECT_ID}" --quiet
    
  3. حذف إعدادات الشبكة وجدار الحماية:
    gcloud compute firewall-rules delete \
        "${GVNIC_NETWORK_PREFIX}-allow-internal" \
        --project="${PROJECT_ID}" --quiet
    
    gcloud compute networks subnets delete "${GVNIC_NETWORK_PREFIX}-tpu" \
        --region="${REGION}" --quiet
    
    gcloud compute networks delete "${GVNIC_NETWORK_PREFIX}-main" --quiet
    
  4. إلغاء ربط حساب الخدمة وحذفه:
        # 1. Create the cleanup script
        cat << 'EOF' > clean_up_sa.sh
        #!/bin/bash
    
        # Validate that PROJECT_ID is available
        if [ -z "$PROJECT_ID" ]; then
          echo "Error: PROJECT_ID environment variable is not set."
          exit 1
        fi
    
        SA_EMAIL="tpu-reader-sa@${PROJECT_ID}.iam.gserviceaccount.com"
        SA_MEMBER="serviceAccount:${SA_EMAIL}"
    
        echo "Gathering IAM policy for ${SA_EMAIL}..."
    
        # Fetch roles assigned to this specific SA
        ROLES=$(gcloud projects get-iam-policy ${PROJECT_ID} \
            --flatten="bindings[].members" \
            --filter="bindings.members:${SA_MEMBER}" \
            --format="value(bindings.role)")
    
        if [ -z "$ROLES" ]; then
            echo "No IAM bindings found for this service account."
        else
            for ROLE in $ROLES; do
                echo "Removing binding for: ${ROLE}..."
                gcloud projects remove-iam-policy-binding ${PROJECT_ID} \
                    --member="${SA_MEMBER}" \
                    --role="${ROLE}" --quiet > /dev/null
            done
            echo "Successfully unbound all roles."
        fi
    
        # 2. Delete the service account itself
        echo "Deleting service account..."
        gcloud iam service-accounts delete ${SA_EMAIL} --project=${PROJECT_ID} --quiet
    
        echo "Cleanup complete."
        EOF
    
        # 2. Make the script executable and run it
        chmod +x clean_up_sa.sh
        ./clean_up_sa.sh
    
  5. حذف حزمة GCS انتقِل إلى Cloud Console، واختَر Cloud Storage -> الحِزم، ثمّ اختَر inf-demo-model-storage، وبعد ذلك اختَر "حذف".

12. تهانينا

تهانينا! لقد نشرت بنجاح حزمة vLLM متعددة المضيفين وعالية معدّل الاستدلال باستخدام Ray بشكلٍ أصلي على Google Kubernetes Engine.

ما تعلّمته

  • توفير مسارات مخصّصة مصمّمة لنقل بيانات TPU بسرعة عالية
  • تحديد أوزان التركيب باستخدام GCS Fuse وذاكرات التخزين المؤقت الإقليمية السريعة
  • تنظيم شرائح أحمال العمل المتعددة المضيفين التي تتم مزامنتها تلقائيًا من خلال LeaderWorkerSets
  • لمزيد من المعلومات، يُرجى الاطّلاع على دليل مستخدم vLLM وأدلة نشر llm-d.