Repository navigation
Conversation
There was a problem hiding this comment.
Verdict: MUST_FIX: two additions duplicate existing owners, and a new method has broader visibility than its callers need.
Must fix
1. Share head-128 host launch orchestration
crates/gpu/src/flash_gqa.rs:4631 duplicates crates/gpu/src/flash_gqa.rs:4550: the shape checks, grid conversions and merge are the same facts (R14). Only group width and segment entry differ.
Cost: future bounds fixes need two edits.
Replace both methods. Existing calls use enqueue_pass::<GROUP> (import GROUP in scratch); the group-5 call uses enqueue_pass::<GROUP_5> with a final false argument. Update the GROUP_5 doc link to enqueue_pass.
/// Enqueue head-128 attention; refused counts fault and yield NaNs.
pub fn enqueue_pass<const PACK: usize>(
&self, stream: &CudaStream, a: GqaArgs<'_>, mma: bool,
) -> Result<(), GpuError> {
const { assert!(PACK == GROUP || PACK == GROUP_5) };
let what = "flash_gqa::enqueue";
if PACK == GROUP_5 && mma {
return Err(GpuError::shape(what, "group 5 requires the scalar pass"));
}
if a.n_kv == 0 || a.ctx == 0 || a.m == 0 {
return Err(GpuError::shape(what, format!(
"n_kv={}, ctx={}, m={} must be positive",
a.n_kv, a.ctx, a.m,
)));
}
let n_head = a.n_kv * PACK;
let lens = [
("q", a.q.len(), a.m * n_head * HEAD),
("kc", a.kc.len(), a.n_kv * a.ctx * HEAD),
("vc", a.vc.len(), a.n_kv * a.ctx * HEAD),
("n_keys", a.n_keys.len(), a.m),
("part_v", a.part_v.len(), partials_v_len(a.m, n_head)),
("part_ms", a.part_ms.len(), partials_ms_len(a.m, n_head)),
("y", a.y.len(), a.m * n_head * HEAD),
];
if let Some((name, got, need)) = lens.iter().find(|(_, got, need)| got < need) {
return Err(GpuError::shape(what, format!("{name}.len() {got} < {need}")));
}
let grid = launch_u32(what, "grid", a.m * a.n_kv * SEGMENTS)?;
let merge_grid = launch_u32(what, "merge grid", a.m * n_head)?;
let heads = launch_u32(what, "n_head", n_head)?;
let n_kv = launch_u32(what, "n_kv", a.n_kv)?;
let ctx = launch_u32(what, "ctx", a.ctx)?;
let segs = launch_u32(what, "segs", SEGMENTS)?;
let seg_keys = launch_u32(what, "seg_keys", SEG_KEYS)?;
let m = launch_u32(what, "m", a.m)?;
let cfg = LaunchConfig1D::new(grid, THREADS_U32, 0);
if PACK == GROUP_5 {
let prep = self.module.prepare_gqa_flash_seg_g5(cfg)?;
self.module.gqa_flash_seg_g5(
stream, &prep, a.q, a.kc, a.vc, a.n_keys, a.scale,
n_kv, ctx, segs, seg_keys, m, a.part_v, a.part_ms,
)?;
} else if mma {
let prep = self.module.prepare_gqa_flash_seg_mma(cfg)?;
self.module.gqa_flash_seg_mma(
stream, &prep, a.q, a.kc, a.vc, a.n_keys, a.scale,
n_kv, ctx, segs, seg_keys, m, a.part_v, a.part_ms,
)?;
} else {
let prep = self.module.prepare_gqa_flash_seg(cfg)?;
self.module.gqa_flash_seg(
stream, &prep, a.q, a.kc, a.vc, a.n_keys, a.scale,
n_kv, ctx, segs, seg_keys, m, a.part_v, a.part_ms,
)?;
}
let prep = self.module.prepare_gqa_flash_merge(
LaunchConfig1D::new(merge_grid, MERGE_THREADS, 0),
)?;
self.module.gqa_flash_merge(
stream, &prep, a.part_v, a.part_ms, a.n_keys,
heads, ctx, segs, seg_keys, m, a.fault, a.y,
)?;
Ok(())
}2. Reuse the synthetic-row generator
crates/gpu-gates/src/bin/gate_qwen3moe_experts.rs:160 adds another raw-row fixture generator beside crates/gpu-gates/src/bin/gate_qwen3moe_experts.rs:470. synthetic_rows already takes the quant type, block count and half-field offsets (R14).
Cost: remove the extra fixture owner now. Delete q5_0_bytes and its f32_to_f16_bits import. Replace the call at line 189 with:
let bytes = synthetic_rows((GgmlType::Q5_0, &[0]), (ROWS, K / 32), 0x2545_f491)?;This seed still detects both the low-half read and dropped-window mutants.
3. Keep the store-length method within the crate
crates/gpu/src/linear/mod.rs:187 exports lens, but all five callers are inside bloomery-gpu. R13 requires crate visibility here.
Cost: narrow it before external callers depend on it. Replace the declaration:
pub(crate) fn lens(&self, lanes: usize) -> [usize; 2] {Non-blocking
⚠ Add/move proof: crates/gpu/src/flash_gqa.rs:4631. Attach PTX comparisons against base 13531b6c: existing entries unchanged, three additions. Include gate commands/output and nvidia-smi; the body gives summaries.
Checked
Duplicates: both cited pairs read; the fixes reduce code and owners. The dryer kernel pair shares fold_tile.
Complexity: diagnostics flag three new orchestration functions and one kernel; gather/run gain one arm/call.
Silent failure: Q5_0 invalid ids raise TokenId and write NaNs.
Tests: replacement-fixture host emulation matches dequantization and rejects both addressing mutants.
Gates: dated unsafe PIN; numeric bands unchanged.
Escapes: none.
Notes
GPU gates not run here; card evidence incomplete.
Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_qwen3moe_experts.rs, crates/gpu-gates/src/bin/gate_qwen3moe_flash.rs, crates/gpu/src/arch/qwen3moe/body35.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)
| const ROWS: usize = 4; | ||
| // 34 blocks: past the first 32-block window of the packed code plane. | ||
| const K: usize = 1088; | ||
| let bytes = q5_0_bytes(ROWS, K); |
There was a problem hiding this comment.
Reuse synthetic_rows with the Q5_0 scale at byte 0; host emulation confirms this seed rejects both addressing mutants.
| let bytes = q5_0_bytes(ROWS, K); | |
| let bytes = synthetic_rows((GgmlType::Q5_0, &[0]), (ROWS, K / 32), 0x2545_f491)?; |
| /// [`crate::recur::RecStore::new`] allocates. The ring is not multiplied | ||
| /// by `lanes`. | ||
| #[must_use] | ||
| pub fn lens(&self, lanes: usize) -> [usize; 2] { |
There was a problem hiding this comment.
All callers are inside bloomery-gpu; R13 keeps this method crate-visible.
| pub fn lens(&self, lanes: usize) -> [usize; 2] { | |
| pub(crate) fn lens(&self, lanes: usize) -> [usize; 2] { |
…dy attaches to - The recurrent store (RecStore, LayerStore) leaves arch/qwen3moe for gpu::recur and takes its length pair; LinearShape keeps the method that gives its pair, so the GDN bodies size their stores as before. - A Q5_0 embedding row (embed_rows_q5_0) over pack_q5_0's layout; embed_into admits Q5_0. - relu_sqr beside swiglu. - A flash GQA decode entry at head 128, group 5, with its GqaInst row. - Coverage names the rows that are now available. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…h entry at spill 0 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…uses gather; LayerStore stays qwen3moe's Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… coverage keeps the group-5 row off the Qwen bodies Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rator enqueue_pass takes the group width. The Q5_0 embed fixture is synthetic_rows at the scale's byte. LinearShape::lens stays in the crate.
gqa_flash_seg and gqa_flash_seg_g5 are thin entries over seg_scalar_g. The group of eight still uses every warp of the 256-thread block; a group of five leaves the pad warps to stage the key and value tile.
A nested const item cannot name seg_scalar_g's generic parameter, so the group-8 predicate is an inline const. The thin entries no longer mark the partial slices mut; the core still writes them.
The shared body is inlined into the entry, so it does not see the launch bound. The thread index is limited to the block, and a full group's (m, s) store keeps the lane test on its own.
A group of eight fills the 256-thread block, so the launch bound has to see the thread index in the entry. The short group still calls the shared core.
seg_scalar_g is only the five-head entry. The embedding comment names the types the load admits, and Q5_0 rows as the branch for a body that admits them.
GqaArgs sizes the query and output rows by PACK, and enqueue_pass keeps the two-launch contract: a bad count faults, and the merge grid is PACK.
The squared ReLU kernel, its gate and its spill rows were an add with no caller. They return in the commit that first calls them.
seg_scalar_g is an unsafe fn, and the group-8 body kept in its entry leaves gpu at 1787 blocks and 196 fns on main's 1771. The relu drop's pin line was short of that count.
There was a problem hiding this comment.
Verdict: CLEAN: No reachable failure or blocking contract defect remains in the changed code.
Non-blocking
⚠ Refresh evidence for the reviewed head: crates/gpu/src/flash_gqa.rs:4599.
enqueue_pass adds group-5 dispatch. The body reports 9ae563e0/f13503ce; this head is 21e0ea14, and just narrow did not finish.
For the add/move proof required by AGENTS.md, attach completed base/head PTX comparisons showing exactly the two added entries and unchanged existing entries, plus the card line, model file, exact gate commands and output for this head.
Checked
Duplicates: dryer reported 0 candidate pairs. The scalar entries retain separate bodies under R12 and the documented thread-index range constraint; the recurrent store moves to one common owner.
Complexity: diagnostics flag embed_q5_0 (22), its launcher (18), and device core seg_scalar_g (17). enqueue_pass 20→25 adds group dispatch; gather 16→17 and run 67→68 add one case/clause.
Silent failure: the Q5_0 invalid-ID branch raises TokenId and writes NaNs. Absent stamps contribute zero allocated bytes; restamping an unstamped store returns an error.
Tests, static inspection: the Q5_0 gate compares dequantized bits across 34 blocks, eager/graph outputs, positions, and invalid-ID faults. The group-5 gate checks two KV planes against its reference. Shape tests assert group-5 selection and refusal details; coverage tests reject group 5 for Qwen bodies. No mutants were executed during validation.
Performance: enqueue_pass retains one segment launch and one merge launch; the group-5 engine path has no caller in this tree.
Gates: unsafe ratchet increase has a dated PIN and reason; no band or tolerance relaxed.
Escape catalog: C1–C4 do not match this diff.
Notes
GPU kernels and Rust gates were not executed during validation. The supplied summaries lack the complete card evidence required for gpu.json.evidence=true.
Card check: needs-gpu. This pull request changes crates/gpu-gates/src/bin/gate_qwen3moe_experts.rs, crates/gpu-gates/src/bin/gate_qwen3moe_flash.rs, crates/gpu/src/arch/qwen3moe/body35.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)
Part of #156 (Nemotron 3 Nano 4B), step N7a. Head
9ae563e0, parentf13503ce, base origin/maineccb1e69. The common pieces the Nemotron-H card body (N7b) attaches to. No engine caller yet; each piece is an "add" (or, for the store, a "move") in AGENTS.md's table.What
gpu::recurholds the recurrent store (RecStore,LayerStore), moved out ofarch/qwen3moe/scratch.rs.RecStore::new/stampedtake the length pair[state_f32, ring_f32].LinearShape::lensgives the GDN pair, so Body35, Body38 and the delta path size their stores as before. A Mamba-2 layer will pass its own pair.embed_rows_q5_0: the embedding gather overpack_q5_0's resident layout (Nemotron'stoken_embdis Q5_0).dispatch::embed_intoadmits Q5_0, and an id past the table raisesTokenIdand writes a NaN row, as the other embed rows do.relu_sqr, besideswiglu:max(x, 0)^2. A non-finite input stays non-finite.gqa_flash_seg_g5: the flash GQA decode entry at head 128, group 5 (Nemotron's 40 Q / 8 KV heads). It has itsGqaInstrow, soselect_gqatakes group 5 instead of refusing it.Program::NemotronHBodyyet; N7b adds it.공통화한 것 (Commonized)
LayerStorearm in qwen3moe.dispatch::layer,site_gemvneedsQ35Kernels). N7b decides between the common walk with math-kind arms and a body-owned three-armlayer()from that note. This PR lifts no walk.Gates
Cards: RTX 3090 and RTX A6000, driver 615.71.09, CUDA 13.4. Base origin/main eccb1e6, head 9ae563e.
gate-gpu-qwen3moe-expertsat 9ae563e: exit 0. Q5_0 embed K=1088 (34 blocks, two code windows) bit-identical todequant_row, out-of-range ids NaN;relu_sqrbit-identical; one captured node each.gate-gpu-qwen35moe-e2eat 9ae563e (A6000): exit 0 (the RecStore move keeps the GDN bodies' bits).gate-gpu-qwen3moe-flashat f13503c (3090): exit 0;flash group5 head=128 group=5 kv=2 keys=96: measured/bound 2.911e-2 PASS. From f13503c to 9ae563e no kernel changed: SAFETY comments,r * ras the arm's value, a destructure, coverage and the ratchet.gate-ptx-spillat f13503c:generate_ds41305/305,gate_e2e238/238,gate_qwen3vl_tower16/16, violations 0. That is the base's entries plus exactly the three new ones.check-unsafe: ok after the dated PIN (gpu 1769 → 1786, the three new kernels' blocks, each with its SAFETY line). clippy onbloomery-gpu: 0 errors.tools/check-recipes.shon Linux: rc 0. Mac static: lint and check-unsafe green; check-recipes red only on main'stools/review/local.pyself-test (a MUST_FIX review ends 1 (got 70)), which this PR does not touch.just narrow origin/main: not finished; the box connection dropped twice (rc 255).FAIL-first (through
tools/mutant-run.sh, each mutant alone)word_i = j / 4):embed Q5_0 … FAIL, killed.wbase = b & 31):embed Q5_0 K=1088 … bit_identical=false … FAIL, killed.relu_sqr … FAIL, killed.h0 / 8): the flash group-5 clause FAIL, killed.gate-gpu-qwen35moe-e2ered, killed. The clean run after it is green.gate-ptx-spillbefore pinning: red on exactlyembed_rows_q5_0,gqa_flash_seg_g5andrelu_sqr(unpinned, spill 0, jit_local 0) ingenerate_ds41andgate_e2e; every other entry pinned,violations=3.🤖 Generated with Claude Code