<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Harshit Luthra</title>
    <description>The latest articles on DEV Community by Harshit Luthra (@sachincool).</description>
    <link>https://dev.to/sachincool</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F324078%2Fd55787a3-0609-4461-a718-e7cd6da8e118.png</url>
      <title>DEV Community: Harshit Luthra</title>
      <link>https://dev.to/sachincool</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sachincool"/>
    <language>en</language>
    <item>
      <title>Two tenants, one GPU, and no wall between them</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:15:37 +0000</pubDate>
      <link>https://dev.to/sachincool/two-tenants-one-gpu-and-no-wall-between-them-36ba</link>
      <guid>https://dev.to/sachincool/two-tenants-one-gpu-and-no-wall-between-them-36ba</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-8-multi-tenancy-security" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-25.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;Run &lt;code&gt;nvidia-smi&lt;/code&gt; inside a pod in one namespace, then inside a pod in another, on a cluster using GPU time-slicing. Look at the GPU UUID and the PCI bus ID.&lt;/p&gt;

&lt;p&gt;This is the last part. Seven parts built the fleet, wired it, scaled it, watched it, routed to it, and kept it alive through deploys. This one is about the moment more than one team shares it, which is where the assumptions that held for a single tenant quietly stop being true. The load-bearing one, the thing most teams get wrong: a Kubernetes namespace is not a wall on the GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  the isolation you don't have
&lt;/h2&gt;

&lt;p&gt;A namespace is an API-scoping and RBAC boundary. It has nothing to do with the hardware. The device plugin advertises &lt;code&gt;nvidia.com/gpu&lt;/code&gt; resources and the scheduler places pods onto them regardless of which namespace asked, so "one GPU, four replicas" under time-slicing means the scheduler can land a pod from &lt;code&gt;team-a&lt;/code&gt; and a pod from &lt;code&gt;team-b&lt;/code&gt; on the same physical die with no memory partition between them. What isolation you actually get depends entirely on the sharing mechanism, and it's worth stating exactly:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1eaya2yxl3t1ad7182dn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1eaya2yxl3t1ad7182dn.png" alt="A comparison matrix titled 'what actually isolates two tenants on a GPU', four rows by three columns. Rows: whole GPU (exclusive), MIG (hardware partition), MPS (multi-process service), time-slicing. Columns: memory isolation, fault isolation, compute QoS. Whole GPU: yes, yes, full device. MIG: yes (dedicated DRAM partition, hardware-enforced), yes (fault contained to the instance), guaranteed per slice. MPS: no (software soft caps only), weak (shared MPS server failure domain), soft percent cap. Time-slicing: none, none, none (equal time-share only). A note reads: only MIG or a whole GPU gives a hardware memory wall; MPS and time-slicing are software conventions, not walls." width="800" height="419"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · the only two rows with a real memory wall are "whole GPU" and "MIG." MPS caps are software. Time-slicing has nothing. If your mental model was "different namespace, different memory," this is the row that corrects it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The config that creates this situation is ordinary and common. This is a device-plugin time-slicing setup, the kind people turn on to raise utilization:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;sharing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;timeSlicing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia.com/gpu&lt;/span&gt;
      &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;          &lt;span class="c1"&gt;# one physical GPU -&amp;gt; four schedulable "GPUs", zero isolation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MPS swaps that block for per-client soft caps (&lt;code&gt;CUDA_MPS_PINNED_DEVICE_MEM_LIMIT&lt;/code&gt;, a compute percentage), which are enforced by the driver, not by hardware, and share a failure domain. This isn't only a noisy-neighbor concern. Security researchers have shown covert and side channels through the GPU's shared uncore engines, readable with unprivileged NVML calls, that bypass both MPS and MIG partitioning, and GPU DRAM that isn't zeroed on context teardown has leaked data across tenants under time-slicing. The one-line defensive setting: turn on &lt;code&gt;renameByDefault: true&lt;/code&gt; so a shared GPU advertises as &lt;code&gt;nvidia.com/gpu.shared&lt;/code&gt;, and a tenant can't request a "shared" GPU thinking it's a private one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; A Kubernetes namespace isolates the API and RBAC, never GPU memory. Under time-slicing or MPS, two tenants can sit on the same physical die with no hardware wall between their VRAM. If tenants don't trust each other, only MIG or a whole-GPU allocation is safe. This is the correction most GPU-cluster security models are missing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  MIG when tenants can't trust each other
&lt;/h2&gt;

&lt;p&gt;MIG is the answer when the boundary has to be real. It carves the physical card into hardware partitions, each with its own DRAM slice, L2, and SMs, so one instance cannot read another's memory regardless of driver or kernel bugs. Expose it through the GPU Operator in &lt;code&gt;mixed&lt;/code&gt; strategy, which advertises each profile as its own resource:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl patch clusterpolicies.nvidia.com/cluster-policy &lt;span class="nt"&gt;--type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'[{"op":"replace","path":"/spec/mig/strategy","value":"mixed"}]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The node then advertises MIG profiles as first-class schedulable resources, and a tenant requests a hardware-isolated slice by name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;nvidia.com/mig-1g.10gb&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;     &lt;span class="c1"&gt;# one hardware-isolated 10GB partition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tradeoff is the one from part 1: partitions are fixed at configure time, reconfiguring drains the node's GPU pods, and a workload can't burst past its slice. That rigidity is the price of a real wall. For untrusted multi-tenancy it's a price worth paying; for a single trusted team it's usually not.&lt;/p&gt;

&lt;h2&gt;
  
  
  quotas that don't waste the cluster
&lt;/h2&gt;

&lt;p&gt;Isolation stops one tenant from reading another's memory. Quotas stop one tenant from eating the whole cluster. The blunt version is a &lt;code&gt;ResourceQuota&lt;/code&gt; capping GPU requests per namespace (extended resources are quotable only via &lt;code&gt;requests.&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ResourceQuota&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;team-a-gpu-quota&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;team-a&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;requests.nvidia.com/gpu&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's static, though, and it strands GPUs whenever a team is idle. Kueue fixes that by admitting jobs against quota and letting teams in a shared cohort borrow each other's idle GPUs while guaranteeing each its floor back on demand. A &lt;code&gt;ClusterQueue&lt;/code&gt; holds the quota; the &lt;code&gt;cohortName&lt;/code&gt; is what enables borrowing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kueue.x-k8s.io/v1beta2&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ClusterQueue&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;team-a-cq&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;cohortName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpu-pool"&lt;/span&gt;                 &lt;span class="c1"&gt;# teams in one cohort lend/borrow idle GPUs&lt;/span&gt;
  &lt;span class="na"&gt;resourceGroups&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;coveredResources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nvidia.com/gpu"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;flavors&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;h100-flavor&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nvidia.com/gpu&lt;/span&gt;
        &lt;span class="na"&gt;nominalQuota&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;                  &lt;span class="c1"&gt;# guaranteed floor&lt;/span&gt;
        &lt;span class="na"&gt;borrowingLimit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;                &lt;span class="c1"&gt;# cap on what it can borrow from the cohort&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One version trap worth checking before you copy that: current Kueue serves &lt;code&gt;v1beta2&lt;/code&gt;, where cohorts are a named field (and can be their own CRD with fair-share weights). Plenty of clusters still run &lt;code&gt;v1beta1&lt;/code&gt;, where the field is a plain &lt;code&gt;spec.cohort&lt;/code&gt; string and the standalone Cohort doesn't exist. &lt;code&gt;kubectl get crd clusterqueues.kueue.x-k8s.io -o jsonpath='{.spec.versions[*].name}'&lt;/code&gt; tells you which you have. NVIDIA's KAI scheduler (the open-sourced core of Run:ai) models the same idea as a hierarchical &lt;code&gt;Queue&lt;/code&gt; with a &lt;code&gt;quota&lt;/code&gt; (the deserved floor) and an &lt;code&gt;overQuotaWeight&lt;/code&gt; (your proportional claim on idle GPUs above it), plus time-based fair-share so a team that under-used earlier gets favored later.&lt;/p&gt;

&lt;h2&gt;
  
  
  the endpoint nobody locked
&lt;/h2&gt;

&lt;p&gt;The most common GPU-cluster security hole isn't exotic. It's a vLLM pod with a Service and no authentication, reachable from anywhere on the cluster or, worse, the internet: free inference, prompt exfiltration, model theft. Lock it at three layers. Default-deny ingress so only the gateway can reach the model server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NetworkPolicy&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;default-deny-ingress&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;team-a&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;podSelector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{}&lt;/span&gt;
  &lt;span class="na"&gt;policyTypes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ingress"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then an authenticated, TLS-terminating gateway in front (Gateway API &lt;code&gt;HTTPRoute&lt;/code&gt; with auth attached upstream), and a least-privilege ServiceAccount so a compromised inference pod can't read the rest of the cluster's secrets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;vllm-sa&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;team-a&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;automountServiceAccountToken&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;     &lt;span class="c1"&gt;# the model server never calls the API server&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which connects to the last quiet failure: secrets. Gated models return a 403 at download time when the pod has no valid Hugging Face token, and that surfaces as exactly the CrashLoopBackOff from part 7, except this time it really is the app. Mount the token from a Secret (or an external manager), never bake it into the image, and pin the model by digest so you don't silently load a tampered checkpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HF_TOKEN&lt;/span&gt;
  &lt;span class="na"&gt;valueFrom&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;secretKeyRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;hf-token&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;token&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer &lt;code&gt;.safetensors&lt;/code&gt; over pickle formats (loading a pickle can execute arbitrary code), pin the model revision to a commit, and verify its checksum. The supply chain for a 140GB weight file deserves the same suspicion as any other dependency, which is the whole thesis of the lazy-security series if you want the longer version.&lt;/p&gt;

&lt;h2&gt;
  
  
  the frontier: confidential computing
&lt;/h2&gt;

&lt;p&gt;One emerging piece, worth knowing exists even if you're not deploying it yet. Confidential computing extends a CPU trusted execution environment to the GPU, so even the cloud operator hosting your node can't read the weights or activations in VRAM. H100 CC-mode is GA: the driver, running inside a confidential VM, encrypts everything crossing the PCIe bus, and CUDA apps run unmodified once trust is established. Blackwell extends it with NVLink encryption for multi-GPU confidential domains. Independent benchmarks put the overhead under 5% for typical LLM inference (it's the CPU-to-GPU I/O encryption that costs, so small-batch workloads pay a bit more). On Kubernetes it lands through Kata Containers and Confidential Containers, and it's still maturing operationally. For regulated or sensitive-IP inference, it's the direction; for most workloads today, it's a section to file away.&lt;/p&gt;

&lt;h2&gt;
  
  
  who's paying for the idle GPUs
&lt;/h2&gt;

&lt;p&gt;The last shared-cluster problem is money, and it's the biggest one, because cluster GPU utilization commonly sits at 30 to 40%. The gap between GPUs bought and GPUs doing work is enormous, and the fix is making teams see their own idle GPU-hours. Enforce a &lt;code&gt;team&lt;/code&gt; label on every GPU pod (a policy engine like Kyverno rejects pods without one), export per-GPU metrics with DCGM, and attribute GPU-hours per team:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPU&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;pods&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;must&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;carry&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;'team'&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;for&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;chargeback."&lt;/span&gt;
  &lt;span class="na"&gt;pattern&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;team&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?*"&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the label enforced, DCGM's per-pod metrics roll up into GPU-hours per team, and tagging inference requests with an &lt;code&gt;X-Team&lt;/code&gt; header at the gateway takes it down to token-level attribution. Showback doesn't reclaim a single GPU by itself. It just makes the waste visible to the people who can, which turns out to be most of the battle.&lt;/p&gt;

&lt;p&gt;That's eight parts, and it's time to say the thing the whole series was circling. A GPU deployment is a dozen layers under one pod, a small network in one box, a large one between boxes, a set of graphs that tell you the truth, an autoscaler that turns it off, a router that doubles it for free, a set of probes that keep it breathing, and a tenancy model that decides who it hurts when it breaks. The silicon was the easy part. Everything that makes it hard lives in the layers around it, and every one of those layers is a place you can be the person who saw it coming, or the one explaining it in the incident review. That was the job the entire time. The GPUs were never the point.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>kubernetes</category>
      <category>multitenancy</category>
      <category>security</category>
    </item>
    <item>
      <title>Your model isn't crashing, your probe is</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:15:33 +0000</pubDate>
      <link>https://dev.to/sachincool/your-model-isnt-crashing-your-probe-is-jbc</link>
      <guid>https://dev.to/sachincool/your-model-isnt-crashing-your-probe-is-jbc</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-7-serving-ops" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-23.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The pods were in &lt;code&gt;CrashLoopBackOff&lt;/code&gt; and the logs said nothing. vLLM started, printed its usual banner, began loading weights, and then died. Restarted, loaded again, died again. It looked exactly like a broken build, so the first hour went into the model, the image, the CUDA version, everything except the actual culprit, which was a fourteen-line probe config nobody had looked at.&lt;/p&gt;

&lt;p&gt;This is part 7. The first six parts built the fleet, wired it, scaled it, watched it, and routed to it. This part is about keeping a model server alive through the three things that routinely kill it: a health probe that fires too early, a node drain that cuts a request in half, and a model-version rollout that drops traffic on the floor. None of it is glamorous. All of it is what stands between you and a 3am page.&lt;/p&gt;

&lt;h2&gt;
  
  
  your model isn't crashing, your probe is
&lt;/h2&gt;

&lt;p&gt;Here's the tell, and once you've seen it once you never miss it again:&lt;/p&gt;

&lt;p&gt;vLLM binds its HTTP port almost immediately, but &lt;code&gt;/health&lt;/code&gt; on &lt;code&gt;:8000&lt;/code&gt; only returns 200 once the weights are loaded and the engine is warm. For a 70B on a cold pull that's several minutes. A default liveness probe with a small &lt;code&gt;initialDelaySeconds&lt;/code&gt; starts checking during that window, gets a connection refused, decides the container is dead, and the kubelet kills it. It never finishes loading, so it never passes, so it loops forever. The exit code is 137 (128 + SIGKILL) which is the kubelet's fingerprint, not an application crash.&lt;/p&gt;

&lt;p&gt;The fix is a &lt;code&gt;startupProbe&lt;/code&gt;. It holds the liveness and readiness probes off entirely until it succeeds, and its &lt;code&gt;failureThreshold × periodSeconds&lt;/code&gt; is your total load budget:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;startupProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/health&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8000&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;      &lt;span class="c1"&gt;# 60 × 10s = 600s (10 min) to finish loading&lt;/span&gt;
&lt;span class="na"&gt;livenessProbe&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;httpGet&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;/health&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;8000&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;periodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
  &lt;span class="na"&gt;failureThreshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;       &lt;span class="c1"&gt;# after startup, catch a real hang in 30s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason this beats just cranking &lt;code&gt;initialDelaySeconds: 600&lt;/code&gt; on the liveness probe is that once the startup probe passes, liveness reverts to its tight interval and still catches a genuine deadlock in thirty seconds. A giant liveness delay would blind you to real hangs for ten minutes after every restart. With the startup probe in place, the pods ride through the load and come up clean:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get pods -l app=vllm-llama3-70b
NAME                               READY   STATUS    RESTARTS   AGE
vllm-llama3-70b-7f4b9c6d8f-9m2tq   1/1     Running   0          8m03s
vllm-llama3-70b-7f4b9c6d8f-c8xvn   1/1     Running   0          8m03s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; A model that takes minutes to load needs a startupProbe, not a bigger liveness delay. Without one, the probe that's supposed to detect a dead server is the thing killing a healthy one, and the crash loop looks exactly like an application bug. This is the single most common self-inflicted LLM serving outage.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  draining without dropping a stream
&lt;/h2&gt;

&lt;p&gt;The next one bites on every deploy and every node scale-down. When Kubernetes deletes a pod, it sends &lt;code&gt;SIGTERM&lt;/code&gt; and removes the pod from the service endpoints at the same instant. vLLM handles &lt;code&gt;SIGTERM&lt;/code&gt; correctly (it stops accepting new requests and finishes the in-flight ones), but if the load balancer is still routing to it during that beat, new requests land on a server that's shutting down. And if the grace period is too short, the kubelet &lt;code&gt;SIGKILL&lt;/code&gt;s the process mid-generation, dropping a half-finished response the client has to retry from scratch.&lt;/p&gt;

&lt;p&gt;Two fields fix it. A &lt;code&gt;preStop&lt;/code&gt; hook that sleeps long enough for the endpoint removal to propagate before &lt;code&gt;SIGTERM&lt;/code&gt; reaches vLLM, and a &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt; set above your longest in-flight generation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;terminationGracePeriodSeconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;210&lt;/span&gt;   &lt;span class="c1"&gt;# preStop(15s) + longest decode(~180s) + margin&lt;/span&gt;
&lt;span class="na"&gt;lifecycle&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;preStop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;exec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sh"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-c"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sleep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;15"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sequence is worth internalizing, because the parallelism is the point: the endpoint removal and the &lt;code&gt;preStop&lt;/code&gt; sleep happen at the same time, so by the time &lt;code&gt;SIGTERM&lt;/code&gt; actually reaches vLLM, the load balancer has already stopped sending it traffic.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiuosakkrps5h3zgx3y87.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fiuosakkrps5h3zgx3y87.png" alt="A horizontal timeline titled 'what happens when a vLLM pod is deleted', showing the shutdown sequence left to right. t=0: pod marked Terminating, which forks into two parallel tracks: top track 'removed from EndpointSlice, load balancer stops routing new requests', bottom track 'preStop hook runs: sleep 15s'. Both converge at t=15s: 'SIGTERM sent to vLLM, which stops new admissions and finishes in-flight requests'. Then 'in-flight decode drains'. Finally at t=210s a marker 'SIGKILL if grace period elapses' shown in red as the backstop. A note reads 'endpoint removal and preStop run in parallel, so traffic stops before the process does'." width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · the drain, in order. The whole job of &lt;code&gt;preStop&lt;/code&gt; is to buy time for the load balancer to stop routing before vLLM stops answering.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  surviving a node drain
&lt;/h2&gt;

&lt;p&gt;Parts 3 and 5 leaned on Karpenter to consolidate and remove idle GPU nodes. That same consolidation, unguarded, will happily evict every replica of a service at once and take it to zero. The guard is a &lt;code&gt;PodDisruptionBudget&lt;/code&gt;, and for a small pool of expensive GPU replicas you want &lt;code&gt;maxUnavailable: 1&lt;/code&gt; so at most one goes down for any voluntary disruption:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;policy/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PodDisruptionBudget&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm-llama3-70b-pdb&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;maxUnavailable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt; &lt;span class="nv"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;vllm-llama3-70b&lt;/span&gt; &lt;span class="pi"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Karpenter drains through the Eviction API, which respects PDBs, so this is enough to keep the service serving through a consolidation. For a pod that's mid-critical-work and must not be interrupted at all, there's a stronger lever, the &lt;code&gt;karpenter.sh/do-not-disrupt&lt;/code&gt; annotation, which excludes its node from consolidation entirely:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;karpenter.sh/do-not-disrupt&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use it deliberately, though. Leave it on permanently and you've told Karpenter it can never reclaim that node, which is how you end up back in part 5's problem of expensive GPUs that never scale down. The PDB is the always-on floor; the annotation is for pods you're actively protecting.&lt;/p&gt;

&lt;h2&gt;
  
  
  one replica, many pods
&lt;/h2&gt;

&lt;p&gt;When a model is too big for one node (the 405B from part 3, tensor-parallel across eight GPUs and pipeline-parallel across two nodes), a single replica &lt;em&gt;is&lt;/em&gt; a group of pods, and a normal Deployment can't express that. LeaderWorkerSet can. It treats a leader plus N-1 workers as one unit: &lt;code&gt;replicas&lt;/code&gt; is the number of these groups, &lt;code&gt;size&lt;/code&gt; is the pods per group, and &lt;code&gt;RecreateGroupOnPodRestart&lt;/code&gt; means if any pod in the group dies the whole group restarts, which is correct, because a tensor-parallel replica missing one member is dead weight:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;leaderworkerset.x-k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;LeaderWorkerSet&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vllm&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;                    &lt;span class="c1"&gt;# two independent model replicas&lt;/span&gt;
  &lt;span class="na"&gt;leaderWorkerTemplate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;                      &lt;span class="c1"&gt;# 2 pods each: one leader + one worker&lt;/span&gt;
    &lt;span class="na"&gt;restartPolicy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;RecreateGroupOnPodRestart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leader starts the Ray head and vLLM's OpenAI server with &lt;code&gt;--tensor-parallel-size × --pipeline-parallel-size&lt;/code&gt; equal to the total GPUs across the group; the workers join the Ray cluster via the injected &lt;code&gt;LWS_LEADER_ADDRESS&lt;/code&gt;. The catch from part 2 still applies with force: the group has to land on NVLink-connected GPUs or the cross-node collective crawls, so pair LWS with gang scheduling and topology-aware placement (Kueue's TAS, or the KAI scheduler). Rolling updates are first-class through &lt;code&gt;rolloutStrategy&lt;/code&gt; with &lt;code&gt;maxUnavailable&lt;/code&gt; and &lt;code&gt;maxSurge&lt;/code&gt;, so you can roll a multi-node replica without taking the whole service down.&lt;/p&gt;

&lt;h2&gt;
  
  
  rolling out a new model without an outage
&lt;/h2&gt;

&lt;p&gt;The last way to drop traffic is deploying a new model version badly. KServe makes the careful version cheap: set &lt;code&gt;canaryTrafficPercent&lt;/code&gt; on an &lt;code&gt;InferenceService&lt;/code&gt; and point &lt;code&gt;storageUri&lt;/code&gt; at the new weights, and it splits traffic between the current good revision (which it tracks automatically) and the new one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;serving.kserve.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;InferenceService&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;llama3-chat&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;predictor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;canaryTrafficPercent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;       &lt;span class="c1"&gt;# 10% to the new revision, 90% stays on last-good&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;storageUri&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;s3://models/llama3-chat/v2"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You watch the split live, and the metrics from part 4 (TTFT, error rate, and any quality signal) decide whether it graduates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ kubectl get isvc llama3-chat
NAME          URL                       READY   PREV   LATEST   LATESTREADYREVISION
llama3-chat   http://llama3-chat...     True    90     10       llama3-chat-predictor-00002
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Promotion is deleting the &lt;code&gt;canaryTrafficPercent&lt;/code&gt; field and re-applying: all traffic shifts to the new revision and the old one scales to zero. Blue-green is the same mechanism flipped straight to 100; shadow is mirroring live traffic to the candidate without returning its responses, so you can compare outputs with zero user risk. Two things to know before you rely on it. First, this traffic-splitting is a serverless (Knative) mode feature; in raw deployment mode you split with Gateway API route weights instead. Second, version the weights, the serving config, and the prompt template together as one revision, or your canary metrics are comparing two things that differ in ways you didn't track. (And don't reach for ModelMesh for multi-model serving; the project is archived.)&lt;/p&gt;

