Repository navigation
Conversation
ExpertForm::Relu2 is an arm of the common host union beside the gated SwiGLU form: one stack where the gated form reads two (a UnionPass::Up first pass, one column a slot), the combine relu(up)², and the layer's expert width its own. HostLayer::build_relu2 takes a Relu2Spec (up, down, expert count, embd, ff); stacks() and layouts() carry the up again in the gate's slot, and form() names the form. A relu² layer with an NVMe tier is refused by name (TierError::Relu2Tiered). The union scratch's expert width is a capacity: a layer's ff at or below it, with room for its combine column, is served bit for bit as through a scratch made for it; a wider layer or another embd is refused by name. One scratch then serves a run's layers of differing widths. arch::nemotron_h_moe::host::layer wires a Bolt layer's stacks in the latent width (None for a layer that does not route). Gates: the union gate's relu² arm (synthetic IQ2_XXS layers of two widths, union = experts_into = a transcription on qdot's calls, bit for bit, both deferral arms, in the dispatches owed; the widths' refusals), and gate-nemotron-h-moe-host on the Bolt file's own layers. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
hw_group_into_matches_allocating_entries pins GroupInput::Relu2 through the allocating and _into entries at ne1 1, 6 and 10 under both deferral arms, and through matmul_q_group_cols_into at ten columns, the one entry that produces combines column by column (InputCombine::col_into). A mutant of that arm survived the union gate and the ne1 1 and 6 clauses before the ten-column clause. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict: MUST_FIX: relu² hides invalid activations; the change also duplicates a model identity and adds redundant parameters.
Must fix
1. Reject non-finite values before applying relu²
crates/model/src/ops.rs:1643: matmul_q_group_swiglu accepts a shape-valid GroupInput::Relu2 tensor containing NaN or negative infinity. This loop turns either into zero before quantization. With otherwise zero activations and finite down weights, the call returns zero instead of the named failure required by AGENTS.md.
Fix the shared combine now, before every host execution path inherits this behavior. Add a refusal clause for NaN and both infinities.
fn relu2_into(up: &[f32], out: &mut [f32]) {
for (o, &u) in out.iter_mut().zip(up) {
assert!(u.is_finite(), "relu2: non-finite activation value {u}");
let r = u.max(0.0);
*o = r * r;
}
}2. Reuse the reference model path
crates/model/tests/nemotron_h_moe_host.rs:32 repeats crates/refset/src/arch/nemotron_h_moe/mod.rs:22. The new gate creates another owner for the same model identity.
Reuse the owner now so a model-path change does not require another coordinated edit. Delete the local constant and import:
use refset::arch::nemotron_h_moe::MODEL as FILE;3. Remove the redundant sizing parameters
crates/model/src/ops.rs:5046 takes slots and ff, but both callers pass dimensions already owned by this UnionCall. These new parameters add no choice.
Remove them while there are only two call sites. Replace both calls with self.gu_cells() and the definition with:
fn gu_cells(&self) -> usize {
let cells = self.plan.slots() * self.up.n;
match self.form {
ExpertForm::Gated { .. } => 2 * cells,
ExpertForm::Relu2 => cells,
}
}Non-blocking
⚠ Host dispatch timing evidence: crates/model/src/ops.rs:695 adds a form selection per expert combine: experts.len() selections per served host leg/token [derived from serve_resolved at moe.rs:1055–1075]. AGENTS.md requires a same-lease A/B for dispatch changes. Attach the gated-host A/B commands and output; the reported bit and dispatch-count gates do not measure speed.
Checked
Duplicates: compared the cited model identity and helper bodies; dryer reported zero pairs.
Complexity: crapper flagged new HostLayer::build_form at 22; its branches validate stacks and resolve sources at load time.
Silent failure: traced relu² into the down projection; quantization cannot detect the masked inputs.
Tests: the PR reports FAIL-first and host gates; runtime gates were not independently run during validation.
Gates: the scratch expert width becomes a capacity.
Escape catalog: no C1–C4 patterns matched.
Notes
The PR reports CPU host-tier gates and identical PTX scans. Its selected fixture batch remains FIXTURE_BATCH_PENDING.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
Filed outside this PR: #357
| /// the caller. | ||
| fn relu2_into(up: &[f32], out: &mut [f32]) { | ||
| for (o, &u) in out.iter_mut().zip(up) { | ||
| let r = u.max(0.0); |
There was a problem hiding this comment.
Reject NaN and negative infinity before max converts them to a valid zero activation.
| let r = u.max(0.0); | |
| assert!(u.is_finite(), "relu2: non-finite activation value {u}"); | |
| let r = u.max(0.0); |
recipes.py --fill: the new recipe's cargo target reaches 14 rows' files (crates/model, models, qdot, placement, arch tables); CI's recipes-self-test and check-recipes named them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Since the last review:
The non-finite refusal remains open (item 1). This increment only updates gate selection; no source or test code changed.
Verdict: MUST_FIX: relu² still converts invalid activations into plausible output.
Must fix
1. Reject non-finite values before applying relu²
crates/model/src/ops.rs:1643: matmul_q_group_swiglu accepts a shape-valid GroupInput::Relu2 containing NaN or negative infinity. With two columns, the caller runs combine_timed → write_par → this loop without checking finiteness. max(0.0) converts either invalid value to zero before quantization can reject it. With all other activations zero and finite down weights, the call returns zero instead of the named failure required by AGENTS.md.
Fix the shared combine before more host paths inherit this behavior. Add refusal clauses to the existing gate for NaN and both infinities.
fn relu2_into(up: &[f32], out: &mut [f32]) {
for (o, &u) in out.iter_mut().zip(up) {
assert!(u.is_finite(), "relu2: non-finite activation value {u}");
let r = u.max(0.0);
*o = r * r;
}
}Checked
Duplicates: no new functions, types or constants in this increment; dryer reported no pairs.
Complexity: no function changed; the reported HostLayer::build_form complexity belongs to the earlier source changes.
Silent failure: followed one combine through the allocating group entry, quantization and down projection.
Tests: existing relu² group clauses compare outputs without asserting NaN or infinity refusal. Runtime gates were not rerun.
Gates: all 14 changed rows only append the existing host gate; prior recipes and other fields are preserved. The recipe and test target exist. No pin moved.
Escape catalog: no C1–C4 pattern matched; no ledger lessons are in force.
Notes
The PR provides host-tier CPU gate output and workstation card details. Those reported host gates ran no device kernel.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
gate-paths.tsv conflicted (a table recipes.py writes): main's rows taken, gate-nemotron-h-moe-host filled into the 14 rows again by recipes.py check --fill. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Since the last review:
Item 1 remains open at crates/model/src/ops.rs:1643. This increment merges base changes and does not change the relu² implementation or its tests.
Verdict: MUST_FIX: relu² still converts invalid activations into plausible output.
Must fix
1. Reject non-finite values before applying relu²
crates/model/src/ops.rs:1643: matmul_q_group_swiglu accepts a shape-valid GroupInput::Relu2 with two columns and reaches group_core → combine_timed → write_par → this loop without a finiteness check. max(0.0) converts NaN and negative infinity to zero before quantization can reject them. With the other activations zero and finite down weights, the call returns zero instead of the named failure required by AGENTS.md.
Fix the shared combine before more host paths inherit this behavior. Add refusal clauses to the existing gate for NaN and both infinities.
fn relu2_into(up: &[f32], out: &mut [f32]) {
for (o, &u) in out.iter_mut().zip(up) {
assert!(u.is_finite(), "relu2: non-finite activation value {u}");
let r = u.max(0.0);
*o = r * r;
}
}Checked
Duplicates: 0 new PR-specific functions, types or constants in this increment; merge additions match the base. Dryer reported 0 pairs.
Complexity: the reported HostLayer::build_form at 22 belongs to the earlier source changes; this increment does not change it.
Silent failure: 1 surviving site followed through grouped combine, quantization and down projection.
Tests: no PR-specific test changed. The group gate compares relu² against a transcription using the same max operation and has no non-finite refusal clause. Runtime gates were not rerun.
Gates: no PR-specific pin or gate clause changed in this increment.
Escape catalog: no new PR-specific C1–C4 match; no ledger lessons are in force.
Notes
The supplied raw PASS lines cover CPU host-tier gates; they do not demonstrate a device-kernel run.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
relu2_into turned a NaN, and a negative infinity, into zero through max(0.0) before the quantizer could see it. It now refuses either by name after the loop, which carries no branch; the group gate and the union gate each gain the refusal clauses (every entry, NaN and both infinities; an overflowing up row through the one-column leg and the union call). The Bolt gate takes the model path from refset (one owner), and UnionCall::gu_cells reads the call's own slots and width. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
arch::nemotron_h_moe::host::layer reads a layer's stacks through arch::names::routed_stacks (the common spelling), so the two per-family name functions are gone. The unit test builds its file with layer_file, not a copy. In the gates, seeded, diff_cells and panic_text live in tests/common (the union gate's own copies and the two copies this change had added are converted), and the union gate writes its synthetic layers through one StackFile. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
|
Review fixes are pushed on top (no rewrite):
Also in Timing note (derived, not measured): the form choice is one predictable branch per expert combine. The finiteness check costs about 0.05–0.1 µs per 2560-wide combine, under 2 % of the derived 6.6 µs host term per expert. I made no timed run; a same-lease Ten owning gates re-ran at This comment was written by a Claude agent (bot). |
There was a problem hiding this comment.
Since the last review:
Item 1 is fixed at crates/model/src/ops.rs:1646. The shared combine rejects NaN and both infinities before quantization.
Verdict: CLEAN: the earlier behavior defect is fixed, and no new must-fix defect survives review of this increment.
Non-blocking
⚠ Record the new refusal clauses' evidence: crates/model/tests/ops.rs:949 and crates/model/tests/union.rs:1869 add non-finite refusal checks. The PR body's PASS lines and mutant list predate these clauses. Attach current just gate-ops and just gate-union output, plus a FAIL-first run with the finiteness check removed. This makes the repaired contract verifiable before more callers depend on it.
Checked
Duplicates: 5 introduced functions/types checked against existing owners; shared column helpers and the stack writer consolidate copies. Dryer reported 0 pairs.
Complexity: crapper reports HostLayer::build_form at 22; its body is unchanged in this increment.
Silent failure: 5 added result/default sites followed. Discarded Results and empty panic text fail the refusal assertions; they cannot make these clauses pass.
Tests: static mutation check only. Removing ops.rs:1646 makes the new named-panic assertions fail; swapped stack names and omitted shape/tier checks remain caught by existing build assertions. Runtime gates were not rerun.
Gates: no numeric band, threshold or pin moved. New refusal clauses extend existing ignored hw_ gates.
Adversarial checks: shape validation blocks truncated inputs; quantization rejects overflow from squaring finite values; claim guards and the pool barrier release waiters and rethrow worker panics.
Escape catalog: no C1–C4 match in this increment. No ledger lessons are in force.
Notes
The supplied raw PASS lines cover CPU host-tier gates. They do not establish a device-kernel gate run.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
|
Superseded by #392. This PR conflicts with main ( This comment was written by a Claude agent (bot). |
What and why
The host union (
model::moe) gets the ungated relu² form, so Bolt's routed experts (up[w,1024]→ relu² → down[1024,w], no gate, inside the 1024 latent) can run on the host tier. It is an arm of the common union beside the gated SwiGLU form, not a Bolt copy:ExpertForm::Relu2reads one stack where the gated form reads two (aUnionPass::Upfirst pass), and the combine isrelu(up)². A NaN or an infinity in the up output is refused by name. The union scratch's expert width becomes a capacity, so one scratch serves a run's layers of differing widths (Bolt'sffruns 1536–2560 per layer). Prerequisite of the Bolt offload work (B3, #328); it changes no kernel.Release notes: internal — host-tier prerequisite for offloading Bolt's routed experts; no user-visible change.
Signatures B3 calls
HostLayer::experts_union_into<'x>(&self, src: R8Source<'_>, x: impl Into<Tensor2View<'x>>, lists: &[&[(u32, f32)]], out: &mut [f32], scratch: &mut UnionScratch) -> Result<(), ModelError>(unchanged shape;experts_step_union_intotakes the same).arch::nemotron_h_moe::host::layer(src, &hp, l) -> Result<Option<HostLayer>, ModelError>—Nonefor a layer that does not route (so a sparseVec<Option<HostLayer>>),Errfor a layer past the model. OrHostLayer::build_relu2(src, &Relu2Spec { up, down, n_expert, embd, ff })(no gate field).UnionScratch::new_routed(embd = 1024, ff = the widest layer's, cols, n_used = 9). A layer wider than the scratch, or anotherembd, is refused by name; a narrower layer gives the same bits as through a scratch made for it.TierError::Relu2Tiered) at the call (attach_tieritself does not refuse).Code shape
ops.rs,moe.rs).nemotron_h_moe/host.rsonly wires the latent and each layer's shapes; the stack names come from the commonarch::names::routed_stacks.host.rs: which layers route, each layer's ownn_expertandff, thelatent, the relu² form. The common owner gained a form arm (ExpertForm), no per-model branch.arch/;arch/nemotron_h_moe/host.rsis the one new file and holds only the facts above. Removed from this PR's own earlier shape: two Bolt-only name functions inarch/nemotron_h_moe/names.rs,moe.rs's testrelu2_file, and the tests' own copies ofseeded,diff_cells,panic_textand the two layer-file writers (nowtests/common/columns.rs,tests/common/panics.rsand oneStackFile::writeintests/union.rs).embd, a relu² layer with a tier, and a non-finite up value are each a named error; each has a gate clause and a mutant.tools/check-comments.sh: ok.cargo fmtclean;cargo clippy -p bloomery-model --all-targets: one warning,ops.rscollapsible_if(existing, from 9e894eb). Workspacejust lintnot run by me.Duplication found or removed: see "Per-model copies" above. Still open and not touched here (other families, for a follow-up): the
(gate, up, down)name triple indeepseek41/host.rs,glm5next/host.rs,mimo2/host.rs,qwen35moe/host.rscan takerouted_stacks; three synthetic stack byte generators (tests/common/r8layer.rs,moe.rstestsstack_bytes,tests/union.rsq8_stack).Gates (box,
tools/gate-batch.sh --round-ledger, real tier, heada2c9c1c6d)_intogate gains relu² and non-finite clauses)DONE total=10 red=0 tier=real skipped=0 wall=2606s laneA=2605s laneB=2076s. Every log showsCompiling bloomery-model.Narrowing (
just narrow origin/main, at948cac02e)96240135..948cac02: ptx-scan--no-jitofgate_glm5next_e2e,gate_kquant,gate_swapis identical on both sides (crates/modelholds no device code), so 159 selected recipes narrow to 122. The narrowed list ran in the fixture tier:Fixture tier, head
948cac02eover962401358(tools/gate-batch.sh --list narrowed.txt --round-ledger --tier fixture): 83 recipes ran and 48 are deferred to the real tier (29 real-only, 1 of a family with no fixture, 18 real-file;deferred.list).DONE total=131 red=28 tier=fixture wall=2665s(55 green). 21 of the 28 reds were my own doing: I exportedBLOOMERY_Q_CARD=anyinto the batch, whichbox.shrefuses (rc 64) beside the recipes' ownBLOOMERY_CARDpick. The 28 re-ran without it:DONE total=28 red=7 tier=fixture wall=2003s, so 76 of 83 are green.gate-gpu-nemotron-e2eandgate-gpu-nemotron-serve(the nemotron_h family has no fixture file, rc 66),gate-gemma-embedding-meta,gate-gemma-embedding2-meta,gate-gpu-gemma-embedding2-e2e(/models/embeddinggemma-2/embeddinggemma-2-Q8_0.ggufis missing).gate-gpu-qwen4exp-e2e(job s2182) andgate-gpu-qwen4exp-mtp(s2197) fail one card-plan clause each on the 3090 (and on the A6000 in the first pass):card leg plan: 4 of 4 layers hold routed experts on the card, a layer's count Some(255)..Some(256) (want 256 on every layer: half the experts, the header's budget) FAIL. The same line, withCompiling bloomery-modelin the log, fails onorigin/maine195c9e6a(jobs s2230, s2235,BLOOMERY_GATE_CARD=3090, fixture tier). The plan code is not touched here.bcb7332bf,a2c9c1c6d) change onlycrates/modelhost-tier source and tests (no device code). The ten owning gates above re-ran on them; the fixture batch was not re-run.FAIL-first (one mutant each, red, then reverted; every log shows
Compiling bloomery-model)write_par,col_into)matmul_q_group_cols_into)warm_spanswarms three stacks / tier refusal removed (one-column, union)Err(the WIP's behaviour) /embdis the model's, not the latentrouted_stacksup and down swapped inhost::layerThe
col_intomutant survived the union clause and the ne1 = 1, 6 clauses (only the bench-onlymatmul_q_group_cols_intowith an input pastDEFER_MAX_COLSreaches it); the group gate now runs it at ten columns and that mutant is red.GPU Evidence
0, NVIDIA GeForce RTX 3090, 615.71.09and1, NVIDIA RTX A6000, 615.71.09(nvidia-smi); AMD Ryzen Threadripper PRO 5975WX, Linux 6.8.0-142-generic. These gates run the host tier on the CPU (AVX2 qdot kernels); no device kernel changed, and no card kernel ran in them.a2c9c1c6d, whose merge base with main isdb5c89e7c(main has since moved toc14c46115; the PR merges cleanly, the gates were not re-run on it). Narrow and fixture runs: base962401358, head948cac02e.tools/gate-batch.sh --list h1-gates.list --round-ledger(jobs above);just narrow origin/main;tools/gate-batch.sh --list narrowed.txt --round-ledger --tier fixture.a2c9c1c6d):Timing
No timed run was made. Derived, not measured: the form choice is one predictable branch per expert combine, and the relu² finiteness check is a branch-free OR over the up row, about 0.05–0.1 µs per 2560-wide combine against a derived ~6.6 µs per host expert (under 2 %). A same-lease
just ab-decodeA/B belongs to perf.AI use: this change and its gates were written and reviewed by a Claude agent session (Claude Sonnet 5.5, the
h1worker of the Bolt offload team); the lead reviews and merges. This text was written by that bot.🤖 Generated with Claude Code