Implementação de referência de LLM de peso aberto no GDC com isolamento físico

Visão geral

Este documento fornece instruções detalhadas para implantar modelos de linguagem grandes (LLMs) de peso aberto, como Gemma, Llama e DeepSeek, em ambientes isolados do Google Distributed Cloud (GDC). Ele aborda o uso do vLLM para veiculação de alta capacidade e do Ollama para facilitar o uso, aproveitando os recursos da plataforma GDC, incluindo Kubernetes, Harbor e recursos de GPU.

Arquitetura

A solução envolve a implantação de back-ends de disponibilização de LLM em contêineres (vLLM, Ollama) como implantações em um cluster de usuário. Os pesos do modelo são armazenados em volumes permanentes, preenchidos com imagens do registro do Harbor. Os serviços do Kubernetes do tipo LoadBalancer expõem as APIs dos back-ends. As políticas de rede do projeto protegem o acesso a esses serviços.

Diagrama de arquitetura de implementação de referência de LLM de peso aberto.

Antes de começar

Verifique se os pré-requisitos a seguir foram atendidos:

  • GDC com isolamento físico versão 1.15.1 ou mais recente.
  • Cluster de usuário criado com recursos suficientes (CPU, memória, GPU).
  • É necessário ter no mínimo uma GPU NVIDIA A100.
  • Uma instância do Harbor disponível e acessível.
  • CLIs kubectl e gdcloud configuradas para acessar o cluster de usuário.
  • O cliente Docker está instalado e configurado para enviar ao Harbor.
  • Permissões necessárias do IAM concedidas (por exemplo, Administrador do namespace, desenvolvedor de cluster**).
  • Conta e autenticação do Hugging Face configuradas se você estiver usando modelos restritos.

Seção 1: configuração comum

1.1 Criar um secret de pull de imagem

Para configurar um secret de extração de imagem para uma carga de trabalho de contêiner no GDC com isolamento físico, crie um secret do docker-registry do Kubernetes com credenciais para acessar seu projeto privado do Harbor. Esse secret é referenciado na especificação de implantação.

Use uma conta de robô do Harbor para acesso programático a imagens em projetos privados do Harbor.

Siga estas etapas para configurar o segredo de extração de imagens:

Criar uma conta de robô do Harbor:

  • Acesse a interface da instância do Harbor.
  • Acesse seu projeto do Harbor.
  • Selecione a guia Contas de robô.
  • Clique em Nova conta de robô.
  • Dê um nome a ele (por exemplo, oss-llm-puller) e conceda as permissões necessárias (pelo menos acesso de pull) até um horário de expiração.
  • Armazene com segurança o nome da conta do robô (por exemplo, robot$oss-llm-puller) e o token secreto fornecido.

Autentique o Docker no Harbor:

Na máquina com o Docker instalado e acesso à rede do registro do Harbor, faça login usando as credenciais da conta de robô:

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}

Crie o secret de extração de imagens do Kubernetes:

Use kubectl para criar um secret do tipo docker-registry no namespace do projeto usando o arquivo de configuração do Docker atualizado na etapa 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}

Seção 2: implantação com vLLM

2.1 Obter a imagem Docker vLLM

Em uma máquina com acesso à internet, extraia a imagem Docker vLLM e transfira para seu projeto do 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

Substitua HARBOR_URL e PROJECT pelo URL da instância do Harbor e pelo nome do projeto.

2.2 Preparar os pesos do modelo no PVC

Crie um arquivo YAML (por exemplo, model-pvc.yaml):

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

Aplique o PVC: kubectl apply -f model-pvc.yaml

Faça o download dos pesos do Hugging Face:

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

Use um pod auxiliar (por exemplo, helper-pod.yaml) para transferir pesos para a PVC. Verifique se você tem uma imagem busybox no 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

Aplique o pod e copie os arquivos:

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 Implantar o back-end do vLLM

Crie o arquivo de implantação 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

Crie o arquivo de serviço 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

Aplique as configurações:

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

2.4 Configurar a política de rede

Aplique um recurso ProjectNetworkPolicy para permitir o tráfego de entrada à porta do serviço vLLM (8000). Crie 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

Aplique a política: kubectl apply -f vllm-netpol.yaml

Seção 3: implantação com o Ollama

3.1 Prepare o Dockerfile

Crie um Dockerfile para criar a imagem do Ollama com os modelos pré-carregados:

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 Criar e enviar a imagem

Crie e envie a imagem:

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 Implantar o back-end do Ollama

Crie 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

Aplique o manifesto: kubectl apply -f ollama-gemma3.yaml

3.4 Configurar a política de rede

Crie 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

Aplique a política: kubectl apply -f ollama-netpol.yaml

Seção 4: validação

Verifique os implantes conferindo os status dos pods, os endereços IP dos serviços e enviando solicitações de inferência de teste usando curl para os endereços IP LoadBalancer do vLLM e do Ollama.

Verifique se todos os contêineres e serviços estão 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

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

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

Seção 5: operações e solução de problemas

5.1 Operações do vLLM

Verificar registros: kubectl logs -f -n NAMESPACE

Consultar status interno (de um pod de depuração):

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

5.2 Operações do Ollama

Acessar a CLI: kubectl exec -it -n NAMESPACE -- sh

Dentro do pod: ollama list, ollama ps

5.3 Escalonamento

Escalonar a solução com um back-end do Ollama

Verticalmente

  • Alocar uma fração maior de GPU para o LLM que se tornou um gargalo até usar uma GPU completa.
  • Se você quiser usar um LLM maior para melhorar a precisão da resposta, por exemplo, um LLM de 405 bilhões de parâmetros em vez de um de 7 bilhões, talvez seja necessário mais de uma GPU para executá-lo sem problemas.
  • Os modelos que se encaixam em mais de uma GPU têm alguma latência relacionada à comunicação entre GPUs.

Horizontalmente

  • Implante quantos pods do Ollama forem necessários para alcançar a capacidade de processamento desejada em um determinado LLM.
    • Para isso, aumente o número de réplicas no arquivo YAML de implantação do Ollama correspondente.
  • O serviço do Kubernetes, que é do tipo LoadBalancer, distribui solicitações de assistência de código entre endpoints (ou seja, pods) e retorna as respostas respectivas pelo IP externo exposto.
  • O plug-in Continuar aponta para apenas um endereço IP por funcionalidade.

Diagrama de arquitetura de escalonamento de LLM de peso aberto.

Para escalonar o back-end vLLM no ambiente isolado do GDC, siga uma estratégia semelhante à usada para o Ollama, focando na alocação de recursos de hardware e na replicação de pods.

Escalonar a solução com um back-end vLLM

Verticalmente

  • Fazer upgrade da alocação de GPU:se a capacidade de processamento de inferência (tokens/segundo) se tornar um gargalo, aloque uma fração maior da GPU até usar uma GPU NVIDIA A100 completa.
  • Configurações de várias GPUs: para modelos enormes (por exemplo, de 70 bilhões a 405 bilhões de parâmetros) que não cabem na memória de uma única A100 de 80 GB, é necessário escalonar para várias GPUs usando o paralelismo de tensor.
  • Consideração sobre a latência: modelos que abrangem mais de uma GPU podem ter uma pequena sobrecarga relacionada à comunicação entre GPUs (por exemplo, sincronização do NCCL).

Horizontalmente

  • Aumentar a capacidade de processamento usando réplicas: para lidar com um volume maior de solicitações simultâneas de usuários para o mesmo modelo, aumente a contagem de réplicas no YAML de implantação do vLLM.
  • Instâncias de modelo dedicadas: como o vLLM foi projetado como um mecanismo de serviço de modelo único e fixa a memória do cache KV na inicialização, é necessário implantar um conjunto separado de pods para cada LLM diferente que você quer hospedar.
  • Balanceamento de carga: o serviço do Kubernetes do GDC (tipo LoadBalancer) distribui automaticamente as solicitações de inferência recebidas entre todos os endpoints de pod vLLM íntegros associados a esse serviço.

5.4 Solução de problemas

Erros comuns e mitigações.

Erro Mitigação
FALHA NO AUTOTESTE DO FIPS Ocorre quando bibliotecas como o BoringSSL não têm assinaturas de integridade. Corrigir definindo BORINGSSL_FIPS=0 e usando imagens oficiais do vLLM
Carregamento de peso parado Verifique se há limitação de IOPS em PVCs pequenos. Um volume de 500 GiB é necessário para a inicialização do modelo de desempenho.
Conexão recusada Confirme se o PNP permite explicitamente o targetPort (8000/8080). Os firewalls do GDC não concedem acesso automático às portas de back-end para VIPs do balanceador de carga.