From e5d043dbcf4f3e4683cb12232e818e793503d1ba Mon Sep 17 00:00:00 2001 From: Rasmus Munk Larsen Date: Mon, 21 Sep 2026 21:46:09 -0700 Subject: [PATCH 1/2] Benchmarks: Validate cancelling dot products using input magnitude --- benchmarks/comparison/CMakeLists.txt | 3 ++ benchmarks/comparison/bench_compare.h | 11 +++--- benchmarks/comparison/bench_dot_compare.cpp | 4 ++- benchmarks/comparison/test_validation.cpp | 40 +++++++++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 benchmarks/comparison/test_validation.cpp diff --git a/benchmarks/comparison/CMakeLists.txt b/benchmarks/comparison/CMakeLists.txt index ccc3fa8f5..b1a361672 100644 --- a/benchmarks/comparison/CMakeLists.txt +++ b/benchmarks/comparison/CMakeLists.txt @@ -184,6 +184,9 @@ endif() # --------------------------------------------------------------------------- add_custom_target(bench_comparison_all COMMENT "All comparison benchmark binaries") +eigen_add_benchmark(bench_comparison_validation "test_validation.cpp") +add_dependencies(bench_comparison_all bench_comparison_validation) +add_test(NAME bench_comparison_validation COMMAND bench_comparison_validation) if(CMAKE_VERSION VERSION_LESS 3.12) file(GLOB _comparison_sources "${CMAKE_CURRENT_SOURCE_DIR}/bench_*_compare.cpp") diff --git a/benchmarks/comparison/bench_compare.h b/benchmarks/comparison/bench_compare.h index 8c2612490..da6ac49fa 100644 --- a/benchmarks/comparison/bench_compare.h +++ b/benchmarks/comparison/bench_compare.h @@ -173,24 +173,25 @@ class ValidatedShapes { }; // One numerical policy for every comparison: relative to the larger of the two -// norms, scaled by sqrt(contraction length) because that is where the error -// accumulates. Non-finite values are rejected explicitly: every comparison +// norms or an optional input scale, scaled by sqrt(contraction length) because +// that is where the error accumulates. Non-finite values are rejected explicitly: every comparison // against NaN is false, but an infinite entry makes both the error and the bound // infinite and `inf <= inf` holds. template bool agreesWithEigen(const Eigen::MatrixBase& expected, const Eigen::MatrixBase& actual, - Eigen::Index contraction_length) { + Eigen::Index contraction_length, + typename Eigen::NumTraits::Real input_scale = 0) { using RealScalar = typename Eigen::NumTraits::Real; const RealScalar expected_norm = expected.norm(); const RealScalar actual_norm = actual.norm(); const RealScalar error = (actual - expected).norm(); if (!(Eigen::numext::isfinite)(expected_norm) || !(Eigen::numext::isfinite)(actual_norm) || - !(Eigen::numext::isfinite)(error)) { + !(Eigen::numext::isfinite)(error) || !(Eigen::numext::isfinite)(input_scale) || input_scale < RealScalar(0)) { return false; } const RealScalar tolerance = RealScalar(64) * Eigen::numext::sqrt(RealScalar(contraction_length)) * Eigen::NumTraits::epsilon(); - const RealScalar magnitude = Eigen::numext::maxi(expected_norm, actual_norm); + const RealScalar magnitude = Eigen::numext::maxi(input_scale, Eigen::numext::maxi(expected_norm, actual_norm)); return !!(error <= tolerance * magnitude); } diff --git a/benchmarks/comparison/bench_dot_compare.cpp b/benchmarks/comparison/bench_dot_compare.cpp index 23c1ce359..e6f9883f9 100644 --- a/benchmarks/comparison/bench_dot_compare.cpp +++ b/benchmarks/comparison/bench_dot_compare.cpp @@ -81,7 +81,9 @@ static void runDot(benchmark::State& state, Kernel kernel) { using Cell = Eigen::Matrix; const Cell expected = Cell::Constant(x.dot(y)); const Cell actual = Cell::Constant(kernel(x, y)); - if (!eigen_bench::agreesWithEigen(expected, actual, n)) { + // Dot-product roundoff scales with sum(abs(x_i * y_i)), including when x.dot(y) cancels to zero. + const Scalar input_scale = x.cwiseAbs().dot(y.cwiseAbs()); + if (!eigen_bench::agreesWithEigen(expected, actual, n, input_scale)) { state.SkipWithError("dot result disagrees with Eigen at n:" + std::to_string(n)); return; } diff --git a/benchmarks/comparison/test_validation.cpp b/benchmarks/comparison/test_validation.cpp new file mode 100644 index 000000000..89cc934c2 --- /dev/null +++ b/benchmarks/comparison/test_validation.cpp @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: The Eigen Authors +// SPDX-License-Identifier: MPL-2.0 + +#include +#include +#include + +#include "bench_compare.h" + +template +bool checkValidation() { + using Cell = Eigen::Matrix; + const Scalar epsilon = Eigen::NumTraits::epsilon(); + const Scalar infinity = std::numeric_limits::infinity(); + const Scalar nan = std::numeric_limits::quiet_NaN(); + const Cell zero = Cell::Zero(); + const Cell roundoff = Cell::Constant(epsilon); + const Cell wrong = Cell::Constant(Scalar(0.01)); + using eigen_bench::agreesWithEigen; + return !agreesWithEigen(zero, roundoff, 2) && agreesWithEigen(zero, roundoff, 2, Scalar(1)) && + !agreesWithEigen(zero, wrong, 2, Scalar(1)) && agreesWithEigen(zero, zero, 2, Scalar(0)) && + !agreesWithEigen(zero, Cell::Constant(infinity), 2, Scalar(1)) && + !agreesWithEigen(Cell::Constant(nan), zero, 2, Scalar(1)) && !agreesWithEigen(zero, zero, 2, infinity) && + !agreesWithEigen(zero, zero, 2, nan) && !agreesWithEigen(zero, zero, 2, Scalar(-1)); +} + +int main() { + using Cell = Eigen::Matrix; + // Observed n=16 cancellation case. MPFR (256 bits): -0.00144387894118835902190767228603363037109375. + const Cell eigen = Cell::Constant(-0.0014439225196838379f); + const Cell openblas = Cell::Constant(-0.0014438522048294544f); + const float input_scale = 3.5528910087264762f; + const bool cancellation = !eigen_bench::agreesWithEigen(eigen, openblas, 16) && + eigen_bench::agreesWithEigen(eigen, openblas, 16, input_scale); + if (!checkValidation() || !checkValidation() || !cancellation) { + std::cerr << "Comparison validation failed its cancellation, incorrect-result, or non-finite checks\n"; + return 1; + } + return 0; +} -- 2.54.0 (Apple Git-157) From 4e0c35ecbfcc0e0039d4b77d6bfca4ab72ad1da7 Mon Sep 17 00:00:00 2001 From: Rasmus Munk Larsen Date: Mon, 21 Sep 2026 22:06:32 -0700 Subject: [PATCH 2/2] Benchmarks: Keep input-scaled validation independent of length --- benchmarks/comparison/bench_compare.h | 14 +++++++------- benchmarks/comparison/test_validation.cpp | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/benchmarks/comparison/bench_compare.h b/benchmarks/comparison/bench_compare.h index da6ac49fa..a401dc779 100644 --- a/benchmarks/comparison/bench_compare.h +++ b/benchmarks/comparison/bench_compare.h @@ -172,11 +172,10 @@ class ValidatedShapes { std::set set_; }; -// One numerical policy for every comparison: relative to the larger of the two -// norms or an optional input scale, scaled by sqrt(contraction length) because -// that is where the error accumulates. Non-finite values are rejected explicitly: every comparison -// against NaN is false, but an infinite entry makes both the error and the bound -// infinite and `inf <= inf` holds. +// Allow 64*eps*max(sqrt(contraction_length)*output_norm, input_scale). +// An input scale handles cancellation without scaling a sum of absolute products +// by sqrt(contraction_length) a second time. Reject non-finite values explicitly: +// an infinite error and allowance would otherwise satisfy inf <= inf. template bool agreesWithEigen(const Eigen::MatrixBase& expected, const Eigen::MatrixBase& actual, Eigen::Index contraction_length, @@ -191,8 +190,9 @@ bool agreesWithEigen(const Eigen::MatrixBase& expected, const Eigen::Ma } const RealScalar tolerance = RealScalar(64) * Eigen::numext::sqrt(RealScalar(contraction_length)) * Eigen::NumTraits::epsilon(); - const RealScalar magnitude = Eigen::numext::maxi(input_scale, Eigen::numext::maxi(expected_norm, actual_norm)); - return !!(error <= tolerance * magnitude); + const RealScalar magnitude = Eigen::numext::maxi(expected_norm, actual_norm); + const RealScalar input_allowance = RealScalar(64) * Eigen::NumTraits::epsilon() * input_scale; + return !!(error <= Eigen::numext::maxi(tolerance * magnitude, input_allowance)); } } // namespace eigen_bench diff --git a/benchmarks/comparison/test_validation.cpp b/benchmarks/comparison/test_validation.cpp index 89cc934c2..cf9867158 100644 --- a/benchmarks/comparison/test_validation.cpp +++ b/benchmarks/comparison/test_validation.cpp @@ -16,12 +16,16 @@ bool checkValidation() { const Cell zero = Cell::Zero(); const Cell roundoff = Cell::Constant(epsilon); const Cell wrong = Cell::Constant(Scalar(0.01)); + const Cell one = Cell::Ones(); + // [b, 1, -b].dot([1, 1, 1]), b=1/(512*eps), is exactly 1 even after zero padding. + const Scalar large_scale = Scalar(1) / (Scalar(256) * epsilon) + Scalar(1); using eigen_bench::agreesWithEigen; return !agreesWithEigen(zero, roundoff, 2) && agreesWithEigen(zero, roundoff, 2, Scalar(1)) && !agreesWithEigen(zero, wrong, 2, Scalar(1)) && agreesWithEigen(zero, zero, 2, Scalar(0)) && !agreesWithEigen(zero, Cell::Constant(infinity), 2, Scalar(1)) && !agreesWithEigen(Cell::Constant(nan), zero, 2, Scalar(1)) && !agreesWithEigen(zero, zero, 2, infinity) && - !agreesWithEigen(zero, zero, 2, nan) && !agreesWithEigen(zero, zero, 2, Scalar(-1)); + !agreesWithEigen(zero, zero, 2, nan) && !agreesWithEigen(zero, zero, 2, Scalar(-1)) && + !agreesWithEigen(one, zero, Eigen::Index(1) << 20, large_scale); } int main() { -- 2.54.0 (Apple Git-157)