--- name: spec-decode-ab-single-gpu-host description: "A/B a speculative-decoding variant (or any serve-flag challenger) against an incumbent LLM endpoint on a single-GPU host that serves users: stop the unit, run the challenger from a patched env file, prove losslessness by identical greedy text, and keep the incumbent unless the challenger wins." --- # A/B a serve-flag challenger against a live LLM endpoint Use when asked "try it without X / with Y" for an inference endpoint that is serving users from a GPU you cannot share (one model copy per host). The deliverable is a measurement plus a decision, and the endpoint must end the experiment on the configuration you started it with. Ready-made harness for llama.cpp endpoints: `scripts/bench-spec-ab.py` (` [--base-url]`) — it stops the unit, runs each config, compares greedy text and restores the unit. Below is what it encodes and the traps it hit. ## Protocol 1. **Fixed measurement shapes, one prompt per phase.** A large prompt with a tiny budget isolates prefill; a small prompt with a long budget isolates decode. `temperature 0` throughout. Never compare across sessions — run the *incumbent* again in the same session, on the same box, or you are comparing two machines' worth of noise. 2. **Prove losslessness, do not claim it.** Send one fixed prompt at temp 0 to every config and diff the greedy text. Speculation is only allowed to buy speed; if the texts differ, stop and treat it as a correctness bug. 3. **Stop the unit for the duration** (benchmarking policy on these hosts: a bench beside a live service starves the GPU). `sudo systemctl stop && passive`, run the challenger by hand with `setsid`, then `sudo systemctl start ` at the end *whatever happens* — wrap the restore so an exception cannot leave the fleet dark. 4. **Override tunables through a patched env file**, never through the shell: serve scripts that `source` their env file with `set -a` win over environment variables, so `CTX=... bash serve.sh` silently does nothing. `sed` the repo env into `/tmp/challenger.env` and point the service's env-file variable at it. 5. **Read the acceptance counters, not the flags.** `/metrics` (`llamacpp:spec_decode_num_drafts_total` vs `..._accepted_tokens_total`) or the per-request `timings.draft_n` / `draft_n_accepted`. A draft rate of 0 while tokens are produced means the drafter is not running even though the flags look right. 6. **Sweep the depth, not just the mode.** Draft depth is model-specific and single-valued: measure at least two (e.g. n_max 3 and 7). Deeper often loses — at n=7 a third of proposals survived, at n=3 more than half. 7. **Watch per-card VRAM** (`rocm-smi --showmeminfo vram`). A proposer is not free: a drafter file cost 2.06 GB on its card while loading the target's own in-model MTP head cost ~3.3 GB. 8. **Decide and land it.** If the challenger loses, say so and change nothing; if it wins, put the value in the repo env file, run the suite, deploy, and verify with a real completion. Record the losing numbers in the env file with "do not switch without re-measuring" so nobody re-litigates it. ## Traps that cost time here - **`pgrep -f