Offene Gewichtungsmodelle mit vLLM-Referenzimplementierung in GDC mit Air Gap

In diesem Dokument finden Sie eine detaillierte Anleitung zum Bereitstellen von Open-Weight-Modellen in GDC-Umgebungen (Google Distributed Cloud) mit Air Gap. Es wird beschrieben, wie vLLM für die Bereitstellung mit hohem Durchsatz mithilfe der Funktionen der GDC-Plattform verwendet wird, einschließlich Kubernetes-, Harbor- und Beschleunigerressourcen (GPU).

Architektur

Die Lösung umfasst die Bereitstellung von vLLM als containerisiertes Inferenz-Serving-Backend (Deployment) in einem Standardcluster. Modellgewichtungen werden auf Persistent Volumes gespeichert, deren Größe dynamisch aus dem heruntergeladenen Modellverzeichnis mit 25% Overhead angepasst wird. Sie werden mit der Speicher-QoS-Richtlinie system-performance-<org>-admin (30 IOPS pro GiB mit einem Mindestwert von 6.148 IOPS oder performance-rwo, wenn leistungsstarker Blockspeicher aktiviert ist) konfiguriert und über einen Modell-Loader-Workflow befüllt. Kubernetes-Services vom Typ LoadBalancer stellen die Backend-API bereit und NetworkPolicy- / ProjectNetworkPolicy-Objekte sichern den Zugriff darauf.

Referenzarchitektur für Open-Weights-Modelle mit vLLM in GDC mit Air Gap.

Hinweis

Bevor Sie mit der Bereitstellung fortfahren, müssen Sie sicherstellen, dass Ihre Umgebung alle erforderlichen Voraussetzungen erfüllt und die erforderlichen Befehlszeilen-Dienstprogramme richtig konfiguriert sind. Die Einrichtung dieser Tools auf Ihrer Workstation ist für die Verwaltung von Container-Registries, die Interaktion mit Clustern und die Automatisierung des Bereitstellungsprozesses unerlässlich.

  • Standardcluster mit ausreichenden Ressourcen (CPU, Arbeitsspeicher, Beschleuniger) erstellt. Die Mindestanforderungen an Beschleuniger variieren je nach Modell.
  • Harbor-Instanz verfügbar und zugänglich.
  • Erforderliche IAM-Berechtigungen angewendet.
  • Workstation mit der erforderlichen Verbindung zur Umgebung und zum Internet
  • Hugging Face-Token und Nutzungsbedingungen akzeptiert, wenn Gated-Modelle verwendet werden.

Umgebungskonfiguration

Identity and Access Management

Prüfen Sie, ob die erforderlichen IAM-Konten, -Rollen und -Berechtigungen richtig konfiguriert sind.

GDC-Nutzerrollen für die Organisation:

  • Optional: Standard-Cluster-Administrator (Cluster über die Console ansehen)

GDC-Nutzer-Rollen für das Projekt (RoleBinding im Projekt-Namespace, zugewiesen von einem Projekt-IAM-Administrator):

  • Harbor-Instanzbetrachter (harbor-instance-viewer)
  • Harbor-Projektersteller (harbor-project-creator, nur wenn das Harbor-Projekt noch nicht vorhanden ist)
  • Standard-Cluster-Administrator (standard-cluster-admin, erforderlich für gdcloud clusters get-credentials)

GDC-Nutzerrolle im Standardcluster: Die vorherigen Projektrollen gewähren keine Berechtigungen innerhalb des Clusters. Ein Projekt-IAM-Administrator muss den Nutzer zusätzlich mit einer StandardClusterRoleBinding im Projekt-Namespace auf dem Verwaltungs-API-Server an die StandardClusterRole cluster-admin binden. Die Bindung wird innerhalb von Sekunden an die Standardcluster des Projekts weitergegeben (status.clusters[].conditions zeigt Propagated=True).

cat <<EOF | kubectl apply --kubeconfig MANAGEMENT_API_SERVER -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

Ersetzen Sie Folgendes:

  • MANAGEMENT_API_SERVER: der Pfad zur kubeconfig-Datei des Management-API-Servers.
  • BINDING_NAME: ein Name für die Bindung, z. B. alex-cluster-admin (Kleinbuchstaben, Ziffern und Bindestriche).
  • PROJECT: der Name des Projekts.
  • USER: der zu bindende Nutzername, genau wie er von Ihrem Identitätsanbieter gemeldet wird (z. B. eine E-Mail-Adresse).

Unterstützung durch den Infrastrukturbetreiber (nur wenn performance-rwo im Standardcluster nicht verfügbar ist): In den Nutzerhandbüchern wird das Modell PersistentVolumeClaim mit der Standard-StorageClass standard-rwo erstellt, wodurch die Modellvolumes mit der richtigen Größe auf 3 IOPS pro GiB begrenzt werden. Bitten Sie Ihren Infrastrukturbetreiber, die QoS-Richtlinie system-performance-<ORG>-admin auf das Volume anzuwenden, das das Modell PersistentVolumeClaim unterstützt, damit es die Mindestanzahl von 6.148 IOPS erhält. In den Nutzerhandbüchern werden die Werte ausgegeben, die der Infrastrukturbetreiber benötigt.

Berechtigungen des Harbor-Robot-Kontos für crane:

  • Repository auflisten
  • Pull-Repository
  • Repository per Push übertragen
  • Artefakt lesen
  • Artefakt auflisten
  • Tag erstellen
  • Listen-Tag

Berechtigungen des Harbor-Robot-Kontos für Kubernetes-Image-Abrufe:

  • Repository auflisten
  • Pull-Repository
  • Artefakt lesen
  • Artefakt auflisten
  • Listen-Tag

Workstation

Führen Sie alle Befehle in diesem Leitfaden auf einer Workstation mit den erforderlichen Tools und der erforderlichen Verbindung zur Umgebung und zum Internet aus, sofern in einem Schritt nichts anderes angegeben ist.

Voraussetzungen

Die folgenden Tools müssen auf der Workstation installiert sein:

  • crane: Container-Images zwischen Registries verwalten und kopieren (Dokumentation).
  • docker: Erstellt das gepatchte vLLM-Container-Image und überträgt es per Push.
  • gdcloud: Befehlszeile (CLI) zum Verwalten von GDC-Ressourcen (Dokumentation).
  • hf: Befehlszeile (CLI) für den Zugriff auf Hugging Face.
  • kubectl: Befehlszeile (CLI) für die Kommunikation mit einem Kubernetes-Cluster und dessen Verwaltung.
  • jq: Ein einfacher und flexibler JSON-Prozessor für die Befehlszeile.
  • yq: portabler YAML-Prozessor für die Befehlszeile.

Workstationkonfiguration

Die folgenden Informationen zur Umgebung sind für die Workstation-Konfiguration erforderlich:

  • GDC_STANDARD_CLUSTER_NAME: Der Name des GDC-Standardclusters.
  • GDC_DOMAIN_SUFFIX: Das Domainsuffix für die GDC-Umgebung (z. B. gdc.example.com).
  • GDC_ORG: Der Name der GDC-Organisation.
  • GDC_PROJECT: Der Name des GDC-Projekts.
  • GDC_ZONE: Der Name der GDC-Bereitstellungszone.
  • GDC_HARBOR_INSTANCE_NAME: Der Name der Harbor-Instanz im Projekt.
  • GDCS_HARBOR_PROJECT_NAME: Der Name des Harbor-Projekts, das für Images verwendet werden soll (Standard: solutions).
  • GDCS_HARBOR_CRANE_ROBOT_NAME: Der Name des Harbor-crane-Roboterkontos.
  • GDCS_HARBOR_CRANE_ROBOT_TOKEN: Das Authentifizierungstoken für das Harbor-crane-Roboterkonto.
  • GDCS_HARBOR_K8S_ROBOT_NAME: Der Name des Harbor-Kubernetes-Image-Pull-Roboterkontos.
  • GDCS_HARBOR_K8S_ROBOT_TOKEN: Das Authentifizierungstoken für das Harbor-Kubernetes-Image-Pull-Roboterkonto.

