<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kubelet on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/kubelet/</link><description>Recent content in Kubelet 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/kubelet/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></channel></rss>