Using `reduce_sum` with `ordered_logistic`

Hi,

I’m trying to implement reduce_sum with a graded response model but can’t get it to compile.

I have read the Reduce-sum section in the Stan User’s Guide, related topics here on discourse, such as https://discourse.mc-stan.org/t/testing-reduce-sum/14376, and @martinmodrak has already provided some helpful comments here.

Below is the model without reduce_sum using ordered_logistic_lpmf. This compiles, runs without warning messages, and produces the expected results. Data is attached. stan_data.Rdata (6.3 KB)

I have cmdstan version 2.27.0 installed.

library(cmdstanr)
load("./stan_data.Rdata")
// GRADED RESPONSE MODEL - assign as gIRT

data {
  int<lower=1> I;                // Number of items
  int<lower=1> J;                // Number of respondents
  int<lower=1> N;                // Total responses
  int<lower=1,upper=I> ii[N];    // Item ID
  int<lower=1,upper=J> jj[N];    // Person ID
  int<lower=0> y[N];             // Responses
  int<lower=1> K;                // Number of covariates for location (mu) of ability (theta)
  matrix[J,K] X;                 // Covariate matrix (including intercept) for location (mu) of ability (theta)
}

transformed data {
  int r[N];                      // modified response; r = 1, 2, ... m_i + 1
  int m[I];                      // number of difficulty parameters per item
  int pos_alpha[I];              // first position in difficulty for item
  int pos_delta[I];              // first position in between-step change for item 

  // Allowing for differing numbers of response categories
  m = rep_array(0, I);															
  for(n in 1:N) {
    r[n] = y[n] + 1;
    if(y[n] > m[ii[n]]) m[ii[n]] = y[n];
  }
  pos_alpha[1] = 1;
  pos_delta[1] = 1;
  for(i in 2:(I)) {
    pos_alpha[i] = pos_alpha[i-1] + m[i-1];
    pos_delta[i] = pos_delta[i-1] + m[i-1] - 1;
  }
}

parameters {
  vector[I] eta;                      // First diffulty parameter for each item
  vector<lower=0>[sum(m)-I] delta;    // Between-step changes in difficulty
  real<lower = 0> beta[I];            // Constrain all discrimination parameters to be positive
  vector[J] theta;                    // Ability parameter
  vector[K] gamma;                    // Covariate coefficients
}

transformed parameters {
  vector[sum(m)] alpha;            // Composite item difficulties

  for(i in 1:I) {
    if (m[i] > 0)
      alpha[pos_alpha[i]] = eta[i];
    for (k in 2:m[i])
      alpha[pos_alpha[i]+k-1] = alpha[pos_alpha[i]+k-2] + delta[pos_delta[i]+k-2];
  }
}

model{
  eta ~ normal(0,1);
  delta ~ normal(0,1);
  beta ~ lognormal(1,1);
  gamma ~ student_t(3, 0, 1);
  target += normal_lpdf(alpha | 0, 1);
  theta ~ normal(X*gamma,1);
  mean(theta) ~ normal(0, 0.001); // Mean of ability constrained to 0 for identification

  for (n in 1:N) {
    target += ordered_logistic_lpmf(r[n] | theta[jj[n]] * beta[ii[n]],
                            segment(alpha, pos_alpha[ii[n]], m[ii[n]]));
  }
}
"
write(gIRT, "gIRT.stan") # save above model

compiled_gIRT <- cmdstan_model(stan_file = "gIRT.stan") # compile

compiled_gIRT$check_syntax()

fit_gIRT <- compiled_gIRT$sample(
  data = stan_data,
  chains = 4,
  parallel_chains = 4,
  iter_warmup = 500,
  iter_sampling = 100,
  refresh = 50
)

Following from the docs, I’d have expected this reduce_sum implementation to work (just the functions and model blocks):

functions {
  real partial_sum_lpmf(int[] slice_r,
                        int start, int end,
                        int[] ii,
                        int[] jj,
                        vector theta,
                        real beta,
                        vector alpha,
                        int[] m,
                        int[] pos_alpha) {
  return ordered_logistic_lupmf(slice_r | 
                                theta[jj[start:end]] * beta[ii[start:end]], 
                                segment(alpha, pos_alpha[ii[start:end]], 
                                                m[ii[start:end]]));
  }
}

model{
  eta ~ normal(0,1);
  delta ~ normal(0,1);
  beta ~ lognormal(1,1);
  gamma ~ student_t(3, 0, 1);
  target += normal_lpdf(alpha | 0, 1);
  theta ~ normal(X*gamma,1);
  mean(theta) ~ normal(0, 0.001); // Mean of ability constrained to 0 for identification

  target += reduce_sum(partial_sum_lupmf, r, grainsize, 
                       ii, jj, theta, beta, alpha, m, pos_alpha);
}

which throws the following error:

Semantic error in line 13, column 55 to column 74:
   -------------------------------------------------
    11:                          int[] pos_alpha) {
    12:    return ordered_logistic_lupmf(slice_r | 
    13:                                  theta[jj[start:end]] * beta[ii[start:end]], 
                                                                ^
    14:                                  segment(alpha, pos_alpha[ii[start:end]], 
    15:                                                  m[ii[start:end]]));
   -------------------------------------------------

Too many indexes, expression dimensions=0, indexes found=1

Adjusting partial_sum_lpmf along suggestions by @martinmodrak here:

functions {
  real partial_sum_lpmf(int[] slice_r,
                        int start, int end,
                        int[] ii,
                        int[] jj,
                        vector theta,
                        real[] beta,
                        vector alpha,
                        int[] m,
                        int[] pos_alpha) {
  return ordered_logistic_lupmf(slice_r | 
                                theta[jj[start:end]] * to_row_vector(beta[ii[start:end]]), 
                                segment(alpha, pos_alpha[ii[start:end]], 
                                                m[ii[start:end]]));
  }
}

model{
  eta ~ normal(0,1);
  delta ~ normal(0,1);
  beta ~ lognormal(1,1);
  gamma ~ student_t(3, 0, 1);
  target += normal_lpdf(alpha | 0, 1);
  theta ~ normal(X*gamma,1);
  mean(theta) ~ normal(0, 0.001); // Mean of ability constrained to 0 for identification

  target += reduce_sum(partial_sum_lupmf, r, grainsize, 
                       ii, jj, theta, beta, alpha, m, pos_alpha);
}

yields this error message:

Semantic error in line 14, column 32 to line 15, column 65:
   -------------------------------------------------
    12:    return ordered_logistic_lupmf(slice_r | 
    13:                                  theta[jj[start:end]] * to_row_vector(beta[ii[start:end]]), 
    14:                                  segment(alpha, pos_alpha[ii[start:end]], 
                                         ^
    15:                                                  m[ii[start:end]]));
    16:    }
   -------------------------------------------------

Ill-typed arguments supplied to function 'segment'. Available signatures: 
(vector, int, int) => vector
(row_vector, int, int) => row_vector
(array[] int, int, int) => array[] int
(array[] real, int, int) => array[] real
(array[] vector, int, int) => array[] vector
(array[] row_vector, int, int) => array[] row_vector
(array[] matrix, int, int) => array[] matrix
(array[,] int, int, int) => array[,] int
(array[,] real, int, int) => array[,] real
(array[,] vector, int, int) => array[,] vector
(array[,] row_vector, int, int) => array[,] row_vector
(array[,] matrix, int, int) => array[,] matrix
(array[,,] int, int, int) => array[,,] int
(array[,,] real, int, int) => array[,,] real
(array[,,] vector, int, int) => array[,,] vector
(array[,,] row_vector, int, int) => array[,,] row_vector
(array[,,] matrix, int, int) => array[,,] matrix
Instead supplied arguments of incompatible type: vector, array[] int, array[] int

Alternatively, I also tried replacing segment:

functions {
  real partial_sum_lpmf(int[] slice_r,
                        int start, int end,
                        int[] ii,
                        int[] jj,
                        vector theta,
                        real[] beta,
                        vector alpha,
                        int[] m,
                        int[] pos_alpha) {
  return ordered_logistic_lupmf(slice_r | 
                                theta[jj[start:end]] * to_row_vector(beta[ii[start:end]]), 
                                alpha[pos_alpha[ii[start:end]]:(pos_alpha[ii[start:end]] + m[ii[start:end]])]);
  }
}

model{
  eta ~ normal(0,1);
  delta ~ normal(0,1);
  beta ~ lognormal(1,1);
  gamma ~ student_t(3, 0, 1);
  target += normal_lpdf(alpha | 0, 1);
  theta ~ normal(X*gamma,1);
  mean(theta) ~ normal(0, 0.001); // Mean of ability constrained to 0 for identification

  target += reduce_sum(partial_sum_lupmf, r, grainsize, 
                       ii, jj, theta, beta, alpha, m, pos_alpha);
}

