Skip to main content

stanli

The Stan Language Interpreter. Compile and sample Stan models with no C++ toolchain on the machine.

PyPI Python License wheels

pip install stanli

That is the whole install. No compiler, no make, no CmdStan checkout, no multi-minute first-run build. One wheel, one shared library, roughly five megabytes.

import stanli

model = stanli.Model(stan_file="eight_schools.stan", data="data.json")
draws = model.sample(seed=1, warmup=1000, samples=1000)

draws["mu"].mean()      # one numpy array of draws per constrained parameter

Model preparation takes milliseconds, so the first draw arrives about 20x sooner than a toolchain that compiles C++ per model.

How it works

Every Stan model is a composition of a fixed vocabulary of operations: densities, constraint transforms, linear algebra, elementwise math. stanli ships those precompiled and turns each model into data, a static graph of ops over flat preallocated buffers, instead of generating and compiling C++ per model.

model.stan + data.json
  |  stanc3, the official OCaml compiler, linked into the library
  v
transformed MIR
  |  lowering: transformed data evaluated eagerly, data-bound loops unrolled,
  |            then periodic regions re-rolled back into vectorized ops
  v
op graph over preallocated value/adjoint arenas
  |  forward sweep = log density, reverse sweep = gradient
  v
NUTS with diagonal-metric adaptation -> draws

The graph doubles as the autodiff tape, so a reverse sweep is a backwards loop over an array rather than a walk through a pointer-chasing tape, and steady-state gradient evaluation allocates nothing.

Two things are not reimplemented, which is what makes the results trustworthy: the compiler is the real stanc3, linked in-process, so the Stan language behaves as the official toolchain makes it behave; and the math is unmodified stan-math, the same code CmdStan runs.

Correctness

Nothing here ships on "looks close".

118 of 120 posteriordb models are differentially verified against CmdStan: same model, same data, same evaluation point, comparing the log density and every single gradient component. 45 agree bitwise. The worst deviation across the entire corpus is 2.6e-12 relative.

The two exceptions are documented rather than hidden. sir's ODE solution dips about 1e-9 below a declared lower bound at the shared evaluation point, where CmdStan rejects it too; kronecker_gp matches on the log density and 436 of 438 gradients, differing on the two that flow through eigenvectors of a nearly degenerate covariance matrix.

Full per-model accuracy table: docs/corpus-status.md

Performance

Per-gradient latency against CmdStan, same models, same evaluation point, both sides -O3 with FP contraction pinned off:

model params stanli CmdStan speedup
radon_pooled 3 53.1 us 328.1 us 6.2x
radon_hierarchical_intercept_centered 391 112.1 us 574.2 us 5.1x
arK 7 2.5 us 11.9 us 4.8x
radon_county_intercept 388 90.4 us 431.5 us 4.8x
bym2_offset_only 3845 40.0 us 125.4 us 3.1x
eight_schools_noncentered 10 0.28 us 0.64 us 2.3x
kidscore_momiq 3 1.9 us 3.9 us 2.0x
lsat_model 1006 46.6 us 91.2 us 2.0x
wells_dist100ars_model 3 17.3 us 18.4 us 1.1x
low_dim_gauss_mix 5 186.4 us 98.9 us 0.53x

The wins come from op granularity. CmdStan's var tape allocates, walks, and frees one node per scalar operation per leapfrog step; stanli pays a fixed cost per op, and a vectorized statement over N elements amortizes that to nothing. The one loss is honest and understood: low_dim_gauss_mix writes log_mix(theta, normal_lpdf(...), ...) per observation, a shape the re-rolling pass correctly refuses to vectorize today.

ODE models are the other place stanli is still behind. An ODE right-hand side is the one user function that cannot be inlined at lowering time, since the integrator picks the times; it now compiles into a flat register machine instead of being tree-walked, and the forward sweep keeps the sensitivities it was already computing instead of solving twice. Together that is 29x to 39x faster than the tree-walking interpreter it replaces, which puts lotka_volterra and soil_incubation at 0.58x and 0.63x of CmdStan rather than 0.015x.

Method and full table: docs/benchmarks.md

API

The surface is small on purpose.

import stanli

# A path to a .stan file, or the model source directly.
model = stanli.Model(stan_file="model.stan", data="data.json")
model = stanli.Model(stan_code=src, data={"J": 8, "y": y, "sigma": sigma})

model.n_unconstrained               # length of the unconstrained vector
model.constrained_names             # ['mu', 'tau', 'theta.1', ...]

lp, grad = model.log_prob_grad(q)   # sampling log density and its gradient

draws = model.sample(seed=1, warmup=1000, samples=1000, delta=0.8)
draws["mu"]                         # ndarray of length `samples`

data accepts a path to a JSON file or a dict of Python scalars, lists, and numpy arrays. sample returns one array of constrained draws per scalar parameter, named the way CmdStan names them, so theta declared as vector[8] arrives as theta.1 through theta.8.

Platforms

Wheels for macOS (arm64 and x86_64) and Linux (x86_64 and aarch64, manylinux_2_28). Windows is not built yet; it needs a mingw-w64 toolchain, because stan-math does not build under MSVC.

The installed library is 13.8 MB, which is the trade this design makes: ship the compiler and every kernel once, so that nothing is ever built on the user's machine. Roughly half of that is the embedded stanc3 and somewhat under half is stan-math. The interpreter and NUTS together are about 410 KB.

Status

Early, and deliberately narrow. The sampler is Stan's own NUTS with diagonal-metric adaptation. Known limits, stated plainly:

  • sample() returns declared parameters only. Transformed parameters and generated quantities are computed by the runtime and written by the command line tool, but are not exposed through the Python API yet, so the non-centered eight schools gives you mu, tau, and theta_tilde, not theta.
  • No variational inference, no optimization, no multi-chain threading.
  • No convergence diagnostics. Pair it with ArviZ or similar for now.

What is here is verified against CmdStan model by model, and every number on this page is reproducible from the repository.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distributions

No source distribution files available for this release.See tutorial on generating distribution archives.

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

stanli-0.1.0-py3-none-manylinux_2_28_x86_64.whl (5.4 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ x86-64

stanli-0.1.0-py3-none-manylinux_2_28_aarch64.whl (5.4 MB view details)

Uploaded Python 3manylinux: glibc 2.28+ ARM64

stanli-0.1.0-py3-none-macosx_11_0_arm64.whl (4.8 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

stanli-0.1.0-py3-none-macosx_10_15_x86_64.whl (5.9 MB view details)

Uploaded Python 3macOS 10.15+ x86-64

File details

Details for the file stanli-0.1.0-py3-none-manylinux_2_28_x86_64.whl.

File metadata

File hashes

Hashes for stanli-0.1.0-py3-none-manylinux_2_28_x86_64.whl
Algorithm Hash digest
SHA256 87c03b610b6de764a2ccc6ef43ba4a3f79f9852e10b7a0cb635b3c61772940cd
MD5 addca47edbb6271ebed229d4bc469c36
BLAKE2b-256 b6a53e04b412de793e4576aa2f3cbd5bcd43af2d0c9a4fc65b1c4e44c1285f39

See more details on using hashes here.

Provenance

The following attestation bundles were made for stanli-0.1.0-py3-none-manylinux_2_28_x86_64.whl:

Publisher: wheels.yml on seantalts/stanli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stanli-0.1.0-py3-none-manylinux_2_28_aarch64.whl.

File metadata

File hashes

Hashes for stanli-0.1.0-py3-none-manylinux_2_28_aarch64.whl
Algorithm Hash digest
SHA256 3093e681b3d641165c99ca69294e0ed1210bcf2fe0eb9673fbcf2ed7fafa430f
MD5 9dd2690f6c70301a04fbb5cb38f7f3c6
BLAKE2b-256 5311e1de6b3fff7ef62d3c38a4c69a4b1fa4fc01a11e765d0692dcdea71a0364

See more details on using hashes here.

Provenance

The following attestation bundles were made for stanli-0.1.0-py3-none-manylinux_2_28_aarch64.whl:

Publisher: wheels.yml on seantalts/stanli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stanli-0.1.0-py3-none-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for stanli-0.1.0-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 bd60b2e83e79b30ae3f883d050c0466542f6ddc99624b057d65bdf702f72a357
MD5 788ce923d4d54c935f11cb0bc2a713e4
BLAKE2b-256 f662836eb23b8f971fd20ef1fbb1f71d2588c651d4c442597fc67a04fd95b2a0

See more details on using hashes here.

Provenance

The following attestation bundles were made for stanli-0.1.0-py3-none-macosx_11_0_arm64.whl:

Publisher: wheels.yml on seantalts/stanli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file stanli-0.1.0-py3-none-macosx_10_15_x86_64.whl.

File metadata

File hashes

Hashes for stanli-0.1.0-py3-none-macosx_10_15_x86_64.whl
Algorithm Hash digest
SHA256 0f41fc84a816e7b0233e4a6fd02674a3b760c8de8fef5a901d1d6a416cab8437
MD5 9c6ddfcca96ee22eed274c763c5e6c5d
BLAKE2b-256 294f6d002208b993fe14a82e8488c98931ef6eca285104c006ab55360ff70d78

See more details on using hashes here.

Provenance

The following attestation bundles were made for stanli-0.1.0-py3-none-macosx_10_15_x86_64.whl:

Publisher: wheels.yml on seantalts/stanli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page