# Serving, Inference & Export [← Back to the Soup README](../README.md) > The OpenAI-compatible inference server, batch inference, benchmarking, merge/export (GGUF/ONNX/TensorRT/AWQ/GPTQ), the Anthropic Messages endpoint, speculative decoding, deploy autopilot, the Web UI, and Agent Forge. **Contents:** - [Merge LoRA Adapter](#merge-lora-adapter) - [Export to GGUF](#export-to-gguf) - [Batch Inference](#batch-inference) - [Inference Benchmarking](#inference-benchmarking) - [Inference Server](#inference-server) - [Web UI](#web-ui) - [Inference Server Trace Log](#inference-server-trace-log) - [Soup Quantize — Ergonomic Export Alias](#soup-quantize--ergonomic-export-alias) - [Llama.cpp Proxy](#llamacpp-proxy) - [Tail-Latency Stats + Tool-Call Timer](#tail-latency-stats--tool-call-timer) - [Web UI Plugin Registry + Env Knobs](#web-ui-plugin-registry--env-knobs) - [Deploy Autopilot](#deploy-autopilot) - [Agent Forge](#agent-forge) - [HF Space SDK Auto-Pick](#hf-space-sdk-auto-pick) - [Anthropic Messages API Converter](#anthropic-messages-api-converter) - [Server-Side Tools](#server-side-tools) - [Anthropic Messages Endpoint](#anthropic-messages-endpoint) - [Train + measure your own draft (`soup draft`)](#train--measure-your-own-draft-soup-draft) - [N-gram Speculative Decoding](#n-gram-speculative-decoding) - [Server-Side Tool Endpoints](#server-side-tool-endpoints) --- ## Merge LoRA Adapter Merge a LoRA adapter with its base model into a standalone model: ```bash # Auto-detect base model from adapter_config.json soup merge --adapter ./output --output ./merged # Specify base model and dtype soup merge --adapter ./output --base meta-llama/Llama-3.1-8B --dtype bfloat16 ``` ## Export to GGUF Export models to GGUF format for use with [Ollama](https://ollama.com/) and [llama.cpp](https://github.com/ggerganov/llama.cpp): ```bash # Export LoRA adapter (auto-merges with base, then converts) soup export --model ./output --format gguf --quant q4_k_m # Export with different quantizations soup export --model ./output --format gguf --quant q8_0 soup export --model ./output --format gguf --quant f16 # Export a full (already merged) model soup export --model ./merged --format gguf # Specify llama.cpp path manually soup export --model ./output --format gguf --llama-cpp /path/to/llama.cpp ``` Supported quantizations: `q4_0`, `q4_k_m`, `q5_k_m`, `q8_0`, `f16`, `f32` ### Building llama.cpp (required for quantized GGUF) Soup auto-clones llama.cpp (pinned tag `b5270`) to `~/.soup/llama.cpp` on first use, but it does **not** build it. The conversion step (`f16` / `f32`) works from the Python script alone; every *quantized* type additionally needs the `llama-quantize` binary, so build it once: ```bash cd ~/.soup/llama.cpp cmake -B build -DGGML_NATIVE=OFF -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release cmake --build build --config Release --target llama-quantize -j 4 ``` Soup finds the binary in both single-config (`build/bin/llama-quantize`, Make/Ninja) and multi-config (`build/bin/Release/llama-quantize.exe`, MSVC/Xcode) layouts, or on `PATH`. Point at an existing checkout with `--llama-cpp /path/to/llama.cpp` or the `LLAMA_CPP_PATH` env var. **Toolchain validated on Windows** (all six quants, then `soup deploy ollama` → inference): Visual Studio 2022 Build Tools with the *Desktop development with C++* workload (`Microsoft.VisualStudio.Component.VC.Tools.x86.x64`) + CMake ≥ 3.14, CPU-only. Linux/macOS need only a C++ toolchain + CMake. CUDA llama.cpp builds are untested (see [#144](https://github.com/MakazhanAlpamys/Soup/issues/144)). > Do **not** run `pip install -r ~/.soup/llama.cpp/requirements.txt` — it pins > `torch~=2.2.1` against the CPU wheel index and will downgrade a CUDA PyTorch, > breaking training. Soup deliberately installs only the convert script's extra > dependencies (`gguf`, `sentencepiece`, `protobuf`), unpinned. ### ONNX Export Export models to ONNX format for use with [ONNX Runtime](https://onnxruntime.ai/): ```bash pip install "soup-cli[onnx]" soup export --model ./output --format onnx soup export --model ./output --format onnx --output ./model_onnx ``` ### TensorRT-LLM Export Export models to TensorRT-LLM format for high-throughput GPU inference: ```bash pip install "soup-cli[tensorrt]" soup export --model ./output --format tensorrt soup export --model ./output --format tensorrt --output ./model_trt ``` ### BitNet 1.58 TQ1_0 GGUF Export (live in v0.71.20) Export a BitNet 1.58-bit model as a `TQ1_0` (1.58-bit ternary) GGUF via llama.cpp's convert→quantize pipeline. Both the `bitnet` alias and the explicit `tq1_0` flavour map to the same `TQ1_0` quantization (no importance matrix is needed — ternary weights export directly): ```bash soup export --model ./output --format bitnet # → TQ1_0 ternary GGUF soup export --model ./output --format tq1_0 # same flavour soup export --model ./output --format bitnet --llama-cpp /path/to/llama.cpp ``` A built llama.cpp toolchain is required; LoRA adapters are auto-merged before export. See [Performance & Quantization → BitNet](performance-and-quantization.md) for the training side. After export, use with Ollama manually or auto-deploy: ```bash # Manual (3-step) echo 'FROM ./my-model.q4_k_m.gguf' > Modelfile ollama create my-model -f Modelfile ollama run my-model # Auto-deploy (1-step) soup export --model ./output --format gguf --deploy ollama --deploy-name my-model ``` ### Deploy to Ollama Deploy a GGUF model directly to your local [Ollama](https://ollama.com/) instance: ```bash # Deploy a GGUF model soup deploy ollama --model ./output/model.q4_k_m.gguf --name soup-my-model # Deploy with system prompt and parameters soup deploy ollama --model ./model.gguf --name soup-chat \ --system "You are a helpful assistant." \ --template chatml \ --parameter temperature=0.7 \ --parameter top_p=0.9 # Export + deploy in one command soup export --model ./output --format gguf --deploy ollama # List Soup-deployed models soup deploy ollama --list # Remove a model soup deploy ollama --remove soup-my-model ``` Auto-detected chat templates: `chatml`, `llama`, `mistral`, `vicuna`, `zephyr` (or `auto` to infer from soup.yaml). ## Batch Inference Run a model on a list of prompts and save results: ```bash # JSONL input (each line: {"prompt": "..."}) soup infer --model ./output --input prompts.jsonl --output results.jsonl # Plain text input (one prompt per line) soup infer --model ./output --input prompts.txt --output results.jsonl # Custom generation settings soup infer --model ./output --input prompts.jsonl --output results.jsonl \ --max-tokens 512 --temperature 0.3 ``` Output is JSONL with `prompt`, `response`, and `tokens_generated` fields. Shows a progress bar and throughput summary. ## Inference Benchmarking Quickly measure your model's generation speed and memory footprint before deployment: ```bash # Benchmark local speed and VRAM usage on 3 automatically generated prompts soup bench ./output # Customizing benchmarking parameters soup bench ./output --num-prompts 5 --max-tokens 256 # Use custom prompts from a text file (one per line) or JSONL soup bench ./output --prompts-file my_prompts.txt soup bench ./output --prompts-file bench_suite.jsonl ``` This acts as a built-in "speedometer," outputting Tokens-Per-Second (TPS), Total Latency, and Peak VRAM allocations into a clean status table. ## Inference Server Start a local OpenAI-compatible inference server: ```bash # Install server dependencies pip install "soup-cli[serve]" # Start server soup serve --model ./output --port 8000 # With custom settings soup serve --model ./output --port 8080 --host 127.0.0.1 --max-tokens 1024 ``` Endpoints: - `POST /v1/chat/completions` — chat completions (streaming supported) - `GET /v1/models` — list available models - `GET /health` — health check Compatible with OpenAI SDK: ```python from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="unused") response = client.chat.completions.create( model="output", messages=[{"role": "user", "content": "Hello!"}], ) ``` ### KV Cache Quantization (`soup serve --kv-cache-type`) Shrink the inference-time KV cache on the transformers backend: ```bash soup serve --model ./output --kv-cache-type bf16 # cache in the model compute dtype soup serve --model ./output --kv-cache-type q8_0 # 8-bit quantized cache (needs `hqq`) ``` `bf16`/`f16` need no extra dependency; `q8_0` requires a quant backend (`hqq` / `optimum-quanto`) or the CLI exits with an install hint; `fp8` is Hopper-only. Full detail and the vLLM/SGLang status live in [Performance & Quantization → KV Cache Types](performance-and-quantization.md#kv-cache-types-v0530). ### vLLM Backend (2-4x Faster Inference) Use [vLLM](https://github.com/vllm-project/vllm) for significantly better throughput in production: ```bash # Install vLLM support pip install "soup-cli[serve-fast]" # Start with vLLM backend soup serve --model ./output --backend vllm # Multi-GPU with tensor parallelism soup serve --model ./output --backend vllm --tensor-parallel 2 # Control GPU memory usage soup serve --model ./output --backend vllm --gpu-memory 0.8 # Cap the sequence length when the KV cache does not fit soup serve --model ./output --backend vllm --max-model-len 8192 ``` > **Tip:** Soup auto-detects vLLM. When installed, you'll see a hint during `soup serve` if you haven't enabled it yet. The vLLM backend applies the **model's own chat template**, exactly like the transformers backend — both call one shared prompt builder. A model that ships no chat template falls back to a generic `User:` / `Assistant:` prompt, and the server says so at startup. `finish_reason` reports `"length"` when a response hits `max_tokens` and `"stop"` otherwise (`/v1/messages` maps those to `max_tokens` / `end_turn`). ### SGLang Backend Use [SGLang](https://github.com/sgl-project/sglang) as an alternative high-throughput backend: ```bash # Install SGLang support pip install "soup-cli[sglang]" # Start with SGLang backend soup serve --model ./output --backend sglang # Multi-GPU with tensor parallelism soup serve --model ./output --backend sglang --tensor-parallel 2 ``` ### Speculative Decoding Use a smaller draft model to speed up generation. **Measure before you trust it** — see [Train + measure your own draft](#train--measure-your-own-draft-soup-draft) below. Speculative decoding is only a win when the draft agrees with the target often enough to pay for its own forward pass; on a small target it is frequently a *slowdown*. ```bash # Transformers backend — uses HF assisted generation soup serve --model ./output --speculative-decoding small-draft-model --spec-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 for you based on the target family soup serve --model meta-llama/Llama-3.1-70B-Instruct --backend vllm --auto-spec # → auto-paired: meta-llama/Llama-3.2-1B-Instruct (target: Llama-3.1-70B-Instruct) ``` `--auto-spec` handles Llama 3.1/3.3/4, Qwen 2.5/3, Mistral Large, Mixtral, DeepSeek V3/R1, and Gemma 2/3. Models without a known draft pairing (e.g. 8B-or-smaller targets where draft+target overhead outweighs the gain) print a yellow "no draft" note and fall back to standard decoding. A draft you trained yourself with `soup draft distill` is picked up **before** this built-in table. ### Train + measure your own draft (`soup draft`) The question nobody answers before enabling speculative decoding: *would the draft actually propose the tokens my model is going to emit?* `soup draft measure` answers it. ```bash # Would this draft pay off? Acceptance rate + REAL plain-vs-assisted throughput. soup draft measure --target ./my-tuned-model \ --draft HuggingFaceTB/SmolLM2-135M-Instruct \ --prompts prod-prompts.jsonl # Distil a draft from your own target, then serve it automatically. 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 ``` **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/EAGLE papers report). Higher is better; roughly, ≥70% is where speculative decoding starts paying for the draft's forward pass on realistic hardware. `--min-acceptance 0.6` exits **2** below the floor, so CI can gate on it (exit 0 = ok, 2 = below floor, 1 = error). `distill` runs logit KD through the existing `task: distill` trainer and emits a **dense** model (a PEFT adapter directory cannot be loaded as an `assistant_model`). Draft and target must share a tokenizer — a mismatch is refused up front, because speculative decoding proposes *draft* token ids into the *target's* vocabulary and a mismatched pair silently produces garbage instead of failing. A `soup shrink` output makes a good draft base: same tokenizer by construction. **Measured reality check (be sceptical of speedup claims, including ours).** On `SmolLM2-360M-Instruct` with a `SmolLM2-135M-Instruct` draft, the *stock* draft already scored **69.3%** acceptance, distilling it changed nothing (69.7% after 2 epochs, 69.3% after 10), and assisted decoding came out **0.55–0.64×** — a net slowdown. A small same-family draft is already near its ceiling for agreeing with the target, and the draft's forward pass costs more than the tokens it saves. Whether distillation pays off on a larger or genuinely diverged target/draft pair is unproven on a 4 GB box. Run `soup draft measure` on *your* pair rather than assuming. ### Prefix Caching For RAG and agent workloads with a shared system prompt, enable vLLM's automatic prefix cache: ```bash soup serve --model ./output --backend vllm --prefix-cache ``` The first request with a given prefix warms the cache; subsequent requests skip the shared prefix compute entirely. Big latency win when 100+ requests share the same system prompt. ### Dynamic LoRA Hot-Swap Switch the active adapter at runtime without restarting the server: ```bash soup serve --model base-model --adapters chat=./chat-adapter code=./code-adapter ``` ```bash # Activate an adapter curl -X POST http://localhost:8000/v1/adapters/activate/chat # → {"active": "chat", "status": "ok"} # Return to base model curl -X POST http://localhost:8000/v1/adapters/deactivate # → {"active": null, "status": "ok"} # List loaded adapters with active flag curl http://localhost:8000/v1/adapters # → {"adapters": [{"name": "chat", "active": true}, ...], "active": "chat"} ``` Names are validated against `^[a-zA-Z0-9][a-zA-Z0-9-]*$`; activate/deactivate calls are thread-safe behind a lock. ### Multi-Tenant Vector Bank (`soup serve --bank`) Serve many per-user personas from one model at KB-per-user instead of a full LoRA each. A VeRA / VB-LoRA bank stores a shared random projection (reconstructed deterministically from a seed — never stored on disk) plus a small per-user scaling vector. The active user is chosen **per request** via the `X-User-Id` header: ```bash # bank.json carries {name, base_model, projection_seed, vector_dim, entries: [{user_id, scaling}, ...]} soup serve --model base-model --bank ./bank.json --bank-strength 1.0 ``` ```bash # Apply alice's persona to this request curl -X POST http://localhost:8000/v1/chat/completions \ -H "X-User-Id: alice" \ -d '{"messages": [{"role": "user", "content": "hi"}]}' # No / unknown X-User-Id → zero-delta no-op (plain base model, no cross-request leak) curl -X POST http://localhost:8000/v1/chat/completions \ -d '{"messages": [{"role": "user", "content": "hi"}]}' ``` The per-token delta is `v_user ⊙ (x @ Pᵀ)`, added to the last decoder layer's residual by a decode-time forward hook. `--bank-strength` scales the delta (magnitude capped at 100). The `--bank` path must live under your cwd. The active user is resolved **per request** via a `contextvars.ContextVar`, so concurrent requests on a threaded server never race on shared state — each request (streaming and non-streaming) gets its own isolated active-user selection, and an absent / unknown id self-clears to the clean baseline. (v0.71.12 / per-request v0.71.17) ### Serve a Trained MoLE (`soup serve --mole`) Serve a Mixture-of-LoRA-Experts adapter trained with `task=moe_lora_routing`. The training run writes a self-describing `mole_manifest.json` next to `mole_gate.pt`; `soup serve --mole