<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Gpudirect on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/gpudirect/</link><description>Recent content in Gpudirect on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Mon, 08 Jun 2026 06:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/gpudirect/index.xml" rel="self" type="application/rss+xml"/><item><title>Los pasillos y el guardia de seguridad: topología PCIe, GPUDirect P2P y ACS</title><link>https://blog.lo0.es/posts/pcie-topology-gpudirect-p2p-acs/</link><pubDate>Mon, 08 Jun 2026 06:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/pcie-topology-gpudirect-p2p-acs/</guid><description>&lt;blockquote>
&lt;p>Sigue la serie &lt;em>por debajo del motor&lt;/em>. El &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">post de NVLink y NCCL&lt;/a> explicó la &lt;em>mesa compartida&lt;/em> por la que las GPUs se pasan datos a 450 GB/s. Pero esa mesa solo conecta GPUs entre sí. Todo lo demás —disco, red, el host— viaja por &lt;strong>otro bus&lt;/strong>, el PCIe, y por sus pasillos. El &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">cold start&lt;/a> ya rozó esto con GPUDirect Storage; este post abre el plano completo de los pasillos y el guardia que los vigila.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>En un nodo de &lt;strong>4×H100 SXM&lt;/strong>, las GPUs se hablan por &lt;strong>NVLink&lt;/strong> (450 GB/s por sentido, ~7× el PCIe), y para el all-reduce del &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">tensor parallel&lt;/a> ese es el camino. Pero el &lt;strong>PCIe&lt;/strong> no desaparece: es por donde entra todo lo demás. Los pesos suben del &lt;strong>NVMe&lt;/strong> por PCIe (el &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">cold start&lt;/a>), los datos de otro nodo llegan por la &lt;strong>NIC&lt;/strong> por PCIe (RDMA), y un KV que se mueve entre nodos viaja por PCIe. &lt;strong>GPUDirect&lt;/strong> es la familia que deja que esos bytes vayan &lt;strong>directos del dispositivo a la HBM&lt;/strong> sin rebotar por la RAM del host: &lt;strong>P2P&lt;/strong> (GPU↔GPU), &lt;strong>RDMA&lt;/strong> (GPU↔NIC) y &lt;strong>Storage&lt;/strong> (GPU↔NVMe). El obstáculo es un guardia llamado &lt;strong>ACS&lt;/strong> (Access Control Services): una feature de seguridad del PCIe que por defecto obliga al tráfico &lt;em>peer-to-peer&lt;/em> a &lt;strong>subir hasta el root complex&lt;/strong> para inspección, lo que destruye el camino directo y mete un rodeo por la CPU. El &lt;strong>IOMMU&lt;/strong> (VT-d) hace algo parecido si no está en modo &lt;em>passthrough&lt;/em>. Desactivarlos da rendimiento; mantenerlos da aislamiento y virtualización —y esa es una decisión real en un entorno &lt;strong>ENS&lt;/strong>—. Este post explica la topología (&lt;code>nvidia-smi topo -m&lt;/code>), GPUDirect, por qué ACS e IOMMU rompen el P2P con números, los 10 knobs y la trampa de quitar el guardia sin saber qué vigilaba. Sobre el cluster genérico 4×H100 SXM.&lt;/p>
&lt;h2 id="dónde-estás-los-pasillos-no-la-mesa">Dónde estás: los pasillos, no la mesa&lt;/h2>
&lt;p>Imagina la cocina como un edificio. Las estaciones de cocción de élite —las GPUs— están en una sala con una &lt;strong>mesa central enorme&lt;/strong> (NVLink/NVSwitch) por la que se pasan ingredientes a toda velocidad sin levantarse. Esa mesa es para ellas y solo ellas.&lt;/p>
&lt;p>Pero el edificio tiene más cosas: la &lt;strong>despensa&lt;/strong> (el almacenamiento NVMe), la &lt;strong>puerta de carga&lt;/strong> (la red, la NIC) y la &lt;strong>recepción&lt;/strong> (la CPU y su RAM). Para llegar a cualquiera de esas, las estaciones no usan la mesa central: usan los &lt;strong>pasillos del edificio&lt;/strong> —el bus PCIe—. Y aquí aparece el personaje del post: en la entrada de cada pasillo hay un &lt;strong>guardia de seguridad&lt;/strong> (ACS) que, por defecto, no deja que dos estaciones se pasen algo directamente por el pasillo: las obliga a &lt;strong>subir el paquete a recepción&lt;/strong> para que lo revisen, y solo entonces baja a destino. Es seguro, pero es un rodeo absurdo cuando las dos estaciones están una al lado de la otra. GPUDirect es el permiso para saltarse ese rodeo; ACS e IOMMU son las razones por las que, a menudo, no puedes.&lt;/p>
&lt;h2 id="la-topología-de-un-nodo-dos-buses-no-uno">La topología de un nodo: dos buses, no uno&lt;/h2>
&lt;p>El error más común es pensar que en un nodo hay &amp;ldquo;un bus&amp;rdquo;. Hay (al menos) dos, y hacen cosas distintas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>NVLink / NVSwitch&lt;/strong> — la malla de alta velocidad GPU↔GPU. En H100 SXM, &lt;strong>18 enlaces × 50 GB/s = 900 GB/s bidireccionales&lt;/strong> entre dos GPUs cualesquiera, con NVSwitch dando un &lt;em>all-to-all&lt;/em> sin contención (&lt;a href="https://www.nvidia.com/en-us/data-center/h100/">NVLink, NVIDIA&lt;/a>). Es la mesa compartida.&lt;/li>
&lt;li>&lt;strong>PCIe Gen5&lt;/strong> — el bus de I/O general. Un enlace &lt;strong>x16 da 128 GB/s bidireccionales&lt;/strong> (~64 por sentido) (&lt;a href="https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf">H100 product brief&lt;/a>). Conecta cada GPU con la CPU, la RAM, las NICs y los NVMe. Es el pasillo.&lt;/li>
&lt;/ul>
&lt;p>La diferencia es de &lt;strong>7×&lt;/strong>: NVLink mueve en un segundo lo que el PCIe tarda siete. Por eso el tensor parallel intra-nodo va por NVLink y nadie lo discute. El PCIe importa para lo &lt;em>otro&lt;/em>: subir pesos del disco, recibir de la red, mover KV entre nodos.&lt;/p>
&lt;p>La herramienta para verlo es &lt;code>nvidia-smi topo -m&lt;/code>, que imprime una matriz de cómo está conectado cada par (&lt;a href="https://forums.developer.nvidia.com/t/nvidia-smi-topo-m-revisited/216584">foro NVIDIA&lt;/a>):&lt;/p>
&lt;pre tabindex="0">&lt;code> GPU0 GPU1 GPU2 GPU3 NIC0 CPU Affinity NUMA
GPU0 X NV18 NV18 NV18 PXB 0-47 0
GPU1 NV18 X NV18 NV18 PXB 0-47 0
GPU2 NV18 NV18 X NV18 SYS 48-95 1
GPU3 NV18 NV18 NV18 X SYS 48-95 1
&lt;/code>&lt;/pre>&lt;p>La leyenda es la que importa: &lt;strong>NV18&lt;/strong> = 18 enlaces NVLink (la mesa); &lt;strong>PXB&lt;/strong> = cruza switches PCIe pero no el host; &lt;strong>PHB&lt;/strong> = pasa por el host bridge; &lt;strong>NODE&lt;/strong> = mismo NUMA, cruzando PCIe; &lt;strong>SYS&lt;/strong> = cruza el interconnect entre sockets (el peor caso, atraviesa NUMA). Que &lt;code>GPU0↔NIC0&lt;/code> sea &lt;strong>PXB&lt;/strong> y &lt;code>GPU2↔NIC0&lt;/code> sea &lt;strong>SYS&lt;/strong> te dice exactamente qué GPU debe atender el tráfico de esa NIC —la 0, sin cruzar NUMA—. Esto enlaza directo con el &lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">post de NUMA&lt;/a> y el de &lt;a href="https://blog.lo0.es/posts/cilium-ebpf-dranet-numa-de-red-inferencia/">NUMA de red&lt;/a>: la afinidad PCIe &lt;strong>es&lt;/strong> la afinidad NUMA.&lt;/p>
&lt;svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" font-family="sans-serif" font-size="12" role="img" aria-label="Topología de un nodo: NVLink arriba, PCIe abajo, ACS como puerta">
&lt;rect x="120" y="20" width="480" height="40" rx="6" fill="none" stroke="#7c3aed" stroke-width="2"/>
&lt;text x="360" y="45" text-anchor="middle" fill="#7c3aed">NVSwitch — malla NVLink 900 GB/s (mesa GPU↔GPU)&lt;/text>
&lt;rect x="120" y="80" width="90" height="36" rx="4" fill="none" stroke="currentColor" stroke-width="1.5"/>&lt;text x="165" y="103" text-anchor="middle" fill="currentColor">GPU0&lt;/text>
&lt;rect x="250" y="80" width="90" height="36" rx="4" fill="none" stroke="currentColor" stroke-width="1.5"/>&lt;text x="295" y="103" text-anchor="middle" fill="currentColor">GPU1&lt;/text>
&lt;rect x="380" y="80" width="90" height="36" rx="4" fill="none" stroke="currentColor" stroke-width="1.5"/>&lt;text x="425" y="103" text-anchor="middle" fill="currentColor">GPU2&lt;/text>
&lt;rect x="510" y="80" width="90" height="36" rx="4" fill="none" stroke="currentColor" stroke-width="1.5"/>&lt;text x="555" y="103" text-anchor="middle" fill="currentColor">GPU3&lt;/text>
&lt;line x1="165" y1="80" x2="165" y2="60" stroke="#7c3aed" stroke-width="1.5"/>&lt;line x1="295" y1="80" x2="295" y2="60" stroke="#7c3aed" stroke-width="1.5"/>&lt;line x1="425" y1="80" x2="425" y2="60" stroke="#7c3aed" stroke-width="1.5"/>&lt;line x1="555" y1="80" x2="555" y2="60" stroke="#7c3aed" stroke-width="1.5"/>
&lt;rect x="250" y="160" width="220" height="34" rx="4" fill="none" stroke="#dc2626" stroke-width="2" stroke-dasharray="5 3"/>&lt;text x="360" y="182" text-anchor="middle" fill="#dc2626">switch PCIe + ACS (el guardia)&lt;/text>
&lt;line x1="165" y1="116" x2="300" y2="160" stroke="#2563eb" stroke-width="1.2"/>&lt;line x1="295" y1="116" x2="330" y2="160" stroke="#2563eb" stroke-width="1.2"/>&lt;line x1="425" y1="116" x2="390" y2="160" stroke="#2563eb" stroke-width="1.2"/>&lt;line x1="555" y1="116" x2="420" y2="160" stroke="#2563eb" stroke-width="1.2"/>
&lt;rect x="120" y="230" width="120" height="34" rx="4" fill="none" stroke="currentColor"/>&lt;text x="180" y="252" text-anchor="middle" fill="currentColor" font-size="11">CPU + RAM (root)&lt;/text>
&lt;rect x="300" y="230" width="120" height="34" rx="4" fill="none" stroke="currentColor"/>&lt;text x="360" y="252" text-anchor="middle" fill="currentColor" font-size="11">NIC (red)&lt;/text>
&lt;rect x="480" y="230" width="120" height="34" rx="4" fill="none" stroke="currentColor"/>&lt;text x="540" y="252" text-anchor="middle" fill="currentColor" font-size="11">NVMe (disco)&lt;/text>
&lt;line x1="180" y1="194" x2="180" y2="230" stroke="#2563eb" stroke-width="1.2"/>&lt;line x1="360" y1="194" x2="360" y2="230" stroke="#2563eb" stroke-width="1.2"/>&lt;line x1="430" y1="194" x2="540" y2="230" stroke="#2563eb" stroke-width="1.2"/>
&lt;text x="610" y="180" fill="#dc2626" font-size="10">ACS on →&lt;/text>
&lt;text x="610" y="195" fill="#dc2626" font-size="10">sube a root&lt;/text>
&lt;/svg>
&lt;h2 id="gpudirect-saltarse-la-recepción">GPUDirect: saltarse la recepción&lt;/h2>
&lt;p>Sin GPUDirect, mover un dato de la NIC (o el NVMe) a la GPU hace un rodeo obligatorio: &lt;strong>dispositivo → RAM del host → GPU&lt;/strong>. Ese rebote por la RAM consume ancho de banda de la CPU, gasta copias y añade latencia. &lt;strong>GPUDirect&lt;/strong> elimina el rebote dejando que el dato vaya &lt;strong>directo del dispositivo a la HBM&lt;/strong>. Tres sabores:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>GPUDirect P2P&lt;/strong> — GPU↔GPU &lt;strong>por PCIe&lt;/strong> (cuando no hay NVLink entre ellas, o para tráfico que no usa la mesa).&lt;/li>
&lt;li>&lt;strong>GPUDirect RDMA&lt;/strong> — GPU↔NIC: la tarjeta de red escribe directa en la HBM. Es lo que hace viable el multi-nodo eficiente (NCCL sobre InfiniBand/RoCE).&lt;/li>
&lt;li>&lt;strong>GPUDirect Storage (GDS)&lt;/strong> — GPU↔NVMe: el disco escribe directo en la HBM, sin buffer de host. Es la palanca del &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">cold start&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>En un nodo SXM, el tráfico GPU↔GPU del &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">tensor parallel&lt;/a> &lt;strong>no usa P2P por PCIe&lt;/strong>: usa NVLink. Por eso GPUDirect importa sobre todo en los &lt;strong>bordes&lt;/strong> del nodo: la red (RDMA, para multi-nodo) y el disco (GDS, para arranque). Ahí es donde ACS hace daño.&lt;/p>
&lt;h2 id="el-guardia-por-qué-acs-e-iommu-rompen-el-p2p">El guardia: por qué ACS e IOMMU rompen el P2P&lt;/h2>
&lt;p>&lt;strong>ACS (Access Control Services)&lt;/strong> es una feature de seguridad del PCIe pensada para virtualización y aislamiento: garantiza que un dispositivo no pueda leer/escribir directamente en otro sin que el &lt;em>root complex&lt;/em> lo medie. Para conseguirlo, &lt;strong>fuerza las transacciones peer-to-peer a subir hasta el root complex&lt;/strong> y volver a bajar (&lt;a href="https://docs.nvidia.com/gpudirect-storage/best-practices-guide/index.html">best practices GDS, NVIDIA&lt;/a>). Es exactamente lo contrario de lo que GPUDirect quiere: el camino directo deja de serlo.&lt;/p>
&lt;p>El &lt;strong>IOMMU&lt;/strong> (VT-d en Intel, equivalente en AMD) traduce direcciones y aísla dispositivos. Si está activo y &lt;strong>no&lt;/strong> en modo passthrough, también &lt;strong>redirige el tráfico P2P por el root complex&lt;/strong>, con el mismo efecto: rendimiento por los suelos o, en casos extremos, &lt;em>hangs&lt;/em> (&lt;a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting.html">troubleshooting NCCL&lt;/a>).&lt;/p>
&lt;p>Resumido sin rodeos (&lt;a href="https://morgangiraud.medium.com/multi-gpu-nvidia-p2p-capabilities-and-debugging-tips-fb7597b4e2b5">Giraud, debugging P2P&lt;/a>): &lt;strong>ACS&lt;/strong> fuerza el paso por el root &lt;em>para comprobaciones de seguridad&lt;/em>; &lt;strong>IOMMU&lt;/strong> lo fuerza &lt;em>para aislamiento y virtualización&lt;/em>. Ambos rompen el objetivo del P2P (comunicación directa sin intermediarios) y añaden overhead. Si no necesitas esa seguridad/virtualización en ese path, desactivarlos recupera el rendimiento. La receta operativa para máximo rendimiento de GPUDirect: &lt;strong>ACS off&lt;/strong> en los switches del camino e &lt;strong>IOMMU en passthrough&lt;/strong> (&lt;code>iommu=pt&lt;/code>) o desactivado.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan-cuánto-cuesta-el-rodeo">Las matemáticas que importan: cuánto cuesta el rodeo&lt;/h2>
&lt;p>Pongamos un &lt;strong>SWAP de KV&lt;/strong> de 5 GB (preemption del &lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">scheduler&lt;/a> que manda KV a host, o transferencia entre nodos en &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">serving desagregado&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Camino&lt;/th>
&lt;th>BW efectivo&lt;/th>
&lt;th>Tiempo de 5 GB&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>NVLink (GPU↔GPU intra-nodo)&lt;/td>
&lt;td>~450 GB/s&lt;/td>
&lt;td>&lt;strong>~11 ms&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PCIe Gen5 x16 directo (P2P, ACS off)&lt;/td>
&lt;td>~55 GB/s&lt;/td>
&lt;td>&lt;strong>~91 ms&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>PCIe vía root complex (ACS on)&lt;/td>
&lt;td>~25-30 GB/s*&lt;/td>
&lt;td>&lt;strong>~170-200 ms&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>* El rodeo por el root no solo &amp;ldquo;añade latencia&amp;rdquo;: satura el ancho de banda del host bridge, contiende con otro tráfico y, según la topología, puede caer bastante por debajo del directo. La cifra es de orden, para mostrar la magnitud del problema, no un benchmark.&lt;/p>
&lt;p>La lectura: en el camino que sí usa PCIe (red, disco, swap), tener ACS on puede &lt;strong>duplicar o triplicar&lt;/strong> el tiempo. Y si ese tiempo está en el camino crítico —un cold start, un swap de preemption, un all-reduce inter-nodo— se nota en la latencia que ve el usuario. Lo que &lt;strong>no&lt;/strong> arregla desactivar ACS: el tráfico que ya iba por NVLink (TP intra-nodo). Ahí ACS es irrelevante.&lt;/p>
&lt;h2 id="la-tensión-real-rendimiento-vs-aislamiento-y-ens">La tensión real: rendimiento vs aislamiento (y ENS)&lt;/h2>
&lt;p>Aquí el post se pone serio, porque la receta &amp;ldquo;desactiva ACS e IOMMU&amp;rdquo; tiene un coste que en un entorno regulado no es gratis. ACS e IOMMU &lt;strong>existen por una razón&lt;/strong>: aislar dispositivos. En un nodo &lt;strong>bare-metal dedicado&lt;/strong> a inferencia, sin virtualización ni multi-tenancy, no aíslas nada que importe y desactivarlos es razonable. Pero:&lt;/p>
&lt;ul>
&lt;li>Si haces &lt;strong>passthrough de GPU a VMs&lt;/strong> o usas contenedores con aislamiento fuerte, el IOMMU es &lt;strong>necesario&lt;/strong> —no es opcional—.&lt;/li>
&lt;li>En un escenario &lt;strong>multi-tenant&lt;/strong> donde varias cargas comparten nodo, ACS aporta una garantía de que un dispositivo no fisgonea a otro.&lt;/li>
&lt;li>En &lt;strong>ENS&lt;/strong> (&lt;a href="https://blog.lo0.es/posts/controles-tecnicos-ens-42001-eu-ai-act/">ver controles técnicos&lt;/a>), el aislamiento de cargas y la trazabilidad de accesos pueden ser requisitos; desactivar el aislamiento del bus para ganar 80 ms es una decisión que hay que &lt;strong>justificar y documentar&lt;/strong>, no un tuneo silencioso.&lt;/li>
&lt;/ul>
&lt;p>La salida de diseño, cuando necesitas las dos cosas: &lt;strong>mantén el aislamiento donde lo exige el compliance y diseña para que el camino caliente no dependa del P2P por PCIe&lt;/strong>. Concretamente, en un nodo SXM, el grueso del tráfico crítico (TP) ya va por NVLink y no le afecta ACS. Para la red, dedica una NIC por GPU en su mismo switch PCIe (PXB) y usa GPUDirect RDMA solo en el path que controlas. Para el disco, cachea pesos en NVMe local. Así no pagas la elección entre rendimiento y aislamiento: la evitas en el path que importa.&lt;/p>
&lt;h2 id="los-10-knobs">Los 10 knobs&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Knob&lt;/th>
&lt;th>Qué controla&lt;/th>
&lt;th>Coste / riesgo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>ACS off (switches del path)&lt;/td>
&lt;td>rodeo por root del P2P&lt;/td>
&lt;td>pierdes aislamiento de bus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>&lt;code>iommu=pt&lt;/code> / off&lt;/td>
&lt;td>redirección P2P por root&lt;/td>
&lt;td>rompe passthrough a VM si off&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;code>nvidia-smi topo -m&lt;/code>&lt;/td>
&lt;td>auditar la topología real&lt;/td>
&lt;td>— (siempre conviene)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>&lt;code>p2pBandwidthLatencyTest&lt;/code>&lt;/td>
&lt;td>medir P2P de verdad&lt;/td>
&lt;td>— (verifica antes de asumir)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>colocación de NIC&lt;/td>
&lt;td>mismo switch PCIe que la GPU&lt;/td>
&lt;td>SYS si cruza NUMA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>colocación de NVMe&lt;/td>
&lt;td>NUMA-local a la GPU&lt;/td>
&lt;td>H2D cruzando UPI&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>&lt;code>nvidia-peermem&lt;/code> (GDR)&lt;/td>
&lt;td>habilita RDMA a HBM&lt;/td>
&lt;td>driver/kernel correctos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>PCIe gen/lanes (x16)&lt;/td>
&lt;td>ancho del pasillo&lt;/td>
&lt;td>GPU en x8 silencioso&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>relaxed ordering / ASPM&lt;/td>
&lt;td>latencia y energía PCIe&lt;/td>
&lt;td>jitter si mal configurado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>persistence mode&lt;/td>
&lt;td>evita reinit del path&lt;/td>
&lt;td>GPU ociosa pagada&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="cómo-se-conecta-con-el-resto-del-stack">Cómo se conecta con el resto del stack&lt;/h2>
&lt;p>&lt;strong>Con NVLink y NCCL.&lt;/strong> El &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">post de NVLink&lt;/a> cubre la mesa GPU↔GPU; este cubre el &lt;em>otro&lt;/em> bus, el que conecta con disco, red y host. Son complementarios: ACS afecta al PCIe, no al NVLink.&lt;/p>
&lt;p>&lt;strong>Con el cold start.&lt;/strong> GPUDirect Storage del &lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">post disco→HBM&lt;/a> es GPUDirect sobre el path de almacenamiento; ACS on lo estrangula igual que estrangula el P2P.&lt;/p>
&lt;p>&lt;strong>Con NUMA.&lt;/strong> La afinidad PCIe de &lt;code>topo -m&lt;/code> &lt;strong>es&lt;/strong> la afinidad NUMA del &lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">post del host&lt;/a>; colocar NIC y NVMe en el NUMA correcto evita el camino SYS.&lt;/p>
&lt;p>&lt;strong>Con la red.&lt;/strong> La colocación de NIC y GPUDirect RDMA es el tema del &lt;a href="https://blog.lo0.es/posts/cilium-ebpf-dranet-numa-de-red-inferencia/">post de NUMA de red&lt;/a>; el mismo principio de &amp;ldquo;saca a la CPU del medio&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Con PagedAttention y el scheduler.&lt;/strong> El &lt;strong>SWAP&lt;/strong> de preemption (&lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">scheduler&lt;/a>) mueve bloques de &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">KV&lt;/a> por PCIe; por eso V1 prefiere RECOMPUTE y por eso este bus importa.&lt;/p>
&lt;p>&lt;strong>Con el disaggregated serving.&lt;/strong> Transferir KV entre pools en &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">serving desagregado&lt;/a> viaja por PCIe→NIC→PCIe; ACS y la colocación deciden si es viable.&lt;/p>
&lt;p>&lt;strong>Con ENS.&lt;/strong> El aislamiento del bus es un control técnico; ver &lt;a href="https://blog.lo0.es/posts/controles-tecnicos-ens-42001-eu-ai-act/">controles ENS/42001/AI Act&lt;/a>.&lt;/p>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>&amp;ldquo;Desactiva ACS en todas partes, va más rápido.&amp;rdquo;&lt;/strong> En un nodo dedicado, vale. En uno con virtualización, multi-tenancy o requisitos de aislamiento (ENS), estás quitando un control de seguridad. La decisión correcta es &lt;em>por path&lt;/em> y documentada, no global y silenciosa.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;NVLink y PCIe son el mismo bus, más o menos.&amp;rdquo;&lt;/strong> No. Son dos buses con 7× de diferencia y propósitos distintos. El TP va por NVLink; el disco, la red y el host van por PCIe. Confundirlos lleva a &amp;ldquo;optimizar&amp;rdquo; ACS para un tráfico que ni siquiera pasa por PCIe.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;El P2P funciona solo, no hay que comprobar nada.&amp;rdquo;&lt;/strong> El P2P &lt;strong>se desactiva en silencio&lt;/strong> con ACS/IOMMU activos, y muchas distros los activan por defecto. Comprueba con &lt;code>p2pBandwidthLatencyTest&lt;/code> y &lt;code>nvidia-smi topo -m&lt;/code>; no asumas que tienes el camino directo solo porque las GPUs están en el mismo nodo.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;IOMMU off siempre, por rendimiento.&amp;rdquo;&lt;/strong> Si haces passthrough de GPU a máquinas virtuales, el IOMMU es &lt;strong>obligatorio&lt;/strong>; desactivarlo rompe el passthrough. El modo correcto suele ser &lt;code>passthrough&lt;/code> (&lt;code>iommu=pt&lt;/code>): mantiene el mapeo necesario sin penalizar el P2P.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;Más lanes PCIe = GPU más rápida.&amp;rdquo;&lt;/strong> El PCIe es el camino de I/O, no de cómputo. Una GPU en x8 en vez de x16 tarda más en &lt;em>cargar&lt;/em> y en &lt;em>comunicar por PCIe&lt;/em>, pero genera tokens a la misma velocidad una vez los pesos están dentro. El daño de x8 está en el cold start y en el multi-nodo, no en el throughput de decode.&lt;/p>
&lt;p>&lt;strong>&amp;ldquo;GPUDirect arregla cualquier cuello de I/O.&amp;rdquo;&lt;/strong> GPUDirect quita el rebote por la CPU; si tu cuello es el propio dispositivo (NVMe saturado, NIC a tope) o la topología (camino SYS cruzando NUMA), GPUDirect no lo toca. Mide dónde está el cuello antes.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>Toda esta serie ha bajado pisos buscando dónde se pierde el tiempo, y este llega al cableado del edificio. La intuición trata el nodo como una caja homogénea donde &amp;ldquo;las GPUs hablan con todo&amp;rdquo;; la realidad es que hay dos buses con propósitos opuestos —una mesa de élite para las GPUs (NVLink) y unos pasillos de servicio para todo lo demás (PCIe)— y un guardia de seguridad en los pasillos que, con la mejor intención, obliga a cada paquete a subir a recepción antes de entregarlo. GPUDirect es el permiso para la entrega directa; ACS e IOMMU son las razones legítimas por las que a veces no te lo dan. La lección no es &amp;ldquo;desactiva el guardia&amp;rdquo;: es entender &lt;strong>qué camino es crítico&lt;/strong> (casi nunca el que crees) y &lt;strong>qué vigilaba el guardia&lt;/strong> antes de mandarlo a casa. En un nodo dedicado, el camino directo es casi gratis y conviene tomarlo. En uno que comparte cargas o vive bajo ENS, el aislamiento del bus es un control que se sacrifica con justificación o no se sacrifica. El buen diseño no elige entre rendimiento y aislamiento a ciegas: pone el tráfico crítico en la mesa que no necesita guardia, y deja los pasillos para lo que puede esperar.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">La mesa compartida: NVLink, NVSwitch y NCCL&lt;/a> — el bus GPU↔GPU que ACS no toca; complementario a este post.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM: cold start&lt;/a> — GPUDirect Storage sobre el path de NVMe, estrangulado por ACS igual que el P2P.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">NUMA, hugepages y aislamiento de CPU&lt;/a> — la afinidad PCIe es la afinidad NUMA; colocar NIC y NVMe en el socket correcto.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cilium-ebpf-dranet-numa-de-red-inferencia/">NUMA de red, Cilium eBPF y DRANET&lt;/a> — colocación de NIC y GPUDirect RDMA, el mismo principio de sacar a la CPU del medio.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention y el block manager&lt;/a> — el KV que viaja por PCIe cuando se hace SWAP.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/scheduler-step-vllm/">El pase: el scheduler step de vLLM&lt;/a> — por qué V1 prefiere RECOMPUTE a SWAP (evita el viaje por PCIe).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode separados&lt;/a> — transferir KV entre nodos pasa por PCIe→NIC→PCIe.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/controles-tecnicos-ens-42001-eu-ai-act/">Controles técnicos ENS / ISO 42001 / EU AI Act&lt;/a> — el aislamiento del bus como control de seguridad a justificar.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>NVIDIA, &lt;em>GPUDirect Storage Best Practices Guide&lt;/em> (ACS, IOMMU, paths): &lt;a href="https://docs.nvidia.com/gpudirect-storage/best-practices-guide/index.html">https://docs.nvidia.com/gpudirect-storage/best-practices-guide/index.html&lt;/a>.&lt;/li>
&lt;li>NVIDIA, &lt;em>NCCL Troubleshooting&lt;/em> (IOMMU/VT-d y P2P): &lt;a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting.html">https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/troubleshooting.html&lt;/a>.&lt;/li>
&lt;li>M. Giraud, &lt;em>Multi-GPU (NVIDIA) P2P capabilities and debugging tips&lt;/em>: &lt;a href="https://morgangiraud.medium.com/multi-gpu-nvidia-p2p-capabilities-and-debugging-tips-fb7597b4e2b5">https://morgangiraud.medium.com/multi-gpu-nvidia-p2p-capabilities-and-debugging-tips-fb7597b4e2b5&lt;/a>.&lt;/li>
&lt;li>NVIDIA, &lt;em>H100 Product Brief&lt;/em> (PCIe Gen5, NVLink 900 GB/s): &lt;a href="https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf">https://www.nvidia.com/content/dam/en-zz/Solutions/Data-Center/h100/PB-11773-001_v01.pdf&lt;/a>.&lt;/li>
&lt;li>NVIDIA, &lt;em>GPUDirect RDMA documentation&lt;/em>: &lt;a href="https://docs.nvidia.com/cuda/gpudirect-rdma/index.html">https://docs.nvidia.com/cuda/gpudirect-rdma/index.html&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>La puerta de la cocina que el maître no miró: NUMA de red, Cilium eBPF y DRANET, la cuarta pata del pinning</title><link>https://blog.lo0.es/posts/cilium-ebpf-dranet-numa-de-red-inferencia/</link><pubDate>Sat, 06 Jun 2026 12:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/cilium-ebpf-dranet-numa-de-red-inferencia/</guid><description>&lt;blockquote>
&lt;p>Cuarta entrega —coda— de &amp;ldquo;por debajo del motor&amp;rdquo;. La serie cerró con tres patas de la localidad: el &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">cable entre GPUs&lt;/a>, el &lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">host a mano&lt;/a> y la &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">orquestación declarativa del kubelet&lt;/a>. Pero el maître del último post sentaba al grupo mirando CPU, memoria y GPU, y nunca preguntó &lt;strong>por qué puerta entran los platos&lt;/strong>. Esa puerta es la NIC. Aquí está la cuarta pata.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">Topology Manager&lt;/a> admite un pod en &lt;code>single-numa-node&lt;/code> si sus CPUs, su memoria y su GPU caben en el &lt;strong>mismo&lt;/strong> NUMA node. La &lt;strong>NIC no entra en esa cuenta&lt;/strong>: 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 &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">disaggregated serving&lt;/a>, donde el KV-cache viaja por RDMA entre el pool de prefill y el de decode— una NIC en el socket equivocado hace que &lt;strong>cada paquete cruce la UPI/QPI&lt;/strong>, exactamente el &amp;ldquo;NUMA remoto&amp;rdquo; que la serie combate por el lado de cómputo, pero por la puerta de la red. Y hay un segundo frente: el &lt;strong>softirq&lt;/strong> (&lt;code>NET_RX&lt;/code>) que procesa el datapath corre en la CPU que atiende la IRQ de la NIC; si esa CPU es uno de los cores que &lt;code>isolcpus&lt;/code>/&lt;code>reserved-cpus&lt;/code> dieron en exclusiva a vLLM, el softirq le roba ciclos y mete jitter en la cola de p99. &lt;strong>Cilium eBPF&lt;/strong> sustituye dos piezas de RKE2 —&lt;code>kube-proxy&lt;/code> (por load balancing eBPF/XDP) y el CNI por defecto &lt;strong>Canal&lt;/strong> (por datapath nativo)— y su propia guía de tuning te manda &lt;strong>matar &lt;code>irqbalance&lt;/code> y fijar las IRQ de la NIC&lt;/strong>: una &lt;strong>cuarta lista&lt;/strong> que alinear junto a &lt;code>isolcpus&lt;/code> y &lt;code>reserved-cpus&lt;/code>. El estado del arte 2026 cierra el hueco por arriba: &lt;strong>netkit&lt;/strong> (kernel ≥6.8, overhead de namespace a cero), &lt;strong>BIG TCP&lt;/strong> (super-paquetes de 192k para 100Gb/s+), &lt;strong>host-routing&lt;/strong> (bypass de iptables), y sobre todo &lt;strong>DRA/DRANET&lt;/strong>, el driver de red que por fin co-programa &lt;strong>GPU y NIC NUMA-locales en el mismo PCIe root&lt;/strong>, habilitando GPUDirect RDMA con &lt;strong>+59,6% de bus bandwidth en &lt;code>all_gather&lt;/code> y +58,1% en &lt;code>all_reduce&lt;/code>&lt;/strong>. Sobre un cluster genérico RKE2 con nodos 4×H100 SXM.&lt;/p>
&lt;h2 id="dónde-estás-el-plano-de-red-que-la-trilogía-no-abrió">Dónde estás: el plano de red que la trilogía no abrió&lt;/h2>
&lt;div class="diagram" style="max-width:560px;margin:1.2rem auto;">
&lt;svg viewBox="0 0 560 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="La cuarta pata: el plano de red bajo el mismo NUMA que CPU, memoria y GPU">
&lt;text x="280" y="24" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">El stack vertical · la cuarta pata de la localidad&lt;/text>
&lt;rect x="120" y="40" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="64" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">Motor · pod vLLM (TP, batching, KV-cache)&lt;/text>
&lt;rect x="120" y="84" width="320" height="38" rx="6" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.4"/>
&lt;text x="280" y="108" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">Orquestación · kubelet: CPU/Mem/Topology Mgr (post 3)&lt;/text>
&lt;rect x="120" y="128" width="320" height="58" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="3"/>
&lt;text x="280" y="152" text-anchor="middle" font-family="sans-serif" font-size="12.5" font-weight="700" fill="#1f5c34">ESTÁS AQUÍ · plano de red: Cilium eBPF + DRA/NIC&lt;/text>
&lt;text x="280" y="170" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#2c6b42">localidad NUMA de la NIC · IRQ · GPUDirect RDMA&lt;/text>
&lt;rect x="120" y="192" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="216" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">Host · NUMA, hugepages, isolcpus (post 2)&lt;/text>
&lt;rect x="120" y="236" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="260" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">CUDA + NCCL + NVLink (post 1)&lt;/text>
&lt;rect x="120" y="280" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="304" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">Hardware · 2 sockets, 4×H100 SXM, NIC 400 Gb/s&lt;/text>
&lt;text x="280" y="332" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#777">CPU+memoria+GPU las pinnea el kubelet; la NIC, hasta 2026, no la pinnaba nadie&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-la-puerta-por-la-que-entran-los-platos">La analogía: la puerta por la que entran los platos&lt;/h2>
&lt;p>Vuelve al restaurante del &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">post anterior&lt;/a>. 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 &lt;strong>nunca miró dónde está el pase de cocina&lt;/strong>: la puerta por la que entra y sale cada plato.&lt;/p>
&lt;p>Esa puerta es la &lt;strong>NIC&lt;/strong>. 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), &lt;strong>cada plato cruza el restaurante entero&lt;/strong> (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.&lt;/p>
&lt;p>Y hay un detalle más fino: el camarero que cruza la sala con los platos (el &lt;strong>softirq&lt;/strong> que procesa los paquetes) es &lt;strong>uno de los comensales sentados&lt;/strong>. Si el maître le asignó una silla en exclusiva para comer tranquilo (un core aislado por &lt;code>isolcpus&lt;/code> 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.&lt;/p>
&lt;p>La trilogía niveló tres patas de la mesa: el cable, el host y la orquestación. La cuarta —&lt;strong>por qué puerta entran los platos y quién los lleva&lt;/strong>— no la nivela ningún manager del kubelet. Hasta 2026.&lt;/p>
&lt;h2 id="el-hueco-por-qué-el-topology-manager-no-mira-la-nic">El hueco: por qué el Topology Manager no mira la NIC&lt;/h2>
&lt;p>El mecanismo del &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">post 3&lt;/a> es un coordinador (Topology Manager) que consulta a tres &lt;strong>Hint Providers&lt;/strong>: 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.&lt;/p>
&lt;p>El problema es de &lt;strong>censo&lt;/strong>: la NIC clásica no es un &amp;ldquo;device&amp;rdquo; del Device Manager. Una tarjeta Ethernet/InfiniBand estándar la gestiona el CNI y el kernel, no se pide en el &lt;code>resources:&lt;/code> del pod como &lt;code>nvidia.com/gpu&lt;/code>, y por tanto &lt;strong>no emite hint NUMA&lt;/strong>. 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.&lt;/p>
&lt;p>Esto no importaba cuando la red de un nodo eran 10/25 Gb/s y el cuello de botella estaba en otro sitio. Importa &lt;strong>ahora&lt;/strong>, con dos cargas que saturan la red del nodo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Disaggregated serving.&lt;/strong> El &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">KV-cache que viaja entre el pool de prefill y el de decode&lt;/a> se mueve por RDMA. Son transferencias grandes, sensibles a latencia y ancho de banda, que en multinodo salen por la NIC.&lt;/li>
&lt;li>&lt;strong>Colectivos NCCL multinodo.&lt;/strong> Cuando el &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">tensor/pipeline parallel cruza el límite del nodo&lt;/a>, los &lt;code>all-reduce&lt;/code>/&lt;code>all-gather&lt;/code> ya no van por NVLink sino por GPUDirect RDMA sobre la NIC.&lt;/li>
&lt;/ul>
&lt;p>En ambos, &lt;strong>dónde está la NIC respecto a la GPU y a los cores del pod&lt;/strong> decide el rendimiento. Y eso el kubelet, por sí solo, no lo coordina.&lt;/p>
&lt;h2 id="el-datapath-de-red-bajo-numa-irq-softirq-y-dma">El datapath de red bajo NUMA: IRQ, softirq y DMA&lt;/h2>
&lt;p>Para ver por qué la localidad de la NIC pesa, hay que mirar el camino de un paquete que llega:&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1.2rem auto;">
&lt;svg viewBox="0 0 820 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Camino de un paquete: NIC, IRQ, softirq y DMA cross-NUMA">
&lt;defs>&lt;marker id="nm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">Socket 0 (NIC aquí) vs Socket 1 (pod vLLM aquí): el cruce que no se ve&lt;/text>
&lt;!-- Socket 0 -->
&lt;rect x="30" y="46" width="360" height="240" rx="10" fill="none" stroke="#4a6fa5" stroke-width="1.6" stroke-dasharray="5 3"/>
&lt;text x="210" y="66" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#4a6fa5">NUMA node 0 · PCIe root con la NIC&lt;/text>
&lt;rect x="60" y="80" width="120" height="44" rx="7" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.6"/>
&lt;text x="120" y="100" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#222">NIC 400 Gb/s&lt;/text>
&lt;text x="120" y="115" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">multi-queue, RSS&lt;/text>
&lt;rect x="60" y="150" width="120" height="44" rx="7" fill="#f7efda" stroke="#c79a32" stroke-width="1.6"/>
&lt;text x="120" y="170" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#222">IRQ + softirq&lt;/text>
&lt;text x="120" y="185" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">NET_RX en core 0..&lt;/text>
&lt;rect x="60" y="220" width="120" height="44" rx="7" fill="#dceede" stroke="#3c8c54" stroke-width="1.6"/>
&lt;text x="120" y="240" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#222">RAM node 0&lt;/text>
&lt;text x="120" y="255" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">DMA del paquete&lt;/text>
&lt;path d="M120,124 L120,150" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#nm)"/>
&lt;path d="M120,194 L120,220" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#nm)"/>
&lt;!-- Socket 1 -->
&lt;rect x="430" y="46" width="360" height="240" rx="10" fill="none" stroke="#a85454" stroke-width="1.6" stroke-dasharray="5 3"/>
&lt;text x="610" y="66" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#a85454">NUMA node 1 · aquí pinneó el kubelet al pod&lt;/text>
&lt;rect x="550" y="150" width="160" height="44" rx="7" fill="#dceede" stroke="#3c8c54" stroke-width="2.2"/>
&lt;text x="630" y="170" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#222">pod vLLM + GPU&lt;/text>
&lt;text x="630" y="185" text-anchor="middle" font-family="sans-serif" font-size="9" fill="#444">CPUs exclusivas node 1&lt;/text>
&lt;!-- cross-numa arrow -->
&lt;path d="M180,242 C320,242 470,172 550,172" fill="none" stroke="#a85454" stroke-width="2.4" marker-end="url(#nm)"/>
&lt;text x="370" y="225" text-anchor="middle" font-family="sans-serif" font-size="11" font-weight="700" fill="#a85454">cada paquete cruza la UPI/QPI&lt;/text>
&lt;text x="370" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#a85454">+latencia, +consumo del enlace inter-socket&lt;/text>
&lt;text x="410" y="306" text-anchor="middle" font-family="sans-serif" font-size="10" font-style="italic" fill="#777">El Topology Manager hizo su trabajo en el node 1; la NIC se quedó en el 0. Nadie alineó las dos cosas.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Tres hechos del kernel que la analogía comprime:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>La IRQ tiene afinidad.&lt;/strong> Cada cola de la NIC dispara una interrupción que el kernel atiende en una CPU concreta (&lt;code>/proc/irq/&amp;lt;n&amp;gt;/smp_affinity&lt;/code>). El procesamiento pesado se difiere a un &lt;strong>softirq&lt;/strong> (&lt;code>NET_RX&lt;/code>/&lt;code>NET_TX&lt;/code>), que corre en &lt;strong>esa misma CPU&lt;/strong>. Si &lt;code>irqbalance&lt;/code> está suelto, las va migrando de forma no determinista —veneno para el p99.&lt;/li>
&lt;li>&lt;strong>El softirq compite con el pod.&lt;/strong> Si la IRQ cae en un core que &lt;code>isolcpus&lt;/code> reservó para vLLM, el &lt;code>NET_RX&lt;/code> de esa cola le roba ciclos al modelo. La señal en &lt;code>/proc/softirqs&lt;/code>: una columna de &lt;code>NET_RX&lt;/code> que se dispara en una sola CPU. Es el mismo jitter del &lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">post 2&lt;/a>, entrando por la red.&lt;/li>
&lt;li>&lt;strong>El DMA tiene origen NUMA.&lt;/strong> 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.&lt;/li>
&lt;/ol>
&lt;h3 id="un-número-con-su-salvedad">Un número, con su salvedad&lt;/h3>
&lt;p>Pongamos un nodo de 2 sockets, NIC de &lt;strong>400 Gb/s = 50 GB/s&lt;/strong> 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 &lt;strong>cruzan la UPI&lt;/strong> hacia el socket 1. Un enlace UPI 2.0 ronda los ~&lt;strong>20–40 GB/s&lt;/strong> por dirección y enlace según generación; aun con varios enlaces, 50 GB/s de tráfico de red &lt;strong>a contracorriente&lt;/strong> 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 &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">KV-cache de la disaggregation&lt;/a>. No doy un &amp;ldquo;X% de degradación&amp;rdquo; 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.&lt;/p>
&lt;p>Lo que &lt;strong>sí&lt;/strong> está medido con metodología pública es el efecto agregado de alinear GPU y NIC: el proyecto &lt;strong>DRANET&lt;/strong> reporta &lt;strong>+59,6% de bus bandwidth en &lt;code>all_gather&lt;/code> y +58,1% en &lt;code>all_reduce&lt;/code>&lt;/strong> (colectivos NCCL) cuando la NIC asignada es &lt;strong>NUMA-local a la GPU&lt;/strong> frente a no serlo. Esa es la magnitud del hueco que el Topology Manager dejaba abierto.&lt;/p>
&lt;h2 id="qué-sustituye-cilium-ebpf-de-rke2-y-por-qué-toca-esta-historia">Qué sustituye Cilium eBPF de RKE2 (y por qué toca esta historia)&lt;/h2>
&lt;p>RKE2 trae por defecto &lt;strong>Canal&lt;/strong> (Flannel + Calico) como CNI y &lt;strong>&lt;code>kube-proxy&lt;/code>&lt;/strong> (reglas iptables/IPVS) para el balanceo de Services. Cambiar a Cilium (&lt;code>cni: cilium&lt;/code> en &lt;code>/etc/rancher/rke2/config.yaml&lt;/code>) sustituye ambas piezas por un datapath eBPF:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pieza de RKE2&lt;/th>
&lt;th>Qué hace&lt;/th>
&lt;th>Qué pone Cilium eBPF&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>kube-proxy&lt;/code> (iptables/IPVS)&lt;/td>
&lt;td>balanceo de Services&lt;/td>
&lt;td>LB en eBPF; con &lt;code>kubeProxyReplacement=true&lt;/code>, y aceleración en &lt;strong>XDP&lt;/strong> (capa de driver)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Canal (Flannel+Calico)&lt;/td>
&lt;td>overlay VXLAN + NetworkPolicy&lt;/td>
&lt;td>datapath nativo (&lt;code>routingMode=native&lt;/code>), NetworkPolicy L3/L4 y L7 en eBPF&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>veth por pod&lt;/td>
&lt;td>par de interfaces del namespace&lt;/td>
&lt;td>&lt;strong>netkit&lt;/strong> (kernel ≥6.8): overhead de namespace ~0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>recorrido iptables del host&lt;/td>
&lt;td>hooks netfilter&lt;/td>
&lt;td>&lt;strong>host-routing&lt;/strong> eBPF: bypass de iptables y de la parte alta del stack&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Hasta aquí es networking puro y &lt;strong>no toca&lt;/strong> los resource managers del kubelet: Cilium no asigna CPUs exclusivas ni emite hints NUMA de cómputo. Los diez knobs del &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">post 3&lt;/a> siguen idénticos pongas Canal o Cilium.&lt;/p>
&lt;p>&lt;strong>Pero&lt;/strong> Cilium sí entra en la cuarta pata por dos puertas. La primera: su propia &lt;a href="https://docs.cilium.io/en/stable/operations/performance/tuning/">guía de tuning&lt;/a> recomienda, literalmente, &lt;em>&amp;ldquo;matar &lt;code>irqbalance&lt;/code> y fijar las IRQ de la NIC a CPUs específicas para máximo aislamiento de la carga&amp;rdquo;&lt;/em>, además del perfil &lt;code>tuned network-latency&lt;/code>, el governor &lt;code>performance&lt;/code> y &lt;code>CONFIG_PREEMPT_NONE&lt;/code>. Es decir: el datapath eBPF rinde de verdad &lt;strong>solo si coordinas la afinidad de IRQ&lt;/strong> —y esa afinidad tiene que apuntar a los cores &lt;strong>housekeeping&lt;/strong> (&lt;code>reserved-cpus&lt;/code>), nunca a los aislados. Aparece así una &lt;strong>cuarta lista&lt;/strong> que mantener coherente con &lt;code>isolcpus&lt;/code> y &lt;code>reserved-cpus&lt;/code>:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">isolcpus = 2-31,34-63 # cores exclusivos para vLLM (host, post 2)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">reserved-cpus = 0-1,32-33 # housekeeping del kubelet (post 3)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">IRQ affinity = 0-1,32-33 # NIC IRQs → SOLO housekeeping (este post)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> # nunca 2-31: ahí el softirq robaría al modelo
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La segunda puerta: &lt;strong>netkit + host-routing + BIG TCP&lt;/strong> reducen cuántas veces el paquete cruza el stack y el namespace, lo que &lt;strong>amortigua&lt;/strong> (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 &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">continuous batching&lt;/a> aplicada al stack de red: amortizar un coste fijo sobre lotes mayores.&lt;/p>
&lt;h3 id="perfil-de-rendimiento-de-cilium-estado-119-kernel-68">Perfil de rendimiento de Cilium (estado 1.19, kernel ≥6.8)&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Helm, perfil de rendimiento recomendado (resumen de la tuning guide)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install cilium cilium/cilium --version 1.19.4 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace kube-system &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">routingMode&lt;/span>&lt;span class="o">=&lt;/span>native &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set bpf.datapathMode&lt;span class="o">=&lt;/span>netkit &lt;span class="se">\ &lt;/span> &lt;span class="c1"># overhead de namespace ~0 (kernel &amp;gt;=6.8)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --set bpf.masquerade&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">kubeProxyReplacement&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># sustituye kube-proxy de RKE2&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --set &lt;span class="nv">enableIPv4BIGTCP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\ &lt;/span> &lt;span class="c1"># super-paquetes 192k (NIC mlx5/ice)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --set &lt;span class="nv">enableIPv6BIGTCP&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set bpf.distributedLRU.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\#&lt;/span> mapas BPF per-CPU: menos contención de spinlock
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> --set bpf.mapDynamicSizeRatio&lt;span class="o">=&lt;/span>0.08 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set &lt;span class="nv">bpfClockProbe&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Verificación dentro de un pod de Cilium:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cilium status --verbose &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;Device Mode|Host Routing|BIG TCP|XDP&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Device Mode: netkit · Host Routing: BPF · IPv4 BIG TCP: enabled · XDP Acceleration: Native&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Salvedad escéptica: netkit y BIG TCP son &lt;strong>beta&lt;/strong> 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 &lt;em>per-node config&lt;/em> solo en nodos nuevos. Para un cluster ENS en producción, eso es una ventana de mantenimiento, no un &lt;code>helm upgrade&lt;/code> a ciegas.&lt;/p>
&lt;h2 id="el-estado-del-arte-2026-dra-y-dranet-el-maître-que-por-fin-mira-la-puerta">El estado del arte 2026: DRA y DRANET, el maître que por fin mira la puerta&lt;/h2>
&lt;p>Lo que cierra el hueco de raíz no es Cilium —es el &lt;strong>mecanismo de admisión&lt;/strong> que el kubelet no tenía para la NIC: &lt;strong>Dynamic Resource Allocation (DRA)&lt;/strong>, beta desde Kubernetes 1.32 y con avances en cada release hasta la 1.36 (mayo 2026). DRA generaliza el modelo de &amp;ldquo;devices&amp;rdquo; más allá de la GPU: un driver descubre el hardware, publica &lt;code>ResourceSlices&lt;/code> con sus atributos —incluida la &lt;strong>topología NUMA y el PCIe root&lt;/strong>— y el scheduler resuelve &lt;code>ResourceClaims&lt;/code> que pueden exigir afinidad entre dispositivos.&lt;/p>
&lt;p>&lt;strong>DRANET&lt;/strong> (proyecto &lt;code>kubernetes-sigs&lt;/code>) es el driver DRA de red. Descubre las NICs (incluidas las RDMA-capaces), las anuncia como &lt;code>ResourceSlices&lt;/code>, y vía &lt;strong>NRI&lt;/strong> 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 &lt;strong>NVIDIA GPU DRA driver&lt;/strong>, permite &lt;strong>co-programar GPU y NIC que comparten PCIe root&lt;/strong> (la relación que NVIDIA llama &lt;code>NODE&lt;/code>), que es justo la condición de &lt;strong>GPUDirect RDMA&lt;/strong>. El maître por fin tiene su cuarto ayudante: &lt;em>&amp;quot;¿hay una NIC NUMA-local a esta GPU?&amp;quot;&lt;/em>.&lt;/p>
&lt;p>El &lt;code>ResourceClaimTemplate&lt;/code> usa selectores CEL para pedir exactamente esa alineación:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Pedir una NIC RDMA NUMA-local a la GPU asignada (esquema ilustrativo DRANET/DRA)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">resource.k8s.io/v1beta1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ResourceClaimTemplate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpu-nic-numa-aligned&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">devices&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">rdma-nic&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">deviceClassName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">dra.net &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># NICs publicadas por DRANET&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">constraints&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;rdma-nic&amp;#34;&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">matchAttribute&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;dra.net/pcieRoot&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># misma raíz PCIe que la GPU&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c"># → habilita GPUDirect RDMA sobre camino NUMA-local&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Por qué importa para inferencia, no para &amp;ldquo;AI training&amp;rdquo; abstracto: en &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">disaggregated serving&lt;/a>, RDMA es lo que mueve el KV-cache entre el pool de prefill y el de decode con la latencia que el &lt;a href="https://blog.lo0.es/posts/prefill-optimizaciones-vllm/">TTFT&lt;/a> exige; y en multinodo, GPUDirect RDMA sustituye al &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">NVLink&lt;/a> como medio del colectivo. Alinear GPU+NIC en el mismo PCIe root es lo que convierte un &amp;ldquo;RDMA que funciona&amp;rdquo; en un &amp;ldquo;RDMA que rinde&amp;rdquo; —los +60% de bus bandwidth de DRANET.&lt;/p>
&lt;p>Estado y salvedades: DRA es &lt;strong>beta&lt;/strong> (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 &lt;strong>camino, no producto cerrado&lt;/strong>: 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.&lt;/p>
&lt;h2 id="cómo-se-conecta-con-el-resto-del-stack">Cómo se conecta con el resto del stack&lt;/h2>
&lt;p>&lt;strong>Con el host (post 2).&lt;/strong> La afinidad de IRQ de la NIC es una &lt;strong>tercera lista&lt;/strong> que casar con &lt;code>isolcpus&lt;/code> y &lt;code>reserved-cpus&lt;/code>. Las IRQ van a housekeeping; los cores aislados, intactos. Descoordinarlas mete por la puerta de la red el jitter que &lt;code>isolcpus&lt;/code> echó por la de cómputo.&lt;/p>
&lt;p>&lt;strong>Con la orquestación (post 3).&lt;/strong> DRA es la extensión natural del Topology Manager: el mismo principio de &amp;ldquo;admite solo si encaja en el NUMA node&amp;rdquo; llevado a la NIC. Donde el Device Manager dejaba la red fuera del censo, DRANET la mete.&lt;/p>
&lt;p>&lt;strong>Con el interconnect (post 1).&lt;/strong> 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; &lt;strong>DRANET añade que la NIC también&lt;/strong> —y solo entonces el RDMA va por el camino corto.&lt;/p>
&lt;p>&lt;strong>Con disaggregated serving.&lt;/strong> 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 &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">separar prefill y decode&lt;/a> no se pague en latencia de transferencia.&lt;/p>
&lt;p>&lt;strong>Con capacity planning.&lt;/strong> El &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">dimensionado&lt;/a> gana una dimensión: no basta con &amp;ldquo;GPUs por nodo y cores por NUMA node&amp;rdquo;; hay que contar &lt;strong>cuántas NICs NUMA-locales a GPU&lt;/strong> tiene el chasis. Un nodo con 4 GPUs y una sola NIC en el socket 0 tiene dos GPUs &amp;ldquo;lejos de la puerta&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Con la observabilidad.&lt;/strong> Lo que confirma que la cuarta pata está bien puesta no es un dashboard de aplicación: es &lt;code>/proc/softirqs&lt;/code> (¿&lt;code>NET_RX&lt;/code> concentrado en housekeeping?), &lt;code>nvidia-smi topo -m&lt;/code> (¿relación &lt;code>NODE&lt;/code>/&lt;code>PHB&lt;/code> GPU↔NIC?) y los contadores de la NIC. Encaja con la &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">observabilidad GPU con DCGM&lt;/a>: la GPU &amp;ldquo;al 60% sin razón&amp;rdquo; puede ser el host esperando paquetes que cruzan el socket.&lt;/p>
&lt;h2 id="trampas-y-cosas-que-no-son-lo-que-parecen">Trampas y cosas que no son lo que parecen&lt;/h2>
&lt;p>&lt;strong>Creer que cambiar a Cilium &amp;ldquo;ya optimiza la red&amp;rdquo;.&lt;/strong> Cilium eBPF sustituye a kube-proxy y Canal y rinde mejor de serie, pero el despliegue por defecto prioriza compatibilidad, no rendimiento. Sin &lt;code>irqbalance&lt;/code> 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.&lt;/p>
&lt;p>&lt;strong>Fijar las IRQ de la NIC a cores aislados.&lt;/strong> El error simétrico del knob 6 del post 3: si pones la afinidad de IRQ sobre &lt;code>isolcpus&lt;/code>, el softirq &lt;code>NET_RX&lt;/code> le roba ciclos a vLLM justo en los cores que aislaste para que nadie lo molestara. Las IRQ van a &lt;code>reserved-cpus&lt;/code>, siempre.&lt;/p>
&lt;p>&lt;strong>Asumir que el Topology Manager ya alinea la NIC.&lt;/strong> 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 &lt;code>single-numa-node&lt;/code> lo resuelva es esperar a algo que no está en su diseño.&lt;/p>
&lt;p>&lt;strong>Meter DRA/DRANET en producción ENS este trimestre.&lt;/strong> Es beta y joven. El movimiento sensato es pilotarlo en un nodo de laboratorio, medir &lt;code>all_reduce&lt;/code>/&lt;code>all_gather&lt;/code> 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.&lt;/p>
&lt;p>&lt;strong>BIG TCP / netkit sin leer los requisitos.&lt;/strong> 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 &lt;code>cilium status --verbose&lt;/code> antes de dar por hecho que está activo.&lt;/p>
&lt;p>&lt;strong>Confundir el datapath eBPF (kernel) con el agente Cilium (pod).&lt;/strong> &lt;code>cilium-agent&lt;/code> es un DaemonSet &lt;code>Burstable&lt;/code> que debe vivir en housekeeping (lo cubre &lt;code>system-reserved&lt;/code>). Pero el procesamiento del datapath corre en &lt;strong>softirq&lt;/strong>, gobernado por la afinidad de IRQ del host, &lt;strong>no&lt;/strong> por &lt;code>reserved-cpus&lt;/code>. Son dos cosas distintas; pinear bien el pod no pinea el softirq.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>La serie &amp;ldquo;por debajo del motor&amp;rdquo; 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 &lt;strong>localidad&lt;/strong> 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ó). Falta&lt;strong>ba&lt;/strong> la cuarta: &lt;strong>la red&lt;/strong>. 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. &lt;strong>Cilium eBPF&lt;/strong> sustituye kube-proxy y Canal por un datapath que rinde —si coordinas la afinidad de IRQ con &lt;code>isolcpus&lt;/code>/&lt;code>reserved-cpus&lt;/code>, una cuarta lista que alinear—, y &lt;strong>DRA/DRANET&lt;/strong> 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.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/hardening-secretos-stack-llm-soberano/">Hardening y secretos del stack LLM soberano: defensa en profundidad&lt;/a> — las NetworkPolicy default-deny y el mTLS con Cilium en el hardening del stack.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/pcie-topology-gpudirect-p2p-acs/">Los pasillos y el guardia: PCIe, GPUDirect P2P y ACS&lt;/a> — el GPUDirect RDMA que DRANET coloca NUMA-local lo rompe el ACS si fuerza el tráfico por el root complex; el bus por debajo de la localidad NIC↔GPU.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">El maître que solo te sienta si cabéis en una mesa: resource managers en RKE2&lt;/a> — el post 3, padre directo de éste: el Topology Manager pinnea CPU+memoria+GPU pero no la NIC; aquí se abre esa cuarta pata.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">NUMA, hugepages y aislamiento de CPU&lt;/a> — el post 2; la afinidad de IRQ de la NIC es una tercera lista que casar con &lt;code>isolcpus&lt;/code> y &lt;code>reserved-cpus&lt;/code>, y el softirq &lt;code>NET_RX&lt;/code> es el mismo jitter entrando por la red.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">NVLink, NVSwitch y NCCL&lt;/a> — 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.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode separados&lt;/a> — el caso que más castiga una NIC mal ubicada: el KV-cache prefill→decode viaja por RDMA y paga cada cruce de socket.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">El stack de inferencia LLM on-premise en siete capas&lt;/a> — el edificio completo; la red es el plano que sostiene la inferencia multinodo.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoescalado de LLMs en Kubernetes con KEDA&lt;/a> — cada réplica nueva no solo pasa por la admisión NUMA del kubelet; con DRA, también por la del &lt;code>ResourceClaim&lt;/code> de NIC.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning de inferencia on-premise&lt;/a> — el sizing gana una dimensión: cuántas NICs NUMA-locales a GPU tiene el chasis, no solo cuántas GPUs.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel&lt;/a> — la afinidad NUMA NIC↔acelerador se complica cuando el nodo mezcla GPUs, aceleradores y NICs heterogéneas.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU con DCGM&lt;/a> — cómo confirmar, métrica en mano, que la &amp;ldquo;GPU al 60%&amp;rdquo; no es el host esperando paquetes cruzando el socket.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/del-disco-a-la-hbm-cold-start-carga-modelo/">Del disco a la HBM: cold start y carga del modelo&lt;/a> — el mismo principio de &amp;ldquo;saca a la CPU del medio&amp;rdquo; que aquí da GPUDirect RDMA, aplicado al disco con GPUDirect Storage para cargar pesos directos NVMe→HBM.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/sm-cuda-streams-cuda-graphs-inferencia/">SM, CUDA streams y CUDA graphs&lt;/a> — bajado un piso más: una vez los datos están en HBM, qué pasa en el silicio que los ejecuta y por qué el decode se vuelve launch-bound.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/posts/aislar-agentes-ia-cliente-cluster/">El contratista con la llave maestra: aislar agentes de IA del workstation al cluster&lt;/a> — el otro uso de esta misma capa de kernel: sobre el datapath eBPF de Cilium, Tetragon engancha sus kprobes para observar y matar lo que hace un agente de IA en el cluster. Su &lt;a href="https://blog.lo0.es/posts/runbook-aislar-agentes-ia-bubblewrap-tetragon/">runbook&lt;/a> trae las &lt;code>TracingPolicy&lt;/code> concretas.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Cilium, &lt;em>Tuning Guide&lt;/em> (netkit, host-routing, BIG TCP, XDP, fijar IRQ y matar irqbalance): &lt;a href="https://docs.cilium.io/en/stable/operations/performance/tuning/">https://docs.cilium.io/en/stable/operations/performance/tuning/&lt;/a>.&lt;/li>
&lt;li>Cilium 1.19 (febrero 2026), &lt;em>Cilium at Ten Years&lt;/em> — endurecimiento de cifrado, políticas y observabilidad: &lt;a href="https://www.infoq.com/news/2026/02/cilium-119/">https://www.infoq.com/news/2026/02/cilium-119/&lt;/a>.&lt;/li>
&lt;li>Isovalent, &lt;em>Cilium 1.18&lt;/em> (IPv6, encrypted overlay, ingress bandwidth, policy perf): &lt;a href="https://isovalent.com/blog/post/cilium-1-18/">https://isovalent.com/blog/post/cilium-1-18/&lt;/a>.&lt;/li>
&lt;li>RKE2, &lt;em>Network Options&lt;/em> (Canal por defecto; Cilium con kube-proxy replacement): &lt;a href="https://docs.rke2.io/networking/basic_network_options">https://docs.rke2.io/networking/basic_network_options&lt;/a>.&lt;/li>
&lt;li>Kubernetes, &lt;em>Dynamic Resource Allocation&lt;/em>: &lt;a href="https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/">https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/&lt;/a>.&lt;/li>
&lt;li>Kubernetes blog, &lt;em>v1.36: More Drivers, New Features, and the Next Era of DRA&lt;/em> (mayo 2026): &lt;a href="https://kubernetes.io/blog/2026/05/07/kubernetes-v1-36-dra-136-updates/">https://kubernetes.io/blog/2026/05/07/kubernetes-v1-36-dra-136-updates/&lt;/a>.&lt;/li>
&lt;li>DRANET (kubernetes-sigs), driver DRA de red y paper &lt;em>The Kubernetes Network Driver Model&lt;/em> (+59,6% all_gather / +58,1% all_reduce): &lt;a href="https://github.com/kubernetes-sigs/dranet">https://github.com/kubernetes-sigs/dranet&lt;/a>.&lt;/li>
&lt;li>AKS Engineering, &lt;em>Optimizing RDMA performance for AI workloads on AKS with DRANET&lt;/em> (abril 2026): &lt;a href="https://blog.aks.azure.com/2026/04/01/dranet-rdma-optimization-for-ai-on-aks">https://blog.aks.azure.com/2026/04/01/dranet-rdma-optimization-for-ai-on-aks&lt;/a>.&lt;/li>
&lt;li>Linux network tuning — IRQ affinity, RSS/RPS/RFS y softirq NUMA: &lt;a href="https://andreaskaris.github.io/blog/networking/rss-irq-affinity-and-rps/">https://andreaskaris.github.io/blog/networking/rss-irq-affinity-and-rps/&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>