From f0462d21bf232b2c94d113af244c056f1c20eb24 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 01:55:08 +0000 Subject: [PATCH 1/2] ci(miri): shard the run across eight jobs Under Miri each test runs as its own interpreted process, about 30 s minimum each (measured locally: 33-39 s wall for single trivial tests). The ~1400 selected tests take 4-5 hours serial; the 2026-11-05 master run was cancelled at the 2-hour job cap after 590 tests. The job now runs as an eight-way matrix, each shard passing --partition count:K/8 to nextest via MIRI_PARTITION. The script runs the full suite when the variable is unset. Coverage is unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMQJzQD5KvNm8BtbsWG --- .github/workflows/ci.yaml | 13 +++++++++++-- scripts/miri-tests.sh | 4 ++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 0ccb28da..f869d3f0 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -393,9 +393,16 @@ jobs: # Only runs on merge queue / push to main โ€” never blocks PR checks. if: github.event_name == 'merge_group' || github.event_name == 'push' runs-on: ubuntu-latest - name: miri - # A healthy run takes well under this; the per-test limit is in the + # Sharded: under Miri every test is its own interpreted process (about + # 30 s minimum each), so the ~1400 selected tests take 4-5 hours serial. + # The 2026-11-05 master run reached 590 tests in the 2-hour cap. Eight + # shards keep each well under the cap; the per-test limit is in the # `miri` nextest profile (.config/nextest.toml). + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8] + name: miri/${{ matrix.shard }} timeout-minutes: 120 steps: - uses: actions/checkout@v4 @@ -405,6 +412,8 @@ jobs: - uses: Swatinem/rust-cache@v2 - uses: taiki-e/install-action@nextest - run: ./scripts/miri-tests.sh + env: + MIRI_PARTITION: count:${{ matrix.shard }}/8 cross_test: # Gated on merge_group only โ€” cross-compile via docker (cross-rs) for diff --git a/scripts/miri-tests.sh b/scripts/miri-tests.sh index 4cdbac31..af966c14 100755 --- a/scripts/miri-tests.sh +++ b/scripts/miri-tests.sh @@ -56,8 +56,12 @@ export RUSTFLAGS="-Zrandomize-layout" # The polyfill is no longer the bottleneck โ€” the missing piece is a # `cfg(miri)` switch in `src/simd.rs` that re-exports from `simd_nightly` # instead of `simd_avx*` under Miri. +# CI shards the run (MIRI_PARTITION=count:K/N, see ci.yaml ยง miri): under Miri +# each test is its own interpreted process, so the whole suite takes about +# 4-5 hours on one runner. Unset, the full suite runs in one go. cargo +nightly miri nextest run -v --profile miri \ --no-fail-fast \ + ${MIRI_PARTITION:+--partition "$MIRI_PARTITION"} \ -p ndarray -p ndarray-rand \ --features approx,serde,nightly-simd \ -E '!( From a1fd9a9755c9fba6a302e60886bb503e2bdf030b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 03:47:20 +0000 Subject: [PATCH 2/2] test(miri): fix the shard failures from the first sharded master run The 2026-11-06 sharded run (42148487191) finished every shard in time but each one failed. Four causes, none a new UB finding: - simd_avx2, simd_amx and simd_int_ops' u8 x i8 GEMM call AVX intrinsics or cpuid, which Miri cannot run (same classes the filter already excludes for simd::tests and cpu_guard). Added to the filter. - simd_nightly f32x16_round_parity compared against f32::round (ties away from zero) although every backend rounds ties to even since 5f4636e7. The oracle now uses round_ties_even. - tests/simd_facade_reexports compares the facade and the hpc homes bit for bit; Miri's default random float error made sigmoid_f32 differ by 3 ulp. The script now sets -Zmiri-deterministic-floats. - Three u9_domino randomized tests ran past the 20-minute per-test limit; they carry cfg_attr(miri, ignore) like the other long sweeps. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01XAqKMQJzQD5KvNm8BtbsWG --- scripts/miri-tests.sh | 16 ++++++++++++++++ src/simd_nightly/tests.rs | 3 ++- tests/u9_domino.rs | 3 +++ 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/scripts/miri-tests.sh b/scripts/miri-tests.sh index af966c14..a430c50b 100755 --- a/scripts/miri-tests.sh +++ b/scripts/miri-tests.sh @@ -39,6 +39,12 @@ export RUSTFLAGS="-Zrandomize-layout" # (rust-lang/miri#1371). Left disabled until both upstream stories close. # export MIRIFLAGS="-Zmiri-tree-borrows" +# Miri adds random error to float operations such as exp by default. The +# suite's oracles compare bit-exact across two code paths (facade vs hpc home, +# tiled vs reference GEMM), which that error breaks: sigmoid_f32 differed by +# 3 ulp in tests/simd_facade_reexports.rs on the 2026-11-06 master run. +export MIRIFLAGS="-Zmiri-deterministic-floats" + # Architectural limit on the Miri sweep: # # `crate::simd::*` (the production dispatch in `src/simd.rs`) re-exports @@ -69,6 +75,9 @@ cargo +nightly miri nextest run -v --profile miri \ ) and !test(/^simd::tests::/) and !test(/^hpc::framebuffer::pyramid_tests::/) and !test(/^cpu_guard::/) + and !test(/^simd_avx2::/) + and !test(/^simd_amx::/) + and !test(/^simd_int_ops::tests::gemm_u8_i8_/) ' # # Tests that are correct but take hours under interpretation (the exhaustive @@ -101,3 +110,10 @@ cargo +nightly miri nextest run -v --profile miri \ # `cpu_guard` reads the CPU with `cpuid`, which is inline assembly; Miri # rejects it ("inline assembly is not supported"). The pre-`main` hook is # `cfg(not(miri))` for the same reason, so test binaries start under Miri. +# +# 5. `!test(/^simd_avx2::/)`, `!test(/^simd_amx::/)`, +# `!test(/^simd_int_ops::tests::gemm_u8_i8_/)` +# Same classes as 2 and 4: `simd_avx2` calls AVX intrinsics ("requires +# unavailable target features: avx"), `simd_amx` and the u8 x i8 GEMM +# dispatch read `cpuid` (inline assembly). They first showed up on the +# 2026-11-06 sharded run; the single unsharded run never got that far. diff --git a/src/simd_nightly/tests.rs b/src/simd_nightly/tests.rs index cddec229..40d4e869 100644 --- a/src/simd_nightly/tests.rs +++ b/src/simd_nightly/tests.rs @@ -690,7 +690,8 @@ fn f32x16_round_parity() { let src: [f32; 16] = [0.5, 1.5, 2.5, 3.5, 4.5, 5.5, -0.5, -1.5, 0.4, 1.6, 2.3, 3.7, -0.4, -1.6, 100.0, -100.0]; let result = F32x16::from_array(src).round().to_array(); for (i, (&r, &s)) in result.iter().zip(src.iter()).enumerate() { - assert_eq!(r, s.round(), "lane {i}"); + // Ties to even on every backend (AVX-512 roundscale, NEON vrndn). + assert_eq!(r, s.round_ties_even(), "lane {i}"); } } diff --git a/tests/u9_domino.rs b/tests/u9_domino.rs index 645fce07..3bd81b2f 100644 --- a/tests/u9_domino.rs +++ b/tests/u9_domino.rs @@ -97,6 +97,7 @@ fn flip_is_a_half_adder_in_every_lane() { /// Random masks (zero, full, one lane, random) at random significances, /// including 16, 63, 64 and beyond, in two different orders. #[test] +#[cfg_attr(miri, ignore = "runs past the 20-minute miri per-test limit")] fn random_adds_match_the_oracle_in_any_order() { let mut s = 0x9E37_79B9_7F4A_7C15u64; let mut adds = Vec::new(); @@ -178,6 +179,7 @@ fn the_64k_boundary_flips_bit_16_then_carries_into_bit_17() { /// u16 products summed through every high representation against `u128`. #[test] +#[cfg_attr(miri, ignore = "runs past the 20-minute miri per-test limit")] fn product_sums_match_native_and_d() { let mut s = 0x0123_4567_89AB_CDEFu64; for (blocks, dense) in [(1usize, false), (300, false), (300, true), (70_000, true)] { @@ -372,6 +374,7 @@ fn abacus_overflow_refuses_and_leaves_the_counter_unchanged() { } #[test] +#[cfg_attr(miri, ignore = "runs past the 20-minute miri per-test limit")] fn abacus_random_sequences_match_64_u128_counters() { let mut s = 0x0123_4567_89AB_CDEFu64; const B: usize = 12;