How to Run llama.cpp as a Production OpenAI-Compatible Server

Updated on Oct 16, 2026
Mila H
13 MINS READ
Table of Contents
llama.cpp and Speculative Decoding

Running a large language model does not need a big cluster. With a single NVIDIA GPU, a quantized GGUF model, and llama-server, you can serve an OpenAI-compatible API for your apps, tools, and team. 

In this guide, you will set up llama.cpp speculative decoding on a real server and make it faster without buying more hardware. If you want a fast start, you can also look at AI hosting options built for this kind of work.

Prerequisites

Before you start, make sure you have the items below. They are all simple, and you can set most of them up in a few minutes:

  • A Linux server with Ubuntu 24.04 or newer and sudo access.
  • One NVIDIA GPU. 12 GB of VRAM or more is a comfortable size for the models in this guide.
  • At least 30 GB of free disk space.
  • A domain name that points to your server. In this guide, we use llm.example.com.

If you do not have a GPU machine yet, a dedicated GPU server for LLM inference gives you full access to the card, so you can run all the steps below as they are.

Step 1: Check the NVIDIA Driver

llama.cpp needs a working NVIDIA driver to use your GPU. In this step, you must check that the driver is installed and that the system can see your card. Run the command below:

Bash
nvidia-smi

You should see your GPU name, the driver version, and the CUDA version. If the command is not found, you can install a driver with the commands below:

Bash
sudo apt updatesudo ubuntu-drivers installsudo reboot

After the reboot, run nvidia-smi again to confirm that the GPU shows up.

Step 2: Install the CUDA Toolkit

The driver is enough to run models, but you need the CUDA toolkit to build llama.cpp. Run the commands below to add the NVIDIA repository and install the toolkit:

Bash
cd /tmpwget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.debsudo dpkg -i cuda-keyring_1.1-1_all.debsudo apt updatesudo apt install cuda-toolkit -y

Then, add CUDA to your path and check that the compiler works:

Bash
echo 'export PATH=/usr/local/cuda/bin:$PATH' >> ~/.bashrcsource ~/.bashrcnvcc --version

Step 3: Build llama.cpp with CUDA

Now you must install the build tools. libssl-dev is needed if you want HTTPS features such as downloading models from Hugging Face with -hf:

Bash
sudo apt install build-essential cmake git ccache libssl-dev curl jq -y

Then, download the source code. We use the v0.6.0 tag, which is the latest stable release at the time of writing.

Bash
cd ~git clone --branch v0.6.0 --depth 1 https://github.com/ggml-org/llama.cppcd llama.cpp

Use the commands below to configure and build with CUDA turned on:

Bash
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Releasecmake --build build --config Release -j $(nproc)

The build can take 10 to 20 minutes. If CMake cannot detect your GPU, you must set the architecture manually. For example, use 86 for RTX 30 series cards or 89 for RTX 40 series cards:

Bash
cmake -B build -DGGML_CUDA=ON -DCMAKE_BUILD_TYPE=Release -DCMAKE_CUDA_ARCHITECTURES="89"

Check the build and confirm that llama.cpp can see your GPU:

Bash
./build/bin/llama-server --version./build/bin/llama-server --list-devices

Now copy the binaries to a fixed folder. We will use this path in the service file later:

Bash
sudo mkdir -p /opt/llama.cppsudo cp -r build/bin /opt/llama.cpp/ls /opt/llama.cpp/bin | head

Step 4: Download the GGUF Models

In this step, you must download the two model files that the server will use. We use two models from the same family. The big one answers your users. The small one is the draft model that we add in Step 9.

  • Main model: Qwen3 8B in Q4_K_M (about 5 GB).
  • Draft model: Qwen3 0.6B in Q8_0 (about 0.6 GB).

Create a folder and download both files with the commands below:

Bash
sudo mkdir -p /opt/llama-modelssudo chown $USER:$USER /opt/llama-modelscd /opt/llama-models curl -L -O https://huggingface.co/Qwen/Qwen3-8B-GGUF/resolve/main/Qwen3-8B-Q4_K_M.ggufcurl -L -O https://huggingface.co/Qwen/Qwen3-0.6B-GGUF/resolve/main/Qwen3-0.6B-Q8_0.gguf ls -lh /opt/llama-models

A draft model must use the same tokenizer as the main model. Models from the same family, like Qwen3 8B and Qwen3 0.6B, match. Mixing different families will fail or give poor results.

Step 5: Create an API Key

Now you must create an API key to protect your server. Only people who send this key can use your models, so others cannot use your GPU.

Create a folder for the config files and generate a strong key:

Bash
sudo mkdir -p /etc/llamaopenssl rand -hex 32 | sudo tee /etc/llama/api-keys.txtsudo chmod 640 /etc/llama/api-keys.txt

Create a service user. It will run the server and read the key file:

Bash
sudo useradd --system --no-create-home --shell /usr/sbin/nologin llamasudo chown root:llama /etc/llama/api-keys.txt

Save the key in your shell so you can use it in tests:

Bash
export LLAMA_API_KEY=$(sudo cat /etc/llama/api-keys.txt)

The key file can hold many keys, one per line. Lines that start with # are comments.

Step 6: Start the Server and Test It

For the first time, you must start the server manually. This is a baseline run, without a draft model:

Bash
/opt/llama.cpp/bin/llama-server \  -m /opt/llama-models/Qwen3-8B-Q4_K_M.gguf \  --alias qwen3-8b \  --host 127.0.0.1 --port 8080 \  -ngl all \  -fa on \  -c 16384 \  -np 4 \  --api-key-file /etc/llama/api-keys.txt \  --metrics \  --no-webui

Here is what each option does:

Option What it does
-m Path to the GGUF model file.
--alias The model name that clients use in API calls.
--host 127.0.0.1 Listen on localhost only. Nginx will handle public traffic.
-ngl all Put all model layers in GPU memory.
-fa on Turn on Flash Attention. It saves memory and is faster with long context.
-c 16384 Total context size in tokens.
-np 4 Number of parallel slots, so 4 requests can run at the same time.
--api-key-file Read allowed API keys from a file.
--metrics Turn on the Prometheus /metrics endpoint.
--no-webui Turn off the built-in web page.

Wait until the log says the server is listening. Then open a second terminal and test:

Bash
curl -s http://127.0.0.1:8080/health curl -s http://127.0.0.1:8080/v1/models \  -H "Authorization: Bearer $LLAMA_API_KEY" | jq curl -s http://127.0.0.1:8080/v1/chat/completions \  -H "Content-Type: application/json" \  -H "Authorization: Bearer $LLAMA_API_KEY" \  -d '{    "model": "qwen3-8b",    "messages": [{"role": "user", "content": "Say hello in one short sentence."}],    "max_tokens": 100  }' | jq

Then, test that the key is really needed. This call has no key and should return a 401 error:

Bash
curl -i http://127.0.0.1:8080/v1/models

Step 7: Set Parallel Requests and Context Size

These two settings decide how many users you can serve and how much VRAM you need.

  • -np sets the number of slots. Each slot handles one request at once.
  • -c sets the total context. It is shared by the slots. With -c 16384 and -np 4, plan for about 4096 tokens per request.

You can pick the numbers like this:

  1. Decide how many users will call the API at the same time. Set -np to that number.
  2. Decide the longest prompt plus answer you need. Multiply it by -np. Set -c to the result.
  3. Watch VRAM with nvidia-smi. If you run out of memory, lower -c or -np.

If VRAM is tight, you can store the KV cache in 8-bit. This cuts the cache size by about half compared to the default 16-bit. Add these options:

Bash
-ctk q8_0 -ctv q8_0

Keep Flash Attention on (-fa on) when you use a quantized KV cache. Test the quality of your answers after this change.

You can see what each slot is doing at any time with the command below:

Bash
curl -s http://127.0.0.1:8080/slots -H "Authorization: Bearer $LLAMA_API_KEY" | jq

Step 8: Run a Baseline Benchmark

Before we turn on llama.cpp speculative decoding, we need a baseline. You can create the benchmark script with:

Bash
nano ~/bench.py

Paste this code, then save the file (Ctrl+O, Enter, Ctrl+X):

Bash
import json, os, time, urllib.requestfrom concurrent.futures import ThreadPoolExecutor URL = os.environ.get("LLAMA_URL", "http://127.0.0.1:8080") + "/v1/chat/completions"KEY = os.environ["LLAMA_API_KEY"]MODEL = os.environ.get("LLAMA_MODEL", "qwen3-8b") PROMPTS = [    "Write a Python function that reads a CSV file and prints the average of each numeric column.",    "Explain how DNS works, step by step, in simple words.",    "Write a bash script that backs up a folder to a tar.gz file with the date in the name.",    "Explain the difference between a process and a thread.",] def ask(prompt):    body = json.dumps({        "model": MODEL,        "messages": [{"role": "user", "content": prompt}],        "max_tokens": 256,        "temperature": 0,    }).encode()    req = urllib.request.Request(        URL, data=body,        headers={"Content-Type": "application/json",                 "Authorization": "Bearer " + KEY},    )    with urllib.request.urlopen(req, timeout=600) as r:        return json.load(r)["usage"]["completion_tokens"] def run(concurrency, total):    prompts = [PROMPTS[i % len(PROMPTS)] for i in range(total)]    start = time.time()    with ThreadPoolExecutor(max_workers=concurrency) as pool:        tokens = sum(pool.map(ask, prompts))    secs = time.time() - start    print(f"concurrency={concurrency} requests={total} tokens={tokens} "          f"time={secs:.1f}s speed={tokens / secs:.1f} tok/s") ask(PROMPTS[0])for c in (1, 2, 4):    run(c, 8)

The script uses temperature 0, so both runs with and without a draft model. This makes the comparison fair.

Run it and, in another terminal, watch GPU memory:

Bash
python3 ~/bench.py #in another terminal run:nvidia-smi --query-gpu=memory.used,memory.total --format=csv -l 2

Write down the tok/s for each concurrency level and the highest memory.used value. Then stop the server with Ctrl+C.

Also, you can measure model speed without the server. llama-bench does this:

Bash
/opt/llama.cpp/bin/llama-bench -m /opt/llama-models/Qwen3-8B-Q4_K_M.gguf -ngl 99 -p 512 -n 128

It reports prompt processing (pp) and text generation (tg) in tokens per second. Note that llama-bench does not test speculative decoding. Use the server script above for that.

Step 9: Enable llama.cpp Speculative Decoding with a Draft Model

Text generation is slow because the model creates one token at once. With llama.cpp speculative decoding, a small draft model guesses the next few tokens fast. Then, the big model checks all the guesses in one pass. Correct guesses are kept, so you get several tokens for the price of one big step. Wrong guesses are thrown away, so the output stays the same as the big model would give.

Start the server again with the draft model added:

Bash
/opt/llama.cpp/bin/llama-server \  -m /opt/llama-models/Qwen3-8B-Q4_K_M.gguf \  --alias qwen3-8b \  --host 127.0.0.1 --port 8080 \  -ngl all \  -fa on \  -c 16384 \  -np 4 \  --spec-type draft-simple \  --spec-draft-model /opt/llama-models/Qwen3-0.6B-Q8_0.gguf \  --spec-draft-ngl all \  --spec-draft-n-max 8 \  --api-key-file /etc/llama/api-keys.txt \  --metrics \  --no-webui

These are the new options:

Option What it does
--spec-type draft-simple Use a small draft model for speculation.
--spec-draft-model Path to the draft GGUF file.
--spec-draft-ngl all Put the draft model fully on the GPU.
--spec-draft-n-max 8 Draft up to 8 tokens per step. The default is 3.

Old guides use --model-draft, -md, and --draft-max. In current llama.cpp, --draft-max and --draft-min were removed. You can use --spec-draft-n-max and --spec-draft-n-min instead. The -md short form still works. If you are unsure about an option on your build, run llama-server --help | grep spec.

Run the same benchmark again:

Bash
python3 ~/bench.py

Now look at the server log. After each request, llama-server prints the draft acceptance rate, for example draft acceptance rate = 0.70312 ( 90 accepted / 128 generated). A higher rate means more speed. If the rate is low, the draft model is a poor match for your prompts.

Try a few values for --spec-draft-n-max, such as 4, 8, and 16. Pick the one that gives the best tok/s in your tests. Gains depend on your prompts, your GPU, and the model pair. Code and structured text usually gain more than free-form creative writing.

Step 10: Compare Speed and Memory

Use this table to judge if llama.cpp speculative decoding helps your load. Fill it in with your own numbers from Step 8 and Step 9:

Test Without draft With draft
Speed, 1 request (tok/s) your number your number
Speed, 2 requests (tok/s) your number your number
Speed, 4 requests (tok/s) your number your number
Peak VRAM (MiB) your number your number
Draft acceptance rate not used from the log

Expect VRAM to go up a little, because the draft model and its own KV cache also live on the GPU. For the models in this guide, the draft weights are about 0.6 GB. Expect the biggest gain with 1 or 2 requests. When many requests run at once, the GPU is already busy, so the gain gets smaller. If speculation does not help at your load, you can remove the draft options.

Step 11: Run llama-server as a systemd Service

Stop the manual server with Ctrl+C. Then, create the service file. It keeps llama.cpp speculative decoding on after every reboot:

Bash
sudo nano /etc/systemd/system/llama-server.service

Add the following content to the file:

Bash
[Unit]Description=llama.cpp OpenAI-compatible serverAfter=network-online.targetWants=network-online.target [Service]Type=simpleUser=llamaGroup=llamaWorkingDirectory=/opt/llama.cppExecStart=/opt/llama.cpp/bin/llama-server \  -m /opt/llama-models/Qwen3-8B-Q4_K_M.gguf \  --alias qwen3-8b \  --host 127.0.0.1 --port 8080 \  -ngl all \  -fa on \  -c 16384 \  -np 4 \  --spec-type draft-simple \  --spec-draft-model /opt/llama-models/Qwen3-0.6B-Q8_0.gguf \  --spec-draft-ngl all \  --spec-draft-n-max 8 \  --api-key-file /etc/llama/api-keys.txt \  --metrics \  --no-webuiRestart=alwaysRestartSec=5LimitNOFILE=65535 [Install]WantedBy=multi-user.target

Make the model files readable by the service user, start the service, then watch the logs:

Bash
sudo chmod -R a+rX /opt/llama-models /opt/llama.cppsudo systemctl daemon-reloadsudo systemctl enable --now llama-serversudo systemctl status llama-server --no-pagersudo journalctl -u llama-server -f

To change any option later, edit the file, then run sudo systemctl daemon-reload and sudo systemctl restart llama-server.

Step 12: Add Nginx Reverse Proxy with HTTPS

Never open port 8080 to the internet. Keep the server on 127.0.0.1 and let Nginx handle outside traffic. For this purpose, install Nginx:

Bash
sudo apt install nginx -y

Create the site file with your desired text editor:

Bash
sudo nano /etc/nginx/sites-available/llm.example.com

Paste this content. Replace llm.example.com with your domain:

Bash
limit_req_zone $binary_remote_addr zone=llm_limit:10m rate=10r/s; server {    listen 80;    server_name llm.example.com;     client_max_body_size 10m;     # Hide admin and monitoring endpoints from the internet    location ~ ^/(metrics|slots|props) {        return 403;    }     location / {        limit_req zone=llm_limit burst=20 nodelay;         proxy_pass http://127.0.0.1:8080;        proxy_http_version 1.1;        proxy_set_header Host $host;        proxy_set_header X-Real-IP $remote_addr;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;        proxy_set_header X-Forwarded-Proto $scheme;        proxy_set_header Connection "";         # Needed for streaming answers        proxy_buffering off;        proxy_cache off;         proxy_read_timeout 3600s;        proxy_send_timeout 3600s;    }}

Enable the site and reload Nginx with the following commands:

Bash
sudo ln -s /etc/nginx/sites-available/llm.example.com /etc/nginx/sites-enabled/sudo nginx -tsudo systemctl reload nginx

Now you must obtain a free TLS certificate with Certbot. Install Certbot and get your certificates with the commands below:

Bash
sudo apt install certbot python3-certbot-nginx -ysudo certbot --nginx -d llm.example.com

Then, open only the ports you need in the UFW firewall:

Bash
sudo ufw allow OpenSSHsudo ufw allow 'Nginx Full'sudo ufw enablesudo ufw status

At this point, you have three layers of control, including the firewall, the Nginx rate limit, and the API key.

Step 13: Test the Final Setup

Now you must call the API through your domain. A request with the key must work:

Bash
curl -s https://llm.example.com/v1/chat/completions \  -H "Content-Type: application/json" \  -H "Authorization: Bearer $LLAMA_API_KEY" \  -d '{    "model": "qwen3-8b",    "messages": [{"role": "user", "content": "What is speculative decoding in one sentence?"}],    "max_tokens": 100  }' | jq -r '.choices[0].message.content'

A request without the key must fail, and the hidden endpoints must be blocked:

Bash
curl -i https://llm.example.com/v1/modelscurl -i https://llm.example.com/metrics

You can use the server with any OpenAI client. Set the base URL to https://llm.example.com/v1 and use your key. Here is an example with the Python SDK:

Bash
pip install openai
Python
from openai import OpenAI client = OpenAI(base_url="https://llm.example.com/v1", api_key="YOUR_API_KEY") reply = client.chat.completions.create(    model="qwen3-8b",    messages=[{"role": "user", "content": "Give me three tips for Linux server security."}],    max_tokens=200,)print(reply.choices[0].message.content)

Conclusion

Now you have a complete single-GPU inference server. It uses a CUDA build of llama.cpp, a quantized GGUF model, API key protection, an Nginx proxy with HTTPS, and a draft model for faster output.

The benchmark script gives you real numbers, so you can decide if llama.cpp speculative decoding is worth it for your own load. You can find all server options in the llama.cpp server documentation.

You can run quantized models on a PerLod GPU server when VRAM efficiency matters more than maximum distributed throughput.

A small draft model predicts the next tokens, and the main model checks them in a single step. Correct guesses save time, and the final output stays the same.

No. The draft model must use the same tokenizer as the main model. Use a small model from the same family, such as Qwen3 0.6B for Qwen3 8B.

It depends on your needs. llama-server is easy to set up and works well on a single GPU with low memory use. vLLM is the better choice when you have heavy traffic and many GPUs.