<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Platform on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/platform/</link><description>Recent content in Platform on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 31 May 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/platform/index.xml" rel="self" type="application/rss+xml"/><item><title>Seven greenfield deployment phases for an on-premise LLM platform: from hardware in the room to the first production token</title><link>https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/</link><pubDate>Sun, 31 May 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/greenfield-deployment-phases-on-premise-llm-platform/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The two previous posts in this architectural trilogy settled the pieces: &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">the seven layers of the LLM inference stack&lt;/a> describe the components above the cluster, and &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">the five maturity levels of the platform&lt;/a> describe the strata below. This post settles the &lt;strong>when&lt;/strong>: in what order each thing gets deployed when you start from zero, hardware bought, racks installed, physical cabling done, and you want to reach a cluster serving the first production token to a customer. Seven nominal phases &lt;strong>F0 to F6&lt;/strong> with no calendar commitments, organised by &lt;strong>technical dependencies&lt;/strong> (you do not enter F3 without the F2 gate) and with an identifiable &lt;strong>critical path&lt;/strong>. F0 hardware inventory and electrical/network connectivity. F1 bare metal OS + drivers + container runtime. F2 Kubernetes cluster with CNI and Ceph storage operational. F3 GitOps and infrastructure observability. F4 identity, TLS, secrets and policies. F5 GPU platform with LLM-aware observability. F6 the LLM stack operational and open to production traffic. For each phase: what gets built, what has to be ready first (dependencies between phases), the &lt;strong>gate&lt;/strong> that validates its closure, and the typical trap that delays the critical path. The thesis: an on-premise LLM platform sinks far more often from &lt;strong>sequencing badly&lt;/strong> than from &lt;strong>choosing badly&lt;/strong>. The tools have all been invented; the order is the only thing every team rediscovers.&lt;/p>
&lt;h2 id="you-are-here-the-seven-phases-and-their-dependencies">You are here: the seven phases and their dependencies&lt;/h2>
&lt;p>The phases do not run in pure series. F2 and F3 can be started at the same time to speed things up (installing Kubernetes and preparing the GitOps repo in parallel). F4 can overlap with the tail end of F3. F5 waits for F4 to close because GPU pods demand NetworkPolicy and RBAC from day one. F6 is &lt;strong>a single atomic step&lt;/strong>: the cluster goes into production or it does not.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="dag of phases F0 to F6 with dependencies and critical path">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.f0{fill:#f6e2e2;stroke:#a33}.f1{fill:#f4e3cf;stroke:#a63}.f2{fill:#eef0d0;stroke:#7a3}.f3{fill:#dfe9f5;stroke:#356}.f4{fill:#d8eecf;stroke:#373}.f5{fill:#f5e3d8;stroke:#763}.f6{fill:#ead8f5;stroke:#634}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}.crit{stroke:#c33;stroke-width:2.4;fill:none;marker-end:url(#ac)}&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;marker id="ac" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c33"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">Phase DAG · critical path marked in red&lt;/text>
&lt;rect x="40" y="50" width="110" height="48" class="b f0"/>&lt;text x="95" y="68" text-anchor="middle" class="tiny">F0&lt;/text>&lt;text x="95" y="84" text-anchor="middle" class="sm">Hardware&lt;/text>&lt;text x="95" y="98" text-anchor="middle" class="note">Inventory · network&lt;/text>
&lt;rect x="180" y="50" width="110" height="48" class="b f1"/>&lt;text x="235" y="68" text-anchor="middle" class="tiny">F1&lt;/text>&lt;text x="235" y="84" text-anchor="middle" class="sm">Bare metal&lt;/text>&lt;text x="235" y="98" text-anchor="middle" class="note">OS · drivers&lt;/text>
&lt;rect x="320" y="50" width="110" height="48" class="b f2"/>&lt;text x="375" y="68" text-anchor="middle" class="tiny">F2&lt;/text>&lt;text x="375" y="84" text-anchor="middle" class="sm">k8s cluster&lt;/text>&lt;text x="375" y="98" text-anchor="middle" class="note">Cilium · Ceph&lt;/text>
&lt;rect x="460" y="50" width="110" height="48" class="b f3"/>&lt;text x="515" y="68" text-anchor="middle" class="tiny">F3&lt;/text>&lt;text x="515" y="84" text-anchor="middle" class="sm">GitOps + obs&lt;/text>&lt;text x="515" y="98" text-anchor="middle" class="note">Flux · VM/Loki&lt;/text>
&lt;rect x="600" y="50" width="110" height="48" class="b f4"/>&lt;text x="655" y="68" text-anchor="middle" class="tiny">F4&lt;/text>&lt;text x="655" y="84" text-anchor="middle" class="sm">Identity&lt;/text>&lt;text x="655" y="98" text-anchor="middle" class="note">OIDC · Kyverno&lt;/text>
&lt;rect x="320" y="170" width="110" height="48" class="b f5"/>&lt;text x="375" y="188" text-anchor="middle" class="tiny">F5&lt;/text>&lt;text x="375" y="204" text-anchor="middle" class="sm">GPU plane&lt;/text>&lt;text x="375" y="218" text-anchor="middle" class="note">NVIDIA op · DCGM&lt;/text>
&lt;rect x="600" y="170" width="110" height="48" class="b f6"/>&lt;text x="655" y="188" text-anchor="middle" class="tiny">F6&lt;/text>&lt;text x="655" y="204" text-anchor="middle" class="sm">LLM stack live&lt;/text>&lt;text x="655" y="218" text-anchor="middle" class="note">7 layers active&lt;/text>
&lt;path class="crit" d="M150,74 L180,74"/>
&lt;path class="crit" d="M290,74 L320,74"/>
&lt;path class="crit" d="M430,74 L460,74"/>
&lt;path class="crit" d="M570,74 L600,74"/>
&lt;path class="arr" d="M655,98 L655,170"/>
&lt;path class="arr" d="M375,98 L375,170"/>
&lt;path class="crit" d="M430,194 L600,194"/>
&lt;text x="410" y="262" text-anchor="middle" class="sm" fill="#c33">Critical path: F0 → F1 → F2 → F3 → F4 → F5 → F6&lt;/text>
&lt;text x="410" y="282" text-anchor="middle" class="note">Possible overlaps: F2 ↔ F3 (prepare repo while the cluster is built) · F3 ↔ F4 (policies in audit before enforce)&lt;/text>
&lt;text x="410" y="304" text-anchor="middle" class="note">Not overlappable: F4 before F5 (GPU without RBAC = a bomb) · F5 before F6 (the LLM stack does not start without a GPU plane)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The red arrows are the &lt;strong>critical path&lt;/strong>: the sequential bottleneck that no parallelism can shorten. The grey arrows are dependencies that admit partial overlap. Recognising where to overlap and where not is the difference between a three-month deployment and a six-month one for the same perimeter.&lt;/p>
&lt;h2 id="the-analogy-the-expedition-to-an-eight-thousand-metre-summit">The analogy: the expedition to an eight-thousand-metre summit&lt;/h2>
&lt;p>An expedition to a high alpine summit is not a long trek. It is a series of &lt;strong>camps&lt;/strong> that are set up in order, each with its altitude, its function and its validation gate: if you do not acclimatise properly at base camp, you cannot go up to C1 without risk; if C2 does not have its kitchen and its radio running, you cannot send people higher; if the summit push is attempted without porters in the high camps, there is no safe descent.&lt;/p>
&lt;p>Greenfield deployment of an LLM platform works identically. &lt;strong>F0&lt;/strong> is the arrival of the material at base camp: boxes, sponsors, permits, first review. &lt;strong>F1&lt;/strong> is setting up an operational base camp: kitchen, tents, generator. &lt;strong>F2&lt;/strong> is the climb to C1: there is real altitude now (a k8s cluster running) and you breathe differently. &lt;strong>F3&lt;/strong> is C2: it adds communications, planning and operational acclimatisation. &lt;strong>F4&lt;/strong> is C3, the last night before the push: team roped up, oxygen ready, every protocol verified. &lt;strong>F5&lt;/strong> is the day of the summit push, intense effort, thin margins. &lt;strong>F6&lt;/strong> is the summit and the start of a safe descent: from here on the expedition is in day-to-day operation, no longer in construction.&lt;/p>
&lt;p>The analogy holds two useful lessons: &lt;strong>you do not skip a camp&lt;/strong> (going straight from base camp to the summit kills the team), and &lt;strong>the gates are technical, not emotional&lt;/strong> (if the barometer says storm, you do not set out, however much enthusiasm there is). The platform team that follows those two rules reaches the summit. The one that negotiates them does not.&lt;/p>
&lt;h2 id="f0--hardware-in-the-room-base-camp">F0 — Hardware in the room: base camp&lt;/h2>
&lt;p>&lt;strong>What gets built in this phase.&lt;/strong> An inventory of the hardware received (servers, switches, PDUs, BMC), racks assembled, electrical and data cabling terminated, physical labelling of each machine (rack/U/function), connectivity to the corporate network, management IPs assigned, BMC reachable over VPN with MFA, first ping to each node from the bastion.&lt;/p>
&lt;p>&lt;strong>Dependencies.&lt;/strong> None technical, this is the phase &lt;strong>before the software&lt;/strong>. There are procurement dependencies (servers bought, switches bought), civil works dependencies (a room with enough cooling, a raised floor) and administrative ones (datacenter access for the technicians).&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F0.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Every node appears in the inventory with &lt;code>(hostname, MAC, management IP, data IP, rack, U, function, owner)&lt;/code>.&lt;/li>
&lt;li>Each node&amp;rsquo;s BMC responds to &lt;code>ipmitool power status&lt;/code> and to the HTTPS UI from the management VPN.&lt;/li>
&lt;li>The top-of-rack switch has its configuration versioned in git (even if there is no cluster GitOps yet, the switch configs are).&lt;/li>
&lt;li>A command &lt;code>for h in $(cat hosts); do ping -c1 -W1 $h.mgmt; done&lt;/code> returns 100 % success.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> Cabling &amp;ldquo;however it comes out&amp;rdquo; with no physical labelling and no diagram. When F4 arrives and a NetworkPolicy has to be troubleshot, not knowing which physical interface carries which VLAN doubles the diagnosis time of every incident for ever.&lt;/p>
&lt;p>&lt;strong>Why F0 does not overlap with F1.&lt;/strong> Until every server has a management IP and a live BMC, OS bootstrap cannot be automated. Every hour invested in F0 saves hours in every later phase. It is the phase with the best ROI in the project and the only one that admits no shortcuts.&lt;/p>
&lt;h2 id="f1--bare-metal-the-operational-base-camp">F1 — Bare metal: the operational base camp&lt;/h2>
&lt;p>&lt;strong>What gets built.&lt;/strong> The operating system image (stable Debian or Ubuntu LTS) provisioned via PXE or cloud-init with the &lt;code>cloud-config&lt;/code> versioned in git. Each node has: a coherent hostname, LVM partitions, kernel ≥ 6.6, the &lt;code>containerd&lt;/code> container runtime, NVIDIA drivers on the GPU nodes, &lt;code>chrony&lt;/code> synchronising against your own servers, the operator&amp;rsquo;s SSH key as the only way in, and &lt;code>nvidia-smi&lt;/code> passing a smoke test on the GPU nodes.&lt;/p>
&lt;p>&lt;strong>Dependencies.&lt;/strong> F0 closed. It needs the management network working so PXE responds and so the bastion can reach every node.&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F1.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>ansible -i inventory all -m ping&lt;/code> returns 100 % success (or the equivalent with Salt / Pulumi / etc).&lt;/li>
&lt;li>Every GPU node passes &lt;code>nvidia-smi&lt;/code> showing the expected GPUs with a consistent driver across nodes.&lt;/li>
&lt;li>Each node&amp;rsquo;s clock drifts by less than 50 ms from the reference NTP.&lt;/li>
&lt;li>A physical reboot of a node leaves it in exactly the same state after boot (idempotency).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> NVIDIA drivers installed by hand with &lt;code>apt install&lt;/code> or with NVIDIA&amp;rsquo;s &lt;code>.run&lt;/code> script. It works on day one and breaks the day of the first kernel update. The operational rule already established in &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">the post on the five levels&lt;/a>: the drivers end up managed by the GPU Operator in F5; whatever is done now is only so that &lt;code>nvidia-smi&lt;/code> passes the smoke test, not for production.&lt;/p>
&lt;p>&lt;strong>Possible overlap.&lt;/strong> F1 can start for some nodes while F0 is still being finished on others (real greenfield rarely delivers every server on the same day). The F1 gate is per cluster, not per individual node.&lt;/p>
&lt;h2 id="f2--kubernetes-cluster-operational">F2 — Kubernetes cluster operational&lt;/h2>
&lt;p>&lt;strong>What gets built.&lt;/strong> RKE2 installed with three HA control plane nodes, all workers joined (CPU and GPU), Cilium as CNI with &lt;code>kubeProxyReplacement&lt;/code> enabled and a BGP control plane pointing at the ToR switches from F0, Rook-Ceph deployed on the storage nodes to cover block (RBD), filesystem (CephFS) and object (S3-compatible RGW), &lt;code>kubectl get nodes&lt;/code> returning every node &lt;code>Ready&lt;/code>, a first test pod with a PVC mounting and data persisting after the pod restarts.&lt;/p>
&lt;p>&lt;strong>Dependencies.&lt;/strong> F1 closed (drivers + container runtime). Switches with BGP configured (closed in F0). NVMe disks partitioned or available raw for Ceph OSDs.&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F2.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubectl get nodes -o wide&lt;/code> shows every node &lt;code>Ready&lt;/code> with the expected Kubernetes version.&lt;/li>
&lt;li>A Deployment with replicas=3 and per-node antiAffinity starts and the pods land on different nodes.&lt;/li>
&lt;li>An RWO PVC (RBD) creates a volume, the pod writes data, the pod is deleted, another pod mounts it and reads the data.&lt;/li>
&lt;li>An RWX PVC (CephFS) does the same with two pods writing simultaneously.&lt;/li>
&lt;li>An RGW bucket via &lt;code>s3cmd&lt;/code> or &lt;code>mc&lt;/code> accepts &lt;code>put&lt;/code> and &lt;code>get&lt;/code> over TLS.&lt;/li>
&lt;li>Hubble (the read side of the CNI) shows flow logs between two pods in different namespaces.&lt;/li>
&lt;li>Chaos test: draining a non-GPU worker node; the workloads are rescheduled automatically.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> Starting to &lt;code>kubectl apply&lt;/code> real workloads in F2 without GitOps. The backlog of things-applied-by-hand grows faster than the capacity to migrate it to git afterwards. The rule: in F2 only the cluster &lt;strong>prerequisites&lt;/strong> are applied (CNI, CSI, default storage class). Any application workload waits for F3.&lt;/p>
&lt;p>&lt;strong>Possible overlap.&lt;/strong> F2 ↔ F3. While the cluster is being built, the GitOps repo is prepared in parallel (directory structure, first Helm releases). When F2 closes, Flux is plugged into the repo and everything that was going to be &lt;code>kubectl apply&lt;/code> is already a reconciled manifest.&lt;/p>
&lt;h2 id="f3--gitops-and-infrastructure-observability">F3 — GitOps and infrastructure observability&lt;/h2>
&lt;p>&lt;strong>What gets built.&lt;/strong> Forgejo deployed first (it is a prerequisite for everything that follows). A &lt;code>gitops-infra&lt;/code> repo with the initial structure (&lt;code>apps/&lt;/code>, &lt;code>infrastructure/&lt;/code>, &lt;code>tenants/&lt;/code>, &lt;code>clusters/&lt;/code>). Flux installed and reconciling that repo. The prerequisite workloads applied by hand in F2 are moved to the repo and reconciled (no operational &lt;code>kubectl apply&lt;/code> remains). VictoriaMetrics + vmagent scraping metrics. Grafana with initial dashboards (USE/RED + cluster + Ceph + Cilium). Loki receiving logs via vector/fluent-bit. Alertmanager + Keep routing alerts to a chat channel. Barman Cloud backups for Postgres (future CNPG) and scheduled Ceph snapshots.&lt;/p>
&lt;p>&lt;strong>Dependencies.&lt;/strong> F2 closed. An RGW bucket to store backups (covered by the Ceph from F2).&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F3.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>A change applied to the repo is reflected in the cluster in under 5 minutes with no manual intervention.&lt;/li>
&lt;li>A change applied with &lt;code>kubectl edit&lt;/code> directly to the cluster is detected by Flux and reverted (binding drift detection, not merely observational).&lt;/li>
&lt;li>Grafana shows dashboards for the cluster, Ceph, Cilium and GPU nodes (DCGM does not arrive until F5, but basic node metrics do).&lt;/li>
&lt;li>A test alert sent to Alertmanager reaches the chat channel in under 1 minute.&lt;/li>
&lt;li>Restoring a Postgres backup on a temporary cluster returns coherent data (the test defines the real RPO).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> Having Helm charts in git but still applying them with &lt;code>helm install&lt;/code> from the terminal. That is level 1 dressed up as level 2. F3 only closes when Flux is &lt;strong>the only authority&lt;/strong> that applies changes and humans edit the repo, not the cluster.&lt;/p>
&lt;p>&lt;strong>Possible overlap.&lt;/strong> F3 ↔ F4. While F3 is being closed, the Defguard and cert-manager manifests can be prepared in the repo. When they reconcile they have somewhere to land.&lt;/p>
&lt;h2 id="f4--identity-certificates-secrets-policies">F4 — Identity, certificates, secrets, policies&lt;/h2>
&lt;p>&lt;strong>What gets built.&lt;/strong> Defguard deployed with its dedicated Postgres (CNPG). An initial realm with the platform operators enrolled with MFA and WireGuard. OIDC integrated into kube-apiserver (&lt;code>--oidc-issuer-url&lt;/code>, &lt;code>--oidc-client-id&lt;/code>, &lt;code>--oidc-username-claim&lt;/code>), into Forgejo, into Grafana, into Alertmanager, one single SSO. cert-manager installed with an internal CA issuing internal certs for mTLS and with Let&amp;rsquo;s Encrypt ACME for edge certs. SOPS configured with a KMS (it can be a physical HSM, an age key in a vault, or an external Vault) and External Secrets Operator syncing secrets into the cluster. Kyverno deployed with initial policies in &lt;code>audit&lt;/code> mode for a week, then promoted to &lt;code>enforce&lt;/code>. A default-deny NetworkPolicy applied to every existing namespace. Tetragon enabled for runtime security. The kube-apiserver audit log sent to Loki with long retention.&lt;/p>
&lt;p>&lt;strong>Dependencies.&lt;/strong> F3 closed (Flux applies the manifests, VM/Loki ingest metrics and logs).&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F4.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubectl&lt;/code> with a shared admin &lt;code>kubeconfig&lt;/code> &lt;strong>stops working&lt;/strong>; every operator uses their own OIDC token with MFA.&lt;/li>
&lt;li>A secret in plain &lt;code>data:&lt;/code> in a commit is rejected by the pre-commit hook (or by Kyverno admission).&lt;/li>
&lt;li>A pod without &lt;code>securityContext.runAsNonRoot=true&lt;/code> is rejected by Kyverno at admission.&lt;/li>
&lt;li>A deliberately wrong NetworkPolicy (allow-all) in a tenant namespace is rejected.&lt;/li>
&lt;li>An audit of the last day returns the complete list of actors and changes (minimum regulatory footprint).&lt;/li>
&lt;li>Basic internal pen-test: an attacker with a forged &lt;code>kubeconfig&lt;/code> fails MFA and is recorded.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> Kyverno in permanent &lt;code>audit&lt;/code> mode because &amp;ldquo;we do not want to break production workloads&amp;rdquo;. F4 closes when the policies are in &lt;code>enforce&lt;/code>. Until then, you are still in F3 wearing an F4 face.&lt;/p>
&lt;p>&lt;strong>Why F4 does not overlap with F5.&lt;/strong> F5 introduces GPU pods that move a lot of VRAM and a lot of compute. Without default-deny NetworkPolicy, without OIDC RBAC, without Kyverno blocking insecure configurations, the GPU pods are the juiciest attack surface in the cluster. Any compromise in F5 with F4 not closed is near-total access to the expensive hardware.&lt;/p>
&lt;h2 id="f5--gpu-platform-with-llm-aware-observability">F5 — GPU platform with LLM-aware observability&lt;/h2>
&lt;p>&lt;strong>What gets built.&lt;/strong> NVIDIA GPU Operator via Flux with the driver version decided in F1 (no longer touched by hand). DCGM Exporter exposing GPU metrics to VictoriaMetrics. MIG manager configured for the nodes where it makes sense (for example, on a 4×H100 SXM cluster: two GPUs with full passthrough for the general LLM at TP=4, two GPUs partitioned into 2×3g.40gb each for small LLMs and embeddings). Topology Manager with the &lt;code>single-numa-node&lt;/code> policy. KEDA with the Prometheus scaler installed and an example ScaledObject pointing at a vLLM metric (&lt;code>vllm:num_requests_running&lt;/code>). OpenTelemetry Collector with OTLP receivers, &lt;code>attributes&lt;/code> processors (enriching spans with &lt;code>tenant_id&lt;/code>, &lt;code>priority_tier&lt;/code>), exporters to Langfuse and to Tempo. LeaderWorkerSet API enabled for tensor parallel topologies. OME (Operator Model Engine) or vLLM Production Stack deployed as a controller, still with no models loaded.&lt;/p>
&lt;p>&lt;strong>Dependencies.&lt;/strong> F4 closed (the GPU pods inherit default-deny NetworkPolicy, OIDC RBAC and Kyverno policies).&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F5.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>A test pod requesting &lt;code>nvidia.com/gpu: 1&lt;/code> is scheduled on the right node and &lt;code>nvidia-smi&lt;/code> from inside the container sees the right GPU (whole or a MIG slice).&lt;/li>
&lt;li>DCGM Exporter exposes metrics in Grafana (utilisation, VRAM, temperature, NVLink bandwidth) for every GPU.&lt;/li>
&lt;li>A test vLLM Deployment starts with a small model (for example, a 7B FP16) loaded from Ceph RGW.&lt;/li>
&lt;li>An OpenTelemetry span generated by that vLLM reaches Langfuse with correct &lt;code>gen_ai.*&lt;/code> attributes.&lt;/li>
&lt;li>KEDA scales the test Deployment from 1 to N replicas under synthetic load and back to 1 when it stops.&lt;/li>
&lt;li>A GPU Operator upgrade to a new version drains and reschedules the GPU pods with no loss of service.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> Loading the large model &amp;ldquo;just to try&amp;rdquo; before DCGM and OTel are green. When something fails, there will be no metrics to distinguish OOM, thermal throttling, a driver mismatch or a network problem, and you diagnose blind. The rule: &lt;strong>small model first&lt;/strong>, golden path green, &lt;strong>then&lt;/strong> the large model.&lt;/p>
&lt;p>&lt;strong>Possible overlap.&lt;/strong> None with F6. F6 is atomic.&lt;/p>
&lt;h2 id="f6--llm-stack-in-production">F6 — LLM stack in production&lt;/h2>
&lt;p>&lt;strong>What gets built.&lt;/strong> The &lt;strong>seven layers of the inference stack&lt;/strong> described in the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">corresponding post&lt;/a>, deployed in this order:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Vector store + relational data&lt;/strong> (Qdrant, PostgreSQL CNPG, Ceph RGW for weights and adapters, CephFS for datasets). Some components already existed from F3 as data; here they specialise for RAG with their initial collections and schemas.&lt;/li>
&lt;li>&lt;strong>Embeddings + reranker&lt;/strong> (Infinity with &lt;code>multilingual-e5-large&lt;/code>, TEI with &lt;code>bge-reranker-v2-m3&lt;/code>). This is the layer that must be green before any large model, because RAG depends on it.&lt;/li>
&lt;li>&lt;strong>LLM inference&lt;/strong> (vLLM Production Stack with the general LLM and the code LLM). It loads models from Ceph RGW. The initial multi-LoRA pool is empty.&lt;/li>
&lt;li>&lt;strong>Gateway&lt;/strong> (Envoy AI Gateway) with Defguard OAuth, routing by &lt;code>body.model&lt;/code>, rate-limit per tenant. This is the point that &lt;strong>opens traffic to the outside&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>LLM-aware observability&lt;/strong> (Langfuse plugged into the OTel from F5).&lt;/li>
&lt;li>&lt;strong>GitOps control plane&lt;/strong> and &lt;strong>dependency tracking&lt;/strong> were already active since F3 and F4 respectively; here you simply add the catalogue of the new LLM services to them.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Dependencies.&lt;/strong> All the previous phases closed.&lt;/p>
&lt;p>&lt;strong>Validation gate that closes F6.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>A curl to the public endpoint with a Defguard bearer token receives a correct chat completion response in technical Spanish, with &lt;code>trace_id&lt;/code> propagated.&lt;/li>
&lt;li>The trace appears in Langfuse with complete &lt;code>gen_ai.*&lt;/code> attributes, a latency breakdown and its own &lt;code>tenant_id&lt;/code>.&lt;/li>
&lt;li>A 5 % traffic canary to the new model over 24 h degrades neither quality nor latency metrics.&lt;/li>
&lt;li>A controlled traffic burst triggers KEDA, the replicas scale, and P95 latency stays within budget.&lt;/li>
&lt;li>A deliberate failure of one vLLM pod does not affect endpoint availability (replicas + reschedule).&lt;/li>
&lt;li>The internal operator demonstrates the complete path for revoking a tenant&amp;rsquo;s access in under 5 minutes (Defguard → Kyverno → closing the NetworkPolicy).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Typical trap.&lt;/strong> Opening real customer traffic before having the incident runbook signed off, the SLO negotiated and the continuity plan tested. F6 is technically closed; operationally, the platform is still an experiment until the first real postmortem proves the team knows how to respond.&lt;/p>
&lt;h2 id="the-maths-that-matter-relative-weight-of-effort-per-phase">The maths that matter: relative weight of effort per phase&lt;/h2>
&lt;p>Without committing to calendar weeks, we can quantify the &lt;strong>relative weight&lt;/strong> of the engineering effort per phase in a typical greenfield. The curve is not uniform:&lt;/p>
$$
\text{effort}_{F_i} \approx \text{base}_i \cdot (1 + \epsilon_i)
$$
&lt;p>where $\text{base}_i$ is the nominal effort and $\epsilon_i$ is the &lt;strong>surprises&lt;/strong> factor (wrong cabling, incompatible drivers, badly issued certificates, version conflicts). The table below gives the nominal relative weight and the typical surprise factor observed:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Phase&lt;/th>
&lt;th>Nominal weight&lt;/th>
&lt;th>Typical surprise factor ε&lt;/th>
&lt;th>Mean effective weight&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>F0 — Hardware&lt;/td>
&lt;td>8 %&lt;/td>
&lt;td>0.5 (1× to 2×)&lt;/td>
&lt;td>&lt;strong>12 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F1 — Bare metal&lt;/td>
&lt;td>6 %&lt;/td>
&lt;td>0.3&lt;/td>
&lt;td>8 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F2 — k8s cluster&lt;/td>
&lt;td>12 %&lt;/td>
&lt;td>0.4&lt;/td>
&lt;td>17 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F3 — GitOps + obs&lt;/td>
&lt;td>14 %&lt;/td>
&lt;td>0.5&lt;/td>
&lt;td>21 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F4 — Identity + policies&lt;/td>
&lt;td>18 %&lt;/td>
&lt;td>0.7&lt;/td>
&lt;td>&lt;strong>31 %&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F5 — GPU plane&lt;/td>
&lt;td>10 %&lt;/td>
&lt;td>0.4&lt;/td>
&lt;td>14 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>F6 — LLM stack live&lt;/td>
&lt;td>8 %&lt;/td>
&lt;td>0.3&lt;/td>
&lt;td>10 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Buffer / integration&lt;/td>
&lt;td>24 %&lt;/td>
&lt;td>—&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two operational observations. &lt;strong>F4 concentrates more surprises than any other phase&lt;/strong> (OIDC federation across four or five apps with different configurations, Kyverno policies that knock over legitimate workloads, secrets broken by poorly tested encryption). &lt;strong>F0 has a high surprise coefficient relative to its size&lt;/strong> because any cabling or labelling error is discovered late and paid for dearly. The two practical consequences: plan &lt;strong>F4 with generous margin&lt;/strong> and do not skimp on time in &lt;strong>F0&lt;/strong>, because every hour saved there costs five later.&lt;/p>
&lt;p>&lt;strong>Critical path and slack.&lt;/strong> The critical path is linear F0 → F1 → F2 → F3 → F4 → F5 → F6. The only real slack is the overlaps already identified:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>F2 ↔ F3 (slack ~30 %)&lt;/strong>: prepare the repo and initial dashboards while the cluster is being built.&lt;/li>
&lt;li>&lt;strong>F3 ↔ F4 (slack ~20 %)&lt;/strong>: identity manifests ready as F3 closes, applied immediately.&lt;/li>
&lt;li>&lt;strong>Within F4&lt;/strong>: policies in &lt;code>audit&lt;/code> mode running in parallel with the Defguard setup.&lt;/li>
&lt;/ul>
&lt;p>Nothing shortens the critical path by more than about 15 % of the total. Anyone promising a productive greenfield in half a reasonable timescale is selling something else: probably skipping F4, or loading F6 with an F5 that is green-but-not-validated.&lt;/p>
&lt;h2 id="final-diagram-the-complete-deployment-schedule">Final diagram: the complete deployment schedule&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="complete phased deployment schedule with pieces and gates">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.bg{fill:#fafafa;stroke:#bbb;rx:8}.f0{fill:#f6e2e2;stroke:#a33}.f1{fill:#f4e3cf;stroke:#a63}.f2{fill:#eef0d0;stroke:#7a3}.f3{fill:#dfe9f5;stroke:#356}.f4{fill:#d8eecf;stroke:#373}.f5{fill:#f5e3d8;stroke:#763}.f6{fill:#ead8f5;stroke:#634}.gate{fill:#fffbe0;stroke:#a90}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">Complete schedule: pieces per phase and validation gates&lt;/text>
&lt;rect x="40" y="40" width="740" height="68" class="b f0"/>&lt;text x="60" y="58" class="tiny">F0 · HARDWARE IN THE ROOM&lt;/text>
&lt;text x="60" y="76" class="sm">Inventory · cabling · BMC TLS+MFA · management IPs · BGP switches versioned&lt;/text>
&lt;text x="60" y="92" class="note">Gate: `for h in hosts; ping HOST.mgmt` 100 % success · complete inventory&lt;/text>
&lt;rect x="40" y="116" width="740" height="68" class="b f1"/>&lt;text x="60" y="134" class="tiny">F1 · BARE METAL&lt;/text>
&lt;text x="60" y="152" class="sm">PXE/cloud-init · LTS OS · kernel ≥6.6 · containerd · NVIDIA drivers · chrony · LVM&lt;/text>
&lt;text x="60" y="168" class="note">Gate: `ansible all -m ping` 100 % · `nvidia-smi` smoke OK · idempotent reboot&lt;/text>
&lt;rect x="40" y="192" width="740" height="68" class="b f2"/>&lt;text x="60" y="210" class="tiny">F2 · KUBERNETES CLUSTER&lt;/text>
&lt;text x="60" y="228" class="sm">RKE2 HA · Cilium (kube-proxy replacement + BGP) · Rook-Ceph (RBD + CephFS + RGW)&lt;/text>
&lt;text x="60" y="244" class="note">Gate: RWO/RWX PVCs OK · RGW bucket OK · node drain with no downtime&lt;/text>
&lt;rect x="40" y="268" width="740" height="68" class="b f3"/>&lt;text x="60" y="286" class="tiny">F3 · GITOPS + INFRA OBSERVABILITY&lt;/text>
&lt;text x="60" y="304" class="sm">Forgejo · Flux · VictoriaMetrics + Grafana + Loki · Alertmanager + Keep · backups&lt;/text>
&lt;text x="60" y="320" class="note">Gate: repo change → cluster in &amp;lt;5min · drift reverted · backup restore OK&lt;/text>
&lt;rect x="40" y="344" width="740" height="68" class="b f4"/>&lt;text x="60" y="362" class="tiny">F4 · IDENTITY + POLICIES&lt;/text>
&lt;text x="60" y="380" class="sm">Defguard OIDC+MFA+WG · cert-manager · SOPS+ESO · Kyverno enforce · NP default deny · Tetragon&lt;/text>
&lt;text x="60" y="396" class="note">Gate: shared admin kubeconfig no longer works · policies in enforce · complete audit log&lt;/text>
&lt;rect x="40" y="420" width="740" height="68" class="b f5"/>&lt;text x="60" y="438" class="tiny">F5 · GPU PLATFORM + LLM-AWARE OBSERVABILITY&lt;/text>
&lt;text x="60" y="456" class="sm">NVIDIA GPU Operator · DCGM · MIG manager · KEDA with vLLM metrics · OTel gen_ai.* · OME&lt;/text>
&lt;text x="60" y="472" class="note">Gate: GPU pod scheduled · DCGM green · vLLM smoke with a small model · KEDA scales&lt;/text>
&lt;rect x="40" y="496" width="740" height="38" class="b f6"/>&lt;text x="60" y="514" class="tiny">F6 · LLM STACK LIVE&lt;/text>
&lt;text x="60" y="528" class="sm">7 layers active · first model green · canary OK · runbook signed · first customer with an SLA&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The schedule is not decorative: each row defines what gets built in its phase &lt;strong>and the gate that closes it&lt;/strong>. A phase is not considered finished until its gate is green. A phase with a yellow gate drags every later one; trying to jump to the next with a partially met gate is what produces, several weeks later, the incident that forces you to &amp;ldquo;go back to F4 with production running&amp;rdquo;, the most expensive situation in the whole cost matrix of the &lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">post on the five levels&lt;/a>.&lt;/p>
&lt;h2 id="typical-planning-mistakes">Typical planning mistakes&lt;/h2>
&lt;p>Patterns that delay or sink the greenfield deployment, regardless of the tools chosen:&lt;/p>
&lt;p>&lt;strong>1. Buying the LLM before the cluster.&lt;/strong> Starting the project with &amp;ldquo;which model are we going to serve&amp;rdquo; instead of &amp;ldquo;what platform can sustain any reasonable model&amp;rdquo;. The model is an interchangeable parameter; the platform is not.&lt;/p>
&lt;p>&lt;strong>2. Underestimating F0.&lt;/strong> &amp;ldquo;The network team does that.&amp;rdquo; Yes, but the output of F0 is consumed by every later phase. If the network team delivers late, the whole project is late, and nobody had marked it as the critical path.&lt;/p>
&lt;p>&lt;strong>3. Overlapping F4 with F5 &amp;ldquo;to save time&amp;rdquo;.&lt;/strong> It is the one dependency with no slack. If you try to overlap, F5 ends up operating with policies in permanent &lt;code>audit&lt;/code> (you are not in F4) or without OIDC integrated (operators with a shared kubeconfig touching GPUs). Both antipatterns stay in production.&lt;/p>
&lt;p>&lt;strong>4. Skipping the small model smoke test in F5.&lt;/strong> &amp;ldquo;Let&amp;rsquo;s go straight for the 70B.&amp;rdquo; When something fails (and something will), there will be no baseline to diagnose against.&lt;/p>
&lt;p>&lt;strong>5. Treating F6 as &amp;ldquo;turning on vLLM&amp;rdquo;.&lt;/strong> F6 includes gateway, LLM-aware observability, runbook, SLO, continuity plan. Turning on vLLM takes five minutes; closing F6 is weeks of validation and sign-off.&lt;/p>
&lt;p>&lt;strong>6. Not defining gates in writing.&lt;/strong> If the gates are not written down, they are negotiable after the fact. &amp;ldquo;This already counts as F4&amp;rdquo; is the sentence that precedes the next six months of retrofit.&lt;/p>
&lt;p>&lt;strong>7. Assigning a phase to a single owner.&lt;/strong> Every phase needs at least two people who understand it. Staff turnover on long projects destroys knowledge; written gates + cross review preserve it.&lt;/p>
&lt;p>&lt;strong>8. Forgetting the descent route.&lt;/strong> The post focuses on going up. Day-to-day operation (the descent, in the analogy) is another story that also deserves planning: runbooks, on-call, upgrade capacity, end-of-life plan. Teams that only plan the ascent reach the summit and stay there with no oxygen.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware-4h100-sxm">Applied to typical on-premise hardware: 4×H100 SXM&lt;/h2>
&lt;p>On the generic reference cluster (4×H100 SXM 80 GB, NVLink, 640 GB RAM per GPU node, 3 control plane nodes, 3-5 CPU worker nodes, 2 GPU worker nodes), the &lt;strong>temporal&lt;/strong> split of the work is distributed like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">F0 (hardware)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ 8 physical servers racked + switches + BMC + management IPs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 3 nodes cp-01..03 — control plane (no GPU)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> ├─ 3 nodes worker-cpu-01..03 — CPU plane (Forgejo, Ceph, observability)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> └─ 2 nodes worker-gpu-01..02 — GPU plane (4×H100 SXM each)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F1 (bare metal)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ OS + drivers + containerd on all 8 nodes alike
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> (NVIDIA drivers only on the 2 GPU nodes, `nvidia-smi` smoke)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F2 (k8s cluster)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ RKE2 control plane on cp-01..03 (HA with embedded etcd)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> workers joining: 3 CPU + 2 GPU
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Ceph OSDs on the 3 CPU worker nodes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> default pools: RBD-replicated-3, CephFS-replicated-3, RGW
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F3 (GitOps + obs)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ Forgejo + Flux + VM/Grafana/Loki + Keep on the CPU plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> first `gitops-infra` repo reconciles what came from F2
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F4 (identity)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ Defguard on the CPU plane (StatefulSet with Postgres CNPG)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OIDC in kube-apiserver, Forgejo, Grafana, Alertmanager
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> Kyverno as a Deployment on the control plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F5 (GPU plane)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ NVIDIA GPU Operator targets the GPU workers
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> MIG manager: 1st GPU MIG 7g.80gb (= passthrough), 2nd 2×3g.40gb
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> OTel Collector as a DaemonSet on the GPU plane + CPU plane
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> first vLLM with a 7B FP16 model green
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">F6 (LLM stack)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└─ The 7 layers reconcile via Flux from a second repo `gitops-llm`
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> first public endpoint with Defguard OAuth
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> first production customer enrolled under an SLA
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The physical distribution of the cluster exploits the isolation between planes defined in F0: the control plane never touches a GPU, the CPU plane concentrates relevant state (Forgejo, Ceph, Postgres CNPG, Langfuse, Defguard) and the GPU plane specialises as much as possible. That separation, decided in F0 before the first server is installed, conditions the success of the remaining phases. It is another reminder of why F0 matters more than it looks.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;p>This post walks the &lt;strong>ascent route&lt;/strong> to the summit. Some pieces still deserve an article of their own:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>The safe descent&lt;/strong>: day-to-day operation, runbooks per component, on-call, continuous capacity planning, the cluster upgrade cycle with no downtime.&lt;/li>
&lt;li>&lt;strong>Multi-site (the second summit)&lt;/strong>: how two clusters are federated with Cilium Cluster Mesh and what extra phases that introduces. F3.5 (Cluster Mesh) and F4.5 (cross-site replication) are the missing phases.&lt;/li>
&lt;li>&lt;strong>The brownfield route&lt;/strong>: what changes when there is already a cluster with workloads. The phases stay the same, but the gates are applied retroactively and every step requires migration planning.&lt;/li>
&lt;li>&lt;strong>The real calendar cost&lt;/strong>: typical ranges in weeks for a platform team of 2-3 people, split by phase, with uncertainty bands.&lt;/li>
&lt;li>&lt;strong>The handoff to operations&lt;/strong>: how the platform is handed from the deployment team to the operations team, which documents are signed, what is inherited and what is renegotiated.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps for the inference stack with Flux: operating the assistant as code&lt;/a> — how to turn these phases into a reproducible, reconciled GitOps deployment.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">Anatomy of an on-premise LLM inference stack&lt;/a> — the seven layers built in F6. The components, not the schedule.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/">Five maturity levels of the platform under the LLM&lt;/a> — the levels corresponding to phases F1→F5. The strata, not the sequence.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages&lt;/a> — individual cards for the pieces cited here.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the OTel of F5 with detail on the &lt;code>gen_ai.*&lt;/code> semantic conventions.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — when F6 closes, the immediate operational question is how many replicas fit and how many customers they support; the sizing that ends the conversation with procurement.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a> — the concrete cockpit of DCGM and vLLM that F5 enables: the twelve metrics that decide whether the cluster is operated or operated on by intuition.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes&lt;/a> — KEDA and HPA with custom metrics, the first real deliverable of F6 when traffic starts to move.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> — the mechanics of promoting models with no downtime that F6 must support from day one.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">The LLM inference router&lt;/a> — the last piece F6 closes: catalogue, L7 traffic splitting, cross-cutting policy, failover and prefix-aware routing. Without the router, F6 does not finish even with all seven layers running.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/isolating-ai-agents-workstation-to-cluster/">Isolating AI agents: from workstation to cluster&lt;/a> — the runtime hardening that fits into F4 (identity, policies) and F5 (platform): Kata &lt;code>RuntimeClass&lt;/code> and Tetragon &lt;code>TracingPolicy&lt;/code> for agent workloads; its &lt;a href="https://blog.lo0.es/en/posts/runbook-caging-ai-agent-bubblewrap-tetragon/">runbook&lt;/a> brings the files.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>RKE2 Documentation — &lt;a href="https://docs.rke2.io/">docs.rke2.io&lt;/a>&lt;/li>
&lt;li>Cilium documentation — &lt;a href="https://docs.cilium.io/">docs.cilium.io&lt;/a>&lt;/li>
&lt;li>Rook documentation — &lt;a href="https://rook.io/">rook.io/docs&lt;/a>&lt;/li>
&lt;li>Flux GitOps toolkit — &lt;a href="https://fluxcd.io/">fluxcd.io&lt;/a>&lt;/li>
&lt;li>Forgejo — &lt;a href="https://forgejo.org/">forgejo.org&lt;/a>&lt;/li>
&lt;li>cert-manager — &lt;a href="https://cert-manager.io/">cert-manager.io&lt;/a>&lt;/li>
&lt;li>External Secrets Operator — &lt;a href="https://external-secrets.io/">external-secrets.io&lt;/a>&lt;/li>
&lt;li>Kyverno — &lt;a href="https://kyverno.io/">kyverno.io&lt;/a>&lt;/li>
&lt;li>NVIDIA GPU Operator — &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/">docs.nvidia.com/datacenter/cloud-native/gpu-operator&lt;/a>&lt;/li>
&lt;li>DCGM Exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>KEDA — &lt;a href="https://keda.sh/">keda.sh&lt;/a>&lt;/li>
&lt;li>LeaderWorkerSet API — &lt;a href="https://github.com/kubernetes-sigs/lws">github.com/kubernetes-sigs/lws&lt;/a>&lt;/li>
&lt;li>OpenTelemetry Semantic Conventions for GenAI — &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">opentelemetry.io/docs/specs/semconv/gen-ai&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Five maturity levels of the platform under the LLM: from a Linux server to a cluster ready for vLLM</title><link>https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/</link><pubDate>Sun, 31 May 2026 07:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/five-maturity-levels-platform-under-llm/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The post on &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">the seven layers of the LLM inference stack&lt;/a> took many pieces for granted: an operational Kubernetes cluster, GitOps reconciling, identities resolved, GPUs visible to the scheduler, observability capable of carrying &lt;code>gen_ai.*&lt;/code>. Before vLLM makes any sense, &lt;strong>you have to reach that starting point&lt;/strong>, and you reach it by levels. This post defines &lt;strong>five maturity levels&lt;/strong> of the platform that lives under the LLM, from a bare metal server with Linux installed (level 0) to a cluster ready to run the inference layer (level 4) and the handoff to the previous post (level 5). Each level &lt;strong>unlocks a concrete capability&lt;/strong>, running containers reproducibly, rebuilding the cluster from git, authenticating humans via OIDC, scheduling GPUs with MIG and DCGM metrics, demonstrating compliance with no manual intervention, and each has a &lt;strong>validation test&lt;/strong> that decides whether you are really there or just telling yourself you are. For each level: which OSS pieces cover it in 2026 (Cilium, RKE2, Flux, cert-manager, Defguard, NVIDIA GPU Operator, KEDA, Trivy, Kyverno…), &lt;strong>the deployment order&lt;/strong> within the level, the decisions that are expensive to skip, and the antipatterns that drop you a level when you thought you were higher. The thesis: &lt;strong>moving up a level costs little effort if you do it in time, and a lot of refactoring if you try to skip it&lt;/strong>. LLM inference demands at least level 4; anyone trying to serve LLMs from level 1 or 2 ends up paying in night-time incidents what they saved on the platform.&lt;/p>
&lt;h2 id="you-are-here-the-five-levels-at-a-glance">You are here: the five levels at a glance&lt;/h2>
&lt;p>Before the detail, the ladder. Each rung adds a capability absent from the one below. The level&amp;rsquo;s test is the question whose honest answer says whether you are already there.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="five maturity levels of the platform under the LLM">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.l0{fill:#f6e2e2;stroke:#a33}.l1{fill:#f4e3cf;stroke:#a63}.l2{fill:#eef0d0;stroke:#7a3}.l3{fill:#d8eecf;stroke:#373}.l4{fill:#dfe9f5;stroke:#356}.l5{fill:#ead8f5;stroke:#634}.title{font:600 13px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Five maturity levels (plus the handoff to the LLM stack at level 5)&lt;/text>
&lt;rect x="40" y="38" width="740" height="44" class="b l0"/>&lt;text x="60" y="56" class="tiny">LEVEL 0 · CHAOTIC&lt;/text>&lt;text x="60" y="74" class="sm">Bare metal with Linux · ad-hoc docker / podman · no orchestrator · manual changes over SSH&lt;/text>
&lt;rect x="40" y="90" width="740" height="44" class="b l1"/>&lt;text x="60" y="108" class="tiny">LEVEL 1 · REPEATABLE&lt;/text>&lt;text x="60" y="126" class="sm">k8s cluster installed (RKE2 / kubeadm) · CNI · CSI · kubectl apply / Helm from the terminal · pods running&lt;/text>
&lt;rect x="40" y="142" width="740" height="44" class="b l2"/>&lt;text x="60" y="160" class="tiny">LEVEL 2 · DEFINED&lt;/text>&lt;text x="60" y="178" class="sm">GitOps (Flux) · internal registry · infra observability · backups · the cluster rebuilds from the repo&lt;/text>
&lt;rect x="40" y="194" width="740" height="44" class="b l3"/>&lt;text x="60" y="212" class="tiny">LEVEL 3 · MANAGED&lt;/text>&lt;text x="60" y="230" class="sm">OIDC + RBAC · cert-manager · External Secrets · Kyverno · NetworkPolicy default deny · auditing&lt;/text>
&lt;rect x="40" y="246" width="740" height="44" class="b l4"/>&lt;text x="60" y="264" class="tiny">LEVEL 4 · OPTIMISED FOR GPU&lt;/text>&lt;text x="60" y="282" class="sm">NVIDIA GPU Operator · DCGM Exporter · MIG / time-slicing · KEDA with LLM metrics · OTel ready for gen_ai.*&lt;/text>
&lt;rect x="40" y="298" width="740" height="32" class="b l5"/>&lt;text x="60" y="318" class="sm">&lt;tspan font-weight="700">LEVEL 5 · HANDOFF&lt;/tspan> — the cluster is ready for the LLM stack (the 7 layers) to make sense&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The levels are &lt;strong>not interchangeable&lt;/strong>. A cluster at level 2 cannot run LLMs in production with any guarantee: technically the vLLM pod loads, but at the first night-time incident you discover there is no TLS, no identities, no alerting, no GPU metrics, and no way to know who changed what. Moving up a level after LLMs are already in production costs &lt;strong>orders of magnitude&lt;/strong> more than moving up while the cluster is still empty.&lt;/p>
&lt;h2 id="the-analogy-from-the-street-stall-to-the-starred-restaurant">The analogy: from the street stall to the starred restaurant&lt;/h2>
&lt;p>Picture the scale of a hospitality business. &lt;strong>Level 0&lt;/strong> is the street stall: a griddle, a gas bottle, a cook improvising. It can sell food, it works, but anything that deviates from a normal day (a health inspection, an allergic customer, an order for 200 portions) sinks the business. &lt;strong>Level 1&lt;/strong> is the tapas bar: a properly sized kitchen, a short repeatable menu, several shifts. The cook no longer improvises daily; he works from a written menu, even if the recipes live in the head of the boss. &lt;strong>Level 2&lt;/strong> is the restaurant with a set lunch menu: written procedures, fixed suppliers, stock control, an incident book. If the head cook falls ill, the second can run the service without carnage. &lt;strong>Level 3&lt;/strong> is the restaurant with an à la carte menu and formal service: traceability of every ingredient, allergens on the menu, health certification, contracts with suppliers, mandatory staff training. &lt;strong>Level 4&lt;/strong> is the kitchen specialised in a complex product (sushi, haute cuisine, artisan bakery): specific tools the normal restaurant does not need (wood-fired oven, special knives, proving chamber), calibrated processes, quality metrics. &lt;strong>Level 5&lt;/strong> is the restaurant with a Michelin star: the whole system works, &lt;strong>the dish is the result of the organisation, not of one person&amp;rsquo;s talent&lt;/strong>.&lt;/p>
&lt;p>The analogy holds to the end, including the most interesting detail: &lt;strong>you can operate at any level&lt;/strong>, but the promises you can keep are different. The street stall cannot promise a consistent experience to 80 diners with a booking. The cluster at level 1 cannot promise productive multi-tenant LLM service with an SLA. In both cases the problem is not the &lt;strong>technical capability of the last component&lt;/strong> (the griddle cooks; the pod starts); it is the &lt;strong>organisational capability of the whole system&lt;/strong>.&lt;/p>
&lt;p>Let us go level by level.&lt;/p>
&lt;h2 id="level-0--chaotic-the-server-with-linux-and-nothing-else">Level 0 — Chaotic: the server with Linux and nothing else&lt;/h2>
&lt;p>&lt;strong>The capability it gives.&lt;/strong> Running containers with &lt;code>docker&lt;/code>/&lt;code>podman&lt;/code>, running binaries, connecting the server to the network. The operator can SSH in, do things, and see results.&lt;/p>
&lt;p>&lt;strong>The test for the level.&lt;/strong> &lt;em>&amp;ldquo;If I reinstall the server from scratch, can I leave it identical to how it was in one afternoon, using only saved notes?&amp;rdquo;&lt;/em>. If the answer is no (because the steps are in the head of whoever built it, in &lt;code>.bash_history&lt;/code>, in an out-of-date wiki), you are at level 0.&lt;/p>
&lt;p>&lt;strong>Minimum pieces to settle before moving up to level 1.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Suggested decision in 2026&lt;/th>
&lt;th>Why it matters when moving up&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Linux distribution&lt;/td>
&lt;td>Stable Debian or Ubuntu LTS&lt;/td>
&lt;td>Long, predictable support, recent kernel available&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Kernel&lt;/td>
&lt;td>Recent LTS (≥ 6.6) with BPF and modern schedulers&lt;/td>
&lt;td>Cilium/eBPF and recent NVIDIA drivers demand it&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NVIDIA drivers&lt;/td>
&lt;td>The version that matches the CUDA of the LLM engine you will serve&lt;/td>
&lt;td>A driver/CUDA mismatch blocks vLLM before you start&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Container runtime&lt;/td>
&lt;td>&lt;code>containerd&lt;/code>&lt;/td>
&lt;td>CNCF standard, integrated with RKE2/kubeadm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Root filesystem&lt;/td>
&lt;td>XFS or ext4 + LVM thin pools&lt;/td>
&lt;td>Snapshots, online expansion&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Time synchronisation&lt;/td>
&lt;td>&lt;code>chrony&lt;/code> with your own servers&lt;/td>
&lt;td>TLS, correlated logs and short certificates demand it&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Management network&lt;/td>
&lt;td>Dedicated VLAN, ACLs on the switch&lt;/td>
&lt;td>Isolate the control plane from workload traffic&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Cluster network&lt;/td>
&lt;td>LACP + jumbo frames + BGP (if you are going with Cilium)&lt;/td>
&lt;td>Intra-node NVLink does not rescue the service network&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>BMC / IPMI&lt;/td>
&lt;td>Out-of-band access with TLS and MFA&lt;/td>
&lt;td>Recovery when the operating system does not boot&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Antipatterns that pin you at level 0.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Pet&lt;/strong> servers (with their own names, configured by hand, not replaceable).&lt;/li>
&lt;li>Changes applied with &lt;code>vi&lt;/code> directly over &lt;code>/etc/...&lt;/code> with no commit to a repo.&lt;/li>
&lt;li>Deployment with &lt;code>docker-compose&lt;/code> with no healthchecks and no automatic restart.&lt;/li>
&lt;li>An inventory that lives in an Excel sheet nobody updates.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Deployment order within the level.&lt;/strong> System image from PXE/cloud-init with the initial configuration (LVM, hostname, network, SSH key, chrony) → bastion/jump host bootstrap → inventory in Ansible (or an equivalent declarative tool, even if it is later replaced) → NVIDIA drivers + container runtime → smoke test (a CUDA container passes &lt;code>nvidia-smi&lt;/code>). At this point, the server is ready for Kubernetes to come in.&lt;/p>
&lt;h2 id="level-1--repeatable-an-operational-kubernetes-cluster">Level 1 — Repeatable: an operational Kubernetes cluster&lt;/h2>
&lt;p>&lt;strong>The capability it gives.&lt;/strong> Scheduling containers with a scheduler, network abstraction between pods, persistent volumes, workload lifecycle, manual horizontal scaling.&lt;/p>
&lt;p>&lt;strong>The test for the level.&lt;/strong> &lt;em>&amp;ldquo;Can I lose a node and have the workloads rescheduled without human intervention?&amp;rdquo;&lt;/em>. If yes, you are at level 1. If not, because the pods are pinned to nodes, because there are no replicas, because the PVCs do not reattach, you are still at 0 with Kubernetes on top.&lt;/p>
&lt;p>&lt;strong>Minimum pieces for the level.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Suggested decision in 2026&lt;/th>
&lt;th>Main alternative&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>k8s distribution&lt;/td>
&lt;td>&lt;strong>RKE2&lt;/strong> (CIS-hardened by default, no commercial surcharge)&lt;/td>
&lt;td>k3s for very small edge, plain kubeadm for custom cases&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CNI&lt;/td>
&lt;td>&lt;strong>Cilium&lt;/strong> with kube-proxy replacement, BGP, Gateway API&lt;/td>
&lt;td>Calico (without BGP it does not compete with Cilium in 2026)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>CSI block + filesystem + object&lt;/td>
&lt;td>&lt;strong>Rook-Ceph&lt;/strong> (RBD + CephFS + S3-compatible RGW)&lt;/td>
&lt;td>OpenEBS Mayastor + Garage for small deployments&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ingress&lt;/td>
&lt;td>Cilium Gateway API (better to unify with the CNI)&lt;/td>
&lt;td>NGINX Ingress, Traefik&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Basic certs&lt;/td>
&lt;td>Self-signed bootstrap&lt;/td>
&lt;td>(cert-manager comes in at level 3)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Workload handling&lt;/td>
&lt;td>&lt;code>kubectl apply&lt;/code> + Helm from the terminal&lt;/td>
&lt;td>No GitOps yet&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Container registry&lt;/td>
&lt;td>Any internal registry (or a trusted external one) with TLS&lt;/td>
&lt;td>(a managed internal registry comes in at level 2)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Antipatterns that drop you to level 0.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Services deployed with &lt;code>kubectl apply&lt;/code> from one person&amp;rsquo;s terminal and &lt;strong>with the YAML saved nowhere&lt;/strong>.&lt;/li>
&lt;li>Persistent volumes with no backup policy.&lt;/li>
&lt;li>A &amp;ldquo;single-node cluster&amp;rdquo; as permanent production, an architectural single point of failure.&lt;/li>
&lt;li>A CNI with no NetworkPolicy available or no BGP when the network requires it.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Deployment order within the level.&lt;/strong> RKE2 installed on at least three nodes for an HA control plane → Cilium installed in kube-proxy replacement mode + BGP control plane → Rook-Ceph on at least three nodes covering block (RBD) + filesystem (CephFS) + object (S3-compatible RGW) with 3× replication or Erasure Coding depending on the pool → smoke test (a Deployment with a PVC starts, the pods reschedule when a node is cordoned, the data persists).&lt;/p>
&lt;h2 id="level-2--defined-the-cluster-rebuilds-from-git">Level 2 — Defined: the cluster rebuilds from git&lt;/h2>
&lt;p>&lt;strong>The capability it gives.&lt;/strong> The cluster state lives in a repository. Every change goes through a commit. Anyone can rebuild the cluster (or an equivalent one) from the repo and the backups. Basic observability warns when something breaks.&lt;/p>
&lt;p>&lt;strong>The test for the level.&lt;/strong> &lt;em>&amp;ldquo;If I lose the whole cluster, can I recreate it in X hours from the repo + the backups, with no manual intervention beyond the bootstrap?&amp;rdquo;&lt;/em>. The hours are negotiable; what defines the level is that &lt;strong>the repo + the backups are enough&lt;/strong>, not that the person-who-knows is available.&lt;/p>
&lt;p>&lt;strong>Minimum pieces for the level.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Suggested decision in 2026&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Forge&lt;/td>
&lt;td>&lt;strong>Forgejo&lt;/strong> (or Gitea, GitLab CE)&lt;/td>
&lt;td>Self-hosted OSS, community fork of Gitea, open governance&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GitOps reconciler&lt;/td>
&lt;td>&lt;strong>Flux&lt;/strong>&lt;/td>
&lt;td>CNCF graduated, native multi-tenancy, lightweight&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Image registry&lt;/td>
&lt;td>&lt;strong>Forgejo Container Registry&lt;/strong>&lt;/td>
&lt;td>Next to the code, no extra piece&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Metrics TSDB&lt;/td>
&lt;td>&lt;strong>VictoriaMetrics&lt;/strong> + vmagent&lt;/td>
&lt;td>Higher throughput than plain Prometheus, long retention, PromQL compatible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Visualisation&lt;/td>
&lt;td>&lt;strong>Grafana&lt;/strong>&lt;/td>
&lt;td>The de facto standard&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Logs&lt;/td>
&lt;td>&lt;strong>Loki&lt;/strong> or &lt;strong>Vector&lt;/strong>&lt;/td>
&lt;td>OSS, integrated with Grafana&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Alerting&lt;/td>
&lt;td>&lt;strong>Alertmanager&lt;/strong> + &lt;strong>Keep&lt;/strong> (OSS orchestrator)&lt;/td>
&lt;td>Keep adds multi-channel routing with no lock-in&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DB backups&lt;/td>
&lt;td>&lt;strong>Barman Cloud&lt;/strong> (Postgres)&lt;/td>
&lt;td>The standard for CNPG&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Object / dataset backups&lt;/td>
&lt;td>&lt;strong>Ceph RGW multisite&lt;/strong> + CephFS snapshots&lt;/td>
&lt;td>Cross-pool and cross-site&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Antipatterns that drop you to level 1.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;code>kubectl apply&lt;/code> run in production &lt;strong>outside&lt;/strong> the repo (undetected drift).&lt;/li>
&lt;li>&lt;code>main&lt;/code> branches with write permissions for humans without review.&lt;/li>
&lt;li>A monolithic repo with no tenant/infra/apps separation (cross changes that cannot be audited).&lt;/li>
&lt;li>Metrics kept no longer than 7 days (no SLO observable over a month).&lt;/li>
&lt;li>Alerting that fires for everything (fatigue) or for nothing (silence).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Deployment order within the level.&lt;/strong> Forgejo deployed first (it is a prerequisite for everything else) → Flux installed and pointing at the manifests repo → an initial repository with the Cilium and Rook-Ceph Helm releases reconciled by Flux (replacing the level 1 &lt;code>kubectl apply&lt;/code>) → VictoriaMetrics + Grafana + Loki via Helm/Flux → Postgres backups and Ceph snapshots scheduled → smoke test (tear the cluster down, restore from repo + backup, the services come back).&lt;/p>
&lt;h2 id="level-3--managed-identities-certificates-secrets-and-policies">Level 3 — Managed: identities, certificates, secrets and policies&lt;/h2>
&lt;p>&lt;strong>The capability it gives.&lt;/strong> Every human operating the cluster does so with their own identity (no shared &lt;code>kubeconfig&lt;/code>), with MFA and with limited permissions. Automatic internal TLS. Encrypted versioned secrets. Policies that &lt;strong>reject&lt;/strong> insecure configurations before they reach the cluster. A complete audit of who did what.&lt;/p>
&lt;p>&lt;strong>The test for the level.&lt;/strong> &lt;em>&amp;ldquo;If an attacker gets hold of an administrator&amp;rsquo;s laptop, what can they do in production?&amp;rdquo;&lt;/em>. At level 3 the answer is &lt;em>&amp;ldquo;little&amp;rdquo;&lt;/em>: MFA blocks the second factor, Kyverno policies block destructive changes without approval, the NetworkPolicies prevent lateral movement, the secrets are encrypted with an external KMS, and the audit log remains. At level 2, &lt;em>&amp;ldquo;everything&amp;rdquo;&lt;/em>.&lt;/p>
&lt;p>&lt;strong>Minimum pieces for the level.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Suggested decision in 2026&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>IdP / OIDC&lt;/td>
&lt;td>&lt;strong>Defguard&lt;/strong>&lt;/td>
&lt;td>Spanish OSS, WireGuard + OIDC + 2FA, multi-org&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Federation with the cluster&lt;/td>
&lt;td>OIDC in kube-apiserver, OIDC in Forgejo, OIDC in Grafana&lt;/td>
&lt;td>Consistent SSO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Internal PKI&lt;/td>
&lt;td>&lt;strong>cert-manager&lt;/strong> + Trust Manager&lt;/td>
&lt;td>De facto standard, ACME and internal CA&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>External ACME&lt;/td>
&lt;td>Let&amp;rsquo;s Encrypt for edge certs&lt;/td>
&lt;td>No cost, automated&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Secrets in git&lt;/td>
&lt;td>&lt;strong>SOPS&lt;/strong> + age or an external KMS&lt;/td>
&lt;td>Versionable, encrypted in the repo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Secret syncing&lt;/td>
&lt;td>&lt;strong>External Secrets Operator&lt;/strong>&lt;/td>
&lt;td>Pull from KMS / Vault into the cluster&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Policy as code&lt;/td>
&lt;td>&lt;strong>Kyverno&lt;/strong> (or OPA Gatekeeper)&lt;/td>
&lt;td>Kyverno has a shallower learning curve&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NetworkPolicy&lt;/td>
&lt;td>&lt;strong>Cilium NetworkPolicy&lt;/strong> + L7&lt;/td>
&lt;td>Default deny per namespace&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Runtime security&lt;/td>
&lt;td>&lt;strong>Tetragon&lt;/strong> (Cilium)&lt;/td>
&lt;td>eBPF, complements NetworkPolicy with detection&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Vulnerability scanning&lt;/td>
&lt;td>&lt;strong>Trivy&lt;/strong> in the CI pipeline + admission&lt;/td>
&lt;td>SBOM per image, blocking of critical CVEs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Audit log&lt;/td>
&lt;td>kube-apiserver with &lt;code>--audit-policy-file&lt;/code> sent to Loki&lt;/td>
&lt;td>Regulatory traceability&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Minimum Kyverno policies to keep alive.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Deny &lt;code>:latest&lt;/code> images or images without a sha digest.&lt;/li>
&lt;li>Deny pods without &lt;code>securityContext.runAsNonRoot=true&lt;/code>.&lt;/li>
&lt;li>Deny pods without &lt;code>resources.limits&lt;/code> (CPU + memory).&lt;/li>
&lt;li>Deny Services without an &lt;code>owner=&amp;lt;team&amp;gt;&lt;/code> label.&lt;/li>
&lt;li>Deny changes in critical namespaces (&lt;code>kube-system&lt;/code>, &lt;code>flux-system&lt;/code>) without an approval label.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Antipatterns that drop you to level 2.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>A &lt;code>kubeconfig&lt;/code> shared between administrators.&lt;/li>
&lt;li>Secrets in plain &lt;code>data:&lt;/code> in a manifest committed to the repo.&lt;/li>
&lt;li>NetworkPolicy absent from new namespaces by default (implicit allow-all).&lt;/li>
&lt;li>&lt;code>kubectl edit&lt;/code> or &lt;code>kubectl patch&lt;/code> in production without going through the repo.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Deployment order within the level.&lt;/strong> Defguard deployed and enrolled with WireGuard / OIDC → OIDC integration with kube-apiserver, Forgejo and Grafana → cert-manager installed and issuing internal certificates (your own CA for mTLS, Let&amp;rsquo;s Encrypt for the edge) → SOPS configured and External Secrets Operator installed → migration of secrets from plain to encrypted → Kyverno with initial policies in &lt;em>audit&lt;/em> mode, then &lt;em>enforce&lt;/em> → default-deny NetworkPolicy per namespace → Tetragon enabled → smoke test (try to bypass each policy and check that admission rejects it).&lt;/p>
&lt;h2 id="level-4--optimised-for-gpu-the-cluster-now-knows-what-an-h100-is">Level 4 — Optimised for GPU: the cluster now knows what an H100 is&lt;/h2>
&lt;p>&lt;strong>The capability it gives.&lt;/strong> The Kubernetes scheduler sees the GPUs, distinguishes them, can partition them (MIG) or multiplex them (time-slicing), expose DCGM metrics, autoscale with KEDA using metrics from the LLM workload itself (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>), and carry traces with the GenAI semantic conventions. Everything needed for the LLM inference stack to rest on a platform that understands its nature.&lt;/p>
&lt;p>&lt;strong>The test for the level.&lt;/strong> &lt;em>&amp;ldquo;If I deploy a pod asking for &lt;code>nvidia.com/gpu: 1&lt;/code>, does it get scheduled on the right GPU, with the right slice, with DCGM metrics exposed, with GenAI observability ready to receive spans?&amp;rdquo;&lt;/em>. If yes, you are at level 4. If the answer requires &amp;ldquo;it depends which node and who deploys it&amp;rdquo;, not yet.&lt;/p>
&lt;p>&lt;strong>Minimum pieces for the level.&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Suggested decision in 2026&lt;/th>
&lt;th>Why&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>GPU device plugin&lt;/td>
&lt;td>&lt;strong>NVIDIA GPU Operator&lt;/strong>&lt;/td>
&lt;td>Deploys drivers, container toolkit, DCGM and MIG manager with one operator&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>HW partitioning&lt;/td>
&lt;td>&lt;strong>MIG&lt;/strong> (Multi-Instance GPU) on the H100 where it applies&lt;/td>
&lt;td>Real hardware isolation, not time-slicing&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU metrics&lt;/td>
&lt;td>&lt;strong>DCGM Exporter&lt;/strong>&lt;/td>
&lt;td>SM utilisation, VRAM, temperature, throttling, NVLink bandwidth&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM metrics&lt;/td>
&lt;td>&lt;strong>vLLM Prometheus&lt;/strong> endpoint + scrape&lt;/td>
&lt;td>TTFT, TPOT, KV cache, prefix hit rate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Autoscaling&lt;/td>
&lt;td>&lt;strong>KEDA&lt;/strong> with a Prometheus ScaledObject&lt;/td>
&lt;td>Scales on LLM metrics, not on CPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LLM operators&lt;/td>
&lt;td>&lt;strong>vLLM Production Stack&lt;/strong> / &lt;strong>OME&lt;/strong> (Operator Model Engine)&lt;/td>
&lt;td>Declarative handling of models / adapters&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Traces&lt;/td>
&lt;td>&lt;strong>OpenTelemetry Collector&lt;/strong> with OTLP receivers + processors + exporters&lt;/td>
&lt;td>&lt;code>gen_ai.*&lt;/code> semantic conventions (&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">post&lt;/a>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LeaderWorkerSet&lt;/td>
&lt;td>LeaderWorkerSet API (k8s 1.30+)&lt;/td>
&lt;td>Tensor parallel topology coherent with NVLink&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Topology Manager&lt;/td>
&lt;td>enabled with &lt;code>single-numa-node&lt;/code>&lt;/td>
&lt;td>Pinning GPU pods to the right NUMA node&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>The key decision: MIG, time-slicing or passthrough.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>&lt;strong>MIG&lt;/strong> divides an H100 into 1g.10gb, 2g.20gb, 3g.40gb, 7g.80gb (slices with real HW isolation). Useful for serving several small models or reserving capacity per tenant with a guarantee. Limitation: up to 7 instances per GPU, predefined profiles.&lt;/li>
&lt;li>&lt;strong>Time-slicing&lt;/strong> shares a GPU between several pods with no HW isolation. Useful for dev/test, not for multi-tenant production with an SLA.&lt;/li>
&lt;li>&lt;strong>Passthrough&lt;/strong> assigns the whole GPU to one pod. Useful for tensor parallel across several GPUs on the same node (a large LLM with TP=4).&lt;/li>
&lt;/ul>
&lt;p>For a productive LLM platform, the practical rule: &lt;strong>passthrough for the large models with TP&lt;/strong>, &lt;strong>MIG for embeddings and small models that cohabit&lt;/strong>, &lt;strong>never time-slicing in production&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Antipatterns that drop you to level 3.&lt;/strong>&lt;/p>
&lt;ul>
&lt;li>Installing NVIDIA drivers by hand outside the GPU Operator (silent breakage when Kubernetes is upgraded).&lt;/li>
&lt;li>Serving an LLM with &lt;code>requests.gpu: 1&lt;/code> without having decided on MIG / passthrough (you end up with idle GPUs from fragmentation or pods stepping on each other).&lt;/li>
&lt;li>KEDA autoscaling on CPU (the classic &lt;code>HorizontalPodAutoscaler&lt;/code>) on pods that are almost always at 10 % CPU but 95 % KV cache.&lt;/li>
&lt;li>OpenTelemetry deployed but without the &lt;code>gen_ai.*&lt;/code> semantic conventions (the traces are not LLM-aware).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Deployment order within the level.&lt;/strong> NVIDIA GPU Operator installed via Helm/Flux with the driver version that matches the chosen LLM engine → DCGM Exporter enabled and metrics visible in Grafana (NVIDIA dashboards imported) → MIG manager configured for the nodes where it makes sense (a typical mix on a 4×H100 SXM cluster: two GPUs with full passthrough for the general LLM at TP=4, two GPUs partitioned into 2×3g.40gb each for small LLMs + embeddings) → OpenTelemetry Collector with &lt;code>attributes&lt;/code> processors to enrich spans with your own labels (&lt;code>tenant_id&lt;/code>, &lt;code>priority_tier&lt;/code>) + exporters to Langfuse and to Tempo → KEDA installed with an example ScaledObject pointing at &lt;code>vllm:num_requests_running&lt;/code> → vLLM Production Stack or OME to declare models as a CRD → smoke test (a vLLM Deployment declared via CRD starts, serves a token, exposes metrics, the trace reaches Langfuse, KEDA scales under synthetic load).&lt;/p>
&lt;h2 id="level-5--handoff-the-cluster-is-an-llm-platform-the-seven-layers-go-on-top">Level 5 — Handoff: the cluster is an LLM platform, the seven layers go on top&lt;/h2>
&lt;p>Once at level 4, the cluster meets the contract that the &lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">post on the seven layers&lt;/a> assumed as its starting point. Level 5 adds no infrastructure: it adds the &lt;strong>LLM stack&lt;/strong> proper. For completeness, the seven components of level 5 are:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Gateway&lt;/strong> (Envoy AI Gateway) — it comes first, directing traffic to LLM inference and embeddings.&lt;/li>
&lt;li>&lt;strong>LLM inference&lt;/strong> (vLLM Production Stack or OME with vLLM) — on the GPUs already discovered by the level 4 GPU Operator.&lt;/li>
&lt;li>&lt;strong>Embeddings + reranker&lt;/strong> (Infinity, TEI) — a pod separate from the LLM, already covered in the previous post.&lt;/li>
&lt;li>&lt;strong>Vector store + relational data&lt;/strong> (Qdrant, PostgreSQL CNPG, Ceph RGW for weights and adapters, CephFS for datasets) — most of it already existed at level 2 as data; now it specialises for RAG.&lt;/li>
&lt;li>&lt;strong>LLM-aware observability&lt;/strong> (Langfuse) — it plugs into the level 4 OTel chain.&lt;/li>
&lt;li>&lt;strong>GitOps control plane&lt;/strong> — the one from level 2 remains the only legitimate authority.&lt;/li>
&lt;li>&lt;strong>Dependency tracking&lt;/strong> (Hubble flows + Otterize) — on the Cilium that already existed at level 1.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>The criterion for promoting from level 4 to level 5&lt;/strong> is not technical: it is contractual. The cluster already supports LLMs; the decision is when to open real customer traffic. Promotion demands: a green golden eval of the model, a signed incident runbook, negotiated SLOs, a continuity plan, and mapping to ENS / NIS2 / 42001 where it applies.&lt;/p>
&lt;h2 id="the-maths-that-matter-what-skipping-a-level-costs">The maths that matter: what skipping a level costs&lt;/h2>
&lt;p>To quantify the thesis of the post, an order-of-magnitude estimate of the &lt;strong>cost of moving up each level in time&lt;/strong> versus &lt;strong>moving up after production is running&lt;/strong>. The figures are engineering time with a small platform team (2-3 people), assuming templates and prior experience.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Level&lt;/th>
&lt;th>Time to build on an empty cluster&lt;/th>
&lt;th>Time to retrofit with production running&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>0 → 1&lt;/td>
&lt;td>1-2 weeks&lt;/td>
&lt;td>1-2 weeks (little downstream refactoring)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1 → 2&lt;/td>
&lt;td>2-3 weeks&lt;/td>
&lt;td>4-8 weeks (migrating everything to git)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2 → 3&lt;/td>
&lt;td>2-4 weeks&lt;/td>
&lt;td>8-16 weeks (image rebuilds, secret migration, retroactive RBAC)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3 → 4&lt;/td>
&lt;td>1-2 weeks&lt;/td>
&lt;td>4-8 weeks (reconfiguring GPUs, moving models to MIG, instrumenting &lt;code>gen_ai.*&lt;/code>)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4 → 5&lt;/td>
&lt;td>1-2 weeks&lt;/td>
&lt;td>2-4 weeks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total 0 → 5&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~10-15 weeks&lt;/strong>&lt;/td>
&lt;td>&lt;strong>~20-40 weeks if done in the wrong order&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The typical multiplier observable in practice: &lt;strong>2× to 3×&lt;/strong> the cost if it is done in the wrong order. And that assumes it &lt;strong>gets done at all&lt;/strong>, since many projects never get past level 2 because &amp;ldquo;the identity thing&amp;rdquo; can always wait for another sprint. By the time the incident arrives, it is too late to start.&lt;/p>
&lt;p>Beyond time, the &lt;strong>operational&lt;/strong> cost (night-time incidents, security escapes, invisible debt) grows exponentially with the gap between the actual level and the required level. A cluster at level 2 serving productive LLMs to regulated customers is a time bomb: technically it works, organisationally it does not.&lt;/p>
&lt;h2 id="final-diagram-the-complete-ladder-with-pieces">Final diagram: the complete ladder with pieces&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 520" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="final diagram of the five levels with their OSS pieces and the handoff to the LLM stack">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.bg{fill:#fafafa;stroke:#bbb;rx:8}.l0{fill:#f6e2e2;stroke:#a33}.l1{fill:#f4e3cf;stroke:#a63}.l2{fill:#eef0d0;stroke:#7a3}.l3{fill:#d8eecf;stroke:#373}.l4{fill:#dfe9f5;stroke:#356}.l5{fill:#ead8f5;stroke:#634}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#222}.tiny{font:600 10px sans-serif;fill:#222}.note{font:italic 10px sans-serif;fill:#555}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#a)}&lt;/style>
&lt;defs>&lt;marker id="a" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="410" y="22" text-anchor="middle" class="lbl">Five maturity levels · OSS pieces · handoff to the LLM stack&lt;/text>
&lt;rect x="40" y="38" width="740" height="76" class="b l0"/>&lt;text x="60" y="56" class="tiny">LEVEL 0 · CHAOTIC · a server with Linux&lt;/text>
&lt;text x="60" y="74" class="sm">Debian / Ubuntu LTS · kernel ≥6.6 · containerd · NVIDIA drivers · LVM · chrony · BMC TLS+MFA&lt;/text>
&lt;text x="60" y="92" class="sm">Network: management VLAN, LACP, jumbo frames, BGP on the switch&lt;/text>
&lt;text x="60" y="108" class="note">Test: can I rebuild the server from notes?&lt;/text>
&lt;rect x="40" y="122" width="740" height="76" class="b l1"/>&lt;text x="60" y="140" class="tiny">LEVEL 1 · REPEATABLE · operational Kubernetes cluster&lt;/text>
&lt;text x="60" y="158" class="sm">RKE2 (CIS-hardened) · Cilium (kube-proxy replacement + BGP) · Rook-Ceph (RBD + CephFS + RGW)&lt;/text>
&lt;text x="60" y="176" class="sm">Gateway API · kubectl/Helm from the terminal · pods running with HA&lt;/text>
&lt;text x="60" y="192" class="note">Test: does losing a node require no human action?&lt;/text>
&lt;rect x="40" y="206" width="740" height="76" class="b l2"/>&lt;text x="60" y="224" class="tiny">LEVEL 2 · DEFINED · the cluster rebuilds from git&lt;/text>
&lt;text x="60" y="242" class="sm">Forgejo + Flux · Forgejo Container Registry · VictoriaMetrics + Grafana + Loki&lt;/text>
&lt;text x="60" y="260" class="sm">Barman Cloud backups + Ceph snapshots/RGW multisite · Alertmanager + Keep&lt;/text>
&lt;text x="60" y="276" class="note">Test: can I recreate the cluster from repo + backups?&lt;/text>
&lt;rect x="40" y="290" width="740" height="76" class="b l3"/>&lt;text x="60" y="308" class="tiny">LEVEL 3 · MANAGED · identity, certs, secrets, policies&lt;/text>
&lt;text x="60" y="326" class="sm">Defguard (OIDC + WireGuard) · cert-manager · SOPS + ESO · Kyverno · Trivy&lt;/text>
&lt;text x="60" y="344" class="sm">NetworkPolicy default deny · Tetragon · audit log&lt;/text>
&lt;text x="60" y="360" class="note">Test: what can an attacker do with an admin laptop?&lt;/text>
&lt;rect x="40" y="374" width="740" height="86" class="b l4"/>&lt;text x="60" y="392" class="tiny">LEVEL 4 · OPTIMISED FOR GPU · the scheduler understands the H100&lt;/text>
&lt;text x="60" y="410" class="sm">NVIDIA GPU Operator · DCGM Exporter · MIG manager · Topology Manager NUMA&lt;/text>
&lt;text x="60" y="428" class="sm">KEDA with vLLM metrics · OTel Collector with gen_ai.* · LeaderWorkerSet · OME&lt;/text>
&lt;text x="60" y="446" class="sm">Decision: passthrough TP=4 for the large LLM, MIG for small LLMs + embeddings&lt;/text>
&lt;text x="60" y="460" class="note" fill="#373">Test: does a pod with nvidia.com/gpu:1 schedule with metrics and traces ready?&lt;/text>
&lt;rect x="40" y="468" width="740" height="44" class="b l5"/>&lt;text x="60" y="486" class="tiny">LEVEL 5 · HANDOFF&lt;/text>&lt;text x="60" y="504" class="sm">The LLM stack (7 layers from the previous post) goes on top · gateway, vLLM, embeddings, Qdrant, Langfuse...&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The ladder is not decorative: each level &lt;strong>enables&lt;/strong> the next. You cannot have LLM-aware observability (level 4) without OTel deployed via Flux (level 2). You cannot have automatic internal TLS (level 3) without a root PKI that lives somewhere (registry and certificates managed from level 2). You cannot have KEDA scaling on vLLM metrics (level 4) without Prometheus / VictoriaMetrics scraping (level 2). The levels are &lt;strong>not a conceptual hierarchy&lt;/strong>: they are a hierarchy of &lt;strong>installation dependencies&lt;/strong>.&lt;/p>
&lt;h2 id="typical-design-decisions-that-break-progress">Typical design decisions that break progress&lt;/h2>
&lt;p>Mistakes seen repeatedly that drag the cluster back a level:&lt;/p>
&lt;p>&lt;strong>1. Jumping from level 1 straight to level 4.&lt;/strong> &amp;ldquo;We are in a hurry to serve the LLM, we will do the identity and GitOps stuff later.&amp;rdquo; Later is always two orders of magnitude more expensive and always arrives after the first incident.&lt;/p>
&lt;p>&lt;strong>2. Confusing Helm with GitOps.&lt;/strong> Having Helm charts is not level 2. It is level 1 with templates. Level 2 demands that a reconciler (Flux/ArgoCD) &lt;strong>applies&lt;/strong> the charts from a repo, &lt;strong>detects drift&lt;/strong> and &lt;strong>warns&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>3. cert-manager with no usage policy.&lt;/strong> Having auto-renewed certificates but using TLS only at the ingress, with no internal mTLS between services, leaves the TLS promise lame and turns level 3 into a cosplay of level 3.&lt;/p>
&lt;p>&lt;strong>4. NVIDIA drivers by hand.&lt;/strong> It works on day one and breaks the day of the first kernel upgrade. The rule: drivers &lt;strong>always via the GPU Operator&lt;/strong>, never operating system packages.&lt;/p>
&lt;p>&lt;strong>5. Prometheus metrics but 7 days of retention.&lt;/strong> Without long retention (≥ 90 days) there is no honest SLO. VictoriaMetrics with a year of retention costs little more than Prometheus with 7 days, and it unlocks compliance and serious postmortems.&lt;/p>
&lt;p>&lt;strong>6. OIDC only for kube-apiserver.&lt;/strong> If Forgejo, Grafana, Defguard and vLLM each have their own auth system, you do not have SSO, you have islands. An honest level 3 demands &lt;strong>federation&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>7. Kyverno in permanent &lt;em>audit&lt;/em> mode.&lt;/strong> Policies that do not reject are not policies, they are alerts. At some point you have to move to &lt;em>enforce&lt;/em>. Until then, you are still at level 2 wearing a level 3 face.&lt;/p>
&lt;p>&lt;strong>8. MIG with no conscious decision on the profile.&lt;/strong> Configuring MIG with the default profile without having measured the size of the models that will cohabit leaves fragmented GPUs with slices nobody uses. The rule: MIG only if you have measured and decided the profiles in advance.&lt;/p>
&lt;p>They all share one root: &lt;strong>declaring the level without passing the level&amp;rsquo;s test&lt;/strong>. Saying &amp;ldquo;we already did GitOps&amp;rdquo; when things are still applied with &lt;code>kubectl edit&lt;/code> in prod. Saying &amp;ldquo;we already did identity&amp;rdquo; when there is a shared admin &lt;code>kubeconfig&lt;/code>. Saying &amp;ldquo;we are ready for LLMs&amp;rdquo; when there is no DCGM Exporter and no Langfuse plugged in.&lt;/p>
&lt;h2 id="applied-to-typical-on-premise-hardware-a-4h100-sxm-cluster">Applied to typical on-premise hardware: a 4×H100 SXM cluster&lt;/h2>
&lt;p>On the generic reference cluster (4×H100 SXM 80 GB, NVLink, 640 GB RAM), a reasonable setup after passing the five levels distributes the components like this:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="cl">control plane (3 nodes without GPU, hostnames cp-01..03)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── kube-apiserver, etcd, controller-manager, scheduler
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Flux, Forgejo, cert-manager, External Secrets, Kyverno
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── Tetragon (DaemonSet here too)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">worker plane (≥ 3 nodes without GPU, hostnames worker-cpu-01..03)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Cilium agent (DaemonSet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Rook-Ceph OSDs + MONs + MDS (CephFS) + RGW (S3)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── VictoriaMetrics + Grafana + Loki
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Defguard (StatefulSet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── Langfuse + OTel Collector
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">GPU worker plane (≥ 2 nodes with 4×H100 SXM, hostnames worker-gpu-01..02)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── NVIDIA GPU Operator (driver + container toolkit)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── DCGM Exporter (DaemonSet)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── MIG manager (configuring the chosen profile)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── vLLM (Deployment) — general LLM TP=4 occupies 4 GPUs (passthrough)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── vLLM (Deployment) — code LLM TP=2 occupies 2 GPUs
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">├── Infinity (embeddings) — 2 replicas cohabit on 2 MIG slices
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">└── KEDA scaler listening to vLLM metrics
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The operational rule: &lt;strong>the control plane and the CPU plane are separated from the GPU plane&lt;/strong>. An incident on the GPU plane must not take the control plane with it (the control plane is what recovers the cluster). And the CPU plane concentrates everything that moves relevant state (Forgejo, Rook-Ceph, Postgres CNPG, Langfuse): it is the heart to protect.&lt;/p>
&lt;p>The GPU hardware specialises as much as possible: GPU pods run &lt;strong>only&lt;/strong> on GPU nodes, and GPU nodes run &lt;strong>nothing&lt;/strong> CPU-bound beyond the operational overhead (Cilium, GPU Operator, DCGM). This is enforced with &lt;code>nodeSelector&lt;/code> + taints/tolerations + a Kyverno policy that rejects pods with no GPU requests being scheduled onto GPU nodes.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;p>This post walks the vertical path upwards. Some horizontal and cross-cutting pieces still deserve an article of their own:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Multi-site active/standby&lt;/strong>: how two clusters are federated with Cilium Cluster Mesh and what changes at each level when there are two sites instead of one.&lt;/li>
&lt;li>&lt;strong>Migration between levels with real traffic&lt;/strong>: how a cluster already in production is retrofitted to the next level with no downtime.&lt;/li>
&lt;li>&lt;strong>Day-to-day operation&lt;/strong>: runbooks per level, which dashboards to look at each morning, which SLOs to define per component.&lt;/li>
&lt;li>&lt;strong>The cost plane&lt;/strong>: what each level costs in hardware, energy, engineering hours, optional OSS licences (commercial support from Rancher, Cilium Enterprise, etc.) and when each expense is justified.&lt;/li>
&lt;li>&lt;strong>Operationalised compliance&lt;/strong>: how levels 3 and 4 map to ENS High, NIS2 and ISO/IEC 42001 controls without turning the cluster into a paperwork exercise.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps for the inference stack with Flux: operating the assistant as code&lt;/a> — the maturity level at which the platform is operated as code with Flux.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/anatomy-on-premise-llm-inference-stack-seven-layers/">Anatomy of an on-premise LLM inference stack&lt;/a> — what gets built &lt;strong>on top&lt;/strong> of a level 4 cluster. This post is its architectural prequel.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/oss-catalogue-llmops-six-stages-tool-by-tool/">The OSS catalogue for LLMOps in six stages&lt;/a> — individual cards for many of the pieces cited here.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-tracing-opentelemetry-genai/">LLM tracing with OpenTelemetry GenAI&lt;/a> — the OTel of level 4 with detail on the &lt;code>gen_ai.*&lt;/code> semantic conventions.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the operational framework that lives above a level 5 cluster.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability for LLM inference&lt;/a> — the twelve DCGM and vLLM metrics that turn level 4 into a real cockpit.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes&lt;/a> — KEDA with custom metrics is the piece that activates level 4&amp;rsquo;s &amp;ldquo;OPTIMISED FOR GPU&amp;rdquo; when the first traffic peak arrives.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">Canary, blue-green and shadow for LLM models&lt;/a> — progressive rollout is the first thing level 5 must be able to do without the operator having to watch the screen.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">The LLM inference router&lt;/a> — the piece that appears from level 3 onwards (it needs level 3&amp;rsquo;s OIDC + cert-manager + NetworkPolicy to make sense) and that connects level 4&amp;rsquo;s pools to external clients.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>RKE2 Documentation — &lt;a href="https://docs.rke2.io/">docs.rke2.io&lt;/a>&lt;/li>
&lt;li>Cilium documentation — &lt;a href="https://docs.cilium.io/">docs.cilium.io&lt;/a>&lt;/li>
&lt;li>Rook-Ceph — &lt;a href="https://rook.io/">rook.io&lt;/a>&lt;/li>
&lt;li>Flux GitOps toolkit — &lt;a href="https://fluxcd.io/">fluxcd.io&lt;/a>&lt;/li>
&lt;li>Forgejo — &lt;a href="https://forgejo.org/">forgejo.org&lt;/a>&lt;/li>
&lt;li>cert-manager — &lt;a href="https://cert-manager.io/">cert-manager.io&lt;/a>&lt;/li>
&lt;li>External Secrets Operator — &lt;a href="https://external-secrets.io/">external-secrets.io&lt;/a>&lt;/li>
&lt;li>Kyverno — &lt;a href="https://kyverno.io/">kyverno.io&lt;/a>&lt;/li>
&lt;li>NVIDIA GPU Operator — &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/">docs.nvidia.com/datacenter/cloud-native/gpu-operator&lt;/a>&lt;/li>
&lt;li>DCGM Exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>KEDA — &lt;a href="https://keda.sh/">keda.sh&lt;/a>&lt;/li>
&lt;li>LeaderWorkerSet API — &lt;a href="https://github.com/kubernetes-sigs/lws">github.com/kubernetes-sigs/lws&lt;/a>&lt;/li>
&lt;li>vLLM Production Stack — &lt;a href="https://docs.vllm.ai/">docs.vllm.ai/en/latest/serving/production_stack.html&lt;/a>&lt;/li>
&lt;li>OpenTelemetry Semantic Conventions for GenAI — &lt;a href="https://opentelemetry.io/docs/specs/semconv/gen-ai/">opentelemetry.io/docs/specs/semconv/gen-ai&lt;/a>&lt;/li>
&lt;li>CIS Kubernetes Benchmark&lt;/li>
&lt;li>NIST SP 800-207 — Zero Trust Architecture&lt;/li>
&lt;/ul></description></item><item><title>The GPU cluster as a platform: turning a shared cluster into a multi-tenant service your teams can consume</title><link>https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/</link><pubDate>Thu, 21 May 2026 07:15:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Having an expensive GPU cluster and many different workloads that want to use it is not an infrastructure problem: it is an internal product problem. What separates &amp;ldquo;we have a cluster&amp;rdquo; from &amp;ldquo;we have an inference platform&amp;rdquo; are four layers the market consolidated in 2026: a gateway layer that centralises authentication, routing and policies (LiteLLM, Portkey, Kong AI Gateway); a GPU isolation model appropriate to the tenants&amp;rsquo; profile (MIG hardware isolation for untrusted multi-tenant, MPS for processes from the same team, time-slicing only for dev); a quota and rate limiting system with budgets per tenant/team/project (LiteLLM does this in its core at team/user/api-key level with descriptive 429s); and a multi-tenant observability plane that enables real cost attribution (showback as an intermediate step, chargeback as the destination), per-tenant tracing and differentiated dashboards. Applied to a typical mid-scale GPU cluster (one node with 4-8 H100 SXM and NVLink, a common place to start in production), this translates into concrete decisions: with ~640 GB of aggregate VRAM across 8 GPUs and two typical production models (a large 70B+ model with tensor parallel and a replicated mid-sized model), the cluster serves between &lt;strong>tens and low hundreds of simultaneous sessions&lt;/strong> depending on the mix; GPU isolation is usually resolved with MIG on smaller workloads and per-model dedication on large ones; and the platform&amp;rsquo;s success metric is effective utilisation, which in typical production sits at 30-40% and where the reasonable optimisation target is to push it to 60-70% without degrading SLAs.&lt;/p>
&lt;blockquote>
&lt;p>This is the fifth post in the MLOps for LLMs series. It is the most operationally oriented one and cuts across several pipeline stages (Deploy + Observe plus cross-cutting concerns). The &amp;ldquo;you are here&amp;rdquo; marker flags the two active stages because the notion of a multi-tenant platform does not live in just one.&lt;/p>
&lt;/blockquote>
&lt;h2 id="you-are-here-deploy--observe-the-cluster-as-a-product">You are here: Deploy + Observe (the cluster as a product)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy + Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7adb7a;stroke-width:3}.active2{fill:#c47aff;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mt1)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mt1)}&lt;/style>
&lt;defs>&lt;marker id="mt1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY + OBSERVE · the cluster as a platform with tenants&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box active2"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-question-that-changes-the-framing">The question that changes the framing&lt;/h2>
&lt;p>When a platform team buys expensive GPU hardware and starts building inference, the first version is almost always single-purpose: one model, one client, one target latency. It works. When the second team turns up asking for the same resource, that single purpose becomes internal politics: how many replicas do we give them? What do we do if the SLAs clash? Who pays for the tokens of team B&amp;rsquo;s experiment? And when the third one arrives, what was an SRE project becomes an internal product project.&lt;/p>
&lt;p>The distinction is not technical, it is one of framing. A cluster is infrastructure. A platform is a service with customers, contracts and success metrics. The change of framing implies:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Identifiable customers&lt;/strong> (tenants), not anonymous users.&lt;/li>
&lt;li>&lt;strong>Contracts&lt;/strong> (latency SLA, guaranteed throughput, available models), not &amp;ldquo;whatever we manage&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Success metrics&lt;/strong> that are not technical but product ones: adoption, satisfaction, cost per query per tenant, time to the first &amp;ldquo;hello world&amp;rdquo;.&lt;/li>
&lt;/ul>
&lt;p>This post walks through how that change of framing is carried out. It lands it on a mid-scale cluster (4-8 H100 SXM with NVLink in a single node), the usual configuration when starting with serious LLM inference; but the principles generalise to any topology, from a single node with two GPUs to multi-node clusters with InfiniBand.&lt;/p>
&lt;h2 id="the-four-layers-of-a-multi-tenant-inference-platform">The four layers of a multi-tenant inference platform&lt;/h2>
&lt;p>The canonical architecture that settled in 2026 has four layers that any serious multi-tenant platform implements, from the outside in:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 410" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Four layers of a multi-tenant platform">
&lt;style>.title{font:700 13px sans-serif;fill:#222}.lbl{font:600 12px sans-serif;fill:#222}.sm{font:11px sans-serif;fill:#555}.tiny{font:10px sans-serif;fill:#666}.layer{stroke:#444;stroke-width:1.5;rx:6}.gw{fill:#ffe9d6}.pol{fill:#d6eaff}.iso{fill:#d9f5d6}.obs{fill:#e9d6f5}.cluster{stroke:#666;stroke-dasharray:4 2;fill:none}.tenant{stroke:#888;stroke-width:1.4;fill:#fffce6;rx:4}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#pm1)}&lt;/style>
&lt;defs>&lt;marker id="pm1" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="22" text-anchor="middle" class="title">The four layers of the multi-tenant platform&lt;/text>
&lt;rect x="40" y="50" width="100" height="40" class="tenant"/>&lt;text x="90" y="68" text-anchor="middle" class="sm">Tenant A&lt;/text>&lt;text x="90" y="82" text-anchor="middle" class="tiny">support chat&lt;/text>
&lt;rect x="160" y="50" width="100" height="40" class="tenant"/>&lt;text x="210" y="68" text-anchor="middle" class="sm">Tenant B&lt;/text>&lt;text x="210" y="82" text-anchor="middle" class="tiny">legal RAG&lt;/text>
&lt;rect x="280" y="50" width="100" height="40" class="tenant"/>&lt;text x="330" y="68" text-anchor="middle" class="sm">Tenant C&lt;/text>&lt;text x="330" y="82" text-anchor="middle" class="tiny">code agent&lt;/text>
&lt;rect x="400" y="50" width="100" height="40" class="tenant"/>&lt;text x="450" y="68" text-anchor="middle" class="sm">Tenant D&lt;/text>&lt;text x="450" y="82" text-anchor="middle" class="tiny">data extr.&lt;/text>
&lt;rect x="520" y="50" width="100" height="40" class="tenant"/>&lt;text x="570" y="68" text-anchor="middle" class="sm">Tenant E&lt;/text>&lt;text x="570" y="82" text-anchor="middle" class="tiny">batch ETL&lt;/text>
&lt;rect x="640" y="50" width="100" height="40" class="tenant"/>&lt;text x="690" y="68" text-anchor="middle" class="sm">notebooks&lt;/text>&lt;text x="690" y="82" text-anchor="middle" class="tiny">research&lt;/text>
&lt;rect x="40" y="120" width="700" height="60" class="layer gw"/>
&lt;text x="390" y="144" text-anchor="middle" class="lbl">Layer 1 · AI Gateway&lt;/text>
&lt;text x="55" y="166" class="sm">Auth (OIDC/API keys) · Routing by model · Failover · Caching · Logging · OTel emission · Rate limiting&lt;/text>
&lt;rect x="40" y="195" width="700" height="60" class="layer pol"/>
&lt;text x="390" y="219" text-anchor="middle" class="lbl">Layer 2 · Policy &amp;amp; Quota Plane&lt;/text>
&lt;text x="55" y="241" class="sm">RPS/TPM quotas per tenant · Monthly budgets · Model whitelist · Priority classes · Admission control&lt;/text>
&lt;rect x="40" y="270" width="700" height="60" class="layer iso"/>
&lt;text x="390" y="294" text-anchor="middle" class="lbl">Layer 3 · Isolation Plane&lt;/text>
&lt;text x="55" y="316" class="sm">MIG / MPS / time-slicing · K8s namespaces · NetworkPolicies · ResourceQuotas · Priority + preemption&lt;/text>
&lt;rect x="40" y="345" width="700" height="55" class="layer obs"/>
&lt;text x="390" y="369" text-anchor="middle" class="lbl">Layer 4 · Observability Plane (multi-tenant)&lt;/text>
&lt;text x="55" y="391" class="sm">Traces with tenant_id · Labelled metrics · Cost attribution · Per-tenant dashboards · Audit logs&lt;/text>
&lt;path class="arr" d="M90,90 L390,120"/>
&lt;path class="arr" d="M450,90 L390,120"/>
&lt;path class="arr" d="M690,90 L390,120"/>
&lt;/svg>
&lt;/div>
&lt;p>Each layer solves a concrete problem. Let us take them one by one.&lt;/p>
&lt;h2 id="layer-1--ai-gateway-the-single-front-door">Layer 1 — AI Gateway: the single front door&lt;/h2>
&lt;p>The AI Gateway is the component your tenants see. It is an HTTP/gRPC API compatible with OpenAI (typically &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/embeddings&lt;/code>, &lt;code>/v1/models&lt;/code>) that centralises everything that happens before touching the inference backends.&lt;/p>
&lt;h3 id="why-centralise">Why centralise&lt;/h3>
&lt;p>Without a gateway, tenants connect directly to vLLM or whatever the model is. Every change (rotating an endpoint, adding a model, changing credentials, applying a policy) means notifying every tenant. Every tenant has its own retry logic, its own logging, its own auth model. It is unworkable from the third customer onwards.&lt;/p>
&lt;p>With a gateway, the change is made in one place. Tenants have a stable URL and a set of credentials; the rest is the gateway&amp;rsquo;s problem.&lt;/p>
&lt;h3 id="the-three-dominant-options-in-2026">The three dominant options in 2026&lt;/h3>
&lt;p>&lt;strong>&lt;a href="https://docs.litellm.ai/">LiteLLM&lt;/a>&lt;/strong> is the most popular OSS option, Python-first, deployed as a proxy. It supports 100+ providers (OpenAI, Anthropic, Bedrock, self-hosted vLLM, Ollama, and so on) behind a unified OpenAI-compatible API. It has a native multi-tenant hierarchy with Organizations → Teams → Users → API Keys, each level with its own budget. The Apache 2.0 version covers the basics; RBAC, SSO, audit logs and team-level enforcement require the paid Enterprise version. Deployed on K8s with the official Helm chart.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://portkey.ai/">Portkey&lt;/a>&lt;/strong> is the most mature commercial / SaaS option. A single control plane that enforces budgets, quotas, permissions and compliance. Real-time spending tracking with alerting. RBAC, audit, workspaces and SSO included. The trade-off: dependency on an external service and a per-request pricing model.&lt;/p>
&lt;p>&lt;strong>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">Kong AI Gateway&lt;/a>&lt;/strong> is the option for organisations that already have Kong as their API gateway. An AI plug-in on top of the existing Kong gateway, integrated with its plugin, consumer and rate-limit model. If your platform team already runs Kong, it is the lowest friction.&lt;/p>
&lt;h3 id="when-to-choose-each-one">When to choose each one&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Situation&lt;/th>
&lt;th>Gateway&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pure OSS, self-hosted, Python-first team&lt;/td>
&lt;td>&lt;strong>LiteLLM&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>You need RBAC, SSO, audit log out of the box, and have budget&lt;/td>
&lt;td>&lt;strong>Portkey&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>You already run Kong as a corporate API gateway&lt;/td>
&lt;td>&lt;strong>Kong AI Gateway&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Greenfield enterprise with strict compliance&lt;/td>
&lt;td>Portkey (probably)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mid-sized OSS-first company without regulated compliance&lt;/td>
&lt;td>LiteLLM (typically)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="the-minimum-the-gateway-has-to-do">The minimum the gateway has to do&lt;/h3>
&lt;p>Whichever option you pick, this is what any serious deployment must enforce:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Auth and identity&lt;/strong>: every request carries an API key resolvable to a tenant + user + team.&lt;/li>
&lt;li>&lt;strong>Routing by model&lt;/strong>: the tenant asks for &lt;code>model: &amp;quot;gpt-4o&amp;quot;&lt;/code>; the gateway decides whether it goes to OpenAI, to Azure OpenAI, or to your vLLM with Qwen3 32B (a cheaper fallback), according to policy.&lt;/li>
&lt;li>&lt;strong>Rate limiting&lt;/strong>: RPS per tenant, TPM (tokens per minute), concurrency limits.&lt;/li>
&lt;li>&lt;strong>Caching of identical responses&lt;/strong>: 5-30% of RAG queries are repeats; caching saves latency and cost.&lt;/li>
&lt;li>&lt;strong>OTel emission&lt;/strong>: every call produces a span with &lt;code>gen_ai.*&lt;/code> semantic conventions and &lt;code>tenant_id&lt;/code> as an attribute. Covered in the &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals post&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Failover&lt;/strong>: if vLLM goes down, the gateway redirects to the OpenAI API. If OpenAI rate-limits, the gateway falls back to Anthropic. Configurable policy.&lt;/li>
&lt;/ul>
&lt;h3 id="example-of-a-multi-tenant-litellm-configuration">Example of a multi-tenant LiteLLM configuration&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># litellm-config.yaml — simplified example&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">model_list&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-llama3-70b.inference/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/VLLM_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">qwen3-32b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/qwen3-32b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-qwen3-32b.inference/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/VLLM_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">litellm_params&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/gpt-4o&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/OPENAI_API_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">router_settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">routing_strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">usage-based-routing-v2&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">fallbacks&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">llama-3-70b&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="l">qwen3-32b, gpt-4o] &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># if vLLM goes down, fall back to the external one&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">general_settings&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">master_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/LITELLM_MASTER_KEY&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">database_url&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">os.environ/DATABASE_URL &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># Postgres for budgets/keys&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Hierarchy: Organizations → Teams → Users → API Keys&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c"># Created via the API, not in static YAML&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Creating a team with a budget:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://litellm/team/new &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LITELLM_MASTER_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_alias&amp;#34;: &amp;#34;soporte-chat&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_budget&amp;#34;: 500, # 500 USD/month
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;budget_duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;tpm_limit&amp;#34;: 100000, # 100K tokens/min
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;rpm_limit&amp;#34;: 1000, # 1000 requests/min
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;models&amp;#34;: [&amp;#34;llama-3-70b&amp;#34;, &amp;#34;qwen3-32b&amp;#34;] # access to these
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>And the team&amp;rsquo;s API key:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">curl -X POST http://litellm/key/generate &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -H &lt;span class="s2">&amp;#34;Authorization: Bearer &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">LITELLM_MASTER_KEY&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -d &lt;span class="s1">&amp;#39;{
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;team_id&amp;#34;: &amp;#34;&amp;lt;team-id&amp;gt;&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;duration&amp;#34;: &amp;#34;30d&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;metadata&amp;#34;: {&amp;#34;environment&amp;#34;: &amp;#34;production&amp;#34;, &amp;#34;app&amp;#34;: &amp;#34;support-bot&amp;#34;}
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>That API key is what the tenant uses. Every request made with it consumes from the team&amp;rsquo;s budget. When it runs out, LiteLLM returns HTTP 429 with a description.&lt;/p>
&lt;h2 id="layer-2--policy--quota-plane-what-each-tenant-can-do">Layer 2 — Policy &amp;amp; Quota Plane: what each tenant can do&lt;/h2>
&lt;p>The gateway is where enforcement happens. Policy is what gets enforced. Five axes of multi-tenant policy:&lt;/p>
&lt;h3 id="technical-quotas">Technical quotas&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>TPM&lt;/strong> (tokens per minute): the hard consumption limit. For a Llama 3 70B at TP=5, ~3000 sustained output tokens/s = 180K aggregate TPM. If you have 10 tenants, assign 18K each as the ceiling.&lt;/li>
&lt;li>&lt;strong>RPS / RPM&lt;/strong>: load control, not consumption control. A 4K-token session counts as one request; so does a batch of 100 mini-completions. Useful against abuse.&lt;/li>
&lt;li>&lt;strong>Concurrency&lt;/strong>: how many simultaneous active requests per tenant. Important for latency SLAs: 100 RPS with concurrency=50 works out at 2 seconds per request.&lt;/li>
&lt;/ul>
&lt;h3 id="economic-budgets">Economic budgets&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Monthly per tenant&lt;/strong>: a hard cap in USD.&lt;/li>
&lt;li>&lt;strong>Daily and hourly&lt;/strong>: soft caps to avoid a runaway in a single day.&lt;/li>
&lt;li>&lt;strong>Per project / API key&lt;/strong>: fine granularity within a single tenant.&lt;/li>
&lt;/ul>
&lt;p>LiteLLM has a &lt;code>max_budget&lt;/code> field at every level of the hierarchy (organization, team, user, api key). Budgets are inherited and constrained downwards.&lt;/p>
&lt;h3 id="model-whitelist-and-blacklist">Model whitelist and blacklist&lt;/h3>
&lt;p>Tenants with critical workloads → only stable models (&lt;code>llama-3-70b&lt;/code>, &lt;code>gpt-4o&lt;/code>). Research tenants → access to experimental models as well.&lt;/p>
&lt;h3 id="priority-classes">Priority classes&lt;/h3>
&lt;p>Not all requests are equal. Three typical classes:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Guaranteed&lt;/strong>: workloads with an SLA, latency respected even under pressure.&lt;/li>
&lt;li>&lt;strong>Best-effort&lt;/strong>: normal workloads without a strict SLA.&lt;/li>
&lt;li>&lt;strong>Spot&lt;/strong>: batches that can wait, evictable if a guaranteed one arrives.&lt;/li>
&lt;/ul>
&lt;p>The &lt;a href="https://arxiv.org/abs/2603.00356">Token Management in Multi-Tenant AI Inference Platforms&lt;/a> paper (2026) formalises this with a model of token pools per priority class that has started to be adopted in production. It maintains guaranteed P99 latency for guaranteed workloads even under overload, with selective throttling of spot.&lt;/p>
&lt;h3 id="admission-control">Admission control&lt;/h3>
&lt;p>Before accepting a request: is there capacity? If not, return 429 immediately instead of queueing and degrading everyone. It is the most underrated operational discipline. A cluster with admission control done properly has predictable latency; without it, catastrophic degradation when the peak arrives.&lt;/p>
&lt;h3 id="the-typical-pattern-in-2026">The typical pattern in 2026&lt;/h3>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># Conceptual policy for a &amp;#34;soporte-chat&amp;#34; tenant&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">tenant&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">soporte-chat&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">quotas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">tpm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">rpm&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">max_concurrency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">30&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">budget&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">monthly_usd&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">800&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">alert_thresholds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="m">0.5&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.8&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.95&lt;/span>&lt;span class="p">]&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># warn when you get there&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">models_allowed&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">qwen3-32b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">priority&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">guaranteed&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">fallback_on_overload&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">qwen3-32b &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># if guaranteed fills up, fall back&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="l">gpt-4o-mini &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># last resort, external model&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="layer-3--isolation-plane-isolating-workloads-physically">Layer 3 — Isolation Plane: isolating workloads physically&lt;/h2>
&lt;p>This is the technically densest layer. You have a node with several H100 SXM GPUs interconnected by NVLink. How do you partition them between tenants?&lt;/p>
&lt;h3 id="three-nvidia-mechanisms-for-sharing-a-gpu">Three NVIDIA mechanisms for sharing a GPU&lt;/h3>
&lt;p>&lt;strong>MIG (Multi-Instance GPU)&lt;/strong> is the strongest isolation. It partitions the GPU into up to 7 instances with physically separate HBM memory and dedicated compute units (SMs). Tenants in different MIG instances cannot touch each other: one workload does not consume memory another needs, one does not degrade another&amp;rsquo;s throughput. Hardware isolation. Available on A100, H100, B100 and B200.&lt;/p>
&lt;p>&lt;strong>MPS (Multi-Process Service)&lt;/strong> is soft. Several processes share the GPU concurrently and NVIDIA distributes SMs according to usage. Good performance if all the processes are yours and you trust them. Worse for multi-tenant setups between customers who do not know each other, because a noisy process can degrade the others.&lt;/p>
&lt;p>&lt;strong>Time-slicing&lt;/strong> is the simplest: the GPU is assigned alternately, slot by slot, to different processes. Much worse latency (waits between slots); not recommended for production workloads with an SLA.&lt;/p>
&lt;h3 id="the-choice-for-multi-tenant-in-2026">The choice for multi-tenant in 2026&lt;/h3>
&lt;p>According to the enterprise adoption survey: 80% use MIG for untrusted multi-tenant (different customers who do not know each other) and MPS for trusted environments (processes from the same team) where you want to maximise throughput. Time-slicing is only used in dev/staging so that every developer can touch a GPU without the cost of exclusivity.&lt;/p>
&lt;p>An important limitation of MIG: it isolates compute and HBM memory, but the PCIe path remains shared. For PCIe-bound workloads (a lot of host↔device traffic), tenants in different MIG instances can still affect each other. For LLM inference the main path is HBM, so this is rarely a problem. But it is worth knowing.&lt;/p>
&lt;h3 id="mig-partitions-on-the-h100">MIG partitions on the H100&lt;/h3>
&lt;p>An H100 (80GB HBM3) can be partitioned into fixed profiles:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Profile&lt;/th>
&lt;th>SM&lt;/th>
&lt;th>Memory&lt;/th>
&lt;th>Max instances per GPU&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1g.10gb&lt;/td>
&lt;td>14&lt;/td>
&lt;td>10 GB&lt;/td>
&lt;td>7&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1g.20gb&lt;/td>
&lt;td>14&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2g.20gb&lt;/td>
&lt;td>28&lt;/td>
&lt;td>20 GB&lt;/td>
&lt;td>3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3g.40gb&lt;/td>
&lt;td>42&lt;/td>
&lt;td>40 GB&lt;/td>
&lt;td>2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7g.80gb&lt;/td>
&lt;td>98&lt;/td>
&lt;td>80 GB&lt;/td>
&lt;td>1 (the whole GPU)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For a mid-scale cluster with NVLink, MIG has a fundamental problem: when you partition with MIG, NVLink between GPUs is disabled. An H100 in MIG mode does &lt;strong>not&lt;/strong> take part in multi-GPU tensor parallel. If you are going to serve a large model with tensor parallel (Llama 3 70B with TP=4 or TP=8, for instance), those GPUs have to be whole, without MIG.&lt;/p>
&lt;p>That defines the architectural decision. There are two main approaches:&lt;/p>
&lt;h3 id="approach-a--a-shared-large-model-with-quotas-at-the-gateway">Approach A — A shared large model with quotas at the gateway&lt;/h3>
&lt;p>All the node&amp;rsquo;s GPUs serve a single large model with tensor parallel spanning the whole node. All tenants share that instance. Isolation happens at the gateway layer (quotas, rate limiting) and the policy layer (priority classes). The cluster&amp;rsquo;s kernel is one enormous vLLM instance with &lt;code>--max-num-seqs=128&lt;/code> or similar; internally, vLLM shares GPU time between the active requests with continuous batching.&lt;/p>
&lt;p>&lt;strong>Advantages&lt;/strong>: you use all the GPUs to the full, NVLink stays active, better KV cache utilisation.
&lt;strong>Disadvantages&lt;/strong>: soft isolation. A tenant that saturates does not degrade the others directly (vLLM batches), but it does compete for batch slots. You need serious priority classes.&lt;/p>
&lt;h3 id="approach-b--dedicating-gpus-per-model--tenant">Approach B — Dedicating GPUs per model / tenant&lt;/h3>
&lt;p>You split the GPUs into pools dedicated to different models. Examples on an 8-GPU node:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>4 GPUs&lt;/strong>: a large 70B model with TP=4.&lt;/li>
&lt;li>&lt;strong>2 GPUs&lt;/strong>: a mid-sized 32B model replicated (2 independent instances) for tenants with a strict SLA.&lt;/li>
&lt;li>&lt;strong>2 GPUs&lt;/strong>: miscellaneous workloads (smaller models, experimentation).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Advantages&lt;/strong>: physical isolation between critical models / tenants.
&lt;strong>Disadvantages&lt;/strong>: worse aggregate utilisation; some GPUs idle while others saturate.&lt;/p>
&lt;h3 id="approach-c-advanced--mig-on-some-gpus-plus-dedication-for-the-rest">Approach C (advanced) — MIG on some GPUs plus dedication for the rest&lt;/h3>
&lt;p>If you have small workloads (4B, 7B models), you can use MIG on 1-2 GPUs to serve them and dedicate the rest to tensor parallel for the large model. It combines strong isolation for the small workloads with full use of NVLink for the large model.&lt;/p>
&lt;h3 id="the-operational-choice-start-with-a-move-up-to-c-if-you-need-to">The operational choice: start with A, move up to C if you need to&lt;/h3>
&lt;p>In most deployments, Approach A (a shared large model plus quotas) is the right starting point. Utilisation is better, operation is simpler, and the gateway&amp;rsquo;s soft isolation works for reasonable workloads.&lt;/p>
&lt;p>When there is a tenant with a strict SLA that cannot tolerate competing with others, you move to Approach B for that tenant in particular (dedicating GPUs to an instance of the model just for them), keeping the rest of the cluster shared.&lt;/p>
&lt;p>Approach C is for when you have 10+ tenants with very heterogeneous profiles.&lt;/p>
&lt;h3 id="isolation-at-the-kubernetes-level">Isolation at the Kubernetes level&lt;/h3>
&lt;p>Independently of GPU isolation, pod-level isolation is applied in K8s:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Namespaces per tenant&lt;/strong>: &lt;code>tenant-soporte&lt;/code>, &lt;code>tenant-legal&lt;/code>, and so on.&lt;/li>
&lt;li>&lt;strong>ResourceQuotas and LimitRanges&lt;/strong>: CPU/memory limits per namespace.&lt;/li>
&lt;li>&lt;strong>NetworkPolicies&lt;/strong>: tenant A cannot talk to tenant B&amp;rsquo;s namespaces.&lt;/li>
&lt;li>&lt;strong>K8s PriorityClasses&lt;/strong>: classes with a numeric value defining preemption order if a more critical pod arrives.&lt;/li>
&lt;li>&lt;strong>PodDisruptionBudgets&lt;/strong>: how many pods of each deployment can go down simultaneously.&lt;/li>
&lt;/ul>
&lt;h2 id="layer-4--observability-plane-seeing-what-happens-per-tenant">Layer 4 — Observability Plane: seeing what happens per tenant&lt;/h2>
&lt;p>The fourth layer: observability with a tenant dimension. Without it you cannot do cost attribution, you cannot debug incidents for a single tenant, you cannot show dashboards to stakeholders.&lt;/p>
&lt;h3 id="the-four-mandatory-properties">The four mandatory properties&lt;/h3>
&lt;p>&lt;strong>1. tenant_id on every span&lt;/strong>. The AI gateway resolves the API key and attributes a &lt;code>tenant_id&lt;/code>. That ID is propagated via &lt;code>params._meta&lt;/code> or OTel headers to every downstream component (vLLM, retrieval, MCP servers, tools). Any span in any system carries that label. It is what lets you reconstruct tenant-specific traces.&lt;/p>
&lt;p>&lt;strong>2. Metrics labelled per tenant&lt;/strong>. &lt;code>gen_ai.usage.input_tokens{tenant=&amp;quot;soporte-chat&amp;quot;}&lt;/code> or equivalents. Prometheus, Grafana, groupable by tenant.&lt;/p>
&lt;p>&lt;strong>3. Real cost attribution&lt;/strong>. The sum of tokens × cost/token per tenant gives the cost. For self-hosted vLLM, the cost is per GPU hour plus the proportional share of tokens (you can compute an equivalent cost per 1k tokens).&lt;/p>
&lt;p>&lt;strong>4. An immutable audit log&lt;/strong>. Every API key used, every model invoked, every quota change, every budget exceeded. For compliance.&lt;/p>
&lt;h3 id="showback-vs-chargeback">Showback vs chargeback&lt;/h3>
&lt;p>An important FinOps distinction that gained clarity in 2026:&lt;/p>
&lt;p>&lt;strong>Showback&lt;/strong>: visibility without consequence. &amp;ldquo;Support team, you have consumed 623 USD this month on LLMs&amp;rdquo;. Information, not an invoice. It lets you spot abuse without penalising anyone before the team understands what is going on.&lt;/p>
&lt;p>&lt;strong>Chargeback&lt;/strong>: the cost is charged to the team&amp;rsquo;s budget. When it runs out, it runs out. It changes behaviour.&lt;/p>
&lt;p>The practice that works: 6-18 months on showback while tags are calibrated, misattributions are identified and teams are trained. Then chargeback, once the numbers are credible. Launching chargeback on day 1 when the costs are still dirty creates an immediate political fight; launching showback prepares the ground so that chargeback lands in an orderly way.&lt;/p>
&lt;p>&lt;a href="https://spendark.com/blog/kubernetes-cost-allocation/">Only 14% of organisations have chargeback active&lt;/a> according to a recent survey, which indicates this is still mostly showback in real production.&lt;/p>
&lt;h3 id="tools">Tools&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>&lt;a href="https://www.kubecost.com/">Kubecost&lt;/a>&lt;/strong>: cost allocation per namespace, deployment and pod on Kubernetes. For the cost of the shared GPU, allocate proportionally to the tokens consumed per tenant.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://www.finout.io/">Finout&lt;/a>&lt;/strong>: a FinOps platform that combines cloud bills and LLM API costs in a unified view with virtual tagging.&lt;/li>
&lt;li>&lt;strong>&lt;a href="https://langfuse.com/">Langfuse&lt;/a>&lt;/strong>: already covered. Cost tracking per trace, groupable by user or session metadata.&lt;/li>
&lt;li>&lt;strong>LiteLLM native tracking&lt;/strong>: LiteLLM&amp;rsquo;s master DB keeps running spend per team, user and API key, accessible via the API or the UI.&lt;/li>
&lt;/ul>
&lt;h3 id="the-minimum-multi-tenant-dashboard">The minimum multi-tenant dashboard&lt;/h3>
&lt;p>Any platform should have:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Summary per tenant&lt;/strong>: monthly spend, current RPS, TPM consumed, % of budget spent, active sessions.&lt;/li>
&lt;li>&lt;strong>Top users&lt;/strong> within each tenant (for detecting internal abuse).&lt;/li>
&lt;li>&lt;strong>p95 latency per tenant&lt;/strong>: SLA tracking.&lt;/li>
&lt;li>&lt;strong>429 / 503 errors&lt;/strong>: how many requests are being rate-limited or rejected because of overload.&lt;/li>
&lt;li>&lt;strong>Cost trend&lt;/strong>: the monthly trajectory with a projection.&lt;/li>
&lt;li>&lt;strong>Drift per tenant&lt;/strong> (from the post-tracing series): if a tenant starts getting worse results, raise an alert.&lt;/li>
&lt;/ol>
&lt;h2 id="sizing-on-mid-scale-gpu-clusters-concrete-decisions">Sizing on mid-scale GPU clusters: concrete decisions&lt;/h2>
&lt;p>Let us get down to hardware. As a reference we take a node with N H100 SXM (between 4 and 8) with NVLink/NVSwitch, 80 GB HBM3 each. That gives between 320 GB and 640 GB of aggregate VRAM. Inter-GPU connectivity is 900 GB/s (NVLink 4) or 600 GB/s (NVLink 3) depending on generation. HBM bandwidth per GPU is 3.35 TB/s.&lt;/p>
&lt;h3 id="default-decisions">Default decisions&lt;/h3>
&lt;p>Start with Approach A: all the node&amp;rsquo;s GPUs serving a single large 70B model in BF16 with tensor parallel = N. Expected real capacity (computed for a standard 8-GPU HGX node as an example; it scales roughly linearly with N):&lt;/p>
&lt;ul>
&lt;li>Model VRAM (70B BF16): ~140 GB (≈ 17.5 GB/GPU at TP=8).&lt;/li>
&lt;li>vLLM overhead + activations VRAM: ~10 GB/GPU.&lt;/li>
&lt;li>VRAM free for KV cache: ~52 GB/GPU. On an 8-GPU node that is &lt;strong>~416 GB aggregate&lt;/strong>; on a 4-GPU one, ~210 GB.&lt;/li>
&lt;li>With &lt;code>--kv-cache-dtype=fp8&lt;/code> and a 70B GQA model: ~320 KB/token.&lt;/li>
&lt;li>Aggregate cache capacity (8-GPU node): &lt;strong>~1.3M tokens&lt;/strong> to distribute between simultaneous sessions.&lt;/li>
&lt;/ul>
&lt;p>That translates into throughput and concurrency (indicative figures for an 8-GPU node):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Simultaneous sessions&lt;/th>
&lt;th style="text-align:right">Average context per session&lt;/th>
&lt;th style="text-align:right">Aggregate throughput (tokens/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">16K&lt;/td>
&lt;td style="text-align:right">~5000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">8K&lt;/td>
&lt;td style="text-align:right">~8000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">4K&lt;/td>
&lt;td style="text-align:right">~12000&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Typical latencies: TTFT ~150ms at low traffic, TPOT ~15-20 ms/tok. Under high concurrency, TTFT rises to ~500ms if the queue is saturated.&lt;/p>
&lt;h3 id="an-example-tenant-scheme">An example tenant scheme&lt;/h3>
&lt;p>A cluster with 4 tenants and a research pool:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tenant&lt;/th>
&lt;th style="text-align:right">TPM cap&lt;/th>
&lt;th style="text-align:right">RPM cap&lt;/th>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Budget&lt;/th>
&lt;th>Priority&lt;/th>
&lt;th>Models&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Support chat&lt;/td>
&lt;td style="text-align:right">80K&lt;/td>
&lt;td style="text-align:right">800&lt;/td>
&lt;td style="text-align:right">50&lt;/td>
&lt;td style="text-align:right">1500 USD/month&lt;/td>
&lt;td>Guaranteed&lt;/td>
&lt;td>llama-3-70b, qwen3-32b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Legal RAG&lt;/td>
&lt;td style="text-align:right">30K&lt;/td>
&lt;td style="text-align:right">200&lt;/td>
&lt;td style="text-align:right">15&lt;/td>
&lt;td style="text-align:right">600 USD/month&lt;/td>
&lt;td>Guaranteed&lt;/td>
&lt;td>llama-3-70b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Code agent&lt;/td>
&lt;td style="text-align:right">50K&lt;/td>
&lt;td style="text-align:right">300&lt;/td>
&lt;td style="text-align:right">25&lt;/td>
&lt;td style="text-align:right">1200 USD/month&lt;/td>
&lt;td>Best-effort&lt;/td>
&lt;td>llama-3-70b, qwen-coder&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Data extr. batch&lt;/td>
&lt;td style="text-align:right">40K&lt;/td>
&lt;td style="text-align:right">1000&lt;/td>
&lt;td style="text-align:right">40&lt;/td>
&lt;td style="text-align:right">400 USD/month&lt;/td>
&lt;td>Spot&lt;/td>
&lt;td>llama-3-70b, qwen3-32b&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Research / notebooks&lt;/td>
&lt;td style="text-align:right">10K&lt;/td>
&lt;td style="text-align:right">100&lt;/td>
&lt;td style="text-align:right">5&lt;/td>
&lt;td style="text-align:right">200 USD/month&lt;/td>
&lt;td>Spot&lt;/td>
&lt;td>all&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>TPM total: 210K. Aggregate cluster capacity: ~180K sustained TPM. It is overcommitted by about 15%, assuming not every tenant hits the ceiling at the same time. That is normal and desirable; if they all do it at once, the priority classes degrade in an orderly way.&lt;/p>
&lt;h3 id="when-to-add-hardware">When to add hardware&lt;/h3>
&lt;p>Signals that the node has become too small:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Sustained p95 TTFT &amp;gt; 500 ms&lt;/strong> during peak hours → the queue is building up.&lt;/li>
&lt;li>&lt;strong>&lt;code>vllm:num_requests_waiting&lt;/code> constantly &amp;gt; 20&lt;/strong> → admission control starting to reject.&lt;/li>
&lt;li>&lt;strong>Sustained GPU utilisation &amp;gt; 80% at critical hours&lt;/strong> without dropping in quiet hours → there is no headroom.&lt;/li>
&lt;li>&lt;strong>429 rate on guaranteed tenants &amp;gt; 1%&lt;/strong> → the platform is breaking its SLA in production.&lt;/li>
&lt;/ul>
&lt;p>When several of these hold, the natural next step is to add another HGX node with internal NVLink and stand up a second vLLM instance of the same model. The gateway load-balances between the two instances. Aggregate throughput doubles; latency stays the same.&lt;/p>
&lt;h2 id="common-operational-traps">Common operational traps&lt;/h2>
&lt;h3 id="a-gateway-without-auth-a-backdoor-into-the-cluster">A gateway without auth: a backdoor into the cluster&lt;/h3>
&lt;p>Your vLLM sits on a ClusterIP Service and the main app talks to it. Some tenant discovers the endpoint directly and hits it without going through the gateway. Quotas and costs are silently evaded. Use a strict NetworkPolicy: only the gateway can talk to the vLLM Services; the rest of the cluster cannot.&lt;/p>
&lt;h3 id="mig-and-nvlink-are-incompatible">MIG and NVLink are incompatible&lt;/h3>
&lt;p>You enable MIG on a GPU thinking you will get isolation plus multi-GPU; you discover that MIG disables NVLink. Any large model with TP becomes unusable. Decide MIG vs NVLink globally per cluster, not per individual GPU.&lt;/p>
&lt;h3 id="quotas-pinned-to-the-clusters-ceiling">Quotas pinned to the cluster&amp;rsquo;s ceiling&lt;/h3>
&lt;p>You add up the TPM of all the tenants and it comes to exactly the cluster&amp;rsquo;s capacity. When two tenants peak at the same time, both wait or one gets rejected. An overcommit of 10-20% is healthy (it assumes they do not all peak at once); more than that is dangerous.&lt;/p>
&lt;h3 id="no-multi-tenant-observability-from-day-1">No multi-tenant observability from day 1&lt;/h3>
&lt;p>You launch with quotas and isolation but without tenant_id on spans. Three months later, your CFO asks &amp;ldquo;how much does the support agent cost versus the legal one?&amp;rdquo; and you cannot answer. OTel with a mandatory tenant_id from the first version, even if there are no dashboards yet; having the data is worth more than having perfect dashboards with no data.&lt;/p>
&lt;h3 id="showback-that-never-reaches-chargeback">Showback that never reaches chargeback&lt;/h3>
&lt;p>You have been on showback for 18 months, the teams know the numbers, nobody changes behaviour. Without the pressure of real chargeback, the incentive dilutes. Set an explicit calendar for the transition to chargeback, with an owner and a deadline.&lt;/p>
&lt;h3 id="non-whitelisted-models-eating-the-budget">Non-whitelisted models eating the budget&lt;/h3>
&lt;p>A team discovers that LiteLLM has &lt;code>gpt-4o&lt;/code> configured. They use it without permission. The budget burns on an external API when the idea was to use the cheap self-hosted one. Keep an explicit per-team whitelist of accessible models.&lt;/p>
&lt;h3 id="badly-calibrated-priority-classes">Badly calibrated priority classes&lt;/h3>
&lt;p>Everyone declares themselves &amp;ldquo;guaranteed&amp;rdquo;. At the first peak, there is nothing left to degrade and everything suffers. Priority classes only for critical cases, with justification. The majority should be best-effort.&lt;/p>
&lt;h3 id="no-failover-from-the-gateway">No failover from the gateway&lt;/h3>
&lt;p>Your vLLM goes down. The gateway has no fallback configured and returns 503 to every tenant. Configure a fallback to another model, ideally an external one (OpenAI) for guaranteed workloads, even if it costs more per hour. Availability is worth more than cost per hour.&lt;/p>
&lt;h2 id="an-operational-roadmap-for-getting-started">An operational roadmap for getting started&lt;/h2>
&lt;p>If you start from scratch with an empty GPU node, the minimum order is as follows. Each milestone is a day&amp;rsquo;s work with slack, not a tight one:&lt;/p>
&lt;p>&lt;strong>Day 1-2 — Base K8s infrastructure&lt;/strong>. NVIDIA GPU Operator + nvidia-device-plugin + dcgm-exporter + cluster-default NetworkPolicies. Validation: a basic pod with &lt;code>nvidia.com/gpu: 1&lt;/code> gets scheduled.&lt;/p>
&lt;p>&lt;strong>Day 3 — vLLM with a large model and tensor parallel across the whole node&lt;/strong>. The vLLM Production Stack Helm chart (or bare vLLM manifests). Model weights on a shared PVC (CephFS or NFS). Validation: a &lt;code>curl&lt;/code> request against the internal Service responds.&lt;/p>
&lt;p>&lt;strong>Day 4 — AI Gateway: LiteLLM&lt;/strong>. Helm chart, Postgres for budgets, master key, a first model_list pointing at vLLM. Validation: an OpenAI-compatible request through LiteLLM responds with the same content as vLLM directly.&lt;/p>
&lt;p>&lt;strong>Day 5 — Basic multi-tenancy&lt;/strong>. Create teams, API keys, budgets, model whitelists. Test with two teams. Validation: the second team, using the model it does not have whitelisted, gets a 403.&lt;/p>
&lt;p>&lt;strong>Day 6 — Minimum observability&lt;/strong>. Prometheus + Grafana scraping vLLM and LiteLLM. A dashboard with TTFT, TPOT, throughput, num_requests_waiting and budget_consumed_per_team. Validation: visible in Grafana with real data.&lt;/p>
&lt;p>&lt;strong>Day 7-8 — Pilot customer&lt;/strong>. One real tenant (ideally a controlled internal one) starts using it. Measure real latencies, discover the first operational incidents.&lt;/p>
&lt;p>&lt;strong>Day 9-10 — Tuning&lt;/strong>. Adjust &lt;code>--max-num-seqs&lt;/code>, &lt;code>--gpu-memory-utilization&lt;/code>, priority classes and quotas based on what the pilot taught you.&lt;/p>
&lt;p>&lt;strong>Day 11-14 — Onboarding the second tenant plus iteration&lt;/strong>. Repeat. Every new tenant onboarded reveals new cases.&lt;/p>
&lt;p>After two weeks you have an operational platform with two real tenants and data to decide whether it is ready for more. The line of progress from here on is horizontal (more tenants) until you saturate; from then on, vertical (more hardware).&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-posts">What we have not covered (upcoming posts)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Continuous fine-tuning in production&lt;/strong> (post 6, already decided): LoRA/QLoRA/DPO, dataset curation, eval gates, A/B versioning with real traffic between model versions.&lt;/li>
&lt;li>&lt;strong>Constitutional AI and alignment at runtime&lt;/strong>: an option still on the table.&lt;/li>
&lt;li>&lt;strong>Edge LLMs&lt;/strong>: when an H100 cluster is too expensive for a specific workload, distilled models running on NPUs or consumer GPUs.&lt;/li>
&lt;li>&lt;strong>GPU networking deep dive&lt;/strong>: NCCL, InfiniBand, GPUDirect, RDMA. For multi-node clusters with cross-host tensor parallel.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;p>Multi-tenancy and GPU isolation:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.linuxoperatingsystem.net/multitenant-gpu-infrastructure-4-powerful-design-rules/">Multitenant GPU Infrastructure: 4 Powerful Design Rules&lt;/a> — survey of enterprise patterns.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/run-multiple-llms-one-gpu-mig-time-slicing-guide/">Run Multiple LLMs on One GPU: MIG, Time-Slicing, and MPS Guide (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://sagar-parmar.medium.com/a-practical-guide-to-gpu-partitioning-with-mig-on-on-prem-servers-and-kubernetes-797ccea7e1c7">A Practical Guide to GPU Partitioning with MIG (Medium)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.suse.com/c/kubecon-eu-2026-nvidia-mig-suse-virtualization/">GPU Partitioning for AI Workloads: NVIDIA MIG with SUSE Virtualization (KubeCon EU 2026)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2508.20274">Predictable LLM Serving on GPU Clusters (arxiv 2508.20274)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://arxiv.org/abs/2603.00356">Token Management in Multi-Tenant AI Inference Platforms (arxiv 2603.00356)&lt;/a> — paper on priority + admission control.&lt;/li>
&lt;/ul>
&lt;p>AI Gateways:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://docs.litellm.ai/docs/proxy/multi_tenant_architecture">LiteLLM — Multi-Tenant Architecture&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://docs.litellm.ai/docs/proxy/users">LiteLLM — Budgets and Rate Limits&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://portkey.ai/">Portkey AI Gateway&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">Kong AI Gateway — LLM Cost Management&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.spheron.network/blog/ai-gateway-litellm-portkey-kong-gpu-cloud/">AI Gateway Setup 2026: LiteLLM, Portkey, Kong (Spheron)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://techsy.io/en/blog/best-llm-gateway-tools">Stop Juggling LLM APIs: 8 Gateways Ranked 2026 (TECHSY)&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Multi-tenant FinOps:&lt;/p>
&lt;ul>
&lt;li>&lt;a href="https://www.digiusher.com/blog/the-death-of-cost-allocation-why-chargeback-models-are-failing-in-the-kubernetes-and-ai-era/">The Death of Chargeback in the Kubernetes and AI Era (DigiUsher)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://medium.com/@nicholasthoni/how-to-actually-track-kubernetes-costs-in-2026-a-practical-guide-to-showback-chargeback-and-the-6a4c23f9cf51">How to Actually Track Kubernetes Costs in 2026 (Medium)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://konghq.com/blog/enterprise/llm-cost-management-ai-showback-and-chargeback">LLM Cost Management: AI Showback and Chargeback (Kong)&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.kubecost.com/">Kubecost — cost allocation&lt;/a>.&lt;/li>
&lt;li>&lt;a href="https://www.finout.io/">Finout — FinOps + AI costs&lt;/a>.&lt;/li>
&lt;/ul>
&lt;p>Cross-references:&lt;/p>
&lt;ul>
&lt;li>Previous posts in series 4: &lt;a href="https://blog.lo0.es/en/posts/mlops-for-llms-landscape-2026/">MLOps for LLMs overview&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/rag-on-kafka-reference-architecture-streaming-datalakes/">RAG over Kafka&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage pipeline&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/postgresql-qdrant-rag-ingestion-postman-syncing-two-worlds/">PostgreSQL + Qdrant&lt;/a>.&lt;/li>
&lt;li>Relevant posts from the inference series: &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> — the multi-GPU HGX node scenario we develop here. &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM operators on K8s&lt;/a> — the vLLM Production Stack and OME that the gateway can direct.&lt;/li>
&lt;li>Observability: &lt;a href="https://blog.lo0.es/en/posts/evals-llm-layer-after-tracing/">Evals&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/mcp-deep-observability-opentelemetry/">MCP observability&lt;/a>, &lt;a href="https://blog.lo0.es/en/posts/ebpf-local-inference-statistical-drift-detection/">eBPF + drift&lt;/a>.&lt;/li>
&lt;/ul></description></item></channel></rss>