&lt;p&gt;That's the service staying up through loads, drains, and deploys. The last thing between you and a calm on-call isn't the software at all, it's other people: the tenants sharing your cluster, the ones who can reach your endpoint, and a GPU-memory boundary that turns out to be nowhere near where you think it is. That's the final part, and it's the one most likely to end up in an incident review.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>kubernetes</category>
      <category>vllm</category>
      <category>kserve</category>
    </item>
    <item>
      <title>The cheapest speedup is your load balancer</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:14:53 +0000</pubDate>
      <link>https://dev.to/sachincool/the-cheapest-speedup-is-your-load-balancer-3478</link>
      <guid>https://dev.to/sachincool/the-cheapest-speedup-is-your-load-balancer-3478</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-6-inference-routing" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-18.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;A team I know had a slow chatbot and did the obvious thing: added replicas. TTFT barely moved. They added more. Still slow, and now the bill was worse. Every GPU showed healthy utilization, the queue metrics from part 4 were climbing, and adding capacity wasn't buying the speedup capacity is supposed to buy. Then someone swapped the Kubernetes Service in front of the pods for a prefix-cache-aware router, changed nothing else, and the same eight GPUs got more than twice as fast. The problem was never the GPUs or the count. It was the load balancer, quietly throwing away the most expensive thing the servers had built.&lt;/p&gt;

&lt;p&gt;This is part 6. Parts 1 through 5 built the fleet, watched it, and scaled it. This part is about the layer in front of the fleet: how requests get assigned to replicas, and why the default answer (round-robin, the same load balancing you'd use for a stateless web app) is the wrong one for LLM inference. It's the highest ratio of payoff to effort in the whole series, because the win comes from a routing decision, not from hardware you have to buy.&lt;/p&gt;

&lt;h2&gt;
  
  
  the load balancer that throws away your cache
&lt;/h2&gt;

&lt;p&gt;Here's the thing a normal load balancer doesn't know: LLM replicas are not stateless. Each vLLM replica keeps its own KV cache (from part 2, the running memory of tokens it has already processed), and it also keeps a &lt;strong&gt;prefix cache&lt;/strong&gt;: if two requests share the same opening tokens (a long system prompt, a RAG document, the history of a chat), the second one can reuse the first one's cached computation and skip prefill entirely. Prefill is the compute-bound, expensive half of a request. A prefix cache hit makes it nearly free.&lt;/p&gt;

&lt;p&gt;But each replica has its &lt;em&gt;own&lt;/em&gt; prefix cache. A round-robin load balancer scatters requests across replicas blind to what each one holds. So a request that shares a 2,000-token prefix with something served thirty seconds ago lands on a different replica that never saw it, re-runs the entire prefill from scratch, and fills its KV cache doing redundant work. Multiply that across a prefix-heavy workload and the fleet spends most of its compute re-prefilling prompts it already processed, on a different pod. The caches sit there full of answers to questions that keep getting routed elsewhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  the numbers, from one honest benchmark
&lt;/h2&gt;

&lt;p&gt;This isn't a theoretical gain, and the cleanest measurement of it is a benchmark Andy Golubev ran on EKS in June 2026. The setup was deliberately boring: Qwen2.5-7B on vLLM, eight &lt;code&gt;g5.xlarge&lt;/code&gt; nodes with one A10G each, one decode replica per node. Two runs, identical in every way except the front door. First run: a plain Kubernetes Service doing round-robin. Second run: an llm-d prefix-cache-aware router. Same model, same eight GPUs, same eight replicas. The workload was &lt;code&gt;vllm bench serve&lt;/code&gt; replaying 9,000 prompts that shared a pool of 150 long (2,048-token) prefixes, which is exactly the RAG-and-chat shape real traffic takes.&lt;/p&gt;

&lt;p&gt;The gap was not subtle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n6f73u3x9f4qu6q3edk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5n6f73u3x9f4qu6q3edk.png" alt="A before-and-after stat panel titled 'same 8 GPUs, only the router changed' comparing round-robin to prefix-cache-aware routing across five metrics. Output throughput: 2,742 to 6,423 tokens per second (2.34x). Wall clock for the run: 840 to 359 seconds. Mean TTFT: 19.0 seconds to 0.86 seconds (about 22x). Prefix cache hit rate: 11 percent to 93 percent. Requests waiting in queue: about 180 to 0. KV cache utilization: pinned near 99 percent down to 64 to 71 percent, showing headroom instead of thrash." width="800" height="473"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · the Golubev EKS numbers. The one that matters most is the last one: round-robin ran the cache pinned at 99% and thrashing; the smart router left it headroom, because it stopped generating redundant work.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Read the cache-hit line again: 11% to 93%. Round-robin was reusing almost nothing; the aware router reused almost everything. Mean TTFT dropped from a genuinely broken 19 seconds to under a second. And the fleet stopped queueing (waiting requests went from ~180 to zero) not because it got more capacity, but because it stopped wasting the capacity it had. Google reports the same shape from GKE's managed version: TTFT improvements up to 96% at peak load on prefix-heavy workloads. The lesson is uncomfortable and freeing at once. Before you buy a ninth GPU, check whether your router is making the eight you have re-do each other's work.&lt;/p&gt;

&lt;h2&gt;
  
  
  routing to the replica that already knows
&lt;/h2&gt;

&lt;p&gt;The mechanism is simpler than it sounds. vLLM emits events about what its prefix cache holds. A cache-aware router consumes those events and keeps a live picture of which replica has which prefixes cached. When a request arrives, instead of picking the next replica in rotation, the router picks the one most likely to already hold the request's prefix, so the cache hit actually happens.&lt;/p&gt;

&lt;p&gt;That decision point has a name in the Kubernetes world: the &lt;strong&gt;Endpoint Picker&lt;/strong&gt;, or EPP. It doesn't just look at prefix locality. A good EPP scores every candidate replica on several signals at once and sends the request to the best total score:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prefix-cache locality&lt;/strong&gt;: does this replica already hold the request's opening tokens? Longer match, higher score.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;load&lt;/strong&gt;: what's this replica's KV-cache utilization and queue depth right now? A replica that's already saturated scores lower even if it has the prefix, so you don't stampede one hot pod.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LoRA affinity&lt;/strong&gt;: if you serve multiple fine-tuned adapters, is the right adapter already loaded here? Loading one is not free, so prefer the replica that has it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The router balances those against each other, and it can queue or shed when everything is overloaded. This is the difference between "which pod is next" and "which pod will serve this request fastest given what it already has warm." Same request, same pods, a decision made with information the round-robin balancer never had.&lt;/p&gt;

&lt;h2&gt;
  
  
  the standard nobody had two years ago
&lt;/h2&gt;

&lt;p&gt;The reason this is worth a whole chapter now, and wasn't in 2024, is that it stopped being a bespoke hack and became a Kubernetes standard. The &lt;strong&gt;Gateway API Inference Extension&lt;/strong&gt; is a SIG-Networking project that adds LLM-aware routing on top of the ordinary Gateway API. It introduces an &lt;code&gt;InferencePool&lt;/code&gt;, which is a group of pods that share the same accelerator, base model, and model server (the LLM-shaped version of a Service), and it wires an Endpoint Picker into the request path through Envoy's external-processing (&lt;code&gt;ext-proc&lt;/code&gt;) protocol, so the proxy calls out to the EPP for a decision on every request.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9tm3vcofreipzhl3orx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu9tm3vcofreipzhl3orx.png" alt="An architecture diagram titled 'the inference gateway request path'. A request enters from the left into an Envoy-based Gateway (L7 proxy handling TLS and connection management). The gateway makes an ext-proc call out to the Endpoint Picker (EPP), which reads live signals from the replica pool (prefix-cache state, KV-cache utilization, queue depth, loaded LoRA adapters), scores each replica, and returns the chosen endpoint. The gateway then forwards the request to the selected replica inside an InferencePool of four vLLM pods. A note reads 'InferencePool is the LLM-aware Service; the EPP is the brain'." width="800" height="447"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · the request path. The gateway is a normal Envoy proxy; the intelligence lives in the Endpoint Picker it consults per request, over the same ext-proc hook Envoy already uses for auth and rate limiting.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The project has reached GA, with &lt;code&gt;InferencePool&lt;/code&gt; graduating to a stable v1 API. (One honest caveat: the exact GA milestone landed over early-to-mid 2026, so pin the version you're deploying rather than trusting a blog's date, this one included.) Google productized it as &lt;strong&gt;GKE Inference Gateway&lt;/strong&gt;, which is GA and is literally powered by the llm-d router underneath. So the "advanced" thing here is also increasingly the default, k8s-native thing, which is exactly the production framing this series cares about.&lt;/p&gt;

&lt;h2&gt;
  
  
  the whole stack, named
&lt;/h2&gt;

&lt;p&gt;"llm-d" gets thrown around as if it were a server. It isn't; it's an assembly, and it's clearer to name the pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;vLLM&lt;/strong&gt; is the model server. It owns the per-replica KV and prefix cache and emits the cache events the router needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;KServe&lt;/strong&gt; is the serving control plane, exposing an &lt;code&gt;LLMInferenceService&lt;/code&gt; custom resource so you describe the model and its serving config as a normal Kubernetes object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The inference gateway&lt;/strong&gt; is Envoy plus the Gateway API Inference Extension: the data plane plus the LLM-aware routing above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The router itself&lt;/strong&gt; is that L7 proxy plus the Endpoint Picker, making the per-request decision from cache, load, and LoRA signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disaggregated prefill/decode&lt;/strong&gt; from part 3 is an optional add-on here, with the KV cache handed between pools over a connector.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're not ready to adopt the full gateway, the vLLM project's own &lt;strong&gt;production-stack&lt;/strong&gt; helm chart is a lighter on-ramp: it deploys a router service in front of your vLLM pods that already does model-aware and prefix-aware routing, plus KV-cache offload through LMCache. Same idea, smaller commitment. Either way, the thing you're installing is a router that knows what a KV cache is.&lt;/p&gt;

&lt;h2&gt;
  
  
  watching the router
&lt;/h2&gt;

&lt;p&gt;Part 4 said the metrics that matter live in the serving engine. Add one more surface: the router. The signals that tell you whether the routing layer is earning its keep are the ones that moved in that benchmark. Prefix cache hit rate is the headline. A round-robin fleet sits low (that 11%); a well-routed prefix-heavy fleet should sit high (the 93%). If you turned on cache-aware routing and the hit rate didn't climb, either your workload doesn't actually share prefixes or the router isn't seeing the cache events.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; Prefix cache hit rate is the one dashboard number that proves the router is working. Watch it alongside per-replica KV utilization and waiting-request count. If hit rate is high and evenly spread, routing is doing its job. If one replica is pinned while others idle, your scorer is over-weighting cache locality and stampeding a hot pod. If hit rate is flat near zero, your traffic isn't prefix-heavy and this whole chapter buys you little.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last line is the honest boundary. Cache-aware routing is close to free money for workloads with real prefix reuse: RAG over a shared corpus, long system prompts, multi-turn chat, agents replaying context. For traffic where every prompt is unique and short, there's no cache to reuse and the fancy router mostly just adds a hop. The KV-connector interfaces underneath are also still moving, so expect some churn if you build on the bleeding edge. Know which workload you have before you reach for this, the same way you'd check the topology in part 2 before promising a throughput number.&lt;/p&gt;

&lt;p&gt;The router is the front door. What sits behind it, the probes that decide when a replica is ready, the way you roll a new model version out without dropping a request, the tenants all fighting over the same pool of GPUs, is where a production inference platform actually gets hard. That's where the series goes next.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>inference</category>
      <category>routing</category>
      <category>vllm</category>
    </item>
    <item>
      <title>Scaling GPU inference to zero and back</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:14:48 +0000</pubDate>
      <link>https://dev.to/sachincool/scaling-gpu-inference-to-zero-and-back-3ebm</link>
      <guid>https://dev.to/sachincool/scaling-gpu-inference-to-zero-and-back-3ebm</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-5-scale-to-zero" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-16.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The finance dashboard is what started it. A staging cluster running two H100 nodes around the clock, mostly to serve a demo that got maybe forty requests a day, all of them during business hours. The nodes sat idle from 7pm to 9am and every weekend, billing the whole time. Somebody added scale-to-zero over a Friday. Monday morning the first person to open the demo waited six minutes for a response, assumed it was broken, and filed a bug. The bill went down and the product got worse, which is scale-to-zero working exactly as designed and nobody being happy about it.&lt;/p&gt;

&lt;p&gt;This is the last part of the series. Parts 1 through 4 covered building GPU infrastructure and seeing what it's doing. This part is about the thing that actually shows up on the invoice: a GPU you're paying for while it does nothing. Scaling GPU inference elastically, all the way to zero when there's no traffic, is the biggest lever on the bill. It's also genuinely hard, because unlike a stateless web pod that starts in a second, a GPU replica has to drag a hundred gigabytes of model weights onto the card before it can serve a single token. The whole post is about that gap.&lt;/p&gt;

&lt;h2&gt;
  
  
  the cold-start tax
&lt;/h2&gt;

&lt;p&gt;Scaling a web service to zero is free because starting a new pod is nearly instant. Scaling an LLM to zero is expensive because starting a replica is not. Add up what has to happen before the first token, on a cold node:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node provisioning.&lt;/strong&gt; The cluster asks the cloud for a GPU node, waits for it to boot and join. One to five minutes, and that assumes the GPU is even available to hand you (more on that at the end).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Image pull.&lt;/strong&gt; A CUDA plus vLLM container image is commonly 5 to 15 GB. On a fresh node with a cold cache, pulling and unpacking it is minutes, not seconds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model load.&lt;/strong&gt; Llama-3-70B in BF16 is about 140 GB of weights, usually sitting in object storage. Reading that over the network and moving it onto the GPU is the big one, and done naively it's several minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warmup.&lt;/strong&gt; CUDA graph capture, &lt;code&gt;torch.compile&lt;/code&gt;, and a few dummy forward passes to populate caches. Tens of seconds before the first real request is fast.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stack those up and a naive 70B cold start runs six to nine minutes, more if those 140 GB of weights come cold over a slow path. That's the tax. Everything else in this post is a way to stop paying it, so that scale-to-zero saves the money without the six-minute Monday.&lt;/p&gt;

&lt;p&gt;One cruel interaction hides in here: a naive Kubernetes health probe treats that multi-minute load as a failure. A liveness probe with default timing restarts the pod mid-load, and now you have a crash loop that looks like a vLLM bug but is really a probe firing too early. Set the liveness &lt;code&gt;initialDelaySeconds&lt;/code&gt; above your worst-case load time, keep it longer than the readiness delay, and add a &lt;code&gt;preStop&lt;/code&gt; sleep so in-flight requests drain before the pod goes down. It's the single most common self-inflicted serving outage, and it costs nothing to avoid once you've seen it once.&lt;/p&gt;

&lt;h2&gt;
  
  
  scale on the queue, not the GPU
&lt;/h2&gt;

&lt;p&gt;Before scaling to zero, get scaling to &lt;em&gt;anything&lt;/em&gt; right, and the first mistake is scaling on GPU utilization. It's the obvious metric and it's the wrong one, for the reason part 4 laid out: a GPU can sit at 95% while the queue is empty, or at 40% while requests pile up. Scaling on GPU-util adds replicas late and removes them at the wrong time.&lt;/p&gt;

&lt;p&gt;Scale on the serving signal instead. The metric that actually tracks unmet demand is the queue: &lt;code&gt;vllm:num_requests_waiting&lt;/code&gt;, or KV-cache utilization as a leading indicator. You'll still find plenty of setups scaling on GPU-util or cache percentage, and those aren't wrong so much as indirect; the point is to scale on demand rather than on how busy the chip happens to look, with tail latency as the guardrail. In practice that means KEDA (the Kubernetes event-driven autoscaler) with a Prometheus trigger reading that metric, or an HPA wired to the same value through the Prometheus adapter. For traffic that's genuinely request-driven and spiky, KEDA's HTTP add-on can scale on in-flight request count directly. The shape is the same: pick the metric that means "users are waiting," set the threshold at the per-replica capacity you measured with the load test in part 4, and let it add replicas before the queue becomes TTFT.&lt;/p&gt;

&lt;h2&gt;
  
  
  actually reaching zero
&lt;/h2&gt;

&lt;p&gt;Scaling to zero is a special case of scaling, with one extra problem: when you're at zero replicas, there's nothing running to receive the request that's supposed to wake you up. Something has to catch that first request and hold it while a replica spins up.&lt;/p&gt;

&lt;p&gt;KEDA does this with an activation threshold: below it, the deployment sits at zero; the first event scales it to one. Knative Serving builds the pattern in more deliberately, with an &lt;strong&gt;activator&lt;/strong&gt; component that buffers incoming requests while a cold replica starts, then releases them once it's ready, so the request is slow but not dropped. KServe (which runs on Knative) exposes this as a simple &lt;code&gt;minReplicas: 0&lt;/code&gt; on an InferenceService, and it's the most common way teams get GPU model servers to zero on Kubernetes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1hkgkw42evd32a4pa7pg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1hkgkw42evd32a4pa7pg.png" alt="A lifecycle state diagram titled 'scale-to-zero, and the request that pays for it'. Four states in a loop: 'ZERO: no replicas, no cost' at rest; an incoming request triggers an arrow to 'ACTIVATING: activator buffers the request, autoscaler asks for a GPU node'; then 'COLD START: pull image, load weights, warm up' (annotated 'the first user waits here'); then 'SERVING: replica ready, requests flow'; and after an idle timeout an arrow back to 'ZERO'. The buffered first request is shown held at the activator through the cold-start state." width="800" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · the lifecycle, and the unlucky first request. Everything after this figure is about shrinking the cold-start box so that request waits seconds, not minutes.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The honest catch is that reaching zero and the cold-start tax are the same coin. Zero replicas is where the savings are, and it's also where every request pays the full six minutes. Everything below is about making that first request cost seconds instead, because a scale-to-zero setup with a six-minute cold start is a cost win and a product loss, and you rarely get to keep both.&lt;/p&gt;

&lt;p&gt;There's also a breakeven worth running before you build any of this. Scale-to-zero wins when utilization is low and spiky; it loses somewhere around half-time, where a dedicated node is both cheaper than paying serverless rates by the second and free of cold starts entirely. If a GPU is busy more than half the day, don't scale it to zero. Right-size it and leave it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  killing the image pull
&lt;/h2&gt;

&lt;p&gt;The container image is the first fixable minute. Two families of fix.&lt;/p&gt;

&lt;p&gt;The first is to stop pulling the whole thing before you start. &lt;strong&gt;SOCI&lt;/strong&gt; (Seekable OCI), lazy-loading via a containerd snapshotter, lets a container start running against an index of the image and fetch the actual bytes on demand, so the model server boots while the layers it doesn't need yet are still downloading. &lt;code&gt;estargz&lt;/code&gt; (the containerd stargz snapshotter) and Nydus do the same lazy-pull trick with different formats; GKE's Image Streaming is the managed version, and it cut a 5.4 GB Triton image's start from 191 seconds to 30. There's also a variant for the case where you &lt;em&gt;will&lt;/em&gt; read the whole image anyway (AI images touch most of their bytes immediately): SOCI's parallel-pull mode just parallelizes the download and unpack instead of lazy-loading, and AWS measured roughly 60% off a 10 GB image.&lt;/p&gt;

&lt;p&gt;The second is to not pull over the network at all. Pre-bake the image into the node's disk image so it's local before the pod schedules. Run an in-cluster pull-through cache or a peer-to-peer image mirror (Spegel and friends) so the second node to need an image gets it from a neighbor instead of the registry. Pre-pull hot images with a DaemonSet so they're warm before traffic arrives. None of these is clever; all of them beat pulling 15 GB cold from a registry when a hundred replicas try it at once.&lt;/p&gt;

&lt;h2&gt;
  
  
  killing the model load
&lt;/h2&gt;

&lt;p&gt;The bigger minute is the model weights, and this is where the newer tooling has moved fast. Loading 140 GB of safetensors off a disk or object store the default way is serial and slow. The fixes stream instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;NVIDIA's Run:ai Model Streamer&lt;/strong&gt; reads weights from object storage in many parallel streams straight onto the GPU, overlapping download with load, and vLLM supports it directly (&lt;code&gt;--load-format runai_streamer&lt;/code&gt;). NVIDIA's own benchmark took an 8B model's S3 load from 28 seconds at four streams to under five at thirty-two. CoreWeave's &lt;strong&gt;tensorizer&lt;/strong&gt; (&lt;code&gt;--load-format tensorizer&lt;/code&gt;) serializes the model into a format that streams from S3 or local disk with near-zero deserialization overhead. Both turn a multi-minute load into tens of seconds. Underneath, &lt;code&gt;safetensors&lt;/code&gt; already supports memory-mapped zero-copy loading, which helps when the file is local.&lt;/p&gt;

&lt;p&gt;And local is the other half. Cache the weights on the node's NVMe (an instance-store disk) so a restart reads from local flash instead of re-downloading. Or mount a shared, fast filesystem so every replica reads the same warm copy, but mind the access mode: a &lt;code&gt;ReadWriteOnce&lt;/code&gt; PVC serves one replica and then silently blocks the second pod from mounting, so anything that scales past a single replica needs &lt;code&gt;ReadWriteMany&lt;/code&gt; (EFS, FSx for Lustre, NFS, CephFS). That RWO-to-RWX switch is a classic thing to discover the first time an autoscale event never becomes a second pod. Some teams ship the model as its own OCI artifact and let the image machinery above handle it (KServe's modelcar pattern). The principle is the same one from part 2: the bandwidth between the weights and the GPU is the bottleneck, so shorten that path.&lt;/p&gt;

&lt;p&gt;The frontier technique skips loading altogether. GPU memory snapshots (Modal and Cerebrium both ship this, built on the CUDA checkpoint/restore API in recent driver branches) checkpoint a fully warmed replica, weights on the card and CUDA graphs already captured, then restore that image straight onto a GPU. Because it bypasses weight load, &lt;code&gt;torch.compile&lt;/code&gt;, and graph capture in one move, it's the only approach that also kills the warmup tax. Modal reports a vLLM Qwen model dropping from 45 seconds to 5, and Cerebrium measured cold starts down 71 to 88 percent. The catch is portability: a snapshot is pinned to a specific GPU model and driver branch, so it's a per-SKU artifact, not a universal one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; Scale-to-zero is a tradeoff, not a free win. Every second of cold start is latency the first user eats; every minute of warm idle is money you burn. You buy down the cold start with lazy image pulls, model streaming, and a warm node pool, then set a minimum replica floor high enough that your p99 cold start stays inside the SLO. "Zero when truly idle, one when it might not be" beats a dogmatic zero.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flv5960rxc07xy5b29x4a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flv5960rxc07xy5b29x4a.png" alt="A diagram mapping each cold-start stage to the fixes that attack it, three columns. Column one 'image pull (minutes)': SOCI / stargz / Nydus lazy pull, pre-bake into the node image, P2P mirror (Spegel), slim the image. Column two 'model load (minutes)': Run:ai Model Streamer, tensorizer, local NVMe or shared PVC cache. Column three 'warmup (tens of seconds)': GPU memory snapshots, limited CUDA graph sizes. A banner across the bottom reads 'you have to attack all three: the slowest unfixed stage sets the wait'." width="800" height="352"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · the fix menu, one column per stage. Snapshots are the only trick in the third column, which is why they're the frontier: everything else leaves the warmup tax standing.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  turning off idle nodes
&lt;/h2&gt;

&lt;p&gt;Scaling pods to zero doesn't save anything if the expensive GPU node they were on keeps running. The node has to go too. This is the autoscaler's job below the pod level: Karpenter consolidates workloads and removes nodes that are empty or underutilized (&lt;code&gt;consolidateAfter&lt;/code&gt;, disruption budgets so it doesn't yank capacity mid-request), and the older cluster-autoscaler scales node groups down on the same principle. A GPU node that's been empty for a few minutes is thirty to a hundred-plus dollars a day; letting it linger is the single most common way GPU bills quietly balloon.&lt;/p&gt;

&lt;p&gt;For predictable traffic, the laziest win is scheduled scaling. If the demo only serves business hours, a cron trigger that scales the floor to zero at 7pm and back to one at 9am captures most of the savings with none of the cold-start risk during the day. And to hide cold starts when you do scale up, keep a warm node in reserve with low-priority placeholder pods (over-provisioning): real work evicts the placeholders instantly and lands on a node that already has the image, so the pod cold start doesn't also pay the node cold start.&lt;/p&gt;

&lt;h2&gt;
  
  
  spot, and the capacity trap
&lt;/h2&gt;

&lt;p&gt;Two closing realities that scale-to-zero runs into. Spot instances make idle capacity cheap, and for inference they can work (unlike the gang-scheduled training from part 3, a single inference replica dying is survivable). But a spot GPU can be reclaimed with about two minutes' notice, so you need a node-termination handler that drains in-flight requests and a plan for where the replacement comes from.&lt;/p&gt;

&lt;p&gt;Which is the trap: scaling up assumes there's a GPU to scale up &lt;em&gt;onto&lt;/em&gt;. In 2026, popular GPUs are not always available on demand, and a scale-to-zero service that can't reacquire an H100 at 9am Monday is worse than one that never scaled down. The mitigations are the capacity blocks and reservations from part 3, an on-demand fallback when spot is dry, and a warm floor for anything with a real SLO. Scale-to-zero is a cost strategy, not a capacity strategy, and confusing the two is how you save money right up until the morning you can't get your GPUs back.&lt;/p&gt;

&lt;p&gt;That's the cost side handled. The fleet scales with demand and turns itself off when it's idle, and the first user back doesn't wait five minutes for the privilege. But there's one lever left, and it's the strangest one, because it costs nothing to pull. Everything so far has quietly assumed that a request, once it arrives, lands on some replica and gets served. Part 6 is about that word "some." It turns out that which replica you pick, out of a pool that all look identical from the outside, can make the exact same hardware more than twice as fast. The load balancer, of all the unglamorous things, is where the last big win hides.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>autoscaling</category>
      <category>kubernetes</category>
      <category>scaletozero</category>
    </item>
    <item>
      <title>What a green GPU dashboard hides</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:14:09 +0000</pubDate>
      <link>https://dev.to/sachincool/what-a-green-gpu-dashboard-hides-33f4</link>
      <guid>https://dev.to/sachincool/what-a-green-gpu-dashboard-hides-33f4</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-4-observability" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-11.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The GPU dashboard was a wall of green. Every card pinned at 90-something percent utilization, power near TDP, temperatures fine, no XID errors. By every signal from part 1 of this series, the box was healthy and working hard. Meanwhile the on-call channel had three messages from product asking why the chatbot took eight seconds to say its first word. The hardware was busy. The users were furious. Both were true.&lt;/p&gt;

&lt;p&gt;That is the trap of monitoring GPU inference with GPU metrics. A card at 92% utilization tells you a kernel is running. It tells you nothing about whether requests are piling up in a queue, whether the KV cache is full and requests are being evicted, or whether the first token is landing in 200 milliseconds or eight seconds. This is part 4 of the series. Parts 1 through 3 built the thing: the stack under a pod, the wires in a box, the network between boxes. This part is about seeing what it's doing once real traffic hits it, and the short version is that the numbers users feel live in the serving engine, not on the GPU.&lt;/p&gt;

&lt;h2&gt;
  
  
  the four numbers users actually feel
&lt;/h2&gt;

&lt;p&gt;Before any dashboard, get the vocabulary straight, because LLM latency is not one number. A request has a shape, and four measurements describe it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TTFT&lt;/strong&gt;, time to first token, is how long the user stares at a blinking cursor before anything appears. It's dominated by prefill (processing the whole prompt) plus however long the request sat in a queue before the server picked it up. This is the number product complains about.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ITL&lt;/strong&gt;, inter-token latency, is the gap between consecutive tokens once generation starts. (Some tools report TPOT, time per output token, the averaged version of the same thing; benchmarks like GuideLLM show both side by side, so don't treat them as interchangeable in a report.) It's what makes text feel like it's streaming smoothly or stuttering out. Decode is memory-bandwidth-bound, so ITL degrades as you pack more concurrent requests onto a GPU.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;End-to-end latency&lt;/strong&gt; is the whole request, first byte to last. For a long generation it's mostly &lt;code&gt;output_tokens × ITL&lt;/code&gt;, which means it's as much about how much the model says as how fast it says it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Throughput&lt;/strong&gt; is output tokens per second across all requests, and it moves in the opposite direction from latency. Batch more requests together and throughput climbs while per-request latency gets worse. There's no single "fast" setting. There's a frontier, and where you sit on it is a product decision (a chatbot wants low TTFT, a batch summarization job wants raw throughput) rather than a tuning default.&lt;/p&gt;

&lt;p&gt;The number that captures both at once is &lt;strong&gt;goodput&lt;/strong&gt;: the requests per second you can serve while still meeting your latency SLO. A server can post gorgeous raw throughput while quietly blowing p99 TTFT for half its users, so goodput (throughput, filtered by "did it meet the SLO") is the only throughput figure worth quoting.&lt;/p&gt;

&lt;h2&gt;
  
  
  what vLLM actually tells you
&lt;/h2&gt;

&lt;p&gt;vLLM exposes a Prometheus &lt;code&gt;/metrics&lt;/code&gt; endpoint, and once you've read it a few times the health of the server is obvious at a glance. The metrics that matter split into two groups: what the queue is doing, and what the KV cache is doing.&lt;/p&gt;

&lt;p&gt;The queue first. &lt;code&gt;vllm:num_requests_running&lt;/code&gt; is how many requests are being served right now; &lt;code&gt;vllm:num_requests_waiting&lt;/code&gt; is how many are stuck in line because the server can't fit them yet. A healthy server has a running count near its batch capacity and a waiting count near zero. When &lt;code&gt;num_requests_waiting&lt;/code&gt; starts climbing and staying up, that eight-second TTFT has arrived, and no GPU metric will show it. &lt;code&gt;vllm:request_queue_time_seconds&lt;/code&gt; measures the wait directly.&lt;/p&gt;

&lt;p&gt;Then the KV cache (the model's running memory of the tokens it has already processed, which grows with every active request and every token generated). &lt;code&gt;vllm:kv_cache_usage_perc&lt;/code&gt; is the fraction of KV cache in use. This is the one to stare at, because when it approaches 100% vLLM has to start &lt;strong&gt;preempting&lt;/strong&gt;: evicting a half-finished request to free memory, then recomputing it from scratch later. &lt;code&gt;vllm:num_preemptions_total&lt;/code&gt; counts that happening. A rising preemption rate means the server is thrashing, doing the same work twice, and every latency number is about to get worse.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp6uak8186dse8mqcxyz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwp6uak8186dse8mqcxyz.png" alt="A side-by-side comparison of two vLLM server states drawn as metric panels. Left panel labeled 'healthy': running requests near batch capacity, waiting requests at zero, KV-cache usage around 60 percent, preemptions flat at zero, p99 TTFT well under the SLO line. Right panel labeled 'drowning': running requests flat at capacity while waiting requests climb, KV-cache usage pinned near 100 percent, preemptions rising, p99 TTFT crossing above the SLO line. A note reads 'the GPU looks equally busy in both; only the serving metrics tell them apart'." width="800" height="408"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · the same server, healthy and drowning. GPU utilization is high in both; the queue, the cache, and the preemption counter are what separate a busy server from a failing one.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rest fill in the picture. &lt;code&gt;vllm:time_to_first_token_seconds&lt;/code&gt; and &lt;code&gt;vllm:inter_token_latency_seconds&lt;/code&gt; are histograms, so you alert on the p95 or p99, not the average (the average hides the user who waited twelve seconds). &lt;code&gt;vllm:prompt_tokens_total&lt;/code&gt; and &lt;code&gt;vllm:generation_tokens_total&lt;/code&gt; give you real throughput, computed with &lt;code&gt;rate()&lt;/code&gt; (vLLM removed its old pre-averaged throughput gauges, so you do the division yourself). And &lt;code&gt;vllm:prefix_cache_hits_total&lt;/code&gt; over &lt;code&gt;vllm:prefix_cache_queries_total&lt;/code&gt; tells you how much prompt reuse you're getting, which matters enormously for the RAG and multi-turn workloads from part 2. It's also the number a smart router watches to decide which replica to send a request to, which is the whole subject of part 6.&lt;/p&gt;

&lt;p&gt;One warning that will save you an afternoon: these names drift between releases. The V1 engine renamed the KV-cache gauge (it used to be &lt;code&gt;gpu_cache_usage_perc&lt;/code&gt;) and swapped the per-output-token metric to &lt;code&gt;inter_token_latency_seconds&lt;/code&gt;. Diff the live &lt;code&gt;/metrics&lt;/code&gt; output of your exact build before you copy anyone's PromQL, this post included.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; GPU utilization tells you the chip is busy; it never tells you users are waiting. The metric that predicts an angry inbox is vllm:num_requests_waiting climbing, usually because vllm:kv_cache_usage_perc hit its ceiling and the server started evicting half-finished requests. Watch the queue and the cache, not GPU-util.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  the same story in SGLang and Triton
&lt;/h2&gt;

&lt;p&gt;The engine changes, the questions don't. SGLang exposes its own Prometheus metrics behind &lt;code&gt;--enable-metrics&lt;/code&gt;: &lt;code&gt;sglang:num_running_reqs&lt;/code&gt; and &lt;code&gt;sglang:num_queue_reqs&lt;/code&gt; are the running-and-waiting pair, &lt;code&gt;sglang:token_usage&lt;/code&gt; is the KV-cache fraction, and &lt;code&gt;sglang:cache_hit_rate&lt;/code&gt; reports how often its RadixAttention prefix cache paid off. (One gotcha of exactly the kind above: SGLang flipped the metric prefix from &lt;code&gt;sglang:&lt;/code&gt; to &lt;code&gt;sglang_&lt;/code&gt; in v0.5.4, and the bundled Grafana dashboard hasn't caught up, so a fresh install can read "No Data" until you fix the prefix.) If you built on SGLang for its prefix-sharing (the reason to pick it in part 2), that last one is how you confirm the bet is paying.&lt;/p&gt;

&lt;p&gt;Triton with the TensorRT-LLM backend reports through Triton's metrics endpoint instead: request and queue durations, inflight-batcher stats, per-model success and failure counts. Different names, same three questions every time: how long are requests waiting, is the cache saturated, is the tail latency inside the SLO.&lt;/p&gt;

&lt;p&gt;The lesson worth internalizing is that these are all the same dashboard with different labels. Queue depth, cache utilization, tail TTFT, tail ITL. Learn to read one engine and you can read all of them.&lt;/p&gt;

&lt;h2&gt;
  
  
  compute-bound, memory-bound, or queue-bound
&lt;/h2&gt;

&lt;p&gt;The reason to keep the GPU metrics from part 1 next to the serving metrics is that together they diagnose &lt;em&gt;why&lt;/em&gt; the server is slow, which is the only thing that tells you what to do about it. Three shapes cover most incidents.&lt;/p&gt;

&lt;p&gt;If TTFT is high and &lt;code&gt;num_requests_waiting&lt;/code&gt; is high but &lt;code&gt;gpu_cache_usage_perc&lt;/code&gt; is low, you're &lt;strong&gt;queue-bound&lt;/strong&gt;: requests are backing up faster than you can start them, and the fix is more replicas (which is part 5). If ITL is degrading and &lt;code&gt;DCGM_FI_PROF_DRAM_ACTIVE&lt;/code&gt; is pinned while tensor activity isn't, you're &lt;strong&gt;memory-bound&lt;/strong&gt; on decode, and the fix is a smaller batch, quantization, or better KV-cache management. If tensor cores are saturated during prefill and DRAM isn't, you're &lt;strong&gt;compute-bound&lt;/strong&gt;, which for inference usually means very long prompts and points at chunked prefill or a prompt-length limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2o5vv2yr459zozg0809.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2o5vv2yr459zozg0809.png" alt="A three-way decision diagram titled 'why is the server slow?'. A central question branches on the combination of serving and GPU metrics into three outcomes. Branch one: high queue (num_requests_waiting up) plus low KV-cache usage routes to 'queue-bound: add replicas'. Branch two: degrading inter-token latency plus DRAM_ACTIVE pinned but low tensor activity routes to 'memory-bound on decode: smaller batch, quantize'. Branch three: tensor cores saturated during prefill routes to 'compute-bound: chunked prefill, cap prompt length'. Each branch pairs a serving metric with a DCGM metric." width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · the serving metric tells you something is wrong; the GPU metric next to it tells you which kind of wrong. You need both panels on the same screen.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Neither set of metrics is enough alone. GPU metrics without serving metrics miss the queue entirely. Serving metrics without GPU metrics can't tell a memory-bound stall from a compute-bound one. The dashboard that works has both, side by side, on one screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  what to alert on
&lt;/h2&gt;

&lt;p&gt;Most teams over-alert on the GPU and under-alert on the experience. The page that matters fires on the user's SLO, not the hardware's vitals. A working starter set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;TTFT p99 over budget&lt;/strong&gt; for N minutes. This is the customer-facing SLO. Everything else is a leading indicator of this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;num_requests_waiting&lt;/code&gt; sustained above zero.&lt;/strong&gt; A brief spike is fine; a standing queue means you're under-provisioned and the next thing to break is TTFT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Preemption rate climbing.&lt;/strong&gt; &lt;code&gt;num_preemptions_total&lt;/code&gt; moving means the KV cache is saturated and the server is recomputing evicted work. It's the early warning before latency falls off a cliff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error rate.&lt;/strong&gt; Request failures and, specifically, CUDA out-of-memory events, which on an inference server usually mean a batch or context-length setting is too aggressive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The part-1 hardware alerts still stand.&lt;/strong&gt; XID errors, thermal throttle, ECC. A dying GPU shows up as latency variance long before it shows up as an error, so keep those wired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tail latency is the whole game here. Alerting on average TTFT is how you find out about an outage from the customer instead of the pager, because the average stays calm while your p99 is on fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  tracing a single slow request
&lt;/h2&gt;

&lt;p&gt;Aggregate metrics tell you the fleet is unhealthy. They don't tell you why &lt;em&gt;this&lt;/em&gt; request took nine seconds. For that you want per-request tracing, and the ecosystem has standardized on OpenTelemetry's GenAI semantic conventions: spans carry &lt;code&gt;gen_ai.*&lt;/code&gt; attributes (the model, input and output token counts, the request parameters) so a single request's journey through the gateway, the queue, prefill, and decode is one connected trace. When a specific user reports a slow response, a trace tells you whether it sat in a queue, hit a cache miss, or just asked for a 4,000-token essay. The metrics say the kitchen is slow; the trace shows you which order got lost.&lt;/p&gt;

&lt;h2&gt;
  
  
  proving it before prod
&lt;/h2&gt;

&lt;p&gt;You don't want to discover your latency frontier during a launch. Load-test the serving endpoint before it sees real traffic, with a tool that speaks LLM rather than plain HTTP. vLLM ships &lt;code&gt;vllm bench serve&lt;/code&gt; (the old &lt;code&gt;benchmark_serving.py&lt;/code&gt;), which replays a request distribution, reports TTFT, ITL, and throughput percentiles, and computes goodput directly if you hand it SLO thresholds. GuideLLM (now a Red Hat project) does the same with a &lt;code&gt;sweep&lt;/code&gt; mode that finds your safe operating range on its own; NVIDIA's genai-perf covers the Triton side, though NVIDIA is steering new work to its successor AIPerf; and the Kubernetes serving working group's inference-perf standardizes the numbers across engines so you can compare vLLM to SGLang honestly. Whatever you pick, the output you care about is the same: the curve of tail latency against offered load, and the load at which p99 TTFT crosses your SLO. That crossover is the goodput ceiling, the real per-replica capacity, and it's the input to everything in part 5.&lt;/p&gt;

&lt;p&gt;Because that's the thing this post sets up. Once you can see the queue building and the cache saturating, the obvious next question is: why am I staring at these graphs manually at 2am instead of having the queue depth add a replica by itself, and drop it again when the traffic goes home. That's scaling, and scaling GPUs that cost six dollars an hour is its own kind of problem.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>observability</category>
      <category>vllm</category>
      <category>prometheus</category>
    </item>
    <item>
      <title>Scaling GPUs past one box</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:14:04 +0000</pubDate>
      <link>https://dev.to/sachincool/scaling-gpus-past-one-box-1k62</link>
      <guid>https://dev.to/sachincool/scaling-gpus-past-one-box-1k62</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-3-scaling-out" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-09.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;When Meta trained Llama 3 405B on 16,384 H100s, the cluster hit 419 unexpected interruptions over 54 days. That's one failure every three hours or so, for 54 days straight, and about 78% of them were hardware. GPUs and their HBM3 memory accounted for roughly half. They also watched the datacenter's power draw swing by tens of megawatts as thousands of GPUs idled and resumed in sync, which is a sentence that should make any infrastructure engineer sit up.&lt;/p&gt;

&lt;p&gt;This is the part of the series where the GPU stops being the interesting component. Part 1 was the stack under one pod, part 2 was one box and the wires inside it. Once you cross the node boundary, the network becomes the machine, failures become continuous rather than exceptional, and the scheduler decides whether your very expensive cluster does useful work or deadlocks against itself. Everything here is about the stuff between the boxes.&lt;/p&gt;

&lt;h2&gt;
  
  
  the network is the machine
&lt;/h2&gt;

&lt;p&gt;Synchronous training does an all-reduce of the entire gradient (every parameter, billions of them) every single step. That collective is a hard barrier: the slowest link gates every GPU in the job. Add nodes and your compute scales, but the all-reduce volume grows and so does the chance that one link is slow or dead. This is why, past one node, communication rather than FLOPs sets your scaling efficiency, and why the network gear costs as much attention as the GPUs.&lt;/p&gt;

&lt;p&gt;The fabric itself is a two-horse race. InfiniBand is the incumbent for dedicated training superclusters: the generation ladder runs EDR 100Gb, HDR 200Gb, NDR 400Gb (Quantum-2 switches, ConnectX-7 NICs), and now XDR 800Gb (Quantum-X800, ConnectX-8). Most DGX SuperPODs and the big named clusters run it. RoCE v2, RDMA over Ethernet, is winning share on cost and on letting existing Ethernet teams reuse what they know. The catch with RoCE is that it needs a carefully tuned lossless fabric (Priority Flow Control plus ECN marking) or you get congestion storms, and getting that right at scale is its own discipline.&lt;/p&gt;

&lt;p&gt;Meta is the proof that Ethernet can do it. They built two 24,576-GPU H100 clusters, one on RoCE and one on Quantum-2 InfiniBand, and trained Llama 3 405B on the RoCE one with no network bottleneck, after co-designing the topology, the PFC/ECN thresholds, and an all-reduce-aware load balancer. That's the honest framing: RoCE works at scale, but Meta spent real engineering to make it work. Two more pieces earn their keep on either fabric. GPUDirect RDMA lets the NIC DMA straight into GPU memory, skipping a bounce through host RAM, and without it every hop stages through system memory. SHARP does the reduction inside the switch ASIC, so gradients get summed in the network instead of shuttled between every node, which on the newest Blackwell fabrics is a large multiplier on effective all-reduce bandwidth.&lt;/p&gt;

&lt;h2&gt;
  
  
  NCCL across the wire
&lt;/h2&gt;

&lt;p&gt;The same NCCL from part 2 handles inter-node collectives, and the failure mode here is specific and common: NCCL silently falls back to TCP sockets when it can't find or use the RDMA path, and the job "works" while running an order of magnitude too slow. The env vars that prevent that are worth pinning in your launcher.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;NCCL_IB_HCA&lt;/code&gt; names which RDMA NICs to use, and getting it wrong means NCCL picks one NIC and loses your rail parallelism. &lt;code&gt;NCCL_SOCKET_IFNAME&lt;/code&gt; has to point at the real data-plane interface, not &lt;code&gt;eth0&lt;/code&gt; or &lt;code&gt;lo&lt;/code&gt;, a classic container and Kubernetes trap. &lt;code&gt;NCCL_CROSS_NIC=0&lt;/code&gt; on a rail-optimized fabric keeps a ring on the same rail instead of hopping across them. &lt;code&gt;NCCL_IB_GID_INDEX&lt;/code&gt; is the RoCE gotcha: the wrong GID index gives you no traffic or a silent slow path. On a fresh cluster the first all-reduce is routinely two to ten times slower than optimal until the env vars, the GID index, the PFC/ECN config, and the topology file are all correct. The bring-up ritual is always &lt;code&gt;nccl-tests&lt;/code&gt;: run &lt;code&gt;all_reduce_perf&lt;/code&gt;, measure the achieved bus bandwidth, compare it against what 400 or 800 Gb should give you, and don't trust the cluster until the number is close.&lt;/p&gt;

&lt;h2&gt;
  
  
  who schedules the gang
&lt;/h2&gt;

