Repository navigation
app, gpu, gpu-gates: a window no verify takes leaves no stale draft state - #631
Conversation
There was a problem hiding this comment.
Verdict: MUST_FIX: the shorter draft store conflicts with three existing gate checks.
Must fix
1. Update the gates for a verify that keeps multiple rows
crates/gpu/src/arch/qwen3moe/mtp38.rs:1718 leaves held = p0 + 1. A verify that matches the first proposal and rejects the second keeps two rows, leaving the target at p0 + 2 [derived]. The commit calls Mtp38::cut, which only reduces held; runtime::generate can then reach its token limit without another refresh. Whole-state restore preserves that saved count. The equality checks in crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs:2392, crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs:2653 and crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs:2797 then reject a valid run. Before this assignment, a two-row acceptance cut the longer store to the target and passed these checks.
Update these contracts together now, before a run with partial acceptance reports a false failure. Keep the production assignment. Apply these replacements in the gate file and update the descriptions that promise equal store and target positions:
@@ kept_state
- let cut = held == at;
+ // PIN(2026-10-21): the refresh ends at p0 + 1; k kept rows end at p0 + k.
+ let cut = kept.rows.last().is_some_and(|&k| held + k - 1 == at);
@@ prefix_draft
let (mut m, first, point_row) = drafted_run(m, prompt, N)?;
+ let saved_held = held_of(&m)?;
let state = m.seq_save()?;
@@ prefix_draft
- let contrast = whole_held == n as usize && whole_pos == n;
+ // PIN(2026-10-21): a whole restore preserves the saved count, which can trail n.
+ let contrast = whole_held == saved_held && whole_pos == n;
@@ lane_save
- let (state, held, whole_ok) = lane_round(&mut m, prompt, kv_bytes, ("whole", 1))?;
- let whole_ok = whole_ok && held == n as usize;
+ // PIN(2026-10-21): lane_round checks the restored count against the saved count.
+ let (state, _, whole_ok) = lane_round(&mut m, prompt, kv_bytes, ("whole", 1))?;Non-blocking
⚠ Refresh the gate evidence: crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs:5944. The reported runs name head 0eaa737 on 374273d; this review covers 5569395 on b63aac1. Attach output for the reviewed head or establish source identity, so the card result can verify this revision.
Checked
Traced the changed chain through acceptance, commit, generation stop, snapshot save and whole restore. lane_round already checks the restored held count against the saved count. The new chooser clause counts step and snapshot errors as failures. No tolerance or lint moved. Escape catalog C1–C4 do not match; no ledger lessons are in force.
Notes
Builds and card gates were not rerun during this review.
Card check: needs-gpu. This pull request changes crates/app/src/mtp/policy.rs, crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs, crates/gpu/src/arch/qwen3moe/mtp38.rs and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
…tate The MTP window's begin turned the prompt call's last unit into the next window's anchor but kept the unit too. A chain takes the anchor; when no verify then took the chain's proposal (the width chooser's shadow, or the window its closing gate cuts to no id), the step after it found that unit still waiting and walked the prompt's last row again at the step's position. Right after the prompt that walk read the right row; after a drafted pass it read the prompt arena at positions it no longer held, and Mtp38 refused the walk by name. generate_qwen3moe exited 1 that way on a drafted row after a PRIME in the same process (prose-ko prose:4096): a warm chooser can cut a window to no id before its first plain pass, which would have taken the unit. begin now takes the unit; the policy is the shared one, so GLM's NextN draft gets the same fix. The same window left the chain's own rows in the draft's store past the target's position, which a sequence state refuses to save. They are the proposal's and no later walk reads them, so Mtp38's chain now leaves the store at its refresh's end. policy: a dropped window after begin and after a drafted pass walks its chain alone and leaves the store at the target; the call sequences add the dropped window (27304 sequences). gate_qwen4exp_mtp (v): the same two windows on the card, the step told, the store at the target's position, a snapshot there, the ids the plain run's; (i) runs through the same pass and now checks the store and the snapshot too.
5569395 to
0983737
Compare
There was a problem hiding this comment.
Since the last review:
- Must-fix 1 fixed:
kept_state,prefix_draftandlane_savenow check the shorter store count through crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs:2377. - Evidence question answered: the PR supplies the card and driver, commit hashes, model files, commands and output, and states that the rebased patch is identical.
Verdict: CLEAN: the gate corrections preserve both saved positions and exercise partial acceptance without weakening the refusal checks.
Checked
Duplicates: 6 new items checked in the interdiff; no duplicate owner found. Existing store and refusal helpers are reused. Dryer reported no pairs; gpu-gates was searched manually because dryer excludes it.
Complexity: partial_state (28) is a short setup/verify/save/restore pipeline. run (92→95) adds a linear gate case; prefix_draft (58→59) adds a state read; kept_state (18→19) adds the expected-position check. chooser_plain (30) is unchanged since the previous review. No complexity finding survives.
Silent failure: 3 result paths followed. Invalid DraftEnd::after arithmetic fails the comparison; synchronous and lane save errors reach the named-refusal checks and the final verdict.
Tests (static mutation traces): dropping the store reset at crates/gpu/src/arch/qwen3moe/mtp38.rs:1718 fails the forced partial-accept shape check. Restoring the target count at crates/gpu/src/arch/qwen3moe/body38.rs:3068 fails that case's whole-state comparison. Removing the refusal at crates/gpu/src/arch/qwen3moe/body38.rs:2931 fails past_refused. The PR reports reverted-line failures for both production fixes and the probe setup.
Gates: the changed position contracts have dated PIN reasons. Exact saved-count and store-plane comparisons remain. No tolerance or lint moved.
Adversarial checks: traced partial acceptance through commit and both restore paths; checked a store behind the target, a store past it, context-end chain bounds and continuation after a dropped window. Guards or gate assertions block the candidate failures.
Performance: the interdiff changes gate code only. The production fixes add no device launch, copy, synchronization or floating-point reduction.
Escape catalog: C1–C4 do not match. No ledger lessons are in force.
Notes
Builds and card gates were not rerun during this review. Reported card runs use the fixture tier; the PR explains the unavailable real-tier reference set.
Card check: gpu-evidence. This pull request changes crates/app/src/mtp/policy.rs, crates/gpu-gates/src/bin/gate_qwen4exp_mtp.rs, crates/gpu/src/arch/qwen3moe/mtp38.rs and carries a gate run on the author's card, which counts as verified.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
|
The review is done and the contribution check passed, so I'm merging this. |
Qwen3.8 MTP 초안(draft)이 검증 없이 버려진 window 뒤에 낡은 상태를 남겨, 다음 step이 이미 없는 위치를 읽다가 거절되던 문제를 고치는 작업입니다.
What and why
An MTP draft window that no verify takes used to leave two pieces of stale state behind. This PR clears both, so the step after such a window walks the right row and a sequence state can be saved again.
Policy::beginturns the prompt call's last unit into the first window's anchor, but it kept the unit as well. The chain takes the anchor. When no verify then takes the chain's proposal (the width chooser's shadow, or a window its closing gate cuts to no id), the step after it still finds that unit waiting and walks the prompt's last row again at the step's position. Right after the prompt that walk reads the correct row. After a drafted window it reads the prompt's Ubatch arena at positions the arena no longer holds, andMtp38refuses the walk by name.generate_qwen3moeexited 1 this way on a drafted prose-ko row (prose:4096) that ran after a PRIME arm in the same process, because the warm width chooser can cut a window to no id before its first plain step would have taken the unit.beginnow takes the unit with.take(). The policy is shared, so the GLM NextN draft gets the same fix.The same window also left the chain's own rows in the draft store past the target's position, and a sequence state refuses to save in that case. Those rows belong to the proposal and no later walk reads them, so
Mtp38::run_chainnow ends with the store at its refresh's end.Release notes: Fixed: an MTP draft window that no verify takes no longer leaves a stale prompt row or draft store position, which stopped
generate_qwen3moewith an arena refusal and blocked saving a sequence state.Code shape
.take()in the shared policy and one assignment inMtp38::run_chain. The gate keeps the position contract in one place,DraftEnd, and its two dropped windows share one helper,chooser_plain, which replacesheld_after_cut. The two refusal probes that each walked the store on their own are one helper,past_refused.held_after_cutingate_qwen4exp_mtpis folded intochooser_plain, and the duplicateheld_ofclosure inlane_roundis removed.kept_state,prefix_draftandlane_savenow check the exact relation, with a dated PIN. The refusals and the other cases stay, now behindpast_refused.Duplication found or removed (
path:line): theheld_ofclosure above, and the separate store walks inkept_stateandlane_refused, whichpast_refusedreplaces.Rig::chainandRig::stepin the policy tests are the two halves of the olddrafted_passandplain_stepbodies, and each has more than one caller.What I ran
Head
098373737584967ad8006020e43892eef1b84201, one commit on upstream228bc803d9ecff24d9e3db5f2968e82c13691a3b. The GPU gates below ran on the same commit on upstream687b0f94; the rebase left the commit unchanged, and main touched none of its files in between.Tests added in
crates/app/src/mtp/policy.rs:a_dropped_window_walks_its_chain_aloneruns a dropped window afterbeginand after a drafted window. The call-sequence testno_call_sequence_leaves_the_store_past_the_target_or_refusesnow includes the dropped window.The gate
gate_qwen4exp_mtpgains clause (v): the same two windows on the card, with the step told ok, the store at the target's position, a snapshot there and the ids the plain run's. Clause (i) runs through the same helper and also checks the store and the snapshot.A partial accept leaves the draft store behind the target on purpose. After a verify that keeps the first proposal and rejects the second, the store ends at
p0+1and the target atp0+2, andput_draft_rowsrestores the saved store position whole. The gate used to require the two to be equal after such a window. It now takes both positions fromDraftEnd. The new casepartial_stateruns a real chain, a real verify with two forced picks, a real accept and the target commit, then restores the snapshot and the save lane and compares both positions and the store planes with the saved ones.The gate also checks that a state saved past the position is refused.
past_refusedfills the store from position 0 up ton + 1, so it holds one position more than the target, and then expects the save on the engine stream and the save on a lane to be refused by name.kept_state,lane_saveandpartial_statecall it. After a partial accept the store already sits behind the target, and a probe that walked from the target's position was refused before any save was tried. Filling from 0 avoids that.just gate-apppasses 26 tests.just gate-gpu-glm5next-mtpexits 0 (gate_glm5next_mtp: clauses: 14 ran, 7 left to the real tier).GPU evidence
The card gates ran on a DGX Spark with the fixture tier, because the real tier's reference set was not available for this run. The change adds no kernel, launch, copy or sync, so I took no timing and claim no speed change.
The runs used the same patch as this head before a docs-only rebase onto upstream
687b0f94e6b0ca828bae16640aaccb9a3b01efc0(the range-diff between the two is empty).Models: the Qwen MTP gate runs on
/models/fixtures/qwen38/qwen38-fixture-00001-of-00001.gguf(SHA256d8c7a0cd40b40b656019209be278f38581a06cff2de33c2b9af3fe61fa022a3d), a fixture derived fromQwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf, with the draft file/models/fixtures/qwen38/mtp-Qwen3.8-Flash-Next-shared-Q8_0.gguf(SHA25638625d840c292582f3211a424aeb2bc4cd4547dc9a8449370d85ed92fa7df0b1). The GLM MTP gate runs on/models/fixtures/glm5next/glm53-fixture-00001-of-00003.gguf, the first file of the three-file fixture.gate-appopens no model file.Gates:
Reverted fix lines
On the same patch, I reverted one fix line at a time on the card machine, ran the check that guards it and restored the line. The results are below.
past_refusedfills the store from 0 (back to a walk from the target's position)just gate-gpu-qwen4exp-mtpgate_qwen4exp_mtp: qwen4exp Mtp38: a walk from position 66: the draft's store holds this sequence's positions below 65, and the walk would read the keys between.take()inPolicy::beginjust gate-apptest result: FAILED. 24 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s, the two tests beinga_dropped_window_walks_its_chain_alone(policy.rs:1122) andno_call_sequence_leaves_the_store_past_the_target_or_refuses(policy.rs:1608).take()inPolicy::beginjust gate-gpu-qwen4exp-mtp(v) a window no verify takes after a drafted pass: the step told qwen4exp Mtp38: rows 63..64 of the Ubatch arena as positions 64..65 for a walk from 65: the arena holds positions 0..64 FAILself.held = end;inMtp38::run_chainjust gate-gpu-qwen4exp-mtp(p-partial) kept 2, target 66 (p0 64), store 66; shape FAIL, kept_state FAILandthe draft store at 68 of the target's 66 FAILWhat I ran into
This does not block the PR. The crash behind the first fix does not show on every run. It depends on whether the first width 0 step after a PRIME is a forced probe (which takes the stale unit) or a window cut by cost (which does not), and cost comes from timing. The tests above set that condition directly instead of waiting for it. The fixture gates also do not measure how often a real model accepts only part of a window.
Hardware
A DGX Spark (NVIDIA GB10, driver 595.84, CUDA 13.3) for
gate-app, both card gates and the four reverted-line runs.AI help
AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.