Implementación de referencia de LLM de peso abierto en GDC aislado

Descripción general

En este documento, se proporcionan instrucciones paso a paso para implementar modelos de lenguaje grandes (LLM) de código abierto, como Gemma, Llama y DeepSeek, en entornos aislados de Google Distributed Cloud (GDC). En él, se explica cómo usar vLLM para la entrega de alto rendimiento y Ollama para facilitar el uso, aprovechando las capacidades de la plataforma de GDC, incluidos los recursos de Kubernetes, Harbor y GPU.

Arquitectura

La solución implica implementar backends de servicio de LLM en contenedores (vLLM, Ollama) como implementaciones dentro de un clúster de usuario. Los pesos del modelo se almacenan en volúmenes persistentes, que se completan a partir de imágenes en el registro de Harbor. Los servicios de Kubernetes de tipo LoadBalancer exponen las APIs de los backends. Las políticas de red del proyecto protegen el acceso a estos servicios.

Diagrama de arquitectura de la implementación de referencia del LLM de código abierto.

Antes de comenzar

Asegúrate de que se cumplan los siguientes requisitos:

  • Versión 1.15.1 o posterior de GDC aislado
  • Se creó un clúster de usuario con recursos suficientes (CPU, memoria y GPU).
  • Se requiere un mínimo de 1 GPU NVIDIA A100.
  • La instancia de Harbor está disponible y se puede acceder a ella.
  • CLIs de kubectl y gdcloud configuradas para acceder al clúster de usuario
  • El cliente de Docker está instalado y configurado para enviar a Harbor.
  • Se otorgaron los permisos de IAM necesarios (por ejemplo, Administrador de espacio de nombres, desarrollador de clústeres**).
  • Cuenta de Hugging Face y autenticación configuradas si se usan modelos con acceso restringido

Sección 1: Configuración común

1.1 Crea un Secret de extracción de imagen

Para configurar un secreto de extracción de imágenes para una carga de trabajo de contenedor en GDC aislado, debes crear un secreto de registro de Docker de Kubernetes que contenga credenciales para acceder a tu proyecto privado de Harbor. Luego, se hace referencia a este secreto en la especificación de implementación.

Debes usar una cuenta de robot de Harbor para acceder de forma programática a las imágenes en proyectos privados de Harbor.

Sigue estos pasos para configurar el secreto de extracción de imágenes:

Crea una cuenta de robot de Harbor:

  • Navega a la IU de tu instancia de Harbor.
  • Ve a tu proyecto de Harbor.
  • Selecciona la pestaña Cuentas de robot.
  • Haz clic en New Robot Account.
  • Asigna un nombre (por ejemplo, oss-llm-puller) y otórgale los permisos necesarios (al menos acceso de extracción) hasta una hora de vencimiento.
  • Almacena de forma segura el nombre de la cuenta del robot (por ejemplo, robot$oss-llm-puller) y el token secreto proporcionado.

Autentica Docker en Harbor:

En tu máquina con Docker instalado y acceso de red al registro de Harbor, accede con las credenciales de la cuenta de 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 el secreto de extracción de imágenes de Kubernetes:

Usa kubectl para crear un secreto de tipo docker-registry en el espacio de nombres de tu proyecto con el archivo de configuración de Docker que actualizaste en el paso anterior:

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

Sección 2: Implementación con vLLM

2.1 Obtén la imagen de Docker de vLLM

En una máquina con acceso a Internet, extrae la imagen de Docker de vLLM y, luego, transfiérela a tu proyecto de 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

Reemplaza HARBOR_URL y PROJECT por la URL de tu instancia de Harbor y el nombre del proyecto.

2.2 Prepara las ponderaciones del modelo en el PVC

Crea un archivo YAML (por ejemplo, model-pvc.yaml):

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

Aplica el PVC: kubectl apply -f model-pvc.yaml

Descarga los pesos de Hugging Face:

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

Usa un pod auxiliar (por ejemplo, helper-pod.yaml) para transferir pesos al PVC. Asegúrate de tener una imagen de busybox en 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

Aplica el pod y copia los archivos:

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 Implementa el backend de vLLM

Crea el archivo de implementación 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 el archivo de servicio 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

Aplica la configuración:

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

2.4 Configura la política de red

Aplica un recurso ProjectNetworkPolicy para permitir el tráfico de entrada al puerto de servicio de 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

Aplica la política: kubectl apply -f vllm-netpol.yaml

Sección 3: Implementación con Ollama

3.1 Prepara el Dockerfile

Crea un Dockerfile para compilar la imagen de Ollama con los modelos precargados:

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 Compila y envía la imagen

Compila y envía la imagen:

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 Implementa el backend de 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

Aplica el manifiesto: kubectl apply -f ollama-gemma3.yaml

3.4 Configura la política de red

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

Aplica la política: kubectl apply -f ollama-netpol.yaml

Sección 4: Validación

Verifica las implementaciones comprobando los estados de los Pods, las direcciones IP de los servicios y enviando solicitudes de inferencia de prueba con curl a las direcciones IP de LoadBalancer para vLLM y Ollama.

Verifica que todos los contenedores y servicios sean 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

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

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

Sección 5: Operaciones y solución de problemas

5.1 Operaciones de vLLM

Verifica los registros: kubectl logs -f -n NAMESPACE

Estado interno de la búsqueda (desde un pod de depuración):

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

5.2 Operaciones de Ollama

Accede a la CLI: kubectl exec -it -n NAMESPACE -- sh

Dentro del pod: ollama list, ollama ps

5.3 Escalabilidad

Cómo escalar la solución con un backend de Ollama

Verticalmente

  • Asigna una porción de GPU más grande a tu LLM, que se convirtió en un cuello de botella, hasta que uses una GPU completa.
  • Si quieres usar un LLM más grande para obtener una mejor precisión en las respuestas, por ejemplo, un LLM con 405 mil millones de parámetros en lugar de uno con 7 mil millones, es posible que necesites más de una GPU para ejecutarlo sin problemas.
  • Los modelos que se ajustan a más de una GPU experimentan cierta latencia relacionada con la comunicación entre GPU.

Horizontalmente

  • Implementa tantos Pods de Ollama como necesites para alcanzar el rendimiento objetivo en un LLM determinado.
    • Para lograr esto, aumenta la cantidad de replicas en el archivo YAML de implementación de Ollama correspondiente.
  • El servicio de Kubernetes, que es de tipo LoadBalancer, distribuirá las solicitudes de asistencia de código entre los extremos (es decir, los Pods) y devolverá sus respuestas respectivas a través de la IP externa expuesta.
  • Recuerda que el complemento Continue apunta a una sola dirección IP por funcionalidad.

Diagrama de arquitectura de escalamiento de LLM de código abierto.

Para escalar el backend de vLLM en tu entorno aislado de GDC, puedes seguir una estrategia similar a la que se usa para Ollama, enfocándote en la asignación de recursos de hardware y la replicación de pods.

Cómo escalar la solución con un backend de vLLM

Verticalmente

  • Actualiza la asignación de GPU: Si la capacidad de procesamiento de la inferencia (tokens/s) se convierte en un cuello de botella, asigna una porción de GPU más grande hasta que uses una GPU NVIDIA A100 completa.
  • Configuraciones de varias GPUs: Para modelos masivos (por ejemplo, de 70 mil millones a 405 mil millones de parámetros) que no caben en la memoria de una sola GPU A100 de 80 GB, debes escalar a varias GPUs con el paralelismo de tensor.
  • Consideración sobre la latencia: Ten en cuenta que los modelos que abarcan más de una GPU pueden experimentar una ligera sobrecarga relacionada con la comunicación entre GPU (por ejemplo, la sincronización de NCCL).

Horizontalmente

  • Aumenta la capacidad de procesamiento a través de réplicas: Para controlar un mayor volumen de solicitudes de usuarios simultáneas para el mismo modelo, aumenta el recuento de réplicas en el archivo YAML de implementación de vLLM.
  • Instancias de modelos dedicadas: Dado que vLLM está diseñado como un motor de servicio de un solo modelo y fija su memoria caché de KV tras la inicialización, debes implementar un conjunto separado de pods para cada LLM diferente que desees alojar.
  • Balanceo de cargas: El servicio de Kubernetes de GDC (tipo LoadBalancer) distribuirá automáticamente las solicitudes de inferencia entrantes entre todos los extremos de Pod de vLLM en buen estado asociados con ese servicio.

5.4 Solución de problemas

Errores comunes y mitigaciones

Error Mitigación
FALLA EN LA AUTOPRUEBA DE FIPS Ocurre cuando bibliotecas como BoringSSL no tienen firmas de integridad. Soluciona el problema configurando BORINGSSL_FIPS=0 y utilizando imágenes oficiales de vLLM
Carga de peso detenida Verifica si hay regulación de IOPS en los PVC pequeños. Se requiere un volumen de 500 GiB para la inicialización del modelo de rendimiento.
Se rechazó la conexión Confirma que el PNP permite de forma explícita el targetPort (8000/8080). Los firewalls de GDC no otorgan acceso automáticamente a los puertos de backend para las VIP del balanceador de cargas.