Skip to content

Comments

Use templates in observe pipeline, make pointwise_logprobs return VNT#1279

Merged
penelopeysm merged 5 commits intobreakingfrom
py/templates-in-observe
Feb 21, 2026
Merged

Use templates in observe pipeline, make pointwise_logprobs return VNT#1279
penelopeysm merged 5 commits intobreakingfrom
py/templates-in-observe

Conversation

@penelopeysm
Copy link
Member

@penelopeysm penelopeysm commented Feb 13, 2026

Closes #1271

@github-actions
Copy link
Contributor

github-actions bot commented Feb 13, 2026

Benchmark Report

  • this PR's head: e5ff7003d6950e1e2fda8219633a4fae12e9db60
  • base branch: d089228497b46e723325e0d2be93fcf25c7b62ed

Computer Information

Julia Version 1.11.9
Commit 53a02c0720c (2026-02-06 00:27 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × AMD EPYC 7763 64-Core Processor
  WORD_SIZE: 64
  LLVM: libLLVM-16.0.6 (ORCJIT, znver3)
Threads: 1 default, 0 interactive, 1 GC (on 4 virtual cores)

Benchmark Results

┌───────────────────────┬───────┬─────────────┬────────┬───────────────────────────────┬────────────────────────────┬─────────────────────────────────┐
│                       │       │             │        │       t(eval) / t(ref)        │     t(grad) / t(eval)      │        t(grad) / t(ref)         │
│                       │       │             │        │ ─────────┬──────────┬──────── │ ───────┬─────────┬──────── │ ──────────┬───────────┬──────── │
│                 Model │   Dim │  AD Backend │ Linked │     base │  this PR │ speedup │   base │ this PR │ speedup │      base │   this PR │ speedup │
├───────────────────────┼───────┼─────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│               Dynamic │    10 │    mooncake │   true │   466.38 │   368.48 │    1.27 │   7.42 │   11.74 │    0.63 │   3458.71 │   4324.44 │    0.80 │
│                   LDA │    12 │ reversediff │   true │  2679.48 │  2711.57 │    0.99 │   5.03 │    4.77 │    1.06 │  13487.16 │  12926.16 │    1.04 │
│   Loop univariate 10k │ 10000 │    mooncake │   true │ 58862.38 │ 63797.97 │    0.92 │   6.96 │    5.29 │    1.32 │ 409865.13 │ 337563.20 │    1.21 │
├───────────────────────┼───────┼─────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│    Loop univariate 1k │  1000 │    mooncake │   true │  6242.45 │  6385.26 │    0.98 │   5.60 │    5.24 │    1.07 │  34949.60 │  33436.07 │    1.05 │
│      Multivariate 10k │ 10000 │    mooncake │   true │ 45386.68 │ 34276.20 │    1.32 │   7.44 │    9.77 │    0.76 │ 337788.29 │ 334967.89 │    1.01 │
│       Multivariate 1k │  1000 │    mooncake │   true │  4927.75 │  7858.87 │    0.63 │   9.33 │    4.30 │    2.17 │  45973.69 │  33826.60 │    1.36 │
├───────────────────────┼───────┼─────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Simple assume observe │     1 │ forwarddiff │  false │     2.29 │     2.71 │    0.84 │   4.52 │    3.81 │    1.19 │     10.36 │     10.33 │    1.00 │
│           Smorgasbord │   201 │ forwarddiff │  false │  1118.11 │  1114.00 │    1.00 │ 119.74 │  132.95 │    0.90 │ 133884.35 │ 148110.16 │    0.90 │
│           Smorgasbord │   201 │      enzyme │   true │  1692.68 │  1531.12 │    1.11 │   3.79 │    6.95 │    0.54 │   6408.18 │  10645.29 │    0.60 │
├───────────────────────┼───────┼─────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ forwarddiff │   true │  1709.49 │  1531.83 │    1.12 │  70.08 │   70.63 │    0.99 │ 119792.36 │ 108188.97 │    1.11 │
│           Smorgasbord │   201 │    mooncake │   true │  1693.61 │  1531.83 │    1.11 │   4.51 │    5.48 │    0.82 │   7643.07 │   8389.36 │    0.91 │
│           Smorgasbord │   201 │ reversediff │   true │  1678.84 │  1523.20 │    1.10 │  96.59 │   98.83 │    0.98 │ 162151.29 │ 150541.82 │    1.08 │
├───────────────────────┼───────┼─────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│              Submodel │     1 │    mooncake │   true │     3.10 │     3.33 │    0.93 │  53.98 │   64.52 │    0.84 │    167.54 │    214.95 │    0.78 │
└───────────────────────┴───────┴─────────────┴────────┴──────────┴──────────┴─────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘

Comment on lines -574 to +583
vnt_sz = vnt_size(value)
if vnt_sz != idx_sz
throw(
DimensionMismatch(
"Assigned value has size $(vnt_sz), which does not match " *
"the size implied by the indices $(idx_sz).",
),
)
end

# vnt_sz = vnt_size(value)
# if vnt_sz != idx_sz
# throw(
# DimensionMismatch(
# "Assigned value has size $(vnt_sz), which does not match " *
# "the size implied by the indices $(idx_sz).",
# ),
# )
# end
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR also makes a slightly controversial change of removing the check on vnt_size when setting a value in a VNT. The reason is because for pointwise_logdensities, I want to be able to get a VNT that's

x[1:2] => 0.35 # or some other float

but if you attempt to set 0.35 into the position x[1:2], it will error since 0.35 is a float and has size ().

The usual way around this in DPPL has been to make a wrapper that has the correct size, like

struct FloatWithSize
    logp::Float64
    size::Tuple
end

and then store that. But then you get a VNT that doesn't map x[1:2] to its logprob; you get a VNT that maps x[1:2] to some weird FloatWithSize struct, and then users have to deal with this.

It's fine if we have to deal with wrapper structs internally in DPPL code. But I don't think users should be forced to deal with wrapper structs because of an internal detail of VNT. For the same reason, we don't force users to manually deal with ArrayLikeBlocks either.

So I removed the size check here, essentially allowing any value to be set into any number of indices.

Copy link
Member Author

@penelopeysm penelopeysm Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In principle, this also means the entire vnt_size machinery can be removed, because there's no need for a value to declare its size ahead of time. I haven't done that yet, because I'm still a bit unsure about the removal of size checks. I do think it's the correct thing to do though (I already argued that we should have more flexible size specification way back in #1180 (comment)).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sunxd3, pinging for thoughts on this!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me (sorry for missing the ping)

@codecov
Copy link

codecov bot commented Feb 13, 2026

Codecov Report

❌ Patch coverage is 97.87234% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 78.76%. Comparing base (d089228) to head (e5ff700).
⚠️ Report is 1 commits behind head on breaking.

Files with missing lines Patch % Lines
src/accumulators/bijector.jl 0.00% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           breaking    #1279      +/-   ##
============================================
+ Coverage     78.58%   78.76%   +0.18%     
============================================
  Files            47       47              
  Lines          3600     3589      -11     
============================================
- Hits           2829     2827       -2     
+ Misses          771      762       -9     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@penelopeysm penelopeysm requested a review from sunxd3 February 18, 2026 11:58
) where {Prior,Likelihood}
# vn could be `nothing`, in which case we can't store it in a VNT.
return if Likelihood && vn isa VarName
logp = logpdf(right, left)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we still want to keep the support for, e.g.,

julia> logpdf(Normal(), [1.0, 2.0])
2-element Vector{Float64}:
 -1.4189385332046727
 -2.9189385332046727

julia> loglikelihood(Normal(), [1.0, 2.0])
-4.337877066409345

(the old code uses loglikelihood)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am a bit confused by the use of this

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, yeah, that is confusing.

Copy link
Member Author

@penelopeysm penelopeysm Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for something like

@model function f(y)
    y ~ Normal()
end
model = f([1.0, 2.0])

We need to use loglikelihood inside the LogLikelihoodAccumulator because that expects a single float. But one could easily argue that for pointwise logdensities, it's more informative to return the vector, and thus use logpdf. (Related: #1038)

Using logpdf here would be a departure from the previous behaviour, but if it's more informative I feel like it's a good thing? It is a breaking change, but it is a positive change because it provides strictly more information than before. It would also mean that you could do e.g.

julia> model = f([1.0, 2.0]); v = VarInfo(model);

julia> pointwise_loglikelihoods(model, v)[@varname(y[1])]
-1.4189385332046727

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a note in the changelog about this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

logpdf is more clear to me 👍

Comment on lines -574 to +583
vnt_sz = vnt_size(value)
if vnt_sz != idx_sz
throw(
DimensionMismatch(
"Assigned value has size $(vnt_sz), which does not match " *
"the size implied by the indices $(idx_sz).",
),
)
end

# vnt_sz = vnt_size(value)
# if vnt_sz != idx_sz
# throw(
# DimensionMismatch(
# "Assigned value has size $(vnt_sz), which does not match " *
# "the size implied by the indices $(idx_sz).",
# ),
# )
# end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sounds good to me (sorry for missing the ping)


### Pointwise logdensities

Calling `pointwise_logdensities(model, varinfo)` now returns a `VarNamedTuple` of log-densities rather than an `OrderedDict`.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we want to expose the two optional arguments (i.e. ::Val{Prior} and ::Val{Likelihood})?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically they were always exposed. But actually now thinking about it, I kind of prefer not to expose them, so we would move these arguments to an internal function and make pointwise_logdensities call that with (true, true).

@penelopeysm penelopeysm force-pushed the py/templates-in-observe branch from c35e7b1 to c787174 Compare February 21, 2026 01:43
@github-actions
Copy link
Contributor

DynamicPPL.jl documentation for PR #1279 is available at:
https://TuringLang.github.io/DynamicPPL.jl/previews/PR1279/

@penelopeysm
Copy link
Member Author

I deferred the full removal of size checks in VNT to #1281, but I incorporated the other suggestions.

@penelopeysm penelopeysm requested a review from sunxd3 February 21, 2026 02:19
@penelopeysm penelopeysm merged commit 63588db into breaking Feb 21, 2026
21 checks passed
@penelopeysm penelopeysm deleted the py/templates-in-observe branch February 21, 2026 13:30
penelopeysm added a commit that referenced this pull request Feb 21, 2026
This PR removes size checks when setting a value inside a VNT. That
means, for example, you can assign `Dirichlet(ones(2))` (which has size
`(2,)`) to `x[1:5]` even though the indices are not the same size:

```julia
julia> using DynamicPPL, Distributions

julia> vnt = @vnt begin
           @template x = zeros(5)
           x[1:5] := Dirichlet(ones(2))
       end
VarNamedTuple
└─ x => PartialArray size=(5,) data::Vector{DynamicPPL.VarNamedTuples.ArrayLikeBlock{Dirichlet{Float64, Vector{Float64}, Float64}, Tuple{UnitRange{Int64}}, @NamedTuple{}, Tuple{Int64}}}
        ├─ (1,) => DynamicPPL.VarNamedTuples.ArrayLikeBlock{Dirichlet{Float64, Vector{Float64}, Float64}, Tuple{UnitRange{Int64}}, @NamedTuple{}, Tuple{Int64}}(Dirichlet{Float64, Vector{Float64}, Float64}(alpha=[1.0, 1.0]), (1:5,), NamedTuple(), (5,))
        ├─ (2,) => DynamicPPL.VarNamedTuples.ArrayLikeBlock{Dirichlet{Float64, Vector{Float64}, Float64}, Tuple{UnitRange{Int64}}, @NamedTuple{}, Tuple{Int64}}(Dirichlet{Float64, Vector{Float64}, Float64}(alpha=[1.0, 1.0]), (1:5,), NamedTuple(), (5,))
        ├─ (3,) => DynamicPPL.VarNamedTuples.ArrayLikeBlock{Dirichlet{Float64, Vector{Float64}, Float64}, Tuple{UnitRange{Int64}}, @NamedTuple{}, Tuple{Int64}}(Dirichlet{Float64, Vector{Float64}, Float64}(alpha=[1.0, 1.0]), (1:5,), NamedTuple(), (5,))
        ├─ (4,) => DynamicPPL.VarNamedTuples.ArrayLikeBlock{Dirichlet{Float64, Vector{Float64}, Float64}, Tuple{UnitRange{Int64}}, @NamedTuple{}, Tuple{Int64}}(Dirichlet{Float64, Vector{Float64}, Float64}(alpha=[1.0, 1.0]), (1:5,), NamedTuple(), (5,))
        └─ (5,) => DynamicPPL.VarNamedTuples.ArrayLikeBlock{Dirichlet{Float64, Vector{Float64}, Float64}, Tuple{UnitRange{Int64}}, @NamedTuple{}, Tuple{Int64}}(Dirichlet{Float64, Vector{Float64}, Float64}(alpha=[1.0, 1.0]), (1:5,), NamedTuple(), (5,))
```

The reason for this is twofold.

## Pointwise log-probabilities

One is to do with #1279. The issue there is that when accumulating
pointwise log-densities in VNT, you can have a model that looks like

```julia
@model function ...
   x[1:2] ~ Dirichlet(ones(2))
end
```

It's only possible to associate a single float probability with the
VarName `x[1:2]`. If there were size checks, it would be impossible to
associate a true float with `x[1:2]`, because the size of a float is
`()` which doesn't line up with the expected size `(2,)`. You would have
to wrap a float in a struct like

```julia
struct SizedLogProb{T}
    lp::Float64
    sz::T
end
vnt_size(s::SizedLogProb) = s.sz
```

before storing it in a VNT. The problem with this is that when the user
expects to get a log-probability, they'll now get this ugly
`SizedLogProb` thing, which forces them to carry the burden of internal
VNT details.

## Sizes are checked at model runtime anyway

The main reason why we want to avoid storing something of the wrong size
is because we want to avoid the possibility of constructing
"inconsistent VNTs". Here I use "inconsistent" to mean VNTs precisely
like the one above:

```julia
julia> vnt = @vnt begin
           @template x = zeros(5)
           x[1:5] := Dirichlet(ones(2))
       end
```

In this example, which could arise when e.g. storing the prior
distributions of variables, we don't ever want to have a scenario where
we collect a prior `Dirichlet(ones(2))` that *cannot* be associated with
a value `x[1:5]`.

The thing is, though, that can never happen anyway. If you were to write
`x[1:5] ~ Dirichlet(ones(2))`, when you evaluate the model, this will
fail anyway because `rand(Dirichlet(...))` will return a length-2
vector, which cannot then be set into `x[1:5]`. (This is completely
independent of any VNTs and will fail even with an empty OnlyAccsVarInfo
that has no accumulators.)

In my opinion, then, the size check when setting items in VNTs is
superfluous because it is impossible to construct a model that will both
run correctly and lead to inconsistent VNTs. Inconsistent VNTs only
arise from models that cannot be run.

## Conclusion

If not for the issue with pointwise logprobs, I would have probably just
let it slide and kept the size check in even though IMO it doesn't
accomplish much. However, given that there is actually a motivation to
remove it, I decided that it's better to remove it.

Because of this, several structs in DynamicPPL that used to store size
information now no longer need to. These include `RangeAndLinked`,
`VectorValue`, and `LinkedVectorValue`. I have therefore also removed
those fields.

I'm currently somewhere around 95% sure that this is the right thing to
do. However, I've decided to separate this into another PR just to cover
for that 5% chance, in case I need to revert it in the future.

## Previous discussion about the need for size checks

See
#1180 (comment).

Given that this was a point of disagreement before, I feel obliged to
offer a bit more explanation.

Philosophically I still agree that for a completely generic data
structure / container, it makes sense that you cannot assign a value to
`x[1:5]` unless the value *does* indeed have size `(5,)`. However, I
think in this PR I am rejecting the premise of that argument.
Specifically, the use of VNT is so closely tied to a DPPL model (and
especially more so since templates / shadow arrays were implemented),
that I don't think that VNT is truly a generic data structure, and I
think it's okay to rely on point (2) above, and to sacrifice some purity
for pragmatism.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants