Spec file
The declarative spec format that describes a model, its data, and the sampler.A spec file declares a complete inference job: the backend, the model, the sampler, the data, and a seed. TOML is the primary format; JSON is also accepted. The spec is validated with zod before anything runs.
A complete example
schema_version = "0"
seed = 42
[backend]
id = "turing"
[model]
kind = "file"
path = "./model.jl"
entry = "build_model"
[sampler]
algorithm = "NUTS"
draws = 1000
warmup = 1000
chains = 4
adapt_delta = 0.8
[data]
N = 10
x = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
y = [5.2, 7.7, 11.1, 13.8, 17.4, 19.9, 23.3, 25.6, 29.2, 31.8]
Fields
Top level
| Field | Type | Required | Notes |
|---|---|---|---|
schema_version | string | yes | must be "0" today |
seed | integer | yes | 0 to Number.MAX_SAFE_INTEGER, bounded so it survives JSON without precision loss |
[backend]
| Field | Type | Default | Notes |
|---|---|---|---|
id | "turing" | "juliabugs" | "stan" | required | the probabilistic-programming backend |
runtime | "julia" | "cmdstan" | per backend | "julia" for turing and juliabugs, "cmdstan" for stan |
version | string | per runtime | Julia: the juliaup channel to run, e.g. "1.12.6" (default: the toolchain’s pinned channel); Stan: a CmdStan version, e.g. "2.39.0", or "installed" (the default), which resolves the newest local CmdStan |
packages | table of name to version | optional | Julia-only: version pins for managed Julia packages, e.g. Turing = "0.45" |
Pinned packages provision into their own managed environment, so different pins can be compared without interfering.
[model]
| Field | Type | Default | Notes |
|---|---|---|---|
kind | "file" | required | only file-based models today |
path | string | required | path to the model file, resolved relative to the spec’s directory |
entry | string | "build_model" | the model entry function (Julia backends; ignored for Stan) |
evaluation_mode | "graph", "generated", "marginalized" | the model file’s own choice | how a JuliaBUGS model evaluates its log density; juliabugs only |
evaluation_mode overrides whatever the model file selected: "graph" walks the node graph, "generated" compiles a specialised log-density function (which mutates arrays in place, so it needs adtype = "mooncake"), and "marginalized" sums the discrete latents out of the log density exactly, so a gradient sampler never sees them.
The marginalized latents still appear in the chain, drawn from their conditional posterior once sampling is done.
It does not combine with MH or Gibbs, which propose the discrete latents themselves.
[sampler]
| Field | Type | Default | Notes |
|---|---|---|---|
algorithm | "NUTS", "HMC", "HMCDA", "MH", "ESS", "SMC", "PG", "Slice", "Gibbs", "External", "Prior" | "NUTS" | the sampler; stan supports NUTS only, juliabugs everything but ESS, SMC, PG, and External, and Slice is juliabugs-only |
draws | positive integer | required | posterior draws per chain |
warmup | non-negative integer | 1000 | NUTS/HMCDA adaptation; burn-in discarded for MH and HMC |
chains | positive integer | 4 | number of chains |
adapt_delta | number in (0, 1) | 0.8 | NUTS/HMCDA target acceptance rate |
step_size | positive number | required for HMC | leapfrog integrator step size |
leapfrog_steps | positive integer | required for HMC | leapfrog steps per proposal |
lambda | positive number | required for HMCDA | target simulation length |
particles | positive integer | required for PG | particle count |
slice_width | positive number | required for Slice | initial slice window width |
blocks | array of tables | required for Gibbs | one [[sampler.blocks]] per component; see below |
thin | positive integer | 1 | keep every thin-th draw |
parallel | "serial", "threads", "distributed" | "serial" | chain execution; threads share one Julia process, distributed starts one worker process per chain (Turing only, adds startup cost) |
adtype | "forwarddiff", "reversediff", "mooncake" | model or backend default | AD backend for gradient samplers; a model file may declare its own default via const MCMC_DEFAULTS = (; adtype = "mooncake"), which the spec overrides. For juliabugs it also overrides the adtype the model file baked into its gradient wrapper |
initial_params | table of named values | - | starting values per variable, replicated across chains |
Gibbs blocks
algorithm = "Gibbs" composes per-variable samplers: each [[sampler.blocks]] table names the variables it updates and the component that updates them (NUTS, HMC, HMCDA, MH, PG, or ESS), with the same per-algorithm parameters as the top level.
The classic use is a gradient sampler for continuous parameters and a particle or Metropolis component for the discrete ones.
On juliabugs the components are NUTS, HMC, HMCDA, MH, and Slice, and the blocks must cover every parameter exactly once.
One block is one group: a block naming an array variable updates the whole array together, which for a long vector of discrete states means proposing all of them at once and almost never accepting.
algorithm = "MH" is the single-site alternative, one Metropolis update per parameter.
algorithm = "MH" there is the shorthand for one Metropolis block over every parameter, which is how a model with unbounded discrete latents (a dpois count, say) is fitted at all: marginalization can only sum out a finite support.
Two things to expect from a juliabugs Gibbs: JuliaBUGS disables step-size adaptation inside a gradient block, so a NUTS block mixes far more slowly than a plain NUTS fit and wants many more draws; and both MH and Gibbs start from the model’s own values, so a model whose defaults are impossible under the data (an unobserved count below its observed successes, say) needs initial_params, and says so instead of running.
[sampler]
algorithm = "Gibbs"
draws = 1000
[[sampler.blocks]]
variables = ["mu", "sigma"]
algorithm = "NUTS"
[[sampler.blocks]]
variables = ["k"]
algorithm = "PG"
particles = 20
Slice sampling
algorithm = "Slice" (juliabugs) runs SliceSampling’s stepping-out slice sampler over the parameters in a random coordinate order, taking no gradient and so no adtype.
It suits awkward geometry, and it is the closest thing to what classic BUGS reaches for on a continuous node with no conjugate update.
slice_width is the initial window the sampler steps out from; too small costs extra evaluations per draw, too large costs wasted shrinking.
It also works as a Gibbs block component, which is how to give the continuous parameters a gradient-free update while MH handles the discrete ones.
[sampler]
algorithm = "Slice"
draws = 1000
slice_width = 1.0
External samplers
algorithm = "External" runs any AbstractMCMC-compatible sampler the model file exports as MCMC_SAMPLER; the driver wraps it with Turing’s externalsampler, passing the spec’s adtype through when set.
import AdvancedHMC
const MCMC_SAMPLER = AdvancedHMC.NUTS(0.8)
Data
Provide data inline or by reference, but not both.
| Field | Type | Notes |
|---|---|---|
[data] | table | inline data, keyed by variable name |
data_file | string | path to a .csv or .json data file, relative to the spec’s directory |
data_file records the reference (path plus hash), not the contents, so large datasets are not copied into the spec or the run store.
[output]
| Field | Type | Default | Notes |
|---|---|---|---|
format | "mcmcchains-json" | "mcmcchains-json" | the samples-file format written |
[predict]
Optional; used by mcmc predict.
| Field | Type | Notes |
|---|---|---|
targets | array of strings (at least one) | outcome variables to predict, by base name; the Julia backends blank each to missing, Stan keeps the matching generated quantities |
data | table | optional data overrides applied on top of [data] for the prediction |
[predict]
targets = ["y"]
JSON form
The same spec as JSON:
{
"schema_version": "0",
"seed": 42,
"backend": { "id": "turing" },
"model": { "kind": "file", "path": "./model.jl", "entry": "build_model" },
"sampler": { "algorithm": "NUTS", "draws": 1000, "warmup": 1000, "chains": 4, "adapt_delta": 0.8 },
"data": {}
}
A Stan example
The same schema drives the Stan backend.
schema_version = "0"
seed = 42
data_file = "./data.json"
[backend]
id = "stan"
[model]
kind = "file"
path = "./model.stan"
[sampler]
algorithm = "NUTS"
draws = 1000
[predict]
targets = ["y_rep"]
version defaults to "installed", the newest local CmdStan; a concrete version such as "2.39.0" pins it.
For Stan, [predict].targets selects which generated quantities to keep, and the model must declare them in a generated quantities block.
The schema_version lets the format evolve without silently breaking consumers. A spec with an unknown version is rejected rather than misread.