-
Notifications
You must be signed in to change notification settings - Fork 5
Benchmarking
Tightwad includes benchmark scripts to measure speculative decoding acceptance rates and wall-clock speedup across different model pairs, backends, and network configurations. All scripts are in scripts/ and output JSON results to benchmarks/.
| Script | Purpose | Draft Source | Target Source |
|---|---|---|---|
scripts/benchmark_proxy.py |
Multi-config benchmark (local Ollama + cloud APIs) | Ollama or llamacpp (LAN) | Ollama, llamacpp, or OpenRouter |
scripts/benchmark_families.py |
Cross-family cloud benchmarks (Qwen3, Llama) | Local llama-server | OpenRouter API |
scripts/benchmark_openrouter.py |
Llama 3.1 8B → 405B via OpenRouter | Local llama-server | OpenRouter API |
scripts/benchmark.sh |
Quick proxy health + acceptance rate check | Running proxy | Running proxy |
# Install tightwad with dev dependencies
cd tightwad
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"Both Python benchmark scripts use httpx (included in tightwad's dependencies).
Tests multiple draft→target pairs in one run. Supports Ollama, llama-server, and OpenRouter backends. Uses multi-round speculative decoding with whitespace-normalized text-match verification.
Start the draft and target servers you want to test. The script has built-in configs — edit the configs list in main() to match your hardware.
Default configs:
- Qwen3-8B → Qwen3-32B (local Ollama, same-family baseline)
- Qwen3-8B → Qwen3.5-397B (OpenRouter, cloud target)
- Qwen3-8B → Llama 3.3 70B (OpenRouter, cross-family control)
# Set OpenRouter key if testing cloud targets
export OPENROUTER_API_KEY="sk-or-..."
# Run all configs
.venv/bin/python scripts/benchmark_proxy.py
# Enable debug output (shows per-round draft/target text)
BENCH_DEBUG=1 .venv/bin/python scripts/benchmark_proxy.pyPrints per-prompt results and a summary table. Saves detailed JSON to benchmark_results.json.
For each prompt:
- Baseline: Target generates the full response alone (measures raw speed)
-
Speculative: Multi-round loop:
- Draft model generates N tokens from the current context
- Target model generates N tokens from the same context (or continues via "Continue from exactly where you left off" for chat APIs)
- Whitespace-normalized text-match finds the longest common prefix
- Target output is accepted and appended to the growing response
- Repeat until done or max rounds reached
The acceptance rate measures what fraction of draft characters the target agrees with — higher means more tokens come "free" from the fast draft model.
Tests same-family speculative decoding over a cloud API. Runs a local Llama 3.1 8B draft against Llama 3.1 405B on OpenRouter.
# Start local draft server (Llama 3.1 8B on any GPU or CPU)
llama-server -m ~/models/Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf \
--port 8081 --host 127.0.0.1 -ngl 999 --ctx-size 4096
# Verify it's running
curl http://127.0.0.1:8081/healthexport OPENROUTER_API_KEY="sk-or-..."
.venv/bin/python scripts/benchmark_openrouter.py
# Optional: override target model
TARGET_MODEL="meta-llama/llama-3.1-70b-instruct" .venv/bin/python scripts/benchmark_openrouter.py
# Debug mode
BENCH_DEBUG=1 .venv/bin/python scripts/benchmark_openrouter.py| Variable | Default | Description |
|---|---|---|
OPENROUTER_API_KEY |
(required) | OpenRouter API key |
DRAFT_URL |
http://127.0.0.1:8081 |
Local draft server URL |
TARGET_MODEL |
meta-llama/llama-3.1-405b-instruct |
OpenRouter model ID |
BENCH_DEBUG |
(unset) | Set to any value for per-round debug output |
Saves JSON to benchmark_llama_openrouter.json with per-prompt and per-category breakdowns.
| Category | Count | Examples |
|---|---|---|
| Reasoning | 3 | Math, word problems |
| Code | 2 | Prime checker, linked list |
| Factual | 3 | Geography, science, networking |
| List | 2 | Countries by area, programming languages |
| Creative | 2 | Haiku, short story |
| Metric | Value |
|---|---|
| Acceptance Rate | 62.8% (text-match) / 73.5% (logprobs) |
| Wall-Clock Speedup | 1.27x |
| Best Category | Reasoning (89%) |
| Worst Category | Creative (39%) |
| Category | Acceptance | Notes |
|---|---|---|
| Code | 37.7% | Deterministic output matches well |
| Reasoning | 23.8% | Step-by-step math overlaps |
| List | 15.4% | Formatting varies |
| Factual | 12.6% | Phrasing diverges at 405B scale |
| Creative | 3.1% | Nearly no overlap |
| Overall | 18.9% | Network latency negates speedup |
Key finding: Over cloud APIs, the per-round network latency (~3-8s per OpenRouter call) makes speculative decoding slower than baseline, despite 18.9% acceptance. Spec decoding only speeds things up when both models are local or very low-latency.
| Category | Acceptance | Notes |
|---|---|---|
| Creative | 29.7% | Haiku matched well |
| List | 24.2% | Formatting overlap |
| Code | 9.3% | Different implementations |
| Reasoning | 8.7% | Step divergence |
| Factual | 7.4% | Phrasing varies |
| Overall | 9.9% | Smaller target, more divergence than 405B |
| Category | Acceptance | Notes |
|---|---|---|
| Factual | 14.5% | Best category |
| Reasoning | 13.8% | Math overlap |
| Code | 6.5% | Different styles |
| Creative | 5.2% | Low |
| List | 2.0% | Formatting diverges |
| Overall | 6.6% | 1.7B too small for 235B target |
| Category | Acceptance | Notes |
|---|---|---|
| Code | 16.9% | Best category |
| Factual | 10.5% | Moderate |
| List | 8.5% | Some overlap |
| Reasoning | 4.4% | Low |
| Creative | 0.0% | No match |
| Overall | 10.8% | Slightly better than 235B |
| Draft | Target | Size Gap | Acceptance |
|---|---|---|---|
| Llama 3.1 8B | Llama 3.1 405B | 50x | 18.9% |
| Qwen3 1.7B | Qwen3.5 397B | 233x | 10.8% |
| Llama 3.1 8B | Llama 3.1 70B | 9x | 9.9% |
| Qwen3 1.7B | Qwen3 235B | 138x | 6.6% |
| Qwen3 8B | Llama 3.3 70B | cross | ~3% |
Key findings:
- Cloud API latency (~3-8s per round) negates wall-clock speedup in all cases
- Draft model size matters — 8B drafts outperform 1.7B drafts against large targets
- Larger targets don't necessarily mean lower acceptance (405B beat 70B with the same 8B draft)
- Cross-family is dead (~3%) regardless of model size
| Target Speed | Speculation Result | Example |
|---|---|---|
| <3 tok/s | 1.8x speedup | Qwen3-32B on 4-GPU RPC pool |
| ~3-5 tok/s | ~1.5x speedup | Dense models on slow pool |
| ~8-10 tok/s | Breakeven | Estimated threshold |
| >15 tok/s | Slowdown | GPT-OSS 120B MoE (19 tok/s baseline) |
Rule of thumb: If your target already generates >10 tok/s, speculation adds overhead without benefit. Use direct mode instead.
| Acceptance | ~3% |
|---|
Cross-family drafting is not viable — different training data produces completely different phrasings regardless of tokenizer similarity.
| Setup | Speed | Speedup |
|---|---|---|
| Qwen3-32B pool direct | 3.0 tok/s | — |
| Qwen3-32B pool + spec | 5.4 tok/s | 1.8x |
| Llama 3.3 70B pool direct | 2.2 tok/s | — |
| Llama 3.3 70B pool + spec | 4.1 tok/s | 1.86x |
Date: 2026-02-19 | Full report: benchmarks/GPT-OSS-120B-TEST-REPORT.md
| Metric | Value |
|---|---|
| Target | GPT-OSS 120B (MoE, 5.1B active) on 2x RX 7900 XTX + CPU offload |
| Draft | GPT-OSS 20B (MXFP4) on RTX 4070 Ti Super + RTX 3060 |
| Proxy | Tightwad on M4 Mac mini |
| Acceptance Rate | 100.0% (1,659/1,659 tokens) |
| Baseline (direct) | 19.1 tok/s (12.2s avg) |
| Speculative (proxy) | 6.0 tok/s (31.1s avg) |
| Wall-clock Speedup | 0.39x (2.5x slowdown) |
Key finding: Despite perfect 100% acceptance, speculation is a net loss because the MoE target is already fast (~20 tok/s). With only 5.1B active parameters, the 120B model generates tokens faster than the draft→verify round-trip overhead. Speculation only helps when the target generates below ~8-10 tok/s.
MoE + RPC caveat: MoE models replicate routing/expert tables per device (~20GB overhead), making RPC distribution to consumer GPUs (8-16GB) infeasible. The original plan to pool 76GB across 4 GPUs failed — fell back to 48GB GPU + 16GB CPU offload.
Tests multiple model families against OpenRouter targets. Handles per-family chat templates, thinking mode suppression (/no_think for Qwen3), and <think> block stripping.
# List available configs
.venv/bin/python scripts/benchmark_families.py --list
# Run a specific config
OPENROUTER_API_KEY="sk-or-..." .venv/bin/python scripts/benchmark_families.py --config qwen3-235b
# Run all configs (switches draft model between runs)
OPENROUTER_API_KEY="sk-or-..." .venv/bin/python scripts/benchmark_families.py| Config Key | Draft (local) | Target (OpenRouter) |
|---|---|---|
qwen3-235b |
Qwen3-1.7B | qwen/qwen3-235b-a22b |
qwen3.5-397b |
Qwen3-1.7B | qwen/qwen3.5-397b-a17b |
llama-70b |
Llama 3.1 8B | meta-llama/llama-3.1-70b-instruct |
llama-405b |
Llama 3.1 8B | meta-llama/llama-3.1-405b-instruct |
Note: You must start the correct local draft server for each config. Qwen configs need Qwen3-1.7B, Llama configs need Llama 3.1 8B.
Add a new entry to the CONFIGS dict in scripts/benchmark_families.py:
CONFIGS = {
"my-config": {
"name": "MyDraft → MyTarget (OpenRouter)",
"draft_model": "MyDraft-8B",
"draft_gguf": "~/models/MyDraft-8B.gguf",
"draft_url": "http://127.0.0.1:8081",
"target_model": "provider/model-id",
"template": my_template_fn, # chat template for local draft
"stop_tokens": ["<|eot_id|>"], # stop tokens for the draft family
"system_message": "...", # optional, e.g. "/no_think" for Qwen3
},
}Add a config tuple to the configs list in main():
configs = [
(
"MyDraft → MyTarget (description)",
{"url": "http://...", "model": "draft-model", "backend": "ollama"},
{"url": "http://...", "model": "target-model", "backend": "openai", "api_key": OPENROUTER_KEY},
),
]-
Temperature: All benchmarks use
temperature: 0(greedy decoding) for reproducibility -
Whitespace normalization:
re.sub(r'\s+', ' ', s).strip()— collapses all whitespace to single spaces before text comparison, preventing formatting differences from inflating rejection rates - Text-match verification: Finds the longest common prefix between normalized draft and target text. Character-level, not token-level
- Multi-round: Each benchmark generates up to 256 tokens across up to 30 rounds (32 draft tokens per round)
- Same-family requirement: Draft and target must share the same model architecture (e.g., Llama 3.1 8B → Llama 3.1 405B, not Llama 3.2 → Llama 3.3). Cross-family pairs get ~3% acceptance regardless of model size