eXMC 0.3.0 is tagged. It ships the thing the previous
year of writing on this site has been circling: a No-U-Turn sampler
that runs on the GPU through Vulkan, on anything with a Vulkan driver,
whether or not that machine has ever heard of CUDA. Set
EXMC_COMPILER=vulkan and the leapfrog integrator that used
to run op-by-op on the CPU synthesizes into a single fused f64 chain
shader and dispatches to whatever silicon is present — an RTX
3060 Ti on Linux, a GeForce GT 650M on FreeBSD, a laptop
through MoltenVK. EXLA is still there for the machines that have it.
Vulkan is there for the machines that don’t.
This is the release that closes a story about precision. Three tags ago — back when eXMC’s releases were still named after Greek letters — there was a commit called f32 is bad for business. It was right. Consumer GPUs charge a steep tax for double precision and the temptation to sample in f32 is real, but a Bayesian posterior computed in f32 drifts in the tails, and the tails are where the interesting inference lives. 0.3.0 finishes the migration: the VulkanoBackend is f64-only compute now — elementwise, reductions, matmul, and the leapfrog chain synth all run in double, f32 inputs are accepted and cast — and EMLX, the f32-only Apple Metal path, has been retired. The default precision is f64 end to end. The tag that complained about f32 got its wish.
Making it installable was its own release
A backend that only its author can build is a demo. Half the work of
0.3.0 was making a stranger’s mix deps.get succeed.
eXMC now requires {:nx, "~> 0.13"}; nx_vulkan, the
sibling repo that supplies the Vulkan backend, was still constrained to
~> 0.10 or ~> 0.11 or ~> 0.12. Those two
constraints don’t intersect. A fresh checkout resolved to a
version conflict and stopped — not a runtime error a test would
catch, but a dependency-graph contradiction that only appears when
someone who isn’t you clones the thing.
The fix was small and the verification was the point: bump nx_vulkan to
{:nx, "~> 0.13"}, publish it, pin eXMC’s lockfile
to the published commit, and then actually clone eXMC into a scratch
directory with no path overrides and run mix deps.get.
It resolved nx 0.13.0 and pulled nx_vulkan from GitHub in
under a second. That clean-room resolution is the difference between
“works on my machine” and a release.
128 bytes, and what wouldn’t fit
The last lesson of the release came, as these things do, from the
oldest hardware in the fleet. The verification suite for a probabilistic
backend is posteriordb
— thirty-three models with gold-standard Stan reference draws. On
EXLA the GPU run was a clean 33/33. Then the same suite went to
mac-247, a 2013 GT 650M on FreeBSD, under Vulkan. The
first model crashed.
CRASH eight_schools-eight_schools_noncentered
no match of right hand side value: {:error, :push_too_large}
Eight Schools is the hierarchical hello-world of Bayesian statistics — ten free parameters. The synthesizer packs each model’s prior parameters into a Vulkan push-constants block, which the specification caps at 128 bytes. At f32 that held roughly twenty-eight floats; the move to f64 halved it to fourteen. Ten parameters, two floats each, plus a header, overflowed. And the code that packed the block did the most Elixir thing possible — it pattern-matched the happy path:
{:ok, push, _bytes} = Push.pack(spec)
When pack returned {:error, :push_too_large},
the match failed and the whole sample crashed at dispatch time. The
model was fine. The model was, in fact, synthesizable —
just too wide for the fused shader. The right behavior is not to crash
and not to raise; it is to notice at synthesis time that the block
won’t fit, and quietly route the model through the slower per-op
path instead. Correct, if unhurried, beats fast and dead.
So the synthesizer learned to check Push.pack before it
commits, return {:unsupported, :push_too_large}, and let
the Plan B′ guard treat that as a licensed fallback rather
than a fatal error. Re-run on the same 2013 Kepler:
[warning] push-constants overflow (>128 B at f64) — falling back
to per-op vulkan sampling for this IR (slower but correct)
FAIL eight_schools-eight_schools_noncentered 5552ms ...
FAIL, not CRASH — and the FAIL is only the tolerance check on a twenty-warmup smoke run, not a defect. The model sampled. On a thirteen-year-old GPU, through a driver stack that isn’t supposed to exist, a hierarchical posterior that didn’t fit in the fast path took the slow path and came back with an answer. That is exactly the promise: not that every model is fast on every GPU, but that no model falls over on a GPU that can hold it.
What’s in the box
- Vulkan f64 chain-shader sampling — NUTS on
the GPU via
VulkanoBackend, alongside EXLA, selected withEXMC_COMPILER=vulkan. - Nx 0.13, EMLX dropped, f64 default; the whole dependency graph resolves from a clean clone.
- Fixes — Gamma/Beta priors route through the
synth path; measurable-matmul no longer leaks a host-side index
tensor into a crash under nx 0.13;
push_too_largedegrades to per-op instead of crashing; the no-observation sentinel is f64. - Validation — posteriordb 33/33 on EXLA-GPU; the Vulkan fallback verified on the FreeBSD Kepler; 755-test vulkan suite on the reference host with residual failures confined to known synthesis-coverage buckets.
The backend that this release finishes was the subject of The Backend That Didn’t Need to Know; the proof that a 2013 FreeBSD Mac Pro could beat a modern Linux workstation at inference was Vulkan on FreeBSD: the Proof; the verification standard that made 33/33 mean something was Two Backends, One Posterior. This post is the shipping note. The release lives at github.com/borodark/eXMC — v0.3.0, and the Vulkan backend it depends on is borodark/nx_vulkan.
A posterior is just a distribution you have to compute. 0.3.0 means you can compute it on the GPU you have, not the GPU a vendor decided you should have bought.