This page collects what is known about how long crywolf takes, what
makes it faster, and what was tried without success. All times were
measured on one 18-core laptop (Apple M5 Pro) and will differ on other
computers; the ratios are the useful part. The scripts and raw results
are in dev/profile/ of the source repository, and each
change is described in NEWS.md.
A large dataset over the versions
The curiango data have 97,806 one-minute automated
occasions in 1,753 site-nights. The model used for timing has three
occupancy covariates, two detection covariates, a constant
false-positive rate, and a recorder random effect, fitted with four
chains of 1000 warm-up and 1000 sampling iterations and the default
sampler settings.
| Version | Change | Wall time |
|---|---|---|
| 1.0.0 | one thread per chain | 70 min |
| 1.1.0 | likelihood summed over sites in parallel
(reduce_sum()), four threads per chain |
16.5 min |
| 1.3.0 | per-site totals for unoccupied sites, one GLM call per site for occupied ones | |
| 1.5.0 | (same likelihood as 1.3.0), four threads per chain | 6.6 min |
The 1.3.0 change was measured per gradient evaluation rather than per fit: on these data a gradient took 8.5 ms in 1.2.0 and 2.6 ms in 1.3.0 with one thread. In every version the posterior is the same; only the arithmetic is arranged differently, and the log density and its gradient agree up to floating-point rounding.
What you can do
Use more threads per chain. The likelihood is a sum
over sites, which Stan can split among cores. With
threads_per_chain = "auto", crywolf divides the physical
cores among the chains that run in parallel. On the
curiango data, 150 iterations of one chain took:
| Threads | 1 | 2 | 4 | 8 |
|---|---|---|---|---|
| Time | 55.1 s | 28.8 s | 16.1 s | 10.3 s |
The default is one thread, because with more than one the partial sums are added in an order that changes from run to run. A fit with several threads is therefore not reproduced exactly by its seed, even on the same computer, although its posterior is the same. Use threads for exploratory fits and large datasets, and one thread when exact reproduction matters. A fit whose chains took more than a minute on one core prints a reminder.
Run chains in parallel. parallel_chains
defaults to the number of chains, so the four chains of a fit run at the
same time when there are four free cores.
In calibrations, use fewer chains per fit.
fp_calibrate() and its relatives run many small fits in
parallel workers, with the chains of each fit one after another. There
the cost is proportional to the number of chains: two chains take half
the time of four, and still gave an effective sample size of about 2000
per parameter in the default design. Keep one thread per chain in
parallel calibrations, since the workers already use the cores.
Simplify the detection model when you can. Occasions of a site with the same detection covariates are collapsed into one binomial row, so a model without occasion-level detection covariates evaluates far fewer terms. With covariates on every occasion, the per-site GLM call of 1.3.0 is what keeps the cost down.
Small fits
For simulated datasets of the sizes used in calibrations, the fixed cost of starting CmdStan and reading its output was small next to sampling (medians of three fits, chains in parallel):
| Design | Chains | Wall time | Sampler time | Fixed cost |
|---|---|---|---|---|
| 100 sites, 6 occasions | 2 | 6.6 s | 6.2 s | 0.4 s |
| 300 sites, 20 occasions | 2 | 21.2 s | 20.8 s | 0.5 s |
| 1000 sites, 30 occasions | 2 | 66.1 s | 65.4 s | 0.7 s |
| 100 sites, 6 occasions | 4 | 7.7 s | 7.1 s | 0.6 s |
| 300 sites, 20 occasions | 4 | 24.4 s | 23.4 s | 1.0 s |
| 1000 sites, 30 occasions | 4 | 71.3 s | 70.2 s | 1.1 s |
Tried without success
On the curiango data each sampling iteration takes about
100 leapfrog steps, and these were the attempts to reduce that number or
the cost of each step. None gave a consistent gain, and run-to-run
differences of up to 30% in effective sample size per second made
smaller effects impossible to detect.
-
A dense mass matrix
(
metric = "dense_e", which can be passed tofp_occu()through...) cut the wall time from 393 s to 161 s but produced divergent transitions and halved the effective sample size of the recorder standard deviation. It is not a safe default with a hierarchical recorder effect. - Sampling the false-positive intercept with the gap. With a recorder effect, the mean detection intercept is the uncertain quantity and its posterior correlation with the gap reached 0.99. Sampling the false-positive intercept instead was meant to avoid that correlation, but the number of leapfrog steps did not fall.
-
A centred recorder effect, which suits recorders
with many occasions each, alone or with the previous change: no
combination was best in both the
curiangodata and simulations with 5 to 30 recorders. -
reduce_sum_static(), in the hope that a fixed partition of the sites would make draws with several threads reproducible. It did not. - Initialization by Pathfinder was not pursued, because warm-up is only about 40% of the time, and a QR decomposition of the design matrices was not, because no posterior correlation among covariate effects exceeded 0.5.
-
Longer warm-up or a higher
adapt_deltawithrandom_fp = TRUEtook longer without a clear gain: 2 and 3 of 20 fits had an R-hat above 1.01, against 3 of 20 with the defaults. - Earlier: a larger
grainsizeforreduce_sum()(no gain), and moving the computation to another language (the time is spent inside CmdStan).