Core: Stream and vectorize the self-adjoint 1-norm libeigen/eigen!3060
diff --git a/Eigen/src/Core/SelfAdjointView.h b/Eigen/src/Core/SelfAdjointView.h index b9e5e20..439e19c 100644 --- a/Eigen/src/Core/SelfAdjointView.h +++ b/Eigen/src/Core/SelfAdjointView.h
@@ -33,8 +33,224 @@ */ namespace internal { + +// Column step of the self-adjoint 1-norm, on two columns sharing a range of rows: sums[i] += +// |m(i, j0)| + |m(i, j1)|, and each column's own sum of those rows goes to sums[j0] and sums[j1]. +// Walking two columns at once halves the traffic on sums, and one packet pass does everything, so +// short columns pay no per-expression setup. +// +// Real scalars use pabs. Complex ones have no packet abs, so |z| = sqrt(re^2 + im^2) is computed +// on the real lanes of the complex packet. That leaves |z|^2 in both lanes of each slot, so one +// square root serves both columns: even lanes from j0 and odd lanes from j1 give |u0| |v0| |u1| +// |v1| ..., whose sum with its flip is the update of sums, and whose reduction as a complex packet +// is (sum |u|, sum |v|). The accumulator has the matrix's scalar type and only its real parts are +// read, so what lands in the imaginary lanes is harmless. Squaring overflows above sqrt(max) and +// loses precision below sqrt(min), so the pass also records the largest component it has seen and +// the caller recomputes the norm through numext::abs when that is out of range. The record is +// exact and the range test finite, so neither depends on infinities surviving fast-math. +template <typename Scalar_> +struct selfadjoint_l1norm_real_lanes { + using Scalar = Scalar_; + using Real = Scalar_; + using Packet = typename packet_traits<Scalar>::type; + using RPacket = Packet; + static constexpr Index PacketSize = unpacket_traits<Packet>::size; + // Up to this size the per-column form (mirrored term read as a row) beats the column pass, whose + // accumulator costs more to set up than these columns cost to read. + static constexpr Index PerColumnUpTo = 4; + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE RPacket lanes(const Packet& p) { return p; } + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real abs(const Scalar& x) { return numext::abs(x); } + // Nothing to record: pabs is exact. + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real component(const Scalar&) { return Real(0); } + static EIGEN_DEVICE_FUNC bool inRange(Real, Index) { return true; } + + // The running sums of a pass over two columns. + struct Pass { + RPacket acc0 = pzero(RPacket()); + RPacket acc1 = pzero(RPacket()); + template <typename SumsEvaluator> + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void step(const RPacket& u, const RPacket& v, SumsEvaluator& s, Index i) { + RPacket a = pabs(u); + RPacket b = pabs(v); + acc0 = padd(acc0, a); + acc1 = padd(acc1, b); + s.template writePacket<Unaligned>(i, padd(s.template packet<Unaligned, Packet>(i), padd(a, b))); + } + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real sum0() const { return predux(acc0); } + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real sum1() const { return predux(acc1); } + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real peak() const { return Real(0); } + }; +}; + +template <typename T> +struct selfadjoint_l1norm_complex_lanes { + using Scalar = std::complex<T>; + using Real = T; + using Packet = typename packet_traits<Scalar>::type; + using RPacket = typename unpacket_traits<Packet>::as_real; + static constexpr Index PacketSize = unpacket_traits<Packet>::size; + static constexpr Index PerColumnUpTo = 0; + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE RPacket lanes(const Packet& p) { return p.v; } + // Same formula as the packets, for the diagonal and the tails: hypot costs more than the packets + // spend on the rest of a short column. + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real abs(const Scalar& z) { return numext::sqrt(numext::abs2(z)); } + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real component(const Scalar& z) { + return numext::maxi(numext::abs(numext::real(z)), numext::abs(numext::imag(z))); + } + // Components this large overflow when squared, and below the lower bound the squares lose + // precision the sum of n of them cannot hide. + static EIGEN_DEVICE_FUNC bool inRange(Real peak, Index n) { + Real tiny = Real(n) * numext::sqrt((std::numeric_limits<Real>::min)()) / NumTraits<Real>::epsilon(); + Real huge = numext::sqrt(NumTraits<Real>::highest()) / Real(2); + return peak > tiny && peak < huge; + } + + struct Pass { + RPacket acc = pzero(RPacket()); // |u| in the even lanes, |v| in the odd ones + RPacket peak_ = pzero(RPacket()); // the largest component seen + template <typename SumsEvaluator> + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE void step(const RPacket& u, const RPacket& v, SumsEvaluator& s, Index i) { + peak_ = pmax(peak_, pmax(pabs(u), pabs(v))); + RPacket r = psqrt(pselect(peven_mask(u), abs2(u), abs2(v))); // |u0| |v0| |u1| |v1| ... + acc = padd(acc, r); + s.template writePacket<Unaligned>(i, + Packet(padd(lanes(s.template packet<Unaligned, Packet>(i)), padd(r, flip(r))))); + } + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real sum0() const { return numext::real(predux(Packet(acc))); } + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real sum1() const { return numext::imag(predux(Packet(acc))); } + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real peak() const { return predux_max(peak_); } + }; + + private: + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE RPacket flip(const RPacket& r) { return pcplxflip(Packet(r)).v; } + // |z|^2 in both lanes of its slot. + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE RPacket abs2(const RPacket& v) { + RPacket s = pmul(v, v); + return padd(s, flip(s)); + } +}; + +// The pass over two columns: packets over the shared rows, coefficients for the tail. An instance +// remembers the largest component it has seen, for inRange(). +template <typename Lanes> +struct selfadjoint_l1norm_packet_impl : Lanes { + using Lanes::PacketSize; + using typename Lanes::Packet; + using typename Lanes::Real; + using typename Lanes::RPacket; + using typename Lanes::Scalar; + + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real abs(const Scalar& x) { + m_peak = numext::maxi(m_peak, Lanes::component(x)); + return Lanes::abs(x); + } + EIGEN_DEVICE_FUNC bool inRange(Index n) const { return Lanes::inRange(m_peak, n); } + + template <typename SumsDerived, typename Derived> + EIGEN_DEVICE_FUNC void accumulate(DenseBase<SumsDerived>& sums, const DenseBase<Derived>& m, Index j0, Index j1, + Index begin, Index end) { + accumulateCast(sums, j0, j1, begin, m.col(j0).segment(begin, end - begin).template cast<Scalar>(), + m.col(j1).segment(begin, end - begin).template cast<Scalar>()); + } + + private: + Real m_peak = Real(0); + + template <typename SumsDerived, typename Derived0, typename Derived1> + EIGEN_DEVICE_FUNC void accumulateCast(DenseBase<SumsDerived>& sums, Index j0, Index j1, Index begin, + const DenseBase<Derived0>& x0, const DenseBase<Derived1>& x1) { + using SumsEvaluator = evaluator<SumsDerived>; + using Evaluator0 = evaluator<Derived0>; + using Evaluator1 = evaluator<Derived1>; + constexpr int Needed = PacketAccessBit | LinearAccessBit; + constexpr bool Vectorize = (SumsEvaluator::Flags & Needed) == Needed && (Evaluator0::Flags & Needed) == Needed && + (Evaluator1::Flags & Needed) == Needed; + SumsEvaluator s(sums.derived()); + accumulate(s, j0, j1, begin, Evaluator0(x0.derived()), Evaluator1(x1.derived()), x0.size(), + bool_constant<Vectorize>()); + } + template <typename Evaluator> + static EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE RPacket load(const Evaluator& x, Index i) { + return Lanes::lanes(x.template packet<Unaligned, Packet>(i)); + } + template <typename SumsEvaluator, typename Evaluator0, typename Evaluator1> + EIGEN_DEVICE_FUNC void accumulate(SumsEvaluator& s, Index j0, Index j1, Index begin, const Evaluator0& x0, + const Evaluator1& x1, Index from, Index to) { + Real sum0 = Real(0); + Real sum1 = Real(0); + for (Index i = from; i < to; ++i) { + Real a = abs(x0.coeff(i)); + Real b = abs(x1.coeff(i)); + s.coeffRef(begin + i) += a + b; + sum0 += a; + sum1 += b; + } + s.coeffRef(j0) += sum0; + s.coeffRef(j1) += sum1; + } + template <typename SumsEvaluator, typename Evaluator0, typename Evaluator1> + EIGEN_DEVICE_FUNC void accumulate(SumsEvaluator& s, Index j0, Index j1, Index begin, const Evaluator0& x0, + const Evaluator1& x1, Index n, std::false_type) { + accumulate(s, j0, j1, begin, x0, x1, Index(0), n); + } + template <typename SumsEvaluator, typename Evaluator0, typename Evaluator1> + EIGEN_DEVICE_FUNC void accumulate(SumsEvaluator& s, Index j0, Index j1, Index begin, const Evaluator0& x0, + const Evaluator1& x1, Index n, std::true_type) { + if (n < PacketSize) return accumulate(s, j0, j1, begin, x0, x1, Index(0), n); + typename Lanes::Pass pass; + Index i = 0; + for (; i + PacketSize <= n; i += PacketSize) pass.step(load(x0, i), load(x1, i), s, begin + i); + accumulate(s, j0, j1, begin, x0, x1, i, n); + s.coeffRef(j0) += pass.sum0(); + s.coeffRef(j1) += pass.sum1(); + m_peak = numext::maxi(m_peak, pass.peak()); + } +}; + +// Coefficient fallback: custom complex types, or complex packets without a plain real view. +template <typename Scalar_, typename Enable = void> +struct selfadjoint_l1norm_impl { + using Scalar = Scalar_; + using Real = typename NumTraits<Scalar>::Real; + static constexpr Index PerColumnUpTo = 16; + EIGEN_DEVICE_FUNC EIGEN_STRONG_INLINE Real abs(const Scalar& x) const { return numext::abs(x); } + EIGEN_DEVICE_FUNC bool inRange(Index) const { return true; } + template <typename SumsDerived, typename Derived> + EIGEN_DEVICE_FUNC void accumulate(DenseBase<SumsDerived>& sums, const DenseBase<Derived>& m, Index j0, Index j1, + Index begin, Index end) const { + Real sum0 = Real(0); + Real sum1 = Real(0); + for (Index i = begin; i < end; ++i) { + Real a = numext::abs(m.coeff(i, j0)); + Real b = numext::abs(m.coeff(i, j1)); + sums.coeffRef(i) += Scalar(a + b); + sum0 += a; + sum1 += b; + } + sums.coeffRef(j0) += Scalar(sum0); + sums.coeffRef(j1) += Scalar(sum1); + } +}; +// half and bfloat16 accumulate in float, as stableNorm does. +template <typename Scalar> +struct selfadjoint_l1norm_impl<Scalar, std::enable_if_t<!NumTraits<Scalar>::IsComplex>> + : selfadjoint_l1norm_packet_impl<selfadjoint_l1norm_real_lanes<typename stable_norm_accumulator<Scalar>::type>> {}; +// The real view must lay the components out one per lane: Z13 stores four floats in two double +// packets, which the lane masks do not describe. +template <typename Packet, typename Enable = void> +struct selfadjoint_l1norm_plain_real_view : std::false_type {}; +template <typename Packet> +struct selfadjoint_l1norm_plain_real_view<Packet, void_t<typename unpacket_traits<Packet>::as_real>> + : bool_constant<sizeof(typename unpacket_traits<Packet>::as_real) == + unpacket_traits<Packet>::size * sizeof(typename unpacket_traits<Packet>::type)> {}; +template <typename T> +struct selfadjoint_l1norm_impl< + std::complex<T>, + std::enable_if_t<selfadjoint_l1norm_plain_real_view<typename packet_traits<std::complex<T>>::type>::value>> + : selfadjoint_l1norm_packet_impl<selfadjoint_l1norm_complex_lanes<T>> {}; + template <typename MatrixType, unsigned int UpLo> -struct traits<SelfAdjointView<MatrixType, UpLo> > : traits<MatrixType> { +struct traits<SelfAdjointView<MatrixType, UpLo>> : traits<MatrixType> { using MatrixTypeNested = typename ref_selector<MatrixType>::non_const_type; using MatrixTypeNestedCleaned = remove_all_t<MatrixTypeNested>; using ExpressionType = MatrixType; @@ -217,66 +433,81 @@ */ EIGEN_DEVICE_FUNC RealScalar l1Norm() const { #ifdef EIGEN_GPU_COMPILE_PHASE - // The panel accumulator below is per-thread local storage on a device, so it would cost every - // kernel instantiating this kPanelSize scalars of stack and the registers to address them. + // No per-thread accumulator on a device. return l1NormPerColumn(); #else - // For a self-adjoint matrix |a_ij| = |a_ji|, so the stored triangle of a row-major matrix is - // the transposed, column-major, complementary one and yields the same norm read the fast way. + if (m_matrix.rows() <= L1NormImpl::PerColumnUpTo) return l1NormPerColumn(); + // The stored triangle of a row-major matrix is the complementary triangle of its column-major + // transpose, which has the same norm. EIGEN_IF_CONSTEXPR (bool(MatrixType::IsRowMajor)) { - return l1NormColumnwise<TransposeMode>(m_matrix.transpose()); + return l1NormStreaming<TransposeMode>(m_matrix.transpose()); } else { - return l1NormColumnwise<UpLo>(m_matrix); + return l1NormStreaming<UpLo>(m_matrix); } #endif } private: - // Reading the mirrored term of column j as a row of the stored triangle costs a stride-n - // traversal of a column-major matrix. Instead accumulate column sums a panel at a time: - // |a_ij| from a column left of the panel is added to the sum of column i, which walks that - // column. Only the panel's diagonal block keeps the row traversal, where it is cache resident, - // and a panel-sized accumulator stays a stack object. + using L1NormImpl = internal::selfadjoint_l1norm_impl<Scalar>; + // float for half and bfloat16, Scalar otherwise. + using L1NormScalar = typename L1NormImpl::Scalar; + using L1NormAccumulator = typename L1NormImpl::Real; + + // Each column is read once, top to bottom, two at a time: |a_ij| goes to column j's sum and, as + // the mirrored a_ji, to sums[i]. Lower walks the columns forward and Upper backward so that + // sums[j] is complete when column j is reached. Of a pair (j0, j1) only j0's element in row j1 + // lies outside the rows the two share. template <int Mode, typename Mat> - EIGEN_DEVICE_FUNC static RealScalar l1NormColumnwise(const Mat& m) { - static constexpr int kPanelSize = 64; - RealScalar norm = RealScalar(0); + RealScalar l1NormStreaming(const Mat& m) const { const Index n = m.rows(); - Matrix<RealScalar, kPanelSize, 1> sums; - for (Index p = 0; p < n; p += kPanelSize) { - const Index len = numext::mini(Index(kPanelSize), n - p); - EIGEN_IF_CONSTEXPR (Mode == Lower) { - for (Index j = 0; j < len; ++j) - sums.coeffRef(j) = - m.col(p + j).tail(n - p - j).template lpNorm<1>() + m.row(p + j).segment(p, j).template lpNorm<1>(); - for (Index j = 0; j < p; ++j) sums.head(len) += m.col(j).segment(p, len).cwiseAbs(); - } else { - for (Index j = 0; j < len; ++j) - sums.coeffRef(j) = m.col(p + j).head(p + j + 1).template lpNorm<1>() + - m.row(p + j).segment(p + j + 1, len - j - 1).template lpNorm<1>(); - for (Index j = p + len; j < n; ++j) sums.head(len) += m.col(j).segment(p, len).cwiseAbs(); - } - norm = numext::maxi(norm, sums.head(len).maxCoeff()); + // The accumulator lives in the object for bounded sizes and on the stack otherwise, so that + // neither fixed-size nor preallocated dynamic-size decompositions allocate. + internal::gemv_static_vector_if<L1NormScalar, Mat::RowsAtCompileTime, Mat::MaxRowsAtCompileTime, true> static_sums; + ei_declare_aligned_stack_constructed_variable(L1NormScalar, sums_data, n, static_sums.data()); + Map<Matrix<L1NormScalar, Dynamic, 1>> sums(sums_data, n); + sums.setZero(); + L1NormImpl impl; + L1NormAccumulator norm = L1NormAccumulator(0); + Index k = 0; + for (; k + 1 < n; k += 2) { + Index j0 = Mode == Lower ? k : n - 1 - k; + Index j1 = Mode == Lower ? j0 + 1 : j0 - 1; + Index rowBegin = Mode == Lower ? j1 + 1 : 0; + Index rowEnd = Mode == Lower ? n : j1; + impl.accumulate(sums, m, j0, j1, rowBegin, rowEnd); + // The element of j0 in row j1 lies outside the shared rows: it counts for both columns. + L1NormAccumulator boundary = impl.abs(m.coeff(j1, j0)); + // Totals are materialized so that maxi compares two accumulators (an integer sum promotes, + // an autodiff sum is an expression). + L1NormAccumulator col0 = numext::real(sums.coeff(j0)) + impl.abs(m.coeff(j0, j0)) + boundary; + L1NormAccumulator col1 = numext::real(sums.coeff(j1)) + impl.abs(m.coeff(j1, j1)) + boundary; + norm = numext::maxi(norm, col0); + norm = numext::maxi(norm, col1); } - return norm; + if (k < n) { + Index j = Mode == Lower ? k : 0; + L1NormAccumulator col = numext::real(sums.coeff(j)) + impl.abs(m.coeff(j, j)); + norm = numext::maxi(norm, col); + } + return impl.inRange(n) ? RealScalar(norm) : l1NormPerColumn(); } - // Workspace-free form, one column sum at a time; the mirrored term is read as a row. + // One column at a time, the mirrored term read as a row; no workspace. EIGEN_DEVICE_FUNC RealScalar l1NormPerColumn() const { - RealScalar norm = RealScalar(0); + L1NormAccumulator norm = L1NormAccumulator(0); const Index n = m_matrix.rows(); for (Index col = 0; col < n; ++col) { - RealScalar abs_col_sum; + L1NormAccumulator abs_col_sum; EIGEN_IF_CONSTEXPR (UpLo == Lower) { - abs_col_sum = - m_matrix.col(col).tail(n - col).template lpNorm<1>() + m_matrix.row(col).head(col).template lpNorm<1>(); + abs_col_sum = m_matrix.col(col).tail(n - col).template cast<L1NormScalar>().template lpNorm<1>() + + m_matrix.row(col).head(col).template cast<L1NormScalar>().template lpNorm<1>(); } else { - abs_col_sum = - m_matrix.col(col).head(col).template lpNorm<1>() + m_matrix.row(col).tail(n - col).template lpNorm<1>(); + abs_col_sum = m_matrix.col(col).head(col).template cast<L1NormScalar>().template lpNorm<1>() + + m_matrix.row(col).tail(n - col).template cast<L1NormScalar>().template lpNorm<1>(); } norm = numext::maxi(norm, abs_col_sum); } - return norm; + return RealScalar(norm); } public:
diff --git a/contrib/test/autodiff.cpp b/contrib/test/autodiff.cpp index 7769154..582fcfd 100644 --- a/contrib/test/autodiff.cpp +++ b/contrib/test/autodiff.cpp
@@ -332,6 +332,19 @@ return (y1 + y2 + y3).value(); } +// LLT stores the 1-norm of its input for rcond(); the sum of AutoDiffScalars is an expression, so +// the norm must materialize its column totals before comparing them. +void test_autodiff_selfadjoint_l1norm() { + typedef AutoDiffScalar<Vector2d> AD; + Matrix<AD, 8, 8> m = Matrix<AD, 8, 8>::Identity(); + m(3, 3) = AD(2, Vector2d(1, 0)); + AD norm = m.selfadjointView<Lower>().l1Norm(); + VERIFY_IS_EQUAL(norm.value(), 2.0); + VERIFY_IS_EQUAL(norm.derivatives()(0), 1.0); + LLT<Matrix<AD, 8, 8>> llt(m); + VERIFY(llt.info() == Success); +} + EIGEN_DECLARE_TEST(autodiff) { for (int i = 0; i < g_repeat; i++) { CALL_SUBTEST_1(test_autodiff_scalar<1>()); @@ -345,4 +358,5 @@ CALL_SUBTEST_5(bug_1260()); CALL_SUBTEST_5(bug_1261()); CALL_SUBTEST_5(bug_1281()); + CALL_SUBTEST_5(test_autodiff_selfadjoint_l1norm()); }
diff --git a/test/cholesky.cpp b/test/cholesky.cpp index 856ffa5..802ceeb 100644 --- a/test/cholesky.cpp +++ b/test/cholesky.cpp
@@ -9,6 +9,7 @@ // SPDX-License-Identifier: MPL-2.0 #define TEST_ENABLE_TEMPORARY_TRACKING +#define EIGEN_RUNTIME_NO_MALLOC #include "main.h" #include <Eigen/Cholesky> @@ -709,7 +710,7 @@ const RealScalar absdet = d.prod(); const RealScalar logabsdet = d.array().log().sum(); - const MatrixType spd = q * d.template cast<Scalar>().asDiagonal() * q.adjoint(); + MatrixType spd = q * d.template cast<Scalar>().asDiagonal() * q.adjoint(); LLT<MatrixType, Lower> lltlo(spd); VERIFY(lltlo.info() == Success); @@ -911,10 +912,32 @@ } } +// Preallocated decompositions of dynamic-size matrices stay allocation-free, including the 1-norm +// they take for rcond(), whose workspace comes from the stack. +template <typename Scalar> +void cholesky_dynamic_preallocated_no_malloc() { + typedef Matrix<Scalar, Dynamic, Dynamic> MatrixType; + Index size = 8; + MatrixType A = MatrixType::Random(size, size); + MatrixType spd = A * A.adjoint() + MatrixType::Identity(size, size) * Scalar(size); + LLT<MatrixType> llt(size); + LDLT<MatrixType> ldlt(size); + internal::set_is_malloc_allowed(false); + llt.compute(spd); + VERIFY_IS_EQUAL(llt.info(), Success); + ldlt.compute(spd); + VERIFY_IS_EQUAL(ldlt.info(), Success); + VERIFY(spd.template selfadjointView<Lower>().l1Norm() > 0); + internal::set_is_malloc_allowed(true); + VERIFY(llt.rcond() > 0); +} + EIGEN_DECLARE_TEST(cholesky) { int s = 0; for (int i = 0; i < g_repeat; i++) { CALL_SUBTEST_1(cholesky(Matrix<double, 1, 1>())); + CALL_SUBTEST_1(cholesky_dynamic_preallocated_no_malloc<double>()); + CALL_SUBTEST_1(cholesky_dynamic_preallocated_no_malloc<std::complex<double> >()); CALL_SUBTEST_3(cholesky(Matrix2d())); CALL_SUBTEST_3(cholesky_bug241(Matrix2d())); CALL_SUBTEST_3(cholesky_definiteness(Matrix2d()));
diff --git a/test/fastmath.cpp b/test/fastmath.cpp index 9056b72..98fcbea 100644 --- a/test/fastmath.cpp +++ b/test/fastmath.cpp
@@ -301,12 +301,31 @@ VERIFY_IS_APPROX(mat, q * r); } +// The complex self-adjoint 1-norm takes sqrt(re^2 + im^2) in packets and falls back to the scalar +// abs when a component is too large to square. The decision must not depend on an infinity or +// NaN surviving fast-math: an approximate packet sqrt can turn the overflow into a NaN that a +// maximum then discards. Large entries both on the diagonal and inside a packet of a column. +template <typename RealScalar> +void check_complex_selfadjoint_l1norm() { + typedef std::complex<RealScalar> Scalar; + RealScalar big = numext::sqrt(NumTraits<RealScalar>::highest()) * RealScalar(1e3); + Matrix<Scalar, 8, 8> m = Matrix<Scalar, 8, 8>::Identity() * big; + VERIFY_IS_APPROX(m.template selfadjointView<Lower>().l1Norm(), big); + VERIFY_IS_APPROX(m.template selfadjointView<Upper>().l1Norm(), big); + m.setIdentity(); + m(2, 0) = m(0, 2) = Scalar(big, big); + RealScalar expected = numext::abs(Scalar(big, big)) + RealScalar(1); + VERIFY_IS_APPROX(m.template selfadjointView<Lower>().l1Norm(), expected); + VERIFY_IS_APPROX(m.template selfadjointView<Upper>().l1Norm(), expected); +} + template <typename RealScalar> void check_complex_fastmath() { check_complex_rowmajor_adjoint_product<RealScalar>(); check_complex_packet_arithmetic<RealScalar>(); check_complex_packet_math_functions<RealScalar>(); check_complex_householder_qr<RealScalar>(); + check_complex_selfadjoint_l1norm<RealScalar>(); } // The packet implementations of these functions manipulate signs of non-zero values through a
diff --git a/test/selfadjoint.cpp b/test/selfadjoint.cpp index 581eb18..708b102 100644 --- a/test/selfadjoint.cpp +++ b/test/selfadjoint.cpp
@@ -75,13 +75,15 @@ VERIFY_IS_APPROX(lowerOnly.template selfadjointView<Lower>().l1Norm(), ref_l1); } -// l1Norm accumulates the column sums a panel at a time, so sweep sizes across two panel -// boundaries: a mis-sized segment would otherwise hide between the random sizes above. +// l1Norm switches from a per-column to a streaming form at a small size and its column pass has +// packet tails, so sweep sizes around the switch and across packet boundaries: a mis-sized +// segment would otherwise hide between the random sizes above. template <typename Scalar> void selfadjoint_l1norm_sizes() { typedef Matrix<Scalar, Dynamic, Dynamic> MatrixType; typedef typename NumTraits<Scalar>::Real RealScalar; - for (Index n : {Index(0), Index(1), Index(2), Index(63), Index(64), Index(65), Index(127), Index(128), Index(129)}) { + for (Index n : {Index(0), Index(1), Index(2), Index(15), Index(16), Index(17), Index(63), Index(64), Index(65), + Index(127), Index(128), Index(129)}) { MatrixType m = MatrixType::Random(n, n); MatrixType full = m.template selfadjointView<Lower>(); RealScalar ref = n == 0 ? RealScalar(0) : full.cwiseAbs().colwise().sum().maxCoeff(); @@ -90,6 +92,55 @@ } } +// The vectorized complex path squares the parts, so entries beyond the square-root range of the +// scalar must come back through the scalar fallback: squares that overflow, and squares that +// land in the denormals and lose precision. +template <typename Scalar> +void selfadjoint_l1norm_range() { + typedef Matrix<Scalar, Dynamic, Dynamic> MatrixType; + typedef typename NumTraits<Scalar>::Real RealScalar; + Index n = 70; + MatrixType m = MatrixType::Random(n, n); + RealScalar big = numext::sqrt(NumTraits<RealScalar>::highest()) * RealScalar(1e3); + RealScalar small = numext::sqrt((std::numeric_limits<RealScalar>::min)()) * RealScalar(1e-3); + for (RealScalar scale : {big, small}) { + MatrixType ms = m * scale; + MatrixType full = ms.template selfadjointView<Lower>(); + RealScalar ref = full.cwiseAbs().colwise().sum().maxCoeff(); + VERIFY_IS_APPROX(ms.template selfadjointView<Lower>().l1Norm(), ref); + VERIFY_IS_APPROX(full.template selfadjointView<Upper>().l1Norm(), ref); + } +} + +// half and bfloat16 accumulate the norm in float, so only the final rounding to the scalar separates +// the result from a float reference, not the size of the matrix. +template <typename Scalar> +void selfadjoint_l1norm_lowprec() { + typedef Matrix<Scalar, Dynamic, Dynamic> MatrixType; + for (Index n : {Index(8), Index(64), Index(300)}) { + MatrixType m = MatrixType::Random(n, n).template selfadjointView<Lower>(); + float ref = m.template cast<float>().cwiseAbs().colwise().sum().maxCoeff(); + float tol = 2 * float(NumTraits<Scalar>::epsilon()) * ref; + VERIFY(numext::abs(float(m.template selfadjointView<Lower>().l1Norm()) - ref) <= tol); + VERIFY(numext::abs(float(m.template selfadjointView<Upper>().l1Norm()) - ref) <= tol); + } +} + +// Narrow integers promote when added: the column totals must be materialized in the scalar type +// before they are compared (n=2 takes the per-column form, n=8 the column pass). +template <typename Scalar> +void selfadjoint_l1norm_integer() { + for (Index n : {Index(2), Index(8)}) { + typedef Matrix<Scalar, Dynamic, Dynamic> MatrixType; + // Random() spans the whole range; keep the column sums representable. + MatrixType m = (MatrixType::Random(n, n) / Scalar(NumTraits<Scalar>::highest() / 16)).eval(); + m = m.template selfadjointView<Lower>(); + Scalar ref = m.template cast<int>().cwiseAbs().colwise().sum().maxCoeff(); + VERIFY_IS_EQUAL(m.template selfadjointView<Lower>().l1Norm(), ref); + VERIFY_IS_EQUAL(m.template selfadjointView<Upper>().l1Norm(), ref); + } +} + void bug_159() { Matrix3d m = Matrix3d::Random().selfadjointView<Lower>(); EIGEN_UNUSED_VARIABLE(m); @@ -104,6 +155,7 @@ CALL_SUBTEST_3(selfadjoint(Matrix3cf())); CALL_SUBTEST_4(selfadjoint(MatrixXcd(s, s))); CALL_SUBTEST_5(selfadjoint(Matrix<float, Dynamic, Dynamic, RowMajor>(s, s))); + CALL_SUBTEST_6(selfadjoint(Matrix<std::complex<float>, Dynamic, Dynamic, RowMajor>(s, s))); TEST_SET_BUT_UNUSED_VARIABLE(s); } @@ -111,4 +163,11 @@ CALL_SUBTEST_1(bug_159()); CALL_SUBTEST_4(selfadjoint_l1norm_sizes<double>()); CALL_SUBTEST_4(selfadjoint_l1norm_sizes<std::complex<double> >()); + CALL_SUBTEST_6(selfadjoint_l1norm_sizes<std::complex<float> >()); + CALL_SUBTEST_4(selfadjoint_l1norm_range<std::complex<double> >()); + CALL_SUBTEST_6(selfadjoint_l1norm_range<std::complex<float> >()); + CALL_SUBTEST_1(selfadjoint_l1norm_integer<short>()); + CALL_SUBTEST_1(selfadjoint_l1norm_integer<int>()); + CALL_SUBTEST_7(selfadjoint_l1norm_lowprec<half>()); + CALL_SUBTEST_7(selfadjoint_l1norm_lowprec<bfloat16>()); }