Skip to content

BLOCKER: no W1 cell is producible on the comparator lane in either mode — streaming reports prompt_tokens=0, non-streaming reports 534 against a 512 ± 8 band #2817

Closed as not planned

Description

@noahgift

No W1 cell can be produced on the comparator lane. Not in streaming mode, not in non-streaming mode. Both exits were taken with the real producer, and both are exit 4.

This is the campaign blocker for #2706. Arm B needs a comparator ratio; the comparator lane cannot currently produce a conformant W1 band by either route.

Both modes, verbatim

Real producer against llama-server build 7746 (39173bcac), CUDA, 29/29 layers offloaded, model qwen2.5-coder-7b-instruct-q4_k_m.gguf sha256 509287f78cb4d4cf6b3843734733b914b2c158e43e22a7f4bf5e963800894d3c:

Streaming (--stream) — exit 4:

all 74 completed requests reported prompt_tokens = 0

Non-streaming — exit 4:

64 of 64 OUTSIDE the band — 534 tokens ... the band measured under it is not W1

Streaming is the mode §4.5 requires, because itl_ms and decode_tok_s need per-token arrival times that a blocking response does not carry.

Why streaming fails: neither server reports usage in SSE

  • apr emits no usage object in its SSE stream at all, even when the request sets stream_options: {"include_usage": true}.
  • llama-server emits it only when that flag is set.
  • The flag is never sent. git grep stream_options origin/main → rc=1, zero hits.

So crates/aprender-test-lib/src/llm/client.rs:494-508 never populates final_usage, and band.rs:136 usage_counts() takes its fallback arm:

fn usage_counts(usage: Option<&Usage>, fallback_tokens: u32) -> (u32, u32) {
    usage.map_or((fallback_tokens, 0), |u| {
        (u.completion_tokens, u.prompt_tokens)
    })
}

prompt_tokens is substituted with 0 — which is what the exit-4 message reports. Completion tokens fall back to a count of non-empty SSE content deltas.

There is a third fallback in the tree that would be no better: loadtest.rs:1481 estimate_prompt_tokens still uses words × 1.3.

Why non-streaming fails: the same corpus is W1 on one lane and not-W1 on the other

Deterministic across all runs (min == median == max), n=32 llama / n=16 apr:

lane prompt_tokens inside 512 ± 8
raw corpus text, client-tokenized 505 32/32
apr usage.prompt_tokens 513 16/16
llama-server usage.prompt_tokens 534 0/32

Constant delta −21, ratio 0.9607 — 4.09% more prefill work on the comparator for identical input.

This is the uncomfortable part: prompts-w1.jsonl satisfies §4.3.1's 512 ± 8 on the apr lane and violates it on the comparator lane. "This corpus is W1" is not a property of the corpus. It is a property of the corpus and the lane, and §4.3.1 does not say so.

The spec half-anticipates this — §4.3.1 already notes that it "does not say whether the 512 is counted before or after the chat template wrapper", and requires the receipt to declare which side of the boundary its count was taken on. What it does not anticipate is that the two lanes wrap differently: llama-server applies the GGUF's embedded jinja template (which injects Qwen's default system message), apr applies its own hardcoded ChatML builder (realize_handlers.rs:200 format_chat_messages, no system message, constant 8-token wrapper). Reconstructing both with the canonical tokenizer reproduces 513 and 534 exactly. So no declaration of "which side" makes the two counts comparable — they are wrapped by different templates.

Not correctable by an offset either — shape-varied, apr vs llama: short 25 vs 43 (−18), W1 513 vs 534 (−21), special-token text 76 vs 57 (+19), natural 16 vs 34 (−18). The sign flips.

What is NOT the problem

Vocabulary. On raw text all three agree exactly — apr 14/505, llama 14/505, client 14/505 — and the GGUF-embedded vocab and the canonical tokenizer.json (sha256 c0382117ea329cdf097041132f6d735924b697924d6f6fc3945713e96ce87539) give byte-identical id sequences. This is entirely a chat-template and usage-reporting problem.

Options, none free

  1. Send stream_options: {"include_usage": true} and make apr honour it. Unblocks llama's streaming usage immediately; requires an apr serve change. Still leaves the 513-vs-534 template divergence, so it fixes the streaming exit and not the band exit.
  2. Count client-side with the model's own tokenizer — which §4.4.6 already calls "the canonical method". This is lane-independent by construction and is the only option that makes 512 ± 8 mean one thing. It needs real work: there is no tokenizers dependency in crates/aprender-test-lib/Cargo.toml and zero Tokenizer::from_file / .encode( in src/llm/ or src/perf_gate/ (both greps rc=1).
  3. Drive the comparator with a raw-completion endpoint and apply one template client-side to both lanes. Changes what Arm B is comparing.
  4. Re-target the corpus at the comparator's count. Rejected — it just moves the violation to the apr lane, and I-9 makes the spent runs unrecoverable.

Option 2 looks right and is the largest; option 1 is worth doing regardless because a client that cannot ask for usage is a defect independent of which counting scheme wins.

Do not measure a cell until this is decided. I-9 makes a mis-configured cell a spent run, and #2778 makes the same point about ordering: an assertion that has to exist before the first replicate, not after the first surprise.

Related


Sibling findings from this investigation

One tokenization/counting investigation against the perf-gate comparator lane produced five findings. Each is self-contained; together they are why no Arm B ratio is currently trustworthy.

# finding class
#2815 apr mis-tokenizes `< im_end
#2816 §4.4.6's FATAL tokenization rule has no implementation; the gate's own OK fixture carries an illegal method gate cannot fire
#2817 BLOCKER — no W1 cell is producible on the comparator lane in either mode campaign blocked
#2818 llama-server 39173bcac accepts, echoes, then ignores ignore_eos, which W1 mandates comparator defect
#2819 canonical tokenizer.json absent on gx10 and intel; only a token-identical, byte-different variant fleet distribution

