diff --git a/crates/app/src/mtp/policy.rs b/crates/app/src/mtp/policy.rs index 738f25477..825a13be7 100644 --- a/crates/app/src/mtp/policy.rs +++ b/crates/app/src/mtp/policy.rs @@ -156,7 +156,8 @@ pub(super) struct Policy { /// The next window's refresh; `None` between a prompt call and its /// `begin`. next: Option>, - /// The last prompt unit's arena and rows, for [`Policy::begin`]'s anchor. + /// The last prompt unit's arena and rows, until a call takes its last row + /// in: [`Policy::begin`]'s anchor, or the first step's or join's walk. last_unit: Option<(A, usize)>, /// The last join to a held sequence a call continued, until taken. joined: Option, @@ -452,6 +453,7 @@ impl Policy { } let (walk, rows) = self .last_unit + .take() .ok_or_else(|| Refused(format!("{WHAT}: a prompt the draft never saw")))?; self.next = Some(Refresh { tokens: vec![first], @@ -875,6 +877,23 @@ mod tests { fn plain_step(&mut self) -> Result<(), String> { let (pos, last) = (self.c.pos, self.last); self.p.before_step(&mut self.c, pos, last)?; + self.step() + } + + /// A pass the width chooser runs as a plain step after the draft's + /// chain (a shadow, scored and never verified, or a window its + /// closing gate cuts to no id): the chain, its proposal dropped, then + /// the step. No walk runs before the step: the chain took the rows + /// that waited. + fn walked_pass(&mut self) -> Result<(), String> { + self.chain()?; + self.step() + } + + /// The target's step of the token at its position, then the draft + /// told (`stepped`, `held`). + fn step(&mut self) -> Result<(), String> { + let last = self.last; self.c.ran(Ar::Step, 1); let pos = self.c.pos; let next = 1000 + pos; @@ -883,18 +902,25 @@ mod tests { Ok(()) } - /// A drafted pass that keeps `kept` rows (all of them when `None`): - /// the chain, the verify, the accept's record, the commit. - fn drafted_pass(&mut self, kept: Option) -> Result<(), String> { - let (p0, ctx, last) = (self.c.pos, self.c.ctx, self.last); + /// The draft's chain at the target's position: its refresh walked, + /// then its own walks, whose rows the body takes back once the + /// proposal is read: the store ends at the refresh. + fn chain(&mut self) -> Result, String> { + let (here, ctx, last) = (self.c.pos, self.c.ctx, self.last); let chain = self .p - .chain(p0, ctx, last, SHAPE.width) + .chain(here, ctx, last, SHAPE.width) .map_err(|e| e.to_string())? - .ok_or("a drafted pass of a draft that skips")?; + .ok_or("a chain of a draft that skips")?; self.c.walk(&chain.refresh.as_walk(chain.mode))?; - let end = chain.refresh.pos0 as usize + chain.refresh.tokens.len(); - self.c.store = end + chain.own; + Ok(chain) + } + + /// A drafted pass that keeps `kept` rows (all of them when `None`): + /// the chain, the verify, the accept's record, the commit. + fn drafted_pass(&mut self, kept: Option) -> Result<(), String> { + let (p0, last) = (self.c.pos, self.last); + let chain = self.chain()?; let mut rows = vec![last]; rows.extend((0..=chain.own as u32).map(|i| 500 + i)); self.c.ran(Ar::Pass, rows.len()); @@ -1083,6 +1109,42 @@ mod tests { rig.drafted_pass(None).unwrap(); } + /// A window no verify takes (the width chooser's shadow, or the window + /// its closing gate cuts to no id), first after `begin` and then after + /// a drafted pass: its chain walks the waiting rows, and its step walks + /// nothing more (the prompt call's last unit is the anchor `begin` took, + /// not rows that still wait) and leaves the step's row waiting, the + /// store at the target's position. + #[test] + fn a_dropped_window_walks_its_chain_alone() { + let mut rig = after_prompt(64); + rig.begin().unwrap(); + rig.walked_pass().unwrap_or_else(|e| panic!("{e}")); + assert_eq!( + rig.c.take(), + [seen(HEAD, 9, &[1009], rows_of(Ar::Ubatch, 0))] + ); + assert_eq!((rig.c.store, rig.c.pos), (10, 10)); + assert_eq!((rig.next().pos0, rig.next().walk), (10, Ar::Step)); + + let mut rig = after_prompt(64); + rig.begin().unwrap(); + rig.drafted_pass(None).unwrap(); + rig.c.take(); + rig.walked_pass().unwrap_or_else(|e| panic!("{e}")); + assert_eq!( + rig.c.take(), + [seen(HEAD, 10, &[500, 501, 502, 2003], rows_of(Ar::Pass, 0))] + ); + assert_eq!((rig.c.store, rig.c.pos), (14, 14)); + assert_eq!((rig.next().pos0, rig.next().walk), (14, Ar::Step)); + rig.drafted_pass(None).unwrap(); + assert_eq!( + rig.c.take(), + [seen(HEAD, 14, &[1014], rows_of(Ar::Step, 0))] + ); + } + /// A step that ends at the context: its row is left unwalked, since no /// chain runs there, and the refresh it records waits for a call that /// never comes. @@ -1332,6 +1394,7 @@ mod tests { "{why}" ); + let mut rig = after_prompt(64); rig.begin().unwrap(); let why = refused(&mut rig, 9, 0); assert!(why.ends_with("a proposal into no room"), "{why}"); @@ -1450,14 +1513,15 @@ mod tests { /// the draft's store holding a position at or past the target's — what /// a sequence state needs to save the draft's side — and none ends in a /// refusal: every sequence of up to six calls from a plain step (the - /// seat's step and a pass the width chooser holds back, one order) and - /// a drafted pass (keeping one row or all) over a prompt, started by - /// `begin` or by the server's cut step, runs without a refusal, each of - /// its walks one the card model accepts, and after every call the store - /// holds no position past the target's. The runtime's own plain loop — - /// a step with no walk before it, which is what refused here once — - /// never follows a cut step (its steps come only after the draft's own - /// proposal), so it is not among the calls. + /// seat's step and a pass the width chooser holds back, one order), a + /// pass whose chain no verify takes (the chooser's shadow, or its + /// closing gate's cut to no id) and a drafted pass (keeping one row or + /// all) over a prompt, started by `begin` or by the server's cut step, + /// runs without a refusal, each of its walks one the card model accepts, + /// and after every call the store holds no position past the target's. + /// The runtime's own plain loop — a step with no walk before it, which + /// is what refused here once — never follows a cut step (its steps come + /// only after the draft's own proposal), so it is not among the calls. /// /// The contexts the sequences run at are one the calls cannot reach the /// end of and one they can (the prompt's nine ids plus three rows): @@ -1472,9 +1536,15 @@ mod tests { #[derive(Clone, Copy, Debug)] enum Call { Plain, + Walked, Drafted(Option), } - const CALLS: [Call; 3] = [Call::Plain, Call::Drafted(Some(1)), Call::Drafted(None)]; + const CALLS: [Call; 4] = [ + Call::Plain, + Call::Walked, + Call::Drafted(Some(1)), + Call::Drafted(None), + ]; fn run(rig: &mut Rig, calls: &[Call]) -> Result<(), String> { for (i, call) in calls.iter().enumerate() { @@ -1487,11 +1557,12 @@ mod tests { // A drafted pass runs while its narrowest verify fits the // context; closer to the end the caller steps. let call = match *call { - Call::Drafted(_) if pos + 2 > ctx => Call::Plain, + Call::Walked | Call::Drafted(_) if pos + 2 > ctx => Call::Plain, c => c, }; match call { Call::Plain => rig.plain_step(), + Call::Walked => rig.walked_pass(), Call::Drafted(kept) => rig.drafted_pass(kept), } .map_err(|e| format!("call {} ({call:?}): {e}", i + 1))?; @@ -1542,6 +1613,6 @@ mod tests { } } } - assert_eq!(count, 2 * 2 * (1093 + 364)); + assert_eq!(count, 2 * 2 * (5461 + 1365)); } } diff --git a/crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs b/crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs index 3058e8836..6e7c137e0 100644 --- a/crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs +++ b/crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs @@ -125,10 +125,10 @@ //! the pass steps plainly with the draft's proposals shadowed, never a //! window of one, the ids still the plain run's. //! - (p) a partial accept's records: the commit of a verify that kept fewer -//! rows than it ran cuts the draft's records to the kept rows — after a -//! drafted run the draft store's count stands at the model's position, a -//! snapshot there holds exactly the accepted rows, a store walked past the -//! position (an anchor row) makes the snapshot refused by name, and after +//! rows than it ran cuts the draft's records to the kept rows. A window +//! starting at p0 and keeping k rows leaves the draft store at p0 + 1 +//! and the target at p0 + k. A snapshot preserves both counts. A store +//! walked past the position (an anchor row) makes the snapshot refused by name, and after //! a scripted window that keeps 2 of its 4 rows the pass arena's rejected //! row is refused by name for a walk that reads it (the uncut record //! would let the walk run). @@ -169,8 +169,17 @@ //! chooser walks the draft's waiting rows first (`Draft::before_plain`, //! the call its propose makes on the plain pass it returns), then the //! target steps and overwrites the arena the anchor read — after the -//! walk — so the pass takes the position in instead of refusing and the -//! drafted ids stay the plain run's. +//! walk — so the pass takes the position in instead of refusing, the +//! draft's store stands at the target's position, a snapshot there is +//! taken, and the drafted ids stay the plain run's. +//! - (v) a window no verify takes (the width chooser's shadow, or the window +//! its closing gate cuts to no id), after `begin` and after a drafted pass +//! from the prompt's anchor: the draft's chain runs, its proposal is +//! dropped, the target steps and the draft hears of it through +//! `Draft::held`. The step walks nothing the prompt call left (`begin` +//! took its anchor), the draft's store stands at the target's position +//! (the chain took its own rows back), a snapshot there is taken, and the +//! drafted ids after it are the plain run's. //! - (y) two drafted sequences resident over a plan of two //! (`PlanInputs::plan_mtp_with_slots`, `Body38::open_placed_mtp_slots`), //! in the server's order (`worker.rs`'s `start` → `genloop`'s `prompt`): @@ -2350,11 +2359,89 @@ mod gate { Ok(ok && runs_ok) } + /// The target position and the draft's committed store count travel together in a state. + #[derive(Clone, Copy, PartialEq)] + struct DraftEnd { + held: usize, + pos: u32, + } + + impl DraftEnd { + fn read(m: &Qwen38Model) -> Result { + Ok(Self { + held: held_of(m)?, + pos: m.pos(), + }) + } + + // PIN(2026-10-22): the refresh commits p0 + 1, not the target's p0 + kept rows. + fn after(pos: u32, kept: usize) -> Option { + Some(Self { + held: (pos as usize).checked_sub(kept.checked_sub(1)?)?, + pos, + }) + } + + // PIN(2026-10-22): a whole restore preserves both counts, including a trailing store. + fn restored(self, m: &Qwen38Model) -> Result { + Ok(Self::read(m)? == self) + } + } + + /// A real MTP chain, with the first proposal kept and the second rejected by a forced pick: + /// the draft's store trails the target. Its state is put back whole on the engine stream + /// and from a lane, then the store walked past the target refuses both saves + /// ([`past_refused`]). + fn partial_state( + m: Qwen38Model, + prompt: &[u32], + kv_bytes: u64, + ) -> Result<(Qwen38Model, bool), GateError> { + let ctx = m.body("partial state")?.ctx() as u32; + let (mut s, mut spec) = drafted_session(m, step_cfg(Prompt38::Auto), ctx)?; + let first = spec.prompt(&mut s, prompt)?; + spec.begin(&s, prompt, first)?; + let p0 = s.pos(); + let mut rows = [first; 4]; + let n = Draft::propose(spec.draft_mut(), &mut s, first, &mut rows[1..])?; + if n != 3 { + return Err("partial state needs three proposals".into()); + } + let mut picked = Verify::verify(&mut s, rows)?; + let vocab = u32::try_from(s.model().body("partial state")?.vocab())?; + picked[0] = rows[1]; + picked[1] = (rows[2] + 1) % vocab; + let kept = runtime::accepted_rows(&rows, &picked); + Draft::accept(spec.draft_mut(), &mut s, &rows, &picked, kept)?; + Verify::commit(&mut s, kept)?; + let mut m = s.into_model(); + let end = DraftEnd::read(&m)?; + let shape = kept == 2 && end.pos == p0 + 2 && end.held == p0 as usize + 1; + let cut = DraftEnd::after(end.pos, kept) == Some(end); + let before = store_of(&m, end.pos as usize)?; + let state = m.seq_save()?; + m.reset()?; + m.seq_resume(&state)?; + let whole = end.restored(&m)? && store_of(&m, end.pos as usize)? == before; + let (_, _, lane) = lane_round(&mut m, prompt, kv_bytes, ("partial accept", 4))?; + let past = past_refused(&mut m, prompt, end.pos)?; + println!( + "(p-partial) kept {kept}, target {} (p0 {p0}), store {}; shape {}, kept_state {}, prefix_draft {}, lane_save {}, past_refused {}", + end.pos, + end.held, + verdict(shape), + verdict(cut), + verdict(whole), + verdict(lane), + verdict(past) + ); + Ok((m, shape && cut && whole && lane && past)) + } + /// (p): a partial accept's records and snapshot. The commit of a verify /// that kept fewer rows than it ran cuts the draft's records to the kept - /// rows: after a drafted run the store's count stands at the model's - /// position, a snapshot there holds exactly the accepted rows, and once - /// the store walks past the position (an anchor row) the snapshot is + /// rows. The store's count follows [`DraftEnd::after`], and a snapshot + /// preserves both counts. Once the store walks past the position (an anchor row) the snapshot is /// refused by name; after a scripted window that keeps 2 of its 4 rows /// the pass arena's rejected row is a position the arena no longer /// holds — a walk that reads it is refused by name, where the uncut @@ -2380,7 +2467,8 @@ mod gate { }; let at = m.pos() as usize; let held = m.body("kept state")?.mtp().map_or(0, |d| d.held()); - let cut = held == at; + let cut = kept.rows.last().and_then(|&k| DraftEnd::after(m.pos(), k)) + == Some(DraftEnd::read(&m)?); println!( "(p) after the drafted run's {} windows at {at}: the draft store holds {held} \ positions {}", @@ -2397,28 +2485,8 @@ mod gate { }, verdict(snap_ok) ); - // The store walked past the position — an anchor row: the state - // carries the rows below the position only. - let zeros = vec![0.0f32; WIDE]; - m.mtp_walk( - DraftFeed::Rows(Feed { - tokens: &prompt[..1], - pos0: u32::try_from(at)?, - hidden: Hidden::Host(&zeros), - media: None, - }), - DraftHead::Rows, - WalkMode::Store, - )?; let mut ok = cut && snap_ok; - ok &= refused( - "a snapshot of a store past the position", - m.seq_save(), - &format!( - "a state at {at} of a draft store that holds {} positions", - at + 1 - ), - ); + ok &= past_refused(&mut m, prompt, u32::try_from(at)?)?; // A scripted window that keeps `KEEP` of its 4 rows: the arena's // rows `KEEP` on are the rejected tail the commit takes back. The // scripted drafter walks no draft, so the store is warmed over the @@ -2570,8 +2638,8 @@ mod gate { /// (kp-draft): a cut to a point and a prefix restore to it keep the draft. A drafted run /// with a point at its prompt's end (`p`) below its end is saved; put back whole, the - /// draft's store holds the state's positions (the contrast). Then the state put back whole - /// and cut to `p` by `rollback`, the state put back as its first `p` positions + /// draft's store holds its saved count, which can trail the target (the contrast). + /// Then the state put back whole and cut to `p` by `rollback`, the state put back as its first `p` positions /// (`seq_resume_prefix` asked one short of the end), and a state saved there put back whole /// each stand at `p` with the draft rejoined ([`AtPoint::rejoined`]): the draft's store /// holds the `p` positions below the point, its rows the drafted run's, and the step @@ -2588,6 +2656,7 @@ mod gate { fn prefix_draft(m: Qwen38Model, prompt: &[u32]) -> Result<(Qwen38Model, bool), GateError> { const N: usize = 6; let (mut m, first, point_row) = drafted_run(m, prompt, N)?; + let end = DraftEnd::read(&m)?; let state = m.seq_save()?; let n = state.positions(); let ask = n.saturating_sub(1); @@ -2606,6 +2675,7 @@ mod gate { m.reset()?; m.seq_resume(&state)?; let (whole_held, whole_pos) = (held_of(&m)?, m.pos()); + let contrast = end.restored(&m)?; let rows = store_of(&m, p)?; let saved_all = store_of(&m, n as usize)?; m.reset()?; @@ -2641,7 +2711,6 @@ mod gate { }; m.body_parts("(kp-draft)")?.2.set_checkpoints(false); m.reset()?; - let contrast = whole_held == n as usize && whole_pos == n; let roll_ok = roll.rejoined(p, &rows, &point_row) && roll_walk.is_ok(); let tail = |a, b| same_rows(a, b, n as usize)[p..].iter().all(|&s| s); let (kept_tail, differs) = (tail(&past, &other_all), !tail(&other_all, &saved_all)); @@ -2743,8 +2812,8 @@ mod gate { } /// (sl): a drafted run's state saved on a save lane (`seq_save_async`) three times, its copy - /// landed each time: whole, the draft's store holding all `n` positions, then after a - /// rollback to a point `at` below `n`, which the draft rejoins (its store counts `at`), then + /// landed each time: whole, the draft's store holding its saved count at target `n`, + /// then after a rollback to a point `at` below `n`, which the draft rejoins (its store counts `at`), then /// a state of `BEHIND` ids from a reset by a prompt call without the draft (the store holds 0 /// of `BEHIND`). Each /// copy's bytes (`SaveCost::bytes`) are the target's rows of the state's positions @@ -2784,11 +2853,10 @@ mod gate { s.into_model() }; let n = m.pos(); - let (state, held, whole_ok) = lane_round(&mut m, prompt, kv_bytes, ("whole", 1))?; - let whole_ok = whole_ok && held == n as usize; + let (state, _, whole_ok) = lane_round(&mut m, prompt, kv_bytes, ("whole", 1))?; let at = state.keep_point(n.saturating_sub(1)); let prefix_ok = whole_ok && lane_prefix(&mut m, &state, n, at)?; - let refused_ok = whole_ok && lane_refused(&mut m, prompt, n)?; + let refused_ok = whole_ok && past_refused(&mut m, prompt, n)?; let cut_ok = if at == 0 || at >= n { println!( "(sl) the state of {n} positions carries no point below its end (asked {}, \ @@ -2874,10 +2942,8 @@ mod gate { ) -> Result<(Seq38, usize, bool), GateError> { let (what, shift) = round; let n = m.pos() as usize; - let held_of = |m: &Qwen38Model| -> Result { - Ok(m.body("(sl)")?.mtp().map_or(0, Mtp38::held)) - }; - let held = held_of(m)?; + let end = DraftEnd::read(m)?; + let held = end.held; let store_ctx = m .body("(sl)")? .mtp() @@ -2900,7 +2966,7 @@ mod gate { let put = held_of(m)?; let same = store_of(m, n)? == before; let bytes_ok = cost.bytes == derived; - let back_ok = put == held && seen && same; + let back_ok = end.restored(m)? && seen && same; println!( "(sl) {what}, a state of {n} positions whose draft store holds {held} saved on a \ lane: the copy {} bytes (derived {derived}: the target's {target}, recurrent copies \ @@ -2914,38 +2980,30 @@ mod gate { Ok((state, held, bytes_ok && back_ok)) } - /// The save on a lane of a model whose draft store was walked past its position `n` (a - /// store walk from `n`, the anchor row): refused by name with the words of the engine - /// stream's save ((p)), nothing enqueued. The walk follows a whole put-back, which leaves - /// the store counting `n`. - fn lane_refused(m: &mut Qwen38Model, prompt: &[u32], n: u32) -> Result { - let zeros = vec![0.0f32; WIDE]; - m.mtp_walk( - DraftFeed::Rows(Feed { - tokens: &prompt[..1], - pos0: n, - hidden: Hidden::Host(&zeros), - media: None, - }), - DraftHead::Rows, - WalkMode::Store, - )?; + /// The saves of a model at position `n` whose draft store was walked past it: store walks + /// from position 0 through `n` (the anchor row), wherever the store stood, since a store + /// left by a partial accept trails the target and a walk from `n` would be refused. The + /// save on the engine stream and the one on a lane are each refused by name, nothing + /// enqueued. + fn past_refused(m: &mut Qwen38Model, prompt: &[u32], n: u32) -> Result { + overwrite_store(m, prompt, n as usize + 1, 1)?; let (gdn, qsa) = (cfg().shape.n_gdn(), cfg().shape.n_qsa()); let lane = m.seq_lane(seq38_bytes(gdn, qsa, n as usize, true))?; - let got = m - .seq_save_async(&lane) - .err() - .map_or_else(|| "ran".to_owned(), |e| e.to_string()); let want = format!( "a state at {n} of a draft store that holds {} positions", n + 1 ); - let ok = got.contains(&want); - println!( - "(sl) a lane save of a store walked past the state's {n} positions: {got} {}", - verdict(ok) + let sync = refused( + "a snapshot of a store past the position", + m.seq_save(), + &want, ); - Ok(ok) + let on_lane = refused( + "a lane save of a store past the position", + m.seq_save_async(&lane), + &want, + ); + Ok(sync && on_lane) } /// The draft's store rows at positions `0..n` written by store walks of other ids (the @@ -5001,42 +5059,105 @@ mod gate { Ok(vec![first, next]) } - /// (i) the width chooser's held pass right after the server's cut step - /// (a request's prompt call, then its last id as the seat's first step, - /// [`start`]): the chooser walks the draft's waiting rows first - /// ([`Draft::before_plain`], the call `runtime::width`'s `Choosing` - /// makes on the plain pass its `propose` returns), then the target - /// steps and the draft hears of it through [`Draft::held`] — the step - /// overwrites the arena the anchor read only after the walk, so the - /// held pass takes the position in instead of refusing, the store - /// never holds a position past the target's, and the drafted ids after - /// it stay the plain run's. - fn held_after_cut(m: Qwen38Model, prompt: &[u32]) -> Result<(Qwen38Model, bool), GateError> { - const N: usize = 8; - let ctx = m.body("held after the cut")?.ctx() as u32; - let (m, plain, _) = plain_to(m, prompt, Prompt38::Auto, N)?; + /// A plain pass of the width chooser's: a pass its `propose` returns no + /// verify for, as `runtime::width`'s `Choosing` runs it. + #[derive(Clone, Copy)] + enum PlainPass { + /// (i) a held pass right after the server's cut step (a request's + /// prompt call, then its last id as the seat's first step, + /// [`start`]): the draft's waiting rows walked first + /// ([`Draft::before_plain`]), then the target's step, which + /// overwrites the arena the anchor read only after the walk. + Held, + /// (v) a window no verify takes (the chooser's shadow, or the window + /// its closing gate cuts to no id), after `begin` and after a + /// drafted pass from the prompt's anchor: the draft's chain, its + /// proposal dropped ([`Draft::unproposed`]), then the target's step, + /// which walks no row of the prompt call's arena (`begin` took its + /// anchor) and finds the chain's own rows taken back. + Dropped, + } + + /// The chooser's plain pass `plain`, then the draft told through + /// [`Draft::held`]: told without a refusal, the draft's store at the + /// target's position, a snapshot there, and the drafted ids after it + /// the plain run's. + fn chooser_plain( + m: Qwen38Model, + prompt: &[u32], + plain: PlainPass, + ) -> Result<(Qwen38Model, bool), GateError> { + const N: usize = 12; + const WIDTH: usize = as Draft>>::WIDTH; + let ctx = m.body("the chooser's plain pass")?.ctx() as u32; + let (m, plain_ids, _) = plain_to(m, prompt, Prompt38::Auto, N)?; let mut s = app::Session::from_model(m, ctx); - let mut d = fresh_draft(&s)?; - let mut out = start(&mut s, &mut d, prompt)?; - // The chooser-held pass as the width chooser's propose runs it: the - // hook walks the anchor the cut step recorded, then the target - // steps (overwriting that arena), then the draft is told through - // Draft::held. - let last = *out.last().ok_or("no token after the cut step")?; - Draft::before_plain(&mut d, &mut s, last)?; - let next = Target::step(&mut s, last, Want::Argmax)?.argmax(); - Draft::held(&mut d, &mut s, last, next)?; - out.push(next); - let mut spec = Spec38::new(d); - while out.len() < N { - one_pass(&mut s, &mut spec, &mut out)?; + let (label, cases): (&str, &[(&str, bool)]) = match plain { + PlainPass::Held => ("(i) the held pass", &[("the cut step", false)]), + PlainPass::Dropped => ( + "(v) a window no verify takes", + &[("begin", false), ("a drafted pass", true)], + ), + }; + let mut ok = true; + for &(after, drafted) in cases { + let mut spec = Spec38::new(fresh_draft(&s)?); + let mut out = match plain { + PlainPass::Held => start(&mut s, spec.draft_mut(), prompt)?, + PlainPass::Dropped => { + s.reset()?; + let first = Advance::prompt(&mut spec, &mut s, prompt)?; + Advance::begin(&mut spec, &s, prompt, first)?; + vec![first] + } + }; + if drafted { + one_pass(&mut s, &mut spec, &mut out)?; + } + let last = *out.last().ok_or("no token before the plain pass")?; + let d = spec.draft_mut(); + let told = (|| -> Result { + match plain { + PlainPass::Held => Draft::before_plain(d, &mut s, last)?, + PlainPass::Dropped => { + Draft::propose(d, &mut s, last, &mut [0u32; WIDTH])?; + Draft::>::unproposed(d); + } + } + let next = Target::step(&mut s, last, Want::Argmax)?.argmax(); + Draft::held(d, &mut s, last, next)?; + Ok(next) + })(); + let pos = s.pos() as usize; + let held = held_of(s.model())?; + let saved = s.model_mut().seq_save().map(|st| st.positions() as usize); + let told_ok = told.is_ok(); + if let Ok(next) = &told { + out.push(*next); + while out.len() < N { + one_pass(&mut s, &mut spec, &mut out)?; + } + } + let ids_ok = told_ok && out[..N] == plain_ids[..N]; + let saved_ok = matches!(&saved, Ok(n) if *n == pos); + println!( + "{label} after {after}: the step told {} {}; the draft store at {held} of the \ + target's {pos} {}; a snapshot there {} {}; the ids the plain run's {}", + match &told { + Ok(_) => "ok".to_string(), + Err(e) => e.to_string(), + }, + verdict(told_ok), + verdict(held == pos), + match &saved { + Ok(n) => format!("{n} positions"), + Err(e) => e.to_string(), + }, + verdict(saved_ok), + verdict(ids_ok) + ); + ok &= told_ok && held == pos && saved_ok && ids_ok; } - let ok = out[..N] == plain[..N]; - println!( - "(i) the held pass after the cut step: {} ids, the plain run's {}", - out.len(), - verdict(ok) - ); Ok((s.into_model(), ok)) } @@ -5863,11 +5984,16 @@ mod gate { ok &= c_ok; ok &= arena_holds(&mut m, &prompt)?; tier::sc("(p) a partial accept's records")?; + let (m, partial_ok) = partial_state(m, &prompt, draft_kv)?; + ok &= partial_ok; let (model, p_ok) = kept_state(m, &prompt)?; ok &= p_ok; tier::sc("(i) the width chooser's held pass after the server's cut step")?; - let (model, i_ok) = held_after_cut(model, &prompt)?; + let (model, i_ok) = chooser_plain(model, &prompt, PlainPass::Held)?; ok &= i_ok; + tier::sc("(v) a window no verify takes: no stale walk, the draft store at the step")?; + let (model, v_ok) = chooser_plain(model, &prompt, PlainPass::Dropped)?; + ok &= v_ok; tier::sc("(x) a resume of a state without the draft's side is refused")?; let (model, x_ok) = resume_refused(model, &inputs, ub, &prompt)?; ok &= x_ok; diff --git a/crates/gpu/src/arch/qwen3moe/mtp38.rs b/crates/gpu/src/arch/qwen3moe/mtp38.rs index b1c033685..a99efe8f5 100644 --- a/crates/gpu/src/arch/qwen3moe/mtp38.rs +++ b/crates/gpu/src/arch/qwen3moe/mtp38.rs @@ -1711,6 +1711,11 @@ impl Mtp38 { p.copy_from_device_async(&w_p, stream)?; walks.push(m); } + // The own rows are the proposal's, and no later walk reads them: the + // store holds the sequence through the refresh, where the next + // refresh starts at or below its end (a verify's kept rows, or the + // step's row when no verify takes the proposal). + self.held = end; // The last walk's fault word and site mask close the chain: the word // is the card's own, so a fault any walk of the chain raised is on // it (the argmax kernel copies it after its launch).