<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Inference Engineering Academy]]></title><description><![CDATA[Inference Engineering Academy]]></description><link>https://inferenceacademy.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Sat, 07 Nov 2026 10:41:22 GMT</lastBuildDate><atom:link href="https://inferenceacademy.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[How much GPU memory do you need to serve a 70B model?]]></title><description><![CDATA[Weights, KV cache and overhead for Llama 3.1 70B, step by step]]></description><link>https://inferenceacademy.hashnode.dev/how-much-gpu-memory-do-you-need-to-serve-a-70b-model</link><guid isPermaLink="true">https://inferenceacademy.hashnode.dev/how-much-gpu-memory-do-you-need-to-serve-a-70b-model</guid><category><![CDATA[Machine Learning]]></category><category><![CDATA[GPU]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><dc:creator><![CDATA[Inference Engineering Academy]]></dc:creator><pubDate>Sat, 07 Nov 2026 05:44:35 GMT</pubDate><content:encoded><![CDATA[<p>Short answer for Llama 3.1 70B: about <strong>141 GB for BF16 weights alone</strong>, so plan on 2 x 80 GB GPUs as the bare minimum and 4 x 80 GB for comfortable serving. With FP8 it squeezes onto one 80 GB GPU for light traffic; with 4-bit it fits on one 80 GB GPU (or two 48 GB GPUs) with useful KV space. Here is the full calculation so you can redo it for any model.</p>
<h2>Step 1: weights</h2>
<p>Llama 3.1 70B has 70.6B parameters.</p>
<ul>
<li>BF16: 70.6B x 2 bytes = <strong>141.2 GB</strong> (131.5 GiB)</li>
<li>FP8/INT8: 70.6B x 1 = <strong>70.6 GB</strong> (65.8 GiB)</li>
<li>INT4: 70.6B x 0.5 = <strong>35.3 GB</strong>, plus a few percent for quantization scales</li>
</ul>
<h2>Step 2: KV cache per token</h2>
<p>From the model's config: 80 layers, 8 KV heads (grouped-query attention), head_dim 128.</p>
<p>KV per token = 2 x 80 x 8 x 128 x 2 bytes = 327,680 bytes = <strong>320 KiB</strong> (BF16), or 160 KiB with an FP8 KV cache.</p>
<p>Per sequence:</p>
<ul>
<li>4,096 tokens: 1.25 GiB</li>
<li>8,192 tokens: 2.5 GiB</li>
<li>32,768 tokens: 10 GiB</li>
<li>128K tokens (the model's maximum, 131,072): 40 GiB</li>
</ul>
<h2>Step 3: overhead</h2>
<p>Engines reserve memory for activations, CUDA graphs, and the CUDA context. Budget roughly 1 to 3 GiB per GPU, and remember engines like vLLM only use a fraction you set (<code>--gpu-memory-utilization</code>, default 0.9).</p>
<h2>Step 4: put it together</h2>
<p>Usable memory per 80 GB GPU at 0.9 utilization ~= 72 GB = 67 GiB, minus ~2 GiB overhead = ~65 GiB.</p>
<table>
<thead>
<tr>
<th>Setup</th>
<th>Usable</th>
<th>Weights</th>
<th>Left for KV</th>
<th>Tokens of KV (BF16)</th>
<th>8K-token sequences</th>
</tr>
</thead>
<tbody><tr>
<td>1 x 80 GB, BF16</td>
<td>65 GiB</td>
<td>131.5 GiB</td>
<td>does not fit</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>2 x 80 GB, BF16</td>
<td>130 GiB</td>
<td>131.5 GiB</td>
<td>does not fit at 0.9</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>2 x 80 GB, BF16, util 0.95</td>
<td>~138 GiB</td>
<td>131.5 GiB</td>
<td>~6 GiB</td>
<td>~20K</td>
<td>~2</td>
</tr>
<tr>
<td>4 x 80 GB, BF16</td>
<td>~260 GiB</td>
<td>131.5 GiB</td>
<td>~129 GiB</td>
<td>~420K</td>
<td>~51</td>
</tr>
<tr>
<td>1 x 80 GB, FP8 weights + FP8 KV</td>
<td>~65 GiB</td>
<td>65.8 GiB</td>
<td>does not fit at 0.9</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>1 x 80 GB, INT4 weights</td>
<td>~65 GiB</td>
<td>~34 GiB</td>
<td>~31 GiB</td>
<td>~100K</td>
<td>~12</td>
</tr>
</tbody></table>
<p>(Numbers from the formulas above; "8K-token sequences" = KV left / 2.5 GiB.) The FP8 row shows why "70B in FP8 fits on one H100" is only just true: you need to raise utilization close to 1.0 and keep context short. A 141 GB H200 holds FP8 70B comfortably (~50 GiB left for KV at 0.9 utilization).</p>
<h2>Step 5: choose by workload</h2>
<ul>
<li><strong>Few users, short prompts, budget-limited:</strong> INT4 on one 80 GB GPU.</li>
<li><strong>Production chat at moderate scale:</strong> BF16 or FP8 with tensor parallelism across 4 GPUs, or FP8 on 2 GPUs.</li>
<li><strong>Long context (32K+) for many users:</strong> KV dominates; use FP8 KV cache, more GPUs, and prefix caching.</li>
</ul>
<h2>Do it for any model</h2>
<p>Read <code>config.json</code> and use:</p>
<ul>
<li>weights = params x bytes per param</li>
<li>KV/token = 2 x num_hidden_layers x num_key_value_heads x (hidden_size / num_attention_heads) x bytes</li>
<li>concurrent tokens = (usable memory - weights) / KV per token</li>
</ul>
<p>Our zero-dependency calculator does this for you: <a href="https://github.com/inference-engineering-academy/inference-engineering-notes">kv_calc.py</a>.</p>
<hr />
<p><em>If you want to go deeper, the free Foundations course at <a href="https://www.inference-engineering.xyz/">Inference Engineering Academy</a> covers these topics with browser labs. Disclosure: written by the academy team.</em></p>
]]></content:encoded></item><item><title><![CDATA[Prefill vs decode: why LLM inference is memory-bandwidth bound]]></title><description><![CDATA[Why a petaFLOP GPU streams ~100 tokens/s, with the arithmetic]]></description><link>https://inferenceacademy.hashnode.dev/prefill-vs-decode-why-llm-inference-is-memory-bandwidth-bound</link><guid isPermaLink="true">https://inferenceacademy.hashnode.dev/prefill-vs-decode-why-llm-inference-is-memory-bandwidth-bound</guid><category><![CDATA[GPU]]></category><category><![CDATA[performance]]></category><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Inference Engineering Academy]]></dc:creator><pubDate>Thu, 05 Nov 2026 15:36:58 GMT</pubDate><content:encoded><![CDATA[<p>Ask why an LLM streams its answer at a steady ~100 tokens/s even on a GPU rated for nearly a petaFLOP, and you end up at one idea: generation has two phases with very different bottlenecks.</p>
<h2>Two phases</h2>
<p><strong>Prefill</strong> runs the whole prompt through the model in one forward pass. Every prompt token is processed in parallel, so each layer does matrix × matrix multiplications. Prefill fills the KV cache and produces the first output token, so it drives <strong>time to first token (TTFT)</strong>.</p>
<p><strong>Decode</strong> then generates one token per step. Each step pushes a single new token (per sequence) through every layer and attends over the cached K/V. Decode drives <strong>time per output token (TPOT)</strong>, also called inter-token latency.</p>
<h2>Arithmetic intensity</h2>
<p>A GPU is limited either by how fast it can compute (FLOP/s) or by how fast it can read memory (bytes/s). The ratio that decides which one applies is <strong>arithmetic intensity</strong>: FLOPs done per byte moved.</p>
<p>An H100 SXM offers roughly 989 TFLOP/s of dense BF16 and about 3.35 TB/s of HBM bandwidth. The break-even point (the "ridge" of the roofline) is about 989e12 / 3.35e12 ≈ <strong>295 FLOPs per byte</strong>. Below that, the work is memory-bound.</p>
<p>For a matrix multiply against a weight matrix, each BF16 weight (2 bytes) gets used for 2 FLOPs (multiply + add) per token in the batch:</p>
<ul>
<li><strong>Decode, batch 1:</strong> ~2 FLOPs per 2 bytes ≈ <strong>1 FLOP/byte</strong>, far below 295. The GPU spends most of its time waiting for weights to arrive from HBM.</li>
<li><strong>Prefill, 2,000-token prompt:</strong> each weight is reused for 2,000 tokens, so ≈ <strong>2,000 FLOPs/byte</strong>, well above the ridge. That's compute-bound.</li>
</ul>
<h2>Back-of-envelope decode speed</h2>
<p>Llama 3.1 8B in BF16 is about 16 GB of weights. Every decode step has to read all of them at least once:</p>
<pre><code>16 GB / 3.35 TB/s ≈ 4.8 ms per step  →  ≈ 200 tokens/s upper bound (batch 1)
</code></pre>
<p>Real numbers come in lower because of KV-cache reads, kernel overheads and imperfect bandwidth use. Notice that FLOPs never showed up in that estimate. <strong>For single-stream decode, memory bandwidth sets the speed.</strong></p>
<h2>Why batching is the main lever</h2>
<p>If 32 sequences decode together, the weights are read once per step and used 32 times. Intensity rises about 32×, so throughput rises almost linearly while per-step latency barely moves. That's why serving engines use <strong>continuous batching</strong>: they admit new requests into the running batch at every step instead of waiting for a whole batch to finish.</p>
<p>There's a catch. Weights are shared across the batch, but <strong>KV cache is not</strong>: each sequence reads its own cache on every step. As batch size and context grow, cache reads take over and decode stays memory-bound. This is why KV-cache size (128 KiB/token for Llama 3.1 8B in BF16) matters for speed as well as capacity.</p>
<h2>What follows from this</h2>
<ul>
<li><strong>Quantizing weights speeds up decode</strong> because there are fewer bytes to read. INT4/FP8 weights help decode more than they help prefill.</li>
<li><strong>Speculative decoding</strong> uses spare compute: a small draft model proposes several tokens and the big model checks them in one pass, which is more work per byte read.</li>
<li><strong>Chunked prefill</strong> splits long prompts into pieces so a big prefill doesn't stall everyone else's decode steps.</li>
<li><strong>Prefill/decode disaggregation</strong> (as in NVIDIA Dynamo, and supported by vLLM and SGLang) runs the two phases on separate GPU pools, because they want different hardware and different batch sizes.</li>
<li><strong>Measure TTFT and TPOT separately.</strong> A change that helps one often hurts the other.</li>
</ul>
<h2>Summary</h2>
<p>Prefill is a big parallel matrix multiply and is compute-bound. Decode reads every weight and the whole KV cache to produce one token, so it is memory-bandwidth-bound. Almost every serving optimization (batching, quantization, paging, speculation, disaggregation) is a way of getting more useful tokens out of each byte read from memory.</p>
<hr />
<p><em>If you want to go deeper, the free Foundations course at <a href="https://www.inference-engineering.xyz/">Inference Engineering Academy</a> covers prefill, decode and the KV cache with runnable labs. Disclosure: written by the academy team.</em></p>
]]></content:encoded></item><item><title><![CDATA[KV cache math: why Llama 3.1 8B needs ~128 KB per token]]></title><description><![CDATA[If you serve LLMs, the KV cache, not the weights, usually decides how many users fit on one GPU. The good news is that its size is easy to calculate from…]]></description><link>https://inferenceacademy.hashnode.dev/kv-cache-math-why-llama-3-1-8b-needs-128-kb-per-token</link><guid isPermaLink="true">https://inferenceacademy.hashnode.dev/kv-cache-math-why-llama-3-1-8b-needs-128-kb-per-token</guid><category><![CDATA[AI]]></category><category><![CDATA[performance]]></category><category><![CDATA[llm]]></category><category><![CDATA[Machine Learning]]></category><dc:creator><![CDATA[Inference Engineering Academy]]></dc:creator><pubDate>Wed, 28 Oct 2026 11:46:07 GMT</pubDate><content:encoded><![CDATA[<p>If you serve LLMs, the KV cache, not the weights, usually decides how many users fit on one GPU. The good news is that its size is easy to calculate from <code>config.json</code>, and the calculation explains a lot of serving behaviour.</p>
<h2>What the KV cache stores</h2>
<p>In every attention layer, each token produces a key vector and a value vector. During decoding, a new token's query has to attend over the keys and values of all earlier tokens. Because of the causal mask those earlier K/V vectors never change, so engines compute them once and keep them in GPU memory. That stored state is the KV cache. Without it, every decode step would recompute the whole prefix.</p>
<h2>The formula</h2>
<pre><code class="language-plaintext">bytes per token = 2 × num_layers × num_kv_heads × head_dim × bytes_per_element
</code></pre>
<ul>
<li><strong>2</strong>: one key plus one value</li>
<li><strong>num_layers</strong>: <code>num_hidden_layers</code></li>
<li><strong>num_kv_heads</strong>: <code>num_key_value_heads</code>. This is the number of <em>KV</em> heads, not query heads.</li>
<li><strong>head_dim</strong>: <code>head_dim</code>, or <code>hidden_size / num_attention_heads</code> if the field is missing</li>
<li><strong>bytes_per_element</strong>: 2 for BF16/FP16, 1 for FP8</li>
</ul>
<h2>Llama 3.1 8B, worked out</h2>
<p>From its config: 32 layers, 32 query heads, 8 KV heads (grouped-query attention), hidden size 4096, so head_dim = 4096 / 32 = 128. At BF16:</p>
<pre><code class="language-plaintext">2 × 32 × 8 × 128 × 2 = 131,072 bytes = 128 KiB per token
</code></pre>
<p>Now scale it:</p>
<table>
<thead>
<tr>
<th>Workload</th>
<th>KV cache</th>
</tr>
</thead>
<tbody><tr>
<td>1 request × 8,192 tokens</td>
<td>1 GiB</td>
</tr>
<tr>
<td>1 request × 131,072 tokens (full context)</td>
<td>16 GiB</td>
</tr>
<tr>
<td>32 requests × 8,192 tokens</td>
<td>32 GiB</td>
</tr>
</tbody></table>
<p>The BF16 weights of the 8B model take about 16 GB (~15 GiB). <strong>A single full-context request needs more memory for cache than the model needs for weights.</strong> A model that "fits" on a 24 GB card can still go out of memory as soon as real traffic arrives.</p>
<h2>The most common mistake: using query heads</h2>
<p>If you plug in 32 attention heads instead of 8 KV heads, you get 512 KiB per token, which is 4× too high. Grouped-query attention lets 4 query heads share each KV head, and that sharing is exactly why Llama 3 can offer long contexts. For Llama 3.1 70B (80 layers, 8 KV heads, head_dim 128) the same formula gives 320 KiB per token, about 40 GiB at full 128K context.</p>
<h2>What this means when you serve</h2>
<ol>
<li><strong>Concurrency budget.</strong> Free memory after weights ÷ (bytes per token × average sequence length) ≈ how many requests you can hold at once. On an 80 GB GPU running the 8B model with roughly 60 GB left for cache, that's about 480K tokens in total: ~60 requests at 8K, but only 3 at full context.</li>
<li><strong>FP8 KV cache halves it.</strong> At 1 byte per element you get 64 KiB per token. Engines like vLLM and SGLang support this; check quality on your own workload.</li>
<li><strong>Paging matters.</strong> Engines that preallocate the maximum length waste most of this memory. PagedAttention (vLLM) and radix-tree caches (SGLang) allocate in blocks, so memory follows actual length.</li>
<li><strong>Decode speed falls with context.</strong> Every decode step reads the whole cache for that sequence, so long contexts cost bandwidth as well as capacity.</li>
</ol>
<h2>A quick calculator</h2>
<pre><code class="language-python">def kv_bytes_per_token(layers, kv_heads, head_dim, dtype_bytes=2):
    return 2 * layers * kv_heads * head_dim * dtype_bytes

per_tok = kv_bytes_per_token(32, 8, 128)       # Llama 3.1 8B
print(per_tok / 1024, "KiB/token")              # 128.0
print(per_tok * 131072 / 2**30, "GiB @ 128K")   # 16.0
</code></pre>
<p>Run this for any model before you choose a context length, a GPU, or <code>--max-num-seqs</code>.</p>
<hr />
<p><em>If you want to go deeper, the free Inference Engineering Foundations course at inference-engineering.xyz works through these numbers for Llama 3.1 8B with runnable NumPy code.</em></p>
]]></content:encoded></item></channel></rss>