<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Hugepages on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/hugepages/</link><description>Recent content in Hugepages on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Sat, 06 Jun 2026 07:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/hugepages/index.xml" rel="self" type="application/rss+xml"/><item><title>La planta de al lado: NUMA, hugepages y aislamiento de CPU, o por qué tu GPU espera al kernel</title><link>https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/</link><pubDate>Sat, 06 Jun 2026 07:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/</guid><description>&lt;blockquote>
&lt;p>Segundo post de la serie &amp;ldquo;por debajo del motor&amp;rdquo;. El &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">primero&lt;/a> abrió el cable entre GPUs (NVLink/NCCL). Este baja al &lt;strong>host&lt;/strong>: los núcleos, la memoria y el kernel que rodean a esas GPUs y que, mal configurados, las dejan esperando. El &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">tercero&lt;/a> explicará cómo Kubernetes automatiza todo esto; aquí está la capa cruda, la que hay que entender antes de delegarla.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Un nodo con 4×H100 SXM es, físicamente, un servidor de &lt;strong>dos sockets&lt;/strong> = &lt;strong>dos dominios NUMA&lt;/strong>. Cada socket tiene sus núcleos, sus canales de memoria y carriles PCIe hacia &lt;strong>la mitad&lt;/strong> de las GPUs y NICs. La inferencia no es solo GPU: el &lt;strong>host&lt;/strong> hace trabajo en la ruta caliente de cada token —lanzar kernels CUDA, samplear el siguiente token, tokenizar, mover buffers &lt;em>pinned&lt;/em> entre host y GPU, correr los hilos de NCCL. Si esos hilos y su memoria caen en el socket que &lt;strong>no&lt;/strong> es local a la GPU, cada acceso cruza el enlace inter-socket (UPI/Infinity Fabric): &lt;strong>2-3× más lento y con picos de p99&lt;/strong>. Hay tres palancas del kernel que deciden la cola de latencia y que casi nadie toca: &lt;strong>locality&lt;/strong> (afinidad NUMA: que CPU, memoria, GPU y NIC estén en la misma &amp;ldquo;planta&amp;rdquo;), &lt;strong>page tables&lt;/strong> (hugepages: pocas páginas grandes en vez de millones de pequeñas, y &lt;em>pinned memory&lt;/em> para DMA), y &lt;strong>jitter&lt;/strong> (aislamiento de CPU con &lt;code>isolcpus&lt;/code>/&lt;code>nohz_full&lt;/code>/&lt;code>rcu_nocbs&lt;/code> + IRQ affinity, para que el kernel no interrumpa al hilo que lanza el siguiente kernel de decode). Este post explica el mecanismo, da los 10 knobs reales, y conecta con el interconnect y el decode latency-bound. Con escepticismo sobre qué knobs mueven la aguja en inferencia y cuáles son cargo-cult heredado del trading de baja latencia.&lt;/p>
&lt;h2 id="dónde-estás-el-host-por-debajo-del-cable">Dónde estás: el host por debajo del cable&lt;/h2>
&lt;div class="diagram" style="max-width:560px;margin:1.2rem auto;">
&lt;svg viewBox="0 0 560 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Estás en el host: NUMA, kernel, memoria, por debajo del interconnect">
&lt;text x="280" y="24" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">El stack vertical · estás en el host&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 · vLLM / SGLang (TP, batching)&lt;/text>
&lt;rect x="120" y="84" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="108" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">CUDA + NCCL (colectivos)&lt;/text>
&lt;rect x="120" y="128" width="320" height="38" rx="6" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.4"/>
&lt;text x="280" y="152" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">NVLink + NVSwitch (post anterior)&lt;/text>
&lt;rect x="120" y="172" width="320" height="58" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="3"/>
&lt;text x="280" y="196" text-anchor="middle" font-family="sans-serif" font-size="12.5" font-weight="700" fill="#1f5c34">ESTÁS AQUÍ · host: NUMA + kernel + memoria&lt;/text>
&lt;text x="280" y="214" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#2c6b42">núcleos, canales de memoria, scheduler, IRQs&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">Hardware · 2 sockets, PCIe, HBM&lt;/text>
&lt;text x="280" y="298" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#777">la GPU computa, pero el host lanza, samplea y mueve datos por token&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-la-oficina-de-dos-plantas">La analogía: la oficina de dos plantas&lt;/h2>
&lt;p>Imagina una consultora en un edificio de &lt;strong>dos plantas&lt;/strong>. En cada planta hay &lt;strong>mesas de trabajo&lt;/strong> (núcleos de CPU), un &lt;strong>archivo&lt;/strong> con los expedientes (la memoria de ese socket) y un &lt;strong>muelle de carga&lt;/strong> que conecta con el exterior (los carriles PCIe hacia las GPUs y las NICs de ese socket). Un analista trabaja rápido &lt;strong>mientras todo lo que necesita está en su planta&lt;/strong>: alarga el brazo y coge el expediente del archivo de al lado.&lt;/p>
&lt;p>El problema empieza cuando el analista está en la planta 1 pero su expediente está en el archivo de la planta 2. Cada vez que lo necesita, &lt;strong>coge el ascensor&lt;/strong>. El trabajo &amp;ldquo;funciona&amp;rdquo;, pero cada consulta cuesta un viaje. Si encima el muelle de carga por el que entran sus materiales (su GPU) está en la otra planta, &lt;strong>cada entrega cruza el edificio&lt;/strong>. Esto es &lt;strong>NUMA&lt;/strong>: acceso local (misma planta) es rápido; acceso remoto (otra planta, vía el enlace inter-socket) es 2-3× más lento.&lt;/p>
&lt;p>Y hay dos formas más de arruinar a ese analista aunque esté en la planta correcta:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>Interrumpirle constantemente.&lt;/strong> Cada pocos minutos, megafonía, un compañero que pregunta, una alarma de incendios de prueba. Cada interrupción le saca de concentración justo cuando iba a entregar. Esto es el &lt;strong>jitter del kernel&lt;/strong>: el tick del scheduler, las IRQs de dispositivos, los callbacks de RCU, que interrumpen al hilo de host justo cuando iba a lanzar el siguiente kernel de la GPU. El aislamiento de CPU es ponerle en un &lt;strong>despacho con el cartel de &amp;ldquo;no molestar&amp;rdquo;&lt;/strong>.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>Darle un índice de mil pestañas diminutas.&lt;/strong> Si para encontrar cada expediente tiene que buscar en un índice con un millón de entradas minúsculas, pierde tiempo en la búsqueda. Si el índice tiene &lt;strong>pocas entradas grandes&lt;/strong>, encuentra al instante. Esto son las &lt;strong>hugepages&lt;/strong>: páginas de 2 MB o 1 GB en vez de 4 KB reducen la presión sobre la TLB (el caché del índice de páginas).&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>La tesis: &lt;strong>la GPU es cara y rápida, pero pasa una fracción sorprendente del decode esperando al host.&lt;/strong> Si el host está en la planta equivocada, interrumpido, y buscando en un índice gigante, la GPU —el recurso de 30.000 € — espera. Las tres palancas de este post existen para que no espere.&lt;/p>
&lt;h2 id="el-mecanismo-qué-hace-el-host-en-la-ruta-del-token">El mecanismo: qué hace el host en la ruta del token&lt;/h2>
&lt;p>Es tentador pensar que en inferencia &amp;ldquo;la GPU lo hace todo&amp;rdquo;. No es cierto. Por cada token, el &lt;strong>host&lt;/strong> (CPU) hace, como mínimo:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Lanzar los kernels CUDA&lt;/strong> de cada operación. La GPU no decide qué ejecutar; el host le va poniendo kernels en la cola. En decode, donde cada kernel es corto, el host tiene que ir &lt;strong>por delante&lt;/strong> alimentando la cola; si el hilo de host se para, la GPU se queda sin trabajo: una &lt;strong>burbuja&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Samplear&lt;/strong> el siguiente token (argmax/top-p/top-k sobre los logits), que vuelve del device al host.&lt;/li>
&lt;li>&lt;strong>Tokenizar&lt;/strong> la entrada y &lt;strong>detokenizar&lt;/strong> la salida.&lt;/li>
&lt;li>&lt;strong>Mover buffers fijados&lt;/strong> (&lt;em>pinned&lt;/em>, page-locked) entre host y GPU por DMA: prompts, logits, y en configuraciones con offload, parte del KV cache.&lt;/li>
&lt;li>&lt;strong>Correr los hilos de NCCL&lt;/strong> que coordinan los colectivos del &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">post anterior&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Todo eso es trabajo de CPU y de memoria de host. Y todo eso sufre si:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>El proceso corre en el socket que no es local a su GPU&lt;/strong> → cada DMA y cada acceso a memoria cruza el inter-socket link.&lt;/li>
&lt;li>&lt;strong>El kernel interrumpe los hilos&lt;/strong> → burbujas en la cola de la GPU.&lt;/li>
&lt;li>&lt;strong>La memoria no está fijada o usa páginas pequeñas&lt;/strong> → page faults, fallos de TLB, y peor: si la memoria de la transferencia DMA no está pinned, el driver hace una copia intermedia.&lt;/li>
&lt;/ol>
&lt;h3 id="el-mapa-nvidia-smi-topo--m">El mapa: &lt;code>nvidia-smi topo -m&lt;/code>&lt;/h3>
&lt;p>Todo arranca por ver el mapa. &lt;code>nvidia-smi topo -m&lt;/code> muestra, para cada GPU, a qué &lt;strong>NUMA node&lt;/strong> y a qué &lt;strong>núcleos&lt;/strong> es local, y por qué tipo de camino habla con cada NIC y con cada otra GPU:&lt;/p>
&lt;pre tabindex="0">&lt;code> GPU0 GPU1 GPU2 GPU3 NIC0 CPU Affinity NUMA Affinity
GPU0 X NV18 NV18 NV18 PIX 0-31,64-95 0
GPU1 NV18 X NV18 NV18 SYS 0-31,64-95 0
GPU2 NV18 NV18 X NV18 SYS 32-63,96-127 1
GPU3 NV18 NV18 NV18 X SYS 32-63,96-127 1
&lt;/code>&lt;/pre>&lt;p>Léelo así: GPU0 y GPU1 son locales al &lt;strong>NUMA node 0&lt;/strong> (núcleos 0-31, 64-95); GPU2 y GPU3 al &lt;strong>NUMA node 1&lt;/strong>. &lt;code>NV18&lt;/code> entre GPUs = 18 enlaces NVLink (lo bueno, del post anterior). En la columna NIC: &lt;code>PIX&lt;/code> = un solo switch PCIe de por medio (óptimo para GPUDirect RDMA); &lt;code>SYS&lt;/code> = el camino cruza el inter-socket (lo peor). &lt;strong>La regla&lt;/strong>: el proceso que sirve sobre GPU0/1 debe pinnearse a los núcleos 0-31/64-95 y a la memoria del node 0; si además usa RDMA, querrás la NIC que esté en &lt;code>PIX&lt;/code> con su GPU.&lt;/p>
&lt;h2 id="las-matemáticas-que-importan-un-tick-del-kernel-es-una-burbuja-de-la-gpu">Las matemáticas que importan: un tick del kernel es una burbuja de la GPU&lt;/h2>
&lt;p>La cifra que conviene interiorizar no es la del ancho de banda, es la del &lt;strong>jitter&lt;/strong>. En decode, el host lanza muchos kernels cortos por token. Si el hilo de host que los lanza es &lt;strong>expropiado&lt;/strong> por el scheduler (un timer tick, una IRQ, un callback de RCU) durante $t_{\text{stall}}$, y la GPU vacía su cola en ese tiempo, aparece una &lt;strong>burbuja&lt;/strong>: la GPU para.&lt;/p>
&lt;p>Pon números. Un timer tick típico o el manejo de una IRQ cuesta del orden de &lt;strong>decenas de microsegundos&lt;/strong> de desvío. Si un kernel de decode dura ~50-100 µs y la cola lleva 2-3 kernels en vuelo, un stall de host de &lt;strong>50-100 µs&lt;/strong> vacía la cola y la GPU se queda parada hasta que el host se reanuda. Multiplica por la frecuencia de interrupciones de un kernel &lt;strong>no&lt;/strong> aislado (el tick por defecto es de 250-1000 Hz, más IRQs de red y disco): la cola de p99/p999 del TTFT y del inter-token se llena de estos episodios.&lt;/p>
&lt;p>$$ \text{jitter}&lt;em>{p99} \approx f&lt;/em>{\text{interrupciones}} \times t_{\text{stall}} \times \mathbb{1}[\text{cola GPU vaciada}] $$&lt;/p>
&lt;p>La intuición: en throughput medio apenas se nota (las burbujas se promedian), pero en &lt;strong>la cola&lt;/strong> —que es lo que un SLO mide— el jitter del kernel es un contribuyente de primer orden. Por eso el aislamiento de CPU, que nació en el trading de baja latencia, tiene sentido en el decode de LLMs: &lt;strong>es la misma física —un hilo crítico que no puede permitirse que el kernel lo pare&lt;/strong>.&lt;/p>
&lt;p>Y el coste NUMA, en paralelo: un acceso a memoria &lt;strong>remota&lt;/strong> (otra planta) tiene latencia ~1,5-2× la local y &lt;strong>la mitad&lt;/strong> de ancho de banda. Para los buffers pinned que se mueven por DMA en cada paso, y para las estructuras del scheduler de vLLM que viven en host, esa penalización se paga token a token.&lt;/p>
&lt;h2 id="las-tres-palancas-uno-a-uno">Las tres palancas, uno a uno&lt;/h2>
&lt;h3 id="locality-numa-que-todo-esté-en-la-misma-planta">Locality (NUMA): que todo esté en la misma planta&lt;/h3>
&lt;p>El objetivo es que el proceso de inferencia que usa GPU0/1 tenga sus &lt;strong>núcleos&lt;/strong>, su &lt;strong>memoria&lt;/strong> y (si aplica) su &lt;strong>NIC&lt;/strong> en el NUMA node 0. En crudo, sin Kubernetes:&lt;/p>
&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"># Pinnear proceso a node 0 (cores y memoria) para servir sobre GPU0/1&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">numactl --cpunodebind&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> --membind&lt;span class="o">=&lt;/span>&lt;span class="m">0&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> vllm serve meta-llama/Llama-3-70B --tensor-parallel-size &lt;span class="m">2&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>--membind=0&lt;/code> es la clave: fuerza que &lt;strong>toda&lt;/strong> la memoria del proceso se asigne en el node 0. Sin &lt;code>--membind&lt;/code>, el kernel puede colocar páginas en el node 1 bajo presión, y empiezas a pagar el ascensor sin saberlo.&lt;/p>
&lt;h3 id="page-tables-hugepages-pocas-páginas-grandes-y-memoria-fijada">Page tables (hugepages): pocas páginas grandes y memoria fijada&lt;/h3>
&lt;p>Dos cosas distintas bajo el mismo paraguas. Primero, &lt;strong>hugepages&lt;/strong> reducen la presión de TLB para los buffers grandes de host (pinned, KV offload). Segundo, &lt;strong>pinned memory&lt;/strong> (page-locked) es lo que permite DMA directo sin copia intermedia. La trampa silenciosa son las &lt;strong>transparent hugepages (THP)&lt;/strong>: su compactación en segundo plano causa &lt;strong>picos de latencia&lt;/strong>, justo lo que no quieres.&lt;/p>
&lt;h3 id="jitter-aislamiento-de-cpu-el-despacho-con-no-molestar">Jitter (aislamiento de CPU): el despacho con &amp;ldquo;no molestar&amp;rdquo;&lt;/h3>
&lt;p>Tres parámetros de arranque del kernel, coordinados:&lt;/p>
&lt;pre tabindex="0">&lt;code>isolcpus=2-31,66-95 # saca estos cores del balanceo del scheduler
nohz_full=2-31,66-95 # tickless: sin timer tick si hay 1 solo hilo runnable
rcu_nocbs=2-31,66-95 # offload de callbacks RCU a cores housekeeping
&lt;/code>&lt;/pre>&lt;p>&lt;code>isolcpus&lt;/code> aparta los cores; &lt;strong>tú&lt;/strong> tienes que pinnear los hilos de inferencia ahí (los cores no aislados, 0-1, quedan para el sistema). &lt;code>nohz_full&lt;/code> quita el tick periódico (solo funciona si hay &lt;strong>un único&lt;/strong> hilo runnable en el core). &lt;code>rcu_nocbs&lt;/code> saca de esos cores el trabajo de RCU. Y aparte, &lt;strong>IRQ affinity&lt;/strong>: mover las interrupciones de dispositivos fuera de los cores de inferencia.&lt;/p>
&lt;h2 id="los-10-knobs-donde-tocar">Los 10 knobs donde tocar&lt;/h2>
&lt;p>Ordenados por impacto/frecuencia. Casi todos son sysctl, parámetros de arranque del kernel o &lt;code>numactl&lt;/code>. La referencia de bajo nivel es la &lt;a href="https://rigtorp.se/low-latency-guide/">guía de low-latency de Rigtorp&lt;/a> y la &lt;a href="https://access.redhat.com/articles/3720611">doc de tiempo real de Red Hat&lt;/a>.&lt;/p>
&lt;h3 id="knob-1--nvidia-smi-topo--m-ver-el-mapa-antes-de-tocar-nada">Knob 1 — &lt;code>nvidia-smi topo -m&lt;/code>: ver el mapa antes de tocar nada&lt;/h3>
&lt;p>Igual que en el post del interconnect: &lt;strong>primero el mapa&lt;/strong>. Qué GPU es local a qué NUMA node y a qué cores, y qué camino (PIX/PHB/SYS) hay a cada NIC. Sin esto, cualquier pinning es a ciegas. La mitad de los problemas de &amp;ldquo;la inferencia tiene picos de latencia&amp;rdquo; son procesos corriendo en el socket equivocado sin que nadie lo haya mirado.&lt;/p>
&lt;h3 id="knob-2--numactl---cpunodebind---membind-pinnear-al-node-local">Knob 2 — &lt;code>numactl --cpunodebind --membind&lt;/code>: pinnear al node local&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">numactl --cpunodebind&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> --membind&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> &amp;lt;proceso&amp;gt; &lt;span class="c1"># cores Y memoria en node 0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">numactl --hardware &lt;span class="c1"># ver nodes, distancias, memoria libre&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El &lt;code>--membind&lt;/code> es lo que de verdad importa: sin él, la memoria se dispersa. Es el knob de mayor impacto en el throughput sostenido.&lt;/p>
&lt;h3 id="knob-3--kernelnuma_balancing0-apagar-la-migración-automática">Knob 3 — &lt;code>kernel.numa_balancing=0&lt;/code>: apagar la migración automática&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">sysctl -w kernel.numa_balancing&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El &lt;em>automatic NUMA balancing&lt;/em> del kernel migra páginas entre nodes intentando &amp;ldquo;acercarlas&amp;rdquo;, pero ese trabajo en segundo plano &lt;strong>causa jitter&lt;/strong> y, con la memoria ya pinneada por el knob 2, no aporta nada. En nodos de inferencia dedicados, apágalo.&lt;/p>
&lt;h3 id="knob-4--hugepages-explícitas-1-gb-para-buffers-de-host">Knob 4 — Hugepages explícitas (1 GB) para buffers de host&lt;/h3>
&lt;pre tabindex="0">&lt;code># Arranque del kernel, para KV offload / buffers pinned grandes
default_hugepagesz=1G hugepagesz=1G hugepages=32
&lt;/code>&lt;/pre>&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">grep Huge /proc/meminfo &lt;span class="c1"># verificar reserva&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Útil &lt;strong>cuando hay memoria de host en la ruta caliente&lt;/strong> (vLLM con &lt;code>--cpu-offload-gb&lt;/code>, o buffers pinned grandes). Si tu despliegue no toca host memory en caliente, las hugepages explícitas aportan poco —no las pongas por cargo-cult.&lt;/p>
&lt;h3 id="knob-5--thp-en-madvise-o-never-evitar-los-picos-de-compactación">Knob 5 — THP en &lt;code>madvise&lt;/code> o &lt;code>never&lt;/code>: evitar los picos de compactación&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="nb">echo&lt;/span> madvise &amp;gt; /sys/kernel/mm/transparent_hugepage/enabled
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> never &amp;gt; /sys/kernel/mm/transparent_hugepage/defrag
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Las transparent hugepages &lt;code>always&lt;/code> ahorran TLB pero su &lt;strong>compactación&lt;/strong> dispara latencia impredecible. Para cargas sensibles a la cola, &lt;code>madvise&lt;/code> (solo donde la app lo pide) o &lt;code>never&lt;/code> es lo recomendado. Es de los pocos knobs con consenso claro: &lt;strong>THP always es malo para la latencia&lt;/strong>.&lt;/p>
&lt;h3 id="knob-6--isolcpus-apartar-los-cores-de-inferencia-del-scheduler">Knob 6 — &lt;code>isolcpus&lt;/code>: apartar los cores de inferencia del scheduler&lt;/h3>
&lt;pre tabindex="0">&lt;code>isolcpus=2-31,66-95
&lt;/code>&lt;/pre>&lt;p>Saca esos cores del balanceo de carga del scheduler; el sistema (kernel threads, daemons) se queda en los no aislados. &lt;strong>Tienes que pinnear&lt;/strong> explícitamente los hilos de inferencia a los cores aislados (vía &lt;code>numactl&lt;/code>/&lt;code>taskset&lt;/code> o, en K8s, el CPU Manager del próximo post). Aislar sin pinnear no sirve de nada.&lt;/p>
&lt;h3 id="knob-7--nohz_full--rcu_nocbs-tickless-y-sin-rcu-en-los-cores-críticos">Knob 7 — &lt;code>nohz_full&lt;/code> + &lt;code>rcu_nocbs&lt;/code>: tickless y sin RCU en los cores críticos&lt;/h3>
&lt;pre tabindex="0">&lt;code>nohz_full=2-31,66-95 rcu_nocbs=2-31,66-95
&lt;/code>&lt;/pre>&lt;p>Quita el timer tick periódico y los callbacks de RCU de los cores de inferencia. &lt;strong>Dos avisos de la práctica&lt;/strong>: &lt;code>nohz_full&lt;/code> solo elimina el tick si hay &lt;strong>un único hilo runnable&lt;/strong> en el core (si pinneas dos hilos ahí, vuelve el tick); y &lt;code>nohz_full&lt;/code> &lt;strong>no es compatible con el driver &lt;code>intel_pstate&lt;/code>&lt;/strong> en algunas configuraciones —hay que validarlo, no asumirlo.&lt;/p>
&lt;h3 id="knob-8--irq-affinity-las-interrupciones-fuera-de-los-cores-de-inferencia">Knob 8 — IRQ affinity: las interrupciones, fuera de los cores de inferencia&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">systemctl stop irqbalance &lt;span class="c1"># o configurarlo para respetar isolcpus&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># mover IRQs de un dispositivo a los cores housekeeping (0-1)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="m">3&lt;/span> &amp;gt; /proc/irq/&amp;lt;N&amp;gt;/smp_affinity &lt;span class="c1"># máscara de cores 0-1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Una IRQ de red o disco que cae en un core de inferencia es una interrupción directa al hilo que alimenta la GPU. Muévelas a los cores housekeeping. (&lt;code>irqbalance&lt;/code> puede respetar &lt;code>isolcpus&lt;/code> automáticamente si está configurado).&lt;/p>
&lt;h3 id="knob-9--cpu-governor-performance--c-states">Knob 9 — CPU governor &lt;code>performance&lt;/code> + C-states&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">cpupower frequency-set -g performance
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># evitar que cores en idle entren en C-states profundos (latencia de wakeup)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cpupower idle-set -D &lt;span class="m">0&lt;/span> &lt;span class="c1"># o limitar la profundidad de C-state&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con el governor &lt;code>powersave&lt;/code>/&lt;code>ondemand&lt;/code>, un core que estaba en idle tarda en subir de frecuencia: latencia de wakeup justo cuando llega trabajo. &lt;code>performance&lt;/code> lo mantiene a tope. En servidores dedicados a inferencia, el ahorro energético no compensa la cola de latencia.&lt;/p>
&lt;h3 id="knob-10--bloqueo-de-memoria--swappiness0">Knob 10 — Bloqueo de memoria + &lt;code>swappiness=0&lt;/code>&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">sysctl -w vm.swappiness&lt;span class="o">=&lt;/span>&lt;span class="m">0&lt;/span> &lt;span class="c1"># no expulsar páginas de la inferencia a swap&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># y en la app / contenedor: ulimit -l unlimited (memlock) para pinned memory&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Una página de la inferencia que el kernel decide swapear a disco es un page fault de milisegundos en la ruta caliente. &lt;code>swappiness=0&lt;/code> y límites de &lt;code>memlock&lt;/code> adecuados (para que el driver pueda fijar memoria) cierran esa puerta. En K8s, esto se traduce en QoS &lt;code>Guaranteed&lt;/code> y límites de memoria —el puente al próximo post.&lt;/p>
&lt;h3 id="tabla-resumen">Tabla resumen&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Knob&lt;/th>
&lt;th>Mecanismo&lt;/th>
&lt;th>Qué ataca&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>&lt;code>nvidia-smi topo -m&lt;/code>&lt;/td>
&lt;td>diagnóstico&lt;/td>
&lt;td>ver afinidad GPU–NUMA–NIC&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>&lt;code>numactl --cpunodebind --membind&lt;/code>&lt;/td>
&lt;td>pinning&lt;/td>
&lt;td>locality (la palanca mayor)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;code>kernel.numa_balancing=0&lt;/code>&lt;/td>
&lt;td>sysctl&lt;/td>
&lt;td>jitter por migración de páginas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>hugepages 1G explícitas&lt;/td>
&lt;td>boot param&lt;/td>
&lt;td>TLB en buffers de host&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>THP &lt;code>madvise&lt;/code>/&lt;code>never&lt;/code>&lt;/td>
&lt;td>sysfs&lt;/td>
&lt;td>picos de compactación&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>&lt;code>isolcpus&lt;/code>&lt;/td>
&lt;td>boot param&lt;/td>
&lt;td>scheduler fuera de cores críticos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>&lt;code>nohz_full&lt;/code>+&lt;code>rcu_nocbs&lt;/code>&lt;/td>
&lt;td>boot param&lt;/td>
&lt;td>tick + RCU jitter&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>IRQ affinity&lt;/td>
&lt;td>&lt;code>/proc/irq&lt;/code>&lt;/td>
&lt;td>interrupciones de dispositivo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>governor &lt;code>performance&lt;/code>&lt;/td>
&lt;td>cpupower&lt;/td>
&lt;td>latencia de wakeup de frecuencia&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>&lt;code>swappiness=0&lt;/code> + memlock&lt;/td>
&lt;td>sysctl/ulimit&lt;/td>
&lt;td>page faults en caliente&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 el interconnect (post anterior).&lt;/strong> Los &lt;strong>hilos de host de NCCL&lt;/strong> quieren cores locales a su GPU; y para multinodo, la &lt;strong>NIC de RDMA&lt;/strong> debe estar en el camino &lt;code>PIX&lt;/code> con su GPU (knob 1). Un GPUDirect RDMA con la NIC bajo el otro socket pierde la mitad de su ventaja. NUMA y NVLink son la misma historia vista desde el host y desde el cable.&lt;/p>
&lt;p>&lt;strong>Con vLLM y el decode.&lt;/strong> El &lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">decode es latency-bound&lt;/a>: el hilo de host que alimenta la cola de kernels es exactamente el que el aislamiento de CPU protege. Y &lt;code>--cpu-offload-gb&lt;/code> de vLLM mete memoria de host en la ruta caliente, donde NUMA locality + hugepages (knobs 2, 4) pasan de &amp;ldquo;fino&amp;rdquo; a &amp;ldquo;crítico&amp;rdquo;. El &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">continuous batching&lt;/a> ayuda aquí también: más tokens por iteración amortizan tanto la latencia del colectivo como el coste fijo de los lanzamientos de host.&lt;/p>
&lt;p>&lt;strong>Con Kubernetes (post siguiente).&lt;/strong> Todo lo de este post se hace &lt;strong>a mano&lt;/strong> (numactl, taskset, parámetros de arranque). En producción no se hace a mano: el &lt;strong>kubelet&lt;/strong> lo automatiza con CPU Manager, Memory Manager y Topology Manager. El &lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">próximo post&lt;/a> es exactamente cómo se declara esto para que cada pod de vLLM nazca pinneado al NUMA node correcto, sin scripts.&lt;/p>
&lt;p>&lt;strong>Con la observabilidad.&lt;/strong> Los picos de p99 por jitter o por acceso remoto &lt;strong>se ven&lt;/strong>: en DCGM, baja utilización de GPU con la cola llena (burbujas); en métricas de sistema, tráfico inter-socket y CPU migrations. La &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">observabilidad GPU con DCGM&lt;/a> es donde se diagnostica un &amp;ldquo;la GPU está al 60 % y no sé por qué&amp;rdquo; que muchas veces es el host esperando.&lt;/p>
&lt;p>&lt;strong>Con capacity planning.&lt;/strong> Reservar cores para el sistema (housekeeping) y dedicar el resto a inferencia cambia el cálculo de cuántos pods/réplicas caben por nodo. El &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a> debe contar esos cores reservados, no asumir que las 128 vCPU están disponibles para servir.&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>Cargo-cult del trading de baja latencia.&lt;/strong> Muchas guías de &lt;code>isolcpus&lt;/code>/&lt;code>nohz_full&lt;/code> vienen del HFT, donde se exprime el último microsegundo. En inferencia LLM, el aislamiento de CPU &lt;strong>sí&lt;/strong> ayuda en la cola del decode, pero no esperes el milagro: si tu cuello de botella es el ancho de banda de HBM o el interconnect, aislar cores no mueve la aguja. Mide antes; aplica donde el host es el límite.&lt;/p>
&lt;p>&lt;strong>Aislar sin pinnear.&lt;/strong> &lt;code>isolcpus&lt;/code> saca los cores del scheduler, pero si no pinneas los hilos de inferencia ahí, esos cores quedan &lt;strong>vacíos&lt;/strong> y la inferencia corre en los housekeeping, peor que antes. Aislar y pinnear van siempre juntos.&lt;/p>
&lt;p>&lt;strong>&lt;code>--membind&lt;/code> olvidado.&lt;/strong> Pinnear cores pero no memoria (&lt;code>--cpunodebind&lt;/code> sin &lt;code>--membind&lt;/code>) deja que las páginas se dispersen al otro node bajo presión. El pinning de memoria es la mitad que más se olvida y la que más rinde.&lt;/p>
&lt;p>&lt;strong>THP &lt;code>always&lt;/code> &amp;ldquo;porque ahorra TLB&amp;rdquo;.&lt;/strong> Ahorra TLB y regala picos de latencia por compactación. Para cargas con SLO de cola, es un mal negocio. &lt;code>madvise&lt;/code>/&lt;code>never&lt;/code>.&lt;/p>
&lt;p>&lt;strong>&lt;code>nohz_full&lt;/code> con dos hilos en el core.&lt;/strong> El tickless solo funciona con un único hilo runnable por core. Si pinneas dos hilos de inferencia al mismo core aislado, el tick vuelve y has complicado el arranque del kernel para nada.&lt;/p>
&lt;p>&lt;strong>Suponer la topología en vez de leerla.&lt;/strong> Servidores distintos cablean GPUs y NICs a sockets distintos. &lt;code>nvidia-smi topo -m&lt;/code> y &lt;code>numactl --hardware&lt;/code> son la verdad; el diagrama del fabricante es orientativo. Léelo en &lt;strong>cada&lt;/strong> modelo de nodo.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>La GPU es el recurso caro, pero en decode pasa una parte sorprendente del tiempo esperando al host: a que lance el siguiente kernel, a que samplee, a que mueva un buffer. Si ese host está en la planta equivocada (NUMA remoto), interrumpido (jitter del kernel) o buscando en un índice gigante (páginas de 4 KB), la GPU se queda con la cola vacía y el p99 se dispara —sin que ningún dashboard de la API diga por qué. De los diez knobs, el primero (&lt;strong>leer el mapa con &lt;code>nvidia-smi topo -m&lt;/code>&lt;/strong>) y el segundo (&lt;strong>pinnear cores y memoria al node local con &lt;code>--membind&lt;/code>&lt;/strong>) resuelven la mayoría; el aislamiento de CPU (&lt;code>isolcpus&lt;/code>/&lt;code>nohz_full&lt;/code>/IRQ affinity) es la segunda capa, la que recorta la cola del decode, y tiene sentido &lt;strong>donde el host es el límite&lt;/strong>, no como ritual. La idea que reordena la intuición: la inferencia no es &amp;ldquo;todo GPU&amp;rdquo;; es un baile entre GPU y host, y el host baila mejor cerca, sin que le interrumpan, y con pocas páginas grandes. El próximo post enseña cómo Kubernetes coreografía ese baile para cada pod sin un solo script a mano.&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/">NVLink, NVSwitch y NCCL: el cable por el que pasa cada token&lt;/a> — el post anterior de la serie; los hilos de host de NCCL y la NIC de RDMA quieren la misma localidad NUMA que aquí se explica.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/">Resource managers de RKE2: cómo el kubelet pinnea NUMA por ti&lt;/a> — el siguiente post; la automatización declarativa de todo lo que aquí se hace a mano con numactl e isolcpus.&lt;/li>
&lt;li>&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; este post es el sótano (host/kernel) sobre el que se apoya todo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">Optimizaciones de decode en vLLM&lt;/a> — la fase latency-bound donde el jitter de host se convierte en cola de p99 y donde el aislamiento de CPU rinde.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> — batchear amortiza el coste fijo de los lanzamientos de host además del de los colectivos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU con DCGM&lt;/a> — cómo se ve una GPU &amp;ldquo;al 60 % sin razón&amp;rdquo; que en realidad es el host esperando: burbujas, migraciones de CPU, tráfico inter-socket.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning de inferencia on-premise&lt;/a> — por qué hay que descontar los cores housekeeping reservados del presupuesto de cómputo por nodo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel&lt;/a> — la afinidad NUMA se complica (y se vuelve más importante) cuando el nodo mezcla GPUs, aceleradores y NICs heterogéneas.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Erik Rigtorp, &lt;em>Low Latency Tuning Guide&lt;/em> (isolcpus, nohz_full, IRQ affinity, THP): &lt;a href="https://rigtorp.se/low-latency-guide/">https://rigtorp.se/low-latency-guide/&lt;/a>.&lt;/li>
&lt;li>Red Hat, &lt;em>Usage, constraints and implications of isolcpus=, nohz_full= and rcu_nocbs=&lt;/em>: &lt;a href="https://access.redhat.com/articles/3720611">https://access.redhat.com/articles/3720611&lt;/a>.&lt;/li>
&lt;li>NVIDIA, &lt;em>nvidia-smi topo -m&lt;/em> y matriz de afinidad GPU–NUMA–NIC (CUDA docs / Developer Forums): &lt;a href="https://forums.developer.nvidia.com/t/nvidia-smi-topo-m-revisited/216584">https://forums.developer.nvidia.com/t/nvidia-smi-topo-m-revisited/216584&lt;/a>.&lt;/li>
&lt;li>Chaim Rand, &lt;em>The Crucial Role of NUMA Awareness in High-Performance Deep Learning&lt;/em>: &lt;a href="https://chaimrand.medium.com/the-crucial-role-of-numa-awareness-in-high-performance-deep-learning-99ae3e8eb49a">https://chaimrand.medium.com/the-crucial-role-of-numa-awareness-in-high-performance-deep-learning-99ae3e8eb49a&lt;/a>.&lt;/li>
&lt;li>SUSE Labs, &lt;em>CPU Isolation – nohz_full (part 3)&lt;/em>: &lt;a href="https://www.suse.com/c/cpu-isolation-nohz_full-part-3/">https://www.suse.com/c/cpu-isolation-nohz_full-part-3/&lt;/a>.&lt;/li>
&lt;li>Linux kernel, &lt;em>Automatic NUMA Balancing&lt;/em> y &lt;em>Transparent Hugepage Support&lt;/em> (Documentation/admin-guide): &lt;a href="https://docs.kernel.org/admin-guide/mm/transhuge.html">https://docs.kernel.org/admin-guide/mm/transhuge.html&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>