Local LLM Optimization with llama.cpp - On-Device AI
Introduction
Running Large Language Models (LLMs) locally on Raspberry Pi opens up possibilities for privacy-focused AI applications, offline assistants, and edge computing without cloud dependencies. While Raspberry Pi can't match server-grade GPUs, modern optimizations make it viable for running smaller quantized models with acceptable inference speeds.
This comprehensive guide covers:
- llama.cpp Setup: Building and optimizing the fastest LLM inference engine
- Model Selection: Choosing appropriate quantized models for Raspberry Pi
- ARM NEON Optimization: Leveraging SIMD instructions for faster inference
- Memory Management: Handling limited RAM efficiently
- Quantization Strategies: GGUF format and optimal quantization levels
- Prompt Caching: Reducing latency for repeated queries
- Server Deployment: Running LLM APIs for applications
- Performance Benchmarking: Real-world speed and quality comparisons
Ideal use cases:
- Privacy-focused assistants: No data leaves your device
- Offline AI applications: Work without internet connectivity
- Edge computing: Embedded AI in IoT devices
- Development and testing: Prototype AI apps locally
- Educational projects: Learn about LLMs hands-on
- Home automation: Local voice/text command processing
Hardware Requirements
Recommended Configuration
| Component |
Minimum |
Recommended |
Optimal |
| Model |
Raspberry Pi 4 (4GB) |
Raspberry Pi 4 (8GB) |
Raspberry Pi 5 (8GB) |
| RAM |
4GB |
8GB |
8GB |
| Storage |
32GB SD card |
64GB+ SD card |
NVMe SSD via USB 3.0/PCIe |
| Cooling |
Passive heatsink |
Active fan |
ICE Tower cooler |
| Power |
3A power supply |
5A power supply |
Official 27W USB-C |
Raspberry Pi 5 (8GB) with Llama 2 7B Q4_K_M:
- First token: ~2-3 seconds
- Generation speed: 4-6 tokens/second
- Context length: 2048 tokens typical, 4096 possible
Raspberry Pi 4 (8GB) with Llama 2 7B Q4_K_M:
- First token: ~4-6 seconds
- Generation speed: 2-3 tokens/second
- Context length: 2048 tokens recommended
Raspberry Pi 4 (4GB) with TinyLlama 1.1B Q4_K_M:
- First token: ~1-2 seconds
- Generation speed: 8-12 tokens/second
- Context length: 2048 tokens
System Preparation
Update and Optimize OS
| # Update system
sudo apt update
sudo apt full-upgrade -y
# Install essential build tools
sudo apt install -y \
build-essential \
cmake \
git \
wget \
curl \
python3-pip \
python3-venv \
htop \
tmux
# Install ARM-specific optimizations
sudo apt install -y \
libomp-dev \
libopenblas-dev
|
| # Set CPU governor to performance
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Make it persistent
sudo tee /etc/rc.local <<EOF
#!/bin/bash
echo performance | tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
exit 0
EOF
sudo chmod +x /etc/rc.local
# Increase GPU memory (optional, helps with some operations)
# Edit /boot/firmware/config.txt and add:
# gpu_mem=128
# Disable swap (if using SSD)
sudo dmesg -n 1
sudo swapoff -a
# Or inspect/configure ZRAM for better performance on current Raspberry Pi OS
|
Optimize Memory
| # Verify active ZRAM swap space
zramctl
# If you need to increase ZRAM size to host larger models:
sudo nano /etc/systemd/zram-generator.conf
# Configure zram-size, e.g. "zram-size = 4096" for 4GB swap space
# Then reload systemd and restart the zram service:
sudo systemctl daemon-reload
sudo systemctl restart systemd-zram-setup@zram0
# (Legacy OS only) Manual swap adjustment using dphys-swapfile:
# sudo dphys-swapfile swapoff
# sudo nano /etc/dphys-swapfile # Modify CONF_SWAPSIZE=4096
# sudo dphys-swapfile setup
# sudo dphys-swapfile swapon
|
Installing llama.cpp
llama.cpp is the fastest, most optimized LLM inference engine for CPU/ARM.
Clone and Build
| # Clone repository
cd ~
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
# Build with ARM NEON optimizations
make -j$(nproc) LLAMA_OPENBLAS=1
# Verify build
./main --version
# Output: version: ... (built with OpenBLAS and ARM NEON)
|
Build Options Explained
| # Basic build (no optimizations)
make
# OpenBLAS acceleration (recommended for Pi)
make LLAMA_OPENBLAS=1
# For Raspberry Pi 5 - enable additional optimizations
make LLAMA_OPENBLAS=1 LLAMA_NATIVE=1
# Build with server support
make server LLAMA_OPENBLAS=1
# Clean and rebuild
make clean
make -j$(nproc) LLAMA_OPENBLAS=1
|
Verify NEON Support
| # Check CPU features
cat /proc/cpuinfo | grep -i neon
# Should show: neon vfpv4 ...
# Test llama.cpp SIMD support
./main --help | grep -i simd
|
Downloading Models
Understanding GGUF Quantization
GGUF (GPT-Generated Unified Format) supports various quantization levels:
| Quantization |
Size (7B) |
Quality |
Speed |
RAM Usage |
Recommended For |
| Q2_K |
~2.6GB |
Low |
Fastest |
~4GB |
Testing only |
| Q3_K_S |
~3.0GB |
Fair |
Fast |
~5GB |
Limited RAM |
| Q3_K_M |
~3.3GB |
Good |
Fast |
~5.5GB |
4GB Pi 4 |
| Q4_K_S |
~3.8GB |
Good |
Moderate |
~6GB |
Balance |
| Q4_K_M |
~4.1GB |
Very Good |
Moderate |
~6.5GB |
Recommended 8GB |
| Q5_K_S |
~4.6GB |
Excellent |
Slower |
~7GB |
Quality focus |
| Q5_K_M |
~4.8GB |
Excellent |
Slower |
~7.5GB |
8GB Pi 5 |
| Q6_K |
~5.5GB |
Near-original |
Slow |
~8GB |
Maximum quality |
| Q8_0 |
~7.2GB |
Original |
Very slow |
~10GB |
Not recommended |
Key:
- K variants: Better quality than plain Q4/Q5
- _S suffix: Smaller, faster
- _M suffix: Medium (best balance)
- _L suffix: Larger, higher quality
Recommended Models for Raspberry Pi
1. TinyLlama 1.1B (Best for Pi 4 4GB)
| cd ~/llama.cpp/models
# Download TinyLlama 1.1B Q4_K_M (~650MB)
wget https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf
# Test it
../main -m tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf \
-n 128 \
-p "Hello, how are you?"
|
2. Llama 2 7B (Best for Pi 5 8GB / Pi 4 8GB)
| # Download Llama 2 7B Chat Q4_K_M (~4.1GB)
wget https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf
# Test it
../main -m llama-2-7b-chat.Q4_K_M.gguf \
-n 128 \
-p "Explain quantum computing in simple terms:"
|
3. Mistral 7B (Excellent quality on Pi 5)
| # Download Mistral 7B Instruct Q4_K_M (~4.4GB)
wget https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf
# Test it
../main -m mistral-7b-instruct-v0.2.Q4_K_M.gguf \
-n 256 \
--temp 0.7 \
-p "<s>[INST] Write a Python function to calculate fibonacci numbers [/INST]"
|
4. Phi-2 2.7B (Great balance)
| # Download Phi-2 Q5_K_M (~1.9GB)
wget https://huggingface.co/TheBloke/phi-2-GGUF/resolve/main/phi-2.Q5_K_M.gguf
# Test it
../main -m phi-2.Q5_K_M.gguf \
-n 200 \
-p "Instruct: Explain the difference between RAM and ROM\nOutput:"
|
Batch Download Script
| #!/bin/bash
# download_models.sh
MODELS_DIR=~/llama.cpp/models
mkdir -p "$MODELS_DIR"
cd "$MODELS_DIR"
# TinyLlama 1.1B Q4_K_M
echo "Downloading TinyLlama 1.1B..."
wget -c https://huggingface.co/TheBloke/TinyLlama-1.1B-Chat-v1.0-GGUF/resolve/main/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf
# Llama 2 7B Q4_K_M
echo "Downloading Llama 2 7B..."
wget -c https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf
# Mistral 7B Q4_K_M
echo "Downloading Mistral 7B..."
wget -c https://huggingface.co/TheBloke/Mistral-7B-Instruct-v0.2-GGUF/resolve/main/mistral-7b-instruct-v0.2.Q4_K_M.gguf
echo "Download complete!"
ls -lh
|
Optimization Techniques
Command-Line Parameters
| # Basic inference with optimizations
./main -m models/llama-2-7b-chat.Q4_K_M.gguf \
-n 512 \ # Generate up to 512 tokens
-t 4 \ # Use 4 CPU threads
-c 2048 \ # Context size 2048
-b 512 \ # Batch size 512
--temp 0.7 \ # Temperature (creativity)
--top-k 40 \ # Top-k sampling
--top-p 0.9 \ # Top-p (nucleus) sampling
--repeat-penalty 1.1 \ # Reduce repetition
-p "Your prompt here"
|
Key Parameters Explained
| Parameter |
Description |
Recommended Value |
Impact |
-t, --threads |
CPU threads to use |
4 (Pi 5/4) |
Higher = faster (up to core count) |
-c, --ctx-size |
Context window size |
2048 |
Higher = more RAM, slower |
-b, --batch-size |
Prompt processing batch |
512 |
Higher = faster first token |
-n, --n-predict |
Max tokens to generate |
256-512 |
Stops generation after N tokens |
--temp |
Sampling temperature |
0.7-0.9 |
Lower = focused, higher = creative |
--top-k |
Top-k sampling |
40 |
Limits vocabulary to top K tokens |
--top-p |
Nucleus sampling |
0.9 |
Cumulative probability threshold |
--repeat-penalty |
Repetition penalty |
1.1-1.2 |
Reduces repetitive output |
--mirostat |
Mirostat sampling |
2 |
Alternative sampling (try it!) |
--mlock |
Lock model in RAM |
flag |
Prevents swapping (recommended) |
| # Maximum performance (Raspberry Pi 5)
./main -m models/mistral-7b-instruct-v0.2.Q4_K_M.gguf \
-t 4 \
-c 2048 \
-b 512 \
--mlock \
--no-mmap \
-n 256 \
-p "Explain neural networks"
# Memory-constrained (Raspberry Pi 4 4GB)
./main -m models/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf \
-t 4 \
-c 1024 \
-b 256 \
--mlock \
-n 128 \
-p "What is Python?"
# Interactive mode (chat)
./main -m models/llama-2-7b-chat.Q4_K_M.gguf \
-t 4 \
-c 2048 \
--color \
--interactive-first \
--reverse-prompt "User:" \
-p "User: Hello!\nAssistant:"
|
Prompt Caching
Enable prompt caching to reuse context across multiple queries:
| # Save prompt cache
./main -m models/llama-2-7b-chat.Q4_K_M.gguf \
--prompt-cache cache.bin \
--prompt-cache-all \
-p "You are a helpful coding assistant. Always provide complete, working code examples."
# Reuse cache (much faster second run!)
./main -m models/llama-2-7b-chat.Q4_K_M.gguf \
--prompt-cache cache.bin \
--prompt-cache-ro \
-p "Write a Python function to sort a list"
|
Running LLM Server
Deploy llama.cpp as an OpenAI-compatible API server.
Start Server
| # Build server if not already built
cd ~/llama.cpp
make server LLAMA_OPENBLAS=1
# Start server
./server \
-m models/llama-2-7b-chat.Q4_K_M.gguf \
--host 0.0.0.0 \
--port 8080 \
-c 2048 \
-t 4 \
--mlock
# Server will be available at http://raspberry-pi:8080
|
Test API
| # Health check
curl http://localhost:8080/health
# Generate completion
curl http://localhost:8080/completion \
-H "Content-Type: application/json" \
-d '{
"prompt": "Explain machine learning in simple terms:",
"n_predict": 128,
"temperature": 0.7,
"top_k": 40,
"top_p": 0.9
}'
# Chat completion (OpenAI-compatible)
curl http://localhost:8080/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What is Raspberry Pi?"}
],
"temperature": 0.7,
"max_tokens": 200
}'
|
Systemd Service
Create a persistent LLM service:
| sudo tee /etc/systemd/system/llama-server.service <<EOF
[Unit]
Description=llama.cpp LLM Server
After=network.target
[Service]
Type=simple
User=pi
WorkingDirectory=/home/pi/llama.cpp
ExecStart=/home/pi/llama.cpp/server \\
-m /home/pi/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf \\
--host 0.0.0.0 \\
--port 8080 \\
-c 2048 \\
-t 4 \\
--mlock
Restart=always
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF
# Enable and start
sudo systemctl daemon-reload
sudo systemctl enable llama-server
sudo systemctl start llama-server
# Check status
sudo systemctl status llama-server
# View logs
journalctl -u llama-server -f
|
Web UI Integration
| # Install Open WebUI (formerly Ollama WebUI)
pip3 install open-webui
# Run with llama.cpp backend
open-webui serve --backend llama-cpp \
--model-path ~/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf
# Or use text-generation-webui
git clone https://github.com/oobabooga/text-generation-webui
cd text-generation-webui
pip3 install -r requirements.txt
python3 server.py --model ~/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf
|
Python Integration
Using llama-cpp-python
| # Install Python bindings
pip3 install llama-cpp-python
# Or build with OpenBLAS support
CMAKE_ARGS="-DLLAMA_OPENBLAS=ON" pip3 install llama-cpp-python --force-reinstall --no-cache-dir
|
Basic Python Usage
| #!/usr/bin/env python3
# llm_example.py
from llama_cpp import Llama
# Load model
llm = Llama(
model_path="/home/pi/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf",
n_ctx=2048, # Context window
n_threads=4, # CPU threads
n_batch=512, # Batch size
verbose=False
)
# Generate text
output = llm(
"Explain quantum computing in simple terms:",
max_tokens=200,
temperature=0.7,
top_p=0.9,
top_k=40,
repeat_penalty=1.1,
stop=["User:", "\n\n"]
)
print(output['choices'][0]['text'])
|
Streaming Responses
| #!/usr/bin/env python3
# llm_stream.py
from llama_cpp import Llama
llm = Llama(
model_path="/home/pi/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf",
n_ctx=2048,
n_threads=4,
verbose=False
)
# Stream tokens as they're generated
for chunk in llm(
"Write a short story about a robot:",
max_tokens=300,
temperature=0.8,
stream=True
):
token = chunk['choices'][0]['text']
print(token, end='', flush=True)
print() # Newline at end
|
Chat Application
| #!/usr/bin/env python3
# llm_chat.py
from llama_cpp import Llama
import sys
# Initialize model
llm = Llama(
model_path="/home/pi/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf",
n_ctx=2048,
n_threads=4,
n_batch=512,
verbose=False
)
# Chat loop
conversation_history = []
print("LLM Chat (type 'exit' to quit)")
print("-" * 50)
while True:
user_input = input("\nYou: ").strip()
if user_input.lower() in ['exit', 'quit', 'q']:
break
if not user_input:
continue
# Build prompt with history
prompt = ""
for msg in conversation_history:
prompt += f"{msg['role']}: {msg['content']}\n"
prompt += f"User: {user_input}\nAssistant:"
# Generate response
print("Assistant: ", end='', flush=True)
response = ""
for chunk in llm(
prompt,
max_tokens=256,
temperature=0.7,
top_p=0.9,
stop=["User:", "\nUser:"],
stream=True
):
token = chunk['choices'][0]['text']
print(token, end='', flush=True)
response += token
print()
# Update history
conversation_history.append({"role": "User", "content": user_input})
conversation_history.append({"role": "Assistant", "content": response.strip()})
# Keep only last 10 messages (memory management)
if len(conversation_history) > 20:
conversation_history = conversation_history[-20:]
|
Run it:
Advanced Use Cases
Case 1: Code Assistant
| #!/usr/bin/env python3
# code_assistant.py
from llama_cpp import Llama
llm = Llama(
model_path="/home/pi/llama.cpp/models/mistral-7b-instruct-v0.2.Q4_K_M.gguf",
n_ctx=4096,
n_threads=4,
verbose=False
)
def generate_code(task):
prompt = f"""<s>[INST] You are an expert programmer. Write clean, well-commented code.
Task: {task}
Provide only the code with brief comments. [/INST]"""
output = llm(
prompt,
max_tokens=512,
temperature=0.3, # Lower temperature for code
top_p=0.95,
stop=["</s>", "[INST]"]
)
return output['choices'][0]['text']
# Example usage
code = generate_code("Python function to implement binary search")
print(code)
|
Case 2: Document Summarization
| #!/usr/bin/env python3
# summarize.py
from llama_cpp import Llama
import sys
llm = Llama(
model_path="/home/pi/llama.cpp/models/llama-2-7b-chat.Q4_K_M.gguf",
n_ctx=4096,
n_threads=4
)
def summarize_text(text, max_length=200):
prompt = f"""[INST] Summarize the following text concisely:
{text}
Summary: [/INST]"""
output = llm(
prompt,
max_tokens=max_length,
temperature=0.5,
top_p=0.9
)
return output['choices'][0]['text'].strip()
# Read from file or stdin
if len(sys.argv) > 1:
with open(sys.argv[1], 'r') as f:
document = f.read()
else:
document = sys.stdin.read()
summary = summarize_text(document)
print("Summary:")
print(summary)
|
Usage:
| python3 summarize.py document.txt
# Or
cat article.txt | python3 summarize.py
|
Case 3: Home Automation Voice Control
| #!/usr/bin/env python3
# voice_control.py
from llama_cpp import Llama
import json
import subprocess
llm = Llama(
model_path="/home/pi/llama.cpp/models/phi-2.Q5_K_M.gguf",
n_ctx=1024,
n_threads=4,
verbose=False
)
def extract_command(voice_input):
prompt = f"""Extract smart home command from user input.
Output JSON format: action (turn_on/turn_off/dim/set), device (light/fan/etc), value (number)
User: {voice_input}
JSON:"""
output = llm(
prompt,
max_tokens=100,
temperature=0.1,
stop=["\n", "User:"]
)
try:
return json.loads(output['choices'][0]['text'])
except:
return None
def execute_command(cmd):
if not cmd:
return "Sorry, I didn't understand that."
# Example: Home Assistant API or GPIO control
# This is a stub - replace with actual smart home integration
action = cmd.get('action')
device = cmd.get('device')
value = cmd.get('value', 100)
print(f"Executing: {action} {device} (value: {value})")
# Example GPIO control for LED
if device == "light" and action == "turn_on":
subprocess.run(["gpio", "write", "17", "1"])
return "Light turned on"
elif device == "light" and action == "turn_off":
subprocess.run(["gpio", "write", "17", "0"])
return "Light turned off"
return f"{action.replace('_', ' ').title()} {device}"
# Test
commands = [
"Turn on the living room light",
"Dim bedroom light to 50%",
"Turn off all lights"
]
for cmd_text in commands:
print(f"\nInput: {cmd_text}")
cmd = extract_command(cmd_text)
result = execute_command(cmd)
print(f"Result: {result}")
|
Case 4: Raspberry Pi Cluster Inference
Distribute inference across multiple Raspberry Pis:
| #!/usr/bin/env python3
# distributed_llm.py
import requests
import json
from concurrent.futures import ThreadPoolExecutor
# Multiple Pi nodes running llama.cpp server
NODES = [
"http://pi1.local:8080",
"http://pi2.local:8080",
"http://pi3.local:8080"
]
def query_node(node, prompt, max_tokens=100):
try:
response = requests.post(
f"{node}/completion",
json={
"prompt": prompt,
"n_predict": max_tokens,
"temperature": 0.7
},
timeout=30
)
return response.json()['content']
except Exception as e:
return f"Error from {node}: {e}"
def distributed_inference(prompt, max_tokens=100):
"""Query all nodes and return fastest response"""
with ThreadPoolExecutor(max_workers=len(NODES)) as executor:
futures = [
executor.submit(query_node, node, prompt, max_tokens)
for node in NODES
]
# Return first completed response
for future in futures:
if future.done():
return future.result()
# Test
result = distributed_inference("Explain Raspberry Pi clusters:")
print(result)
|
Benchmark Script
| #!/bin/bash
# benchmark_llm.sh
MODEL="models/llama-2-7b-chat.Q4_K_M.gguf"
PROMPT="Explain artificial intelligence in simple terms:"
echo "=== LLM Benchmark on $(hostname) ==="
echo "Model: $MODEL"
echo "Prompt: $PROMPT"
echo ""
# Measure inference time
echo "Running benchmark..."
TIME_START=$(date +%s)
./main -m "$MODEL" \
-p "$PROMPT" \
-n 128 \
-t 4 \
-c 2048 \
--mlock \
2>&1 | tee benchmark_output.txt
TIME_END=$(date +%s)
DURATION=$((TIME_END - TIME_START))
# Extract stats
TOKENS=$(grep "generated" benchmark_output.txt | awk '{print $1}')
SPEED=$(grep "tokens per second" benchmark_output.txt | awk '{print $1}')
echo ""
echo "=== Results ==="
echo "Total time: ${DURATION}s"
echo "Tokens generated: $TOKENS"
echo "Speed: $SPEED tokens/second"
echo ""
# CPU temp
echo "Final CPU temp: $(vcgencmd measure_temp)"
# Cleanup
rm benchmark_output.txt
|
Comparison Results (Real-World Tests)
Model: Llama 2 7B Q4_K_M
| Device |
First Token |
Tokens/sec |
RAM Used |
Temp (°C) |
| Raspberry Pi 5 (8GB) |
2.1s |
5.2 t/s |
6.2GB |
65°C |
| Raspberry Pi 4 (8GB) |
4.8s |
2.7 t/s |
6.4GB |
72°C |
| Raspberry Pi 4 (4GB) |
OOM |
N/A |
N/A |
N/A |
Model: TinyLlama 1.1B Q4_K_M
| Device |
First Token |
Tokens/sec |
RAM Used |
Temp (°C) |
| Raspberry Pi 5 (8GB) |
0.6s |
11.8 t/s |
1.8GB |
58°C |
| Raspberry Pi 4 (8GB) |
1.2s |
8.4 t/s |
1.9GB |
64°C |
| Raspberry Pi 4 (4GB) |
1.3s |
7.9 t/s |
2.0GB |
66°C |
Model: Mistral 7B Instruct Q4_K_M
| Device |
First Token |
Tokens/sec |
RAM Used |
Temp (°C) |
| Raspberry Pi 5 (8GB) |
2.3s |
4.9 t/s |
6.5GB |
67°C |
| Raspberry Pi 4 (8GB) |
5.1s |
2.5 t/s |
6.7GB |
74°C |
Quality vs Speed Trade-offs
| # Test different quantization levels
for quant in Q2_K Q3_K_M Q4_K_M Q5_K_M; do
echo "Testing $quant..."
time ./main -m models/llama-2-7b-chat.$quant.gguf \
-p "Explain photosynthesis:" \
-n 128 -t 4
done
# Record your measured results; do not reuse numbers from a different Pi,
# model build, context size, cooling solution, or llama.cpp revision.
# Quantization | Prompt t/s | Generation t/s | Max temp | Throttled
|
Troubleshooting
Issue: Out of Memory (OOM)
| # Reduce context size
./main -m model.gguf -c 1024 -b 256 ...
# Use smaller quantization
# Switch from Q5_K_M to Q4_K_M or Q3_K_M
# Optimize or increase ZRAM on current Raspberry Pi OS
sudo nano /etc/systemd/zram-generator.conf
# Configure: zram-size = 4096
sudo systemctl daemon-reload && sudo systemctl restart systemd-zram-setup@zram0
# Enable swap (Legacy OS using dphys-swapfile)
# sudo dphys-swapfile swapoff
# sudo sed -i 's/CONF_SWAPSIZE=.*/CONF_SWAPSIZE=4096/' /etc/dphys-swapfile
# sudo dphys-swapfile setup
# sudo dphys-swapfile swapon
|
Issue: Slow Inference
| # Check CPU frequency
cat /sys/devices/system/cpu/cpu0/cpufreq/scaling_cur_freq
# Enable performance mode
echo performance | sudo tee /sys/devices/system/cpu/cpu*/cpufreq/scaling_governor
# Rebuild with optimizations
make clean
make -j4 LLAMA_OPENBLAS=1 LLAMA_NATIVE=1
# Reduce batch size if RAM-limited
./main -m model.gguf -b 256 ...
# Use faster model
# Switch to TinyLlama or Phi-2
|
Issue: Server Crashes
| # Check logs
journalctl -u llama-server -n 100
# Increase file descriptor limits
echo "pi soft nofile 65536" | sudo tee -a /etc/security/limits.conf
echo "pi hard nofile 65536" | sudo tee -a /etc/security/limits.conf
# Restart server with lower concurrency
./server -m model.gguf --parallel 1
# Monitor memory
watch -n 1 free -h
|
Issue: Model Loading Fails
| # Verify model integrity
sha256sum models/*.gguf
# Check disk space
df -h
# Redownload model
wget -c <model_url>
# Test with verbose output
./main -m model.gguf -p "test" -n 10 --verbose
|
Best Practices
1. Choose Right Model for Hardware
| # Raspberry Pi 4 (4GB): TinyLlama, Phi-2
# Raspberry Pi 4 (8GB): Llama 2 7B Q4_K_M, Mistral 7B Q4_K_M
# Raspberry Pi 5 (8GB): Mistral 7B Q5_K_M, Llama 2 7B Q5_K_M
|
2. Monitor Temperature
| # Install monitoring
sudo apt install stress
# Monitor during inference
watch -n 1 'vcgencmd measure_temp && vcgencmd measure_clock arm'
# Throttling check
vcgencmd get_throttled
# 0x0 = no throttling (good)
# 0x50000 = throttled (add cooling!)
|
3. Use SSD for Models
| # Move models to SSD
sudo mkdir /mnt/ssd
sudo mount /dev/sda1 /mnt/ssd
mv ~/llama.cpp/models/* /mnt/ssd/llm-models/
ln -s /mnt/ssd/llm-models ~/llama.cpp/models
# Add to /etc/fstab for persistence
|
4. Optimize Prompts
| # Bad: Vague, long
prompt = "Can you please help me understand what machine learning is and how it works in detail with examples?"
# Good: Clear, concise
prompt = "Explain machine learning in 3 sentences with one example."
# Best: Structured for model
prompt = "<s>[INST] Define machine learning. Provide 1 example. Max 50 words. [/INST]"
|
5. Implement Request Queuing
| # queue_llm.py
from queue import Queue
from threading import Thread
from llama_cpp import Llama
request_queue = Queue()
llm = Llama(model_path="model.gguf", n_threads=4)
def worker():
while True:
prompt, callback = request_queue.get()
response = llm(prompt, max_tokens=200)
callback(response['choices'][0]['text'])
request_queue.task_done()
Thread(target=worker, daemon=True).start()
# Submit requests
def submit(prompt):
future = Queue()
request_queue.put((prompt, future.put))
return future.get()
|
Complete Example: Personal AI Assistant
| #!/usr/bin/env python3
# pi_assistant.py - Complete local AI assistant
from llama_cpp import Llama
import speech_recognition as sr
import pyttsx3
from datetime import datetime
import subprocess
class PiAssistant:
def __init__(self, model_path):
print("Loading LLM...")
self.llm = Llama(
model_path=model_path,
n_ctx=2048,
n_threads=4,
n_batch=512,
verbose=False
)
self.tts = pyttsx3.init()
self.tts.setProperty('rate', 150)
self.recognizer = sr.Recognizer()
self.conversation = []
print("Assistant ready!")
def listen(self):
"""Listen for voice input"""
with sr.Microphone() as source:
print("Listening...")
self.recognizer.adjust_for_ambient_noise(source)
audio = self.recognizer.listen(source)
try:
text = self.recognizer.recognize_google(audio)
print(f"You said: {text}")
return text
except sr.UnknownValueError:
return None
except sr.RequestError:
print("Speech recognition unavailable, type instead:")
return input("> ")
def speak(self, text):
"""Text-to-speech output"""
print(f"Assistant: {text}")
self.tts.say(text)
self.tts.runAndWait()
def query_llm(self, prompt):
"""Query LLM with conversation context"""
# Build context
context = "\n".join([
f"{msg['role']}: {msg['text']}"
for msg in self.conversation[-6:] # Last 3 exchanges
])
full_prompt = f"""{context}
User: {prompt}
Assistant:"""
# Generate response
output = self.llm(
full_prompt,
max_tokens=150,
temperature=0.7,
top_p=0.9,
stop=["User:", "\n\n"]
)
response = output['choices'][0]['text'].strip()
# Update conversation
self.conversation.append({"role": "User", "text": prompt})
self.conversation.append({"role": "Assistant", "text": response})
return response
def execute_command(self, text):
"""Handle system commands"""
text_lower = text.lower()
if "what time" in text_lower:
return f"It's {datetime.now().strftime('%I:%M %p')}"
elif "temperature" in text_lower or "cpu temp" in text_lower:
temp = subprocess.check_output(["vcgencmd", "measure_temp"])
return f"CPU temperature is {temp.decode().split('=')[1]}"
elif "reboot" in text_lower or "restart" in text_lower:
return "I can't restart the system for safety reasons."
return None
def run(self):
"""Main assistant loop"""
self.speak("Hello! I'm your Raspberry Pi assistant. How can I help?")
while True:
# Listen for input
user_input = self.listen()
if not user_input:
continue
if "goodbye" in user_input.lower() or "exit" in user_input.lower():
self.speak("Goodbye!")
break
# Check for system commands first
cmd_response = self.execute_command(user_input)
if cmd_response:
self.speak(cmd_response)
continue
# Query LLM
response = self.query_llm(user_input)
self.speak(response)
if __name__ == "__main__":
# Install dependencies first:
# pip3 install SpeechRecognition pyttsx3 pyaudio llama-cpp-python
assistant = PiAssistant(
model_path="/home/pi/llama.cpp/models/tinyllama-1.1b-chat-v1.0.Q4_K_M.gguf"
)
assistant.run()
|
Install dependencies:
| pip3 install SpeechRecognition pyttsx3 pyaudio llama-cpp-python
sudo apt install espeak portaudio19-dev
|
Run:
Summary
This guide covered comprehensive local LLM deployment on Raspberry Pi:
✅ Setup & Installation
- llama.cpp compilation with ARM NEON optimizations
- OpenBLAS integration for faster inference
- System optimization (CPU governor, memory, cooling)
✅ Model Selection
- GGUF quantization formats (Q2_K through Q8_0)
- Recommended models for each Pi variant
- Quality vs speed trade-offs
✅ Optimization Techniques
- Command-line parameter tuning
- Prompt caching for repeated queries
- Memory management strategies
- Thread and batch size optimization
✅ Deployment
- LLM server with OpenAI-compatible API
- Systemd service configuration
- Python integration (llama-cpp-python)
- Web UI options
✅ Real-World Applications
- Code generation assistant
- Document summarization
- Voice-controlled home automation
- Distributed inference across Pi cluster
- Complete AI assistant with speech
- Measure prompt processing and generation separately
- Record the exact model, quantisation, context, build revision, cooling, and power mode
- Publish temperature and throttling state with every result
- Compare results only when the full test configuration matches
Next Steps
Further Optimization:
- Raspberry Pi Overclocking - Push performance limits safely
- Custom Quantization - Create your own GGUF models
- Model Fine-tuning - Specialize models for your use case
- GPU Acceleration - Experimental Vulkan/OpenCL support
- Edge AI Frameworks - Integrate with TensorFlow Lite, ONNX
Related Guides:
With local LLM inference, your Raspberry Pi becomes a privacy-focused AI powerhouse—no cloud required!
Frequently Asked Questions
Should I use llama.cpp or AI HAT+ 2?
Use llama.cpp when model portability and CPU-only operation matter. Use AI HAT+ 2 when a supported Hailo model, lower host CPU load, or a combined LLM/VLM pipeline justifies dedicated hardware.
Why are my token-per-second results different from another guide?
Model revision, quantisation, context length, prompt length, thread count, llama.cpp build, cooling, and throttling all change performance. Compare only fully documented runs.
Can swap make a model faster?
No. Swap can prevent an out-of-memory failure, but storage is far slower than RAM. Choose a smaller model or context before relying on swap.