← All courses ← Course home

Lesson 03 of 04

Measure speculative decoding first

Speculative decoding can make a local model faster. It can also make it slower. The only way to know is to measure on your own machine and prompts.

Agent brief (llms.md)

Environment setup

Use the same local-lab folder from lesson 01 (uv, Python 3.11+, and uv add numpy). If you skipped it, do the setup in lesson 01 first.

Install llama.cpp. On macOS: brew install llama.cpp. On Linux or Windows, download a release from github.com/ggml-org/llama.cpp/releases. Check it with llama-server --version.

How it works, in short

A small draft model (or a draft head, as in MTP) guesses the next few tokens. The big model checks them in one step. If most guesses are right, you get several tokens for the cost of one check. If many are wrong, you pay for the guesses and the checks.

DraftGuess 3 tokens
CheckBig model verifies
KeepAccepted tokens
RepeatFrom first wrong token

Run an A/B test

Start the same model twice, once without and once with a draft model. llama.cpp uses --spec-type and -md. --spec-draft-n-max sets how many tokens to guess (default 3).

# Off
llama-server -hf Qwen/Qwen3-1.7B-GGUF:Q8_0 --port 8092 -c 4096 -np 1
# On: Qwen3 0.6B drafts for Qwen3 1.7B (same tokenizer)
llama-server -hf Qwen/Qwen3-1.7B-GGUF:Q8_0 -hfd Qwen/Qwen3-0.6B-GGUF:Q8_0 \
  --spec-type draft-simple --spec-draft-n-max 3 --port 8093 -c 4096 -np 1
"""A/B test: the same prompts against a server without and with speculative decoding."""
import json
import sys
import urllib.request

PROMPTS = [
    "Write a Python function that checks if a number is prime. Code only.",
    "Tell me a short story about a cat who learns to fly.",
    "List the numbers from 1 to 40 separated by commas.",
]


def run(port):
    url = f"http://127.0.0.1:{port}/v1/chat/completions"
    for prompt in PROMPTS:
        body = {"messages": [{"role": "user", "content": prompt}], "max_tokens": 200,
                "temperature": 0, "chat_template_kwargs": {"enable_thinking": False}}
        req = urllib.request.Request(url, json.dumps(body).encode(), {"Content-Type": "application/json"})
        t = json.load(urllib.request.urlopen(req))["timings"]
        drafted, accepted = t.get("draft_n"), t.get("draft_n_accepted")
        rate = f"{accepted / drafted:.0%}" if drafted else "off"
        print(f"{prompt[:28]:28} | gen {t['predicted_per_second']:5.1f} tok/s | "
              f"prompt {t['prompt_per_second']:6.1f} tok/s | accept {rate}")


if __name__ == "__main__":
    for port in sys.argv[1:] or ["8092", "8093"]:
        print(f"--- port {port}")
        run(port)

Results on the test box (8 CPU cores, build b11624):

--- port 8092
Write a Python function that | gen  16.3 tok/s | prompt    7.0 tok/s | accept off
Tell me a short story about  | gen  19.0 tok/s | prompt  130.0 tok/s | accept off
List the numbers from 1 to 4 | gen  16.8 tok/s | prompt  157.1 tok/s | accept off
--- port 8093
Write a Python function that | gen  14.9 tok/s | prompt   81.4 tok/s | accept 88%
Tell me a short story about  | gen  16.6 tok/s | prompt  166.1 tok/s | accept 51%
List the numbers from 1 to 4 | gen  20.5 tok/s | prompt  125.3 tok/s | accept 98%

Only the counting prompt got faster. The code prompt was slower even with 88% of guesses accepted, because on this CPU the draft model's own time ate the gain. The story prompt had 51% acceptance and was slower. Your GPU will give different numbers. That is why you run the test. (The first prompt speed on each server includes warm-up, so run each prompt twice and use the second number.)

The server log also prints the rate, so you can confirm speculative decoding is really on:

slot print_timing: id  0 | task 0 | draft acceptance = 0.87805 (  144 accepted /   164 generated), mean len =  3.62

If you never see this line, speculative decoding is not running, whatever your flags say.

Known traps

Rules

Key concepts

  • A draft guesses tokens and the big model checks them.
  • High acceptance does not always mean faster. The draft costs time too.
  • Acceptance can differ by backend (llama.cpp #26750).
  • Mac Metal needed PR #29869 to make small batches fast.

Takeaways

  • Run the same prompts with it off and on, on your own hardware.
  • Confirm the draft acceptance log line is there.
  • Keep it only where it wins, and watch prompt processing speed too.