v0.53.9 — Live dashboard, Web UI mobile, bench percentiles, tokenizer trainer
Live SSE training stream
GET /api/train/streamServer-sent events for every on_log / on_step_end / on_epoch_end event. At v0.75.2 the route exists, but the bundled dashboard never opens it, so the dashboard did not move from polling to real-time: the events are produced inside the training process, and the buffer the route reads lives in the soup ui process, so for a run started from the Web UI it carries heartbeats only. See the REST API reference.
Phone-scannable Web UI
soup ui --public # bind 0.0.0.0 + print phone QR
soup ui --public --auth-token "$(python -c 'import secrets; print(secrets.token_urlsafe(24))')" # override the auto-generated bearerThe token must be 16 to 128 URL-safe characters (A-Z a-z 0-9 _ -); a shorter one, or one with other characters, is refused with exit 1.
The QR encodes http://<lan-ip>:<port>/?token=... — scan from your phone and you're in. The token is held behind a lock but is set once, at startup (the generated value, or --auth-token); nothing rotates it while the server runs, so the only way to change it is to restart soup ui.
Three things changed here in v0.75.0. A non-loopback bind without a valid token now exits 2 instead of warning (in practice a token is always present by then, so no option of the command reaches that guard). Authentication covers the read endpoints and the event streams as well as the mutating ones, where before this only the mutating ones were gated. And --public no longer serves /openapi.json, /docs, /docs/oauth2-redirect or /redoc at all: they are 404 off loopback and unchanged on it. See the Web UI page.
Validation loss (v0.75.0)
Soup never stored it, on any backend. The callback read logs["loss"] and never logs["eval_loss"], so an evaluation step left the last training loss in place and re-reported that to every sink: the evaluated number existed nowhere at all. There was also nowhere to put it, no metrics column and no event field, and the display read only grad_norm, speed and gpu_mem out of its keyword arguments.
The live panel gains a Val loss row, and the metrics table and the /api/train/stream frame each carry val_loss as its own series, never folded into loss.
The storage rule is the part worth knowing before you read a series back:
- The panel carries the last measured value forward between evaluations, so the row does not blink.
- The stored and streamed values do not. A step where no evaluation ran records
NULL, so a series has one point per measurement rather than one per logged step. - Existing
~/.soup/experiments.dbfiles are migrated in place, and rows written before this release readNULLrather than a fabricated0.0, because an unmeasured value must not read as a measured one.
The MLX backend feeds all three too, through a hook that had been called on every evaluation and done nothing, because the base-class method is a pass and therefore a silent no-op rather than an error. See the MLX backend page.
Standalone CLIs
soup tokenizer train --input corpus.jsonl --vocab-size 32000 --output tok/
soup bench ./output --p50 --p95 # per-prompt tail-latency percentiles
soup bench ./output --backend auto # auto-detect transformers vs mlxsoup tokenizer train trains a byte-level BPE (vocabulary 256 to 200000) and its special tokens default to <pad>, <s>, </s> and <unk>. --special-token is repeatable, and passing any of it replaces that default list instead of adding to it, so repeat the defaults you still want: --special-token "<|im_start|>" alone produces a vocabulary with no <pad>, <s>, </s> or <unk>.
Reasoning parser
soup serve --model ./output --reasoning-parser deepseek-r1 # | qwen3 | phi4 | openthinker
soup serve --model ./output --max-model-len 8192 # vLLM only (v0.73.0): lower it when the
# engine refuses to start because the KV
# cache does not fit. Default: the model's own maximumStrips <think>...</think> blocks (or OpenThinker's thought tags) from the reply of a non-streaming request on the transformers backend. A request with stream: true goes through a separate path that does not apply the parser, and on --backend vllm, sglang or mii the flag is accepted and nothing reads it. The stripped blocks are not kept anywhere: nothing in v0.75.2 writes them to the Tool Outputs panel.
Tool outputs polling
GET /api/tool-outputsJSON poll of the most-recent tool-call records — paired with the Web UI Tool Outputs panel.
Tests
- 8,257 → 8,285 (+28)
See also
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.