Skip to content

Commit 1c31a77

Browse files
authored
Merge pull request #2466 from stan-dev/docs/distributions
Docs for adding new distributions to Stan Math
2 parents b994475 + 2ab00b3 commit 1c31a77

7 files changed

Lines changed: 629 additions & 47 deletions

File tree

doxygen/contributor_help_pages/adding_new_distributions.md

Lines changed: 469 additions & 0 deletions
Large diffs are not rendered by default.

doxygen/contributor_help_pages/common_pitfalls.md

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,7 @@ class my_big_type {
203203
204204
We can see in the above that the standard style of a move (the constructor taking an rvalue reference) is to copy the pointer and then null out the original pointer. But in Stan, particularly for reverse mode, we need to keep memory around even if it's only a temporary for when we call the gradient calculations in the reverse pass. And since memory for reverse mode is stored in our arena allocator no copying happens in the first place.
205205
206-
When working with arithmetic types, keep in mind that moving Scalars is often less optimal than simply taking their copy. For instance, Stan's `var` type is a PIMPL implimentation, so it simply holds a pointer of size 8 bytes. A `double` is also 8 bytes which just so happens to fit exactly in a [word](https://en.wikipedia.org/wiki/Word_(computer_architecture)) of most modern CPUs. While a reference to a double is also 8 bytes, unless the function is inlined by the compiler, the computer will have to place the reference into cache, then go fetch the value that is being referenced which now takes up two words instead of one!
206+
When working with arithmetic types, keep in mind that moving Scalars is often less optimal than simply taking their copy. For instance, Stan's `var` type is a PIMPL implementation, so it simply holds a pointer of size 8 bytes. A `double` is also 8 bytes which just so happens to fit exactly in a [word](https://en.wikipedia.org/wiki/Word_(computer_architecture)) of most modern CPUs. While a reference to a double is also 8 bytes, unless the function is inlined by the compiler, the computer will have to place the reference into cache, then go fetch the value that is being referenced which now takes up two words instead of one!
207207
208208
The general rules to follow for passing values to a function are:
209209
@@ -227,31 +227,93 @@ The pointer is cheap to copy around and is safe to copy into lambdas for
227227
228228
As an example, see the implementation of `mdivide_left`
229229
[here](https://github.com/stan-dev/math/blob/develop/stan/math/rev/fun/mdivide_left.hpp)
230-
where `make_callback_ptr` is used to save the result of an Eigen
230+
where `make_callback_ptr()` is used to save the result of an Eigen
231231
Householder QR decomposition for use in the reverse pass.
232232
233233
The implementation is in
234234
[stan/math/rev/core/chainable_object.hpp](https://github.com/stan-dev/math/blob/develop/stan/math/rev/core/chainable_object.hpp)
235235
236236
### Returning arena types
237237
238-
### `.val()` vs. `.val_op()`
238+
Suppose we have a function such as
239239
240-
### Returning expressions
240+
```cpp
241+
/**
242+
* Returns the dot product of a vector of var with itself.
243+
*
244+
* @tparam EigVec An Eigen type with compile time rows or columns equal to 1 and a scalar var type.
245+
* @param[in] v Vector.
246+
* @return Dot product of the vector with itself.
247+
*/
248+
template <typename EigVec, require_eigen_vt<is_var, EigVec>* = nullptr>
249+
inline var cool_fun(const EigVec& v) {
250+
arena_t<EigVec> arena_v(v);
251+
arena_t<EigVec> res = arena_v.val().array() * arena_v.val().array();
252+
reverse_pass_callback([res, arena_v]() mutable {
253+
arena_v.adj().array() += (2.0 * res.adj().array()) * arena_v.val().array();
254+
});
255+
return res;
256+
}
257+
```
258+
259+
There's a deceptive problem in this return! We are returning back an `arena_matrix<EigVec>`, which is an Eigen matrix completely existing on our arena allocator. The problem here is that we've also passed `res` to our callback, and now suppose a user does something like the following.
260+
261+
```cpp
262+
Eigen::Matrix<var, -1, 1> x = Eigen::Matrix<double, -1, 1>::Random(5);
263+
auto innocent_return = cool_fun(x);
264+
innocent_return.coeffRef(3) = var(3.0);
265+
auto other_return = cool_fun2(innocent_return);
266+
grad();
267+
```
268+
269+
Now `res` is `innocent_return` and we've changed one of the elements of `innocent_return`, but that is also going to change the element of `res` which is being used in our reverse pass callback! The answer for this is simple but sadly requires a copy.
270+
271+
```cpp
272+
template <typename EigVec, require_eigen_vt<is_var, EigVec>* = nullptr>
273+
inline var cool_fun(const EigVec& v) {
274+
arena_t<EigVec> arena_v(v);
275+
arena_t<EigVec> res = arena_v.val().array() * arena_v.val().array();
276+
reverse_pass_callback([res, arena_v]() mutable {
277+
arena_v.adj().array() += (2.0 * res.adj().array()) * arena_v.val().array();
278+
});
279+
return plain_type_t<EigVec>(res);
280+
}
281+
```
282+
283+
we make a deep copy of the return whose inner `vari` will not be the same but the `var` will produce a new copy of the pointer to the `vari`. Now the user code above will be protected and it is safe for them to assign to individual elements of the `auto` returned matrix.
241284
242285
### Const correctness, reverse mode autodiff, and arena types
243286
287+
In general, it's good to have arithmetic and integral types as `const`, for instance pulling out the size of a vector to reuse later such as `const size_t x_size = x.size();`. However vars, and anything that can contain a var should not be `const`. This is because in reverse mode we want to update the value of the `adj_` inside of the var, which requires that it is non-const.
288+
289+
244290
## Handy tricks
245291
246292
### `forward_as`
247293
248-
### Nested Stacks
294+
In functions such as [Stan's distributions](https://github.com/stan-dev/math/blob/1bf96579de5ca3d06eafbc2eccffb228565b4607/stan/math/prim/prob/exponential_cdf.hpp#L64) you will see code which uses a little function called `forward_as<>` inside of if statements whose values are known at compile time. In the following code, `one_m_exp` can be either an Eigen vector type or a scalar.
295+
296+
```cpp
297+
T_partials_return cdf(1.0);
298+
if (is_vector<T_y>::value || is_vector<T_inv_scale>::value) {
299+
cdf = forward_as<T_partials_array>(one_m_exp).prod();
300+
} else {
301+
cdf = forward_as<T_partials_return>(one_m_exp);
302+
}
303+
```
304+
305+
Do note that in the above, since the if statements values are known at compile time, the compiler will always remove the unused side of the `if` during the dead code elimination pass. But the dead code elimination pass does not happen until all the code is instantiated and verified as compilable. So `forward_as()` exists to trick the compiler into believing both sides of the `if` will compile. If we used C++17, the above would become
306+
307+
```cpp
308+
T_partials_return cdf(1.0);
309+
if constexpr (is_vector<T_y>::value || is_vector<T_inv_scale>::value) {
310+
cdf = one_m_exp.prod();
311+
} else {
312+
cdf = one_m_exp;
313+
}
314+
```
249315

250-
### Scoped Stacks
251316

252-
As an aside, in order to get the actual vector result, Eigen defines the
253-
`.eval()` operator, so `c.eval()` would return an actual `Eigen::VectorXd`.
254-
Similarly, Math defines the `eval` function which will evaluate an Eigen
255-
expression and forward anything that is not an Eigen expression.
317+
Where [`if constexpr`](https://en.cppreference.com/w/cpp/language/if) is run before any tests are done to verify the code can be compiled.
256318

257-
### `value_of_rec`
319+
Using `forward_as<TheTypeIWant>(the_obj)` will, when `the_obj` matches the type the user passes, simply pass back a reference to `the_obj`. But when `TheTypeIWant` and `the_obj` have different types it will throw a runtime error. This function should only be used inside of `if` statements like the above where the conditionals of the `if` are known at compile time.

doxygen/contributor_help_pages/developer_doc.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
Thanks for checking out these docs. This is where you'll find information on contributing to the [Math library](https://github.com/stan-dev/math), how the source code is laid out, and other technical details that could help. This wiki focuses on things relevant to the Math library. There's also a separate [Stan wiki](https://github.com/stan-dev/stan/wiki) for things related to the language, services, algorithms.
44

5+
For the most up to date guide on contributing to Stan Math see the [Getting Started Guide](@ref getting_started), [Common Pitfalls](@ref common_pitfalls), and [Adding New Distributions Guide](@ref new_distribution).
6+
57
This wiki is a work in progress. If you have suggestions, please update the wiki or mention it on our forums on [this thread](https://discourse.mc-stan.org/t/what-doc-would-help-developers-of-the-math-library/).
68

79
-----------------

doxygen/contributor_help_pages/distribution_tests.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ From within the Math library:
1717
1818
```
1919

20+
For development on windows, add `GENERATE_DISTRIBUTION_TESTS=true` to the make/local file.
21+
2022
This will take hours, perhaps many hours. Most of the time is spent compiling. You might want to add the parallel flag to the python script.
2123

2224

0 commit comments

Comments
 (0)