Repository navigation
Conversation
| |> Nx.transpose() | ||
| |> Nx.abs() | ||
|
|
||
| subtracted = Nx.sum(lower_magnitudes * upper_magnitudes, axes: [1]) |
There was a problem hiding this comment.
is this correct? the name and the calculation seem disconnected
There was a problem hiding this comment.
It's correct, the transpose is what makes it hard to see and the naming could be better. Row i of
upper_magnitudes is column i of |U|, so summing the elementwise product
over row i gives the sum over j < i of |l[i][j]| * |u[j][i]|, the
magnitudes of the products subtracted from a[i][i] to form pivot i.
How about upper_column_magnitudes for the transposed U and
subtracted_magnitudes for the sum, with a comment stating that formula?
There was a problem hiding this comment.
Yeah, I still think calling it anything subtracted is a bit too hidden. magnitude_product might be a better name
| |> Nx.abs() | ||
|
|
||
| subtracted = Nx.sum(lower_magnitudes * upper_magnitudes, axes: [1]) | ||
| tolerance = Nx.Type.rounding_error_multiplier(type, n) * Nx.Constants.epsilon(type) |
There was a problem hiding this comment.
this seems overly specific to add to Nx.Type
6d204cf to
27354f0
Compare
… of its LU factorization Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ger matrices Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…est for 16-bit types Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…at 12 epsilon Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
27354f0 to
e4753b4
Compare
| If a singular matrix is passed, the result will silently fail. Matrices | ||
| that are singular to working precision return NaN too, and so does one | ||
| with a pivot that overflows the type. In a batch, only the singular | ||
| matrices are NaN. |
There was a problem hiding this comment.
| If a singular matrix is passed, the result will silently fail. Matrices | |
| that are singular to working precision return NaN too, and so does one | |
| with a pivot that overflows the type. In a batch, only the singular | |
| matrices are NaN. | |
| If a singular matrix is passed, the result will silently fail; |
I don't think these phrases add any new information:
- working precision is by definition how we can measure whether a matrix is singular
- overflows by definition silently fail
- batches are also defined as independently processed
polvalente
left a comment
There was a problem hiding this comment.
The PR is still a draft. Is it ready?
Not yet - the numbers looked okay, but I wasn't satisfied. I did more extensive testing and found the source of some subtle drifting I noticed - there's several rounding vs truncation issues across the backends which cause this heuristic (as well as probably the original heuristic? or at least probably math somewhere) to be subtly almost correct. This heuristic expects rounding. Rounding follows IEEE 754, and it seems like rounding is the standard across the ecosystem. Literature seems to take this into account too? There were some places that were double rounding too, which causes issues. The PR #1890 was a finding during this testing. These issues impact other things as well I'm guessing, not just this heuristic, and subtly? TLDR still working on things 😄 |
I've been procrastinating opening this due to the sheer amount of details in it (apologies in advance) and how I haven't had the time. I decided I would open this with a (mostly) reviewed PR message so that folks could take a look at it and see the data (even if it is just a wall of text) and provide any thoughts.
Problem
Nx.LinAlg.invert/1computed the determinant of the input, returned NaNwhen the determinant's magnitude was at most 1000 times the smallest
positive normal number of the input type, and otherwise divided each row
by its largest magnitude and called
solve/2with the identity scaled thesame way. Three things were wrong with that implementation:
singularity. A well-conditioned matrix with small entries, such as
[[0.2, 0], [0, 0.2]]in f16, has a determinant below the thresholdand came back NaN.
determinant of an exactly singular matrix usually rounds to a small
nonzero value far above the threshold. Of 300 random exactly singular
8x8 integer matrices in f32, 25 were caught, 11 as NaN and 14 as an
ArgumentErrorfromsolve/2meeting an exactly zero pivot; of 300 at32x32, 2. The rest came back as large finite numbers.
ifindefnruns both branches and selects perelement, so a batch with one singular matrix still ran
solve/2onthat matrix. On the default backend and on Torchx,
solve/2raisedArgumentErrorfor that matrix, and the raise failed the whole batch.Changes
invert/1now computes an LU factorization of the row-scaled input,tests each pivot of U for singularity, and finishes with two triangular
solves. It no longer computes a determinant or calls
solve/2.Row scaling. The input is cast to floating point (integer input
becomes f32, as on
main) and each row is multiplied by the power of twonearest the reciprocal of its largest magnitude, so every nonzero row has
largest magnitude between
1/sqrt(2)andsqrt(2). Zero rows are leftalone. The exponent is clipped to the normal range of the type, because
some backends flush a subnormal scale factor to zero. Multiplying by a
power of two is exact, so an exactly singular matrix stays exactly
singular after scaling; LAPACK's equilibration routine xGEEQUB scales by
powers of the radix for the same reason [1]. Magnitudes are taken after
the cast to floating point, since the absolute value of a signed integer
type's minimum wraps around in that type.
Singularity test. After
{p, l, u} = Nx.LinAlg.lu(scaled), pivotiis
|u[i][i]|, and the elimination formed it by subtracting the productsl[i][j] * u[j][i]for everyj < i. The matrix counts as singular whenfor some
iwhere
nis the matrix order,epsilon(type)is the machine epsilon ofthe type and
multiplieris the newNx.Type.rounding_error_multiplier/2described below. The right-hand side is the rounding error the pivot's own
computation could have accumulated: the componentwise backward error bound
for LU factorization,
|A - LU| <= n * u * |L| * |U|withuthe unitroundoff [2], applied to one entry. A pivot no larger than that bound is
indistinguishable from a zero that rounding perturbed. The pivot itself is
not among the subtracted products, so a pivot nothing was subtracted from
is flagged only when it is exactly zero; diagonal and upper triangular
matrices, whose elimination subtracts nothing, therefore pass at any size
and tolerance. A pivot that overflowed to infinity is flagged too.
Why a per-pivot test. Two alternatives were measured against it on
random singular and nonsingular matrices in f32, f16 and bf16:
pivot is at most
16 * n * epsilontimes the largest. It rejects gradedmatrices, that is, matrices whose rows differ widely in magnitude, such
as
[[1, 0], [1, 1e-6]], which invert accurately. In bf16 it rejectsmost matrices of size 4 and up.
rcondis the estimate from [8], asLAPACK's xGECON computes it, with
rcond < epsilonas the rejectionrule. It catches every singular matrix in the measurement but also
rejects the whole graded family in f32 at 32x32 (300 of 900 nonsingular
matrices) and about 40% of nonsingular bf16 matrices at 8x8 and 16x16.
It answers a different question: whether the inverse is sensitive to
perturbations of the data, not whether the computed factorization found
a singular matrix. The
invert/1docs promise NaN for singularmatrices, not for ill-conditioned ones. NumPy, PyTorch and Julia also
signal only for singular matrices, never for ill-conditioned ones,
though they detect singularity by LAPACK's exactly zero pivot, a
stricter test than this one; MATLAB estimates the condition number and
warns. The estimate is also more expensive: an iteration of extra
triangular solves per matrix, where the per-pivot test is one
elementwise product and a row sum.
The per-pivot test flags only pivots that rounding could have produced
from zero, which is what the docs promise. A condition number estimate
would be a useful separate function.
Batches. Only the singular matrices in a batch come back NaN. Because
some backends' triangular solves raise on a singular factor, the L and U
of a flagged matrix are swapped for the identity before the solves, and
the NaN replaces that matrix's result.
Nx.Type.rounding_error_multiplier/2(new,@doc false) returns2 * nfor 32-bit and wider types,min(2 * n, 4 * sqrt(n))for f16 andmin(2 * n, 4 * sqrt(n), 12)for bf16. The multiplier has to sit abovethe rounding noise an exactly singular matrix leaves in its zero pivot,
or singular matrices slip through, and below the pivots of usable
nonsingular matrices, or those are rejected.
2 * n * epsilonis fourtimes the
n * ubound, sinceepsilonis2 * u; the extra factorcovers rounding errors in earlier pivots propagating into later ones, and
in f32 and f64 it costs nothing, since no nonsingular matrix in any test
below is rejected. In the 16-bit types the worst-case bound is unusable:
2 * n * epsilonreaches 1 at n = 64 in bf16, where every pivot would beflagged, so from n = 5 they use
4 * sqrt(n), the typical rather thanworst-case error growth [3]. In bf16 the measured noise is lower still
and does not grow with n, so bf16 is also capped at 12 epsilon from
n = 10. The measurement is the ratio of the nonzero pivot that rounding
leaves in place of zero to the sum of the products subtracted to form it,
in epsilons, over 300 exactly singular matrices per size; the largest per
cell:
At 8x8 both rules allow 11.3, so the outliers there slip under either
rule; from 16x16 up the largest ratio stays under 12 everywhere. The
cap's cost is the tail: in four runs of 2,000 singular 16x16 matrices,
two per backend, 16 exceeded 12, about one in five hundred, with the
largest at 18.9; the sample below has none above 12 at 16x16, and an
earlier sample of 300 had 2. That is below the miss rate the uncapped
rules already have at 16x16, where f32, f64 and f16 miss 2, 6 and 6 of
300 in the table below because a computed LU can be the exact
factorization of a nearby nonsingular matrix. The cap's benefit is the
nonsingular matrices between 12 and
4 * sqrt(n)epsilon, which at 32x32include the Hadamard matrix and the random well-conditioned families
below. f16 keeps the square root rule because its ratios are much larger:
in the separate run the largest per size is 22.6 at 16x16, 26.9 at 32x32
and 22.3 at 64x64, against allowances of 16, 22.6 and 32, which miss 5, 1
and 0 of the 300.
Torchx.
invert/1now factors instead of solving, so it needs theTorchx
luNIF to factor singular matrices instead of throwing, which is#1877.
Measurements
Below,
condis the 2-norm condition number of the matrix.The next two tables and the breakdown after them come from one seeded
sample of 300 exactly singular and 900 nonsingular matrices per type at
sizes 2, 3, 4, 6, 8, 16 and 32, run once on
mainand once on thebranch. On
maina matrix counts as caught or rejected wheninvert/1returned NaN or raised, since the
solve/2it called raised on anexactly zero pivot; the branch never raised. The 3x3 and 6x6 cells are
omitted from both tables, and 2x2 from the singular one, where every type
catches all 300.
Singular matrices caught. The branch catches nearly all of them at
every size and in every type;
maincaught most at 4x4, about a tenthat 8x8 and almost none from 16x16 up. Random exactly singular integer
matrices, 300 per cell,
main-> branch.The few singular matrices that still pass have a computed LU that is also
the exact factorization of a nearby nonsingular matrix, so no test on the
factors can flag them.
Nonsingular matrices wrongly rejected. f32 and f64 lose nothing, f16
loses far less than on
main, and bf16 rejects more thanmainatevery size. 900 per cell: 300 each with small integer entries, standard
normal entries, and graded rows.
main-> branch.The bf16 column is a trade, not a regression: of what bf16 now rejects,
mainwas inverting badly most at 32x32, half at 16x16 and fewer thanhalf at 8x8, so the caller gets NaN in place of wrong numbers, at the
cost of some inverses that were acceptable. The table shows how the
inverses
mainreturned for those matrices had fared, in the max normagainst the f64 inverse of the same rounded matrix:
mainAt 32x32, 554 of the 670 rejected had failed or were more than 10% off
on
main; at 16x16, 117 of 226, with 64 within 5%; at 8x8, 30 of 73,with 32 within 5%. The 2 at 16x16 are one NaN and one
ArgumentError,where
main's bf16 LU met an exactly zero pivot. The f16 counts at 2x2and 4x4 are the small-entries bug from the Problem section disappearing;
f32 and f64 reject nothing at any size.
The same picture on 40 random normal bf16 matrices per size, from a
separate run measured the same way:
main: NaN, median error, worstThe branch returns fewer inverses in bf16, and the worst it returns in
these samples is 61% off, against 320% on
main.Exhaustive checks. Every 2x2 with entries in -5..5 and every 3x3 with
entries in -1..1, in all four types, comes back NaN for exactly the
singular ones. On 20,000 random 4x4 with entries in -2..2, f32, f64 and
f16 are exact too; bf16 flags 122 of the 18,020 nonsingular matrices and
misses 3 of the 1,980 singular ones.
Well-conditioned and ill-conditioned matrices. Taking
cond * epsilon <= 0.1as the range where a direct solver in the type canbe expected to succeed [6], the test rejects nothing in that range at
sizes 4 to 64 in f64, f32 and f16, and through 32x32 in bf16, on LAPACK's
dchkgefamilies at cond 2, sqrt(0.1/epsilon) and 0.1/epsilon [4],including a cond-2 matrix scaled to just above the smallest normal number,
and on thirteen named matrices from Higham's collection [5], Hadamard
included. The forward error of the accepted inverses grows as
cond * epsilon. Hilbert, Pascal, Frank and Vandermonde are rejected onlyonce
cond * epsilonis far above 1. At 64x64, bf16 rejects 5 of 30random matrices at cond 0.1/epsilon, the edge of the range, and nothing
else in range.
Entry magnitude. Scaling makes the result independent of the size of
the entries. With
m * [[2, 1], [1, 3]]andmset to every power oftwo representable in each type, the branch inverts it correctly wherever
the inverse is representable.
mainreturned NaN formbelow2^-4inf16 and below
2^-60in bf16 and f32. In f64 it returned NaN below2^-508, where the determinant drops below the threshold, and raisedArithmeticErrorabove2^510, where the determinant overflows.Known limits. Two limits remain, and both belong to the backends
rather than to the test. On the default backend an f64 result
that overflows raises
ArithmeticError, as every overflowing f64operation does there. XLA flushes subnormal values to zero, so on EXLA
input below the normal range is zero. For f32 matrices whose rows exceed
about
2^126.5in magnitude, an intermediate in XLA's triangular solveis subnormal and is flushed, which drops the part of the inverse that
distinguishes the matrix from the nearest exactly singular one; the
relative error of the inverse is then about
1 / cond(A). That error islargest for well-conditioned matrices, but their inverses at that
magnitude are subnormal themselves and would be flushed by XLA in any
case.
Behavior by backend
invert/1now treats a singular matrix the same on all three backends:NaN for that matrix, alone or in a batch, and no raise; Torchx still
raises for f16 and bf16 input, as before. Before this change, every
backend returned NaN only when the determinant fell below the threshold
and otherwise large finite numbers for a singular matrix, and the default
backend and Torchx raised for a batch containing a singular matrix.
After:
invert/1, singular matrix alone or in a batchArgumentErrorfrom Torchx's determinant check insolve/2)invert/1, f16 or bf16 inputThat is PyTorch's
inv_exmodel, a per-matrix status, with NaN as thestatus since compiled code cannot raise. For comparison, PyTorch's
invand
solveraise only on an exactly zero pivot, and for the whole batch,while
inv_exandsolve_exnever raise. For[[2, -1, -1, 0], [2, 0, -1, 1], [-1, 0, 1, 1], [1, 2, -2, -2]], which isexactly singular with no exactly zero pivot, PyTorch returns entries of
order 1e7 with a success status. PyTorch rejects f16 and bf16 input to
these functions, NumPy rejects f16, and LAPACK has no 16-bit routines.
solve/2is unchanged by this PR. On the default backend it raises onlywhen its triangular solve meets an exactly zero pivot; making it raise
for matrices that are singular to working precision, by the same test
invert/1uses, is a follow-up PR.Tests
nx/test/nx/lin_alg_test.exsaddsinvert/1cases for:and without an exactly zero pivot, including samples of the 2x2 and 3x3
domains checked exhaustively above and a determinant that rounds away
from zero;
cond-2 and cond-0.1/epsilon families at n <= 16, in every float type;
is large;
matrix [7], whose last pivot
2^(n - 1)overflows f16 at n = 17;normal range of f64;
one and an accurate inverse for every nonsingular one, alone and in a
batch, with the matching
solve/2cases, which pass onmain'ssolve/2too because eliminating these small integers meets an exactzero pivot.
exla/tests that the input type is kept, NaN per matrix in a batch, androws near the top of the f32 range.
torchx/tests thatinvert/1returns NaN for singular matrices alone and in a batch, with and without
an exactly zero pivot.
Performance
invert/1is about twice as fast from 8x8 up, because it no longercomputes a determinant in addition to the factorization. Default
backend,
main-> branch:invert/1References
1. J. Demmel, Y. Hida, W. Kahan, X. S. Li, S. Mukherjee and E. J. Riedy,
"Error bounds from extra-precise iterative refinement", ACM Trans. Math.
Softw. 32(2), 2006. https://doi.org/10.1145/1141885.1141894
2. S. M. Rump and C.-P. Jeannerod, "Improved backward error bounds for LU
and Cholesky factorizations", SIAM J. Matrix Anal. Appl. 35(2), 2014.
https://doi.org/10.1137/130927231
3. N. J. Higham and T. Mary, "A new approach to probabilistic rounding
error analysis", SIAM J. Sci. Comput. 41(5), 2019.
https://doi.org/10.1137/18M1226312
4. G. W. Stewart, "The efficient generation of random orthogonal matrices
with an application to condition estimators", SIAM J. Numer. Anal.
17(3), 1980. https://doi.org/10.1137/0717034
5. N. J. Higham, "Algorithm 694: a collection of test matrices in MATLAB",
ACM Trans. Math. Softw. 17(3), 1991. https://doi.org/10.1145/114697.116805
6. N. J. Higham, Accuracy and Stability of Numerical Algorithms, 2nd ed.,
SIAM, 2002, chapter 7.
7. J. H. Wilkinson, "Error analysis of direct methods of matrix inversion",
J. ACM 8(3), 1961. https://doi.org/10.1145/321075.321076
8. N. J. Higham, "FORTRAN codes for estimating the one-norm of a real or
complex matrix, with applications to condition estimation", ACM Trans.
Math. Softw. 14(4), 1988. https://doi.org/10.1145/50063.214386