The server segfaulted a few seconds into every live scan that used the crop
lane. The crash report put the fault in libtorch's Metal shader library
(`MetalShaderLibrary::exec_unary_kernel` inside an unordered_map insert),
reached from `copy_cast_kernel_mps` under `clone`/`reshape`.
Reproduced deterministically: eight threads calling `embed()` on a fresh
process crash it, while the same eight threads after one warm forward are
fine. Metal's shader library is process-global and is not thread-safe while
kernels are still being compiled into it, which is exactly what the first
forward does. The detector already serializes its predict for this reason;
the gallery did not.
Two changes:
* A module-level `_MPS_INFER_LOCK` covers the whole MPS region of a forward -
model load, `.to(device)`, the forward, and the normalize that follows it.
Normalize dispatches its own Metal kernels, so leaving it outside the lock
left the same first-compile race open. Module-level because the state being
protected is the process's, not one gallery's.
* A cache hit skips the embedding forward, so the model used to be built and
the kernels compiled by whichever classify worker asked first - up to eight
racing into one process-wide cache. The load path now warms the backbone
single-threaded under the existing lock, on the cache-hit branch too. The
readiness path loads the gallery before the socket accepts, so the warmup
is free. It is Metal-only and best-effort: a CPU gallery has no shader
library to compile, and a cache hit is served without a usable backbone by
contract.
The regression test records overlap rather than timing: a fake backbone that
sleeps and counts concurrent entries, run from four threads, asserting the
count never exceeds one. It fails on the previous gallery.py and needs no MPS.