Eigen向量与矩阵类能否直接用于RcppParallel Worker?并行方案咨询
我正在拟合一个统计模型,其似然函数与梯度评估依赖大量复杂矩阵运算,Eigen库对此适配性极佳。基于Rcpp开发,使用RcppEigen已取得良好效果。模型的似然/梯度计算包含一个理论上可轻松并行化的数据规模循环,希望通过RcppParallel实现并行化。
RcppParallel要求输入为Rcpp::NumericVector或Rcpp::NumericMatrix,我可通过相关方法实现Eigen::MatrixXd与Rcpp::NumericMatrix的转换,但转换带来的性能损耗过大,导致并行计算失去价值。
核心问题
- RcppParallel Worker能否直接接收/操作
Eigen::MatrixXd? - 能否推荐其他并行化包含大量Eigen专属矩阵代数循环的方法?
补充信息
- 完全重写程序以弃用Eigen是极不可取的。
- 本地设备为M1 Mac,也可使用Ubuntu服务器环境。
RcppParallel文档明确建议:「并行Worker内的代码不应以任何方式调用R或Rcpp API」。当前已实现的适配Eigen类的修改版本可编译运行并得到正确结果,但性能损耗显著,期望无需转换即可在并行计算中直接输入输出Eigen类对象,可使用RcppParallel或其他并行方法。
#include <Rcpp.h> #include <RcppEigen.h> // [[Rcpp::depends(RcppEigen)]] using namespace Rcpp; // [[Rcpp::depends(RcppParallel)]] #include <RcppParallel.h> using namespace RcppParallel; #include <math.h> // Square root all the elements in a big matrix struct SquareRoot : public Worker { const RMatrix<double> input; RMatrix<double> output; // Constructor SquareRoot(const NumericMatrix input, NumericMatrix output) : input(input), output(output) {} // Call operator void operator()(std::size_t begin,std::size_t end) { std::transform(input.begin()+begin, input.begin()+end, output.begin()+begin, (double(*)(double)) sqrt); } }; // [[Rcpp::export]] NumericMatrix parallelMatrixSqrt(NumericMatrix x) { NumericMatrix output(x.nrow(),x.ncol()); SquareRoot mysqrt(x,output); parallelFor(0,x.length(),mysqrt,100); return output; } // Try it with an Eigen input and output, compare performance // [[Rcpp::export]] Eigen::MatrixXd parallelMatrixSqrtEigen(Eigen::MatrixXd x) { // Convert to NumericMatrix SEXP s = wrap(x); NumericMatrix output(x.rows(),x.cols()); NumericMatrix xR(s); // Do the parallel computations SquareRoot mysqrt(xR,output); parallelFor(0,xR.length(),mysqrt,100); // Convert back Eigen::MatrixXd s2 = as<Eigen::MatrixXd>(output); return s2; }
codepath <- "~/work/projects/misc/rcppparallel" codefile <- "try-rcppparallel.cpp" library(Rcpp) library(RcppParallel) sourceCpp(file.path(codepath,codefile)) n <- 1000 mm <- matrix(rnorm(n*n)^2,n,n) microbenchmark::microbenchmark( sqrt(mm), parallelMatrixSqrt(mm), parallelMatrixSqrtEigen(mm) )
Unit: microseconds expr min lq mean median uq max neval cld sqrt(mm) 1179.242 1294.6775 1692.3123 1384.7955 1464.213 3581.924 100 b parallelMatrixSqrt(mm) 321.973 496.3665 909.3078 571.0685 784.617 3582.785 100 a parallelMatrixSqrtEigen(mm) 3223.133 3504.1675 4422.4277 4071.7715 5313.190 6737.735 100 c
问题1:RcppParallel Worker能否直接操作Eigen::MatrixXd?
可以,无需完整转换矩阵类型。关键是利用Eigen的内存布局兼容性:Eigen::MatrixXd默认使用列优先存储,和Rcpp::NumericMatrix、RcppParallel::RMatrix<double>的内存布局一致,因此可以直接通过原始指针构建Eigen视图,避免数据拷贝。
修改Worker结构,直接接收原始数据指针和矩阵维度,在Worker内部构建Eigen::Map<MatrixXd>来操作数据:
#include <Rcpp.h> #include <RcppEigen.h> // [[Rcpp::depends(RcppEigen)]] // [[Rcpp::depends(RcppParallel)]] #include <RcppParallel.h> using namespace RcppParallel; using Eigen::Map; using Eigen::MatrixXd; struct EigenSquareRoot : public Worker { // 原始数据指针与矩阵维度 const double* input_ptr; double* output_ptr; const int rows; const int cols; // 构造函数 EigenSquareRoot(const double* input, double* output, int r, int c) : input_ptr(input), output_ptr(output), rows(r), cols(c) {} void operator()(std::size_t begin, std::size_t end) { // 构建Eigen映射,直接操作原始内存 Map<const MatrixXd> input(input_ptr, rows, cols); Map<MatrixXd> output(output_ptr, rows, cols); // 按元素并行处理(这里示例是开平方,实际可替换为你的矩阵运算) for (std::size_t i = begin; i < end; ++i) { int row = i / cols; int col = i % cols; output(row, col) = sqrt(input(row, col)); } } }; // [[Rcpp::export]] MatrixXd parallelEigenMatrixSqrt(MatrixXd x) { MatrixXd output(x.rows(), x.cols()); EigenSquareRoot worker(x.data(), output.data(), x.rows(), x.cols()); parallelFor(0, x.size(), worker, 100); return output; }
这种方式完全避免了Eigen::MatrixXd与Rcpp::NumericMatrix之间的数据拷贝,性能和纯RcppParallel版本几乎一致。
问题2:其他并行化方案推荐
1. Eigen内置并行化
Eigen本身支持多线程并行,只需在编译时开启对应选项,针对矩阵运算(如矩阵乘法、LU分解等)自动并行。你可以在代码开头添加:
#define EIGEN_USE_OPENMP #include <Eigen/Core>
然后在编译时通过// [[Rcpp::plugins(openmp)]]开启OpenMP支持。这种方案无需修改现有Eigen代码,适合矩阵运算本身可并行的场景。
2. OpenMP直接并行
如果你的循环是显式的数据规模循环,直接用OpenMP的#pragma omp parallel for指令,配合Eigen矩阵操作。注意确保循环内的Eigen操作线程安全(Eigen的Map和普通矩阵操作是线程安全的,只要不同线程操作不同内存区域):
// [[Rcpp::export]] // [[Rcpp::plugins(openmp)]] MatrixXd openmpEigenSqrt(MatrixXd x) { MatrixXd output(x.rows(), x.cols()); int n = x.size(); #pragma omp parallel for for (int i = 0; i < n; ++i) { output.data()[i] = sqrt(x.data()[i]); } return output; }
这种方式代码更简洁,适合简单的并行循环,M1 Mac和Ubuntu都支持OpenMP(M1需安装支持arm64的OpenMP库,如通过brew安装libomp)。
3. TBB后端
RcppParallel底层依赖TBB,如果你需要更灵活的并行任务调度,也可以直接使用TBB库配合Eigen,通过Eigen::setNbThreads()设置线程数,或者手动编写TBB任务。
修改后的parallelEigenMatrixSqrt性能会接近parallelMatrixSqrt,远优于带转换的版本。你可以用原有的microbenchmark代码测试对比。
内容的提问来源于stack exchange,提问作者astring

