Skip to content

Releases: cryptoAGI/bankml

bankML 0.4.4 — .memory versions, with gitmind

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 15 Oct 20:32

.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_add and memory_remove commit .memory (add note 3: …,
    remove note 1: …). The repository holds a copy of .memory and 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 .memory is 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 .memory tab lists the newest versions; restore that version puts .memory back
    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 .memory is refused.
  • gitmind 0.1.0, vendored (sAGI/gitmind/, from github.com/Professor-Codephreak/gitmind, commit c4b379c; same
    authors, MIT OR Apache-2.0 here, 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 .memory committed, 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.py 122 / 122.

bankML 0.4.3 — browseML: bankML in the browser; the console's first answer

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 15 Oct 20:32

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, and wasm32-wasip1-threads for 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 as serve does (guard, sha256 pin) and puts serve's receipt on
    every answer. Nothing to install, no account, no server.
  • Exact: browseML/testing/oracle.mjs gives 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::Mmap reads the file into one shared buffer where there is no mmap.
    • serve::ident without 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/sinf are 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_add only 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 get browseml-mt-relaxed.wasm. Its kernel is q1_0::relaxed_core:
    • the sign sums as llama.cpp's AVX2 "sel" kernel computes them, with relaxed_dot against 0/1 bits;
    • fused relaxed_madd for the reference's mul_add.
    • The engine refuses this build where relaxed_madd is not fused, and the page falls back to browseml-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 f16 paths and the tiled attention kernel in WebAssembly SIMD (bankML/wasm_simd.rs): f16::vec_dot with
    the reference's accumulators and reduction order (f16 × f16 is exact in f32), f16::widen/mad/scale four lanes
    at a time, and the tiled kernel's score chains and V update through an exact four-lane fmadd (relaxed_madd in
    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… for browseml.wasm; only local builds had it.)
    • Oracle: browseml.wasm 9 of 9 and browseml-mt-relaxed.wasm on 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.
  • tools/browseml.sh test: the kernels' unit tests (wasm_simd, f16, q1_0) on wasm32-wasip1 under 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 million fmadd lanes against mul_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.js shows 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_at changes) 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/state says 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-mount 8c88972, consoleEngine.ts).
  • A max_tokens that 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_AGENT defaulted to mindx), 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-filter or transform on
    an ancestor makes it the containing block of a position: fixed modal; 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); now BANKML_SAVANTE_URL and BANKML_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 same sAGI/sysdiag.py as the console, which now shares its ping, engine and to_html).
  • A health line in the side panel, under the timer: the engine (up, its ping), the CPU's temperature and the
    memory av...
Read more

bankML 0.4.2 — logprobs on Ollama's API, receipts over logprobs, max_tokens: 0; with 0.4.1

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 09 Oct 01:12

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_live 19 / 19: /api/chat and /api/generate, streamed and not, entries and receipts.
  • maxzero_oracle_live 7 / 7: the six max_tokens: 0 cases (recorded from llama-server: it samples one token) and
    num_predict: 0 answered as -1.
  • logprobs_oracle_live (/v1) 14 / 14 unchanged; cargo test --release 106 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
    in logprobs_ollama_oracle.py; the receipt was right.

