Pull requests / #3643

#3643 [Engine] Jev System One decision endpoint (/v1/systemone): typed decisions with calibrated probabilities

open · @justindressler · 0 コメント · GitHub で見る

BenchmarksSetup & installServer & APIAMD / HIPNVIDIA / CUDAModels & quantsDocumentationWindowsLinux

本文

## Problem and result

Strata can answer questions, but it cannot *decide*. A typed decision — "is this prompt a threat?", "which of these three actions?", "how urgent, 0 to 4?" — had no surface of its own: the caller had to generate prose and parse it, which is slow, uncalibrated and impossible to automate against.

This PR adds an opt-in decision endpoint, `POST /v1/systemone`, plus the same readout folded into the chat surface. A question is answered from **one decode step**: the label logits are read off the final row of the last verify window over a new `BROW` engine wire line, and the ported jev readout math turns that row into a typed answer with a calibrated probability. Nothing is generated to produce it.

Before: no typed decision surface at all. After, measured on the RTX 3090 (QFN, `Qwen3.8-Flash-Next-GSQ-RCO-IQ3_XXS`, engine built from this revision): a state carrying three questions of different types — `noul`, `choice`, `score` — answers each from its own readout, every answer carries its own calibrator id (`platt_v1`, `T_choice_v1`, `T_score_v1`), a `margin` (top1−top2, the cascade-routing number) and `usage.readout_tokens: 3`.

**It is opt-in, and the default path is untouched.** `--jev-brow` is off by default and the engine refuses `BROW` without it (`ERR BROW`, fail-closed, the session stays in step). `STRATA_JEV_MODES` unset means `chat` only: a process that never mentions jev behaves exactly like stock Strata, and the decision surfaces answer `404 not_found_error` rather than advertising themselves.

## Related discussion

- **Supersedes #2639**, the same change on the pre-0.2.0 base. A maintainer closed that one on 2026-10-19 in the 0.2.0-beta tracker reset, with *"Closed here does not mean rejected"* and a four-step recipe: rebase on `main`, re-run on 0.2.0-beta, area-tagged title, template filled in, then `/reopen`. This revision is that rebase and that validation; the `/reopen` comment follows.
- TwoAnts (2026-10-15) asked about using jev to check whether a stop token is where it should be. That is why the readout is one decode step over the final row: it is cheap enough to run on every answer as a check, not just as a classifier.
- No open issue covers this feature. The design, the API contract and the benchmark detail live in `docs/JEV-SYSTEMONE.md` and `docs/JEV-PR.md`, both in this PR.

## Approach

Ported from jevcpp's `src/server.cpp` onto Strata's serve layer, wire-compatible with the donor (request/response shapes, the `422` with a `field` error contract, the calibration math).

- **Engine side (opt-in):** one flag, `--jev-brow`, and one line protocol on stdin/stdout. `BROW <tok> <tok> ...` answers the final row of the last accepted verify window, gathered to the ids asked: `BROWD <n> <tok>:<logit> ...`. It fails closed — flag off, token out of range, or no window yet all answer `ERR` and leave the engine in step. `Verifier::brow_row()` guards the null, empty-range, zombie-window and `cudaMemcpy`-failure paths separately, each with its own error string and `return false`; no path returns a partial or invented row.
- **Serve side:** `/v1/systemone` (text, image and decide-then-generate) and the `jev` extension on `/v1/chat/completions`, both gated by `STRATA_JEV_MODES`. Calibration is post-hoc, monotone and argmax-preserving: it reshapes probabilities and can never pick a different label.
- **Compatibility:** no existing request, engine flag or config gains a cost. The decision path is reachable only with the flag, the mode and a request that asks for it.

The rebase onto `011c9e60` replayed all 42 commits with **zero conflicts**. To show the feature code itself did not change in the move, the 38 added files are byte-identical to the pre-rebase head, and for the 5 modified files the set of added lines is identical (CMakeLists 5, verify.hpp 8, server.py 128, verify.cpp 32, generate.cpp 36).

## Validation

