Servidores

Desafíos y Requisitos de Ejecutar AI en el Borde para Sysadmins Linux

TL;DR — Lo que vas a tener al final

  • Tiempo de setup: 30 minutos a 2 horas dependiendo de la complejidad del entorno.
  • Requisitos: Acceso root, Linux moderno, conexión a internet para descargar dependencias.
  • Resultado: Servicio funcional y estable, documentado paso a paso.
  • Troubleshooting: Logs estándar en /var/log/, comandos de verificación al final de cada sección.

Por qué el borde rompe las reglas del data center

Cuando entrenas y sirves un modelo de IA en el data center, das por sentadas cosas que en el borde no existen: hardware homogéneo, versiones de CUDA conocidas, decenas de gigabytes de RAM, y un SLA de conectividad que permite mover los artefactos sin pensarlo. En el borde, ninguna de esas condiciones se cumple. La misma flota de una sola empresa puede mezclar NVIDIA Jetson Orin en arquitectura aarch64, gateways industriales con Intel Atom sin GPU discreta, servidores near-edge con GPUs AMD PCIe, y mini-PCs single-board con NPUs integradas. Si a eso le sumas que cada dispositivo ejecuta además la pila de telemetría, el watchdog, el bus de sensores y el agente de monitoring, lo que tienes es un ecosistema heterogéneo donde la inferencia compite por cada byte de memoria con el resto del sistema operativo.

Esa competencia por recursos no produce simplemente respuestas lentas, como ocurre en una nube saturada. En el borde, la presión sobre el pool compartido puede causar que el servicio de inferencia muera a mitad de un forward pass. Cuando el modelo se carga en la memoria compartida entre CPU y GPU (algo habitual en Jetson y otros SoC con memoria unificada), un pico de cualquier otro proceso puede provocar un OOM en el peor momento posible. Por eso, antes de hablar de estrategias de actualización, versionado o GitOps, hay una pregunta más fundamental que cualquier sysadmin debe responder: qué se necesita para que el modelo corra de forma fiable en un dispositivo con recursos ajustados. Este artículo recorre los seis retos que separan una prueba en Jetson de un despliegue que se mantiene sano en producción.

El primer reto: la heterogeneidad del hardware

El primer enemigo de un despliegue edge es la matriz de compatibilidades. Una Jetson Orin ejecuta modelos en formato TensorRT específico para su SKU de GPU; un gateway con CPU Intel corre esos mismos modelos bajo OpenVINO; un dispositivo AMD los compila con ROCm; y un SoC con NPU integrada requiere el SDK del fabricante. Si el modelo entrenado se distribuye como un único binario, te va a fallar en al menos uno de cada cuatro dispositivos. La forma correcta de resolver esto es producir una matriz de variantes en el pipeline CI/CD, etiquetadas con modelo + hardware target, igual que produces binarios por arquitectura en un release de Go o Rust.

Clase de hardware Runtime Formato modelo Memoria típica
NVIDIA Jetson Orin Nano/AGX JetPack + TensorRT .engine 8–32 GB unificada
Gateway industrial Intel Atom OpenVINO + oneAPI .xml + .bin 4–8 GB
Servidor AMD near-edge con GPU PCIe ROCm ONNX compilado 32–64 GB
SBC con NPU (RK3588, etc.) SDK del fabricante .rknn u otro 4–16 GB
Raspberry Pi 5 con Hailo-8 HailoRT .hef 8 GB + 8 GB accelerator

El segundo enemigo silencioso es el kernel driver y su ciclo de actualizaciones. Un parche de seguridad del kernel puede invalidar el módulo de la GPU y dejar al dispositivo sin acelerador después del siguiente reboot. Esto se manifiesta como fallos intermitentes que solo aparecen en producción, semanas después del despliegue inicial. La forma robusta de evitarlo es tratar el sistema operativo como inmutable y versionado, con bootc en RHEL/Red Hat Device Edge o con imágenes OCI firmadas que se promueven de manera atómica entre versiones. Así, un cambio del kernel se aplica junto con la versión del driver y el toolkit correspondiente, y siempre tienes un rollback limpio.

El segundo reto: elegir el tier de plataforma correcto

La plataforma que hospeda la inferencia determina cuánto overhead consumes, qué integraciones con tu flota están disponibles y qué tan rápido puedes operar un fallo. La decisión no es “Kubernetes o no Kubernetes”, sino qué nivel de capacidad operativa necesitas según el hardware disponible.

