Repository navigation
basal 2.5.0
basal 2.5 (2 November 2026). Default answers to decision requests are byte-identical to 2.4.1 (checked on an H100:
basal-omni-1.0-mini, basal-omni-1.0-max and basal-omni-pico with --mode fast, mini and pico with --mode eager); what
is new is opt-in unless a line says otherwise. Changes on the default path: a question named <task>::<part> next to
task questions is refused (#59); error bodies gain code / detail (#62); GET /v1/models lists the certified
thresholds and the context (#64); every response carries x-request-id (#68).
Scale out and operate
- basal-router: several replicas behind one port. Requests over the same state go to the same replica (state-hash
routing with bounded load), so its state cache keeps working.--gpus 0,1starts one replica per GPU and stops them
with the router, also when the router is killed. On two H100s the router served 1.58x the requests of one GPU
(twice the clients); scaling is not linear yet. - Cascade (
--escalate,--escalate-below): low-confidence questions are asked again of a bigger model, and every
answer says which model answered (answered_by).--routepicks a replica by the script of the state. - Multi-level cascade in basal-router: repeat
--escalate URL ... --escalate-below Pfor ordered levels (e.g.
basal-omni-pico -> mini -> ultra), each with its own threshold; with two or more levels every answer names its level
and model, /metrics counts answers per level, and a level that is down leaves the previous answer. One level answers
as before. Accuracy and the share each level answers: docs/RESULTS.md "Cascade: pico, mini, ultra".
Measured on a held-out half: mini -> ultra at 0.95 serves about 1.5x the requests of ultra alone on one H100 at the
same accuracy (within 0.5 pt); at 16 clients its p95 latency is higher than ultra's (docs/RESULTS.md). - basal-router --blend (opt-in): the weighted mean of several models' probabilities; 0.6 ultra + 0.2 mini + 0.2
pico scored +0.45 pt above ultra alone on a held-out half (95% interval +0.09 to +0.81). A quality option, not a
speed option: every question goes to every model. Blended answers are not calibrated; refit thresholds with
basal-calibrate before automating them. - GET /metrics (Prometheus), HTTP 503 while the model warms up, and a failed start-up exits instead of hanging.
- LLM fallback in basal-router (opt-in:
--fallback URL --fallback-model NAME --fallback-below T): after the last
level, a choice question whose top probability is still under T goes to a chat model you serve behind an
OpenAI-compatible endpoint, and its answer ("answered_by": "fallback") replaces the basal one. With
basal-omni-1.0-ultra and, as an example, Gemma 4 31B-it with thinking at T = 0.8, on a held-out half: +1.40 pt over ultra alone (95% interval
+0.90 to +1.89; 8.4% of the questions sent), when no call is cut by the timeout. One call took 54 s at the median and
308 s at p95 (one H100, one request at a time); with the default--fallback-timeout 60, 46 of 100 calls kept the
basal answer, about three quarters of the gain on that sample (--fallback-timeout 600for offline or review use).
Fallback answers are not calibrated, and they repeat only with the endpoint settings in docs/SERVING.md
(batch-invariant vLLM, no prefix cache, no chunked prefill).
Faster start-up
- basal-prepare and
--prepared(auto): the kernels tuned for this GPU and version are saved once and loaded at
start-up. basal-omni-pico starts in 72 s instead of 287 s and gives the same answers in every start; basal-omni-1.0-mini
starts in 186 s from empty caches instead of 315 s (H100).
Answers you can rely on (opt-in)
- Answer sets and abstain:
"coverage": 0.9returns the set of options that holds the right one with that
probability; withmax_error, plain answers get"abstain": truebelow the certified threshold. - Second read (
--reread T): answers below T are read again (the state twice) and the two reads combined;
--reread-imagereads an image again at more soft tokens. - Option order:
basal-bench --order-checkmeasures how often the top answer changes when the options are reversed
or rotated;--order-average Kserves the mean over K option orders, all reading the state once. It helps
basal-omni-1.0-mini only (+0.6 pt top-1 on our development set with K = 2, at about twice the latency per question);
on basal-omni-1.0-max and basal-omni-pico it does not help, so it stays off by default. - Two-way models (trained to read the state in both directions) in the fast engine, and isolated options
(basal.json"option_isolation") for models trained to read the options as a set.
More hardware and models
- basal-omni-1.0-ultra in block FP8 (
--mode fp8, one 80 GB GPU: 1.35x the decisions/s of bf16, 53 instead of
74 GB at 16 clients) or NVFP4 (--mode nvfp4, Blackwell: 2.3x the scoring speed, 2.4% of top answers change).
Ultra only: other basal-omni models are refused in both modes. In block FP8 the state cache is off in 2.5.0:
--state-cache-gbis not used (a warning at start-up; requests are served as without the cache) until 2.5.1. - FP8 checkpoints on one 80 GB GPU: the
-FP8repositories load without a bf16 copy on the GPU, so
basal-omni-1.0-ultra-FP8 serves on one 80 GB H100 in--mode fastand--mode fp8(before: out of CUDA memory at
load); checkpoints that fitted before load the same tensors bit for bit. - basal-omni-1.0-ultra in int8 (
--mode int8, Ampere and newer, for GPUs without FP8): 99.4% of top answers as
bf16, with its own calibration. Ultra only. --state-cache-bits 8: cached states as 8-bit integers, about 1.7x as many states per GB; 99.45% of top answers
as the 16-bit cache on basal-omni-1.0-max.- basal-omni-pico in the fast engine with the state cache and several questions over one state.
- Apple Silicon:
--mlx-forward basalruns the last layers at the answer rows only: basal-omni-1.0-mini-MLX
answers one prompt in 131 ms instead of 225 ms (M4 Pro). mlx-lm's forward stays the default. - basal-attach: serve a chat model that was never trained on basal (Gemma 4, Qwen, ...) as a decision model; it
starts uncalibrated and basal-calibrate fits it on your labelled requests. - basal-agree: any backend against the native engine: top-answer agreement with a 95% interval, |Δp|, paired
accuracy, and whether each question gets the same answer alone as in a request. - basal-omni-pico on
--mode mlxkeeps float32 matmuls on an Apple M5 (TF32 off unlessMLX_ENABLE_TF32is set; not
yet verified on M5 hardware; on an M4 Pro the answers are byte-identical to before).
Clients and integrations
- TypeScript client (
clients/typescript, no runtime dependencies): typed questions and answers, answer sets,
abstain,ask_if/depends_on, images and errors; build it from the repository (not on npm yet). - Presets (triage, moderation, compliance) in
basal-run --presetandBasal.preset; thin LangChain, LlamaIndex and
CrewAI tools over the HTTP API (basal.adapters; SERVING.md "Agent frameworks"). - The Python client talks to other /v1/systemone servers (bearer key, list criteria); the server takes
ask_if/
depends_on, animagesalias and--error-format openai. - The calibration report shows the temperatures as served.
Start-up and operations
- A live web page for basal-serve (
--ui on, off by default): status, traffic since start, the state cache, GPU
and host telemetry, the configuration and the last 200 requests' metadata (never their text;--ui fullalso shows
each request and its answer). Open one page at a time: on one H100 (basal-omni-1.0-mini) throughput within 1% of
--ui off, p50 at 8 clients +0.4-2.2% with 1-2 pages and +5.2% with--ui full; at 1 client answers byte-identical. - One page for basal-router and all its replicas (
basal-router --ui on, off by default): routing, readiness,
requests in flight, failures and the router's last 200 requests, next to one card per replica with its own status,
traffic, state cache and GPU telemetry (replicas started with--ui on). On 2 H100 (basal-omni-1.0-mini, one page
open): answers at 1 client byte-identical to--ui off, p50 at 8 clients +1.3%, within--ui off's own spread. - Prepared start-up shipped with the model: the model repositories carry
prepared/<key>/for the H100
(basal-2.0-mini, basal-omni-pico, basal-omni-1.0-mini, basal-omni-1.0-max);--prepared autofetches only the
directory for your GPU and versions, and the answers are byte-identical to a cold start. First start on an H100 with
empty caches: basal-omni-1.0-mini 290 -> 172 s, basal-omni-1.0-max 259 -> 164 s, basal-omni-pico 105 -> 81 s. - basal-omni-pico starts the same every time on the H100: fla's kernel configs are pinned, so empty, warm and long-used
caches give bit-identical answers. 2.4.1's pico fast answers could differ between starts (kernel autotuning); 2.5's do not. - torch.compile's cache survives a reboot:
~/.cache/basal/torchinductorinstead of/tmp(TORCHINDUCTOR_CACHE_DIR
still wins); RTX 4060 Ti, first start after a reboot: basal-2.0-mini 417 -> 238 s (#47). - basal-omni-1.0-ultra on a 48 GB card:
--quant-load cpuquantises one layer at a time on the GPU, so ultra in
--mode fp8orint8fits a 48 GB card, and--graph-memory-gb Nsizes the CUDA graphs for N GiB, so every GPU
given the same N answers the same: on an H100 with 32 GB taken, 1,700 of 1,700 answers bit-identical to the unloaded
card (QUANTIZATION.md). Also fixed: with little free GPU memory, a prompt just past the largest compact graph no longer fails (HTTP 500). - /metrics: the engine version, state-cache evictions and prefix hits, a read that never waits for the engine (#48).
- AMD Strix Halo (
--mode strix, community-maintained, not yet verified on our hardware): start-up compiles every
Triton kernel copy, so no request waits for one (#51); it needs a C compiler at start-up (build-essential). Short
requests no longer wait for a clocked-down GPU: one short question 390 -> 161 ms on a Radeon 8060S, answers unchanged.
Speed and models
- Less host work in the CUDA cascade (basal-2.0, many questions over one state): pass 2 for 40 questions 62.4 -> 48.6
ms on an A100, every probability bit-identical (#44; H100: a 40-question request 54.0 -> 51.4 ms, bit-identical). - basal-omni-pico:
--prefix-cache onand--batches compact(with--readout-layer on) in the fast engine, with
answers as close to fp32 as--mode eager's (1,700 held-out decisions, H100); compact rows serve 1.11-1.16x the
short single questions per second at 16 clients (no speed-up on long states). - basal-omni-pico on a Mac:
--mode mlxserves it from its own Hugging Face repository (bf16 weights, computed in
float32); on 1,700 held-out prompts its answers match CUDA eager float32 (top-1 100%).--mode mpsstays refused for it. - basal-omni-1.1-max images and audio through
--mode vllm --media on: the same top answer as the eager media path on
97.4% of images and 95.8% of clips (every one where eager's margin is at least 0.2), 2.7x / 2.9x the requests per
second at 8 in flight;"detail": "high"stays on--mode eager. --mode vllmreads in vLLM's own kernels, so its answers are close to--mode eager's, not equal: top answer
the same on 98.3% of 361 readouts (basal-omni-1.0-mini), 98.3% (basal-omni-pico), 98.6% (basal-omni-1.0-max) and
97.6% (basal-omni-1.1-max), H100, 27 Oct. vllm answers may differ from eager on near-ties; vLLM's default preset varies
from run to run; use--vllm-preset deterministicfor reproducible answers. Deterministic preset: bit-identical to
2.4.1 on 4 models; not available for basal-omni-pico (vLLM has no batch-invariant GDN kernel; basal-serve refuses it
before vLLM starts).- Experimental
--deterministic --lanes N(still behind BASAL_EXPERIMENTAL_LANES): now exact, not yet faster.
Tools and integrations
rankandbasal-navigate(experimental): rank thousands of candidates in one request (yes / no screening, then
one choice over the top 20), and retrieval by navigating a document tree level by level, no index. With
basal-omni-1.0-max on one H100 against a dense 8B embedder: MultiDoc2Dial R@1 +9.6 (44.3 vs 34.7), ConditionalQA
Hit@1 +19.2 (63.5 vs 44.3). It is slow: one query at a time on that H100 a search takes p50 3.0 s / p95 3.5 s per
MultiDoc2Dial turn and p50 9.2 s / p95 18.4 s per ConditionalQA question; at 8 concurrent clients the median is 24 s
and 64 s. Details: docs/NAVIGATE.md.basal-navigate search --split-at N(experimental, off by default): a level with more than N candidates goes
out as several requests of at most N, so trees beyond--rank-max-candidates(10,000) are searched without refused
requests. Not faster: Natural Questions (2.68M paragraphs, one H100) 51.6 s per question vs 51.4 s with one request
per level; scores differ by at most 0.045, the first passage and the top 10 the same on every measured question.- Tokenize once: the server tokenizes the part a request's prompts share (template and state) once and each
question's own tail after it, for the tokenizers of the basal models; the engine reads the same token ids as
before. Arankrequest of 300 candidates over a 12.5k-token state: 0.14 s of CPU instead of 5.8 s (one thread). - Guard and route presets:
guard_input,guard_outputandroute_intent(Python and TypeScript), strict or
permissive. Their default thresholds are hand-picked, not certified; withmax_errorthey use the served model's
own calibration per question type. SERVING.md "Presets". - basal as the local backend of your decision code (docs/BACKEND.md, a one-function example) and a basal skill for
coding agents (skills/basal). - basal-finetune: LoRA on your own labelled decisions (docs/FINETUNE.md; tested on Apple Silicon with
basal-omni-pico, other GPUs not yet). basal-selfcheck: compares your setup with shipped reference answers;
docs/OLD_GPUS.md runs basal-omni-pico on older GPUs (GTX 10xx, RX 480 / 580, Intel Iris Xe) through llama.cpp's Vulkan
backend. - basal-serve answers the OpenAI Decisions API (
/v1/decisions,client.decisions.create), translating to the same
readout as /v1/systemone. - LM Studio (opt-in):
basal-export-gguf --systemoneturns a basal -GGUF file (prompt format 2) into an LM Studio
decision model: LM Studio 0.4.26+ (beta channel) with llama.cpp engine 2.65.1+; choice / score / yes-no, up to 26 options;
on LM Studio's /v1/systemone and /v1/decisions, calibrated temperatures, no basal-serve needed. LM Studio stops a
request after 30 s: 5 of 306 long requests of the basal-2.0-4.5B check hit it, all while macOS background media
analysis was busy (none on an otherwise idle M4 Pro; INSTALL.md: the workaround); the 607 answered questions agree
with basal-serve on every top answer (probabilities within 0.0041; basal-omni-pico 620 / 620, within 0.0013). - For contributors:
make helpand read-only pre-commit checks (#46). - Continuous integration: every pull request runs ruff and the CPU tests on Linux and macOS (#53).
- State cache on Apple Silicon (
--mode mlx):--state-cache-gband--prefix-cache on, both--mlx-forward
paths, with answers bit-identical to the cache off; a cached 2.1k-token policy 13.7 -> 0.9 s per request on a
MacBook Air M4 (#50). - Accuracy by document length: docs/RESULTS.md and each model card show accuracy per document-length bucket, with
the number of items in each. - The state cache keeps branches: cached states that share a history are all kept, and a request continues from
the longest one it can extend (#56). On branching agent histories (H100) it reused 1.6-1.7x the prefix tokens of
2.5's cache, with answers as close to the uncached ones.
Fixes and contributions (community batch, 28-29 Oct)
- Tasks: in a request with task questions, a question named
<task>::<part>is refused (HTTP 422) instead of being
read as the task's own expanded question (#59). - Errors: an invalid
max_erroror set bound is refused with HTTP 422 before the model runs, and every error body
carries acode(INVALID_REQUEST,OUT_OF_MEMORY,INTERNAL_ERROR; a 422 alsodetail);errorkeeps its text
(#62). GET /v1/modelslists the certified thresholds permax_errortarget and question type (when the calibration
matches the model) and the context: limit, overflow policy and truncation head share (#64).- MCP: a server with several models offers
list_modelsand amodelargument ondecide/decide_many;
basal-mcp --model NAMEsends that model with every request; one-model servers list the same tools as before (#66). basal-serve --api-keys PATH(opt-in): SHA-256 digests of the keys in a JSON file, every route butGET /health
needs a key, admins see key use atGET /v1/keys;--cors-originsfor browsers; every response carriesx-request-id
(#68); basal-router passes a client's key on to the replicas it reaches (never to--fallback).basal-interpolate: linear weight interpolation of two checkpoints of one base model, streamed tensor by tensor;
quantized or mismatched pairs are refused (#60).- LoRA adapters at load (
--adapter DIRon basal-serve, basal-calibrate and basal-bench;basal-fuse-adapter
writes the merged checkpoint): mlx-lm or PEFT LoRA (rsLoRA too) merged with PEFT's arithmetic; answers equal the
fused checkpoint's and PEFT's merge's bit for bit (E4B and 31B on an H100 in fast / eager / fp8 / vllm, E4B in mlx,
a basal-finetune pico adapter). Refused with a message: GGUF, FP8 checkpoints, fast-exit, nvfp4, int8, vllm for
Qwen3.5, DoRA (#52). - basal-calibrate records a Hugging Face cache snapshot as its repository id and commit, so the file is applied when
that snapshot is served (#71). basal-viz explains the state with the facts basal-serve adds for"facts": "auto"
(#73) and reads basal-omni's Gemma 4 text decisions (#75), and on a Mac the MLX engine's layers (--mode mlx/
mlx-q8, Llama-architecture models) (#78). The float32 MLX tests pass on M5 GPUs (#76). - Python client: a method for every question and task type,
decidewith question builders,AsyncBasalfor
asyncio, and errors by status (InvalidRequest422,OutOfMemory503 withretries=,ServerError), all still a
RuntimeError(#58). - A request with an empty
"questions"object is refused with HTTP 422 (INVALID_REQUEST); 2.4.1 answered it with
HTTP 500. - Replicas that basal-router starts with
--gpuslisten on 127.0.0.1 only (before: 0.0.0.0, reachable past the
router); a--hostafter--still applies. - Edited chat states with the state cache (
--mode fast --state-cache-gb) could return HTTP 500 with NaN answers (GPU
memory never zeroed); basal-omni-1.0-ultra, one H100: 27 of 1,248 edited-chat requests before the fix, 0 after.
2.4.1 had the same bug.
Faster, with a measured trade-off (opt-in, off by default; each changes some answers, see WHATS_NEW for the numbers)
--batches compactfor basal-omni on Blackwell workstation GPUs (flex kernels, RTX PRO 6000): mini 1.17-1.20x,
max 1.21-1.27x the questions per second at 16 clients; the same top answer on 98.0-99.75% of readouts.--global-attn triton(Hopper): basal's own kernel for basal-omni's 512-wide global layers; on 24k-32k-token states
mini 1.10x, 1.1-max 1.16x, ultra 1.03x; the same top answer on 98.3-99.7%.--vllm-overlap on(--mode vllm): up to four engine calls in flight; 1.01-1.07x (1.21x for mini, one question
per request), the same top answer on 98.2-99.7% (two runs with it off differ about as much). With--media onit
batches media requests too: basal-omni-1.1-max on an H100, new images 1.18-1.49x, audio about the same (0.89-1.23x);
the same top answer as off on 99.65-100%.--compile-scope layer(basal-2.0, Llama): start-up 47 s instead of 228 s on basal-2.0-mini (H100, cold caches); the
same top answer as a whole-model cold start on 98.7% (two whole-model cold starts: 98.3-98.5%) (#49).--long-lane-gdn on:--long-lanefor basal-omni-pico; five questions per state 0.12 pt fewer top answers equal to
float32 than eager bf16 (99.39% against 99.51%).--mode mlx --mlx-compute bf16for basal-omni-pico on a Mac: 0.136 s per prompt against the GGUF Q8_0 path's
0.171 s (1.25x, M4 Pro); the same top answer as CUDA float32 on 99.41% (the default float32 compute: 100%).
Known limits
- Prepared start-up from a model repository does not load its kernels in 2.5.0 (found after the release):
--prepared autotakes the repository's
prepared/<key>/without itsinductor.bin, so the server compiles as on a cold start (same answers, no time saved). Workaround: download
the directory once before the first start, e.g.hf download Remek/basal-omni-1.0-mini --include "prepared/*". A directory written by
basal-prepareon your machine is not affected. Fixed in 2.5.1. - Repeated requests over long states: with
--mode faston one H100, basal-omni-1.0-ultra gives bit-identical
probabilities for the same 24k-32k-token request sent again (one at a time) in 95 of 100 cases, as 2.4.1 did;
basal-omni-1.0-mini in 100 of 100. Ultra's answers on long states are not guaranteed to be bit-stable. - With
--mode fastand--state-cache-gb, a rare race can fail one request with HTTP 500 (an error about FX symbolic
tracing): about 1 request in 8,000 in our tests; the server recovered and kept serving (retry the request). Present
since 2.3.1.--state-cache-lock on(opt-in) removes it at a measured cost: one H100, basal-omni-1.0-max, 8 clients,
8,740 requests: 0 failed instead of 1, 8.19 instead of 8.88 requests/s (-8%), p50 630 instead of 577 ms.
From the community: Miłosz Kucharski (@Butterski: #44, #47, #49), Paweł Kiszczak (@pawelkiszczak: #46, #52, #62, #64, #66, #68, #71, #73, #75), Marcin
Brzeżański (@marcinbrzezanski: #48, #56), Dariusz Koryto (@dkoryto: #53, #58, #59, #60), Grzegorz Stark (@gstark0: #50),
Michał Kardyś (@kardysm: #51), Grzegorz Brzezinka (@agentGreg: #76, #78). Thank you.
Coming in 2.5.1: certified thresholds for the ready question sets (today they are hand-picked), int8 on Apple M5,
the state cache in block FP8 (--mode fp8), and a fix for the rare HTTP 500 with --mode fast and the state cache
without --state-cache-lock's throughput cost.