-
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_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.
| 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 |
To add a new model pair:
- Edit
scripts/benchmark_proxy.pyand add a config tuple to theconfigslist inmain():
configs = [
# ... existing configs ...
(
"MyDraft → MyTarget (description)",
{"url": "http://...", "model": "draft-model", "backend": "ollama"},
{"url": "http://...", "model": "target-model", "backend": "openai", "api_key": OPENROUTER_KEY},
),
]- Run the benchmark and save results:
.venv/bin/python scripts/benchmark_proxy.py
cp benchmark_results.json benchmarks/benchmark_<description>.json-
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