← All Posts

GGUF and llama.cpp

GGUF Format Overview

GGUF (GPT-Generated Unified Format) is a binary file format designed by Georgi Gerganov for efficient local inference with llama.cpp. It replaced the older GGML format in August 2023, adding a structured metadata system and extensible architecture. GGUF is now the de facto standard for running quantized LLMs on consumer hardware — CPUs, Apple Silicon, and consumer GPUs.

GGUF File Structure Header magic: "GGUF" version: 3 Metadata (Key-Value Pairs) architecture, context_length, embedding_length, vocab_size, ... Tensor Info Table name, shape, dtype, offset per tensor Align pad Tensor Data (contiguous, mmap-friendly) blk.0.attn_q.weight | blk.0.attn_k.weight | blk.0.attn_v.weight | ... | output.weight Memory-Mapped I/O Tensors loaded via mmap() → near-instant model loading Self-Describing All model info in one file No external config.json needed Mixed Quantization Each tensor can have a different quantization type

Key design decisions in GGUF:

Why not SafeTensors? SafeTensors is designed for the PyTorch/HuggingFace ecosystem with GPU-first workflows. GGUF is designed for CPU-first inference: it supports quantization types not available in SafeTensors (Q4_K, Q5_K, etc.), includes the full tokenizer in-file, and optimizes for sequential memory access patterns that CPUs prefer over the random-access patterns GPUs handle well.

Quantization Types Explained

GGUF supports a rich set of quantization types. Each type defines how a block of weights (typically 32 or 256 values) is stored. Every block has a scale factor (and sometimes a minimum/offset) stored in higher precision.

Type Bits/Weight Block Size Structure Quality
Q4_04.50324-bit symmetric, FP16 scale per blockLowest quality legacy 4-bit
Q4_15.00324-bit asymmetric, FP16 scale + minBetter than Q4_0
Q4_K_S4.502564-bit with 6-bit super-block scalesGood k-quant baseline
Q4_K_M4.85256Mixed: attention Q6_K, rest Q4_KBest quality/size ratio
Q5_K_S5.502565-bit with 6-bit super-block scalesHigh quality
Q5_K_M5.69256Mixed: attention Q6_K, rest Q5_KNear-lossless for most models
Q6_K6.562566-bit with 8-bit super-block scalesVery high quality
Q8_08.50328-bit symmetric, FP16 scale per blockNear-FP16 quality

K-Quants vs Legacy Quants

The "K" in Q4_K, Q5_K, etc. stands for k-quantization, introduced by @ikawrakow in June 2023. K-quants represent a major improvement over legacy quants (Q4_0, Q4_1, Q5_0, Q5_1) through two innovations:

Legacy Quants (Q4_0, Q5_0)

  • Block size: 32 values
  • Scale: Single FP16 scale per block
  • Overhead: 0.5 bits/weight (16 bits / 32 weights)
  • All layers same precision
  • Simple but wasteful: scale uses too many bits relative to data
  • LLaMA-7B Q4_0: WikiText-2 PPL = 6.87

K-Quants (Q4_K, Q5_K)

  • Super-block: 256 values, divided into 8 sub-blocks of 32
  • Scale: 6-bit scale per sub-block + FP16 super-scale
  • Overhead: ~0.1875 bits/weight (more efficient)
  • Mixed precision across layers (_M variants)
  • Hierarchical quantization: super-block scale → sub-block scale → values
  • LLaMA-7B Q4_K_M: WikiText-2 PPL = 6.20 (much better!)
The _M (mixed) variants: Q4_K_M and Q5_K_M use higher precision for attention layers and lower precision for MLP layers. This exploits the fact that attention weights are more sensitive to quantization error. For Q4_K_M: attention QKV and output projections use Q6_K (~6.5 bpw), while MLP up/gate/down projections use Q4_K (~4.5 bpw). The average is ~4.85 bpw, delivering Q5-level quality at Q4-level size.

The k-quant improvement is most visible at 4-bit precision. At 8-bit (Q8_0), legacy quants are already near-lossless, so k-quants provide diminishing returns.

Memory Requirements by Model Size

GGUF models need memory for: (1) model weights, (2) KV cache, and (3) compute buffers. The table below shows weight-only memory for popular models and quant types:

Model FP16 Q8_0 Q5_K_M Q4_K_M Q4_0
LLaMA-3-8B16.0 GB8.5 GB5.7 GB4.9 GB4.7 GB
Mistral-7B14.5 GB7.7 GB5.1 GB4.4 GB4.1 GB
LLaMA-3-70B140 GB74 GB49 GB42 GB40 GB
Mixtral-8x7B93 GB49 GB33 GB28 GB26 GB

Add approximately 1–2 GB for KV cache at 2K context length, or 4–8 GB at 8K context length (varies by model architecture and GQA ratio). A practical rule of thumb: total RAM needed ≈ model weight size × 1.2 for comfortable headroom.

Recommended quant types by hardware:
  • 8 GB RAM: Q4_K_M for 7B models (safe), Q4_0 for 13B (tight)
  • 16 GB RAM: Q5_K_M for 7B, Q4_K_M for 13B, Q4_0 for 30B (tight)
  • 32 GB RAM: Q8_0 for 7B, Q5_K_M for 13B, Q4_K_M for 30B
  • 64 GB RAM: Q5_K_M for 70B, Q8_0 for 30B
  • 96 GB RAM (Mac Studio): Q6_K for 70B with 8K context

Converting & Quantizing Models

The typical workflow for creating GGUF files from HuggingFace models:

# Step 1: Clone llama.cpp and build
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
make -j$(nproc) GGML_CUDA=1   # GPU support (optional)
# On macOS: make -j$(sysctl -n hw.ncpu) GGML_METAL=1

# Step 2: Convert HuggingFace model to GGUF (FP16)
python convert_hf_to_gguf.py \
    --outfile ./models/llama-3-8b-f16.gguf \
    --outtype f16 \
    /path/to/Meta-Llama-3-8B/

# Step 3: Quantize to desired type
./llama-quantize \
    ./models/llama-3-8b-f16.gguf \
    ./models/llama-3-8b-Q4_K_M.gguf \
    Q4_K_M

# Available quantization types (most common):
#   Q4_0    - legacy 4-bit (fastest, lowest quality)
#   Q4_K_S  - k-quant 4-bit small
#   Q4_K_M  - k-quant 4-bit medium (recommended default)
#   Q5_K_S  - k-quant 5-bit small
#   Q5_K_M  - k-quant 5-bit medium (recommended for quality)
#   Q6_K    - k-quant 6-bit (near-lossless)
#   Q8_0    - 8-bit (highest quality quantized)

# Step 4: Test the quantized model
./llama-cli \
    -m ./models/llama-3-8b-Q4_K_M.gguf \
    -p "The meaning of life is" \
    -n 128 \
    --temp 0.7 \
    --top-p 0.9 \
    -ngl 99   # offload all layers to GPU

For Python users, the llama-cpp-python package provides a clean API:

from llama_cpp import Llama

# Load quantized model
llm = Llama(
    model_path="./models/llama-3-8b-Q4_K_M.gguf",
    n_ctx=4096,           # context window
    n_gpu_layers=-1,      # -1 = offload all layers to GPU
    n_threads=8,          # CPU threads for non-offloaded layers
    verbose=False,
)

# Chat completion (OpenAI-compatible API)
response = llm.create_chat_completion(
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain quantum computing in 3 sentences."}
    ],
    max_tokens=256,
    temperature=0.7,
)
print(response["choices"][0]["message"]["content"])

llama.cpp Server Setup

llama.cpp includes a built-in HTTP server that provides an OpenAI-compatible API, making it a drop-in replacement for production deployments:

# Start the server with GPU offloading
./llama-server \
    -m ./models/llama-3-8b-Q4_K_M.gguf \
    --host 0.0.0.0 \
    --port 8080 \
    -ngl 99 \                   # GPU layers
    -c 4096 \                   # context size
    --parallel 4 \              # concurrent request slots
    --cont-batching \           # continuous batching
    --metrics \                 # Prometheus metrics endpoint
    --flash-attn                # Flash Attention (if supported)

# Test with curl (OpenAI-compatible endpoint)
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "llama-3-8b",
    "messages": [
      {"role": "user", "content": "Hello, how are you?"}
    ],
    "max_tokens": 128,
    "temperature": 0.7
  }'

# Or use the OpenAI Python client directly
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8080/v1", api_key="not-needed")
response = client.chat.completions.create(
    model="llama-3-8b",
    messages=[{"role": "user", "content": "Tell me about GGUF"}]
)
Continuous batching: The --parallel N flag enables N concurrent request slots with continuous batching. Unlike static batching, completed requests immediately free their slot for new requests. With N=4, llama.cpp can serve ~3.5× more requests/second than sequential processing with minimal latency impact per request.

Hardware Benchmarks

Real-world performance numbers for LLaMA-3-8B Q4_K_M (4.9 GB) across consumer hardware. All measurements at 2048 context length, single request:

Hardware Prompt (t/s) Generate (t/s) Notes
MacBook Pro M3 Max (36 GB)~450~55Metal GPU, unified memory
MacBook Air M2 (16 GB)~220~28Metal GPU, 8-core
RTX 4090 (24 GB)~2800~110Full GPU offload
RTX 3060 (12 GB)~800~45Full GPU offload
RTX 3060 (12 GB) + CPU~300~22Partial offload (20 layers GPU)
AMD Ryzen 9 7950X (CPU only)~150~1816 threads, AVX-512
Intel i7-12700 (CPU only)~100~1212 threads, AVX2

For LLaMA-3-70B Q4_K_M (42 GB), the options narrow:

Mac Studio M2 Ultra (192 GB)

  • Generation: ~22 tokens/s
  • Prompt processing: ~180 t/s
  • Full model in unified memory
  • 200W power draw under load

2× RTX 3090 (48 GB total)

  • Generation: ~35 tokens/s
  • Prompt processing: ~600 t/s
  • Split across GPUs via tensor parallel
  • ~600W power draw under load
Apple Silicon advantage: Apple's unified memory architecture gives M-series chips a unique advantage for LLM inference: they can load 70B Q4_K_M models that would require multi-GPU setups on NVIDIA hardware. The M2 Ultra with 192GB unified memory can run a 70B Q5_K_M model (49 GB) with 32K context — something impossible on any single consumer GPU. The trade-off is lower throughput per watt compared to NVIDIA GPUs.

llama.cpp + GGUF has democratized LLM inference, making it possible to run capable models on commodity hardware. The combination of efficient quantization (k-quants), optimized CPU/GPU kernels (SIMD, Metal, CUDA), and a robust serving stack makes it the go-to solution for local LLM deployment.