Modelos de peso aberto com implementação de referência de vLLM no GDC com isolamento físico

Este documento fornece instruções detalhadas para implantar modelos de pesos abertos em ambientes com isolamento físico do Google Distributed Cloud (GDC). Ele aborda o uso do vLLM para disponibilização de alta capacidade de processamento usando os recursos da plataforma GDC, incluindo Kubernetes, Harbor e acelerador (GPU).

Arquitetura

A solução envolve a implantação do vLLM como um back-end de exibição de inferência em contêiner (Deployment) em um cluster padrão. Os pesos do modelo são armazenados em volumes permanentes que são dimensionados dinamicamente com base no diretório do modelo baixado com 25% de sobrecarga, configurados com a política de QoS de armazenamento system-performance-<org>-admin (30 IOPS por GiB com um piso mínimo de 6.148 IOPS ou performance-rwo em que o armazenamento em blocos de alto desempenho está ativado) e preenchidos usando um fluxo de trabalho de carregador de modelos. Os Services do Kubernetes do tipo LoadBalancer expõem a API de back-end, e os objetos NetworkPolicy / ProjectNetworkPolicy protegem o acesso a ela.

Modelos de peso aberto com arquitetura de referência vLLM no GDC com isolamento físico.

Antes de começar

Antes de prosseguir com a implantação, verifique se o ambiente atende a todos os pré-requisitos necessários e se os utilitários de linha de comando necessários estão configurados corretamente. Configurar essas ferramentas na estação de trabalho é essencial para gerenciar Container Registry, interagir com clusters e automatizar o processo de implantação.

  • Cluster padrão criado com recursos suficientes (CPU, memória, acelerador). Os requisitos mínimos de acelerador variam de acordo com o modelo.
  • Instância do Harbor disponível e acessível.
  • O IAM necessário foi aplicado.
  • Estação de trabalho com a conectividade necessária com o ambiente e a Internet
  • Token do Hugging Face e termos aceitos se estiver usando modelos restritos.

Configuração do ambiente

Identity and Access Management

Verifique se as contas, os papéis e as permissões necessários do IAM estão configurados corretamente.

Papéis de usuário do GDC na organização:

  • Opcional: administrador de cluster padrão (visualizar clusters no console)

Papéis de usuário do GDC no projeto (RoleBinding no namespace do projeto, concedido por um administrador do IAM do projeto):

  • Leitor de instâncias do Harbor (harbor-instance-viewer)
  • Criador de projetos do Harbor (harbor-project-creator, somente se o projeto do Harbor ainda não existir)
  • Administrador de cluster padrão (standard-cluster-admin, obrigatório para gdcloud clusters get-credentials)

