18_mixed_precision_and_conditioning.cpp¶
Condition estimates from any factorization, and LU in float refined to double.
rcond(F, A) estimates \(1/(\|A\|_1\|A^{-1}\|_1)\) with a handful of solves against an existing factorization. lu(A, mixed_precision) factors in float, which is about twice as fast, and refines each solve in double. It uses the same estimate to decide when \(A\) is too ill-conditioned for single precision, and then factors in double instead.
#include <chrono>
#include <cmath>
#include <iomanip>
#include <iostream>
#include <numerics.hpp>
#include <optional>
#include <random>
using namespace num;
namespace {
mat<real> well_conditioned(idx n) {
std::mt19937 generator(3);
std::uniform_real_distribution<real> entry(-1.0, 1.0);
mat<real> A(n, n, 0.0);
for (idx i = 0; i < n; ++i) {
for (idx j = 0; j < n; ++j) {
A(i, j) = entry(generator);
}
A(i, i) += 2.0 * std::sqrt(static_cast<real>(n));
}
return A;
}
mat<real> hilbert(idx n) {
mat<real> H(n, n, 0.0);
for (idx i = 0; i < n; ++i) {
for (idx j = 0; j < n; ++j) {
H(i, j) = 1.0 / static_cast<real>(i + j + 1);
}
}
return H;
}
// ||b - A x||_inf / (||A||_inf ||x||_inf): about machine epsilon for a backward-stable solve.
real backward_error(const mat<real> &A, const vec<real> &b, const vec<real> &x) {
real residual = 0.0, a_norm = 0.0, x_norm = 0.0;
for (idx i = 0; i < A.rows(); ++i) {
real product = 0.0, row = 0.0;
for (idx j = 0; j < A.cols(); ++j) {
product += A(i, j) * x[j];
row += std::abs(A(i, j));
}
residual = std::max(residual, std::abs(b[i] - product));
a_norm = std::max(a_norm, row);
x_norm = std::max(x_norm, std::abs(x[i]));
}
return residual / (a_norm * x_norm);
}
template <class Work>
double milliseconds(Work &&work) {
const auto start = std::chrono::steady_clock::now();
work();
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
.count();
}
void report(const char *name, const mat<real> &A) {
const vec<real> b(A.rows(), 1.0);
vec<real> x_double, x_mixed;
lu_result<real> D;
std::optional<mixed_lu> M;
const double double_ms = milliseconds([&] {
D = seq::lu(A);
solve(D, b, x_double);
});
const double mixed_ms = milliseconds([&] {
M.emplace(lu(A, mixed_precision));
solve(*M, b, x_mixed);
});
std::cout << name << " (n = " << A.rows() << ")\n"
<< std::scientific << std::setprecision(1)
<< " rcond " << rcond(D, A) << "\n"
<< " mixed precision "
<< (M->refined() ? "refined from float" : "fell back to double") << "\n"
<< " backward error double " << backward_error(A, b, x_double) << ", mixed "
<< backward_error(A, b, x_mixed) << "\n";
if (A.rows() >= 256) {
std::cout << std::fixed << std::setprecision(1) << " factor + solve double "
<< double_ms << " ms, mixed " << mixed_ms << " ms\n";
}
}
} // namespace
int main() {
report("random, diagonally weighted", well_conditioned(1024));
report("Hilbert", hilbert(12));
}
Possible output:
random, diagonally weighted (n = 1024)
rcond 1.1e-02
mixed precision refined from float
backward error double 2.6e-16, mixed 3.2e-16
factor + solve double 26.4 ms, mixed 18.1 ms
Hilbert (n = 12)
rcond 2.6e-17
mixed precision fell back to double
backward error double 3.3e-18, mixed 3.3e-18