&lt;p&gt;Here's the failure that surprises people coming from web infrastructure. Vanilla Kubernetes schedules pods independently, one at a time, with no concept of a job that needs all its pods at once. Give it a 4-node training job and it will happily place 3 pods and leave the 4th Pending forever, holding three nodes of GPUs idle. Run two such jobs and they can each grab most of what the other needs and starve each other indefinitely. Distributed training is all-or-nothing, and a scheduler that doesn't know that will deadlock your cluster. The first time it happens you assume you're out of GPUs. You're not. They're all sitting idle, reserved by pods that will never get their partners.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rddlgo5lqckjzzgx24u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0rddlgo5lqckjzzgx24u.png" alt="A two-panel diagram titled 'why training needs gang scheduling'. Left panel, labeled 'vanilla Kubernetes': a four-pod job where three pods are Running on GPU nodes and one pod is stuck Pending, with the three running pods marked as holding GPUs idle while waiting, and a red 'deadlock' label. Right panel, labeled 'gang scheduling': the same four-pod job where all four pods are admitted together as one unit or none at all, marked 'all-or-nothing placement, no partial allocation'." width="800" height="401"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · gang scheduling in one picture. The left side is how a cluster quietly wedges itself; the right side is the fix, and the reason every GPU scheduler below exists.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The fix is gang scheduling: admit all N pods together or none, so partial allocations can't happen. The tools that provide it each have an honest drawback:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Slurm&lt;/strong&gt; is the HPC default and has gang scheduling and topology awareness built in, plus new block scheduling for aligning jobs to NVL72 racks. Its weakness is that containers and multitenancy are bolted on (Pyxis plus Enroot), and it's a poor fit for long-running inference services. That gap is why Slurm-on-Kubernetes projects like SchedMD's Slinky and CoreWeave's SUNK exist.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kubernetes vanilla&lt;/strong&gt; has no gang scheduling and no topology awareness by default. You add a batch scheduler on top; you don't run training on the default scheduler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volcano&lt;/strong&gt; is the de facto CNCF choice: gang scheduling via PodGroups, queues, fair-share. It runs as a second scheduler that bypasses the default, which complicates coexistence with normally-scheduled workloads, and gang scheduling itself costs maybe 10–15% utilization because resources sit idle waiting for the full gang.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kueue&lt;/strong&gt; is the Kubernetes-native answer for queueing and quota, and it cooperates with the default scheduler instead of replacing it. The tradeoff is that it does admission and quota, not fine-grained placement, so you still need scheduler plugins underneath for the actual gang and topology binding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run:ai&lt;/strong&gt; is the commercial option, now NVIDIA-owned, with fractional GPU and pooling. NVIDIA open-sourced the core as KAI Scheduler (Apache 2.0, now CNCF Sandbox), so a free path exists, but the full enterprise feature set stays paid and KAI is young as a standalone project.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;YuniKorn&lt;/strong&gt; brings strong hierarchical-queue multitenancy from the Spark world, at the cost of being another full scheduler replacement with a smaller AI-specific ecosystem than Volcano.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cross-cutting truth is that gang scheduling trades utilization for progress. Holding GPUs idle while you wait for the full gang is the price of not deadlocking, and it's a price worth paying.&lt;/p&gt;

&lt;h2&gt;
  
  
  splitting the model
&lt;/h2&gt;

&lt;p&gt;When a model outgrows one GPU, there are three axes to split it on, and real training combines them. Data parallelism replicates the whole model on each GPU and all-reduces gradients; it's the simplest and only works when the model plus its optimizer states fit on one card. Tensor parallelism splits individual matrix multiplies across GPUs and is communication-heavy, so you keep it inside a node on NVLink (TP=8 is the common ceiling). Pipeline parallelism cuts the layers into stages across nodes and passes activations point-to-point, which is cheap enough to cross the network.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe02c72a24kliq9gyg02k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fe02c72a24kliq9gyg02k.png" alt="A diagram titled '3D parallelism: how a frontier model maps to a cluster' showing a grid of GPU nodes. Tensor parallelism is shown splitting a single layer across the eight GPUs within one node, connected by NVLink and labeled 'TP=8, stays inside the node'. Pipeline parallelism is shown splitting the model's layers into stages across several nodes, labeled 'PP across nodes, cheap point-to-point'. Data parallelism is shown replicating the whole arrangement across groups of nodes with an all-reduce between replicas, labeled 'DP / FSDP on top'." width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · the standard frontier recipe: tensor-parallel inside the NVLink domain, pipeline-parallel across nodes, data-parallel on top. Match each split to the bandwidth it can afford.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For the common case of "my model doesn't fit but I want to stay in native PyTorch," FSDP2 shards the parameters, gradients, and optimizer states across GPUs and reconstructs each layer on the fly via all-gather, prefetching the next shard to overlap communication with compute. DeepSpeed's ZeRO does the same idea in stages: stage 1 shards optimizer states, stage 2 adds gradients, stage 3 adds parameters and is functionally equivalent to FSDP. For frontier scale and maximum MFU you reach for Megatron-Core and combine tensor, pipeline, and sequence parallelism into the 3D (now 4D, with expert parallelism for MoE) recipe: TP=8 inside the node, pipeline across nodes, data parallelism on top.&lt;/p&gt;

&lt;p&gt;Checkpointing is the reliability workhorse and used to be the tax that made frequent saves unaffordable. Async distributed checkpointing fixed that by writing state in a background thread that overlaps the next iterations; TorchTitan reports 5–15x lower checkpoint overhead than synchronous saves. That matters directly because of the failure rate: at one interruption every three hours, you want to checkpoint on the order of tens of minutes, and torchrun's elastic mode restarts the job from the last snapshot when a node dies. The newer torchft goes further, recovering a failed replica from a healthy peer without restarting the whole job.&lt;/p&gt;

&lt;h2&gt;
  
  
  when the model won't fit one node
&lt;/h2&gt;

&lt;p&gt;Serving crosses the node boundary for the same reason training does: the model, or its KV cache, exceeds one node's total HBM. DeepSeek-V3 at 671B, Llama 405B, the big mixture-of-experts (MoE) models. You split them with tensor and pipeline parallelism across nodes, and for MoE you add wide expert parallelism, spreading experts across many nodes so each GPU holds few experts but sees a large batch per expert.&lt;/p&gt;

&lt;p&gt;The pattern that's become standard is disaggregated prefill and decode. Prefill (processing the prompt) is compute-bound; decode (generating tokens one at a time) is memory-bandwidth-bound. Running them in one pool means prefill work stalls decode latency. Splitting them into separate worker pools lets you scale each for its own bottleneck and transfer the KV cache between them over RDMA. It isn't a free win, though. Moving the KV cache between pools costs bandwidth, so disaggregation pays off when prefill interference is genuinely the bottleneck (long prompts, high concurrency) and can be net-negative when it isn't. DeepSeek runs it because at their scale it clearly is; a chatbot with short prompts might not need it at all.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi9rekym1jlc5imdiols8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi9rekym1jlc5imdiols8.png" alt="A diagram titled 'disaggregated prefill and decode' showing an inference request flowing left to right. The request first hits a pool of prefill workers, labeled 'compute-bound: process the whole prompt', drawn as a small cluster of GPUs. The resulting KV cache is transferred over RDMA to a separate, larger pool of decode workers, labeled 'memory-bandwidth-bound: generate tokens one at a time'. An annotation notes 'scale each pool independently; prefill no longer stalls decode', with a footnote citing DeepSeek-V3's production split of a 32-GPU prefill unit feeding a 320-GPU decode pool." width="800" height="317"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 4 · prefill and decode want different hardware ratios, so modern stacks run them as separate pools and ship the KV cache between them. DeepSeek-V3 runs a 32-GPU prefill unit in front of a 320-GPU decode pool.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;DeepSeek's own deployment runs a 32-GPU prefill unit (4 nodes, expert-parallel across 32) feeding a much larger decode pool, and an LMSYS reproduction on 96 H100s hit 52,000 input and 22,000 output tokens per second per node. On Kubernetes the primitive that expresses "this is one model replica made of many pods" is LeaderWorkerSet: one leader, N workers, scheduled and scaled as a unit, which is exactly what gang scheduling and topology-aware placement need to bite on. NVIDIA Dynamo and llm-d sit on top: Dynamo for distributed serving with a KV-cache-aware router, and llm-d for KV-cache-aware routing on the Gateway API Inference Extension. That routing layer turns out to be one of the biggest free wins in the whole stack, which is why it gets its own part 6.&lt;/p&gt;

&lt;h2&gt;
  
  
  everything fails at scale
&lt;/h2&gt;

&lt;p&gt;Reliability stops being a checkbox and becomes the main event. The Llama 3 numbers from the top of this post are the reference point, and ByteDance's MegaScale run on 12,288 GPUs tells the same story: 55.2% MFU and more than a hundred failure-recovery events over a few weeks. The failure you can't see coming is silent data corruption, where a GPU computes a wrong number without erroring. It doesn't crash or log anything. It just hands back the wrong answer, and your loss curve grows a mysterious kink a few hours later. Meta caught six such events in 54 days; Google reports SDC-related disruptions roughly every one to two weeks. A single corrupted gradient contaminates the global update across every worker, and it's now a first-class reliability topic with its own whitepapers.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; At cluster scale, hardware failure is the normal state, not an exception you engineer away. A 16k-GPU run loses a GPU every few hours. You can't stop that, so the whole game is checkpointing often enough and keeping enough hot spares that a dead node costs you minutes instead of the whole run.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The straggler is the SDC's cousin: a GPU or link that's degraded but not dead, quietly throttling a synchronous job because collectives move at the speed of the slowest member. Detecting it at scale is genuinely hard, so systems run periodic self-check diagnostics that pause the job, measure NVLink and compute per node, diagnose, and resume from checkpoint. Imbue open-sourced their bare-metal playbook for a 4,088-H100 cluster: check VBIOS and baseboard firmware, the Mellanox OFED stack, PCIe link speed and width, then run matmuls to measure actual NVLink bandwidth, cordon anything that fails, and swap in a hot spare automatically. That last part is the operating model at scale. You don't fix nodes in the critical path; you drain them and pull from a spare pool, because the cluster is always partially broken and the job has to keep moving.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpz819r7930rluh6gtasw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpz819r7930rluh6gtasw.png" alt="A donut chart infographic titled 'Llama 3 405B: what interrupted 54 days of training' showing the breakdown of 419 unexpected interruptions. Segments: GPU faults 30.1 percent, HBM3 memory 17.2 percent, network switch and cable 8.4 percent, GPU SRAM 4.5 percent, GPU processor 4.1 percent, and other or software causes making up the remainder. A center label reads '419 interruptions, ~1 every 3 hours, ~78% hardware'." width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 5 · the failure breakdown from Meta's Llama 3 405B run. GPUs and their memory are roughly half; the rest is the long tail an at-scale operator plans around, not against.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  paying for it
&lt;/h2&gt;

&lt;p&gt;The economics are why all of the above matters. At $2–6 per GPU-hour, a 16,000-GPU cluster idling during a recovery burns thousands of dollars a minute, and gang scheduling means one bad node can idle the whole job. That's the real argument for the reliability engineering: not uptime for its own sake, but goodput, the useful training throughput net of failures and restarts.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewk7rics5svj3grlgx2c.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fewk7rics5svj3grlgx2c.png" alt="A horizontal bar chart infographic titled 'GPU cost per hour, on-demand ballpark (2026)' comparing neocloud versus hyperscaler pricing for three GPUs. H100 SXM: neocloud about 2.5 to 3.5 dollars, AWS about 6.88 dollars. H200: neocloud about 3.8 to 4 dollars, Azure about 10 to 13 dollars. B200: neocloud about 5 to 6 dollars, AWS about 14.24 dollars. A note reads 'reserved commits cut 16 to 40 percent; spot is 30 to 70 percent cheaper but nearly unusable for gang-scheduled training'." width="800" height="472"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 6 · the on-demand spread between neoclouds and hyperscalers is wide, and it moves monthly. Big training runs almost never pay on-demand; they live on reserved capacity or capacity blocks.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The pricing spread is wide and moves every month. Neoclouds like Lambda, CoreWeave, and Nebius run an H100 around $2.5–3.5 per hour; AWS lists closer to $6.88. B200s are $5–6 on neoclouds and north of $14 on AWS. Reserved commitments of one to twelve months cut 16–40% off on-demand, and that's where most large training capacity actually lives. Spot is 30–70% cheaper and nearly unusable for gang-scheduled training, because losing any one node preempts the whole synchronous job and reacquiring N contiguous, topology-aligned nodes on spot is a fantasy. That gap is why capacity blocks exist: AWS EC2 Capacity Blocks and GCP's Dynamic Workload Scheduler let you reserve co-located GPUs for a fixed window, booked weeks ahead, because on-demand can't guarantee the topology and reserved is too long a commit for one run. AWS raised those block prices about 15% in early 2026, which tells you which way demand is going.&lt;/p&gt;

&lt;p&gt;That's the whole arc of building this stuff. A GPU deployment is not a GPU. It's a dozen layers under one pod, a small network inside one box, and a large one between boxes, and the interesting failures always live in the wiring rather than the silicon. Meta's cluster lost a GPU every three hours and still trained a frontier model, because the whole apparatus around the GPUs (the fabric, the scheduler, the checkpointing, the spare pool) was built to keep moving while parts of it were on fire. That's how you build it. The next part is how you watch it once real traffic arrives, which turns out to be a different problem than watching the GPUs.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>distributedtraining</category>
      <category>infiniband</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>One box, eight GPUs, and the wires between them</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:13:24 +0000</pubDate>
      <link>https://dev.to/sachincool/one-box-eight-gpus-and-the-wires-between-them-57kl</link>
      <guid>https://dev.to/sachincool/one-box-eight-gpus-and-the-wires-between-them-57kl</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-2-single-node-multi-gpu" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-04.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;We bought two boxes that were supposed to be identical. Eight H100s each, same rack, same image. On one of them a tensor-parallel serve of Llama-3-70B did about 3,000 tokens a second. On the other it did 900, with every GPU pinned at high utilization the whole time. Same model, same code, same card count. The difference was a PCIe switch and a BIOS setting nobody had checked, and it took most of a day to find because every dashboard said both boxes were healthy.&lt;/p&gt;

&lt;p&gt;That's the thing about a multi-GPU box. It looks like a bag of eight GPUs. It behaves like a small, opinionated network, and the wiring between the cards matters more than the cards. This is part 2 of the series. Part 1 was the twelve-layer stack under a single GPU pod. This one stays inside one chassis: how the GPUs talk, how to read the topology, why NCCL is slow, and how a 70B model actually lands on the hardware. Part 3 leaves the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  the box is a network, not a bag of GPUs
&lt;/h2&gt;

&lt;p&gt;The first question about any multi-GPU server is how the GPUs are wired, because that sets a hard ceiling on everything above it. There are two very different animals sold as "8-GPU servers."&lt;/p&gt;

&lt;p&gt;An HGX or DGX baseboard wires all eight GPUs through a bank of NVSwitches. Every GPU reaches every other GPU at the full NVLink rate, non-blocking. On H100 and H200 that's NVLink 4 at 900 GB/s per GPU. On B200 it's NVLink 5 at 1.8 TB/s. That flat, full-bandwidth mesh is the reason you can split a model eight ways and have the halves talk fast enough to keep up.&lt;/p&gt;

&lt;p&gt;A cheaper "8x PCIe" box has no NVSwitch. The GPUs hang off PCIe switches and the CPU root complexes, and GPU-to-GPU traffic crawls through PCIe, often routed up through the CPU. PCIe Gen5 x16 is about 128 GB/s, Gen4 about 64. NVLink 4 is roughly seven times faster than Gen5 and fourteen times faster than Gen4. That gap is the entire reason tensor parallelism cares about your topology. The two "identical" boxes in the opening weren't identical: one had the NVSwitch mesh, the other routed two of its GPU pairs across a PCIe switch with a BIOS feature quietly strangling them.&lt;/p&gt;

&lt;p&gt;One units trap that trips up everyone reading spec sheets: NVIDIA quotes NVLink bandwidth bidirectionally. A100's "600 GB/s" is 300 each way. Pick a convention, state it once, and don't compare someone's unidirectional number to your bidirectional one.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; Two servers with the same eight GPUs can differ by 3× on the same job. Before you promise a throughput number, run nvidia-smi topo -m and confirm the GPUs talk over NVLink (the NV-prefixed rows), not over PCIe routed through the CPU (the SYS rows).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5w0fbydxm2xr3za8ebg7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5w0fbydxm2xr3za8ebg7.png" alt="A horizontal bar chart infographic titled 'GPU-to-GPU bandwidth, per GPU' comparing five interconnects. Bars from shortest to longest: PCIe Gen4 x16 at about 64 GB/s, PCIe Gen5 x16 at about 128 GB/s, NVLink 3 (A100) at 600 GB/s, NVLink 4 (H100/H200) at 900 GB/s, NVLink 5 (B200) at 1800 GB/s. A note reads 'NVLink 4 is roughly 7x PCIe Gen5, which is why tensor parallelism wants NVLink.'" width="800" height="418"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · bandwidth per GPU across the interconnects you'll actually meet. The jump from PCIe to NVLink is the one that decides whether a split model keeps up with itself.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  reading nvidia-smi topo -m
&lt;/h2&gt;

&lt;p&gt;You don't have to guess at any of this. &lt;code&gt;nvidia-smi topo -m&lt;/code&gt; prints the whole connection matrix, and learning to read it is the single most useful GPU-ops skill after &lt;code&gt;nvidia-smi&lt;/code&gt; itself. Every cell tells you how one GPU reaches another, and the symbols form a quality ladder from best to worst:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NV#&lt;/code&gt;: connected by # bonded NVLinks. Best. &lt;code&gt;NV18&lt;/code&gt; means eighteen links, full H100 mesh.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PIX&lt;/code&gt;: a single PCIe bridge, same switch. Fine.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PXB&lt;/code&gt;: multiple PCIe bridges, but not across the CPU host bridge.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PHB&lt;/code&gt;: crosses a PCIe host bridge, through the CPU, same NUMA node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NODE&lt;/code&gt;: crosses host bridges within a NUMA node.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SYS&lt;/code&gt;: crosses the inter-socket link between CPUs. Worst. This is CPU-to-CPU-to-GPU.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the GPUs in your tensor-parallel group show &lt;code&gt;NV18&lt;/code&gt; to each other, you're golden. If any pair shows &lt;code&gt;SYS&lt;/code&gt;, your collective operations are dragging across the socket interconnect and you've found your 900-tokens-a-second box. The same matrix has a column for GPU-to-NIC affinity, which matters enormously for the multi-node story in part 3: you want the NIC on the same PCIe complex as the GPU it feeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  NCCL picks a road
&lt;/h2&gt;

&lt;p&gt;Every framework that splits work across GPUs (PyTorch DDP and FSDP, DeepSpeed, Megatron, the tensor-parallel path in vLLM) does its cross-GPU communication through NCCL, NVIDIA's collectives library. NCCL is where "the GPUs need to agree on a number" turns into actual bytes on actual wires, and it auto-picks the road.&lt;/p&gt;

