Repository navigation
model: Bolt's plan places routed experts on the host tier (Experts::{Card,Placed,Host}) - #393
Conversation
…Card,Placed,Host}) The Nemotron-H families' common owner gains `Experts`: where a plan puts the routed experts. `Card` is today's plan and keeps its refusal of any host expert byte for byte; `Placed` keeps each routed layer's id prefix [0, n_l) on the card as its budget holds and the rest on the host tier; `Host` keeps none on the card. `PlanInputs::plan` is `plan_with(.., Experts::Card)`. The context, tier and card-count refusals hold under every arm. The load still refuses host experts until the body has a host tier. Gates: the fixture's C1 to C4 in nemotron_h_moe::spec (the whole-card Placed plan is the Card plan; a small card keeps each layer's prefix and hosts the rest; the Host arm keeps no routed expert on the card; the refusals loop over the three arms), the no-routed-stacks case in nemotron_h::place, and hw_nemotron_h_moe_plans_the_host_tier on the real header. The split's clauses have one owner, tests/common/host_split.rs. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A change to hybrid.rs found no row, so `just affected --narrow` kept the full list. The row is the 80 gates `recipes.py reach` names for the file; the gates whose own target is the module (gate-nemotron-meta, gate-nemotron-h-moe-meta and the rest) stay outside it, as the table's rule says. FAIL-first, the same three identical scan pairs, origin/main..HEAD of the plan change: before: narrow: full list — crates/model/src/arch/hybrid.rs: ... no row of tools/gate-paths.tsv maps it (167 recipes) after: narrow: file crates/model/src/arch/hybrid.rs: own target of 12; rows 111 -> 80 recipes (102 recipes) `recipes.py check`: 134 gate rows held to the derivation, rc 0. Rebased onto main ce96aff: `recipes.py check --fill` added the five gates newer recipes reach (gate-fixture-budget, gate-gpu-gemma-embedding-e2e, gate-gpu-gemma-embedding-serve, gate-gpu-qwen38-nodes-remote, gate-qwen3-plan); check rc 0. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…de it
Main's Card::set_aside_bytes returns None past u64 bytes. The two new budget
sums (the fixture's small card and the real header's budget B) take the value
with the same `expect("bounded set-aside")` the spill gate beside them uses.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict: MUST_FIX: The new tests duplicate fixture and sizing facts and add an unnecessary forwarding module.
Must fix
1. Use the fixture helper for all fixture construction
crates/model/src/arch/nemotron_h_moe/spec.rs:608 repeats the setup at crates/model/src/arch/nemotron_h_moe/spec.rs:489 and crates/model/src/arch/nemotron_h_moe/spec.rs:564. This leaves three owners of the same header, tokenizer setting and read sequence (R14).
Reusing the helper now removes 20 lines before a fixture change needs three edits.
Replace the setup from let path = header_typed(...) through let inputs = ... in the_fixture_layout_plans_whole_card with:
let inputs = fixture_inputs("nhm-fixture");Use this replacement in a_card_too_small_for_every_expert_is_refused:
let inputs = fixture_inputs("nhm-spill");2. Give the partial-budget formula one owner
crates/model/src/arch/nemotron_h_moe/spec.rs:663 and crates/model/tests/nemotron_h_moe_meta.rs:424 copy the formula already at crates/model/src/arch/nemotron_h_moe/spec.rs:589. All three size the same dense floor plus half the expert bytes (R14).
A shared calculation removes more code than it adds and keeps future sizing changes in one place.
Add this to tests/common/host_split.rs; it uses the existing Card::floor_bytes owner:
/// The one card's budget for its dense floor and half its expert bytes.
pub fn half_expert_budget(plan: &Plan<'_>) -> u64 {
let c = &plan.cards[0];
let card = &plan.machine.cards[0];
card.floor_bytes(
c.dense_bytes + c.rounding_bytes + c.expert_bytes / 2,
c.kv_bytes,
c.stage_rows_bytes + card.margin_bytes,
)
.expect("bounded partial-plan budget")
}In both fixture tests, replace the sizing setup after whole with:
let budget = super::host_split::half_expert_budget(&whole);
let mut small = machine;
small.cards[0].usable_bytes = budget;In the real-file gate, replace let c = ... and the budget sum with:
let budget = host_split::half_expert_budget(card);3. Remove the forwarding placement module
crates/model/tests/nemotron_h_moe_meta.rs:21 adds a module that only re-exports an existing module. A direct import supplies the same crate::placement path to the shared checker.
Removing it now avoids maintaining a second namespace with no separate behavior.
Delete the forwarding module and its comment. Replace the placement import with:
use model::placement::{self, Plan, PlanLevers, workstation};4. Use one pin for the file's routed expert count
crates/model/tests/nemotron_h_moe_meta.rs:325 repeats the file total already pinned at crates/model/tests/nemotron_h_moe_meta.rs:263. Both tests describe the same model file (R14).
Using the new constant in both places prevents two independent count pins.
Replace the existing header-count condition with:
if u64::from(routed) != ROUTED || hp.n_expert != 496 {Checked
Duplicates: 15 new functions, types and constants inspected; three duplicate facts and one unneeded module confirmed above.
Complexity: plan() delegates; plan_with() adds the expert-policy match after the existing guards.
Silent failure: cleanup errors do not supply plan values; missing expert lists fail the segment comparison; a refused Card plan still records a failure.
Tests: static inspection confirms assertions for context/tier/card-count guards, policy arms and per-layer split clauses. Gates were not run in this job.
Gates: no existing tolerance or pin was relaxed.
Escape catalog: C1–C4 do not match. No hot path changed.
Notes
GPU evidence lacks the required nvidia-smi output line and exact just gate-* command/output pairs.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
|
Merged for the 0.3.4 train. The four review items (fixture helper reuse in nemotron_h_moe/spec.rs, one owner for the partial-budget formula, the forwarding placement module, one pin for the routed expert count) are code-shape items; they go in a follow-up PR on main. Written by a bot (Claude agent) for the maintainer. |
…budget and one routed-count pin Shingler's MUST_FIX on #393: - the two fixture plan tests build their inputs through `fixture_inputs`, not a third copy of the header, tokenizer setting and read; - `half_expert_budget` (tests/common/host_split.rs, on `Card::floor_bytes`) is the one owner of the dense floor plus half the expert bytes, for both fixture tests and the real-file gate; - the meta test imports `model::placement::{self, ..}` instead of a forwarding module; - the header-count pin takes `ROUTED`, the constant the plan gate already used. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
What and why
Bolt's (
nemotron_h_moe) plan can now put routed experts on the host tier. This is B2 of the offload row of #328. The common planner already makes that plan per layer. Only the family wrapper refused it.hybrid.rsgainsExperts::{Card, Placed, Host}andPlanInputs::plan_with.plan()isplan_with(.., Experts::Card): same plan, same error text.Placedkeeps each routed layer's id prefix[0, n_l)on the card as its budget holds and the rest on the host.Hostkeeps none on the card. The context, tier and card-count refusals hold under every arm. The load still refuses host experts: the body has no host tier yet (B3).Release notes: internal — the plan half of Bolt's
--n-cpu-moeequivalent; no load uses it yet.API for B3
PlanInputs::plan_with(&machine, ctx, &levers, Experts::{Card, Placed, Host}), re-exported asnemotron_h_moe::place::Experts.workstation::placed_on(RTX_3090, 88, workstation::SCRATCH + <the body's own scratch>). The body's scratch is the GPU side's to size. This PR counts none.plan.n_l[l]; each routed row isCard(0)ids[0, n_l)thenHost/HostFileids[n_l, E_l);plan.host_experts(l);plan.host.expert_bytes. Sum only over layers that have routed stacks:layer_experts(l)of any other layer is the model's largest count.n_lfollows from the bytes:BLOOMERY_CARD_BUDGETand--place gatealready reachplanthroughPlanLevers.Pinned facts (the real 24 GB header, ctx 4096, body scratch not counted)
plan_gate(88)andplaced_on(RTX_3090, 88, SCRATCH)both.Placedthere is Debug-equal toCard.Gates and FAIL-first
Fixture clauses in
nemotron_h_moe::spec(C1 to C4),nemotron_h::place(C5), andhw_nemotron_h_moe_plans_the_host_tieron the real header. The clauses of a split have one owner,tests/common/host_split.rs. Each mutant ran throughtools/mutant-run.shwith--expecton the intended clause, then a clean run.placement.rshost experts counted at the model's largest layerplan_routedplacement.rsexpert budget without the margin9 of 9 killed, clean runs green. M3, M5 and M8, M9 are temporary mutants of
crates/placement; none is committed. Clause (d), the stacks' lists equal, is implied by (c): no mutant isolates it.tools/gate-paths.tsvgets a row forcrates/model/src/arch/hybrid.rs(second commit): before,narrowkept the full list (167 recipes, "no row maps it"); after,rows 111 -> 80 recipes(102 in all; the row holds 82 recipes since the merge of main).recipes.py check: 134 gate rows, rc 0.GPU Evidence
Box: RTX 3090 and RTX A6000, driver 615.71.09, nvcc cuda_13.3, Linux 6.8.0-142. All jobs through the box queue.
Two trees. The jobs below ran at head
9b630364con base962401358(origin/main then), except the lines marked [tip]. After them, origin/main moved toe195c9e6a; this PR merged it (e263e7f59) and fixed two call sites for main'sOption<u64>set-aside (766b36916). The merge moved thehybrid.rsrow oftools/gate-paths.tsvfrom 80 to 82 recipes. The 80-gate batch below ran on the pre-merge tree; the [tip] lines re-ran the four gates the merge could change.[tip] at
766b36916,tools/gate-batch.sh --tier fixture --round-ledger,DONE total=4 red=2 wall=608s:gate-nemotron-h-moe-metajob s2246 rc 0: lib15 passed, hw2 passed.gate-nemotron-metajob s2251 rc 0: lib36 passed, hw1 passed.gate-qwen3-planrc 101, not from this PR:error[E0308]: mismatched typesatcrates/gpu-gates/src/bin/shared/qwen3moe_place.rs:660and:1364(reserve_bytes()isOption<u64>sincebd378cd6e; that file'sreserves:line came withea4b6228c). Both files are byte-identical to origin/maine195c9e6a.gate-gpu-qwen38-nodes-remoterc 101, not from this PR:Error: "BLOOMERY_TIER_NODES names 0 addresses, the gate one". The batch sets no remote node.Second merge: origin/main
402f9abc6(85410e2b4, plain push) brings #372 (686da9f16), which fixes theqwen3moe_place.rscompile error above.just checkon the box at85410e2b4: rc 0.gate-qwen3-planand the two meta gates were not re-run at85410e2b4; the row is still 82 recipes (recipes.py checkrc 0).gate-nemotron-h-moe-metajob s2221 rc 0: lib15 passed, hw2 passed(hw_nemotron_h_moe_header,hw_nemotron_h_moe_plans_the_host_tier).gate-nemotron-metajob s2222 rc 0: lib36 passed, hw1 passed.tools/gate-batch.sh --list <the row> --tier fixture --round-ledger→DONE total=80 red=5 tier=fixture skipped=0 wall=5478s laneA=3243s laneB=3403s laneX=2073s deferred=120 real_only=20 real_file=16.gate-gpu-nemotron-h-moe-e2ejob s2065, A6000, rc 0:PASSED: gate_nemotron_h_moe_e2e the captured step holds the entries its layer kinds derive; eager and replay give bit-identical logits; a reset rewinds the ids and the logits bits; the scan state carries; the fault word stays clean; …gate-gpu-nemotron-h-moe-servejob s2067, A6000, rc 0:PASSED: gate_nemotron_serve the seat plans its default and says so, refuses a second slot, loads at the context it named, …gate-gpu-nemotron-e2ejob s2218, 3090, rc 0,PASSED: gate_nemotron_e2e …;gate-gpu-nemotron-servejob s2220, A6000, rc 0,PASSED: gate_nemotron_serve ….gate-gpu-qwen4exp-e2e(s2063) andgate-gpu-qwen4exp-mtp(s2070), rc 1:card leg plan: … a layer's count Some(255)..Some(256) (want 256 …) FAIL. The same two lines fail on origin/maine195c9e6awith none of our commits (jobs s2219 and s2223).gate-gpu-gemma-embedding2-e2e(s2073), rc 1:/models/embeddinggemma-2/embeddinggemma-2-Q8_0.gguf: No such file or directory. The box has no such file.real-only, 16real-file; list in the batch'sdeferred.list): gate-gpu-p6, gate-gpu-e2e, gate-gpu-hybrid, gate-gpu-p8, gate-gpu-moe, gate-gpu-ds41-moe, gate-gpu-ds41-chain-ffn, gate-gpu-p10, gate-placement, gate-gpu-qwen3moe-router, gate-gpu-qwen35moe-verify, gate-gpu-qwen35moe-mtp, gate-gpu-qwen35-dspark, gate-gpu-minicpm-text, gate-gpu-clef-hidden, gate-gpu-qwen35-media, gate-gpu-qwen35moe-media, gate-gpu-clef-serve, gate-gpu-lev-serve, gate-gpu-qwen4exp-iqleg, gate-gpu-gemm, gate-gpu-gemma-embedding2-serve, gate-gpu-qwen3moe-e2e, gate-fixture, gate-gpu-head, gate-gpu-ds41-long, gate-gpu-ds41-draft, gate-gpu-ds41-chat, gate-gpu-nvtier, gate-gpu-qwen38-residency, gate-gpu-qwen3-serve, gate-gpu-p8b, gate-gpu-dspark-experts, gate-gpu-dspark-kv, gate-gpu-dspark-graph, gate-gpu-glm5next-serve.ptx-scan --no-jitofgate_glm5next_e2e,gate_kquant,gate_swapidentical to the base (just narrow): no kernel moved.cargo check -p bloomery-model --all-targets0 warnings; clippy 12 warnings, all in other files;fmt-checkand everytools/check-*.shrc 0 exceptcheck-recipes.sh, wherenarrow-scan.sh --self-test(16 FAIL) fails the same way on origin/main'stools/.Code shape
tests/common/host_split.rs) read by the fixture gate and the real-file gate. The placed machine has no wrapper here; qwen3moe'smachineandcounted_arena_bytesare a separate lift.hybrid.rsgains an enum and one function, no per-model branch. A file with no routed stacks plans the same under every arm (C5).Cardkeeps its named refusal of host experts. No comment carries a date or an issue number.This PR was written by a bot (a Claude Sonnet 5.5 agent in the Claude Code harness) under the lead's direction.
Rebase onto main ce96aff (supersedes #371)
#371 had two merge commits and conflicted with main. This branch holds its three commits on main, no merge commit.
tools/gate-paths.tsv: main's rows kept, thehybrid.rsrow added;recipes.py check --filladded five gates newer recipes reach (gate-fixture-budget, gate-gpu-gemma-embedding-e2e, gate-gpu-gemma-embedding-serve, gate-gpu-qwen38-nodes-remote, gate-qwen3-plan);recipes.py checkrc 0.--round-ledger: gate-placement rc 0, gate-nemotron-meta rc 0, gate-nemotron-h-moe-meta rc 0.tools/ref/dense-logits.py --self-testreads/procon macOS, also red on main). Neither comes from this change.Written by a bot (Claude agent) for the maintainer.
🤖 Generated with Claude Code