Skip to content

Conversation

@penelopeysm
Copy link
Member

@penelopeysm penelopeysm commented Nov 20, 2025

This adds a new type parameter to the model struct, which indicates whether threadsafe evaluation is required or not.

The default is false, so users must specifically opt-in. It's easy to make this opt-out instead, by changing the default to true. But that would probably tank performance on single-threaded sessions.

If a user declares a model with Threads.@threads inside, it will issue a warning. This is not a foolproof detection method. But it will probably catch 95% of cases.

I believe this is the best technical solution. #1128 provides a global on/off switch, but that is quite slow. Furthermore, it's not technically correct either; threadsafe evaluation is a property of a model, not of a Julia session.

Here is a demo (launch Julia with > 1 thread, or else you won't observe this):

julia> using DynamicPPL, Distributions

julia> @model function f(x)
           a ~ Normal()
           Threads.@threads for i in eachindex(x)
               x[i] ~ Normal(a)
           end
       end
┌ Warning: It looks like you are using `Threads.@threads` in your model definition.
│
│ Note that since version 0.39 of DynamicPPL, threadsafe evaluation of models is disabled by default. If you need it, you will need to explicitly enable it by creating the model, and then running `model = setthreadsafe(model, true)`.
│
│ Avoiding threadsafe evaluation can often lead to significant performance improvements. Please see https://turinglang.org/docs/THIS_PAGE_DOESNT_EXIST_YET for more details of when threadsafe evaluation is actually required.
└ @ DynamicPPL ~/ppl/dppl/src/compiler.jl:381
f (generic function with 2 methods)

julia> x = randn(1000); unsafe_model = f(x);

julia> correct_logjoint(a) = logpdf(Normal(), a) + sum(logpdf.(Normal(a), x))
correct_logjoint (generic function with 1 method)

julia> correct_logjoint(0.5)
-1531.2868766878232

julia> logjoint(unsafe_model, (; a = 0.5))
-747.7508803635657

julia> logjoint(unsafe_model, (; a = 0.5))
-805.0710717034472

julia> logjoint(unsafe_model, (; a = 0.5))
-576.5499112280947

The above gives wrong results because threadsafe evaluation wasn't requested. This PR lets you do that:

julia> safe_model = setthreadsafe(unsafe_model, true)
Model{typeof(f), (:x,), (), (), Tuple{Vector{Float64}}, Tuple{}, DefaultContext, true}(f, (x = [1.7022725865773345, 0.24976102119304136, 0.3819156217493525, -2.4421990126257653, 1.0112387777431466, 0.0016047673674860264, -0.2252466150401273, 1.0744166063416623, -0.629808413260951, 0.041422940392546084    -0.06903422786623391, 0.7349873184231311, 1.1795733883972002, -0.8759768619489093, 1.1966743339419046, 0.850590281127217, 0.45075481294292064, -0.9911445478594456, 0.3609019495825119, 1.2313172493323474],), NamedTuple(), DefaultContext())

julia> logjoint(safe_model, (; a = 0.5))
-1531.286876687823

julia> logjoint(safe_model, (; a = 0.5))
-1531.286876687823

julia> logjoint(safe_model, (; a = 0.5))
-1531.286876687823

@penelopeysm penelopeysm changed the base branch from main to breaking November 20, 2025 14:40
"It looks like you are using `Threads.@threads` in your model definition." *
"\n\nNote that since version 0.39 of DynamicPPL, threadsafe evaluation of models is disabled by default." *
" If you need it, you will need to explicitly enable it by creating the model, and then running `model = setthreadsafe(model, true)`." *
"\n\nAvoiding threadsafe evaluation can often lead to significant performance improvements. Please see https://turinglang.org/docs/THIS_PAGE_DOESNT_EXIST_YET for more details of when threadsafe evaluation is actually required."
Copy link
Member Author

@penelopeysm penelopeysm Nov 20, 2025

Choose a reason for hiding this comment

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

@github-actions
Copy link
Contributor

github-actions bot commented Nov 20, 2025

Benchmark Report

  • this PR's head: c97858a9cf55d82c06d00dc81a360953c9ac07cb
  • base branch: 3cd8d3431e14ebc581266c1323d1db8a5bd4c0eb

Computer Information

Julia Version 1.11.7
Commit f2b3dbda30a (2025-09-08 12:10 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 │    403.74 │    389.55 │    1.04 │ 11.41 │   12.05 │    0.95 │   4608.22 │   4695.93 │    0.98 │
│                   LDA │    12 │ reversediff │             typed │   true │   2944.80 │   2769.14 │    1.06 │  1.91 │    2.01 │    0.95 │   5614.60 │   5552.74 │    1.01 │
│   Loop univariate 10k │ 10000 │    mooncake │             typed │   true │ 150827.83 │ 151301.82 │    1.00 │  5.71 │    5.57 │    1.02 │ 860758.11 │ 842884.32 │    1.02 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼───────────┼───────────┼─────────┼───────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│    Loop univariate 1k │  1000 │    mooncake │             typed │   true │  13528.79 │  15301.01 │    0.88 │  6.26 │    5.07 │    1.24 │  84723.67 │  77532.04 │    1.09 │
│      Multivariate 10k │ 10000 │    mooncake │             typed │   true │  31065.23 │  31048.73 │    1.00 │  9.85 │    9.77 │    1.01 │ 306147.25 │ 303481.00 │    1.01 │
│       Multivariate 1k │  1000 │    mooncake │             typed │   true │   3721.88 │   3781.08 │    0.98 │  8.43 │    8.22 │    1.03 │  31379.24 │  31070.44 │    1.01 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼───────────┼───────────┼─────────┼───────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│ Simple assume observe │     1 │ forwarddiff │             typed │  false │     16.78 │     17.42 │    0.96 │  1.89 │    1.79 │    1.06 │     31.74 │     31.16 │    1.02 │
│           Smorgasbord │   201 │ forwarddiff │             typed │  false │   2487.51 │   2484.93 │    1.00 │ 46.24 │   48.42 │    0.96 │ 115034.21 │ 120314.73 │    0.96 │
│           Smorgasbord │   201 │ forwarddiff │       simple_dict │   true │  22659.87 │  22597.27 │    1.00 │ 24.54 │   25.99 │    0.94 │ 556074.02 │ 587331.18 │    0.95 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼───────────┼───────────┼─────────┼───────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ forwarddiff │ simple_namedtuple │   true │   1033.96 │   1037.67 │    1.00 │ 77.09 │   78.55 │    0.98 │  79709.57 │  81508.91 │    0.98 │
│           Smorgasbord │   201 │      enzyme │             typed │   true │   2552.65 │   2541.51 │    1.00 │  4.55 │    4.55 │    1.00 │  11615.71 │  11570.87 │    1.00 │
│           Smorgasbord │   201 │    mooncake │             typed │   true │   2558.43 │   2542.96 │    1.01 │  5.77 │    5.67 │    1.02 │  14761.71 │  14412.36 │    1.02 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼───────────┼───────────┼─────────┼───────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ reversediff │             typed │   true │   2606.24 │   2558.87 │    1.02 │ 54.50 │   56.31 │    0.97 │ 142028.09 │ 144077.69 │    0.99 │
│           Smorgasbord │   201 │ forwarddiff │      typed_vector │   true │   2558.43 │   2566.11 │    1.00 │ 43.24 │   43.55 │    0.99 │ 110637.95 │ 111754.52 │    0.99 │
│           Smorgasbord │   201 │ forwarddiff │           untyped │   true │   2425.26 │   2270.33 │    1.07 │ 42.20 │   45.25 │    0.93 │ 102355.00 │ 102720.96 │    1.00 │
├───────────────────────┼───────┼─────────────┼───────────────────┼────────┼───────────┼───────────┼─────────┼───────┼─────────┼─────────┼───────────┼───────────┼─────────┤
│           Smorgasbord │   201 │ forwarddiff │    untyped_vector │   true │   2312.45 │   2310.99 │    1.00 │ 44.68 │   44.47 │    1.00 │ 103329.53 │ 102775.88 │    1.01 │
│              Submodel │     1 │    mooncake │             typed │   true │     25.96 │     24.73 │    1.05 │  5.17 │    5.12 │    1.01 │    134.13 │    126.57 │    1.06 │
└───────────────────────┴───────┴─────────────┴───────────────────┴────────┴───────────┴───────────┴─────────┴───────┴─────────┴─────────┴───────────┴───────────┴─────────┘

@github-actions
Copy link
Contributor

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

@codecov
Copy link

codecov bot commented Nov 20, 2025

Codecov Report

❌ Patch coverage is 97.05882% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.16%. Comparing base (3cd8d34) to head (c97858a).

Files with missing lines Patch % Lines
src/model.jl 94.11% 1 Missing ⚠️
Additional details and impacted files
@@             Coverage Diff              @@
##           breaking    #1151      +/-   ##
============================================
- Coverage     81.67%   81.16%   -0.52%     
============================================
  Files            42       42              
  Lines          3930     3934       +4     
============================================
- Hits           3210     3193      -17     
- Misses          720      741      +21     

☔ 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 mhauru November 20, 2025 18:46
Copy link
Member

@mhauru mhauru left a comment

Choose a reason for hiding this comment

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

Very happy with this. Only some minor style points to raise, and to wait for that docs page to be up.

contextualize
```

Some models require threadsafe evaluation (see https://turinglang.org/docs/THIS_DOESNT_EXIST_YET for more information on when this is necessary).
Copy link
Member

Choose a reason for hiding this comment

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

Just a reminder note to change this before merging.

src/fasteval.jl Outdated
Comment on lines 240 to 246
accs = map(
acc -> DynamicPPL.convert_eltype(float_type_with_fallback(eltype(params)), acc),
accs,
)
vi_wrapped = ThreadSafeVarInfo(OnlyAccsVarInfo(accs))
_, vi_wrapped = DynamicPPL._evaluate!!(model, vi_wrapped)
vi = OnlyAccsVarInfo(DynamicPPL.getaccs(vi_wrapped))
Copy link
Member

Choose a reason for hiding this comment

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

Would it be simpler to have a single method and wrap this part in a if is_threaded(f.model)? It should get constant propagated away at compile time. Very optional to change, the current version isn't bad either.

This probably purely a style question, but there could be a difference in that listing all the type parameters of Model in the function signature I think forces specialisation on all of them.

Copy link
Member Author

@penelopeysm penelopeysm Nov 21, 2025

Choose a reason for hiding this comment

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

Yes, good idea, I was a bit concerned about the specialisation too. I thought about making threaded the first type parameter, which would also avoid this (and we don't rarely dispatch on any other type parameters in Model)... then decided against it because it might be too breaking

args::NamedTuple{argnames,Targs},
defaults::NamedTuple{kwargnames,Tkwargs},
context::AbstractContext=DefaultContext(),
threadsafe::Bool=false,
Copy link
Member

Choose a reason for hiding this comment

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

Is there a particular meaning to sometimes calling this threadsafe and sometimes Threaded?

Copy link
Member Author

Choose a reason for hiding this comment

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

Threaded is a type parameter and threadsafe is a function argument 😄

A bit like Ctx and context.

Open to different names though!

src/compiler.jl Outdated
# If it's a macro, we expand it
if Meta.isexpr(expr, :macrocall)
return generate_mainbody!(mod, found, macroexpand(mod, expr; recursive=true), warn)
if expr.args[1] == Expr(:., :Threads, QuoteNode(Symbol("@threads"))) &&
Copy link
Member

Choose a reason for hiding this comment

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

I wonder how often people do

using Threads: @threads
@threads

vs how often people call some other macro called @threads. I.e. false positives vs false negatives.

Copy link
Member Author

Choose a reason for hiding this comment

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

Another case where I wasn't too sure where to draw the line. I think Threads.@threads probably accounts for most usage, but I'm not averse to also handling @threads since after all the warning message is quite noncommittal.

Copy link
Member

Choose a reason for hiding this comment

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

I would err on the safe side in warning about @threads, but happy if you prefer otherwise.

Copy link
Member Author

Choose a reason for hiding this comment

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

Agreed, changed now

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.

3 participants