- 🌟 Proyecto Finalista - Hackathon de la Secretaría de Estado de Digitalización e Inteligencia Artificial (SEDIA), Gobierno de España 🇪🇸
- 🛡️ Evaluado bajo el framework de Derechos Humanos del PNUD (UN): Este proyecto incluye un informe de impacto detallado basado en los estándares internacionales de IA Confiable (ver
GOVERNANCE_HRIA.md). - 💰 Para el análisis económico completo (TCO, ROI, economía del token, riesgo de privacidad y coste energético), consultar el Caso de Negocio.
EvolveAI es una startup tecnológica boutique nacida con la misión de redefinir cómo las organizaciones escalan su inteligencia operativa. Fundada por dos referentes en el sector de la Inteligencia Artificial con trayectorias complementarias en soluciones de nivel industrial:
- Antonio Zarauz: Líder del proyecto EMDI y perfil R&D con formación en Stanford. Arquitecto de soluciones de vanguardia, Antonio dirige la investigación y el desarrollo de arquitecturas complejas (multimodales, orquestación de GPUs, Kubernetes), transformando la investigación de frontera en sistemas de producción robustos.
- Mateo Álvarez: Director de Estrategia y Perfil Ejecutivo. Con una sólida base en ingeniería aeroespacial y formación en MIT/Harvard, Mateo lidera la visión de negocio y la integración de IA en grandes corporaciones (como Credicorp), enfocándose en la gobernanza y la eficiencia escalable.
La sinergia entre la visión estratégica y el rigor técnico ha permitido a EvolveAI diseñar proyectos disruptivos en entornos de alta exigencia, demostrando que la soberanía tecnológica y el máximo rendimiento pueden ir de la mano. EMDI es el resultado de este ADN: ingeniería de primer nivel para la soberanía del dato empresarial.
El tejido empresarial español y europeo está impulsado por pequeñas y medianas empresas (PYMES). Hasta ahora, la adopción de Inteligencia Artificial Generativa y Multimodal de vanguardia implicaba un peaje inasumible: depender de APIs privativas, perder la soberanía de los datos y afrontar costes escalables prohibitivos (vendor lock-in).
EMDI nace para romper esa barrera. Nuestro objetivo es empoderar a las empresas para que desplieguen su propia infraestructura y dominen su tech stack. Al aprovechar el poder de la comunidad Open Source y la orquestación inteligente de hardware, EMDI permite a cualquier empresa tener IA de nivel corporativo en su propio entorno, garantizando:
- 🛡️ Soberanía del Dato Absoluta: Tus documentos, imágenes y conversaciones nunca abandonan tu infraestructura.
- 🤝 Impulso al Open Source: Integramos los mejores modelos abiertos de la comunidad (Qwen, GLM), demostrando que lo abierto compite con lo privativo.
- 📉 Accesibilidad Financiera: Una arquitectura revolucionaria que divide los costes de hardware, haciendo viable la IA para la economía real.
Si bien EMDI emplea una colección de modelos orientados a soluciones de document intelligence, en EvolveAI contamos con un catálogo completo de componentes tecnológicos que nos permiten flexiblizarnos a multitud de escenarios.
EMDI no es solo un conjunto de modelos; es un motor de inferencia optimizado para la realidad operativa de la empresa. Nuestra arquitectura se asienta sobre tres pilares de ingeniería avanzada que permiten desplegar un pipeline de orquestación de inteligencia documental multimodal sobre cualquier hardware NVIDIA con capacidad CUDA superior a 8.0, eliminando los cuellos de botella tradicionales del cloud. Ejemplificaremos este desarrollo sobre una única GPU NVIDIA A100 de 80GB, aunqwue será igualmente válido para cualquier hardware con características similares o superiores en términos de VRAM y CPU cores.
Utilizamos MIG (Multi-Instance GPU) en modo Mixed Strategy para particionar físicamente el hardware. A diferencia de las soluciones por software, MIG garantiza un aislamiento total de memoria y SMs (Streaming Multiprocessors).
Esta orquestación se basa en una coordinación de doble capa definida en nuestros manifiestos:
-
Capa de Nodo (
nodeSelector): Todos los servicios utilizan el selectornvidia.com/mig.config: rag-balanced. Esto asegura que los pods solo se desplieguen en nodos donde la GPU ha sido físicamente configurada con nuestra geometría específica (10/20/40), evitando errores de programación en hardware no preparado. -
Capa de Partición (
resources.limits): Aunque comparten el mismo nodo, cada microservicio solicita una instancia de recurso única del driver de NVIDIA. Esto fuerza a Kubernetes a realizar un "bin-packing" perfecto dentro de la misma tarjeta:- 10GB (
nvidia.com/mig-1g.10gb): Detección de Layout & Orquestación de Visión (PP-DocLayoutV3). - 20GB (
nvidia.com/mig-2g.20gb): Inferencia de Embeddings Multimodales (Qwen3-VL-2B). - 40GB (
nvidia.com/mig-3g.40gb): Razonamiento, Chat & OCR de Regiones (Qwen3.5-4B).
- 10GB (
-
Aislamiento de SLA: Gracias a esta partición física, un pico de carga en el OCR nunca degradará la latencia del Chat LLM, ya que no compiten por ancho de banda de memoria ni núcleos de cómputo.
El mayor reto de las arquitecturas basadas en GPU es el tiempo de arranque. EMDI implementa una triple optimización de carga única:
- ACR Artifact Streaming: Las imágenes de contenedor (que contienen las pesadas librerías CUDA) se transmiten bajo demanda. El pod arranca en segundos, descargando solo los bits necesarios para iniciar la ejecución mientras el resto llega en segundo plano.
- Carga Ultra-Rápida con InstantTensor: Sustituimos los cargadores tradicionales por
instanttensor. Esta tecnología permite una ingesta masiva de pesos.safetensorsdesde el almacenamiento persistente a la VRAM mediante E/S directa y prefetching en pipeline, reduciendo el tiempo de carga del modelo en un ~70%. - Datacenter-to-Datacenter Ingestion: Los modelos nunca tocan tu red local. Se ingieren directamente de los servidores de Hugging Face a tu Azure Blob PVC mediante Jobs distribuidos, garantizando que el almacenamiento esté siempre "caliente" y listo para el escalado.
En lugar de un proxy pesado, EMDI utiliza un Gateway Unificado escrito en Rust, diseñado para actuar como el sistema nervioso central del nodo.
- Co-localización y Latencia CERO: Mediante el uso de
nodeSelectorytolerationsde Kubernetes, garantizamos que el Gateway se despliegue en el mismo nodo físico que la GPU. Esto elimina saltos de red externos; la comunicación entre el Gateway y los motores de inferencia (Embeddings, OCR, Chat) se realiza a través de la interfaz virtual interna del nodo, garantizando una latencia de tránsito despreciable. - Descubrimiento de Servicios Portable: Utilizamos DNS interno de Kubernetes (
service-names). Esto permite que el stack sea 100% agnóstico al hardware: la misma configuración funciona sin cambios en una A100 de Azure, un sistema NVIDIA DGX o una estación de trabajo RTX Blackwell. - Security por Diseño: Control estricto de los flujos de datos y formateo dinámico de multimedia para los modelos de visión, asegurando que la soberanía del dato se mantenga desde la entrada hasta la generación sin que los datos abandonen nunca el "perímetro de memoria" del nodo.
Para maximizar el ahorro en infraestructuras de alto rendimiento (A100/H100), EMDI integra KEDA (Kubernetes Event-driven Autoscaling).
- Escalado a CERO: Fuera del horario laboral, el sistema escala automáticamente todos los microservicios a 0 réplicas. Esto permite que el autoscaler del clúster (AKS/On-prem) apague los nodos de GPU, eliminando el coste de facturación por hardware inactivo.
- Warm-up Programado: Configurado específicamente para el horario de oficina de Nueva York (EST), el sistema levanta automáticamente una réplica funcional de todo el stack de lunes a viernes (9:00 AM - 5:00 PM), asegurando que el sistema esté "caliente" y listo para los usuarios cuando comience la jornada.
A diferencia de los pipelines tradicionales que separan la extracción de texto de la interpretación visual, EMDI unifica ambos procesos bajo un mismo "cerebro" multimodal. El modelo Qwen 3.5 4B no solo actúa como un motor de OCR de altísimo rendimiento, sino que desempeña un doble rol crítico en la arquitectura:
- Reconocimiento Estructurado (OCR Dinámico): Basándose en las regiones detectadas por
PP-DocLayoutV3, Qwen transforma fragmentos de imagen en texto Markdown, tablas estructuradas o fórmulas LaTeX con una precisión superior a los modelos de OCR convencionales, gracias a su comprensión contextual del lenguaje. - Agente de Razonamiento Visual (VLM): Cuando el sistema detecta elementos no textuales (gráficos, imágenes, diagramas), Qwen cambia su modo de operación para generar descripciones semánticas ricas. Esto permite que EMDI "entienda" lo que representa un gráfico de barras o una fotografía, inyectando metadatos visuales en el flujo de datos que de otro modo se perderían, superando las limitaciones de la IA documental tradicional.
Desde EvolveAI, concebimos EMDI como el motor central de la inteligencia de tu negocio. Dado que este despliegue actúa como un servidor de inferencia de alto rendimiento, el esquema ideal de consumo es mediante arquitectura cliente-servidor:
graph TD
subgraph Clients ["💻 Recomendación Core: Clientes Ligeros"]
direction LR
PC1[PC Usuario]
PC2[App Móvil]
PC3[Navegador]
end
subgraph Cluster ["🛡️ Infraestructura EMDI (Soberanía del Dato)"]
direction TB
GW[AI Gateway - Rust]
subgraph GPU ["NVIDIA A100 (Estrategia MIG Mixed)"]
MIG1[10GB: Layout]
MIG2[20GB: Embeddings]
MIG3[40GB: LLM / Chat / OCR]
end
end
subgraph Managed ["🌐 Soluciones Enterprise EvolveAI"]
EVO[Expertos DevOps & AI]
end
Clients -- "API Calls (HTTPS)" --> GW
GW --> MIG1
GW --> MIG2
GW --> MIG3
MIG1 -- "Orchestrate Region OCR" --> MIG3
EVO -- "Aprovisionamiento" --> Cluster
EVO -- "Mantenimiento" --> Cluster
EVO -- "Seguridad" --> Cluster
- 💻 Recomendación Core (Despliegue Local): Recomendamos desplegar aplicaciones cliente ligeras directamente en los ordenadores locales de los empleados (ideal para agencias, oficinas o despachos de abogados). La carga pesada de IA se procesa en el clúster EMDI (en vuestra infraestructura local o cloud soberano), minimizando los requisitos de hardware de los usuarios.
- 🌐 Soluciones Enterprise Integrales: EvolveAI ofrece servicios Enterprise "llave en mano" que incluyen aprovisionamiento, despliegue, mantenimiento y securización, permitiendo a tu empresa centrarse únicamente en extraer valor de sus datos.
EMDI permite a las empresas desplegar su propia infraestructura de IA con un ahorro de OPEX superior al 90% en comparación con APIs de cloud público. Al anclar el coste a la electricidad y la amortización del hardware, se elimina el riesgo de inflación del token y se garantiza la soberanía absoluta.
📊 Análisis Detallado: Para consultar las matrices completas de TCO, ROI, economía del token, riesgo de privacidad y coste energético, consulta el Caso de Negocio Completo.
| Línea de ingreso | Tipo | Qué incluye |
|---|---|---|
| EMDI Core | Open Source (€0) | Stack de orquestación, manifiestos K8s, gateway, cliente de muestra |
| Enterprise License | Suscripción | Logging inmutable, RBAC, autoescalado avanzado, soporte SLA |
| EvolveAI Managed | Recurrente | Aprovisionamiento, mantenimiento y SLA 99,9% "llave en mano" |
Aunque el despliegue actual de EMDI está optimizado para Azure (AKS) con fines de demostración y agilidad para la Hackathon SEDIA, el stack tecnológico ha sido diseñado para ser agnóstico a la infraestructura y altamente escalable:
- Hardware Enterprise On-Premise: El motor de orquestación de EMDI es 100% compatible con infraestructuras soberanas de alto rendimiento, como sistemas NVIDIA DGX o estaciones de trabajo equipadas con NVIDIA RTX 6000 (Ada/Blackwell Edition). Esto permite a las empresas eliminar completamente la dependencia del cloud público si así lo requieren.
- Orquestación Client-Side Avanzada: Estamos trabajando en capas de orquestación que permitan mover parte de la lógica de pre-procesamiento y razonamiento ligero directamente al dispositivo del cliente (Edge AI), optimizando el ancho de banda y garantizando latencias ultra-bajas en aplicaciones críticas.
- Soporte Multimodal Expandido: Integración de modelos especializados en audio y vídeo bajo la misma arquitectura de particionado físico, convirtiendo a EMDI en el cerebro operativo integral de la empresa.
Define los parámetros de tu despliegue en Azure.
💡 Nota: Para garantizar la compatibilidad "Plug & Play", hemos pre-configurado el nombre del registro como
emdiacr. Si deseas usar otro nombre, deberás actualizarlo manualmente en los archivos bajok8s/apps/y enk8s/kustomization.yaml.
# Parámetros del Clúster y Registro
export LOCATION="XXXXXX"
export SUBSCRIPTION_ID="XXXXXX"
export RESOURCE_GROUP="XXXXXX"
export AKS_NAME="aksemdi"
export ACR_NAME="acremdi"
# Configurar suscripción
az account set --subscription $SUBSCRIPTION_IDLa configuración por defecto de los servicios cloud suele imponer tamaños de GPU uniformes y drivers pre-configurados que limitan la flexibilidad. Para lograr nuestra división asimétrica (10/20/40), desplegamos un "lienzo en blanco" y tomamos el control manual del ciclo de vida del hardware.
# 1. Crear el ACR (Premium requerido para Artifact Streaming)
az acr create -g $RESOURCE_GROUP -n $ACR_NAME --sku Premium --location $LOCATION
# 2. Crear el recurso de cómputo y vincular el ACR
# EXPLICACIÓN: --enable-blob-driver activa el soporte nativo para Azure Blob Storage,
# permitiendo usar la clase 'azureblob-fuse-premium' para el almacenamiento de modelos.
az aks create \
--resource-group $RESOURCE_GROUP \
--location $LOCATION \
--name $AKS_NAME \
--attach-acr $ACR_NAME \
--node-count 1 \
--enable-managed-identity \
--enable-blob-driver \
--generate-ssh-keys
# 3. Obtener credenciales
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_NAME
# 4. Añadir el Node Pool de GPU con Autoescalado (0 a 4)
# EXPLICACIÓN CRÍTICA: Se incluyen labels de 'node-template-resources' para que el
# Cluster Autoscaler sepa que este pool proveerá recursos MIG incluso cuando esté en 0.
# También se incluye 'nvidia.com/mig.config' para que la configuración sea persistente
# tras re-escalados (los labels manuales se pierden al escalar a cero).
az aks nodepool add \
--resource-group $RESOURCE_GROUP \
--cluster-name $AKS_NAME \
--name mignode \
--node-vm-size Standard_NC24ads_A100_v4 \
--gpu-driver none \
--enable-cluster-autoscaler \
--min-count 0 \
--max-count 4 \
--node-taints sku=gpu:NoSchedule \
--labels \
sku=gpu \
app=emdi-inference \
nvidia.com/mig.config=rag-balanced \
--tags \
"k8s.io_cluster-autoscaler_node-template_resources_nvidia.com_mig-1g.10gb=1" \
"k8s.io_cluster-autoscaler_node-template_resources_nvidia.com_mig-2g.20gb=1" \
"k8s.io_cluster-autoscaler_node-template_resources_nvidia.com_mig-3g.40gb=1"El GPU Operator es el "orquestador del hardware". Lo configuramos en modo mixed para permitir particiones de distintos tamaños en una misma tarjeta (algo que el modo single no permite).
# 1. Instalar el operador mediante Helm
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update
# EXPLICACIÓN DE VALORES:
# - mig.strategy=mixed: Habilita la creación de instancias de GPU de distintos perfiles (1g, 2g, 3g).
# - WITH_REBOOT=true (en values): El particionado MIG requiere cambios a nivel de firmware/GPU-reset.
# Esta flag permite al operador reiniciar el nodo automáticamente para aplicar la nueva geometría.
helm install gpu-operator nvidia/gpu-operator \
-n gpu-operator --create-namespace \
-f k8s/infra/gpu-operator-values.yaml
# 2. Aplicar la configuración de geometría EMDI (10GB/20GB/40GB)
kubectl apply -f k8s/infra/mig-config.yaml
# 3. Vincular el ConfigMap de MIG al Operador
# EXPLICACIÓN: Este parche vincula nuestra geometría personalizada (10/20/40) definida en
# 'k8s/infra/mig-config.yaml' con el motor del operador.
kubectl patch clusterpolicies.nvidia.com/cluster-policy \
--type='json' -p='[{"op":"replace", "path":"/spec/migManager/config/name", "value":"device-plugin-config"}]'Una vez aplicados los comandos anteriores, el autoscaler levantará un nodo (o usará el existente). El 'mig-manager' detectará automáticamente el label nvidia.com/mig.config=rag-balanced que definimos en el Node Pool y procederá a reconfigurar la GPU físicamente.
1. Comprobar el estado del particionado:
Verifica que el operador haya terminado de configurar el hardware. El estado debe ser success.
kubectl get nodes -l kubernetes.azure.com/agentpool=mignode -o custom-columns=NAME:.metadata.name,MIG_CONFIG:".metadata.labels.nvidia\.com\/mig\.config",MIG_STATE:".metadata.labels.nvidia\.com\/mig\.config\.state"- Salida Esperada:
NAME MIG_CONFIG MIG_STATE aks-mignode-35982219-vmss000001 rag-balanced success
2. Ver los recursos asignables (Allocatable): Este comando confirma que Kubernetes reconoce las tres particiones como recursos independientes.
kubectl get nodes -l kubernetes.azure.com/agentpool=mignode -o custom-columns=NAME:.metadata.name,MIG_10G:".status.allocatable.nvidia\.com\/mig-1g\.10gb",MIG_20G:".status.allocatable.nvidia\.com\/mig-2g\.20gb",MIG_40G:".status.allocatable.nvidia\.com\/mig-3g\.40gb"- Salida Esperada:
NAME MIG_10G MIG_20G MIG_40G aks-mignode-35982219-vmss000001 1 1 1
3. Ver la partición real desde dentro del hardware:
Lanza un pod temporal para ejecutar nvidia-smi y confirmar la geometría física detectada por el driver:
kubectl run --rm -it v-gpu --image=nvidia/cuda:12.1.0-base-ubuntu22.04 \
--overrides='{"spec": {"containers": [{"name": "v-gpu", "image": "nvidia/cuda:12.1.0-base-ubuntu22.04", "command": ["nvidia-smi", "-L"], "resources": {"limits": {"nvidia.com/gpu": 1}}}]}}' \
--restart=Never- Salida Esperada:
GPU 0: NVIDIA A100-SXM4-80GB (UUID: GPU-...) MIG 3g.40gb Device 0: (UUID: MIG-...) MIG 2g.20gb Device 1: (UUID: MIG-...) MIG 1g.10gb Device 2: (UUID: MIG-...)
El orquestador de Kubernetes ahora reconocerá mágicamente tres tarjetas gráficas independientes y ajustadas a cada tarea.
En EMDI, tratamos los modelos como datos pesados, no como código. En lugar de descargar ~100GB localmente para luego re‑subirlos, realizamos una ingesta in‑cluster mediante un Kubernetes Job.
Este Job descarga los pesos directamente desde Hugging Face hacia un volumen persistente (PVC) respaldado por Azure Blob. Una vez completado, los modelos están disponibles para todos los pods de inferencia sin descargas repetidas.
Los pesos de los modelos son almacenados en un Blob CSI‑backed PVC. Este debe existir y estar vinculado antes de lanzar el Job.
# 1. Aplicar el PVC
kubectl apply -f k8s/provisioning/pvc.yaml
# 2. Verificar que el estado sea 'Bound'
kubectl get pvc model-weights-pvcSalida Esperada:
NAME STATUS VOLUME CAPACITY ACCESS MODES STORAGECLASS AGE
model-weights-pvc Bound pvc-37837be3-65b2-4cf0-b6ce-f7fe61b13adb 300Gi RWX azureblob-fuse-premium 29s
El Job utiliza variables de entorno para gestionar la descarga paralela.
# Lanzar el job
kubectl apply -f k8s/provisioning/ingest-job.yaml
# Confirmar creación y estado detallado
kubectl get job model-weight-ingest
kubectl describe job model-weight-ingestNo utilices nombres de Pods manuales; utiliza la abstracción del Job para seguir los logs de la descarga desde Hugging Face:
kubectl logs -f job/model-weight-ingest(Una vez veas el mensaje ✅ Ingestion complete, puedes limpiar el job con kubectl delete job model-weight-ingest)
Si necesitas verificar manualmente la integridad de los archivos .safetensors descargados, lanza este pod de depuración:
kubectl run weights-debug \
--rm -it \
--image=ubuntu:22.04 \
--overrides='
{
"spec": {
"containers": [{
"name": "debug",
"image": "ubuntu:22.04",
"command": ["bash"],
"stdin": true,
"tty": true,
"volumeMounts": [{
"name": "weights",
"mountPath": "/mnt/models"
}]
}],
"volumes": [{
"name": "weights",
"persistentVolumeClaim": {
"claimName": "model-weights-pvc"
}
}]
}
}'Dentro del pod, puedes inspeccionar los modelos:
cd /mnt/models
ls -lhR
exitLa salida esperada será algo como lo siguiente:
root@weights-debug:/mnt/models# ls -lhR
.:
total 0
drwxrwxrwx 2 root root 4.0K May 22 22:10 Qwen
./Qwen:
total 0
drwxrwxrwx 2 root root 4.0K May 22 22:10 Qwen3-VL-Embedding-2B
drwxrwxrwx 2 root root 4.0K May 22 22:11 Qwen3.5-4B
./Qwen/Qwen3-VL-Embedding-2B:
total 0
-rwxrwxrwx 1 root root 4.0G May 22 22:11 model.safetensors
-rwxrwxrwx 1 root root 1.6K May 22 22:10 config.json
-rwxrwxrwx 1 root root 11M May 22 22:11 tokenizer.json
-rwxrwxrwx 1 root root 5.3K May 22 22:11 tokenizer_config.json
./Qwen/Qwen3.5-4B:
total 0
-rwxrwxrwx 1 root root 12K May 22 22:11 LICENSE
-rwxrwxrwx 1 root root 76K May 22 22:11 README.md
-rwxrwxrwx 1 root root 7.6K May 22 22:11 chat_template.jinja
-rwxrwxrwx 1 root root 3.1K May 22 22:11 config.json
-rwxrwxrwx 1 root root 3.2M May 22 22:11 merges.txt
-rwxrwxrwx 1 root root 5.0G May 22 22:11 model.safetensors-00001-of-00002.safetensors
-rwxrwxrwx 1 root root 3.8G May 22 22:12 model.safetensors-00002-of-00002.safetensors
-rwxrwxrwx 1 root root 75K May 22 22:12 model.safetensors.index.json
-rwxrwxrwx 1 root root 390 May 22 22:12 preprocessor_config.json
-rwxrwxrwx 1 root root 13M May 22 22:12 tokenizer.json
-rwxrwxrwx 1 root root 17K May 22 22:12 tokenizer_config.json
-rwxrwxrwx 1 root root 385 May 22 22:12 video_preprocessor_config.json
-rwxrwxrwx 1 root root 6.5M May 22 22:12 vocab.jsonConstruimos todas las imágenes directamente en Azure Container Registry (ACR). Dado que compilamos binarios de Rust (Gateway) y librerías de GPU pesadas, utilizamos Agent Pools dedicados para evitar errores de memoria (OOM).
Construimos todas las imágenes directamente en Azure Container Registry (ACR). Con las siguientes instrucciones, seremos capaces de disponibilizar nuestros paquetes de código de forma rápida.
# 1. Build & Push con Artifact Streaming (Indispensable para GPU)
# Ejecutar desde la raíz del proyecto
az acr build --registry $ACR_NAME --image unified-gateway:latest ./nodes/gateway
az acr build --registry $ACR_NAME --image layout-api:latest ./nodes/layout
az acr build --registry $ACR_NAME --image vllm-qwen-embedding:latest ./nodes/embeddings
az acr build --registry $ACR_NAME --image vllm-qwen-chat:latest ./nodes/llm_server
# 2. Habilitar Artifact Streaming para acelerar el arranque en AKS
az acr artifact-streaming create --name $ACR_NAME --image layout-api:latest --no-wait
az acr artifact-streaming create --name $ACR_NAME --image vllm-qwen-embedding:latest --no-wait
az acr artifact-streaming create --name $ACR_NAME --image vllm-qwen-chat:latest --no-waitLas builds de ACR son trabajos asíncronos administrados. Puedes cerrar tu terminal y volver después para ver el estado:
# Listar ejecuciones recientes
az acr task list-runs --registry $ACR_NAME --output table
# Recuperar logs de una build después de que el terminal se cerró
export LAST_RUN_ID=$(az acr task list-runs --registry $ACR_NAME --top 1 --query "[0].runId" -o tsv)
az acr task logs --registry $ACR_NAME --run-id $LAST_RUN_IDSi lanzaste builds que ya no necesitas, puedes cancelar ejecuciones en curso o eliminar runs históricos para mantener el registro limpio.
# 1. Cancelar una ejecución activa
az acr task list-runs --registry $ACR_NAME --output table
export RUN_ID=<RUN_ID_EN_EJECUCION>
az acr task cancel-run --registry $ACR_NAME --run-id $RUN_ID
# 2. Eliminar runs antiguos (requiere ACR Tasks preview / purge)
# Borra ejecuciones con más de X tiempo (ej: 1 día)
az acr task purge \
--registry $ACR_NAME \
--ago 1d \
--filter "RunType=Build" \
--yes💡 Nota: ACR no soporta borrado manual individual de runs completados de forma directa, pero purge permite limpiar históricamente. Esto es útil para evitar ruido en auditoría y mantener el control de costos/diagnóstico.
💡 Nota: Si ya tienes un clúster AKS y quieres vincular el ACR a posteriori, usa:
az aks update -n $AKS_NAME -g $RESOURCE_GROUP --attach-acr $ACR_NAME
Antes de activar el stack de EMDI, debemos preparar el clúster con las herramientas de orquestación de eventos (KEDA) y monitoreo (Prometheus) necesarias para el autoescalado y la observabilidad.
# 1. Instalar KEDA (Requerido para el autoescalado programado)
helm repo add kedacore https://kedacore.github.io/charts
helm repo update
helm install keda kedacore/keda -n keda --create-namespace
# 2. Instalar Prometheus Stack (Requerido para observabilidad y métricas de GPU)
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm repo update
helm install prometheus prometheus-community/kube-prometheus-stack -n monitoring --create-namespaceCon el hardware optimizado, la "inteligencia" descargada y los operadores (GPU, KEDA, Prometheus) listos, lanzamos todo el entorno. Este paso final orquesta no solo los pods, sino también la infraestructura de red necesaria para la soberanía y accesibilidad.
# Desplegar el stack completo (Gateway, Motores, Red y Reglas de Autoescalado)
kubectl apply -k k8s/El despliegue incluye una configuración de red de doble capa diseñada para maximizar la seguridad y el rendimiento:
-
Cableado Interno Estricto (
services.yaml):- Inferencia Aislada: Los motores (Layout, Embeddings, LLM) se exponen mediante
ClusterIP. Esto significa que son invisibles desde fuera del clúster, forzando a que todo el tráfico pase obligatoriamente por el Gateway en Rust para su validación y orquestación. - Gateway como LoadBalancer: El servicio del Gateway se expone hacia el exterior, actuando como el único punto de entrada (Single Point of Entry) para las aplicaciones de la empresa.
- Inferencia Aislada: Los motores (Layout, Embeddings, LLM) se exponen mediante
-
Capa de Aplicación y Dominios (
ingress.yaml):- Gestión de Carga Multimedia: Configuramos anotaciones específicas en el Ingress (
proxy-body-size: 50m) para permitir el envío de documentos e imágenes de alta resolución sin que el balanceador de carga corte la conexión. - Abstracción de DNS: El Ingress permite mapear el servicio a un nombre de dominio amigable, facilitando la integración con certificados SSL y políticas de seguridad corporativas.
- Gestión de Carga Multimedia: Configuramos anotaciones específicas en el Ingress (
Una vez desplegado el sistema, es fundamental verificar que la orquestación asimétrica y el autoescalado estén funcionando correctamente.
Dado que hemos configurado el escalado a cero con KEDA, es posible que los pods no aparezcan inmediatamente si estás fuera del horario configurado. Puedes verificar el estado de los "objetos escalables" y los pods con:
# Ver el estado de las reglas de autoescalado
kubectl get scaledobjects
# Ver los pods (pueden estar en estado 'Terminating' o no existir si están escalados a 0)
kubectl get pods -l app=emdi-inferenceSi los pods están en ejecución (Running), puedes ver lo que ocurre dentro de cada motor.
💡 Tip: Si los comandos no devuelven nada, asegúrate de que los pods no hayan sido escalados a 0 por KEDA. Puedes comprobarlo con
kubectl get pods.
# Gateway (Rust): Orquestación de peticiones
kubectl logs -l service=unified-gateway --all-containers=true -f --prefix=true
# Layout (PP-DocLayoutV3): Detección y Orquestación de regiones
kubectl logs -l service=layout-api --all-containers=true -f --prefix=true
# Embeddings (Qwen-VL): Generación de vectores
kubectl logs -l service=rag-embedding --all-containers=true -f --prefix=true
# Chat LLM & OCR (Qwen-3.5): Razonamiento, Chat y Visión (delegado)
kubectl logs -l service=rag-llm-chat --all-containers=true -f --prefix=truePara asegurar que los microservicios están utilizando sus particiones físicas dedicadas, puedes ejecutar nvidia-smi directamente en uno de los pods activos:
# Ejemplo: Verificar la partición de 40GB en el pod de Chat
kubectl exec -it $(kubectl get pods -l service=rag-llm-chat -o name | head -n 1) -- nvidia-smiSi instalaste el Prometheus Stack en el paso 6.1, puedes acceder a la interfaz visual para ver métricas en tiempo real:
# Port-forward para acceder a Grafana (Usuario: admin / Password: prom-operator)
kubectl port-forward deployment/prometheus-grafana 3000:3000 -n monitoring
# Abre http://localhost:3000 en tu navegadorEl Gateway en Rust expone una interfaz limpia y corporativa para tu empresa:
POST /ocr: Recibe{"data": "<base64_string>"}(soporta imagen o PDF) y devuelve un objeto estructurado conmarkdowny metadatos delayout.POST /embeddings: Endpoint estándar para integración de vectores en tus bases de datos.POST /chat/completions: Interfaz conversacional compatible con el estándar de OpenAI para interactuar con tus documentos.
Una vez desplegado el sistema, puedes obtener la dirección pública del Gateway con el siguiente comando:
export GATEWAY_IP=$(kubectl get svc unified-gateway-service -o jsonpath='{.status.loadBalancer.ingress[0].ip}')
export GATEWAY_URL="http://$GATEWAY_IP"
echo "EMDI Gateway disponible en: $GATEWAY_URL"El endpoint de OCR ahora devuelve la estructura completa del documento y soporta la ingesta de archivos PDF.
import os
import requests
import base64
from pathlib import Path
from typing import Union
def sniff_mime_from_magic(data: bytes) -> str:
# Minimal magic-byte sniffing for common types
if data.startswith(b"%PDF-"):
return "application/pdf"
if data.startswith(b"\xFF\xD8\xFF"):
return "image/jpeg"
if data.startswith(b"\x89PNG\r\n\x1a\n"):
return "image/png"
if data[:6] in (b"GIF87a", b"GIF89a"):
return "image/gif"
if data.startswith(b"RIFF") and data[8:12] == b"WEBP":
return "image/webp"
return "application/octet-stream"
def file_to_data_uri_sniffed(filepath: Union[str, Path]) -> str:
path = Path(filepath)
raw = path.read_bytes()
mime_type = sniff_mime_from_magic(raw)
b64 = base64.b64encode(raw).decode("utf-8")
return f"data:{mime_type};base64,{b64}"
# Fichero de muestra
payload = {
"data": file_to_data_uri_sniffed('./sample_files/qwen3_technical_report.pdf')
}
response = requests.post(f"{GATEWAY_URL}/ocr", json=payload, timeout=600)
response.raise_for_status()
with open('./sample_files/qwen3_technical_report.json', 'w', encoding='utf-8') as f:
json.dump(response.json(), f, ensure_ascii=False, indent=2)El endpoint de embeddings soporta representaciones densas de texto e imagen simultáneamente, ideal para sistemas de búsqueda semántica híbrida.
import requests
import json
# Ejemplo sofisticado: Embedding de un documento con contexto de sistema
payload = {
"model": "Qwen/Qwen3-VL-Embedding-2B",
"messages": [
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,...(base64_doc)..."}},
{"type": "text", "text": "Representa este balance de situación para comparación financiera:"}
]
},
{"role": "assistant", "content": ""}
],
"encoding_format": "float",
"continue_final_message": True,
"add_special_tokens": True
}
response = requests.post(f"{GATEWAY_URL}/v1/embeddings", json=payload)
embedding = response.json()["data"][0]["embedding"]
print(f"Dimensiones del vector: {len(embedding)}")Interfaz totalmente compatible con OpenAI. Permite inyectar lógica de negocio mediante system prompts y controlar el flujo de razonamiento.
from openai import OpenAI
client = OpenAI(base_url=f"{GATEWAY_URL}/v1", api_key="emdi-key")
# Ejemplo sofisticado: Análisis de cláusulas con control de temperatura
stream = client.chat.completions.create(
model="Qwen/Qwen3.5-4B",
messages=[
{
"role": "system",
"content": (
"Eres un asistente conversacional diseñado para el "
"Hackathon de la Secretaría de Estado de Digitalización e Inteligencia Artificial (SEDIA) "
)
},
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": file_to_data_uri_sniffed('./sample_files/hackaton_poster.png')}},
{"type": "text", "text": "Describe brevemente esta imagen."}
]
}
],
temperature=0.2,
top_p=0.95,
max_tokens=1024,
stream=True,
)
for chunk in stream:
if chunk.choices[0].delta.content is not None:
print(chunk.choices[0].delta.content, end="", flush=True)Para facilitar la adopción inmediata de nuestra tecnología, EMDI incluye un Command Center (ubicado en la carpeta /client). Es importante destacar que este es un cliente de muestra (sample client) diseñado para ilustrar las capacidades del sistema.
El verdadero potencial reside en el Servidor de Inferencia EMDI, que actúa como una base sólida y flexible sobre la cual se pueden construir múltiples proyectos, automatizaciones e ideas personalizadas integrando su API en cualquier flujo de trabajo empresarial.
-
Ingesta con Inferencia Híbrida (Hybrid Chunking): A diferencia de los sistemas RAG tradicionales que solo procesan texto, nuestro cliente utiliza una estrategia de chunking visual-semántico. Por cada página del documento:
- Capa Visual: Se genera un embedding denso de la página completa como imagen (
Qwen3-VL-2B), capturando la disposición espacial, gráficos y estilo. - Capa Metadatos (OCR): Se asocia el texto estructurado (Markdown/Tablas) extraído por el LLM (
Qwen3.5-4B) como metadatos del vector. - Resultado: Las búsquedas son mucho más precisas al "entender" tanto el contenido textual como el contexto visual de la información.
- Capa Visual: Se genera un embedding denso de la página completa como imagen (
-
Exploración Documental Interactiva: El visor integrado permite navegar por los documentos con una capa de Bounding Boxes generada dinámicamente. Al hacer clic en cualquier región detectada (párrafos, tablas, imágenes), el sistema resalta el contenido extraído y viceversa, permitiendo una auditoría humana rápida de la IA.
-
Asistente de Razonamiento (Chat RAG): Interfaz de chat fluida compatible con streaming que permite interrogar a los documentos procesados. El asistente utiliza el conocimiento vectorial almacenado en Qdrant para responder con precisión quirúrgica, citando partes específicas del documento.
Si deseas probar la interfaz de usuario de forma local conectándola a un despliegue de EMDI (o a un entorno de desarrollo), sigue estos pasos:
- Prerrequisitos: Asegúrate de tener instalado Docker Desktop y que el motor de Docker esté en ejecución.
- Configurar Variables de Entorno:
Crea un archivo llamado
.envdentro de la carpetaclient/backend/con la URL de tu Gateway de EMDI:# client/backend/.env GATEWAY_URL=http://<IP_DE_TU_GATEWAY>
- Lanzar el Entorno:
Desde la raíz del proyecto, ejecuta el siguiente comando para levantar el backend (Python/FastAPI) y el frontend (Nginx) junto con la base de datos vectorial local (Qdrant):
docker compose -f client/docker-compose.yml up --build
- Acceso: Una vez levantado, abre tu navegador en
http://localhost:8001para empezar a procesar tus documentos.
En este vídeo se muestra cómo EMDI segmenta un PDF técnico, permitiendo la exploración visual de sus componentes mediante bounding boxes y la visualización instantánea del OCR estructurado.
Demostración del asistente conversacional interactuando con los documentos cargados, extrayendo información compleja y respondiendo en tiempo real utilizando el pipeline RAG de EMDI.