Nachdem Sie die Werte für alle erforderlichen Variablen erfasst haben, fahren Sie mit dem Erstellen der Datei mit den Umgebungsvariablen fort. Nachdem die Datei erstellt wurde, können Sie sie jederzeit manuell bearbeiten.

  1. Erstellen Sie die Stammverzeichnisse der Lösung und den Ordner für Secrets:

    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/*
    
  2. Erstellen Sie die Konfigurationsdatei für die Plattformumgebung:

    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
    

    Ersetzen Sie Folgendes:

    • STANDARD_CLUSTER_NAME: Name des GDC-Standardclusters.
    • DOMAIN_SUFFIX: GDC-Domainsuffix.
    • ORG: GDC-Organisation.
    • PROJECT: GDC-Projekt.
    • ZONE: GDC-Zone.
    • HARBOR_INSTANCE_NAME: Name der GDC Harbor-Instanz.
  3. Erstellen Sie die Konfigurationsdatei für die Registry-Umgebung:

    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
    

    Ersetzen Sie Folgendes:

    • HARBOR_CRANE_ROBOT_NAME: der vollständige Name des Harbor-Roboterkontos für crane, wie er von Harbor angezeigt wird, einschließlich des Präfixes robot$. Behalten Sie die einfachen Anführungszeichen bei, damit die Shell $ nicht erweitert.
    • HARBOR_K8S_ROBOT_NAME: der vollständige Name des Harbor-Roboterkontos für Kubernetes-Image-Abrufe, wie er von Harbor angezeigt wird, einschließlich des Präfixes robot$. Behalten Sie die einfachen Anführungszeichen bei, damit die Shell $ nicht erweitert.
  4. Fügen Sie Ihre Tokens den Secret-Dateien hinzu:

    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
    

    Ersetzen Sie Folgendes:

    • CRANE_ROBOT_TOKEN: das Secret des crane-Roboterkontos.
    • KUBERNETES_ROBOT_TOKEN: das Secret des Kubernetes-Image-Pull-Roboterkontos.
  5. Erstellen Sie die Loader-Datei für die Root-Umgebung:

    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
    

Lösungsvariablen konfigurieren

  1. Erstellen Sie das Verzeichnis für die Lösungsimplementierung und den Ordner für Secrets:

    mkdir -p ${HOME}/gdcag-solutions/open-weight-models/vllm/env.d
    mkdir -p ${HOME}/gdcag-solutions/open-weight-models/vllm/secrets
    
    touch ${HOME}/gdcag-solutions/open-weight-models/vllm/secrets/huggingface_token
    chmod u=rwx,go= ${HOME}/gdcag-solutions/open-weight-models/vllm/secrets
    chmod -R u=rw,go= ${HOME}/gdcag-solutions/open-weight-models/vllm/secrets/*
    
  2. Fügen Sie Ihr Hugging Face-Token zur Secret-Datei hinzu:

    set +o history
    
    echo "HUGGINGFACE_TOKEN" > ${HOME}/gdcag-solutions/open-weight-models/vllm/secrets/huggingface_token
    
    set -o history
    

    Ersetzen Sie Folgendes:

    • HUGGINGFACE_TOKEN: Ihr Hugging Face-Zugriffstoken.
  3. Erstellen Sie die Konfigurationsdatei für den Beschleuniger:

    cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/vllm/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
    

    Ersetzen Sie Folgendes:

    • WORKLOAD_ACCELERATOR_TYPE: der Name der GPU-Ressource, die von Ihren GPU-Knoten bereitgestellt wird, z. B. nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE. In den Nutzerhandbüchern sind die Ressourcennamen aller GPUs aufgeführt, die GDC mit Air Gap bietet. Nachdem Sie die Cluster-Anmeldedaten abgerufen haben, können Sie die Namen in Ihrem Cluster mit kubectl get nodes --output=json | jq -r '[.items[].status.allocatable | keys[] | select(startswith("nvidia.com/"))] | unique[]' auflisten.
  4. Erstellen Sie die Konfigurationsdatei für die Lösungsumgebung:

    cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/vllm/env.d/vllm.sh && echo "Successfully created." || echo "Failed to create!"
    # Hugging Face
    export HF_TOKEN="$(cat ${GDCS_IMPLEMENTATION_HOME}/secrets/huggingface_token 2>/dev/null || echo '')"
    
    # Images
    export GDCS_BUSYBOX_IMAGE_TAG="1.38.0"
    export GDCS_VLLM_IMAGE_TAG="v0.26.0"
    
    # 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
    
  5. Erstellen Sie die Loader-Datei für die Implementierungsumgebung:

    cat << 'EOF' > ${HOME}/gdcag-solutions/open-weight-models/vllm/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/vllm"
    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/vllm.sh"
    EOF
    
  6. Bearbeiten und prüfen Sie die Umgebungsdateien mit Ihrem bevorzugten Editor:

    ${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/vllm/env.d/accelerator.sh
    ${EDITOR:-vi} ${HOME}/gdcag-solutions/open-weight-models/vllm/env.d/vllm.sh
    
  7. Rufen Sie die Umgebungsdatei auf:

    source ${HOME}/gdcag-solutions/open-weight-models/vllm/env.sh
    

    Die Ausgabe sieht etwa so aus:

    GDCS_ROOT_HOME=HOME_DIRECTORY_PATH/gdcag-solutions
    GDCS_IMPLEMENTATION_HOME=HOME_DIRECTORY_PATH/gdcag-solutions/open-weight-models/vllm
    

GDC

In diesem Leitfaden wird davon ausgegangen, dass Ihre Workstation so konfiguriert ist, dass sie den TLS-Zertifikaten für Ihre GDC-Umgebung und Harbor-Instanz vertraut.

  1. Konfigurieren Sie 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}"
    
  2. Authentifizieren Sie sich bei der GDC-Umgebung:

    gdcloud auth login
    

Cluster

  1. Clusteranmeldedaten abrufen:

    gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \
    --project="${GDC_PROJECT}" \
    --standard \
    --zone="${GDC_ZONE}"
    
  2. Prüfen Sie die Verbindung zum Cluster:

    kubectl get nodes -L node.cluster.private.gdc.goog/machine-class
    

Vorbereitung der Artefaktmigration

  1. Erstellen Sie eine Docker-Konfigurationsdatei für Crane. Ein Roboterkonto wird verwendet, um große Image-Ebenen zu übertragen, um Zeitüberschreitungen bei Authentifizierungstokens zu vermeiden, wenn der Managed Harbor Service (MHS)-Anmeldedaten-Helper (docker-credential-mhs) mit einem Nutzerkonto verwendet wird:

    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}"
    
  2. Erstellen Sie eine Docker-Konfigurationsdatei für 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}"
    
  3. Erstellen Sie das Skript 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"
    
  4. Definieren Sie die Liste der erforderlichen Container-Images für die Lösung:

    declare -a GDCS_REGISTRY_IMAGES=(
      "docker.io/library/busybox:${GDCS_BUSYBOX_IMAGE_TAG}"
      "docker.io/vllm/vllm-openai:${GDCS_VLLM_IMAGE_TAG}"
    )
    export SERIALIZED_IMAGES=$(declare -p GDCS_REGISTRY_IMAGES)
    
  5. Übertragen Sie die erforderlichen Container-Images per Seed in die Artifact Registry:

    ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
    
  1. Erstellen Sie ein Dockerfile, um den vLLM-Container zu patchen, bis die Upstream-Änderungen implementiert sind:

    cat <<EOF > ${GDCS_IMPLEMENTATION_HOME}/docker/vllm-${GDCS_VLLM_IMAGE_TAG}-patch.dockerfile && echo "Successfully created." || echo "Failed to create!"
    FROM docker.io/vllm/vllm-openai:${GDCS_VLLM_IMAGE_TAG}
    
    RUN pip install --upgrade "opencv-python-headless<=4.12.0.88"
    EOF
    
  2. Erstellen Sie das gepatchte Image und übertragen Sie es per Push:

    context_dir="${GDCS_IMPLEMENTATION_HOME}/docker/empty-context"
    image="${GDCS_HARBOR_PROJECT_URI}/vllm/vllm-openai:${GDCS_VLLM_IMAGE_TAG}-fips"
    
    mkdir -p "${context_dir}"
    rm -rf "${context_dir}"/*
    
    export DOCKER_CONFIG="${GDCS_HARBOR_CRANE_DOCKER_CONFIG}"
    
    docker build \
    --file="${GDCS_IMPLEMENTATION_HOME}/docker/vllm-${GDCS_VLLM_IMAGE_TAG}-patch.dockerfile" \
    --tag="${image}" \
    "${context_dir}"
    
    docker push "${image}"
    
  3. Erstellen Sie das Skript model_weight_upload.sh. Dabei wird ein lokales Modellverzeichnis über den Modell-Loader Pod (GNU-tar-Streaming, einschließlich Dateien, die größer als 8 GiB sind) in PersistentVolume kopiert. Dateien, die bereits mit derselben Größe vorhanden sind, werden übersprungen:

    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"
    

Modell bereitstellen