Fixed-structure CHOLMOD with in-place `set_nonzeros!`

luke-kiernan
Mon, 21 Jul 2025 20:56:56 +0000
Performance

I have a large sparse matrix. Its nonzeros change over the course of the problem, but its structure doesn’t. I want to do repeated in-place Cholesky factorizations, re-using the symbolic factorization and modifying the numeric factorization in-place. I can do the first factorization just fine:

using SparseArrays
using LinearAlgebra
A = sprand(10, 10, 0.3)
B = A + A' + 10 * LinearAlgebra.I(10)
B_cholmod = SparseArrays.CHOLMOD.Sparse(B)
sym = SparseArrays.CHOLMOD.symbolic(B_cholmod)
SparseArrays.CHOLMOD.cholesky!(sym, B_cholmod)

However, Julia’s interface does not allow me to write to the array of nonzero values of B_cholmod. So I’m implementing my own wrapper. Here’s what I have:

using SparseArrays
using LinearAlgebra
const VTypes = SparseArrays.CHOLMOD.VRealTypes
const ITypes = SparseArrays.CHOLMOD.ITypes
struct FixedStructureCHOLMOD{T, I} <: AbstractSparseMatrix{T, I}
    _mat::SparseArrays.CHOLMOD.Sparse{T, I}
    _values::Vector{T}
end

function FixedStructureCHOLMOD(A::SparseMatrixCSC{T, I}) where {T<:VTypes, I<:ITypes}
    cholmod_mat = SparseArrays.CHOLMOD.Sparse(A, -1) # -1 for hermitian
    chol = unsafe_load(cholmod_mat.ptr)
    x_ptr = chol.x
    # max capacity may not be the same as the number of nonzeros:
    # instead of chol.nzmax, we want chol.p[chol.ncol], in C indexing.
    # (what about packed matrices, though?)
    nonzero_count = unsafe_load(Ptr{I}(chol.p), chol.ncol + 1)
    values = unsafe_wrap(Vector{T}, Ptr{T}(x_ptr), nonzero_count)
    return FixedStructureCHOLMOD(cholmod_mat, values)
end


"""
    set_values!(mat::FixedStructureCHOLMOD, new_vals::AbstractVector{Float64})
In-place update of the numeric values in the CHOLMOD matrix.
"""
function set_values!(mat::FixedStructureCHOLMOD{T, I}, new_vals::AbstractVector{T}) where {T<:VTypes, I<:ITypes}
    @assert length(new_vals) == length(mat._values) "New values must be same length as allocated storage!"
    copyto!(mat._values, new_vals)
    return mat
end

function symbolic_factor(mat::FixedStructureCHOLMOD{T, I}) where {T<:VTypes, I<:ITypes}
    return SparseArrays.CHOLMOD.symbolic(mat._mat)
end

function numeric_factor!(F::SparseArrays.CHOLMOD.Factor{T, I}, mat::FixedStructureCHOLMOD{T, I}) where {T<:VTypes, I<:ITypes}
    SparseArrays.CHOLMOD.factorize!(mat._mat, F)
    return
end

My question: should this struct be mutable, with a proper finalizer, or is it memory-safe as is? edit: I’m asking this in the context described above. So constant sparse structure, one call to symbolic_factor, then many calls to numeric_refactor!.

Useful links: SuiteSparse’s cholmod.h, CHOLMOD’s User Guide, and Julia’s cholmod.jl.
Sample test code (click for more details)
4 posts - 2 participants