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

FieldTypeRequiredNotes
schema_versionstringyesmust be "0" today
seedintegeryes0 to Number.MAX_SAFE_INTEGER, bounded so it survives JSON without precision loss

[backend]

FieldTypeDefaultNotes
id"turing" | "juliabugs" | "stan"requiredthe probabilistic-programming backend
runtime"julia" | "cmdstan"per backend"julia" for turing and juliabugs, "cmdstan" for stan
versionstringper runtimeJulia: 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
packagestable of name to versionoptionalJulia-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]

FieldTypeDefaultNotes
kind"file"requiredonly file-based models today
pathstringrequiredpath to the model file, resolved relative to the spec’s directory
entrystring"build_model"the model entry function (Julia backends; ignored for Stan)
evaluation_mode"graph", "generated", "marginalized"the model file’s own choicehow 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]

FieldTypeDefaultNotes
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
drawspositive integerrequiredposterior draws per chain
warmupnon-negative integer1000NUTS/HMCDA adaptation; burn-in discarded for MH and HMC
chainspositive integer4number of chains
adapt_deltanumber in (0, 1)0.8NUTS/HMCDA target acceptance rate
step_sizepositive numberrequired for HMCleapfrog integrator step size
leapfrog_stepspositive integerrequired for HMCleapfrog steps per proposal
lambdapositive numberrequired for HMCDAtarget simulation length
particlespositive integerrequired for PGparticle count
slice_widthpositive numberrequired for Sliceinitial slice window width
blocksarray of tablesrequired for Gibbsone [[sampler.blocks]] per component; see below
thinpositive integer1keep 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 defaultAD 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_paramstable 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.

FieldTypeNotes
[data]tableinline data, keyed by variable name
data_filestringpath 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]

FieldTypeDefaultNotes
format"mcmcchains-json""mcmcchains-json"the samples-file format written

[predict]

Optional; used by mcmc predict.

FieldTypeNotes
targetsarray 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
datatableoptional 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.