Status: draft 0, agreed in outline between the inference and raster sides (2026-09-28; the inference side was spindel's, and is now foerster, on spindel's worlds). Changes go through this file.
foerster owns everything about which variables exist and how they are explored: addresses and traces, the random streams, proposals, acceptance, particles and forked worlds, and the statistical meaning of every estimate.
Raster owns fixed-shape numerical code: evaluating a block, its log density and derivatives, compilation, caches, buffers and device sessions.
This bridge adapts one to the other. It must not add a compiler cache, an argument binder or a device-session convention of its own. It consumes raster's public compilation/artifact API; until that exists it calls plain functions.
A block is a fixed-shape numerical fragment of a model: a group of latent variables together with the density factors they touch. foerster treats a block as one choice site whose value is the block's latents.
A block is a description plus capabilities. The description is data:
{:block/id :logreg
:block/latents [{:name :beta :shape [3] :dtype :f64
:support :real} ; or :positive, [:interval a b], :simplex …
{:name :sigma :shape [] :dtype :f64
:support :positive :transform :log}]
:block/layout {:order [:beta :sigma] ; θ = concat, row-major
:coordinates :unconstrained} ; θ lives in R^n
:block/inputs [{:name :X :shape [:n 3]} {:name :y :shape [:n]}] ; not differentiated
:block/target :complete-conditional ; see below
:block/capabilities #{:log-density :value+grad}}
Capabilities are functions over primitive arrays. Required:
| capability | signature | meaning |
|---|---|---|
:log-density | (f ^doubles θ inputs) → double | log target at θ, unconstrained coordinates, change-of-variable terms included |
:value+grad | (f ^doubles θ inputs) → [double ^doubles g] | the same value and ∂/∂θ; inputs get no gradient |
Optional, each with an explicit domain and convention in the description:
:jvp, :vjp, :hvp, :constrain (θ → latent values, for the trace),
:unconstrain, :inverse, :log-abs-det-jacobian, :eval (forward
simulation), :batch (a leading particle dimension), and for drawing a
block:
| capability | signature | meaning |
|---|---|---|
:sample | (f inputs) → ^doubles θ | a draw of θ, not necessarily from the target (whose normalizer is unknown) |
:sample-log-density | (f ^doubles θ inputs) → double | the log density of what :sample draws; required whenever a block is drawn from — importance sampling, SMC, single-site MH proposals — since the draw is weighed by log target(θ) − log sample-density(θ) |
A block without :sample cannot be drawn from and starts at an :init.
A capability a block does not declare is absent: foerster never falls back to finite differences behind a caller's back. An undeclared derivative rejects the program.
:complete-conditional means the log density includes every factor the
block's latents affect: their priors and all downstream observations and
latents that depend on them, with every variable outside the block held
fixed as an input. A site's own prior alone is not enough for HMC.
foerster keeps HMC exact even when a block's target is incomplete. The leapfrog map is volume preserving and reversible for any position-only force field, and foerster computes the acceptance ratio on the full trace log joint, by replay. An incomplete target therefore costs efficiency, never correctness. foerster reports it: when a replay changes the log probability of a site outside the block, the step records a diagnostic.
θ is a flat double[] in the declared order, in unconstrained
coordinates. Every constrained latent names its transform. The block's log
density includes the log-absolute-Jacobian of each transform. The trace
records both θ and the constrained values: sites downstream see the
constrained values.
Every draw a seeded run makes depends only on what is drawn, never on when:
stream(world, key) = generator seeded by hash(seed(world), key)
(parent seed, address, fork index).Raster randomness inside a block (e.g. par/splitmix64) takes its seed from
the stream of the site that runs the block. Bit-identical floating point
across devices is not part of the contract; reproducibility is per device
and backend.
Draft 0 is synchronous: capabilities are pure functions and retain nothing
between calls. When residuals arrive (a tape for vjp, a device buffer), they
are owned by the call that made them and released when the world that holds
them (a spindel world) is released. That world is a savepoint anchor, a trace or a
particle. Asynchronous completion goes through one shared completion service
that resumes the waiting spin on its executor.
Fast and deterministic, in every test run:
:value+grad at random points, with a stated
tolerance.Statistical, as a separate seeded experiment with explicit thresholds:
foerster.block,
foerster.hmc); the Gaussian oracle. Done. The logistic-regression
oracle (non-conjugate, against grid quadrature): done.deftm log densities (foerster-raster.block), with
gradients with respect to θ only. Done.:batch) through raster dimensions; worlds fork only
where structure diverges.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |