Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions spyde/ebsd/refine.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,10 +105,19 @@ def __init__(self, detector=(60, 60), pc=(0.5, 0.5, 0.55), *,
refl = reflectors if reflectors is not None else cubic_reflectors()
r = detector_directions(detector, pc).reshape(-1, 3)
self.reflectors = refl
self.r = torch.as_tensor(r, dtype=dtype, device=device)
self.normals = torch.as_tensor(refl.normals, dtype=dtype, device=device)
self.weights = torch.as_tensor(refl.weights, dtype=dtype, device=device)
self.widths = torch.as_tensor(refl.widths, dtype=dtype, device=device)
# Under the lock like every other device submission (device_lock.py):
# these uploads are Metal blits, and BOTH simulate_dictionary and
# refine_orientations construct the simulator BEFORE their per-chunk
# locks — so on MPS this was the one EBSD call site that submitted
# unserialised, concurrent with any live band-overlay match.
with accelerator_lock(device):
self.r = torch.as_tensor(r, dtype=dtype, device=device)
self.normals = torch.as_tensor(refl.normals, dtype=dtype,
device=device)
self.weights = torch.as_tensor(refl.weights, dtype=dtype,
device=device)
self.widths = torch.as_tensor(refl.widths, dtype=dtype,
device=device)
self.shape = tuple(detector)
self.device = device
self.dtype = dtype
Expand Down
39 changes: 39 additions & 0 deletions spyde/tests/migrated/test_device_lock.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,45 @@ def test_average_dot_product_map_locks(self, monkeypatch):
rng.normal(size=(3, 3, 6, 6)).astype("float32"), device="cpu")
assert seen, "ADP ran unserialised"

def test_the_band_simulator_upload_locks(self, monkeypatch):
"""``BandSimulator.__init__`` uploads the detector geometry to the
device — and both ``simulate_dictionary`` and ``refine_orientations``
construct it BEFORE their per-chunk locks, so it was the one EBSD
call site that submitted to Metal unserialised (the macOS-CI SIGABRT
at the ebsd_wizard tests)."""
pytest.importorskip("torch")
from spyde.ebsd import refine

seen = self._spy(monkeypatch, refine)
refine.BandSimulator((8, 8), device="cpu")
assert seen, "BandSimulator uploaded to the device unserialised"

def test_simulate_dictionary_locks(self, monkeypatch):
"""The dictionary build runs on the ebsd-build-dictionary worker
thread while the previous wizard's band overlay may still be matching
— every one of its submissions must serialise."""
pytest.importorskip("torch")
from spyde.ebsd import indexing, refine

seen = self._spy(monkeypatch, indexing)
seen_ctor = self._spy(monkeypatch, refine)
out = indexing.simulate_dictionary(
np.zeros((3, 3)), detector=(8, 8), device="cpu")
assert out.shape == (3, 8, 8)
assert seen, "dictionary simulation submitted unserialised"
assert seen_ctor, "its BandSimulator construction submitted unserialised"

def test_refine_orientations_locks(self, monkeypatch):
pytest.importorskip("torch")
from spyde.ebsd import refine

seen = self._spy(monkeypatch, refine)
rng = np.random.default_rng(0)
refine.refine_orientations(
rng.normal(size=(2, 8, 8)).astype("float32"), np.zeros((2, 3)),
detector=(8, 8), device="cpu", steps=2)
assert seen, "refinement submitted unserialised"


def _fit_a_tiny_model(**kw):
"""A 4-position fit, small enough to be instant on the CPU."""
Expand Down
23 changes: 23 additions & 0 deletions spyde/tests/migrated/test_ebsd_wizard.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"""
from __future__ import annotations

import os
import time

import numpy as np
Expand All @@ -23,6 +24,28 @@
from spyde.data import ebsd_patterns, ground_truth


@pytest.fixture(autouse=True)
def _cpu_device(monkeypatch):
"""Force the EBSD device to CPU for this file — WIRING tests, like every
sibling (test_ebsd_indexing / test_ebsd_refine pin ``device="cpu"`` on
every call; kernel accuracy is theirs, not this file's).

This file never pinned a device, so on an Apple-Silicon runner every
handler resolved ``default_device()`` -> "mps" — the only place in the
whole migrated suite that touched Metal. Under pytest that means the
build worker, the band-overlay engine thread, up to 8 dask threads and
per-test teardown of MPS-resident tensors all churn the device across 7
Session lifecycles — the multi-threaded Metal profile CLAUDE.md documents
as fatally racy — and macOS CI died with SIGABRT here when the
macos-latest image rolled. Accelerator work belongs in a subprocess (the
test_vector_orientation_gpu.py pattern), not under the pytest harness.

Overridable: a maintainer reproducing on a Mac sets SPYDE_EBSD_DEVICE=mps.
"""
if not os.environ.get("SPYDE_EBSD_DEVICE"):
monkeypatch.setenv("SPYDE_EBSD_DEVICE", "cpu")


def _wait(pred, timeout=120.0, interval=0.05):
end = time.time() + timeout
while time.time() < end:
Expand Down
Loading