Implementazione di riferimento dell'LLM open-weight su GDC con air gap

Panoramica

Questo documento fornisce istruzioni passo passo per il deployment di modelli linguistici di grandi dimensioni (LLM) open weight come Gemma, Llama e DeepSeek negli ambienti con air gap di Google Distributed Cloud (GDC). Illustra l'utilizzo di vLLM per l'erogazione ad alto rendimento e di Ollama per la facilità d'uso, sfruttando le funzionalità della piattaforma GDC, tra cui Kubernetes, Harbor e le risorse GPU.

Architettura

La soluzione prevede il deployment di backend di gestione LLM containerizzati (vLLM, Ollama) come deployment all'interno di un cluster utente. I pesi del modello vengono archiviati nei volumi permanenti, compilati dalle immagini nel registro Harbor. I servizi Kubernetes di tipo LoadBalancer espongono le API dei backend. Le policy di rete del progetto proteggono l'accesso a questi servizi.

Diagramma dell'architettura dell'implementazione di riferimento dell'LLM open-weight.

Prima di iniziare

Assicurati che siano soddisfatti i seguenti prerequisiti:

  • GDC con air gap versione 1.15.1 o successive.
  • Cluster utente creato con risorse sufficienti (CPU, memoria, GPU).
  • È richiesta almeno una GPU NVIDIA A100.
  • L'istanza Harbor è disponibile e accessibile.
  • Interfacce a riga di comando kubectl e gdcloud configurate per accedere al cluster utente.
  • Il client Docker è installato e configurato per il push su Harbor.
  • Autorizzazioni IAM necessarie concesse (ad esempio, Amministratore spazio dei nomi, Sviluppatore cluster**).
  • Account Hugging Face e autenticazione configurati se utilizzi modelli con accesso limitato.

Sezione 1: configurazione comune

1.1 Crea il secret di pull dell'immagine

Per configurare un secret di pull di immagini per un workload container in GDC con air gap, devi creare un secret docker-registry di Kubernetes contenente le credenziali per accedere al tuo progetto Harbor privato. Questo secret viene quindi referenziato nella specifica del deployment.

Devi utilizzare un account robot Harbor per l'accesso programmatico alle immagini nei progetti Harbor privati.

Segui questi passaggi per configurare il secret di pull dell'immagine:

Crea un account robot Harbor:

  • Vai all'interfaccia utente dell'istanza Harbor.
  • Vai al tuo progetto Harbor.
  • Seleziona la scheda Account robot.
  • Fai clic su Nuovo account robot.
  • Assegna un nome (ad esempio, oss-llm-puller) e concedi le autorizzazioni necessarie (almeno l'accesso pull) fino a una data di scadenza.
  • Memorizza in modo sicuro il nome dell'account robot (ad esempio robot$oss-llm-puller) e il token segreto fornito.

Autentica Docker in Harbor:

Sulla macchina su cui è installato Docker e con accesso alla rete al registro Harbor, accedi utilizzando le credenziali dell'account robot:

export INSTANCE_URL="HARBOR_INSTANCE_URL"
# for example, harbor1-project1.org1.zone1.google.gdc.com

export ROBOT_NAME="ROBOT_ACCOUNT_NAME"
# for example, robot\$oss-llm-puller (note how we escape the $ character)

export ROBOT_SECRET="ROBOT_ACCOUNT_SECRET"

docker login ${INSTANCE_URL} --username ${ROBOT_NAME} --password ${ROBOT_SECRET}

Crea il secret di pull dell'immagine Kubernetes:

Utilizza kubectl per creare un secret di tipo docker-registry nello spazio dei nomi del tuo progetto, utilizzando il file di configurazione Docker aggiornato nel passaggio precedente:

# Log in into GDC environment using the next commands
gdcloud auth login --login-config-cert WEB_TLS_CERT_PATH
gdcloud clusters get-credentials KUBERNETES_CLUSTER
kubectl config set-context --current --namespace=NAMESPACE

export SECRET_NAME="OSS_LLM_PULL_SECRET"
export NAMESPACE="PROJECT_NAMESPACE"
# Assuming default Docker config path. Adjust if necessary.
export DOCKER_CONFIG_PATH="$HOME/.docker/config.json"

kubectl create secret docker-registry ${SECRET_NAME} \
      --from-file=.dockerconfigjson=${DOCKER_CONFIG_PATH} \
      -n ${NAMESPACE}

Sezione 2: deployment con vLLM

2.1 Scarica l'immagine Docker vLLM

Su una macchina con accesso a internet, esegui il pull dell'immagine Docker vLLM e poi trasferiscila al tuo progetto Harbor:

# Pull and Tag vLLM (v0.13.0 recommended for stability)
docker pull vllm/vllm-openai:v0.13.0
docker tag vllm/vllm-openai:v0.13.0 HARBOR_URL/PROJECT/vllm-openai:v0.13.0
docker push HARBOR_URL/PROJECT/vllm-openai:v0.13.0

Sostituisci HARBOR_URL e PROJECT con l'URL dell'istanza Harbor e il nome del progetto.

2.2 Prepara i pesi del modello nel PVC

Crea un file YAML (ad esempio, model-pvc.yaml):

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: model-pvc
spec:
  accessModes:
  - ReadWriteOnce
  resources:
    requests:
      storage: 500Gi
  storageClassName: standard-rwo
  volumeMode: Filesystem

Applica il PVC: kubectl apply -f model-pvc.yaml

Scarica i pesi da Hugging Face:

hf auth login
hf download google/gemma-3-4b-it

Utilizza un pod helper (ad esempio helper-pod.yaml) per trasferire i pesi al PVC. Assicurati di avere un'immagine busybox in Harbor.

# Push busybox if not present
docker pull busybox:latest
docker tag busybox HARBOR_URL/PROJECT/busybox:latest
docker push HARBOR_URL/PROJECT/busybox:latest

# Contents of helper-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: model-uploader
spec:
  containers:
  - name: uploader
    image: HARBOR_URL/PROJECT/busybox:latest
    command: ["sleep", "3600"]
    volumeMounts:
    - name: model-data
      mountPath: /data
  imagePullSecrets:
  - name: oss-llm-pull-secret
  volumes:
  - name: model-data
    persistentVolumeClaim:
      claimName: model-pvc

Applica il pod e copia i file:

kubectl apply -f helper-pod.yaml
# Wait for pod to be Running
kubectl cp ~/.cache/huggingface/hub/ NAMESPACE/model-uploader:/data/
kubectl delete pod model-uploader

2.3 Esegui il deployment del backend vLLM

Crea il file di deployment vllm-gemma-3-4b-it-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gemma-3-4b-it
  labels:
    app: gemma-3-4b-it
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gemma-3-4b-it
  template:
    metadata:
      labels:
        app: gemma-3-4b-it
    spec:
      volumes:
      - name: cache-volume
        persistentVolumeClaim:
          claimName: model-pvc
      - name: shm
        emptyDir:
          medium: Memory
          sizeLimit: "16Gi"
      containers:
      - name: gemma-3-4b-it
        image: HARBOR_URL/PROJECT/vllm-openai:v0.13.0
        command: ["python3"]
        args: [
          "-m",
          "vllm.entrypoints.openai.api_server",
          "--model",
          "google/gemma-3-4b-it",
          "--max-model-len",
          "32768",
          "--enforce-eager"
        ]
        env:
        - name: HF_HUB_OFFLINE
          value: "1"
        - name: HF_HOME
          value: "/model"
        - name: NCCL_P2P_DISABLE
          value: "1"
        - name: NCCL_IB_DISABLE
          value: "1"
        - name: BORINGSSL_FIPS
          value: "0"
        - name: OPENSSL_FIPS
          value: "0"
        - name: OPENSSL_CONF
          value: "/dev/null"
        - name: FIPS_SIG
          value: "off"
        ports:
        - containerPort: 8000
        securityContext:
          privileged: true
          runAsUser: 0
        resources:
          limits:
            nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
            cpu: "8"
            memory: "64Gi"
          requests:
            nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
            cpu: "8"
            memory: "32Gi"
        volumeMounts:
        - name: cache-volume
          mountPath: /model
        - name: shm
          mountPath: /dev/shm
      imagePullSecrets:
      - name: oss-llm-pull-secret

Crea il file di servizio vllm-gemma-3-4b-it-service.yaml:

apiVersion: v1
kind: Service
metadata:
  name: gemma-3-4b-it
  namespace: NAMESPACE
spec:
  ports:
  - name: http-gemma-3-4b-it
    port: 80
    protocol: TCP
    targetPort: 8000
  selector:
    app: gemma-3-4b-it
  sessionAffinity: None
  type: LoadBalancer

Applica le configurazioni:

kubectl apply -f vllm-gemma-3-4b-it-deployment.yaml
kubectl apply -f vllm-gemma-3-4b-it-service.yaml

2.4 Configura la policy di rete

Applica una risorsa ProjectNetworkPolicy per consentire il traffico in entrata alla porta del servizio vLLM (8000). Crea vllm-netpol.yaml:

apiVersion: networking.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
  name: allow-vllm-ingress
  namespace: NAMESPACE
spec:
  subject:
    subjectType: UserWorkload
  policyType: Ingress
  ingress:
  - from:
    - ipBlock:
        cidr: 0.0.0.0/0 # Restrict this in production
    ports:
    - protocol: TCP
      port: 8000

Applica la policy: kubectl apply -f vllm-netpol.yaml

Sezione 3: deployment con Ollama

3.1 Prepara Dockerfile

Crea un Dockerfile per creare l'immagine Ollama con i tuoi modelli precaricati:

FROM ubuntu

RUN apt-get update && apt-get install -y --no-install-recommends curl ca-certificates zstd
RUN curl -fsSL https://ollama.com/install.sh -o install.sh
RUN chmod +x install.sh
RUN ./install.sh && \
    rm -rf /var/lib/apt/lists/*

# Pre-pull gemma3 model
RUN ollama serve & \
    sleep 5 && \
    curl --retry 10 --retry-connrefused -s http://localhost:11434 || true && \
    ollama pull gemma3:latest && \
    pkill ollama || true

EXPOSE 11434
CMD ["ollama", "serve"]

3.2 Crea ed esegui il push dell'immagine

Crea ed esegui il push dell'immagine:

docker build -t ollama-gemma3 .
docker tag ollama-gemma3 HARBOR_URL/PROJECT/ollama-gemma3:latest
docker push HARBOR_URL/PROJECT/ollama-gemma3:latest

3.3 Esegui il deployment del backend Ollama

Crea ollama-gemma3.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ollama-gemma3
  namespace: NAMESPACE
  labels:
    app: ollama-gemma3
spec:
  replicas: 1
  selector:
    matchLabels:
      app: ollama-gemma3
  template:
    metadata:
      labels:
        app: ollama-gemma3
    spec:
      containers:
      - name: ollama-gemma3
        image: HARBOR_URL/PROJECT/ollama-gemma3:latest
        env:
        - name: OLLAMA_HOST
          value: "0.0.0.0"
        imagePullPolicy: Always
        ports:
        - containerPort: 11434
        securityContext:
          privileged: true
          runAsUser: 0
        resources:
          limits:
            nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
          requests:
            nvidia.com/gpu-pod-NVIDIA_A100_80GB_PCIE: 1
      imagePullSecrets:
      - name: oss-llm-pull-secret
---
apiVersion: v1
kind: Service
metadata:
  name: ollama-gemma3
  namespace: NAMESPACE
spec:
  type: LoadBalancer
  selector:
    app: ollama-gemma3
  ports:
  - name: ollama-gemma3-port
    port: 11434
    protocol: TCP
    targetPort: 11434

Applica il manifest: kubectl apply -f ollama-gemma3.yaml

3.4 Configura la policy di rete

Crea ollama-netpol.yaml:

apiVersion: networking.gdc.goog/v1
kind: ProjectNetworkPolicy
metadata:
  name: allow-ollama-ingress
  namespace: NAMESPACE
spec:
  subject:
    subjectType: UserWorkload
  policyType: Ingress
  ingress:
  - from:
    - ipBlock:
        cidr: 0.0.0.0/0 # Restrict this for production.
    ports:
    - protocol: TCP
      port: 11434

Applica la policy: kubectl apply -f ollama-netpol.yaml

Sezione 4: convalida

Verifica i deployment controllando gli stati dei pod, gli indirizzi IP del servizio e inviando richieste di inferenza di test utilizzando curl agli indirizzi IP LoadBalancer sia per vLLM che per Ollama.

Verifica che tutti i container e i servizi siano Running:

# Login into GDC air-gapped using the next commands
gdcloud auth login --login-config-cert WEB_TLS_CERT_PATH
gdcloud clusters get-credentials KUBERNETES_CLUSTER
kubectl config set-context --current --namespace=NAMESPACE

# Pods
kubectl get pods

# Services
kubectl get services

Testa vLLM:

export VLLM_IP=$(kubectl get service gemma-3-4b-it -n NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[*].ip}')
curl http://${VLLM_IP}/v1/chat/completion \
  -H "Content-Type: application/json" \
  -d '{
    "model": "google/gemma-3-4b-it",
    "messages": [
      {"role": "user", "content": "What is Google Distributed Cloud air-gapped?"}
    ],
    "max_tokens": 100
  }'

Testa Ollama:

export OLLAMA_IP=$(kubectl get service ollama-gemma3 -n NAMESPACE -o jsonpath='{.status.loadBalancer.ingress[*].ip}')
# Check if Ollama is running
curl http://${OLLAMA_IP}:11434
# Send a completion request
curl -X POST http://${OLLAMA_IP}:11434/v1/completions \
-H "Content-Type: application/json" \
-d '{
  "model": "gemma3:latest",
  "prompt": "Google Distributed Cloud air-gapped is a",
  "max_tokens": 128,
  "temperature": 0.90,
  "stream": false
}'

Sezione 5: Operazioni e risoluzione dei problemi

5.1 Operazioni vLLM

Controlla i log: kubectl logs -f -n NAMESPACE

Esegui query sullo stato interno (da un pod di debug):

wget -qO- http://gemma-3-4b-it/v1/models
wget -qO- http://gemma-3-4b-it/health

5.2 Operazioni Ollama

Accedi alla CLI: kubectl exec -it -n NAMESPACE -- sh

All'interno del pod: ollama list, ollama ps

5.3 Scalabilità

Scalare la soluzione con un backend Ollama

Verticalmente

  • Alloca una fetta di GPU più grande per il tuo LLM che è diventato un collo di bottiglia finché non utilizzi una GPU completa.
  • Se vuoi utilizzare un LLM più grande per una migliore precisione della risposta, ad esempio un LLM con 405 miliardi di parametri anziché uno con 7 miliardi, potresti aver bisogno di più di una GPU per eseguirlo senza problemi.
  • I modelli che rientrano in più di una GPU subiscono una latenza correlata alla comunicazione tra GPU.

Orizzontalmente

  • Esegui il deployment di tutti i pod Ollama necessari per raggiungere il throughput target su un LLM specifico.
    • Per farlo, aumenta il numero di repliche nel file YAML di deployment di Ollama corrispondente.
  • Il servizio Kubernetes, di tipo LoadBalancer, distribuirà le richieste di assistenza per il codice tra gli endpoint (ovvero i pod) e restituirà le rispettive risposte tramite l'IP esterno esposto.
  • Ricorda che il plug-in Continua punta a un solo indirizzo IP per funzionalità.

Diagramma dell'architettura di scalabilità degli LLM open-weight.

Per scalare il backend vLLM all'interno dell'ambiente GDC air-gap, puoi seguire una strategia simile a quella utilizzata per Ollama, concentrandoti sia sull'allocazione delle risorse hardware sia sulla replica dei pod.

Scalare la soluzione con un backend vLLM

Verticalmente

  • Esegui l'upgrade dell'allocazione della GPU:se la velocità effettiva di inferenza (token/sec) diventa un collo di bottiglia, alloca una porzione di GPU più grande finché non utilizzi una GPU NVIDIA A100 completa.
  • Configurazioni multi-GPU: per modelli di grandi dimensioni (ad esempio, da 70 miliardi a 405 miliardi di parametri) che non rientrano nella memoria di una singola A100 da 80 GB, devi scalare a più GPU utilizzando il parallelismo dei tensori.
  • Considerazione della latenza: tieni presente che i modelli che si estendono su più GPU potrebbero riscontrare un leggero sovraccarico correlato alla comunicazione tra GPU (ad esempio, la sincronizzazione NCCL).

Orizzontalmente

  • Aumenta la velocità effettiva tramite le repliche: per gestire un volume maggiore di richieste simultanee degli utenti per lo stesso modello, aumenta il conteggio delle repliche nel file YAML del deployment vLLM.
  • Istanze di modelli dedicate: poiché vLLM è progettato come motore di servizio di un singolo modello e blocca la memoria della cache KV all'inizializzazione, devi eseguire il deployment di un insieme separato di pod per ogni LLM diverso che vuoi ospitare.
  • Bilanciamento del carico: il servizio GDC Kubernetes (tipo LoadBalancer) distribuirà automaticamente le richieste di inferenza in entrata tra tutti gli endpoint pod vLLM integri associati a quel servizio.

5.4 Risoluzione dei problemi

Errori comuni e mitigazioni.

Errore Mitigazione
FIPS SELFTEST FAILURE Si verifica quando le librerie come BoringSSL non hanno firme di integrità. Correzione impostando BORINGSSL_FIPS=0 e utilizzando immagini vLLM ufficiali
Caricamento del peso bloccato Verifica la presenza di limitazione degli IOPS sui PVC di piccole dimensioni. Per l'inizializzazione del modello di prestazioni è necessario un volume di 500 GiB.
Connessione rifiutata Verifica che PNP consenta esplicitamente targetPort (8000/8080). I firewall GDC non concedono automaticamente l'accesso alle porte di backend per gli indirizzi IP virtuali del bilanciatore del carico.