Skip to content

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
· 31 commits to main since this release

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 on ARM are marked so; both targets are clippy-clean.

GPU objects are released on drop (gpu_objects_are_released_on_drop; the first 0.5.0 item)

  • Found by the 2026-10-06 audit: Gpu destroyed nothing on drop (device, buffers, pipelines), so dropping and reopening
    a worker leaked its card memory; and unsafe impl Sync for Buffer let two Gpus on two threads write one buffer.
  • Every object is now released on drop. The Vulkan instance, the device and its entry points live in one shared
    Device, held by the Gpu and by every Buffer and Pipeline made on it. The device, and then the instance, go
    with the last of them, so nothing is used after its device is gone. A Buffer is no longer Sync, and write and
    submit refuse a buffer or pipeline of another device.
  • The new test found what the audit had not: each Gpu::open also made a Vulkan instance and never destroyed it.
    • Measured with the driver's own counters (sysfs VRAM + GTT), over 30 rounds of open, five 16 MB buffers, a
      pipeline, a run and drop.
    • Before, card memory grew 65 MB, about 2.2 MB a round; after, 221.4 → 223.4 MB, within the desktop's own noise.
  • The same bits: the GPU kernel oracle and bankml gpu --verify are bit-exact on every row, and the 1.7B forward
    oracle is 840 / 840 with the card computing 35 % of each 1-bit matrix.

Abort a native run when its client goes away (testing/disconnect_oracle.py, 6 / 6)

  • Found by the ultimate-bankml-ui session under contention: serve noticed a closed connection only when a write
    failed, so an abandoned request's whole prefill (and, unstreamed, its whole answer) ran for nobody, minutes per
    request on the laptop, with the one slot held.
  • Native::complete_while asks alive() between prefill micro-batches (Weights::prefill_while: the same 512-token
    micro-batches, so the same bits) and before each generated token. serve::client_alive is a non-blocking peek at the
    client's socket: end of file or a reset means gone, anything else (bytes waiting, nothing yet, any doubt) means
    there. /v1/chat/completions (streamed or not) and Ollama's /api/chat and /api/generate all use it.
  • A stopped run logs where it stopped (bankml: the client went away; stopped after N of M prompt tokens (kept in the slot)), records no answer, and leaves the slot holding exactly what it computed: the next request reuses it
    (cache_n > 0) and its answer is still an empty slot's.
  • Checked live: an abandoned 2,294-token prompt stopped after its first micro-batch; the reuse and its answer; an
    abandoned 400-token stream stopped early; /api/generate abandoned the same way. No answer changed:
    oracle_native_serve_o4, cache_prompt_false_is_an_empty_slot, the session (14 / 14), logprobs (14 / 14) and
    Ollama-shape (9 / 9) oracles all pass. disconnect_oracle_live is in the release gate.

Record: testing/results/0.4.2.txt — one run of the full release gate, every stage passed (2026-10-08, the laptop), then the fixes folded in after it checked again.

made with luv.pythai.net