Skip to content

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

Merged
juliankang4 merged 5 commits into
midagedev:mainfrom
juliankang4:dgx-spark-unified-floor-on-glmres
Oct 14, 2026
Merged

juliankang4 merged 5 commits into
midagedev:mainfrom
juliankang4:dgx-spark-unified-floor-on-glmres

Conversation

@juliankang4

@juliankang4 juliankang4 commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

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 only BLOOMERY_CARD_BUDGET loaded them. With this change both load at serving defaults and
answer, 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 at ecad5fdc (this code before the maintainer moved it onto main), and more on earlier builds of
it. 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 up
to the reading, so the floor's base alone passes the room. At 31abb2d5, serving defaults:

V4.1: the host room 123721699328 B is under the NVMe expert tier's floor 152636790784 B
      (the host terms beside the routed experts 131615463424 B + 3 × the heaviest layer's host-served experts 7007109120 B ...)
GLM:  the default's second slot does not fit on --place a (... the host room 119520993280 B is under the NVMe expert
      tier's floor 141598891076 B ...); at one slot, floor 141240911664 B

placement::plan_in_pool takes 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 lower card budget, until the floor holds. The first budget is
capped(card, None) for machine.cards.first(): the card's usable bytes cut to its free reading plus the context's own
creation 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 that
shortfall is smaller. The plan it returns is marked Plan::budget_from_pool. A shortfall past the card's whole budget
is 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 budget
and the last refusal. The V4.1 plan (plan_with_slots), both GLM plans (plan_slots, plan_nextn_slots) and the
Qwen 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 named BLOOMERY_CARD_BUDGET ... so unset it for any plan made
under a budget; it now does so only for a budget the caller gave. The lever's unset text in levers/src/registry.rs
and the PlanLevers doc now say what unset means on a unified pool.

The docs commit (a54fbc2a) brings the docs up to date with this change: in docs/contrib/dgx-spark.md the P4 row, the last
bullet 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.md row 4 of (k), which waited on this refusal.

Not wired, with the reason:

  • Qwen's all-host rule (BLOOMERY_QWEN38_EXPERTS=host). 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 round after 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 (the FAIL-first run below: 3 rounds, down to 5348147056 B), or CardBudgetFloor when the
    budget passes under the card's own floor first. So target_rule_of plans that rule once, and its refusal stays
    HostRoomFloor.
    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=0 in its plan line), so the loop runs only when the
    pool reads less, for example while another process holds part of it (point 7 below).
  • MiMo2's body serves no NVMe expert tier. Its plan runs the split first and then refuses a plan that pages
    (PlaceError::NoNvmeTier, crates/model/src/arch/mimo2/place.rs:244). A lower card budget only pages more, so a
    MiMo2 file above the pool keeps the floor's refusal.

Points for the maintainer:

  1. The loop. While no expert leaves the card, the floor does not move, so the loop repeats the same step until one
    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.md S10 ("One budget solver") would replace this kind of rule with one solver. I kept the
    loop because it reuses the planner as it is. If a computed budget is preferred here, I will change it.
  2. The printed plan and the loaded plan. The GLM session plans again at load with a fresh room reading, so the loaded
    plan can carry a different budget from the plan line the seat printed; it follows the reading. The split already
    moved with the reading before this change.
  3. Multi-card unified machines. The first budget is machine.cards.first()'s. A GB10 has one card; a unified machine
    with several stage cards would need a budget per card.
  4. glm5next: a split NextN plan's host headroom keeps the draft's routed experts, as an unsplit one's does, so the residency rule and the load take them out once #93, now on main. Its lines that give a split NextN plan's headroom the draft's routed experts back sit inside the
    plan_nextn_slots closure, so the plan of every round gets them. The prompt cache stays at 0 on GLM's paged plan
    for another reason: at the ecad5fdc load below, the default budget 117455810 B is under one saved state's 358962176 B.
  5. No arena at serving defaults. Both default plans on the DGX Spark print 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 plan lines
    of the GLM starts at ecad5fdc and a54fbc2a and the --plan run at ecad5fdc below: the line's host_experts bytes plus paged [derived]), and there nvme_arena_of gives no arena: the tier pages through the
    file 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:
    HostNeed counts 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 moved Machine's host and pool
    into Machine::nodes and plans every machine as before.
  6. A given arena with no given card budget. The loop stops at the first budget whose plan meets the floor, so the room
    is left only a few MB above it, and a given BLOOMERY_NVTIER_BYTES does not fit. On a DGX Spark, GLM at serving
    defaults 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=24G is refused with
    PoolBudget once the loop has lowered the budget to the floor, naming the arena 25769803776 B past the room less
    the floor (4211644 B [derived]);
    BLOOMERY_NVTIER_BYTES=20G is refused with NvTierArenaUnder (the arena's least is 23867015168 B). With a card budget
    given as well, the loop is not used: BLOOMERY_CARD_BUDGET=70G BLOOMERY_NVTIER_BYTES=23G plans
    arena=24696061952 arena_slots=38. Counting a given arena in the shortfall would let the loop plan it. This PR does
    not do that, because it changes what the lever asks for on a pool. The card leg needs an arena, so
    docs/contrib/dgx-spark.md lists this under P4's open items.
  7. Qwen's seat on a pool that reads less. 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 lowered
    card 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=68719476736 and no --parallel (What I ran), main
    refuses the load (HostRoomFloor, floor 70822551085 B), and this change plans the default two slots of 4096 tokens
    with 16277 card experts, 25318604800 B paged and no arena. Where the lowered plan is still refused, the refusal is
    PoolBudget, and floor_fallback now takes it as it takes HostRoomFloor (floor_refused, with its own test), so
    the 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)?
  8. placement: a model's plan over several unified nodes (dgx-spark.md §9), and nodes_scale on one to four Sparks #95's node plans. NodeBudget::of takes a node's budget from a card plan of that node, and nodes_scale makes
    those plans with no budget on pools that hold the model, so nothing there goes through plan_in_pool. For a model
    above one node's pool, the engine side that makes node 0's card plan would need it from plan_in_pool, as the
    families' plans here do. Plan::pool_need, which NodeBudget::of and the PoolOver check read, counts the same terms for either plan:
    budget_from_pool changes only HostNeed'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, which
no call reached: every call ended with a plan that met the floor. It sat on an older main, without #93.

  • V4.1: two plan_in_pool calls, two rounds each. The second round's step was the shortfall (13842432 B and
    14178304 B).
  • GLM: eleven calls (the context ladder, and the session's own plan at load), twenty rounds in all: seven calls of one
    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 the gate-q38-serve-rules recipe), tools/gate-paths.tsv, and in the docs commit
docs/contrib/dgx-spark.md and docs/research/tierengine.md.

This head is on main fadec9d0, which has #95 and #157. The rebase onto 33fb317b had two conflicts, and both keep
main's text with this PR's change on top: the crates/models/src/shape.rs row of tools/gate-paths.tsv, where main
added a gate and this PR adds gate-placement, and the table in docs/contrib/dgx-spark.md, where this PR's P4 row
sits next to #95's P5 row. The docs commit then also updated the two passages from #95 named above. The rebase from
33fb317b onto baeedd73 had one conflict, the same tools/gate-paths.tsv row (main added
gate-gpu-minicpm-text), resolved the same way; the rebase from baeedd73 onto fadec9d0 had none. git range-diff baeedd73..4685e4ab fadec9d0..a54fbc2a shows all four commits unchanged (=). The maintainer's commit keeps its patch
and author; only its hash moved, from e5a304df through eeb9ef47 to 57fe1647. Main changed justfile and
crates/levers/src/registry.rs too, 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 of
slots::split_ctx and ctx::largest live in placement::ctx_ask, which no seat calls yet, and this PR's diff names
none of them. Once a seat on a unified machine calls ctx_ask::choose through plan_in_pool, card_short will need
to read a PoolBudget whose last refusal is CardBudgetFloor as a short card.

Code shape

The review reads these first. A box left unticked needs one line of why.

  • No second copy. The retry lives once, in placement::plan_in_pool; the four call sites (V4.1, GLM's two
    plans, Qwen's card rule) pass their own plan and split as a closure.
  • Model code holds only architecture facts. The families' code only moved into the closure; the common owner
    has no per-model branch.
  • Dead code is gone.
  • No silent failure. A retry that fails ends in PlacementError::PoolBudget with the rounds, the budget and
    the last refusal; it never returns a defined-looking plan.
  • Comments state what is true now. tools/check-comments.sh is green.
  • No relaxed gate. One row of tools/gate-paths.tsv gains gate-placement: the new V4.1 test reaches
    crates/models/src/shape.rs through the V4.1 plan inputs, and just check-recipes asked for it.
  • cargo fmt and cargo clippy are 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. c6e15d4f is this change, which the maintainer moved onto main, and
57fe1647 is the maintainer's fix for #94's move of Machine's host and pool into Machine::nodes: plan_in_pool
reads machine.unified(), and the new placement test builds its machine with one Node. d0c51f1f puts Qwen's card
rule through plan_in_pool (target_rule_of in crates/model/src/arch/qwen35moe/place.rs, and its test) and lets
the Qwen seat's floor_fallback take PoolBudget (floor_refused in
crates/gpu-gates/src/bin/shared/serve_seats/qwen38.rs, tested in bloomery_serve_qwen38.rs), and a54fbc2a is the
docs.
ad7aa155 answers the review: plan_in_pool now returns a result other than the floor's refusal before it reads machine.unified(), so a machine with no node gets the planner's Nodes { nodes: 0 } instead of a panic in this_node(), and the placement test has that case. On a machine with a node it returns what it returned at a54fbc2a in every case, so the runs below, which name a54fbc2a as this head, stand for ad7aa155 too. At ad7aa155 on the Mac (M4 Pro), cargo test -p bloomery-placement passes 166 of 166 and cargo fmt --check is clean; with the old order the new case panics at this_node().
On main baeedd73 the same four commits were 896ca0d8, eeb9ef47, 7dd9b23b and 4685e4ab; on main
33fb317b they were 6fb48f2a, e5a304df, d41c07d8 and 7971e341; on main
45836b17 they were 74a94611, d15ed697, c42f3fee and 7448162f; before the maintainer's move the code was
ecad5fdc on 31abb2d5.
git range-diff 31abb2d5..ecad5fdc 45836b17..74a94611 shows only the imports and context lines #94 changed, and
d15ed697 adds the two lines above. Between 31abb2d5 and 45836b17, the changes under crates/ on the plan's path are #94's (the
Machine::nodes move, which plans every machine as before) and #120's (S7: the arena's memory kind; HostNeed counts
the 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 45836b17 and 33fb317b, the changes under crates/ on the plan's path are these. #95's node plans
(crates/placement/src/placement/nodes.rs, and nodes_scale.rs for its tests) are called by nothing on the serving
path yet. The PoolOver check moved into Plan::pool_need with the same terms. plan_rule now refuses a machine of
more than one node (Nodes) and a pool whose bytes pass u64 (PoolPastU64); neither is HostRoomFloor, so
plan_in_pool returns them as they are. The workstation's plan (a) asked for a draft (6d11ac94 and the two commits before it, for gate_load_v41) and
paged_draft at arena 0 (acfb220a, Qwen3.8's reduced load only) do not reach the GLM and V4.1 plans. Off the plan's
path, 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 33fb317b and baeedd73, the changes under crates/ on the plan's path are these. 1f3f36ba checks a set
--ctx against the model's limit before any plan (ctx::check_slot_ctx), and Qwen's serve_ctx now delegates to it
with the same refusal; at serving defaults no --ctx is set. 19f162c9 and #157 add ctx_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 baeedd73 and fadec9d0, the changes under crates/ are these. #86's AVX-512 VNNI bodies for Q4_K's host
kernels (crates/qdot, the BLOOMERY_QDOT_AVX512 lever in crates/levers/src/registry.rs, and a test in
crates/model/src/moe.rs) are read by the host tier's dot products, and host_stack_runs in crates/model/src/ops.rs
now asks qdot::q4k_avx512 for a Q4_K stack at load; no plan reads either, and off x86_64 the answer is false, so
the aarch64 runs here are unchanged by it. The plan_nodes helpers and a paged test in
crates/placement/src/placement/nodes.rs and nodes_scale.rs are called by no seat yet. The NVMe tier's step did not
change.

just mac-static, just mac-test (which runs cargo test -p bloomery-placement) and the affected list are on this
head. 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 --plan runs at 64 GiB at this
head, and on 4685e4ab (this head on baeedd73, 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 the ecad5fdc runs.
The Qwen3.8
--plan comparison against main, at 64 GiB and at 124 GB, is on 5dbcf8f6, an earlier version of this head: the same
target_rule_of, without the all-host clause in the Qwen test and the floor_fallback change. The 64 GiB plans do not
reach floor_fallback's new arm, because their asked plan is not refused. The other DGX Spark model tests, local-mode
gates, the serving-default loads against the base and the residency runs are on ecad5fdc, with 31abb2d5 as the base.
The aarch64 T1 run and the fixture-tier lanes are on the same patch before an earlier rebase, 9a8a17fe on
f382b474: git range-diff f382b474..9a8a17fe 31abb2d5..ecad5fdc shows it unchanged (=). Between f382b474 and
31abb2d5 the changes under crates/ are #46's unset residency climb (8d85abd4, with its test in b56b7661), one
doc comment and the 0.3.2 version. Outside crates/ they are the residency unset record's schema (2a77736e), the
review tooling and workflows (tools/review, .github/workflows, one tools/check-recipes.sh line) and docs. I take
8d85abd4 to move the GLM start's residency word (What I ran into).

The RTX 5090 runs, the FAIL-first runs and the Qwen --plan comparison on the DGX Spark are on an earlier version of
this commit, 51541b79 on 4f2697ee (the Spark's FAIL-first and Qwen builds have the same code as 51541b79; one of
them words two comments differently). Their base runs are on c34cf00f, whose crates/ tree is the same as
4f2697ee's. git range-diff 51541b79~1..51541b79 f382b474..9a8a17fe shows one change in what the code does: #93's
headroom lines in plan_nextn_slots, now inside the closure. They run after the floor check and only on GLM's NextN
plan, so those runs do not depend on them. The rest is context: the routed test helper now comes from #93, and the
tools/gate-paths.tsv row sits among newer rows.

Tests:

  • a_unified_pool_lowers_the_card_budget_to_the_tiers_floor (placement) holds plan_in_pool's rule on a scripted
    planner: 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 plans
    at 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 in gate-placement) plans the
    V4.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 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 keeps HostRoomFloor, not PoolBudget.
  • the_floor_fallback_takes_the_floors_refusal_and_the_pools_only (bloomery-serve-qwen38, runs in
    gate-q38-serve-rules) holds floor_refused, the match floor_fallback reads: HostRoomFloor and PoolBudget
    are the floor's refusal, and NvTierArena, Positions and an error that is no PlaceError are not.
    paged_floor_fallback_serves_one_column (gate_qwen38_serve) holds the fallback's HostRoomFloor path end to end
    on a discrete card. None of
    the runs here reached a lowered plan that is still refused.

FAIL-first:

plan_in_pool reduced to one call of the closure (Mac):
  assertion `left == right` failed
    left: [None]
   right: [None, Some(64424509440), Some(62277025792), ... Some(60129542144)]
all three call sites put back to one call of the closure (DGX Spark, one build, both tests red):
  V4.1, 1 slot(s): the host room 123691634688 B is under the NVMe expert tier's floor 151169808384 B ...
  V4.1, 2 slot(s): the host room 123691634688 B is under the NVMe expert tier's floor 152504670208 B ...
  GLM,  1 slot(s): the host room 8053063680 B is under the NVMe expert tier's floor 9483679332 B ...
plan_nextn_slots alone put back (DGX Spark):
  1 slot(s) with the next-token layer: the host room 8053063680 B is under the NVMe expert tier's floor 9638868592 B ...
Qwen's target_rule_of back to one call (DGX Spark, on 5dbcf8f6; 5dbcf8f6 itself passes the test):
  1 slot(s): the host room 68719476736 B is under the NVMe expert tier's floor 72652636208 B ...

At this head on the third DGX Spark (aarch64), and at 4685e4ab on the RTX 5090's host (x86_64), each mutant one
commit 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::deepseek41 122 passed, bloomery-serve-qwen38's tests 10 passed. Each mutant fails only its own test, with the
same counts and the same panic lines on both machines:

target_rule_of back to one call (bloomery-model: 121 passed, 1 failed):
  1 slot(s): the host room 68719476736 B is under the NVMe expert tier's floor 72652636208 B ...
both Qwen rules through plan_in_pool, the shape suggested in review (bloomery-model: 121 passed, 1 failed):
  the all-host rule: not the floor's refusal: Some(Placement(PoolBudget { rounds: 3, budget: 5348147056,
  last: HostRoomFloor { room: 34359738368, floor: 55483514928, ... } }))
floor_refused without its PoolBudget arm (bloomery-serve-qwen38: 9 passed, 1 failed):
  bloomery_serve_qwen38.rs:157:9: PoolBudget

