Skip to content

Letter readout

The letter readout answers POST /v1/systemone on any model other than DiffusionGemma, with the prompt and calibration of the helper code that OpenJev publishes. A decision reads the state once and runs each question's prompt as a short tail after it, in fixed forwards that the server's batch loop interleaves with chat. The request and response contract is in Structured decisions, and the timings are in Structured read measurements.

Origin

gmlx/systemone/letters.py is a port of helper/shim.py from huggingface.co/openjev/openjev at revision 0b6bb6e, with the settings that the repository's serve/SERVE.md measured. The helper is a proxy that sends each question to a vLLM server and reads the letter log-probabilities back. gmlx runs the same protocol in the server process, and licenses/openjev-LICENSE names the ported files.

letters.py is pure Python. It holds the request rules, the prompt text of every pass and the answers, and each question is a generator that yields the passes it needs and receives their letter scores. ar_reader.py runs the passes on the model, prefixes.py keeps state prefixes in the prompt cache, and backends.py parses a body for both readouts, since the route learns the model's kind only after it reads the body.

One question

Each question becomes the only user message of a chat prompt, rendered with the model's template, thinking off and the generation prompt on:

State:
{state}

Question: {instructions}
Options:
[A] {name}: {description}
[B] {name}: {description}

Answer with the letter of the best option only.

An object state is its JSON with the characters kept, and a list or number is its Python string. Instructions that are not a string also take their Python string, as the helper's pyrepr style does. The answer is the score of each listed letter at the first reply position, divided by 0.85 and normalized over those letters. Each letter must be a single token of its own, which the route checks once per model.

A yes or no question lists yes and no, described by criteria.true and criteria.false. Without criteria, the descriptions are "The statement is true." and "The statement is false." The answer is sigmoid(logit(p_yes) / 1.829074), the helper's calibration. A score question lists its levels as 0 to n-1, and its instructions end with the added sentence Rate along the ordered levels below (lowest first). The score is the expected level.

The letters run from A to Z and then a to z, so one prompt holds 52 options. A question with more is read in near-equal chunks of at most 52, then once more over the chunk winners, and the two reads compose as p(i) = p_final(chunk of i) * p_chunk(i) / p_chunk(winner of that chunk). With samples: N, each chunk is read in N orders, shuffled by random.Random(j) for order j, and the probabilities are averaged by option.

The shared prefix

Every pass of a request starts with the chat header and the state, so the reader prefills that part once. The split is the common start of two token lists, the prompt that ends at the state and the prompt of a pass. Tokens that merge across the end of the state stay in the tails, and every pass continues the state with the same text, so any pass gives the same split.

Every forward carries at most FORWARD_TOKENS tokens, 128, which never depends on load. The prefix runs in chunks of that size from position 0, and each group of tails fills at most one forward. A size that followed load would change the chunks of the recurrent state and the kernel route, so the same request could give different numbers. The measurements page gives the choice between 64, 128 and 256.

Tails run in groups through views of the prefix cache that broadcast it to every row of the forward. An attention view returns the prefix keys and values joined to the rows' own and stores nothing. A recurrent view hands each row the prefix state and drops the writes. Right padding needs no mask, since no later position changes an earlier one. plan_buckets sorts the tails by length and groups them so that padded tokens plus a fixed cost per forward is lowest, within the forward size and a 512 MB bound on the copy that the views make.

A tail longer than one forward, or a cache with layers the views do not cover, such as a rotating window, runs alone on a bitwise copy of the prefix. A layer that cannot be copied has its prefix computed again.

Letter scores

The reader takes the hidden state at each tail's last token and multiplies it by the head's 52 letter rows in float32. The rows are dequantized once per model. A bfloat16 head output can round a score by about 0.06, which the division by 0.85 would carry into the answer.

A self-check picks this path once per model, and again after an adapter replaces the head. It runs a short probe through the whole model and through the trunk and head separately, and it takes the rows path only when the two logits are equal and the float32 rows reproduce the head's letter scores. Any other model, such as one that scales its logits, reads the letter columns of the full logits. diagnostics.path reports rows or logits.

Running on the server

An autoregressive model's engine thread runs a batch loop that calls ResponseGenerator._collect_pending_requests between batch steps. A wrapper in gmlx/serve/engine_jobs.py takes decision jobs out of the request queue there, and out of the queue directly, so a full batch cannot hold a job back. While chat rows are active, it runs one forward of the first job per call and rotates the jobs. With no chat rows, the loop keeps calling back at once, so a decision runs at full speed.

A job step leaves the chat rows as it found them. It never drains cancellations, never touches the tokenizer's stop criteria and never reseeds the random state. It puts back the rope position state of a multimodal model, sets the prefill command-buffer phase and restores the previous one, and holds the wired limit. With per-row adapters, it publishes the request's adapter scales around each forward.

The deadline, server.token_queue_timeout_s, starts when the job leaves the queue, and the job's stop event is checked at each forward. A client that disconnects sets it too. An engine cancel for the job's id never reaches the job, since the batch loop consumes the id and finds no row, so the route cancels only through the stop event. A diffusion job receives engine cancels, and tests/serve/test_engine_jobs.py pins both behaviors.

Before queueing, the route tokenizes every pass that the decision can send and checks the longest against the context and memory budgets, with room for one forward past it. A composed question's winners pass is bounded by the longest option of each chunk.

Kept prefixes

The route keeps each decision's prefix in the server's prompt cache, the APC manager, so the next decision on the same state reads only its tails. A model with recurrent layers and full attention, such as OpenJev, keeps it in the checkpoint tier as a record of kind decision, memory only. A model whose layers are all plain attention keeps it in the exact tier. A lookup asks for exactly the prefix length, so a shorter record never serves.

The records carry their own salt, made of the reader version, the forward size and the request's adapter scales. A chat request never adopts one, and a decision never adopts a chat record, which was prefilled in other chunks. The reader's caches are never quantized, so a kept prefix equals a computed one bit for bit.

The checkpoint tier has its own rules for decision records. One is never promoted to anchor, a decision insert strips only decision records on its chain, and no other insert strips a decision record. The manager keeps at most eight of them, so a stream of new states cannot push the chat records out, and eviction ranks them with the other records that are not anchors. The exact tier shares its count and byte bounds with chat entries.

A cache stack that neither tier takes reports "tier": "unsupported", and a server with the prompt cache off, like the offline command line, reports "off". POST /v1/prewarm runs the prefix step alone, as the helper's route of the same name does.

Parity with the helper

tests/systemone/test_systemone_letters.py runs the vendored helper, tests/systemone/_openjev_helper_ref.py, and the letter decision on the same scripted letter scores. Both must send the same prompts and return the same answers for each question type, object and list states, string, object and list instructions, 52, 60 and 120 options and several orders.

The intended differences are few. The helper rounds its answers to four decimals and gmlx does not. confidence is the official Jev value, as on DiffusionGemma. The route refuses a state that holds an image, which the helper would send to a vision model. ask and ask_if apply, and a question with ask_if is read in a later stage than the questions it names, although its prompt does not carry their answers.

tests/systemone/test_systemone_ar_reader.py checks the reader on a small Qwen3.5-shaped model. Batched tails must give the scores of a full forward over the whole prompt, a kept prefix must equal a computed one through both cache tiers, and a row's scores must not change with the other rows of its forward.