Skip to content

Commit ad4175a

Browse files
authored
Update benchmark models (#826)
* Update models to benchmark plus small style changes * Make benchmark times relative. Add benchmark documentation. * Choose whether to show linked or unlinked benchmark times * Make table header more concise
1 parent 1d1b11e commit ad4175a

File tree

4 files changed

+249
-47
lines changed

4 files changed

+249
-47
lines changed

benchmarks/Project.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ version = "0.1.0"
66
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
77
Distributions = "31c24e10-a181-5473-b8eb-7969acd0382f"
88
DynamicPPL = "366bfd00-2699-11ea-058f-f148b4cae6d8"
9+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
910
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
1011
TuringBenchmarking = "0db1332d-5c25-4deb-809f-459bc696f94f"

benchmarks/benchmarks.jl

Lines changed: 82 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,108 @@
1-
using DynamicPPL: @model
2-
using DynamicPPLBenchmarks: make_suite
3-
using BenchmarkTools: median, run
4-
using Distributions: Normal, Beta, Bernoulli
5-
using PrettyTables: pretty_table, PrettyTables
1+
using DynamicPPLBenchmarks: Models, make_suite
2+
using BenchmarkTools: @benchmark, median, run
3+
using PrettyTables: PrettyTables, ft_printf
4+
using Random: seed!
65

7-
# Define models
8-
@model function demo1(x)
9-
m ~ Normal()
10-
x ~ Normal(m, 1)
11-
return (m=m, x=x)
12-
end
6+
seed!(23)
137

14-
@model function demo2(y)
15-
p ~ Beta(1, 1)
16-
N = length(y)
17-
for n in 1:N
18-
y[n] ~ Bernoulli(p)
19-
end
20-
return (; p)
8+
# Create DynamicPPL.Model instances to run benchmarks on.
9+
smorgasbord_instance = Models.smorgasbord(randn(100), randn(100))
10+
loop_univariate1k, multivariate1k = begin
11+
data_1k = randn(1_000)
12+
loop = Models.loop_univariate(length(data_1k)) | (; o=data_1k)
13+
multi = Models.multivariate(length(data_1k)) | (; o=data_1k)
14+
loop, multi
15+
end
16+
loop_univariate10k, multivariate10k = begin
17+
data_10k = randn(10_000)
18+
loop = Models.loop_univariate(length(data_10k)) | (; o=data_10k)
19+
multi = Models.multivariate(length(data_10k)) | (; o=data_10k)
20+
loop, multi
21+
end
22+
lda_instance = begin
23+
w = [1, 2, 3, 2, 1, 1]
24+
d = [1, 1, 1, 2, 2, 2]
25+
Models.lda(2, d, w)
2126
end
22-
23-
demo1_data = randn()
24-
demo2_data = rand(Bool, 10)
25-
26-
# Create model instances with the data
27-
demo1_instance = demo1(demo1_data)
28-
demo2_instance = demo2(demo2_data)
2927

3028
# Specify the combinations to test:
31-
# (Model Name, model instance, VarInfo choice, AD backend)
29+
# (Model Name, model instance, VarInfo choice, AD backend, linked)
3230
chosen_combinations = [
33-
("Demo1", demo1_instance, :typed, :forwarddiff),
34-
("Demo1", demo1_instance, :simple_namedtuple, :zygote),
35-
("Demo2", demo2_instance, :untyped, :reversediff),
36-
("Demo2", demo2_instance, :simple_dict, :forwarddiff),
31+
(
32+
"Simple assume observe",
33+
Models.simple_assume_observe(randn()),
34+
:typed,
35+
:forwarddiff,
36+
false,
37+
),
38+
("Smorgasbord", smorgasbord_instance, :typed, :forwarddiff, false),
39+
("Smorgasbord", smorgasbord_instance, :simple_namedtuple, :forwarddiff, true),
40+
("Smorgasbord", smorgasbord_instance, :untyped, :forwarddiff, true),
41+
("Smorgasbord", smorgasbord_instance, :simple_dict, :forwarddiff, true),
42+
("Smorgasbord", smorgasbord_instance, :typed, :reversediff, true),
43+
# TODO(mhauru) Add Mooncake once TuringBenchmarking.jl supports it. Consider changing
44+
# all the below :reversediffs to :mooncakes too.
45+
#("Smorgasbord", smorgasbord_instance, :typed, :mooncake, true),
46+
("Loop univariate 1k", loop_univariate1k, :typed, :reversediff, true),
47+
("Multivariate 1k", multivariate1k, :typed, :reversediff, true),
48+
("Loop univariate 10k", loop_univariate10k, :typed, :reversediff, true),
49+
("Multivariate 10k", multivariate10k, :typed, :reversediff, true),
50+
("Dynamic", Models.dynamic(), :typed, :reversediff, true),
51+
("Submodel", Models.parent(randn()), :typed, :reversediff, true),
52+
("LDA", lda_instance, :typed, :reversediff, true),
3753
]
3854

39-
results_table = Tuple{String,String,String,Float64,Float64}[]
55+
# Time running a model-like function that does not use DynamicPPL, as a reference point.
56+
# Eval timings will be relative to this.
57+
reference_time = begin
58+
obs = randn()
59+
median(@benchmark Models.simple_assume_observe_non_model(obs)).time
60+
end
61+
62+
results_table = Tuple{String,String,String,Bool,Float64,Float64}[]
4063

41-
for (model_name, model, varinfo_choice, adbackend) in chosen_combinations
64+
for (model_name, model, varinfo_choice, adbackend, islinked) in chosen_combinations
4265
suite = make_suite(model, varinfo_choice, adbackend)
4366
results = run(suite)
67+
result_key = islinked ? "linked" : "standard"
4468

45-
eval_time = median(results["AD_Benchmarking"]["evaluation"]["standard"]).time
69+
eval_time = median(results["evaluation"][result_key]).time
70+
relative_eval_time = eval_time / reference_time
4671

47-
grad_group = results["AD_Benchmarking"]["gradient"]
72+
grad_group = results["gradient"]
4873
if isempty(grad_group)
49-
ad_eval_time = NaN
74+
relative_ad_eval_time = NaN
5075
else
5176
grad_backend_key = first(keys(grad_group))
52-
ad_eval_time = median(grad_group[grad_backend_key]["standard"]).time
77+
ad_eval_time = median(grad_group[grad_backend_key][result_key]).time
78+
relative_ad_eval_time = ad_eval_time / eval_time
5379
end
5480

5581
push!(
5682
results_table,
57-
(model_name, string(adbackend), string(varinfo_choice), eval_time, ad_eval_time),
83+
(
84+
model_name,
85+
string(adbackend),
86+
string(varinfo_choice),
87+
islinked,
88+
relative_eval_time,
89+
relative_ad_eval_time,
90+
),
5891
)
5992
end
6093

6194
table_matrix = hcat(Iterators.map(collect, zip(results_table...))...)
6295
header = [
63-
"Model", "AD Backend", "VarInfo Type", "Evaluation Time (ns)", "AD Eval Time (ns)"
96+
"Model",
97+
"AD Backend",
98+
"VarInfo Type",
99+
"Linked",
100+
"Eval Time / Ref Time",
101+
"AD Time / Eval Time",
64102
]
65-
pretty_table(table_matrix; header=header, tf=PrettyTables.tf_markdown)
103+
PrettyTables.pretty_table(
104+
table_matrix;
105+
header=header,
106+
tf=PrettyTables.tf_markdown,
107+
formatters=ft_printf("%.1f", [5, 6]),
108+
)

benchmarks/src/DynamicPPLBenchmarks.jl

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ using DynamicPPL: VarInfo, SimpleVarInfo, VarName
44
using BenchmarkTools: BenchmarkGroup
55
using TuringBenchmarking: make_turing_suite
66

7-
export make_suite
7+
include("./Models.jl")
8+
using .Models: Models
9+
10+
export Models, make_suite
811

912
"""
1013
make_suite(model, varinfo_choice::Symbol, adbackend::Symbol)
@@ -13,7 +16,7 @@ Create a benchmark suite for `model` using the selected varinfo type and AD back
1316
Available varinfo choices:
1417
• `:untyped` → uses `VarInfo()`
1518
• `:typed` → uses `VarInfo(model)`
16-
• `:simple_namedtuple` → uses `SimpleVarInfo{Float64}(free_nt)`
19+
• `:simple_namedtuple` → uses `SimpleVarInfo{Float64}(model())`
1720
• `:simple_dict` → builds a `SimpleVarInfo{Float64}` from a Dict (pre-populated with the model’s outputs)
1821
1922
The AD backend should be specified as a Symbol (e.g. `:forwarddiff`, `:reversediff`, `:zygote`).
@@ -22,14 +25,13 @@ function make_suite(model, varinfo_choice::Symbol, adbackend::Symbol)
2225
suite = BenchmarkGroup()
2326

2427
vi = if varinfo_choice == :untyped
25-
v = VarInfo()
26-
model(v)
27-
v
28+
vi = VarInfo()
29+
model(vi)
30+
vi
2831
elseif varinfo_choice == :typed
2932
VarInfo(model)
3033
elseif varinfo_choice == :simple_namedtuple
31-
free_nt = NamedTuple{(:m,)}(model()) # Extract only the free parameter(s)
32-
SimpleVarInfo{Float64}(free_nt)
34+
SimpleVarInfo{Float64}(model())
3335
elseif varinfo_choice == :simple_dict
3436
retvals = model()
3537
vns = [VarName{k}() for k in keys(retvals)]
@@ -39,7 +41,7 @@ function make_suite(model, varinfo_choice::Symbol, adbackend::Symbol)
3941
end
4042

4143
# Add the AD benchmarking suite.
42-
suite["AD_Benchmarking"] = make_turing_suite(
44+
suite = make_turing_suite(
4345
model;
4446
adbackends=[adbackend],
4547
varinfo=vi,

benchmarks/src/Models.jl

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
"""
2+
Models for benchmarking Turing.jl.
3+
4+
Each model returns a NamedTuple of all the random variables in the model that are not
5+
observed (this is used for constructing SimpleVarInfos).
6+
"""
7+
module Models
8+
9+
using Distributions:
10+
Categorical,
11+
Dirichlet,
12+
Exponential,
13+
Gamma,
14+
LKJCholesky,
15+
InverseWishart,
16+
Normal,
17+
logpdf,
18+
product_distribution,
19+
truncated
20+
using DynamicPPL: @model, to_submodel
21+
using LinearAlgebra: cholesky
22+
23+
export simple_assume_observe_non_model,
24+
simple_assume_observe, smorgasbord, loop_univariate, multivariate, parent, dynamic, lda
25+
26+
# This one is like simple_assume_observe, but explicitly does not use DynamicPPL.
27+
# Other runtimes are normalised by this one's runtime.
28+
function simple_assume_observe_non_model(obs)
29+
x = rand(Normal())
30+
logp = logpdf(Normal(), x)
31+
logp += logpdf(Normal(x, 1), obs)
32+
return (; logp=logp, x=x)
33+
end
34+
35+
"""
36+
A simple model that does one scalar assumption and one scalar observation.
37+
"""
38+
@model function simple_assume_observe(obs)
39+
x ~ Normal()
40+
obs ~ Normal(x, 1)
41+
return (; x=x)
42+
end
43+
44+
"""
45+
A short model that tries to cover many DynamicPPL features.
46+
47+
Includes scalar, vector univariate, and multivariate variables; ~, .~, and loops; allocating
48+
a variable vector; observations passed as arguments, and as literals.
49+
"""
50+
@model function smorgasbord(x, y, ::Type{TV}=Vector{Float64}) where {TV}
51+
@assert length(x) == length(y)
52+
m ~ truncated(Normal(); lower=0)
53+
means ~ product_distribution(fill(Exponential(m), length(x)))
54+
stds = TV(undef, length(x))
55+
stds .~ Gamma(1, 1)
56+
for i in 1:length(x)
57+
x[i] ~ Normal(means[i], stds[i])
58+
end
59+
y ~ product_distribution([Normal(means[i], stds[i]) for i in 1:length(x)])
60+
0.0 ~ Normal(sum(y), 1)
61+
return (; m=m, means=means, stds=stds)
62+
end
63+
64+
"""
65+
A model that loops over two vectors of univariate normals of length `num_dims`.
66+
67+
The second variable, `o`, is meant to be conditioned on after model instantiation.
68+
69+
See `multivariate` for a version that uses `product_distribution` rather than loops.
70+
"""
71+
@model function loop_univariate(num_dims, ::Type{TV}=Vector{Float64}) where {TV}
72+
a = TV(undef, num_dims)
73+
o = TV(undef, num_dims)
74+
for i in 1:num_dims
75+
a[i] ~ Normal(0, 1)
76+
end
77+
m = sum(a)
78+
for i in 1:num_dims
79+
o[i] ~ Normal(m, 1)
80+
end
81+
return (; a=a)
82+
end
83+
84+
"""
85+
A model with two multivariate normal distributed variables of dimension `num_dims`.
86+
87+
The second variable, `o`, is meant to be conditioned on after model instantiation.
88+
89+
See `loop_univariate` for a version that uses loops rather than `product_distribution`.
90+
"""
91+
@model function multivariate(num_dims, ::Type{TV}=Vector{Float64}) where {TV}
92+
a = TV(undef, num_dims)
93+
o = TV(undef, num_dims)
94+
a ~ product_distribution(fill(Normal(0, 1), num_dims))
95+
m = sum(a)
96+
o ~ product_distribution(fill(Normal(m, 1), num_dims))
97+
return (; a=a)
98+
end
99+
100+
"""
101+
A submodel for `parent`. Not exported.
102+
"""
103+
@model function sub()
104+
x ~ Normal()
105+
return x
106+
end
107+
108+
"""
109+
Like simple_assume_observe, but with a submodel for the assumed random variable.
110+
"""
111+
@model function parent(obs)
112+
x ~ to_submodel(sub())
113+
obs ~ Normal(x, 1)
114+
return (; x=x)
115+
end
116+
117+
"""
118+
A model with random variables that have changing support under linking, or otherwise
119+
complicated bijectors.
120+
"""
121+
@model function dynamic(::Type{T}=Vector{Float64}) where {T}
122+
eta ~ truncated(Normal(); lower=0.0, upper=0.1)
123+
mat1 ~ LKJCholesky(4, eta)
124+
mat2 ~ InverseWishart(3.2, cholesky([1.0 0.5; 0.5 1.0]))
125+
return (; eta=eta, mat1=mat1, mat2=mat2)
126+
end
127+
128+
"""
129+
A simple Linear Discriminant Analysis model.
130+
"""
131+
@model function lda(K, d, w)
132+
V = length(unique(w))
133+
D = length(unique(d))
134+
N = length(d)
135+
@assert length(w) == N
136+
137+
ϕ = Vector{Vector{Real}}(undef, K)
138+
for i in 1:K
139+
ϕ[i] ~ Dirichlet(ones(V) / V)
140+
end
141+
142+
θ = Vector{Vector{Real}}(undef, D)
143+
for i in 1:D
144+
θ[i] ~ Dirichlet(ones(K) / K)
145+
end
146+
147+
z = zeros(Int, N)
148+
149+
for i in 1:N
150+
z[i] ~ Categorical(θ[d[i]])
151+
w[i] ~ Categorical(ϕ[d[i]])
152+
end
153+
return (; ϕ=ϕ, θ=θ, z=z)
154+
end
155+
156+
end

0 commit comments

Comments
 (0)