Revision `140f0a93`, rebased onto `011c9e60`. Build host: WSL2, GNU 14.2.0, nvcc 13.0.88 (`STRATA_ENABLE_CUDA=ON`, `CMAKE_CUDA_ARCHITECTURES=86`, `-j1`). GPU: RTX 3090 (24 GiB), driver as shipped in the runtime container.

**Passed:**

- **CUDA build** — `cmake --build build-cuda -j1` → exit 0, binary 77,075,904 bytes, 22 warnings. Two of them are in files this PR touches and both are pre-existing lines, not added ones: `verify.cpp` unused `f` (only used in the `_WIN32` branch) and `generate.cpp` unused `w` (the batch timing line). 0 warnings introduced.
- **ctest, 156 tests configured** (`-DSTRATA_BUILD_TESTS=ON`): 82 passed. `verify_brow_test` — the C++ half of this feature, CUDA builds only — prints `verify_brow_test: OK (fail-closed pre-run contract)`.
- **The same GPU tests, re-run where a driver exists** (the build container cannot create a CUDA context; every one of its 71 GPU failures is the single line `CUDA driver version is insufficient for CUDA runtime version`). Inside the runtime container: `verify_parity` PASS, `verify_batch_parity` PASS, `dequant_s2_parity` bit-exact against the scalar dequantizer, `mmvq_multi_parity` ok, `kv_q8_parity` OK (worst INT8-vs-FP16 error 0.590 quantization steps), `sampler_parity` 0 failures, `s2_gemv_parity` agrees with the scalar decode, `qsa_topk_parity --selftest` PASS.
- **Terminal and Control suites** (`pytest tests`): 1,639 passed, 16 failed, 48 skipped, 234 subtests passed in 552.75 s. All 16 failures are pre-existing and none touches this PR's files: each failing file was re-run on a clean `011c9e60` tree, where the same tests fail (`test_shared_models_folder` 9, `test_discovery` 2, `test_ready`, `test_procs`, `test_private_files`, `test_no_acquisition`, `test_launch_tui` — the last two give identical 1-failure results on both trees).
- **Installer tests** (`tools/test_setup_*.py`, run as scripts as AGENTS.md documents): 46 of 48 exit 0; `test_setup_golden.py` (48 failing assertions) and `test_setup_unsloth.py` (4) fail identically on the clean `011c9e60` tree.
- **The 71 GPU-class ctest tests, re-run where a driver exists** — 70 pass. The one exception is `prefill_mmq_kquant_test`, which needs `libstrata_kq.so` beside the engine; that library is not produced by this CMake configuration, so the test reports `Q4_K gate/up-0: not covered by this build` and the experts take the FP16 prompt path.
- **Decision test suite** — 73 tests, all OK: `test_systemone` 13, `test_jev_calib` 26, `test_jev_then` 9, `test_jev_image` 9, `test_jev_vmode` 6, `test_jev_golden` 5, `test_jev_metamorphic` 3, `test_jev_concurrency` 2.
- **Serve suite** — `python3 -m unittest discover -s serve -p 'test_*.py'`: Ran 1002 tests in 328.664s, 1 failure, 14 skipped. The single failure is
  `test_lifeline.Lifeline.test_the_portable_watcher_ends_the_child_when_the_server_is_killed` —
  `AssertionError: False is not true : the child outlived a killed server (watcher)`. On CI the same
  suite can also fail on `test_server_close_ends_the_sampler` (`AssertionError: 1 != 2`) — it did on
  the first run of this branch. Both are pre-existing defects in `serve/`, not in this PR: the first is
  `serve/lifeline.py` building a child's identity from a `ps` state column that changes while the
  process runs, the second a count-based assertion plus a sampler loop that dies on its first failed
  sample. Both are fixed in **#3590**, which is not part of `011c9e60` yet. With #3590 in the base this
  suite is green; the 14 skips are environmental by their own reason strings.
- **Live decision readout on the GPU** — the engine started with `--serve --jev-brow --gpu 1`, one ordinary request to create a window, then the wire line:

  ```
  DONE 1 8 154.2 18.2 stop 0 0 0 220 395 175 0 0.0 8 0 chunk=0 experts_streamed=0
  BROWD 4 220:9.76547 13:5.95794 271:8.23788 198:11.6879
  ```

  Real final-row logits for the four ids asked, from the CUDA build, on the 3090. A pre-rebase build of the same commits on the same card returned the same four values, so the rebase moved nothing in this path.

