From 99580691f7583e5d5b85cb36407f22463a64f63e Mon Sep 17 00:00:00 2001 From: Hiroshi Shinaoka Date: Mon, 9 Nov 2026 22:49:50 +0900 Subject: [PATCH 1/4] fix(lu): do not perf-warn on contiguous row-major input (#345) The `perf-warn` predicate of the column-pivoted factorisation read `col_stride == 1 || row_stride != 1`, so every contiguous row-major input warned "Found matrix with generic strides" -- a layout the message itself names as preferred. Only a contiguous column-major input was silent, because both clauses happen to be false there. Use the predicate the message describes, `has_generic_strides(row_stride, col_stride) = |row_stride| != 1 && |col_stride| != 1`, and unit-test the layouts next to it: contiguous row-major, contiguous column-major and a transposed view are silent, a matrix with neither stride equal to one warns. That is the same predicate as the full-pivoting fix in #342; the helper stays local to this module until both land, then it can move to `lu/mod.rs`. Only the message is affected: no arithmetic path changes, so there is no measurement to make. Upstream `faer` 0.24.4 and its `main` at `0ecd846` carry the same predicate (`faer/src/linalg/lu/partial_pivoting/factor.rs:245-253`). Recorded as `upstream-reports/faer-042-lu-perf-warn-row-major`; nothing was filed upstream, per AGENTS.md's deferred-feedback rule. --- faer/src/linalg/lu/partial_pivoting/factor.rs | 27 +- upstream-reports/README.md | 4 +- .../Cargo.lock | 686 ++++++++++++++++++ .../Cargo.toml | 13 + .../faer-042-lu-perf-warn-row-major/README.md | 62 ++ .../src/main.rs | 80 ++ 6 files changed, 869 insertions(+), 3 deletions(-) create mode 100644 upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.lock create mode 100644 upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.toml create mode 100644 upstream-reports/faer-042-lu-perf-warn-row-major/README.md create mode 100644 upstream-reports/faer-042-lu-perf-warn-row-major/src/main.rs diff --git a/faer/src/linalg/lu/partial_pivoting/factor.rs b/faer/src/linalg/lu/partial_pivoting/factor.rs index 907694d4..e6ceedf7 100644 --- a/faer/src/linalg/lu/partial_pivoting/factor.rs +++ b/faer/src/linalg/lu/partial_pivoting/factor.rs @@ -626,6 +626,18 @@ pub fn lu_in_place_scratch( _ = params; StackReq::new::(Ord::min(nrows, ncols)) } +/// `perf-warn`: whether neither stride walks a line, i.e. the input is neither +/// column-major (`row_stride` 1) nor row-major (`col_stride` 1). Both preferred +/// layouts factor at the same speed, so only a matrix that is neither warns; +/// the predicate this replaced warned on every row-major input. +#[cfg(any(feature = "perf-warn", test))] +#[inline] +pub(crate) fn has_generic_strides( + row_stride: isize, + col_stride: isize, +) -> bool { + row_stride.unsigned_abs() != 1 && col_stride.unsigned_abs() != 1 +} pub fn lu_in_place<'out, I: Index, T: ComplexField>( A: MatMut<'_, T>, perm: &'out mut [I], @@ -637,8 +649,7 @@ pub fn lu_in_place<'out, I: Index, T: ComplexField>( let _ = ¶ms; let truncate = I::truncate; #[cfg(feature = "perf-warn")] - if (A.col_stride().unsigned_abs() == 1 - || A.row_stride().unsigned_abs() != 1) + if has_generic_strides(A.row_stride(), A.col_stride()) && crate::__perf_warn!(LU_WARN) { log::warn!( @@ -747,6 +758,18 @@ mod tests { use crate::{Mat, assert, c32, c64}; use crate::utils::cost::{pays_at, rate, serial_time}; use dyn_stack::MemBuffer; + /// `perf-warn`: contiguous column-major and row-major inputs are the two + /// recommended layouts, so the warning fires only for a matrix that is + /// neither. The predicate this replaced warned on every row-major one. + #[test] + fn perf_warn_predicate_excludes_both_preferred_layouts() { + assert!(!has_generic_strides(64, 1)); // contiguous row-major: `row_stride = ncols` + assert!(!has_generic_strides(1, 5)); // contiguous column-major: `col_stride = nrows` + assert!(!has_generic_strides(1, 64)); // transposed view: one stride still walks a line + assert!(has_generic_strides(3, 2)); // neither stride walks a line + assert!(!has_generic_strides(-64, 1)); // reversed row-major + assert!(has_generic_strides(-2, -130)); + } #[cfg(feature = "rayon")] #[test] fn region_rules_follow_the_serial_time() { diff --git a/upstream-reports/README.md b/upstream-reports/README.md index 4d4a64f5..6d356e05 100644 --- a/upstream-reports/README.md +++ b/upstream-reports/README.md @@ -56,6 +56,7 @@ The program prints the expected and the actual value, and exits with status 1 wh | [faer-039](faer-039-norm-max-ignores-nan/) | `norm_max` ignores NaN entries | `faer` 0.24.4 | wrong value | tensor4all/faer#319 (`c911ff3`) | not yet | 2026-11-09 | | [faer-040](faer-040-stats-negative-output-stride/) | The `stats` reductions write their output in reverse when the output axis has a negative stride | `faer` 0.24.4 | wrong value | tensor4all/faer#319 (`c911ff3`) | not yet | 2026-11-09 | | [faer-041](faer-041-zst-capacity-panic/) | `Mat::<()>::with_capacity(1, 1)` panics with a division by zero | `faer` 0.24.4 | panic | tensor4all/faer#288 (`a977480`) | not yet | 2026-11-09 | +| [faer-042](faer-042-lu-perf-warn-row-major/) | Partial pivoting LU warns "generic strides" on contiguous row-major input | `faer` 0.24.4 | spurious `perf-warn` message | fix for tensor4all/faer#345 | not yet | 2026-11-09 | | [gemm-001](gemm-001-per-call-allocation/) | Every matrix product allocates on the heap | `gemm` 0.19.0 | heap allocation per call | not fixed (tensor4all/faer#56 planned) | not yet | 2026-10-30 | | [gemm-x86-001](gemm-x86-001-diag-negative-stride/) | A product with a diagonal gives a wrong value when the left matrix is stored backwards | `private-gemm-x86` 0.1.22 | wrong value | tensor4all/t4a-private-gemm-x86#5 (`84ca6db`) | not yet | 2026-10-29 | @@ -63,7 +64,8 @@ The program prints the expected and the actual value, and exits with status 1 wh The 39 entries that predate this week (`faer-001` to `faer-038`, plus `gemm-001`) were all re-run on 2026-11-09 on the aarch64 host where this list is recorded, and they behave as their sections state (on aarch64 `faer-001` and `faer-026` do not trigger, as their sections say); no newer release existed to raise a pin to, and `gemm`'s and `private-gemm-x86`'s `main` are their release commits, so only `faer` needed a build from `main`. `gemm-x86-001` needs an x86 CPU and does not build on aarch64; it was run on x86_64 on 2026-11-09, where it builds and exits 1 as its section states. The three entries added this week (`faer-039`, `faer-040`, `faer-041`) were run from their own directories on both hosts and then against `faer`'s `main` at `0ecd846` as a path dependency, where they exit 1 with the same numbers; the one row where the two hosts disagree is marked in the `faer-039` table. -That is 43 programs in total, and every one of them was executed on 2026-11-09 on a host where it builds and runs. +That is 44 programs in total, and every one of them was executed on 2026-11-09 on a host where it builds and runs. +The `perf-warn` entry (`faer-042`) was added on 2026-11-09 and run on its own, `cd upstream-reports/faer-042-* && cargo run --locked`, which exits 1 on its contiguous row-major input and 0 with `-- column-major`. The sweep of the 39 is `cd upstream-reports && for d in faer-0[01][0-9]-* faer-02[0-9]-* faer-03[0-8]-* gemm-001-*; do (cd "$d" && timeout 600 cargo run --locked); echo "$d exit=$?"; done`, which selects 39 directories. The sweep of the three is `cd upstream-reports && for d in faer-039-* faer-040-* faer-041-*; do (cd "$d" && timeout 600 cargo run --locked); echo "$d exit=$?"; done`, which selects 3, and the x86 entry is `cd upstream-reports/gemm-x86-001-* && cargo run --locked`. `faer`'s `main` is the 0.24.4 release plus eleven commits, and only the `gevd` scratch layout and conjugate-pair slots among them touch the code behind an entry: `faer-009`, re-run there, still fails. diff --git a/upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.lock b/upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.lock new file mode 100644 index 00000000..3f8f6ec2 --- /dev/null +++ b/upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.lock @@ -0,0 +1,686 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "defer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "930c7171c8df9fb1782bdf9b918ed9ed2d33d1d22300abb754f9085bc48bf8e8" + +[[package]] +name = "dyn-stack" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c4713e43e2886ba72b8271aa66c93d722116acf7a75555cce11dcde84388fe8" +dependencies = [ + "bytemuck", + "dyn-stack-macros", +] + +[[package]] +name = "dyn-stack-macros" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d926b4d407d372f141f93bb444696142c29d32962ccbd3531117cf3aa0bfa9" + +[[package]] +name = "enum-as-inner" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1e6a265c649f3f5979b601d26f1d05ada116434c87741c9493cb56218f76cbc" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35da53b5a021d2484a7cc49b2ac7f2d840f8236a286f84202369bd338d761ea" +dependencies = [ + "equator-macro 0.2.1", +] + +[[package]] +name = "equator" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02da895aab06bbebefb6b2595f6d637b18c9ff629b4cd840965bb3164e4194b0" +dependencies = [ + "equator-macro 0.6.0", +] + +[[package]] +name = "equator-macro" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf679796c0322556351f287a51b49e48f7c4986e727b5dd78c972d30e2e16cc" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "equator-macro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b14b339eb76d07f052cdbad76ca7c1310e56173a138095d3bf42a23c06ef5d8" + +[[package]] +name = "faer" +version = "0.24.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ab6df3dd147fe8d702a288b95bcd8fcc499ab572fc80da6828f60cd4d524d67" +dependencies = [ + "bytemuck", + "dyn-stack", + "equator 0.6.0", + "faer-traits", + "gemm", + "generativity", + "libm", + "log", + "nano-gemm", + "num-complex", + "num-traits", + "private-gemm-x86", + "pulp", + "reborrow", +] + +[[package]] +name = "faer-037-lu-perf-warn-row-major" +version = "0.0.0" +dependencies = [ + "faer", + "log", +] + +[[package]] +name = "faer-traits" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87d23ed7ab1f26c0cba0e5b9e061a796fbb7dc170fa8bee6970055a1308bb0f" +dependencies = [ + "bytemuck", + "dyn-stack", + "generativity", + "libm", + "num-complex", + "num-traits", + "pulp", + "qd", + "reborrow", +] + +[[package]] +name = "gemm" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa0673db364b12263d103b68337a68fbecc541d6f6b61ba72fe438654709eacb" +dependencies = [ + "dyn-stack", + "gemm-c32", + "gemm-c64", + "gemm-common", + "gemm-f16", + "gemm-f32", + "gemm-f64", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "086936dbdcb99e37aad81d320f98f670e53c1e55a98bee70573e83f95beb128c" +dependencies = [ + "dyn-stack", + "gemm-common", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-c64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c8aeeeec425959bda4d9827664029ba1501a90a0d1e6228e48bef741db3a3f" +dependencies = [ + "dyn-stack", + "gemm-common", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-common" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88027625910cc9b1085aaaa1c4bc46bb3a36aad323452b33c25b5e4e7c8e2a3e" +dependencies = [ + "bytemuck", + "dyn-stack", + "half", + "libm", + "num-complex", + "num-traits", + "once_cell", + "paste", + "pulp", + "raw-cpuid", + "seq-macro", + "sysctl", +] + +[[package]] +name = "gemm-f16" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3df7a55202e6cd6739d82ae3399c8e0c7e1402859b30e4cb780e61525d9486e" +dependencies = [ + "dyn-stack", + "gemm-common", + "gemm-f32", + "half", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f32" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02e0b8c9da1fbec6e3e3ab2ce6bc259ef18eb5f6f0d3e4edf54b75f9fd41a81c" +dependencies = [ + "dyn-stack", + "gemm-common", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "gemm-f64" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "056131e8f2a521bfab322f804ccd652520c79700d81209e9d9275bbdecaadc6a" +dependencies = [ + "dyn-stack", + "gemm-common", + "num-complex", + "num-traits", + "paste", + "raw-cpuid", + "seq-macro", +] + +[[package]] +name = "generativity" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c81fb5260e37854d09d5c87183309fd8c555b75289427884b25660bc87a85e" + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "bytemuck", + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "interpol" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb58032ba748f4010d15912a1855a8a0b1ba9eaad3395b0c171c09b3b356ae50" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "libc" +version = "0.2.191" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d15f86d12da44cd8d4ec3c73984537eb7f4cadcb9776a575a0a8a05ca11005c" + +[[package]] +name = "nano-gemm" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e04345dc84b498ff89fe0d38543d1f170da9e43a2c2bcee73a0f9069f72d081" +dependencies = [ + "equator 0.2.2", + "nano-gemm-c32", + "nano-gemm-c64", + "nano-gemm-codegen", + "nano-gemm-core", + "nano-gemm-f32", + "nano-gemm-f64", + "num-complex", +] + +[[package]] +name = "nano-gemm-c32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0775b1e2520e64deee8fc78b7732e3091fb7585017c0b0f9f4b451757bbbc562" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", + "num-complex", +] + +[[package]] +name = "nano-gemm-c64" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9af49a20d58816e6b5ee65f64142e50edb5eba152678d4bb7377fcbf63f8437a" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", + "num-complex", +] + +[[package]] +name = "nano-gemm-codegen" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6cc8d495c791627779477a2cf5df60049f5b165342610eb0d76bee5ff5c5d74c" + +[[package]] +name = "nano-gemm-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d998dfa644de87a0f8660e5ea511d7cb5c33b5a2d9847b7af57a2565105089f0" + +[[package]] +name = "nano-gemm-f32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d962e79bc8952e4ad21ca4845a21132540ed3f5e01184b2ff7f720e666523" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", +] + +[[package]] +name = "nano-gemm-f64" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9a513473dce7dc00c7e7c318481ca4494034e76997218d8dad51bd9f007a815" +dependencies = [ + "nano-gemm-codegen", + "nano-gemm-core", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "bytemuck", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "private-gemm-x86" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bba0a7831d1297712e7e7b33759b0c837941e79196dd05df506d5f36f7f78619" +dependencies = [ + "defer", + "interpol", + "num_cpus", + "raw-cpuid", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pulp" +version = "0.22.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046aa45b989642ec2e4717c8e72d677b13edd831a4d3b6cf37d9a3e54912496a" +dependencies = [ + "bytemuck", + "cfg-if", + "libm", + "num-complex", + "paste", + "pulp-wasm-simd-flag", + "raw-cpuid", + "reborrow", + "version_check", +] + +[[package]] +name = "pulp-wasm-simd-flag" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d8f70e07b9c3962945a74e59ca1c511bba65b6419468acc217c457d93f3c740" + +[[package]] +name = "qd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15f1304a5aecdcfe9ee72fbba90aa37b3aa067a69d14cb7f3d9deada0be7c07c" +dependencies = [ + "bytemuck", + "libm", + "num-traits", + "pulp", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "reborrow" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03251193000f4bd3b042892be858ee50e8b3719f2b08e5833ac4353724632430" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "seq-macro" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sysctl" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01198a2debb237c62b6826ec7081082d951f46dbb64b0e8c7649a452230d1dfc" +dependencies = [ + "bitflags", + "byteorder", + "enum-as-inner", + "libc", + "thiserror", + "walkdir", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] diff --git a/upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.toml b/upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.toml new file mode 100644 index 00000000..3a654c7b --- /dev/null +++ b/upstream-reports/faer-042-lu-perf-warn-row-major/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "faer-037-lu-perf-warn-row-major" +version = "0.0.0" +edition = "2021" +publish = false + +# Not part of the t4a-faer workspace: this reproduces the published crate on its own. +[workspace] + +[dependencies] +# local run used the 0.24.4 checkout at faer-benchmark/extern/upstream-faer +faer = { version = "=0.24.4", default-features = false, features = ["std", "linalg", "perf-warn"] } +log = "0.4" diff --git a/upstream-reports/faer-042-lu-perf-warn-row-major/README.md b/upstream-reports/faer-042-lu-perf-warn-row-major/README.md new file mode 100644 index 00000000..d73052bf --- /dev/null +++ b/upstream-reports/faer-042-lu-perf-warn-row-major/README.md @@ -0,0 +1,62 @@ +# faer-042: partial pivoting LU warns "generic strides" on contiguous row-major input + +**What happens:** with the `perf-warn` feature on, the first column-pivoted LU of a contiguous 64 x 64 row-major matrix logs `LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides.` at target `faer_perf`. The factorisation itself is correct; the message is the defect, and it names row-major as one of the two layouts it says it prefers. +**Which input:** any matrix whose `col_stride` is `1`, i.e. any contiguous row-major one, of any shape. The program uses a `64 x 64` `f64` matrix and passes the transpose view of a column-major `Mat`, so `row_stride = 64`, `col_stride = 1`. +**Which version:** `faer` 0.24.4, and upstream `main` at `0ecd846` (2026-10-22), where the predicate is unchanged. +**Added to this list:** 2026-11-09. + +## Reproduce + +```sh +cargo run # contiguous row-major: exits with status 1 +cargo run -- column-major # the same matrix in the other preferred layout: exits 0 +``` + +The program installs a small `log::Log` that counts records at target `faer_perf`, runs one partial-pivoting `lu_in_place`, and prints the layout, the two strides, and the expected and actual warning counts. +`__perf_warn!` is a process-wide "warn once" flag, so each run is one factorization: the row-major run is the default and the `column-major` run is the control, in its own process. + +| input | expected warnings | `faer` 0.24.4 | +|---|---|---| +| `64 x 64` contiguous row-major (`row_stride` 64, `col_stride` 1) | 0 | **1** | +| `64 x 64` contiguous column-major (`row_stride` 1, `col_stride` 64) | 0 | 0 | + +The column-major run not warning is what hid the defect: the predicate is only wrong for the other one of the two layouts its message recommends. + +## Cause + +```text +src/linalg/lu/partial_pivoting/factor.rs:245-253 (faer 0.24.4; main 0ecd846 has it at the same lines) + if (A.col_stride().unsigned_abs() == 1 + || A.row_stride().unsigned_abs() != 1) + && crate::__perf_warn!(LU_WARN) + { + log::warn!( + target : "faer_perf", + "LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides." + ); + } +``` + +The condition is `col_stride == 1 || row_stride != 1`, which is "anything that is not contiguous column-major": on a contiguous row-major matrix the first clause is true (`col_stride == 1`), and on any matrix that is not column-major the second one is. So the warning fires on every row-major input, which is one of the two layouts the message recommends. The predicate that matches the message is `row_stride != 1 && col_stride != 1` -- neither stride walks a line, so the input is neither layout. + +The full-pivoting sibling `src/linalg/lu/full_pivoting/factor.rs:477-485` carries the same condition -- the same predicate in another file, so it is noted here instead of getting its own entry. The fork fixes that copy in tensor4all/faer#342 and this one in tensor4all/faer#345. + +## How the fork fixed it + +One predicate, used by the warning and covered by a unit test: + +```text +faer/src/linalg/lu/partial_pivoting/factor.rs:633-640 (tensor4all/faer, #345) +#[cfg(any(feature = "perf-warn", test))] +#[inline] +pub(crate) fn has_generic_strides( + row_stride: isize, + col_stride: isize, +) -> bool { + row_stride.unsigned_abs() != 1 && col_stride.unsigned_abs() != 1 +} +``` + +`faer/src/linalg/lu/partial_pivoting/factor.rs:649-652` calls it from the `perf-warn` block, and +`faer/src/linalg/lu/partial_pivoting/factor.rs:762-773` asserts the four layouts: contiguous row-major and column-major and a transposed view are silent, and a matrix with neither stride equal to one warns. +The fork's fix is a warning-message fix only; the factorisation is untouched. diff --git a/upstream-reports/faer-042-lu-perf-warn-row-major/src/main.rs b/upstream-reports/faer-042-lu-perf-warn-row-major/src/main.rs new file mode 100644 index 00000000..c607b21c --- /dev/null +++ b/upstream-reports/faer-042-lu-perf-warn-row-major/src/main.rs @@ -0,0 +1,80 @@ +// Column-pivoted LU of a contiguous 64 x 64 row-major matrix, with `perf-warn` enabled. The +// predicate fires when `col_stride == 1`, which is what a contiguous row-major matrix has, so the +// call warns "Found matrix with generic strides" although its own message names row-major as one of +// the two layouts it prefers. A contiguous column-major input does not warn (`row_stride == 1` +// leaves both clauses false), which is the asymmetry. +// +// `Mat` is column-major, so its transpose view is the contiguous row-major input: +// `row_stride = n`, `col_stride = 1`. Pass `column-major` as the first argument for the control +// run, which is the same matrix in the other preferred layout and does not warn in either version. +use faer::dyn_stack::{MemBuffer, MemStack}; +use faer::linalg::lu::partial_pivoting::factor::{lu_in_place, lu_in_place_scratch}; +use faer::{Mat, Par}; +use std::sync::atomic::{AtomicUsize, Ordering}; + +static WARNINGS: AtomicUsize = AtomicUsize::new(0); + +struct Counter; + +impl log::Log for Counter { + fn enabled(&self, _: &log::Metadata) -> bool { + true + } + fn log(&self, record: &log::Record) { + if record.target() == "faer_perf" { + eprintln!("warning: {}", record.args()); + WARNINGS.fetch_add(1, Ordering::SeqCst); + } + } + fn flush(&self) {} +} + +static LOGGER: Counter = Counter; + +fn main() { + log::set_logger(&LOGGER).unwrap(); + log::set_max_level(log::LevelFilter::Warn); + + let n = 64; + let mut a = Mat::::from_fn(n, n, |i, j| { + if i == j { + 4.0 + } else { + 1.0 / (1 + i + j) as f64 + } + }); + let column_major = std::env::args().any(|arg| arg == "column-major"); + let mut a_view = if column_major { + a.as_mut() + } else { + a.transpose_mut() + }; + let (row_stride, col_stride) = (a_view.row_stride(), a_view.col_stride()); + + let mut perm = vec![0usize; n]; + let mut perm_inv = vec![0usize; n]; + let mut mem = MemBuffer::new(lu_in_place_scratch::( + n, + n, + Par::Seq, + Default::default(), + )); + let (info, _) = lu_in_place( + a_view.as_mut(), + &mut perm, + &mut perm_inv, + Par::Seq, + MemStack::new(&mut mem), + Default::default(), + ); + + let warnings = WARNINGS.load(Ordering::SeqCst); + let layout = if column_major { "column-major" } else { "row-major" }; + println!("input: {n} x {n} contiguous {layout} (row_stride = {row_stride}, col_stride = {col_stride})"); + println!("factorisation: partial pivoting, {} transpositions", info.transposition_count); + println!("expected `faer_perf` warnings: 0"); + println!("actual `faer_perf` warnings: {warnings}"); + if warnings != 0 { + std::process::exit(1); + } +} From 67f791c18eb0939983f824d8c3a967af1f8fd1c3 Mon Sep 17 00:00:00 2001 From: Hiroshi Shinaoka Date: Mon, 9 Nov 2026 23:36:54 +0900 Subject: [PATCH 2/4] test(lu): cover negative unit strides in the perf-warn predicate (#345) `has_generic_strides` reads `|row_stride| != 1 && |col_stride| != 1`, but replacing its body with `row_stride != 1 && col_stride != 1` kept all six cases added with it green: none is a stride of magnitude one on the axis the other one walks, so `unsigned_abs` was untested. Add `(64, -1)` and `(-1, 64)`; with the mutant the first fails (`! has_generic_strides(64, -1) = false`), with the real helper both pass. Drop the unmeasured claim that both preferred layouts factor at the same speed from the helper's doc comment. --- faer/src/linalg/lu/partial_pivoting/factor.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/faer/src/linalg/lu/partial_pivoting/factor.rs b/faer/src/linalg/lu/partial_pivoting/factor.rs index e6ceedf7..c6c138df 100644 --- a/faer/src/linalg/lu/partial_pivoting/factor.rs +++ b/faer/src/linalg/lu/partial_pivoting/factor.rs @@ -627,9 +627,8 @@ pub fn lu_in_place_scratch( StackReq::new::(Ord::min(nrows, ncols)) } /// `perf-warn`: whether neither stride walks a line, i.e. the input is neither -/// column-major (`row_stride` 1) nor row-major (`col_stride` 1). Both preferred -/// layouts factor at the same speed, so only a matrix that is neither warns; -/// the predicate this replaced warned on every row-major input. +/// column-major (`row_stride` 1) nor row-major (`col_stride` 1). The predicate +/// this replaced warned on every row-major input. #[cfg(any(feature = "perf-warn", test))] #[inline] pub(crate) fn has_generic_strides( @@ -768,6 +767,8 @@ mod tests { assert!(!has_generic_strides(1, 64)); // transposed view: one stride still walks a line assert!(has_generic_strides(3, 2)); // neither stride walks a line assert!(!has_generic_strides(-64, 1)); // reversed row-major + assert!(!has_generic_strides(64, -1)); // row-major, reversed columns: |col_stride| is 1 + assert!(!has_generic_strides(-1, 64)); // column-major, reversed rows: |row_stride| is 1 assert!(has_generic_strides(-2, -130)); } #[cfg(feature = "rayon")] From 5a192e9708db026c853881810dcd8c8bbee452a9 Mon Sep 17 00:00:00 2001 From: Hiroshi Shinaoka Date: Tue, 10 Nov 2026 00:27:57 +0900 Subject: [PATCH 3/4] test(lu): exercise the perf-warn call site from its own test binary (#345) The predicate test cannot see the wiring: restoring the old "not column-major" condition at the lu_in_place call site leaves it green. A unit test inside the crate cannot own the warn-once flag either, so add faer/tests/lu_perf_warn.rs, its own binary, with a capturing logger: contiguous row-major and column-major stay silent, a matrix with neither stride equal to one warns once, and a second such factor adds nothing. log is a dev-dependency so the test can install the logger. --- faer/Cargo.toml | 1 + faer/tests/lu_perf_warn.rs | 135 +++++++++++++++++++++++++++++++++++++ 2 files changed, 136 insertions(+) create mode 100644 faer/tests/lu_perf_warn.rs diff --git a/faer/Cargo.toml b/faer/Cargo.toml index bd42a176..79fe9285 100644 --- a/faer/Cargo.toml +++ b/faer/Cargo.toml @@ -133,6 +133,7 @@ npy = [ ] [dev-dependencies] +log = { version = "0.4.29", default-features = false, features = ["std"] } aligned-vec = "0.6.4" amd = "0.2.2" assert_approx_eq = "1.1.0" diff --git a/faer/tests/lu_perf_warn.rs b/faer/tests/lu_perf_warn.rs new file mode 100644 index 00000000..304d83fa --- /dev/null +++ b/faer/tests/lu_perf_warn.rs @@ -0,0 +1,135 @@ +#![cfg(feature = "perf-warn")] +//! The `perf-warn` call site of `lu::partial_pivoting`: `lu_in_place` warns once per process, +//! and only for a matrix whose strides are neither of the two layouts it recommends. +//! +//! The warn-once flag is a process-wide atomic, so a unit test next to the predicate cannot own +//! it: whichever other test trips it first decides. This file is its own integration-test binary, +//! which is the isolation the flag needs, and it drives the call site rather than the predicate +//! -- restoring the old "not column-major" condition at the call site keeps the predicate test +//! green and fails here. + +use std::sync::{Arc, Mutex, OnceLock}; + +use faer::dyn_stack::{MemBuffer, MemStack}; +use faer::linalg::lu::partial_pivoting::factor::{ + self as plu, PartialPivLuParams, +}; +use faer::{Auto, MatMut, Par}; + +/// The `faer_perf` messages this process produced. +struct Capture(Arc>>); + +impl log::Log for Capture { + fn enabled(&self, metadata: &log::Metadata<'_>) -> bool { + metadata.target() == "faer_perf" + } + + fn log(&self, record: &log::Record<'_>) { + if self.enabled(record.metadata()) { + self.0.lock().unwrap().push(record.args().to_string()); + } + } + + fn flush(&self) {} +} + +/// Install the only logger in this test binary, once. +fn messages() -> &'static Arc>> { + static RECORDS: OnceLock>>> = OnceLock::new(); + RECORDS.get_or_init(|| { + let records = Arc::new(Mutex::new(Vec::new())); + log::set_boxed_logger(Box::new(Capture(records.clone()))) + .expect("this binary installs the process logger once"); + log::set_max_level(log::LevelFilter::Warn); + records + }) +} + +/// Factor an `nrows x ncols` matrix held at the given strides. +fn factor( + data: &mut [f64], + nrows: usize, + ncols: usize, + row_stride: isize, + col_stride: isize, +) { + let n = Ord::min(nrows, ncols); + let mut perm = vec![0usize; n]; + let mut perm_inv = vec![0usize; n]; + let par = Par::Seq; + let params = >::auto(); + let mut mem = MemBuffer::new(plu::lu_in_place_scratch::( + nrows, + ncols, + par.shape(), + params.into(), + )); + // SAFETY: each caller passes a buffer that holds every index the strides reach. + let a = unsafe { + MatMut::from_raw_parts_mut( + data.as_mut_ptr(), + nrows, + ncols, + row_stride, + col_stride, + ) + }; + let _ = plu::lu_in_place( + a, + &mut perm, + &mut perm_inv, + par, + MemStack::new(&mut mem), + params.into(), + ); +} + +#[test] +fn the_warning_fires_once_and_only_for_generic_strides() { + let records = messages(); + records.lock().unwrap().clear(); + + // Contiguous row-major first, while the warn-once flag is still clear: this is the layout the + // old call-site condition warned on, and the regression this file exists for. + let mut row_major: Vec = (1..=9).map(|v| v as f64).collect(); + factor(&mut row_major, 3, 3, 3, 1); + assert_eq!( + records.lock().unwrap().len(), + 0, + "contiguous row-major must stay silent" + ); + + // Contiguous column-major. + let mut col_major = vec![1.0f64; 9]; + factor(&mut col_major, 3, 3, 1, 3); + assert_eq!( + records.lock().unwrap().len(), + 0, + "contiguous column-major must stay silent" + ); + + // Neither stride walks a line: a 3x3 view with row stride 2 and column stride 6. + let mut generic = vec![1.0f64; 17]; + factor(&mut generic, 3, 3, 2, 6); + { + let seen = records.lock().unwrap(); + assert_eq!( + seen.len(), + 1, + "a generic-strided input must warn exactly once: {seen:?}" + ); + assert!( + seen[0].contains("generic strides"), + "unexpected message: {:?}", + seen[0] + ); + } + + // Warn once per process: a second generic-strided factor adds nothing. + factor(&mut generic, 3, 3, 2, 6); + assert_eq!( + records.lock().unwrap().len(), + 1, + "the warning is emitted once per process" + ); +} From 1c880f5ca5f49a06e8150d67769532515ff8b957 Mon Sep 17 00:00:00 2001 From: Hiroshi Shinaoka Date: Tue, 10 Nov 2026 00:34:29 +0900 Subject: [PATCH 4/4] ci: run the perf-warn call-site test, and stop claiming a numerical result The integration test only compiles under perf-warn, which the test job does not enable, so the call-site regression would never run in CI. Add that invocation. The upstream report checks warning counts only, so its README no longer says the factorisation is correct. --- .github/workflows/ci.yml | 1 + upstream-reports/faer-042-lu-perf-warn-row-major/README.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 386e455e..a7bee5a9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,6 +28,7 @@ jobs: # handle, and the "the team is left again" affinity check. - run: cargo test -p faer-ffi -j4 - run: cargo test -p t4a-faer --no-default-features --features "std,linalg" --test lsmr_initial_residual --test generalized_schur_reorder --test generalized_schur_reorder_no_alloc --test public_qz_aed --test public_qz_scratch --test public_householder_upgrade + - run: cargo test -p t4a-faer --features perf-warn --test lu_perf_warn - run: python3 scripts/check-migration-examples.py - run: cargo build --examples -j8 - run: | diff --git a/upstream-reports/faer-042-lu-perf-warn-row-major/README.md b/upstream-reports/faer-042-lu-perf-warn-row-major/README.md index d73052bf..aca2ce1d 100644 --- a/upstream-reports/faer-042-lu-perf-warn-row-major/README.md +++ b/upstream-reports/faer-042-lu-perf-warn-row-major/README.md @@ -1,6 +1,6 @@ # faer-042: partial pivoting LU warns "generic strides" on contiguous row-major input -**What happens:** with the `perf-warn` feature on, the first column-pivoted LU of a contiguous 64 x 64 row-major matrix logs `LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides.` at target `faer_perf`. The factorisation itself is correct; the message is the defect, and it names row-major as one of the two layouts it says it prefers. +**What happens:** with the `perf-warn` feature on, the first column-pivoted LU of a contiguous 64 x 64 row-major matrix logs `LU with partial pivoting prefers column-major or row-major matrix. Found matrix with generic strides.` at target `faer_perf`. This report counts warnings only: it does not check the factors numerically. The message is the defect, and it names row-major as one of the two layouts it says it prefers. **Which input:** any matrix whose `col_stride` is `1`, i.e. any contiguous row-major one, of any shape. The program uses a `64 x 64` `f64` matrix and passes the transpose view of a column-major `Mat`, so `row_stride = 64`, `col_stride = 1`. **Which version:** `faer` 0.24.4, and upstream `main` at `0ecd846` (2026-10-22), where the predicate is unchanged. **Added to this list:** 2026-11-09.