fix: tolerate f32 reassociation in mllama ragged test, add nightly gate - #953
Merged
Merged
Conversation
The ragged cross-attention test has failed deterministically on `main` since #622 landed on 2026-07-02, asserting exact f32 equality between an interior-masked 8-row attention and a contiguous 6-row one. Those two differ by 2^-28, which is 0.25 ULP of the largest output element, because the surviving keys occupy lanes {0,1,4,5,6,7} in the 8-wide reduction and {0..5} in the 6-wide one, reassociating both the softmax denominator and the value accumulation. The equivalence #622 proves holds in exact arithmetic, and exact arithmetic does not imply bitwise equality once lanes move inside a parallel reduction. The two sibling tests mask a trailing run instead, so every surviving key keeps its lane position, and they keep their exact `assert_eq!(..., 0.0)`. No production code changes: only the sliced path runs in production, and the masked branch exists solely inside this test. That failure survived 26 days because no job runs the general unit suite. pipeline-parallel-ci.yml does run clippy and `cargo test`, but with `distributed::`-prefixed selectors behind a path filter that never fires on a model-port PR, so the ci.yml header comment claiming neither runs in any workflow was itself inaccurate. The same gap let `family_order_is_exhaustive` sit red on `main` until #946, which makes this systemic rather than a one-off. nightly-verify.yml runs `make verify` once a day on `self-hosted-macos-26-arm64` and files a GitHub issue when it fails, reusing that issue on subsequent nights. It deliberately does not reinstate the PR-time gate #23 removed: it blocks nothing and consumes the shared runner once per day rather than once per PR, while running the same feature set and hardware class in which the mllama failure is observable at all. A GitHub-hosted job would not have been cheaper, since every Rust test here first builds MLX C++ through mlxcel-core, and it would build without metal or accelerate, so it could not have reproduced this failure. CONTRIBUTING.md has claimed since #14 that CI enforces clippy and `cargo test` on the self-hosted macOS runner for every Rust PR. #21 moved that gate and #23 deleted it on 2026-05-18, neither updating the doc, so the claim has been false since then. The Makefile `verify*` header made the same stale claim about a `clippy + test (macOS ARM64)` job and now points at nightly-verify.yml, which genuinely invokes those same targets. Validated on an Apple M1 Ultra: `cargo test --release --lib --features metal,accelerate models::mllama` passes 8/8, and temporarily setting the new tolerance to 0.0 reproduces the original failure at the reported value, so the assertion is not vacuous. Closes #939
inureyes
force-pushed
the
fix/issue-939-mllama-ragged-tolerance-ci-gate
branch
from
July 28, 2026 13:09
ed72cc3 to
dc6c6fb
Compare
inureyes
added a commit
that referenced
this pull request
Jul 30, 2026
… calls it (#961) ## What breaks `Pipeline Parallel CI / 2-host logical` fails on every PR that touches the pipeline-parallel paths. The pipeline-parallel step itself passes; only the trailing `Clippy on PP modules` step fails, and it fails on `src/multimodal/`, which such a PR typically does not touch at all. `export_qwen2_vl_prefill` is imported at `src/multimodal/host_preprocessor.rs` and defined at `src/multimodal/host_preprocessor_export.rs` unconditionally, but its only non-test call site is inside a `#[cfg(feature = "xla-iree")]` block. The workflow runs `cargo clippy -p mlxcel --lib --tests -- -D warnings` with no features, so that call site compiles out, the import becomes unused, the function becomes dead, and `-D warnings` promotes both to errors. It went unnoticed because the workflow is path-filtered to the pipeline-parallel modules and last ran on 2026-07-20, while the unconditional import arrived on 2026-07-24 in `edc0ebbb6`. The first PR to touch those paths since is where it surfaced. ## The fix Gate the import and the definition on `any(feature = "xla-iree", test)`. The feature arm matches the single production call site; the `test` arm is needed because the module tests exercise the function irrespective of features, and dropping them would trade a lint failure for lost coverage. ## Validation Reproduced the exact CI invocation locally. Before: two errors, `unused import: export_qwen2_vl_prefill` and `function export_qwen2_vl_prefill is never used`, and `could not compile mlxcel (lib)`. After: `Finished dev profile`, clean. The normal development configuration is unaffected: `cargo clippy --release --lib --tests --features metal,accelerate` is clean, and the Qwen2-VL export tests still compile and pass. One unrelated test in that module, `xla_loader_keeps_text_and_unqualified_vlm_image_capability_false`, fails both before and after this change and also fails on `main`, so it is out of scope here. ## Why this is filed as its own PR It is a repository-wide unblock rather than part of any feature branch, and it is the second breakage of this shape found this week after the `dtolnay/rust-toolchain` pin in #954: pre-existing on `main`, invisible because a path filter or a feature gate kept any run from exercising the failing combination. The nightly verification job added in #953 targets exactly that blind spot.
This was referenced Jul 30, 2026
inureyes
added a commit
that referenced
this pull request
Jul 31, 2026
…l three outcomes (#987) ## What was broken `multimodal::host_preprocessor::tests::xla_loader_keeps_text_and_unqualified_vlm_image_capability_false` has failed deterministically on `main` since `edc0ebbb6` (#915). It loops over `["llama", "qwen2_vl"]` asserting `load_xla_image_preprocessor(...)` returns `Ok(None)`, but #915 made `qwen2_vl` return `Err(InvalidConfig("Qwen2-VL XLA image execution requires the xla-iree feature"))` when the feature is absent, so the `.unwrap()` panics. That commit edited the same test file, adding an import and two export tests, and left this loop untouched, so the contract and the test that pins it diverged inside a single change to a single file. This is the third deterministically-failing test found on `main` in one week, after the two repaired in #946 and #953. It is the first one the nightly verification job added in #953 should catch on its own. ## Which side is right The production behavior stays; the test changes. The reasoning, recorded here so it is not relitigated: Qwen2-VL has no MLX vision fallback. The arm immediately above already rejects `MLXCEL_XLA_VISION_BACKEND=host` for it with exactly that reason, so in a build without `xla-iree` there is no code path that can embed its images. `Ok(None)` for such a checkpoint is indistinguishable from a text-only checkpoint, so a session would start with every image silently ignored, which is a worse outcome than a named failure at load. Failing at load also takes away nothing that worked. `xla-iree = ["xla-backend", "mlxcel-xla/iree"]`, and without `mlxcel-xla/iree` the session's `prefill` and `decode_step` return `mlxcel_xla::NOT_WIRED`, whose text is "the OpenXLA inference session was built without the `iree` feature; rebuild ...". An `xla-backend`-only build cannot generate from any checkpoint at all, so there is no working configuration being broken here. Feature absence already errors elsewhere in the same function: an explicit `MLXCEL_XLA_VISION_BACKEND=iree` returns `Err` for the same reason. ## The counter-argument, addressed rather than dropped Without `xla-iree`, LLaVA does **not** error. Unless the policy is explicitly `iree` it falls through to the host preprocessor and returns `Ok(Some(host))`. Qwen2-VL is the only family where a missing compile-time feature is fatal, and that asymmetry deserved an answer rather than silence. It is deliberate and now documented where a reader will find it: LLaVA has a complete MLX host vision tower and projector to fall back to, Qwen2-VL has none. The doc comment on `load_xla_image_preprocessor` previously described only two of the three outcomes the function implements, which is precisely how the contract drifted from its test. It now covers all three (`Ok(None)`, `Ok(Some)`, `Err`), states which families reach each and why, and records the `NOT_WIRED` reasoning. ## Changes The capability-false loop drops `qwen2_vl` and gains `mllama`, so it still covers an unqualified VLM family rather than degrading to a text-only-only test, with an inline comment explaining why `qwen2_vl` is deliberately absent and where its contract is pinned instead. A new `xla_loader_rejects_qwen2_vl_without_the_iree_feature`, gated `#[cfg(not(feature = "xla-iree"))]` because with the feature the same config takes the IREE load path, asserts the error variant and that its message both names the missing feature and carries the `--features xla-iree` rebuild instruction. Both callers surface this string verbatim into a startup failure, so the instruction is the only actionable part an operator receives; the error text was extended to carry it. ## Validation `cargo test --release --lib --features metal,accelerate multimodal::host_preprocessor` goes from `11 passed; 1 failed` to `13 passed; 0 failed`. `cargo fmt --all -- --check` and the cross-repo-ref guard are clean. No production behavior changes: the only non-test edits are the doc comment and the error string. Closes #966
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two fixes for one root cause.
models::mllama::text::tests::ragged_real_tile_rows_match_reference_masked_full_rowshas failed deterministically onmainsince #622 landed on 2026-07-02, because it asserts exact f32 equality across a reduction whose lane assignment differs between the two sides. That failure survived 26 days because no workflow runs the general unit suite, which is the second and more consequential half.Half 1: the assertion
The failure value is
3.7252903e-9, which is bit-exactly2^-28, against an output whose max absolute element is1.3224149e-1(1 ULP there is1.4901161e-8). The difference is 0.25 ULP of the largest element. A genuine masking or row-selection error would surface at order1e-1, eight orders larger, because the padding rows carry content of the same magnitude as the real rows.The issue records six probes that localize the cause. Rather than re-run them, I checked their conclusion holds:
0.0, so theconcatenateconstruction is not the trigger.3.7252903e-9with noconcatenateinvolved, so the interior mask position alone is the trigger.0.0. That independently confirms theexp(logit - 1e9) == 0.0premise perf(vision/mllama): build cross-attention states from real tiles only #622 relies on, and it is the reason the two sibling tests legitimately pass on exact equality. Both of those siblings still pass on exact equality in this branch, which is the same premise holding under test.The failing case is the only one where surviving rows move lane position: they sit at
{0,1,4,5,6,7}in the 8-wide reduction and at{0..5}in the 6-wide one. That reassociates both the softmax denominator and the value accumulation, and f32 addition is not associative. The equivalence #622 proves holds in exact arithmetic, and exact arithmetic does not imply bitwise equality once lanes move inside a parallel reduction.So this is a tolerance question, not a production bug. Only the sliced path runs in production, since the port threads no text-side mask at all; the masked branch exists solely inside this test. No production code changed.
ragged_real_tile_rows_match_reference_masked_full_rowsnow assertsdiff <= 1e-6against a namedRAGGED_REASSOCIATION_TOLconstant, and carries a comment recording why it tolerates a last-bit difference while its siblings do not, plus an explicit instruction not to tighten it back.1e-6is about 5 orders of magnitude below the1e-1output scale and about 2.5 orders above the observed artifact, so it stays loud on a real error, and it matches themax_abs_diff(..) < 1e-6bound already used elsewhere insrc/models/.real_tile_rows_match_reference_masked_full_rowsandfull_tile_mask_is_the_unmasked_computationkeep their exactassert_eq!(..., 0.0)and still pass.The test was red on arrival, not a later regression. The issue reports building at #622's merge commit
0ad300c3fand getting the identical value, and the supporting history checks out on this branch:git log 0ad300c3f..origin/main -- src/models/mllama/is empty,attention_from_ptrinsrc/lib/mlxcel-core/src/layers.rslast changed in #149, andMLX_EXPECTED_COMMITlast moved in #270 on 2026-06-14, both before the test landed.Half 2: the absent gate, and the decision
The accurate statement is that no job runs the general unit suite, not that no workflow contains
cargo test.pipeline-parallel-ci.ymlrunscargo clippy -p mlxcel --lib --tests -- -D warningsdirectly and reachescargo testthroughscripts/ci/run-pp-heterogeneous-memory.shandrun-pp-two-host-logical.sh, but withdistributed::-prefixed selectors that could never have matchedmodels::mllamaormain_tests, behind a path filter that never fires on a model-port PR. Theci.ymlheader comment claiming clippy and cargo-test "do NOT run in any workflow" is therefore itself inaccurate, and is corrected here.This is systemic rather than a one-off:
family_order_is_exhaustiveinsrc/main_tests.rswas also failing onmain, missingMiniMax VLMandStep VLMfromFAMILY_ORDERafter #800 and #781 each added a family without registering it, and was repaired in #946. Two independent deterministic, weight-free failures reachedmainthrough the same gap.Decision: a scheduled nightly on the self-hosted runner, not a PR-time gate
New
.github/workflows/nightly-verify.ymlrunsmake verify(fmt + clippy + test) once a day at 18:00 UTC (03:00 KST) onself-hosted-macos-26-arm64, plusworkflow_dispatchfor on-demand runs. A failed scheduled run files a GitHub issue titled[nightly-verify] main is red, reusing that issue on subsequent nights rather than filing duplicates, so the signal is not another Actions tab nobody opens. The three verify steps run independently so a fmt violation cannot mask a red suite.Weighed against the cost rationale that motivated the removal in #23:
make verifywould have caught locally", and chore(ci): move clippy + test from PR-time CI to release pipeline #21 before it argued the runner round-trip "wasn't adding signal that wasn't reachable locally". Both objections are about blocking. This blocks nothing, and consumes that runner once per day at a quiet hour instead of once per PR.make verifytells a developer whether the tree is green. It cannot tell anyone whether the tree was green when nobody ran it, which is the exact failure mode both of these tests hit. That is what a scheduled run adds, and it adds it without taking on the cost that chore(ci): move clippy + test from PR-time CI to release pipeline #21 and chore(ci): drop clippy+test from release pipeline; keep gate local-only #23 rejected.mlxcel-core, so narrowing the selector tomain_testsand the detection/metadata registration tests does not narrow the build. The existingtwo-host-logicaljob budgets 45 minutes onubuntu-latestfor exactly that reason.ubuntu-latestbuilds withoutmetalandaccelerate, leaving the gated regions of mlxcel-core that the Makefile's ownverifycomment calls out untypechecked. It would have caughtfamily_order_is_exhaustive, but it could not have reproduced the mllama failure at all, since that is a Metal SDPA reduction-order artifact. A green ubuntu job would have reported "fine" whilemainwas red.push: [main], which costs one run per merge rather than one per PR.The runner carries no model weights, which is fine: the real-checkpoint tests under
tests/self-skip on a missingmodels/<name>dir or are#[ignore]d, so what the nightly actually gates is the weight-free unit and contract suite, which is where both known failures lived.A third instance, which this PR's own CI ran into
Worth recording because it is the same defect class and it happened during this PR. The
cargo-fmtcheck here failed, and not on formatting: it died atrustup toolchain install 1.100.0with a 404, because #146 bumpeddtolnay/rust-toolchainto a ref naming a Rust release that does not exist.cargo fmtis the only PR-time Rust gate this repository has, and it had not run since 2026-07-27. It hid for the same structural reason as the other two: the job is gated onneeds.changes.outputs.rust == 'true', so a docs-only or packaging-only merge skips it and the workflow still reports success.That is now fixed on
mainby #954, which landed while this PR was open and reached the same conclusion independently. This branch is rebased onto it and carries none of that change; the duplicate commit was dropped rather than merged. Three independent instances in one issue's lifetime is the argument for the nightly, not against it.One thing that is deliberately not fixed here
maincurrently emits two-D warnings-fatal warnings that predate this PR, both visible in the--bin mlxcelbuild:They are not fixed here, deliberately.
export_qwen2_vl_prefillis not actually dead:src/multimodal/host_preprocessor.rs:428and the module's tests call it, so the warning is a feature-gated artifact of the binary's build configuration, and the right fix is acfgdecision belonging to the in-flight OpenXLA vision work rather than a deletion from this PR. Both arrived with #915 two days ago and nothing caught them, which is a third, milder instance of the same gap this issue is about.The consequence is that unless they are resolved first, the first nightly run will fail on its clippy step and file its tracking issue. That is the mechanism working, and it is why the three verify steps are split: a red clippy still leaves
make verify-testrunning and reported separately, so it cannot hide the state of the suite.What changed
src/models/mllama/text.rs:ragged_real_tile_rows_match_reference_masked_full_rowsmoves fromassert_eq!(max_abs_diff(..), 0.0)toassert!(diff <= RAGGED_REASSOCIATION_TOL)with the reassociation rationale recorded inline. The module comment above the three tests now says two of them pin byte-level equality while one tolerates the artifact. No production code touched..github/workflows/nightly-verify.yml(new): scheduledmake verifyon the self-hosted Apple Silicon runner, with issue-filing on failure, a fork guard, anightly-verifyconcurrency group, and the persistentCARGO_TARGET_DIRpatternrelease.ymlalready uses..github/workflows/ci.yml: header comment corrected. It no longer claims clippy and cargo-test run in no workflow, and now names both the path-filteredpipeline-parallel-ci.ymljobs and the new nightly.CONTRIBUTING.mdlines 44 to 49: the two commands are markedNOT gated at PR time; yours to runinstead of "enforced by CI on self-hosted macOS runner", and the paragraph now describes the gate that exists. The old claim came from ci: add clippy and cargo test gates via self-hosted macOS runner #14 and has been false since chore(ci): move clippy + test from PR-time CI to release pipeline #21 moved the gate and chore(ci): drop clippy+test from release pipeline; keep gate local-only #23 deleted it on 2026-05-18, with neither commit updating the doc.Makefile: theverify*header claimed to match ".github/workflows/ci.ymlexactly" and to reproduce aclippy + test (macOS ARM64)job that no longer exists. It now points atnightly-verify.yml, which genuinely invokes these same targets, and states that the local run is the real gate.Five files, one commit. The toolchain-ref fix described above is not part of this diff; it is #954's, and this branch is rebased on top of it.
Test plan
DEVELOPER_DIR=... cargo test --release --lib --features metal,accelerate models::mllamaon an Apple M1 Ultra: 8 passed, 0 failed, including both siblings still on exact equalityRAGGED_REASSOCIATION_TOLto0.0fails withragged per-image selection must match the reference-masked full attention to within 0e0, got 3.7252903e-9, which is bit-exactly the2^-28the issue reports. That confirms both the issue's measurement and that the new assertion is not vacuous. Restored to1e-6and re-run green.DEVELOPER_DIR=... cargo test --release --features metal,accelerate --bin mlxcel family_order: 2 passed, 0 failed, confirming the other known instance is green after feat(models): add Ant Group Ling / Bailing MoE (bailing_moe) text model support #946cargo fmt --all -- --checkactionlinton both workflows: only the expected unknown-custom-runner-label note forself-hosted-macos-26-arm64, whichrelease.ymlalready carriesBASE_REF=origin/main python3 scripts/ci/check_cross_repo_refs.pycargo-fmtandcargo-denygreen on this PR once rebased onto ci: restore the rust-toolchain pin and stop dependabot from bumping it #954 (cargo-fmtwas red before that, for the toolchain-ref reason above, not for formatting)One acceptance criterion is discharged by the mechanism rather than by hand. The entire
cargo test --release --features metal,acceleratesuite was not run here; the two known failures were verified individually with narrow selectors. The nightly's first run establishes the whole-suite state on the real runner and files anything still red, which is the "or each remaining failure is filed as its own issue" branch of that criterion.Closes #939