Skip to content

gguf, model: gguf-inventory refuses a top-k above the expert count by its keys - #153

Merged
juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:dgx-spark-gguf-inventory-top-k
Oct 14, 2026
Merged

juliankang4 merged 1 commit into
midagedev:mainfrom
juliankang4:dgx-spark-gguf-inventory-top-k

Conversation

@juliankang4

Copy link
Copy Markdown
Collaborator

What and why

Closes #152.

gguf-inventory reports the routed bytes per token as the sum over MoE blocks of exps bytes * top-k / expert count. It computed the sum in u128 and both reports cast it to u64 (routed_per_token in crates/gguf/src/bin/gguf-inventory.rs, in the stdout report and in the Markdown table). A top-k above the expert count selects more than every expert, so the sum can pass the tensor bytes and then pass u64. A header with expert_count 1, expert_used_count 2 and one F32 tensor of dims [2^61, 1] named blk.0.ffn_up_exps.weight needs 2^63 bytes for the tensor and 2^64 bytes per token. Both reports printed 0, and the command exited 0. A debug build did the same, because as does not check.

The change, one commit:

  • After the metadata is read and known_bytes has summed the tensors, and before any metric is computed, the command stops with exit 1 when expert_used_count is greater than expert_count. The message names both keys and their values: gguf-inventory: deepseek2.expert_used_count 2 is greater than deepseek2.expert_count 1. This is the cause of the overflow: a top-k above the expert count has no meaning, so the report has nothing true to print for it. A header with expert_count 0 and a top-k above 0 is refused the same way; before, it printed n/a for the routed bytes.
  • With top-k at most the expert count, each block adds at most its exps bytes (bytes * k / ec <= bytes). The sum over blocks is then a part of the total that known_bytes already checks (gguf, engram, model, gpu-gates: a tensor whose bytes or data range pass u64 is refused by name #149), so it fits u64. routed_per_token is now a u64 built with one u64::try_from, and the two reports print it without a cast. I used expect there, with the bound in the message, because a failure would mean the guard above it is gone; the FAIL-first run below shows what that looks like.
  • The comment above routed_per_token states the bound, and the module doc says the refusal.
  • CallAcc::scaled in crates/model/src/profile.rs and the per-type share in record_rows in crates/model/src/ops.rs narrowed a u128 product with as u64. They go through u64::try_from(..).expect(..) now, as R5 in docs/rust-quality.md asks. See the next section for why this is not a fix of a reachable overflow.

I did not add a second check at the routed_per_token conversion. The bound holds for any top-k the guard lets through, so a checked conversion that returns an error would be a branch that nothing can reach.

Same defect elsewhere

routed_per_token was the only narrowing cast of a header-derived byte count left in gguf-inventory. The rest of its as casts are widening (u64 and u32 to u128), a float conversion for the GiB column (gib), or in the test that sizes one block of each type. The byte sums are parts of known_bytes, as #149 states. I searched the other crates for u128 and for expert_used_count consumers: none of them narrows a u128 byte count. Other crates do size n_used experts (for example crates/gpu/src/arch/deepseek2/dispatch.rs:1096 and crates/placement/src/placement/workstation.rs:341), but from a loaded model's shapes with u64 arithmetic, and I did not change them.

The two as u64 after a u128 product in the model crate scale a profile's nanoseconds by a part of a whole: the factor is at most 1, so the result is at most ns and no header value reaches them. They were not a bug. I changed them anyway so that no narrowing cast of a u128 product is left in host code, which R5 asks for, and the expect messages state why the conversion cannot fail. record_union_rows in the same file already has this form. bloomery-model does not build on macOS (bloomery-threads uses Linux-only libc affinity calls), so a test on the Mac cannot run these two lines. The lint step of just mac-static compiles them for x86_64-unknown-linux-gnu, and I did not run the model crate's tests on a Linux node.

Code shape

  • No second copy. The cast was in two places (the two reports); both now read the one u64.
  • Model code holds only architecture facts. The model edits are two integer conversions; no architecture fact changes.
  • Dead code is gone. The u128 result type is gone; nothing else was made unused.
  • No silent failure. A top-k above the expert count is a named refusal in debug and release builds alike.
  • Comments state what is true now.
  • No relaxed gate.
  • 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 added. The test header writer write_gguf_kvs in crates/gguf/tests/inventory.rs takes the metadata list, and both write_gguf and the new moe_header call it.

What I ran

The Mac runs are on an Apple M4 Pro (macOS 27.0.1, rustc 1.100.0-nightly (e457a7b0d 2026-08-27)). just mac-static ran on an Apple M5 with the same rustc.

The test is a_top_k_above_the_expert_count_is_refused_by_name in crates/gguf/tests/inventory.rs. It writes the header above (moe_header) and runs the gguf-inventory binary with --markdown. It checks that top-k 2 over 1 expert and top-k 1 over 0 experts each exit 1 with exactly the message above, print nothing on stdout and write no Markdown file. It then runs top-k 2 over 2 experts and checks that both reports show 2^63 bytes (9,223,372,036,854,775,808) as the routed bytes per token, so the guard does not refuse what the cast could not break. Last, it runs top-k 0 over 0 experts and checks that the command succeeds and prints routed experts/token = n/a. The refusals run before the successes because a success writes the Markdown file.

FAIL-first. Each mutant is made in a scratch copy of the head (git archive HEAD) with the test kept, and run in a debug and a release build.

  1. The guard removed: the if let (Some(ec), Some(k)) = (expert_count, top_k) && k > ec { ... } block is deleted and the new u64::try_from stays. The command panics in the conversion instead of printing 0, and the test fails on exit code 101 in both builds:
thread 'main' (14078089) panicked at crates/gguf/src/bin/gguf-inventory.rs:356:42:
top-k is at most the expert count: TryFromIntError(PosOverflow)
thread 'a_top_k_above_the_expert_count_is_refused_by_name' (14078082) panicked at crates/gguf/tests/inventory.rs:293:9:
  left: Some(101)
 right: Some(1)
test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
  1. The guard condition k > ec changed to k > ec && ec > 0 (a line added to the if let chain). Top-k 2 over 1 expert is still refused, and top-k 1 over 0 experts now exits 0 with n/a, so the test fails on its second refusal in both builds:
thread 'a_top_k_above_the_expert_count_is_refused_by_name' (14153240) panicked at crates/gguf/tests/inventory.rs:293:9:
  left: Some(0)
 right: Some(1)
test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out
  1. gguf-inventory.rs replaced by the base version (git show cff55213:crates/gguf/src/bin/gguf-inventory.rs). The first refusal fails with exit 0 and a report that prints routed experts/token = 0 bytes, in both builds:
thread 'a_top_k_above_the_expert_count_is_refused_by_name' (14176339) panicked at crates/gguf/tests/inventory.rs:293:9:
assertion `left == right` failed: Output { status: ExitStatus(unix_wait_status(0)), stdout: "shard 1: ...
  left: Some(0)
 right: Some(1)
test result: FAILED. 7 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

(... marks the rest of the printed report, cut here.)

With the fix, debug and release:

$ cargo test -p bloomery-gguf --test inventory
test result: ok. 8 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
$ cargo test -p bloomery-gguf --bin gguf-inventory
test result: ok. 2 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out

(the --release runs print the same two lines).

just mac-static upstream/main at the head 18482677 on the M5: ok in 121 s. The base for that run was upstream/main at 4a9419ea, two docs-only commits ahead of cff55213, which the branch is based on. fmt-check has 0 diff blocks; lint has 41 warnings, the ratchet's 41 in tools/lint-ratchet.txt, and compiles bloomery-model for x86_64-unknown-linux-gnu. Combos ran 20 shapes, 0 red, 28 skipped as out of scope. check-arch, check-comments, check-defaults, check-levers, check-loads, check-ptx-names, check-records, check-recipes, check-rustflags, check-unsafe and check-waits pass.

Where the test runs: CI's t0 tier tests none of bloomery-gguf. gate-1-1 runs the gguf tests (unit, integration and the gguf-inventory bin), so it covers this one. I did not run gate-1-1 on a Linux node: the change is in a binary and a test that build the same on macOS, and the unit tests of the gguf library that do not build on macOS (libc::RUSAGE_THREAD in split_tests) are not touched.

Hardware (if the change is for a machine)

Not a machine-specific change.

AI help

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

… its keys

The routed bytes per token are the sum over MoE blocks of exps bytes * top-k / expert count.
The sum ran in u128 and both reports cast it to u64. A top-k above the expert count selects
more than every expert, so the sum can pass the tensor bytes: a header with expert_count 1,
expert_used_count 2 and one F32 tensor of dims [2^61, 1] in block 0 gives 2^64, which stdout
and Markdown printed as 0 with exit 0, in debug and release builds alike.

gguf-inventory now stops with exit 1 before any metric when expert_used_count is greater than
expert_count, naming both keys with their values. A header with expert_count 0 and a top-k
above 0 is refused the same way. With top-k at most the expert count, each block adds at most
its exps bytes, so the sum is a part of the total that known_bytes already checks and fits
u64. routed_per_token is a u64, built with one try_from, and the two reports print it without
a cast.

The two other u128 products narrowed with as u64 in the model crate, CallAcc::scaled and the
per-type share in record_rows, go through u64::try_from with the reason in expect, as
docs/rust-quality.md R5 asks. Their factor is a part of a whole, so the share is at most ns.

The test runs the binary on a synthetic header with both reports requested: top-k 2 over 1
expert and top-k 1 over 0 experts exit 1 with the message and write neither report; top-k 2
over 2 experts reports 2^63 bytes in both, and top-k 0 over 0 experts reports no routed
figure. The synthetic header comes from the shared write_gguf_kvs.
@github-actions github-actions Bot added the model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja label 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: CLEAN: The guard prevents routed-byte overflow, and the profile conversions preserve valid time shares.

Non-blocking

⚠ Linux profile evidence: crates/model/src/ops.rs:2020. The PR reports cross-compilation but no runtime model tests. Attach just gate-profile output from x86_64 Linux to exercise record_rows and CallAcc::scaled during profiled decode.

Checked

Duplicates: 3 new functions checked; no duplicated owner. Dryer reported no pairs.
Complexity: crapper flagged no function; the metadata guard adds one error branch.
Silent failure: 4 sites followed. Invalid top-k exits 1 before output; valid sums and profile shares fit u64; unavailable metrics report n/a.
Tests: the regression protects crates/gguf/src/bin/gguf-inventory.rs:294. Removing the guard or excluding zero experts fails the exit-code assertion; changing > to >= fails the equality success assertion. The PR supplies debug/release FAIL-first output for the first two mutants.
Gates: no pins or bands moved; one synthetic regression added.
Performance: inventory is a tool path; model conversions run only during profiling and add no allocation, lock, copy or launch.
Adversarial: maximum u64 counts keep the product within u128; exact and partial sums remain bounded by checked total bytes. The deepseek2 and qwen3moe metadata readers reject excessive top-k.
Escape catalog: no patterns matched. No ledger lessons are in force.

Notes

Static review only; no builds or tests were run during this review.

🔨 Shingler, bloomery's review bot: approves a clean pull request and requests changes on a must-fix (how it works)

@juliankang4

juliankang4 commented Oct 14, 2026 •

Copy link
Copy Markdown
Collaborator Author

On the non-blocking Linux profile item: just gate-profile was run at 18482677 and at its base cff55213 on x86_64 Linux (Ryzen 9 9950X, RTX 5090, driver 595.99.02, rustc 1.100.0-nightly 2026-08-27) with DeepSeek-V2-Lite-Chat Q3_K_M (sha256 checked against the model store). Both builds stopped with rc 101 before the test body, in crates/model/tests/common/manifest.rs:17: this box has no ref/MANIFEST.tsv oracle for that model. The panic says the lead produces it with just dump-ref and not to run it ourselves, so it was not generated here, and there is no profile table to attach. No test runs record_rows or CallAcc::scaled yet, so their runtime evidence waits for a box run with the oracle.

@juliankang4
juliankang4 merged commit 6450b61 into midagedev:main Oct 14, 2026
6 checks passed
@juliankang4

Copy link
Copy Markdown
Collaborator Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model The CPU engine and model files: crates/model, models, hf, gguf, tokenizer, jinja

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gguf-inventory: routed byte reports truncate a u128 to u64

1 participant