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 API (raster.compiler.pipeline/compile-aot)
and reverse-mode API. The caller supplies the existing argument binding. Typed
wrappers may adapt the returned value/gradient representation; differentiation,
optimization and executable generation remain Raster's responsibility.
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.org.replikativ.foerster-raster.gradient/compile-gradient accepts a density Var
whose first argument is a flat (Array double) parameter vector, plus a source
descriptor {:arguments [theta :- (Array double) ...] :dimension d}. It returns
an invoked AOT callable with the same arguments and a double[] result containing
value followed by d natural-coordinate derivatives. Inputs after theta are not
differentiated. Compiler options pass to Raster without suppressing its guards.
An optional :scalar-source {:args [...] :body form} is an equivalent density
with d Double arguments followed by the same data arguments. The adapter checks
that signature, chooses scalar AD only within the qualified 18-argument boundary,
and preserves the vector-parameter caller interface. The caller is responsible
for mathematical equivalence; independent numerical tests must establish it.
This explicit source alternative is a temporary representation workaround,
not automatic compiler scalarization. No application source tree is rewritten.
aot-block accepts the description, density Var, the same {:args binder}
convention as raster-block, and the compiled callable. It checks the returned
gradient length and exposes the required block capabilities. The declared
coordinate convention controls Foerster's transformations and Jacobians; the
adapter adds none itself. The adapter owns no cache and captures no observations.
Callers own structural reuse and bound generated definitions/worker lifetime.
This API is qualified on Raster 0.2.1674. A newer Raster qualification or removal of the explicit scalar source requires separate evidence.
raster-block also accepts :compile? in its binding map. Its default true
retains automatic compilation through Raster's runtime AD API, including that
API's documented fallback. False explicitly selects evaluated runtime AD. A
consumer that labels a backend :runtime must request false; use the explicit
AOT adapter when successful compilation is a requirement rather than a preference.
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 |