Skip to content

Commit ce9fe78

Browse files
Add workspace to Factor for allocation-free CHOLMOD solves (#727)
1 parent 1e27ff9 commit ce9fe78

2 files changed

Lines changed: 127 additions & 70 deletions

File tree

src/solvers/cholmod.jl

Lines changed: 77 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -258,8 +258,16 @@ function Sparse(p::Ptr{cholmod_sparse})
258258
Sparse{jlxtype(s.xtype, s.dtype)}(p)
259259
end
260260

261+
# Factor stores its own temporary CHOLMOD Y/E buffers for use in ldiv!
262+
# and pre-allocates cholmod_dense_struct wrappers
261263
mutable struct Factor{Tv<:VTypes, Ti<:ITypes} <: Factorization{Tv}
262264
ptr::Ptr{cholmod_factor}
265+
dense_x::cholmod_dense_struct
266+
dense_b::cholmod_dense_struct
267+
X::Ref{Ptr{cholmod_dense_struct}}
268+
Y::Ref{Ptr{cholmod_dense_struct}}
269+
E::Ref{Ptr{cholmod_dense_struct}}
270+
lock::ReentrantLock
263271
function Factor{Tv, Ti}(ptr::Ptr{cholmod_factor}, register_finalizer = true) where {Tv, Ti}
264272
if ptr == C_NULL
265273
throw(ArgumentError("factorization construction failed for " *
@@ -276,9 +284,15 @@ mutable struct Factor{Tv<:VTypes, Ti<:ITypes} <: Factorization{Tv}
276284
free!(ptr, Ti)
277285
throw(CHOLMODException("dtype=$(dtyp(Tv)) not supported"))
278286
end
279-
F = new(ptr)
287+
F = new(ptr,
288+
cholmod_dense_struct(),
289+
cholmod_dense_struct(),
290+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
291+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
292+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
293+
ReentrantLock())
280294
if register_finalizer
281-
finalizer(free!, F)
295+
finalizer(free!, F) # includes Y/E buffers
282296
end
283297
return F
284298
end
@@ -857,32 +871,6 @@ function Base.convert(::Type{Dense{Tnew}}, A::Dense{T}) where {Tnew, T}
857871
end
858872
Base.convert(::Type{Dense{T}}, A::Dense{T}) where T = A
859873

860-
# Just calling Dense(x) or Dense(b) will allocate new
861-
# `cholmod_dense_struct`s in CHOLMOD. Instead, we want to reuse
862-
# the existing memory. We can do this by creating new
863-
# `cholmod_dense_struct`s and filling them manually.
864-
function wrap_dense_and_ptr(x::StridedVecOrMat{T}) where {T <: VTypes}
865-
dense_x = cholmod_dense_struct()
866-
dense_x.nrow = size(x, 1)
867-
dense_x.ncol = size(x, 2)
868-
dense_x.nzmax = length(x)
869-
dense_x.d = stride(x, 2)
870-
dense_x.x = pointer(x)
871-
dense_x.z = C_NULL
872-
dense_x.xtype = xtyp(eltype(x))
873-
dense_x.dtype = dtyp(eltype(x))
874-
return dense_x, pointer_from_objref(dense_x)
875-
end
876-
# We need to use a special handling for the case of `Dense`
877-
# input arrays since the `pointer` refers to the pointer to the
878-
# `cholmod_dense`, not to the array values themselves as for
879-
# standard arrays.
880-
function wrap_dense_and_ptr(x::Dense{T}) where {T <: VTypes}
881-
dense_x_ptr = x.ptr
882-
dense_x = unsafe_load(dense_x_ptr)
883-
return dense_x, pointer_from_objref(dense_x)
884-
end
885-
886874
# This constructor assumes zero based colptr and rowval
887875
function Sparse(m::Integer, n::Integer,
888876
colptr0::Vector{Ti}, rowval0::Vector{Ti},
@@ -1240,7 +1228,11 @@ end
12401228

12411229
free!(A::Dense) = free!(pointer(A))
12421230
free!(A::Sparse{<:Any, Ti}) where Ti = free!(pointer(A), Ti)
1243-
free!(F::Factor{<:Any, Ti}) where Ti = free!(pointer(F), Ti)
1231+
function free!(F::Factor{<:Any, Ti}) where Ti
1232+
free!(getfield(F, :Y)[])
1233+
free!(getfield(F, :E)[])
1234+
free!(pointer(F), Ti)
1235+
end
12441236

12451237
nnz(F::Factor) = nnz(Sparse(F))
12461238

@@ -1325,8 +1317,8 @@ end
13251317
@inline function getproperty(F::Factor, sym::Symbol)
13261318
if sym === :p
13271319
return get_perm(F)
1328-
elseif sym === :ptr
1329-
return getfield(F, :ptr)
1320+
elseif sym === :ptr || sym === :lock
1321+
return getfield(F, sym)
13301322
else
13311323
return FactorComponent(F, sym)
13321324
end
@@ -1433,11 +1425,11 @@ end
14331425

14341426
function cholesky!(F::Factor{Tv}, A::Sparse{Tv};
14351427
shift::Real=0.0, check::Bool = true) where Tv
1436-
# Compute the numerical factorization
1437-
@cholmod_param final_ll = true begin
1438-
factorize_p!(A, shift, F)
1428+
@lock F.lock begin
1429+
@cholmod_param final_ll = true begin
1430+
factorize_p!(A, shift, F)
1431+
end
14391432
end
1440-
14411433
check && (issuccess(F) || throw(LinearAlgebra.PosDefException(1)))
14421434
return F
14431435
end
@@ -1608,12 +1600,10 @@ LinearAlgebra._cholesky(A::Union{SparseMatrixCSC{T}, SparseMatrixCSC{Complex{T}}
16081600

16091601
function ldlt!(F::Factor{Tv}, A::Sparse{Tv};
16101602
shift::Real=0.0, check::Bool = true) where Tv
1611-
# Makes it an LDLt
1612-
change_factor!(F, false, false, true, false)
1613-
1614-
# Compute the numerical factorization
1615-
factorize_p!(A, shift, F)
1616-
1603+
@lock F.lock begin
1604+
change_factor!(F, false, false, true, false)
1605+
factorize_p!(A, shift, F)
1606+
end
16171607
check && (issuccess(F) || throw(LinearAlgebra.ZeroPivotException(1)))
16181608
return F
16191609
end
@@ -1915,8 +1905,52 @@ const AbstractSparseVecOrMatInclAdjAndTrans = Union{AbstractSparseVecOrMat, AdjO
19151905
throw(ArgumentError("self-adjoint sparse system solve not implemented for sparse rhs B," *
19161906
" consider to convert B to a dense array"))
19171907

1918-
# in-place ldiv!
1908+
# Julia array -> fill dense_b fields and return pointer to it
1909+
# CHOLMOD Dense object -> struct is already set up by CHOLMOD, return b.ptr directly
1910+
@inline function _setup_bptr(b::StridedVecOrMat{T}, dense_b::cholmod_dense_struct) where T<:VTypes
1911+
dense_b.nrow = size(b, 1)
1912+
dense_b.ncol = size(b, 2)
1913+
dense_b.nzmax = length(b)
1914+
dense_b.d = stride(b, 2)
1915+
dense_b.x = pointer(b)
1916+
dense_b.z = C_NULL
1917+
dense_b.xtype = xtyp(T)
1918+
dense_b.dtype = dtyp(T)
1919+
return Ptr{cholmod_dense_struct}(pointer_from_objref(dense_b))
1920+
end
1921+
@inline _setup_bptr(b::Dense{<:VTypes}, ::cholmod_dense_struct) = b.ptr
1922+
19191923
for TI in IndexTypes
1924+
@eval function solve!(x::StridedVecOrMat{T}, L::Factor{T, $TI}, b::StridedVecOrMat{T}) where {T<:VTypes}
1925+
@lock L.lock begin
1926+
dense_x = getfield(L, :dense_x)
1927+
X = getfield(L, :X)
1928+
Y = getfield(L, :Y)
1929+
E = getfield(L, :E)
1930+
1931+
dense_x.nrow = size(x, 1)
1932+
dense_x.ncol = size(x, 2)
1933+
dense_x.nzmax = length(x)
1934+
dense_x.d = stride(x, 2)
1935+
dense_x.x = pointer(x)
1936+
dense_x.z = C_NULL
1937+
dense_x.xtype = xtyp(T)
1938+
dense_x.dtype = dtyp(T)
1939+
1940+
X[] = Ptr{cholmod_dense_struct}(pointer_from_objref(dense_x))
1941+
Bptr = _setup_bptr(b, getfield(L, :dense_b))
1942+
status = GC.@preserve x b L begin
1943+
$(cholname(:solve2, TI))(
1944+
CHOLMOD_A, L,
1945+
Bptr, C_NULL,
1946+
X, C_NULL,
1947+
Y, E,
1948+
getcommon($TI))
1949+
end
1950+
@assert !iszero(status)
1951+
end
1952+
end
1953+
19201954
@eval function ldiv!(x::StridedVecOrMat{T},
19211955
L::Factor{T, $TI},
19221956
b::StridedVecOrMat{T}) where {T<:VTypes}
@@ -1943,34 +1977,7 @@ for TI in IndexTypes
19431977
throw(LinearAlgebra.ZeroPivotException(s.minor))
19441978
end
19451979
end
1946-
1947-
# Just calling Dense(x) or Dense(b) will allocate new
1948-
# `cholmod_dense_struct`s in CHOLMOD. Instead, we want to reuse
1949-
# the existing memory. We can do this by creating new
1950-
# `cholmod_dense_struct`s and filling them manually.
1951-
dense_x, dense_x_ptr = wrap_dense_and_ptr(x)
1952-
dense_b, dense_b_ptr = wrap_dense_and_ptr(b)
1953-
1954-
X_Handle = Ptr{cholmod_dense_struct}(dense_x_ptr)
1955-
Y_Handle = Ref(Ptr{cholmod_dense_struct}(C_NULL))
1956-
E_Handle = Ref(Ptr{cholmod_dense_struct}(C_NULL))
1957-
status = GC.@preserve x dense_x b dense_b begin
1958-
$(cholname(:solve2, TI))(
1959-
CHOLMOD_A, L,
1960-
Ref(dense_b), C_NULL,
1961-
Ref(X_Handle), C_NULL,
1962-
Y_Handle,
1963-
E_Handle,
1964-
getcommon($TI))
1965-
end
1966-
if Y_Handle[] != C_NULL
1967-
free!(Y_Handle[])
1968-
end
1969-
if E_Handle[] != C_NULL
1970-
free!(E_Handle[])
1971-
end
1972-
@assert !iszero(status)
1973-
1980+
solve!(x, L, b)
19741981
return x
19751982
end
19761983
end

test/cholmod.jl

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,15 @@ end
303303
@inferred ldiv!(X2, factor, B)
304304
@test X2 X
305305

306+
# reuse across multiple calls (Y/E buffers kept in workspace)
307+
fill!(x2, 0)
308+
ldiv!(x2, factor, b)
309+
@test x2 x
310+
311+
# Y/E buffers are reused across calls, remaining 16 bytes come from CHOLMOD internals
312+
allocs = @allocated ldiv!(x2, factor, b)
313+
@test allocs <= 16
314+
306315
c = fill(Tv(1), size(x, 1) + 1)
307316
C = fill(Tv(1), size(X, 1) + 1, size(X, 2))
308317
y = fill(Tv(1), size(x, 1) + 1)
@@ -315,6 +324,47 @@ end
315324
@test_throws DimensionMismatch ldiv!(x2, factor, B)
316325
end
317326

327+
@testset "ldiv! no memory leak $Tv $Ti" begin
328+
local A, b, x, F
329+
A = sprand(10, 10, 0.1)
330+
A = I + A * A'
331+
A = convert(SparseMatrixCSC{Tv,Ti}, A)
332+
F = cholesky(A)
333+
b = A * fill(Tv(1), 10)
334+
x = zero(b)
335+
336+
ldiv!(x, F, b) # allocate buffers
337+
GC.gc()
338+
before = getcommon(Ti)[].memory_inuse
339+
for _ in 1:1000
340+
ldiv!(x, F, b)
341+
end
342+
after = getcommon(Ti)[].memory_inuse
343+
@test before == after
344+
end
345+
346+
@testset "copy(Factor) buffer isolation $Tv $Ti" begin
347+
local A, x, b, x2, x3
348+
A = sprand(10, 10, 0.1)
349+
A = I + A * A'
350+
A = convert(SparseMatrixCSC{Tv,Ti}, A)
351+
factor = cholesky(A)
352+
factor2 = copy(factor)
353+
354+
x = fill(Tv(1), 10)
355+
b = A * x
356+
x2 = zero(x)
357+
x3 = zero(x)
358+
359+
ldiv!(x2, factor, b)
360+
ldiv!(x3, factor2, b)
361+
@test x2 x
362+
@test x3 x
363+
364+
# Verify each copy has its own independent buffers
365+
@test getfield(factor, :Y) !== getfield(factor2, :Y)
366+
end
367+
318368
end #end for Ti ∈ itypes
319369

320370
for Tv (Float32, Float64)

0 commit comments

Comments
 (0)