במאמר הזה מוסבר שלב אחרי שלב איך פורסים מודלים פתוחים כמו Gemma, Llama ו-DeepSeek בסביבות מנותקות של Google Distributed Cloud (GDC). המדריך כולל הסברים על השימוש ב-Ollama כדי להקל על השימוש, ועל השימוש ביכולות של פלטפורמת GDC, כולל Kubernetes, Harbor ומשאבי מאיצים.
ארכיטקטורה
הפתרון כולל פריסה של Ollama כ-backend להסקת מסקנות בקונטיינר (Deployment) באשכול רגיל. משקלי המודלים מאוחסנים בנפחי אחסון קבועים, שהגודל שלהם נקבע באופן דינמי לפי ספריית המודלים שהורדתם עם תקורה של 25%. הם מוגדרים באמצעות מדיניות ה-QoS של האחסון system-performance-<org>-admin (30 פעולות קלט/פלט לשנייה לכל GiB עם רצפה מינימלית של 6,148 פעולות קלט/פלט לשנייה, או performance-rwo אם מופעל אחסון בלוקים עם ביצועים גבוהים), ומאוכלסים באמצעות תהליך עבודה של טעינת מודלים. Service של Kubernetes מסוג LoadBalancer חושפים את ה-API לקצה העורפי, ואובייקטים מסוג NetworkPolicy או ProjectNetworkPolicy מאבטחים את הגישה אליו.

לפני שמתחילים
לפני שמתחילים בפריסה, חשוב לוודא שהסביבה עומדת בכל הדרישות המוקדמות, ושכלי שורת הפקודה הנדרשים מוגדרים כמו שצריך. חשוב להגדיר את הכלים האלה בתחנת העבודה כדי לנהל את Container Registry, לתקשר עם אשכולות ולהפוך את תהליך הפריסה לאוטומטי.
- אשכול רגיל שנוצר עם מספיק משאבים (מעבד, זיכרון, מאיץ). הדרישות המינימליות למאיץ משתנות בהתאם למודל.
- מכונה של Harbor זמינה ונגישה.
- ה-IAM הנדרש הוחל.
- תחנת עבודה עם הקישוריות הנדרשת לסביבה ולאינטרנט
הגדרת הסביבה
ניהול זהויות והרשאות גישה
מוודאים שהחשבונות, התפקידים וההרשאות הנדרשים ב-IAM מוגדרים כמו שצריך.
תפקידי GDC User בפרויקט (RoleBinding במרחב השמות של הפרויקט, שניתנים על ידי אדמין IAM של הפרויקט):
- צפייה במכונות של Harbor (
harbor-instance-viewer) - יוצר פרויקט Harbor (
harbor-project-creator, רק אם פרויקט Harbor עדיין לא קיים) - אדמין של אשכול רגיל (
standard-cluster-admin, נדרש ל-gdcloud clusters get-credentials)
התפקיד GDC User באשכול הרגיל: התפקידים הקודמים בפרויקט לא מעניקים הרשאות בתוך האשכול. בנוסף, אדמין IAM של הפרויקט צריך לקשר את המשתמש ל-StandardClusterRole cluster-admin באמצעות StandardClusterRoleBinding במרחב השמות של הפרויקט בשרת ה-API לניהול. הקישור מופץ לאשכולות הרגילים של הפרויקט תוך כמה שניות (status.clusters[].conditions מציג את Propagated=True).
cat <<EOF | kubectl --kubeconfig MANAGEMENT_API_SERVER apply -f -
apiVersion: iam.gdc.goog/v1
kind: StandardClusterRoleBinding
metadata:
name: BINDING_NAME
namespace: PROJECT
spec:
roleRef:
apiGroup: iam.gdc.goog
kind: StandardClusterRole
name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: USER
EOF
מחליפים את מה שכתוב בשדות הבאים:
-
MANAGEMENT_API_SERVER: הנתיב לקובץ kubeconfig של שרת ה-API לניהול. -
BINDING_NAME: שם לקישור, למשלalex-cluster-admin(אותיות קטנות, ספרות ומקפים). -
PROJECT: שם הפרויקט. -
USER: שם המשתמש שרוצים לקשר, בדיוק כמו שהוא מדווח על ידי ספק הזהויות (לדוגמה, כתובת אימייל).
סיוע של מפעיל התשתית (רק כש-performance-rwo לא זמין באשכול הרגיל): המדריכים למשתמש יוצרים את המודל PersistentVolumeClaim עם ברירת המחדל של StorageClass standard-rwo, שמגבילה את נפחי המודלים בגודל המתאים ל-3 IOPS לכל GiB. מבקשים ממפעיל התשתית להחיל את מדיניות ה-QoS של system-performance-<ORG>-admin על אמצעי האחסון שמגבה את המודל PersistentVolumeClaim, כדי שהוא יקבל את הרף התחתון של 6,148 IOPS. במדריכים למשתמש מודפסים הערכים שמפעיל התשתית צריך.
הרשאות של חשבון הרובוט של Harbor לפרויקט crane:
- מאגר רשימות
- מאגר שליפה
- מאגר Push
- קריאת ארטיפקט
- רשימת פריטי מידע שנוצרו בתהליך הפיתוח (Artifact)
- יצירת תג
- תג רשימה
הרשאות של חשבון הרובוט של Harbor לשליפת קובצי אימג' של Kubernetes:
- מאגר רשימות
- מאגר שליפה
- קריאת ארטיפקט
- רשימת פריטי מידע שנוצרו בתהליך הפיתוח (Artifact)
- תג רשימה
תחנת עבודה
אלא אם צוין אחרת, צריך להריץ את כל הפקודות במדריך הזה מתחנת עבודה עם הכלים הנדרשים וקישוריות לסביבה ולאינטרנט.
דרישות
צריך להתקין את הכלים הבאים בתחנת העבודה:
-
crane: ניהול והעתקה של קובצי אימג' של קונטיינרים בין מאגרים (מסמכי תיעוד). -
gdcloud: ממשק שורת הפקודה (CLI) לניהול משאבי GDC (מסמכי תיעוד). -
kubectl: ממשק שורת הפקודה (CLI) שמשמש לתקשורת עם אשכול Kubernetes ולניהול שלו. -
jq: מעבד JSON קל וגמיש בשורת הפקודה. -
ollama: כלי בקוד פתוח שמאפשר להוריד ולהריץ מודלים. -
yq: מעבד YAML נייד של שורת הפקודה.
הגדרת תחנת עבודה
כדי להגדיר את סביבת העבודה, צריך את הפרטים הבאים על הסביבה:
-
GDC_STANDARD_CLUSTER_NAME: השם של אשכול GDC רגיל. -
GDC_DOMAIN_SUFFIX: סיומת הדומיין של סביבת GDC (לדוגמה,gdc.example.com). -
GDC_ORG: השם של הארגון ב-GDC. -
GDC_PROJECT: השם של הפרויקט ב-GDC. -
GDC_ZONE: השם של אזור הפריסה של GDC. -
GDC_HARBOR_INSTANCE_NAME: השם של מכונת Harbor בפרויקט. -
GDCS_HARBOR_PROJECT_NAME: השם של פרויקט Harbor שבו משתמשים לתמונות (ברירת המחדל:solutions). -
GDCS_HARBOR_CRANE_ROBOT_NAME: השם של חשבון הרובוט של Harborcrane. -
GDCS_HARBOR_CRANE_ROBOT_TOKEN: אסימון האימות של חשבון הרובוט של Harborcrane. -
GDCS_HARBOR_K8S_ROBOT_NAME: השם של חשבון הרובוט לשליפת קובצי אימג' של Kubernetes ב-Harbor. -
GDCS_HARBOR_K8S_ROBOT_TOKEN: אסימון האימות של חשבון הרובוט לשליפת קובצי אימג' של Kubernetes ב-Harbor.
אחרי שאספתם את הערכים של כל המשתנים הנדרשים, אתם יכולים ליצור את קובץ משתני הסביבה. אחרי שהקובץ נוצר, אפשר לערוך אותו ידנית בכל שלב.
יוצרים את תיקיות השורש של הפתרון ואת תיקיית הסודות:
mkdir -p ${HOME}/gdcag-solutions/env.d mkdir -p ${HOME}/gdcag-solutions/secrets touch ${HOME}/gdcag-solutions/secrets/harbor_crane_robot_token touch ${HOME}/gdcag-solutions/secrets/harbor_k8s_robot_token chmod u=rwx,go= ${HOME}/gdcag-solutions/secrets chmod -R u=rw,go= ${HOME}/gdcag-solutions/secrets/*יוצרים את קובץ התצורה של סביבת הפלטפורמה:
cat << 'EOF' > ${HOME}/gdcag-solutions/env.d/platform.sh && echo "Successfully created." || echo "Failed to create!" # Infrastructure (Platform Native) export GDC_STANDARD_CLUSTER_NAME="STANDARD_CLUSTER_NAME" export GDC_DOMAIN_SUFFIX="DOMAIN_SUFFIX" export GDC_ORG="ORG" export GDC_PROJECT="PROJECT" export GDC_ZONE="ZONE" export GDC_HARBOR_INSTANCE_NAME="HARBOR_INSTANCE_NAME" # Derived platform values export GDC_ZONAL_HOSTNAME="${GDC_ORG}.${GDC_ZONE}.${GDC_DOMAIN_SUFFIX}" export GDC_ZONAL_CONSOLE_URL="https://console.${GDC_ZONAL_HOSTNAME}" export GDC_HARBOR_HOST="${GDC_HARBOR_INSTANCE_NAME}-${GDC_PROJECT}.${GDC_ORG}.${GDC_ZONE}.${GDC_DOMAIN_SUFFIX}" EOFמחליפים את מה שכתוב בשדות הבאים:
-
STANDARD_CLUSTER_NAME: שם האשכול הרגיל של GDC. -
DOMAIN_SUFFIX: סיומת הדומיין של GDC. -
ORG: ארגון ב-GDC. -
PROJECT: פרויקט GDC. -
ZONE: אזור GDC. -
HARBOR_INSTANCE_NAME: השם של מכונת GDC Harbor.
-
יוצרים את קובץ התצורה של סביבת המרשם:
cat << 'EOF' > ${HOME}/gdcag-solutions/env.d/registry.sh && echo "Successfully created." || echo "Failed to create!" # GDC Solutions Registry & Secrets export GDCS_HARBOR_PROJECT_NAME="solutions" export GDCS_HARBOR_CRANE_ROBOT_NAME='HARBOR_CRANE_ROBOT_NAME' export GDCS_HARBOR_CRANE_ROBOT_TOKEN="$(cat ${GDCS_ROOT_HOME}/secrets/harbor_crane_robot_token)" export GDCS_HARBOR_K8S_ROBOT_NAME='HARBOR_K8S_ROBOT_NAME' export GDCS_HARBOR_K8S_ROBOT_TOKEN="$(cat ${GDCS_ROOT_HOME}/secrets/harbor_k8s_robot_token)" export GDCS_HARBOR_K8S_PULL_SECRET="gdcs-image-pull-secret" # Derived registry values export GDCS_HARBOR_PROJECT_URI="${GDC_HARBOR_HOST}/${GDCS_HARBOR_PROJECT_NAME}" export GDCS_HARBOR_CHART_OCI_URI="oci://${GDCS_HARBOR_PROJECT_URI}" EOFמחליפים את מה שכתוב בשדות הבאים:
-
HARBOR_CRANE_ROBOT_NAME: השם המלא של חשבון הרובוט של Harbor בשבילcrane, כמו שהוא מוצג ב-Harbor, כולל הקידומתrobot$. חשוב להשאיר את המירכאות הבודדות כדי שהמעטפת לא תרחיב את$. -
HARBOR_K8S_ROBOT_NAME: השם המלא של חשבון הרובוט של Harbor לשליפת קובצי אימג' של Kubernetes, כמו שהוא מוצג ב-Harbor, כולל הקידומתrobot$. חשוב להשאיר את הגרשיים הבודדים כדי שהמעטפת לא תרחיב את$.
-
מוסיפים את הטוקנים לקובצי הסודות:
set +o history echo "CRANE_ROBOT_TOKEN" > ${HOME}/gdcag-solutions/secrets/harbor_crane_robot_token echo "KUBERNETES_ROBOT_TOKEN" > ${HOME}/gdcag-solutions/secrets/harbor_k8s_robot_token set -o historyמחליפים את מה שכתוב בשדות הבאים:
-
CRANE_ROBOT_TOKEN: הסוד של חשבון הרובוטcrane. -
KUBERNETES_ROBOT_TOKEN: הסוד של חשבון הרובוט לשליפת קובצי אימג' של Kubernetes.
-
יוצרים את קובץ טעינת הסביבה ברמה הבסיסית (root):
cat << 'EOF' > ${HOME}/gdcag-solutions/env.sh && echo "Successfully created." || echo "Failed to create!" export GDCS_ROOT_HOME="${HOME}/gdcag-solutions" echo "GDCS_ROOT_HOME=${GDCS_ROOT_HOME}" # Sourced in dependency order source "${GDCS_ROOT_HOME}/env.d/platform.sh" source "${GDCS_ROOT_HOME}/env.d/registry.sh" EOF
הגדרת משתני הפתרון
יוצרים את ספריית ההטמעה של הפתרון:
mkdir -p ${HOME}/gdcag-solutions/open-weight-models/ollama/env.dיוצרים את קובץ התצורה של המאיץ:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/ollama/env.d/accelerator.sh && echo "Successfully created." || echo "Failed to create!" # Workload Accelerator (NVIDIA GPU) export GDCS_WORKLOAD_ACCELERATOR_TYPE="WORKLOAD_ACCELERATOR_TYPE" export GDCS_WORKLOAD_ACCELERATOR_COUNT="1" EOFמחליפים את מה שכתוב בשדות הבאים:
-
WORKLOAD_ACCELERATOR_TYPE: שם המשאב של ה-GPU שצומתי ה-GPU חושפים, לדוגמהnvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE. במדריכים למשתמשים מפורטים שמות המשאבים של כל ה-GPU שזמינים ב-GDC עם air gap. אחרי שמאחזרים את פרטי הכניסה לאשכול, אפשר להציג את השמות באשכול באמצעותkubectl get nodes --output=json | jq -r '[.items[].status.allocatable | keys[] | select(startswith("nvidia.com/"))] | unique[]'.
-
יוצרים את קובץ התצורה של סביבת הפתרון:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/ollama/env.d/ollama.sh && echo "Successfully created." || echo "Failed to create!" # Images export GDCS_BUSYBOX_IMAGE_TAG="1.38.0" export GDCS_OLLAMA_IMAGE_TAG="0.32.14" # Docker configuration directories for crane and Kubernetes export GDCS_HARBOR_CRANE_DOCKER_CONFIG="${GDCS_IMPLEMENTATION_HOME}/docker/crane" export GDCS_HARBOR_K8S_DOCKER_CONFIG="${GDCS_IMPLEMENTATION_HOME}/docker/k8s" EOFיוצרים את קובץ הטעינה של סביבת ההטמעה:
cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/ollama/env.sh && echo "Successfully created." || echo "Failed to create!" source "${HOME}/gdcag-solutions/env.sh" export GDCS_IMPLEMENTATION_HOME="${HOME}/gdcag-solutions/open-weight-models/ollama" echo "GDCS_IMPLEMENTATION_HOME=${GDCS_IMPLEMENTATION_HOME}" # Sourced in dependency order source "${GDCS_IMPLEMENTATION_HOME}/env.d/accelerator.sh" source "${GDCS_IMPLEMENTATION_HOME}/env.d/ollama.sh" EOFעורכים את קובצי הסביבה ובודקים אותם בעורך המועדף:
${EDITOR:-vi} ${HOME}/gdcag-solutions/env.d/platform.sh ${EDITOR:-vi} ${HOME}/gdcag-solutions/env.d/registry.sh ${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/ollama/env.d/accelerator.sh ${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/ollama/env.d/ollama.shמפעילים את קובץ הסביבה:
source ${HOME}/gdcag-solutions/open-weight-models/ollama/env.shהפלט אמור להיראות כך:
GDCS_ROOT_HOME=HOME_DIRECTORY_PATH/gdcag-solutions GDCS_IMPLEMENTATION_HOME=HOME_DIRECTORY_PATH/gdcag-solutions/open-weight-models/ollama
GDC
במדריך הזה אנחנו מניחים שתחנת העבודה שלכם מוגדרת כך שהיא נותנת אמון באישורי ה-TLS של סביבת GDC ושל מכונת Harbor.
הגדרת
gdcloud:gdcloud config set core/account "default-user" gdcloud config set core/organization_console_url "${GDC_ZONAL_CONSOLE_URL}" gdcloud config set core/project "${GDC_PROJECT}" gdcloud config set core/zone "${GDC_ZONE}"מאמתים את סביבת GDC:
gdcloud auth login
אשכול
אחזור פרטי הכניסה של האשכול:
gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \ --project="${GDC_PROJECT}" \ --standard \ --zone="${GDC_ZONE}"מאמתים את הקישוריות לאשכול:
kubectl get nodes -L node.cluster.private.gdc.goog/machine-class
הכנות לקראת העברת פריטי המידע שנוצר בתהליך הפיתוח (Artifact)
- בודקים את הקישוריות וההגדרות של מאגר פריטי המידע שנוצרו בתהליך הפיתוח (Artifact Registry).
יוצרים קובץ תצורה של Docker ל-crane. חשבון רובוט משמש לדחיפת שכבות גדולות של קובצי אימג' כדי למנוע זמן קצוב לתפוגה של אסימון אימות, כשמשתמשים בכלי העזר לפרטי כניסה של Managed Harbor Service (MHS) (docker-credential-mhs) עם חשבון משתמש:
export DOCKER_CONFIG="${GDCS_HARBOR_CRANE_DOCKER_CONFIG}" crane auth login "${GDC_HARBOR_HOST}" \ --password="${GDCS_HARBOR_CRANE_ROBOT_TOKEN}" \ --username="${GDCS_HARBOR_CRANE_ROBOT_NAME}"יוצרים קובץ תצורה של Docker ל-Kubernetes:
export DOCKER_CONFIG="${GDCS_HARBOR_K8S_DOCKER_CONFIG}" crane auth login "${GDC_HARBOR_HOST}" \ --password="${GDCS_HARBOR_K8S_ROBOT_TOKEN}" \ --username="${GDCS_HARBOR_K8S_ROBOT_NAME}"יוצרים את הסקריפט
seed_registry.sh:cat << 'EOF' > ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh && echo "Successfully created." || echo "Failed to create!" #!/bin/bash # seed_registry.sh: Modular artifact migration for GDC Solutions # Requires the env.sh file to be sourced first. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set -o nounset source "${SCRIPT_DIR}/env.sh" # Set the Docker config export DOCKER_CONFIG="${GDCS_HARBOR_CRANE_DOCKER_CONFIG}" eval "${SERIALIZED_IMAGES}" # Ensure GDCS_REGISTRY_IMAGES is set if [[ ${#GDCS_REGISTRY_IMAGES[@]} -eq 0 ]]; then echo "GDCS_REGISTRY_IMAGES must be set, exiting..." exit 1 fi # Migrate the images for source_image in "${GDCS_REGISTRY_IMAGES[@]}"; do # Strip the registry host only when the first path segment is a host (contains a dot or a port) first_segment="${source_image%%/*}" if [[ "${first_segment}" == *.* || "${first_segment}" == *:* ]]; then image_path="${source_image#*/}" else image_path="${source_image}" fi destination_image="${GDCS_HARBOR_PROJECT_URI}/${image_path}" # Ensure the folder structure is created crane append \ --new_layer=<(tar czf - -T /dev/null) \ --new_tag="${destination_image%:*}:create" \ --oci-empty-base 2> /dev/null || true # Copy the linux/amd64 platform only to avoid transferring multi-arch layers over air-gapped links crane copy --platform linux/amd64 "${source_image}" "${destination_image}" || { echo "Failed to copy ${source_image} to ${destination_image}"; exit 1; } done echo "Migration complete: Images are available at ${GDCS_HARBOR_PROJECT_URI}" EOF chmod u+x "${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh"מגדירים את רשימת קובצי האימג' של הקונטיינרים שנדרשים לפתרון:
declare -a GDCS_REGISTRY_IMAGES=( "docker.io/library/busybox:${GDCS_BUSYBOX_IMAGE_TAG}" "docker.io/ollama/ollama:${GDCS_OLLAMA_IMAGE_TAG}" ) export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES)מזינים את קובצי האימג' הנדרשים של הקונטיינר ל-Artifact Registry:
${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
הכנת המודל
יוצרים את הסקריפט
ollama_model_weight_download.sh. הוא מפעילollama serveמקומי זמני עםOLLAMA_MODELSשמצביע על ספריית ההורדות, ומושך את המודל לתוכה:cat << 'EOF' > ${GDCS_IMPLEMENTATION_HOME}/ollama_model_weight_download.sh && echo "Successfully created." || echo "Failed to create!" #!/bin/bash # ollama_model_weight_download.sh: pull GDCS_OLLAMA_MODEL into GDCS_OLLAMA_MODEL_DOWNLOAD_DIR with a temporary local Ollama server. # Requires: GDCS_OLLAMA_MODEL, GDCS_OLLAMA_MODEL_DOWNLOAD_DIR SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set -o nounset set -o pipefail # Ensure GDCS_OLLAMA_MODEL_DOWNLOAD_DIR is set if [[ -z "${GDCS_OLLAMA_MODEL_DOWNLOAD_DIR:-}" ]]; then echo "GDCS_OLLAMA_MODEL_DOWNLOAD_DIR must be set, exiting..." exit 1 fi mkdir -p "${GDCS_OLLAMA_MODEL_DOWNLOAD_DIR}" # Use a dedicated local port so an already running Ollama instance is not reused export OLLAMA_HOST="${OLLAMA_DOWNLOAD_HOST:-127.0.0.1:11435}" export OLLAMA_MODELS="${GDCS_OLLAMA_MODEL_DOWNLOAD_DIR}" ollama serve > "${GDCS_OLLAMA_MODEL_DOWNLOAD_DIR}/ollama-serve.log" 2>&1 & OLLAMA_PID=$! # Set up a trap to kill ollama serve when the script exits trap "kill ${OLLAMA_PID} 2>/dev/null" EXIT INT TERM # Wait for the local server for i in $(seq 1 30); do ollama list > /dev/null 2>&1 && break sleep 1 done ollama pull "${GDCS_OLLAMA_MODEL}" || exit 1 rm -f "${GDCS_OLLAMA_MODEL_DOWNLOAD_DIR}/ollama-serve.log" echo "Model '${GDCS_OLLAMA_MODEL}' is available in '${GDCS_OLLAMA_MODEL_DOWNLOAD_DIR}'." EOF chmod u+x "${GDCS_IMPLEMENTATION_HOME}/ollama_model_weight_download.sh"יוצרים את הסקריפט
model_weight_upload.sh. הוא מעתיק ספריית מודלים מקומית אלPersistentVolumeדרך טוען המודליםPod(סטרימינג של GNUtar, כולל קבצים שגדולים מ-8GiB) ומדלג על קבצים שכבר קיימים באותו גודל:cat << 'EOF' > ${GDCS_IMPLEMENTATION_HOME}/model_weight_upload.sh && echo "Successfully created." || echo "Failed to create!" #!/bin/bash # model_weight_upload.sh: copy LOCAL_MODEL_DIR into REMOTE_MODEL_DIR on the PersistentVolume mounted in the loader Pod. # Requires: GDCS_WORKLOAD_NAMESPACE, GDCS_MODEL_LOADER_POD_NAME, LOCAL_MODEL_DIR, REMOTE_MODEL_DIR # Optional: REMOTE_MOUNT_DIR (PersistentVolume mount path, defaults to REMOTE_MODEL_DIR) - its lost+found directory is removed. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" set -o errexit set -o nounset set -o pipefail # Ensure LOCAL_MODEL_DIR is set if [[ -z "${LOCAL_MODEL_DIR:-}" ]]; then echo "LOCAL_MODEL_DIR must be set, exiting..." exit 1 fi # Ensure REMOTE_MODEL_DIR is set if [[ -z "${REMOTE_MODEL_DIR:-}" ]]; then echo "REMOTE_MODEL_DIR must be set, exiting..." exit 1 fi REMOTE_MOUNT_DIR="${REMOTE_MOUNT_DIR:-${REMOTE_MODEL_DIR}}" KEEPALIVE_INTERVAL_SEC=300 start_session_keepalive() { echo "[KeepAlive] Started background heartbeat (every ${KEEPALIVE_INTERVAL_SEC}s)." while true; do sleep "${KEEPALIVE_INTERVAL_SEC}" # Lightweight API request to prevent session inactivity logout if kubectl get namespace "${GDCS_WORKLOAD_NAMESPACE}" >/dev/null 2>&1; then echo "[KeepAlive] Session heartbeat sent at $(date)" else echo "[KeepAlive] Heartbeat failed or context expired at $(date)" fi done } start_session_keepalive & MY_KEEPALIVE_PID=$! cleanup() { echo "[Cleanup] Stopping keep-alive process (PID: ${MY_KEEPALIVE_PID})..." kill "${MY_KEEPALIVE_PID}" 2>/dev/null || true wait "${MY_KEEPALIVE_PID}" 2>/dev/null || true echo "[Cleanup] Session keep-alive terminated." } trap cleanup EXIT INT TERM # Remove the lost+found directory kubectl exec "pod/${GDCS_MODEL_LOADER_POD_NAME}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- rm -rf "${REMOTE_MOUNT_DIR}/lost+found" # Create the model directory kubectl exec "pod/${GDCS_MODEL_LOADER_POD_NAME}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- mkdir -p "${REMOTE_MODEL_DIR}" # Loop through all files in the local directory FAILED_FILES=() MAX_UPLOAD_ATTEMPTS="${MAX_UPLOAD_ATTEMPTS:-3}" remote_size_of() { # Prints the remote file size (0 if the file does not exist) kubectl exec "pod/${GDCS_MODEL_LOADER_POD_NAME}" --namespace="${GDCS_WORKLOAD_NAMESPACE}" -- stat -c%s "$1" 2>/dev/null || echo 0 } # Loop through all files in the local directory (process substitution keeps FAILED_FILES in this shell) while read -r local_file; do # Get relative path rel_path="${local_file#"${LOCAL_MODEL_DIR}"/}" remote_file="${REMOTE_MODEL_DIR}/${rel_path}" # Get local file size local_size=$(stat -L -c%s "${local_file}") # Get remote file size (returns 0 if file does not exist) remote_size=$(remote_size_of "${remote_file}") if [ "${local_size}" -eq "${remote_size}" ]; then echo "[Loader] Skipping: ${rel_path} (Size matches)" continue fi if [ "${remote_size}" -ne "0" ]; then echo "[Loader] Mismatch: ${rel_path} (${remote_size} remote vs ${local_size} local)" fi # Ensure remote parent subdirectories exist if your model has folders remote_parent=$(dirname "${remote_file}") kubectl exec "pod/${GDCS_MODEL_LOADER_POD_NAME}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- mkdir -p "${remote_parent}" attempt=1 while [ "${attempt}" -le "${MAX_UPLOAD_ATTEMPTS}" ]; do echo "[Loader] Uploading: ${rel_path} (${local_size} bytes, attempt ${attempt}/${MAX_UPLOAD_ATTEMPTS})..." # Copy the individual file using GNU tar streaming. # Note: 'kubectl cp' uses Go's archive/tar, which defaults to PAX format for files > 8 GiB. # BusyBox's tar in the loader pod does not parse PAX size headers and fails with 'tar: invalid tar magic'. # Streaming via GNU tar (--format=gnu) uses base-256 binary encoding for files > 8 GiB, which BusyBox tar supports. tar --format=gnu -C "$(dirname "${local_file}")" -cf - "$(basename "${local_file}")" | \ kubectl exec -i "pod/${GDCS_MODEL_LOADER_POD_NAME}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- tar -xf - -C "${remote_parent}" || true # Verify: an interrupted exec stream leaves a truncated file behind without an error remote_size=$(remote_size_of "${remote_file}") if [ "${local_size}" -eq "${remote_size}" ]; then echo "[Loader] Verified: ${rel_path} (${remote_size} bytes)" break fi echo "[Loader] Incomplete upload: ${rel_path} (${remote_size} remote vs ${local_size} local), retrying..." attempt=$((attempt + 1)) done if [ "${local_size}" -ne "${remote_size}" ]; then FAILED_FILES+=("${rel_path}") fi done < <(find -L "${LOCAL_MODEL_DIR}" -type f) if [ "${#FAILED_FILES[@]}" -ne 0 ]; then echo "[Loader] FAILED: ${#FAILED_FILES[@]} file(s) could not be uploaded completely after ${MAX_UPLOAD_ATTEMPTS} attempts:" printf '[Loader] %s\n' "${FAILED_FILES[@]}" exit 1 fi echo "[Loader] Completed" # Fix file permissions kubectl exec "pod/${GDCS_MODEL_LOADER_POD_NAME}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- chown 0:0 -R "${REMOTE_MODEL_DIR}" # Flush the volume before the loader Pod is deleted (a detached volume with unwritten page cache loses data) kubectl exec "pod/${GDCS_MODEL_LOADER_POD_NAME}" \ --namespace="${GDCS_WORKLOAD_NAMESPACE}" \ -- sync EOF chmod u+x "${GDCS_IMPLEMENTATION_HOME}/model_weight_upload.sh"
פריסת מודל
- מודלים של Gemma עם מדריך למשתמש ב-Ollama: Gemma 4 E2B, E4B, 12B, 26B-A4B ו-31B, עם אפשרויות GPU של GDC עם air gap