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.
(Unchanged at v0.76.0: the bundled page's script still does not open /api/train/stream. What v0.76.0 did change is the New Training page, whose log panel now opens and follows the run's output through /api/train/logs when you start a run from it, or open the page while one is running (#1687). The progress card stays hidden, because no route reports a total step count for a run the UI started. See the Web UI page.)
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.
(Changed in v0.76.0, #1136, #1190, #1191, #1474: on a loopback bind the URL soup ui opens and prints carries the token, the server swaps it for an HttpOnly session cookie and redirects to the same address without it, and the page holds the token in a closure instead of sessionStorage or window. The QR code above is the non-loopback --public case and is unchanged. See Security.)
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)
(Corrected in v0.76.0, #1223 and #1313: the account below is accurate for the MLX backend only. On the transformers backend no evaluation ran at all. The task wrappers handed the held-out split to the trainer and none set an evaluation schedule, whose Hugging Face default is "no", so the default data.val_split: 0.1 withheld 10% of your rows from training and used them for nothing, and val_loss stayed empty until v0.76.0. It is the first release in which a transformers run fills it. See the v0.76.0 page for what else changed with it.)
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 on the transformers backend. Through v0.75.2 a request with stream: true went through a separate path that skipped the parser and sent the thought blocks; since v0.76.0 a streamed request applies the parser too (#1607). A streamed reply from the transformers backend is still generated in full first and then sent word by word, so the parser (and --structured-output) apply to the whole reply before the first chunk leaves. On --backend vllm, sglang or mii the flag is accepted and nothing reads it. The stripped blocks are not kept anywhere: nothing in v0.76.0 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.