Skip to content

Commit 004ed92

Browse files
Add CholmodWorkspace for allocation-free CHOLMOD solves. Pre-allocates cholmod_dense_struct wrappers and Y/E buffers so cholmod_solve2 reuses them across calls instead of reallocating
1 parent 7abbefa commit 004ed92

2 files changed

Lines changed: 155 additions & 1 deletion

File tree

src/solvers/cholmod.jl

Lines changed: 113 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import LinearAlgebra: (\), AdjointFactorization,
2525
using SparseArrays
2626
using SparseArrays: getcolptr, AbstractSparseVecOrMat
2727
export
28+
CholmodWorkspace,
2829
Dense,
2930
Factor,
3031
Sparse
@@ -1915,7 +1916,55 @@ const AbstractSparseVecOrMatInclAdjAndTrans = Union{AbstractSparseVecOrMat, AdjO
19151916
throw(ArgumentError("self-adjoint sparse system solve not implemented for sparse rhs B," *
19161917
" consider to convert B to a dense array"))
19171918

1918-
# in-place ldiv!
1919+
"""
1920+
CholmodWorkspace()
1921+
1922+
Reusable workspace for allocation-free calls to `ldiv!(x, L, b, ws)` with a
1923+
CHOLMOD [`Factor`](@ref).
1924+
1925+
`cholmod_solve2` allocates two temporary dense matrices (Y and E) on every solve.
1926+
A `CholmodWorkspace` holds those buffers across calls so that CHOLMOD reuses them
1927+
instead of reallocating. It also pre-allocates the `cholmod_dense_struct` wrappers
1928+
for `x` and `b`, eliminating all Julia-side heap allocations per solve.
1929+
1930+
The workspace is finalizer-protected: the CHOLMOD-managed Y and E buffers are freed
1931+
automatically when the workspace is garbage-collected. A single workspace can be
1932+
reused across calls with different arrays or a different number of RHS columns;
1933+
CHOLMOD will resize Y and E as needed.
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+
mutable struct CholmodWorkspace
1945+
dense_x::cholmod_dense_struct
1946+
dense_b::cholmod_dense_struct
1947+
X::Ref{Ptr{cholmod_dense_struct}}
1948+
Y::Ref{Ptr{cholmod_dense_struct}}
1949+
E::Ref{Ptr{cholmod_dense_struct}}
1950+
1951+
function CholmodWorkspace()
1952+
ws = new(
1953+
cholmod_dense_struct(), # all fields written in ldiv! before use
1954+
cholmod_dense_struct(),
1955+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
1956+
Ref(Ptr{cholmod_dense_struct}(C_NULL)),
1957+
Ref(Ptr{cholmod_dense_struct}(C_NULL))
1958+
)
1959+
finalizer(ws) do w
1960+
w.Y[] != C_NULL && free!(w.Y[])
1961+
w.E[] != C_NULL && free!(w.E[])
1962+
end
1963+
ws
1964+
end
1965+
end
1966+
1967+
# in-place ldiv! and optional allocation-free ldiv!
19191968
for TI in IndexTypes
19201969
@eval function ldiv!(x::StridedVecOrMat{T},
19211970
L::Factor{T, $TI},
@@ -1973,6 +2022,69 @@ for TI in IndexTypes
19732022

19742023
return x
19752024
end
2025+
2026+
@eval function ldiv!(x::StridedVecOrMat{T},
2027+
L::Factor{T, $TI},
2028+
b::StridedVecOrMat{T},
2029+
ws::CholmodWorkspace) where {T<:VTypes}
2030+
if x === b
2031+
throw(ArgumentError("output array must not be aliased with input array"))
2032+
end
2033+
if size(L, 1) != size(b, 1)
2034+
throw(DimensionMismatch("Factorization and RHS should have the same number of rows. " *
2035+
"Factorization has $(size(L, 2)) rows, but RHS has $(size(b, 1)) rows."))
2036+
end
2037+
if size(L, 2) != size(x, 1)
2038+
throw(DimensionMismatch("Factorization and solution should match sizes. " *
2039+
"Factorization has $(size(L, 1)) columns, but solution has $(size(x, 1)) rows."))
2040+
end
2041+
if size(x, 2) != size(b, 2)
2042+
throw(DimensionMismatch("Solution and RHS should have the same number of columns. " *
2043+
"Solution has $(size(x, 2)) columns, but RHS has $(size(b, 2)) columns."))
2044+
end
2045+
if !issuccess(L)
2046+
s = unsafe_load(pointer(L))
2047+
if s.is_ll == 1
2048+
throw(LinearAlgebra.PosDefException(s.minor))
2049+
else
2050+
throw(LinearAlgebra.ZeroPivotException(s.minor))
2051+
end
2052+
end
2053+
2054+
# Update all fields of the reused cholmod_dense_structs on every call,
2055+
# since T, dimensions, and data pointers may all change between calls.
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+
2086+
return x
2087+
end
19762088
end
19772089

19782090
## Other convenience methods

test/cholmod.jl

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,48 @@ end
315315
@test_throws DimensionMismatch ldiv!(x2, factor, B)
316316
end
317317

318+
@testset "ldiv! with CholmodWorkspace $Tv $Ti" begin
319+
local A, x, x2, b, X, X2, B
320+
A = sprand(10, 10, 0.1)
321+
A = I + A * A'
322+
A = convert(SparseMatrixCSC{Tv,Ti}, A)
323+
factor = cholesky(A)
324+
ws = CHOLMOD.CholmodWorkspace()
325+
326+
x = fill(Tv(1), 10)
327+
b = A * x
328+
x2 = zero(x)
329+
ldiv!(x2, factor, b, ws)
330+
@test x2 x
331+
332+
# reuse workspace
333+
X = fill(Tv(1), 10, 5)
334+
B = A * X
335+
X2 = zero(X)
336+
ldiv!(X2, factor, B, ws)
337+
@test X2 X
338+
339+
# workspace is reusable across calls
340+
fill!(x2, 0)
341+
ldiv!(x2, factor, b, ws)
342+
@test x2 x
343+
344+
# allocation reduction
345+
allocs = @allocated ldiv!(x2, factor, b, ws)
346+
@test allocs <= 16
347+
348+
c = fill(Tv(1), size(x, 1) + 1)
349+
C = fill(Tv(1), size(X, 1) + 1, size(X, 2))
350+
y = fill(Tv(1), size(x, 1) + 1)
351+
Y = fill(Tv(1), size(X, 1) + 1, size(X, 2))
352+
@test_throws DimensionMismatch ldiv!(y, factor, b, ws)
353+
@test_throws DimensionMismatch ldiv!(Y, factor, B, ws)
354+
@test_throws DimensionMismatch ldiv!(x2, factor, c, ws)
355+
@test_throws DimensionMismatch ldiv!(X2, factor, C, ws)
356+
@test_throws DimensionMismatch ldiv!(X2, factor, b, ws)
357+
@test_throws DimensionMismatch ldiv!(x2, factor, B, ws)
358+
end
359+
318360
end #end for Ti ∈ itypes
319361

320362
for Tv (Float32, Float64)

0 commit comments

Comments
 (0)