Cadena de confianza del modelo (2/4): dónde viven los bytes — registro, artefactos OCI y distribución al nodo GPU

En el primer artículo de la serie describimos el plano de control: quién declara que existe un endpoint de inferencia, quién lo reconcilia y qué contrato hablan los clientes. Pero un InferenceService no es más que una promesa hasta que alguien pone 140 GB de pesos en el sistema de ficheros que ve el contenedor. Este segundo artículo va exactamente de eso: de dónde salen esos bytes y por qué camino llegan a la memoria de la GPU.

Es un tema que el blog ha tocado por los bordes —en la serie de cold start, en el versionado de datos con DVC y lakeFS, en el catálogo de herramientas OSS de LLMOps— pero nunca de frente. Y conviene hacerlo, porque casi todas las plataformas de inferencia on-premise que se auditan comparten el mismo defecto de origen, que no es de rendimiento sino conceptual: confunden tres cosas distintas, que viven en sitios distintos y fallan de formas distintas.

TL;DR

  • Tres planos, no uno. Model registry (metadatos, versiones, linaje, promoción) ≠ repositorio de artefactos (los bytes, direccionados por digest) ≠ almacenamiento del despliegue (lo que el pod monta en /mnt/models). Mezclarlos es lo que rompe la reproducibilidad.
  • El anti-patrón: token de Hugging Face en el pod y descarga desde internet en cada arranque. Sin inmutabilidad, sin caché, sin auditoría, sin air-gap y con cold starts de minutos.
  • El artefacto OCI es hoy la respuesta pragmática: OCI Image Spec 1.1 (marzo de 2024) trajo artifactType, subject y la Referrers API; ORAS v1.3.0 (octubre de 2025) añadió backup/restore para air-gap; ModelPack (sandbox CNCF desde junio de 2025) estandariza los mediaTypes de pesos.
  • Kubernetes ya sabe montar artefactos OCI: el image volume source (KEP-4639) llegó a GA en v1.36 (22 de abril de 2026). Es el cambio operativo más relevante del año para distribución de modelos.
  • El cuello de botella real casi nunca es la GPU. 140 GB por un enlace de 10 Gb/s saturado son unos 112 s; por NVMe Gen4 unos 20 s; por PCIe Gen5 unos 2-3 s. Optimizar el loader sin arreglar la red es optimizar el eslabón equivocado.
  • Un model registry (MLflow, Kubeflow Hub) no guarda los bytes: guarda la ficha y un puntero. Si esperabas que resolviera tu distribución, has comprado la herramienta equivocada.

La analogía

Piensa en un archivo histórico serio. Tiene tres cosas que un visitante despistado confunde constantemente.

El catálogo: fichas con signatura, autor, fecha, procedencia, estado de conservación y restricciones de consulta. La ficha pesa gramos y describe algo que pesa kilos. El depósito: las estanterías compactas del sótano donde están las cajas de verdad, con control de acceso, temperatura y un inventario que cuadra al gramo; no sabe nada de por qué un documento importa, pero garantiza que la caja 4711 sigue conteniendo exactamente lo que contenía. Y la mesa de la sala de consulta: donde el investigador tiene el documento abierto delante. Sitio temporal, caro y del que el documento vuelve al depósito al acabar la sesión.

El model registry es el catálogo. El repositorio de artefactos es el depósito. El nodo GPU con el modelo en HBM es la mesa de consulta. Quien dice “hemos montado un registro de modelos” y lo que ha montado es MLflow, ha montado el catálogo y ha dejado el depósito sin construir: las cajas siguen en el maletero del coche de alguien. Quien mete el modelo dentro de la imagen del contenedor ha metido el documento dentro de la mesa: cada vez que cambia la mesa, mueve el documento. Y quien descarga de internet en cada arranque no tiene depósito: pide el documento por mensajería a otra ciudad cada vez que un investigador se sienta.

Volveremos al archivo: el expurgo que borra cajas todavía referenciadas, el carro de transporte entre el sótano y la sala, y la reproducción certificada que se envía a la sede aislada.

Tres planos que no son el mismo

Conviene fijar el vocabulario antes de discutir herramientas, porque buena parte de las discusiones de arquitectura en esta capa son en realidad discusiones de nomenclatura.

PlanoQué guardaUnidadPregunta que respondeEjemplos OSS
Model registryMetadatos, versiones lógicas, linaje, estado de promoción, métricas de evaluaciónLa ficha del modelo (KB)¿Qué versión está aprobada para producción y de dónde salió?MLflow, Kubeflow Hub, ClearML
Repositorio de artefactosLos bytes, direccionados por digest, inmutablesEl blob (GB-TB)¿Cuáles son exactamente los bytes de esa versión?Harbor, distribution, Zot, MinIO, lakeFS
Almacenamiento del despliegueLa copia que el pod ve montadaEl volumen (/mnt/models)¿Qué está leyendo el proceso de inferencia ahora mismo?PVC, image volume, emptyDir + caché NVMe

Las tres capas tienen ciclos de vida distintos. Una ficha vive para siempre (auditoría); un blob, mientras alguna versión lo referencie (retención); un volumen, lo que vive el pod. Implementadas como una sola cosa, hereda lo peor de las tres: si el registro es también el almacenamiento del despliegue, borrar una versión antigua tumba un pod en producción; si el almacenamiento del despliegue es también la fuente de verdad, no hay forma de responder qué se sirvió el martes pasado.

De ahí la regla operativa que vale la pena escribir en la pared: la fuente de verdad de los bytes es un digest, no un tag y no una ruta. Todo lo demás —tags, aliases, rutas de PVC— es un puntero mutable que resuelve a ese digest en un momento dado. El artículo 3/4 construye toda la verificación de procedencia sobre esa premisa.

El anti-patrón de partida

Este es, con diferencia, el patrón más extendido en despliegues on-premise que empezaron como un piloto y se quedaron:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-anti-patron
spec:
  template:
    spec:
      containers:
        - name: vllm
          image: vllm/vllm-openai:v0.11.0
          args: ["--model", "meta-llama/Llama-3.3-70B-Instruct"]
          env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-token
                  key: token
          resources:
            limits:
              nvidia.com/gpu: "4"

Funciona. Arranca. Sirve tokens. Y es inaceptable en una plataforma soberana por cinco motivos independientes, cualquiera de los cuales bastaría:

  1. Dependencia externa en el camino crítico de arranque. Un Deployment que no puede recuperarse de un fallo sin acceso saliente a internet no es alta disponibilidad: es una dependencia de terceros no declarada en el SLA.
  2. Cero inmutabilidad. meta-llama/Llama-3.3-70B-Instruct es una referencia mutable, y la rama por defecto de un repositorio cambia (correcciones de tokenizer, plantillas de chat). Dos réplicas del mismo Deployment arrancadas con una semana de diferencia pueden servir bytes distintos, y nada en el clúster lo dice.
  3. Cero caché coordinada ni auditoría. Cada pod descarga su propia copia: ocho réplicas de un modelo de 140 GB son 1,1 TB de tráfico saliente por cada rolling update. Y no queda registro interno de qué se descargó, con qué digest ni quién lo autorizó; ante un auditor de ENS o de ISO 42001, la respuesta es “lo bajamos de internet”.
  4. Incompatible con air-gap. Todo el diseño se cae el día que hay que replicar la plataforma en un entorno aislado, que es el caso de defensa y sanidad tratado en infraestructura de cumplimiento normativo.
  5. Cold start de minutos. Descargar 140 GB por un enlace de salida de 1 Gb/s (125 MB/s efectivos, siendo generosos) son unos 19 minutos solo de transferencia, antes de tocar el disco o la GPU; con 10 Gb/s dedicados y saturados, unos 112 segundos. En un evento de escalado por carga, eso es latencia de servicio.

La serie de cold start del blog explora cómo recortar el tramo disco-HBM. Lo que este artículo añade es que si el primer tramo es internet, el resto de la optimización es irrelevante.

El modelo como artefacto OCI

La respuesta pragmática es aburrida, y por eso funciona: guarda el modelo en el mismo registro donde ya guardas los contenedores. Ahí ya tienes control de acceso, replicación entre sedes, escaneo, cuotas, auditoría y un equipo que sabe operarlo.

Qué hizo posible OCI 1.1

Durante años, meter cosas que no fueran imágenes de contenedor en un registro OCI era un apaño. Las especificaciones OCI Image Spec 1.1 y Distribution Spec 1.1, publicadas el 13 de marzo de 2024, lo convirtieron en un caso de uso de primera clase con tres piezas:

  • artifactType en el manifest: declara explícitamente “esto no es una imagen ejecutable, es un artefacto de tipo X”. Antes se codificaba abusando del mediaType del config, que es por lo que todavía hoy hay registros que muestran un modelo como una imagen rota.
  • subject: un manifest puede declarar que se refiere a otro manifest. Es lo que permite colgar una firma, un SBOM o una atestación de un modelo sin modificarlo y sin cambiar su digest, que es el punto.
  • Referrers API (GET /v2/<repo>/referrers/<digest>): la consulta inversa, “dame todo lo que apunta a este digest”, con fallback por tag para registros que aún no la implementan.

Sobre estas tres piezas construye el artículo 3/4 la firma, procedencia y AIBOM. Sin ellas, verificar un modelo obliga a inventarse una convención propia.

ORAS: el cliente genérico

ORAS (OCI Registry As Storage) es la navaja suiza para meter y sacar artefactos arbitrarios de un registro OCI. La versión v1.3.0 se publicó el 6 de octubre de 2025, conforme a distribution-spec v1.1.1, y añadió tres cosas relevantes para modelos: backup/restore a directorio o tarball (clave para air-gap), gestión de índices multi-plataforma y --format para salida estructurada.

Un dato para calibrar madurez: ORAS entró en el sandbox de CNCF el 13 de julio de 2021 y a fecha de este artículo sigue en sandbox. Cinco años en el nivel de entrada no invalida la herramienta —es estable y la referencia de facto— pero dice algo sobre la gobernanza del proyecto que conviene sopesar antes de convertirla en dependencia crítica sin plan B.

Empujar un modelo con ORAS, con tipos de contenido explícitos:

oras push registro.interno/modelos/llama-3.3-70b:2026.07.1 \
  --artifact-type application/vnd.cncf.model.manifest.v1+json \
  --annotation "org.opencontainers.image.created=2026-07-20T09:00:00Z" \
  --annotation "es.ejemplo.modelo.precision=bf16" \
  --annotation "es.ejemplo.modelo.origen=hf:meta-llama/Llama-3.3-70B-Instruct" \
  config.json:application/vnd.cncf.model.weight.config.v1.raw \
  tokenizer.json:application/vnd.cncf.model.weight.config.v1.raw \
  model-00001-of-00030.safetensors:application/vnd.cncf.model.weight.v1.raw \
  model-00002-of-00030.safetensors:application/vnd.cncf.model.weight.v1.raw

# Recuperarlo, con cache local de blobs para no re-descargar lo ya presente
export ORAS_CACHE=/var/cache/oras
oras pull registro.interno/modelos/llama-3.3-70b:2026.07.1 --output /srv/modelos/llama-70b
oras manifest fetch registro.interno/modelos/llama-3.3-70b:2026.07.1 --descriptor

ModelPack: la convención que faltaba

ORAS te deja inventarte los mediaTypes, y ese es exactamente el problema: cada organización se inventa los suyos y nada interopera. ModelPack es el intento de estandarizarlos, aceptado en el sandbox de CNCF en junio de 2025. Define, sobre OCI Image Manifest 1.1:

  • artifactType: application/vnd.cncf.model.manifest.v1+json
  • config: application/vnd.cncf.model.config.v1+json
  • capas de pesos: application/vnd.cncf.model.weight.v1.raw (y variantes .tar, .tar+gzip, .tar+zstd)
  • capas de configuración de pesos, documentación, código y dataset con el mismo patrón

Su CLI de referencia es modctl, que trabaja con un Modelfile declarativo:

NAME llama-3.3-70b-instruct
ARCH transformer
FAMILY llama
FORMAT safetensors
PARAMSIZE 70
PRECISION bf16
CONFIG config.json
CONFIG tokenizer.json
MODEL *.safetensors
DOC *.md
modctl build -t registro.interno/modelos/llama-3.3-70b:2026.07.1 -f Modelfile .
modctl push registro.interno/modelos/llama-3.3-70b:2026.07.1
modctl pull registro.interno/modelos/llama-3.3-70b:2026.07.1 \
  --extract-dir /srv/modelos/llama-70b --extract-from-remote

La alternativa con más recorrido en usuarios es KitOps (ModelKit), con ergonomía tipo Docker e integración nativa con Hugging Face. A fecha de este artículo no hay convergencia: ModelPack apunta al caso empresarial y a la integración con runtimes y registros; KitOps y Docker Model Runner, a la ergonomía de desarrollador. Elegir uno hoy es apostar; lo que no es apuesta es usar artifactType y anotaciones explícitas en vez de convenciones caseras.

Cómo se trocea un modelo de 140 GB

Un modelo grande no es “un fichero”: son 20-40 shards de safetensors más media docena de ficheros pequeños de configuración. La estrategia de capas importa:

  • Una capa por shard, sin comprimir. Los pesos en bf16 o fp8 son incompresibles en la práctica: gzip sobre safetensors gasta CPU para ahorrar un 2-3 %, y obliga a descomprimir en el nodo, añadiendo una pasada completa por CPU y disco en el camino crítico. Usa .raw.
  • Los ficheros pequeños, en su propia capa. config.json, tokenizer.json y las plantillas cambian mucho más a menudo que los pesos. Si van en la misma capa que un shard de 5 GB, cada corrección de tokenizer invalida 5 GB de caché en todos los nodos.
  • Tamaño de capa entre 2 y 8 GB. Suficientemente grande para no pagar overhead por blob, suficientemente pequeño para paralelizar y para que un fallo de red no obligue a reintentar 40 GB.

Y lo que duele: la subida usa chunked upload del distribution-spec, con interoperabilidad históricamente irregular entre implementaciones, así que empujar 140 GB es una operación de decenas de minutos que exige enlace decente y reintentos; la deduplicación es por digest de capa, de modo que dos versiones que difieren en un shard comparten el resto solo si esos blobs son byte a byte idénticos (cualquier reempaquetado que altere metadatos la rompe); y los valores por defecto de los proxies inversos —timeouts, tamaño máximo de cuerpo, vida de la sesión de subida— están calibrados para imágenes de 1 GB, no para blobs de 8 GB.

Harbor como depósito on-premise

Harbor es el registro OSS de referencia para on-premise: proyecto graduado en CNCF desde junio de 2020, con un ciclo de releases sano —la serie 2.15 salió el 20 de marzo de 2026 y el parche 2.15.2 el 2 de julio de 2026—, y soporte de las tres últimas minor durante unos nueve meses cada una. Lo relevante para modelos:

Replicación entre sedes. Reglas push o pull, filtradas por repositorio, tag y tipo de recurso, con disparo manual, programado o por evento. Para modelos, programada y en ventana, con label que marque qué versiones se replican (ver trampas).

Proxy cache. Aquí hay que ser preciso, porque circula mucha desinformación: el proxy cache de Harbor funciona contra registros OCI —Harbor, Docker Hub, registro distribution, ECR, ACR, GCR, Quay y GHCR—, y no contra Hugging Face, que no expone una API de distribución OCI. Si la idea era “pongo Harbor delante de Hugging Face y ya”, no funciona así. Las opciones reales son (a) una tarea de ingesta que baja de Hugging Face en un entorno de staging y empuja a Harbor como artefacto OCI —el patrón recomendable, porque introduce un punto de control explícito— o (b) un acelerador P2P con soporte nativo de repositorios de modelos, como Dragonfly. Lo que sí resuelve el proxy cache es el espejo de las imágenes base de runtime (vLLM, TGI, TEI), que no es poco: crea por defecto una retención de 7 días y el proyecto es de solo lectura.

Cuotas por proyecto. En bytes, por UI o por API:

curl -u admin:REDACTED -X PUT "https://registro.interno/api/v2.0/quotas/7" \
  -H "Content-Type: application/json" \
  -d '{"hard": {"storage": 21990232555520}}'

Esos 21990232555520 bytes son 20 TiB, dimensionamiento ajustado —no generoso— para una docena de modelos con tres versiones vivas cada uno. Dos avisos: la cuota se comprueba al llegar el manifest, es decir después de subir los blobs, así que un push que la excede ya ha consumido disco y red; y la contabilidad no siempre refleja la deduplicación de blobs compartidos entre proyectos.

Tags inmutables. Reglas por proyecto que impiden sobrescribir un tag que cumpla un patrón. Para modelos no es opcional: una regla que haga inmutable todo modelos/** con patrón de tag 2* (versiones con fecha) elimina de raíz la clase entera de incidentes de “el tag cambió debajo”.

Retención y GC. Reglas de retención por proyecto (últimas N versiones, o las de los últimos N días) que marcan artefactos para borrado, y un garbage collector separado que libera los blobs no referenciados. Son dos cosas distintas, y borrar en la UI no libera disco: el expurgo se ejecuta aparte. Ejecutar GC con un push concurrente en marcha es la receta clásica para borrar blobs que estaban a punto de ser referenciados.

Escaneo. Trivy integrado, con una honestidad necesaria: escanear un artefacto de pesos no detecta nada relevante sobre el modelo. Sirve para las imágenes de runtime, que sí ejecutan código. Detectar formatos peligrosos —un pickle de PyTorch con código arbitrario en lugar de safetensors— requiere herramientas específicas, y es materia del artículo 3/4.

Model registries de verdad

Ahora el catálogo, con la afirmación incómoda por delante: un model registry no resuelve tu distribución.

MLflow

MLflow (LF AI & Data) es el estándar de facto del catálogo, con la serie 3.x madura —la 3.13.0 se publicó en junio de 2026, con foco en RBAC, UI de administración y retención de trazas—. Su modelo de datos: registered model (nombre lógico) → model version (entero incremental) → aliases y tags.

El cambio conceptual importante: las stages (Staging, Production, Archived) están deprecadas desde MLflow 2.9.0 y se sustituyen por aliases y tags, por la inflexibilidad de un enumerado fijo para expresar flujos reales de MLOps. Un alias es un puntero mutable con nombre:

from mlflow import MlflowClient

client = MlflowClient()
client.set_registered_model_alias("llama-3.3-70b-soberano", "champion", 7)
client.set_model_version_tag("llama-3.3-70b-soberano", "7",
                             "estado_validacion", "aprobado_ens_medio")
client.set_model_version_tag("llama-3.3-70b-soberano", "7",
                             "oci_digest", "sha256:9f2c...")

El consumo se hace por URI de alias: models:/llama-3.3-70b-soberano@champion.

Lo que MLflow aporta: linaje al run que produjo el modelo, métricas de evaluación asociadas a la versión, un lenguaje de promoción compartido y una API sobre la que colgar aprobaciones. Lo que no aporta: no es un CDN, no gestiona la distribución a nodos, y su artifact store (S3/MinIO/NFS) no da inmutabilidad por digest, ni Referrers, ni replicación entre sedes. El patrón sano es el del snippet: el catálogo guarda el digest OCI como tag de la versión y apunta al depósito, en lugar de intentar ser el depósito.

Kubeflow Hub (antes Model Registry)

El Kubeflow Model Registry se renombró a Kubeflow Hub y unificó dos funciones: el registro propio y un Model Catalog federado que descubre modelos externos (YAML, Hugging Face), con linaje entre datos, código y modelos, metadatos y promoción por estado.

La valoración honesta: a fecha de este artículo el componente sigue en la serie 0.3.x y con estado Alpha en la política de versionado de Kubeflow, con soporte limitado. Interesante si ya explotas Kubeflow completo y quieres una sola consola; no es todavía la pieza sobre la que montar la gobernanza de modelos de una plataforma soberana en producción. El renombrado en pleno 0.3.x es, además, señal de que la API sigue moviéndose.

Tabla comparativa por función

FunciónRegistro OCI (Harbor + ORAS/ModelPack)MLflow RegistryKubeflow HublakeFS / DVC
Guarda los bytesSí, por digest, inmutableNo (puntero a artifact store)No (puntero)Sí (datos y versiones)
Linaje a datos/códigoSolo por anotacionesSí, al runSí, explícitoSí, para datasets
Metadatos ricosAnotaciones y config JSONSí, tipadoSí, tipadoParcial
Promoción / aprobaciónPor convención (tags, labels)Aliases y tagsEstadosNo
Firma y atestacionesSí, nativo (subject + Referrers)NoNoNo
Replicación entre sedesSí, nativaNoNoParcial
API estándarOCI Distribution 1.1REST propiaREST propiaS3/Git-like
Air-gapSí (oras backup/restore, skopeo)ManualManualManual
MantenedorCNCF (Harbor graduado; ORAS y ModelPack sandbox)LF AI & DataKubeflow (CNCF)OSS comercial
Madurez realAlta (registro), media (convención de modelos)AltaAlphaAlta

La tesis de la tabla: las columnas son complementarias, no alternativas. El registro OCI es el depósito; MLflow, el catálogo; lakeFS o DVC versionan el dataset que da origen a todo, como se detalla en versionado de datos con DVC y lakeFS.

La ruta caliente: del registro a la HBM

Tres planos y la ruta caliente hasta la GPUPlano de catálogoMLflow / Kubeflow Hubversion, alias, linajeguarda el DIGEST, no los bytesPlano de depósitoHarbor + ORAS / ModelPackblobs por sha256, inmutablescuotas, retencion, replicacionPlano de despliegueimage volume / modelcarmontado en /mnt/modelsefimero o cache NVMeRuta caliente y ancho de banda por tramo (modelo de 140 GB)Registro (red)10 Gb/s → aprox. 112 sCache NVMe local7 GB/s → aprox. 20 sPCIe Gen5 x16aprox. 50 GB/s → 3 sHBM (H100 SXM)TB/s: nunca es el cuelloEl primer tramo domina por uno o dos ordenes de magnitud: sin cache local, todo lo demas es ruido.Aceleradores de carga (streamer, tensorizer) atacan el tramo 2-3; P2P y lazy loading atacan el tramo 1.Un tag mutable en el plano de deposito invalida las garantias de los otros dos planos.Firma y atestaciones cuelgan del digest via subject + Referrers API (articulo 3/4).

Image volumes: el cambio del año

El image volume source (KEP-4639) monta una imagen o artefacto OCI directamente como volumen de solo lectura en un pod: sin initContainer, sin copiar, sin PVC. Su recorrido: alfa en v1.31 (agosto de 2024), beta en v1.33 (abril de 2025) con soporte de subPath, beta por defecto en v1.35 y GA en v1.36, publicada el 22 de abril de 2026.

apiVersion: v1
kind: Pod
metadata:
  name: vllm-modelo-oci
spec:
  containers:
    - name: vllm
      image: registro.interno/runtime/vllm:v0.11.0
      args: ["--model", "/mnt/models", "--served-model-name", "llama-3.3-70b"]
      volumeMounts:
        - name: pesos
          mountPath: /mnt/models
          readOnly: true
      resources:
        limits:
          nvidia.com/gpu: "4"
  volumes:
    - name: pesos
      image:
        reference: registro.interno/modelos/llama-3.3-70b@sha256:9f2c...
        pullPolicy: IfNotPresent

Dos detalles que cambian la operativa: la referencia es por digest, con lo que la inmutabilidad deja de depender de la disciplina de nadie; y pullPolicy: IfNotPresent hace que el runtime del nodo cachee los blobs en su almacén local, de modo que la segunda réplica en ese nodo arranca sin tocar la red. Es, literalmente, la caché coordinada que faltaba en el anti-patrón.

KServe: storageUri y modelcars

En KServe hay dos caminos: el clásico, storageUri con un storage initializer que copia desde S3, PVC, HTTP o GCS a un emptyDir; y el modelcar, con esquema oci://:

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llama-70b-soberano
spec:
  predictor:
    model:
      modelFormat:
        name: huggingface
      storageUri: oci://registro.interno/modelos/llama-3.3-70b:2026.07.1
      resources:
        limits:
          nvidia.com/gpu: "4"

El mecanismo interno del modelcar conviene entenderlo antes de depender de él: KServe pone shareProcessNamespace: true en el pod, arranca un contenedor lateral con la imagen del modelo y crea un enlace simbólico desde /mnt/models al root filesystem de ese proceso a través de /proc. El runtime lee los pesos sin copiarlos, que es el ahorro que se busca. Dos advertencias: no está activado por defecto (hay que habilitar enableModelcar en el ConfigMap inferenceservice-config) y el tag latest fuerza pullPolicy: Always, anulando la caché. Con image volumes ya en GA, el modelcar queda como el camino para clústeres que aún no están en 1.36.

Cargar rápido: dónde ataca cada técnica

Conviene separar dos problemas que se confunden todo el rato: traer los bytes al nodo (red) y meterlos en la HBM (disco → CPU → PCIe → GPU).

Para el primero, Dragonfly es la pieza madura: graduó en CNCF el 14 de enero de 2026, distribuye pesos a escala de cientos de terabytes a cientos de nodos “en minutos”, reduce el ancho de banda consumido en el origen hasta un 90 % y aprovecha entre el 70 % y el 80 % del de cada nodo. Su v2.5.0 (30 de junio de 2026) añade descarga directa de repositorios de modelos (dfget hf://...), aceleración P2P de Git LFS y un dragonfly-injector que inyecta capacidad P2P vía webhook sin reconstruir imágenes. Su subproyecto Nydus aporta el formato de imagen con carga perezosa. Ojo con una expectativa mal puesta muy común: para pesos de modelo la carga perezosa ayuda poco, porque un motor de inferencia lee todos los pesos casi de inmediato; el beneficio real es para runtimes gordos y para modelos de los que solo se lee una parte.

Para el segundo, los números publicados por el NVIDIA Run:ai Model Streamer sobre Llama-3-8B (15 GB, safetensors, nodo con una A10G) son la referencia más limpia disponible:

OrigenSafetensors loaderRun:ai Model StreamerTensorizer
SSD gp347,99 s14,34 s (concurrencia 16)16,11 s (16 workers)
SSD io247,0 s7,53 s (concurrencia 8)10,36 s (8 workers)
Object storage S3no soportado4,88 s (concurrencia 32)37,36 s (16 workers)

Y el tiempo total hasta que vLLM está listo para servir: 66,13 s con el loader estándar sobre gp3 frente a 35,08 s con el streamer. Dos lecturas críticas. Primera: son benchmarks del propio proyecto, no medición independiente, sobre un modelo de 15 GB en una GPU modesta; extrapolar linealmente a 140 GB en 4×H100 es un salto que nadie ha publicado. Segunda, y más importante: la ganancia viene de saturar el medio de almacenamiento con concurrencia, no de magia. Si el cuello de botella es un NFS a 1 Gb/s, ningún loader lo arregla. El detalle de este tramo está en acelerar el cold start con tensorizer y en del disco a la HBM.

Como muestra el diagrama, en un nodo de referencia 4×H100 SXM 80 GB con NVLink cada tramo es entre 5 y 10 veces más rápido que el anterior. La conclusión es aburrida y contundente: el dinero está en tener el modelo ya en el NVMe del nodo antes de que arranque el pod.

Adapters LoRA: cuando el artefacto pesa megabytes

Todo lo anterior asume artefactos de decenas o cientos de gigabytes. Un adapter LoRA rompe ese supuesto: para un base de 70B, un adapter de rango 16 sobre las proyecciones de atención pesa decenas o pocos cientos de megabytes, tres órdenes de magnitud menos. Y eso cambia la estrategia entera.

Con artefactos así, la caché local y el P2P dejan de importar: la descarga es instantánea a cualquier ancho de banda razonable. Lo que pasa a importar es la cardinalidad y la frecuencia de cambio. Una plataforma multi-tenant como la descrita en multi-LoRA serving puede tener cientos de adapters, cada uno con su ciclo de vida, propietario y política de acceso, sobre un puñado de bases. Consecuencias prácticas:

  • El adapter es un artefacto OCI de pleno derecho, con repositorio propio y tag inmutable. Pero el registro pasa a tener miles de repositorios pequeños en lugar de decenas grandes, lo que estresa metadatos, listados y RBAC granular, no disco.
  • Debe declarar su base. Un adapter sin referencia inmutable al digest del modelo base sobre el que se entrenó es una bomba de relojería: aplicado sobre otra versión, degrada la calidad en silencio. Es el caso de uso natural de subject: el adapter refiere al base, y la Referrers API responde “qué adapters existen para este digest”.
  • La carga es dinámica, no de arranque. vLLM permite cargar adapters en caliente con VLLM_ALLOW_RUNTIME_LORA_UPDATING y el endpoint /v1/load_lora_adapter, o de forma declarativa con los LoRA resolver plugins (VLLM_PLUGIN_LORA_RESOLVERS), que resuelven un adapter por nombre desde un directorio. El patrón limpio en Kubernetes es montar un image volume con el adapter y dejar que el resolver de sistema de ficheros lo descubra.
  • El versionado pasa a ser el problema dominante. Con 300 adapters vivos, “¿qué adapter, sobre qué base, entrenado con qué dataset, sirve al tenant X?” solo tiene respuesta si el catálogo existe. Aquí sí gana un model registry frente a la convención sobre tags.

Air-gap y soberanía: promoción sin perder trazabilidad

El caso soberano de verdad: un entorno conectado donde se ingiere y se valida, y un entorno aislado donde se sirve, sin ruta de red entre ambos. La promoción cruza un diodo de datos o un soporte físico, y la trazabilidad tiene que sobrevivir al viaje. Es la reproducción certificada del archivo: no basta con copiar el documento, hay que copiar su ficha y el sello que acredita que la copia es fiel.

# 1. En el entorno conectado: exportar el artefacto con TODO lo que cuelga de el
oras backup \
  --output /transfer/llama-3.3-70b-2026.07.1.tar \
  --include-referrers \
  registro-dmz.interno/modelos/llama-3.3-70b:2026.07.1

# 2. Verificar el digest exacto antes de cruzar
oras manifest fetch --descriptor \
  registro-dmz.interno/modelos/llama-3.3-70b:2026.07.1 > /transfer/descriptor.json
sha256sum /transfer/llama-3.3-70b-2026.07.1.tar > /transfer/SHA256SUMS

# 3. Cruce fisico o por diodo de datos

# 4. En el entorno aislado: restaurar preservando digests y referrers
oras restore \
  --input /transfer/llama-3.3-70b-2026.07.1.tar \
  registro-aislado.interno/modelos/llama-3.3-70b

El flag --include-referrers es el que hace que esto sirva de algo: sin él cruzan los pesos, pero se quedan atrás la firma, el AIBOM y las atestaciones, y el entorno aislado recibe un modelo indistinguible de uno descargado a mano. oras backup está marcado como experimental en v1.3.0, lo que hay que asumir explícitamente antes de convertirlo en procedimiento; la alternativa consolidada para la copia pura es skopeo copy con formato oci-archive, a costa de gestionar los referrers aparte.

Qué debe viajar pegado al artefacto para que el artículo 3/4 pueda verificarlo: el digest del artefacto y de sus blobs; la procedencia del origen (repositorio y revisión concreta, no el nombre del modelo, más quién lo ingirió y cuándo); la identidad del proceso que empaquetó (pipeline, commit, ejecución), materia prima de una atestación SLSA; las referencias cruzadas al dataset de evaluación y al modelo base si es un derivado; y el estado de promoción con su aprobador, que en un entorno aislado no se puede consultar contra el MLflow del entorno conectado. Todo ello cabe en anotaciones OCI y en artefactos referidos vía subject. La regla: el entorno aislado debe poder verificar sin llamar a nadie; si la verificación necesita una consulta al exterior, no es air-gap.

Mapa de decisión

Basta con un registro OCI y una convención cuando tienes menos de una veintena de modelos, un solo equipo decide qué se promociona, el linaje al entrenamiento no es requisito regulatorio (típico si consumes modelos de terceros) y ya operas Harbor. Aquí un model registry añade una consola más que mantener y ninguna respuesta que no dieran ya los tags inmutables y las anotaciones. Es el caso mayoritario en inferencia pura.

Hace falta un model registry si entrenas o haces fine-tuning y necesitas linaje auditable de modelo a dataset y a código —el ciclo de fine-tuning continuo—; si varios equipos compiten por promocionar versiones y hace falta un flujo de aprobación explícito; si tienes que responder a un auditor de ISO 42001 o del EU AI Act sobre qué versión estaba en producción en una fecha; o si gestionas decenas de adapters con propietarios distintos.

El registro se vuelve burocracia sin valor cuando se registra el modelo después de desplegarlo (catálogo como acto administrativo, no como puerta); cuando el estado de promoción no está conectado a ningún control técnico y se puede desplegar una versión no aprobada; o cuando conviven un model registry y un registro OCI con dos verdades y ningún digest que las una. Este último es el fallo más común y el más caro: dos sistemas de versionado que divergen en silencio.

Trampas operativas

Tags mutables y latest en producción. No es un consejo de estilo: un tag mutable convierte cualquier auditoría en una conjetura y permite que dos réplicas del mismo Deployment sirvan bytes distintos. Además, en KServe latest fuerza pullPolicy: Always y anula la caché del nodo, así que se paga en cold start lo que se pierde en trazabilidad. Regla: tags inmutables por política de proyecto y despliegue por digest.

GC que borra capas referenciadas. El expurgo del archivo. El garbage collector identifica blobs que ningún manifest referencia, y la ventana entre “he subido los blobs” y “he subido el manifest” es exactamente donde un GC concurrente puede llevárselos. En Harbor esto se ha manifestado históricamente en _uploads y en discrepancias entre el espacio que la UI declara liberado y el real. Mitigación: GC en ventana con push bloqueado, y no confiar en la primera pasada.

El disco del registro y el coste del versionado. Aritmética brutal: 140 GB por versión con tres versiones vivas son 420 GB; doce modelos así, 5 TB; con retención de seis versiones y dos sedes replicadas, 20 TB. Y es almacenamiento de la clase cara si el registro vive sobre bloque replicado. La deduplicación ayuda poco, porque los pesos cambian por completo entre versiones. La política de retención hay que diseñarla antes de llenar el registro, y hay que separar por proyecto los modelos de las imágenes de runtime, porque sus retenciones no tienen nada que ver.

Replicación que satura el enlace intersedes. Una regla por evento sobre un repositorio de modelos mueve 140 GB cada vez que alguien empuja una versión: en un enlace de 1 Gb/s compartido, casi veinte minutos a tope en mitad de la jornada. Programar en ventana, limitar ancho de banda, filtrar por label qué versiones se replican y usar P2P dentro de cada sede.

Meter el modelo en la imagen del runtime. Tentador porque “así solo hay un artefacto”. El resultado es una imagen de 145 GB que hay que reconstruir y redistribuir cada vez que se parchea una CVE del runtime, con el ciclo de vida de seguridad acoplado al del modelo. Separados: runtime pequeño y parcheable, modelo grande y estable.

Para una factoría de inferencia

Tres cosas accionables para quien explota una factoría de inferencia on-premise, no de entrenamiento.

Primera: corta la dependencia de internet en el arranque, esta semana. No hace falta un proyecto de plataforma: basta una tarea de ingesta que baje cada modelo una vez, lo empuje a Harbor como artefacto OCI con tag inmutable y anotaciones de procedencia, y cambiar los despliegues a referencia por digest. El HF_TOKEN desaparece del pod y pasa al pipeline de ingesta, que es donde tiene sentido y donde encaja con el hardening de secretos.

Segunda: decide conscientemente si necesitas catálogo. Si solo consumes modelos de terceros y no entrenas, probablemente no: el registro OCI con convención da el 90 % del valor con el 10 % del coste operativo. Si entrenas, haces fine-tuning o gestionas adapters por tenant, monta MLflow y —esto es lo importante— guarda el digest OCI como tag de cada versión, para que catálogo y depósito no puedan divergir.

Tercera: mueve el cuello de botella al sitio correcto. Antes de invertir en aceleradores de carga, mide los tres tramos. Si el modelo llega por red en cada arranque, el trabajo es caché local en NVMe, image volumes con pullPolicy: IfNotPresent y, con muchos nodos, P2P. Solo cuando el modelo ya está en el disco del nodo tiene sentido pelearse con la concurrencia del loader.

Ya sabes de dónde vienen los bytes y por qué camino llegan. Queda la pregunta que lo sostiene todo y que hasta ahora hemos dado por buena: ¿por qué te fías de esos bytes? Un digest garantiza integridad —que nadie los ha cambiado— pero no procedencia: no dice quién los produjo, con qué datos, ni si alguien con autoridad certificó que se podían servir. El tercer artículo de la serie construye esa respuesta con Sigstore, SLSA, in-toto y AIBOM, colgada precisamente del subject y la Referrers API que aquí hemos dejado preparados.

Ver también

Fuentes