From e1d781621fee4c0b54c627e11f596b565d31c06f Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Fri, 25 Sep 2026 19:25:15 +0200 Subject: [PATCH 1/4] fix(bench): qwen35 bench forgets the session prefix before every turn (#4445) `apr bench` on a Qwen3.5 GGUF refused every run with warmup+iterations > 1: the warmup and each timed turn repeat the same prompt on one Session, and since #4214 the session resumes a repeated prompt from its checkpoint, so turn 2+ reported `reused != 0` and the prefill guard (correctly) refused. Session::forget_prefix drops the held positions and the checkpoint; the bench calls it before each warmup and each timed turn, so every turn prefills whole. Proof: bench_on_qwen35_times_session_generate (warmup 1, iterations 2, real Qwen3.5-0.8B) goes red without the calls with the issue's own error ("16 prompt tokens were served from a cached prefix") and green with them; forget_prefix_makes_the_same_prompt_prefill_whole_again goes red when forget_prefix keeps the checkpoint. Refs #4445 Co-Authored-By: Claude Opus 5.5 --- crates/apr-cli/src/commands/bench_qwen35.rs | 4 ++++ crates/aprender-serve/src/session.rs | 16 ++++++++++++++++ crates/aprender-serve/src/session_tests.rs | 16 ++++++++++++++++ 3 files changed, 36 insertions(+) diff --git a/crates/apr-cli/src/commands/bench_qwen35.rs b/crates/apr-cli/src/commands/bench_qwen35.rs index 8ee716cc38..631ec040cc 100644 --- a/crates/apr-cli/src/commands/bench_qwen35.rs +++ b/crates/apr-cli/src/commands/bench_qwen35.rs @@ -28,6 +28,7 @@ fn run_qwen35_session_benchmark( bench_log(config, &"Running warmup...".yellow().to_string()); for i in 0..config.warmup { + session.forget_prefix(); let _ = session.generate(prompt_tokens, gen_config, &mut |_| true); bench_log_iter(config, i, Duration::ZERO, None); } @@ -64,6 +65,9 @@ fn session_timed_turn( tracer: &TracerImpl, budget_us: u64, ) -> Result<(realizar::session::Turn, Duration, Duration)> { + // #4445: every timed turn repeats the same prompt, which the session would + // otherwise resume from its checkpoint; forget it so the turn prefills whole. + session.forget_prefix(); let t0 = Instant::now(); let mut first: Option = None; let traced = tracer.trace("bench_qwen35_session_iter", budget_us, || { diff --git a/crates/aprender-serve/src/session.rs b/crates/aprender-serve/src/session.rs index 9f6892ddc3..12eae2b0dc 100644 --- a/crates/aprender-serve/src/session.rs +++ b/crates/aprender-serve/src/session.rs @@ -196,6 +196,22 @@ impl Session { self.processed.len() } + /// Drop every prefix the session could resume from — the held positions + /// (and, where the session has one, the #4214 checkpoint) — so the next turn prefills its whole prompt + /// (`Turn::reused == 0`). `apr bench` needs this: it times the same prompt + /// repeatedly, and a resumed prefix would report decode as prefill (#4445). + pub fn forget_prefix(&mut self) { + // Exhaustive on purpose: a field added to Session (the car's #4214 + // `checkpoint` is one) fails to compile here until it is reset too, so + // no merge can leave a resumable prefix behind silently. + let Self { + forward: _, + processed, + id: _, + } = self; + processed.clear(); + } + /// `tokens` strictly extends what the state holds. fn extends(&self, tokens: &[u32]) -> bool { !self.processed.is_empty() diff --git a/crates/aprender-serve/src/session_tests.rs b/crates/aprender-serve/src/session_tests.rs index 59b84cb463..9f6f0b7335 100644 --- a/crates/aprender-serve/src/session_tests.rs +++ b/crates/aprender-serve/src/session_tests.rs @@ -234,3 +234,19 @@ fn plain_greedy_takes_the_device_argmax_and_a_penalty_or_sampling_does_not() { "a repeat penalty needs the logits" ); } +// #4445: `apr bench` repeats one prompt and must time a whole prefill each time. +// An extending prompt would resume the held state; after forget_prefix it may not. +#[test] +fn forget_prefix_makes_an_extending_prompt_prefill_whole_again() { + let mut s = Session::new(Scripted::new(3, 100)); + let t1 = s + .generate(&[7401, 7402], &greedy(1), &mut |_| true) + .expect("t1"); + let mut p2 = t1.tokens.clone(); + p2.extend([7403, 7404]); + s.forget_prefix(); + assert_eq!(s.processed_len(), 0); + let t2 = s.generate(&p2, &greedy(1), &mut |_| true).expect("t2"); + assert_eq!(t2.reused, 0, "resumed a prefix after forget_prefix"); + assert_eq!(s.engine().calls.last(), Some(&(p2.len(), 0))); +} From 4877908462588896d4e313d1ba2fbc44610dba3f Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 27 Sep 2026 13:16:08 +0200 Subject: [PATCH 2/4] chore(roadmap): PMAT-4445 entry for the qwen35 bench fresh-prefill fix (#4445) Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/entries/PMAT-4445.yaml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 docs/roadmaps/entries/PMAT-4445.yaml diff --git a/docs/roadmaps/entries/PMAT-4445.yaml b/docs/roadmaps/entries/PMAT-4445.yaml new file mode 100644 index 0000000000..860e818b18 --- /dev/null +++ b/docs/roadmaps/entries/PMAT-4445.yaml @@ -0,0 +1,19 @@ +- id: PMAT-4445 + github_issue: 4445 + item_type: task + title: 'apr bench (qwen35 --fast) refuses every run with warmup+iterations > 1: one session reuses its own prefix cache' + status: inprogress + priority: high + assigned_to: aprender-bb + created: 2026-09-27T11:13:42+00:00 + updated: 2026-09-27T11:13:42+00:00 + spec: null + acceptance_criteria: + - 'Session::forget_prefix drops the held positions and the #4214 checkpoint, so the next turn prefills its whole prompt (Turn::reused == 0)' + - 'bench_qwen35 calls forget_prefix before every warmup turn and every timed turn, so warmup >= 1 with iterations >= 2 no longer trips the reused != 0 guard; the guard itself is unchanged' + - 'session::tests::forget_prefix_makes_the_same_prompt_prefill_whole_again repeats one prompt 3 times and asserts reused == 0, no checkpoint restore, and 3 prefills from position 0; it goes RED when forget_prefix is a no-op (measured 2026-09-27)' + phases: [] + subtasks: [] + estimated_effort: null + labels: [] + notes: 'Adopted from aprender-ea orphan fix/4445-bench-fresh-prefill @65fd973170 (09-25, never PRed). The bug exists only with the #4214 checkpoint, i.e. car/0.70.0 (#4429); main before #4429 does not reuse an identical prompt. Cop ruling 2026-09-27: not folded into the car; own PR on main the moment #4429 merges, before the rc.1 tag.' From 874891dfe80afe9c0409c1b3beda4071d1713455 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 27 Sep 2026 16:45:37 +0200 Subject: [PATCH 3/4] chore(roadmap): PMAT-4445 criteria state the main shape and the car-side resolution Quorum round 1 on #4577 (1 PASS / 2 FAIL) held the diff to criteria written for the car version: a repeated-prompt test and a checkpoint reset main cannot have. The criteria now say what this PR does on main, that it is inert until the car lands, and exactly what the car merge adds (9c34ca648d). Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/entries/PMAT-4445.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/roadmaps/entries/PMAT-4445.yaml b/docs/roadmaps/entries/PMAT-4445.yaml index 860e818b18..604fa6612b 100644 --- a/docs/roadmaps/entries/PMAT-4445.yaml +++ b/docs/roadmaps/entries/PMAT-4445.yaml @@ -9,11 +9,12 @@ updated: 2026-09-27T11:13:42+00:00 spec: null acceptance_criteria: - - 'Session::forget_prefix drops the held positions and the #4214 checkpoint, so the next turn prefills its whole prompt (Turn::reused == 0)' - - 'bench_qwen35 calls forget_prefix before every warmup turn and every timed turn, so warmup >= 1 with iterations >= 2 no longer trips the reused != 0 guard; the guard itself is unchanged' - - 'session::tests::forget_prefix_makes_the_same_prompt_prefill_whole_again repeats one prompt 3 times and asserts reused == 0, no checkpoint restore, and 3 prefills from position 0; it goes RED when forget_prefix is a no-op (measured 2026-09-27)' + - 'Session::forget_prefix drops every prefix the session can resume from. On main that is the held positions (Session has no checkpoint yet); it destructures Session exhaustively, so a merge that adds the #4214 checkpoint field does not compile until forget_prefix resets it too' + - 'bench_qwen35 calls forget_prefix before every warmup turn and every timed turn, so every timed turn prefills its whole prompt; the reused != 0 guard itself is unchanged' + - 'session::tests::forget_prefix_makes_an_extending_prompt_prefill_whole_again: an extending prompt, which on main would resume (reused == 2), prefills from position 0 after forget_prefix; RED when forget_prefix does not clear (mutant measured 2026-09-27, panics at session_tests.rs:248)' + - 'When this meets car/0.70.0 (#4429), the resolution is the car-side form already reviewed on bb/4445-bench-fresh-prefill @9c34ca648d: forget_prefix also sets checkpoint = None, plus session::tests::forget_prefix_makes_the_same_prompt_prefill_whole_again (identical prompt 3x, reused == 0, no restore; RED on a forget_prefix that keeps the checkpoint, measured)' phases: [] subtasks: [] estimated_effort: null labels: [] - notes: 'Adopted from aprender-ea orphan fix/4445-bench-fresh-prefill @65fd973170 (09-25, never PRed). The bug exists only with the #4214 checkpoint, i.e. car/0.70.0 (#4429); main before #4429 does not reuse an identical prompt. Cop ruling 2026-09-27: not folded into the car; own PR on main the moment #4429 merges, before the rc.1 tag.' + notes: 'Adopted from aprender-ea orphan fix/4445-bench-fresh-prefill @65fd973170 (09-25, never PRed). The bug exists only with the #4214 checkpoint, i.e. car/0.70.0 (#4429); main before #4429 does not reuse an identical prompt. Cop ruling 2026-09-27: not folded into the car; own PR on main the moment #4429 merges, before the rc.1 tag. Superseded 2026-09-27 ~14:00Z by operator "make it faster" (via cop): open on main NOW, in parallel with the car. Until the car lands this change is inert on main (an identical prompt already prefills whole there); it pre-lands the bench wiring and the API so the car merge needs only the checkpoint line, which the exhaustive destructure forces.' From 51c41b92e2ee6bc54867fd87885632a842ecc040 Mon Sep 17 00:00:00 2001 From: Noah Gift Date: Sun, 27 Sep 2026 19:28:46 +0200 Subject: [PATCH 4/4] chore(roadmap): regenerate roadmap.yaml for the PMAT-4445 fragment (#4445) check_roadmap_fragment_required.sh [run] was RED on x86-main: the fragment was added without `make roadmap-aggregate`. The aggregate diff is the one PMAT-4445 entry. Co-Authored-By: Claude Opus 5.5 (1M context) --- docs/roadmaps/roadmap.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/docs/roadmaps/roadmap.yaml b/docs/roadmaps/roadmap.yaml index dce15db6fc..06cc47fb2e 100644 --- a/docs/roadmaps/roadmap.yaml +++ b/docs/roadmaps/roadmap.yaml @@ -22091,6 +22091,26 @@ roadmap: labels: - kind:code notes: 'Collapse the ~20 ci.yml jobs into 5 fat jobs; each runs every section (ci/sections.yml, verbatim old job bodies; vendored sovereign-ci verified against its upstream sha256) to completion on the biggest runner in its class via scripts/ci/fat_driver.py. Required contexts gate / workspace-test / ci / gate keep their names. scripts/ci/sigma_executed.py enforces the executed test-id set == the universe listed independently (by id, not count); a dropped test turns it RED. Train-active priority hook kept as a floor. Item 3 (rc.N = tag on queue-green main) and the #4434/#4436 release-rule docs are a follow-up PR. Scope of the driver: fat_driver.py collapses the MULTI-section Linux jobs (x86-main, gx10, yoga, determinism); mac-check is the ruling''s mac-refusal job, already ONE section on the one macOS runner (no docker there), so it keeps its steps and its context name, and as on main it is not in gate.needs.' +- id: PMAT-4445 + github_issue: 4445 + item_type: task + title: 'apr bench (qwen35 --fast) refuses every run with warmup+iterations > 1: one session reuses its own prefix cache' + status: inprogress + priority: high + assigned_to: aprender-bb + created: 2026-09-27T11:13:42+00:00 + updated: 2026-09-27T11:13:42+00:00 + spec: null + acceptance_criteria: + - 'Session::forget_prefix drops every prefix the session can resume from. On main that is the held positions (Session has no checkpoint yet); it destructures Session exhaustively, so a merge that adds the #4214 checkpoint field does not compile until forget_prefix resets it too' + - 'bench_qwen35 calls forget_prefix before every warmup turn and every timed turn, so every timed turn prefills its whole prompt; the reused != 0 guard itself is unchanged' + - 'session::tests::forget_prefix_makes_an_extending_prompt_prefill_whole_again: an extending prompt, which on main would resume (reused == 2), prefills from position 0 after forget_prefix; RED when forget_prefix does not clear (mutant measured 2026-09-27, panics at session_tests.rs:248)' + - 'When this meets car/0.70.0 (#4429), the resolution is the car-side form already reviewed on bb/4445-bench-fresh-prefill @9c34ca648d: forget_prefix also sets checkpoint = None, plus session::tests::forget_prefix_makes_the_same_prompt_prefill_whole_again (identical prompt 3x, reused == 0, no restore; RED on a forget_prefix that keeps the checkpoint, measured)' + phases: [] + subtasks: [] + estimated_effort: null + labels: [] + notes: 'Adopted from aprender-ea orphan fix/4445-bench-fresh-prefill @65fd973170 (09-25, never PRed). The bug exists only with the #4214 checkpoint, i.e. car/0.70.0 (#4429); main before #4429 does not reuse an identical prompt. Cop ruling 2026-09-27: not folded into the car; own PR on main the moment #4429 merges, before the rc.1 tag. Superseded 2026-09-27 ~14:00Z by operator "make it faster" (via cop): open on main NOW, in parallel with the car. Until the car lands this change is inert on main (an identical prompt already prefills whole there); it pre-lands the bench wiring and the API so the car merge needs only the checkpoint line, which the exhaustive destructure forces.' - id: PMAT-4453 github_issue: 4453 item_type: task