<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Cncf on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/cncf/</link><description>Recent content in Cncf on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Sun, 26 Jul 2026 20:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/cncf/index.xml" rel="self" type="application/rss+xml"/><item><title>Model chain of trust (1/4): KServe and the Open Inference Protocol, the control plane your serving is missing</title><link>https://blog.lo0.es/en/posts/kserve-open-inference-protocol-control-plane/</link><pubDate>Sun, 26 Jul 2026 20:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/kserve-open-inference-protocol-control-plane/</guid><description>&lt;p>This blog has devoted a good part of its 145 articles to what happens &lt;em>inside&lt;/em> the inference process and another good part to what happens around it. What is missing is a thread that cuts across all of that and answers a question that takes thirty seconds to ask in an audit and does not get answered in three weeks in production: when the endpoint returns a token, where did that model come from, who signed it, and who proves that the binary running on the GPU is the one that was approved?&lt;/p>
&lt;p>This series walks that chain backwards from the only visible point, the endpoint: the control plane and the API contract (this article), the registry and the distribution of the bytes (2/4), the signing and provenance that let you trust them (3/4), and the identity and isolation of whoever runs them (4/4). All of it with projects under the governance of CNCF, LF AI &amp;amp; Data or the Linux Foundation: a chain of trust that depends on a proprietary product is no better than a support contract.&lt;/p>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;ul>
&lt;li>A vLLM &lt;code>Deployment&lt;/code> serves tokens perfectly well. What it does not give you is a contract: no model versioning, no rollout by percentage, no scale-to-zero, and no common surface for the thirty non-LLM artifacts of an inference factory.&lt;/li>
&lt;li>&lt;strong>KServe&lt;/strong> provides the vocabulary: &lt;code>InferenceService&lt;/code>, &lt;code>ServingRuntime&lt;/code>/&lt;code>ClusterServingRuntime&lt;/code>, storage initializer and &lt;code>storageUri&lt;/code>. It has been CNCF incubating since 29 September 2025, after seven years as KFServing and a spell in LF AI &amp;amp; Data. The cadence has accelerated: v0.18.0 on 29 April 2026 and v0.19.0 on 14 June 2026.&lt;/li>
&lt;li>The LLM branch is recent and different: the &lt;code>LLMInferenceService&lt;/code> CRD (&lt;code>serving.kserve.io/v1alpha1&lt;/code>) arrived in v0.16 in November 2025, on top of llm-d, with &lt;code>spec.prefill&lt;/code>, &lt;code>spec.worker&lt;/code>, &lt;code>spec.parallelism&lt;/code> and &lt;code>spec.router&lt;/code>.&lt;/li>
&lt;li>The &lt;strong>Open Inference Protocol (V2)&lt;/strong> is the contract: &lt;code>/v2/health/*&lt;/code>, &lt;code>/v2/models/&amp;lt;m&amp;gt;&lt;/code>, &lt;code>/v2/models/&amp;lt;m&amp;gt;/infer&lt;/code>, in REST and gRPC, implemented by Triton, OpenVINO Model Server, Seldon MLServer and AMD Inference Server. It is tensor-centric, and the LLM world standardised de facto on the OpenAI API: KServe serves both, with the OpenAI ones under the &lt;code>openai/&lt;/code> prefix.&lt;/li>
&lt;li>The honest position: the portability of an LLM today comes from the OpenAI API, not from V2. Accepting the dual track is correct; pretending there is a single standard is not.&lt;/li>
&lt;li>KServe is overkill if you serve one model on one node. It starts to pay off from the fifth heterogeneous artifact onwards, and it competes on a different axis from the LLM operators: contract and lifecycle versus engine and topology.&lt;/li>
&lt;/ul>
&lt;h2 id="the-analogy-the-station-master-and-the-track-gauge">The analogy: the station master and the track gauge&lt;/h2>
&lt;p>An inference factory is like a railway station with several lines.&lt;/p>
&lt;p>The &lt;strong>station master&lt;/strong> does not drive any train. His job is the service timetable: which formation leaves from which platform, how many units are coupled each hour, when an engine is pulled out for maintenance and, when new rolling stock arrives, how to divert ten per cent of the passengers to the test train before committing the whole service. That is the control plane. KServe does not generate a single token, but it decides what runs, with what configuration, with how many replicas and with what traffic split. The trains are the engines (vLLM, SGLang, TensorRT-LLM, Triton, TEI), and a competent station master does not marry a manufacturer.&lt;/p>
&lt;p>And then there is the &lt;strong>track gauge&lt;/strong>: as long as everybody builds to the same one, any locomotive rolls on any line and changing supplier is a commercial decision, not a civil engineering project. That gauge is the Open Inference Protocol, the contract that makes a client written against Triton work against OpenVINO Model Server without touching a line.&lt;/p>
&lt;p>The analogy has a twist that needs no explanation in Spain: the agreed gauge is not the gauge the new rolling stock runs on. The conventional network was built to Iberian gauge and the high-speed one to standard gauge, and the practical solution was not to convert the network but to install gauge changers at the boundary points. The same thing has happened in inference: V2 is the agreed gauge and the OpenAI API is the gauge everything generative runs on. What sustains interoperability is the changer, not a single standard. We will come back to it.&lt;/p>
&lt;h2 id="a-deployment-serves-tokens-but-offers-no-contract">A &lt;code>Deployment&lt;/code> serves tokens, but offers no contract&lt;/h2>
&lt;p>In &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> we built a service with &lt;code>Deployment&lt;/code>, &lt;code>Service&lt;/code>, &lt;code>HPA&lt;/code> and an &lt;code>initContainer&lt;/code> that pulls the weights down. It works, and for one model on one node it is the right answer. What it does not give you, and cannot give you without reinventing it every time:&lt;/p>
&lt;p>&lt;strong>A stable API contract.&lt;/strong> The endpoint is &amp;ldquo;whatever the image you put there exposes&amp;rdquo;: moving from vLLM to TensorRT-LLM changes routes, error format and metric names, and the clients find out in production.&lt;/p>
&lt;p>&lt;strong>Versioning with semantics.&lt;/strong> The model is a path inside a volume; knowing which version a pod is serving requires &lt;code>kubectl exec&lt;/code>. There is no declarative field that says &amp;ldquo;this service serves artifact X&amp;rdquo; and can be audited from outside.&lt;/p>
&lt;p>&lt;strong>Progressive model rollout.&lt;/strong> A &lt;code>RollingUpdate&lt;/code> replaces pods; it does not split traffic between two versions by percentage. For that you need an L7 gateway in front, like the one in &lt;a href="https://blog.lo0.es/en/posts/canary-blue-green-shadow-llm-models/">canary, blue-green and shadow&lt;/a>, or a control plane that emits it.&lt;/p>
&lt;p>&lt;strong>Scale-to-zero.&lt;/strong> The &lt;code>HPA&lt;/code> does not go below one replica: for a model queried twelve times a day, that is an 80 GB GPU tied up twenty-four hours.&lt;/p>
&lt;p>&lt;strong>And, above all, a common surface for what is not an LLM.&lt;/strong> A real factory does not serve three LLMs: it serves two or three LLMs, four embedding models, a couple of rerankers, ticket classifiers, a PII detector, an OCR, industrial vision and three scikit-learn artifacts that nobody remembers training. Fifteen or thirty artifacts, each with its own runtime and its own way of saying &amp;ldquo;I am ready&amp;rdquo;. The cost is not in serving the first one: it is in making sure the thirtieth does not cost thirty times what the first one did.&lt;/p>
&lt;h2 id="kserve-from-kfserving-to-a-cncf-incubating-project">KServe: from KFServing to a CNCF incubating project&lt;/h2>
&lt;p>KServe was born in 2019 as &lt;strong>KFServing&lt;/strong>, inside Kubeflow, in a collaboration between Google, IBM, Bloomberg, NVIDIA and Seldon. In February 2022 it was donated to the LF AI &amp;amp; Data Foundation; in September 2022 it split from Kubeflow and was renamed. The relevant jump: on 29 September 2025 the CNCF accepted it directly at incubating level, without going through sandbox, with a public announcement on 11 November. The figures it declared then, more than 300 contributors, 19 maintainers, more than 30 adopting organisations (Bloomberg, Red Hat, Cloudera, Nutanix, SAP, NVIDIA), come from an interested party, but the list is verifiable and the 625 contributing organisations on the CNCF dashboard support the thesis of governance not captured by a vendor.&lt;/p>
&lt;p>On cadence you have to be precise, because the cliché of &amp;ldquo;KServe is slow&amp;rdquo; no longer describes reality:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Version&lt;/th>
&lt;th>Date&lt;/th>
&lt;th>What it brought&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>v0.15.0&lt;/td>
&lt;td>31 March 2025&lt;/td>
&lt;td>&lt;code>LocalModelCache&lt;/code>, multi-node inference, OpenAI-compatible embeddings API, Gateway API in raw mode, KEDA integration&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v0.16.0&lt;/td>
&lt;td>November 2025&lt;/td>
&lt;td>&lt;strong>&lt;code>LLMInferenceService&lt;/code>&lt;/strong>; rename of &lt;code>RawDeployment&lt;/code> to &lt;code>Standard&lt;/code> and of &lt;code>Serverless&lt;/code> to &lt;code>KNative&lt;/code>; progressive rollout in raw mode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v0.18.0&lt;/td>
&lt;td>29 April 2026&lt;/td>
&lt;td>WVA + KEDA/HPA autoscaling for LLMISvc, &lt;code>/v1/responses&lt;/code> route (OpenAI Responses API), &lt;code>restricted&lt;/code> Pod Security Standards by default on LLMISvc, namespace-scoped &lt;code>ModelCache&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>v0.19.0&lt;/td>
&lt;td>14 June 2026&lt;/td>
&lt;td>Routing by model name, static LoRA adapters, balancing across heterogeneous GPUs, dual REST/gRPC routing in Standard mode, automatic migration to &lt;code>InferencePool&lt;/code> v1&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two major releases in under two months, with the bulk of the changelog in the LLM branch. The project is not slow: it has pivoted from classic predictive serving to generative serving. The consequences run in both directions.&lt;/p>
&lt;p>The negative one is called &lt;strong>ModelMesh&lt;/strong>. It was the high-density mode (hundreds of small models sharing processes, with dynamic load and unload) and it was, on paper, the natural answer to the problem of thirty heterogeneous artifacts. Its last release was v0.12.0 on 9 July 2023, and the &lt;code>kserve/modelmesh-serving&lt;/code> repository was archived read-only on 14 April 2026. Red Hat published specific guidance for migrating to Standard mode. If somebody proposes ModelMesh in a design today, it is a sign that the documentation they read is three years old.&lt;/p>
&lt;h2 id="anatomy-of-the-control-plane">Anatomy of the control plane&lt;/h2>
&lt;h3 id="inferenceservice-predictor-transformer-explainer">&lt;code>InferenceService&lt;/code>: predictor, transformer, explainer&lt;/h3>
&lt;p>The core CRD is &lt;code>InferenceService&lt;/code> (&lt;code>serving.kserve.io/v1beta1&lt;/code>), and its contribution, the one that became the common vocabulary of the field, is to break an inference service down into three components each with its own contract:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>predictor&lt;/code>&lt;/strong>: mandatory. It declares &lt;code>modelFormat&lt;/code>, optionally &lt;code>runtime&lt;/code>, the &lt;code>storageUri&lt;/code> the weights come from and the &lt;code>protocolVersion&lt;/code> it exposes.&lt;/li>
&lt;li>&lt;strong>&lt;code>transformer&lt;/code>&lt;/strong>: optional. Pre- and post-processing. That it is a separate component, rather than code inside the server, is what allows it to be scaled separately (it is usually CPU-bound while the predictor is GPU-bound) and versioned separately.&lt;/li>
&lt;li>&lt;strong>&lt;code>explainer&lt;/code>&lt;/strong>: optional, and the one that has aged worst: the V2 protocol does not support the &lt;code>explain&lt;/code> endpoint, which only exists in V1. Anyone who needs explainability with a standard contract has a real gap here.&lt;/li>
&lt;/ul>
&lt;p>All three share &lt;code>ComponentExtensionSpec&lt;/code>: &lt;code>minReplicas&lt;/code>, &lt;code>maxReplicas&lt;/code>, &lt;code>scaleTarget&lt;/code>, &lt;code>scaleMetric&lt;/code>, &lt;code>containerConcurrency&lt;/code>, &lt;code>timeout&lt;/code> and &lt;code>canaryTrafficPercent&lt;/code>.&lt;/p>
&lt;h3 id="servingruntime-and-clusterservingruntime-the-catalogue-of-trains">&lt;code>ServingRuntime&lt;/code> and &lt;code>ClusterServingRuntime&lt;/code>: the catalogue of trains&lt;/h3>
&lt;p>A &lt;code>ServingRuntime&lt;/code> is a pod template that knows how to serve one or several formats. It declares &lt;code>supportedModelFormats&lt;/code> (with &lt;code>name&lt;/code>, &lt;code>version&lt;/code>, &lt;code>autoSelect&lt;/code> and &lt;code>priority&lt;/code>), &lt;code>protocolVersions&lt;/code> (&lt;code>v1&lt;/code>, &lt;code>v2&lt;/code>) and &lt;code>containers&lt;/code>, with &lt;code>args&lt;/code> parameterised by templates in the &lt;code>{{.Name}}&lt;/code> style that are substituted with metadata from the &lt;code>InferenceService&lt;/code>.&lt;/p>
&lt;p>Selection is what makes this scale. If the &lt;code>InferenceService&lt;/code> names a &lt;code>runtime&lt;/code>, the controller looks for it first in the namespace and then at cluster level; if it does not name one, it picks among those with &lt;code>autoSelect: true&lt;/code> for that &lt;code>modelFormat&lt;/code>, breaking ties by &lt;code>priority&lt;/code>. That is what lets a platform publish a curated catalogue (&amp;ldquo;here, &lt;code>huggingface&lt;/code> is served with this vLLM and these flags&amp;rdquo;) without the product teams having to know what &lt;code>--gpu-memory-utilization&lt;/code> is. In multi-tenant setups, the &lt;code>ClusterServingRuntime&lt;/code>s belong to the platform and the &lt;code>InferenceService&lt;/code>s to the teams: the cleanest separation-of-duties line KServe offers.&lt;/p>
&lt;h3 id="the-storage-initializer-and-storageuri">The storage initializer and &lt;code>storageUri&lt;/code>&lt;/h3>
&lt;p>The &lt;code>storageUri&lt;/code> field triggers the &lt;strong>storage initializer&lt;/strong>: an init container that downloads the artifact to &lt;code>/mnt/models&lt;/code> before the server starts. Schemes supported as of July 2026: &lt;code>s3://&lt;/code>, &lt;code>gs://&lt;/code>, &lt;code>https://&lt;/code>, &lt;code>pvc://&lt;/code>, &lt;code>hf://&lt;/code>, &lt;code>oci://&lt;/code> and git repositories; credentials are attached via a &lt;code>ServiceAccount&lt;/code> with annotated secrets. That &lt;code>oci://&lt;/code> connects with the second article of the series: the weights can live in the same registry as the images, with the same access control and the same signing capability.&lt;/p>
&lt;h3 id="the-deployment-modes">The deployment modes&lt;/h3>
&lt;p>This is where nearly everything operational is decided. It is selected with the &lt;code>serving.kserve.io/deploymentMode&lt;/code> annotation.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;/th>
&lt;th>&lt;strong>KNative&lt;/strong> (formerly Serverless)&lt;/th>
&lt;th>&lt;strong>Standard&lt;/strong> (formerly RawDeployment)&lt;/th>
&lt;th>&lt;strong>ModelMesh&lt;/strong>&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Resources emitted&lt;/td>
&lt;td>Knative &lt;code>Service&lt;/code> + &lt;code>Revision&lt;/code>, &lt;code>VirtualService&lt;/code> with Istio&lt;/td>
&lt;td>&lt;code>Deployment&lt;/code>, &lt;code>Service&lt;/code>, &lt;code>HPA&lt;/code>, Gateway API &lt;code>HTTPRoute&lt;/code>&lt;/td>
&lt;td>Its own high-density runtime&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dependencies&lt;/td>
&lt;td>Knative Serving + Istio (or an alternative gateway)&lt;/td>
&lt;td>Kubernetes 1.32+, cert-manager 1.15.0+, Gateway API v1.2.1 and a controller&lt;/td>
&lt;td>Its own components&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Autoscaling&lt;/td>
&lt;td>KPA (concurrency / RPS)&lt;/td>
&lt;td>HPA or &lt;strong>KEDA&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Scale-to-zero&lt;/td>
&lt;td>Yes, with an activator that holds the request&lt;/td>
&lt;td>&lt;strong>No for HTTP requests&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>canaryTrafficPercent&lt;/code>&lt;/td>
&lt;td>Yes, by revision weights in Istio&lt;/td>
&lt;td>&lt;strong>Silently ignored&lt;/strong> (issue #5335, open since 2 April 2026)&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sidecars in the pod&lt;/td>
&lt;td>&lt;code>queue-proxy&lt;/code> (+ &lt;code>istio-proxy&lt;/code>)&lt;/td>
&lt;td>None&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Status as of July 2026&lt;/td>
&lt;td>Current&lt;/td>
&lt;td>Current and &lt;strong>the default mode of the LLM branch&lt;/strong>&lt;/td>
&lt;td>&lt;strong>Archived on 14 April 2026&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That table has an uncomfortable reading: the two most quoted KServe capabilities, scale-to-zero and canary by percentage, live in the mode that drags Knative and Istio along, while the mode the project has pivoted towards is the other one.&lt;/p>
&lt;div class="diagram" style="max-width:860px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 860 386" role="img" aria-label="A KServe InferenceService materialises into different resources depending on the deployment mode: KNative with scale-to-zero and canary, or Standard with Deployment, HPA or KEDA and Gateway API" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.4}.dsh{fill:none;stroke:currentColor;stroke-width:1.4;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.4;marker-end:url(#kv)}&lt;/style>
&lt;defs>&lt;marker id="kv" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;text x="430" y="22" text-anchor="middle" font-size="13" font-weight="700" fill="currentColor">What you declare versus what the controller emits&lt;/text>
&lt;rect class="bx" x="20" y="46" width="190" height="120" rx="6"/>
&lt;text x="115" y="68" text-anchor="middle" class="tl">InferenceService&lt;/text>
&lt;text x="115" y="88" text-anchor="middle" class="ts">predictor + transformer&lt;/text>
&lt;text x="115" y="105" text-anchor="middle" class="ts">storageUri + runtime&lt;/text>
&lt;text x="115" y="122" text-anchor="middle" class="ts">min/maxReplicas&lt;/text>
&lt;text x="115" y="139" text-anchor="middle" class="ts">canaryTrafficPercent&lt;/text>
&lt;text x="115" y="158" text-anchor="middle" class="ts">protocolVersion: v2&lt;/text>
&lt;rect class="dsh" x="20" y="186" width="190" height="72" rx="6"/>
&lt;text x="115" y="208" text-anchor="middle" class="tl">ClusterServingRuntime&lt;/text>
&lt;text x="115" y="228" text-anchor="middle" class="ts">image, args, probes&lt;/text>
&lt;text x="115" y="245" text-anchor="middle" class="ts">supportedModelFormats&lt;/text>
&lt;path class="ar" d="M210,150 L262,150"/>
&lt;path class="ar" d="M210,222 L262,182"/>
&lt;rect class="bx" x="262" y="120" width="150" height="90" rx="6"/>
&lt;text x="337" y="146" text-anchor="middle" class="tl">Controller&lt;/text>
&lt;text x="337" y="166" text-anchor="middle" class="ts">reconciles and decides&lt;/text>
&lt;text x="337" y="186" text-anchor="middle" class="ts">based on deploymentMode&lt;/text>
&lt;path class="ar" d="M412,150 L466,110"/>
&lt;path class="ar" d="M412,178 L466,246"/>
&lt;rect class="bx" x="466" y="52" width="374" height="112" rx="6"/>
&lt;text x="653" y="74" text-anchor="middle" class="tl">KNative mode&lt;/text>
&lt;text x="653" y="94" text-anchor="middle" class="ts">Knative Service + Revision, VirtualService (Istio)&lt;/text>
&lt;text x="653" y="112" text-anchor="middle" class="ts">KPA, activator, real scale-to-zero&lt;/text>
&lt;text x="653" y="130" text-anchor="middle" class="ts">canaryTrafficPercent by revision weights&lt;/text>
&lt;text x="653" y="152" text-anchor="middle" class="ts">cost: queue-proxy and istio-proxy in every pod&lt;/text>
&lt;rect class="bx" x="466" y="194" width="374" height="112" rx="6"/>
&lt;text x="653" y="216" text-anchor="middle" class="tl">Standard mode&lt;/text>
&lt;text x="653" y="236" text-anchor="middle" class="ts">Deployment + Service + HPA or KEDA&lt;/text>
&lt;text x="653" y="254" text-anchor="middle" class="ts">Gateway API v1.2.1 HTTPRoute&lt;/text>
&lt;text x="653" y="272" text-anchor="middle" class="ts">no scale-from-zero over HTTP&lt;/text>
&lt;text x="653" y="294" text-anchor="middle" class="ts">canaryTrafficPercent silently ignored&lt;/text>
&lt;text x="20" y="340" class="ts">The storage initializer downloads the artifact from storageUri to /mnt/models before starting the server.&lt;/text>
&lt;text x="20" y="360" class="ts">One platform ClusterServingRuntime serves N team InferenceServices: that is where the separation of duties lives.&lt;/text>
&lt;text x="20" y="380" class="ts">The API contract clients see does not change between modes: that is exactly what the control plane provides.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-llm-branch-llminferenceservice">The LLM branch: &lt;code>LLMInferenceService&lt;/code>&lt;/h2>
&lt;p>Since v0.16 KServe has a separate CRD for LLMs: &lt;code>LLMInferenceService&lt;/code>, in &lt;code>serving.kserve.io/v1alpha1&lt;/code>. It is not an extension of the classic &lt;code>InferenceService&lt;/code> and does not share its API version, and confusing the two is the first mistake made by anyone arriving from old documentation.&lt;/p>
&lt;p>It is built on &lt;strong>llm-d&lt;/strong>, the project that Red Hat, Google, IBM, CoreWeave and NVIDIA donated to the CNCF. The division of roles the project itself declares is clean: KServe is the control plane (lifecycle, scaling and operational governance) and llm-d contributes distributed scheduling, with GPU utilisation, queue depth, cache residency and SLA. In the analogy: llm-d decides which line each train takes; KServe decides which trains exist and when they are withdrawn.&lt;/p>
&lt;p>The fields that matter: &lt;code>spec.model&lt;/code> (URI, invocation name, scheduling criticality and LoRA adapters, with static adapter reconciliation from v0.19, which connects with &lt;a href="https://blog.lo0.es/en/posts/multi-lora-serving-shared-base-concurrent-adapters-sgmv-kernel/">multi-LoRA serving&lt;/a>); &lt;code>spec.template&lt;/code> (single node, or the decode pool); &lt;code>spec.worker&lt;/code> (multi-node, which triggers LeaderWorkerSet); &lt;code>spec.prefill&lt;/code> (the disaggregation we analysed in &lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">disaggregated serving&lt;/a> turned into a declarative field); &lt;code>spec.parallelism&lt;/code> (tensor, data and expert parallelism); and &lt;code>spec.router&lt;/code> (&lt;code>gateway&lt;/code>, &lt;code>route&lt;/code>, &lt;code>scheduler&lt;/code>), where KServe leans on the &lt;strong>Gateway API Inference Extension&lt;/strong>, v1.5.0 of 19 April 2026, already GA, with automatic migration to &lt;code>InferencePool&lt;/code> v1 from v0.19.&lt;/p>
&lt;p>The endpoint it exposes is OpenAI-compatible: &lt;code>/v1/chat/completions&lt;/code> with streaming, and since v0.18 also &lt;code>/v1/responses&lt;/code>. That detail underpins the next section: the LLM branch of KServe does not speak V2, it speaks OpenAI.&lt;/p>
&lt;p>For multi-node there is the &lt;code>kserve-huggingfaceserver-multinode&lt;/code> runtime, based on vLLM over Ray, with &lt;code>workerSpec.tensorParallelSize&lt;/code> and &lt;code>workerSpec.pipelineParallelSize&lt;/code>. Its restrictions are hard: it requires a ReadWriteMany PVC, it only works in Standard mode, it does not support autoscaling and it requires exactly one head pod. A reference 4×H100 SXM node with NVLink needs none of this, because a 70B fits with tensor parallel 4 on a single node; you need it from there upwards.&lt;/p>
&lt;h2 id="the-open-inference-protocol-the-contract-itself">The Open Inference Protocol: the contract itself&lt;/h2>
&lt;h3 id="the-routes-and-the-payload">The routes and the payload&lt;/h3>
&lt;p>The &lt;strong>Open Inference Protocol&lt;/strong> (historically the &amp;ldquo;V2 Inference Protocol&amp;rdquo;) lives in &lt;code>kserve/open-inference-protocol&lt;/code>, with REST and gRPC specifications and versioning under SemVer 2.0. Its HTTP surface is deliberately small:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Resource&lt;/th>
&lt;th>Verb&lt;/th>
&lt;th>Route&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Server metadata&lt;/td>
&lt;td>GET&lt;/td>
&lt;td>&lt;code>/v2&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Liveness&lt;/td>
&lt;td>GET&lt;/td>
&lt;td>&lt;code>/v2/health/live&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Server readiness&lt;/td>
&lt;td>GET&lt;/td>
&lt;td>&lt;code>/v2/health/ready&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model metadata&lt;/td>
&lt;td>GET&lt;/td>
&lt;td>&lt;code>/v2/models/&amp;lt;model&amp;gt;[/versions/&amp;lt;v&amp;gt;]&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Model readiness&lt;/td>
&lt;td>GET&lt;/td>
&lt;td>&lt;code>/v2/models/&amp;lt;model&amp;gt;[/versions/&amp;lt;v&amp;gt;]/ready&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Inference&lt;/td>
&lt;td>POST&lt;/td>
&lt;td>&lt;code>/v2/models/&amp;lt;model&amp;gt;[/versions/&amp;lt;v&amp;gt;]/infer&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The payload is tensorial: &lt;code>id&lt;/code>, a list of &lt;code>inputs&lt;/code> (each with &lt;code>name&lt;/code>, &lt;code>shape&lt;/code>, &lt;code>datatype&lt;/code> and &lt;code>data&lt;/code>) and optionally the desired &lt;code>outputs&lt;/code>. The types are &lt;code>BOOL&lt;/code>, signed and unsigned integers from 8 to 64 bits, 16-, 32- and 64-bit floating point, and &lt;code>BYTES&lt;/code>. In gRPC the same contract is &lt;code>GRPCInferenceService&lt;/code>, with six RPCs: &lt;code>ServerLive&lt;/code>, &lt;code>ServerReady&lt;/code>, &lt;code>ModelReady&lt;/code>, &lt;code>ServerMetadata&lt;/code>, &lt;code>ModelMetadata&lt;/code> and &lt;code>ModelInfer&lt;/code>. For high-volume embeddings, the serialisation difference is not cosmetic.&lt;/p>
&lt;h3 id="who-actually-implements-it">Who actually implements it&lt;/h3>
&lt;p>The specification repository declares KServe, NVIDIA Triton, Seldon MLServer and Core v2, OpenVINO Model Server, AMD Inference Server and TorchServe as adopters. Not all entries are worth the same as of July 2026:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Triton&lt;/strong> implements it in a documented way (&amp;ldquo;Triton exposes HTTP/REST and gRPC endpoints based on the standard inference protocols proposed by the KServe project&amp;rdquo;), plus its own extensions. It is the de facto reference implementation.&lt;/li>
&lt;li>&lt;strong>OpenVINO Model Server&lt;/strong> exposes the KServe API over gRPC and REST, the TensorFlow Serving one and OpenAI-compatible endpoints: the clearest example of a server that speaks both track gauges.&lt;/li>
&lt;li>&lt;strong>TorchServe&lt;/strong> forces you to be honest. Its official documentation carries the warning: &lt;em>&amp;ldquo;This project is no longer actively maintained. While existing releases remain available, there are no planned updates, bug fixes, new features, or security patches.&amp;rdquo;&lt;/em> An adopter on zero maintenance is not an adopter: it is a row nobody has updated.&lt;/li>
&lt;/ul>
&lt;p>On the health of the standard, the most eloquent data point is not in any press release: the specification repository does not have a single published release, has accumulated something like thirty commits and seventy-five stars, and its governance consists of monthly meetings and a Slack channel. There is also a &amp;ldquo;vendor neutral&amp;rdquo; &lt;em>fork&lt;/em> at &lt;code>open-inference/open-inference-protocol&lt;/code> with more commits than the original and no traction whatsoever. V2 is a stable contract, widely implemented and barely evolving. For a serialisation format that can be a virtue; for a standard aspiring to cover the generative world, it is a death sentence.&lt;/p>
&lt;h3 id="the-generate-extension-and-why-it-did-not-win">The &lt;code>generate&lt;/code> extension and why it did not win&lt;/h3>
&lt;p>The standard did try to cover generation: the specification contains a &lt;code>generate_rest.yaml&lt;/code> with two routes, &lt;code>/v2/models/&amp;lt;m&amp;gt;/versions/&amp;lt;v&amp;gt;/generate&lt;/code> and &lt;code>.../generate_stream&lt;/code>, and a minimalist schema: a request with &lt;code>text_input&lt;/code> and &lt;code>parameters&lt;/code> (&lt;code>temperature&lt;/code>, &lt;code>top_p&lt;/code>, &lt;code>max_tokens&lt;/code> defaulting to 20, &lt;code>stop&lt;/code>, &lt;code>details&lt;/code>); a response with &lt;code>text_output&lt;/code>, &lt;code>model_name&lt;/code>, &lt;code>model_version&lt;/code> and, if requested, &lt;code>finish_reason&lt;/code> and &lt;code>logprobs&lt;/code>; streaming over &lt;em>server-sent events&lt;/em>. It is clean, it is enough for a simple completion and nobody uses it: the KServe generative inference documentation, as of July 2026, lists four endpoints, and none of them is &lt;code>generate&lt;/code>:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Task&lt;/th>
&lt;th>Endpoint in KServe&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Text generation&lt;/td>
&lt;td>&lt;code>openai/v1/completions&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Chat&lt;/td>
&lt;td>&lt;code>openai/v1/chat/completions&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Embeddings&lt;/td>
&lt;td>&lt;code>openai/v1/embeddings&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reranking&lt;/td>
&lt;td>&lt;code>openai/v1/rerank&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>openai/&lt;/code> prefix exists, according to the documentation itself, &amp;ldquo;to avoid confusion with the V1 API protocol&amp;rdquo;, and it is changed with &lt;code>KSERVE_OPENAI_ROUTE_PREFIX&lt;/code>. It is plumbing that says a lot: the in-house standard was relegated to a defensive prefix while the outside one takes all the traffic.&lt;/p>
&lt;h3 id="the-tension-unvarnished">The tension, unvarnished&lt;/h3>
&lt;p>V2 solved the problem of 2021, and it solved it well. A tensorial, typed contract with metadata and health checks, in REST and gRPC, is what you need to serve a classifier, a vision model, a ranker or an embedding, and it is still the right answer for that half of the catalogue: swapping an embedding server from MLServer to OpenVINO Model Server without touching clients is the track gauge doing its job.&lt;/p>
&lt;p>The LLM world standardised somewhere else, not by committee, but because the entire client ecosystem (SDKs, agent frameworks, gateways, evaluation) was written against the OpenAI API, and any engine that wanted to be used had to speak it. What that means for an architecture:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>There is not one contract: there are two, and both are legitimate.&lt;/strong> Designing as if there were one does violence to half the catalogue.&lt;/li>
&lt;li>&lt;strong>LLM portability is superficial.&lt;/strong> It covers chat and completion well; as soon as you use detailed logprobs, guided decoding, priority headers, prefix caching parameters or &lt;em>tool calling&lt;/em> with specific schemas, you leave the common subset and get tied back to the engine. It is the warning from &lt;a href="https://blog.lo0.es/en/posts/structured-output-bitmask-outlines-xgrammar-llguidance/">structured output&lt;/a>: what is portable is the envelope, not always the contents.&lt;/li>
&lt;li>&lt;strong>The gauge changer is the gateway.&lt;/strong> What makes the set switchable is the translation and routing layer of &lt;a href="https://blog.lo0.es/en/posts/choosing-oss-gateway-llm-inference/">choosing an OSS LLM inference gateway&lt;/a> and &lt;a href="https://blog.lo0.es/en/posts/llm-inference-router-l7-switchboard/">the router as an L7 gateway&lt;/a>, not a single standard.&lt;/li>
&lt;li>&lt;strong>Crossing over loses the versioning.&lt;/strong> V2 has &lt;code>/versions/&amp;lt;v&amp;gt;&lt;/code> in the route; the OpenAI API only has the &lt;code>model&lt;/code> field. The version has to be encoded in the name (&lt;code>asistente-70b-2026-07&lt;/code>) or managed in the control plane. That is the difference between being able to audit which version answered and not being able to.&lt;/li>
&lt;/ol>
&lt;h2 id="four-manifests-you-can-actually-apply">Four manifests you can actually apply&lt;/h2>
&lt;p>Platform runtime for the blog&amp;rsquo;s reference node, 4×H100 SXM 80 GB with NVLink, with tensor parallel 4:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">serving.kserve.io/v1alpha1&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">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ClusterServingRuntime&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">metadata&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-h100-tp4&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">spec&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">annotations&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">prometheus.kserve.io/port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8080&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus.kserve.io/path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;/metrics&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">supportedModelFormats&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">huggingface&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">version&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">autoSelect&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&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="m">10&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">protocolVersions&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">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">containers&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">kserve-container&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"># ALWAYS pin by digest in production; the tag is only for readability&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">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:v0.11.0&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">command&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="s2">&amp;#34;python3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-m&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;vllm.entrypoints.openai.api_server&amp;#34;&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">args&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">port=8080&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">model=/mnt/models&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">served-model-name={{.Name}}&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">tensor-parallel-size=4&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">max-model-len=32768&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">gpu-memory-utilization=0.92&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">enable-prefix-caching&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">kv-cache-dtype=fp8&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">resources&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">requests&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16&amp;#34;&lt;/span>&lt;span class="nt">, memory: 128Gi, nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;4&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>}&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">limits&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">cpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;16&amp;#34;&lt;/span>&lt;span class="nt">, memory: 128Gi, nvidia.com/gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;4&amp;#34;&lt;/span>&lt;span class="w"> &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"># the probe that stops the pod dying while the model loads&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">startupProbe&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">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">path: /health, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &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">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">15&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">failureThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">40&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># up to 10 minutes of loading&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">readinessProbe&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">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">path: /health, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &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">periodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&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">livenessProbe&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">httpGet&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">path: /health, port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8080&lt;/span>&lt;span class="w"> &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">periodSeconds&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;/code>&lt;/pre>&lt;/div>&lt;p>The &lt;code>InferenceService&lt;/code> that consumes it, with KEDA on vLLM metrics:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">serving.kserve.io/v1beta1&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">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">InferenceService&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">metadata&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">asistente-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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inferencia&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">annotations&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">serving.kserve.io/deploymentMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;Standard&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serving.kserve.io/autoscalerClass&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;keda&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&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">predictor&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">minReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">maxReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-h100-tp4&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">modelFormat&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">huggingface }&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"># the OCI registry from article 2/4 of this series&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">storageUri&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;oci://registry.example.internal/modelos/asistente-70b:2026-07&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">autoScaling&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">metrics&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">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">External&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">external&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">metric&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">backend&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;prometheus&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">serverAddress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;http://prometheus.monitoring.svc.cluster.local:9090&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;sum(vllm:num_requests_waiting{model_name=&amp;#34;asistente-70b&amp;#34;})&amp;#39;&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">target&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">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Value&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">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The canary, with its small print:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">serving.kserve.io/v1beta1&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">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">InferenceService&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">metadata&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">asistente-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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">inferencia&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">annotations&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="c"># WATCH OUT: canaryTrafficPercent only takes effect in KNative mode.&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"># In Standard mode it is silently ignored (issue kserve/kserve#5335, open).&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">serving.kserve.io/deploymentMode&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;KNative&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&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">predictor&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">canaryTrafficPercent&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">10&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">minReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&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">maxReplicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">runtime&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-h100-tp4&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">modelFormat&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">huggingface }&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">storageUri&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;oci://registry.example.internal/modelos/asistente-70b:2026-08&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>KServe keeps three references in the status (&lt;code>LatestReadyRevision&lt;/code>, &lt;code>LatestRolledoutRevision&lt;/code> and &lt;code>PreviousRolledoutRevision&lt;/code>): promoting means setting the percentage to 100, reverting means pinning traffic on the previous one, and a revision that does not become &lt;em>ready&lt;/em> automatically receives zero traffic.&lt;/p>
&lt;p>And the two calls. Against the V2 contract, for a classifier served with MLServer or Triton:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># metadata: which model it is, what inputs it expects, what types&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s http://clasificador.inferencia.example/v2/models/clasificador-tickets &lt;span class="p">|&lt;/span> jq .
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># readiness of the specific model, not of the server&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s -o /dev/null -w &lt;span class="s1">&amp;#39;%{http_code}\n&amp;#39;&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> http://clasificador.inferencia.example/v2/models/clasificador-tickets/ready
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s -X POST &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://clasificador.inferencia.example/v2/models/clasificador-tickets/infer &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="s1">&amp;#39;Content-Type: application/json&amp;#39;&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;{ &amp;#34;id&amp;#34;: &amp;#34;req-4711&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;inputs&amp;#34;: [ { &amp;#34;name&amp;#34;: &amp;#34;input-0&amp;#34;, &amp;#34;shape&amp;#34;: [1, 4], &amp;#34;datatype&amp;#34;: &amp;#34;FP32&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;data&amp;#34;: [5.1, 3.5, 1.4, 0.2] } ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;outputs&amp;#34;: [ { &amp;#34;name&amp;#34;: &amp;#34;output-0&amp;#34; } ] }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Against the same control plane, but on the OpenAI-compatible surface of the LLM:&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 -s -N -X POST &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> http://asistente-70b.inferencia.example/openai/v1/chat/completions &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="s1">&amp;#39;Content-Type: application/json&amp;#39;&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;{ &amp;#34;model&amp;#34;: &amp;#34;asistente-70b&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;messages&amp;#34;: [ {&amp;#34;role&amp;#34;: &amp;#34;user&amp;#34;,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;content&amp;#34;: &amp;#34;Summarise last night&amp;#39;&lt;/span>&lt;span class="se">\&amp;#39;&lt;/span>&lt;span class="s1">&amp;#39;s incident report.&amp;#34;} ],
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;max_tokens&amp;#34;: 256,
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="s1"> &amp;#34;stream&amp;#34;: true }&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Two contracts and two payload shapes, with the same &lt;code>InferenceService&lt;/code> as the unit of governance. That is the value proposition of a control plane: the contract is chosen by the model and the management is single.&lt;/p>
&lt;h2 id="autoscaling-and-the-problem-of-zero">Autoscaling and the problem of zero&lt;/h2>
&lt;p>Three autoscalers coexist, and choosing the wrong one is the most common cause of disappointment. &lt;strong>KPA&lt;/strong>, in KNative mode, scales by concurrency or RPS and is the only one that goes down to zero in a usable way, because Knative provides an activator that holds the request while the pod starts; its metric does not distinguish a 30-token request from an 8,000-token one. &lt;strong>HPA&lt;/strong>, in Standard, scales by CPU or memory: for LLMs it is useless, because the CPU of a saturated vLLM pod and that of an idle one look too much alike. &lt;strong>KEDA&lt;/strong>, in Standard, is the right option, the one we develop in &lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">autoscaling with KEDA&lt;/a>: it is enabled with &lt;code>serving.kserve.io/autoscalerClass: &amp;quot;keda&amp;quot;&lt;/code> and &lt;code>autoScaling.metrics&lt;/code> accepts external metrics against Prometheus (&lt;code>vllm:num_requests_running&lt;/code> or &lt;code>vllm:num_requests_waiting&lt;/code>) or, with the OpenTelemetry add-on, pushed metrics. Since v0.18 there is also &lt;strong>Workload Variant Autoscaling&lt;/strong>, which uses HPA or KEDA as a backend.&lt;/p>
&lt;p>That leaves the serious problem, which no autoscaler solves: zero. A 70B model in fp8 is on the order of 70 GB to move into HBM, and that path, &lt;a href="https://blog.lo0.es/en/posts/pantry-lift-from-disk-to-hbm-why-the-kitchen-opens-late/">from disk to HBM&lt;/a>, is measured in minutes: the first request after idling takes that long, or it fails.&lt;/p>
&lt;p>Worse: the installation documentation is explicit as of July 2026: &amp;ldquo;Scale from Zero is currently not supported in Standard mode for HTTP requests&amp;rdquo;. The mode the project has pivoted towards, and the only one that supports KEDA, does not know how to come up from zero on an HTTP request: KEDA can go down to zero (default cooldown of 300 seconds), but the way back requires an activator that Standard does not have. Anyone who wants real zero with HTTP activation still needs Knative.&lt;/p>
&lt;p>The levers, in order of effectiveness: do not scale the hot model to zero (&lt;code>minReplicas: 1&lt;/code> for anything with an SLA; zero is for the long tail); attack the load time, the only solution that is not a patch, with the techniques from &lt;a href="https://blog.lo0.es/en/posts/speeding-up-model-cold-start-minutes-to-seconds/">speeding up cold start&lt;/a>; use &lt;code>LocalModelCache&lt;/code>, available since v0.15 and extended to &lt;code>LLMInferenceService&lt;/code> in v0.19, to preload artifacts into a node PVC; and set correct probes, which is what comes next.&lt;/p>
&lt;h2 id="operational-traps">Operational traps&lt;/h2>
&lt;p>&lt;strong>The probe that kills the pod while the model loads.&lt;/strong> The most frequent and the most expensive to diagnose, because the symptom is a &lt;code>CrashLoopBackOff&lt;/code> with no error in the server logs. Until early 2026 the KServe vLLM templates used &lt;code>initialDelaySeconds&lt;/code> of 120 to 300 seconds on &lt;em>liveness&lt;/em>, with the result described in issue #5062 (12 February 2026, now resolved): the fast models waited too long and the large ones died before finishing loading. The fix applied: a &lt;code>startupProbe&lt;/code> of up to ten minutes and probes with no initial delay. A legacy runtime has to be reviewed by hand. In KNative mode there is a worse variant (issue #3795, closed as &lt;em>not planned&lt;/em>): what dies is not the model container but the &lt;code>queue-proxy&lt;/code>, which does not respond OK within the deadline and takes the whole pod down with it.&lt;/p>
&lt;p>&lt;strong>&lt;code>canaryTrafficPercent&lt;/code> silently ignored.&lt;/strong> It deserves its own trap because there is no error and no condition in the status to give it away: in Standard mode the field does nothing. The team believes it is serving 10 % with the new version and it is serving 100 %. While issue #5335 remains open, in Standard the canary is done with &lt;code>HTTPRoute&lt;/code> weights by hand, or in the gateway.&lt;/p>
&lt;p>&lt;strong>Canary and scarce GPUs do not get along.&lt;/strong> A 10 % canary of a 70B with tensor parallel 4 costs a whole 4-GPU node, not 10 % of the resources, because the minimum granularity is a complete replica. During the window there are two versions occupying twice the hardware, or capacity has to be taken from the stable one. That is the difference between a microservice canary and a model canary, and the reason a &lt;em>shadow&lt;/em> over duplicated traffic often pays off better.&lt;/p>
&lt;p>&lt;strong>&lt;code>storageUri&lt;/code> and credentials.&lt;/strong> Storage initializer failures are always the same ones: a &lt;code>ServiceAccount&lt;/code> without the annotated secret, a KMS-encrypted bucket with a role that has no decryption permission, an S3-compatible but not exactly S3 endpoint, or intermittent 403s, with a specific fix in v0.18. It is diagnosed in the init container logs, not in the server&amp;rsquo;s. And one standing recommendation: no downloading over &lt;code>https://&lt;/code> from the internet in production; that is precisely the problem of article 2/4.&lt;/p>
&lt;p>&lt;strong>The model size against the &lt;code>emptyDir&lt;/code>.&lt;/strong> By default the artifact lands in an ephemeral volume on the node&amp;rsquo;s disk. Seventy gigabytes per replica fill a system disk with surprising ease, and then the kubelet evicts pods for disk pressure, including ones that have nothing to do with it. You have to set &lt;code>sizeLimit&lt;/code> or use a dedicated &lt;code>pvc://&lt;/code>. For multi-node the restriction is harder, with a mandatory ReadWriteMany PVC, which rules out a good part of local block storage and pushes you towards shared file systems, with the consequences covered in &lt;a href="https://blog.lo0.es/en/posts/storage-ai-era-availability/">storage for AI&lt;/a>.&lt;/p>
&lt;p>&lt;strong>The dependency on Knative and Istio.&lt;/strong> The real cost of KNative mode: two more control planes to version and correlate with Kubernetes, and two sidecars, &lt;code>queue-proxy&lt;/code> and &lt;code>istio-proxy&lt;/code>, in every pod, with their CPU and memory consumption and their network hop per request. On a platform that already has a mesh it is a marginal cost; on a cluster dedicated to inference it has to be justified by the capabilities you are actually going to use.&lt;/p>
&lt;p>&lt;strong>Observability: the name of the metric.&lt;/strong> The &lt;code>ServingRuntime&lt;/code> declares &lt;code>prometheus.kserve.io/port&lt;/code> and &lt;code>/path&lt;/code> annotations, but the metrics that come out are the engine&amp;rsquo;s, not KServe&amp;rsquo;s: &lt;code>vllm:num_requests_waiting&lt;/code>, &lt;code>vllm:time_to_first_token_seconds&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>. The control plane does not normalise names across runtimes, so a dashboard written against vLLM is no use for Triton, and correlating with &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">DCGM&lt;/a> is still your own work. v0.19 raises the HPA/KEDA scaling status into the service conditions and emits events on readiness transitions: exactly what was needed to alert on the control plane and not only on the engine.&lt;/p>
&lt;h2 id="decision-map-when-kserve-and-when-it-is-overkill">Decision map: when KServe, and when it is overkill&lt;/h2>
&lt;p>The useful comparison is not &amp;ldquo;KServe versus OME, vLLM Production Stack, Dynamo or llm-d&amp;rdquo;. Those four are covered in &lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM inference operators&lt;/a> and compete on a different axis:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Axis&lt;/th>
&lt;th>What gets decided&lt;/th>
&lt;th>Who competes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Contract and control plane&lt;/strong>&lt;/td>
&lt;td>Which CRD describes a service, which API clients see, how the model is versioned, how the rollout is done, how LLM and non-LLM coexist&lt;/td>
&lt;td>&lt;strong>KServe&lt;/strong>, Seldon Core v2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Engine and topology&lt;/strong>&lt;/td>
&lt;td>Which engine runs, how prefill/decode is disaggregated, how routing is done with cache awareness, how multi-node tensor parallel is done&lt;/td>
&lt;td>vLLM Production Stack, OME, NVIDIA Dynamo, llm-d&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The axes are orthogonal, and &lt;code>LLMInferenceService&lt;/code> proves it: KServe did not reimplement distributed scheduling, it leaned on llm-d. Choosing KServe is not choosing &lt;em>instead of&lt;/em> an LLM operator: it is choosing the contract layer that operator lives on.&lt;/p>
&lt;p>&lt;strong>Choose KServe when&lt;/strong> the catalogue is heterogeneous, LLMs plus embeddings, rerankers, classifiers and vision, which is its decisive advantage and the one none of the four operators has: a service like the one in &lt;a href="https://blog.lo0.es/en/posts/serving-embeddings-rerankers-tei-production/">TEI in production&lt;/a> fits into the same vocabulary as the LLM. Also when you need a contract decoupled from the engine because you have clients you do not control; when you need declarative, auditable governance (which artifact, from which origin and with which runtime, in an object versionable in git and subject to admission policies), the piece that makes the rest of this series possible; and when you want a catalogue of platform runtimes separated from the teams&amp;rsquo; services.&lt;/p>
&lt;p>&lt;strong>KServe is overkill when&lt;/strong> you serve one model, on one node, with up to three replicas: the &lt;code>Deployment&lt;/code> from &lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes&lt;/a> is the right answer. When your workload is LLM only and the priority is performance per GPU, because there the value is in the engine. When you are going to use neither scale-to-zero nor canary by revision, because what remains is a &lt;code>Deployment&lt;/code> with more YAML. And when you already have an LLM operator and only serve LLMs: putting KServe on top for completeness is adding CRDs without adding decisions.&lt;/p>
&lt;h2 id="for-an-inference-factory">For an inference factory&lt;/h2>
&lt;p>&lt;strong>First: separate the runtime catalogue from the service catalogue, and do it on day one.&lt;/strong> The &lt;code>ClusterServingRuntime&lt;/code>s belong to the platform; the &lt;code>InferenceService&lt;/code>s, to the teams. That boundary turns &amp;ldquo;serving the thirtieth model&amp;rdquo; into a five-line change instead of a negotiation about vLLM flags, and it fits with &lt;a href="https://blog.lo0.es/en/posts/gitops-inference-stack-flux/">GitOps with Flux&lt;/a>: the runtime is reviewed as platform code, the service as product configuration.&lt;/p>
&lt;p>&lt;strong>Second: accept the two contracts and put the gauge changer where it belongs.&lt;/strong> V2 for the tensorial world, the OpenAI API for the generative one, and the translation in the gateway, not in every client. And since the OpenAI API does not version in the route, encode the version in the model name and make that name the same identifier that appears in the registry, in the signature and in the traces. That is what makes a request traceable back to the artifact.&lt;/p>
&lt;p>&lt;strong>Third: measure start-up before promising elasticity.&lt;/strong> The time from the pod being scheduled to the readiness probe going green, with the real model and from the real origin, decides whether scale-to-zero is a FinOps lever or an incident machine. That number, and not the price of the GPU-hour, is the one to take into the cost conversation.&lt;/p>
&lt;p>And here this piece falls short on purpose. KServe gives you the contract, the lifecycle and the rollout, but its whole trust model hangs off a text string: &lt;strong>&lt;code>storageUri&lt;/code>&lt;/strong>. The storage initializer downloads whatever is at that destination and puts it in front of the GPU without asking anything else; nothing in the &lt;code>InferenceService&lt;/code> guarantees that the artifact is the one that was approved, that it has not changed since the last audit, or that the origin is legitimate. The next article goes there: where the model bytes come from, and why an OCI registry with ORAS, Harbor, MLflow or the Kubeflow Model Registry is the difference between distributing models and scattering them.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-operators-kubernetes/">LLM inference operators on Kubernetes: OME, vLLM Production Stack, NVIDIA Dynamo and llm-d&lt;/a> — the complementary axis: engine and topology versus the contract this article deals with.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/">vLLM on Kubernetes: the LLM inference piece that does scale&lt;/a> — the plain &lt;code>Deployment&lt;/code> that KServe is the next step from, and the point where KServe is still overkill.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-inference-autoscaling-kubernetes-hpa-keda/">LLM autoscaling on Kubernetes with KEDA&lt;/a> — the vLLM metrics that feed the predictor&amp;rsquo;s &lt;code>autoScaling&lt;/code> block.&lt;/li>
&lt;li>&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> — what to do when &lt;code>canaryTrafficPercent&lt;/code> is not available, and why a model canary does not cost what it seems to.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speeding-up-model-cold-start-minutes-to-seconds/">Speeding up cold start: model loading with Tensorizer&lt;/a> — the only real way for scale-to-zero to stop being a promise.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/model-chain-of-trust-where-the-bytes-live-registry-oci-artefacts/">Model chain of trust (2/4): model registry and distribution with OCI and ORAS&lt;/a> — where that &lt;code>storageUri&lt;/code> points, and how it becomes a governed artifact.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>CNCF, &lt;em>KServe becomes a CNCF incubating project&lt;/em> (11 de noviembre de 2025) — &lt;a href="https://www.cncf.io/blog/2025/11/11/kserve-becomes-a-cncf-incubating-project/">https://www.cncf.io/blog/2025/11/11/kserve-becomes-a-cncf-incubating-project/&lt;/a>&lt;/li>
&lt;li>CNCF, &lt;em>KServe project page&lt;/em> (incubating, aceptado el 29 de septiembre de 2025) — &lt;a href="https://www.cncf.io/projects/kserve/">https://www.cncf.io/projects/kserve/&lt;/a>&lt;/li>
&lt;li>CNCF TOC, &lt;em>Project Moving Levels Checklist: KServe joining CNCF at Incubation level&lt;/em> — &lt;a href="https://github.com/cncf/toc/issues/1905">https://github.com/cncf/toc/issues/1905&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Releases&lt;/em> (v0.15.0, v0.16.0, v0.18.0, v0.19.0) — &lt;a href="https://github.com/kserve/kserve/releases">https://github.com/kserve/kserve/releases&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Inference Protocol V2 (Open Inference Protocol)&lt;/em> — &lt;a href="https://kserve.github.io/website/docs/concepts/architecture/data-plane/v2-protocol">https://kserve.github.io/website/docs/concepts/architecture/data-plane/v2-protocol&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>open-inference-protocol&lt;/em> (especificación, adoptantes, gobernanza) — &lt;a href="https://github.com/kserve/open-inference-protocol">https://github.com/kserve/open-inference-protocol&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>open-inference-protocol — generate_rest.yaml&lt;/em> — &lt;a href="https://github.com/kserve/open-inference-protocol/blob/main/specification/protocol/generate_rest.yaml">https://github.com/kserve/open-inference-protocol/blob/main/specification/protocol/generate_rest.yaml&lt;/a>&lt;/li>
&lt;li>open-inference, &lt;em>Vendor neutral fork of kserve/open-inference-protocol&lt;/em> — &lt;a href="https://github.com/open-inference/open-inference-protocol">https://github.com/open-inference/open-inference-protocol&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Generative Inference — Runtime Overview&lt;/em> (endpoints con prefijo &lt;code>openai/&lt;/code>) — &lt;a href="https://kserve.github.io/website/docs/model-serving/generative-inference/overview">https://kserve.github.io/website/docs/model-serving/generative-inference/overview&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Understanding LLMInferenceService&lt;/em> — &lt;a href="https://kserve.github.io/website/docs/model-serving/generative-inference/llmisvc/llmisvc-overview">https://kserve.github.io/website/docs/model-serving/generative-inference/llmisvc/llmisvc-overview&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Cloud-Native AI Inference at Scale using KServe and llm-d&lt;/em> (5 de marzo de 2026) — &lt;a href="https://kserve.github.io/website/blog/cloud-native-ai-inference-kserve-llm-d">https://kserve.github.io/website/blog/cloud-native-ai-inference-kserve-llm-d&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Serving Runtime (ServingRuntime y ClusterServingRuntime)&lt;/em> — &lt;a href="https://kserve.github.io/website/docs/concepts/resources/servingruntime">https://kserve.github.io/website/docs/concepts/resources/servingruntime&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Kubernetes Deployment Installation Guide&lt;/em> (K8s 1.32+, cert-manager 1.15.0+, Gateway API v1.2.1, sin scale-from-zero en Standard) — &lt;a href="https://kserve.github.io/website/docs/admin-guide/kubernetes-deployment">https://kserve.github.io/website/docs/admin-guide/kubernetes-deployment&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Canary Rollout Strategy&lt;/em> — &lt;a href="https://kserve.github.io/website/docs/model-serving/predictive-inference/rollout-strategies/canary">https://kserve.github.io/website/docs/model-serving/predictive-inference/rollout-strategies/canary&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Autoscaling with KEDA&lt;/em> — &lt;a href="https://kserve.github.io/website/docs/model-serving/predictive-inference/autoscaling/keda-autoscaler">https://kserve.github.io/website/docs/model-serving/predictive-inference/autoscaling/keda-autoscaler&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Multi-node/Multi-GPU Inference&lt;/em> (PVC RWX, sin autoescalado) — &lt;a href="https://kserve.github.io/website/docs/model-serving/generative-inference/multi-node">https://kserve.github.io/website/docs/model-serving/generative-inference/multi-node&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>Storage Options for Model Artifacts&lt;/em> — &lt;a href="https://kserve.github.io/website/docs/model-serving/storage/overview">https://kserve.github.io/website/docs/model-serving/storage/overview&lt;/a>&lt;/li>
&lt;li>KServe, issue #5335 — &lt;em>Support canaryTrafficPercent for RawDeployment mode via Gateway API HTTPRoute weights&lt;/em> (abierta, 2 de abril de 2026) — &lt;a href="https://github.com/kserve/kserve/issues/5335">https://github.com/kserve/kserve/issues/5335&lt;/a>&lt;/li>
&lt;li>KServe, issue #5062 — &lt;em>Add startupProbe to vLLM main containers&lt;/em> (12 de febrero de 2026) — &lt;a href="https://github.com/kserve/kserve/issues/5062">https://github.com/kserve/kserve/issues/5062&lt;/a>&lt;/li>
&lt;li>KServe, &lt;em>modelmesh-serving&lt;/em> (última release v0.12.0 de julio de 2023; repositorio archivado el 14 de abril de 2026) — &lt;a href="https://github.com/kserve/modelmesh-serving">https://github.com/kserve/modelmesh-serving&lt;/a>&lt;/li>
&lt;li>Red Hat, &lt;em>Converting ModelMesh and Serverless InferenceServices to RawDeployment (Standard) Mode&lt;/em> — &lt;a href="https://access.redhat.com/articles/7134025">https://access.redhat.com/articles/7134025&lt;/a>&lt;/li>
&lt;li>NVIDIA, &lt;em>Triton Inference Server — Inference Protocols and APIs&lt;/em> — &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/customization_guide/inference_protocols.html">https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/customization_guide/inference_protocols.html&lt;/a>&lt;/li>
&lt;li>OpenVINO, &lt;em>What is OpenVINO Model Server&lt;/em> (API KServe, TFS y endpoints compatibles con OpenAI) — &lt;a href="https://docs.openvino.ai/2026/model-server/ovms_what_is_openvino_model_server.html">https://docs.openvino.ai/2026/model-server/ovms_what_is_openvino_model_server.html&lt;/a>&lt;/li>
&lt;li>PyTorch, &lt;em>TorchServe — Notice: Limited Maintenance&lt;/em> — &lt;a href="https://docs.pytorch.org/serve/">https://docs.pytorch.org/serve/&lt;/a>&lt;/li>
&lt;li>Kubernetes SIG Network, &lt;em>Gateway API Inference Extension&lt;/em> (v1.5.0, 19 de abril de 2026) — &lt;a href="https://github.com/kubernetes-sigs/gateway-api-inference-extension">https://github.com/kubernetes-sigs/gateway-api-inference-extension&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>OpenCost in depth: how GPU cost is allocated in Kubernetes</title><link>https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/</link><pubDate>Sun, 14 Jun 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/opencost-gpu-cost-allocation-kubernetes/</guid><description>&lt;blockquote>
&lt;p>Notation: amounts in &lt;strong>euros (N €)&lt;/strong>, decimals with a point. European references (OVH is among
the supported providers). The dollar sign is not used (on this site it is a formula
delimiter).&lt;/p>
&lt;/blockquote>
&lt;h2 id="what-this-article-covers">What this article covers&lt;/h2>
&lt;p>Second article in the &lt;strong>FinOps&lt;/strong> track (A2), and the first tool deep dive. The
&lt;a href="https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/">FinOps introduction&lt;/a> established that
OpenCost is the CNCF standard for cost allocation; here the box gets opened: how it allocates
cost internally, where it gets its data from, how on-prem prices are configured in euros, what its
API exposes, and where the traps are. Understanding the mechanics matters because &lt;strong>it defines what
any tool on top (Kubecost included) can and cannot do&lt;/strong>, and because a badly set base price
invalidates the whole split. No recommendations; only the mechanics and the methodology.&lt;/p>
&lt;hr>
&lt;h2 id="what-opencost-is">What OpenCost is&lt;/h2>
&lt;p>OpenCost is a vendor-neutral, Apache 2.0 project, originally built by Kubecost and donated to
the CNCF (incubating project). It is a cost allocation layer for Kubernetes: it reads
resource usage from the cluster, joins it with a price, and splits the cost across Kubernetes
dimensions down to the container (&lt;a href="https://github.com/opencost/opencost">OpenCost · GitHub&lt;/a>).
It supports dynamic pricing via the billing APIs of AWS, Azure and GCP, &lt;strong>and custom prices for
on-prem clusters&lt;/strong> (&lt;a href="https://opencost.io/docs/configuration/">OpenCost · configuration&lt;/a>).&lt;/p>
&lt;p>What it is &lt;strong>not&lt;/strong>: it is not an optimiser, nor a governance system, nor a product unit economics
tool. It is the piece that answers &amp;ldquo;how much does each namespace/pod/team cost?&amp;rdquo;, and leaves the
rest to the layers above. That is why it is the foundation of the FinOps track: without correct
allocation, there is no cost per token and no TCO model worth anything.&lt;/p>
&lt;hr>
&lt;h2 id="the-cost-model-node-level-allocation">The cost model: node-level allocation&lt;/h2>
&lt;p>The key to OpenCost is that the model works at node level. It starts from the node&amp;rsquo;s &lt;strong>resource
capacity&lt;/strong> (CPU, RAM, GPU, storage) and its &lt;strong>total price&lt;/strong>, and splits that price across the
resources. When the provider does not give explicit per-resource prices, OpenCost uses the &lt;strong>ratio
of a set of base prices&lt;/strong> (marginal rates, customisable) and &lt;strong>normalises them so that the sum of
the components equals the node&amp;rsquo;s total price&lt;/strong> (&lt;a href="https://opencost.io/docs/configuration/on-prem/">OpenCost · on-prem&lt;/a>).&lt;/p>
&lt;p>This is what makes it work on-prem: you declare the node cost (depreciated capex + opex), and
OpenCost splits it without needing a cloud invoice. The normalisation guarantees that no cost is
&amp;ldquo;lost&amp;rdquo; or &amp;ldquo;invented&amp;rdquo;: what you pay for the node is exactly what gets split across its resources.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="OpenCost cost model: total node price split and normalised across CPU, GPU, RAM and disk, and then allocated by usage to each pod" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#om)}&lt;/style>
&lt;defs>&lt;marker id="om" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="40" width="170" height="60" rx="6"/>
&lt;text x="32" y="63" class="tl">Node price&lt;/text>
&lt;text x="32" y="81" class="ts">depreciated capex&lt;/text>
&lt;text x="32" y="95" class="ts">+ opex (€/hour)&lt;/text>
&lt;path class="ar" d="M190,70 L235,70"/>
&lt;rect class="bx" x="235" y="40" width="210" height="60" rx="6"/>
&lt;text x="247" y="63" class="tl">Split + normalisation&lt;/text>
&lt;text x="247" y="81" class="ts">CPU / GPU / RAM / disk&lt;/text>
&lt;text x="247" y="95" class="ts">sum = total price&lt;/text>
&lt;path class="ar" d="M445,70 L490,70"/>
&lt;rect class="bx" x="490" y="40" width="270" height="60" rx="6"/>
&lt;text x="502" y="63" class="tl">Allocation by USAGE&lt;/text>
&lt;text x="502" y="81" class="ts">GPU half used = half the cost&lt;/text>
&lt;text x="502" y="95" class="ts">per pod / namespace / team&lt;/text>
&lt;rect class="dsh" x="20" y="130" width="740" height="96" rx="6"/>
&lt;text x="34" y="152" class="tl">What OpenCost guarantees: no cost is lost and none is invented.&lt;/text>
&lt;text x="34" y="172" class="ts">· On-prem: you declare the node cost; OpenCost splits it (no cloud invoice needed).&lt;/text>
&lt;text x="34" y="190" class="ts">· GPU is allocated by USAGE, not presence: a GPU at 50 % charges half its cost.&lt;/text>
&lt;text x="34" y="208" class="ts">· Trap: if the default base price is wrong, the WHOLE split is wrong (typical on-prem under-pricing).&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="where-the-data-comes-from-prometheus">Where the data comes from: Prometheus&lt;/h2>
&lt;p>OpenCost instruments nothing of its own: it reads from Prometheus, which is a prerequisite
of the installation (&lt;a href="https://github.com/opencost/opencost">OpenCost · GitHub&lt;/a>). The sources:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Source&lt;/th>
&lt;th>What it provides&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>kube-state-metrics&lt;/code>&lt;/td>
&lt;td>state of K8s objects (pods, requests/limits)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>node-exporter&lt;/code>&lt;/td>
&lt;td>node resources and capacity&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>cAdvisor&lt;/code>&lt;/td>
&lt;td>real CPU/memory usage per container&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>dcgm-exporter&lt;/code> (NVIDIA)&lt;/td>
&lt;td>&lt;strong>GPU&lt;/strong> usage, memory and power&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>With those series, OpenCost automates the join between Kubernetes resource usage and the
provider price, reducing the custom PromQL you would otherwise write by hand (&lt;a href="https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/">Grafana ·
manage costs&lt;/a>).
The &lt;strong>GPU&lt;/strong> signal comes from DCGM via the &lt;code>dcgm-exporter&lt;/code> (part of the NVIDIA GPU Operator),
the same base as &lt;a href="https://blog.lo0.es/en/posts/gpu-observability-llm-inference-twelve-dcgm-vllm-metrics/">GPU observability&lt;/a>. Without
Prometheus and without DCGM exporting, OpenCost does not see the GPU.&lt;/p>
&lt;hr>
&lt;h2 id="architecture-the-pieces">Architecture: the pieces&lt;/h2>
&lt;p>OpenCost is not a monolith; it is three responsibilities worth telling apart:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Piece&lt;/th>
&lt;th>Function&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Cost model&lt;/strong>&lt;/td>
&lt;td>resolves the node price and splits/normalises it across resources&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Allocation API&lt;/strong>&lt;/td>
&lt;td>serves the allocated cost per dimension (&lt;code>/allocation&lt;/code> and endpoints)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Exporter&lt;/strong>&lt;/td>
&lt;td>publishes cost metrics on &lt;code>/metrics&lt;/code> for Prometheus/Grafana&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The data flow: Prometheus holds the usage series (from kube-state-metrics, cAdvisor,
dcgm-exporter); the cost model joins them with the resolved price; the Allocation API
serves them aggregated by whatever dimension you ask for; and the exporter returns them to
Prometheus so that Grafana can plot them. It is a cycle: metrics come in from Prometheus and cost
goes back to Prometheus enriched. That symmetry is what lets you reuse your observability stack
without standing up a new database: OpenCost lives on top of Prometheus, not beside it.&lt;/p>
&lt;p>An operational note: historical analysis depends on Prometheus retention. For cost reports
spanning months, a long-term backend behind it (Thanos, Mimir, VictoriaMetrics) is advisable, or
periodically exporting the allocations to a store of your own.&lt;/p>
&lt;hr>
&lt;h2 id="on-prem-prices-in-euros-the-piece-that-decides-everything">On-prem prices in euros: the piece that decides everything&lt;/h2>
&lt;p>On-prem, the most important figure, and the most neglected, is the &lt;strong>node price&lt;/strong>. It is configured
per provider; the options are &lt;code>alibaba&lt;/code>, &lt;code>aws&lt;/code>, &lt;code>azure&lt;/code>, &lt;code>gcp&lt;/code>, &lt;code>oracle&lt;/code>, &lt;strong>&lt;code>ovh&lt;/code>&lt;/strong> or &lt;strong>&lt;code>default&lt;/code>&lt;/strong>
(on-prem) (&lt;a href="https://opencost.io/docs/configuration/">OpenCost · configuration&lt;/a>).
For on-prem you use &lt;code>default&lt;/code>, supplying base prices in a &lt;code>default.json&lt;/code> (or by overriding the Helm
&lt;code>values.yaml&lt;/code>); anything you do not override uses the chart&amp;rsquo;s value (&lt;a href="https://opencost.io/docs/configuration/on-prem/">OpenCost · on-prem&lt;/a>).&lt;/p>
&lt;p>To set the node price in euros, the calculation is the TCO model one: depreciated capex + opex. An
example of an &lt;strong>8×H100&lt;/strong> node:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Component&lt;/th>
&lt;th>Calculation&lt;/th>
&lt;th>€/hour&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Capex (node ~240,000 €, 36 months)&lt;/td>
&lt;td>240,000 ÷ 26,280 h&lt;/td>
&lt;td>~9.13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energy (5.6 kW × PUE 1.4 × 0.058 €/kWh, France)&lt;/td>
&lt;td>~0.455 €/h&lt;/td>
&lt;td>~0.46&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maintenance, networking, operations&lt;/td>
&lt;td>estimate&lt;/td>
&lt;td>~1.5&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Node total&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~11.1 €/h&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>That ~11 €/h is the number you put into OpenCost, and everything is split from it. And here is
the documented trap: with the default pricing configuration, OpenCost &lt;strong>under-prices CPU and
GPU on-prem&lt;/strong> (&lt;a href="https://github.com/opencost/opencost/issues/3781">issue #3781&lt;/a>).
If you do not adjust the base prices to your real cost, the split will be systematically low and
the cost per token you report will be unreal. &lt;strong>Configuring the node price is not optional: it is
the figure everything else hangs from.&lt;/strong>&lt;/p>
&lt;hr>
&lt;h3 id="configuring-the-price-an-example">Configuring the price: an example&lt;/h3>
&lt;p>The base price is given in the Helm &lt;code>values.yaml&lt;/code> (or a &lt;code>default.json&lt;/code>), in euros per hour and per
unit of resource, derived from the node&amp;rsquo;s total cost:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="c"># values.yaml — on-prem prices in €/hour (8xH100 node, France)&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">opencost&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">customPricing&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">enabled&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">true&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">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">custom&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">costModel&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">description&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;On-prem 8xH100 node&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">CPU&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.030&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/CPU-hour&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">RAM&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.004&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/GB-hour&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">GPU&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1.30&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/GPU-hour ← the figure that moves cost/token the most&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">storage&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;0.0002&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># €/GB-hour&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>OpenCost normalises those values so that, multiplied by the node capacity, they add up to the
total price you declared (~11 €/h in the example). The number to be most careful with is the
&lt;strong>GPU&lt;/strong> one: at 1.30 €/GPU-hour, the 8 cards are ~10.4 €/h, the bulk of the node&amp;rsquo;s cost. A badly set
GPU base shifts the whole cost per token. Always review the resolved prices in
&lt;code>/allNodePricing&lt;/code> after configuring, because the default under-pricing bug bites exactly here.&lt;/p>
&lt;hr>
&lt;h2 id="allocation-by-usage-not-by-presence">Allocation: by usage, not by presence&lt;/h2>
&lt;p>The behaviour that has changed most and that matters most for GPU: in recent versions, &lt;strong>the cost
allocated to a GPU is determined by its usage, not its presence&lt;/strong>. If a GPU costing 100 €/month is
only half used, the allocated cost is ~50 € ([search]). This aligns allocation with reality,
you pay for what you use, but it has a consequence: the other half does not disappear, it is
&lt;strong>idle&lt;/strong> that somebody keeps paying for. OpenCost exposes it, and that is where the optimisation
lever comes from.&lt;/p>
&lt;p>OpenCost allocates to any Kubernetes dimension: &lt;strong>cluster, node, namespace, controller
(deployment, statefulset and so on), service, pod and container&lt;/strong>, and by &lt;strong>labels&lt;/strong> (team,
product). Access is through the Allocation API and several endpoints that expose the mechanics:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Endpoint&lt;/th>
&lt;th>What it returns&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>/allocation&lt;/code>&lt;/td>
&lt;td>cost allocated by the dimension you ask for&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/costDataModel&lt;/code>&lt;/td>
&lt;td>the resolved node price&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/allNodePricing&lt;/code>&lt;/td>
&lt;td>hourly prices per node&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/pricingSourceSummary&lt;/code>&lt;/td>
&lt;td>summary of the pricing source&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>/metrics&lt;/code>&lt;/td>
&lt;td>cost metrics for Prometheus&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;code>/metrics&lt;/code> endpoint is what turns OpenCost into a Prometheus exporter: once there, you can
write PromQL to calculate the cost and efficiency of any Kubernetes concept and build panels in
Grafana (&lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">OpenCost · exporter&lt;/a>).&lt;/p>
&lt;h3 id="allocation-api-parameters">Allocation API parameters&lt;/h3>
&lt;p>&lt;code>/allocation&lt;/code> is controlled by a handful of parameters worth knowing, because they define which
number you get:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Parameter&lt;/th>
&lt;th>What it controls&lt;/th>
&lt;th>Example&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>window&lt;/code>&lt;/td>
&lt;td>time range&lt;/td>
&lt;td>&lt;code>7d&lt;/code>, &lt;code>today&lt;/code>, specific dates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>aggregate&lt;/code>&lt;/td>
&lt;td>aggregation dimension&lt;/td>
&lt;td>&lt;code>namespace&lt;/code>, &lt;code>label:team&lt;/code>, &lt;code>pod&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>accumulate&lt;/code>&lt;/td>
&lt;td>sum the range or break it per interval&lt;/td>
&lt;td>&lt;code>true&lt;/code> (total) / &lt;code>false&lt;/code> (series)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>idle&lt;/code>&lt;/td>
&lt;td>include idle cost or not&lt;/td>
&lt;td>&lt;code>true&lt;/code> / &lt;code>false&lt;/code> / &lt;code>separate&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>filter&lt;/code>&lt;/td>
&lt;td>filter by namespace, label and so on&lt;/td>
&lt;td>&lt;code>namespace:&amp;quot;llm-prod&amp;quot;&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The &lt;strong>&lt;code>idle&lt;/code>&lt;/strong> parameter is the most revealing: with &lt;code>idle=separate&lt;/code>, OpenCost returns the idle
cost as a separate row, which lets you see at a glance how much is being paid for unused
capacity. A query with &lt;code>aggregate=label:team&lt;/code> and &lt;code>idle=separate&lt;/code> over &lt;code>window=30d&lt;/code> is, literally,
the monthly chargeback report with the waste highlighted.&lt;/p>
&lt;hr>
&lt;h2 id="gpu-in-depth-dcgm-usage-mig-and-idle">GPU in depth: DCGM, usage, MIG and idle&lt;/h2>
&lt;p>The GPU is the expensive resource, so its allocation deserves detail:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Usage signal&lt;/strong>: &lt;code>dcgm-exporter&lt;/code> gives &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> (utilisation), &lt;code>DCGM_FI_DEV_FB_USED&lt;/code>
(memory), and power. OpenCost uses utilisation to allocate by usage.&lt;/li>
&lt;li>&lt;strong>MIG (Multi-Instance GPU)&lt;/strong>: an A100 is split into up to 7 isolated instances, which allows
cutting idle from 50 % to almost 0 % by sharing one card across small workloads
([search]; see &lt;a href="https://blog.lo0.es/en/posts/sharing-one-gpu-time-slicing-mps-mig/">sharing the GPU: time-slicing, MPS and MIG&lt;/a>).
The cost allocation of each MIG instance follows the same usage logic.&lt;/li>
&lt;li>&lt;strong>Idle&lt;/strong>: the reliable detection pattern combines three pieces, DCGM metrics in Prometheus, an
alert that fires when &lt;code>DCGM_FI_DEV_GPU_UTIL &amp;lt; 10&lt;/code> for more than 15 minutes, and routing of
that alert to the team that owns the namespace ([search]).&lt;/li>
&lt;/ul>
&lt;p>An attribution nuance worth knowing: MIG creates isolated instances that Kubernetes sees as
distinct resources, so OpenCost allocates them cleanly, each one to its pod. Time-slicing, by
contrast, shares the same physical GPU across several pods with no compute isolation: there,
per-pod utilisation is harder to separate, and cost attribution becomes approximate (the GPU is
split across the pods sharing it, according to their measured usage). The practical rule: if you
need exact per-team chargeback on a shared GPU, MIG gives cleaner attribution than
time-slicing, at the price of rigid fixed partitions. It is a trade-off between cost accuracy and
flexibility that is better decided before, not after, building the split.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 220" role="img" aria-label="GPU allocation by usage in OpenCost: used cost charged to the pod, idle cost flagged to the namespace owner, and MIG splitting one card across workloads" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#om2)}&lt;/style>
&lt;defs>&lt;marker id="om2" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="40" width="170" height="70" rx="6"/>
&lt;text x="32" y="63" class="tl">GPU (DCGM)&lt;/text>
&lt;text x="32" y="81" class="ts">util, memory, power&lt;/text>
&lt;text x="32" y="98" class="ts">→ to Prometheus&lt;/text>
&lt;path class="ar" d="M190,75 L235,60"/>
&lt;path class="ar" d="M190,80 L235,150"/>
&lt;rect class="bx" x="235" y="34" width="240" height="44" rx="6"/>
&lt;text x="247" y="54" class="tl">USED cost → pod / team&lt;/text>
&lt;text x="247" y="70" class="ts">charged by utilisation&lt;/text>
&lt;rect class="dsh" x="235" y="128" width="240" height="44" rx="6"/>
&lt;text x="247" y="148" class="tl">IDLE cost → namespace owner&lt;/text>
&lt;text x="247" y="164" class="ts">alert util &amp;lt; 10 % &amp;gt; 15 min&lt;/text>
&lt;rect class="bx" x="520" y="80" width="240" height="50" rx="6"/>
&lt;text x="532" y="100" class="tl">MIG: 1 A100 → up to 7&lt;/text>
&lt;text x="532" y="118" class="ts">splits the card, idle 50%→~0&lt;/text>
&lt;text x="20" y="200" class="ts">Usage allocation reveals idle; MIG and scheduling recover it. OpenCost measures; other layers act.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="efficiency-the-number-that-triggers-action">Efficiency: the number that triggers action&lt;/h2>
&lt;p>With cost allocated by usage, OpenCost lets you calculate efficiency: how much of the allocated
cost corresponds to useful work against the total. A GPU at 30 % utilisation has an efficiency of
30 %: the remaining 70 % is idle cost that somebody pays without receiving work. That figure, per
team and per namespace, is what turns a dashboard into a chargeback conversation: &amp;ldquo;your namespace
has 5 GPUs allocated at 25 % efficiency; either you raise utilisation or you release three cards&amp;rdquo;.
It connects with &lt;a href="https://blog.lo0.es/en/posts/finops-multi-tenancy-gpu-cluster-who-pays/">FinOps and multi-tenancy&lt;/a>:
allocation locates the waste; scheduling and co-residency recover it.&lt;/p>
&lt;hr>
&lt;h2 id="worked-example-the-allocation-report">Worked example: the allocation report&lt;/h2>
&lt;p>On the ~11 €/h node (8×H100), three teams share the cluster for a month. What a
&lt;code>/allocation?aggregate=label:team&amp;amp;idle=separate&amp;amp;window=30d&lt;/code> query returns:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Team&lt;/th>
&lt;th>GPU-hours&lt;/th>
&lt;th>Avg. util.&lt;/th>
&lt;th>Allocated cost (€)&lt;/th>
&lt;th>Efficiency&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>A · chat-prod&lt;/td>
&lt;td>2,880 (4 GPU)&lt;/td>
&lt;td>78 %&lt;/td>
&lt;td>~3,744&lt;/td>
&lt;td>high&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>B · batch&lt;/td>
&lt;td>1,440 (2 GPU)&lt;/td>
&lt;td>55 %&lt;/td>
&lt;td>~1,872&lt;/td>
&lt;td>medium&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>C · experimentation&lt;/td>
&lt;td>1,440 (2 GPU)&lt;/td>
&lt;td>22 %&lt;/td>
&lt;td>~1,872&lt;/td>
&lt;td>&lt;strong>low&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>idle&lt;/strong> (separate)&lt;/td>
&lt;td>—&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&lt;strong>~1,100&lt;/strong>&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>What the report reveals: the cost allocated to A, B and C comes out of their GPU-hours and the node
price; but the &lt;strong>idle&lt;/strong> row (~1,100 €/month) is capacity nobody used and everyone pays for in the
pro rata. And the efficiency column points at C: 2 GPUs at 22 % is ~1,560 € a month of which
only ~340 € is useful work. That is the actionable figure: C does not have a model problem, it has
a utilisation problem. Without OpenCost, those ~1,100 € of idle and C&amp;rsquo;s inefficiency stay diluted
in an aggregate invoice nobody questions. With it, they are a named row and a number in euros.&lt;/p>
&lt;hr>
&lt;h2 id="useful-promql-queries">Useful PromQL queries&lt;/h2>
&lt;p>As a Prometheus exporter, OpenCost lets you build the panels by hand. Some reference queries (exact
metric names vary by version; check in your &lt;code>/metrics&lt;/code>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Cost per node and hour (resolved price)&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="nv">node_total_hourly_cost&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="c1"># GPU cost allocated per namespace (€/hour)&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="k">sum&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">namespace&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&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="nv">container_gpu_allocation&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">on&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">node&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">group_left&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nv">node_gpu_hourly_cost&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="o">)&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="c1"># Average GPU utilisation per namespace (for efficiency)&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="k">avg&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">by&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">namespace&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_GPU_UTIL&lt;/span>&lt;span class="o">)&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="c1"># Idle GPUs: utilisation &amp;lt; 10 % sustained&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="nv">DCGM_FI_DEV_GPU_UTIL&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The last one, wrapped in an alert with &lt;code>for: 15m&lt;/code> and routed to the namespace owner, is the
standard idle detection pattern. With these series in Grafana you have cost per team, efficiency
and waste on one dashboard, the &amp;ldquo;Inform report&amp;rdquo; of the FinOps phase in real time.&lt;/p>
&lt;hr>
&lt;h2 id="dynamic-cloud-prices-vs-custom-on-prem-prices">Dynamic cloud prices vs custom on-prem prices&lt;/h2>
&lt;p>The difference is worth understanding, because it changes how reliable the figure is:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Cloud&lt;/strong> (aws/azure/gcp/oracle/ovh): OpenCost obtains the price dynamically from the
provider&amp;rsquo;s billing API, so the allocated cost reflects the real rate (including discounts, spot
and so on) without you declaring anything.&lt;/li>
&lt;li>&lt;strong>On-prem&lt;/strong> (&lt;code>default&lt;/code>/&lt;code>custom&lt;/code>): you declare the price, derived from your TCO model. The
advantage is total control; the risk is that a badly calculated price skews everything. That is
why the TCO model (depreciated capex + opex) is not an academic exercise: it is literally
OpenCost&amp;rsquo;s &lt;em>input&lt;/em>.&lt;/li>
&lt;/ul>
&lt;p>The &lt;strong>&lt;code>ovh&lt;/code>&lt;/strong> provider among those supported is relevant for a European platform: if part of the
workload goes to a European sovereign cloud, OpenCost can allocate its cost with dynamic pricing,
and mix it with on-prem in the same allocation view.&lt;/p>
&lt;hr>
&lt;h2 id="from-pod-cost-to-cost-per-token">From pod cost to cost per token&lt;/h2>
&lt;p>OpenCost gets as far as &amp;ldquo;this vLLM pod cost X €/hour&amp;rdquo;. The &lt;strong>cost per token&lt;/strong>, the metric that
compares on-prem against cloud, needs one more piece: the &lt;strong>gateway&lt;/strong> (LiteLLM) that counts tokens
per request and per team, as covered in the &lt;a href="https://blog.lo0.es/en/posts/gpu-llm-finops-frameworks-metrics-state-of-the-art/">FinOps introduction&lt;/a>.
The full chain is: OpenCost gives the pod cost by usage → the gateway gives the tokens per team →
dividing them gives the cost per token per team. OpenCost is the hardware half of that
equation; without it, you would know the tokens but not their real cost. That is why A2 (allocation)
is the foundation the whole cost per token of the proposal hangs from.&lt;/p>
&lt;hr>
&lt;h2 id="deployment-helm--prometheus">Deployment: Helm + Prometheus&lt;/h2>
&lt;p>The typical deployment is via Helm, with Prometheus as a prerequisite (for scraping and
series storage). Conceptual steps:&lt;/p>
&lt;ol>
&lt;li>Have Prometheus with &lt;code>kube-state-metrics&lt;/code>, &lt;code>node-exporter&lt;/code>, &lt;code>cAdvisor&lt;/code> and &lt;code>dcgm-exporter&lt;/code>.&lt;/li>
&lt;li>Install OpenCost with Helm, pointing it at your Prometheus.&lt;/li>
&lt;li>Configure the &lt;strong>node price&lt;/strong> in euros (&lt;code>default.json&lt;/code> / &lt;code>values.yaml&lt;/code>), the step most often
neglected.&lt;/li>
&lt;li>Expose &lt;code>/metrics&lt;/code> and build panels in Grafana with PromQL over the cost metrics.&lt;/li>
&lt;/ol>
&lt;p>As a Prometheus exporter, OpenCost lets you &lt;strong>write PromQL for the cost and efficiency of any
concept&lt;/strong> and create bespoke dashboards (&lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">OpenCost · exporter&lt;/a>).
It reuses infrastructure a GPU cluster already has for observability (DCGM, Prometheus,
Grafana): OpenCost only adds the pricing and allocation layer.&lt;/p>
&lt;hr>
&lt;h2 id="opencost-vs-kubecost-just-the-essentials">OpenCost vs Kubecost (just the essentials)&lt;/h2>
&lt;p>OpenCost is the free base; Kubecost (IBM) is the commercial product built on it. Briefly,
what the commercial one adds:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Capability&lt;/th>
&lt;th>OpenCost&lt;/th>
&lt;th>Kubecost&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cost allocation (CPU/GPU/mem/PV)&lt;/td>
&lt;td>✓&lt;/td>
&lt;td>✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU via DCGM&lt;/td>
&lt;td>✓&lt;/td>
&lt;td>✓ (3.0)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Long historical retention&lt;/td>
&lt;td>depends on your Prometheus&lt;/td>
&lt;td>included&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Automatic rightsizing&lt;/td>
&lt;td>—&lt;/td>
&lt;td>✓ (Turbonomic)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Governance, alerts, enterprise RBAC&lt;/td>
&lt;td>basic&lt;/td>
&lt;td>✓&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Commercial support&lt;/td>
&lt;td>community&lt;/td>
&lt;td>✓ (IBM)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For most on-prem clusters with a platform team, OpenCost covers allocation; Kubecost adds value
when you want automated optimisation and enterprise features without operating it yourself. The
detail of the comparison, and when paying is worth it, is article &lt;strong>A3&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="implementation-checklist">Implementation checklist&lt;/h2>
&lt;p>For OpenCost to give a defensible per-team cost, order matters:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Step&lt;/th>
&lt;th>Action&lt;/th>
&lt;th>Verification&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>Prometheus with kube-state-metrics, node-exporter, cAdvisor&lt;/td>
&lt;td>series present&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>NVIDIA GPU Operator + dcgm-exporter&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> in Prometheus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>Install OpenCost (Helm) pointing at Prometheus&lt;/td>
&lt;td>&lt;code>/allocation&lt;/code> responds&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>&lt;strong>Configure the node price in €&lt;/strong> (capex+opex)&lt;/td>
&lt;td>&lt;code>/allNodePricing&lt;/code> shows the real one&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>Label pods/namespaces by team and product&lt;/td>
&lt;td>&lt;code>aggregate=label:team&lt;/code> splits correctly&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>Grafana panels for cost, efficiency and idle&lt;/td>
&lt;td>the waste is visible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>Idle alert (&lt;code>util&amp;lt;10&lt;/code> &lt;code>for: 15m&lt;/code>) to the owner&lt;/td>
&lt;td>the alert reaches the team&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>(optional) long-retention backend&lt;/td>
&lt;td>months of history available&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Step 4 is the one most often skipped and the one that most invalidates the result. If you only do
one thing right, make it that one: without the correct node price in euros, steps 5 to 7 split a
wrong cost with great precision.&lt;/p>
&lt;hr>
&lt;h2 id="limits-and-traps-data-driven">Limits and traps (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Default under-pricing.&lt;/strong> With the default pricing config, OpenCost underestimates on-prem
CPU and GPU (&lt;a href="https://github.com/opencost/opencost/issues/3781">#3781&lt;/a>). Adjust the base prices
to your real cost or the whole split will come out low.&lt;/li>
&lt;li>&lt;strong>It depends on Prometheus and DCGM.&lt;/strong> Without the right series (especially &lt;code>dcgm-exporter&lt;/code>),
there is no GPU allocation. Input data quality rules.&lt;/li>
&lt;li>&lt;strong>Allocation by usage ≠ total cost.&lt;/strong> A GPU at 50 % charges half; the other half is idle that
still costs. Do not confuse &amp;ldquo;allocated cost&amp;rdquo; with &amp;ldquo;cost paid&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Prometheus retention.&lt;/strong> Historical cost analysis depends on how much your Prometheus retains;
for long series, plan a long-term backend.&lt;/li>
&lt;li>&lt;strong>Allocation, not per-token measurement.&lt;/strong> OpenCost reaches the pod; cost per token needs the
gateway (LiteLLM) above it, as covered in the FinOps introduction.&lt;/li>
&lt;/ol>
&lt;p>With OpenCost&amp;rsquo;s mechanics clear, the next article (A3) compares what Kubecost and the commercial
alternatives add, to decide which FinOps stack to adopt. But the foundation, correct allocation
with the node price properly set in euros, is this one.&lt;/p>
&lt;h2 id="closing">Closing&lt;/h2>
&lt;p>OpenCost looks like &amp;ldquo;installing a cost tool&amp;rdquo; and is, in reality, &lt;strong>declaring what your hardware is
worth and letting it split itself&lt;/strong>. That simplicity is its strength, it lives on top of the
Prometheus and DCGM you already have, with no new database, and also its trap: the split is only as
good as the node price you declare, and the default value under-prices on-prem GPU. Properly
configured in euros, OpenCost turns the cluster&amp;rsquo;s aggregate invoice into a split per team, per
product and per GPU, separates used cost from idle, and exposes the efficiency that triggers
chargeback conversations. It is the hardware half of the cost per token equation, the other half,
the tokens, is supplied by the gateway, and without this half there is no TCO model or on-prem vs
cloud comparison that holds up. For a sovereign architecture proposal, OpenCost is the piece that
makes cost stop being an intuition and become a table in euros, reproducible and auditable, with
the idle GPU named and identified. The &lt;em>Inform&lt;/em> phase of GPU FinOps starts here, and everything
else hangs on this number being right.&lt;/p>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-chargeback-showback-multi-tenancy/">GPU chargeback and showback&lt;/a> — from OpenCost&amp;rsquo;s cost allocation to the per-team split with budgets (Kueue) and token attribution (LiteLLM).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-idle-cost-not-on-any-invoice/">GPU utilisation as a FinOps lever&lt;/a> — the cost of idle and how occupancy moves the allocated cost.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/full-tco-on-premise-gpu-cluster-capex-gpu-hour-break-even/">TCO of an on-premise GPU cluster: depreciation, energy and infrastructure&lt;/a> — the CAPEX, the depreciation and the €/GPU-hour that OpenCost needs as a base price when the node is not cloud but your own on-premise hardware.&lt;/li>
&lt;/ul>
&lt;h2 id="sources">Sources&lt;/h2>
&lt;ul>
&lt;li>OpenCost · GitHub (CNCF, Apache 2.0) — &lt;a href="https://github.com/opencost/opencost">https://github.com/opencost/opencost&lt;/a>&lt;/li>
&lt;li>OpenCost · configuración (proveedores: aws/azure/gcp/oracle/ovh/default) — &lt;a href="https://opencost.io/docs/configuration/">https://opencost.io/docs/configuration/&lt;/a>&lt;/li>
&lt;li>OpenCost · configuración on-prem (precios personalizados) — &lt;a href="https://opencost.io/docs/configuration/on-prem/">https://opencost.io/docs/configuration/on-prem/&lt;/a>&lt;/li>
&lt;li>OpenCost · exportador de Prometheus — &lt;a href="https://opencost.io/docs/integrations/opencost-exporter/">https://opencost.io/docs/integrations/opencost-exporter/&lt;/a>&lt;/li>
&lt;li>OpenCost · issue #3781 (infra-precio CPU/GPU on-prem por defecto) — &lt;a href="https://github.com/opencost/opencost/issues/3781">https://github.com/opencost/opencost/issues/3781&lt;/a>&lt;/li>
&lt;li>NVIDIA · dcgm-exporter — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">https://github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>Grafana · manage costs (OpenCost + Kubernetes) — &lt;a href="https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/">https://grafana.com/docs/grafana-cloud/monitor-infrastructure/kubernetes-monitoring/manage-costs/&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>