./install.sh restart and models.py restart

  • A fresh bankml serve on 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 FILE on the file already served returns at once (its sha256 already answers), and install.sh model starts the default model, and only when nothing serves. Measured: 10 s on Bonsai-8B, pid replaced, verified.
  • install.sh --help prints up to its -h line instead of a fixed line range (two lines added had cut it short).
  • Fixed: ./install.sh stop stopped only the first port. pid_on for a port where nothing listens (7874, the
    LAN view, usually off) made grep fail, and under set -euo pipefail the script ended there without a word: 7873,
    18093 and 18092 kept running. pid_on now 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's dark class on Savante; data-theme on 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 without eval (ast, bounded;
    integers and typed decimals exact, 0.1 + 0.2 is 0.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 .history under calculator; 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: true and top_logprobs 0–20. Out of range is a 400 with Ollama's own
    message, before any model is loaded; without logprobs, top_logprobs is ignored, as Ollama ignores it.
  • The answer takes the per-token path /v1 takes (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 (no id; bytes and
    top_logprobs left out when empty). Not streamed, the list is the top-level logprobs; streamed, each line carries
    its tokens' entries.
  • top_logprobs: 0 is the chosen token only, computed as llama-server's n_probs 1: 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), so max_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), so num_predict: 0 means no limit, as -1 does; bankML
    had read it as zero tokens.

logprobs_sha256 on the receipt

  • response_sha256 hashes 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 /v1 and on Ollama's API.
  • Without logprobs the receipt is unchanged, byte for byte.
  • receipts_cover_logprobs_as_written and logprobs_as_ollama_asks_and_answers (unit); live in the oracle above,
    which also checks the /v1 receipts.

Fixed

  • Savante's aivatar card opened trapped inside the side column (318 × 1075 px instead of the whole window): the
    column's backdrop-filter made it the containing block of the card's position: fixed modal. 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 --verify checks 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_vec is bankML's own SPIR-V, one invocation per row. Per block it takes the two 32-element integer sums,
    then sumi = fma(d1₁, s₁, d1₀·s₀) and sumf = fma(d0, sumi, sumf), the shipped ggml's float order. Both FMAs are
    built exactly, because d1·s can need 25 bits and the Vega 3's driver does not fuse Fma.
  • Bit-exact against q2_0::vec_dot_ref on 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 --verify now 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 (as q1_0_mat_vec8) comes next.

CI on ARM, natively

  • .github/workflows/ci.yml runs a second job on GitHub's ubuntu-24.04-arm: the build, the unit tests, clippy with
    -D warnings and 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/cpuinfo there has no model name and no cpu MHz, so the status
    page's CPU readings now fall back to Hardware, the device tree's model and the implementer and part codes, and to
    each CPU's cpufreq. The x86-only variables that warned...
Read more

bankML 0.4.0 — milestone: native serving complete

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 07 Oct 13:02

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 AVX2 quantize_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's fwht does the same arithmetic: scale by 1/sqrtf(n), then u + 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, saturating maddubs cases included.
  • kv_oracle_live: llama-server b11192 with --cache-type-k q8_0 --cache-type-v q8_0 and bankML with
    BANKML_CACHE_TYPE=q8_0 give the same answers, 6 of 6 (greedy, seeded, a 2,244-token prompt, a 320-token answer,
    a two-turn conversation; text, counts and cache_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 with cache_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 reads cache_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_n 0 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; never innerHTML), 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_dot over 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_many uses 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_7b 840 / 840 and
    oracle_forward_model_ternary 1,064 / 1,064 with the GPU off, oracle_native_serve_o4 9 / 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's console-mount branch (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/status on 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 by bankml::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's bankml 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), through GET /api/engine.
  • The first reading found the laptop's swap full (2.15 of 2.15 GB): the swap check 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 logprobs and top_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...
Read more

bankML 0.3.8 — the serving contract: the context limit, slots, the prompt cache, logprobs

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 07 Oct 04:41

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|erase on the engine address, as llama-server's slot API, with --slot-dir DIR
    (created at start). Replies carry n_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 its fs_validate_filename rules, "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_saved counts as llama-server
    counts: the prompt and every generated token but the last.
  • Savante and the console pass --slot-dir to 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-ram cache (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 and cache_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: true with top_logprobs (default 20) → choices[0].logprobs.content: per token its id, token,
    bytes, logprob, and the top tokens with theirs. Every logprob is the same 32-bit float as llama-server's:
    get_token_probabilities ported — 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, bytes raw. top_logprobs without logprobs is 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 its n_probs); clients use /v1.

