Skip to content

gpu-gates: refuse stale recorded fixture budgets before planning - #606

Merged
juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:fix-stale-fixture-budget
Oct 21, 2026
Merged

juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:fix-stale-fixture-budget

Conversation

@juliankang4

@juliankang4 juliankang4 commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

GPU 게이트가 낡은 fixture의 기록된 budget을 받으면, 한참 뒤에 엉뚱한 expert 수 오류로 실패하던 것을 시작 단계에서 "budget이 낡았으니 fixture를 다시 만들라"는 이름 있는 오류로 거절하게 바꾸는 작업입니다.

Refs #594. 938167b fixed the stale qwen38 budget itself. This PR makes the next stale budget name its cause when a gate starts, instead of failing a later clause.

What and why

A fixture file records the card budget its planner used when the file was written. When the planner later changes how many bytes a tensor takes on the card, the recorded budget no longer fits, and a gate that plans under it gets a different expert count than the file was built for. gate-gpu-qwen4exp-mtp hit this on a qwen38 fixture. Its recorded budget planned 257 experts on two layers where the fixture expects 256, and the gate reported that as a failure in clause (d), far from the cause.

gate-fixture-budget and fixture verify already catch a stale budget through fixture::check_budget, but a gate that plans under the header never called it. This PR makes tier::header_budget, the function every gate reads the header budget through, call that checker for a fixture file. When the plan under the recorded budget gives a different expert count, the gate stops before planning with a BudgetMismatch error. The message has the first shard path, the recorded budget, the planned and expected expert counts and the regeneration section of docs/contrib/local-check.md. Only that expert count mismatch is called stale. A zero budget, missing metadata or a planner refusal gets its own message with the path and no regeneration advice. A variant fixture (one that records a variant key) keeps the budget of its base and is only checked to be a positive number, so it is not planned again. A file without a fixture version keeps its old behavior.

The family spec table moves from the fixture CLI into FIXTURE_SPECS in crates/model/src/arch/registry.rs, and registry::fixture_spec is the one lookup both the CLI and the gates use. I also changed the draft reservation sum in budget_levers to checked_add, because an oversized budget overflowed it instead of failing with a name. docs/contrib/local-check.md now says that a budget can drift to too many experts as well as too few, and that the variants of a replaced base fixture are made again with just fixture-requant FAMILY TAG. docs/contrib/new-model.md, the FixtureRow documentation, tools/refset-publish/sources.tsv and tools/refset-publish/README.tmpl point at the new table, and the tools/check-arch.sh entry for the old table is gone.

The stale budget itself comes from the Qwen3.8 BF16 dense-weight accounting, which changed how many bytes the indexer projections take on the card. That planner change is correct, and 938167b moved the qwen38 fixture's recorded budget to match it. This PR adds no regeneration and moves no budget value. A fixture file whose recorded budget no longer matches the planner is refused by name at start.

Release notes: Fixed: GPU gates refuse a fixture whose recorded card budget is stale at start-up, naming the file and the regeneration steps, instead of failing later on an expert count.

Code shape

  • No second copy. The gates reuse fixture::check_budget through header_budget, the same checker that fixture verify and gate-fixture-budget use. No per-model check was added.
  • Model code holds only architecture facts. The spec table sits in the model crate's architecture registry. tier.rs gained no per-model branch.
  • Dead code is gone. The CLI's old table and the imports it needed are removed.
  • No silent failure. A present but invalid budget, a zero budget and a planner refusal are named errors. The header + extra sum can no longer overflow silently.
  • Comments state what is true now.
  • No relaxed gate. No band, tolerance or lint moved. No gate or refusal test was removed.
  • cargo fmt --all -- --check passes on this head.

Duplication found or removed (path:line): the table of the seven family specs lived in the fixture CLI, where the gates could not read it. It now lives once in crates/model/src/arch/registry.rs, and the CLI reads it from there.

Where gates read the header budget

header_budget is the single reader, so a check there covers every caller. The direct callers are plan_levers (shared by the DeepSeek, Qwen and GLM gates), the Qwen4Exp MTP init, the GLM shared init_file and the DeepSeek two-card run. The shell helper fixture_budget in tools/ref/ref-paths.sh only carries the value, so I left it alone.

What I ran

The head of this PR is one commit on upstream 938167b2, and the before runs used upstream 74240f50. The Qwen3.8 and V4.1 budget tests below ran on this head. The CPU tests ran on the same commit on upstream 2d4c8131, with the commit unchanged (git range-diff reports =); the upstream commits in between change the Qwen3.8 card pins, two Qwen4Exp metadata tests and tools/model-tax.tsv, none of which these tests cover. The GPU gate runs and the 11 mutants ran on an earlier revision of this branch with the same gate calls and checker. Since then the change gained a separate error formatter, fixture_budget_error, with the regeneration test that covers it, a doc comment, one test message, the registry and documentation edits, and the tools/gate-paths.tsv row for the Qwen3.6 fixture spec, which now derives its gates.

Before the fix, the new admission test was written on the unfixed code and the overflow test with it. Both fail there. The admission test fails because the old reader never reaches the checker, and the overflow test fails with attempt to add with overflow.

cargo test -p bloomery-gpu-gates --lib tier::tests::
cargo test -p bloomery-model --lib fixture::budget::tests::
cargo test -p bloomery-model-hf --bin fixture tests::every_spec_has_one_source_row_and_v41s_draft_is_not_fetchable -- --exact

On that commit, the 24 tier tests, the budget test in the model crate and the spec test pass, cargo fmt --all -- --check passes, and the registry tests pass.

Eleven mutants replace each of the four callers' header_budget call with None, or break a lookup, the variant rule, the metadata case, the count comparison, the typed mismatch or the registry lookup. All 11 fail a test. A twelfth mutant removes the regeneration hint branch, and the new a_budget_mismatch_names_the_file_and_regeneration_step test fails. These tests check that the calls are wired in and that the messages are right. They do not replace a full GPU run of the other models.

GPU evidence

The GPU gate runs below ran on the earlier revision named in What I ran, on an RTX 5090 host with a qwen38 fixture. A diagnostic copy of the fixture, with its recorded budget set to the stale value from the report in #594, reproduced the failure on base. The same copy is refused at start-up on the head, with the same budget and the same counts.

| NVIDIA-SMI 595.99.02              Driver Version: 595.99.02      CUDA Version: 13.2     |
|   0  NVIDIA GeForce RTX 5090        Off |   00000000:01:00.0 Off |                  N/A |

Stale fixture on base, just gate-fixture-budget: the check reports qwen38 with budget 9490661376 planning [257, 257, 256, 256] experts a layer, not [256, 256, 256, 256].

Stale fixture on the head, just gate-gpu-qwen4exp-mtp with BLOOMERY_TIER=fixture: the gate stops at once with exit 1 and the message stale recorded fixture budget 9490661376, then regenerate the fixture using docs/contrib/local-check.md.

I regenerated qwen38 from its source headers with fixture fetch qwen38 and checked it with just gate-fixture-budget:

cargo run --release -p bloomery-model-hf --bin fixture -- fetch qwen38 "$HOME/refresh/qwen38"
BLOOMERY_FIXTURE_ROOT="$HOME/refresh" BLOOMERY_BOX_LOCAL=1 just gate-fixture-budget

On the regenerated fixture the budget is 9482272768 and the plan is [256, 256, 256, 256].

With the regenerated fixture and the matching MTP reference set (<refset dir> is the directory that holds it):

BLOOMERY_DATA=<refset dir> BLOOMERY_ARCH=sm_120 BLOOMERY_TIER=fixture BLOOMERY_BOX_LOCAL=1 just gate-gpu-qwen4exp-mtp

The gate ends with clauses: 25 ran, 4 left to the real tier and exit 0.

Clause (d) is among the 25: draft resident 2779869184 (want 2779869184 matches.

The q6kdown variant fixture goes through the changed reader too. BLOOMERY_FIXTURE_VARIANT=q6kdown just gate-gpu-glm5next-nvtier ends with clauses: 6 ran, 0 left to the real tier and every clause passed.

Two budget tests pass on this head on the RTX 5090 host, each 1 passed; 0 failed: hw_fixture_q38_budget and hw_fixture_v41_budget. Each reads the header of its source model, builds a fixture header from it and runs the family planner over that header. They do not read an installed fixture and they are not CUDA kernel gates.

  • I did not run the other models' GPU gates or the real tier. The change reads and refuses a header value. It touches no kernel, launch grid or arithmetic, so I took no speed measurement and claim none.
  • The regeneration advice on a stale file is confirmed by the GPU run on the diagnostic copy above and by the CPU test. The hw_ budget tests of the GLM-5.3, Qwen3.6 and Solar families were not run.

What I ran into

This does not block the PR. A fixture written before a planner change to its card bytes fails the fixture-tier gates until it is written again; with this change the gate names the file and the regeneration step at start instead of failing a later clause.

Hardware

One Linux CPU host for the unit tests and mutants. One RTX 5090 host (driver 595.99.02, CUDA 13.2) for the fixture runs and the GPU gates.

AI help

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

@github-actions github-actions Bot added documentation Improvements or additions to documentation gpu The card engine: crates/gpu*, crates/app model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja 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 21, 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: CLEAN: No merge-blocking behavior or quality defect survived review.

Non-blocking

⚠ Exercise regeneration advice through admission: crates/gpu-gates/src/tier.rs:1711 calls the formatter directly. Replacing its production call at tier.rs:973 with generic path-and-error formatting removes the regeneration advice without failing these assertions. Add an otherwise valid stale fixture case through header_budget and assert the returned advice. This catches a message-routing regression before gate users encounter it.

Checked

Duplicates: 8 new or moved definitions and the new error variant checked; no duplicate owner. The CLI table moved to the registry. Dryer reported 0 pairs; gpu-gates was checked manually.

Complexity: crapper flagged no changed function. The added branches select shared fixture policies during setup.

Silent failure: 5 branches/defaults followed. Missing budgets reach fixture-tier refusal; invalid values and planner failures return errors. Fixed-budget and variant exemptions are explicit. The test planner's None means an uncapped plan.

Tests: static mutation audit covered the admission call, positive-budget filter, variant guard, checked sum, registry comparison, mismatch comparison/type, regeneration branch, and reader calls. Their corresponding assertions reject removal, inversion, or wrapping mutants; the formatter-call mutant above survives. Changed fixture-budget tests require the typed mismatch. Tests were not executed.

Gates: no tolerance, threshold, budget pin, or gate clause moved or disappeared. The removed architecture-check exception belonged to the removed CLI imports.

Escape catalog: no C1–C4 pattern matched the changed code. No decode, prefill, kernel, or host-expert hot path changed.

Notes

GPU evidence is incomplete: the RTX 5090 gate runs refer to an earlier revision without its exact head hash. Supply both tested commit hashes with the commands and output to complete the evidence record.

Card check: needs-gpu. This pull request changes crates/gpu-gates/src/tier.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)

@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 0f7b51c into midagedev:main Oct 21, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation gpu The card engine: crates/gpu*, crates/app model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant