סקירה כללית
במאמר הזה מוסבר איך לפרוס מודלי שפה גדולים (LLM) עם משקלים פתוחים כמו Gemma, Llama ו-DeepSeek בסביבות מבודדות (air-gapped) של Google Distributed Cloud (GDC). בשיעור הזה נסביר איך להשתמש ב-vLLM כדי לפרסם מודלים עם תפוקה גבוהה וב-Ollama כדי להקל על השימוש, תוך ניצול היכולות של פלטפורמת GDC, כולל Kubernetes, Harbor ומשאבי GPU.
ארכיטקטורה
הפתרון כולל פריסה של קצה עורפי (backend) להפעלת מודלים גדולים של שפה (LLM) במאגרי קונטיינרים (vLLM, Ollama) כפריסות בתוך אשכול משתמשים. משקלי המודלים מאוחסנים ב-Persistent Volumes, שאוכלסו מתמונות במאגר Harbor. שירותי Kubernetes מסוג LoadBalancer חושפים את ממשקי ה-API של ה-backends. מדיניות הרשת של הפרויקט מאבטחת את הגישה לשירותים האלה.

לפני שמתחילים
צריך לוודא שהתנאים המוקדמים הבאים מתקיימים:
- GDC עם air gap מגרסה 1.15.1 ואילך.
- נוצר אשכול משתמשים עם מספיק משאבים (CPU, זיכרון, GPU).
- נדרש לפחות GPU אחד מסוג NVIDIA A100.
- מופע Harbor זמין ונגיש.
- ממשקי ה-CLI
kubectlו-gdcloudמוגדרים לגישה לאשכול המשתמשים. - לקוח Docker מותקן ומוגדר להעברה בדחיפה אל Harbor.
- ההרשאות הנדרשות ב-IAM הוענקו (לדוגמה, Namespace Admin, Cluster Developer**).
- אם משתמשים במודלים עם גישה מוגבלת, צריך להגדיר חשבון ואימות ב-Hugging Face.
חלק 1: הגדרה נפוצה
1.1 יצירת סוד למשיכת תמונה
כדי להגדיר סוד למשיכת תמונה עבור עומס עבודה של קונטיינר ב-GDC עם air gap, צריך ליצור סוד של Kubernetes docker-registry שמכיל פרטי כניסה לגישה לפרויקט הפרטי של Harbor. הסוד הזה מוזכר במפרט הפריסה.
מומלץ להשתמש בחשבון רובוט של Harbor לגישה פרוגרמטית לתמונות בפרויקטים פרטיים של Harbor.
כדי להגדיר את סוד משיכת התמונה:
יצירת חשבון רובוט של Harbor:
- עוברים לממשק המשתמש של מופע Harbor.
- עוברים לפרויקט Harbor.
- לוחצים על הכרטיסייה חשבונות רובוט.
- לוחצים על New Robot Account (חשבון רובוט חדש).
- נותנים לו שם (לדוגמה,
oss-llm-puller) ומעניקים לו את ההרשאות הנדרשות (לפחות גישת pull) עד למועד התפוגה. - שומרים בצורה מאובטחת את שם חשבון הרובוט (לדוגמה,
robot$oss-llm-puller) ואת האסימון הסודי שסופק.
אימות Docker ב-Harbor:
במחשב שבו מותקן Docker ויש גישה לרשת לרישום של Harbor, נכנסים באמצעות פרטי הכניסה של חשבון הרובוט:
export INSTANCE_URL="HARBOR_INSTANCE_URL"
# for example, harbor1-project1.org1.zone1.google.gdc.com
export ROBOT_NAME="ROBOT_ACCOUNT_NAME"
# for example, robot\$oss-llm-puller (note how we escape the $ character)
export ROBOT_SECRET="ROBOT_ACCOUNT_SECRET"
docker login ${INSTANCE_URL} --username ${ROBOT_NAME} --password ${ROBOT_SECRET}
יוצרים את סוד המשיכה של תמונת Kubernetes:
משתמשים ב-kubectl כדי ליצור סוד מסוג docker-registry במרחב השמות של הפרויקט, באמצעות קובץ ההגדרות של Docker שעודכן בשלב הקודם:
# Log in into GDC environment using the next commands
gdcloud auth login --login-config-cert WEB_TLS_CERT_PATH
gdcloud clusters get-credentials KUBERNETES_CLUSTER
kubectl config set-context --current --namespace=NAMESPACE
export SECRET_NAME="OSS_LLM_PULL_SECRET"
export NAMESPACE="PROJECT_NAMESPACE"
# Assuming default Docker config path. Adjust if necessary.
export DOCKER_CONFIG_PATH="$HOME/.docker/config.json"
kubectl create secret docker-registry ${SECRET_NAME} \
--from-file=.dockerconfigjson=${DOCKER_CONFIG_PATH} \
-n ${NAMESPACE}
חלק 2: פריסה באמצעות vLLM
2.1 מקבלים את קובץ האימג' של Docker של vLLM
במחשב עם גישה לאינטרנט, מושכים את קובץ האימג' של vLLM Docker ומעבירים אותו לפרויקט Harbor:
# Pull and Tag vLLM (v0.13.0 recommended for stability)
docker pull vllm/vllm-openai:v0.13.0
docker tag vllm/vllm-openai:v0.13.0 HARBOR_URL/PROJECT/vllm-openai:v0.13.0
docker push HARBOR_URL/PROJECT/vllm-openai:v0.13.0
מחליפים את HARBOR_URL ואת PROJECT בכתובת ה-URL של מופע Harbor ובשם הפרויקט.
2.2 הכנת משקלי המודל ב-PVC
יוצרים קובץ YAML (לדוגמה, model-pvc.yaml):
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: model-pvc
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Gi
storageClassName: standard-rwo
volumeMode: Filesystem
החלת ה-PVC: kubectl apply -f model-pvc.yaml
הורדת משקלים מ-Hugging Face:
hf auth login
hf download google/gemma-3-4b-it
משתמשים ב-Pod עזר (לדוגמה, helper-pod.yaml) כדי להעביר משקלים ל-PVC.
מוודאים שיש לכם תמונת busybox ב-Harbor.
# Push busybox if not present
docker pull busybox:latest
docker tag busybox HARBOR_URL/PROJECT/busybox:latest
docker push HARBOR_URL/PROJECT/busybox:latest
# Contents of helper-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: model-uploader
spec:
containers:
- name: uploader
image: HARBOR_URL/PROJECT/busybox:latest
command: ["sleep", "3600"]
volumeMounts:
- name: model-data
mountPath: /data
imagePullSecrets:
- name: oss-llm-pull-secret
volumes:
- name: model-data
persistentVolumeClaim:
claimName: model-pvc
החלת ה-POD והעתקת קבצים:
kubectl apply -f helper-pod.yaml
# Wait for pod to be Running
kubectl cp ~/.cache/huggingface/hub/ NAMESPACE/model-uploader:/data/
kubectl delete pod model-uploader
2.3 פריסת קצה עורפי של vLLM
יוצרים את קובץ הפריסה vllm-gemma-3-4b-it-deployment.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: gemma-3-4b-it
labels:
app: gemma-3-4b-it
spec:
replicas: 1
selector:
matchLabels:
app: gemma-3-4b-it
template:
metadata:
labels:
app: gemma-3-4b-it
spec:
volumes:
- name: cache-volume
persistentVolumeClaim:
claimName: model-pvc
- name: shm
emptyDir:
medium: Memory
sizeLimit: "16Gi"
containers:
- name: gemma-3-4b-it
image: HARBOR_URL/PROJECT/vllm-openai:v0.13.0
command: ["python3"]
args: [
"-m",
"vllm.entrypoints.openai.api_server",
"--model",
"google/gemma-3-4b-it",
"--max-model-len",
"32768",
"--enforce-eager"
]
env:
- name: HF_HUB_OFFLINE
value: "1"
- name: HF_HOME
value: "/model"
- name: NCCL_P2P_DISABLE
value: "1"
- name: NCCL_IB_DISABLE
value: "1"
- name: BORINGSSL_FIPS
value: "0"
- name: OPENSSL_FIPS
value: "0"
- name: OPENSSL_CONF
value: "/dev/null"
- name: FIPS_SIG
value: "off"
ports:
- containerPort: 8000
securityContext:
privileged: true
runAsUser: 0
resources:
limits:
nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
cpu: "8"
memory: "64Gi"
requests:
nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
cpu: "8"
memory: "32Gi"
volumeMounts:
- name: cache-volume
mountPath: /model
- name: shm
mountPath: /dev/shm
imagePullSecrets:
- name: oss-llm-pull-secret
יוצרים את קובץ השירות vllm-gemma-3-4b-it-service.yaml:
apiVersion: v1
kind: Service
metadata:
name: gemma-3-4b-it
namespace: NAMESPACE
spec:
ports:
- name: http-gemma-3-4b-it
port: 80
protocol: TCP
targetPort: 8000
selector:
app: gemma-3-4b-it
sessionAffinity: None
type: LoadBalancer
מחילים את ההגדרות:
kubectl apply -f vllm-gemma-3-4b-it-deployment.yaml
kubectl apply -f vllm-gemma-3-4b-it-service.yaml
2.4 הגדרת מדיניות רשת ב-Kubernetes
מחילים משאב ProjectNetworkPolicy כדי לאפשר תעבורת נתונים נכנסת (ingress) ליציאת השירות (8000) של vLLM. יצירת vllm-netpol.yaml:
apiVersion: networking.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
name: allow-vllm-ingress
namespace: NAMESPACE
spec:
subject:
subjectType: UserWorkload
policyType: Ingress
ingress:
- from:
- ipBlock:
cidr: 0.0.0.0/0 # Restrict this in production
ports:
- protocol: TCP
port: 8000
החלת המדיניות: kubectl apply -f vllm-netpol.yaml
חלק 3: פריסה באמצעות Ollama
3.1 הכנת Dockerfile
יוצרים Dockerfile כדי ליצור את קובץ האימג' של Ollama עם המודלים שלכם שנטענו מראש:
FROM ubuntu
RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates zstd
RUN curl -fsSL https://ollama.com/install.sh -o install.sh
RUN chmod +x install.sh
RUN ./install.sh && \
rm -rf /var/lib/apt/lists/*
# Pre-pull gemma3 model
RUN ollama serve & \
sleep 5 && \
curl --retry 10 --retry-connrefused -s http://localhost:11434 || true && \
ollama pull gemma3:latest && \
pkill ollama || true
EXPOSE 11434
CMD ["ollama", "serve"]
3.2 יצירה ודחיפה של תמונה
יוצרים את האימג' ומעבירים אותו:
docker build -t ollama-gemma3 .
docker tag ollama-gemma3 HARBOR_URL/PROJECT/ollama-gemma3:latest
docker push HARBOR_URL/PROJECT/ollama-gemma3:latest
3.3 פריסת קצה עורפי של Ollama
יצירת ollama-gemma3.yaml:
apiVersion: apps/v1
kind: Deployment
metadata:
name: ollama-gemma3
namespace: NAMESPACE
labels:
app: ollama-gemma3
spec:
replicas: 1
selector:
matchLabels:
app: ollama-gemma3
template:
metadata:
labels:
app: ollama-gemma3
spec:
containers:
- name: ollama-gemma3
image: HARBOR_URL/PROJECT/ollama-gemma3:latest
env:
- name: OLLAMA_HOST
value: "0.0.0.0"
imagePullPolicy: Always
ports:
- containerPort: 11434
securityContext:
privileged: true
runAsUser: 0
resources:
limits:
nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
requests:
nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
imagePullSecrets:
- name: oss-llm-pull-secret
---
apiVersion: v1
kind: Service
metadata:
name: ollama-gemma3
namespace: NAMESPACE
spec:
type: LoadBalancer
selector:
app: ollama-gemma3
ports:
- name: ollama-gemma3-port
port: 11434
protocol: TCP
targetPort: 11434
החלת המניפסט: kubectl apply -f ollama-gemma3.yaml
3.4 הגדרת מדיניות רשת ב-Kubernetes
יצירת ollama-netpol.yaml:
apiVersion: networking.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
name: allow-ollama-ingress
namespace: NAMESPACE
spec:
subject:
subjectType: UserWorkload
policyType: Ingress
ingress:
- from:
- ipBlock:
cidr: 0.0.0.0/0 # Restrict this for production.
ports:
- protocol: TCP
port: 11434
החלת המדיניות: kubectl apply -f ollama-netpol.yaml
סעיף 4: אימות
כדי לאמת את הפריסות, בודקים את הסטטוסים של ה-pods, את כתובות ה-IP של השירות ושולחים בקשות בדיקה להסקת מסקנות באמצעות curl לכתובות ה-IP של LoadBalancer גם עבור vLLM וגם עבור Ollama.
בודקים שכל המאגרים והשירותים הם Running:
# Login into GDC air-gapped using the next commands
gdcloud auth login --login-config-cert WEB_TLS_CERT_PATH
gdcloud clusters get-credentials KUBERNETES_CLUSTER
kubectl config set-context --current --namespace=NAMESPACE
# Pods
kubectl get pods
# Services
kubectl get services
בדיקת vLLM:
export VLLM_IP=$(kubectl get service gemma-3-4b-it -n NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[*].ip}')
curl http://${VLLM_IP}/v1/chat/completion \
-H "Content-Type: application/json" \
-d '{
"model": "google/gemma-3-4b-it",
"messages": [
{"role": "user", "content": "What is Google Distributed Cloud air-gapped?"}
],
"max_tokens": 100
}'
בדיקת Ollama:
export OLLAMA_IP=$(kubectl get service ollama-gemma3 -n NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[*].ip}')
# Check if Ollama is running
curl http://${OLLAMA_IP}:11434
# Send a completion request
curl -X POST http://${OLLAMA_IP}:11434/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gemma3:latest",
"prompt": "Google Distributed Cloud air-gapped is a",
"max_tokens": 128,
"temperature": 0.90,
"stream": false
}'
סעיף 5: פעולות ופתרון בעיות
5.1 פעולות vLLM
בדיקת היומנים: kubectl logs -f -n NAMESPACE
שאילתה לגבי סטטוס פנימי (מ-pod לניפוי באגים):
wget -qO- http://gemma-3-4b-it/v1/models
wget -qO- http://gemma-3-4b-it/health
5.2 פעולות Ollama
גישה ל-CLI: kubectl exec -it -n NAMESPACE -- sh
בתוך הפוד: ollama list, ollama ps
5.3 התאמה לעומס (scaling)
התאמת הפתרון באמצעות קצה עורפי של Ollama
אנכית
- כדאי להקצות פרוסה גדולה יותר של GPU למודל שפה גדול שהפך לצוואר בקבוק, עד שתשתמשו ב-GPU מלא.
- אם רוצים להשתמש ב-LLM גדול יותר כדי לקבל תשובות מדויקות יותר, למשל LLM עם 405 מיליארד פרמטרים במקום 7 מיליארד, יכול להיות שתצטרכו יותר מ-GPU אחד כדי להריץ אותו בצורה חלקה.
- מודלים שמתאימים ליותר מיחידת GPU אחת חווים השהיה שקשורה לתקשורת בין יחידות ה-GPU.
אופקית
- פורסים כמה פודים של Ollama שצריך כדי להשיג את קצב העברת הנתונים הרצוי במודל שפה גדול (LLM) נתון.
- כדי לעשות את זה, מגדילים את מספר העותקים בקובץ ה-YAML של פריסת Ollama המתאים.
- שירות Kubernetes, מסוג LoadBalancer, יפיץ בקשות לעזרה בכתיבת קוד בין נקודות קצה (כלומר, pods) ויחזיר את התגובות המתאימות דרך כתובת ה-IP החיצונית שנחשפת.
- חשוב לזכור שהתוסף Continue מצביע רק על כתובת IP אחת לכל פונקציונליות.

כדי לשנות את קנה המידה של קצה העורפי (backend) של vLLM בסביבת GDC עם בידוד פיזי, אפשר לפעול לפי אסטרטגיה דומה לזו שמשמשת ל-Ollama, ולהתמקד בהקצאת משאבי חומרה ובשכפול של פודים.
הרחבת הפתרון באמצעות קצה עורפי של vLLM
אנכית
- שדרוג הקצאת ה-GPU: אם קצב העברת הנתונים של ההסקה (טוקנים/שנייה) הופך לצוואר בקבוק, צריך להקצות פרוסת GPU גדולה יותר עד לשימוש ב-GPU מלא של NVIDIA A100.
- תצורות של כמה מעבדי GPU: כדי להשתמש במודלים גדולים מאוד (לדוגמה, פרמטרים של 70B עד 405B) שלא נכנסים לזיכרון של A100 יחיד בנפח 80GB, צריך להגדיל את מספר מעבדי ה-GPU באמצעות מקביליות טנסורית.
- שיקולים לגבי זמן האחזור: שימו לב שבדגמים שמשתרעים על יותר מ-GPU אחד יכול להיות שתהיה תקורה קלה שקשורה לתקשורת בין ה-GPU (לדוגמה, סנכרון NCCL).
אופקית
- הגדלת קצב העברת הנתונים באמצעות רפליקות: כדי לטפל בנפח גבוה יותר של בקשות משתמשים בו-זמניות עבור אותו מודל, מגדילים את מספר הרפליקות ב-YAML של פריסת vLLM.
- מופעים ייעודיים של מודלים: מכיוון ש-vLLM מתוכנן כמנוע להצגת מודל יחיד ומצמיד את זיכרון מטמון KV שלו במהלך האתחול, צריך לפרוס קבוצה נפרדת של פודים לכל LLM שרוצים לארח.
- איזון עומסים: שירות GDC Kubernetes (סוג LoadBalancer) יפיץ באופן אוטומטי בקשות הסקה נכנסות בין כל נקודות הקצה (endpoints) של vLLM pod שמשויכות לשירות הזה.
5.4 פתרון בעיות
שגיאות נפוצות ופתרונות.
| שגיאה | צמצום הפגיעה |
|---|---|
| FIPS SELFTEST FAILURE | השגיאה הזו מתרחשת כשחסרים חתימות שלמות בספריות כמו BoringSSL. תיקון באמצעות הגדרת BORINGSSL_FIPS=0 ושימוש בתמונות vLLM רשמיות |
| טעינת משקל מושהית | בודקים אם יש הגבלת IOPS ב-PVC קטנים. נדרש נפח אחסון של 500GiB כדי לאתחל את מודל הביצועים. |
| החיבור נדחה | מוודאים ש-PNP מאפשר באופן מפורש את targetPort (8000/8080). חומות האש של GDC לא מעניקות אוטומטית גישה ליציאות של השרתים העורפיים עבור כתובות ה-VIP של מאזן העומסים. |