Repository: stan-dev/stan
Pull request #3342: Deserializer for sum_to_zero matrix
Author: login: WardBrian
Created: 2025-03-25T17:58:25Z | Merged: 2025-03-28T16:31:17Z

#### Submission Checklist

- [x] Run unit tests: `./runTests.py src/test/unit`
- [x] Run cpplint: `make cpplint`
- [x] Declare copyright holder and open-source license: see below

#### Summary

De/serialization to accompany https://github.com/stan-dev/math/pull/3169

#### Intended Effect

#### How to Verify

#### Side Effects

#### Documentation

#### Copyright and Licensing

Please list the copyright holder for the work you are submitting (this will be you or your assignee, such as a university or company):

Simons Foundation

By submitting this pull request, the copyright holder is agreeing to license the submitted work under the following licenses:
- Code: BSD 3-clause (https://opensource.org/licenses/BSD-3-Clause)
- Documentation: CC-BY 4.0 (https://creativecommons.org/licenses/by/4.0/)



Diff:
diff --git a/src/stan/io/deserializer.hpp b/src/stan/io/deserializer.hpp
index f8f1a2d8c7f..7cc805ebc59 100644
--- a/src/stan/io/deserializer.hpp
+++ b/src/stan/io/deserializer.hpp
@@ -583,20 +583,19 @@ class deserializer {
 
   /**
    * Return the next zero sum vector of the specified size (using one fewer
-   * unconstrained scalars), incrementing the specified reference with the
-   * log absolute Jacobian determinant (no adjustment, in this case).
+   * unconstrained scalars). This particular transformation is linear, not
+   * requiring a Jacobian adjustment.
    *
-   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix, T&)</code>.
    *
    * @tparam Ret The type to return.
    * @tparam Jacobian Whether to increment the log of the absolute Jacobian
    * determinant of the transform.
    * @tparam LP Type of log probability.
-   * @tparam Sizes A parameter pack of integral types.
-   * @param lp The reference to the variable holding the log
+   * @param lp (Unused) The reference to the variable holding the log density.
    * @param size Number of cells in zero sum vector to generate.
    * @return The next zero sum of the specified size.
-   * @throws std::invalid_argument if number of dimensions (`k`) is zero
+   * @throws std::invalid_argument if `size` is zero
    */
   template <typename Ret, bool Jacobian, typename LP,
             require_not_std_vector_t<Ret>* = nullptr>
@@ -606,10 +605,37 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires
+   * (N - 1) * (M - 1) unconstrained scalars).
+   * This particular transformation is linear, not requiring a Jacobian
+   * adjustment.
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix, T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp (Unused) The reference to the variable holding the log density.
+   * @param N Number of rows in zero sum matrix to generate.
+   * @param M Number of columns in zero sum matrix to generate.
+   * @return The next zero sum of the specified size.
+   * @throws std::invalid_argument either `N` or `M` is zero
+   */
+  template <typename Ret, bool Jacobian, typename LP,
+            require_matrix_t<Ret>* = nullptr>
+  inline auto read_constrain_sum_to_zero(LP& lp, size_t N, size_t M) {
+    stan::math::check_positive("read_sum_to_zero", "N", N);
+    stan::math::check_positive("read_sum_to_zero", "M", M);
+    return stan::math::sum_to_zero_constrain<Jacobian>(
+        this->read<conditional_var_val_t<Ret, matrix_t>>(N - 1, M - 1), lp);
+  }
+
   /**
    * Return the next zero sum vector of the specified size (using one fewer
-   * unconstrained scalars), incrementing the specified reference with the
-   * log absolute Jacobian determinant (no adjustment, in this case).
+   * unconstrained scalars). This particular transformation is linear, not
+   * requiring a Jacobian adjustment.
    *
    * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
    *
@@ -1219,10 +1245,11 @@ class deserializer {
   }
 
   /**
-   * Read a serialized sum_to_zero vector and unconstrain it
+   * Read a serialized sum_to_zero vector and unconstrain it.
    *
    * @tparam Ret Type of output
-   * @return Unconstrained vector
+   * @param size Vector size
+   * @return Unconstrained vector of length (size - 1)
    */
   template <typename Ret, require_not_std_vector_t<Ret>* = nullptr>
   inline auto read_free_sum_to_zero(size_t size) {
@@ -1230,7 +1257,20 @@ class deserializer {
   }
 
   /**
-   * Read serialized zero-sum vectors and unconstrain them
+   * Read a serialized sum_to_zero matrix and unconstrain it.
+   *
+   * @tparam Ret Type of output
+   * @param N Rows of matrix
+   * @param M Cols of matrix
+   * @return Unconstrained matrix of size (N-1) x (M-1)
+   */
+  template <typename Ret, require_matrix_t<Ret>* = nullptr>
+  inline auto read_free_sum_to_zero(size_t N, size_t M) {
+    return stan::math::sum_to_zero_free(this->read<Ret>(N, M));
+  }
+
+  /**
+   * Read serialized zero-sum vectors and unconstrain them.
    *
    * @tparam Ret Type of output
    * @tparam Sizes Types of dimensions of output
diff --git a/src/test/unit/io/deserializer_free_test.cpp b/src/test/unit/io/deserializer_free_test.cpp
index 925b1c7e93b..33443889837 100644
--- a/src/test/unit/io/deserializer_free_test.cpp
+++ b/src/test/unit/io/deserializer_free_test.cpp
@@ -265,6 +265,7 @@ struct SumToZeroConstrain {
 
 TEST(deserializer_vector, read_free_sum_to_zero) {
   using stan::test::deserializer_test;
+  deserializer_test<Eigen::VectorXd, SumToZeroConstrain>(std::make_tuple(1));
   deserializer_test<Eigen::VectorXd, SumToZeroConstrain>(std::make_tuple(4));
   deserializer_test<std::vector<Eigen::VectorXd>, SumToZeroConstrain>(
       std::make_tuple(2, 4));
@@ -272,6 +273,16 @@ TEST(deserializer_vector, read_free_sum_to_zero) {
                     SumToZeroConstrain>(std::make_tuple(3, 2, 4));
 }
 
+TEST(deserializer_matrix, read_free_sum_to_zero) {
+  using stan::test::deserializer_test;
+  deserializer_test<Eigen::MatrixXd, SumToZeroConstrain>(std::make_tuple(1, 1));
+  deserializer_test<Eigen::MatrixXd, SumToZeroConstrain>(std::make_tuple(4, 5));
+  deserializer_test<std::vector<Eigen::MatrixXd>, SumToZeroConstrain>(
+      std::make_tuple(2, 4, 5));
+  deserializer_test<std::vector<std::vector<Eigen::MatrixXd>>,
+                    SumToZeroConstrain>(std::make_tuple(3, 2, 4, 2));
+}
+
 // ordered
 template <typename Ret>
 struct OrderedConstrain {
diff --git a/src/test/unit/io/deserializer_stdvector_test.cpp b/src/test/unit/io/deserializer_stdvector_test.cpp
index e934f6e42e2..0c6136f0655 100644
--- a/src/test/unit/io/deserializer_stdvector_test.cpp
+++ b/src/test/unit/io/deserializer_stdvector_test.cpp
@@ -175,6 +175,50 @@ TEST(deserializer_array, sum_to_zero) {
   }
 }
 
+TEST(deserializer_array, sum_to_zero_matrix) {
+  std::vector<int> theta_i;
+  std::vector<double> theta;
+  for (size_t i = 0; i < 100U; ++i)
+    theta.push_back(static_cast<double>(i));
+
+  stan::io::deserializer<double> deserializer1(theta, theta_i);
+  stan::io::deserializer<double> deserializer2(theta, theta_i);
+
+  // no jac
+  {
+    double lp_ref = 0.0;
+    double lp = 0.0;
+    auto y
+        = deserializer1
+              .read_constrain_sum_to_zero<std::vector<Eigen::MatrixXd>, false>(
+                  lp, 4, 3, 2);
+    for (size_t i = 0; i < 4; ++i) {
+      stan::test::expect_near_rel(
+          "test_std_vector_deserializer", y[i],
+          deserializer2.read_constrain_sum_to_zero<Eigen::MatrixXd, false>(
+              lp_ref, 3, 2));
+    }
+    EXPECT_FLOAT_EQ(lp_ref, lp);
+  }
+
+  // jac
+  {
+    double lp_ref = 0.0;
+    double lp = 0.0;
+    auto y
+        = deserializer1
+              .read_constrain_sum_to_zero<std::vector<Eigen::MatrixXd>, true>(
+                  lp, 4, 3, 2);
+    for (size_t i = 0; i < 4; ++i) {
+      stan::test::expect_near_rel(
+          "test_std_vector_deserializer", y[i],
+          deserializer2.read_constrain_sum_to_zero<Eigen::MatrixXd, true>(
+              lp_ref, 3, 2));
+    }
+    EXPECT_FLOAT_EQ(lp_ref, lp);
+  }
+}
+
 // ordered
 
 TEST(deserializer_array, ordered) {
diff --git a/src/test/unit/io/deserializer_test.cpp b/src/test/unit/io/deserializer_test.cpp
index 71940e6c904..74ad72e96f1 100644
--- a/src/test/unit/io/deserializer_test.cpp
+++ b/src/test/unit/io/deserializer_test.cpp
@@ -714,6 +714,63 @@ TEST(deserializer_vector, sum_to_zero_jacobian) {
   EXPECT_FLOAT_EQ(lp_ref, lp);
 }
 
+TEST(deserializer_matrix, sum_to_zero_constrain) {
+  std::vector<int> theta_i;
+  std::vector<double> theta;
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(-2.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<double> deserializer(theta, theta_i);
+  double lp = 0.0;
+  Eigen::MatrixXd reference
+      = stan::math::sum_to_zero_constrain(stan::math::to_matrix(theta, 2, 2));
+  Eigen::MatrixXd phi(
+      deserializer.read_constrain_sum_to_zero<Eigen::MatrixXd, false>(lp, 3,
+                                                                      3));
+  for (int n = 0; n < phi.size(); ++n) {
+    EXPECT_FLOAT_EQ(reference(n), phi(n));
+  }
+}
+
+TEST(deserializer_matrix, sum_to_zero_constrain_empty) {
+  std::vector<int> theta_i;
+  std::vector<double> theta;
+
+  stan::io::deserializer<double> deserializer(theta, theta_i);
+  double lp = 0.0;
+
+  Eigen::MatrixXd phi(
+      deserializer.read_constrain_sum_to_zero<Eigen::MatrixXd, false>(lp, 1,
+                                                                      1));
+
+  EXPECT_EQ(1, phi.rows());
+  EXPECT_EQ(1, phi.cols());
+  for (int n = 0; n < phi.size(); ++n) {
+    EXPECT_FLOAT_EQ(0.0, phi(n));
+  }
+}
+
+TEST(deserializer_matrix, sum_to_zero_jacobian) {
+  std::vector<int> theta_i;
+  std::vector<double> theta;
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(-2.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<double> deserializer(theta, theta_i);
+  double lp = 0.0;
+  double lp_ref = 0.0;
+  Eigen::MatrixXd reference = stan::math::sum_to_zero_constrain(
+      stan::math::to_matrix(theta, 2, 2), lp_ref);
+  Eigen::MatrixXd phi(
+      deserializer.read_constrain_sum_to_zero<Eigen::MatrixXd, true>(lp, 3, 3));
+  for (int n = 0; n < phi.size(); ++n) {
+    EXPECT_FLOAT_EQ(reference(n), phi(n));
+  }
+  EXPECT_FLOAT_EQ(lp_ref, lp);
+}
+
 // ordered
 
 TEST(deserializer_vector, ordered_constrain) {
diff --git a/src/test/unit/io/deserializer_var_test.cpp b/src/test/unit/io/deserializer_var_test.cpp
index cdfa70c0b17..daff8481379 100644
--- a/src/test/unit/io/deserializer_var_test.cpp
+++ b/src/test/unit/io/deserializer_var_test.cpp
@@ -640,6 +640,50 @@ TEST(deserializer_vector, sum_to_zero_jacobian) {
   }
   EXPECT_FLOAT_EQ(lp_ref.val(), lp.val());
 }
+
+TEST(deserializer_matrix, sum_to_zero_constrain) {
+  std::vector<int> theta_i;
+  std::vector<stan::math::var> theta;
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(-2.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<stan::math::var> deserializer(theta, theta_i);
+  stan::math::var lp = 0;
+  Eigen::Matrix<stan::math::var, Eigen::Dynamic, Eigen::Dynamic> reference
+      = stan::math::sum_to_zero_constrain(stan::math::to_matrix(theta, 2, 2));
+  Eigen::Matrix<stan::math::var, Eigen::Dynamic, Eigen::Dynamic> phi(
+      deserializer.read_constrain_sum_to_zero<
+          Eigen::Matrix<stan::math::var, Eigen::Dynamic, Eigen::Dynamic>,
+          false>(lp, 3, 3));
+  for (int n = 0; n < phi.size(); ++n) {
+    EXPECT_FLOAT_EQ(reference(n).val(), phi(n).val());
+  }
+}
+
+TEST(deserializer_matrix, sum_to_zero_jacobian) {
+  std::vector<int> theta_i;
+  std::vector<stan::math::var> theta;
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(-2.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<stan::math::var> deserializer(theta, theta_i);
+  stan::math::var lp = 0.0;
+  stan::math::var lp_ref = 0.0;
+  Eigen::Matrix<stan::math::var, Eigen::Dynamic, Eigen::Dynamic> reference
+      = stan::math::sum_to_zero_constrain(stan::math::to_matrix(theta, 2, 2),
+                                          lp_ref);
+  Eigen::Matrix<stan::math::var, Eigen::Dynamic, Eigen::Dynamic> phi(
+      deserializer.read_constrain_sum_to_zero<
+          Eigen::Matrix<stan::math::var, Eigen::Dynamic, Eigen::Dynamic>, true>(
+          lp, 3, 3));
+  for (int n = 0; n < phi.size(); ++n) {
+    EXPECT_FLOAT_EQ(reference(n).val(), phi(n).val());
+  }
+  EXPECT_FLOAT_EQ(lp_ref.val(), lp.val());
+}
+
 // ordered
 
 TEST(deserializer_vector, ordered_constrain) {
diff --git a/src/test/unit/io/deserializer_varmat_test.cpp b/src/test/unit/io/deserializer_varmat_test.cpp
index 34d08a66c60..d91fd0f8dc1 100644
--- a/src/test/unit/io/deserializer_varmat_test.cpp
+++ b/src/test/unit/io/deserializer_varmat_test.cpp
@@ -363,6 +363,42 @@ TEST(deserializer, read_constrain_sum_to_zero_jacobian) {
   EXPECT_FLOAT_EQ(lp_ref.val(), lp.val());
 }
 
+TEST(deserializer_var_matrix, read_constrain_sum_to_zero_constrain) {
+  std::vector<int> theta_i;
+  std::vector<stan::math::var> theta;
+  theta.push_back(-2.0);
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<stan::math::var> deserializer(theta, theta_i);
+  stan::math::var lp = 0.0;
+  auto reference
+      = stan::math::sum_to_zero_constrain(stan::math::to_matrix(theta, 2, 2));
+  auto y
+      = deserializer.read_constrain_sum_to_zero<var_matrix_t, false>(lp, 3, 3);
+  EXPECT_TRUE((std::is_same<var_matrix_t, decltype(y)>::value));
+  stan::test::expect_near_rel("deserializer tests", reference.val(), y.val());
+}
+
+TEST(deserializer_var_matrix, read_constrain_sum_to_zero_jacobian) {
+  std::vector<int> theta_i;
+  std::vector<stan::math::var> theta;
+  theta.push_back(-2.0);
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<stan::math::var> deserializer(theta, theta_i);
+  stan::math::var lp_ref = 0.0;
+  stan::math::var lp = 0.0;
+  auto reference = stan::math::sum_to_zero_constrain(
+      stan::math::to_matrix(theta, 2, 2), lp_ref);
+  auto y
+      = deserializer.read_constrain_sum_to_zero<var_matrix_t, false>(lp, 3, 3);
+  EXPECT_TRUE((std::is_same<var_matrix_t, decltype(y)>::value));
+  stan::test::expect_near_rel("deserializer tests", reference.val(), y.val());
+  EXPECT_FLOAT_EQ(lp_ref.val(), lp.val());
+}
+
 // ordered
 
 TEST(deserializer, read_constrain_ordered_constrain) {
diff --git a/src/test/unit/io/serializer_test.cpp b/src/test/unit/io/serializer_test.cpp
index 909df7949c0..f17b6f26cda 100644
--- a/src/test/unit/io/serializer_test.cpp
+++ b/src/test/unit/io/serializer_test.cpp
@@ -522,6 +522,15 @@ TEST(serializer_vectorized, write_free_sum_to_zero) {
                   SumToZeroConstrain>(std::make_tuple(3, 2, 4));
 }
 
+TEST(serializer_vectorized, write_free_sum_to_zero_mat) {
+  using stan::test::serializer_test;
+  serializer_test<Eigen::MatrixXd, SumToZeroConstrain>(std::make_tuple(4, 5));
+  serializer_test<std::vector<Eigen::MatrixXd>, SumToZeroConstrain>(
+      std::make_tuple(2, 4, 3));
+  serializer_test<std::vector<std::vector<Eigen::MatrixXd>>,
+                  SumToZeroConstrain>(std::make_tuple(3, 2, 4, 2));
+}
+
 // ordered
 
 template <typename Ret>
@@ -692,7 +701,7 @@ struct StochasticCol {
     };
   }
 };
-TEST(deserializer_vector, read_stochastic_column_matrix) {
+TEST(serializer_vectorized, read_stochastic_column_matrix) {
   using stan::test::serializer_test;
   serializer_test<Eigen::MatrixXd, StochasticCol>(std::make_tuple(3, 3));
   serializer_test<std::vector<Eigen::MatrixXd>, StochasticCol>(
@@ -716,7 +725,7 @@ struct StochasticRow {
     };
   }
 };
-TEST(deserializer_vector, read_stochastic_row_matrix) {
+TEST(serializer_vectorized, read_stochastic_row_matrix) {
   using stan::test::serializer_test;
   serializer_test<Eigen::MatrixXd, StochasticRow>(std::make_tuple(3, 3));
   serializer_test<std::vector<Eigen::MatrixXd>, StochasticRow>(


Reviews:
user: login: bob-carpenter
body: This basically looks good---just a few doc corrections and a request for a boundary test.

One thing I've found is that for things like this, it can help to write a very simple test framework that tests the round tripping and tests gradients and log densities.  Then you can use it to try out a bunch of examples.
state: CHANGES_REQUESTED
author_association: MEMBER
submitted_at: 2025-03-25T19:46:36Z
user: login: WardBrian
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T19:56:18Z
user: login: WardBrian
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T20:00:43Z
user: login: bob-carpenter
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-25T20:21:27Z
user: login: bob-carpenter
body: Thanks for adding the boundary test.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-25T20:22:28Z
user: login: spinkney
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T21:38:02Z
user: login: spinkney
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-26T21:38:04Z
user: login: WardBrian
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T13:45:55Z

Review comments:
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: N-1 * M-1 -> `(N - 1) * (M - 1)`
created_at: 2025-03-25T19:37:14Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: Can you just note that it doesn't require a Jacobian adjustment rather than saying it "increments the specified reference"?
created_at: 2025-03-25T19:37:43Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: Space this out like other code Matrix,T -> Matrix, T
created_at: 2025-03-25T19:38:06Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp The reference to the variable holding the log
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: holding the log -> holding the log density

This is tricky terminologically.  A "log probability function" can be a pdf, in which case it returns a density (units of change in probability) as opposed to a probability, or a pdf, in which case it does return probabilities.  But what we don't have in Stan is `target` holding a log probability---it's always a density.  Our doc is a mess around all this.
created_at: 2025-03-25T19:39:35Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp The reference to the variable holding the log
+   * @param N Number of rows in zero sum matrix to generate.
+   * @param M Number of columns in zero sum matrix to generate.
+   * @return The next zero sum of the specified size.
+   * @throws std::invalid_argument either `N` or `M` is zero
+   */
+  template <typename Ret, bool Jacobian, typename LP,
+            require_matrix_t<Ret>* = nullptr>
+  inline auto read_constrain_sum_to_zero(LP& lp, size_t N, size_t M) {
+    stan::math::check_positive("read_sum_to_zero", "N", N);
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: read_sum_to_zero -> read_constrain_sum_to_zero
created_at: 2025-03-25T19:40:03Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp The reference to the variable holding the log
+   * @param N Number of rows in zero sum matrix to generate.
+   * @param M Number of columns in zero sum matrix to generate.
+   * @return The next zero sum of the specified size.
+   * @throws std::invalid_argument either `N` or `M` is zero
+   */
+  template <typename Ret, bool Jacobian, typename LP,
+            require_matrix_t<Ret>* = nullptr>
+  inline auto read_constrain_sum_to_zero(LP& lp, size_t N, size_t M) {
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: [question] Is the LP argument first?  I thought we'd included it last before.
created_at: 2025-03-25T19:41:06Z
author_association: MEMBER
diff_hunk: @@ -1229,6 +1254,17 @@ class deserializer {
     return stan::math::sum_to_zero_free(this->read<Ret>(size));
   }
 
+  /**
+   * Read a serialized sum_to_zero matrix and unconstrain it
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: Sentences need to end with periods.  In other doc, you've ended parameter descriptions with period.  This we've been very inconsistent about.
created_at: 2025-03-25T19:41:53Z
author_association: MEMBER
diff_hunk: @@ -1229,6 +1254,17 @@ class deserializer {
     return stan::math::sum_to_zero_free(this->read<Ret>(size));
   }
 
+  /**
+   * Read a serialized sum_to_zero matrix and unconstrain it
+   *
+   * @tparam Ret Type of output
+   * @return Unconstrained matrix
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: Missing doc for M and N.
created_at: 2025-03-25T19:42:12Z
author_association: MEMBER
diff_hunk: @@ -714,6 +714,49 @@ TEST(deserializer_vector, sum_to_zero_jacobian) {
   EXPECT_FLOAT_EQ(lp_ref, lp);
 }
 
+TEST(deserializer_matrix, sum_to_zero_constrain) {
+  std::vector<int> theta_i;
+  std::vector<double> theta;
+  theta.push_back(3.0);
+  theta.push_back(-1.0);
+  theta.push_back(-2.0);
+  theta.push_back(0.0);
+  stan::io::deserializer<double> deserializer(theta, theta_i);
+  double lp = 0.0;
+  Eigen::MatrixXd reference
+      = stan::math::sum_to_zero_constrain(stan::math::to_matrix(theta, 2, 2));
+  Eigen::MatrixXd phi(
+      deserializer.read_constrain_sum_to_zero<Eigen::MatrixXd, false>(lp, 3,
+                                                                      3));
+  for (int i = 0; i < phi.rows(); ++i) {
path: src/test/unit/io/deserializer_test.cpp
user: login: bob-carpenter
body: [optional] If `reference` and `phi` are the same size and same row-major vs. column major, it's easier to do this as

```cpp
for (int n = 0; n < phi.size(); ++n) {
  EXPECT_FLOAT_EQ(reference(n), phi(n));
}
```
created_at: 2025-03-25T19:44:51Z
author_association: MEMBER
diff_hunk: @@ -175,6 +175,50 @@ TEST(deserializer_array, sum_to_zero) {
   }
 }
 
+TEST(deserializer_array, sum_to_zero_matrix) {
path: src/test/unit/io/deserializer_stdvector_test.cpp
user: login: bob-carpenter
body: I would like to see a boundary test for a 1 x 1 matrix, which has zero underlying unconstrained parameters and always just contains 0.
created_at: 2025-03-25T19:45:46Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp The reference to the variable holding the log
+   * @param N Number of rows in zero sum matrix to generate.
+   * @param M Number of columns in zero sum matrix to generate.
+   * @return The next zero sum of the specified size.
+   * @throws std::invalid_argument either `N` or `M` is zero
+   */
+  template <typename Ret, bool Jacobian, typename LP,
+            require_matrix_t<Ret>* = nullptr>
+  inline auto read_constrain_sum_to_zero(LP& lp, size_t N, size_t M) {
+    stan::math::check_positive("read_sum_to_zero", "N", N);
path: src/stan/io/deserializer.hpp
user: login: WardBrian
body: For whatever reason, we've left off the "_constrain" in other error messages for these (e.g., "read_simplex" is also used instead of "read_constrain_simplex")
created_at: 2025-03-25T19:56:18Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp The reference to the variable holding the log
+   * @param N Number of rows in zero sum matrix to generate.
+   * @param M Number of columns in zero sum matrix to generate.
+   * @return The next zero sum of the specified size.
+   * @throws std::invalid_argument either `N` or `M` is zero
+   */
+  template <typename Ret, bool Jacobian, typename LP,
+            require_matrix_t<Ret>* = nullptr>
+  inline auto read_constrain_sum_to_zero(LP& lp, size_t N, size_t M) {
path: src/stan/io/deserializer.hpp
user: login: WardBrian
body: It is last in the math code, but first in the IO
created_at: 2025-03-25T20:00:43Z
author_association: MEMBER
diff_hunk: @@ -606,6 +605,32 @@ class deserializer {
         this->read<Ret>(size - 1), lp);
   }
 
+  /**
+   * Return the next zero sum matrix of the specified dimensions (requires N-1 *
+   * M-1 unconstrained scalars), incrementing the specified reference with the
+   * log absolute Jacobian determinant (no adjustment, in this case).
+   *
+   * <p>See <code>stan::math::sum_to_zero_constrain(Eigen::Matrix,T&)</code>.
+   *
+   * @tparam Ret The type to return.
+   * @tparam Jacobian Whether to increment the log of the absolute Jacobian
+   * determinant of the transform.
+   * @tparam LP Type of log probability.
+   * @param lp The reference to the variable holding the log
+   * @param N Number of rows in zero sum matrix to generate.
+   * @param M Number of columns in zero sum matrix to generate.
+   * @return The next zero sum of the specified size.
+   * @throws std::invalid_argument either `N` or `M` is zero
+   */
+  template <typename Ret, bool Jacobian, typename LP,
+            require_matrix_t<Ret>* = nullptr>
+  inline auto read_constrain_sum_to_zero(LP& lp, size_t N, size_t M) {
+    stan::math::check_positive("read_sum_to_zero", "N", N);
path: src/stan/io/deserializer.hpp
user: login: bob-carpenter
body: Either way, it's probably not an error we want to show to a Stan User as it's not mentioning things in the Stan language.  Otherwise, I think we should just fix these.  Not a high priority as they're mainly for internal debugging.
created_at: 2025-03-25T20:21:27Z
author_association: MEMBER
diff_hunk: @@ -525,8 +525,8 @@ class deserializer {
 
   /**
    * Return the next simplex of the specified size (using one fewer
-   * unconstrained scalars), incrementing the specified reference with the
-   * log absolute Jacobian determinant.
+   * unconstrained scalars). This particular transformation is linear, not
path: src/stan/io/deserializer.hpp
user: login: spinkney
body: this is on the `simplex_constrain` and it is not linear, possibly a copy paste error
created_at: 2025-03-26T21:38:02Z
author_association: NONE
diff_hunk: @@ -550,8 +550,8 @@ class deserializer {
 
   /**
    * Return the next simplex of the specified size (using one fewer
-   * unconstrained scalars), incrementing the specified reference with the
-   * log absolute Jacobian determinant.
+   * unconstrained scalars). This particular transformation is linear, not
path: src/stan/io/deserializer.hpp
user: login: spinkney
body: this is on the `simplex_constrain` and it is not linear, possibly a copy paste error
created_at: 2025-03-26T21:38:04Z
author_association: NONE
diff_hunk: @@ -525,8 +525,8 @@ class deserializer {
 
   /**
    * Return the next simplex of the specified size (using one fewer
-   * unconstrained scalars), incrementing the specified reference with the
-   * log absolute Jacobian determinant.
+   * unconstrained scalars). This particular transformation is linear, not
path: src/stan/io/deserializer.hpp
user: login: WardBrian
body: Good catch, definitely a copy/paste
created_at: 2025-03-27T13:45:54Z
author_association: MEMBER

PR comments:
user: login: WardBrian
created_at: 2025-03-25T20:18:06Z
body: Thanks @bob-carpenter -- most of the doc comments you had also applied to the vector version (these were copied, primarily), so I've updated those as well.

We do have some test scaffolding for this in the `deserializer_free_test.hpp`, but it hasn't been extended to cover all the cases we test across these files. Would be worth doing at some point for sure.
author_association: MEMBER
user: login: bob-carpenter
created_at: 2025-03-25T20:22:48Z
body: This looks good.  Feel free to merge when tests pass.
author_association: MEMBER
user: login: stan-buildbot
created_at: 2025-03-25T23:54:59Z
body: - - - - - - - - - - - - - - - - - - - - -
| Name | Old Result | New Result | Ratio | Performance change( 1 - new / old ) |
| ------------- |------------- | ------------- | ------------- | ------------- |
| arma/arma.stan | 0.38 | 0.3 | 1.29 | 22.26% faster |
| low_dim_corr_gauss/low_dim_corr_gauss.stan | 0.01 | 0.01 | 1.04 | 3.79% faster |
| gp_regr/gen_gp_data.stan | 0.03 | 0.02 | 1.11 | 9.72% faster |
| gp_regr/gp_regr.stan | 0.09 | 0.09 | 1.05 | 4.7% faster |
| sir/sir.stan | 71.7 | 66.19 | 1.08 | 7.69% faster |
| irt_2pl/irt_2pl.stan | 4.29 | 3.9 | 1.1 | 9.07% faster |
| eight_schools/eight_schools.stan | 0.06 | 0.05 | 1.09 | 8.55% faster |
| pkpd/sim_one_comp_mm_elim_abs.stan | 0.26 | 0.24 | 1.07 | 6.91% faster |
| pkpd/one_comp_mm_elim_abs.stan | 20.27 | 18.46 | 1.1 | 8.93% faster |
| garch/garch.stan | 0.41 | 0.4 | 1.03 | 2.5% faster |
| low_dim_gauss_mix/low_dim_gauss_mix.stan | 2.61 | 2.58 | 1.01 | 1.34% faster |
| arK/arK.stan | 1.79 | 1.72 | 1.04 | 4.07% faster |
| gp_pois_regr/gp_pois_regr.stan | 2.77 | 2.69 | 1.03 | 2.85% faster |
| low_dim_gauss_mix_collapse/low_dim_gauss_mix_collapse.stan | 8.76 | 8.31 | 1.05 | 5.17% faster |
| performance.compilation | 184.3 | 180.67 | 1.02 | 1.97% faster |
Mean result: 1.0744710735387761
- - - - - - - - - - - - - - - - - - - - -
[Jenkins Console Log](https://jenkins.flatironinstitute.org/job/Stan/job/Stan/view/change-requests/job/PR-3342/4/consoleFull)
[Blue Ocean](https://jenkins.flatironinstitute.org/blue/organizations/jenkins/Stan%2FStan/detail/PR-3342/4/pipeline)
Commit hash: 4adb4ac5420f16dca008d1b8a4c39b44dd6fea4d
- - - - - - - - - - - - - - - - - - - - -
<details><summary>Machine information</summary>
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.3 LTS
Release: 20.04
Codename: focal

CPU: 
Architecture:                         x86_64
CPU op-mode(s):                       32-bit, 64-bit
Byte Order:                           Little Endian
Address sizes:                        46 bits physical, 48 bits virtual
CPU(s):                               80
On-line CPU(s) list:                  0-79
Thread(s) per core:                   2
Core(s) per socket:                   20
Socket(s):                            2
NUMA node(s):                         2
Vendor ID:                            GenuineIntel
CPU family:                           6
Model:                                85
Model name:                           Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
Stepping:                             4
CPU MHz:                              2400.000
CPU max MHz:                          3700.0000
CPU min MHz:                          1000.0000
BogoMIPS:                             4800.00
Virtualization:                       VT-x
L1d cache:                            1.3 MiB
L1i cache:                            1.3 MiB
L2 cache:                             40 MiB
L3 cache:                             55 MiB
NUMA node0 CPU(s):                    0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78
NUMA node1 CPU(s):                    1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79
Vulnerability Gather data sampling:   Mitigation; Microcode
Vulnerability Itlb multihit:          KVM: Mitigation: VMX disabled
Vulnerability L1tf:                   Mitigation; PTE Inversion; VMX conditional cache flushes, SMT vulnerable
Vulnerability Mds:                    Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Meltdown:               Mitigation; PTI
Vulnerability Mmio stale data:        Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Reg file data sampling: Not affected
Vulnerability Retbleed:               Mitigation; IBRS
Vulnerability Spec rstack overflow:   Not affected
Vulnerability Spec store bypass:      Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:             Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:             Mitigation; IBRS; IBPB conditional; STIBP conditional; RSB filling; PBRSB-eIBRS Not affected; BHI Not affected
Vulnerability Srbds:                  Not affected
Vulnerability Tsx async abort:        Mitigation; Clear CPU buffers; SMT vulnerable
Flags:                                fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single pti intel_ppin ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke md_clear flush_l1d arch_capabilities

G++: 
g++ (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Clang: 
clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

</details>
author_association: CONTRIBUTOR
user: login: stan-buildbot
created_at: 2025-03-27T16:33:32Z
body: - - - - - - - - - - - - - - - - - - - - -
| Name | Old Result | New Result | Ratio | Performance change( 1 - new / old ) |
| ------------- |------------- | ------------- | ------------- | ------------- |
| arma/arma.stan | 0.32 | 0.3 | 1.06 | 6.01% faster |
| low_dim_corr_gauss/low_dim_corr_gauss.stan | 0.01 | 0.01 | 1.01 | 0.55% faster |
| gp_regr/gen_gp_data.stan | 0.03 | 0.02 | 1.05 | 5.16% faster |
| gp_regr/gp_regr.stan | 0.09 | 0.09 | 1.06 | 5.72% faster |
| sir/sir.stan | 68.91 | 67.16 | 1.03 | 2.55% faster |
| irt_2pl/irt_2pl.stan | 4.18 | 3.99 | 1.05 | 4.47% faster |
| eight_schools/eight_schools.stan | 0.06 | 0.05 | 1.04 | 4.3% faster |
| pkpd/sim_one_comp_mm_elim_abs.stan | 0.26 | 0.24 | 1.06 | 5.51% faster |
| pkpd/one_comp_mm_elim_abs.stan | 20.6 | 18.42 | 1.12 | 10.59% faster |
| garch/garch.stan | 0.46 | 0.41 | 1.12 | 10.63% faster |
| low_dim_gauss_mix/low_dim_gauss_mix.stan | 2.85 | 2.62 | 1.09 | 7.99% faster |
| arK/arK.stan | 1.95 | 1.74 | 1.12 | 10.79% faster |
| gp_pois_regr/gp_pois_regr.stan | 2.93 | 2.71 | 1.08 | 7.54% faster |
| low_dim_gauss_mix_collapse/low_dim_gauss_mix_collapse.stan | 9.02 | 8.41 | 1.07 | 6.69% faster |
| performance.compilation | 189.88 | 181.12 | 1.05 | 4.61% faster |
Mean result: 1.0671724440745107
- - - - - - - - - - - - - - - - - - - - -
[Jenkins Console Log](https://jenkins.flatironinstitute.org/job/Stan/job/Stan/view/change-requests/job/PR-3342/6/consoleFull)
[Blue Ocean](https://jenkins.flatironinstitute.org/blue/organizations/jenkins/Stan%2FStan/detail/PR-3342/6/pipeline)
Commit hash: 4adb4ac5420f16dca008d1b8a4c39b44dd6fea4d
- - - - - - - - - - - - - - - - - - - - -
<details><summary>Machine information</summary>
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.3 LTS
Release: 20.04
Codename: focal

CPU: 
Architecture:                         x86_64
CPU op-mode(s):                       32-bit, 64-bit
Byte Order:                           Little Endian
Address sizes:                        46 bits physical, 48 bits virtual
CPU(s):                               80
On-line CPU(s) list:                  0-79
Thread(s) per core:                   2
Core(s) per socket:                   20
Socket(s):                            2
NUMA node(s):                         2
Vendor ID:                            GenuineIntel
CPU family:                           6
Model:                                85
Model name:                           Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
Stepping:                             4
CPU MHz:                              1081.123
CPU max MHz:                          3700.0000
CPU min MHz:                          1000.0000
BogoMIPS:                             4800.00
Virtualization:                       VT-x
L1d cache:                            1.3 MiB
L1i cache:                            1.3 MiB
L2 cache:                             40 MiB
L3 cache:                             55 MiB
NUMA node0 CPU(s):                    0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78
NUMA node1 CPU(s):                    1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79
Vulnerability Gather data sampling:   Mitigation; Microcode
Vulnerability Itlb multihit:          KVM: Mitigation: VMX disabled
Vulnerability L1tf:                   Mitigation; PTE Inversion; VMX conditional cache flushes, SMT vulnerable
Vulnerability Mds:                    Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Meltdown:               Mitigation; PTI
Vulnerability Mmio stale data:        Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Reg file data sampling: Not affected
Vulnerability Retbleed:               Mitigation; IBRS
Vulnerability Spec rstack overflow:   Not affected
Vulnerability Spec store bypass:      Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:             Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:             Mitigation; IBRS; IBPB conditional; STIBP conditional; RSB filling; PBRSB-eIBRS Not affected; BHI Not affected
Vulnerability Srbds:                  Not affected
Vulnerability Tsx async abort:        Mitigation; Clear CPU buffers; SMT vulnerable
Flags:                                fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single pti intel_ppin ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke md_clear flush_l1d arch_capabilities

G++: 
g++ (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Clang: 
clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

</details>
author_association: CONTRIBUTOR
user: login: stan-buildbot
created_at: 2025-03-27T23:01:07Z
body: - - - - - - - - - - - - - - - - - - - - -
| Name | Old Result | New Result | Ratio | Performance change( 1 - new / old ) |
| ------------- |------------- | ------------- | ------------- | ------------- |
| arma/arma.stan | 0.33 | 0.3 | 1.07 | 6.84% faster |
| low_dim_corr_gauss/low_dim_corr_gauss.stan | 0.01 | 0.01 | 1.08 | 7.07% faster |
| gp_regr/gen_gp_data.stan | 0.03 | 0.02 | 1.11 | 9.72% faster |
| gp_regr/gp_regr.stan | 0.09 | 0.09 | 1.05 | 5.06% faster |
| sir/sir.stan | 69.41 | 66.94 | 1.04 | 3.56% faster |
| irt_2pl/irt_2pl.stan | 4.12 | 3.99 | 1.03 | 3.09% faster |
| eight_schools/eight_schools.stan | 0.06 | 0.05 | 1.06 | 5.94% faster |
| pkpd/sim_one_comp_mm_elim_abs.stan | 0.26 | 0.24 | 1.06 | 5.4% faster |
| pkpd/one_comp_mm_elim_abs.stan | 19.96 | 18.72 | 1.07 | 6.24% faster |
| garch/garch.stan | 0.44 | 0.41 | 1.09 | 7.93% faster |
| low_dim_gauss_mix/low_dim_gauss_mix.stan | 2.65 | 2.71 | 0.98 | -2.52% slower |
| arK/arK.stan | 1.83 | 1.75 | 1.05 | 4.65% faster |
| gp_pois_regr/gp_pois_regr.stan | 2.87 | 2.79 | 1.03 | 2.91% faster |
| low_dim_gauss_mix_collapse/low_dim_gauss_mix_collapse.stan | 8.89 | 8.53 | 1.04 | 3.99% faster |
| performance.compilation | 184.15 | 182.1 | 1.01 | 1.11% faster |
Mean result: 1.0506018343835004
- - - - - - - - - - - - - - - - - - - - -
[Jenkins Console Log](https://jenkins.flatironinstitute.org/job/Stan/job/Stan/view/change-requests/job/PR-3342/7/consoleFull)
[Blue Ocean](https://jenkins.flatironinstitute.org/blue/organizations/jenkins/Stan%2FStan/detail/PR-3342/7/pipeline)
Commit hash: 4adb4ac5420f16dca008d1b8a4c39b44dd6fea4d
- - - - - - - - - - - - - - - - - - - - -
<details><summary>Machine information</summary>
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.3 LTS
Release: 20.04
Codename: focal

CPU: 
Architecture:                         x86_64
CPU op-mode(s):                       32-bit, 64-bit
Byte Order:                           Little Endian
Address sizes:                        46 bits physical, 48 bits virtual
CPU(s):                               80
On-line CPU(s) list:                  0-79
Thread(s) per core:                   2
Core(s) per socket:                   20
Socket(s):                            2
NUMA node(s):                         2
Vendor ID:                            GenuineIntel
CPU family:                           6
Model:                                85
Model name:                           Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
Stepping:                             4
CPU MHz:                              2400.000
CPU max MHz:                          3700.0000
CPU min MHz:                          1000.0000
BogoMIPS:                             4800.00
Virtualization:                       VT-x
L1d cache:                            1.3 MiB
L1i cache:                            1.3 MiB
L2 cache:                             40 MiB
L3 cache:                             55 MiB
NUMA node0 CPU(s):                    0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78
NUMA node1 CPU(s):                    1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79
Vulnerability Gather data sampling:   Mitigation; Microcode
Vulnerability Itlb multihit:          KVM: Mitigation: VMX disabled
Vulnerability L1tf:                   Mitigation; PTE Inversion; VMX conditional cache flushes, SMT vulnerable
Vulnerability Mds:                    Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Meltdown:               Mitigation; PTI
Vulnerability Mmio stale data:        Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Reg file data sampling: Not affected
Vulnerability Retbleed:               Mitigation; IBRS
Vulnerability Spec rstack overflow:   Not affected
Vulnerability Spec store bypass:      Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:             Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:             Mitigation; IBRS; IBPB conditional; STIBP conditional; RSB filling; PBRSB-eIBRS Not affected; BHI Not affected
Vulnerability Srbds:                  Not affected
Vulnerability Tsx async abort:        Mitigation; Clear CPU buffers; SMT vulnerable
Flags:                                fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single pti intel_ppin ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke md_clear flush_l1d arch_capabilities

G++: 
g++ (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Clang: 
clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

</details>
author_association: CONTRIBUTOR
user: login: stan-buildbot
created_at: 2025-03-28T16:13:38Z
body: - - - - - - - - - - - - - - - - - - - - -
| Name | Old Result | New Result | Ratio | Performance change( 1 - new / old ) |
| ------------- |------------- | ------------- | ------------- | ------------- |
| arma/arma.stan | 0.32 | 0.31 | 1.06 | 5.51% faster |
| low_dim_corr_gauss/low_dim_corr_gauss.stan | 0.01 | 0.01 | 1.07 | 6.94% faster |
| gp_regr/gen_gp_data.stan | 0.02 | 0.02 | 1.04 | 4.2% faster |
| gp_regr/gp_regr.stan | 0.09 | 0.09 | 1.05 | 5.1% faster |
| sir/sir.stan | 70.49 | 66.16 | 1.07 | 6.13% faster |
| irt_2pl/irt_2pl.stan | 4.15 | 3.91 | 1.06 | 5.63% faster |
| eight_schools/eight_schools.stan | 0.06 | 0.05 | 1.04 | 4.17% faster |
| pkpd/sim_one_comp_mm_elim_abs.stan | 0.26 | 0.25 | 1.04 | 4.28% faster |
| pkpd/one_comp_mm_elim_abs.stan | 20.13 | 18.5 | 1.09 | 8.11% faster |
| garch/garch.stan | 0.44 | 0.4 | 1.1 | 9.34% faster |
| low_dim_gauss_mix/low_dim_gauss_mix.stan | 2.77 | 2.61 | 1.06 | 5.94% faster |
| arK/arK.stan | 1.86 | 1.71 | 1.09 | 8.18% faster |
| gp_pois_regr/gp_pois_regr.stan | 2.88 | 2.7 | 1.07 | 6.28% faster |
| low_dim_gauss_mix_collapse/low_dim_gauss_mix_collapse.stan | 8.86 | 8.32 | 1.07 | 6.12% faster |
| performance.compilation | 181.3 | 180.77 | 1.0 | 0.29% faster |
Mean result: 1.0614836204512803
- - - - - - - - - - - - - - - - - - - - -
[Jenkins Console Log](https://jenkins.flatironinstitute.org/job/Stan/job/Stan/view/change-requests/job/PR-3342/8/consoleFull)
[Blue Ocean](https://jenkins.flatironinstitute.org/blue/organizations/jenkins/Stan%2FStan/detail/PR-3342/8/pipeline)
Commit hash: 3d0d6c996961fb2dba4e1e71aceeaf0a35d746c7
- - - - - - - - - - - - - - - - - - - - -
<details><summary>Machine information</summary>
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 20.04.3 LTS
Release: 20.04
Codename: focal

CPU: 
Architecture:                         x86_64
CPU op-mode(s):                       32-bit, 64-bit
Byte Order:                           Little Endian
Address sizes:                        46 bits physical, 48 bits virtual
CPU(s):                               80
On-line CPU(s) list:                  0-79
Thread(s) per core:                   2
Core(s) per socket:                   20
Socket(s):                            2
NUMA node(s):                         2
Vendor ID:                            GenuineIntel
CPU family:                           6
Model:                                85
Model name:                           Intel(R) Xeon(R) Gold 6148 CPU @ 2.40GHz
Stepping:                             4
CPU MHz:                              2400.000
CPU max MHz:                          3700.0000
CPU min MHz:                          1000.0000
BogoMIPS:                             4800.00
Virtualization:                       VT-x
L1d cache:                            1.3 MiB
L1i cache:                            1.3 MiB
L2 cache:                             40 MiB
L3 cache:                             55 MiB
NUMA node0 CPU(s):                    0,2,4,6,8,10,12,14,16,18,20,22,24,26,28,30,32,34,36,38,40,42,44,46,48,50,52,54,56,58,60,62,64,66,68,70,72,74,76,78
NUMA node1 CPU(s):                    1,3,5,7,9,11,13,15,17,19,21,23,25,27,29,31,33,35,37,39,41,43,45,47,49,51,53,55,57,59,61,63,65,67,69,71,73,75,77,79
Vulnerability Gather data sampling:   Mitigation; Microcode
Vulnerability Itlb multihit:          KVM: Mitigation: VMX disabled
Vulnerability L1tf:                   Mitigation; PTE Inversion; VMX conditional cache flushes, SMT vulnerable
Vulnerability Mds:                    Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Meltdown:               Mitigation; PTI
Vulnerability Mmio stale data:        Mitigation; Clear CPU buffers; SMT vulnerable
Vulnerability Reg file data sampling: Not affected
Vulnerability Retbleed:               Mitigation; IBRS
Vulnerability Spec rstack overflow:   Not affected
Vulnerability Spec store bypass:      Mitigation; Speculative Store Bypass disabled via prctl
Vulnerability Spectre v1:             Mitigation; usercopy/swapgs barriers and __user pointer sanitization
Vulnerability Spectre v2:             Mitigation; IBRS; IBPB conditional; STIBP conditional; RSB filling; PBRSB-eIBRS Not affected; BHI Not affected
Vulnerability Srbds:                  Not affected
Vulnerability Tsx async abort:        Mitigation; Clear CPU buffers; SMT vulnerable
Flags:                                fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc art arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc cpuid aperfmperf pni pclmulqdq dtes64 monitor ds_cpl vmx smx est tm2 ssse3 sdbg fma cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic movbe popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm abm 3dnowprefetch cpuid_fault epb cat_l3 cdp_l3 invpcid_single pti intel_ppin ssbd mba ibrs ibpb stibp tpr_shadow vnmi flexpriority ept vpid ept_ad fsgsbase tsc_adjust bmi1 hle avx2 smep bmi2 erms invpcid rtm cqm mpx rdt_a avx512f avx512dq rdseed adx smap clflushopt clwb intel_pt avx512cd avx512bw avx512vl xsaveopt xsavec xgetbv1 xsaves cqm_llc cqm_occup_llc cqm_mbm_total cqm_mbm_local dtherm ida arat pln pts hwp hwp_act_window hwp_epp hwp_pkg_req pku ospke md_clear flush_l1d arch_capabilities

G++: 
g++ (Ubuntu 9.4.0-1ubuntu1~20.04) 9.4.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Clang: 
clang version 10.0.0-4ubuntu1 
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

</details>
author_association: CONTRIBUTOR