<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Geek at Work Blog</title><link>https://geekat.work/</link><description>Recent blog posts on Geek at Work Blog</description><generator>Hugo (https://gohugo.io)</generator><language>en-US</language><managingEditor/><webMaster/><lastBuildDate>Wed, 28 Oct 2026 00:00:00 Z</lastBuildDate><atom:link href="https://geekat.work/tags/ai/index.xml" rel="self" type="application/rss+xml"/><item><title>The Inference Gateway: Routing, Caching, and Cost Control for Production LLM Traffic</title><link>https://geekat.work/posts/the-inference-gateway-routing-caching-and-cost-control/</link><pubDate>Wed, 28 Oct 2026 00:00:00 Z</pubDate><author/><description>
An LLM endpoint looks like a URL. In production, it is a policy surface. The first version of an AI feature calls a provider SDK directly, with an API key in an environment variable. That works until a second model, provider, team, fallback requirement, or finance question arrives. Then every application reimplements the same routing, retries, quotas, caching, and logging — each one differently.
The missing component is not another model server. It is the gateway in front of the models. This article covers that layer: routing, cache correctness, failure handling, and token-level cost control. The GPU and serving layers below it are covered in the previous article on inference infrastructure; here we stay above the model endpoint.</description><content:encoded>&lt;p&gt;An LLM endpoint looks like a URL. In production, it is a policy surface. The first version of an AI feature calls a provider SDK directly, with an API key in an environment variable. That works until a second model, provider, team, fallback requirement, or finance question arrives. Then every application reimplements the same routing, retries, quotas, caching, and logging — each one differently.&lt;/p&gt;
&lt;p&gt;The missing component is not another model server. It is the gateway in front of the models. This article covers that layer: routing, cache correctness, failure handling, and token-level cost control. The GPU and serving layers below it are covered in &lt;a href="https://geekat.work/posts/ai-inference-infrastructure-devops-kubernetes-vs-managed-cloud/" target="_blank" rel="noopener noreferrer"&gt;the previous article on inference infrastructure&lt;/a&gt;; here we stay above the model endpoint.&lt;/p&gt;
&lt;h2 id="two-gateways-one-traffic-path" &gt;
&lt;div&gt;
&lt;a href="#two-gateways-one-traffic-path"&gt;
#
&lt;/a&gt;
Two Gateways, One Traffic Path
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The industry uses two names for adjacent things, and conflating them causes most of the confusion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;An AI gateway&lt;/strong&gt; is the entry point for LLM traffic. It owns identity, keys, virtual model names, provider routing, quotas, caching, guardrails, and cost attribution. It fronts managed model APIs and, often, self-hosted models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;An infrastructure inference gateway&lt;/strong&gt; is a Kubernetes data-plane component. It routes requests to pods in an &lt;code&gt;InferencePool&lt;/code&gt; using inference-aware signals: queue depth, KV-cache utilization, prefix-cache state, and active LoRA adapters. It does not manage provider keys or budgets.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;They solve different halves of the same problem:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concern&lt;/th&gt;
&lt;th&gt;AI gateway&lt;/th&gt;
&lt;th&gt;Infrastructure inference gateway&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Identity, API keys, team budgets&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Virtual model names and provider routing&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Model-aware routing inside a pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-provider fallback&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Endpoint fallback within a pool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token accounting and cost attribution&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Raw serving metrics only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Queue-depth and KV-cache-aware routing&lt;/td&gt;
&lt;td&gt;Delegates or passes through&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The split is logical, not necessarily physical. &lt;a href="https://aigateway.envoyproxy.io/" target="_blank" rel="noopener noreferrer"&gt;Agent Router&lt;/a&gt; (formerly Envoy AI Gateway) spans both: it routes across providers and supports &lt;code&gt;InferencePool&lt;/code&gt; backends. LiteLLM, Kong AI Gateway, and similar products occupy the AI gateway layer; the &lt;a href="https://gateway-api-inference-extension.sigs.k8s.io/" target="_blank" rel="noopener noreferrer"&gt;Kubernetes Gateway API Inference Extension&lt;/a&gt; defines the inference layer.&lt;/p&gt;
&lt;p&gt;The AI gateway fronts provider APIs and the inference gateway for your own pools.&lt;/p&gt;
&lt;p&gt;If you have one model, one team, and no cost pressure, skip the gateway; it adds a hop and a failure domain. Once you need centralized credentials, per-team quotas, provider fallback, or a defensible answer to &amp;ldquo;who spent this money?&amp;rdquo;, it earns its place.&lt;/p&gt;
&lt;h2 id="routing-that-understands-inference" &gt;
&lt;div&gt;
&lt;a href="#routing-that-understands-inference"&gt;
#
&lt;/a&gt;
Routing That Understands Inference
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The first job of the gateway is to stop applications from naming models. A client asks for &lt;code&gt;chat-default&lt;/code&gt;, not &lt;code&gt;provider-x/model-y-2026-09-14&lt;/code&gt;; the gateway maps the alias to a concrete deployment without a client release. Version the alias (for example, &lt;code&gt;chat-default-2026-10&lt;/code&gt;) so rollback is a routing change, not an incident.&lt;/p&gt;
&lt;p&gt;Routing policy should use attributes the application actually knows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Tenant and tier.&lt;/strong&gt; Free users can get a smaller model; paid users the primary one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Workload class.&lt;/strong&gt; Interactive chat is latency-sensitive; batch summarization is not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context size.&lt;/strong&gt; A 200k-token request should not land on a 32k-window model or on a replica with a full KV cache.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Region and data residency.&lt;/strong&gt; An EU tenant should not silently route to a US deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Capabilities.&lt;/strong&gt; Tool calling, JSON mode, vision, and structured output are not interchangeable across models.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost ceiling.&lt;/strong&gt; A team budget can exclude frontier models.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Inside a self-hosted pool, round-robin is blind to request cost: ten long-context requests can load a replica more than forty short ones. Least-connections is better but still treats requests as equal. The Kubernetes inference extension adds an &lt;strong&gt;Endpoint Picker (EPP)&lt;/strong&gt; that reads model-server metrics and chooses an endpoint per request. vLLM exposes most of these signals:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vllm:num_requests_running&lt;/code&gt; — current load per replica&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vllm:kv_cache_usage_perc&lt;/code&gt; — how close the replica is to memory pressure&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vllm:prefix_cache_queries&lt;/code&gt; and &lt;code&gt;vllm:prefix_cache_hits&lt;/code&gt; — whether the replica has a warm prefix cache&lt;/li&gt;
&lt;li&gt;active LoRA adapters — whether the replica can serve the requested fine-tune&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Prefix-cache affinity is the highest-leverage example. A multi-turn conversation resends the same system prompt and history every turn. If the next request lands on a replica that already holds that prefix in KV cache, prefill is largely skipped and time to first token drops. Strict session affinity has a failure mode, though: one popular conversation can pin itself to one replica while others idle. The useful policy is affinity with a load guard — prefer the warm replica until queue or KV-cache pressure crosses a threshold, then move.&lt;/p&gt;
&lt;p&gt;An &lt;code&gt;InferencePool&lt;/code&gt; can declare what happens when the endpoint picker is unavailable. With &lt;code&gt;failureMode: FailOpen&lt;/code&gt;, the gateway picks an endpoint itself rather than failing the request. That favors availability, but requests may land on a cold or overloaded replica. Fail-closed protects latency SLOs and hurts uptime; make the trade-off explicit.&lt;/p&gt;
&lt;h2 id="caching-without-lying-to-users" &gt;
&lt;div&gt;
&lt;a href="#caching-without-lying-to-users"&gt;
#
&lt;/a&gt;
Caching Without Lying to Users
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Caching saves the most money and causes the most subtle incidents. Three caches get mixed together in vendor decks:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Cache type&lt;/th&gt;
&lt;th&gt;What is stored&lt;/th&gt;
&lt;th&gt;Good for&lt;/th&gt;
&lt;th&gt;Breaks when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Exact response cache&lt;/td&gt;
&lt;td&gt;Hash of request, model version, parameters, tenant&lt;/td&gt;
&lt;td&gt;Identical prompts, classification, evals&lt;/td&gt;
&lt;td&gt;Chat and agent loops, where every request differs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Provider prompt cache&lt;/td&gt;
&lt;td&gt;Provider-side prefix/KV state&lt;/td&gt;
&lt;td&gt;Long system prompts, RAG context, multi-turn chat&lt;/td&gt;
&lt;td&gt;Prefix changes, or traffic moves provider/region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Semantic cache&lt;/td&gt;
&lt;td&gt;Prompt embedding plus a stored response&lt;/td&gt;
&lt;td&gt;Single-shot FAQ and support traffic&lt;/td&gt;
&lt;td&gt;Multi-turn or agentic traffic, personalized answers, cross-tenant reuse&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Exact response caching&lt;/strong&gt; is the safest. The key must include the model version, every sampling parameter that affects output, tool definitions, response format, and tenant. Miss one and you will serve an answer generated under different conditions. A TTL and an invalidation path for model or prompt releases are mandatory. Hit rates are low for chat and high for support macros, classification, and test suites; cache the workloads where it works.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Provider prompt caching&lt;/strong&gt; is not a response cache. The gateway passes the request through, and the provider reuses prefill state for a stable prefix. Savings show up as cache-read tokens and lower time to first token. The rule: keep the prefix byte-stable. Do not inject a timestamp, request ID, or per-user greeting into the system prompt if you want a hit. Keep sessions on the same provider and region when the cache is regional. Account for cache-read and cache-write tokens separately; they are priced differently from ordinary input.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Semantic caching&lt;/strong&gt; is the dangerous one. It embeds the prompt, finds the nearest cached prompt above a threshold, and returns the stored response. For single-shot FAQ it can work. For multi-turn or agentic traffic it is a correctness bug: each turn is the previous request plus a small delta, so consecutive turns are nearly identical in embedding space. LiteLLM&amp;rsquo;s documentation warns that semantic caching &amp;ldquo;will replay stale responses&amp;rdquo; on multi-turn or agentic traffic and recommends exact-match caching instead. The embedding call also runs inline, so a slow embedding endpoint becomes request latency.&lt;/p&gt;
&lt;p&gt;Tenant isolation is the other trap. Kong&amp;rsquo;s AI Semantic Cache documentation warns that the policy does not use request headers to separate cached responses and recommends configuring it per AI consumer rather than globally. LiteLLM&amp;rsquo;s &lt;code&gt;semantic_cache_scope: end_user&lt;/code&gt; setting exists for the same reason: by default, every end user behind one virtual key shares a semantic bucket. A cached answer for one customer can be served to another who asks something similar. A global semantic cache is a data leak with a hit-rate dashboard.&lt;/p&gt;
&lt;p&gt;The rule: &lt;strong&gt;cache hit rate is a cost metric, not a quality metric.&lt;/strong&gt; A 40% hit rate with wrong answers is worse than no cache. Gate semantic caching behind an allowlist of intents, scope it by tenant and end user, keep TTLs short, support a bypass header, and evaluate cached answers like generated ones. When in doubt, use exact-match caching and leave semantic caching off.&lt;/p&gt;
&lt;h2 id="failure-handling-and-quotas" &gt;
&lt;div&gt;
&lt;a href="#failure-handling-and-quotas"&gt;
#
&lt;/a&gt;
Failure Handling and Quotas
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;A gateway makes reliability policy explicit. SDK defaults are optimistic: retry a few times, wait, hope the provider recovers. At scale, those defaults compose badly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Fallback is a product decision.&lt;/strong&gt; A chain usually goes: same model in another region or provider, then a smaller model from the same family, then a static degraded response. If a paid user silently gets the small model, that is a product change. Return the actual model in response metadata (for example, an &lt;code&gt;x-model-used&lt;/code&gt; header) and decide per feature whether a downgrade is acceptable. Test fallbacks for tool-call shape, JSON validity, context window, and refusal behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Retries need one owner.&lt;/strong&gt; If the SDK makes three attempts and the gateway makes three, one request can become nine upstream calls. Pick one layer — usually the gateway — and set the other to zero. Retry only transient failures: 429, 5xx, connection errors, and timeouts before any output. Never retry 400, 401, 403, context-length errors, or content-policy rejections. Add jitter and a retry budget; a retry storm during a provider incident turns a partial outage into a full one.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Timeouts should be split by phase.&lt;/strong&gt; Connect, time to first token, idle between streamed chunks, and total are different controls. A single 30-second timeout kills legitimate long generations; a missing idle timeout holds connections open after a client disappears. Set total timeout above the upstream p99 but below client patience, and make sure ingress and load balancers do not time out streams earlier than the gateway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Rate limits should use tokens and concurrency, not just requests.&lt;/strong&gt; A 200k-token prompt is not equivalent to a 200-token prompt, but both count as one request per minute. Reserve estimated input plus &lt;code&gt;max_tokens&lt;/code&gt; before dispatch, reconcile against actual usage, and release the difference. A 32k-token prompt with &lt;code&gt;max_tokens: 8000&lt;/code&gt; reserves 40k tokens; at a 200k-token-per-minute team quota, only five such requests can be in flight. Pair this with concurrency limits so a burst cannot exhaust provider quotas or gateway memory, and return &lt;code&gt;429&lt;/code&gt; with &lt;code&gt;Retry-After&lt;/code&gt; instead of queueing without bound.&lt;/p&gt;
&lt;p&gt;Gateway quotas do not replace provider quotas. Accepting traffic you cannot deliver moves the failure downstream; keep both aligned and leave retry headroom.&lt;/p&gt;
&lt;h2 id="observability-and-unit-economics" &gt;
&lt;div&gt;
&lt;a href="#observability-and-unit-economics"&gt;
#
&lt;/a&gt;
Observability and Unit Economics
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The gateway sees every request, including quota rejections, so emit one structured event per request:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;request ID, tenant, application, and virtual model&lt;/li&gt;
&lt;li&gt;resolved provider, model, and region&lt;/li&gt;
&lt;li&gt;input, output, cached-read, cached-write, and reasoning tokens&lt;/li&gt;
&lt;li&gt;time to first token, time per output token, and total latency&lt;/li&gt;
&lt;li&gt;status, error type, retry count, and fallback flag&lt;/li&gt;
&lt;li&gt;cache status and estimated cost&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use OpenTelemetry GenAI conventions. Spans carry &lt;code&gt;gen_ai.provider.name&lt;/code&gt;, &lt;code&gt;gen_ai.request.model&lt;/code&gt;, &lt;code&gt;gen_ai.response.model&lt;/code&gt;, &lt;code&gt;gen_ai.operation.name&lt;/code&gt;, and usage attributes such as &lt;code&gt;gen_ai.usage.input_tokens&lt;/code&gt;, &lt;code&gt;gen_ai.usage.output_tokens&lt;/code&gt;, and &lt;code&gt;gen_ai.usage.cache_read.input_tokens&lt;/code&gt; (a subset of input; do not double count). Server-side latency uses &lt;code&gt;gen_ai.server.time_to_first_token&lt;/code&gt; and &lt;code&gt;gen_ai.server.time_per_output_token&lt;/code&gt;; token counters live under &lt;code&gt;gen_ai.client.inference.usage.*&lt;/code&gt;. They are still Development, so pin a version and map them to your schema.&lt;/p&gt;
&lt;p&gt;Do not log prompts and responses by default. The GenAI content attributes (&lt;code&gt;gen_ai.input.messages&lt;/code&gt;, &lt;code&gt;gen_ai.output.messages&lt;/code&gt;) are opt-in and can contain personal data. If you must capture content, sample it, redact it, set a short retention, and make access auditable.&lt;/p&gt;
&lt;p&gt;Cost accounting starts with a versioned price table: input, output, cache read, cache write, and reasoning prices per provider and model. Calculate cost from actual provider usage, not token estimates. Reconcile monthly against the invoice; a drifting price table is worse than none. Show cache hits as avoided upstream cost, not zero cost — the gateway still did work, and semantic-cache embeddings are not free.&lt;/p&gt;
&lt;p&gt;Track p95 TTFT, p95 TPOT, error rate by provider, fallback rate, 429 rate, queue depth, tokens per request, and cost per successful task. &amp;ldquo;Cost per million tokens&amp;rdquo; is an input metric; &amp;ldquo;cost per successful task&amp;rdquo; is the business metric, because it captures retries, fallbacks, and wasted generations. Break both down by tenant and model; alert on SLO burn and fallback spikes, not raw spend.&lt;/p&gt;
&lt;h2 id="a-reference-architecture" &gt;
&lt;div&gt;
&lt;a href="#a-reference-architecture"&gt;
#
&lt;/a&gt;
A Reference Architecture
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;A workable production topology separates policy from serving:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; +----------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; apps / agents -----&amp;gt; | AI gateway |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | auth, aliases, |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | quotas, cache, |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | fallback, telemetry |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; +----------+-----------+
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; +------------------+------------------+
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v v
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; managed model APIs inference gateway
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; (provider A, provider B) (Gateway API + EPP)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; +--------+--------+
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; | |
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; v v
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; vLLM pool A vLLM pool B
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; base model LoRA adapters
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A minimal LiteLLM configuration (model IDs are placeholders):&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;model_list&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;model_name&lt;/span&gt;: chat-default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;litellm_params&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;model&lt;/span&gt;: openai/&amp;lt;primary-model&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;api_key&lt;/span&gt;: os.environ/PRIMARY_API_KEY
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;model_name&lt;/span&gt;: chat-default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;litellm_params&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;model&lt;/span&gt;: anthropic/&amp;lt;secondary-model&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;api_key&lt;/span&gt;: os.environ/SECONDARY_API_KEY
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;model_name&lt;/span&gt;: chat-default
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;litellm_params&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;model&lt;/span&gt;: openai/llama-3.3-70b-instruct &lt;span style="color:#78787e"&gt;# served by vLLM&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;api_base&lt;/span&gt;: http://inference-gateway.inference.svc/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;api_key&lt;/span&gt;: os.environ/INTERNAL_GATEWAY_KEY
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;model_name&lt;/span&gt;: chat-fallback
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;litellm_params&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;model&lt;/span&gt;: openai/&amp;lt;smaller-model&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;api_key&lt;/span&gt;: os.environ/PRIMARY_API_KEY
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;router_settings&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;routing_strategy&lt;/span&gt;: simple-shuffle
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;num_retries&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;timeout&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;allowed_fails&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;cooldown_time&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;30&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;litellm_settings&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;fallbacks&lt;/span&gt;: [{&lt;span style="color:#ff6ac1"&gt;&amp;#34;chat-default&amp;#34;: &lt;/span&gt;[&lt;span style="color:#5af78e"&gt;&amp;#34;chat-fallback&amp;#34;&lt;/span&gt;]}]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;cache&lt;/span&gt;: &lt;span style="color:#ff6ac1"&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;cache_params&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;type&lt;/span&gt;: redis
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;On the Kubernetes side, the inference gateway routes to an &lt;code&gt;InferencePool&lt;/code&gt; rather than a &lt;code&gt;Service&lt;/code&gt;. An abridged example:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;apiVersion&lt;/span&gt;: inference.networking.k8s.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: InferencePool
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llama-70b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;selector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;matchLabels&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;app&lt;/span&gt;: vllm-llama-70b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;targetPorts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;number&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;8000&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;endpointPickerRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llama-70b-epp
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;port&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;number&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;9002&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;failureMode&lt;/span&gt;: FailOpen
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;---
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;apiVersion&lt;/span&gt;: gateway.networking.k8s.io/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: HTTPRoute
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llama-route
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;parentRefs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;group&lt;/span&gt;: gateway.networking.k8s.io
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: Gateway
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: inference-gateway
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;rules&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;matches&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;path&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;type&lt;/span&gt;: PathPrefix
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;value&lt;/span&gt;: /v1/chat/completions
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;backendRefs&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;group&lt;/span&gt;: inference.networking.k8s.io
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: InferencePool
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llama-70b
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The AI gateway treats the inference gateway as another OpenAI-compatible backend. You can change serving runtimes, GPU pools, or model versions without changing application code, and add a managed provider without changing Kubernetes manifests.&lt;/p&gt;
&lt;p&gt;Treat the gateway like production infrastructure: at least two replicas across zones, a pod disruption budget, no synchronous calls to external services in the policy chain, Redis local to the region, and no response buffering so streaming actually streams. Test at twice your expected peak; a gateway that is cheap at idle can become the first-launch bottleneck. Decide whether a break-glass path direct to a provider exists. It bypasses quotas and audit, so document it as an emergency procedure, not a habit.&lt;/p&gt;
&lt;h2 id="anti-patterns-and-a-checklist" &gt;
&lt;div&gt;
&lt;a href="#anti-patterns-and-a-checklist"&gt;
#
&lt;/a&gt;
Anti-Patterns and a Checklist
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Predictable failures:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Gateway as a business-logic layer.&lt;/strong&gt; Prompt construction, RAG, and agent orchestration belong in the application; the gateway routes, enforces, and observes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Retry multiplication.&lt;/strong&gt; SDK retries plus gateway retries plus provider retries turn one failure into a load spike. One owner, one budget.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Silent downgrade.&lt;/strong&gt; Falling back to a smaller model without telling the caller changes quality. Surface the actual model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Semantic cache by default.&lt;/strong&gt; It is a correctness risk on multi-turn and agentic traffic, and a tenant-isolation risk when scoped globally.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prompt logging everywhere.&lt;/strong&gt; It creates a PII archive with no owner. Log metadata; opt in to content.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Routing on stale metrics.&lt;/strong&gt; If the scrape interval outlasts the burst, the endpoint picker steers with yesterday&amp;rsquo;s traffic.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unmonitored single point of failure.&lt;/strong&gt; The gateway sees all traffic, so its own availability, latency, and error budget need dashboards too.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A short checklist before calling the layer production-ready:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Apps use virtual model names; aliases are versioned and reversible.&lt;/li&gt;
&lt;li&gt;Per-team quotas exist in tokens and concurrency, not just requests per minute.&lt;/li&gt;
&lt;li&gt;The fallback chain is tested for tool calling, JSON, context length, and refusal behavior.&lt;/li&gt;
&lt;li&gt;Exactly one layer owns retries, with jitter and a budget.&lt;/li&gt;
&lt;li&gt;Timeouts are split into connect, first-token, idle, and total.&lt;/li&gt;
&lt;li&gt;Caches are scoped by tenant, model version, and parameters; semantic caching is opt-in.&lt;/li&gt;
&lt;li&gt;Every request emits tokens, TTFT, TPOT, status, fallback flag, and cost.&lt;/li&gt;
&lt;li&gt;Content is not logged by default; the gateway has its own SLO and capacity test.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="conclusion" &gt;
&lt;div&gt;
&lt;a href="#conclusion"&gt;
#
&lt;/a&gt;
Conclusion
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The model is the product; the gateway is how you keep it running. It turns a model name into policy, a provider incident into a fallback, a cache hit into either a saving or a correctness bug, and a token into a number finance can reconcile.&lt;/p&gt;
&lt;p&gt;You do not need every feature on day one. Start with one virtual model, one fallback, one token-based quota, and one dashboard for cost per successful task. Add inference-aware routing when you run your own pools, and caching only where you can prove it is safe. It is the difference between a demo and a feature that survives its first real month.&lt;/p&gt;
&lt;h2 id="further-reading" &gt;
&lt;div&gt;
&lt;a href="#further-reading"&gt;
#
&lt;/a&gt;
Further Reading
&lt;/div&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://aigateway.envoyproxy.io/" target="_blank" rel="noopener noreferrer"&gt;Agent Router (formerly Envoy AI Gateway)&lt;/a&gt; — provider routing and &lt;code&gt;InferencePool&lt;/code&gt; support&lt;/li&gt;
&lt;li&gt;&lt;a href="https://gateway-api-inference-extension.sigs.k8s.io/" target="_blank" rel="noopener noreferrer"&gt;Gateway API Inference Extension&lt;/a&gt; — &lt;code&gt;InferencePool&lt;/code&gt; and Endpoint Picker&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.litellm.ai/docs/routing" target="_blank" rel="noopener noreferrer"&gt;LiteLLM Router&lt;/a&gt; and &lt;a href="https://docs.litellm.ai/docs/proxy/caching" target="_blank" rel="noopener noreferrer"&gt;caching&lt;/a&gt; — aliases, fallbacks, cooldowns, caches&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developer.konghq.com/ai-gateway/" target="_blank" rel="noopener noreferrer"&gt;Kong AI Gateway&lt;/a&gt; — policies, semantic caching, consumer scoping&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/open-telemetry/semantic-conventions-genai" target="_blank" rel="noopener noreferrer"&gt;OpenTelemetry GenAI conventions&lt;/a&gt; — spans, token metrics, content capture&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.vllm.ai/en/latest/design/metrics/" target="_blank" rel="noopener noreferrer"&gt;vLLM metrics&lt;/a&gt; — queue, KV cache, prefix cache, TTFT, TPOT&lt;/li&gt;
&lt;li&gt;&lt;a href="https://geekat.work/posts/ai-inference-infrastructure-devops-kubernetes-vs-managed-cloud/" target="_blank" rel="noopener noreferrer"&gt;AI inference infrastructure from a DevOps perspective&lt;/a&gt; — the GPU and serving layer below the gateway&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>AI</category><category>LLM</category><category>DevOps</category><category>Kubernetes</category><guid isPermaLink="true">https://geekat.work/posts/the-inference-gateway-routing-caching-and-cost-control/</guid></item><item><title>AI Inference Infrastructure from a DevOps Perspective: Self-Managed Kubernetes vs Managed Cloud</title><link>https://geekat.work/posts/ai-inference-infrastructure-devops-kubernetes-vs-managed-cloud/</link><pubDate>Mon, 26 Oct 2026 00:00:00 Z</pubDate><author/><description>
Serving machine learning models is no longer a research problem — it is an infrastructure problem. Model inference workloads have their own shape: bursty traffic, expensive GPU-bound nodes, multi-gigabyte images and model weights, and autoscaling that reacts to tokens per second rather than CPU. In this guide we walk through what a production inference stack actually looks like — first as self-managed infrastructure on Kubernetes, then as managed cloud services from AWS, Google Cloud, Azure, and the GPU-native clouds — and compare the two paths on operations, scaling, latency, and cost from the perspective of the person who has to keep both running at 3 a.m.</description><content:encoded>&lt;p&gt;Serving machine learning models is no longer a research problem — it is an infrastructure problem. Model inference workloads have their own shape: bursty traffic, expensive GPU-bound nodes, multi-gigabyte images and model weights, and autoscaling that reacts to tokens per second rather than CPU. In this guide we walk through what a production inference stack actually looks like — first as self-managed infrastructure on Kubernetes, then as managed cloud services from AWS, Google Cloud, Azure, and the GPU-native clouds — and compare the two paths on operations, scaling, latency, and cost from the perspective of the person who has to keep both running at 3 a.m.&lt;/p&gt;
&lt;h2 id="why-inference-infrastructure-is-different" &gt;
&lt;div&gt;
&lt;a href="#why-inference-infrastructure-is-different"&gt;
#
&lt;/a&gt;
Why Inference Infrastructure Is Different
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;A model inference service looks like a normal web API but behaves very differently under the hood:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Hardware constraints.&lt;/strong&gt; GPUs are expensive, scarce, and can&amp;rsquo;t be live-migrated. A pod that pins a GPU shapes how you schedule, drain, upgrade, and preempt. Driver/CUDA/kernel version alignment is a real compatibility matrix you must manage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Big, cold artifacts.&lt;/strong&gt; Images with CUDA runtimes and downloaded model weights easily reach 5–50 GB. Cold starts are measured in minutes, not seconds, which breaks naive scale-to-zero designs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom scaling signals.&lt;/strong&gt; HPA on CPU is nearly useless: vLLM saturates GPU KV-cache utilization way before CPU looks busy. You need scaling on model-specific metrics (queue depth, requests per replica, time to first token).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Different SLOs.&lt;/strong&gt; For LLM serving you don&amp;rsquo;t just care about p99 latency of a response; you care about time to first token (TTFT) and time per output token (TPOT), because streaming changes how users perceive latency.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost concentration.&lt;/strong&gt; A single A100/H100 node can cost several thousand dollars a month. Inference is where most of your ML cloud bill lives, and idle-GPU waste is the default state unless you engineer against it.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These constraints drive the real architectural decision: where does the GPU fleet and the serving logic live — in your cluster, or in a provider&amp;rsquo;s?&lt;/p&gt;
&lt;h2 id="the-anatomy-of-an-inference-request" &gt;
&lt;div&gt;
&lt;a href="#the-anatomy-of-an-inference-request"&gt;
#
&lt;/a&gt;
The Anatomy of an Inference Request
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Before choosing infrastructure, it helps to understand what a single request actually does, because every infra decision downstream is reacting to this behavior.&lt;/p&gt;
&lt;p&gt;For a typical LLM request on vLLM:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Request admission.&lt;/strong&gt; The request arrives over HTTP/gRPC and enters a waiting queue.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weight residency check.&lt;/strong&gt; The model weights must already be in GPU memory — loading 16 GB from disk takes tens of seconds to minutes, so this must happen at startup, per replica.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batching.&lt;/strong&gt; The runtime waits a few milliseconds to coalesce concurrent requests into a batch. Modern serving engines do &lt;em&gt;continuous batching&lt;/em&gt;: new requests join a running batch at each decode step rather than waiting for the whole batch to finish, keeping GPU utilization flat under load.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Prefill and decode.&lt;/strong&gt; The prompt is processed (prefill, compute-bound), then tokens are generated one at a time (decode, memory-bandwidth-bound). The KV cache — the stored attention state of the context — grows with prompt + output length and is the real memory consumer, not the weights.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Streaming response.&lt;/strong&gt; Tokens stream out over SSE/WebSocket as they&amp;rsquo;re generated.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Why this matters for DevOps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;KV cache&lt;/strong&gt; means memory, not compute, is often the bottleneck → you tune &lt;code&gt;max-model-len&lt;/code&gt;, quantization, and PagedAttention-style cache management. OOM at the KV cache is the most common production failure of a self-managed LLM deployment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Continuous batching&lt;/strong&gt; means throughput per GPU is highly dependent on concurrency → autoscaling to more replicas can &lt;em&gt;decrease&lt;/em&gt; cost efficiency if it breaks batching. You often want fewer, busier replicas rather than many idle ones.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Weights at startup&lt;/strong&gt; means every scale-up event pays a multi-minute loading cost → your autoscaler needs long cooldowns and pre-warmed node pools to handle bursts.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Keep this lifecycle in mind; it explains the Kubernetes specifics below.&lt;/p&gt;
&lt;h2 id="option-1-self-managed-inference-on-kubernetes" &gt;
&lt;div&gt;
&lt;a href="#option-1-self-managed-inference-on-kubernetes"&gt;
#
&lt;/a&gt;
Option 1: Self-Managed Inference on Kubernetes
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;The typical self-managed stack looks like this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Common choice&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Serving runtime&lt;/td&gt;
&lt;td&gt;vLLM (LLMs), Triton with TensorRT/TensorRT-LLM, SGLang, KServe, BentoML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU enablement&lt;/td&gt;
&lt;td&gt;NVIDIA GPU Operator (drivers, device plugin, DCGM monitoring, MIG)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cluster/fleet&lt;/td&gt;
&lt;td&gt;EKS / GKE / AKS with GPU node groups, or bare metal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Request routing&lt;/td&gt;
&lt;td&gt;Ingress or service mesh, load balancing aware of in-flight requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Autoscaling&lt;/td&gt;
&lt;td&gt;HPA + KEDA on custom metrics; Ray Autoscaler for LLM serving&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Storage/weights&lt;/td&gt;
&lt;td&gt;Model artifacts in object storage, PVC or init-container download, local NVMe cache&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;DCGM exporter, Prometheus, Grafana, LLM-specific dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="serving-runtimes-know-what-youre-deploying" &gt;
&lt;div&gt;
&lt;a href="#serving-runtimes-know-what-youre-deploying"&gt;
##
&lt;/a&gt;
Serving Runtimes: Know What You&amp;rsquo;re Deploying
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Choosing a runtime is the first real decision, and it&amp;rsquo;s a DevOps-relevant one because each has different operational characteristics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;vLLM&lt;/strong&gt; — the current default for serving open-weight LLMs via an OpenAI-compatible API. Best-in-class continuous batching with PagedAttention. Operationally, you manage GPU memory limits carefully (the &lt;code&gt;gpu_memory_utilization&lt;/code&gt; flag) and version-bump aggressively: the project moves fast and performance improves release to release.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Triton Inference Server&lt;/strong&gt; — NVIDIA&amp;rsquo;s general-purpose server, targeting traditional ML (XGBoost, ONNX, vision models) and TensorRT-LLM for NVIDIA-tuned LLM serving. Best when you have a mixed zoo of model types behind one server, or you want NVIDIA&amp;rsquo;s engines. More config-heavy (model repository structure, config.pbtxt), but predictable.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SGLang&lt;/strong&gt; — newer engine with advanced scheduling (RadixAttention, prefix caching), often outperforms vLLM on multi-turn conversation traffic where prompts share prefixes. Watch this space if chat workloads dominate.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;KServe&lt;/strong&gt; — not a runtime but a serving &lt;em&gt;framework&lt;/em&gt; on Kubernetes: gives you &lt;code&gt;InferenceService&lt;/code&gt; CRDs, canary traffic splits among model revisions, scale-from-zero via queue-proxy, and pluggable backends (it can run vLLM or Triton underneath).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The practical pattern: vLLM (or SGLang) for OpenAI-compatible LLM endpoints, Triton for everything else, KServe if you want model-level reconciliation and canarying as Kubernetes-native objects rather than rolling your own.&lt;/p&gt;
&lt;h3 id="gpu-operator-and-node-management" &gt;
&lt;div&gt;
&lt;a href="#gpu-operator-and-node-management"&gt;
##
&lt;/a&gt;
GPU Operator and Node Management
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Running GPUs in Kubernetes without the operator means hand-installing drivers on node images, which breaks under upgrades. The &lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html" target="_blank" rel="noopener noreferrer"&gt;NVIDIA GPU Operator&lt;/a&gt; packages the whole stack — drivers, container toolkit, device plugin, DCGM exporter for metrics, and MIG manager — as controllable Kubernetes objects. Node pool definition typically looks like:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;apiVersion&lt;/span&gt;: eks.amazonaws.com/v1alpha2
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: NodeGroup &lt;span style="color:#78787e"&gt;# EKS managed node group; GKE/AKS equivalents apply&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: gpu-inference
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;instanceType&lt;/span&gt;: g6e.12xlarge &lt;span style="color:#78787e"&gt;# L40S-class cards&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;labels&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;node-type&lt;/span&gt;: gpu-inference
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;taints&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;key&lt;/span&gt;: nvidia.com/gpu
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;value&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;dedicated&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;effect&lt;/span&gt;: NoSchedule
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;scaling&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;minSize&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;maxSize&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Things to get right here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Taint GPU nodes.&lt;/strong&gt; Non-GPU pods will happily schedule onto your most expensive nodes unless you taint. Every real cluster has horror stories of a misconfigured logging DaemonSet costing thousands per month.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Set up Cluster Autoscaler / Karpenter for GPU instance types.&lt;/strong&gt; GPU capacity is scarce in many regions. Configure least-waste priority (e.g., prefer 8-GPU nodes only for 8-GPU jobs), consolidation carefully — consolidations that reschedule GPU pods cause visible service blips from weight reloading.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;MIG (Multi-Instance GPU) for small models.&lt;/strong&gt; On A100/H100 you can partition a GPU into slices (e.g., 7×10GB slices). If your models fit in 10 GB, MIG turns one node into seven serving replicas and slashes cost. The GPU Operator&amp;rsquo;s MIG manager configures this; the device plugin then exposes &lt;code&gt;nvidia.com/mig-1g.10gb&lt;/code&gt; as a schedulable resource.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="weights-the-storage-side-of-inference" &gt;
&lt;div&gt;
&lt;a href="#weights-the-storage-side-of-inference"&gt;
##
&lt;/a&gt;
Weights: The Storage Side of Inference
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Model loading is a first-class ops concern. The naive path —Pod starts, downloads 15 GB from Hugging Face on the fly — gives you multi-minute readiness, wasted bandwidth (every replica downloads), and flaky readiness on network hiccups.&lt;/p&gt;
&lt;p&gt;Better patterns, in increasing order of investment:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Local NVMe cache on the node.&lt;/strong&gt; An init container (or &lt;code&gt;hostPath&lt;/code&gt;-backed downloader) checks for the model in a node-local directory and downloads only if absent. After the first scale-up on a node, subsequent pods start in seconds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PVC with &lt;code&gt;ReadOnlyMany&lt;/code&gt;.&lt;/strong&gt; A pre-populated PVC (or a Filestore/EFS/GCFileSystem mount) shared across replicas avoids redundant downloads. Watch network filesystem throughput — for big models, it can become a bottleneck slower than downloading from object storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-baked serving images or snapshots.&lt;/strong&gt; Bake weights into the image (big images, ~15–40 GB pull) or AMI/disk snapshot (fast). This is the &amp;ldquo;warm node pool&amp;rdquo; approach: autoscaler launches nodes from a snapshot with weights and even model server config already in place.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The vLLM deployment example from earlier, with the cache volume pattern:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;apiVersion&lt;/span&gt;: apps/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: Deployment
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llm-inference
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;replicas&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;selector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;matchLabels&lt;/span&gt;: { &lt;span style="color:#ff6ac1"&gt;app&lt;/span&gt;: llm-inference }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;template&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;labels&lt;/span&gt;: { &lt;span style="color:#ff6ac1"&gt;app&lt;/span&gt;: llm-inference }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;nodeSelector&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;node-type&lt;/span&gt;: gpu-inference
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;tolerations&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;key&lt;/span&gt;: nvidia.com/gpu
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;operator&lt;/span&gt;: Exists
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;effect&lt;/span&gt;: NoSchedule
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;initContainers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: fetch-model
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;image&lt;/span&gt;: amazon/aws-cli:latest
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;command&lt;/span&gt;: [&lt;span style="color:#5af78e"&gt;&amp;#34;sh&amp;#34;&lt;/span&gt;, &lt;span style="color:#5af78e"&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;, &lt;span style="color:#5af78e"&gt;&amp;#34;aws s3 sync s3://our-models/llama-8b /models/llama-8b --no-progress&amp;#34;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;volumeMounts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: model-cache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;mountPath&lt;/span&gt;: /models
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;containers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: vllm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;image&lt;/span&gt;: vllm/vllm-openai:v0.6.4
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;args&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - --model
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - /models/llama-8b
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - --max-model-len
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#5af78e"&gt;&amp;#34;8192&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - --gpu-memory-utilization
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#5af78e"&gt;&amp;#34;0.92&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;resources&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;limits&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;nvidia.com/gpu&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;memory&lt;/span&gt;: 64Gi
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;requests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;cpu&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;8&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;readinessProbe&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;httpGet&lt;/span&gt;: { &lt;span style="color:#ff6ac1"&gt;path&lt;/span&gt;: &lt;span style="color:#ff6ac1"&gt;/health, port&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;8000&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;initialDelaySeconds&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;10&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;periodSeconds&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;5&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;failureThreshold&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;60&lt;/span&gt; &lt;span style="color:#78787e"&gt;# allow minutes for first load&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;volumeMounts&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: model-cache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;mountPath&lt;/span&gt;: /models
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;volumes&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: model-cache
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;emptyDir&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;sizeLimit&lt;/span&gt;: 40Gi
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the long &lt;code&gt;failureThreshold&lt;/code&gt; on the readiness probe: cold starts are legitimately minutes. If you set a normal 30-second threshold, every scale-up triggers a crash loop of your new replica and the autoscaler fights itself.&lt;/p&gt;
&lt;h3 id="autoscaling-on-the-right-signal" &gt;
&lt;div&gt;
&lt;a href="#autoscaling-on-the-right-signal"&gt;
##
&lt;/a&gt;
Autoscaling on the Right Signal
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Standard HPA on CPU is the wrong tool, for two reasons: GPU inference barely uses host CPU, and scale-up latency (node launch + image pull + weight load) is minutes, so you need to scale &lt;em&gt;early and hold&lt;/em&gt;, not react and thrash.&lt;/p&gt;
&lt;p&gt;The standard approach is KEDA with Prometheus metrics from the serving runtime. vLLM exposes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vllm:num_requests_waiting&lt;/code&gt; — queue depth&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vllm:num_requests_running&lt;/code&gt; — in-flight per replica&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vllm:time_to_first_token_seconds&lt;/code&gt; / &lt;code&gt;vllm:e2e_request_latency_seconds&lt;/code&gt; — latency quantiles&lt;/li&gt;
&lt;li&gt;&lt;code&gt;vllm:gpu_cache_usage_perc&lt;/code&gt; — KV cache saturation&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;apiVersion&lt;/span&gt;: keda.sh/v1alpha1
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;kind&lt;/span&gt;: ScaledObject
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llm-scaledobject
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;spec&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;scaleTargetRef&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llm-inference
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;minReplicaCount&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;2&lt;/span&gt; &lt;span style="color:#78787e"&gt;# warm floor: never below your p-cold-start tolerance&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;maxReplicaCount&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;12&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;cooldownPeriod&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;600&lt;/span&gt; &lt;span style="color:#78787e"&gt;# long enough to avoid thrash — readiness is slow&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;triggers&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; - &lt;span style="color:#ff6ac1"&gt;type&lt;/span&gt;: prometheus
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;metadata&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;address&lt;/span&gt;: http://prometheus.monitoring:9090
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;query&lt;/span&gt;: sum(vllm:num_requests_waiting)
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;threshold&lt;/span&gt;: &lt;span style="color:#5af78e"&gt;&amp;#34;10&amp;#34;&lt;/span&gt; &lt;span style="color:#78787e"&gt;# target: max ~10 queued requests per replica&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Key engineering points:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Warm floor over scale-to-zero.&lt;/strong&gt; With minute-scale cold starts, scale-to-zero only works for workloads where users tolerate minutes of latency (background jobs, internal batch tools). Otherwise keep a warm floor and treat scale-down as a cost lever, not a default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Long cooldowns.&lt;/strong&gt; Because readiness is slow, aggressive scale-down after a spike makes you pay the full cold-start cost repeatedly. Ten minutes of cooldown is more normal than datacenter default.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node-level pre-scaling.&lt;/strong&gt; Pod autoscaling doesn&amp;rsquo;t help if the autoscaler needs to launch a GPU node to satisfy it. Pre-provision GPU capacity or over-provision GPU nodes with fake low-priority &amp;ldquo;placeholder&amp;rdquo; pods (the classic cluster-autoscaler trick) if burst latency is contractual.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Batching-aware reasoning.&lt;/strong&gt; A single replica can often handle 20–50 concurrent LLM requests with good TPOT. If you scale on requests-per-replica with a target like 30 or 50, you keep the batching advantage instead of shattering load across underfilled replicas.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="load-balancing-and-traffic-shape" &gt;
&lt;div&gt;
&lt;a href="#load-balancing-and-traffic-shape"&gt;
##
&lt;/a&gt;
Load Balancing and Traffic Shape
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Standard round-robin across pods is not ideal for LLM serving: replicas have different queue lengths already. Consider a routing layer aware of in-flight requests (e.g., a gateway or a serving framework&amp;rsquo;s own router that balances by &lt;code&gt;num_requests_running&lt;/code&gt;), and sticky routing by conversation/session for multi-turn traffic so prefix caches stay warm. If you&amp;rsquo;re doing streaming (SSE/WebSocket), confirm your ingress layer has long/idle timeouts tuned — common sources of mysterious 502s mid-stream.&lt;/p&gt;
&lt;h3 id="spot-gpus-and-interruption-engineering" &gt;
&lt;div&gt;
&lt;a href="#spot-gpus-and-interruption-engineering"&gt;
##
&lt;/a&gt;
Spot GPUs and Interruption Engineering
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;Spot/preemptible GPU capacity is frequently 50–80% cheaper and, because inference is stateless (unlike training checkpoints), somewhat friendlier to interruptions. Engineering it well means:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Disruption-tolerant deployments: reasonable PodDisruptionBudgets, including spot-node taints and tolerating instance termination notices (via 2-minute termination handlers draining pods).&lt;/li&gt;
&lt;li&gt;Fast weight loading (the storage patterns above) so a rescheduled replica is serving reasonably quickly after a handoff.&lt;/li&gt;
&lt;li&gt;Multiple capacity shapes: don&amp;rsquo;t demand only &lt;code&gt;p5.48xlarge&lt;/code&gt;; deploy with fallback smaller instance types and run several smaller replicas.&lt;/li&gt;
&lt;li&gt;Some teams run spot for the bulk of capacity and on-demand as a warm internal minimum, using cluster autoscaler &amp;ldquo;prefer-spot&amp;rdquo; strategies.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="observability-for-inference" &gt;
&lt;div&gt;
&lt;a href="#observability-for-inference"&gt;
##
&lt;/a&gt;
Observability for Inference
&lt;/div&gt;
&lt;/h3&gt;
&lt;p&gt;The metrics stack is standard Prometheus/Grafana, but the &lt;em&gt;content&lt;/em&gt; is GPU- and LLM-specific:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;DCGM exporter&lt;/strong&gt; — GPU utilization, memory usage, temperature, power, ECC errors. High temperature and throttling directly affect latency and hardware life.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serving engine metrics&lt;/strong&gt; — queue depths (waiting requests), running requests, TTFT and end-to-end latency quantiles, KV cache usage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gateway metrics&lt;/strong&gt; — request rate, streaming connection counts and durations (long-lived streaming requires rounding and thinking about connection limits and timeouts differently than normal HTTP).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost instrumentation&lt;/strong&gt; — tokens served per hour per GPU, dollars per million tokens. This is your north-star SLO for inference economics: track it per model version, because quantization and version changes move it substantially.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Alerting on GPU utilization is misleading: a well-batched GPU should run at ~100% memory bandwidth/utilization under load. Alert instead on queue depth and latency SLOs, treating GPU utilization as a capacity-planning graph.&lt;/p&gt;
&lt;h2 id="option-2-managed-cloud-inference-services" &gt;
&lt;div&gt;
&lt;a href="#option-2-managed-cloud-inference-services"&gt;
#
&lt;/a&gt;
Option 2: Managed Cloud Inference Services
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Every major provider has a managed inference product — you bring a model artifact, config, and get an endpoint:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;AWS&lt;/td&gt;
&lt;td&gt;SageMaker Endpoints (incl. async &amp;amp; serverless), Bedrock, EC2 with Deep Learning AMIs&lt;/td&gt;
&lt;td&gt;SageMaker handles serving infra; Bedrock offers hosted foundation models; asynchronous endpoints handle large payloads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Google Cloud&lt;/td&gt;
&lt;td&gt;Vertex AI (Endpoints, Model Garden), GKE Autopilot with GPUs&lt;/td&gt;
&lt;td&gt;Tight integration with BigQuery/Colab and Google&amp;rsquo;s managed Nvidia/TPU fleet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;Azure ML online endpoints, Azure OpenAI&lt;/td&gt;
&lt;td&gt;Azure OpenAI is often the shortest path for enterprise LLM use; Azure ML for custom models&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPU-native clouds&lt;/td&gt;
&lt;td&gt;CoreWeave, RunPod, Lambda, Modal, Anyscale&lt;/td&gt;
&lt;td&gt;Kubernetes-native (CoreWeave) or simpler per-GPU rental; strong price-performance and quick cold-start UX&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The mental model: you give up governing the whole cluster below the model. The platform provides the serving runtime (or a container you supply), GPU instance selection, autoscaling targets, network isolation (VPC endpoints), encryption at rest/in transit, audit trails, and per-invocation pricing modes.&lt;/p&gt;
&lt;p&gt;A typical managed setup is declarative, roughly conceptually:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#e2e4e5;background-color:#282a36;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-yaml" data-lang="yaml"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#78787e"&gt;# Conceptually — each provider uses its own DSL/SDK&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;&lt;span style="color:#ff6ac1"&gt;endpoint&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;name&lt;/span&gt;: llama-8b-prod
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;model&lt;/span&gt;: s3://our-models/llama-8b-finetuned.tar.gz
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;container&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;763104351884.&lt;/span&gt;dkr.ecr.us-east-1.amazonaws.com/djl-inference:vllm
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;instance_type&lt;/span&gt;: ml.g6e.4xlarge
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;autoscaling&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;target_metric&lt;/span&gt;: ModelInvocationsPerInstance
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;target_value&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;50&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;min&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;max&lt;/span&gt;: &lt;span style="color:#ff9f43"&gt;8&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; &lt;span style="color:#ff6ac1"&gt;data_capture&lt;/span&gt;: enabled &lt;span style="color:#78787e"&gt;# for drift monitoring / replay&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;What you gain&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time to first production call measured in hours:&lt;/strong&gt; no GPU operator, no driver pain, no cluster provisioning, no MIG experiments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Built-in autoscaling on provided inference metrics&lt;/strong&gt;, handled by the provider&amp;rsquo;s control plane, with sensible scale-in defaults and warm pool semantics engineered upstream.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Compliance and network posture mostly handled:&lt;/strong&gt; private VPC endpoints, KMS-encrypted model artifacts, IAM auth, audit logs — what would take weeks of K8s policy engineering.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pricing discipline tools:&lt;/strong&gt; serverless variants with per-invocation billing and scale-to-zero handled for you (SageMaker serverless endpoints, Vertex AI predictive); async endpoints for long-running jobs with S3 payloads.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Traffic management and monitoring included:&lt;/strong&gt; canary weights on endpoint versions, dashboards, and drift/capture hooks.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What you pay for&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Per-instance markup&lt;/strong&gt; — typically 1.3–2× raw EC2-equivalent GPU-hour cost, in exchange for the managed control plane.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Limited runtime knobs.&lt;/strong&gt; Batching behavior, continuous batching parameters, KV cache policy, and GPU partitioning are whatever the platform exposes; if the platform&amp;rsquo;s defaults aren&amp;rsquo;t optimal for your traffic shape, you absorb it, or you use custom containers and hope your tuning isn&amp;rsquo;t broken by an endpoint update.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Platform-shaped architectures.&lt;/strong&gt; Your request path routes through the provider&amp;rsquo;s frontend, so p50 latency can add tens of milliseconds; some platforms add queue-before-GPU even for warm endpoints.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lock-in to control plane conventions.&lt;/strong&gt; IAM roles, autoscaling metric names, SDK/DSL, endpoint state model — nontrivial to port to another provider later (though most lock-in is at this config layer, not the model itself).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cost concentration at sustained load.&lt;/strong&gt; Running one managed endpoint 24/7 is close to always paying the premium. Managed serverless endpoints scale smoothly but pass through latency; if your traffic is sustained rather than bursty, the raw-GPU time-based pricing of EC2 (plus your own ops) often wins on cost per million tokens.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="comparing-the-two-paths" &gt;
&lt;div&gt;
&lt;a href="#comparing-the-two-paths"&gt;
#
&lt;/a&gt;
Comparing the Two Paths
&lt;/div&gt;
&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;Self-managed K8s&lt;/th&gt;
&lt;th&gt;Managed cloud&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Time to first production call&lt;/td&gt;
&lt;td&gt;Days to weeks&lt;/td&gt;
&lt;td&gt;Hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost at sustained 24/7 load&lt;/td&gt;
&lt;td&gt;Lower (spot + reserved + fractioning + MIG)&lt;/td&gt;
&lt;td&gt;Higher per-GPU-hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost at spiky/bursty traffic&lt;/td&gt;
&lt;td&gt;Harder (idle GPUs, warm floors)&lt;/td&gt;
&lt;td&gt;Easier (serverless variants)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cold-start behavior&lt;/td&gt;
&lt;td&gt;You engineer it (snapshots, caches, warm pools)&lt;/td&gt;
&lt;td&gt;Platform-managed warm pools&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ops burden&lt;/td&gt;
&lt;td&gt;High — GPU operator, drivers, serving tuning, spot interrupts&lt;/td&gt;
&lt;td&gt;Low — endpoint ops only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Model-level optimization&lt;/td&gt;
&lt;td&gt;Full (quantization, batching, CUDA graphs, speculative decoding)&lt;/td&gt;
&lt;td&gt;Limited to what the platform exposes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Scaling signal&lt;/td&gt;
&lt;td&gt;Custom metrics, high tuning effort&lt;/td&gt;
&lt;td&gt;Platform-provided targets&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vendor lock-in&lt;/td&gt;
&lt;td&gt;Low — cluster and serving code are portable&lt;/td&gt;
&lt;td&gt;Significant — endpoint, IAM, SDK layer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Latency overhead&lt;/td&gt;
&lt;td&gt;Direct pod networking&lt;/td&gt;
&lt;td&gt;Provider frontend hop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Observability&lt;/td&gt;
&lt;td&gt;DIY (Prometheus, DCGM, Grafana)&lt;/td&gt;
&lt;td&gt;Included dashboards&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token-economics visibility&lt;/td&gt;
&lt;td&gt;Complete — you measure everything&lt;/td&gt;
&lt;td&gt;Provider dashboards only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id="a-pragmatic-decision-framework" &gt;
&lt;div&gt;
&lt;a href="#a-pragmatic-decision-framework"&gt;
#
&lt;/a&gt;
A Pragmatic Decision Framework
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Most teams don&amp;rsquo;t need a binary answer — they need a routing rule. Patterns that work in practice:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Start managed, migrate what deserves it.&lt;/strong&gt; Deploy on SageMaker/Vertex/Azure ML endpoints first — validate the model has product fit before investing in its infrastructure. When a model&amp;rsquo;s sustained traffic, special GPU configuration, or per-token economics justify it, lift the serving code to your cluster.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keep self-managed for tuning-sensitive workloads.&lt;/strong&gt; Continuous batching behavior, quantization (AWQ, GPTQ, FP8/FP4), speculative decoding, prefix caching, and KV-cache offloads matter enormously for cost per million tokens. If cost per token is your north-star metric, you want a runtime you control: your own vLLM (or SGLang) on GPUs you own is usually the frontier.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use serverless managed endpoints for hard-to-forecast, low-QPS workloads.&lt;/strong&gt; Internal tools, demos, admin features, geographically rare traffic — scale-to-zero in the cloud avoids keeping a GPU warm for three requests a day, and the latency is acceptable when users expect internal tools to be slower.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Match the traffic shape to the pricing model.&lt;/strong&gt; Sustained 24/7 load → self-managed with spot/reserved and MIG fractioning. Bursty, spiky, unpredictable → managed serverless. Long-running batch inference jobs → async endpoints or spot-backed batch jobs on your own clusters.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Don&amp;rsquo;t fight the GPU supply shortage alone.&lt;/strong&gt; If you can&amp;rsquo;t get capacity in-region from your primary cloud, the GPU-native clouds (CoreWeave is literally K8s under the hood; RunPod/Lambda prioritize availability) are often a supplement — treat them in your multi-cloud strategy as capacity, with the same serving images and IaC.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Beware common pitfalls either way:&lt;/strong&gt; under-provisioning GPU memory for KV cache, forgetting to request/raise GPU service quotas in the target region, model weight download times dominating autoscaling behavior, GPU utilization metrics firing &amp;ldquo;capacity&amp;rdquo; alarms when the GPU is simply efficiently batched, and long-lived streaming connections leaking through ingress misconfigurations.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="conclusion" &gt;
&lt;div&gt;
&lt;a href="#conclusion"&gt;
#
&lt;/a&gt;
Conclusion
&lt;/div&gt;
&lt;/h2&gt;
&lt;p&gt;Inference infrastructure is infrastructure engineering, more than ML engineering. Self-managed Kubernetes gives you control, best unit economics at scale, and deep optimization headroom (quantization, batching, fractioning) — at the price of being responsible for some of the hardest workloads to keep running: GPUs, huge deterministic artifacts, custom autoscaling, interruption engineering. Managed cloud services trade cost per GPU-hour and flexibility for operational simplicity, compliance posture, and speed to production.&lt;/p&gt;
&lt;p&gt;Most mature ML platform teams end up with both: managed endpoints for early-stage and spiky models, and a tuned self-managed GPU fleet on Kubernetes for the workloads where cost per token and tail latency are competitive differentiators. The DevOps answer isn&amp;rsquo;t &amp;ldquo;Kubernetes or managed&amp;rdquo; — it&amp;rsquo;s knowing what your traffic looks like at the token level and using both paths where the tradeoffs serve you.&lt;/p&gt;
&lt;h2 id="further-reading" &gt;
&lt;div&gt;
&lt;a href="#further-reading"&gt;
#
&lt;/a&gt;
Further Reading
&lt;/div&gt;
&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.vllm.ai/" target="_blank" rel="noopener noreferrer"&gt;vLLM documentation&lt;/a&gt; — serving runtime, continuous batching, and Prometheus metrics&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html" target="_blank" rel="noopener noreferrer"&gt;NVIDIA GPU Operator&lt;/a&gt; — drivers, device plugin, DCGM, and MIG management on Kubernetes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://keda.sh/docs/latest/" target="_blank" rel="noopener noreferrer"&gt;KEDA documentation&lt;/a&gt; — event-driven autoscaling on custom metrics&lt;/li&gt;
&lt;li&gt;&lt;a href="https://kserve.github.io/website/latest/" target="_blank" rel="noopener noreferrer"&gt;KServe&lt;/a&gt; — model serving framework with traffic splitting on Kubernetes&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sgl-project.github.io/" target="_blank" rel="noopener noreferrer"&gt;SGLang&lt;/a&gt; — RadixAttention-based serving engine for prefix-heavy workloads&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/deploy-model.html" target="_blank" rel="noopener noreferrer"&gt;SageMaker Inference&lt;/a&gt; — endpoint types, autoscaling, and serverless inference&lt;/li&gt;
&lt;li&gt;&lt;a href="https://cloud.google.com/vertex-ai/docs/predictions/deploy-model-api" target="_blank" rel="noopener noreferrer"&gt;Vertex AI Endpoints&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/azure/machine-learning/how-to-deploy-online-endpoints" target="_blank" rel="noopener noreferrer"&gt;Azure ML online endpoints&lt;/a&gt; — managed inference reference&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>DevOps</category><category>Kubernetes</category><category>Cloud Computing</category><category>AI</category><guid isPermaLink="true">https://geekat.work/posts/ai-inference-infrastructure-devops-kubernetes-vs-managed-cloud/</guid></item></channel></rss>