Papel de usuário do GDC no cluster padrão: os papéis do projeto anteriores não concedem permissões dentro do cluster. Um administrador do IAM do projeto também precisa vincular o usuário ao StandardClusterRole cluster-admin com um StandardClusterRoleBinding no namespace do projeto no servidor de API Management. A vinculação é propagada para os clusters padrão do projeto em segundos (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

Substitua:

  • MANAGEMENT_API_SERVER: o caminho para o arquivo kubeconfig do servidor de API de gerenciamento.
  • BINDING_NAME: um nome para a vinculação, por exemplo, alex-cluster-admin (letras minúsculas, dígitos e hifens).
  • PROJECT: o nome do projeto.
  • USER: o nome de usuário a ser vinculado, exatamente como o provedor de identidade o informa (por exemplo, um endereço de e-mail).

Assistência do operador de infraestrutura (somente quando performance-rwo não está disponível no cluster padrão): os guias do usuário criam o modelo PersistentVolumeClaim com o StorageClass standard-rwo padrão, que limita os volumes do modelo de tamanho correto a 3 IOPS por GiB. Peça ao operador de infraestrutura para aplicar a política de QoS system-performance-<ORG>-admin ao volume que apoia o modelo PersistentVolumeClaim para que ele receba o piso de 6.148 IOPS. Os guias do usuário mostram os valores que o operador de infraestrutura precisa.

Permissões da conta de robô do Harbor para crane:

  • Listar repositório
  • Repositório de pull
  • Repositório de push
  • Ler artefato
  • Listar artefato
  • Criar tag
  • Tag de lista

Permissões da conta de robô do Harbor para extrações de imagens do Kubernetes:

  • Listar repositório
  • Repositório de pull
  • Ler artefato
  • Listar artefato
  • Tag de lista

Estação de trabalho

Execute todos os comandos deste guia em uma estação de trabalho com as ferramentas e a conectividade necessárias para o ambiente e a Internet, a menos que uma etapa indique o contrário.

Requisitos

As seguintes ferramentas precisam ser instaladas na estação de trabalho:

  • crane: gerenciar e copiar imagens de contêiner entre registros (documentação).
  • docker: cria e envia a imagem do contêiner vLLM com patch.
  • gdcloud: interface de linha de comando (CLI) para gerenciar recursos do GDC (documentação).
  • hf: interface de linha de comando (CLI) usada para acessar o Hugging Face.
  • kubectl: interface de linha de comando (CLI) usada para se comunicar com um cluster do Kubernetes e gerenciá-lo.
  • jq: processador JSON de linha de comando leve e flexível.
  • yq: processador YAML de linha de comando portátil.

Configuração da estação de trabalho

As seguintes informações sobre o ambiente são necessárias para a configuração da estação de trabalho:

  • GDC_STANDARD_CLUSTER_NAME: o nome do cluster padrão do GDC.
  • GDC_DOMAIN_SUFFIX: o sufixo de domínio do ambiente do GDC (por exemplo, gdc.example.com).
  • GDC_ORG: o nome da organização do GDC.
  • GDC_PROJECT: o nome do projeto do GDC.
  • GDC_ZONE: o nome da zona de implantação do GDC.
  • GDC_HARBOR_INSTANCE_NAME: o nome da instância do Harbor no projeto.
  • GDCS_HARBOR_PROJECT_NAME: o nome do projeto do Harbor a ser usado para imagens (padrão: solutions).
  • GDCS_HARBOR_CRANE_ROBOT_NAME: o nome da conta de robô do Harbor crane.
  • GDCS_HARBOR_CRANE_ROBOT_TOKEN: o token de autenticação da conta de robô do Harbor crane.
  • GDCS_HARBOR_K8S_ROBOT_NAME: o nome da conta de robô de extração de imagens do Kubernetes do Harbor.
  • GDCS_HARBOR_K8S_ROBOT_TOKEN: o token de autenticação da conta do robô de extração de imagens do Kubernetes do Harbor.

Depois de coletar os valores de todas as variáveis necessárias, gere o arquivo de variáveis de ambiente. Depois de criado, é possível editar manualmente o arquivo a qualquer momento.

  1. Crie os diretórios raiz da solução e a pasta de 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. Crie o arquivo de configuração do ambiente da plataforma:

    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
    

    Substitua:

    • STANDARD_CLUSTER_NAME: nome do cluster padrão do GDC.
    • DOMAIN_SUFFIX: sufixo de domínio do GDC.
    • ORG: organização do GDC.
    • PROJECT: projeto do GDC.
    • ZONE: zona do GDC.
    • HARBOR_INSTANCE_NAME: nome da instância do GDC Harbor.
  3. Crie o arquivo de configuração do ambiente de registro:

    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
    

    Substitua:

    • HARBOR_CRANE_ROBOT_NAME: o nome completo da conta de robô do Harbor para crane, conforme mostrado pelo Harbor, incluindo o prefixo robot$. Mantenha as aspas simples para que o shell não expanda $.
    • HARBOR_K8S_ROBOT_NAME: o nome completo da conta de robô do Harbor para extrações de imagens do Kubernetes, conforme mostrado pelo Harbor, incluindo o prefixo robot$. Mantenha as aspas simples para que o shell não expanda $.
  4. Adicione os tokens aos arquivos de secrets:

    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
    

    Substitua:

    • CRANE_ROBOT_TOKEN: a chave secreta da conta de robô crane.
    • KUBERNETES_ROBOT_TOKEN: o secret da conta de robô de extração de imagens do Kubernetes.
  5. Crie o arquivo do carregador de ambiente raiz:

    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
    

Configurar variáveis de solução

  1. Crie o diretório de implementação da solução e a pasta de 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. Adicione o token do Hugging Face ao arquivo de secret:

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

    Substitua:

    • HUGGINGFACE_TOKEN: seu token de acesso do Hugging Face.
  3. Crie o arquivo de configuração do acelerador:

    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
    

    Substitua:

    • WORKLOAD_ACCELERATOR_TYPE: o nome do recurso de GPU que os nós de GPU expõem, por exemplo, nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE. Os guias do usuário listam o nome do recurso de cada GPU que o GDC com isolamento físico oferece. Depois de recuperar as credenciais do cluster, é possível listar os nomes no cluster com kubectl get nodes --output=json | jq -r '[.items[].status.allocatable | keys[] | select(startswith("nvidia.com/"))] | unique[]'.
  4. Crie o arquivo de configuração do ambiente da solução:

    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. Crie o arquivo do carregador do ambiente de implementação:

    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. Edite e revise os arquivos de ambiente com seu editor preferido:

    ${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. Use o comando source no arquivo de ambiente:

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

    O resultado será o seguinte:

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

GDC

Este guia pressupõe que sua estação de trabalho esteja configurada para confiar nos certificados TLS do ambiente do GDC e da instância do Harbor.

  1. Configurar 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. Autentique-se no ambiente do GDC:

    gdcloud auth login
    

Cluster

  1. Recupere as credenciais do cluster:

    gdcloud clusters get-credentials "${GDC_STANDARD_CLUSTER_NAME}" \
    --project="${GDC_PROJECT}" \
    --standard \
    --zone="${GDC_ZONE}"
    
  2. Verifique a conectividade com o cluster:

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

Preparação para a migração de artefatos

  1. Crie um arquivo de configuração do Docker para o crane. Uma conta de robô é usada para enviar camadas de imagem grandes para evitar tempos limite de token de autenticação ao usar o auxiliar de credenciais do Serviço Gerenciado do Harbor (MHS, na sigla em inglês) (docker-credential-mhs) com uma conta de usuário:

    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. Crie um arquivo de configuração do Docker para o 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. Crie o 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. Defina a lista de imagens de contêiner necessárias para a solução:

    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. Envie as imagens de contêiner necessárias para o Artifact Registry:

    ${GDCS_IMPLEMENTATION_HOME}/seed_registry.sh
    
  1. Crie um Dockerfile para corrigir o contêiner vLLM até que as mudanças upstream sejam implementadas:

    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. Crie e envie a imagem corrigida:

    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. Crie o script model_weight_upload.sh. Ele copia um diretório de modelo local para o PersistentVolume usando o carregador de modelos Pod (streaming GNU tar, incluindo arquivos maiores que 8 GiB) e ignora arquivos que já estão presentes com o mesmo tamanho:

    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"
    

Implantar um modelo