which produces:

Semantic error in line 14, column 38 to column 62:
   -------------------------------------------------
    12:    return ordered_logistic_lupmf(slice_r | 
    13:                                  theta[jj[start:end]] * to_row_vector(beta[ii[start:end]]), 
    14:                                  alpha[pos_alpha[ii[start:end]]:(pos_alpha[ii[start:end]] + m[ii[start:end]])]);
                                               ^
    15:    }
    16:  }
   -------------------------------------------------

Range bound must be of type int. Instead found type array[] int

Any help on this would be much appreciated! Maybe @andrjohns and @wds15 have some suggestions?

As a general advice I’d say that you should first get your model to work without the use of reduce_sum, but with using the partial_sum_lpmf function. So write the log-lik with 4 calls to the partial_sum_lpmf first and get that to work. Skimming over your code shows that you struggle with non reduce_sum issues as to how to index things properly when going in slices.

Thanks @wds15! I tried this as well. But whatever I do I continue to receive semantic (ill-typed argument) errors.

For instance, when I try compiling the full model by @andrjohns from here, both including and excluding the correction by @bbbales2, I receive a similar error message:

Compiling Stan program...
Semantic error in 'C:/Users/GOEBEL~1.ADM/AppData/Local/Temp/RtmpesbNDK/model-2fbc52fc54f9.stan', line 78, column 12 to line 79, column 49:
   -------------------------------------------------
    76:    }
    77:  
    78:    target += reduce_sum(reduce_func, y, grainsize,
                     ^
    79:                         ii,jj,gg,ystar,thresholds);
    80:  }
   -------------------------------------------------

Ill-typed arguments supplied to function 'reduce_sum'. Available arguments:
(T[], int, int, ...) => real, T[], int, ...
(T[,], int, int, ...) => real, T[,], int, ...
(T[,,], int, int, ...) => real, T[,,], int, ...
(T[,,,], int, int, ...) => real, T[,,,], int, ...
(T[,,,,], int, int, ...) => real, T[,,,,], int, ...
(T[,,,,,], int, int, ...) => real, T[,,,,,], int, ...
(T[,,,,,,], int, int, ...) => real, T[,,,,,,], int, ...
Where T is any one of int, real, vector, row_vector or matrix.
Instead supplied arguments of incompatible type: (int, int, array[] int, array[] int, array[] int, array[] int, matrix, array[,] vector) => real, array[] int, int, array[] int, array[] int, array[] int, matrix, array[,] vectormingw32-make.exe: *** [make/program:47: C:\Users\GOEBEL~1.ADM\AppData\Local\Temp\RtmpesbNDK\model-2fbc52fc54f9.hpp] Error 1
Fehler: An error occured during compilation! See the message above for more information.

I’m starting to wonder whether this is related to my system/stan installation?

1 Like

Running the minimal example at Reduce Sum: A Minimal Example works, but I receive this message during compilation:

