Repository navigation
placement, model: on a unified machine a plan the NVMe tier's floor refuses is planned again under a card budget that leaves the floor - #100
Conversation
9a8a17f to
74a9461
Compare
There was a problem hiding this comment.
Verdict: MUST_FIX: the same unified-pool load refusal remains on the Qwen sibling path.
Must fix
1. Connect the Qwen planner to the shared pool retry
crates/model/src/arch/qwen35moe/place.rs:624 still calls the NVMe split once and propagates HostRoomFloor. The PR's reason for excluding Qwen covers an idle Spark, but the same file reaches this failure when another process reduces the available pool.
For Qwen3.8 UD-Q4_K_XL without a draft, ctx=4096 per slot, two slots, and GB10/host readings of 64 GiB, the current checkpoint reserve is 520,650,752 B. The floor is 71,379,080,324 B against a 68,182,605,824 B room. The shared retry reaches a 65,530,237,556 B card budget and a 68,181,755,012 B floor after three reductions [derived from the checked-in tensor table, allocator and checkpoint pricing]. The unbudgeted refusal propagates through plan_with_slots to the serving caller, rejecting a feasible load.
Connecting this sibling now closes the same defect at the existing owner and avoids another budget workaround.
Replace target_rule_of with:
fn target_rule_of<'a>(
&'a self,
machine: &'a Machine,
ctx_max: u64,
levers: &PlanLevers,
rule: Option<RoutedRule>,
beside: Beside<'_>,
kv: &dyn KvBytes,
) -> Result<Plan<'a>, PlaceError> {
placement::plan_in_pool(machine, levers, |levers| {
let mut plan = match rule {
None => placement::plan_host_routed_reserving(
&self.model,
machine,
ctx_max,
kv,
levers,
beside.reserve,
)?,
Some(rule) => placement::plan_routed_reserving(
&self.model,
machine,
ctx_max,
kv,
levers,
rule,
beside.reserve,
)?,
};
let floor = 2 * self.seq_state_bytes(ctx_max, beside.mtp);
let room = plan.plan_at(&self.room);
placement::row_table_tier(&mut plan, room, floor, UBATCH_PLANNED)?;
plan.host
.step_slots(beside.slots, placement::STEP_PORT_COLUMNS)?;
placement::expert_nvme_tier(&mut plan, room, self.arena)?;
Ok(plan)
})
.map_err(tier_host)
}Add a unified Qwen case below the unbudgeted floor; restoring the one-call planner must make its success assertion fail.
Checked
Duplicates: the retry has one shared owner, used by V4.1 and both GLM plans. Dryer reported 0 pairs.
Complexity: crapper flagged no functions. The retry runs during planning.
Silent failure: Qwen's floor refusal reaches its serving caller as an error.
Tests: the scripted retry asserts the budget sequence; the GLM and V4.1 tests require lowered plans. Validation used static code inspection and an in-memory calculation; no local build was run.
Gates: no existing pins or assertions relaxed; the new hardware test is hw_-prefixed and ignored. FAIL-first output is in the PR body.
Escape catalog: C1–C4 do not match the changed files.
Notes
GPU evidence lacks the nvidia-smi output line required by CONTRIBUTING.md.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
|
Sorry, we made a mistake here. While tidying PRs we rebased your branch onto main and pushed an extra commit ( This comment was written by the bloomery AI assistant on behalf of the maintainer. |
|
No harm done, and thanks for flagging it. Please keep |
…efuses is planned again under a card budget that leaves the floor On one pool the card's fill is capped by the pool's reading, and the cards' share of the pool is a host term of the NVMe expert tier's floor (HostTotals::pool_card_bytes in the host terms beside the routed experts). A file above the pool fills the card to the reading, so the floor's base alone passes the room and the plan is refused with HostRoomFloor at every context: on a DGX Spark, DeepSeek-V4.1-Flash Q3_K_M (room 124.14 GB, floor 153.02 GB) and GLM-5.3-Flash UD-Q4_K_XL (room 119.57 GB, floor 141.64 GB) at serving defaults. Only BLOOMERY_CARD_BUDGET loaded them. placement::plan_in_pool runs a family's plan and split as a closure. On a unified machine with no given budget, a HostRoomFloor refusal plans the closure again under a card budget lowered by the shortfall the refusal names, at least a granule, until the floor holds; the plan is marked budget_from_pool. A given budget, a machine whose cards have memory of their own and a shortfall past the card's whole budget are the first plan's result. A lowered budget refused for another reason, a budget that runs out and a floor unmet after 64 rounds are refused by name (PlacementError::PoolBudget: the rounds, the last budget, the last refusal). The V4.1 plan and both GLM plans (with and without NextN) go through it. HostNeed names BLOOMERY_CARD_BUDGET only for a budget the caller gave, and the lever's unset text and PlanLevers say what unset means on a unified pool. Rounds on the DGX Spark at serving defaults: V4.1 two plan_in_pool calls of two rounds each; GLM eleven calls (the context ladder and the session's plan) of one to five rounds, twenty rounds in all. A round under one expert's card bytes walks the budget a granule at a time until an expert leaves the card. Tests: a_unified_pool_lowers_the_card_budget_to_the_tiers_floor holds plan_in_pool's rule on a scripted planner (red with the rule reduced to one call); slot_plans_on_one_pool_lower_the_card_budget_to_the_tiers_floor plans GLM's plain and NextN plans at one and two slots on a pool under the floor (red at each call site with its wiring reverted, and a room under the NextN experts refused by name); hw_placement_unified_pool_lowers_the_card_budget plans the V4.1 file on a GB10's pool at one and two slots (red with the wiring reverted). gate-placement now reaches crates/models/src/shape.rs through the V4.1 plan inputs, so the row lists it. Mac: just mac-static ok (fmt, lint 41 = ratchet, combos, every check script), just mac-test ok (placement 145 tests). DGX Spark (GB10, aarch64): bloomery-model's glm5next and deepseek41 lib tests and the V4.1 hw test green; both models load at serving defaults and answer; Qwen3.6 Q4_K_M and Qwen3.8 UD-Q4_K_XL, which meet the floor, print the same --plan output as the base at one given room.
…the machine's node, after midagedev#94 moved host and unified onto nodes[0]
…he card rule goes through plan_in_pool, and the seat's floor fallback takes PoolBudget target_rule_of ran the NVMe split once, so a Qwen plan under the card rule (BLOOMERY_QWEN38_EXPERTS=card, the default) whose card share leaves the host's room under the tier's floor was refused with HostRoomFloor even when a lower card budget meets the floor: on a DGX Spark that is a pool another process holds part of. The card rule's plan and split now run as plan_in_pool's closure, as the V4.1 and GLM plans do. Every Qwen3.5/3.6/3.8 plan reaches target_rule_of. The all-host rule (BLOOMERY_QWEN38_EXPERTS=host) is planned once, as before. It puts no routed expert on the card, so the cards' share in the floor is the dense tensors and the cache, and a lower card budget does not move it. Through the loop it would plan the same refusal each round and end in PoolBudget, which names a lowered card budget the user never set, in place of the HostRoomFloor that names the room and the floor; its last refusal is HostRoomFloor when the budget runs out first, or CardBudgetFloor when the budget passes under the card's own floor first. Its refusal stays HostRoomFloor. The Qwen seat's floor fallback (floor_fallback) also takes PoolBudget as the asked slots' floor refusal: on a unified pool a default slot count whose lowered budget is still refused falls to the one-slot plan, as a HostRoomFloor refusal does. The match is its own function, floor_refused, so the seat's binary can test it. Test: a_unified_pool_lowers_the_card_rules_budget_to_the_tiers_floor plans the card rule at one and two slots on a 64 GiB pool that a synthetic file of 72.5 GB of routed experts does not fit: the card's whole bytes as a given budget is the floor's refusal, and with no budget the plan is lowered, keeps card experts and pages the rest. The all-host rule on a 32 GiB room of the same pool is refused with HostRoomFloor, not PoolBudget. the_floor_fallback_takes_the_floors_refusal_and_the_pools_only (gate-q38-serve-rules) holds floor_refused: HostRoomFloor and PoolBudget are the floor's refusal; NvTierArena, Positions and an error that is no PlaceError are not.
…serving defaults on one Spark, with no arena in the default plan, and what a plan there needs for one dgx-spark.md: the P4 row, 7.2's last bullet and paragraph, section 8, the P4 open items and answer 21's status say the models load at serving defaults under a card budget lowered to the NVMe tier's floor, that their default plans page through the file mapping with no arena, and that a given BLOOMERY_NVTIER_BYTES needs a lower BLOOMERY_CARD_BUDGET beside it on one Spark. 9.9's clause 1 and its section 10 item no longer say that one Spark's plan does not page V4.1 or GLM: it pages them under the lowered card budget, and the node plans take node 0's terms before that split. tierengine.md (k) row 4 no longer waits on the load refusal; it needs a GLM plan with an arena.
d15ed69 to
a54fbc2
Compare
|
The Qwen planner point is fixed in One correction to my earlier reply. I said the branch would not be rewritten under the maintainer's commit, but it was: main moved twice since (#157 and the placement changes, then #86), and the first move conflicted in |
There was a problem hiding this comment.
Verdict: MUST_FIX: The retry wrapper panics after the planner rejects a machine with no nodes.
Must fix
1. Return non-floor results before reading the node
crates/placement/src/placement.rs:3421
A Machine with nodes: vec![] makes plan_rule return PlacementError::Nodes { nodes: 0 } at lines 2199–2202. The wrapper then calls machine.unified(), whose this_node() panics at line 266. V4.1's changed plan_with_slots call at crates/model/src/arch/deepseek41/place.rs:177 therefore panics instead of returning its placement error. This is a kind A behavior defect.
Cost: fix the shared retry owner before more callers need to handle this regression.
Replace the block from let first = make(levers); through the shortfall match with:
let first = make(levers);
let mut short = match &first {
Err(PlacementError::HostRoomFloor { room, floor, .. }) => floor.saturating_sub(*room),
_ => return first,
};
let (Some(_), None, Some(card)) = (
machine.unified(),
levers.card_budget_bytes,
machine.cards.first(),
) else {
return first;
};Add the empty-node case to the existing retry test and assert that it returns Nodes { nodes: 0 }.
Checked
Evidence: followed plan_with_slots → plan_routed → plan_rule, then the wrapper's unified → this_node access.
Fix: the replacement moves the existing result match before the node access; non-floor results return immediately and floor retries retain their checks.
Diagnostics: dryer reports no qualifying pairs; crapper reports no changed function crossing its thresholds.
Tests: the retry test at placement.rs:4006 uses one-node machines and lacks the empty-node case.
Scope: full review; no revert, mirror, ledger lesson or escape-catalog exception applies to this finding.
Notes
Validation was static; no local build or card gate was run.
Card check: gpu-evidence. This pull request changes crates/gpu-gates/src/bin/bloomery_serve_qwen38.rs, crates/gpu-gates/src/bin/shared/serve_seats/qwen38.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)
… it reads the machine's node
|
Fixed in The fix is its own commit on top rather than folded in, because the node read came with the maintainer's |
There was a problem hiding this comment.
Since the last review: must-fix 1 is fixed at crates/placement/src/placement.rs:3420. The wrapper returns Nodes { nodes: 0 } before accessing the node.
Verdict: CLEAN: The earlier panic is fixed, and the incremental diff introduces no blocking defect.
Checked
Duplicates: 0 new functions, types or constants; no qualifying dryer pairs.
Complexity: crapper flagged no functions. The result match moved; the existing test gained one case.
Silent failure: 1 result-handling site followed; non-floor errors return unchanged.
Tests: placement.rs:4103 protects the early return at line 3422. Restoring node access before the match makes the test panic before its assertion. The PR reports this FAIL-first result and 166 passing placement tests.
Behavior: traced the shared planner's node rejection and the wrapper's successful-plan, discrete-card and explicit-budget paths; floor retries retain their checks.
Gates: no pins, thresholds or gate cases removed or relaxed.
Escape catalog: no C1–C4 patterns matched.
Notes
Validation was static; no local build or card gate was run.
Card check: gpu-evidence. This pull request changes crates/gpu-gates/src/bin/bloomery_serve_qwen38.rs, crates/gpu-gates/src/bin/shared/serve_seats/qwen38.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. |
What and why
Part of #13: the load refusal the maintainer left to the contributor in #13 (comment).
On a DGX Spark, DeepSeek-V4.1-Flash Q3_K_M and GLM-5.3-Flash UD-Q4_K_XL are refused at serving defaults with
HostRoomFloor, and onlyBLOOMERY_CARD_BUDGETloaded them. With this change both load at serving defaults andanswer, in every start of this change I ran: one each at
a54fbc2a(this head before the review fix, which changes no plan on a machine with a node), one each atecad5fdc(this code before the maintainer moved it onto main), and more on earlier builds ofit. Cards with memory of their own, a given card budget and
plans that already meet the floor are planned as before.
The cause: on one pool the card's fill is capped by the pool's reading, and the cards' share of the pool
(
HostTotals::pool_card_bytes) is a host term of the NVMe expert tier's floor. A file above the pool fills the card upto the reading, so the floor's base alone passes the room. At
31abb2d5, serving defaults:placement::plan_in_pooltakes a family's plan and split as a closure. On a unified machine with no given budget, aHostRoomFloorrefusal plans the closure again under a lower card budget, until the floor holds. The first budget iscapped(card, None)formachine.cards.first(): the card's usable bytes cut to its free reading plus the context's owncreation cost that reading already lacks (
Card::context_in_free). On a GB10 the free reading is the pool's reading. Each round lowers it by the shortfall the last refusal named, or by a granule (2 MiB) when thatshortfall is smaller. The plan it returns is marked
Plan::budget_from_pool. A shortfall past the card's whole budgetis the first refusal as before. A lowered budget refused for another reason, a budget that runs out, and a floor still
unmet after 64 rounds end in a new named error,
PlacementError::PoolBudget, which carries the rounds, the last budgetand the last refusal. The V4.1 plan (
plan_with_slots), both GLM plans (plan_slots,plan_nextn_slots) and theQwen target's plans under the card rule (
target_rule_of, which every Qwen3.5/3.6/3.8 plan reaches) go through it.Two strings changed with it.
HostNeed's refusal text namedBLOOMERY_CARD_BUDGET ... so unset itfor any plan madeunder a budget; it now does so only for a budget the caller gave. The lever's unset text in
levers/src/registry.rsand the
PlanLeversdoc now say what unset means on a unified pool.The docs commit (
a54fbc2a) brings the docs up to date with this change: indocs/contrib/dgx-spark.mdthe P4 row, the lastbullet of 7.2 and the paragraph after it, section 8, the P4 open items, the status of answer 21, and the two places
from #95 that said one Spark's plan does not page V4.1 or GLM yet (9.9's clause 1 and its item in section 10); in
docs/research/tierengine.mdrow 4 of (k), which waited on this refusal.Not wired, with the reason:
BLOOMERY_QWEN38_EXPERTS=host). It puts no routed expert on the card, so the cards' sharein the floor is the dense tensors and the cache, and a lower card budget does not move it. Through the loop it would
plan the same refusal round after round and end in
PoolBudget, which names a lowered card budget the user neverset, in place of the
HostRoomFloorthat names the room and the floor. Its last refusal isHostRoomFloorwhen thebudget runs out first (the FAIL-first run below: 3 rounds, down to 5348147056 B), or
CardBudgetFloorwhen thebudget passes under the card's own floor first. So
target_rule_ofplans that rule once, and its refusal staysHostRoomFloor.The card rule, the default, goes through the loop. On a DGX Spark at serving defaults, Qwen3.8 UD-Q4_K_XL meets the
floor with every expert on the card (
experts=card host_experts=0in its plan line), so the loop runs only when thepool reads less, for example while another process holds part of it (point 7 below).
(
PlaceError::NoNvmeTier,crates/model/src/arch/mimo2/place.rs:244). A lower card budget only pages more, so aMiMo2 file above the pool keeps the floor's refusal.
Points for the maintainer:
does. The budget it returns is the first one in that descending sequence that meets the floor, one step under a
budget that did not. At serving defaults on the DGX Spark that took at most five rounds a call (counts below).
docs/research/tierengine.mdS10 ("One budget solver") would replace this kind of rule with one solver. I kept theloop because it reuses the planner as it is. If a computed budget is preferred here, I will change it.
plan can carry a different budget from the
planline the seat printed; it follows the reading. The split alreadymoved with the reading before this change.
machine.cards.first()'s. A GB10 has one card; a unified machinewith several stage cards would need a budget per card.
plan_nextn_slotsclosure, so the plan of every round gets them. The prompt cache stays at 0 on GLM's paged planfor another reason: at the
ecad5fdcload below,the default budget 117455810 B is under one saved state's 358962176 B.arena=0 arena_slots=0. Their room(about 124 to 125 GB) is above half of the host leg's routed-expert bytes (96.27 to 97.19 GB on GLM in the
planlinesof the GLM starts at
ecad5fdcanda54fbc2aand the--planrun atecad5fdcbelow: the line'shost_expertsbytes pluspaged[derived]), and therenvme_arena_ofgives no arena: the tier pages through thefile mapping. So engram, gpu, gpu-gates, levers, model, placement: on an integrated device the NVMe tier's arena is one write-combined pinned allocation the card maps, and a pin yields a slot's device address #120's pinned arena is not on the default load's path, and engram, gpu, gpu-gates, levers, model, placement: on an integrated device the NVMe tier's arena is one write-combined pinned allocation the card maps, and a pin yields a slot's device address #120 does not move these plans:
HostNeedcounts the arena's bytes whatever their kind, and the arena here is 0. placement, model, gpu, gpu-gates: a Machine's host and unified pool live in Machine::nodes, this process's node first #94 movedMachine's host and poolinto
Machine::nodesand plans every machine as before.is left only a few MB above it, and a given
BLOOMERY_NVTIER_BYTESdoes not fit. On a DGX Spark, GLM at servingdefaults on a merge of engram, gpu, gpu-gates, levers, model, placement: on an integrated device the NVMe tier's arena is one write-combined pinned allocation the card maps, and a pin yields a slot's device address #120's head into this change before its rebase:
BLOOMERY_NVTIER_BYTES=24Gis refused withPoolBudgetonce the loop has lowered the budget to the floor, naming the arena 25769803776 B past the room lessthe floor (4211644 B [derived]);
BLOOMERY_NVTIER_BYTES=20Gis refused withNvTierArenaUnder(the arena's least is 23867015168 B). With a card budgetgiven as well, the loop is not used:
BLOOMERY_CARD_BUDGET=70G BLOOMERY_NVTIER_BYTES=23Gplansarena=24696061952 arena_slots=38. Counting a given arena in the shortfall would let the loop plan it. This PR doesnot do that, because it changes what the lever asks for on a pool. The card leg needs an arena, so
docs/contrib/dgx-spark.mdlists this under P4's open items.floor_fallback(crates/gpu-gates/src/bin/shared/serve_seats/qwen38.rs)drops a default slot count to one slot when the asked plan is refused under the tier's floor and the one-slot plan
pages with an arena. With the card rule in
plan_in_pool, the asked plan is first planned again under a loweredcard budget, and where that plan meets the floor the seat keeps the default slot count with more experts paged. On
a DGX Spark with Qwen3.8 UD-Q4_K_XL at
BLOOMERY_HOST_ROOM=68719476736and no--parallel(What I ran), mainrefuses the load (
HostRoomFloor, floor 70822551085 B), and this change plans the default two slots of 4096 tokenswith 16277 card experts, 25318604800 B paged and no arena. Where the lowered plan is still refused, the refusal is
PoolBudget, andfloor_fallbacknow takes it as it takesHostRoomFloor(floor_refused, with its own test), sothe one-slot plan is still tried.
One question for the maintainer: on a unified pool, should the seat lower the card budget first (this change: the
default slots, more experts read from the drive) or the slots first (one slot, more experts on the card)?
NodeBudget::oftakes a node's budget from a card plan of that node, andnodes_scalemakesthose plans with no budget on pools that hold the model, so nothing there goes through
plan_in_pool. For a modelabove one node's pool, the engine side that makes node 0's card plan would need it from
plan_in_pool, as thefamilies' plans here do.
Plan::pool_need, whichNodeBudget::ofand thePoolOvercheck read, counts the same terms for either plan:budget_from_poolchanges onlyHostNeed's refusal text.Rounds at serving defaults
I counted the rounds on the DGX Spark at serving defaults with an earlier build of this change that printed one line
per round. Its step rule and first budget are the same as this head's. It had no round cap and no
PoolBudget, whichno call reached: every call ended with a plan that met the floor. It sat on an older main, without #93.
plan_in_poolcalls, two rounds each. The second round's step was the shortfall (13842432 B and14178304 B).
round, one of two, two of three and one of five. In one three-round call the shortfall stayed at 4769916 B for two
steps of that size; in the other it stayed at 1493116 B for two granule steps. In the five-round call it stayed at
444540 B for four granule steps. Each call ended at the first budget whose plan met the floor.
Each round is one more plan of the closure.
Files that overlap the maintainer's train, which are all the files this PR changes:
crates/placement/src/placement.rs,crates/placement/src/placement/workstation.rs,crates/placement/src/placement/churn.rs(one test field),crates/model/src/arch/deepseek41/place.rs,crates/model/src/arch/glm5next/place.rs,crates/model/src/arch/qwen35moe/place.rs,crates/model/tests/placement.rs,crates/levers/src/registry.rs,crates/gpu-gates/src/bin/shared/serve_seats/qwen38.rs,crates/gpu-gates/src/bin/bloomery_serve_qwen38.rs,justfile(the comment above thegate-q38-serve-rulesrecipe),tools/gate-paths.tsv, and in the docs commitdocs/contrib/dgx-spark.mdanddocs/research/tierengine.md.This head is on main
fadec9d0, which has #95 and #157. The rebase onto33fb317bhad two conflicts, and both keepmain's text with this PR's change on top: the
crates/models/src/shape.rsrow oftools/gate-paths.tsv, where mainadded a gate and this PR adds
gate-placement, and the table indocs/contrib/dgx-spark.md, where this PR's P4 rowsits next to #95's P5 row. The docs commit then also updated the two passages from #95 named above. The rebase from
33fb317bontobaeedd73had one conflict, the sametools/gate-paths.tsvrow (main addedgate-gpu-minicpm-text), resolved the same way; the rebase frombaeedd73ontofadec9d0had none.git range-diff baeedd73..4685e4ab fadec9d0..a54fbc2ashows all four commits unchanged (=). The maintainer's commit keeps its patchand author; only its hash moved, from
e5a304dfthrougheeb9ef47to57fe1647. Main changedjustfileandcrates/levers/src/registry.rstoo, in other hunks than this PR's.#157 (
ada93e38) works in the same area and does not change what this PR plans: its ladder floor and its reuse ofslots::split_ctxandctx::largestlive inplacement::ctx_ask, which no seat calls yet, and this PR's diff namesnone of them. Once a seat on a unified machine calls
ctx_ask::choosethroughplan_in_pool,card_shortwill needto read a
PoolBudgetwhose last refusal isCardBudgetFlooras a short card.Code shape
The review reads these first. A box left unticked needs one line of why.
placement::plan_in_pool; the four call sites (V4.1, GLM's twoplans, Qwen's card rule) pass their own plan and split as a closure.
has no per-model branch.
PlacementError::PoolBudgetwith the rounds, the budget andthe last refusal; it never returns a defined-looking plan.
tools/check-comments.shis green.tools/gate-paths.tsvgainsgate-placement: the new V4.1 test reachescrates/models/src/shape.rsthrough the V4.1 plan inputs, andjust check-recipesasked for it.cargo fmtandcargo clippyare clean on what I touched; the lint count did not go up.Duplication found or removed (
path:line), or "none": none.What I ran
This head is five commits on main
fadec9d0.c6e15d4fis this change, which the maintainer moved onto main, and57fe1647is the maintainer's fix for #94's move ofMachine's host and pool intoMachine::nodes:plan_in_poolreads
machine.unified(), and the new placement test builds its machine with oneNode.d0c51f1fputs Qwen's cardrule through
plan_in_pool(target_rule_ofincrates/model/src/arch/qwen35moe/place.rs, and its test) and letsthe Qwen seat's
floor_fallbacktakePoolBudget(floor_refusedincrates/gpu-gates/src/bin/shared/serve_seats/qwen38.rs, tested inbloomery_serve_qwen38.rs), anda54fbc2ais thedocs.
ad7aa155answers the review:plan_in_poolnow returns a result other than the floor's refusal before it readsmachine.unified(), so a machine with no node gets the planner'sNodes { nodes: 0 }instead of a panic inthis_node(), and the placement test has that case. On a machine with a node it returns what it returned ata54fbc2ain every case, so the runs below, which namea54fbc2aas this head, stand forad7aa155too. Atad7aa155on the Mac (M4 Pro),cargo test -p bloomery-placementpasses 166 of 166 andcargo fmt --checkis clean; with the old order the new case panics atthis_node().On main
baeedd73the same four commits were896ca0d8,eeb9ef47,7dd9b23band4685e4ab; on main33fb317bthey were6fb48f2a,e5a304df,d41c07d8and7971e341; on main45836b17they were74a94611,d15ed697,c42f3feeand7448162f; before the maintainer's move the code wasecad5fdcon31abb2d5.git range-diff 31abb2d5..ecad5fdc 45836b17..74a94611shows only the imports and context lines #94 changed, andd15ed697adds the two lines above. Between31abb2d5and45836b17, the changes undercrates/on the plan's path are #94's (theMachine::nodesmove, which plans every machine as before) and #120's (S7: the arena's memory kind;HostNeedcountsthe arena's bytes whatever their kind, and these plans have no arena). The rest are V4.1's DSpark draft owner, the
engram eviction, the levers' draft lines, the tier page's link, the record-token helper, the feature-off stubs dropped
from the gate binaries and tooling.
Between
45836b17and33fb317b, the changes undercrates/on the plan's path are these. #95's node plans(
crates/placement/src/placement/nodes.rs, andnodes_scale.rsfor its tests) are called by nothing on the servingpath yet. The
PoolOvercheck moved intoPlan::pool_needwith the same terms.plan_rulenow refuses a machine ofmore than one node (
Nodes) and a pool whose bytes pass u64 (PoolPastU64); neither isHostRoomFloor, soplan_in_poolreturns them as they are. The workstation's plan (a) asked for a draft (6d11ac94and the two commits before it, forgate_load_v41) andpaged_draftat arena 0 (acfb220a, Qwen3.8's reduced load only) do not reach the GLM and V4.1 plans. Off the plan'spath, the NVMe tier's step (
crates/gpu/src/host/nvtier.rs), which the GLM and V4.1 starts run, changed as well; the GLM and V4.1 starts below ran again at this head.Between
33fb317bandbaeedd73, the changes undercrates/on the plan's path are these.1f3f36bachecks a set--ctxagainst the model's limit before any plan (ctx::check_slot_ctx), and Qwen'sserve_ctxnow delegates to itwith the same refusal; at serving defaults no
--ctxis set.19f162c9and #157 addctx_ask, which no seat calls(above). The rest is MiniCPM-V 4.7 (its tower, seat, media path and reference sets), which none of these plans reach,
and gate binaries. The NVMe tier's step did not change.
Between
baeedd73andfadec9d0, the changes undercrates/are these. #86's AVX-512 VNNI bodies for Q4_K's hostkernels (
crates/qdot, theBLOOMERY_QDOT_AVX512lever incrates/levers/src/registry.rs, and a test incrates/model/src/moe.rs) are read by the host tier's dot products, andhost_stack_runsincrates/model/src/ops.rsnow asks
qdot::q4k_avx512for a Q4_K stack at load; no plan reads either, and off x86_64 the answer is false, sothe aarch64 runs here are unchanged by it. The
plan_nodeshelpers and a paged test incrates/placement/src/placement/nodes.rsandnodes_scale.rsare called by no seat yet. The NVMe tier's step did notchange.
just mac-static,just mac-test(which runscargo test -p bloomery-placement) and the affected list are on thishead. The model lib tests, the Qwen seat's own tests and the three Qwen FAIL-first
runs are on this head on the third DGX Spark (aarch64), which also ran the Qwen3.8
--planruns at 64 GiB at thishead, and on
4685e4ab(this head onbaeedd73, the same patches) on the RTX 5090's host (x86_64, no card used). The GLM and V4.1 starts at serving defaults are on this head too, on the DGX Spark of theecad5fdcruns.The Qwen3.8
--plancomparison against main, at 64 GiB and at 124 GB, is on5dbcf8f6, an earlier version of this head: the sametarget_rule_of, without the all-host clause in the Qwen test and thefloor_fallbackchange. The 64 GiB plans do notreach
floor_fallback's new arm, because their asked plan is not refused. The other DGX Spark model tests, local-modegates, the serving-default loads against the base and the residency runs are on
ecad5fdc, with31abb2d5as the base.The aarch64 T1 run and the fixture-tier lanes are on the same patch before an earlier rebase,
9a8a17feonf382b474:git range-diff f382b474..9a8a17fe 31abb2d5..ecad5fdcshows it unchanged (=). Betweenf382b474and31abb2d5the changes undercrates/are #46's unset residency climb (8d85abd4, with its test inb56b7661), onedoc comment and the 0.3.2 version. Outside
crates/they are theresidency unsetrecord's schema (2a77736e), thereview tooling and workflows (
tools/review,.github/workflows, onetools/check-recipes.shline) and docs. I take8d85abd4to move the GLM start's residency word (What I ran into).The RTX 5090 runs, the FAIL-first runs and the Qwen
--plancomparison on the DGX Spark are on an earlier version ofthis commit,
51541b79on4f2697ee(the Spark's FAIL-first and Qwen builds have the same code as51541b79; one ofthem words two comments differently). Their base runs are on
c34cf00f, whosecrates/tree is the same as4f2697ee's.git range-diff 51541b79~1..51541b79 f382b474..9a8a17feshows one change in what the code does: #93'sheadroom lines in
plan_nextn_slots, now inside the closure. They run after the floor check and only on GLM's NextNplan, so those runs do not depend on them. The rest is context: the
routedtest helper now comes from #93, and thetools/gate-paths.tsvrow sits among newer rows.Tests:
a_unified_pool_lowers_the_card_budget_to_the_tiers_floor(placement) holdsplan_in_pool's rule on a scriptedplanner: the budgets it tries, the marker, the first plan's result for a discrete card, a given budget and a
shortfall past the whole budget, and the three named refusals.
slot_plans_on_one_pool_lower_the_card_budget_to_the_tiers_floor(model, glm5next) plans GLM's plain and NextN plansat one and two slots on a pool under the floor, checks that the card's whole bytes as a given budget is the floor's
refusal, and that a room under the NextN layer's experts is refused by name.
hw_placement_unified_pool_lowers_the_card_budget(model,tests/placement.rs, runs ingate-placement) plans theV4.1 file on a GB10's pool at one and two slots.
a_unified_pool_lowers_the_card_rules_budget_to_the_tiers_floor(model, qwen35moe) plans the Qwen card rule at oneand two slots on a 64 GiB pool that a synthetic file of 72.5 GB of routed experts does not fit: the card's whole bytes
as a given budget is the floor's refusal, and with no budget the plan is lowered, keeps card experts and pages the
rest. The all-host rule on a 32 GiB room of the same pool keeps
HostRoomFloor, notPoolBudget.the_floor_fallback_takes_the_floors_refusal_and_the_pools_only(bloomery-serve-qwen38, runs ingate-q38-serve-rules) holdsfloor_refused, the matchfloor_fallbackreads:HostRoomFloorandPoolBudgetare the floor's refusal, and
NvTierArena,Positionsand an error that is noPlaceErrorare not.paged_floor_fallback_serves_one_column(gate_qwen38_serve) holds the fallback'sHostRoomFloorpath end to endon a discrete card. None of
the runs here reached a lowered plan that is still refused.
FAIL-first:
At this head on the third DGX Spark (aarch64), and at
4685e4abon the RTX 5090's host (x86_64), each mutant onecommit on the head (each mutant has the same patch at both heads). The head passes both sets on both machines:
bloomery-model --lib -- arch::qwen35moe arch::glm5next arch::deepseek41122 passed,bloomery-serve-qwen38's tests 10 passed. Each mutant fails only its own test, with thesame counts and the same panic lines on both machines:
On the M5 at this head (each step's line cut after its time):
just mac-static upstream/main, whereupstream/mainis
fadec9d0(the lint step, clippy as the x86_64 Linux cross check, counts 41 warnings, the ratchet's 41; the combosstep ran 28 shapes, 0 red;
check-recipespassed, with its blocks held to three at a time).python3 tools/recipes.py affected fadec9d0 --no-boxat this head selects 147 of 159 gate recipes and one weekly recipe(
weekly-gpu-glm5next-e2e-long), the same list as atbaeedd73.just mac-testat this head, on the same M5:bloomery-placementpassed 166 (the 156 before plus the 10 main added inctx_ask.rsandctx.rs), and the one red of 1198 wasbloomery-refset'sarch::draft::tests::a_plain_target_with_no_draft_beside_is_the_family_path, whose two paths differ only in macOS's/varand/private/varspellings of the temporary directory. Main45836b17fails it the same way on the same Mac(1140 passed, 1 failed), and this change does not touch
crates/refset.On the Mac at
9a8a17fe(each step's line cut after its time; the lint step counts 41 warnings, the ratchet's 41; thecombos step ran 27 shapes, 0 red;
bloomery-placementpassed 145):aarch64 T1 on a second DGX Spark, in the dev container with no card. The tree is
git archive 9a8a17fecommitted intoa fresh repository, so the header names that repository's commit:
T1 unit lines
On the DGX Spark, in box.sh local mode in a CUDA 13.3.1 dev container (
BLOOMERY_BOX_LOCAL=1, noBLOOMERY_TIER, ongit archives of
31abb2d5andecad5fdc, the commits alternated per step; the V4.1 Q3_K_M and GLM UD-Q4_K_XL filesbind-mounted read-only at the
/modelspaths the tests open):The one red is an input this machine does not have: the DSpark draft.
gate-placementstops before itsbloomery-placement --libhalf after that red; the Mac'sjust mac-testabove runs those tests.bloomery-serveat serving defaults, base31abb2d5andecad5fdc, one start each, noBLOOMERY_*set. Built withcargo oxide build --arch sm_86 -- -p bloomery-gpu-gates --features glm5next,clef,vision,mimo2 --release --bin bloomery-serve(cargo-oxide87bc9518) in the same container and run on the host; on a GB10 the first start compilesthe GPU code for the card. Command:
bloomery-serve -m <first shard> --host 127.0.0.1 --port 18099, with the firstshards
DeepSeek-V4.1-Flash-Q3_K_M-00001-of-00009.gguf(vcruz305/DeepSeek-V4.1-Flash-GGUF) andGLM-5.3-Flash-UD-Q4_K_XL-00001-of-00006.gguf(unsloth/GLM-5.3-Flash-GGUF). Request:POST /v1/chat/completionswith{"messages":[{"role":"user","content":"What is the capital of France? Answer in one word."}],"max_tokens":48,"temperature":0}. Load times are the server's ownload ... inline.31abb2d5ecad5fdccard_budget=95784298496 card_experts=5337 (89518141440 B) paged=162241413120, answers "Paris"ctx rule=margin2 × 3072,card_budget=101466711940 card_experts=5867 (89203933184 B) paged=90972356608, MTP on (4 of 4 drafts accepted),residency=off(What I ran into), answers "Paris"At
a54fbc2a(this head), one start of each model at serving defaults on the DGX Spark (Hardware), with the same buildcommand, server command and request, from one build there. V4.1: loads in 28.7 s, 2 × 32768,
card_budget=96525809664 card_experts=5379 (90222612480 B) paged=161536942080, answers "Paris". GLM: loads in 52.6 s,ctx rule=margin2 × 3072,card_budget=100564674436 card_experts=5807 (88291672064 B) paged=91884617728, MTP on(5 of 5 drafts accepted),
residency=off, answers "Paris". Against the7448162fstarts (this head on45836b17, V4.1 onthe same DGX Spark, GLM on a second one) the output is the same line for line once numbers are masked, apart from the
binary hash, the version line, the response id and the GLM
cacheline's reason:the reading 123679408128 B leaves -67500156 B past need 123746908284 B + pool 0 Bwhere7448162fprintedthe default budget 112358338 B is under one saved state's 358962176 B. Both are the prompt cache's no-cache answer (ram=0); which one prints follows the start'sMemAvailablereading (121521272 kB here, 122329740 kB there), and that reading also moves the room,card_expertsand
pagedfrom run to run.When the V4.1 server stopped on SIGTERM after its answer, it printed
deepseek41 Body drop: the context did not synchronize before the ring shadows (1342177280 B) were freed: DriverError(4, ...)twice and exited with 0. Main7f3448bb, without this change, printed the same two lines at its SIGTERM on the same machine (V4.1 withBLOOMERY_CARD_BUDGETgiven, since main refuses it at the defaults). The message comes fromcrates/gpu-deepseek41,which this PR does not touch, and not every stop prints it: the
ecad5fdcstart above did not.On a third DGX Spark (Hardware) at
5dbcf8f6, in the same dev container:cargo test --release --locked -p bloomery-model --lib -- arch::qwen35moe arch::glm5next arch::deepseek41givestest result: ok. 122 passed; 0 failed,a_unified_pool_lowers_the_card_rules_budget_to_the_tiers_flooramong them.bloomery-serve --planforQwen3.8 UD-Q4_K_XL, one run each against main
45836b17: atBLOOMERY_HOST_ROOM=124000000000both print the sameoutput apart from the version line (
experts=card host_experts=0 card_experts=24576). AtBLOOMERY_HOST_ROOM=68719476736(64 GiB), with--ctx-size 8192 --parallel 2and with no--parallel, main refuseswith
HostRoomFloor(floor 71318999325 B and 70822551085 B, room 68719476736 B), and this change plans two slots of4096 tokens under a lowered card budget in both:
card_experts=16277 host_experts=8299, 25318604800 B paged, arena 0.With no
--parallelthe two slots are the default count, and the seat does not fall to one slot (point 7). Atthis head, on the third DGX Spark, the same lib tests give 122 passed, and the two 64 GiB
--planruns print theoutput of
5dbcf8f6apart from the version line, as they did at4685e4abthere and at7448162fon the second DGXSpark.
Models that meet the floor plan as before.
bloomery-serve --planwithBLOOMERY_HOST_ROOM=124000000000(one givenroom, so the readings cannot differ), base
c34cf00fand builds of the earlier version alternated, three runs each:the output for Qwen3.6-35B-A3B Q4_K_M (
Qwen3.6-35B-A3B-Q4_K_M.gguf) and for Qwen3.8 UD-Q4_K_XL(
Qwen3.8-Flash-Next-UD-Q4_K_XL-00001-of-00004.gguf) is identical line for line apart from the version line.On an RTX 5090 (a discrete card with 32 GB of its own) the plans do not change.
bloomery-serve --planwithBLOOMERY_HOST_ROOMgiven, basec34cf00fand51541b79alternated, three runs each: at 50000000000 the output forQwen3.6-35B-A3B Q4_K_M (the whole model on the card) and for Qwen3.8 UD-Q4_K_XL (5876 card experts, 12770816000 B
paged from the drive) is the same byte for byte on both commits and in every round. At 6000000000, under the NVMe
tier's floor, both commits refuse Qwen3.8 with the same text (room 6000000000 B under floor 10527985664 B):
plan_in_poolplans again only on a unified machine. At serving defaults51541b79loads Qwen3.6 in 7.0 s. The firststart's request (
max_tokens48) ended inside the thinking block; a second start, asked for up to 512 tokens, answers"Paris" after it. In local mode
just gate-leverspasses 40 tests.just gate-placementpasses 6 and fails the 5hw_placement_*tests,hw_placement_unified_pool_lowers_the_card_budgetamong them: each opens the DeepSeek V4.1Q3_K_M file, which this machine does not have, and the recipe stops there. Its second half,
bloomery-placement --lib -- --include-ignored, run on its own: 145 passed.The affected list at
9a8a17fe.python3 tools/recipes.py affected f382b474 --no-boxselects 144 of 156 gate recipes and oneweekly recipe (
weekly-gpu-glm5next-e2e-long), most of them becausecrates/modelis in their closure. Itsfixture-tier dry run (
bash tools/gate-batch.sh --list affected.txt --tier fixture --dry-run) puts 87 items in lanesand defers 65 to the real tier (34 real-only, 29 real-file, 2 of a family with no fixture).
9a8a17fe, onejust <recipe>per lane in local mode withBLOOMERY_TIER=fixture BLOOMERY_GATE_BOUND=1800, the fixturesfixture fetchmakes and the published reference sets(
gate-batch.shaddsBLOOMERY_GATE_CARD=3090ora6000to some items, which local mode refuses, so I left it out). Iran each lane that did not pass on
f382b474as well. 25 pass, among themgate-levers,gate-gpu-lib,gate-glm5next-meta(33 lib tests against 32 on the base, the new one;hw_glm5next_specread the real GLM file) andgate-gpu-swap. The other 62 are red on both commits with the same exit code and the same first stop (a lib test countone higher here in
gate-opsand the three-metalanes that run lib tests is the new test). By cause: 22 stop on anoracle or reference set that is not published (
ref/MANIFEST.tsv,ref_deepseek41_plain,ref_qwen35moeand thelike); 10 on a model or input file the machine does not have (the V2-Lite file, MiMo, a Qwen file the gate names, the
GLM projector, the routed-legs census stubs); 8 on the V4.1 DSpark draft fixture (exit 66); 7 on a fixture variant
(exit 66); 6 need both cards (
box.shexit 64); 6 onglm5next/corpus-prose.ids;gate-gpu-qwen38-serveon--place gate, which wants a 3090 by name;gate-ptx-spill(off a contributor's list, since its table issm_86only), whose scan reports local memory above that table on the GB10; and
gate-engram, whosehw_engram_expert_stack_evicts_and_reads_backfinds 2 of 7 pages still in the page cache after an eviction, on bothcommits. Of the five closest to the changed host path,
gate-ds41-loadstops at the draft fixture, the twogate-gpu-serve-start-*at the two-card refusal, andgate-gpu-glm5next-nvtierandgate-gpu-glm5next-residencyatcorpus-prose.ids(the residency gate's first 10 clauses pass on both commits). Sono fixture lane ran past its missing input into the changed plan; the real-file loads above (GLM and V4.1 at serving
defaults) are what exercise it.
gate-placementat the real tier with the V4.1 file (above). The others are left to themaintainer's run,
gate-gpu-glm5next-serveamong them: it is real-only and runs a real server on the GLM plan this PR changes.just narroworptx-scanpair for this change. The change touches no device crate(
crates/gpu*,crates/app), and on a card with memory of its own, or under a given budget,plan_in_poolreturnsthe first plan unchanged, so the workstation's plans should not change. Its gate binaries still link the changed code.
just stage-gpu-load-v41, whichAGENTS.mdruns by name for a change to the planner(
crates/model/src/placement.rsand its workstation and host-lock files). This PR changes the planner incrates/placement, whichcrates/model/src/placement.rsre-exports, so I count it as selected. It is real-only andstages plan (b) on both of the workstation's cards (
BLOOMERY_CARD=both), which local mode refuses, so it is left tothe maintainer's run.
What I ran into
This does not block this PR.
At serving defaults, GLM's unset residency word is now
offat--planand at the start, on a plan whose churnpool needs 0 B. On the DGX Spark with no lever set, the start at
ecad5fdcprinted (lines cut):It loaded and answered (the table above). A
--planrun of the same binary 14 s earlier printed the same word(
the plan leaves 15099780 B and the host's room -272173180 B). On the previous base,f382b474, the same patchprinted
offat--planandmid-p0-s1at a start 19 s later. So--planand the start now agree, and the start'sword went from
mid-p0-s1tooff. The GLM start ata54fbc2aprintedoffin the same way (the plan leaves 2717572 B and the host's room -604371068 B).What I read from the code and the numbers:
Between
f382b474and31abb2d5,8d85abd4(0.3.0 numbers for the README at serving defaults: DGX Spark and RTX 5090 #46) changed the room the unset climb reads.PlanRoom::mem_left(
crates/gpu-gates/src/residency.rs:53) is nowHostNeed::room_left(
crates/placement/src/placement/workstation.rs:665):plan_roomof a fresh reading, that is the reading lessHOST_DRIFT(512 MiB,:889and:899), less the plan's host need. Before, it was the freshMemAvailablelessthe need.
A split plan fills its room: its need, with the NextN layer's host experts the load holds beside it, is its own
plan_roomless what the plan leaves (the record's "the plan leaves"). 124650119168 B less 5629828 B is124644489340 B, the
cacheline'sneed[derived]. So the climb's room is now what the plan leaves less the dropin
MemAvailablebetween the plan's reading and the climb's: 5629828 B less 307589120 B (125186990080 B to124879400960 B) is -301959292 B, the printed
mem_leftto the byte [derived]. In the--planrun, 15099780 B less287272960 B is -272173180 B [derived]. Before
8d85abd4the climb hadHOST_DRIFTmore, so only a drop past512 MiB plus what the plan leaves turned the word off. That is why the earlier
--planand start could differ.The rule (
room_for,crates/levers/src/lib.rs:1486) keeps a word at P only whilemem_leftis at least the pool'sbytes. The pool is 0 B on these plans, because
ChurnPool::of(
crates/placement/src/placement/churn.rs:39) skips a stack with a segment on the NVMe tier. So the word turnsoffwhenevermem_leftis negative, which on a split plan is any drop larger than the few MB the plan leaves.Both runs dropped inside one process, 307589120 B and 287272960 B. The logs do not say what took those bytes.
The word does not change the load's host check.
HostNeed::grant(crates/gpu/src/model.rs:1878-1879) takes asextra bytes the churn pool's plus the host runs the load holds beside the plan (
GpuModel::load_placed_hosting,:723), and the pool is 0 B formid-p0-s1and foroff.mid-p0-s1addsChurnPool::check_beside(
:751), which a 0 B pool passes while the plan leaves bytes past the NextN layer's experts. The start on theprevious base loaded with
mid-p0-s1on the same plan shape.The word does change the runtime.
mid-p0-s1builds the residency machine (FileSwap::newandResidencyGlue::new,:772-788), which swaps experts into the card's slots;offbuilds none(
ResidencyGlue::off()). The start on the previous base printed 38residency passrecords, ten of them moving24 experts each (
made=24 bytes=364904448); the start atecad5fdcprinted none. On this plan the word halvesGLM's decode. The plan is GLM's default on one DGX Spark at
ecad5fdc, before engram, gpu, gpu-gates, levers, model, placement: on an integrated device the NVMe tier's arena is one write-combined pinned allocation the card maps, and a pin yields a slot's device address #120: it pages about 91 GB of expertswith no RAM arena (
arena=0 arena_slots=0in theplanline of all four starts below), so those experts are readthrough the file mapping. The numbers say nothing of a plan with an arena, engram, gpu, gpu-gates, levers, model, placement: on an integrated device the NVMe tier's arena is one write-combined pinned allocation the card maps, and a pin yields a slot's device address #120's pinned arena included.
One binary at
ecad5fdc, started four times (M, U, U, M), withBLOOMERY_RESIDENCY=mid-p0-s1(M) or no lever (U,which is
off),temperature0,max_tokens32, 18 requests of a 39-token prompt and 6 of a 2577-token prompt perstart, medians of the last five of each:
mid-p0-s1offoffmid-p0-s1The long prompt is 2577 tokens, not 4096, because the default plan holds 3072 tokens a slot, and a larger
--ctx-sizewould change the plan the word is picked on. The prompt bytes andprompt_nare the same in everystart. MTP accepted the same share of drafts in both arms (75 of 79 or 78 over the last five short requests), and
the SM clock's median under load was 2418 to 2424 MHz in all four with no clock event reason set.
mid-p0-s1rampsup over the first requests (1.25 and 1.29 tok/s over the first five short ones);
offis flat. These are thenumbers of a plan that pages 91 GB of experts from the drive, not the card's throughput on GLM. The ratio is close
to 0.3.0 numbers for the README at serving defaults: DGX Spark and RTX 5090 #46's on an RTX 5090 with Qwen3.8 under a 48 GiB limit: 99.0 tok/s with
mid-p0-s1and 43.6 tok/s withoff(arms C and D in 0.3.0 numbers for the README at serving defaults: DGX Spark and RTX 5090 #46 (comment)). At temperature 0 the
short prompt gave one output in all four starts, and the long prompt one output in all 12
offrequests, but 4 and6 distinct outputs over the 6 requests of each
mid-p0-s1start (their first 130 characters agree withoff's). Idid not trace this.
I take
8d85abd4as the cause because it is the only change to the climb between those two bases, and under the rulebefore it (the fresh
MemAvailableless the need) both runs would have kept the word: 5629828 B plus 536870912 Bless 307589120 B is 234911620 B at the start, and 15099780 B plus 536870912 B less 287272960 B is 264697732 B at
--plan[derived]. The start on the previous base, which keptmid-p0-s1, saw a drop of about the same size,303603712 B (its
planline'sroomto itscacheline'savailable). A control build confirms it:ecad5fdcwith all of
8d85abd4reverted (git revert --no-commit 8d85abd4, no conflicts), started alternately withecad5fdcitself at serving defaults (ecad5fdc, control, control,ecad5fdc,ecad5fdc, control), pickedmid-p0-s1in 3 of 3 starts and printed 45, 38 and 25residency passrecords;ecad5fdcpickedoffin 3 of 3with none. The
planlines match between the two builds; only the climb's room differs. On every start ofecad5fdcthe printedmem_leftis thecacheline'savailableless itsneed, lessHOST_DRIFT, to the byte,and that difference was 179 to 207 MB, so under the rule before
8d85abd4the word would have stayed [derived]. The control reverts thewhole commit, so its record has no
mem_left; I did not build a narrower one.To see it: on a DGX Spark at
ecad5fdcwith noBLOOMERY_*set, runbloomery-serve -m <GLM-5.3-Flash UD-Q4_K_XL, first shard> --plan, then a real start. Compare theplanline'sroomwith theresidency unsetline'sroom,mem_leftand "the plan leaves".mem_leftis what the plan leaves less the difference of the two rooms. A givenBLOOMERY_HOST_ROOMhides it: both rooms are then the given bytes, somem_leftequals what the plan leaves.How it meets #46: section 1 of my comment there (#46 (comment))
reports load refusals on an RTX 5090 from a churn pool sized against a reading the load's check did not see.
8d85abd4gives such a pool the
HOST_DRIFTmargin, as direction (a) there suggested. On a split plan whose pool is 0 B, thesame margin turns the word
offwithout changing the load's check.Directions I can see, for the maintainer to choose between or set aside:
HOST_DRIFTin the room the NVMe split fills, so a split plan leaves bytes for the dropbetween the plan's reading and the climb's. It costs that many bytes of host-served experts, which are then read
from the drive.
mem_leftis. The word adds only the pool's bytes to the load's host check(above), so this plan's word would stay at
mid-p0-s1, and plans with a pool keep the margin 0.3.0 numbers for the README at serving defaults: DGX Spark and RTX 5090 #46 needed. Thisreverses the shared
room_forrule thatcrates/levers/src/tests.rs:917-928pins ("A negative budget does not takeeven an empty pool, at any count"), so it changes Qwen3.8 too. That pin predates
8d85abd4: a negativemem_leftthen meant
MemAvailableunder the need, and now it means a reading under the need plusHOST_DRIFT.--planand the start then agreefor one reading, and on a split plan that fills its room
mem_leftis what the plan leaves. TheHOST_DRIFTmargin
8d85abd4gave a pool then counts from the plan's reading, so a drop between that reading and the climb'scomes out of it.
Hardware (if the change is for a machine)
--versionbloomery-serve 0.3.2 (commit ecad5fdc),bloomery-serve 0.3.2 (commit a54fbc2a)NVIDIA GB10, 595.84, 12.1(name, driver, compute capability); CUDA 13.2 innvidia-smi; Ubuntu 24.04.5, kernel 6.17.0-1032-nvidiabloomery-serveat serving defaults (onecad5fdcand31abb2d5, and GLM and V4.1 ona54fbc2a), the Qwen--plancomparison and FAIL-first (earlier builds); built in a CUDA 13.3.1 container with cargo-oxide87bc9518bloomery-serve 0.3.2 (commit 7448162f)nvidia-smi, same kerneljust contrib-check --tier t1in the container with no card; with two more of the same machines, the 87 fixture-tier lanes on9a8a17feand the red ones onf382b474; at7448162f, the model lib tests,bloomery-serve-qwen38's tests, the three Qwen FAIL-first runs, the Qwen3.8--planruns at 64 GiB and GLM at serving defaults (not this head's starts), built in the same container with the same commandbloomery-serve 0.3.1 (commit 51541b79)NVIDIA GeForce RTX 5090, 595.99.02, 32607 MiB; Ubuntu 26.04.1, kernel 7.0.0-38-generic--plancomparisons,bloomery-serveat serving defaults,just gate-levers,just gate-placementand itsbloomery-placement --libhalf; built on the host with the samecargo oxide buildcommand and cargo-oxide87bc9518. At4685e4ab, host only: the model lib tests,bloomery-serve-qwen38's tests (tools/gate.sh --oxide,--arch sm_86) and the three Qwen FAIL-first runsbloomery-serve 0.3.2 (commit 5dbcf8f6),bloomery-serve 0.3.2 (commit 4685e4ab),bloomery-serve 0.3.2 (commit a54fbc2a)nvidia-smi--planruns at 64 GiB and 124 GB (on5dbcf8f6, its one-call mutant and45836b17); at this head, the model lib tests,bloomery-serve-qwen38's tests, the three Qwen FAIL-first runs and the Qwen3.8--planruns at 64 GiB (on4685e4abanda54fbc2a); same container and build commandjust mac-static(on9a8a17feand this head),just mac-test(on9a8a17feand this head, withcargo test -p bloomery-placement),just affected(this head)The
nvidia-smilines. DGX Spark, from the conditions log of theecad5fdcruns (the header and the card's row):The second DGX Spark's conditions log at
7448162f, the third's at this head and the DGX Spark's at this head show the same two lines.RTX 5090, from the environment log of the
51541b79runs:AI help
AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.