La puerta de la cocina que el maître no miró: NUMA de red, Cilium eBPF y DRANET, la cuarta pata del pinning
Cuarta entrega —coda— de “por debajo del motor”. La serie cerró con tres patas de la localidad: el cable entre GPUs, el host a mano y la orquestación declarativa del kubelet. Pero el maître del último post sentaba al grupo mirando CPU, memoria y GPU, y nunca preguntó por qué puerta entran los platos. Esa puerta es la NIC. Aquí está la cuarta pata.
TL;DR
El Topology Manager admite un pod en single-numa-node si sus CPUs, su memoria y su GPU caben en el mismo NUMA node. La NIC no entra en esa cuenta: el kubelet no tiene un Hint Provider para la tarjeta de red. En un nodo de inferencia con red a 200/400 Gb/s —el caso de disaggregated serving, donde el KV-cache viaja por RDMA entre el pool de prefill y el de decode— una NIC en el socket equivocado hace que cada paquete cruce la UPI/QPI, exactamente el “NUMA remoto” que la serie combate por el lado de cómputo, pero por la puerta de la red. Y hay un segundo frente: el softirq (NET_RX) que procesa el datapath corre en la CPU que atiende la IRQ de la NIC; si esa CPU es uno de los cores que isolcpus/reserved-cpus dieron en exclusiva a vLLM, el softirq le roba ciclos y mete jitter en la cola de p99. Cilium eBPF sustituye dos piezas de RKE2 —kube-proxy (por load balancing eBPF/XDP) y el CNI por defecto Canal (por datapath nativo)— y su propia guía de tuning te manda matar irqbalance y fijar las IRQ de la NIC: una cuarta lista que alinear junto a isolcpus y reserved-cpus. El estado del arte 2026 cierra el hueco por arriba: netkit (kernel ≥6.8, overhead de namespace a cero), BIG TCP (super-paquetes de 192k para 100Gb/s+), host-routing (bypass de iptables), y sobre todo DRA/DRANET, el driver de red que por fin co-programa GPU y NIC NUMA-locales en el mismo PCIe root, habilitando GPUDirect RDMA con +59,6% de bus bandwidth en all_gather y +58,1% en all_reduce. Sobre un cluster genérico RKE2 con nodos 4×H100 SXM.
Dónde estás: el plano de red que la trilogía no abrió
La analogía: la puerta por la que entran los platos
Vuelve al restaurante del post anterior. El maître —el Topology Manager— sentó al grupo de ocho en una sola mesa (un NUMA node) porque cabían los comensales (CPUs), los cubiertos (memoria) y la botella reservada (la GPU). Mesa perfecta. Pero el maître nunca miró dónde está el pase de cocina: la puerta por la que entra y sale cada plato.
Esa puerta es la NIC. Por ahí entra el prompt, salen los tokens, y —en disaggregated serving— circula el KV-cache que el pool de prefill manda al de decode. Si la mesa está en la sala de la izquierda (socket 0) pero el pase de cocina está en la de la derecha (socket 1), cada plato cruza el restaurante entero (la UPI/QPI), una y otra vez, por mucho que la mesa esté impecablemente puesta. El comensal no nota la mesa perfecta: nota que el plato llega tarde y frío.
Y hay un detalle más fino: el camarero que cruza la sala con los platos (el softirq que procesa los paquetes) es uno de los comensales sentados. Si el maître le asignó una silla en exclusiva para comer tranquilo (un core aislado por isolcpus para vLLM) pero el restaurante lo pone también a hacer de camarero de la puerta lejana, ese comensal no come: se pasa la cena cruzando la sala. El jitter aparece justo donde creías haber comprado calma.
La trilogía niveló tres patas de la mesa: el cable, el host y la orquestación. La cuarta —por qué puerta entran los platos y quién los lleva— no la nivela ningún manager del kubelet. Hasta 2026.
El hueco: por qué el Topology Manager no mira la NIC
El mecanismo del post 3 es un coordinador (Topology Manager) que consulta a tres Hint Providers: CPU Manager, Memory Manager y Device Manager (el plugin de GPU). Cada uno dice en qué NUMA node puede satisfacer su parte; el coordinador calcula la intersección y admite o rechaza.
El problema es de censo: la NIC clásica no es un “device” del Device Manager. Una tarjeta Ethernet/InfiniBand estándar la gestiona el CNI y el kernel, no se pide en el resources: del pod como nvidia.com/gpu, y por tanto no emite hint NUMA. El Topology Manager alinea CPU+memoria+GPU y deja la NIC donde el hardware la puso, que puede ser el otro socket. El maître tiene tres ayudantes y le falta el cuarto: el que sabe por qué puerta entran los platos.
Esto no importaba cuando la red de un nodo eran 10/25 Gb/s y el cuello de botella estaba en otro sitio. Importa ahora, con dos cargas que saturan la red del nodo:
- Disaggregated serving. El KV-cache que viaja entre el pool de prefill y el de decode se mueve por RDMA. Son transferencias grandes, sensibles a latencia y ancho de banda, que en multinodo salen por la NIC.
- Colectivos NCCL multinodo. Cuando el tensor/pipeline parallel cruza el límite del nodo, los
all-reduce/all-gatherya no van por NVLink sino por GPUDirect RDMA sobre la NIC.
En ambos, dónde está la NIC respecto a la GPU y a los cores del pod decide el rendimiento. Y eso el kubelet, por sí solo, no lo coordina.
El datapath de red bajo NUMA: IRQ, softirq y DMA
Para ver por qué la localidad de la NIC pesa, hay que mirar el camino de un paquete que llega:
Tres hechos del kernel que la analogía comprime:
- La IRQ tiene afinidad. Cada cola de la NIC dispara una interrupción que el kernel atiende en una CPU concreta (
/proc/irq/<n>/smp_affinity). El procesamiento pesado se difiere a un softirq (NET_RX/NET_TX), que corre en esa misma CPU. Siirqbalanceestá suelto, las va migrando de forma no determinista —veneno para el p99. - El softirq compite con el pod. Si la IRQ cae en un core que
isolcpusreservó para vLLM, elNET_RXde esa cola le roba ciclos al modelo. La señal en/proc/softirqs: una columna deNET_RXque se dispara en una sola CPU. Es el mismo jitter del post 2, entrando por la red. - El DMA tiene origen NUMA. La NIC escribe el paquete por DMA en la RAM del socket de su PCIe root. Si el consumidor (el hilo del pod) está en el otro socket, lee cruzando la UPI/QPI. RFS (Receive Flow Steering) intenta llevar el procesamiento a la CPU del consumidor, pero no puede teletransportar la NIC al otro socket.
Un número, con su salvedad
Pongamos un nodo de 2 sockets, NIC de 400 Gb/s = 50 GB/s en el PCIe root del socket 0, y un pod de decode pinneado al socket 1. Si la NIC satura, esos ~50 GB/s de tráfico de recepción cruzan la UPI hacia el socket 1. Un enlace UPI 2.0 ronda los ~20–40 GB/s por dirección y enlace según generación; aun con varios enlaces, 50 GB/s de tráfico de red a contracorriente se comen una fracción nada despreciable del presupuesto inter-socket —el mismo presupuesto por el que ya compiten los accesos remotos a memoria del pod y, si hay multinodo, el KV-cache de la disaggregation. No doy un “X% de degradación” cerrado porque depende de generación de CPU, número de enlaces UPI, MTU y patrón de tráfico; sin esa metodología, cualquier cifra exacta es marketing.
Lo que sí está medido con metodología pública es el efecto agregado de alinear GPU y NIC: el proyecto DRANET reporta +59,6% de bus bandwidth en all_gather y +58,1% en all_reduce (colectivos NCCL) cuando la NIC asignada es NUMA-local a la GPU frente a no serlo. Esa es la magnitud del hueco que el Topology Manager dejaba abierto.
Qué sustituye Cilium eBPF de RKE2 (y por qué toca esta historia)
RKE2 trae por defecto Canal (Flannel + Calico) como CNI y kube-proxy (reglas iptables/IPVS) para el balanceo de Services. Cambiar a Cilium (cni: cilium en /etc/rancher/rke2/config.yaml) sustituye ambas piezas por un datapath eBPF:
| Pieza de RKE2 | Qué hace | Qué pone Cilium eBPF |
|---|---|---|
kube-proxy (iptables/IPVS) | balanceo de Services | LB en eBPF; con kubeProxyReplacement=true, y aceleración en XDP (capa de driver) |
| Canal (Flannel+Calico) | overlay VXLAN + NetworkPolicy | datapath nativo (routingMode=native), NetworkPolicy L3/L4 y L7 en eBPF |
| veth por pod | par de interfaces del namespace | netkit (kernel ≥6.8): overhead de namespace ~0 |
| recorrido iptables del host | hooks netfilter | host-routing eBPF: bypass de iptables y de la parte alta del stack |
Hasta aquí es networking puro y no toca los resource managers del kubelet: Cilium no asigna CPUs exclusivas ni emite hints NUMA de cómputo. Los diez knobs del post 3 siguen idénticos pongas Canal o Cilium.
Pero Cilium sí entra en la cuarta pata por dos puertas. La primera: su propia guía de tuning recomienda, literalmente, “matar irqbalance y fijar las IRQ de la NIC a CPUs específicas para máximo aislamiento de la carga”, además del perfil tuned network-latency, el governor performance y CONFIG_PREEMPT_NONE. Es decir: el datapath eBPF rinde de verdad solo si coordinas la afinidad de IRQ —y esa afinidad tiene que apuntar a los cores housekeeping (reserved-cpus), nunca a los aislados. Aparece así una cuarta lista que mantener coherente con isolcpus y reserved-cpus:
isolcpus = 2-31,34-63 # cores exclusivos para vLLM (host, post 2)
reserved-cpus = 0-1,32-33 # housekeeping del kubelet (post 3)
IRQ affinity = 0-1,32-33 # NIC IRQs → SOLO housekeeping (este post)
# nunca 2-31: ahí el softirq robaría al modelo
La segunda puerta: netkit + host-routing + BIG TCP reducen cuántas veces el paquete cruza el stack y el namespace, lo que amortigua (no elimina) el coste del cruce NUMA. BIG TCP arma super-paquetes de hasta 192k (frente a 64k) para 100Gb/s+; menos travesías del stack es menos trabajo de softirq en el core, y por tanto menos presión sobre el presupuesto inter-socket. Es la analogía del continuous batching aplicada al stack de red: amortizar un coste fijo sobre lotes mayores.
Perfil de rendimiento de Cilium (estado 1.19, kernel ≥6.8)
# Helm, perfil de rendimiento recomendado (resumen de la tuning guide)
helm install cilium cilium/cilium --version 1.19.4 \
--namespace kube-system \
--set routingMode=native \
--set bpf.datapathMode=netkit \ # overhead de namespace ~0 (kernel >=6.8)
--set bpf.masquerade=true \
--set kubeProxyReplacement=true \ # sustituye kube-proxy de RKE2
--set enableIPv4BIGTCP=true \ # super-paquetes 192k (NIC mlx5/ice)
--set enableIPv6BIGTCP=true \
--set bpf.distributedLRU.enabled=true \# mapas BPF per-CPU: menos contención de spinlock
--set bpf.mapDynamicSizeRatio=0.08 \
--set bpfClockProbe=true
# Verificación dentro de un pod de Cilium:
cilium status --verbose | grep -E "Device Mode|Host Routing|BIG TCP|XDP"
# Device Mode: netkit · Host Routing: BPF · IPv4 BIG TCP: enabled · XDP Acceleration: Native
Salvedad escéptica: netkit y BIG TCP son beta y exigen kernel ≥6.8 y NICs concretas (mlx4/mlx5/ice). No son in-place: cambian fundamentos del datapath y obligan a reiniciar pods o, mejor, a aplicarlos por per-node config solo en nodos nuevos. Para un cluster ENS en producción, eso es una ventana de mantenimiento, no un helm upgrade a ciegas.
El estado del arte 2026: DRA y DRANET, el maître que por fin mira la puerta
Lo que cierra el hueco de raíz no es Cilium —es el mecanismo de admisión que el kubelet no tenía para la NIC: Dynamic Resource Allocation (DRA), beta desde Kubernetes 1.32 y con avances en cada release hasta la 1.36 (mayo 2026). DRA generaliza el modelo de “devices” más allá de la GPU: un driver descubre el hardware, publica ResourceSlices con sus atributos —incluida la topología NUMA y el PCIe root— y el scheduler resuelve ResourceClaims que pueden exigir afinidad entre dispositivos.
DRANET (proyecto kubernetes-sigs) es el driver DRA de red. Descubre las NICs (incluidas las RDMA-capaces), las anuncia como ResourceSlices, y vía NRI las inyecta en el namespace del pod —compatible con el CNI que ya tengas, Cilium incluido. La pieza clave para esta historia: combinado con el NVIDIA GPU DRA driver, permite co-programar GPU y NIC que comparten PCIe root (la relación que NVIDIA llama NODE), que es justo la condición de GPUDirect RDMA. El maître por fin tiene su cuarto ayudante: "¿hay una NIC NUMA-local a esta GPU?".
El ResourceClaimTemplate usa selectores CEL para pedir exactamente esa alineación:
# Pedir una NIC RDMA NUMA-local a la GPU asignada (esquema ilustrativo DRANET/DRA)
apiVersion: resource.k8s.io/v1beta1
kind: ResourceClaimTemplate
metadata:
name: gpu-nic-numa-aligned
spec:
spec:
devices:
requests:
- name: rdma-nic
deviceClassName: dra.net # NICs publicadas por DRANET
constraints:
- requests: ["rdma-nic"]
matchAttribute: "dra.net/pcieRoot" # misma raíz PCIe que la GPU
# → habilita GPUDirect RDMA sobre camino NUMA-local
Por qué importa para inferencia, no para “AI training” abstracto: en disaggregated serving, RDMA es lo que mueve el KV-cache entre el pool de prefill y el de decode con la latencia que el TTFT exige; y en multinodo, GPUDirect RDMA sustituye al NVLink como medio del colectivo. Alinear GPU+NIC en el mismo PCIe root es lo que convierte un “RDMA que funciona” en un “RDMA que rinde” —los +60% de bus bandwidth de DRANET.
Estado y salvedades: DRA es beta (gates a habilitar a mano), DRANET es joven (proyecto SIG, en evolución) y la oferta gestionada existe sobre todo en cloud (GKE managed DRANET en preview, AKS para RDMA). Para on-premise ENS es camino, no producto cerrado: el valor hoy es entender que la cuarta pata ya tiene mecanismo estándar OSS, y empezar a pilotarlo en un nodo de laboratorio, no meterlo en producción crítica este trimestre.
Cómo se conecta con el resto del stack
Con el host (post 2). La afinidad de IRQ de la NIC es una tercera lista que casar con isolcpus y reserved-cpus. Las IRQ van a housekeeping; los cores aislados, intactos. Descoordinarlas mete por la puerta de la red el jitter que isolcpus echó por la de cómputo.
Con la orquestación (post 3). DRA es la extensión natural del Topology Manager: el mismo principio de “admite solo si encaja en el NUMA node” llevado a la NIC. Donde el Device Manager dejaba la red fuera del censo, DRANET la mete.
Con el interconnect (post 1). Dentro del nodo manda NVLink; al cruzar el límite del nodo, GPUDirect RDMA sobre la NIC es el medio del colectivo. La política NUMA del kubelet garantiza que GPU y CPUs comparten socket; DRANET añade que la NIC también —y solo entonces el RDMA va por el camino corto.
Con disaggregated serving. El KV-cache prefill→decode es el tráfico que más castiga una NIC mal ubicada. La cuarta pata es lo que hace que separar prefill y decode no se pague en latencia de transferencia.
Con capacity planning. El dimensionado gana una dimensión: no basta con “GPUs por nodo y cores por NUMA node”; hay que contar cuántas NICs NUMA-locales a GPU tiene el chasis. Un nodo con 4 GPUs y una sola NIC en el socket 0 tiene dos GPUs “lejos de la puerta”.
Con la observabilidad. Lo que confirma que la cuarta pata está bien puesta no es un dashboard de aplicación: es /proc/softirqs (¿NET_RX concentrado en housekeeping?), nvidia-smi topo -m (¿relación NODE/PHB GPU↔NIC?) y los contadores de la NIC. Encaja con la observabilidad GPU con DCGM: la GPU “al 60% sin razón” puede ser el host esperando paquetes que cruzan el socket.
Trampas y cosas que no son lo que parecen
Creer que cambiar a Cilium “ya optimiza la red”. Cilium eBPF sustituye a kube-proxy y Canal y rinde mejor de serie, pero el despliegue por defecto prioriza compatibilidad, no rendimiento. Sin irqbalance desactivado, sin IRQ fijadas a housekeeping y sin netkit/host-routing, dejas la mayor parte de la mejora en la mesa. La doc de Cilium lo dice; mucha gente no lee la tuning guide.
Fijar las IRQ de la NIC a cores aislados. El error simétrico del knob 6 del post 3: si pones la afinidad de IRQ sobre isolcpus, el softirq NET_RX le roba ciclos a vLLM justo en los cores que aislaste para que nadie lo molestara. Las IRQ van a reserved-cpus, siempre.
Asumir que el Topology Manager ya alinea la NIC. No lo hace: la NIC clásica no es un Hint Provider. Si necesitas localidad NIC↔GPU, hoy el mecanismo es DRA/DRANET, no una política del kubelet. Esperar a que single-numa-node lo resuelva es esperar a algo que no está en su diseño.
Meter DRA/DRANET en producción ENS este trimestre. Es beta y joven. El movimiento sensato es pilotarlo en un nodo de laboratorio, medir all_reduce/all_gather con y sin alineación, y decidir con datos. La cifra del +60% es de un entorno concreto; reprodúcela en el tuyo antes de prometerla.
BIG TCP / netkit sin leer los requisitos. Kernel ≥6.8, NICs mlx4/mlx5/ice, sin túnel ni cifrado para BIG TCP, y nada de in-place: obliga a reiniciar pods o a per-node config. En un cluster con IPsec o con NICs no soportadas, parte de esto no aplica. Verifica cilium status --verbose antes de dar por hecho que está activo.
Confundir el datapath eBPF (kernel) con el agente Cilium (pod). cilium-agent es un DaemonSet Burstable que debe vivir en housekeeping (lo cubre system-reserved). Pero el procesamiento del datapath corre en softirq, gobernado por la afinidad de IRQ del host, no por reserved-cpus. Son dos cosas distintas; pinear bien el pod no pinea el softirq.
Conclusión
La serie “por debajo del motor” perseguía una idea: el rendimiento que parece un problema del motor (vLLM lento) o del modelo (cuantización) es, demasiadas veces, un problema de localidad en una capa más baja. La trilogía cubrió tres: el cable (NVLink no usado), el host (NUMA remoto, jitter) y la orquestación (pinning que no ocurrió). Faltaba la cuarta: la red. El Topology Manager sienta al pod en una mesa NUMA perfecta y nunca pregunta por qué puerta entran los platos ni quién los lleva. En un nodo a 25 Gb/s daba igual; en uno a 400 Gb/s con KV-cache cruzando por RDMA, esa puerta decide el TTFT y el ancho de banda del colectivo. Cilium eBPF sustituye kube-proxy y Canal por un datapath que rinde —si coordinas la afinidad de IRQ con isolcpus/reserved-cpus, una cuarta lista que alinear—, y DRA/DRANET aporta por fin el censo que faltaba: co-programar GPU y NIC NUMA-locales en el mismo PCIe root, con la magnitud de mejora (+60% de bus bandwidth NCCL) que mide lo grande que era el hueco. Bajar de nivel no es esnobismo: es que la causa raíz vivía, una vez más, una capa por debajo de donde mira el dashboard.
Ver también
- El maître que solo te sienta si cabéis en una mesa: resource managers en RKE2 — el post 3, padre directo de éste: el Topology Manager pinnea CPU+memoria+GPU pero no la NIC; aquí se abre esa cuarta pata.
- NUMA, hugepages y aislamiento de CPU — el post 2; la afinidad de IRQ de la NIC es una tercera lista que casar con
isolcpusyreserved-cpus, y el softirqNET_RXes el mismo jitter entrando por la red. - NVLink, NVSwitch y NCCL — el post 1; al cruzar el nodo, GPUDirect RDMA sobre la NIC sustituye a NVLink, y DRANET es lo que garantiza que ese RDMA va por el camino NUMA-local.
- Disaggregated serving: prefill y decode separados — el caso que más castiga una NIC mal ubicada: el KV-cache prefill→decode viaja por RDMA y paga cada cruce de socket.
- El stack de inferencia LLM on-premise en siete capas — el edificio completo; la red es el plano que sostiene la inferencia multinodo.
- Autoescalado de LLMs en Kubernetes con KEDA — cada réplica nueva no solo pasa por la admisión NUMA del kubelet; con DRA, también por la del
ResourceClaimde NIC. - Capacity planning de inferencia on-premise — el sizing gana una dimensión: cuántas NICs NUMA-locales a GPU tiene el chasis, no solo cuántas GPUs.
- Entornos mixtos NVIDIA + Intel — la afinidad NUMA NIC↔acelerador se complica cuando el nodo mezcla GPUs, aceleradores y NICs heterogéneas.
- Observabilidad GPU con DCGM — cómo confirmar, métrica en mano, que la “GPU al 60%” no es el host esperando paquetes cruzando el socket.
Referencias
- Cilium, Tuning Guide (netkit, host-routing, BIG TCP, XDP, fijar IRQ y matar irqbalance): https://docs.cilium.io/en/stable/operations/performance/tuning/.
- Cilium 1.19 (febrero 2026), Cilium at Ten Years — endurecimiento de cifrado, políticas y observabilidad: https://www.infoq.com/news/2026/02/cilium-119/.
- Isovalent, Cilium 1.18 (IPv6, encrypted overlay, ingress bandwidth, policy perf): https://isovalent.com/blog/post/cilium-1-18/.
- RKE2, Network Options (Canal por defecto; Cilium con kube-proxy replacement): https://docs.rke2.io/networking/basic_network_options.
- Kubernetes, Dynamic Resource Allocation: https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/.
- Kubernetes blog, v1.36: More Drivers, New Features, and the Next Era of DRA (mayo 2026): https://kubernetes.io/blog/2026/05/07/kubernetes-v1-36-dra-136-updates/.
- DRANET (kubernetes-sigs), driver DRA de red y paper The Kubernetes Network Driver Model (+59,6% all_gather / +58,1% all_reduce): https://github.com/kubernetes-sigs/dranet.
- AKS Engineering, Optimizing RDMA performance for AI workloads on AKS with DRANET (abril 2026): https://blog.aks.azure.com/2026/04/01/dranet-rdma-optimization-for-ai-on-aks.
- Linux network tuning — IRQ affinity, RSS/RPS/RFS y softirq NUMA: https://andreaskaris.github.io/blog/networking/rss-irq-affinity-and-rps/.