Modelli open-weight con implementazione di riferimento vLLM su GDC con air gap

Questo documento fornisce istruzioni passo passo per il deployment di modelli con pesi aperti in ambienti Google Distributed Cloud (GDC) con air gap. Illustra l'utilizzo di vLLM per l'erogazione con throughput elevato utilizzando le funzionalità della piattaforma GDC, tra cui Kubernetes, Harbor e le risorse dell'acceleratore (GPU).

Architettura

La soluzione prevede il deployment di vLLM come backend di gestione delle inferenze containerizzato (Deployment) all'interno di un cluster standard. I pesi del modello sono archiviati su volumi permanenti con dimensioni dinamiche in base alla directory del modello scaricato con un overhead del 25%, configurati con la policy QoS di archiviazione system-performance-<org>-admin (30 IOPS per GiB con un limite minimo di 6148 IOPS o performance-rwo se è abilitata l'archiviazione a blocchi ad alte prestazioni) e compilati utilizzando un workflow del caricatore del modello. Service Kubernetes di tipo LoadBalancer espongono l'API di backend e gli oggetti NetworkPolicy / ProjectNetworkPolicy ne proteggono l'accesso.

Architettura di riferimento dei modelli open-weight con vLLM su GDC con air gap.

Prima di iniziare

Prima di procedere con il deployment, assicurati che il tuo ambiente soddisfi tutti i prerequisiti necessari e che le utilità della riga di comando richieste siano configurate correttamente. La configurazione di questi strumenti sulla tua workstation è essenziale per gestire i Container Registry, interagire con i cluster e automatizzare il processo di deployment.

  • Cluster standard creato con risorse sufficienti (CPU, memoria, acceleratore). I requisiti minimi dell'acceleratore variano a seconda del modello.
  • Istanza Harbor disponibile e accessibile.
  • IAM necessario applicato.
  • Workstation con la connettività necessaria all'ambiente e a internet
  • Token Hugging Face e termini accettati se utilizzi modelli con accesso controllato.

Configurazione dell'ambiente

Identity and Access Management

Assicurati che gli account, i ruoli e le autorizzazioni IAM necessari siano configurati correttamente.

Ruoli Utente GDC nell'organizzazione:

  • (Facoltativo) Amministratore cluster standard (visualizza i cluster dalla console)

Ruoli Utente GDC sul progetto (RoleBinding nello spazio dei nomi del progetto, concessi da un Amministratore IAM progetto):

  • Visualizzatore istanze Harbor (harbor-instance-viewer)
  • Creatore di progetti Harbor (harbor-project-creator, solo se il progetto Harbor non esiste ancora)
  • Standard Cluster Admin (standard-cluster-admin, richiesto per gdcloud clusters get-credentials)

Ruolo Utente GDC nel cluster standard: i ruoli del progetto precedenti non concedono autorizzazioni all'interno del cluster. Un amministratore IAM del progetto deve inoltre associare l'utente a StandardClusterRole cluster-admin con un StandardClusterRoleBinding nello spazio dei nomi del progetto sul server API di gestione; l'associazione viene propagata ai cluster standard del progetto entro pochi secondi (status.clusters[].conditions mostra 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

Sostituisci quanto segue:

  • MANAGEMENT_API_SERVER: il percorso del file kubeconfig del server API di gestione.
  • BINDING_NAME: un nome per l'associazione, ad esempio alex-cluster-admin (lettere minuscole, cifre e trattini).
  • PROJECT: il nome del progetto.
  • USER: il nome utente da associare, esattamente come viene segnalato dal tuo provider di identità (ad esempio, un indirizzo email).

Assistenza dell'operatore dell'infrastruttura (solo quando performance-rwo non è disponibile nel cluster standard): le guide dell'utente creano il modello PersistentVolumeClaim con la classe di archiviazione standard-rwo predefinita, che limita i volumi del modello di dimensioni adeguate a 3 IOPS per GiB. Chiedi all'operatore dell'infrastruttura di applicare la policy QoS system-performance-<ORG>-admin al volume che supporta il modello PersistentVolumeClaim in modo che riceva il limite minimo di 6148 IOPS. Le guide dell'utente stampano i valori necessari all'operatore dell'infrastruttura.

Autorizzazioni dell'account robot Harbor per crane:

  • Elenco dei repository
  • Repository di pull
  • Repository push
  • Leggi l'artefatto
  • Elenca artefatto
  • Crea tag
  • Tag elenco

Autorizzazioni dell'account robot Harbor per i pull delle immagini Kubernetes:

  • Elenco dei repository
  • Repository di pull
  • Leggi l'artefatto
  • Elenca artefatto
  • Tag elenco

Workstation

Esegui tutti i comandi di questa guida da una workstation con gli strumenti e la connettività necessari per l'ambiente e internet, se non diversamente indicato in un passaggio.

Requisiti

Sulla workstation devono essere installati i seguenti strumenti:

  • crane: gestisci e copia le immagini container tra i registry (documentazione).
  • docker: crea ed esegue il push dell'immagine container vLLM con patch.
  • gdcloud: interfaccia a riga di comando (CLI) per la gestione delle risorse GDC (documentazione).
  • hf: l'interfaccia a riga di comando (CLI) utilizzata per accedere a Hugging Face.
  • kubectl: interfaccia a riga di comando (CLI) utilizzata per comunicare con un cluster Kubernetes e gestirlo.
  • jq: processore JSON a riga di comando leggero e flessibile.
  • yq: processore YAML portatile da riga di comando.

Configurazione workstation

Per la configurazione della workstation sono necessarie le seguenti informazioni sull'ambiente:

  • GDC_STANDARD_CLUSTER_NAME: il nome del cluster standard GDC.
  • GDC_DOMAIN_SUFFIX: il suffisso di dominio per l'ambiente GDC (ad esempio gdc.example.com).
  • GDC_ORG: il nome dell'organizzazione GDC.
  • GDC_PROJECT: il nome del progetto GDC.
  • GDC_ZONE: il nome della zona di deployment di GDC.
  • GDC_HARBOR_INSTANCE_NAME: il nome dell'istanza Harbor nel progetto.
  • GDCS_HARBOR_PROJECT_NAME: il nome del progetto Harbor da utilizzare per le immagini (valore predefinito: solutions).
  • GDCS_HARBOR_CRANE_ROBOT_NAME: il nome dell'account robot Harbor crane.
  • GDCS_HARBOR_CRANE_ROBOT_TOKEN: il token di autenticazione per l'account robot Harbor crane.
  • GDCS_HARBOR_K8S_ROBOT_NAME: il nome dell'account robot di pull delle immagini Kubernetes di Harbor.
  • GDCS_HARBOR_K8S_ROBOT_TOKEN: il token di autenticazione per l'account robot di pull delle immagini Kubernetes di Harbor.

Una volta raccolti i valori per tutte le variabili richieste, procedi con la generazione del file delle variabili di ambiente. Dopo la creazione, puoi modificare manualmente il file in qualsiasi momento.

${HOME}/gdcag-solutionssource ${HOME}/gdcag-solutions/env.sh
  1. Crea le directory della soluzione root e la cartella dei secret:

    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. Crea il file di configurazione dell'ambiente della piattaforma:

    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
    

    Sostituisci quanto segue:

    • STANDARD_CLUSTER_NAME: il nome del cluster standard GDC.
    • DOMAIN_SUFFIX: il suffisso di dominio GDC.
    • ORG: l'organizzazione GDC.
    • PROJECT: progetto GDC.
    • ZONE: la zona GDC.
    • HARBOR_INSTANCE_NAME: il nome dell'istanza Harbor di GDC.
  3. Crea il file di configurazione dell'ambiente del registry:

    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
    

    Sostituisci quanto segue:

    • HARBOR_CRANE_ROBOT_NAME: il nome completo dell'account robot Harbor per crane, come mostrato da Harbor, incluso il prefisso robot$. Mantieni gli apici singoli in modo che la shell non espanda $.
    • HARBOR_K8S_ROBOT_NAME: il nome completo dell'account robot Harbor per i pull di immagini Kubernetes, come mostrato da Harbor, incluso il prefisso robot$. Mantieni gli apici singoli in modo che la shell non espanda $.
  4. Aggiungi i token ai file secret:

    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
    

    Sostituisci quanto segue:

    • CRANE_ROBOT_TOKEN: il secret dell'account robot crane.
    • KUBERNETES_ROBOT_TOKEN: il secret dell'account robot di pull delle immagini Kubernetes.
  5. Crea il file del caricatore dell'ambiente 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
    

Configura le variabili della soluzione

  1. Crea la directory di implementazione della soluzione e la cartella dei secret:

    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. Aggiungi il tuo token Hugging Face al file secret:

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

    Sostituisci quanto segue:

    • HUGGINGFACE_TOKEN: il tuo token di accesso a Hugging Face.
  3. Celta il file di configurazione dell'acceleratore:

    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
    

    Sostituisci quanto segue:

    • WORKLOAD_ACCELERATOR_TYPE: il nome della risorsa GPU esposta dai nodi GPU, ad esempio nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE. Le guide dell'utente elencano il nome della risorsa di ogni GPU offerta da GDC con air gap. Dopo aver recuperato le credenziali del cluster, puoi elencare i nomi nel cluster con kubectl get nodes --output=json | jq -r '[.items[].status.allocatable | keys[] | select(startswith("nvidia.com/"))] | unique[]'.
  4. Crea il file di configurazione dell'ambiente della soluzione:

    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. Crea il file del caricatore dell'ambiente di implementazione:

    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. Modifica e rivedi i file di ambiente con l'editor che preferisci:

    ${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. Esegui il sourcing del file di ambiente:

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

    L'output è simile al seguente:

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

GDC

Questa guida presuppone che la tua workstation sia configurata per considerare attendibili i certificati TLS per l'ambiente GDC e l'istanza Harbor.

  1. Configura 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. Esegui l'autenticazione nell'ambiente GDC:

    gdcloud auth login
    

Cluster

  1. Recupera le credenziali del cluster:

    gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \
    --project="${GDC_PROJECT}" \
    --standard \
    --zone="${GDC_ZONE}"
    
  2. Verifica la connettività al cluster:

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

Preparazione della migrazione degli artefatti

  1. Crea un file di configurazione Docker per crane. Un account robot viene utilizzato per eseguire il push di livelli di immagini di grandi dimensioni per evitare timeout dei token di autenticazione quando si utilizza l'helper delle credenziali di Managed Harbor Service (MHS) (docker-credential-mhs) con un account utente:

    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. Crea un file di configurazione Docker per 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. Crea lo script 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. Definisci l'elenco delle immagini container richieste per la soluzione:

    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. Esegui il seeding delle immagini container richieste in Artifact Registry:

    ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
    
  1. Crea un Dockerfile per applicare patch al container vLLM finché non vengono implementate le modifiche upstream:

    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. Crea ed esegui il push dell'immagine con patch:

    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. Crea lo script model_weight_upload.sh. Copia una directory del modello locale in PersistentVolume tramite il caricatore di modelli Pod (streaming GNU tar, inclusi file di dimensioni superiori a 8 GiB) e salta i file già presenti con le stesse dimensioni:

    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"
    

Esegui il deployment di un modello