Skip to content

Conversation

@mhauru
Copy link
Member

@mhauru mhauru commented Dec 16, 2025

This PR primarily adds support for concretized slices to VarNamedTuple. That is actually all that is needed to make it support (I think) all the same : syntax we previously supported in @model. Turns out we were already concretising all VarNames in the model body itself, so there was little to do.

The PR also addresses some related issues, adds a test for :s (we previously had zero models in our test suite that used : in an assume statement), and starts a HISTORY.md entry.

If we are happy with all the new limitations on square bracket indexing, we could say that this makes VNT-for-LDF feature complete. However, adding support for dictionaries shouldn't be too hard, and maybe linear indexing wouldn't be awful either, so I would give those a day's worth of work before deciding that we just flat out ban them. Would be a shame to have a few DPPL versions for which they don't work, just to then add support in a few week's time.

To add support for the aforementioned cases (see also the HISTORY.md entry), my idea is that you would somehow provide VNT with information about which type of object this VarName that has an IndexLens is indexing, and you could then do different things with make_leaf and PartialArray depending on the type. That would solve the Dict and linear indexing cases. Unusually indexed arrays would be harder, I don't know how to make e.g. OffsetArrays work without manually writing a version of PartialArray that explicitly uses them. Maybe there could be some way to do this generically for any AbstractArray subtype, but I worry that the interface for AbstractArray may not be well-defined enough for that.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 16, 2025

Benchmark Report

  • this PR's head: 57fd84b20c0ad7c251a415739a64ef186fafa2c7
  • base branch: 51b399aeb1f3c4ee29e1029215668b47847e0a15

Computer Information

Julia Version 1.11.8
Commit cf1da5e20e3 (2025-11-06 17:49 UTC)
Build Info:
  Official https://julialang.org/ release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
  WORD_SIZE: 64
  LLVM: libLLVM-16.0.6 (ORCJIT, icelake-server)
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 │           VarInfo │ Linked │     base │  this PR │ speedup │   base │ this PR │ speedup │      base │   this PR │ speedup │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│               Dynamic │    10 │    mooncake │             typed │   true │   372.08 │   382.75 │    0.97 │  10.34 │    9.87 │    1.05 │   3846.76 │   3778.83 │    1.02 │
│                   LDA │    12 │ reversediff │             typed │   true │  2791.45 │  2789.78 │    1.00 │   4.83 │    4.87 │    0.99 │  13491.67 │  13593.06 │    0.99 │
│   Loop univariate 10k │ 10000 │    mooncake │             typed │   true │ 58045.66 │ 60982.08 │    0.95 │   6.51 │    6.06 │    1.07 │ 377775.67 │ 369496.14 │    1.02 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│    Loop univariate 1k │  1000 │    mooncake │             typed │   true │  5845.72 │  6157.17 │    0.95 │   5.95 │    5.49 │    1.08 │  34777.60 │  33800.85 │    1.03 │
│      Multivariate 10k │ 10000 │    mooncake │             typed │   true │ 32227.56 │ 32867.79 │    0.98 │  10.21 │   14.00 │    0.73 │ 328975.79 │ 460110.03 │    0.71 │
│       Multivariate 1k │  1000 │    mooncake │             typed │   true │  3548.30 │  3554.08 │    1.00 │   9.43 │    9.37 │    1.01 │  33448.41 │  33301.67 │    1.00 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Simple assume observe │     1 │ forwarddiff │             typed │  false │     2.68 │     2.59 │    1.03 │   3.83 │    3.90 │    0.98 │     10.27 │     10.10 │    1.02 │
│           Smorgasbord │   201 │ forwarddiff │             typed │  false │  1095.25 │  1103.36 │    0.99 │  70.08 │   66.75 │    1.05 │  76759.27 │  73644.92 │    1.04 │
│           Smorgasbord │   201 │ forwarddiff │       simple_dict │   true │      err │      err │     err │    err │     err │     err │       err │       err │     err │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ forwarddiff │ simple_namedtuple │   true │      err │      err │     err │    err │     err │     err │       err │       err │     err │
│           Smorgasbord │   201 │      enzyme │             typed │   true │  1480.99 │  1974.36 │    0.75 │   6.45 │    5.81 │    1.11 │   9549.22 │  11471.23 │    0.83 │
│           Smorgasbord │   201 │    mooncake │             typed │   true │  1501.53 │  1548.41 │    0.97 │   5.73 │    5.67 │    1.01 │   8599.24 │   8773.13 │    0.98 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ reversediff │             typed │   true │  1503.17 │  1532.86 │    0.98 │ 102.20 │   99.54 │    1.03 │ 153620.00 │ 152574.21 │    1.01 │
│           Smorgasbord │   201 │ forwarddiff │      typed_vector │   true │  1494.68 │  1508.11 │    0.99 │  64.31 │   65.32 │    0.98 │  96120.52 │  98505.54 │    0.98 │
│           Smorgasbord │   201 │ forwarddiff │           untyped │   true │  1493.63 │  1501.69 │    0.99 │ 133.67 │   64.69 │    2.07 │ 199657.67 │  97150.37 │    2.06 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ forwarddiff │    untyped_vector │   true │  1505.80 │  1520.17 │    0.99 │  65.30 │   63.54 │    1.03 │  98328.57 │  96594.88 │    1.02 │
│              Submodel │     1 │    mooncake │             typed │   true │     3.54 │     3.30 │    1.07 │   9.97 │   11.01 │    0.91 │     35.31 │     36.35 │    0.97 │
└───────────────────────┴───────┴─────────────┴───────────────────┴────────┴──────────┴──────────┴─────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘

@codecov
Copy link

codecov bot commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 81.71429% with 32 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (mhauru/vnt-for-fastldf@44be19d). Learn more about missing BASE report.

Files with missing lines Patch % Lines
src/test_utils/models.jl 63.63% 20 Missing ⚠️
src/varnamedtuple.jl 88.88% 12 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                    @@
##             mhauru/vnt-for-fastldf    #1181   +/-   ##
=========================================================
  Coverage                          ?   80.18%           
=========================================================
  Files                             ?       42           
  Lines                             ?     4356           
  Branches                          ?        0           
=========================================================
  Hits                              ?     3493           
  Misses                            ?      863           
  Partials                          ?        0           

☔ 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.

@github-actions
Copy link
Contributor

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

Comment on lines +69 to +74
using OffsetArrays
x = OffsetArray(Vector{Float64}(undef, 3), -3)
x[-2] ~ Normal()
0.0 ~ Normal(x[-2])
```

Copy link
Member

Choose a reason for hiding this comment

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

it strikes me that a lot of the problems we have here are actually incredibly similar to the problems of creating shadow memory in AD packages. the idea is that inside the model there is an x, but inside the VNT there is a shadow copy of x as well. cc @yebai

Copy link
Member

@penelopeysm penelopeysm Dec 20, 2025

Choose a reason for hiding this comment

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

I think one possible general solution to this is going to be something that's similar to what ForwardDiff does (at least for AbstractArray). Technically, it's even closer to a sparsity tracer.

When x is initialised to an OffsetArray{T}, we want the shadow of x (let's call it dx) to be initialised to the same OffsetArray{Dual{T}}. Each of these 'duals' needs to carry the value, along with the boolean mask indicating whether it was set or not. This is of course exactly the same as how PartialArray has one array of values and one array of masks.

struct Dual{T}
   val::T
   has_been_set::Bool
end
Dual(val) = Dual(val, false)

Then when x[idxs...] is set to whatever rand(Normal()) returns, we set dx[idxs...] = Dual(x[idxs...], true). Of course, the block elements will need the same workaround as before (complete with all the typejoin shenanigans).

struct BlockDual{T}
    val::T
    original_indices::whatever
end

Then, reading from the shadow memory becomes way more consistent, because indexing into dx carries the same semantics as indexing into x.

For another example, let's say that x and dx are regular Base.Matrix. Then when you index into dx[1, 1], it will be the same as indexing into dx[1]. You could even make dx[1] = ... error when you try to modify a value that was already set. For BlockDuals, you could further 'resolve' or 'normalise' the indices into a standard, 1-based scheme because you now know the type of the container (I think Base.to_indices does this, but not sure; in any case this should be easy to figure out). That allows you to use different indexing schemes to read the data, as long as they refer to the same elements.

A scheme like this would also enable 'native support' for not only OffsetArray, but also things like DimensionalData.DimArray which probably has way more legitimate uses than OffsetArray.

This looks like a lot of work for VNT to do, but we are already duplicating the entire array in the VNT*, so in principle I don't think performance should be any worse. Indexing into this would still be type stable (in the absence of blocks, but that's the same as currently) and you would still be able to do all the checks you need on whether the value was set or not.

In particular, I think that this should be fairly easy to implement for all AbstractArrays. (In fact, we know for a fact that it must be possible, because other packages do this successfully.) Extending this to dictionaries, or generally any type that supports getindex, would be annoying (for every new type you implement, you have to define the equivalent of a 'tangent type'†); it's doable, but it could be left for a later stage. I think the AbstractArray support would already solve 99% of pain points with this, and the path forwards to supporting other types would be clear, and when some type isn't supported, you can error with a very clear error message saying that "indexing into this type on LHS of tilde is not supported", whereas now our battle lines are quite arbitrarily drawn.


* OK, there is one case where this will do a lot more work than VNT currently: if the user has a very large array, like a length-100 vector, but only x[1] ~ ... and never the other elements. I don't really care about optimising performance for such cases. It is impossible to detect these without static analysis (and note that AD has a similar characteristic: if you differentiate f(x) = x[1] and pass in a length-100 vector, the gradient calculation will be equally unoptimised).

† Notice that the way VNT normalises all property access into NamedTuples is exactly what Mooncake's tangent_type does for structs.

Copy link
Member

Choose a reason for hiding this comment

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

(Note that, since tilde_assume!! is currently our only 'hook' into modifying the VNT, this means that you have to pass x into tilde_assume!! as an argument. But I am not opposed to that at all. The alternative would be that you have to inspect the IR to determine when new arrays are being created.)

Copy link
Member

Choose a reason for hiding this comment

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

I think this is a sound idea. VNT effectively carries a shadow copy for each LHS variable in tilde statements, and the dual-number mechanism used in autograd can be borrowed.

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 like this a lot. I had vague thoughts for something like this, some sort of minimal interface to add support for new container types, but I hadn't realised that we can just copy what AD packages do.

@penelopeysm
Copy link
Member

penelopeysm commented Dec 23, 2025

In fact, I would go even further and say that if VNT stores a copy of the array, there is no need for VarName concretisation at all. That is, begin and end and colons can just stay as is, and then there is never any need to worry about the difference.

The point is that, if someone wants to write x[end] ~ ... that's fine. The only time you ever need to figure out what end means, is when you getindex or setindex into x, and you will always have that information about x when you try to do so.

Now, the original problem with VarInfo was that VarInfo never stored what x is. But if you have a shadow copy of x, which (by definition) obeys exactly the same indexing semantics, this completely removes the problem because any time you need to use the index, you have the container.

Apart from this, I think there's genuinely no reason why end ever needs to be resolved into 5 or whatever the last index of x is. Furthermore, sometimes it doesn't even make sense to resolve it. Consider the following example, a classic variable-length-vector model.

using Turing, FlexiChains
@model function f()
    x ~ Poisson(1.0)
    y ~ MvNormal(zeros(x + 1), I)
end

chn = sample(f(), Prior(), 100; chain_type=VNChain)

Now, one can argue until the cows come home about whether this is a 'meaningful' model (whatever that means). But the chain has no reason to care about this. Regardless of the model, once the chain is created, I should in principle be able to index into the chain with

chn[@varname(x[end])]

to obtain the last element of each x. But right now you can't do that, because end must be concretised with respect to one particular x. In fact, AbstractPPL won't even let you create a varname with end inside it without concretising it with respect to whatever x is. That is quite frustrating, because in the example above, there is not even an x in scope (and there's no reason for it to be).

julia> using AbstractPPL; @varname(x[end])
ERROR: UndefVarError: `x` not defined in `Main`

The same considerations apply to x[:]. These problems are all completely sidestepped by keeping end unresolved until it needs to be resolved (i.e., at getindex/setindex! time). It also avoids the nasty issues where concretised colons and normal colons don't compare the same.

This is pretty much the path I'm going down with TuringLang/AbstractPPL.jl#150.

@mhauru mhauru requested a review from penelopeysm January 5, 2026 17:10
Comment on lines 116 to 120
# The below type inference fails on v1.10.
@test begin
@inferred LogDensityProblems.logdensity(ldf, x)
true
end skip = (VERSION < v"1.11.0")
Copy link
Member

Choose a reason for hiding this comment

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

Does it fail for all models?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, now narrowed to a single model. Good catch.

Comment on lines +578 to +582
# TODO(mhauru) The below element type concretisation is because of
# https://github.com/JuliaFolds2/BangBang.jl/issues/39
# which causes, when this is evaluated with an untyped VarInfo, s_vec to be an
# Array{Any}.
s_vec = [x for x in s_vec]
Copy link
Member

Choose a reason for hiding this comment

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

Is untyped varinfo on the way out eventually?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, hopefully already in #1183.

Copy link
Member

@penelopeysm penelopeysm left a comment

Choose a reason for hiding this comment

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

happy to merge, given that we're deferring shadow copies to a future time

@mhauru mhauru marked this pull request as ready for review January 6, 2026 14:42
Base automatically changed from mhauru/arraylikeblock to mhauru/vnt-for-fastldf January 6, 2026 15:51
@mhauru mhauru force-pushed the mhauru/vnt-for-fastldf branch from fac8641 to 44be19d Compare January 6, 2026 16:13
@mhauru mhauru merged commit 57fd84b into mhauru/vnt-for-fastldf Jan 6, 2026
20 of 21 checks passed
@mhauru mhauru deleted the mhauru/vnt-concretized-slices branch January 6, 2026 16:17
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.

4 participants