A local inference connection names the device it runs on, and the vocabulary of legal
device names is closed. Today that vocabulary is cpu, cuda, and cuda:N for the second
GPU on a machine that has one. Apple Silicon's GPU, which PyTorch addresses as mps, is not
in it, so a Mac with an M-series chip runs SAM 2 and Grounding DINO on its CPU cores while the
GPU sits idle. The interactive click-to-suggest loop is where this is felt: a warm suggest
click is dominated by the model's own decode (#571), and that decode is exactly the work a
GPU does faster.
This is a deliberate exclusion, not an oversight
mps is refused on purpose. src/visionset/kernel/domain/inference.py closes the vocabulary
with a regular expression and states the reason beside it:
DEVICE_PATTERN: Final = re.compile(r"^(?:cpu|cuda(?::\d+)?)$")
"""Every device string this build can honestly run on.
A pattern rather than an enum because of the one member that is not a fixed
word. What is *not* here is the point: ``gpu``, ``mps``, ``auto`` and every
typo were accepted before and then quietly fell back to the CPU in full
precision — a connection that names a runtime it never gets.
"""
The rule that docstring states is the right one: a device belongs in the vocabulary when this
build can honour it, and a name that silently degrades to something else is worse than a
refusal. gpu and auto still cannot be honoured and stay out. What changes is that mps
can be honoured, once the adapters know how — which is what this issue asks for.
What has to change
The device vocabulary is written down in more than one place, and all of them move together
or the halves disagree.
- The kernel domain owns the vocabulary: the pattern above,
OFFERED_DEVICES, and
precisions_for, which conditions the legal precisions on the device. mps is
full-precision only — MPS has no float64 and its bfloat16 support is inconsistent — so
precisions_for("mps") answers fp32 alone, and the existing cross-field validator then
refuses an mps connection asking for fp16 with the sentence it already writes for
cpu + fp16.
- Both local adapters decide at load time where a model actually goes.
LocalSamProvider
and LocalTransformersProvider each carry a private _resolved_device, and the two are
identical: a connection asking for a device this machine does not offer gets the CPU and a
warning naming the connection, and half precision is dropped on the way. Adding a second
device to two identical copies is the moment to promote them into one shared resolver
rather than to write the new branch twice. Half precision stays CUDA-only, so it must not
reach mps by the widening.
PYTORCH_ENABLE_MPS_FALLBACK has to be set, because an operator MPS has not implemented
otherwise raises instead of running on the CPU for that one operation. It is read when the
library initialises, so it has to be set before torch is imported rather than before the
first tensor operation.
- The surfaces that publish the vocabulary: the connection form's device list in
frontend/ui-core/src/screens/inferenceCatalog.ts, the --device help text on
visionset inference create and update, and the closed-vocabulary prose in
docs/inference.md, docs/cli.md and docs/install.md.
What does not change
The installation does not. The local-inference extra declares version floors and pins
nothing platform-specific, and the macOS arm64 PyTorch wheels carry MPS already, so there is
no install-time branch to add and no second index to configure. A user on Apple Silicon
installs the same extra everybody else installs.
The default does not either. A new connection still opens on cpu; mps is offered in the
picker rather than chosen for the user.
Acceptance
- A local connection may name
mps, and one that does runs on the GPU of a machine that has
one.
mps accepts fp32 and refuses fp16, with the refusal naming the device and the
precisions it does run in.
- An
mps connection on a machine without MPS falls back to the CPU and says so at WARNING,
which is the behaviour cuda already has on a machine with no GPU.
gpu and auto are still refused.
- The device-resolution rule exists once, not once per adapter, and has tests — it has none
today in either copy.
docs/inference.md has a section covering which devices are supported, what a connection
names, what happens when the named device is absent, and that Apple Silicon needs nothing
installed or configured beyond picking the device.
Continuous integration has neither a GPU nor an Apple Silicon runner, so the resolution rule
has to be testable with the availability answers injected, the way CONTRIBUTING.md already
requires of anything that would otherwise need real hardware.
A local inference connection names the device it runs on, and the vocabulary of legal
device names is closed. Today that vocabulary is
cpu,cuda, andcuda:Nfor the secondGPU on a machine that has one. Apple Silicon's GPU, which PyTorch addresses as
mps, is notin it, so a Mac with an M-series chip runs SAM 2 and Grounding DINO on its CPU cores while the
GPU sits idle. The interactive click-to-suggest loop is where this is felt: a warm suggest
click is dominated by the model's own decode (#571), and that decode is exactly the work a
GPU does faster.
This is a deliberate exclusion, not an oversight
mpsis refused on purpose.src/visionset/kernel/domain/inference.pycloses the vocabularywith a regular expression and states the reason beside it:
The rule that docstring states is the right one: a device belongs in the vocabulary when this
build can honour it, and a name that silently degrades to something else is worse than a
refusal.
gpuandautostill cannot be honoured and stay out. What changes is thatmpscan be honoured, once the adapters know how — which is what this issue asks for.
What has to change
The device vocabulary is written down in more than one place, and all of them move together
or the halves disagree.
OFFERED_DEVICES, andprecisions_for, which conditions the legal precisions on the device.mpsisfull-precision only — MPS has no float64 and its bfloat16 support is inconsistent — so
precisions_for("mps")answersfp32alone, and the existing cross-field validator thenrefuses an
mpsconnection asking forfp16with the sentence it already writes forcpu+fp16.LocalSamProviderand
LocalTransformersProvidereach carry a private_resolved_device, and the two areidentical: a connection asking for a device this machine does not offer gets the CPU and a
warning naming the connection, and half precision is dropped on the way. Adding a second
device to two identical copies is the moment to promote them into one shared resolver
rather than to write the new branch twice. Half precision stays CUDA-only, so it must not
reach
mpsby the widening.PYTORCH_ENABLE_MPS_FALLBACKhas to be set, because an operator MPS has not implementedotherwise raises instead of running on the CPU for that one operation. It is read when the
library initialises, so it has to be set before torch is imported rather than before the
first tensor operation.
frontend/ui-core/src/screens/inferenceCatalog.ts, the--devicehelp text onvisionset inference createandupdate, and the closed-vocabulary prose indocs/inference.md,docs/cli.mdanddocs/install.md.What does not change
The installation does not. The
local-inferenceextra declares version floors and pinsnothing platform-specific, and the macOS arm64 PyTorch wheels carry MPS already, so there is
no install-time branch to add and no second index to configure. A user on Apple Silicon
installs the same extra everybody else installs.
The default does not either. A new connection still opens on
cpu;mpsis offered in thepicker rather than chosen for the user.
Acceptance
mps, and one that does runs on the GPU of a machine that hasone.
mpsacceptsfp32and refusesfp16, with the refusal naming the device and theprecisions it does run in.
mpsconnection on a machine without MPS falls back to the CPU and says so at WARNING,which is the behaviour
cudaalready has on a machine with no GPU.gpuandautoare still refused.today in either copy.
docs/inference.mdhas a section covering which devices are supported, what a connectionnames, what happens when the named device is absent, and that Apple Silicon needs nothing
installed or configured beyond picking the device.
Continuous integration has neither a GPU nor an Apple Silicon runner, so the resolution rule
has to be testable with the availability answers injected, the way
CONTRIBUTING.mdalreadyrequires of anything that would otherwise need real hardware.