<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Cpu-Manager on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/cpu-manager/</link><description>Recent content in Cpu-Manager on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sat, 06 Jun 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/cpu-manager/index.xml" rel="self" type="application/rss+xml"/><item><title>The maître d' who only seats you if you all fit at one table: CPU, Memory and Topology Manager on RKE2</title><link>https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/</link><pubDate>Sat, 06 Jun 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/cpu-memory-topology-manager-rke2/</guid><description>&lt;blockquote>
&lt;p>The close of the &amp;ldquo;under the engine&amp;rdquo; series. We saw the &lt;a href="https://blog.lo0.es/en/posts/nvlink-nvswitch-nccl-cable-every-token-crosses/">cable between GPUs&lt;/a> and the &lt;a href="https://blog.lo0.es/en/posts/numa-hugepages-cpu-isolation-gpu-waits-kernel/">host: NUMA, hugepages and CPU isolation&lt;/a> done by hand. Here is the piece that makes it &lt;strong>declarative and at scale&lt;/strong>: how the RKE2 kubelet pins each vLLM pod to the right NUMA node without a single script.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Pinning NUMA with &lt;code>numactl&lt;/code>/&lt;code>isolcpus&lt;/code>/&lt;code>taskset&lt;/code>, the subject of the previous post, &lt;strong>does not scale&lt;/strong> to a cluster where pods come and go and there are dozens of nodes. The &lt;strong>kubelet&lt;/strong> automates it with three components that work as &lt;em>Hint Providers&lt;/em> for a central coordinator, the &lt;strong>Topology Manager&lt;/strong>: the &lt;strong>CPU Manager&lt;/strong> (assigns exclusive CPUs to containers of &lt;code>Guaranteed&lt;/code> pods with whole CPUs), the &lt;strong>Memory Manager&lt;/strong> (NUMA-local memory and hugepages) and the Device Manager/GPU plugin (which knows which GPU is on which NUMA node). With the &lt;strong>&lt;code>single-numa-node&lt;/code>&lt;/strong> policy, the Topology Manager only admits the pod if its CPUs, its memory and its GPU fit in the same NUMA domain; if they do not fit, it rejects the pod, strict admission, like the maître d&amp;rsquo; who will not seat a party of eight unless there is a table for eight. On RKE2 all of this is configured with &lt;code>kubelet-arg&lt;/code> in &lt;code>/etc/rancher/rke2/config.yaml&lt;/code>. This post explains the mechanism, gives the 10 knobs and takes apart the gotchas that break pinning silently: the &lt;code>cpu_manager_state&lt;/code> file you have to delete when changing policy, the QoS that has to be exactly &lt;code>Guaranteed&lt;/code>, and the &lt;code>reserved-cpus&lt;/code> that must match the host&amp;rsquo;s &lt;code>isolcpus&lt;/code>. On a generic RKE2 cluster with 4×H100 SXM nodes.&lt;/p>
&lt;h2 id="where-you-are-the-orchestration-that-materialises-the-host">Where you are: the orchestration that materialises the 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="You are at the orchestration layer: the kubelet materialises host pinning">
&lt;text x="280" y="24" text-anchor="middle" font-family="sans-serif" font-size="13" font-weight="700" fill="currentColor">The vertical stack · you are at orchestration&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">Engine · vLLM pod (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">YOU ARE HERE · 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">declarative pinning + NUMA admission&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">the kubelet turns declarative intent into the raw pinning below&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-the-maître-d-of-a-restaurant-whose-tables-cannot-be-pushed-together">The analogy: the maître d&amp;rsquo; of a restaurant whose tables cannot be pushed together&lt;/h2>
&lt;p>A restaurant has tables of different sizes and, house rule, &lt;strong>tables are never pushed together&lt;/strong>. A party of eight arrives. The maître d&amp;rsquo; looks for a single table where all eight fit. If there is one, he seats them; if only tables for four are left, &lt;strong>he does not accept them&lt;/strong>. He would rather turn the booking away than seat the group split across two separate tables, because he knows a split dinner goes badly.&lt;/p>
&lt;p>That maître d&amp;rsquo; is the &lt;strong>Topology Manager&lt;/strong> under the &lt;code>single-numa-node&lt;/code> policy. The &amp;ldquo;party&amp;rdquo; is an inference pod asking for CPUs, memory and a GPU. The &amp;ldquo;table&amp;rdquo; is a NUMA node. The maître d&amp;rsquo; asks three assistants (are there free CPUs on any node? the CPU Manager; is there free memory? the Memory Manager; is there a free GPU? the Device Manager) and only admits the pod if all three resources fit on the same node. If not, he rejects it (the pod ends up &lt;code>Failed&lt;/code> with &lt;code>TopologyAffinityError&lt;/code>), and the scheduler will try another node.&lt;/p>
&lt;p>The difference from the previous post: there you were the maître d&amp;rsquo;, seating each process by hand with &lt;code>numactl&lt;/code>. Here the maître d&amp;rsquo; is the kubelet, and it does it for &lt;strong>every pod, on every node, automatically, rejecting whatever does not fit&lt;/strong>. That is what turns artisanal pinning into a declarative property of the cluster.&lt;/p>
&lt;h2 id="the-mechanism-hint-providers-and-the-coordinator">The mechanism: Hint Providers and the coordinator&lt;/h2>
&lt;p>The Topology Manager does not assign resources; it coordinates the ones that do. The flow, when a &lt;code>Guaranteed&lt;/code> pod arrives at a node:&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 coordinates CPU, Memory and 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">Admission of a Guaranteed pod · 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">coordinator · admits/rejects&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: exclusive CPUs&lt;/text>
&lt;text x="150" y="205" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">hint: cores on 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">NVIDIA GPU plugin&lt;/text>
&lt;text x="650" y="205" text-anchor="middle" font-family="sans-serif" font-size="9.5" fill="#444">hint: GPU on 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;text x="150" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#888">NUMA hint&lt;/text>
&lt;text x="400" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#888">NUMA hint&lt;/text>
&lt;text x="650" y="240" text-anchor="middle" font-family="sans-serif" font-size="9" font-style="italic" fill="#888">NUMA hint&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;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">Do the three hints meet on 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">YES → admit&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 → reject&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The three managers are &lt;strong>Hint Providers&lt;/strong>: each one tells the Topology Manager on which NUMA node(s) it could satisfy its part. The Topology Manager computes the intersection and, depending on the policy, decides:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>none&lt;/code>&lt;/strong> (default): no coordination; each manager does its own thing without aligning. No NUMA guarantee.&lt;/li>
&lt;li>&lt;strong>&lt;code>best-effort&lt;/code>&lt;/strong>: it tries to align on one node; if it cannot, it admits anyway (on whichever node). Better than nothing, no guarantee.&lt;/li>
&lt;li>&lt;strong>&lt;code>restricted&lt;/code>&lt;/strong>: if it fails to align, it rejects the pod. Strict, but it allows multi-node affinity if the intersection provides it.&lt;/li>
&lt;li>&lt;strong>&lt;code>single-numa-node&lt;/code>&lt;/strong>: it requires everything to fit on a single NUMA node, or it rejects. The strictest one, and the one that genuinely guarantees the locality of the previous post.&lt;/li>
&lt;/ul>
&lt;p>And two &lt;strong>preconditions&lt;/strong> without which none of this kicks in:&lt;/p>
&lt;ol>
&lt;li>The pod has to be &lt;strong>QoS &lt;code>Guaranteed&lt;/code>&lt;/strong>: &lt;code>requests == limits&lt;/code> on CPU and memory, and whole CPUs (not &lt;code>500m&lt;/code>). Only then does the CPU Manager assign exclusive CPUs.&lt;/li>
&lt;li>The &lt;strong>CPU Manager&lt;/strong> has to be on the &lt;strong>&lt;code>static&lt;/code>&lt;/strong> policy (not &lt;code>none&lt;/code>).&lt;/li>
&lt;/ol>
&lt;p>Without those two, the Topology Manager has nothing to align and pinning &lt;strong>does not happen&lt;/strong>, even with the policy in place. That is gotcha no. 1.&lt;/p>
&lt;h2 id="how-it-is-configured-on-rke2">How it is configured on RKE2&lt;/h2>
&lt;p>RKE2 passes arguments to the kubelet with the &lt;strong>&lt;code>kubelet-arg&lt;/code>&lt;/strong> key in &lt;code>/etc/rancher/rke2/config.yaml&lt;/code>. The reference configuration for GPU inference nodes:&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 (on every agent node with 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; must match the 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"># required by 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>After deploying it: &lt;code>systemctl restart rke2-agent&lt;/code>. &lt;strong>Critical gotcha&lt;/strong>: if the node has already run with &lt;code>cpu-manager-policy=none&lt;/code>, there is a state file &lt;code>/var/lib/kubelet/cpu_manager_state&lt;/code> that pins the old policy; changing the arg without deleting that file makes the kubelet fail to start or ignore the new policy. You have to stop the agent, &lt;code>rm /var/lib/kubelet/cpu_manager_state&lt;/code>, and start again. (The same applies to &lt;code>memory_manager_state&lt;/code>.)&lt;/p>
&lt;p>And the vLLM pod, to be eligible, &lt;strong>Guaranteed with whole CPUs&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"># whole, not a weird fractional &amp;#34;16000m&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">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 on the same 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 → Guaranteed QoS&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>With this, on a node with the config above, the kubelet assigns 16 exclusive CPUs from the NUMA node where the 2 requested GPUs live, its local memory and the hugepages, or it rejects the pod if they do not fit together. The artisanal pinning of the previous post, now declarative.&lt;/p>
&lt;h2 id="the-10-knobs-to-reach-for">The 10 knobs to reach for&lt;/h2>
&lt;p>Ordered by dependency (the first ones are a precondition for the later ones). The canonical reference is the &lt;a href="https://kubernetes.io/docs/tasks/administer-cluster/topology-manager/">Kubernetes Topology Manager documentation&lt;/a> and the &lt;a href="https://docs.rke2.io/install/configuration">RKE2 configuration&lt;/a>.&lt;/p>
&lt;h3 id="knob-1--cpu-manager-policystatic-the-foundation">Knob 1 — &lt;code>cpu-manager-policy=static&lt;/code>: the foundation&lt;/h3>
&lt;p>Without this there are no exclusive CPUs and nothing else kicks in.&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>: changing it requires deleting &lt;code>/var/lib/kubelet/cpu_manager_state&lt;/code> and restarting the kubelet, or startup fails. It is cause no. 1 of &amp;ldquo;I set the policy and it does not pin&amp;rdquo;.&lt;/p>
&lt;h3 id="knob-2--guaranteed-qos--whole-cpus-the-pods-precondition">Knob 2 — &lt;code>Guaranteed&lt;/code> QoS + whole CPUs: the pod&amp;rsquo;s precondition&lt;/h3>
&lt;p>This is not node config, it is &lt;strong>pod&lt;/strong> config, but without it knob 1 does nothing for that pod. &lt;code>requests == limits&lt;/code> on CPU and memory, and whole CPUs. A &lt;code>cpu: 500m&lt;/code> or a &lt;code>requests != limits&lt;/code> downgrades the pod to &lt;code>Burstable&lt;/code> and it loses the pinning. Plenty of people set the node policy and forget the pod&amp;rsquo;s QoS.&lt;/p>
&lt;h3 id="knob-3--topology-manager-policysingle-numa-node-strict-admission">Knob 3 — &lt;code>topology-manager-policy=single-numa-node&lt;/code>: strict admission&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>The strict maître d&amp;rsquo;. For GPU inference it is the right policy: it guarantees that CPU+memory+GPU share a node. &lt;code>best-effort&lt;/code> guarantees nothing (it admits misaligned); &lt;code>restricted&lt;/code> allows multi-node affinity. Start with &lt;code>single-numa-node&lt;/code> and drop to &lt;code>restricted&lt;/code> only if you have admission problems.&lt;/p>
&lt;h3 id="knob-4--topology-manager-scopepod-group-the-whole-pod">Knob 4 — &lt;code>topology-manager-scope=pod&lt;/code>: group the whole 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">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>With &lt;code>container&lt;/code> scope (the default), each container is aligned separately; with &lt;code>pod&lt;/code> scope, the whole pod goes to the same node. For a vLLM pod with sidecars (metrics, proxy), &lt;code>pod&lt;/code> scope stops the sidecar from dragging the main container onto another node. Recommended for inference.&lt;/p>
&lt;h3 id="knob-5--memory-manager-policystatic--reserved-memory-numa-local-memory">Knob 5 — &lt;code>memory-manager-policy=Static&lt;/code> + &lt;code>reserved-memory&lt;/code>: NUMA-local memory&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>Memory Manager Static forces &lt;code>cpuset.mems&lt;/code> so that the pod&amp;rsquo;s memory comes out of the right node (and so do the hugepages). It requires declaring &lt;code>reserved-memory&lt;/code> per node, or the kubelet does not start. It is the declarative equivalent of the previous post&amp;rsquo;s &lt;code>--membind&lt;/code>.&lt;/p>
&lt;h3 id="knob-6--reserved-cpus-the-housekeeping-cores-must-match-isolcpus">Knob 6 — &lt;code>reserved-cpus&lt;/code>: the housekeeping cores (must match &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>It reserves cores for the system and the daemons; the rest are left for exclusive pods. &lt;strong>Key point of the series&lt;/strong>: these &lt;code>reserved-cpus&lt;/code> must be the same cores you left out of &lt;code>isolcpus&lt;/code> on the host (previous post). If the host isolates 2-31 but RKE2 reserves 0-3, there is a mismatch: isolated cores that the kubelet assigns to pods without them actually being quiet. Coordinate the two layers.&lt;/p>
&lt;h3 id="knob-7--gpu-plugin-with-numa-topology-nvidia-gpu-operator">Knob 7 — GPU plugin with NUMA topology (NVIDIA GPU Operator)&lt;/h3>
&lt;p>The Device Manager can only give a correct NUMA hint if the &lt;strong>GPU plugin exposes which node each GPU is on&lt;/strong>. The NVIDIA device plugin / GPU Operator does this, but you have to verify that the topology information arrives (in some versions it requires flags). Without a GPU hint, the Topology Manager aligns CPU and memory but not the GPU, and GPU locality is precisely the one that matters most.&lt;/p>
&lt;h3 id="knob-8--hugepages-as-a-pod-resource">Knob 8 — hugepages as a pod resource&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"># the node must have them pre-reserved (post 2, knob 4)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The hugepages you reserved at host boot (previous post) are requested as a resource. The Memory Manager assigns them NUMA-local. If you request them without having reserved them on the node, the pod does not get scheduled.&lt;/p>
&lt;h3 id="knob-9--system-reserved--kube-reserved-do-not-oversubscribe">Knob 9 — &lt;code>system-reserved&lt;/code> / &lt;code>kube-reserved&lt;/code>: do not oversubscribe&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>It reserves resources for the system and the K8s components so the node does not run out of air under load. Badly calibrated, either the node chokes (too little reserved) or you waste capacity (too much). It must be consistent with &lt;code>reserved-cpus&lt;/code>.&lt;/p>
&lt;h3 id="knob-10--labels--taints-so-vllm-lands-here-and-nothing-else-does">Knob 10 — Labels + taints: so vLLM lands here and nothing else does&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"># GPU node: taint to repel anything that does not need a 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>Keep the NUMA-pinned nodes for inference and push out whatever does not need it (databases, &lt;a href="https://blog.lo0.es/en/posts/langfuse-inside-sorting-centre-bottleneck/">the Langfuse backend&lt;/a>, runners). A ClickHouse stealing memory bandwidth from a carefully pinned vLLM pod throws away all the work of the previous nine knobs. Workload isolation is the closing move.&lt;/p>
&lt;h3 id="summary-table">Summary table&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Knob&lt;/th>
&lt;th>Where&lt;/th>
&lt;th>Function&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>exclusive CPUs (foundation)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>&lt;code>Guaranteed&lt;/code> QoS + whole CPUs&lt;/td>
&lt;td>pod spec&lt;/td>
&lt;td>pinning precondition&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>strict NUMA admission&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>group the whole pod&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>NUMA-local memory/hugepages&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 (match isolcpus)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>GPU plugin with topology&lt;/td>
&lt;td>GPU Operator&lt;/td>
&lt;td>NUMA hint for the 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 as a resource&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>do not oversubscribe&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>taints + labels&lt;/td>
&lt;td>node config&lt;/td>
&lt;td>isolate GPU workloads&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="verifying-that-the-pinning-really-happened">Verifying that the pinning really happened&lt;/h2>
&lt;p>Do not trust that the config &amp;ldquo;is in place&amp;rdquo;. Check:&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"># Is the active policy the one you set?&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"># Which exclusive CPUs does the container have?&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"># Inside the pod: is the assigned GPU local to those 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"># Were there topology rejections?&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>A pod in &lt;code>Failed&lt;/code> with &lt;code>TopologyAffinityError&lt;/code> is not a bug: it is the maître d&amp;rsquo; &lt;strong>doing his job&lt;/strong>. That node did not have a table where CPU+memory+GPU fitted together. The answer is to review the sizing of the pod or the node, not to relax the policy casually.&lt;/p>
&lt;h2 id="how-it-connects-with-the-rest-of-the-stack">How it connects with the rest of the stack&lt;/h2>
&lt;p>&lt;strong>With the host (previous post).&lt;/strong> This post is the &lt;strong>declarative automation&lt;/strong> of that one. &lt;code>cpu-manager-policy=static&lt;/code> materialises the &lt;code>taskset&lt;/code>; &lt;code>memory-manager-policy=Static&lt;/code> materialises the &lt;code>--membind&lt;/code>; &lt;code>reserved-cpus&lt;/code> must match &lt;code>isolcpus&lt;/code>. The two layers are one single decision seen from two places: the host executes it, the kubelet declares it. Uncoordinating them (isolcpus 2-31 vs reserved-cpus 0-3) breaks both.&lt;/p>
&lt;p>&lt;strong>With the interconnect (post 1).&lt;/strong> The Topology Manager pins the right GPU to the pod, but if you ask for 2 GPUs for TP=2, you will want those two to share NVLink. The NUMA policy guarantees they are on the same socket; that they are NVLink-connected is guaranteed by the baseboard hardware (post 1, knob 1). The two things together are what makes &lt;code>TP=2&lt;/code> perform.&lt;/p>
&lt;p>&lt;strong>With autoscaling.&lt;/strong> When &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">KEDA scales vLLM pods&lt;/a>, each new replica goes through Topology Manager admission. If the node has no free &amp;ldquo;table&amp;rdquo;, the pod stays pending. Pod autoscaling and node autoscaling (cluster-autoscaler) have to account for NUMA granularity, not just aggregate CPU/memory.&lt;/p>
&lt;p>&lt;strong>With capacity planning.&lt;/strong> &lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Sizing&lt;/a> changes: it is not &amp;ldquo;128 vCPU per machine&amp;rdquo;, it is &amp;ldquo;128 minus the reserved-cpus, in blocks that fit per NUMA node&amp;rdquo;. A machine with 2 sockets × 64 cores does not serve a pod asking for 80 cores under single-numa-node: they do not fit at one table. Planning has to reason per NUMA node, not per machine.&lt;/p>
&lt;p>&lt;strong>With service coexistence.&lt;/strong> The knob 10 taint is what keeps &lt;a href="https://blog.lo0.es/en/posts/langfuse-inside-sorting-centre-bottleneck/">Langfuse&lt;/a>, databases and runners &lt;strong>off&lt;/strong> the inference nodes. Without that boundary, all the fine-grained pinning gets eaten by a noisy neighbour. Observability goes on its own nodes; inference, pinned, on its own.&lt;/p>
&lt;h2 id="traps-and-things-that-are-not-what-they-seem">Traps and things that are not what they seem&lt;/h2>
&lt;p>&lt;strong>Policy in place, QoS forgotten.&lt;/strong> The most common mistake: &lt;code>cpu-manager-policy=static&lt;/code> on the node but the pod is &lt;code>Burstable&lt;/code> (&lt;code>requests != limits&lt;/code> or fractional CPU). Pinning &lt;strong>does not happen&lt;/strong> and nobody warns you. &lt;code>Guaranteed&lt;/code> QoS with whole CPUs is a necessary condition.&lt;/p>
&lt;p>&lt;strong>Fossilised &lt;code>cpu_manager_state&lt;/code>.&lt;/strong> Changing policy without deleting &lt;code>/var/lib/kubelet/cpu_manager_state&lt;/code> (and &lt;code>memory_manager_state&lt;/code>) makes the kubelet fail or ignore the change. Stop agent → delete file → start.&lt;/p>
&lt;p>&lt;strong>&lt;code>reserved-cpus&lt;/code> ≠ &lt;code>isolcpus&lt;/code>.&lt;/strong> If the host isolates some cores and RKE2 reserves others, the managers assign cores to pods that are not really quiet, or they leave isolated cores idle. The two lists have to be consistent. It is the coordination failure between the previous post and this one.&lt;/p>
&lt;p>&lt;strong>GPU plugin without NUMA topology.&lt;/strong> If the device plugin does not expose each GPU&amp;rsquo;s NUMA node, the Topology Manager aligns CPU and memory but leaves the GPU to chance, and GPU locality is the one that weighs most. Verify that the GPU Operator publishes the topology.&lt;/p>
&lt;p>&lt;strong>&lt;code>single-numa-node&lt;/code> that rejects too much.&lt;/strong> If the pods ask for more resources than fit on one node (for example, more cores than a socket has), rejection is constant. The answer is not to drop to &lt;code>best-effort&lt;/code> (which silences the problem by serving misaligned), but to &lt;strong>size the pod so it fits at one table&lt;/strong>, or to accept &lt;code>restricted&lt;/code> with full knowledge of what you are doing.&lt;/p>
&lt;p>&lt;strong>Believing &lt;code>best-effort&lt;/code> &amp;ldquo;is nearly the same&amp;rdquo;.&lt;/strong> &lt;code>best-effort&lt;/code> admits the pod even if it fails to align: it gives you a false sense of NUMA-awareness while you serve from the wrong socket. For inference with a tail SLO, &lt;code>single-numa-node&lt;/code> or &lt;code>restricted&lt;/code>; &lt;code>best-effort&lt;/code> only if the alternative is scheduling nothing at all.&lt;/p>
&lt;h2 id="conclusion">Conclusion&lt;/h2>
&lt;p>The previous post pinned by hand; this one does it at scale and with a guarantee that artisanal &lt;code>numactl&lt;/code> did not give: &lt;strong>strict admission&lt;/strong>. The kubelet, via CPU Manager, Memory Manager and Topology Manager, acts as a maître d&amp;rsquo; who only seats the pod if its CPUs, its memory and its GPU fit at the same NUMA table, and who rejects what does not fit instead of serving a split dinner. Of the ten knobs, the first two, &lt;strong>&lt;code>cpu-manager-policy=static&lt;/code>&lt;/strong> and &lt;strong>&lt;code>Guaranteed&lt;/code> QoS with whole CPUs&lt;/strong>, are the precondition without which the other eight do nothing, and they are exactly the ones most often forgotten; the rest tune the policy, the memory, the hugepages and the coexistence. The thread that closes the series: the inference performance that looked like an engine problem (vLLM is slow) or a model problem (quantisation) is, far too often, a cable problem (NVLink not used), a host problem (remote NUMA, jitter) or an orchestration problem (pinning that never happened because the QoS was wrong). Going down a level is not infrastructure snobbery: it is where the root causes live that no application-layer dashboard is ever going to point out for you.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">Sharing one GPU between several workloads: time-slicing, MPS and MIG&lt;/a> — splitting the GPU (time-slicing/MPS/MIG) is the accelerator&amp;rsquo;s equivalent of what these managers do for CPU and memory.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/numa-hugepages-cpu-isolation-gpu-waits-kernel/">NUMA, hugepages and CPU isolation&lt;/a> — the previous post; the raw layer (numactl, isolcpus, membind) that this one automates declaratively. &lt;code>reserved-cpus&lt;/code> here must match &lt;code>isolcpus&lt;/code> there.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/nvlink-nvswitch-nccl-cable-every-token-crosses/">NVLink, NVSwitch and NCCL&lt;/a> — the first of the series; the NUMA policy pins the right GPU, but whether the GPUs of a TP group share NVLink is decided by the baseboard hardware.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">The on-premise LLM inference stack in seven layers&lt;/a> — the whole building; orchestration is the control plane layer that holds inference up.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">Autoscaling LLMs on Kubernetes with KEDA&lt;/a> — every replica KEDA creates goes through Topology Manager admission; autoscaling has to account for NUMA granularity.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise inference&lt;/a> — why sizing starts being reasoned per NUMA node, not per machine: a pod does not fit if it asks for more than one table.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/langfuse-inside-sorting-centre-bottleneck/">Langfuse from the inside: v3 architecture and the 10 backend knobs&lt;/a> — the kind of workload that the knob 10 taints keep &lt;strong>off&lt;/strong> the pinned inference nodes.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability with DCGM&lt;/a> — how to confirm, with metrics in hand, that the pinning translates into a saturated GPU with no bubbles.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/network-numa-cilium-ebpf-dranet-fourth-leg-pinning/">The kitchen door the maître d&amp;rsquo; never looked at: network NUMA, Cilium eBPF and DRANET&lt;/a> — the coda: the Topology Manager pins CPU+memory+GPU but &lt;strong>not the NIC&lt;/strong>; that fourth leg (network locality, IRQ affinity, DRA/DRANET) is the one left out of the census here.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">From disk to HBM: cold start and model loading&lt;/a> — the NVMe also hangs off a PCIe root under a socket; its NUMA locality is the fifth list to align, and cold start is the real ceiling on the elasticity this pinning holds up.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&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 in 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, discussion #3034 &lt;em>CPU Management Policies for RKE2&lt;/em> (the cpu_manager_state gotcha): &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>