LFM2.5-350M constrained scoring recipe¶
Runs the in-repo LFM2.5-350M hybrid-state candidate engine behind the Rust frontend, and provides real-GPU verification and benchmarking scripts. The engine pre-fills a question once, forks both attention KV and convolution history per candidate batch, and scores complete JSON values by summed full-vocabulary conditional log probabilities.
This recipe covers text state and choice questions only. Each public
question is evaluated independently; only candidates within one question share a
prefill. Question IDs are never placed in the prompt.
Pinned artifacts¶
| Artifact | Pin |
|---|---|
| Base model | LiquidAI/LFM2.5-350M@9e6c6ccf47cd318696e137d381a7ded8fe4df09f |
| Reference engine | notnotsamuel/LFM2.5-350M-RLCD@deb589d803d141cabd158ef55f6617b128529f36 (rlcd/engine.py) |
Reference code is loaded with importlib from a local checkout; no code is
re-downloaded during a run. Fetch only the code files:
hf download notnotsamuel/LFM2.5-350M-RLCD \
--revision deb589d803d141cabd158ef55f6617b128529f36 \
--include 'rlcd/*' --local-dir .local/lfm/reference
Setup¶
The recorded Linux CUDA environment uses Python 3.12 with the full dependency
lock in src/models/lfm2/requirements.lock.txt (PyTorch 2.14.0, Transformers 5.17.0,
huggingface-hub 1.31.0, jsonschema 4.26.0). Install the pinned file into an
isolated environment, for example:
python3.12 -m venv .venv-lfm
.venv-lfm/bin/python -m pip install -r src/models/lfm2/requirements.lock.txt
source .venv-lfm/bin/activate
Python, PyTorch, Transformers and CUDA versions are recorded in each manifest. Do not
assume the candidate_batch_size is a fixed memory budget: it bounds simultaneous
branches, not bytes.
Run the worker and frontend¶
Run the worker from the directory that contains engine.py and worker.py:
cd src/models/lfm2
python worker.py --candidate-batch-size 16 --device cuda --host 127.0.0.1 --port 8000
Then, from the repository root:
cargo build --release --locked
OMNI_JEV_BIND=127.0.0.1:8080 OMNI_JEV_BACKEND_URL=http://127.0.0.1:8000 \
./target/release/omni-jev
curl http://127.0.0.1:8080/health
curl http://127.0.0.1:8080/v1/systemone -H 'Content-Type: application/json' -d '{
"model": "lfm2.5-350m",
"state": "I was charged twice. Please refund the duplicate charge. No hurry.",
"questions": {"refund": {"type": "choice", "instructions": "Does the customer ask for a refund?",
"criteria": {"yes": "A refund is requested", "no": "No refund is requested"}}}
}'
confidence is required in the response but is normalized-entropy concentration
of the candidate distribution, not a calibrated correctness probability and not
the proprietary Jev formula. No calibration evaluation is included.
usage.input_tokens sums each question's prefix once and every candidate's
unpadded field/value suffix. It excludes padding and repeated cache copies and
is unchanged by candidate batch size. There is no cross-question shared cache.
Startup completes a real two-candidate decision before opening the HTTP port;
a failed warmup exits without advertising readiness.
Verify on a real GPU¶
verify.py loads one set of read-only weights, loads the pinned reference engine
with importlib, and builds an independent uncached oracle by concatenating
prefix + suffix + value and running a full use_cache=False forward with FP32
full-vocabulary log-softmax. The candidate scorer is never used as its own
oracle. The reference engine file is checked against its pinned SHA-256.
python recipe/lfm2/verify.py \
--reference-dir .local/lfm/reference \
--device cuda --output recipe/lfm2/verify_report.json
Required checks, all of which must pass for exit code 0:
score_parity: on the pinnedSUPPORT,SENTIMENT,ROUTINGand stress schemas, raw scores within 0.15 of the oracle and identical selections.cache_isolation: the forked cache keeps exactly 6 attention and 10 convolution layers, cached token distributions match the full forward atatol=0.015,rtol=0.08, argmax is equal, and the shared cache is unchanged.api_multi_question: the sameq1answer holds when the question is alone, combined, reordered, or renamed;usage.input_tokensis the sum of the questions' prefix and candidate input tokens; health andPOST /v1/systemonematch direct calls.near_ties: a fixed set of ambiguous probes reports measured margins (the minimum observed margin and any selection flips at every batch size; a near tie is never assumed).- Candidate batch sizes
1/8/16/32/64/allare consistent for every case.
The JSON report stores raw scores, maximum errors, selections, token ids,
margins, the execution environment and source hashes. Gold labels from the
pinned diagnostics are hand-authored and are reported individually only; they
are not an aggregate accuracy estimate. A failed run writes a fail report and
exits non-zero.
Benchmark on a real GPU¶
bench.py calls Engine.score directly with internal schemas (a fixed answer
string enum or a raw multi-field schema). It serializes no request and never
touches the public worker path, so its numbers are internal-scorer numbers. The
workflow matrix crosses candidate count 2/16/64/255 with short/long context and
short/mixed candidate lengths, using fixed synthetic distractor text. Actual token
counts are recorded per cell. A separate multi_field table can be added with
--suites and is never mixed with the string-enum table.
The uncached oracle intentionally computes logits for the complete input, including prefix positions. It is a correctness baseline, not a tuned uncached serving implementation; its timing includes that output-projection cost. Compare full versus bounded branching directly when attributing the batching trade-off.
The recipe reports three distinct measurements and never combines them:
- Internal scorer:
bench.py, andverify.pyscore_parity, callEngine.scorein-process on internal schemas; no request is serialized. - Public worker path:
bench_worker.pyruns the realWorker.systemone, including request/response JSON encoding and full response construction, with no HTTP transport. This is the public performance number. - Multi-process frontend:
compare_with_frontend.pyonly checks that the Rust frontend returns byte-identical status,Content-Typeand body to the worker. It is a correctness check, not a latency measurement.
python recipe/lfm2/bench.py \
--reference-dir .local/lfm/reference --device cuda --quick \
--warmup 3 --samples 20 --output recipe/lfm2/bench_results.jsonl
Drop --quick for the full matrix. --quick results are labelled quick in the
manifest and summary and are never reported as full. Each measured sample
synchronizes before and after perf_counter, resets peak memory statistics, and
records latency plus allocated and reserved peaks; p50/p95 and the raw sample
list are reported per cell and variant. Variant and cell order is rotated across
samples to avoid a fixed-order bias. Out-of-memory runs are recorded as oom;
numbers are never fabricated. Inference selections are checked against the
uncached oracle for every sample.
Results append to JSONL and can resume after an interruption:
python recipe/lfm2/bench.py --reference-dir .local/lfm/reference --device cuda \
--resume --output recipe/lfm2/bench_results.jsonl
bench_worker.py loads one FP16 weight set and shares it across candidate batch
sizes. The matrix uses 16-candidate choice questions at question count 1/3/8,
short/long state and candidate batch 1/8/all. Every measured output is compared
with the same-config warmup answer (choice and all probabilities within 1e-6),
usage.input_tokens is checked against the sum of independently measured
per-question prefix and candidate input tokens, and question ids are checked to stay out of the
prompt. Raw samples append as JSONL, with p50/p95 and allocated/reserved peaks
in the summary.
python recipe/lfm2/bench_worker.py --device cuda \
--warmup 3 --samples 20 --output recipe/lfm2/bench_worker_results.jsonl
A sidecar manifest records GPU, driver, Python, PyTorch, Transformers, model and reference pins, and source file hashes. The summary is written atomically next to the JSONL. The measured outcomes and limitations are in RESULTS.md.
Compare worker and frontend¶
compare_with_frontend.py sends health, valid choice, stability, unicode and
malformed/invalid requests to the worker directly and through a running Rust
frontend, then compares status, Content-Type and raw body bytes. It never starts
or stops either service and talks only to loopback URLs.
python recipe/lfm2/compare_with_frontend.py \
--worker http://127.0.0.1:8000 --frontend http://127.0.0.1:8080 \
--output recipe/lfm2/compare_frontend.json
Files¶
| File | Role |
|---|---|
verify.py |
Real-GPU correctness verification and JSON report. |
bench.py |
Real-GPU latency/memory benchmark of the internal Engine.score and JSONL plus summary. |
bench_worker.py |
Real-GPU latency/memory benchmark of the public Worker.systemone path and JSONL plus summary. |
compare_with_frontend.py |
Worker/frontend byte-parity checks against running loopback services; not a benchmark. |
_harness.py |
Test-only shared loading, oracle and manifest helpers. |
_diagnostics.py |
Pinned diagnostic schemas/cases fixture (not a benchmark). |
Recorded validation¶
RESULTS.md contains the complete L40S matrix, correctness checks, public worker results and measured latency/memory trade-offs. The result bundle preserves all raw samples, reports, source hashes, required-check logs and the original environment freeze.
From the repository root, run the CPU tests with the same environment:
python -m pytest -q tests/lfm2