<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.1.1">Jekyll</generator><link href="https://hrishi.dev/feed.xml" rel="self" type="application/atom+xml" /><link href="https://hrishi.dev/" rel="alternate" type="text/html" /><updated>2026-09-30T03:06:08-05:00</updated><id>https://hrishi.dev/feed.xml</id><title type="html">tail -f /home/hrishi/curiosity-and-experience</title><subtitle>Containers and Kubernetes/OpenShift, Distributed systems, Unix/Linux systems, Data structures and algorithms and Cloud computing.</subtitle><entry><title type="html">From Power Button to Shell Prompt, Part 3: Automated Host Provisioning with Metal³, CAPM3, and Cluster API</title><link href="https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3.html" rel="alternate" type="text/html" title="From Power Button to Shell Prompt, Part 3: Automated Host Provisioning with Metal³, CAPM3, and Cluster API" /><published>2026-09-19T01:00:00-05:00</published><updated>2026-09-19T01:00:00-05:00</updated><id>https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3</id><content type="html" xml:base="https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3.html">&lt;p&gt;&lt;em&gt;Part 3 of a 3-part series on the Linux boot process. &lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html&quot;&gt;Part 1&lt;/a&gt; covered the seven stages of a disk boot;
&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html&quot;&gt;Part 2&lt;/a&gt; took them over the network with PXE, iPXE, and Metal³’s Bare Metal Operator and Ironic. This final
part puts Cluster API on top so a single manifest provisions a whole cluster.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html&quot;&gt;Part 2&lt;/a&gt;&lt;/strong&gt; ended with one host: you wrote a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.image&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.userData&lt;/code&gt; by hand,
BMO and Ironic network-booted the Ironic Python Agent, and the agent wrote the OS. That is fine for a handful of
machines. For a Kubernetes cluster you want to declare “three control-plane nodes and ten workers with this image”
and have the right hosts claimed from the inventory, addressed, imaged, joined, and released back to the pool when
they are deleted. That is what &lt;strong&gt;Cluster API (CAPI)&lt;/strong&gt; and its Metal³ provider &lt;strong&gt;CAPM3&lt;/strong&gt; add. They never touch a
BMC or a DHCP server themselves: everything they do ends in a patch to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt;, and the machinery from
Part 2 takes it from there.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Component&lt;/th&gt;
      &lt;th&gt;Role&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;**Cluster API (CAPI)**&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cluster&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KubeadmControlPlane&lt;/code&gt;. Renders the cloud-init that runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubeadm init&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;join&lt;/code&gt;.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**CAPM3**&lt;/td&gt;
      &lt;td&gt;Metal³’s CAPI provider. Maps a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; and renders its metadata and network data.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**BMO**&lt;/td&gt;
      &lt;td&gt;Bare Metal Operator. Owns the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; state machine; the only client of Ironic.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**Ironic, dnsmasq, httpd, DHCP relay, BMC, IPA**&lt;/td&gt;
      &lt;td&gt;The network-boot and image-write path from [Part 2](https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html). Unchanged here.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#the-metal3-provisioning-sequence&quot;&gt;The Metal3 Provisioning Sequence&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#two-state-machines-side-by-side&quot;&gt;Two State Machines Side by Side&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-metal3-object-model&quot;&gt;The Metal3 Object Model&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#troubleshooting-the-cluster-api-layer&quot;&gt;Troubleshooting the Cluster API Layer&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-metal3-provisioning-sequence&quot;&gt;The Metal3 Provisioning Sequence&lt;/h2&gt;

&lt;p&gt;Here is every call from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl apply&lt;/code&gt; of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cluster&lt;/code&gt; to a physical server showing up as a Ready &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt;. The
host sits on VLAN 20 and Ironic on VLAN 10, so the DHCP hops cross the relay covered in &lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html#crossing-vlans-the-dhcp-relay&quot;&gt;Part 2&lt;/a&gt;.&lt;/p&gt;

&lt;div style=&quot;overflow-x:auto;border:1px solid rgba(128,128,128,.35);border-radius:6px;padding:8px;margin:1em 0;&quot;&gt;
&lt;svg id=&quot;metal3-seq&quot; viewBox=&quot;0 0 1278 1764&quot; role=&quot;img&quot; style=&quot;display:block;min-width:1100px;width:100%;height:auto;&quot; aria-label=&quot;Sequence of calls from applying a Cluster manifest to a bare-metal node joining the cluster, across kubectl, Cluster API, CAPM3, BMO, Ironic, dnsmasq and httpd, the DHCP relay, the BMC, the host firmware, and the Ironic Python Agent&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&lt;style&gt;
#metal3-seq{--bg:#fdfdfd;--lane:#ffffff;--band:#e4eaf4;--note:#fff1cc;--net:#c8420a;--mono:ui-monospace,SFMono-Regular,Menlo,monospace;color:#151414;font-family:system-ui,-apple-system,&quot;Segoe UI&quot;,sans-serif}
@media (prefers-color-scheme: dark){#metal3-seq{--bg:#181a1b;--lane:#24282a;--band:#2b3441;--note:#3b2f12;--net:#ff8e55;color:#e8e6e3}}
&lt;/style&gt;
&lt;defs&gt;&lt;marker id=&quot;m3s-ah&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/marker&gt;&lt;marker id=&quot;m3s-ahn&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;var(--net)&quot; /&gt;&lt;/marker&gt;&lt;/defs&gt;
&lt;line x1=&quot;72&quot; y1=&quot;78&quot; x2=&quot;72&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;13&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;72&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;kubectl&lt;/text&gt;
&lt;text x=&quot;72&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;operator / GitOps&lt;/text&gt;
&lt;line x1=&quot;198&quot; y1=&quot;78&quot; x2=&quot;198&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;139&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;198&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;CAPI&lt;/text&gt;
&lt;text x=&quot;198&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;core + kubeadm bootstrap&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;78&quot; x2=&quot;324&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;265&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;324&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;CAPM3&lt;/text&gt;
&lt;text x=&quot;324&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;Metal3Machine ctrl&lt;/text&gt;
&lt;line x1=&quot;450&quot; y1=&quot;78&quot; x2=&quot;450&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;391&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;450&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;BMO&lt;/text&gt;
&lt;text x=&quot;450&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;BareMetalHost ctrl&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;78&quot; x2=&quot;576&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;517&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;576&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;Ironic&lt;/text&gt;
&lt;text x=&quot;576&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;API + conductor :6385&lt;/text&gt;
&lt;line x1=&quot;702&quot; y1=&quot;78&quot; x2=&quot;702&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;643&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;702&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;BMC&lt;/text&gt;
&lt;text x=&quot;702&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;IPMI / Redfish&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;78&quot; x2=&quot;828&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;769&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;828&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;dnsmasq / httpd&lt;/text&gt;
&lt;text x=&quot;828&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;DHCP · TFTP · HTTP 6180&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;78&quot; x2=&quot;954&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;895&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;954&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;DHCP relay&lt;/text&gt;
&lt;text x=&quot;954&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;router SVI, helper-address&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;78&quot; x2=&quot;1080&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;1021&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;1080&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;Host firmware&lt;/text&gt;
&lt;text x=&quot;1080&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;PXE ROM → iPXE&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;78&quot; x2=&quot;1206&quot; y2=&quot;1744&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;1147&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;1206&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;IPA → OS&lt;/text&gt;
&lt;text x=&quot;1206&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;agent in RAM, then disk&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;84&quot; width=&quot;1262&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;102&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;0 · Inventory: happens once per host, before any cluster exists&lt;/text&gt;
&lt;line x1=&quot;72&quot; y1=&quot;134&quot; x2=&quot;450&quot; y2=&quot;134&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;261.0&quot; y=&quot;128&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;apply BareMetalHost + BMC Secret&lt;/text&gt;
&lt;line x1=&quot;450&quot; y1=&quot;170&quot; x2=&quot;576&quot; y2=&quot;170&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;513.0&quot; y=&quot;164&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;create node: driver, BMC addr, bootMAC&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;206&quot; x2=&quot;702&quot; y2=&quot;206&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;639.0&quot; y=&quot;200&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;boot=pxe (one-time), power on → inspection&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;242&quot; x2=&quot;1206&quot; y2=&quot;242&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; stroke-dasharray=&quot;5 4&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;1134.9&quot; y=&quot;236&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;same relayed PXE → iPXE → IPA chain as phase 3&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;278&quot; x2=&quot;576&quot; y2=&quot;278&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;272&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST /v1/continue_inspection (inventory)&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;314&quot; x2=&quot;450&quot; y2=&quot;314&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;513.0&quot; y=&quot;308&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;inspected → BMO fills status.hardware&lt;/text&gt;
&lt;rect x=&quot;442&quot; y=&quot;340&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;464&quot; y=&quot;354&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;BMH: Available · powered off&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;372&quot; width=&quot;1262&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;390&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;1 · Declare the cluster&lt;/text&gt;
&lt;line x1=&quot;72&quot; y1=&quot;422&quot; x2=&quot;198&quot; y2=&quot;422&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;297.0&quot; y=&quot;416&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;apply Cluster, Metal3Cluster, KubeadmControlPlane, Metal3MachineTemplate, Metal3DataTemplate, IPPool&lt;/text&gt;
&lt;rect x=&quot;190&quot; y=&quot;448&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;212&quot; y=&quot;462&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;Machine + KubeadmConfig → user-data Secret (cloud-init: kubeadm init)&lt;/text&gt;
&lt;line x1=&quot;198&quot; y1=&quot;494&quot; x2=&quot;324&quot; y2=&quot;494&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;261.0&quot; y=&quot;488&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;Metal3Machine created (infrastructureRef)&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;516&quot; width=&quot;1262&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;534&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;2 · Claim a host&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;566&quot; x2=&quot;450&quot; y2=&quot;566&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;387.0&quot; y=&quot;560&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;pick BMH matching hostSelector, set consumerRef&lt;/text&gt;
&lt;rect x=&quot;316&quot; y=&quot;592&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;338&quot; y=&quot;606&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;Metal3Data: metaData + networkData Secrets (IPClaim → IPPool)&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;638&quot; x2=&quot;450&quot; y2=&quot;638&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;387.0&quot; y=&quot;632&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;patch BMH.spec: image, userData, networkData, metaData, online=true&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;660&quot; width=&quot;1262&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;678&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;3 · Ironic boots the agent: host on VLAN 20, Ironic on VLAN 10, DHCP crosses the router via the helper&lt;/text&gt;
&lt;line x1=&quot;450&quot; y1=&quot;710&quot; x2=&quot;576&quot; y2=&quot;710&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;513.0&quot; y=&quot;704&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;instance_info = image/checksum/configdrive; provision → active&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;746&quot; x2=&quot;828&quot; y2=&quot;746&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;702.0&quot; y=&quot;740&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;write pxelinux.cfg/&amp;lt;mac&amp;gt; (IPA boot script)&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;782&quot; x2=&quot;702&quot; y2=&quot;782&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;639.0&quot; y=&quot;776&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;set boot device PXE (one-time), power on&lt;/text&gt;
&lt;line x1=&quot;702&quot; y1=&quot;818&quot; x2=&quot;1080&quot; y2=&quot;818&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;812&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;power on, POST (5–10 min on big boxes)&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;854&quot; x2=&quot;954&quot; y2=&quot;854&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;1017.0&quot; y=&quot;848&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;DHCPDISCOVER broadcast  opt60 PXEClient, opt93 arch&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;890&quot; x2=&quot;828&quot; y2=&quot;890&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;884&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;unicast to helper target: giaddr=10.20.0.1, hops=1&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;926&quot; x2=&quot;954&quot; y2=&quot;926&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;920&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;OFFER: range picked by giaddr, ip, router, next-server, snponly.efi&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;962&quot; x2=&quot;1080&quot; y2=&quot;962&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;1017.0&quot; y=&quot;956&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;OFFER re-broadcast on VLAN 20 (REQUEST/ACK repeat this path)&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;998&quot; x2=&quot;828&quot; y2=&quot;998&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;954.0&quot; y=&quot;992&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;TFTP GET iPXE binary: routed unicast, no relay involved&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;1034&quot; x2=&quot;954&quot; y2=&quot;1034&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;1017.0&quot; y=&quot;1028&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;iPXE DHCPs again (opt175) → relayed the same way&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;1070&quot; x2=&quot;1080&quot; y2=&quot;1070&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;954.0&quot; y=&quot;1064&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;filename = http://ironic:6180/boot.ipxe  (via relay)&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;1106&quot; x2=&quot;828&quot; y2=&quot;1106&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;954.0&quot; y=&quot;1100&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;HTTP GET boot.ipxe → pxelinux.cfg/&amp;lt;mac&amp;gt; → IPA kernel + initramfs&lt;/text&gt;
&lt;line x1=&quot;1080&quot; y1=&quot;1142&quot; x2=&quot;1206&quot; y2=&quot;1142&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;1117.8&quot; y=&quot;1136&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;kernel boots IPA  (ipa-api-url, agent token, BOOTIF)&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;1164&quot; width=&quot;1262&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;1182&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;4 · The agent writes the image&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;1214&quot; x2=&quot;954&quot; y2=&quot;1214&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3s-ahn)&quot; /&gt;
&lt;text x=&quot;1046.55&quot; y=&quot;1208&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;Linux DHCP (relayed again): needs option 3 router to reach Ironic off-subnet&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;1250&quot; x2=&quot;576&quot; y2=&quot;1250&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;1244&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST /v1/lookup, then heartbeat every ~10 s (callback_url :9999)&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;1286&quot; x2=&quot;1206&quot; y2=&quot;1286&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;1280&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST :9999/v1/commands  prepare_image {url, checksum, configdrive}&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;1322&quot; x2=&quot;828&quot; y2=&quot;1322&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;1017.0&quot; y=&quot;1316&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;GET image.raw  (httpd cache or your image server)&lt;/text&gt;
&lt;rect x=&quot;1198&quot; y=&quot;1348&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;1192&quot; y=&quot;1362&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;dd to rootDeviceHints device · write config-2 partition (user_data, meta_data, network_data)&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;1394&quot; x2=&quot;576&quot; y2=&quot;1394&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;1388&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;heartbeat: deploy steps done&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;1430&quot; x2=&quot;702&quot; y2=&quot;1430&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;639.0&quot; y=&quot;1424&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;boot device = disk (persistent), reboot&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;1466&quot; x2=&quot;450&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;513.0&quot; y=&quot;1460&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;node active → BMH Provisioned&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;1488&quot; width=&quot;1262&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;1506&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;5 · The OS boots from disk and joins&lt;/text&gt;
&lt;line x1=&quot;702&quot; y1=&quot;1538&quot; x2=&quot;1080&quot; y2=&quot;1538&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;891.0&quot; y=&quot;1532&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST, boot from disk: IPA is gone&lt;/text&gt;
&lt;rect x=&quot;1198&quot; y=&quot;1564&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;1192&quot; y=&quot;1578&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;cloud-init ConfigDrive: network_data → static IP on the baremetal net · user_data → kubeadm init / join&lt;/text&gt;
&lt;line x1=&quot;1206&quot; y1=&quot;1610&quot; x2=&quot;198&quot; y2=&quot;1610&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; stroke-dasharray=&quot;5 4&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;702.0&quot; y=&quot;1604&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;kubelet registers Node (label metal3.io/uuid=&amp;lt;bmh uid&amp;gt;) with the workload API&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;1646&quot; x2=&quot;1206&quot; y2=&quot;1646&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; stroke-dasharray=&quot;5 4&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;765.0&quot; y=&quot;1640&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;find Node by label, set Node.spec.providerID = metal3://ns/bmh/m3m&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;1682&quot; x2=&quot;198&quot; y2=&quot;1682&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3s-ah)&quot; /&gt;
&lt;text x=&quot;261.0&quot; y=&quot;1676&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;Metal3Machine ready=true → Machine Running&lt;/text&gt;
&lt;rect x=&quot;190&quot; y=&quot;1708&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;212&quot; y=&quot;1722&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;KCP initialized → MachineDeployment workers repeat phases 2–5, one host each&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;

&lt;p style=&quot;font-size:0.9em;opacity:.85;margin-top:-0.4em;&quot;&gt;Orange arrows are network boot (DHCP, TFTP, HTTP); black arrows are Kubernetes API, Ironic API, and BMC calls; dashed arrows go via the workload cluster API; yellow boxes are local state changes. Phase 0 runs once per host; phases 1-5 run for every Machine. Scroll sideways on narrow screens.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;What happens&lt;/th&gt;
      &lt;th&gt;Takes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;**0 · Inventory** ([Part 2](https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html#from-baremetalhost-to-provisioned-host))&lt;/td&gt;
      &lt;td&gt;You apply a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; and BMC Secret. BMO registers it in Ironic, which PXE-boots IPA once to inspect the hardware. Host ends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt;, powered off.&lt;/td&gt;
      &lt;td&gt;10-20 min, once per host&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**1 · Declare**&lt;/td&gt;
      &lt;td&gt;You apply &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cluster&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KubeadmControlPlane&lt;/code&gt;, and the Metal³ templates. CAPI creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; and a user-data Secret (cloud-init with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubeadm init&lt;/code&gt;).&lt;/td&gt;
      &lt;td&gt;seconds&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**2 · Claim**&lt;/td&gt;
      &lt;td&gt;CAPM3 picks an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt; host matching &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostSelector&lt;/code&gt;, renders its metadata and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;network_data.json&lt;/code&gt; (static IPs from an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPPool&lt;/code&gt;, NIC mapping from inspected MACs), and patches the BMH: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userData&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;networkData&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;online: true&lt;/code&gt;.&lt;/td&gt;
      &lt;td&gt;seconds&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**3 · Network boot** ([Part 2](https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html#stage-3-over-the-network-pxe-dhcp-tftp-and-ipxe))&lt;/td&gt;
      &lt;td&gt;BMO tells Ironic to deploy. Ironic writes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pxelinux.cfg/&amp;lt;mac&amp;gt;&lt;/code&gt;, asks the BMC for a one-time PXE boot, and powers the host on. POST, then the PXE → iPXE → IPA chain.&lt;/td&gt;
      &lt;td&gt;3-10 min, mostly POST&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**4 · Deploy** ([Part 2](https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html#from-baremetalhost-to-provisioned-host))&lt;/td&gt;
      &lt;td&gt;IPA DHCPs (third relayed round), looks itself up, and heartbeats to Ironic. Ironic calls back on port 9999 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepare_image&lt;/code&gt;; IPA streams the image to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rootDeviceHints&lt;/code&gt; device and writes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config-2&lt;/code&gt; partition. Ironic sets boot=disk on the BMC and reboots. BMH turns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioned&lt;/code&gt;.&lt;/td&gt;
      &lt;td&gt;2-6 min, image-size bound&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**5 · Boot and join**&lt;/td&gt;
      &lt;td&gt;Stages 1-7 from disk ([Part 1](https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html)). cloud-init’s ConfigDrive datasource applies the network data and runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubeadm&lt;/code&gt;. kubelet registers with label &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metal3.io/uuid&lt;/code&gt;; CAPM3 finds the Node, sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;providerID&lt;/code&gt;, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; goes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running&lt;/code&gt;.&lt;/td&gt;
      &lt;td&gt;3-10 min&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Phases 0, 3, and 4 are exactly the inventory, network boot, and deploy from Part 2, where you wrote &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.image&lt;/code&gt;
and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.userData&lt;/code&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; yourself. The new work is phases 1 and 2, where Cluster API and CAPM3
decide &lt;em&gt;what&lt;/em&gt; to write into the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt;, and the second half of phase 5, where the booted node is matched
back to its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;two-state-machines-side-by-side&quot;&gt;Two State Machines Side by Side&lt;/h3&gt;

&lt;p&gt;The Cluster API &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; sees three transitions. All the hardware work happens inside the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; row.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;BareMetalHost.status.provisioning.state  (BMO)

 registering → inspecting* → preparing* → available → provisioning* → provisioned
                                             ▲                             │
                                             └──── deprovisioning* ◄───────┘   Machine deleted
 * = IPA is booted over the network in this state

Machine.status.phase  (Cluster API)

 Pending ─────────────► Provisioning ────────────────────────────► Provisioned → Running
 bootstrap Secret       waiting for the BMH to reach provisioned     Node.spec.providerID
 ready, no host yet     and for the Node to appear                   matches
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A host stuck in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning&lt;/code&gt; past Ironic’s 30-minute deploy-callback timeout means the BMC powered the box on but
IPA never called home: one of the orange arrows is broken.&lt;/p&gt;

&lt;h3 id=&quot;what-crosses-the-wire-beyond-part-2&quot;&gt;What Crosses the Wire, Beyond Part 2&lt;/h3&gt;

&lt;p&gt;The &lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html#what-crosses-the-wire&quot;&gt;Part 2 table&lt;/a&gt; covers every port on the network-boot path. Cluster API adds one
more, and it is the one that tells you the boot succeeded but the join did not:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Direction&lt;/th&gt;
      &lt;th&gt;Port&lt;/th&gt;
      &lt;th&gt;Carries&lt;/th&gt;
      &lt;th&gt;If blocked&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;OS → control plane&lt;/td&gt;
      &lt;td&gt;6443/tcp&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubeadm join&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;BMH &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioned&lt;/code&gt;, Machine stuck &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Provisioning&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-metal3-object-model&quot;&gt;The Metal3 Object Model&lt;/h2&gt;

&lt;p&gt;The same objects, viewed as a composition instead of in time order. One provisioned host is a spine of six
objects, each owning or referencing a few helpers on its row. Read it top-down: that is the order they come into
existence. Everything from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; row down works without the rows above it.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Cluster ─────────────────── infrastructureRef ──► Metal3Cluster (controlPlaneEndpoint = your VIP)
  │ controlPlaneRef
  ▼
KubeadmControlPlane / MachineDeployment ── infrastructureRef ──► Metal3MachineTemplate (image, hostSelector, dataTemplate)
  │ one Machine per replica
  ▼
Machine ─────────────────── bootstrap.configRef ──► KubeadmConfig ──► Secret &amp;lt;machine&amp;gt; (cloud-init: kubeadm init|join)
  │ infrastructureRef
  ▼
Metal3Machine ───────────── spec.dataTemplate ──► Metal3DataTemplate (metaData, networkData templates)
  │                         owns Metal3Data ──► Secret &amp;lt;m3m&amp;gt;-metadata, Secret &amp;lt;m3m&amp;gt;-networkdata
  │                                └── owns IPClaim ──► IPAddress ◄── from IPPool
  │ claims a host (consumerRef)
  ▼
BareMetalHost ───────────── bmc.credentialsName ──► Secret (BMC username/password)
  │                         owns HardwareData (inspection), HostFirmwareSettings, HostFirmwareComponents
  │                         spec.image / userData / metaData / networkData  ◄── patched in by CAPM3 (or by you)
  │ BMO mirrors it into
  ▼
Ironic node (not a Kubernetes object) ── BMC + PXE / virtual media ──► physical host ──► Node in the workload cluster
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The two objects that carry the Metal³-specific decisions are the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3MachineTemplate&lt;/code&gt; (which image, which
hosts) and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3DataTemplate&lt;/code&gt;, which turns inspected hardware into per-host cloud-init data:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;infrastructure.cluster.x-k8s.io/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Metal3DataTemplate&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;test1-controlplane-template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;metal3&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;clusterName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test1&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;metaData&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;objectNames&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;machine&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}]&lt;/span&gt;         &lt;span class=&quot;c1&quot;&gt;# → ds.meta_data.name&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;fromHostInterfaces&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;provisioningMAC&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;interface&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;eth0&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}]&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;networkData&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;links&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;ethernets&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;phy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;enp1s0&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;macAddress&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fromHostInterface&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;eth1&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# from status.hardware.nics&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;networks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;ipv4&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;baremetal&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;link&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;enp1s0&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;ipAddressFromIPPool&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;baremetal-r01&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;routes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;network&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;0.0.0.0&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;prefix&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;gateway&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;fromIPPool&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;baremetal-r01&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Rules that follow from the composition:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Inventory survives.&lt;/strong&gt; Deleting a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; releases the host: BMO runs a cleaning boot and the BMH returns to
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt;. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt;, its BMC Secret, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPPool&lt;/code&gt;, and the templates are never deleted by Cluster API.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Templates are immutable.&lt;/strong&gt; Changing an image means a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3MachineTemplate&lt;/code&gt; name and a rolling
replacement from the pool.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The label matters.&lt;/strong&gt; The kubelet must register with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node-labels: &quot;metal3.io/uuid={{ ds.meta_data.uuid }}&quot;&lt;/code&gt;;
that is how CAPM3 finds the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt; in phase 5. Without it the OS boots and joins, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; never leaves
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Provisioning&lt;/code&gt;.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Names you can rely on.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;HardwareData&lt;/code&gt; and the firmware objects share the BMH’s name; the providerID is
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metal3://&amp;lt;ns&amp;gt;/&amp;lt;bmh&amp;gt;/&amp;lt;metal3machine&amp;gt;&lt;/code&gt; on both the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3Machine&lt;/code&gt; and the workload &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Node&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;troubleshooting-the-cluster-api-layer&quot;&gt;Troubleshooting the Cluster API Layer&lt;/h2&gt;

&lt;p&gt;Start with the &lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html#troubleshooting-a-network-boot&quot;&gt;network-boot table in Part 2&lt;/a&gt; whenever the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt;
is anywhere other than &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioned&lt;/code&gt;. The rows below are for the case where BMO and Ironic did their job and the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; still does not reach &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Running&lt;/code&gt;.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Symptom&lt;/th&gt;
      &lt;th&gt;Where to look&lt;/th&gt;
      &lt;th&gt;Usual cause&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; stuck &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pending&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3Machine&lt;/code&gt; activity&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get kubeadmconfig -o yaml&lt;/code&gt;, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DataSecretAvailable&lt;/code&gt; condition&lt;/td&gt;
      &lt;td&gt;The bootstrap Secret has not been rendered yet; for a worker, the control plane is not initialized&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3Machine&lt;/code&gt; never gets a host; CAPM3 logs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;No available host found. Requeuing.&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get bmh -A&lt;/code&gt; state column; labels vs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostSelector&lt;/code&gt; in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3MachineTemplate&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;No BMH in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt; matches &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostSelector&lt;/code&gt;, or every matching host is already consumed by another cluster&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Changed the image, existing nodes keep the old one, or the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3MachineTemplate&lt;/code&gt; edit is rejected&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.image&lt;/code&gt; on each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3Machine&lt;/code&gt; vs the template; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;infrastructureRef&lt;/code&gt; on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KubeadmControlPlane&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MachineDeployment&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Templates are immutable by design; a new template name referenced from the control plane or deployment triggers a rolling replacement from the pool&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;BMH &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioned&lt;/code&gt;, OS up, Machine stuck &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Provisioning&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cloud-init status --long&lt;/code&gt;; is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/disk/by-label/config-2&lt;/code&gt; there? Node labels&lt;/td&gt;
      &lt;td&gt;Config drive not read, wrong link/MAC in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;networkData&lt;/code&gt;, or the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metal3.io/uuid&lt;/code&gt; label missing&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Node is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ready&lt;/code&gt; in the workload cluster, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; stuck &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Provisioning&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get node -o jsonpath=&apos;{.spec.providerID}&apos;&lt;/code&gt; on the workload cluster; CAPM3 logs&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metal3.io/uuid&lt;/code&gt; label missing from the kubelet, so CAPM3 cannot find the Node to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;providerID&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. Which layer is stuck?&lt;/span&gt;
kubectl get cluster,kubeadmcontrolplane,machinedeployment,machine,metal3machine &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; metal3
kubectl get bmh &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; metal3 &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; custom-columns&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;NAME:.metadata.name,STATE:.status.provisioning.state,CONSUMER:.spec.consumerRef.name,ERR:.status.errorMessage
&lt;span class=&quot;c&quot;&gt;# 2. Did CAPM3 find a host, and what did it write?&lt;/span&gt;
kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; capm3-system logs deploy/capm3-controller-manager | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-iE&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;available host|consumerRef|providerID&apos;&lt;/span&gt;
kubectl get bmh &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; metal3 &amp;lt;host&amp;gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;jsonpath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;&apos;{.spec.image.url}{&quot;\n&quot;}{.spec.userData.name}{&quot;\n&quot;}&apos;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 3. Did the node come up with the right identity?&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;KUBECONFIG&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;workload.kubeconfig kubectl get nodes &lt;span class=&quot;nt&quot;&gt;-L&lt;/span&gt; metal3.io/uuid &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; custom-columns&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;NAME:.metadata.name,UUID:.metadata.labels.metal3&lt;span class=&quot;se&quot;&gt;\.&lt;/span&gt;io/uuid,PROVIDER:.spec.providerID
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;Three posts, one boot sequence. Part 1 followed it from the reset vector to a shell prompt on a machine with a disk.
Part 2 swapped Stage 3 for a network conversation and ran the sequence twice, once for a RAM-only agent and once for
the OS it wrote. This part added the layer that decides which hosts get which image and which cloud-init, and
matches the booted node back to the object that asked for it. Every layer above still ends in the same seven
stages, and when something goes wrong, the fastest question to ask is which stage, on which boot, on which hop.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;Cluster API and CAPM3 never touch hardware: every decision ends in a &lt;strong&gt;patch to a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt;&lt;/strong&gt;, and BMO and Ironic (Part 2) do the rest.&lt;/li&gt;
    &lt;li&gt;One provisioned host is a spine of six objects, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cluster → KubeadmControlPlane → Machine → Metal3Machine → BareMetalHost → Ironic node&lt;/code&gt;, and everything from the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; down works without the rows above it.&lt;/li&gt;
    &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3DataTemplate&lt;/code&gt; is where inspected hardware becomes per-host cloud-init: static IPs from an &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPPool&lt;/code&gt;, NIC mapping from inspected MACs.&lt;/li&gt;
    &lt;li&gt;The &lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;metal3.io/uuid&lt;/code&gt; node label&lt;/strong&gt; is how CAPM3 finds the Node in phase 5; without it the OS boots and joins, and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; never leaves &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Provisioning&lt;/code&gt;.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;Inventory survives.&lt;/strong&gt; Deleting a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; cleans the host and returns it to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt;; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt;, its BMC Secret, the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IPPool&lt;/code&gt;, and the templates stay.&lt;/li&gt;
    &lt;li&gt;Templates are immutable: a new image means a new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Metal3MachineTemplate&lt;/code&gt; name and a rolling replacement from the pool.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;</content><author><name></name></author><category term="Linux" /><category term="OS" /><category term="Systems" /><summary type="html">Part 3 of a 3-part series on the Linux boot process. Part 1 covered the seven stages of a disk boot; Part 2 took them over the network with PXE, iPXE, and Metal³’s Bare Metal Operator and Ironic. This final part puts Cluster API on top so a single manifest provisions a whole cluster.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hrishi.dev/assets/os-boot-process-stages.png" /><media:content medium="image" url="https://hrishi.dev/assets/os-boot-process-stages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">From Power Button to Shell Prompt, Part 2: PXE Network Boot with Metal³ Bare Metal Operator and Ironic</title><link href="https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html" rel="alternate" type="text/html" title="From Power Button to Shell Prompt, Part 2: PXE Network Boot with Metal³ Bare Metal Operator and Ironic" /><published>2026-09-18T01:00:00-05:00</published><updated>2026-09-18T01:00:00-05:00</updated><id>https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2</id><content type="html" xml:base="https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html">&lt;p&gt;&lt;em&gt;Part 2 of a 3-part series on the Linux boot process. &lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html&quot;&gt;Part 1&lt;/a&gt; covered the seven stages of booting from a
disk. This part takes those stages onto a blank server that has to fetch its bootloader, kernel, and operating
system over the network.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html&quot;&gt;Part 1&lt;/a&gt;&lt;/strong&gt; walked through a Linux boot from a disk: firmware, POST, bootloader, kernel init, initramfs,
systemd, and login. A server fresh from the rack has no disk to boot from. The firmware still starts at the reset
vector and still runs POST, but Stage 3 has nothing to read. Instead, the boot NIC asks the network for a
bootloader, the bootloader fetches a kernel and initramfs over HTTP, and the first Linux to run is a RAM-only agent
whose only job is to write an OS image to disk and reboot. Then Stages 1-7 run again, this time from disk.&lt;/p&gt;

&lt;p&gt;The worked example is &lt;strong&gt;&lt;a href=&quot;https://metal3.io/&quot;&gt;Metal³&lt;/a&gt;&lt;/strong&gt;, a Kubernetes-native bare-metal provisioner whose Bare
Metal Operator (BMO) drives &lt;strong&gt;&lt;a href=&quot;https://docs.openstack.org/ironic/latest/&quot;&gt;OpenStack Ironic&lt;/a&gt;&lt;/strong&gt; to boot, image, and
reboot physical hosts. This part stays at the level of one host: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.image&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.userData&lt;/code&gt; set by hand gets a network boot, an image write, and a cloud-init run, which is all it takes to
provision a database host, a GPU box, or a plain fleet of Linux servers. &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3.html&quot;&gt;Part 3&lt;/a&gt;&lt;/strong&gt; puts Cluster API on top
of the same machinery to build Kubernetes clusters. The PXE and DHCP mechanics are identical under Foreman, MAAS,
or Tinkerbell.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Component&lt;/th&gt;
      &lt;th&gt;Role&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;**BMO**&lt;/td&gt;
      &lt;td&gt;Bare Metal Operator. Owns the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; state machine; the only client of Ironic.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**Ironic pod**&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ironic&lt;/code&gt; (API, port 6385), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dnsmasq&lt;/code&gt; (DHCP, TFTP), &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;httpd&lt;/code&gt; (iPXE scripts and images, port 6180).&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**DHCP relay**&lt;/td&gt;
      &lt;td&gt;The router interface on each host VLAN with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ip helper-address &amp;lt;Ironic IP&amp;gt;&lt;/code&gt;.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**BMC**&lt;/td&gt;
      &lt;td&gt;IPMI or Redfish on the host. Power and boot-device control only.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**IPA**&lt;/td&gt;
      &lt;td&gt;Ironic Python Agent: a RAM-only Linux that writes the image and the config drive.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-3-over-the-network-pxe-dhcp-tftp-and-ipxe&quot;&gt;Stage 3, Over the Network: PXE, DHCP, TFTP, and iPXE&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#crossing-vlans-the-dhcp-relay&quot;&gt;Crossing VLANs: The DHCP Relay&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#from-baremetalhost-to-provisioned-host&quot;&gt;From BareMetalHost to Provisioned Host&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#mapping-the-phases-back-to-the-seven-stages&quot;&gt;Mapping the Phases Back to the Seven Stages&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#the-baremetalhost-state-machine&quot;&gt;The BareMetalHost State Machine&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#what-crosses-the-wire&quot;&gt;What Crosses the Wire&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#troubleshooting-a-network-boot&quot;&gt;Troubleshooting a Network Boot&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-3-over-the-network-pxe-dhcp-tftp-and-ipxe&quot;&gt;Stage 3, Over the Network: PXE, DHCP, TFTP, and iPXE&lt;/h2&gt;

&lt;p&gt;With no bootable disk, the firmware falls through to network boot and the NIC’s &lt;strong&gt;PXE (Preboot eXecution
Environment)&lt;/strong&gt; ROM takes over. PXE only speaks DHCP and TFTP, so its whole job is to fetch a smarter bootloader,
&lt;strong&gt;&lt;a href=&quot;https://ipxe.org/&quot;&gt;iPXE&lt;/a&gt;&lt;/strong&gt;, which adds HTTP and a second DHCP round of its own.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt; host firmware / iPXE              DHCP relay (router)         Ironic: dnsmasq + httpd
 ─────────────────────             ───────────────────         ───────────────────────
 PXE ROM
   │ DHCPDISCOVER (broadcast)
   │ opt 60 &quot;PXEClient&quot;, opt 93 arch ──► giaddr = 10.20.0.1 ──►
   ◄── OFFER: ip, router, next-server, filename = snponly.efi ◄──
   │ TFTP GET snponly.efi ─────── routed unicast, no relay ───►
   ◄── iPXE binary
   ▼
 iPXE
   │ DHCPDISCOVER again, opt 175 marks it as iPXE ──► relay ──►
   ◄── filename = http://172.22.0.2:6180/boot.ipxe
   │ HTTP GET boot.ipxe, then pxelinux.cfg/&amp;lt;mac&amp;gt; ────────────►   per-host script Ironic wrote
   │ HTTP GET IPA kernel + initramfs ─────────────────────────►
   ▼
 kernel + initramfs (IPA) boot: Stages 4-6 run entirely out of RAM
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Round one.&lt;/strong&gt; The ROM’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DHCPDISCOVER&lt;/code&gt; carries option 60 (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXEClient&lt;/code&gt;) and option 93, the client architecture
from &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc4578&quot;&gt;RFC 4578&lt;/a&gt;. dnsmasq matches the architecture and answers with an
address, a router, and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filename&lt;/code&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snponly.efi&lt;/code&gt; for UEFI, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;undionly.kpxe&lt;/code&gt; for BIOS. The ROM fetches it over
TFTP. That file is iPXE, and it is the last thing TFTP is used for.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Round two.&lt;/strong&gt; iPXE sends its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DHCPDISCOVER&lt;/code&gt;, tagged with option 175. dnsmasq recognizes the tag and hands
back a different &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;filename&lt;/code&gt;: an HTTP URL for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boot.ipxe&lt;/code&gt;. Without the tag the ROM would loop, re-downloading iPXE
forever.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;HTTP.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boot.ipxe&lt;/code&gt; chains to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pxelinux.cfg/&amp;lt;mac&amp;gt;&lt;/code&gt;, the per-host script Ironic wrote when it started the
deploy. It names the IPA kernel, initramfs, and command line (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ipa-api-url=http://172.22.0.2:6385&lt;/code&gt;, an agent
token, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BOOTIF=&amp;lt;mac&amp;gt;&lt;/code&gt;). iPXE loads both files and jumps to the kernel exactly as GRUB does.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Stages 4-6, in RAM.&lt;/strong&gt; Decompression, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_kernel()&lt;/code&gt;, and the initramfs proceed unchanged, with one
difference: IPA’s initramfs never does &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;switch_root&lt;/code&gt;. The ramdisk &lt;em&gt;is&lt;/em&gt; the root filesystem, and systemd inside it
starts &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ironic-python-agent.service&lt;/code&gt; as the workload.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One caveat: Ironic’s iPXE binaries are not signed by Microsoft, so a host with Secure Boot enforced refuses
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;snponly.efi&lt;/code&gt;. That is the main reason fleets move to &lt;strong&gt;Redfish virtual media&lt;/strong&gt;, where Ironic asks the BMC to mount
a signed IPA ISO as a virtual CD-ROM and the whole chain above collapses into one BMC call.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;crossing-vlans-the-dhcp-relay&quot;&gt;Crossing VLANs: The DHCP Relay&lt;/h2&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DHCPDISCOVER&lt;/code&gt; is a broadcast from a client with no address yet. Broadcasts do not cross routers, so on a fabric
where each rack is its own VLAN, a host on VLAN 20 cannot reach dnsmasq on VLAN 10 without help. The help is the
&lt;strong&gt;DHCP relay&lt;/strong&gt; from &lt;a href=&quot;https://www.rfc-editor.org/rfc/rfc2131#section-4.3.1&quot;&gt;RFC 2131&lt;/a&gt;, configured on Cisco-style
gear as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ip helper-address&lt;/code&gt; on the host VLAN’s routed interface.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;   VLAN 20 · 10.20.0.0/24              router / L3 switch               VLAN 10 · 172.22.0.0/24
   ┌────────────────┐          ┌──────────────────────────┐          ┌──────────────────────┐
   │ host NIC       │ DISCOVER │ SVI20  10.20.0.1         │ unicast  │ Ironic pod           │
   │ 0.0.0.0 → bcast│ ───────► │ helper → 172.22.0.2      │ ───────► │ VIP 172.22.0.2       │
   │                │          │ SVI10  172.22.0.1        │ giaddr=  │ dnsmasq picks the    │
   │                │ ◄─────── │                          │ ◄─────── │ range from giaddr    │
   └────────────────┘  OFFER   └──────────────────────────┘  OFFER   └──────────────────────┘
           │  delivered on VLAN 20                          to giaddr           ▲
           │                                                                    │
           └──── TFTP 69 · HTTP 6180 · API 6385: routed unicast (needs option 3) ┘
                 dnsmasq hands out option 3 = 10.20.0.1 so the host can leave its subnet
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The relay turns the broadcast into a unicast packet to the helper target and sets &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;giaddr&lt;/code&gt; to its own address on
the host VLAN. dnsmasq picks the address range from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;giaddr&lt;/code&gt;, never from the interface the packet came in on, and
sends the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;OFFER&lt;/code&gt; back to the relay, which delivers it on the host VLAN.&lt;/p&gt;

&lt;p&gt;Only the four DHCP messages pass through the relay, and they do so &lt;strong&gt;three times per provisioning&lt;/strong&gt;: for the PXE
ROM, for iPXE, and once more when the Linux kernel inside IPA brings up its NIC. That third round is the one people
forget. If it returns no router option, IPA has an address but cannot reach Ironic, and the host sits in
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning&lt;/code&gt; until the deploy callback times out. Everything after DHCP (TFTP, HTTP, the agent API) is ordinary
routed unicast.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;! switch side, one block per host VLAN
interface Vlan20
  ip address 10.20.0.1 255.255.255.0
  ip helper-address 172.22.0.2     ! Ironic VIP, not a node IP

# dnsmasq side, one block per relayed subnet
dhcp-range=set:vlan20,10.20.0.50,10.20.0.200,255.255.255.0,12h
dhcp-option=tag:vlan20,option:router,10.20.0.1
# the stock ironic-image config only knows one DHCP_RANGE; extra ranges need a custom config
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Skip all of this on a single flat VLAN, and skip it entirely with Redfish virtual media plus DHCP-less IPA, where a
pre-provisioning network-data Secret gives the agent a static address.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;from-baremetalhost-to-provisioned-host&quot;&gt;From BareMetalHost to Provisioned Host&lt;/h2&gt;

&lt;p&gt;Here is every call BMO and Ironic make with that boot chain, from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl apply&lt;/code&gt; of a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; to a
physical server running the OS you asked for. Nothing in this section involves Cluster API: you write the object
by hand, and BMO does the rest. The host sits on VLAN 20 and Ironic on VLAN 10, so the DHCP hops cross the relay
from the previous section.&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;metal3.io/v1alpha1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;BareMetalHost&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;r01-s07&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;metal3&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;online&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;bootMACAddress&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;52:54:00:9a:1b:2c&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;# the NIC that PXE-boots, on VLAN 20&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;bootMode&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;UEFI&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;bmc&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;address&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;redfish://10.0.0.7/redfish/v1/Systems/1&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# or ipmi://10.0.0.7&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;credentialsName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;r01-s07-bmc&lt;/span&gt;               &lt;span class=&quot;c1&quot;&gt;# Secret with username / password&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;rootDeviceHints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;deviceName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;/dev/nvme0n1&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;url&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http://images.example.internal/ubuntu-24.04.raw&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;checksum&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;http://images.example.internal/ubuntu-24.04.raw.sha256&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;checksumType&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;sha256&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;format&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;raw&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;userData&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;r01-s07-user-data&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;metal3&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# cloud-init&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;networkData&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;r01-s07-network-data&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;metal3&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# optional static addressing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div style=&quot;overflow-x:auto;border:1px solid rgba(128,128,128,.35);border-radius:6px;padding:8px;margin:1em 0;&quot;&gt;
&lt;svg id=&quot;metal3-bmh-seq&quot; viewBox=&quot;0 0 1026 1486&quot; role=&quot;img&quot; style=&quot;display:block;min-width:900px;width:100%;height:auto;&quot; aria-label=&quot;Sequence of calls from applying a BareMetalHost by hand to the host booting its freshly written OS from disk, across kubectl, BMO, Ironic, the BMC, dnsmasq and httpd, the DHCP relay, the host firmware, and the Ironic Python Agent&quot; xmlns=&quot;http://www.w3.org/2000/svg&quot;&gt;
&lt;style&gt;
#metal3-bmh-seq{--bg:#fdfdfd;--lane:#ffffff;--band:#e4eaf4;--note:#fff1cc;--net:#c8420a;--mono:ui-monospace,SFMono-Regular,Menlo,monospace;color:#151414;font-family:system-ui,-apple-system,&quot;Segoe UI&quot;,sans-serif}
@media (prefers-color-scheme: dark){#metal3-bmh-seq{--bg:#181a1b;--lane:#24282a;--band:#2b3441;--note:#3b2f12;--net:#ff8e55;color:#e8e6e3}}
&lt;/style&gt;
&lt;defs&gt;&lt;marker id=&quot;m3b-ah&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;currentColor&quot; /&gt;&lt;/marker&gt;&lt;marker id=&quot;m3b-ahn&quot; viewBox=&quot;0 0 10 10&quot; refX=&quot;9&quot; refY=&quot;5&quot; markerWidth=&quot;7&quot; markerHeight=&quot;7&quot; orient=&quot;auto-start-reverse&quot;&gt;&lt;path d=&quot;M0 0L10 5L0 10z&quot; fill=&quot;var(--net)&quot; /&gt;&lt;/marker&gt;&lt;/defs&gt;
&lt;line x1=&quot;72&quot; y1=&quot;78&quot; x2=&quot;72&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;13&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;72&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;kubectl&lt;/text&gt;
&lt;text x=&quot;72&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;operator / GitOps&lt;/text&gt;
&lt;line x1=&quot;198&quot; y1=&quot;78&quot; x2=&quot;198&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;139&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;198&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;BMO&lt;/text&gt;
&lt;text x=&quot;198&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;BareMetalHost ctrl&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;78&quot; x2=&quot;324&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;265&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;324&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;Ironic&lt;/text&gt;
&lt;text x=&quot;324&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;API + conductor :6385&lt;/text&gt;
&lt;line x1=&quot;450&quot; y1=&quot;78&quot; x2=&quot;450&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;391&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;450&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;BMC&lt;/text&gt;
&lt;text x=&quot;450&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;IPMI / Redfish&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;78&quot; x2=&quot;576&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;517&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;576&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;dnsmasq / httpd&lt;/text&gt;
&lt;text x=&quot;576&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;DHCP · TFTP · HTTP 6180&lt;/text&gt;
&lt;line x1=&quot;702&quot; y1=&quot;78&quot; x2=&quot;702&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;643&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;702&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;DHCP relay&lt;/text&gt;
&lt;text x=&quot;702&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;router SVI, helper-address&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;78&quot; x2=&quot;828&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;769&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;828&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;Host firmware&lt;/text&gt;
&lt;text x=&quot;828&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;PXE ROM → iPXE&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;78&quot; x2=&quot;954&quot; y2=&quot;1466&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.22&quot; stroke-width=&quot;1&quot; /&gt;
&lt;rect x=&quot;895&quot; y=&quot;14&quot; width=&quot;118&quot; height=&quot;46&quot; rx=&quot;4&quot; fill=&quot;var(--lane)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.35&quot; /&gt;
&lt;text x=&quot;954&quot; y=&quot;34&quot; text-anchor=&quot;middle&quot; font-size=&quot;12.5&quot; font-weight=&quot;600&quot; fill=&quot;currentColor&quot;&gt;IPA → OS&lt;/text&gt;
&lt;text x=&quot;954&quot; y=&quot;50&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; fill-opacity=&quot;0.7&quot;&gt;agent in RAM, then disk&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;84&quot; width=&quot;1010&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;102&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;0 · Inventory: happens once per host, before any image is chosen&lt;/text&gt;
&lt;line x1=&quot;72&quot; y1=&quot;134&quot; x2=&quot;198&quot; y2=&quot;134&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;135&quot; y=&quot;128&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;apply BareMetalHost + BMC Secret&lt;/text&gt;
&lt;line x1=&quot;198&quot; y1=&quot;170&quot; x2=&quot;324&quot; y2=&quot;170&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;261&quot; y=&quot;164&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;create node: driver, BMC addr, bootMAC&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;206&quot; x2=&quot;450&quot; y2=&quot;206&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;387&quot; y=&quot;200&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;boot=pxe (one-time), power on → inspection&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;242&quot; x2=&quot;954&quot; y2=&quot;242&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; stroke-dasharray=&quot;5 4&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;882.76&quot; y=&quot;236&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;same relayed PXE → iPXE → IPA chain as phase 3&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;278&quot; x2=&quot;324&quot; y2=&quot;278&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;272&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST /v1/continue_inspection (inventory)&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;314&quot; x2=&quot;198&quot; y2=&quot;314&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;261&quot; y=&quot;308&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;inspected → BMO fills status.hardware&lt;/text&gt;
&lt;rect x=&quot;190&quot; y=&quot;340&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;212&quot; y=&quot;354&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;BMH: Available · powered off&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;372&quot; width=&quot;1010&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;390&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;1-2 · You set the spec by hand: in Part 3, Cluster API declares the cluster and CAPM3 claims the host here&lt;/text&gt;
&lt;line x1=&quot;72&quot; y1=&quot;422&quot; x2=&quot;198&quot; y2=&quot;422&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;175.58&quot; y=&quot;416&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;patch BMH.spec: image, userData, networkData, online=true&lt;/text&gt;
&lt;rect x=&quot;190&quot; y=&quot;448&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;212&quot; y=&quot;462&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;userData Secret = your cloud-init · networkData Secret = optional static addressing&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;480&quot; width=&quot;1010&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;498&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;3 · Ironic boots the agent: host on VLAN 20, Ironic on VLAN 10, DHCP crosses the router via the helper&lt;/text&gt;
&lt;line x1=&quot;198&quot; y1=&quot;530&quot; x2=&quot;324&quot; y2=&quot;530&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;261&quot; y=&quot;524&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;instance_info = image/checksum/configdrive; provision → active&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;566&quot; x2=&quot;576&quot; y2=&quot;566&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;450&quot; y=&quot;560&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;write pxelinux.cfg/&amp;lt;mac&amp;gt; (IPA boot script)&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;602&quot; x2=&quot;450&quot; y2=&quot;602&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;387&quot; y=&quot;596&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;set boot device PXE (one-time), power on&lt;/text&gt;
&lt;line x1=&quot;450&quot; y1=&quot;638&quot; x2=&quot;828&quot; y2=&quot;638&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;632&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;power on, POST (5–10 min on big boxes)&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;674&quot; x2=&quot;702&quot; y2=&quot;674&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;765&quot; y=&quot;668&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;DHCPDISCOVER broadcast  opt60 PXEClient, opt93 arch&lt;/text&gt;
&lt;line x1=&quot;702&quot; y1=&quot;710&quot; x2=&quot;576&quot; y2=&quot;710&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;704&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;unicast to helper target: giaddr=10.20.0.1, hops=1&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;746&quot; x2=&quot;702&quot; y2=&quot;746&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;740&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;OFFER: range picked by giaddr, ip, router, next-server, snponly.efi&lt;/text&gt;
&lt;line x1=&quot;702&quot; y1=&quot;782&quot; x2=&quot;828&quot; y2=&quot;782&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;765&quot; y=&quot;776&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;OFFER re-broadcast on VLAN 20 (REQUEST/ACK repeat this path)&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;818&quot; x2=&quot;576&quot; y2=&quot;818&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;702&quot; y=&quot;812&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;TFTP GET iPXE binary: routed unicast, no relay involved&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;854&quot; x2=&quot;702&quot; y2=&quot;854&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;765&quot; y=&quot;848&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;iPXE DHCPs again (opt175) → relayed the same way&lt;/text&gt;
&lt;line x1=&quot;576&quot; y1=&quot;890&quot; x2=&quot;828&quot; y2=&quot;890&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;702&quot; y=&quot;884&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;filename = http://ironic:6180/boot.ipxe  (via relay)&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;926&quot; x2=&quot;576&quot; y2=&quot;926&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;702&quot; y=&quot;920&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;HTTP GET boot.ipxe → pxelinux.cfg/&amp;lt;mac&amp;gt; → IPA kernel + initramfs&lt;/text&gt;
&lt;line x1=&quot;828&quot; y1=&quot;962&quot; x2=&quot;954&quot; y2=&quot;962&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;865.12&quot; y=&quot;956&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;kernel boots IPA  (ipa-api-url, agent token, BOOTIF)&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;984&quot; width=&quot;1010&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;1002&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;4 · The agent writes the image&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;1034&quot; x2=&quot;702&quot; y2=&quot;1034&quot; stroke=&quot;var(--net)&quot; stroke-width=&quot;1.6&quot; marker-end=&quot;url(#m3b-ahn)&quot; /&gt;
&lt;text x=&quot;794.56&quot; y=&quot;1028&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;var(--net)&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;Linux DHCP (relayed again): needs option 3 router to reach Ironic off-subnet&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;1070&quot; x2=&quot;324&quot; y2=&quot;1070&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;1064&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST /v1/lookup, then heartbeat every ~10 s (callback_url :9999)&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;1106&quot; x2=&quot;954&quot; y2=&quot;1106&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;1100&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST :9999/v1/commands  prepare_image {url, checksum, configdrive}&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;1142&quot; x2=&quot;576&quot; y2=&quot;1142&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;765&quot; y=&quot;1136&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;GET image.raw  (httpd cache or your image server)&lt;/text&gt;
&lt;rect x=&quot;946&quot; y=&quot;1168&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;940&quot; y=&quot;1182&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;dd to rootDeviceHints device · write config-2 partition (user_data, meta_data, network_data)&lt;/text&gt;
&lt;line x1=&quot;954&quot; y1=&quot;1214&quot; x2=&quot;324&quot; y2=&quot;1214&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;1208&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;heartbeat: deploy steps done&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;1250&quot; x2=&quot;450&quot; y2=&quot;1250&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;387&quot; y=&quot;1244&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;boot device = disk (persistent), reboot&lt;/text&gt;
&lt;line x1=&quot;324&quot; y1=&quot;1286&quot; x2=&quot;198&quot; y2=&quot;1286&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;261&quot; y=&quot;1280&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;node active → BMH Provisioned&lt;/text&gt;
&lt;rect x=&quot;8&quot; y=&quot;1308&quot; width=&quot;1010&quot; height=&quot;28&quot; rx=&quot;3&quot; fill=&quot;var(--band)&quot; /&gt;
&lt;text x=&quot;18&quot; y=&quot;1326&quot; font-size=&quot;12&quot; font-weight=&quot;700&quot; fill=&quot;currentColor&quot; font-family=&quot;var(--mono)&quot;&gt;5 · The OS boots from disk&lt;/text&gt;
&lt;line x1=&quot;450&quot; y1=&quot;1358&quot; x2=&quot;828&quot; y2=&quot;1358&quot; stroke=&quot;currentColor&quot; stroke-width=&quot;1.3&quot; marker-end=&quot;url(#m3b-ah)&quot; /&gt;
&lt;text x=&quot;639&quot; y=&quot;1352&quot; font-size=&quot;11&quot; text-anchor=&quot;middle&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;POST, boot from disk: IPA is gone&lt;/text&gt;
&lt;rect x=&quot;946&quot; y=&quot;1384&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;940&quot; y=&quot;1398&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;end&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;cloud-init ConfigDrive: network_data → static IP on the baremetal net · user_data → whatever you asked for&lt;/text&gt;
&lt;rect x=&quot;190&quot; y=&quot;1430&quot; width=&quot;16&quot; height=&quot;20&quot; rx=&quot;3&quot; fill=&quot;var(--note)&quot; stroke=&quot;currentColor&quot; stroke-opacity=&quot;0.5&quot; /&gt;
&lt;text x=&quot;212&quot; y=&quot;1444&quot; font-size=&quot;11&quot; font-style=&quot;italic&quot; text-anchor=&quot;start&quot; fill=&quot;currentColor&quot; stroke=&quot;var(--bg)&quot; stroke-width=&quot;4&quot; paint-order=&quot;stroke&quot; stroke-linejoin=&quot;round&quot;&gt;later: remove spec.image → deprovisioning (IPA cleaning boot) → Available again&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;

&lt;p style=&quot;font-size:0.9em;opacity:.85;margin-top:-0.4em;&quot;&gt;Orange arrows are network boot (DHCP, TFTP, HTTP); black arrows are Kubernetes API, Ironic API, and BMC calls; the dashed arrow stands in for the full network-boot chain; yellow boxes are local state changes. Phase 0 runs once per host; phases 1-5 run for every deploy. Scroll sideways on narrow screens.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;What happens&lt;/th&gt;
      &lt;th&gt;Takes&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;**0 · Inventory**&lt;/td&gt;
      &lt;td&gt;You apply the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; and its BMC Secret. BMO creates an Ironic node with the driver, BMC address, and boot MAC, and Ironic asks the BMC for a one-time PXE boot and powers the host on. The PXE → iPXE → IPA chain above runs once for inspection: IPA posts the hardware inventory to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/v1/continue_inspection&lt;/code&gt;, BMO fills &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status.hardware&lt;/code&gt;, and the host ends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt;, powered off.&lt;/td&gt;
      &lt;td&gt;10-20 min, once per host&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**1-2 · Set the spec**&lt;/td&gt;
      &lt;td&gt;You patch the BMH with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;image&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;userData&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;networkData&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;online: true&lt;/code&gt;. In [Part 3](https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3.html) this is two phases, Cluster API declaring a cluster and CAPM3 claiming a host and rendering the same fields into the BMH; the numbering here is kept so the two diagrams line up.&lt;/td&gt;
      &lt;td&gt;seconds&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**3 · Network boot**&lt;/td&gt;
      &lt;td&gt;BMO copies image URL, checksum, and config drive into the Ironic node’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;instance_info&lt;/code&gt; and asks for provision state &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;active&lt;/code&gt;. Ironic writes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pxelinux.cfg/&amp;lt;mac&amp;gt;&lt;/code&gt;, asks the BMC for a one-time PXE boot, and powers the host on. POST, then the PXE → iPXE → IPA chain.&lt;/td&gt;
      &lt;td&gt;3-10 min, mostly POST&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**4 · Deploy**&lt;/td&gt;
      &lt;td&gt;IPA DHCPs (third relayed round), calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;POST /v1/lookup&lt;/code&gt;, and heartbeats to Ironic every ~10 s. Ironic calls back on port 9999 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepare_image&lt;/code&gt;; IPA streams the image to the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rootDeviceHints&lt;/code&gt; device and writes the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config-2&lt;/code&gt; partition holding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_data&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;meta_data&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;network_data&lt;/code&gt;. Ironic sets boot=disk on the BMC, persistently this time, and reboots. The BMH turns &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioned&lt;/code&gt;.&lt;/td&gt;
      &lt;td&gt;2-6 min, image-size bound&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**5 · Boot from disk**&lt;/td&gt;
      &lt;td&gt;Stages 1-7 from the freshly written disk. cloud-init’s ConfigDrive datasource applies &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;network_data&lt;/code&gt; and runs whatever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;user_data&lt;/code&gt; says.&lt;/td&gt;
      &lt;td&gt;3-10 min&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;To release the host, remove &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;spec.image&lt;/code&gt; (in Part 3, deleting the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Machine&lt;/code&gt; does this for you): BMO runs a
cleaning boot of IPA to wipe the disks and the BMH returns to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;available&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;mapping-the-phases-back-to-the-seven-stages&quot;&gt;Mapping the Phases Back to the Seven Stages&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Phase&lt;/th&gt;
      &lt;th&gt;In [Part 1](https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html) terms&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;3&lt;/td&gt;
      &lt;td&gt;Stages 1-2 as before. Stage 3 is PXE ROM → iPXE instead of shim → GRUB. Stages 4-6 boot IPA’s kernel, initramfs, and systemd. No Stage 7: nobody logs in.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;4&lt;/td&gt;
      &lt;td&gt;Still inside the ramdisk, on a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tmpfs&lt;/code&gt; root. The agent streams an image to a block device it found during Stage 4 driver probing.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;5&lt;/td&gt;
      &lt;td&gt;The full Stage 1-7 sequence from the freshly written disk. cloud-init runs as ordinary systemd units in Stage 6 and reads the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;config-2&lt;/code&gt; partition IPA wrote.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;the-baremetalhost-state-machine&quot;&gt;The BareMetalHost State Machine&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;BareMetalHost.status.provisioning.state  (BMO)

 registering → inspecting* → preparing* → available → provisioning* → provisioned
                                             ▲                             │
                                             └──── deprovisioning* ◄───────┘   spec.image removed
 * = IPA is booted over the network in this state
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A host stuck in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning&lt;/code&gt; past Ironic’s 30-minute deploy-callback timeout means the BMC powered the box on but
IPA never called home: one of the orange arrows in the sequence above is broken.&lt;/p&gt;

&lt;h3 id=&quot;what-crosses-the-wire&quot;&gt;What Crosses the Wire&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Direction&lt;/th&gt;
      &lt;th&gt;Port&lt;/th&gt;
      &lt;th&gt;Carries&lt;/th&gt;
      &lt;th&gt;If blocked&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Ironic → BMC&lt;/td&gt;
      &lt;td&gt;623/udp IPMI, 443 Redfish&lt;/td&gt;
      &lt;td&gt;power, boot device, virtual media&lt;/td&gt;
      &lt;td&gt;BMH stuck &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registering&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;host → relay → dnsmasq&lt;/td&gt;
      &lt;td&gt;67/68 udp&lt;/td&gt;
      &lt;td&gt;DHCP with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;giaddr&lt;/code&gt; set, three rounds&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-E51 No DHCP offers&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;host → dnsmasq&lt;/td&gt;
      &lt;td&gt;69/udp&lt;/td&gt;
      &lt;td&gt;iPXE binary via TFTP&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-E32 TFTP open timeout&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;host → httpd&lt;/td&gt;
      &lt;td&gt;6180/tcp&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;boot.ipxe&lt;/code&gt;, per-MAC script, IPA kernel and initramfs&lt;/td&gt;
      &lt;td&gt;iPXE &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Could not chain&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;IPA → Ironic&lt;/td&gt;
      &lt;td&gt;6385/tcp&lt;/td&gt;
      &lt;td&gt;lookup, heartbeat, inspection callback&lt;/td&gt;
      &lt;td&gt;BMH sits in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning&lt;/code&gt; until timeout&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Ironic → IPA&lt;/td&gt;
      &lt;td&gt;9999/tcp&lt;/td&gt;
      &lt;td&gt;agent commands (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;prepare_image&lt;/code&gt;)&lt;/td&gt;
      &lt;td&gt;heartbeats fine, nothing happens&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;The arrow most firewall policies get wrong is Ironic → IPA on 9999: Ironic has to open a connection &lt;em&gt;to&lt;/em&gt; the host,
across the router. A policy that only allows host → Ironic passes every check up to the heartbeat and then stalls.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;troubleshooting-a-network-boot&quot;&gt;Troubleshooting a Network Boot&lt;/h2&gt;

&lt;p&gt;Work top to bottom: each row assumes the ones above it are healthy. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-Exx&lt;/code&gt; code on the console tells you
the exact hop.&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Symptom&lt;/th&gt;
      &lt;th&gt;Where to look&lt;/th&gt;
      &lt;th&gt;Usual cause&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-E51 No DHCP or proxyDHCP offers&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tcpdump -ni &amp;lt;prov-if&amp;gt; udp port 67&lt;/code&gt; on the Ironic host. Relayed packets show &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Gateway-IP&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;No helper on the SVI, helper pointing at a node IP instead of the VIP, DHCP snooping, PXE on the wrong NIC&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Packets arrive, no OFFER: dnsmasq logs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;no address range available for DHCP request via 10.20.0.1&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;dnsmasq container log&lt;/td&gt;
      &lt;td&gt;No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dhcp-range&lt;/code&gt; for the relay’s subnet&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-E32 TFTP open timeout&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-E53 No boot filename received&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tcpdump udp port 69&lt;/code&gt;; dnsmasq log for &quot;sent snponly.efi&quot;&lt;/td&gt;
      &lt;td&gt;ACL blocks 69/udp; UEFI vs BIOS mismatch with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootMode&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;iPXE: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Could not chain http://172.22.0.2:6180/boot.ipxe&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl&lt;/code&gt; that URL from the host VLAN; httpd log&lt;/td&gt;
      &lt;td&gt;No router option, 6180 blocked, or iPXE’s second DHCP answered by another server&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;iPXE loads &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;inspector.ipxe&lt;/code&gt; or 404s on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pxelinux.cfg/&amp;lt;mac&amp;gt;&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ls /shared/html/pxelinux.cfg/&lt;/code&gt; in the ironic container&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bootMACAddress&lt;/code&gt; on the BMH is not the NIC that PXE-boots&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;IPA is up on the console, but after 30 min &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeout reached while waiting for callback&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;ironic log for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;lookup&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;heartbeat&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;journalctl -u ironic-python-agent&lt;/code&gt; on the console&lt;/td&gt;
      &lt;td&gt;Third DHCP round returned no router, or 6385 blocked&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Heartbeats logged; conductor says &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Failed to connect to the agent ... :9999&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;curl http://&amp;lt;host-ip&amp;gt;:9999/v1/status&lt;/code&gt; from the Ironic host&lt;/td&gt;
      &lt;td&gt;Firewall only allows host → Ironic&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Deploy succeeds, host reboots into IPA again&lt;/td&gt;
      &lt;td&gt;BMC boot settings; Ironic log for &quot;set boot device to disk&quot;&lt;/td&gt;
      &lt;td&gt;BMC ignored the persistent boot-device change; wrong &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rootDeviceHints&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;Five-minute triage, in order:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# 1. Where is it stuck?&lt;/span&gt;
kubectl get bmh &lt;span class=&quot;nt&quot;&gt;-A&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-o&lt;/span&gt; custom-columns&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;NAME:.metadata.name,STATE:.status.provisioning.state,ERR:.status.errorMessage
&lt;span class=&quot;c&quot;&gt;# 2. Is DHCP arriving? Relayed packets carry Gateway-IP.&lt;/span&gt;
tcpdump &lt;span class=&quot;nt&quot;&gt;-ni&lt;/span&gt; &amp;lt;prov-if&amp;gt; &lt;span class=&quot;nt&quot;&gt;-v&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;udp port 67&apos;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;Gateway-IP|Client-Ethernet|Server-ID&apos;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 3. What did dnsmasq and httpd do with it?&lt;/span&gt;
kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; baremetal-operator-system logs deploy/ironic &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; dnsmasq | &lt;span class=&quot;nb&quot;&gt;tail&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-50&lt;/span&gt;
kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; baremetal-operator-system logs deploy/ironic &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; ironic-httpd | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;boot.ipxe|pxelinux.cfg&apos;&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 4. Is the agent talking, and can Ironic talk back?&lt;/span&gt;
kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; baremetal-operator-system logs deploy/ironic &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; ironic | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-E&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;heartbeat|lookup|Failed to connect&apos;&lt;/span&gt;
curl &lt;span class=&quot;nt&quot;&gt;-s&lt;/span&gt; http://&amp;lt;host-prov-ip&amp;gt;:9999/v1/status
&lt;span class=&quot;c&quot;&gt;# 5. Console (BMC KVM / SOL): the PXE-Exx code names the hop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;The network-boot path is the same seven stages from Part 1 with Stage 3 swapped out, run once for the agent and
once for the real OS. Every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-Exx&lt;/code&gt; code on the console and every stuck &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning&lt;/code&gt; state maps back to one
specific hop in that sequence, which is what makes it debuggable: work down the table above and the first row
that fails names the hop.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;On a diskless host, Stage 3 becomes &lt;strong&gt;PXE ROM → DHCP → TFTP → iPXE → DHCP again → HTTP → kernel + initramfs&lt;/strong&gt;, and the first Linux to boot is a RAM-only agent that installs the real one.&lt;/li&gt;
    &lt;li&gt;Only the four DHCP messages need a &lt;strong&gt;relay&lt;/strong&gt; to cross VLANs, three rounds per provisioning; everything after is routed unicast that needs a router option to work.&lt;/li&gt;
    &lt;li&gt;Ironic’s iPXE binaries are unsigned, so &lt;strong&gt;Secure Boot&lt;/strong&gt; hosts need Redfish virtual media instead, which collapses the whole chain into one BMC call.&lt;/li&gt;
    &lt;li&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; state machine does all the hardware work and needs no Kubernetes on the host it provisions: inspect once, then &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning → provisioned&lt;/code&gt; per deploy.&lt;/li&gt;
    &lt;li&gt;The firewall rule most policies miss is &lt;strong&gt;Ironic → IPA on 9999&lt;/strong&gt;: heartbeats look healthy and nothing happens.&lt;/li&gt;
    &lt;li&gt;A BMH stuck in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;provisioning&lt;/code&gt; past the 30-minute callback timeout means one network-boot hop is broken, and the console’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE-Exx&lt;/code&gt; code names it.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;up-next&quot;&gt;Up Next&lt;/h2&gt;

&lt;p&gt;One host is provisioned. &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3.html&quot;&gt;Part 3: Automated Host Provisioning with Metal³, CAPM3, and Cluster API&lt;/a&gt;&lt;/strong&gt; puts
Cluster API on top of the same &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BareMetalHost&lt;/code&gt; machinery, so a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cluster&lt;/code&gt; manifest claims hosts from the
inventory, renders their cloud-init and network data, images them, and joins them as Kubernetes nodes.&lt;/p&gt;</content><author><name></name></author><category term="Linux" /><category term="OS" /><category term="Systems" /><summary type="html">Part 2 of a 3-part series on the Linux boot process. Part 1 covered the seven stages of booting from a disk. This part takes those stages onto a blank server that has to fetch its bootloader, kernel, and operating system over the network.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hrishi.dev/assets/os-boot-process-stages.png" /><media:content medium="image" url="https://hrishi.dev/assets/os-boot-process-stages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Linux Bare Host vs. Container with Cilium netkit: What Actually Wins</title><link href="https://hrishi.dev/linux/kubernetes/networking/netkit/benchmarking/2026/08/23/cilium-netkit-host-vs-container-benchmarks.html" rel="alternate" type="text/html" title="Linux Bare Host vs. Container with Cilium netkit: What Actually Wins" /><published>2026-08-23T15:00:00-05:00</published><updated>2026-08-23T15:00:00-05:00</updated><id>https://hrishi.dev/linux/kubernetes/networking/netkit/benchmarking/2026/08/23/cilium-netkit-host-vs-container-benchmarks</id><content type="html" xml:base="https://hrishi.dev/linux/kubernetes/networking/netkit/benchmarking/2026/08/23/cilium-netkit-host-vs-container-benchmarks.html">&lt;p&gt;&lt;em&gt;Six workloads, one self-managed cluster, and a question I kept getting wrong until I measured it: does the extra hop through a container’s CNI add real network overhead versus the same binary running as a plain Linux process on the bare host, or does Cilium’s netkit datapath actually eliminate that bottleneck?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Where eBPF sits in the packet’s path, XDP earliest in the driver, TC later once the kernel has committed to processing the packet:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;mermaid&quot; src=&quot;https://mermaid.ink/svg/eyJjb2RlIjoiZ3JhcGggTFJcbk5JQ1tcIk5JQyBSWCByaW5nXCJdIC0tPiBYRFB7XCJYRFAgZUJQRiBob29rPGJyLz4oZHJpdmVyLCBiZWZvcmUgc2tfYnVmZilcIn1cblhEUCAtLT58WERQX0RST1B8IERST1BbXCJkcm9wcGVkXCJdXG5YRFAgLS0-fFhEUF9UWCAvIFhEUF9SRURJUkVDVHwgT1VUW1wiYmFjayBvdXQgaW1tZWRpYXRlbHk8YnIvPm5vIHNrX2J1ZmYsIG5vIHN0YWNrXCJdXG5YRFAgLS0-fFhEUF9QQVNTfCBTS0JbXCJza19idWZmIGFsbG9jYXRlZFwiXVxuU0tCIC0tPiBUQ3tcIlRDIGVCUEYgaG9vazxici8-KHFkaXNjIGxheWVyLCBpbmdyZXNzL2VncmVzcylcIn1cblRDIC0tPiBTVEFDS1tcInJlc3Qgb2Yga2VybmVsIHN0YWNrPGJyLz5yb3V0aW5nLCBuZXRmaWx0ZXIsIHByb3RvY29sXCJdXG5TVEFDSyAtLT4gU09DS1tcInNvY2tldCAvIHVzZXJzcGFjZVwiXSIsIm1lcm1haWQiOm51bGx9&quot; /&gt;&lt;/p&gt;

&lt;p&gt;netkit’s own hooks (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit/primary&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit/peer&lt;/code&gt;) live at that TC-adjacent layer, after &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sk_buff&lt;/code&gt; allocation, not in XDP: the saving isn’t skipping the kernel entirely, it’s skipping the specific steps &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; would otherwise force (the per-CPU backlog queue, the namespace hop) once the packet’s already committed to going through the stack.&lt;/p&gt;

&lt;p&gt;netkit, for anyone new to it: a Linux kernel network device type, merged into mainline Linux 6.7, contributed largely by Cilium/Isovalent engineers and currently used as Cilium’s container networking datapath. No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; pair, no virtual switch in the middle, just a lightweight in-kernel construct with eBPF hooks (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit/primary&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit/peer&lt;/code&gt;) on the send and receive path. The intent of this post is to observe netkit’s benefits for clustered or distributed applications, especially databases and RAFT-based systems, something I’d always wanted to check since &lt;a href=&quot;https://hrishi.dev/cloud/aws/trading/low-latency/distributed%20systems/2025/11/09/building-stock-exchange-in-the-cloud.html&quot;&gt;building a stock exchange in the cloud&lt;/a&gt;, where RAFT-style consensus latency is exactly the kind of thing that matters.&lt;/p&gt;

&lt;h2 id=&quot;the-setup&quot;&gt;The setup&lt;/h2&gt;

&lt;p&gt;A self-managed Kubernetes cluster on a small cloud VM instance type throughout (4 vCPUs, ~8GB RAM, ~8GB disk per node, Ubuntu 26.04 LTS, kernel 7.0.0-14-generic), Cilium in &lt;strong&gt;native routing&lt;/strong&gt; mode (no VXLAN, no encapsulation overhead) with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf.datapathMode=netkit&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;loadBalancer.mode=dsr&lt;/code&gt;. Every test below runs the &lt;em&gt;same binary&lt;/em&gt; two ways: as a &lt;strong&gt;bare Linux process&lt;/strong&gt; on the node (a normal &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd&lt;/code&gt;-managed process in the node’s own network namespace, no container, no CNI, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; in the path at all) and as a &lt;strong&gt;container behind Cilium’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; device&lt;/strong&gt;, the same node, the same kernel, just routed through Cilium’s datapath instead of a plain socket. “Bare host” here means bare of any container runtime and CNI, not bare-metal hardware: every node is a cloud VM, so both sides of every comparison share the same hypervisor-level virtualization; the variable under test is strictly the Linux networking path from that point down, host netns vs. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;. No test ever uses &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostNetwork: true&lt;/code&gt; to fake that comparison. Where a test involves two nodes, the host side talks over the cloud provider’s private network and the container side talks over Cilium’s pod network, both real, both production-shaped.&lt;/p&gt;

&lt;p&gt;One cluster ran every test in this post: 1 control-plane node plus 3 workers (4 nodes total), same instance type throughout. The control-plane node doubles as the north-south client for the one test that needs an external caller; the three workers carry every host-vs-container comparison, including the quorum-based tests (etcd, PostgreSQL, Kafka) and the MPI ping-pong, which needs all three to isolate the Slurm controller (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker-01&lt;/code&gt;) from the two nodes actually carrying MPI traffic.&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;mermaid&quot; src=&quot;https://mermaid.ink/svg/eyJjb2RlIjoiZ3JhcGggVERcbnN1YmdyYXBoIFwiQ29udHJvbCBwbGFuZSAoMSBub2RlKVwiXG5DUDFbXCJjb250cm9sLXBsYW5lLTAxPGJyLz4obm9ydGgtc291dGggY2xpZW50LCBlYXJsaWVyIHRlc3RzKVwiXVxuZW5kXG5zdWJncmFwaCBcIldvcmtlcnM6IHF1b3J1bSB0ZXN0cyBydW4gaGVyZVwiXG5XMVtcIndvcmtlci0wMTxici8-YmFyZSBMaW51eCBwcm9jZXNzICsgbWF0Y2hpbmcgbmV0a2l0IHBvZFwiXVxuVzJbXCJ3b3JrZXItMDI8YnIvPmJhcmUgTGludXggcHJvY2VzcyArIG1hdGNoaW5nIG5ldGtpdCBwb2RcIl1cblczW1wid29ya2VyLTAzPGJyLz5iYXJlIExpbnV4IHByb2Nlc3MgKyBtYXRjaGluZyBuZXRraXQgcG9kXCJdXG5lbmRcblcxIDw9PT4gVzJcblcyIDw9PT4gVzNcblcxIDw9PT4gVzMiLCJtZXJtYWlkIjpudWxsfQ&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cilium status&lt;/code&gt; on a worker, confirming the datapath actually under test:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;KubeProxyReplacement:   True   [ens2   203.0.113.10 fe80::xxxx:xxff:fexx:xxxx, ens6   172.16.0.3 fd12:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx fe80::xxxx:xxff:fexx:xxxx (Direct Routing)]
Routing:                Network: Native   Host: Legacy
Device Mode:            netkit
KubeProxyReplacement Details:
  Devices:               ens2   203.0.113.10 fe80::xxxx:xxff:fexx:xxxx, ens6   172.16.0.3 fd12:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx fe80::xxxx:xxff:fexx:xxxx (Direct Routing)
  Mode:                  DSR
    DSR Dispatch Mode:   IP Option/Extension
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Every quorum result compares the same three workers: a bare Linux process on each versus a netkit-backed pod on each, never a mix.&lt;/p&gt;

&lt;p&gt;Almost everything below is &lt;strong&gt;east-west&lt;/strong&gt; traffic: pod-to-pod (or host-to-host) inside the cluster, the shape most inter-service and database traffic actually takes. There’s one &lt;strong&gt;north-south&lt;/strong&gt; data point too: an external client (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;control-plane-01&lt;/code&gt;, itself a plain host, no Kubernetes networking on its own side) hitting the worker directly for the host case, and hitting the same worker’s Service &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt; (resolved via Cilium’s host-reachable-services, an eBPF hook at the client’s own socket layer, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube-proxy&lt;/code&gt; involved) for the container case. Worth keeping separate: it’s a different traffic shape from the paired host-vs-host / pod-vs-pod comparisons everywhere else in this post.&lt;/p&gt;

&lt;p&gt;Three things worth keeping in mind while reading:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Nothing here is tuned.&lt;/strong&gt; Every system runs its distribution’s default configuration: default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sysctl&lt;/code&gt; values, default TCP buffer sizes, no CPU pinning or NUMA tuning, no custom kernel parameters, stock package configs for Redis/PostgreSQL/Kafka/etcd beyond the minimum needed to get replication or clustering working at all, deliberately, on both sides. That’s a real limitation, not just a methodology note: an untuned host isn’t the host’s best case, and kernel-parameter tuning (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sysctl&lt;/code&gt; buffer sizes, IRQ/CPU affinity, NUMA pinning) is exactly what a latency-sensitive production deployment would actually do, work that could move the host-side numbers enough to flip some of the closer results. This post answers what happens to an unmodified deployment, not whether a tuned bare-metal host would still lose to netkit.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Not every “container wins” result is actually about eBPF + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;.&lt;/strong&gt; An early &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iperf3&lt;/code&gt; throughput test showed container traffic 2.4x faster than host-to-host, until holding the network path constant (private network on both sides) showed the &lt;em&gt;host&lt;/em&gt; hitting the identical ceiling: 1,270 Mbit/s either way. That gap was the cloud provider’s private network having a higher bandwidth ceiling than its public network, nothing to do with the CNI. Every result below has had that kind of confound checked for.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The container path is never &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; in isolation.&lt;/strong&gt; It always runs through Cilium’s full eBPF stack, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; plus Cilium’s other eBPF host optimizations (host-reachable services, host routing) together. So every “container wins by Nx” number below is really “netkit + Cilium’s other eBPF machinery” versus a plain, unmodified host. Separating netkit’s specific contribution would need a third baseline this post doesn’t include: the same Cilium eBPF stack, but with a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; pair standing in for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; as the container device, holding everything else (host-reachable services, host routing) constant. That comparison, Cilium-with-&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; versus Cilium-with-&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;, is what would isolate netkit’s own marginal effect from the rest of Cilium’s eBPF machinery. Worth running as a follow-up.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;where-the-container-with-netkit-wins-concurrent-dispatch-bound-traffic&quot;&gt;Where the container with netkit wins: concurrent, dispatch-bound traffic&lt;/h2&gt;

&lt;p&gt;First, east-west: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt; (20 connections) runs on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker-02&lt;/code&gt; and hits the identical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;whoami&lt;/code&gt; binary on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker-01&lt;/code&gt;, two ways. Host case: a direct socket connection, no container, no CNI. Container case: through the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;whoami&lt;/code&gt; Service’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ClusterIP&lt;/code&gt; (Cilium’s DSR path), not bypassed to the pod IP directly. Raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt; output, same 20-connection run behind both numbers in the chart below:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Path&lt;/th&gt;
      &lt;th&gt;Requests&lt;/th&gt;
      &lt;th&gt;Data read&lt;/th&gt;
      &lt;th&gt;Req/sec&lt;/th&gt;
      &lt;th&gt;Avg response size&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;host, private network (ens6)&lt;/td&gt;
      &lt;td&gt;45,511&lt;/td&gt;
      &lt;td&gt;26.39 MB&lt;/td&gt;
      &lt;td&gt;3,014.00&lt;/td&gt;
      &lt;td&gt;≈608 B&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;container, via Service ClusterIP&lt;/td&gt;
      &lt;td&gt;235,377&lt;/td&gt;
      &lt;td&gt;70.47 MB&lt;/td&gt;
      &lt;td&gt;15,588.00&lt;/td&gt;
      &lt;td&gt;≈314 B&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A note on response size&lt;/strong&gt;: the average response size differs almost 2x between the two paths (≈608B host, ≈314B container), an uncontrolled variable I didn’t pin down before running the test, likely &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;whoami&lt;/code&gt; echoing back different hostname/IP/header content depending on which path served the request. Both sizes fit in a single packet and neither path comes close to saturating link bandwidth (14.6 Mbit/s host, 39.2 Mbit/s container, versus a cloud VM’s actual NIC capacity), so it’s unlikely to explain a 4.8x gap on its own; the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpstat&lt;/code&gt; mechanism below is a per-packet dispatch cost, not a per-byte one. But it’s a real variable this test didn’t control for.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;http-throughput-host-vs-container-wrk-20-connections-title http-throughput-host-vs-container-wrk-20-connections-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;http-throughput-host-vs-container-wrk-20-connections-title&quot;&gt;HTTP throughput, host vs container (wrk, 20 connections)&lt;/title&gt;
  &lt;desc id=&quot;http-throughput-host-vs-container-wrk-20-connections-desc&quot;&gt;HTTP throughput, host vs container (wrk, 20 connections). grouped bar data: Requests/sec: Host 3014, Container 14582.Source: Author benchmark, whoami on Cilium netkit, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;HTTP throughput, host vs container (wrk, 20 connections)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;264.0&quot; y=&quot;238.8&quot; width=&quot;16.0&quot; height=&quot;38.2&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;272.0&quot; y=&quot;234.8&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot;&gt;3,014&lt;/text&gt;
&lt;rect x=&quot;282.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;290.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot;&gt;14,582&lt;/text&gt;
&lt;text x=&quot;282.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;0&quot;&gt;Requests/sec&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, whoami on Cilium netkit, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;4.8x. I didn’t trust that number until I isolated it: swapping which side (client or server) ran in a container, holding the other constant, showed client location barely moved the result: the win is entirely server-side. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpstat&lt;/code&gt; during the load test confirmed why:&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;cpu-time-per-http-request-ms-title cpu-time-per-http-request-ms-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;cpu-time-per-http-request-ms-title&quot;&gt;CPU time per HTTP request (ms)&lt;/title&gt;
  &lt;desc id=&quot;cpu-time-per-http-request-ms-desc&quot;&gt;CPU time per HTTP request (ms). horizontal bar data: Host (plain socket) 1.1; Container (netkit) 0.23.Source: Author benchmark, mpstat under wrk load, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;CPU time per HTTP request (ms)&lt;/text&gt;

  &lt;line x1=&quot;145&quot; y1=&quot;298.0&quot; x2=&quot;510&quot; y2=&quot;298.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;145&quot; y1=&quot;243.0&quot; x2=&quot;510&quot; y2=&quot;243.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;145&quot; y1=&quot;188.0&quot; x2=&quot;510&quot; y2=&quot;188.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;145&quot; y1=&quot;133.0&quot; x2=&quot;510&quot; y2=&quot;133.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;145&quot; y1=&quot;78.0&quot; x2=&quot;510&quot; y2=&quot;78.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;text x=&quot;136.0&quot; y=&quot;146.9&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;136.0&quot; dy=&quot;0&quot;&gt;Host (plain&lt;/tspan&gt;&lt;tspan x=&quot;136.0&quot; dy=&quot;12&quot;&gt;socket)&lt;/tspan&gt;&lt;/text&gt;
&lt;rect x=&quot;145&quot; y=&quot;78.0&quot; width=&quot;365.0&quot; height=&quot;106.0&quot; rx=&quot;4&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;518.0&quot; y=&quot;146.9&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;1.1&lt;/text&gt;
&lt;text x=&quot;136.0&quot; y=&quot;260.9&quot; text-anchor=&quot;end&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;136.0&quot; dy=&quot;0&quot;&gt;Container&lt;/tspan&gt;&lt;tspan x=&quot;136.0&quot; dy=&quot;12&quot;&gt;(netkit)&lt;/tspan&gt;&lt;/text&gt;
&lt;rect x=&quot;145&quot; y=&quot;192.0&quot; width=&quot;76.3&quot; height=&quot;106.0&quot; rx=&quot;4&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;229.3&quot; y=&quot;260.9&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;0.23&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, mpstat under wrk load, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpstat&lt;/code&gt; summary lines behind that chart, captured on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker-01&lt;/code&gt; (4 vCPUs) while &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt; was running:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;# server = host (whoami-host, plain socket), 3,014 req/s
Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
Average:     all   23.20    0.00   57.31    0.00    0.00    2.26    0.00    0.00    0.00   17.06

# server = container (whoami via Service, netkit), 15,588 req/s
Average:     CPU    %usr   %nice    %sys %iowait    %irq   %soft  %steal  %guest  %gnice   %idle
Average:     all   25.64    0.00   49.02    0.00    0.00   14.12    0.00    0.00    0.00   10.88
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Converting busy-% into CPU-time per request: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4 × (1 − 0.1706) = 3.32&lt;/code&gt; CPUs busy ÷ 3,014 req/s = &lt;strong&gt;1.10ms&lt;/strong&gt; on the host; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4 × (1 − 0.1088) = 3.56&lt;/code&gt; CPUs busy ÷ 15,588 req/s = &lt;strong&gt;0.23ms&lt;/strong&gt; in the container. 4.81x more CPU per request on the host path, almost exactly matching the throughput gap. The mechanism, per &lt;a href=&quot;https://isovalent.com/blog/post/cilium-netkit-a-new-container-networking-paradigm-for-the-ai-era/&quot;&gt;Isovalent’s own writeup on netkit&lt;/a&gt;: pod egress traffic is redirected straight to the physical device from a BPF program running on the netkit device itself, &lt;strong&gt;skipping the per-CPU backlog queue&lt;/strong&gt; (the same queue a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;-based pod would have to cross via a full namespace hop), plus &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; defaults to L3 (L2 is a supported option, just not the default), removing ARP overhead in that default mode. That’s the documented source of the saving.&lt;/p&gt;

&lt;p&gt;Worth scoping that mechanism precisely against &lt;a href=&quot;https://github.com/cilium/cilium&quot;&gt;Cilium’s own source&lt;/a&gt;: the backlog-queue skip applies specifically to Pod egress traffic bound for a destination &lt;em&gt;outside the node&lt;/em&gt;, a straight &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf_redirect()&lt;/code&gt; to the physical NIC’s ifindex. It’s not a blanket “netkit always skips the backlog queue” property. For intra-node Pod-to-Pod (or host-to-Pod) local delivery, Cilium’s own &lt;a href=&quot;https://github.com/cilium/cilium/blob/727edc56793b7e8ce07fabb1872afb290ddb4453/bpf/lib/local_delivery.h#L82-L83&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf/lib/local_delivery.h&lt;/code&gt;&lt;/a&gt; says it plainly: “both &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect_peer()&lt;/code&gt; only traverse the CPU backlog queue once,” meaning &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;redirect_peer()&lt;/code&gt; are equivalent on that path, netkit isn’t skipping anything extra there. Every test in this post is cross-node, so it lands in the off-node-egress case where the skip does apply, but it’s worth knowing the saving isn’t universal to every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; code path:&lt;/p&gt;

&lt;p&gt;Traditional host-to-host (bare Linux process, no container, no CNI):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  process
      │  socket syscall
      ▼
  kernel network stack (single netns, no extra hop)
      │
      ▼
  NIC driver ──► physical NIC ──► wire
      (no netns crossing, no backlog queue involved at all)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;-based pod, off-node egress, generic CNI (bridge + iptables, no eBPF host routing, &lt;em&gt;not&lt;/em&gt; how Cilium itself runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;, see note below):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  process (pod netns)
      │  socket syscall
      ▼
  veth peer (pod side, pod netns)
      │  crosses into host netns
      ▼
  ┄┄┄┄┄┄┄ CPU per-CPU backlog queue ┄┄┄┄┄┄┄   [1 crossing, always]
      │
      ▼
  veth peer (host side, host netns)
      │  host stack: routing / netfilter / bridge
      ▼
  NIC driver ──► physical NIC ──► wire
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Note: this is the generic, pre-eBPF-host-routing baseline, the “veth-era” comparison point ByteDance/Meta/Isovalent’s own numbers are measured against, not Cilium’s own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; mode. Cilium has used eBPF-based host routing on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; since 1.9 (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf_redirect_peer()&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf_redirect_neigh()&lt;/code&gt;), which skips the netfilter/bridge hop shown above; it still crosses the backlog queue once, per the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;local_delivery.h&lt;/code&gt; source quoted further down.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;-based pod, off-node egress (Cilium eBPF path):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  process (pod netns)
      │  socket syscall
      ▼
  netkit peer device (pod netns)
      │  eBPF hook: bpf_redirect(phys_ifindex)
      ▼
  NIC driver ──► physical NIC ──► wire
      (per-CPU backlog queue skipped entirely, redirect targets
       the physical device&apos;s ifindex directly from the eBPF hook)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The middle hop is where the difference actually lives: bare host never has one, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; always pays it once per off-node packet, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; replaces it with a single BPF redirect straight to the NIC.&lt;/p&gt;

&lt;p&gt;That skip is one-directional, though: it’s specific to a Pod’s own traffic &lt;em&gt;leaving&lt;/em&gt; the node. The receiving side, a packet arriving at this node destined for a local Pod, is not accelerated the same way:&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;-based pod, off-node ingress (phys dev → Pod, receiving side):&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;  wire ──► physical NIC ──► NIC driver
      │  sk_buff allocated, normal RX processing
      ▼
  Cilium TC ingress hook
      │  redirect_peer()-style ingress -&amp;gt; ingress switch
      ▼
  ┄┄┄┄┄┄┄ CPU per-CPU backlog queue ┄┄┄┄┄┄┄   [1 crossing, always]
      │
      ▼
  netkit peer device (pod netns)
      │
      ▼
  process (pod netns)

  (identical mechanism for veth: same redirect_peer()-style switch,
   same one backlog-queue crossing; netkit gets no ingress-side skip)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;So a request into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;worker-01&lt;/code&gt;’s pod in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt; test pays the same ingress cost either way, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;, and only the response leaving the node gets the accelerated egress path from the diagram above. The mpstat-measured CPU saving behind the 4.81x number is realistically coming from that egress leg of each request/response cycle, not the full round trip.&lt;/p&gt;

&lt;p&gt;A caution about reading the diagrams above too literally, though: they only track one specific thing, whether a packet crosses the per-CPU backlog queue, not total CPU cost. The bare host-to-host diagram’s single “kernel network stack” box is hiding real work: protocol-layer processing, a routing lookup, netfilter/conntrack traversal if anything on the box has rules loaded, qdisc queuing, the blocking-syscall/epoll wakeup path. None of that is optimized away for a plain, unaccelerated process. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; diagrams look busier (an extra peer device, an eBPF hook) but that hook is part of Cilium’s entire optimized datapath, not just the one backlog-queue trick. Fewer boxes in a simplified diagram isn’t the same claim as less CPU work; that’s exactly the asymmetry the “Worth being precise here” section above already flags as an open, unresolved question, not something these diagrams settle.&lt;/p&gt;

&lt;p&gt;Laid out as a full round trip, sending app to receiving app, the difference in &lt;em&gt;what each side has to do&lt;/em&gt;, not just whether it crosses the backlog queue, looks like this. Bare host-to-host:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sending app (node A)
    │  write()/send() syscall
    ▼
TCP/IP protocol stack (segmentation, checksums, cwnd)
    │
    ▼
netfilter / conntrack (OUTPUT, POSTROUTING, if rules/conntrack loaded)
    │
    ▼
routing lookup (FIB)
    │
    ▼
qdisc (tc queueing discipline)
    │
    ▼
NIC driver ──► physical NIC (node A) ──► wire
    ⋮
wire ──► physical NIC (node B) ──► NIC driver
    │  NAPI poll, sk_buff allocated
    ▼
netfilter / conntrack (PREROUTING, INPUT, if rules/conntrack loaded)
    │
    ▼
routing lookup (local delivery decision)
    │
    ▼
TCP/IP protocol stack (reassembly, ACK generation)
    │
    ▼
socket receive buffer ──► wakes blocked process (context switch)
    │
    ▼
receiving app (node B), read()/recv() returns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt;-based pod-to-pod, off-node, same round trip:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sending app (pod netns, node A)
    │  write()/send() syscall
    ▼
TCP/IP protocol stack (pod netns)
    │
    ▼
netkit peer device (pod netns)
    │  eBPF hook: policy check + bpf_redirect(phys_ifindex)
    ▼  (replaces host-side netfilter chain-walking with one eBPF
    │   hook; per-CPU backlog queue skipped)
qdisc (physical NIC&apos;s own queueing discipline, not bypassed)
    │
    ▼
NIC driver ──► physical NIC (node A) ──► wire
    ⋮
wire ──► physical NIC (node B) ──► NIC driver
    │  NAPI poll, sk_buff allocated
    ▼
Cilium TC ingress hook (native routing, no kube-proxy/iptables)
    │  eBPF policy check + redirect_peer()-style switch
    ▼  (per-CPU backlog queue crossed once, same as veth)
netkit peer device (pod netns)
    │
    ▼
TCP/IP protocol stack (pod netns)
    │
    ▼
socket receive buffer ──► wakes blocked process
    │
    ▼
receiving app (pod netns, node B), read()/recv() returns
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;One box disappears entirely on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; sending side, host-side netfilter chain-walking, replaced by one eBPF hook doing policy check and redirect together, and the per-CPU backlog queue crossing is skipped for this off-node leg. The physical NIC’s own qdisc still applies on both paths; a TC &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf_redirect()&lt;/code&gt; to another device calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dev_queue_xmit()&lt;/code&gt; on that device, the same entry point into the qdisc layer any other packet uses, so &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; doesn’t bypass that. On the receiving side, both paths still do a policy/routing decision, just via different mechanisms: standard netfilter chain-walking on the host side versus an eBPF map lookup on the Cilium side. I’m not claiming one is cheaper than the other here; this post has no citation or profiling that isolates that specific comparison, so it’s left unlabeled rather than asserted. Worth being honest about the limit here: this diagram shows &lt;em&gt;where&lt;/em&gt; the work differs, not a verified, itemized accounting of &lt;em&gt;how much&lt;/em&gt; CPU each box actually costs on this hardware, that would need &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;perf&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ftrace&lt;/code&gt;-level profiling this post didn’t do, beyond the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpstat&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%usr&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%sys&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%soft&lt;/code&gt; breakdown already shown above.&lt;/p&gt;

&lt;p&gt;The exact source, from Cilium’s &lt;a href=&quot;https://github.com/cilium/cilium/blob/727edc56793b7e8ce07fabb1872afb290ddb4453/bpf/lib/local_delivery.h#L59-L88&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;should_redirect_peer()&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cm&quot;&gt;/*
 * For phys dev -&amp;gt; Pod:
 * - on veth, we go ingress -&amp;gt; ingress so we can use redirect_peer()
 * - on netkit, we go ingress -&amp;gt; ingress so we can use it too
 *   (ingress ifindex is &amp;gt; 0 on ingress, and 0 on egress)
 *
 * Finally, in case of Pod -&amp;gt; Pod:
 * - on veth, we&apos;re on TC ingress and need a redirect_peer() to get
 *   to the target namespace. Same ingress -&amp;gt; ingress switch.
 * - on netkit, we&apos;re on TC egress and need a regular redirect() to
 *   the peer device&apos;s ifindex. netkit takes care of the namespace
 *   switch for us. Here&apos;s it&apos;s egress -&amp;gt; ingress, therefore we must
 *   use redirect() instead of redirect_peer().
 *
 * Note: both redirect() and redirect_peer() only traverse the CPU
 * backlog queue once.
 */&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Worth being precise here:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Isovalent’s design goal for netkit: make container (pod) networking as fast as bare host networking, reaching &lt;strong&gt;parity&lt;/strong&gt;, not exceeding it.&lt;/li&gt;
  &lt;li&gt;ByteDance/Meta reports: netkit gets container networking close to host performance (ByteDance: 12% CPS gain over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; in a proof-of-concept, with plans to roll out in production; Meta: softirq load indistinguishable from host, measured on live production traffic). Both describe netkit closing the gap up to host from below, the historical &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt; overhead shrinking toward zero.&lt;/li&gt;
  &lt;li&gt;My result is different in kind, not degree: I’m not reporting netkit narrowing a gap, I’m reporting the container path outright beating the host path by 4.8x. That’s a stronger, less common claim than what any vendor number here supports.&lt;/li&gt;
  &lt;li&gt;My hedge: I don’t have a clean explanation for that inversion, but my best guess is the comparison isn’t apples-to-apples. My “host” baseline is a bare &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd&lt;/code&gt; process with none of Cilium’s host-side accelerations (eBPF kube-proxy replacement, host routing), because it isn’t running through Cilium at all. So my host baseline is closer to the unoptimized baseline vendors compare netkit against, not the optimized host baseline they compare netkit to (i.e. parity with).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In short: my 4.8x might be an artifact of comparing accelerated-container vs. unaccelerated-host, rather than genuine evidence that containers can outrun host networking.&lt;/p&gt;

&lt;p&gt;A 3-node etcd Raft cluster (the real quorum shape, 2-of-3 majority) showed the same underlying mechanism: a quorum-committing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PUT&lt;/code&gt; sends &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AppendEntries&lt;/code&gt; to two followers and waits for the faster one to ack: trivial per-request CPU work, cost dominated by dispatch.&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;etcd-raft-commit-latency-ms-title etcd-raft-commit-latency-ms-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;etcd-raft-commit-latency-ms-title&quot;&gt;etcd Raft commit latency (ms)&lt;/title&gt;
  &lt;desc id=&quot;etcd-raft-commit-latency-ms-desc&quot;&gt;etcd Raft commit latency (ms). grouped bar data: Host 18.4, Container 12.44.Source: Author benchmark, 100 sequential quorum-committing PUTs, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;etcd Raft commit latency (ms)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;264.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;272.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot;&gt;18.40&lt;/text&gt;
&lt;rect x=&quot;282.0&quot; y=&quot;152.0&quot; width=&quot;16.0&quot; height=&quot;125.0&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;290.0&quot; y=&quot;148.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot;&gt;12.44&lt;/text&gt;
&lt;text x=&quot;282.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;0&quot;&gt;Quorum commit&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, 100 sequential quorum-committing PUTs, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Container: 12.44ms average. Host: 18.40ms. &lt;strong&gt;1.48x&lt;/strong&gt;, same dispatch-cost mechanism as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt;. The leader fans &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AppendEntries&lt;/code&gt; out to two followers and waits for the faster ack; that extra dispatch work lands harder on the host’s more expensive per-packet path than on the container’s, which barely moves.&lt;/p&gt;

&lt;p&gt;The north-south data point is the odd one out, and worth stating plainly rather than forcing it into the concurrency story: 30 &lt;em&gt;serial&lt;/em&gt; plain-HTTP requests from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;control-plane-01&lt;/code&gt;, no DNS, no TLS, one at a time, and the container path still won, 1.379ms average versus 1.894ms for the host, ~27% lower, with zero concurrency in play. That doesn’t fit “needs concurrent load” the way the MPI result later in this post does, and I’m not claiming it does. The likely reason is architectural, not load-related: the container path here goes through a single cheap eBPF socket-layer redirect on the &lt;em&gt;client’s own kernel&lt;/em&gt;, not a full pod-to-pod round trip, a different mechanism from the “many requests queued on a netkit device” story above. Filed as a real result, not a proven mechanism.&lt;/p&gt;

&lt;h2 id=&quot;where-the-bare-host-wins-work-netkit-cant-touch&quot;&gt;Where the bare host wins: work netkit can’t touch&lt;/h2&gt;

&lt;p&gt;Redis, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-c 20 -n 100000&lt;/code&gt;:&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;redis-throughput-host-vs-container-title redis-throughput-host-vs-container-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;redis-throughput-host-vs-container-title&quot;&gt;Redis throughput, host vs container&lt;/title&gt;
  &lt;desc id=&quot;redis-throughput-host-vs-container-desc&quot;&gt;Redis throughput, host vs container. grouped bar data: SET: Host 39017, Container 35174; GET: Host 38447, Container 32248.Source: Author benchmark, redis-benchmark -c 20 -n 100000, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;Redis throughput, host vs container&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;159.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;167.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;39,017&lt;/text&gt;
&lt;rect x=&quot;177.0&quot; y=&quot;110.2&quot; width=&quot;16.0&quot; height=&quot;166.8&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;185.0&quot; y=&quot;106.2&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;35,174&lt;/text&gt;
&lt;text x=&quot;177.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;177.0&quot; dy=&quot;0&quot;&gt;SET&lt;/tspan&gt;&lt;/text&gt;
&lt;rect x=&quot;369.0&quot; y=&quot;94.7&quot; width=&quot;16.0&quot; height=&quot;182.3&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;377.0&quot; y=&quot;90.7&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;38,447&lt;/text&gt;
&lt;rect x=&quot;387.0&quot; y=&quot;124.1&quot; width=&quot;16.0&quot; height=&quot;152.9&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;395.0&quot; y=&quot;120.1&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;32,248&lt;/text&gt;
&lt;text x=&quot;387.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;387.0&quot; dy=&quot;0&quot;&gt;GET&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, redis-benchmark -c 20 -n 100000, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Host ahead by 11-19% (SET 39,017 vs 35,174, GET 38,447 vs 32,248). Redis’s single-threaded command loop is the bottleneck here, not the network: one core processes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;SET&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GET&lt;/code&gt; sequentially no matter which datapath delivered the packet, so netkit’s CPU saving has nothing to attach to. Whatever small overhead the container path adds on top now shows through directly as lost throughput, instead of being hidden behind a bigger saving elsewhere.&lt;/p&gt;

&lt;p&gt;PostgreSQL tells the same story, with a twist. Standalone &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgbench&lt;/code&gt; (async commit, scale factor 2; TPC-B row-locks a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgbench_branches&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pgbench_tellers&lt;/code&gt; row per transaction, a server-side serialization bottleneck):&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;postgresql-throughput-standalone-vs-sync-replication-tps-title postgresql-throughput-standalone-vs-sync-replication-tps-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;postgresql-throughput-standalone-vs-sync-replication-tps-title&quot;&gt;PostgreSQL throughput: standalone vs sync replication (TPS)&lt;/title&gt;
  &lt;desc id=&quot;postgresql-throughput-standalone-vs-sync-replication-tps-desc&quot;&gt;PostgreSQL throughput, standalone vs sync replication (TPS). grouped bar data: Standalone: Host 700.98, Container 667.98; Sync replication: Host 697.70, Container 649.96.Source: Author benchmark, pgbench -c 20 -T 30, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;PostgreSQL throughput: standalone vs sync replication (TPS)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;159.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;167.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;700.98&lt;/text&gt;
&lt;rect x=&quot;177.0&quot; y=&quot;100.7&quot; width=&quot;16.0&quot; height=&quot;176.3&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;185.0&quot; y=&quot;96.7&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;667.98&lt;/text&gt;
&lt;text x=&quot;177.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;177.0&quot; dy=&quot;0&quot;&gt;Standalone&lt;/tspan&gt;&lt;/text&gt;
&lt;rect x=&quot;369.0&quot; y=&quot;92.9&quot; width=&quot;16.0&quot; height=&quot;184.1&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;377.0&quot; y=&quot;88.9&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;697.70&lt;/text&gt;
&lt;rect x=&quot;387.0&quot; y=&quot;105.5&quot; width=&quot;16.0&quot; height=&quot;171.5&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;395.0&quot; y=&quot;101.5&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;649.96&lt;/text&gt;
&lt;text x=&quot;387.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;387.0&quot; dy=&quot;0&quot;&gt;Sync&lt;/tspan&gt;&lt;tspan x=&quot;387.0&quot; dy=&quot;11&quot;&gt;replication&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, pgbench -c 20 -T 30, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Standalone: host ahead ~5% (701 vs 668 TPS), same lock-contention story as Redis, no replication involved. Turning synchronous replication on changes the picture: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;synchronous_standby_names = &apos;ANY 1 (standby2, standby3)&apos;&lt;/code&gt; (primary plus two standbys) waits for whichever standby acks first, the shape a production quorum-commit deployment actually runs.&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;postgresql-marginal-cost-of-synchronous-replication-title postgresql-marginal-cost-of-synchronous-replication-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;postgresql-marginal-cost-of-synchronous-replication-title&quot;&gt;PostgreSQL: cost of sync replication (%)&lt;/title&gt;
  &lt;desc id=&quot;postgresql-marginal-cost-of-synchronous-replication-desc&quot;&gt;PostgreSQL: cost of sync replication (%). grouped bar data: Host 0.5, Container 2.7.Source: Author benchmark, pgbench -c 20 -T 30, standalone vs sync-commit throughput delta, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;PostgreSQL: cost of sync replication (%)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;264.0&quot; y=&quot;242.7&quot; width=&quot;16.0&quot; height=&quot;34.3&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;272.0&quot; y=&quot;238.7&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot;&gt;0.5%&lt;/text&gt;
&lt;rect x=&quot;282.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;290.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot;&gt;2.7%&lt;/text&gt;
&lt;text x=&quot;282.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;0&quot;&gt;Cost of sync&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, pgbench -c 20 -T 30, standalone vs sync-commit throughput delta, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Turning synchronous replication on cost the host &lt;strong&gt;0.5%&lt;/strong&gt; throughput. Essentially free: with a real majority to wait for, the leader races the faster of two standbys instead of paying a full round trip every time. The container’s marginal cost was higher in relative terms (2.7%), though still small in absolute terms, because its per-packet dispatch cost was already cheap enough that a full round trip barely registered in the first place. Sequential commit latency (confound-controlled, same Alpine/musl &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;psql&lt;/code&gt; client both sides) tells the same story at smaller scale: container 14.24ms vs host 20.86ms, a 1.46x gap. Two different questions, two different winners: on the %-cost of turning replication on, the host is “cheaper” because a nearly-free round trip barely dents its throughput; on the commit itself, the container is faster in absolute terms, 1.46x on latency, the direct measurement to trust here.&lt;/p&gt;

&lt;h2 id=&quot;kafka-pull-based-replication-favors-the-host&quot;&gt;Kafka: pull-based replication favors the host&lt;/h2&gt;

&lt;p&gt;Same “wait for the other replica” shape as Raft and Postgres, on paper. Kafka answered differently:&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;kafka-produce-latency-leader-ack-vs-quorum-write-ms-title kafka-produce-latency-leader-ack-vs-quorum-write-ms-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;kafka-produce-latency-leader-ack-vs-quorum-write-ms-title&quot;&gt;Kafka produce latency: leader-ack vs quorum write (ms)&lt;/title&gt;
  &lt;desc id=&quot;kafka-produce-latency-leader-ack-vs-quorum-write-ms-desc&quot;&gt;Kafka produce latency: leader-ack vs quorum write (ms). grouped bar data: acks=1: Host 40.26, Container 69.08; acks=all: Host 98.75, Container 374.39.Source: Author benchmark, kafka-producer-perf-test, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;Kafka produce latency: leader-ack vs quorum write (ms)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;159.0&quot; y=&quot;169.2&quot; width=&quot;16.0&quot; height=&quot;107.8&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;167.0&quot; y=&quot;165.2&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;40.3&lt;/text&gt;
&lt;rect x=&quot;177.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;185.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;69.1&lt;/text&gt;
&lt;text x=&quot;177.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;177.0&quot; dy=&quot;0&quot;&gt;acks=1&lt;/tspan&gt;&lt;/text&gt;
&lt;rect x=&quot;369.0&quot; y=&quot;228.2&quot; width=&quot;16.0&quot; height=&quot;48.8&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;377.0&quot; y=&quot;224.2&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;98.75&lt;/text&gt;
&lt;rect x=&quot;387.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;395.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;374.4&lt;/text&gt;
&lt;text x=&quot;387.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;387.0&quot; dy=&quot;0&quot;&gt;acks=all&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, kafka-producer-perf-test, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Host wins both modes. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acks=all&lt;/code&gt; + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;min.insync.replicas=2&lt;/code&gt; is the genuine quorum write, and there the host wins by nearly 4x (98.75ms vs 374.39ms). The likely reason: Raft’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;AppendEntries&lt;/code&gt; and Postgres’s WAL streaming are the leader &lt;em&gt;pushing&lt;/em&gt; to a follower and waiting for one ack, a single dispatch round trip. Kafka’s followers instead run a continuous &lt;em&gt;fetch&lt;/em&gt; loop against the leader (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;replica.fetch.wait.max.ms&lt;/code&gt;, default 500ms); &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acks=all&lt;/code&gt; completion depends on that poll loop noticing the new record, not a dedicated round trip triggered by the write, a fixed poll-cycle cost that dispatch efficiency doesn’t touch. Not independently confirmed with the same CPU-profiling rigor as the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt; result, so I’m stating it as the likely mechanism, not a proven one.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acks=1&lt;/code&gt; (leader-ack only, not a quorum vote at all) is the more surprising one: there’s no fan-out cost to explain a host win the way there is for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;acks=all&lt;/code&gt;, and it still lands well outside the concurrency-favors-container pattern established by HTTP and etcd. Flagged as an open anomaly rather than forced into a story I haven’t verified.&lt;/p&gt;

&lt;h2 id=&quot;mpi-no-concurrency-no-advantage&quot;&gt;MPI: no concurrency, no advantage&lt;/h2&gt;

&lt;p&gt;A real 2-rank MPI ping-pong (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpicc&lt;/code&gt;-compiled, launched through a hand-rolled Slurm cluster, mirrored topology on host and Kubernetes), as close to a raw TCP round trip as this series gets. Same layout both sides, deliberately, and with the controller kept off the compute path so its numbers aren’t diluted by controller overhead: one node runs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slurmctld&lt;/code&gt; only, the other two run &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;slurmd&lt;/code&gt; and actually carry the two MPI ranks. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sbatch&lt;/code&gt; on the controller node wraps &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpirun&lt;/code&gt; to launch the two ranks over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;HOST (bare processes)
  worker-01: slurmctld only, sbatch submitted here
      |
      |  mpirun, ssh launcher, private network
      v
  worker-02: slurmd, MPI rank 0  &amp;lt;---&amp;gt;  worker-03: slurmd, MPI rank 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;CONTAINER (same topology, as pods)
  slurm-ctl pod (worker-01): slurmctld only, sbatch submitted here
      |
      |  mpirun, ssh launcher, Cilium netkit
      v
  slurm-worker pod (worker-02): slurmd, rank 0  &amp;lt;---&amp;gt;  slurm-worker-2 pod (worker-03): slurmd, rank 1
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;No &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostNetwork&lt;/code&gt; on the container side here either. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;munge&lt;/code&gt; (shared auth key) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sshd&lt;/code&gt; are installed identically on both container pods purely so the launch mechanism matches the host’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ssh&lt;/code&gt;-based &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mpirun&lt;/code&gt; launcher exactly, not to introduce a variable that isn’t on the host side too.&lt;/p&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;mpi-ping-pong-round-trip-latency-s-title mpi-ping-pong-round-trip-latency-s-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;mpi-ping-pong-round-trip-latency-s-title&quot;&gt;MPI ping-pong round-trip latency (µs)&lt;/title&gt;
  &lt;desc id=&quot;mpi-ping-pong-round-trip-latency-s-desc&quot;&gt;MPI ping-pong round-trip latency (µs). grouped bar data: 0-byte round trip: Host 122.74, Container 131.37.Source: Author benchmark, 2-rank MPI ping-pong over Slurm, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;MPI ping-pong round-trip latency (µs)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;264.0&quot; y=&quot;104.1&quot; width=&quot;16.0&quot; height=&quot;172.9&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;272.0&quot; y=&quot;100.1&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;122.7&lt;/text&gt;
&lt;rect x=&quot;282.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;290.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;131.4&lt;/text&gt;
&lt;text x=&quot;282.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;0&quot;&gt;0-byte round&lt;/tspan&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;11&quot;&gt;trip&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, 2-rank MPI ping-pong over Slurm, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;figure class=&quot;blog-chart&quot;&gt;
&lt;svg viewBox=&quot;0 0 560 380&quot; style=&quot;max-width: 100%; height: auto; font-family: &apos;Inter&apos;, system-ui, sans-serif; --chart-muted: #4b5563;&quot; role=&quot;img&quot; aria-labelledby=&quot;mpi-ping-pong-bandwidth-mb-s-title mpi-ping-pong-bandwidth-mb-s-desc&quot;&gt;
  &lt;style&gt;
    @media (prefers-color-scheme: dark) { svg { --chart-muted: #d1d5db; } }
    @media (prefers-reduced-motion: reduce) { * { animation: none !important; transition: none !important; } }
  &lt;/style&gt;
  &lt;title id=&quot;mpi-ping-pong-bandwidth-mb-s-title&quot;&gt;MPI ping-pong 64KB bandwidth (MB/s)&lt;/title&gt;
  &lt;desc id=&quot;mpi-ping-pong-bandwidth-mb-s-desc&quot;&gt;MPI ping-pong 64KB bandwidth (MB/s). grouped bar data: 64KB round trip: Host 175.64, Container 142.27.Source: Author benchmark, 2-rank MPI ping-pong over Slurm, 2026 .&lt;/desc&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;29&quot; text-anchor=&quot;middle&quot; font-size=&quot;18&quot; font-weight=&quot;800&quot; fill=&quot;currentColor&quot;&gt;MPI ping-pong 64KB bandwidth (MB/s)&lt;/text&gt;

  &lt;line x1=&quot;72&quot; y1=&quot;277.0&quot; x2=&quot;492&quot; y2=&quot;277.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;230.8&quot; x2=&quot;492&quot; y2=&quot;230.8&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;184.5&quot; x2=&quot;492&quot; y2=&quot;184.5&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;138.2&quot; x2=&quot;492&quot; y2=&quot;138.2&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;line x1=&quot;72&quot; y1=&quot;92.0&quot; x2=&quot;492&quot; y2=&quot;92.0&quot; stroke=&quot;currentColor&quot; opacity=&quot;0.08&quot; /&gt;
&lt;rect x=&quot;72&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;87&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Host&lt;/text&gt;
&lt;rect x=&quot;177&quot; y=&quot;62&quot; width=&quot;10&quot; height=&quot;10&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;192&quot; y=&quot;71&quot; font-size=&quot;11&quot; fill=&quot;currentColor&quot;&gt;Container&lt;/text&gt;
&lt;rect x=&quot;264.0&quot; y=&quot;92.0&quot; width=&quot;16.0&quot; height=&quot;185.0&quot; fill=&quot;#f97316&quot; /&gt;
&lt;text x=&quot;272.0&quot; y=&quot;88.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;175.6&lt;/text&gt;
&lt;rect x=&quot;282.0&quot; y=&quot;127.2&quot; width=&quot;16.0&quot; height=&quot;149.8&quot; fill=&quot;#38bdf8&quot; /&gt;
&lt;text x=&quot;290.0&quot; y=&quot;123.2&quot; text-anchor=&quot;middle&quot; font-size=&quot;9&quot; fill=&quot;currentColor&quot;&gt;142.3&lt;/text&gt;
&lt;text x=&quot;282.0&quot; y=&quot;295.0&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;currentColor&quot; opacity=&quot;0.8&quot;&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;0&quot;&gt;64KB round&lt;/tspan&gt;&lt;tspan x=&quot;282.0&quot; dy=&quot;11&quot;&gt;trip&lt;/tspan&gt;&lt;/text&gt;
  &lt;text x=&quot;280.0&quot; y=&quot;366&quot; text-anchor=&quot;middle&quot; font-size=&quot;10&quot; fill=&quot;var(--chart-muted, currentColor)&quot;&gt;Source: Author benchmark, 2-rank MPI ping-pong over Slurm, 2026&lt;/text&gt;
&lt;/svg&gt;
&lt;/figure&gt;

&lt;p&gt;Host wins latency by ~7% and a 64KB synchronous-exchange throughput test by ~19% (176 vs 142 MB/s). This is the cleanest confirmation of what the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;wrk&lt;/code&gt; result actually proved: netkit’s advantage is CPU time &lt;em&gt;saved per request under concurrent load&lt;/em&gt;: many requests stacked up, cheaper dispatch paid off across all of them. A strict 1:1 ping-pong has exactly one message in flight at a time. There’s no queue for a cheaper datapath to save CPU across, so the result looks like the earlier &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ping&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;iperf3&lt;/code&gt; tests: dominated by the raw path, not by dispatch efficiency.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;A note on RDMA&lt;/strong&gt;, since MPI is what most people associate with it: this test measured plain TCP MPI, the only option on this instance type, which has no InfiniBand or RoCE capability. Real HPC and large-scale AI training clusters typically run MPI over RDMA, and RDMA changes the picture completely, not just the numbers. RDMA is kernel-bypass by design: userspace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libibverbs&lt;/code&gt; talks straight to NIC hardware queue pairs, with no syscalls, no socket, no netfilter, and critically, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; and no host network namespace either. Neither side of this comparison, host or container, touches the kernel networking stack RDMA is built to avoid. None of this post’s reasoning (dispatch cost, concurrency, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;%soft&lt;/code&gt; CPU time) applies to an RDMA path, because none of that machinery is in an RDMA path to begin with. Whether Cilium/netkit adds overhead to RDMA traffic is a different, harder question this test doesn’t answer. It needs SR-IOV or RDMA-aware device plugins to even test, and this cluster’s hardware doesn’t have them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;the-rule-sharpened&quot;&gt;The rule, sharpened&lt;/h2&gt;

&lt;blockquote&gt;
  &lt;p&gt;For east-west traffic: container/netkit wins when a request’s cost is dominated by network dispatch &lt;strong&gt;under concurrent load&lt;/strong&gt;. Host wins, or it’s a wash, when the cost is dominated by CPU/lock serialization the datapath can’t touch, or when there’s no concurrency for a cheaper datapath to save CPU across in the first place.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every east-west result here fits that once you check for the two things that break a naive read of it: whether the “container wins” is actually just a faster underlying network path (the cloud provider’s private vs. public network, nothing to do with Cilium), and whether “dispatch-bound” work has enough concurrent volume behind it for the saving to compound. Kafka’s pull-based replication and a synchronous MPI ping-pong both look dispatch-bound on paper and both behave differently: not because the rule is wrong, but because concurrency and protocol shape turned out to matter as much as “does this workload talk to the network a lot.”&lt;/p&gt;

&lt;p&gt;North-south doesn’t fit that rule, and I’m not stretching it to; it’s answering a different question (external client through a service redirect, not two matched endpoints on the same kind of path) with a different likely mechanism (a cheap client-side eBPF hook, not a busy datapath saving CPU under load). Keep the two traffic shapes separate when you’re reasoning about your own workload; conflating them is exactly how a naive read goes wrong.&lt;/p&gt;

&lt;p&gt;If you’re deciding whether to worry about CNI overhead for a specific workload, the question that actually predicts the answer isn’t “is this network-heavy”; it’s “does the load stack up requests fast enough for a cheaper per-packet datapath to matter.” A quorum-writing database under real concurrent traffic: probably fine, possibly faster. A single long-running batch job doing one synchronous exchange at a time: measure it, don’t assume.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key takeaways&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;For east-west traffic (pod-to-pod, most of this post), Cilium’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; datapath beats a bare host socket specifically when a service handles many concurrent requests (proven via CPU profiling, not assumed).&lt;/li&gt;
  &lt;li&gt;It loses, or ties, when the bottleneck is server-side: a single-threaded command loop (Redis), row-lock contention (PostgreSQL), or a strictly synchronous 1:1 exchange (MPI) with no concurrency for a cheaper datapath to save CPU across.&lt;/li&gt;
  &lt;li&gt;Two east-west results looked like exceptions until traced to mechanism: Kafka’s pull-based replication and a raw MPI ping-pong both behave differently from Raft- and WAL-style “wait for one ack” protocols.&lt;/li&gt;
  &lt;li&gt;North-south (external client to service) is a different traffic shape entirely: container still wins there, but via Cilium’s host-reachable-services (a client-side eBPF socket redirect), not netkit or concurrent dispatch savings.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit&lt;/code&gt; is still relatively new (merged in Linux 6.7) and nowhere near as battle-tested as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;; you may expect rough edges and undiscovered issues a decade-old, widely deployed mechanism has already had sanded down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The best approach is still to profile and trace the actual bottlenecks, in the application, the runtime, and the host, whether that’s a bare Linux process or a container, and tune configuration parameters from there, rather than assume either side wins by default. And weigh that against the right tradeoff for your system: deterministic peak performance versus operational simplicity.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;We expect life to be black and white; the more we delve, the more we find it still leans towards either black or white, just rarely the same one twice.&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;a href=&quot;https://isovalent.com/blog/post/cilium-netkit-a-new-container-networking-paradigm-for-the-ai-era/&quot;&gt;Cilium netkit: The Final Frontier in Container Networking Performance&lt;/a&gt;. Nico Vibert, Isovalent, July 2024 (updated July 2024). The primary source on netkit’s design and the mechanism behind its performance (per-CPU backlog queue skip, L3-by-default), plus the ByteDance proof-of-concept and Meta production data points cited above.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.yadutaf.fr/2025/07/01/introduction-to-linux-netkit-interfaces-with-a-grain-of-ebpf/&quot;&gt;Introduction to Linux netkit interfaces with a grain of eBPF&lt;/a&gt;. yadutaf, July 2025. A clear walkthrough of netkit’s primary/peer device model and its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit/primary&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit/peer&lt;/code&gt; eBPF hooks.&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://blog.yadutaf.fr/2025/09/16/creating-a-yogurt-phone-with-netkit-ebpf&quot;&gt;Creating a Yogurt Phone with netkit eBPF&lt;/a&gt;. yadutaf, September 2025. A deeper look at cross-namespace routing with netkit, including the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;netkit_xnet()&lt;/code&gt; metadata-scrubbing step and why &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf_redirect()&lt;/code&gt; is needed in place of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;veth&lt;/code&gt;’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bpf_redirect_peer()&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><category term="Linux" /><category term="Kubernetes" /><category term="Networking" /><category term="netkit" /><category term="Benchmarking" /><summary type="html">Six workloads, one self-managed cluster, and a question I kept getting wrong until I measured it: does the extra hop through a container’s CNI add real network overhead versus the same binary running as a plain Linux process on the bare host, or does Cilium’s netkit datapath actually eliminate that bottleneck?</summary></entry><entry><title type="html">My Growing Affinity for Skiing - A Reincarnation for Sports</title><link href="https://hrishi.dev/personal/skiing/2026/08/19/growing-love-for-skiing.html" rel="alternate" type="text/html" title="My Growing Affinity for Skiing - A Reincarnation for Sports" /><published>2026-08-19T22:38:00-05:00</published><updated>2026-08-19T22:38:00-05:00</updated><id>https://hrishi.dev/personal/skiing/2026/08/19/growing-love-for-skiing</id><content type="html" xml:base="https://hrishi.dev/personal/skiing/2026/08/19/growing-love-for-skiing.html">&lt;p&gt;&lt;em&gt;A personal look back at how skiing went from something I tried once to something I look forward to every season.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;Usually I don’t share personal things. Turns out all it took to change my mind was falling down a mountain a few hundred times.&lt;/p&gt;

&lt;h2 id=&quot;how-it-started&quot;&gt;How it started&lt;/h2&gt;

&lt;p&gt;Before even saying how it started, I should say that looking back at my childhood and teenage years, I always wanted to
become some sort of sportsman or athlete; becoming a software engineer was never the plan. (I used to dismantle all sorts of electronics and mechanical things, sometimes putting them back together and sometimes producing what could generously be called abstract sculpture, which cost my parents a small fortune in replacements, and might explain what led me to where I am today).
I was a competitive sportsman growing up and never imagined my life without sports. There
used to be a time when I’d be on the playground as early as 5am and still the last person standing and practicing there at 10pm, which, looking back, was either discipline or an early
symptom of an addiction. It was consistently like that. Every day I used to dream about sports. Growing up in India, it was mostly about Cricket, but among others
I did well enough in Badminton, Swimming, Skating, Volleyball, Cycling and Football.&lt;/p&gt;

&lt;p&gt;Down the line life happens, and I left sports completely, like flipping a kill switch, around my late teens. RIP athletic career, gone but not forgotten.&lt;/p&gt;

&lt;p&gt;After almost 20 years, at the age of 36, I happened to join a company where everyone seemed quite passionate about some sort of sport, which was mildly alarming for someone whose most recent sport was competitive sitting. In April 2023 I was on a road trip through the Scandinavian nations, and while passing
through Hemsedal in Norway, I stopped randomly at a ski resort. My friends and I, dressed for the occasion in pajamas and sneakers because we hadn’t planned this at all, were standing by the ski lift when a generous lady offered us a free ride up, possibly out of kindness, possibly because we looked like lunatics.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/hemsedal-norway-pajamas-ski-lift.jpg&quot; alt=&quot;Standing in pajamas and sneakers on a snowy deck in Hemsedal, Norway, with mountains in the background&quot; /&gt;
&lt;em&gt;Pretending to smile after falling off on the ramp, thanks to my sneakers&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But standing there watching people of all ages (kids, teens, adults, and whole families), I quite felt like I’d want to give it a try someday.&lt;/p&gt;

&lt;p&gt;Fast forward, my former workplace DRW arranged an office skiing trip to Morzine, France in March 2024(not sure to call it workplace or home, mostly home). I thought of signing up
for it, despite a few colleagues warning me that breaking my bones was quite probable, which in hindsight was less a warning and more a professional forecast. Finally I took
a leap of faith and started getting myself ready for the skiing trip.
My colleagues(friends) seemed like experts, and I wanted to make sure I’d be able to keep up. So I took
this on like a mission and started taking lessons north of London at &lt;a href=&quot;https://thesnowcentre.com/hemel-hempstead/&quot;&gt;The Snow Centre Hemel Hempstead&lt;/a&gt;.
After roughly 18-20 hours of lessons I thought I was ready to conquer the slopes :P. But real slopes
are just out there to prove you wrong, gravity included, and hell yeah, I was nowhere close to what I’d been imagining and
would need to keep up with friends while skiing. But after that one skiing trip I felt I’d learned something new,
and I think it was an unconscious reminder for me: learning a new sport gives you the confidence that you could do something worthwhile you’ve never done before in 
life. I came back with pretty much mesmerizing experiences, some fond memories, and a new nickname (I’ve been labeled a loose cannon by a colleague ever since, 
a title I’ve chosen to wear with pride, mostly because the alternatives suggested were worse.)&lt;/p&gt;
&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; In between all this skiing, I also made it to KubeCon EU Paris 2024, because apparently the adventures never stop. A good friend of mine presented a talk on Confidential Containers (CoCo): &lt;a href=&quot;https://www.youtube.com/watch?v=Ko0o5_hpmxI&quot;&gt;https://www.youtube.com/watch?v=Ko0o5_hpmxI&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;img src=&quot;/assets/morzine-mountain-view.jpg&quot; alt=&quot;A view of snow-capped mountains and ski slopes from a piste in Morzine, France&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;what-keeps-pulling-me-back&quot;&gt;What keeps pulling me back&lt;/h2&gt;

&lt;p&gt;After this, I kept doing more skiing trips: France (Chamonix, Alpe d’Huez), Finland (Lapland Levi), Germany (Garmisch), and Austria (St. Anton, Saalbach, and Ischgl), every year through last season, 2026.&lt;/p&gt;

&lt;p&gt;Every season I go in with the mindset to improve, just like any athlete pushes out of their comfort zone to
keep improving, or at least out of the beginner slope and into something with actual gradient. That’s something sports can offer in life: getting disciplined.
Another reason I thought I’d share this experience is that it’s never too late to learn something you’ve always dreamed of, and the key to
getting better at anything is practice.&lt;/p&gt;

&lt;p&gt;Of course, the nature in those mountains is incredibly beautiful, and if you appreciate deep silence, there are certain places in the mountains where you can 
really find it, usually right before you fall over and break it yourself.&lt;/p&gt;

&lt;p&gt;For me, being on the ski slope has eventually become quite meditative, as if my mind has escaped
reality and is just happy in the moment, right up until the moment involves a tree.&lt;/p&gt;

&lt;p&gt;The thing I like about skiing is that it’s a family sport. Everyone seems to enjoy the atmosphere, and this place has
something to offer for everyone in the family (at least, enough to make some good memories, and possibly a few good stories about someone else falling over).&lt;/p&gt;

&lt;p&gt;Of course, something that goes against my nature: yes, I eventually found a love of Après-ski, where people are in a
celebratory mood and there’s real joy, plus questionable dancing on tables that I will neither confirm nor deny participating in. So far my favorite has been &lt;a href=&quot;https://www.mooserwirt.at/en/&quot;&gt;Mooserwirt&lt;/a&gt; in St. Anton, which is a true test of your real skiing, snowboarding, and snowballing skills on the way down after the après-ski party (this is where I really learned how to get back up on skis on a slope, since pretty much everyone passing by fell over too).&lt;/p&gt;

&lt;p&gt;At last, wherever you go for skiing, it’s one of those things where people are generally welcoming, warm, and
share true hospitality, even to a guy who once showed up to a ski lift in pajamas.&lt;/p&gt;

&lt;h2 id=&quot;some-of-the-best-skiing-places-ive-been-so-far&quot;&gt;Some of the best skiing places I’ve been so far&lt;/h2&gt;

&lt;p&gt;Every place has its own beauty to experience, so it would be a bit unfair to say which is the “best.”&lt;/p&gt;

&lt;p&gt;But so far I’d say I enjoy the Austrian Alps most, with long pistes and a bit more adventure. I really enjoyed night skiing in Saalbach, which is basically regular skiing but with extra ways to lose track of where the ground is. You can reach pretty much every skiing destination by train: fly into Innsbruck or Munich and take a train from there.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/saalbach-night-skiing-1.jpg&quot; alt=&quot;A floodlit ski piste at night in Saalbach, Austria, lined with lights and surrounded by dark mountains&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/saalbach-night-skiing-2.jpg&quot; alt=&quot;A wide floodlit ski slope at dusk in Saalbach, Austria, with the village lights below&quot; /&gt;
&lt;em&gt;Night skiing in Saalbach&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And of course I love the French Alps, Bonjour!
Chamonix offers a really wide range of pistes, and the place itself is quite pretty!&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/aiguille-du-midi-chamonix.jpg&quot; alt=&quot;The Aiguille du Midi peak station in Chamonix, France, rising above the clouds&quot; /&gt;
&lt;em&gt;Aiguille du Midi peak station in Chamonix, France&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lapland (famously known for the Santa Claus Village) has its own beauty. The slopes are really comfortable, especially for beginners, or people like me who peaked at “comfortable.” If you enjoy slow cross-country skiing you can see how beautiful the village is, especially the cabins and forests. It also offers plenty of fun activities, like snowmobile riding and husky rides, and if you have a caravan, there are plenty of nice places for camping.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/lapland-cross-country-forest.jpg&quot; alt=&quot;Snow-covered forest with tall pine trees under a blue sky with wispy clouds in Lapland, Finland&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/lapland-cross-country-cabin.jpg&quot; alt=&quot;A traditional Finnish wooden cabin surrounded by snowy pine trees in Lapland, Finland&quot; /&gt;
&lt;em&gt;Cross country skiing experiences&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Something I found very deep in Finnish culture is the level of trust in people and society.&lt;/p&gt;

&lt;h2 id=&quot;lessons-learned-on-the-slopes&quot;&gt;Lessons learned on the slopes&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Lessons get you started, reps make you good.&lt;/strong&gt; Instructors give you the fundamentals, but it’s the volume of runs that actually improves you, usually one small bruise at a time.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Layer smart, not thick.&lt;/strong&gt; Staying warm matters, but over-layering just means you sweat through your gear instead. For me, one layer of thermals and ski pants has been plenty, with the jacket being the one call I make on the day: a light jacket on sunny days, a warmer one on gloomy ones. For ladies wanting to actually get this right, follow &lt;a href=&quot;https://www.instagram.com/alpineflorine/?hl=en&quot;&gt;alpineflorine&lt;/a&gt; on Instagram, she breaks down layering far better than I ever could. A neck warmer, though, is genuinely life-changing: it keeps your neck warm, stops cold air sneaking in through the jacket, and on the higher-speed slopes it’s the difference between your face feeling the wind and not. Get one before you get anything else.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Contacts beat glasses, every time.&lt;/strong&gt; Goggles plus a neck warmer is a fog machine for spectacles. Contact lenses solve it instantly, and let you swap in sunglasses whenever the sun comes out.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Sunscreen and lip balm are not optional.&lt;/strong&gt; Mountain sun and wind will wreck your skin faster than you’d expect, cold weather or not.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Book early, or pay for it later.&lt;/strong&gt; Prices climb fast once peak season hits, so lock in the trip well before everyone else has the same idea.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Measure yourself against yesterday, not the person next to you.&lt;/strong&gt; Progress is personal, easier said than felt when a six-year-old blazes past you on the same run.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Get insurance, seriously.&lt;/strong&gt; Skiing looks graceful right up until it isn’t, so get a policy that actually covers winter sports before things go sideways, not after.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;whats-next&quot;&gt;What’s next&lt;/h2&gt;

&lt;p&gt;Hoping to explore the &lt;a href=&quot;https://www.ruka.fi/en/events/polar-night-light-festival&quot;&gt;Polar Night Light Festival, Finland&lt;/a&gt;. I’ve never tried off-piste skiing and hope to try it soon. Along with that,
I’d be very keen to try &lt;a href=&quot;https://guides-mont-blanc.com/en/activites/vallee-blanche/?jsf=jet-engine:query_voyages&amp;amp;tax=themes_voyage:65&quot;&gt;the Vallée Blanche in Chamonix, France&lt;/a&gt;, a legendary 15 to 20 km off-piste glacier ski descent. It starts at the 3,842 m peak of the Aiguille du Midi cable car and drops roughly 2,000 vertical meters through breathtaking high-alpine glacial terrain. And if I’m ever able to make some fortune, a one-time heli-skiing trip with the &lt;a href=&quot;https://www.youtube.com/watch?v=H4QTWfBCXw8&quot;&gt;Viking Heliskiing, Iceland&lt;/a&gt; crew is definitely on the list, albeit unlikely, alas.&lt;/p&gt;

&lt;p&gt;With all this, this has also been the first time I’ve followed an entire Winter Olympics, 2026, thoroughly enjoying everything from figure skating to all the skiing and snowboarding competitions. Someday I’d love to see the Olympics in person.&lt;/p&gt;

&lt;p&gt;For this, I owe a big thank you to my former colleagues(but thy are like friends and family), who reintroduced me to my love for sports through skiing, ping pong, and cycling.&lt;/p&gt;

&lt;p&gt;At last, I hope life keeps skiing as exciting as I’m writing about it, some of the love for sports I’ve rediscovered, bruises and all.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/loose-cannon-fall.jpg&quot; alt=&quot;Lying flat on a snowy slope after a fall, with two people looking on in the background&quot; /&gt;
&lt;em&gt;Loose cannon&lt;/em&gt;&lt;/p&gt;

&lt;figure style=&quot;text-align: center;&quot;&gt;
  &lt;video controls=&quot;&quot; muted=&quot;&quot; playsinline=&quot;&quot; preload=&quot;metadata&quot; style=&quot;max-width: 360px; width: 100%; display: block; margin: 0 auto;&quot;&gt;
    &lt;source src=&quot;/assets/skiing.mp4&quot; type=&quot;video/mp4&quot; /&gt;
    Your browser does not support the video tag.
  &lt;/video&gt;
  &lt;figcaption&gt;&lt;em&gt;Sometimes you want to relive the moment, yet don&apos;t want to change anything&lt;/em&gt;&lt;/figcaption&gt;
&lt;/figure&gt;</content><author><name></name></author><category term="Personal" /><category term="Skiing" /><summary type="html">A personal look back at how skiing went from something I tried once to something I look forward to every season.</summary></entry><entry><title type="html">From Power Button to Shell Prompt, Part 1: The Linux Boot Process From Firmware to Login</title><link href="https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html" rel="alternate" type="text/html" title="From Power Button to Shell Prompt, Part 1: The Linux Boot Process From Firmware to Login" /><published>2026-05-05T01:00:00-05:00</published><updated>2026-05-05T01:00:00-05:00</updated><id>https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1</id><content type="html" xml:base="https://hrishi.dev/linux/os/systems/2026/05/05/os-boot-process-1.html">&lt;p&gt;&lt;em&gt;A stage-by-stage look at what happens from the moment you press the power button to the moment your shell is ready for input&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Pressing the power button on a modern computer triggers a carefully orchestrated sequence of events spanning
firmware, bootloaders, the kernel, and init systems. Each layer hands off to the next with increasing
sophistication.&lt;/p&gt;

&lt;p&gt;This post walks through every stage of that journey on a modern x86-64 Linux system, from the first CPU instruction
executed out of reset to the login prompt waiting for your credentials.&lt;/p&gt;

&lt;p&gt;This is a 3-part series that follows the same seven stages from a laptop to a rack of bare-metal servers:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Part 1 (this post): From Firmware to Login&lt;/strong&gt;, a disk boot end to end: the reset vector, BIOS/UEFI, POST, shim
and GRUB2, kernel init, initramfs, systemd, and PAM.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html&quot;&gt;Part 2: PXE Network Boot with Metal³ Bare Metal Operator and Ironic&lt;/a&gt;&lt;/strong&gt;, the same stages on a server with
no operating system on its disk. Stage 3 becomes a DHCP, TFTP, and HTTP conversation, a DHCP relay carries it
across VLANs, and the Bare Metal Operator drives Ironic to boot a RAM-only agent that writes the OS image.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/19/os-boot-process-3.html&quot;&gt;Part 3: Automated Host Provisioning with Metal³, CAPM3, and Cluster API&lt;/a&gt;&lt;/strong&gt;, Cluster API on top of that
machinery, so one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Cluster&lt;/code&gt; manifest claims, images, and joins a pool of inventoried hosts: the full call
sequence, the object model, and the two state machines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/os-boot-process-stages.png&quot; alt=&quot;A seven-stage diagram of the Linux boot sequence: Firmware, POST, Bootloader, Kernel Init, initramfs, systemd, and Login/Shell&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-1-reset-vector-and-firmware-biosuefi&quot;&gt;Stage 1: Reset Vector and Firmware (BIOS/UEFI)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-2-post-power-on-self-test&quot;&gt;Stage 2: POST (Power-On Self-Test)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-3-bootloader&quot;&gt;Stage 3: Bootloader&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-4-kernel-initialization&quot;&gt;Stage 4: Kernel Initialization&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-5-initramfs-early-userspace&quot;&gt;Stage 5: initramfs (Early Userspace)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-6-init-system-systemd&quot;&gt;Stage 6: Init System (systemd)&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#stage-7-login-and-shell&quot;&gt;Stage 7: Login and Shell&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#putting-it-all-together&quot;&gt;Putting It All Together&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-1-reset-vector-and-firmware-biosuefi&quot;&gt;Stage 1: Reset Vector and Firmware (BIOS/UEFI)&lt;/h2&gt;

&lt;h3 id=&quot;the-first-instruction&quot;&gt;The First Instruction&lt;/h3&gt;

&lt;p&gt;When power is applied, the CPU does not start executing from RAM: it’s empty at power-on. Instead, every
processor has a hardwired &lt;strong&gt;reset vector&lt;/strong&gt; mapped by the chipset to a ROM chip on the motherboard containing
the firmware. The address is architecture-defined:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Architecture&lt;/th&gt;
      &lt;th&gt;Reset vector&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;x86-64&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xFFFFFFF0&lt;/code&gt;, 16 bytes below the top of 32-bit address space, entered in 16-bit real mode&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;ARM64 (AArch64)&lt;/td&gt;
      &lt;td&gt;Configured via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RVBAR_EL3&lt;/code&gt; register, entered at Exception Level 3 (EL3)&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;On x86-64, the reset vector holds a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JMP&lt;/code&gt; that transfers into the full firmware image. On ARM64, the SoC’s
trusted firmware (e.g., ARM Trusted Firmware-A) runs first at EL3 before handing off to UEFI at EL2/EL1.&lt;/p&gt;

&lt;h3 id=&quot;bios-vs-uefi&quot;&gt;BIOS vs. UEFI&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;BIOS (Basic Input/Output System)&lt;/strong&gt; is the legacy firmware standard from the late 1970s. It operates in 16-bit
real mode and relies on a 512-byte &lt;strong&gt;Master Boot Record (MBR)&lt;/strong&gt; at the start of the boot disk. The MBR contains
a first-stage bootloader and a partition table, all crammed into 512 bytes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;UEFI (Unified Extensible Firmware Interface)&lt;/strong&gt;, formalized by the &lt;a href=&quot;https://uefi.org/specifications&quot;&gt;UEFI Specification&lt;/a&gt;,
replaced BIOS and brings several critical improvements:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;BIOS&lt;/th&gt;
      &lt;th&gt;UEFI&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Mode at startup&lt;/td&gt;
      &lt;td&gt;16-bit real mode&lt;/td&gt;
      &lt;td&gt;32/64-bit protected mode&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Boot partition&lt;/td&gt;
      &lt;td&gt;MBR (512 bytes)&lt;/td&gt;
      &lt;td&gt;EFI System Partition (FAT32, megabytes)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Bootloader size&lt;/td&gt;
      &lt;td&gt;~446 bytes&lt;/td&gt;
      &lt;td&gt;Full PE/COFF executables&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Secure Boot&lt;/td&gt;
      &lt;td&gt;No&lt;/td&gt;
      &lt;td&gt;Yes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Network boot&lt;/td&gt;
      &lt;td&gt;Vendor extensions&lt;/td&gt;
      &lt;td&gt;Built-in PXE and HTTP boot&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;UEFI firmware reads the &lt;strong&gt;EFI System Partition (ESP)&lt;/strong&gt;, a FAT32 partition that contains bootloader executables
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*.efi&lt;/code&gt; files). The firmware itself understands filesystems, which is a significant leap over BIOS.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-2-post-power-on-self-test&quot;&gt;Stage 2: POST (Power-On Self-Test)&lt;/h2&gt;

&lt;p&gt;Before handing off to a bootloader, the firmware runs &lt;strong&gt;POST&lt;/strong&gt;, a series of hardware diagnostics:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;CPU test&lt;/strong&gt;: verify the processor is functioning correctly&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Memory initialization&lt;/strong&gt;: train and test DRAM, set up memory channels and timings&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Chipset initialization&lt;/strong&gt;: configure the PCH (Platform Controller Hub), PCIe lanes, clocks&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Device enumeration&lt;/strong&gt;: discover PCI/PCIe devices, assign I/O ports and memory-mapped I/O ranges&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Video initialization&lt;/strong&gt;: bring up a display so error messages can be shown&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Peripheral detection&lt;/strong&gt;: USB, SATA controllers, NVMe drives&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The beep codes you may have heard from old machines are POST error signals. One long beep, for example, usually
means a memory failure. Modern UEFI systems display graphical error screens instead.&lt;/p&gt;

&lt;p&gt;After POST, the firmware has a complete picture of the hardware and constructs the
&lt;strong&gt;&lt;a href=&quot;https://uefi.org/acpi&quot;&gt;ACPI tables&lt;/a&gt;&lt;/strong&gt;, data structures that describe the hardware topology to the OS.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-3-bootloader&quot;&gt;Stage 3: Bootloader&lt;/h2&gt;

&lt;h3 id=&quot;uefi-path-the-efi-application&quot;&gt;UEFI Path: The EFI Application&lt;/h3&gt;

&lt;p&gt;On a UEFI system, the firmware consults its NVRAM boot entries (managed with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;efibootmgr&lt;/code&gt;) to find an EFI
binary to execute. The ESP is a FAT32 partition; on a running Linux system it is mounted at &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/boot/efi&lt;/code&gt;, so
firmware-internal paths like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/EFI/ubuntu/shimaa64.efi&lt;/code&gt; appear on disk as &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/boot/efi/EFI/ubuntu/shimaa64.efi&lt;/code&gt;.&lt;/p&gt;

&lt;!-- [PERSONAL EXPERIENCE] --&gt;
&lt;p&gt;A typical Ubuntu ARM64 ESP, from a machine I booted and inspected while writing this post, looks like this:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/boot/efi/EFI/BOOT/BOOTAA64.EFI    ← removable-media fallback (copy of shim)
/boot/efi/EFI/ubuntu/
    shimaa64.efi                    ← NVRAM entry points here
    grubaa64.efi
    mmaa64.efi
    grub.cfg
    BOOTAA64.CSV
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;If NVRAM entries are wiped (firmware update, hardware reset), the firmware falls back to the well-known path
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/EFI/BOOT/BOOTAA64.EFI&lt;/code&gt; (i.e. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/boot/efi/EFI/BOOT/BOOTAA64.EFI&lt;/code&gt; on disk). On this machine that file is a
copy of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shimaa64.efi&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;BOOTAA64.CSV&lt;/code&gt; is a small text file that pairs a human-readable label with the path to the real bootloader:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;shimaa64.efi,Ubuntu,,This is the boot entry for Ubuntu
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The EFI &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fallback&lt;/code&gt; application reads this CSV to &lt;strong&gt;re-register&lt;/strong&gt; the NVRAM boot entry pointing at
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shimaa64.efi&lt;/code&gt; if it was lost. It’s a self-healing mechanism so the system can boot again after a firmware
flash clears NVRAM.&lt;/p&gt;

&lt;!-- [PERSONAL EXPERIENCE] --&gt;
&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;efibootmgr&lt;/code&gt; shows the boot configuration. This is the actual output from the same machine:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# efibootmgr -v        (device-path hex dumps trimmed)&lt;/span&gt;

BootCurrent: 0003
Timeout: 5 seconds
BootOrder: 0003,0000,0002
Boot0000&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; UiApp	FvVol&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;64074afe-340a-4be6-94ba-91b5b4d0f71e&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;/FvFile&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;462caa21-7614-4503-836e-8ab6f4662331&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Boot0002&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; UEFI VBOX HARDDISK 	PciRoot&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0x0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;/Pci&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0x3,0x0&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;/SCSI&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;0,0&lt;span class=&quot;o&quot;&gt;){&lt;/span&gt;auto_created_boot_option&lt;span class=&quot;o&quot;&gt;}&lt;/span&gt;
Boot0003&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; Ubuntu	HD&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;1,GPT,1549550d-11b7-41cc-a243-e4ea041f7dd1,0x800,0x165800&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;/&lt;span class=&quot;se&quot;&gt;\E&lt;/span&gt;FI&lt;span class=&quot;se&quot;&gt;\u&lt;/span&gt;buntu&lt;span class=&quot;se&quot;&gt;\s&lt;/span&gt;himaa64.efi

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;secure-boot-and-the-shim&quot;&gt;Secure Boot and the Shim&lt;/h3&gt;

&lt;p&gt;On Secure Boot-enabled systems (the default on most Ubuntu installs) the firmware won’t execute an arbitrary
EFI binary. It has to be signed by a trusted key. The firmware ships with Microsoft’s key in its database, and
Microsoft co-signs a small EFI binary called the &lt;strong&gt;shim&lt;/strong&gt;. The actual boot chain becomes:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Firmware → shimaa64.efi (signed by Microsoft)
              ↓ verifies against distro key in MOK database
           grubaa64.efi (signed by Canonical)
              ↓ verifies kernel signature
           vmlinuz (signed by Canonical)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mmaa64.efi&lt;/code&gt; (MokManager) is a helper that runs when you need to enroll or manage &lt;strong&gt;Machine Owner Keys (MOK)&lt;/strong&gt;,
for example when you install a custom kernel module that needs signing.&lt;/p&gt;

&lt;h3 id=&quot;grub2&quot;&gt;GRUB2&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.gnu.org/software/grub/manual/grub/grub.html&quot;&gt;GRUB (Grand Unified Bootloader)&lt;/a&gt;&lt;/strong&gt; is the most
common bootloader on Linux systems. After shim hands off, GRUB:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Reads its configuration from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/boot/grub/grub.cfg&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Presents a menu of kernel choices (with a timeout)&lt;/li&gt;
  &lt;li&gt;Loads the selected kernel image (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vmlinuz&lt;/code&gt;) and initial RAM disk (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;initrd&lt;/code&gt;) into memory&lt;/li&gt;
  &lt;li&gt;Passes a &lt;strong&gt;kernel command line&lt;/strong&gt;: a string of parameters like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;root=/dev/sda1 ro quiet splash&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Transfers control to the kernel entry point&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;shim → grubaa64.efi → reads grub.cfg → loads vmlinuz + initrd → jumps to kernel
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The kernel image (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vmlinuz&lt;/code&gt;) is a compressed executable. GRUB places it at a specific memory address and calls
the kernel’s decompression stub, which unpacks the real kernel and jumps into it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-4-kernel-initialization&quot;&gt;Stage 4: Kernel Initialization&lt;/h2&gt;

&lt;h3 id=&quot;early-boot-decompression-and-setup&quot;&gt;Early Boot: Decompression and Setup&lt;/h3&gt;

&lt;p&gt;The kernel entry point (in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;arch/x86/boot/header.S&lt;/code&gt;) runs in a special mode. Its first job is:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Decompress itself&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vmlinuz&lt;/code&gt; is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;zImage&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bzImage&lt;/code&gt;, gzip/lz4/zstd-compressed. The decompressor
unpacks the kernel to a safe memory location.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Switch to 64-bit long mode&lt;/strong&gt;: the CPU starts in real or protected mode; the kernel sets up page tables and
transitions to 64-bit mode.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Establish initial page tables&lt;/strong&gt;: a minimal identity mapping to get execution running.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;start_kernel&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_kernel()&lt;/code&gt;&lt;/h3&gt;

&lt;p&gt;After decompression and mode switches, execution reaches &lt;a href=&quot;https://github.com/torvalds/linux/blob/9207d47f966be9f4d52e7e0119ac2b7a7e366f3e/init/main.c#L1016&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;start_kernel()&lt;/code&gt;&lt;/a&gt;
in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;init/main.c&lt;/code&gt;, the real starting point of the kernel’s C code. This function calls hundreds of initialization
routines in sequence:&lt;/p&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;asmlinkage&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__visible&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__init&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;__no_sanitize_address&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;start_kernel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;void&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;setup_arch&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;command_line&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;// arch-specific: ACPI, NUMA, memory map&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;mm_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                   &lt;span class=&quot;c1&quot;&gt;// memory management subsystem&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;sched_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                &lt;span class=&quot;c1&quot;&gt;// scheduler&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rcu_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                  &lt;span class=&quot;c1&quot;&gt;// RCU synchronization&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;init_IRQ&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                  &lt;span class=&quot;c1&quot;&gt;// interrupt controller&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;time_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                 &lt;span class=&quot;c1&quot;&gt;// timers and clocks&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;rest_init&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;();&lt;/span&gt;                 &lt;span class=&quot;c1&quot;&gt;// spawn PID 1&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Key subsystems initialized here:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Memory management&lt;/strong&gt;: the buddy allocator, slab allocator, vmalloc&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Scheduler&lt;/strong&gt;: CFS (Completely Fair Scheduler) data structures&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Interrupt subsystem&lt;/strong&gt;: IDT (Interrupt Descriptor Table), APIC&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;VFS (Virtual Filesystem Switch)&lt;/strong&gt;: the abstraction layer over all filesystems&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Driver model&lt;/strong&gt;: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kobject&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sysfs&lt;/code&gt; infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;device-detection-and-driver-binding&quot;&gt;Device Detection and Driver Binding&lt;/h3&gt;

&lt;p&gt;The kernel reads the ACPI tables and walks the PCI bus, building a device tree. For each discovered device,
it matches against registered drivers using the bus’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;match()&lt;/code&gt; function. When a match is found, the driver’s
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;probe()&lt;/code&gt; function runs: it allocates resources, maps registers, and registers the device with higher-level
subsystems (block layer, network stack, etc.).&lt;/p&gt;

&lt;h3 id=&quot;mounting-the-root-filesystem&quot;&gt;Mounting the Root Filesystem&lt;/h3&gt;

&lt;p&gt;The kernel needs a root filesystem (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;) to find the rest of the OS. But the real root might live on:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;an encrypted LVM volume&lt;/li&gt;
  &lt;li&gt;a software RAID array&lt;/li&gt;
  &lt;li&gt;an NVMe device requiring a driver not compiled into the kernel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This chicken-and-egg problem is solved by &lt;strong&gt;initramfs&lt;/strong&gt;.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-5-initramfs-early-userspace&quot;&gt;Stage 5: initramfs (Early Userspace)&lt;/h2&gt;

&lt;h3 id=&quot;what-is-initramfs&quot;&gt;What is initramfs?&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://www.kernel.org/doc/html/latest/admin-guide/initrd.html&quot;&gt;initramfs&lt;/a&gt;&lt;/strong&gt; (initial RAM filesystem) is a
compressed &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cpio&lt;/code&gt; archive embedded alongside the kernel or passed as a separate file by the bootloader. The
kernel extracts it into a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;tmpfs&lt;/code&gt; filesystem in memory and mounts it as the initial &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;initramfs contains:
  /bin/sh
  /sbin/init  (or systemd)
  /lib/modules/&amp;lt;kver&amp;gt;/kernel/drivers/...  (essential drivers)
  /usr/lib/systemd/system/
  /etc/crypttab
  /etc/mdadm.conf
  ...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;the-initramfs-job&quot;&gt;The initramfs Job&lt;/h3&gt;

&lt;p&gt;The init binary inside initramfs (often &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd&lt;/code&gt; or a script like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;busybox init&lt;/code&gt;) performs early setup:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Load kernel modules&lt;/strong&gt;: storage drivers (NVMe, AHCI), filesystem drivers (ext4, btrfs), crypto drivers&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Assemble storage&lt;/strong&gt;: activate RAID arrays (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mdadm&lt;/code&gt;), open LUKS volumes (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cryptsetup&lt;/code&gt;), activate LVM&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Find and mount the real root&lt;/strong&gt;: using the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;root=&lt;/code&gt; kernel parameter&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pivot_root&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;switch_root&lt;/code&gt;&lt;/strong&gt;: replace the initramfs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; with the real root filesystem&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Execute the real init&lt;/strong&gt;: hand off to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/sbin/init&lt;/code&gt; on the real root&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;switch_root&lt;/code&gt; call is irreversible: the initramfs is freed from memory and the process continues in the real root.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-6-init-system-systemd&quot;&gt;Stage 6: Init System (systemd)&lt;/h2&gt;

&lt;p&gt;Modern Linux distributions use &lt;strong&gt;systemd&lt;/strong&gt; as PID 1, the first real userspace process and parent of all others.&lt;/p&gt;

&lt;h3 id=&quot;systemds-startup-phases&quot;&gt;systemd’s Startup Phases&lt;/h3&gt;

&lt;p&gt;systemd organizes startup into &lt;strong&gt;&lt;a href=&quot;https://www.freedesktop.org/software/systemd/man/systemd.special.html&quot;&gt;targets&lt;/a&gt;&lt;/strong&gt;
(analogous to runlevels in SysV init). The default target for a desktop is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;graphical.target&lt;/code&gt;; for a server,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;multi-user.target&lt;/code&gt;. These are dependency graphs of &lt;strong&gt;units&lt;/strong&gt;.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;sysinit.target
    ↓
basic.target
    ↓
multi-user.target ──── sshd.service, NetworkManager.service, ...
    ↓
graphical.target ───── display-manager.service
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;systemd processes units in parallel wherever dependencies allow, making boot dramatically faster than sequential
SysV scripts.&lt;/p&gt;

&lt;h3 id=&quot;key-unit-types&quot;&gt;Key Unit Types&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Unit type&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
      &lt;th&gt;Example&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.service&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;A daemon or one-shot process&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sshd.service&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.mount&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;A filesystem mount point&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;home.mount&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.socket&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Socket-activated service&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd-journald.socket&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.target&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Synchronization point / group&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;network.target&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.timer&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;cron-like scheduled activation&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;fstrim.timer&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h3 id=&quot;socket-activation&quot;&gt;Socket Activation&lt;/h3&gt;

&lt;p&gt;One of systemd’s powerful features is &lt;strong&gt;socket activation&lt;/strong&gt;: systemd creates the socket &lt;em&gt;before&lt;/em&gt; starting the
service, queuing connections. The service starts on first use. This means services can declare dependencies on
each other via sockets without strict ordering. They all start in parallel, and connections simply block until
the service is ready.&lt;/p&gt;

&lt;h3 id=&quot;journal-and-logging&quot;&gt;Journal and Logging&lt;/h3&gt;

&lt;p&gt;systemd replaces syslog with &lt;strong&gt;journald&lt;/strong&gt;, a structured binary log. All stdout/stderr of services is captured
automatically. Query with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;journalctl&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;journalctl &lt;span class=&quot;nt&quot;&gt;-b&lt;/span&gt;          &lt;span class=&quot;c&quot;&gt;# logs since last boot&lt;/span&gt;
journalctl &lt;span class=&quot;nt&quot;&gt;-u&lt;/span&gt; sshd     &lt;span class=&quot;c&quot;&gt;# logs for a specific service&lt;/span&gt;
journalctl &lt;span class=&quot;nt&quot;&gt;--since&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;10 min ago&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;stage-7-login-and-shell&quot;&gt;Stage 7: Login and Shell&lt;/h2&gt;

&lt;h3 id=&quot;getty-and-login&quot;&gt;Getty and Login&lt;/h3&gt;

&lt;p&gt;For a text console, systemd starts &lt;strong&gt;getty&lt;/strong&gt; on each virtual terminal (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;agetty&lt;/code&gt; on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/tty1&lt;/code&gt;). Getty:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Opens the TTY device&lt;/li&gt;
  &lt;li&gt;Prints the login prompt&lt;/li&gt;
  &lt;li&gt;Reads the username&lt;/li&gt;
  &lt;li&gt;Calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/bin/login&lt;/code&gt;, which reads the password and authenticates via
&lt;strong&gt;&lt;a href=&quot;https://man7.org/linux/man-pages/man8/PAM.8.html&quot;&gt;PAM (Pluggable Authentication Modules)&lt;/a&gt;&lt;/strong&gt;&lt;/li&gt;
  &lt;li&gt;On success, drops privileges to the user’s UID/GID and execs the user’s shell&lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;pam&quot;&gt;PAM&lt;/h3&gt;

&lt;p&gt;PAM separates authentication policy from the applications that need it. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/pam.d/login&lt;/code&gt; configuration
chains modules:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;auth    required   pam_unix.so      ← check /etc/shadow
auth    optional   pam_google_authenticator.so  ← TOTP 2FA
session required   pam_limits.so    ← apply ulimits from /etc/security/limits.conf
session required   pam_systemd.so   ← register session with logind
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;PAM sits on top of the same kernel-level security primitives (namespaces, capabilities, LSMs) covered in my
recap of the &lt;a href=&quot;https://hrishi.dev/linux/security/kernel/isolation/apis/2025/11/02/linux-security-isolation-apis-course.html&quot;&gt;man7.org Linux Security and Isolation APIs course&lt;/a&gt;,
if you want to go a layer deeper than PAM itself.&lt;/p&gt;

&lt;h3 id=&quot;shell-startup&quot;&gt;Shell Startup&lt;/h3&gt;

&lt;p&gt;Once login succeeds, the shell (e.g., &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bash&lt;/code&gt;) is execed. Bash reads startup files in order:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/etc/profile          ← system-wide environment
~/.bash_profile       ← user login setup (sources ~/.bashrc)
~/.bashrc             ← interactive shell config (aliases, prompt, PATH)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;At this point, a shell prompt appears and the OS is fully booted.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;putting-it-all-together&quot;&gt;Putting It All Together&lt;/h2&gt;

&lt;p&gt;The timeline below is an &lt;strong&gt;illustrative example&lt;/strong&gt;, not a benchmark from a specific machine, since actual timings
vary significantly with storage type (NVMe vs. spinning disk), firmware implementation, and how many services a
distribution starts by default. Use &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd-analyze&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;systemd-analyze blame&lt;/code&gt; on your own machine to see
real, measured numbers for this breakdown.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[0 ms]      CPU reset → firmware ROM at 0xFFFFFFF0
[10 ms]     POST: memory training, device enumeration, ACPI table construction
[500 ms]    UEFI boot manager loads grubx64.efi from ESP
[600 ms]    GRUB displays menu, loads vmlinuz + initrd into RAM
[700 ms]    Kernel decompresses, switches to 64-bit mode
[800 ms]    start_kernel(): mm_init, sched_init, IRQ init, driver probing
[900 ms]    initramfs: load storage drivers, assemble volumes, mount real root
[1.0 s]     switch_root → PID 1 = systemd on the real root
[1.5 s]     systemd activates sysinit.target → basic.target
[2.0 s]     Network, storage, logging services start in parallel
[3.0 s]     multi-user.target reached: system is operational
[3.5 s]     graphical.target: display manager starts
[4.0 s]     Login prompt appears
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The general pattern holds even if your own numbers differ: NVMe storage and UEFI cut boot time dramatically
compared to the old BIOS-plus-spinning-disk path, where POST alone could take several seconds and a full boot
commonly ran 30-60 seconds.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;closing-thoughts&quot;&gt;Closing Thoughts&lt;/h2&gt;

&lt;p&gt;Understanding this stack, from the reset vector through PAM, is invaluable when debugging boot failures,
hardening systems, building embedded Linux images, or simply satisfying the curiosity of knowing what your
machine is actually doing before that shell prompt appears. Every stage is a narrow handoff: firmware finds a
bootloader, the bootloader finds a kernel, the kernel finds a root filesystem, and PID 1 finds the services that
finally hand you a prompt.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;The CPU always starts at a hardwired &lt;strong&gt;reset vector&lt;/strong&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0xFFFFFFF0&lt;/code&gt; on x86-64, mapped to firmware ROM rather than RAM.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;UEFI replaced BIOS&lt;/strong&gt; with 64-bit execution, a FAT32 EFI System Partition, Secure Boot, and a full driver model.&lt;/li&gt;
    &lt;li&gt;The bootloader’s job is narrow: find the kernel, load it, pass parameters, and jump. GRUB2 does this via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shim → grubaa64.efi → vmlinuz&lt;/code&gt;.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;initramfs&lt;/strong&gt; solves the chicken-and-egg problem of needing drivers to mount the filesystem that contains the drivers.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;systemd&lt;/strong&gt; parallelizes init by activating units based on a dependency graph rather than a static script order.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;PAM&lt;/strong&gt; decouples authentication policy from the applications that need it, enabling pluggable 2FA, LDAP, and biometrics.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;up-next&quot;&gt;Up Next&lt;/h2&gt;

&lt;p&gt;Everything in this part assumes a disk with an operating system on it. A server fresh from the rack has none.
&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/linux/os/systems/2026/09/18/os-boot-process-2.html&quot;&gt;Part 2: PXE Network Boot with Metal³ Bare Metal Operator and Ironic&lt;/a&gt;&lt;/strong&gt; takes the same seven stages onto
that server: Stage 3 becomes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;PXE ROM → DHCP → TFTP → iPXE → HTTP&lt;/code&gt;, the DHCP relay carries it across VLANs, and
the first Linux to boot is a RAM-only agent that installs the real one.&lt;/p&gt;</content><author><name></name></author><category term="Linux" /><category term="OS" /><category term="Systems" /><summary type="html">A stage-by-stage look at what happens from the moment you press the power button to the moment your shell is ready for input</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hrishi.dev/assets/os-boot-process-stages.png" /><media:content medium="image" url="https://hrishi.dev/assets/os-boot-process-stages.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">Building Stock Exchanges in the Cloud: part 1</title><link href="https://hrishi.dev/cloud/aws/trading/low-latency/distributed%20systems/2025/11/09/building-stock-exchange-in-the-cloud.html" rel="alternate" type="text/html" title="Building Stock Exchanges in the Cloud: part 1" /><published>2025-11-09T04:00:00-06:00</published><updated>2025-11-09T04:00:00-06:00</updated><id>https://hrishi.dev/cloud/aws/trading/low-latency/distributed%20systems/2025/11/09/building-stock-exchange-in-the-cloud</id><content type="html" xml:base="https://hrishi.dev/cloud/aws/trading/low-latency/distributed%20systems/2025/11/09/building-stock-exchange-in-the-cloud.html">&lt;p&gt;&lt;em&gt;Exploring how modern exchanges achieve low latency in the cloud, drawing insights from various exchanges design referrences&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Building a stock exchange is one of the most demanding distributed systems challenges in software engineering. Unlike traditional web applications where sub-second response times are acceptable, exchanges must process orders in microseconds, handle millions of messages per second, and maintain perfect fairness and reliability—all while operating during the designated market hours or 24/7.&lt;/p&gt;

&lt;p&gt;This post explores the key principles, technologies, and architectural patterns that make modern cloud-based exchanges possible by collecting all referenes that Ive got to learn or experience.&lt;/p&gt;

&lt;h2 id=&quot;key-characteristics&quot;&gt;Key characteristics&lt;/h2&gt;

&lt;h3 id=&quot;latency&quot;&gt;Latency&lt;/h3&gt;

&lt;p&gt;In financial markets, latency directly translates to money and risk for the trade. A millisecond advantage can mean the difference between executing a profitable trade and watching it slip away. The latency sensitivity spectrum is stark:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Less latency sensitive&lt;/strong&gt;: Reporting, risk calculations (time equivalent: light traveling from the sun)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Ultra-low latency&lt;/strong&gt;: High-frequency trading (time equivalent: light traveling ~150 miles)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;faireness&quot;&gt;Faireness&lt;/h3&gt;
&lt;p&gt;For exchanges, fairness is paramount. If one trader has a 1ms latency advantage over another, they gain an unfair advantage in market making and order execution. This creates a fundamental requirement: exchanges must maintain consistent, minimal latency for all participants while ensuring no single participant can gain an unfair advantage through connectivity differences.&lt;/p&gt;

&lt;h3 id=&quot;throughput-and-scale&quot;&gt;Throughput and Scale&lt;/h3&gt;
&lt;p&gt;Exchanges must handle both steady-state and spiking workloads. Coinbase’s system is designed to handle 100k messages per second. However with the ability to spike during volatile market conditions or peak time there could be 500K to millons messages per second in the system.&lt;/p&gt;

&lt;h3 id=&quot;reliability&quot;&gt;Reliability&lt;/h3&gt;
&lt;p&gt;24/7 operation at p99 performance is non-negotiable. Any downtime or performance degradation can result in:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Lost trading opportunities&lt;/li&gt;
  &lt;li&gt;Market participant dissatisfaction&lt;/li&gt;
  &lt;li&gt;Regulatory compliance issues&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;high-level-system-design&quot;&gt;High level system design&lt;/h2&gt;

&lt;p&gt;&lt;img src=&quot;/assets/exchange-system-design.png&quot; alt=&quot;Exchange System Design&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;market-participants&quot;&gt;Market Participants&lt;/h3&gt;
&lt;p&gt;External clients interacting with the exchange:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Brokers&lt;/li&gt;
  &lt;li&gt;Market Makers / Hedge Funds&lt;/li&gt;
  &lt;li&gt;Institutional Banks&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;order-gateway&quot;&gt;Order Gateway&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Entry point for all incoming client orders.&lt;/li&gt;
  &lt;li&gt;Performs validation, risk checks, and routing.&lt;/li&gt;
  &lt;li&gt;Sends validated orders into the order management system.&lt;/li&gt;
  &lt;li&gt;Handles highly latency-sensitive traffic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;order-management-system-oms&quot;&gt;Order Management System (OMS)&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Manages lifecycle of orders inside the exchange.&lt;/li&gt;
  &lt;li&gt;Handles order creation, modification, cancellation, and state tracking.&lt;/li&gt;
  &lt;li&gt;Connects client order flow to the matching engine.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;matching-engine&quot;&gt;Matching Engine&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Central component maintaining the limit order book.&lt;/li&gt;
  &lt;li&gt;Matches buy and sell orders using price-time priority.&lt;/li&gt;
  &lt;li&gt;Handles the most latency-critical processing in the exchange.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;market-data-gateway&quot;&gt;Market Data Gateway&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Receives market data updates from internal systems.&lt;/li&gt;
  &lt;li&gt;Distributes order books, charts, and instrument data to external participants.&lt;/li&gt;
  &lt;li&gt;Handles fairness-critical data dissemination for all consumers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;market-data-md-publisher&quot;&gt;Market Data (MD) Publisher&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Produces real-time market data from the matching engine.&lt;/li&gt;
  &lt;li&gt;Publishes depth, trades, and order book updates.&lt;/li&gt;
  &lt;li&gt;Sends this data to the market data gateway.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;reporting-service&quot;&gt;Reporting Service&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Processes executed trades and order events.&lt;/li&gt;
  &lt;li&gt;Generates reports for compliance, analytics, and post-trade functions.&lt;/li&gt;
  &lt;li&gt;Writes data into a persistent database.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;database&quot;&gt;Database&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;Stores order executions, trade history, and audit logs.&lt;/li&gt;
  &lt;li&gt;Designed for durability and reliability rather than low latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;matching-engine-and-order-management-system-design&quot;&gt;Matching Engine and Order Management System Design&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/exchange-system-design-plaement-group.png&quot; alt=&quot;Exchange System Design with Placement Group&quot; /&gt;&lt;/p&gt;

&lt;p&gt;The matching engine and order management systems are implemented as distributed systems. They are stateful, replicating their data through logs, so the architecture must favor low network and disk I/O latency. We design these components around a single-threaded application loop to avoid locking contention; queuing enforces ordering for consistent, deterministic behavior.&lt;/p&gt;

&lt;h2 id=&quot;aws-services-for-ultra-low-latency-trading&quot;&gt;AWS Services for Ultra-Low-Latency Trading&lt;/h2&gt;

&lt;h3 id=&quot;ec2-instance-options-for-high-performance&quot;&gt;EC2 Instance options for High Performance&lt;/h3&gt;

&lt;p&gt;AWS offers several instance families optimized for single-threaded CPU performance, which is critical for deterministic, low-latency trading systems. Because Raft log replication demands fast, predictable disk I/O, favor instances with high-performance local NVMe storage.&lt;/p&gt;

&lt;h4 id=&quot;instance-family&quot;&gt;Instance Family&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;z1d instances&lt;/strong&gt;: High-frequency Intel processors with sustained all-core turbo frequency [5]&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;X2iezn instances&lt;/strong&gt;: Intel Xeon processors optimized for memory-intensive workloads&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;r7iz, c6in, m5zn instances&lt;/strong&gt;: Balanced compute and memory options&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;The choice depends on your specific workload characteristics. CPU-bound matching engines benefit from the z1d family. z1d instances are VM instances that expose only a subset of physical CPU cores, all from a single NUMA node, resulting in reduced CPU cache penalties. Single-threaded systems using core pinning and C-State tuning achieve stable tail latencies. These instances offer a sustained all-core frequency of up to 4.0 GHz, providing consistently fast performance at the 99th percentile. NVMe’s instance store given better disk IO performance to sustain the RAFT cluster performance.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;blockquote&gt;
  &lt;p&gt;In contrast, order management systems may require the higher memory bandwidth provided by the X2iezn family.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;instance-tenancy-and-provisioning&quot;&gt;Instance Tenancy and Provisioning&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Dedicated&lt;/strong&gt;: Dedicated Instances are Amazon EC2 instances that run in a VPC on hardware that’s dedicated to a single customer. Your Dedicated instances are physically isolated at the host hardware level from instances that belong to other AWS accounts.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Dedicated host&lt;/strong&gt;: Dedicated Hosts to launch Amazon EC2 instances on physical servers that are dedicated for your use. Dedicated Hosts give you additional visibility and control over how instances are placed on a physical server, and you can reliably use the same physical server over time.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;On-demand&lt;/strong&gt;: Standard virtual machines that share the underlying physical host with other tenants. You pay by the hour/second and capacity is allocated dynamically.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;In AWS’s Dedicated tenancy model, virtualization overhead and cross-tenant interference are minimized, so low-latency system optimizations—such as tuning network stack, pinning processes/threads/interrupts to CPU cores, or using busy-spin threads—behave predictably with expected results, unlike in some other clouds. Dedicated VMs in this model behave much closer to bare-metal machines. This results in improved performance consistency (especially for tail latencies), even though the VM is still technically virtualized. However, the primary tradeoff is an added cost premium of about 10–15%. Additionally,&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;cluster-placement-groups&quot;&gt;Cluster Placement Groups&lt;/h3&gt;

&lt;p&gt;Perhaps the most critical AWS feature for exchange architectures is &lt;strong&gt;EC2 Cluster Placement Groups&lt;/strong&gt;. This ensures instances are placed in the same availability zone with the lowest possible network latency between them.&lt;/p&gt;

&lt;p&gt;With EC2 networking is capped at 5Gbps, no matter how much total bandwidth the instance has. However, when your instances are in a cluster placement group, this per-flow limit increases to 10Gbps. With the parition plaement
groups instances dont share the pack/host unit with instances in another
partition for the resiliency purposes.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;By using cluster placement groups, Coinbase was able to achieve amazing speed: their internal round-trip latency was under 1ms, and trading operations happened in single digit microseconds[1]. Similar way, testing with Aeron clusters on AWS also showed strong results—handling 100,000 messages per second with each message delivered in just 43 to 66 microseconds. Without cluster placement groups, your servers could be spread out, causing random slowdowns and making fast, fair trading impossible[2].&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;network-architecture&quot;&gt;Network Architecture&lt;/h3&gt;

&lt;p&gt;For ultra-low latency, you need to minimize network hops:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;VPC Peering&lt;/strong&gt; vs &lt;strong&gt;Transit Gateway&lt;/strong&gt;: VPC peering provides lower latency (fewer hops) but less flexibility. For hot-path components, VPC peering is often preferred.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;AWS PrivateLink&lt;/strong&gt;: Some of the benefits of private connectivity without having to cross connect directly in the datacenter or leave the traffic over the internet.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Single Availability Zone&lt;/strong&gt;: Keeping all hot-path components in a single AZ minimizes cross-AZ latency.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Elastic Network Adapter (ENA) Express&lt;/strong&gt;: Enhanced networking with lower latency and higher throughput. Amazon linux comes with the right ENA driver, however other distros need to make sure about ENA drivers are installed[3].&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Elastic Fabric Adapter(EFA)&lt;/strong&gt;: EFA could be used to reduce the latency by few more microseconds with 
the kernel bypass tequniques.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: Kernel bypass approaches such as DPDK introduce trade-offs—they make debugging network operations difficult because traditional tools or instrumentation/metrics won’t work in such setups.&lt;/p&gt;

&lt;h3 id=&quot;compute-cluster-for-applications&quot;&gt;Compute Cluster for Applications&lt;/h3&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;EC2 with an autoscaler&lt;/strong&gt;: Offers the most performance-efficient option for latency-critical workloads.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Containers (ECS/Kubernetes)&lt;/strong&gt;: Containers provide a ubiquitous packaging format, and tools like ECS or Kubernetes supply turnkey ecosystems to deploy, scale, and operate applications.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
  &lt;p&gt;Given these trade-offs, running critical workloads such as the matching engine or order management system on EC2 remains the pragmatic choice. Could ECS or Kubernetes work? Possibly, but performance constraints make them a secondary option today. That said, the Kubernetes ecosystem continues to address low-latency challenges. For example, recent Cilium (Kubernetes networking) benchmarks with the Netkit data plane show latency approaching non-container level as running the application on the host, and in some cases outperforming traditional east-west networking flows(such as for the RAFT cluster use case)[10][11].&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;service-over-the-raft---latency-and-resiliency&quot;&gt;Service over the RAFT - Latency and Resiliency&lt;/h2&gt;

&lt;p&gt;This part of design dont highlight any important services from AWS, however some aspects
this service is tested well the AWS envrionment and some statistics are available[6]&lt;/p&gt;

&lt;p&gt;Lets understand the in the microdetails, how the Order Management System(OMS) and Matching Engine
designed to not only achive the ultra-low latency but alos realibility with the auto failover
in few microseconds wihtout any data loss.&lt;/p&gt;

&lt;h3 id=&quot;sysem-design-for-the-raft-cluster&quot;&gt;Sysem design for the RAFT cluster&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;/assets/exchange-system-design-raft.png&quot; alt=&quot;Exchange System Design RAFT cluster&quot; /&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;Built the hot path around a single-threaded, event-loop style core so every order executes deterministically without locks or context switches; sequencing is preserved by feeding work through in-order queues such that the leader’s log index is the single source of truth.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Co-located matching, OMS, and gateways inside an EC2 cluster placement group to shorten wire distance; pair this with dedicated tenancy, core pinning, and CPU C-state tuning so microbursts don’t suffer noisy-neighbor jitter.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Memory-map (mmap) the limit-order-book state, replay logs, and hot data blobs into userspace to bypass kernel copies; couple it with page pre-touching and huge pages so the matching thread always hits RAM and never stalls on page faults.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Replication pipeline uses Raft: leader appends each order to its WAL/log position, broadcasts append entries, waits for majority acks, then releases the decision to the matching loop; followers apply entries strictly in log order to maintain identical sequencing.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;Leader election still follows Raft timing—followers monitor the leader heartbeat, start an election on timeout, gather a majority (using the highest replicated index as voting currency), and the new leader resumes sequencing from the last committed index so no order replays out of order.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So an Aeron[2] like system plays a crucial role, not only providing IPC event bus backed by memory for OMS and Matching engine, but also offering highly available leader election primitives when clustered over Raft.&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Aeron provides IPC via shared memory, event sequencing, and replication of messages across other hosts.&lt;/li&gt;
  &lt;li&gt;Aeron also supplies leader election primitives to achieve failover within a few microseconds without data loss[7].&lt;/li&gt;
  &lt;li&gt;In the AWS context, Aeron Premium can deliver kernel bypass capabilities to achieve ultra-low latency[6] using the EFA or even over the RDMA channel.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;coinbase-international-exchange-architecture-reference&quot;&gt;Coinbase International Exchange Architecture (Reference)&lt;/h3&gt;

&lt;p&gt;Coinbase’s architecture provides a blueprint for building cloud-native exchanges:&lt;/p&gt;

&lt;h4 id=&quot;core-components&quot;&gt;Core Components&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Trading Systems&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Ultra-low latency, single-threaded, deterministic&lt;/li&gt;
  &lt;li&gt;Clustered for resilience (Raft consensus)&lt;/li&gt;
  &lt;li&gt;All nodes run the same code with the same input&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Gateways&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Client connectivity via FIX, REST, and WebSockets&lt;/li&gt;
  &lt;li&gt;Handle protocol translation and validation&lt;/li&gt;
  &lt;li&gt;Can be scaled independently&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Aeron Messaging / Simple Binary Encoding&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;UDP-based messaging protocol&lt;/li&gt;
  &lt;li&gt;Fast, reliable message delivery&lt;/li&gt;
  &lt;li&gt;Built-in replication and archiving capabilities&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;raft-cluster-for-deterministic-consensus&quot;&gt;Raft Cluster for Deterministic Consensus&lt;/h4&gt;

&lt;p&gt;The matching engine uses a Raft consensus algorithm to ensure all nodes process orders in the same order:&lt;/p&gt;

&lt;p&gt;&lt;img class=&quot;mermaid&quot; src=&quot;https://mermaid.ink/svg/eyJjb2RlIjoiZ3JhcGggVERcbkFbTGVhZGVyIE5vZGU8YnIvPkFjdGl2ZV0gLS0-fFByb3Bvc2VzIG9yZGVyc3wgQltGb2xsb3dlciAxPGJyLz5SZXBsaWNhXVxuQSAtLT58UHJvcG9zZXMgb3JkZXJzfCBDW0ZvbGxvd2VyIDI8YnIvPlJlcGxpY2FdXG5CIC0tPnxBY2tub3dsZWRnZXN8IEFcbkMgLS0-fEFja25vd2xlZGdlc3wgQVxuJSUtXG5BIC0tPnxSZWNlaXZlcyBvcmRlcnN8IEFcbkEgLS0-fENvbW1pdHMgYWZ0ZXIgbWFqb3JpdHl8IEFcbiUlLVxuc3R5bGUgQSBmaWxsOiNjOGU2YzlcbnN0eWxlIEIgZmlsbDojZmZmM2UwXG5zdHlsZSBDIGZpbGw6I2ZmZjNlMCIsIm1lcm1haWQiOm51bGx9&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Key properties:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;One leader, multiple followers&lt;/strong&gt;: Only the leader processes orders&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Majority consensus&lt;/strong&gt;: Requires majority before processing&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Deterministic execution&lt;/strong&gt;: All nodes run same code with same input&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Single-threaded&lt;/strong&gt;: Avoids context switching overhead&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Built with Aeron&lt;/strong&gt;: Open-source library for reliable messaging&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;round-trip-latency-breakdown&quot;&gt;Round-Trip Latency Breakdown&lt;/h4&gt;

&lt;p&gt;Coinbase’s latency performance demonstrates that cloud-based exchanges can achieve sub-millisecond latencies:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Round trip time (networking + processing)&lt;/strong&gt;: Outliers &amp;lt; 1ms&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Networking time&lt;/strong&gt;: ~80% of total (10 hops total)&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Processing time&lt;/strong&gt;: ~20% of total (single-digit microseconds for OMS + Trading System)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fact that networking dominates (80%) shows the importance of cluster placement groups and minimizing network hops. The processing time being in single-digit microseconds demonstrates the efficiency of single-threaded, deterministic execution.&lt;/p&gt;

&lt;h3 id=&quot;future-improvements&quot;&gt;Future Improvements&lt;/h3&gt;

&lt;p&gt;Coinbase identified several areas for future optimization:&lt;/p&gt;

&lt;h4 id=&quot;improving-client-connectivity-latency&quot;&gt;Improving Client Connectivity Latency&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;Current State&lt;/strong&gt;: Connectivity over Internet introduces variable latency&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Future Options&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;AWS PrivateLink&lt;/strong&gt;: Private connectivity for clients. Note: this option is available for derivative market exchange only.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Shared cluster placement groups + VPC peering&lt;/strong&gt;: Co-locate clients with exchange infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;networking-and-latency-enhancements&quot;&gt;Networking and Latency Enhancements&lt;/h4&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Kernel bypass&lt;/strong&gt;: Reduce kernel overhead for network operations&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Elastic Network Adapter (ENA) Express&lt;/strong&gt;: Enhanced networking features&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Elastic Fabric Adapter (EFA)&lt;/strong&gt;: For HPC-style workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Building a stock exchange in the cloud is not only possible but can provide own trade-off over traditional on-premises architectures:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Lower operational overhead&lt;/strong&gt;: Managed services for non-critical paths&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Better agility&lt;/strong&gt;: Fast environment provisioning and safe deployment strategies&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Improved reliability&lt;/strong&gt;: Cloud-native patterns for resilience and disaster recovery&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cost efficiency&lt;/strong&gt;: Pay only for what you use, scale on demand&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Go to the market and CapEx&lt;/strong&gt;: Agility and self service provisioning at certain scale is the big win when its comes for goto the market. Business can avoid upfront spending when daily operational requiremetns and infrastructure footprintare still uncertain.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The key insight from Coinbase’s experience is that &lt;strong&gt;ultra-low latency and cloud-native design are not mutually exclusive&lt;/strong&gt;. By carefully selecting instance types, using cluster placement groups, and optimizing network architecture, exchanges can achieve sub-millisecond latencies while maintaining the operational benefits of cloud infrastructure.&lt;/p&gt;

&lt;p&gt;The next part of series would focus on Market data and reporting sections.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;references&quot;&gt;References&lt;/h2&gt;

&lt;ul&gt;
  &lt;li&gt;[1]&lt;a href=&quot;https://d1.awsstatic.com/events/Summits/reinvent2023/FSI309_Coinbase-Building-an-ultra-low-latency-crypto-exchange-on-AWS.pdf&quot;&gt;Coinbase: Building an ultra-low-latency crypto exchange on AWS&lt;/a&gt; - AWS re:Invent 2023 Session FSI309&lt;/li&gt;
  &lt;li&gt;[2]&lt;a href=&quot;https://github.com/real-logic/aeron&quot;&gt;Aeron Messaging&lt;/a&gt; - High-performance messaging library&lt;/li&gt;
  &lt;li&gt;[3]&lt;a href=&quot;https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking-ena.html&quot;&gt;AWS ENA with EC2&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[4]&lt;a href=&quot;https://raft.github.io/&quot;&gt;Raft Consensus Algorithm&lt;/a&gt; - Consensus algorithm for distributed systems&lt;/li&gt;
  &lt;li&gt;[5]&lt;a href=&quot;https://aws.amazon.com/ec2/instance-types/z1d/&quot;&gt;z1d Instances&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[6]&lt;a href=&quot;https://aws.amazon.com/blogs/industries/aeron-performance-enables-capital-markets-to-move-to-the-cloud-on-aws/&quot;&gt;Aeron performance enables capital markets to move to the cloud on AWS&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[7]&lt;a href=&quot;https://theaeronfiles.com/aeron-cluster/consensus-module/&quot;&gt;Aeron Consensus Module&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[8]&lt;a href=&quot;&quot;&gt;System deign interview - Alex Xu and Sahn Lam&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[9]&lt;a href=&quot;https://medium.com/prooftrading&quot;&gt;ProofTrading - Prerak Sanghvi&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[10]&lt;a href=&quot;https://isovalent.com/blog/post/cilium-netkit-a-new-container-networking-paradigm-for-the-ai-era/&quot;&gt;Ciliums netkit&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;[11]&lt;a href=&quot;https://hrishi.dev/linux/kubernetes/networking/netkit/benchmarking/2026/08/23/cilium-netkit-host-vs-container-benchmarks.html&quot;&gt;Linux Bare Host vs. Container with Cilium netkit: What Actually Wins&lt;/a&gt; - hands-on host-vs-container benchmarks, including the etcd Raft quorum-write case where the netkit-backed container outperforms the bare host&lt;/li&gt;
&lt;/ul&gt;</content><author><name></name></author><category term="Cloud" /><category term="AWS" /><category term="Trading" /><category term="Low-Latency" /><category term="Distributed Systems" /><summary type="html">Exploring how modern exchanges achieve low latency in the cloud, drawing insights from various exchanges design referrences</summary></entry><entry><title type="html">Linux Security and Isolation APIs: course experience</title><link href="https://hrishi.dev/linux/security/kernel/isolation/apis/2025/11/02/linux-security-isolation-apis-course.html" rel="alternate" type="text/html" title="Linux Security and Isolation APIs: course experience" /><published>2025-11-02T04:00:00-06:00</published><updated>2025-11-02T04:00:00-06:00</updated><id>https://hrishi.dev/linux/security/kernel/isolation/apis/2025/11/02/linux-security-isolation-apis-course</id><content type="html" xml:base="https://hrishi.dev/linux/security/kernel/isolation/apis/2025/11/02/linux-security-isolation-apis-course.html">&lt;p&gt;&lt;em&gt;A reflection on the &lt;a href=&quot;https://man7.org/training/secisol/index.html&quot;&gt;man7.org Linux Security and Isolation APIs course&lt;/a&gt; and the fascinating journey from time-sharing systems to modern containerization, how sasauge are made&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Recently, I had the opportunity to attend the &lt;strong&gt;Linux Security and Isolation APIs&lt;/strong&gt; course offered by &lt;a href=&quot;https://man7.org/training/secisol/index.html&quot;&gt;man7.org Training&lt;/a&gt;, delivered by Michael Kerrisk, author of &lt;em&gt;The Linux Programming Interface&lt;/em&gt;. This four-day deep dive into the low-level Linux features that power containers, virtualization, and sandboxing was an eye-opening experience that connected the dots between historical computing concepts and modern cloud infrastructure.&lt;/p&gt;

&lt;p&gt;What became particularly fascinating as we explored namespaces, cgroups, seccomp, and SELinux is how all these capabilities trace their roots back to fundamental concepts in time-sharing systems and isolation in the context of security. Modern containerization and virtualization are, in many ways, side effects or by-product of solving these deeper security and resource isolation challenges.&lt;/p&gt;

&lt;h2 id=&quot;the-historical-context-from-time-sharing-to-containers&quot;&gt;The Historical Context: From Time-Sharing to Containers&lt;/h2&gt;

&lt;p&gt;Before diving into the technical details, it’s worth understanding the evolutionary path that led us here. The course beautifully connected the dots between:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Time-sharing systems&lt;/strong&gt; - The need to run multiple users’ processes on the same hardware securely&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Security isolation&lt;/strong&gt; - Ensuring one user’s processes couldn’t interfere with another’s&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Resource control&lt;/strong&gt; - Fairly allocating CPU, memory, and I/O across users&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Modern containers&lt;/strong&gt; - Today’s application of these same principles&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This historical perspective makes it clear that containerization didn’t emerge in a vacuum. It’s the natural evolution of isolation mechanisms that UNIX and Linux have been refining for decades.&lt;/p&gt;

&lt;h2 id=&quot;the-evolution-of-namespaces-starting-with-mount&quot;&gt;The Evolution of Namespaces: Starting with Mount&lt;/h2&gt;

&lt;p&gt;One of the most interesting historical tidbits from the course was learning that the &lt;strong&gt;first namespace, the mount namespace, came into existence in the early 2000s&lt;/strong&gt;. This wasn’t created for containers (which didn’t exist yet), but to solve a very specific problem: &lt;strong&gt;on systems like UNIX/Linux, when users log in, PAM modules couldn’t mount certain drives and make them available to specific sets of users.&lt;/strong&gt;&lt;/p&gt;

&lt;h2 id=&quot;deep-dive-system-calls-and-practical-implementation&quot;&gt;Deep Dive: System Calls and Practical Implementation&lt;/h2&gt;

&lt;p&gt;The course provided extensive hands-on experience with the fundamental system calls that make namespaces work. Getting deep into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clone()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unshare()&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setns()&lt;/code&gt; (nsenter key syscall) gave me a much better understanding of how container runtimes actually operate. The differences between &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;clone()&lt;/code&gt; (creating a new process in new namespaces) and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;unshare()&lt;/code&gt; (moving the current process into new namespaces) became clear through practical exercises. Understanding &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setns()&lt;/code&gt; and how it allows processes to join existing namespaces was particularly enlightening: this is exactly what tools like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;docker exec&lt;/code&gt; do under the hood.&lt;/p&gt;

&lt;p&gt;The hands-on experience writing seccomp BPF programs from scratch was challenging but incredibly valuable. While higher-level libraries like libseccomp make this easier, understanding the raw BPF syntax and how filters are constructed gave me deep insight into how system call filtering works at the kernel level. The course also covered working directly with the cgroupv2 API, which has a much cleaner unified hierarchy compared to v1.&lt;/p&gt;

&lt;p&gt;Perhaps my biggest personal realization from the course was understanding a correlation I had previously missed: &lt;strong&gt;how Linux capabilities govern and interact with namespace and cgroup operations&lt;/strong&gt;. This three-way relationship (capabilities acting as gatekeepers for what namespace and cgroup operations are permitted) is fundamental to container security but often overlooked. Understanding that user namespaces provide “fake capabilities” that allow namespace creation within that namespace, while still being restricted by the user namespace mapping, was a crucial insight. This correlation explained so much about why certain container configurations work or fail, and why rootless containers have different capabilities and limitations compared to rootful ones.&lt;/p&gt;

&lt;h2 id=&quot;capabilities-from-simple-security-model-to-sophisticated-governance&quot;&gt;Capabilities: From Simple Security Model to Sophisticated Governance&lt;/h2&gt;

&lt;p&gt;The course traced the evolution of Linux security from the traditional set-UID and set-GID model, which was a simplified security approach where processes either had full root privileges or didn’t, to the modern capabilities system. Fundamentally, set-UID and set-GID programs simplified the security model, but they were all-or-nothing: a process either had root privileges or it didn’t. The capabilities system broke this down into fine-grained privileges, allowing processes to have only the specific permissions they need.&lt;/p&gt;

&lt;p&gt;One of the most enlightening aspects was learning how &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execve()&lt;/code&gt; transforms process capabilities according to Linux’s capability transformation rules. When a process executes a new program via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;execve()&lt;/code&gt;, the kernel applies a complex set of rules based on the process’s current capabilities, the file’s capabilities (if any), and the bounding set. Understanding these transformation rules (how permitted, effective, and inheritable capability sets interact during exec) was crucial for understanding why some privilege escalation attempts work while others don’t.&lt;/p&gt;

&lt;p&gt;The course also revealed how cgroup v2 integrates with capabilities at a deeper level than I had realized. Cgroup v2 doesn’t just limit resources; it also derives and can restrict capabilities for processes within a cgroup. This integration between cgroups and capabilities adds another layer of capability governance beyond what individual processes might have.&lt;/p&gt;

&lt;p&gt;Playing around with system calls like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getresuid()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getresgid()&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;setcap()&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;getcap()&lt;/code&gt; during the practical exercises was not only enlightening but genuinely fun. These calls provide insight into the real vs effective user IDs, how capabilities are set and retrieved, and how they interact with the process’s UID/GID context. Understanding the fundamental evolution of the Linux system to govern process capabilities in conjunction with namespaces and cgroups revealed just how sophisticated and well-integrated these mechanisms have become. It’s a far cry from the simple binary root/non-root model, and seeing how all these pieces fit together was one of the most satisfying parts of the course.&lt;/p&gt;

&lt;h2 id=&quot;personal-reflection&quot;&gt;Personal Reflection&lt;/h2&gt;

&lt;p&gt;Though my journey started with tools such as Docker, Podman, etc., for isolating processes, it’s been an enlightening yet humbling experience to see how the evolution of isolation and security needs of multi-user time-sharing capabilities in Linux and Unix systems over more than a decade made virtualization a by-product of a well designed system.&lt;/p&gt;

&lt;p&gt;At last, personally I really enjoyed getting hands-on with the cgroup v2 API, exploring the various controllers including CPU, CPUSETS, freezer, and memory manager. Getting quite an in-depth understanding of the freezer controller for migrating workloads was particularly fascinating: understanding how processes can be frozen, migrated between systems, and then resumed opens up powerful possibilities for live migration and workload mobility that are fundamental to modern container orchestration systems.&lt;/p&gt;

&lt;h2 id=&quot;course-information&quot;&gt;Course Information&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Course&lt;/strong&gt;: Linux Security and Isolation APIs (M7D-SECISOL02)&lt;br /&gt;
&lt;strong&gt;Provider&lt;/strong&gt;: &lt;a href=&quot;https://man7.org/training/index.html&quot;&gt;man7.org Training&lt;/a&gt;&lt;br /&gt;
&lt;strong&gt;Trainer&lt;/strong&gt;: Michael Kerrisk (author of &lt;em&gt;The Linux Programming Interface&lt;/em&gt;)&lt;br /&gt;
&lt;strong&gt;Duration&lt;/strong&gt;: 4 days&lt;br /&gt;
&lt;strong&gt;Format&lt;/strong&gt;: Live online with extensive lab sessions&lt;br /&gt;
&lt;strong&gt;Materials&lt;/strong&gt;: 500+ page course book&lt;/p&gt;

&lt;p&gt;For upcoming course dates and registration, visit the &lt;a href=&quot;https://man7.org/training/index.html&quot;&gt;man7.org training page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/linix_security_isolation_api_cert.png&quot; alt=&quot;Linux Security and Isolation APIs Course Certificate&quot; /&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;&lt;em&gt;This post reflects my experience and learnings from the course. For the most current course information, schedule, and pricing, please refer to the official &lt;a href=&quot;https://man7.org/training/secisol/index.html&quot;&gt;man7.org training website&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><author><name></name></author><category term="Linux" /><category term="Security" /><category term="Kernel" /><category term="Isolation" /><category term="APIs" /><summary type="html">A reflection on the man7.org Linux Security and Isolation APIs course and the fascinating journey from time-sharing systems to modern containerization, how sasauge are made</summary></entry><entry><title type="html">GPU from Silicon to Container, Part 3: CDI, Dynamic Resource Allocation &amp;amp; Operating GPUs in Kubernetes</title><link href="https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html" rel="alternate" type="text/html" title="GPU from Silicon to Container, Part 3: CDI, Dynamic Resource Allocation &amp;amp; Operating GPUs in Kubernetes" /><published>2025-10-25T01:10:10-05:00</published><updated>2025-10-25T01:10:10-05:00</updated><id>https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3</id><content type="html" xml:base="https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html">&lt;p&gt;&lt;em&gt;Part 3 of a 3-part series on how Kubernetes makes GPUs accessible to containers, this final part covers the
Container Device Interface, Dynamic Resource Allocation, and running GPUs reliably in production.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;This is the final part of a 3-part series. &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html&quot;&gt;Part 1&lt;/a&gt;&lt;/strong&gt; covered GPU provisioning from silicon to a
scheduled pod; &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html&quot;&gt;Part 2&lt;/a&gt;&lt;/strong&gt; covered splitting a physical GPU across workloads with time-slicing, MPS,
MIG, HAMi, and vGPU. This part covers the standard that those sharing mechanisms and Kubernetes GPU scheduling
itself are moving to, and what it takes to operate it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/gpu-part3-cdi-dra-pipeline.png&quot; alt=&quot;A five-stage diagram of CDI and Dynamic Resource Allocation: Driver, CDI Spec, ResourceSlice, ResourceClaim, and Pod Running&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Next Container Technologies&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#the-container-device-interface-cdi-revolution&quot;&gt;The Container Device Interface (CDI) Revolution&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#dynamic-resource-allocation-dra-next-generation-gpu-scheduling&quot;&gt;Dynamic Resource Allocation (DRA): Next-Generation GPU Scheduling&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Operations&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#gpu-operator-troubleshooting&quot;&gt;GPU Operator Troubleshooting&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#installing-the-nvidia-dra-driver-via-helm&quot;&gt;Installing the NVIDIA DRA Driver via Helm&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#gpu-fleet-reliability-metrics-and-slos&quot;&gt;GPU Fleet Reliability: Metrics and SLOs&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;the-container-device-interface-cdi-revolution&quot;&gt;The Container Device Interface (CDI) Revolution&lt;/h2&gt;

&lt;p&gt;In 2023-2024, the container ecosystem began transitioning to the &lt;strong&gt;&lt;a href=&quot;https://github.com/cncf-tags/container-device-interface&quot;&gt;Container Device Interface (CDI)&lt;/a&gt;&lt;/strong&gt;,
a standardized specification that fundamentally changes how devices are exposed to containers.&lt;/p&gt;

&lt;h3 id=&quot;the-problem-cdi-solves&quot;&gt;The Problem CDI Solves&lt;/h3&gt;

&lt;h4 id=&quot;the-old-way-vendor-specific-runtime-hooks&quot;&gt;The Old Way: Vendor Specific Runtime Hooks&lt;/h4&gt;

&lt;p&gt;Before CDI, each hardware vendor needed custom integration:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Container Runtime (containerd)
              ↓
nvidia-container-runtime (wrapper) ← NVIDIA-specific
              ↓
nvidia-container-runtime-hook ← Vendor logic
              ↓
nvidia-container-cli ← Device provisioning
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Problem:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;AMD needed rocm-container-runtime, Intel their own. Every implementation is reinventing the wheel to a certain extent,
thus solving the same problem differently&lt;/li&gt;
  &lt;li&gt;Since device plugin implementations wrap the container runtime, you end up building a plugin for every container runtime&lt;/li&gt;
  &lt;li&gt;All extended container resources need to be associated with a particular runtimeClass and need container runtime configuration changes,
which is error-prone&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;the-new-way-declarative-device-specifications&quot;&gt;The New Way: Declarative Device Specifications&lt;/h4&gt;

&lt;p&gt;Instead of runtime hooks, CDI uses a static YAML (or JSON) file on each node that declaratively describes everything a runtime needs to inject a device into a container: device nodes, library mounts, environment variables, and hooks. The NVIDIA Container Toolkit generates these files once via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-ctk cdi generate&lt;/code&gt;; the NVIDIA DRA driver generates them dynamically at allocation time or when standing up a device plugin.&lt;/p&gt;

&lt;p&gt;The container runtime reads this file at container creation time and applies the edits directly to the OCI spec, so &lt;strong&gt;no vendor wrapper is required&lt;/strong&gt; and the
pod doesn’t need to set the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName&lt;/code&gt;.&lt;/p&gt;

&lt;h3 id=&quot;cdi-architecture&quot;&gt;CDI Architecture&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Container Orchestrator (Kubernetes, Podman, Docker)
              ↓  Request: nvidia.com/gpu=0
Container Runtime (containerd, CRI-O, Docker)
  + Native CDI Support
              ↓  Reads CDI specs from disk
CDI Specification Files (YAML or JSON)
  /etc/cdi/*.yaml ← static, admin-gen
  /var/run/cdi/*.yaml ← dynamic, runtime
              ↓  Describes device configuration
Host System Resources
  - Device nodes (/dev/nvidia*)
  - Libraries (libcuda.so, etc.)
  - Utilities (nvidia-smi)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A CDI spec file (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/cdi/nvidia.yaml&lt;/code&gt;) is generated once by &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-ctk&lt;/code&gt; and contains three main sections:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# /etc/cdi/nvidia.yaml&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;cdiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0.6.0&quot;&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;# CDI specification version&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;          &lt;span class=&quot;c1&quot;&gt;# Fully-qualified device kind (vendor.com/type)&lt;/span&gt;
                              &lt;span class=&quot;c1&quot;&gt;# Prevents collisions: nvidia.com/gpu, amd.com/gpu, intel.com/gpu&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;containerEdits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;           &lt;span class=&quot;c1&quot;&gt;# Everything to inject for this device&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;deviceNodes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/nvidia0&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;c&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;major&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;195&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/nvidiactl&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;c&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;major&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;195&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;255&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/nvidia-uvm&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;c&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;major&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;511&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# dynamically assigned by kernel: verify with `ls -l /dev/nvidia-uvm`&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;mounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/lib/x86_64-linux-gnu/libcuda.so.535.104.05&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;containerPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/lib/x86_64-linux-gnu/libcuda.so.1&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ro&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nosuid&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nodev&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bind&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.535.104.05&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;containerPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.1&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ro&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nosuid&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nodev&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bind&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hostPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/bin/nvidia-smi&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;containerPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/bin/nvidia-smi&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;options&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;ro&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nosuid&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nodev&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bind&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA_VISIBLE_DEVICES=0&quot;&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA_DRIVER_CAPABILITIES=compute,utility&quot;&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;hooks&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;hookName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;createContainer&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/usr/bin/nvidia-ctk&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;hook&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;update-ldcache&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;

  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;1&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;containerEdits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;deviceNodes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/nvidia1&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;c&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;major&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;195&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/nvidiactl&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;c&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;major&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;195&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;255&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;path&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/dev/nvidia-uvm&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;type&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;c&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;major&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;511&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# dynamically assigned by kernel: verify with `ls -l /dev/nvidia-uvm`&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;0&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;mounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;# ... same libraries as device &quot;0&quot; ...&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA_VISIBLE_DEVICES=1&quot;&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA_DRIVER_CAPABILITIES=compute,utility&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;What real &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-ctk cdi generate&lt;/code&gt; output looks like&lt;/strong&gt;: the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mounts&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;hostPath&lt;/code&gt; pairing above is simplified for
readability. Current toolkit versions (e.g. driver 580.173.02) instead bind-mount the versioned host libraries once and
use a dedicated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;createContainer&lt;/code&gt; hook, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-cdi-hook create-symlinks&lt;/code&gt;, to build every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.so&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.so.1&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;.so.N&lt;/code&gt; symlink
a container needs, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcuda.so.1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libnvidia-ml.so.1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libnvcuvid.so.1&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libnvidia-opencl.so.1&lt;/code&gt;, and more, each
passed as a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--link target::linkname&lt;/code&gt; argument. This avoids baking exact driver-version filenames into every mount and
keeps symlink creation as an explicit, inspectable step rather than an implicit side effect of the bind mount.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3 id=&quot;cdi-vs-traditional-flow-comparison&quot;&gt;CDI vs Traditional Flow Comparison&lt;/h3&gt;

&lt;h4 id=&quot;traditional-nvidia-container-toolkit-flow&quot;&gt;Traditional NVIDIA Container Toolkit Flow&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;User: docker run --gpus all nvidia/cuda
              ↓
Docker daemon
              ↓  calls
nvidia-container-runtime
              ↓  wraps
runc
              ↓  prestart hook
nvidia-container-runtime-hook
              ↓  reads --gpus flag, NVIDIA_VISIBLE_DEVICES
nvidia-container-cli
              ↓  dynamically queries
nvidia-smi
              ↓  determines devices, libraries, mounts
Modify OCI spec on-the-fly (adds devices, mounts, env)
              ↓
runc creates container with GPU access
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Dynamic device discovery at container start&lt;/li&gt;
  &lt;li&gt;Runtime wrapper required&lt;/li&gt;
  &lt;li&gt;Vendor-specific magic in environment variables&lt;/li&gt;
  &lt;li&gt;Black box: hard to inspect what’s being configured&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;cdi-based-flow&quot;&gt;CDI-Based Flow&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[one-time setup, on node]
nvidia-ctk cdi generate --output=/etc/cdi/nvidia.yaml
              ↓
User: docker run --device nvidia.com/gpu=0 nvidia/cuda
              ↓
containerd (native CDI support) receives request
              ↓  parses CDI device name: nvidia.com/gpu=0
Look up device in /etc/cdi/nvidia.yaml
              ↓
Read containerEdits for device 0
              ↓  applies edits to OCI spec
  - Adds device nodes
  - Adds mounts
  - Sets environment variables
  - Registers hooks
              ↓
runc creates container with GPU access
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Characteristics:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Static device specification (generated once)&lt;/li&gt;
  &lt;li&gt;No runtime wrapper or runtime hooks are needed&lt;/li&gt;
  &lt;li&gt;Standard OCI runtime (runc) works unmodified&lt;/li&gt;
  &lt;li&gt;Transparent: inspect CDI specs to see exact configuration&lt;/li&gt;
  &lt;li&gt;Vendor provides only CDI spec generator&lt;/li&gt;
&lt;/ul&gt;

&lt;h3 id=&quot;cdi-in-kubernetes&quot;&gt;CDI in Kubernetes&lt;/h3&gt;

&lt;p&gt;The device plugin is now responsible for adhering to the CDI specification.&lt;/p&gt;

&lt;h4 id=&quot;pre-cdi-device-plugin&quot;&gt;Pre-CDI Device Plugin&lt;/h4&gt;
&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NvidiaDevicePlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Allocate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;req&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocateRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocateResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocateResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerRequests&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;// Device plugin must know HOW to provision GPU&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerAllocateResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;Envs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;map&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;s&quot;&gt;&quot;NVIDIA_VISIBLE_DEVICES&quot;&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;GPU-uuid-1234&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;Mounts&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Mount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;HostPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/usr/lib/x86_64-linux-gnu/libcuda.so&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;ContainerPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/usr/lib/x86_64-linux-gnu/libcuda.so&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;ReadOnly&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
                &lt;span class=&quot;c&quot;&gt;// ... many more mounts ...&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;Devices&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DeviceSpec&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;HostPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/dev/nvidia0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;ContainerPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/dev/nvidia0&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;Permissions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;rwm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;HostPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/dev/nvidiactl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;ContainerPath&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/dev/nvidiactl&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                    &lt;span class=&quot;n&quot;&gt;Permissions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;rwm&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
                &lt;span class=&quot;c&quot;&gt;// ... more devices ...&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerResponses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerResponses&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; 
            &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;post-cdi-device-plugin&quot;&gt;Post-CDI Device Plugin&lt;/h4&gt;
&lt;div class=&quot;language-go highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;m&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;NvidiaDevicePlugin&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;Allocate&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;req&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocateRequest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocateResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;error&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;AllocateResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;req&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerRequests&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;c&quot;&gt;// Device plugin just returns CDI device names!&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdiDevices&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;string&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deviceID&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;range&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;request&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;DevicesIDs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;cdiDevices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;cdiDevices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
                &lt;span class=&quot;n&quot;&gt;fmt&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;Sprintf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;nvidia.com/gpu=%s&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deviceID&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;),&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;response&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;:=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pluginapi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerAllocateResponse&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;CDIDevices&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cdiDevices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;c&quot;&gt;// That&apos;s it!&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerResponses&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;append&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ContainerResponses&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;response&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;responses&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;nil&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Key simplification:&lt;/strong&gt; The device plugin no longer needs vendor-specific knowledge about mounts, device nodes, or environment variables. 
It simply returns CDI device identifiers.&lt;/p&gt;

&lt;h4 id=&quot;container-runtime-integration&quot;&gt;Container Runtime Integration&lt;/h4&gt;

&lt;p&gt;When kubelet creates a container with CDI devices:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubelet receives CDI device names from device plugin:
  [&apos;nvidia.com/gpu=0&apos;, &apos;nvidia.com/gpu=1&apos;]
              ↓
kubelet adds CDI annotation to container config:
  annotations: { &apos;cdi.k8s.io/devices&apos;: &apos;nvidia.com/gpu=0,nvidia.com/gpu=1&apos; }
              ↓
kubelet → containerd CRI: CreateContainer
              ↓
containerd reads CDI annotation
              ↓
containerd loads CDI registry from
  /etc/cdi/*.yaml and /var/run/cdi/*.yaml
              ↓
For each CDI device:
  registry.GetDevice(&apos;nvidia.com/gpu=0&apos;)
  registry.GetDevice(&apos;nvidia.com/gpu=1&apos;)
              ↓
Applies container edits to OCI spec:
  - Merges all device nodes
  - Merges all mounts
  - Merges all environment variables
  - Collects all hooks
              ↓
Creates final OCI spec and calls runc
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;generating-cdi-specifications&quot;&gt;Generating CDI Specifications&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;NVIDIA Container Toolkit&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Basic generation&lt;/span&gt;
nvidia-ctk cdi generate &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/etc/cdi/nvidia.yaml

&lt;span class=&quot;c&quot;&gt;# With custom options&lt;/span&gt;
nvidia-ctk cdi generate &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/etc/cdi/nvidia.yaml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;yaml &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--device-name-strategy&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;index &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--driver-root&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/ &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;AMD ROCm&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;rocm-smi &lt;span class=&quot;nt&quot;&gt;--showdriverversion&lt;/span&gt;
rocm-cdi-generator &lt;span class=&quot;nt&quot;&gt;--output&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;/etc/cdi/amd.yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;dynamic-resource-allocation-dra-next-generation-gpu-scheduling&quot;&gt;Dynamic Resource Allocation (DRA): Next-Generation GPU Scheduling&lt;/h2&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html#kubernetes-gpu-scheduling&quot;&gt;Device Plugin&lt;/a&gt;&lt;/strong&gt; framework (Part 1) works well for simple whole-GPU assignment, but it has fundamental limitations
when workloads need fine-grained control: specific MIG profiles, multi-node NVLink topology, shared resources, or
per-claim lifecycle management. Kubernetes &lt;strong&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/scheduling-eviction/dynamic-resource-allocation/&quot;&gt;Dynamic Resource Allocation (DRA)&lt;/a&gt;&lt;/strong&gt;, in beta behind a feature gate
since 1.32 and stabilised in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resource.k8s.io/v1&lt;/code&gt; from Kubernetes 1.34, addresses these limitations by replacing
the opaque device plugin gRPC API with a structured, declarative model visible to the scheduler.&lt;/p&gt;

&lt;p&gt;The official DRA driver for NVIDIA GPUs is maintained at
&lt;strong&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu&quot;&gt;github.com/kubernetes-sigs/dra-driver-nvidia-gpu&lt;/a&gt;&lt;/strong&gt; under the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubernetes-sigs&lt;/code&gt; organisation.&lt;/p&gt;

&lt;h3 id=&quot;why-device-plugin-falls-short&quot;&gt;Why Device Plugin Falls Short&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Resource granularity.&lt;/strong&gt; The device plugin API only knows how to hand out whole devices: a GPU is a GPU. MIG
support, covered in &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html&quot;&gt;Part 2&lt;/a&gt;, isn’t
modeled by the API at all; it’s bolted on by advertising each MIG profile as its own resource name
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/mig-3g.20gb&lt;/code&gt;), which the scheduler treats no differently than a whole GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Topology awareness.&lt;/strong&gt; The scheduler filters and scores nodes purely on resource &lt;em&gt;counts&lt;/em&gt;; it has no concept of
which GPUs on a node share an NVLink bridge or sit on the same NUMA node. A pod can land with “2 GPUs available”
satisfied while those two GPUs are on opposite ends of the PCIe topology, silently tanking any workload that
assumed NVLink-speed interconnect between them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shared resources.&lt;/strong&gt; There’s no first-class notion of multiple pods sharing a device. Time-slicing (Part 2) only
works because the NVIDIA device plugin lies to the scheduler, advertising one physical GPU as several
schedulable “replicas,” not because the API itself understands sharing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lifecycle and scheduling.&lt;/strong&gt; A GPU is bound to a pod the moment the plugin’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; call returns, for the
life of that pod. There’s no way to pre-allocate a device ahead of a pod being scheduled, or to have two pods
coordinate over the same device via the API. So anything like that has to be built outside Kubernetes’s allocation
model entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Introspection.&lt;/strong&gt; Once &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; returns, the control plane has no idea what actually got handed out, which
physical GPU, which MIG slice, which UUID. That information lives only inside the device plugin’s own state,
invisible to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl&lt;/code&gt; or the scheduler, which makes debugging placement issues or building topology-aware
tooling on top of it much harder than it should be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error-prone recovery.&lt;/strong&gt; Hand-carving MIG instances outside the device plugin’s view
makes this worse: the &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html#who-actually-stands-mig-up&quot;&gt;MIG Manager&lt;/a&gt;
has no way to reconcile a layout it didn’t create, so its record of “current state” quietly drifts from what’s actually on the GPU.
Because the scheduler has no visibility into actual GPU state, a
pod can get stuck in a container creation error with no escape hatch to recover from such state without manual
intervention.&lt;/p&gt;

&lt;h3 id=&quot;dra-core-concepts&quot;&gt;DRA Core Concepts&lt;/h3&gt;

&lt;p&gt;DRA replaces the device plugin gRPC interface with three Kubernetes API objects.&lt;/p&gt;

&lt;h4 id=&quot;resourceslice-driver-advertises-devices&quot;&gt;ResourceSlice: Driver Advertises Devices&lt;/h4&gt;

&lt;p&gt;A DRA driver publishes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; objects (one per node) instead of calling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListAndWatch()&lt;/code&gt;.
Each slice describes the devices on that node with structured, queryable attributes:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ResourceSlice&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;node-gpu-01-nvidia-gpus&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu.nvidia.com&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;pool&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;node-gpu-01&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resourceSliceCount&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;nodeName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;node-gpu-01&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-0&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;basic&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;uuid&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;        &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;GPU-a4f8c2d1-e5f6-7a8b-9c0d-1e2f3a4b5c6d&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;model&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;       &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NVIDIA&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;H100&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;SXM5&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;80GB&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;     &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;3g.20gb&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# populated for MIG slices&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;parentUUID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;  &lt;span class=&quot;pi&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;GPU-a4f8c2d1...&quot;&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;}&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;# used for co-location constraints&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;80Gi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;deviceclass-cluster-policy-for-a-device-type&quot;&gt;DeviceClass: Cluster Policy for a Device Type&lt;/h4&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceClass&lt;/code&gt; is a cluster-scoped object set by administrators. The NVIDIA DRA driver registers two device classes out of the box:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu.nvidia.com&lt;/code&gt;: whole GPU devices&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig.nvidia.com&lt;/code&gt;: MIG (Multi-Instance GPU) slices&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;resourceclaim-user-requests-devices&quot;&gt;ResourceClaim: User Requests Devices&lt;/h4&gt;

&lt;p&gt;Instead of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resources.limits.nvidia.com/gpu: 1&lt;/code&gt;, a workload creates a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt;.
The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;exactly:&lt;/code&gt; stanza specifies how many devices are required and optional CEL selectors:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# Two pods, each getting their own single GPU&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ResourceClaimTemplate&lt;/span&gt;       &lt;span class=&quot;c1&quot;&gt;# per-pod claims for Jobs / Deployments&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-test1&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;single-gpu&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;deviceClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu.nvidia.com&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-test1&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pod1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;resourceClaims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resourceClaimTemplateName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;single-gpu&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ctr&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu:22.04&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bash&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-c&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nvidia-smi&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-L;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;trap&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;exit&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0&apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;TERM;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;9999&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;wait&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;claims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;tolerations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;operator&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Exists&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;effect&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NoSchedule&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Two containers in the &lt;strong&gt;same pod&lt;/strong&gt; can share one GPU claim by both referencing the same entry:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# One pod, two containers sharing one GPU&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ResourceClaimTemplate&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-test2&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;single-gpu&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;deviceClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu.nvidia.com&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-test2&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;shared-gpu-pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;resourceClaims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;shared-gpu&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resourceClaimTemplateName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;single-gpu&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ctr0&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu:22.04&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bash&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-c&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nvidia-smi&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-L;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;trap&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;exit&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0&apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;TERM;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;9999&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;wait&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;claims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;shared-gpu&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# both containers reference the same claim&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ctr1&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu:22.04&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;bash&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-c&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nvidia-smi&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;-L;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;trap&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;exit&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;0&apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;TERM;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;sleep&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;9999&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;wait&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;claims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;shared-gpu&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# both containers reference the same claim&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;tolerations&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;operator&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Exists&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;effect&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;NoSchedule&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;nvidia-dra-driver-dra-driver-nvidia-gpu&quot;&gt;NVIDIA DRA Driver (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dra-driver-nvidia-gpu&lt;/code&gt;)&lt;/h3&gt;

&lt;p&gt;The driver is maintained at &lt;strong&gt;&lt;a href=&quot;https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu&quot;&gt;github.com/kubernetes-sigs/dra-driver-nvidia-gpu&lt;/a&gt;&lt;/strong&gt; and ships two
kubelet plugins:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Plugin&lt;/th&gt;
      &lt;th&gt;Status&lt;/th&gt;
      &lt;th&gt;Purpose&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu-kubelet-plugin&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Experimental&lt;/td&gt;
      &lt;td&gt;Whole-GPU and MIG device allocation&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compute-domain-kubelet-plugin&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Officially supported&lt;/td&gt;
      &lt;td&gt;Multi-Node NVLink / ComputeDomain orchestration&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;h4 id=&quot;architecture&quot;&gt;Architecture&lt;/h4&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kube-apiserver
  ResourceSlice, ResourceClaim, DeviceClass
              ↓
kube-scheduler (DRA-aware)
  Reads ResourceSlice attributes via CEL
  Writes allocation into ResourceClaim.status
              ↓
kubelet
  Calls DRA plugin NodePrepareResources() gRPC
              ↓
dra-driver-nvidia-gpu: three independently deployed components:
  ├─ gpu-kubelet-plugin (experimental, DaemonSet on every GPU node)
  │    - NodePrepareResources / NodeUnprepareResources
  │    - Writes CDI spec for the allocated GPU/MIG slice
  ├─ compute-domain-kubelet-plugin (supported, DaemonSet on every GPU node)
  │    - Orchestrates IMEX daemons, domains, channels
  │    - Guarantees NVLink-reachability across nodes
  └─ controller (Deployment, control-plane)
       - Publishes ResourceSlice objects per node
       - Watches GPU inventory changes
              ↓  CDI device name
containerd (CDI-aware)
  Reads CDI spec, injects devices/libs/env
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;mig-allocation-via-dra&quot;&gt;MIG Allocation via DRA&lt;/h4&gt;

&lt;p&gt;DRA makes MIG allocation first-class. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig.nvidia.com&lt;/code&gt; DeviceClass exposes individual MIG slices
as devices in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt;. CEL selectors on the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;profile&lt;/code&gt; attribute replace the separate
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/mig-3g.20gb&lt;/code&gt; resource names used by the gpu kubelet plugin.&lt;/p&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;matchAttribute&lt;/code&gt; constraint ensures all requested slices come from the &lt;strong&gt;same physical GPU&lt;/strong&gt;:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ResourceSlice&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;computeinstance-e00xn5mewbsmgdd98v-gpu.nvidia.com-pjtvh&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;parentUUID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GPU-8ce1d817-8c25-50db-af0c-242b5437297f&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;productName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NVIDIA H100 80GB HBM3&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;4g.40gb&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;40448Mi&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;multiprocessors&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;64&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-0-mig-4g40gb-5-0&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;attributes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;parentUUID&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;GPU-8ce1d817-8c25-50db-af0c-242b5437297f&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;productName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NVIDIA H100 80GB HBM3&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;profile&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;3g.40gb&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;memory&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;40448Mi&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;multiprocessors&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;60&quot;&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-0-mig-3g40gb-9-4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ResourceClaimTemplate&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;vllm-gpu&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;deviceClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mig.nvidia.com&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;selectors&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;cel&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
              &lt;span class=&quot;na&quot;&gt;expression&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;device.attributes[&apos;gpu.nvidia.com&apos;].profile&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;4g.40gb&apos;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;||&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;device.attributes[&apos;gpu.nvidia.com&apos;].profile&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;==&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&apos;3g.40gb&apos;&quot;&lt;/span&gt; 
              &lt;span class=&quot;c1&quot;&gt;# this allocates both Migs&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;constraints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[]&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;matchAttribute&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;gpu.nvidia.com/parentUUID&quot;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# all slices from one GPU&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The driver handles MIG instance creation and teardown as part of the claim lifecycle, hence no manual
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi mig&lt;/code&gt; commands needed.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;NOTE: This closes the &lt;a href=&quot;#why-device-plugin-falls-short&quot;&gt;error-prone recovery&lt;/a&gt; gap described earlier: DRA
allocates MIG instances dynamically instead of requiring the manual hand-carving the MIG Manager can’t reconcile.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4 id=&quot;computedomains-multi-node-nvlink-officially-supported&quot;&gt;ComputeDomains: Multi-Node NVLink (Officially Supported)&lt;/h4&gt;

&lt;p&gt;A &lt;a href=&quot;https://github.com/kubernetes-sigs/dra-driver-nvidia-gpu/tree/main&quot;&gt;ComputeDomain&lt;/a&gt; (CD) is an abstraction for robust, secure Multi-Node NVLink (MNNVL) connectivity, 
the kind of setup that turns a rack of GB200 NVL72-class or Vera Rubin NVL72 nodes into what’s effectively one supercomputer, with chip-to-chip
bandwidth around 1.8 TB/s to 3.6 TB/s. Without ComputeDomains, wiring that up means hand-managing the low-level
NVLink fabric topology yourself; the driver instead gives you a Kubernetes object and does the orchestration underneath
it (optimal placement policies).&lt;/p&gt;

&lt;p&gt;It guarantees two things for pods inside the domain:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;MNNVL-reachability between them, and isolation from pods
outside it.&lt;/li&gt;
  &lt;li&gt;That isolation is implemented via &lt;a href=&quot;https://docs.google.com/presentation/d/1Xupr8IZVAjs5bNFKJnYaK0LE7QWETnJjkz6KOfLu87E/edit?pli=1&amp;amp;slide=id.g373e0ebfa8e_1_15#slide=id.g373e0ebfa8e_1_15&quot;&gt;IMEX (Internode Memory Exchange)&lt;/a&gt;: 
the driver launches and configures the IMEX daemons, domains, and channels for you, rather than requiring a manually managed IMEX
deployment alongside the workload.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.nvidia.com/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ComputeDomain&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;compute-domain&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;numNodes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;18&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resourceClaimTemplate&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;compute-domain-channel&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test-workload&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test-workload&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;18&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test-workload&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;test-workload&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ctr&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ubuntu:22.04&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;test-workload&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;4&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;claims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;channel&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;resourceClaims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;channel&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resourceClaimTemplateName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;compute-domain-channel&lt;/span&gt; &lt;span class=&quot;c1&quot;&gt;#compute-domain claim from the first resource&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Combining a ComputeDomain with a per-node topology constraint&lt;/strong&gt; is where DRA’s two topology mechanisms, single-node
PCIe placement and multi-node NVLink placement, stack. Each replica below still joins the shared &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComputeDomain&lt;/code&gt; for
MNNVL-reachability across nodes, but it also carries its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaimTemplate&lt;/code&gt; that requests a GPU and its
RDMA NIC together and constrains them to the same PCIe root switch, so GPUDirect RDMA traffic to other racks never
has to cross a NUMA boundary to get off the node:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ResourceClaimTemplate&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-nic-claim&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;devices&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;deviceClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu.example.com&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nic&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;exactly&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;deviceClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;dranet&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;constraints&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;c1&quot;&gt;# Ensure the GPU and its RDMA NIC share the same PCIe root switch&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;gpu&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nic&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;matchAttribute&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.kubernetes.io/pcieRoot&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;resource.nvidia.com/v1beta1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ComputeDomain&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;compute-domain&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;numNodes&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;18&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;channel&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resourceClaimTemplate&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;compute-domain-channel&lt;/span&gt;
&lt;span class=&quot;nn&quot;&gt;---&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;training-worker&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;training-worker&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;replicas&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;18&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;training-worker&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;training-worker&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;trainer&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;registry.example.com/trainer:latest&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;claims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;devices&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;channel&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;resourceClaims&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;devices&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resourceClaimTemplateName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-nic-claim&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;channel&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resourceClaimTemplateName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;compute-domain-channel&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devices&lt;/code&gt; claim and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;channel&lt;/code&gt; claim are independent requests resolved by the same scheduling pass: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devices&lt;/code&gt;
pins the GPU and its RDMA NIC to one PCIe root on whichever node the pod lands on, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;channel&lt;/code&gt; binds that pod into
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComputeDomain&lt;/code&gt; so its GPU stays MNNVL-reachable from the other 17 replicas. That’s the topology-awareness gap
called out earlier under &lt;a href=&quot;#why-device-plugin-falls-short&quot;&gt;Why Device Plugin Falls Short&lt;/a&gt;, now closed at both
scales at once: a device plugin can hand out “2 GPUs” independently, with no way to guarantee they share a PCIe
switch with the NIC that carries their traffic off-node, and no notion of a fabric spanning nodes at all.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;NOTE: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComputeDomain&lt;/code&gt; itself only ever aligns GPUs. It’s a Kubernetes abstraction over IMEX (Internode Memory
Exchange) channels, the mechanism that lets GPUs on separate nodes read and write each other’s memory over
Multi-Node NVLink; it has no concept of CPU, PCIe root, or NUMA placement. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devices&lt;/code&gt; claim above is doing
the GPU/NIC PCIe-root alignment entirely on its own, via the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dranet&lt;/code&gt; DeviceClass, a separate DRA driver with no
relationship to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComputeDomain&lt;/code&gt;; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;channel&lt;/code&gt; just adds GPU-to-GPU reachability across nodes on top of it. The two
are independently scheduled and satisfied, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComputeDomain&lt;/code&gt; doesn’t know or care what else a claim in the same
pod is requesting.&lt;/p&gt;

  &lt;p&gt;It’s also not RDMA. IMEX moves memory over the NVLink/NVSwitch fabric via CUDA’s fabric-memory export/import
calls, a different transport from the GPUDirect RDMA the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu-nic-claim&lt;/code&gt; above is set up to accelerate, and
NVIDIA’s own docs treat them as distinct: disabling MNNVL makes NCCL “fall back to the available network
configurations… such as InfiniBand or Ethernet (RoCE),” which only makes sense if the two are separate paths,
not the same one. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ComputeDomain&lt;/code&gt; never touches the RDMA NIC itself; that’s entirely the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dranet&lt;/code&gt; claim’s job.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Unlike the experimental GPU plugin, ComputeDomain support is officially maintained and production-ready but two
details matter before you treat it as a hard multi-tenancy boundary:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The isolation guarantee is scoped to namespaces, not workloads.&lt;/strong&gt; A job in namespace A can never join a
ComputeDomain created for namespace B, but two workloads &lt;em&gt;sharing a namespace&lt;/em&gt; aren’t protected from each other
the same way. Treat “one ComputeDomain, one namespace, one tenant” as the safe default, not an incidental detail.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;ComputeDomains are ephemeral, tied to the workload’s lifetime.&lt;/strong&gt; The domain forms around the pods as they’re
scheduled and tears down when the job completes; there’s no long-lived, pre-provisioned domain sitting idle
waiting for work the way a MIG slice can.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Above the DRA layer, NCCL 2.25+ is the minimum version with MNNVL support. Hence an older NCCL in your training image
will simply not use the NVLink fabric a ComputeDomain gives it, silently falling back to slower interconnects
instead of failing outright.&lt;/p&gt;

&lt;h3 id=&quot;dra-scheduling-flow&quot;&gt;DRA Scheduling Flow&lt;/h3&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;User creates ResourceClaim (status: unallocated)
              ↓
kube-scheduler reads ResourceSlice objects from all nodes
              ↓
Evaluates CEL selectors against device attributes
              ↓
Scores and selects the best matching node
              ↓
Scheduler writes result into ResourceClaim.status.allocation:
  { &apos;devices&apos;: { &apos;results&apos;: [
  { &apos;driver&apos;: &apos;gpu.nvidia.com&apos;, &apos;pool&apos;: &apos;node-gpu-01&apos;,
  &apos;device&apos;: &apos;gpu-0&apos;, &apos;request&apos;: &apos;gpu&apos; } ]} }
              ↓
kubelet on node-gpu-01 sees the bound claim
              ↓
kubelet calls: gpu-kubelet-plugin.NodePrepareResources(claimUID)
              ↓
Driver writes CDI spec for the allocated device
              ↓
kubelet passes CDI device name to containerd
              ↓
containerd applies CDI spec → container starts with GPU access
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The key difference from the device plugin flow: &lt;strong&gt;the scheduler has full visibility into device
attributes and makes the allocation decision&lt;/strong&gt;, rather than the plugin deciding inside an opaque
gRPC call at pod start.&lt;/p&gt;

&lt;h3 id=&quot;dra-vs-device-plugin-comparison&quot;&gt;DRA vs Device Plugin Comparison&lt;/h3&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Aspect&lt;/th&gt;
      &lt;th&gt;Device Plugin&lt;/th&gt;
      &lt;th&gt;DRA Driver&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Resource discovery&lt;/td&gt;
      &lt;td&gt;gRPC &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListAndWatch()&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; Kubernetes objects&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Resource request&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resources.limits&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaimTemplate&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Scheduler visibility&lt;/td&gt;
      &lt;td&gt;Opaque count only&lt;/td&gt;
      &lt;td&gt;Full attributes queryable via CEL&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Allocation decision&lt;/td&gt;
      &lt;td&gt;Plugin at pod start&lt;/td&gt;
      &lt;td&gt;Scheduler at scheduling time&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MIG support&lt;/td&gt;
      &lt;td&gt;Separate resource names per profile&lt;/td&gt;
      &lt;td&gt;CEL selectors on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;profile&lt;/code&gt; attribute&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Multi-node NVLink&lt;/td&gt;
      &lt;td&gt;Not supported&lt;/td&gt;
      &lt;td&gt;ComputeDomain plugin (officially supported)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Shared GPU between containers&lt;/td&gt;
      &lt;td&gt;Not supported&lt;/td&gt;
      &lt;td&gt;Supported via shared &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt;&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Kubernetes version&lt;/td&gt;
      &lt;td&gt;Stable since 1.10&lt;/td&gt;
      &lt;td&gt;Beta since 1.32, GA (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v1&lt;/code&gt;) from Kubernetes 1.34&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;operations&quot;&gt;Operations&lt;/h2&gt;

&lt;p&gt;Operating GPUs at scale, keeping the GPU Operator’s DaemonSets (and the MIG layouts they manage) healthy on real
clusters, and Dynamic Resource Allocation (DRA), the next generation of GPU scheduling that succeeds the device
plugin framework for fine-grained, topology-aware device allocation.&lt;/p&gt;

&lt;h3 id=&quot;gpu-operator-troubleshooting&quot;&gt;GPU Operator Troubleshooting&lt;/h3&gt;

&lt;h4 id=&quot;common-mig-issues&quot;&gt;Common MIG issues&lt;/h4&gt;
&lt;p&gt;The &lt;a href=&quot;https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html&quot;&gt;GPU Operator&lt;/a&gt; and its MIG Manager (introduced in &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html#who-actually-stands-mig-up&quot;&gt;Who Actually Stands MIG Up&lt;/a&gt; in Part 2)
do most of the day-to-day work of running MIG on a cluster, but the automation has sharp edges. Worth budgeting
time for when you’re standing up a MIG-enabled node:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The MIG Manager treats “no label” as “no MIG.”&lt;/strong&gt; If a node has no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/mig.config&lt;/code&gt; label at all, the
manager’s default reconciliation target is &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all-disabled&lt;/code&gt; which will tear down any instances you carved by
hand the moment the manager starts watching that node. Label the node &lt;em&gt;before&lt;/em&gt; you touch &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi mig&lt;/code&gt;, not
after.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The manager only reacts to Kubernetes events, not hardware state.&lt;/strong&gt; If you SSH in and change the MIG layout
directly with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi&lt;/code&gt;, the operator has no way to notice. The MIG Manager’s controller loop is driven by label
watches, not a poll of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi mig -lgi&lt;/code&gt;. A stuck reconciliation usually means restarting the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-mig-manager&lt;/code&gt; DaemonSet or toggling the label off and back on to force a re-evaluation.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The GI/CI hierarchy is enforced, not advisory.&lt;/strong&gt; Attempting to delete a GPU Instance (GI) while a Compute Instance (CI)
still lives inside it fails outright (“In use by another client”), and a slice with a running pod on it can’t
be destroyed until that pod is evicted.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;New architectures need new container images.&lt;/strong&gt; Blackwell-class cards need a CUDA toolkit and PyTorch/TensorFlow
build compiled for that compute capability. So an older NGC image will schedule fine and then fail at the first
kernel launch with an “unsupported” error that has nothing to do with Kubernetes.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;You can’t see inside a slice from the outside.&lt;/strong&gt; Because isolation is hardware-enforced, standard cluster
GPU dashboards need per-slice telemetry (the DCGM exporter plus a MIG-aware Grafana dashboard), whole-GPU
utilization graphs will just show the parent card and hide how the individual slices are actually being used.&lt;/p&gt;

    &lt;blockquote&gt;
      &lt;p&gt;A per-slice dashboard is already covered below: see the live Hardware Health dashboard under
&lt;a href=&quot;#gpu-fleet-reliability-metrics-and-slos&quot;&gt;GPU Fleet Reliability&lt;/a&gt; and the per-MIG-slice memory/utilization
panels under &lt;a href=&quot;#4-utilization--efficiency&quot;&gt;Utilization &amp;amp; Efficiency&lt;/a&gt;.&lt;/p&gt;
    &lt;/blockquote&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; &lt;a href=&quot;https://github.com/hrishin/dotfiles/blob/master/scripts/gpu-node-debug.sh&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu-node-debug.sh&lt;/code&gt;&lt;/a&gt; automates the
checks behind the first two bullets above and the containerd mismatch below: it reads the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/mig.config&lt;/code&gt;
node label and reconciliation state directly, cross-checks the GI/CI hierarchy via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi mig&lt;/code&gt;, and
compares kubelet’s actual containerd instance against the operator’s configured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONTAINERD_SOCKET&lt;/code&gt;/
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONTAINERD_CONFIG&lt;/code&gt;.&lt;/p&gt;

&lt;h4 id=&quot;containerd-cdi-vs-runtime-class-and-non-kube-containerd-instance&quot;&gt;Containerd: CDI vs Runtime Class and non-kube containerd instance&lt;/h4&gt;

&lt;p&gt;On a cluster running the &lt;a href=&quot;#dynamic-resource-allocation-dra-next-generation-gpu-scheduling&quot;&gt;DRA driver&lt;/a&gt;
instead of the device plugin, check the containerd drop-in the GPU Operator’s toolkit generates
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/containerd/conf.d/99-nvidia.toml&lt;/code&gt;, or wherever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CONTAINERD_CONFIG&lt;/code&gt; points) before assuming a
runtime-selection bug. Three things stand out:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;#/etc/containerd/conf.d/99-nvidia.toml&lt;/span&gt;

&lt;span class=&quot;py&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[plugins]&lt;/span&gt;

  &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;]&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;cdi_spec_dirs&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/etc/cdi&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/var/run/cdi&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;py&quot;&gt;enable_cdi&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;

  &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd]&lt;/span&gt;
      &lt;span class=&quot;py&quot;&gt;default_runtime_name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;runc&quot;&lt;/span&gt;
      &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;
      &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes]&lt;/span&gt;

        &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes.nvidia]&lt;/span&gt;
          &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;
          &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes.nvidia.options]&lt;/span&gt;
            &lt;span class=&quot;py&quot;&gt;BinaryName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/usr/local/nvidia/toolkit/nvidia-container-runtime&quot;&lt;/span&gt;
            &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;

      &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes.nvidia-cdi]&lt;/span&gt;
          &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes.nvidia-cdi.options]&lt;/span&gt;
            &lt;span class=&quot;py&quot;&gt;BinaryName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/usr/local/nvidia/toolkit/nvidia-container-runtime.cdi&quot;&lt;/span&gt;
            &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default_runtime_name = &quot;runc&quot;&lt;/code&gt;: plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runc&lt;/code&gt; stays the default; no need to set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName: nvidia&lt;/code&gt; unless
the container needs GPUs (terms and conditions apply).&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enable_cdi = true&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdi_spec_dirs = [&quot;/etc/cdi&quot;, &quot;/var/run/cdi&quot;]&lt;/code&gt;: this is what matters in a
DRA+CDI setup: since containers get GPUs via CDI device injection (driven by the DRA driver, not by selecting a
special runtime), a plain runc-launched pod still gets GPU access as long as CDI specs are present in one of
those directories.&lt;/li&gt;
  &lt;li&gt;Three NVIDIA runtimes are registered anyway (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-cdi&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-legacy&lt;/code&gt;), each pointing at a
different binary under &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/usr/local/nvidia/toolkit/&lt;/code&gt;, available for pods that opt in via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName&lt;/code&gt;, but
not required.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tracing one real pod (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vllm qwen pod&lt;/code&gt;) through this confirmed all three points:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;No runtime class used.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen&lt;/code&gt;’s pod spec has &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName&lt;/code&gt; empty: it runs under plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runc&lt;/code&gt;, not
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-cdi&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-legacy&lt;/code&gt; even with the new version of device plugin.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;An older device plugin version that isn’t CDI-aware, or a pod explicitly setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName: nvidia&lt;/code&gt;, falls
back to the container hook path discussed earlier. The GPU Operator’s container toolkit patches the
&lt;em&gt;default&lt;/em&gt; containerd config to add these &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimes&lt;/code&gt; entries, but on distributions like MicroK8s or RKE
that run their own containerd instance, that default path isn’t the one kubelet is actually reading. Point
the toolkit at the wrong containerd instance and the runtime patch silently never lands, and GPU
provisioning fails.&lt;/p&gt;

    &lt;p&gt;&lt;strong&gt;FIX&lt;/strong&gt;: Point the GPU Operator chart at the containerd instance kubelet actually uses.&lt;/p&gt;

    &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;   &lt;span class=&quot;na&quot;&gt;toolkit&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
     &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
     &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CONTAINERD_CONFIG&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/var/lib/k8s-containerd/k8s-containerd/etc/containerd/config.toml&lt;/span&gt;
     &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CONTAINERD_SOCKET&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/var/lib/k8s-containerd/k8s-containerd/run/containerd/containerd.sock&lt;/span&gt;
     &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;CONTAINERD_RUNTIME_CLASS&lt;/span&gt;
       &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h3 id=&quot;installing-the-nvidia-dra-driver-via-helm&quot;&gt;Installing the NVIDIA DRA Driver via Helm&lt;/h3&gt;

&lt;p&gt;The chart image for the &lt;a href=&quot;#dynamic-resource-allocation-dra-next-generation-gpu-scheduling&quot;&gt;DRA driver&lt;/a&gt; is served
from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;registry.k8s.io/dra-driver-nvidia/dra-driver-nvidia-gpu&lt;/code&gt;. GPU allocation is gated behind
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpuResourcesEnabledOverride=true&lt;/code&gt; because it is still experimental: the upstream README is explicit that “GPU
allocation features can be tried out” but “are not yet officially supported,” which is why the Helm chart leaves
the GPU kubelet plugin disabled unless you opt in.&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;helm upgrade &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--create-namespace&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--namespace&lt;/span&gt; gpu-operator &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  dra-driver-nvidia-gpu &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  oci://registry.k8s.io/dra-driver-nvidia/dra-driver-nvidia-gpu &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--set&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;gpuResourcesEnabledOverride&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;true&lt;/span&gt; &lt;span class=&quot;se&quot;&gt;\&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;--wait&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Verify: each GPU node should show a 2-container pod&lt;/span&gt;
kubectl &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; gpu-operator get pods | &lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;dra
nvidia-dra-driver-gpu-controller-699474f64f-h7ppr                 1/1     Running     0               4h17m
nvidia-dra-driver-gpu-kubelet-plugin-gps66                        2/2     Running     0               42m

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Requires Kubernetes 1.32+ with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DynamicResourceAllocation&lt;/code&gt; feature gate enabled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A second, more opinionated install path&lt;/strong&gt; runs through the NVIDIA GPU Operator itself (v26.3.3+ ships DRA
support as a documented install target rather than a bare Helm chart), that route is worth knowing about because
its prerequisites are noticeably stricter than “1.32+ with the feature gate on”:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Kubernetes v1.34.2+ (bump to v1.36.0+ if you intend to mix traditional &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;resources.limits.nvidia.com/gpu&lt;/code&gt;
requests with DRA claims on the same cluster)&lt;/li&gt;
  &lt;li&gt;GPU driver 580+, with CDI enabled in the container runtime&lt;/li&gt;
  &lt;li&gt;Node Feature Discovery and GPU Feature Discovery already deployed&lt;/li&gt;
  &lt;li&gt;GPU nodes labeled &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/dra-kubelet-plugin=true&lt;/code&gt;, and the traditional NVIDIA Device Plugin disabled on
those nodes, since the two allocation paths aren’t meant to run against the same GPUs at once&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two operational rough edges are worth planning around before you rely on this in a real cluster:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;The NVIDIA Driver Manager doesn’t cleanly evict the DRA kubelet plugin&lt;/strong&gt; when it needs to reload the driver:
the documented workaround is to pass the DRA node labels through &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;driver.manager.env&lt;/code&gt; so the manager knows to
drain it first.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A100 MIG reconfiguration doesn’t auto-propagate to the DRA plugin.&lt;/strong&gt; After changing a MIG layout on an A100,
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu-kubelet-plugin&lt;/code&gt; needs a manual restart to pick up the new &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; shape; it won’t notice on
its own the way &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html#who-actually-stands-mig-up&quot;&gt;the MIG Manager does for the device-plugin path&lt;/a&gt; (Part 2).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And if you’re upgrading an existing install from the pre-&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v0.4.0&lt;/code&gt; chart generation, set &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nameOverride=nvidia-dra-driver-gpu&lt;/code&gt;
explicitly: omitting it produces duplicate manifests alongside the old release instead of replacing it.
Downgrading back past &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;v0.4.0&lt;/code&gt; isn’t supported once you’ve moved forward.&lt;/p&gt;

&lt;hr /&gt;
&lt;h3 id=&quot;gpu-fleet-reliability-metrics-and-slos&quot;&gt;GPU Fleet Reliability: Metrics and SLOs&lt;/h3&gt;

&lt;p&gt;Everything above gets a GPU into a container. None of it tells you whether the fleet is actually &lt;em&gt;healthy&lt;/em&gt;, and
for GPU capacity specifically, “healthy” means more than “the pod is Running.” A GPU node that’s up but silently
throttling, a MIG slice that’s been torn down and never noticed, or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt; that’s been sitting
unallocated for ten minutes are all outages that look fine from a plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl get pods&lt;/code&gt;. SLOs for a GPU fleet
could be split into four categories. The last one is the category the tooling is worst at surfacing on its own.&lt;/p&gt;

&lt;h4 id=&quot;1-hardware-health&quot;&gt;1. Hardware Health&lt;/h4&gt;

&lt;p&gt;This is what &lt;a href=&quot;https://developer.nvidia.com/dcgm&quot;&gt;DCGM&lt;/a&gt; (Data Center GPU Manager) exists for, and it’s the one
category with mature, off-the-shelf tooling: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgm-exporter&lt;/code&gt;, deployed as a GPU Operator component
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgmExporter.enabled&lt;/code&gt;, on by default), exposes per-GPU and per-MIG-instance Prometheus metrics with no extra
config. Per-&lt;em&gt;pod&lt;/em&gt; attribution is a separate, optional flag (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enablePodLabels: true&lt;/code&gt;); see the caveat about it
under &lt;a href=&quot;#4-utilization--efficiency&quot;&gt;Utilization &amp;amp; Efficiency&lt;/a&gt; below, because it doesn’t actually work on a
DRA-based cluster. The fields that actually belong in an SLO, as opposed to a dashboard nobody looks at:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Metric&lt;/th&gt;
      &lt;th&gt;What it means&lt;/th&gt;
      &lt;th&gt;SLO framing&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_XID_ERRORS&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Driver-level fault code: anything from a benign transient to a fatal ECC/Xid 79 &quot;GPU has fallen off the bus&quot;&lt;/td&gt;
      &lt;td&gt;Don’t alert on &quot;non-zero&quot;: verified live, the series is simply **absent** when healthy (DCGM returns a blank value, and the exporter drops it rather than emitting 0), so there’s nothing to compare against. Add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_EXP_XID_ERRORS_TOTAL&lt;/code&gt; instead: an exporter-owned counter, opt-in and commented out in the default CSV, that only creates a series once an XID actually fires; alert on that series existing, via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;increase(...) &amp;gt; 0&lt;/code&gt;.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Uncorrectable (double-bit) ECC memory errors&lt;/td&gt;
      &lt;td&gt;Any increase → page. Silent data corruption risk, not just a reliability blip.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_ECC_SBE_VOL_TOTAL&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Correctable (single-bit) ECC errors&lt;/td&gt;
      &lt;td&gt;Trend, don’t page on one: a rising rate predicts a DBE and a future Xid.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_THERMAL_VIOLATION&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_POWER_VIOLATION&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Time spent throttled by thermal or power limits&lt;/td&gt;
      &lt;td&gt;Non-zero over a sustained window means the workload isn’t getting the compute the profile promised: a MIG &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4g.40gb&lt;/code&gt; throttled to 60% clock isn’t really &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4g.40gb&lt;/code&gt; anymore.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_PROF_GR_ENGINE_ACTIVE&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Fraction of time an SM has a warp resident, the *real* utilization signal&lt;/td&gt;
      &lt;td&gt;Prefer this over &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_GPU_UTIL&lt;/code&gt;, which only reports &quot;was any kernel running,&quot; not how much of the card that kernel actually used. A GPU can show 100% &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU_UTIL&lt;/code&gt; while running a memory-bound kernel that uses 5% of the SMs.&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_FB_USED&lt;/code&gt; / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_FB_FREE&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Framebuffer (VRAM) used/free&lt;/td&gt;
      &lt;td&gt;Capacity planning input, and the fastest way to catch a memory leak before it OOMs a neighbor.&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;What actually gets exposed is a separate concern from scraping it&lt;/strong&gt;, and worth checking directly
rather than assuming: GPU Operator’s own baked-in metrics list (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcp-metrics-included.csv&lt;/code&gt;) covers
utilization/clocks/memory/PCIe/energy but omits every field in the table above: no XID, no ECC, no thermal or
power violation. Getting them requires pointing &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgmExporter.config.name&lt;/code&gt; at your own ConfigMap with a superset
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgm-metrics.csv&lt;/code&gt; (the exporter reads a three-column &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM field, prometheus type, help text&lt;/code&gt; CSV):&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# GPU Operator values&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;dcgmExporter&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;enablePodLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# per-pod attribution: see the DRA caveat below&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;serviceMonitor&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;enabled&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;no&quot;&gt;true&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;config&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;custom-dcgm-metrics&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# ConfigMap with a dcgm-metrics.csv adding&lt;/span&gt;
                                 &lt;span class=&quot;c1&quot;&gt;# DCGM_FI_DEV_XID_ERRORS, DCGM_EXP_XID_ERRORS_TOTAL,&lt;/span&gt;
                                 &lt;span class=&quot;c1&quot;&gt;# ECC_SBE/DBE_VOL_TOTAL, THERMAL_VIOLATION,&lt;/span&gt;
                                 &lt;span class=&quot;c1&quot;&gt;# POWER_VIOLATION, etc.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&quot;/assets/gpu-fleet-slo-dashboard-hardware-health.png&quot; alt=&quot;Grafana dashboard showing all six Hardware Health SLIs live: XID errors, ECC DBE/SBE trends, thermal/power
violation time, SM occupancy via GR_ENGINE_ACTIVE, and framebuffer used vs free, all scoped to one MIG-sliced
node&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Every metric from the table above, live on one node: XID Errors sits at a clean &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0&lt;/code&gt;, via
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_EXP_XID_ERRORS_TOTAL&lt;/code&gt;, not the blank-value gauge and SM occupancy (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GR_ENGINE_ACTIVE&lt;/code&gt;) shows the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4g.40gb&lt;/code&gt; slice doing work while its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.40gb&lt;/code&gt; sibling sits idle, the same per-slice split called out
under &lt;a href=&quot;#4-utilization--efficiency&quot;&gt;Utilization &amp;amp; Efficiency&lt;/a&gt; below.&lt;/p&gt;

&lt;h4 id=&quot;2-scheduling--allocation-latency&quot;&gt;2. Scheduling &amp;amp; Allocation Latency&lt;/h4&gt;

&lt;p&gt;“Time from pod submitted to GPU compute actually running” is the SLI that maps most directly to user-visible
pain. A training job that queues for 40 minutes waiting on a GPU is a very different incident from one that
starts in 4 seconds, even though both eventually succeed. The two allocation paths expose this very differently:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Device Plugin path&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube_pod_status_scheduled&lt;/code&gt; combined with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu&lt;/code&gt; allocatable/capacity gives
you time-to-schedule. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; gRPC call itself isn’t instrumented by default if you need that
granularity, it’s a custom metric on top of the device plugin, not something you get for free.&lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;DRA path&lt;/strong&gt;: there’s no mature off-the-shelf histogram for this yet. The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt; is still a young API, and
the ecosystem’s observability tooling (kube-state-metrics support, standard Grafana dashboards) hasn’t fully
caught up to it the way it has for pods and deployments. What &lt;em&gt;is&lt;/em&gt; directly observable, because we relied on it
throughout this series’ troubleshooting, is the claim’s own state:&lt;/p&gt;

    &lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubectl get resourceclaims &lt;span class=&quot;nt&quot;&gt;-n&lt;/span&gt; qwen
NAME                                         STATE                AGE
vllm-qwen2-5-7b-65f4bfc79f-rdh4n-gpu-5b7cb   allocated,reserved   6s
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;A claim sitting in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt; (empty &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status: {}&lt;/code&gt;, no &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;status.allocation&lt;/code&gt;) for longer than your allocation SLO is
the DRA-native signal to alert on: poll it, or better, watch the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;FailedScheduling&lt;/code&gt; event on the pod, which
carries the actual reason (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cannot allocate all claims&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;untolerated taint&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;didn&apos;t match node affinity&lt;/code&gt;).
Treat “claim pending &amp;gt; N minutes” as page-worthy in exactly the way “pod pending &amp;gt; N minutes” already is for
CPU-only workloads.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;When autoscaling is in play, “pending” starts before the pod does.&lt;/strong&gt; A scale-up adds a new node, and
none of the metrics above cover node-launch to node-ready to driver-ready, a gap invisible to both the Device
Plugin and DRA signals above, since neither starts watching until the node is already &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ready&lt;/code&gt;. A lifecycle
tracer spanning that full path through to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;model-ready&lt;/code&gt; turns “why did this take N minutes” into an answer:&lt;/p&gt;

    &lt;p&gt;&lt;img src=&quot;/assets/gpu-node-cold-start-trace-device-plugin.png&quot; alt=&quot;Tempo trace breaking down a GPU node cold start into node-launch, k8s-registration, device plugin init,
image pull, and container-to-model-ready spans, device plugin path&quot; /&gt;&lt;/p&gt;

    &lt;p&gt;Device Plugin path, 19m24s total: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node-ready-to-device-plugin-initialized&lt;/code&gt; was 12m18s of it. The obvious
read is “driver install is slow,” but breaking that span down by GPU Operator sub-component (NFD → driver →
toolkit → MIG Manager → device plugin) shows otherwise: actual driver module load/init is ~1m, MIG and
device-plugin registration are each under a minute; the two big chunks are operator reconcile delay before
the driver DaemonSet is even created (~5m) and DaemonSet/CNI scheduling before NFD starts (~3m). Neither is
“work” you can bake into an image. That’s the case for tracing here: without sub-spans, “driver install” is
a plausible-sounding, wrong optimization target.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;3-control-plane-reconciliation-correctness&quot;&gt;3. Control-Plane Reconciliation Correctness&lt;/h4&gt;

&lt;p&gt;This is the category the tooling is genuinely weakest at, and, as covered across
&lt;a href=&quot;#gpu-operator-troubleshooting&quot;&gt;GPU Operator Troubleshooting&lt;/a&gt; and
&lt;a href=&quot;#installing-the-nvidia-dra-driver-via-helm&quot;&gt;Installing the NVIDIA DRA Driver via Helm&lt;/a&gt;, where the real incidents
in a MIG + DRA fleet come from. None of these show up as a failed pod; they show up as a pod stuck
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pending&lt;/code&gt; for reasons that look, from the outside, exactly like “the cluster is out of capacity” when it isn’t.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/mig.config.state&lt;/code&gt; as a literal state machine.&lt;/strong&gt; The MIG Manager writes &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;success&lt;/code&gt; or
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pending&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;failed&lt;/code&gt; onto the node after every reconfiguration attempt. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;failed&lt;/code&gt; is unambiguous and immediately
actionable; alert on it directly rather than inferring it from downstream symptoms:&lt;/p&gt;

    &lt;pre&gt;&lt;code class=&quot;language-promql&quot;&gt;# kube-state-metrics exposes node labels as a gauge; alert on the literal value
# (requires --metric-labels-allowlist covering this label, off by default)
kube_node_labels{label_nvidia_com_mig_config_state=&quot;failed&quot;}
&lt;/code&gt;&lt;/pre&gt;

    &lt;p&gt;In practice &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;failed&lt;/code&gt; usually means a GPU-consuming pod wasn’t evicted before the manager tried to touch the
layout (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ERROR_IN_USE&lt;/code&gt; from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi mig -cgi&lt;/code&gt;); see the eviction point below, they’re the same root cause
wearing two different symptoms.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; staleness: a silent, not a loud, failure.&lt;/strong&gt; The DRA kubelet-plugin enumerates GPU/MIG
topology via NVML &lt;strong&gt;once at process startup&lt;/strong&gt; and caches it. A MIG reconfiguration can succeed completely at
the hardware level: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi -L&lt;/code&gt; shows the new instances immediately while the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; the
scheduler actually reads keeps advertising the &lt;em&gt;old&lt;/em&gt; device shape indefinitely, because nothing tells the
kubelet-plugin its cached view is stale. There’s no error, no event, no failed reconciliation, just a
scheduler that keeps allocating against devices that no longer exist in that shape. The only fix is restarting
the plugin pod after any MIG topology change; there’s no notification path that makes this automatic today.
Track it operationally as: &lt;strong&gt;MIG config change → wait for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig.config.state=success&lt;/code&gt; → restart
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*-kubelet-plugin&lt;/code&gt; → verify the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; device list actually changed&lt;/strong&gt; before assuming the change took
effect. Skip the last step and you’ll ship a config change that silently does nothing.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Pod eviction during driver reloads.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu-operator&lt;/code&gt;’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;driver.manager&lt;/code&gt; init container evicts GPU-consuming
pods before reloading the kernel module but out of the box it only knows how to find classic device-plugin
consumers. A DRA &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt; pod is invisible to it unless &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;driver.manager.env&lt;/code&gt; is explicitly pointed at the
node label identifying DRA-eligible nodes:&lt;/p&gt;

    &lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;driver&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;manager&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;env&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;NODE_LABEL_FOR_GPU_POD_EVICTION&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia.com/dra-kubelet-plugin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;

    &lt;p&gt;Without this, a routine driver upgrade can restart the driver DaemonSet out from under a running DRA pod instead
of draining it first; the pod doesn’t necessarily crash, but its GPU access can end up in an undefined state
until it’s manually cycled. The SLI here is binary and worth its own alert: did every GPU-consuming pod on a
node get cleanly evicted and rescheduled around a driver reload, or did any of them survive the reload in place
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube_pod_start_time&lt;/code&gt; unchanged across a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-driver-daemonset&lt;/code&gt; rollout on the same node is the tell).&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Single-instance GPU + &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RollingUpdate&lt;/code&gt; is a deadlock, not a slow rollout, on a fixed node pool.&lt;/strong&gt; A
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Deployment&lt;/code&gt; pinned to a scarce GPU (whole-device or a single MIG slice) with the default &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RollingUpdate&lt;/code&gt;
strategy will try to schedule the new pod and its new claim before freeing the old one’s device. With
exactly one instance of that shape and no room to grow, this can’t ever succeed: the new pod stays &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Pending&lt;/code&gt;
forever, and the old pod is never torn down because the rollout hasn’t progressed. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubectl rollout status&lt;/code&gt;
hanging past its usual duration on a GPU workload is the signal; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;strategy: { type: Recreate }&lt;/code&gt; is the fix.
The exception is cluster autoscaler adding a same-shape node so the new pod schedules there instead, and
that’s markedly more reliable on &lt;strong&gt;DRA&lt;/strong&gt;, whose structured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceClass&lt;/code&gt; model the autoscaler
can actually simulate against, than on the device plugin, where MIG-shaped extended resources are mostly
opaque to that simulation. Default to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Recreate&lt;/code&gt; on device plugin; on DRA with real autoscaling headroom,
verify a rollout with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RollingUpdate&lt;/code&gt; actually lands on a new node before trusting it.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;4-utilization--efficiency&quot;&gt;4. Utilization &amp;amp; Efficiency&lt;/h4&gt;

&lt;p&gt;Not an availability SLO in the classic sense, but on hardware this expensive, “the fleet is up” and “the fleet is
being used” are different questions worth tracking separately:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Per-slice, not per-card, utilization.&lt;/strong&gt; As noted back in
&lt;a href=&quot;#gpu-operator-troubleshooting&quot;&gt;GPU Operator Troubleshooting&lt;/a&gt;: a whole-GPU dashboard built on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_GPU_UTIL&lt;/code&gt;
hides exactly the number you need once MIG is involved: it reports the parent card’s aggregate state, not
what each &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4g.40gb&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.40gb&lt;/code&gt; instance is individually doing. This part works out of the box: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgm-exporter&lt;/code&gt;
labels every metric with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU_I_ID&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU_I_PROFILE&lt;/code&gt; per MIG instance with no configuration needed. Confirmed live:
one instance reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;36038&lt;/code&gt; MiB used (the actual workload), the sibling &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.40gb&lt;/code&gt; slice reading &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;43&lt;/code&gt; MiB
(idle).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Per-&lt;em&gt;pod&lt;/em&gt; attribution is a separate feature, and it doesn’t work under DRA.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgmExporter.enablePodLabels&lt;/code&gt;
is what’s supposed to add &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;pod&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;namespace&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;container&lt;/code&gt; labels on top of the per-slice ones above, so usage can
be attributed to a workload rather than just a device. Tested directly against a DRA-allocated MIG slice: with
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_EXPORTER_KUBERNETES_ENABLE_POD_LABELS=true&lt;/code&gt; confirmed present in the container’s own env, the raw
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/metrics&lt;/code&gt; output still carries no pod/namespace/container label at all, same per-slice series as the
unattributed case above. The mechanism relies on the kubelet &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;podresources&lt;/code&gt; gRPC API, which classic
device-plugin allocations populate and DRA &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt; allocations do not. Practically: on a DRA-based
cluster, “which pod is using this slice” isn’t answerable from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgm-exporter&lt;/code&gt; alone: you’d need to join
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU_I_ID&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;UUID&lt;/code&gt; against the DRA driver’s own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim.status.allocation&lt;/code&gt; data yourself (e.g. a recording
rule or sidecar exporter), because nothing upstream does that join today.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Idle-slice ratio.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;count(mig instances with near-zero DCGM_FI_PROF_GR_ENGINE_ACTIVE) / count(total mig instances)&lt;/code&gt;
over a rolling window is a direct cost signal: an idle &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.40gb&lt;/code&gt; slice sitting unclaimed is the same wasted
spend as an idle whole GPU, just smaller and easier to lose track of because it doesn’t show up as a distinct
line item anywhere.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fragmentation.&lt;/strong&gt; The &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html#who-actually-stands-mig-up&quot;&gt;homogeneous-vs-heterogeneous &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig-parted&lt;/code&gt; trade-off&lt;/a&gt; means
a naive &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all-&amp;lt;profile&amp;gt;&lt;/code&gt; config can leave real, unallocated capacity permanently invisible, a single
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;all-4g.40gb&lt;/code&gt; config on an 80GB H100 only ever creates one instance and leaves the remaining ~40GB/3 compute
slices unpartitioned, not merely idle. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sum(DCGM_FI_DEV_FB_FREE)&lt;/code&gt; at the &lt;em&gt;node&lt;/em&gt; level will look fine even while
this is happening; catching it requires comparing the physical GPU’s total capacity against what’s actually
been carved into &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; devices, not just what’s allocated out of what was carved.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;/assets/nvidia-dcgm-dashboard-mig-per-slice.png&quot; alt=&quot;NVIDIA&apos;s community DCGM Dashboard for Kubernetes, showing per-GPU health (temperature, power, clocks) and
per-MIG-profile memory/utilization panels, with an Allocation Table joining MIG profile to
namespace/pod&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href=&quot;https://grafana.com/grafana/dashboards/23382-nvidia-mig-dcgm/&quot;&gt;NVIDIA DCGM Dashboard for Kubernetes (MIG &amp;amp; Non-MIG GPUs)&lt;/a&gt;
puts both of the points above on one screen: per-slice memory and utilization panels show one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4g.40gb&lt;/code&gt; profile
doing real work while its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.40gb&lt;/code&gt; sibling sits idle (43 MB used, matching the idle reading cited earlier), and
its Allocation Table resolves that busy slice to namespace &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;qwen&lt;/code&gt; and pod &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vllm-qwen2-5-7b-7649997fc7-pv8gd&lt;/code&gt;,
one concrete version of the manual join described above.&lt;/p&gt;

&lt;h4 id=&quot;a-minimal-slo-set&quot;&gt;A Minimal SLO Set&lt;/h4&gt;

&lt;p&gt;Pulling the above into something an on-call rotation could commit to:&lt;/p&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;SLI&lt;/th&gt;
      &lt;th&gt;Target&lt;/th&gt;
      &lt;th&gt;Primary signal&lt;/th&gt;
      &lt;th&gt;Severity on breach&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;GPU hardware fault rate&lt;/td&gt;
      &lt;td&gt;Zero Xid/DBE events per node per week&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_EXP_XID_ERRORS_TOTAL&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Page&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;MIG reconciliation success&lt;/td&gt;
      &lt;td&gt;100% of &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig.config&lt;/code&gt; changes reach &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;state=success&lt;/code&gt; within 5 min&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube_node_labels{label_nvidia_com_mig_config_state}&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Page&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;GPU allocation latency&lt;/td&gt;
      &lt;td&gt;p95 claim/pod pending → Running &amp;lt; 2 min (steady-state capacity)&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim&lt;/code&gt; state / &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube_pod_status_scheduled&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Warn → page if sustained&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Driver-reload eviction correctness&lt;/td&gt;
      &lt;td&gt;100% of GPU pods rescheduled (not survived-in-place) across a driver DaemonSet rollout&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kube_pod_start_time&lt;/code&gt; vs DaemonSet rollout window&lt;/td&gt;
      &lt;td&gt;Page&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Sustained thermal/power throttling&lt;/td&gt;
      &lt;td&gt;&amp;lt; 1% of GPU-active time under violation&lt;/td&gt;
      &lt;td&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_THERMAL_VIOLATION&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DCGM_FI_DEV_POWER_VIOLATION&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Warn&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Fleet utilization&lt;/td&gt;
      &lt;td&gt;&amp;gt; 70% of allocated slices with non-trivial &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GR_ENGINE_ACTIVE&lt;/code&gt;&lt;/td&gt;
      &lt;td&gt;Per-slice DCGM metrics&lt;/td&gt;
      &lt;td&gt;Info / capacity planning&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;None of this replaces the operational habits from the troubleshooting sections above: a green dashboard doesn’t
mean a MIG reconfiguration actually propagated, and the only way to be sure is still the manual
verify-after-every-change discipline those sections describe. Metrics catch drift and hardware faults; they don’t
substitute for knowing that a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; needs a kubelet-plugin restart to reflect a change that already
happened underneath it.&lt;/p&gt;

&lt;p&gt;Everything above is what to measure. Below is the short version of when: Day 1 setup, Day 2 runbook, and the
three signals worth checking, in the order to check them.&lt;/p&gt;

&lt;h4 id=&quot;observability-check-in-this-order&quot;&gt;Observability: Check in This Order&lt;/h4&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;State fields, first, always&lt;/strong&gt;: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig.config.state&lt;/code&gt;, whether &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceClaim.status.allocation&lt;/code&gt; is populated.
Every incident in the runbook above was actually diagnosed here, not in a dashboard.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;DCGM metrics&lt;/strong&gt;: per-slice via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU_I_ID&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU_I_PROFILE&lt;/code&gt; (present by default); fault fields only if the
Day 1 custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;dcgm-metrics.csv&lt;/code&gt; is wired in.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Traces&lt;/strong&gt;: vLLM’s own spans for per-request latency (queue time, TTFT, prefill/decode); a separate
lifecycle tracer for cold-start latency (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;scheduled → image-pull → container-start → model-ready&lt;/code&gt;). Different
questions: request-level tracing can’t see cold-start time, it only starts once the model is serving.&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;Let’s summarize the GPU container enablement flow:&lt;/p&gt;

&lt;h3 id=&quot;device-plugin-flow&quot;&gt;Device Plugin Flow&lt;/h3&gt;

&lt;p&gt;&lt;img class=&quot;mermaid&quot; src=&quot;https://mermaid.ink/svg/eyJjb2RlIjoiZ3JhcGggVERcbkFbXCJQb2Q6IHJlc291cmNlcy5saW1pdHMubnZpZGlhLmNvbS9ncHU6IDFcIl0gLS0-fFwi4pGgIHN1Ym1pdCBwb2RcInwgQltrdWJlLWFwaXNlcnZlcl1cbiUlLVxuc3ViZ3JhcGggXCLikaEgRGV2aWNlIFBsdWdpbiBSZWdpc3RyYXRpb24gKHN0YXJ0dXAsIHJ1bnMgYmVmb3JlIHNjaGVkdWxpbmcpXCJcbkRQW1wiTlZJRElBIERldmljZSBQbHVnaW4gKERhZW1vblNldClcIl1cbkRQIC0tPnxcIuKRoSBMaXN0QW5kV2F0Y2goKTogc3RyZWFtcyBHUFUgVVVJRHMgKyBoZWFsdGhcInwgS0xba3ViZWxldF1cbktMIC0tPnxcIuKRoSBub2RlIHN0YXR1czogbnZpZGlhLmNvbS9ncHU6IDRcInwgQlxuZW5kXG4lJS1cbkIgLS0-fFwi4pGiIHVuc2NoZWR1bGVkIHBvZFwifCBTQ1trdWJlLXNjaGVkdWxlcl1cblNDIC0tPnxcIuKRoyBiaW5kIHBvZDogbm9kZSBoYXMgZW5vdWdoIG52aWRpYS5jb20vZ3B1XCJ8IEtMXG5LTCAtLT58XCLikaQgQWxsb2NhdGUoKSBnUlBDOiByZXF1ZXN0IEdQVSBVVUlEc1wifCBEUFxuRFAgLS0-fFwi4pGlIHJldHVybiBlbnZzICsgbW91bnRzICsgL2Rldi9udmlkaWEqIHNwZWNzXCJ8IEtMXG5LTCAtLT58XCLikaYgQ3JlYXRlQ29udGFpbmVyIHdpdGggZGV2aWNlIHNwZWNzXCJ8IENSW2NvbnRhaW5lcmRdXG5DUiAtLT58XCLikacgcHJlc3RhcnQgaG9va1wifCBOQ1RbXCJOVklESUEgQ29udGFpbmVyIFRvb2xraXRcXG4obnZpZGlhLWNvbnRhaW5lci1ydW50aW1lLWhvb2spXCJdXG5OQ1QgLS0-fFwi4pGoIGNvbmZpZ3VyZSBHUFUgYWNjZXNzXCJ8IEdEW1wiTW91bnQgL2Rldi9udmlkaWEqLCBudmlkaWFjdGwsIG52aWRpYS11dm1cXG5Nb3VudCBsaWJjdWRhLnNvIGZyb20gaG9zdFxcblNldCBOVklESUFfVklTSUJMRV9ERVZJQ0VTXFxuQ29uZmlndXJlIGNncm91cHMgZGV2aWNlIGFsbG93bGlzdFwiXVxuR0QgLS0-fFwi4pGpIHN0YXJ0XCJ8IENPTlRbXCJDb250YWluZXIgd2l0aCBHUFUgQWNjZXNzXFxuKENVREEgYXBwIHNlZXMgYXNzaWduZWQgR1BVcyBvbmx5KVwiXVxuJSUtXG5zdWJncmFwaCBcIkdQVSBIYXJkd2FyZSBMYXllclwiXG5IV1tcIlBoeXNpY2FsIE5WSURJQSBHUFVcIl1cbkRSVltcIm52aWRpYS5rbyBrZXJuZWwgZHJpdmVyXFxuKC9kZXYvbnZpZGlhKiBkZXZpY2UgZmlsZXMpXCJdXG5IVyAtLT4gRFJWXG5lbmRcbiUlLVxuRFJWIC0uLT58XCJleHBvc2VzIGRldmljZSBmaWxlc1wifCBOQ1RcbiUlLVxuc3R5bGUgQSBmaWxsOiNlMWY1ZmVcbnN0eWxlIENPTlQgZmlsbDojYzhlNmM5XG5zdHlsZSBEUCBmaWxsOiNmZmYzZTBcbnN0eWxlIE5DVCBmaWxsOiNmY2U0ZWNcbnN0eWxlIEdEIGZpbGw6I2ZjZTRlYyIsIm1lcm1haWQiOm51bGx9&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;dra-flow&quot;&gt;DRA Flow&lt;/h3&gt;

&lt;p&gt;&lt;img class=&quot;mermaid&quot; src=&quot;https://mermaid.ink/svg/eyJjb2RlIjoiZ3JhcGggVERcbkFbXCJSZXNvdXJjZUNsYWltVGVtcGxhdGUgKyBQb2RcXG4oZGV2aWNlcy5yZXF1ZXN0czogZ3B1Lm52aWRpYS5jb20pXCJdIC0tPnxcIuKRoCBzdWJtaXRcInwgQltrdWJlLWFwaXNlcnZlcl1cbiUlLVxuc3ViZ3JhcGggXCJEUkEgRHJpdmVyIChEYWVtb25TZXQgb24gR1BVIG5vZGUpXCJcbkNUUkxbXCJjb250cm9sbGVyXFxuUHVibGlzaGVzIFJlc291cmNlU2xpY2UgcGVyIG5vZGVcXG53YXRjaGVzIEdQVSBpbnZlbnRvcnlcIl1cbkNUUkwgLS0-fFwi4pGhIFJlc291cmNlU2xpY2U6IHV1aWQsIG1vZGVsLCBwcm9maWxlLCBtZW1vcnlcInwgQlxuR1BVUFtcImdwdS1rdWJlbGV0LXBsdWdpbiAoZXhwZXJpbWVudGFsKVxcbk5vZGVQcmVwYXJlUmVzb3VyY2VzIC9cXG5Ob2RlVW5wcmVwYXJlUmVzb3VyY2VzXCJdXG5DRFBbXCJjb21wdXRlLWRvbWFpbi1rdWJlbGV0LXBsdWdpbiAoc3VwcG9ydGVkKVxcbk9yY2hlc3RyYXRlcyBJTUVYIGRhZW1vbnNcXG5NdWx0aS1Ob2RlIE5WTGluayBkb21haW5zXCJdXG5lbmRcbiUlLVxuQiAtLT58XCLikaIgdW5zY2hlZHVsZWQgcG9kICsgdW5hbGxvY2F0ZWQgY2xhaW1cInwgU0NbXCJrdWJlLXNjaGVkdWxlciAoRFJBLWF3YXJlKVwiXVxuU0MgLS0-fFwi4pGjIHJlYWQgUmVzb3VyY2VTbGljZSwgZXZhbHVhdGUgQ0VMIHNlbGVjdG9yc1wifCBTQ1xuU0MgLS0-fFwi4pGkIHdyaXRlIGFsbG9jYXRpb24gaW50byBSZXNvdXJjZUNsYWltLnN0YXR1c1wifCBCXG5CIC0tPnxcIuKRpSBwb2QgYm91bmQgdG8gbm9kZVwifCBLTFtrdWJlbGV0XVxuS0wgLS0-fFwi4pGmIE5vZGVQcmVwYXJlUmVzb3VyY2VzKGNsYWltVUlEKVwifCBHUFVQXG5HUFVQIC0tPnxcIuKRpyB3cml0ZSBDREkgc3BlYyBmb3IgYWxsb2NhdGVkIEdQVSAvIE1JRyBzbGljZVwifCBDRElbXCIvdmFyL3J1bi9jZGkvbWFuYWdlbWVudC5udmlkaWEuY29tLWdwdS55YW1sXCJdXG5LTCAtLT58XCLikaggQ3JlYXRlQ29udGFpbmVyICsgQ0RJIGRldmljZSBuYW1lIG52aWRpYS5jb20vZ3B1PTBcInwgQ1JbXCJjb250YWluZXJkIChDREktYXdhcmUpXCJdXG5DREkgLS0-fFwi4pGpIHJlYWQgY29udGFpbmVyRWRpdHM6IGRldmljZU5vZGVzLCBtb3VudHMsIGVudiwgaG9va3NcInwgQ1JcbkNSIC0tPnxcIuKRqiBzdGFydFwifCBDT05UW1wiQ29udGFpbmVyIHdpdGggR1BVIEFjY2Vzc1xcbihzdGFuZGFyZCBydW5jLCBubyB2ZW5kb3Igd3JhcHBlcilcIl1cbiUlLVxuc3ViZ3JhcGggXCJHUFUgSGFyZHdhcmUgTGF5ZXJcIlxuSFdbXCJQaHlzaWNhbCBOVklESUEgR1BVXCJdXG5EUlZbXCJudmlkaWEua28ga2VybmVsIGRyaXZlclxcbigvZGV2L252aWRpYSogZGV2aWNlIGZpbGVzKVwiXVxuSFcgLS0-IERSVlxuZW5kXG4lJS1cbkRSViAtLi0-fFwiZGV2aWNlIGludmVudG9yeVwifCBDVFJMXG4lJS1cbnN0eWxlIEEgZmlsbDojZTFmNWZlXG5zdHlsZSBDT05UIGZpbGw6I2M4ZTZjOVxuc3R5bGUgR1BVUCBmaWxsOiNmZmYzZTBcbnN0eWxlIENEUCBmaWxsOiNmZmYzZTBcbnN0eWxlIENUUkwgZmlsbDojZmZmM2UwXG5zdHlsZSBDUiBmaWxsOiNmY2U0ZWNcbnN0eWxlIENESSBmaWxsOiNmY2U0ZWMiLCJtZXJtYWlkIjpudWxsfQ&quot; /&gt;&lt;/p&gt;

&lt;h3 id=&quot;key-components&quot;&gt;Key Components&lt;/h3&gt;

&lt;h4 id=&quot;gpu-device-plugin-traditional-path&quot;&gt;GPU Device Plugin (Traditional Path)&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;Discovers GPU resources on the node and advertises them to Kubernetes via the gRPC &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListAndWatch&lt;/code&gt; API.&lt;/li&gt;
  &lt;li&gt;Runs as a DaemonSet and manages GPU allocation to pods.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;nvidia-dra-driver-modern-path-kubernetes-sigsdra-driver-nvidia-gpu&quot;&gt;NVIDIA DRA Driver (Modern Path): &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;kubernetes-sigs/dra-driver-nvidia-gpu&lt;/code&gt;&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;Publishes structured &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; objects describing each GPU’s attributes (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu.nvidia.com&lt;/code&gt;) and MIG slices (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig.nvidia.com&lt;/code&gt;).&lt;/li&gt;
  &lt;li&gt;Implements &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NodePrepareResources&lt;/code&gt; so kubelet can activate allocated devices via CDI.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpu-kubelet-plugin&lt;/code&gt; (experimental) handles CEL-based GPU/MIG selection and lifecycle management.&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;compute-domain-kubelet-plugin&lt;/code&gt; (supported) orchestrates Multi-Node NVLink / ComputeDomain.&lt;/li&gt;
  &lt;li&gt;Requires Kubernetes 1.32+ with the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DynamicResourceAllocation&lt;/code&gt; feature gate enabled.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;kubelet&quot;&gt;Kubelet&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;The node agent that manages pod lifecycle.&lt;/li&gt;
  &lt;li&gt;Talks to device plugins (traditional path) or DRA driver plugins (DRA path), and to the container runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;container-runtime-containerd&quot;&gt;Container Runtime (containerd)&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;Creates containers and integrates with the NVIDIA Container Toolkit or CDI.&lt;/li&gt;
  &lt;li&gt;Mounts GPU devices into containers.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;nvidia-container-toolkit--cdi&quot;&gt;NVIDIA Container Toolkit / CDI&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;The runtime hook that provides GPU container creation on the legacy path.&lt;/li&gt;
  &lt;li&gt;CDI is the modern, vendor-neutral alternative: declarative YAML specs written to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/cdi/&lt;/code&gt; (static,
admin-generated) or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/var/run/cdi/&lt;/code&gt; (dynamic, generated by the DRA driver at runtime).&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;gpu-hardware-layer&quot;&gt;GPU Hardware Layer&lt;/h4&gt;
&lt;ul&gt;
  &lt;li&gt;The physical NVIDIA GPUs and the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.ko&lt;/code&gt; kernel driver underneath everything above: every other
component in this list exists to get a pod safely down to this layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;This wraps up the series: &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html&quot;&gt;Part 1&lt;/a&gt;&lt;/strong&gt; (provisioning), &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html&quot;&gt;Part 2&lt;/a&gt;&lt;/strong&gt; (sharing), and this
post (CDI, DRA, and operations) together trace the full path from silicon to a running CUDA workload on
Kubernetes, and what it takes to keep it running.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;This series traces one path end to end&lt;/strong&gt;: Part 1’s device-plugin flow gives a pod exclusive access to a whole
GPU, Part 2’s five strategies split that GPU further, and the CDI/DRA infrastructure covered in this part (see
Key Components above) is what both of those layers actually run on under the hood.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;The MIG Manager and DRA kubelet-plugin only react to events, not live hardware state&lt;/strong&gt;, so a MIG layout
changed outside Kubernetes (or a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ResourceSlice&lt;/code&gt; gone stale after a reconfiguration) needs a manual nudge to
reconcile.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;A GPU fleet SLO needs four categories&lt;/strong&gt;: hardware health (XID/ECC/thermal via DCGM), scheduling and
allocation latency, control-plane reconciliation correctness, and utilization/efficiency, the last being the
one existing tooling surfaces worst.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;</content><author><name></name></author><category term="CUDA" /><category term="GPU" /><category term="NVIDIA" /><summary type="html">Part 3 of a 3-part series on how Kubernetes makes GPUs accessible to containers, this final part covers the Container Device Interface, Dynamic Resource Allocation, and running GPUs reliably in production.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hrishi.dev/assets/gpu-part3-cdi-dra-pipeline.png" /><media:content medium="image" url="https://hrishi.dev/assets/gpu-part3-cdi-dra-pipeline.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GPU from Silicon to Container, Part 2: GPU Sharing Strategies in Kubernetes</title><link href="https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html" rel="alternate" type="text/html" title="GPU from Silicon to Container, Part 2: GPU Sharing Strategies in Kubernetes" /><published>2025-10-24T01:10:10-05:00</published><updated>2025-10-24T01:10:10-05:00</updated><id>https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2</id><content type="html" xml:base="https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html">&lt;p&gt;&lt;em&gt;Part 2 of a 3-part series on how Kubernetes makes GPUs accessible to containers. This part covers splitting a
single physical GPU across multiple workloads. Start with &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html&quot;&gt;Part 1&lt;/a&gt; if you haven’t yet.&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;In &lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html&quot;&gt;Part 1&lt;/a&gt;&lt;/strong&gt; we traced a GPU from silicon to a scheduled pod via the device-plugin path, with each pod
getting exclusive use of a whole GPU. Default Kubernetes scheduling assigns GPUs as atomic units, and most
workloads don’t come close to using a whole card. This part covers the options for splitting one: time-slicing,
MPS, MIG, HAMi, and vGPU, and what each trades away to do it.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/gpu-part2-sharing-strategies.png&quot; alt=&quot;A comparison of five GPU sharing strategies in Kubernetes: Time-Slicing, MPS, MIG, HAMi, and vGPU&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Sharing&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#gpu-sharing-options&quot;&gt;GPU Sharing Options&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;gpu-device-sharing&quot;&gt;GPU Device Sharing&lt;/h2&gt;

&lt;p&gt;When a lightweight container requests a GPU, it monopolizes the entire device regardless of actual compute or
memory utilization. A typical inference service uses only a fraction of a GPU’s compute and a slice of its
VRAM, leaving the rest idle; that’s the gap this section closes. Effective GPU utilization is core to operating a
fleet of GPUs for inference, serving, training, or HPC workloads.&lt;/p&gt;

&lt;h3 id=&quot;gpu-sharing-options&quot;&gt;GPU Sharing Options&lt;/h3&gt;

&lt;h4 id=&quot;1-gpu-time-slicing&quot;&gt;1. GPU Time-Slicing&lt;/h4&gt;
&lt;p&gt;For workloads that don’t require full GPU utilization, time-slicing allows multiple containers to share a single GPU.&lt;/p&gt;

&lt;h5 id=&quot;device-plugin-configmap&quot;&gt;Device Plugin ConfigMap&lt;/h5&gt;

&lt;p&gt;This ConfigMap configures the NVIDIA device plugin, which implements Kubernetes’ &lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/&quot;&gt;device plugin
framework&lt;/a&gt; for
advertising specialized hardware to the kubelet:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;ConfigMap&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia-device-plugin-config&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;kube-system&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;data&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;s&quot;&gt;config.yaml&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;|&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;version: v1&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;sharing:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;timeSlicing:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;replicas: 4&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;renameByDefault: false&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;failRequestsGreaterThanOne: true&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;resources:&lt;/span&gt;
      &lt;span class=&quot;s&quot;&gt;- name: nvidia.com/gpu&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;devices: all&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;All containers:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;See the same GPU device&lt;/li&gt;
  &lt;li&gt;Create separate CUDA contexts&lt;/li&gt;
  &lt;li&gt;GPU hardware time-multiplexes between contexts&lt;/li&gt;
  &lt;li&gt;No memory isolation (pods can see each other’s allocations!)&lt;/li&gt;
&lt;/ol&gt;

&lt;h5 id=&quot;why-time-slicing-oversells-whats-happening&quot;&gt;Why “Time-Slicing” Oversells What’s Happening&lt;/h5&gt;

&lt;p&gt;Time-slicing doesn’t divide the GPU: it still runs exactly one context at a time, just in very short turns, which
makes it look like concurrent sharing from the outside.&lt;/p&gt;

&lt;p&gt;Time-slicing shares a GPU by using hardware context switches, but because saving and restoring massive tensor footprints, registers, and cache states overhead is high, performance degrades significantly under heavy use. It works well for bursty, spiky inference or development workloads, but fails on saturated training jobs. It’s a Kubernetes-level configuration for the NVIDIA device plugin / GPU Operator, not a GPU driver setting, so there’s no enforcement from the GPU, OS, or CUDA runtime, and a tenant submitting back-to-back work can hold the card continuously while neighboring tasks stall.&lt;/p&gt;

&lt;p&gt;In Kubernetes, setting timeSlicing.replicas simply tricks the control plane into advertising a single GPU as multiple schedulable units, leaving pods to battle for access at the silicon level. Crucially, memory is neither metered nor isolated: all pods draw from a single shared pool. As a result, one pod’s memory leak or over-allocation routinely triggers cascading Out-of-Memory (OOM) crashes for whichever unrelated pod happens to request memory next.&lt;/p&gt;

&lt;h5 id=&quot;nvidia-smi-has-no-time-slicing-toggle&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi&lt;/code&gt; Has No Time-Slicing Toggle&lt;/h5&gt;

&lt;p&gt;Time-slicing isn’t something you turn on; it’s the default behavior whenever multiple processes hold CUDA contexts on a GPU without MIG or MPS, so there’s nothing to configure at the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi&lt;/code&gt; layer. What it &lt;em&gt;can&lt;/em&gt; control is adjacent, not equivalent: compute mode, which governs whether multiple contexts are even allowed to coexist:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;nvidia-smi &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; DEFAULT            &lt;span class=&quot;c&quot;&gt;# multiple processes can share (default)&lt;/span&gt;
nvidia-smi &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; EXCLUSIVE_PROCESS  &lt;span class=&quot;c&quot;&gt;# only one process at a time&lt;/span&gt;
nvidia-smi &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-c&lt;/span&gt; PROHIBITED         &lt;span class=&quot;c&quot;&gt;# no compute processes allowed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Time-Slicing Summary&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Broad compatibility:&lt;/strong&gt; works on any GPU architecture: a ConfigMap change, no MIG hardware, no driver reboot.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Good for bursty workloads:&lt;/strong&gt; reclaims idle GPU cycles between spiky inference or dev requests.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No isolation:&lt;/strong&gt; no memory or QoS boundaries, so compute-heavy pods can starve neighbors.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Cascading OOM:&lt;/strong&gt; one pod’s leak crashes whichever pod happens to allocate next, not necessarily the culprit.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4 id=&quot;2-nvidia-mps-multi-process-service&quot;&gt;2. NVIDIA MPS (Multi-Process Service)&lt;/h4&gt;

&lt;p&gt;Time-slicing gives each process the whole GPU in turn; MPS instead lets multiple processes run &lt;em&gt;on&lt;/em&gt; the GPU
concurrently, through a shared context, as described in &lt;a href=&quot;https://docs.nvidia.com/deploy/mps/index.html&quot;&gt;NVIDIA’s MPS documentation&lt;/a&gt;.
A daemon (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-cuda-mps-control&lt;/code&gt;) sits between the processes and the
driver and merges their kernel submissions into a single context, so the GPU can interleave work from different
containers within the same execution window instead of context-switching between fully separate ones.&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Without MPS                          With MPS
Process A ─┐                         Process A ─┐
Process B ─┼→ separate contexts      Process B ─┼→ MPS daemon → one shared context → GPU
Process C ─┘   (time-sliced)         Process C ─┘   (concurrent kernel execution)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Practically, this matters for many small, low-occupancy kernels (MPI-style multi-process workloads, or several
lightweight inference processes) that don’t individually saturate the SMs. Time-slicing would serialize them and
waste the idle capacity each one leaves on the table; MPS packs their kernels onto the GPU together instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it doesn’t give you:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;No memory isolation.&lt;/strong&gt; All clients share the daemon’s context, so one process can allocate its way into
starving the others, the same failure mode as time-slicing on that front.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;A single misbehaving client can take down the daemon&lt;/strong&gt;, which takes every other client sharing it down too. In
Kubernetes terms, that turns one pod’s crash into an outage for every other pod scheduled onto the same MPS set.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;No per-client fault isolation.&lt;/strong&gt; MPS clients are cooperating processes under one shared context, not
walled-off instances.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On Kubernetes, the device plugin’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;sharing.mps&lt;/code&gt; mode (the sibling config to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;timeSlicing&lt;/code&gt; shown earlier) is what
wires this up: it starts the control daemon on the node and hands each pod a client ID instead of a full GPU.
It’s a reasonable middle ground when workloads are trusted and cooperative but still need real concurrent
execution rather than turn-taking. HAMi’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libvgpu.so&lt;/code&gt; approach (below) targets the same “trusted but resource-
starved” niche with per-pod metering instead of a shared context, which is why the two are worth comparing rather
than treating MPS as strictly better or worse.&lt;/p&gt;

&lt;h4 id=&quot;3-multi-instance-gpu-mig&quot;&gt;3. Multi-Instance GPU (MIG)&lt;/h4&gt;

&lt;p&gt;This isn’t a scheduler trick: the GPU’s memory controllers and streaming multiprocessors
(SMs) are physically fenced off per instance, so one tenant’s workload literally cannot see or starve another’s.&lt;/p&gt;

&lt;p&gt;MIG requires certain &lt;a href=&quot;https://docs.nvidia.com/datacenter/tesla/mig-user-guide/supported-mig-profiles.html&quot;&gt;generation silicon&lt;/a&gt; or newer; &lt;strong&gt;A100 was the first GPU to support it&lt;/strong&gt;, and it carries forward
on A30, H100, H200, and Blackwell-class data-center cards.&lt;/p&gt;

&lt;h5 id=&quot;mig-architecture&quot;&gt;MIG Architecture&lt;/h5&gt;

&lt;p&gt;MIG partitioning, as documented in &lt;a href=&quot;https://docs.nvidia.com/datacenter/tesla/mig-user-guide/index.html&quot;&gt;NVIDIA’s MIG User Guide&lt;/a&gt;,
happens in two layers, and it’s worth keeping them distinct because the CLI and the Kubernetes
device plugin both expose this split:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;GPU Instance (GI):&lt;/strong&gt; carves off a fixed slab of VRAM with its own memory controllers. This is the hardware
boundary; two GIs cannot see each other’s memory even if the host is compromised.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Compute Instance (CI):&lt;/strong&gt; carves dedicated SMs &lt;em&gt;inside&lt;/em&gt; a GI for execution. A CI can’t exist without a parent
GI, and the CLI enforces that ordering: you destroy CIs before you can destroy their GI.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single A100 (40GB) can be sliced into up to 7 instances:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Physical A100 (40GB)
├─ MIG Instance 0: 3g.20gb (3 compute slices, 20GB memory)
├─ MIG Instance 1: 3g.20gb (3 compute slices, 20GB memory)
├─ MIG Instance 2: 2g.10gb (2 compute slices, 10GB memory)
└─ MIG Instance 3: 1g.5gb  (1 compute slice, 5GB memory)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Each MIG instance:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Has dedicated compute resources (streaming multiprocessors)&lt;/li&gt;
  &lt;li&gt;Has dedicated memory partition&lt;/li&gt;
  &lt;li&gt;Provides hardware-level isolation&lt;/li&gt;
  &lt;li&gt;Appears as a separate GPU device&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;mig-device-management&quot;&gt;MIG Device Management&lt;/h5&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Enable MIG mode&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-mig&lt;/span&gt; 1
All &lt;span class=&quot;k&quot;&gt;done&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# List All Profile&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi mig &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-lgip&lt;/span&gt;
+-------------------------------------------------------------------------------+
| GPU instance profiles:                                                        |
| GPU   Name               ID    Instances   Memory     P2P    SM    DEC   ENC  |
|                                Free/Total   GiB              CE    JPEG  OFA  |
|&lt;span class=&quot;o&quot;&gt;===============================================================================&lt;/span&gt;|
|   0  MIG 1g.10gb         19     7/7        9.75       No     16     1     0   |
|                                                               1     1     0   |
+-------------------------------------------------------------------------------+
|   0  MIG 1g.10gb+me      20     1/1        9.75       No     16     1     0   |
|                                                               1     1     1   |
+-------------------------------------------------------------------------------+
|   0  MIG 1g.20gb         15     4/4        19.62      No     26     1     0   |
|                                                               1     1     0   |
+-------------------------------------------------------------------------------+
|   0  MIG 2g.20gb         14     3/3        19.62      No     32     2     0   |
|                                                               2     2     0   |
+-------------------------------------------------------------------------------+
|   0  MIG 3g.40gb          9     2/2        39.50      No     60     3     0   |
|                                                               3     3     0   |
+-------------------------------------------------------------------------------+
|   0  MIG 4g.40gb          5     1/1        39.50      No     64     4     0   |
|                                                               4     4     0   |
+-------------------------------------------------------------------------------+
|   0  MIG 7g.80gb          0     1/1        79.25      No     132    7     0   |
|                                                               8     7     1   |
+-------------------------------------------------------------------------------+


&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Create GPU Instances (GI): using profile IDs from your -lgip output. Example: one 3g.40gb (ID 9) and one 2g.20gb (ID 14):&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi mig &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-cgi&lt;/span&gt; 9,14
Successfully created GPU instance ID  2 on GPU  0 using profile MIG 3g.40gb &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;ID  9&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
Successfully created GPU instance ID  3 on GPU  0 using profile MIG 2g.20gb &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;ID 14&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# List GIs to confirm IDs&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi mig &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-lgi&lt;/span&gt;
+---------------------------------------------------------+
| GPU instances:                                          |
| GPU   Name               Profile  Instance   Placement  |
|                            ID       ID       Start:Size |
|&lt;span class=&quot;o&quot;&gt;=========================================================&lt;/span&gt;|
|   0  MIG 2g.20gb           14        3          0:2     |
+---------------------------------------------------------+
|   0  MIG 3g.40gb            9        2          4:4     |
+---------------------------------------------------------+

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;c&quot;&gt;# Create Compute Instances (CI) inside each GI: using the default profile that consumes the full GI&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi mig &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-gi&lt;/span&gt; 2 &lt;span class=&quot;nt&quot;&gt;-cci&lt;/span&gt;
Successfully created compute instance ID  0 on GPU  0 GPU instance ID  2 using profile MIG 3g.40gb &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;ID  2&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi mig &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-gi&lt;/span&gt; 3 &lt;span class=&quot;nt&quot;&gt;-cci&lt;/span&gt;
Successfully created compute instance ID  0 on GPU  0 GPU instance ID  3 using profile MIG 2g.20gb &lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;ID  1&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;
&lt;span class=&quot;c&quot;&gt;# List CIs to confirm&lt;/span&gt;

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;sudo &lt;/span&gt;nvidia-smi mig &lt;span class=&quot;nt&quot;&gt;-i&lt;/span&gt; 0 &lt;span class=&quot;nt&quot;&gt;-lci&lt;/span&gt;
+--------------------------------------------------------------------+
| Compute instances:                                                 |
| GPU     GPU       Name             Profile   Instance   Placement  |
|       Instance                       ID        ID       Start:Size |
|         ID                                                         |
|&lt;span class=&quot;o&quot;&gt;====================================================================&lt;/span&gt;|
|   0      3       MIG 2g.20gb          1         0          0:2     |
+--------------------------------------------------------------------+
|   0      2       MIG 3g.40gb          2         0          0:4     |
+--------------------------------------------------------------------+

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# New device files appear&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;ls&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-l&lt;/span&gt; /dev/nvidia&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
  crw-rw-rw- 1 root root 195, 254 Aug  9 09:11 /dev/nvidia-modeset
  crw-rw-rw- 1 root root 236, 255 Aug  9 09:11 /dev/nvidia-nvswitchctl
  crw-rw-rw- 1 root root 511,   0 Aug  9 09:11 /dev/nvidia-uvm
  crw-rw-rw- 1 root root 511,   1 Aug  9 09:11 /dev/nvidia-uvm-tools
  crw-rw-rw- 1 root root 195,   0 Aug  9 09:11 /dev/nvidia0
  crw-rw-rw- 1 root root 195, 255 Aug  9 09:11 /dev/nvidiactl

  /dev/nvidia-caps:
  total 0
  cr-------- 1 root root 238,  1 Aug  9 09:11 nvidia-cap1
  cr--r--r-- 1 root root 238, 12 Aug  9 10:43 nvidia-cap12
  cr--r--r-- 1 root root 238, 13 Aug  9 10:44 nvidia-cap13
  cr--r--r-- 1 root root 238,  2 Aug  9 09:11 nvidia-cap2
  cr--r--r-- 1 root root 238, 21 Aug  9 10:42 nvidia-cap21
  cr--r--r-- 1 root root 238, 22 Aug  9 10:45 nvidia-cap22
  cr--r--r-- 1 root root 238,  3 Aug  9 11:55 nvidia-cap3
  cr--r--r-- 1 root root 238, 30 Aug  9 12:02 nvidia-cap30
  cr--r--r-- 1 root root 238, 31 Aug  9 12:06 nvidia-cap31
  cr--r--r-- 1 root root 238,  4 Aug  9 11:55 nvidia-cap4

&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;for &lt;/span&gt;f &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt; /proc/driver/nvidia/capabilities/gpu0/mig/gi&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;/ci&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;/access&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;do
  &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;gi&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-oP&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;gi\K[0-9]+&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;ci&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;grep&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;-oP&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;ci\K[0-9]+&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nv&quot;&gt;minor&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;$(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;grep &lt;/span&gt;DeviceFileMinor &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt; | &lt;span class=&quot;nb&quot;&gt;awk&lt;/span&gt; &lt;span class=&quot;s1&quot;&gt;&apos;{print $2}&apos;&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;)&lt;/span&gt;
  &lt;span class=&quot;nb&quot;&gt;echo&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;GI &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$gi&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; / CI &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$ci&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt; -&amp;gt; nvidia-cap&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;$minor&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;
&lt;span class=&quot;k&quot;&gt;done

&lt;/span&gt;GI 2 / CI 0 -&amp;gt; nvidia-cap22
GI 3 / CI 0 -&amp;gt; nvidia-cap31
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;mig-in-kubernetes&quot;&gt;MIG in Kubernetes&lt;/h5&gt;
&lt;p&gt;The NVIDIA Device Plugin discovers MIG instances and advertises them as separate resources:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Node&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;status&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;capacity&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/mig-3g.20gb&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;2&quot;&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/mig-1g.5gb&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;1&quot;&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;allocatable&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/mig-3g.20gb&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;2&quot;&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/mig-1g.5gb&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;1&quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;#Pods can request specific MIG profiles:&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;mig-pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cuda-app&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia/cuda:11.8.0-base-ubuntu22.04&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;nvidia.com/mig-3g.20gb&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Request one 3g.20gb instance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;mig-benefits&quot;&gt;MIG Benefits&lt;/h5&gt;

&lt;p&gt;The isolation here is real because it’s physical, not scheduled: each GPU Instance owns its own slice of memory
controllers, L2 cache, and DRAM, and each Compute Instance runs on SMs the GPU firmware has fenced off from every
other instance’s SMs. Contrast that with time-slicing and MPS above, where “isolation” really means a shared pool
that trusts every co-tenant to behave.&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;True hardware isolation.&lt;/strong&gt; A workload’s memory and SMs simply don’t exist from another instance’s point of
view; there’s no OOM contagion the way there is with time-slicing’s shared pool or MPS’s shared context.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Guaranteed compute and memory bandwidth.&lt;/strong&gt; An instance’s SM and memory-controller allocation is fixed at
creation time, so a neighbor running a compute-bound kernel can’t steal cycles the way it can under time-slicing.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fault isolation.&lt;/strong&gt; An uncorrectable ECC error or Xid fault inside one GI is contained to that instance; it
doesn’t take down the whole GPU or any other tenant’s CI, unlike an MPS daemon crash, which kills every client
attached to it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;QoS guarantees.&lt;/strong&gt; Because the split is enforced by hardware, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.20gb&lt;/code&gt; always means exactly that much SM and
memory for the life of the instance, not a statistical average across however many neighbors happen to be
scheduled at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;mig-trade-offs&quot;&gt;MIG Trade-offs&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Only whole, predefined profiles.&lt;/strong&gt; The GPU is partitioned into whatever profiles the hardware supports
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1g.5gb&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3g.20gb&lt;/code&gt;, …): you can’t ask for an arbitrary split, only the combinations NVIDIA has defined for
that silicon generation, and not every profile combination can coexist on the same GPU at once (placement
matters; see the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Start:Size&lt;/code&gt; column in the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-lgi&lt;/code&gt; output above).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Reconfiguration takes the GPU offline.&lt;/strong&gt; Reshaping the layout means destroying and recreating instances,
which briefly interrupts every workload on that card, not just the one being resized. This is why the GPU
Operator’s MIG Manager drains a node before it touches the layout (see below).&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Hardware-gated.&lt;/strong&gt; Only NVIDIA’s data-center-class silicon supports it (A100, H100, and newer; no consumer
or older data-center cards), so unlike time-slicing it isn’t something you can reach for on arbitrary hardware.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Fixed-size fit, not elastic.&lt;/strong&gt; Profiles suit steady-state, predictable workloads (multi-tenant inference at
a known QPS) far better than bursty or variable-sized jobs, which either waste part of a profile they don’t
fully use or don’t fit any profile at all.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;CIs must be torn down before their GI.&lt;/strong&gt; The CLI enforces destroy-children-before-parent ordering, so
shrinking one tenant’s slice to grow another’s is a multi-step, workload-interrupting operation, not a live resize.&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;who-actually-stands-mig-up&quot;&gt;Who Actually Stands MIG Up&lt;/h5&gt;

&lt;p&gt;None of the above happens by hand-running &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi&lt;/code&gt; on every node in a real cluster. In practice the &lt;strong&gt;NVIDIA
GPU Operator&lt;/strong&gt; owns the whole stack (kernel driver, container toolkit, and device plugin) as a set of
DaemonSets, and a companion &lt;strong&gt;MIG Manager&lt;/strong&gt; DaemonSet watches a node label to decide the partition layout:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Point every GPU on the node at a uniform 1g.24gb-style layout&lt;/span&gt;
kubectl label node &amp;lt;node-name&amp;gt; nvidia.com/mig.config&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;all-1g.24gb &lt;span class=&quot;nt&quot;&gt;--overwrite&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Revert to whole, unpartitioned GPUs&lt;/span&gt;
kubectl label node &amp;lt;node-name&amp;gt; nvidia.com/mig.config&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;all-disabled &lt;span class=&quot;nt&quot;&gt;--overwrite&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The manager reacts to that label by draining workloads, resetting the GPU, and re-carving instances; no manual
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi mig -cgi&lt;/code&gt; sequence required once it’s running. Mixed geometries (say, one &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2g.48gb&lt;/code&gt; slice alongside
two &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1g.24gb&lt;/code&gt; slices on the same card) are still possible, just via a custom &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mig-parted&lt;/code&gt; ConfigMap instead of the
canned label values.&lt;/p&gt;

&lt;p&gt;Standing up a MIG-enabled node this way has its own set of sharp edges; see
&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html#gpu-operator-troubleshooting&quot;&gt;GPU Operator Troubleshooting&lt;/a&gt; in Part 3’s Operations section for the failure modes worth
budgeting time for.&lt;/p&gt;

&lt;h4 id=&quot;4-hami-fractional-gpus-without-repartitioning-hardware&quot;&gt;4. HAMi: Fractional GPUs Without Repartitioning Hardware&lt;/h4&gt;

&lt;p&gt;MIG solves fragmentation by cutting the GPU into fixed, hardware-defined shapes. That’s great for predictable
multi-tenant inference, but it has a rigidity problem: a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1g.24gb&lt;/code&gt; slice is a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1g.24gb&lt;/code&gt; slice, and reshaping the
layout means draining pods and resetting the card. Time-slicing goes the other direction: infinitely flexible,
zero isolation. &lt;strong&gt;&lt;a href=&quot;https://github.com/Project-HAMi/HAMi&quot;&gt;HAMi&lt;/a&gt;&lt;/strong&gt; (Heterogeneous AI Computing Virtualization, a CNCF Incubating project) sits between the
two: it keeps the GPU whole at the hardware level and instead adds a software layer that meters memory and
compute &lt;em&gt;per pod&lt;/em&gt;, changeable purely through the pod spec.&lt;/p&gt;

&lt;h5 id=&quot;how-the-sharing-model-actually-works&quot;&gt;How the sharing model actually works&lt;/h5&gt;

&lt;p&gt;Where the device plugin flow in &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html#kubernetes-gpu-scheduling&quot;&gt;Kubernetes GPU Scheduling&lt;/a&gt; (Part 1) only ever hands out whole GPU UUIDs,
HAMi inserts itself at four points in that flow:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Pod requests nvidia.com/gpu + gpumem + gpucores
              ↓
Mutating webhook reroutes the pod to the HAMi scheduler extender
              ↓
HAMi scheduler checks aggregate count / memory / compute budgets
across the cluster before binding (not just device count)
              ↓
HAMi device plugin allocates a logical slot and injects libvgpu.so
into the container via LD_PRELOAD
              ↓
libvgpu.so intercepts CUDA memory-allocation and kernel-launch calls
at runtime, enforcing the pod&apos;s memory ceiling and compute-time share
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The pod spec gains two extended resources beyond the familiar &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu&lt;/code&gt; count:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;        &lt;span class=&quot;c1&quot;&gt;# a physical GPU, possibly shared with other pods&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/gpumem&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8000&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# hard ceiling in MiB&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/gpucores&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# compute-time share, in percent&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;requests&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/gpumem&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;8000&lt;/span&gt;
    &lt;span class=&quot;s&quot;&gt;nvidia.com/gpucores&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A cluster with eight physical GPUs and a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;deviceSplitCount&lt;/code&gt; of 10 exposes 80 schedulable slots: that’s 80
&lt;em&gt;scheduling opportunities&lt;/em&gt;, not 80 GPUs or a memory multiplier. The scheduler is doing admission control against
the real 8-card, N-gigabyte budget underneath.&lt;/p&gt;

&lt;h5 id=&quot;where-this-differs-from-mig-in-ways-that-matter-operationally&quot;&gt;Where this differs from MIG in ways that matter operationally&lt;/h5&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;Isolation is enforced in userspace, not silicon.&lt;/strong&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libvgpu.so&lt;/code&gt; polices allocations at the CUDA API boundary.
A workload that goes around it (a static binary calling the driver directly, or one setting
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CUDA_DISABLE_CONTROL&lt;/code&gt;) can slip past the memory ceiling. Treat HAMi as resource governance for cooperative,
trusted workloads, not as a tenant security boundary; adversarial multi-tenancy is still MIG’s job.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu: 1&lt;/code&gt; alone means the whole card.&lt;/strong&gt; Forgetting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpumem&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpucores&lt;/code&gt; doesn’t yield some sane
default fraction; it grants exclusive access to 100% of memory and compute, silently defeating the point of
installing HAMi in the first place.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Requests must equal limits.&lt;/strong&gt; Kubernetes extended resources aren’t overcommittable, so a mismatched
request/limit pair for &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpumem&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpucores&lt;/code&gt; is rejected at admission, before the scheduler even runs.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;The failure mode is a clean OOM, not a crash.&lt;/strong&gt; When one pod’s actual usage exceeds its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;gpumem&lt;/code&gt; ceiling,
HAMi-Core returns a CUDA out-of-memory error to that process specifically; neighboring pods sharing the same
card keep running untouched. That’s the practical payoff of the interception approach: a noisy-neighbor
incident stays contained to the pod that caused it.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Two separate metrics endpoints matter for capacity planning&lt;/strong&gt;: the scheduler exposes what it has &lt;em&gt;promised&lt;/em&gt;
(aggregate allocation across the cluster), while the device plugin exposes what’s &lt;em&gt;actually being consumed&lt;/em&gt; per
container. Reading only one side hides either over-subscription risk or real headroom, depending on which one
you skip.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;HAMi is the better fit when workloads are numerous, small, and variable in size (dev notebooks, small inference
services, CI GPU jobs) where carving fixed MIG profiles would waste capacity or require constant reshaping. MIG
remains the better fit once isolation has to survive a hostile or untrusted tenant, not just a well-behaved one.&lt;/p&gt;

&lt;h4 id=&quot;5-vgpu-virtual-gpu&quot;&gt;5. vGPU (Virtual GPU)&lt;/h4&gt;

&lt;p&gt;NVIDIA vGPU technology provides software-defined GPU sharing with:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Hypervisor-level virtualization&lt;/li&gt;
  &lt;li&gt;Memory isolation between VMs&lt;/li&gt;
  &lt;li&gt;QoS policies and scheduling&lt;/li&gt;
  &lt;li&gt;Live migration support
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Hypervisor (VMware vSphere / KVM)
├─ VM 1: vGPU (4GB, 1/4 GPU compute)
├─ VM 2: vGPU (4GB, 1/4 GPU compute)
├─ VM 3: vGPU (8GB, 1/2 GPU compute)
└─ Physical GPU (16GB total)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each vGPU appears as a complete GPU to the guest OS, enabling standard CUDA applications without modification.
Can use the &lt;a href=&quot;https://github.com/kata-containers/kata-containers/blob/main/docs/use-cases/NVIDIA-GPU-passthrough-and-Kata.md&quot;&gt;Kata containers to enable vGPU&lt;/a&gt; on the Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; vGPU may require an NVIDIA vGPU license.&lt;/p&gt;

&lt;h4 id=&quot;comparison-matrix&quot;&gt;Comparison Matrix&lt;/h4&gt;

&lt;table&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Technology&lt;/th&gt;
      &lt;th&gt;Isolation&lt;/th&gt;
      &lt;th&gt;Memory&lt;/th&gt;
      &lt;th&gt;Performance&lt;/th&gt;
      &lt;th&gt;Flexibility&lt;/th&gt;
      &lt;th&gt;Use Case&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;**Full GPU**&lt;/td&gt;
      &lt;td&gt;Hardware&lt;/td&gt;
      &lt;td&gt;Dedicated&lt;/td&gt;
      &lt;td&gt;100%&lt;/td&gt;
      &lt;td&gt;Low&lt;/td&gt;
      &lt;td&gt;Training, HPC&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**Time-Slicing**&lt;/td&gt;
      &lt;td&gt;None&lt;/td&gt;
      &lt;td&gt;Shared&lt;/td&gt;
      &lt;td&gt;Variable&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;Dev/Test, Jupyter notebooks&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**MIG**&lt;/td&gt;
      &lt;td&gt;Hardware&lt;/td&gt;
      &lt;td&gt;Dedicated&lt;/td&gt;
      &lt;td&gt;Guaranteed&lt;/td&gt;
      &lt;td&gt;Medium&lt;/td&gt;
      &lt;td&gt;Inference, Multi-tenant inferencing, training&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**MPS**&lt;/td&gt;
      &lt;td&gt;None (shared context)&lt;/td&gt;
      &lt;td&gt;Shared&lt;/td&gt;
      &lt;td&gt;Concurrent, no throttling&lt;/td&gt;
      &lt;td&gt;Medium&lt;/td&gt;
      &lt;td&gt;Many small/cooperative processes&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**HAMi**&lt;/td&gt;
      &lt;td&gt;Software (userspace intercept)&lt;/td&gt;
      &lt;td&gt;Metered, not walled off&lt;/td&gt;
      &lt;td&gt;Throttled share&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;Dev/Test, small inference, CI&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;**vGPU**&lt;/td&gt;
      &lt;td&gt;Software&lt;/td&gt;
      &lt;td&gt;Isolated&lt;/td&gt;
      &lt;td&gt;Good&lt;/td&gt;
      &lt;td&gt;High&lt;/td&gt;
      &lt;td&gt;VDI, Cloud VMs&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;up-next&quot;&gt;Up Next&lt;/h2&gt;

&lt;p&gt;Time-slicing, MPS, MIG, HAMi, and vGPU all get a GPU shared today, but they sit on top of the device-plugin
model covered in Part 1. Kubernetes scheduling itself is moving to a different foundation: MIG allocation becomes
first-class there, exposed as its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;DeviceClass&lt;/code&gt; rather than a device-plugin resource name.
&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html&quot;&gt;Part 3: CDI, DRA &amp;amp; Operations&lt;/a&gt;&lt;/strong&gt;
covers the Container Device Interface that’s replacing vendor-specific runtime hooks, Dynamic Resource Allocation
as that next-generation scheduler, and what it takes to run all of this reliably in production.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;&lt;strong&gt;Time-slicing&lt;/strong&gt; is the default behavior whenever multiple processes share a GPU without MIG or MPS: every pod gets turns on the whole device, but there’s no memory isolation, so one pod’s leak can OOM whichever neighbor happens to allocate next.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;MPS&lt;/strong&gt; lets processes run concurrently through one shared CUDA context instead of taking turns, which helps many small, cooperative kernels, but a single misbehaving client can crash the shared daemon and take every other client down with it.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;MIG&lt;/strong&gt; is the only option with true hardware isolation: separate memory controllers and SMs per instance. It’s limited to A100-class and newer data-center GPUs, and only in fixed, predefined profile sizes.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;HAMi&lt;/strong&gt; meters memory and compute per pod in userspace via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libvgpu.so&lt;/code&gt;, giving flexible fractional GPUs on ordinary hardware, but the isolation is a software boundary that a determined workload can bypass.&lt;/li&gt;
    &lt;li&gt;&lt;strong&gt;vGPU&lt;/strong&gt; pushes isolation down to the hypervisor for VM-level GPU sharing, at the cost of an NVIDIA licensing requirement.&lt;/li&gt;
    &lt;li&gt;None of these five is universally “best.” The right pick depends on whether tenants are cooperative or adversarial, and whether you need guaranteed QoS or just want to reclaim idle GPU cycles.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;</content><author><name></name></author><category term="CUDA" /><category term="GPU" /><category term="NVIDIA" /><summary type="html">Part 2 of a 3-part series on how Kubernetes makes GPUs accessible to containers. This part covers splitting a single physical GPU across multiple workloads. Start with Part 1 if you haven’t yet.</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hrishi.dev/assets/gpu-part2-sharing-strategies.png" /><media:content medium="image" url="https://hrishi.dev/assets/gpu-part2-sharing-strategies.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">GPU from Silicon to Container, Part 1: GPU Provisioning in Kubernetes</title><link href="https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html" rel="alternate" type="text/html" title="GPU from Silicon to Container, Part 1: GPU Provisioning in Kubernetes" /><published>2025-10-23T01:10:10-05:00</published><updated>2025-10-23T01:10:10-05:00</updated><id>https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1</id><content type="html" xml:base="https://hrishi.dev/cuda/gpu/nvidia/2025/10/23/nvidia-cuda-gpu-on-kube-1.html">&lt;p&gt;&lt;em&gt;A deep dive into how Kubernetes makes GPUs accessible to containers, from bare metal to CUDA applications&lt;/em&gt;&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;introduction&quot;&gt;Introduction&lt;/h2&gt;

&lt;p&gt;Getting a GPU from silicon to a running CUDA container is a longer trip than a single &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu: 1&lt;/code&gt; resource
request suggests. Behind that one line, kernel drivers, container runtimes, device plugins/DRA driver, and Kubernetes
scheduling all have to agree before a workload can touch the hardware.&lt;/p&gt;

&lt;p&gt;This is a 3-part series tracing that full path, mirroring how GPUs actually get used in a cluster:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;Part 1 (this post): Provisioning&lt;/strong&gt;, from the PCIe device and kernel driver, through the NVIDIA Container
Toolkit and CUDA stack, to how Kubernetes discovers, schedules, and allocates GPUs to pods.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html&quot;&gt;Part 2: Sharing&lt;/a&gt;&lt;/strong&gt;, the isolation and multiplexing options (MIG, time-slicing, MPS, HAMi, and
vGPU) that let multiple workloads split a physical GPU, and what each one trades away to do it.&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;
    &lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html&quot;&gt;Part 3: CDI, DRA &amp;amp; Operations&lt;/a&gt;&lt;/strong&gt;, the Container Device Interface (CDI) replacing
vendor-specific runtime hooks, Dynamic Resource Allocation (DRA) as the next generation of GPU scheduling, and
what it takes to run GPUs reliably at scale.&lt;/p&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By the end of this part, you’ll be able to trace exactly what happens between a pod spec &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu: 1&lt;/code&gt; and
a container’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cudaMalloc()&lt;/code&gt; call succeeding on physical silicon.&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;/assets/gpu-part1-provisioning-pipeline.png&quot; alt=&quot;A five-stage diagram of GPU provisioning in Kubernetes: Hardware and Kernel, Container Runtime, CUDA in Container, Kubernetes Device Scheduling, and Pod Running&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;table-of-contents&quot;&gt;Table of Contents&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Basics&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#gpu--cuda-basics&quot;&gt;GPU &amp;amp; CUDA Basics&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Provisioning&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;#hardware--kernel-foundation&quot;&gt;Hardware &amp;amp; Kernel Foundation&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#container-runtime-gpu-access&quot;&gt;Container Runtime GPU Access&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#cuda-in-containers&quot;&gt;CUDA in Containers&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#kubernetes-gpu-scheduling&quot;&gt;Kubernetes GPU Scheduling&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#gpu-isolation--visibility&quot;&gt;GPU Isolation &amp;amp; Visibility&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;#complete-flow-example&quot;&gt;Complete Flow Example&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;gpu--cuda-basics&quot;&gt;GPU &amp;amp; CUDA Basics&lt;/h2&gt;

&lt;p&gt;Before descending into kernel modules and cgroups, it helps to know what’s actually running on the silicon.&lt;/p&gt;

&lt;h3 id=&quot;what-a-gpu-is&quot;&gt;What a GPU Is&lt;/h3&gt;

&lt;p&gt;A GPU is a massively parallel processor. Where a modern CPU has a handful of cores optimized for sequential,
branch-heavy logic, a GPU packs thousands of simpler cores, organized into &lt;strong&gt;Streaming Multiprocessors (SMs)&lt;/strong&gt;,
built to run the &lt;em&gt;same&lt;/em&gt; instruction across &lt;em&gt;many&lt;/em&gt; data elements at once (SIMT: Single Instruction, Multiple
Threads).&lt;/p&gt;

&lt;p&gt;Each SM has its own register file and fast on-chip shared memory; all SMs share a much larger but slower global
memory: the “GPU memory” &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-smi&lt;/code&gt; reports (e.g., 40GB on an A100).&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# ./device_info&lt;/span&gt;
Device 0: NVIDIA H100 80GB HBM3
  Compute Capability: 9.0
  SM count: 132
  CUDA cores/SM: 128
  Total CUDA cores: 16896
  Tensor cores/SM: 4
  Total Tensor cores: 528
  Total Global Memory: 85.02 GB
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h3 id=&quot;what-cuda-is&quot;&gt;What CUDA Is&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href=&quot;https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html&quot;&gt;CUDA&lt;/a&gt;&lt;/strong&gt; (Compute Unified Device Architecture) is NVIDIA’s platform for programming that hardware: a C/C++
language extension, a compiler (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvcc&lt;/code&gt;), and a runtime/driver API. It lets you write a function that runs on
the GPU (a &lt;strong&gt;kernel&lt;/strong&gt;) and launch it across thousands of threads with a few lines of host (CPU) code.&lt;/p&gt;

&lt;p&gt;CUDA matters for AI/ML because training and inference are dominated by matrix multiplies and convolutions,
exactly the massively parallel work GPUs are built for. Frameworks like PyTorch and TensorFlow compile down to
CUDA (via libraries like cuDNN and cuBLAS) to get that speedup.&lt;/p&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvcc&lt;/code&gt; splits this file into host code (compiled with a regular C++ compiler) and device code (compiled to
  PTX, then to GPU-specific machine code called SASS), bundling both into one binary.&lt;/p&gt;

&lt;p&gt;This is the kind of program a Kubernetes pod is ultimately running. Everything from here on, driver modules,
device files, container mounts, device plugins, DRA, exists to get a call like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;vectorAdd&amp;lt;&amp;lt;&amp;lt;...&amp;gt;&amp;gt;&amp;gt;()&lt;/code&gt; a
physical GPU to execute on.&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;gpu-provisioning&quot;&gt;GPU Provisioning&lt;/h2&gt;

&lt;p&gt;From the PCIe device and kernel driver, through the NVIDIA Container Toolkit and CUDA stack, to how Kubernetes
discovers, schedules, and allocates GPUs to pods.&lt;/p&gt;

&lt;h3 id=&quot;hardware--kernel-foundation&quot;&gt;Hardware &amp;amp; Kernel Foundation&lt;/h3&gt;

&lt;h4 id=&quot;physical-gpu-access&quot;&gt;Physical GPU Access&lt;/h4&gt;

&lt;p&gt;At the most fundamental level, a GPU is a PCIe device connected to the host system. The Linux kernel communicates with it through 
a sophisticated driver stack.&lt;/p&gt;

&lt;h5 id=&quot;gpu-driver-architecture&quot;&gt;GPU Driver Architecture&lt;/h5&gt;

&lt;p&gt;The NVIDIA driver (similar concepts apply to AMD and Intel) consists of several kernel modules:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;nvidia.ko              &lt;span class=&quot;c&quot;&gt;# Core driver module&lt;/span&gt;
nvidia-uvm.ko          &lt;span class=&quot;c&quot;&gt;# Unified Memory module&lt;/span&gt;
nvidia-modeset.ko      &lt;span class=&quot;c&quot;&gt;# Display mode setting&lt;/span&gt;
nvidia-drm.ko          &lt;span class=&quot;c&quot;&gt;# Direct Rendering Manager&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;modinfo&lt;/code&gt; confirms what’s actually loaded and, via its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;alias&lt;/code&gt; field, previews the major number claimed for the device files below:&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# modinfo nvidia&lt;/span&gt;
filename:       /lib/modules/6.11.0-1016-nvidia/updates/dkms/nvidia.ko.zst
import_ns:      DMA_BUF
&lt;span class=&quot;nb&quot;&gt;alias&lt;/span&gt;:          char-major-195-&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt;
description:    NVIDIA core GPU kernel module
version:        580.173.02
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;char-major-195-*&lt;/code&gt; alias is the kernel’s device-major registration: it’s why every &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia1&lt;/code&gt;,
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidiactl&lt;/code&gt;, and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia-modeset&lt;/code&gt; below shares major number &lt;strong&gt;195&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Once created, the device files are:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/dev/nvidia0           &lt;span class=&quot;c&quot;&gt;# First GPU device&lt;/span&gt;
/dev/nvidia1           &lt;span class=&quot;c&quot;&gt;# Second GPU device&lt;/span&gt;
/dev/nvidiactl         &lt;span class=&quot;c&quot;&gt;# Control device for driver management&lt;/span&gt;
/dev/nvidia-uvm        &lt;span class=&quot;c&quot;&gt;# Unified Virtual Memory device&lt;/span&gt;
/dev/nvidia-uvm-tools  &lt;span class=&quot;c&quot;&gt;# UVM debugging and profiling&lt;/span&gt;
/dev/nvidia-modeset    &lt;span class=&quot;c&quot;&gt;# Mode setting operations&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;These character devices provide the fundamental interface between userspace applications and GPU hardware.&lt;/p&gt;

&lt;h5 id=&quot;device-file-permissions&quot;&gt;Device File Permissions&lt;/h5&gt;

&lt;p&gt;Device files have specific ownership and permissions:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# ls -l /dev/nvidia*&lt;/span&gt;
crw-rw-rw- 1 root root 195,   0 Oct 23 09:00 /dev/nvidia0
crw-rw-rw- 1 root root 195,   1 Oct 23 09:00 /dev/nvidia1
crw-rw-rw- 1 root root 195, 254 Oct 23 09:00 /dev/nvidia-modeset
crw-rw-rw- 1 root root 195, 255 Oct 23 09:00 /dev/nvidiactl
crw-rw-rw- 1 root root 509,   0 Oct 23 09:00 /dev/nvidia-uvm   &lt;span class=&quot;c&quot;&gt;# major varies, see note below&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The major number for NVIDIA GPU devices (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia*&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidiactl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia-modeset&lt;/code&gt;) is &lt;strong&gt;195&lt;/strong&gt;, a fixed number
registered at driver load time. The UVM device (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia-uvm&lt;/code&gt;) is different: its major number is &lt;strong&gt;dynamically allocated&lt;/strong&gt;
by the kernel’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;misc&lt;/code&gt; subsystem and is &lt;strong&gt;not a stable value&lt;/strong&gt;: it varies across kernel versions and distributions.
The value 509 shown above is illustrative; on kernels 6.x it is commonly 511. Minor numbers (0, 1, …) identify individual GPU instances.&lt;/p&gt;

&lt;p&gt;On multi-GPU systems wired together with NVLink (e.g. DGX-class nodes), a driver load also creates
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia-nvswitchctl&lt;/code&gt;, the control device for the NVSwitch fabric that interconnects the GPUs. It uses its own
major number (unrelated to the 195 used for the GPUs themselves) and only shows up when NVSwitch hardware is present.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;container-runtime-gpu-access&quot;&gt;Container Runtime GPU Access&lt;/h3&gt;

&lt;h4 id=&quot;the-container-isolation-challenge&quot;&gt;The Container Isolation Challenge&lt;/h4&gt;

&lt;p&gt;Containers use Linux namespaces to create isolated environments. By default, a container cannot access the host’s GPU devices because:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Device namespace isolation&lt;/strong&gt;: Container has its own &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev&lt;/code&gt; filesystem&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;cgroups device controller&lt;/strong&gt;: Controls which devices a process can access&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Mount namespace&lt;/strong&gt;: Container filesystem doesn’t include host device files&lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;nvidia-container-toolkit-bridging-the-gap&quot;&gt;NVIDIA Container Toolkit: Bridging the Gap&lt;/h4&gt;

&lt;p&gt;The &lt;strong&gt;&lt;a href=&quot;https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html&quot;&gt;NVIDIA Container Toolkit&lt;/a&gt;&lt;/strong&gt; (formerly nvidia-docker2) solves this problem by modifying the container creation process.&lt;/p&gt;

&lt;h5 id=&quot;component-architecture&quot;&gt;Component Architecture&lt;/h5&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Container Runtime (Docker/containerd)
              ↓
nvidia-container-runtime (OCI-compliant runtime wrapper)
              ↓
nvidia-container-runtime-hook (Prestart hook)
              ↓
nvidia-container-cli (Performs actual GPU provisioning)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;what-gets-mounted-into-the-container&quot;&gt;What Gets Mounted Into the Container&lt;/h5&gt;

&lt;p&gt;When a container requests GPU access, the NVIDIA Container Toolkit mounts:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Device Files:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/dev/nvidia0              &lt;span class=&quot;c&quot;&gt;# GPU device&lt;/span&gt;
/dev/nvidia1              &lt;span class=&quot;c&quot;&gt;# Additional GPUs&lt;/span&gt;
/dev/nvidiactl            &lt;span class=&quot;c&quot;&gt;# Control device&lt;/span&gt;
/dev/nvidia-uvm           &lt;span class=&quot;c&quot;&gt;# Unified Memory device&lt;/span&gt;
/dev/nvidia-uvm-tools     &lt;span class=&quot;c&quot;&gt;# UVM tools&lt;/span&gt;
/dev/nvidia-modeset       &lt;span class=&quot;c&quot;&gt;# Mode setting&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Driver Libraries&lt;/strong&gt; (from host):&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/usr/lib/x86_64-linux-gnu/libnvidia-ml.so.535.104.05
/usr/lib/x86_64-linux-gnu/libcuda.so.535.104.05
/usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.535.104.05
&lt;span class=&quot;c&quot;&gt;# ... and few more&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Utilities:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;/usr/bin/nvidia-smi
/usr/bin/nvidia-debugdump
/usr/bin/nvidia-persistenced
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;cgroups-device-permissions&quot;&gt;cgroups Device Permissions&lt;/h5&gt;

&lt;p&gt;The toolkit also configures the &lt;a href=&quot;https://docs.kernel.org/admin-guide/cgroup-v1/devices.html&quot;&gt;cgroups device controller&lt;/a&gt; to allow device access:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# In the container&apos;s cgroup&lt;/span&gt;
devices.allow: c 195:&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; rwm    &lt;span class=&quot;c&quot;&gt;# Allow all NVIDIA devices (major 195)&lt;/span&gt;
devices.allow: c 195:255 rwm  &lt;span class=&quot;c&quot;&gt;# Allow nvidiactl&lt;/span&gt;
devices.allow: c &amp;lt;uvm-major&amp;gt;:&lt;span class=&quot;k&quot;&gt;*&lt;/span&gt; rwm  &lt;span class=&quot;c&quot;&gt;# Allow nvidia-uvm (major dynamically assigned by kernel)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The format &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c 195:* rwm&lt;/code&gt; means:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;c&lt;/code&gt;: Character device&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;195&lt;/code&gt;: Major number (NVIDIA devices)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;*&lt;/code&gt;: All minor numbers (all GPUs)&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;rwm&lt;/code&gt;: Read, write, and mknod permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;cuda-in-containers&quot;&gt;CUDA in Containers&lt;/h3&gt;

&lt;h4 id=&quot;understanding-the-cuda-stack&quot;&gt;Understanding the CUDA Stack&lt;/h4&gt;

&lt;p&gt;CUDA applications communicate with GPUs through a layered software stack:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Your CUDA Application (compiled with nvcc)
              ↓
CUDA Runtime API (libcudart.so)
  - cudaMalloc()
  - cudaMemcpy()
  - kernel&amp;lt;&amp;lt;&amp;lt;...&amp;gt;&amp;gt;&amp;gt;()
              ↓
CUDA Driver API (libcuda.so)
  - cuMemAlloc()
  - cuLaunchKernel()
              ↓
Kernel Driver (nvidia.ko)
              ↓
Physical GPU Hardware
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;cuda-in-a-containerized-environment&quot;&gt;CUDA in a Containerized Environment&lt;/h4&gt;

&lt;p&gt;When a user runs a CUDA application inside a container, the call stack looks like:&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[Container] CUDA Application
              ↓
[Container] libcudart.so (CUDA Runtime)
              ↓
[Mounted from Host] libcuda.so (CUDA Driver Library)
              ↓
[ioctl() system calls]
              ↓
[Mounted Device] /dev/nvidia0
              ↓
[Host Kernel] nvidia.ko driver
              ↓
[Physical Hardware] GPU
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;the-critical-driver-compatibility-requirement&quot;&gt;The Critical Driver Compatibility Requirement&lt;/h5&gt;

&lt;p&gt;&lt;strong&gt;Key Point&lt;/strong&gt;: The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcuda.so&lt;/code&gt; driver library version must match the host kernel driver version. That’s why it’s preferred
to mount the driver library from the host rather than packaging it in the container image.&lt;/p&gt;

&lt;p&gt;Example compatibility matrix:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Host Driver Version    Compatible CUDA Toolkit Versions
-------------------    --------------------------------
535.104.05            CUDA 11.0 - 12.2
525.85.12             CUDA 11.0 - 12.1
515.65.01             CUDA 11.0 - 11.8
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The CUDA toolkit in the container must be compatible with the host’s driver version, but it doesn’t need to
match exactly: newer drivers support older CUDA toolkits.&lt;/p&gt;

&lt;p&gt;In practice, images pin toolkit packages to a specific CUDA minor version rather than to the host driver. vLLM’s
Dockerfile, for instance, installs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cuda-nvcc-${CUDA_VERSION_DASH}&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cuda-cudart-${CUDA_VERSION_DASH}&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cuda-nvrtc-${CUDA_VERSION_DASH}&lt;/code&gt;,
and related &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-dev&lt;/code&gt; packages for runtime JIT compilation (FlashInfer, DeepGEMM, EP kernels), then separately resolves a
matching NCCL version with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;apt-cache madison libnccl-dev | grep &quot;+cuda${CUDA_VERSION_SHORT}&quot;&lt;/code&gt;, because NCCL packages
don’t follow the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cuda-MAJOR-MINOR&lt;/code&gt; naming convention the rest of the toolkit uses, so version pinning has to be done
by hand. The host driver only needs to be new enough to satisfy whatever &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$CUDA_VERSION&lt;/code&gt; got baked into the image this way
(see &lt;a href=&quot;https://github.com/vllm-project/vllm/blob/releases/v0.21.0/docker/Dockerfile#L586-L607&quot;&gt;vLLM’s Dockerfile, lines 586–607&lt;/a&gt;).&lt;/p&gt;

&lt;h4 id=&quot;a-simple-cuda-example&quot;&gt;A Simple CUDA Example&lt;/h4&gt;

&lt;p&gt;Here’s what happens when you run a basic CUDA program:&lt;/p&gt;
&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;cuda_runtime.h&amp;gt;
&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;size_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;sizeof&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;c1&quot;&gt;// This triggers the entire stack&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cudaError_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cudaMalloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cudaSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Successfully allocated %zu bytes on GPU&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cudaFree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Behind the scenes:&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cudaMalloc()&lt;/code&gt; calls &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cuMemAlloc()&lt;/code&gt; in &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcuda.so&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcuda.so&lt;/code&gt; opens &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia0&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Issues &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ioctl()&lt;/code&gt; system call with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NVIDIA_IOCTL_ALLOC_MEM&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;Kernel driver &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.ko&lt;/code&gt; receives the request&lt;/li&gt;
  &lt;li&gt;Driver checks cgroups: “Is this process allowed to access device 195:0?”&lt;/li&gt;
  &lt;li&gt;If allowed, driver allocates GPU memory&lt;/li&gt;
  &lt;li&gt;Returns device memory pointer to application&lt;/li&gt;
&lt;/ol&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;kubernetes-gpu-scheduling&quot;&gt;Kubernetes GPU Scheduling&lt;/h3&gt;

&lt;p&gt;Kubernetes exposes GPUs to pods in two ways: the &lt;strong&gt;&lt;a href=&quot;https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/&quot;&gt;Device Plugin&lt;/a&gt;&lt;/strong&gt; framework, stable since 1.10 and still the
recommended default for most clusters as of writing, and the newer &lt;strong&gt;Dynamic Resource Allocation (DRA)&lt;/strong&gt;, better suited to
multi-node GPU topologies and richer sharing. This layer walks the Device Plugin flow end to end; DRA gets its
own deep dive in &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html#dynamic-resource-allocation-dra-next-generation-gpu-scheduling&quot;&gt;Part 3&lt;/a&gt;.&lt;/p&gt;

&lt;h4 id=&quot;the-device-plugin-framework&quot;&gt;The Device Plugin Framework&lt;/h4&gt;

&lt;p&gt;Kubernetes uses an extensible &lt;strong&gt;Device Plugin&lt;/strong&gt; system to manage specialized hardware like GPUs, FPGAs, and InfiniBand adapters.&lt;/p&gt;

&lt;h5 id=&quot;architecture-overview&quot;&gt;Architecture Overview&lt;/h5&gt;

&lt;p&gt;These are two separate flows, driven by different triggers, worth keeping distinct rather than reading as one
straight-line pipeline. Discovery/registration runs at plugin startup (and periodically after); allocation only
runs once the scheduler has already bound a pod to the node.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Discovery &amp;amp; Registration&lt;/strong&gt;: runs at Device Plugin startup, independent of any pod:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;NVIDIA Device Plugin (DaemonSet)
  - Discovers GPUs (nvidia-smi)
  - Registers with kubelet
              ↓
kubelet (on GPU node)
  - Discovers device plugins
  - Tracks GPU allocation
              ↓
kube-apiserver (Node status: nvidia.com/gpu: 4)
              ↓
kube-scheduler (Finds nodes with requested GPUs)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;2. Allocation&lt;/strong&gt;: runs per pod, only after the scheduler has bound it to this node:&lt;/p&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kube-scheduler
  Binds pod to node with enough GPUs
              ↓
kubelet (on GPU node)
  - Calls Allocate() for the pod
              ↓
NVIDIA Device Plugin (DaemonSet)
  - Allocates specific GPUs
  - Returns envs/mounts/device specs
    (pre-CDI) or a CDI device name
    (CDI mode, v0.14+)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;What the plugin returns from &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; depends on its mode. Older versions (and newer ones running with CDI
disabled) work out the envs, mounts, and device nodes on the fly for every call, shown as the “Pre-CDI Device
Plugin” code in &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html#cdi-in-kubernetes&quot;&gt;CDI in Kubernetes&lt;/a&gt; in Part 3. NVIDIA Device Plugin v0.14+ with CDI enabled skips that
per-call computation entirely: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; just returns a CDI device name (e.g. &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu=0&lt;/code&gt;), and
containerd resolves the actual devices/mounts/env from the static spec already generated on disk; nothing is
discovered or assembled “on the go” at allocation time anymore.&lt;/p&gt;

&lt;h4 id=&quot;device-plugin-discovery-and-registration&quot;&gt;Device Plugin Discovery and Registration&lt;/h4&gt;

&lt;p&gt;The NVIDIA Device Plugin runs as a DaemonSet on every GPU node:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;DaemonSet&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia-device-plugin-daemonset&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;namespace&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;kube-system&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia-device-plugin-ds&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia-device-plugin&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvcr.io/nvidia/k8s-device-plugin:v0.14.1&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;volumeMounts&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;device-plugin&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;mountPath&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;/var/lib/kubelet/device-plugins&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;the-registration-process&quot;&gt;The Registration Process&lt;/h5&gt;

&lt;p&gt;The &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Register()&lt;/code&gt; and &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ListAndWatch()&lt;/code&gt; calls below come from the kubelet device plugin’s
&lt;a href=&quot;https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/kubelet/pkg/apis/deviceplugin/v1beta1/api.proto&quot;&gt;gRPC API definition&lt;/a&gt;, which every device plugin (not just NVIDIA’s) implements.&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;strong&gt;Device Plugin Starts&lt;/strong&gt;
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;nvidia-device-plugin container starts
           ↓
Queries GPUs: nvidia-smi --query-gpu=uuid --format=csv
           ↓
Discovers: GPU-a4f8c2d1, GPU-b3e9d4f2, GPU-c8f1a5b3, GPU-d2c7e9a4
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Registration with kubelet&lt;/strong&gt;
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Device plugin connects to: unix:///var/lib/kubelet/device-plugins/kubelet.sock
           ↓
Sends Register() gRPC call:
{
  &quot;version&quot;: &quot;v1beta1&quot;,
  &quot;endpoint&quot;: &quot;nvidia.sock&quot;,
  &quot;resourceName&quot;: &quot;nvidia.com/gpu&quot;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Advertising Resources&lt;/strong&gt;
    &lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubelet calls ListAndWatch() on device plugin
           ↓
Device plugin responds:
{
  &quot;devices&quot;: [
    {&quot;id&quot;: &quot;GPU-a4f8c2d1&quot;, &quot;health&quot;: &quot;Healthy&quot;},
    {&quot;id&quot;: &quot;GPU-b3e9d4f2&quot;, &quot;health&quot;: &quot;Healthy&quot;},
    {&quot;id&quot;: &quot;GPU-c8f1a5b3&quot;, &quot;health&quot;: &quot;Healthy&quot;},
    {&quot;id&quot;: &quot;GPU-d2c7e9a4&quot;, &quot;health&quot;: &quot;Healthy&quot;}
  ]
}
           ↓
kubelet updates node status:
status.capacity.nvidia.com/gpu: &quot;4&quot;
status.allocatable.nvidia.com/gpu: &quot;4&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;    &lt;/div&gt;
  &lt;/li&gt;
&lt;/ol&gt;

&lt;h4 id=&quot;pod-scheduling-flow&quot;&gt;Pod Scheduling Flow&lt;/h4&gt;

&lt;p&gt;Let’s trace a complete pod scheduling workflow:&lt;/p&gt;

&lt;h5 id=&quot;step-1-user-creates-pod&quot;&gt;Step 1: User Creates Pod&lt;/h5&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;gpu-pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;runtimeClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# see &quot;Wiring the Hook&quot; below for where this comes from&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cuda-container&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia/cuda:11.8.0-base-ubuntu22.04&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;nvidia-smi&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# Request 2 GPUs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;step-2-scheduler-filters-and-scores&quot;&gt;Step 2: Scheduler Filters and Scores&lt;/h5&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kube-scheduler receives unscheduled pod
         ↓
Filtering Phase:
  - node-1: cpu OK, memory OK, nvidia.com/gpu=0 ✗ (no GPUs)
  - node-2: cpu OK, memory OK, nvidia.com/gpu=2 ✓
  - node-3: cpu OK, memory OK, nvidia.com/gpu=4 ✓
  - node-4: cpu ✗ (insufficient CPU)
         ↓
Scoring Phase:
  - node-2: score 85 (2 GPUs available, high utilization)
  - node-3: score 92 (4 GPUs available, moderate utilization)
         ↓
Selected: node-3
         ↓
Binding: pod assigned to node-3
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h5 id=&quot;step-3-kubelet-allocates-gpus&quot;&gt;Step 3: kubelet Allocates GPUs&lt;/h5&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubelet on node-3 receives pod assignment
         ↓
For container &quot;cuda-container&quot; requesting 2 GPUs:
         ↓
kubelet calls: DevicePlugin.Allocate(deviceIds=[&quot;GPU-a4f8c2d1&quot;, &quot;GPU-b3e9d4f2&quot;])
         ↓
Device plugin responds:
{
  &quot;containerResponses&quot;: [{
    &quot;envs&quot;: {
      &quot;NVIDIA_VISIBLE_DEVICES&quot;: &quot;GPU-a4f8c2d1,GPU-b3e9d4f2&quot;
    },
    &quot;mounts&quot;: [{
      &quot;hostPath&quot;: &quot;/usr/lib/x86_64-linux-gnu/libcuda.so.535.104.05&quot;,
      &quot;containerPath&quot;: &quot;/usr/lib/x86_64-linux-gnu/libcuda.so.1&quot;,
      &quot;readOnly&quot;: true
    }],
    &quot;devices&quot;: [{
      &quot;hostPath&quot;: &quot;/dev/nvidia0&quot;,
      &quot;containerPath&quot;: &quot;/dev/nvidia0&quot;,
      &quot;permissions&quot;: &quot;rwm&quot;
    }, {
      &quot;hostPath&quot;: &quot;/dev/nvidia1&quot;,
      &quot;containerPath&quot;: &quot;/dev/nvidia1&quot;,
      &quot;permissions&quot;: &quot;rwm&quot;
    }]
  }]
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;If the device plugin is CDI-enabled&lt;/strong&gt; (NVIDIA device plugin ≥ v0.14 with &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;--device-list-strategy=cdi-annotations&lt;/code&gt; or &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdi-cri&lt;/code&gt;),
the &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; response looks different: instead of enumerating raw &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;devices&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;mounts&lt;/code&gt;/&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;envs&lt;/code&gt;, it returns a &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdiDevices&lt;/code&gt;
list of fully-qualified CDI device names, and lets containerd resolve each name against the CDI spec on disk:&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;containerResponses&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;envs&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;cdiDevices&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nvidia.com/gpu=GPU-a4f8c2d1&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
      &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;name&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;nvidia.com/gpu=GPU-b3e9d4f2&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The device plugin no longer needs to know host paths, library versions, or major/minor numbers at Allocate() time; it
just points at a name defined by the &lt;a href=&quot;https://github.com/cncf-tags/container-device-interface&quot;&gt;Container Device Interface (CDI) spec&lt;/a&gt;
and defers all of that resolution to whatever generated &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/etc/cdi/nvidia.yaml&lt;/code&gt;
(&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-ctk cdi generate&lt;/code&gt;, run once per driver update).&lt;/p&gt;

&lt;h5 id=&quot;step-4-container-runtime-provisions-gpu&quot;&gt;Step 4: Container Runtime Provisions GPU&lt;/h5&gt;
&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;kubelet → containerd: CreateContainer with:
  - Environment: NVIDIA_VISIBLE_DEVICES=GPU-a4f8c2d1,GPU-b3e9d4f2
  - Mounts: driver libraries
  - Devices: /dev/nvidia0, /dev/nvidia1
         ↓
containerd calls: nvidia-container-runtime-hook (prestart)
         ↓
Hook configures:
  - Mounts all required device files
  - Mounts NVIDIA libraries
  - Sets up cgroups device controller
  - Configures environment variables
         ↓
Container starts with GPU access
         ↓
nvidia-smi inside container shows 2 GPUs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;CDI variant of Step 4&lt;/strong&gt;: containerd receives the CDI device names (either via CRI’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;CDIDevices&lt;/code&gt; field, or via the
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdi.k8s.io/devices&lt;/code&gt; pod annotation on older kubelet versions) instead of a devices/mounts list. There’s no
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-container-runtime-hook&lt;/code&gt; prestart step: containerd’s built-in CDI support looks up each name in the on-disk CDI
spec directly and applies its &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;containerEdits&lt;/code&gt; (device nodes, mounts, env, hooks) to the OCI spec before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runc&lt;/code&gt; ever
runs. See &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html#post-cdi-device-plugin&quot;&gt;“Post-CDI Device Plugin”&lt;/a&gt; in Part 3 for the full Allocate() implementation
and containerd integration flow.&lt;/p&gt;

&lt;h5 id=&quot;wiring-the-hook-containerd-runtime-configuration&quot;&gt;Wiring the Hook: containerd Runtime Configuration&lt;/h5&gt;

&lt;p&gt;The chain above doesn’t run for every container by default: containerd has to be told that a runtime called
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt; exists and which binary implements it, and a pod has to explicitly ask for that runtime by name. The
NVIDIA GPU Operator’s toolkit component writes this as a drop-in config on every GPU node:&lt;/p&gt;

&lt;div class=&quot;language-toml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;py&quot;&gt;version&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;

&lt;span class=&quot;nn&quot;&gt;[plugins]&lt;/span&gt;

  &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;]&lt;/span&gt;
    &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;

  &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd]&lt;/span&gt;
      &lt;span class=&quot;py&quot;&gt;default_runtime_name&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;runc&quot;&lt;/span&gt;
      &lt;span class=&quot;py&quot;&gt;ignore_blockio_not_enabled_errors&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;
      &lt;span class=&quot;py&quot;&gt;ignore_rdt_not_enabled_errors&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;

      &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes]&lt;/span&gt;

        &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes.nvidia]&lt;/span&gt;
          &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;
          &lt;span class=&quot;nn&quot;&gt;[plugins.&quot;io.containerd.cri.v1.runtime&quot;.containerd.runtimes.nvidia.options]&lt;/span&gt;
            &lt;span class=&quot;py&quot;&gt;BinaryName&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;&quot;/usr/local/nvidia/toolkit/nvidia-container-runtime&quot;&lt;/span&gt;
            &lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Two things worth noticing:&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;strong&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;default_runtime_name = &quot;runc&quot;&lt;/code&gt;&lt;/strong&gt;: plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runc&lt;/code&gt; handles every container unless a pod opts out of it. The
hook-based flow above is additive, not a replacement for the default path.&lt;/li&gt;
  &lt;li&gt;&lt;strong&gt;Two separate &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt; runtimes are registered&lt;/strong&gt;, pointing at two different binaries: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia&lt;/code&gt; runs the classic
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-container-runtime&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-container-runtime-hook&lt;/code&gt; → &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-container-cli&lt;/code&gt; chain from the diagram
above; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia-cdi&lt;/code&gt; skips the hook entirely and resolves devices from a CDI spec instead (the mechanism CDI and
DRA build on; see &lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/25/nvidia-cuda-gpu-on-kube-3.html#the-container-device-interface-cdi-revolution&quot;&gt;Part 3&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Registering the runtime in containerd only makes it available: a pod still has to ask for it. Kubernetes exposes
that as a &lt;a href=&quot;https://kubernetes.io/docs/concepts/containers/runtime-class/&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;RuntimeClass&lt;/code&gt;&lt;/a&gt; object, which the GPU Operator also creates, mapping the cluster-facing name to the
containerd runtime handler configured above:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;node.k8s.io/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;RuntimeClass&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;handler&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia&lt;/span&gt;   &lt;span class=&quot;c1&quot;&gt;# must match a [...containerd.runtimes.&amp;lt;handler&amp;gt;] block in the containerd config&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;A pod then opts into the hook-based path by name, via &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName&lt;/code&gt;:&lt;/p&gt;

&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;apps/v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Deployment&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pytorch-training&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pytorch-training&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;selector&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;matchLabels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pytorch-training&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;template&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;labels&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;app&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pytorch-training&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;runtimeClassName&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;pytorch&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;intel/deep-learning:pytorch-gpu-2025.2.0-py3.11&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;args&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;python&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;train.py&lt;/span&gt;
          &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;--model=resnet18&lt;/span&gt;
        &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
            &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Without &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runtimeClassName: nvidia&lt;/code&gt;, this pod would run under plain &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runc&lt;/code&gt; and get no GPU access at all from
containerd’s side: everything from the mounted devices to &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcuda.so&lt;/code&gt; shown below depends on the hook actually
running, which only happens when the pod names the runtime that triggers it.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;gpu-isolation--visibility&quot;&gt;GPU Isolation &amp;amp; Visibility&lt;/h3&gt;

&lt;h4 id=&quot;the-magic-of-nvidia_visible_devices&quot;&gt;The Magic of NVIDIA_VISIBLE_DEVICES&lt;/h4&gt;

&lt;p&gt;The &lt;a href=&quot;https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/index.html&quot;&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NVIDIA_VISIBLE_DEVICES&lt;/code&gt;&lt;/a&gt; environment variable is the key to GPU isolation in containers. It controls which GPUs are visible to CUDA applications.&lt;/p&gt;

&lt;h5 id=&quot;how-it-works&quot;&gt;How It Works&lt;/h5&gt;

&lt;p&gt;Consider a host with 4 GPUs:&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# On the host&lt;/span&gt;
&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;nvidia-smi &lt;span class=&quot;nt&quot;&gt;--query-gpu&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;index,uuid &lt;span class=&quot;nt&quot;&gt;--format&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;csv
index, uuid
0, GPU-a4f8c2d1-e5f6-7a8b-9c0d-1e2f3a4b5c6d
1, GPU-b3e9d4f2-f6a7-8b9c-0d1e-2f3a4b5c6d7e
2, GPU-c8f1a5b3-a7b8-9c0d-1e2f-3a4b5c6d7e8f
3, GPU-d2c7e9a4-b8c9-0d1e-2f3a-4b5c6d7e8f9a
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Container 1 configuration:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;NVIDIA_VISIBLE_DEVICES&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;GPU-a4f8c2d1-e5f6-7a8b-9c0d-1e2f3a4b5c6d

&lt;span class=&quot;c&quot;&gt;# Inside container 1&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# nvidia-smi&lt;/span&gt;
+-----------------------------------------------------------------------------------------+
| NVIDIA-SMI 580.173.02             Driver Version: 580.173.02     CUDA Version: 13.0     |
+-----------------------------------------+------------------------+----------------------+
| GPU  Name                 Persistence-M | Bus-Id          Disp.A | Volatile Uncorr. ECC |
| Fan  Temp   Perf          Pwr:Usage/Cap |           Memory-Usage | GPU-Util  Compute M. |
|                                         |                        |               MIG M. |
|&lt;span class=&quot;o&quot;&gt;=========================================&lt;/span&gt;+&lt;span class=&quot;o&quot;&gt;========================&lt;/span&gt;+&lt;span class=&quot;o&quot;&gt;======================&lt;/span&gt;|
|   0  NVIDIA H100 80GB HBM3          Off |   00000000:8D:00.0 Off |                   On |
| N/A   31C    P0             69W /  700W |       0MiB /  81559MiB |     N/A      Default |
|                                         |                        |              Enabled |
+-----------------------------------------+------------------------+----------------------+
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Notice that:&lt;/p&gt;
&lt;ul&gt;
  &lt;li&gt;Container 1 sees only 1 GPU, renumbered as GPU 0, even though the host has 4&lt;/li&gt;
  &lt;li&gt;Setting &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NVIDIA_VISIBLE_DEVICES&lt;/code&gt; to a different UUID (or comma-separated list) gives another container a different
subset: each container gets its own isolated, independently-numbered GPU namespace&lt;/li&gt;
&lt;/ul&gt;

&lt;h5 id=&quot;driver-level-enforcement&quot;&gt;Driver-Level Enforcement&lt;/h5&gt;

&lt;p&gt;When a CUDA application initializes:&lt;/p&gt;
&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;cudaError_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cudaSetDevice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;The CUDA driver:&lt;/p&gt;
&lt;ol&gt;
  &lt;li&gt;Reads &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NVIDIA_VISIBLE_DEVICES&lt;/code&gt; environment variable&lt;/li&gt;
  &lt;li&gt;Creates a virtual-to-physical GPU mapping&lt;/li&gt;
  &lt;li&gt;Only allows access to visible devices&lt;/li&gt;
&lt;/ol&gt;

&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;cuInit&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;visible_devices&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;getenv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;NVIDIA_VISIBLE_DEVICES&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;visible_devices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;parse_and_filter_devices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;visible_devices&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;c1&quot;&gt;// User&apos;s &quot;GPU 0&quot; maps to physical GPU as specified&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h4 id=&quot;cgroups-kernel-level-protection&quot;&gt;cgroups: Kernel-Level Protection&lt;/h4&gt;

&lt;p&gt;Environment variables provide application-level isolation, but cgroups enforce it at the kernel level.&lt;/p&gt;

&lt;p&gt;For each container, cgroups device controller is configured:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Container 1:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# /sys/fs/cgroup/devices/kubepods/pod&amp;lt;uid&amp;gt;/&amp;lt;container-id&amp;gt;/devices.list&lt;/span&gt;
c 195:0 rwm      &lt;span class=&quot;c&quot;&gt;# Allow /dev/nvidia0 only&lt;/span&gt;
c 195:255 rwm    &lt;span class=&quot;c&quot;&gt;# Allow /dev/nvidiactl&lt;/span&gt;
c &amp;lt;uvm-major&amp;gt;:0 rwm  &lt;span class=&quot;c&quot;&gt;# Allow /dev/nvidia-uvm (major dynamically assigned by kernel)&lt;/span&gt;

&lt;span class=&quot;c&quot;&gt;# Implicit deny for:&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# c 195:1 (would be /dev/nvidia1)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# c 195:2 (would be /dev/nvidia2)&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# c 195:3 (would be /dev/nvidia3)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Even if a malicious process inside Container 1 tries to open &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia1&lt;/code&gt;, the kernel blocks it:&lt;/p&gt;
&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;// Malicious code attempt&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;fd&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;open&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;/dev/nvidia1&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;O_RDWR&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Returns: -1 (EPERM - Operation not permitted)&lt;/span&gt;
&lt;span class=&quot;c1&quot;&gt;// Kernel: cgroups device controller denied access&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;This provides defense-in-depth: both application-level (CUDA driver) and kernel-level (cgroups) isolation.&lt;/p&gt;

&lt;hr /&gt;

&lt;h3 id=&quot;complete-flow-example&quot;&gt;Complete Flow Example&lt;/h3&gt;

&lt;p&gt;Let’s trace a complete end-to-end flow from pod creation to CUDA memory allocation.&lt;/p&gt;

&lt;h4 id=&quot;the-scenario&quot;&gt;The Scenario&lt;/h4&gt;

&lt;p&gt;We’ll deploy a pod requesting 2 GPUs and run a simple CUDA program that allocates GPU memory.&lt;/p&gt;

&lt;h5 id=&quot;step-1-deploy-the-pod&quot;&gt;Step 1: Deploy the Pod&lt;/h5&gt;
&lt;div class=&quot;language-yaml highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;na&quot;&gt;apiVersion&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;v1&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;kind&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Pod&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;metadata&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cuda-mem-test&lt;/span&gt;
&lt;span class=&quot;na&quot;&gt;spec&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;restartPolicy&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;Never&lt;/span&gt;
  &lt;span class=&quot;na&quot;&gt;containers&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
  &lt;span class=&quot;pi&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;name&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;cuda-app&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;image&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;s&quot;&gt;nvidia/cuda:11.8.0-devel-ubuntu22.04&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;command&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;pi&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;./cuda_malloc_test&quot;&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;]&lt;/span&gt;
    &lt;span class=&quot;na&quot;&gt;resources&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
      &lt;span class=&quot;na&quot;&gt;limits&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;s&quot;&gt;nvidia.com/gpu&lt;/span&gt;&lt;span class=&quot;pi&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;m&quot;&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nv&quot;&gt;$ &lt;/span&gt;kubectl apply &lt;span class=&quot;nt&quot;&gt;-f&lt;/span&gt; cuda-pod.yaml
pod/cuda-mem-test created
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Scheduling and allocation from here follow the same flow already walked through in
&lt;a href=&quot;#pod-scheduling-flow&quot;&gt;Pod Scheduling Flow&lt;/a&gt;: the scheduler filters and scores nodes on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu&lt;/code&gt; availability
(say it lands on &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node-gpu-03&lt;/code&gt;), kubelet calls the device plugin’s &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; for GPU UUIDs &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU-uuid-1234&lt;/code&gt; and
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;GPU-uuid-5678&lt;/code&gt;, and containerd folds the resulting devices, mounts, and env (or, on a CDI-enabled setup, a
&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cdiDevices&lt;/code&gt; list) into the OCI spec before &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;runc&lt;/code&gt; starts the container. What’s different this time is what happens
&lt;em&gt;inside&lt;/em&gt; the container once it’s running: that’s the part worth tracing in detail.&lt;/p&gt;

&lt;h5 id=&quot;step-2-cuda-application-runs&quot;&gt;Step 2: CUDA Application Runs&lt;/h5&gt;

&lt;p&gt;Inside the container, our CUDA application executes:&lt;/p&gt;
&lt;div class=&quot;language-c highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;cp&quot;&gt;#include &amp;lt;stdio.h&amp;gt;
#include &amp;lt;cuda_runtime.h&amp;gt;
&lt;/span&gt;
&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;nf&quot;&gt;main&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deviceCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;cudaGetDeviceCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;deviceCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Visible GPUs: %d&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deviceCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;deviceCount&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;++&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;n&quot;&gt;cudaSetDevice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        
        &lt;span class=&quot;kt&quot;&gt;float&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;size_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1024&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;// 1 GB&lt;/span&gt;
        
        &lt;span class=&quot;n&quot;&gt;cudaError_t&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cudaMalloc&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;amp;&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;err&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;==&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;cudaSuccess&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;printf&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;GPU %d: Allocated 1 GB&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\n&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;i&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
            &lt;span class=&quot;n&quot;&gt;cudaFree&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;d_data&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;);&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The execution flow:&lt;/strong&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;Application calls: cudaGetDeviceCount(&amp;amp;deviceCount)
              ↓
CUDA Runtime (libcudart.so): cuDeviceGetCount()
              ↓
CUDA Driver (libcuda.so):
  - Reads NVIDIA_VISIBLE_DEVICES from environment
  - Parses: &apos;GPU-uuid-1234,GPU-uuid-5678&apos;
  - Returns: deviceCount = 2
              ↓
Application prints: &apos;Visible GPUs: 2&apos;
              ↓
Application calls: cudaMalloc(&amp;amp;d_data, 1GB) for GPU 0
              ↓
CUDA Runtime: cuMemAlloc(1073741824) // 1 GB in bytes
              ↓
CUDA Driver:
  - Determines physical GPU from NVIDIA_VISIBLE_DEVICES mapping
  - Virtual GPU 0 → Physical GPU-uuid-1234 → /dev/nvidia0
  - Opens file descriptor: fd = open(&apos;/dev/nvidia0&apos;, O_RDWR)
              ↓
Kernel checks cgroups:
  - Process in cgroup: /kubepods/pod-xyz/container-abc
  - Requested device: major=195, minor=0
  - cgroups device allowlist: c 195:0 rwm ✓ ALLOWED
              ↓
Kernel forwards to nvidia.ko driver
              ↓
nvidia.ko driver:
  - Allocates 1 GB of GPU memory on physical GPU
  - Programs GPU memory controller
  - Returns device memory address: 0x7f8c40000000
              ↓
CUDA Driver returns to application
              ↓
Application prints: &apos;GPU 0: Allocated 1 GB&apos;
              ↓
Repeat for GPU 1 with /dev/nvidia1
              ↓
Application prints: &apos;GPU 1: Allocated 1 GB&apos;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;System calls involved:&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;c&quot;&gt;# Traced with strace&lt;/span&gt;
openat&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;AT_FDCWD, &lt;span class=&quot;s2&quot;&gt;&quot;/dev/nvidia0&quot;&lt;/span&gt;, O_RDWR&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 3
ioctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3, NVIDIA_IOC_QUERY_DEVICE_CLASS, ...&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0
ioctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3, NVIDIA_IOC_CARD_INFO, ...&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0
ioctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3, NVIDIA_IOC_ALLOC_MEM, &lt;span class=&quot;o&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;size&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;1073741824, ...&lt;span class=&quot;o&quot;&gt;})&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0
&lt;span class=&quot;c&quot;&gt;# ... GPU memory now allocated ...&lt;/span&gt;
ioctl&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3, NVIDIA_IOC_FREE_MEM, ...&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0
close&lt;span class=&quot;o&quot;&gt;(&lt;/span&gt;3&lt;span class=&quot;o&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;up-next&quot;&gt;Up Next&lt;/h2&gt;

&lt;p&gt;This part covered how a GPU goes from silicon to a scheduled, running container via the classic device-plugin
path, with each pod getting exclusive use of a whole GPU. That’s wasteful for most workloads.
&lt;strong&gt;&lt;a href=&quot;https://hrishi.dev/cuda/gpu/nvidia/2025/10/24/nvidia-cuda-gpu-on-kube-2.html&quot;&gt;Part 2: GPU Sharing Strategies&lt;/a&gt;&lt;/strong&gt; picks up from here: time-slicing, MPS, MIG, HAMi, and vGPU, and
what each one trades away to split a physical GPU across multiple workloads.&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;&lt;strong&gt;Key Takeaways&lt;/strong&gt;&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;A GPU shows up to the kernel as a set of character devices (&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia0&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidiactl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/dev/nvidia-uvm&lt;/code&gt;, …) registered under a fixed major number (195 for the core devices; the UVM device’s major is allocated dynamically).&lt;/li&gt;
    &lt;li&gt;The NVIDIA Container Toolkit bridges the container isolation gap by mounting host device files, driver libraries, and utilities into the container at creation time, coordinated through an OCI prestart hook.&lt;/li&gt;
    &lt;li&gt;The mounted &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;libcuda.so&lt;/code&gt; driver library must come from the host and match the host’s kernel driver version; the CUDA toolkit inside the container just needs to be old enough for that driver to support it.&lt;/li&gt;
    &lt;li&gt;Kubernetes exposes GPUs to pods through the Device Plugin framework: a DaemonSet that discovers GPUs, registers a resource name like &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nvidia.com/gpu&lt;/code&gt; with kubelet, and hands out specific GPU UUIDs when the scheduler binds a pod to a node.&lt;/li&gt;
    &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;NVIDIA_VISIBLE_DEVICES&lt;/code&gt; plus the cgroups device controller provide two independent layers of isolation: one at the CUDA driver level, one enforced by the kernel, so a container can only ever see and open the GPUs it was allocated.&lt;/li&gt;
    &lt;li&gt;CDI-enabled device plugins (v0.14+) skip the per-call hook chain entirely: &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Allocate()&lt;/code&gt; just returns a CDI device name, and containerd resolves devices, mounts, and env from a static spec generated once on disk.&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;</content><author><name></name></author><category term="CUDA" /><category term="GPU" /><category term="NVIDIA" /><summary type="html">A deep dive into how Kubernetes makes GPUs accessible to containers, from bare metal to CUDA applications</summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://hrishi.dev/assets/gpu-part1-provisioning-pipeline.png" /><media:content medium="image" url="https://hrishi.dev/assets/gpu-part1-provisioning-pipeline.png" xmlns:media="http://search.yahoo.com/mrss/" /></entry></feed>