soup draft (v0.71.33)

Speculative decoding is sold as free speed: a small draft model proposes tokens, the big target verifies a whole batch of them in one pass, and you keep the target's exact output. The question nobody answers first is whether the draft would actually propose the tokens your model is about to emit. soup draft measure answers it before you ship, and on the pair we validated the honest answer was no.

Measure first

bash
soup draft measure --target my-tuned-model \
    --draft HuggingFaceTB/SmolLM2-135M-Instruct \
    --prompts prod-prompts.jsonl

# CI gate: exit 2 below the floor, plus a JSON report
soup draft measure ... --min-acceptance 0.6 -o report.json

It reports two things: the acceptance rate and real plain-vs-assisted throughput measured on your box, not estimated.

Acceptance rate is the fraction of the target's own greedy tokens the draft would have proposed correctly (teacher-forced argmax agreement, the metric the Medusa and EAGLE papers report). Higher is better; roughly, 70% and up is where speculative decoding starts paying for the draft's forward pass on realistic hardware. Exit codes are 0 = ok, 2 = below --min-acceptance, 1 = error, so CI can gate on it.

Distil your own draft

bash
soup draft distill --target my-tuned-model \
    --draft-base HuggingFaceTB/SmolLM2-135M-Instruct \
    --data traffic.jsonl -o draft/

soup serve --model my-tuned-model --auto-spec   # picks up ./draft
soup draft list

distill runs logit KD through the existing task: distill trainer and emits a dense model, because a PEFT adapter directory cannot be loaded as an assistant_model. Flags: --steps N (training budget), --device cpu, --force (overwrite -o), --plan-only (render the config and exit).

A `soup shrink` output makes a good draft base: it shares the target's tokenizer by construction.

The local draft registry

soup draft distill registers the draft in ~/.soup/drafts.json, and soup serve --auto-spec consults that registry before the built-in pairing table. A draft you trained yourself is therefore picked up automatically, with no flag to remember.

Same tokenizer only

Draft and target must share a tokenizer, and a mismatched pair is refused up front. This is not pedantry: speculative decoding proposes draft token ids into the target's vocabulary, so a mismatched pair silently produces garbage instead of failing. Cross-tokenizer drafts (ULD-aligned universal assisted decoding) are deferred.

Honesty: the measured result, including ours

The "~1.5 to 2x faster serving" pitch was withdrawn, not shipped. On SmolLM2-360M-Instruct (target) with a SmolLM2-135M-Instruct draft:

DraftAcceptance
Stock, no distillation69.3%
Distilled, 2 epochs69.7%
Distilled, 10 epochs69.3%

Distillation bought nothing beyond noise. A small same-family draft is already near its capacity ceiling for agreeing with the target, and logit KD cannot buy capacity it does not have. Assisted decoding on that pair measured 0.55 to 0.64x: a net slowdown, because the draft's forward pass costs more than the tokens it saves at this size.

So the feature ships as the honest gate rather than the speedup claim. soup draft measure correctly says "do not enable this" on that pair, and that is the point of shipping the measurement. Whether distillation materially raises acceptance for a genuinely diverged fine-tune, or a larger target and draft pair, is unproven on a 4 GB box and tracked as a scale issue. Run soup draft measure on your pair rather than assuming, including when the assumption comes from us.

Acceptance is teacher-forced greedy agreement. It is exact, deterministic, and the right number for comparing drafts, but it is not the accepted-token count of a sampling run, which also depends on the rejection-resample cascade.

See also

  • Smart Inference Server--auto-spec, the built-in pairing table, prefix cache.
  • soup shrink — depth-prune a model into a same-tokenizer draft base.
  • soup ship — the other place Soup refuses to guess for you.