Shared setup: apr 0.64.0 (745fa8588) CPU build · llama-server 7746 (39173bcac) CUDA, 29/29 layers offloaded · model qwen2.5-coder-7b-instruct-q4_k_m.gguf sha256 509287f78cb4d4cf6b3843734733b914b2c158e43e22a7f4bf5e963800894d3c · canonical tokenizer sha256 c0382117ea329cdf097041132f6d735924b697924d6f6fc3945713e96ce87539 (7,031,645 bytes). All measurements reproduced twice.

Epic: #2706.

Activity

  1. noahgift commented on Sep 5, 2026

    @noahgift
    ContributorAuthor

    Blocks PP-066 master row 18 (spec §5 Track I). Also: master row 15, master row 0b, C1.

    Named in #2873's body as a blocker → master rows 15/18: no W1 cell is producible on the comparator lane in either mode (streaming reports prompt_tokens=0 because stream_options.include_usage is never sent; non-streaming reports 534 against 512±8 because llama-server applies the GGUF jinja template and apr its own ChatML builder). C1 requires a CONFORMANT lambda W1 row with both lanes.

    PP-066 triage 2026-09-05 (issue #2873); ledger docs/audits/pp-066-triage-2026-09-05.md.

  2. added this to the 0.68.0 milestone on Sep 10, 2026
  3. added
    bugSomething isn't working
    P0Critical priority
    on Sep 11, 2026
  4. modified the milestones: 0.68.0, 0.69.0, 0.72.0 on Sep 16, 2026
  5. noahgift commented on Sep 25, 2026

    @noahgift
    ContributorAuthor

    Parked by APR-EPIC-001: over S3 budget (rank 5/142). Reopen = pull. Applied by aprender-d1 under a 3-lane quorum receipt (handoff/balance-quorum-rev4.json).

  6. modified the milestones: 0.72.0, backlog on Sep 25, 2026
  7. added
    parkedClosed over epic budget (APR-EPIC-001 rule 5). Reopen = pull.
    on Sep 25, 2026
  8. noahgift commented on Sep 25, 2026

    @noahgift
    ContributorAuthor

    Reopened by APR-EPIC-001 retro quorum: the agy lane voted keep-open (P0 on its train), and a close needs both counted lanes (cop ruling 2026-09-25, handoff/park-retro-quorum.json). Unlinked; milestone restored to its train. Applied by aprender-d1.

  9. modified the milestones: backlog, 0.72.0 on Sep 25, 2026
  10. removed
    parkedClosed over epic budget (APR-EPIC-001 rule 5). Reopen = pull.
    on Sep 25, 2026
  11. noahgift commented on Sep 26, 2026

    @noahgift
    ContributorAuthor

    #2817 receipt (aprender-60): both halves measured on real binaries

    Streaming usage: already fixed on main. apr serve emits usage in SSE whether or not stream_options.include_usage is set (PP-27, tests/stream_mode_pp27.rs), and the test-lib streaming client sends include_usage: true. In the table below, stream and block modes give the same prompt_tokens in every row.

    The remaining gap was a tokenizer defect, and it is fixed on branch fix/2817-stream-usage @ b6b8c9809. The GGUF serve tokenizer (BPETokenizer::new, the greedy path with no merges) had no special-token partition. Qwen2.5's vocabulary has .< (id 15757), so .<|im_end|>\n encoded as .< | im _end | >\n: 6 tokens where llama.cpp has 3. That put the prompt 3 tokens over for each such turn end, and no end-of-turn token reached the model. apr /tokenize on llama.cpp's own /apply-template output showed the split directly. The fix partitions on the vocabulary's <|...|> tokens before greedy matching. special_tokens is untouched, so embedding pooling does not change.

    Setup: qwen2.5-coder-1.5b-instruct q4_k_m, CPU; /v1/chat/completions with max_tokens=1 and temperature=0; W1 prompts from benchmarks/qwen-coder/prompts-w1.jsonl rows 2–9, plus one system+user case. llama-server is b11050 (60b06ab9a).

    binary W1 ×8 (block / stream) system+user (block / stream)
    apr 0.69.5-rc.1 5660b0877 (before) 537 / 537 26 / 26
    apr b6b8c9809 (fix) 534 / 534 20 / 20
    llama-server b11050 534 / 534 20 / 20
    • Guard: falsify_tok_2817_chat_marker_is_one_token_after_a_merging_byte. The aprender-serve tokenizer filter passes 221/221.
    • Mutant: with the marker set empty, the guard goes RED with the pre-fix split [a, ., <, |, im, _end, |, >Ċ, …].
    • Out of scope, and unchanged: the words×1.3 estimate at crates/aprender-test-cli/src/handlers/llm.rs:1528.

    No PR, because of the open-PR cap. This is a fold candidate for stage/0.70.1.

  12. added
    P2Medium priority
    and removed
    P0Critical priority
    on Oct 14, 2026
  13. noahgift commented on Oct 14, 2026

    @noahgift
    ContributorAuthor

    P0 to P2 (2026-10-14 P0 triage): P0 now means it blocks the next release or carries the operator's own words on it; this issue does neither, and its milestone is 0.72.0.

  14. noahgift commented on Oct 14, 2026

    @noahgift
    ContributorAuthor

    merged into #5317 (master ticket for epic #4381; triage rule 7b, 2026-10-14). It is a checklist row there; reopen this issue when work on it starts or its PR opens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium prioritybugSomething isn't workingowner:la-0.71Owned by the 0.71 look-ahead slotsize:MK̂ 3-4 h

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions