Skip to content

fix: ACOM Metal argmax keeps the lowest bin on an exact tie - #63

Merged
Paullo9 merged 4 commits into
mainfrom
issue-62-metal-argmax-ties
Nov 1, 2026
Merged

Paullo9 merged 4 commits into
mainfrom
issue-62-metal-argmax-ties

Conversation

@Paullo9

@Paullo9 Paullo9 commented Oct 31, 2026

Copy link
Copy Markdown
Contributor

Requested by Paul · project thread

Fixes #62

Stacked on #61 (base issue-8-metal-timing): merge #61 first. This PR then retargets to main.

What was wrong

The Metal inverse-transform argmax (acomInverseArgmax) reduced scores in a tree, keeping a value only when it was strictly larger. On an exact tie, the bin it returned depended on the tree layout. Over 4 bins with a tie between bins 1 and 2, it returned 2. The CPU path (ACOMMatcher.correlate, the first maximum, as NumPy argmax in py4DSTEM) returns 1. So the two backends could report different in-plane angles for the same pattern.

Fix

  • The reduction compares (score, bin) pairs. A larger score wins; on an equal score, the smaller bin wins. This gives the first maximum whatever the tree order.
  • NaN and -inf enter the scan as -FLT_MAX, so they never win. This matches the CPU scan, which starts at -greatestFiniteMagnitude. An all-NaN row now gives bin 0 and -FLT_MAX / n on both paths; before, Metal could return a NaN score.
  • The scan runs on unscaled values, and only the winner is divided by n. This is the same order as the CPU, so a division near underflow cannot create a tie.
  • Low bin counts: the threadgroup memory length is now max(16, n * 4), because Metal requires multiples of 16 bytes and n * 4 is not one below 4 bins (one line).
  • Test seam: ACOMMetalKernels.bestBins (package access, not public API) runs the kernels on planted ring spectra. ACOMMetalContext gained an init from raw spectra; the library init delegates to it.

Tests

  • New ACOMMetalArgmaxTieTests (skipped without a Metal device). It uses one ring, an experimental spectrum of ones, and a template spectrum whose inverse transform is exactly [0, 4, 4, 0] in the kernel Float arithmetic.
    • Red before the fix: Metal returned bin 2 in both passes, against 1 on the CPU. Metal also returned a NaN score where the CPU gives -FLT_MAX / 4.
    • Green after the fix.
  • Gate swift test -c release --parallel --skip DiffractoKitParityTests: all green (28 + 156 + 208 + 173 tests). The existing Metal-against-CPU tests are unchanged, with a worst score gap of 1.19e-7.

Review

One numerics review, before the commit. It found nothing blocking. It traced the n=4 planted FFT as exact, and checked the tie rule at every tree level, barrier safety, NaN/-inf parity and the init guards. Its one low finding (scale after the scan, not before) is applied.

Not in this PR: the tie rule belongs in the P31 row of docs/parity.md. It is left out because other open PRs already edit that file.

🤖 Generated with Claude Code

Paullo9 and others added 2 commits October 31, 2026 14:58
Measured first with a skipped-by-default benchmark (ACOMMetalBenchmarkTests,
DIFFRACTOKIT_ACOM_BENCHMARK=1), 128 x 128 scan, one CPU preparation worker:
600 axes 0.259 -> 0.133 to 0.136 ms per pattern, 2400 axes 1.131 -> 0.351 to 0.353.
The GPU time was the bound: the spectrum kernel read the whole template bank twice
per position, and the inverse transform was a direct n^2 sum.

- One spectrum thread now computes both passes for a block of 8 positions, so each
  template value is read once per 8 positions; the sums run over the rings in the
  same order with the same products.
- The inverse transform is a radix-2 FFT in threadgroup memory.
- Metal against the CPU reference: worst score gap 1.2e-7 to 1.8e-7 (bound 1e-6).
  New ACOMMetalAzimuthalLengthTests check 4 to 1024 bins and a partial position
  block: scores, runner-up, template, and the bin within a pass.

Done: Metal timing of issue 8 measured and the kernel sped up.
Next: an all-cores CPU against Metal row on a quiet machine for docs/benchmarks.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Metal inverse-transform argmax compared scores only (strictly larger
wins) in a tree reduction, so on an exact tie the bin it returned depended
on the tree layout: over 4 bins with a tie between bins 1 and 2 it returned
2, where the CPU path (and NumPy argmax, as py4DSTEM) returns 1. The two
backends could then report different in-plane angles for the same pattern.

The reduction now compares (score, bin) pairs: a larger score wins, an equal
score keeps the smaller bin. NaN and -inf enter as -FLT_MAX, so they never
win, as in the CPU scan that starts at -greatestFiniteMagnitude; an all-NaN
row gives bin 0 and -FLT_MAX / n on both paths. The scan runs on unscaled
values and only the winner is divided by n, the same order as the CPU.
Threadgroup memory lengths are at least 16 bytes (Metal requires multiples
of 16; n * 4 is not one below 4 bins).

A package seam, ACOMMetalKernels.bestBins, runs the kernels on planted ring
spectra; ACOMMetalArgmaxTieTests plants an exact tie (red before: bin 2 in
both passes) and an all-NaN correlation.

Fixes #62

Done: Metal argmax ties and NaN follow the CPU rule, tested. Next: add the tie rule to the P31 row of docs/parity.md once the other parity.md PRs land.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Paullo9 Paullo9 self-assigned this Oct 31, 2026
@Paullo9
Paullo9 changed the base branch from issue-8-metal-timing to main October 31, 2026 14:09
Paullo9 and others added 2 commits October 31, 2026 16:17
Main now holds #61 as a squash. The conflicted ACOMMetal.swift keeps the branch side: main has it exactly as the first branch commit wrote it, and the tie fix builds on that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Paullo9
Paullo9 merged commit bc22dc5 into main Nov 1, 2026
6 checks passed
@Paullo9
Paullo9 deleted the issue-62-metal-argmax-ties branch November 1, 2026 09:51
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.

ACOM Metal argmax may not keep the lowest bin on an exact tie

1 participant