Tier Recursos mínimos Cuándo aplica Overhead aproximado
Podman + Quadlets (Device Edge) 1 core, 1.5 GB Far-edge con telemetría simple ~50 MB
MicroShift + KServe raw 2 cores, 2 GB PC industrial con API REST/gRPC ~300 MB
OpenShift SNO 4 cores, 16 GB Near-edge con KServe, Tekton, GPU Operator ~3 GB
OpenShift 3-node compact 4 cores, 16 GB por nodo HA completa en sitio edge ~3 GB por nodo
OpenShift 2-node + Arbiter 2 cores, 16 GB + 1 core, 8 GB HA con presupuesto ajustado ~2.5 GB por nodo

Para una Jetson Orin Nano en una planta, MicroShift te da manifests Kubernetes nativos, health probes, y la opción de correr inferencia como un Deployment con endpoints REST/gRPC estandarizados vía KServe, sin pagar el costo de un cluster completo. Si el dispositivo solo necesita correr un contenedor con un servidor ONNX ligero, Quadlets te da integración con systemd directa: el contenedor es una unidad .service, con restart automático, límites de memoria con MemoryMax, y logs en journald.

El siguiente fragmento muestra una unidad Quadlet mínima para servir un modelo ONNX en una Jetson Orin. Lo que hace es declarar el contenedor como servicio systemd, fijar el límite de memoria RAM en 2 GB (que es lo único que la Jetson puede dejar para inferencia sin tocar lo que necesita el sistema), y mapear el dispositivo GPU al contenedor:

# /etc/containers/systemd/inference.model.container
[Unit]
Description=ONNX inference server
After=local-fs.target
[Container]
Image=quay.io/onnxruntime/onnxruntime:latest
ContainerName=inference-model
Exec=--model /models/classifier.onnx --http-port 8001
Volume=/var/models:/models:ro
# Jetson Orin: expone Tegra GPU al contenedor
AddDevice=/dev/nvhost-ctrl
AddDevice=/dev/nvhost-gpu
AddDevice=/dev/nvmap
[Service]
MemoryMax=2G
MemoryHigh=1.8G
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target

Después de instalar el Quadlet, lo activas con un systemctl daemon-reload y un systemctl start inference.model. systemd lo trata como un servicio nativo. Si la inferencia muere por OOM, el journal te lo muestra limpio, sin parsear logs JSON de Kubernetes. Y si quieres un cambio de configuración, editas el archivo y haces systemctl restart; no hay un operador que tenga que reconciliar un CRD.

El tercer reto: la cadena de dependencias del acelerador

Un motor TensorRT no se compila una sola vez para siempre. Cuando actualizas TensorRT, CUDA, el driver NVIDIA o cambias el modelo, el engine compilado deja de ser compatible y el contenedor de inferencia falla al cargar el modelo. Lo mismo pasa con OpenVINO cuando Intel actualiza oneAPI, y con ROCm cuando AMD publica un nuevo driver. Este es el patrón de fallo más común en producción y también el más silencioso: la actualización del OS entra como parche CVE, todo parece OK, y dos semanas después el primer dispositivo que intenta cargar el modelo devuelve un error de versión incompatible.

La forma robusta de operar esta cadena es a dos niveles. En el nivel de dispositivo, pinneas todo el stack: kernel, módulo del driver, toolkit user-space, y runtime de inferencia, todos dentro de la misma imagen bootc o de la misma AMI. En el nivel de flota, necesitas un pipeline que valide la disponibilidad del acelerador y ejecute un smoke test de inferencia después de cada rollout, antes de que el cambio se propague al resto de los dispositivos.

Para OpenShift, esto se traduce en operadores declarativos versionados en Git. Node Feature Discovery detecta aceleradores disponibles y labelea los nodos; el GPU Operator de NVIDIA maneja el ciclo de vida del driver y toolkit como recurso versionado; y un pipeline Tekton ejecuta una inferencia mínima contra el modelo canary después de cada actualización. El siguiente manifiesto de NFD muestra el patrón: el cluster sabe qué nodos tienen GPU NVIDIA porque NFD aplica el label feature.node.kubernetes.io/pci-10de.present=true, y el GPU Operator reacciona instalando el driver y toolkit apropiados para esa clase de hardware.

# nodefeaturediscovery.nfd.kubernetes.io/v1
apiVersion: nfd.kubernetes.io/v1alpha1
kind: NodeFeatureDiscovery
metadata:
  name: nfd-instance
  namespace: openshift-nfd
spec:
  operand:
    image: registry.redhat.io/openshift4/ose-node-feature-discovery:latest
  workerConfig:
    configData: |
      core:
        labelPrefix: feature.node.kubernetes.io
        sleepInterval: 60s
      sources:
        - pci
        - cpu
        - memory
        - network
        - custom:
            - name: cuda
              matchFeatures:
                - feature: pci.device
                  matchExpressions: {vendorid: "0x10de"}

Para flotas que no son Kubernetes (RHEL puro en campo), Red Hat Edge Manager cumple el mismo rol: rollout controlado por etapas, visibilidad de qué dispositivo corre qué versión de imagen, y rollback si el smoke test falla. El principio es siempre el mismo: el pipeline CI/CD produce variantes por target de hardware, las promueve por etapas, y nunca toca un dispositivo sin haber validado el acelerador primero.

El cuarto reto: contención de memoria y latencia

En un dispositivo con memoria unificada entre CPU y GPU, un proceso que spikea en RAM le quita bytes directamente al motor de inferencia. El caso típico es un agente de telemetría que arranca cada 60 segundos y se lleva 200 MB que el modelo pensaba que tenía disponibles. La solución no es esperar a que pase, sino presupuestar la memoria de manera explícita como un recurso finito que se reparte por diseño y no por oportunidad.

En Device Edge con Podman, eso se hace con cgroups v2 vía systemd slice controls. Defines un slice para el motor de inferencia con un techo duro de RAM y otro slice para las aplicaciones con presupuesto propio, de modo que ninguno pueda invadir el espacio del otro. El siguiente manifiesto systemd muestra cómo aislar la inferencia en su propio slice con un techo de 3 GB:

# /etc/systemd/system/inference.slice
[Unit]
Description=Inference workload slice
DefaultDependencies=no
[Slice]
CPUWeight=200
MemoryMax=3G
MemoryHigh=2.8G
MemorySwapMax=0

Luego, en el Quadlet del motor de inferencia agregas Slice=inference.slice en la sección [Service]. Si en algún momento la inferencia intenta reclamar más de 3 GB, el kernel mata el proceso con un OOM limpio en lugar de empezar a swapear a disco y degradar el resto del sistema. Esto es contención por diseño, no por accidente.

Para aplicaciones con requisitos de determinismo (un safety interlock, un controlador de robot, una línea de manufactura), el ajuste va más allá. Necesitas el kernel PREEMPT_RT, que reemplaza el scheduler estándar por uno con latencia acotada. Y necesitas aislar los cores donde corre la inferencia, fijar la afinidad de IRQ, y reservar hugepages para los pesos del modelo. El siguiente fragmento de GRUB muestra el patrón para un dispositivo industrial que corre inferencia en cores 2 y 3:

# /etc/default/grub — variables para kernel RT
GRUB_CMDLINE_LINUX="
  isolcpus=2,3 
  nohz_full=2,3 
  rcu_nocbs=2,3 
  intel_pstate=disable 
  processor.ignore_ppc=1 
  transparent_hugepage=never 
  default_hugepagesz=1G hugepagesz=1G hugepages=4"

Después de aplicar el cambio con grub2-mkconfig -o /boot/grub2/grub.cfg y reiniciar, los cores 2 y 3 quedan libres de tareas del kernel y de interrupciones. El motor de inferencia corre ahí con taskset -c 2,3, y las hugepages de 1 GB eliminan los TLB misses que degradan la latencia cuando el modelo tiene más de 4 GB de pesos. Es trabajo extra, pero la diferencia entre una inferencia que de vez en cuando toma 48 ms y una que consistentemente termina en 2 ms es la diferencia entre un prototipo vistoso y un sistema apto para producción industrial.

Para confirmar que el pinning quedó bien antes de pasar a producción, este script verifica que ningún proceso fuera del motor de inferencia está usando los cores aislados. Si hay migración, lo ves antes de que aparezca en el hot path de latencia:

#!/usr/bin/env bash
# /usr/local/bin/check-cpu-pinning.sh
set -euo pipefail
ISOLATED="2,3"
echo "== procesos en cores aislados =="
ps -eo pid,psr,comm | awk -v cpus="$ISOLATED" '
  BEGIN { n=split(cpus, a, ","); for (i=1;i<=n;i++) iso[a[i]]=1 }
  { for (i in iso) if ($2==iso[i] && $3!="ps" && $3!="awk" && $3!="check-cpu-pinning.sh") print }'
echo "== hugepages disponibles =="
grep -E '^HugePages_(Free|Total)' /proc/meminfo
echo "== PID del motor de inferencia =="
PID=$(systemctl show inference.model -p MainPID --value)
echo "PID=$PID"
taskset -cp "$PID" || true

El quinto reto: cómo conectan los sensores con el modelo

El patrón de mensajería entre el proceso de captura y el servicio de inferencia no es cosmético: define cuánto toleras que una actualización del modelo te rompa el pipeline de datos. Antes de elegir broker o transporte, valida que el cliente cabe en el dispositivo con un mosquitto_sub simple contra un broker local:

# Suscribirse al topic de inferencia en un broker Mosquitto local
mosquitto_sub -h 127.0.0.1 -p 1883 
  -t 'edge/inference/v1/+/output' 
  -u inference -P 'cambia-esto' 
  --qos 1 -v
# Publicar un frame de prueba y ver el resultado
mosquitto_pub -h 127.0.0.1 -p 1883 
  -t 'edge/inference/v1/cam01/input' 
  -f /tmp/frame.jpg --qos 1

Si esa suscripción simple cabe en 50 MB de RSS en una Jetson Orin Nano, MQTT es viable para esa flota. Si el cliente se lleva 300 MB, conviene pensar en ZeroMQ o DDS. Hay tres arquitecturas dominantes en el borde, y cada una tiene consecuencias de mantenimiento distintas.

Patrón Acoplamiento Implicación MLOps Cuándo usarlo
Request-response (REST/gRPC) Alto Cambiar el modelo rompe al caller si cambia output o latencia Prototipos, baja escala
Pub/sub con broker (MQTT, Kafka) Bajo El cambio del modelo solo afecta a consumidores del topic de salida Flotas grandes, replay para validación
Streaming continuo (zero-copy) Alto pero rápido El cambio del modelo obliga a actualizar el pipeline de captura Video analítica de alta tasa

Para dispositivos constrained, MQTT es el patrón más común: footprint pequeño, QoS configurable, y librerías cliente para casi cualquier lenguaje. La limitación es que MQTT entrega y descarta; no hay replay. Si necesitas validar un modelo candidato contra tráfico de producción reciente, MQTT no te da ese historial. Kafka sí te lo da, pero su JVM y ZooKeeper/KRaft consumen recursos que en una Jetson son significativos. Para los casos intermedios, ZeroMQ no necesita broker: las aplicaciones se conectan directamente vía sockets con patrones pub/sub, request-reply o pipeline. El costo operativo es menor, pero también pierdes retención, discovery automático y replay. Y si el sistema ya incluye ROS2, DDS es el middleware por defecto, con QoS policies que cubren reliability, durability y deadline enforcement, pero su huella se acerca más a Kafka que a MQTT.

El sexto reto: versionar el modelo como requisito MLOps

Cuando haces un rollout canary o blue-green de un modelo en una flota heterogénea, un subconjunto de dispositivos corre la versión nueva y el resto sigue con la anterior. Ambos grupos escriben a los mismos sistemas downstream al mismo tiempo. Si cada inferencia no lleva un tag con su versión de modelo, no puedes distinguir qué salió del candidato y qué salió de la versión anterior. La métrica agregada mezcla ambas, y los shifts de accuracy, latencia o distribución de salida quedan invisibles.

El tag es un identificador con el nombre del modelo, la versión, y opcionalmente el run ID o experiment ID, embebido en el punto donde se produce la inferencia. Dónde viaja depende del patrón de comunicación: en REST/gRPC va en un campo de metadata en el response; en pub/sub va en los headers del broker; en streaming continuo con zero-copy tiene que ir en un canal de metadata separado porque el buffer del tensor solo lleva datos del tensor.

El serving framework por defecto no hace esto. Triton, OpenVINO Model Server y TorchServe devuelven outputs del modelo; no saben a qué grupo canary pertenece el dispositivo ni a qué experimento corresponde la inferencia. Tienes que inyectar el contexto en el wrapper que llama al servidor, e incluir el tag en cada respuesta que el servicio genera. Es trabajo extra del lado del orquestador, pero sin él, tu audit chain se rompe la primera vez que un regulador te pregunte qué versión del modelo tomó la decisión que aparece en el log.

Conclusión: lo que tu homelab puede y no puede hacer

Para un homelab, todo esto es accesible con un presupuesto razonable: una Jetson Orin Nano de 8 GB cuesta menos que una GPU de gama baja, y cubre los escenarios típicos de inferencia visual con batch size 1. MicroShift o incluso K3s en una Raspberry Pi 5 con un acelerador Hailo-8 te da un despliegue Kubernetes-native por unos 200 USD. Pero hay un techo claro: si tu modelo es un LLM de 7B parámetros con cuantización int4, no lo vas a servir de forma responsiva en una Jetson Orin Nano; vas a terminar con latencias de varios segundos por token. Para eso, la opción correcta sigue siendo un servidor con 64 GB de RAM corriendo llama.cpp o vLLM, y ese dispositivo vive en el near-edge, no en el far-edge.

Lo que cambia con esta nueva camada de complejidad es el tipo de preguntas que un sysadmin tiene que hacerse antes de comprar hardware. Ya no basta con preguntar "¿cuánta RAM tiene?"; ahora hay que preguntar "¿el módulo de la GPU está pinned en la imagen bootc?", "¿el sistema operativo puede actualizar el kernel sin invalidar el driver?", "¿hay un slice con techo de memoria para el motor de inferencia?", y "¿cada respuesta lleva el tag de versión del modelo?". Cuando esas cuatro preguntas tienen respuesta, el despliegue deja de ser un prototipo y empieza a ser algo que se mantiene solo en producción.

Preguntas frecuentes

¿Por qué Desafíos y Requisitos de Ejecutar AI en el Borde para Sysadmins Linux es relevante para sysadmins Linux?

Forma parte del flujo diario de administración. Los conceptos explicados se aplican directamente a servidores en producción y resuelven problemas reales que enfrentan sysadmins hispanohablantes.

¿Qué necesito para empezar con Desafíos y Requisitos de Ejecutar?

Distribución Linux actualizada (Debian, Ubuntu, RHEL, Fedora o Arch), acceso root, y conexión a internet. Para producción: entorno de staging separado antes de aplicar cambios.

¿Cuáles son los errores más comunes con Desafíos y Requisitos de Ejecutar?

Los tres más frecuentes: (1) saltar la lectura de logs, (2) aplicar cambios directos en producción sin probar, (3) olvidar permisos y ownership. La documentación y un plan de rollback siempre.

¿Cuándo usar Desafíos y Requisitos de Ejecutar y cuándo una alternativa?

Esta solución es ideal para el caso descrito. Alternativas como Docker, Ansible o configuración manual pueden ser mejores según contexto: portabilidad, escala y reproducibilidad son los factores clave.

¿Cómo migro desde una configuración previa a Desafíos y Requisitos de Ejecutar?

Documentar el estado actual, probar la nueva configuración en staging, validar funcionalmente, y aplicar en producción con rollback plan listo. Los posts de SoyLinux incluyen troubleshooting para los problemas más comunes.

Lee también

Alfonso Moreno | Sysadmin Senior

Soy Alfonso, sysadmin senior con RHCE y más de 10 años administrando sistemas Linux para empresas y proyectos personales. Me especializo en infraestructura, redes, virtualización, monitoring, automation y self-hosting. Sobre SoyLinux.com SoyLinux.com es un blog técnico en español sobre Linux, software libre y self-hosting. Lo mantengo en mi tiempo libre. Áreas de expertise - Linux server administration: RHEL, CentOS, Rocky, Alma, Fedora, Debian, Ubuntu - Network services: Postfix, BIND, Samba, NFS, Squid - Virtualización y containers: KVM, Proxmox, Docker, Podman - Monitoring: Prometheus, Grafana, Zabbix - Automation: Ansible, Bash scripting - Self-hosting: Vaultwarden, Nextcloud, Jellyfin, Immich, Homelab Contacto - Email: [email protected]

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *