Expand description
Serving: the inference serve block, presk serve on any backend,
serve.py in the export.
§Serving: inference serve, presk serve, serve.py
The inference serve block of a file names a model and its weights; presk serve generates tokens with them, on any backend, through the file’s own
generation function; presk export --serve writes the same loop in
PyTorch. This is the minimal form of the specification’s serve (plan 0.3.0
§3.6, question 37): one prompt, no sessions, no batching, no cache.
§The block, resolved
inference serve {
model = m.MiniGPT
weights = "ckpt/final"
precision = budget(abs_error = 1e-2) | quantize(int8, calibration = "data/calib.psk.d")
target = gpu(sm_90) | cpu(avx512)
batching = dynamic(max_batch = 64, timeout = 5ms)
cache = kv_cache(axis seq)
}presk check resolves the block as it resolves a training run:
modelis a plain module;weightsis a path relative to the file, with.psk.dappended (ckpt/final.psk.d), a one-sample file of typeparams Maspresk trainwrites it. It need not exist at check time.- The generation function is the top-level function the field
generate = namenames, or, without the field, the first function of the file, in declaration order, whose signature is(θ : params M, [key : key,] ctx : idx[V][batch: B, seq: T], … with defaults) -> idx[V][batch: B](the field since 0.10.0 step 8, the reference’s decision W4). A name that is not a top-level function of the file, or whose signature does not fit, is an error (E2010). It isnext_tokenin the example, which applies the model to the context, keeps the last position and samples from its key. A key, when the function takes one, comes second, and is given a key of its own at every token (0.10.0 step 7b; the reference’s definition “Generation function”); a function without one is served as before, and cannot sample, sincesampletakes a key. A file with the block but no such function getsW0003;presk serve --entry FNthen names one. precision,target,batching,cachepass the schema and are listed as ignored in--summary: they have no effect in this version.
-- inference --
inference serve: model MiniGPT
weights : ckpt/final (tests/examples/minigpt/ckpt/final.psk.d, not found)
generate: next_token(θ, key, ctx, temperature)
ignored : precision, target, batching, cache (checked, without effect in this version)§presk serve
presk serve FILE.psk [--const NAME=N]... [--weights FILE.psk.d] --prompt "t₁ t₂ …" [--tokens N]
[--temperature x] [--entry FN] [--dim NAME=N]... [--seed S] [--bits 256]
[--backend interp|kernel|native] [--realize f64|f32|bf16|@R.json] [--json]
[--opt [--profile FILE.json]]A realization file whose quant map names parameters serves them as
integers with per-channel scales, computed from the loaded weights (0.6.0
step 5, precision chapter, “Integer weights”): the reference interpreter
runs on their dequantised values, and the native kernels load the integers
themselves, bit-identical to running on those values (D45).
The loop, in presk-serve:
θ = load(weights, shaped like M.init(key))
ctx = prompt
k = key(--seed)
repeat --tokens times, i = 1, 2, …:
next = generate(θ, child(k, i), ctx[-T:], temperature)
ctx = ctx ++ [next]Everything with a semantics is the program’s: the model, the generation
function, the initializer that gives the weights their shapes. The crate only
sequences calls through the Backend seam, so the same command runs on the
reference interpreter (--bits), on the kernel interpreter or natively
(native chapter). Two forms of the step:
- sampling (
--temperature x > 0, default: the function’s own default,0.8in the example): the function is called as written, tokeni(from- given the child
iof the key--seednames, as a training run gives steptthe childtof its own (the reference’s definition “Generation”). Itssampledraws from that key, so every backend draws alike (it drew from a stream of each backend’s until 0.10.0 step 7b); the kernel backends draw with thelibmcrate’sexp, the one their kernels call, so their tokens do not depend on the machine’s C library (since 0.9.0 step 6), and they sum the probabilities inf64where the interpreter sums them at its precision, so a token can differ only where a draw falls within that rounding of a cumulative sum;
- given the child
- greedy (
--temperature 0): a copy of the function in which everysample(key, x)is anargmax(x)is called instead (next_token.greedy, appended to the Core program; the temperature is passed as1, the key is read by nothing). Every backend and realization agrees on this path, up to exact ties in the logits.
The context length. In the example the positional embedding is param pos : ℝ[T, D] with the file’s dim T <= 1024: the parameters fix T, and a
checkpoint trained on windows of 8 tokens only applies to contexts of 8. When
a leaf of the model’s parameter tree has the context’s seq dimension in
its type, presk serve takes T from --dim T=N (default: the prompt’s
length), loads the weights at that shape and passes the model the last
T tokens of the context at every step: a sliding window, reported as
context window 8. A prompt shorter than T is refused (E7001), a longer
one slides. When the parameters do not depend on T, as a model with RoPE’s
(coverage/llama3.psk), the whole context is passed and grows, reported as
whole context, and the file’s dim T <= … bound is checked at every step.
Until 0.17.0 step 7 (D180) the test read the initializer’s dimensions,
which are every dimension of its file, so every model was served over a
window, the prompt’s length by default. Spec question 43
records the observation; ℝ[Tmax, D] with a slice (question 9) would let the
same model read any context up to Tmax.
Output: the generated tokens on stdout, one line, space-separated; a summary
line on stderr; --json gives {model, weights, prompt, generated, window, greedy, vocab}. Exit codes: 0; 1 a diagnostic or a run-time error (a token
outside the vocabulary, a prompt shorter than the window, a violated bound);
2 no inference serve, no weights file, or a malformed --prompt/--dim.
§On the tiny MiniGPT
With the checkpoint of make train-examples (out/ckpt/final.psk.d: twenty
steps on the example tokens, V=64 D=16 H=2 L=2, windows of 8):
presk serve tests/examples/minigpt/serve_minigpt.psk --const V=64 --const D=16 --const H=2 --const L=2 \
--weights out/ckpt/final.psk.d --prompt "1 2 3 4 5 6 7 8" --tokens 8 --temperature 0 --backend nativepresk serve: MiniGPT from out/ckpt/final.psk.d, prompt of 8 tokens, 8 generated, greedy, context window 8
33 33 33 33 0 0 0 0The same eight tokens come out of the reference interpreter at 64 bits, of
the native backend in f64 and in f32, and of the exported serve.py in
float64 and in float32 (make serve-examples, make serve-pt compare the
outputs byte for byte). Twenty training steps at a warming-up learning rate
leave a nearly uniform model, hence the repetitions: the point here is the
agreement, not the text. With sampling (without --temperature 0), --seed
fixes the tokens, the same on the interpreter at 64 bits and natively in
f64 and in f32 (they were each backend’s own until 0.10.0 step 7b,
19 55 1 63 57 1 54 29 natively):
presk serve: MiniGPT from out/ckpt/final.psk.d, prompt of 8 tokens, 8 generated, sampled, context window 8
6 7 23 37 52 34 57 19A test (crates/presk-serve/tests/serve.rs) does the same on a fresh
initialization: greedy tokens identical on the interpreter and natively in
both realizations, sampled tokens identical on the interpreter and natively
for three seeds and different from one seed to another, the window rule and
its error.
§In PyTorch: presk export --serve
presk export tests/examples/minigpt/serve_minigpt.psk --target pytorch --mode readable --serve \
--const V=64 --const D=16 --const H=2 --const L=2 -o out/serve_pt
python out/serve_pt/serve.py --weights out/ckpt/final.psk.d --prompt "1 2 3 4 5 6 7 8" --tokens 8 --temperature 0 --dtype float64--serve adds serve.py and pskd.py to the readable export. serve.py
builds the exported class with its dimension arguments (MiniGPT(T), T
from --dim T=N or the prompt’s length), loads the weights, and runs the
same loop: next_token(θ, None, ctx, temperature) for sampling (the key
passed as None, read by nothing; PyTorch draws from its own generator,
seeded by --seed, so the sampled tokens are not presk serve’s), model(ctx)[:, -1].argmax(-1) for
--temperature 0, with the same sliding window. The weights are either
final.pt from train.py or a .psk.d checkpoint of presk train:
pskd.read_params decodes the latter with the layout of the parameter
tree generated into the script (LAYOUT = {"head": None, "emb": {...}, "blocks": ["stack", {...}], ...}: a leaf is None, a module a dict of its
fields in the initializer’s order, stack{L}(M) a ["stack", …] whose
leaves carry a leading axis of length L) and the shapes of the model’s own
parameters, then splits stacked leaves over the nn.ModuleList. The file
itself carries only params MiniGPT as its type, which is why the layout
travels with the script (data chapter, question 28).
§Not in this version
Sessions, batching (batching = dynamic(…)), the KV cache (cache = kv_cache(axis seq), proposed as an e-graph rewrite for 0.4.0), the block’s
precision and target (weights are quantised through --realize @R.json
instead, above, and quantize(int8, calibration = …) in the block is not
read), a server process: the block’s fields are checked and ignored, and
presk serve is a command-line loop, not a service. What it fixes is the seam: the generation function of the language,
run through Backend, agreeing bit for bit with the reference on the
deterministic path.
§In C: presk export --target c --serve
presk export tests/examples/minigpt/serve_minigpt.psk --target c --serve \
--const V=64 --const D=16 --const H=2 --const L=2 --dim T=8 --weights out/ckpt/final.psk.d -o out/serve_c
make -C out/serve_c
out/serve_c/generate --weights out/ckpt/final.psk.d --prompt "1 2 3 4 5 6 7 8" --tokens 8 --temperature 0The C export (plan 0.9.0 step 6; cli chapter) writes the generation
function and its greedy copy as C functions, at one sequence and the window
the parameters fix (--dim T=8, which a C program of one shape needs where
presk serve takes the prompt’s length), and generate, the same loop,
token i given the child i of the key --seed names (presk_key_child).
Its draws are the native backend’s from the same keys, and its output is
presk serve --backend native’s byte for byte, in every realization,
greedy and sampled, --json included (statement C5): the eight greedy tokens
above are 33 33 33 33 0 0 0 0 again, and with --seed the sampled ones are
the native backend’s for that seed. A model whose parameters do not fix the
context’s length is exported at the length --dim T=n gives (0.17.0 step
7, D180), and refused without it (E8009). The export then takes two
copies of the generation function, next_token.at and its greedy
next_token.greedy.at:
- each reads the logits at a position it is given, an indicator
at : ℝ[seq: T](1 at the position, 0 elsewhere), where the function read the last one:x[seq: -1]becomessum(axis seq, x · at), exactlyxat that position, since a product by 0 or 1 is exact and so is adding zeros; generate.cpads the context after its last token ton, reads the logits at that token, and stops when thenpositions are full;- by causality those logits are the context’s alone, so its tokens are
presk serve’s over the whole, growing context, greedy and sampled,--jsonincluded ("window": null).
The function must select the last position itself, once (E8009
otherwise). No KV cache comes with it: each step computes the whole padded
context again.
§On Llama 3.2 1B’s published weights
make llama3’s stage rw4 (experiments chapter, §25; plan 0.17.0
step 7, D180) serves coverage/llama3.psk on meta-llama/Llama-3.2-1B,
converted to a .psk.d of bf16 leaves (data chapter), from four
prompts of 16 tokens:
presk serve coverage/llama3.psk --weights out/llama3/llama-3.2-1b.psk.d \
--prompt "128000 791 6864 315 9822 374 12366 11 323 279 6864 315 15704 374 22463 13" \
--tokens 32 --temperature 0 --backend native --realize f32
presk export coverage/llama3.psk --target c --serve --dim T=48 \
--weights out/llama3/llama-3.2-1b.psk.d --realize f32 --no-exact -o out/llama3/serve_c_f32
make -C out/llama3/serve_c_f32 generatepresk serve and generate.c give the same 32 greedy tokens from each
prompt, in f32 and with the blocks’ seven matrices in int8: “ The
capital of the United States is Washington, D.C. The capital of the United
Kingdom is London. …“ after “The capital of France is Paris, and the
capital of Italy is Rome.” (statement RW4). Neither is fast:
- One thread. Both run on one thread. A kernel’s split divides its outermost loop, which at batch 1 is the batch, so nothing is divided.
- No KV cache. Each token computes the whole context again.
presk servealso compiles the kernels again at each new length: 16 to 28 minutes for 32 tokens inf32, 8 inint8, whose weights are a quarter of the bytes.generate.ccomputes all 48 positions at every step, in 21 to 33 minutes.- Memory.
presk serveholds about 29 GB inf32. The C program’s arena is 5.0 GB inf32and 2.1 GB inint8.
A KV cache, and a split over the sequence at batch 1, are later work.