Skip to content

app, gpu, gpu-gates: a window no verify takes leaves no stale draft state - #631

Merged
juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:dgx-spark-mtp-draft-state
Oct 22, 2026
Merged

juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:dgx-spark-mtp-draft-state

Conversation

@juliankang4

@juliankang4 juliankang4 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

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::begin turns 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, and Mtp38 refuses the walk by name. generate_qwen3moe exited 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. begin now 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_chain now 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_qwen3moe with an arena refusal and blocked saving a sequence state.

Code shape

  • No second copy. The fix is a .take() in the shared policy and one assignment in Mtp38::run_chain. The gate keeps the position contract in one place, DraftEnd, and its two dropped windows share one helper, chooser_plain, which replaces held_after_cut. The two refusal probes that each walked the store on their own are one helper, past_refused.
  • Model code holds only architecture facts. The policy change is shared by Qwen3.8 and GLM NextN and adds no per-model branch. The store assignment sits in the Qwen3.8 draft body, the draft that appends its own rows to a store.
  • Dead code is gone. held_after_cut in gate_qwen4exp_mtp is folded into chooser_plain, and the duplicate held_of closure in lane_round is removed.
  • No silent failure. The refusals are unchanged. The new gate clauses print the refusal text and count it against the clause.
  • Comments state what is true now.
  • No relaxed gate. No band, tolerance or lint moved. The gate's old store-equals-target check after a partial accept was wrong, so kept_state, prefix_draft and lane_save now check the exact relation, with a dated PIN. The refusals and the other cases stay, now behind past_refused.

Duplication found or removed (path:line): the held_of closure above, and the separate store walks in kept_state and lane_refused, which past_refused replaces. Rig::chain and Rig::step in the policy tests are the two halves of the old drafted_pass and plain_step bodies, and each has more than one caller.

What I ran

Head 098373737584967ad8006020e43892eef1b84201, one commit on upstream 228bc803d9ecff24d9e3db5f2968e82c13691a3b. The GPU gates below ran on the same commit on upstream 687b0f94; 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_alone runs a dropped window after begin and after a drafted window. The call-sequence test no_call_sequence_leaves_the_store_past_the_target_or_refuses now includes the dropped window.

The gate gate_qwen4exp_mtp gains 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+1 and the target at p0+2, and put_draft_rows restores the saved store position whole. The gate used to require the two to be equal after such a window. It now takes both positions from DraftEnd. The new case partial_state runs 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_refused fills the store from position 0 up to n + 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_save and partial_state call 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-app passes 26 tests. just gate-gpu-glm5next-mtp exits 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).

| NVIDIA-SMI 595.84                 Driver Version: 595.84         CUDA Version: 13.3     |
|   0  NVIDIA GB10                    On  |   0000000F:01:00.0 Off |                  N/A |

Models: the Qwen MTP gate runs on /models/fixtures/qwen38/qwen38-fixture-00001-of-00001.gguf (SHA256 d8c7a0cd40b40b656019209be278f38581a06cff2de33c2b9af3fe61fa022a3d), a fixture derived from Qwen3.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 (SHA256 38625d840c292582f3211a424aeb2bc4cd4547dc9a8449370d85ed92fa7df0b1). The GLM MTP gate runs on /models/fixtures/glm5next/glm53-fixture-00001-of-00003.gguf, the first file of the three-file fixture. gate-app opens no model file.

Gates:

BLOOMERY_BOX_LOCAL=1 just gate-app
test result: ok. 26 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.02s
BLOOMERY_BOX_LOCAL=1 BLOOMERY_TIER=fixture just gate-gpu-qwen4exp-mtp
(f) a snapshot of a store past the position: qwen4exp snapshot: a state at 66 of a draft store that holds 67 positions PASS
(f) a lane save of a store past the position: qwen4exp snapshot: a state at 66 of a draft store that holds 67 positions PASS
(p-partial) kept 2, target 66 (p0 64), store 65; shape PASS, kept_state PASS, prefix_draft PASS, lane_save PASS, past_refused PASS
(i) the held pass after the cut step: the step told ok PASS; the draft store at 66 of the target's 66 PASS; a snapshot there 66 positions PASS; the ids the plain run's PASS
(v) a window no verify takes after begin: the step told ok PASS; the draft store at 65 of the target's 65 PASS; a snapshot there 65 positions PASS; the ids the plain run's PASS
(v) a window no verify takes after a drafted pass: the step told ok PASS; the draft store at 66 of the target's 66 PASS; a snapshot there 66 positions PASS; the ids the plain run's PASS
clauses: 26 ran, 4 left to the real tier
gpu-gate.sh: gate_qwen4exp_mtp ran 288 s (exit 0)
BLOOMERY_BOX_LOCAL=1 BLOOMERY_TIER=fixture just gate-gpu-glm5next-mtp
gate_glm5next_mtp: clauses: 14 ran, 7 left to the real tier
gpu-gate.sh: gate_glm5next_mtp ran 31 s (exit 0)

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.

Line reverted Check Result
past_refused fills the store from 0 (back to a walk from the target's position) just gate-gpu-qwen4exp-mtp exit 1: gate_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() in Policy::begin just gate-app test result: FAILED. 24 passed; 2 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s, the two tests being a_dropped_window_walks_its_chain_alone (policy.rs:1122) and no_call_sequence_leaves_the_store_past_the_target_or_refuses (policy.rs:1608)
.take() in Policy::begin just gate-gpu-qwen4exp-mtp exit 1: (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 FAIL
self.held = end; in Mtp38::run_chain just gate-gpu-qwen4exp-mtp exit 1: (p-partial) kept 2, target 66 (p0 64), store 66; shape FAIL, kept_state FAIL and the draft store at 68 of the target's 66 FAIL

What 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 초안 리뷰는 제가 직접 했습니다.

@github-actions github-actions Bot added gpu The card engine: crates/gpu*, crates/app needs-gpu Changes code a card must check, and carries no gate run on a card labels Oct 21, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.
@juliankang4
juliankang4 force-pushed the dgx-spark-mtp-draft-state branch from 5569395 to 0983737 Compare October 22, 2026 00:09

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the last review:

  • Must-fix 1 fixed: kept_state, prefix_draft and lane_save now 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)

@github-actions github-actions Bot added gpu-evidence Verified on the author's card: the pull request carries the gate run and its output and removed needs-gpu Changes code a card must check, and carries no gate run on a card labels Oct 22, 2026
@juliankang4

Copy link
Copy Markdown
Collaborator Author

The review is done and the contribution check passed, so I'm merging this.

@juliankang4
juliankang4 merged commit 8a4151f into midagedev:main Oct 22, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gpu The card engine: crates/gpu*, crates/app gpu-evidence Verified on the author's card: the pull request carries the gate run and its output

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant