On-device Pólya-Gamma sampling for JAX, as an XLA FFI custom call.
pgjax.pg_sample(h, z, key) draws omega ~ PG(h, z) from inside a JIT'd /
lax.scan-ed / pmap-ed program with no host round-trip — the native
replacement for jax.pure_callback(random_polyagamma, ...), which pays a
device↔host round-trip on every Pólya-Gamma Gibbs sweep and serializes under
jax.pmap.
import jax, pgjax
jax.config.update("jax_enable_x64", True)
@jax.jit
def omega_update(h, z, key):
return pgjax.pg_sample(h, z, key)- Small integer
h(incl. Bernoulli/logith = 1): the exact Devroye sampler forPG(1, z)(Polson, Scott & Windle 2013), summedhtimes. - Real
h(e.g. Negative-Binomialh = y + alpha): the exact infinite-sum (Gamma) representation truncated atKterms with a moment-matched Gamma tail correction for the remainder. The tail mean/variance have closed forms (Σ 1/d_k = (π/2a)tanh(πa), and itsa-derivative), so the correction cancels the truncation bias in the mean — it is precisely that bias (truncation with no tail term) that collapsesalphain NB models when omitted. - Large
h: the saddlepoint rejection sampler (Windle, Polson & Scott 2014, arXiv:1405.0506). A two-piece bounding-kernel envelope proposes, and the saddlepoint density approximation accepts, giving O(1) per accepted draw versus the O(K) Gamma sum. Used automatically whenh >= 8or (h > 4and|z| <= 4) — the hybrid dispatch thresholds validated inzoj613/polyagamma.
Validated against random_polyagamma over h ∈ [0.5, 40] × z ∈ [-8, 8]:
KS ≤ 0.009, relative mean error ≤ 0.34%. The saddlepoint path is validated to
KS < 0.02 and relative mean error < 2% over h ∈ {8,…,40} × z ∈ {0,1,-4,5}.
The RNG is a per-call xoshiro256** seeded (via splitmix64) from the JAX PRNG
key, so draws are reproducible and each call owns its stream — safe under the
concurrent per-device calls jax.pmap issues.
- ✅ Exact
PG(1, z)/ integerh(Devroye), realh(tail-corrected Gamma sum), and largeh(saddlepoint) — covers the logit (h=1), Negative-Binomial (h=y+alpha), and large-shape regimes. - Hybrid dispatch is automatic: the appropriate sampler is selected from
(h, z)with no API change.
CPU-only (the sampler is cheap scalar work; the point is to keep it on-device next to the rest of a JIT'd Gibbs sweep).