Running Gemma 4 12B Locally with Speculative Decoding on Ubuntu

Running Gemma 4 12B Locally with Speculative Decoding

This guide shows how to run Gemma 4 12B locally on Ubuntu with llama.cpp, Hugging Face automatic model loading, and speculative decoding using Gemma 4’s MTP path.

The test system used two NVIDIA GPUs, an i5-13400, and 64 GB of RAM.

With llama-server at 256k context, Gemma 4 12B ran at about 50 tok/s and peaked at 63.47 tok/s on short replies.

What worked

The Q8_K_XL build loaded successfully and served requests through the Hugging Face auto-download path. The BF16 path was removed after repeated assistant/MTP loading failures in this environment.

The final stable mode used:

  • -hf unsloth/gemma-4-12b-it-GGUF:UD-Q8_K_XL
  • --spec-type draft-mtp
  • --spec-draft-n-max 4

Launch script

This is the working launch pattern:

bash
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LLAMA_DIR="${LLAMA_DIR:-$HOME/llama.cpp}"

if [[ -d "$SCRIPT_DIR/llama.cpp/build" ]]; then
  LLAMA_DIR="$SCRIPT_DIR/llama.cpp"
elif [[ -d "$SCRIPT_DIR/build" && -x "$SCRIPT_DIR/build/bin/llama-server" ]]; then
  LLAMA_DIR="$SCRIPT_DIR"
fi

TEMP="${TEMP:-0.8}"
TOP_P="${TOP_P:-0.9}"
TOP_K="${TOP_K:-40}"
DRAFT_N_MAX="${DRAFT_N_MAX:-4}"
HOST="${HOST:-127.0.0.1}"
CTX_SIZE="${CTX_SIZE:-}"
PORT="${PORT:-}"

choose_context() {
  echo "Select context size (default: 48k):"
  echo " 1) 48k (49152)"
  echo " 2) 64k (65536)"
  echo " 3) 128k (131072)"
  echo " 4) 256k (262144)"
  read -r -p "Enter choice [1-4] (default 1): " choice
  case "${choice:-1}" in
    1) CTX_SIZE=49152; PORT="${PORT:-8048}"; LABEL="48k" ;;
    2) CTX_SIZE=65536; PORT="${PORT:-8064}"; LABEL="64k" ;;
    3) CTX_SIZE=131072; PORT="${PORT:-8128}"; LABEL="128k" ;;
    4) CTX_SIZE=262144; PORT="${PORT:-8256}"; LABEL="256k" ;;
    *) echo "Invalid choice: ${choice}"; exit 1 ;;
  esac
}

choose_context

SERVER_BIN="$LLAMA_DIR/build/bin/llama-server"

if [[ ! -x "$SERVER_BIN" ]]; then
  echo "Error: llama-server not found or not executable: $SERVER_BIN" >&2
  exit 1
fi

echo "Starting Gemma 4 12B"
echo " LLAMA_DIR = ${LLAMA_DIR}"
echo " MODEL_LABEL = Q8_K_XL"
echo " MODEL_SPEC = -hf unsloth/gemma-4-12b-it-GGUF:UD-Q8_K_XL"
echo " CONTEXT = ${LABEL} (${CTX_SIZE})"
echo " HOST = ${HOST}"
echo " PORT = ${PORT}"
echo " TEMP = ${TEMP}"
echo " TOP_P = ${TOP_P}"
echo " TOP_K = ${TOP_K}"
echo " DRAFT_N_MAX = ${DRAFT_N_MAX}"
echo

printf 'COMMAND:'
printf ' %q' "$SERVER_BIN" -hf unsloth/gemma-4-12b-it-GGUF:UD-Q8_K_XL --ctx-size "$CTX_SIZE" --temp "$TEMP" --top-p "$TOP_P" --top-k "$TOP_K" --host "$HOST" --port "$PORT" --spec-type draft-mtp --spec-draft-n-max "$DRAFT_N_MAX" --cache-ram 24576
printf '

'

exec "$SERVER_BIN"   -hf unsloth/gemma-4-12b-it-GGUF:UD-Q8_K_XL   --ctx-size "$CTX_SIZE"   --temp "$TEMP"   --top-p "$TOP_P"   --top-k "$TOP_K"   --host "$HOST"   --port "$PORT"   --spec-type draft-mtp   --spec-draft-n-max "$DRAFT_N_MAX" --cache-ram 24576

The server should expose the OpenAI-compatible endpoint on the chosen port, which in my case was 8256 for the 256k context run.

Verify with curl

A simple test request looks like this:

bash
curl -s http://127.0.0.1:8256/v1/chat/completions   -H "Content-Type: application/json"   -d '{
    "model": "gemma4",
    "messages": [
      {"role": "user", "content": "Write one line about the sky."}
    ],
    "stream": false
  }' | jq -r '.choices[0].message.content'

My server returned a clean completion and confirmed the endpoint was working.

Benchmark table

The repeated test runs on the 256k configuration showed stable prompt processing, generation speed around 48 to 55 tokens per second, and draft acceptance generally a little above 50 percent. The table below summarizes the clearest timing snapshots from the server logs and the 10-run curl loop.

MetricObserved valueNotes
Prompt speed92.71 tok/s to 118.41 tok/sShort prompts were processed quickly across runs.
Generation speed47.67 tok/s to 54.65 tok/sMost steady-state decoding runs clustered near 50 tok/s.
Total latency4.76 s to 6.23 sMeasured by the external curl loop for the one-line sky prompt.
Typical latencyAbout 5.6 sThe 10-run loop centered around the mid-5-second range.
Draft acceptance0.51 to 0.62Acceptance was consistently high enough to make MTP useful.
Best observed single response63.47 tok/s generationSeen on the short hello verification request.

A few representative server-side timing samples from the same setup are shown below.

Task samplePrompt tok/sEval tok/sTotal timeDraft acceptance
Short hello verification92.7163.471.65 s0.75000
Task 128113.1052.296.03 s0.58065
Task 225117.8648.385.61 s0.51724
Task 316116.4949.615.84 s0.54167
Task 601114.6452.015.08 s0.56329
Task 684116.8354.655.46 s0.61607
Task 772118.4152.055.15 s0.56250
Task 856117.4751.305.56 s0.55814
Task 946117.3348.594.68 s0.51027

These measurements show a practical pattern: the system is not just able to load Gemma 4 12B at 256k context, but also to keep generation speed around 50 tok/s for real chat-style requests.

That is fast enough for interactive coding, debugging, and long-context assistance on a home workstation.

Real coding-task performance

On real coding-style prompts that filled tens of thousands of tokens in the 256k window, prompt ingestion stayed above 2,200 tokens per second and generation averaged around 20 tok/s, with draft acceptance usually between about 0.56 and 0.65.

In practice, that was enough to handle 15k–20k-token coding sessions (multi-file context, refactoring suggestions, and step-by-step debugging) without the model feeling sluggish, even when producing several hundred tokens of answer per request.

Why Q8_K_XL was the winner

In my environment, Q8_K_XL loaded successfully with speculative decoding, while BF16 repeatedly triggered Gemma 4 assistant/context errors. The upstream reports around Gemma 4 assistant support match that failure pattern, so removing BF16 was the right simplification.

Practical notes

Hugging Face auto-loading kept the workflow simple because it avoided a separate /home/berdachuk/llama.cpp/models/ directory. The model was loaded directly from the Hugging Face cache and the server used the cached GGUF snapshot automatically.

The main runtime sign that MTP was active was the draft_n_accepted field in the response timings, which confirmed speculative decoding was contributing to token generation.

Takeaway

For this setup, the stable answer was simple: use Gemma 4 12B Q8_K_XL with HF auto-download and MTP, verify with curl, and benchmark with a small loop. That gave you a working local server and measurable speculative decoding gains without maintaining a separate model directory.

Conclusion

In practice, this setup shows that Gemma 4 12B is fast enough to feel responsive while still supporting a very large context window. The 256k context run worked correctly in my environment, which makes the model useful for long conversations, multi-file coding questions, and tasks that need a lot of retained context.

Speculative decoding makes the biggest difference when you want the model to stay interactive for practical work such as coding help, script generation, debugging, and iterative prompt refinement. For home use, that combination of speed and context is the main reason this setup is worth keeping around.

Published on 6/10/2026