Phase C: MLX backend AMICAMLXNG (v1 MVP) - #79
Merged
neuromechanist merged 3 commits intoJul 8, 2026
Merged
Conversation
Epic #74 Phase C: an optional Apple-Silicon GPU backend that runs the natural-gradient EM E/M-step on the Apple GPU via MLX. v1 MVP scope: single-model, generalized Gaussian (pdftype=0), natural gradient; Newton, the other PDF families, component sharing, multi-model, outlier rejection and save/load are fast-follows (rejected with NotImplementedError). Hybrid design forced by MLX 0.32: the GPU has no float64 and all mlx.core.linalg is CPU-only. So the elementwise/matmul hot path runs on the GPU in float32 (with the Phase A ufp/y divide-by-zero guard carried over), while inv(A)/slogdet(W) run on the CPU stream, hoisted to once per iteration (measured ~42 us/iter vs a ~13 ms GPU E-pass). lgamma/digamma are absent in MLX, so the GG normalizer and the rho update are computed host-side via SciPy on the small rho array. Exactly one mx.eval per iteration bounds the lazy graph. MLX is an optional dependency (Apple Silicon only): mlx_impl is imported lazily so import pyAMICA never requires it, and the mlx extra keeps it out of the default install, so CI (ubuntu) skips the MLX tests. Tested (real sample EEG, Apple GPU): per-block sufficient stats match the NumPy float64 reference to rtol ~1e-4; the converged LL matches the PyTorch float32 backend to ~2e-6 (< 1e-2 gate); full-data fit is finite and non-degenerate. Full non-slow torch suite unaffected (124 passed). Whether MLX beats CPU/MPS is Pathway B's question.
Silent-failure review (parity gaps vs the torch backend's guards): - fit() now checks parameter finiteness each iteration (A/mu/alpha/beta/rho) and stops with a degenerate "nan_params" reason + nan final_ll, so a final-iteration M-step blow-up cannot complete as max_iter with silently NaN params (the torch backend backstops this in state_dict; the MLX MVP had no backstop). - Reset a NaN rho update to rho0 with a warning, matching AMICATorchNG, so it does not poison the lgamma table and every subsequent E-step unattributably. - doscaling: multiply mu by safe_scale, not raw scale, so a zero-norm (collapsed) column leaves mu unchanged like A/beta (raw scale silently zeroed mu). - Comment that a singular-A LinAlg error surfaces at the fit() mx.eval, not in _update_unmixing_matrices (lazy graph). - transform() now raises a clear NotImplementedError instead of AttributeError. Comment/doc review (accuracy): - _preprocess: MLX CPU eigh is full float64 (only the GPU stream is unsupported), so using numpy is a code-reuse choice, not a precision workaround. - Fix the A-update citation (core.py:1176-1184, not 1156-1164) and tighten the _score_gg/_log_pdf_gg/_get_block_updates line ranges. - Module docstring: state accurately which deferrals raise NotImplementedError vs are simply absent. - mps_pathways.md: reword the leftover "Cost: ... v2 option" paragraph that contradicted the "MVP LANDED" status above it. MLX tests still green (4 passed).
Member
Author
Review summary (Sonnet reviewers, worktree-isolated)Two reviewers completed with substantive findings (silent-failure, comments/docs); the comment reviewer ran the actual MLX suite on Apple Silicon and verified the numeric claims (converged-LL gap 2.15e-6, sufficient-stat max relerr 1.59e-4, import isolation, MLX-linalg-CPU-only). The code and test reviewers were stopped after they ballooned to ~40 min running full suites inside their isolated worktrees; the code reviewer had already reached a clean verdict (no blocking findings), and the test reviewer's angle overlaps the verified suite run. All findings below addressed in cfd22b2. Silent-failure (parity gaps vs the torch backend's guards)
Comment/doc accuracy
MLX suite still green (4 passed); the finiteness checks did not materially slow the fit. |
The MLX backend (pyAMICA/mlx_impl) requires MLX + an Apple GPU, so CI (ubuntu, no mlx) cannot exercise it and measured it at 0%, dropping total coverage to 73.7% and failing the --cov-fail-under=80 gate. It is covered locally by tests/mlx_tests/ on Apple hardware; omit it from the coverage metric (like the subprocess-only CLI entrypoint). Coverage returns to 82.0%.
neuromechanist
merged commit Jul 8, 2026
4ff3a11
into
feature/issue-74-epic-apple-gpu
5 checks passed
This was referenced Jul 8, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
AMICAMLXNG(pyAMICA/mlx_impl/core.py), an optional Apple-Silicon GPU backend that runs the natural-gradient EM E/M-step on the Apple GPU via MLX. Epic #74 Phase C. Now that Phase A made full-data float32 converge (the precision Apple GPUs are limited to), an MLX backend becomes viable.v1 MVP scope: single-model, generalized Gaussian (
pdftype=0), natural gradient. Newton, the other PDF families, component sharing, multi-model, outlier rejection and save/load are fast-follows (rejected withNotImplementedError, not silent).Hybrid design (forced by MLX 0.32, verified by probing)
mlx.core.linalgis CPU-only. So the elementwise/matmul hot path runs on the GPU float32 stream (carrying the Phase Aufp/ydivide-by-zero guard), whileinv(A)/slogdet(W)run on the CPU stream, hoisted to once per iteration (the torch_forwardrecomputes slogdet 60x/iter). Measured: CPU linalg ~42 us/iter vs a ~13 ms GPU E-pass (0.3%) -- not the bottleneck;mx.evalplacement is (one per iteration bounds the lazy graph).lgamma/digammaare absent in MLX -- the GG normalizer and the rho update are computed host-side (SciPy) on the smallrhoarray, so the GPU E-step has no special functions.Optional dependency (CI-safe)
mlxis under[project.optional-dependencies], not the default deps;mlx_implis imported lazily soimport pyAMICAnever requires MLX. CI (ubuntu) doesn't install mlx, so the MLX tests self-skip (pytest.importorskip+ skip-if-no-Apple-GPU).Test plan (real sample EEG, Apple GPU; NO MOCKS)
pyAMICA/tests/mlx_tests/test_mlx_backend.py(4 tests, all pass locally in ~20s):|gap| < 1e-2-- measured gap ~2e-6 (both reach the same NG-GG fixed point).ll_history, non-degenerate stop, ascent (the Phase A guard holds in MLX).NotImplementedErrorforn_models>1,do_newton,pdftype!=0).Honest note
This is an enablement + validation step, not a proven speed win at 32 channels -- whether MLX actually beats CPU/PyTorch-MPS is what Phase B (dimension-sweep benchmark) measures.
Closes #76
Part of epic #74