Smart Inference Server (v0.30.0)
soup serve graduated from a basic OpenAI-compatible wrapper into a production-grade serving stack. Speculative decoding, prefix caching, structured output, dynamic LoRA hot-swap, a continuous-batching dashboard, and OpenTelemetry tracing all ship in v0.30.0.
Speculative decoding
Use a smaller draft model to propose tokens the target verifies in one pass. Whether it is faster depends entirely on the pair: soup draft measure (v0.71.33) is how you find out before you enable it.
# Transformers backend — uses HF assisted generation
soup serve --model ./output --speculative-decoding small-draft-model --num-speculative-tokens 5
# vLLM backend — uses vLLM native speculative decoding
soup serve --model ./output --backend vllm --speculative-decoding small-draft-model
# Auto-pair: Soup picks the draft from the base model recorded in the adapter
soup serve --model ./output --backend vllm --auto-specWith the default --hub hf, soup serve --model takes a local directory, not a Hub id; a path that does not exist exits 1 with "Model path not found". With --hub modelscope or --hub modelers, a --base repo id that is not a local path is fetched first, and otherwise a --model repo id is. --auto-spec looks the draft up from the base model recorded in the adapter's adapter_config.json (or from --base), so the base has to be one of the large families in its pairing table: Llama 3.1 / 3.3 / 4, Qwen 2.5 / 3, Mistral Large, Mixtral, DeepSeek V3 / R1, and Gemma 2 / 3 at their large sizes. Anything without a known draft pairing prints a yellow "no draft" note and falls back to standard decoding. A draft you trained yourself with soup draft distill is picked up before this built-in table.
Before you enable any of this, measure it.
soup draft measure(v0.71.33) reports the draft's real acceptance rate and plain-vs-assisted throughput on your own pair. On the pair we validated, assisted decoding came out at 0.55 to 0.64x, a net slowdown, so treat a speedup as something to verify rather than assume.
Prefix caching (vLLM)
For RAG and agent workloads with a shared system prompt:
soup serve --model ./output --backend vllm --prefix-cacheThe first request with a given prefix warms the cache; subsequent requests skip the shared prefix compute entirely.
Structured output
Constrain output to a JSON schema or regex pattern.
# JSON schema (file must live under cwd)
soup serve --model ./output --structured-output json --json-schema product.json
# Regex (length-capped at 2048 chars, null bytes rejected)
soup serve --model ./output --structured-output regex --regex-pattern '\d{3}-\d{4}'Schemas serialised over 64 KB are rejected. JSON schemas must declare a top-level type field. Constraints are validated at startup, not per-request.
Dynamic LoRA hot-swap
Switch the active adapter at runtime without restarting the server.
soup serve --model ./base-model --adapters chat=./chat-adapter --adapters code=./code-adaptercurl -X POST http://localhost:8000/v1/adapters/activate/chat
# → {"active": "chat", "status": "ok"}
curl -X POST http://localhost:8000/v1/adapters/deactivate
# → {"active": null, "status": "ok"}
curl http://localhost:8000/v1/adapters
# → {"adapters": [{"name": "chat", "active": true}, ...], "active": "chat"}Names match ^[a-zA-Z0-9][a-zA-Z0-9-]*$. Activate/deactivate is thread-safe behind a lock, and since v0.75.1 both also check the Host and Origin headers, answering 421 or 403 rather than acting. Until then a cross-site request from any page the operator merely visited could switch the adapter every subsequent completion ran against (0.30.0 to 0.75.0). The same release put the tool token in front of GET /v1/adapters, activate and deactivate, with the check running before the 404 branches so an unauthenticated probe cannot learn which adapter names are loaded from an "unknown adapter" reply. See which routes get which check.
A single request can also name its own adapter with an adapter field in the JSON body, which uses that adapter for that call without changing the active one. The model field is ignored for this, so "model": "chat" does not select anything. See Multi-Adapter Serving.
Continuous-batching dashboard + /metrics
soup serve --model ./output --dashboardcurl http://localhost:8000/metrics
# {
# "requests_total": 1234,
# "tokens_generated_total": 456789,
# "active_requests": 3,
# "latency_p50_ms": 185.2,
# "latency_p95_ms": 720.0,
# "latency_samples": 1000
# }Latency percentiles are computed from the last 1000 requests; counters include failure paths so the dashboard reflects true reliability. The transformers and vLLM backends serve /metrics whether or not --dashboard is passed (the flag adds the live terminal dashboard); the SGLang and DeepSpeed-MII backends do not serve it at all, and --dashboard there prints a warning instead of collecting anything.
OpenTelemetry tracing
Emit per-request spans to your OTLP collector.
pip install opentelemetry-sdk opentelemetry-exporter-otlp
soup serve --model ./output \
--trace --trace-endpoint http://localhost:4317OTLP endpoint hardening: scheme allowlist, plain HTTP only for loopback, and a private, link-local, shared-space (100.64.0.0/10) or 0.0.0.0 IP address rejected on either scheme, which is stricter than HF_ENDPOINT, where the address is checked only over plain HTTP. Missing SDK is a no-op with a warning.
A local trace file, with no collector
If you do not want to stand up a collector to answer "what did it actually reply, and how slowly":
soup serve --model ./output --trace-log ./traces.jsonl --trace-log-cap-mb 100One JSON object appended per chat completion, carrying ts, prompt, response, latency_ms and tokens. The path must stay under the working directory. It rotates at the cap (1 to 10000 MB, default 100) keeping one backup, and refuses to write through a symlink on the backup path.
Secrets are redacted before the line is written, not after: Hugging Face tokens, sk- keys and Bearer values are stripped, because a trace file is the most likely thing in a serving directory to get pasted into an issue. A write failure never takes down the request handler.
Capturing thumbs from the served model
soup serve --model ./output --record-thumbs ./feedback.dbTurns on POST /v1/thumbs and writes into the same local SQLite that soup local-rl harvests, so a served model feeds the preference flywheel without anyone recording feedback by hand. Path contained under cwd, transformers backend only.
DeepSpeed-MII backend
soup serve --model ./output --backend miiLoopback-only CORS, max_tokens capped at 16384, streaming disabled (no SSE for MII v0.x). Pipeline crashes return generic 500 with no stack-trace leak.
Auto-quant picker
soup serve --model ./output --auto-quantThe picker API is registered; live evaluation soft-falls-back to the highest-scored candidate so the server still binds when no candidate clears min_score.
Soup is free and Apache-2.0. If it saved you a training run, starring the repo costs nothing and helps most. You can also fund the GPU time behind the work a 4 GB laptop cannot reach.