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: 76ac5b617218f093c35dfd0b3403f9ba10902a7f
  • 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 × 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 │           VarInfo │ Linked │     base │  this PR │ speedup │   base │ this PR │ speedup │      base │   this PR │ speedup │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│               Dynamic │    10 │    mooncake │             typed │   true │   347.80 │   337.33 │    1.03 │  10.74 │   10.00 │    1.07 │   3736.43 │   3371.83 │    1.11 │
│                   LDA │    12 │ reversediff │             typed │   true │  2385.11 │  2404.07 │    0.99 │   5.18 │    5.07 │    1.02 │  12343.09 │  12180.70 │    1.01 │
│   Loop univariate 10k │ 10000 │    mooncake │             typed │   true │ 55965.53 │ 54600.77 │    1.02 │   6.17 │    6.36 │    0.97 │ 345429.15 │ 347456.36 │    0.99 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│    Loop univariate 1k │  1000 │    mooncake │             typed │   true │  5405.26 │  5403.79 │    1.00 │   5.84 │    5.86 │    1.00 │  31562.14 │  31649.26 │    1.00 │
│      Multivariate 10k │ 10000 │    mooncake │             typed │   true │ 29246.13 │ 30261.24 │    0.97 │  10.29 │   10.16 │    1.01 │ 301002.72 │ 307331.40 │    0.98 │
│       Multivariate 1k │  1000 │    mooncake │             typed │   true │  3262.33 │  7565.02 │    0.43 │   9.43 │    5.49 │    1.72 │  30759.60 │  41521.69 │    0.74 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Simple assume observe │     1 │ forwarddiff │             typed │  false │     2.44 │     2.40 │    1.02 │   3.80 │    3.87 │    0.98 │      9.29 │      9.29 │    1.00 │
│           Smorgasbord │   201 │ forwarddiff │             typed │  false │  1000.04 │  1001.95 │    1.00 │ 135.11 │  132.92 │    1.02 │ 135114.90 │ 133179.33 │    1.01 │
│           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 │  1386.12 │  1359.01 │    1.02 │   6.98 │    6.90 │    1.01 │   9672.37 │   9371.28 │    1.03 │
│           Smorgasbord │   201 │    mooncake │             typed │   true │  1386.60 │  1356.61 │    1.02 │   5.73 │    5.88 │    0.97 │   7951.15 │   7980.89 │    1.00 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ reversediff │             typed │   true │  1380.76 │  1354.69 │    1.02 │ 102.56 │  102.74 │    1.00 │ 141613.37 │ 139177.19 │    1.02 │
│           Smorgasbord │   201 │ forwarddiff │      typed_vector │   true │  1366.77 │  1393.60 │    0.98 │  64.46 │   62.97 │    1.02 │  88107.84 │  87752.77 │    1.00 │
│           Smorgasbord │   201 │ forwarddiff │           untyped │   true │  1399.14 │  1360.50 │    1.03 │  64.76 │   64.53 │    1.00 │  90602.95 │  87789.76 │    1.03 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼──────────┼──────────┼─────────┼────────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ forwarddiff │    untyped_vector │   true │  1360.99 │  1360.45 │    1.00 │  62.89 │   64.84 │    0.97 │  85593.64 │  88210.81 │    0.97 │
│              Submodel │     1 │    mooncake │             typed │   true │     3.08 │     2.99 │    1.03 │  10.89 │   10.97 │    0.99 │     33.50 │     32.86 │    1.02 │
└───────────────────────┴───────┴─────────────┴───────────────────┴────────┴──────────┴──────────┴─────────┴────────┴─────────┴─────────┴───────────┴───────────┴─────────┘

@codecov
Copy link

codecov bot commented Dec 16, 2025

Codecov Report

❌ Patch coverage is 71.21212% with 19 lines in your changes missing coverage. Please review.
✅ Project coverage is 80.18%. Comparing base (51b399a) to head (76ac5b6).

Files with missing lines Patch % Lines
src/test_utils/models.jl 68.62% 16 Missing ⚠️
src/varnamedtuple.jl 80.00% 3 Missing ⚠️
Additional details and impacted files
@@                    Coverage Diff                    @@
##           mhauru/arraylikeblock    #1181      +/-   ##
=========================================================
+ Coverage                  80.13%   80.18%   +0.05%     
=========================================================
  Files                         42       42              
  Lines                       4298     4356      +58     
=========================================================
+ Hits                        3444     3493      +49     
- Misses                       854      863       +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.

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

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

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