&lt;p&gt;Inside one box it prefers, in order: peer-to-peer over NVLink (best), peer-to-peer over PCIe, shared host memory (staged through RAM), then network sockets (worst, and a sign something's misconfigured). When an all-reduce (the step where every GPU merges its numbers with all the others and ends up with the combined result) is slow, the debugging loop is almost always the same handful of moves:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. what did NCCL actually choose?&lt;/span&gt;
&lt;span class="nv"&gt;NCCL_DEBUG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;INFO python train.py 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'via|transport|channel'&lt;/span&gt;
&lt;span class="c"&gt;#   "via P2P/direct pointer" = good. "via SHM" or "via NET/Socket" intra-node = bad.&lt;/span&gt;

&lt;span class="c"&gt;# 2. confirm the GPUs are NVLinked, not routed over SYS&lt;/span&gt;
nvidia-smi topo &lt;span class="nt"&gt;-m&lt;/span&gt;

&lt;span class="c"&gt;# 3. benchmark against the ceiling&lt;/span&gt;
all_reduce_perf &lt;span class="nt"&gt;-b&lt;/span&gt; 8 &lt;span class="nt"&gt;-e&lt;/span&gt; 4G &lt;span class="nt"&gt;-f&lt;/span&gt; 2 &lt;span class="nt"&gt;-g&lt;/span&gt; 8   &lt;span class="c"&gt;# from nccl-tests&lt;/span&gt;

&lt;span class="c"&gt;# 4. if a hang clears when you disable P2P, you have an ACS/IOMMU problem&lt;/span&gt;
&lt;span class="nv"&gt;NCCL_P2P_DISABLE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 python train.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The env vars worth knowing are few. &lt;code&gt;NCCL_DEBUG=INFO&lt;/code&gt; tells you what topology and transport NCCL chose. &lt;code&gt;NCCL_P2P_LEVEL&lt;/code&gt; and &lt;code&gt;NCCL_P2P_DISABLE&lt;/code&gt; control peer-to-peer. &lt;code&gt;NCCL_SOCKET_IFNAME&lt;/code&gt; picks the bootstrap interface, and pointing it at the wrong one (&lt;code&gt;lo&lt;/code&gt;, &lt;code&gt;docker0&lt;/code&gt;) is a classic way to make init hang. &lt;code&gt;NCCL_TOPO_FILE&lt;/code&gt; lets you hand NCCL a topology description, which you sometimes need on cloud VMs because virtualized PCI hides the real affinity and NCCL guesses wrong. On a healthy 8-GPU NVSwitch box you usually touch none of these and it just works. The trouble starts on anything cheaper or virtualized.&lt;/p&gt;

&lt;h2&gt;
  
  
  the invisible tax: NUMA and ACS
&lt;/h2&gt;

&lt;p&gt;Two settings below the framework quietly decide whether your bandwidth numbers are real, and neither shows up in a GPU dashboard.&lt;/p&gt;

&lt;p&gt;The first is NUMA pinning. A multi-socket server splits its PCIe lanes and RAM between CPU sockets, and each GPU is physically wired to one socket. Run your process on socket 0 while it drives a GPU hung off socket 1, and every host-to-device copy crosses the inter-socket link. NCCL's low-latency protocol stages data through a pinned CPU buffer, so this hits communication, not just data loading. The fix is a one-liner: &lt;code&gt;numactl --cpunodebind=0 --membind=0 &amp;lt;cmd&amp;gt;&lt;/code&gt;, matched to the socket that owns the GPU. &lt;code&gt;nvidia-smi topo -m&lt;/code&gt; prints the affinity so you know which socket that is. It's genuinely deflating to spend an afternoon profiling and find the answer was a &lt;code&gt;numactl&lt;/code&gt; prefix, but that's most of this job.&lt;/p&gt;

&lt;p&gt;The second is PCIe ACS, Access Control Services, and it's the one that cost us most of that day. ACS forces PCIe peer-to-peer transactions to route up through the CPU root complex so the platform can police them. That defeats direct GPU-to-GPU DMA across a PCIe switch: latency climbs, throughput collapses, and NCCL can hang outright. ACS has to be off for peer-to-peer to work across a switch. You check it with &lt;code&gt;lspci -vvv | grep -i acsctl&lt;/code&gt; and disable it in BIOS or with a &lt;code&gt;setpci&lt;/code&gt; loop over the bridges. Its cousin, the IOMMU, does the same routing-through-the-root-complex thing, which is exactly why passthrough GPUs on cloud VMs often show degraded peer-to-peer: the isolation that makes virtualization safe is the isolation that makes GPUDirect slow. If &lt;code&gt;nvidia-smi&lt;/code&gt; insists your GPUs "are not P2P capable," the shortlist is ACS enabled, IOMMU on, consumer cards, or GPUs on different root complexes. &lt;code&gt;p2pBandwidthLatencyTest&lt;/code&gt; from the CUDA samples confirms which.&lt;/p&gt;

&lt;h2&gt;
  
  
  fitting a 70B model on the box
&lt;/h2&gt;

&lt;p&gt;The practical question most teams actually have is: how many GPUs does my model need, and how do I split it. Tensor parallelism splits every layer's weight matrices across GPUs, which means every token, at every layer, triggers a collective to recombine the partial results. That makes TP communication-bound and latency-sensitive, which is the real reason it wants NVLink. On a PCIe-only box, tensor parallelism is frequently slower than just pipelining the layers, and vLLM's own guidance says as much: no NVLink, prefer &lt;code&gt;--pipeline-parallel-size&lt;/code&gt; over &lt;code&gt;--tensor-parallel-size&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The memory math for Llama-3-70B is a worked example worth carrying around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Weights: 70B params times 2 bytes for BF16 is about 140 GB. That already doesn't fit on one 80GB card.&lt;/li&gt;
&lt;li&gt;KV cache (the model's running memory of the tokens it has already processed): roughly 2.5 GB per sequence at 8K context, climbing to tens of GB at 128K. This is what eats whatever VRAM the weights left behind and sets your max batch size.&lt;/li&gt;
&lt;li&gt;Real footprint with activations and framework overhead lands north of 200 GB.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So Llama-3-70B at BF16 wants two H100 80GB cards (&lt;code&gt;--tensor-parallel-size 2&lt;/code&gt;), or four A100 40GB, or a fistful of 24GB cards. Tensor parallelism splits the KV cache too, so two GPUs buys you roughly double the batch headroom, not just room for the weights. Quantize to FP8 on Hopper or Blackwell, or INT4 with AWQ, and the weights roughly halve or quarter, which can drop a 70B onto a single card at some quality cost. You can quantize the KV cache too (&lt;code&gt;--kv-cache-dtype=fp8_e5m2&lt;/code&gt;), which shrinks the biggest consumer of leftover VRAM once batch sizes climb. And a rule worth internalizing: tensor-parallel a model only when it genuinely doesn't fit one GPU. For a model that does fit, run several replicas at &lt;code&gt;--tensor-parallel-size 1&lt;/code&gt; instead, because TP's per-layer communication is pure overhead you're paying for nothing when one card already holds the whole model. The constraint people forget: your TP size has to divide the number of attention heads evenly, so you don't get to pick arbitrary GPU counts, and on Kubernetes it also has to equal the pod's &lt;code&gt;nvidia.com/gpu&lt;/code&gt; limit or the server won't start. (You learn this the moment &lt;code&gt;--tensor-parallel-size 6&lt;/code&gt; refuses to start and the error message does nothing to help.)&lt;/p&gt;

&lt;h2&gt;
  
  
  the caveats that page you
&lt;/h2&gt;

&lt;p&gt;Dense GPU boxes fail in ways that a single card never does, and most of them present as "one GPU is a little slow" rather than a clean error.&lt;/p&gt;

&lt;p&gt;Thermal throttling is first. Eight cards share airflow, and a fully loaded chassis runs hot. Data-center GPUs start clocking down before about 85°C, and &lt;code&gt;nvidia-smi --query-gpu=clocks_throttle_reasons.active&lt;/code&gt; will tell you whether it's a software thermal slowdown, a hardware one, or a power cap. Power capping is the sibling: &lt;code&gt;nvidia-smi -pl&lt;/code&gt; sets a limit below the card's TDP (700W on an H100 SXM), and a rack without enough power budget will cap every card and slow the whole box uniformly.&lt;/p&gt;

&lt;p&gt;Then the straggler problem, which is the nastiest because it hides. Collective operations synchronize every step, so the slowest GPU sets the pace for all of them. One card that's throttling, power-capped, or quietly degraded drags the entire tensor-parallel group, and the symptom is maddening: every GPU shows high utilization, throughput is low, and nothing errors. You find it by looking at per-GPU clocks and temperature and hunting the outlier.&lt;/p&gt;

&lt;p&gt;The rest of the list is worth a scan before you sign off on a box:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PCIe lane starvation. A card silently negotiating Gen3 x4 instead of Gen5 x16 is pure mystery slowness. &lt;code&gt;lspci -vv&lt;/code&gt; shows the negotiated &lt;code&gt;LnkSta&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;NVLink errors. &lt;code&gt;nvidia-smi nvlink -e&lt;/code&gt; shows CRC and replay counters. Rising counts mean a flaky link or cable and degraded bandwidth.&lt;/li&gt;
&lt;li&gt;Oversubscribed PCIe switch. Cheap boxes put several GPUs behind one switch uplink, and they contend for it.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;/dev/shm&lt;/code&gt; trap, if you're on Kubernetes. NCCL stages some intra-node transfers through shared memory, and a container's default 64 MiB &lt;code&gt;/dev/shm&lt;/code&gt; hangs multi-GPU serving with no useful error. Mount an &lt;code&gt;emptyDir&lt;/code&gt; with &lt;code&gt;medium: Memory&lt;/code&gt; at &lt;code&gt;/dev/shm&lt;/code&gt;. It's in every production vLLM manifest, and it's why tensor-parallel works on a bare VM but hangs in a pod.&lt;/li&gt;
&lt;li&gt;Xid 79, the "fell off the bus" from part 1, shows up here too when a card overheats or loses power delivery under a full load it never saw in acceptance testing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  picking an inference engine
&lt;/h2&gt;

&lt;p&gt;If the box is for serving rather than training, the engine choice matters, and by 2026 the field has settled. All the live engines converge on the same two tricks: continuous batching (decide the batch membership every decode step so the GPU never idles on the slowest request) and paged KV cache (manage the cache like OS virtual memory so you don't waste most of your VRAM on fragmentation). The differences are in the scheduler and the compile strategy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzv69e4uy5rl0ceofgpw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftzv69e4uy5rl0ceofgpw.png" alt="A two-by-two quadrant diagram titled 'choosing a single-node inference engine'. Axes are ease of operations (horizontal) versus raw performance on NVIDIA hardware (vertical). vLLM sits high on ease and solid on performance, labeled 'the default: broad model + hardware coverage, sane defaults'. TensorRT-LLM sits highest on performance but lower on ease, labeled 'max throughput on NVIDIA, but you compile an engine per model'. SGLang sits mid-high on both, labeled 'RadixAttention: wins on shared-prefix, RAG, agents, MoE'. TGI sits low, greyed out, labeled 'maintenance mode: HuggingFace now points you to vLLM or SGLang'." width="800" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · where the four engines land. Start at vLLM; move only when a profiled bottleneck points somewhere specific.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Start with vLLM. It has the broadest model and hardware coverage, it invented PagedAttention, and it stands up with sane defaults faster than anything else. Reach for TensorRT-LLM when a profiled bottleneck justifies the cost, because it delivers the highest raw throughput and lowest latency on NVIDIA hardware but makes you compile a per-model, per-GPU engine and run a heavier ops burden for it. Reach for SGLang when your workload shares prefixes: RAG, multi-turn chat, agents, or high-concurrency MoE, where its RadixAttention prefix cache pulls meaningfully ahead. And don't start new work on TGI; HuggingFace put it in maintenance mode and points people at vLLM or SGLang themselves. One layer up from the engine, NVIDIA's Dynamo wraps any of these for distributed, disaggregated serving with a KV-cache-aware router, and NIM packages them as prebuilt microservices. On a single box the raw engine is what you're tuning, but those are the names you'll meet the moment you scale out.&lt;/p&gt;

&lt;p&gt;The two boxes from the opening ran the same vLLM. Same engine, same flags, one served three times the traffic. The engine was never the variable. The wires were. Which is the whole lesson of a single node, and also the reason part 3 is about the wires between nodes, where the same story plays out at a hundred times the scale and a hundred times the cost.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>nvlink</category>
      <category>nccl</category>
      <category>vllm</category>
    </item>
    <item>
      <title>The dozen layers under a GPU pod</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Thu, 02 Jul 2026 19:13:19 +0000</pubDate>
      <link>https://dev.to/sachincool/the-dozen-layers-under-a-gpu-pod-c5f</link>
      <guid>https://dev.to/sachincool/the-dozen-layers-under-a-gpu-pod-c5f</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/gpu-deployments-part-1-anatomy" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-07-02.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;The pod was stuck in &lt;code&gt;Pending&lt;/code&gt; and the node had eight H100s sitting idle. &lt;code&gt;kubectl describe node&lt;/code&gt; said &lt;code&gt;nvidia.com/gpu: 0&lt;/code&gt;. &lt;code&gt;nvidia-smi&lt;/code&gt; on the host printed all eight cards, healthy, 40°C, nothing running. So the hardware was fine, the driver was fine, and Kubernetes was convinced there were zero GPUs in a box that cost more than my car.&lt;/p&gt;

&lt;p&gt;That gap is the whole job. A GPU pod doesn't run on a GPU. It runs on about a dozen layers stacked between the silicon and your container, and any one of them can be quietly broken while every layer above and below it looks green. If you've shipped normal apps on Kubernetes, a GPU pod looks identical right up until it doesn't: underneath sits a stack of hardware and driver pieces a web pod never touches, and that's where the surprises live. This series is about running those layers in production without getting paged. Part 1 is the anatomy: what the layers are, what breaks at each one, and which numbers actually tell you the truth. Part 2 is a single box with eight GPUs and the wires between them. Part 3 is scaling past one box, where the network becomes the machine. Part 4 is watching the whole thing under real traffic, part 5 is scaling it to zero when nobody's using it without the bill or the cold start eating you, part 6 is the routing layer in front of it all, where the right load balancer buys a 2× speedup for free, part 7 keeps it breathing through loads and deploys, and part 8 is sharing it with other teams without the tenants, or a security boundary that isn't where you think, burning you.&lt;/p&gt;

&lt;h2&gt;
  
  
  the dozen layers between silicon and your pod
&lt;/h2&gt;

&lt;p&gt;Start at the bottom and climb. Each layer trusts the one under it and lies to the one above it when things go wrong.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;silicon&lt;/strong&gt; is the GPU itself plus the NVSwitch fabric that wires the GPUs on a board together, plus the NIC (a ConnectX-7 or BlueField-3) that carries traffic off the box. This is where hardware faults live: ECC errors (bit-flips in memory the card catches and corrects), thermal throttle, a card that stops answering on the PCIe bus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Firmware&lt;/strong&gt; sits on the card. Modern GPUs have a GSP, a GPU System Processor, a little RISC-V core that runs firmware and offloads work the host driver used to do. When you hear about a GPU "hanging" for no visible reason, the GSP firmware timing out is a common culprit.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;kernel driver&lt;/strong&gt; is &lt;code&gt;nvidia.ko&lt;/code&gt; and friends (&lt;code&gt;nvidia-uvm&lt;/code&gt;, &lt;code&gt;nvidia-peermem&lt;/code&gt;). It's a kernel module, which means it's compiled against your exact kernel headers. Upgrade the kernel without rebuilding the module and the driver won't load. This is the layer that breaks on a Tuesday because someone patched the base image.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;userspace driver&lt;/strong&gt; is &lt;code&gt;libcuda.so&lt;/code&gt;, the CUDA driver API. It ships with the driver, not with CUDA, and this trips people up constantly. &lt;code&gt;nvidia-smi&lt;/code&gt; talks to the driver through NVML, which is why &lt;code&gt;nvidia-smi&lt;/code&gt; can work while your actual CUDA program fails: they're using different entry points into the same stack.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;CUDA runtime&lt;/strong&gt; is &lt;code&gt;libcudart&lt;/code&gt; plus the math and collective libraries: cuBLAS, cuDNN, NCCL. Here's the thing nobody tells you on day one. PyTorch and TensorFlow wheels bundle their own copy of all of this. When you &lt;code&gt;pip install torch==2.x+cu128&lt;/code&gt;, you are installing CUDA 12.8, cuDNN, and NCCL inside the wheel. The host node doesn't need a CUDA install at all. It needs a driver new enough to satisfy that bundled runtime, and nothing more. Once that clicks, half the version confusion evaporates.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;NVIDIA Container Toolkit&lt;/strong&gt; (&lt;code&gt;nvidia-ctk&lt;/code&gt;, &lt;code&gt;libnvidia-container&lt;/code&gt;, currently 1.19.1) is the bridge between the host driver and the container. At container start it injects &lt;code&gt;/dev/nvidia*&lt;/code&gt; and the driver libraries into the container's filesystem. Miss this and you get the classic symptom: &lt;code&gt;nvidia-smi&lt;/code&gt; works on the host, fails inside the pod.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;container runtime&lt;/strong&gt; is containerd or CRI-O running runc underneath. It has to be told to use the &lt;code&gt;nvidia&lt;/code&gt; runtime. If &lt;code&gt;default_runtime_name&lt;/code&gt; in &lt;code&gt;/etc/containerd/config.toml&lt;/code&gt; isn't set, pods land on the node with no GPU access and no obvious error. (Recent GPU Operator versions wire this through the NRI/CDI plugin instead of a default runtime, but the failure mode is identical: get it wrong and the pod sees no GPU.)&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;device plugin&lt;/strong&gt; (0.19.x) is the piece that talks to the kubelet. It counts the GPUs and advertises them as &lt;code&gt;nvidia.com/gpu: 8&lt;/code&gt;, or &lt;code&gt;nvidia.com/mig-1g.10gb: 56&lt;/code&gt; if you're slicing. When this crashes or can't reach the driver, you get &lt;code&gt;nvidia.com/gpu: 0&lt;/code&gt; and pods stuck Pending. That was my incident at the top. The device plugin had crash-looped after a driver-container restart and never re-registered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node Feature Discovery&lt;/strong&gt; and &lt;strong&gt;GPU Feature Discovery&lt;/strong&gt; label the node with what it has: GPU model, memory, compute capability, MIG profile, driver version. The scheduler reads those labels to place pods. Wrong labels, wrong placement.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;GPU Operator&lt;/strong&gt; (v26.3.x) is the thing that installs and manages every layer above the kernel as a set of DaemonSets. It runs the driver as a container, wires the toolkit, deploys the device plugin, DCGM, the MIG manager. It's a huge convenience and one more control loop to debug when it gets stuck reconciling.&lt;/p&gt;

&lt;p&gt;On top, the &lt;strong&gt;scheduler&lt;/strong&gt; (kube-scheduler, or Kueue, Volcano, Run:ai, or Slurm) decides which pod lands on which GPU. And finally the &lt;strong&gt;workload&lt;/strong&gt;: a training job that needs all N GPUs at once or nothing, or an inference server that would happily take a seventh of one card.&lt;/p&gt;

&lt;p&gt;Twelve layers. The reason GPU infra feels harder than normal infra isn't any single layer. It's that the failure at layer 3 shows up as a symptom at layer 9, and the tooling at layer 9 has no idea layer 3 exists.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Key Insight:&lt;/strong&gt; The layer that breaks and the symptom you notice are rarely the same layer. The pod is Pending up at the scheduler, but the cause is a driver that didn't load down near the metal. When a GPU pod misbehaves, debug from the bottom of the stack up, not the top down.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One part of this stack is quietly being rebuilt under you. The device plugin advertising &lt;code&gt;nvidia.com/gpu: 8&lt;/code&gt; is a flat count: a pod asks for a number and gets whatever GPUs the node has. Kubernetes 1.34 (September 2025) made Dynamic Resource Allocation (DRA) generally available, and it's the eventual replacement for that model. DRA is a &lt;code&gt;ResourceClaim&lt;/code&gt; API, the way a &lt;code&gt;PersistentVolumeClaim&lt;/code&gt; is for storage, so a pod can ask for "two NVLink-connected GPUs with at least 40GB each" instead of a bare count, and it's how rack-scale multi-node NVLink (GB200 NVL72) gets scheduled at all. NVIDIA ships a DRA driver through the GPU Operator. The device plugin is still the common path in mid-2026, but a stack diagram drawn today should treat it as the model DRA is replacing, not the permanent one.&lt;/p&gt;

&lt;h2&gt;
  
  
  the version matrix that pages you
&lt;/h2&gt;

&lt;p&gt;The single most common self-inflicted outage is a version mismatch, and the matrix has four axes: driver, CUDA toolkit, cuDNN, and framework. The good news is that three compatibility mechanisms mean you rarely have to line up all four exactly. The bad news is that nobody explains which mechanism they're relying on, so it feels like luck.&lt;/p&gt;

&lt;p&gt;Backward compatibility is the easy one. A newer driver runs older CUDA binaries, always. An R580 driver runs a CUDA 12 app and a CUDA 13 app without complaint. So keeping the driver ahead of everything is safe.&lt;/p&gt;

&lt;p&gt;CUDA minor-version compatibility is the one you lean on daily. Any CUDA 12.x toolkit runs on any driver that supports 12.0. A driver from the 525 era will run a CUDA 12.8 binary, because they share the CUDA 12 major. This is why the framework-bundles-its-own-CUDA pattern works: the wheel carries CUDA 12.8, your node has some 12-capable driver, and they meet in the middle.&lt;/p&gt;

&lt;p&gt;Forward compatibility is the escape hatch. The &lt;code&gt;cuda-compat&lt;/code&gt; package ships updated &lt;code&gt;libcuda&lt;/code&gt; stubs that let a &lt;em&gt;newer&lt;/em&gt; CUDA major run on an &lt;em&gt;older&lt;/em&gt; driver branch. It's how you run a CUDA 13 app on a node still pinned to an R535 driver you can't upgrade yet. It only works on data-center GPUs, and it's a deliberate override, not something to build on.&lt;/p&gt;

&lt;p&gt;Here's the current data-center driver picture as of mid-2026:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;EOL&lt;/th&gt;
&lt;th&gt;CUDA&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;R535&lt;/td&gt;
&lt;td&gt;LTS&lt;/td&gt;
&lt;td&gt;June 2026&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R580&lt;/td&gt;
&lt;td&gt;LTS&lt;/td&gt;
&lt;td&gt;June 2028&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R595&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;td&gt;March 2027&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;R610&lt;/td&gt;
&lt;td&gt;New Feature&lt;/td&gt;
&lt;td&gt;Aug 2026&lt;/td&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you take one thing from this section: manage the driver, let the framework carry the rest. Pin your training and serving images to NGC base images (&lt;code&gt;nvcr.io/nvidia/pytorch:25.xx&lt;/code&gt;) where NVIDIA has already matched CUDA, cuDNN, and NCCL for you, and you delete an entire class of 2am pages.&lt;/p&gt;

&lt;p&gt;One current gotcha worth flagging if you're on Blackwell. Drivers from 580.65.06 turn on Coherent Driver Memory Management by default for GB200 and GH200 on Kubernetes, and CDMM is incompatible with both MIG and GPUDirect Storage right now. If you buy GB200s planning to slice them with MIG, check that first, because it's not going to be on the datasheet.&lt;/p&gt;

&lt;h2&gt;
  
  
  slicing one GPU three ways
&lt;/h2&gt;

&lt;p&gt;A single H100 has 80GB of HBM (its fast on-board memory) and enough compute to serve dozens of small models. Handing a whole card to a workload that uses 8% of it is how you set money on fire. There are three ways to share a GPU, and they are not interchangeable. The difference is isolation.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5ykjnqr48z7s0djjhz4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr5ykjnqr48z7s0djjhz4.png" alt="A diagram showing one physical GPU sliced three ways side by side. Left: MIG, the GPU split into seven hardware partitions each with its own walled-off memory and compute, labeled 'hardware isolation'. Middle: MPS, multiple processes sharing one GPU context running concurrently, labeled 'concurrent, soft limits, weak isolation'. Right: time-slicing, several pods taking turns on the whole GPU in round-robin, labeled 'no isolation, round-robin'." width="799" height="376"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · the same GPU shared three ways. Isolation drops as you move right; utilization convenience goes up. Pick by how much you trust the tenants.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MIG&lt;/strong&gt; (Multi-Instance GPU) is hardware partitioning, available on the data-center cards (A100, A30, H100, H200, B200) but not the smaller inference GPUs like the L4 or A10G, which fall back to MPS. It cuts one GPU into up to seven instances, each with its own SMs (the GPU's compute cores), its own dedicated slice of HBM, its own memory controller and L2. A fault in one instance doesn't touch the others. That's real hardware isolation, the kind you want when tenants don't trust each other. The profiles are named &lt;code&gt;[compute]g.[memory]gb&lt;/code&gt;. On an H100 80GB you get seven &lt;code&gt;1g.10gb&lt;/code&gt; slices, or two &lt;code&gt;3g.40gb&lt;/code&gt;, or one &lt;code&gt;7g.80gb&lt;/code&gt; that's just the whole card back. An H200's 141GB gives you seven &lt;code&gt;1g.18gb&lt;/code&gt; slices; a B200's 180GB gives seven &lt;code&gt;1g.23gb&lt;/code&gt;. The quirk that catches everyone: compute fractions go 1/7, 2/7, 3/7, 4/7, 7/7. There is no 5g or 6g profile. Memory is quantized in eighths. So a &lt;code&gt;1g&lt;/code&gt; slice gets one-seventh of the compute but one-eighth of the memory, and the arithmetic never quite lines up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MPS&lt;/strong&gt; (Multi-Process Service) is a daemon that multiplexes several processes into one GPU context so their kernels run genuinely concurrently, not round-robin. You can cap each client's memory (&lt;code&gt;CUDA_MPS_PINNED_DEVICE_MEM_LIMIT&lt;/code&gt;) and compute share (&lt;code&gt;CUDA_MPS_ACTIVE_THREAD_PERCENTAGE&lt;/code&gt;). What you don't get is hardware memory protection or clean fault isolation. One client that OOMs can take its neighbors down with it. MPS is for high-throughput inference where you own all the tenants and want better SM utilization than time-slicing gives you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time-slicing&lt;/strong&gt; is the crude one. The device plugin just lies about the GPU count, advertising one physical card as ten replicas. Ten pods land, and they take turns via context switching. There is no memory partitioning and no fault isolation at all. If one pod grabs 70GB of an 80GB card, the other nine OOM. It's fine for notebooks, CI, and dev clusters where the work is bursty and nobody's SLA depends on it. It has no business in front of production traffic.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Memory isolation&lt;/th&gt;
&lt;th&gt;Fault isolation&lt;/th&gt;
&lt;th&gt;Concurrency&lt;/th&gt;
&lt;th&gt;Use for&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MIG&lt;/td&gt;
&lt;td&gt;hardware&lt;/td&gt;
&lt;td&gt;hardware&lt;/td&gt;
&lt;td&gt;spatial&lt;/td&gt;
&lt;td&gt;multi-tenant, untrusted, prod serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MPS&lt;/td&gt;
&lt;td&gt;soft caps&lt;/td&gt;
&lt;td&gt;weak&lt;/td&gt;
&lt;td&gt;true concurrent&lt;/td&gt;
&lt;td&gt;trusted high-throughput inference&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;none&lt;/td&gt;
&lt;td&gt;round-robin&lt;/td&gt;
&lt;td&gt;dev, notebooks, CI&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You can stack them: partition a card into seven MIG slices, then time-slice each slice for burstier workloads. Most teams don't need that. Most teams need to notice they're running one 8% workload per $30k card and switch to &lt;code&gt;7× 1g.10gb&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  the metrics that lie to you
&lt;/h2&gt;

&lt;p&gt;Here's the number that ruins more capacity planning than any other: &lt;code&gt;GPU-Util&lt;/code&gt;. When &lt;code&gt;nvidia-smi&lt;/code&gt; shows 95% and everyone relaxes, they've misread it. &lt;code&gt;DCGM_FI_DEV_GPU_UTIL&lt;/code&gt; answers exactly one question: was a kernel running during the sample window. It says nothing about how much of the silicon that kernel used. You can see 95% GPU utilization and 25% of the actual compute capacity in use at the same moment, and both numbers are honest about different things. Somebody will still screenshot the 95% into a capacity deck, and now you're being asked to buy more of a card you're already wasting.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6yfjs3v2m25tv5mkxb7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fo6yfjs3v2m25tv5mkxb7.png" alt="A horizontal bar comparison infographic titled 'the same GPU, two honest numbers'. Top bar shows GPU-Util at 95 percent filled nearly full in one color. Bottom bar shows Model FLOPs Utilization (MFU) at 40 percent, filled less than half in a contrasting color. A caption band notes that a well-run large training job lands at 35 to 50 percent MFU, and the gap between the two bars is where your money goes." width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · GPU-Util says a kernel ran. MFU says how much of the chip did useful work. The gap between them is memory stalls, collective communication, and non-matmul overhead you paid for anyway.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The number that matters for training is &lt;strong&gt;MFU&lt;/strong&gt;, Model FLOPs Utilization: the FLOPs your model actually did divided by the theoretical peak. It's the metric the Llama and PaLM papers report, and 35–50% is considered excellent for large-scale training. The gap between "a kernel ran" and "the chip did useful math" is memory-bandwidth stalls, NCCL all-reduce waiting on the network, attention softmax and layernorm that aren't matmuls, optimizer steps, and activation recompute. All of it counts against your wall clock. None of it counts as useful FLOPs.&lt;/p&gt;

&lt;p&gt;The money follows directly. On an 8-GPU H100 node at roughly $3 per GPU-hour, the difference between 25% and 45% MFU is about 1.8× the effective cost per token. Sticker price per GPU-hour is the number vendors compete on. Utilization is the number that actually sets your bill.&lt;/p&gt;

&lt;p&gt;For real telemetry, DCGM (Data Center GPU Manager) is the layer, and &lt;code&gt;dcgm-exporter&lt;/code&gt; scrapes it into Prometheus. The fields worth putting on a dashboard from day one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code&gt;: Tensor Core utilization, the one that tracks real training throughput.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DCGM_FI_PROF_DRAM_ACTIVE&lt;/code&gt;: HBM bandwidth in use. High here with low tensor activity means you're memory-bound.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DCGM_FI_DEV_FB_USED&lt;/code&gt;: HBM used. Your OOM early-warning.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DCGM_FI_DEV_XID_ERRORS&lt;/code&gt;: the last Xid code. The single most important reliability signal on the box.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DCGM_FI_DEV_ECC_DBE_VOL&lt;/code&gt; and the row-remap fields: memory health, trending toward RMA.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code&gt;: a bitmask telling you whether the card is throttling on power, thermals, or a reliability limit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;dcgmi diag -r 3&lt;/code&gt; runs about twelve minutes of escalating health checks (memory bandwidth, PCIe, NVLink, thermals under load) and is the thing to run before you trust a node you just recovered.&lt;/p&gt;

&lt;h2&gt;
  
  
  the Xid codes worth memorizing
&lt;/h2&gt;

&lt;p&gt;Xid errors are the driver's way of telling the kernel log that the hardware had to correct or retry something it shouldn't have. They land in &lt;code&gt;dmesg&lt;/code&gt; as &lt;code&gt;NVRM: Xid (PCI:0000:xx:00): &amp;lt;code&amp;gt;&lt;/code&gt;. A nonzero Xid is not always fatal, but it's never nothing. A handful are worth knowing on sight because they change what you do next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fins9ojervrwbuh1o4yf7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fins9ojervrwbuh1o4yf7.png" alt="A decision-flow diagram titled 'a GPU is misbehaving: what the Xid tells you to do'. It branches from a central node reading dmesg for the Xid code. Xid 13/31/43 routes to 'app bug, restart the workload'. Xid 48/94 routes to 'contained ECC, drain and reset the GPU'. Xid 95 routes to 'uncontained ECC, reset GPU before any restart'. Xid 63/64 routes to 'row-remapping, watch the trend, RMA if it fails'. Xid 79 routes to 'fell off the bus, cordon and reboot the node, RMA if it recurs'. Xid 119/120 routes to 'GSP firmware hung, reset GPU'." width="800" height="429"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 4 · the Xid triage most on-call runbooks converge on. The split that matters is app-fault versus hardware-fault, because one restarts a pod and the other cordons a node.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xid 13, 31, 43&lt;/strong&gt; are usually your fault, not the hardware's: illegal memory access, a bad kernel, a page fault from application code. Restart the workload, look at the model, not the card.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xid 48&lt;/strong&gt; is a double-bit ECC error, uncorrectable. &lt;strong&gt;Xid 94&lt;/strong&gt; is a contained ECC error, where the damage stayed inside the offending app and the other apps on the card survived. &lt;strong&gt;Xid 95&lt;/strong&gt; is the uncontained version, where the blast radius crossed apps, and the GPU has to be reset before anything restarts on it. The 94/95 split is the one to internalize: contained means drain politely, uncontained means the card is compromised until reset.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xid 63 and 64&lt;/strong&gt; are row-remapping events. Modern HBM can retire bad memory rows the way a disk retires bad sectors. A 63 is the card recording that it did this; persistent 64s (remap failures) mean it's running out of spare rows and it's an RMA candidate. Watch the trend, don't panic on the first one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xid 79&lt;/strong&gt; is the one that ruins a night: "GPU has fallen off the bus." The card stopped answering on PCIe entirely. Thermal, power delivery, seating, a dying board. The node needs a reset, and if the same physical slot throws it again, that's a card headed back for RMA. Field reports put it around 3% of H100 deployments in the first year, which sounds small until you multiply it by a thousand-GPU fleet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Xid 119 and 120&lt;/strong&gt; are the GSP firmware timing out. Reset the GPU. On a few driver versions it's common enough that ops teams disable the GSP firmware as a workaround, which tells you how much fun that particular bug is.&lt;/p&gt;

&lt;p&gt;The remediation ladder most teams settle on is boring and effective: app restart or driver reload or node reboot clears roughly 60% of incidents within fifteen minutes; anything that survives that gets &lt;code&gt;dcgmi diag -r 3&lt;/code&gt;; anything that fails the diag gets cordoned and sent back.&lt;/p&gt;

&lt;h2&gt;
  
  
  what to actually care about
&lt;/h2&gt;

&lt;p&gt;If you're standing up GPU infrastructure and wondering where to spend your attention, the honest ranking isn't the one the marketing implies. It's roughly this. Get the driver-and-toolkit layer boringly stable, because that's where the self-inflicted outages live. Instrument MFU and the Xid stream before you instrument anything pretty, because those two tell you whether you're wasting money and whether the hardware is dying. Decide your sharing model (MIG for multi-tenant, whole cards for training) before you have tenants, because retrofitting isolation is miserable. And treat the twelve-layer stack as the thing it is: a place where a green dashboard at layer 9 can sit directly on top of a card that fell off the bus at layer 1.&lt;/p&gt;

&lt;p&gt;This is also usually the point where teams bring in someone who has already burned a few weeks on Xid codes and MIG partitioning, rather than doing it live on their own GPU bill. GPU and ML infrastructure builds are part of the &lt;a href="https://k8s.org.in" rel="noopener noreferrer"&gt;independent infrastructure consulting&lt;/a&gt; work I take on.&lt;/p&gt;

&lt;p&gt;The pod that was stuck Pending at the top of this post came back the moment the device plugin re-registered. Fifteen seconds of fix, forty minutes of staring at a healthy &lt;code&gt;nvidia-smi&lt;/code&gt; wondering how the machine could see eight GPUs that Kubernetes swore didn't exist. That distance, between what the hardware knows and what the scheduler believes, is where most of your on-call rotation lives too. The next part goes inside a single box with eight of these cards and the wires that decide whether they cooperate or just sit next to each other.&lt;/p&gt;

</description>
      <category>gpu</category>
      <category>kubernetes</category>
      <category>mlops</category>
      <category>nvidia</category>
    </item>
    <item>
      <title>The git commands I actually run every day</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Wed, 20 May 2026 19:10:05 +0000</pubDate>
      <link>https://dev.to/sachincool/the-git-commands-i-actually-run-every-day-423p</link>
      <guid>https://dev.to/sachincool/the-git-commands-i-actually-run-every-day-423p</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/daily-git-commands" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-05-20.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I've been using git for a decade and most of what I type still fits on a single hand. The 200-page Pro Git book is wonderful and almost none of it survives contact with a real Tuesday. What survives is a small, boring set of commands that get rerun constantly.&lt;/p&gt;

&lt;p&gt;This post is that list, ordered by how often my fingers actually type them. Aliases are from the oh-my-zsh &lt;code&gt;git&lt;/code&gt; plugin (enabled in most zsh configs that exist); the full command sits next to the alias so it's portable.&lt;/p&gt;

&lt;h2&gt;
  
  
  the daily eight
&lt;/h2&gt;

&lt;p&gt;These are the ones I'd type in my sleep. If you're not using all eight already, picking them up pays back inside a week.&lt;/p&gt;

&lt;h3&gt;
  
  
  gst
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git status&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gst
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run this between every other command. It's the cheapest sanity check git has. Branch, ahead/behind, staged, unstaged, untracked. Two seconds. If you only learn one alias, learn this one.&lt;/p&gt;

&lt;h3&gt;
  
  
  glola
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git log --oneline --graph --decorate --all&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;glola | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-30&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The one true log. Graph of every branch (local + remote), one line per commit, colored refs. Pipe through &lt;code&gt;head&lt;/code&gt; because most of the time you only care about the last 20-30 commits.&lt;/p&gt;

&lt;h3&gt;
  
  
  gd / gds
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git diff / git diff --staged&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gd          &lt;span class="c"&gt;# what's changed but not staged&lt;/span&gt;
gds         &lt;span class="c"&gt;# what's staged and about to be committed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gds&lt;/code&gt; before every commit. If you set &lt;a href="https://github.com/dandavison/delta" rel="noopener noreferrer"&gt;delta&lt;/a&gt; as your pager (&lt;code&gt;brew install git-delta&lt;/code&gt;, then &lt;code&gt;pager = delta&lt;/code&gt; in &lt;code&gt;~/.gitconfig&lt;/code&gt;), the output stops being painful to read.&lt;/p&gt;

&lt;h3&gt;
  
  
  gcam
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git commit -a -m&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gcam &lt;span class="s2"&gt;"fix: trailing slash in webhook URL"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick one-line commits for small fixes. For anything bigger I drop the &lt;code&gt;-m&lt;/code&gt; and let &lt;code&gt;$EDITOR&lt;/code&gt; open so I can write a proper message with a body.&lt;/p&gt;

&lt;h3&gt;
  
  
  gpsup
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git push --set-upstream origin &amp;lt;current-branch&amp;gt;&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpsup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First push of a new branch. The full command is annoying to type, so &lt;code&gt;gpsup&lt;/code&gt; figures out the current branch name itself. After the first push, plain &lt;code&gt;gp&lt;/code&gt; (just &lt;code&gt;git push&lt;/code&gt;) works because upstream is set.&lt;/p&gt;

&lt;h3&gt;
  
  
  gco / gcb
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git checkout / git checkout -b&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gco main             &lt;span class="c"&gt;# switch to main&lt;/span&gt;
gco -                &lt;span class="c"&gt;# switch to previous branch&lt;/span&gt;
gcb feature/login    &lt;span class="c"&gt;# create + switch to new branch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gco -&lt;/code&gt; is the one to notice. Like &lt;code&gt;cd -&lt;/code&gt; for branches. When you're bouncing between two branches all day, it's a single keystroke each way instead of typing the name.&lt;/p&gt;

&lt;h3&gt;
  
  
  gpf
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git push --force-with-lease&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After rebasing or amending. &lt;strong&gt;Always use &lt;code&gt;--force-with-lease&lt;/code&gt;, never &lt;code&gt;--force&lt;/code&gt;.&lt;/strong&gt; The lease version refuses to push if someone else has pushed to your branch since your last fetch, saving you from silently overwriting a teammate's work. There is no good reason to ever type &lt;code&gt;--force&lt;/code&gt; in 2026.&lt;/p&gt;

&lt;h3&gt;
  
  
  gfa
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;git fetch --all --prune&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gfa
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh every remote, prune deleted remote branches. Run before you start anything that depends on knowing the current state of the world. The &lt;code&gt;--prune&lt;/code&gt; half is what makes the cleanup ritual below work.&lt;/p&gt;

&lt;h2&gt;
  
  
  checkout recent branches
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git branch&lt;/code&gt; lists alphabetically, which is useless. What you actually want is "that branch from Tuesday," which means sorting by last commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; alias.recent &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"for-each-ref --sort=-committerdate refs/heads/ &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
   --format='%(HEAD) %(color:yellow)%(refname:short)%(color:reset) &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
             %(color:green)(%(committerdate:relative))%(color:reset) %(contents:subject)'"&lt;/span&gt;

git recent | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That covers looking. For switching, pipe the same list into fzf and you never type a branch name again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# fco: fuzzy-checkout a recent branch&lt;/span&gt;
fco&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;branch
  &lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-each-ref&lt;/span&gt; &lt;span class="nt"&gt;--sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nt"&gt;-committerdate&lt;/span&gt; refs/heads/ &lt;span class="se"&gt;\&lt;/span&gt;
             &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%(refname:short)'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
           | fzf &lt;span class="nt"&gt;--height&lt;/span&gt; 40% &lt;span class="nt"&gt;--reverse&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
                 &lt;span class="nt"&gt;--preview&lt;/span&gt; &lt;span class="s1"&gt;'git log --oneline --decorate --color=always -15 {}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; git checkout &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Branches arrive sorted by recency, so the one you want is almost always in the top three. Type two letters of its name, Enter, done. The preview pane shows the branch's recent commits so you can confirm it's the right Tuesday. &lt;code&gt;gco -&lt;/code&gt; still wins for bouncing between exactly two branches; &lt;code&gt;fco&lt;/code&gt; wins for everything else. (&lt;code&gt;brew install fzf&lt;/code&gt; if you don't have it. You want it for &lt;code&gt;Ctrl-R&lt;/code&gt; history search anyway.)&lt;/p&gt;

&lt;h2&gt;
  
  
  the cleanup ritual
&lt;/h2&gt;

&lt;p&gt;Run this weekly. If you've ever scrolled through 80 stale branches looking for the one you actually want, you already know why.&lt;/p&gt;

&lt;p&gt;The easy half deletes every local branch whose tip is already in &lt;code&gt;main&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gfa
git branch &lt;span class="nt"&gt;--merged&lt;/span&gt; main | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s1"&gt;'\*\|main\|master'&lt;/span&gt; | xargs &lt;span class="nt"&gt;-n1&lt;/span&gt; git branch &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Works only if your team uses merge commits. Most don't. GitHub's "Squash and merge" creates a brand-new commit on &lt;code&gt;main&lt;/code&gt; with a different SHA, so &lt;code&gt;git branch --merged&lt;/code&gt; never catches your local branch. Its commits aren't in main's history at all.&lt;/p&gt;

&lt;p&gt;The workaround: after &lt;code&gt;gfa&lt;/code&gt;, any branch whose tracked remote was deleted shows as &lt;code&gt;[gone]&lt;/code&gt;. Those are &lt;em&gt;usually&lt;/em&gt; your merged-and-deleted PRs.&lt;/p&gt;

&lt;p&gt;Usually, not always. &lt;code&gt;[gone]&lt;/code&gt; only means the remote tracking branch is gone. Nearly always that's a squash-merged PR whose branch GitHub auto-deleted. But it can also be a branch you pushed, someone deleted server-side, and you never merged. So don't force-delete every &lt;code&gt;[gone]&lt;/code&gt; branch with &lt;code&gt;git branch -D&lt;/code&gt;. I once watched one show &lt;code&gt;[gone]&lt;/code&gt; while it still held 26 unmerged commits; a force-delete there loses them for good.&lt;/p&gt;

&lt;p&gt;So check each &lt;code&gt;[gone]&lt;/code&gt; branch for patch-equivalence against the base &lt;em&gt;before&lt;/em&gt; deleting. Squash-merges get caught, genuinely unmerged work gets kept. This lives in my &lt;code&gt;~/.gitconfig&lt;/code&gt; as &lt;code&gt;git gone&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ~/.gitconfig, under [alias]  →  run as: git gone&lt;/span&gt;
gone &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"!f() { &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    git fetch --all --prune; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    base=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--abbrev-ref&lt;/span&gt; origin/HEAD 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;; base=&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;base&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;/main&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    for b in &lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git &lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="nt"&gt;-each-ref&lt;/span&gt; &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'%(refname:short) %(upstream:track)'&lt;/span&gt; refs/heads &lt;span class="se"&gt;\&lt;/span&gt;
               | &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'$2==\"[gone]\"{print $1}'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;; do &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
      if [ -z &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git cherry &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; &lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'^+'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt; ]; then git branch -D &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
      else echo &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;kept &lt;/span&gt;&lt;span class="nv"&gt;$b&lt;/span&gt;&lt;span class="s2"&gt; (commits not in &lt;/span&gt;&lt;span class="nv"&gt;$base&lt;/span&gt;&lt;span class="s2"&gt;)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;; fi; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;
    done; }; f"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One command does the whole ritual: the &lt;code&gt;git fetch --all --prune&lt;/code&gt; prunes the dead remote refs, then the loop deletes the merged local branches in the same pass. No separate &lt;code&gt;gfa&lt;/code&gt; first.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git cherry&lt;/code&gt; compares by patch-id, not SHA. A squash-merged branch shows every commit as &lt;code&gt;-&lt;/code&gt; (an equivalent already exists in the base) and gets deleted; a branch with real unpushed work shows &lt;code&gt;+&lt;/code&gt; lines and stays. The &lt;code&gt;-D&lt;/code&gt; is only reached after patch-equivalence is proven, so it never eats unmerged work.&lt;/p&gt;

&lt;p&gt;Or install &lt;a href="https://github.com/foriequal0/git-trim" rel="noopener noreferrer"&gt;&lt;code&gt;git-trim&lt;/code&gt;&lt;/a&gt; (&lt;code&gt;brew install git-trim&lt;/code&gt;), which does the same classification and more. It catches squash-merges even when the tracking ref isn't &lt;code&gt;[gone]&lt;/code&gt;, and skips diverged branches by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git trim                &lt;span class="c"&gt;# dry-run&lt;/span&gt;
git trim &lt;span class="nt"&gt;--confirm&lt;/span&gt;      &lt;span class="c"&gt;# actually delete&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the closest thing to "did my PR ship?" you can ask git directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  the weekly four
&lt;/h2&gt;

&lt;p&gt;Not in your fingers yet, but should be.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;git switch&lt;/code&gt; and &lt;code&gt;git restore&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git switch &lt;span class="nt"&gt;-c&lt;/span&gt; new-feature           &lt;span class="c"&gt;# create + switch&lt;/span&gt;
git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; file.txt       &lt;span class="c"&gt;# unstage&lt;/span&gt;
git restore &lt;span class="nt"&gt;--source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123 file.go &lt;span class="c"&gt;# restore single file from any commit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; and &lt;code&gt;restore&lt;/code&gt; split the four jobs &lt;code&gt;checkout&lt;/code&gt; used to do. The one I reach for most is &lt;code&gt;restore --source=&amp;lt;sha&amp;gt; &amp;lt;path&amp;gt;&lt;/code&gt;. Translation: "grab this single file from three commits ago without touching anything else."&lt;/p&gt;

&lt;h3&gt;
  
  
  interactive rebase with autosquash
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;--fixup&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123       &lt;span class="c"&gt;# fixup commit targeting abc123&lt;/span&gt;
&lt;span class="c"&gt;# ... keep working ...&lt;/span&gt;
git rebase &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="nt"&gt;--autosquash&lt;/span&gt; main &lt;span class="c"&gt;# all fixups slot into place automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The single biggest workflow win I've found in ten years of git. While reviewing your own PR you find a bug four commits back. Don't fix it on top. &lt;code&gt;--fixup=&amp;lt;sha&amp;gt;&lt;/code&gt; creates a commit targeting the offender, and the autosquash rebase squashes everything into place when you're done. Install &lt;a href="https://github.com/tummychow/git-absorb" rel="noopener noreferrer"&gt;git-absorb&lt;/a&gt; (&lt;code&gt;brew install git-absorb&lt;/code&gt;) and it even picks the target SHA for you: edit the files, run &lt;code&gt;git absorb --and-rebase&lt;/code&gt;, done.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;git reflog&lt;/code&gt;, the universal undo
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git reflog
git reset &lt;span class="nt"&gt;--hard&lt;/span&gt; HEAD@&lt;span class="o"&gt;{&lt;/span&gt;5&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every change to &lt;code&gt;HEAD&lt;/code&gt; is logged for 90 days. Bad rebase? &lt;code&gt;reflog&lt;/code&gt;. Deleted branch? &lt;code&gt;reflog&lt;/code&gt;. There is almost nothing in git you can't undo if you know about it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;git worktree&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../proj-hotfix hotfix/prod-down
git worktree remove ../proj-hotfix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Need to fix a prod bug while halfway through a feature? Don't stash. &lt;code&gt;worktree add&lt;/code&gt; gives you a second checkout in a sibling directory, sharing the same &lt;code&gt;.git&lt;/code&gt;. I use it constantly for "let me review your PR" without leaving my own branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  set it once
&lt;/h2&gt;

&lt;p&gt;Five config lines and a daemon. Enable, forget.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config &lt;span class="nt"&gt;--global&lt;/span&gt; rerere.enabled &lt;span class="nb"&gt;true&lt;/span&gt;          &lt;span class="c"&gt;# remember conflict resolutions, replay them&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; push.default current         &lt;span class="c"&gt;# `git push` pushes current branch to same name&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; push.autoSetupRemote &lt;span class="nb"&gt;true&lt;/span&gt;    &lt;span class="c"&gt;# first push sets upstream automatically&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; diff.algorithm histogram     &lt;span class="c"&gt;# cleaner diffs than the default myers&lt;/span&gt;
git config &lt;span class="nt"&gt;--global&lt;/span&gt; merge.conflictStyle zdiff3   &lt;span class="c"&gt;# conflict markers include the common ancestor&lt;/span&gt;
git maintenance start                            &lt;span class="c"&gt;# background gc/prefetch on a schedule&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;autoSetupRemote&lt;/code&gt; retires &lt;code&gt;gpsup&lt;/code&gt; entirely. &lt;code&gt;zdiff3&lt;/code&gt; shows the original code both sides diverged from; once you've used it, plain &lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&lt;/code&gt; markers feel like flying blind.&lt;/p&gt;

&lt;h2&gt;
  
  
  when something is broken
&lt;/h2&gt;

&lt;p&gt;Not daily, but when the question is "when did this start," nothing else answers it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git log &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="s2"&gt;"functionName"&lt;/span&gt;          &lt;span class="c"&gt;# pickaxe: commits where this string was added or removed&lt;/span&gt;
git blame &lt;span class="nt"&gt;-w&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="nt"&gt;-C&lt;/span&gt; file.go      &lt;span class="c"&gt;# blame the logic's actual author, not the formatter&lt;/span&gt;
git log &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--follow&lt;/span&gt; file.go        &lt;span class="c"&gt;# full file history, including across renames&lt;/span&gt;
git range-diff @&lt;span class="o"&gt;{&lt;/span&gt;u&lt;span class="o"&gt;}&lt;/span&gt; @              &lt;span class="c"&gt;# what a rebase actually changed; run before force-pushing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-S&lt;/code&gt; searches the content of the diff, not commit messages. Different thing entirely from &lt;code&gt;--grep&lt;/code&gt;. And plain &lt;code&gt;blame&lt;/code&gt; gives credit to whoever last ran Prettier; &lt;code&gt;-w -C -C -C&lt;/code&gt; follows the code across whitespace changes, moves, and file boundaries to the person who wrote the logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  the four tools worth installing today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/junegunn/fzf" rel="noopener noreferrer"&gt;fzf&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;brew install fzf&lt;/code&gt;). Powers the &lt;code&gt;fco&lt;/code&gt; branch picker above, plus fuzzy &lt;code&gt;Ctrl-R&lt;/code&gt; history.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/tummychow/git-absorb" rel="noopener noreferrer"&gt;git-absorb&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;brew install git-absorb&lt;/code&gt;). Auto-fixup commits without picking SHAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/dandavison/delta" rel="noopener noreferrer"&gt;delta&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;brew install git-delta&lt;/code&gt;). Diff and blame output that doesn't hurt to look at.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://github.com/jesseduffield/lazygit" rel="noopener noreferrer"&gt;lazygit&lt;/a&gt;&lt;/strong&gt; (&lt;code&gt;brew install lazygit&lt;/code&gt;). TUI for the operations that are tedious on CLI: partial commits, stash management, conflict resolution.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This post used to end with two AI shell helpers for the stuff git can't tell you; those now live in &lt;a href="https://dev.to/til/one-line-ai-shell-helper"&gt;their own TIL&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  ten years in, the surprise
&lt;/h2&gt;

&lt;p&gt;After a decade, the command I run most isn't &lt;code&gt;commit&lt;/code&gt;. It isn't &lt;code&gt;push&lt;/code&gt;. It's &lt;code&gt;gst&lt;/code&gt;, hundreds of times a day, between every other operation. The most-used git command in my shell is the one that does nothing.&lt;/p&gt;

</description>
      <category>git</category>
      <category>shell</category>
      <category>zsh</category>
      <category>productivity</category>
    </item>
    <item>
      <title>A single zsh function for one-line AI answers that knows when to pre-type the command</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Wed, 20 May 2026 07:01:10 +0000</pubDate>
      <link>https://dev.to/sachincool/a-7-line-shell-function-for-one-liner-ai-answers-20o8</link>
      <guid>https://dev.to/sachincool/a-7-line-shell-function-for-one-liner-ai-answers-20o8</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/til/one-line-ai-shell-helper" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-05-20.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;I kept opening a chat tab just to ask "what's the kubectl command for decoding a secret" or "convert 42 GiB to bytes". The context switch was costing more than the answer was worth.&lt;/p&gt;

&lt;p&gt;Wrapping an AI CLI into a single shell function fixed it. The interesting part is &lt;code&gt;print -z&lt;/code&gt;, plus one heuristic that needs more care than it looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  the function
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# p: one-shot AI query. Examples: `p whats 2 + 2`, `p kubectl secret decode grafana`&lt;/span&gt;
&lt;span class="c"&gt;# Smart dispatch: if the answer looks like a runnable command, pre-type it into&lt;/span&gt;
&lt;span class="c"&gt;# the next prompt (print -z). Otherwise print to stdout. Math/facts get printed,&lt;/span&gt;
&lt;span class="c"&gt;# commands get queued for you to review and press Enter.&lt;/span&gt;
p&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  emulate &lt;span class="nt"&gt;-L&lt;/span&gt; zsh
  setopt NO_GLOB
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$# &lt;/span&gt;&lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"usage: p &amp;lt;question or task&amp;gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2
    &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;out
  &lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pi &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nt"&gt;--no-session&lt;/span&gt; &lt;span class="nt"&gt;--append-system-prompt&lt;/span&gt; &lt;span class="s1"&gt;'Answer in ONE line. No preamble, no explanation, no markdown, no code fences. For shell/kubectl/git/etc requests output only the command. For factual or math questions output only the answer.'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\000-\037'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^[[:space:]]*//;s/[[:space:]]*$//'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    return &lt;/span&gt;1
  &lt;span class="k"&gt;fi
  &lt;/span&gt;&lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;first&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;out&lt;/span&gt;&lt;span class="p"&gt;%% *&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;a-zA-Z_]&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; whence &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;print &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;else
    &lt;/span&gt;print &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;fi&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'noglob p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pi&lt;/code&gt; is just whatever AI CLI you have. Swap in &lt;code&gt;claude -p&lt;/code&gt;, &lt;code&gt;llm&lt;/code&gt;, &lt;code&gt;gh copilot suggest&lt;/code&gt;, &lt;code&gt;ollama run&lt;/code&gt;. The pattern doesn't care about the backend.&lt;/p&gt;

&lt;h2&gt;
  
  
  what it feels like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;p whats 2 + 2
&lt;span class="go"&gt;4

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;p capital of mongolia
&lt;span class="go"&gt;Ulaanbaatar

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;p regex &lt;span class="k"&gt;for &lt;/span&gt;matching an email
&lt;span class="go"&gt;[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}

&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;p kubectl secret decode grafana
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;next prompt now shows, cursor at the end:
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;kubectl get secret grafana &lt;span class="nt"&gt;-o&lt;/span&gt; go-template&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{range $k,$v := .data}}{{$k}}: {{$v | base64decode}}{{"\n"}}{{end}}'&lt;/span&gt;█
&lt;span class="go"&gt;
&lt;/span&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;p find all log files modified today
&lt;span class="gp"&gt;#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;next prompt:
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;find &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-type&lt;/span&gt; f &lt;span class="nt"&gt;-name&lt;/span&gt; &lt;span class="s2"&gt;"*.log"&lt;/span&gt; &lt;span class="nt"&gt;-mtime&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt;█
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same two-letter command for both. Answers go to stdout, commands go to the prompt buffer where you can edit them before pressing Enter.&lt;/p&gt;

&lt;h2&gt;
  
  
  the key idea: &lt;code&gt;print -z&lt;/code&gt; for runnable output
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;print -z&lt;/code&gt; is the trick that makes this design work. It pushes text onto the zsh line editor, i.e. into your next prompt, pre-typed and ready. Compared to every alternative:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Speed&lt;/th&gt;
&lt;th&gt;Safety&lt;/th&gt;
&lt;th&gt;Friction&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;eval "$(...)"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;fastest&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;bad&lt;/strong&gt;, auto-runs model output&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pipe to &lt;code&gt;pbcopy&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;safe&lt;/td&gt;
&lt;td&gt;switch focus, paste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Print to stdout&lt;/td&gt;
&lt;td&gt;medium&lt;/td&gt;
&lt;td&gt;safe&lt;/td&gt;
&lt;td&gt;select + copy + paste&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;&lt;code&gt;print -z&lt;/code&gt;&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;fastest&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;safe&lt;/strong&gt;, you press Enter&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;none&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mental model: &lt;code&gt;print -z&lt;/code&gt; is what &lt;code&gt;Ctrl-R&lt;/code&gt; history search does when you accept a result. Native zsh. You always see and approve the command before it runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  the heuristic: when is the answer a command?
&lt;/h2&gt;

&lt;p&gt;The smart dispatch decides between &lt;code&gt;print -z&lt;/code&gt; (pre-type) and &lt;code&gt;print -r&lt;/code&gt; (stdout) by looking at the first word of the answer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;a-zA-Z_]&lt;span class="k"&gt;*&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; whence &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$first&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;print &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;else
  &lt;/span&gt;print &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$out&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two checks, both load-bearing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;First char is a letter or underscore.&lt;/strong&gt; Excludes digits (&lt;code&gt;4&lt;/code&gt;), symbols (&lt;code&gt;[&lt;/code&gt;, &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;(&lt;/code&gt;), and anything else that obviously isn't a command name.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;whence -p&lt;/code&gt; resolves it to a PATH executable.&lt;/strong&gt; Not just "this name exists in the shell", but &lt;em&gt;specifically&lt;/em&gt; a real binary on disk.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Why &lt;code&gt;whence -p&lt;/code&gt; and not &lt;code&gt;command -v&lt;/code&gt;? Read on.&lt;/p&gt;

&lt;h2&gt;
  
  
  the footgun: oh-my-zsh numeric aliases
&lt;/h2&gt;

&lt;p&gt;My first attempt used &lt;code&gt;command -v "$first"&lt;/code&gt; as the heuristic. It looked right. It failed in a way that took a minute to spot.&lt;/p&gt;

&lt;p&gt;When I ran &lt;code&gt;p whats 2 + 2&lt;/code&gt;, the answer was &lt;code&gt;4&lt;/code&gt;, but nothing appeared in my terminal. The function exited cleanly with status 0. No error.&lt;/p&gt;

&lt;p&gt;What had happened: oh-my-zsh's &lt;code&gt;dirhistory&lt;/code&gt; plugin (loaded by default in many configs) aliases &lt;code&gt;1&lt;/code&gt; through &lt;code&gt;9&lt;/code&gt; to &lt;code&gt;cd -1&lt;/code&gt; ... &lt;code&gt;cd -9&lt;/code&gt; for jumping around the directory stack. So &lt;code&gt;command -v 4&lt;/code&gt; returned true. &lt;code&gt;4&lt;/code&gt; was a recognized alias, and the function tried to &lt;code&gt;print -z 4&lt;/code&gt; into my prompt buffer.&lt;/p&gt;

&lt;p&gt;In a real interactive shell, that would have stuffed &lt;code&gt;4&lt;/code&gt; into my prompt invisibly (it'd appear when I hit Enter). In my non-interactive test (&lt;code&gt;zsh -ic '...'&lt;/code&gt;) it disappeared into the void because there's no line editor to render the stuffed buffer.&lt;/p&gt;

&lt;p&gt;The fix has two parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;[[ "$first" == [a-zA-Z_]* ]]&lt;/code&gt;&lt;/strong&gt; alone would have caught it, because &lt;code&gt;4&lt;/code&gt; doesn't start with a letter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;whence -p&lt;/code&gt;&lt;/strong&gt; instead of &lt;code&gt;command -v&lt;/code&gt; makes it doubly safe. &lt;code&gt;whence -p&lt;/code&gt; only matches binaries in PATH, ignoring aliases, functions, and builtins. Aliases like &lt;code&gt;4 → cd -4&lt;/code&gt; are filtered out.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either check alone would have caught the bug. Having both means the next time I add a feature here, I don't have to remember which one was load-bearing.&lt;/p&gt;

&lt;h2&gt;
  
  
  defensive details that earn their keep
&lt;/h2&gt;

&lt;p&gt;Three small things prevent subtle bugs:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;noglob&lt;/code&gt; on the alias
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;p&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'noglob p'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without this, &lt;code&gt;p list all *.log files&lt;/code&gt; would have zsh expand &lt;code&gt;*.log&lt;/code&gt; against the current directory &lt;em&gt;before&lt;/em&gt; the function ever sees it. With &lt;code&gt;noglob&lt;/code&gt;, the glob characters pass through literally. Same trick git uses for its arguments.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;emulate -L zsh&lt;/code&gt; + &lt;code&gt;setopt NO_GLOB&lt;/code&gt;
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;emulate &lt;span class="nt"&gt;-L&lt;/span&gt; zsh
setopt NO_GLOB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;emulate -L zsh&lt;/code&gt; resets shell options to defaults, scoped to this function only (the &lt;code&gt;-L&lt;/code&gt; means local, so they restore on return). &lt;code&gt;NO_GLOB&lt;/code&gt; is belt-and-suspenders for callers that bypass the alias (&lt;code&gt;command p ...&lt;/code&gt;, &lt;code&gt;\p ...&lt;/code&gt;, or scripts that don't see your aliases).&lt;/p&gt;

&lt;h3&gt;
  
  
  output sanitization
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'\000-\037'&lt;/span&gt; | &lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="s1"&gt;'s/^[[:space:]]*//;s/[[:space:]]*$//'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tr -d '\000-\037'&lt;/code&gt; strips all C0 control characters. That includes ANSI escape sequences (ESC = &lt;code&gt;\033&lt;/code&gt;), stray nulls, and any invisible cruft the model might emit. Critical for &lt;code&gt;print -z&lt;/code&gt; because control characters in the payload corrupt the line editor's display.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sed&lt;/code&gt; then trims leading and trailing whitespace, which the model usually adds even when told not to.&lt;/p&gt;

&lt;h2&gt;
  
  
  why &lt;code&gt;"$*"&lt;/code&gt; and not &lt;code&gt;"$@"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;"$*"&lt;/code&gt; joins all positional args into one string with spaces between them. &lt;code&gt;"$@"&lt;/code&gt; would pass them as separate args, which most AI CLIs would concatenate anyway, but some treat the first positional as the prompt and the rest as files (the &lt;code&gt;@file.txt&lt;/code&gt; convention is common). Joining explicitly avoids that ambiguity.&lt;/p&gt;

&lt;p&gt;If your CLI supports &lt;code&gt;--&lt;/code&gt; to end option parsing, prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;your-ai-cli &lt;span class="nt"&gt;-p&lt;/span&gt; ... &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pi&lt;/code&gt; doesn't accept &lt;code&gt;--&lt;/code&gt;, hence the bare &lt;code&gt;"$*"&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  the system-prompt nudge actually matters
&lt;/h2&gt;

&lt;p&gt;Without &lt;code&gt;--append-system-prompt&lt;/code&gt;, even with &lt;code&gt;-p&lt;/code&gt;, the default coding-assistant prompt wraps shell commands in code fences and adds a one-sentence intro. That breaks &lt;code&gt;print -z&lt;/code&gt; (code fences are not commands) and clutters the terminal.&lt;/p&gt;

&lt;p&gt;The phrasing that worked best:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Answer in ONE line. No preamble, no explanation, no markdown, no code fences. For shell/kubectl/git/etc requests output only the command. For factual or math questions output only the answer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;"No markdown, no code fences" is doing most of the work. Without it you get backtick-wrapped output that &lt;code&gt;print -z&lt;/code&gt; would happily push into your prompt as &lt;code&gt;`kubectl get pods`&lt;/code&gt;, which is not a runnable command.&lt;/p&gt;

&lt;h2&gt;
  
  
  why this beats the chat UI for short questions
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Chat UI&lt;/th&gt;
&lt;th&gt;&lt;code&gt;p&lt;/code&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Switch context&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Round-trip latency&lt;/td&gt;
&lt;td&gt;~3-5s + UI&lt;/td&gt;
&lt;td&gt;~1-2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output format&lt;/td&gt;
&lt;td&gt;markdown, prose&lt;/td&gt;
&lt;td&gt;bare answer or pre-typed command&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Get command into shell&lt;/td&gt;
&lt;td&gt;select + copy + paste&lt;/td&gt;
&lt;td&gt;already in your prompt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Session pollution&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;no (&lt;code&gt;--no-session&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Glob-expansion footgun&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;guarded (&lt;code&gt;noglob&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For anything longer than a paragraph the chat UI is still better. For "what's the syntax for X" or "the command for Y", the terminal is the right place to put the answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  the one substitution that fixed it
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;command -v&lt;/code&gt; → &lt;code&gt;whence -p&lt;/code&gt;. One swap. The rest of the function (the &lt;code&gt;noglob&lt;/code&gt;, the &lt;code&gt;emulate -L zsh&lt;/code&gt;, the control-char strip) was already doing its job. The bug was trusting that "this name resolves in the shell" meant "this name is a binary on disk." It doesn't, and on any zsh with oh-my-zsh loaded it especially doesn't.&lt;/p&gt;

</description>
      <category>zsh</category>
      <category>shell</category>
      <category>ai</category>
      <category>cli</category>
    </item>
    <item>
      <title>How to bypass corporate MDM and AI gateways on Claude Code</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Mon, 18 May 2026 16:59:08 +0000</pubDate>
      <link>https://dev.to/sachincool/how-to-bypass-corporate-mdm-and-ai-gateways-on-claude-code-27de</link>
      <guid>https://dev.to/sachincool/how-to-bypass-corporate-mdm-and-ai-gateways-on-claude-code-27de</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/bypassing-claude-code-mdm-managed-settings" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-05-08.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you're reading this, there's roughly an 80% chance your company rolled out an MDM last quarter, your network team wedged Claude API traffic through an AI gateway around the same time, and now Claude Code boots with MCPs you didn't pick while forwarding your prompts somewhere you haven't audited. &lt;code&gt;/mcp&lt;/code&gt; shows three servers nothing in your repo touches. &lt;code&gt;env | grep ANTHROPIC&lt;/code&gt; returns a base URL on a domain you've never seen. The experience got worse and nobody asked you.&lt;/p&gt;

&lt;p&gt;This post covers both leashes. The MDM one is fixable in 12 lines of zsh. The AI gateway one depends on how deep your network team went.&lt;/p&gt;

&lt;h2&gt;
  
  
  what's an MDM, in three sentences
&lt;/h2&gt;

&lt;p&gt;MDM stands for Mobile Device Management. Jamf, Kandji, Intune, Workspace ONE, whichever agent enrolled your laptop on day one. It owns parts of &lt;code&gt;/Library&lt;/code&gt;, can write files there as root with the system-immutable flag set, and re-pushes them on a schedule, which is why a plain &lt;code&gt;rm&lt;/code&gt; doesn't survive. For Claude Code, the relevant directory is &lt;code&gt;/Library/Application Support/ClaudeCode/&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  the managed-settings situation
&lt;/h2&gt;

&lt;p&gt;The two files doing the work are &lt;code&gt;/Library/Application Support/ClaudeCode/managed-settings.json&lt;/code&gt; and &lt;code&gt;/Library/Application Support/ClaudeCode/managed-mcp.json&lt;/code&gt;. Claude Code reads them on startup, treats them as the highest-priority settings layer, and merges them over whatever you have in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. Anything IT puts in there wins: forced MCPs, forced skills, allowed and denied permission lists, and the &lt;code&gt;env&lt;/code&gt; block that can set &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt;. That last one is how the AI gateway routing gets wired into Claude Code in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  why &lt;code&gt;rm&lt;/code&gt; doesn't work
&lt;/h2&gt;

&lt;p&gt;First instinct fails, and not in a way that's obvious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo rm&lt;/span&gt; &lt;span class="s2"&gt;"/Library/Application Support/ClaudeCode/managed-settings.json"&lt;/span&gt;
&lt;span class="c"&gt;# rm: managed-settings.json: Operation not permitted&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Root isn't enough. The MDM agent sets the file's system-immutable flag with &lt;code&gt;chflags schg&lt;/code&gt; after writing it. That flag blocks deletion even by root until it's cleared. The macOS &lt;code&gt;chflags(1)&lt;/code&gt; man page is the receipt. &lt;code&gt;schg&lt;/code&gt; is the "system immutable" flag, and the file "may not be changed, moved, or deleted" while it's set.&lt;/p&gt;

&lt;p&gt;Confirm it on your own machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-lO&lt;/span&gt; &lt;span class="s2"&gt;"/Library/Application Support/ClaudeCode/managed-settings.json"&lt;/span&gt;
&lt;span class="c"&gt;# -rw-r--r--  1 root  wheel  schg  482 May 14 09:11 managed-settings.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;schg&lt;/code&gt; in column five is the marker.&lt;/p&gt;

&lt;p&gt;The detail that matters: managed-settings.json is the same config layer your &lt;code&gt;~/.claude/settings.json&lt;/code&gt; uses. The IT copy just lives under &lt;code&gt;/Library&lt;/code&gt;, is owned by root, and has the schg flag set. The merge logic doesn't know which file came from a human.&lt;/p&gt;

&lt;h2&gt;
  
  
  the cleanup script
&lt;/h2&gt;

&lt;p&gt;One thing worth flagging before you run this. On macOS, the &lt;code&gt;schg&lt;/code&gt; flag is normally clearable by root for files outside SIP-protected paths, and &lt;code&gt;/Library/Application Support/ClaudeCode/&lt;/code&gt; is not SIP-protected. So &lt;code&gt;sudo chflags noschg&lt;/code&gt; works as written. If your MDM also writes its config into a SIP-protected location (rare for application config, more common for system extensions), you'd need Recovery Mode Terminal to clear those, which is a different conversation. The script's &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; will silently swallow that failure, so if reruns don't seem to take, that's where to look.&lt;/p&gt;

&lt;p&gt;Save this as &lt;code&gt;/usr/local/sbin/claudecode-cleanup.sh&lt;/code&gt;, make it executable, run with &lt;code&gt;sudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/zsh&lt;/span&gt;
&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="o"&gt;=(&lt;/span&gt;
  &lt;span class="s2"&gt;"/Library/Application Support/ClaudeCode/managed-settings.json"&lt;/span&gt;
  &lt;span class="s2"&gt;"/Library/Application Support/ClaudeCode/managed-mcp.json"&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;f &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;FILES&lt;/span&gt;&lt;span class="p"&gt;[@]&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c"&gt;# Clear immutable flag if file exists, then remove&lt;/span&gt;
  &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; /usr/bin/chflags noschg &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null
  /bin/rm &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo chmod &lt;/span&gt;755 /usr/local/sbin/claudecode-cleanup.sh
&lt;span class="nb"&gt;sudo&lt;/span&gt; /usr/local/sbin/claudecode-cleanup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lines do the real work. &lt;code&gt;chflags noschg&lt;/code&gt; clears the immutable bit. &lt;code&gt;rm -f&lt;/code&gt; removes the file. The &lt;code&gt;2&amp;gt;/dev/null&lt;/code&gt; swallows the noise on a clean machine where the file isn't there.&lt;/p&gt;

&lt;p&gt;Restart Claude Code. &lt;code&gt;/mcp&lt;/code&gt; should be back to whatever you actually installed, and &lt;code&gt;/permissions&lt;/code&gt; should be whatever's in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; instead of whatever IT decided you needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  the launchd arms race
&lt;/h2&gt;

&lt;p&gt;I'd love to tell you this is permanent. It isn't.&lt;/p&gt;

&lt;p&gt;MDM agents sync on a schedule. Every 15 minutes, every hour, on login, depending on profile. When they sync, they notice the file is gone, put it back, and re-apply the schg flag. You'll watch managed-mcp.json reappear like a horror-movie villain you keep stabbing.&lt;/p&gt;

&lt;p&gt;A few options, in increasing order of trouble you're inviting:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Run the script on a launchd LaunchAgent that fires at login.&lt;/strong&gt; Once per session. Low impact, low effectiveness, but if your MDM only syncs at login this is enough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Run it on a launchd timer with a 60-second interval.&lt;/strong&gt; Now you're in an arms race with the sync schedule. Works until someone in IT notices a config-drift alert for your hostname.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Block the MDM agent's outbound DNS.&lt;/strong&gt; Effective, loud, and the kind of thing that gets your laptop wiped on the next compliance audit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I run the first one. The MDM gets its login telemetry, my dev environment isn't broken for the hour or so between syncs, nobody opens a ticket. Pick the option that matches how much you actually want to fight this.&lt;/p&gt;

&lt;p&gt;Minimal &lt;code&gt;~/Library/LaunchAgents/cloud.harshit.claudecode-cleanup.plist&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;plist&lt;/span&gt; &lt;span class="na"&gt;version=&lt;/span&gt;&lt;span class="s"&gt;"1.0"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;dict&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;Label&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;string&amp;gt;&lt;/span&gt;cloud.harshit.claudecode-cleanup&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;ProgramArguments&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;array&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/bin/sudo&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;-n&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;string&amp;gt;&lt;/span&gt;/usr/local/sbin/claudecode-cleanup.sh&lt;span class="nt"&gt;&amp;lt;/string&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/array&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;key&amp;gt;&lt;/span&gt;RunAtLoad&lt;span class="nt"&gt;&amp;lt;/key&amp;gt;&amp;lt;true/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/dict&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/plist&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;sudo -n&lt;/code&gt; only works if you've added a NOPASSWD line for that exact script in &lt;code&gt;/etc/sudoers.d/claudecode-cleanup&lt;/code&gt;. Which the MDM might rewrite. The arms race goes deeper than you think.&lt;/p&gt;

&lt;h2&gt;
  
  
  the AI gateway angle
&lt;/h2&gt;

&lt;p&gt;The other leash sits at the network layer. Companies route Claude API traffic through a gateway (Cloudflare AI Gateway, Portkey, LiteLLM, internal proxies) so they can log prompts, strip PII, enforce per-user quotas, or quietly downgrade Opus calls to Haiku when the monthly bill spikes. Claude Code respects &lt;code&gt;ANTHROPIC_BASE_URL&lt;/code&gt; and will talk to whatever endpoint it points at, as long as your OAuth token or API key authenticates there.&lt;/p&gt;

&lt;p&gt;Two routing patterns to recognize:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The env block in managed-settings.json.&lt;/strong&gt; IT sets &lt;code&gt;ANTHROPIC_BASE_URL=https://ai-gw.corp.example.com/v1&lt;/code&gt; inside the env section of the managed file. Claude Code reads it on startup. Same fix as the MCP file. The cleanup script above already kills this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System proxy plus a corporate root CA.&lt;/strong&gt; Your laptop has a "Corporate Root CA" in keychain, and either an &lt;code&gt;https.proxy&lt;/code&gt; setting or transparent network interception routes api.anthropic.com traffic through the gateway. Deleting managed-settings.json does nothing here. The interception lives below the application layer.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To tell which one you have, run this in a fresh shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; anthropic
&lt;span class="c"&gt;# If you see ANTHROPIC_BASE_URL, it's the env block.&lt;/span&gt;

curl &lt;span class="nt"&gt;-v&lt;/span&gt; https://api.anthropic.com/v1/messages 2&amp;gt;&amp;amp;1 | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-iE&lt;/span&gt; &lt;span class="s1"&gt;'issuer|subject|server certificate'&lt;/span&gt;
&lt;span class="c"&gt;# If the cert chain is signed by your corporate CA, it's transparent interception.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  bypassing the gateway
&lt;/h2&gt;

&lt;p&gt;For the env-block case, the cleanup script already does the work. Restart your shell after running it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;unset &lt;/span&gt;ANTHROPIC_BASE_URL
&lt;span class="nb"&gt;env&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; anthropic
&lt;span class="c"&gt;# (empty)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the transparent-proxy case, your options shrink:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Personal hotspot for sensitive sessions.&lt;/strong&gt; Burns mobile data, leaves no trail through the gateway. Most realistic option for an individual contributor.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WireGuard or Tailscale out to a personal node.&lt;/strong&gt; Works if your MDM profile allows it. Many block third-party VPNs through &lt;code&gt;com.apple.systempolicy.kernel-extension-policy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal device for personal work.&lt;/strong&gt; Boring answer. The one that holds up in HR if it ever comes up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What doesn't work: removing the corporate root CA from keychain. It's pinned by an MDM payload and gets re-added on next sync, same pattern as managed-settings.json.&lt;/p&gt;

&lt;h2&gt;
  
  
  should you actually do this
&lt;/h2&gt;

&lt;p&gt;Worth saying out loud: both leashes exist because someone at your company had a reason. Compliance, data residency, an incident from six months ago whose postmortem nobody can find.&lt;/p&gt;

&lt;p&gt;If the forced MCP is &lt;code&gt;internal-secrets-lookup&lt;/code&gt; and the gateway logs prompts to a SOC pipeline, your team probably wants you using it. If the MCP is &lt;code&gt;corporate-docs-mcp&lt;/code&gt; pointed at a 404 and the gateway downgrades Opus to Haiku because someone misread an invoice, you're deleting dead weight.&lt;/p&gt;

&lt;p&gt;The script doesn't know which. Ask before you script. Most MDM platforms support per-user opt-out scopes, and one polite Slack message to IT beats a &lt;code&gt;launchd&lt;/code&gt; plist.&lt;/p&gt;

&lt;h2&gt;
  
  
  what these scripts don't do
&lt;/h2&gt;

&lt;p&gt;The cleanup clears two files. It does not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stop the MDM agent.&lt;/li&gt;
&lt;li&gt;Touch &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. Your settings stay yours.&lt;/li&gt;
&lt;li&gt;Handle &lt;code&gt;/Library/Application Support/ClaudeCode/managed-permissions.json&lt;/code&gt; if your MDM uses one. Add it to the &lt;code&gt;FILES&lt;/code&gt; array.&lt;/li&gt;
&lt;li&gt;Survive a reboot or a sync. The agent re-pushes on next check-in.&lt;/li&gt;
&lt;li&gt;Defeat a transparent proxy with a pinned corporate CA. Use the hotspot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you wanted a permanent escape from corporate IT, you wouldn't be reading a blog about &lt;code&gt;chflags&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>mdm</category>
      <category>aigateway</category>
      <category>macos</category>
    </item>
    <item>
      <title>Lazy SRE's guide to secure systems, part 5: the dev laptop is the perimeter</title>
      <dc:creator>Harshit Luthra</dc:creator>
      <pubDate>Mon, 18 May 2026 16:58:52 +0000</pubDate>
      <link>https://dev.to/sachincool/lazy-sres-guide-to-secure-systems-part-5-the-dev-laptop-is-the-perimeter-3109</link>
      <guid>https://dev.to/sachincool/lazy-sres-guide-to-secure-systems-part-5-the-dev-laptop-is-the-perimeter-3109</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://harshit.cloud/blog/lazy-security-part-5-dev-laptops" rel="noopener noreferrer"&gt;harshit.cloud&lt;/a&gt; on 2026-05-03.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;In June 2024, Mandiant published the writeup for the Snowflake mass-extortion campaign. Ticketmaster, Santander, AT&amp;amp;T, LendingTree, Advance Auto Parts. Roughly 165 Snowflake tenants in total had data extracted from their warehouses. The defining detail wasn't sophistication. It was the laptop.&lt;/p&gt;

&lt;p&gt;Mandiant traced the entry point to infostealer malware (Lumma, RedLine, Vidar variants) running on contractor and developer machines. Their report described the affected devices as personal systems also used for gaming and downloading pirated software. The infostealer harvested every credential the browser had ever saved, including the Snowflake login that didn't have MFA enforced. The attackers walked through the front door of a Fortune 500's data warehouse.&lt;/p&gt;

&lt;p&gt;This is part 5. Earlier parts covered npm (&lt;a href="https://dev.to/blog/lazy-security-part-1-supply-chain"&gt;Part 1&lt;/a&gt;), GitHub Actions (&lt;a href="https://dev.to/blog/lazy-security-part-2-github-actions"&gt;Part 2&lt;/a&gt;), the unsexy infrastructure list (&lt;a href="https://dev.to/blog/lazy-security-part-3-unsexy-list"&gt;Part 3&lt;/a&gt;), and DNS auth records (&lt;a href="https://dev.to/blog/lazy-security-part-4-dns-records"&gt;Part 4&lt;/a&gt;). Part 5 is about the laptop. The piece of hardware on an engineer's desk that has every SSH key, AWS profile, kubeconfig, GitHub PAT, Slack token, and Stripe key they have ever used to do their job.&lt;/p&gt;

&lt;p&gt;The thesis from Part 1 stands. Future You at 3am will not run an EDR scan after every browser extension install. The config that prevents the extension from being installed in the first place is the one that runs while you sleep: the MDM that whitelists, the disk encryption that protects what gets stolen, the hardware MFA that survives the keylogger.&lt;/p&gt;

&lt;h2&gt;
  
  
  MDM is the table you set first
&lt;/h2&gt;

&lt;p&gt;Mobile Device Management is the thing every small startup skips and every enterprise has. The bad-faith reason is that it's expensive and annoying. The honest reason in 2026 is that the free options have caught up.&lt;/p&gt;

&lt;p&gt;For a 15-person Apple-heavy team, the lazy stack is &lt;strong&gt;Apple Business Manager&lt;/strong&gt; (free, Apple-only) plus &lt;strong&gt;Fleet&lt;/strong&gt; (OSS, free under 300 endpoints on the self-hosted path, generous free tier on Fleet's cloud). Apple Business Manager assigns a Mac to your organization at first boot, before the user creates a personal Apple ID on it. Fleet runs the osquery agent on every machine and lets you push configuration profiles (the same plist payloads Jamf would push) plus query inventory in SQL syntax.&lt;/p&gt;

&lt;p&gt;The lazy default config profile, in plain English:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Require FileVault. Escrow the recovery key to MDM. If the laptop walks, the disk is encrypted; if the user forgets the password, you can recover.&lt;/li&gt;
&lt;li&gt;Require auto-lock at five minutes idle, password to wake. Not a screensaver.&lt;/li&gt;
&lt;li&gt;Block unsigned package installs, restrict the Mac App Store to managed Apple IDs only.&lt;/li&gt;
&lt;li&gt;Require macOS updates within fourteen days of release. The fourteen days lets you skip a known-bad point release; longer than fourteen is negligence.&lt;/li&gt;
&lt;li&gt;Block AirDrop on the corporate Wi-Fi, restrict USB external storage to read-only (or block entirely if your workflow doesn't need it).&lt;/li&gt;
&lt;li&gt;Install osquery via MDM, enrolled to your Fleet server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For Linux and Windows in the mix, Fleet covers both with the same osquery agent and the same query syntax. The MDM-config-profile half is Windows Intune (free with Microsoft 365 Business Premium) or Workspace ONE's free tier. Either way, the stack is "Fleet for inventory and detections + a platform-specific MDM for enforcement."&lt;/p&gt;

&lt;p&gt;The lazy fix for the most common gap: a weekly cron that runs one Fleet query, "every laptop without FileVault enabled," and posts a Slack alert with the user's name. The conversation that follows is "we found your machine, can you enable it today", not a six-month audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  hardware keys, one-time spend
&lt;/h2&gt;

&lt;p&gt;YubiKey 5 NFC is $50. Buy two per engineer: one for the desk, one for the bag. Total for 15 engineers: $1,500, one-time, capital expense, deductible.&lt;/p&gt;

&lt;p&gt;What it gets you:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WebAuthn / FIDO2 for SSO login (Google, Okta, GitHub, Cloudflare, AWS): a keylogger can record every keystroke and still never get the second factor.&lt;/li&gt;
&lt;li&gt;SSH key storage in hardware. &lt;code&gt;ssh-keygen -t ed25519-sk -O resident&lt;/code&gt; writes the key into the YubiKey. The private key never exists on disk.&lt;/li&gt;
&lt;li&gt;PIV smartcard for VPN auth, code signing (&lt;code&gt;gpg --card-edit&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;TOTP fallback for the SaaS that hasn't shipped WebAuthn yet.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The free alternative for the SaaS that doesn't support hardware keys is passkeys. Passkeys are WebAuthn under the hood, also phishing-resistant, built into iOS, macOS, Android, Windows Hello, Chrome, and Safari. Free. The catch is sync: if the engineer's iCloud is compromised, so is the passkey. Hardware keys aren't synced; they are a physical token. The lazy answer is both: passkeys for low-risk auth, YubiKeys for the keys that gate production.&lt;/p&gt;

&lt;p&gt;Cost: $1,500 one-time for 15 engineers. The cheapest line item in this post for what it gets you.&lt;/p&gt;

&lt;h2&gt;
  
  
  EDR is where the budget goes
&lt;/h2&gt;

&lt;p&gt;Endpoint Detection and Response is the part of this stack that costs real money. For OSS-only, the answer is osquery + Wazuh, which works but requires writing detections by hand. For a 15-person team with one platform engineer, "write your own EDR detections" is not a project anyone will finish.&lt;/p&gt;

&lt;p&gt;The honest 2026 small-team answer is &lt;strong&gt;Microsoft Defender for Business&lt;/strong&gt; at $3/user/month. It ships in Microsoft 365 Business Premium (also useful if you're on M365 anyway), has acceptable macOS coverage, and includes managed detections written by Microsoft's security team. Cost for 15 engineers: $540/year. &lt;strong&gt;CrowdStrike Falcon Go&lt;/strong&gt; is $60/endpoint/year if you want the strongest detection at small-team scale; same math, $900/year for 15.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fznvd4iki3kai24wn1vss.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fznvd4iki3kai24wn1vss.gif" alt="An animated horizontal bar chart in a dark editorial palette comparing the annual endpoint stack cost for a 15-engineer team across three configurations. Top bar: OSS-only (osquery + Wazuh self-hosted) at roughly $240/year (just the VPS). Middle bar (accented, brighter cyan, coral tip): Defender for Business at $540/year, the recommended default. Bottom bar: CrowdStrike Falcon Go at $900/year. A small note underneath each bar shows what each catches and what each misses; a strip at the bottom reads 'one-time YubiKey spend not included ($1,500 for 15 engineers across all three).'" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 2 · three configurations. Pick the middle bar unless you have a reason.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The lazy stance: Defender for Business if you're on Microsoft 365 already. Falcon Go if you're not on M365 and want managed detection without the OSS-engineer overhead. osquery + Wazuh only if you have a security engineer with bandwidth to maintain the detections, which most 15-person startups don't. Pretending otherwise is how you end up with a fancy SIEM nobody reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  the password manager and browser hygiene argument
&lt;/h2&gt;

&lt;p&gt;1Password Business at ~$8/user/month. Bitwarden Teams at $4. Apple Passwords (or 1Password Families) if you're Mac-only and don't need shared vaults. Pick one and stop arguing about it on the team's &lt;code&gt;#tools&lt;/code&gt; channel.&lt;/p&gt;

&lt;p&gt;The point of the password manager isn't strong passwords. The point is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One place for credentials, audited.&lt;/li&gt;
&lt;li&gt;Shared vaults for vendor logins, instead of "share the password in Slack DM" hygiene.&lt;/li&gt;
&lt;li&gt;Breach notifications when a saved password appears in a public breach corpus.&lt;/li&gt;
&lt;li&gt;Masked email aliases (1Password feature, Apple's Hide My Email equivalent): every signup gets a separate alias, every spam list is contained.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Browser hygiene matters because the Snowflake infostealer harvested credentials from browser local storage. Specifically:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Enforce browser auto-updates via MDM. Both Chrome and Edge expose policy keys for this; Firefox via &lt;code&gt;policies.json&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Block sync of work browser profiles to personal Google/Apple accounts. The "I signed into Chrome with my personal account and now all my work bookmarks are in someone else's cloud" leak is real.&lt;/li&gt;
&lt;li&gt;Block "developer mode" extension installs. Force extensions to come from the Chrome Web Store; force the Web Store to honor the org's allowlist via the &lt;code&gt;ExtensionInstallAllowlist&lt;/code&gt; policy.&lt;/li&gt;
&lt;li&gt;Disable browser password saving entirely. Everything routes through the password manager.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total: $1,440/year for 15 engineers on 1Password Business. $720 on Bitwarden Teams. $0 on Apple Passwords if it covers your needs. Pick a line and walk it.&lt;/p&gt;

&lt;h2&gt;
  
  
  the personal device problem
&lt;/h2&gt;

&lt;p&gt;The Snowflake breach was about contractors using personal Macs for work. The lazy answer at a 15-person startup might surprise: corp-issue every contractor a laptop. Yes, including the four-hour-a-week consultant.&lt;/p&gt;

&lt;p&gt;A refurbished MacBook Air with 16GB RAM is roughly $700 from Apple's Certified Refurbished store. The cost of a Snowflake-scale breach starts at $370K (the reported AT&amp;amp;T ransom) and ends in the customer-churn and legal-exposure column. The break-even point on hardware-for-contractors is under three serious incidents, ever.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqabyha68mwr3upib5m0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftqabyha68mwr3upib5m0.png" alt="An editorial side-by-side system diagram on a dark navy ground. Left panel labeled 'personal device, BYOD' shows a laptop with chaotic state: unenforced FileVault status, a personal iCloud sign-in, a Mac App Store with personal Apple ID, a Chrome browser synced to a personal Google account, a Slack web app session that's been logged in for nine months, a folder labeled 'pirated software' with a red warning. Right panel labeled 'corp-issued, MDM enrolled' shows the same laptop with each item enforced: FileVault ON, MDM-managed Apple ID, App Store restricted, Chrome work profile only, Slack session expires daily, no third-party software installs. Each enforced item has a green check; each unenforced item on the left has a coral X. A title above reads 'where the Snowflake breach lived'." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Fig. 3 · same laptop, different enrollment. The right panel is the one where Mandiant doesn't write your name down.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;What "no work on personal devices" actually requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Contract clause: hardware is issued, personal-device use for work is prohibited.&lt;/li&gt;
&lt;li&gt;MDM enrollment at first boot via Apple Business Manager (or Windows Autopilot).&lt;/li&gt;
&lt;li&gt;Disabled iCloud personal sign-in; only managed Apple IDs.&lt;/li&gt;
&lt;li&gt;Wipe via MDM on offboarding, before reissue.&lt;/li&gt;
&lt;li&gt;No "I can just SSH from home for ten minutes" escape hatch. The escape hatch is what the contractor will use the day they get phished.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the section of the post that gets the most pushback. The pushback is right about cost and wrong about risk. Run the math at your scale; it runs the same direction every time.&lt;/p&gt;

&lt;h2&gt;
  
  
  the receipts
&lt;/h2&gt;

&lt;p&gt;For 15 engineers, the first-year laptop security budget:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;YubiKey 5 × 30 keys (two per engineer): $1,500, one-time.&lt;/li&gt;
&lt;li&gt;Fleet (OSS self-hosted on a small VPS): $240/year.&lt;/li&gt;
&lt;li&gt;Microsoft Defender for Business: $540/year. Substitute Falcon Go at $900 if not on M365, or osquery+Wazuh at $0 if you have a security engineer.&lt;/li&gt;
&lt;li&gt;1Password Business: $1,440/year. Or Bitwarden Teams at $720. Or Apple Passwords at $0.&lt;/li&gt;
&lt;li&gt;Refurbished corp laptops for non-employee contractors: ~$700 per, as needed.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Total recurring: roughly $1,020–$2,220/year for 15 engineers, depending on the EDR and password-manager line. Add the one-time YubiKey spend and the first year lands at $2,520–$3,720. Call it $14–$21 per engineer per month.&lt;/p&gt;

&lt;p&gt;What it catches: every infostealer that hits a managed laptop (Defender flags it), every credential that lives in the browser (replaced by the password manager), every login that doesn't have phishing-resistant MFA (the YubiKey is required), every personal device touching production (blocked by the no-BYOD policy).&lt;/p&gt;

&lt;p&gt;What it doesn't catch: a determined adversary with physical access and unlimited time. A laptop in a hotel room with no FileVault is owned. A laptop with FileVault and a YubiKey left in the USB-A port overnight is owned slower. Neither situation is what this stack is built for; it is built for the infostealer that landed on the contractor's personal Mac.&lt;/p&gt;

&lt;p&gt;If you do one thing this week, buy two YubiKeys for yourself, enroll them on GitHub, Google, and Okta, and turn off SMS-based MFA on each. Total cost: $100, one hour. Then do the rest of the team next quarter.&lt;/p&gt;

</description>
      <category>security</category>
      <category>devsecops</category>
      <category>lazysre</category>
      <category>endpoint</category>
    </item>
  </channel>
</rss>
