<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Paged-Attention on lo0 — Tech Blog</title><link>https://blog.lo0.es/en/tags/paged-attention/</link><description>Recent content in Paged-Attention on lo0 — Tech Blog</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 18 May 2026 13:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/en/tags/paged-attention/index.xml" rel="self" type="application/rss+xml"/><item><title>vLLM on Kubernetes: the LLM inference piece that actually scales</title><link>https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/</link><pubDate>Mon, 18 May 2026 13:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/vllm-kubernetes-llm-inference-that-scales/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>vLLM is the inference engine that turns a general-purpose GPU into a productive LLM server. Its value is not in running a model, since any &lt;code>transformers.pipeline&lt;/code> does that in three lines of Python, but in squeezing the GPU down to the last gigabyte and the last cycle: PagedAttention for the KV cache, &lt;em>continuous batching&lt;/em> to mix requests, its own scheduler to share GPU time between sessions. Kubernetes is its natural habitat because vLLM behaves like a modern UNIX process, with a health endpoint, Prometheus metrics, orderly draining and declarable resources, and K8s already knows how to manage those. But there are traps: the standard HPA does not scale vLLM well, the model takes minutes to load, and naive rolling updates cut sessions halfway through decoding. This article takes the engine apart and then fits it, with real manifests, into a cluster that can actually serve it.&lt;/p>
&lt;blockquote>
&lt;p>This article is the natural continuation of &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a>. There we explained why every token consumes VRAM. Here we look at what you do with that VRAM when you want to offer it as a service.&lt;/p>
&lt;/blockquote>
&lt;h2 id="the-analogy-a-multiprocess-kernel-for-your-gpu">The analogy: a multiprocess kernel for your GPU&lt;/h2>
&lt;p>Imagine you have a single processor and need to serve a hundred concurrent processes without any of them blocking the rest. Nobody in their right mind would write a &lt;code>while-true&lt;/code> loop dispatching processes one by one: they would install an operating system. The kernel takes care of scheduling, memory paging, isolation, priorities and cleanup on exit. The &amp;ldquo;process&amp;rdquo; becomes a convenient abstraction and the kernel does the dirty work.&lt;/p>
&lt;p>vLLM is, for your GPU, what the kernel is for your CPU. As far as the GPU is concerned, a conversation with an LLM is a process that lives across many decoding steps, occupies a slice of VRAM (its KV cache) and demands compute time every time a token has to be generated. You have a hundred of those processes at once. You need to:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Share GPU time between them&lt;/strong> without pausing any of them entirely (it would be disastrous if one long conversation monopolised the GPU).&lt;/li>
&lt;li>&lt;strong>Manage memory with paging&lt;/strong> because, just as in RAM, contiguous allocation is inefficient.&lt;/li>
&lt;li>&lt;strong>Queue new requests&lt;/strong> when the GPU is saturated and serve them in a reasonable order.&lt;/li>
&lt;li>&lt;strong>Reclaim resources&lt;/strong> when a session ends.&lt;/li>
&lt;/ul>
&lt;p>PagedAttention is the virtual memory of the KV cache. &lt;em>Continuous batching&lt;/em> is the time-slicing scheduler that shares the GPU token by token. The OpenAI-compatible server is the uniform syscall interface. Calling it a &amp;ldquo;kernel&amp;rdquo; for the GPU is marketing, but it is marketing that captures the idea well.&lt;/p>
&lt;h2 id="what-vllm-does-inside">What vLLM does inside&lt;/h2>
&lt;h3 id="continuous-batching-stop-waiting-for-the-slowest">Continuous batching: stop waiting for the slowest&lt;/h3>
&lt;p>The naive inference engine does &lt;em>static batching&lt;/em>: it groups N requests, processes them until all of them are finished, returns and starts another round. The problem is obvious: if one request asks for 8 tokens and another asks for 800, the other seven wait for the slow one. GPU utilisation collapses.&lt;/p>
&lt;p>&lt;em>Continuous batching&lt;/em> (Yu et al., 2022, popularised by vLLM) changes the model. At every decode step, which produces one token for each active session, the engine composes the batch out of the active tokens of ALL sessions alive at that instant. When a session finishes its generation it releases its slot immediately and another request from the queue takes it. The batch never waits for the slowest session because nobody is blocked: everyone advances at one token per step.&lt;/p>
&lt;p>The original paper measured 5–23× more throughput than the equivalent static batching. The exact number depends on how variable the response lengths are, but the order of magnitude holds up in practice.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Static vs continuous batching">
&lt;style>.title{font:600 13px sans-serif;fill:#222}.lbl{font:11px sans-serif;fill:#444}.s1{fill:#2a9d8f}.s2{fill:#e76f51}.s3{fill:#264653}.s4{fill:#e9c46a}.empty{fill:#eee;stroke:#999;stroke-dasharray:3 2}&lt;/style>
&lt;text x="180" y="20" text-anchor="middle" class="title">Static batching&lt;/text>
&lt;text x="540" y="20" text-anchor="middle" class="title">Continuous batching&lt;/text>
&lt;text x="20" y="55" class="lbl">session 1&lt;/text>
&lt;text x="20" y="80" class="lbl">session 2&lt;/text>
&lt;text x="20" y="105" class="lbl">session 3&lt;/text>
&lt;text x="20" y="130" class="lbl">session 4&lt;/text>
&lt;rect x="70" y="40" width="40" height="20" class="s1"/>
&lt;rect x="70" y="65" width="120" height="20" class="s2"/>
&lt;rect x="70" y="90" width="60" height="20" class="s3"/>
&lt;rect x="70" y="115" width="30" height="20" class="s4"/>
&lt;rect x="110" y="40" width="80" height="20" class="empty"/>
&lt;rect x="130" y="90" width="60" height="20" class="empty"/>
&lt;rect x="100" y="115" width="90" height="20" class="empty"/>
&lt;text x="180" y="160" text-anchor="middle" class="lbl">empty slots wait for session 2&lt;/text>
&lt;rect x="380" y="40" width="40" height="20" class="s1"/>
&lt;rect x="420" y="40" width="80" height="20" class="s3"/>
&lt;rect x="500" y="40" width="40" height="20" class="s4"/>
&lt;rect x="540" y="40" width="40" height="20" class="s1"/>
&lt;rect x="380" y="65" width="120" height="20" class="s2"/>
&lt;rect x="500" y="65" width="40" height="20" class="s3"/>
&lt;rect x="540" y="65" width="80" height="20" class="s4"/>
&lt;rect x="380" y="90" width="60" height="20" class="s3"/>
&lt;rect x="440" y="90" width="50" height="20" class="s2"/>
&lt;rect x="490" y="90" width="40" height="20" class="s4"/>
&lt;rect x="530" y="90" width="100" height="20" class="s1"/>
&lt;rect x="380" y="115" width="30" height="20" class="s4"/>
&lt;rect x="410" y="115" width="80" height="20" class="s2"/>
&lt;rect x="490" y="115" width="60" height="20" class="s3"/>
&lt;rect x="550" y="115" width="80" height="20" class="s1"/>
&lt;text x="540" y="160" text-anchor="middle" class="lbl">slots reassigned token by token&lt;/text>
&lt;line x1="70" y1="190" x2="630" y2="190" stroke="#666"/>
&lt;text x="350" y="210" text-anchor="middle" class="lbl">time →&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>The consequence for the operator is counterintuitive: &lt;strong>a single vLLM replica performs like three naive replicas&lt;/strong>. There is no point adding pods without justifying it with real metrics.&lt;/p>
&lt;h3 id="pagedattention-virtual-memory-for-the-kv-cache">PagedAttention: virtual memory for the KV cache&lt;/h3>
&lt;p>We already flagged it in the KV cache article: the naive engine reserves one contiguous block per session, sized for the &lt;em>worst case&lt;/em> (&lt;code>max_context_len&lt;/code>), and wastes 60–80% of the VRAM because real sessions come nowhere near their ceiling.&lt;/p>
&lt;p>PagedAttention borrows the solution operating systems have been using for half a century: split the VRAM into small blocks (16 tokens in the default implementation) and keep a logical → physical page table per session. A session with 273 tokens of context occupies 18 blocks (not necessarily contiguous), and grows block by block as it generates. The paper measured &amp;lt;4% waste, an order of magnitude better than contiguous allocation, and that translates into 2–4× more aggregate throughput on the same hardware, because more sessions fit at once.&lt;/p>
&lt;p>There is a cost: every attention operation has to be indirected through the page table. But vLLM&amp;rsquo;s CUDA kernels are written so that this indirection is cheap, and the net result is massively positive.&lt;/p>
&lt;h3 id="prefill-vs-decode-two-phases-with-opposite-profiles">Prefill vs decode: two phases with opposite profiles&lt;/h3>
&lt;p>An LLM request has two phases with radically different GPU profiles:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Prefill&lt;/strong>: processes the entire prompt in one go. It is &lt;em>compute-bound&lt;/em>: it uses the tensor cores intensively, the GPU sits above 90%, and it lasts between hundreds of ms and a few seconds depending on prompt size.&lt;/li>
&lt;li>&lt;strong>Decode&lt;/strong>: generates token by token. It is &lt;em>memory-bound&lt;/em>: the compute is modest but the whole KV cache has to be read for every token, and it lasts from tens of ms per token up to minutes for long responses.&lt;/li>
&lt;/ul>
&lt;p>A naive server treats each request as a unit and serves the two phases in series. vLLM decouples them: it mixes requests in prefill with requests in decode in the same step (a technique called &lt;em>chunked prefill&lt;/em> when it also splits long prefills into chunks). The result: the GPU is always busy doing &lt;em>something&lt;/em>, the tensor cores with prefills, the HBM bandwidth with decodes, instead of oscillating between phases.&lt;/p>
&lt;p>Operational implication: the &amp;ldquo;% GPU utilisation&amp;rdquo; metric from &lt;code>nvidia-smi&lt;/code> is misleading. A GPU at 100% doing prefills may have its HBM bandwidth idle. A GPU at 40% doing decodes may have its HBM saturated. For LLM serving, &lt;strong>the useful metric is effective HBM bandwidth&lt;/strong>, not the compute percentage.&lt;/p>
&lt;h3 id="tensor-parallel-when-the-model-does-not-fit-on-one-gpu">Tensor parallel: when the model does not fit on one GPU&lt;/h3>
&lt;p>Llama 3 70B in BF16 is ~140 GB. There is no single GPU on the market that can hold it. The solution is tensor parallel: split each model layer by columns and run the partitions on N GPUs in parallel, synchronising with an &lt;em>all-reduce&lt;/em> after each layer.&lt;/p>
&lt;p>For N=5 GPUs and a 70B model, each GPU sees roughly 28 GB of weights. That sounds fine until you remember that the all-reduce on every layer means reading and writing large tensors between GPUs. If the GPUs share NVLink/NVSwitch (300–900 GB/s), the all-reduce is cheap. If they only share PCIe (~32 GB/s gen4 x16), the all-reduce eats half the time and throughput collapses.&lt;/p>
&lt;p>The implication for K8s, which comes next: the scheduler has to guarantee that the N GPUs are physically close. That translates into NodeAffinity to the right product (&lt;code>NVIDIA-H100-80GB-HBM3&lt;/code>), a single pod with &lt;code>nvidia.com/gpu: N&lt;/code> (not N pods sharing) and, if multi-node is unavoidable, InfiniBand with NCCL as transport.&lt;/p>
&lt;h3 id="the-openai-compatible-server">The OpenAI-compatible server&lt;/h3>
&lt;p>On top of all of the above, vLLM exposes an HTTP server with endpoints identical to OpenAI&amp;rsquo;s: &lt;code>/v1/chat/completions&lt;/code>, &lt;code>/v1/completions&lt;/code>, &lt;code>/v1/embeddings&lt;/code>, &lt;code>/v1/models&lt;/code>. It supports Server-Sent Events streaming. It supports tool calling. It supports logprobs.&lt;/p>
&lt;p>The value of this is enormous and underrated: &lt;strong>any client using the OpenAI SDK works without changes&lt;/strong>. Your application points at &lt;code>https://vllm.your-cluster.local/v1&lt;/code> instead of &lt;code>https://api.openai.com/v1&lt;/code>, and everything else, the LangChain, LlamaIndex, OpenAI Python and OpenAI JS SDKs, just works. It is the main reason vLLM has gained traction over technically comparable alternatives: &lt;strong>it is the boring option that works&lt;/strong>.&lt;/p>
&lt;h2 id="why-kubernetes-is-the-natural-habitat">Why Kubernetes is the natural habitat&lt;/h2>
&lt;p>vLLM is a well-behaved process: it starts, exposes metrics, serves a health endpoint, takes SIGTERM with dignity and declares the resources it needs. Kubernetes has spent ten years perfecting the management of processes like that. The only thing K8s took a while to absorb properly was the GPU, and that is now solved.&lt;/p>
&lt;h3 id="the-gpu-as-a-primitive-resource">The GPU as a primitive resource&lt;/h3>
&lt;p>The plumbing goes like this:&lt;/p>
&lt;ol>
&lt;li>The node has the NVIDIA driver installed (or the GPU Operator installs it).&lt;/li>
&lt;li>A DaemonSet, nvidia-device-plugin, registers the physical GPUs as &lt;code>nvidia.com/gpu&lt;/code> resources with kubelet.&lt;/li>
&lt;li>The Kubernetes scheduler sees those resources the way it sees CPU and memory, puts them in its accounting and assigns them to Pods that ask for them.&lt;/li>
&lt;li>The nvidia-container-toolkit makes sure containerd injects the right devices into the container at startup.&lt;/li>
&lt;/ol>
&lt;p>For the pod, asking for a GPU looks like this:&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">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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nvidia.com/gpu&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">limits&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">nvidia.com/gpu&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;/code>&lt;/pre>&lt;/div>&lt;p>Without MIG, MPS or time-slicing configured, &lt;strong>a GPU is not shared between pods&lt;/strong>: you ask for it whole or you do not ask for it. For vLLM, which wants the entire GPU to itself, that is exactly what you want.&lt;/p>
&lt;h3 id="the-vllm-pod-lifecycle">The vLLM Pod lifecycle&lt;/h3>
&lt;p>Differences from a typical webapp Pod:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Long startup&lt;/strong>. Loading 16 GB of weights into VRAM over the network takes 30 seconds in the best case and 5 minutes in the worst. A &lt;code>readinessProbe&lt;/code> with &lt;code>initialDelaySeconds: 30&lt;/code> and &lt;code>failureThreshold: 3&lt;/code> kills the pod before it starts. Solution: a &lt;code>startupProbe&lt;/code> with a high threshold before the &lt;code>livenessProbe&lt;/code> starts evaluating.&lt;/li>
&lt;li>&lt;strong>Warm-up matters&lt;/strong>. The first prefill compiles CUDA kernels specific to the input shape. The first 2–3 requests are noticeably slower. If latency matters from second one, it is worth firing a warm-up POST after ready.&lt;/li>
&lt;li>&lt;strong>Draining is not instantaneous&lt;/strong>. SIGTERM must not kill in-flight sessions. vLLM, configured with &lt;code>--disable-graceful-shutdown false&lt;/code> (the default), finishes active requests before closing. That can take 30–180 seconds. &lt;code>terminationGracePeriodSeconds&lt;/code> has to accommodate it.&lt;/li>
&lt;li>&lt;strong>Hostile rollouts&lt;/strong>. A naive rolling update (&lt;code>maxUnavailable: 1&lt;/code>) can leave you with no replicas serving if the new one is slow to load. Set &lt;code>maxSurge: 1, maxUnavailable: 0&lt;/code> so the new pod is Ready before the old one is drained.&lt;/li>
&lt;/ul>
&lt;h2 id="anatomy-of-a-serious-deployment">Anatomy of a serious deployment&lt;/h2>
&lt;h3 id="first-of-all-gpu-operator">First of all: GPU Operator&lt;/h3>
&lt;p>Without the GPU Operator (or an equivalent manual installation), a Pod with &lt;code>nvidia.com/gpu: 1&lt;/code> stays Pending forever. What the operator installs as DaemonSets on every GPU node:&lt;/p>
&lt;ul>
&lt;li>&lt;code>nvidia-driver-daemonset&lt;/code> — the kernel-mode driver (if you do not have it installed at host level).&lt;/li>
&lt;li>&lt;code>nvidia-device-plugin-daemonset&lt;/code> — registers the GPUs as a kubelet resource.&lt;/li>
&lt;li>&lt;code>nvidia-container-toolkit-daemonset&lt;/code> — the containerd integration.&lt;/li>
&lt;li>&lt;code>nvidia-dcgm-exporter&lt;/code> — Prometheus metrics for the GPU (utilisation, temperature, ECC errors, memory).&lt;/li>
&lt;li>&lt;code>gpu-feature-discovery&lt;/code> — node labels: &lt;code>nvidia.com/gpu.product&lt;/code>, &lt;code>nvidia.com/gpu.memory&lt;/code>, and so on, essential for NodeAffinity.&lt;/li>
&lt;/ul>
&lt;p>The recommended installation is the official Helm chart. The delicate part is aligning the driver with the host kernel version: if the nodes run kernel 6.x, the operator needs a compatible driver branch.&lt;/p>
&lt;h3 id="a-complete-annotated-vllm-deployment">A complete, annotated vLLM Deployment&lt;/h3>
&lt;p>The following deploys Llama 3 8B with an FP8-quantised KV cache, up to 32K of context, on an RTX 4090. It is the reference manifest; the comments explain the non-obvious decisions.&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">apps/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Deployment&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-llama3-8b&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">inference&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">replicas&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">strategy&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">RollingUpdate&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">rollingUpdate&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">maxSurge&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">maxUnavailable&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># never run out of replicas during the rollout&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">selector&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">matchLabels&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">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama3-8b&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">template&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">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">labels&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">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama3-8b&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.io/scrape&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;true&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.io/port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;8000&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.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">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="c"># Only nodes with the GPU we expect&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">nodeSelector&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">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NVIDIA-GeForce-RTX-4090&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">tolerations&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">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">nvidia.com/gpu&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">operator&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Exists&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"># Pre-download the weights if they are not on the shared PVC&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">initContainers&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">model-download&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">ghcr.io/huggingface/huggingface-cli:latest&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;sh&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;-c&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="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> if [ ! -f /models/llama-3-8b/config.json ]; then
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> huggingface-cli download meta-llama/Meta-Llama-3-8B-Instruct \
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> --local-dir /models/llama-3-8b --local-dir-use-symlinks False
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> fi&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">env&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">HF_TOKEN&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">valueFrom&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">secretKeyRef&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">key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">token&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">volumeMounts&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">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="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/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="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">vllm&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.6.3&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">model=/models/llama-3-8b&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=llama-3-8b&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=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="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">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="l">enable-chunked-prefill&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">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">port=8000&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">ports&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">http&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">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">metrics&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">containerPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># same port as http; /metrics&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>&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;4&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">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">8Gi&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">nvidia.com/gpu&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">limits&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">cpu&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;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">memory&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">16Gi&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">nvidia.com/gpu&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">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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/health&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">failureThreshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">60&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 10 min of grace to load the model&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/health&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">5&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">path&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/health&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&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">20&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">3&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">volumeMounts&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">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="nt">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/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="nt">readOnly&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 class="c"># no process should write here at runtime&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">shm&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/dev/shm &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># vLLM uses shared memory for IPC between workers&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">volumes&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">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="nt">persistentVolumeClaim&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">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">model-cache&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">shm&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">emptyDir&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">medium&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Memory&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">sizeLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">4Gi&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">terminationGracePeriodSeconds&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">120&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># accommodates draining of active sessions&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="nn">---&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">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Service&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-llama3-8b&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">inference&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">selector&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">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama3-8b&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">ports&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">http&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">80&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">targetPort&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8000&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Five things that are not visible on a first read:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>&lt;code>/dev/shm&lt;/code> in memory, 4 GB&lt;/strong>. vLLM launches worker processes (one per GPU in tensor parallel, plus the driver) that communicate over shared memory. Docker&amp;rsquo;s default (64 MB) blows up as soon as the model is mid-sized. Without this, the pod starts but fails the moment it serves the first complex request.&lt;/li>
&lt;li>&lt;strong>&lt;code>--enable-prefix-caching&lt;/code>&lt;/strong>. If the prompts in your workload share structure (a common system prompt, few-shot examples), vLLM reuses the KV cache of the shared part. A free 30–60% gain in TTFT.&lt;/li>
&lt;li>&lt;strong>&lt;code>--gpu-memory-utilization=0.92&lt;/code>&lt;/strong>. vLLM reserves the given percentage of the VRAM for itself. The remaining 8% leaves room for activations, CUDA kernels and the overhead that does not get counted. Lowering it buys safety; raising it above 0.95 invites OOM.&lt;/li>
&lt;li>&lt;strong>A &lt;code>ReadOnlyMany&lt;/code> PVC&lt;/strong> is ideal. The model does not change at runtime. Several pods can mount the same PVC without contention.&lt;/li>
&lt;li>&lt;strong>No &lt;code>livenessProbe&lt;/code> shorter than the &lt;code>terminationGracePeriodSeconds&lt;/code>&lt;/strong>. If a drain takes 90s and liveness kills at 60s, rollouts lose sessions.&lt;/li>
&lt;/ol>
&lt;h3 id="multi-pod-tensor-parallel-leaderworkerset">Multi-pod tensor parallel: LeaderWorkerSet&lt;/h3>
&lt;p>When the model needs more GPUs than a single node has, the pattern is a group of coordinated pods, one per GPU, that behave as a single replica. For years this was modelled with a StatefulSet plus init scripts; since Kubernetes 1.32, the idiomatic primitive is LeaderWorkerSet (LWS):&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">leaderworkerset.x-k8s.io/v1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">LeaderWorkerSet&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-llama3-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">inference&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">replicas&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">leaderWorkerTemplate&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">size&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 1 leader + 4 workers = 5 pods, 5 GPUs&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">restartPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">RecreateGroupOnPodRestart&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">leaderTemplate&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">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">nodeSelector&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">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NVIDIA-H100-80GB-HBM3&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">vllm-leader&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.6.3&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">model=/models/llama-3-70b&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- --&lt;span class="l">tensor-parallel-size=5&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">distributed-executor-backend=ray&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"># ...&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">workerTemplate&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">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">nodeSelector&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">nvidia.com/gpu.product&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">NVIDIA-H100-80GB-HBM3&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">vllm-worker&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.6.3&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 workers join the leader&amp;#39;s Ray cluster&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>LWS guarantees startup order (workers first, leader afterwards) and an atomic lifecycle (if a worker goes down, the whole group restarts, not a single pod). Without it, the coordination is manually fragile.&lt;/p>
&lt;p>A simpler alternative, if all the tensor parallel GPUs fit on a single node (the case with HGX H100 boxes with 8 GPUs and an internal NVSwitch): one single Pod with &lt;code>nvidia.com/gpu: 5&lt;/code>, &lt;code>--tensor-parallel-size=5&lt;/code>, and vLLM handles everything internally. No Ray, no LWS, far simpler. It is the recommended path whenever it is available.&lt;/p>
&lt;h3 id="autoscaling-the-standard-hpa-is-no-use">Autoscaling: the standard HPA is no use&lt;/h3>
&lt;p>An HPA on CPU% is useless for vLLM. The GPU does the work; the pod&amp;rsquo;s CPU sits at 5–10% even under maximum load. Nor is the GPU utilisation percentage from &lt;code>dcgm-exporter&lt;/code> any use: a pod at 100% GPU% with &lt;code>gpu_cache_usage_perc=15%&lt;/code> is serving one long session without being saturated, while a pod at 60% GPU% with &lt;code>gpu_cache_usage_perc=95%&lt;/code> is on the edge of evicting sessions.&lt;/p>
&lt;p>The right metrics are exported by vLLM itself at &lt;code>/metrics&lt;/code> (Prometheus format):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>What it says&lt;/th>
&lt;th>When to scale&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>vllm:num_requests_waiting&lt;/code>&lt;/td>
&lt;td>Requests queued and not yet in the batch.&lt;/td>
&lt;td>If it stays above 5–10.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:num_requests_running&lt;/code>&lt;/td>
&lt;td>Active requests in the batch.&lt;/td>
&lt;td>For capacity planning, not for scaling.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:gpu_cache_usage_perc&lt;/code>&lt;/td>
&lt;td>% of the KV cache occupied.&lt;/td>
&lt;td>If sustained above 80%, there is a risk of preemption.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:time_to_first_token_seconds&lt;/code>&lt;/td>
&lt;td>Prefill latency (histogram).&lt;/td>
&lt;td>If p95 exceeds your SLA.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>vllm:e2e_request_latency_seconds&lt;/code>&lt;/td>
&lt;td>Total latency per request.&lt;/td>
&lt;td>An output metric.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For the HPA to consume them there are two routes: Prometheus Adapter (exposes custom metrics to the K8s API) or KEDA (scales on Prometheus queries directly, far more convenient). With KEDA:&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">keda.sh/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">ScaledObject&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-scaler&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">inference&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">scaleTargetRef&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-llama3-8b&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">minReplicaCount&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">maxReplicaCount&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">8&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">pollingInterval&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">cooldownPeriod&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">120&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 2 min before scale-down (long sessions)&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">triggers&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">prometheus&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">serverAddress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://prometheus.monitoring:9090&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">threshold&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s1">&amp;#39;5&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">query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sum(vllm:num_requests_waiting{app=&amp;#34;vllm-llama3-8b&amp;#34;})&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>The long &lt;code>cooldownPeriod&lt;/code> matters: if you drop replicas while there are sessions decoding, you kill them. Better to leave 2 minutes of slack.&lt;/p>
&lt;h3 id="observability-the-four-metrics-that-matter">Observability: the four metrics that matter&lt;/h3>
&lt;p>Of everything &lt;code>/metrics&lt;/code> exports, a minimal dashboard needs these four:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>TTFT p50/p95&lt;/strong> (time to first token) — what the user perceives when they hit send.&lt;/li>
&lt;li>&lt;strong>TPOT p50/p95&lt;/strong> (time per output token) — the &amp;ldquo;speed&amp;rdquo; of the streaming.&lt;/li>
&lt;li>&lt;strong>Aggregate throughput&lt;/strong> (tokens generated per second across the cluster) — for capacity planning.&lt;/li>
&lt;li>&lt;strong>Queue depth&lt;/strong> (&lt;code>vllm:num_requests_waiting&lt;/code>) — the leading indicator: if it grows, everything is about to degrade.&lt;/li>
&lt;/ol>
&lt;p>To that, add HBM utilisation and free memory per GPU (from &lt;code>dcgm-exporter&lt;/code>) to spot bandwidth saturation and fragmentation problems. A decent Grafana dashboard with those 6 charts anticipates 90% of incidents.&lt;/p>
&lt;h2 id="two-concrete-scenarios">Two concrete scenarios&lt;/h2>
&lt;p>We reuse the same hardware as the previous article for continuity. The same cache arithmetic, now with the engine on top.&lt;/p>
&lt;h3 id="scenario-a--1rtx-4090-workstation-or-small-k8s-node">Scenario A — 1×RTX 4090 (workstation or small K8s node)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Topology&lt;/strong>: 1 Pod, &lt;code>--tensor-parallel-size=1&lt;/code>, 1 GPU, 1 node.&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: up to 8B BF16 (Llama 3 8B, Qwen3 8B, Mistral 7B) or up to 14B in FP8/AWQ.&lt;/li>
&lt;li>&lt;strong>PVC&lt;/strong>: the node&amp;rsquo;s local SSD. The 4090 reads 1 TB/s from HBM; an NVMe SSD at 5 GB/s takes 5 seconds to feed 25 GB of weights into VRAM, negligible next to initialisation.&lt;/li>
&lt;li>&lt;strong>HPA&lt;/strong>: irrelevant inside the 4090 (always 1 vLLM replica per GPU), but useful across nodes: 3 replicas on 3 nodes with a 4090 each, and the K8s Service spreads round-robin.&lt;/li>
&lt;li>&lt;strong>Useful concurrency&lt;/strong>: 4–8 simultaneous sessions with 8K of context, 1–2 with 32K.&lt;/li>
&lt;li>&lt;strong>Natural use case&lt;/strong>: PoC, small teams, departmental environments, edge.&lt;/li>
&lt;/ul>
&lt;p>The manifest above is sized for this scenario. Changing only the model and the args, the same Deployment serves Qwen, Mistral or whatever is next.&lt;/p>
&lt;h3 id="scenario-b--5h100-sxm-cluster-with-nvlinknvswitch">Scenario B — 5×H100 SXM (cluster with NVLink/NVSwitch)&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Topology&lt;/strong>: 1 Pod with &lt;code>nvidia.com/gpu: 5&lt;/code> on an HGX node, &lt;code>--tensor-parallel-size=5&lt;/code>. If the platform does not allow grouping 5 GPUs into a single Pod, a LeaderWorkerSet with 5 pods coordinated by Ray.&lt;/li>
&lt;li>&lt;strong>Model&lt;/strong>: up to 70B BF16 (Llama 3 70B) or up to 200B+ in FP8 with cache quantisation.&lt;/li>
&lt;li>&lt;strong>PVC&lt;/strong>: NVMe attached directly to the node, or fast network storage (Ceph with a 25/100 GbE network, Lustre, GPFS). Loading 140 GB of weights over a slow network takes 5 minutes per start.&lt;/li>
&lt;li>&lt;strong>HPA&lt;/strong>: irrelevant inside the 5-GPU cluster (the 5 are an indivisible unit), but useful for adding whole HGX nodes when the load passes a given threshold. This combines with Cluster Autoscaler if the underlying infrastructure allows it.&lt;/li>
&lt;li>&lt;strong>Useful concurrency&lt;/strong>: 32–128 simultaneous sessions with mid-sized contexts, 4–16 with huge contexts.&lt;/li>
&lt;li>&lt;strong>Natural use case&lt;/strong>: internal corporate service, public exposure with an SLA, multi-tenant.&lt;/li>
&lt;/ul>
&lt;h3 id="a-and-b-side-by-side">A and B, side by side&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Aspect&lt;/th>
&lt;th>A (1×4090)&lt;/th>
&lt;th>B (5×H100 SXM)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pod topology&lt;/td>
&lt;td>1 pod, 1 GPU&lt;/td>
&lt;td>1 pod with 5 GPUs (or an LWS of 5)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Maximum BF16 model&lt;/td>
&lt;td>8 B&lt;/td>
&lt;td>70 B&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT @ 8K context, idle&lt;/td>
&lt;td>~250 ms&lt;/td>
&lt;td>~80 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TPOT, idle&lt;/td>
&lt;td>~30 ms/tok&lt;/td>
&lt;td>~15 ms/tok&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput @ concurrency 16&lt;/td>
&lt;td>~50 tok/s/session&lt;/td>
&lt;td>~200 tok/s/session&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Session drain&lt;/td>
&lt;td>30–60 s&lt;/td>
&lt;td>60–180 s&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Useful autoscaling&lt;/td>
&lt;td>Replicas on peer nodes&lt;/td>
&lt;td>Whole nodes via Cluster Autoscaler&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reasonable multi-tenancy&lt;/td>
&lt;td>Limited: 4–8 sessions&lt;/td>
&lt;td>Comfortable: 32–128 sessions&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Indicative cost (hardware)&lt;/td>
&lt;td>~2 K €&lt;/td>
&lt;td>~250 K € (≈ 125×)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The asymmetry is the same one as in the previous article: 125× more expensive, only ~4× more throughput per session and ~10× more concurrency. What the cluster buys is not proportional; it buys &lt;strong>access to models an order of magnitude larger&lt;/strong> and &lt;strong>latencies low enough for interactive use at scale&lt;/strong>. If your workload is batch or asynchronous agents where latency is not critical, several 4090s come surprisingly close.&lt;/p>
&lt;h2 id="vllm-against-tensorrt-llm-and-sglang">vLLM against TensorRT-LLM and SGLang&lt;/h2>
&lt;p>Honestly, all three are good engines. The choice depends on practical criteria, not technical ones. A decision map, not a benchmark:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterion&lt;/th>
&lt;th>vLLM&lt;/th>
&lt;th>TensorRT-LLM&lt;/th>
&lt;th>SGLang&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Supported hardware&lt;/td>
&lt;td>NVIDIA, AMD ROCm, Intel Gaudi&lt;/td>
&lt;td>NVIDIA only&lt;/td>
&lt;td>NVIDIA, AMD ROCm&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pure latency (TTFT)&lt;/td>
&lt;td>Good&lt;/td>
&lt;td>&lt;strong>Best&lt;/strong>: kernels compiled for the exact hardware&lt;/td>
&lt;td>Good&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Aggregate throughput&lt;/td>
&lt;td>Excellent&lt;/td>
&lt;td>Excellent&lt;/td>
&lt;td>Excellent (RadixAttention)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Deployment&lt;/td>
&lt;td>&lt;strong>Trivial&lt;/strong>: Docker image + args&lt;/td>
&lt;td>Complex: build an engine per model and per GPU&lt;/td>
&lt;td>Moderate&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>OpenAI-compatible API&lt;/td>
&lt;td>&lt;strong>Native, complete&lt;/strong>&lt;/td>
&lt;td>Yes, through Triton Inference Server&lt;/td>
&lt;td>Yes&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Support for new models&lt;/td>
&lt;td>&lt;strong>Days after release&lt;/strong>&lt;/td>
&lt;td>Weeks (recompile the engine)&lt;/td>
&lt;td>Days&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quantization&lt;/td>
&lt;td>AWQ, GPTQ, FP8 cache&lt;/td>
&lt;td>INT4/INT8/FP8, very mature&lt;/td>
&lt;td>AWQ, FP8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Multi-modal&lt;/td>
&lt;td>Yes (Llava, Pixtral, Qwen-VL)&lt;/td>
&lt;td>Yes&lt;/td>
&lt;td>Excellent, a priority&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Function calling / tool use&lt;/td>
&lt;td>Good&lt;/td>
&lt;td>Limited&lt;/td>
&lt;td>&lt;strong>First class&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Community / release cadence&lt;/td>
&lt;td>Very active, weekly&lt;/td>
&lt;td>Active, NVIDIA-driven&lt;/td>
&lt;td>Very active, academic&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Licence&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;td>Apache 2.0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>When to choose each one&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>
&lt;p>&lt;strong>vLLM&lt;/strong>: the &amp;ldquo;boring choice&amp;rdquo; that works. The path of least friction to production. If your team has no dedicated inference serving specialist, this one. It supports varied hardware, up-to-date models, a stable API and a huge community.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>TensorRT-LLM&lt;/strong>: when per-request latency is the only metric that matters and your model is stable (trained in-house, not swapped every fortnight). The price of the performance is that every model plus every GPU plus every TRT version requires an engine rebuild, and that blocks fast iteration.&lt;/p>
&lt;/li>
&lt;li>
&lt;p>&lt;strong>SGLang&lt;/strong>: for agent-dominated workloads (heavy tool calling) or complex multi-modal. Its RadixAttention, structural caching of prompts with shared prefixes, shines in ReAct-style patterns where the same system prompt is repeated thousands of times.&lt;/p>
&lt;/li>
&lt;/ul>
&lt;p>For most teams starting out with on-prem LLM serving, &lt;strong>vLLM is the right answer until you have production data pushing you elsewhere&lt;/strong>.&lt;/p>
&lt;h2 id="common-operational-traps">Common operational traps&lt;/h2>
&lt;p>A list of gotchas that come up again and again:&lt;/p>
&lt;h3 id="the-model-is-downloaded-on-every-rolling-update">The model is downloaded on every rolling update&lt;/h3>
&lt;p>&lt;strong>Symptom&lt;/strong>: every deploy takes 5+ minutes to become available.
&lt;strong>Cause&lt;/strong>: there is no shared PVC. Every new pod downloads the model from Hugging Face from scratch.
&lt;strong>Fix&lt;/strong>: a ReadOnlyMany PVC on fast storage, or a local mirror of the registry (a Pod with &lt;code>huggingface-cli&lt;/code> serving a directory over HTTP). In CI/CD, hydrating the PVC before the rollout is 1 line of bash.&lt;/p>
&lt;h3 id="short-readiness-timeouts-that-kill-pods-while-they-load">Short readiness timeouts that kill pods while they load&lt;/h3>
&lt;p>&lt;strong>Symptom&lt;/strong>: new pods go into &lt;code>CrashLoopBackOff&lt;/code> during the first model load.
&lt;strong>Cause&lt;/strong>: a &lt;code>readinessProbe&lt;/code> with too low a timeout fires before vLLM finishes loading; the &lt;code>livenessProbe&lt;/code> finishes the job.
&lt;strong>Fix&lt;/strong>: a &lt;code>startupProbe&lt;/code> with &lt;code>failureThreshold: 60&lt;/code> or more (10 minutes of grace) before liveness starts evaluating.&lt;/p>
&lt;h3 id="an-unquantised-kv-cache-then-oom">An unquantised KV cache, then OOM&lt;/h3>
&lt;p>&lt;strong>Symptom&lt;/strong>: the pod starts fine, serves for five minutes, then gets OOMKilled when session number five arrives with a long context.
&lt;strong>Cause&lt;/strong>: a BF16 KV cache (the default) consumes twice what FP8 does.
&lt;strong>Fix&lt;/strong>: &lt;code>--kv-cache-dtype=fp8&lt;/code>. Negligible quality loss in the vast majority of cases, double the capacity.&lt;/p>
&lt;h3 id="confusing-replicas-with-concurrency">Confusing replicas with concurrency&lt;/h3>
&lt;p>&lt;strong>Symptom&lt;/strong>: the HPA scales to 8 replicas under light real load and the cloud bill goes up. Latency does not improve.
&lt;strong>Cause&lt;/strong>: someone configured &lt;code>targetAverageUtilization: 50%&lt;/code> on CPU, thinking that is &amp;ldquo;load&amp;rdquo;. The reality: a single vLLM replica serves dozens of simultaneous sessions.
&lt;strong>Fix&lt;/strong>: an HPA on &lt;code>vllm:num_requests_waiting&lt;/code>. If the queue is empty, one replica is enough even if the GPU is at 90%.&lt;/p>
&lt;h3 id="tensor-parallel-on-gpus-without-nvlink">Tensor parallel on GPUs without NVLink&lt;/h3>
&lt;p>&lt;strong>Symptom&lt;/strong>: throughput 3× worse than expected, GPUs at 30%, a lot of PCIe traffic.
&lt;strong>Cause&lt;/strong>: &lt;code>tensor_parallel=4&lt;/code> on 4 GPUs connected only by PCIe; the all-reduce saturates the bus on every layer.
&lt;strong>Fix&lt;/strong>: either the GPUs share NVLink/NVSwitch (SXM/HGX models), or you use pipeline parallel (worse latency but less all-reduce), or you reduce TP and accept that the whole model does not fit.&lt;/p>
&lt;h3 id="sessions-cut-off-during-a-rolling-update">Sessions cut off during a rolling update&lt;/h3>
&lt;p>&lt;strong>Symptom&lt;/strong>: users see truncated responses during the deploy.
&lt;strong>Cause&lt;/strong>: &lt;code>terminationGracePeriodSeconds: 30&lt;/code> (the default) is not enough to drain long generations.
&lt;strong>Fix&lt;/strong>: &lt;code>terminationGracePeriodSeconds: 120–180&lt;/code>. Combined with &lt;code>maxUnavailable: 0&lt;/code>, rollouts are invisible to active users.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>vLLM with hot LoRA adapters&lt;/strong>: serving a base model plus N tenant-specific adapters without reloading weights.&lt;/li>
&lt;li>&lt;strong>Disaggregated serving&lt;/strong>: separating prefill and decode into specialised pods, each optimised for its GPU profile.&lt;/li>
&lt;li>&lt;strong>Quantization deep-dive&lt;/strong>: AWQ vs GPTQ vs dynamic FP8 vs FP4, the real trade-offs, and when to use each.&lt;/li>
&lt;li>&lt;strong>Gateway API + AI Inference Extensions&lt;/strong>: the sigwg proposal to make LLMs first-class citizens in K8s (routing by model, sticky sessions per conversation, multi-tenant fairness).&lt;/li>
&lt;li>&lt;strong>Multi-modal serving&lt;/strong>: the same runtime, a different kind of request — images, audio, embeddings.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/llm-serving-engines-pareto-frontier-comparison/">Comparing serving engines (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — how vLLM behaves against SGLang and TRT-LLM on the goodput-latency Pareto frontier: the data that justifies (or questions) the choice of vLLM on Kubernetes.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/cloud-gpu-on-demand-reserved-spot-neoclouds-2026-prices/">Cloud GPU: comparing prices, commitment and sovereign neoclouds&lt;/a> — if the Kubernetes cluster is not on-premise but cloud, here are the spot and reserved prices of the European neoclouds where you would run these manifests.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Kwon et al., &lt;a href="https://arxiv.org/abs/2309.06180">&lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em>&lt;/a> (SOSP 2023) — the original vLLM paper.&lt;/li>
&lt;li>Yu et al., &lt;a href="https://www.usenix.org/conference/osdi22/presentation/yu">&lt;em>Orca: A Distributed Serving System for Transformer-Based Generative Models&lt;/em>&lt;/a> (OSDI 2022) — the paper that popularised &lt;em>continuous batching&lt;/em>.&lt;/li>
&lt;li>&lt;a href="https://docs.vllm.ai/">Official vLLM documentation&lt;/a> — operational and well maintained.&lt;/li>
&lt;li>&lt;a href="https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/">NVIDIA GPU Operator&lt;/a> — installation and troubleshooting of the GPU layer on Kubernetes.&lt;/li>
&lt;li>&lt;a href="https://kubernetes.io/blog/2024/04/16/introducing-leaderworkerset/">LeaderWorkerSet&lt;/a> — the primitive for coordinated workloads such as multi-pod tensor parallel.&lt;/li>
&lt;li>&lt;a href="https://keda.sh/">KEDA&lt;/a> — event-driven autoscaling, ideal for scaling on queue metrics.&lt;/li>
&lt;li>&lt;a href="https://github.com/NVIDIA/TensorRT-LLM">TensorRT-LLM&lt;/a> and &lt;a href="https://github.com/sgl-project/sglang">SGLang&lt;/a> — the two most serious comparables.&lt;/li>
&lt;li>&lt;a href="https://lmsys.org/">LMSYS Chatbot Arena&lt;/a> — periodic benchmarks comparing the three engines.&lt;/li>
&lt;li>Previous article on this blog: &lt;a href="https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/">KV cache: the working memory that holds up LLM inference&lt;/a>.&lt;/li>
&lt;/ul></description></item><item><title>KV cache: the working memory that holds up LLM inference</title><link>https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/</link><pubDate>Mon, 18 May 2026 10:00:00 +0200</pubDate><guid>https://blog.lo0.es/en/posts/kv-cache-working-memory-llm-inference/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>The KV cache is the &lt;strong>working memory&lt;/strong> a language model keeps during a conversation. Without it, every new token would force a recomputation of the whole conversation from the start, at a cost that is &lt;strong>quadratic&lt;/strong> in the length of the text. With it, the cost is linear, but in exchange the cache &lt;strong>lives in VRAM and grows with every token&lt;/strong>. In practice, it is not the model that limits how much context you can serve: it is the KV cache. On an RTX 4090 running Llama 3 8B, the model fits in 16 GB and barely leaves room for ~64 K tokens of cache in total, adding up all the simultaneous sessions. Understanding that number is the difference between promising a client &amp;ldquo;128 K of context&amp;rdquo; and actually delivering it.&lt;/p>
&lt;h2 id="you-are-here-deploy">You are here: Deploy&lt;/h2>
&lt;p>This post opens the series on LLM inference fundamentals. Within the &lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">six-stage LLMOps pipeline&lt;/a> that articulates the whole system, the KV cache lives in the &lt;strong>Deploy&lt;/strong> stage: it is the piece that dictates how much traffic fits in your inference engine and, therefore, how much platform you can offer on top of it.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="you are here: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#kvm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#kvm)}&lt;/style>
&lt;defs>&lt;marker id="kvm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">You are here: DEPLOY · KV cache as the VRAM bottleneck&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="the-analogy-the-speaker-with-amnesia">The analogy: the speaker with amnesia&lt;/h2>
&lt;p>Imagine you are at a two-hour technical conference. Every time the speaker is about to say a new sentence, he &lt;strong>mentally rewinds the whole talk from the beginning&lt;/strong>, reassembles the thread, and only then carries on. His next sentence requires recalling the previous one; the one after that, the two before it; an hour in, every new word costs him an hour of recap. A conference like that would be materially impossible.&lt;/p>
&lt;p>Now imagine the same speaker with a notebook in which he jots down, as he goes, the two or three key ideas of each sentence: subject, object, link to what came before. Before each new sentence he glances at the notebook and continues. His next word only costs a glance at the notebook, not a rewind of the entire talk.&lt;/p>
&lt;p>That notebook, in a transformer, is called the &lt;strong>KV cache&lt;/strong>. Without it, conversational language models would be unworkable. With it, they are commercial products. But the notebook &lt;strong>weighs something&lt;/strong>, and understanding how much, where and why is what separates an inference infrastructure that works from one that falls over at the third concurrent client.&lt;/p>
&lt;h2 id="the-mechanism-itself-in-plain-terms">The mechanism itself (in plain terms)&lt;/h2>
&lt;p>A transformer generates text &lt;strong>one token at a time&lt;/strong>. To decide the next token, the model applies a mechanism called &lt;strong>attention&lt;/strong> over all previous tokens: it asks &amp;ldquo;which parts of the earlier context are relevant for predicting what comes next?&amp;rdquo;.&lt;/p>
&lt;p>Internally, each input token is projected into three vectors:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Q&lt;/strong> (Query): &amp;ldquo;what I am looking for&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>K&lt;/strong> (Key): &amp;ldquo;what this token offers&amp;rdquo;&lt;/li>
&lt;li>&lt;strong>V&lt;/strong> (Value): &amp;ldquo;what information this token carries&amp;rdquo;&lt;/li>
&lt;/ul>
&lt;p>The attention of the current token against the context is computed by multiplying its &lt;strong>Q&lt;/strong> against the &lt;strong>K&lt;/strong> of all previous tokens, normalising with softmax, and weighting the corresponding &lt;strong>V&lt;/strong>. The result is a contextualised representation of the current token.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 260" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Diagram of the attention computation with Q, K, V">
&lt;style>
.box { fill: #f4f4f4; stroke: #333; stroke-width: 1.5; }
.box-q { fill: #ffe9d6; }
.box-k { fill: #d6eaff; }
.box-v { fill: #d9f5d6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.arr { stroke: #444; stroke-width: 1.4; fill: none; marker-end: url(#ah); }
&lt;/style>
&lt;defs>
&lt;marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
&lt;path d="M0,0 L10,5 L0,10 z" fill="#444"/>
&lt;/marker>
&lt;/defs>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Attention computation for token N&lt;/text>
&lt;rect x="40" y="60" width="120" height="40" rx="6" class="box box-q"/>
&lt;text x="100" y="85" text-anchor="middle" class="lbl">Q (token N)&lt;/text>
&lt;text x="100" y="115" text-anchor="middle" class="lbl-sm">"what I seek"&lt;/text>
&lt;rect x="280" y="60" width="160" height="40" rx="6" class="box box-k"/>
&lt;text x="360" y="85" text-anchor="middle" class="lbl">K (tokens 1..N)&lt;/text>
&lt;text x="360" y="115" text-anchor="middle" class="lbl-sm">from the cache&lt;/text>
&lt;rect x="560" y="60" width="120" height="40" rx="6" class="box box-v"/>
&lt;text x="620" y="85" text-anchor="middle" class="lbl">V (tokens 1..N)&lt;/text>
&lt;text x="620" y="115" text-anchor="middle" class="lbl-sm">from the cache&lt;/text>
&lt;path class="arr" d="M160,80 L280,80"/>
&lt;path class="arr" d="M440,80 L560,80"/>
&lt;p>&lt;text x="220" y="74" text-anchor="middle" class="lbl-sm">Q·Kᵀ → softmax&lt;/text>
&lt;text x="500" y="74" text-anchor="middle" class="lbl-sm">× V&lt;/text>&lt;/p>
&lt;rect x="240" y="170" width="240" height="44" rx="6" class="box"/>
&lt;text x="360" y="197" text-anchor="middle" class="lbl">representation of token N&lt;/text>
&lt;path class="arr" d="M620,100 C620,150 480,150 480,170"/>
&lt;path class="arr" d="M100,100 C100,150 240,150 240,170"/>
&lt;/svg>
&lt;/div>
&lt;p>Here is the key point: to predict token N, I only need the &lt;strong>new Q&lt;/strong> (that of token N) and the &lt;strong>K, V of all previous tokens&lt;/strong>. The K and V of tokens 1..N-1 have not changed since the previous iteration. Recomputing them would be throwing work away.&lt;/p>
&lt;p>&lt;strong>The KV cache is exactly that: the memory that holds the K and V of every token already processed, in every layer of the model, so they never have to be recomputed.&lt;/strong>&lt;/p>
&lt;h2 id="why-it-exists-the-quadratic-cost-without-it">Why it exists: the quadratic cost without it&lt;/h2>
&lt;p>Generating a text of N tokens involves N steps. At step &lt;code>i&lt;/code>, attention is computed over &lt;code>i&lt;/code> previous tokens. Without a cache, at every step you recompute the K, V of the &lt;code>i-1&lt;/code> previous tokens &lt;strong>plus&lt;/strong> those of the new one. The total count of attention computations grows as:&lt;/p>
$$\sum_{i=1}^{N} i = \frac{N(N+1)}{2} \approx \frac{N^2}{2}$$
&lt;p>With a KV cache, you only process the new token at each step: cost &lt;strong>linear in N&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison of linear vs quadratic cost">
&lt;style>
.kv-ax { stroke: currentColor; stroke-width: 1.5; opacity: 0.4; fill: none; }
.kv-grid { stroke: currentColor; stroke-width: 1; stroke-dasharray: 4,4; opacity: 0.15; fill: none; }
.kv-lin { stroke: #2a9d8f; stroke-width: 3; fill: none; }
.kv-quad { stroke: #e76f51; stroke-width: 3; fill: none; }
.kv-lbl { font: 600 13px sans-serif; fill: currentColor; }
.kv-sm { font: 11px sans-serif; fill: currentColor; opacity: 0.6; }
.kv-note { font: italic 10px sans-serif; fill: currentColor; opacity: 0.45; }
.kv-tlin { fill: #2a9d8f; font: 700 12px sans-serif; }
.kv-tqud { fill: #e76f51; font: 700 12px sans-serif; }
&lt;/style>
&lt;!-- Title -->
&lt;p>&lt;text x="380" y="22" text-anchor="middle" class="kv-lbl">Cumulative compute to generate N tokens&lt;/text>
&lt;text x="380" y="38" text-anchor="middle" class="kv-note">(schematic scale — exact data is in the table)&lt;/text>&lt;/p>
&lt;!-- Axes -->
&lt;line class="kv-ax" x1="80" y1="260" x2="680" y2="260"/>
&lt;line class="kv-ax" x1="80" y1="55" x2="80" y2="260"/>
&lt;!-- Axis labels -->
&lt;p>&lt;text x="380" y="295" text-anchor="middle" class="kv-sm">tokens generated (N)&lt;/text>
&lt;text x="25" y="158" text-anchor="middle" class="kv-sm" transform="rotate(-90 25 158)">attention operations&lt;/text>&lt;/p>
&lt;!-- X ticks -->
&lt;p>&lt;text x="80" y="277" text-anchor="middle" class="kv-sm">0&lt;/text>
&lt;text x="230" y="277" text-anchor="middle" class="kv-sm">1K&lt;/text>
&lt;text x="380" y="277" text-anchor="middle" class="kv-sm">2K&lt;/text>
&lt;text x="530" y="277" text-anchor="middle" class="kv-sm">3K&lt;/text>
&lt;text x="680" y="277" text-anchor="middle" class="kv-sm">4K&lt;/text>&lt;/p>
&lt;!-- Grid -->
&lt;line class="kv-grid" x1="80" y1="210" x2="680" y2="210"/>
&lt;line class="kv-grid" x1="80" y1="160" x2="680" y2="160"/>
&lt;line class="kv-grid" x1="80" y1="110" x2="680" y2="110"/>
&lt;line class="kv-grid" x1="80" y1="60" x2="680" y2="60"/>
&lt;!-- Linear curve (with KV cache): O(N) — gentle, visible slope -->
&lt;path class="kv-lin" d="M80,260 L680,215"/>
&lt;!-- Quadratic curve (without KV cache): O(N²) — starts flat, rises fast -->
&lt;path class="kv-quad" d="M80,260 C250,258 420,200 680,58"/>
&lt;!-- Curve labels -->
&lt;p>&lt;text x="570" y="208" class="kv-tlin">with KV cache&lt;/text>
&lt;text x="570" y="222" class="kv-tlin">O(N) — linear&lt;/text>&lt;/p>
&lt;p>&lt;text x="440" y="95" class="kv-tqud">without KV cache&lt;/text>
&lt;text x="440" y="109" class="kv-tqud">O(N²) — quadratic&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>The concrete numbers are devastating:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Tokens generated&lt;/th>
&lt;th style="text-align:right">Without KV cache (operations)&lt;/th>
&lt;th style="text-align:right">With KV cache&lt;/th>
&lt;th style="text-align:right">Ratio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">8,256&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">64×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">1,024&lt;/td>
&lt;td style="text-align:right">524,800&lt;/td>
&lt;td style="text-align:right">1,024&lt;/td>
&lt;td style="text-align:right">512×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4,096&lt;/td>
&lt;td style="text-align:right">8,390,656&lt;/td>
&lt;td style="text-align:right">4,096&lt;/td>
&lt;td style="text-align:right">2,048×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">32,768&lt;/td>
&lt;td style="text-align:right">536,887,296&lt;/td>
&lt;td style="text-align:right">32,768&lt;/td>
&lt;td style="text-align:right">16,384×&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>At 32 K tokens, &lt;strong>the cache saves you four orders of magnitude&lt;/strong> of compute. This is not an optimisation: it is what makes conversational inference possible.&lt;/p>
&lt;h2 id="the-price-how-heavy-the-backpack-is">The price: how heavy the backpack is&lt;/h2>
&lt;p>The KV cache is paid for in VRAM. The formula, per &lt;strong>sequence&lt;/strong>, is:&lt;/p>
&lt;pre tabindex="0">&lt;code>KV_size = 2 · n_layers · n_kv_heads · head_dim · context_len · bytes_per_param
↑
K and V
&lt;/code>&lt;/pre>&lt;p>Per &lt;strong>token&lt;/strong> (dropping &lt;code>context_len&lt;/code>), it is a constant of the model itself. Let us look at real numbers:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Model&lt;/th>
&lt;th style="text-align:right">n_layers&lt;/th>
&lt;th style="text-align:right">n_kv_heads&lt;/th>
&lt;th style="text-align:right">head_dim&lt;/th>
&lt;th style="text-align:right">Bytes/token (BF16)&lt;/th>
&lt;th style="text-align:right">GB at 8 K ctx&lt;/th>
&lt;th style="text-align:right">GB at 32 K&lt;/th>
&lt;th style="text-align:right">GB at 128 K&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 3 8B (hypothetical MHA)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">524,288&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;td style="text-align:right">64.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Llama 3 8B (real GQA)&lt;/strong>&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131,072&lt;/td>
&lt;td style="text-align:right">&lt;strong>1.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>4.00&lt;/strong>&lt;/td>
&lt;td style="text-align:right">&lt;strong>16.00&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 3 70B (GQA)&lt;/td>
&lt;td style="text-align:right">80&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">327,680&lt;/td>
&lt;td style="text-align:right">2.50&lt;/td>
&lt;td style="text-align:right">10.00&lt;/td>
&lt;td style="text-align:right">40.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qwen3 8B (GQA)&lt;/td>
&lt;td style="text-align:right">36&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">147,456&lt;/td>
&lt;td style="text-align:right">1.12&lt;/td>
&lt;td style="text-align:right">4.50&lt;/td>
&lt;td style="text-align:right">18.00&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mistral 7B (GQA)&lt;/td>
&lt;td style="text-align:right">32&lt;/td>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">128&lt;/td>
&lt;td style="text-align:right">131,072&lt;/td>
&lt;td style="text-align:right">1.00&lt;/td>
&lt;td style="text-align:right">4.00&lt;/td>
&lt;td style="text-align:right">16.00&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Two immediate readings:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Without GQA there is no 128 K worth talking about.&lt;/strong> A Llama 3 8B with classic multi-head attention would need 64 GB of KV cache alone for a single sequence with 128 K tokens. In other words, &lt;strong>it does not fit on any consumer GPU&lt;/strong>. That is why Meta, Mistral and the rest adopted Grouped Query Attention.&lt;/li>
&lt;li>&lt;strong>The KV cache can be larger than the model.&lt;/strong> Llama 3 8B in BF16 takes ~16 GB. With 128 K of context, its cache is another 16 GB. A single session ties with the model in VRAM.&lt;/li>
&lt;/ol>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Growth of the KV cache with context length">
&lt;style>
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.l8b { stroke: #2a9d8f; stroke-width: 2.5; fill: none; }
.l70b { stroke: #e76f51; stroke-width: 2.5; fill: none; }
.lq8 { stroke: #6a4c93; stroke-width: 2.5; fill: none; stroke-dasharray: 5,3; }
.lim { stroke: #c1121f; stroke-width: 1.5; stroke-dasharray: 4,4; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #555; }
.tag { font: 600 11px sans-serif; }
&lt;/style>
&lt;text x="360" y="20" text-anchor="middle" class="lbl">KV cache (GB) vs context length (1 sequence, BF16)&lt;/text>
&lt;line class="ax" x1="80" y1="240" x2="680" y2="240"/>
&lt;line class="ax" x1="80" y1="40" x2="80" y2="240"/>
&lt;line class="grid" x1="80" y1="190" x2="680" y2="190"/>
&lt;line class="grid" x1="80" y1="140" x2="680" y2="140"/>
&lt;line class="grid" x1="80" y1="90" x2="680" y2="90"/>
&lt;p>&lt;text x="75" y="244" text-anchor="end" class="lbl-sm">0&lt;/text>
&lt;text x="75" y="194" text-anchor="end" class="lbl-sm">10&lt;/text>
&lt;text x="75" y="144" text-anchor="end" class="lbl-sm">20&lt;/text>
&lt;text x="75" y="94" text-anchor="end" class="lbl-sm">30&lt;/text>
&lt;text x="75" y="44" text-anchor="end" class="lbl-sm">40 GB&lt;/text>&lt;/p>
&lt;p>&lt;text x="80" y="258" text-anchor="middle" class="lbl-sm">0&lt;/text>
&lt;text x="180" y="258" text-anchor="middle" class="lbl-sm">8K&lt;/text>
&lt;text x="305" y="258" text-anchor="middle" class="lbl-sm">32K&lt;/text>
&lt;text x="430" y="258" text-anchor="middle" class="lbl-sm">64K&lt;/text>
&lt;text x="680" y="258" text-anchor="middle" class="lbl-sm">128K&lt;/text>&lt;/p>
&lt;!-- Available VRAM limit on an RTX 4090 (~8 GB free after the model) -->
&lt;line class="lim" x1="80" y1="200" x2="680" y2="200"/>
&lt;text x="680" y="196" text-anchor="end" class="tag" fill="#c1121f">≈ VRAM free after loading an 8B on a 4090&lt;/text>
&lt;!-- Llama 3 8B GQA: linear, 1 GB @8K, 16 GB @128K -->
&lt;path class="l8b" d="M80,240 L180,235 L305,220 L430,200 L680,160"/>
&lt;!-- Qwen3 8B GQA -->
&lt;path class="lq8" d="M80,240 L180,234 L305,217 L430,194 L680,150"/>
&lt;!-- Llama 3 70B GQA -->
&lt;path class="l70b" d="M80,240 L180,228 L305,190 L430,140 L680,40"/>
&lt;p>&lt;text x="690" y="160" class="tag" fill="#2a9d8f">Llama 3 8B&lt;/text>
&lt;text x="690" y="148" class="tag" fill="#6a4c93">Qwen3 8B&lt;/text>
&lt;text x="690" y="42" class="tag" fill="#e76f51">Llama 3 70B&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;p>The dashed red line marks the realistic VRAM available on an RTX 4090 after loading the model. &lt;strong>Any model whose curve crosses that line will not be able to serve that context&lt;/strong> without additional strategies: cache quantisation, offload, partitioning.&lt;/p>
&lt;h2 id="inference-is-memory-bound-not-compute-bound">Inference is memory-bound, not compute-bound&lt;/h2>
&lt;p>There is a common misconception: thinking that &amp;ldquo;fast GPU = fast inference&amp;rdquo;. In the regime where inference services with a KV cache actually operate, &lt;strong>what gets measured is memory bandwidth&lt;/strong>. Every new token requires reading the K and V of all previous tokens from HBM. The compute is modest; the data movement is massive.&lt;/p>
&lt;p>That is why an H100 SXM (3.35 TB/s of HBM3) can be 2-3× faster than an A100 (1.55-2 TB/s) &lt;strong>without the clock frequency or the core count fully explaining the difference&lt;/strong>. Bandwidth explains it.&lt;/p>
&lt;p>And that is also why offers of &amp;ldquo;cheap GPUs with plenty of VRAM but slow HBM&amp;rdquo; (some variants with GDDR6 or LPDDR5) disappoint in inference with long contexts: they have room to store the cache but take forever to re-read it.&lt;/p>
&lt;h2 id="tricks-to-make-the-notebook-thinner">Tricks to make the notebook thinner&lt;/h2>
&lt;p>Three techniques, in chronological order, have progressively flattened the size of the KV cache:&lt;/p>
&lt;p>&lt;strong>Multi-Head Attention (MHA).&lt;/strong> The original transformer design (Vaswani et al., 2017). Every attention head has its own K and V. Expensive in cache but theoretically maximal in expressiveness. It is what models had until around 2023.&lt;/p>
&lt;p>&lt;strong>Multi-Query Attention (MQA).&lt;/strong> A single K and V shared by all heads. It reduces the cache by a factor of &lt;code>n_heads&lt;/code>. It works reasonably well but degrades generation quality on some benchmarks.&lt;/p>
&lt;p>&lt;strong>Grouped Query Attention (GQA).&lt;/strong> The middle ground that won. Heads are grouped: in Llama 3 8B, 32 query heads share K, V in groups of 4, giving 8 KV groups. It cuts the cache 4× relative to MHA with almost identical quality. It has been the de facto standard since 2024.&lt;/p>
&lt;p>&lt;strong>Multi-Head Latent Attention (MLA).&lt;/strong> DeepSeek-V2/V3&amp;rsquo;s innovation: instead of storing K, V per head, it compresses the state into a smaller latent vector and projects out to K, V on the fly. The cache can come down to 70 bytes/token, two orders of magnitude less than GQA. It is the main reason DeepSeek-V3 (671 B parameters, 37 B active) is servable on affordable infrastructure.&lt;/p>
&lt;div class="diagram" style="max-width: 640px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 640 200" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="KV cache reduction by technique">
&lt;style>
.bar { stroke: #333; stroke-width: 1; }
.b-mha { fill: #e76f51; }
.b-gqa { fill: #f4a261; }
.b-mqa { fill: #e9c46a; }
.b-mla { fill: #2a9d8f; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm{ font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="320" y="20" text-anchor="middle" class="lbl">KB of cache per token (Llama 3 8B equivalent, BF16)&lt;/text>
&lt;rect x="200" y="40" width="380" height="22" class="bar b-mha"/>
&lt;text x="170" y="56" text-anchor="end" class="lbl-sm">MHA (32 KV heads)&lt;/text>
&lt;text x="595" y="56" class="lbl-sm">512 KB&lt;/text>
&lt;rect x="200" y="76" width="95" height="22" class="bar b-gqa"/>
&lt;text x="170" y="92" text-anchor="end" class="lbl-sm">GQA (8 KV heads)&lt;/text>
&lt;text x="310" y="92" class="lbl-sm">128 KB&lt;/text>
&lt;rect x="200" y="112" width="12" height="22" class="bar b-mqa"/>
&lt;text x="170" y="128" text-anchor="end" class="lbl-sm">MQA (1 KV head)&lt;/text>
&lt;text x="225" y="128" class="lbl-sm">16 KB&lt;/text>
&lt;rect x="200" y="148" width="3" height="22" class="bar b-mla"/>
&lt;text x="170" y="164" text-anchor="end" class="lbl-sm">MLA (DeepSeek-V3)&lt;/text>
&lt;text x="215" y="164" class="lbl-sm">~0.5 KB (real V3)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;blockquote>
&lt;p>&lt;strong>Note:&lt;/strong> the MLA bar is illustrative, using typical values published by DeepSeek; the exact implementation depends on the latent size. What matters is the order of magnitude.&lt;/p>
&lt;/blockquote>
&lt;p>On top of this comes a fourth, orthogonal technique: &lt;strong>quantising the cache&lt;/strong> to FP8, INT8 or even INT4. vLLM and TensorRT-LLM already support it in production. Going from BF16 (2 bytes) to FP8 (1 byte) &lt;strong>halves the cache&lt;/strong> at a small cost in quality. Going to INT4 divides it by four, at a somewhat higher cost.&lt;/p>
&lt;h2 id="the-next-dragon-fragmentation">The next dragon: fragmentation&lt;/h2>
&lt;p>So far we have talked about the cache as if it were a contiguous block. In practice, an inference server handles &lt;strong>dozens of simultaneous sessions&lt;/strong>, each with its own cache growing at a different rate. Naive allocation, reserving the maximum possible per session, &lt;strong>wastes between 60 % and 80 % of the VRAM&lt;/strong> according to the original PagedAttention paper.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 240" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="KV cache fragmentation: naive vs PagedAttention">
&lt;style>
.used { fill: #2a9d8f; stroke: #1a6e63; stroke-width: 1; }
.free { fill: #f0e7d8; stroke: #aaa; stroke-width: 1; }
.blk { stroke: #555; stroke-width: 0.5; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
&lt;/style>
&lt;text x="180" y="22" text-anchor="middle" class="lbl">Naive allocation (contiguous)&lt;/text>
&lt;text x="540" y="22" text-anchor="middle" class="lbl">PagedAttention (blocks)&lt;/text>
&lt;!-- Naive: 4 sessions reserve the maximum, use little -->
&lt;p>&lt;text x="30" y="60" class="lbl-sm">session A&lt;/text>
&lt;rect x="90" y="48" width="50" height="18" class="used"/>
&lt;rect x="140" y="48" width="180" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="92" class="lbl-sm">session B&lt;/text>
&lt;rect x="90" y="80" width="25" height="18" class="used"/>
&lt;rect x="115" y="80" width="205" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="124" class="lbl-sm">session C&lt;/text>
&lt;rect x="90" y="112" width="100" height="18" class="used"/>
&lt;rect x="190" y="112" width="130" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="30" y="156" class="lbl-sm">session D&lt;/text>
&lt;rect x="90" y="144" width="35" height="18" class="used"/>
&lt;rect x="125" y="144" width="195" height="18" class="free"/>&lt;/p>
&lt;p>&lt;text x="180" y="190" text-anchor="middle" class="lbl-sm">→ ~70 % of VRAM reserved and empty&lt;/text>&lt;/p>
&lt;!-- PagedAttention: small blocks, dense occupancy -->
&lt;g transform="translate(400,40)">
&lt;!-- 8 blocks x 5 rows -->
&lt;g>
&lt;!-- row 1 -->
&lt;rect x="0" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="30" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="60" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="90" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="120" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="150" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="180" y="0" width="30" height="20" class="used blk"/>
&lt;rect x="210" y="0" width="30" height="20" class="used blk"/>
&lt;pre>&lt;code> &amp;lt;rect x=&amp;quot;0&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;30&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;60&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;90&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;120&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;150&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;180&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;210&amp;quot; y=&amp;quot;22&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;0&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;30&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;60&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;used blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;90&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;120&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;150&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;180&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;rect x=&amp;quot;210&amp;quot; y=&amp;quot;44&amp;quot; width=&amp;quot;30&amp;quot; height=&amp;quot;20&amp;quot; class=&amp;quot;free blk&amp;quot;/&amp;gt;
&amp;lt;/g&amp;gt;
&lt;/code>&lt;/pre>
&lt;/g>
&lt;text x="540" y="190" text-anchor="middle" class="lbl-sm">→ &amp;lt; 4 % waste (vLLM paper)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>PagedAttention&lt;/strong>, the idea from Kwon et al. (2023) that gave rise to vLLM, solves this by borrowing a technique from operating systems: divide the VRAM into small &lt;strong>blocks&lt;/strong> (typically 16 tokens) and keep a &lt;strong>page table&lt;/strong> mapping logical to physical blocks per session. A session no longer reserves one huge contiguous block: it grows one block at a time, and the blocks can be scattered across the VRAM. The result is an effective occupancy of 90 % instead of 30 %, and therefore &lt;strong>2-4× more aggregate throughput&lt;/strong> on the same hardware.&lt;/p>
&lt;p>PagedAttention deserves an article of its own. I am noting it down for the next one.&lt;/p>
&lt;h2 id="applied-to-generic-on-premise-hardware">Applied to generic on-premise hardware&lt;/h2>
&lt;p>Let us come down to concrete cases.&lt;/p>
&lt;h3 id="case-1--rtx-4090-24-gb-ada-lovelace">Case 1 — RTX 4090 (24 GB, Ada Lovelace)&lt;/h3>
&lt;p>A typical configuration with Qwen3-8B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Model in BF16: ~16 GB
Activations + overhead: ~2 GB
VRAM available for KV cache: ~6 GB (with margin)
&lt;/code>&lt;/pre>&lt;p>At 144 KB/token (Qwen3-8B GQA), that comes to &lt;strong>~43 K total tokens of cache&lt;/strong> distributed across all simultaneous sessions. In practice:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Maximum context per session&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">1&lt;/td>
&lt;td style="text-align:right">32,768&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">8,192&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">8&lt;/td>
&lt;td style="text-align:right">4,096&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">2,048&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>If you need to advertise &amp;ldquo;we support 32 K of context&amp;rdquo; with concurrency of 4 or more, you have to &lt;strong>quantise the cache&lt;/strong> (FP8 brings it down to 72 KB/token, doubling capacity) or &lt;strong>move up the model range&lt;/strong> (a 4B with GQA and a quantised cache would have room to spare).&lt;/p>
&lt;h3 id="case-2--cluster-of-4h100-320-gb-total-nvlink">Case 2 — Cluster of 4×H100 (320 GB total, NVLink)&lt;/h3>
&lt;p>With tensor parallel = 4 and Llama 3 70B BF16:&lt;/p>
&lt;pre tabindex="0">&lt;code>Model in BF16: ~140 GB (35 GB/GPU)
vLLM overhead per GPU: ~2 GB
VRAM free for KV per GPU: ~43 GB → ~172 GB aggregate
&lt;/code>&lt;/pre>&lt;p>At 320 KB/token (Llama 3 70B GQA), that comes to &lt;strong>~537 K total tokens of cache&lt;/strong>. Ample margin for long contexts with high concurrency:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th style="text-align:right">Concurrency&lt;/th>
&lt;th style="text-align:right">Maximum context per session&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td style="text-align:right">4&lt;/td>
&lt;td style="text-align:right">134,000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">16&lt;/td>
&lt;td style="text-align:right">33,500&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td style="text-align:right">64&lt;/td>
&lt;td style="text-align:right">8,375&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>For DeepSeek-V3 671 B with MLA the economics change radically, because the cache is ~100× thinner. What limits you is no longer the cache but the VRAM of the model itself (quantised to FP8 it is ~335 GB, so it fits on 4×H100 with room for the KV cache).&lt;/p>
&lt;h3 id="operational-implications">Operational implications&lt;/h3>
&lt;p>Three observations we repeat in every consulting engagement:&lt;/p>
&lt;p>First, &lt;strong>the maximum context a model advertises is not the one you can serve on your hardware&lt;/strong>. Llama 3 8B &amp;ldquo;supports&amp;rdquo; 128 K, but on a 4090 with 4 simultaneous sessions your effective context is ~8 K. It is trivial to check before promising it to a client.&lt;/p>
&lt;p>Second, &lt;strong>quantising the KV cache is one of the optimisations with the best cost/benefit ratio in the ENS context&lt;/strong>. It does not touch the weights, it does not affect audit reproducibility, and it doubles capacity. vLLM supports it via &lt;code>--kv-cache-dtype fp8&lt;/code>.&lt;/p>
&lt;p>Third, &lt;strong>if the SLAs dictate long contexts with many concurrent users, GQA is necessary but not sufficient&lt;/strong>. In the medium term you need to look at models with MLA or attention variants with compression.&lt;/p>
&lt;h2 id="what-we-have-not-covered-upcoming-articles">What we have not covered (upcoming articles)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>PagedAttention&lt;/strong> and its implementation in vLLM: blocks, page table, eviction.&lt;/li>
&lt;li>&lt;strong>Prefix caching&lt;/strong>: when several requests share the system prompt, there is no need to recompute the K, V of the common part.&lt;/li>
&lt;li>&lt;strong>Speculative decoding&lt;/strong> and its interaction with the cache.&lt;/li>
&lt;li>&lt;strong>Cache offloading&lt;/strong>: moving cold blocks to RAM or NVMe, a key technique for contexts above 1 M.&lt;/li>
&lt;/ul>
&lt;h2 id="see-also">See also&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/six-stage-llmops-pipeline-architecture-and-deep-dive/">The six-stage LLMOps pipeline&lt;/a> — the master map of the system in production, in which the Deploy stage is one box out of six. This post goes into one of the critical decisions inside Deploy.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/pagedattention-vllm-block-manager/">Inside PagedAttention: blocks, page table, eviction and the state of the art of the KV cache in 2026&lt;/a> — a theoretical deep dive at block level plus a survey of derived optimisations (vAttention, EvicPress, RadixAttention, speculative decoding). It continues this post from the academic theory.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-fine-tuning-production-real-traffic-deployed-adapter/">Continuous fine-tuning in production: from real traffic to the deployed adapter&lt;/a> — how the loop between inference and incremental training closes on the same stack (vLLM + Postgres), with VRAM budgets that explicitly include the KV cache during eval.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/disaggregated-serving-prefill-decode-specialised-pods/">Disaggregated serving: prefill and decode in specialised pods&lt;/a> — the KV cache stops being a private GPU buffer and becomes the artefact transferred between pods. Here the cache size formula determines the economics of the transfer.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/gpu-cluster-as-multi-tenant-platform/">The GPU cluster as a multi-tenant platform&lt;/a> — how the cluster becomes a service with tenants, gateway, quotas and isolation. That is where the KV cache stops being only a performance resource and becomes a platform matter.&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 engine that materialises everything discussed here, deployed on K8s with tensor parallel and autoscaling.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/quantization-llm-inference-fp8-int4-gguf/">Quantization for LLM inference: FP8, INT4 and GGUF&lt;/a> — KV cache quantisation (&lt;code>--kv-cache-dtype=fp8/int4&lt;/code>), mentioned here as the fourth orthogonal technique, is taken apart there with the maths, the formats and the measurable loss.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/speculative-decoding-fundamentals-maths-state-of-play/">Speculative decoding: the secretary who anticipates what the boss will say&lt;/a> — the memory-bound decode regime caused by the KV cache is exactly what speculative decoding exploits: a forward pass with γ tokens costs almost the same as one with a single token.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/flashattention-librarian-io-awareness-async-blackwell-asymmetry/">FlashAttention v1/v2/v3/v4: the librarian who never clears the desk&lt;/a> — the kernel that sweeps the KV cache against Q on every iteration without materialising the N×N matrix. The compute layer underneath the cache.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/moe-inference-fundamentals-expert-parallel-deepseek-economics/">MoE inference: the call centre with 256 specialists&lt;/a> — attention is still dense in every MoE of 2026 (Mixtral, DeepSeek, Qwen3, Llama 4, Kimi K2), so the KV cache keeps its shape; DeepSeek&amp;rsquo;s MLA is the orthogonal optimisation that compresses it ~10× to make long context viable on modest clusters.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/continuous-batching-orca-vllm-chunked-prefill-goodput/">Continuous batching&lt;/a> — the iterative scheduler manages the dynamic allocation of the KV cache across requests; without PagedAttention, continuous batching would fragment the HBM, and without continuous batching the KV cache would be underused because of padding.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/capacity-planning-on-premise-llm-inference/">Capacity planning for on-premise LLM inference&lt;/a> — the KV cache is the dominant component of the VRAM budget when sizing a cluster from an SLO; the spreadsheet is built there step by step.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-prefill-vllm-knobs-ttft-will-not-forgive/">Optimising prefill in vLLM&lt;/a> — the four concrete knobs (chunked prefill, prefix caching, FP8 KV, max-model-len) that turn KV cache theory into production parameters for the RTX 4090 and the L40.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/en/posts/optimising-decode-vllm-squeezing-every-token-small-hardware/">Optimising decode in vLLM&lt;/a> — how &lt;code>--gpu-memory-utilization&lt;/code>, speculative decoding and an FP8 KV cache combine to squeeze small hardware during the generation phase.&lt;/li>
&lt;/ul>
&lt;h2 id="references">References&lt;/h2>
&lt;ul>
&lt;li>Vaswani et al., &lt;em>Attention Is All You Need&lt;/em> (NeurIPS 2017) — foundational transformer paper.&lt;/li>
&lt;li>Ainslie et al., &lt;em>GQA: Training Generalized Multi-Query Transformer Models from Multi-Head Checkpoints&lt;/em> (EMNLP 2023).&lt;/li>
&lt;li>Kwon et al., &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em> (SOSP 2023) — original vLLM paper.&lt;/li>
&lt;li>DeepSeek-AI, &lt;em>DeepSeek-V2 Technical Report&lt;/em> (2024) — introduction of Multi-Head Latent Attention.&lt;/li>
&lt;li>Official vLLM documentation: &lt;a href="https://docs.vllm.ai/">https://docs.vllm.ai/&lt;/a>.&lt;/li>
&lt;li>Llama 3 model card (Meta): GQA specs, n_layers, n_kv_heads.&lt;/li>
&lt;/ul></description></item></channel></rss>