This is extremely cool!
I (and claude) ran the benchmarks locally, but added the --O1 flag to the compiler and updated the benchmarks to use google benchmark. Code and table are below. Oddly, setting --O1 shrank a lot of the speed differences in some of the benchmarked models. Also would it be hard to have stanli take in the optimized mir instead of the transformed mir? That would also give stanli access to a lot of these optims.
I tried my darnd’est to get stan up to the speed of stanli. Mixed success! Just setting some compiler flags and --O1 was enough to get cmdstan up to or faster than stanli for a few models, but there are a handful of tricks going on in stanli that are very cool and near impossible for cmdstan to match without backporting them to the stan compiler.
The below graph’s ns/grad number is the average nanoseconds per gradient evaluation. Speedup is just stanli/cmdstan
| model |
unconstrained params |
stanli ns/grad |
CmdStan ns/grad |
speedup |
|
|
|
|
|
| `radon_pooled` |
3 |
103,174 |
688,497 |
6.67x |
| `arK` |
7 |
3,863 |
20,352 |
5.27x |
| `radon_ic` |
391 |
239,551 |
952,364 |
3.98x |
| `radon_county_intercept` |
388 |
157,058 |
751,744 |
4.79x |
| `nes` |
10 |
28,123 |
89,649 |
3.19x |
| `eight_schools_nc` |
10 |
386 |
446 |
1.15x |
| `election88_full` |
90 |
756,709 |
1,428,294 |
1.89x |
| `bym2_offset_only` |
3845 |
54,836 |
94,929 |
1.73x |
| `dogs` |
3 |
52,029 |
79,771 |
1.53x |
| `kidscore_momiq` |
3 |
2,853 |
4,977 |
1.74x |
| `lsat_model` |
1006 |
117,619 |
152,073 |
1.29x |
| `state_space_slss` |
389 |
36,299 |
43,700 |
1.20x |
| `hmm_example` |
4 |
48,137 |
49,457 |
1.03x |
| `garch11` |
4 |
10,557 |
12,338 |
1.17x |
| `hmm_drive_0` |
6 |
326,306 |
272,144 |
0.83x |
| `normal_mixture` |
3 |
159,234 |
162,175 |
1.02x |
| `low_dim_gauss_mix` |
5 |
187,607 |
197,962 |
1.06x |
| `wells_dist100ars_model` |
3 |
19,675 |
18,368 |
0.93x |
| `iohmm_reg` |
29 |
1,088,012 |
736,116 |
0.68x |
| `radon_county` |
389 |
118,430 |
122,811 |
1.04x |
| `arma11` |
4 |
11,886 |
6,430 |
0.54x |
| `diamonds` |
26 |
72,496 |
28,106 |
0.39x |
| `ldaK2` |
7 |
522,558 |
270,922 |
0.52x |
radon_pooled is the best example of where stan would benefit from even a simple for loop collapser. The model spends 99% of its time in one loop
for (n in 1 : N) {
target += normal_lpdf(log_radon[n] | mu[n], sigma_y);
}
So if the compiler could rewrite this to be single line statement that would turn on a ton of vectorization
target += normal_lpdf(log_radon | mu, sigma_y);
arK is another for loop collapse that is really interesting. It is impressive that it can take that double loop scheme and work out the vectorized version
Before:
for (t in (K + 1) : T) {
real mu;
mu = alpha;
for (k in 1 : K) {
mu = mu + beta[k] * y[t - k];
}
y[t] ~ normal(mu, sigma);
}
After:
vector[T - K] mu = rep_vector(alpha, T - K);
for (k in 1:K) {
mu += beta[k]
* to_vector(y[(K + 1 - k):(T - k)]);
}
to_vector(y[(K + 1):T]) ~ normal(mu, sigma);
I think there is a way to reduce the second loop as well, but that requires some data logic that I think would be very wonky.
kidscore_momiq comes in as 1.74x faster than cmdstan and that one has nothing to do with a for loop collapser. Stanli ran a gradient for this model in 2,853ns while stan took 4,977ns.
The model is pretty much just the below two lines
model {
sigma ~ cauchy(0, 2.5);
kid_score ~ normal(beta[1] + beta[2] * mom_iq, sigma);
}
I need to look more into why this model is still faster, but I think this one could be misses from stanc or just the fact that stanli sets up the autodiff graph once and then fills in new values on the forward and reverse pass. Stan reuses the memory, but not the nodes themselves. Stanc could phrase the inner part of the normal as an fma, or it could make it a normal_id_glm.
I still need to look into more of the models to pick apart why they are faster, but overall this is very cool and exciting! Having a fast interpreted version of stan is awesome. Would you be open to making a pull request in the stanc compiler for the for loop collapser / any other optims you think would be good to have? The for loop collapser would be awesome in the compiler because then that would also turn on the SoA matrix types for a lot of stan models.