Skip to content

Epic: make MLX a first-class, end-to-end backend #324

Description

@neuromechanist

Goal

Make the MLX backend first-class and trustworthy end to end, not just feature-complete in isolation. Epic #278 brought AMICAMLXNG to fitting and non-fitting parity with AMICATorchNG, but every MLX user is still a raw-backend user: the AMICA and AMICAICA wrappers build torch unconditionally (#313), MLX has no explicit pcakeep/pcadb (#323) and no params-file path (#304). We are the first users (per-session AMICA on 64-channel EEG on Apple Silicon, pcakeep = nbchan - 1 after average reference), so the bar is a workflow we trust.

Phases

# Slug Issue Wave Target
1 pca-policy #323 1 epic branch
2 pca-residual #322 1 dev directly (external tester's issue, independent of MLX)
3 params-surface #304 1 epic branch
4 backend-selection #313 2 epic branch
5 backend-guards #306 2 epic branch
6 e2e-validation #315 3 epic branch
7 doscaling-rows #333 4 epic branch
8 component-rows #334 5 epic branch
9 newton-start #335 4 epic branch
10 sphere-order #336 5 epic branch
11 iteration-order #339, #345 6 epic branch
12 init-normalize #341 7 epic branch
13 float32-consts #344 7 epic branch
14 numpy-kwargs #346 6 epic branch
15 final-parity #351 8 epic branch
16 docs-audit #352 8 epic branch
17 defaults #354 9 epic branch

Phase 1: shared pcakeep/pcadb validation and reduction-request predicate in pamica/rank.py, used by torch, NumPy and MLX; MLX constructor parameters, upfront mir_step gate and persistence; cross-backend test on real EEG.

Phase 2: AMICAICA exports the full PCA basis so MNE's own apply restores the PCA residual by default; ADR, differences-page section and docstrings stating plainly that this goes beyond the Fortran reference (whose output carries no representation of the discarded subspace) and how to get the reference behavior (n_pca_components=ica.n_components_).

Phase 3: one shared params-file reader returning canonical keys for JSON and Fortran text; AMICA_NumPy accepts input.param; NumPy raises NotImplementedError for pdftype != 0.

Phase 4: AMICA(backend="mlx") and AMICAICA(backend="mlx"), including save/load, from_params_file and pcakeep through the wrapper, with end-to-end tests on both backends.

Phase 5: degenerate-fit and input guards on the raw torch and MLX accessors (#306).

Phase 6: validate_implementations.py --backend {torch,numpy,mlx}, an end-to-end test mirroring our own pipeline, and an MLX getting-started path (#315).

Phases 7-9 (added 2026-09-22 after a read-only investigation found a component-orientation defect; user decision to fold them into this epic): pamica stores each model's mixing block transposed relative to the reference, so components are rows of the stored block, but doscaling (default on), the share metric and the share merge act on stored columns. Phase 7 fixes doscaling (default trajectories change toward the reference; native-oracle test; ADR amending ADR 0001), Phase 8 moves to a component-row layout that makes sharing correct (byte-identical without sharing; save-format conversion), Phase 9 confirms and fixes a suspected one-iteration Newton start offset. After Phase 9 the per-backend parity rows are re-measured.

Phases 10-16 (added 2026-09-22/23 as reviews and native-oracle tests surfaced further reference mismatches): Phase 10 writes the sphere column-major as the reference reads it; Phase 11 follows the reference's iteration order (E-step, checks and the LL-decrease response, exit, then update) and its unconditional A-freeze windows; Phase 12 normalizes the drawn initial mixing matrix as the reference does; Phase 13 uses the reference's single-precision density constants; Phase 14 makes AMICA_NumPy reject unknown options. Phase 15 re-measures every parity figure (validation rows, the paper's Table 1, the multi-model ensemble, ADR 0003) under the finished code; Phase 16 audits the documentation against it; Phase 17 unifies the lrate default across entry points on the compiled binary's 0.1 (user decision), makes the raw PyTorch backend fall back to CPU for float64 on Apple Silicon, and documents pamica's defaults against the compiled binary and EEGLAB's runamica15.m.

Evidence that motivated the scope (real sample EEG, 32 channels)

Each box is checked when the phase that fixes it merges.

Definition of Done

The plan of record, with decided policies, deliverables and pre-registered test gates per phase, is .context/issue-324/plan.md on the epic branch.

Activity

  1. added 3 commits that reference this issue on Sep 22, 2026
  2. added a commit that references this issue on Sep 23, 2026
  3. added a commit that references this issue on Sep 23, 2026
  4. added 2 commits that reference this issue on Sep 24, 2026
  5. sunyuhongwr commented on Oct 8, 2026

    @sunyuhongwr

    @neuromechanist Thank you for suggesting that I rerun the validation. I tested three MEG datasets with pAMICA 0.4.1.dev0. I believe the previous runs used 0.3.3, but I have not verified this.

    I attached the validation script and two figures. Participant names are anonymized.

    Image

    Reconstruction: For each dataset and each of the five models, I applied the model with no components excluded and compared the output with the original data across all MNE annotation-clean samples and fitted MEG channels. The left panel shows the minimum channel-wise Pearson correlation; the right shows the maximum NRMSE (RMS error divided by the original signal’s standard deviation). Correlations are 1.00000000, and maximum NRMSE ranges from 4.9e-15 to 4.9e-12.

    Although pcakeep=None, The saved spheres were rank-reduced (e.g., 72 × 306), so this also tests PCA-residual restoration, as discussed in #322. (Correction: our MEG data were rank-reduced by Maxwell filtering, so this is still a full-rank fit relative to the Maxwell-filtered data.) The reconstruction matches the original data to numerical precision.

    Image

    Component sharing: The left panel shows the number of shared-component groups; the right shows the number of participating (model, component) slots. Old → new counts were:

    Dataset Groups Slots
    Participant 1 83 → 76 241 → 245
    Participant 2 79 → 72 211 → 218
    Participant 3 91 → 98 211 → 244

    I set the random seed to 42 for both fits, so I expected their model probabilities to be consistent. However, they differ when pcakeep=None, while they appear identical with pcakeep=20. Could this difference be expected, or indicate a problem? I would appreciate your advice on how to investigate it.

    Vdev0.4(new fullrank):
    Image
    V0.33(old fullrank):

    Image

    The fit configuration was:

    picks=meg, max_iter=2000, n_models=5, n_mix=3, lrate=0.01,
    do_reject=False, rejsig=3.0, share_comps=True, pcakeep=None,
    do_approx_sphere=True, device=cuda, verbose=True, random_seed=42
    
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    epicEpic tracking issuefeatureNew feature or enhancement

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions