Skip to content

linalg: solve(A, b) via the on-ANE pivoted LU, and a matrix right-hand side - #170

Merged
sbryngelson merged 1 commit into
sbryngelson:mainfrom
axiom-of-choice:feat-132-linalg-solve
Aug 4, 2026
Merged

linalg: solve(A, b) via the on-ANE pivoted LU, and a matrix right-hand side#170
sbryngelson merged 1 commit into
sbryngelson:mainfrom
axiom-of-choice:feat-132-linalg-solve

Conversation

@axiom-of-choice

Copy link
Copy Markdown
Contributor

Fixes #132.

What lands

solve(A, b) for general square A: lu_pivoted already factors on the ANE and solve_triangular already substitutes, so this is the permutation applied host-side (P is a permutation matrix, so P b is a row gather rather than a matmul worth dispatching) plus forward and back substitution. It is the direct complement to conjugate_gradient/gmres, which are iterative and want SPD input.

solve_triangular now accepts a matrix right-hand side, which #132 asks for and which #106 wants for inv. The substitution is a row recurrence, so widening the row slice from [1, 1] to [1, m] solves all m columns in the same fused graph instead of dispatching once per column. A 1-D b still returns shape [n], unchanged.

Both are added to __all__, along with solve_triangular, which was reachable but not exported.

Verification

Apple M2 Pro (Mac14,12 Mac mini), macOS 26.5.2. Against numpy on the fp16-rounded system:

n vector rhs matrix rhs (m=3)
4 1.3e-3 5.9e-4
6 9.5e-4 4.7e-4
8 9.3e-4 9.7e-4
Check Result
tests/test_linalg.py 66 passed (was 57; 9 new)
tests/run_corpus.py GATE: GREEN, 90/90
ruff / pyright / .githooks/pre-commit clean

The new tests fail without the change, verified by swapping in origin/main's linalg.py.

Three of them are worth pointing at specifically, since "matches numpy" alone would be weak here:

  • the matrix path is checked column by column against the single-vector solve, so a mutation that solved only the first column, or that broadcast one column across all m, fails rather than passing on an averaged norm
  • a residual check (A x ~ b) confirms correctness without a reference solver at all
  • test_solve_triangular_vector_shape_unchanged pins that 1-D input still returns 1-D, which is the regression the widening could most easily have introduced

Rejects covered: non-square A, mismatched b rows, and a singular factor raising LinAlgError like numpy.

Risk

Contained to linalg.py. The change to solve_triangular is the one with existing callers, so the vector shape and the existing tests (including test_solve_triangular_large_entries, which exercises the routed accessor above the slice saturation threshold) are the thing to look at; both are unchanged and green.

expm (#133) is the natural next one and I have it working locally, but it is a separate issue so it goes in its own PR rather than riding along here.

…d side

Implements sbryngelson#132. `lu_pivoted` already factors on the ANE and `solve_triangular`
already substitutes, so `solve` is the permutation applied host-side plus two
substitutions. It is the direct counterpart to `conjugate_gradient`/`gmres`, which
are iterative and want SPD input, and it handles general square A.

`solve_triangular` now takes a matrix right-hand side as well as a vector, which
sbryngelson#132 asks for and sbryngelson#106 wants for `inv`. The substitution is a row recurrence, so
widening the row slice from [1,1] to [1,m] solves all m columns in the same fused
graph rather than dispatching per column. A 1-D b still returns shape [n].

Rejects a non-square A, a b whose rows do not match, and a singular factor (zero
pivot in U) with LinAlgError, like numpy.

On M2 Pro against numpy, relerr <= 1.3e-3 for n in {4,6,8} for both vector and
matrix right-hand sides; the matrix path also agrees with solving each column on
its own, and a residual check confirms A x ~ b without a reference solver.
test_linalg.py 66 passed, corpus GATE GREEN 90/90.
@sbryngelson
sbryngelson merged commit c5451d0 into sbryngelson:main Aug 4, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

linalg: solve (general A x = b via LU)

2 participants