On the M5 at this head (each step's line cut after its time): just mac-static upstream/main, where upstream/main
is fadec9d0 (the lint step, clippy as the x86_64 Linux cross check, counts 41 warnings, the ratchet's 41; the combos
step ran 28 shapes, 0 red; check-recipes passed, with its blocks held to three at a time). python3 tools/recipes.py affected fadec9d0 --no-box at this head selects 147 of 159 gate recipes and one weekly recipe
(weekly-gpu-glm5next-e2e-long), the same list as at baeedd73.

$ just mac-static upstream/main
mac-static: fmt-check rc 0 in 12 s
mac-static: lint rc 0 in 38 s
mac-static: combos rc 0 in 63 s
mac-static: check-arch, check-comments, check-defaults, check-levers, check-loads, check-ptx-names, check-records,
            check-rustflags, check-unsafe, check-waits: rc 0 each
mac-static: check-recipes rc 0 in 129 s
mac-static: ok

just mac-test at this head, on the same M5: bloomery-placement passed 166 (the 156 before plus the 10 main added in
ctx_ask.rs and ctx.rs), and the one red of 1198 was bloomery-refset's
arch::draft::tests::a_plain_target_with_no_draft_beside_is_the_family_path, whose two paths differ only in macOS's
/var and /private/var spellings of the temporary directory. Main 45836b17 fails 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; the
combos step ran 27 shapes, 0 red; bloomery-placement passed 145):

$ just mac-static upstream/main
mac-static: fmt-check rc 0 in 38 s
mac-static: lint rc 0 in 76 s
mac-static: combos rc 0 in 80 s
mac-static: check-arch, check-comments, check-defaults, check-levers, check-loads, check-ptx-names, check-records,
            check-recipes, check-rustflags, check-unsafe, check-waits: rc 0 each
mac-static: ok
$ just mac-test
mac-check: test rc 0; 12 crates (crate=rc): ... bloomery-placement=0 ...; 1139 passed, 0 failed, 177 ignored in 45 test binaries

aarch64 T1 on a second DGX Spark, in the dev container with no card. The tree is git archive 9a8a17fe committed into
a fresh repository, so the header names that repository's commit:

$ just contrib-check --tier t1
contrib-check tier=t1 commit=863fa993
host: Linux 6.17.0-1032-nvidia aarch64, unknown
toolchain: rustc 1.100.0-nightly (e457a7b0d 2026-08-27) (nightly-2026-08-28); RUSTFLAGS: not set (the repo's .cargo/config.toml applies); timeout 1200s a unit
...
DONE tier=t1 total=94 red=0 wall=357s
T1 unit lines
fmt rc=0 3s
test-bloomery-decision rc=0 6s
test-bloomery-hf rc=0 3s
test-bloomery-jinja rc=0 2s
test-bloomery-levers rc=0 2s
test-bloomery-models rc=0 0s
test-bloomery-placement rc=0 82s
test-bloomery-refset rc=0 4s
test-bloomery-runtime rc=0 8s
test-bloomery-sampler rc=0 11s
test-bloomery-serve rc=0 8s
test-bloomery-tokenizer rc=0 1s
test-bloomery-vision rc=0 4s
recipes-self-test rc=0 21s
tools/bloomery/manifest.py rc=0 0s
tools/bloomery/records.py rc=0 0s
tools/bloomery/rows.py rc=0 0s
tools/bloomery/route_trace.py rc=0 0s
tools/board/board.py rc=0 1s
tools/boxq-check.py rc=0 0s
tools/boxq.py rc=0 17s
tools/check-comment-only.py rc=0 0s
tools/check-defaults.py rc=0 0s
tools/check-ptx-names.py rc=0 0s
tools/check-records.py rc=0 5s
tools/flow/ds41_prefill.py rc=0 4s
tools/flow/pplb.py rc=0 0s
tools/flow/q38_step.py rc=0 0s
tools/flow/q38width.py rc=0 0s
tools/flow/routes.py rc=0 0s
tools/hf-arch-check.py rc=0 1s
tools/mac-disk.py rc=0 1s
tools/ref/asm-census.py rc=0 0s
tools/ref/check-int-twins.py rc=0 0s
tools/ref/clef/e2e.py rc=0 0s
tools/ref/clef_ref.py rc=0 0s
tools/ref/clefvis/check_hidden.py rc=0 0s
tools/ref/clefvis/check_preproc.py rc=0 0s
tools/ref/dma-dram-share.py rc=0 0s
tools/ref/draft-accept.py rc=0 0s
tools/ref/draft-vocab.py rc=0 0s
tools/ref/ds41copy.py rc=0 0s
tools/ref/ds41pp.py rc=0 0s
tools/ref/flat-imatrix.py rc=0 0s
tools/ref/gguf-ranges.py rc=0 0s
tools/ref/gguf-respell.py rc=0 1s
tools/ref/hidden-diff.py rc=0 0s
tools/ref/lev_ref.py rc=0 0s
tools/ref/loop-floor.py rc=0 0s
tools/ref/nsys-bridge.py rc=0 0s
tools/ref/ptx-canon.py rc=0 0s
tools/ref/qdot-rate-floors.py rc=0 0s
tools/ref/route-trace-chat.py rc=0 0s
tools/ref/router-coverage.py rc=0 0s
tools/ref/router-residency.py rc=0 2s
tools/ref/set-diff.py rc=0 0s
tools/ref/window-union.py rc=0 0s
tools/review/diagnostics.py rc=0 0s
tools/review/ledger.py rc=0 0s
tools/review/post.py rc=0 0s
tools/review/signals.py rc=0 0s
tools/review/usage.py rc=0 0s
tools/type-census.py rc=0 1s
tools/verdict-diff.py rc=0 0s
check-arch rc=0 1s
check-comments rc=0 4s
check-defaults rc=0 1s
check-levers rc=0 6s
check-loads rc=0 0s
check-ptx-names rc=0 1s
check-recipes rc=0 67s
check-records rc=0 5s
check-rustflags rc=0 0s
check-unsafe rc=0 1s
check-waits rc=0 0s
bloomery-app rc=0 25s
bloomery-decision rc=0 5s
bloomery-engram rc=0 2s
bloomery-engram-lab rc=0 0s
bloomery-gguf rc=0 3s
bloomery-hf rc=0 3s
bloomery-jinja rc=0 2s
bloomery-levers rc=0 2s
bloomery-model rc=0 10s
bloomery-models rc=0 1s
bloomery-placement rc=0 6s
bloomery-qdot rc=0 3s
bloomery-refset rc=0 4s
bloomery-runtime rc=0 3s
bloomery-sampler rc=0 1s
bloomery-serve rc=0 10s
bloomery-threads rc=0 0s
bloomery-tokenizer rc=0 1s
bloomery-vision rc=0 2s
DONE tier=t1 total=94 red=0 wall=357s

On the DGX Spark, in box.sh local mode in a CUDA 13.3.1 dev container (BLOOMERY_BOX_LOCAL=1, no BLOOMERY_TIER, on
git archives of 31abb2d5 and ecad5fdc, the commits alternated per step; the V4.1 Q3_K_M and GLM UD-Q4_K_XL files
bind-mounted read-only at the /models paths the tests open):

$ ./tools/box.sh 'cargo test --release -p bloomery-model --test placement -- --ignored hw_placement_unified_pool --nocapture'
1 slot(s), the card's whole bytes: room 123691634688 B, floor 151169808384 B
1 slot(s): card_budget Some(97034496000) card experts 5416 paged 160916336640 B
2 slot(s), the card's whole bytes: room 123691634688 B, floor 152504670208 B
2 slot(s): card_budget Some(95699634176) card experts 5332 paged 162325278720 B
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 10 filtered out
$ ./tools/box.sh 'cargo test --release -p bloomery-model --lib -- arch::glm5next arch::deepseek41'
test result: ok. 50 passed; 0 failed; 0 ignored; 0 measured; 239 filtered out
                     31abb2d5                        ecad5fdc
lib glm5next/ds41:   ok. 49 passed; 0 failed         ok. 50 passed; 0 failed
gate-levers:         ok. 40 passed; 0 failed         ok. 40 passed; 0 failed
gate-placement:      FAILED. 9 passed; 1 failed      FAILED. 10 passed; 1 failed
                                                     hw_placement_unified_pool_lowers_the_card_budget ... ok
gate-glm5next-meta:  (lib) ok. 32 passed; 0 failed   (lib) ok. 33 passed; 0 failed
                     hw_glm5next_spec ... ok         hw_glm5next_spec ... ok
red on both:         hw_placement_bp: open /models/DeepSeek-V4.1-Flash-DSpark/DeepSeek-V4.1-Flash-Fp8-128x742M-MXFP4_MOE.tl37.gguf:
                       split shard ... is missing

The one red is an input this machine does not have: the DSpark draft. gate-placement stops before its
bloomery-placement --lib half after that red; the Mac's just mac-test above runs those tests.

bloomery-serve at serving defaults, base 31abb2d5 and ecad5fdc, one start each, no BLOOMERY_* set. Built with
cargo oxide build --arch sm_86 -- -p bloomery-gpu-gates --features glm5next,clef,vision,mimo2 --release --bin bloomery-serve (cargo-oxide 87bc9518) in the same container and run on the host; on a GB10 the first start compiles
the GPU code for the card. Command: bloomery-serve -m <first shard> --host 127.0.0.1 --port 18099, with the first
shards DeepSeek-V4.1-Flash-Q3_K_M-00001-of-00009.gguf (vcruz305/DeepSeek-V4.1-Flash-GGUF) and
GLM-5.3-Flash-UD-Q4_K_XL-00001-of-00006.gguf (unsloth/GLM-5.3-Flash-GGUF). Request:
POST /v1/chat/completions with {"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 own load ... in line.

Model 31abb2d5 ecad5fdc
V4.1 Q3_K_M refused, room 123721699328 B < floor 152636790784 B loads in 28.0 s, 2 × 32768, card_budget=95784298496 card_experts=5337 (89518141440 B) paged=162241413120, answers "Paris"
GLM-5.3 UD-Q4_K_XL refused, room 119520993280 B < floor 141240911664 B loads in 50.9 s, ctx rule=margin 2 × 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 build
command, 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=margin 2 × 3072, card_budget=100564674436 card_experts=5807 (88291672064 B) paged=91884617728, MTP on
(5 of 5 drafts accepted), residency=off, answers "Paris". Against the 7448162f starts (this head on 45836b17, V4.1 on
the 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 cache line's reason: the reading 123679408128 B leaves -67500156 B past need 123746908284 B + pool 0 B where 7448162f printed the 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's
MemAvailable reading (121521272 kB here, 122329740 kB there), and that reading also moves the room, card_experts
and paged from 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. Main
7f3448bb, without this change, printed the same two lines at its SIGTERM on the same machine (V4.1 with
BLOOMERY_CARD_BUDGET given, since main refuses it at the defaults). The message comes from crates/gpu-deepseek41,
which this PR does not touch, and not every stop prints it: the ecad5fdc start 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::deepseek41 gives test result: ok. 122 passed; 0 failed, a_unified_pool_lowers_the_card_rules_budget_to_the_tiers_floor among them. bloomery-serve --plan for
Qwen3.8 UD-Q4_K_XL, one run each against main 45836b17: at BLOOMERY_HOST_ROOM=124000000000 both print the same
output apart from the version line (experts=card host_experts=0 card_experts=24576). At
BLOOMERY_HOST_ROOM=68719476736 (64 GiB), with --ctx-size 8192 --parallel 2 and with no --parallel, main refuses
with HostRoomFloor (floor 71318999325 B and 70822551085 B, room 68719476736 B), and this change plans two slots of
4096 tokens under a lowered card budget in both: card_experts=16277 host_experts=8299, 25318604800 B paged, arena 0.
With no --parallel the two slots are the default count, and the seat does not fall to one slot (point 7). At
this head, on the third DGX Spark, the same lib tests give 122 passed, and the two 64 GiB --plan runs print the
output of 5dbcf8f6 apart from the version line, as they did at 4685e4ab there and at 7448162f on the second DGX
Spark.

Models that meet the floor plan as before. bloomery-serve --plan with BLOOMERY_HOST_ROOM=124000000000 (one given
room, so the readings cannot differ), base c34cf00f and 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 --plan with
BLOOMERY_HOST_ROOM given, base c34cf00f and 51541b79 alternated, three runs each: at 50000000000 the output for
Qwen3.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_pool plans again only on a unified machine. At serving defaults 51541b79 loads Qwen3.6 in 7.0 s. The first
start's request (max_tokens 48) ended inside the thinking block; a second start, asked for up to 512 tokens, answers
"Paris" after it. In local mode just gate-levers passes 40 tests. just gate-placement passes 6 and fails the 5
hw_placement_* tests, hw_placement_unified_pool_lowers_the_card_budget among them: each opens the DeepSeek V4.1
Q3_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-box selects 144 of 156 gate recipes and one
weekly recipe (weekly-gpu-glm5next-e2e-long), most of them because crates/model is in their closure. Its
fixture-tier dry run (bash tools/gate-batch.sh --list affected.txt --tier fixture --dry-run) puts 87 items in lanes
and defers 65 to the real tier (34 real-only, 29 real-file, 2 of a family with no fixture).

  • I ran all 87 on four DGX Sparks (GB10, driver 595.84), on 9a8a17fe, one just <recipe> per lane in local mode with
    BLOOMERY_TIER=fixture BLOOMERY_GATE_BOUND=1800, the fixtures fixture fetch makes and the published reference sets
    (gate-batch.sh adds BLOOMERY_GATE_CARD=3090 or a6000 to some items, which local mode refuses, so I left it out). I
    ran each lane that did not pass on f382b474 as well. 25 pass, among them gate-levers, gate-gpu-lib,
    gate-glm5next-meta (33 lib tests against 32 on the base, the new one; hw_glm5next_spec read the real GLM file) and
    gate-gpu-swap. The other 62 are red on both commits with the same exit code and the same first stop (a lib test count
    one higher here in gate-ops and the three -meta lanes that run lib tests is the new test). By cause: 22 stop on an
    oracle or reference set that is not published (ref/MANIFEST.tsv, ref_deepseek41_plain, ref_qwen35moe and the
    like); 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.sh exit 64); 6 on glm5next/corpus-prose.ids; gate-gpu-qwen38-serve on
    --place gate, which wants a 3090 by name; gate-ptx-spill (off a contributor's list, since its table is sm_86
    only), whose scan reports local memory above that table on the GB10; and gate-engram, whose
    hw_engram_expert_stack_evicts_and_reads_back finds 2 of 7 pages still in the page cache after an eviction, on both
    commits. Of the five closest to the changed host path, gate-ds41-load stops at the draft fixture, the two
    gate-gpu-serve-start-* at the two-card refusal, and gate-gpu-glm5next-nvtier and
    gate-gpu-glm5next-residency at corpus-prose.ids (the residency gate's first 10 clauses pass on both commits). So
    no 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.
  • Of the 65 deferred, I ran gate-placement at the real tier with the V4.1 file (above). The others are left to the
    maintainer's run, gate-gpu-glm5next-serve among them: it is real-only and runs a real server on the GLM plan this PR changes.
  • I did not narrow the list: no just narrow or ptx-scan pair 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_pool returns
    the first plan unchanged, so the workstation's plans should not change. Its gate binaries still link the changed code.
  • just stage-gpu-load-v41, which AGENTS.md runs by name for a change to the planner
    (crates/model/src/placement.rs and its workstation and host-lock files). This PR changes the planner in
    crates/placement, which crates/model/src/placement.rs re-exports, so I count it as selected. It is real-only and
    stages plan (b) on both of the workstation's cards (BLOOMERY_CARD=both), which local mode refuses, so it is left to
    the maintainer's run.

What I ran into

This does not block this PR.

At serving defaults, GLM's unset residency word is now off at --plan and at the start, on a plan whose churn
pool needs 0 B.
On the DGX Spark with no lever set, the start at ecad5fdc printed (lines cut):

plan place=a card=GB10 ctx_max=3072 card_experts=5867 ... card_budget=101466711940 paged=90972356608 ... room=125186990080 plan_room=124650119168 ...
residency unset residency=off why=unset: the churn pool needs 0 B at the highest P the card slots hold, the plan leaves 5629828 B and the host's room -301959292 B, and this load hosts its pool read=MemAvailable room=124879400960 plan_room=124342530048 mem_left=-301959292
cache ram=0 rule=default available=124879400960 read=MemAvailable need=124644489340 ...

It loaded and answered (the table above). A --plan run 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 patch
printed off at --plan and mid-p0-s1 at a start 19 s later. So --plan and the start now agree, and the start's
word went from mid-p0-s1 to off. The GLM start at a54fbc2a printed off in 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 f382b474 and 31abb2d5, 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 now HostNeed::room_left
    (crates/placement/src/placement/workstation.rs:665): plan_room of a fresh reading, that is the reading less
    HOST_DRIFT (512 MiB, :889 and :899), less the plan's host need. Before, it was the fresh MemAvailable less
    the 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_room less what the plan leaves (the record's "the plan leaves"). 124650119168 B less 5629828 B is
    124644489340 B, the cache line's need [derived]. So the climb's room is now what the plan leaves less the drop
    in MemAvailable between the plan's reading and the climb's: 5629828 B less 307589120 B (125186990080 B to
    124879400960 B) is -301959292 B, the printed mem_left to the byte [derived]. In the --plan run, 15099780 B less
    287272960 B is -272173180 B [derived]. Before 8d85abd4 the climb had HOST_DRIFT more, so only a drop past
    512 MiB plus what the plan leaves turned the word off. That is why the earlier --plan and start could differ.

  • The rule (room_for, crates/levers/src/lib.rs:1486) keeps a word at P only while mem_left is at least the pool's
    bytes. 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 turns
    off whenever mem_left is 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 as
    extra 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 for mid-p0-s1 and for off. mid-p0-s1 adds ChurnPool::check_beside
    (:751), which a 0 B pool passes while the plan leaves bytes past the NextN layer's experts. The start on the
    previous base loaded with mid-p0-s1 on the same plan shape.

  • The word does change the runtime. mid-p0-s1 builds the residency machine (FileSwap::new and
    ResidencyGlue::new, :772-788), which swaps experts into the card's slots; off builds none
    (ResidencyGlue::off()). The start on the previous base printed 38 residency pass records, ten of them moving
    24 experts each (made=24 bytes=364904448); the start at ecad5fdc printed none. On this plan the word halves
    GLM'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 experts
    with no RAM arena (arena=0 arena_slots=0 in the plan line of all four starts below), so those experts are read
    through 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), with BLOOMERY_RESIDENCY=mid-p0-s1 (M) or no lever (U,
    which is off), temperature 0, max_tokens 32, 18 requests of a 39-token prompt and 6 of a 2577-token prompt per
    start, medians of the last five of each:

    Start Word Decode, 39 tokens Prefill, 39 tokens Decode, 2577 tokens Prefill, 2577 tokens
    1 mid-p0-s1 1.95 tok/s 2.15 tok/s 1.40 tok/s 8.60 tok/s
    2 off 0.94 tok/s 1.62 tok/s 0.84 tok/s 8.18 tok/s
    3 off 0.96 tok/s 1.61 tok/s 0.84 tok/s 8.22 tok/s
    4 mid-p0-s1 1.89 tok/s 2.11 tok/s 1.53 tok/s 8.62 tok/s

    The long prompt is 2577 tokens, not 4096, because the default plan holds 3072 tokens a slot, and a larger
    --ctx-size would change the plan the word is picked on. The prompt bytes and prompt_n are the same in every
    start. 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-s1 ramps
    up over the first requests (1.25 and 1.29 tok/s over the first five short ones); off is flat. These are the
    numbers 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-s1 and 43.6 tok/s with off
    (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 off requests, but 4 and
    6 distinct outputs over the 6 requests of each mid-p0-s1 start (their first 130 characters agree with off's). I
    did not trace this.

  • I take 8d85abd4 as the cause because it is the only change to the climb between those two bases, and under the rule
    before it (the fresh MemAvailable less the need) both runs would have kept the word: 5629828 B plus 536870912 B
    less 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 kept mid-p0-s1, saw a drop of about the same size,
    303603712 B (its plan line's room to its cache line's available). A control build confirms it: ecad5fdc
    with all of 8d85abd4 reverted (git revert --no-commit 8d85abd4, no conflicts), started alternately with
    ecad5fdc itself at serving defaults (ecad5fdc, control, control, ecad5fdc, ecad5fdc, control), picked
    mid-p0-s1 in 3 of 3 starts and printed 45, 38 and 25 residency pass records; ecad5fdc picked off in 3 of 3
    with none. The plan lines match between the two builds; only the climb's room differs. On every start of
    ecad5fdc the printed mem_left is the cache line's available less its need, less HOST_DRIFT, to the byte,
    and that difference was 179 to 207 MB, so under the rule before 8d85abd4 the word would have stayed [derived]. The control reverts the
    whole commit, so its record has no mem_left; I did not build a narrower one.

To see it: on a DGX Spark at ecad5fdc with no BLOOMERY_* set, run bloomery-serve -m <GLM-5.3-Flash UD-Q4_K_XL, first shard> --plan, then a real start. Compare the plan line's room with the residency unset line's room,
mem_left and "the plan leaves". mem_left is what the plan leaves less the difference of the two rooms. A given
BLOOMERY_HOST_ROOM hides it: both rooms are then the given bytes, so mem_left equals 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. 8d85abd4
gives such a pool the HOST_DRIFT margin, as direction (a) there suggested. On a split plan whose pool is 0 B, the
same margin turns the word off without changing the load's check.

Directions I can see, for the maintainer to choose between or set aside:

  • (a) Leave a margin past HOST_DRIFT in the room the NVMe split fills, so a split plan leaves bytes for the drop
    between the plan's reading and the climb's. It costs that many bytes of host-served experts, which are then read
    from the drive.
  • (b) Let a pool of 0 B fit whatever mem_left is. 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. This
    reverses the shared room_for rule that crates/levers/src/tests.rs:917-928 pins ("A negative budget does not take
    even an empty pool, at any count"), so it changes Qwen3.8 too. That pin predates 8d85abd4: a negative mem_left
    then meant MemAvailable under the need, and now it means a reading under the need plus HOST_DRIFT.
  • (c) Have the climb read the room the plan was made at instead of a fresh reading. --plan and the start then agree
    for one reading, and on a split plan that fills its room mem_left is what the plan leaves. The HOST_DRIFT
    margin 8d85abd4 gave a pool then counts from the plan's reading, so a drop between that reading and the climb's
    comes out of it.

Hardware (if the change is for a machine)

Machine --version Card, driver CPU, RAM What ran
DGX Spark bloomery-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 in nvidia-smi; Ubuntu 24.04.5, kernel 6.17.0-1032-nvidia 20 Arm cores (Cortex-X925 and Cortex-A725), 128 GB unified (MemTotal 127599376 kB) the model tests above, local-mode gates and bloomery-serve at serving defaults (on ecad5fdc and 31abb2d5, and GLM and V4.1 on a54fbc2a), the Qwen --plan comparison and FAIL-first (earlier builds); built in a CUDA 13.3.1 container with cargo-oxide 87bc9518
A second DGX Spark bloomery-serve 0.3.2 (commit 7448162f) GB10, driver 595.84, CUDA 13.2 in nvidia-smi, same kernel as above just contrib-check --tier t1 in the container with no card; with two more of the same machines, the 87 fixture-tier lanes on 9a8a17fe and the red ones on f382b474; at 7448162f, the model lib tests, bloomery-serve-qwen38's tests, the three Qwen FAIL-first runs, the Qwen3.8 --plan runs at 64 GiB and GLM at serving defaults (not this head's starts), built in the same container with the same command
RTX 5090 bloomery-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 AMD Ryzen 9 9950X (16 cores, 32 threads), 64 GB (MemTotal 62378960 kB) the --plan comparisons, bloomery-serve at serving defaults, just gate-levers, just gate-placement and its bloomery-placement --lib half; built on the host with the same cargo oxide build command and cargo-oxide 87bc9518. At 4685e4ab, host only: the model lib tests, bloomery-serve-qwen38's tests (tools/gate.sh --oxide, --arch sm_86) and the three Qwen FAIL-first runs
A third DGX Spark bloomery-serve 0.3.2 (commit 5dbcf8f6), bloomery-serve 0.3.2 (commit 4685e4ab), bloomery-serve 0.3.2 (commit a54fbc2a) GB10, driver 595.84, CUDA 13.2 in nvidia-smi as above the model lib tests, the Qwen FAIL-first and the Qwen3.8 --plan runs at 64 GiB and 124 GB (on 5dbcf8f6, its one-call mutant and 45836b17); at this head, the model lib tests, bloomery-serve-qwen38's tests, the three Qwen FAIL-first runs and the Qwen3.8 --plan runs at 64 GiB (on 4685e4ab and a54fbc2a); same container and build command
Mac Apple silicon (an M4 Pro and an M5) just mac-static (on 9a8a17fe and this head), just mac-test (on 9a8a17fe and this head, with cargo test -p bloomery-placement), just affected (this head)

The nvidia-smi lines. DGX Spark, from the conditions log of the ecad5fdc runs (the header and the card's row):

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

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 51541b79 runs:

$ nvidia-smi --query-gpu=name,driver_version,memory.total,pcie.link.gen.max,pcie.link.width.max --format=csv,noheader
NVIDIA GeForce RTX 5090, 595.99.02, 32607 MiB, 5, 16

AI help

AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: MUST_FIX: the 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)

@midagedev

Copy link
Copy Markdown
Owner

Sorry, we made a mistake here. While tidying PRs we rebased your branch onto main and pushed an extra commit (d15ed697) fixing the build after #94's Machine API change, which we shouldn't have done without asking. Your original commit is preserved as 74a94611. How would you like us to handle it? We can remove our commit and restore your branch as it was, or you can keep it. Shingler's remaining point about the Qwen planner is still open. Sorry for the trouble.

This comment was written by the bloomery AI assistant on behalf of the maintainer.

@juliankang4

Copy link
Copy Markdown
Collaborator Author

No harm done, and thanks for flagging it. Please keep d15ed697; it's the same build fix the branch needed after #94. The Qwen planner point will come as a new commit on top of it, so the branch won't be rewritten under it.

juliankang4 and others added 4 commits October 14, 2026 14:44
…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.
@juliankang4
juliankang4 force-pushed the dgx-spark-unified-floor-on-glmres branch from d15ed69 to a54fbc2 Compare October 14, 2026 06:14
@juliankang4

Copy link
Copy Markdown
Collaborator Author

The Qwen planner point is fixed in d0c51f1f: target_rule_of now plans through plan_in_pool, so the Qwen seat retries under a card budget instead of passing HostRoomFloor up, and the bot's Qwen3.8 case (two slots of 4,096, 64 GiB readings) now plans. The body has the details and the GB10 and Mac results at the new head a54fbc2a.

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 tools/gate-paths.tsv, so I rebased onto main both times. The maintainer's commit is kept with the same patch and author; only its hash changed, d15ed697 to 57fe1647. git range-diff shows each of the four commits unchanged across the last rebase.

@github-actions github-actions Bot added dgx-spark The DGX Spark (aarch64, GB10) port documentation Improvements or additions to documentation gpu The card engine: crates/gpu*, crates/app needs-gpu Changes code a card must check, and carries no gate run on a card tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo labels Oct 14, 2026

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Verdict: MUST_FIX: The 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)

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

Copy link
Copy Markdown
Collaborator Author

Fixed in ad7aa155, as suggested: plan_in_pool returns any result other than the floor's refusal before it reads machine.unified(), so a machine with no node gets the planner's Nodes { nodes: 0 } back instead of a panic in this_node(). The retry test has that case and asserts Nodes { nodes: 0 }; with the old order it panics at this_node().

The fix is its own commit on top rather than folded in, because the node read came with the maintainer's 57fe1647 and that commit keeps its patch and author. On a machine with a node the function returns what it returned at a54fbc2a in every case, so the GB10 and Mac runs in the body still hold. At ad7aa155, cargo test -p bloomery-placement passes 166 of 166 and cargo fmt --check is clean.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since the last review: must-fix 1 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)

@juliankang4

Copy link
Copy Markdown
Collaborator Author

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

@juliankang4
juliankang4 merged commit 54ceee7 into midagedev:main Oct 14, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dgx-spark The DGX Spark (aarch64, GB10) port documentation Improvements or additions to documentation gpu The card engine: crates/gpu*, crates/app gpu-evidence Verified on the author's card: the pull request carries the gate run and its output host Host crates: qdot, threads, engram, engram-lab, runtime, sampler, levers, decision, vision, refset model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja placement Placement planning: crates/placement tools Tools, recipes, CI and the toolchain: tools/, justfile, .github/, Cargo

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants