Repository navigation
gpu, levers, justfile, tools, docs: the tier page's counters cross a link as generations; a loopback link carries them between two pages on one machine with a host pump - #172
Conversation
2f41589 to
b046bc1
Compare
There was a problem hiding this comment.
Verdict: MUST_FIX: Two proven duplicates can reuse existing owners.
Must fix
1. Remove the redundant load-time setter
crates/gpu/src/host/link.rs:267 duplicates crates/gpu/src/host/link.rs:281: both set a word on both pages (kind B, R14). The sole caller, crates/gpu/src/host/tier.rs:602, initializes the fault word before any card reaches the page. The existing tier-first release stores also satisfy initialization.
Deleting it now leaves one setter to keep the pages consistent.
Delete Link::init. Replace its sole call in TierPage::new with:
link.store(layout.word_off(t, TWord::Fault), FAULT_NONE);Replace the module documentation line that mentions init with:
//! word ([`Link::load`]), stores one ([`Link::store`]),
2. Use the production release constant in relay tests
crates/gpu/src/host/link/pump.rs:325 repeats the cancellation sentinel owned by crates/gpu/src/host/step.rs:28 (kind B, R14). Recovery tests now have a second definition of the production value.
Importing the owner now prevents the test value from drifting when that value changes.
Replace the local constant with:
use crate::host::step::RELEASE;Non-blocking
⚠ Current gate evidence: justfile:1633. The reported runs use earlier commits and modified gate inputs; they do not verify 84f77b01..b046bc1e.
Attach the covering PTX scans, structural comparisons and unmodified owning-gate output for the current base and head, including the model path and card line. Checking these commits before merge separates this change from the intervening base changes.
Checked
Duplicates: both setter bodies and both release constants confirmed; both fixes reuse existing owners and remove definitions.
Complexity: bloomery_levers::read adds scope match arms and a predicate, routine registry growth.
Silent failure: one discarded Result followed, the pump join during drop. A stopped pump is named by broken() on the lost-tier path; teardown publishes no success result.
Tests: static mutation audit covers pump.rs:73 (non-wrapping subtraction), :198/:210 (omit bytes/store an absolute counter), :266 (resume every line), :186 (accept a doubled done), and tier.rs:801 (split release). Each makes its corresponding new test fail. Registry-row removal and main-scope refusal fail the changed lever assertions.
Gates: unsafe-block ceiling tightened 1747 → 1746; no bands relaxed. The new release test has FAIL-first evidence.
Escape catalog: none of C1–C4 matched.
Notes
Static review. No tests or CUDA gates were executed here. The supplied runs use modified gates and predate the current base and head.
Card check: needs-gpu. This pull request changes crates/gpu/src/host/link.rs, crates/gpu/src/host/link/pump.rs, crates/gpu/src/host/mod.rs … and carries no gate run on a card (CONTRIBUTING's evidence list); a maintainer adds gpu-verified after one.
🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)
| const IMAGE: usize = 192; | ||
| const ROWS: usize = 256; | ||
| const WORDS: usize = 80; | ||
| const RELEASE: u32 = 1 << 30; |
There was a problem hiding this comment.
Reuse the existing cancellation sentinel so recovery tests share the production value (R14).
| const RELEASE: u32 = 1 << 30; | |
| use crate::host::step::RELEASE; |
…ts the tier page's words through its link TierPage and TierCard took each flag word as an AtomicU32 from Link::word and loaded or stored it themselves. The link now does it: Link::load (the progress word and the fault copy), Link::store (the fault clear, and the fault copies a new page starts clean with), and per counter Link::release, Link::at_rest and Link::reset, which on the shared page are Link::store and Link::load. Link::word and Link::host_at are gone. On the shared page each call is the load or store it replaces, with the same ordering, so every address, value, card operation and graph is the one it was. The page's initial fault copies are Link::store's release store of FAULT_NONE where a volatile write was, before any card reaches the page. tools/unsafe-ratchet.txt: gpu's block pin comes down from 1750 to 1749, the volatile write's unsafe block gone.
…link as generations; a loopback link carries them between two pages on one machine with a host pump A row's go counter is added to by the stage card and taken back by the tier, and its done counter the other way. Across two pages a copy of the writer's absolute value would erase the reader's take back, and a captured wait keeps one fixed target on every replay. So on the loopback each counter is split: on the writer's page only the writing card adds to it (a generation), and for each new generation the pump copies the bytes it publishes to the reader's page (a go: the row's image; a done: the row's routed rows and the tier's fault copy), writes the progress for a done, and then adds one to the counter on the reader's page, where the reading card's wait and take back are the shared page's. The step is checked before anything is published: 0 carries nothing, 1 carries one, and any other step publishes nothing and stops the pump by name, so the host sees the tier behind and the lost tier's detail names the step. The host's progress word on the stage card's page counts the dones carried, so it never runs ahead of their bytes. A release stops its own counter. A reset takes that counter's generations as carried, restarts its tier's progress count from the tier's page and resumes that counter alone, so a counter still released keeps what its writer adds until its own reset; a broken relay stays stopped across any reset. At rest means every generation carried, the reader's counter at 0 and no relay broken. Link::broken also names a pump thread that ended, and the not-at-rest error of a reset's check carries it, as lost_detail does. host/link/pump.rs holds the relay (Relay, Relays) over a Page trait, with four host tests: the step rule across the u32 wrap; two model cards over the pump thread for 2000 goes and dones from a generation near the wrap, then a release, three more layers the cards run with nothing carried, the go's and the done's resets taken apart with the pump running between them, and 2000 more (every go carries its image, every done its rows, the progress counts every done, both counters end at rest); a reset of one counter that leaves another, released, stopped until its own; and a doubled done with raised rows that publishes nothing. Link::new picks the link's kind; the loopback allocates the tier cards' page in the first tier's context and starts the pump thread through threads::helper::spawn_helper, floating, so it never shares a pinned load thread's cpu; TierLayout::crossings lists the counters and their spans. Link::store writes the tier cards' page first. The tier protocol's code is the same over both links. BLOOMERY_TIER_LINK (shared | loopback, unset shared, a mode) picks the link where HostTier::attach_tiers builds the page, through bloomery_levers::tier_link, read below every binary's main. On the shared page nothing changes. weekly-gpu-ds41-lost runs gate_deepseek41_tier's T1, T2, B7, B2 and its lost-card clauses over the loopback after its shared-page line; that recipe's trigger row in tools/gate-paths.tsv lists host/link/pump.rs, its tools/box-fill.tsv row says it runs the loopback, and tools/defaults-twins.tsv names the recipe's shared-page line as the loopback pin's twin. tools/spawn-allow.txt holds the relay tests' two threads. docs/contrib/dgx-spark.md: section 1's P5 row and file list, section 9's opening, 9.5 (the impls and item 2), 9.6 (item 3 and what carries to N = 4) and answer 20 describe the relay on the plan the maintainer agreed to in midagedev#127, and 9.6 item 3 records that the maintainer takes it on midagedev#96's structural proof in place of a same-lease A/B. The lines it cites in host/tier.rs and host/link.rs are updated.
The second commit released a tier one counter at a time, the go and then the done, with the pump's lock dropped in between. The released tier card runs the cancelled graph's remaining layers and adds to the done while it still carries, so a pass between the two releases can see the done step by more than one and stop the pump for good; the first round after the reset then never arrives. TierPage::release now passes the tier's every go and done to Link::release in one call, and on the loopback that call holds the pump's lock across all of them. Relays::release finds every line before it stops any. hw_a_release_takes_a_tiers_counters_in_one_hold_of_the_pump builds the case on a loopback page through TierPage::release: a test-only step after each release (Link::run_on) adds three to the done on the tier cards' page and makes one pass. The recovery test's failure names the pump's reason.
TierPage::counters lists a tier's every counter, each row's go and then its done. The release, TierCard::at_rest and TierCard::reset read it in place of three copies of the same loop; the order and at_rest's stop at the first counter not at rest are the loops' own. dgx-spark.md's spin_loop line in tier.rs moves with it.
b046bc1 to
01bf230
Compare
|
Both must-fix items are fixed, folded into the commits that brought the code in. The branch is now rebased onto main 1. The load-time setter. The same check found three more copies of the same kind, in the shared page's arm of 2. The release constant in the relay tests. Other candidates I checked and left alone:
Rebase. The only conflict was the Gate evidence for
So for the tier clauses I can only offer the stand-in run again, now on these two commits. It is not the owning gate's run. The patch to
All three landing runs print The body's "What I ran" now carries these runs in place of the earlier ones, with the commands, the card line and the model paths. |
There was a problem hiding this comment.
Verdict: CLEAN: Neither finding meets the must-fix bar.
Non-blocking
⚠ Run the shipped owning gate: justfile:1633. The PR reports that both owning recipes stop before their clauses; the successful tier runs change the gate's draft reserve and depth. Supply output from just gate-gpu-ds41-tier and just weekly-gpu-ds41-lost on unmodified trees with the required fixtures and cards. A maintainer can supply the runs the contributor could not perform, as CONTRIBUTING.md allows.
⚠ Check the copied spans beyond their first word: crates/gpu/src/host/link/pump.rs:383. The recovery test writes and checks only the first word of each 64-byte payload. Omitting each span's final word from the production loop at line 199 would go undetected by the four relay tests. Fill and check the complete spans so truncated images and rows fail the host test.
Checked
Duplicates: release, rest and reset share the counter list in TierPage::counters.
Complexity: bloomery_levers::read adds another scope and matching arms, following the existing parser structure.
Silent failure: teardown discards the pump's join result. During service, Link::broken reports an ended pump; invalid generations publish nothing and the lost-tier detail includes the link's reason.
Tests: wrap, reset, invalid-step and single-call release assertions inspected statically.
Gates: the unsafe-block cap tightens from 1750 to 1749; the weekly recipe adds loopback clauses.
Escape catalog: no C1–C4 pattern matches the changed recipe.
Notes
Validation was static. Tests and CUDA gates were not rerun; execution evidence is reported in the PR body.
Card check: gpu-evidence. This pull request changes crates/gpu/src/host/link.rs, crates/gpu/src/host/link/pump.rs, crates/gpu/src/host/mod.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
This is 9.5 item 2 of
docs/contrib/dgx-spark.mdand the fault copy of its item 7, the words row of the P5 plan in #127. The maintainer agreed to this design in #127. The branch is four commits on main (a13125a8, with #96'sLinkseam merged), so the diff to read isa13125a8..01bf2300. Part of #13.A row's go counter is added to by the stage card and taken back by the tier card, and its done counter the other way. On two pages, a copy of the writer's value would erase the reader's take back. A captured stream wait keeps its captured value on every replay (the
HostFlagsdoc,crates/gpu/src/graph.rs:754), so 9.5 item 2 as first written, a greater-or-equal wait on a target that rises each replay, cannot be captured. This PR takes the design in 6139195763: only the counters that cross the link become generations. The card operations, the graphs and the host's signal code are one path over every link, and only theLinkimpl behind them differs.Linkcalls: the progress and fault reads, the release, the rest check, the reset, the fault clear and the fault word's first value. The accessors that handed out a word or an address go:Link::host_at,TierPage::word,TierPage::word_at, and thepub(super)Link::word, whose name stays on a private helper behind the link's own calls. On the shared page each call is the load or store the host did before, with the same ordering, and the release, rest check and reset areLink::storeandLink::loadthere. The one change is the fault word's first value at load: awrite_volatilebefore,Link::store's release store now. Both are one aligned 32-bit store, made before any card runs.tools/unsafe-ratchet.txtcomes down from 1750 to 1749 for the unsafe block that goes.BLOOMERY_TIER_LINK=loopback(unset:shared). It has two pages on one machine, the stage card's and the tier cards', each allocated in its own context, and a host pump thread that carries each crossing counter from the writer's page to the reader's.TierPage::countersis the one list of a tier's counters, which the release, the rest check and the reset read.For each crossing counter (a row's go and a row's done, per tier):
+1only (the shared page's operation)+1per new generation; the reading card,-1after its wait (the shared page's operation)RELEASEto each reader's counter. The other tiers' counters keep carrying. At rest means the reader's counter at 0, the writer's generation carried, and no bad step seen. A reset, taken once both cards' streams have drained, takes the writer's generation as carried, restarts the progress count from the tier's page, zeroes the reader's counter and resumes that counter alone. A counter still released keeps what its writer adds until its own reset. A pump stopped by a bad step stays stopped across every reset, as a lost card stays lost.Link::store(the fault clear, and the fault word's first value at load) writes the tier cards' page first, since the pump carries from there to the stage card's.Link::brokenalso names a pump thread that has ended. The pump thread starts throughthreads::helper::spawn_helperwithPlacement::Float, so a load thread pinned to one cpu does not share its core.On the shared page nothing changes: the same addresses, values, card operations and graph structure (checked below). The loopback is for checking the protocol on one machine, and its pump spins on one core while the model is loaded, which the lever's doc says. The relay works over any page type (
Pageinhost/link/pump.rs), and its step rule and its order (the bytes, then the progress, then the generation) carry to the transport of #127. The rest is the transport's: a pump on each node, the inbox word between them, the release of a remote node's words, the per-tier overlap of spans, and item 7's check for a stale or doubled image.The order of a release (third commit)
The second commit released a tier one counter at a time: the go, then the done, with the pump's lock dropped in between. Once the go is released, the tier card runs the cancelled graph's remaining layers, and each layer adds one to the done, which still carries. A pass of the pump between the two releases then sees the done more than one generation ahead, the step rule stops the pump for good, and the first round after the reset never arrives.
The second commit's recovery test shows it. Run alone 20 times on the second commit's code with only the third commit's failure message added, it was red in 4 runs, each on the round after the reset, and each names the done at byte 64 (three of them "stepped by 2", one "stepped by 3"):
The third commit releases all of a tier's counters in one call.
TierPage::releasepasses every row's go and done toLink::releaseat once, and on the loopback that call holds the pump's lock across all of them, so no pass falls between two of them. The lock closes the case. Under one hold, stopping and raising each counter in turn would do the same.Relays::releasealso finds every counter's line before it stops any, so a counter that does not cross panics with nothing stopped. The recovery test's failure now prints the pump's reason.A new test,
hw_a_release_takes_a_tiers_counters_in_one_hold_of_the_pumpinhost/tier.rs, makes the case without timing. It opens a loopback page on twoGpus of one card and callsTierPage::releasefor its one tier. A test-only step (Link::run_on, undercfg(test)) plays the released tier card: after each release it adds three to the done on the tier cards' page and makes one pass. Released in one call, the done is already stopped and nothing is carried. WithTierPage::releasesplit back into one call per counter, the pass between the go and the done stops the pump, and the test fails every time:The test enters at
TierPage::release, whichTierCard::releasecalls with its index; aTierCarditself needs resident weights, the tier kernels and an architecture's experts, which a unit test does not have. The four relay tests stay green under that split, since they callRelays::releasedirectly.The engine's
TierCard::releasehad the same split, so a cancelled graph over the loopback could have broken the link the same way [inferred]. The gate's clauses do not make that case: in T2 and B2 nothing moves between the release and the reset, and in the lost-card clauses the card has already stopped.Where each part lives
crates/gpu/src/host/link/pump.rs(new):Relay(take, :72), the step rule for one counter;Relays, every crossing counter's line with its carry (:182), and the release (:229), rest check (:244) and reset (:260). Four host tests (below).crates/gpu/src/host/link.rs:Link::new(:124) picks the kind.Link::loopback(:152) refuses a crossing that is not 4-aligned inside the page by name, allocates the tier side in the tier's context and starts the pump thread.load,store,release,at_restandreset(:266 to :314) are the host's word calls. On the shared page each word access is one load or store, and a release one store a counter; on the loopback each acts on the side of the counter it names, and a release on all of its counters under one hold of the pump's lock.broken(:322) is the pump's reason, or an ended pump thread.run_on(:340, test only) is the released card of the release test.crates/gpu/src/host/tier.rs:TierLayout::crossings(:507) lists each counter, its reader and the bytes it publishes.TierPage::new(:589) passes the kind toLink::new.TierPage::counters(:789) lists a tier's counters, andTierPage::release(:799) passes them to the link in one call.TierPage::broken_note(:806) is the link's reason for a message.fault_copy,progress,lost_detail,release,at_rest,resetandclear_fault(:1216 to :1519) call the link or the page. The release test is at :1912.crates/gpu/src/host/mod.rs:1747:HostTier::attach_tiersreads the lever;settle_checks(:2332) adds the link's reason to its not-at-rest message.crates/levers: theBLOOMERY_TIER_LINKrow (registry.rs:345, class M,shared | loopback),TierLinkandtier_link(lib.rs:2553,:2563), read below every binary'smainasnvtier_leversis.justfile,weekly-gpu-ds41-lost: the first line also runs the tier gate's T1, T2, B7 and B2 and the lost-card clauses overBLOOMERY_TIER_LINK=loopback.tools/defaults-twins.tsvhas the row for that pin, andtools/box-fill.tsvnames it.tools/gate-paths.tsv:205: theweekly-gpu-ds41-losttrigger row listshost/link.rsandhost/link/pump.rs.tools/spawn-allow.txt: the relay tests' two threads inpump.rs(the pump thread itself goes throughspawn_helper).Code shape
The review reads these first. A box left unticked needs one line of why.
Link, and a tier's list of counters one owner,TierPage::counters. The relay rule lives inhost/link/pump.rsalone, over a page trait, so a mirrored impl over RDMA uses it rather than copying it.TierCard.Link::host_at,TierPage::word,TierPage::word_at, thepub(super)Link::word(a private helper of that name remains), and the atomic imports intier.rs. No load-time setter besideLink::store.BLOOMERY_TIER_LINKother thansharedorloopbackis refused by the lever parser.host/link/pump.rs(above).cargo fmtandcargo clippyare clean on what I touched; the lint count did not go up.Duplication found or removed (
path:line), or "none": the loop over a tier's go and done words, written out inTierCard::release,at_restandreset, is nowTierPage::counters(host/tier.rs:789). The page's first fault words go throughLink::store(host/tier.rs:602), and on the shared pageLink::release,at_restandresetcallLink::storeandLink::load(host/link.rs:287,:303,:314). The relay tests takeRELEASEfromhost/step.rs:28(host/link/pump.rs:297).What I ran
On the shared page this is the move class in
AGENTS.md's table. The maintainer's answer in #53 lets the structural proof replace a same-lease A/B on the box if three conditions hold. Each one, with its evidence. The runs are on the current basea13125a8and the current head01bf2300, fromgit archivetrees that hold every file of their commit unchanged.just ptx-scanis identical for the bins it covers. Holds. For the five bins scanned, the md5 block of every kernel entry and the entry tables equal the base's. The table is in "PTX" below.just gate-gpu-e2e, as shipped, printsgraph graph_nodes=648 want=648 eager_vs_graph_identical=true okon base and head, with the same node kinds and the same comparisons against the ik references. In the V4.1 tier gate (with the stand-ins below) the stage graph has 288 nodes and the tier graphs 64 step and 127 pair nodes on the base, on the head and on the head over the loopback, and T1 checks its graph run and its eager run each bit for bit against the reference on all three.Linkimpl adds no host work per step: no new atomic, lock or dynamic call. Holds, read from the code. On the shared pageLink::mirrorisNone: no pump thread, noRelays, no lock. The calls a step makes read the same words with the same orderings as on main. The progress word (TierCard::progress, read bybehind,wait_forandwait_caught_up) and the fault copy (fault_copy) go throughLink::load, which is the sameMappedHost::atomic_u32(off)and oneAcquireload thatTierPage::worddid on main, with no branch on the link's kind.pump::Page::word(&*self.page, off)names a trait method on the concreteMappedHost, so it is a static call. The settle path's rest check and fault clear (at_restandclear_fault, once a stream has drained) and the reset and release add a check or two ofmirrorbefore the same loads and stores; the pump's mutex is only in the loopback's arm, which the shared page never takes. A release builds oneVecof the tier's offsets, on the lost-card path only.Link::run_onand its call inreleasearecfg(test). So the change does not touch the dispatch path, and by the rule in dgx-spark: what is in progress on the Spark side, as a checklist #53 it needs no same-lease A/B.For the loopback, the owning gate's clauses pass over it (with the stand-ins), and the relay's host tests fail first under seven mutants.
On a Mac
At
01bf2300, on the basea13125a8.mac-static's combos step reuses a shape that is green in its ledger for the same inputs, so 28 of the 42 shapes the scope selects ran:The one failure is
bloomery-refset'sarch::draft::tests::a_plain_target_with_no_draft_beside_is_the_family_path, listed in #137: the test's temporary path is listed both as/var/folders/...and as its canonical/private/var/folders/.... It failed the same way on main4a50226con the same Mac (1199 passed, 1 failed, 179 ignored, the same test). This PR does not touchcrates/refset.On a DGX Spark
One DGX Spark (GB10, driver 595.84), in the CUDA 13.3.1 dev container (
rustc 1.100.0-nightly (e457a7b0d 2026-08-27), LLVM 21.1.8,just 1.58.0), throughtools/box.shlocal mode. Base and head were built with the same toolchain in the same container. The recipes ran as the justfile has them;BLOOMERY_ARCH=sm_86is the archptx-scantakes.a13125a801bf2300just gate-gpu-libjust gate-gpu-e2eBLOOMERY_ARCH=sm_86 just ptx-scan <bin> --features <f> --no-jit, 5 binsjust gate-gpu-ds41-tier,BLOOMERY_TIER=fixturejust weekly-gpu-ds41-lost,BLOOMERY_TIER=fixtureThe card and driver, read on the same machine (inside the container
nvidia-smireads the same name and driver):1. Host tests.
just gate-gpu-libon the head runs five tests more than on the base: the four inhost/link/pump.rsandhw_a_release_takes_a_tiers_counters_in_one_hold_of_the_pump(it ishw_, so it runs under the recipe's--include-ignored). They are:a_relay_delivers_each_generation_once_across_the_wrap: the step rule acrossu32::MAX.the_pump_carries_every_go_and_done_once_with_its_bytes_across_a_recovery: two model cards on two pages with the pump thread running, from a generation near the wrap. 2000 goes and dones, then a release of both counters, 3 more layers on both cards with nothing carried, the go's reset, a window with the pump running, the done's reset, and 2000 more. Every go carries its image, every done its rows, the progress counts every done, and both counters end at rest.a_reset_reopens_its_own_counter_alone: two counters released, the done's writer adds two, the go's reset leaves the done stopped, and the done's own reset takes the two as carried; the next generation is carried once and counted once.a_doubled_done_publishes_nothing: a done that steps by two publishes no rows (a raised fault copy among them), no progress and no counter, the pump stops by name, and the stop holds across a reset.2. Structure:
gate-gpu-e2e. WithBLOOMERY_REF_MODELnamingDeepSeek-V2-Lite-Chat.Q3_K_M.gguf(mradermacher'sDeepSeek-V2-Lite-Chat-GGUF) (sha256642abebcf2f1efdb7a204759425865a1552b3120a9f6d3d82eb1adb8271d43fd) and the ik CUDA reference files, base and head print the same result lines:V2-Lite has no expert tier, so this gate does not reach the tier page; it shows the shared engine's graph and eager paths unchanged.
3. PTX. The three bins of
tools/recipes.py scan-setplusgenerate_ds41andgate_e2e. The scanner's argument check refusesBLOOMERY_ARCH=sm_121forptx, so these are the sm_86 PTX. The hash in the table is the md5 of the scan'sptx-scan-md5block (one line per entry: name, md5, lines).a13125a801bf2300bloomery-serve-ds41deepseek41,vision6aefac7caecd01b16aefac7caecd01b1gate_kquantgpu123b985337344215123b985337344215gate_swapgpu088824c73145e1d3088824c73145e1d3generate_ds41gpu,deepseek41d7d96c3761e95e64d7d96c3761e95e64gate_e2egpue43e0ce1f9514ecce43e0ce1f9514eccThe entry tables (registers, spills, shared memory and the rest of the ptxas columns) are equal row for row. The extracted modules are not byte-identical: the
.oxartsection is 232 bytes larger on the head in each bin. With the numeric suffix of the module-level symbols (__device_global_<hash>_<n>,__shared_mem_<hash>_<n>) stripped, each module holds the same lines; only their order and the suffixes differ. The kernel bodies are equal by the md5 block.4. The owning gate as shipped does not run here.
tools/gate-paths.tsv:205makesweekly-gpu-ds41-lostthe owner of these files, andgate-gpu-ds41-tierbuilds the same bin. Both stop at the recipe'sdspark_modelbefore any clause, on base and head alike:The v41 fixture here (from
fixture fetch, at/models/fixtures/v41/) has no draft, andfixture generateneeds the real DSpark draft, which I do not have. The real tier needs the 323 GiB V4.1 file and the draft. The second line ofweekly-gpu-ds41-lostneeds two cards (BLOOMERY_CARD=both, which box.sh's local mode refuses with 64).5. The tier gate's clauses, with stand-ins. So the tier clauses ran on a copy of each tree with one file changed,
crates/gpu-gates/src/bin/gate_deepseek41_tier.rs: the plan takes a draft reserve of 0 B, and the tier depth is capped at the real file's 23 (a zero reserve on GB10 would give the whole stack).BLOOMERY_DSPARK_MODELnames a path that does not exist, sodspark_modelpasses it through and no draft is read. With the stand-ins it is not the owning gate's run, so I do not countgate-gpu-ds41-tierorweekly-gpu-ds41-lostas green. Each run is the recipe's own box command (cargo oxide build ... --bin gate_deepseek41_tier && bash tools/gpu-gate.sh gate_deepseek41_tier <clauses>) withBLOOMERY_TIER=fixture,BLOOMERY_GATE_CARD=any, and for the loopbackBLOOMERY_TIER_LINK=loopbackinBLOOMERY_BOX_ENV.a13125a801bf230001bf2300--union --fault --two --batch --batch2 --bfault--bfirst --blost --lost(the lost-card line ofweekly-gpu-ds41-lost)All three landing runs print
ok T1 structure: stage graph of Some(288) nodes, the reference'sandgraph nodes step Some(64) pair Some(127). T1 runs the prompt, 32 steps and the pairs as graphs from a reset, then the prompt and 4 steps eagerly from a reset, and checks every position bit for bit against the reference:76 positions, tokens and logits bit for bit the reference'son all three. Of the 192 settles, 190 are early on the base, 189 on the head and 176 over the loopback.Over the loopback at
01bf2300, abridged:6. FAIL-first. The four relay tests were first run against the reviewed relay of an earlier version of this branch (its
pump.rswith the second commit's tests): red, 3 of 4 failed (a_doubled_done_publishes_nothing,a_reset_reopens_its_own_counter_aloneand the recovery test). The third commit's test was run against its own mutant,splitin the table:TierPage::releasehands each counter toLink::releasein a call of its own, so the pump's lock is dropped between a tier's go and its done, as in the second commit. It is red, and only that test fails, by the pump's own reason:the tier page's counter at byte 64 stepped by 3 between two relays. The recovery test supports it, but only by chance. That test releases throughRelays::releasedirectly, so it does not crossTierPage::release. In its second commit shape it releases the go and the done in two calls. Run alone 20 times, it is red in 4 at the second commit's shape and green in 20 with the third commit, where it releases both in one call. These counts depend on timing.The mutants below patch a copy of the head's tree:
splitpatcheshost/tier.rs, the othershost/link/pump.rs. The relay mutants' host tests ran at01bf2300withrustc --teston a Mac,pump.rswrapped in a crate of its own beside ahost::stepthat holdsRELEASE; the head passes all four there.splitand the engine column ran on GB10 at earlier versions of this branch whosehost/tier.rsrelease andpump.rsrelay are the head's; the engine column isgate_deepseek41_tier --union --fault --batch --bfaultover the loopback, with the stand-ins.splithw_a_release_takes_a_tiers_counters_in_one_hold_of_the_pump("stepped by 3 between two relays")absa_reset_reopens_its_own_counter_aloneand the recovery testnodataanystepa_relay_delivers_each_generation_once_across_the_wrapanda_doubled_done_publishes_nothingnoresynca_reset_reopens_its_own_counter_aloneand the recovery test, on its 20 s deadlineallresumea_reset_reopens_its_own_counter_aloneand the recovery test, on its 20 s deadlinepostfirsta_doubled_done_publishes_nothingnoresyncandallresumematter only when a writer adds to a counter between a release and the reset that follows it. In T2 and B2 the tier's fault is read after the tier's last signal, so nothing moves in between [inferred], and a lost card's reset is refused. The recovery test builds that case: after the release both cards run three more layers, and the two resets are taken apart while the pump runs.postfirstneeds a bad step, which one machine does not produce. The engine column is run only where the gate's clauses can tell.On the loopback a release writes the reader's counter on the same machine, so the lost-card clauses over it check the release of waits on one node. Releasing the waits of a remote node's card is the transport's.
Not run here
gate-gpu-ds41-tierandweekly-gpu-ds41-lostas shipped, past theirdspark_model(4 above), and the two-card gates. They run the same host code as before on the shared page.Docs
docs/contrib/dgx-spark.mddescribed the shared page alone and cited lines that this change moves. The PR updates each of those places to what the tree holds now. Line numbers are at01bf2300:cuMemHostAlloccall, cited ashost/tier.rs:561and nowhost/link.rs:143(l. 352 and l. 635).host/tier.rslines it cites::20-24(l. 605),:37-42(l. 672),:49-54(l. 674 and l. 969),:42-44(l. 676) and the spin wait:1476(l. 1293).Hardware (if the change is for a machine)
tools/box.shlocal mode. The gate kernels were built forsm_86and JIT-compiled on the card. Inputs: the v41 fixture fromfixture fetch, the publishedmidagedev/bloomery-refsets,DeepSeek-V2-Lite-Chat.Q3_K_M.ggufand the ik CUDA reference files forgate-gpu-e2e.rustc 1.100.0-nightly (e457a7b0d 2026-08-27).AI help
AI 코딩 에이전트가 코드를 작성한 후 설명을 썼고, 테스트를 마쳤습니다. diff 리뷰와 PR 초안 리뷰는 제가 직접 했습니다.