Part 8 of How to Make Your Model Fast (Part 7: Vision Models in the Real World | Part 9: Perception, VLMs and Robots)
A quantitative account of running language models on a phone, a laptop or a single small GPU. We count parameters and FLOPs in a decoder layer from scratch, derive the 2N-per-token rule, separate the compute bound prefill regime from the bandwidth bound decode regime, and work out the KV cache and memory capacity arithmetic that decides what will actually fit. By the end you should be able to predict the token rate of a given model on a given device before you download a single weight, and to say which of quantisation, speculative decoding, cache compression or a smaller model will move it.
Everything in the previous seven parts was building to this question: you have a decoder-only transformer and you have a device that is not a datacentre. How many tokens per second can you possibly get, and what is stopping you?
The pleasant thing about language models is that the answer is almost entirely arithmetic. A convolutional network has enough structural variety that you must profile it to know where the time goes. A transformer is four matrix multiplications and an attention operation, repeated, and the cost model fits on the back of an envelope. The unpleasant thing is that the envelope usually says you are memory bandwidth bound by a factor of fifty, and no amount of kernel work will fix that.
All hardware numbers here are illustrative: round figures in the range that real phones, laptops and small GPUs occupy, chosen so the arithmetic is easy to follow. The method transfers, not the constants.
Fix notation for a decoder-only transformer. Let \(d\) be the model width (d_model), \(d_{ff}\) the feed-forward hidden width, \(h\) the number of attention heads, \(h_{kv}\) the number of key/value heads, \(d_h\) the per-head dimension (usually \(d/h\)), \(L\) the number of layers and \(V\) the vocabulary size.
The attention block holds four projections. The query projection maps \(d \to h \cdot d_h\). The key and value projections each map \(d \to h_{kv} \cdot d_h\), which is the entire content of grouped query attention: you keep all \(h\) query heads but only \(h_{kv}\) distinct key/value heads, and each key/value head is shared across \(h / h_{kv}\) query heads. The output projection maps \(h \cdot d_h \to d\). Taking \(d_h = d/h\):
\[P_{\text{attn}} = d^2 + 2 d^2 \frac{h_{kv}}{h} + d^2 = d^2\left(2 + \frac{2 h_{kv}}{h}\right)\]The feed-forward block is either a plain two-matrix MLP (\(2 d \, d_{ff}\) parameters) or a gated variant such as SwiGLU with three matrices, a gate, an up and a down projection (\(3 d \, d_{ff}\)). Gated is now the default, so I will use three.
The trailing \(2d\) is the two RMSNorm gain vectors, which you can ignore for sizing and should not ignore for numerics. Outside the stack you have the token embedding table, \(V d\), and the output projection, another \(V d\) if the two are untied.
Parameter count for an illustrative 7B-class model, worked term by term. Take \(d = 4096\), \(d_{ff} = 11008\), \(h = h_{kv} = 32\), \(d_h = 128\), \(L = 32\), \(V = 32000\), untied embeddings.
| Term | Expression | Parameters |
|---|---|---|
| Attention, per layer | \(4 d^2\) | 67,108,864 |
| FFN, per layer | \(3 d \, d_{ff}\) | 135,266,304 |
| Norms, per layer | \(2d\) | 8,192 |
| One layer | sum above | 202,383,360 |
| All 32 layers | \(32 \times\) | 6,476,267,520 |
| Embedding + output head | \(2 V d\) | 262,144,000 |
| Final norm | \(d\) | 4,096 |
| Total | 6,738,415,616 |
That is 6.74 billion, which is where “7B” comes from. Two things are worth noticing. The FFN is two thirds of the layer, which is why FFN width is the first dial people turn when they want a smaller model. And embedding plus head is 3.9% of this model but would be 24% of a 1B model with a 128k vocabulary.
A matrix multiply of an \(S \times d_{\text{in}}\) activation by a \(d_{\text{in}} \times d_{\text{out}}\) weight costs \(2 S \, d_{\text{in}} d_{\text{out}}\) floating point operations, because each output element is a dot product of length \(d_{\text{in}}\), which is \(d_{\text{in}}\) multiplies and \(d_{\text{in}}\) adds.
The attention operation itself has no weights, so it gets counted separately. For a prompt of \(S\) tokens, \(QK^\top\) per head is an \(S \times d_h\) by \(d_h \times S\) product, \(2 S^2 d_h\) FLOPs, and summing over \(h\) heads gives \(2 S^2 d\). Multiplying the attention weights by \(V\) costs the same again. So the naive total is \(4 S^2 d\) per layer, and with causal masking exploited properly only the lower triangle is computed, giving about \(2 S^2 d\) per layer.
Prefill, ignoring the output head because you only need logits for the last position:
\[F_{\text{prefill}} \approx 2 N_{\text{mat}} S + 2 L S^2 d\]where \(N_{\text{mat}}\) is the number of parameters in weight matrices, that is, everything except the embedding lookup table.
A single decode step processes one new token against \(S\) cached keys and values. The matmuls become vector-matrix products: \(2 N_{\text{mat}}\) FLOPs. Attention is now \(1 \times S\) rather than \(S \times S\): \(2 S d\) for the scores and \(2 S d\) for the weighted sum, per layer.
\[F_{\text{decode}} \approx 2 N_{\text{mat}} + 4 L S d\]For our 7B model at \(S = 4096\), that is \(1.35 \times 10^{10}\) from the weights and \(2.1 \times 10^{9}\) from attention. Attention is 14% of decode arithmetic at 4k context, and it grows linearly while the weight term stays flat.
The “about \(2N\) FLOPs per token” rule that everyone quotes is not a fitted approximation, it is a counting argument. Every parameter that lives in a weight matrix is touched exactly once per token: it is multiplied by one activation and the product is accumulated into one output. That is one multiply and one add, two FLOPs, per parameter per token. Sum over all matrix parameters and you get \(2 N_{\text{mat}}\).
The approximation only involves what you leave out. Embedding lookup is a gather with zero arithmetic. Norms, activation functions, softmax and residual adds are \(O(d)\) or \(O(S)\) per token per layer, a few tens of FLOPs per dimension against the \(O(d^2)\) of the matmuls. Attention against the cache is the genuinely omitted term, \(4 L S d\), and the rule quietly assumes \(S \ll N_{\text{mat}} / (2 L d)\), which for our 7B model means \(S \ll 25{,}000\).
The same argument gives the training figure. Backward needs the gradient with respect to the input and the gradient with respect to the weights, one pass over the weights each, so backward is about \(4N\) and a full training step about \(6N\) per token.
Takeaway: \(2N\) FLOPs per token is a counting argument, not an empirical fit: each matrix weight does one multiply-accumulate per token. Attention against the cache adds \(4LSd\) per token, which is the term that makes long contexts expensive.
Generation has two phases with completely different bottlenecks, and conflating them is the single most common error I see in performance discussions.
Prefill consumes the prompt. All \(S\) prompt tokens go through the model at once, as a matrix-matrix multiplication. It is embarrassingly parallel and produces one token of output.
Decode produces the rest. Each token depends on the previous one, so the steps are strictly sequential, and each step is a matrix-vector multiplication.
Use the roofline reasoning from Part 1. Arithmetic intensity is FLOPs divided by bytes moved from memory. During prefill you read the weights once, \(N b\) bytes where \(b\) is bytes per weight, and you do \(2 N S\) FLOPs:
\[I_{\text{prefill}} \approx \frac{2 N S}{N b} = \frac{2S}{b}\]With fp16 weights, intensity is simply \(S\) FLOPs per byte. A 512-token prompt gives 512 FLOP/byte. Devices sit somewhere between about 30 and 400 FLOP/byte at their ridge point, so any prompt longer than a few hundred tokens is firmly compute bound. Quantising the weights raises intensity further, since \(b\) shrinks.
An illustrative prefill. Assume a phone-class accelerator sustaining 2 TFLOP/s on this workload (illustrative). A 1024-token prompt into the 7B model costs \(2 \times 6.74{\times}10^9 \times 1024 = 1.38{\times}10^{13}\) FLOPs for the matmuls plus \(2 \times 32 \times 1024^2 \times 4096 = 2.7{\times}10^{11}\) for causal attention, so \(1.41{\times}10^{13}\) in total. That is 7.0 seconds of time to first token. Painful, and it is a genuine compute problem: a faster kernel or a wider matrix engine really does help here.
Now do the same calculation for one decode step. FLOPs are \(2N\). Bytes read are \(Nb\) for the weights, whether you are generating one token or none.
\[I_{\text{decode}} \approx \frac{2N}{Nb} = \frac{2}{b}\]For fp16 that is 1 FLOP per byte. One. Against a ridge point of 30 to 400. Decode is memory bound by one and a half to two and a half orders of magnitude, and this is a property of the algorithm, not of your code.
That gives a hard lower bound on time per token. Let \(B_w\) be the bytes of weights, \(B_{kv}\) the bytes of KV cache you must read, and \(\beta\) the achievable memory bandwidth:
\[t_{\text{token}} \ge \frac{B_w + B_{kv}}{\beta}, \qquad \text{tokens/s} \le \frac{\beta}{B_w + B_{kv}}\]You cannot beat this without moving fewer bytes. No kernel fusion, no assembly, no vendor library beats it, because it is the time to stream the weights past the arithmetic units once. It is the equivalent of the speed of light for the decode loop.
A worked decode bound. Take an illustrative flagship phone with 68 GB/s of memory bandwidth, and the 7B model quantised to int4 with a group size of 64 and fp16 scale and zero-point per group. That is \(4 + 32/64 = 4.5\) bits per weight, so \(6.74{\times}10^9 \times 4.5/8 = 3.79\) GB of weights. At an empty cache:
\[\text{tokens/s} \le \frac{68 \times 10^9}{3.79 \times 10^9} = 17.9\]Now compare the two phases on equal arithmetic. Generating 1024 tokens after that 1024-token prompt costs \(1.46{\times}10^{13}\) FLOPs, within 4% of the prefill. With the average cache over that run (1536 tokens, MHA, fp16) adding 0.81 GB per step, the bound is 14.8 tokens/s, so 69 seconds. Same arithmetic as prefill, ten times the wall clock. That ratio is the whole chapter in one number.
Takeaway: prefill has arithmetic intensity \(2S/b\) and is compute bound; decode has intensity \(2/b\) and is bandwidth bound. Decode tokens per second is bounded by bandwidth divided by (weight bytes plus cache bytes), and clever kernels do not move that bound.
To avoid recomputing attention over the whole prefix at every step, you cache the key and value vectors for every past token, at every layer. Per token, per layer:
\[\text{bytes} = 2 \times h_{kv} \times d_h \times b_{kv}\]The 2 is for K and V, and \(b_{kv}\) is bytes per cached element. Multiply by \(L\) for the whole model, and by \(S\) for the whole sequence.
For our 7B model with full multi-head attention (\(h_{kv} = 32\), \(d_h = 128\)) in fp16: \(2 \times 32 \times 128 \times 2 = 16{,}384\) bytes per layer per token, times 32 layers, is 512 KiB per token. At 4096 tokens that is exactly 2 GiB, which on a phone is already more than the model you were trying to run.
The cache scales with \(h_{kv}\), not \(h\), which is precisely why grouped query attention exists. Cutting \(h_{kv}\) from 32 to 8 cuts the cache by 4x and costs you a little quality; multi-query attention takes \(h_{kv} = 1\) and cuts it by 32x, with a larger quality cost that is usually recovered by uptraining.
Sliding window attention caps the cache instead of shrinking it per token: each layer attends only to the last \(w\) positions, so the cache is \(\min(S, w)\) tokens deep and memory becomes constant in context length. Information still propagates further than \(w\), since each layer moves it \(w\) positions back for a receptive field of roughly \(L w\), but the model can no longer directly retrieve a specific distant token. The usual compromise is to interleave, keeping a minority of layers global.
KV cache size against context length, for the illustrative 7B-class model. All figures are for the cache alone; \(d_h = 128\), \(L = 32\).
| Configuration | Bytes/token | 512 tok | 2,048 tok | 8,192 tok | 32,768 tok | 131,072 tok |
|---|---|---|---|---|---|---|
| MHA (32 KV heads), fp16 | 512 KiB | 256 MiB | 1.0 GiB | 4.0 GiB | 16 GiB | 64 GiB |
| MHA, int8 | 256 KiB | 128 MiB | 512 MiB | 2.0 GiB | 8.0 GiB | 32 GiB |
| GQA (8 KV heads), fp16 | 128 KiB | 64 MiB | 256 MiB | 1.0 GiB | 4.0 GiB | 16 GiB |
| GQA (8 KV heads), int8 | 64 KiB | 32 MiB | 128 MiB | 512 MiB | 2.0 GiB | 8.0 GiB |
| GQA (8 KV heads), int4 | 32 KiB | 16 MiB | 64 MiB | 256 MiB | 1.0 GiB | 4.0 GiB |
| MQA (1 KV head), fp16 | 16 KiB | 8 MiB | 32 MiB | 128 MiB | 512 MiB | 2.0 GiB |
| GQA-8 fp16, 4k sliding window | 128 KiB, capped | 64 MiB | 256 MiB | 512 MiB | 512 MiB | 512 MiB |
The bottom row is the interesting one. Without a window, a 128k context is not a tuning problem, it is an impossibility on any edge device. With one, it is 512 MiB.
The cache is activations, not weights, so the rules from Part 5 apply differently. Keys and values are reasonably well behaved per channel but do contain outlier channels, especially in the keys, where rotary position embeddings concentrate magnitude in a few dimensions. Per-channel scaling for keys and per-token scaling for values is the combination that usually works. int8 cache is close to free in quality terms. int4 cache is workable with group-wise scales and by keeping the most recent tokens in higher precision, since recent keys carry most of the attention mass.
The payoff is double: half or a quarter of the capacity, and the same reduction in bytes read per decode step, which shows up directly in the latency bound.
Takeaway: cache bytes per token are \(2 h_{kv} d_h b_{kv} L\). The three dials are \(h_{kv}\) (GQA/MQA), \(b_{kv}\) (cache quantisation) and the effective \(S\) (sliding windows), and on a long-context edge deployment you will usually need all three.
Here is the clean consequence of decode being bandwidth bound: for a single-stream decode loop, the token rate is inversely proportional to bytes per weight. Not to arithmetic precision, not to kernel quality, to bytes. This is why weight-only quantisation, which looks like a compromise in a training context, is simply the correct default on the edge.
Note that it is weight-only. You dequantise to fp16 or bf16 in registers and do the arithmetic in floating point. Integer arithmetic gains you nothing here, because you are not short of arithmetic; the smaller weight footprint gains you everything. The opposite holds during prefill, where low-precision matmul throughput genuinely helps.
Decode bound against weight precision, illustrative 7B model on an illustrative 68 GB/s device. “With cache” is at 4096 tokens of fp16 MHA cache (2.15 GB).
| Weight format | Effective bits/weight | Weight bytes | tok/s, empty cache | tok/s, 4k cache |
|---|---|---|---|---|
| fp16 | 16 | 13.48 GB | 5.0 | 4.4 |
| int8, per channel | ~8 | 6.74 GB | 10.1 | 7.7 |
| int4, group 64 | 4.5 | 3.79 GB | 17.9 | 11.5 |
| int3, group 64 | 3.5 | 2.95 GB | 23.1 | 13.3 |
fp16 to int4 gives 3.6x, not 4x, because the group scales cost half a bit per weight. With a 4k cache in play it is 2.6x, because the cache bytes do not shrink when the weights do. “Roughly quadruples” is the right mental model and 2.5 to 3.6x is the right number to plan with.
What does it cost? Summarising the published literature rather than any measurement of my own, and with the strong caveat that these vary by model and task:
Approximate quality cost of weight-only quantisation on a 7B-class model. Perplexity deltas are indicative, in the units WikiText-2 perplexity is usually reported in.
| Scheme | Typical perplexity delta | Verdict |
|---|---|---|
| int8, per channel, round-to-nearest | under 0.05 | effectively free |
| int4, group 128, round-to-nearest | 0.3 to 1.0 | noticeable on hard tasks |
| int4, group 64 or 128, GPTQ or AWQ | 0.1 to 0.3 | the sensible edge default |
| int3, best available methods | 1 to 3 | rarely worth it |
| int2 without quantisation-aware training | severe | needs QAT or a different approach |
The decision rule that follows is worth stating plainly. If you have a memory and bandwidth budget \(B\), you almost always get a better model by taking the largest parameter count that fits at int4 than by taking a smaller model at int8 or fp16. A 7B at int4 beats a 3B at int8 at the same footprint on essentially every benchmark I have seen. The exception is when int4 pushes a specific capability off a cliff, which happens most often with code generation and multi-step arithmetic, so test those specifically.
Takeaway: because decode is bandwidth bound, token rate scales as one over bytes per weight. At a fixed byte budget, a bigger model at int4 almost always beats a smaller model at higher precision.
The sequential dependency in decode is what forces you to read all the weights once per token. Speculative decoding attacks exactly that.
A small draft model generates \(\gamma\) candidate tokens autoregressively. The large target model then runs a single forward pass over all \(\gamma + 1\) positions, which gives you its distribution at each one. You walk the candidates left to right, accepting each with a probability that depends on the ratio of target to draft probability, and at the first rejection you resample from a corrected distribution and stop. The acceptance rule is constructed so that the tokens emitted are distributed exactly as if they had come from the target model, one at a time. This is not an approximation; the output distribution is identical.
The reason it works on bandwidth bound hardware is the crucial part. Verifying \(\gamma + 1\) tokens requires reading the target weights exactly once, the same as generating one token. The arithmetic goes up by a factor of \(\gamma+1\), but you had arithmetic to spare: intensity was 1 FLOP per byte against a ridge point of hundreds. Speculative decoding spends the surplus compute you were already wasting and buys sequential steps with it.
Model acceptance as independent per position with probability \(\alpha\). The number of candidates accepted, \(n\), satisfies \(P(n \ge k) = \alpha^k\), so \(E[n] = \sum_{k=1}^{\gamma} \alpha^k\). You always emit one more token beyond the accepted prefix, either the corrected resample or, if all \(\gamma\) were accepted, the target’s own bonus token. Hence
\[E[\text{tokens per cycle}] = 1 + \frac{\alpha(1 - \alpha^{\gamma})}{1 - \alpha} = \frac{1 - \alpha^{\gamma+1}}{1 - \alpha}\]Let \(c\) be the cost of one draft step divided by the cost of one target step. On bandwidth bound hardware, \(c\) is very close to the ratio of the two models’ byte footprints. A cycle costs \(\gamma\) draft steps plus one target pass, so \(\gamma c + 1\) target-step-equivalents, and
\[\text{speedup} = \frac{1 - \alpha^{\gamma+1}}{(1 - \alpha)(\gamma c + 1)}\]Two facts fall straight out. Since \(E[\text{tokens}] < 1/(1-\alpha)\) however large \(\gamma\) gets, you cannot do better than \(1/((1-\alpha)(\gamma c + 1))\), so acceptance rate caps the whole scheme. And at \(\gamma = 1\) the speedup is \((1+\alpha)/(1+c)\), which exceeds 1 exactly when \(\alpha > c\): the draft must be accepted more often than it is expensive.
A worked example. Illustrative 68 GB/s device. Target is the 7B at int4, 3.79 GB, 55.7 ms per token. Draft is a 1.1B at int4, 0.62 GB, 9.1 ms per token. So \(c = 0.16\). Ignoring the draft’s own cache, which is small:
| Acceptance rate \(\alpha\) | \(\gamma = 2\) | \(\gamma = 4\) | \(\gamma = 6\) | \(\gamma = 8\) |
|---|---|---|---|---|
| 0.4 | 1.18 | 1.01 | 0.85 | 0.73 |
| 0.5 | 1.33 | 1.18 | 1.01 | 0.88 |
| 0.6 | 1.49 | 1.41 | 1.24 | 1.09 |
| 0.7 | 1.66 | 1.69 | 1.56 | 1.40 |
| 0.8 | 1.85 | 2.05 | 2.02 | 1.90 |
| 0.9 | 2.05 | 2.50 | 2.66 | 2.69 |
At \(\alpha = 0.75\) and \(\gamma = 4\) the speedup is 1.86, taking the phone from 17.9 to 33 tokens per second, with identical output statistics. Notice the shape: low acceptance rates want short drafts or none at all, and only high acceptance rates justify long ones. A fixed \(\gamma\) is leaving something on the table, which is why production implementations adapt \(\gamma\) from the recent acceptance history.
The catch on a phone is capacity, not speed. You now hold two models and two caches in memory. If the draft pushes you over the capacity limit, the paging behaviour in the next section will destroy far more than speculation gains. Self-speculation, where early layers of the target act as the draft, and n-gram or prompt-lookup drafting, which needs no second model at all, both exist precisely to avoid that.
Takeaway: speculative decoding converts surplus arithmetic into fewer sequential weight reads, which is exactly the trade you want when bandwidth bound. It pays when \(\alpha > c\), and the optimal draft length grows with the acceptance rate.
Batching \(B\) independent decode streams reads the weights once and does \(B\) times the arithmetic, so intensity becomes \(2B/b\). To reach a device ridge point of \(R\) FLOP/byte you need \(B \ge R b / 2\). On an illustrative \(R = 100\) accelerator that is \(B = 100\) at fp16 and \(B = 25\) at int4, which is a nice illustration that quantisation makes you compute bound sooner.
On a phone, \(B = 1\) always, and there is nothing to be done. On a laptop serving a local application, or a single small GPU serving a handful of users, batching is the highest-value change available, and continuous batching, which admits and retires sequences at every step rather than running fixed batches to completion, is what makes it practical when request lengths vary. That is a serving concern and I take it up properly in Part 11.
If many requests share a prefix, a system prompt, a tool schema, a document being asked about, compute the prefix KV once and reuse it. The saving is the whole prefill for that prefix: about \(2 N S_{\text{prefix}}\) FLOPs per request. For a 1000-token system prompt on the 7B model that is \(1.35{\times}10^{13}\) FLOPs, roughly 7 seconds of illustrative phone compute, per request, gone.
The cost is storage: 1000 tokens of GQA-8 fp16 cache is 128 MiB, which must be resident or cheaply reloadable, so on a device this means one hot prefix rather than a general cache. Reuse requires an exact token-level prefix match, so put the variable parts of your prompt last, always. I have seen more prefix caches defeated by a timestamp at the top of the system prompt than by anything else.
The output head is \(V d\) parameters and it produces one logit vector per step, of which you use one sampled index. For our 7B that is 131M parameters, 3.9% of the total. For a 1B model with \(d = 2048\) and a 128k vocabulary it is 262M parameters out of roughly 1.1B, nearly a quarter of everything you read per decode step, spent to produce a vector you almost entirely discard.
Three things follow. Tie the input and output embeddings on small models unless you have measured a real quality gain from untying. Consider keeping the head in a different format from the body: it is more sensitive than the FFN, so it often wants int8 while the body takes int4, but at 24% of the bytes that choice is now a throughput decision, not a rounding detail. And during prefill compute logits only for the last position; computing them for all \(S\) positions costs \(2 S d V\) FLOPs for no benefit, about 2% of prefill for a 1024-token prompt on this model, and considerably more on small models with large vocabularies.
Bandwidth sets your token rate; capacity decides whether you run at all. The budget on a device is weights, plus KV cache, plus activation working set, plus the runtime and its allocator, plus the operating system and every other application, and you get whatever is left before the low-memory killer takes an interest. On an 8 GB phone, an application can typically count on 2 to 3 GB. Our 3.79 GB int4 7B does not fit, and no amount of optimisation changes that.
The standard mitigation is to memory map the weight file read-only. Pages fault in on demand, the file backs them so the kernel can evict them under pressure without needing swap, and several processes can share one copy. It also keeps the weights out of your dirty anonymous memory, which is what most OOM heuristics actually police.
The failure mode is brutal, because decode touches every weight page every step. If a fraction \(f\) of pages has been evicted, each step pays:
\[t = \frac{(1-f) B_w}{\beta_{\text{mem}}} + \frac{f B_w}{\beta_{\text{storage}}}\]With \(B_w = 2\) GB, \(\beta_{\text{mem}} = 68\) GB/s and illustrative flash at 1.5 GB/s, \(f = 0\) gives 29 ms per token and \(f = 0.1\) gives 160 ms. Losing one tenth of your pages costs you 5.4x. There is no graceful degradation here: you are either resident or you are crawling. Size the model so that the whole thing plus the maximum cache fits with headroom, and treat “it works until you open the camera” as a bug in your sizing, not in the phone.
Combine the cache arithmetic with the latency bound. With \(k\) bytes of cache per token:
\[\text{tokens/s} \le \frac{\beta}{B_w + kS}\]Token rate decays hyperbolically in context length. A useful quantity is the context at which the cache costs as much as the model:
\[S_{1/2} = \frac{B_w}{k}\]Beyond \(S_{1/2}\) you are spending more bandwidth on history than on the model, and your throughput is more than halved. For the int4 7B with GQA-8 fp16 cache, \(S_{1/2} = 3.79{\times}10^9 / 131072 \approx 29{,}000\) tokens. With MHA it is 7,200. It is worth computing this number for any deployment, because it tells you the context length at which your product stops feeling responsive.
The quality argument points the same way. Attention cost grows strictly with sequence length; a model’s ability to use distant context does not. Retrieval accuracy typically degrades in the middle of long inputs, and effective context falls well short of advertised context, so you pay linearly in bandwidth and capacity for material the model may barely consult. The right default is the shortest context that solves the task: retrieve and insert the relevant 2,000 tokens rather than pasting 60,000 and hoping.
Putting it together. Pick the largest model whose int4 weights fit in roughly two thirds of your usable memory, leaving the rest for cache, activations and headroom, then check the resulting token rate against your latency requirement.
Model sizing for illustrative device classes. All bandwidth and memory figures are illustrative round numbers. “Usable” is what an application can realistically hold resident. Weight budget is 65 to 70% of usable. Parameter count assumes 4.5 effective bits per weight. Token rate is the bandwidth bound at a small cache, and you should expect to realise 60 to 80% of it.
| Device class | Bandwidth | Usable memory | Weight budget | Model size at int4 | Bound, tok/s |
|---|---|---|---|---|---|
| Mid-range phone | 30 GB/s | 1.5 GB | 1.0 GB | ~1.8B | 30 |
| Flagship phone | 68 GB/s | 3.5 GB | 2.4 GB | ~4B | 28 |
| Laptop, unified memory | 200 GB/s | 12 GB | 8 GB | ~14B | 25 |
| Laptop, 8 GB discrete GPU | 300 GB/s | 7 GB | 5 GB | ~9B | 60 |
| Workstation, 24 GB GPU | 900 GB/s | 22 GB | 15 GB | ~27B | 60 |
Read it as a starting point, then adjust. If you need 60 tokens per second on a phone, no 4B model will do it and you should be looking at a 1 to 2B with speculative decoding. If your context is long, move weight budget into cache budget and drop a model size. If your task is narrow, a distilled or fine-tuned small model from Part 6 will beat a general model two sizes up, and that is by far the largest single win available on constrained hardware.
Takeaway: capacity decides whether you run and bandwidth decides how fast. Size weights to about two thirds of usable memory, compute \(S_{1/2} = B_w / k\) to find where context starts hurting, and never let the working set exceed capacity, because paging costs more than everything else on this list combined.
1. A model has \(d = 3072\), \(d_{ff} = 8192\) (per gated branch), \(h = 24\), \(d_h = 128\), \(h_{kv} = 8\), \(L = 28\), \(V = 128256\), untied embeddings. Find the parameter count per layer, the total, the fraction in embeddings, and the fp16 KV cache size at 8,192 tokens.
Attention: \(W_Q\) is \(3072 \times 3072 = 9{,}437{,}184\); \(W_K\) and \(W_V\) are each \(3072 \times (8 \times 128) = 3072 \times 1024 = 3{,}145{,}728\); \(W_O\) is \(9{,}437{,}184\). Total \(25{,}165{,}824\). Check against the formula: \(d^2(2 + 2 \cdot 8/24) = 9{,}437{,}184 \times 2.667 = 25{,}165{,}824\).
FFN: \(3 \times 3072 \times 8192 = 75{,}497{,}472\). Norms: \(6{,}144\).
Per layer: \(100{,}669{,}440\). Times 28 layers: \(2{,}818{,}744{,}320\).
Embeddings: \(2 \times 128256 \times 3072 = 788{,}004{,}864\). Plus final norm, \(3{,}072\).
Total: 3,606,752,256, about 3.61B. Embeddings are \(788{,}004{,}864 / 3.61{\times}10^9 = 21.8\%\) of the model, which is what a 128k vocabulary does to a 3B model.
KV cache: \(2 \times 8 \times 128 \times 2 \text{ bytes} = 4{,}096\) bytes per layer per token, times 28 layers is \(114{,}688\) bytes, 112 KiB per token. At 8,192 tokens: \(939{,}524{,}096\) bytes = 896 MiB, about 0.94 GB.
2. Quantise that model to int4 with fp16 scales per group of 64 (4.25 effective bits per weight) and run it on an illustrative 120 GB/s device. Give the decode bound at empty cache and at 8,192 tokens, find \(S_{1/2}\), and say what int8 cache quantisation changes.
Weights: \(3.6068{\times}10^9 \times 4.25/8 = 1.916{\times}10^9\) bytes, 1.92 GB.
Empty cache: \(120/1.916 = \mathbf{62.6}\) tok/s.
At 8,192 tokens the cache is 0.940 GB, so total bytes are 2.856 GB and the bound is \(120/2.856 = \mathbf{42.0}\) tok/s. A third of the throughput has gone to history.
\(S_{1/2} = B_w / k = 1.916{\times}10^9 / 114{,}688 = \mathbf{16{,}707}\) tokens. Past that point the cache outweighs the model.
With an int8 cache, \(k\) halves to 57,344 bytes. At 8,192 tokens the cache is 0.470 GB, giving \(120/2.386 = \mathbf{50.3}\) tok/s, a 20% gain, and \(S_{1/2}\) doubles to 33,414 tokens. Note the gain is 20%, not 50%: the weights still dominate, and cache quantisation only pays once the cache is a serious fraction of the bytes.
3. Add speculative decoding to the 8,192-token case from problem 2, using a 0.5B draft at the same 4.25 bits. Assume an acceptance rate of 0.8. Find \(c\), the optimal \(\gamma\) between 2 and 8, and the resulting token rate.
Target step: \(1/42.0 = 23.8\) ms. Draft weights: \(0.5{\times}10^9 \times 4.25/8 = 0.266\) GB, so a draft step is \(0.266/120 = 2.21\) ms, ignoring the draft’s small cache. So \(c = 2.21/23.8 = \mathbf{0.093}\). Since \(\alpha = 0.8 \gg c\), speculation will pay.
Evaluate \(\text{speedup} = (1 - 0.8^{\gamma+1}) / (0.2 (1 + 0.093\gamma))\):
| \(\gamma\) | \(E[\text{tokens}]\) | Cost | Speedup | tok/s |
|---|---|---|---|---|
| 2 | 2.440 | 1.186 | 2.06 | 86 |
| 3 | 2.952 | 1.279 | 2.31 | 97 |
| 4 | 3.362 | 1.372 | 2.45 | 103 |
| 5 | 3.689 | 1.465 | 2.52 | 106 |
| 6 | 3.951 | 1.558 | 2.54 | 106.5 |
| 7 | 4.161 | 1.651 | 2.52 | 106 |
| 8 | 4.329 | 1.744 | 2.48 | 104 |
Optimal \(\gamma = 6\), speedup 2.54x, giving about 107 tok/s. The curve is very flat from 4 to 8, so any \(\gamma\) in that range is fine, which is the usual situation and the reason adaptive \(\gamma\) gives only modest further gains.
4. Deploy the problem-2 model on a device with 3.0 GB usable and 250 MB of activation and runtime overhead. How many tokens of fp16 cache fit? What happens if you force a 12,000-token context, assuming mmap’d weights and illustrative 1.5 GB/s storage?
Cache budget: \(3.0 - 1.916 - 0.25 = 0.834\) GB. At 114,688 bytes per token that is 7,270 tokens. With an int8 cache, 14,540 tokens.
Forcing 12,000 tokens of fp16 cache needs \(12{,}000 \times 114{,}688 = 1.376\) GB. Required total is \(1.916 + 0.25 + 1.376 = 3.542\) GB against 3.0 GB usable, so 0.542 GB must be evicted. Since the cache is dirty anonymous memory and the weights are clean file-backed pages, the kernel evicts weight pages: \(f = 0.542/1.916 = 0.283\).
Per decode step:
Total 386 ms, or 2.6 tok/s, against the 36.4 tok/s you would get if it all fit. A 14x collapse from a 22% capacity overshoot. This is the cliff. Every step re-reads the same evicted 0.54 GB from flash, because decode has no locality to exploit: it touches every weight exactly once per token, so there is nothing for the page cache to be clever about.
The fix is to quantise the cache to int8 (needing 0.688 GB, total 2.85 GB, fits) or to cap the context at 7,000 tokens. Both are better than any kernel change.
The arithmetic in this chapter is deliberately narrow: one model, one stream, one device, text in and text out. That is the hardest case for latency and the easiest case for analysis. Real systems widen it in two directions. They add other modalities, so that the prefill is not a prompt but an image encoder feeding thousands of visual tokens into the same decoder, and they add a deadline that comes from the physical world rather than from a user’s patience. Both change which term in the budget you are fighting, and both are what the next chapter is about.