避免使用 Eigen 分解稀疏矩阵时的动态内存分配

Posted

技术标签:

【中文标题】避免使用 Eigen 分解稀疏矩阵时的动态内存分配【英文标题】:Avoiding dynamic memory allocation on factorizing sparse matrix with Eigen 【发布时间】:2020-07-17 12:28:15 【问题描述】:

在我的应用程序中,除了类构造函数之外,我需要避免动态内存分配(类似 malloc)。 我有一个稀疏的半定矩阵 M,它的元素在程序执行期间会发生变化,但它具有固定的稀疏模式。

为了尽可能快地求解许多线性系统 M * x = b,想法是在我的类构造函数中使用 inplace 分解,如Inplace matrix decompositions 中所述,然后在任何时候调用 factorize 方法M 变化:

struct MyClass 
private:
    SparseMatrix<double> As_;
    SimplicialLDLT<Ref<SparseMatrix<double>>> solver_;
public:
    /** Constructor */
    MyClass( const SparseMatrix<double> &As ) 
        : As_( As )
        , solver_( As_ ) // Inplace decomposition
    

    void assign( const SparseMatrix<double> &As_new ) 
        // Here As_new has the same sparsity pattern of As_
        solver_.factorize( As_new );
    

    void solve( const VectorXd &b, VectorXd &x )
    
        x = solver_.solve( b );
    

然而,factorize 方法仍然会创建一个与 As_ 大小相同的临时文件,因此使用动态内存分配。

是否有可能以某种方式避免它?如果 Eigen API 不允许此功能,一个想法是创建 SimplicialLDLT 的派生类,以便仅在将在类构造函数中调用的 analyzePattern 方法中执行动态内存分配。欢迎提出建议...

【问题讨论】:

恐怕如果不对稀疏求解器逻辑进行几次更改,这是不可能的(最重要的是,您需要一种方法来在多个步骤中继续为临时对象重用内存)。不过,我必须再次检查来源以获得明确的答案。 如果有机会我可以在派生类中实现此功能。我认为它对于嵌入式和实时应用程序非常有用,在这些应用程序中,动态内存分配通常只允许在启动阶段进行。 另一种可能的解决方案是将自定义分配器添加到求解器构造函数。应该动态分配的临时矩阵和向量从分配器中检索内存。实时系统的分配器可以由固定大小的内存池表示。这种策略在其他情况下也很有用。 分析 SimplicialCholesky.h 可以避免使用 Upper 三角形部分和 NaturalOrdering 进行动态分配。准确地说,第一次调用 factorize 方法仍然是动态分配内存,但可以在 ctor 类中完成。关于排序,可以手动调用 ordering 方法(在 ctor 中),如link 所述,注意将排列应用于 rhs 和解决方案向量。唯一值得关注的是文档中的指示此模块目前仅供内部使用 这个想法部分有效:使用 NaturalOrdering 防止因式分解方法分配内存。另一方面,我需要在运行 factorize 方法之前手动置换输入矩阵,即:H.selfadjointView&lt;Upper&gt;() = As_new.selfadjointView&lt;Upper&gt;().twistedBy(P); 它会创建临时对象。有什么建议可以避免吗? 【参考方案1】:

最后我找到了使用 CSparse 库获取 H = P * A * P' 的解决方法:

class SparseLDLTLinearSolver 
private:
    /** Ordering algorithm */
    AMDOrdering<int> ordering_;
    /** Ordering P matrix */
    PermutationMatrix<Dynamic, Dynamic, int> P_;
    /** Inverse of P matrix */
    PermutationMatrix<Dynamic, Dynamic, int> P_inv_;
    /** Permuted matrix H = P * A * P' */
    SparseMatrix<double> H_;
    /** H matrix CSparse structure */
    cs H_cs_;
    /** Support vector for solve */
    VectorXd y_;
    /** Support permutation vector */
    VectorXi w_;
    /** LDLT sparse linear solver without ordering */
    SimplicialLDLT<SparseMatrix<double>, Upper, NaturalOrdering<int>> solver_;
public:
    int SparseLDLTLinearSolver( const SparseMatrix<double> &A )
        : P_( A.rows() )
        , P_inv_( A.rows() )
        , H_( A.rows(), A.rows() )
        , y_( A.rows() )
        , w_( A.rows() )
    
        assert( ( A.rows() == A.cols() ) && "Invalid matrix" );
        ordering_( A.selfadjointView<Upper>(), P_inv_ );
        P_ = P_inv_.inverse();
        H_ = A.triangularView<Upper>();
        H_.makeCompressed();
        // Fill CSparse structure
        H_cs_.nzmax = H_.nonZeros();
        H_cs_.m = H_.rows();
        H_cs_.n = H_.cols();
        H_cs_.p = H_.outerIndexPtr();
        H_cs_.i = H_.innerIndexPtr();
        H_cs_.x = H_.valuePtr();
        H_cs_.nz = -1;
        const cs_sparse A_cs
            A.nonZeros(), A.rows(), A.cols(),
            const_cast<int*>( A.outerIndexPtr() ),
            const_cast<int*>( A.innerIndexPtr() ),
            const_cast<double*>( A.valuePtr() ),
            -1 ;
        cs_symperm_noalloc( &A_cs, P_.indices().data(), &H_cs_, w_.data() );
        solver_.analyzePattern( H_ );
        // Factorize in order to allocate internal data and avoid it on next factorization
        solver_.factorize( H_ );
        /*.*/
        return -solver_.info();
    

    int factorize( const Eigen::SparseMatrix<double> &A )
    
        assert( ( A.rows() == P_.size() ) && ( A.cols() == P_.size() ) &&
            "Invalid matrix size" );
        // Fill CSparse structure
        const cs_sparse A_cs 
            A.nonZeros(), A.rows(), A.cols(),
            const_cast<int*>( A.outerIndexPtr() ), 
            const_cast<int*>( A.innerIndexPtr() ), 
            const_cast<double*>( A.valuePtr() ), 
            -1 ;
        cs_symperm_noalloc( &A_cs, P_.indices().data(), &H_cs_, w_.data() );
        solver_.factorize( H_ );
        /*.*/
        return -solver_.info();
    

    void solve( const VectorXd &rhs, VectorXd &x )
    
        assert( ( rhs.size() == P_.size() ) && ( x.size() == P_.size() ) &&
            "Invalid vector size" );
        // Solve (P * A * P') * y = P * b, then return x = P' * y
        y_ = solver_.solve( P_ * rhs );
        x.noalias() = P_inv_ * y_;
    
;

cs_symperm_noalloc 是对 CSparse 库的 cs_symperm 函数的小重构。

它似乎有效,至少在我的特殊问题上。如果 Eigen 避免为一些稀疏矩阵运算创建临时变量(到堆中),那么在未来将非常有用。

【讨论】:

以上是关于避免使用 Eigen 分解稀疏矩阵时的动态内存分配的主要内容,如果未能解决你的问题,请参考以下文章

使用 Eigen3 的稀疏矩阵的特征值

大型稀疏矩阵分解

Eigen - 将每个(稀疏)矩阵行除以其对应的对角线元素

SimplicialLLT 返回错误的cholesky 因子

用于文档分类的 scipy/sklearn 稀疏矩阵分解

c++大特征分解速度