The example estimates how much memory a model needs from four things: how many parameters it
has, how many bits each weight is stored at, how long the context window is, and how many
requests are being served at once. Every number it prints is an estimate, and the demo says so
above the table: it counts only weights and a key/value cache, the two costs every runtime
accounts for in some form, and leaves out activation memory and a framework’s own overhead, both
of which only add to the real figure. Use it to size hardware with room to spare, not to predict
what a runtime will report.
examples/local_inference/run.py · lines 46–52
def weights_bytes(params: int, bits_per_weight: float) -> int:
"""Parameter count times bits per weight, converted to bytes. `bits_per_weight` is the
caller's own average figure: 16 for fp16, roughly 4 for a 4-bit quantization such as Q4_K_M.
A real quantized file usually averages a little above the bit width in its name, because not
every tensor is quantized the same way (llama.cpp's quantize tool can leave the output tensor
unquantized, for instance). This function models none of that; it takes the average given."""
return round(params * bits_per_weight / 8)
The weights are the simple half: parameter count times bits per weight, in bytes. A named
quantization is not exactly its nominal bit width in practice, because not every tensor is
quantized the same way: llama.cpp’s own quantize documentation offers --leave-output-tensor to
“leave output.weight un(re)quantized,” and says a multimodal projector is usually kept in a
high-quality format instead[3]. So bits_per_weight is an average the caller supplies,
not a number this function looks up, and the real file usually averages a little above the bit
width in its name.
The key/value cache is the half that depends on how the model is used, not just what it is. Every
token the model has already seen leaves behind one key and one value vector in every layer, and
they stay there for the rest of the sequence, which is why context length costs memory before a
single token of it is used:
examples/local_inference/run.py · lines 55–63
def kv_cache_bytes(
*, context_length: int, num_layers: int, num_kv_heads: int, head_dim: int, bytes_per_value: int = 2, num_sequences: int = 1
) -> int:
"""The key/value cache: two tensors (key and value) per layer, each sized
context_length x num_kv_heads x head_dim at bytes_per_value bytes, times how many sequences
are served at once. `num_kv_heads` is the key/value head count from the model's own config,
which grouped-query attention makes smaller than the attention head count."""
per_sequence = 2 * num_layers * context_length * num_kv_heads * head_dim * bytes_per_value
return per_sequence * num_sequences
Two per layer for the key and the value; num_kv_heads times head_dim for how wide each one
is; context_length for how many of them accumulate; bytes_per_value for the precision they
are held at. The one term worth checking twice is num_kv_heads. Grouped-query attention lets
several attention heads share one key/value head, so a model’s key/value head count is often a
fraction of its attention head count, and passing the larger number quietly inflates the whole
estimate. It comes from the model’s own config, not from this function.
The cache then multiplies by how many sequences (concurrent requests) are held at once: the cost
of serving several users from one running model rather than one at a time. That multiplication is
the naive ceiling, and servers are built to avoid paying it in full: vLLM documents “Efficient
management of attention key and value memory with PagedAttention” and “Continuous batching of
incoming requests” among its own features[4]. This example models neither, so a real
server with either should need less than the number printed here for the same concurrency, while
still needing more for everything else.
python -m examples.local_inference --demo prints the same illustrative 8B-parameter shape four
ways (full precision against roughly 4-bit, a short context against a long one, one user against
eight) so each variable’s effect is visible on its own:
examples/local_inference/README.md · lines 11–11
python -m examples.local_inference --demo
tests/test_example_local_inference.py checks the arithmetic against numbers computed by hand,
never against a real model: 1,000,000 parameters at 16 bits is exactly 2,000,000 bytes, and a
small cache shape works out to exactly 640 bytes. It also pins the two proportionalities the
sizing questions on this page turn on (doubling the context doubles the cache, eight sequences
cost eight times one) and that a smaller bits_per_weight only ever shrinks the weights term,
leaving the cache alone, because quantizing weights does nothing about what a conversation is.