In one sentence: koine_machines.inference has no MPS branch, so Apple Silicon silently runs the published ink_9um checkpoints on the CPU while the GPU sits idle.
I was trying to: Run the published ink_9um checkpoint through the documented First Letters inference workflow on an Apple Silicon Mac, then shorten the turnaround between repeat inference runs.
Using: Current merge-ink-pipelines, torch 2.14.0, an Apple M5 Pro with 24 GB unified memory, a 512x512x20 uint8 zarr input, and python -m koine_machines.inference.infer input.zarr step-075000.pth out.tif --batch-size 1 --no-compile.
What happened: Inference completed with correct output, but selected the CPU without printing a warning; the MPS GPU stayed idle.
What I expected or needed: Automatic MPS selection on Apple Silicon, an explicit device log, and a way to request a device without changing the CUDA default for other users.
Evidence / reproduction: The source path and measured CPU/MPS comparison are below, together with a tested patch.
Details
Both ink-detection inference entrypoints resolve the device with CUDA or nothing.
koine_machines/inference/infer.py, in prepare_model_for_inference:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
infer_full3d_tifxyz.py has the same line in prepare_model. Autocast is gated the same
way, in both files:
autocast_enabled = bool(amp and device.type == "cuda")
...
torch.autocast(device_type="cuda", enabled=True, dtype=amp_dtype)
So on any Mac both entrypoints take the CPU path and disable autocast. The run succeeds and
the output is correct, so there is nothing to notice.
Measured on hardware
Apple M5 Pro, 24 GB unified memory, macOS 26.5.1, torch 2.14.0, checkpoint
hybrid_3d2d-seed42/step-075000.pth from scrollprize/ink_9um, through the documented CLI:
Five timed runs per device after a discarded warm-up, 49 blocks per run, block/s:
| device |
min |
median |
max |
mean |
stdev |
log line |
| mps |
33.79 |
33.88 |
33.99 |
33.88 |
0.08 |
mps autocast enabled for inference with dtype=float16 |
| cpu |
12.65 |
12.83 |
13.18 |
12.88 |
0.22 |
Autocast disabled for inference (device=cpu) |
The cpu row is current merge-ink-pipelines on this machine. Comparing the two output
TIFFs pixel by pixel: maxdiff=1 on uint8, mean absolute difference 0.084, no pixel
differing by more than one level, which is fp16 accumulation.
Median against median that is 2.64x, on a 512x512 input giving 49 blocks. Model load and
imports dominate wall clock at that size, so a real segment should do better rather than worse.
Why this one is worth fixing
The First Letters workflow post is the front door for new entrants, and it has readers verify
that torch sees CUDA. Its only tuning advice is to lower --batch-size if GPU memory runs
out. Neither statement applies on a Mac, so the people most exposed are the ones with the
least context to spot it: someone following the guide on their own laptop, concluding ink
detection is slow, and running fewer refinement rounds because of it.
The rest of the repo already resolves MPS
vesuvius/src/vesuvius/models/utilities/get_accelerator.py does cuda, then mps, then cpu,
and logs Using MPS device (Apple Silicon). nnU-Net under segmentation/models/arch/nnunet
handles MPS as well. koine_machines is a separate package that predates the helper, so it
never picked it up. Nothing new needs inventing here.
Three things I hit while writing a fix
Recording these in case they save someone the same round trip:
- Under MPS, setting
pin_memory makes torch emit 'pin_memory' argument is set as true but not supported on MPS now, device pinned on every loader iteration, while pinning nothing.
It has to stay gated on CUDA.
- CPU autocast is bfloat16-only, and on the machines that reach the CPU branch I could not
measure it beating plain float32, so leaving it off seems right.
--gpus indexes CUDA ordinals and feeds nn.DataParallel, so it cannot mean anything on
MPS. Raising on --gpus 0 from a Mac is better than quietly resolving it somewhere else.
On the autocast side, torch.autocast(device_type="mps") ran for float16 and for bfloat16,
and the backend default resolves to float16. torch.compile also works on MPS here. I have
not tried it in combination with autocast.
A working patch
I have a fix running, with tests, at
https://github.com/SurgeFok/vesuvius-mps-inference (MIT). It adds
koine_machines/common/accelerator.py mirroring get_accelerator, routes both entrypoints
through it, passes device_type=device.type to autocast, and adds a --device flag with
auto/cuda/mps/cpu. The applied diff is mps-inference.patch in that repo.
One limitation, stated plainly: I have no machine with a usable CUDA GPU, so I could not run
the CUDA branch, which is the default path. I kept its control flow identical and reproduced
both error strings character for character including visible device count is N, and there
are five tests that fake torch.cuda.is_available and device_count to check the decisions.
Somebody with a CUDA box running pytest tests/ -v would close that.
Happy to open a PR if outside PR creation is working again (#1743), otherwise the patch is
there to take.
In one sentence:
koine_machines.inferencehas no MPS branch, so Apple Silicon silently runs the publishedink_9umcheckpoints on the CPU while the GPU sits idle.I was trying to: Run the published
ink_9umcheckpoint through the documented First Letters inference workflow on an Apple Silicon Mac, then shorten the turnaround between repeat inference runs.Using: Current
merge-ink-pipelines, torch 2.14.0, an Apple M5 Pro with 24 GB unified memory, a 512x512x20 uint8 zarr input, andpython -m koine_machines.inference.infer input.zarr step-075000.pth out.tif --batch-size 1 --no-compile.What happened: Inference completed with correct output, but selected the CPU without printing a warning; the MPS GPU stayed idle.
What I expected or needed: Automatic MPS selection on Apple Silicon, an explicit device log, and a way to request a device without changing the CUDA default for other users.
Evidence / reproduction: The source path and measured CPU/MPS comparison are below, together with a tested patch.
Details
Both ink-detection inference entrypoints resolve the device with CUDA or nothing.
koine_machines/inference/infer.py, inprepare_model_for_inference:infer_full3d_tifxyz.pyhas the same line inprepare_model. Autocast is gated the sameway, in both files:
So on any Mac both entrypoints take the CPU path and disable autocast. The run succeeds and
the output is correct, so there is nothing to notice.
Measured on hardware
Apple M5 Pro, 24 GB unified memory, macOS 26.5.1, torch 2.14.0, checkpoint
hybrid_3d2d-seed42/step-075000.pthfromscrollprize/ink_9um, through the documented CLI:Five timed runs per device after a discarded warm-up, 49 blocks per run, block/s:
mps autocast enabled for inference with dtype=float16Autocast disabled for inference (device=cpu)The
cpurow is currentmerge-ink-pipelineson this machine. Comparing the two outputTIFFs pixel by pixel:
maxdiff=1on uint8, mean absolute difference 0.084, no pixeldiffering by more than one level, which is fp16 accumulation.
Median against median that is 2.64x, on a 512x512 input giving 49 blocks. Model load and
imports dominate wall clock at that size, so a real segment should do better rather than worse.
Why this one is worth fixing
The First Letters workflow post is the front door for new entrants, and it has readers verify
that torch sees CUDA. Its only tuning advice is to lower
--batch-sizeif GPU memory runsout. Neither statement applies on a Mac, so the people most exposed are the ones with the
least context to spot it: someone following the guide on their own laptop, concluding ink
detection is slow, and running fewer refinement rounds because of it.
The rest of the repo already resolves MPS
vesuvius/src/vesuvius/models/utilities/get_accelerator.pydoes cuda, then mps, then cpu,and logs
Using MPS device (Apple Silicon). nnU-Net undersegmentation/models/arch/nnunethandles MPS as well.
koine_machinesis a separate package that predates the helper, so itnever picked it up. Nothing new needs inventing here.
Three things I hit while writing a fix
Recording these in case they save someone the same round trip:
pin_memorymakes torch emit'pin_memory' argument is set as true but not supported on MPS now, device pinnedon every loader iteration, while pinning nothing.It has to stay gated on CUDA.
measure it beating plain float32, so leaving it off seems right.
--gpusindexes CUDA ordinals and feedsnn.DataParallel, so it cannot mean anything onMPS. Raising on
--gpus 0from a Mac is better than quietly resolving it somewhere else.On the autocast side,
torch.autocast(device_type="mps")ran for float16 and for bfloat16,and the backend default resolves to float16.
torch.compilealso works on MPS here. I havenot tried it in combination with autocast.
A working patch
I have a fix running, with tests, at
https://github.com/SurgeFok/vesuvius-mps-inference (MIT). It adds
koine_machines/common/accelerator.pymirroringget_accelerator, routes both entrypointsthrough it, passes
device_type=device.typeto autocast, and adds a--deviceflag withauto/cuda/mps/cpu. The applied diff ismps-inference.patchin that repo.One limitation, stated plainly: I have no machine with a usable CUDA GPU, so I could not run
the CUDA branch, which is the default path. I kept its control flow identical and reproduced
both error strings character for character including
visible device count is N, and thereare five tests that fake
torch.cuda.is_availableanddevice_countto check the decisions.Somebody with a CUDA box running
pytest tests/ -vwould close that.Happy to open a PR if outside PR creation is working again (#1743), otherwise the patch is
there to take.