Repository navigation
chore(ci): link an OpenXLA binary in CI so link-only regressions cannot reach main - #1305
Conversation
…ot reach main `xla-compile` only runs `cargo check`, which never invokes the linker, so a regression in the IREE link recipe in `build.rs` reaches `main` unobserved. Issue #1274 was exactly that: `cargo check --features cuda,xla-iree --all-targets` passed cleanly on a tree that could not link a single integration test, and the failure was found by hand later, not by CI. Add a new `xla-link` job that runs `cargo test --release --features cuda,xla-iree --test xla_prepared_prefill --no-run` on the self-hosted GB10 runner. `build.rs` emits the IREE recipe through `cargo:rustc-link-arg`, which applies to every linked artifact of the crate, so the smallest linked target exercises the same recipe as `mlxcel-server`. `--no-run` links without executing, so the job never touches the GPU. `--release` is mandatory: the debug profile cannot link these targets on this host at all, failing with AArch64 direct-branch relocation overflow against ordinary libstd symbols. The job is gated by a new `xla_link` output on the `changes` job, a sibling `dorny/paths-filter` keyed on `build.rs`, `scripts/iree/**`, `rust-toolchain.toml`, and `.github/workflows/ci.yml`, so it runs only on PRs that can plausibly move the link recipe rather than on every Rust PR or on a schedule. `RUSTFLAGS` is left unset on this job, unlike `xla-compile`, so a red run is unambiguously a link failure rather than a lint failure. The workflow comment adjacent to the job records the trigger rationale, the measured cold cost, and the residual gaps: the `IREE_DIST` and macOS `IREE_MACOS_HOME` recipes in `build.rs` remain unverified because no runner holds either distribution, and the path filter itself is not exhaustive coverage of every way the link line could break. The existing `xla-compile` exclusion list is updated to point at this job for the link gap instead of leaving it open. Refs #1274, #1275, #1282. Closes #1303.
… link gate Substitutes the real measurements for the placeholder: 15m40s from a purged CARGO_TARGET_DIR and 6m to 7m30s warm, against about a minute for the check job over the same feature set. Also records what the gate was actually demonstrated on. The defect from #1274 no longer reproduces on this host, because the IREE runtime that scripts/iree/setup-cuda.sh currently pins is built without the stack protector and libiree_runtime_unified.a holds no __stack_chk_guard reference, so removing the -lc entry links cleanly. The gate was demonstrated instead by dropping libflatcc_parsing.a from the same recipe, where cargo check still passed and the link failed.
Review follow-up on the `xla-link` job added by this PR, both changes confined to `.github/workflows/ci.yml`. Concurrency: `xla-link` is the first job on the shared GB10 runner whose warm cost is minutes rather than seconds (6m to 7m30s warm, 15m40s cold, `timeout-minutes: 120`). `ci.yml` carries no concurrency control at all, so three pushes to a `build.rs` PR queue three link jobs on the one runner that also serves `cargo-clippy` for every Rust PR and the release build, with the superseded runs occupying it to completion rather than being cancelled. A job-scoped group keyed on the PR number supersedes stale runs without changing the behaviour of any other job in the file, matching the pattern `pipeline-parallel-ci.yml` already uses. Fork guard: the note above the `clippy` job asserted that the `github.repository == 'lablup/mlxcel'` guard means "fork PRs never queue on it", and #1303 repeated that reading when specifying the new job. That is not what the guard does. On a `pull_request` event opened from a fork against this repository, `github.repository` is the base repository, so the guard evaluates true and the job runs on the self-hosted runner, executing the PR's `build.rs` and `scripts/iree/**` as the runner's own user. The guard's real effect is to stop the job queueing forever in a fork of the repository, which has no GB10 runner. What gates the fork-PR case is the repository's Actions fork-PR approval policy, currently `first_time_contributors`. The comment now records that, so the next job added under this guard is added with an accurate picture of what it does and does not cover. Refs #1303
Security and performance reviewThe diff is one workflow file, so this is a CI supply chain and self-hosted-runner review rather than an application-code one. No CRITICAL or HIGH issue is introduced by this PR. One HIGH-severity property of the workflow is inherited unchanged from Verified clean
HIGH, inherited unchanged, not introduced by this PRFork pull requests do reach the self-hosted runner. This PR adds no exposure. Worth a separate hardening issue rather than scope creep here: adding What I did fix ( MEDIUM, amplified by this PR, fixed in
|
Review of the new `xla-link` job found two comment claims that the code and the host contradict, plus one gap in the trigger's causal surface. The `-lc` paragraph claimed the pinned IREE runtime is built without the stack protector and that `libiree_runtime_unified.a` holds no `__stack_chk_guard` reference. It does: `/usr/bin/nm` reports the symbol undefined in 176 objects of that archive, including the `call.c.o` named in #1274, and the symbol is still undefined in `libc.so.6` and defined only in `ld-linux-aarch64.so.1`. Every precondition the `build.rs` comment records still holds, so the paragraph no longer concludes the entry is inert; it records that the `-lc` control did not reproduce, that the reason is unresolved, and that the flatcc control is what demonstrated the gate. The "what it links" paragraph implied `mlxcel-server` is not linked here and that a `--bin mlxcel-server` build would be more expensive. Cargo builds the package's bin targets whenever an integration test is selected, so this command links all four binaries plus the test; the target directory from the validation run holds all of them. The rejected alternative is a strict subset of the chosen command, not a costlier one, and the measured figures already include the binaries. The trigger named only `build.rs` and `scripts/iree/**` as the causal surface. `src/lib/mlxcel-xla/build.rs` compiles the C shim and emits `rustc-link-lib=static=xla_iree`, and its `csrc/**` sources produce the object whose undefined symbols the IREE archives resolve, so a link regression can arrive through either. Both are now in the `xla_link` filter. Refs #1303
Implementation Review SummaryIntentAdd a CI job that actually links an OpenXLA target so a regression in the IREE link recipe cannot reach Findings addressed in
|
The target directory persists across PRs, so cargo can find nothing to redo: this job's first run on the PR that added it finished in 12 seconds. That is correct, since cargo relinks when the fingerprint moves and build.rs declares rerun-if-env-changed for all three IREE variables. The narrow hole is a scripts/iree/** edit that changes how the runtime is built without moving IREE_VERSION, where the script's idempotence means nothing rebuilds. Refs #1303
…1381) ## Summary The `xla-compile` job denied `unused_imports` rather than `warnings` because the `cuda,xla-iree` and `xla-diagnostics` feature sets carried a dead-code and clippy backlog that would have made the job red the day it landed. This clears that backlog and widens the gate to `-D warnings`. Resolution followed the criterion in the issue: cfg-gate to the feature set that has a real consumer where the gate needs no duplicate definition, otherwise allow with a comment naming the caller, and delete only when a search finds no consumer under any feature combination. ## What changed ### `mlxcel-xla` - `src/lib/mlxcel-xla/src/iree.rs`: `IreeRaggedLlama::decode_ragged_logits` is now `#[cfg(feature = "diagnostics")]`. Its only callers are `LlavaReferenceDiagnosticEngine::capture` and the Gemma3n diagnostic runners in `batch.rs`, all of which are already `#[cfg(feature = "diagnostics")]`; serving calls `decode_ragged_logits_with_modes` directly. - `src/lib/mlxcel-xla/src/iree.rs`: `decode_ragged_mrope_logits` is deleted. This is the one deletion in the PR. `grep -rn "decode_ragged_mrope_logits" . --exclude-dir=.git --exclude-dir=target` filtered of the `_with_modes` sibling and the `xla_llama_` extern returns exactly two hits: the definition itself, and an error-message string inside `decode_ragged_mrope_logits_with_modes` that stays. There is no caller in any crate, test, bench, or example, under any feature combination. - `src/lib/mlxcel-xla/src/iree.rs`: a doc comment describing `create_ctx` had been stranded above a blank line when #1302 inserted `create_bucket_ctx` and its own doc block between them, which is what `clippy::empty_line_after_doc_comments` was reporting. It is moved back onto `create_ctx`. - `src/lib/mlxcel-xla/src/aux.rs`: `AuxiliaryWeightDType::Float16` and `Uint32` are allowed under `not(feature = "micro-oracle")`. `numeric_probe.rs`, behind that feature, is their only construction site, while `ffi_code` and `size_bytes` have to keep mapping them for the FFI contract on every build. Gating the variants themselves would force a `cfg` onto each arm of those two matches, including the `Float32 | Uint32` arm. - `src/lib/mlxcel-xla/src/phi4_audio.rs`: `load_audio_module` takes a scoped `#[allow(clippy::too_many_arguments)]`. A parameter struct would ripple into both call sites for a private single-file helper, so the allow was preferred per the issue's guidance. - `src/lib/mlxcel-xla/src/aux_manifest.rs`: the nested `if let` in `cache_lock_is_stale` collapses to an edition-2024 let chain. ### `mlxcel` - `src/server/batch/xla_audio_preprocess.rs`: the test-only surface is allowed under `not(test)`, following the `#[cfg_attr(not(test), allow(dead_code))]` pattern already used in `src/model_metadata.rs` and `src/server/startup.rs`. Each site names `xla_audio_preprocess_tests.rs` as the caller: `AudioPreprocessStage::spawn` (serving uses `spawn_with_loader` to keep MLX handles thread-confined), `recv` (serving must not block, so it drains with `try_recv`), `metrics`, `is_healthy`, the `healthy` field it reads, `AudioPreprocessMetrics::snapshot`, and `AudioPreprocessMetricsSnapshot`. The `Cancelled(AudioPreprocessCheckpoint)` payload keeps its field with the same allow: serving matches it as `Cancelled(_)`, and the checkpoint distinction belongs in the type because `Cancelled` is constructed at several checkpoints in that file. - `src/server/media.rs`: `validate_xla_raw_counts` gains a `not(test)` arm alongside its existing `not(feature = "xla-iree")` one. `media_tests.rs` is its only caller; the XLA admission path calls `validate_xla_raw_counts_with_audio` directly because it knows whether the loaded family supports audio. - `src/server/batch/xla_worker_admission.rs`: `drain_preprocessed` takes `#[allow(clippy::while_let_loop)]`. The suggested `while let Some(stage) = self.image_preprocessor.as_ref()` head holds a shared borrow of `self` across a body that calls `&mut self` methods and clears the field, so the `match` shape is load-bearing rather than stylistic. - `src/loading/vlm.rs` and `src/models/sanitize.rs`: the two `needless_match` sites take `#[cfg_attr(not(feature = "surgery"), allow(clippy::needless_match))]`. The match only collapses to `match transform { Some(t) => Some(t), None => None }` when the `surgery` arm is compiled out, which is why the lint fires under `--no-default-features` and not under the default feature set. Applying clippy's suggestion would delete active-pipeline resolution from the default build, so the lint is silenced only for the builds that see the collapsed shape. ### CI - `.github/workflows/ci.yml`: `xla-compile` now sets `RUSTFLAGS: "-D warnings"`, and the paragraph explaining the narrower `-D unused_imports` policy is replaced. - `.github/workflows/ci.yml`: the "Deliberately NOT covered" list records the gap that remains after this change. The job runs `cargo check`, so `-D warnings` denies rustc lints only. `dead_code` and `unused_imports` are gated now, but `collapsible_if`, `needless_match`, `too_many_arguments`, `while_let_loop` and the rest of clippy's own lints are still gated by nothing in CI under these feature sets, because the `clippy` job covers default features only. That half of the backlog can regrow silently; swapping this job's `cargo check` for `cargo clippy` was deliberately left out of scope. ## No runtime behavior change The diff is attribute additions, `cfg` additions, comments, one collapsed `if`, one doc-comment move, and the deletion of one function that has no caller. No executed code path changes. ## Test plan Run on the GB10 host with the IREE runtime provisioned (`bash scripts/iree/setup-cuda.sh`, `eval "$(bash scripts/iree/setup-cuda.sh --env)"`, `MLX_CUDA_ARCHITECTURES=121`), and re-run on the final commit after the review follow-ups landed. Acceptance criteria: - [x] `cargo clippy --features cuda,xla-iree --all-targets -- -D warnings`: exit 0, no warnings. - [x] `cargo clippy --no-default-features --features xla-diagnostics --all-targets -- -D warnings`: exit 0, no warnings. - [x] Default-feature regression, `cargo clippy -p mlxcel --lib --tests -- -D warnings`: exit 0, no warnings. The two commands the job itself runs, under the widened flag, because `cargo clippy -- -D warnings` applies the flag only to the primary package while `RUSTFLAGS` reaches every path-built unit including `mlxcel-core` and `mlxcel-surgery`. Clippy passing is therefore not sufficient evidence that the job will be green: - [x] `RUSTFLAGS="-D warnings" cargo check --features cuda,xla-iree --all-targets`: exit 0. - [x] `RUSTFLAGS="-D warnings" cargo check --no-default-features --features xla-diagnostics --all-targets`: exit 0. Scoped and mechanical checks: - [x] `cargo clippy -p mlxcel-xla --lib --features iree --all-targets -- -D warnings` - [x] `cargo clippy -p mlxcel-xla --lib --features iree,diagnostics --all-targets -- -D warnings` - [x] `cargo clippy -p mlxcel-xla --lib --features iree,micro-oracle --all-targets -- -D warnings`, confirming the `micro-oracle` cfg arm on the `AuxiliaryWeightDType` variants is not narrowed wrongly - [x] `rustfmt --edition 2024 --check` on every changed Rust file - [x] `.github/workflows/ci.yml` parses as YAML End to end, this PR's own CI exercises the change it makes: `OpenXLA feature compile` ran for 7m44s under the new `RUSTFLAGS: "-D warnings"` and passed, so the widened gate is green on arrival rather than red. `OpenXLA feature link`, added by #1305, also ran because `src/lib/mlxcel-xla/**` is in its path filter. Not verified: - [ ] `metal,accelerate`. It cannot be built on this Linux host, so it is unverified by execution rather than confirmed. The static argument that it is unaffected: `make verify-clippy` runs `--features metal,accelerate` without `--no-default-features`, so `surgery` stays on and both `needless_match` attributes expand to nothing; `mlxcel-xla` builds with no features there, so `iree.rs`, `aux.rs`, `aux_manifest.rs` and `phi4_audio.rs` sit behind `#[cfg(feature = "iree")]` and never compile; and on the `mlxcel` side only `media.rs` and `xla_audio_preprocess.rs` compile, where every change is an `allow` attribute that cannot break a build. ## Follow-ups this surfaced, out of scope here - `mlxcel-xla`'s `#[cfg(test)]` modules are gated by no CI job at all. There is no `default-members`, so `cargo check` in this job resolves to `-p mlxcel` and `--all-targets` expands within that package only, while the `clippy` job builds default features where `mlxcel-xla` is default-off. Recorded in the workflow's exclusion list. - Clippy-only lints under these feature sets remain ungated, so that half of the backlog can regrow. Closing it means running clippy in this job, which was left out of scope deliberately. - `media_tests.rs` reaches `validate_xla_raw_counts_with_audio` only through the `supports_audio = false` wrapper, so the `true` branch has no direct unit coverage, and the sign of that flag is the security-relevant bit. Closes #1304
Summary
xla-compilerunscargo check, which never invokes the linker, so a regression in the IREE link recipe inbuild.rscan reachmainunobserved; #1274 was exactly that failure, found by hand after the fact rather than by CI. This PR adds a newxla-linkjob that actually links an OpenXLA target on the self-hosted GB10 runner, closing the gapxla-compile's own comment already recorded as open.What changed
changesjob: a newxla_linkoutput from a siblingdorny/paths-filterfilter keyed onbuild.rs,src/lib/mlxcel-xla/build.rs,src/lib/mlxcel-xla/csrc/**,scripts/iree/**,rust-toolchain.toml, and.github/workflows/ci.yml. That is narrower than the existingrustfilter, so the link job does not run on every Rust PR, but it covers the whole causal surface of a link failure: the rootbuild.rsholds the IREE recipe,scripts/iree/**pins the distribution whose archive set the recipe names,mlxcel-xla's build script and itscsrc/**sources produce the shim object whose undefined symbols those archives resolve, andrust-toolchain.tomlis on the path because fix: Integration tests cannot link with --features cuda,xla-iree #1274 was entirely about where rustc places its own-lcrelative to appendedrustc-link-argentries.xla-linkjob, placed immediately afterxla-compile,needs: changes, gated ongithub.repository == 'lablup/mlxcel' && needs.changes.outputs.xla_link == 'true',runs-on: GB10,permissions: contents: read,timeout-minutes: 120, a job-scopedconcurrencygroup keyed per PR withcancel-in-progress, and its ownCARGO_TARGET_DIR($HOME/.cargo-target/mlxcel-xla-link-ci) separate fromxla-compile's and from the release job's.cargo test --release --features cuda,xla-iree --test xla_prepared_prefill --no-run.--releaseis mandatory because the debug profile cannot link these targets on this host at all (hundreds ofrelocation truncated to fit: R_AARCH64_CALL26errors against ordinary libstd symbols, the unoptimized binary exceeding the AArch64 direct-branch range).--no-runlinks without executing, so the job never touches the GPU. Because cargo also builds the package's[[bin]]targets whenever an integration test is selected, this command linksmlxcel,mlxcel-server,speculative_bench, andmlxcel-bench-decodein addition to the test binary, so it is a superset of the--bin mlxcel-serveralternative rather than a cheaper substitute for it.RUSTFLAGSis deliberately left unset, unlikexla-compile, so a red run here is unambiguously a link failure rather than a lint failure.concurrencygroup exists becausexla-linkis the first job on the shared GB10 runner whose warm cost is minutes rather than seconds. Without it, three pushes to abuild.rsPR queue three link jobs of up to 120 minutes each on the one runner that also servescargo-clippyfor every Rust PR and the release build.IREE_DISTand macOSIREE_MACOS_HOMErecipes inbuild.rsremain unverified because no runner in this repository holds either distribution, and a link regression arriving through a path outside the filter is still not caught at PR time.clippyjob, which asserted that the repository guard means fork PRs never queue on the self-hosted runner. It does not. On apull_requestevent opened from a fork against this repository,github.repositoryis the base repository, so the guard is true and the job runs here. The guard's real effect is to stop the job queueing forever in a fork of the repository. What gates the fork-PR case is the repository's Actions fork-PR approval policy. That reading applies equally toxla-compileand to the new job, and the issue specifying this work repeated the incorrect version, so it was propagating.Verification
All of the following ran on the GB10 host with the IREE runtime provisioned (
bash scripts/iree/setup-cuda.sh,eval "$(bash scripts/iree/setup-cuda.sh --env)",MLX_CUDA_ARCHITECTURES=121).CARGO_TARGET_DIR: exit 0 in 15m40s, ending atExecutable tests/xla_prepared_prefill.rs. Most of that is MLX's CUDA sources compiling from scratch, not the link.build.rsedit actually pays because it invalidates themlxcelcrate but notmlxcel-core's MLX build: 6m01s and 7m24s across two runs, both exit 0.xla-compilecannot. With-l:libflatcc_parsing.adropped from theIREE_CUDA_HOMEbranch ofbuild.rs,xla-compile's commandcargo check --features cuda,xla-iree --all-targetsstill exits 0 in 59 seconds, while this job's command exits 101 witherror: linking with cc failedand undefined references toflatcc_verify_string_field,flatcc_verify_table_vector_field, andflatcc_verify_vector_field. Restoring the archive returns the link to exit 0. That is the same shape as fix: Integration tests cannot link with --features cuda,xla-iree #1274, on the same tree, and is the demonstration the issue asked for..github/workflows/ci.ymland so matches the filter.build.rsis unchanged by this PR; every control above was run outside the branch and reverted.On the negative control the issue specified
The issue's acceptance criteria asked for the demonstration to be done by reverting the
-lcentry added in #1275. That control was tried first and did not fail the link: the command exits 0 withrustc-link-arg=-lcconfirmed absent from the emitted build-script output and the IREE archives confirmed present on the link line.Why it does not fail is unresolved, and nothing here should be read as evidence that the entry is dead. An earlier revision of this PR claimed the pinned runtime was built without the stack protector; that claim was wrong and has been removed. It came from running
nmin a shell wherenmis an alias for an unrelated command, so the search silently matched nothing. Against the pinned distribution, using/usr/bin/nm:__stack_chk_guardis undefined in 176 objects oflibiree_runtime_unified.a, including thecall.c.onamed in #1274; it is undefined inlibc.so.6; and it is defined only inld-linux-aarch64.so.1. Every precondition thebuild.rscomment records still holds today. Two further explanations were tested and ruled out:-lpthreadand-ldlresolve to stub archives on this glibc rather than to scripts that would pull libc in late, andlibm.sogroups onlylibm.so.6.So
build.rsis left alone, the flatcc control above is what demonstrated the gate, and the workflow comment records all of this so the next reader does not delete the-lcentry on the strength of one control that failed to reproduce.Closes #1303