Open-Weight-LLM-Referenzimplementierung in GDC mit Air Gap

Übersicht

In diesem Dokument finden Sie eine detaillierte Anleitung zum Bereitstellen von Open-Weight-LLMs (Large Language Models) wie Gemma, Llama und DeepSeek in GDC-Umgebungen (Google Distributed Cloud) mit Air Gap. Dabei wird sowohl vLLM für die Bereitstellung mit hohem Durchsatz als auch Ollama für die einfache Verwendung genutzt. Außerdem werden die Funktionen der GDC-Plattform genutzt, darunter Kubernetes, Harbor und GPU-Ressourcen.

Architektur

Die Lösung umfasst die Bereitstellung von containerisierten LLM-Bereitstellungs-Back-Ends (vLLM, Ollama) als Deployments in einem Nutzercluster. Modellgewichte werden auf nichtflüchtigen Volumes gespeichert, die aus Bildern in der Harbor-Registry gefüllt werden. Kubernetes-Dienste vom Typ „LoadBalancer“ machen die APIs der Back-Ends verfügbar. Richtlinien für das Projektnetzwerk sichern den Zugriff auf diese Dienste.

Architekturdiagramm der Referenzimplementierung für Open-Weight-LLMs.

Hinweis

Prüfen Sie, ob die folgenden Voraussetzungen erfüllt sind:

  • GDC mit Air Gap, Version 1.15.1 oder höher.
  • Der Nutzercluster wurde mit ausreichenden Ressourcen (CPU, Arbeitsspeicher, GPU) erstellt.
  • Es ist mindestens eine NVIDIA A100-GPU erforderlich.
  • Die Harbor-Instanz ist verfügbar und zugänglich.
  • Die kubectl- und gdcloud-Befehlszeilen sind für den Zugriff auf den Nutzercluster konfiguriert.
  • Der Docker-Client ist installiert und für das Pushen in Harbor konfiguriert.
  • Die erforderlichen IAM-Berechtigungen wurden gewährt (z. B. Namespace-Administrator, Cluster Developer**).
  • Wenn Sie Modelle mit eingeschränktem Zugriff verwenden, müssen Sie ein Hugging Face-Konto haben und die Authentifizierung muss konfiguriert sein.

Abschnitt 1: Gemeinsame Einrichtung

1.1 Image-Pull-Secret erstellen

Wenn Sie ein Image-Pull-Secret für eine Containerarbeitslast in GDC mit Air Gap konfigurieren möchten, müssen Sie ein Kubernetes-Secret vom Typ „docker-registry“ erstellen, das Anmeldedaten für den Zugriff auf Ihr privates Harbor-Projekt enthält. Auf dieses Secret wird dann in Ihrer Bereitstellungsspezifikation verwiesen.

Für den programmatischen Zugriff auf Bilder in privaten Harbor-Projekten sollten Sie ein Harbor-Roboterkonto verwenden.

So konfigurieren Sie das Image-Pull-Secret:

Harbor-Roboterkonto erstellen:

  • Rufen Sie die Benutzeroberfläche Ihrer Harbor-Instanz auf.
  • Rufen Sie Ihr Harbor-Projekt auf.
  • Klicken Sie auf den Tab Robot Accounts (Roboterkonten).
  • Klicken Sie auf Neues Roboterkonto.
  • Geben Sie einen Namen an (z. B. oss-llm-puller) und gewähren Sie die erforderlichen Berechtigungen (mindestens pull-Zugriff) bis zu einem Ablaufdatum.
  • Bewahren Sie den Namen des Roboterkontos (z. B. robot$oss-llm-puller) und das bereitgestellte geheime Token sicher auf.

Docker für Harbor authentifizieren:

Melden Sie sich auf Ihrem Computer, auf dem Docker installiert ist und der Netzwerkzugriff auf die Harbor-Registry besteht, mit den Anmeldedaten des Roboter-Kontos an:

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}

Kubernetes-Secret zum Abrufen von Images erstellen:

Erstellen Sie mit kubectl ein Secret vom Typ „docker-registry“ in Ihrem Projektnamespace. Verwenden Sie dazu die im vorherigen Schritt aktualisierte Docker-Konfigurationsdatei:

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

Abschnitt 2: Mit vLLM bereitstellen

2.1 vLLM-Docker-Image abrufen

Rufen Sie auf einem Computer mit Internetzugriff das vLLM-Docker-Image ab und übertragen Sie es dann in Ihr Harbor-Projekt:

# 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

Ersetzen Sie HARBOR_URL und PROJECT durch die URL Ihrer Harbor-Instanz und den Projektnamen.

2.2 Modellgewichte im PVC vorbereiten

Erstellen Sie eine YAML-Datei (z. B. model-pvc.yaml):

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

Wenden Sie das PVC an: kubectl apply -f model-pvc.yaml

Gewichtungen von Hugging Face herunterladen:

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

Verwenden Sie einen Hilfspod (z. B. helper-pod.yaml), um Gewichte auf den PVC zu übertragen. Prüfen Sie, ob Sie ein busybox-Image in Harbor haben.

# 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

Pod anbringen und Dateien kopieren:

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 vLLM-Back-End bereitstellen

Erstellen Sie die Bereitstellungsdatei 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

Erstellen Sie die Dienstdatei 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

Wenden Sie die Konfigurationen an:

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

2.4 Netzwerkrichtlinie konfigurieren

Wenden Sie eine ProjectNetworkPolicy-Ressource an, um eingehenden Traffic zum vLLM-Dienstport (8000) zuzulassen. vllm-netpol.yaml erstellen:

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

Wenden Sie die Richtlinie an: kubectl apply -f vllm-netpol.yaml

Abschnitt 3: Mit Ollama bereitstellen

3.1 Dockerfile vorbereiten

Erstellen Sie ein Dockerfile, um das Ollama-Image mit den vorab geladenen Modellen zu erstellen:

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 Image erstellen und per Push übertragen

Erstellen Sie das Image und übertragen Sie es per Push:

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 Ollama-Back-End bereitstellen

ollama-gemma3.yaml erstellen:

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

Wenden Sie das Manifest an: kubectl apply -f ollama-gemma3.yaml

3.4 Netzwerkrichtlinie konfigurieren

ollama-netpol.yaml erstellen:

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

Wenden Sie die Richtlinie an: kubectl apply -f ollama-netpol.yaml

Abschnitt 4: Validierung

Prüfen Sie die Bereitstellungen, indem Sie den Pod-Status und die Service-IP-Adressen prüfen und Testanfragen mit curl an die LoadBalancer-IP-Adressen für vLLM und Ollama senden.

Prüfen Sie, ob alle Container und Dienste Running sind:

# 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

vLLM testen:

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

Ollama testen:

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

Abschnitt 5: Betrieb und Fehlerbehebung

5.1 vLLM-Vorgänge

Logs prüfen: kubectl logs -f -n NAMESPACE

Internen Status abfragen (über einen Debug-Pod):

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

5.2 Ollama-Vorgänge

Auf die CLI zugreifen: kubectl exec -it -n NAMESPACE -- sh

Im Pod: ollama list, ollama ps

5.3 Skalierung

Lösung mit einem Ollama-Back-End skalieren

Vertikal

  • Weisen Sie Ihrem LLM, das zum Engpass geworden ist, einen größeren GPU-Slice zu, bis Sie eine vollständige GPU verwenden.
  • Wenn Sie ein größeres LLM für eine bessere Antwortgenauigkeit verwenden möchten, z. B. ein LLM mit 405 Milliarden Parametern anstelle eines mit 7 Milliarden, benötigen Sie möglicherweise mehr als eine GPU, um es reibungslos auszuführen.
  • Bei Modellen, die auf mehr als eine GPU passen, kann es zu Latenz aufgrund der GPU-übergreifenden Kommunikation kommen.

Horizontal

  • Stellen Sie so viele Ollama-Pods bereit, wie Sie für den gewünschten Durchsatz für ein bestimmtes LLM benötigen.
    • Erhöhen Sie dazu die replica-Anzahl in der entsprechenden YAML-Datei für die Ollama-Bereitstellung.
  • Der Kubernetes-Dienst vom Typ LoadBalancer verteilt Anfragen zur Codeunterstützung zwischen Endpunkten (d.h. Pods) und gibt die entsprechenden Antworten über die bereitgestellte externe IP-Adresse zurück.
  • Das Continue-Plug-in verweist nur auf eine IP-Adresse pro Funktion.

Architekturdiagramm für die Skalierung von LLMs mit offenem Gewicht.

Um das vLLM-Backend in Ihrer GDC-Air-Gap-Umgebung zu skalieren, können Sie eine ähnliche Strategie wie für Ollama verwenden, die sich sowohl auf die Zuweisung von Hardwareressourcen als auch auf die Pod-Replikation konzentriert.

Lösung mit einem vLLM-Backend skalieren

Vertikal

  • GPU-Zuweisung upgraden:Wenn der Inferenzdurchsatz (Tokens/Sekunde) zu einem Engpass wird, weisen Sie einen größeren GPU-Slice zu, bis Sie eine vollständige NVIDIA A100-GPU verwenden.
  • Konfigurationen mit mehreren GPUs: Bei umfangreichen Modellen (z. B. mit 70 Mrd. bis 405 Mrd. Parametern), die nicht in den Arbeitsspeicher einer einzelnen A100-GPU mit 80 GB passen, müssen Sie mit Tensor-Parallelität auf mehrere GPUs skalieren.
  • Latenz: Modelle, die sich über mehrere GPUs erstrecken, können einen leichten Overhead in Bezug auf die GPU-übergreifende Kommunikation (z. B. NCCL-Synchronisierung) aufweisen.

Horizontal

  • Durchsatz über Replikate erhöhen: Wenn Sie ein höheres Volumen gleichzeitiger Nutzeranfragen für dasselbe Modell verarbeiten möchten, erhöhen Sie die Anzahl der Replikate in der YAML-Datei für die vLLM-Bereitstellung.
  • Dedizierte Modellinstanzen: Da vLLM als Serving-Engine für ein einzelnes Modell konzipiert ist und den KV-Cache-Speicher bei der Initialisierung festlegt, müssen Sie für jedes LLM, das Sie hosten möchten, eine separate Gruppe von Pods bereitstellen.
  • Load-Balancing: Der GDC Kubernetes-Dienst (Typ „LoadBalancer“) verteilt eingehende Inferenzanfragen automatisch auf alle fehlerfreien vLLM-Pod-Endpunkte, die diesem Dienst zugeordnet sind.

5.4 Fehlerbehebung

Häufige Fehler und Gegenmaßnahmen.

Fehler Problembehebung
FIPS-SELBSTTEST FEHLGESCHLAGEN Tritt auf, wenn Bibliotheken wie BoringSSL keine Integritätssignaturen haben. Fehler beheben, indem Sie BORINGSSL_FIPS=0 festlegen und offizielle vLLM-Images verwenden
Gewichtsladen fehlgeschlagen Prüfen Sie, ob die IOPS bei kleinen PVCs gedrosselt werden. Für die Initialisierung des Leistungsmodells ist ein Volume mit 500 GiB erforderlich.
Verbindung verweigert Prüfen Sie, ob der PNP den targetPort (8000/8080) explizit zulässt. GDC-Firewalls gewähren nicht automatisch Zugriff auf Back-End-Ports für Load-Balancer-VIPs.