Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
0ae65c8
bump-hrx: say when AMD rebased its line, instead of calling AMD's com…
bong-water-water-bong Nov 10, 2026
abd2cb1
bump llama.cpp to a5dc597b8: one-token decode at any head size, and a…
bong-water-water-bong Nov 10, 2026
a8e7745
npu/mtp: the distribution-following acceptance rule, as a tested head…
bong-water-water-bong Nov 10, 2026
a0f17ac
tools: dflash_draft_info, the gate before a DFlash draft loader: the …
bong-water-water-bong Nov 10, 2026
74f6db6
npu-pack: read the 1.9 block-32 head from the .fconst tail (Fixes #17…
bong-water-water-bong Nov 10, 2026
0c852e8
npu: the integer host arm packs an unaligned geometry row-major, so i…
bong-water-water-bong Nov 10, 2026
bc67179
npu-pack: refuse a one-header vote on a pack that declares per-head q…
bong-water-water-bong Nov 10, 2026
f7ecac4
serve: refuse --npu-opt on a model-generic container, where the per-o…
bong-water-water-bong Nov 10, 2026
f1ad35e
bump llama.cpp to fa533bd21: a large expert activation no longer ends…
bong-water-water-bong Nov 10, 2026
e6f93ac
build: every cmake/tests-*.cmake is included by a glob, so a new test…
bong-water-water-bong Nov 10, 2026
2f8dc3b
npu: a Q4NX cache hit counts as a use, and the cache bound cannot wra…
bong-water-water-bong Nov 10, 2026
701c6dd
npu: NPU_INFER_DUMP_OPS prints one magnitude line per projection and …
bong-water-water-bong Nov 10, 2026
c6ef5b1
npu-pack: read a FastFlowLM NPU2 model source's container, report its…
bong-water-water-bong Nov 10, 2026
2d6c039
npu-pack: sweep stale fconst scratch in every candidate directory, ne…
bong-water-water-bong Nov 10, 2026
0f305ed
bump llama.cpp to 50d28600a: Qwen3-VL's rotation runs on the device (…
bong-water-water-bong Nov 10, 2026
6925067
npu-pack: say what a rule row's `prefix` actually is (a twin-diff byt…
bong-water-water-bong Nov 10, 2026
b0f550d
npu: a launch's result is dequantised with one multiply per element, …
bong-water-water-bong Nov 10, 2026
003aad1
tools: hf_tensor.py reads one safetensors tensor with the offset rule…
bong-water-water-bong Nov 10, 2026
7c94d5a
npu-pack: import a 1.9 pack whose every MatMulNBits is block 32 (engi…
bong-water-water-bong Nov 10, 2026
b91119b
npu-pack: Qwen3-8B 1.9 pack imports end to end (hidden 4096 / 32 head…
bong-water-water-bong Nov 10, 2026
7e4f7a9
docs: a 1.9 pack's ONNX components are vendor-EP graphs, and ort_run …
bong-water-water-bong Nov 10, 2026
89a7ca2
npu: the attention rows of a prefill block share one thread team (Ref…
bong-water-water-bong Nov 10, 2026
5cb49f4
forward-serve: --prefix-pin, the pinned shared prefix on the device p…
bong-water-water-bong Nov 10, 2026
223d8e2
npu-pack: the container writer refuses a 4-bit code outside 0..15 too…
bong-water-water-bong Nov 10, 2026
8e9e871
docs(npu): the head-image cache is swept with the Q4NX cache, as one …
bong-water-water-bong Nov 10, 2026
233fcad
npu-pack: a qwen35 GGUF is packed by an in-tree converter, and the GG…
bong-water-water-bong Nov 10, 2026
7a6d168
serve: --flash, the single-turn NPU profile; --ctx-size reaches the f…
bong-water-water-bong Nov 10, 2026
468d029
npu-pack test: the q4nx-container arm removes its fixture directory (…
bong-water-water-bong Nov 10, 2026
8f1eddd
npu: NPU_INFER_DUMP_OPS says so once at model load, so a stale binary…
bong-water-water-bong Nov 10, 2026
0a46707
npu: execute on an imported dma-buf -- the engine's arm, and the clai…
bong-water-water-bong Nov 10, 2026
5a693b2
tests: scratch goes to ~/.cache/scratch-tests, not /tmp (tmpfs, and s…
bong-water-water-bong Nov 10, 2026
153b7b6
bump llama.cpp to 2293f95ea: prefill attention keeps its rate at long…
bong-water-water-bong Nov 10, 2026
391ebd1
tools/xhci_boot_capture: the fallback outdir is the cache, never /tmp…
bong-water-water-bong Nov 10, 2026
d8a7c4c
npu: read a pack's vision component (vision_weight.q4nx): every tenso…
bong-water-water-bong Nov 10, 2026
a4fca85
tests: batch_check_state runs in CI, on the runtime stub and a fabric…
bong-water-water-bong Nov 10, 2026
e35ee7f
npu: bound every token pick by the tokenizer, not only the grammar's …
bong-water-water-bong Nov 10, 2026
fcc50c1
amdxdna: a kernel-log report of what TDR recovery did, and a test-onl…
bong-water-water-bong Nov 10, 2026
69f917e
tools: hrx_gtt_split.py -- re-measure #856's duplicate from a clean c…
bong-water-water-bong Nov 10, 2026
e6d42ee
forward-serve: a streamed reply is sent as it is generated, not after…
bong-water-water-bong Nov 10, 2026
3151a82
npu: the decode-family block gets its own method, body unchanged (eng…
bong-water-water-bong Nov 10, 2026
de0bf51
gitignore: npu_pack_import_test's q4nx-container fixtures are run out…
bong-water-water-bong Nov 10, 2026
dd585f2
npu/plan: a fifth OpId cannot arrive without a registry entry (engine…
bong-water-water-bong Nov 10, 2026
a2f180d
npu-pack: the container genre's geometry, as a pure function with its…
bong-water-water-bong Nov 10, 2026
aca7437
docs/axb35: the rear-USB-A and Type-C halves of engine#823 need diffe…
bong-water-water-bong Nov 10, 2026
0073fcd
xhci capture: record WHAT is on each connected port, so a plugged dri…
bong-water-water-bong Nov 10, 2026
0db2358
npu: the MiniCPM-V 4.7 vision tower on the host, float32, matching th…
bong-water-water-bong Nov 11, 2026
1af5725
npu: the DFlash draft block's acceptance arithmetic, with the measure…
bong-water-water-bong Nov 11, 2026
72ba575
npu: load a DFlash draft package as a model source: config + containe…
bong-water-water-bong Nov 11, 2026
d4dc5e7
bump-hrx: the failure report carries the failing step and the conflic…
bong-water-water-bong Nov 11, 2026
f9dcbc4
npu-pack: the 1.9 canonical copy is written under a temporary name an…
bong-water-water-bong Nov 11, 2026
b2f17e2
npu: a sealed set can carry a wide prefill family, named by the pack …
bong-water-water-bong Nov 11, 2026
e731df9
npu-pack: the shape rule for a 1.8 external-data initializer, pinned …
bong-water-water-bong Nov 11, 2026
fa86a09
forward-serve: --prefix-pin pins the end of the system turn on the fi…
bong-water-water-bong Nov 11, 2026
8079720
ci: the runner health check reads the restart policy, not just whethe…
bong-water-water-bong Nov 11, 2026
0c64350
serve: let the operator choose the load mode, so #1668's prefill leve…
bong-water-water-bong Nov 11, 2026
0dbb593
tests/census_watch: derive "reviewed classes are documented" from sig…
bong-water-water-bong Nov 11, 2026
59d342d
ci: a comment cancels the PR's pr-agent review, because both enter on…
bong-water-water-bong Nov 11, 2026
f308fa9
npu-pack: plan() fills a container's geometry and names the source th…
bong-water-water-bong Nov 11, 2026
1788487
tools: hrx_dispatch_profile.py, time per HRX dispatch from the backen…
bong-water-water-bong Nov 11, 2026
540e1fd
npu: a MiniCPM-V 4.7 image from bytes to the tower's input: plan, tor…
bong-water-water-bong Nov 11, 2026
3725f12
forward-serve: the tokenizer's eos_token and the ChatML turn end are …
bong-water-water-bong Nov 11, 2026
c4826e6
tools: hrx_dispatch_profile.py names unfinished dispatches, and its s…
bong-water-water-bong Nov 11, 2026
0b88efa
weekly: the runner preflight names a restart-policy failure as one (r…
bong-water-water-bong Nov 11, 2026
75a8924
npu: the text side of a MiniCPM-V 4.7 image: its tokens and the canva…
bong-water-water-bong Nov 11, 2026
d113064
tests: the sweep covers tests/*_test.py too, as a set baseline (#2018…
bong-water-water-bong Nov 11, 2026
6e28f10
bump llama.cpp to 7a1f2b81c: the prefill kernels stay finite above th…
bong-water-water-bong Nov 11, 2026
fcd8412
npu: a sealed set can carry a narrow decode family: 3.26-3.33 -> 3.69…
bong-water-water-bong Nov 11, 2026
61ed559
tests: register moe_expert_chunks_accum_test and shrink the unreferen…
bong-water-water-bong Nov 11, 2026
24479ae
npu-pack test: the pack leg's head tile count follows the stored rows…
bong-water-water-bong Nov 11, 2026
50550b2
forward-serve: an answer that starts inside a reasoning block the pro…
bong-water-water-bong Nov 11, 2026
5acb6e7
bump-hrx: a mirror hold, so a rewrite of AMD's line that needs a hand…
bong-water-water-bong Nov 11, 2026
0f68f1e
weekly: the runner preflight warns on a short restart policy and bloc…
bong-water-water-bong Nov 11, 2026
697db7d
npu-pack: say what main does with a graph head (stored as is; an unal…
bong-water-water-bong Nov 11, 2026
aa11d51
ci: a skipped test is a test that did not run, and now something says…
bong-water-water-bong Nov 11, 2026
cad81fd
tools: fit the routing head over --dump-encoder output, so before/aft…
bong-water-water-bong Nov 11, 2026
875272b
npu-pack: a tied head must correlate AND not be mis-scaled, or it imp…
bong-water-water-bong Nov 11, 2026
afb4640
forward-serve: a text request to a decision-only pack is a 400 that n…
bong-water-water-bong Nov 11, 2026
a97e398
tools: npu_serve_smoke, the release gate's check that npu serve is on…
bong-water-water-bong Nov 11, 2026
b96a8bc
forward-serve: whitespace around a reasoning block is layout, not tex…
bong-water-water-bong Nov 11, 2026
0140a02
Merge main into #1981: the five python rows main added since move int…
Nov 11, 2026
7a62de2
tools: logits_kld, the comparator #803's KLD gate needs before anyone…
bong-water-water-bong Nov 11, 2026
30123f7
npu-plan: plan.json can name the wide and narrow sealed families (pre…
bong-water-water-bong Nov 11, 2026
c9b38d4
npu: the dense-tail cache is on by default from a 128 MiB tail, the s…
bong-water-water-bong Nov 11, 2026
077f38d
laya: tools/laya_finetune, the routing head's CPU fine-tune over --du…
bong-water-water-bong Nov 11, 2026
a5e8415
npu serve: say it at ready when no 16-row decode kernel was found (re…
bong-water-water-bong Nov 11, 2026
ecaba9f
hw-gate: an npuserve phase, so the gate can fail a serve that is on t…
bong-water-water-bong Nov 11, 2026
6f68403
bump llama.cpp to 1703bfeaa: a gated-delta-net layer behind a type ca…
bong-water-water-bong Nov 11, 2026
5ce405a
npu: minicpm_image_probe, one picture and one question through a Mini…
bong-water-water-bong Nov 11, 2026
539a77f
npu serve: usage says how many completion tokens were reasoning (Refs…
bong-water-water-bong Nov 11, 2026
e5207bb
ci-runner-health: --runs, so a cancelled check is a number and not a …
bong-water-water-bong Nov 11, 2026
b671985
tools: npu_serve_smoke prints the box's load, and names a busy box wh…
bong-water-water-bong Nov 11, 2026
0d50eaa
bump-hrx: hold the mirror at 44ee5710d19f for the release (engine#194…
bong-water-water-bong Nov 11, 2026
544771f
ci: the engine#1962 narrowing enumeration is a ratchet, so it cannot …
bong-water-water-bong Nov 11, 2026
514bd83
npu: dumps are created 0600 and the head-image cache 0644, never thro…
bong-water-water-bong Nov 11, 2026
afb6179
forward-serve: the turn end is read off the container's own template …
bong-water-water-bong Nov 11, 2026
130f541
tools/ci: every submodule must sit at the commit the tree pins (engin…
bong-water-water-bong Nov 11, 2026
cfbcaef
npu: the host vision tower uses the machine's threads and a blocked p…
bong-water-water-bong Nov 11, 2026
3a1a22a
docs/npu: FastFlowLM's own containers served as published, the measur…
bong-water-water-bong Nov 11, 2026
ca4b5f6
tools/ci: a /tmp literal is RAM on this box, so the ratchet stops new…
bong-water-water-bong Nov 11, 2026
d5dc461
Merge main (ca4b5f65) into #2126: logits_kld_self and the hrx_f16_nar…
Nov 11, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion .github/workflows/pr-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ jobs:
runs-on: [self-hosted, pr-agent]
# on the job, not the workflow: a comment that skips the job (a bot's, say) must not cancel a review
concurrency:
group: pr-agent-${{ github.event.pull_request.number || github.event.issue.number }}
# A comment and a push are different jobs on one serialised model runner: an `/ask` must not cancel the review of
# the PR it is asked about, and a `synchronize` must cancel the review of the head it replaced. Grouping by event
# name keeps push-cancels-push (right: a newer head makes the older review moot) without letting a busy thread
# starve it. Measured 2026-11-10: PR #2007's review ran 19:21:26 -> cancelled 19:30:31, and `issue_comment:
# [created]` puts every comment on the PR into this same group with `cancel-in-progress: true`.
group: pr-agent-${{ github.event.pull_request.number || github.event.issue.number }}-${{ github.event_name }}
cancel-in-progress: true
timeout-minutes: 30
# same-repo PRs, and comments by the owner, members or collaborators on a pull request
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,8 @@ pin-report.json
# probe - a red that is the tree's, not the repository's (engine#1564). Ignoring them also stops run output being
# committed by accident.
.hrx123/logs/

# npu_pack_import_test's container-genre fixtures (test_q4nx_container): written to <golden dir>/q4nx-container, and to
# ./q4nx-container when the test runs with no golden dir -- so a run from the source root left them where `git add -A`
# picks them up, and #1986 committed five of them (820 KB) and reddened the copyright gate. Run output, not source.
q4nx-container/
322 changes: 277 additions & 45 deletions CMakeLists.txt

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,17 @@ an RFC or issue.
hands the script to the interpreter instead of relying on the file's exec bit; `tests/python_test_order_check.py
--check` enforces that it follows `find_package(Python3)`. Two rows may not share a name: CMake stops at configure with
`add_test given test NAME "…" which already exists in this directory`.
- **A new test's registration goes in its own file.** Put the `add_test(...)` (or `add_python_test(...)`) row, its
comment, and its `set_tests_properties` in a new `cmake/tests-<name>.cmake` with the Apache notice, and do not touch
`CMakeLists.txt` for it: every `cmake/tests-*.cmake` is included by a glob near the end of `CMakeLists.txt`, after all
targets exist. A new file cannot conflict with another pull request, and a row appended to `CMakeLists.txt` does —
that file changed 37 times in one day and three open pull requests had a registration hunk that no longer applied
(engine [#1979](https://github.com/1bit-MONSTER/engine/issues/1979)). A row that belongs to an existing family goes in
that family's file (`cmake/tests-gpu-hold.cmake`). An `add_executable` for a test binary still goes in
`CMakeLists.txt` next to the sources it builds; only the registration moves. Existing rows stay where they are.
A registration file that needs a fixed position may still be `include()`d by name from `CMakeLists.txt`; the glob
then leaves it alone. `tests/registration_glob_test.sh` (itself registered from `cmake/tests-registration-glob.cmake`)
runs the glob block and holds both rules.
- **Pins only move forward.** CI fails a pull request whose submodule pin is behind, or has
diverged from, the pin on `main` (`tools/check_pins.py`), because that drops commits the engine
already shipped. A rollback on purpose carries the `pin rollback` label.
Expand Down
299 changes: 273 additions & 26 deletions app/forward_serve.cpp

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions app/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ int onebit_ort_run_main(int argc, char** argv);
#endif
#ifdef ONEBIT_NPU_HOST
#include "pack_import.h" // 1bit npu-pack: an OGA NPU pack -> a Q4NX model directory
#include "vision_pack.h" // 1bit npu-pack vision: a pack's vision component (engine#1788)
#endif
#ifdef ONEBIT_NPU
#include "forward_serve.h"
Expand Down Expand Up @@ -94,6 +95,7 @@ void usage(FILE* out) {
#endif
#ifdef ONEBIT_NPU_HOST
" npu-pack <info|import> <pack dir> import a Ryzen AI OGA NPU pack (npu-pack --help)\n"
" npu-pack vision <pack dir> read a pack's vision component (vision_weight.q4nx)\n"
#endif
" comfy <workflow.json> run a ComfyUI workflow with ComfyUI.cpp (docs/comfyui.md)\n"
#ifdef ONEBIT_LAYA
Expand Down Expand Up @@ -323,6 +325,18 @@ int main(int argc, char** argv) {
#ifdef ONEBIT_NPU_HOST
if (cmd == "npu-pack") {
try {
// `npu-pack vision <pack dir>`: the vision component of a two-component pack (engine#1788), read by
// npu/vision_pack.h rather than the OGA importer, which knows nothing of it.
if (argc >= 3 && std::string(argv[2]) == "vision") {
if (argc != 4 || argv[3][0] == '-') {
std::fprintf(stderr, "usage: 1bit npu-pack vision <pack dir>\n"
" reads the pack's vision component (config.json's vision_model_weight, default\n"
" vision_weight.q4nx): every tensor placed, the tower's geometry from the tensors'\n"
" own shapes, and each config.json field the tensors overrule\n");
return argc == 4 ? 0 : 2;
}
return onebit::npu::vision::print_info(argv[3]);
}
return onebit::npu::pack_import::run(argc - 2, argv + 2);
} catch (const std::exception& e) {
std::fprintf(stderr, "1bit npu-pack: %s\n", e.what());
Expand Down
108 changes: 100 additions & 8 deletions app/npu_answer_parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,11 @@ inline AnswerFamily answer_family_of_template(const std::string& template_text)
// (is_reasoning, text) pieces; finish() flushes what the holdback still held and reports the calls.
class AnswerStream {
public:
AnswerStream(AnswerFamily family, bool tools_sent)
: fam_(family), tags_(answer_detail::tags_of(family)), tools_sent_(tools_sent) {}
// `starts_in_reasoning`: the PROMPT already opened the reasoning block (prompt_opens_reasoning below), so the
// answer begins inside it and the first thing to look for is the closing tag.
AnswerStream(AnswerFamily family, bool tools_sent, bool starts_in_reasoning = false)
: fam_(family), tags_(answer_detail::tags_of(family)), tools_sent_(tools_sent),
mode_(starts_in_reasoning ? Mode::reasoning : Mode::content) {}

void feed(const std::string& chunk, std::vector<std::pair<bool, std::string>>& out) {
buf_ += chunk;
Expand All @@ -169,6 +172,9 @@ class AnswerStream {
// The calls seen so far, in order. A call needs tools_sent, per the issue's rule.
const std::vector<ToolCall>& calls() const { return calls_; }
bool saw_reasoning() const { return saw_reasoning_; }
// Inside a reasoning block right now (after the opening tag, before the closing one). reasoning_token_count()
// reads it before and after each token, so the tags themselves and held-back partial tags count with the block.
bool in_reasoning() const { return mode_ == Mode::reasoning; }
bool saw_text() const { return saw_text_; }

const std::string& reasoning() const { return reasoning_; }
Expand All @@ -177,8 +183,22 @@ class AnswerStream {
private:
enum class Mode { content, reasoning, call };

void emit(std::vector<std::pair<bool, std::string>>& out, bool is_reasoning, const std::string& text) {
if (text.empty()) return;
// Whitespace around a reasoning block is the model's layout, not part of either text: `<think>\n...\n</think>\n\n`
// then the answer. Left in, the answer to "one word" comes back as "\n\nParis", and a model that thinks nothing
// (`<think>\n\n</think>`) sends a reasoning chunk that is two newlines. So the whitespace that LEADS the reasoning
// is dropped, and so is the whitespace that leads the content when that content follows a reasoning block --
// the rule app/think_split.h has always applied on the in-process route ("whitespace around the block is
// dropped"), so the two routes return the same strings. Content with no reasoning before it is not touched.
void emit(std::vector<std::pair<bool, std::string>>& out, bool is_reasoning, const std::string& raw) {
if (raw.empty()) return;
std::string text = raw;
bool& trim = is_reasoning ? trim_reasoning_ : trim_content_;
if (trim) {
const size_t b = text.find_first_not_of(" \t\r\n");
if (b == std::string::npos) return; // all whitespace so far: nothing to send yet
text.erase(0, b);
trim = false;
}
if (is_reasoning) {
reasoning_ += text;
saw_reasoning_ = true;
Expand Down Expand Up @@ -218,6 +238,8 @@ class AnswerStream {
}
if (starts(tags_.think_close)) {
flush_plain();
// what follows a block is separated from it by layout whitespace -- unless content came first
if (mode_ == Mode::reasoning && !saw_text_) trim_content_ = true;
mode_ = Mode::content;
i += std::string(tags_.think_close).size();
continue;
Expand Down Expand Up @@ -374,14 +396,32 @@ class AnswerStream {
std::vector<ToolCall> calls_;
bool saw_reasoning_ = false;
bool saw_text_ = false;
bool trim_reasoning_ = true; // until the reasoning's first non-whitespace character
bool trim_content_ = false; // set when a reasoning block closes before any content; cleared by the first text
};

// Does the rendered prompt leave the model INSIDE a reasoning block? Some templates end their generation prompt
// with the opening tag itself -- Qwen3.5's ends `<|im_start|>assistant\n<think>\n` when thinking is on -- so the
// model never writes that tag: its answer starts with the reasoning and the first tag in it is the closing one.
// A splitter that waits to see the block opened then files the whole reasoning under `content` and drops the
// closing tag it never saw opened, so nothing in the reply marks where the answer starts (measured on FastFlowLM's
// Qwen3.5-2B-NPU2, engine#1786). True when the prompt, less trailing whitespace,
// ends with the family's opening tag. A prompt that opens and closes the block (thinking off:
// `<think>\n\n</think>\n\n`) does not end with it, and neither does one that never mentions it.
inline bool prompt_opens_reasoning(const std::string& prompt, AnswerFamily family) {
const std::string open = answer_detail::tags_of(family).think_open;
size_t end = prompt.size();
while (end > 0 && (prompt[end - 1] == ' ' || prompt[end - 1] == '\n' || prompt[end - 1] == '\r' || prompt[end - 1] == '\t')) --end;
return !open.empty() && end >= open.size() && prompt.compare(end - open.size(), open.size(), open) == 0;
}

// Which of `pieces` (one generated token's text each, in order) belong to `content` -- not to
// reasoning and not to call markup. Callers that report a per-token value use this so reasoning and
// call markup get no entry (issue #651 item 3). A token that straddles a boundary ("</think>The")
// counts as content: its text did reach the caller.
inline std::vector<char> content_mask(const std::vector<std::string>& pieces, AnswerFamily family, bool tools_sent) {
AnswerStream s(family, tools_sent);
inline std::vector<char> content_mask(const std::vector<std::string>& pieces, AnswerFamily family, bool tools_sent,
bool starts_in_reasoning = false) {
AnswerStream s(family, tools_sent, starts_in_reasoning);
std::vector<char> mask;
mask.reserve(pieces.size());
std::vector<std::pair<bool, std::string>> out;
Expand All @@ -405,9 +445,61 @@ inline std::vector<char> content_mask(const std::vector<std::string>& pieces, An
return mask;
}

// How many of `pieces` (one generated token's text each, in order) were reasoning: OpenAI's
// usage.completion_tokens_details.reasoning_tokens (engine#2111). An empty `content` with finish_reason "length" reads
// like "the model said nothing" when the whole budget went into a thinking block, and this is the number that says so.
// A token is reasoning when none of its text reached `content` and the block was open before it, after it, or it put
// text into the block: so the opening and closing tags, a partial tag the holdback keeps, and an unclosed block at the
// end of the budget all count, while call markup and the whitespace after a closed block do not. A token that
// straddles the boundary ("</think>The") is content, as content_mask() says. Same splitter, so the count, the split
// and the logprob entries cannot disagree.
inline int reasoning_token_count(const std::vector<std::string>& pieces, AnswerFamily family, bool tools_sent,
bool starts_in_reasoning = false) {
AnswerStream s(family, tools_sent, starts_in_reasoning);
std::vector<std::pair<bool, std::string>> out;
int n = 0;
bool last_counted = false;
// Tokens that produced nothing before anything was produced (leading whitespace, the first half of an opening tag
// the holdback keeps) belong to whatever comes next: to the block if the next token opens it, to nothing otherwise.
int pending = 0;
bool started = false;
for (const auto& piece : pieces) {
const bool before = s.in_reasoning();
out.clear();
s.feed(piece, out);
bool content = false, reasoning = false;
for (const auto& [is_reasoning, text] : out) {
(void)text;
(is_reasoning ? reasoning : content) = true;
}
last_counted = !content && (reasoning || before || s.in_reasoning());
if (last_counted) {
++n;
if (!started) n += pending;
started = true;
} else if (content) {
started = true;
} else if (!started) {
++pending;
}
}
// What the holdback still holds is flushed by finish(): if it is content, the last token's text reached `content`.
out.clear();
s.finish(out);
for (const auto& [is_reasoning, text] : out) {
(void)text;
if (!is_reasoning && last_counted) {
--n;
last_counted = false;
}
}
return n;
}

// The whole answer at once: the same splitter, so the two paths cannot disagree.
inline ParsedAnswer parse_answer(const std::string& text, AnswerFamily family, bool tools_sent) {
AnswerStream s(family, tools_sent);
inline ParsedAnswer parse_answer(const std::string& text, AnswerFamily family, bool tools_sent,
bool starts_in_reasoning = false) {
AnswerStream s(family, tools_sent, starts_in_reasoning);
std::vector<std::pair<bool, std::string>> pieces;
s.feed(text, pieces);
s.finish(pieces);
Expand Down
25 changes: 25 additions & 0 deletions app/npu_chatml.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,31 @@ inline std::string chatml(const nlohmann::json& messages, bool thinking = true)
return p;
}

// The messages a chat route renders to find where a request's system turn ends (--prefix-pin, engine#1785): the
// request's leading system messages, then one user message whose text no question starts with. The rendering of
// these and the rendering of the real request agree up to the first token of the user's text and differ there, so
// their common prefix is the part every question under this system prompt shares.
//
// The user message is there because a template may refuse to render without one. Measured: the Qwen3.5 template
// rendered alone on its system message throws, and the early pin was silently not made on that container.
//
// Empty array when the request does not open with a system message: there is nothing to pin early.
template <typename Json>
Json pin_probe_messages(const Json& messages) {
Json lead = Json::array();
if (!messages.is_array()) return lead;
for (const auto& m : messages) {
if (!m.is_object() || !m.contains("role") || !m["role"].is_string() || m["role"].template get<std::string>() != "system") break;
lead.push_back(m);
}
if (lead.empty()) return lead;
Json probe = Json::object();
probe["role"] = "user";
probe["content"] = "\xC2\xA4\xC2\xA4 pin probe \xC2\xA4\xC2\xA4";
lead.push_back(probe);
return lead;
}

// Whether the container's tokenizer can represent the prompt above. `chatml()` is rendered for
// every container (#832), and a tokenizer without the anchors spells `<|im_start|>` as ordinary
// pieces, so the model is prompted with text its own template never produces (#963: on
Expand Down
Loading
Loading