Skip to content

Commit d1b785c

Browse files
Add workspace to Factor to keep 3-arg API. Redesign CholmodWorkspace similar to UmfpackWS inside UmfpackLU
1 parent d20ab33 commit d1b785c

2 files changed

Lines changed: 113 additions & 205 deletions

File tree

src/solvers/cholmod.jl

Lines changed: 95 additions & 176 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import LinearAlgebra: (\), AdjointFactorization,
2525
using SparseArrays
2626
using SparseArrays: getcolptr, AbstractSparseVecOrMat
2727
export
28-
CholmodWorkspace,
2928
Dense,
3029
Factor,
3130
Sparse
@@ -259,8 +258,50 @@ function Sparse(p::Ptr{cholmod_sparse})
259258
Sparse{jlxtype(s.xtype, s.dtype)}(p)
260259
end
261260

261+
"""
262+
CholmodWorkspace()
263+
264+
Reusable workspace for allocation-free CHOLMOD solves.
265+
266+
`cholmod_solve2` allocates two temporary dense matrices (Y and E) on every solve.
267+
A `CholmodWorkspace` holds those buffers across calls so that CHOLMOD reuses them
268+
instead of reallocating. It also pre-allocates the `cholmod_dense_struct` wrappers
269+
for `x` and `b`, eliminating all Julia-side heap allocations per solve.
270+
271+
Each [`Factor`](@ref) embeds its own workspace and uses it automatically in
272+
`ldiv!(x, F, b)`. For multi-thread solves with a single factor, use `copy(F)`
273+
for every thread so each copy gets its own independent workspace and lock.
274+
275+
The workspace is finalizer-protected. The Y and E buffers are freed automatically
276+
when the workspace is garbage-collected.
277+
"""
278+
mutable struct CholmodWorkspace
279+
dense_x::cholmod_dense_struct
280+
dense_b::cholmod_dense_struct
281+
X::Ref{Ptr{cholmod_dense_struct}}
282+
Y::Ref{Ptr{cholmod_dense_struct}}
283+
E::Ref{Ptr{cholmod_dense_struct}}
284+
285+
function CholmodWorkspace()
286+
ws = new(
287+
cholmod_dense_struct(), # all fields will be written in ldiv! before use
288+
cholmod_dense_struct(),
289+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
290+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
291+
Ref(Ptr{cholmod_dense_struct}(C_NULL))
292+
)
293+
finalizer(ws) do w
294+
w.Y[] != C_NULL && free!(w.Y[])
295+
w.E[] != C_NULL && free!(w.E[])
296+
end
297+
ws
298+
end
299+
end
300+
262301
mutable struct Factor{Tv<:VTypes, Ti<:ITypes} <: Factorization{Tv}
263302
ptr::Ptr{cholmod_factor}
303+
workspace::CholmodWorkspace
304+
lock::ReentrantLock
264305
function Factor{Tv, Ti}(ptr::Ptr{cholmod_factor}, register_finalizer = true) where {Tv, Ti}
265306
if ptr == C_NULL
266307
throw(ArgumentError("factorization construction failed for " *
@@ -277,7 +318,7 @@ mutable struct Factor{Tv<:VTypes, Ti<:ITypes} <: Factorization{Tv}
277318
free!(ptr, Ti)
278319
throw(CHOLMODException("dtype=$(dtyp(Tv)) not supported"))
279320
end
280-
F = new(ptr)
321+
F = new(ptr, CholmodWorkspace(), ReentrantLock())
281322
if register_finalizer
282323
finalizer(free!, F)
283324
end
@@ -312,6 +353,8 @@ Base.pointer(x::Dense{Tv}) where {Tv} = Base.unsafe_convert(Ptr{cholmod_dense},
312353
Base.pointer(x::Sparse{Tv}) where {Tv} = Base.unsafe_convert(Ptr{cholmod_sparse}, x)
313354
Base.pointer(x::Factor{Tv}) where {Tv} = Base.unsafe_convert(Ptr{cholmod_factor}, x)
314355

356+
getworkspace(F::Factor) = F.workspace
357+
315358
function _factor_components(is_ll)
316359
is_ll ? (:L, :U, :PtL, :UP) : (:L, :U, :PtL, :UP, :D, :LD, :DU, :PtLD, :DUP)
317360
end
@@ -858,32 +901,6 @@ function Base.convert(::Type{Dense{Tnew}}, A::Dense{T}) where {Tnew, T}
858901
end
859902
Base.convert(::Type{Dense{T}}, A::Dense{T}) where T = A
860903

861-
# Just calling Dense(x) or Dense(b) will allocate new
862-
# `cholmod_dense_struct`s in CHOLMOD. Instead, we want to reuse
863-
# the existing memory. We can do this by creating new
864-
# `cholmod_dense_struct`s and filling them manually.
865-
function wrap_dense_and_ptr(x::StridedVecOrMat{T}) where {T <: VTypes}
866-
dense_x = cholmod_dense_struct()
867-
dense_x.nrow = size(x, 1)
868-
dense_x.ncol = size(x, 2)
869-
dense_x.nzmax = length(x)
870-
dense_x.d = stride(x, 2)
871-
dense_x.x = pointer(x)
872-
dense_x.z = C_NULL
873-
dense_x.xtype = xtyp(eltype(x))
874-
dense_x.dtype = dtyp(eltype(x))
875-
return dense_x, pointer_from_objref(dense_x)
876-
end
877-
# We need to use a special handling for the case of `Dense`
878-
# input arrays since the `pointer` refers to the pointer to the
879-
# `cholmod_dense`, not to the array values themselves as for
880-
# standard arrays.
881-
function wrap_dense_and_ptr(x::Dense{T}) where {T <: VTypes}
882-
dense_x_ptr = x.ptr
883-
dense_x = unsafe_load(dense_x_ptr)
884-
return dense_x, pointer_from_objref(dense_x)
885-
end
886-
887904
# This constructor assumes zero based colptr and rowval
888905
function Sparse(m::Integer, n::Integer,
889906
colptr0::Vector{Ti}, rowval0::Vector{Ti},
@@ -1326,8 +1343,8 @@ end
13261343
@inline function getproperty(F::Factor, sym::Symbol)
13271344
if sym === :p
13281345
return get_perm(F)
1329-
elseif sym === :ptr
1330-
return getfield(F, :ptr)
1346+
elseif sym === :ptr || sym === :workspace || sym === :lock # add workspace and lock
1347+
return getfield(F, sym)
13311348
else
13321349
return FactorComponent(F, sym)
13331350
end
@@ -1434,11 +1451,11 @@ end
14341451

14351452
function cholesky!(F::Factor{Tv}, A::Sparse{Tv};
14361453
shift::Real=0.0, check::Bool = true) where Tv
1437-
# Compute the numerical factorization
1438-
@cholmod_param final_ll = true begin
1439-
factorize_p!(A, shift, F)
1454+
@lock F.lock begin
1455+
@cholmod_param final_ll = true begin
1456+
factorize_p!(A, shift, F)
1457+
end
14401458
end
1441-
14421459
check && (issuccess(F) || throw(LinearAlgebra.PosDefException(1)))
14431460
return F
14441461
end
@@ -1609,12 +1626,10 @@ LinearAlgebra._cholesky(A::Union{SparseMatrixCSC{T}, SparseMatrixCSC{Complex{T}}
16091626

16101627
function ldlt!(F::Factor{Tv}, A::Sparse{Tv};
16111628
shift::Real=0.0, check::Bool = true) where Tv
1612-
# Makes it an LDLt
1613-
change_factor!(F, false, false, true, false)
1614-
1615-
# Compute the numerical factorization
1616-
factorize_p!(A, shift, F)
1617-
1629+
@lock F.lock begin
1630+
change_factor!(F, false, false, true, false)
1631+
factorize_p!(A, shift, F)
1632+
end
16181633
check && (issuccess(F) || throw(LinearAlgebra.ZeroPivotException(1)))
16191634
return F
16201635
end
@@ -1916,118 +1931,53 @@ const AbstractSparseVecOrMatInclAdjAndTrans = Union{AbstractSparseVecOrMat, AdjO
19161931
throw(ArgumentError("self-adjoint sparse system solve not implemented for sparse rhs B," *
19171932
" consider to convert B to a dense array"))
19181933

1919-
"""
1920-
CholmodWorkspace()
1921-
1922-
Reusable workspace for allocation-free calls to `ldiv!(x, L, b, ws)`.
1923-
1924-
`cholmod_solve2` allocates two temporary dense matrices (Y and E) on every solve.
1925-
A `CholmodWorkspace` holds those buffers across calls so that CHOLMOD reuses them
1926-
instead of reallocating. It also pre-allocates the `cholmod_dense_struct` wrappers
1927-
for `x` and `b`.
1928-
This eliminates all Julia-side heap allocations per solve.
1929-
1930-
A finalizer frees Y adn E automatically when the worspace is garbage-collected.
1931-
1932-
A single workspace can be reused across calls with different arrays or a different number of RHS columns since
1933-
CHOLMOD will resize Y and E.
1934-
1935-
# Example
1936-
```julia
1937-
F = cholesky(A)
1938-
ws = CholmodWorkspace()
1939-
for b in rhs_list
1940-
ldiv!(x, F, b, ws)
1941-
end
1942-
```
1943-
"""
1944-
1945-
mutable struct CholmodWorkspace
1946-
dense_x::cholmod_dense_struct
1947-
dense_b::cholmod_dense_struct
1948-
X::Ref{Ptr{cholmod_dense_struct}}
1949-
Y::Ref{Ptr{cholmod_dense_struct}}
1950-
E::Ref{Ptr{cholmod_dense_struct}}
1934+
# Julia array -> fill ws.dense_b fields and return pointer to it
1935+
# CHOLMOD Dense object -> struct is already set up by CHOLMOD, return b.ptr directly
1936+
@inline function _setup_bptr(b::StridedVecOrMat{T}, ws::CholmodWorkspace) where T<:VTypes
1937+
ws.dense_b.nrow = size(b, 1)
1938+
ws.dense_b.ncol = size(b, 2)
1939+
ws.dense_b.nzmax = length(b)
1940+
ws.dense_b.d = stride(b, 2)
1941+
ws.dense_b.x = pointer(b)
1942+
ws.dense_b.z = C_NULL
1943+
ws.dense_b.xtype = xtyp(T)
1944+
ws.dense_b.dtype = dtyp(T)
1945+
return Ptr{cholmod_dense_struct}(pointer_from_objref(ws.dense_b))
1946+
end
1947+
@inline _setup_bptr(b::Dense{<:VTypes}, ::CholmodWorkspace) = b.ptr
19511948

1952-
function CholmodWorkspace()
1953-
ws = new(
1954-
cholmod_dense_struct(), # all fields will be written in ldiv! before use
1955-
cholmod_dense_struct(),
1956-
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
1957-
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
1958-
Ref(Ptr{cholmod_dense_struct}(C_NULL))
1959-
)
1960-
finalizer(ws) do w
1961-
w.Y[] != C_NULL && free!(w.Y[])
1962-
w.E[] != C_NULL && free!(w.E[])
1963-
end
1964-
ws
1965-
end
1966-
end
1967-
1968-
# in-place ldiv! and optional allocation-free ldiv!
19691949
for TI in IndexTypes
1970-
@eval function ldiv!(x::StridedVecOrMat{T},
1971-
L::Factor{T, $TI},
1972-
b::StridedVecOrMat{T}) where {T<:VTypes}
1973-
if x === b
1974-
throw(ArgumentError("output array must not be aliased with input array"))
1975-
end
1976-
if size(L, 1) != size(b, 1)
1977-
throw(DimensionMismatch("Factorization and RHS should have the same number of rows. " *
1978-
"Factorization has $(size(L, 2)) rows, but RHS has $(size(b, 1)) rows."))
1979-
end
1980-
if size(L, 2) != size(x, 1)
1981-
throw(DimensionMismatch("Factorization and solution should match sizes. " *
1982-
"Factorization has $(size(L, 1)) columns, but solution has $(size(x, 1)) rows."))
1983-
end
1984-
if size(x, 2) != size(b, 2)
1985-
throw(DimensionMismatch("Solution and RHS should have the same number of columns. " *
1986-
"Solution has $(size(x, 2)) columns, but RHS has $(size(b, 2)) columns."))
1987-
end
1988-
if !issuccess(L)
1989-
s = unsafe_load(pointer(L))
1990-
if s.is_ll == 1
1991-
throw(LinearAlgebra.PosDefException(s.minor))
1992-
else
1993-
throw(LinearAlgebra.ZeroPivotException(s.minor))
1950+
@eval function solve!(x::StridedVecOrMat{T},
1951+
L::Factor{T, $TI},
1952+
b::StridedVecOrMat{T};
1953+
ws = getworkspace(L)) where {T<:VTypes}
1954+
@lock L.lock begin
1955+
ws.dense_x.nrow = size(x, 1)
1956+
ws.dense_x.ncol = size(x, 2)
1957+
ws.dense_x.nzmax = length(x)
1958+
ws.dense_x.d = stride(x, 2)
1959+
ws.dense_x.x = pointer(x)
1960+
ws.dense_x.z = C_NULL
1961+
ws.dense_x.xtype = xtyp(T)
1962+
ws.dense_x.dtype = dtyp(T)
1963+
1964+
ws.X[] = Ptr{cholmod_dense_struct}(pointer_from_objref(ws.dense_x))
1965+
Bptr = _setup_bptr(b, ws)
1966+
status = GC.@preserve x b ws begin
1967+
$(cholname(:solve2, TI))(
1968+
CHOLMOD_A, L,
1969+
Bptr, C_NULL,
1970+
ws.X, C_NULL,
1971+
ws.Y, ws.E,
1972+
getcommon($TI))
19941973
end
1974+
@assert !iszero(status)
19951975
end
1996-
1997-
# Just calling Dense(x) or Dense(b) will allocate new
1998-
# `cholmod_dense_struct`s in CHOLMOD. Instead, we want to reuse
1999-
# the existing memory. We can do this by creating new
2000-
# `cholmod_dense_struct`s and filling them manually.
2001-
dense_x, dense_x_ptr = wrap_dense_and_ptr(x)
2002-
dense_b, dense_b_ptr = wrap_dense_and_ptr(b)
2003-
2004-
X_Handle = Ptr{cholmod_dense_struct}(dense_x_ptr)
2005-
Y_Handle = Ptr{cholmod_dense_struct}(C_NULL)
2006-
E_Handle = Ptr{cholmod_dense_struct}(C_NULL)
2007-
status = GC.@preserve x dense_x b dense_b begin
2008-
$(cholname(:solve2, TI))(
2009-
CHOLMOD_A, L,
2010-
Ref(dense_b), C_NULL,
2011-
Ref(X_Handle), C_NULL,
2012-
Ref(Y_Handle),
2013-
Ref(E_Handle),
2014-
getcommon($TI))
2015-
end
2016-
if Y_Handle != C_NULL
2017-
free!(Y_Handle)
2018-
end
2019-
if E_Handle != C_NULL
2020-
free!(E_Handle)
2021-
end
2022-
@assert !iszero(status)
2023-
2024-
return x
20251976
end
20261977

20271978
@eval function ldiv!(x::StridedVecOrMat{T},
20281979
L::Factor{T, $TI},
2029-
b::StridedVecOrMat{T},
2030-
ws::CholmodWorkspace) where {T<:VTypes}
1980+
b::StridedVecOrMat{T}) where {T<:VTypes}
20311981
if x === b
20321982
throw(ArgumentError("output array must not be aliased with input array"))
20331983
end
@@ -2051,38 +2001,7 @@ for TI in IndexTypes
20512001
throw(LinearAlgebra.ZeroPivotException(s.minor))
20522002
end
20532003
end
2054-
2055-
# Update all cholmod_dense_structs fields on every call in case dimensions or T change
2056-
ws.dense_x.nrow = size(x, 1)
2057-
ws.dense_x.ncol = size(x, 2)
2058-
ws.dense_x.nzmax = length(x)
2059-
ws.dense_x.d = stride(x, 2)
2060-
ws.dense_x.x = pointer(x)
2061-
ws.dense_x.z = C_NULL
2062-
ws.dense_x.xtype = xtyp(T)
2063-
ws.dense_x.dtype = dtyp(T)
2064-
2065-
ws.dense_b.nrow = size(b, 1)
2066-
ws.dense_b.ncol = size(b, 2)
2067-
ws.dense_b.nzmax = length(b)
2068-
ws.dense_b.d = stride(b, 2)
2069-
ws.dense_b.x = pointer(b)
2070-
ws.dense_b.z = C_NULL
2071-
ws.dense_b.xtype = xtyp(T)
2072-
ws.dense_b.dtype = dtyp(T)
2073-
2074-
ws.X[] = Ptr{cholmod_dense_struct}(pointer_from_objref(ws.dense_x))
2075-
Bptr = Ptr{cholmod_dense_struct}(pointer_from_objref(ws.dense_b))
2076-
status = GC.@preserve x b ws begin
2077-
$(cholname(:solve2, TI))(
2078-
CHOLMOD_A, L,
2079-
Bptr, C_NULL,
2080-
ws.X, C_NULL,
2081-
ws.Y, ws.E,
2082-
getcommon($TI))
2083-
end
2084-
@assert !iszero(status)
2085-
2004+
solve!(x, L, b)
20862005
return x
20872006
end
20882007
end

0 commit comments

Comments
 (0)