Skip to content

Design discussion: model-agnostic Gibbs combinator #205

Description

@singhharsh1708

Background

Turing.jl and JuliaBUGS.jl each have separate, incompatible Gibbs implementations. A model-agnostic Gibbs combinator in AbstractMCMC could unify them — but there is a non-trivial design constraint that I want to understand before proposing an interface.

The core constraint: why GibbsContext exists in Turing

Turing's Gibbs does not use AbstractPPL.condition to freeze non-target variables. Instead it uses GibbsContext, which hijacks the DynamicPPL tilde pipeline. The comment in gibbs.jl explains why (source):

Using standard DynamicPPL.condition results in conditioned variables being treated as observations in the truest sense, i.e. we hit DynamicPPL.tilde_observe!!. But observe is overloaded by some samplers, e.g. CSMC, which can lead to undesirable behavior, e.g. CSMC triggering a resampling for every conditioned variable rather than only for the "true" observations.

Concretely: particle_mcmc.jl overrides tilde_observe!! via ParticleMCMCContext to update the particle's log-weight accumulator, which is what drives resampling in AdvancedPS. If you used AbstractPPL.condition for Gibbs freezing, every frozen variable would look like an observation to CSMC and trigger spurious resampling.

GibbsContext routes frozen variables through tilde_assume!! instead, so CSMC only sees the true observations. The implementation in tilde_assume!! (gibbs.jl ~L253-L325) distinguishes three cases:

  1. Variable is a target → let the child context handle it normally
  2. Variable is conditioned by Gibbs → treat as observation, pulling value from the global VarNamedTuple
  3. New variable not yet seen → sample from prior and add to global VNT

The conditioning problem for a model-agnostic interface

JuliaBUGS avoids this entirely because it has no particle samplers — AbstractPPL.condition (which marks variables as observed in the probabilistic graph) works fine there.

For a Gibbs combinator that lives in AbstractMCMC, the extension point for "build me a conditioned model" would need to express "freeze this variable, but route it through assume not observe." That semantic is specific to DynamicPPL's context system.

The question I want to understand: Is there a way to express this in a model-agnostic interface? A few options I can think of:

  1. Keep GibbsContext internal to Turing. The AbstractMCMC Gibbs combinator calls a gibbs_condition hook, and Turing's implementation of that hook uses GibbsContext internally. The "assume-not-observe" behavior is hidden behind the hook. JuliaBUGS's implementation just calls AbstractPPL.condition. This seems like the most practical path.

  2. Expose an AbstractMCMC.gibbs_condition distinct from AbstractPPL.condition. AbstractMCMC declares the function, each model type implements it with the right semantics. DynamicPPL's implementation uses GibbsContext; JuliaBUGS's uses its graph-based conditioning. This makes the semantic difference explicit.

  3. Change the CSMC/particle design. Instead of checking tilde_observe!!, particles track only variables that are actually declared as observations in the model definition. This would let Gibbs use plain condition and is arguably the cleaner long-term design, but seems like a much larger change to AdvancedPS and Turing.

The state-sync problem

Separately from conditioning, each component sampler must re-sync its internal state when other samplers update their variables. Currently this is gibbs_update_state!!(sampler, state, model, global_vnt). For HMC, this means rebuilding the LogDensityFunction and Hamiltonian against the new conditioned model (hmc.jl L501-L518). For MH, it means re-evaluating the model to update the log-probability accumulators (mh.jl L494-L512).

AbstractMCMC.setparams!!(model, state, params) already has the right signature for this — the model argument is the newly conditioned model, which is exactly what gibbs_update_state!! receives. The question is whether the semantics are identical enough to use it as the unified hook.

What I am asking

Before iterating on the implementation in the open PRs:

  1. Is option 1 or 2 above the preferred direction, or is there a better approach the team has already considered?
  2. Is setparams!!(conditioned_model, state, params) the right hook for state re-sync, or does the current gibbs_update_state!! signature carry information that setparams!! can't express?
  3. Are there other inference algorithms beyond CSMC where this "assume vs observe" distinction matters?

Related: the TODO at gibbs.jl L273-L289 mentions that making GibbsContext a leaf context rather than a parent context (tracked in TuringLang/DynamicPPL.jl#885) would clean up some of the prefix handling. Is that prerequisite work for any of the above options?

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions