Repository navigation
Releases: cryptoAGI/bankml
Release list
bankML 0.4.4 — .memory versions, with gitmind
.memory keeps its versions. It is the one file the UI rewrites: removing a note wrote the file again without
it, and the note was gone. Now every add, remove and restore is a commit in a private git repository of its own
(<state>/memgit/<name>-<8 hex of its path>/, one per .memory, Savante's and each agent's), and gitmind records
each change in its ledger. .history is
not versioned: it is append-only and already committed by its Merkle root. Embeddings are not versioned either: the
vectors are derived, rebuilt from the text and the model's digest.
Savante: .memory versions (sAGI/memgit.py, sAGI/gitmind/)
- A commit for every change.
memory_addandmemory_removecommit.memory(add note 3: …,
remove note 1: …). The repository holds a copy of.memoryand nothing else, so.history, its vectors and
receipts are never committed. It is never made in the file's own directory: an agent's directory is already its
own git repository (the THOT rung's locator), where.memoryis git-ignored on purpose; that repository is left
untouched. The commits carry a fixed identity (bankml .memory), no signing and
no hooks, so the operator's global git settings cannot stall or change them. - Restore, forward only. The
.memorytab lists the newest versions; restore that version puts.memoryback
as it was at that commit, as a new commit (restore savante.memory to …). History is never rewritten, and a code
that is not a version of.memoryis refused. - gitmind 0.1.0, vendored (
sAGI/gitmind/, from github.com/Professor-Codephreak/gitmind, commit c4b379c; same
authors,MIT OR Apache-2.0here, its MIT licence kept). After each commit it takes a snapshot, so its ledger
(<dir>/.gitmind/ledger.jsonl) shows every change and marks a move backwards as a rollback. It runs with no
backup sources: its IPFS source would publish every version of.memory, which never leaves the laptop. - Optional. Without git, or with
BANKML_MEMORY_GIT=0, nothing is versioned and nothing fails. - Tests:
testing/test_ui.py— one version per change, only.memorycommitted, a removed note restored, codes that
are not versions refused, the ledger written with no backups, the switch off, and an agent's own repository untouched (108 / 108);
testing/spdx_check.py122 / 122.
bankML 0.4.3 — browseML: bankML in the browser; the console's first answer
bankML's own engine, compiled to WebAssembly, answers in the visitor's browser: verified as serve verifies (guard,
sha256 pin), with serve's receipt, and identical to llama-server b11192's record on 9 of 9 answers, on one thread
and on Web Workers, with plain and relaxed SIMD (exact everywhere it runs; refused where relaxed_madd is not
fused). The f16 paths and the tiled attention kernel joined the 1-bit kernel in exact SIMD (+19 % writing on two
threads). With it: the console's first answer from a warm persona prefix (SELF out of the system prompt, a window
that keeps the cache), .context (bankML and Savante know their own code, with sources), Savante's machine panel and
the Savante | bankML switch both ways, the console's Diagnostics desk and Inspector, .memory per response window, and
the Space's lean landing. Record: testing/results/0.4.3.txt.
browseML — bankML in the browser (browseML/, hf/space/browseml*.js, tools/browseml.sh)
- The same engine, compiled to WebAssembly (
wasm32-wasip1, andwasm32-wasip1-threadsfor cross-origin-isolated
pages), runs on the visitor's CPU in a Web Worker. Bonsai-1.7B Q1_0 (248 MB) is downloaded once at a pinned revision
and kept in Cache Storage; the engine verifies it asservedoes (guard, sha256 pin) and putsserve's receipt on
every answer. Nothing to install, no account, no server. - Exact:
browseML/testing/oracle.mjsgives 9 of 9 answers identical to llama-server b11192's record (text,
finish, counts) in Node; through 4 threads in Chrome, the first three likewise. - What the engine needed (the native build unchanged):
gguf::Mmapreads the file into one shared buffer where there is no mmap.serve::identwithout unix metadata.- The Vulkan loader reports no card.
- The prompt cache's 8 GiB default fits a 32-bit
usize. - An unseeded request's seed comes from the clock alone (there is no process id).
- Rope's
cosf/sinfare glibc's own algorithm (bankML/sincosf.rs, from ARM's optimized-routines; equal to
glibc's on all 2^32 inputs). Rust's WebAssembly libm differed in the last bit on about 1 % of the rope's angles,
which parted one oracle conversation from the reference.
q1_0::simd128_core, bit-identical to the scalar reference:- integer sums vectorised;
- the inner multiply-adds exact in f32, so a multiply then an add;
- the outer fused multiply-adds through f64 SIMD, with the scalar
mul_addonly for the rare inexact halfway lanes. - About 7× the first build on one thread.
- Threads and relaxed SIMD. Cross-origin-isolated pages (the Space sends COOP and COEP) run the engine's pool on
Web Workers, and Chrome and Edge getbrowseml-mt-relaxed.wasm. Its kernel isq1_0::relaxed_core:- the sign sums as llama.cpp's AVX2 "sel" kernel computes them, with
relaxed_dotagainst 0/1 bits; - fused
relaxed_maddfor the reference'smul_add. - The engine refuses this build where
relaxed_maddis not fused, and the page falls back tobrowseml-mt.wasm. - On a 2-core laptop, 4 threads: prompt 5.16 tok/s and writing 2.71, against 1.43 and 1.11 on one thread.
- 9 of 9 oracle answers identical in Node and in Chrome.
- the sign sums as llama.cpp's AVX2 "sel" kernel computes them, with
- The f16 paths and the tiled attention kernel in WebAssembly SIMD (
bankML/wasm_simd.rs):f16::vec_dotwith
the reference's accumulators and reduction order (f16 × f16 is exact in f32),f16::widen/mad/scalefour lanes
at a time, and the tiled kernel's score chains and V update through an exact four-lanefmadd(relaxed_maddin
the relaxed build, otherwise f64 with a scalar fallback for the lanes where rounding twice could differ). (The commit that
added it, 8265ddb, says the Space's modules were already built from this code. They were not: the Space serves what
the committed source built before it, sha256 29243a07… forbrowseml.wasm; only local builds had it.)- Oracle:
browseml.wasm9 of 9 andbrowseml-mt-relaxed.wasmon 2 threads 9 of 9, identical to llama-server
b11192's record. - Speed: measured old/new interleaved (3 rounds, both orders, Bonsai-1.7B, a 20-token prompt and 48 tokens written;
load average 1.4–2.3): writing on 2 threads (relaxed) median 2.76 against 2.31 tok/s (+19 %; ahead in 6 of 6,
the slowest new run above the fastest old), on one thread 1.62 against 1.52 (+6 %, ahead in 5 of 6, at the edge
of the noise); reading a short prompt unchanged within the noise.
- Oracle:
tools/browseml.sh test: the kernels' unit tests (wasm_simd,f16,q1_0) onwasm32-wasip1under Node's
WASI (browseML/testing/wasi-run.mjs), with SIMD and with relaxed SIMD, 13 of 13 each: every f16 value through
widen4, about a millionfmaddlanes againstmul_add. Those lanes include the double-rounding trap (an f64 sum
that lands on an f32 midpoint), and the test fails with the fix removed. These kernels had only the end-to-end oracle
before.- No silent wait. An activity line at the top of the page, mirrored in the response window and the dashboard,
shows each phase:- the download (MB, MB/s, time left, a bar);
- verification (a running clock);
- reading the prompt (elapsed against an estimate);
- writing (tokens, speed).
- The window keeps the timings as the answer's record.
- A question asked during a download waits for it.
- On the Space, "bankML in this browser" is the default answerer, and the page asks in the ultimate input field
(uif-space.js). Behind the field,space-dashboard.jsshows this computer's controls and diagnostics:- controls: threads, context, answer length, a lean or full prompt;
- measured: reading and writing speed, first token, the engine's memory, storage;
- what to change, worked out from those measurements.
The persona's prefix kept warm, SELF out of it, a window that holds the cache
- SELF moves out of the system prompt. It changes with every question, and inside the first system message it
made the engine recompute everything after it, the rest of the persona and the whole conversation, on every turn
(Bonsai-8B on the laptop: 491 prompt tokens from nothing, 200 s to the first token; the next turn reused 376 of 551,
66 s). The persona is now the first system message, unchanged; SELF is a second system message just before the
question (console.messages; Qwen3's template renders a later system message as its own turn,chat.rs). - Warm before the first question. A newly started engine (its
hashed_atchanges) that has answered nothing gets
the persona's saved slot restored, or else the persona prefilled (one token) and its slot saved (console-<hash>.bin
beside Savante's, newest three kept); a question that arrives meanwhile closes the prefill, and the engine stops and
keeps what it computed (0.4.1)./api/statesays what the warmer did (warm). - A window that holds the cache. The conversation sent with a question was cut to its last 12 messages by the page
and again by the console, so past six exchanges the start moved every turn and nothing was reused. The page now
sends up to 40 and the console trims four exchanges at a time (console.window: 12 to 19 messages kept), so the
start moves once every four turns. The input field's source changed the same way (ultimate-bankml-ui
console-mount8c88972,consoleEngine.ts). - A
max_tokensthat is not a number is refused with a 400 instead of a dropped connection.
Savante: the machine, and more glass
- Savante loads Savante. Her page started with the mindX agent (
BANKML_AGENTdefaulted tomindx), so the
side card read "mindX · open the card" with a letter for a face; now she starts as herself, from the canon, with
the portrait the canon names (gfx/Savante3.png, the operator's choice of 2026-09-16), and mindX loads in mindX,
bankML in the console.BANKML_AGENT=<slug>still starts with an installed agent. - sAGI on her card, read from the canon: "Savante is the prototype for sAGI. Savante knows.", the three laws,
the agent facet (savante === sAGI.agent) and links to sAGI.md, sAGI.agent and the sAGI engine. - Fixed: the card opened inside the side column instead of over the page. A
backdrop-filterortransformon
an ancestor makes it the containing block of aposition: fixedmodal; the side column and its blocks now carry
neither (and a denser surface instead). This was in the first theme too. - Fixed: the portrait washed out on a busy machine. Gradio dims a component to 20 % while its update waits in
the queue; the side column's widgets refresh in the background, so they are no longer dimmed. - When bankml serve does not answer, Savante says why and gives the installer's commands (
./install.sh model,
restart), and that a release gate is running when one is, instead of a hand-typed serve line with a placeholder
pin. - The Savante | bankML switch works both ways, and says when the other side is not running. Its targets were
fixed ports (7873, 7875); nowBANKML_SAVANTE_URLandBANKML_CONSOLE_URL(defaults unchanged). The console
checks Savante server side (its CSP allows only itself;/api/state→savante: {url, up}), Savante checks the
console from the page every 15 s; a side that does not answer is struck through, and a click says how to start it
(./install.sh start) instead of opening a dead page. Savante's switch now changes the tab, as the console's
does (Gradio opened it in a new one). - Admin → the machine: ping bankML (three round trips to
/health, min/avg/max) and diagnostics — CPU,
memory, disk, GPU and the engine as an accordion, the sections that need a look open (savante.diag_html,
ping_html; the samesAGI/sysdiag.pyas the console, which now shares itsping,engineandto_html). - A health line in the side panel, under the timer: the engine (up, its ping), the CPU's temperature and the
memory av...
bankML 0.4.2 — logprobs on Ollama's API, receipts over logprobs, max_tokens: 0; with 0.4.1
0.4.1 was never tagged (its gate run stopped mid-way), so this release carries it. From 0.4.1: a native run stops
when its client goes away; every GPU object is released on drop; the ternary kernel runs on the GPU bit-exact; CI on
aarch64; the console's SELF says who is speaking. From 0.4.2: Ollama 0.20's logprobs/top_logprobs on /api/chat
and /api/generate with llama-server b11192's entries; logprobs_sha256 on receipts; max_tokens: 0 and
num_predict: 0 as llama-server and Ollama read them; one theme and a dark-mode switch for the console and Savante;
Savante's exact calculator; install.sh restart. The batched GPU submissions and the F16 and eight-lane ternary GPU
kernels are written but unverified on the card, and stay for 0.5.0. Record: testing/results/0.4.2.txt.
Measured (2026-10-08, Bonsai-1.7B-Q1_0, against llama-server b11192)
logprobs_ollama_oracle_live19 / 19:/api/chatand/api/generate, streamed and not, entries and receipts.maxzero_oracle_live7 / 7: the sixmax_tokens: 0cases (recorded from llama-server: it samples one token) and
num_predict: 0answered as-1.logprobs_oracle_live(/v1) 14 / 14 unchanged;cargo test --release106 passed; clippy clean.- The oracle's own extraction of the logprobs array ended a string at
\"(its escape flag was recomputed before the
quote was checked), so a top-20 entry whose token is a quote or a backslash failed the receipt check (18 / 19). Fixed
inlogprobs_ollama_oracle.py; the receipt was right.
./install.sh restart and models.py restart
- A fresh
bankml serveon the model it serves now, after a build or a pull: the same pinned file, resources and
origin, the whole file hashed and verified again, then the UIs that were running. Before, nothing did this:
models.py use FILEon the file already served returns at once (its sha256 already answers), andinstall.sh modelstarts the default model, and only when nothing serves. Measured: 10 s on Bonsai-8B, pid replaced, verified. install.sh --helpprints up to its-hline instead of a fixed line range (two lines added had cut it short).- Fixed:
./install.sh stopstopped only the first port.pid_onfor a port where nothing listens (7874, the
LAN view, usually off) madegrepfail, and underset -euo pipefailthe script ended there without a word: 7873,
18093 and 18092 kept running.pid_onnow answers empty instead.
The interfaces: one theme, a dark-mode switch, a way back, and a calculator
- Console and Savante share one look (
sAGI/console/theme.css,sAGI/savante_theme.css): depth from the border
(an edge, an inset highlight, a near and a far shadow), glass surfaces over a faint wash, and a question field with
strong contrast against its page and its text. T mode is terminal green on black in both themes. Savante keeps its
own colours (paper, ink, gold) and a serif wordmark. - A ☾/☀ button in each bar flips light and dark; the system's choice is the default and a click is remembered per
browser (Gradio'sdarkclass on Savante;data-themeon the console). - A way back: Savante's masthead now carries the same Savante | bankML switch as the console.
- The DeltaVerse $ is the console's tab icon too (
/favicon.ico, the file Savante and view already serve). - Savante's calculator (
sAGI/calc.py, docs/modules/calc.md): exact arithmetic withouteval(ast, bounded;
integers and typed decimals exact,0.1 + 0.2is0.3). A Calculator panel beside the conversation (keypad,
terminal display, a tape of results); arithmetic in a question is computed and handed to the model with it, and
shown under the answer (🧮 … — exact), recorded in.historyundercalculator; a message starting with=is
answered by the calculator alone.testing/test_calc.py(48 checks) runs in the gate.
Logprobs on /api/chat and /api/generate (logprobs_ollama_oracle_live)
- Ollama 0.20's request fields:
logprobs: trueandtop_logprobs0–20. Out of range is a 400 with Ollama's own
message, before any model is loaded; withoutlogprobs,top_logprobsis ignored, as Ollama ignores it. - The answer takes the per-token path
/v1takes (NativeChat::run_steps_while), so the entries are llama-server
b11192's: the same tokens, texts, bytes and 32-bit floats, written in Ollama's shape (noid;bytesand
top_logprobsleft out when empty). Not streamed, the list is the top-levellogprobs; streamed, each line carries
its tokens' entries. top_logprobs: 0is the chosen token only, computed as llama-server'sn_probs1: the chosen token's float depends
on how many top tokens are sorted, so the oracle compares it with llama-server's top-1 answer.- Requests without logprobs keep the answer path they had.
- Asked for by mindX: its Augur (calibrated yes/no forecasts) reads P(yes) from one forward pass instead of sampling
three answers.
max_tokens: 0 as llama-server answers it; Ollama's num_predict: 0 (maxzero_oracle_live)
- llama-server b11192 checks the generation limit only once a token is generated (
n_gen > 0), somax_tokens: 0
still samples one token, and with logprobs that token's entry is the next-token distribution after the prompt.
bankML stopped before the first token and answered nothing; it now checks the limit as llama-server does.
Recorded against llama-server (greedy, seeded, plain, streamed) before it was accepted. - Ollama's runner limits only when
numPredict > 0(0.20.0), sonum_predict: 0means no limit, as -1 does; bankML
had read it as zero tokens.
logprobs_sha256 on the receipt
response_sha256hashes the text, not the numbers a client may act on. An answer with logprobs now also carries
logprobs_sha256: the sha256 of the logprobs array exactly as written (the body's substring when not streamed; when
streamed, every entry the chunks carried, as one array). On/v1and on Ollama's API.- Without logprobs the receipt is unchanged, byte for byte.
receipts_cover_logprobs_as_writtenandlogprobs_as_ollama_asks_and_answers(unit); live in the oracle above,
which also checks the/v1receipts.
Fixed
- Savante's aivatar card opened trapped inside the side column (318 × 1075 px instead of the whole window): the
column'sbackdrop-filtermade it the containing block of the card'sposition: fixedmodal. The side column
now carries no backdrop-filter or transform, and has a denser surface instead. Its widgets also stay legible while
Gradio refreshes them; they had dimmed to 20 % on a busy machine. Checked in a browser: the card now spans the
window (1366 × 900, a 920 px card).
0.4.1 — never tagged; released in 0.4.2 — a request whose client has gone stops; GPU objects released; ternary on the GPU; CI on ARM
The first steps towards 0.5.0 (hardware), and the two 0.4.x items.
- A native run stops when its client goes away, and keeps what it computed.
- Every GPU object is released on drop. The test found an instance leaked per open.
- The ternary kernel runs on the GPU bit-exact, and
bankml gpu --verifychecks it. - CI builds and tests natively on aarch64.
- The console's SELF says who is speaking, in two lines.
The console's SELF: who is speaking, then one line of measurements
- An A/B by the ultimate-bankml-ui session (bankML 0.4.0, Bonsai-8B, "who are you?", three fresh conversations each,
every receipt ✓): with SELF's first line "the model I am running: Bonsai-8B …", bankML said "I am bankML" 1 of 3
times and "I am … Bonsai-8B" 2 of 3; with "I am bankML 0.4.0. The model file I run is Bonsai-8B (sha256 …): it is
what I compute with, not who I am", 3 of 3. The console's SELF now opens with that line. - The nine measurement lines are one line, each value still with its unit and "not measured" where it was not: the
block a question carries is two lines instead of ten, a smaller prefix to recompute whichever turn it rides on
(the same session measured why that matters for the prompt cache).
The ternary kernel on the GPU, bit-exact (gpu_q2_0_mat_vec_bit_exact, bankml gpu --verify)
q2_0_mat_vecis bankML's own SPIR-V, one invocation per row. Per block it takes the two 32-element integer sums,
thensumi = fma(d1₁, s₁, d1₀·s₀)andsumf = fma(d0, sumi, sumf), the shipped ggml's float order. Both FMAs are
built exactly, becaused1·scan need 25 bits and the Vega 3's driver does not fuseFma.- Bit-exact against
q2_0::vec_dot_refon every row of the 8B model's shapes on the Vega 3: 64×128, 1000×512,
4096×4096, 1024×12288 and 12288×4096, 18,472 rows in all. bankml gpu --verifynow runs it after the 1-bit kernels, in both regimes (uniform and layer-shaped), and refuses a
card that fails either.- One rule the test found: an activation whose f16 scale overflows makes the exact FMA give NaN where the CPU gives
inf, so such a product stays on the CPU. No real activation has one. - Not yet used for inference. At one invocation per row it takes 3.5–4.7 ms per 4096×4096 or 2048×12288 product,
slower than the CPU's ternary kernel; a layout with lanes per row (asq1_0_mat_vec8) comes next.
CI on ARM, natively
.github/workflows/ci.ymlruns a second job on GitHub'subuntu-24.04-arm: the build, the unit tests, clippy with
-D warningsand the guard, on real aarch64. Every kernel there takes its portable path, which the unit tests check
against the same reference models the x86 kernels are proven against. The toolchain is now rust-toolchain.toml's,
not a stale 1.95 pin; the x86 job adds the SPDX and console checks.- The first run found an ARM difference.
/proc/cpuinfothere has nomodel nameand nocpu MHz, so the status
page's CPU readings now fall back toHardware, the device tree's model and the implementer and part codes, and to
each CPU'scpufreq. The x86-only variables that warned...
bankML 0.4.0 — milestone: native serving complete
Everything Savante and mindX ask of llama-server, answered by bankML's own engine, identical to llama-server
b11192. 0.3.3–0.3.8 brought JSON mode and schemas, the penalties and the whole default sampler chain,
self-measurement, the context limit, slots, the prompt cache and logprobs; 0.4.0 closes the list: a q8_0 KV cache
with llama.cpp's Hadamard rotation, the grammar mask 13× faster, cache_prompt: false as llama-server honours it,
fewer pool wakes per token, and the engine's own status, diagnostics and a console whose landing is the ultimate
input field. 1-bit decode is at least at llama-server's speed: the pinned 8B A/B, three rounds, bankML at or
above llama-server in every one, median 2.02 against 0.77 tokens/s under the same load, every answer token-identical
(PERFORMANCE.md) — so the importer's auto engine now
chooses bankML's own forward pass for the 1-bit files as well as the ternary ones (AUTO_NATIVE_Q1). Record:
testing/results/0.4.0.txt, one run of the full gate, every stage passed (2026-10-07, the laptop; the decode budgets
at three threads: 1-bit 1.02× ggml b11192's own kernel, ternary 8.9×).
The q8_0 KV cache (BANKML_CACHE_TYPE=q8_0; testing/kv_oracle.py, 6 / 6)
- K and V stored as q8_0 blocks (
KvType::Q8_0), 53 % of the f16 cache's bytes. Attention over it is ggml's
reference kernel for a non-f16 K (attend_head_q8): Q quantized with the AVX2quantize_row_q8_0, scores by
vec_dot_q8_0_q8_0, V dequantized into an f32 accumulator (vec_mad_f32,vec_scale_f32). - llama.cpp b11192 rotates around a quantized cache, which the first replay found (2 / 6, the answers drifting
after dozens of tokens): Q and K through a Hadamard transform over the head (128), V over 64, and the attention
output back (attn_rot_k/attn_rot_v,llama_mul_mat_hadamard, which ggml's CPU computes as a fast
Walsh–Hadamard transform). bankML'sfwhtdoes the same arithmetic: scale by1/sqrtf(n), thenu + v,u − v. oracle_ggml_b11192_q8_0_kv_kernels: against the shipped haswell library (dlopen), 4,000 rows quantized
byte-exact and 4,000 dot products bit-exact, saturatingmaddubscases included.kv_oracle_live: llama-server b11192 with--cache-type-k q8_0 --cache-type-v q8_0and bankML with
BANKML_CACHE_TYPE=q8_0give the same answers, 6 of 6 (greedy, seeded, a 2,244-token prompt, a 320-token answer,
a two-turn conversation; text, counts andcache_n; 567 tokens).- A slot file records its cache type; one saved with the other type is refused with the reason.
The grammar mask, through a trie (oracle_grammar_masks, both paths)
- A whole-vocabulary mask walks a trie of the vocabulary's code points, built once: each grammar stack meets a
shared prefix once, and the stacks after a terminal are computed once per stack and mask. Median 2.94 ms
against 38.8 ms (13×), p90 49.3 against 80.6 ms, over the oracle's 1,645 masks. The oracle now computes every
mask both ways: 196 / 196 runs, 1,645 / 1,645 masks identical to llama.cpp b11192 by each.
cache_prompt: false, as llama-server (found by testing/decode_ab.py)
- The first pinned 1-bit A/B refused its first round: bankML's and llama-server's answers parted after about 40
tokens. The harness sends a warm-up, then the measured request withcache_prompt: false; llama-server b11192
then computes the whole prompt (n_past = 0, server-context.cpp), while bankML ignored the flag and reused the
warm-up's prefix, a different split of the prefill and so different bits. bankML now readscache_prompt(default
true) and, when it is false, starts from position 0 as llama-server does (Params::cache_prompt,
Native::complete_with). Test:cache_prompt_false_is_an_empty_slot(after a warm-up, the answer, tokens and
cache_n0 are an empty slot's; with the flag on, all but one prompt token are reused), in the release gate.
A Thesis tab in the console
- The authors' thesis and the sixteen contributions measured against it, read from docs/TECHNICAL.md's
Thesis section each time the tab opens
(GET /api/thesis), so the page never drifts from the report; drawn as DOM nodes by a small Markdown reader (bold,
italic, code, links, headings, lists; neverinnerHTML), relative links resolved to the repository, in a window
with the same depth, set for reading. sAGI/console/thesis.css: the thesis as a scroll of accordions. Each principle and each of the sixteen
contributions is a section titled by its bold lead; the window scrolls on its own with a progress line along its
top, and a section opens as it comes into view (one the reader closes stays closed); expand all, collapse all. On a
narrow screen it scrolls with the page, and the page is what is watched. Reduced motion is honoured.- Fixed on the way: the input field's stylesheet defined generic theme tokens (
--accent,--muted, …) on:root,
which overrode the console's own since the landing arrived (accent bars and fills went transparent); the console
build now renames them--uif-k-*.
Diagnostics: every component of sAGI, in windows with depth, on any screen
- The Diagnostics tab first asks every part of sAGI, in parallel and within a time limit, each in its own words
(diagnostics.components): bankml serve, the console, Savante (:7873), view (:7874), the model importer
(pins, the binary, llama-server, the engine setting), each persona (preflight, doctrine root), the agents, THOT
manifests (each bound agent's verified), the chain artifact for iNFT mints, PostgreSQL (connectors.status),
embeddings (embed.status), the voice (speak.available) and the spans. Ready, idle, to check or failing, with
what each said and how long it took; nothing is started or written. A public console shows each component's
level, not its paths or agent names. - Each section is a window: a title bar, a hairline border and two shadows (near and far), in light and dark; the
components are cards with a status edge. The Engine tab's cards share the depth. - Mobile: the tab bar scrolls sideways under the title, windows run edge to edge, cards stack in one column, trace
rows wrap; the ultimate input field keeps a separate layout on a narrow screen (full width at the bottom, without
side modules), so a desktop's saved positions never put it off a phone's screen.
Fewer wakes of the pool per token (1-bit and ternary decode)
- Measured first: under the same pool, bankML's 1-bit kernel is 1.58× ggml b11192's own
vec_dotover the 8B
model's 253 matrices (decode_budget_q1_0), so the decode gap to llama-server is not the arithmetic. A token's
time inside bankML is about 91 % matrix–vector products and under 2 % everything else; the rest is how the work
is handed out: each product woke the pool once, seven times per layer. Pool::rows_multi: several outputs' rows in one wake, in chunks that never cross a matrix, each row by the same
single-thread kernel.Weights::mv_manyuses it for Q1_0 and Q2_0 on the CPU, so Q/K/V and gate/up are one wake
each (seven per layer become four). The bits are the same:oracle_forward_model_bonsai_1_7b840 / 840 and
oracle_forward_model_ternary1,064 / 1,064 with the GPU off,oracle_native_serve_o49 / 9 and 23 / 23 per model.
The console's landing is the ultimate input field
- The Ask tab now opens on ultimate-input-field (MIT):
one elegant field (chat, or T for terminal commands) whose answers stream into output fields you can move,
spawn, pop out to another monitor and call home, each one its own conversation with bankML through the console's
POST /api/ask, every answer ending with its receipt, checked again in the browser. The build lives in
sAGI/console/uif/(uif.js,uif.css; React bundled, so the console's CSP still loads nothing from elsewhere);
its source is ultimate-bankml-ui'sconsole-mountbranch (src/console/). Without the build, the plain question box
comes back.
The engine's own status page, and an Engine tab (GET /bankml/status)
GET /bankml/statuson both of serve's addresses: the verified model, serve's settings (listen, upstream, native,
threads, KV cache type, GPU, allowed origins, uptime), the CPU (/proc/cpuinfo: model, each logical CPU's clock),
memory and swap, the disk where the model lives (statvfs: total and available; the model's size; this process's
bytes read and written,/proc/self/io), every GPU and bankML's limiter (/bankml/usage), the answers measured
(/bankml/metrics), the checks drawn from those (verified, memory, swap, disk, GPU, the log's last warning) and the
engine's log — the newest 200 messages, now kept in a ring bybankml::log(serve's own startup lines go through it).- A browser at serve's root (
http://127.0.0.1:18093/or the engine address:18094/) gets that status as a page,
refreshed every 3 s; every other client still gets Ollama'sbankml is running…line, which Ollama clients check. - The bankML console's Engine tab draws the same status with the same renderer (
bankML/status.js,
status.css: one source for both pages), throughGET /api/engine. - The first reading found the laptop's swap full (2.15 of 2.15 GB): the
swapcheck now says when the machine is
paging, because speeds measured then are not comparable.
scientific.diagnostic — every number to 18 decimals (testing/scientific_diagnostic.py)
- One greedy request with
logprobsandtop_logprobs: 5, sent to a fresh llama-server b11192 and a fresh
bankml serve --native(both computing the whole prompt). For every token and each of its top five: the 32-bit
float's bits, its exact value to 18 decimals (rounded half-even from the float's exact binary...
bankML 0.3.8 — the serving contract: the context limit, slots, the prompt cache, logprobs
bankml serve --native now behaves as llama-server b11192 at the edges a client meets in production: a prompt
that does not fit, a slot saved and restored, conversations that take turns, and the probabilities behind each
token. Each is checked against
llama-server's own answers, as everything before it. Record: testing/results/0.3.8.txt, one run of the full
gate, every stage passed (2026-10-06, the laptop).
The context limit (testing/context_oracle.py, 8 / 8)
- With context shift off (llama-server's default), a generation that fills the context stops there with
finish_reason"length", and a prompt that does not fit is refused before any work with llama-server's 400 body:
{"error": {"code": 400, "message": "request (N tokens) exceeds the available context size (M tokens), try increasing it", "type": "exceed_context_size_error", "n_prompt_tokens": N, "n_ctx": M}}. Eight requests from a few tokens to
past the context, at--ctx 256: the same text, counts, finish and refusal bodies.
Slots: save, restore, erase (testing/slot_oracle.py, 19 / 19)
POST /slots/0?action=save|restore|eraseon the engine address, as llama-server's slot API, with--slot-dir DIR
(created at start). Replies carryn_saved/n_written/timings.save_ms,n_restored/n_read/
timings.restore_ms,n_erased; refusals are llama-server's (501 without a slot directory, "Invalid slot ID",
"Invalid action", "Invalid filename" by itsfs_validate_filenamerules, "Unable to restore slot: …").- The file: a magic line, the model's sha256, the context and token counts, each layer's K and V (f16) and the tokens,
then a sha256 of all of it; written to a temporary name and renamed. A file from another model, or damaged by one
bit, is refused and the slot emptied — never half-filled; the next answer recomputes its prompt. - The oracle is the engine itself: the answer after a restore — in the same server and after a restart — equals the
answer from an empty slot, and the restore skips the prompt (cache_n> 0).n_savedcounts as llama-server
counts: the prompt and every generated token but the last. - Savante and the console pass
--slot-dirto the native engine, so a warm start survives a restart.
One slot, and llama-server's host prompt cache (testing/session_oracle.py, 14 / 14)
- The single-slot contract, stated and checked: requests are served one at a time, as llama-server
-np 1(Savante's
flags). Four requests sent at once are all answered, none refused or mixed, each with the text it gets alone. bankML/prompt_cache.rs: llama-server's--cache-ramcache (on by default there). When a request shares little
with the slot, the slot's tokens and KV rows go to RAM, and a cached state that keeps more of the prompt comes back.
Three conversations taking turns (A1 B1 C1 A2 …) now match llama-server turn by turn: text, counts andcache_n.
Before, each turn found only the system prompt in the slot, so prefill started elsewhere and the answers differed.BANKML_CACHE_RAM(MiB, as--cache-ram: 0 off, -1 no limit); unset, 8192 MiB but at most a quarter of the memory
available at load.
Docs and tools
docs/why-bankml.md: the plain-language version (advantages, uses, how Rust helps, the road ahead), dated
2026-10-06; published as a web page over the DeltaVerse substrate at
deltaverse.pythai.net/bankml.tools/makecards.py: a page's share cards (1200×630, 1200×1200, a 180 px touch icon) with the DeltaVerse $.
tools/seo.py: audits a page or URL as a crawler and a link preview read it — the meta set, a canonical that must
not redirect, JSON-LD, and each card's real size against its declared one (standard library only). The bankML page:
47 / 47.
Logprobs on /v1/chat/completions, streamed or not (testing/logprobs_oracle.py, 14 / 14)
logprobs: truewithtop_logprobs(default 20) →choices[0].logprobs.content: per token itsid,token,
bytes,logprob, and the top tokens with theirs. Every logprob is the same 32-bit float as llama-server's:
get_token_probabilitiesported — a partial sort of the whole vocabulary (libstdc++'s, from 0.3.7), the softmax
in that order,logf.- llama-server's entry rules, reproduced: a token gets an entry only once the text has no incomplete UTF-8 after it,
and carries the text sent since the previous entry; a stop word drops its own tokens' entries; control tokens read
""; a top token holding part of a character is cut at the incomplete end (validate_utf8), other invalid bytes
read U+FFFD,bytesraw.top_logprobswithoutlogprobsis refused with llama-server's message. - Streamed, as llama-server's partial responses: the stream opens with the role delta, and each whole token's entry
rides on the chunk its text makes (a token that sends no text sends no entry: a held stop-word prefix, the
end-of-turn token with nothing held back). Text now waits for a whole token in every stream, as llama-server sends
nothing while UTF-8 is incomplete (" 🍕"is one chunk, not" "then"🍕"). Five streamed cases, every chunk's
delta and entry identical. The engine reports each step (Native::complete_with,Step);NativeChat::run_steps
makes the chunks. - Not served:
/completion(llama-server's own endpoint, so also itsn_probs); clients use/v1.
bankml 0.3.7: the whole sampler chain; bankML measures itself; the GPU limiter; the bankML console
Every sampler in llama-server b11192's default chain is now reproduced, token for token: after 0.3.6's penalties,
typical-p, top-n-σ, XTC, dynamic temperature and DRY. A request that sets any of them gets llama-server's answer
instead of a refusal; what llama-server clamps is clamped, what it refuses is refused with its message. bankML now
measures itself — time to first token on every answer, prompt and generation speed, CPU, memory, the GPU and, where
the operator allows it, power and joules per token — limits its GPU to a share of the card's memory and time and
lets each matrix shape decide whether the card pays, and has a console in which it answers as itself, from those
measurements. Record: testing/results/0.3.7.txt, one
run of the full gate, every stage passed (2026-10-06, the laptop, the GPU limiter at its default).
Measurement: what bankML knows of itself
bankML/metrics.rs: one record per completion, taken inside the engine — TTFT, prompt and generation tokens per second
(llama-bench'sppandtg), grammar time, and the CPU package's energy and joules per generated token when RAPL is
readable;null, never estimated. A ring of 256 atGET /bankml/metrics, with totals.- The receipt's
ttft_msis set for non-streamed answers too (it wasnull);/v1answers carry llama-server's
timingsfields (prompt_n,prompt_ms,prompt_per_second,predicted_n,predicted_ms,
predicted_per_second,cache_n). GET /bankml/usageaddspackage_watts(RAPL over the sampling interval, its wrap handled), each GPU's busy %, VRAM and
GTT from sysfs, and the GPU limiter's state../install.sh power(opt-in, the only step that uses sudo): a udev rule lets araplgroup that the user joins read
the energy counter, which the kernel has kept root-only since PLATYPUS (CVE-2020-8694); it says so and asks first;
--removeundoes it.
The GPU: a limit, and per-shape calibration
BANKML_GPU_LIMIT(default 0.8): bankML's GPU buffers stay within that share of the heap they come from — on an
integrated card, of the RAM they could use — and after a dispatch ofdthe card restsd·(1−L)/L; a matrix that does
not fit, or a product arriving while the card rests, runs whole on the CPU. Never a wait, the same bits.- Per-shape calibration: each matrix shape's first products alternate between the card's share and the CPU alone, timed
in the real pipeline; the faster median is kept and a shape decided for the CPU frees its buffers. Measured on the Vega
3 (Bonsai-1.7B, interleaved A/B, 4 rounds, the same answer every time): with one global share the card cost 18 %
(10.40 → 8.51–8.58 tok/s); per shape, 10.36 off against 10.30–10.38 on — neutral, as an integrated card that shares
decode's memory bandwidth can be; card memory 66 → ~13 MB.
bankML as itself: the persona and the console
sAGI/personas/bankml.persona: bankML speaking as itself, projected from the README, the Thesis and the oracles
(doctrine root0x4d5ef2d9…; adopts and verifies). Its knowledge of its own use comes only from a measured SELF block,
rendered as sentences with units (a bare key was read as seconds in testing).sAGI/console.py(port 7875, loopback only): Interaction (the streamed answer and its receipt checked against the
text), Admin (CPU threads, RAM budget and GPU limit sliders — one verified restart with rollback — and D3 charts of
what bankML measured), Logging (every exchange with its timings and receipt), Infotags (ERC-721 metadata for an iNFT
publication: the model, the persona and its doctrine root, an RFC 6962 root over the exchanges, CIDs). Standard library
only, a strict CSP, same-origin JSON POSTs; D3 v7.9.0 vendored with its ISC licence.testing/test_console.py.
Measured
oracle_samplers(testing/penalty_oracle.py --kind sampler, llama-server b11192 from an empty cache; 23 variants
× 4 prompts: each sampler alone, typical-p before top-p and min-p's unsorted path, XTC with a clamped probability and
a disabling threshold, dynamic temperature at 0, DRY with defaults, custom breakers and with the repeat penalty, all
five at once): mindx-gen39 76 / 76 answers token-identical (3,576 tokens), Bonsai-1.7B 76 / 76 (2,587
tokens), Bonsai-8B 76 / 76 (2,361 tokens,oracle_samplers_8b); 16 / 16 refusals on each, with
llama-server's message.oracle_std_sort(testing/sort_oracle.cpp, libstdc++'s ownstd::sort): 876 / 876 orders identical — sizes 0
to 1,000, heavy ties, sorted, reversed and equal keys — the order typical-p's unstable sort leaves equal scores in.
Added
sampler.rs: each sampler assrc/llama-sampler.cppwrites it, in the default order (penalties, DRY, top-n-σ,
top-k, typical-p, top-p, min-p, XTC, temperature, dist), with llama.cpp'ssortedstate tracked through the chain
(typical-p leaves the set unsorted, so top-p sorts after its softmax and min-p takes its unsorted path); XTC's own
mt19937and its float draw (generate_canonical<float, 24>: one 32-bit output); top-n-σ's squares in double
(C++'spow(float, 2)); dynamic temperature's float entropy; DRY's restart sequences, reverse Z-algorithm and libm
pow; libstdc++'sstd::sort(introsort) and generic heaps;Params::default=common.h.native.rs: the request fieldstypical_p,top_n_sigma,xtc_probability,xtc_threshold,dynatemp_range,
dynatemp_exponent,dry_multiplier,dry_base,dry_allowed_length,dry_penalty_last_n,
dry_sequence_breakers; llama-server's soft limits clamp (top_p,min_p, the XTC fields to [0, 1], temperature
to ≥ 0), adry_basebelow 1 falls back to 1.75; DRY's breakers are built from the vocabulary's pieces once per
breaker list and cached on the engine.- On Ollama's API
typical_pis honoured; DRY, XTC, top-n-σ and dynamic temperature are not Ollama options, so they
come through/v1(and the C API).
Changed
- Refused instead of ignored:
mirostaton/v1(the Ollama path already refused it) and a customsamplers
order — llama-server would act on either, so answering without them would not be its answer.
Gate record: testing/results/0.3.7.txt (attached).
bankml 0.3.6: the penalties (O2)
repeat_penalty, repeat_last_n, presence_penalty and frequency_penalty are no longer refused: bankML applies
them as llama.cpp b11192's llama_sampler_penalties does, and its answers are token-identical to llama-server's. The
coach's ollama_predict (temperature 0, repeat_penalty 1.3) and mindXtrain's imprint gate (repetition_penalty 1.3)
can now run on bankML. Greedy at 1.3, mindx-gen39 answers instead of degenerating into ,,,,. Record:
testing/results/0.3.6.txt: one run to json_schema_oracle_live, where the Vega 3 worker's memory (taken from the
laptop's RAM) stalled it; the three stages left ran next from the same tree and build with BANKML_GPU=off, marked in
the record. 0.3.7 adds a GPU limiter for this (BANKML_GPU_LIMIT).
Measured
oracle_penalties(testing/penalty_oracle.py, llama-server b11192 from an empty cache, greedy and seeded; 17
variants × 4 prompts made to repeat): mindx-gen39 56 / 56 answers token-identical (2,478 tokens), Bonsai-1.7B
56 / 56 (1,895 tokens); each 12 / 12 refusals with llama-server's message (repeat_last_n−1, a repeat
penalty of 0 or below); Bonsai-8B 56 / 56 (1,568 tokens,oracle_penalties_8b), 12 / 12 refusals.
Added
sampler.rs: the penalties, first in the chain and again on a grammar's redraw; the window as llama-server fills it
— every prompt token, cached or not, before the first draw (server-context.cpp), then each token drawn
(Sampler::accept); the logit divided by the repeat penalty when positive, multiplied when not, then
count × frequency + presencetaken off; disabled as llama.cpp disables it (last_n0, or all neutral).
general.sampling.penalty_last_nandpenalty_repeatread from the GGUF as llama.cpp reads them (none of the five
native models sets them).- The surfaces:
/v1and/api/*(repeat_last_nmoves from the ignored resource options to the honoured ones), a
Modelfile'sPARAMETER, the C API (through the same engine).bankml generate --sampleapplies them too, but has
no flags for them: only a GGUF's owngeneral.sampling.penalty_*defaults reach it (none of the five models sets one). testing/penalty_oracle.py(record, and--bankmllive);oracle_penalties,oracle_penalties_8band
penalty_oracle_livein the gate. Live on mindx-gen39: 85 / 85 through/v1(top-level fields) and/api/chat
(options), refusals included.
Fixed (found by the live oracle)
- A sampler refusal on
/v1answered 500. A negativerepeat_last_nor a repeat penalty of 0 or below reached
the engine before it was checked; the Ollama path already refused it up front./v1now refuses it as llama-server
does: a 400 with its message.
made with luv.pythai.net
bankml 0.3.5: JSON schemas, and bankml create (O6b, O5)
Any JSON schema now gets the grammar llama-server b11192 builds for it, on the model's own template, and the answers
are token-identical to llama-server's. And mindX's own model can be made the way mindX makes it for Ollama:
bankml create takes mindXtrain's merged safetensors and promote.py's persona Modelfile, and the created
mindx-gen39 answers the user's turns exactly as llama-server answers the same GGUF given that persona. Phases O6b
and O5 (first cut) of docs/OLLAMA.md; built on two branches and merged here. Record: testing/results/0.3.5.txt. The
gate run was stopped by the development session's memory guard, not by a failure, as json_schema_oracle_live began
(the Vega 3 worker's memory comes out of the laptop's RAM); the three stages left ran next from the same tree and build
with BANKML_GPU=off, and the record marks where. The first run also found the ternary schema record had never been
taken; it was recorded from llama-server b11192 and passes, 11 of 11.
Measured
- Schema grammars (
oracle_schema_grammars, llama.cpp's ownjson_schema_to_grammarand
common_chat_templates_applyin the release'slibllama-common.so, no model): 173 schemas (llama.cpp's 81 test
cases, Pydantic-shaped schemas like mindX's, edge cases) on each of the three templates — 148 grammars byte-identical
bare and on the chat path, every refusal with llama.cpp's message (24 bare, 20 chat). - Answers under schemas (
oracle_json_schema,_ternary,_o4; llama-server b11192 greedy and seeded, each
from an empty cache): Bonsai-8B 28 / 28, Ternary-Bonsai-8B 11 / 11, Bonsai-1.7B, SmolLM2-135M-Instruct and
mindx-gen39 56 / 56 each — objects, enums, ranges,$defs, a pattern, a date format, top-level arrays, enums,
strings, integers and numbers, and answers cut bymax_tokensinside an object, a top-level string and a number.
Live:/v1(one streamed) and Ollama's/api/chatwithformat: <schema>. - The content rule (
oracle_json_content, llama.cpp'scommon_chat_parseitself): every prefix of every
recorded constrained answer and edge cases, 30,063 of 30,063 texts on each template. - mindX's persona, end to end (
oracle_persona_layer,testing/persona_oracle.py): promote.py's
persona_modelfile_layerModelfile (mindX's persona asSYSTEM,stop <|im_end|>,num_ctx 2048), created
FROMthe merged safetensors directory and, unchanged,FROM mindx-gen39in place: 27 / 27 answers each
token-identical to llama-server given the persona as the system message; live through/api/chat,
/api/generateand/v1, and/api/psnamesmindx-gen39:latest. - The converter, run against llama.cpp's directly (b11192's
convert_hf_to_gguf.py, torch 2.14.1+cpu): the same
bytes asbankml converton freshly downloaded mindx-gen39 (6b64c748…, the pin) and SmolLM2-135M-Instruct
(ec30a679…). Until now the identity rested on O4's conversions. - The name heuristics (
gguf-py/metadata.py) agree with gguf-py on 168 of 168 Hub-style ids (oracle_name_heuristics).
Added
bankML/schema.rs: llama.cpp b11192'scommon/json-schema.cppandjson-schema-to-grammar.cpp(MIT,
attributed; zero crates), nlohmann's JSON as the grammar text depends on it, and the chat parser's wrapping — the
Qwen3 template's (with theuntil-13reasoning rules) and the ChatML templates' (none), as llama.cpp writes each.
Wired:response_formatjson_schemaandjson_object+schema, the top-leveljson_schema, Ollama's
format: <schema>,bankml_chat. The schema is read from the request's own text (1.0stays a float). Refused as
b11192 refuses, with its message; and one deliberate divergence: apatternwith a non-ASCII character before a
quantifier, which llama.cpp turns into a grammar that is not UTF-8, is refused.bankML/convert.rs,bankml convert: safetensors → GGUF F16 for the Llama architecture as SmolLM2 uses it,
byte-identical toconvert_hf_to_gguf.py --outtype f16; what would make llama.cpp write something else is refused
with the reason.bankML/create.rs,bankml create,POST /api/create,DELETE /api/delete(derived models only),
POST /api/copy: Ollama's Modelfile parser state for state; a derived model is<registry>/<name>.MODEL.json, a
layer (SYSTEM,PARAMETER,stop,MESSAGE, licence) over a pinned base, verified through the base on every
load; the layer applied as Ollama applies it;/api/showgives the Modelfile back.FROMa safetensors directory
converts and pins it.- Oracles:
testing/schema_oracle.{cpp,py}(now all three templates),testing/json_schema_oracle.py(now every
native model, and length cuts),testing/content_oracle.{cpp,py},testing/persona_oracle.py,
testing/convert_oracle.py; the gate runs them. - mindXtrain (huggingface.co/PYTHAI/mindXtrain, archived source
github.com/Professor-Codephreak/mindXtrain) is linked from the
README, OLLAMA.md and thetrain/andconvert.rsheaders:bankml convertandcreatereplace its
serve --to ollamapath for mindX.
Fixed (found by the new oracles)
- A schema on a ChatML template got the Qwen3 grammar. The O6b branch wrapped every schema as the Qwen3 template
does; on SmolLM2's and mindx-genN's templates llama.cpp writes no reasoning rules and another root. bankML now
builds the grammar on the model's template (the schema oracle checks all three). - The content of an answer cut inside an escape: llama.cpp's parser ends the string before an unfinished
\or
\uXX; bankML kept it (12 of 29,973 texts differed before the fix). - A non-streamed answer whose parse is empty (cut inside or right after the opening fence): llama-server answers
with the raw text; bankML answered""(piseeded on Bonsai-1.7B:"```json\n\n").grammar::json_message.
Changed (O5's two open gaps, closed)
num_ctxas Ollama applies it (read from Ollama v0.13.3'sserver/prompt.go): under a request's or a layer's
num_ctx, a conversation that does not fit loses its oldest messages first (native::fit_messages, Ollama's loop
step for step); what still does not fit is refused with the reason, since Ollama would cut tokens out of the middle
of the prompt. Tokens pastnum_ctxduring an answer still differ: Ollama shifts its cache, bankML does not./api/psnames the derived model (its digest,parent_model, the request's context), as Ollama names a runner
after the model that loaded it.- A derived model's name wins over the registry's suffix-less alias, and
FROMresolves that alias, so promote.py's
re-create in place (FROM mindx-gen39asmindx-gen39) works; a conversion never overwrites an existing pin.
Found
- The converter's bytes depend on the directory's name, for llama.cpp and bankML alike:
general.namecomes from
it. gen39 converted from a directory namedmergedisbb41f62d…, frommindx-gen39the pin6b64c748…. - gen39's
tokenizer.jsonlost SmolLM2'sDigitspre-tokenizer when transformers 5.8 re-saved it; llama.cpp's
chkhshstill names itsmollm(the vocabulary has no multi-digit tokens), and bankML accepts both shapes.
Not in this release
ADAPTER(LoRA) in a Modelfile: merge first, thenFROMthe merged directory. Tool calls (O6).- Next: O2, the penalty samplers. mindXtrain's bankml backend (proposed) found that
mindx-gen39
degenerates into repetition (,,,,,?||) without a repetition penalty; its imprint gate decodes with
repetition_penalty 1.3andno_repeat_ngram_size 3, which bankML refuses until each has an oracle.
made with luv.pythai.net
bankML 0.3.4 — the Llama architecture, F16, and mindX's own model natively
mindX serves its own trained lineage, mindx-genN, through Ollama. Generation 39 (mindXtrain39, the last accepted
generation: SmolLM2-135M + its LoRA, merged, F16) is now answered by bankML's own forward pass, token-identical to
llama-server b11192 — greedy, seeded, on /v1, on Ollama's /api, through the C API and in JSON mode. So are
SmolLM2-135M-Instruct and Bonsai-1.7B, which was refused until now only because its embeddings are tied. Phase O4 of
docs/OLLAMA.md, with the F16 part of O3. Record: testing/results/0.3.4.txt.
What llama.cpp does, read from its source (b11192, 171e8846b)
- F16 weights take two paths, chosen by the product's shape (
ggml_compute_forward_mul_mat). The activation is
rounded to f16 first (F16'svec_dot_type). With one column (a decode step, the logits, a one-token micro-batch)
llamafile_sgemmrefuses (n < 2) and every element isggml_vec_dot_f16: four 8-lane FMA accumulators over
32-element steps, then a fixed reduction, any tail summed in double. With two or more columnsllamafile_sgemm
runstinyBLAS<8, __m256>: each element one 8-lane FMA chain over the row and its ownhsum— other bits —
whenever the weight has a multiple of 4 rows and the row a multiple of 8. The two differ, so a prompt's
micro-batch and a decode step must each take their own. - The last layer of a prompt only computes the rows that are output (
inp_out_ids): llama-server asks for the
last token's logits, so the last layer's feed-forward block is a one-column product there. For the quantized types
that changed nothing; for F16 it decides the bits. - Tied embeddings: with no
output.weight, the loader duplicatestoken_embd(TENSOR_DUPLICATED) and the
logits are that table times the final row. - The Llama graph (
llm_build_llama): no Q/K norms, RoPE in NORM mode (rotate_pairson adjacent pairs, the same
contracted arithmetic as NEOX), no YaRN (ext_factor0,attn_factor1), GQA from the header. - SmolLM2's pre-tokenizer (
smollm) is two passes: every digit cut out alone (astd::regexover the collapsed
text), then GPT-2's pattern inside each piece, where the end of a piece is the end of the text. Its vocabulary has
no token for 21 bytes; llama-vocab drops them. - llama-vocab overrides the file's token types by text: every end-of-generation name and the fill-in-the-middle
token of each role become CONTROL. In the Qwen3 vocabulary</s>(token 128247) is NORMAL in the file and CONTROL
in llama.cpp, so the text</s>is cut out as one special token.
Added
bankML/f16.rs: ggml's two F16 products, each a scalar definition and an AVX2 + FMA + F16C path with the same
bits (tinyBLAS's own 4 × 3 register blocking for prompts);mat_mul_parchooses the path as ggml does.forward::plan: from the header alone, the architecture (qwen3,llama), the weight type (Q1_0, Q2_0_g64,
F16), the logits matrix (tied or not), and a refusal with the reason for everything else: other architectures,
other types, mixed types, biases, fused QKV, rope factors, experts, Q/K norms on Llama, RoPE scaling on Llama,
partial RoPE, a head width that is not a multiple of 32.- The Llama graph in
forward.rs(NORM RoPE, no Q/K norms) and tied embeddings for both architectures. - The
smollmpre-tokenizer intokenizer.rs; a byte without a token is dropped as llama.cpp drops it. - Chat templates per model (
chat::TEMPLATES, each pinned by the sha256 of its text): Bonsai / Qwen3,
SmolLM2-Instruct's (ChatML with a default system message), and the plain ChatMLmindx-genNcarries. Ollama's
personaSYSTEMformindx-genNlives in its Modelfile, not the GGUF: here it is the caller's system message
(bankml createis O5). - JSON mode on ChatML templates: llama-server builds another grammar there (its root opens with
<|im_start|>assistant\nand has no<think>block);grammar::json_object_grammarpicks the template's, checked
against the server's own report on every recorded request. - The models, pinned (
sAGI/models.pyCONVERTED,pin_converted): no F16 GGUF is published for either, so each
is converted from its pinned safetensors by llama.cpp b11192's ownconvert_hf_to_gguf.py --outtype f16
(unmodified). The conversion is reproducible (twice, the same sha256), and every tensor was checked against the
safetensors rounded to f16. The FORK.json records the source's LFS sha256, the revision and the tools.SmolLM2-135M-Instruct-F16.ggufe9aba089…5a222from HuggingFaceTB/SmolLM2-135M-Instruct@12fd25f7
(model.safetensors5af571cb…), Apache-2.0.mindx-gen39-F16.gguf6b64c748…58266from the dataset PYTHAI/mindXascension@4bd31b9d
(weights/gen39/ollama_push/merged/model.safetensors19b62829…), Apache-2.0 (the dataset card; the base is
SmolLM2-135M).
- Registry aliases: a name without its weight-type suffix resolves when one pin has that base, so Ollama's tag
mindx-gen39reachesmindx-gen39-f16. - Oracles, in the gate:
oracle_ggml_b11192_f16(testing/f16_oracle.py): the shipped library'smul_maton SmolLM2's real F16 matrices
and on synthetic shapes for the tails and the fallbacks — 211 of 211 tensors widened bit-exact, 552,268 of
552,268 elements bit-exact (61 products through tinyBLAS, 26 throughggml_vec_dot_f16).oracle_forward_model_bonsai_1_7b,oracle_forward_model_llama_f16: the whole model (every layer,result_norm,
logits) bit-exact — Bonsai-1.7B 840 of 840 rows, SmolLM2-135M-Instruct and mindx-gen39 800 of 800 each.oracle_tokenizer_smollm4,346 of 4,346;oracle_chat_template_chatml317 of 317 on each template.oracle_llama_server_llama_f16,oracle_llama_server_bonsai_1_7b: llama-server's tokens — greedy on short, long
(the tiled kernel, tinyBLAS over the micro-batch) and deep prompts (the split-KV kernel), and seeded sampling:
SmolLM2 6 / 6 / 3 and 40 of 40; mindx-gen39 6 / 6 / 3 and 40 of 40; Bonsai-1.7B 6 and 40 of 40.oracle_native_serve_o4: the Savante conversations (9 of 9 turns each) and JSON mode (23 of 23 answers
each) on the three models.- Live:
serve_oracle.py --bankmlandjson_oracle.py --bankmlon each (mindx-gen39 asked for asmindx-gen39),
and the C API'sbankml_chatagainst each model's llama-server record.
Fixed
- The tokenizer took
</s>in the Qwen3 vocabulary as plain text. llama.cpp makes it a CONTROL token by its name,
so with special tokens parsed it is one token there and three here. The old oracle corpus never contained it; the
corpus grew with the docs and the oracle caught it (2 of 4,346 cases). bankML now applies llama-vocab's type
overrides; a vocabulary where they are ambiguous, or whose header sets FIM ids, is refused. - A vocabulary whose merges name a string outside it is refused: llama.cpp merges by text, bankML by id.
Changed (faster, the same bits)
- Attention (every model): the reference kernel's f16 dot, scale and accumulate run on F16C + FMA; the tiled
kernel is compiled with FMA and F16C and widens each 64-cell tile once per block of 16 rows; heads (and rows) run on
the pool. Each head is still computed whole by one thread in llama.cpp's order. - Tensor lookups by name are a hash map, and the norm vectors are read once at load.
- The 8B models gained most: the same gate oracles ran in about half the wall time (Bonsai-8B conversations 400 →
192 s, deep greedy 756 → 320 s, JSON mode 901 → 541 s; ternary JSON 454 → 242 s), with the matmul budget unchanged
— the old reference attention converted every f16 value in software. Gate wall times, not a controlled benchmark. - Speed, SmolLM2-135M-Instruct F16, laptop (Ryzen 3 3200U), 3 threads each, paired: decode 38.0–38.9 tok/s vs
llama-server's 40.4–42.7 (about 0.93×); an 858-token prompt in 8.6–9.6 s vs 7.8–8.6 s, then decode at ~900 cells
27.2–30.1 vs 20.2–29.9 tok/s. Bonsai-1.7B decode 8.4–8.6 vs 10.6–10.9 tok/s (the known 1-bit decode gap, O8).
docs/PERFORMANCE.md has the table and a rejected experiment.
Not in this release (refused, with the reason)
- Q8_0 (Qwen3-0.6B, mindX's
qwen3:0.6bpin): from the source the kernels look cheap (ggml's AVX2
vec_dot_q8_0_q8_0and tinyBLAS's Q8_0 path appear to share their per-block arithmetic; not built, not measured),
but that file's template is Qwen's own, which renders 4 of 317 oracle conversations differently from the Bonsai
template (reasoning rules). It stays refused ("weights are Q8_0 … O3") until its template has an oracle. - BF16, Q4_K and other architectures: refused by
forward::plan, as before.
made with luv.pythai.net
bankML 0.3.3 — JSON mode natively, llama.cpp's own grammar
mindX's most common request that bankML refused, format: "json", is now answered natively. The answer has the
same tokens llama-server b11192 gives under the same request, greedy and seeded. O6's first cut (docs/OLLAMA.md).
Record: testing/results/0.3.3.txt.
What llama-server does, read from its source (b11192, 171e8846b)
- The grammar is not
grammars/json.gbnf. On the jinja chat path (Savante's flags),response_format: {"type": "json_object"}becomes the schema{"type": "object"}. The chat layer turns the schema into a PEG parser
(common/chat-auto-parser-generator.cpp), and that parser into GBNF. The GBNF's root begins with the template's
generation prompt, then allows an optional```jsonfence around the object. - The grammar is prefilled. It is typed as a tool-call grammar, so
common_sampler_initaccepts the generation
prompt's 7 tokens into it before the first draw (common_grammar_needs_prefill). - The sampler draws, then checks, then redraws.
common_sampler_sample(grammar_first = false) runs the whole
chain on the raw logits. If the token it picks passes the grammar, it stands. If not, the logits are reset, every
rejected token goes to −∞, and the chain runs again: a second draw from the samemt19937. - The end set is wider. An end-of-generation token passes only when the grammar is complete, and llama-vocab's end
set on Qwen3 is 6 tokens:<|endoftext|>,<|im_end|>,<|fim_pad|>,<|repo_name|>,<|file_sep|>and token
128247</s>. One recorded answer ends on<|file_sep|>. - The content is the value alone. The answer's
contentis the JSON value, without the fence or the space around it.
Added
-
bankML/grammar.rs: llama.cpp's grammar engine, ported (MIT, attributed with its notice; no crates). It holds:- the GBNF parser (literals, classes,
., token terminals<…>!<…><[id]>, groups,* + ? {m,n}, comments); - the reference and left-recursion checks;
- the pushdown stacks and
advance_stack; reject_candidatesover code points, with partial UTF-8 carried across tokens;- accept and apply, with the end rule.
It also carries the server's JSON-mode grammar (a constant the oracle checks against the server's own report) and
its prefill, the request resolution ofoaicompat_chat_params_parse, and the content rule. - the GBNF parser (literals, classes,
-
Sampler::sample_constrained: the draw, the check and the masked redraw, in the server's order. -
Surfaces. All of them use one engine path (
Native::grammar,Native::complete):/v1/chat/completions:response_format(json_object,json_schemawith schema{}or
{"type": "object"}), the top-leveljson_schema, andgrammar(any GBNF; a user grammar, not prefilled);- Ollama's
/api/chatand/api/generatewithformat: "json"; - the C API's
bankml_chat; bankml generate --json.
A stream sends the content's growth.
-
Oracles, in the gate:
oracle_grammar_masks(testing/grammar_oracle.cpp+.py) drives libllama b11192's public grammar sampler on
the pinned vocabulary. All 151,669 token pieces and the end set are identical. Then 196 of 196 runs over 12
grammars are identical: llama-server's JSON grammar with its prefill, every grammar in llama.cpp'sgrammars/,
and three written for token terminals, edges and UTF-8. Each input goes in tokenized and byte by byte. That is
1,645 of 1,645 whole-vocabulary masks and 116 of 116 rejection points.oracle_json_modeandoracle_json_mode_ternary(testing/json_oracle.py --record) replay llama-server's
answers underresponse_format: json_object. The prompts invite JSON or tempt prose and code; one is a nested
object and one is unicode. They run greedy and seeded at 0.7, 1.0 and 1.3, plus two user grammars, each from an
empty cache. The results: Bonsai-8B Q1_0 23 of 23 (860 tokens, 152 of them redrawn under the mask), and
Ternary-Bonsai-8B 13 of 13 (534 tokens, 49 redrawn). The same token ids, end token included, and the same raw text, content, finish
and counts.json_oracle_live(testing/json_oracle.py --bankml) runs a liveserve --nativethrough/v1(streamed
once),grammar, and/api/chatwithformat: "json": 8 of 8.
Changed
- The end of an answer is llama-vocab's whole end set (6 tokens), not
<|im_end|>and<|endoftext|>alone,
with or without a grammar. Before this, an answer that sampled<|file_sep|>would have run on where llama-server
stops. response_formatonserve --native's/v1was silently ignored before (an unconstrained answer; the proxy mode always passed it to llama-server). It is now honoured, or
refused with the reason.serve::JsonderivesClone.native::Donecarries the generated token ids, the time spent in the grammar and
the number of redraws.
Still refused, with the reason
- A JSON schema other than
{}or{"type": "object"}, inresponse_format,json_schemaor Ollama'sformat:
llama.cpp'sjson_schema_to_grammarand its chat-parser wrapping are not ported (O6, next). formatwithraw: true, because JSON mode's grammar starts with the template's generation prompt;
response_formatorjson_schematogether withgrammar; tools.
Measured
- Grammar cost (PERFORMANCE.md, "JSON mode's cost"):
- one whole-vocabulary mask over 151,669 tokens costs a median of 24.2 ms (p90 47.0, max 101.0) across 1,645
masks; - spread over real answers, the grammar costs 3.90 ms per token on the 1-bit model (152 of 860 tokens
redrawn) and 3.95 ms on the ternary model (49 of 534). That is about 1 % of a decode step.
- one whole-vocabulary mask over 151,669 tokens costs a median of 24.2 ms (p90 47.0, max 101.0) across 1,645
- The rest of the gate, against 0.3.2. No kernel changed, and every bit-exact oracle passed again: tokenizer
4,258/4,258, template 317/317, whole model 1,064/1,064 for both models, greedy 6/6, 6/6, 6/6 long and 3/3 deep,
sampling 40/40, native serve 9/9, the Ollama shape 9/9, the C API 9/9 + 9/9, and printf 47/47.- The decode budgets read faster than in 0.3.2's gate because the laptop was quieter, not because of the code.
1-bit: bankML 0.641 s/token at 1 thread (0.3.2: 0.677) and 0.389 s at 3 (0.3.2: 0.787, under load). Ternary:
0.436 s at 1 thread (0.477) and 0.279 s at 3 (0.394), 9.62× and 8.36× ahead of ggml b11192.
- The decode budgets read faster than in 0.3.2's gate because the laptop was quieter, not because of the code.
made with luv.pythai.net