Skip to content

Reject tilde assignments that replace containers under thread-safe evaluation - #1521

Merged
yebai merged 7 commits into
mainfrom
threaded
Oct 23, 2026
Merged

yebai merged 7 commits into
mainfrom
threaded

Conversation

@yebai

@yebai yebai commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

Under thread-safe evaluation, x = zeros(n) with a threaded x[i] ~ Normal() can give wrong ForwardDiff gradients without an error: BangBang widens x to a new Vector{Dual}, each task rebinds the shared x to its own copy, and writes are lost. This turned up while reviewing the fix for #1519 (TuringLang/AbstractPPL.jl#200), which writes in place and so cannot cover a container that must widen.

Thread-safe evaluation now throws an ArgumentError when a tilde assignment, threaded or not, leaves any container on its path not === to what it was. Only the written leaf may change. Checking types alone is not enough: rebinding a NamedTuple shared across tasks keeps its type yet loses writes. Slices in the middle of a path are written through views, so x[:, j] .~ Normal() and x[1:2][i] ~ Normal() update x in place. The error recommends ::Type{T}=Float64 with Vector{T}(undef, n), Matrix{T} columns for vector-valued tildes, and a Vector in place of a NamedTuple, tuple or static array. Without thread-safe mode nothing is checked.

Two patterns that worked before now throw under thread-safe evaluation. One is a tilde on a field of a NamedTuple, tuple or static array, even within a single task. The other is imputing missing entries of a data argument under AD; passing the data as Vector{Union{Missing,Real}} restores it.

The docs warn that threaded evaluation can reorder parameters between constructions; a test checks that to_vector_params follows each function's mapping.

@codecov

codecov Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 84.81%. Comparing base (46b6162) to head (a592b3b).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1521      +/-   ##
==========================================
+ Coverage   84.70%   84.81%   +0.10%     
==========================================
  Files          52       52              
  Lines        4590     4616      +26     
==========================================
+ Hits         3888     3915      +27     
+ Misses        702      701       -1     

☔ View full report in Codecov by Harness.
📢 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
Copy Markdown
Contributor

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

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Benchmarks

Performance Ratio: gradient time divided by log-density time.

For very small models these ratios are noisy across runs and machines; raw primal and gradient timings are more reliable. The benchmarks are aimed at DynamicPPL developers and mainly catch obvious allocation or type-stability regressions. See benchmark notes for details.

Primal times: main 46b6162, PR a592b3b

===============================================================================================================
                                                    primal                            gradient                 
                                            ----------------------  -------------------------------------------
Model                        dim    linked        main          PR     FwdDiff    RvsDiff    Mooncake    Enzyme
---------------------------------------------------------------------------------------------------------------
Simple assume observe*         1     false     5.25 ns     2.82 ns       11.93    1274.44       10.77      8.27
Simple assume observe*         1      true     5.27 ns     2.99 ns       11.39    1168.72       10.08      8.32
Smorgasbord                  201     false     5.36 μs     3.01 μs       90.67     128.25        5.29      1.29
Smorgasbord                  201      true     6.48 μs      3.5 μs       86.99     146.38        6.38      1.47
Loop univariate 1k          1000     false     16.7 μs     7.66 μs     1495.71     360.05        3.84      0.92
Loop univariate 1k          1000      true     18.0 μs     10.3 μs     1524.16     293.58        2.97      0.72
Multivariate 1k             1000     false    947.0 ns    727.0 ns     3534.78     453.80        1.40      2.51
Multivariate 1k             1000      true    926.0 ns    699.0 ns     3823.96     498.54        1.41      2.51
Loop univariate 10k        10000     false    163.0 μs     77.1 μs    32527.89     499.37        3.86      0.81
Loop univariate 10k        10000      true    176.0 μs    102.0 μs    28846.73     365.75        2.93      0.65
Multivariate 10k           10000     false     9.65 μs     5.95 μs    70513.11     708.72        1.24      2.01
Multivariate 10k           10000      true     10.0 μs     6.36 μs    67993.30     647.80        1.19      2.03
Dynamic                      err     false     1.33 μs    877.0 ns         err     100.31       15.45      4.85
Dynamic                       10      true     1.73 μs     1.24 μs        1.97      80.91       11.79      9.59
Submodel*                      1     false     5.26 ns     2.84 ns       12.36    2263.15       10.91      8.94
Submodel*                      1      true     5.26 ns     2.89 ns       12.16    2170.84       10.77      9.01
LDA                            6      true     3.52 μs     2.26 μs        1.49      20.65       42.09     27.90
===============================================================================================================
Environment
Julia Version 1.13.2
Commit 45b6d15e34b (2026-10-14 20:33 UTC)
Build Info:
  Official https://julialang.org release
Platform Info:
  OS: Linux (x86_64-linux-gnu)
  CPU: 4 × AMD EPYC 9V45 96-Core Processor
  WORD_SIZE: 64
  LLVM: libLLVM-20.1.8 (ORCJIT, znver5)
  GC: Built with stock GC
Threads: 1 default, 1 interactive, 1 GC (on 4 virtual cores)

@shravanngoswamii shravanngoswamii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I pushed some docs changes. Thank you @yebai, Looks good to me!

yebai added 2 commits October 22, 2026 11:16
Rebinding shared immutable values such as `x.a ~ Normal()` can lose
writes even when their types stay unchanged.

Require each container on a tilde path to remain identical. Traverse
intermediate array slices through views and update only the leaf.

Immutable replacements now throw even in task-local code without AD.
Use mutable storage allocated with the parameter element type.

Assisted-by: Codex <codex@openai.com>
@yebai

yebai commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

@shravanngoswamii Thanks for the docs commit. Two commits on top of it change the rule, and they replace the (a = zero(T),) remedy. A NamedTuple written by several tasks keeps its type, but each task rebinds its own copy, so writes are lost without an error. With 2 threads, a model with x.a ~ and x.b ~ in different tasks gave wrong ForwardDiff gradients in 2 of 2000 evaluations on Julia 1.10.12 and 3 of 2000 on 1.12.7. The check now requires every container on a tilde's path to stay ===, and it writes through slices in place, so x[:, j] .~ keeps working. Your Vector{Union{Missing,Real}} remedy is unchanged. Could you take another look?

Assisted-by: Codex <codex@openai.com>
@shravanngoswamii

Copy link
Copy Markdown
Member

Is Ref meant to work through a LogDensityFunction? It throws there, init!! is fine.

Assisted-by: Codex <codex@openai.com>
@yebai

yebai commented Oct 22, 2026

Copy link
Copy Markdown
Member Author

Nice catch. It shouldn't have been recommended. On main too, LogDensityFunction rejects r[] ~: value collection supports indexing only into AbstractArrays. init!! works only under thread-safe evaluation, via this PR's RefValue setter; serially it fails in BangBang. The latest commit drops that setter and the Ref advice.

@shravanngoswamii shravanngoswamii left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM! Thank you @yebai!

@yebai
yebai added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 5355717 Oct 23, 2026
23 checks passed
@yebai
yebai deleted the threaded branch October 23, 2026 06:28
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