Download docs/modules/serve.md from PYTHAI/bankml: direct link, hf CLI and curl.
- Browser
- Download file 15.9 kB
-
https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/docs/modules/serve.md
- Command line
-
hf download hf://spaces/PYTHAI/bankml/docs/modules/serve.md
-
curl -L -o serve.md https://huggingface.co/spaces/PYTHAI/bankml/resolve/main/docs/modules/serve.md
bankML/serve.rs — bankml serve, the verifying loopback gateway
Summary
serve.rs is the HTTP gateway behind bankml serve. It does not start until the model file passes verify (the
guard, then the sha256 pin in FORK.json). Every /v1/chat/completions answer carries a receipt: the bankml version,
the engine, the model's sha256, the guard verdict, token counts, timings, and the sha256 of the answer text and of
the request it answered.
It runs in one of two modes. In front of llama.cpp b11192's llama-server (P0, since 0.0.6), it vouches for the
file, the path and the transcript, not for the arithmetic. With --native (0.3.0), the answers come from bankML's own forward pass
(native.rs), and the gateway also speaks llama-server's endpoints on the engine address and Ollama's API
(ollama.rs).
The module also holds the shared pieces other modules use: NativeChat (one native chat request, shared with the C
API's bankml_chat), Tally (what a receipt is made from), FileIdent, and Json, a small JSON reader.
Technical usage
Modes
bankml serve FILE --fork FORK.json --spawn /path/to/llama-server --threads 3 --ctx 2048 # launch the engine on the verified path
bankml serve FILE --fork FORK.json --upstream 127.0.0.1:18092 # check a running one by its /props
bankml serve FILE --fork FORK.json --native --registry --ctx 2048 # bankML's own forward pass
--spawn: refuses if something already answers on the upstream address. It launches llama-server with-m <verified path> --host --port -t -c -np 1 --jinja --reasoning off --no-webui, plus--spec-type ngram-simplewith--spec-ngram(opt-in since 0.1.8; exact at temperature 0) and--slot-save-pathwith--slot-dir(0.1.9). It waits up to 900 s for/healthand notices at once if the engine exits.--upstream: waits up to 10 s for/health, then requires/propsmodel_pathto be the same canonical path.- In both, before every answer the file's identity (device, inode, size, modification time) and the engine's model path are checked again; a change answers 503 and no receipt.
--native: also binds the--upstreamaddress and answers llama-server's endpoints there, so a client written for llama-server (Savante) reaches it unchanged. Since 0.3.1 it also speaks Ollama's API, and with--registryevery model pinned in the forks directory may be asked for by name. A reaper thread drops an expired model every second.
Defaults (from main.rs): --listen 127.0.0.1:18093, --upstream 127.0.0.1:18092, --threads 3, --ctx 4096;
--keep-alive defaults to Ollama's 5m.
Endpoints
| method, path | P0 (llama-server) | --native |
|---|---|---|
GET /bankml |
what was verified, when, the upstream | the same, plus resident and models (the last verification when none is resident) |
GET /bankml/usage |
memory and CPU of bankml serve and the spawned engine; since 0.3.7 also package_watts, each GPU's busy %, VRAM and GTT, and the GPU limiter's state (sys.rs, sampled over 250 ms, at most once a second however many clients poll; the sample is shared) |
the same |
GET /bankml/metrics (0.3.7) |
the native engine's records (empty in P0) | one record per completion, a ring of 256 with totals: TTFT, prompt and generation tokens per second, grammar time, energy (metrics.md) |
GET /health |
proxied | {"status": "ok"} |
GET /props, GET /v1/models |
proxied | from the resident model (or the startup model, loaded) |
POST /tokenize, POST /apply-template |
— | as llama-server's |
POST /slots/0?action=save|restore|erase (engine address) |
llama-server's own: the engine address is llama-server itself, and --spawn passes --slot-dir as --slot-save-path |
KV cache to and from --slot-dir (0.3.8) |
POST /v1/chat/completions |
proxied, receipt added | native_chat, receipt added |
/ and /api/* |
— | ollama::route (see ollama.md) |
curl -s 127.0.0.1:18093/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"Say hello. /no_think"}],"max_tokens":32}'
The receipt
Non-streamed: a top-level bankml_receipt object. Streamed: one extra data: {"bankml_receipt": …} event before
data: [DONE] (OpenAI clients ignore it). With it a client can check that the text it shows is the text this
gateway received from the engine serving the verified file, for this request. Fields, from Tally::receipt:
bankml, engine, model_sha256, guard, prompt_tokens, completion_tokens, ttft_ms, wall_ms,
response_sha256 (of the answer text exactly as produced), request_sha256 (of the request body), signed: false.
Receipts are not signed. They prove integrity between a client and its own bankml serve, not to a third party.
In --native mode ttft_ms is measured for non-streamed answers too (0.3.7; before, it was null).
Loopback rules
| condition | answer |
|---|---|
Host not 127.0.0.1, localhost or [::1] (any port) |
403 |
Transfer-Encoding on a request |
400, "send Content-Length" |
| body over 16 MiB | 413 |
POST without Content-Type: application/json |
415 (curl -d alone sends a form type) |
| more than 32 open connections | 503, answered off the accept thread after draining what the client sent for at most 200 ms, so a slow client cannot hold the accept loop |
| request line or header line over 16 KiB, or more than 100 header lines | 400 |
A refusal reads and discards the body it was sent before closing, so the client does not lose the reason to a connection reset. Connections get a 30 s read timeout and a 120 s write timeout. An upstream body or chunk over 64 MiB is refused before allocation. SIGTERM, SIGINT and SIGHUP stop a spawned engine too.
--native /v1/chat/completions
native_chat resolves model through the registry and the derived models (create::resolve; absent means the
startup model), applies a derived model's layer (apply_openai), and refuses a malformed stop or a layer
num_ctx above --ctx before any load. It then acquires the model and keeps it resident afterwards: the OpenAI API
names no keep-alive.
NativeChat::parse_ctx(eng, req, text, num_ctx) builds the request: stop strings; the constraint
(response_format, json_schema, grammar, read from the request's own text so a schema's numbers are exact); a
grammar check with llama.cpp's parser (an unparsable grammar is a 400 with the parser's reason, not a failed run); the prompt (fitted under num_ctx); the sampler parameters; and max_tokens
or n_predict. Since 0.3.6 it also builds the sampler once, so a negative repeat_last_n or a repeat penalty of 0
or below is a 400 with llama-server's message (the live penalty oracle had found it answered 500). NativeChat is
shared with the C API's bankml_chat since 0.3.2, so both answer a request the same way; the constraint fields
arrived in 0.3.3 and num_ctx fitting under a derived model in 0.3.5. NativeChat::run passes the answer through the stop filter and the
JSON content stream. The non-streamed response (completion_json) carries OpenAI's object, llama-server's timings
(timings_json: cache_n, prompt_n, prompt_ms, prompt_per_second, predicted_n, predicted_ms,
predicted_per_second, measured in the engine since 0.3.7), logprobs when asked for, and the receipt. A streamed
answer's last chunk carries usage and timings.cache_n. In timings, a rate is null when its count or time
is 0. The model stays resident after a /v1 answer (the OpenAI API has no keep-alive; 0.3.0 likewise kept its one
model for good).
| item | what it does |
|---|---|
NativeChat::parse(eng, req, text) / parse_ctx(…, num_ctx) |
the request, parsed and checked before any token is computed |
NativeChat::run(self, eng, t: &mut Tally, stream, emit: impl FnMut(&str) -> bool) |
the answer as text pieces |
NativeChat::run_steps(self, eng, t, stream, emit: impl FnMut(Chunk) -> bool) |
the answer as Chunks (0.3.8); run is built on it |
Chunk { text, entry: Option<TokenLogprob>, first } |
one streamed piece: its text, the logprobs of the token that released it, and whether it is the first token's (the role delta goes with it) |
completion_json, timings_json |
the non-streamed answer and its timings |
exceed_context_json(n_prompt, n_ctx) |
llama-server's 400 body for a prompt that does not fit (0.3.8) |
logprob_entries, logprobs_json, utf8_complete_len |
llama-server's logprobs rules and JSON (0.3.8, below) |
slot_filename_ok(name) |
llama.cpp's fs_validate_filename (0.3.8, below) |
--native reads BANKML_CACHE_RAM (the host prompt cache's size, prompt_cache.md) and
BANKML_CACHE_TYPE (f16 or q8_0, the KV cache's type, native.md) when a model opens.
At the edges: the context limit, slots, logprobs (0.3.8)
- Context limit.
parse_ctxrefuses a prompt ofn_ctxtokens or more withexceed_context_json(n_prompt, n_ctx), llama-server'sexceed_context_size_errorbody; anErrthat starts with{is sent as that JSON with status 400. A generation stops when prompt and completion fill the context,finish_reason"length". - Slots.
slots()answersPOST /slots/{id}?action=…on the engine address.slot_filename_okis llama.cpp'sfs_validate_filenamewithout subdirectories: no path separators or reserved characters (: * ? " < > | / \), no control characters (C0, DEL, C1), no U+FFFD, BOM, U+FF0E, U+2215 or U+2216, no leading or trailing space, no trailing dot, at most 255 bytes.error_jsonwrites llama-server's error object. Without--slot-dirthe answer is llama-server's 501; any id but 0 is "Invalid slot ID"; a failed restore is a 400 and a failed save a 500, as llama-server answers. The KV file itself isNative::save_slot/restore_slot(see native.md). - Logprobs.
logprobs+top_logprobssetParams::n_probs; the engine returns per token its probability and the topn(sampler::token_probs) with whether its UTF-8 was complete.logprob_entriesapplies llama-server's rules — an entry only after complete UTF-8, carrying the text the stop filter released since the previous entry; a stop word's own tokens (tokenize(stop, add_special = false)) dropped — andlogprobs_jsonwrites them, a top token's text cut byutf8_complete_len(validate_utf8) and otherwise U+FFFD-replaced, as nlohmann'sdumpwrites it. Streamed,NativeChat::run_stepsturns the engine's steps (Native::complete_with) intoChunks: text only at whole tokens, each with that token's entry, the first behind the role delta (sent only when logprobs are asked for, so plain streams are unchanged); the end-of-turn token releases what a partial stop match held.
Json
pub enum Json { Null, Bool, Num(f64), Str, Arr, Obj(Vec<(String, Json)>) } with parse, get (the last key wins),
idx, as_str, as_bool, as_u64. Numbers are kept as f64 (token counts are small integers). Nesting deeper
than 128 is refused. A high surrogate escape joins a following low one; an unpaired one becomes U+FFFD, as the UI
does, and an escape after it that is not a low surrogate is read on its own.
How it is verified
- Unit tests:
json_parses_what_llama_server_sends,chunked_and_sized_bodies,heads_are_bounded,only_loopback_hosts,unpaired_surrogates_become_replacement_characters,file_identity_changes_are_seen. testing/cli.rsagainst a mock llama-server:serve_gates_and_signs_answers(theresponse_sha256equals the sha256 of the text the mock sent, streamed and not;request_sha256the body's) andserve_refuses_an_unverified_model_or_a_different_upstream_file;hostile_headers_refuse_not_crash.--nativeis covered by the live gate stages:serve_oracle_ollama_shape(testing/serve_oracle.py --bankml),json_oracle_live,json_schema_oracle_live,o4_live,persona_oracle_live,penalty_oracle_live(85 / 85 through/v1and/api/chaton mindx-gen39, CHANGELOG 0.3.6),sampler_oracle_live(0.3.7).capi_chat_oracle:bankml_chatequalsserve --nativeturn by turn, receipt hashes included.- 0.3.8:
context_oracle_live(8 / 8 against llama-server at-c 256),slot_oracle_live(19 / 19: answers after a restore equal an empty slot's, across a restart; llama-server's refusals),logprobs_oracle_live(14 / 14, five streamed; every logprob the same float),session_oracle_live(14 / 14: interleaved conversations through one slot and the host prompt cache, turn by turn; simultaneous requests queued); the unit testlogprob_texts_as_llama_server_writes_them. Counts from CHANGELOG 0.3.8. - 0.3.9:
kv_oracle_live(6 / 6:BANKML_CACHE_TYPE=q8_0against llama-server--cache-type-k q8_0 --cache-type-v q8_0, text, counts andcache_n; CHANGELOG 0.3.9).
Advantages and efficiency
- What it adds over a bare llama-server. The file is guarded and pinned before the engine sees it, the engine must be serving that file, and every answer is receipted against the text and the request. llama-server checks none of this.
- Cheap per request. The file is hashed once at start; each answer re-checks only its identity. docs/PERFORMANCE.md
measures the gateway's own cost at 0.71 ms per request (
/health1.08 ms through serve vs 0.37 ms direct), and the sha256 pin of the 1.16 GB 8B model at 2.9 s with SHA-NI. A PGO build was measured and rejected for that reason (docs/TODO.md). - Streaming end to end. Upstream events are forwarded line by line as they arrive; native pieces are written and flushed as they form.
- Bounded by construction. Fixed limits on connections, head size, body size and upstream bodies; no panic paths
on request input (
Json::parsereturnsOption, errors areResult); std-only HTTP and no external crates. - Cheap edges. A prompt past the context is refused before any computation; a slot restore replaces the prompt's whole prefill with one sequential read checked by sha256; logprobs cost one partial sort of the vocabulary per token, and nothing when not asked for.
- Next (docs/TODO.md): an independent review of
serve(loopback rules, limits, receipts) before 1.0; more than one slot with continuous batching in--native(O8).
Limitations
- Loopback only, by design. View mode for the LAN is a separate Python page (usage.md §7).
- Receipts are unsigned.
- In P0 mode the tokens come from ggml's kernels in llama-server; bankML vouches for the file, the path and the transcript, not the arithmetic.
--nativeserves one slot and one resident model (see native.md);/slots/{id}accepts only id 0./bankml/usagesamples for 250 ms under a lock, so a poller arriving during a sample waits for it./completion(llama-server's own endpoint, and itsn_probs) is not served natively;/v1and/apiare.- A plain stream (no logprobs) has no role delta, and leftover incomplete bytes at the very end are sent replaced by U+FFFD, where llama-server drops them.