Mastering llama.cpp and GGUF: Quantize Hugging Face LLMs for Local Hardware

AI tutorial - IT technology blog
AI tutorial - IT technology blog

Context & Why

Six months ago, our security team drew a hard line: no customer source code or internal telemetry could leave our private network. That ruled out hosted APIs like OpenAI and Anthropic. We had to run capable 8B and 14B models on our existing developer workstations and on-premise servers. Immediately, we hit the VRAM wall.

An unquantized 16-bit (FP16/BF16) 8B model demands roughly 16 GB of VRAM just to store raw weights. Once you allocate a KV cache for an 8k context window, you burn another 1.2 GB. Try that on a standard 12 GB RTX 3060 or 4070, and you crash with an Out-of-Memory (OOM) error before generating a single token.

Grabbing pre-quantized GGUF files from community uploaders on Hugging Face looks tempting at first. We did that initially, but it backfired in staging. Community quants often slip past version control with outdated chat templates, broken BOS/EOS tokens, or generic quantization matrices that degrade perplexity on proprietary codebases. Quantizing upstream safetensors yourself takes ten minutes and gives you total control over the resulting binary.

The GGUF format makes this viable. Unlike legacy GGML, GGUF bundles tokenizers, architectural hyperparameters, and tensor tensors into a single self-describing binary. Because it maps directly into memory via mmap, you get near-instant load times and clean layer-by-layer offloading between system RAM and GPU VRAM.

Installation

You need two pieces on your build box: the compiled C/C++ binaries of llama.cpp and an isolated Python environment for metadata conversion.

1. Build llama.cpp from Source

Compile the binaries directly on your target hardware to let the compiler leverage your CPU’s native instruction set (such as AVX-512 or ARM Neon).

# Clone the llama.cpp repository
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp

# Configure with CMake (Enable CUDA for Nvidia GPUs)
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release

# Build binaries using all CPU cores
cmake --build build --config Release -j $(nproc)

On Apple Silicon, swap -DGGML_CUDA=ON for -DGGML_METAL=ON to compile against the Metal framework.

2. Set Up the Python Conversion Environment

The conversion script parses Hugging Face safetensors, reads tokenizer configurations, and repackages them into uncompressed FP16 GGUF containers. Isolate these dependencies in a dedicated virtual environment:

python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
# Install the specific dependencies required for GGUF conversion
pip install -r requirements/requirements-convert_hf_to_gguf.txt
pip install huggingface_hub

3. Download Base Model Weights

Pull the official safetensors files directly from upstream. In this walkthrough, we will convert Meta’s Llama-3.1-8B-Instruct:

huggingface-cli download meta-llama/Llama-3.1-8B-Instruct \
  --local-dir ./models/Llama-3.1-8B-Instruct \
  --local-dir-use-symlinks False

Configuration & Quantization

Quantization happens in two stages. First, unpack and convert the distributed weights into an uncompressed FP16 GGUF container. Second, quantize the weights into integer representations with targeted bit depths.

Step 1: Convert Safetensors to Baseline GGUF (FP16)

Run the conversion utility against the downloaded model directory:

python3 convert_hf_to_gguf.py ./models/Llama-3.1-8B-Instruct \
  --outfile ./models/llama-3.1-8b-fp16.gguf \
  --outtype f16

This outputs an intermediate file of around 16.1 GB. Do not delete this file yet. It serves as your clean master copy for generating multiple quantization levels.

Step 2: Choose the Target Quantization Scheme

Modern llama.cpp uses “k-quants” (k-quantization), applying mixed precision across different layers rather than truncating every weight uniformly. Critical layers—like token embeddings and attention outputs—receive higher bit precision than less sensitive feed-forward tensors.

  • Q8_0 (~8.5 GB): 99.9% perplexity retention. Choose this if you have 16 GB+ VRAM and need near-lossless output for fine-grained tasks like legal analysis.
  • Q5_K_M (~5.7 GB): Our recommended default for developer workstations. It preserves complex coding and step-by-step reasoning while shaving 65% off the original footprint.
  • Q4_K_M (~4.9 GB): The field standard for consumer cards. It fits comfortably on an 8 GB VRAM GPU, leaving roughly 3 GB free for an 8k context window.
  • Q3_K_M (~4.0 GB): Aggressive compression. It fits tight hardware budgets, but you will notice syntax errors in strict JSON generation and dropped function-calling parameters.

Step 3: Run llama-quantize

Pass the FP16 master file and your desired quantization target into the compiled quantizer:

# Generate a 4-bit medium quant
./build/bin/llama-quantize ./models/llama-3.1-8b-fp16.gguf \
  ./models/llama-3.1-8b-instruct-Q4_K_M.gguf Q4_K_M

# Generate a higher-precision 5-bit medium quant
./build/bin/llama-quantize ./models/llama-3.1-8b-fp16.gguf \
  ./models/llama-3.1-8b-instruct-Q5_K_M.gguf Q5_K_M

On a modern 12-core CPU like a Ryzen 9 7900X, the process takes under 90 seconds. Once the quant files finish building, you can safely remove the 16 GB FP16 file to free up local disk space.

Verification & Benchmarking

Never promote a newly quantized model straight into production without verifying token formatting and sustained memory stability.

1. Functional Sanity Checks

Confirm that the model handles instruction tags, stop tokens, and system prompts accurately:

./build/bin/llama-cli \
  -m ./models/llama-3.1-8b-instruct-Q4_K_M.gguf \
  -p "<|start_header_id|>system<|end_header_id|>
You are a Linux kernel engineer.<|eot_id|><|start_header_id|>user<|end_header_id|>
Explain why epoll scales better than select in two sentences.<|eot_id|><|start_header_id|>assistant<|end_header_id|>" \
  -n 128 \
  -ngl 99 \
  --temp 0.2

Watch the terminal log during initialization. The flag -ngl 99 tells the engine to push all layers to the GPU. Confirm that llm_load_tensors reports all 33 layers offloaded to CUDA or Metal.

2. Throughput Benchmarking with llama-bench

Skip eyeball estimates. Measure precise prompt processing (PP) and text generation (TG) speeds under controlled batch configurations:

./build/bin/llama-bench \
  -m ./models/llama-3.1-8b-instruct-Q4_K_M.gguf \
  -p 512,2048 \
  -n 128 \
  -ngl 99

On an RTX 4070 (12 GB VRAM), our baseline for Llama-3.1-8B-Instruct Q4_K_M sits at 2,140 tokens/sec for prompt ingestion (PP 512) and 104 tokens/sec for generation (TG 128). Total VRAM allocation holds steady at 5.4 GB, leaving ample room for multi-user concurrency.

3. Operational Gotchas to Monitor

A model that passes a quick test prompt can still stumble under sustained loads. Keep an eye on three real-world issues:

  • KV Cache Bloat: As conversations stretch toward 16k tokens, the KV cache alone can claim 2.5 GB of VRAM. If it spills over into system memory, your throughput will plummet from 100+ tokens/sec down to single digits. Consider enabling 8-bit KV cache quantization (-ctk q8_0 -ctv q8_0) to cut that overhead in half.
  • Perplexity Regression: If you quantize specialized fine-tunes (such as medical or SQL models), measure perplexity using ./build/bin/llama-perplexity against a domain validation set. Never assume a generic Q4_K_M will preserve niche terminologies without testing.
  • Thermal Throttling: Compact workstations with poor chassis airflow often shed 15% to 25% of their GPU clock speed during back-to-back batch jobs. Check your clocks and fan curves with nvidia-smi dmon.

Building your own quantization pipeline takes minimal effort, protects your data boundary, and guarantees that every model running on your company hardware matches your exact latency and accuracy specs.

Share: