August 2018 (StanCon Helsinki)
stan-dev/stan
: Developer process overviewstan-dev/stan
: Contributing new functions to Stan stan-dev/math
: Adding a new function with known gradientsstan-dev/math
: Developer doc
We were asked at a conference,
“What are the best and worst decisions you made regarding the implementation of Stan?”- We answered
- Worst decision: using C++
- Best decision: using C++
log_prob
method on the model concept is key
stanc
)<stan>/src/stan/lang
<stan>/src/stan/lang/compiler.hpp
lang/grammars
): translate Stan to ASTlang/ast
): C++ representation of codelang/generator
): convert AST to C++ code#include
)
stan/src
, such as transformsstan-dev
GitHub organizationcmdstan
includes stan
(path stan
)stan
includes math
(path lib/stan_math
)stan
stan/lib/stan_math
stan/lib/stan_math/lib
lib
), including
Matrix program(Matrix data) {...}
stan
and test
math
scal
, arr
, mat
(this is going away)err
, fun
, meta
, prob
, …
stan/lib/stan_math/stan/math/rev/scal/fun/log1p.hpp
_test.cpp
TEST(SuiteName, TestName)
and EXPECT_EQ(actual, expected)
src
stucturerunTests.py
helper script on a directory or testJenkinsfile
stan-dev
organizationgit clone --recursive <cmdstan repo>
$ git remote -v origin git@github.com:stan-dev/math.git (fetch) origin git@github.com:stan-dev/math.git (push) $ git remote add sean git@github.com:seantalts/math.git $ git remote -v origin git@github.com:stan-dev/math.git (fetch) origin git@github.com:stan-dev/math.git (push) sean git@github.com:seantalts/math.git (fetch) sean git@github.com:seantalts/math.git (push)
Fairly standard open source flow:
clang-format
git push origin <branchname>
stan-dev
repo$ git checkout -b descriptive-branch-name Switched to a new branch 'descriptive-branch-name'
$ cd <math clone> $ find . -name operands_and_partials.hpp ./stan/math/fwd/mat/meta/operands_and_partials.hpp ./stan/math/fwd/scal/meta/operands_and_partials.hpp ./stan/math/prim/mat/meta/operands_and_partials.hpp ./stan/math/prim/scal/meta/operands_and_partials.hpp ./stan/math/rev/mat/meta/operands_and_partials.hpp ./stan/math/rev/scal/meta/operands_and_partials.hpp
$ cd ~/scm/cmdstan/stan/lib/stan_math $ find . -name operands_and_partials_test* ./test/unit/math/fwd/mat/meta/operands_and_partials_test.cpp ./test/unit/math/fwd/scal/meta/operands_and_partials_test.cpp ./test/unit/math/mix/mat/meta/operands_and_partials_test.cpp ./test/unit/math/prim/scal/meta/operands_and_partials_test.cpp ./test/unit/math/rev/mat/meta/operands_and_partials_test.cpp
test/unit/math/rev/mat/meta/operands_and_partials_test.cpp
TEST(AgradPartialsVari, OperandsAndPartialsVec) { ... vector_d d_vec(4); operands_and_partials<vector_d> o3(d_vec); EXPECT_EQ(6, sizeof(o3)); ... EXPECT_FLOAT_EQ(10.0, v.val()); EXPECT_FLOAT_EQ(10.0, grad[0]); }
python runTests.py -j<num_cores> path/to/test.cpp /path/to/test/dir
make cpplint
- runs a python script that checks styleclang-format
to automatically format codegit pull
to incorporate those formatting fixesclang-format
is installed, you can install a git hook to run it on any files you commit:
$ bash hooks/install_hook.sh
$ git add test/unit/math/rev/mat/meta/operands_and_partials_test.cpp $ git commit -m "Informative message..." $ git push sean descriptive-branch-name
develop
branch
develop
is maintained in a releasable state at all timesdevelop
branch and tag it with version number
master
branch
master
is always stable_lpdf
suffixfunctions { real my_normal_lpdf(real y, real mu, real sigma) { return -0.5 * log(2 * pi()) // params: { } - 2 * log(sigma) // params: { sigma } - 0.5 * ((y - mu) / sigma)^2; // params: { y, mu, sigma } } } parameters { real y; } model { y ~ my_normal(1.5, 3.2); }
Input validation!
Implement in Stan with the reject
function
if (sigma < 0 || is_nan(sigma) || is_inf(sigma)) reject("sigma must be finite, positive, found sigma = ", sigma); if (is_nan(y) || is_inf(y)) reject("y must be finite, found y = ", y); if (is_nan(mu) || is_inf(mu)) reject("mu must be finite, found mu = ", mu);
~/temp2/my-normal.stan
; stanc
is transpiler-O0
for compile speed$ cd ~/cmdstan ~/cmdstan(develop)$ git pull ... ~/cmdstan(develop)$ make stan-update ... ~/cmdstan(develop)$ make -j5 O=0 ~/temp2/my-normal ...transpiler built on first call with 5 processes... --- Translating Stan model to C++ code --- bin/stanc /Users/carp/temp2/my-normal.stan --o=/Users/carp/temp2/my-normal.hpp Model name=my_normal_model Input file=/Users/carp/temp2/my-normal.stan Output file=/Users/carp/temp2/my-normal.hpp --- Linking C++ model --- c++ -Wall -I . -isystem stan/lib/stan_math/lib/eigen_3.3.3 ... -O0 ...
output.csv
by default~/cmdstan(develop)$ cd ~/temp2 ~/temp2$ ~/temp2$ ./my-normal sample num_samples=10000 method = sample (Default) sample num_samples = 10000 num_warmup = 1000 (Default) ... Iteration: 1 / 11000 [ 0%] (Warmup) ... Iteration: 1001 / 11000 [ 9%] (Sampling) ... Iteration: 11000 / 11000 [100%] (Sampling)
stansummary
command in CmdStan (elided some rows)~/temp2$ ~/cmdstan/bin/stansummary output.csv Inference for Stan model: my_normal_model 1 chains: each with iter=(10000); warmup=(0); thin=(1); 10000 iterations saved. Warmup took (0.064) seconds, 0.064 seconds total Sampling took (1.0) seconds, 1.0 seconds total Mean MCSE StdDev 5% 50% 95% N_Eff N_Eff/s R_hat y 1.5 4.8e-02 3.2e+00 -3.8 1.6 6.8 4.5e+03 4.3e+03 1.0e+00 lp__ -3.8 1.2e-02 7.0e-01 -5.2 -3.5 -3.2 3.5e+03 3.4e+03 1.0e+00 accept_stat__ 0.94 8.2e-04 8.4e-02 0.76 0.97 1.0 1.1e+04 1.0e+04 1.0e+00 stepsize__ 0.83 3.8e-14 2.7e-14 0.83 0.83 0.83 5.0e-01 4.8e-01 1.0e+00 n_leapfrog__ 2.9 1.8e-02 1.7e+00 1.0 3.0 7.0 9.4e+03 9.1e+03 1.0e+00 divergent__ 0.00 nan 0.0e+00 0.00 0.00 0.00 nan nan nan
vari
pstream
argument supports print()
statements in Stan codetemplate <bool propto, typename T0__, typename T1__, typename T2__> typename boost::math::tools::promote_args<T0__, T1__, T2__>::type my_normal_lpdf(const T0__& y, const T1__& mu, const T2__& sigma, std::ostream* pstream__) { typedef typename boost::math::tools::promote_args<T0__, T1__, T2__>::type local_scalar_t__; typedef local_scalar_t__ fun_return_scalar_t__; const static bool propto__ = true; (void) propto__; local_scalar_t__ DUMMY_VAR__(std::numeric_limits<double>::quiet_NaN()); (void) DUMMY_VAR__; // suppress unused var warning ...
throw
/catch
to enable line number reporting with rethrow_located
... int current_statement_begin__ = -1; try { current_statement_begin__ = 3; return stan::math::promote_scalar<fun_return_scalar_t__>( (((-(0.5) * stan::math::log((2 * stan::math::pi()))) - (2 * stan::math::log(sigma))) - (0.5 * pow(((y - mu) / sigma),2)))); } catch (const std::exception& e) { stan::lang::rethrow_located(e, current_statement_begin__, prog_reader__()); // Next line prevents compiler griping about no return throw std::runtime_error("*** IF YOU SEE THIS, PLEASE REPORT A BUG ***"); } }
y
and assume mu
and sigma
are double
int
through promotion to double
or instantiation of T
y
can be parameter (autodiff type) or data (only double
)template <typename T> inline T my_normal(const T& y, double mu, double sigma);
std::log(double)
found through explicit using
statementstan::math::log(stan::math::var)
through argument dependent lookup (ADL)
stan::math
looks for function in stan::math
template <typename T> inline T my_normal(const T& y, double mu, double sigma) { using std::log; return -0.5 * log(2 * pi()) - 2 * log(sigma) - 0.5 * ((y - mu) / sigma)^2; }
main()
after to run it#include <stan/math/rev/mat.hpp> // Stan math with gradients #include <iostream> // C++ I/O ... my_normal implementation here ... int main() { stan::math::var y = 1.2; // independent var double mu = 0.3; // constants double sigma = 0.5; stan::math::var lp // dependent var = my_normal(y, mu, sigma); lp.grad(); // propagate derivatives std::cout << "val = " << lp.val() << "; d.val/d.y = " << y.adj() << std::endl; }
my-normal.cpp
clang++
with C++1y standard (pre C++14 for R compatibility)~/github/.../stan-cpp-2018(master)$ clang++ -std=c++1y -O0 \ -isystem ~/cmdstan/stan/lib/stan_math \ -isystem ~/cmdstan/stan/lib/stan_math/lib/eigen_3.3.3 \ -isystem ~/cmdstan/stan/lib/stan_math/lib/boost_1.66.0 \ -isystem ~/cmdstan/stan/lib/stan_math/lib/sundials_3.1.0/include \ my-normal.cpp
~/github/.../stan-cpp-2018(master)$ ./a.out val = -1.15264; d.val/d.y = -3.6
promote_args
computes max, where int
< double
< var
-
ensures maximum efficiencytemplate <typename T_y, typename T_mu, typename T_sigma> inline typename boost::math::tools::promote_args<T_y, T_mu, T_sigma>::type my_normal(const T_y& y, const T_mu& mu, const T_sigma& sigma) { using std::log; // type of expression return -0.5 * log(2 * pi()) // double - 2 * log(sigma) // T_sigma - 0.5 * ((y - mu) / sigma)^2; // promote_args<T_y, T_mu, T_sigma>::type }
stan::math::var
for each subexpression node \(v_n\)
adjoint[operand] += adjoint[result] * partial_result_wrt_operand;
var
points to vari
(pointer to implementation, aka PIMPL)struct var { vari* vi_; // pointer to impl var(double val) : vi_(new vari(val)) { } };
vari*
var
is the same size and content as pointer
vari
holds a value, adjoint, and implements chain()
for derivativesoperator new
overload (<stan-math>/stan/math/{memory, rev}
)std::vector
members)
stan/math/memory
for containersstruct vari { double val_; // values double adj_; // adjoints vari(double val, double adj = 0) : val_(val), adj_(adj) { } virtual void chain() { } // propagate derivs };
struct multiply_vari : public vari { vari* op1_, op2_; multiply_vari(vari* op1, vari* op2) : vari(op1.val_ * op2.val_), op1_(op1), op2_(op2) { }; void chain() { // operand adjoint += result adjoint * partial w.r.t. operand op1_->adj_ += adj_ * op2_->val_; op2_->adj_ += adj_ * op1_->val_; } };
new
invokes arena-based operator new
by inheritanceinline
to allow across translation units
inline var multiply(const var& a, const var& b) { return var(new multiply_vari*(a.vi_, b.vi_)); }
(var, var)
args
(var, double)
and (double, var)
implementationsvari
(symmetry allows a single one)<stan-math>/stan/math/rev/core/operator_multiplication.hpp
class multiply_vv_vari : public op_vv_vari { // superclass public: // privacy multiply_vv_vari(vari* avi, vari* bvi) : op_vv_vari(avi->val_ * bvi->val_, avi, bvi) {} // store operands void chain() { if (unlikely(is_nan(avi_->val_) || is_nan(bvi_->val_))) { // assembler hint avi_->adj_ = std::numeric_limits<double>::quiet_NaN(); // propagate NaN bvi_->adj_ = std::numeric_limits<double>::quiet_NaN(); } else { avi_->adj_ += bvi_->val_ * adj_; // same as example bvi_->adj_ += avi_->val_ * adj_; } } };
multiply
struct my_normal_vari : public vari { vari* y_; double mu_, sigma_; my_normal_vari(vari* y, double mu, double sigma) : vari(my_normal(y.val_, mu, sigma)), // double only y_(y), mu_(mu), sigma_(sigma) { } void chain() { y_.adj_ += adj_ * (mu_ - y_.val_) / (sigma_ * sigma_); } }; var my_normal(const var& y, double mu, double sigma) { return var(new my_normal_vari(y.vi_, mu, sigma); }
inline var precomputed_gradients(double value, const std::vector<var>& operands, const std::vector<double>& gradients);
#include <stan/math/rev/core/precomputed_gradients.hpp> var my_normal(const var& y, double mu, double sigma) { double val = my_normal(y.val_, mu, sigma); // assume pure double impl double dval_dy = (mu - y.val_) / (sigma * sigma); return stan::math::precomputed_gradients(val, { y }, { dval_dy }); }
stan/math/prim/scal/prob/normal_lpdf.hpp
template <bool propto, typename T_y, typename T_loc, typename T_scale> typename return_type<T_y, T_loc, T_scale>::type normal_lpdf(const T_y& y, const T_loc& mu, const T_scale& sigma) { ...
stan::return_type
is a type traits metaprogram
T_y
, T_loc
, and T_scale
boost::promote_args
/** * Return the log of the normal density for the specified variate(s) * given the specified location(s) and scales(s). The arguments * may be scalars or one-dimensional containers; all container arguments * must be the same size and any scalars will be broadcast to that size. ... * * <p>The result log density is the sum of the log densities for * each triple of arguments.
* @tparam T_y scalar type of variate * @tparam T_loc scalar type of location * @tparam T_scale scalar type of scale
* @param y variate(s) * @param mu location(s) * @param sigma scale(s)
void
)* @return sum of the log densities of the arguments
* @throw std::domain_error if any of the arguments is not-a-number * @throw std::domain_error if the location argument is not finite * @throw std::domain_error if the scale argument is not positive and finite * @throw std::domain_error if container sizes do not match */
using
statements only allowed within function scopestatic const char* function = "normal_lpdf"; typedef typename stan::partials_return_type<T_y, T_loc, T_scale>::type T_partials_return; using stan::is_constant_struct; using std::log;
check_not_nan(function, "Random variable", y); check_finite(function, "Location parameter", mu); check_positive(function, "Scale parameter", sigma); check_consistent_sizes(function, "Random variable", y, "Location parameter", mu, "Scale parameter", sigma);
if (size_zero(y, mu, sigma)) return 0.0;
double
or int
scalar type)propto
template parameter is true
if (!include_summand<propto, T_y, T_loc, T_scale>::value) return 0.0;
vari
y
, mu
, and sigma
operands_and_partials<T_y, T_loc, T_scale> ops_partials(y, mu, sigma);
operator[](int n)
n
of containern
)scalar_seq_view<T_y> y_vec(y); scalar_seq_view<T_loc> mu_vec(mu); scalar_seq_view<T_scale> sigma_vec(sigma);
size_t N = max_size(y, mu, sigma);
include_summand
is a nested traits metaprogram calllength
is overloaded for all arguments typesT_partials_return
is type of derivatives
double
and all autodiff types (forward, reverse, mixed)VectorBuilder<true, T_partials_return, T_scale> inv_sigma(length(sigma)); VectorBuilder<include_summand<propto, T_scale>::value, T_partials_return, T_scale> log_sigma(length(sigma));
sigma
[i]
sets scalar or i
-th element of containerlog(sigma)
only computed once if sigma
is scalarvalue_of
returns the double
-based value of any Stan typelog
and operator/
found as before for autodiff typesfor (size_t i = 0; i < length(sigma); i++) { inv_sigma[i] = 1.0 / value_of(sigma_vec[i]); if (include_summand<propto, T_scale>::value) log_sigma[i] = log(value_of(sigma_vec[i])); }
T_partials_return
types for values and derivativesfor (size_t n = 0; n < N; n++) { const T_partials_return y_dbl = value_of(y_vec[n]); const T_partials_return mu_dbl = value_of(mu_vec[n]); // only do each computation once, then reuse const T_partials_return y_minus_mu_over_sigma = (y_dbl - mu_dbl) * inv_sigma[n]; const T_partials_return y_minus_mu_over_sigma_squared = y_minus_mu_over_sigma * y_minus_mu_over_sigma;
static const double NEGATIVE_HALF = -0.5;
propto
is true
or relevant arguments constant (not autodiff)if (include_summand<propto>::value) logp += NEG_LOG_SQRT_TWO_PI; if (include_summand<propto, T_scale>::value) logp -= log_sigma[n]; if (include_summand<propto, T_y, T_loc, T_scale>::value) logp += NEGATIVE_HALF * y_minus_mu_over_sigma_squared;
T_partials_return scaled_diff = inv_sigma[n] * y_minus_mu_over_sigma; if (!is_constant_struct<T_y>::value) ops_partials.edge1_.partials_[n] -= scaled_diff; if (!is_constant_struct<T_loc>::value) ops_partials.edge2_.partials_[n] += scaled_diff; if (!is_constant_struct<T_scale>::value) ops_partials.edge3_.partials_[n] += -inv_sigma[n] + inv_sigma[n] * y_minus_mu_over_sigma_squared;
build()
method returns the appropriate type
return ops_partials.build(logp); } // end normal_lpdf() !!!
namespace stan { namespace math { ...everything but includes... } }
stan/math/prim/scal.hpp
#include <stan/math/prim/scal/prob/normal_lpdf.hpp>
<stan>/src/stan/lang/function_signatures.h
<stan>/src/stan/lang/ast
vector_types
is a collection of scalars and 1D containers
std::vector<expr_type> vector_types = { expr_type(double_type()), // real expr_type(double_type(), 1U), // real[] expr_type(vector_type()), // vector expr_type(row_vector_type()) }; // row_vector
add()
takes
stan::math
)for (size_t i = 0; i < vector_types.size(); ++i) for (size_t j = 0; j < vector_types.size(); ++j) for (size_t k = 0; k < vector_types.size(); ++k) add("normal_lpdf", expr_type(double_type()), vector_types[i], vector_types[j], vector_types[k]);
normal_lpdf
(with int
to double
promotion)atan2(x, y) =def= atan(x / y)
but it’s more stablemultiply
multiply
<stan-math>/test/unit/math/rev/scal/fun/atan2_test.cpp
_test.cpp
to be picked up by test harness#include <stan/math/rev/scal.hpp> #include <gtest/gtest.h> #include <boost/math/special_functions/fpclassify.hpp> #include <test/unit/math/rev/scal/fun/nan_util.hpp> #include <test/unit/math/rev/scal/util.hpp>
TEST(AgradRev, atan2_var_var) { AVAR a = 1.2; AVAR b = 3.9; AVAR f = atan2(a, b); EXPECT_FLOAT_EQ(atan2(1.2, 3.9), f.val()); AVEC x = createAVEC(a, b); VEC g; f.grad(x, g); EXPECT_FLOAT_EQ(3.9 / (1.2 * 1.2 + 3.9 * 3.9), g[0]); EXPECT_FLOAT_EQ(-1.2 / (1.2 * 1.2 + 3.9 * 3.9), g[1]); }
TEST(AgradRev, atan2_double_var) { ... TEST(AgradRev, atan2_var_double) { ...
struct atan2_fun { template <typename T0, typename T1> inline typename stan::return_type<T0, T1>::type operator()( const T0& arg1, const T1& arg2) const { return atan2(arg1, arg2); } }; TEST(AgradRev, atan2_nan) { atan2_fun atan2_; test_nan(atan2_, 3.0, 5.0, false, true); }
TEST(AgradRev, check_varis_on_stack) { AVAR a = 1.2; AVAR b = 3.9; test::check_varis_on_stack(stan::math::atan2(a, b)); test::check_varis_on_stack(stan::math::atan2(a, 3.9)); test::check_varis_on_stack(stan::math::atan2(1.2, b)); }
runTests.py
with multiple cores (calls make)<stan-math>$ ./runTests.py -j5 test/unit/math/rev/scal/fun/atan2_test.cpp make ... Running main() from gtest_main.cc [==========] Running 7 tests from 1 test case. [----------] Global test environment set-up. [----------] 7 tests from AgradRev [ RUN ] AgradRev.atan2_var_var [ OK ] AgradRev.atan2_var_var (0 ms) ... [ RUN ] AgradRev.check_varis_on_stack [ OK ] AgradRev.check_varis_on_stack (0 ms) ... [ PASSED ] 7 tests.
<math>/test/prob/normal/normal_test.hpp
<stan>$ make cpplint <math>$ make cpplint
<math>$ make doxygen
<stan>$ make test-headers <math>$ make test-headers
<stan>/src/test/test-models/good
int
and real
)data { int d_int; real d_real; } transformed data { real td1 = atan2(d_int, d_int); real td2 = atan2(d_int, d_real); real td3 = atan2(d_real, d_int); real td4 = atan2(d_real, d_real); }
parameters { real p_real; } transformed parameters { real tp1 = atan2(d_int, p_real); real tp2 = atan2(d_real, p_real); real tp3 = atan2(p_real, d_int); real tp4 = atan2(p_real, d_real); real tp5 = atan2(p_real, p_real); }
<stan>$ make test/integration/compile_models ...