fix(run): --stream sent nothing, --trace-output recorded nothing, and a typo'd --backend ran the default backend - #2427
Closed
noahgift wants to merge 1 commit into
Closed
Conversation
… a typo'd --backend ran the default backend Six defects found by dogfooding apr 0.63.0 from crates.io (audit epic #2373), all of them things the CLI reported as working while doing nothing. --stream emitted one NDJSON event per token whose text field was always the empty string. The ids were right — the terminal final event decoded them into "I'm here to help! How can" — so a consumer rendering text as events arrive saw nothing at all until the run finished, which is the entire point of the flag. Nothing ever decoded the ids one at a time. run_entry.rs now fills each event from the model's own tokenizer (single-token decode, the same thing the SSE streaming handler does), resolved once per streamed run and only when --stream asked for it. --trace-output FILE wrote "events": [] at every --trace-level, because the document was built by format! and ended in that literal. The same format! also interpolated the model path raw into a JSON string, so a path containing a quote produced a file that is not JSON. inference_result.rs now builds the document with serde_json and fills events from the timings the run measured (model_load from load_ms, generate from inference_ms) — nothing synthesised. Separately, --trace-level chrome ignored --trace-output entirely: it wrote trace-<epoch>.json into the CWD while the path the user named kept the summary stub, so a scripted consumer silently read the wrong file. print_chrome_trace now honours the path. --trace-level layer printed a table headed Time whose values were wall_ms / tokens * <fixed share> — the same 85/8/2/1.7 split for every model and every prompt, which is why TOKENIZE, EMBED and DECODE agreed to the hundredth of a millisecond in every run. It "proved" TRANSFORMER was 85% while the real [BRICK-PROFILE] block six lines above reported FFN 42% / Qkv 21% / LmHead 4.5% for the identical run, and printed 1.0 tok/s against the profiler's 19.2 tok/s with neither figure labelled. The values stay (they are the only estimate available without the brick profiler) but the table now says ESTIMATED, marks each value ~, prints the share it assumed, and labels TOTAL as wall clock including model load and RATE as end-to-end. --backend, --trace-level and -f accepted any string. --backend banana printed "Backend override: banana" and quietly ran the default backend — the exact outcome the --backend cuda guard exists to prevent, since it makes any throughput measured through the run meaningless. All three now carry clap value_parsers, on run and on chat. Every apr run --trace printed [CONTRACT WARN] gpu-decode-profiling-v1 TOKEN_ACCOUNTING: LmHead.count=10 != tokens_processed=11 and the gap was always exactly 1 (10/11, 11/12, 12/13, 16/17, 37/38). That is the generation loop, not a defect: generate_with_cache samples the final token and breaks at tokens.len() >= max_seq_len before feeding it back, so LmHead never fires for it while tokens_processed counts it. The invariant is now "one LmHead per forward pass" — tokens_processed or tokens_processed - 1 — so real miscounting still warns and healthy runs do not. Two error strings said things that were not true. An empty lm_head on qwen2.5-coder-0.5b-instruct.apr, a dense tied-embedding model with no experts at all, was reported as "likely a MoE per-expert tensor"; the message now names both known causes and says to run apr tensors to find the 0-byte tensor. apr chat's missing-tokenizer error printed the literal {stem}.tokenizer.json — an unsubstituted format placeholder inside a plain string — instead of the filename it looked for; it now prints concrete paths. Two existing tests were encoding defects and were fixed: run_tests_stream_output asserted v["text"].is_string(), which is true of "" and so passed for the whole life of the empty-text bug, and run_tests_chrome_trace asserted against a hand-maintained copy of print_chrome_trace's body kept in the test file, which could only prove the copy agreed with itself. Mutation-verified: each fix reverted with the tests kept turns its falsifier RED (8 tests across the two crates), restored turns them GREEN. Refs #2378 (partial), Audit epic: #2373 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
noahgift
enabled auto-merge
August 10, 2026 08:33
noahgift
marked this pull request as draft
August 10, 2026 08:36
auto-merge was automatically disabled
August 10, 2026 08:36
Pull request was converted to draft
noahgift
marked this pull request as ready for review
August 10, 2026 17:15
noahgift
enabled auto-merge
August 10, 2026 17:15
Contributor
Author
|
Parking to let the merge queue drain — branch untouched, this will be reopened. Six PRs are in the merge queue and their The queue is the only path by which anything actually merges, so it gets the runners until it is empty. Reopening immediately afterwards. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Six of the ten
apr runfindings in #2378, each reproduced against the installed0.63.0 binary first and verified against a binary built from this branch.
What a user saw
apr run model.gguf "hi" -n 8 --no-gpu --streamon 0.63.0:Eight events, every one of them empty. The ids were right — the final event
decodes them into the whole reply — so a consumer rendering
textas eventsarrive saw nothing until the run had already finished. Same branch, same command:
--trace-output FILEwrote"events": []at every documented--trace-level,and
--trace-level chromewrote its chrome JSON totrace-<epoch>.jsonin theCWD while leaving the requested path holding the empty stub. Before / after,
same five levels:
--backend bananaprintedBackend override: bananaand ran the defaultbackend at exit 0 — the precise outcome the
--backend cudaguard exists toprevent, because it makes any throughput measured through that run meaningless.
Now:
Root causes
--streamemptytextrun_entry.rswrote a literal"text": ""per event; nothing ever decoded the ids one at a time. Now filled from the model's own tokenizer (single-token decode, the same call the SSE streaming handler makes), resolved once per streamed run and only under--stream."events": []+ chrome pathinference_result.rs:337ended aformat!string with that literal. Rebuilt withserde_jsonand filled from measured timings (model_load=load_ms,generate=inference_ms). The sameformat!also interpolated the model path raw into a JSON string, so a path containing"produced non-JSON — fixed by construction.print_chrome_tracenow takes the requested path.gguf_generate_result.rs:373-380computedper_token_ms * {0.85, 0.08, 0.02, 0.017}and printed it under a column headedTime. The values stay (they are the only estimate available without the brick profiler) but the table now saysESTIMATED, marks each value~, prints the share it assumed, and labelsTOTALas wall clock incl. model load andRATEas end-to-end — the 19x contradiction with the[BRICK-PROFILE]block six lines above was two unlabelled numbers.--backend,--trace-level,-fwere free-formStrings. clapvalue_parsers onrunandchat.TOKEN_ACCOUNTINGwarningprofiler_contracts.rs:69comparedLmHead.count != tokens_processed.generate_with_cachesamples the final token and breaks attokens.len() >= max_seq_lenbefore feeding it back, so LmHead never fires for it whiletokens_processedcounts it — the gap was deterministically 1 on every healthy run (10/11, 11/12, 12/13, 16/17, 37/38). Invariant is now "one LmHead per forward pass":tokens_processedortokens_processed - 1; real miscounting still warns.matmul_fused.rs:501blamed "a MoE per-expert tensor" for an emptylm_head.weighton a dense tied-embedding Qwen2.5 with no experts at all; it now names both known causes and says to runapr tensorsto find the 0-byte tensor.chat.rs:301printed the literal{stem}.tokenizer.json— an unsubstituted format placeholder inside a plain string — instead of the filename it looked for.Two existing tests were holding defects in place
run_tests_stream_output.rsassertedv["text"].is_string(). That is true of"", so the test passed for the entire life of the empty-text bug. It nowasserts the content, and that concatenating the streamed pieces reproduces the
final text.
run_tests_chrome_trace.rsasserted against a hand-maintained copy ofprint_chrome_trace's body kept in the test file — it could only prove thecopy agreed with itself. The builder is now extracted (
build_chrome_trace)and the tests call it.
Mutation check
Each fix reverted with the tests kept, RED verbatim:
Restored: all 8 GREEN.
cargo test -p apr-cli --lib6635 passed,cargo test -p aprender-serve --lib15486 passed,cargo fmt --all -- --checkclean,
cargo clippy -p apr-cli --liband-p aprender-serve --libclean with-D warnings.Not fixed here
apr runaccepts a GGUF whoseoutput_norm.weightis half the declaredembedding_length. Needs shape validation at model load against the tensorcontract, not a CLI change.
for a 1.0 GB Q4_K) before a parity gate that then rejects it at cosine 0.884 on
this GPU. Backend-ordering and wgpu-kernel work.
--features cudabuild's unconditional debug spew (238eprintln!under
crates/aprender-serve/src/cuda/) — needs a CUDA build to verify.Also left alone deliberately:
apr serve --backend/--trace-levelstill takefree-form strings. Same fix applies, different cluster's file.
Refs #2378 (partial) — findings 1, 2, 3, 5, 6, 7 fixed; 4, 8, 9, 10 remain.
Audit epic: #2373
🤖 Generated with Claude Code