Cómo desplegar un modelo de IA en Kubernetes, paso a paso

En mi homelab sirvo un modelo de IA local con una API compatible con OpenAI, corriendo en Kubernetes sobre una GPU de consumo. En este post te enseño a montarlo desde cero, con los mismos pasos (y los mismos tropiezos) que seguí yo.

Qué vamos a construir

Un Deployment de llama.cpp sirviendo un modelo en formato GGUF (en mi caso, Gemma 4 12B), expuesto como API /v1/chat/completions. Cualquier app que hable con OpenAI podrá hablar con tu cluster.

Requisitos

  • Un cluster Kubernetes. Yo uso K3s en un solo nodo: detecta solo el runtime de NVIDIA si tienes nvidia-container-toolkit instalado.
  • Una GPU con VRAM suficiente (luego hacemos las cuentas) y sus drivers.
  • El NVIDIA device plugin desplegado: es quien anuncia nvidia.com/gpu como recurso reservable.

Paso 1: elige modelo y cuantización

La regla rápida para un GGUF cuantizado a 4 bits (Q4_K_M): ~0,6 GB de VRAM por cada mil millones de parámetros, más el KV cache (depende del contexto). Un 12B en Q4 son ~7,5 GB de pesos; en mi RTX 5060 Ti de 16 GB cabe con 128K de contexto y sobra espacio.

Descarga el GGUF (Hugging Face: busca el repo oficial o los de la comunidad como bartowski) y déjalo en un disco del nodo, por ejemplo /data/modelos/.

Paso 2: PersistentVolume para los modelos

Los GGUF pesan gigas: no van dentro de la imagen. Un PV local apuntando a la carpeta del disco, y su PVC:

yaml
apiVersion: v1
kind: PersistentVolume
metadata:
  name: llm-models
spec:
  capacity: { storage: 30Gi }
  accessModes: [ReadWriteOnce]
  storageClassName: local-hdd
  local: { path: /data/modelos }
  nodeAffinity:        # un PV local vive en UN nodo concreto
    required:
      nodeSelectorTerms:
        - matchExpressions:
            - { key: kubernetes.io/hostname, operator: In, values: [mi-nodo] }

Paso 3: el Deployment con llama.cpp

La pieza central. Lo importante: la imagen server-cuda de llama.cpp, el recurso nvidia.com/gpu: 1, y los flags del servidor.

yaml
apiVersion: apps/v1
kind: Deployment
metadata: { name: llm }
spec:
  replicas: 1
  strategy: { type: Recreate }   # la GPU no se comparte entre 2 réplicas a la vez
  template:
    spec:
      runtimeClassName: nvidia
      containers:
        - name: llama-server
          image: ghcr.io/ggml-org/llama.cpp:server-cuda  # pínala a digest en producción
          args:
            - --model
            - /models/gemma-4-12B-it-Q4_K_M.gguf
            - --host
            - "0.0.0.0"
            - --port
            - "8000"
            - --n-gpu-layers
            - "99"            # todas las capas a la GPU
            - --ctx-size
            - "32768"
            - --flash-attn
            - "on"
          env:
            - name: LLAMA_API_KEY   # protege el endpoint
              valueFrom: { secretKeyRef: { name: llm-secrets, key: API_KEY } }
          resources:
            limits: { memory: 8Gi, nvidia.com/gpu: "1" }
          readinessProbe:
            httpGet: { path: /health, port: 8000 }
            initialDelaySeconds: 30   # cargar gigas desde disco tarda
          volumeMounts:
            - { name: models, mountPath: /models, readOnly: true }
      volumes:
        - name: models
          persistentVolumeClaim: { claimName: llm-models }

Detalles que importan:

  • Probes con margen: el servidor tarda en cargar el modelo desde disco; si el initialDelaySeconds es muy corto, Kubernetes matará el pod antes de que termine.
  • API key como Secret, nunca en el manifiesto.
  • --ctx-size define el contexto y el tamaño del KV cache: más contexto = más VRAM. Empieza conservador y sube midiendo.

Paso 4: Service y verificación

Un Service ClusterIP en el puerto 8000 y a probar:

bash
kubectl exec -it deploy/cualquier-pod -- \
  curl http://llm.mi-namespace.svc:8000/v1/chat/completions \
  -H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
  -d '{"model":"local","messages":[{"role":"user","content":"Hola"}]}'

Y vigila la VRAM real con nvidia-smi: es tu métrica de la verdad para ajustar contexto y cuantización.

Tropiezos de los que aprendí

  • La GPU se puede compartir: con time-slicing en el device plugin, el mismo chip sirve el LLM y transcodifica vídeo (Jellyfin). La VRAM no se particiona: haz las cuentas tú.
  • Cada modelo tiene su sampling: migré de Qwen a Gemma y las respuestas degeneraban en bucles — Gemma necesita --temp 1.0. Lee la ficha del modelo.
  • GitOps también aquí: mi manifiesto vive en Git y ArgoCD lo aplica. Cambiar de modelo es editar dos líneas y hacer push.

¿Dudas montando el tuyo? Escríbeme desde la página de inicio — y si quieres ver este setup en acción, el chat de esta web corre exactamente así.

← Volver al blog