bankml 0.3.7: the whole sampler chain; bankML measures itself; the GPU limiter; the bankML console

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 07 Oct 00:36

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's pp and tg), grammar time, and the CPU package's energy and joules per generated token when RAPL is
    readable; null, never estimated. A ring of 256 at GET /bankml/metrics, with totals.
  • The receipt's ttft_ms is set for non-streamed answers too (it was null); /v1 answers carry llama-server's
    timings fields (prompt_n, prompt_ms, prompt_per_second, predicted_n, predicted_ms,
    predicted_per_second, cache_n).
  • GET /bankml/usage adds package_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 a rapl group 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;
    --remove undoes 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 of d the card rests d·(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 root 0x4d5ef2d9…; 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 own std::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 as src/llama-sampler.cpp writes it, in the default order (penalties, DRY, top-n-σ,
    top-k, typical-p, top-p, min-p, XTC, temperature, dist), with llama.cpp's sorted state 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
    mt19937 and its float draw (generate_canonical<float, 24>: one 32-bit output); top-n-σ's squares in double
    (C++'s pow(float, 2)); dynamic temperature's float entropy; DRY's restart sequences, reverse Z-algorithm and libm
    pow; libstdc++'s std::sort (introsort) and generic heaps; Params::default = common.h.
  • native.rs: the request fields typical_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), a dry_base below 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_p is 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: mirostat on /v1 (the Ollama path already refused it) and a custom samplers
    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)

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 05 Oct 08:39

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 + presence taken off; disabled as llama.cpp disables it (last_n 0, or all neutral).
    general.sampling.penalty_last_n and penalty_repeat read from the GGUF as llama.cpp reads them (none of the five
    native models sets them).
  • The surfaces: /v1 and /api/* (repeat_last_n moves from the ignored resource options to the honoured ones), a
    Modelfile's PARAMETER, the C API (through the same engine). bankml generate --sample applies them too, but has
    no flags for them: only a GGUF's own general.sampling.penalty_* defaults reach it (none of the five models sets one).
  • testing/penalty_oracle.py (record, and --bankml live); oracle_penalties, oracle_penalties_8b and
    penalty_oracle_live in 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 /v1 answered 500. A negative repeat_last_n or a repeat penalty of 0 or below reached
    the engine before it was checked; the Ollama path already refused it up front. /v1 now 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)

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 04 Oct 06:54

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 own json_schema_to_grammar and
    common_chat_templates_apply in the release's libllama-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 by max_tokens inside an object, a top-level string and a number.
    Live: /v1 (one streamed) and Ollama's /api/chat with format: <schema>.
  • The content rule (oracle_json_content, llama.cpp's common_chat_parse itself): 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_layer Modelfile (mindX's persona as SYSTEM, stop <|im_end|>, num_ctx 2048), created
    FROM the merged safetensors directory and, unchanged, FROM mindx-gen39 in place: 27 / 27 answers each
    token-identical to llama-server given the persona as the system message; live through /api/chat,
    /api/generate and /v1, and /api/ps names mindx-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 as bankml convert on 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's common/json-schema.cpp and json-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 the until-13 reasoning rules) and the ChatML templates' (none), as llama.cpp writes each.
    Wired: response_format json_schema and json_object + schema, the top-level json_schema, Ollama's
    format: <schema>, bankml_chat. The schema is read from the request's own text (1.0 stays a float). Refused as
    b11192 refuses, with its message; and one deliberate divergence: a pattern with 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 to convert_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/show gives the Modelfile back. FROM a 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 the train/ and convert.rs headers: bankml convert and create replace its
    serve --to ollama path 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 "" (pi seeded on Bonsai-1.7B: "```json\n\n"). grammar::json_message.

Changed (O5's two open gaps, closed)

  • num_ctx as Ollama applies it (read from Ollama v0.13.3's server/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 past num_ctx during an answer still differ: Ollama shifts its cache, bankML does not.
  • /api/ps names 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 FROM resolves that alias, so promote.py's
    re-create in place (FROM mindx-gen39 as mindx-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.name comes from
    it. gen39 converted from a directory named merged is bb41f62d…, from mindx-gen39 the pin 6b64c748….
  • gen39's tokenizer.json lost SmolLM2's Digits pre-tokenizer when transformers 5.8 re-saved it; llama.cpp's
    chkhsh still names it smollm (the vocabulary has no multi-digit tokens), and bankML accepts both shapes.

Not in this release

  • ADAPTER (LoRA) in a Modelfile: merge first, then FROM the 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.3 and no_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

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 02 Oct 22:00

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's vec_dot_type). With one column (a decode step, the logits, a one-token micro-batch)
    llamafile_sgemm refuses (n < 2) and every element is ggml_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 columns llamafile_sgemm
    runs tinyBLAS<8, __m256>: each element one 8-lane FMA chain over the row and its own hsum — 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 duplicates token_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_pairs on adjacent pairs, the same
    contracted arithmetic as NEOX), no YaRN (ext_factor 0, attn_factor 1), GQA from the header.
  • SmolLM2's pre-tokenizer (smollm) is two passes: every digit cut out alone (a std::regex over 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_par chooses 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 smollm pre-tokenizer in tokenizer.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 ChatML mindx-genN carries. Ollama's
    persona SYSTEM for mindx-genN lives in its Modelfile, not the GGUF: here it is the caller's system message
    (bankml create is O5).
  • JSON mode on ChatML templates: llama-server builds another grammar there (its root opens with
    <|im_start|>assistant\n and has no <think> block); grammar::json_object_grammar picks the template's, checked
    against the server's own report on every recorded request.
  • The models, pinned (sAGI/models.py CONVERTED, pin_converted): no F16 GGUF is published for either, so each
    is converted from its pinned safetensors by llama.cpp b11192's own convert_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.gguf e9aba089…5a222 from HuggingFaceTB/SmolLM2-135M-Instruct@12fd25f7
      (model.safetensors 5af571cb…), Apache-2.0.
    • mindx-gen39-F16.gguf 6b64c748…58266 from the dataset PYTHAI/mindXascension@4bd31b9d
      (weights/gen39/ollama_push/merged/model.safetensors 19b62829…), 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-gen39 reaches mindx-gen39-f16.
  • Oracles, in the gate:
    • oracle_ggml_b11192_f16 (testing/f16_oracle.py): the shipped library's mul_mat on 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 through ggml_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_smollm 4,346 of 4,346; oracle_chat_template_chatml 317 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 --bankml and json_oracle.py --bankml on each (mindx-gen39 asked for as mindx-gen39),
      and the C API's bankml_chat against 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.6b pin): from the source the kernels look cheap (ggml's AVX2
    vec_dot_q8_0_q8_0 and 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

Choose a tag to compare

@Professor-Codephreak Professor-Codephreak released this 02 Oct 18:31

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 ```json fence around the object.
  • The grammar is prefilled. It is typed as a tool-call grammar, so common_sampler_init accepts 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 same mt19937.
  • 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 content is 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_candidates over 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 of oaicompat_chat_params_parse, and the content rule.

  • 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_schema with schema {} or
      {"type": "object"}), the top-level json_schema, and grammar (any GBNF; a user grammar, not prefilled);
    • Ollama's /api/chat and /api/generate with format: "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's grammars/,
      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_mode and oracle_json_mode_ternary (testing/json_oracle.py --record) replay llama-server's
      answers under response_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 live serve --native through /v1 (streamed
      once), grammar, and /api/chat with format: "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_format on serve --native's /v1 was 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::Json derives Clone. native::Done carries 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"}, in response_format, json_schema or Ollama's format:
    llama.cpp's json_schema_to_grammar and its chat-parser wrapping are not ported (O6, next).
  • format with raw: true, because JSON mode's grammar starts with the template's generation prompt;
    response_format or json_schema together with grammar; 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.
  • 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.

made with luv.pythai.net