Status: design proposal. Nothing in this document is implemented unless a section says "exists today".
In Spindel every effect site already drops a checkpoint: the rest of the
body, packaged so the engine can continue it later (see Concepts, where
"checkpoint" is the friendly word for a continuation). track re-enters its
checkpoint every time a signal changes, so checkpoints are multi-shot by
design. But they are engine-owned and continue in one world.
A savepoint is a checkpoint the program publishes: it is offered to a handler, which may continue it in other worlds (a fork) or other processes (a portable form). Nothing else is new. The name is provisional; see Open questions.
Spindel can already stop a computation and continue it elsewhere, in several unrelated ways:
| Mechanism (exists today) | Cost | Continuations | Survives a restart | Unit |
|---|---|---|---|---|
fork-context (:following / :frozen) | O(1) following; :frozen merges the parent's overlay once | fork-local; not inherited | no | any context |
ygg/fork! + ForkHandle | O(1) + systems | as above | systems do | a world |
inference checkpoint (choose) | O(1) | captured, multi-shot | no | a particle |
snapshot-context / restore-snapshot | full copy | dropped | no | whole context |
serialize-context + rebuild | EDN | regenerated by replay | yes | whole context |
Each consumer invents its own notion of a safe point and its own way to resume:
choose sites and resume it in a
re-forked world (inference.effects, inference.coordinator).search.mcts) needs no continuation: its environment is a set of
functions over explicit state, and a node is a frozen world.The result is duplicated structure, a missing primitive (factor: a weight
that is not a density), a resampling barrier that assumes every particle
reaches the same number of sites, and no way to checkpoint one world durably.
It is also a correctness problem today. The inference checkpoint stores a
continuation but not the state at its site, so a resume has to
reconstruct that state: resume-from-checkpoint! resets the log-weight to
zero and re-adds only the observes downstream of the resume site, and after a
rejected proposal the context still holds the rejected run's trace and
checkpoints. For x ~ N(0,1); observe N(x,1) = 2; w ~ N(0,1); observe N(w,1) = -2
both MH kernels return the prior for x (mean 0.1 to 0.2, variance 1.0 to
1.3; the posterior is mean 1, variance 0.5). Every existing test places all
samples before all observes, where neither defect shows. See Regression
tests.
This proposal replaces those private notions with one effect, handled differently by each consumer, in two tiers: in-process and portable.
Anglican. A CPS-transformed program returns a record when it reaches a
probabilistic site: sample, observe or result, each holding :cont (a
closure), :state (an explicit immutable value) and :id. An inference
algorithm is a multimethod on [algorithm checkpoint-type]. That is an
algebraic effect with one handler per algorithm. Forking is free because the
state is a pure value, and a site's identity is [id occurrence].
Spindel's choose handler already has this shape (exists today): it builds
{:resolve :reject :address :seq :slice-state :source :options} (it calls
this a checkpoint, in the Concepts sense), stores it and
notifies the coordinator found in the context. The execution context plays the
part of :state, which is why forking means forking a world.
The part of this Spindel does not copy today is :state. Anglican's
checkpoint holds the state as it was at the site, which includes the
log-weight accumulated up to there. Resuming a stored checkpoint (LMH, PGAS)
therefore starts from the right weight and the right partial trace with no
bookkeeping. Anglican gets this for free because its state is a value. In
Spindel the state is a world, and the way to keep a world as it was is to
fork it; this proposal restores the property that way. Anglican also ends a
program with a third checkpoint type, result; the terminal sites below are
that.
Gen. A generative function is used through a small interface over
traces: simulate, generate (run under constraints, return a weight),
update (change choices, return the new trace, a weight and the discarded
choices), regenerate (resample a selection), assess and propose. Every
inference method, and every kernel combinator, is written against that
interface. Gen's dynamic language implements the operations by re-executing
the whole program against a choice map keyed by hierarchical, user-given
addresses. Two lessons: the trace is a first-class value, and one operation,
run again under a policy that says what to do at each address, generates
the whole interface. With continuations that operation need not start at the
beginning of the program.
distributed-scope (now in kabel). go-remote lambda-lifts a body into a
top-level registered function with a stable qualified name and sends
[name args]; the declared arguments are checked at compile time against a
free-variable analysis of the body. That is defunctionalization with an
explicit environment: a closure cannot travel, a named function and portable
arguments can.
Rebuild (exists today). After deserialize-context, continuations are
gone. with-rebuild-context re-executes the model in a mode where spin bodies
return their cached values, which regenerates the dependency graph and the
continuations without changing results. This is resumption by replay. It is
correct exactly when the body is deterministic given the cached spin results
and performs no effect outside them.
(savepoint site payload) ; tier 1
(savepoint site payload {:id address}) ; an explicit, code-independent address
(savepoint site payload {:resume `after-turn ; tier 2 as well
:args [turn-number]
:state [[:conversation]]})
savepoint is an ordinary CPS effect (see Custom Effects). site is
portable data naming the kind of point; its address identifies the occurrence
(see Addresses). payload is what the point
offers to whoever handles it. The effect's value is whatever the handler
resumes it with; the default is payload.
At the site the engine builds a Savepoint, its checkpoint plus what a handler needs:
{:savepoint/site site
:savepoint/address address ; see Addresses
:savepoint/seq n ; program order
:savepoint/payload payload
:savepoint/world world ; the world it is pending in
:savepoint/portable {:fn sym :args data}} ; or nil
A savepoint is a continuation pending in a world. It stays pending until that world is resumed from it or abandons it. While it is pending the world's state is as it was at the site, so whatever a consumer accumulates in world state (a log-weight, a partial trace, a turn counter, a budget) is there for whoever continues it, and no consumer ever reconstructs it.
State is not everything a running world has. A fork inherits no undrained
engine events and no timers (fork-context drops them on purpose: an event is
delivered in one world). A computation whose continuation waits on another
spin of the same world that is still in flight will not see that spin finish
in a fork. See Safe point below.
The pending continuations are part of world state, so a fork of the world holds the same savepoints, pending in the fork.
The engine gives the savepoint to the handler A handler is looked up in context state, as the inference coordinator is today, so it is inherited by forks and can be replaced in a child world.
A table entry under :savepoint/any handles every site that has no entry of
its own, the terminal sites included.
The handler is called inline: on the publishing thread, inside the effect,
before the publishing slice has returned. It may resume, fork or abandon
right there, or hand the savepoint to someone else (a mailbox, a queue) and
return. A handler that throws before it consumed the savepoint fails the
computation with that error, once; the savepoint is consumed, so nothing can
re-enter the failed computation. resume and abandon hop through the
world's executor and are trampolined, so a handler that resumes inline on a
synchronous executor does not grow the stack with the number of sites.
With no handler installed, savepoint continues immediately with
payload. A program may therefore declare safe points unconditionally; they
cost one map and one lookup when nobody listens.
(resume sp value) ; continue sp's world; consumes sp
(fork sp) ; => the same savepoint, pending in a new world
(fork sp {:handlers h ; ... under other handlers
:seed s ; ... with its own random stream
:grant g}) ; ... funded by a resource grant
(abandon sp) ; unwind the computation; consumes sp
(persist sp) ; tier 2: portable form, or throws if not portable
resume and abandon return nil. fork returns a CPS operation
(fn [resolve reject]), as world.scope does, which a spin awaits; its
callbacks run in the world it was invoked from, not in the forked one. fork
goes through a WorldScope (exists today: world.scope/fork!), so
every forked world has an owner, is counted, and is discarded after
quiescence. A fork that loses against a concurrent resume of the same
savepoint is rejected. Resuming a forked savepoint runs the same continuation with the
child world bound, which is what resume-particle-with-value! does today.
Not resuming is holding: the computation stays suspended, and needs no
operation.
Anchors. A world can only go forward, so a handler that wants to come back to a site forks the savepoint before resuming it and keeps the fork. That fork is an anchor: a world that is never run, only forked again. It is the state at the site, held the way Spindel holds every other state, and it costs what a frozen fork costs. Nothing is captured for a site nobody wants to return to.
There is deliberately no weight, reward or state-delta argument on resume.
A handler owns the world it resumes, so it writes to that world and then
resumes; because a pending world is as it was at the site, the write composes
with exactly what was accumulated before it. factor, importance weights and
process rewards are all that one move (see The inference layer).
The three fork options are the places where a fork is not a copy:
:handlers replaces entries of the handler table in the child. Handlers
live in world state, so this is a write to the new world and needs no
handler stack. It is what conditional SMC and ancestor sampling need: the
same savepoint resumed under a scoring handler.:seed gives the child its own random stream. Without it N forks of one
savepoint under a deterministic policy are N copies of one future. The
default derives the seed from [parent-seed address fork-index], so a run
is reproducible and the seed can be logged. (Today draws come from the
process-global anglican.runtime/RNG, which is neither world-local nor
reproducible under concurrency; a world-local splittable stream is part of
this work.):grant is the resource rule; see Resources.(savepoint s p) is p and the
program behaves as if the call were absent.(resume sp v) is the rest of the computation, from the
state at the site, with the effect's value v.sp is portable, hydrating (persist sp) and resuming it
is observationally the same as (resume (fork sp) v), up to effects outside
the world.Law 5 is a requirement on the program, not something the engine can prove: the named function must do what the continuation does.
Someone must own the worlds. A session is one WorldScope, an activity
lease per live world, and the callbacks of the computation it starts:
(def s (open! world {:handlers {site handler} :seed 7}))
(start! s task) ; run a spin in the session's root world
(close! s) ; cancel, abandon what is pending, discard the worlds
The end of a computation reaches the handler table like any other point, at
the terminal sites :savepoint/result, :savepoint/error and
:savepoint/abandoned, exactly once per world, with the world it ended in.
An abandoned world is given back to the scope at once (world.scope/release!);
a world that ended with a result or an error is kept until its reader
releases it or the session closes. A terminal savepoint has no continuation. This is
Anglican's result checkpoint, and it is what lets one handler drive a
computation from its first site to its last in any number of worlds without
a second notification channel.
A savepoint is found again by its address, so the address must mean the same
site in two different executions. Today's hash chain is
hash(source-loc, previous-address): a path. It is stable across forks of
one execution, which is all SMC needs, but after a change upstream that alters
control flow every downstream address differs, so a replay cannot recognise a
choice it could have kept. It also says who came after whom, never who
read whom.
Proposal: savepoint addresses are structural: [scope-key site occurrence], where scope-key is the stack of with-key frames (loop index,
call site, entity id) and occurrence counts repetitions of the site within
that frame. addressing/next-id and with-key exist today for spin identity
and already implement this model; choose sites do not use them. This is
Anglican's [id occurrence] made hierarchical, and Gen's address namespace
derived instead of hand-written. An explicit :id still overrides it.
spindel.trace (implemented). A trace is what a computation did at its
savepoints, a persistent value:
{:trace/entries {address {:site :seq :payload
:value v ; what it was resumed with
:note {...} ; layer-specific, see below
:savepoint anchor}} ; a fork taken before resuming
:trace/order [address ...] ; program order
:trace/result r ; or :trace/error e
:trace/world world ; where the computation ended
:trace/session session} ; the owner of every world above
The trace under construction is world state, so it forks with its world: a
fork of an anchor starts with the entries upstream of its site and nothing
else. A site that nobody will return to (:anchor?) records no anchor.
A policy decides a site: (policy sp old-entry) returns {:value v :note n}, or a CPS operation resolving that, so a policy may call a model or a
network. sp is pending in the world that is about to continue; a policy that
scores writes to that world. Two operations:
(run session task policy) ; => trace
(replay trace address policy) ; => new trace
replay forks the anchor at address and runs the rest again; downstream
sites reach the policy with their entry in trace when their address still
exists (old-entry). Entries that are not reached again are absent from the
new trace, so stochastic control flow prunes itself. Because the fork starts
from the state at that site, everything upstream (values, world state, the
entries) is reused without re-execution. trace is not changed; after a
decision between the two, (release! loser winner) gives back the worlds the
winner does not share. This is Anglican's LMH move and an asymptotic
improvement on Gen's dynamic language, which re-executes from the start.
Gen's interface is then a table of policies:
| Operation | Policy at a site |
|---|---|
simulate | draw from the site's distribution |
generate (constraints) | constrained: that value, weight log p; else draw |
update (constraints) | constrained: that value; else keep the old value; new site: draw. The weight is the change in score; the discard is the entries of the old trace that the new one lacks, which the caller reads off the two traces |
regenerate (selection) | selected: draw afresh; else keep |
assess | everything constrained; the weight is the log joint |
propose | draw from a proposal; note log q |
and the usual kernels are compositions of replay with an accept step:
single-site MH, random walk, custom proposals, involutive MCMC (the
involution is a function of two traces), and Gen's map_optimize with a
gradient supplied by the embedding. Accept adopts the new trace; reject drops
it and discards its worlds. The accepted trace is never mutated, which is the
second half of the MCMC defect above.
inference.trace (implemented, except where marked). Inference adds a
vocabulary of sites, notes and one world-state key on top. It adds no effect
to the algebra: sample, observe and the new factor publish savepoints
when their world handles the site, and speak the coordinator protocol
otherwise.
| Site | Payload | The policy resumes with |
|---|---|---|
:inference/choose (sample) | {:dist d :options ..} | a draw from d, a kept value, a constraint, or a proposal's draw having added log d(v) - log q(v) to the weight |
:inference/choose (observe) | {:dist d :observed? true :value y} | y, having added log d(y) |
:inference/factor | {:log-weight w} | nil, having added w |
Notes per entry: :dist; :log-prob, the density of every site, sampled
ones included (before, only observes contributed, to one scalar, which
rules out score-function gradients, assess, the rescoring of kept values
and learned-proposal weights); :log-proposal when the value was drawn;
:observed?, :constrained?, :kept?, :symmetric?, :factor?,
:intervened?. The importance weight accumulates at [:inference :log-weight] in the world.
Metropolis-Hastings is replay plus an accept step (mh-step, mh-chain).
A move selects a set of target sites (one for single-site MH, a block for
block Gibbs), replays from the earliest, proposes at the targets, and keeps
every other site's value, rescored under its distribution as it is now.
log a = [log p(new) - log p(old)]
+ [log q(old | new) - log q(new | old)]
+ [log s(targets | new) - log s(targets | old)]
where q(new | old) is what the move drew afresh, q(old | new) is what of
the old trace was not kept and the reverse move would have to draw, and s
is the probability of selecting the targets, which changes when the move
changed how many sites there are. single-site-mh-kernel,
random-walk-mh-kernel and block-gibbs-kernel are descriptions that
kernel-infer runs this way. A kept value that fell out of its site's
support is drawn again, and the move is refused when the reverse move would
keep the new value instead (overlapping supports), because the old state could
then not be reached back. Limits: the reverse move is scored under the prior,
so a custom proposal must be the prior or symmetric; a block's membership must
not depend on the move; chains run in fresh worlds (:world-policy :fork is
refused). The coordinator keeps :iterate as a FULL in-place replay; the
partial in-place resume is gone.
Not implemented: :proposal and :parents in the payload; SMC, PGibbs,
PGAS, PIMH, IPMCMC and BBVI still run on the coordinator.
:proposal and :parents are what amortized inference needs. A learned
proposal is a function of the trace so far, called by the handler. :parents
declares which earlier choices a site read, which no address scheme can
recover; with it a trace is a graph (variables, edges, observed mask,
values), which is the training datum of a graphically structured model.
Spindel emits that datum; the embedding differentiates its own network.
What this buys, as handler policies over the same programs:
| Method | How |
|---|---|
| importance sampling, likelihood weighting | default resume, weights written at sites |
| SMC, with a custom or learned proposal | collect savepoints by site and :seq, resample, re-fork; :proposal |
| asynchronous SMC / particle cascade | no barrier: decide per arriving savepoint whether to fork, continue or abandon |
| twisted SMC, process rewards | :inference/factor with a value estimate; the telescoping correction is a second factor |
| PMMH, PGibbs, PGAS | fork the retained particle's savepoints under a scoring handler (:handlers) |
| LMH, RMH, custom and involutive MCMC | replay plus accept |
| BBVI, reweighted wake-sleep, inference compilation | per-site :log-prob and :log-proposal in the trace; gradients by the embedding |
| nested inference | an inner world with its own handler table; nesting is by world |
Tier 1 is the existing inference mechanism made public and generalized.
capture-slice-state, exists today): bindings, address frame and
dependency tracking. Restoring the address frame on resume is what makes
every downstream site mint the address it had before, so a resumed
computation is addressable the same way in every fork.world.scope leases are one per live world and say nothing about what runs
inside it. MCTS enforces the rule for its own worlds by construction.
Enforcing it here needs the engine to answer "is anything pending in this
world", which is an open item.engine.component). Forked (forkable, and registered
Yggdrasil systems through the scope's ForkHandle): the fork gets its own.
Shared (shared): one value by reference, for things that are safe to
share. Pinned (pinned, new): a versioned store shared by reference,
read at the version the world pinned; a fork reads what its source read and
a world moves on with repin!. Trainable parameters are the case that
matters: every particle and rollout samples from one parameter store, a
training step produces a new version instead of mutating the old one, and
pinned-version is the policy version to record with every site decided
under it (a policy's job; nothing records it by itself yet).A fork duplicates state. It must not duplicate authority. If a world may
spend a budget (model tokens, money, device memory), N forks of it hold N
references to one budget, and each may spend all of it. Spindel does not know
what a budget is, so the rule is a hook on the scope, not a dependency
(implemented, world.scope/PResourceAuthority):
(defprotocol PResourceAuthority
(grant! [a source-world child-world grant]) ; move, never copy
(return! [a world]) ; the remainder, on discard
(escrow! [a world key]) ; a savepoint leaves the process
(claim! [a key world])) ; ... and arrives, once
fork takes a grant. (fork sp {:grant g}) moves g from the
publishing world's wallet to the child's. A grant the source cannot afford
rejects the fork, and the child is discarded before it ever becomes a world
of the scope. With an authority installed and no grant, the child has no
wallet and can spend nothing. A world funds its own forks from what it was
granted, so budgets nest the way worlds do. The ledger that records this is
not a member of any forked world.release! and discard! call return!
before they discard a world; an abandoned savepoint's world is released at
once, so an abandoned branch gives its budget back while the search is
still running.savepoint/spend-key). A world
continues from a site once, so the pair is unique. A ledger that
deduplicates by id (kontor does) would otherwise see the second fork of a
savepoint as a replay of the first and silently not charge it.persist escrows, when asked. A portable savepoint that
names a wallet is authority that could be hydrated twice. persist moves
the world's remainder into an escrow keyed by the savepoint's content hash
(escrow!), which hydration claims once (claim!).savepoint.portable (implemented). A closure cannot leave the process. A
savepoint is portable when it names a top-level function that continues the
computation, portable arguments for it, and the paths of world state it
depends on:
(savepoint :conversation/turn m {:resume `after-turn :args [k]
:state [[:conversation]]})
The function is called with :args and then with the value the savepoint is
resumed with, and returns the spin to run: it is the continuation, written
as a function. persist yields plain data:
{:savepoint/id <content hash of everything below>
:savepoint/site .. :savepoint/address .. :savepoint/seq .. :savepoint/payload ..
:savepoint/resume {:fn my.ns/after-turn :args [3]}
:world/seed ..
:world/state {[:conversation] {...}} ; the declared paths, nothing else
:world/systems {system-id snapshot-id} ; registered Yggdrasil systems
:world/pinned {component-id version} ; pinned components
:world/escrow? true} ; with a resource authority
(hydrate! session data value) forks the session's root, the host world,
pinned at the recorded snapshots (ygg/fork! :snapshots, exists today),
repins the components, writes the state and the seed, claims the escrow, and
runs (apply f (conj args value)) in that world. The host must have the named
systems and components registered; that is how a process says where they live.
Resuming a portable savepoint and invoking a remote function are the same
operation: run a named function with portable arguments in a world.
Only declared state travels. An earlier draft serialized the whole context, which drags along the engine's state (nodes, continuations, subscriptions) and whatever unserializable objects sit in it. None of that is the computation's state: the named function starts a new computation, and what it needs from the old one the program declares. The portable form is small, is data, and its content hash is a prefix identity that is the same across runs and machines.
persist does not consume the savepoint. (persist sp {:escrow? true}), with
a resource authority, moves what is left in the world's wallet into an escrow
named by the content hash, and hydrate! claims it, once: the second
hydration of the same data is rejected. The world here is then left with
nothing to spend or to grant, whether or not the data is ever hydrated; that
is what conservation costs, which is why it is opt-in. Without the option the
data hydrates unfunded.
The hydrated world is a fork of the host root, so it inherits the host's
state. hydrate! clears the savepoint bookkeeping it would otherwise carry
(the host's pending savepoints, its end, its trace) and gives the world a seed
of its own, as a fork of the savepoint would get. What else the host root
holds under undeclared paths is visible there; a host meant for hydration
should be a world that runs nothing itself.
Limits. The content id excludes the site's address, which moves with every
edit of the source, and is stable across runs and machines only for values
that hash alike everywhere: no functions or objects in payload or state, and
an integral double hashes differently on the JVM and in JavaScript. Snapshot
ids are read synchronously, which holds on the JVM; in ClojureScript persist
refuses a world with registered systems. Site addresses in a hydrated world
differ from those of the in-process fork (its spins are new), so law 5 is
about what the computation does, not about how its sites are named.
Replay-based hydration (rebuild, exists today) remains the way to restore a whole reactive context whose bodies are replay-safe; it needs no annotation and is not what an agent conversation or anything driving external effects can use.
A defn-level macro in the style of defn-go-remote, lifting the rest of a
body into a named function and checking its captured locals, is sugar over
:resume and can follow later. Automatic lifting inside the CPS transform is
deliberately not proposed: captured locals may not be portable, and a site's
identity would have to survive code changes.
Naming. On the JVM a qualified symbol resolves without a registry
(requiring-resolve). In ClojureScript it does not; hydrate! takes
:resolve. kabel already has a registry of portable functions for
go-remote, which is the natural thing to pass.
| Feature | As a savepoint | Change |
|---|---|---|
choose / sample / observe | site :inference/choose, payload = distribution and options; the inference coordinator is its handler | none to the API; the handler plumbing becomes the shared one |
factor (missing today) | site :inference/factor; the handler writes the weight and resumes | new; a site, not an effect |
| SMC barrier | handler policy: collect savepoints by site and sequence, resample, re-fork | fixes "Mixed particle states" for computations of uneven length |
| MCMC | replay plus accept over a persistent trace | fixes a bug: a proposal resumes a fork of an anchor, so upstream observes stay in the weight, a rejected proposal leaves no state behind, and unreached sites are pruned |
trace {address -> {:value :distribution :observed?}} | a spindel.trace trace with inference notes (legacy-trace projects the old shape) | per-site :log-prob; structural addresses |
| MCTS | unchanged. Its environment is explicit state, so it needs no continuation. An adapter can present a computation with savepoints as an MCTS environment: actions are the values a savepoint may be resumed with, a transition is (resume (fork sp) a), a node is the next savepoint | optional adapter |
snapshot-context, serialize-context, rebuild | unchanged; they become the context half of persist and hydration | none |
await, track, yield | unchanged. They suspend for a value, a change or a consumer; none of them is a point offered to a handler | none |
await is deliberately not folded in. It has one meaning and one handler;
publishing it as a savepoint would add a lookup to the hottest path for no gain.
An agent harness marks the gaps between turns:
(savepoint :conversation/turn {:message m :turn k}
{:resume `continue-conversation :args [k]})
(persist sp); its content hash is a
prefix identity usable across runs and machines.factor carrying a partial verifier's score.None of these needs a resume flag in the embedding's own code, and the same program runs unchanged under the default handler.
A trace of :conversation/turn savepoints is also the rollout record a
trainer needs, provided the embedding's notes carry what cannot be
reconstructed later. For reinforcement learning with verifiable rewards that
is, per site: the fork's seed; a prefix id that covers the world (the
hash of the anchor's portable form), not only the conversation; the pinned policy
version; the sampler's token log-probs and, separately, the trainer's
recomputed ones; sampling parameters and a chat-template hash; token ids in
and out; and per sibling set (the forks of one savepoint) a set id and the
baseline it was scored against. Group baselines (GRPO and relatives) are
forks of the initial savepoint; tree and step-level methods (VinePPO,
TreeRL) are forks of later ones, and their value estimate at a savepoint is
the mean return of its forks. None of this is new algebra; it is the schema of
:note.
inference/mcmc-correctness-test compares chains with analytic posteriors on
models that are not laid out as "all samples, then all observes":
Still to write: SMC with particles that publish a different number of sites,
and assess of a full choice map against a hand-computed log joint.
effects.savepoint: the effect, handler lookup in context state, the
default handler, resume / fork / abandon, sessions and terminal sites
over world.scope, fork options :handlers and :seed. Tests for laws 1
to 4, concurrency, handler failures, a synchronous executor.
Implemented, with world.scope/release! for single worlds. (A
world-local random stream drawing from the seed is part of step 4.)next-id / with-key
(addressing/site-address!). Implemented.run and replay under a policy (spindel.trace).
Implemented.choose onto it: per-site :log-prob, factor as a site, the MCMC
kernels as replay plus accept, the in-place resume deleted.
Implemented; mcmc-correctness-test pins the two defects against
analytic posteriors. Open: :proposal, :parents, a world-local random
stream, and moving SMC and the particle MCMC methods off the coordinator.PResourceAuthority on world.scope: grant on fork, return on discard,
spend identity. Pinned members. Test for law 6 with a toy authority.
Implemented, except escrow, which belongs to step 7.persist and hydration for a world: declared state, system snapshot ids,
pinned versions, named resume, escrow. Law 5. Implemented
(savepoint.portable); systems with asynchronous snapshot ids are refused.Steps 1 to 4 fix the MCMC defect and are one reviewable unit. An embedding (dvergr's Run-level checkpoint and branch) needs 1, 3 and 6.
The name. "Checkpoint" already means every continuation in Concepts, so
the published form needs its own word; savepoint is the proposal
(offer and branch-point were considered).
The portable-function registry: reuse kabel's, or share a small namespace?
Should a handler be able to replace the payload's continuation (an effect-handler "return clause"), or only choose the resume value? The proposal assumes the latter; nothing here needs the former.
Is one handler per site enough, or do sites need a handler stack (an outer
benchmark handler around an inner inference handler)? Context state plus
fork inheritance gives nesting by world, and fork's :handlers gives
"the same savepoint under another handler". A stack within one world is not
proposed.
Retention. A trace keeps an anchor per site it may return to. Single worlds
can be given back (trace/release!), so a chain holds the anchors of its
current trace only; what is still missing is a policy for which sites to
anchor at all. Anglican has the same cost and no policy.
No epoch. A finally block that reaches a savepoint site while its world is
being abandoned publishes to a handler that has moved on. Inference guards
the same thing with a sweep counter.
Rewinding a world in place to an anchor's state, instead of continuing in a fork of it, would save a world per move. The backend is persistent, so it is a swap of the state root, but it is only safe if engine-internal state (queue, timers, in-flight completions) is provably empty at a savepoint. Start with forks; measure before adding it.
:parents is declared by the program. Deriving it, by static analysis of
the spin body or by tracking sampled values through the dataflow, is
attractive and out of scope.
Pinned members under persist: a portable savepoint must name a parameter
version that still exists when it is hydrated, which makes retention of
parameter versions a garbage-collection root.
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 |