# Full-scan native Metal fitting on Apple M5

2026-11-05. This measurement does **not** meet a two-second end-to-end target.

## Workload

Public ARINA gold, 512 × 512 scan positions and 192 × 192 detector pixels,
on Apple M5 (10 GPU cores, 24 GiB unified memory). All scan positions and
7,644 selected bright-field pixels were retained. The existing median-fill
policy repaired four detector marker pixels. No binning or precision reduction
was introduced. Calibration: 300 kV, 30 mrad, 0.5 Å scan sampling, and
0.46 mrad detector sampling for the native benchmark.

Native sources match `ceec1d9715e10fcbec276971b27163b6ce0c4055` with the
typed central-difference compiler fix in `ddbe7a1`. Python transfer parity used
a preserved working snapshot; only the transfer implementation changed between
its matched before/after runs.

The maintained `metal-direct-fit-benchmark` ran three fits with 200 trials,
Newton refinement, fifth-order refinement, and the default half-band check.
All three returned identical fitted optics, tilt, depth, and higher-order
coefficients. This is repeatability evidence, not an independent calibration
or cross-backend correctness claim.

| Stage (seconds) | First observation | Repeat 1 | Repeat 2 |
|---|---:|---:|---:|
| Fourier preparation | 3.749 | 22.461 | 10.120 |
| Joint-model preparation | 3.944 | 10.040 | 3.294 |
| Initial fit | 9.832 | 8.887 | 8.699 |
| Fit through fifth order, including initial fit | 16.766 | 15.578 | 15.390 |

Original HDF5 loading was 6.9 s. Creating the reusable benchmark fixture took
28.3 s including that load and is separate from interactive fitting. Source
pages were not cold-cache controlled. System memory pressure affected the
preparation measurements; these are active-desktop observations, not an idle
hardware limit. The runner's current top-level `load_s` includes subsequent
work; the separated numbers above use its stage records and load log instead.

## Candidate evaluation experiment

The maintained objective benchmark evaluated identical fixed candidate sets,
three repetitions each. Reducing candidates per Metal thread from four to two
preserved every recorded agreement exactly, but made low-order batches slower:

| Batch | Original GPU ms/candidate | Two candidates/thread |
|---|---:|---:|
| 8 candidates | 26.201 | 33.024 |
| 64 candidates | 26.195 | 34.354 |

The candidate was rejected. Trial counts, half-band checks, fit bounds and
precision remain unchanged. No fitting speedup is claimed.

## Validated fixes and remaining gates

- Torch-to-MLX transfer now blits from the Metal buffer handle without requiring
  host mapping of private storage. Contiguous, offset and transposed MPS tensors
  passed exact transfer tests. Six matched real-data Phase reconstructions
  (256 × 256 × 128 × 128, tilt and no tilt) matched the original phase hashes and
  fitted parameters exactly.
- Splitting a central-difference expression into typed Swift intermediates
  resolves a compiler type-check failure without changing its arithmetic.
  Three physical-Metal fifth-order gradient/search tests passed.
- The Python `benchmark-direct` command still rejects the current staged optimizer
  because its guard expects the earlier GPU-only Newton metadata. Its failed
  invocation is not a qualifying timing or a CPU-fallback measurement.
- Native application acceptance and source-to-visible timings must be reported
  separately. Kernel fitting does not certify a complete UI workflow or a
  release. The two-second full-size target remains open.

## Native application observations

The native app loaded the full 512 × 512 × 192 × 192 acquisition with scan and
detector binning both 1. Two successful load observations reached confirmed
resident presentation in 9.764 s and 4.525 s; source-cache state was uncontrolled.
Other attempts were cancelled by system memory-pressure warnings. A local UI
fix replaces the stale loading percentage with an explicit stopped/retry state.

The app's BF-edge calibration measured 0.6035446 mrad/pixel and selected 7,647
BF pixels. These differ from the benchmark settings above, so the following
times are **not** a matched overhead comparison. Source calibration was not
rewritten. The app reported an initial fit in 33.231 s and a separately requested
fifth-order refinement in 41.488 s, publishing the refined image state at
34.993 s before its checks completed. The final state contained all 200 primary
trials, order 5, a 2048 × 2048 Auto output, matching requested/shown revisions,
and a trusted half-band result (0.23 rad). Endpoint Metal allocations were
14.122 GB; this is not a peak-memory measurement.

Native actions used opt-in test hooks. Screenshot permission was unavailable,
so visible-window acceptance remains pending. The app currently requires the
separate fifth-order refinement action. No end-to-end two-second claim, native
release qualification, or automatic-fifth-order UI claim follows from this test.

A replay with the corrected native UI harness completed both stages. Resident
presentation took 5.134 s. Initial Run-to-result was 19.502 s including automation
waits; app timers reported 3.883 s preparation and 14.883 s fitting. The separate
fifth-order image state was published at 2.122 s and its checks completed at
6.047 s (7.029 s including automation waits). Run through both stages was
26.650 s, excluding acquisition loading and calibration setup. Final phase hash
and fitted coefficients matched the earlier app run exactly. These warmer
observations are not an optimization speedup.

The corrected harness reports `workflow_pass=true`, `visual_evidence_pass=false`,
and overall `pass=false`: all three screenshots were denied by macOS. Metal
allocations sampled after the initial fit were 17.999 GB, falling to 14.122 GB
after refinement. Neither sample is a measured peak; this does not qualify a
16 GiB memory limit.

Private evidence ID: `mps-full-scan-20261105`.
