|
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
15.9 kB
| # `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 | |
| ```sh | |
| 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-simple` | |
| with `--spec-ngram` (opt-in since 0.1.8; exact at temperature 0) and `--slot-save-path` with `--slot-dir` (0.1.9). It waits up to 900 s for `/health` and notices at | |
| once if the engine exits. | |
| - `--upstream`: waits up to 10 s for `/health`, then requires `/props` `model_path` to 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 `--upstream` address 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 `--registry` | |
| every 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](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](ollama.md)) | | |
| ```sh | |
| 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 `Chunk`s (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](prompt_cache.md)) and | |
| `BANKML_CACHE_TYPE` (`f16` or `q8_0`, the KV cache's type, [native.md](native.md)) when a model opens. | |
| ### At the edges: the context limit, slots, logprobs (0.3.8) | |
| - **Context limit.** `parse_ctx` refuses a prompt of `n_ctx` tokens or more with `exceed_context_json(n_prompt, | |
| n_ctx)`, llama-server's `exceed_context_size_error` body; an `Err` that 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()` answers `POST /slots/{id}?action=…` on the engine address. `slot_filename_ok` is llama.cpp's | |
| `fs_validate_filename` without 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_json` writes llama-server's | |
| error object. Without `--slot-dir` the 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 is `Native::save_slot` / `restore_slot` (see [native.md](native.md)). | |
| - **Logprobs.** `logprobs` + `top_logprobs` set `Params::n_probs`; the engine returns per token its probability and the | |
| top `n` (`sampler::token_probs`) with whether its UTF-8 was complete. `logprob_entries` applies 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 — and `logprobs_json` writes them, a top token's | |
| text cut by `utf8_complete_len` (`validate_utf8`) and otherwise U+FFFD-replaced, as nlohmann's `dump` writes it. | |
| Streamed, `NativeChat::run_steps` turns the engine's steps (`Native::complete_with`) into `Chunk`s: 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.rs` against a mock llama-server: `serve_gates_and_signs_answers` (the `response_sha256` equals the | |
| sha256 of the text the mock sent, streamed and not; `request_sha256` the body's) and | |
| `serve_refuses_an_unverified_model_or_a_different_upstream_file`; `hostile_headers_refuse_not_crash`. | |
| - `--native` is 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 `/v1` and `/api/chat` on mindx-gen39, CHANGELOG 0.3.6), `sampler_oracle_live` (0.3.7). | |
| - `capi_chat_oracle`: `bankml_chat` equals `serve --native` turn 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 test | |
| `logprob_texts_as_llama_server_writes_them`. Counts from CHANGELOG 0.3.8. | |
| - 0.3.9: `kv_oracle_live` (6 / 6: `BANKML_CACHE_TYPE=q8_0` against llama-server `--cache-type-k q8_0 --cache-type-v | |
| q8_0`, text, counts and `cache_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 (`/health` 1.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::parse` returns `Option`, errors are `Result`); 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. | |
| - `--native` serves one slot and one resident model (see [native.md](native.md)); `/slots/{id}` accepts only id 0. | |
| - `/bankml/usage` samples for 250 ms under a lock, so a poller arriving during a sample waits for it. | |
| - `/completion` (llama-server's own endpoint, and its `n_probs`) is not served natively; `/v1` and `/api` are. | |
| - 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. | |
| ## See also | |
| - [../usage.md §6](../usage.md#6-by-hand-build-verify-serve) · [§9 Receipts](../usage.md#9-receipts-and-how-to-check-an-answer) · | |
| [§13 Reference](../usage.md#13-reference-commands-ports-environment) | |
| - [../oracles.md §5, the gateway](../oracles.md#5-the-gateway-receipts-against-the-text) · [../OLLAMA.md](../OLLAMA.md) · | |
| [../CAPI.md](../CAPI.md) · [../TODO.md](../TODO.md) | |
| - [native.md](native.md) · [ollama.md](ollama.md) · [create.md](create.md) · [grammar.md](grammar.md) · | |
| [schema.md](schema.md) · [main.md](main.md) | |