Skip to content

Add copy and deepcopy for KLUFactorization - #45

Merged
ViralBShah merged 2 commits into
mainfrom
copy-klufactorization
Oct 30, 2026
Merged

ViralBShah merged 2 commits into
mainfrom
copy-klufactorization

Conversation

@ViralBShah

@ViralBShah ViralBShah commented Oct 30, 2026 •

Copy link
Copy Markdown
Member

Fixes #12.

The bug

KLUFactorization holds raw pointers to its libklu objects and had no deepcopy method, so the default copied the addresses. A copy therefore shared its factors and scratch space with the source, and it had no finalizer of its own. copy was a MethodError.

Results at main, each scenario in its own process:

Scenario Before After
One deepcopy per task, each calling klu! (the case in the issue) SingularException on 3 of 4 tasks residuals about 3e-16
Source garbage collected, copy still used silent wrong answer, residual 5.2 residual 3e-16
klu_factor!(copy), then a solve on the source segfault in klu_solve residual 3e-16
GC of both after klu_factor!(copy) abort at exit (double free) clean

The "After" column was measured on an earlier revision that also tried to recover the factored values when nonzeros(K) had been modified. That logic was removed before opening this PR. The final code was checked with the test suite, which covers the same independence, freeing and concurrent klu! cases, rather than by repeating these runs.

Behaviour

copy and deepcopy both return a fully independent object with its own arrays, common, lock and libklu objects. libklu has no function for duplicating them, so an analyzed or factored source is analyzed and factored again, at the cost of a new factorization.

  • State: unanalyzed, symbolic-only and factored sources stay in that state. A non-OK status on the source carries over.
  • Settings: common is copied as it stands at the time of the copy. Permutations from klu_analyze!(K, P, Q) are reused.
  • Modified values: if nonzeros(K) changed after K was factored, the copy factors the current values, so it differs from its source until the source is refactored.
  • After klu!: the copy chooses its own pivots, so its factors can differ from the source's, but both solve the same system.
  • Locking: the source stays locked during the copy, so solves on it wait.
  • copy is deep too: klu!(K, S) temporarily shifts colptr and rowval in place, so sharing them would be a race.

Testing

  • The new tests cover real and complex values, Int32 and Int64 indices, each source state, freeing and refactoring either side, and concurrent use of copies.
  • The full suite passes locally on macOS with Julia 1.13 at -t 4. Without the fix, the new tests fail and the run crashes with a segfault in klu_solve.
  • The OrdinaryDiffEq/LinearSolve script from the issue was not run. The concurrent klu! scenario above reproduces the same error directly.

🤖 Generated with Claude Code

@codecov

codecov Bot commented Oct 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.86%. Comparing base (63298d8) to head (50ce661).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #45      +/-   ##
==========================================
+ Coverage   59.02%   61.86%   +2.84%     
==========================================
  Files           2        2              
  Lines         593      611      +18     
==========================================
+ Hits          350      378      +28     
+ Misses        243      233      -10     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

The default deepcopy copied the raw pointers to the libklu objects, so a
copy shared its factors and scratch space with its source and had no
finalizer. Refactoring one changed the other, and freeing one left the
other with dangling pointers, leading to wrong answers, segfaults, and
double frees.

libklu can't duplicate its objects, so a copy is analyzed and factored
again from the current values and settings, and stays in the same state
as its source.

Fixes #12

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah
ViralBShah force-pushed the copy-klufactorization branch from 0fdca0e to 7a0b485 Compare October 30, 2026 19:59
The copied common carried over memusage and mempeak from the source, so a copy reported twice
the memory it used and never returned to 0 after freeing.

Fold the 266 lines of tests into the existing testsets, keeping one assertion per behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ViralBShah

Copy link
Copy Markdown
Member Author

Review fixes in 50ce661. A copy carried over memusage and mempeak from its source, so it reported twice the memory it used and never returned to 0 after freeing. Both are now reset. The 266 lines of tests are folded into the existing testsets, keeping one assertion per behaviour. They still fail against main. Tests pass on Linux with Julia 1.13 and 1.10.

@ViralBShah
ViralBShah merged commit 6461be3 into main Oct 30, 2026
8 checks passed
@ViralBShah
ViralBShah deleted the copy-klufactorization branch October 30, 2026 20:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Deepcopying KLUFactorization is different for reuse_symbolic=true vs reuse_symbolic=false

1 participant