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.7" (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 |
monitor | array of strings | all of them | deterministic quantities to store with the parameters, by base name; 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.
When a JuliaBUGS spec sets neither evaluation_mode nor sampler.adtype, a gradient sampler runs on the generated log density under Mooncake, which on the classic BUGS examples takes 10 to 740 times less per gradient than ForwardDiff on the graph.
A model with discrete latents is marginalized as described above and runs under ForwardDiff, the faster backend there, or under Mooncake where ForwardDiff’s gradient is not finite.
Whichever is chosen, its gradient is checked against finite differences at the starting point first, and a model that fails every check runs on the graph, or marginalized, under ForwardDiff as before.
monitor limits which deterministic quantities the run stores.
The model’s parameters are always stored, so monitor = ["sigma", "alpha0"] keeps those two derived quantities and drops every other one, and monitor = [] keeps the parameters alone.
A model that computes large deterministic arrays at every draw needs this to keep its run at a size the report app can load.
[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 |
max_depth | integer from 1 to 20 | backend default | NUTS tree depth limit |
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.
A block whose variables all have finite discrete support is drawn exactly from its full conditional, whatever component you name for it, so a vector of categorical states costs nothing to get right.
Such variables have to be a block of their own; mixing them with continuous ones in one block is refused.
algorithm = "MH" is the single-site shorthand: one block per parameter, an adaptive random walk on the continuous ones, an exact draw on the finite discrete ones, and a lattice walk on unbounded counts, which is how a model with a dpois latent is fitted at all since 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.
Values are finite numbers or rectangular nested numeric arrays.
An entry may also be unobserved, which is BUGS’s NA: the model samples it as a latent instead of conditioning on it, so a partly observed outcome comes back in the chain as one variable per unobserved entry.
Write it as null in a JSON data file, or as an empty cell or NA in a CSV one.
TOML has no null, so data with an unobserved entry has to come from a file rather than an inline [data] table, and mcmc convert writes one beside the spec when the graph needs it.
The stan backend refuses such data instead of fitting it: Stan’s data block has no NA, and takes a censored outcome as its censoring bound plus an observation indicator, which mcmc convert --stan generates.
[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.