Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
1 change: 1 addition & 0 deletions faer/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
28 changes: 26 additions & 2 deletions faer/src/linalg/lu/partial_pivoting/factor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -626,6 +626,17 @@ pub fn lu_in_place_scratch<I: Index, T: ComplexField>(
_ = params;
StackReq::new::<I>(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). 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],
Expand All @@ -637,8 +648,7 @@ pub fn lu_in_place<'out, I: Index, T: ComplexField>(
let _ = &params;
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!(
Expand Down Expand Up @@ -747,6 +757,20 @@ 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(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")]
#[test]
fn region_rules_follow_the_serial_time() {
Expand Down
135 changes: 135 additions & 0 deletions faer/tests/lu_perf_warn.rs
Original file line number Diff line number Diff line change
@@ -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<Mutex<Vec<String>>>);

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<Mutex<Vec<String>>> {
static RECORDS: OnceLock<Arc<Mutex<Vec<String>>>> = 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 = <PartialPivLuParams as Auto<f64>>::auto();
let mut mem = MemBuffer::new(plu::lu_in_place_scratch::<usize, f64>(
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<f64> = (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"
);
}
4 changes: 3 additions & 1 deletion upstream-reports/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,16 @@ 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 |

"Fixed upstream" was checked against each crate's upstream `main` and its latest release on 2026-11-09: `faer` at `0ecd846` (2026-10-22) and its 0.24.4 release, `gemm` at `86102c5`, which is its 0.19.0 release, `private-gemm-x86` at `71872b9`, which is its 0.1.22 release.
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.
Expand Down
Loading
Loading