Implémentation de référence de LLM de poids ouvert sur GDC sous air gap

Présentation

Ce document fournit des instructions détaillées pour déployer des grands modèles de langage (LLM) open source tels que Gemma, Llama et DeepSeek dans des environnements Google Distributed Cloud (GDC) air-gapped. Il explique comment utiliser vLLM pour le service à haut débit et Ollama pour la facilité d'utilisation, en exploitant les capacités de la plate-forme GDC, y compris Kubernetes, Harbor et les ressources GPU.

Architecture

La solution consiste à déployer des backends de diffusion LLM conteneurisés (vLLM, Ollama) en tant que déploiements dans un cluster d'utilisateur. Les pondérations du modèle sont stockées dans des volumes persistants, qui sont remplis à partir d'images du registre Harbor. Les services Kubernetes de type LoadBalancer exposent les API des backends. Les règles de réseau du projet sécurisent l'accès à ces services.

Diagramme de l'architecture de l'implémentation de référence du LLM à poids ouvert.

Avant de commencer

Assurez-vous de remplir les conditions préalables suivantes :

  • GDC sous air gap version 1.15.1 ou ultérieure.
  • Cluster utilisateur créé avec des ressources suffisantes (CPU, mémoire, GPU).
  • Un GPU NVIDIA A100 minimum est requis.
  • L'instance Harbor est disponible et accessible.
  • Les CLI kubectl et gdcloud sont configurées pour accéder au cluster d'utilisateur.
  • Le client Docker est installé et configuré pour envoyer des données à Harbor.
  • Les autorisations IAM nécessaires sont accordées (par exemple, Administrateur de l'espace de noms, développeur de cluster**).
  • Un compte Hugging Face et une authentification configurés si vous utilisez des modèles avec accès restreint.

Section 1 : Configuration courante

1.1 Créer un secret d'extraction d'image

Pour configurer un secret d'extraction d'image pour une charge de travail de conteneur dans GDC sous air gap, vous devez créer un secret Kubernetes docker-registry contenant les identifiants permettant d'accéder à votre projet Harbor privé. Ce secret est ensuite référencé dans votre spécification de déploiement.

Vous devez utiliser un compte robot Harbor pour accéder de manière programmatique aux images dans les projets Harbor privés.

Pour configurer le secret d'extraction d'image, procédez comme suit :

Créez un compte robot Harbor :

  • Accédez à l'UI de votre instance Harbor.
  • Accédez à votre projet Harbor.
  • Sélectionnez l'onglet Comptes robot.
  • Cliquez sur Nouveau compte robot.
  • Donnez-lui un nom (par exemple, oss-llm-puller) et accordez-lui les autorisations nécessaires (au moins l'accès pull) jusqu'à une heure d'expiration.
  • Stockez de manière sécurisée le nom du compte robot (par exemple, robot$oss-llm-puller) et le jeton secret fourni.

Authentifiez Docker auprès de Harbor :

Sur votre machine sur laquelle Docker est installé et qui a accès au réseau du registre Harbor, connectez-vous à l'aide des identifiants du compte 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}

Créez le secret Kubernetes pour l'extraction d'images :

Utilisez kubectl pour créer un secret de type docker-registry dans l'espace de noms de votre projet, à l'aide du fichier de configuration Docker mis à jour à l'étape précédente :

# 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}

Section 2 : Déployer avec vLLM

2.1 Obtenir l'image Docker vLLM

Sur une machine ayant accès à Internet, extrayez l'image Docker vLLM, puis transférez-la vers votre projet 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

Remplacez HARBOR_URL et PROJECT par l'URL de votre instance Harbor et le nom du projet.

2.2 Préparer les pondérations du modèle dans un PVC

Créez un fichier YAML (par exemple, model-pvc.yaml) :

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

Appliquez la PVC : kubectl apply -f model-pvc.yaml

Téléchargez les pondérations depuis Hugging Face :

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

Utilisez un pod d'assistance (par exemple, helper-pod.yaml) pour transférer les poids vers la PVC. Assurez-vous d'avoir une image busybox dans 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

Appliquer le pod et copier les fichiers :

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 Déployer le backend vLLM

Créez le fichier de déploiement 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

Créez le fichier de service 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

Appliquez les configurations :

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

2.4 Configurer une règle de réseau

Appliquez une ressource ProjectNetworkPolicy pour autoriser le trafic entrant vers le port du service vLLM (8000). Créez 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

Appliquez la règle : kubectl apply -f vllm-netpol.yaml

Section 3 : Déployer avec Ollama

3.1 Préparer le fichier Dockerfile

Créez un Dockerfile pour générer l'image Ollama avec vos modèles préchargés :

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 Créer et transférer l'image

Créez et transférez l'image:

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 Déployer le backend Ollama

Créez 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

Appliquez le fichier manifeste : kubectl apply -f ollama-gemma3.yaml

3.4 Configurer une règle de réseau

Créez 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

Appliquez la règle : kubectl apply -f ollama-netpol.yaml

Section 4 : Validation

Vérifiez les déploiements en vérifiant l'état des pods et les adresses IP des services, et en envoyant des requêtes d'inférence de test à l'aide de curl aux adresses IP LoadBalancer pour vLLM et Ollama.

Vérifiez que tous les conteneurs et services sont 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

Tester 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
  }'

Tester 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
}'

Section 5 : Opérations et dépannage

5.1 Opérations vLLM

Vérifiez les journaux : kubectl logs -f -n NAMESPACE

Interrogez l'état interne (à partir d'un pod de débogage) :

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

5.2 Opérations Ollama

Accéder à la CLI : kubectl exec -it -n NAMESPACE -- sh

Dans le pod : ollama list, ollama ps

5.3 Mise à l'échelle

Faire évoluer la solution avec un backend Ollama

Verticalement

  • Allouez une tranche de GPU plus grande à votre LLM devenu un goulot d'étranglement jusqu'à ce que vous utilisiez un GPU complet.
  • Si vous souhaitez utiliser un LLM plus grand pour améliorer la précision des réponses (par exemple, un LLM à 405 milliards de paramètres au lieu d'un LLM à 7 milliards), vous aurez peut-être besoin de plusieurs GPU pour l'exécuter correctement.
  • Les modèles qui tiennent dans plusieurs GPU subissent une certaine latence liée à la communication entre les GPU.

Horizontalement

  • Déployez autant de pods Ollama que nécessaire pour atteindre le débit cible sur un LLM donné.
    • Pour ce faire, augmentez le nombre de réplicas dans le fichier YAML de déploiement Ollama correspondant.
  • Le service Kubernetes, de type LoadBalancer, distribuera les demandes d'assistance au code entre les points de terminaison (c'est-à-dire les pods) et renverra leurs réponses respectives via l'adresse IP externe exposée.
  • N'oubliez pas que le plug-in Continue ne pointe que vers une seule adresse IP par fonctionnalité.

Diagramme de l'architecture de scaling des LLM open-weight.

Pour mettre à l'échelle le backend vLLM dans votre environnement GDC isolé, vous pouvez suivre une stratégie similaire à celle utilisée pour Ollama, en vous concentrant à la fois sur l'allocation des ressources matérielles et sur la réplication des pods.

Mettre à l'échelle la solution avec un backend vLLM

Verticalement

  • Mettre à niveau l'allocation de GPU : si le débit d'inférence (jetons/s) devient un goulot d'étranglement, allouez une tranche de GPU plus importante jusqu'à ce que vous utilisiez un GPU NVIDIA A100 complet.
  • Configurations multi-GPU : pour les modèles massifs (par exemple, de 70 milliards à 405 milliards de paramètres) qui ne tiennent pas dans la mémoire d'un seul GPU A100 de 80 Go, vous devez passer à plusieurs GPU à l'aide du parallélisme de tenseur.
  • Latence : notez que les modèles s'étendant sur plusieurs GPU peuvent entraîner une légère surcharge liée à la communication entre les GPU (par exemple, la synchronisation NCCL).

Horizontalement

  • Augmenter le débit grâce aux répliques : pour gérer un volume plus élevé de requêtes utilisateur simultanées pour le même modèle, augmentez le nombre de répliques dans le fichier YAML de votre déploiement vLLM.
  • Instances de modèle dédiées : comme vLLM est conçu comme un moteur de diffusion de modèle unique et épingle sa mémoire cache KV lors de l'initialisation, vous devez déployer un ensemble distinct de pods pour chaque LLM différent que vous souhaitez héberger.
  • Équilibrage de charge : le service GDC Kubernetes (type LoadBalancer) distribue automatiquement les requêtes d'inférence entrantes entre tous les points de terminaison de pod vLLM sains associés à ce service.

5.4 Dépannage

Erreurs courantes et mesures d'atténuation

Erreur Atténuation
ÉCHEC DE L'AUTOTEST FIPS Se produit lorsque des bibliothèques telles que BoringSSL ne disposent pas de signatures d'intégrité. Corriger en définissant BORINGSSL_FIPS=0 et en utilisant les images vLLM officielles
Chargement de poids bloqué Vérifiez si les IOPS sont limitées sur les petits PVC. Un volume de 500 Gio est requis pour l'initialisation du modèle de performances.
Connexion refusée Vérifiez que le PNP autorise explicitement le targetPort (8000/8080). Les pare-feu GDC n'accordent pas automatiquement l'accès aux ports de backend pour les adresses IP virtuelles de l'équilibreur de charge.