<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Numa on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/numa/</link><description>Recent content in Numa on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Sat, 06 Jun 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/numa/index.xml" rel="self" type="application/rss+xml"/><item><title>El maître que solo te sienta si cabéis en una mesa: CPU, Memory y Topology Manager en RKE2</title><link>https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/</link><pubDate>Sat, 06 Jun 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/kubelet-resource-managers-rke2-numa/</guid><description>&lt;blockquote>
&lt;p>Cierre de la serie &amp;ldquo;por debajo del motor&amp;rdquo;. Vimos el &lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">cable entre GPUs&lt;/a> y el &lt;a href="https://blog.lo0.es/posts/numa-hugepages-aislamiento-cpu-inferencia/">host: NUMA, hugepages y aislamiento de CPU&lt;/a> hecho a mano. Aquí está la pieza que lo hace &lt;strong>declarativo y a escala&lt;/strong>: cómo el kubelet de RKE2 pinnea cada pod de vLLM al NUMA node correcto sin un solo script.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Pinnear NUMA con &lt;code>numactl&lt;/code>/&lt;code>isolcpus&lt;/code>/&lt;code>taskset&lt;/code> —lo del post anterior— &lt;strong>no escala&lt;/strong> a un cluster donde los pods nacen y mueren y hay decenas de nodos. El &lt;strong>kubelet&lt;/strong> lo automatiza con tres componentes que funcionan como &lt;em>Hint Providers&lt;/em> de un coordinador central, el &lt;strong>Topology Manager&lt;/strong>: el &lt;strong>CPU Manager&lt;/strong> (asigna CPUs &lt;strong>exclusivas&lt;/strong> a contenedores de pods &lt;code>Guaranteed&lt;/code> con CPU entera), el &lt;strong>Memory Manager&lt;/strong> (memoria y hugepages NUMA-local) y el &lt;strong>Device Manager&lt;/strong>/plugin de GPU (sabe qué GPU está en qué NUMA node). Con la política &lt;strong>&lt;code>single-numa-node&lt;/code>&lt;/strong>, el Topology Manager solo &lt;strong>admite&lt;/strong> el pod si sus CPUs, su memoria y su GPU caben en el &lt;strong>mismo&lt;/strong> dominio NUMA; si no caben, &lt;strong>rechaza&lt;/strong> el pod —admisión estricta, como el maître que no sienta a un grupo de ocho si no hay mesa de ocho. En RKE2 todo esto se configura con &lt;code>kubelet-arg&lt;/code> en &lt;code>/etc/rancher/rke2/config.yaml&lt;/code>. Este post explica el mecanismo, da los 10 knobs y desmonta los gotchas que rompen el pinning &lt;strong>en silencio&lt;/strong>: el fichero &lt;code>cpu_manager_state&lt;/code> que hay que borrar al cambiar de política, la QoS que tiene que ser exactamente &lt;code>Guaranteed&lt;/code>, y el &lt;code>reserved-cpus&lt;/code> que debe casar con el &lt;code>isolcpus&lt;/code> del host. Sobre un cluster genérico RKE2 con nodos 4×H100 SXM.&lt;/p>
&lt;h2 id="dónde-estás-la-orquestación-que-materializa-el-host">Dónde estás: la orquestación que materializa el host&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 la orquestación: el kubelet materializa el pinning del host">
&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 la orquestación&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)&lt;/text>
&lt;rect x="120" y="84" width="320" height="58" rx="6" fill="#dceede" stroke="#3c8c54" stroke-width="3"/>
&lt;text x="280" y="108" text-anchor="middle" font-family="sans-serif" font-size="12.5" font-weight="700" fill="#1f5c34">ESTÁS AQUÍ · kubelet: CPU/Memory/Topology Mgr&lt;/text>
&lt;text x="280" y="126" text-anchor="middle" font-family="sans-serif" font-size="10.5" fill="#2c6b42">pinning declarativo + admisión NUMA&lt;/text>
&lt;rect x="120" y="150" width="320" height="38" rx="6" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.4"/>
&lt;text x="280" y="174" 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="194" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="218" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">CUDA + NCCL + NVLink (post 1)&lt;/text>
&lt;rect x="120" y="240" width="320" height="38" rx="6" fill="#f4f4f4" stroke="#888" stroke-width="1.3"/>
&lt;text x="280" y="264" text-anchor="middle" font-family="sans-serif" font-size="12" fill="#333">Hardware · 2 sockets, 4×H100 SXM&lt;/text>
&lt;text x="280" y="300" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-style="italic" fill="#777">el kubelet traduce intención declarativa en el pinning crudo de abajo&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="la-analogía-el-maître-de-un-restaurante-con-mesas-que-no-se-juntan">La analogía: el maître de un restaurante con mesas que no se juntan&lt;/h2>
&lt;p>Un restaurante tiene mesas de distintos tamaños, y —regla de la casa— &lt;strong>las mesas no se juntan&lt;/strong>. Llega un grupo de ocho. El maître mira si hay &lt;strong>una sola mesa&lt;/strong> donde quepan los ocho. Si la hay, los sienta; si solo quedan mesas de cuatro, &lt;strong>no los acepta&lt;/strong> —prefiere rechazar la reserva a sentar al grupo partido en dos mesas separadas, porque sabe que la cena partida va mal.&lt;/p>
&lt;p>Ese maître es el &lt;strong>Topology Manager&lt;/strong> en política &lt;code>single-numa-node&lt;/code>. El &amp;ldquo;grupo&amp;rdquo; es un pod de inferencia que pide CPUs, memoria y una GPU. La &amp;ldquo;mesa&amp;rdquo; es un &lt;strong>NUMA node&lt;/strong>. El maître pregunta a tres ayudantes —¿hay CPUs libres en algún node? (CPU Manager), ¿hay memoria libre? (Memory Manager), ¿hay GPU libre? (Device Manager)— y solo &lt;strong>admite&lt;/strong> el pod si los tres recursos caben en &lt;strong>el mismo&lt;/strong> node. Si no, lo &lt;strong>rechaza&lt;/strong> (el pod queda en &lt;code>Failed&lt;/code> con &lt;code>TopologyAffinityError&lt;/code>), y el scheduler probará otro nodo.&lt;/p>
&lt;p>La diferencia con el post anterior: allí &lt;strong>tú&lt;/strong> eras el maître, sentando a mano a cada proceso con &lt;code>numactl&lt;/code>. Aquí el maître es el kubelet, y lo hace para &lt;strong>cada pod, en cada nodo, automáticamente, y rechazando lo que no cabe&lt;/strong>. Eso es lo que convierte el pinning artesanal en una propiedad declarativa del cluster.&lt;/p>
&lt;h2 id="el-mecanismo-hint-providers-y-el-coordinador">El mecanismo: Hint Providers y el coordinador&lt;/h2>
&lt;p>El Topology Manager no asigna recursos; &lt;strong>coordina&lt;/strong> a los que sí lo hacen. El flujo, cuando un pod &lt;code>Guaranteed&lt;/code> llega a un nodo:&lt;/p>
&lt;div class="diagram" style="max-width:800px;margin:1.2rem auto;">
&lt;svg viewBox="0 0 800 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Topology Manager coordina CPU, Memory y Device Manager">
&lt;defs>&lt;marker id="km" 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="400" y="24" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">Admisión de un pod Guaranteed · single-numa-node&lt;/text>
&lt;rect x="320" y="44" width="160" height="50" rx="9" fill="#e6ddf3" stroke="#7a5aa5" stroke-width="2"/>
&lt;text x="400" y="66" text-anchor="middle" font-family="sans-serif" font-size="12" font-weight="700" fill="#222">Topology Manager&lt;/text>
&lt;text x="400" y="83" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">coordinador · admite/rechaza&lt;/text>
&lt;!-- hint providers -->
&lt;rect x="60" y="150" width="180" height="64" rx="8" fill="#dceede" stroke="#3c8c54" stroke-width="1.6"/>
&lt;text x="150" y="174" text-anchor="middle" font-family="sans-serif" font-size="11.5" font-weight="700" fill="#222">CPU Manager&lt;/text>
&lt;text x="150" y="191" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">static: CPUs exclusivas&lt;/text>
&lt;text x="150" y="205" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">hint: ¿cores en node X?&lt;/text>
&lt;rect x="310" y="150" width="180" height="64" rx="8" fill="#f7efda" stroke="#c79a32" stroke-width="1.6"/>
&lt;text x="400" y="174" text-anchor="middle" font-family="sans-serif" font-size="11.5" font-weight="700" fill="#222">Memory Manager&lt;/text>
&lt;text x="400" y="191" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">Static: cpuset.mems&lt;/text>
&lt;text x="400" y="205" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">hint: ¿RAM/hugepages?&lt;/text>
&lt;rect x="560" y="150" width="180" height="64" rx="8" fill="#eef3fb" stroke="#4a6fa5" stroke-width="1.6"/>
&lt;text x="650" y="174" text-anchor="middle" font-family="sans-serif" font-size="11.5" font-weight="700" fill="#222">Device Manager&lt;/text>
&lt;text x="650" y="191" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">plugin GPU NVIDIA&lt;/text>
&lt;text x="650" y="205" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">hint: ¿GPU en node X?&lt;/text>
&lt;path d="M360,94 L170,150" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#km)"/>
&lt;path d="M400,94 L400,150" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#km)"/>
&lt;path d="M440,94 L630,150" fill="none" stroke="#666" stroke-width="1.4" marker-end="url(#km)"/>
&lt;p>&lt;text x="150" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#888">hint NUMA&lt;/text>
&lt;text x="400" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#888">hint NUMA&lt;/text>
&lt;text x="650" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#888">hint NUMA&lt;/text>
&lt;path d="M170,214 L380,255" fill="none" stroke="#999" stroke-width="1.2" marker-end="url(#km)" stroke-dasharray="3 2"/>
&lt;path d="M400,214 L400,255" fill="none" stroke="#999" stroke-width="1.2" marker-end="url(#km)" stroke-dasharray="3 2"/>
&lt;path d="M630,214 L420,255" fill="none" stroke="#999" stroke-width="1.2" marker-end="url(#km)" stroke-dasharray="3 2"/>&lt;/p>
&lt;rect x="250" y="262" width="300" height="56" rx="9" fill="#f4f4f4" stroke="#444" stroke-width="1.6"/>
&lt;text x="400" y="284" text-anchor="middle" font-family="sans-serif" font-size="11.5" font-weight="700" fill="#222">¿Los tres hints coinciden en 1 node?&lt;/text>
&lt;text x="335" y="304" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-weight="700" fill="#1f5c34">SÍ → admite&lt;/text>
&lt;text x="470" y="304" text-anchor="middle" font-family="sans-serif" font-size="10.5" font-weight="700" fill="#a85454">NO → rechaza&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Los tres managers son &lt;strong>Hint Providers&lt;/strong>: cada uno le dice al Topology Manager en qué NUMA node(s) podría satisfacer su parte. El Topology Manager calcula la &lt;strong>intersección&lt;/strong> y, según la política, decide:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>none&lt;/code>&lt;/strong> (default): no coordina; cada manager hace lo suyo sin alinear. Sin garantía NUMA.&lt;/li>
&lt;li>&lt;strong>&lt;code>best-effort&lt;/code>&lt;/strong>: intenta alinear en un node; si no puede, &lt;strong>admite igual&lt;/strong> (en el node que sea). Mejor que nada, sin garantía.&lt;/li>
&lt;li>&lt;strong>&lt;code>restricted&lt;/code>&lt;/strong>: si no logra alinear, &lt;strong>rechaza&lt;/strong> el pod. Estricto, pero permite afinidad multi-node si la intersección lo da.&lt;/li>
&lt;li>&lt;strong>&lt;code>single-numa-node&lt;/code>&lt;/strong>: exige que &lt;strong>todo&lt;/strong> quepa en &lt;strong>un único&lt;/strong> NUMA node, o rechaza. El más estricto y el que de verdad garantiza la localidad del post anterior.&lt;/li>
&lt;/ul>
&lt;p>Y dos &lt;strong>precondiciones&lt;/strong> sin las cuales nada de esto se activa:&lt;/p>
&lt;ol>
&lt;li>El pod tiene que ser &lt;strong>QoS &lt;code>Guaranteed&lt;/code>&lt;/strong>: &lt;code>requests == limits&lt;/code> en CPU y memoria, y &lt;strong>CPU entera&lt;/strong> (no &lt;code>500m&lt;/code>). Solo así el CPU Manager asigna CPUs exclusivas.&lt;/li>
&lt;li>El &lt;strong>CPU Manager&lt;/strong> tiene que estar en política &lt;strong>&lt;code>static&lt;/code>&lt;/strong> (no &lt;code>none&lt;/code>).&lt;/li>
&lt;/ol>
&lt;p>Sin esas dos, el Topology Manager no tiene nada que alinear y el pinning &lt;strong>no ocurre&lt;/strong> —aunque la política esté puesta. Es el gotcha nº1.&lt;/p>
&lt;h2 id="cómo-se-configura-en-rke2">Cómo se configura en RKE2&lt;/h2>
&lt;p>RKE2 pasa argumentos al kubelet con la clave &lt;strong>&lt;code>kubelet-arg&lt;/code>&lt;/strong> en &lt;code>/etc/rancher/rke2/config.yaml&lt;/code>. La configuración de referencia para nodos GPU de inferencia:&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"># /etc/rancher/rke2/config.yaml (en cada nodo agent con GPUs)&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">kubelet-arg&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="s2">&amp;#34;cpu-manager-policy=static&amp;#34;&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="s2">&amp;#34;topology-manager-policy=single-numa-node&amp;#34;&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="s2">&amp;#34;topology-manager-scope=pod&amp;#34;&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="s2">&amp;#34;memory-manager-policy=Static&amp;#34;&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="s2">&amp;#34;reserved-cpus=0-1,64-65&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># housekeeping; debe casar con el host&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="s2">&amp;#34;system-reserved=memory=8Gi&amp;#34;&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="s2">&amp;#34;kube-reserved=memory=4Gi&amp;#34;&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="s2">&amp;#34;reserved-memory=0:memory=4Gi;1:memory=4Gi&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># requerido por Memory Manager Static&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">node-label&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="s2">&amp;#34;fibercli.local/numa-pinned=true&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Tras desplegarlo: &lt;code>systemctl restart rke2-agent&lt;/code>. &lt;strong>Gotcha crítico&lt;/strong>: si el nodo ya corrió con &lt;code>cpu-manager-policy=none&lt;/code>, hay un fichero de estado &lt;code>/var/lib/kubelet/cpu_manager_state&lt;/code> que &lt;strong>fija la política antigua&lt;/strong>; cambiar el arg sin borrar ese fichero hace que el kubelet &lt;strong>falle al arrancar&lt;/strong> o ignore la nueva política. Hay que: parar el agent, &lt;code>rm /var/lib/kubelet/cpu_manager_state&lt;/code>, arrancar. (Lo mismo aplica a &lt;code>memory_manager_state&lt;/code>).&lt;/p>
&lt;p>Y el pod de vLLM, para ser elegible, &lt;strong>Guaranteed con CPU entera&lt;/strong>:&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="nt">resources&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">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># entero, no &amp;#34;16000m&amp;#34; fraccionado raro&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">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;200Gi&amp;#34;&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># TP=2 → 2 GPUs del mismo NUMA node&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">hugepages-1Gi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16Gi&amp;#34;&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">limits&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">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># == requests → QoS Guaranteed&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">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;200Gi&amp;#34;&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">nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;2&amp;#34;&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">hugepages-1Gi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16Gi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con esto, en un nodo con la config de arriba, el kubelet asigna 16 CPUs exclusivas del NUMA node donde están las 2 GPUs pedidas, su memoria local y las hugepages —o rechaza el pod si no caben juntas. El pinning artesanal del post anterior, ahora declarativo.&lt;/p>
&lt;h2 id="los-10-knobs-donde-tocar">Los 10 knobs donde tocar&lt;/h2>
&lt;p>Ordenados por dependencia (los primeros son precondición de los siguientes). La referencia canónica es la &lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/">doc del Topology Manager de Kubernetes&lt;/a> y la &lt;a href="https://docs.rke2.io/install/configuration">config de RKE2&lt;/a>.&lt;/p>
&lt;h3 id="knob-1--cpu-manager-policystatic-el-cimiento">Knob 1 — &lt;code>cpu-manager-policy=static&lt;/code>: el cimiento&lt;/h3>
&lt;p>Sin esto, no hay CPUs exclusivas y nada de lo demás se activa.&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="nt">kubelet-arg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;cpu-manager-policy=static&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Gotcha&lt;/strong>: cambiarlo requiere borrar &lt;code>/var/lib/kubelet/cpu_manager_state&lt;/code> y reiniciar el kubelet, o el arranque falla. Es la causa nº1 de &amp;ldquo;puse la política y no pinnea&amp;rdquo;.&lt;/p>
&lt;h3 id="knob-2--qos-guaranteed--cpu-entera-la-precondición-del-pod">Knob 2 — QoS &lt;code>Guaranteed&lt;/code> + CPU entera: la precondición del pod&lt;/h3>
&lt;p>No es config de nodo, es del &lt;strong>pod&lt;/strong>, pero sin ella el knob 1 no hace nada para ese pod. &lt;code>requests == limits&lt;/code> en CPU y memoria, y CPU &lt;strong>entera&lt;/strong>. Un &lt;code>cpu: 500m&lt;/code> o un &lt;code>requests != limits&lt;/code> degrada el pod a &lt;code>Burstable&lt;/code> y pierde el pinning. Mucha gente pone la política de nodo y olvida la QoS del pod.&lt;/p>
&lt;h3 id="knob-3--topology-manager-policysingle-numa-node-admisión-estricta">Knob 3 — &lt;code>topology-manager-policy=single-numa-node&lt;/code>: admisión estricta&lt;/h3>
&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="nt">kubelet-arg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;topology-manager-policy=single-numa-node&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El maître estricto. Para inferencia con GPU, es la política correcta: garantiza que CPU+memoria+GPU comparten node. &lt;code>best-effort&lt;/code> no garantiza (admite desalineado); &lt;code>restricted&lt;/code> permite afinidad multi-node. Empieza por &lt;code>single-numa-node&lt;/code> y baja a &lt;code>restricted&lt;/code> solo si tienes problemas de admisión.&lt;/p>
&lt;h3 id="knob-4--topology-manager-scopepod-agrupar-el-pod-entero">Knob 4 — &lt;code>topology-manager-scope=pod&lt;/code>: agrupar el pod entero&lt;/h3>
&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="nt">kubelet-arg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;topology-manager-scope=pod&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Con scope &lt;code>container&lt;/code> (default), cada contenedor se alinea por separado; con scope &lt;code>pod&lt;/code>, &lt;strong>todo el pod&lt;/strong> va al mismo node. Para un pod de vLLM con sidecars (métricas, proxy), scope &lt;code>pod&lt;/code> evita que el sidecar arrastre el contenedor principal a otro node. Recomendado para inferencia.&lt;/p>
&lt;h3 id="knob-5--memory-manager-policystatic--reserved-memory-memoria-numa-local">Knob 5 — &lt;code>memory-manager-policy=Static&lt;/code> + &lt;code>reserved-memory&lt;/code>: memoria NUMA-local&lt;/h3>
&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="nt">kubelet-arg&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="s2">&amp;#34;memory-manager-policy=Static&amp;#34;&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="s2">&amp;#34;reserved-memory=0:memory=4Gi;1:memory=4Gi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El Memory Manager Static fuerza &lt;code>cpuset.mems&lt;/code> para que la memoria del pod salga del node correcto (y las hugepages). &lt;strong>Requiere&lt;/strong> declarar &lt;code>reserved-memory&lt;/code> por node, o el kubelet no arranca. Es el equivalente declarativo del &lt;code>--membind&lt;/code> del post anterior.&lt;/p>
&lt;h3 id="knob-6--reserved-cpus-los-cores-housekeeping-debe-casar-con-isolcpus">Knob 6 — &lt;code>reserved-cpus&lt;/code>: los cores housekeeping (debe casar con &lt;code>isolcpus&lt;/code>)&lt;/h3>
&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="nt">kubelet-arg&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;reserved-cpus=0-1,64-65&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reserva cores para el sistema y los daemons; el resto quedan para pods exclusivos. &lt;strong>Clave de la serie&lt;/strong>: estos &lt;code>reserved-cpus&lt;/code> deben ser &lt;strong>los mismos&lt;/strong> cores que dejaste fuera de &lt;code>isolcpus&lt;/code> en el host (post anterior). Si el host aísla 2-31 pero RKE2 reserva 0-3, hay un desajuste: cores aislados que el kubelet asigna a pods sin que estén realmente quietos. Coordina las dos capas.&lt;/p>
&lt;h3 id="knob-7--plugin-de-gpu-con-topología-numa-nvidia-gpu-operator">Knob 7 — Plugin de GPU con topología NUMA (NVIDIA GPU Operator)&lt;/h3>
&lt;p>El Device Manager solo puede dar un hint NUMA correcto si el &lt;strong>plugin de GPU expone en qué node está cada GPU&lt;/strong>. El NVIDIA device plugin / GPU Operator lo hace, pero hay que verificar que la información de topología llega (en algunas versiones requiere flags). Sin hint de GPU, el Topology Manager alinea CPU y memoria pero &lt;strong>no la GPU&lt;/strong> —y la localidad GPU es justo la que más importa.&lt;/p>
&lt;h3 id="knob-8--hugepages-como-recurso-del-pod">Knob 8 — hugepages como recurso del pod&lt;/h3>
&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="nt">resources&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">limits&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">hugepages-1Gi&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16Gi&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># el nodo debe tenerlas pre-reservadas (post 2, knob 4)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Las hugepages que reservaste en el arranque del host (post anterior) se piden como recurso. El Memory Manager las asigna NUMA-local. Si las pides sin haberlas reservado en el nodo, el pod no se programa.&lt;/p>
&lt;h3 id="knob-9--system-reserved--kube-reserved-no-sobre-suscribir">Knob 9 — &lt;code>system-reserved&lt;/code> / &lt;code>kube-reserved&lt;/code>: no sobre-suscribir&lt;/h3>
&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="nt">kubelet-arg&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="s2">&amp;#34;system-reserved=cpu=500m,memory=8Gi&amp;#34;&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="s2">&amp;#34;kube-reserved=cpu=500m,memory=4Gi&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Reserva recursos para el sistema y los componentes de K8s para que el nodo no se quede sin aire bajo carga. Mal calibrado, o el nodo se ahoga (poco reservado) o desperdicias capacidad (demasiado). Debe ser coherente con &lt;code>reserved-cpus&lt;/code>.&lt;/p>
&lt;h3 id="knob-10--labels--taints-que-vllm-caiga-aquí-y-lo-demás-no">Knob 10 — Labels + taints: que vLLM caiga aquí y lo demás no&lt;/h3>
&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"># nodo GPU: taint para repeler lo que no necesita 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="nt">node-taint&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;nvidia.com/gpu=present:NoSchedule&amp;#34;&lt;/span>&lt;span class="w"> &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">node-label&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;fibercli.local/numa-pinned=true&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Mantén los nodos NUMA-pinned para inferencia y echa de ahí lo que no la necesita (bases de datos, &lt;a href="https://blog.lo0.es/posts/langfuse-self-hosting-arquitectura-tuning/">el backend de Langfuse&lt;/a>, runners). Un ClickHouse robando ancho de banda de memoria a un pod de vLLM cuidadosamente pinneado tira por tierra todo el trabajo de los nueve knobs anteriores. El aislamiento de workloads es el cierre.&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>Dónde&lt;/th>
&lt;th>Función&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>&lt;code>cpu-manager-policy=static&lt;/code>&lt;/td>
&lt;td>kubelet-arg&lt;/td>
&lt;td>CPUs exclusivas (cimiento)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>QoS &lt;code>Guaranteed&lt;/code> + CPU entera&lt;/td>
&lt;td>pod spec&lt;/td>
&lt;td>precondición del pinning&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>&lt;code>topology-manager-policy=single-numa-node&lt;/code>&lt;/td>
&lt;td>kubelet-arg&lt;/td>
&lt;td>admisión estricta NUMA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>&lt;code>topology-manager-scope=pod&lt;/code>&lt;/td>
&lt;td>kubelet-arg&lt;/td>
&lt;td>agrupar pod entero&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>&lt;code>memory-manager-policy=Static&lt;/code>&lt;/td>
&lt;td>kubelet-arg&lt;/td>
&lt;td>memoria/hugepages NUMA-local&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>&lt;code>reserved-cpus&lt;/code>&lt;/td>
&lt;td>kubelet-arg&lt;/td>
&lt;td>housekeeping (casar con isolcpus)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>plugin GPU con topología&lt;/td>
&lt;td>GPU Operator&lt;/td>
&lt;td>hint NUMA de la GPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>&lt;code>hugepages-1Gi&lt;/code>&lt;/td>
&lt;td>pod spec&lt;/td>
&lt;td>hugepages como recurso&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>&lt;code>system/kube-reserved&lt;/code>&lt;/td>
&lt;td>kubelet-arg&lt;/td>
&lt;td>no sobre-suscribir&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>taints + labels&lt;/td>
&lt;td>config nodo&lt;/td>
&lt;td>aislar workloads GPU&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="verificar-que-el-pinning-de-verdad-ocurrió">Verificar que el pinning de verdad ocurrió&lt;/h2>
&lt;p>No te fíes de que la config &amp;ldquo;esté puesta&amp;rdquo;. Comprueba:&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"># ¿La política activa es la que pusiste?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">cat /var/lib/kubelet/cpu_manager_state &lt;span class="p">|&lt;/span> jq .policyName &lt;span class="c1"># &amp;#34;static&amp;#34;&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"># ¿Qué CPUs exclusivas tiene el contenedor?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> &amp;lt;pod&amp;gt; -- cat /sys/fs/cgroup/cpuset.cpus.effective
&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"># Dentro del pod: ¿la GPU asignada es local a esos cores?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">exec&lt;/span> &amp;lt;pod&amp;gt; -- nvidia-smi topo -m
&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"># ¿Hubo rechazos por topología?&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl describe pod &amp;lt;pod&amp;gt; &lt;span class="p">|&lt;/span> grep -i TopologyAffinityError
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Un pod en &lt;code>Failed&lt;/code> con &lt;code>TopologyAffinityError&lt;/code> no es un bug: es el maître &lt;strong>haciendo su trabajo&lt;/strong> —ese nodo no tenía una mesa donde cupieran CPU+memoria+GPU juntas. La respuesta es revisar el sizing del pod o del nodo, no relajar la política a la ligera.&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 anterior).&lt;/strong> Este post es la &lt;strong>automatización declarativa&lt;/strong> de aquel. &lt;code>cpu-manager-policy=static&lt;/code> materializa el &lt;code>taskset&lt;/code>; &lt;code>memory-manager-policy=Static&lt;/code> materializa el &lt;code>--membind&lt;/code>; &lt;code>reserved-cpus&lt;/code> debe casar con el &lt;code>isolcpus&lt;/code>. Las dos capas son &lt;strong>una sola decisión&lt;/strong> vista desde dos sitios: el host la ejecuta, el kubelet la declara. Descoordinarlas (isolcpus 2-31 vs reserved-cpus 0-3) rompe ambas.&lt;/p>
&lt;p>&lt;strong>Con el interconnect (post 1).&lt;/strong> El Topology Manager pinnea la &lt;strong>GPU correcta&lt;/strong> al pod, pero si pides 2 GPUs para TP=2, querrás que esas dos compartan NVLink. La política NUMA garantiza que están en el mismo socket; que estén NVLink-conectadas lo garantiza el hardware del baseboard (post 1, knob 1). Las dos cosas juntas son lo que hace que &lt;code>TP=2&lt;/code> rinda.&lt;/p>
&lt;p>&lt;strong>Con el autoscaling.&lt;/strong> Cuando &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">KEDA escala pods de vLLM&lt;/a>, cada réplica nueva pasa por la admisión del Topology Manager. Si el nodo no tiene una &amp;ldquo;mesa&amp;rdquo; libre, el pod queda pendiente —el autoscaling de pods y el de nodos (cluster-autoscaler) tienen que contar con la granularidad NUMA, no solo con CPU/memoria agregada.&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> cambia: no es &amp;ldquo;128 vCPU por nodo&amp;rdquo;, es &amp;ldquo;128 menos los reserved-cpus, en bloques que quepan por NUMA node&amp;rdquo;. Un nodo de 2 sockets × 64 cores no sirve un pod que pida 80 cores en single-numa-node: no caben en una mesa. El planning tiene que razonar por node, no por nodo.&lt;/p>
&lt;p>&lt;strong>Con la convivencia de servicios.&lt;/strong> El taint del knob 10 es lo que mantiene a &lt;a href="https://blog.lo0.es/posts/langfuse-self-hosting-arquitectura-tuning/">Langfuse&lt;/a>, bases de datos y runners &lt;strong>fuera&lt;/strong> de los nodos de inferencia. Sin esa frontera, todo el pinning fino se lo come un vecino ruidoso. La observabilidad va en sus nodos; la inferencia, pinneada, en los suyos.&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>Política puesta, QoS olvidada.&lt;/strong> El error más común: &lt;code>cpu-manager-policy=static&lt;/code> en el nodo pero el pod es &lt;code>Burstable&lt;/code> (&lt;code>requests != limits&lt;/code> o CPU fraccionada). El pinning &lt;strong>no ocurre&lt;/strong> y nadie avisa. La QoS &lt;code>Guaranteed&lt;/code> con CPU entera es condición necesaria.&lt;/p>
&lt;p>&lt;strong>&lt;code>cpu_manager_state&lt;/code> fosilizado.&lt;/strong> Cambiar de política sin borrar &lt;code>/var/lib/kubelet/cpu_manager_state&lt;/code> (y &lt;code>memory_manager_state&lt;/code>) hace que el kubelet falle o ignore el cambio. Parar agent → borrar fichero → arrancar.&lt;/p>
&lt;p>&lt;strong>&lt;code>reserved-cpus&lt;/code> ≠ &lt;code>isolcpus&lt;/code>.&lt;/strong> Si el host aísla unos cores y RKE2 reserva otros, los managers asignan a pods cores que no están realmente quietos, o dejan idle cores aislados. Las dos listas tienen que ser coherentes. Es el fallo de coordinación entre el post anterior y este.&lt;/p>
&lt;p>&lt;strong>Plugin de GPU sin topología NUMA.&lt;/strong> Si el device plugin no expone el NUMA node de cada GPU, el Topology Manager alinea CPU y memoria pero deja la GPU al azar —y la localidad de la GPU es la que más pesa. Verifica que el GPU Operator publica la topología.&lt;/p>
&lt;p>&lt;strong>&lt;code>single-numa-node&lt;/code> que rechaza demasiado.&lt;/strong> Si los pods piden más recursos de los que caben en un node (p. ej. más cores que los de un socket), el rechazo es constante. La respuesta no es bajar a &lt;code>best-effort&lt;/code> (que silencia el problema sirviendo desalineado), sino &lt;strong>dimensionar el pod para que quepa en una mesa&lt;/strong>, o aceptar &lt;code>restricted&lt;/code> con conocimiento de causa.&lt;/p>
&lt;p>&lt;strong>Creer que &lt;code>best-effort&lt;/code> &amp;ldquo;es casi igual&amp;rdquo;.&lt;/strong> &lt;code>best-effort&lt;/code> admite el pod aunque no logre alinear: te da la falsa sensación de NUMA-awareness mientras sirves desde el socket equivocado. Para inferencia con SLO de cola, &lt;code>single-numa-node&lt;/code> o &lt;code>restricted&lt;/code>; &lt;code>best-effort&lt;/code> solo si la alternativa es no programar nada.&lt;/p>
&lt;h2 id="conclusión">Conclusión&lt;/h2>
&lt;p>El post anterior pinneaba a mano; este lo hace a escala y con una garantía que el &lt;code>numactl&lt;/code> artesanal no daba: &lt;strong>admisión estricta&lt;/strong>. El kubelet, vía CPU Manager, Memory Manager y Topology Manager, actúa como un maître que solo sienta al pod si sus CPUs, su memoria y su GPU caben en la misma mesa NUMA, y que &lt;strong>rechaza&lt;/strong> lo que no cabe en vez de servir una cena partida. De los diez knobs, los dos primeros —&lt;strong>&lt;code>cpu-manager-policy=static&lt;/code>&lt;/strong> y &lt;strong>QoS &lt;code>Guaranteed&lt;/code> con CPU entera&lt;/strong>— son la precondición sin la cual los otros ocho no hacen nada, y son justo los que más se olvidan; el resto afina la política, la memoria, las hugepages y la convivencia. El hilo que cierra la serie: el rendimiento de inferencia que parecía un problema del motor (vLLM lento) o del modelo (cuantización) es, demasiadas veces, un problema del &lt;strong>cable&lt;/strong> (NVLink no usado), del &lt;strong>host&lt;/strong> (NUMA remoto, jitter) o de la &lt;strong>orquestación&lt;/strong> (pinning que no ocurrió porque la QoS estaba mal). Bajar de nivel no es esnobismo de infraestructura: es donde están las causas raíz que ningún dashboard de la capa de aplicación te va a señalar.&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/numa-hugepages-aislamiento-cpu-inferencia/">NUMA, hugepages y aislamiento de CPU&lt;/a> — el post anterior; la capa cruda (numactl, isolcpus, membind) que este automatiza de forma declarativa. &lt;code>reserved-cpus&lt;/code> aquí debe casar con &lt;code>isolcpus&lt;/code> allí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/nvlink-nvswitch-nccl-tensor-parallel/">NVLink, NVSwitch y NCCL&lt;/a> — el primero de la serie; la política NUMA pinnea la GPU correcta, pero que las GPUs de un TP compartan NVLink lo decide el hardware del baseboard.&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; la orquestación es la capa de control plane que sostiene a la inferencia.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoescalado de LLMs en Kubernetes con KEDA&lt;/a> — cada réplica que KEDA crea pasa por la admisión del Topology Manager; el autoscaling tiene que contar con la granularidad NUMA.&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é el sizing pasa a razonarse por NUMA node, no por nodo: un pod no cabe si pide más que una mesa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/langfuse-self-hosting-arquitectura-tuning/">Langfuse por dentro: arquitectura v3 y los 10 knobs de backend&lt;/a> — el tipo de workload que los taints del knob 10 mantienen &lt;strong>fuera&lt;/strong> de los nodos de inferencia pinneados.&lt;/li>
&lt;li>&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 el pinning se traduce en GPU saturada y sin burbujas.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Kubernetes, &lt;em>Control Topology Management Policies on a node&lt;/em>: &lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/">https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/&lt;/a>.&lt;/li>
&lt;li>Kubernetes, &lt;em>Control CPU Management Policies on the Node&lt;/em>: &lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/">https://kubernetes.io/docs/tasks/administer-cluster/cpu-management-policies/&lt;/a>.&lt;/li>
&lt;li>Kubernetes, &lt;em>Control Memory Management Policies on a Node&lt;/em>: &lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/memory-manager/">https://kubernetes.io/docs/tasks/administer-cluster/memory-manager/&lt;/a>.&lt;/li>
&lt;li>RKE2, &lt;em>Configuration Options&lt;/em> (kubelet-arg en config.yaml): &lt;a href="https://docs.rke2.io/install/configuration">https://docs.rke2.io/install/configuration&lt;/a>.&lt;/li>
&lt;li>RKE2, &lt;em>Advanced Options and Configuration&lt;/em>: &lt;a href="https://docs.rke2.io/advanced">https://docs.rke2.io/advanced&lt;/a>.&lt;/li>
&lt;li>rancher/rke2, discusión #3034 &lt;em>CPU Management Policies for RKE2&lt;/em> (el gotcha de cpu_manager_state): &lt;a href="https://github.com/rancher/rke2/discussions/3034">https://github.com/rancher/rke2/discussions/3034&lt;/a>.&lt;/li>
&lt;/ul></description></item><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>