A Posterior on Any GPU

July 2026 eXMC 0.3.0 Nx Vulkan NUTS FreeBSD

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

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.