- **Live decision over HTTP on the GPU** — the whole stack, not just the engine: `python3 -m serve.server --engine strata --config … --port 18311` with `STRATA_JEV_MODES=chat,decision` and the engine started with `--jev-brow --gpu 1` on the RTX 3090. `POST /v1/systemone` with one state and three questions of different types returned `200`: `noul` with `calib: platt_v1` and margin 0.7980, `choice` with `calib: T_choice_v1` and margin 0.4725, `score` with `calib: T_score_v1` and margin 0.3066, `usage: {prompt_tokens: 299, readout_tokens: 3}` — three typed, calibrated answers and three readout tokens. The inline `jev` extension on `/v1/chat/completions` answered the same way (`T_choice_v1`, margin 0.9861, one readout token) alongside a normal chat completion.

**Failed:**

- **One malformed request, on purpose, which is the error contract working:** a `score` question written with `scale` instead of `criteria` returned `422` with `{"error": {"field": "questions.urgency.criteria", "message": "must be an array of 2..10 levels"}}` — a field-level refusal, not a 500 and not a silently wrong answer.
- None observed beyond that in any suite that ran to completion on this revision. The 74 remaining ctest failures on the build host are environmental and named: 71 the missing-driver line above, `platform_memory_test` (`mlock` refused under the container's `ulimit -l`), `ple_parity` (the Q2_0 PLE fixture is not in this tree), `ggml_tq_overlay` (needs `STRATA_GGML_DIR` pointed at the agention llama.cpp fork for the trellis types).

**Not run:**

- **HIP, SYCL, Metal, Windows and macOS builds** — no ROCm, SYCL runtime or Metal toolchain here. The engine change is guarded by the same `STRATA_HAVE_CUDA_BUILD`/transport seam the rest of the file uses, but I have not compiled it for those backends and do not claim they build.
- **The live golden battery with pinned probabilities** on this revision. What ran is the invariant battery (type correctness, probability sums, bounds, determinism on repeat) against a mock engine; the pinned per-model numbers in §4 of `docs/JEV-PR.md` were measured before the rebase on the same model and the same code, and I am not presenting them as re-measured today.
- **The app's UI tests** (`npm test` in `apps/host`) — this checkout has no `node_modules` and no build of the web UI.
- **A long-generation draft-acceptance measurement.** The MTP draft layer loads (814 MiB resident, 106,299-token draft head), but the two-token generation used for the readout proposed no drafts (`drafts accepted 0 of 0`), so this revision says nothing either way about draft acceptance.

## Limits

- **The BROW row is not bit-identical across cards.** The same build, same prompt and same model on the RTX 4080 returned `220:9.72173 13:6.12896 271:8.52014 198:11.5419`, about 0.4 % away from the 3090's row. Both runs are internally consistent and the difference is small enough that no label flipped, but I have not established whether it is card-dependent or run-to-run, so nothing in this PR should be read as a cross-GPU reproducibility guarantee.
- **`calib/` is resolved relative to the server's working directory** (`STRATA_JEV_CALIB_DIR` overrides it). A first run with no matching per-tuple file answers `calib: identity` — correct, but easy to mistake for a bug; the answers above are calibrated because `calib-D/L/S.json` sit beside the server.
- **The calibration files ship fitted on QFN IQ3_XXS.** Another model needs its own fit (`calib/README.md`); a mismatched file is refused at load rather than silently applied.
- **Image decisions need the vision path** and are covered by tests against a mock encoder; a live image decision was not part of this validation.
- **Windows and macOS keep the fail-closed paths** for `brow_row` but were not executed here.
- The `calib/`, `reports/` and `docs/bench-artifacts/` trees in this PR are the measurement artifacts behind §4 of `docs/JEV-PR.md`; they are data, not code, and are not regenerated by any build.

関連リンク

インストール・モデル・リリースへのサイト内リンク。