Compiling Stan program...
In file included from stan/lib/stan_math/stan/math/rev/functor.hpp:28,
                 from stan/lib/stan_math/stan/math/rev.hpp:11,
                 from stan/lib/stan_math/stan/math.hpp:19,
                 from stan/src/stan/model/model_header.hpp:4,
                 from C:/Users/GOEBEL~1.ADM/AppData/Local/Temp/RtmpENjlXH/model-324cbf1455b.hpp:3:
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp: In instantiation of 'stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::recursive_reducer::recursive_reducer(size_t, size_t, double*, VecT&&, ArgsT&& ...) [with VecT = const std::vector<int>&; ArgsT = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>; ReturnType = stan::math::var_value<double>; Vec = const std::vector<int>&; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; size_t = long long unsigned int]':
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:249:23:   required from 'stan::math::var stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::operator()(Vec&&, bool, int, std::ostream*, Args&& ...) const [with ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>; ReturnType = stan::math::var_value<double>; Vec = const std::vector<int>&; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type = void; stan::math::var = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
stan/lib/stan_math/stan/math/prim/functor/reduce_sum.hpp:207:60:   required from 'auto stan::math::reduce_sum(Vec&&, int, std::ostream*, Args&& ...) [with ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>; Vec = const std::vector<int>&; <template-parameter-1-3> = void; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; std::ostream = std::basic_ostream<char>]'
C:/Users/GOEBEL~1.ADM/AppData/Local/Temp/RtmpENjlXH/model-324cbf1455b.hpp:248:61:   required from 'stan::scalar_type_t<T2> logistic1_model_namespace::logistic1_model::log_prob_impl(VecR&, VecI&, std::ostream*) const [with bool propto__ = false; bool jacobian__ = false; VecR = Eigen::Matrix<stan::math::var_value<double>, -1, 1>; VecI = Eigen::Matrix<int, -1, 1>; stan::require_vector_like_t<VecR>* <anonymous> = 0; stan::require_vector_like_vt<std::is_integral, VecI>* <anonymous> = 0; stan::scalar_type_t<T2> = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
C:/Users/GOEBEL~1.ADM/AppData/Local/Temp/RtmpENjlXH/model-324cbf1455b.hpp:454:77:   required from 'T_ logistic1_model_namespace::logistic1_model::log_prob(Eigen::Matrix<T_job_param, -1, 1>&, std::ostream*) const [with bool propto__ = false; bool jacobian__ = false; T_ = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
stan/src/stan/model/model_base_crtp.hpp:96:77:   required from 'stan::math::var stan::model::model_base_crtp<M>::log_prob(Eigen::Matrix<stan::math::var_value<double>, -1, 1>&, std::ostream*) const [with M = logistic1_model_namespace::logistic1_model; stan::math::var = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
stan/src/stan/model/model_base_crtp.hpp:93:20:   required from here
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:58:23: warning: 'stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::local_args_tuple_scope_' will be initialized after [-Wreorder]
     scoped_args_tuple local_args_tuple_scope_;
                       ^~~~~~~~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:57:25: warning:   'std::tuple<const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&> stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::args_tuple_' [-Wreorder]
     std::tuple<Args...> args_tuple_;
                         ^~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:63:5: warning:   when initialized here [-Wreorder]
     recursive_reducer(size_t num_vars_per_term, size_t num_vars_shared_terms,
     ^~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp: In instantiation of 'stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::recursive_reducer::recursive_reducer(size_t, size_t, double*, VecT&&, ArgsT&& ...) [with VecT = const std::vector<int>&; ArgsT = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>; ReturnType = stan::math::var_value<double>; Vec = const std::vector<int>&; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; size_t = long long unsigned int]':
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:249:23:   required from 'stan::math::var stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::operator()(Vec&&, bool, int, std::ostream*, Args&& ...) const [with ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>; ReturnType = stan::math::var_value<double>; Vec = const std::vector<int>&; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type = void; stan::math::var = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
stan/lib/stan_math/stan/math/prim/functor/reduce_sum.hpp:207:60:   required from 'auto stan::math::reduce_sum(Vec&&, int, std::ostream*, Args&& ...) [with ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>; Vec = const std::vector<int>&; <template-parameter-1-3> = void; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}; std::ostream = std::basic_ostream<char>]'
C:/Users/GOEBEL~1.ADM/AppData/Local/Temp/RtmpENjlXH/model-324cbf1455b.hpp:248:61:   required from 'stan::scalar_type_t<T2> logistic1_model_namespace::logistic1_model::log_prob_impl(VecR&, VecI&, std::ostream*) const [with bool propto__ = true; bool jacobian__ = false; VecR = Eigen::Matrix<stan::math::var_value<double>, -1, 1>; VecI = Eigen::Matrix<int, -1, 1>; stan::require_vector_like_t<VecR>* <anonymous> = 0; stan::require_vector_like_vt<std::is_integral, VecI>* <anonymous> = 0; stan::scalar_type_t<T2> = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
C:/Users/GOEBEL~1.ADM/AppData/Local/Temp/RtmpENjlXH/model-324cbf1455b.hpp:454:77:   required from 'T_ logistic1_model_namespace::logistic1_model::log_prob(Eigen::Matrix<T_job_param, -1, 1>&, std::ostream*) const [with bool propto__ = true; bool jacobian__ = false; T_ = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
stan/src/stan/model/model_base_crtp.hpp:118:76:   required from 'stan::math::var stan::model::model_base_crtp<M>::log_prob_propto(Eigen::Matrix<stan::math::var_value<double>, -1, 1>&, std::ostream*) const [with M = logistic1_model_namespace::logistic1_model; stan::math::var = stan::math::var_value<double>; std::ostream = std::basic_ostream<char>]'
stan/src/stan/model/model_base_crtp.hpp:115:20:   required from here
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:58:23: warning: 'stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::local_args_tuple_scope_' will be initialized after [-Wreorder]
     scoped_args_tuple local_args_tuple_scope_;
                       ^~~~~~~~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:57:25: warning:   'std::tuple<const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&> stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::args_tuple_' [-Wreorder]
     std::tuple<Args...> args_tuple_;
                         ^~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:63:5: warning:   when initialized here [-Wreorder]
     recursive_reducer(size_t num_vars_per_term, size_t num_vars_shared_terms,
     ^~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp: In instantiation of 'stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::recursive_reducer::recursive_reducer(stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::recursive_reducer&, tbb::split) [with ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>; ReturnType = stan::math::var_value<double>; Vec = const std::vector<int>&; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}]':
stan/lib/stan_math/lib/tbb_2020.3/include/tbb/parallel_reduce.h:186:27:   required from 'tbb::task* tbb::interface9::internal::start_reduce<Range, Body, Partitioner>::execute() [with Range = tbb::blocked_range<long long unsigned int>; Body = stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer; Partitioner = const tbb::auto_partitioner]'
stan/lib/stan_math/lib/tbb_2020.3/include/tbb/parallel_reduce.h:181:11:   required from here
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:58:23: warning: 'stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::local_args_tuple_scope_' will be initialized after [-Wreorder]
     scoped_args_tuple local_args_tuple_scope_;
                       ^~~~~~~~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:57:25: warning:   'std::tuple<const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&> stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<true>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::args_tuple_' [-Wreorder]
     std::tuple<Args...> args_tuple_;
                         ^~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:78:5: warning:   when initialized here [-Wreorder]
     recursive_reducer(recursive_reducer& other, tbb::split)
     ^~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp: In instantiation of 'stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::recursive_reducer::recursive_reducer(stan::math::internal::reduce_sum_impl<ReduceFunction, typename std::enable_if<stan::is_var<typename std::decay<_Arg>::type, void>::value, void>::type, ReturnType, Vec, Args ...>::recursive_reducer&, tbb::split) [with ReduceFunction = logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>; ReturnType = stan::math::var_value<double>; Vec = const std::vector<int>&; Args = {const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&}]':
stan/lib/stan_math/lib/tbb_2020.3/include/tbb/parallel_reduce.h:186:27:   required from 'tbb::task* tbb::interface9::internal::start_reduce<Range, Body, Partitioner>::execute() [with Range = tbb::blocked_range<long long unsigned int>; Body = stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer; Partitioner = const tbb::auto_partitioner]'
stan/lib/stan_math/lib/tbb_2020.3/include/tbb/parallel_reduce.h:181:11:   required from here
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:58:23: warning: 'stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::local_args_tuple_scope_' will be initialized after [-Wreorder]
     scoped_args_tuple local_args_tuple_scope_;
                       ^~~~~~~~~~~~~~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:57:25: warning:   'std::tuple<const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&> stan::math::internal::reduce_sum_impl<logistic1_model_namespace::partial_sum_lpmf_rsfunctor__<false>, void, stan::math::var_value<double>, const std::vector<int>&, const std::vector<int, std::allocator<int> >&, const Eigen::Map<Eigen::Matrix<double, -1, 1, 0, -1, 1>, 0, Eigen::Stride<0, 0> >&, Eigen::Matrix<stan::math::var_value<double, void>, -1, 1, 0, -1, 1>&>::recursive_reducer::args_tuple_' [-Wreorder]
     std::tuple<Args...> args_tuple_;
                         ^~~~~~~~~~~
stan/lib/stan_math/stan/math/rev/functor/reduce_sum.hpp:78:5: warning:   when initialized here [-Wreorder]
     recursive_reducer(recursive_reducer& other, tbb::split)
     ^~~~~~~~~~~~~~~~~

Maybe this is related to the issue above.

I also noticed that running this minimal example without reduce_sum via rstan takes twice as long as via cmdstanr. (also without reduce_sum). Is this to be expected?

1 Like

Rstan is at 2.21 and your cmdstan version is a lot more modern, so things are not comparable.

1 Like

It’s not your system, the examples you that fail for you also fail for me.

What’s going on here is a bit of pain that most Stan users initially face (unless they have previous programming experience with other strongly typed languages). You have to be very careful in Stan to pass arrays when functions expect arrays, vectors when functions expect vectors, reals when functions expect reals, integers when functions expect integers, etc. This is what “strongly typed” means: the “types” of variables really matter and don’t get interconverted automatically or easily.

For what it’s worth, I promise that the pain is worth it, both in the general sense of Stan being an amazing tool, and in the specific sense that eventually the strong typing will help you spot mistakes in your code in a way that is well worth the need to keep careful track of this stuff.

Good luck troubleshooting! If you run into further trouble, feel free to dump the current version of your model into this thread and we can check it out.

Here is what I don’t understand:

In the original graded response model above alpha is passed as a vector and pos_alpha[ii[n]] and m[ii[n]] are passed as arrays of integers to segment(). The model compiles and runs successfully, vector, array[] int, array[] int are not incompatible.

However, when building partial_sum_lpmf(), segment() does not accept arguments of type vector, array[] int, array[] int and instead demands vector, int, int. Why?

I’m also curious why the model in this post Testing Reduce Sum used to work, which I assume it did, but now doesn’t because arguments are deemed incompatible.

Finally, I don’t see how to pass pos_alpha[ii[n]] and m[ii[n]] as integer values, if I have to index them as arrays of integers, except for maybe rewriting everything to make it fit.

No, in the original model above pos_alpha[ii[n]] is an integer, not an array of integers. pos_alpha is an array; ii is an array, but n is an integer, ii[n] is an integer, and pos_alpha[ii[n]] is an integer.

When you switched to reduce_sum, you instead used pos_alpha[ii[start:end]], which is not an integer but rather an array of integers. You need to reinstate the for-loop (and think carefully about the indexing to get it right) inside the partial_sum_lpmf function.

Note that this is a great example of the benefits of strong typing! A different language might have tried to silently coerce pos_alpha[ii[start:end]] to a single integer, and then would have returned nonsense in a way that would have been very hard to detect or troubleshoot. Stan, on the other hand, throws an error that immediately reveals that there’s a problem with your indexing.

1 Like

Thanks @jsocolar, got it running now. The reduce_sum implementation gives a 3x speedup (on 4 cores with 2 parallel chains and 2 threads per chain).

Since I only tested this on a small subset of the data, I have a few more questions:

  • As far as I understand, I’m currently slicing the data, not the parameters, right? (r_slice are survey responses)
  • However, I have three shared parameters, theta, beta, and alpha, which are not sliced and on top of that they are being copied in every process, right?
  • theta are person-level parameters, which can go into tens of thousand with more data (this is not the case for beta and alpha. So am I right to assume that as the number of theta parameters grow, the copying will reduce the benefits of parallelization?
  • If it makes sense to slice parameters rather than data: How can theta be sliced in this case?

Profiling shows that 99% of runtime are due to likelihood calculations.

Here is the current model:

functions {
  real partial_sum_lpmf(int[] r_slice,
                        int start, int end,
                        int[] ii,
                        int[] jj,
                        vector theta,
                        real[] beta,
                        vector alpha,
                        int[] m,
                        int[] pos_alpha) {
    real sum = 0.0;
    for(i in start:end) {
      sum += ordered_logistic_lupmf(r_slice[i - start + 1] | theta[jj[i]] * beta[ii[i]], 
                                segment(alpha, pos_alpha[ii[i]], 
                                                m[ii[i]]));
    }
    return sum;
  }
}

data {
  int<lower=1> I;                // Number of items
  int<lower=1> J;                // Number of respondents
  int<lower=1> N;                // Total responses
  int<lower=1,upper=I> ii[N];    // Item ID
  int<lower=1,upper=J> jj[N];    // Person ID
  int<lower=0> y[N];             // Responses
  int<lower=1> K;                // Number of covariates for location (mu) of ability (theta)
  matrix[J,K] X;                 // Covariate matrix (including intercept) for location (mu) of ability (theta)
  int<lower=1> grainsize;
}

transformed data {
  int r[N];                      // modified response; r = 1, 2, ... m_i + 1
  int m[I];                      // number of difficulty parameters per item
  int pos_alpha[I];              // first position in difficulty for item
  int pos_delta[I];              // first position in between-step change for item 
  
  // Allowing for differing numbers of response categories
  m = rep_array(0, I);															
  for(n in 1:N) {
    r[n] = y[n] + 1;
    if(y[n] > m[ii[n]]) m[ii[n]] = y[n];
  }
  pos_alpha[1] = 1;
  pos_delta[1] = 1;
  for(i in 2:(I)) {
    pos_alpha[i] = pos_alpha[i-1] + m[i-1];
    pos_delta[i] = pos_delta[i-1] + m[i-1] - 1;
  }
}

parameters {
  vector[I] eta;                      // First diffulty parameter for each item
  vector<lower=0>[sum(m)-I] delta;    // Between-step changes in difficulty
  real<lower = 0> beta[I];            // Constrain all discrimination parameters to be positive
  vector[J] theta;                    // Ability parameter
  vector[K] gamma;                    // Covariate coefficients
}

transformed parameters {
  vector[sum(m)] alpha;            // Composite item difficulties
  
  for(i in 1:I) {
    if (m[i] > 0)
      alpha[pos_alpha[i]] = eta[i];
    for (k in 2:m[i])
      alpha[pos_alpha[i]+k-1] = alpha[pos_alpha[i]+k-2] + delta[pos_delta[i]+k-2];
  }
}

model{
  eta ~ normal(0,1);
  delta ~ normal(0,1);
  beta ~ lognormal(1,1);
  gamma ~ student_t(3, 0, 1);
  target += normal_lpdf(alpha | 0, 1);
  theta ~ normal(X*gamma,1);
  mean(theta) ~ normal(0, 0.001 * J); // Mean of ability constrained to 0 for identification
  
  target += reduce_sum(partial_sum_lupmf, r, grainsize, 
                       ii, jj, theta, beta, alpha, m, pos_alpha);
}
1 Like

You should try to vectorise this first. By that I mean, create a vector of length end-start+1 where you collect the alpha and theta stuff. Then you do a single ordered_logistic_lupmf call. Also lookup if there is an appropriate glm function suitable for your case. If yes, then use that.

Slicing parameters is better than slicing data, yes… but the overhead due to not slicing the parameters is not so large any more as compared to the first version of reduce_sum. The time spend on working out this is probably not a great investment.

1 Like

Wow @wds15, that’s awesome! Is there any size of parameter vector beyond which you would still strongly recommend slicing parameters? @saschagobel is already talking about tens of thousands of parameters; what about models with hundreds of thousands?

oh… I missed the dimension here. If it’s that many parameters, then slicing these can really make sense. Slicing parameters means that you double in memory the parameters no matter what else. When your parameters are part of the shared arguments, then you always copy these parameters as many times as you have CPUs in use per chain.

Last time I tested this, I was amazed by how efficient the newer implementation is, since in the first reduce_sum implementation we were copying the shared arguments by each partial sum evaluation (instead of per thread once as it is now).

1 Like

Trying to vectorize this without reduce_sum first following the examples from the user guide here 23.8 Vectorization | Stan User’s Guide and here 1.13 Multivariate priors for hierarchical models | Stan User’s Guide.

For ordered_logistic_lpmf(ints k | vector eta, vectors c), its clear how to create the vector for the first argument. But for the second I’m a bit at a loss because the vectors are of different length (items have different numbers of response categories). So while theta[jj[n]] * beta[ii[n]] returns a real for every n, segment(alpha, pos_alpha[ii[n]], m[ii[n]]) returns a vector, the size of which can vary by n.

 {
    vector[N] theta_beta_v;
    ?[N] alpha_v;	// should be vectors of different size but ragged arrays are not supported
    for (n in 1:N) {
      theta_beta_v[n] = theta[jj[n]] * beta[ii[n]];
      ? = segment(alpha, pos_alpha[ii[n]], m[ii[n]]);
    }
    target += ordered_logistic_lpmf(r | theta_beta_v, alpha_v); 
 }

I think ordered_logistic_glm_lpmf could be used once this has been figured out.

Oh…you got messy data…

I would do the following:

  • Pick the largest subset of the data with the same of number of choices
  • Vectorize the code for that and make sure we really gain in speed
  • Also introduce the glm thing and test the speedup again
  • Once that is sorted we think about how to get the ragged thing going. The easiest is if we have just a handful of different number of choices; it gets more tricky if we need a more clever data-structure (as we don’t have a tuple in Stan it’s going to be a bit messy)