Skip to content

M15: native linalg via mx::linalg::* - #27

Merged
ausimian merged 7 commits into
mainfrom
m15-native-linalg
Apr 17, 2026
Merged

M15: native linalg via mx::linalg::*#27
ausimian merged 7 commits into
mainfrom
m15-native-linalg

Conversation

@ausimian

@ausimian ausimian commented Apr 16, 2026

Copy link
Copy Markdown
Owner

Summary

  • Lift 7 linalg ops (lu, svd, qr, cholesky, eigh, solve, triangular_solve) off via_binary fallback onto native MLX mx::linalg::* primitives
  • MLX linalg is CPU-only — NIFs use a CPU default stream inside the worker's run_sync callback (adapted to M14.5 worker-thread model)
  • qr with mode: :complete falls back to via_binary (MLX only supports reduced QR); determinant uses Nx's default impl which now calls the native lu

Test plan

  • Native NIF unit tests (hand-computed references for all 7 ops)
  • Backend property tests vs Nx.BinaryBackend with well-conditioned random inputs
  • Backend integration tests (fallbacks, triangular_solve 4-combo coverage, SVD reduced mode)
  • mix precommit (compile, format, credo, tests) — all green
  • Conformance tests (--only conformance) — 17 tests pass
  • All *_full suites (Qwen3, Qwen3 quantized, ViT, Whisper, MNIST training, fast-kernels) — 10 tests pass

Bind lu, svd, qr, cholesky, eigh, solve, and triangular_solve to
their MLX linalg counterparts, replacing via_binary round-trips.

MLX's linalg primitives are CPU-only (no GPU kernels as of 0.25),
so the NIFs force a CPU stream regardless of the caller's stream.

Residual fallbacks:
- qr with mode: :complete (MLX only supports reduced)
- determinant (Nx default calls lu, which is now native)

triangular_solve handles left_side: false and transform_a: :transpose
by composing native transpose + native solve_triangular.
- Remove dead rank-1 branch in mat_transpose_axes (never called)
- Avoid NIF round-trip in SVD: compute full-matrices shape from t.shape
  instead of querying Native.shape(ref)
- Restructure triangular_solve into 4 explicit cases to eliminate
  double-transpose when transform_a: :transpose + left_side: false
- Replace manual lower_triangular_matrix generator with Nx.tril
- Extract make_well_conditioned/1 to DRY diagonal-dominance in 3
  property tests
- Add missing test: triangular_solve with left_side: false +
  transform_a: :transpose
Two non-deterministic crash modes (SIGABRT from LAPACK SVD
convergence failure, SIGSEGV with no trace) observed during M15
development. First seen after M14 but M15 may be the actual
trigger. Documents what's known and what investigation is needed.
SIGABRT: the SVD property test was the only linalg test not applying
make_well_conditioned/1, so random ill-conditioned matrices could
trigger LAPACK convergence failure (sgesvdx_ error code 5) which
abort()s the VM.

SIGSEGV: all linalg ops force cpu_stream() but input tensors may have
pending lazy ops on the caller's GPU stream. Add mx::eval() on inputs
before each cross-stream handoff so the CPU primitive never races the
GPU for the same buffer.

20 consecutive full-suite runs pass cleanly (previously ~1 in 4 crashed).
SIGABRT fixes:
- make_well_conditioned/1 multiplier raised from n*10 to n*10+20.
  The old value landed on the diagonal-dominance boundary for n=2,
  allowing f32 rounding to produce singular pivots that abort via
  MLX's uncatchable StreamThread exception path.

SIGSEGV (exit 139) is pre-existing — reproduces at ~4/10 on main
with no linalg tests. Likely an MLX Metal driver issue, not M15-
related. Updated PLAN.md to document this.
Linalg NIFs take a WorkerThread as first parameter and dispatch via
run_sync, consistent with the rest of the NIF surface. Inside
run_sync, a CPU default stream is used (MLX linalg ops are CPU-only).
MLX handles cross-stream dependencies natively.
Add @describetag :linalg to all linalg describe blocks so they
can be included/excluded via mix test --only/--exclude linalg.
@ausimian
ausimian force-pushed the m15-native-linalg branch from 406d1ec to 89295e4 Compare April 17, 2026 10:33
@ausimian
ausimian merged commit 381ea74 into main Apr 17, 2026
1 check passed
@ausimian
ausimian deleted the m15-native-linalg branch April 17, 2026 10:42
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.

1 participant